Mp 2546 borrow asset modal (#255)

* Refactor Modal folder and setup basic addassetsmodal

* basic tables

* Update basic logic

* small fixes

* Update closing for modals

* fix slider update bug and set borrowing subtitle

* fix store

* add missing dependency

* fix tests for VaultBorrowings

* Add DisplayCurrency test for VaultBorrowings

* trigger updated

* update borrowModal import path

* update imports for modals

* updating paths again

* update structure of modals directory

* fix all file naming and relative imports

* fix icon spacing button and jest.mocked import

* fix icon classes for button

* change Map to array and add BNCoin

* add AssetImage

* update logic for selecting borrow denoms
This commit is contained in:
Bob van der Helm 2023-06-15 13:00:46 +02:00 committed by GitHub
parent 0f8e656651
commit 7b5d4c3255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 845 additions and 199 deletions

View File

@ -1,25 +1,36 @@
import { render } from '@testing-library/react' import { render } from '@testing-library/react'
import VaultBorrowings from 'components/Modals/vault/VaultBorrowings' import { ASSETS } from 'constants/assets'
import BigNumber from 'bignumber.js' import { BN } from 'utils/helpers'
import useStore from 'store'
import DisplayCurrency from 'components/DisplayCurrency'
import VaultBorrowings, { VaultBorrowingsProps } from 'components/Modals/Vault/VaultBorrowings'
jest.mock('hooks/usePrices', () => jest.mock('hooks/usePrices', () =>
jest.fn(() => ({ jest.fn(() => ({
data: [], data: [],
})), })),
) )
jest.mock('hooks/usePrice', () => jest.fn(() => '1'))
jest.mock('hooks/useMarketAssets', () => jest.mock('hooks/useMarketAssets', () =>
jest.fn(() => ({ jest.fn(() => ({
data: [], data: [],
})), })),
) )
jest.mock('components/DisplayCurrency')
const mockedDisplayCurrency = jest
.mocked(DisplayCurrency)
.mockImplementation(() => <div>Display currency</div>)
describe('<VaultBorrowings />', () => { describe('<VaultBorrowings />', () => {
const defaultProps: { const defaultProps: VaultBorrowingsProps = {
account: Account primaryAsset: ASSETS[0],
defaultBorrowDenom: string secondaryAsset: ASSETS[1],
onChangeBorrowings: (borrowings: Map<string, BigNumber>) => void primaryAmount: BN(0),
} = { secondaryAmount: BN(0),
account: { account: {
id: 'test', id: 'test',
deposits: [], deposits: [],
@ -27,12 +38,32 @@ describe('<VaultBorrowings />', () => {
vaults: [], vaults: [],
lends: [], lends: [],
}, },
defaultBorrowDenom: 'test-denom', borrowings: [],
onChangeBorrowings: jest.fn(), onChangeBorrowings: jest.fn(),
} }
beforeAll(() => {
useStore.setState({
baseCurrency: ASSETS[0],
selectedBorrowDenoms: [ASSETS[1].denom],
})
})
afterAll(() => {
useStore.clearState()
mockedDisplayCurrency.mockClear()
})
it('should render', () => { it('should render', () => {
const { container } = render(<VaultBorrowings {...defaultProps} />) const { container } = render(<VaultBorrowings {...defaultProps} />)
expect(container).toBeInTheDocument() expect(container).toBeInTheDocument()
}) })
it('should render DisplayCurrency correctly', () => {
expect(mockedDisplayCurrency).toHaveBeenCalledTimes(1)
expect(mockedDisplayCurrency).toHaveBeenCalledWith(
{ coin: { denom: 'uosmo', amount: '0' } },
expect.anything(),
)
})
}) })

View File

@ -2,17 +2,21 @@ import { BN } from 'utils/helpers'
import getPrices from 'api/prices/getPrices' import getPrices from 'api/prices/getPrices'
import getMarkets from 'api/markets/getMarkets' import getMarkets from 'api/markets/getMarkets'
import getMarketLiquidity from 'api/markets/getMarketLiquidity' import getMarketLiquidity from 'api/markets/getMarketLiquidity'
import { getEnabledMarketAssets } from 'utils/assets'
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> { export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
const liquidity = await getMarketLiquidity() const liquidity = await getMarketLiquidity()
const enabledAssets = getEnabledMarketAssets()
const borrowEnabledMarkets = (await getMarkets()).filter((market: Market) => market.borrowEnabled) const borrowEnabledMarkets = (await getMarkets()).filter((market: Market) => market.borrowEnabled)
const prices = await getPrices() const prices = await getPrices()
const borrow: BorrowAsset[] = borrowEnabledMarkets.map((market) => { const borrow: BorrowAsset[] = borrowEnabledMarkets.map((market) => {
const price = prices.find((coin) => coin.denom === market.denom)?.amount ?? '1' const price = prices.find((coin) => coin.denom === market.denom)?.amount ?? '1'
const amount = liquidity.find((coin) => coin.denom === market.denom)?.amount ?? '0' const amount = liquidity.find((coin) => coin.denom === market.denom)?.amount ?? '0'
const asset = enabledAssets.find((asset) => asset.denom === market.denom)!
return { return {
denom: market.denom, ...asset,
borrowRate: market.borrowRate ?? 0, borrowRate: market.borrowRate ?? 0,
liquidity: { liquidity: {
amount: amount, amount: amount,

View File

@ -1,10 +1,12 @@
import Card from 'components/Card' import classNames from 'classnames'
import AccordionContent, { Item } from './AccordionContent' import Card from 'components/Card'
import AccordionContent, { Item } from 'components/AccordionContent'
interface Props { interface Props {
items: Item[] items: Item[]
allowMultipleOpen?: boolean allowMultipleOpen?: boolean
className?: string
} }
export default function Accordion(props: Props) { export default function Accordion(props: Props) {
@ -19,7 +21,7 @@ export default function Accordion(props: Props) {
} }
return ( return (
<div className='w-full'> <div className={classNames('w-full', props.className)}>
{props.items.map((item, index) => ( {props.items.map((item, index) => (
<Card key={item.title} className='mb-4'> <Card key={item.title} className='mb-4'>
<AccordionContent item={item} index={index} /> <AccordionContent item={item} index={index} />

View File

@ -25,8 +25,8 @@ export default function AccountSummary(props: Props) {
if (!props.account) return null if (!props.account) return null
return ( return (
<div className='flex min-w-[345px] basis-[345px] flex-wrap'> <div className='h-[546px] min-w-[345px] basis-[345px] overflow-y-scroll scrollbar-hide'>
<Card className='mb-4 min-w-fit bg-white/10' contentClassName='flex'> <Card className='mb-4 h-min min-w-fit bg-white/10' contentClassName='flex'>
<Item> <Item>
<DisplayCurrency <DisplayCurrency
coin={{ amount: accountBalance.toString(), denom: baseCurrency.denom }} coin={{ amount: accountBalance.toString(), denom: baseCurrency.denom }}

View File

@ -0,0 +1,19 @@
import Image from 'next/image'
interface Props {
asset: Asset
size: number
className?: string
}
export default function AssetImage(props: Props) {
return (
<Image
src={props.asset.logo}
alt={`${props.asset.symbol} logo`}
width={props.size}
height={props.size}
className={props.className}
/>
)
}

View File

@ -19,6 +19,7 @@ import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell' import TitleAndSubCell from 'components/TitleAndSubCell'
import { getEnabledMarketAssets } from 'utils/assets' import { getEnabledMarketAssets } from 'utils/assets'
import { formatPercent } from 'utils/formatters' import { formatPercent } from 'utils/formatters'
import AssetImage from 'components/AssetImage'
type Props = { type Props = {
data: BorrowAsset[] | BorrowAssetActive[] data: BorrowAsset[] | BorrowAssetActive[]
@ -40,7 +41,7 @@ export const BorrowTable = (props: Props) => {
return ( return (
<div className='flex flex-1 items-center gap-3'> <div className='flex flex-1 items-center gap-3'>
<Image src={asset.logo} alt='token' width={32} height={32} /> <AssetImage asset={asset} size={32} />
<TitleAndSubCell <TitleAndSubCell
title={asset.symbol} title={asset.symbol}
sub={asset.name} sub={asset.name}

View File

@ -93,10 +93,10 @@ const Button = React.forwardRef(function Button(
const [leftIconClassNames, rightIconClassNames] = useMemo(() => { const [leftIconClassNames, rightIconClassNames] = useMemo(() => {
const hasContent = !!(text || children) const hasContent = !!(text || children)
const iconClasses = ['flex items-center justify-center', iconClassName ?? 'h-4 w-4'] const iconClasses = ['flex items-center justify-center', iconClassName ?? 'h-4 w-4']
const leftIconClasses = [iconClasses, hasContent && 'mr-2'] const leftIconClasses = [...iconClasses, hasContent && 'mr-2']
const rightIconClasses = [iconClasses, hasContent && 'ml-2'] const rightIconClasses = [...iconClasses, hasContent && 'ml-2']
return [leftIconClasses, rightIconClasses].map(classNames) return [leftIconClasses, rightIconClasses]
}, [children, iconClassName, text]) }, [children, iconClassName, text])
return ( return (
@ -111,10 +111,10 @@ const Button = React.forwardRef(function Button(
<CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} /> <CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} />
) : ( ) : (
<> <>
{leftIcon && <span className={leftIconClassNames}>{leftIcon}</span>} {leftIcon && <span className={classNames(leftIconClassNames)}>{leftIcon}</span>}
{shouldShowText && <span>{text}</span>} {shouldShowText && <span>{text}</span>}
{children && children} {children && children}
{rightIcon && <span className={rightIconClassNames}>{rightIcon}</span>} {rightIcon && <span className={classNames(rightIconClassNames)}>{rightIcon}</span>}
{hasSubmenu && ( {hasSubmenu && (
<span data-testid='button-submenu-indicator' className='ml-2 inline-block w-2.5'> <span data-testid='button-submenu-indicator' className='ml-2 inline-block w-2.5'>
<ChevronDown /> <ChevronDown />

View File

@ -0,0 +1,27 @@
import classNames from 'classnames'
import { Check } from 'components/Icons'
interface Props {
checked: boolean
onChange: (checked: boolean) => void
}
export default function Checkbox(props: Props) {
return (
<button
onClick={() => props.onChange(props.checked)}
role='checkbox'
aria-checked={props.checked}
className={classNames(
'h-5 w-5 rounded-sm p-0.5',
props.checked && 'relative isolate overflow-hidden rounded-sm',
props.checked &&
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-sm before:p-[1px] before:border-glas',
props.checked ? 'bg-white/20' : 'border border-white/60',
)}
>
{props.checked && <Check />}
</button>
)
}

View File

@ -19,7 +19,12 @@ export default function VaultCard(props: Props) {
const currentAccount = useCurrentAccount() const currentAccount = useCurrentAccount()
function openVaultModal() { function openVaultModal() {
useStore.setState({ vaultModal: { vault: props.vault } }) useStore.setState({
vaultModal: {
vault: props.vault,
selectedBorrowDenoms: [props.vault.denoms.secondary],
},
})
} }
return ( return (

View File

@ -11,7 +11,12 @@ interface Props {
export default function VaultExpanded(props: Props) { export default function VaultExpanded(props: Props) {
function enterVaultHandler() { function enterVaultHandler() {
useStore.setState({ vaultModal: { vault: props.row.original } }) useStore.setState({
vaultModal: {
vault: props.row.original,
selectedBorrowDenoms: [props.row.original.denoms.secondary],
},
})
} }
return ( return (

View File

@ -1,5 +1,6 @@
import Image from 'next/image' import Image from 'next/image'
import AssetImage from 'components/AssetImage'
import { getAssetByDenom } from 'utils/assets' import { getAssetByDenom } from 'utils/assets'
interface Props { interface Props {
@ -15,10 +16,10 @@ export default function VaultLogo(props: Props) {
return ( return (
<div className='relative grid w-12 place-items-center'> <div className='relative grid w-12 place-items-center'>
<div className='absolute'> <div className='absolute'>
<Image src={primaryAsset.logo} alt={`${primaryAsset.symbol} logo`} width={24} height={24} /> <AssetImage asset={primaryAsset} size={24} />
</div> </div>
<div className='absolute'> <div className='absolute'>
<Image className='ml-5 mt-5' src={secondaryAsset.logo} alt='token' width={16} height={16} /> <AssetImage asset={primaryAsset} size={16} className='ml-5 mt-5' />
</div> </div>
</div> </div>
) )

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 13L10.1 10.1M11.6667 6.33333C11.6667 9.27885 9.27885 11.6667 6.33333 11.6667C3.38781 11.6667 1 9.27885 1 6.33333C1 3.38781 3.38781 1 6.33333 1C9.27885 1 11.6667 3.38781 11.6667 6.33333Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@ -26,6 +26,7 @@ export { default as OverlayMark } from 'components/Icons/OverlayMark.svg'
export { default as Plus } from 'components/Icons/Plus.svg' export { default as Plus } from 'components/Icons/Plus.svg'
export { default as PlusCircled } from 'components/Icons/PlusCircled.svg' export { default as PlusCircled } from 'components/Icons/PlusCircled.svg'
export { default as Questionmark } from 'components/Icons/Questionmark.svg' export { default as Questionmark } from 'components/Icons/Questionmark.svg'
export { default as Search } from 'components/Icons/Search.svg'
export { default as Shield } from 'components/Icons/Shield.svg' export { default as Shield } from 'components/Icons/Shield.svg'
export { default as SortAsc } from 'components/Icons/SortAsc.svg' export { default as SortAsc } from 'components/Icons/SortAsc.svg'
export { default as SortDesc } from 'components/Icons/SortDesc.svg' export { default as SortDesc } from 'components/Icons/SortDesc.svg'

View File

@ -4,6 +4,7 @@ import { ReactNode, useEffect, useRef } from 'react'
import Button from 'components/Button' import Button from 'components/Button'
import Card from 'components/Card' import Card from 'components/Card'
import { Cross } from 'components/Icons' import { Cross } from 'components/Icons'
import Text from 'components/Text'
interface Props { interface Props {
header: string | ReactNode header: string | ReactNode
@ -12,6 +13,7 @@ interface Props {
content?: ReactNode | string content?: ReactNode | string
className?: string className?: string
contentClassName?: string contentClassName?: string
modalClassName?: string
open: boolean open: boolean
onClose: () => void onClose: () => void
} }
@ -47,6 +49,7 @@ export default function Modal(props: Props) {
'w-[895px] border-none bg-transparent text-white', 'w-[895px] border-none bg-transparent text-white',
'focus-visible:outline-none', 'focus-visible:outline-none',
'backdrop:bg-black/50 backdrop:backdrop-blur-sm', 'backdrop:bg-black/50 backdrop:backdrop-blur-sm',
props.modalClassName,
)} )}
> >
<Card <Card
@ -57,13 +60,9 @@ export default function Modal(props: Props) {
> >
<div className={classNames('flex justify-between', props.headerClassName)}> <div className={classNames('flex justify-between', props.headerClassName)}>
{props.header} {props.header}
<Button <Button onClick={onClose} leftIcon={<Cross />} iconClassName='h-3 w-3' color='tertiary'>
onClick={onClose} <Text size='sm'>ESC</Text>
leftIcon={<Cross />} </Button>
className='h-8 w-8'
iconClassName='h-2 w-2'
color='tertiary'
/>
</div> </div>
<div className={classNames(props.contentClassName, 'flex-grow')}> <div className={classNames(props.contentClassName, 'flex-grow')}>
{props.children ? props.children : props.content} {props.children ? props.children : props.content}

View File

@ -0,0 +1,129 @@
import {
flexRender,
getCoreRowModel,
getSortedRowModel,
RowSelectionState,
SortingState,
useReactTable,
} from '@tanstack/react-table'
import { useEffect, useState } from 'react'
import classNames from 'classnames'
import { SortAsc, SortDesc, SortNone } from 'components/Icons'
import Text from 'components/Text'
import useStore from 'store'
import useAddVaultAssetTableColumns from 'components/Modals/AddVaultAssets/useAddVaultAssetTableColumns'
interface Props {
assets: BorrowAsset[]
onChangeSelected: (denoms: string[]) => void
}
export default function AddVaultAssetTable(props: Props) {
const selectedDenoms = useStore((s) => s.addVaultBorrowingsModal?.selectedDenoms) || []
const defaultSelected = props.assets.reduce((acc, asset, index) => {
if (selectedDenoms.includes(asset.denom)) {
acc[index] = true
}
return acc
}, {} as { [key: number]: boolean })
const [sorting, setSorting] = useState<SortingState>([{ id: 'symbol', desc: false }])
const [selected, setSelected] = useState<RowSelectionState>(defaultSelected)
const columns = useAddVaultAssetTableColumns()
const table = useReactTable({
data: props.assets,
columns,
state: {
sorting,
rowSelection: selected,
},
onRowSelectionChange: setSelected,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
useEffect(() => {
const selectedDenoms = props.assets
.filter((_, index) => selected[index])
.map((asset) => asset.denom)
props.onChangeSelected(selectedDenoms)
}, [selected, props])
return (
<table className='w-full'>
<thead className='border-b border-b-white/5'>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
return (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className={classNames(
'p-2',
header.column.getCanSort() && 'cursor-pointer',
header.id === 'symbol' ? 'text-left' : 'text-right',
)}
>
<div
className={classNames(
'flex',
header.id === 'symbol' ? 'justify-start' : 'justify-end',
'align-center',
)}
>
<span className='h-6 w-6 text-white'>
{header.column.getCanSort()
? {
asc: <SortAsc />,
desc: <SortDesc />,
false: <SortNone />,
}[header.column.getIsSorted() as string] ?? null
: null}
</span>
<Text
tag='span'
size='sm'
className='flex items-center font-normal text-white/40'
>
{flexRender(header.column.columnDef.header, header.getContext())}
</Text>
</div>
</th>
)
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
return (
<tr
key={row.id}
className='cursor-pointer text-white/60'
onClick={() => row.toggleSelected()}
>
{row.getVisibleCells().map((cell) => {
return (
<td
key={cell.id}
className={classNames(
cell.column.id === 'select' ? `` : 'pl-4 text-right',
'px-4 py-3',
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
}

View File

@ -0,0 +1,46 @@
import { useCallback, useState } from 'react'
import Modal from 'components/Modal'
import useStore from 'store'
import Text from 'components/Text'
import { CircularProgress } from 'components/CircularProgress'
import AddVaultAssetsModalContent from 'components/Modals/AddVaultAssets/AddVaultBorrowAssetsModalContent'
export default function AddVaultBorrowAssetsModal() {
const modal = useStore((s) => s.addVaultBorrowingsModal)
const vaultModal = useStore((s) => s.vaultModal)
const [selectedDenoms, setSelectedDenoms] = useState<string[]>([])
function onClose() {
if (!vaultModal) return
useStore.setState({
addVaultBorrowingsModal: null,
vaultModal: { ...vaultModal, selectedBorrowDenoms: selectedDenoms },
})
}
const updateSelectedDenoms = useCallback((denoms: string[]) => setSelectedDenoms(denoms), [])
const showContent = modal && vaultModal?.vault
return (
<Modal
open={!!(modal && showContent)}
header={<Text>Add Assets</Text>}
onClose={onClose}
modalClassName='max-w-[478px]'
headerClassName='bg-white/10 border-b-white/5 border-b items-center p-4'
>
{showContent ? (
<AddVaultAssetsModalContent
vault={vaultModal?.vault}
defaultSelectedDenoms={modal.selectedDenoms}
onChangeBorrowDenoms={updateSelectedDenoms}
/>
) : (
<CircularProgress />
)}
</Modal>
)
}

View File

@ -0,0 +1,96 @@
import { useCallback, useMemo, useState } from 'react'
import SearchBar from 'components/SearchBar'
import Text from 'components/Text'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
import AddVaultAssetTable from 'components/Modals/AddVaultAssets/AddVaultAssetTable'
interface Props {
vault: Vault
defaultSelectedDenoms: string[]
onChangeBorrowDenoms: (denoms: string[]) => void
}
export default function AddVaultAssetsModalContent(props: Props) {
const [searchString, setSearchString] = useState<string>('')
const { data: borrowAssets } = useMarketBorrowings()
const [selectedPoolDenoms, setSelectedPoolDenoms] = useState<string[]>([])
const [selectedOtherDenoms, setSelectedOtherDenoms] = useState<string[]>([])
const filteredBorrowAssets: BorrowAsset[] = useMemo(() => {
return borrowAssets.filter(
(asset) =>
asset.name.toLowerCase().includes(searchString.toLowerCase()) ||
asset.denom.toLowerCase().includes(searchString.toLowerCase()) ||
asset.symbol.toLowerCase().includes(searchString.toLowerCase()),
)
}, [borrowAssets, searchString])
function onChangeSearchString(value: string) {
setSearchString(value)
}
const [poolAssets, stableAssets] = useMemo(
() =>
filteredBorrowAssets.reduce(
(acc, asset) => {
if (
asset.denom === props.vault.denoms.primary ||
asset.denom === props.vault.denoms.secondary
) {
acc[0].push(asset)
} else if (asset.isStable) {
acc[1].push(asset)
}
return acc
},
[[], []] as [BorrowAsset[], BorrowAsset[]],
),
[filteredBorrowAssets, props.vault.denoms.primary, props.vault.denoms.secondary],
)
const onChangePoolDenoms = useCallback(
(denoms: string[]) => {
setSelectedPoolDenoms(denoms)
props.onChangeBorrowDenoms([...denoms, ...selectedOtherDenoms])
},
[props, selectedOtherDenoms],
)
const onChangeOtherDenoms = useCallback(
(denoms: string[]) => {
setSelectedOtherDenoms(denoms)
props.onChangeBorrowDenoms([...selectedPoolDenoms, ...denoms])
},
[props, selectedPoolDenoms],
)
return (
<>
<div className='border-b border-b-white/5 bg-white/10 px-4 py-3'>
<SearchBar
value={searchString}
placeholder={`Search for e.g. "ETH" or "Ethereum"`}
onChange={onChangeSearchString}
/>
</div>
<div className='h-[446px] overflow-y-scroll scrollbar-hide'>
<div className='p-4'>
<Text>Available Assets</Text>
<Text size='xs' className='mt-1 text-white/60'>
Leverage will be set at 50% for both assets by default
</Text>
</div>
<AddVaultAssetTable assets={poolAssets} onChangeSelected={onChangePoolDenoms} />
<div className='p-4'>
<Text>Assets not in the liquidity pool</Text>
<Text size='xs' className='mt-1 text-white/60'>
These are swapped for an asset within the pool. Toggle Custom Ratio in order to select
these assets below.
</Text>
</div>
<AddVaultAssetTable assets={stableAssets} onChangeSelected={onChangeOtherDenoms} />
</div>
</>
)
}

View File

@ -0,0 +1,54 @@
import { ColumnDef } from '@tanstack/react-table'
import React from 'react'
import Image from 'next/image'
import Checkbox from 'components/Checkbox'
import Text from 'components/Text'
import { formatPercent } from 'utils/formatters'
import { getAssetByDenom } from 'utils/assets'
import AssetImage from 'components/AssetImage'
export default function useAddVaultAssetTableColumns() {
const columns = React.useMemo<ColumnDef<BorrowAsset>[]>(
() => [
{
header: 'Asset',
accessorKey: 'symbol',
id: 'symbol',
cell: ({ row }) => {
const asset = getAssetByDenom(row.original.denom)
if (!asset) return null
return (
<div className='flex items-center'>
<Checkbox checked={row.getIsSelected()} onChange={row.getToggleSelectedHandler()} />
<AssetImage asset={asset} size={24} className='ml-4' />
<div className='ml-2 text-left'>
<Text size='sm' className='mb-0.5 text-white'>
{asset.symbol}
</Text>
<Text size='xs'>{asset.name}</Text>
</div>
</div>
)
},
},
{
id: 'borrowRate',
accessorKey: 'borrowRate',
header: 'Borrow Rate',
cell: ({ row }) => (
<>
<Text size='sm' className='mb-0.5 text-white'>
{formatPercent(row.original.borrowRate ?? 0)}
</Text>
<Text size='xs'>APY</Text>
</>
),
},
],
[],
)
return columns
}

View File

@ -1,4 +1,3 @@
import Image from 'next/image'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import AccountSummary from 'components/Account/AccountSummary' import AccountSummary from 'components/Account/AccountSummary'
@ -18,6 +17,7 @@ import useStore from 'store'
import { hardcodedFee } from 'utils/contants' import { hardcodedFee } from 'utils/contants'
import { formatPercent, formatValue } from 'utils/formatters' import { formatPercent, formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import AssetImage from 'components/AssetImage'
function getDebtAmount(modal: BorrowModal | null) { function getDebtAmount(modal: BorrowModal | null) {
if (!(modal?.marketData as BorrowAssetActive)?.debt) return '0' if (!(modal?.marketData as BorrowAssetActive)?.debt) return '0'
@ -26,7 +26,7 @@ function getDebtAmount(modal: BorrowModal | null) {
function getAssetLogo(modal: BorrowModal | null) { function getAssetLogo(modal: BorrowModal | null) {
if (!modal?.asset) return null if (!modal?.asset) return null
return <Image src={modal.asset.logo} alt={modal.asset.symbol} width={24} height={24} /> return <AssetImage asset={modal.asset} size={24} />
} }
export default function BorrowModal() { export default function BorrowModal() {

View File

@ -3,8 +3,7 @@ import Text from 'components/Text'
import useCurrentAccount from 'hooks/useCurrentAccount' import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store' import useStore from 'store'
import { CircularProgress } from 'components/CircularProgress' import { CircularProgress } from 'components/CircularProgress'
import FundWithdrawModalContent from 'components/Modals/FundWithdraw/FundAndWithdrawModalContent'
import FundWithdrawModalContent from './FundWithdrawModalContent'
export default function FundAndWithdrawModal() { export default function FundAndWithdrawModal() {
const currentAccount = useCurrentAccount() const currentAccount = useCurrentAccount()

View File

@ -1,6 +1,7 @@
import BorrowModal from 'components/Modals/BorrowModal' import VaultModal from 'components/Modals/Vault/VaultModal'
import FundAndWithdrawModal from 'components/Modals/fundwithdraw/FundAndWithdrawModal' import BorrowModal from 'components/Modals/Borrow/BorrowModal'
import VaultModal from 'components/Modals/vault/VaultModal' import FundAndWithdrawModal from 'components/Modals/FundWithdraw/FundAndWithdrawModal'
import AddVaultBorrowAssetsModal from 'components/Modals/AddVaultAssets/AddVaultBorrowAssetsModal'
export default function ModalsContainer() { export default function ModalsContainer() {
return ( return (
@ -8,6 +9,7 @@ export default function ModalsContainer() {
<BorrowModal /> <BorrowModal />
<FundAndWithdrawModal /> <FundAndWithdrawModal />
<VaultModal /> <VaultModal />
<AddVaultBorrowAssetsModal />
</> </>
) )
} }

View File

@ -0,0 +1,197 @@
import { useEffect, useMemo, useState } from 'react'
import BigNumber from 'bignumber.js'
import React from 'react'
import { BN } from 'utils/helpers'
import { findCoinByDenom, getAssetByDenom } from 'utils/assets'
import Button from 'components/Button'
import TokenInput from 'components/TokenInput'
import Divider from 'components/Divider'
import Text from 'components/Text'
import { ArrowRight, ExclamationMarkCircled } from 'components/Icons'
import { formatPercent } from 'utils/formatters'
import Slider from 'components/Slider'
import usePrices from 'hooks/usePrices'
import useMarketAssets from 'hooks/useMarketAssets'
import { calculateMaxBorrowAmounts } from 'utils/vaults'
import useStore from 'store'
import DisplayCurrency from 'components/DisplayCurrency'
import usePrice from 'hooks/usePrice'
import { BNCoin } from 'types/classes/BNCoin'
export interface VaultBorrowingsProps {
account: Account
borrowings: BNCoin[]
primaryAmount: BigNumber
secondaryAmount: BigNumber
primaryAsset: Asset
secondaryAsset: Asset
onChangeBorrowings: (borrowings: BNCoin[]) => void
}
export default function VaultBorrowings(props: VaultBorrowingsProps) {
const { data: marketAssets } = useMarketAssets()
const { data: prices } = usePrices()
const primaryPrice = usePrice(props.primaryAsset.denom)
const secondaryPrice = usePrice(props.secondaryAsset.denom)
const baseCurrency = useStore((s) => s.baseCurrency)
const vaultModal = useStore((s) => s.vaultModal)
const primaryValue = useMemo(
() => props.primaryAmount.times(primaryPrice),
[props.primaryAmount, primaryPrice],
)
const secondaryValue = useMemo(
() => props.secondaryAmount.times(secondaryPrice),
[props.secondaryAmount, secondaryPrice],
)
const borrowingValue = useMemo(() => {
return props.borrowings.reduce((prev, curr) => {
const price = prices.find((price) => price.denom === curr.denom)?.amount
if (!price) return prev
return prev.plus(curr.amount.times(price))
}, BN(0) as BigNumber)
}, [props.borrowings, prices])
const totalValue = useMemo(
() => primaryValue.plus(secondaryValue).plus(borrowingValue),
[primaryValue, secondaryValue, borrowingValue],
)
useEffect(() => {
const selectedBorrowDenoms = vaultModal?.selectedBorrowDenoms || []
if (
props.borrowings.length === selectedBorrowDenoms.length &&
props.borrowings.every((coin) => selectedBorrowDenoms.includes(coin.denom))
) {
return
}
const updatedBorrowings = selectedBorrowDenoms.map((denom) => {
const amount = findCoinByDenom(denom, props.borrowings)?.amount || BN(0)
return new BNCoin({
denom,
amount: amount.toString(),
})
})
props.onChangeBorrowings(updatedBorrowings)
}, [vaultModal, props])
const maxAmounts: BNCoin[] = useMemo(
() =>
calculateMaxBorrowAmounts(
props.account,
marketAssets,
prices,
props.borrowings.map((coin) => coin.denom),
),
[props.borrowings, marketAssets, prices, props.account],
)
const [percentage, setPercentage] = useState<number>(0)
function onChangeSlider(value: number) {
if (props.borrowings.length !== 1) return
const denom = props.borrowings[0].denom
const currentAmount = props.borrowings[0].amount
const maxAmount = maxAmounts.find((coin) => coin.denom === denom)?.amount ?? BN(0)
const newBorrowings: BNCoin[] = [
new BNCoin({
denom,
amount: (
maxAmount.plus(currentAmount).times(value).div(100).decimalPlaces(0) || BN(0)
).toString(),
}),
]
props.onChangeBorrowings(newBorrowings)
setPercentage(value)
}
function updateAssets(denom: string, amount: BigNumber) {
const index = props.borrowings.findIndex((coin) => coin.denom === denom)
props.borrowings[index].amount = amount
props.onChangeBorrowings([...props.borrowings])
}
function onDelete(denom: string) {
const index = props.borrowings.findIndex((coin) => coin.denom === denom)
props.borrowings.splice(index, 1)
props.onChangeBorrowings([...props.borrowings])
if (!vaultModal) return
useStore.setState({
vaultModal: {
...vaultModal,
selectedBorrowDenoms: props.borrowings.map((coin) => coin.denom),
},
})
}
function addAsset() {
useStore.setState({
addVaultBorrowingsModal: {
selectedDenoms: props.borrowings.map((coin) => coin.denom),
},
})
}
return (
<div className='flex flex-grow flex-col gap-4 p-4'>
{props.borrowings.map((coin) => {
const asset = getAssetByDenom(coin.denom)
const maxAmount = maxAmounts.find((maxAmount) => maxAmount.denom === coin.denom)?.amount
if (!asset || !maxAmount)
return <React.Fragment key={`input-${coin.denom}`}></React.Fragment>
return (
<TokenInput
key={`input-${coin.denom}`}
amount={coin.amount}
asset={asset}
max={maxAmount.plus(coin.amount)}
maxText='Max Borrow'
onChange={(amount) => updateAssets(coin.denom, amount)}
onDelete={() => onDelete(coin.denom)}
/>
)
})}
{props.borrowings.length === 1 && <Slider onChange={onChangeSlider} value={percentage} />}
{props.borrowings.length === 0 && (
<div className='flex items-center gap-4 py-2'>
<div className='w-4'>
<ExclamationMarkCircled width={20} height={20} />
</div>
<Text size='xs'>
You have no borrowing assets selected. Click on select borrow assets if you would like
to add assets to borrow.
</Text>
</div>
)}
<Button text='Select borrow assets +' color='tertiary' onClick={addAsset} />
<Divider />
<div className='flex flex-col gap-2'>
<div className='flex justify-between'>
<Text className='text-white/50'>{`${props.primaryAsset.symbol}-${props.secondaryAsset.symbol} Position Value`}</Text>
<DisplayCurrency coin={{ denom: baseCurrency.denom, amount: totalValue.toString() }} />
</div>
{props.borrowings.map((coin) => {
const asset = getAssetByDenom(coin.denom)
const borrowRate = marketAssets?.find((market) => market.denom === coin.denom)?.borrowRate
if (!asset || !borrowRate)
return <React.Fragment key={`borrow-rate-${coin.denom}`}></React.Fragment>
return (
<div key={`borrow-rate-${coin.denom}`} className='flex justify-between'>
<Text className='text-white/50'>Borrow APR {asset.symbol}</Text>
<Text>{formatPercent(borrowRate)}</Text>
</div>
)
})}
</div>
<Button color='primary' text='Deposit' rightIcon={<ArrowRight />} />
</div>
)
}

View File

@ -0,0 +1,49 @@
import BigNumber from 'bignumber.js'
import { useMemo } from 'react'
import DisplayCurrency from 'components/DisplayCurrency'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { formatAmountWithSymbol } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { BNCoin } from 'types/classes/BNCoin'
interface Props {
borrowings: BNCoin[]
}
export default function VaultDepositSubTitle(props: Props) {
const baseCurrency = useStore((s) => s.baseCurrency)
const { data: prices } = usePrices()
const [borrowingTexts, borrowingValue] = useMemo(() => {
const texts: string[] = []
let borrowingValue = BN(0)
props.borrowings.map((coin) => {
const price = prices.find((p) => p.denom === coin.denom)?.amount
if (!price || coin.amount.isZero()) return
borrowingValue = borrowingValue.plus(coin.amount.times(price))
texts.push(
formatAmountWithSymbol({
denom: coin.denom,
amount: coin.amount.toString(),
}),
)
})
return [texts, borrowingValue]
}, [props.borrowings, prices])
return (
<>
{borrowingTexts.join(' + ')}
{borrowingTexts.length > 0 && (
<>
{` = `}
<DisplayCurrency
coin={{ denom: baseCurrency.denom, amount: borrowingValue.toString() }}
/>
</>
)}
</>
)
}

View File

@ -5,8 +5,7 @@ import { ASSETS } from 'constants/assets'
import useCurrentAccount from 'hooks/useCurrentAccount' import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store' import useStore from 'store'
import { CircularProgress } from 'components/CircularProgress' import { CircularProgress } from 'components/CircularProgress'
import VaultModalContent from 'components/Modals/Vault/VaultModalContent'
import VaultModalContent from './VaultModalContent'
export default function VaultModal() { export default function VaultModal() {
const currentAccount = useCurrentAccount() const currentAccount = useCurrentAccount()

View File

@ -3,12 +3,13 @@ import { useCallback, useState } from 'react'
import Accordion from 'components/Accordion' import Accordion from 'components/Accordion'
import AccountSummary from 'components/Account/AccountSummary' import AccountSummary from 'components/Account/AccountSummary'
import VaultBorrowings from 'components/Modals/vault/VaultBorrowings'
import VaultDeposit from 'components/Modals/vault/VaultDeposit'
import VaultDepositSubTitle from 'components/Modals/vault/VaultDepositSubTitle'
import useIsOpenArray from 'hooks/useIsOpenArray' import useIsOpenArray from 'hooks/useIsOpenArray'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import useUpdateAccount from 'hooks/useUpdateAccount' import useUpdateAccount from 'hooks/useUpdateAccount'
import VaultBorrowingsSubTitle from 'components/Modals/Vault/VaultBorrowingsSubTitle'
import VaultDeposit from 'components/Modals/Vault/VaultDeposits'
import VaultBorrowings from 'components/Modals/Vault/VaultBorrowings'
import VaultDepositSubTitle from 'components/Modals/Vault/VaultDepositsSubTitle'
interface Props { interface Props {
vault: Vault vault: Vault
@ -18,7 +19,10 @@ interface Props {
} }
export default function VaultModalContent(props: Props) { export default function VaultModalContent(props: Props) {
const { updatedAccount, onChangeBorrowings } = useUpdateAccount(props.account) const { updatedAccount, onChangeBorrowings, borrowings } = useUpdateAccount(
props.account,
props.vault,
)
const [isOpen, toggleOpen] = useIsOpenArray(2, false) const [isOpen, toggleOpen] = useIsOpenArray(2, false)
const [primaryAmount, setPrimaryAmount] = useState<BigNumber>(BN(0)) const [primaryAmount, setPrimaryAmount] = useState<BigNumber>(BN(0))
const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0)) const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0))
@ -41,6 +45,7 @@ export default function VaultModalContent(props: Props) {
return ( return (
<div className='flex flex-grow items-start gap-6 p-6'> <div className='flex flex-grow items-start gap-6 p-6'>
<Accordion <Accordion
className='h-[546px] overflow-y-scroll scrollbar-hide'
items={[ items={[
{ {
renderContent: () => ( renderContent: () => (
@ -73,11 +78,16 @@ export default function VaultModalContent(props: Props) {
renderContent: () => ( renderContent: () => (
<VaultBorrowings <VaultBorrowings
account={updatedAccount} account={updatedAccount}
defaultBorrowDenom={props.secondaryAsset.denom} borrowings={borrowings}
primaryAmount={primaryAmount}
secondaryAmount={secondaryAmount}
primaryAsset={props.primaryAsset}
secondaryAsset={props.secondaryAsset}
onChangeBorrowings={onChangeBorrowings} onChangeBorrowings={onChangeBorrowings}
/> />
), ),
title: 'Borrow', title: 'Borrow',
subTitle: <VaultBorrowingsSubTitle borrowings={borrowings} />,
isOpen: isOpen[1], isOpen: isOpen[1],
toggleOpen: (index: number) => toggleOpen(index), toggleOpen: (index: number) => toggleOpen(index),
}, },

View File

@ -1,112 +0,0 @@
import { useMemo, useState } from 'react'
import BigNumber from 'bignumber.js'
import { BN } from 'utils/helpers'
import { getAssetByDenom } from 'utils/assets'
import Button from 'components/Button'
import TokenInput from 'components/TokenInput'
import Divider from 'components/Divider'
import Text from 'components/Text'
import { ArrowRight } from 'components/Icons'
import { formatPercent } from 'utils/formatters'
import Slider from 'components/Slider'
import usePrices from 'hooks/usePrices'
import useMarketAssets from 'hooks/useMarketAssets'
import { calculateMaxBorrowAmounts } from 'utils/vaults'
import React from 'react'
interface Props {
account: Account
defaultBorrowDenom: string
onChangeBorrowings: (borrowings: Map<string, BigNumber>) => void
}
export default function VaultBorrowings(props: Props) {
const { data: prices } = usePrices()
const { data: marketAssets } = useMarketAssets()
const [borrowings, setBorrowings] = useState<Map<string, BigNumber>>(
new Map().set(props.defaultBorrowDenom, BN(0)),
)
const maxAmounts: Map<string, BigNumber> = useMemo(
() =>
calculateMaxBorrowAmounts(props.account, marketAssets, prices, Array.from(borrowings.keys())),
[borrowings, marketAssets, prices, props.account],
)
const [percentage, setPercentage] = useState<number>(0)
function onChangeSlider(value: number) {
if (borrowings.size !== 1) return
const denom = Array.from(borrowings.keys())[0]
const newBorrowings = new Map().set(
denom,
maxAmounts.get(denom)?.times(value).div(100).toPrecision(0) || BN(0),
)
setBorrowings(newBorrowings)
props.onChangeBorrowings(newBorrowings)
setPercentage(value)
}
function updateAssets(denom: string, amount: BigNumber) {
const newborrowings = new Map(borrowings)
newborrowings.set(denom, amount)
setBorrowings(newborrowings)
props.onChangeBorrowings(newborrowings)
}
function onDelete(denom: string) {
const newborrowings = new Map(borrowings)
newborrowings.delete(denom)
setBorrowings(newborrowings)
props.onChangeBorrowings(newborrowings)
}
function addAsset() {
const newborrowings = new Map(borrowings)
// Replace with denom parameter from the modal (MP-2546)
newborrowings.set('', BN(0))
setBorrowings(newborrowings)
props.onChangeBorrowings(newborrowings)
}
return (
<div className='flex flex-grow flex-col gap-4 p-4'>
{Array.from(borrowings.entries()).map(([denom, amount]) => {
const asset = getAssetByDenom(denom)
if (!asset) return <React.Fragment key={`input-${denom}`}></React.Fragment>
return (
<TokenInput
key={`input-${denom}`}
amount={amount}
asset={asset}
max={maxAmounts.get(denom)?.plus(amount) || BN(0)}
maxText='Max Borrow'
onChange={(amount) => updateAssets(denom, amount)}
onDelete={() => onDelete(denom)}
/>
)
})}
{borrowings.size === 1 && <Slider onChange={onChangeSlider} value={percentage} />}
<Button text='Select borrow assets +' color='tertiary' onClick={addAsset} />
<Divider />
{Array.from(borrowings.entries()).map(([denom, amount]) => {
const asset = getAssetByDenom(denom)
const borrowRate = marketAssets?.find((market) => market.denom === denom)?.borrowRate
if (!asset || !borrowRate)
return <React.Fragment key={`borrow-rate-${denom}`}></React.Fragment>
return (
<div key={`borrow-rate-${denom}`} className='flex justify-between'>
<Text className='text-white/50'>Borrow APR {asset.symbol}</Text>
<Text>{formatPercent(borrowRate)}</Text>
</div>
)
})}
<Button color='primary' text='Deposit' rightIcon={<ArrowRight />} />
</div>
)
}

View File

@ -0,0 +1,34 @@
import classNames from 'classnames'
import { ChangeEvent } from 'react'
import { Search } from 'components/Icons'
interface Props {
value: string
placeholder: string
onChange: (value: string) => void
}
export default function SearchBar(props: Props) {
function onChange(event: ChangeEvent<HTMLInputElement>) {
props.onChange(event.target.value)
}
return (
<div
className={classNames(
'flex w-full items-center justify-between rounded-sm bg-white/10 p-2.5',
'relative isolate max-w-full overflow-hidden rounded-base',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-base before:p-[1px] before:border-glas',
)}
>
<Search width={14} height={14} className='mr-2.5 text-white' />
<input
value={props.value}
className='h-full w-full bg-transparent text-xs placeholder-white/30 outline-none'
placeholder={props.placeholder}
onChange={(event) => onChange(event)}
/>
</div>
)
}

View File

@ -6,6 +6,7 @@ import { ChevronDown } from 'components/Icons'
import Text from 'components/Text' import Text from 'components/Text'
import { ASSETS } from 'constants/assets' import { ASSETS } from 'constants/assets'
import { formatValue } from 'utils/formatters' import { formatValue } from 'utils/formatters'
import AssetImage from 'components/AssetImage'
interface Props extends Option { interface Props extends Option {
isSelected?: boolean isSelected?: boolean
@ -18,11 +19,7 @@ export default function Option(props: Props) {
const isCoin = !!props.denom const isCoin = !!props.denom
if (isCoin) { if (isCoin) {
const currentAsset = ASSETS.find((asset) => asset.denom === props.denom) const asset = ASSETS.find((asset) => asset.denom === props.denom) || ASSETS[0]
const symbol = currentAsset?.symbol ?? ASSETS[0].symbol
const logo = currentAsset?.logo ?? ASSETS[0].logo
const denom = currentAsset?.denom ?? ASSETS[0].denom
const decimals = currentAsset?.decimals ?? ASSETS[0].decimals
const balance = props.amount ?? '0' const balance = props.amount ?? '0'
if (props.isDisplay) { if (props.isDisplay) {
@ -30,8 +27,8 @@ export default function Option(props: Props) {
<div <div
className={classNames('flex items-center gap-2 bg-white/10 p-3', 'hover:cursor-pointer')} className={classNames('flex items-center gap-2 bg-white/10 p-3', 'hover:cursor-pointer')}
> >
<Image src={logo} alt={`${symbol} token logo`} width={20} height={20} /> <AssetImage asset={asset} size={20} />
<span>{symbol}</span> <span>{asset.symbol}</span>
<span <span
className={classNames( className={classNames(
'inline-block w-2.5 transition-transform', 'inline-block w-2.5 transition-transform',
@ -53,20 +50,25 @@ export default function Option(props: Props) {
'hover:cursor-pointer hover:bg-white/20', 'hover:cursor-pointer hover:bg-white/20',
!props.isSelected ? 'bg-white/10' : 'pointer-events-none', !props.isSelected ? 'bg-white/10' : 'pointer-events-none',
)} )}
onClick={() => props?.onClick && props.onClick(denom)} onClick={() => props?.onClick && props.onClick(asset.denom)}
> >
<div className='row-span-2 flex h-full items-center justify-center'> <div className='row-span-2 flex h-full items-center justify-center'>
<Image src={logo} alt={`${symbol} token logo`} width={32} height={32} /> <AssetImage asset={asset} size={32} />
</div> </div>
<Text className='col-span-2 pb-1'>{symbol}</Text> <Text className='col-span-2 pb-1'>{asset.symbol}</Text>
<Text size='sm' className='col-span-2 pb-1 text-right font-bold'> <Text size='sm' className='col-span-2 pb-1 text-right font-bold'>
{formatValue(balance, { decimals, maxDecimals: 4, minDecimals: 0, rounded: true })} {formatValue(balance, {
decimals: asset.decimals,
maxDecimals: 4,
minDecimals: 0,
rounded: true,
})}
</Text> </Text>
<Text size='sm' className='col-span-2 text-white/50'> <Text size='sm' className='col-span-2 text-white/50'>
{formatValue(5, { maxDecimals: 2, minDecimals: 0, prefix: 'APY ', suffix: '%' })} {formatValue(5, { maxDecimals: 2, minDecimals: 0, prefix: 'APY ', suffix: '%' })}
</Text> </Text>
<Text size='sm' className='col-span-2 text-right text-white/50'> <Text size='sm' className='col-span-2 text-right text-white/50'>
<DisplayCurrency coin={{ denom, amount: balance }} /> <DisplayCurrency coin={{ denom: asset.denom, amount: balance }} />
</Text> </Text>
</div> </div>
) )

View File

@ -13,6 +13,7 @@ import { FormattedNumber } from 'components/FormattedNumber'
import Button from 'components/Button' import Button from 'components/Button'
import { ExclamationMarkTriangle, TrashBin } from 'components/Icons' import { ExclamationMarkTriangle, TrashBin } from 'components/Icons'
import { Tooltip } from 'components/Tooltip' import { Tooltip } from 'components/Tooltip'
import AssetImage from 'components/AssetImage'
interface Props { interface Props {
amount: BigNumber amount: BigNumber
@ -67,7 +68,7 @@ export default function TokenInput(props: Props) {
/> />
) : ( ) : (
<div className='flex min-w-fit items-center gap-2 border-r border-white/20 bg-white/5 p-3'> <div className='flex min-w-fit items-center gap-2 border-r border-white/20 bg-white/5 p-3'>
<Image src={props.asset.logo} alt='token' width={20} height={20} /> <AssetImage asset={props.asset} size={20} />
<Text>{props.asset.symbol}</Text> <Text>{props.asset.symbol}</Text>
</div> </div>
)} )}

View File

@ -4,8 +4,7 @@ import { ReactNode } from 'react'
import { Questionmark } from 'components/Icons' import { Questionmark } from 'components/Icons'
import useStore from 'store' import useStore from 'store'
import TooltipContent from 'components/Tooltip/TooltipContent'
import TooltipContent from './TooltipContent'
interface Props { interface Props {
content: ReactNode | string content: ReactNode | string

View File

@ -97,6 +97,7 @@ export const ASSETS: Asset[] = [
isEnabled: true, isEnabled: true,
isMarket: true, isMarket: true,
isDisplayCurrency: true, isDisplayCurrency: true,
isStable: true,
}, },
{ {
symbol: 'USDC.n', symbol: 'USDC.n',
@ -112,5 +113,6 @@ export const ASSETS: Asset[] = [
isEnabled: IS_TESTNET, isEnabled: IS_TESTNET,
isMarket: IS_TESTNET, isMarket: IS_TESTNET,
isDisplayCurrency: IS_TESTNET, isDisplayCurrency: IS_TESTNET,
isStable: true,
}, },
] ]

View File

@ -4,6 +4,7 @@ import getMarketBorrowings from 'api/markets/getMarketBorrowings'
export default function useMarketBorrowings() { export default function useMarketBorrowings() {
return useSWR(`marketBorrowings`, getMarketBorrowings, { return useSWR(`marketBorrowings`, getMarketBorrowings, {
suspense: true, fallbackData: [],
suspense: false,
}) })
} }

View File

@ -1,4 +1,4 @@
import usePrices from './usePrices' import usePrices from 'hooks/usePrices'
export default function usePrice(denom: string) { export default function usePrice(denom: string) {
const { data: prices } = usePrices() const { data: prices } = usePrices()

View File

@ -1,10 +1,12 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
export default function useUpdateAccount(account: Account) { export default function useUpdateAccount(account: Account, vault: Vault) {
const [updatedAccount, setUpdatedAccount] = useState<Account>(account) const [updatedAccount, setUpdatedAccount] = useState<Account>(account)
const [borrowings, setBorrowings] = useState<BNCoin[]>([])
function getCoin(denom: string, amount: BigNumber): Coin { function getCoin(denom: string, amount: BigNumber): Coin {
return { return {
@ -14,32 +16,33 @@ export default function useUpdateAccount(account: Account) {
} }
const onChangeBorrowings = useCallback( const onChangeBorrowings = useCallback(
(borrowings: Map<string, BigNumber>) => { (borrowings: BNCoin[]) => {
const debts: Coin[] = [...account.debts] const debts: Coin[] = [...account.debts]
const deposits: Coin[] = [...account.deposits] const deposits: Coin[] = [...account.deposits]
const currentDebtDenoms = debts.map((debt) => debt.denom) const currentDebtDenoms = debts.map((debt) => debt.denom)
const currentDepositDenoms = deposits.map((deposit) => deposit.denom) const currentDepositDenoms = deposits.map((deposit) => deposit.denom)
borrowings.forEach((amount, denom) => { borrowings.map((coin) => {
if (amount.isZero()) return if (coin.amount.isZero()) return
if (currentDebtDenoms.includes(denom)) { if (currentDebtDenoms.includes(coin.denom)) {
const index = currentDebtDenoms.indexOf(denom) const index = currentDebtDenoms.indexOf(coin.denom)
const newAmount = BN(debts[index].amount).plus(amount) const newAmount = BN(debts[index].amount).plus(coin.amount)
debts[index] = getCoin(denom, newAmount) debts[index] = getCoin(coin.denom, newAmount)
} else { } else {
debts.push(getCoin(denom, amount)) debts.push(coin.toCoin())
} }
if (currentDepositDenoms.includes(denom)) { if (currentDepositDenoms.includes(coin.denom)) {
const index = currentDepositDenoms.indexOf(denom) const index = currentDepositDenoms.indexOf(coin.denom)
const newAmount = BN(deposits[index].amount).plus(amount) const newAmount = BN(deposits[index].amount).plus(coin.amount)
deposits[index] = getCoin(denom, newAmount) deposits[index] = getCoin(coin.denom, newAmount)
} else { } else {
deposits.push(getCoin(denom, amount)) deposits.push(coin.toCoin())
} }
}) })
setBorrowings(borrowings)
setUpdatedAccount({ setUpdatedAccount({
...account, ...account,
debts, debts,
@ -49,5 +52,5 @@ export default function useUpdateAccount(account: Account) {
[account], [account],
) )
return { updatedAccount, onChangeBorrowings } return { borrowings, updatedAccount, onChangeBorrowings }
} }

View File

@ -2,6 +2,7 @@ import { GetState, SetState } from 'zustand'
export default function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalSlice>) { export default function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalSlice>) {
return { return {
addVaultBorrowingsModal: null,
borrowModal: null, borrowModal: null,
createAccountModal: false, createAccountModal: false,
deleteAccountModal: false, deleteAccountModal: false,

View File

@ -0,0 +1,18 @@
import { BN } from 'utils/helpers'
export class BNCoin {
public denom: string
public amount: BigNumber
constructor(coin: Coin) {
this.denom = coin.denom
this.amount = BN(coin.amount)
}
toCoin(): Coin {
return {
denom: this.denom,
amount: this.amount.toString(),
}
}
}

View File

@ -13,14 +13,14 @@ interface Asset {
isEnabled: boolean isEnabled: boolean
isMarket: boolean isMarket: boolean
isDisplayCurrency?: boolean isDisplayCurrency?: boolean
isStable?: boolean
} }
interface OtherAsset extends Omit<Asset, 'symbol'> { interface OtherAsset extends Omit<Asset, 'symbol'> {
symbol: 'MARS' symbol: 'MARS'
} }
interface BorrowAsset { interface BorrowAsset extends Asset {
denom: string
borrowRate: number | null borrowRate: number | null
liquidity: { liquidity: {
amount: string amount: string
@ -31,3 +31,8 @@ interface BorrowAsset {
interface BorrowAssetActive extends BorrowAsset { interface BorrowAssetActive extends BorrowAsset {
debt: string debt: string
} }
interface BigNumberCoin {
denom: string
amount: BigNumber
}

View File

@ -1,10 +1,10 @@
interface CommonSlice { interface CommonSlice {
accounts: Account[] | null accounts: Account[] | null
address?: string address?: string
balances: Coin[]
client?: import('@marsprotocol/wallet-connector').WalletClient
enableAnimations: boolean enableAnimations: boolean
isOpen: boolean isOpen: boolean
balances: Coin[]
selectedAccount: string | null selectedAccount: string | null
client?: import('@marsprotocol/wallet-connector').WalletClient
status: import('@marsprotocol/wallet-connector').WalletConnectionStatus status: import('@marsprotocol/wallet-connector').WalletConnectionStatus
} }

View File

@ -1,12 +1,11 @@
interface ModalSlice { interface ModalSlice {
addVaultBorrowingsModal: AddVaultBorrowingsModal | null
borrowModal: BorrowModal | null borrowModal: BorrowModal | null
createAccountModal: boolean createAccountModal: boolean
deleteAccountModal: boolean deleteAccountModal: boolean
fundAccountModal: boolean fundAccountModal: boolean
fundAndWithdrawModal: 'fund' | 'withdraw' | null fundAndWithdrawModal: 'fund' | 'withdraw' | null
vaultModal: { vaultModal: VaultModal | null
vault: Vault
} | null
} }
interface BorrowModal { interface BorrowModal {
@ -14,3 +13,12 @@ interface BorrowModal {
marketData: BorrowAsset | BorrowAssetActive marketData: BorrowAsset | BorrowAssetActive
isRepay?: boolean isRepay?: boolean
} }
interface VaultModal {
vault: Vault
selectedBorrowDenoms: string[]
}
interface AddVaultBorrowingsModal {
selectedDenoms: string[]
}

View File

@ -19,3 +19,7 @@ export function getBaseAsset() {
export function getDisplayCurrencies() { export function getDisplayCurrencies() {
return ASSETS.filter((asset) => asset.isDisplayCurrency) return ASSETS.filter((asset) => asset.isDisplayCurrency)
} }
export function findCoinByDenom(denom: string, coins: BigNumberCoin[]) {
return coins.find((coin) => coin.denom === denom)
}

View File

@ -4,6 +4,7 @@ import { IS_TESTNET } from 'constants/env'
import { TESTNET_VAULTS, VAULTS } from 'constants/vaults' import { TESTNET_VAULTS, VAULTS } from 'constants/vaults'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import { getNetCollateralValue } from 'utils/accounts' import { getNetCollateralValue } from 'utils/accounts'
import { BNCoin } from 'types/classes/BNCoin'
export function getVaultMetaData(address: string) { export function getVaultMetaData(address: string) {
const vaults = IS_TESTNET ? TESTNET_VAULTS : VAULTS const vaults = IS_TESTNET ? TESTNET_VAULTS : VAULTS
@ -16,8 +17,8 @@ export function calculateMaxBorrowAmounts(
marketAssets: Market[], marketAssets: Market[],
prices: Coin[], prices: Coin[],
denoms: string[], denoms: string[],
): Map<string, BigNumber> { ): BNCoin[] {
const maxAmounts = new Map<string, BigNumber>() const maxAmounts: BNCoin[] = []
const collateralValue = getNetCollateralValue(account, marketAssets, prices) const collateralValue = getNetCollateralValue(account, marketAssets, prices)
for (const denom of denoms) { for (const denom of denoms) {
@ -29,7 +30,7 @@ export function calculateMaxBorrowAmounts(
const borrowValue = BN(1).minus(borrowAsset.maxLtv).times(borrowAssetPrice) const borrowValue = BN(1).minus(borrowAsset.maxLtv).times(borrowAssetPrice)
const amount = collateralValue.dividedBy(borrowValue).decimalPlaces(0) const amount = collateralValue.dividedBy(borrowValue).decimalPlaces(0)
maxAmounts.set(denom, amount) maxAmounts.push(new BNCoin({ denom, amount: amount.toString() }))
} }
return maxAmounts return maxAmounts