MP-1227: Borrow Page (#24)
* added icon for atom and tokenInfo data update * borrow page initial commit * feat: borrow funds to ca and wallet * close borrow module on tx success * feat: repay funds initial setup * repay funds action hook * repay slider. module state on borrow page component * styling: minor tweak to text colors * limit manual input on repay to max value * borrow funds component slider initial * style: max button typography * AssetRow extracted to separate file. organize imports * ContainerSecondary component added * loading indicator for pending actions * style: progress bar colors * tanstack table added * tanstack react-table dependency missing * table cleanup and layout adjustments * fix account stats formula and update market data to match spreadsheet * calculate max borrow amount hook * reset borrow and repay components on account change * max borrow amount decimals. memorized return * hook tanstack data with real data * redefine borrowedAssetsMap to map * update max borrow amount formulas * remove unnecessary table component. refactor borrow table
This commit is contained in:
parent
53eca46b6c
commit
bbbdca6950
89
components/Borrow/AssetRow.tsx
Normal file
89
components/Borrow/AssetRow.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
import Button from 'components/Button'
|
||||||
|
import { formatCurrency } from 'utils/formatters'
|
||||||
|
|
||||||
|
type AssetRowProps = {
|
||||||
|
data: {
|
||||||
|
denom: string
|
||||||
|
symbol: string
|
||||||
|
icon: string
|
||||||
|
chain: string
|
||||||
|
borrowed: {
|
||||||
|
amount: number
|
||||||
|
value: number
|
||||||
|
} | null
|
||||||
|
borrowRate: number
|
||||||
|
marketLiquidity: number
|
||||||
|
}
|
||||||
|
onBorrowClick: () => void
|
||||||
|
onRepayClick: (value: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssetRow = ({ data, onBorrowClick, onRepayClick }: AssetRowProps) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="cursor-pointer rounded-md bg-[#D8DAEA] px-4 py-2 text-[#585A74] hover:bg-[#D8DAEA]/90"
|
||||||
|
onClick={() => setIsExpanded((current) => !current)}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex flex-1 items-center">
|
||||||
|
<Image src={data.icon} alt="token" width={32} height={32} />
|
||||||
|
<div className="pl-2">
|
||||||
|
<div>{data.symbol}</div>
|
||||||
|
<div className="text-xs">{data.chain}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center text-xs">
|
||||||
|
{data.borrowRate ? `${(data.borrowRate * 100).toFixed(2)}%` : '-'}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center text-xs">
|
||||||
|
{data.borrowed ? (
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">{data.borrowed.amount}</div>
|
||||||
|
<div>{formatCurrency(data.borrowed.value)}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center text-xs">{data.marketLiquidity}</div>
|
||||||
|
<div className="flex w-[50px] items-center justify-end">
|
||||||
|
{isExpanded ? <ChevronUpIcon className="w-5" /> : <ChevronDownIcon className="w-5" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>Additional Stuff Placeholder</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onBorrowClick()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Borrow
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!data.borrowed}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!data.borrowed) return
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
onRepayClick(data.borrowed.amount)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Repay
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssetRow
|
188
components/Borrow/BorrowFunds.tsx
Normal file
188
components/Borrow/BorrowFunds.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { XMarkIcon } from '@heroicons/react/24/solid'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import * as Slider from '@radix-ui/react-slider'
|
||||||
|
|
||||||
|
import Button from 'components/Button'
|
||||||
|
import Container from 'components/Container'
|
||||||
|
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||||
|
import useBorrowFunds from 'hooks/useBorrowFunds'
|
||||||
|
import useTokenPrices from 'hooks/useTokenPrices'
|
||||||
|
import { formatCurrency } from 'utils/formatters'
|
||||||
|
import { Switch } from '@headlessui/react'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import useAllBalances from 'hooks/useAllBalances'
|
||||||
|
import useMarkets from 'hooks/useMarkets'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import ContainerSecondary from 'components/ContainerSecondary'
|
||||||
|
import Spinner from 'components/Spinner'
|
||||||
|
import useCalculateMaxBorrowAmount from 'hooks/useCalculateMaxBorrowAmount'
|
||||||
|
|
||||||
|
const BorrowFunds = ({ tokenDenom, onClose }: any) => {
|
||||||
|
const [amount, setAmount] = useState(0)
|
||||||
|
const [borrowToCreditAccount, setBorrowToCreditAccount] = useState(false)
|
||||||
|
|
||||||
|
const tokenSymbol = getTokenSymbol(tokenDenom)
|
||||||
|
|
||||||
|
const { mutate, isLoading } = useBorrowFunds(amount, tokenDenom, !borrowToCreditAccount, {
|
||||||
|
onSuccess: () => {
|
||||||
|
onClose()
|
||||||
|
toast.success(`${amount} ${tokenSymbol} successfully Borrowed`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: tokenPrices } = useTokenPrices()
|
||||||
|
const { data: balancesData } = useAllBalances()
|
||||||
|
const { data: marketsData } = useMarkets()
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletAmount = useMemo(() => {
|
||||||
|
return BigNumber(balancesData?.find((balance) => balance.denom === tokenDenom)?.amount ?? 0)
|
||||||
|
.div(10 ** getTokenDecimals(tokenDenom))
|
||||||
|
.toNumber()
|
||||||
|
}, [balancesData, tokenDenom])
|
||||||
|
|
||||||
|
const tokenPrice = tokenPrices?.[tokenDenom] ?? 0
|
||||||
|
const borrowRate = Number(marketsData?.[tokenDenom].borrow_rate)
|
||||||
|
|
||||||
|
const maxValue = useCalculateMaxBorrowAmount(tokenDenom, borrowToCreditAccount)
|
||||||
|
|
||||||
|
const percentageValue = useMemo(() => {
|
||||||
|
if (isNaN(amount) || maxValue === 0) return 0
|
||||||
|
|
||||||
|
return (amount * 100) / maxValue
|
||||||
|
}, [amount, maxValue])
|
||||||
|
|
||||||
|
const isSubmitDisabled = !amount || amount < 0
|
||||||
|
|
||||||
|
const handleValueChange = (value: number) => {
|
||||||
|
if (value > maxValue) {
|
||||||
|
setAmount(maxValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAmount(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBorrowTargetChange = () => {
|
||||||
|
setBorrowToCreditAccount((c) => !c)
|
||||||
|
// reset amount due to max value calculations changing depending on borrow target
|
||||||
|
setAmount(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex w-[350px] flex-col justify-between text-sm">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="fixed inset-0 z-40 grid place-items-center bg-black/50">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mb-3 flex justify-between text-base font-bold">
|
||||||
|
<h3>Borrow {tokenSymbol}</h3>
|
||||||
|
<XMarkIcon className="w-5 cursor-pointer" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex flex-col gap-2">
|
||||||
|
<ContainerSecondary>
|
||||||
|
<p className="mb-2">
|
||||||
|
In wallet:{' '}
|
||||||
|
<span className="text-[#585A74]/50">
|
||||||
|
{walletAmount.toLocaleString()} {tokenSymbol}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="mb-5">
|
||||||
|
Borrow Rate: <span className="text-[#585A74]/50">{(borrowRate * 100).toFixed(2)}%</span>
|
||||||
|
</p>
|
||||||
|
<div className="mb-2 flex justify-between">
|
||||||
|
<div>Amount</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border border-black/50 bg-transparent px-2"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => handleValueChange(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
1 {tokenSymbol} ={' '}
|
||||||
|
<span className="text-[#585A74]/50">{formatCurrency(tokenPrice)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[#585A74]/50">{formatCurrency(tokenPrice * amount)}</div>
|
||||||
|
</div>
|
||||||
|
</ContainerSecondary>
|
||||||
|
<ContainerSecondary>
|
||||||
|
<div className="relative mb-4 flex flex-1 items-center">
|
||||||
|
<Slider.Root
|
||||||
|
className="relative flex h-[20px] w-full cursor-pointer touch-none select-none items-center"
|
||||||
|
value={[percentageValue]}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const decimal = value[0] / 100
|
||||||
|
const tokenDecimals = getTokenDecimals(tokenDenom)
|
||||||
|
// limit decimal precision based on token contract decimals
|
||||||
|
const newAmount = Number((decimal * maxValue).toFixed(tokenDecimals))
|
||||||
|
|
||||||
|
setAmount(newAmount)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Slider.Track className="relative h-[6px] grow rounded-full bg-gray-400">
|
||||||
|
<Slider.Range className="absolute h-[100%] rounded-full bg-blue-600" />
|
||||||
|
</Slider.Track>
|
||||||
|
<Slider.Thumb className="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-white !outline-none">
|
||||||
|
<div className="relative top-5 text-xs">{percentageValue.toFixed(0)}%</div>
|
||||||
|
</Slider.Thumb>
|
||||||
|
</Slider.Root>
|
||||||
|
<button
|
||||||
|
className="ml-4 rounded-md bg-blue-600 py-1 px-2 text-xs font-semibold text-white"
|
||||||
|
onClick={() => setAmount(maxValue)}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ContainerSecondary>
|
||||||
|
<ContainerSecondary className="flex items-center justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
Borrow to Credit Account{' '}
|
||||||
|
<Tooltip
|
||||||
|
className="ml-2"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p className="mb-2">
|
||||||
|
OFF = Borrow directly into your wallet by using your account Assets as
|
||||||
|
collateral. The borrowed asset will become a liability in your account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
ON = Borrow into your Account. The borrowed asset will be available in the
|
||||||
|
account as an Asset and appear also as a liability in your account.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={borrowToCreditAccount}
|
||||||
|
onChange={handleBorrowTargetChange}
|
||||||
|
className={`${
|
||||||
|
borrowToCreditAccount ? 'bg-blue-600' : 'bg-gray-400'
|
||||||
|
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
borrowToCreditAccount ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</ContainerSecondary>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" onClick={handleSubmit} disabled={isSubmitDisabled}>
|
||||||
|
Borrow
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BorrowFunds
|
197
components/Borrow/BorrowTable.tsx
Normal file
197
components/Borrow/BorrowTable.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
SortingState,
|
||||||
|
useReactTable,
|
||||||
|
} from '@tanstack/react-table'
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid'
|
||||||
|
|
||||||
|
import { formatCurrency } from 'utils/formatters'
|
||||||
|
import AssetRow from './AssetRow'
|
||||||
|
|
||||||
|
interface Market {
|
||||||
|
denom: string
|
||||||
|
symbol: string
|
||||||
|
icon: string
|
||||||
|
chain: string
|
||||||
|
borrowed: {
|
||||||
|
amount: number
|
||||||
|
value: number
|
||||||
|
} | null
|
||||||
|
borrowRate: number
|
||||||
|
marketLiquidity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// const data = [
|
||||||
|
// {
|
||||||
|
// denom: 'uosmo',
|
||||||
|
// symbol: 'OSMO',
|
||||||
|
// icon: '/tokens/osmo.svg',
|
||||||
|
// chain: 'Osmosis',
|
||||||
|
// borrowed: {
|
||||||
|
// amount: 2.005494,
|
||||||
|
// value: 2.2060434000000004,
|
||||||
|
// },
|
||||||
|
// borrowRate: 0.1,
|
||||||
|
// marketLiquidity: 1000000,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
||||||
|
// symbol: 'ATOM',
|
||||||
|
// icon: '/tokens/atom.svg',
|
||||||
|
// chain: 'Cosmos',
|
||||||
|
// borrowed: null,
|
||||||
|
// borrowRate: 0.25,
|
||||||
|
// marketLiquidity: 1000,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// denom: 'uusdc',
|
||||||
|
// symbol: 'USDC',
|
||||||
|
// icon: '/tokens/atom.svg',
|
||||||
|
// chain: 'Ethereum',
|
||||||
|
// borrowed: {
|
||||||
|
// amount: 100,
|
||||||
|
// value: 99.9776,
|
||||||
|
// },
|
||||||
|
// borrowRate: 0.35,
|
||||||
|
// marketLiquidity: 333,
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: Market[]
|
||||||
|
onBorrowClick: (denom: string) => void
|
||||||
|
onRepayClick: (denom: string, repayAmount: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BorrowTable = ({ data, onBorrowClick, onRepayClick }: Props) => {
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||||
|
|
||||||
|
const columns = React.useMemo<ColumnDef<Market>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
header: 'Asset',
|
||||||
|
id: 'symbol',
|
||||||
|
accessorFn: (row) => (
|
||||||
|
<div className="flex flex-1 items-center">
|
||||||
|
<Image src={row.icon} alt="token" width={32} height={32} />
|
||||||
|
<div className="pl-2">
|
||||||
|
<div>{row.symbol}</div>
|
||||||
|
<div className="text-xs">{row.chain}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'borrowRate',
|
||||||
|
header: 'Borrow Rate',
|
||||||
|
accessorFn: (row) => (
|
||||||
|
<div className="flex flex-1 items-center text-xs">
|
||||||
|
{row.borrowRate ? `${(row.borrowRate * 100).toFixed(2)}%` : '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'age',
|
||||||
|
header: 'Borrowed',
|
||||||
|
accessorFn: (row) => (
|
||||||
|
<div className="flex flex-1 items-center text-xs">
|
||||||
|
{row.borrowed ? (
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">{row.borrowed.amount}</div>
|
||||||
|
<div>{formatCurrency(row.borrowed.value)}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'marketLiquidity',
|
||||||
|
header: 'Liquidity Available',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
enableSorting: false,
|
||||||
|
header: 'Manage',
|
||||||
|
width: 150,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
{row.getIsExpanded() ? (
|
||||||
|
<ChevronUpIcon className="w-5" />
|
||||||
|
) : (
|
||||||
|
<ChevronDownIcon className="w-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
},
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
debugTable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full table-fixed border-spacing-10 text-sm">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<div
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="mb-2 flex rounded-md bg-[#D8DAEA] px-4 py-2 text-xs text-[#585A74]/50"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<div key={header.id} className={`${header.index === 4 ? 'w-[50px]' : 'flex-1'}`}>
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<div
|
||||||
|
{...{
|
||||||
|
className: header.column.getCanSort() ? 'cursor-pointer select-none' : '',
|
||||||
|
onClick: header.column.getToggleSortingHandler(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
{{
|
||||||
|
asc: ' 🔼',
|
||||||
|
desc: ' 🔽',
|
||||||
|
}[header.column.getIsSorted() as string] ?? null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{table.getRowModel().rows.map((row) => {
|
||||||
|
return (
|
||||||
|
<AssetRow
|
||||||
|
key={row.index}
|
||||||
|
data={row.original}
|
||||||
|
onBorrowClick={() => onBorrowClick(row.original.denom)}
|
||||||
|
onRepayClick={(repayAmount: number) => onRepayClick(row.original.denom, repayAmount)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BorrowTable
|
133
components/Borrow/RepayFunds.tsx
Normal file
133
components/Borrow/RepayFunds.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { XMarkIcon } from '@heroicons/react/24/solid'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import * as Slider from '@radix-ui/react-slider'
|
||||||
|
|
||||||
|
import Button from 'components/Button'
|
||||||
|
import Container from 'components/Container'
|
||||||
|
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||||
|
import useRepayFunds from 'hooks/useRepayFunds'
|
||||||
|
import useTokenPrices from 'hooks/useTokenPrices'
|
||||||
|
import { formatCurrency } from 'utils/formatters'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
import useAllBalances from 'hooks/useAllBalances'
|
||||||
|
import ContainerSecondary from 'components/ContainerSecondary'
|
||||||
|
import Spinner from 'components/Spinner'
|
||||||
|
|
||||||
|
const RepayFunds = ({ tokenDenom, amount: repayAmount, onClose }: any) => {
|
||||||
|
const [amount, setAmount] = useState(0)
|
||||||
|
|
||||||
|
const tokenSymbol = getTokenSymbol(tokenDenom)
|
||||||
|
|
||||||
|
const { mutate, isLoading } = useRepayFunds(amount, tokenDenom, {
|
||||||
|
onSuccess: () => {
|
||||||
|
onClose()
|
||||||
|
toast.success(`${amount} ${tokenSymbol} successfully repaid`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: tokenPrices } = useTokenPrices()
|
||||||
|
const { data: balancesData } = useAllBalances()
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletAmount = useMemo(() => {
|
||||||
|
return BigNumber(balancesData?.find((balance) => balance.denom === tokenDenom)?.amount ?? 0)
|
||||||
|
.div(10 ** getTokenDecimals(tokenDenom))
|
||||||
|
.toNumber()
|
||||||
|
}, [balancesData, tokenDenom])
|
||||||
|
|
||||||
|
const tokenPrice = tokenPrices?.[tokenDenom] ?? 0
|
||||||
|
|
||||||
|
const maxValue = walletAmount > repayAmount ? repayAmount : walletAmount
|
||||||
|
const percentageValue = isNaN(amount) ? 0 : (amount * 100) / maxValue
|
||||||
|
const isSubmitDisabled = !amount || amount < 0
|
||||||
|
|
||||||
|
const handleValueChange = (value: number) => {
|
||||||
|
if (value > maxValue) {
|
||||||
|
setAmount(maxValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAmount(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="flex w-[350px] flex-col justify-between text-sm">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="fixed inset-0 z-40 grid place-items-center bg-black/50">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mb-3 flex justify-between text-base font-bold">
|
||||||
|
<h3>Repay {tokenSymbol}</h3>
|
||||||
|
<XMarkIcon className="w-5 cursor-pointer" onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex flex-col gap-2">
|
||||||
|
<ContainerSecondary>
|
||||||
|
<p className="mb-2">
|
||||||
|
In wallet:{' '}
|
||||||
|
<span className="text-[#585A74]/50">
|
||||||
|
{walletAmount.toLocaleString()} {tokenSymbol}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div className="mb-2 flex justify-between">
|
||||||
|
<div>Amount</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="border border-black/50 bg-transparent px-2"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => handleValueChange(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div>
|
||||||
|
1 {tokenSymbol} ={' '}
|
||||||
|
<span className="text-[#585A74]/50">{formatCurrency(tokenPrice)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[#585A74]/50">{formatCurrency(tokenPrice * amount)}</div>
|
||||||
|
</div>
|
||||||
|
</ContainerSecondary>
|
||||||
|
<ContainerSecondary>
|
||||||
|
<div className="relative mb-4 flex flex-1 items-center">
|
||||||
|
<Slider.Root
|
||||||
|
className="relative flex h-[20px] w-full cursor-pointer touch-none select-none items-center"
|
||||||
|
value={[percentageValue]}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const decimal = value[0] / 100
|
||||||
|
const tokenDecimals = getTokenDecimals(tokenDenom)
|
||||||
|
// limit decimal precision based on token contract decimals
|
||||||
|
const newAmount = Number((decimal * maxValue).toFixed(tokenDecimals))
|
||||||
|
|
||||||
|
setAmount(newAmount)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Slider.Track className="relative h-[6px] grow rounded-full bg-gray-400">
|
||||||
|
<Slider.Range className="absolute h-[100%] rounded-full bg-blue-600" />
|
||||||
|
</Slider.Track>
|
||||||
|
<Slider.Thumb className="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-white !outline-none">
|
||||||
|
<div className="relative top-5 text-xs">{percentageValue.toFixed(0)}%</div>
|
||||||
|
</Slider.Thumb>
|
||||||
|
</Slider.Root>
|
||||||
|
<button
|
||||||
|
className="ml-4 rounded-md bg-blue-600 py-1 px-2 text-xs font-semibold text-white"
|
||||||
|
onClick={() => setAmount(maxValue)}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ContainerSecondary>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full" onClick={handleSubmit} disabled={isSubmitDisabled}>
|
||||||
|
Repay
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RepayFunds
|
3
components/Borrow/index.tsx
Normal file
3
components/Borrow/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as AssetRow } from './AssetRow'
|
||||||
|
export { default as BorrowFunds } from './BorrowFunds'
|
||||||
|
export { default as RepayFunds } from './RepayFunds'
|
@ -3,7 +3,7 @@ import React from 'react'
|
|||||||
type Props = {
|
type Props = {
|
||||||
children: string
|
children: string
|
||||||
className?: string
|
className?: string
|
||||||
onClick: () => void
|
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
components/ContainerSecondary.tsx
Normal file
17
components/ContainerSecondary.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const ContainerSecondary = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-md bg-[#D8DAEA] px-3 py-2 text-[#585A74] ${className}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContainerSecondary
|
@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
// move this component outside and probably adapt generic container component to different UI variants
|
|
||||||
export const CreditManagerContainer = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
}) => {
|
|
||||||
return <div className={`rounded-lg bg-[#D8DAEA] p-2 text-[#585A74] ${className}`}>{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreditManagerContainer
|
|
@ -10,7 +10,7 @@ import useDepositCreditAccount from 'hooks/useDepositCreditAccount'
|
|||||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
import useAllBalances from 'hooks/useAllBalances'
|
import useAllBalances from 'hooks/useAllBalances'
|
||||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||||
import CreditManagerContainer from './CreditManagerContainer'
|
import ContainerSecondary from 'components/ContainerSecondary'
|
||||||
|
|
||||||
const FundAccount = () => {
|
const FundAccount = () => {
|
||||||
const [amount, setAmount] = useState(0)
|
const [amount, setAmount] = useState(0)
|
||||||
@ -61,7 +61,7 @@ const FundAccount = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreditManagerContainer className="mb-2 p-3">
|
<ContainerSecondary className="mb-2 p-3">
|
||||||
<p className="mb-6 text-sm">
|
<p className="mb-6 text-sm">
|
||||||
Transfer assets from your injective wallet to your Mars credit account. If you don’t have
|
Transfer assets from your injective wallet to your Mars credit account. If you don’t have
|
||||||
any assets in your injective wallet use the injective bridge to transfer funds to your
|
any assets in your injective wallet use the injective bridge to transfer funds to your
|
||||||
@ -126,7 +126,7 @@ const FundAccount = () => {
|
|||||||
</Slider.Thumb>
|
</Slider.Thumb>
|
||||||
</Slider.Root>
|
</Slider.Root>
|
||||||
<button
|
<button
|
||||||
className="ml-4 rounded-md bg-blue-600 py-1 px-2 text-sm text-white"
|
className="ml-4 rounded-md bg-blue-600 py-1 px-2 text-xs font-semibold text-white"
|
||||||
onClick={() => setAmount(maxValue)}
|
onClick={() => setAmount(maxValue)}
|
||||||
>
|
>
|
||||||
MAX
|
MAX
|
||||||
@ -134,11 +134,11 @@ const FundAccount = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CreditManagerContainer>
|
</ContainerSecondary>
|
||||||
<CreditManagerContainer className="mb-2 flex items-center justify-between">
|
<ContainerSecondary className="mb-2 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold">Lending Assets</h3>
|
<h3 className="font-bold">Lending Assets</h3>
|
||||||
<div className="text-sm opacity-50">Lend assets from account to earn yield.</div>
|
<div className="text-sm text-[#585A74]/50">Lend assets from account to earn yield.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
@ -154,7 +154,7 @@ const FundAccount = () => {
|
|||||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</CreditManagerContainer>
|
</ContainerSecondary>
|
||||||
<Button
|
<Button
|
||||||
className="w-full !rounded-lg"
|
className="w-full !rounded-lg"
|
||||||
onClick={() => mutate()}
|
onClick={() => mutate()}
|
||||||
|
@ -8,10 +8,10 @@ import useWalletStore from 'stores/useWalletStore'
|
|||||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||||
import FundAccount from './FundAccount'
|
import FundAccount from './FundAccount'
|
||||||
import CreditManagerContainer from './CreditManagerContainer'
|
|
||||||
import useTokenPrices from 'hooks/useTokenPrices'
|
import useTokenPrices from 'hooks/useTokenPrices'
|
||||||
import useAccountStats from 'hooks/useAccountStats'
|
import useAccountStats from 'hooks/useAccountStats'
|
||||||
import useMarkets from 'hooks/useMarkets'
|
import useMarkets from 'hooks/useMarkets'
|
||||||
|
import ContainerSecondary from 'components/ContainerSecondary'
|
||||||
|
|
||||||
const CreditManager = () => {
|
const CreditManager = () => {
|
||||||
const [isFund, setIsFund] = useState(false)
|
const [isFund, setIsFund] = useState(false)
|
||||||
@ -41,14 +41,14 @@ const CreditManager = () => {
|
|||||||
if (!address) {
|
if (!address) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 left-auto w-[400px] border-l border-white/20 bg-background-2 p-2">
|
<div className="absolute inset-0 left-auto w-[400px] border-l border-white/20 bg-background-2 p-2">
|
||||||
<CreditManagerContainer>You must have a connected wallet</CreditManagerContainer>
|
<ContainerSecondary>You must have a connected wallet</ContainerSecondary>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 left-auto w-[400px] border-l border-white/20 bg-background-2 p-2">
|
<div className="absolute inset-0 left-auto w-[400px] border-l border-white/20 bg-background-2 p-2">
|
||||||
<CreditManagerContainer className="mb-2">
|
<ContainerSecondary className="mb-2">
|
||||||
{isFund ? (
|
{isFund ? (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-bold">Fund Account</h3>
|
<h3 className="font-bold">Fund Account</h3>
|
||||||
@ -66,12 +66,12 @@ const CreditManager = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CreditManagerContainer>
|
</ContainerSecondary>
|
||||||
{isFund ? (
|
{isFund ? (
|
||||||
<FundAccount />
|
<FundAccount />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CreditManagerContainer className="mb-2 text-sm">
|
<ContainerSecondary className="mb-2 text-sm">
|
||||||
<div className="mb-1 flex justify-between">
|
<div className="mb-1 flex justify-between">
|
||||||
<div>Total Position:</div>
|
<div>Total Position:</div>
|
||||||
<div className="font-semibold">
|
<div className="font-semibold">
|
||||||
@ -82,8 +82,8 @@ const CreditManager = () => {
|
|||||||
<div>Total Liabilities:</div>
|
<div>Total Liabilities:</div>
|
||||||
<div className="font-semibold">{formatCurrency(accountStats?.totalDebt ?? 0)}</div>
|
<div className="font-semibold">{formatCurrency(accountStats?.totalDebt ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
</CreditManagerContainer>
|
</ContainerSecondary>
|
||||||
<CreditManagerContainer>
|
<ContainerSecondary>
|
||||||
<h4 className="font-bold">Balances</h4>
|
<h4 className="font-bold">Balances</h4>
|
||||||
{isLoadingPositions ? (
|
{isLoadingPositions ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
@ -106,7 +106,7 @@ const CreditManager = () => {
|
|||||||
.div(10 ** getTokenDecimals(coin.denom))
|
.div(10 ** getTokenDecimals(coin.denom))
|
||||||
.toNumber()
|
.toNumber()
|
||||||
.toLocaleString(undefined, {
|
.toLocaleString(undefined, {
|
||||||
maximumFractionDigits: 6,
|
maximumFractionDigits: getTokenDecimals(coin.denom),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">-</div>
|
<div className="flex-1">-</div>
|
||||||
@ -134,7 +134,7 @@ const CreditManager = () => {
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CreditManagerContainer>
|
</ContainerSecondary>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,10 +7,17 @@ type Props = {
|
|||||||
const ProgressBar = ({ value }: Props) => {
|
const ProgressBar = ({ value }: Props) => {
|
||||||
const percentageValue = `${(value * 100).toFixed(0)}%`
|
const percentageValue = `${(value * 100).toFixed(0)}%`
|
||||||
|
|
||||||
|
let bgColorClass = 'bg-green-500'
|
||||||
|
if (value < 1 / 3) {
|
||||||
|
bgColorClass = 'bg-red-500'
|
||||||
|
} else if (value < 2 / 3) {
|
||||||
|
bgColorClass = 'bg-yellow-500'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-0 h-4 w-[130px] rounded-full bg-black">
|
<div className="relative z-0 h-4 w-[130px] rounded-full bg-black">
|
||||||
<div
|
<div
|
||||||
className="absolute z-10 h-4 rounded-full bg-green-500"
|
className={`absolute z-10 h-4 rounded-full ${bgColorClass}`}
|
||||||
style={{ width: percentageValue }}
|
style={{ width: percentageValue }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute z-20 flex w-full items-center justify-center gap-x-2 text-xs font-medium text-white">
|
<div className="absolute z-20 flex w-full items-center justify-center gap-x-2 text-xs font-medium text-white">
|
||||||
|
@ -50,6 +50,13 @@ const SemiCircleProgress = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let strokeColorClass = 'stroke-green-500'
|
||||||
|
if (value > 2 / 3) {
|
||||||
|
strokeColorClass = 'stroke-red-500'
|
||||||
|
} else if (value > 1 / 3) {
|
||||||
|
strokeColorClass = 'stroke-yellow-500'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="semicircle-container" style={{ position: 'relative' }}>
|
<div className="semicircle-container" style={{ position: 'relative' }}>
|
||||||
<svg
|
<svg
|
||||||
@ -70,11 +77,12 @@ const SemiCircleProgress = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
|
className={strokeColorClass}
|
||||||
cx={coordinateForCircle}
|
cx={coordinateForCircle}
|
||||||
cy={coordinateForCircle}
|
cy={coordinateForCircle}
|
||||||
r={radius}
|
r={radius}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={stroke}
|
// stroke={stroke}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
strokeDasharray={circumference}
|
strokeDasharray={circumference}
|
||||||
style={{
|
style={{
|
||||||
|
23
components/Tooltip.tsx
Normal file
23
components/Tooltip.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Tippy from '@tippyjs/react'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { InformationCircleIcon } from '@heroicons/react/24/solid'
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
content: string | ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tooltip = ({ content, className }: TooltipProps) => {
|
||||||
|
return (
|
||||||
|
<Tippy
|
||||||
|
appendTo={() => document.body}
|
||||||
|
className="rounded-md bg-[#ED512F] p-2 text-xs"
|
||||||
|
content={<span>{content}</span>}
|
||||||
|
interactive={true}
|
||||||
|
>
|
||||||
|
<InformationCircleIcon className={`w-5 cursor-pointer ${className}`} />
|
||||||
|
</Tippy>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tooltip
|
@ -2,6 +2,7 @@ type Token = {
|
|||||||
symbol: string
|
symbol: string
|
||||||
decimals: number
|
decimals: number
|
||||||
icon: string
|
icon: string
|
||||||
|
chain: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenInfo: { [key in string]: Token } = {
|
const tokenInfo: { [key in string]: Token } = {
|
||||||
@ -9,11 +10,13 @@ const tokenInfo: { [key in string]: Token } = {
|
|||||||
symbol: 'OSMO',
|
symbol: 'OSMO',
|
||||||
decimals: 6,
|
decimals: 6,
|
||||||
icon: '/tokens/osmo.svg',
|
icon: '/tokens/osmo.svg',
|
||||||
|
chain: 'Osmosis',
|
||||||
},
|
},
|
||||||
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
||||||
symbol: 'ATOM',
|
symbol: 'ATOM',
|
||||||
icon: '',
|
icon: '/tokens/atom.svg',
|
||||||
decimals: 6,
|
decimals: 6,
|
||||||
|
chain: 'Cosmos',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ const useAccountStats = () => {
|
|||||||
|
|
||||||
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => {
|
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => {
|
||||||
const tokenWeightedValue = BigNumber(getTokenTotalUSDValue(coin.amount, coin.denom)).times(
|
const tokenWeightedValue = BigNumber(getTokenTotalUSDValue(coin.amount, coin.denom)).times(
|
||||||
Number(marketsData[coin.denom].max_loan_to_value)
|
Number(marketsData[coin.denom].liquidation_threshold)
|
||||||
)
|
)
|
||||||
|
|
||||||
return tokenWeightedValue.plus(acc).toNumber()
|
return tokenWeightedValue.plus(acc).toNumber()
|
||||||
|
108
hooks/useBorrowFunds.tsx
Normal file
108
hooks/useBorrowFunds.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
|
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
|
import { chain } from 'utils/chains'
|
||||||
|
import { contractAddresses } from 'config/contracts'
|
||||||
|
import { hardcodedFee } from 'utils/contants'
|
||||||
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
|
import { queryKeys } from 'types/query-keys-factory'
|
||||||
|
|
||||||
|
const useBorrowFunds = (
|
||||||
|
amount: string | number,
|
||||||
|
denom: string,
|
||||||
|
withdraw = false,
|
||||||
|
options: Omit<UseMutationOptions, 'onError'>
|
||||||
|
) => {
|
||||||
|
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
|
||||||
|
|
||||||
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
|
const address = useWalletStore((s) => s.address)
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
if (!window.keplr) return
|
||||||
|
|
||||||
|
const offlineSigner = window.keplr.getOfflineSigner(chain.chainId)
|
||||||
|
const clientInstance = await SigningCosmWasmClient.connectWithSigner(chain.rpc, offlineSigner)
|
||||||
|
|
||||||
|
setSigningClient(clientInstance)
|
||||||
|
})()
|
||||||
|
}, [address])
|
||||||
|
|
||||||
|
const executeMsg = useMemo(() => {
|
||||||
|
if (!withdraw) {
|
||||||
|
return {
|
||||||
|
update_credit_account: {
|
||||||
|
account_id: selectedAccount,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
borrow: {
|
||||||
|
denom: denom,
|
||||||
|
amount: BigNumber(amount)
|
||||||
|
.times(10 ** 6)
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
update_credit_account: {
|
||||||
|
account_id: selectedAccount,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
borrow: {
|
||||||
|
denom: denom,
|
||||||
|
amount: BigNumber(amount)
|
||||||
|
.times(10 ** 6)
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withdraw: {
|
||||||
|
denom: denom,
|
||||||
|
amount: BigNumber(amount)
|
||||||
|
.times(10 ** 6)
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [amount, denom, withdraw, selectedAccount])
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
async () =>
|
||||||
|
await signingClient?.execute(
|
||||||
|
address,
|
||||||
|
contractAddresses.creditManager,
|
||||||
|
executeMsg,
|
||||||
|
hardcodedFee
|
||||||
|
),
|
||||||
|
{
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount ?? ''))
|
||||||
|
|
||||||
|
// if withdrawing to wallet, need to explicility invalidate balances queries
|
||||||
|
if (withdraw) {
|
||||||
|
queryClient.invalidateQueries(queryKeys.tokenBalance(address, denom))
|
||||||
|
queryClient.invalidateQueries(queryKeys.allBalances(address))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
toast.error(err.message)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useBorrowFunds
|
63
hooks/useCalculateMaxBorrowAmount.tsx
Normal file
63
hooks/useCalculateMaxBorrowAmount.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
|
import { getTokenDecimals } from 'utils/tokens'
|
||||||
|
import useCreditAccountPositions from './useCreditAccountPositions'
|
||||||
|
import useMarkets from './useMarkets'
|
||||||
|
import useTokenPrices from './useTokenPrices'
|
||||||
|
|
||||||
|
const useCalculateMaxBorrowAmount = (denom: string, isUnderCollateralized: boolean) => {
|
||||||
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
|
|
||||||
|
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
|
||||||
|
const { data: marketsData } = useMarkets()
|
||||||
|
const { data: tokenPrices } = useTokenPrices()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!marketsData || !tokenPrices || !positionsData) return 0
|
||||||
|
|
||||||
|
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||||
|
// early return if prices are not fetched yet
|
||||||
|
if (!tokenPrices) return 0
|
||||||
|
|
||||||
|
return BigNumber(amount)
|
||||||
|
.div(10 ** getTokenDecimals(denom))
|
||||||
|
.times(tokenPrices[denom])
|
||||||
|
.toNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalWeightedPositions = positionsData?.coins.reduce((acc, coin) => {
|
||||||
|
const tokenWeightedValue = BigNumber(getTokenTotalUSDValue(coin.amount, coin.denom)).times(
|
||||||
|
Number(marketsData[coin.denom].max_loan_to_value)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tokenWeightedValue.plus(acc).toNumber()
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalLiabilitiesValue = positionsData?.debts.reduce((acc, coin) => {
|
||||||
|
const tokenUSDValue = BigNumber(getTokenTotalUSDValue(coin.amount, coin.denom))
|
||||||
|
|
||||||
|
return tokenUSDValue.plus(acc).toNumber()
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const borrowTokenPrice = tokenPrices[denom]
|
||||||
|
const tokenDecimals = getTokenDecimals(denom)
|
||||||
|
|
||||||
|
if (isUnderCollateralized) {
|
||||||
|
return BigNumber(totalLiabilitiesValue)
|
||||||
|
.minus(totalWeightedPositions)
|
||||||
|
.div(borrowTokenPrice * Number(marketsData[denom].max_loan_to_value) - borrowTokenPrice)
|
||||||
|
.decimalPlaces(tokenDecimals)
|
||||||
|
.toNumber()
|
||||||
|
} else {
|
||||||
|
return BigNumber(totalWeightedPositions)
|
||||||
|
.minus(totalLiabilitiesValue)
|
||||||
|
.div(borrowTokenPrice)
|
||||||
|
.decimalPlaces(tokenDecimals)
|
||||||
|
.toNumber()
|
||||||
|
}
|
||||||
|
}, [denom, isUnderCollateralized, marketsData, positionsData, tokenPrices])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCalculateMaxBorrowAmount
|
@ -38,8 +38,8 @@ const useMarkets = () => {
|
|||||||
wasm: {
|
wasm: {
|
||||||
uosmo: {
|
uosmo: {
|
||||||
denom: 'uosmo',
|
denom: 'uosmo',
|
||||||
max_loan_to_value: '0.7',
|
max_loan_to_value: '0.65',
|
||||||
liquidation_threshold: '0.65',
|
liquidation_threshold: '0.7',
|
||||||
liquidation_bonus: '0.1',
|
liquidation_bonus: '0.1',
|
||||||
reserve_factor: '0.2',
|
reserve_factor: '0.2',
|
||||||
interest_rate_model: {
|
interest_rate_model: {
|
||||||
@ -61,8 +61,8 @@ const useMarkets = () => {
|
|||||||
},
|
},
|
||||||
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
||||||
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
||||||
max_loan_to_value: '0.8',
|
max_loan_to_value: '0.77',
|
||||||
liquidation_threshold: '0.7',
|
liquidation_threshold: '0.8',
|
||||||
liquidation_bonus: '0.1',
|
liquidation_bonus: '0.1',
|
||||||
reserve_factor: '0.2',
|
reserve_factor: '0.2',
|
||||||
interest_rate_model: {
|
interest_rate_model: {
|
||||||
|
77
hooks/useRepayFunds.tsx
Normal file
77
hooks/useRepayFunds.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
|
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
|
import { chain } from 'utils/chains'
|
||||||
|
import { contractAddresses } from 'config/contracts'
|
||||||
|
import { hardcodedFee } from 'utils/contants'
|
||||||
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
|
import { queryKeys } from 'types/query-keys-factory'
|
||||||
|
|
||||||
|
const useRepayFunds = (
|
||||||
|
amount: string | number,
|
||||||
|
denom: string,
|
||||||
|
options: Omit<UseMutationOptions, 'onError'>
|
||||||
|
) => {
|
||||||
|
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
|
||||||
|
|
||||||
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
|
const address = useWalletStore((s) => s.address)
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
if (!window.keplr) return
|
||||||
|
|
||||||
|
const offlineSigner = window.keplr.getOfflineSigner(chain.chainId)
|
||||||
|
const clientInstance = await SigningCosmWasmClient.connectWithSigner(chain.rpc, offlineSigner)
|
||||||
|
|
||||||
|
setSigningClient(clientInstance)
|
||||||
|
})()
|
||||||
|
}, [address])
|
||||||
|
|
||||||
|
const executeMsg = useMemo(() => {
|
||||||
|
return {
|
||||||
|
update_credit_account: {
|
||||||
|
account_id: selectedAccount,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
repay: {
|
||||||
|
denom: denom,
|
||||||
|
amount: BigNumber(amount)
|
||||||
|
.times(10 ** 6)
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [amount, denom, selectedAccount])
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
async () =>
|
||||||
|
await signingClient?.execute(
|
||||||
|
address,
|
||||||
|
contractAddresses.creditManager,
|
||||||
|
executeMsg,
|
||||||
|
hardcodedFee
|
||||||
|
),
|
||||||
|
{
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount ?? ''))
|
||||||
|
queryClient.invalidateQueries(queryKeys.tokenBalance(address, denom))
|
||||||
|
queryClient.invalidateQueries(queryKeys.allBalances(address))
|
||||||
|
},
|
||||||
|
onError: (err: Error) => {
|
||||||
|
toast.error(err.message)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useRepayFunds
|
@ -18,6 +18,8 @@
|
|||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@sentry/nextjs": "^7.12.1",
|
"@sentry/nextjs": "^7.12.1",
|
||||||
"@tanstack/react-query": "^4.3.4",
|
"@tanstack/react-query": "^4.3.4",
|
||||||
|
"@tanstack/react-table": "^8.5.15",
|
||||||
|
"@tippyjs/react": "^4.2.6",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"bignumber.js": "^9.1.0",
|
"bignumber.js": "^9.1.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
|
199
pages/borrow.tsx
199
pages/borrow.tsx
@ -1,69 +1,152 @@
|
|||||||
import React from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import Image from 'next/image'
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
import Container from 'components/Container'
|
import Container from 'components/Container'
|
||||||
|
import useAllowedCoins from 'hooks/useAllowedCoins'
|
||||||
|
import { getTokenDecimals, getTokenInfo } from 'utils/tokens'
|
||||||
|
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||||
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
|
import useMarkets from 'hooks/useMarkets'
|
||||||
|
import useTokenPrices from 'hooks/useTokenPrices'
|
||||||
|
import { BorrowFunds, RepayFunds } from 'components/Borrow'
|
||||||
|
import BorrowTable from 'components/Borrow/BorrowTable'
|
||||||
|
|
||||||
const AssetRow = () => {
|
type ModuleState =
|
||||||
return (
|
| {
|
||||||
<div className="flex rounded-md bg-[#D8DAEA] p-2 text-[#585A74]">
|
show: 'borrow'
|
||||||
<div className="flex flex-1">
|
data: {
|
||||||
<Image src="/tokens/osmo.svg" alt="token" width={24} height={24} />
|
tokenDenom: string
|
||||||
<div className="pl-2">
|
}
|
||||||
<div>DENOM</div>
|
}
|
||||||
<div className="text-xs">Name</div>
|
| {
|
||||||
</div>
|
show: 'repay'
|
||||||
</div>
|
data: {
|
||||||
<div className="flex-1">10.00%</div>
|
tokenDenom: string
|
||||||
<div className="flex-1">
|
amount: number
|
||||||
<div>Amount</div>
|
}
|
||||||
<div>Value</div>
|
}
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div>Amount</div>
|
|
||||||
<div>Value</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-[50px]">ACTION</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Borrow = () => {
|
const Borrow = () => {
|
||||||
|
const [moduleState, setModuleState] = useState<ModuleState | null>(null)
|
||||||
|
|
||||||
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
|
|
||||||
|
const { data: allowedCoinsData } = useAllowedCoins()
|
||||||
|
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
|
||||||
|
const { data: marketsData } = useMarkets()
|
||||||
|
const { data: tokenPrices } = useTokenPrices()
|
||||||
|
|
||||||
|
const borrowedAssetsMap = useMemo(() => {
|
||||||
|
let borrowedAssetsMap: Map<string, string> = new Map()
|
||||||
|
|
||||||
|
positionsData?.debts.forEach((coin) => {
|
||||||
|
borrowedAssetsMap.set(coin.denom, coin.amount)
|
||||||
|
})
|
||||||
|
|
||||||
|
return borrowedAssetsMap
|
||||||
|
}, [positionsData])
|
||||||
|
|
||||||
|
const { borrowedAssets, notBorrowedAssets } = useMemo(() => {
|
||||||
|
return {
|
||||||
|
borrowedAssets:
|
||||||
|
allowedCoinsData
|
||||||
|
?.filter((denom) => borrowedAssetsMap.has(denom))
|
||||||
|
.map((denom) => {
|
||||||
|
const { symbol, chain, icon } = getTokenInfo(denom)
|
||||||
|
const borrowRate = Number(marketsData?.[denom].borrow_rate) || 0
|
||||||
|
const marketLiquidity = BigNumber(marketsData?.[denom].deposit_cap ?? '')
|
||||||
|
.div(10 ** getTokenDecimals(denom))
|
||||||
|
.toNumber()
|
||||||
|
|
||||||
|
const borrowAmount = BigNumber(borrowedAssetsMap.get(denom) as string)
|
||||||
|
.div(10 ** getTokenDecimals(denom))
|
||||||
|
.toNumber()
|
||||||
|
const borrowValue = borrowAmount * (tokenPrices?.[denom] ?? 0)
|
||||||
|
|
||||||
|
const rowData = {
|
||||||
|
denom,
|
||||||
|
symbol,
|
||||||
|
icon,
|
||||||
|
chain,
|
||||||
|
borrowed: {
|
||||||
|
amount: borrowAmount,
|
||||||
|
value: borrowValue,
|
||||||
|
},
|
||||||
|
borrowRate,
|
||||||
|
marketLiquidity,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowData
|
||||||
|
}) ?? [],
|
||||||
|
notBorrowedAssets:
|
||||||
|
allowedCoinsData
|
||||||
|
?.filter((denom) => !borrowedAssetsMap.has(denom))
|
||||||
|
.map((denom) => {
|
||||||
|
const { symbol, chain, icon } = getTokenInfo(denom)
|
||||||
|
const borrowRate = Number(marketsData?.[denom].borrow_rate) || 0
|
||||||
|
const marketLiquidity = BigNumber(marketsData?.[denom].deposit_cap ?? '')
|
||||||
|
.div(10 ** getTokenDecimals(denom))
|
||||||
|
.toNumber()
|
||||||
|
|
||||||
|
const rowData = {
|
||||||
|
denom,
|
||||||
|
symbol,
|
||||||
|
icon,
|
||||||
|
chain,
|
||||||
|
borrowed: null,
|
||||||
|
borrowRate,
|
||||||
|
marketLiquidity,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowData
|
||||||
|
}) ?? [],
|
||||||
|
}
|
||||||
|
}, [allowedCoinsData, borrowedAssetsMap, marketsData, tokenPrices])
|
||||||
|
|
||||||
|
const handleBorrowClick = (denom: string) => {
|
||||||
|
setModuleState({ show: 'borrow', data: { tokenDenom: denom } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRepayClick = (denom: string, repayAmount: number) => {
|
||||||
|
setModuleState({ show: 'repay', data: { tokenDenom: denom, amount: repayAmount } })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<Container className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="mb-5">
|
<Container>
|
||||||
<h3 className="mb-1 text-center font-medium uppercase">Borrowed</h3>
|
<div className="mb-5">
|
||||||
<div className="mb-2 flex rounded-md bg-[#D8DAEA] p-2 text-sm text-[#585A74]/50">
|
<h3 className="mb-1 text-center font-medium uppercase">Borrowed</h3>
|
||||||
<div className="flex-1">Asset</div>
|
<BorrowTable
|
||||||
<div className="flex-1">Borrow Rate</div>
|
data={borrowedAssets}
|
||||||
<div className="flex-1">Borrowed</div>
|
onBorrowClick={handleBorrowClick}
|
||||||
<div className="flex-1">Liquidity Available</div>
|
onRepayClick={handleRepayClick}
|
||||||
<div className="w-[50px]">Manage</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div>
|
||||||
{Array.from(Array(3).keys()).map(() => (
|
<h3 className="mb-1 text-center font-medium uppercase">Not Borrowed Yet</h3>
|
||||||
// eslint-disable-next-line react/jsx-key
|
<BorrowTable
|
||||||
<AssetRow />
|
data={notBorrowedAssets}
|
||||||
))}
|
onBorrowClick={handleBorrowClick}
|
||||||
|
onRepayClick={handleRepayClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
<div>
|
</div>
|
||||||
<h3 className="mb-1 text-center font-medium uppercase">Not Borrowed Yet</h3>
|
{moduleState?.show === 'borrow' && (
|
||||||
<div className="mb-2 flex rounded-md bg-[#D8DAEA] p-2 text-sm text-[#585A74]/50">
|
<BorrowFunds
|
||||||
<div className="flex-1">Asset</div>
|
key={`borrow_${selectedAccount}_${moduleState.data.tokenDenom}`}
|
||||||
<div className="flex-1">Borrow Rate</div>
|
{...moduleState.data}
|
||||||
<div className="flex-1">Borrowed</div>
|
onClose={() => setModuleState(null)}
|
||||||
<div className="flex-1">Liquidity Available</div>
|
/>
|
||||||
<div className="w-[50px]">Manage</div>
|
)}
|
||||||
</div>
|
{moduleState?.show === 'repay' && (
|
||||||
<div className="flex flex-col gap-2">
|
<RepayFunds
|
||||||
{Array.from(Array(5).keys()).map(() => (
|
key={`repay_${selectedAccount}_${moduleState.data.tokenDenom}`}
|
||||||
// eslint-disable-next-line react/jsx-key
|
{...moduleState.data}
|
||||||
<AssetRow />
|
onClose={() => setModuleState(null)}
|
||||||
))}
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
44
public/tokens/atom.svg
Normal file
44
public/tokens/atom.svg
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<svg
|
||||||
|
id='Layer_1'
|
||||||
|
data-name='Layer 1'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 2500 2500'
|
||||||
|
>
|
||||||
|
<title>cosmos-atom-logo</title>
|
||||||
|
<circle cx='1250' cy='1250' r='1250' style='fill:#2e3148' />
|
||||||
|
<circle cx='1250' cy='1250' r='725.31' style='fill:#1b1e36' />
|
||||||
|
<path
|
||||||
|
d='M1252.57,159.47c-134.93,0-244.34,489.4-244.34,1093.11s109.41,1093.11,244.34,1093.11,244.34-489.4,244.34-1093.11S1387.5,159.47,1252.57,159.47ZM1269.44,2284c-15.43,20.58-30.86,5.14-30.86,5.14-62.14-72-93.21-205.76-93.21-205.76-108.69-349.79-82.82-1100.82-82.82-1100.82,51.08-596.24,144-737.09,175.62-768.36a19.29,19.29,0,0,1,24.74-2c45.88,32.51,84.36,168.47,84.36,168.47,113.63,421.81,103.34,817.9,103.34,817.9,10.29,344.65-56.94,730.45-56.94,730.45C1341.92,2222.22,1269.44,2284,1269.44,2284Z'
|
||||||
|
style='fill:#6f7390'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d='M2200.72,708.59c-67.18-117.08-546.09,31.58-1070,332s-893.47,638.89-826.34,755.92,546.09-31.58,1070-332,893.47-638.89,826.34-755.92h0ZM366.36,1780.45c-25.72-3.24-19.91-24.38-19.91-24.38C378,1666.36,478.4,1572.84,478.4,1572.84c249.43-268.36,913.79-619.65,913.79-619.65,542.54-252.42,711.06-241.77,753.81-230a19.29,19.29,0,0,1,14,20.58c-5.14,56-104.17,157-104.17,157C1746.71,1209.36,1398,1397.58,1398,1397.58c-293.83,180.5-661.93,314.09-661.93,314.09-280.09,100.93-369.7,68.78-369.7,68.78h0Z'
|
||||||
|
style='fill:#6f7390'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d='M2198.35,1800.41c67.7-116.77-300.93-456.79-823-759.47S374.43,587.76,306.79,704.73s300.93,456.79,823.3,759.47S2130.71,1917.39,2198.35,1800.41ZM351.65,749.85c-10-23.71,11.11-29.42,11.11-29.42C456.22,702.78,587.5,743,587.5,743c357.15,81.33,994,480.25,994,480.25,490.33,343.11,565.53,494.24,576.8,537.14a19.29,19.29,0,0,1-10.7,22.43c-51.13,23.41-188.07-11.47-188.07-11.47-422.07-113.17-759.62-320.52-759.62-320.52-303.29-163.58-603.19-415.28-603.19-415.28-227.88-191.87-245-285.44-245-285.44Z'
|
||||||
|
style='fill:#6f7390'
|
||||||
|
/>
|
||||||
|
<circle cx='1250' cy='1250' r='128.6' style='fill:#b7b9c8' />
|
||||||
|
<ellipse
|
||||||
|
cx='1777.26'
|
||||||
|
cy='756.17'
|
||||||
|
rx='74.59'
|
||||||
|
ry='77.16'
|
||||||
|
style='fill:#b7b9c8'
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx='552.98'
|
||||||
|
cy='1018.52'
|
||||||
|
rx='74.59'
|
||||||
|
ry='77.16'
|
||||||
|
style='fill:#b7b9c8'
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx='1098.25'
|
||||||
|
cy='1965.02'
|
||||||
|
rx='74.59'
|
||||||
|
ry='77.16'
|
||||||
|
style='fill:#b7b9c8'
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -7,3 +7,11 @@ export const getTokenSymbol = (denom: string) => {
|
|||||||
export const getTokenDecimals = (denom: string) => {
|
export const getTokenDecimals = (denom: string) => {
|
||||||
return tokenInfo[denom]?.decimals ?? 6
|
return tokenInfo[denom]?.decimals ?? 6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTokenIcon = (denom: string) => {
|
||||||
|
return tokenInfo[denom].icon
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTokenInfo = (denom: string) => {
|
||||||
|
return tokenInfo[denom]
|
||||||
|
}
|
||||||
|
31
yarn.lock
31
yarn.lock
@ -1704,6 +1704,11 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@popperjs/core@^2.9.0":
|
||||||
|
version "2.11.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||||
|
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||||
|
|
||||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||||
@ -2148,6 +2153,25 @@
|
|||||||
"@tanstack/query-core" "4.3.4"
|
"@tanstack/query-core" "4.3.4"
|
||||||
use-sync-external-store "^1.2.0"
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
|
"@tanstack/react-table@^8.5.15":
|
||||||
|
version "8.5.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.5.15.tgz#8179d24d7fdf909799a517e8897501c44e51284d"
|
||||||
|
integrity sha512-9rSvhIFeMpfXksFgQNTWnVoJbkae/U8CkHnHYGWAIB/O0Ca51IKap0Rjp5WkIUVBWxJ7Wfl2y13oY+aWcyM6Rg==
|
||||||
|
dependencies:
|
||||||
|
"@tanstack/table-core" "8.5.15"
|
||||||
|
|
||||||
|
"@tanstack/table-core@8.5.15":
|
||||||
|
version "8.5.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.5.15.tgz#e1e674135cd6c36f29a1562a2b846f824861149b"
|
||||||
|
integrity sha512-k+BcCOAYD610Cij6p1BPyEqjMQjZIdAnVDoIUKVnA/tfHbF4JlDP7pKAftXPBxyyX5Z1yQPurPnOdEY007Snyg==
|
||||||
|
|
||||||
|
"@tippyjs/react@^4.2.6":
|
||||||
|
version "4.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71"
|
||||||
|
integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==
|
||||||
|
dependencies:
|
||||||
|
tippy.js "^6.3.1"
|
||||||
|
|
||||||
"@trysound/sax@0.2.0":
|
"@trysound/sax@0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||||
@ -6037,6 +6061,13 @@ tiny-secp256k1@^1.1.3:
|
|||||||
elliptic "^6.4.0"
|
elliptic "^6.4.0"
|
||||||
nan "^2.13.2"
|
nan "^2.13.2"
|
||||||
|
|
||||||
|
tippy.js@^6.3.1:
|
||||||
|
version "6.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
|
||||||
|
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
|
||||||
|
dependencies:
|
||||||
|
"@popperjs/core" "^2.9.0"
|
||||||
|
|
||||||
to-fast-properties@^2.0.0:
|
to-fast-properties@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user