MP-2017: Deposit Funds and Account stats (#21)
* style: fund account font size adjustments * client instance. contract addresses updates. prices hook added * persist lend assets value for every credit account * feat: account stats and semi circular progress * minor code cleanup * display borrowed assets interest rate * fallback screen when no wallet is connected * fix: hydration mismatch * update osmosis testnet endpoints * style: body text color * coin interface imported from cosmos package * risk calculation from ltv assets comment added * svgr setup. inline svg extracted to Icons folder * address removed from local storage. wallet store improvements * rename setAddress action to connect * yield page renamed to earn * refactor: accountStats using BigNumber * update contract addresses * update hardcoded fee * update market mocked values * current leverage added to useAccountStats hook return * leverage naming disambiguation * debt positions labels color update. negative sign before values * remove prefers-color-scheme media query * update redbank mock data
This commit is contained in:
parent
f709c12da2
commit
3022ae9a6a
@ -7,7 +7,7 @@ import { Coin } from '@cosmjs/stargate'
|
|||||||
|
|
||||||
import { getInjectiveAddress } from 'utils/address'
|
import { getInjectiveAddress } from 'utils/address'
|
||||||
import { getExperimentalChainConfigBasedOnChainId } from 'utils/experimental-chains'
|
import { getExperimentalChainConfigBasedOnChainId } from 'utils/experimental-chains'
|
||||||
import { ChainId } from 'types'
|
import { ChainId, Wallet } from 'types'
|
||||||
import useWalletStore from 'stores/useWalletStore'
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
import { chain } from 'utils/chains'
|
import { chain } from 'utils/chains'
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ const ConnectModal = ({ isOpen, onClose }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = await window.keplr.getKey(chain.chainId)
|
const key = await window.keplr.getKey(chain.chainId)
|
||||||
actions.setAddress(key.bech32Address)
|
actions.connect(key.bech32Address, Wallet.Keplr)
|
||||||
|
|
||||||
handleConnectSuccess()
|
handleConnectSuccess()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -72,7 +72,7 @@ const ConnectModal = ({ isOpen, onClose }: Props) => {
|
|||||||
method: 'eth_requestAccounts',
|
method: 'eth_requestAccounts',
|
||||||
})
|
})
|
||||||
const [address] = addresses
|
const [address] = addresses
|
||||||
actions.setAddress(getInjectiveAddress(address))
|
actions.connect(getInjectiveAddress(address), Wallet.Metamask)
|
||||||
handleConnectSuccess()
|
handleConnectSuccess()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: handle exception
|
// TODO: handle exception
|
||||||
|
@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'
|
|||||||
import * as Slider from '@radix-ui/react-slider'
|
import * as Slider from '@radix-ui/react-slider'
|
||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import { Switch } from '@headlessui/react'
|
import { Switch } from '@headlessui/react'
|
||||||
|
import useLocalStorageState from 'use-local-storage-state'
|
||||||
|
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import useAllowedCoins from 'hooks/useAllowedCoins'
|
import useAllowedCoins from 'hooks/useAllowedCoins'
|
||||||
@ -14,10 +15,13 @@ import CreditManagerContainer from './CreditManagerContainer'
|
|||||||
const FundAccount = () => {
|
const FundAccount = () => {
|
||||||
const [amount, setAmount] = useState(0)
|
const [amount, setAmount] = useState(0)
|
||||||
const [selectedToken, setSelectedToken] = useState('')
|
const [selectedToken, setSelectedToken] = useState('')
|
||||||
const [enabled, setEnabled] = useState(false)
|
|
||||||
|
|
||||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
|
|
||||||
|
const [lendAssets, setLendAssets] = useLocalStorageState(`lendAssets_${selectedAccount}`, {
|
||||||
|
defaultValue: false,
|
||||||
|
})
|
||||||
|
|
||||||
const { data: balancesData } = useAllBalances()
|
const { data: balancesData } = useAllBalances()
|
||||||
const { data: allowedCoinsData, isLoading: isLoadingAllowedCoins } = useAllowedCoins()
|
const { data: allowedCoinsData, isLoading: isLoadingAllowedCoins } = useAllowedCoins()
|
||||||
const { mutate } = useDepositCreditAccount(
|
const { mutate } = useDepositCreditAccount(
|
||||||
@ -58,7 +62,7 @@ const FundAccount = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreditManagerContainer className="mb-2 p-3">
|
<CreditManagerContainer className="mb-2 p-3">
|
||||||
<p className="mb-6">
|
<p className="mb-6 text-sm">
|
||||||
Transfer assets from your injective wallet to your Mars credit account. If you don’t have
|
Transfer assets from your injective wallet to your Mars credit account. If you don’t have
|
||||||
any assets in your injective wallet use the injective bridge to transfer funds to your
|
any assets in your injective wallet use the injective bridge to transfer funds to your
|
||||||
injective wallet.
|
injective wallet.
|
||||||
@ -67,7 +71,7 @@ const FundAccount = () => {
|
|||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4">
|
<div className="mb-4 text-sm">
|
||||||
<div className="mb-1 flex justify-between">
|
<div className="mb-1 flex justify-between">
|
||||||
<div>Asset:</div>
|
<div>Asset:</div>
|
||||||
<select
|
<select
|
||||||
@ -95,7 +99,7 @@ const FundAccount = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>In wallet: {walletAmount.toLocaleString()}</p>
|
<p className="text-sm">In wallet: {walletAmount.toLocaleString()}</p>
|
||||||
{/* SLIDER - initial implementation to test functionality */}
|
{/* SLIDER - initial implementation to test functionality */}
|
||||||
{/* TODO: will need to be revamped later on */}
|
{/* TODO: will need to be revamped later on */}
|
||||||
<div className="relative mb-6 flex flex-1 items-center">
|
<div className="relative mb-6 flex flex-1 items-center">
|
||||||
@ -134,19 +138,19 @@ const FundAccount = () => {
|
|||||||
<CreditManagerContainer className="mb-2 flex items-center justify-between">
|
<CreditManagerContainer 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="opacity-50">Lend assets from account to earn yield.</div>
|
<div className="text-sm opacity-50">Lend assets from account to earn yield.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={lendAssets}
|
||||||
onChange={setEnabled}
|
onChange={setLendAssets}
|
||||||
className={`${
|
className={`${
|
||||||
enabled ? 'bg-blue-600' : 'bg-gray-400'
|
lendAssets ? 'bg-blue-600' : 'bg-gray-400'
|
||||||
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
enabled ? 'translate-x-6' : 'translate-x-1'
|
lendAssets ? 'translate-x-6' : 'translate-x-1'
|
||||||
} 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>
|
||||||
|
@ -5,10 +5,13 @@ import Button from '../Button'
|
|||||||
import { formatCurrency } from 'utils/formatters'
|
import { formatCurrency } from 'utils/formatters'
|
||||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
import useWalletStore from 'stores/useWalletStore'
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
import useCreditAccountBalances from 'hooks/useCreditAccountPositions'
|
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||||
import { getTokenDecimals } from 'utils/tokens'
|
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||||
import FundAccount from './FundAccount'
|
import FundAccount from './FundAccount'
|
||||||
import CreditManagerContainer from './CreditManagerContainer'
|
import CreditManagerContainer from './CreditManagerContainer'
|
||||||
|
import useTokenPrices from 'hooks/useTokenPrices'
|
||||||
|
import useAccountStats from 'hooks/useAccountStats'
|
||||||
|
import useMarkets from 'hooks/useMarkets'
|
||||||
|
|
||||||
const CreditManager = () => {
|
const CreditManager = () => {
|
||||||
const [isFund, setIsFund] = useState(false)
|
const [isFund, setIsFund] = useState(false)
|
||||||
@ -16,19 +19,24 @@ const CreditManager = () => {
|
|||||||
const address = useWalletStore((s) => s.address)
|
const address = useWalletStore((s) => s.address)
|
||||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
|
|
||||||
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountBalances(
|
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
|
||||||
selectedAccount ?? ''
|
selectedAccount ?? ''
|
||||||
)
|
)
|
||||||
|
|
||||||
const totalPosition =
|
const { data: tokenPrices } = useTokenPrices()
|
||||||
positionsData?.coins.reduce((acc, coin) => {
|
const { data: marketsData } = useMarkets()
|
||||||
return Number(coin.value) + acc
|
const accountStats = useAccountStats()
|
||||||
}, 0) ?? 0
|
|
||||||
|
|
||||||
const totalDebt =
|
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||||
positionsData?.debt.reduce((acc, coin) => {
|
// early return if prices are not fetched yet
|
||||||
return Number(coin.value) + acc
|
if (!tokenPrices) return 0
|
||||||
}, 0) ?? 0
|
|
||||||
|
return (
|
||||||
|
BigNumber(amount)
|
||||||
|
.div(10 ** getTokenDecimals(denom))
|
||||||
|
.toNumber() * tokenPrices[denom]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return (
|
return (
|
||||||
@ -66,11 +74,13 @@ const CreditManager = () => {
|
|||||||
<CreditManagerContainer className="mb-2 text-sm">
|
<CreditManagerContainer 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">{formatCurrency(totalPosition)}</div>
|
<div className="font-semibold">
|
||||||
|
{formatCurrency(accountStats?.totalPosition ?? 0)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div>Total Liabilities:</div>
|
<div>Total Liabilities:</div>
|
||||||
<div className="font-semibold">{formatCurrency(totalDebt)}</div>
|
<div className="font-semibold">{formatCurrency(accountStats?.totalDebt ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
</CreditManagerContainer>
|
</CreditManagerContainer>
|
||||||
<CreditManagerContainer>
|
<CreditManagerContainer>
|
||||||
@ -87,8 +97,10 @@ const CreditManager = () => {
|
|||||||
</div>
|
</div>
|
||||||
{positionsData?.coins.map((coin) => (
|
{positionsData?.coins.map((coin) => (
|
||||||
<div key={coin.denom} className="flex text-xs text-black/40">
|
<div key={coin.denom} className="flex text-xs text-black/40">
|
||||||
<div className="flex-1">{coin.denom}</div>
|
<div className="flex-1">{getTokenSymbol(coin.denom)}</div>
|
||||||
<div className="flex-1">{formatCurrency(coin.value)}</div>
|
<div className="flex-1">
|
||||||
|
{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||||
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{BigNumber(coin.amount)
|
{BigNumber(coin.amount)
|
||||||
.div(10 ** getTokenDecimals(coin.denom))
|
.div(10 ** getTokenDecimals(coin.denom))
|
||||||
@ -100,11 +112,14 @@ const CreditManager = () => {
|
|||||||
<div className="flex-1">-</div>
|
<div className="flex-1">-</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{positionsData?.debt.map((coin) => (
|
{positionsData?.debts.map((coin) => (
|
||||||
<div key={coin.denom} className="flex text-xs text-red-500">
|
<div key={coin.denom} className="flex text-xs text-red-500">
|
||||||
<div className="flex-1">{coin.denom}</div>
|
<div className="flex-1 text-black/40">{getTokenSymbol(coin.denom)}</div>
|
||||||
<div className="flex-1">{formatCurrency(coin.value)}</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
-{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
-
|
||||||
{BigNumber(coin.amount)
|
{BigNumber(coin.amount)
|
||||||
.div(10 ** getTokenDecimals(coin.denom))
|
.div(10 ** getTokenDecimals(coin.denom))
|
||||||
.toNumber()
|
.toNumber()
|
||||||
@ -112,7 +127,9 @@ const CreditManager = () => {
|
|||||||
maximumFractionDigits: 6,
|
maximumFractionDigits: 6,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">-</div>
|
<div className="flex-1">
|
||||||
|
{(Number(marketsData?.[coin.denom].borrow_rate) * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
3
components/Icons/arrow-right-line.svg
Normal file
3
components/Icons/arrow-right-line.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="13" viewBox="0 0 14 13" fill="currentColor">
|
||||||
|
<path d="M0.234863 6.57567C0.234863 7.07288 0.581403 7.41188 1.08615 7.41188H8.04708L9.62912 7.33655L7.45194 9.31785L5.93771 10.8547C5.77951 11.0129 5.68157 11.2163 5.68157 11.4574C5.68157 11.9244 6.02811 12.2634 6.50272 12.2634C6.72872 12.2634 6.93213 12.173 7.12047 11.9922L11.859 7.20094C11.9871 7.07288 12.0775 6.92221 12.1152 6.74894V11.5478C12.1152 12.0148 12.4692 12.3538 12.9363 12.3538C13.4109 12.3538 13.765 12.0148 13.765 11.5478V1.6111C13.765 1.14403 13.4109 0.797485 12.9363 0.797485C12.4692 0.797485 12.1152 1.14403 12.1152 1.6111V6.39486C12.0775 6.22913 11.9871 6.07846 11.859 5.95039L7.12047 1.15156C6.93213 0.970755 6.72872 0.880354 6.50272 0.880354C6.02811 0.880354 5.68157 1.22689 5.68157 1.68644C5.68157 1.92751 5.77951 2.13845 5.93771 2.28911L7.45194 3.83348L9.62912 5.80725L8.04708 5.73192H1.08615C0.581403 5.73192 0.234863 6.07846 0.234863 6.57567Z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 956 B |
@ -13,13 +13,17 @@ import useCreditAccounts from 'hooks/useCreditAccounts'
|
|||||||
import useCreateCreditAccount from 'hooks/useCreateCreditAccount'
|
import useCreateCreditAccount from 'hooks/useCreateCreditAccount'
|
||||||
import useDeleteCreditAccount from 'hooks/useDeleteCreditAccount'
|
import useDeleteCreditAccount from 'hooks/useDeleteCreditAccount'
|
||||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
|
import useAccountStats from 'hooks/useAccountStats'
|
||||||
|
import SemiCircleProgress from './SemiCircleProgress'
|
||||||
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
|
import ArrowRightLine from 'components/Icons/arrow-right-line.svg'
|
||||||
|
|
||||||
// TODO: will require some tweaks depending on how lower viewport mocks pans out
|
// TODO: will require some tweaks depending on how lower viewport mocks pans out
|
||||||
const MAX_VISIBLE_CREDIT_ACCOUNTS = 5
|
const MAX_VISIBLE_CREDIT_ACCOUNTS = 5
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/trade', label: 'Trade' },
|
{ href: '/trade', label: 'Trade' },
|
||||||
{ href: '/yield', label: 'Yield' },
|
{ href: '/earn', label: 'Earn' },
|
||||||
{ href: '/borrow', label: 'Borrow' },
|
{ href: '/borrow', label: 'Borrow' },
|
||||||
{ href: '/portfolio', label: 'Portfolio' },
|
{ href: '/portfolio', label: 'Portfolio' },
|
||||||
{ href: '/council', label: 'Council' },
|
{ href: '/council', label: 'Council' },
|
||||||
@ -38,6 +42,7 @@ const NavLink = ({ href, children }: { href: string; children: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
|
const address = useWalletStore((s) => s.address)
|
||||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
const setSelectedAccount = useCreditManagerStore((s) => s.actions.setSelectedAccount)
|
const setSelectedAccount = useCreditManagerStore((s) => s.actions.setSelectedAccount)
|
||||||
const toggleCreditManager = useCreditManagerStore((s) => s.actions.toggleCreditManager)
|
const toggleCreditManager = useCreditManagerStore((s) => s.actions.toggleCreditManager)
|
||||||
@ -48,6 +53,8 @@ const Navigation = () => {
|
|||||||
selectedAccount || ''
|
selectedAccount || ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const accountStats = useAccountStats()
|
||||||
|
|
||||||
const { firstCreditAccounts, restCreditAccounts } = useMemo(() => {
|
const { firstCreditAccounts, restCreditAccounts } = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
firstCreditAccounts: creditAccountsList?.slice(0, MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
|
firstCreditAccounts: creditAccountsList?.slice(0, MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
|
||||||
@ -74,105 +81,115 @@ const Navigation = () => {
|
|||||||
<Wallet />
|
<Wallet />
|
||||||
</div>
|
</div>
|
||||||
{/* Sub navigation bar */}
|
{/* Sub navigation bar */}
|
||||||
<div className="flex justify-between border-b border-white/20 px-6 py-3 text-sm text-white/40">
|
{address && (
|
||||||
<div className="flex items-center">
|
<div className="flex justify-between border-b border-white/20 px-6 py-3 text-sm text-white/40">
|
||||||
<SearchInput />
|
<div className="flex items-center">
|
||||||
{firstCreditAccounts.map((account) => (
|
<SearchInput />
|
||||||
<div
|
{firstCreditAccounts.map((account) => (
|
||||||
key={account}
|
<div
|
||||||
className={`cursor-pointer px-4 hover:text-white ${
|
key={account}
|
||||||
selectedAccount === account ? 'text-white' : ''
|
className={`cursor-pointer px-4 hover:text-white ${
|
||||||
}`}
|
selectedAccount === account ? 'text-white' : ''
|
||||||
onClick={() => setSelectedAccount(account)}
|
}`}
|
||||||
>
|
onClick={() => setSelectedAccount(account)}
|
||||||
Account {account}
|
>
|
||||||
</div>
|
Account {account}
|
||||||
))}
|
</div>
|
||||||
{restCreditAccounts.length > 0 && (
|
))}
|
||||||
|
{restCreditAccounts.length > 0 && (
|
||||||
|
<Popover className="relative">
|
||||||
|
<Popover.Button>
|
||||||
|
<div className="flex cursor-pointer items-center px-3 hover:text-white">
|
||||||
|
More
|
||||||
|
<ChevronDownIcon className="ml-1 h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</Popover.Button>
|
||||||
|
<Popover.Panel className="absolute z-10 w-[200px] pt-2">
|
||||||
|
<div className="rounded-2xl bg-white p-4 text-gray-900">
|
||||||
|
{restCreditAccounts.map((account) => (
|
||||||
|
<div
|
||||||
|
key={account}
|
||||||
|
className={`cursor-pointer hover:text-orange-500 ${
|
||||||
|
selectedAccount === account ? 'text-orange-500' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedAccount(account)}
|
||||||
|
>
|
||||||
|
Account {account}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<Popover.Button>
|
<Popover.Button>
|
||||||
<div className="flex cursor-pointer items-center px-3 hover:text-white">
|
<div className="flex cursor-pointer items-center px-3 hover:text-white">
|
||||||
More
|
Manage
|
||||||
<ChevronDownIcon className="ml-1 h-4 w-4" />
|
<ChevronDownIcon className="ml-1 h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
<Popover.Panel className="absolute z-10 w-[200px] pt-2">
|
<Popover.Panel className="absolute z-10 w-[200px] pt-2">
|
||||||
<div className="rounded-2xl bg-white p-4 text-gray-900">
|
{({ close }) => (
|
||||||
{restCreditAccounts.map((account) => (
|
<div className="rounded-2xl bg-white p-4 text-gray-900">
|
||||||
<div
|
<div
|
||||||
key={account}
|
className="mb-2 cursor-pointer hover:text-orange-500"
|
||||||
className={`cursor-pointer hover:text-orange-500 ${
|
onClick={() => {
|
||||||
selectedAccount === account ? 'text-orange-500' : ''
|
close()
|
||||||
}`}
|
createCreditAccount()
|
||||||
onClick={() => setSelectedAccount(account)}
|
}}
|
||||||
>
|
>
|
||||||
Account {account}
|
Create Account
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div
|
||||||
</div>
|
className="mb-2 cursor-pointer hover:text-orange-500"
|
||||||
|
onClick={() => {
|
||||||
|
close()
|
||||||
|
deleteCreditAccount()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close Account
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mb-2 cursor-pointer hover:text-orange-500"
|
||||||
|
onClick={() => alert('TODO')}
|
||||||
|
>
|
||||||
|
Transfer Balance
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer hover:text-orange-500"
|
||||||
|
onClick={() => alert('TODO')}
|
||||||
|
>
|
||||||
|
Rearrange
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
</div>
|
||||||
<Popover className="relative">
|
<div className="flex items-center gap-4">
|
||||||
<Popover.Button>
|
{accountStats && (
|
||||||
<div className="flex cursor-pointer items-center px-3 hover:text-white">
|
<>
|
||||||
Manage
|
<p>{formatCurrency(accountStats.netWorth)}</p>
|
||||||
<ChevronDownIcon className="ml-1 h-4 w-4" />
|
{/* TOOLTIP */}
|
||||||
</div>
|
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||||
</Popover.Button>
|
<SemiCircleProgress
|
||||||
<Popover.Panel className="absolute z-10 w-[200px] pt-2">
|
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||||
{({ close }) => (
|
label="Lvg"
|
||||||
<div className="rounded-2xl bg-white p-4 text-gray-900">
|
/>
|
||||||
<div
|
|
||||||
className="mb-2 cursor-pointer hover:text-orange-500"
|
|
||||||
onClick={() => {
|
|
||||||
close()
|
|
||||||
createCreditAccount()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create Account
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mb-2 cursor-pointer hover:text-orange-500"
|
|
||||||
onClick={() => {
|
|
||||||
close()
|
|
||||||
deleteCreditAccount()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Close Account
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mb-2 cursor-pointer hover:text-orange-500"
|
|
||||||
onClick={() => alert('TODO')}
|
|
||||||
>
|
|
||||||
Transfer Balance
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="cursor-pointer hover:text-orange-500"
|
|
||||||
onClick={() => alert('TODO')}
|
|
||||||
>
|
|
||||||
Rearrange
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<SemiCircleProgress value={accountStats.risk} label="Risk" />
|
||||||
</Popover.Panel>
|
<ProgressBar value={accountStats.health} />
|
||||||
</Popover>
|
</>
|
||||||
</div>
|
)}
|
||||||
<div className="flex items-center gap-4">
|
<div
|
||||||
<p>{formatCurrency(2500)}</p>
|
className="flex w-16 cursor-pointer justify-center hover:text-white"
|
||||||
<div>Lvg</div>
|
onClick={toggleCreditManager}
|
||||||
<div>Risk</div>
|
>
|
||||||
<ProgressBar value={0.43} />
|
<ArrowRightLine />
|
||||||
<div
|
</div>
|
||||||
className="flex w-16 cursor-pointer justify-center hover:text-white"
|
|
||||||
onClick={toggleCreditManager}
|
|
||||||
>
|
|
||||||
<svg width="14" height="13" viewBox="0 0 14 13" fill="currentColor">
|
|
||||||
<path d="M0.234863 6.57567C0.234863 7.07288 0.581403 7.41188 1.08615 7.41188H8.04708L9.62912 7.33655L7.45194 9.31785L5.93771 10.8547C5.77951 11.0129 5.68157 11.2163 5.68157 11.4574C5.68157 11.9244 6.02811 12.2634 6.50272 12.2634C6.72872 12.2634 6.93213 12.173 7.12047 11.9922L11.859 7.20094C11.9871 7.07288 12.0775 6.92221 12.1152 6.74894V11.5478C12.1152 12.0148 12.4692 12.3538 12.9363 12.3538C13.4109 12.3538 13.765 12.0148 13.765 11.5478V1.6111C13.765 1.14403 13.4109 0.797485 12.9363 0.797485C12.4692 0.797485 12.1152 1.14403 12.1152 1.6111V6.39486C12.0775 6.22913 11.9871 6.07846 11.859 5.95039L7.12047 1.15156C6.93213 0.970755 6.72872 0.880354 6.50272 0.880354C6.02811 0.880354 5.68157 1.22689 5.68157 1.68644C5.68157 1.92751 5.77951 2.13845 5.93771 2.28911L7.45194 3.83348L9.62912 5.80725L8.04708 5.73192H1.08615C0.581403 5.73192 0.234863 6.07846 0.234863 6.57567Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{(isLoadingCreate || isLoadingDelete) && (
|
{(isLoadingCreate || isLoadingDelete) && (
|
||||||
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React from 'react'
|
||||||
import { ArrowRightIcon } from '@heroicons/react/24/solid'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: number
|
value: number
|
||||||
@ -7,16 +6,6 @@ type Props = {
|
|||||||
|
|
||||||
const ProgressBar = ({ value }: Props) => {
|
const ProgressBar = ({ value }: Props) => {
|
||||||
const percentageValue = `${(value * 100).toFixed(0)}%`
|
const percentageValue = `${(value * 100).toFixed(0)}%`
|
||||||
const [newValue, setNewValue] = useState(0.77)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setInterval(() => {
|
|
||||||
// randomizing value between value and 1
|
|
||||||
setNewValue(Math.random() * (1 - value) + value)
|
|
||||||
}, 3000)
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
const percentageNewValue = `${(newValue * 100).toFixed(0)}%`
|
|
||||||
|
|
||||||
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">
|
||||||
@ -24,14 +13,8 @@ const ProgressBar = ({ value }: Props) => {
|
|||||||
className="absolute z-10 h-4 rounded-full bg-green-500"
|
className="absolute z-10 h-4 rounded-full bg-green-500"
|
||||||
style={{ width: percentageValue }}
|
style={{ width: percentageValue }}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className="absolute h-4 rounded-full bg-red-500 transition-[width] duration-500"
|
|
||||||
style={{ width: percentageNewValue }}
|
|
||||||
/>
|
|
||||||
<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">
|
||||||
{percentageValue}
|
{percentageValue}
|
||||||
<ArrowRightIcon className="h-3 w-3" />
|
|
||||||
{percentageNewValue}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
104
components/SemiCircleProgress.tsx
Normal file
104
components/SemiCircleProgress.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
stroke?: string
|
||||||
|
strokeWidth?: number
|
||||||
|
background?: string
|
||||||
|
diameter?: 60
|
||||||
|
orientation?: any
|
||||||
|
direction?: any
|
||||||
|
value: number
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SemiCircleProgress = ({
|
||||||
|
stroke = '#02B732',
|
||||||
|
strokeWidth = 6,
|
||||||
|
background = '#D0D0CE',
|
||||||
|
diameter = 60,
|
||||||
|
orientation = 'up',
|
||||||
|
direction = 'right',
|
||||||
|
value = 0,
|
||||||
|
label,
|
||||||
|
}: Props) => {
|
||||||
|
const coordinateForCircle = diameter / 2
|
||||||
|
const radius = (diameter - 2 * strokeWidth) / 2
|
||||||
|
const circumference = Math.PI * radius
|
||||||
|
const percentage = value * 100
|
||||||
|
|
||||||
|
let percentageValue
|
||||||
|
if (percentage > 100) {
|
||||||
|
percentageValue = 100
|
||||||
|
} else if (percentage < 0) {
|
||||||
|
percentageValue = 0
|
||||||
|
} else {
|
||||||
|
percentageValue = percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
const semiCirclePercentage = percentageValue * (circumference / 100)
|
||||||
|
|
||||||
|
let rotation
|
||||||
|
if (orientation === 'down') {
|
||||||
|
if (direction === 'left') {
|
||||||
|
rotation = 'rotate(180deg) rotateY(180deg)'
|
||||||
|
} else {
|
||||||
|
rotation = 'rotate(180deg)'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (direction === 'right') {
|
||||||
|
rotation = 'rotateY(180deg)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="semicircle-container" style={{ position: 'relative' }}>
|
||||||
|
<svg
|
||||||
|
width={diameter}
|
||||||
|
height={diameter / 2}
|
||||||
|
style={{ transform: rotation, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx={coordinateForCircle}
|
||||||
|
cy={coordinateForCircle}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke={background}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
style={{
|
||||||
|
strokeDashoffset: circumference,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={coordinateForCircle}
|
||||||
|
cy={coordinateForCircle}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
style={{
|
||||||
|
strokeDashoffset: semiCirclePercentage,
|
||||||
|
transition: 'stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{label && (
|
||||||
|
<span
|
||||||
|
className="text-xs"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
left: '0',
|
||||||
|
textAlign: 'center',
|
||||||
|
bottom: orientation === 'down' ? 'auto' : '-4px',
|
||||||
|
position: 'absolute',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SemiCircleProgress
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
@ -34,7 +34,7 @@ const WalletPopover = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className=" bg-[#524bb1] hover:bg-[#6962cc]"
|
className=" bg-[#524bb1] hover:bg-[#6962cc]"
|
||||||
onClick={() => actions.setAddress('')}
|
onClick={() => actions.disconnect()}
|
||||||
>
|
>
|
||||||
Disconnect
|
Disconnect
|
||||||
</Button>
|
</Button>
|
||||||
@ -71,18 +71,12 @@ const WalletPopover = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
const Wallet = () => {
|
const Wallet = () => {
|
||||||
const [showConnectModal, setShowConnectModal] = useState(false)
|
const [showConnectModal, setShowConnectModal] = useState(false)
|
||||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const address = useWalletStore((s) => s.address)
|
const address = useWalletStore((s) => s.address)
|
||||||
|
|
||||||
// avoid server-client hydration mismatch
|
|
||||||
useEffect(() => {
|
|
||||||
setHasHydrated(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasHydrated && address ? (
|
{address ? (
|
||||||
<WalletPopover>{formatWalletAddress(address)}</WalletPopover>
|
<WalletPopover>{formatWalletAddress(address)}</WalletPopover>
|
||||||
) : (
|
) : (
|
||||||
<Button className="w-[200px]" onClick={() => setShowConnectModal(true)}>
|
<Button className="w-[200px]" onClick={() => setShowConnectModal(true)}>
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// https://github.com/mars-protocol/rover/blob/master/scripts/deploy/addresses/osmo-test-4.json
|
// https://github.com/mars-protocol/rover/blob/master/scripts/deploy/addresses/osmo-test-4.json
|
||||||
export const contractAddresses = {
|
export const contractAddresses = {
|
||||||
accountNft: 'osmo16v3mvsdnkh4c6ykc885n3x5ay9e36akdzxcl2g93698rqw007xxqesld8w',
|
accountNft: 'osmo1dravtyd0425fkdmkysc3ns7zud05clf5uhj6qqsnkdtrpkewu73q9f3f02',
|
||||||
mockRedBank: 'osmo1xrnx0q3x7kwzss53fry0dwwsc7pff6aq628l6n0rmvegkalp4y7qzl7j7z',
|
mockVault: 'osmo1emcckulm2mkx36xeanhsn3z3zjeql6pgd8yf8a5cf03ccvy7a4dqjw9tl7',
|
||||||
mockOracle: 'osmo1r9u2tfq8n5xpn2g0fq8ha0rj0cyp2fzr5w9jvcqwt3r8lxdfm6yszmtza5',
|
marsOracleAdapter: 'osmo1cw6pv97g7fmhqykrn0gc9ngrx5tnky75rmlwkzxuqhsk58u0n8asz036g0',
|
||||||
mockVault: 'osmo1gg4rpug7vwrnq0ask0k7nmw23z6wl8c8fr7jmup9pdpaal9uc5nqq7lyrm',
|
swapper: 'osmo1w2552km2u9w4k2gjw4n8drmuz5yxw8x4qzy6dl3da824km5cjlys00x3qp',
|
||||||
swapper: 'osmo1ak4x8k2h7s6pq5dnlncmgsmx2nqcaplpfxlmklx2ln7qn6dtny8q70apjv',
|
creditManager: 'osmo18dt5y0ecyd5qg8nqwzrgxuljfejglyh2fjd984s8cy7fcx8mxh9qfl3hwq',
|
||||||
creditManager: 'osmo1963xgmt8agyc6q4k2vhf980kffq6ukkj9mgtwdxxnpj3dak2akdq20z9dw',
|
|
||||||
}
|
}
|
||||||
|
86
hooks/useAccountStats.tsx
Normal file
86
hooks/useAccountStats.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
// displaying 3 levels of risk based on the weighted average of liquidation LTVs
|
||||||
|
// 0.85 -> 25% risk
|
||||||
|
// 0.65 - 0.85 -> 50% risk
|
||||||
|
// < 0.65 -> 100% risk
|
||||||
|
const getRiskFromAverageLiquidationLTVs = (value: number) => {
|
||||||
|
if (value >= 0.85) return 0.25
|
||||||
|
if (value > 0.65) return 0.5
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAccountStats = () => {
|
||||||
|
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 null
|
||||||
|
|
||||||
|
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 totalPosition = positionsData.coins.reduce((acc, coin) => {
|
||||||
|
const tokenTotalValue = getTokenTotalUSDValue(coin.amount, coin.denom)
|
||||||
|
return BigNumber(tokenTotalValue).plus(acc).toNumber()
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalDebt = positionsData.debts.reduce((acc, coin) => {
|
||||||
|
const tokenTotalValue = getTokenTotalUSDValue(coin.amount, coin.denom)
|
||||||
|
return BigNumber(tokenTotalValue).plus(acc).toNumber()
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
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 netWorth = BigNumber(totalPosition).minus(totalDebt).toNumber()
|
||||||
|
|
||||||
|
const liquidationLTVsWeightedAverage = BigNumber(totalWeightedPositions)
|
||||||
|
.div(totalPosition)
|
||||||
|
.toNumber()
|
||||||
|
|
||||||
|
const maxLeverage = BigNumber(1)
|
||||||
|
.div(BigNumber(1).minus(liquidationLTVsWeightedAverage))
|
||||||
|
.toNumber()
|
||||||
|
const currentLeverage = BigNumber(totalPosition).div(netWorth).toNumber()
|
||||||
|
const health = BigNumber(1).minus(BigNumber(currentLeverage).div(maxLeverage)).toNumber() || 1
|
||||||
|
|
||||||
|
const risk = liquidationLTVsWeightedAverage
|
||||||
|
? getRiskFromAverageLiquidationLTVs(liquidationLTVsWeightedAverage)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
health,
|
||||||
|
maxLeverage,
|
||||||
|
currentLeverage,
|
||||||
|
netWorth,
|
||||||
|
risk,
|
||||||
|
totalPosition,
|
||||||
|
totalDebt,
|
||||||
|
}
|
||||||
|
}, [marketsData, positionsData, tokenPrices])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAccountStats
|
@ -1,9 +1,6 @@
|
|||||||
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import useWalletStore from 'stores/useWalletStore'
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
import { chain } from 'utils/chains'
|
|
||||||
import { contractAddresses } from 'config/contracts'
|
import { contractAddresses } from 'config/contracts'
|
||||||
import { queryKeys } from 'types/query-keys-factory'
|
import { queryKeys } from 'types/query-keys-factory'
|
||||||
|
|
||||||
@ -14,25 +11,14 @@ const queryMsg = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useAllowedCoins = () => {
|
const useAllowedCoins = () => {
|
||||||
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
|
|
||||||
const address = useWalletStore((s) => s.address)
|
const address = useWalletStore((s) => s.address)
|
||||||
|
const client = useWalletStore((s) => s.client)
|
||||||
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 result = useQuery<Result>(
|
const result = useQuery<Result>(
|
||||||
queryKeys.allowedCoins(),
|
queryKeys.allowedCoins(),
|
||||||
async () => signingClient?.queryContractSmart(contractAddresses.creditManager, queryMsg),
|
async () => client?.queryContractSmart(contractAddresses.creditManager, queryMsg),
|
||||||
{
|
{
|
||||||
enabled: !!address && !!signingClient,
|
enabled: !!address && !!client,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,69 +1,43 @@
|
|||||||
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { Coin } from '@cosmjs/stargate'
|
||||||
|
|
||||||
import useWalletStore from 'stores/useWalletStore'
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
import { chain } from 'utils/chains'
|
|
||||||
import { contractAddresses } from 'config/contracts'
|
import { contractAddresses } from 'config/contracts'
|
||||||
import { queryKeys } from 'types/query-keys-factory'
|
import { queryKeys } from 'types/query-keys-factory'
|
||||||
|
|
||||||
interface CoinValue {
|
interface DebtAmount {
|
||||||
amount: string
|
amount: string
|
||||||
denom: string
|
denom: string
|
||||||
price: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DebtSharesValue {
|
|
||||||
amount: string
|
|
||||||
denom: string
|
|
||||||
price: string
|
|
||||||
shares: string
|
shares: string
|
||||||
value: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VaultPosition {
|
interface VaultPosition {
|
||||||
locked: string
|
locked: string
|
||||||
unlocked: string
|
unlocked: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VaultPositionWithAddr {
|
|
||||||
addr: string
|
|
||||||
position: VaultPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Result {
|
interface Result {
|
||||||
account_id: string
|
account_id: string
|
||||||
coins: CoinValue[]
|
coins: Coin[]
|
||||||
debt: DebtSharesValue[]
|
debts: DebtAmount[]
|
||||||
vault_positions: VaultPositionWithAddr[]
|
vaults: VaultPosition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const useCreditAccountPositions = (accountId: string) => {
|
const useCreditAccountPositions = (accountId: string) => {
|
||||||
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
|
|
||||||
const address = useWalletStore((s) => s.address)
|
const address = useWalletStore((s) => s.address)
|
||||||
|
const client = useWalletStore((s) => s.client)
|
||||||
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 result = useQuery<Result>(
|
const result = useQuery<Result>(
|
||||||
queryKeys.creditAccountsPositions(accountId),
|
queryKeys.creditAccountsPositions(accountId),
|
||||||
async () =>
|
async () =>
|
||||||
signingClient?.queryContractSmart(contractAddresses.creditManager, {
|
client?.queryContractSmart(contractAddresses.creditManager, {
|
||||||
positions: {
|
positions: {
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
enabled: !!address && !!signingClient,
|
enabled: !!address && !!client,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -71,7 +45,7 @@ const useCreditAccountPositions = (accountId: string) => {
|
|||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
data: useMemo(() => {
|
data: useMemo(() => {
|
||||||
return result?.data
|
return result?.data && { ...result.data }
|
||||||
}, [result.data]),
|
}, [result.data]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import useWalletStore from 'stores/useWalletStore'
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
import { chain } from 'utils/chains'
|
|
||||||
import { contractAddresses } from 'config/contracts'
|
import { contractAddresses } from 'config/contracts'
|
||||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||||
import { queryKeys } from 'types/query-keys-factory'
|
import { queryKeys } from 'types/query-keys-factory'
|
||||||
@ -13,8 +11,8 @@ type Result = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useCreditAccounts = () => {
|
const useCreditAccounts = () => {
|
||||||
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
|
|
||||||
const address = useWalletStore((s) => s.address)
|
const address = useWalletStore((s) => s.address)
|
||||||
|
const client = useWalletStore((s) => s.client)
|
||||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||||
const creditManagerActions = useCreditManagerStore((s) => s.actions)
|
const creditManagerActions = useCreditManagerStore((s) => s.actions)
|
||||||
|
|
||||||
@ -26,22 +24,12 @@ const useCreditAccounts = () => {
|
|||||||
}
|
}
|
||||||
}, [address])
|
}, [address])
|
||||||
|
|
||||||
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 result = useQuery<Result>(
|
const result = useQuery<Result>(
|
||||||
queryKeys.creditAccounts(address),
|
queryKeys.creditAccounts(address),
|
||||||
async () => signingClient?.queryContractSmart(contractAddresses.accountNft, queryMsg),
|
async () => client?.queryContractSmart(contractAddresses.accountNft, queryMsg),
|
||||||
{
|
{
|
||||||
enabled: !!address && !!signingClient,
|
staleTime: Infinity,
|
||||||
|
enabled: !!address && !!client,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (!data.tokens.includes(selectedAccount || '') && data.tokens.length > 0) {
|
if (!data.tokens.includes(selectedAccount || '') && data.tokens.length > 0) {
|
||||||
creditManagerActions.setSelectedAccount(data.tokens[0])
|
creditManagerActions.setSelectedAccount(data.tokens[0])
|
||||||
|
100
hooks/useMarkets.tsx
Normal file
100
hooks/useMarkets.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
interface Market {
|
||||||
|
denom: string
|
||||||
|
max_loan_to_value: string
|
||||||
|
liquidation_threshold: string
|
||||||
|
liquidation_bonus: string
|
||||||
|
reserve_factor: string
|
||||||
|
interest_rate_model: {
|
||||||
|
optimal_utilization_rate: string
|
||||||
|
base: string
|
||||||
|
slope_1: string
|
||||||
|
slope_2: string
|
||||||
|
}
|
||||||
|
borrow_index: string
|
||||||
|
liquidity_index: string
|
||||||
|
borrow_rate: string
|
||||||
|
liquidity_rate: string
|
||||||
|
indexes_last_updated: number
|
||||||
|
collateral_total_scaled: string
|
||||||
|
debt_total_scaled: string
|
||||||
|
deposit_enabled: boolean
|
||||||
|
borrow_enabled: boolean
|
||||||
|
deposit_cap: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result {
|
||||||
|
wasm: {
|
||||||
|
[key: string]: Market
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMarkets = () => {
|
||||||
|
const result = useQuery<Result>(
|
||||||
|
['marketInfo'],
|
||||||
|
() => ({
|
||||||
|
wasm: {
|
||||||
|
uosmo: {
|
||||||
|
denom: 'uosmo',
|
||||||
|
max_loan_to_value: '0.7',
|
||||||
|
liquidation_threshold: '0.65',
|
||||||
|
liquidation_bonus: '0.1',
|
||||||
|
reserve_factor: '0.2',
|
||||||
|
interest_rate_model: {
|
||||||
|
optimal_utilization_rate: '0.7',
|
||||||
|
base: '0.3',
|
||||||
|
slope_1: '0.25',
|
||||||
|
slope_2: '0.3',
|
||||||
|
},
|
||||||
|
borrow_index: '1.002171957411401332',
|
||||||
|
liquidity_index: '1.00055035491698614',
|
||||||
|
borrow_rate: '0.1',
|
||||||
|
liquidity_rate: '0',
|
||||||
|
indexes_last_updated: 1664544343,
|
||||||
|
collateral_total_scaled: '89947659146708',
|
||||||
|
debt_total_scaled: '0',
|
||||||
|
deposit_enabled: true,
|
||||||
|
borrow_enabled: true,
|
||||||
|
deposit_cap: '1000000000000',
|
||||||
|
},
|
||||||
|
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': {
|
||||||
|
denom: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2',
|
||||||
|
max_loan_to_value: '0.8',
|
||||||
|
liquidation_threshold: '0.7',
|
||||||
|
liquidation_bonus: '0.1',
|
||||||
|
reserve_factor: '0.2',
|
||||||
|
interest_rate_model: {
|
||||||
|
optimal_utilization_rate: '0.1',
|
||||||
|
base: '0.3',
|
||||||
|
slope_1: '0.25',
|
||||||
|
slope_2: '0.3',
|
||||||
|
},
|
||||||
|
borrow_index: '1.000000224611044228',
|
||||||
|
liquidity_index: '1.000000023465246067',
|
||||||
|
borrow_rate: '0.25',
|
||||||
|
liquidity_rate: '0',
|
||||||
|
indexes_last_updated: 1664367327,
|
||||||
|
collateral_total_scaled: '0',
|
||||||
|
debt_total_scaled: '0',
|
||||||
|
deposit_enabled: true,
|
||||||
|
borrow_enabled: true,
|
||||||
|
deposit_cap: '1000000000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
staleTime: Infinity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: useMemo(() => {
|
||||||
|
return result?.data && result.data.wasm
|
||||||
|
}, [result.data]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMarkets
|
16
hooks/useTokenPrices.tsx
Normal file
16
hooks/useTokenPrices.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
const useTokenPrices = () => {
|
||||||
|
return useQuery<{ [key in string]: number }>(
|
||||||
|
['tokenPrices'],
|
||||||
|
() => ({
|
||||||
|
uosmo: 1.1,
|
||||||
|
'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2': 11,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
staleTime: Infinity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTokenPrices
|
@ -13,6 +13,15 @@ const nextConfig = {
|
|||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
|
||||||
hideSourceMaps: true,
|
hideSourceMaps: true,
|
||||||
},
|
},
|
||||||
|
webpack(config) {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.svg$/i,
|
||||||
|
issuer: /\.[jt]sx?$/,
|
||||||
|
use: ['@svgr/webpack'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentryWebpackPluginOptions = {
|
const sentryWebpackPluginOptions = {
|
||||||
|
@ -25,10 +25,12 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-toastify": "^9.0.8",
|
"react-toastify": "^9.0.8",
|
||||||
|
"use-local-storage-state": "^18.1.1",
|
||||||
"zustand": "^4.1.1"
|
"zustand": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@keplr-wallet/types": "^0.10.24",
|
"@keplr-wallet/types": "^0.10.24",
|
||||||
|
"@svgr/webpack": "^6.4.0",
|
||||||
"@types/node": "18.7.14",
|
"@types/node": "18.7.14",
|
||||||
"@types/react": "18.0.18",
|
"@types/react": "18.0.18",
|
||||||
"@types/react-dom": "18.0.6",
|
"@types/react-dom": "18.0.6",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { ToastContainer, Zoom } from 'react-toastify'
|
import { ToastContainer, Zoom } from 'react-toastify'
|
||||||
@ -7,7 +8,6 @@ import detectEthereumProvider from '@metamask/detect-provider'
|
|||||||
|
|
||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
import Layout from 'components/Layout'
|
import Layout from 'components/Layout'
|
||||||
import { useEffect } from 'react'
|
|
||||||
import useWalletStore from 'stores/useWalletStore'
|
import useWalletStore from 'stores/useWalletStore'
|
||||||
|
|
||||||
async function isMetamaskInstalled(): Promise<boolean> {
|
async function isMetamaskInstalled(): Promise<boolean> {
|
||||||
@ -19,6 +19,7 @@ async function isMetamaskInstalled(): Promise<boolean> {
|
|||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
|
const address = useWalletStore((s) => s.address)
|
||||||
const actions = useWalletStore((s) => s.actions)
|
const actions = useWalletStore((s) => s.actions)
|
||||||
|
|
||||||
// init store
|
// init store
|
||||||
@ -27,6 +28,8 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||||||
actions.setMetamaskInstalledStatus(await isMetamaskInstalled())
|
actions.setMetamaskInstalledStatus(await isMetamaskInstalled())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions.initialize()
|
||||||
|
|
||||||
verifyMetamask()
|
verifyMetamask()
|
||||||
}, [actions])
|
}, [actions])
|
||||||
|
|
||||||
@ -38,9 +41,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
</Head>
|
</Head>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Layout>
|
<Layout>{address ? <Component {...pageProps} /> : <div>No wallet connected</div>}</Layout>
|
||||||
<Component {...pageProps} />
|
|
||||||
</Layout>
|
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
autoClose={1500}
|
autoClose={1500}
|
||||||
closeButton={false}
|
closeButton={false}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Container from 'components/Container'
|
import Container from 'components/Container'
|
||||||
|
|
||||||
const Yield = () => {
|
const Earn = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Container className="flex-1">Yield Module</Container>
|
<Container className="flex-1">Yield Module</Container>
|
||||||
@ -10,4 +10,4 @@ const Yield = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Yield
|
export default Earn
|
@ -2,15 +2,18 @@ import create from 'zustand'
|
|||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
import { Wallet } from 'types'
|
import { Wallet } from 'types'
|
||||||
|
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||||
|
import { chain } from 'utils/chains'
|
||||||
|
|
||||||
interface WalletStore {
|
interface WalletStore {
|
||||||
address: string
|
address: string
|
||||||
injectiveAddress: string
|
|
||||||
addresses: string[]
|
|
||||||
metamaskInstalled: boolean
|
metamaskInstalled: boolean
|
||||||
wallet: Wallet
|
wallet: Wallet | null
|
||||||
|
client?: CosmWasmClient
|
||||||
actions: {
|
actions: {
|
||||||
setAddress: (address: string) => void
|
disconnect: () => void
|
||||||
|
initialize: () => void
|
||||||
|
connect: (address: string, wallet: Wallet) => void
|
||||||
setMetamaskInstalledStatus: (value: boolean) => void
|
setMetamaskInstalledStatus: (value: boolean) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -19,12 +22,24 @@ const useWalletStore = create<WalletStore>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
address: '',
|
address: '',
|
||||||
injectiveAddress: '',
|
|
||||||
addresses: [],
|
|
||||||
metamaskInstalled: false,
|
metamaskInstalled: false,
|
||||||
wallet: Wallet.Metamask,
|
wallet: null,
|
||||||
actions: {
|
actions: {
|
||||||
setAddress: (address: string) => set(() => ({ address })),
|
disconnect: () => {
|
||||||
|
set(() => ({ address: '', wallet: null }))
|
||||||
|
},
|
||||||
|
initialize: async () => {
|
||||||
|
const clientInstance = await CosmWasmClient.connect(chain.rpc)
|
||||||
|
let address = ''
|
||||||
|
|
||||||
|
if (get().wallet === Wallet.Keplr && window.keplr) {
|
||||||
|
const key = await window.keplr.getKey(chain.chainId)
|
||||||
|
address = key.bech32Address
|
||||||
|
}
|
||||||
|
|
||||||
|
set(() => ({ client: clientInstance, address }))
|
||||||
|
},
|
||||||
|
connect: (address: string, wallet: Wallet) => set(() => ({ address, wallet })),
|
||||||
setMetamaskInstalledStatus: (value: boolean) => set(() => ({ metamaskInstalled: value })),
|
setMetamaskInstalledStatus: (value: boolean) => set(() => ({ metamaskInstalled: value })),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -32,7 +47,9 @@ const useWalletStore = create<WalletStore>()(
|
|||||||
name: 'wallet',
|
name: 'wallet',
|
||||||
partialize: (state) =>
|
partialize: (state) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(state).filter(([key]) => !['metamaskInstalled', 'actions'].includes(key))
|
Object.entries(state).filter(
|
||||||
|
([key]) => !['client', 'metamaskInstalled', 'actions', 'address'].includes(key)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -6,8 +6,9 @@ html,
|
|||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -19,16 +20,6 @@ a {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
color: white;
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* react-toastify */
|
/* react-toastify */
|
||||||
/* https://fkhadra.github.io/react-toastify/how-to-style#override-css-variables */
|
/* https://fkhadra.github.io/react-toastify/how-to-style#override-css-variables */
|
||||||
.Toastify__toast {
|
.Toastify__toast {
|
||||||
|
@ -40,8 +40,8 @@ export const chainsInfo = {
|
|||||||
},
|
},
|
||||||
OsmosisTestnet: {
|
OsmosisTestnet: {
|
||||||
chainId: 'osmo-test-4',
|
chainId: 'osmo-test-4',
|
||||||
rpc: 'https://rpc-test.osmosis.zone',
|
rpc: 'https://osmosis-delphi-testnet-1.simply-vc.com.mt/XF32UOOU55CX/osmosis-rpc',
|
||||||
rest: 'https://lcd-test.osmosis.zone',
|
rest: 'https://osmosis-delphi-testnet-1.simply-vc.com.mt/XF32UOOU55CX/osmosis-lcd',
|
||||||
stakeCurrency: {
|
stakeCurrency: {
|
||||||
coinDenom: 'OSMO',
|
coinDenom: 'OSMO',
|
||||||
coinMinimalDenom: 'uosmo',
|
coinMinimalDenom: 'uosmo',
|
||||||
|
@ -9,5 +9,5 @@ export const hardcodedFee = {
|
|||||||
amount: '100000',
|
amount: '100000',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
gas: '750000',
|
gas: '1500000',
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user