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:
Gustavo Mauricio 2022-10-20 16:39:21 +01:00 committed by GitHub
parent 53eca46b6c
commit bbbdca6950
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1167 additions and 97 deletions

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,3 @@
export { default as AssetRow } from './AssetRow'
export { default as BorrowFunds } from './BorrowFunds'
export { default as RepayFunds } from './RepayFunds'

View File

@ -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
} }

View 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

View File

@ -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

View File

@ -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 dont have Transfer assets from your injective wallet to your Mars credit account. If you dont 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()}

View File

@ -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>

View File

@ -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">

View File

@ -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
View 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

View File

@ -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',
}, },
} }

View File

@ -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
View 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

View 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

View File

@ -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
View 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

View File

@ -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",

View File

@ -1,70 +1,153 @@
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">
<Container>
<div className="mb-5"> <div className="mb-5">
<h3 className="mb-1 text-center font-medium uppercase">Borrowed</h3> <h3 className="mb-1 text-center font-medium uppercase">Borrowed</h3>
<div className="mb-2 flex rounded-md bg-[#D8DAEA] p-2 text-sm text-[#585A74]/50"> <BorrowTable
<div className="flex-1">Asset</div> data={borrowedAssets}
<div className="flex-1">Borrow Rate</div> onBorrowClick={handleBorrowClick}
<div className="flex-1">Borrowed</div> onRepayClick={handleRepayClick}
<div className="flex-1">Liquidity Available</div> />
<div className="w-[50px]">Manage</div>
</div>
<div className="flex flex-col gap-2">
{Array.from(Array(3).keys()).map(() => (
// eslint-disable-next-line react/jsx-key
<AssetRow />
))}
</div>
</div> </div>
<div> <div>
<h3 className="mb-1 text-center font-medium uppercase">Not Borrowed Yet</h3> <h3 className="mb-1 text-center font-medium uppercase">Not Borrowed Yet</h3>
<div className="mb-2 flex rounded-md bg-[#D8DAEA] p-2 text-sm text-[#585A74]/50"> <BorrowTable
<div className="flex-1">Asset</div> data={notBorrowedAssets}
<div className="flex-1">Borrow Rate</div> onBorrowClick={handleBorrowClick}
<div className="flex-1">Borrowed</div> onRepayClick={handleRepayClick}
<div className="flex-1">Liquidity Available</div> />
<div className="w-[50px]">Manage</div>
</div>
<div className="flex flex-col gap-2">
{Array.from(Array(5).keys()).map(() => (
// eslint-disable-next-line react/jsx-key
<AssetRow />
))}
</div>
</div> </div>
</Container> </Container>
</div> </div>
{moduleState?.show === 'borrow' && (
<BorrowFunds
key={`borrow_${selectedAccount}_${moduleState.data.tokenDenom}`}
{...moduleState.data}
onClose={() => setModuleState(null)}
/>
)}
{moduleState?.show === 'repay' && (
<RepayFunds
key={`repay_${selectedAccount}_${moduleState.data.tokenDenom}`}
{...moduleState.data}
onClose={() => setModuleState(null)}
/>
)}
</div>
) )
} }

44
public/tokens/atom.svg Normal file
View 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

View File

@ -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]
}

View File

@ -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"