Mp 2548 deposit into vault (#264)

* Implement vault deposit message

* Merge custom Coin into BNCoin

* Fix build errors

* fixed tests
This commit is contained in:
Bob van der Helm 2023-06-26 10:08:45 +02:00 committed by GitHub
parent c69461b95d
commit 415da05b8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 576 additions and 159 deletions

View File

@ -5,6 +5,7 @@ import { BN } from 'utils/helpers'
import useStore from 'store' import useStore from 'store'
import DisplayCurrency from 'components/DisplayCurrency' import DisplayCurrency from 'components/DisplayCurrency'
import VaultBorrowings, { VaultBorrowingsProps } from 'components/Modals/Vault/VaultBorrowings' import VaultBorrowings, { VaultBorrowingsProps } from 'components/Modals/Vault/VaultBorrowings'
import { TESTNET_VAULTS_META_DATA } from 'constants/vaults'
jest.mock('hooks/usePrices', () => jest.mock('hooks/usePrices', () =>
jest.fn(() => ({ jest.fn(() => ({
@ -20,11 +21,27 @@ jest.mock('hooks/useMarketAssets', () =>
})), })),
) )
jest.mock('hooks/broadcast/useDepositVault', () => jest.fn(() => ({})))
jest.mock('components/DisplayCurrency') jest.mock('components/DisplayCurrency')
const mockedDisplayCurrency = jest const mockedDisplayCurrency = jest
.mocked(DisplayCurrency) .mocked(DisplayCurrency)
.mockImplementation(() => <div>Display currency</div>) .mockImplementation(() => <div>Display currency</div>)
const mockedVault: Vault = {
...TESTNET_VAULTS_META_DATA[0],
apy: 0,
ltv: {
liq: 0.2,
max: 0.1,
},
cap: {
denom: 'test',
max: 10,
used: 2,
},
}
describe('<VaultBorrowings />', () => { describe('<VaultBorrowings />', () => {
const defaultProps: VaultBorrowingsProps = { const defaultProps: VaultBorrowingsProps = {
primaryAsset: ASSETS[0], primaryAsset: ASSETS[0],
@ -38,7 +55,9 @@ describe('<VaultBorrowings />', () => {
vaults: [], vaults: [],
lends: [], lends: [],
}, },
vault: mockedVault,
borrowings: [], borrowings: [],
deposits: [],
onChangeBorrowings: jest.fn(), onChangeBorrowings: jest.fn(),
} }

View File

@ -21,6 +21,7 @@
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"bignumber.js": "^9.1.1", "bignumber.js": "^9.1.1",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"debounce-promise": "^3.1.2",
"moment": "^2.29.4", "moment": "^2.29.4",
"next": "^13.4.7", "next": "^13.4.7",
"react": "^18.2.0", "react": "^18.2.0",
@ -41,6 +42,7 @@
"@svgr/webpack": "^8.0.1", "@svgr/webpack": "^8.0.1",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/debounce-promise": "^3.1.6",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/react": "18.2.14", "@types/react": "18.2.14",
"@types/react-dom": "18.2.4", "@types/react-dom": "18.2.4",

View File

@ -1,10 +1,11 @@
import getAccount from 'api/accounts/getAccount' import getAccount from 'api/accounts/getAccount'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getAccountDebts(accountId: string): Promise<Coin[]> { export default async function getAccountDebts(accountId: string): Promise<BNCoin[]> {
const account = await getAccount(accountId) const account = await getAccount(accountId)
if (account) { if (account) {
return account.debts return account.debts.map((coin) => new BNCoin(coin))
} }
return new Promise((_, reject) => reject('Account not found')) return new Promise((_, reject) => reject('Account not found'))

View File

@ -1,10 +1,11 @@
import getAccount from 'api/accounts/getAccount' import getAccount from 'api/accounts/getAccount'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getAccountDeposits(accountId: string) { export default async function getAccountDeposits(accountId: string): Promise<BNCoin[]> {
const account = await getAccount(accountId) const account = await getAccount(accountId)
if (account) { if (account) {
return account.deposits return account.deposits.map((coin) => new BNCoin(coin))
} }
return new Promise((_, reject) => reject('Account not found')) return new Promise((_, reject) => reject('Account not found'))

View File

@ -8,7 +8,9 @@ import getMarsPrice from 'api/prices/getMarsPrice'
import { ASSETS } from 'constants/assets' import { ASSETS } from 'constants/assets'
import { byDenom } from 'utils/array' import { byDenom } from 'utils/array'
export default async function calculateAssetIncentivesApy(denom: string): Promise<number | null> { export default async function calculateAssetIncentivesApy(
denom: string,
): Promise<BigNumber | null> {
try { try {
const [assetIncentive, market] = await Promise.all([getAssetIncentive(denom), getMarket(denom)]) const [assetIncentive, market] = await Promise.all([getAssetIncentive(denom), getMarket(denom)])
@ -38,7 +40,7 @@ export default async function calculateAssetIncentivesApy(denom: string): Promis
const totalAnnualReturnsValue = annualEmission.plus(marketReturns) const totalAnnualReturnsValue = annualEmission.plus(marketReturns)
const incentivesApy = totalAnnualReturnsValue.dividedBy(marketLiquidityValue).multipliedBy(100) const incentivesApy = totalAnnualReturnsValue.dividedBy(marketLiquidityValue).multipliedBy(100)
return incentivesApy.toNumber() return incentivesApy
} catch (ex) { } catch (ex) {
console.error(ex) console.error(ex)
return null return null

View File

@ -19,8 +19,8 @@ export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
...asset, ...asset,
borrowRate: market.borrowRate ?? 0, borrowRate: market.borrowRate ?? 0,
liquidity: { liquidity: {
amount: amount, amount: BN(amount),
value: BN(amount).times(price).toString(), value: BN(amount).times(price),
}, },
} }
}) })

View File

@ -1,7 +1,8 @@
import getMarkets from 'api/markets/getMarkets' import getMarkets from 'api/markets/getMarkets'
import { getRedBankQueryClient } from 'api/cosmwasm-client' import { getRedBankQueryClient } from 'api/cosmwasm-client'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketDebts(): Promise<Coin[]> { export default async function getMarketDebts(): Promise<BNCoin[]> {
try { try {
const markets: Market[] = await getMarkets() const markets: Market[] = await getMarkets()
const redBankQueryClient = await getRedBankQueryClient() const redBankQueryClient = await getRedBankQueryClient()
@ -14,7 +15,9 @@ export default async function getMarketDebts(): Promise<Coin[]> {
) )
const debtsResults = await Promise.all(debtQueries) const debtsResults = await Promise.all(debtQueries)
return debtsResults.map<Coin>((debt, index) => ({ denom: markets[index].denom, amount: debt })) return debtsResults.map<BNCoin>(
(debt, index) => new BNCoin({ denom: markets[index].denom, amount: debt }),
)
} catch (ex) { } catch (ex) {
throw ex throw ex
} }

View File

@ -1,16 +1,20 @@
import getMarkets from 'api/markets/getMarkets' import getMarkets from 'api/markets/getMarkets'
import getUnderlyingLiquidityAmount from 'api/markets/getMarketUnderlyingLiquidityAmount' import getUnderlyingLiquidityAmount from 'api/markets/getMarketUnderlyingLiquidityAmount'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketDeposits(): Promise<Coin[]> { export default async function getMarketDeposits(): Promise<BNCoin[]> {
try { try {
const markets: Market[] = await getMarkets() const markets: Market[] = await getMarkets()
const depositQueries = markets.map(getUnderlyingLiquidityAmount) const depositQueries = markets.map(getUnderlyingLiquidityAmount)
const depositsResults = await Promise.all(depositQueries) const depositsResults = await Promise.all(depositQueries)
return depositsResults.map<Coin>((deposit, index) => ({ return depositsResults.map<BNCoin>(
(deposit, index) =>
new BNCoin({
denom: markets[index].denom, denom: markets[index].denom,
amount: deposit, amount: deposit,
})) }),
)
} catch (ex) { } catch (ex) {
throw ex throw ex
} }

View File

@ -1,25 +1,26 @@
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import getMarketDeposits from 'api/markets/getMarketDeposits' import getMarketDeposits from 'api/markets/getMarketDeposits'
import getMarketDebts from 'api/markets/getMarketDebts' import getMarketDebts from 'api/markets/getMarketDebts'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getMarketLiquidities(): Promise<Coin[]> { export default async function getMarketLiquidities(): Promise<BNCoin[]> {
const deposits = await getMarketDeposits() const deposits = await getMarketDeposits()
const debts = await getMarketDebts() const debts = await getMarketDebts()
const liquidity: Coin[] = deposits.map((deposit) => { const liquidity: BNCoin[] = deposits.map((deposit) => {
const debt = debts.find((debt) => debt.denom === deposit.denom) const debt = debts.find((debt) => debt.denom === deposit.denom)
if (debt) { if (debt) {
return { return new BNCoin({
denom: deposit.denom, denom: deposit.denom,
amount: BN(deposit.amount).minus(debt.amount).toString(), amount: deposit.amount.minus(debt.amount).toString(),
} })
} }
return { return new BNCoin({
denom: deposit.denom, denom: deposit.denom,
amount: '0', amount: '0',
} })
}) })
if (liquidity) { if (liquidity) {

View File

@ -0,0 +1,19 @@
import { getCreditManagerQueryClient } from 'api/cosmwasm-client'
import { ENV } from 'constants/env'
import { BN } from 'utils/helpers'
export default async function getVaultConfigs(coins: Coin[], lpDenom: string): Promise<BigNumber> {
if (!ENV.ADDRESS_CREDIT_MANAGER) return BN(0)
const creditManagerQueryClient = await getCreditManagerQueryClient()
try {
return BN(
await creditManagerQueryClient.estimateProvideLiquidity({
coinsIn: coins,
lpTokenOut: lpDenom,
}),
)
} catch (ex) {
throw ex
}
}

View File

@ -16,6 +16,7 @@ import Text from 'components/Text'
import { ASSETS } from 'constants/assets' import { ASSETS } from 'constants/assets'
import useStore from 'store' import useStore from 'store'
import { convertToDisplayAmount, demagnify } from 'utils/formatters' import { convertToDisplayAmount, demagnify } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props { interface Props {
data: Account data: Account
@ -43,7 +44,7 @@ export const AccountBalancesTable = (props: Props) => {
{ amount: deposit.amount, denom: deposit.denom }, { amount: deposit.amount, denom: deposit.denom },
displayCurrency, displayCurrency,
prices, prices,
), ).toString(),
apy, apy,
} }
}) })
@ -60,7 +61,7 @@ export const AccountBalancesTable = (props: Props) => {
{ amount: lending.amount, denom: lending.denom }, { amount: lending.amount, denom: lending.denom },
displayCurrency, displayCurrency,
prices, prices,
), ).toString(),
apy, apy,
} }
}) })
@ -104,9 +105,11 @@ export const AccountBalancesTable = (props: Props) => {
return ( return (
<FormattedNumber <FormattedNumber
className='text-right text-xs' className='text-right text-xs'
amount={demagnify( amount={BN(
demagnify(
row.original.amount, row.original.amount,
ASSETS.find((asset) => asset.denom === row.original.denom) ?? ASSETS[0], ASSETS.find((asset) => asset.denom === row.original.denom) ?? ASSETS[0],
),
)} )}
options={{ maxDecimals: 4 }} options={{ maxDecimals: 4 }}
/> />
@ -121,7 +124,7 @@ export const AccountBalancesTable = (props: Props) => {
return ( return (
<FormattedNumber <FormattedNumber
className='text-xs' className='text-xs'
amount={row.original.apy} amount={BN(row.original.apy)}
options={{ maxDecimals: 2, minDecimals: 2, suffix: '%' }} options={{ maxDecimals: 2, minDecimals: 2, suffix: '%' }}
/> />
) )

View File

@ -90,7 +90,7 @@ function Item(props: ItemProps) {
<div className='flex flex-grow items-center justify-end gap-2'> <div className='flex flex-grow items-center justify-end gap-2'>
{props.isPercentage ? ( {props.isPercentage ? (
<FormattedNumber <FormattedNumber
amount={props.current.toString()} amount={props.current}
options={{ suffix: '%', minDecimals: 2, maxDecimals: 2 }} options={{ suffix: '%', minDecimals: 2, maxDecimals: 2 }}
className='text-sm' className='text-sm'
/> />
@ -107,7 +107,7 @@ function Item(props: ItemProps) {
</span> </span>
{props.isPercentage ? ( {props.isPercentage ? (
<FormattedNumber <FormattedNumber
amount={props.change.toString()} amount={props.change}
options={{ suffix: '%', minDecimals: 2, maxDecimals: 2 }} options={{ suffix: '%', minDecimals: 2, maxDecimals: 2 }}
className={classNames('text-sm', increase ? 'text-profit' : 'text-loss')} className={classNames('text-sm', increase ? 'text-profit' : 'text-loss')}
/> />

View File

@ -13,17 +13,18 @@ import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text' import Text from 'components/Text'
import useStore from 'store' import useStore from 'store'
import { formatValue } from 'utils/formatters' import { formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
export const RiskChart = ({ data }: RiskChartProps) => { export const RiskChart = ({ data }: RiskChartProps) => {
const enableAnimations = useStore((s) => s.enableAnimations) const enableAnimations = useStore((s) => s.enableAnimations)
const accountStats = null const accountStats = null
const currentRisk = 0 const currentRisk = BN(0)
return ( return (
<div className='flex w-full flex-wrap overflow-hidden py-2'> <div className='flex w-full flex-wrap overflow-hidden py-2'>
<FormattedNumber <FormattedNumber
className='px-3 pb-2 text-lg' className='px-3 pb-2 text-lg'
amount={currentRisk * 100} amount={currentRisk.times(100)}
options={{ options={{
maxDecimals: 0, maxDecimals: 0,
minDecimals: 0, minDecimals: 0,

View File

@ -10,7 +10,7 @@ export async function AccountDebtTable(props: Props) {
return debtData.map((debt) => { return debtData.map((debt) => {
return ( return (
<p key={debt.denom}> <p key={debt.denom}>
{debt.denom} {debt.amount} {debt.denom} {debt.amount.toString()}
</p> </p>
) )
}) })

View File

@ -1,10 +1,11 @@
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import TitleAndSubCell from 'components/TitleAndSubCell' import TitleAndSubCell from 'components/TitleAndSubCell'
import DisplayCurrency from 'components/DisplayCurrency' import DisplayCurrency from 'components/DisplayCurrency'
import { BNCoin } from 'types/classes/BNCoin'
interface Props { interface Props {
asset: Asset asset: Asset
amount: string amount: BigNumber
} }
export default function AmountAndValue(props: Props) { export default function AmountAndValue(props: Props) {
@ -16,7 +17,11 @@ export default function AmountAndValue(props: Props) {
options={{ decimals: props.asset.decimals, abbreviated: true }} options={{ decimals: props.asset.decimals, abbreviated: true }}
/> />
} }
sub={<DisplayCurrency coin={{ amount: props.amount, denom: props.asset.denom }} />} sub={
<DisplayCurrency
coin={new BNCoin({ amount: props.amount.toString(), denom: props.asset.denom })}
/>
}
className='justify-end' className='justify-end'
/> />
) )

View File

@ -7,7 +7,6 @@ import {
useReactTable, useReactTable,
} from '@tanstack/react-table' } from '@tanstack/react-table'
import classNames from 'classnames' import classNames from 'classnames'
import Image from 'next/image'
import React from 'react' import React from 'react'
import AmountAndValue from 'components/AmountAndValue' import AmountAndValue from 'components/AmountAndValue'

View File

@ -5,6 +5,7 @@ import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text' import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip' import { Tooltip } from 'components/Tooltip'
import useStore from 'store' import useStore from 'store'
import { BN } from 'utils/helpers'
interface Props { interface Props {
balance: number balance: number
@ -68,7 +69,7 @@ export const BorrowCapacity = ({
limitPercentOfMax ? 'opacity-50' : 'opacity-0', limitPercentOfMax ? 'opacity-50' : 'opacity-0',
)} )}
> >
<FormattedNumber animate amount={limit} /> <FormattedNumber animate amount={BN(limit)} />
</div> </div>
)} )}
</div> </div>
@ -126,7 +127,7 @@ export const BorrowCapacity = ({
maxDecimals: decimals, maxDecimals: decimals,
suffix: '%', suffix: '%',
}} }}
amount={percentOfMaxRound} amount={BN(percentOfMaxRound)}
/> />
)} )}
</span> </span>
@ -138,9 +139,9 @@ export const BorrowCapacity = ({
</Tooltip> </Tooltip>
{!hideValues && ( {!hideValues && (
<div className='mt-2 flex opacity-50 text-3xs-caps'> <div className='mt-2 flex opacity-50 text-3xs-caps'>
<FormattedNumber animate amount={balance} className='mr-1' /> <FormattedNumber animate amount={BN(balance)} className='mr-1' />
<span className='mr-1'>of</span> <span className='mr-1'>of</span>
<FormattedNumber animate amount={max} /> <FormattedNumber animate amount={BN(max)} />
</div> </div>
)} )}
</div> </div>

View File

@ -1,9 +1,10 @@
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import useStore from 'store' import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { convertToDisplayAmount } from 'utils/formatters' import { convertToDisplayAmount } from 'utils/formatters'
interface Props { interface Props {
coin: Coin coin: BNCoin | Coin
className?: string className?: string
isApproximation?: boolean isApproximation?: boolean
} }

View File

@ -64,7 +64,7 @@ function LendingMarketsTable(props: Props) {
accessorKey: 'accountDepositValue', accessorKey: 'accountDepositValue',
header: 'Deposited', header: 'Deposited',
cell: ({ row }) => { cell: ({ row }) => {
const accountDepositValue = (row.original.accountLentValue as BigNumber).toNumber() const accountDepositValue = row.original.accountLentValue as BigNumber
return ( return (
<FormattedNumber <FormattedNumber

View File

@ -4,9 +4,10 @@ import { animated, useSpring } from 'react-spring'
import useStore from 'store' import useStore from 'store'
import { FormatOptions, formatValue } from 'utils/formatters' import { FormatOptions, formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props { interface Props {
amount: number | string amount: BigNumber
options?: FormatOptions options?: FormatOptions
className?: string className?: string
animate?: boolean animate?: boolean
@ -14,23 +15,23 @@ interface Props {
export const FormattedNumber = React.memo((props: Props) => { export const FormattedNumber = React.memo((props: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations) const enableAnimations = useStore((s) => s.enableAnimations)
const prevAmountRef = useRef<number>(0) const prevAmountRef = useRef<BigNumber>(BN(0))
useEffect(() => { useEffect(() => {
if (prevAmountRef.current !== Number(props.amount)) prevAmountRef.current = Number(props.amount) if (!prevAmountRef.current.eq(props.amount)) prevAmountRef.current = props.amount
}, [props.amount]) }, [props.amount])
const springAmount = useSpring({ const springAmount = useSpring({
number: Number(props.amount), number: props.amount.toNumber(),
from: { number: prevAmountRef.current }, from: { number: prevAmountRef.current.toNumber() },
config: { duration: 1000 }, config: { duration: 1000 },
}) })
return (prevAmountRef.current === props.amount && props.amount === 0) || return (prevAmountRef.current.eq(props.amount) && props.amount.isZero()) ||
!props.animate || !props.animate ||
!enableAnimations ? ( !enableAnimations ? (
<span className={classNames('number', props.className)}> <span className={classNames('number', props.className)}>
{formatValue(props.amount, { {formatValue(props.amount.toString(), {
minDecimals: props.options?.minDecimals, minDecimals: props.options?.minDecimals,
maxDecimals: props.options?.maxDecimals, maxDecimals: props.options?.maxDecimals,
thousandSeparator: props.options?.thousandSeparator, thousandSeparator: props.options?.thousandSeparator,

View File

@ -4,6 +4,7 @@ import { ReactElement, ReactNode } from 'react'
import { Tooltip } from 'components/Tooltip' import { Tooltip } from 'components/Tooltip'
import useStore from 'store' import useStore from 'store'
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import { BN } from 'utils/helpers'
interface Props { interface Props {
tooltip: string | ReactNode tooltip: string | ReactNode
@ -87,7 +88,7 @@ export const Gauge = ({
)} )}
<FormattedNumber <FormattedNumber
className={classNames(labelClassName, 'text-2xs')} className={classNames(labelClassName, 'text-2xs')}
amount={Math.round(percentage)} amount={BN(Math.round(percentage))}
options={{ maxDecimals: 0, minDecimals: 0 }} options={{ maxDecimals: 0, minDecimals: 0 }}
animate animate
/> />

View File

@ -2,6 +2,7 @@ import classNames from 'classnames'
import { FormattedNumber } from 'components/FormattedNumber' import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text' import Text from 'components/Text'
import { BN } from 'utils/helpers'
interface ValueData extends FormattedNumberProps { interface ValueData extends FormattedNumberProps {
format?: 'number' | 'string' format?: 'number' | 'string'
@ -19,7 +20,11 @@ export const LabelValuePair = ({ label, value, className }: Props) => (
{label} {label}
</Text> </Text>
<Text size='xs' className='text-white/60'> <Text size='xs' className='text-white/60'>
{value.format === 'number' ? <FormattedNumber animate {...value} /> : value.amount || ''} {value.format === 'number' ? (
<FormattedNumber animate {...value} amount={BN(value.amount)} />
) : (
value.amount || ''
)}
</Text> </Text>
</div> </div>
) )

View File

@ -6,27 +6,28 @@ import {
SortingState, SortingState,
useReactTable, useReactTable,
} from '@tanstack/react-table' } from '@tanstack/react-table'
import { useEffect, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import { SortAsc, SortDesc, SortNone } from 'components/Icons' import { SortAsc, SortDesc, SortNone } from 'components/Icons'
import Text from 'components/Text' import Text from 'components/Text'
import useStore from 'store'
import useAddVaultAssetTableColumns from 'components/Modals/AddVaultAssets/useAddVaultAssetTableColumns' import useAddVaultAssetTableColumns from 'components/Modals/AddVaultAssets/useAddVaultAssetTableColumns'
interface Props { interface Props {
assets: BorrowAsset[] assets: BorrowAsset[]
selectedDenoms: string[]
onChangeSelected: (denoms: string[]) => void onChangeSelected: (denoms: string[]) => void
} }
export default function AddVaultAssetTable(props: Props) { export default function AddVaultAssetTable(props: Props) {
const selectedDenoms = useStore((s) => s.addVaultBorrowingsModal?.selectedDenoms) || [] const defaultSelected = useMemo(() => {
const defaultSelected = props.assets.reduce((acc, asset, index) => { return props.assets.reduce((acc, asset, index) => {
if (selectedDenoms.includes(asset.denom)) { if (props.selectedDenoms?.includes(asset.denom)) {
acc[index] = true acc[index] = true
} }
return acc return acc
}, {} as { [key: number]: boolean }) }, {} as { [key: number]: boolean })
}, [props.selectedDenoms, props.assets])
const [sorting, setSorting] = useState<SortingState>([{ id: 'symbol', desc: false }]) const [sorting, setSorting] = useState<SortingState>([{ id: 'symbol', desc: false }])
const [selected, setSelected] = useState<RowSelectionState>(defaultSelected) const [selected, setSelected] = useState<RowSelectionState>(defaultSelected)
@ -46,11 +47,16 @@ export default function AddVaultAssetTable(props: Props) {
}) })
useEffect(() => { useEffect(() => {
const selectedDenoms = props.assets const newSelectedDenoms = props.assets
.filter((_, index) => selected[index]) .filter((_, index) => selected[index])
.map((asset) => asset.denom) .map((asset) => asset.denom)
props.onChangeSelected(selectedDenoms) if (
props.selectedDenoms.length === newSelectedDenoms.length &&
newSelectedDenoms.every((denom) => props.selectedDenoms.includes(denom))
)
return
props.onChangeSelected(newSelectedDenoms)
}, [selected, props]) }, [selected, props])
return ( return (

View File

@ -4,6 +4,7 @@ import SearchBar from 'components/SearchBar'
import Text from 'components/Text' import Text from 'components/Text'
import useMarketBorrowings from 'hooks/useMarketBorrowings' import useMarketBorrowings from 'hooks/useMarketBorrowings'
import AddVaultAssetTable from 'components/Modals/AddVaultAssets/AddVaultAssetTable' import AddVaultAssetTable from 'components/Modals/AddVaultAssets/AddVaultAssetTable'
import useStore from 'store'
interface Props { interface Props {
vault: Vault vault: Vault
@ -14,8 +15,6 @@ interface Props {
export default function AddVaultAssetsModalContent(props: Props) { export default function AddVaultAssetsModalContent(props: Props) {
const [searchString, setSearchString] = useState<string>('') const [searchString, setSearchString] = useState<string>('')
const { data: borrowAssets } = useMarketBorrowings() const { data: borrowAssets } = useMarketBorrowings()
const [selectedPoolDenoms, setSelectedPoolDenoms] = useState<string[]>([])
const [selectedOtherDenoms, setSelectedOtherDenoms] = useState<string[]>([])
const filteredBorrowAssets: BorrowAsset[] = useMemo(() => { const filteredBorrowAssets: BorrowAsset[] = useMemo(() => {
return borrowAssets.filter( return borrowAssets.filter(
@ -49,6 +48,15 @@ export default function AddVaultAssetsModalContent(props: Props) {
[filteredBorrowAssets, props.vault.denoms.primary, props.vault.denoms.secondary], [filteredBorrowAssets, props.vault.denoms.primary, props.vault.denoms.secondary],
) )
const selectedDenoms = useStore((s) => s.addVaultBorrowingsModal?.selectedDenoms)
const [selectedPoolDenoms, setSelectedPoolDenoms] = useState<string[]>(
selectedDenoms?.filter((denom) => poolAssets.map((asset) => asset.denom).includes(denom)) || [],
)
const [selectedOtherDenoms, setSelectedOtherDenoms] = useState<string[]>(
selectedDenoms?.filter((denom) => stableAssets.map((asset) => asset.denom).includes(denom)) ||
[],
)
const onChangePoolDenoms = useCallback( const onChangePoolDenoms = useCallback(
(denoms: string[]) => { (denoms: string[]) => {
setSelectedPoolDenoms(denoms) setSelectedPoolDenoms(denoms)
@ -81,7 +89,11 @@ export default function AddVaultAssetsModalContent(props: Props) {
Leverage will be set at 50% for both assets by default Leverage will be set at 50% for both assets by default
</Text> </Text>
</div> </div>
<AddVaultAssetTable assets={poolAssets} onChangeSelected={onChangePoolDenoms} /> <AddVaultAssetTable
assets={poolAssets}
onChangeSelected={onChangePoolDenoms}
selectedDenoms={selectedPoolDenoms}
/>
<div className='p-4'> <div className='p-4'>
<Text>Assets not in the liquidity pool</Text> <Text>Assets not in the liquidity pool</Text>
<Text size='xs' className='mt-1 text-white/60'> <Text size='xs' className='mt-1 text-white/60'>
@ -89,7 +101,11 @@ export default function AddVaultAssetsModalContent(props: Props) {
these assets below. these assets below.
</Text> </Text>
</div> </div>
<AddVaultAssetTable assets={stableAssets} onChangeSelected={onChangeOtherDenoms} /> <AddVaultAssetTable
assets={stableAssets}
onChangeSelected={onChangeOtherDenoms}
selectedDenoms={selectedOtherDenoms}
/>
</div> </div>
</> </>
) )

View File

@ -83,12 +83,12 @@ export default function BorrowModal() {
useStore.setState({ borrowModal: null }) useStore.setState({ borrowModal: null })
} }
const liquidityAmountString = formatValue(modal?.marketData?.liquidity?.amount || 0, { const liquidityAmountString = formatValue(modal?.marketData?.liquidity?.amount.toString() || 0, {
abbreviated: true, abbreviated: true,
decimals: 6, decimals: 6,
}) })
const liquidityValueString = formatValue(modal?.marketData?.liquidity?.value || 0, { const liquidityValueString = formatValue(modal?.marketData?.liquidity?.value.toString() || 0, {
abbreviated: true, abbreviated: true,
decimals: 6, decimals: 6,
}) })

View File

@ -23,7 +23,7 @@ function DetailsHeader({ data }: Props) {
<FormattedNumber amount={assetApy} options={{ suffix: '%' }} /> <FormattedNumber amount={assetApy} options={{ suffix: '%' }} />
<FormattedNumber <FormattedNumber
className='ml-2 text-xs' className='ml-2 text-xs'
amount={assetApy / 365} amount={assetApy.div(365)}
options={{ suffix: '%/day' }} options={{ suffix: '%/day' }}
/> />
</> </>

View File

@ -7,7 +7,7 @@ import useCurrentAccount from 'hooks/useCurrentAccount'
import useLendAndReclaimModal from 'hooks/useLendAndReclaimModal' import useLendAndReclaimModal from 'hooks/useLendAndReclaimModal'
import DetailsHeader from 'components/Modals/LendAndReclaim/DetailsHeader' import DetailsHeader from 'components/Modals/LendAndReclaim/DetailsHeader'
import AssetAmountSelectActionModal from 'components/Modals/AssetAmountSelectActionModal' import AssetAmountSelectActionModal from 'components/Modals/AssetAmountSelectActionModal'
import AccountBalanceSettableCoin from 'types/classes/AccountBalanceSettableCoin' import { BNCoin } from 'types/classes/BNCoin'
function LendAndReclaimModalController() { function LendAndReclaimModalController() {
const currentAccount = useCurrentAccount() const currentAccount = useCurrentAccount()
@ -48,15 +48,12 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
async (value: BigNumber, isMax: boolean) => { async (value: BigNumber, isMax: boolean) => {
setIsConfirming(true) setIsConfirming(true)
const coin = new AccountBalanceSettableCoin( const coin = BNCoin.fromDenomAndBigNumber(asset.denom, value)
asset.denom,
value.integerValue().toString(),
isMax,
)
const options = { const options = {
fee: hardcodedFee, fee: hardcodedFee,
accountId: currentAccount.id, accountId: currentAccount.id,
coin, coin,
isMax,
} }
await (isLendAction ? lend : reclaim)(options) await (isLendAction ? lend : reclaim)(options)

View File

@ -18,6 +18,7 @@ import useStore from 'store'
import DisplayCurrency from 'components/DisplayCurrency' import DisplayCurrency from 'components/DisplayCurrency'
import usePrice from 'hooks/usePrice' import usePrice from 'hooks/usePrice'
import { BNCoin } from 'types/classes/BNCoin' import { BNCoin } from 'types/classes/BNCoin'
import useDepositVault from 'hooks/broadcast/useDepositVault'
export interface VaultBorrowingsProps { export interface VaultBorrowingsProps {
account: Account account: Account
@ -26,6 +27,8 @@ export interface VaultBorrowingsProps {
secondaryAmount: BigNumber secondaryAmount: BigNumber
primaryAsset: Asset primaryAsset: Asset
secondaryAsset: Asset secondaryAsset: Asset
deposits: BNCoin[]
vault: Vault
onChangeBorrowings: (borrowings: BNCoin[]) => void onChangeBorrowings: (borrowings: BNCoin[]) => void
} }
@ -36,6 +39,13 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
const secondaryPrice = usePrice(props.secondaryAsset.denom) const secondaryPrice = usePrice(props.secondaryAsset.denom)
const baseCurrency = useStore((s) => s.baseCurrency) const baseCurrency = useStore((s) => s.baseCurrency)
const vaultModal = useStore((s) => s.vaultModal) const vaultModal = useStore((s) => s.vaultModal)
const depositIntoVault = useStore((s) => s.depositIntoVault)
const { actions: depositActions, fee: depositFee } = useDepositVault({
vault: props.vault,
deposits: props.deposits.filter((borrowing) => borrowing.amount.gt(0)),
borrowings: props.borrowings.filter((borrowing) => borrowing.amount.gt(0)),
})
const primaryValue = useMemo( const primaryValue = useMemo(
() => props.primaryAmount.times(primaryPrice), () => props.primaryAmount.times(primaryPrice),
@ -139,6 +149,10 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
}) })
} }
function onConfirm() {
depositIntoVault({ fee: depositFee, accountId: props.account.id, actions: depositActions })
}
return ( return (
<div className='flex flex-grow flex-col gap-4 p-4'> <div className='flex flex-grow flex-col gap-4 p-4'>
{props.borrowings.map((coin) => { {props.borrowings.map((coin) => {
@ -191,7 +205,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
) )
})} })}
</div> </div>
<Button color='primary' text='Deposit' rightIcon={<ArrowRight />} /> <Button onClick={onConfirm} color='primary' text='Deposit' rightIcon={<ArrowRight />} />
</div> </div>
) )
} }

View File

@ -77,7 +77,7 @@ export default function VaultDeposit(props: Props) {
() => () =>
props.isCustomRatio props.isCustomRatio
? availableSecondaryAmount ? availableSecondaryAmount
: maxAssetValueNonCustom.dividedBy(secondaryPrice), : maxAssetValueNonCustom.dividedBy(secondaryPrice).decimalPlaces(0),
[props.isCustomRatio, availableSecondaryAmount, secondaryPrice, maxAssetValueNonCustom], [props.isCustomRatio, availableSecondaryAmount, secondaryPrice, maxAssetValueNonCustom],
) )

View File

@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { useCallback, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import Accordion from 'components/Accordion' import Accordion from 'components/Accordion'
import AccountSummary from 'components/Account/AccountSummary' import AccountSummary from 'components/Account/AccountSummary'
@ -10,6 +10,7 @@ import VaultBorrowingsSubTitle from 'components/Modals/Vault/VaultBorrowingsSubT
import VaultDeposit from 'components/Modals/Vault/VaultDeposits' import VaultDeposit from 'components/Modals/Vault/VaultDeposits'
import VaultBorrowings from 'components/Modals/Vault/VaultBorrowings' import VaultBorrowings from 'components/Modals/Vault/VaultBorrowings'
import VaultDepositSubTitle from 'components/Modals/Vault/VaultDepositsSubTitle' import VaultDepositSubTitle from 'components/Modals/Vault/VaultDepositsSubTitle'
import { BNCoin } from 'types/classes/BNCoin'
interface Props { interface Props {
vault: Vault vault: Vault
@ -28,12 +29,24 @@ export default function VaultModalContent(props: Props) {
const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0)) const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0))
const [isCustomRatio, setIsCustomRatio] = useState(false) const [isCustomRatio, setIsCustomRatio] = useState(false)
const deposits: BNCoin[] = useMemo(() => {
const primaryBNCoin = new BNCoin({
denom: props.vault.denoms.primary,
amount: primaryAmount.toString(),
})
const secondaryBNCoin = new BNCoin({
denom: props.vault.denoms.secondary,
amount: secondaryAmount.toString(),
})
return [primaryBNCoin, secondaryBNCoin]
}, [primaryAmount, secondaryAmount, props.vault.denoms.primary, props.vault.denoms.secondary])
const onChangePrimaryAmount = useCallback( const onChangePrimaryAmount = useCallback(
(amount: BigNumber) => setPrimaryAmount(amount), (amount: BigNumber) => setPrimaryAmount(amount.decimalPlaces(0)),
[setPrimaryAmount], [setPrimaryAmount],
) )
const onChangeSecondaryAmount = useCallback( const onChangeSecondaryAmount = useCallback(
(amount: BigNumber) => setSecondaryAmount(amount), (amount: BigNumber) => setSecondaryAmount(amount.decimalPlaces(0)),
[setSecondaryAmount], [setSecondaryAmount],
) )
@ -84,6 +97,8 @@ export default function VaultModalContent(props: Props) {
primaryAsset={props.primaryAsset} primaryAsset={props.primaryAsset}
secondaryAsset={props.secondaryAsset} secondaryAsset={props.secondaryAsset}
onChangeBorrowings={onChangeBorrowings} onChangeBorrowings={onChangeBorrowings}
deposits={deposits}
vault={props.vault}
/> />
), ),
title: 'Borrow', title: 'Borrow',

View File

@ -14,6 +14,7 @@ import Button from 'components/Button'
import { ExclamationMarkTriangle, TrashBin } from 'components/Icons' import { ExclamationMarkTriangle, TrashBin } from 'components/Icons'
import { Tooltip } from 'components/Tooltip' import { Tooltip } from 'components/Tooltip'
import AssetImage from 'components/AssetImage' import AssetImage from 'components/AssetImage'
import { BNCoin } from 'types/classes/BNCoin'
interface Props { interface Props {
amount: BigNumber amount: BigNumber
@ -108,7 +109,7 @@ export default function TokenInput(props: Props) {
</Text> </Text>
<FormattedNumber <FormattedNumber
className='mr-1 text-xs text-white/50' className='mr-1 text-xs text-white/50'
amount={props.max.toNumber()} amount={props.max}
options={{ decimals: props.asset.decimals }} options={{ decimals: props.asset.decimals }}
/> />
<Button <Button
@ -127,8 +128,8 @@ export default function TokenInput(props: Props) {
<div className='flex'> <div className='flex'>
<DisplayCurrency <DisplayCurrency
isApproximation isApproximation
className='inline pl-0.5 text-xs text-white/50' className='inline pl-1 text-xs text-white/50'
coin={{ denom: props.asset.denom, amount: props.amount.toString() }} coin={new BNCoin({ denom: props.asset.denom, amount: props.amount.toString() })}
/> />
</div> </div>
</div> </div>

View File

@ -21,6 +21,7 @@ import useStore from 'store'
import { getBaseAsset, getEnabledMarketAssets } from 'utils/assets' import { getBaseAsset, getEnabledMarketAssets } from 'utils/assets'
import { formatValue, truncate } from 'utils/formatters' import { formatValue, truncate } from 'utils/formatters'
import useWalletBalances from 'hooks/useWalletBalances' import useWalletBalances from 'hooks/useWalletBalances'
import { BN } from 'utils/helpers'
export default function ConnectedButton() { export default function ConnectedButton() {
// --------------- // ---------------
@ -38,7 +39,7 @@ export default function ConnectedButton() {
// LOCAL STATE // LOCAL STATE
// --------------- // ---------------
const [showDetails, setShowDetails] = useToggle() const [showDetails, setShowDetails] = useToggle()
const [walletAmount, setWalletAmount] = useState(0) const [walletAmount, setWalletAmount] = useState(BN(0))
const [isCopied, setCopied] = useClipboard(address || '', { const [isCopied, setCopied] = useClipboard(address || '', {
successDuration: 1000 * 5, successDuration: 1000 * 5,
}) })
@ -62,9 +63,9 @@ export default function ConnectedButton() {
useEffect(() => { useEffect(() => {
if (!walletBalances || walletBalances.length === 0) return if (!walletBalances || walletBalances.length === 0) return
setWalletAmount( setWalletAmount(
BigNumber(walletBalances?.find((coin: Coin) => coin.denom === baseAsset.denom)?.amount ?? 0) BigNumber(
.div(10 ** baseAsset.decimals) walletBalances?.find((coin: Coin) => coin.denom === baseAsset.denom)?.amount ?? 0,
.toNumber(), ).div(10 ** baseAsset.decimals),
) )
const assetDenoms = marketAssets.map((asset) => asset.denom) const assetDenoms = marketAssets.map((asset) => asset.denom)
@ -103,7 +104,7 @@ export default function ConnectedButton() {
{isLoading ? ( {isLoading ? (
<CircularProgress size={12} /> <CircularProgress size={12} />
) : ( ) : (
`${formatValue(walletAmount, { suffix: ` ${baseAsset.symbol}` })}` `${formatValue(walletAmount.toString(), { suffix: ` ${baseAsset.symbol}` })}`
)} )}
</div> </div>
</Button> </Button>

View File

@ -0,0 +1,81 @@
import debounce from 'debounce-promise'
import { useMemo, useState } from 'react'
import { hardcodedFee } from 'utils/constants'
import getMinLpToReceive from 'api/vaults/getMinLpToReceive'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import {
getEnterVaultActions,
getVaultDepositCoinsAndValue,
getVaultSwapActions,
} from 'utils/vaults'
import { BN } from 'utils/helpers'
interface Props {
vault: Vault
deposits: BNCoin[]
borrowings: BNCoin[]
}
export default function useDepositVault(props: Props): { actions: Action[]; fee: StdFee } {
const [minLpToReceive, setMinLpToReceive] = useState<BigNumber>(BN(0))
const { data: prices } = usePrices()
const slippage = useStore((s) => s.slippage)
const debouncedGetMinLpToReceive = useMemo(() => debounce(getMinLpToReceive, 500), [])
const { primaryCoin, secondaryCoin, totalValue } = useMemo(
() => getVaultDepositCoinsAndValue(props.vault, props.deposits, props.borrowings, prices),
[props.deposits, props.borrowings, props.vault, prices],
)
const borrowActions: Action[] = useMemo(() => {
return props.borrowings.map((bnCoin) => ({
borrow: bnCoin.toCoin(),
}))
}, [props.borrowings])
const swapActions: Action[] = useMemo(
() =>
getVaultSwapActions(
props.vault,
props.deposits,
props.borrowings,
prices,
slippage,
totalValue,
),
[totalValue, prices, props.vault, props.deposits, props.borrowings, slippage],
)
useMemo(async () => {
if (primaryCoin.amount.isZero() || secondaryCoin.amount.isZero()) return
const lpAmount = await debouncedGetMinLpToReceive(
[secondaryCoin.toCoin(), primaryCoin.toCoin()],
props.vault.denoms.lp,
)
if (!lpAmount || lpAmount === minLpToReceive) return
setMinLpToReceive(lpAmount)
}, [
primaryCoin,
secondaryCoin,
props.vault.denoms.lp,
debouncedGetMinLpToReceive,
minLpToReceive,
])
const enterVaultActions: Action[] = useMemo(() => {
return getEnterVaultActions(props.vault, primaryCoin, secondaryCoin, minLpToReceive)
}, [props.vault, primaryCoin, secondaryCoin, minLpToReceive])
const actions = useMemo(
() => [...borrowActions, ...swapActions, ...enterVaultActions],
[borrowActions, swapActions, enterVaultActions],
)
return { actions, fee: hardcodedFee }
}

View File

@ -0,0 +1,14 @@
import useSWR from 'swr'
import getMinLpToReceive from 'api/vaults/getMinLpToReceive'
import { BN } from 'utils/helpers'
export default function useMinLpToReceive(coins: Coin[], lpDenom: string) {
return useSWR(
`minLpToReceive-${JSON.stringify(coins)}`,
() => getMinLpToReceive(coins, lpDenom),
{
fallbackData: BN(0),
},
)
}

View File

@ -6,7 +6,8 @@ import { ENV } from 'constants/env'
import { Store } from 'store' import { Store } from 'store'
import { getSingleValueFromBroadcastResult } from 'utils/broadcast' import { getSingleValueFromBroadcastResult } from 'utils/broadcast'
import { formatAmountWithSymbol } from 'utils/formatters' import { formatAmountWithSymbol } from 'utils/formatters'
import AccountBalanceSettableCoin from 'types/classes/AccountBalanceSettableCoin' import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { BNCoin } from 'types/classes/BNCoin'
export default function createBroadcastSlice( export default function createBroadcastSlice(
set: SetState<Store>, set: SetState<Store>,
@ -127,6 +128,30 @@ export default function createBroadcastSlice(
handleResponseMessages(response, `Requested unlock for ${options.vault.name}`) handleResponseMessages(response, `Requested unlock for ${options.vault.name}`)
return !!response.result return !!response.result
}, },
depositIntoVault: async (options: { fee: StdFee; accountId: string; actions: Action[] }) => {
const msg = {
update_credit_account: {
account_id: options.accountId,
actions: options.actions,
},
}
const response = await get().executeMsg({ msg, fee: options.fee })
if (response.result) {
set({
toast: {
message: `Deposited into vault`,
},
})
} else {
set({
toast: {
message: response.error ?? `Transaction failed: ${response.error}`,
isError: true,
},
})
}
return !!response.result
},
withdraw: async (options: { fee: StdFee; accountId: string; coin: Coin }) => { withdraw: async (options: { fee: StdFee; accountId: string; coin: Coin }) => {
const msg = { const msg = {
update_credit_account: { update_credit_account: {
@ -213,13 +238,13 @@ export default function createBroadcastSlice(
) )
return !!response.result return !!response.result
}, },
lend: async (options: { fee: StdFee; accountId: string; coin: AccountBalanceSettableCoin }) => { lend: async (options: { fee: StdFee; accountId: string; coin: BNCoin; isMax?: boolean }) => {
const msg = { const msg = {
update_credit_account: { update_credit_account: {
account_id: options.accountId, account_id: options.accountId,
actions: [ actions: [
{ {
lend: options.coin, lend: options.coin.toActionCoin(options.isMax),
}, },
], ],
}, },
@ -229,21 +254,17 @@ export default function createBroadcastSlice(
handleResponseMessages( handleResponseMessages(
response, response,
`Successfully deposited ${formatAmountWithSymbol(options.coin)}`, `Successfully deposited ${formatAmountWithSymbol(options.coin.toCoin())}`,
) )
return !!response.result return !!response.result
}, },
reclaim: async (options: { reclaim: async (options: { fee: StdFee; accountId: string; coin: BNCoin; isMax?: boolean }) => {
fee: StdFee
accountId: string
coin: AccountBalanceSettableCoin
}) => {
const msg = { const msg = {
update_credit_account: { update_credit_account: {
account_id: options.accountId, account_id: options.accountId,
actions: [ actions: [
{ {
reclaim: options.coin.toActionCoin(), reclaim: options.coin.toActionCoin(options.isMax),
}, },
], ],
}, },
@ -253,7 +274,7 @@ export default function createBroadcastSlice(
handleResponseMessages( handleResponseMessages(
response, response,
`Successfully withdrew ${formatAmountWithSymbol(options.coin)}`, `Successfully withdrew ${formatAmountWithSymbol(options.coin.toCoin())}`,
) )
return !!response.result return !!response.result
}, },

View File

@ -9,6 +9,7 @@ export default function createCommonSlice(set: SetState<CommonSlice>, get: GetSt
enableAnimations: true, enableAnimations: true,
isOpen: true, isOpen: true,
selectedAccount: null, selectedAccount: null,
slippage: 0.02,
status: WalletConnectionStatus.Unconnected, status: WalletConnectionStatus.Unconnected,
} }
} }

View File

@ -1,22 +0,0 @@
import { ActionCoin } from 'types/generated'
class AccountBalanceSettableCoin implements Coin {
public denom: string
public amount: string
public setAccountBalance: boolean
constructor(denom: string, amount: string, setAccountBalance: boolean) {
this.denom = denom
this.amount = amount
this.setAccountBalance = setAccountBalance
}
toActionCoin(): ActionCoin {
return {
denom: this.denom,
amount: this.setAccountBalance ? 'account_balance' : { exact: this.amount },
}
}
}
export default AccountBalanceSettableCoin

View File

@ -1,3 +1,4 @@
import { ActionCoin } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
export class BNCoin { export class BNCoin {
@ -9,10 +10,25 @@ export class BNCoin {
this.amount = BN(coin.amount) this.amount = BN(coin.amount)
} }
static fromDenomAndBigNumber(denom: string, amount: BigNumber) {
return new BNCoin({ denom, amount: amount.toString() })
}
toCoin(): Coin { toCoin(): Coin {
return { return {
denom: this.denom, denom: this.denom,
amount: this.amount.toString(), amount: this.amount.toString(),
} }
} }
toActionCoin(max?: boolean): ActionCoin {
return {
denom: this.denom,
amount: max
? 'account_balance'
: {
exact: this.amount.toString(),
},
}
}
} }

View File

@ -5,19 +5,20 @@
* and run the @cosmwasm/ts-codegen generate command to regenerate this file. * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
*/ */
import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { CosmWasmClient, ExecuteResult, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
import { Coin, StdFee } from '@cosmjs/amino' import { Coin, StdFee } from '@cosmjs/amino'
import { import {
InstantiateMsg,
ExecuteMsg,
Uint128,
Addr, Addr,
ArrayOfAssetIncentiveResponse,
AssetIncentiveResponse,
ConfigResponse,
Decimal,
ExecuteMsg,
InstantiateMsg,
OwnerUpdate, OwnerUpdate,
QueryMsg, QueryMsg,
Decimal, Uint128,
AssetIncentiveResponse,
ArrayOfAssetIncentiveResponse,
ConfigResponse,
} from './MarsIncentives.types' } from './MarsIncentives.types'
export interface MarsIncentivesReadOnlyInterface { export interface MarsIncentivesReadOnlyInterface {
contractAddress: string contractAddress: string

View File

@ -5,22 +5,23 @@
* and run the @cosmwasm/ts-codegen generate command to regenerate this file. * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
*/ */
import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tanstack/react-query' import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from '@tanstack/react-query'
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { StdFee, Coin } from '@cosmjs/amino' import { Coin, StdFee } from '@cosmjs/amino'
import { import {
InstantiateMsg,
ExecuteMsg,
Uint128,
Addr, Addr,
ArrayOfAssetIncentiveResponse,
AssetIncentiveResponse,
ConfigResponse,
Decimal,
ExecuteMsg,
InstantiateMsg,
OwnerUpdate, OwnerUpdate,
QueryMsg, QueryMsg,
Decimal, Uint128,
AssetIncentiveResponse,
ArrayOfAssetIncentiveResponse,
ConfigResponse,
} from './MarsIncentives.types' } from './MarsIncentives.types'
import { MarsIncentivesQueryClient, MarsIncentivesClient } from './MarsIncentives.client' import { MarsIncentivesClient, MarsIncentivesQueryClient } from './MarsIncentives.client'
export const marsIncentivesQueryKeys = { export const marsIncentivesQueryKeys = {
contract: [ contract: [
{ {

View File

@ -1,15 +1,15 @@
interface Account extends AccountChange { interface Account extends AccountChange {
id: string id: string
deposits: Coin[] deposits: BNCoin[]
debts: Coin[] debts: BNCoin[]
lends: Coin[] lends: BNCoin[]
vaults: import('types/generated/mars-mock-credit-manager/MarsMockCreditManager.types').ArrayOfVaultInfoResponse vaults: import('types/generated/mars-mock-credit-manager/MarsMockCreditManager.types').ArrayOfVaultInfoResponse
} }
interface AccountChange { interface AccountChange {
deposits?: Coin[] deposits?: BNCoin[]
debts?: Coin[] debts?: BNCoin[]
lends?: Coin[] lends?: BNCoin[]
vaults?: import('types/generated/mars-mock-credit-manager/MarsMockCreditManager.types').ArrayOfVaultInfoResponse vaults?: import('types/generated/mars-mock-credit-manager/MarsMockCreditManager.types').ArrayOfVaultInfoResponse
} }

View File

@ -23,13 +23,13 @@ interface OtherAsset extends Omit<Asset, 'symbol'> {
interface BorrowAsset extends Asset { interface BorrowAsset extends Asset {
borrowRate: number | null borrowRate: number | null
liquidity: { liquidity: {
amount: string amount: BigNumber
value: string value: BigNumber
} | null } | null
} }
interface BorrowAssetActive extends BorrowAsset { interface BorrowAssetActive extends BorrowAsset {
debt: string debt: BigNumber
} }
interface BigNumberCoin { interface BigNumberCoin {

View File

@ -1,5 +1,3 @@
type AccountBalanceSettableCoin = import('types/classes/AccountBalanceSettableCoin')
interface BroadcastResult { interface BroadcastResult {
result?: import('@marsprotocol/wallet-connector').TxBroadcastResult result?: import('@marsprotocol/wallet-connector').TxBroadcastResult
error?: string error?: string
@ -17,16 +15,23 @@ interface BroadcastSlice {
deleteAccount: (options: { fee: StdFee; accountId: string }) => Promise<boolean> deleteAccount: (options: { fee: StdFee; accountId: string }) => Promise<boolean>
deposit: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise<boolean> deposit: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise<boolean>
unlock: (options: { fee: StdFee; vault: Vault; amount: string }) => Promise<boolean> unlock: (options: { fee: StdFee; vault: Vault; amount: string }) => Promise<boolean>
depositIntoVault: (options: {
fee: StdFee
accountId: string
actions: Action[]
}) => Promise<boolean>
withdraw: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise<boolean> withdraw: (options: { fee: StdFee; accountId: string; coin: Coin }) => Promise<boolean>
lend: (options: { lend: (options: {
fee: StdFee fee: StdFee
accountId: string accountId: string
coin: AccountBalanceSettableCoin coin: BNCoin
isMax?: boolean
}) => Promise<boolean> }) => Promise<boolean>
reclaim: (options: { reclaim: (options: {
fee: StdFee fee: StdFee
accountId: string accountId: string
coin: AccountBalanceSettableCoin coin: BNCoin
isMax?: boolean
}) => Promise<boolean> }) => Promise<boolean>
repay: (options: { repay: (options: {
fee: StdFee fee: StdFee

View File

@ -6,5 +6,6 @@ interface CommonSlice {
enableAnimations: boolean enableAnimations: boolean
isOpen: boolean isOpen: boolean
selectedAccount: string | null selectedAccount: string | null
slippage: number
status: import('@marsprotocol/wallet-connector').WalletConnectionStatus status: import('@marsprotocol/wallet-connector').WalletConnectionStatus
} }

View File

@ -31,7 +31,7 @@ export const calculateAccountDebt = (
if (!account.debts) return BN(0) if (!account.debts) return BN(0)
return account.debts.reduce((acc, debt) => { return account.debts.reduce((acc, debt) => {
const price = prices.find((price) => price.denom === debt.denom)?.amount ?? 0 const price = prices.find((price) => price.denom === debt.denom)?.amount ?? 0
const debtValue = BN(debt.amount).multipliedBy(price) const debtValue = debt.amount.multipliedBy(price)
return acc.plus(debtValue) return acc.plus(debtValue)
}, BN(0)) }, BN(0))
} }

View File

@ -1,5 +1,6 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { BNCoin } from 'types/classes/BNCoin'
import { getEnabledMarketAssets } from 'utils/assets' import { getEnabledMarketAssets } from 'utils/assets'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
@ -155,18 +156,21 @@ export function demagnify(amount: number | string | BigNumber, asset: Asset) {
return value.isZero() ? 0 : value.shiftedBy(-1 * asset.decimals).toNumber() return value.isZero() ? 0 : value.shiftedBy(-1 * asset.decimals).toNumber()
} }
export function convertToDisplayAmount(coin: Coin, displayCurrency: Asset, prices: Coin[]) { export function convertToDisplayAmount(
coin: BNCoin | Coin,
displayCurrency: Asset,
prices: Coin[],
) {
const price = prices.find((price) => price.denom === coin.denom) const price = prices.find((price) => price.denom === coin.denom)
const asset = getEnabledMarketAssets().find((asset) => asset.denom === coin.denom) const asset = getEnabledMarketAssets().find((asset) => asset.denom === coin.denom)
const displayPrice = prices.find((price) => price.denom === displayCurrency.denom) const displayPrice = prices.find((price) => price.denom === displayCurrency.denom)
if (!price || !asset || !displayPrice) return '0' if (!price || !asset || !displayPrice) return BN(0)
return BN(coin.amount) return BN(coin.amount)
.shiftedBy(-1 * asset.decimals) .shiftedBy(-1 * asset.decimals)
.times(price.amount) .times(price.amount)
.div(displayPrice.amount) .div(displayPrice.amount)
.toNumber()
} }
export function convertLiquidityRateToAPR(rate: number) { export function convertLiquidityRateToAPR(rate: number) {

View File

@ -1,5 +1,6 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { BNCoin } from 'types/classes/BNCoin'
import { getBaseAsset } from 'utils/assets' import { getBaseAsset } from 'utils/assets'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
@ -15,7 +16,12 @@ export const getTokenIcon = (denom: string, marketAssets: Asset[]) =>
export const getTokenInfo = (denom: string, marketAssets: Asset[]) => export const getTokenInfo = (denom: string, marketAssets: Asset[]) =>
marketAssets.find((asset) => asset.denom.toLowerCase() === denom.toLowerCase()) || getBaseAsset() marketAssets.find((asset) => asset.denom.toLowerCase() === denom.toLowerCase()) || getBaseAsset()
export function getTokenValue(coin: Coin, prices: Coin[]): BigNumber { export function getTokenValue(coin: BNCoin, prices: Coin[]): BigNumber {
const price = prices.find((price) => price.denom === coin.denom)?.amount || '0' const price = prices.find((price) => price.denom === coin.denom)?.amount || '0'
return BN(price).times(coin.amount) return BN(price).times(coin.amount).decimalPlaces(0)
}
export function getTokenPrice(denom: string, prices: Coin[]): BigNumber {
const price = prices.find((price) => price.denom === denom)?.amount || '0'
return BN(price)
} }

View File

@ -3,6 +3,8 @@ import { TESTNET_VAULTS_META_DATA, VAULTS_META_DATA } from 'constants/vaults'
import { BN } from 'utils/helpers' import { BN } from 'utils/helpers'
import { getNetCollateralValue } from 'utils/accounts' import { getNetCollateralValue } from 'utils/accounts'
import { BNCoin } from 'types/classes/BNCoin' import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { getTokenPrice, getTokenValue } from 'utils/tokens'
export function getVaultMetaData(address: string) { export function getVaultMetaData(address: string) {
const vaults = IS_TESTNET ? TESTNET_VAULTS_META_DATA : VAULTS_META_DATA const vaults = IS_TESTNET ? TESTNET_VAULTS_META_DATA : VAULTS_META_DATA
@ -33,3 +35,160 @@ export function calculateMaxBorrowAmounts(
return maxAmounts return maxAmounts
} }
export function getVaultDepositCoinsAndValue(
vault: Vault,
deposits: BNCoin[],
borrowings: BNCoin[],
prices: Coin[],
) {
const totalValue = [...deposits, ...borrowings].reduce((prev, bnCoin) => {
const price = prices.find((coin) => coin.denom === bnCoin.denom)?.amount
if (!price) return prev
return prev.plus(bnCoin.amount.times(price))
}, BN(0))
const primaryDepositAmount = getTokenPrice(vault.denoms.primary, prices)
.times(totalValue)
.div(2)
.integerValue()
const secondaryDepositAmount = getTokenPrice(vault.denoms.secondary, prices)
.times(totalValue)
.div(2)
.integerValue()
return {
primaryCoin: new BNCoin({
denom: vault.denoms.primary,
amount: primaryDepositAmount.toString(),
}),
secondaryCoin: new BNCoin({
denom: vault.denoms.secondary,
amount: secondaryDepositAmount.toString(),
}),
totalValue,
}
}
export function getVaultSwapActions(
vault: Vault,
deposits: BNCoin[],
borrowings: BNCoin[],
prices: Coin[],
slippage: number,
totalValue: BigNumber,
): Action[] {
const swapActions: Action[] = []
const coins = [...deposits, ...borrowings]
let primaryLeftoverValue = totalValue.div(2).integerValue()
let secondaryLeftoverValue = totalValue.div(2).integerValue()
const [primaryCoins, secondaryCoins, otherCoins] = coins.reduce(
(prev, bnCoin) => {
switch (bnCoin.denom) {
case vault.denoms.primary:
prev[0].push(bnCoin)
break
case vault.denoms.secondary:
prev[1].push(bnCoin)
break
default:
prev[2].push(bnCoin)
}
return prev
},
[[], [], []] as [BNCoin[], BNCoin[], BNCoin[]],
)
primaryCoins.forEach((bnCoin) => {
let value = getTokenValue(bnCoin, prices)
if (value.isLessThanOrEqualTo(primaryLeftoverValue)) {
primaryLeftoverValue = primaryLeftoverValue.minus(value)
} else {
value = value.minus(primaryLeftoverValue)
primaryLeftoverValue = primaryLeftoverValue.minus(primaryLeftoverValue)
otherCoins.push(new BNCoin({ denom: bnCoin.denom, amount: value.toString() }))
}
})
secondaryCoins.forEach((bnCoin) => {
let value = getTokenValue(bnCoin, prices)
if (value.isLessThanOrEqualTo(secondaryLeftoverValue)) {
secondaryLeftoverValue = secondaryLeftoverValue.minus(value)
} else {
value = value.minus(secondaryLeftoverValue)
secondaryLeftoverValue = secondaryLeftoverValue.minus(secondaryLeftoverValue)
otherCoins.push(new BNCoin({ denom: bnCoin.denom, amount: value.toString() }))
}
})
otherCoins.forEach((bnCoin) => {
let value = getTokenValue(bnCoin, prices)
let amount = bnCoin.amount
if (primaryLeftoverValue.isGreaterThan(0)) {
const swapValue = value.isLessThan(primaryLeftoverValue) ? value : primaryLeftoverValue
const swapAmount = swapValue
.dividedBy(prices.find((coin) => coin.denom === bnCoin.denom)?.amount || 1)
.integerValue()
value = value.minus(swapValue)
amount = amount.minus(swapAmount)
primaryLeftoverValue = primaryLeftoverValue.minus(swapValue)
swapActions.push(getSwapAction(bnCoin.denom, vault.denoms.primary, swapAmount, slippage))
}
if (secondaryLeftoverValue.isGreaterThan(0)) {
secondaryLeftoverValue = secondaryLeftoverValue.minus(value)
swapActions.push(getSwapAction(bnCoin.denom, vault.denoms.secondary, amount, slippage))
}
})
return swapActions
}
export function getEnterVaultActions(
vault: Vault,
primaryCoin: BNCoin,
secondaryCoin: BNCoin,
minLpToReceive: BigNumber,
): Action[] {
return [
{
provide_liquidity: {
// Smart Contact demands that secondary coin is first
coins_in: [secondaryCoin.toActionCoin(), primaryCoin.toActionCoin()],
lp_token_out: vault.denoms.lp,
minimum_receive: minLpToReceive.toString(),
},
},
{
enter_vault: {
coin: {
denom: vault.denoms.lp,
amount: 'account_balance',
},
vault: {
address: vault.address,
},
},
},
]
}
function getSwapAction(denomIn: string, denomOut: string, amount: BigNumber, slippage: number) {
return {
swap_exact_in: {
coin_in: {
denom: denomIn,
amount: {
exact: amount.toString(),
},
},
denom_out: denomOut,
slippage: slippage.toString(),
},
}
}

View File

@ -3408,6 +3408,11 @@
resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz" resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz"
integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==
"@types/debounce-promise@^3.1.6":
version "3.1.6"
resolved "https://registry.yarnpkg.com/@types/debounce-promise/-/debounce-promise-3.1.6.tgz#873e838574011095ed0debf73eed3538e1261d75"
integrity sha512-DowqK95aku+OxMCeG2EQSeXeGeE8OCwLpMsUfIbP7hMF8Otj8eQXnzpwdtIKV+UqQBtkMcF6vbi4Otbh8P/wmg==
"@types/estree@*", "@types/estree@^1.0.0": "@types/estree@*", "@types/estree@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz"
@ -4795,6 +4800,11 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0" whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0" whatwg-url "^11.0.0"
debounce-promise@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.2.tgz#320fb8c7d15a344455cd33cee5ab63530b6dc7c5"
integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4" version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
@ -7029,7 +7039,7 @@ locate-path@^6.0.0:
lodash.debounce@^4.0.8: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.merge@^4.6.2: lodash.merge@^4.6.2: