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 = {
|
||||
children: string
|
||||
className?: string
|
||||
onClick: () => void
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
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 useAllBalances from 'hooks/useAllBalances'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
import CreditManagerContainer from './CreditManagerContainer'
|
||||
import ContainerSecondary from 'components/ContainerSecondary'
|
||||
|
||||
const FundAccount = () => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
@ -61,7 +61,7 @@ const FundAccount = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreditManagerContainer className="mb-2 p-3">
|
||||
<ContainerSecondary className="mb-2 p-3">
|
||||
<p className="mb-6 text-sm">
|
||||
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
|
||||
@ -126,7 +126,7 @@ const FundAccount = () => {
|
||||
</Slider.Thumb>
|
||||
</Slider.Root>
|
||||
<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)}
|
||||
>
|
||||
MAX
|
||||
@ -134,11 +134,11 @@ const FundAccount = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CreditManagerContainer>
|
||||
<CreditManagerContainer className="mb-2 flex items-center justify-between">
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary className="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<Switch
|
||||
@ -154,7 +154,7 @@ const FundAccount = () => {
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
</Switch>
|
||||
</CreditManagerContainer>
|
||||
</ContainerSecondary>
|
||||
<Button
|
||||
className="w-full !rounded-lg"
|
||||
onClick={() => mutate()}
|
||||
|
@ -8,10 +8,10 @@ import useWalletStore from 'stores/useWalletStore'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
import FundAccount from './FundAccount'
|
||||
import CreditManagerContainer from './CreditManagerContainer'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useAccountStats from 'hooks/useAccountStats'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import ContainerSecondary from 'components/ContainerSecondary'
|
||||
|
||||
const CreditManager = () => {
|
||||
const [isFund, setIsFund] = useState(false)
|
||||
@ -41,14 +41,14 @@ const CreditManager = () => {
|
||||
if (!address) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold">Fund Account</h3>
|
||||
@ -66,12 +66,12 @@ const CreditManager = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CreditManagerContainer>
|
||||
</ContainerSecondary>
|
||||
{isFund ? (
|
||||
<FundAccount />
|
||||
) : (
|
||||
<>
|
||||
<CreditManagerContainer className="mb-2 text-sm">
|
||||
<ContainerSecondary className="mb-2 text-sm">
|
||||
<div className="mb-1 flex justify-between">
|
||||
<div>Total Position:</div>
|
||||
<div className="font-semibold">
|
||||
@ -82,8 +82,8 @@ const CreditManager = () => {
|
||||
<div>Total Liabilities:</div>
|
||||
<div className="font-semibold">{formatCurrency(accountStats?.totalDebt ?? 0)}</div>
|
||||
</div>
|
||||
</CreditManagerContainer>
|
||||
<CreditManagerContainer>
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary>
|
||||
<h4 className="font-bold">Balances</h4>
|
||||
{isLoadingPositions ? (
|
||||
<div>Loading...</div>
|
||||
@ -106,7 +106,7 @@ const CreditManager = () => {
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 6,
|
||||
maximumFractionDigits: getTokenDecimals(coin.denom),
|
||||
})}
|
||||
</div>
|
||||
<div className="flex-1">-</div>
|
||||
@ -134,7 +134,7 @@ const CreditManager = () => {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CreditManagerContainer>
|
||||
</ContainerSecondary>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -7,10 +7,17 @@ type Props = {
|
||||
const ProgressBar = ({ value }: Props) => {
|
||||
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 (
|
||||
<div className="relative z-0 h-4 w-[130px] rounded-full bg-black">
|
||||
<div
|
||||
className="absolute z-10 h-4 rounded-full bg-green-500"
|
||||
className={`absolute z-10 h-4 rounded-full ${bgColorClass}`}
|
||||
style={{ width: percentageValue }}
|
||||
/>
|
||||
<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 (
|
||||
<div className="semicircle-container" style={{ position: 'relative' }}>
|
||||
<svg
|
||||
@ -70,11 +77,12 @@ const SemiCircleProgress = ({
|
||||
}}
|
||||
/>
|
||||
<circle
|
||||
className={strokeColorClass}
|
||||
cx={coordinateForCircle}
|
||||
cy={coordinateForCircle}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={stroke}
|
||||
// stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
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
|
||||
decimals: number
|
||||
icon: string
|
||||
chain: string
|
||||
}
|
||||
|
||||
const tokenInfo: { [key in string]: Token } = {
|
||||
@ -9,11 +10,13 @@ const tokenInfo: { [key in string]: Token } = {
|
||||
symbol: 'OSMO',
|
||||
decimals: 6,
|
||||
icon: '/tokens/osmo.svg',
|
||||
chain: 'Osmosis',
|
||||
},
|
||||
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
||||
symbol: 'ATOM',
|
||||
icon: '',
|
||||
icon: '/tokens/atom.svg',
|
||||
decimals: 6,
|
||||
chain: 'Cosmos',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ const useAccountStats = () => {
|
||||
|
||||
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => {
|
||||
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()
|
||||
|
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: {
|
||||
uosmo: {
|
||||
denom: 'uosmo',
|
||||
max_loan_to_value: '0.7',
|
||||
liquidation_threshold: '0.65',
|
||||
max_loan_to_value: '0.65',
|
||||
liquidation_threshold: '0.7',
|
||||
liquidation_bonus: '0.1',
|
||||
reserve_factor: '0.2',
|
||||
interest_rate_model: {
|
||||
@ -61,8 +61,8 @@ const useMarkets = () => {
|
||||
},
|
||||
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
||||
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
||||
max_loan_to_value: '0.8',
|
||||
liquidation_threshold: '0.7',
|
||||
max_loan_to_value: '0.77',
|
||||
liquidation_threshold: '0.8',
|
||||
liquidation_bonus: '0.1',
|
||||
reserve_factor: '0.2',
|
||||
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",
|
||||
"@sentry/nextjs": "^7.12.1",
|
||||
"@tanstack/react-query": "^4.3.4",
|
||||
"@tanstack/react-table": "^8.5.15",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"bech32": "^2.0.0",
|
||||
"bignumber.js": "^9.1.0",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
|
199
pages/borrow.tsx
199
pages/borrow.tsx
@ -1,69 +1,152 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
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 = () => {
|
||||
return (
|
||||
<div className="flex rounded-md bg-[#D8DAEA] p-2 text-[#585A74]">
|
||||
<div className="flex flex-1">
|
||||
<Image src="/tokens/osmo.svg" alt="token" width={24} height={24} />
|
||||
<div className="pl-2">
|
||||
<div>DENOM</div>
|
||||
<div className="text-xs">Name</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">10.00%</div>
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
type ModuleState =
|
||||
| {
|
||||
show: 'borrow'
|
||||
data: {
|
||||
tokenDenom: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
show: 'repay'
|
||||
data: {
|
||||
tokenDenom: string
|
||||
amount: number
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex gap-4">
|
||||
<Container className="flex-1">
|
||||
<div className="mb-5">
|
||||
<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">
|
||||
<div className="flex-1">Asset</div>
|
||||
<div className="flex-1">Borrow Rate</div>
|
||||
<div className="flex-1">Borrowed</div>
|
||||
<div className="flex-1">Liquidity Available</div>
|
||||
<div className="w-[50px]">Manage</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<Container>
|
||||
<div className="mb-5">
|
||||
<h3 className="mb-1 text-center font-medium uppercase">Borrowed</h3>
|
||||
<BorrowTable
|
||||
data={borrowedAssets}
|
||||
onBorrowClick={handleBorrowClick}
|
||||
onRepayClick={handleRepayClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.from(Array(3).keys()).map(() => (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<AssetRow />
|
||||
))}
|
||||
<div>
|
||||
<h3 className="mb-1 text-center font-medium uppercase">Not Borrowed Yet</h3>
|
||||
<BorrowTable
|
||||
data={notBorrowedAssets}
|
||||
onBorrowClick={handleBorrowClick}
|
||||
onRepayClick={handleRepayClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<div className="flex-1">Asset</div>
|
||||
<div className="flex-1">Borrow Rate</div>
|
||||
<div className="flex-1">Borrowed</div>
|
||||
<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>
|
||||
</Container>
|
||||
</Container>
|
||||
</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
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) => {
|
||||
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"
|
||||
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":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||
@ -2148,6 +2153,25 @@
|
||||
"@tanstack/query-core" "4.3.4"
|
||||
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":
|
||||
version "0.2.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user