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:
parent
0f8e656651
commit
7b5d4c3255
@ -1,25 +1,36 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import VaultBorrowings from 'components/Modals/vault/VaultBorrowings'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
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.fn(() => ({
|
||||
data: [],
|
||||
})),
|
||||
)
|
||||
|
||||
jest.mock('hooks/usePrice', () => jest.fn(() => '1'))
|
||||
|
||||
jest.mock('hooks/useMarketAssets', () =>
|
||||
jest.fn(() => ({
|
||||
data: [],
|
||||
})),
|
||||
)
|
||||
|
||||
jest.mock('components/DisplayCurrency')
|
||||
const mockedDisplayCurrency = jest
|
||||
.mocked(DisplayCurrency)
|
||||
.mockImplementation(() => <div>Display currency</div>)
|
||||
|
||||
describe('<VaultBorrowings />', () => {
|
||||
const defaultProps: {
|
||||
account: Account
|
||||
defaultBorrowDenom: string
|
||||
onChangeBorrowings: (borrowings: Map<string, BigNumber>) => void
|
||||
} = {
|
||||
const defaultProps: VaultBorrowingsProps = {
|
||||
primaryAsset: ASSETS[0],
|
||||
secondaryAsset: ASSETS[1],
|
||||
primaryAmount: BN(0),
|
||||
secondaryAmount: BN(0),
|
||||
account: {
|
||||
id: 'test',
|
||||
deposits: [],
|
||||
@ -27,12 +38,32 @@ describe('<VaultBorrowings />', () => {
|
||||
vaults: [],
|
||||
lends: [],
|
||||
},
|
||||
defaultBorrowDenom: 'test-denom',
|
||||
borrowings: [],
|
||||
onChangeBorrowings: jest.fn(),
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
useStore.setState({
|
||||
baseCurrency: ASSETS[0],
|
||||
selectedBorrowDenoms: [ASSETS[1].denom],
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
useStore.clearState()
|
||||
mockedDisplayCurrency.mockClear()
|
||||
})
|
||||
|
||||
it('should render', () => {
|
||||
const { container } = render(<VaultBorrowings {...defaultProps} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render DisplayCurrency correctly', () => {
|
||||
expect(mockedDisplayCurrency).toHaveBeenCalledTimes(1)
|
||||
expect(mockedDisplayCurrency).toHaveBeenCalledWith(
|
||||
{ coin: { denom: 'uosmo', amount: '0' } },
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -2,17 +2,21 @@ import { BN } from 'utils/helpers'
|
||||
import getPrices from 'api/prices/getPrices'
|
||||
import getMarkets from 'api/markets/getMarkets'
|
||||
import getMarketLiquidity from 'api/markets/getMarketLiquidity'
|
||||
import { getEnabledMarketAssets } from 'utils/assets'
|
||||
|
||||
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
|
||||
const liquidity = await getMarketLiquidity()
|
||||
const enabledAssets = getEnabledMarketAssets()
|
||||
const borrowEnabledMarkets = (await getMarkets()).filter((market: Market) => market.borrowEnabled)
|
||||
const prices = await getPrices()
|
||||
|
||||
const borrow: BorrowAsset[] = borrowEnabledMarkets.map((market) => {
|
||||
const price = prices.find((coin) => coin.denom === market.denom)?.amount ?? '1'
|
||||
const amount = liquidity.find((coin) => coin.denom === market.denom)?.amount ?? '0'
|
||||
const asset = enabledAssets.find((asset) => asset.denom === market.denom)!
|
||||
|
||||
return {
|
||||
denom: market.denom,
|
||||
...asset,
|
||||
borrowRate: market.borrowRate ?? 0,
|
||||
liquidity: {
|
||||
amount: amount,
|
||||
|
@ -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 {
|
||||
items: Item[]
|
||||
allowMultipleOpen?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Accordion(props: Props) {
|
||||
@ -19,7 +21,7 @@ export default function Accordion(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className={classNames('w-full', props.className)}>
|
||||
{props.items.map((item, index) => (
|
||||
<Card key={item.title} className='mb-4'>
|
||||
<AccordionContent item={item} index={index} />
|
||||
|
@ -25,8 +25,8 @@ export default function AccountSummary(props: Props) {
|
||||
if (!props.account) return null
|
||||
|
||||
return (
|
||||
<div className='flex min-w-[345px] basis-[345px] flex-wrap'>
|
||||
<Card className='mb-4 min-w-fit bg-white/10' contentClassName='flex'>
|
||||
<div className='h-[546px] min-w-[345px] basis-[345px] overflow-y-scroll scrollbar-hide'>
|
||||
<Card className='mb-4 h-min min-w-fit bg-white/10' contentClassName='flex'>
|
||||
<Item>
|
||||
<DisplayCurrency
|
||||
coin={{ amount: accountBalance.toString(), denom: baseCurrency.denom }}
|
||||
|
19
src/components/AssetImage.tsx
Normal file
19
src/components/AssetImage.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
@ -19,6 +19,7 @@ import Text from 'components/Text'
|
||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||
import { getEnabledMarketAssets } from 'utils/assets'
|
||||
import { formatPercent } from 'utils/formatters'
|
||||
import AssetImage from 'components/AssetImage'
|
||||
|
||||
type Props = {
|
||||
data: BorrowAsset[] | BorrowAssetActive[]
|
||||
@ -40,7 +41,7 @@ export const BorrowTable = (props: Props) => {
|
||||
|
||||
return (
|
||||
<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
|
||||
title={asset.symbol}
|
||||
sub={asset.name}
|
||||
|
@ -93,10 +93,10 @@ const Button = React.forwardRef(function Button(
|
||||
const [leftIconClassNames, rightIconClassNames] = useMemo(() => {
|
||||
const hasContent = !!(text || children)
|
||||
const iconClasses = ['flex items-center justify-center', iconClassName ?? 'h-4 w-4']
|
||||
const leftIconClasses = [iconClasses, hasContent && 'mr-2']
|
||||
const rightIconClasses = [iconClasses, hasContent && 'ml-2']
|
||||
const leftIconClasses = [...iconClasses, hasContent && 'mr-2']
|
||||
const rightIconClasses = [...iconClasses, hasContent && 'ml-2']
|
||||
|
||||
return [leftIconClasses, rightIconClasses].map(classNames)
|
||||
return [leftIconClasses, rightIconClasses]
|
||||
}, [children, iconClassName, text])
|
||||
|
||||
return (
|
||||
@ -111,10 +111,10 @@ const Button = React.forwardRef(function Button(
|
||||
<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>}
|
||||
{children && children}
|
||||
{rightIcon && <span className={rightIconClassNames}>{rightIcon}</span>}
|
||||
{rightIcon && <span className={classNames(rightIconClassNames)}>{rightIcon}</span>}
|
||||
{hasSubmenu && (
|
||||
<span data-testid='button-submenu-indicator' className='ml-2 inline-block w-2.5'>
|
||||
<ChevronDown />
|
||||
|
27
src/components/Checkbox.tsx
Normal file
27
src/components/Checkbox.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -19,7 +19,12 @@ export default function VaultCard(props: Props) {
|
||||
const currentAccount = useCurrentAccount()
|
||||
|
||||
function openVaultModal() {
|
||||
useStore.setState({ vaultModal: { vault: props.vault } })
|
||||
useStore.setState({
|
||||
vaultModal: {
|
||||
vault: props.vault,
|
||||
selectedBorrowDenoms: [props.vault.denoms.secondary],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -11,7 +11,12 @@ interface Props {
|
||||
|
||||
export default function VaultExpanded(props: Props) {
|
||||
function enterVaultHandler() {
|
||||
useStore.setState({ vaultModal: { vault: props.row.original } })
|
||||
useStore.setState({
|
||||
vaultModal: {
|
||||
vault: props.row.original,
|
||||
selectedBorrowDenoms: [props.row.original.denoms.secondary],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import AssetImage from 'components/AssetImage'
|
||||
import { getAssetByDenom } from 'utils/assets'
|
||||
|
||||
interface Props {
|
||||
@ -15,10 +16,10 @@ export default function VaultLogo(props: Props) {
|
||||
return (
|
||||
<div className='relative grid w-12 place-items-center'>
|
||||
<div className='absolute'>
|
||||
<Image src={primaryAsset.logo} alt={`${primaryAsset.symbol} logo`} width={24} height={24} />
|
||||
<AssetImage asset={primaryAsset} size={24} />
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
|
3
src/components/Icons/Search.svg
Normal file
3
src/components/Icons/Search.svg
Normal 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 |
@ -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 PlusCircled } from 'components/Icons/PlusCircled.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 SortAsc } from 'components/Icons/SortAsc.svg'
|
||||
export { default as SortDesc } from 'components/Icons/SortDesc.svg'
|
||||
|
@ -4,6 +4,7 @@ import { ReactNode, useEffect, useRef } from 'react'
|
||||
import Button from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import { Cross } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
|
||||
interface Props {
|
||||
header: string | ReactNode
|
||||
@ -12,6 +13,7 @@ interface Props {
|
||||
content?: ReactNode | string
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
modalClassName?: string
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
@ -47,6 +49,7 @@ export default function Modal(props: Props) {
|
||||
'w-[895px] border-none bg-transparent text-white',
|
||||
'focus-visible:outline-none',
|
||||
'backdrop:bg-black/50 backdrop:backdrop-blur-sm',
|
||||
props.modalClassName,
|
||||
)}
|
||||
>
|
||||
<Card
|
||||
@ -57,13 +60,9 @@ export default function Modal(props: Props) {
|
||||
>
|
||||
<div className={classNames('flex justify-between', props.headerClassName)}>
|
||||
{props.header}
|
||||
<Button
|
||||
onClick={onClose}
|
||||
leftIcon={<Cross />}
|
||||
className='h-8 w-8'
|
||||
iconClassName='h-2 w-2'
|
||||
color='tertiary'
|
||||
/>
|
||||
<Button onClick={onClose} leftIcon={<Cross />} iconClassName='h-3 w-3' color='tertiary'>
|
||||
<Text size='sm'>ESC</Text>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classNames(props.contentClassName, 'flex-grow')}>
|
||||
{props.children ? props.children : props.content}
|
||||
|
129
src/components/Modals/AddVaultAssets/AddVaultAssetTable.tsx
Normal file
129
src/components/Modals/AddVaultAssets/AddVaultAssetTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import Image from 'next/image'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import AccountSummary from 'components/Account/AccountSummary'
|
||||
@ -18,6 +17,7 @@ import useStore from 'store'
|
||||
import { hardcodedFee } from 'utils/contants'
|
||||
import { formatPercent, formatValue } from 'utils/formatters'
|
||||
import { BN } from 'utils/helpers'
|
||||
import AssetImage from 'components/AssetImage'
|
||||
|
||||
function getDebtAmount(modal: BorrowModal | null) {
|
||||
if (!(modal?.marketData as BorrowAssetActive)?.debt) return '0'
|
||||
@ -26,7 +26,7 @@ function getDebtAmount(modal: BorrowModal | null) {
|
||||
|
||||
function getAssetLogo(modal: BorrowModal | 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() {
|
@ -3,8 +3,7 @@ import Text from 'components/Text'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useStore from 'store'
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
|
||||
import FundWithdrawModalContent from './FundWithdrawModalContent'
|
||||
import FundWithdrawModalContent from 'components/Modals/FundWithdraw/FundAndWithdrawModalContent'
|
||||
|
||||
export default function FundAndWithdrawModal() {
|
||||
const currentAccount = useCurrentAccount()
|
@ -1,6 +1,7 @@
|
||||
import BorrowModal from 'components/Modals/BorrowModal'
|
||||
import FundAndWithdrawModal from 'components/Modals/fundwithdraw/FundAndWithdrawModal'
|
||||
import VaultModal from 'components/Modals/vault/VaultModal'
|
||||
import VaultModal from 'components/Modals/Vault/VaultModal'
|
||||
import BorrowModal from 'components/Modals/Borrow/BorrowModal'
|
||||
import FundAndWithdrawModal from 'components/Modals/FundWithdraw/FundAndWithdrawModal'
|
||||
import AddVaultBorrowAssetsModal from 'components/Modals/AddVaultAssets/AddVaultBorrowAssetsModal'
|
||||
|
||||
export default function ModalsContainer() {
|
||||
return (
|
||||
@ -8,6 +9,7 @@ export default function ModalsContainer() {
|
||||
<BorrowModal />
|
||||
<FundAndWithdrawModal />
|
||||
<VaultModal />
|
||||
<AddVaultBorrowAssetsModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
197
src/components/Modals/Vault/VaultBorrowings.tsx
Normal file
197
src/components/Modals/Vault/VaultBorrowings.tsx
Normal 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>
|
||||
)
|
||||
}
|
49
src/components/Modals/Vault/VaultBorrowingsSubTitle.tsx
Normal file
49
src/components/Modals/Vault/VaultBorrowingsSubTitle.tsx
Normal 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() }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -5,8 +5,7 @@ import { ASSETS } from 'constants/assets'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useStore from 'store'
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
|
||||
import VaultModalContent from './VaultModalContent'
|
||||
import VaultModalContent from 'components/Modals/Vault/VaultModalContent'
|
||||
|
||||
export default function VaultModal() {
|
||||
const currentAccount = useCurrentAccount()
|
@ -3,12 +3,13 @@ import { useCallback, useState } from 'react'
|
||||
|
||||
import Accordion from 'components/Accordion'
|
||||
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 { BN } from 'utils/helpers'
|
||||
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 {
|
||||
vault: Vault
|
||||
@ -18,7 +19,10 @@ interface 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 [primaryAmount, setPrimaryAmount] = useState<BigNumber>(BN(0))
|
||||
const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0))
|
||||
@ -41,6 +45,7 @@ export default function VaultModalContent(props: Props) {
|
||||
return (
|
||||
<div className='flex flex-grow items-start gap-6 p-6'>
|
||||
<Accordion
|
||||
className='h-[546px] overflow-y-scroll scrollbar-hide'
|
||||
items={[
|
||||
{
|
||||
renderContent: () => (
|
||||
@ -73,11 +78,16 @@ export default function VaultModalContent(props: Props) {
|
||||
renderContent: () => (
|
||||
<VaultBorrowings
|
||||
account={updatedAccount}
|
||||
defaultBorrowDenom={props.secondaryAsset.denom}
|
||||
borrowings={borrowings}
|
||||
primaryAmount={primaryAmount}
|
||||
secondaryAmount={secondaryAmount}
|
||||
primaryAsset={props.primaryAsset}
|
||||
secondaryAsset={props.secondaryAsset}
|
||||
onChangeBorrowings={onChangeBorrowings}
|
||||
/>
|
||||
),
|
||||
title: 'Borrow',
|
||||
subTitle: <VaultBorrowingsSubTitle borrowings={borrowings} />,
|
||||
isOpen: isOpen[1],
|
||||
toggleOpen: (index: number) => toggleOpen(index),
|
||||
},
|
@ -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>
|
||||
)
|
||||
}
|
34
src/components/SearchBar.tsx
Normal file
34
src/components/SearchBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -6,6 +6,7 @@ import { ChevronDown } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
import { formatValue } from 'utils/formatters'
|
||||
import AssetImage from 'components/AssetImage'
|
||||
|
||||
interface Props extends Option {
|
||||
isSelected?: boolean
|
||||
@ -18,11 +19,7 @@ export default function Option(props: Props) {
|
||||
const isCoin = !!props.denom
|
||||
|
||||
if (isCoin) {
|
||||
const currentAsset = ASSETS.find((asset) => asset.denom === props.denom)
|
||||
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 asset = ASSETS.find((asset) => asset.denom === props.denom) || ASSETS[0]
|
||||
const balance = props.amount ?? '0'
|
||||
|
||||
if (props.isDisplay) {
|
||||
@ -30,8 +27,8 @@ export default function Option(props: Props) {
|
||||
<div
|
||||
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} />
|
||||
<span>{symbol}</span>
|
||||
<AssetImage asset={asset} size={20} />
|
||||
<span>{asset.symbol}</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-block w-2.5 transition-transform',
|
||||
@ -53,20 +50,25 @@ export default function Option(props: Props) {
|
||||
'hover:cursor-pointer hover:bg-white/20',
|
||||
!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'>
|
||||
<Image src={logo} alt={`${symbol} token logo`} width={32} height={32} />
|
||||
<AssetImage asset={asset} size={32} />
|
||||
</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'>
|
||||
{formatValue(balance, { decimals, maxDecimals: 4, minDecimals: 0, rounded: true })}
|
||||
{formatValue(balance, {
|
||||
decimals: asset.decimals,
|
||||
maxDecimals: 4,
|
||||
minDecimals: 0,
|
||||
rounded: true,
|
||||
})}
|
||||
</Text>
|
||||
<Text size='sm' className='col-span-2 text-white/50'>
|
||||
{formatValue(5, { maxDecimals: 2, minDecimals: 0, prefix: 'APY ', suffix: '%' })}
|
||||
</Text>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
|
@ -13,6 +13,7 @@ import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import Button from 'components/Button'
|
||||
import { ExclamationMarkTriangle, TrashBin } from 'components/Icons'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import AssetImage from 'components/AssetImage'
|
||||
|
||||
interface Props {
|
||||
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'>
|
||||
<Image src={props.asset.logo} alt='token' width={20} height={20} />
|
||||
<AssetImage asset={props.asset} size={20} />
|
||||
<Text>{props.asset.symbol}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
@ -4,8 +4,7 @@ import { ReactNode } from 'react'
|
||||
|
||||
import { Questionmark } from 'components/Icons'
|
||||
import useStore from 'store'
|
||||
|
||||
import TooltipContent from './TooltipContent'
|
||||
import TooltipContent from 'components/Tooltip/TooltipContent'
|
||||
|
||||
interface Props {
|
||||
content: ReactNode | string
|
||||
|
@ -97,6 +97,7 @@ export const ASSETS: Asset[] = [
|
||||
isEnabled: true,
|
||||
isMarket: true,
|
||||
isDisplayCurrency: true,
|
||||
isStable: true,
|
||||
},
|
||||
{
|
||||
symbol: 'USDC.n',
|
||||
@ -112,5 +113,6 @@ export const ASSETS: Asset[] = [
|
||||
isEnabled: IS_TESTNET,
|
||||
isMarket: IS_TESTNET,
|
||||
isDisplayCurrency: IS_TESTNET,
|
||||
isStable: true,
|
||||
},
|
||||
]
|
||||
|
@ -4,6 +4,7 @@ import getMarketBorrowings from 'api/markets/getMarketBorrowings'
|
||||
|
||||
export default function useMarketBorrowings() {
|
||||
return useSWR(`marketBorrowings`, getMarketBorrowings, {
|
||||
suspense: true,
|
||||
fallbackData: [],
|
||||
suspense: false,
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import usePrices from './usePrices'
|
||||
import usePrices from 'hooks/usePrices'
|
||||
|
||||
export default function usePrice(denom: string) {
|
||||
const { data: prices } = usePrices()
|
||||
|
@ -1,10 +1,12 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
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 [borrowings, setBorrowings] = useState<BNCoin[]>([])
|
||||
|
||||
function getCoin(denom: string, amount: BigNumber): Coin {
|
||||
return {
|
||||
@ -14,32 +16,33 @@ export default function useUpdateAccount(account: Account) {
|
||||
}
|
||||
|
||||
const onChangeBorrowings = useCallback(
|
||||
(borrowings: Map<string, BigNumber>) => {
|
||||
(borrowings: BNCoin[]) => {
|
||||
const debts: Coin[] = [...account.debts]
|
||||
const deposits: Coin[] = [...account.deposits]
|
||||
const currentDebtDenoms = debts.map((debt) => debt.denom)
|
||||
const currentDepositDenoms = deposits.map((deposit) => deposit.denom)
|
||||
|
||||
borrowings.forEach((amount, denom) => {
|
||||
if (amount.isZero()) return
|
||||
borrowings.map((coin) => {
|
||||
if (coin.amount.isZero()) return
|
||||
|
||||
if (currentDebtDenoms.includes(denom)) {
|
||||
const index = currentDebtDenoms.indexOf(denom)
|
||||
const newAmount = BN(debts[index].amount).plus(amount)
|
||||
debts[index] = getCoin(denom, newAmount)
|
||||
if (currentDebtDenoms.includes(coin.denom)) {
|
||||
const index = currentDebtDenoms.indexOf(coin.denom)
|
||||
const newAmount = BN(debts[index].amount).plus(coin.amount)
|
||||
debts[index] = getCoin(coin.denom, newAmount)
|
||||
} else {
|
||||
debts.push(getCoin(denom, amount))
|
||||
debts.push(coin.toCoin())
|
||||
}
|
||||
|
||||
if (currentDepositDenoms.includes(denom)) {
|
||||
const index = currentDepositDenoms.indexOf(denom)
|
||||
const newAmount = BN(deposits[index].amount).plus(amount)
|
||||
deposits[index] = getCoin(denom, newAmount)
|
||||
if (currentDepositDenoms.includes(coin.denom)) {
|
||||
const index = currentDepositDenoms.indexOf(coin.denom)
|
||||
const newAmount = BN(deposits[index].amount).plus(coin.amount)
|
||||
deposits[index] = getCoin(coin.denom, newAmount)
|
||||
} else {
|
||||
deposits.push(getCoin(denom, amount))
|
||||
deposits.push(coin.toCoin())
|
||||
}
|
||||
})
|
||||
|
||||
setBorrowings(borrowings)
|
||||
setUpdatedAccount({
|
||||
...account,
|
||||
debts,
|
||||
@ -49,5 +52,5 @@ export default function useUpdateAccount(account: Account) {
|
||||
[account],
|
||||
)
|
||||
|
||||
return { updatedAccount, onChangeBorrowings }
|
||||
return { borrowings, updatedAccount, onChangeBorrowings }
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { GetState, SetState } from 'zustand'
|
||||
|
||||
export default function createModalSlice(set: SetState<ModalSlice>, get: GetState<ModalSlice>) {
|
||||
return {
|
||||
addVaultBorrowingsModal: null,
|
||||
borrowModal: null,
|
||||
createAccountModal: false,
|
||||
deleteAccountModal: false,
|
||||
|
18
src/types/classes/BNCoin.ts
Normal file
18
src/types/classes/BNCoin.ts
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
9
src/types/interfaces/asset.d.ts
vendored
9
src/types/interfaces/asset.d.ts
vendored
@ -13,14 +13,14 @@ interface Asset {
|
||||
isEnabled: boolean
|
||||
isMarket: boolean
|
||||
isDisplayCurrency?: boolean
|
||||
isStable?: boolean
|
||||
}
|
||||
|
||||
interface OtherAsset extends Omit<Asset, 'symbol'> {
|
||||
symbol: 'MARS'
|
||||
}
|
||||
|
||||
interface BorrowAsset {
|
||||
denom: string
|
||||
interface BorrowAsset extends Asset {
|
||||
borrowRate: number | null
|
||||
liquidity: {
|
||||
amount: string
|
||||
@ -31,3 +31,8 @@ interface BorrowAsset {
|
||||
interface BorrowAssetActive extends BorrowAsset {
|
||||
debt: string
|
||||
}
|
||||
|
||||
interface BigNumberCoin {
|
||||
denom: string
|
||||
amount: BigNumber
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
interface CommonSlice {
|
||||
accounts: Account[] | null
|
||||
address?: string
|
||||
balances: Coin[]
|
||||
client?: import('@marsprotocol/wallet-connector').WalletClient
|
||||
enableAnimations: boolean
|
||||
isOpen: boolean
|
||||
balances: Coin[]
|
||||
selectedAccount: string | null
|
||||
client?: import('@marsprotocol/wallet-connector').WalletClient
|
||||
status: import('@marsprotocol/wallet-connector').WalletConnectionStatus
|
||||
}
|
14
src/types/interfaces/store/modals.d.ts
vendored
14
src/types/interfaces/store/modals.d.ts
vendored
@ -1,12 +1,11 @@
|
||||
interface ModalSlice {
|
||||
addVaultBorrowingsModal: AddVaultBorrowingsModal | null
|
||||
borrowModal: BorrowModal | null
|
||||
createAccountModal: boolean
|
||||
deleteAccountModal: boolean
|
||||
fundAccountModal: boolean
|
||||
fundAndWithdrawModal: 'fund' | 'withdraw' | null
|
||||
vaultModal: {
|
||||
vault: Vault
|
||||
} | null
|
||||
vaultModal: VaultModal | null
|
||||
}
|
||||
|
||||
interface BorrowModal {
|
||||
@ -14,3 +13,12 @@ interface BorrowModal {
|
||||
marketData: BorrowAsset | BorrowAssetActive
|
||||
isRepay?: boolean
|
||||
}
|
||||
|
||||
interface VaultModal {
|
||||
vault: Vault
|
||||
selectedBorrowDenoms: string[]
|
||||
}
|
||||
|
||||
interface AddVaultBorrowingsModal {
|
||||
selectedDenoms: string[]
|
||||
}
|
||||
|
@ -19,3 +19,7 @@ export function getBaseAsset() {
|
||||
export function getDisplayCurrencies() {
|
||||
return ASSETS.filter((asset) => asset.isDisplayCurrency)
|
||||
}
|
||||
|
||||
export function findCoinByDenom(denom: string, coins: BigNumberCoin[]) {
|
||||
return coins.find((coin) => coin.denom === denom)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { IS_TESTNET } from 'constants/env'
|
||||
import { TESTNET_VAULTS, VAULTS } from 'constants/vaults'
|
||||
import { BN } from 'utils/helpers'
|
||||
import { getNetCollateralValue } from 'utils/accounts'
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
|
||||
export function getVaultMetaData(address: string) {
|
||||
const vaults = IS_TESTNET ? TESTNET_VAULTS : VAULTS
|
||||
@ -16,8 +17,8 @@ export function calculateMaxBorrowAmounts(
|
||||
marketAssets: Market[],
|
||||
prices: Coin[],
|
||||
denoms: string[],
|
||||
): Map<string, BigNumber> {
|
||||
const maxAmounts = new Map<string, BigNumber>()
|
||||
): BNCoin[] {
|
||||
const maxAmounts: BNCoin[] = []
|
||||
const collateralValue = getNetCollateralValue(account, marketAssets, prices)
|
||||
|
||||
for (const denom of denoms) {
|
||||
@ -29,7 +30,7 @@ export function calculateMaxBorrowAmounts(
|
||||
const borrowValue = BN(1).minus(borrowAsset.maxLtv).times(borrowAssetPrice)
|
||||
const amount = collateralValue.dividedBy(borrowValue).decimalPlaces(0)
|
||||
|
||||
maxAmounts.set(denom, amount)
|
||||
maxAmounts.push(new BNCoin({ denom, amount: amount.toString() }))
|
||||
}
|
||||
|
||||
return maxAmounts
|
||||
|
Loading…
Reference in New Issue
Block a user