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:
Gustavo Mauricio 2022-10-12 16:41:03 +01:00 committed by GitHub
parent f709c12da2
commit 3022ae9a6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1733 additions and 263 deletions

View File

@ -7,7 +7,7 @@ import { Coin } from '@cosmjs/stargate'
import { getInjectiveAddress } from 'utils/address'
import { getExperimentalChainConfigBasedOnChainId } from 'utils/experimental-chains'
import { ChainId } from 'types'
import { ChainId, Wallet } from 'types'
import useWalletStore from 'stores/useWalletStore'
import { chain } from 'utils/chains'
@ -48,7 +48,7 @@ const ConnectModal = ({ isOpen, onClose }: Props) => {
}
const key = await window.keplr.getKey(chain.chainId)
actions.setAddress(key.bech32Address)
actions.connect(key.bech32Address, Wallet.Keplr)
handleConnectSuccess()
} catch (e) {
@ -72,7 +72,7 @@ const ConnectModal = ({ isOpen, onClose }: Props) => {
method: 'eth_requestAccounts',
})
const [address] = addresses
actions.setAddress(getInjectiveAddress(address))
actions.connect(getInjectiveAddress(address), Wallet.Metamask)
handleConnectSuccess()
} catch (e) {
// TODO: handle exception

View File

@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'
import * as Slider from '@radix-ui/react-slider'
import BigNumber from 'bignumber.js'
import { Switch } from '@headlessui/react'
import useLocalStorageState from 'use-local-storage-state'
import Button from '../Button'
import useAllowedCoins from 'hooks/useAllowedCoins'
@ -14,10 +15,13 @@ import CreditManagerContainer from './CreditManagerContainer'
const FundAccount = () => {
const [amount, setAmount] = useState(0)
const [selectedToken, setSelectedToken] = useState('')
const [enabled, setEnabled] = useState(false)
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
const [lendAssets, setLendAssets] = useLocalStorageState(`lendAssets_${selectedAccount}`, {
defaultValue: false,
})
const { data: balancesData } = useAllBalances()
const { data: allowedCoinsData, isLoading: isLoadingAllowedCoins } = useAllowedCoins()
const { mutate } = useDepositCreditAccount(
@ -58,7 +62,7 @@ const FundAccount = () => {
return (
<>
<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 dont have
any assets in your injective wallet use the injective bridge to transfer funds to your
injective wallet.
@ -67,7 +71,7 @@ const FundAccount = () => {
<p>Loading...</p>
) : (
<>
<div className="mb-4">
<div className="mb-4 text-sm">
<div className="mb-1 flex justify-between">
<div>Asset:</div>
<select
@ -95,7 +99,7 @@ const FundAccount = () => {
/>
</div>
</div>
<p>In wallet: {walletAmount.toLocaleString()}</p>
<p className="text-sm">In wallet: {walletAmount.toLocaleString()}</p>
{/* SLIDER - initial implementation to test functionality */}
{/* TODO: will need to be revamped later on */}
<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">
<div>
<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>
<Switch
checked={enabled}
onChange={setEnabled}
checked={lendAssets}
onChange={setLendAssets}
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`}
>
<span
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`}
/>
</Switch>

View File

@ -5,10 +5,13 @@ import Button from '../Button'
import { formatCurrency } from 'utils/formatters'
import useCreditManagerStore from 'stores/useCreditManagerStore'
import useWalletStore from 'stores/useWalletStore'
import useCreditAccountBalances from 'hooks/useCreditAccountPositions'
import { getTokenDecimals } from 'utils/tokens'
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'
const CreditManager = () => {
const [isFund, setIsFund] = useState(false)
@ -16,19 +19,24 @@ const CreditManager = () => {
const address = useWalletStore((s) => s.address)
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountBalances(
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
selectedAccount ?? ''
)
const totalPosition =
positionsData?.coins.reduce((acc, coin) => {
return Number(coin.value) + acc
}, 0) ?? 0
const { data: tokenPrices } = useTokenPrices()
const { data: marketsData } = useMarkets()
const accountStats = useAccountStats()
const totalDebt =
positionsData?.debt.reduce((acc, coin) => {
return Number(coin.value) + acc
}, 0) ?? 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))
.toNumber() * tokenPrices[denom]
)
}
if (!address) {
return (
@ -66,11 +74,13 @@ const CreditManager = () => {
<CreditManagerContainer className="mb-2 text-sm">
<div className="mb-1 flex justify-between">
<div>Total Position:</div>
<div className="font-semibold">{formatCurrency(totalPosition)}</div>
<div className="font-semibold">
{formatCurrency(accountStats?.totalPosition ?? 0)}
</div>
</div>
<div className="flex justify-between">
<div>Total Liabilities:</div>
<div className="font-semibold">{formatCurrency(totalDebt)}</div>
<div className="font-semibold">{formatCurrency(accountStats?.totalDebt ?? 0)}</div>
</div>
</CreditManagerContainer>
<CreditManagerContainer>
@ -87,8 +97,10 @@ const CreditManager = () => {
</div>
{positionsData?.coins.map((coin) => (
<div key={coin.denom} className="flex text-xs text-black/40">
<div className="flex-1">{coin.denom}</div>
<div className="flex-1">{formatCurrency(coin.value)}</div>
<div className="flex-1">{getTokenSymbol(coin.denom)}</div>
<div className="flex-1">
{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
</div>
<div className="flex-1">
{BigNumber(coin.amount)
.div(10 ** getTokenDecimals(coin.denom))
@ -100,11 +112,14 @@ const CreditManager = () => {
<div className="flex-1">-</div>
</div>
))}
{positionsData?.debt.map((coin) => (
{positionsData?.debts.map((coin) => (
<div key={coin.denom} className="flex text-xs text-red-500">
<div className="flex-1">{coin.denom}</div>
<div className="flex-1">{formatCurrency(coin.value)}</div>
<div className="flex-1 text-black/40">{getTokenSymbol(coin.denom)}</div>
<div className="flex-1">
-{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
</div>
<div className="flex-1">
-
{BigNumber(coin.amount)
.div(10 ** getTokenDecimals(coin.denom))
.toNumber()
@ -112,7 +127,9 @@ const CreditManager = () => {
maximumFractionDigits: 6,
})}
</div>
<div className="flex-1">-</div>
<div className="flex-1">
{(Number(marketsData?.[coin.denom].borrow_rate) * 100).toFixed(1)}%
</div>
</div>
))}
</>

View 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

View File

@ -13,13 +13,17 @@ import useCreditAccounts from 'hooks/useCreditAccounts'
import useCreateCreditAccount from 'hooks/useCreateCreditAccount'
import useDeleteCreditAccount from 'hooks/useDeleteCreditAccount'
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
const MAX_VISIBLE_CREDIT_ACCOUNTS = 5
const navItems = [
{ href: '/trade', label: 'Trade' },
{ href: '/yield', label: 'Yield' },
{ href: '/earn', label: 'Earn' },
{ href: '/borrow', label: 'Borrow' },
{ href: '/portfolio', label: 'Portfolio' },
{ href: '/council', label: 'Council' },
@ -38,6 +42,7 @@ const NavLink = ({ href, children }: { href: string; children: string }) => {
}
const Navigation = () => {
const address = useWalletStore((s) => s.address)
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
const setSelectedAccount = useCreditManagerStore((s) => s.actions.setSelectedAccount)
const toggleCreditManager = useCreditManagerStore((s) => s.actions.toggleCreditManager)
@ -48,6 +53,8 @@ const Navigation = () => {
selectedAccount || ''
)
const accountStats = useAccountStats()
const { firstCreditAccounts, restCreditAccounts } = useMemo(() => {
return {
firstCreditAccounts: creditAccountsList?.slice(0, MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
@ -74,105 +81,115 @@ const Navigation = () => {
<Wallet />
</div>
{/* Sub navigation bar */}
<div className="flex justify-between border-b border-white/20 px-6 py-3 text-sm text-white/40">
<div className="flex items-center">
<SearchInput />
{firstCreditAccounts.map((account) => (
<div
key={account}
className={`cursor-pointer px-4 hover:text-white ${
selectedAccount === account ? 'text-white' : ''
}`}
onClick={() => setSelectedAccount(account)}
>
Account {account}
</div>
))}
{restCreditAccounts.length > 0 && (
{address && (
<div className="flex justify-between border-b border-white/20 px-6 py-3 text-sm text-white/40">
<div className="flex items-center">
<SearchInput />
{firstCreditAccounts.map((account) => (
<div
key={account}
className={`cursor-pointer px-4 hover:text-white ${
selectedAccount === account ? 'text-white' : ''
}`}
onClick={() => setSelectedAccount(account)}
>
Account {account}
</div>
))}
{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.Button>
<div className="flex cursor-pointer items-center px-3 hover:text-white">
More
Manage
<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) => (
{({ close }) => (
<div className="rounded-2xl bg-white p-4 text-gray-900">
<div
key={account}
className={`cursor-pointer hover:text-orange-500 ${
selectedAccount === account ? 'text-orange-500' : ''
}`}
onClick={() => setSelectedAccount(account)}
className="mb-2 cursor-pointer hover:text-orange-500"
onClick={() => {
close()
createCreditAccount()
}}
>
Account {account}
Create Account
</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>
)}
<Popover className="relative">
<Popover.Button>
<div className="flex cursor-pointer items-center px-3 hover:text-white">
Manage
<ChevronDownIcon className="ml-1 h-4 w-4" />
</div>
</Popover.Button>
<Popover.Panel className="absolute z-10 w-[200px] pt-2">
{({ close }) => (
<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 className="flex items-center gap-4">
{accountStats && (
<>
<p>{formatCurrency(accountStats.netWorth)}</p>
{/* TOOLTIP */}
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
<SemiCircleProgress
value={accountStats.currentLeverage / accountStats.maxLeverage}
label="Lvg"
/>
</div>
)}
</Popover.Panel>
</Popover>
</div>
<div className="flex items-center gap-4">
<p>{formatCurrency(2500)}</p>
<div>Lvg</div>
<div>Risk</div>
<ProgressBar value={0.43} />
<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>
<SemiCircleProgress value={accountStats.risk} label="Risk" />
<ProgressBar value={accountStats.health} />
</>
)}
<div
className="flex w-16 cursor-pointer justify-center hover:text-white"
onClick={toggleCreditManager}
>
<ArrowRightLine />
</div>
</div>
</div>
</div>
)}
{(isLoadingCreate || isLoadingDelete) && (
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Spinner />

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'
import { ArrowRightIcon } from '@heroicons/react/24/solid'
import React from 'react'
type Props = {
value: number
@ -7,16 +6,6 @@ type Props = {
const ProgressBar = ({ value }: Props) => {
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 (
<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"
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">
{percentageValue}
<ArrowRightIcon className="h-3 w-3" />
{percentageNewValue}
</div>
</div>
)

View 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

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { Popover } from '@headlessui/react'
import { toast } from 'react-toastify'
import Image from 'next/image'
@ -34,7 +34,7 @@ const WalletPopover = ({ children }: { children: React.ReactNode }) => {
</div>
<Button
className=" bg-[#524bb1] hover:bg-[#6962cc]"
onClick={() => actions.setAddress('')}
onClick={() => actions.disconnect()}
>
Disconnect
</Button>
@ -71,18 +71,12 @@ const WalletPopover = ({ children }: { children: React.ReactNode }) => {
const Wallet = () => {
const [showConnectModal, setShowConnectModal] = useState(false)
const [hasHydrated, setHasHydrated] = useState<boolean>(false)
const address = useWalletStore((s) => s.address)
// avoid server-client hydration mismatch
useEffect(() => {
setHasHydrated(true)
}, [])
return (
<>
{hasHydrated && address ? (
{address ? (
<WalletPopover>{formatWalletAddress(address)}</WalletPopover>
) : (
<Button className="w-[200px]" onClick={() => setShowConnectModal(true)}>

View File

@ -1,9 +1,8 @@
// https://github.com/mars-protocol/rover/blob/master/scripts/deploy/addresses/osmo-test-4.json
export const contractAddresses = {
accountNft: 'osmo16v3mvsdnkh4c6ykc885n3x5ay9e36akdzxcl2g93698rqw007xxqesld8w',
mockRedBank: 'osmo1xrnx0q3x7kwzss53fry0dwwsc7pff6aq628l6n0rmvegkalp4y7qzl7j7z',
mockOracle: 'osmo1r9u2tfq8n5xpn2g0fq8ha0rj0cyp2fzr5w9jvcqwt3r8lxdfm6yszmtza5',
mockVault: 'osmo1gg4rpug7vwrnq0ask0k7nmw23z6wl8c8fr7jmup9pdpaal9uc5nqq7lyrm',
swapper: 'osmo1ak4x8k2h7s6pq5dnlncmgsmx2nqcaplpfxlmklx2ln7qn6dtny8q70apjv',
creditManager: 'osmo1963xgmt8agyc6q4k2vhf980kffq6ukkj9mgtwdxxnpj3dak2akdq20z9dw',
accountNft: 'osmo1dravtyd0425fkdmkysc3ns7zud05clf5uhj6qqsnkdtrpkewu73q9f3f02',
mockVault: 'osmo1emcckulm2mkx36xeanhsn3z3zjeql6pgd8yf8a5cf03ccvy7a4dqjw9tl7',
marsOracleAdapter: 'osmo1cw6pv97g7fmhqykrn0gc9ngrx5tnky75rmlwkzxuqhsk58u0n8asz036g0',
swapper: 'osmo1w2552km2u9w4k2gjw4n8drmuz5yxw8x4qzy6dl3da824km5cjlys00x3qp',
creditManager: 'osmo18dt5y0ecyd5qg8nqwzrgxuljfejglyh2fjd984s8cy7fcx8mxh9qfl3hwq',
}

86
hooks/useAccountStats.tsx Normal file
View 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

View File

@ -1,9 +1,6 @@
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import useWalletStore from 'stores/useWalletStore'
import { chain } from 'utils/chains'
import { contractAddresses } from 'config/contracts'
import { queryKeys } from 'types/query-keys-factory'
@ -14,25 +11,14 @@ const queryMsg = {
}
const useAllowedCoins = () => {
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
const address = useWalletStore((s) => s.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 client = useWalletStore((s) => s.client)
const result = useQuery<Result>(
queryKeys.allowedCoins(),
async () => signingClient?.queryContractSmart(contractAddresses.creditManager, queryMsg),
async () => client?.queryContractSmart(contractAddresses.creditManager, queryMsg),
{
enabled: !!address && !!signingClient,
enabled: !!address && !!client,
staleTime: Infinity,
}
)

View File

@ -1,69 +1,43 @@
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
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 { chain } from 'utils/chains'
import { contractAddresses } from 'config/contracts'
import { queryKeys } from 'types/query-keys-factory'
interface CoinValue {
interface DebtAmount {
amount: string
denom: string
price: string
value: string
}
interface DebtSharesValue {
amount: string
denom: string
price: string
shares: string
value: string
}
export interface VaultPosition {
interface VaultPosition {
locked: string
unlocked: string
}
interface VaultPositionWithAddr {
addr: string
position: VaultPosition
}
interface Result {
account_id: string
coins: CoinValue[]
debt: DebtSharesValue[]
vault_positions: VaultPositionWithAddr[]
coins: Coin[]
debts: DebtAmount[]
vaults: VaultPosition[]
}
const useCreditAccountPositions = (accountId: string) => {
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
const address = useWalletStore((s) => s.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 client = useWalletStore((s) => s.client)
const result = useQuery<Result>(
queryKeys.creditAccountsPositions(accountId),
async () =>
signingClient?.queryContractSmart(contractAddresses.creditManager, {
client?.queryContractSmart(contractAddresses.creditManager, {
positions: {
account_id: accountId,
},
}),
{
enabled: !!address && !!signingClient,
enabled: !!address && !!client,
staleTime: Infinity,
}
)
@ -71,7 +45,7 @@ const useCreditAccountPositions = (accountId: string) => {
return {
...result,
data: useMemo(() => {
return result?.data
return result?.data && { ...result.data }
}, [result.data]),
}
}

View File

@ -1,9 +1,7 @@
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import useWalletStore from 'stores/useWalletStore'
import { chain } from 'utils/chains'
import { contractAddresses } from 'config/contracts'
import useCreditManagerStore from 'stores/useCreditManagerStore'
import { queryKeys } from 'types/query-keys-factory'
@ -13,8 +11,8 @@ type Result = {
}
const useCreditAccounts = () => {
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
const address = useWalletStore((s) => s.address)
const client = useWalletStore((s) => s.client)
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
const creditManagerActions = useCreditManagerStore((s) => s.actions)
@ -26,22 +24,12 @@ const useCreditAccounts = () => {
}
}, [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>(
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) => {
if (!data.tokens.includes(selectedAccount || '') && data.tokens.length > 0) {
creditManagerActions.setSelectedAccount(data.tokens[0])

100
hooks/useMarkets.tsx Normal file
View 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
View 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

View File

@ -13,6 +13,15 @@ const nextConfig = {
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
hideSourceMaps: true,
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
})
return config
},
}
const sentryWebpackPluginOptions = {

View File

@ -25,10 +25,12 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-toastify": "^9.0.8",
"use-local-storage-state": "^18.1.1",
"zustand": "^4.1.1"
},
"devDependencies": {
"@keplr-wallet/types": "^0.10.24",
"@svgr/webpack": "^6.4.0",
"@types/node": "18.7.14",
"@types/react": "18.0.18",
"@types/react-dom": "18.0.6",

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import { ToastContainer, Zoom } from 'react-toastify'
@ -7,7 +8,6 @@ import detectEthereumProvider from '@metamask/detect-provider'
import '../styles/globals.css'
import Layout from 'components/Layout'
import { useEffect } from 'react'
import useWalletStore from 'stores/useWalletStore'
async function isMetamaskInstalled(): Promise<boolean> {
@ -19,6 +19,7 @@ async function isMetamaskInstalled(): Promise<boolean> {
const queryClient = new QueryClient()
function MyApp({ Component, pageProps }: AppProps) {
const address = useWalletStore((s) => s.address)
const actions = useWalletStore((s) => s.actions)
// init store
@ -27,6 +28,8 @@ function MyApp({ Component, pageProps }: AppProps) {
actions.setMetamaskInstalledStatus(await isMetamaskInstalled())
}
actions.initialize()
verifyMetamask()
}, [actions])
@ -38,9 +41,7 @@ function MyApp({ Component, pageProps }: AppProps) {
<link rel="icon" href="/favicon.svg" />
</Head>
<QueryClientProvider client={queryClient}>
<Layout>
<Component {...pageProps} />
</Layout>
<Layout>{address ? <Component {...pageProps} /> : <div>No wallet connected</div>}</Layout>
<ToastContainer
autoClose={1500}
closeButton={false}

View File

@ -1,7 +1,7 @@
import React from 'react'
import Container from 'components/Container'
const Yield = () => {
const Earn = () => {
return (
<div className="flex gap-4">
<Container className="flex-1">Yield Module</Container>
@ -10,4 +10,4 @@ const Yield = () => {
)
}
export default Yield
export default Earn

View File

@ -2,15 +2,18 @@ import create from 'zustand'
import { persist } from 'zustand/middleware'
import { Wallet } from 'types'
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { chain } from 'utils/chains'
interface WalletStore {
address: string
injectiveAddress: string
addresses: string[]
metamaskInstalled: boolean
wallet: Wallet
wallet: Wallet | null
client?: CosmWasmClient
actions: {
setAddress: (address: string) => void
disconnect: () => void
initialize: () => void
connect: (address: string, wallet: Wallet) => void
setMetamaskInstalledStatus: (value: boolean) => void
}
}
@ -19,12 +22,24 @@ const useWalletStore = create<WalletStore>()(
persist(
(set, get) => ({
address: '',
injectiveAddress: '',
addresses: [],
metamaskInstalled: false,
wallet: Wallet.Metamask,
wallet: null,
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 })),
},
}),
@ -32,7 +47,9 @@ const useWalletStore = create<WalletStore>()(
name: 'wallet',
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) => !['metamaskInstalled', 'actions'].includes(key))
Object.entries(state).filter(
([key]) => !['client', 'metamaskInstalled', 'actions', 'address'].includes(key)
)
),
}
)

View File

@ -6,8 +6,9 @@ html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
color: white;
}
a {
@ -19,16 +20,6 @@ a {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
/* react-toastify */
/* https://fkhadra.github.io/react-toastify/how-to-style#override-css-variables */
.Toastify__toast {

View File

@ -40,8 +40,8 @@ export const chainsInfo = {
},
OsmosisTestnet: {
chainId: 'osmo-test-4',
rpc: 'https://rpc-test.osmosis.zone',
rest: 'https://lcd-test.osmosis.zone',
rpc: 'https://osmosis-delphi-testnet-1.simply-vc.com.mt/XF32UOOU55CX/osmosis-rpc',
rest: 'https://osmosis-delphi-testnet-1.simply-vc.com.mt/XF32UOOU55CX/osmosis-lcd',
stakeCurrency: {
coinDenom: 'OSMO',
coinMinimalDenom: 'uosmo',

View File

@ -9,5 +9,5 @@ export const hardcodedFee = {
amount: '100000',
},
],
gas: '750000',
gas: '1500000',
}

1205
yarn.lock

File diff suppressed because it is too large Load Diff