Edit perps position (#728)

This commit is contained in:
Bob van der Helm 2024-01-12 11:54:29 +01:00 committed by GitHub
parent 060a8b8797
commit 647a287a6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 522 additions and 333 deletions

View File

@ -7,6 +7,7 @@ import { MarsIncentivesQueryClient } from 'types/generated/mars-incentives/MarsI
import { MarsMockVaultQueryClient } from 'types/generated/mars-mock-vault/MarsMockVault.client' import { MarsMockVaultQueryClient } from 'types/generated/mars-mock-vault/MarsMockVault.client'
import { MarsOracleOsmosisQueryClient } from 'types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client' import { MarsOracleOsmosisQueryClient } from 'types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client'
import { MarsParamsQueryClient } from 'types/generated/mars-params/MarsParams.client' import { MarsParamsQueryClient } from 'types/generated/mars-params/MarsParams.client'
import { MarsPerpsQueryClient } from 'types/generated/mars-perps/MarsPerps.client'
import { MarsRedBankQueryClient } from 'types/generated/mars-red-bank/MarsRedBank.client' import { MarsRedBankQueryClient } from 'types/generated/mars-red-bank/MarsRedBank.client'
import { MarsSwapperOsmosisQueryClient } from 'types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.client' import { MarsSwapperOsmosisQueryClient } from 'types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.client'
@ -18,6 +19,7 @@ let _redBankQueryClient: Map<string, MarsRedBankQueryClient> = new Map()
let _paramsQueryClient: Map<string, MarsParamsQueryClient> = new Map() let _paramsQueryClient: Map<string, MarsParamsQueryClient> = new Map()
let _incentivesQueryClient: Map<string, MarsIncentivesQueryClient> = new Map() let _incentivesQueryClient: Map<string, MarsIncentivesQueryClient> = new Map()
let _swapperOsmosisClient: Map<string, MarsSwapperOsmosisQueryClient> = new Map() let _swapperOsmosisClient: Map<string, MarsSwapperOsmosisQueryClient> = new Map()
let _perpsClient: Map<string, MarsPerpsQueryClient> = new Map()
let _ICNSQueryClient: Map<string, ICNSQueryClient> = new Map() let _ICNSQueryClient: Map<string, ICNSQueryClient> = new Map()
const getClient = async (rpc: string) => { const getClient = async (rpc: string) => {
@ -159,6 +161,22 @@ const getSwapperQueryClient = async (chainConfig: ChainConfig) => {
} }
} }
const getPerpsQueryClient = async (chainConfig: ChainConfig) => {
try {
const contract = chainConfig.contracts.perps
const rpc = chainConfig.endpoints.rpc
const key = rpc + contract
if (!_perpsClient.get(key)) {
const client = await getClient(rpc)
_perpsClient.set(key, new MarsPerpsQueryClient(client, contract))
}
return _perpsClient.get(key)!
} catch (error) {
throw error
}
}
const getICNSQueryClient = async (chainConfig: ChainConfig) => { const getICNSQueryClient = async (chainConfig: ChainConfig) => {
try { try {
const contract = chainConfig.contracts.params const contract = chainConfig.contracts.params
@ -186,4 +204,5 @@ export {
getRedBankQueryClient, getRedBankQueryClient,
getSwapperQueryClient, getSwapperQueryClient,
getVaultQueryClient, getVaultQueryClient,
getPerpsQueryClient,
} }

View File

@ -0,0 +1,10 @@
import { getPerpsQueryClient } from 'api/cosmwasm-client'
import { BNCoin } from 'types/classes/BNCoin'
export default async function getOpeningFee(chainConfig: ChainConfig, denom: string, size: string) {
const perpsClient = await getPerpsQueryClient(chainConfig)
return perpsClient
.openingFee({ denom, size: size as any })
.then((resp) => BNCoin.fromCoin(resp.fee))
}

View File

@ -0,0 +1,18 @@
import { FormattedNumber } from 'components/FormattedNumber'
type Props = {
asset: Asset
amount: number
}
export default function AssetAmount(props: Props) {
return (
<FormattedNumber
amount={props.amount}
options={{
decimals: props.asset.decimals,
maxDecimals: props.asset.decimals,
suffix: props.asset.symbol,
}}
/>
)
}

View File

@ -13,7 +13,7 @@ export default function DropDownButton(props: Props) {
const [isOpen, toggleIsOpen] = useToggle(false) const [isOpen, toggleIsOpen] = useToggle(false)
return ( return (
<Tooltip <Tooltip
content={<DropDown {...props} />} content={<DropDown closeMenu={() => toggleIsOpen(false)} {...props} />}
type='info' type='info'
placement='bottom' placement='bottom'
contentClassName='!bg-white/10 border border-white/20 backdrop-blur-xl !p-0' contentClassName='!bg-white/10 border border-white/20 backdrop-blur-xl !p-0'
@ -34,26 +34,35 @@ export default function DropDownButton(props: Props) {
interface DropDownProps { interface DropDownProps {
items: DropDownItem[] items: DropDownItem[]
closeMenu: () => void
} }
function DropDown(props: DropDownProps) { function DropDown(props: DropDownProps) {
return ( return (
<div> <div>
{props.items.map((item) => ( {props.items.map((item) => (
<DropDownItem key={item.text} {...item} /> <DropDownItem key={item.text} item={item} closeMenu={props.closeMenu} />
))} ))}
</div> </div>
) )
} }
function DropDownItem(props: DropDownItem) { interface DropDownItemProps {
closeMenu: () => void
item: DropDownItem
}
function DropDownItem(props: DropDownItemProps) {
return ( return (
<button <button
onClick={props.onClick} onClick={() => {
props.item.onClick()
props.closeMenu()
}}
className=' px-4 py-3 flex gap-2 items-center hover:bg-white/5 w-full [&:not(:last-child)]:border-b border-white/10' className=' px-4 py-3 flex gap-2 items-center hover:bg-white/5 w-full [&:not(:last-child)]:border-b border-white/10'
> >
<div className='flex justify-center w-5 h-5'>{props.icon}</div> <div className='flex justify-center w-5 h-5'>{props.item.icon}</div>
<Text size='sm'>{props.text}</Text> <Text size='sm'>{props.item.text}</Text>
</button> </button>
) )
} }

View File

@ -1,9 +1,3 @@
<svg viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path d="M3.33337 8.00065H12.6667M12.6667 8.00065L8.00004 3.33398M12.6667 8.00065L8.00004 12.6673" stroke="currentColor" stroke-width="0.666667" stroke-linecap="round" stroke-linejoin="round"/>
d="M1.8335 6.00065H11.1668M11.1668 6.00065L6.50016 1.33398M11.1668 6.00065L6.50016 10.6673"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 299 B

View File

@ -1,10 +1,13 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import DropDownButton from 'components/Button/DropDownButton' import DropDownButton from 'components/Button/DropDownButton'
import { Cross, Edit } from 'components/Icons' import { Cross, Edit } from 'components/Icons'
import { PerpPositionRow } from 'components/Perps/BalancesTable/usePerpsBalancesData' import { PerpPositionRow } from 'components/Perps/BalancesTable/usePerpsBalancesData'
import useCurrentAccount from 'hooks/useCurrentAccount' import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store' import useStore from 'store'
import { SearchParams } from 'types/enums/searchParams'
import { getSearchParamsObject } from 'utils/route'
export const MANAGE_META = { id: 'manage', header: 'Manage' } export const MANAGE_META = { id: 'manage', header: 'Manage' }
@ -14,6 +17,7 @@ interface Props {
export default function Manage(props: Props) { export default function Manage(props: Props) {
const currentAccount = useCurrentAccount() const currentAccount = useCurrentAccount()
const [searchParams, setSearchParams] = useSearchParams()
const closePerpPosition = useStore((s) => s.closePerpPosition) const closePerpPosition = useStore((s) => s.closePerpPosition)
const ITEMS: DropDownItem[] = useMemo( const ITEMS: DropDownItem[] = useMemo(
@ -21,7 +25,14 @@ export default function Manage(props: Props) {
{ {
icon: <Edit />, icon: <Edit />,
text: 'Edit Position Size', text: 'Edit Position Size',
onClick: () => {}, onClick: () => {
const params = getSearchParamsObject(searchParams)
setSearchParams({
...params,
[SearchParams.PERPS_MARKET]: props.perpPosition.asset.denom,
[SearchParams.PERPS_MANAGE]: 'true',
})
},
}, },
{ {
icon: <Cross width={16} />, icon: <Cross width={16} />,
@ -35,7 +46,13 @@ export default function Manage(props: Props) {
}, },
}, },
], ],
[closePerpPosition, currentAccount, props.perpPosition.asset.denom], [
closePerpPosition,
currentAccount,
props.perpPosition.asset.denom,
searchParams,
setSearchParams,
],
) )
return ( return (

View File

@ -14,7 +14,7 @@ export default function TradeDirection(props: Props) {
<Text <Text
size='xs' size='xs'
className={classNames( className={classNames(
'capitalize px-1 py-0.5 rounded-sm inline', 'capitalize px-1 py-0.5 rounded-sm inline-block',
tradeDirection === 'short' && 'text-error bg-error/20', tradeDirection === 'short' && 'text-error bg-error/20',
tradeDirection === 'long' && 'text-success bg-success/20', tradeDirection === 'long' && 'text-success bg-success/20',
)} )}

View File

@ -0,0 +1,19 @@
import BigNumber from 'bignumber.js'
import { CircularProgress } from 'components/CircularProgress'
import DisplayCurrency from 'components/DisplayCurrency'
import useOpeningFee from 'hooks/perps/useOpeningFee'
type Props = {
denom: string
amount: BigNumber
}
export default function OpeningFee(props: Props) {
const { data: openingFee, isLoading } = useOpeningFee(props.denom, props.amount)
if (isLoading) return <CircularProgress className='h-full' size={12} />
if (props.amount.isZero() || !openingFee) return '-'
return <DisplayCurrency coin={openingFee} />
}

View File

@ -0,0 +1,74 @@
import classNames from 'classnames'
import { useState } from 'react'
import { Cross } from 'components/Icons'
import { LeverageButtons } from 'components/Perps/Module/LeverageButtons'
import { Or } from 'components/Perps/Module/Or'
import usePerpsManageModule from 'components/Perps/Module/PerpsManageModule/usePerpsManageModule'
import PerpsSummary from 'components/Perps/Module/Summary'
import RangeInput from 'components/RangeInput'
import { Spacer } from 'components/Spacer'
import Text from 'components/Text'
import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput'
import { TradeDirectionSelector } from 'components/TradeDirectionSelector'
import { BN } from 'utils/helpers'
export function PerpsManageModule() {
const [tradeDirection, setTradeDirection] = useState<TradeDirection | null>(null)
const [amount, setAmount] = useState<BigNumber | null>(null)
const {
closeManagePerpModule,
previousAmount,
previousTradeDirection,
previousLeverage,
leverage,
asset,
} = usePerpsManageModule(amount)
if (!asset) return null
return (
<div
className={classNames(
'px-4 gap-5 flex flex-col h-full pt-6 pb-4 w-full bg-white/5 absolute rounded-base isolate',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-base before:p-[1px] before:border-glas',
)}
>
<div className='flex justify-between mb-3'>
<Text>Manage Position</Text>
<button onClick={closeManagePerpModule} className='mr-1.5'>
<Cross width={16} />
</button>
</div>
<TradeDirectionSelector
direction={tradeDirection ?? previousTradeDirection}
onChangeDirection={setTradeDirection}
/>
<AssetAmountInput
label='Amount'
max={BN(100000)} // TODO: Implement max calculation
amount={amount ?? previousAmount}
setAmount={setAmount}
asset={asset}
maxButtonLabel='Max:'
disabled={false}
/>
<Or />
<Text size='sm'>Position Leverage</Text>
<RangeInput max={0} value={0} onChange={() => {}} />
<LeverageButtons />
<Spacer />
<PerpsSummary
changeTradeDirection
amount={amount ?? previousAmount}
tradeDirection={tradeDirection ?? previousTradeDirection}
asset={asset}
leverage={leverage}
previousAmount={previousAmount}
previousTradeDirection={previousTradeDirection}
previousLeverage={previousLeverage}
/>
</div>
)
}

View File

@ -0,0 +1,68 @@
import { useCallback, useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { BN_ZERO } from 'constants/math'
import useAllAssets from 'hooks/assets/useAllAssets'
import usePerpPosition from 'hooks/perps/usePerpPosition'
import usePerpsAsset from 'hooks/perps/usePerpsAsset'
import useCurrentAccount from 'hooks/useCurrentAccount'
import usePrice from 'hooks/usePrice'
import usePrices from 'hooks/usePrices'
import { SearchParams } from 'types/enums/searchParams'
import { getAccountNetValue } from 'utils/accounts'
import { demagnify } from 'utils/formatters'
import { getSearchParamsObject } from 'utils/route'
export default function usePerpsManageModule(amount: BigNumber | null) {
const { perpsAsset } = usePerpsAsset()
const [searchParams, setSearchParams] = useSearchParams()
const perpPosition = usePerpPosition(perpsAsset.denom)
const { data: prices } = usePrices()
const assets = useAllAssets()
const account = useCurrentAccount()
const price = usePrice(perpsAsset.denom)
const accountNetValue = useMemo(() => {
if (!account || !prices || !assets) return BN_ZERO
return getAccountNetValue(account, prices, assets)
}, [account, assets, prices])
const closeManagePerpModule = useCallback(() => {
const params = getSearchParamsObject(searchParams)
delete params[SearchParams.PERPS_MANAGE]
setSearchParams({
...params,
})
}, [searchParams, setSearchParams])
const previousAmount = useMemo(() => perpPosition?.size ?? BN_ZERO, [perpPosition?.size])
const previousTradeDirection = useMemo(
() => perpPosition?.tradeDirection || 'long',
[perpPosition?.tradeDirection],
)
const previousLeverage = useMemo(
() =>
price.times(demagnify(previousAmount, perpsAsset)).div(accountNetValue).plus(1).toNumber(),
[accountNetValue, perpsAsset, previousAmount, price],
)
const leverage = useMemo(
() =>
price
.times(demagnify(amount ?? BN_ZERO, perpsAsset))
.div(accountNetValue)
.plus(1)
.toNumber(),
[accountNetValue, amount, perpsAsset, price],
)
return {
closeManagePerpModule,
previousAmount,
previousTradeDirection,
previousLeverage,
leverage,
asset: perpsAsset,
}
}

View File

@ -20,6 +20,7 @@ export function PerpsModule() {
const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market') const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market')
const [tradeDirection, setTradeDirection] = useState<TradeDirection>('long') const [tradeDirection, setTradeDirection] = useState<TradeDirection>('long')
const { perpsAsset } = usePerpsAsset() const { perpsAsset } = usePerpsAsset()
const [leverage, setLeverage] = useState<number>(1)
const [amount, setAmount] = useState<BigNumber>(BN_ZERO) const [amount, setAmount] = useState<BigNumber>(BN_ZERO)
@ -48,7 +49,12 @@ export function PerpsModule() {
<RangeInput max={0} value={0} onChange={() => {}} /> <RangeInput max={0} value={0} onChange={() => {}} />
<LeverageButtons /> <LeverageButtons />
<Spacer /> <Spacer />
<PerpsSummary amount={amount} tradeDirection={tradeDirection} asset={perpsAsset} /> <PerpsSummary
amount={amount}
tradeDirection={tradeDirection}
asset={perpsAsset}
leverage={leverage}
/>
</Card> </Card>
) )
} }

View File

@ -1,16 +1,27 @@
import { useCallback } from 'react' import classNames from 'classnames'
import { useCallback, useMemo } from 'react'
import AssetAmount from 'components/Asset/AssetAmount'
import ActionButton from 'components/Button/ActionButton' import ActionButton from 'components/Button/ActionButton'
import { ArrowRight } from 'components/Icons'
import TradeDirection from 'components/Perps/BalancesTable/Columns/TradeDirection'
import OpeningFee from 'components/Perps/Module/OpeningFee'
import SummaryLine from 'components/SummaryLine' import SummaryLine from 'components/SummaryLine'
import Text from 'components/Text' import Text from 'components/Text'
import useCurrentAccount from 'hooks/useCurrentAccount' import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store' import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin' import { BNCoin } from 'types/classes/BNCoin'
import { formatLeverage } from 'utils/formatters'
type Props = { type Props = {
leverage: number
amount: BigNumber amount: BigNumber
tradeDirection: TradeDirection tradeDirection: TradeDirection
asset: Asset asset: Asset
changeTradeDirection?: boolean
previousAmount?: BigNumber
previousTradeDirection?: 'long' | 'short'
previousLeverage?: number
} }
export default function PerpsSummary(props: Props) { export default function PerpsSummary(props: Props) {
@ -28,20 +39,85 @@ export default function PerpsSummary(props: Props) {
}) })
}, [currentAccount, openPerpPosition, props.amount, props.asset.denom, props.tradeDirection]) }, [currentAccount, openPerpPosition, props.amount, props.asset.denom, props.tradeDirection])
const disabled = useMemo(
() =>
(props.previousAmount && props.previousAmount.isEqualTo(props.amount)) ||
props.amount.isZero(),
[props.amount, props.previousAmount],
)
return ( return (
<div className='border border-white/10 rounded-sm bg-white/5'> <div className='border border-white/10 rounded-sm bg-white/5'>
<ManageSummary {...props} />
<div className='py-4 px-3 flex flex-col gap-1'> <div className='py-4 px-3 flex flex-col gap-1'>
<Text size='xs' className='font-bold mb-2'> <Text size='xs' className='font-bold mb-2'>
Summary Summary
</Text> </Text>
<SummaryLine label='Expected Price'>Something</SummaryLine> <SummaryLine label='Expected Price'>-</SummaryLine>
<SummaryLine label='Fees'>Something</SummaryLine> <SummaryLine label='Fees'>
<SummaryLine label='Total'>Something</SummaryLine> <OpeningFee denom={props.asset.denom} amount={props.amount} />
</SummaryLine>
<SummaryLine label='Total'>-</SummaryLine>
</div> </div>
<ActionButton onClick={onConfirm} className='w-full py-2.5'> <ActionButton onClick={onConfirm} disabled={disabled} className='w-full py-2.5'>
<span className='capitalize mr-1'>{props.tradeDirection}</span> <span className='capitalize mr-1'>{props.tradeDirection}</span>
{props.asset.symbol} {props.asset.symbol}
</ActionButton> </ActionButton>
</div> </div>
) )
} }
function ManageSummary(props: Props) {
const showTradeDirection =
props.previousTradeDirection && props.previousTradeDirection !== props.tradeDirection
const showAmount =
props.previousAmount && props.amount && !props.previousAmount.isEqualTo(props.amount)
const showLeverage =
props.previousLeverage &&
props.leverage &&
props.previousLeverage.toFixed(2) !== props.leverage.toFixed(2)
if (!showTradeDirection && !showLeverage && !showAmount) return null
return (
<div className='pt-4 px-3 flex flex-col gap-1'>
<Text size='xs' className='font-bold mb-2'>
Your new position
</Text>
{showTradeDirection && props.previousTradeDirection && (
<SummaryLine label='Side' contentClassName='flex gap-1'>
<TradeDirection tradeDirection={props.previousTradeDirection} />
<ArrowRight width={16} />
<TradeDirection tradeDirection={props.tradeDirection} />
</SummaryLine>
)}
{showAmount && props.previousAmount && (
<SummaryLine label='Size' contentClassName='flex gap-1'>
<AssetAmount asset={props.asset} amount={props.previousAmount.toNumber()} />
<ArrowRight
width={16}
className={classNames(
props.previousAmount.isGreaterThan(props.amount) ? 'text-error' : 'text-success',
)}
/>
<AssetAmount asset={props.asset} amount={props.amount.toNumber()} />
</SummaryLine>
)}
{showLeverage && props.previousLeverage && (
<SummaryLine label='Leverage' contentClassName='flex gap-1'>
<span>{formatLeverage(props.previousLeverage)}</span>
<ArrowRight
width={16}
className={classNames(
props.leverage > props.previousLeverage ? 'text-error' : 'text-success',
)}
/>
<span>{formatLeverage(props.leverage)}</span>
</SummaryLine>
)}
</div>
)
}

View File

@ -50,6 +50,8 @@ export function PerpsInfo() {
] ]
}, [assetPrice, market]) }, [assetPrice, market])
if (!market) return null
return ( return (
<Card contentClassName='bg-white/10 py-3.5 px-4'> <Card contentClassName='bg-white/10 py-3.5 px-4'>
<div className='flex gap-4 items-center'> <div className='flex gap-4 items-center'>

View File

@ -6,13 +6,14 @@ const infoLineClasses = 'flex flex-row justify-between flex-1 mb-1 text-xs text-
interface SummaryLineProps { interface SummaryLineProps {
children: React.ReactNode children: React.ReactNode
className?: string className?: string
contentClassName?: string
label: string label: string
} }
export default function SummaryLine(props: SummaryLineProps) { export default function SummaryLine(props: SummaryLineProps) {
return ( return (
<div className={classNames(infoLineClasses, props.className)}> <div className={classNames(infoLineClasses, props.className)}>
<span className='opacity-40'>{props.label}</span> <span className='opacity-40'>{props.label}</span>
<span>{props.children}</span> <span className={props.contentClassName}>{props.children}</span>
</div> </div>
) )
} }

View File

@ -0,0 +1,16 @@
import BigNumber from 'bignumber.js'
import useSWR from 'swr'
import getOpeningFee from 'api/perps/getOpeningFee'
import useChainConfig from 'hooks/useChainConfig'
import useDebounce from 'hooks/useDebounce'
export default function useOpeningFee(denom: string, amount: BigNumber) {
const chainConfig = useChainConfig()
const debouncedAmount = useDebounce<string>(amount.toString(), 500)
const enabled = !amount.isZero()
return useSWR(enabled && `${chainConfig.id}/perps/${denom}/openingFee/${debouncedAmount}`, () =>
getOpeningFee(chainConfig, denom, amount.toString()),
)
}

View File

@ -0,0 +1,11 @@
import { useMemo } from 'react'
import { byDenom } from 'utils/array'
import useCurrentAccount from '../useCurrentAccount'
export default function usePerpPosition(denom: string): PerpsPosition | undefined {
const account = useCurrentAccount()
return useMemo(() => account?.perps.find(byDenom(denom)), [account?.perps, denom])
}

View File

@ -6,10 +6,11 @@ import { LocalStorageKeys } from 'constants/localStorageKeys'
import usePerpsEnabledAssets from 'hooks/assets/usePerpsEnabledAssets' import usePerpsEnabledAssets from 'hooks/assets/usePerpsEnabledAssets'
import useLocalStorage from 'hooks/localStorage/useLocalStorage' import useLocalStorage from 'hooks/localStorage/useLocalStorage'
import useChainConfig from 'hooks/useChainConfig' import useChainConfig from 'hooks/useChainConfig'
import { getSearchParamsObject } from 'utils/route'
export default function usePerpsAsset() { export default function usePerpsAsset() {
const chainConfig = useChainConfig() const chainConfig = useChainConfig()
const [searchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const perpsAssets = usePerpsEnabledAssets() const perpsAssets = usePerpsEnabledAssets()
const perpsAssetInParams = searchParams.get('perpsMarket') const perpsAssetInParams = searchParams.get('perpsMarket')
const [perpsAssetInLocalStorage, setPerpsAssetInLocalStorage] = useLocalStorage< const [perpsAssetInLocalStorage, setPerpsAssetInLocalStorage] = useLocalStorage<
@ -18,9 +19,12 @@ export default function usePerpsAsset() {
const updatePerpsAsset = useCallback( const updatePerpsAsset = useCallback(
(denom: string) => { (denom: string) => {
const params = getSearchParamsObject(searchParams)
params.perpsMarket = denom
setSearchParams(params)
setPerpsAssetInLocalStorage(denom) setPerpsAssetInLocalStorage(denom)
}, },
[setPerpsAssetInLocalStorage], [searchParams, setPerpsAssetInLocalStorage, setSearchParams],
) )
return { return {

View File

@ -1,3 +1,5 @@
import { useMemo } from 'react'
import useAccounts from 'hooks/accounts/useAccounts' import useAccounts from 'hooks/accounts/useAccounts'
import useAccountId from 'hooks/useAccountId' import useAccountId from 'hooks/useAccountId'
@ -5,5 +7,5 @@ export default function useCurrentAccount(): Account | undefined {
const accountId = useAccountId() const accountId = useAccountId()
const { data: accounts } = useAccounts('default', undefined, false) const { data: accounts } = useAccounts('default', undefined, false)
return accounts?.find((account) => account.id === accountId) return useMemo(() => accounts?.find((account) => account.id === accountId), [accountId, accounts])
} }

17
src/hooks/useDebounce.ts Normal file
View File

@ -0,0 +1,17 @@
import { useEffect, useState } from 'react'
export default function useDebounce<T>(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}

View File

@ -1,14 +1,22 @@
import { useSearchParams } from 'react-router-dom'
import { PerpsManageModule } from 'components/Perps/Module/PerpsManageModule'
import { PerpsModule } from 'components/Perps/Module/PerpsModule' import { PerpsModule } from 'components/Perps/Module/PerpsModule'
import { PerpsChart } from 'components/Perps/PerpsChart' import { PerpsChart } from 'components/Perps/PerpsChart'
import { PerpsInfo } from 'components/Perps/PerpsInfo' import { PerpsInfo } from 'components/Perps/PerpsInfo'
import { PerpsPositions } from 'components/Perps/PerpsPositions' import { PerpsPositions } from 'components/Perps/PerpsPositions'
import { SearchParams } from 'types/enums/searchParams'
export default function PerpsPage() { export default function PerpsPage() {
const [searchParams] = useSearchParams()
const isManagingPosition = searchParams.get(SearchParams.PERPS_MANAGE) === 'true'
return ( return (
<div className='grid grid-cols-[auto_376px] grid-rows-[min-content_auto_auto] w-full gap-4'> <div className='grid grid-cols-[auto_376px] grid-rows-[min-content_auto_auto] w-full gap-4'>
<PerpsInfo /> <PerpsInfo />
<div className='h-full w-[376px] row-span-3'> <div className='h-full w-[376px] row-span-3 relative'>
<PerpsModule /> {isManagingPosition ? <PerpsManageModule /> : <PerpsModule />}
</div> </div>
<PerpsChart /> <PerpsChart />
<PerpsPositions /> <PerpsPositions />

View File

@ -0,0 +1,5 @@
export enum SearchParams {
ACCOUNT_ID = 'accountId',
PERPS_MARKET = 'perpsMarket',
PERPS_MANAGE = 'perpsManage',
}

View File

@ -1,275 +0,0 @@
// @ts-nocheck
/**
* This file was automatically generated by @cosmwasm/ts-codegen@0.33.0.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
*/
import { MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate'
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
import { toUtf8 } from '@cosmjs/encoding'
import {
HealthContractBaseForString,
IncentivesUnchecked,
Decimal,
Uint128,
OracleBaseForString,
ParamsBaseForString,
RedBankUnchecked,
SwapperBaseForString,
ZapperBaseForString,
InstantiateMsg,
ExecuteMsg,
AccountKind,
Action,
ActionAmount,
LiquidateRequestForVaultBaseForString,
VaultPositionType,
AccountNftBaseForString,
OwnerUpdate,
CallbackMsg,
Addr,
HealthState,
LiquidateRequestForVaultBaseForAddr,
ChangeExpected,
Coin,
ActionCoin,
VaultBaseForString,
ConfigUpdates,
NftConfigUpdates,
VaultBaseForAddr,
QueryMsg,
VaultPositionAmount,
VaultAmount,
VaultAmount1,
UnlockingPositions,
VaultPosition,
LockingVaultAmount,
VaultUnlockingPosition,
ArrayOfAccount,
Account,
ArrayOfCoinBalanceResponseItem,
CoinBalanceResponseItem,
ArrayOfSharesResponseItem,
SharesResponseItem,
ArrayOfDebtShares,
DebtShares,
ArrayOfVaultPositionResponseItem,
VaultPositionResponseItem,
ConfigResponse,
OwnerResponse,
RewardsCollector,
ArrayOfCoin,
Positions,
DebtAmount,
VaultPositionValue,
CoinValue,
VaultUtilizationResponse,
} from './MarsCreditManager.types'
export interface MarsCreditManagerMessage {
contractAddress: string
sender: string
createCreditAccount: (_funds?: Coin[]) => MsgExecuteContractEncodeObject
updateCreditAccount: (
{
accountId,
actions,
}: {
accountId: string
actions: Action[]
},
_funds?: Coin[],
) => MsgExecuteContractEncodeObject
repayFromWallet: (
{
accountId,
}: {
accountId: string
},
_funds?: Coin[],
) => MsgExecuteContractEncodeObject
updateConfig: (
{
updates,
}: {
updates: ConfigUpdates
},
_funds?: Coin[],
) => MsgExecuteContractEncodeObject
updateOwner: (ownerUpdate: OwnerUpdate, _funds?: Coin[]) => MsgExecuteContractEncodeObject
updateNftConfig: (
{
config,
ownership,
}: {
config?: NftConfigUpdates
ownership?: Action
},
_funds?: Coin[],
) => MsgExecuteContractEncodeObject
callback: (callbackMsg: CallbackMsg, _funds?: Coin[]) => MsgExecuteContractEncodeObject
}
export class MarsCreditManagerMessageComposer implements MarsCreditManagerMessage {
sender: string
contractAddress: string
constructor(sender: string, contractAddress: string) {
this.sender = sender
this.contractAddress = contractAddress
this.createCreditAccount = this.createCreditAccount.bind(this)
this.updateCreditAccount = this.updateCreditAccount.bind(this)
this.repayFromWallet = this.repayFromWallet.bind(this)
this.updateConfig = this.updateConfig.bind(this)
this.updateOwner = this.updateOwner.bind(this)
this.updateNftConfig = this.updateNftConfig.bind(this)
this.callback = this.callback.bind(this)
}
createCreditAccount = (_funds?: Coin[]): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
create_credit_account: {},
}),
),
funds: _funds,
}),
}
}
updateCreditAccount = (
{
accountId,
actions,
}: {
accountId: string
actions: Action[]
},
_funds?: Coin[],
): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
update_credit_account: {
account_id: accountId,
actions,
},
}),
),
funds: _funds,
}),
}
}
repayFromWallet = (
{
accountId,
}: {
accountId: string
},
_funds?: Coin[],
): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
repay_from_wallet: {
account_id: accountId,
},
}),
),
funds: _funds,
}),
}
}
updateConfig = (
{
updates,
}: {
updates: ConfigUpdates
},
_funds?: Coin[],
): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
update_config: {
updates,
},
}),
),
funds: _funds,
}),
}
}
updateOwner = (ownerUpdate: OwnerUpdate, _funds?: Coin[]): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
update_owner: ownerUpdate,
}),
),
funds: _funds,
}),
}
}
updateNftConfig = (
{
config,
ownership,
}: {
config?: NftConfigUpdates
ownership?: Action
},
_funds?: Coin[],
): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
update_nft_config: {
config,
ownership,
},
}),
),
funds: _funds,
}),
}
}
callback = (callbackMsg: CallbackMsg, _funds?: Coin[]): MsgExecuteContractEncodeObject => {
return {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: this.sender,
contract: this.contractAddress,
msg: toUtf8(
JSON.stringify({
callback: callbackMsg,
}),
),
funds: _funds,
}),
}
}
}

View File

@ -8,12 +8,13 @@
import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { StdFee } from '@cosmjs/amino' import { StdFee } from '@cosmjs/amino'
import { import {
Decimal,
Uint128, Uint128,
OracleBaseForString, OracleBaseForString,
ParamsBaseForString,
InstantiateMsg, InstantiateMsg,
ExecuteMsg, ExecuteMsg,
OwnerUpdate, OwnerUpdate,
Decimal,
SignedDecimal, SignedDecimal,
QueryMsg, QueryMsg,
ConfigForString, ConfigForString,
@ -22,13 +23,17 @@ import {
ArrayOfDenomStateResponse, ArrayOfDenomStateResponse,
DepositResponse, DepositResponse,
ArrayOfDepositResponse, ArrayOfDepositResponse,
TradingFee,
Coin,
OwnerResponse, OwnerResponse,
PerpDenomState, PerpDenomState,
PnlValues, DenomPnlValues,
PnL, PnL,
PositionResponse, PositionResponse,
PerpPosition, PerpPosition,
Coin, PositionPnl,
PnlCoins,
PnlValues,
ArrayOfPositionResponse, ArrayOfPositionResponse,
PositionsByAccountResponse, PositionsByAccountResponse,
ArrayOfUnlockState, ArrayOfUnlockState,
@ -74,6 +79,7 @@ export interface MarsPerpsReadOnlyInterface {
}) => Promise<ArrayOfPositionResponse> }) => Promise<ArrayOfPositionResponse>
positionsByAccount: ({ accountId }: { accountId: string }) => Promise<PositionsByAccountResponse> positionsByAccount: ({ accountId }: { accountId: string }) => Promise<PositionsByAccountResponse>
totalPnl: () => Promise<SignedDecimal> totalPnl: () => Promise<SignedDecimal>
openingFee: ({ denom, size }: { denom: string; size: SignedDecimal }) => Promise<TradingFee>
} }
export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface { export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface {
client: CosmWasmClient client: CosmWasmClient
@ -95,6 +101,7 @@ export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface {
this.positions = this.positions.bind(this) this.positions = this.positions.bind(this)
this.positionsByAccount = this.positionsByAccount.bind(this) this.positionsByAccount = this.positionsByAccount.bind(this)
this.totalPnl = this.totalPnl.bind(this) this.totalPnl = this.totalPnl.bind(this)
this.openingFee = this.openingFee.bind(this)
} }
owner = async (): Promise<OwnerResponse> => { owner = async (): Promise<OwnerResponse> => {
@ -212,6 +219,20 @@ export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface {
total_pnl: {}, total_pnl: {},
}) })
} }
openingFee = async ({
denom,
size,
}: {
denom: string
size: SignedDecimal
}): Promise<TradingFee> => {
return this.client.queryContractSmart(this.contractAddress, {
opening_fee: {
denom,
size,
},
})
}
} }
export interface MarsPerpsInterface extends MarsPerpsReadOnlyInterface { export interface MarsPerpsInterface extends MarsPerpsReadOnlyInterface {
contractAddress: string contractAddress: string

View File

@ -9,12 +9,13 @@ import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tan
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
import { StdFee } from '@cosmjs/amino' import { StdFee } from '@cosmjs/amino'
import { import {
Decimal,
Uint128, Uint128,
OracleBaseForString, OracleBaseForString,
ParamsBaseForString,
InstantiateMsg, InstantiateMsg,
ExecuteMsg, ExecuteMsg,
OwnerUpdate, OwnerUpdate,
Decimal,
SignedDecimal, SignedDecimal,
QueryMsg, QueryMsg,
ConfigForString, ConfigForString,
@ -23,13 +24,17 @@ import {
ArrayOfDenomStateResponse, ArrayOfDenomStateResponse,
DepositResponse, DepositResponse,
ArrayOfDepositResponse, ArrayOfDepositResponse,
TradingFee,
Coin,
OwnerResponse, OwnerResponse,
PerpDenomState, PerpDenomState,
PnlValues, DenomPnlValues,
PnL, PnL,
PositionResponse, PositionResponse,
PerpPosition, PerpPosition,
Coin, PositionPnl,
PnlCoins,
PnlValues,
ArrayOfPositionResponse, ArrayOfPositionResponse,
PositionsByAccountResponse, PositionsByAccountResponse,
ArrayOfUnlockState, ArrayOfUnlockState,
@ -75,6 +80,8 @@ export const marsPerpsQueryKeys = {
] as const, ] as const,
totalPnl: (contractAddress: string | undefined, args?: Record<string, unknown>) => totalPnl: (contractAddress: string | undefined, args?: Record<string, unknown>) =>
[{ ...marsPerpsQueryKeys.address(contractAddress)[0], method: 'total_pnl', args }] as const, [{ ...marsPerpsQueryKeys.address(contractAddress)[0], method: 'total_pnl', args }] as const,
openingFee: (contractAddress: string | undefined, args?: Record<string, unknown>) =>
[{ ...marsPerpsQueryKeys.address(contractAddress)[0], method: 'opening_fee', args }] as const,
} }
export interface MarsPerpsReactQuery<TResponse, TData = TResponse> { export interface MarsPerpsReactQuery<TResponse, TData = TResponse> {
client: MarsPerpsQueryClient | undefined client: MarsPerpsQueryClient | undefined
@ -85,6 +92,29 @@ export interface MarsPerpsReactQuery<TResponse, TData = TResponse> {
initialData?: undefined initialData?: undefined
} }
} }
export interface MarsPerpsOpeningFeeQuery<TData> extends MarsPerpsReactQuery<TradingFee, TData> {
args: {
denom: string
size: SignedDecimal
}
}
export function useMarsPerpsOpeningFeeQuery<TData = TradingFee>({
client,
args,
options,
}: MarsPerpsOpeningFeeQuery<TData>) {
return useQuery<TradingFee, Error, TData>(
marsPerpsQueryKeys.openingFee(client?.contractAddress, args),
() =>
client
? client.openingFee({
denom: args.denom,
size: args.size,
})
: Promise.reject(new Error('Invalid client')),
{ ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) },
)
}
export interface MarsPerpsTotalPnlQuery<TData> extends MarsPerpsReactQuery<SignedDecimal, TData> {} export interface MarsPerpsTotalPnlQuery<TData> extends MarsPerpsReactQuery<SignedDecimal, TData> {}
export function useMarsPerpsTotalPnlQuery<TData = SignedDecimal>({ export function useMarsPerpsTotalPnlQuery<TData = SignedDecimal>({
client, client,

View File

@ -5,14 +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.
*/ */
export type Decimal = string
export type Uint128 = string export type Uint128 = string
export type OracleBaseForString = string export type OracleBaseForString = string
export type ParamsBaseForString = string
export interface InstantiateMsg { export interface InstantiateMsg {
base_denom: string base_denom: string
closing_fee_rate: Decimal
cooldown_period: number cooldown_period: number
credit_manager: string credit_manager: string
min_position_value: Uint128 max_position_in_base_denom?: Uint128 | null
min_position_in_base_denom: Uint128
opening_fee_rate: Decimal
oracle: OracleBaseForString oracle: OracleBaseForString
params: ParamsBaseForString
} }
export type ExecuteMsg = export type ExecuteMsg =
| { | {
@ -74,7 +80,6 @@ export type OwnerUpdate =
} }
} }
| 'clear_emergency_owner' | 'clear_emergency_owner'
export type Decimal = string
export interface SignedDecimal { export interface SignedDecimal {
abs: Decimal abs: Decimal
negative: boolean negative: boolean
@ -142,12 +147,22 @@ export type QueryMsg =
| { | {
total_pnl: {} total_pnl: {}
} }
| {
opening_fee: {
denom: string
size: SignedDecimal
}
}
export interface ConfigForString { export interface ConfigForString {
base_denom: string base_denom: string
closing_fee_rate: Decimal
cooldown_period: number cooldown_period: number
credit_manager: string credit_manager: string
min_position_value: Uint128 max_position_in_base_denom?: Uint128 | null
min_position_in_base_denom: Uint128
opening_fee_rate: Decimal
oracle: OracleBaseForString oracle: OracleBaseForString
params: ParamsBaseForString
} }
export interface DenomStateResponse { export interface DenomStateResponse {
denom: string denom: string
@ -155,14 +170,11 @@ export interface DenomStateResponse {
funding: Funding funding: Funding
last_updated: number last_updated: number
total_cost_base: SignedDecimal total_cost_base: SignedDecimal
total_size: SignedDecimal
} }
export interface Funding { export interface Funding {
accumulated_size_weighted_by_index: SignedDecimal last_funding_accrued_per_unit_in_base_denom: SignedDecimal
constant_factor: SignedDecimal last_funding_rate: SignedDecimal
index: SignedDecimal
max_funding_velocity: Decimal max_funding_velocity: Decimal
rate: SignedDecimal
skew_scale: Decimal skew_scale: Decimal
} }
export type ArrayOfDenomStateResponse = DenomStateResponse[] export type ArrayOfDenomStateResponse = DenomStateResponse[]
@ -172,6 +184,15 @@ export interface DepositResponse {
shares: Uint128 shares: Uint128
} }
export type ArrayOfDepositResponse = DepositResponse[] export type ArrayOfDepositResponse = DepositResponse[]
export interface TradingFee {
fee: Coin
rate: Decimal
}
export interface Coin {
amount: Uint128
denom: string
[k: string]: unknown
}
export interface OwnerResponse { export interface OwnerResponse {
abolished: boolean abolished: boolean
emergency_owner?: string | null emergency_owner?: string | null
@ -180,19 +201,17 @@ export interface OwnerResponse {
proposed?: string | null proposed?: string | null
} }
export interface PerpDenomState { export interface PerpDenomState {
constant_factor: SignedDecimal
denom: string denom: string
enabled: boolean enabled: boolean
index: SignedDecimal pnl_values: DenomPnlValues
pnl_values: PnlValues
rate: SignedDecimal rate: SignedDecimal
total_cost_base: SignedDecimal total_entry_cost: SignedDecimal
total_size: SignedDecimal total_entry_funding: SignedDecimal
} }
export interface PnlValues { export interface DenomPnlValues {
accrued_funding: SignedDecimal accrued_funding: SignedDecimal
pnl: SignedDecimal pnl: SignedDecimal
unrealized_pnl: SignedDecimal price_pnl: SignedDecimal
} }
export type PnL = export type PnL =
| 'break_even' | 'break_even'
@ -212,14 +231,22 @@ export interface PerpPosition {
current_price: Decimal current_price: Decimal
denom: string denom: string
entry_price: Decimal entry_price: Decimal
pnl: PnL pnl: PositionPnl
size: SignedDecimal size: SignedDecimal
unrealised_funding_accrued: SignedDecimal
} }
export interface Coin { export interface PositionPnl {
amount: Uint128 coins: PnlCoins
denom: string values: PnlValues
[k: string]: unknown }
export interface PnlCoins {
closing_fee: Coin
pnl: PnL
}
export interface PnlValues {
accrued_funding: SignedDecimal
closing_fee: SignedDecimal
pnl: SignedDecimal
price_pnl: SignedDecimal
} }
export type ArrayOfPositionResponse = PositionResponse[] export type ArrayOfPositionResponse = PositionResponse[]
export interface PositionsByAccountResponse { export interface PositionsByAccountResponse {

View File

@ -1,3 +1,5 @@
import { SearchParams } from 'types/enums/searchParams'
export function getRoute( export function getRoute(
page: Page, page: Page,
searchParams: URLSearchParams, searchParams: URLSearchParams,
@ -19,8 +21,8 @@ export function getRoute(
) )
if (accountId) { if (accountId) {
url.searchParams.delete('accountId') url.searchParams.delete(SearchParams.ACCOUNT_ID)
url.searchParams.append('accountId', accountId) url.searchParams.append(SearchParams.ACCOUNT_ID, accountId)
} }
return url.pathname + url.search return url.pathname + url.search
@ -53,3 +55,11 @@ export function getPage(pathname: string): Page {
return 'trade' as Page return 'trade' as Page
} }
export function getSearchParamsObject(searchParams: URLSearchParams) {
const params: { [key: string]: string } = {}
Array.from(searchParams?.entries() || []).forEach(([key, value]) => (params[key] = value))
return params
}