feat: lending page and tables (#248)
This commit is contained in:
parent
7b5d4c3255
commit
878434dfec
37
__tests__/components/Earn/Lend/LendingDetails.test.tsx
Normal file
37
__tests__/components/Earn/Lend/LendingDetails.test.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import LendingDetails from 'components/Earn/Lend/LendingDetails'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
const data: LendingMarketTableData = {
|
||||
asset: ASSETS[0],
|
||||
marketDepositAmount: BN('890546916'),
|
||||
accountDepositValue: BN('0.5498406009348686811'),
|
||||
marketLiquidityAmount: BN('629396551'),
|
||||
marketDepositCap: BN('2500000000000'),
|
||||
marketLiquidityRate: 0.017,
|
||||
marketLiquidationThreshold: 0.61,
|
||||
marketMaxLtv: 0.59,
|
||||
}
|
||||
|
||||
jest.mock('hooks/useDisplayCurrencyPrice', () => () => {
|
||||
const { BN } = require('utils/helpers')
|
||||
|
||||
return {
|
||||
getConversionRate: () => BN(1),
|
||||
convertAmount: () => BN(1),
|
||||
symbol: 'MARS',
|
||||
}
|
||||
})
|
||||
|
||||
describe('<LendingDetails />', () => {
|
||||
afterAll(() => {
|
||||
jest.unmock('hooks/usePrices')
|
||||
})
|
||||
|
||||
it('should render', () => {
|
||||
const { container } = render(<LendingDetails data={data} />)
|
||||
expect(container).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,18 +1,18 @@
|
||||
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'
|
||||
import getMarketLiquidities from 'api/markets/getMarketLiquidities'
|
||||
|
||||
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
|
||||
const liquidity = await getMarketLiquidity()
|
||||
const liquidities = await getMarketLiquidities()
|
||||
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 amount = liquidities.find((coin) => coin.denom === market.denom)?.amount ?? '0'
|
||||
const asset = enabledAssets.find((asset) => asset.denom === market.denom)!
|
||||
|
||||
return {
|
||||
|
@ -2,7 +2,7 @@ import { BN } from 'utils/helpers'
|
||||
import getMarketDeposits from 'api/markets/getMarketDeposits'
|
||||
import getMarketDebts from 'api/markets/getMarketDebts'
|
||||
|
||||
export default async function getMarketLiquidity(): Promise<Coin[]> {
|
||||
export default async function getMarketLiquidities(): Promise<Coin[]> {
|
||||
const deposits = await getMarketDeposits()
|
||||
const debts = await getMarketDebts()
|
||||
|
63
src/components/Earn/Lend/LendingActionButtons.tsx
Normal file
63
src/components/Earn/Lend/LendingActionButtons.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import Button from 'components/Button'
|
||||
import { ArrowDownLine, ArrowUpLine } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import ConditionalWrapper from 'hocs/ConditionalWrapper'
|
||||
import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits'
|
||||
import { byDenom } from 'utils/array'
|
||||
|
||||
interface Props {
|
||||
data: LendingMarketTableData
|
||||
}
|
||||
|
||||
const buttonClassnames = 'm-0 flex w-40 text-lg'
|
||||
const iconClassnames = 'ml-0 mr-1 w-4 h-4'
|
||||
|
||||
function LendingActionButtons(props: Props) {
|
||||
const { asset, accountDepositValue } = props.data
|
||||
const accountDeposits = useCurrentAccountDeposits()
|
||||
const assetDepositAmount = accountDeposits.find(byDenom(asset.denom))?.amount
|
||||
|
||||
return (
|
||||
<div className='flex flex-row space-x-2'>
|
||||
{accountDepositValue && (
|
||||
<Button
|
||||
leftIcon={<ArrowDownLine />}
|
||||
iconClassName={iconClassnames}
|
||||
color='secondary'
|
||||
onClick={() => alert('hello!')}
|
||||
className={buttonClassnames}
|
||||
>
|
||||
Withdraw
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ConditionalWrapper
|
||||
condition={!assetDepositAmount}
|
||||
wrapper={(children) => (
|
||||
<Tooltip
|
||||
type='warning'
|
||||
content={
|
||||
<Text size='sm'>{`You don’t have any ${asset.symbol}. Please first deposit ${asset.symbol} into your credit account before lending.`}</Text>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
leftIcon={<ArrowUpLine />}
|
||||
iconClassName={iconClassnames}
|
||||
disabled={!assetDepositAmount}
|
||||
color='secondary'
|
||||
onClick={() => alert('hello!')}
|
||||
className={buttonClassnames}
|
||||
>
|
||||
Lend
|
||||
</Button>
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LendingActionButtons
|
62
src/components/Earn/Lend/LendingDetails.tsx
Normal file
62
src/components/Earn/Lend/LendingDetails.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||
import { formatPercent, formatValue } from 'utils/formatters'
|
||||
import useDisplayCurrencyPrice from 'hooks/useDisplayCurrencyPrice'
|
||||
|
||||
interface Props {
|
||||
data: LendingMarketTableData
|
||||
}
|
||||
|
||||
function LendingDetails({ data }: Props) {
|
||||
const {
|
||||
convertAmount,
|
||||
getConversionRate,
|
||||
symbol: displayCurrencySymbol,
|
||||
} = useDisplayCurrencyPrice()
|
||||
const {
|
||||
asset,
|
||||
marketMaxLtv,
|
||||
marketDepositAmount,
|
||||
marketLiquidityAmount,
|
||||
marketLiquidationThreshold,
|
||||
} = data
|
||||
const formattedTotalSuppliedValue = formatValue(
|
||||
convertAmount(asset, marketDepositAmount).toNumber(),
|
||||
{
|
||||
abbreviated: true,
|
||||
suffix: ` ${displayCurrencySymbol}`,
|
||||
},
|
||||
)
|
||||
const formattedPrice = formatValue(getConversionRate(asset.denom).toNumber(), {
|
||||
maxDecimals: 2,
|
||||
suffix: ` ${displayCurrencySymbol}`,
|
||||
})
|
||||
const totalBorrowed = marketDepositAmount.minus(marketLiquidityAmount)
|
||||
const utilizationRatePercent = formatPercent(
|
||||
totalBorrowed.dividedBy(marketDepositAmount).toNumber(),
|
||||
2,
|
||||
)
|
||||
|
||||
const details = [
|
||||
{ info: formattedTotalSuppliedValue, title: 'Total Supplied' },
|
||||
{ info: formatPercent(marketMaxLtv, 2), title: 'Max LTV' },
|
||||
{ info: formatPercent(marketLiquidationThreshold, 2), title: 'Liquidation Threshold' },
|
||||
{ info: formattedPrice, title: 'Oracle Price' },
|
||||
{ info: utilizationRatePercent, title: 'Utilization Rate' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='flex flex-1 justify-center rounded-md bg-white bg-opacity-5'>
|
||||
{details.map((detail, index) => (
|
||||
<TitleAndSubCell
|
||||
key={index}
|
||||
className='text-md text-center'
|
||||
containerClassName='m-5 ml-10 mr-10 space-y-2'
|
||||
title={detail.info}
|
||||
sub={detail.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LendingDetails
|
136
src/components/Earn/Lend/LendingMarketsTable.tsx
Normal file
136
src/components/Earn/Lend/LendingMarketsTable.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { ColumnDef, Row, Table } from '@tanstack/react-table'
|
||||
import { useMemo } from 'react'
|
||||
import Image from 'next/image'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Text from 'components/Text'
|
||||
import AssetListTable from 'components/MarketAssetTable'
|
||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||
import { ChevronDown, ChevronRight } from 'components/Icons'
|
||||
import { convertLiquidityRateToAPR, demagnify, formatValue } from 'utils/formatters'
|
||||
import MarketAssetTableRow from 'components/MarketAssetTable/MarketAssetTableRow'
|
||||
import LendingActionButtons from 'components/Earn/Lend/LendingActionButtons'
|
||||
import LendingDetails from 'components/Earn/Lend/LendingDetails'
|
||||
import useDisplayCurrencyPrice from 'hooks/useDisplayCurrencyPrice'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
data: LendingMarketTableData[]
|
||||
}
|
||||
|
||||
function LendingMarketsTable(props: Props) {
|
||||
const { title, data } = props
|
||||
const { symbol: displayCurrencySymbol } = useDisplayCurrencyPrice()
|
||||
const shouldShowAccountDeposit = !!data[0]?.accountDepositValue
|
||||
|
||||
const rowRenderer = (row: Row<LendingMarketTableData>, table: Table<LendingMarketTableData>) => {
|
||||
return (
|
||||
<MarketAssetTableRow
|
||||
key={`lend-asset-${row.id}`}
|
||||
isExpanded={row.getIsExpanded()}
|
||||
resetExpanded={table.resetExpanded}
|
||||
rowData={row}
|
||||
expandedActionButtons={<LendingActionButtons data={row.original} />}
|
||||
expandedDetails={<LendingDetails data={row.original} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columns = useMemo<ColumnDef<LendingMarketTableData>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'asset.name',
|
||||
header: 'Asset',
|
||||
id: 'symbol',
|
||||
cell: ({ row }) => {
|
||||
const asset = row.original.asset
|
||||
|
||||
return (
|
||||
<div className='flex flex-1 items-center gap-3'>
|
||||
<Image src={asset.logo} alt={asset.symbol} width={32} height={32} />
|
||||
<TitleAndSubCell
|
||||
title={asset.symbol}
|
||||
sub={asset.name}
|
||||
className='min-w-15 text-left'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
...(shouldShowAccountDeposit
|
||||
? [
|
||||
{
|
||||
accessorKey: 'accountDepositValue',
|
||||
header: 'Deposited',
|
||||
cell: ({ row }) => {
|
||||
const accountDepositValue = (
|
||||
row.original.accountDepositValue as BigNumber
|
||||
).toNumber()
|
||||
|
||||
return (
|
||||
<FormattedNumber
|
||||
className='text-xs'
|
||||
animate={true}
|
||||
amount={accountDepositValue}
|
||||
options={{ suffix: ` ${displayCurrencySymbol}` }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
} as ColumnDef<LendingMarketTableData>,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
accessorKey: 'marketLiquidityRate',
|
||||
header: 'APR',
|
||||
cell: ({ row }) => {
|
||||
const apr = convertLiquidityRateToAPR(row.original.marketLiquidityRate)
|
||||
return <Text size='xs'>{apr.toFixed(2)}%</Text>
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'marketDepositCap',
|
||||
header: 'Depo. Cap',
|
||||
cell: ({ row }) => {
|
||||
const { marketDepositCap, marketDepositAmount, asset } = row.original
|
||||
const remainingCap = row.original.marketDepositCap.minus(
|
||||
demagnify(marketDepositAmount, asset),
|
||||
)
|
||||
|
||||
const [formattedRemainingCap, formattedDepositCap] = [remainingCap, marketDepositCap].map(
|
||||
(value) =>
|
||||
formatValue(value.toNumber(), {
|
||||
decimals: asset.decimals,
|
||||
abbreviated: true,
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<TitleAndSubCell
|
||||
className='text-xs'
|
||||
title={formattedDepositCap}
|
||||
sub={`${formattedRemainingCap} left`}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'manage',
|
||||
enableSorting: false,
|
||||
header: 'Manage',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center justify-end'>
|
||||
<div className={classNames('w-4')}>
|
||||
{row.getIsExpanded() ? <ChevronDown /> : <ChevronRight />}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[displayCurrencySymbol, shouldShowAccountDeposit],
|
||||
)
|
||||
|
||||
return <AssetListTable title={title} rowRenderer={rowRenderer} columns={columns} data={data} />
|
||||
}
|
||||
|
||||
export default LendingMarketsTable
|
73
src/components/MarketAssetTable/MarketAssetTableRow.tsx
Normal file
73
src/components/MarketAssetTable/MarketAssetTableRow.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { flexRender, Row } from '@tanstack/react-table'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Text from 'components/Text'
|
||||
|
||||
type Props<TData> = {
|
||||
rowData: Row<TData>
|
||||
resetExpanded: (defaultState?: boolean | undefined) => void
|
||||
isExpanded: boolean
|
||||
expandedActionButtons?: JSX.Element
|
||||
expandedDetails?: JSX.Element
|
||||
}
|
||||
|
||||
function AssetListTableRow<TData>(props: Props<TData>) {
|
||||
const renderFullRow = (key: string, content: JSX.Element) => (
|
||||
<tr key={key} className='bg-black/20'>
|
||||
<td
|
||||
className='border-b border-white border-opacity-10 p-4'
|
||||
colSpan={props.rowData.getAllCells().length}
|
||||
>
|
||||
{content}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
const renderExpanded = () => {
|
||||
return (
|
||||
<>
|
||||
{props.expandedActionButtons &&
|
||||
renderFullRow(
|
||||
`${props.rowData.id}-expanded-actions`,
|
||||
<div className='flex flex-1 flex-row justify-between'>
|
||||
<Text className='mt-1 flex p-0 font-bold' size='base'>
|
||||
Details
|
||||
</Text>
|
||||
<div>{props.expandedActionButtons}</div>
|
||||
</div>,
|
||||
)}
|
||||
{props.expandedDetails &&
|
||||
renderFullRow(`${props.rowData.id}-expanded-details`, props.expandedDetails)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
key={props.rowData.id}
|
||||
className={classNames(
|
||||
'cursor-pointer transition-colors',
|
||||
'border-b border-white border-opacity-10',
|
||||
props.rowData.getIsExpanded() ? 'bg-black/20' : 'bg-white/0 hover:bg-white/5',
|
||||
)}
|
||||
onClick={() => {
|
||||
const isExpanded = props.rowData.getIsExpanded()
|
||||
props.resetExpanded()
|
||||
!isExpanded && props.rowData.toggleExpanded()
|
||||
}}
|
||||
>
|
||||
{props.rowData.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<td key={cell.id} className={'p-4 text-right'}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
{props.isExpanded && renderExpanded()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetListTableRow
|
101
src/components/MarketAssetTable/index.tsx
Normal file
101
src/components/MarketAssetTable/index.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
Row,
|
||||
SortingState,
|
||||
Table,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import { SortAsc, SortDesc, SortNone } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import Card from 'components/Card'
|
||||
|
||||
interface Props<TData> {
|
||||
title: string
|
||||
data: TData[]
|
||||
columns: ColumnDef<TData>[]
|
||||
rowRenderer: (row: Row<TData>, table: Table<TData>) => JSX.Element
|
||||
}
|
||||
|
||||
function AssetListTable<TData>(props: Props<TData>) {
|
||||
const { title, data, columns } = props
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
})
|
||||
|
||||
const _rowRenderer = (row: Row<TData>) => props.rowRenderer(row, table)
|
||||
|
||||
if (!data.length) return null
|
||||
|
||||
return (
|
||||
<Card className='mb-4 h-fit w-full bg-white/5' title={title}>
|
||||
<table className='w-full'>
|
||||
<thead className='border-b border-white border-opacity-10 bg-black/20'>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<th
|
||||
key={header.id}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
className={classNames(
|
||||
'px-4 py-3',
|
||||
header.column.getCanSort() && 'cursor-pointer',
|
||||
header.id === 'symbol' ? 'text-left' : 'text-right',
|
||||
{
|
||||
'w-32': header.id === 'manage',
|
||||
'w-48': header.id === 'depositCap',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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='xs'
|
||||
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(_rowRenderer)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetListTable
|
@ -6,11 +6,12 @@ interface Props {
|
||||
title: string | React.ReactNode
|
||||
sub: string | React.ReactNode
|
||||
className?: string
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
export default function TitleAndSubCell(props: Props) {
|
||||
return (
|
||||
<div className='flex flex-col gap-[0.5]'>
|
||||
<div className={classNames('flex flex-col gap-[0.5]', props.containerClassName)}>
|
||||
<Text size='sm' className={props.className}>
|
||||
{props.title}
|
||||
</Text>
|
||||
|
10
src/hocs/ConditionalWrapper.tsx
Normal file
10
src/hocs/ConditionalWrapper.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
interface Props {
|
||||
condition: boolean
|
||||
wrapper: (children: JSX.Element) => JSX.Element
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const ConditionalWrapper = ({ condition, wrapper, children }: Props) =>
|
||||
condition ? wrapper(children) : children
|
||||
|
||||
export default ConditionalWrapper
|
6
src/hooks/useCurrentAccountDeposits.ts
Normal file
6
src/hooks/useCurrentAccountDeposits.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
|
||||
export default function useCurrentAccountDeposits() {
|
||||
const account = useCurrentAccount()
|
||||
return account?.deposits ?? []
|
||||
}
|
6
src/hooks/useDepositEnabledMarkets.ts
Normal file
6
src/hooks/useDepositEnabledMarkets.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import useMarketAssets from 'hooks/useMarketAssets'
|
||||
|
||||
export default function useDepositEnabledMarkets() {
|
||||
const { data: markets } = useMarketAssets()
|
||||
return markets.filter((market) => market.depositEnabled)
|
||||
}
|
39
src/hooks/useDisplayCurrencyPrice.ts
Normal file
39
src/hooks/useDisplayCurrencyPrice.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import useStore from 'store'
|
||||
import { BN } from 'utils/helpers'
|
||||
import { byDenom } from 'utils/array'
|
||||
import usePrices from 'hooks/usePrices'
|
||||
|
||||
function useDisplayCurrencyPrice() {
|
||||
const { data: prices } = usePrices()
|
||||
const displayCurrency = useStore((s) => s.displayCurrency)
|
||||
|
||||
const getConversionRate = useCallback(
|
||||
(denom: string) => {
|
||||
const assetPrice = prices.find(byDenom(denom))
|
||||
const displayCurrencyPrice = prices.find(byDenom(displayCurrency.denom))
|
||||
|
||||
if (assetPrice && displayCurrencyPrice) {
|
||||
return BN(assetPrice.amount).div(displayCurrencyPrice.amount)
|
||||
} else {
|
||||
throw 'Given denom or display currency price has not found'
|
||||
}
|
||||
},
|
||||
[prices, displayCurrency],
|
||||
)
|
||||
|
||||
const convertAmount = useCallback(
|
||||
(asset: Asset, amount: string | number | BigNumber) =>
|
||||
getConversionRate(asset.denom).multipliedBy(BN(amount).shiftedBy(-asset.decimals)),
|
||||
[getConversionRate],
|
||||
)
|
||||
|
||||
return {
|
||||
getConversionRate,
|
||||
convertAmount,
|
||||
symbol: displayCurrency.symbol,
|
||||
}
|
||||
}
|
||||
|
||||
export default useDisplayCurrencyPrice
|
56
src/hooks/useLendingMarketAssetsTableData.ts
Normal file
56
src/hooks/useLendingMarketAssetsTableData.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { BN } from 'utils/helpers'
|
||||
import { byDenom } from 'utils/array'
|
||||
import { getAssetByDenom } from 'utils/assets'
|
||||
import useMarketDeposits from 'hooks/useMarketDeposits'
|
||||
import useMarketLiquidities from 'hooks/useMarketLiquidities'
|
||||
import useDisplayCurrencyPrice from 'hooks/useDisplayCurrencyPrice'
|
||||
import useDepositEnabledMarkets from 'hooks/useDepositEnabledMarkets'
|
||||
import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits'
|
||||
|
||||
function useLendingMarketAssetsTableData(): {
|
||||
lentAssets: LendingMarketTableData[]
|
||||
availableAssets: LendingMarketTableData[]
|
||||
} {
|
||||
const markets = useDepositEnabledMarkets()
|
||||
const accountDeposits = useCurrentAccountDeposits()
|
||||
// TODO: replace market deposits with account.lends when credit manager contract has lend feature
|
||||
const { data: marketDeposits } = useMarketDeposits()
|
||||
const { data: marketLiquidities } = useMarketLiquidities()
|
||||
const { convertAmount } = useDisplayCurrencyPrice()
|
||||
|
||||
return useMemo(() => {
|
||||
const lentAssets: LendingMarketTableData[] = [],
|
||||
availableAssets: LendingMarketTableData[] = []
|
||||
|
||||
markets.forEach(({ denom, depositCap, liquidityRate, liquidationThreshold, maxLtv }) => {
|
||||
const asset = getAssetByDenom(denom) as Asset
|
||||
const marketDepositAmount = BN(marketDeposits.find(byDenom(denom))?.amount ?? 0)
|
||||
const marketLiquidityAmount = BN(marketLiquidities.find(byDenom(denom))?.amount ?? 0)
|
||||
const accountDepositAmount = accountDeposits.find(byDenom(denom))?.amount
|
||||
const accountDepositValue = accountDepositAmount
|
||||
? convertAmount(asset, accountDepositAmount)
|
||||
: undefined
|
||||
|
||||
const lendingMarketAsset: LendingMarketTableData = {
|
||||
asset,
|
||||
marketDepositAmount,
|
||||
accountDepositValue,
|
||||
marketLiquidityAmount,
|
||||
marketDepositCap: BN(depositCap),
|
||||
marketLiquidityRate: liquidityRate,
|
||||
marketLiquidationThreshold: liquidationThreshold,
|
||||
marketMaxLtv: maxLtv,
|
||||
}
|
||||
|
||||
;(lendingMarketAsset.accountDepositValue ? lentAssets : availableAssets).push(
|
||||
lendingMarketAsset,
|
||||
)
|
||||
})
|
||||
|
||||
return { lentAssets, availableAssets }
|
||||
}, [markets, marketDeposits, marketLiquidities, accountDeposits, convertAmount])
|
||||
}
|
||||
|
||||
export default useLendingMarketAssetsTableData
|
10
src/hooks/useMarketDeposits.ts
Normal file
10
src/hooks/useMarketDeposits.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import useSWR from 'swr'
|
||||
|
||||
import getMarketDeposits from 'api/markets/getMarketDeposits'
|
||||
|
||||
export default function useMarketDeposits() {
|
||||
return useSWR(`marketDeposits`, getMarketDeposits, {
|
||||
suspense: true,
|
||||
fallbackData: [],
|
||||
})
|
||||
}
|
10
src/hooks/useMarketLiquidities.ts
Normal file
10
src/hooks/useMarketLiquidities.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import useSWR from 'swr'
|
||||
|
||||
import getMarketLiquidities from 'api/markets/getMarketLiquidities'
|
||||
|
||||
export default function useMarketLiquidities() {
|
||||
return useSWR(`marketLiquidities`, getMarketLiquidities, {
|
||||
suspense: true,
|
||||
fallbackData: [],
|
||||
})
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import Card from 'components/Card'
|
||||
import Tab from 'components/Earn/Tab'
|
||||
import LendingMarketsTable from 'components/Earn/Lend/LendingMarketsTable'
|
||||
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
|
||||
|
||||
export default function LendPage() {
|
||||
const { lentAssets, availableAssets } = useLendingMarketAssetsTableData()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tab />
|
||||
<Card title='Lend'>
|
||||
<></>
|
||||
</Card>
|
||||
<LendingMarketsTable data={lentAssets} title='Lent Assets' />
|
||||
<LendingMarketsTable data={availableAssets} title='Available Markets' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
11
src/types/interfaces/asset.d.ts
vendored
11
src/types/interfaces/asset.d.ts
vendored
@ -36,3 +36,14 @@ interface BigNumberCoin {
|
||||
denom: string
|
||||
amount: BigNumber
|
||||
}
|
||||
|
||||
interface LendingMarketTableData {
|
||||
asset: Asset
|
||||
marketMaxLtv: number
|
||||
marketLiquidityRate: number
|
||||
marketDepositCap: BigNumber
|
||||
marketDepositAmount: BigNumber
|
||||
accountDepositValue?: BigNumber
|
||||
marketLiquidityAmount: BigNumber
|
||||
marketLiquidationThreshold: number
|
||||
}
|
||||
|
2
src/types/interfaces/market.d.ts
vendored
2
src/types/interfaces/market.d.ts
vendored
@ -7,4 +7,6 @@ interface Market {
|
||||
borrowEnabled: boolean
|
||||
depositCap: string
|
||||
maxLtv: number
|
||||
liquidityRate: number
|
||||
liquidationThreshold: number
|
||||
}
|
||||
|
1
src/utils/array.ts
Normal file
1
src/utils/array.ts
Normal file
@ -0,0 +1 @@
|
||||
export const byDenom = (denom: string) => (entity: any) => entity.denom === denom
|
@ -168,3 +168,8 @@ export function convertToDisplayAmount(coin: Coin, displayCurrency: Asset, price
|
||||
.div(displayPrice.amount)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
export function convertLiquidityRateToAPR(rate: number) {
|
||||
const rateMulHundred = rate * 100
|
||||
return rateMulHundred >= 0.01 ? rateMulHundred : 0.0
|
||||
}
|
||||
|
@ -22,5 +22,7 @@ export function resolveMarketResponses(responses: MarketResponse[]): Market[] {
|
||||
borrowEnabled: response.borrow_enabled,
|
||||
depositCap: response.deposit_cap,
|
||||
maxLtv: Number(response.max_loan_to_value),
|
||||
liquidityRate: Number(response.liquidity_rate),
|
||||
liquidationThreshold: Number(response.liquidation_threshold),
|
||||
}))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user