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 { 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(),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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} />
|
||||||
|
@ -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 }}
|
||||||
|
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 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}
|
||||||
|
@ -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 />
|
||||||
|
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()
|
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 (
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
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 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'
|
||||||
|
@ -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}
|
||||||
|
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 { 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() {
|
@ -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()
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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 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()
|
@ -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),
|
||||||
},
|
},
|
@ -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 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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
14
src/types/interfaces/store/modals.d.ts
vendored
14
src/types/interfaces/store/modals.d.ts
vendored
@ -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[]
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user