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 { 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 { getEnabledMarketAssets } from 'utils/assets'
|
import { getEnabledMarketAssets } from 'utils/assets'
|
||||||
|
import getMarketLiquidities from 'api/markets/getMarketLiquidities'
|
||||||
|
|
||||||
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
|
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
|
||||||
const liquidity = await getMarketLiquidity()
|
const liquidities = await getMarketLiquidities()
|
||||||
const enabledAssets = getEnabledMarketAssets()
|
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 = liquidities.find((coin) => coin.denom === market.denom)?.amount ?? '0'
|
||||||
const asset = enabledAssets.find((asset) => asset.denom === market.denom)!
|
const asset = enabledAssets.find((asset) => asset.denom === market.denom)!
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -2,7 +2,7 @@ import { BN } from 'utils/helpers'
|
|||||||
import getMarketDeposits from 'api/markets/getMarketDeposits'
|
import getMarketDeposits from 'api/markets/getMarketDeposits'
|
||||||
import getMarketDebts from 'api/markets/getMarketDebts'
|
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 deposits = await getMarketDeposits()
|
||||||
const debts = await getMarketDebts()
|
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
|
title: string | React.ReactNode
|
||||||
sub: string | React.ReactNode
|
sub: string | React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
containerClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TitleAndSubCell(props: Props) {
|
export default function TitleAndSubCell(props: Props) {
|
||||||
return (
|
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}>
|
<Text size='sm' className={props.className}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</Text>
|
</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 Tab from 'components/Earn/Tab'
|
||||||
|
import LendingMarketsTable from 'components/Earn/Lend/LendingMarketsTable'
|
||||||
|
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
|
||||||
|
|
||||||
export default function LendPage() {
|
export default function LendPage() {
|
||||||
|
const { lentAssets, availableAssets } = useLendingMarketAssetsTableData()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tab />
|
<Tab />
|
||||||
<Card title='Lend'>
|
<LendingMarketsTable data={lentAssets} title='Lent Assets' />
|
||||||
<></>
|
<LendingMarketsTable data={availableAssets} title='Available Markets' />
|
||||||
</Card>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
11
src/types/interfaces/asset.d.ts
vendored
11
src/types/interfaces/asset.d.ts
vendored
@ -36,3 +36,14 @@ interface BigNumberCoin {
|
|||||||
denom: string
|
denom: string
|
||||||
amount: BigNumber
|
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
|
borrowEnabled: boolean
|
||||||
depositCap: string
|
depositCap: string
|
||||||
maxLtv: number
|
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)
|
.div(displayPrice.amount)
|
||||||
.toNumber()
|
.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,
|
borrowEnabled: response.borrow_enabled,
|
||||||
depositCap: response.deposit_cap,
|
depositCap: response.deposit_cap,
|
||||||
maxLtv: Number(response.max_loan_to_value),
|
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