merge main

This commit is contained in:
saml33 2023-07-31 09:56:42 +10:00
commit 840991961f
35 changed files with 1256 additions and 571 deletions

View File

@ -1,5 +1,6 @@
import {
ArrowDownTrayIcon,
ArrowPathIcon,
ExclamationCircleIcon,
LinkIcon,
} from '@heroicons/react/20/solid'
@ -17,7 +18,7 @@ import { TokenAccount } from './../utils/tokens'
import ActionTokenList from './account/ActionTokenList'
import ButtonGroup from './forms/ButtonGroup'
import Label from './forms/Label'
import Button from './shared/Button'
import Button, { IconButton } from './shared/Button'
import Loading from './shared/Loading'
import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions'
import { withValueLimit } from './swap/MarketSwapForm'
@ -63,7 +64,7 @@ export const walletBalanceForToken = (
}
function DepositForm({ onSuccess, token }: DepositFormProps) {
const { t } = useTranslation('common')
const { t } = useTranslation(['common', 'account'])
const [inputAmount, setInputAmount] = useState('')
const [submitting, setSubmitting] = useState(false)
const [selectedToken, setSelectedToken] = useState(
@ -71,6 +72,7 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
)
const [showTokenList, setShowTokenList] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const [refreshingWalletTokens, setRefreshingWalletTokens] = useState(false)
const { connect } = useWallet()
const { maxSolDeposit } = useSolBalance()
const banks = useBanksWithBalances('walletBalance')
@ -110,6 +112,14 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
setShowTokenList(false)
}
const handleRefreshWalletBalances = useCallback(async () => {
if (!publicKey) return
const actions = mangoStore.getState().actions
setRefreshingWalletTokens(true)
await actions.fetchWalletTokens(publicKey)
setRefreshingWalletTokens(false)
}, [publicKey])
const handleDeposit = useCallback(async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
@ -196,13 +206,23 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
<div className="grid grid-cols-2">
<div className="col-span-2 flex justify-between">
<Label text={`${t('deposit')} ${t('token')}`} />
<MaxAmountButton
className="mb-2"
decimals={tokenMax.maxDecimals}
label={t('wallet-balance')}
onClick={setMax}
value={tokenMax.maxAmount}
/>
<div className="mb-2 flex items-center space-x-2">
<MaxAmountButton
decimals={tokenMax.maxDecimals}
label={t('wallet-balance')}
onClick={setMax}
value={tokenMax.maxAmount}
/>
<Tooltip content={t('account:refresh-balance')}>
<IconButton
className={refreshingWalletTokens ? 'animate-spin' : ''}
onClick={handleRefreshWalletBalances}
hideBg
>
<ArrowPathIcon className="h-4 w-4" />
</IconButton>
</Tooltip>
</div>
</div>
<div className="col-span-1">
<TokenListButton

View File

@ -2,7 +2,6 @@ import { useEffect } from 'react'
import mangoStore from '@store/mangoStore'
import { Keypair, PublicKey } from '@solana/web3.js'
import { useRouter } from 'next/router'
import { MangoAccount } from '@blockworks-foundation/mango-v4'
import useMangoAccount from 'hooks/useMangoAccount'
import useInterval from './shared/useInterval'
import { SECONDS } from 'utils/constants'
@ -99,13 +98,9 @@ const HydrateStore = () => {
if (!mangoAccount) return
if (context.slot > lastSeenSlot) {
const decodedMangoAccount = client.program.coder.accounts.decode(
'mangoAccount',
info?.data,
)
const newMangoAccount = MangoAccount.from(
const newMangoAccount = await client.getMangoAccountFromAi(
mangoAccount.publicKey,
decodedMangoAccount,
info,
)
if (newMangoAccount.serum3Active().length > 0) {
await newMangoAccount.reloadSerum3OpenOrders(client)

View File

@ -17,7 +17,14 @@ import Tooltip from './shared/Tooltip'
import { formatTokenSymbol } from 'utils/tokens'
import useMangoAccount from 'hooks/useMangoAccount'
import useJupiterMints from '../hooks/useJupiterMints'
import { Table, Td, Th, TrBody, TrHead } from './shared/TableElements'
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from './shared/TableElements'
import DepositWithdrawModal from './modals/DepositWithdrawModal'
import BorrowRepayModal from './modals/BorrowRepayModal'
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'
@ -33,6 +40,22 @@ import useUnownedAccount from 'hooks/useUnownedAccount'
import useLocalStorageState from 'hooks/useLocalStorageState'
import TokenLogo from './shared/TokenLogo'
import useHealthContributions from 'hooks/useHealthContributions'
import { useSortableData } from 'hooks/useSortableData'
type TableData = {
bank: Bank
balance: number
symbol: string
interestAmount: number
interestValue: number
inOrders: number
unsettled: number
collateralValue: number
assetWeight: string
liabWeight: string
depositRate: number
borrowRate: number
}
const TokenList = () => {
const { t } = useTranslation(['common', 'token', 'trade'])
@ -50,26 +73,80 @@ const TokenList = () => {
const showTableView = width ? width > breakpoints.md : false
const banks = useBanksWithBalances('balance')
const filteredBanks = useMemo(() => {
if (banks.length) {
return showZeroBalances || !mangoAccountAddress
? banks
: banks.filter((b) => Math.abs(b.balance) > 0)
}
return []
}, [banks, mangoAccountAddress, showZeroBalances])
const formattedTableData = useCallback(
(banks: BankWithBalance[]) => {
const formatted = []
for (const b of banks) {
const bank = b.bank
const balance = b.balance
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
// const filteredBanks = useMemo(() => {
// if (!banks.length) return []
// if (showZeroBalances || !mangoAccountAddress) return banks
// const filtered = banks.filter((b) => {
// const contribution =
// initContributions.find((cont) => cont.asset === b.bank.name)
// ?.contribution || 0
// return Math.abs(contribution) > 0
// })
// return filtered
// }, [banks, mangoAccountAddress, showZeroBalances, initContributions])
const hasInterestEarned = totalInterestData.find(
(d) =>
d.symbol.toLowerCase() === symbol.toLowerCase() ||
(symbol === 'ETH (Portal)' && d.symbol === 'ETH'),
)
const interestAmount = hasInterestEarned
? hasInterestEarned.borrow_interest * -1 +
hasInterestEarned.deposit_interest
: 0
const interestValue = hasInterestEarned
? hasInterestEarned.borrow_interest_usd * -1 +
hasInterestEarned.deposit_interest_usd
: 0.0
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
const collateralValue =
initContributions.find((val) => val.asset === bank.name)
?.contribution || 0
const assetWeight = bank.scaledInitAssetWeight(bank.price).toFixed(2)
const liabWeight = bank.scaledInitLiabWeight(bank.price).toFixed(2)
const depositRate = bank.getDepositRateUi()
const borrowRate = bank.getBorrowRateUi()
const data = {
balance,
bank,
symbol,
interestAmount,
interestValue,
inOrders,
unsettled,
collateralValue,
assetWeight,
liabWeight,
depositRate,
borrowRate,
}
formatted.push(data)
}
return formatted
},
[initContributions, spotBalances, totalInterestData],
)
const unsortedTableData = useMemo(() => {
if (!banks.length) return []
if (showZeroBalances || !mangoAccountAddress) {
return formattedTableData(banks)
} else {
const filtered = banks.filter((b) => Math.abs(b.balance) > 0)
return formattedTableData(filtered)
}
}, [banks, mangoAccountAddress, showZeroBalances, totalInterestData])
const {
items: tableData,
requestSort,
sortConfig,
} = useSortableData(unsortedTableData)
return (
<ContentBox hideBorder hidePadding>
@ -89,36 +166,84 @@ const TokenList = () => {
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('token')}</Th>
<Th className="text-left">
<SortableColumnHeader
sortKey="symbol"
sort={() => requestSort('symbol')}
sortConfig={sortConfig}
title={t('token')}
/>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content="A negative balance represents a borrow">
<span className="tooltip-underline">{t('balance')}</span>
<SortableColumnHeader
sortKey="balance"
sort={() => requestSort('balance')}
sortConfig={sortConfig}
title={t('balance')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('tooltip-collateral-value')}>
<span className="tooltip-underline">
{t('collateral-value')}
</span>
<SortableColumnHeader
sortKey="collateralValue"
sort={() => requestSort('collateralValue')}
sortConfig={sortConfig}
title={t('collateral-value')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th className="text-right">{t('trade:in-orders')}</Th>
<Th className="text-right">{t('trade:unsettled')}</Th>
<Th className="flex justify-end" id="account-step-nine">
<Tooltip content="The sum of interest earned and interest paid for each token">
<span className="tooltip-underline">
{t('interest-earned-paid')}
</span>
</Tooltip>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="inOrders"
sort={() => requestSort('inOrders')}
sortConfig={sortConfig}
title={t('trade:in-orders')}
/>
</div>
</Th>
<Th id="account-step-ten">
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="unsettled"
sort={() => requestSort('unsettled')}
sortConfig={sortConfig}
title={t('trade:unsettled')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content="The sum of interest earned and interest paid for each token">
<SortableColumnHeader
sortKey="interestValue"
sort={() => requestSort('interestValue')}
sortConfig={sortConfig}
title={t('interest-earned-paid')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content="The interest rates for depositing (green/left) and borrowing (red/right)">
<span className="tooltip-underline">{t('rates')}</span>
<SortableColumnHeader
sortKey="depositRate"
sort={() => requestSort('depositRate')}
sortConfig={sortConfig}
title={t('rates')}
titleClass="tooltip-underline"
/>
{/* <span className="tooltip-underline">{t('rates')}</span> */}
</Tooltip>
</div>
</Th>
@ -128,47 +253,24 @@ const TokenList = () => {
</TrHead>
</thead>
<tbody>
{filteredBanks.map((b) => {
const bank = b.bank
const tokenBalance = b.balance
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const hasInterestEarned = totalInterestData.find(
(d) =>
d.symbol.toLowerCase() === symbol.toLowerCase() ||
(symbol === 'ETH (Portal)' && d.symbol === 'ETH'),
)
const interestAmount = hasInterestEarned
? hasInterestEarned.borrow_interest * -1 +
hasInterestEarned.deposit_interest
: 0
const interestValue = hasInterestEarned
? hasInterestEarned.borrow_interest_usd * -1 +
hasInterestEarned.deposit_interest_usd
: 0.0
const inOrders =
spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled =
spotBalances[bank.mint.toString()]?.unsettled || 0
const collateralValue =
initContributions.find((val) => val.asset === bank.name)
?.contribution || 0
const assetWeight = bank
.scaledInitAssetWeight(bank.price)
.toFixed(2)
const liabWeight = bank
.scaledInitLiabWeight(bank.price)
.toFixed(2)
{tableData.map((data) => {
const {
balance,
bank,
symbol,
interestAmount,
interestValue,
inOrders,
unsettled,
collateralValue,
assetWeight,
liabWeight,
depositRate,
borrowRate,
} = data
return (
<TrBody key={bank.name}>
<TrBody key={symbol}>
<Td>
<div className="flex items-center">
<div className="mr-2.5 flex flex-shrink-0 items-center">
@ -179,7 +281,7 @@ const TokenList = () => {
</Td>
<Td className="text-right">
<BankAmountWithValue
amount={tokenBalance}
amount={balance}
bank={bank}
stacked
/>
@ -229,17 +331,14 @@ const TokenList = () => {
<div className="flex justify-end space-x-1.5">
<p className="text-th-up">
<FormatNumericValue
value={bank.getDepositRateUi()}
value={depositRate}
decimals={2}
/>
%
</p>
<span className="text-th-fgd-4">|</span>
<p className="text-th-down">
<FormatNumericValue
value={bank.getBorrowRateUi()}
decimals={2}
/>
<FormatNumericValue value={borrowRate} decimals={2} />
%
</p>
</div>
@ -257,8 +356,8 @@ const TokenList = () => {
</>
) : (
<div className="border-b border-th-bkg-3">
{filteredBanks.map((b) => {
return <MobileTokenListItem key={b.bank.name} bank={b} />
{tableData.map((data) => {
return <MobileTokenListItem key={data.bank.name} data={data} />
})}
</div>
)}
@ -268,46 +367,23 @@ const TokenList = () => {
export default TokenList
const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
const MobileTokenListItem = ({ data }: { data: TableData }) => {
const { t } = useTranslation(['common', 'token'])
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
const { mangoAccount } = useMangoAccount()
const { initContributions } = useHealthContributions()
const totalInterestData = mangoStore(
(s) => s.mangoAccount.interestTotals.data,
)
const tokenBank = bank.bank
const mint = tokenBank.mint
const symbol = tokenBank.name === 'MSOL' ? 'mSOL' : tokenBank.name
const hasInterestEarned = totalInterestData.find(
(d) =>
d.symbol.toLowerCase() === symbol.toLowerCase() ||
(symbol === 'ETH (Portal)' && d.symbol === 'ETH'),
)
const interestAmount = hasInterestEarned
? hasInterestEarned.borrow_interest * -1 +
hasInterestEarned.deposit_interest
: 0
const interestValue = hasInterestEarned
? hasInterestEarned.borrow_interest_usd * -1 +
hasInterestEarned.deposit_interest_usd
: 0
const tokenBalance = bank.balance
const unsettled = spotBalances[mint.toString()]?.unsettled || 0
const inOrders = spotBalances[mint.toString()]?.inOrders || 0
const collateralValue =
initContributions.find((val) => val.asset === tokenBank.name)
?.contribution || 0
const assetWeight = tokenBank
.scaledInitAssetWeight(tokenBank.price)
.toFixed(2)
const liabWeight = tokenBank.scaledInitLiabWeight(tokenBank.price).toFixed(2)
const {
bank,
balance,
symbol,
interestAmount,
interestValue,
inOrders,
unsettled,
collateralValue,
assetWeight,
liabWeight,
depositRate,
borrowRate,
} = data
return (
<Disclosure>
@ -319,7 +395,7 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="mr-2.5">
<TokenLogo bank={tokenBank} />
<TokenLogo bank={bank} />
</div>
<div>
<p className="mb-0.5 leading-none text-th-fgd-1">{symbol}</p>
@ -329,15 +405,13 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
<div className="text-right">
<p className="font-mono text-sm text-th-fgd-2">
<FormatNumericValue
value={tokenBalance}
decimals={tokenBank.mintDecimals}
value={balance}
decimals={bank.mintDecimals}
/>
</p>
<span className="font-mono text-xs text-th-fgd-3">
<FormatNumericValue
value={
mangoAccount ? tokenBalance * tokenBank.uiPrice : 0
}
value={mangoAccount ? balance * bank.uiPrice : 0}
decimals={2}
isUsd
/>
@ -385,13 +459,13 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
<p className="text-xs text-th-fgd-3">
{t('trade:in-orders')}
</p>
<BankAmountWithValue amount={inOrders} bank={tokenBank} />
<BankAmountWithValue amount={inOrders} bank={bank} />
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('trade:unsettled')}
</p>
<BankAmountWithValue amount={unsettled} bank={tokenBank} />
<BankAmountWithValue amount={unsettled} bank={bank} />
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
@ -399,7 +473,7 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
</p>
<BankAmountWithValue
amount={interestAmount}
bank={tokenBank}
bank={bank}
value={interestValue}
/>
</div>
@ -407,24 +481,16 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => {
<p className="text-xs text-th-fgd-3">{t('rates')}</p>
<p className="space-x-2 font-mono">
<span className="text-th-up">
<FormatNumericValue
value={tokenBank.getDepositRateUi()}
decimals={2}
/>
%
<FormatNumericValue value={depositRate} decimals={2} />%
</span>
<span className="font-normal text-th-fgd-4">|</span>
<span className="text-th-down">
<FormatNumericValue
value={tokenBank.getBorrowRateUi()}
decimals={2}
/>
%
<FormatNumericValue value={borrowRate} decimals={2} />%
</span>
</p>
</div>
<div className="col-span-1">
<ActionsMenu bank={tokenBank} mangoAccount={mangoAccount} />
<ActionsMenu bank={bank} mangoAccount={mangoAccount} />
</div>
</div>
</Disclosure.Panel>

View File

@ -959,6 +959,7 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
type="error"
/>
<CreateSwitchboardOracleModal
tier={coinTier}
orcaPoolAddress={orcaPoolAddress}
raydiumPoolAddress={raydiumPoolAddress}
baseTokenName={currentTokenInfo.symbol}

View File

@ -30,6 +30,7 @@ type BaseProps = ModalProps & {
openbookMarketPk: string
baseTokenPk: string
baseTokenName: string
tier: string
}
type RaydiumProps = BaseProps & {
@ -45,21 +46,30 @@ type OrcaProps = BaseProps & {
const CreateSwitchboardOracleModal = ({
isOpen,
onClose,
openbookMarketPk,
baseTokenPk,
baseTokenName,
raydiumPoolAddress,
orcaPoolAddress,
tier,
}: RaydiumProps | OrcaProps) => {
const { t } = useTranslation(['governance'])
const connection = mangoStore((s) => s.connection)
const wallet = useWallet()
const quoteTokenName = 'USDC'
const pythUsdOracle = 'Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD'
const tierToSwapValue: { [key: string]: string } = {
PREMIUM: '10000',
MID: '2000',
MEME: '500',
SHIT: '100',
UNTRUSTED: '100',
}
const [creatingOracle, setCreatingOracle] = useState(false)
const create = useCallback(async () => {
try {
const swapValue = tierToSwapValue[tier]
setCreatingOracle(true)
const payer = wallet!.publicKey!
if (!orcaPoolAddress && !raydiumPoolAddress) {
@ -106,7 +116,7 @@ const CreateSwitchboardOracleModal = ({
attempt: [
{
valueTask: {
big: '100',
big: swapValue,
},
},
{
@ -117,7 +127,7 @@ const CreateSwitchboardOracleModal = ({
jupiterSwapTask: {
inTokenAddress: USDC_MINT,
outTokenAddress: baseTokenPk,
baseAmountString: '100',
baseAmountString: swapValue,
},
},
],
@ -134,6 +144,20 @@ const CreateSwitchboardOracleModal = ({
],
},
},
{
multiplyTask: {
job: {
tasks: [
{
oracleTask: {
pythAddress: pythUsdOracle,
pythAllowedConfidenceInterval: 0.1,
},
},
],
},
},
},
],
}),
).finish(),
@ -150,26 +174,14 @@ const CreateSwitchboardOracleModal = ({
cacheTask: {
cacheItems: [
{
variableName: 'IN_TOKEN_QTY',
variableName: 'QTY',
job: {
tasks: [
{
valueTask: {
big: '100',
},
},
{
divideTask: {
job: {
tasks: [
{
serumSwapTask: {
serumPoolAddress:
openbookMarketPk,
},
},
],
},
jupiterSwapTask: {
inTokenAddress: USDC_MINT,
outTokenAddress: baseTokenPk,
baseAmountString: swapValue,
},
},
],
@ -182,12 +194,12 @@ const CreateSwitchboardOracleModal = ({
jupiterSwapTask: {
inTokenAddress: baseTokenPk,
outTokenAddress: USDC_MINT,
baseAmountString: '${IN_TOKEN_QTY}',
baseAmountString: '${QTY}',
},
},
{
divideTask: {
big: '${IN_TOKEN_QTY}',
big: '${QTY}',
},
},
],
@ -200,6 +212,20 @@ const CreateSwitchboardOracleModal = ({
],
},
},
{
multiplyTask: {
job: {
tasks: [
{
oracleTask: {
pythAddress: pythUsdOracle,
pythAllowedConfidenceInterval: 0.1,
},
},
],
},
},
},
],
}),
).finish(),
@ -264,7 +290,6 @@ const CreateSwitchboardOracleModal = ({
baseTokenPk,
connection,
onClose,
openbookMarketPk,
orcaPoolAddress,
raydiumPoolAddress,
wallet,

View File

@ -10,7 +10,14 @@ import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import { breakpoints } from 'utils/theme'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import { LinkButton } from './Button'
import { Table, Td, Th, TrBody, TrHead } from './TableElements'
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from './TableElements'
import useSelectedMarket from 'hooks/useSelectedMarket'
import ConnectEmptyState from './ConnectEmptyState'
import { useWallet } from '@solana/wallet-adapter-react'
@ -27,6 +34,7 @@ import Tooltip from './Tooltip'
import { PublicKey } from '@solana/web3.js'
import { USDC_MINT } from 'utils/constants'
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'
import { useSortableData } from 'hooks/useSortableData'
const BalancesTable = () => {
const { t } = useTranslation(['common', 'account', 'trade'])
@ -52,44 +60,115 @@ const BalancesTable = () => {
return []
}, [banks])
const formattedTableData = useCallback(() => {
const formatted = []
for (const b of filteredBanks) {
const bank = b.bank
const balance = b.balance
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
const collateralValue =
initContributions.find((val) => val.asset === bank.name)
?.contribution || 0
const assetWeight = bank.scaledInitAssetWeight(bank.price)
const liabWeight = bank.scaledInitLiabWeight(bank.price)
const data = {
assetWeight,
balance,
bankWithBalance: b,
collateralValue,
inOrders,
liabWeight,
symbol,
unsettled,
}
formatted.push(data)
}
return formatted
}, [filteredBanks])
const {
items: tableData,
requestSort,
sortConfig,
} = useSortableData(formattedTableData())
return filteredBanks.length ? (
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="bg-th-bkg-1 text-left">{t('token')}</Th>
<Th className="bg-th-bkg-1 text-right">{t('balance')}</Th>
<Th className="text-left">
<SortableColumnHeader
sortKey="symbol"
sort={() => requestSort('symbol')}
sortConfig={sortConfig}
title={t('token')}
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="balance"
sort={() => requestSort('balance')}
sortConfig={sortConfig}
title={t('balance')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('account:tooltip-collateral-value')}>
<span className="tooltip-underline">
{t('collateral-value')}
</span>
<SortableColumnHeader
sortKey="collateralValue"
sort={() => requestSort('collateralValue')}
sortConfig={sortConfig}
title={t('collateral-value')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th className="bg-th-bkg-1 text-right">{t('trade:in-orders')}</Th>
<Th className="bg-th-bkg-1 text-right" id="trade-step-ten">
{t('trade:unsettled')}
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="inOrders"
sort={() => requestSort('inOrders')}
sortConfig={sortConfig}
title={t('trade:in-orders')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="unsettled"
sort={() => requestSort('unsettled')}
sortConfig={sortConfig}
title={t('trade:unsettled')}
/>
</div>
</Th>
</TrHead>
</thead>
<tbody>
{filteredBanks.map((b) => {
const bank = b.bank
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
const collateralValue =
initContributions.find((val) => val.asset === bank.name)
?.contribution || 0
const assetWeight = bank
.scaledInitAssetWeight(bank.price)
.toFixed(2)
const liabWeight = bank.scaledInitLiabWeight(bank.price).toFixed(2)
{tableData.map((data) => {
const {
assetWeight,
balance,
bankWithBalance,
collateralValue,
inOrders,
liabWeight,
symbol,
unsettled,
} = data
const bank = bankWithBalance.bank
return (
<TrBody key={bank.name} className="text-sm">
@ -102,10 +181,10 @@ const BalancesTable = () => {
</div>
</Td>
<Td className="text-right">
<Balance bank={b} />
<Balance bank={bankWithBalance} />
<p className="text-sm text-th-fgd-4">
<FormatNumericValue
value={mangoAccount ? b.balance * bank.uiPrice : 0}
value={mangoAccount ? balance * bank.uiPrice : 0}
isUsd
/>
</p>
@ -121,7 +200,9 @@ const BalancesTable = () => {
<p className="text-sm text-th-fgd-4">
<FormatNumericValue
value={
collateralValue <= -0.01 ? liabWeight : assetWeight
collateralValue <= -0.01
? liabWeight.toFixed(2)
: assetWeight.toFixed(2)
}
/>
x
@ -140,19 +221,18 @@ const BalancesTable = () => {
</Table>
) : (
<div className="border-b border-th-bkg-3">
{filteredBanks.map((b, i) => {
const bank = b.bank
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
const collateralValue =
initContributions.find((val) => val.asset === bank.name)
?.contribution || 0
const assetWeight = bank.scaledInitAssetWeight(bank.price).toFixed(2)
const liabWeight = bank.scaledInitLiabWeight(bank.price).toFixed(2)
{tableData.map((data, i) => {
const {
assetWeight,
balance,
bankWithBalance,
collateralValue,
inOrders,
liabWeight,
symbol,
unsettled,
} = data
const bank = bankWithBalance.bank
return (
<Disclosure key={bank.name}>
@ -172,12 +252,10 @@ const BalancesTable = () => {
</div>
<div className="flex items-center space-x-2">
<div className="text-right">
<Balance bank={b} />
<Balance bank={bankWithBalance} />
<span className="font-mono text-xs text-th-fgd-3">
<FormatNumericValue
value={
mangoAccount ? b.balance * bank.uiPrice : 0
}
value={mangoAccount ? balance * bank.uiPrice : 0}
isUsd
/>
</span>
@ -216,8 +294,8 @@ const BalancesTable = () => {
<FormatNumericValue
value={
collateralValue <= -0.01
? liabWeight
: assetWeight
? liabWeight.toFixed(2)
: assetWeight.toFixed(2)
}
/>
x

View File

@ -40,7 +40,7 @@ const Change = ({
? 'text-th-up'
: change < 0
? 'text-th-down'
: 'text-th-fgd-4'
: 'text-th-fgd-2'
}`}
>
{prefix ? prefix : ''}

View File

@ -163,7 +163,7 @@ const DetailedAreaOrBarChart: FunctionComponent<
) : filteredData.length ? (
<div className="relative">
{setDaysToShow ? (
<div className="mb-4 sm:absolute sm:-top-1 sm:right-0 sm:mb-0 sm:-mb-2 sm:flex sm:justify-end">
<div className="mb-4 sm:absolute sm:-top-1 sm:right-0 sm:mb-0 sm:flex sm:justify-end">
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}

View File

@ -28,23 +28,29 @@ const MarketChange = ({
const change = useMemo(() => {
if (!market || !marketsData) return
const isPerp = market instanceof PerpMarket
let pastPrice = 0
if (market instanceof PerpMarket) {
let dailyVolume = 0
if (isPerp) {
const perpData: MarketData = marketsData?.perpData
const perpEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
pastPrice = perpEntries ? perpEntries[1][0]?.price_24h : 0
dailyVolume = perpEntries ? perpEntries[1][0]?.quote_volume_24h : 0
} else {
const spotData: MarketData = marketsData?.spotData
const spotEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
pastPrice = spotEntries ? spotEntries[1][0]?.price_24h : 0
dailyVolume = spotEntries ? spotEntries[1][0]?.quote_volume_24h : 0
}
const currentPrice =
market instanceof PerpMarket ? market.uiPrice : currentSpotPrice
const change = ((currentPrice - pastPrice) / pastPrice) * 100
const currentPrice = isPerp ? market.uiPrice : currentSpotPrice
const change =
dailyVolume > 0 || isPerp
? ((currentPrice - pastPrice) / pastPrice) * 100
: 0
return change
}, [marketsData, currentSpotPrice])

View File

@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import dayjs from 'dayjs'
import { MouseEventHandler, ReactNode, forwardRef } from 'react'
import { LinkButton } from './Button'
import { ArrowSmallDownIcon } from '@heroicons/react/20/solid'
import { SortConfig } from 'hooks/useSortableData'
export const Table = ({
children,
@ -101,3 +104,35 @@ export const TableDateDisplay = ({
</p>
</>
)
export const SortableColumnHeader = ({
sort,
sortConfig,
sortKey,
title,
titleClass,
}: {
sort: (key: string) => void
sortConfig: SortConfig | null
sortKey: string
title: string
titleClass?: string
}) => {
return (
<LinkButton
className="flex items-center font-normal"
onClick={() => sort(sortKey)}
>
<span className={`text-th-fgd-3 ${titleClass}`}>{title}</span>
<ArrowSmallDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === sortKey
? sortConfig?.direction === 'ascending'
? 'rotate-180'
: 'rotate-360'
: null
}`}
/>
</LinkButton>
)
}

View File

@ -8,6 +8,35 @@ interface GroupedDataItem extends PerpStatsItem {
intervalStartMillis: number
}
const groupByHourlyInterval = (
data: PerpStatsItem[],
intervalDurationHours: number,
) => {
const intervalMillis = intervalDurationHours * 60 * 60 * 1000
const groupedData = []
let currentGroup: GroupedDataItem | null = null
for (let i = 0; i < data.length; i++) {
const obj = data[i]
const date = new Date(obj.date_hour)
const intervalStartMillis =
Math.floor(date.getTime() / intervalMillis) * intervalMillis
if (
!currentGroup ||
currentGroup.intervalStartMillis !== intervalStartMillis
) {
currentGroup = {
...obj,
intervalStartMillis: intervalStartMillis,
}
currentGroup.funding_rate_hourly = obj.funding_rate_hourly * 100
groupedData.push(currentGroup)
} else {
currentGroup.funding_rate_hourly += obj.funding_rate_hourly * 100
}
}
return groupedData
}
const AverageFundingChart = ({
loading,
marketStats,
@ -18,35 +47,6 @@ const AverageFundingChart = ({
const { t } = useTranslation(['common', 'stats', 'trade'])
const [daysToShow, setDaysToShow] = useState('30')
const groupByHourlyInterval = (
data: PerpStatsItem[],
intervalDurationHours: number,
) => {
const intervalMillis = intervalDurationHours * 60 * 60 * 1000
const groupedData = []
let currentGroup: GroupedDataItem | null = null
for (let i = 0; i < data.length; i++) {
const obj = data[i]
const date = new Date(obj.date_hour)
const intervalStartMillis =
Math.floor(date.getTime() / intervalMillis) * intervalMillis
if (
!currentGroup ||
currentGroup.intervalStartMillis !== intervalStartMillis
) {
currentGroup = {
...obj,
intervalStartMillis: intervalStartMillis,
}
currentGroup.funding_rate_hourly = obj.funding_rate_hourly * 100
groupedData.push(currentGroup)
} else {
currentGroup.funding_rate_hourly += obj.funding_rate_hourly * 100
}
}
return groupedData
}
const [interval, intervalString] = useMemo(() => {
if (daysToShow === '30') {
return [24, 'stats:daily']

View File

@ -1,23 +1,49 @@
import { useTranslation } from 'next-i18next'
import { useMemo } from 'react'
import { useCallback } from 'react'
import { useViewport } from '../../hooks/useViewport'
import { COLORS } from '../../styles/colors'
import { breakpoints } from '../../utils/theme'
import ContentBox from '../shared/ContentBox'
import MarketLogos from '@components/trade/MarketLogos'
import useMangoGroup from 'hooks/useMangoGroup'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from '@components/shared/TableElements'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { floorToDecimal, getDecimalCount, numberCompacter } from 'utils/numbers'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import MarketChange from '@components/shared/MarketChange'
import useThemeWrapper from 'hooks/useThemeWrapper'
import useListedMarketsWithMarketData, {
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
import { sortSpotMarkets } from 'utils/markets'
import { useSortableData } from 'hooks/useSortableData'
import Change from '@components/shared/Change'
import { Bank } from '@blockworks-foundation/mango-v4'
type TableData = {
baseBank: Bank | undefined
change: number
market: SerumMarketWithMarketData
marketName: string
price: number
priceHistory:
| {
price: number
time: string
}[]
| undefined
quoteBank: Bank | undefined
volume: number
isUp: boolean
}
const SpotMarketsTable = () => {
const { t } = useTranslation('common')
@ -28,6 +54,64 @@ const SpotMarketsTable = () => {
const { serumMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const formattedTableData = useCallback(
(markets: SerumMarketWithMarketData[]) => {
const formatted = []
for (const m of markets) {
const baseBank = group?.getFirstBankByTokenIndex(m.baseTokenIndex)
const quoteBank = group?.getFirstBankByTokenIndex(m.quoteTokenIndex)
const market = group?.getSerum3ExternalMarket(m.serumMarketExternal)
let price = 0
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
}
const pastPrice = m?.marketData?.price_24h || 0
const priceHistory = m?.marketData?.price_history
const volume = m?.marketData?.quote_volume_24h || 0
const change = volume > 0 ? ((price - pastPrice) / pastPrice) * 100 : 0
const marketName = m.name
const isUp =
price && priceHistory && priceHistory.length
? price >= priceHistory[0].price
: false
const data = {
baseBank,
change,
market: m,
marketName,
price,
priceHistory,
quoteBank,
volume,
isUp,
}
formatted.push(data)
}
return formatted
},
[group],
)
const {
items: tableData,
requestSort,
sortConfig,
} = useSortableData(
formattedTableData(
sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h'),
),
)
const loadingMarketData = isLoading || isFetching
return (
@ -36,142 +120,152 @@ const SpotMarketsTable = () => {
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('rolling-change')}</Th>
<Th className="text-left">
<SortableColumnHeader
sortKey="marketName"
sort={() => requestSort('marketName')}
sortConfig={sortConfig}
title={t('market')}
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="price"
sort={() => requestSort('price')}
sortConfig={sortConfig}
title={t('price')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="change"
sort={() => requestSort('change')}
sortConfig={sortConfig}
title={t('rolling-change')}
/>
</div>
</Th>
<Th className="hidden text-right md:block"></Th>
<Th className="text-right">{t('trade:24h-volume')}</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="volume"
sort={() => requestSort('volume')}
sortConfig={sortConfig}
title={t('trade:24h-volume')}
/>
</div>
</Th>
</TrHead>
</thead>
<tbody>
{sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map(
(mkt) => {
const baseBank = group?.getFirstBankByTokenIndex(
mkt.baseTokenIndex,
)
const quoteBank = group?.getFirstBankByTokenIndex(
mkt.quoteTokenIndex,
)
const market = group?.getSerum3ExternalMarket(
mkt.serumMarketExternal,
)
let price
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
}
{tableData.map((data) => {
const {
baseBank,
change,
market,
marketName,
price,
priceHistory,
quoteBank,
volume,
isUp,
} = data
const priceHistory = mkt?.marketData?.price_history
const volumeData = mkt?.marketData?.quote_volume_24h
const volume = volumeData ? volumeData : 0
const isUp =
price && priceHistory && priceHistory.length
? price >= priceHistory[0].price
: false
return (
<TrBody key={mkt.publicKey.toString()}>
<Td>
<div className="flex items-center">
<MarketLogos market={mkt} size="large" />
<p className="font-body">{mkt.name}</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{price ? (
<>
<FormatNumericValue
value={price}
isUsd={quoteBank?.name === 'USDC'}
/>{' '}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
) : null}
</>
) : (
''
)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<MarketChange market={mkt} />
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={priceHistory}
name={baseBank!.name + quoteBank!.name}
xKey="time"
yKey="price"
/>
</div>
) : baseBank?.name === 'USDC' ||
baseBank?.name === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">
{t('unavailable')}
</p>
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{volume ? (
<span>
{numberCompacter.format(volume)}{' '}
return (
<TrBody key={market.publicKey.toString()}>
<Td>
<div className="flex items-center">
<MarketLogos market={market} size="large" />
<p className="font-body">{marketName}</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{price ? (
<>
<FormatNumericValue
value={price}
isUsd={quoteBank?.name === 'USDC'}
/>{' '}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
) : null}
</>
) : (
''
)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<Change change={change} suffix="%" />
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
data={priceHistory}
name={baseBank!.name + quoteBank!.name}
xKey="time"
yKey="price"
/>
</div>
) : baseBank?.name === 'USDC' ||
baseBank?.name === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{volume ? (
<span>
{numberCompacter.format(volume)}{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
) : (
<span>
0{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
</span>
) : (
<span>
0{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
)}
</p>
</div>
</Td>
</TrBody>
)
},
)}
</span>
)}
</p>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-3">
{sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map(
(market) => {
return (
<MobileSpotMarketItem
key={market.publicKey.toString()}
loadingMarketData={loadingMarketData}
market={market}
/>
)
},
)}
{tableData.map((data) => {
return (
<MobileSpotMarketItem
key={data.market.publicKey.toString()}
loadingMarketData={loadingMarketData}
data={data}
/>
)
})}
</div>
)}
</ContentBox>
@ -181,37 +275,26 @@ const SpotMarketsTable = () => {
export default SpotMarketsTable
const MobileSpotMarketItem = ({
market,
data,
loadingMarketData,
}: {
market: SerumMarketWithMarketData
data: TableData
loadingMarketData: boolean
}) => {
const { t } = useTranslation('common')
const { group } = useMangoGroup()
const { theme } = useThemeWrapper()
const baseBank = group?.getFirstBankByTokenIndex(market.baseTokenIndex)
const quoteBank = group?.getFirstBankByTokenIndex(market.quoteTokenIndex)
const serumMarket = group?.getSerum3ExternalMarket(market.serumMarketExternal)
const price = useMemo(() => {
if (!baseBank || !quoteBank || !serumMarket) return 0
return floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(serumMarket.tickSize),
).toNumber()
}, [baseBank, quoteBank, serumMarket])
const priceHistory = market?.marketData?.price_history
const volueData = market?.marketData?.quote_volume_24h
const volume = volueData ? volueData : 0
const isUp =
price && priceHistory && priceHistory.length
? price >= priceHistory[0].price
: false
const {
baseBank,
change,
market,
marketName,
price,
priceHistory,
quoteBank,
volume,
isUp,
} = data
return (
<Disclosure>
@ -225,7 +308,7 @@ const MobileSpotMarketItem = ({
<div className="flex flex-shrink-0 items-center">
<MarketLogos market={market} />
</div>
<p className="leading-none text-th-fgd-1">{market.name}</p>
<p className="leading-none text-th-fgd-1">{marketName}</p>
</div>
<div className="flex items-center space-x-3">
{!loadingMarketData ? (
@ -245,7 +328,7 @@ const MobileSpotMarketItem = ({
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
<MarketChange market={market} />
<Change change={change} suffix="%" />
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'

View File

@ -8,13 +8,22 @@ import ContentBox from '../shared/ContentBox'
import Tooltip from '@components/shared/Tooltip'
import { Bank, toUiDecimals } from '@blockworks-foundation/mango-v4'
import { NextRouter, useRouter } from 'next/router'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from '@components/shared/TableElements'
import useMangoGroup from 'hooks/useMangoGroup'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import BankAmountWithValue from '@components/shared/BankAmountWithValue'
import useBanksWithBalances from 'hooks/useBanksWithBalances'
import Decimal from 'decimal.js'
import TokenLogo from '@components/shared/TokenLogo'
import { useCallback } from 'react'
import { useSortableData } from 'hooks/useSortableData'
export const goToTokenPage = (token: string, router: NextRouter) => {
const query = { ...router.query, ['token']: token }
@ -29,45 +38,137 @@ const TokenOverviewTable = () => {
const router = useRouter()
const banks = useBanksWithBalances()
return group ? (
const formattedTableData = useCallback(() => {
const formatted = []
for (const b of banks) {
const bank: Bank = b.bank
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
const availableVaultBalance = group
? group.getTokenVaultBalanceByMintUi(bank.mint) -
deposits * bank.minVaultToDepositsRatio
: 0
const available = Decimal.max(
0,
availableVaultBalance.toFixed(bank.mintDecimals),
)
const feesEarned = toUiDecimals(
bank.collectedFeesNative,
bank.mintDecimals,
)
const utilization =
bank.uiDeposits() > 0 ? (bank.uiBorrows() / bank.uiDeposits()) * 100 : 0
const depositRate = bank.getDepositRateUi()
const borrowRate = bank.getBorrowRateUi()
const symbol = bank.name
const data = {
available,
bank,
borrows,
borrowRate,
deposits,
depositRate,
feesEarned,
symbol,
utilization,
}
formatted.push(data)
}
return formatted
}, [banks, group])
const {
items: tableData,
requestSort,
sortConfig,
} = useSortableData(formattedTableData())
return (
<ContentBox hideBorder hidePadding>
{showTableView ? (
<div className="thin-scroll overflow-x-auto">
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('token')}</Th>
<Th className="text-right">{t('total-deposits')}</Th>
<Th className="text-right">{t('total-borrows')}</Th>
<Th className="text-left">
<SortableColumnHeader
sortKey="symbol"
sort={() => requestSort('symbol')}
sortConfig={sortConfig}
title={t('token')}
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="deposits"
sort={() => requestSort('deposits')}
sortConfig={sortConfig}
title={t('total-deposits')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="borrows"
sort={() => requestSort('borrows')}
sortConfig={sortConfig}
title={t('total-borrows')}
/>
</div>
</Th>
<Th className="text-right">
<div className="flex justify-end">
<Tooltip content="The amount available to borrow">
<span className="tooltip-underline">
{t('available')}
</span>
<SortableColumnHeader
sortKey="available"
sort={() => requestSort('available')}
sortConfig={sortConfig}
title={t('available')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content={t('token:fees-tooltip')}>
<span className="tooltip-underline">{t('fees')}</span>
<SortableColumnHeader
sortKey="feesEarned"
sort={() => requestSort('feesEarned')}
sortConfig={sortConfig}
title={t('fees')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content="The deposit rate (green) will automatically be paid on positive balances and the borrow rate (red) will automatically be charged on negative balances.">
<span className="tooltip-underline">{t('rates')}</span>
<SortableColumnHeader
sortKey="depositRate"
sort={() => requestSort('depositRate')}
sortConfig={sortConfig}
title={t('rates')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex justify-end">
<Tooltip content="The percentage of deposits that have been lent out">
<span className="tooltip-underline">
{t('utilization')}
</span>
<SortableColumnHeader
sortKey="utilization"
sort={() => requestSort('utilization')}
sortConfig={sortConfig}
title={t('utilization')}
titleClass="tooltip-underline"
/>
</Tooltip>
</div>
</Th>
@ -75,26 +176,23 @@ const TokenOverviewTable = () => {
</TrHead>
</thead>
<tbody>
{banks.map((b) => {
const bank: Bank = b.bank
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
const availableVaultBalance =
group.getTokenVaultBalanceByMintUi(bank.mint) -
deposits * bank.minVaultToDepositsRatio
const available = Decimal.max(
0,
availableVaultBalance.toFixed(bank.mintDecimals),
)
const feesEarned = toUiDecimals(
bank.collectedFeesNative,
bank.mintDecimals,
)
{tableData.map((data) => {
const {
available,
bank,
borrows,
borrowRate,
deposits,
depositRate,
feesEarned,
symbol,
utilization,
} = data
return (
<TrBody
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
key={bank.name}
key={symbol}
onClick={() =>
goToTokenPage(bank.name.split(' ')[0], router)
}
@ -104,7 +202,7 @@ const TokenOverviewTable = () => {
<div className="mr-2.5 flex flex-shrink-0 items-center">
<TokenLogo bank={bank} />
</div>
<p className="font-body">{bank.name}</p>
<p className="font-body">{symbol}</p>
</div>
</Td>
<Td>
@ -151,32 +249,21 @@ const TokenOverviewTable = () => {
<div className="flex justify-end space-x-1.5">
<p className="text-th-up">
<FormatNumericValue
value={bank.getDepositRateUi()}
value={depositRate}
decimals={2}
/>
%
</p>
<span className="text-th-fgd-4">|</span>
<p className="text-th-down">
<FormatNumericValue
value={bank.getBorrowRateUi()}
decimals={2}
/>
<FormatNumericValue value={borrowRate} decimals={2} />
%
</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{bank.uiDeposits() > 0
? (
(bank.uiBorrows() / bank.uiDeposits()) *
100
).toFixed(1)
: '0.0'}
%
</p>
<p>{utilization.toFixed(1)}%</p>
</div>
</Td>
<Td>
@ -192,23 +279,21 @@ const TokenOverviewTable = () => {
</div>
) : (
<div className="border-b border-th-bkg-3">
{banks.map((b, i) => {
const bank = b.bank
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
const availableVaultBalance =
group.getTokenVaultBalanceByMintUi(bank.mint) -
deposits * bank.minVaultToDepositsRatio
const available = Decimal.max(
0,
availableVaultBalance.toFixed(bank.mintDecimals),
)
const feesEarned = toUiDecimals(
bank.collectedFeesNative,
bank.mintDecimals,
)
{tableData.map((data, i) => {
const {
available,
bank,
borrows,
borrowRate,
deposits,
depositRate,
feesEarned,
symbol,
utilization,
} = data
return (
<Disclosure key={bank.name}>
<Disclosure key={symbol}>
{({ open }) => (
<>
<Disclosure.Button
@ -221,7 +306,7 @@ const TokenOverviewTable = () => {
<div className="mr-2.5 flex flex-shrink-0 items-center">
<TokenLogo bank={bank} />
</div>
<p className="text-th-fgd-1">{bank.name}</p>
<p className="text-th-fgd-1">{symbol}</p>
</div>
<ChevronDownIcon
className={`${
@ -286,7 +371,7 @@ const TokenOverviewTable = () => {
<p className="space-x-2">
<span className="font-mono text-th-up">
<FormatNumericValue
value={bank.getDepositRateUi()}
value={depositRate}
decimals={2}
/>
%
@ -296,7 +381,7 @@ const TokenOverviewTable = () => {
</span>
<span className="font-mono text-th-down">
<FormatNumericValue
value={bank.getBorrowRateUi()}
value={borrowRate}
decimals={2}
/>
%
@ -306,13 +391,7 @@ const TokenOverviewTable = () => {
<div className="col-span-1">
<p className="text-xs">{t('utilization')}</p>
<p className="font-mono text-th-fgd-1">
{bank.uiDeposits() > 0
? (
(bank.uiBorrows() / bank.uiDeposits()) *
100
).toFixed(1)
: '0.0'}
%
{utilization}%
</p>
</div>
<div className="col-span-1">
@ -337,7 +416,7 @@ const TokenOverviewTable = () => {
</div>
)}
</ContentBox>
) : null
)
}
export default TokenOverviewTable

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useMemo } from 'react'
import { useState, useCallback, useMemo, useEffect } from 'react'
import { PublicKey } from '@solana/web3.js'
import {
Cog8ToothIcon,
@ -18,7 +18,7 @@ import Loading from '../shared/Loading'
import { EnterBottomExitBottom } from '../shared/Transitions'
import useQuoteRoutes from './useQuoteRoutes'
import { HealthType } from '@blockworks-foundation/mango-v4'
import { MANGO_MINT, USDC_MINT } from '../../utils/constants'
import { MANGO_MINT, SWAP_MARGIN_KEY, USDC_MINT } from '../../utils/constants'
import { useTokenMax } from './useTokenMax'
import HealthImpact from '@components/shared/HealthImpact'
import { useWallet } from '@solana/wallet-adapter-react'
@ -34,6 +34,7 @@ import TabUnderline from '@components/shared/TabUnderline'
import MarketSwapForm from './MarketSwapForm'
import LimitSwapForm from './LimitSwapForm'
import Switch from '@components/forms/Switch'
import useLocalStorageState from 'hooks/useLocalStorageState'
const set = mangoStore.getState().set
@ -46,6 +47,10 @@ const SwapForm = () => {
const [showConfirm, setShowConfirm] = useState(false)
const [swapOrLimit, setSwapOrLimit] = useState('swap')
const { group } = useMangoGroup()
const [, setSavedSwapMargin] = useLocalStorageState<boolean>(
SWAP_MARGIN_KEY,
true,
)
const { ipAllowed, ipCountry } = useIpAddress()
const {
@ -181,6 +186,10 @@ const SwapForm = () => {
})
}
useEffect(() => {
setSavedSwapMargin(useMargin)
}, [useMargin])
const limitOrderDisabled =
!connected || !amountInFormValue || !amountOutFormValue

View File

@ -7,6 +7,7 @@ import { useEffect, useMemo, useState } from 'react'
import { TokenStatsItem } from 'types'
import { formatYAxis } from 'utils/formatting'
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import TokenRatesChart from './TokenRatesChart'
const ChartTabs = ({ bank }: { bank: Bank }) => {
const { t } = useTranslation('token')
@ -33,8 +34,6 @@ const ChartTabs = ({ bank }: { bank: Bank }) => {
return tokenStats.reduce((a: TokenStatsItem[], c: TokenStatsItem) => {
if (c.token_index === bank.tokenIndex) {
const copy = { ...c }
copy.deposit_apr = copy.deposit_apr * 100
copy.borrow_apr = copy.borrow_apr * 100
a.push(copy)
}
return a.sort(
@ -69,25 +68,18 @@ const ChartTabs = ({ bank }: { bank: Bank }) => {
loading={loadingTokenStats}
small
tickFormat={(x) => formatYAxis(x)}
title={`${bank?.name} ${t('token:deposits')}`}
title={`${t('token:deposits')}`}
xKey="date_hour"
yKey={'total_deposits'}
/>
) : (
<DetailedAreaOrBarChart
<TokenRatesChart
data={statsHistory}
dataKey="deposit_apr"
daysToShow={depositRateDaysToShow}
setDaysToShow={setDepositRateDaysToShow}
heightClass="h-64"
loaderHeightClass="h-[334px]"
loading={loadingTokenStats}
hideChange
small
suffix="%"
tickFormat={(x) => `${x.toFixed(2)}%`}
title={`${bank?.name} ${t('token:deposit-rates')} APR`}
xKey="date_hour"
yKey={'deposit_apr'}
setDaysToShow={setDepositRateDaysToShow}
title={`${t('token:average-deposit-rate')} (APR)`}
/>
)}
</div>
@ -116,25 +108,18 @@ const ChartTabs = ({ bank }: { bank: Bank }) => {
loading={loadingTokenStats}
small
tickFormat={(x) => formatYAxis(x)}
title={`${bank?.name} ${t('token:borrows')}`}
title={`${t('token:borrows')}`}
xKey="date_hour"
yKey={'total_borrows'}
/>
) : (
<DetailedAreaOrBarChart
<TokenRatesChart
data={statsHistory}
dataKey="borrow_apr"
daysToShow={borrowRateDaysToShow}
setDaysToShow={setBorrowRateDaysToShow}
heightClass="h-64"
loaderHeightClass="h-[334px]"
loading={loadingTokenStats}
small
hideChange
suffix="%"
tickFormat={(x) => `${x.toFixed(2)}%`}
title={`${bank?.name} ${t('token:borrow-rates')} APR`}
xKey="date_hour"
yKey={'borrow_apr'}
setDaysToShow={setBorrowRateDaysToShow}
title={`${t('token:average-borrow-rate')} (APR)`}
/>
)}
</div>

View File

@ -0,0 +1,112 @@
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { TokenStatsItem } from 'types'
interface GroupedDataItem extends TokenStatsItem {
intervalStartMillis: number
}
const groupByHourlyInterval = (
data: TokenStatsItem[],
dataKey: 'borrow_apr' | 'deposit_apr',
intervalDurationHours: number,
) => {
const intervalMillis = intervalDurationHours * 60 * 60 * 1000
const groupedData = []
let currentGroup: GroupedDataItem | null = null
let itemsInCurrentGroup = 0
for (let i = 0; i < data.length; i++) {
const obj = data[i]
const date = new Date(obj.date_hour)
const intervalStartMillis =
Math.floor(date.getTime() / intervalMillis) * intervalMillis
if (
!currentGroup ||
currentGroup.intervalStartMillis !== intervalStartMillis
) {
if (currentGroup) {
// calculate the average for the previous group
currentGroup[dataKey] /= itemsInCurrentGroup
groupedData.push(currentGroup)
}
currentGroup = {
...obj,
intervalStartMillis: intervalStartMillis,
}
// initialize the sum for the new group
currentGroup[dataKey] = obj[dataKey] * 100
itemsInCurrentGroup = 1
} else {
// add the value to the sum for the current group
currentGroup[dataKey] += obj[dataKey] * 100
itemsInCurrentGroup++
}
}
// calculate the average for the last group (if it exists)
if (currentGroup) {
currentGroup[dataKey] /= itemsInCurrentGroup
groupedData.push(currentGroup)
}
return groupedData
}
const TokenRatesChart = ({
data,
dataKey,
daysToShow,
loading,
setDaysToShow,
title,
}: {
data: TokenStatsItem[]
dataKey: 'deposit_apr' | 'borrow_apr'
daysToShow: string | undefined
loading: boolean
setDaysToShow: (x: string) => void
title: string
}) => {
const { t } = useTranslation('stats')
const [interval, intervalString] = useMemo(() => {
if (daysToShow === '30') {
return [24, 'stats:daily']
} else if (daysToShow === '7') {
return [6, 'stats:six-hourly']
} else {
return [1, 'stats:hourly']
}
}, [daysToShow])
const chartData = useMemo(() => {
if (!data || !data.length) return []
const groupedData = groupByHourlyInterval(data, dataKey, interval)
return groupedData
}, [data, dataKey, daysToShow, interval])
return (
<DetailedAreaOrBarChart
chartType="bar"
data={chartData}
daysToShow={daysToShow}
setDaysToShow={setDaysToShow}
heightClass="h-64"
loaderHeightClass="h-[334px]"
loading={loading}
small
hideChange
suffix="%"
tickFormat={(x) => `${x.toFixed(2)}%`}
title={`${t(intervalString)} ${title}`}
xKey="date_hour"
yKey={dataKey}
/>
)
}
export default TokenRatesChart

View File

@ -22,16 +22,19 @@ const OrderbookTooltip = () => {
const { averagePrice, cumulativeSize, cumulativeValue, side } =
orderbookTooltip
const isBid = side === 'buy'
const isBuy = side === 'sell'
const oppositeSide = side === 'buy' ? 'sell' : 'buy'
const isPerp = serumOrPerpMarket instanceof PerpMarket
return (
<div
className={`absolute max-w-[75%] w-full top-4 left-1/2 -translate-x-1/2 p-3 rounded-md bg-th-bkg-1 border text-center ${
isBid ? 'border-th-up' : 'border-th-down'
isBuy ? 'border-th-up' : 'border-th-down'
}`}
>
<p>
<span className={isBid ? 'text-th-up' : 'text-th-down'}>{t(side)}</span>
<span className={isBuy ? 'text-th-up' : 'text-th-down'}>
{t(oppositeSide)}
</span>
{` ${formatNumericValue(cumulativeSize, minOrderDecimals)} ${
isPerp ? '' : baseSymbol
} ${t('trade:for')} ${isPerp ? '$' : ''}${formatNumericValue(

View File

@ -5,6 +5,7 @@ import FormatNumericValue from '@components/shared/FormatNumericValue'
import SheenLoader from '@components/shared/SheenLoader'
import SideBadge from '@components/shared/SideBadge'
import {
SortableColumnHeader,
Table,
TableDateDisplay,
Td,
@ -29,6 +30,8 @@ import { breakpoints } from 'utils/theme'
import MarketLogos from './MarketLogos'
import PerpSideBadge from './PerpSideBadge'
import TableMarketName from './TableMarketName'
import { useSortableData } from 'hooks/useSortableData'
import { useCallback } from 'react'
const TradeHistory = () => {
const { t } = useTranslation(['common', 'trade'])
@ -44,6 +47,26 @@ const TradeHistory = () => {
const { connected } = useWallet()
const showTableView = width ? width > breakpoints.md : false
const formattedTableData = useCallback(() => {
const formatted = []
for (const trade of combinedTradeHistory) {
const marketName = trade.market.name
const value = trade.price * trade.size
const sortTime = trade?.time
? trade.time
: dayjs().format('YYYY-MM-DDTHH:mm:ss')
const data = { ...trade, marketName, value, sortTime }
formatted.push(data)
}
return formatted
}, [combinedTradeHistory])
const {
items: tableData,
requestSort,
sortConfig,
} = useSortableData(formattedTableData())
if (!selectedMarket || !group) return null
return mangoAccountAddress &&
@ -54,18 +77,71 @@ const TradeHistory = () => {
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('value')}</Th>
<Th className="text-right">{t('fee')}</Th>
<Th className="text-right">{t('date')}</Th>
<Th className="text-left">
<SortableColumnHeader
sortKey="marketName"
sort={() => requestSort('marketName')}
sortConfig={sortConfig}
title={t('market')}
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="size"
sort={() => requestSort('size')}
sortConfig={sortConfig}
title={t('trade:size')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="price"
sort={() => requestSort('price')}
sortConfig={sortConfig}
title={t('price')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="value"
sort={() => requestSort('value')}
sortConfig={sortConfig}
title={t('value')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="feeCost"
sort={() => requestSort('feeCost')}
sortConfig={sortConfig}
title={t('fee')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="sortTime"
sort={() => requestSort('sortTime')}
sortConfig={sortConfig}
title={t('date')}
/>
</div>
</Th>
<Th />
</TrHead>
</thead>
<tbody>
{combinedTradeHistory.map((trade, index: number) => {
const { side, price, market, size, feeCost, liquidity } = trade
{tableData.map((trade, index: number) => {
const { side, price, market, size, feeCost, liquidity, value } =
trade
return (
<TrBody
key={`${side}${size}${price}${index}`}
@ -88,11 +164,7 @@ const TradeHistory = () => {
<FormatNumericValue value={price} />
</Td>
<Td className="text-right font-mono">
<FormatNumericValue
value={price * size}
decimals={2}
isUsd
/>
<FormatNumericValue value={value} decimals={2} isUsd />
</Td>
<Td className="text-right">
<span className="font-mono">

55
hooks/useSortableData.ts Normal file
View File

@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMemo, useState } from 'react'
type Direction = 'ascending' | 'descending'
export interface SortConfig {
key: string
direction: Direction
}
export function useSortableData<T extends Record<string, any>>(
items: T[],
config: SortConfig | null = null,
): {
items: T[]
requestSort: (key: string) => void
sortConfig: SortConfig | null
} {
const [sortConfig, setSortConfig] = useState<SortConfig | null>(config)
const sortedItems = useMemo(() => {
const sortableItems = items ? [...items] : []
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
if (!isNaN(a[sortConfig.key])) {
return sortConfig.direction === 'ascending'
? a[sortConfig.key] - b[sortConfig.key]
: b[sortConfig.key] - a[sortConfig.key]
}
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1
}
return 0
})
}
return sortableItems
}, [items, sortConfig])
const requestSort = (key: string) => {
let direction: Direction = 'ascending'
if (
sortConfig &&
sortConfig.key === key &&
sortConfig.direction === 'ascending'
) {
direction = 'descending'
}
setSortConfig({ key, direction })
}
return { items: sortedItems, requestSort, sortConfig }
}

View File

@ -22,7 +22,7 @@
},
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-v4": "^0.17.27",
"@blockworks-foundation/mango-v4": "^0.18.3",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
"@metaplex-foundation/js": "0.19.4",
@ -36,7 +36,7 @@
"@solana/wallet-adapter-react": "0.15.32",
"@solana/wallet-adapter-wallets": "0.19.18",
"@solflare-wallet/pfp": "0.0.6",
"@switchboard-xyz/solana.js": "2.2.0",
"@switchboard-xyz/solana.js": "2.4.7",
"@tanstack/react-query": "4.10.1",
"@tippyjs/react": "4.2.6",
"@types/howler": "2.2.7",
@ -71,14 +71,14 @@
"react-i18next": "13.0.2",
"react-nice-dates": "3.1.0",
"react-number-format": "4.9.2",
"react-responsive-pagination": "^2.1.0",
"react-tsparticles": "2.2.4",
"react-window": "1.8.7",
"recharts": "2.5.0",
"tsparticles": "2.2.4",
"walktour": "5.1.1",
"webpack-node-externals": "3.0.0",
"zustand": "4.1.3",
"react-responsive-pagination": "^2.1.0"
"zustand": "4.1.3"
},
"peerDependencies": {
"@project-serum/anchor": "0.25.0",

View File

@ -6,6 +6,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, [
'account',
'common',
'notifications',
'onboarding',

View File

@ -15,6 +15,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, [
'account',
'common',
'notifications',
'onboarding',

View File

@ -19,6 +19,7 @@
"no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",

View File

@ -1,6 +1,8 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"average-borrow-rate": "Average Borrow Rate",
"average-deposit-rate": "Average Deposit Rate",
"borrowing": "Borrowing",
"borrows": "Borrows",
"borrow-rates": "Borrow Rates",

View File

@ -6,6 +6,8 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
@ -17,6 +19,7 @@
"no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
@ -29,5 +32,6 @@
"total-funding-earned": "Total Funding Earned",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral"
"zero-collateral": "Zero Collateral",
"zero-balances": "Show Zero Balances"
}

View File

@ -1,6 +1,8 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"average-borrow-rate": "Average Borrow Rate",
"average-deposit-rate": "Average Deposit Rate",
"borrowing": "Borrowing",
"borrows": "Borrows",
"borrow-rates": "Borrow Rates",

View File

@ -6,6 +6,8 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
@ -17,6 +19,7 @@
"no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
@ -29,5 +32,6 @@
"total-funding-earned": "Total Funding Earned",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral"
"zero-collateral": "Zero Collateral",
"zero-balances": "Show Zero Balances"
}

View File

@ -1,6 +1,8 @@
{
"all-time-high": "All-time High",
"all-time-low": "All-time Low",
"average-borrow-rate": "Average Borrow Rate",
"average-deposit-rate": "Average Deposit Rate",
"borrowing": "Borrowing",
"borrows": "Borrows",
"borrow-rates": "Borrow Rates",

View File

@ -6,6 +6,8 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
@ -17,6 +19,7 @@
"no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
@ -29,5 +32,6 @@
"total-funding-earned": "Total Funding Earned",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral"
"zero-collateral": "Zero Collateral",
"zero-balances": "Show Zero Balances"
}

View File

@ -1,6 +1,8 @@
{
"all-time-high": "历史高价",
"all-time-low": "历史低价",
"average-borrow-rate": "Average Borrow Rate",
"average-deposit-rate": "Average Deposit Rate",
"borrow-rates": "借贷利率",
"borrow-upkeep-rate": "维持借贷的费用",
"borrowing": "借入",

View File

@ -6,6 +6,8 @@
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
"health-contributions": "Health Contributions",
"init-health-contribution": "Init Health Contribution",
"init-health-contributions": "Init Health Contributions",
@ -17,6 +19,7 @@
"no-pnl-history": "No PnL History",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
@ -29,5 +32,6 @@
"total-funding-earned": "Total Funding Earned",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral"
"zero-collateral": "Zero Collateral",
"zero-balances": "Show Zero Balances"
}

View File

@ -1,6 +1,8 @@
{
"all-time-high": "歷史高價",
"all-time-low": "歷史低價",
"average-borrow-rate": "Average Borrow Rate",
"average-deposit-rate": "Average Deposit Rate",
"borrow-rates": "借貸利率",
"borrow-upkeep-rate": "維持借貸的費用",
"borrowing": "借入",

View File

@ -40,6 +40,7 @@ import {
PAGINATION_PAGE_LENGTH,
PRIORITY_FEE_KEY,
RPC_PROVIDER_KEY,
SWAP_MARGIN_KEY,
} from '../utils/constants'
import {
ActivityFeed,
@ -270,12 +271,17 @@ export type MangoStore = {
const mangoStore = create<MangoStore>()(
subscribeWithSelector((_set, get) => {
let rpcUrl = ENDPOINT.url
let swapMargin = true
if (typeof window !== 'undefined' && CLUSTER === 'mainnet-beta') {
const urlFromLocalStorage = localStorage.getItem(RPC_PROVIDER_KEY)
const swapMarginFromLocalStorage = localStorage.getItem(SWAP_MARGIN_KEY)
rpcUrl = urlFromLocalStorage
? JSON.parse(urlFromLocalStorage)
: ENDPOINT.url
swapMargin = swapMarginFromLocalStorage
? JSON.parse(swapMarginFromLocalStorage)
: true
}
let connection: Connection
@ -370,7 +376,7 @@ const mangoStore = create<MangoStore>()(
outputBank: undefined,
inputTokenInfo: undefined,
outputTokenInfo: undefined,
margin: true,
margin: swapMargin,
slippage: 0.5,
swapMode: 'ExactIn',
amountIn: '',

View File

@ -53,6 +53,8 @@ export const PRIORITY_FEE_KEY = 'priorityFeeKey-0.1'
export const SHOW_ORDER_LINES_KEY = 'showOrderLines-0.1'
export const SWAP_MARGIN_KEY = 'swapMargin-0.1'
export const SHOW_SWAP_INTRO_MODAL = 'showSwapModal-0.1'
export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1'

View File

@ -12,7 +12,7 @@
resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08"
integrity sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA==
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.5":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438"
integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==
@ -26,10 +26,10 @@
dependencies:
ws "^8.13.0"
"@blockworks-foundation/mango-v4@^0.17.27":
version "0.17.27"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.17.27.tgz#70998e21e8fa2ae340fec0c36a7f8c07400c6fd4"
integrity sha512-oXvA08MqZ3dvd1ODHwx/XjMGyFvdC+6MTfzNL3vhylJA2JLlck48Fscl0mHL1h9kOdFIHJJPrnCkIW5z5g9pIQ==
"@blockworks-foundation/mango-v4@^0.18.3":
version "0.18.3"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.3.tgz#a3d41cfb85f9b7121469d2ac3d52a7915f677701"
integrity sha512-j45GXiLPncKaSnBPLg0JLYEW2kSRRCWklE0usmhPlPD99KcX/iKqwFEzWtcOuGC5sQJTn4cblTOj5V6iiUiGAw==
dependencies:
"@coral-xyz/anchor" "^0.27.0"
"@project-serum/serum" "0.13.65"
@ -89,7 +89,7 @@
eventemitter3 "^4.0.7"
uuid "^8.3.2"
"@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.27.0":
"@coral-xyz/anchor@^0.26.0", "@coral-xyz/anchor@^0.27.0", "@coral-xyz/anchor@^0.28.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c"
integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g==
@ -118,6 +118,14 @@
bn.js "^5.1.2"
buffer-layout "^1.2.0"
"@coral-xyz/borsh@^0.28.0":
version "0.28.0"
resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.28.0.tgz#fa368a2f2475bbf6f828f4657f40a52102e02b6d"
integrity sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ==
dependencies:
bn.js "^5.1.2"
buffer-layout "^1.2.0"
"@eslint/eslintrc@^1.2.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
@ -1449,7 +1457,7 @@
bs58 "^4.0.1"
superstruct "^0.15.2"
"@solana/spl-token@0.3.7", "@solana/spl-token@^0.3.5", "@solana/spl-token@^0.3.6":
"@solana/spl-token@0.3.7":
version "0.3.7"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.7.tgz#6f027f9ad8e841f792c32e50920d9d2e714fc8da"
integrity sha512-bKGxWTtIw6VDdCBngjtsGlKGLSmiu/8ghSt/IOYJV24BsymRbgq7r12GToeetpxmPaZYLddKwAz7+EwprLfkfg==
@ -1470,6 +1478,15 @@
buffer-layout "^1.2.0"
dotenv "10.0.0"
"@solana/spl-token@^0.3.5", "@solana/spl-token@^0.3.6", "@solana/spl-token@^0.3.8":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.8.tgz#8e9515ea876e40a4cc1040af865f61fc51d27edf"
integrity sha512-ogwGDcunP9Lkj+9CODOWMiVJEdRtqHAtX2rWF62KxnnSWtMZtV9rDhTrZFshiyJmxDnRL/1nKE1yJHg4jjs3gg==
dependencies:
"@solana/buffer-layout" "^4.0.0"
"@solana/buffer-layout-utils" "^0.2.0"
buffer "^6.0.3"
"@solana/wallet-adapter-alpha@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-alpha/-/wallet-adapter-alpha-0.1.9.tgz#863ae3f7108046c9e022c80023bb1b0877a6dec5"
@ -1922,12 +1939,12 @@
"@wallet-standard/app" "^1.0.1"
"@wallet-standard/base" "^1.0.1"
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.44.3", "@solana/web3.js@^1.50.1", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.63.1", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.73.0", "@solana/web3.js@^1.73.2":
version "1.77.3"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.77.3.tgz#2cbeaa1dd24f8fa386ac924115be82354dfbebab"
integrity sha512-PHaO0BdoiQRPpieC1p31wJsBaxwIOWLh8j2ocXNKX8boCQVldt26Jqm2tZE4KlrvnCIV78owPLv1pEUgqhxZ3w==
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.44.3", "@solana/web3.js@^1.50.1", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.63.1", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.73.2", "@solana/web3.js@^1.78.0":
version "1.78.1"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.78.1.tgz#1b1023f81aa98f53ef45eaec642be11c0a0877f3"
integrity sha512-r0WZAYwCfVElfONP/dmWkEfw6wufL+u7lWojEsNecn9PyIIYq+r4eb0h2MRiJ3xkctvTN76G0T6FTGcTJhXh3Q==
dependencies:
"@babel/runtime" "^7.12.5"
"@babel/runtime" "^7.22.6"
"@noble/curves" "^1.0.0"
"@noble/hashes" "^1.3.0"
"@solana/buffer-layout" "^4.0.0"
@ -1939,7 +1956,7 @@
buffer "6.0.3"
fast-stable-stringify "^1.0.0"
jayson "^4.1.0"
node-fetch "^2.6.7"
node-fetch "^2.6.12"
rpc-websockets "^7.5.1"
superstruct "^0.14.2"
@ -2122,18 +2139,19 @@
dependencies:
tslib "^2.4.0"
"@switchboard-xyz/common@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@switchboard-xyz/common/-/common-2.2.3.tgz#f4d39cea8cea9354ad369f749462fa37152c4ec9"
integrity sha512-E4NQf9aXdOiul+sySAbFPAW9k0qz4wRTfqrU7cEa8nRIvUkg6VIZ+5JfajHv/VfK9UOD+6ZfMBxq2+dHkiz9zw==
"@switchboard-xyz/common@^2.2.8":
version "2.2.9"
resolved "https://registry.yarnpkg.com/@switchboard-xyz/common/-/common-2.2.9.tgz#a9e71bb63eb55c8d47295ed10ba35921beb99079"
integrity sha512-ZhGLT8YobM3A3GKv8UMXQHUgW3bthyk+fsqEDWhO0Ej1xcOjGOS3c++K1/9Z8/xl1xekgQ5kj2CKC8AWxr+U+w==
dependencies:
"@solana/web3.js" "^1.66.2"
"@coral-xyz/borsh" "^0.28.0"
"@types/big.js" "^6.1.6"
"@types/bn.js" "^5.1.1"
big.js "^6.2.1"
bn.js "^5.2.1"
bs58 "^5.0.0"
decimal.js "^10.4.3"
lodash "^4.17.21"
protobufjs "^7.2.3"
yaml "^2.2.1"
@ -2145,17 +2163,18 @@
"@project-serum/anchor" "^0.24.2"
big.js "^6.1.1"
"@switchboard-xyz/solana.js@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@switchboard-xyz/solana.js/-/solana.js-2.2.0.tgz#5108cfbbf0ca6e48297ae8c6e8c11f39e04ac32a"
integrity sha512-UzAyKDY1wq1UO50PsKc/6huF6xYX/3B5kA0lmEnZMb+5L6M3YtDckbxk6mD4kG7J0curvvX6Alu9cO6uGqnI3A==
"@switchboard-xyz/solana.js@2.4.7":
version "2.4.7"
resolved "https://registry.yarnpkg.com/@switchboard-xyz/solana.js/-/solana.js-2.4.7.tgz#48013db5a4166a57dbfbf45adbd7a4561a841eb8"
integrity sha512-eISKIkM29HRfs3hS+6Y1r7+gl3KsuHxT9EvwNvU921QL5RUzPnZtRinUQrNm0z4BKGJTzs4HZOUzNfV1acseKw==
dependencies:
"@coral-xyz/anchor" "^0.27.0"
"@coral-xyz/borsh" "^0.27.0"
"@solana/spl-token" "^0.3.6"
"@solana/web3.js" "^1.73.0"
"@switchboard-xyz/common" "^2.2.3"
dotenv "^16.0.3"
"@coral-xyz/anchor" "^0.28.0"
"@coral-xyz/borsh" "^0.28.0"
"@solana/spl-token" "^0.3.8"
"@solana/web3.js" "^1.78.0"
"@switchboard-xyz/common" "^2.2.8"
cron-validator "^1.3.1"
dotenv "^16.3.1"
lodash "^4.17.21"
"@tanstack/query-core@4.10.1":
@ -3992,6 +4011,11 @@ create-hmac@1.1.7, create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cron-validator@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.3.1.tgz#8f2fe430f92140df77f91178ae31fc1e3a48a20e"
integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==
cross-fetch@^3.1.4, cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
@ -4400,10 +4424,10 @@ dotenv@10.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@^16.0.3:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
dotenv@^16.0.3, dotenv@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
drbg.js@^1.0.1:
version "1.0.1"
@ -6555,10 +6579,10 @@ node-fetch@2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.9"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7:
version "2.6.12"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==
dependencies:
whatwg-url "^5.0.0"