Merge branch 'main' into trade-success-animation

This commit is contained in:
tylersssss 2023-01-11 13:11:03 -05:00 committed by GitHub
commit c3f107820e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 3210 additions and 2707 deletions

View File

@ -1,22 +1,52 @@
import { makeApiRequest, parseResolution } from './helpers'
import { subscribeOnStream, unsubscribeFromStream } from './streaming'
import mangoStore from '@store/mangoStore'
import {
DatafeedConfiguration,
LibrarySymbolInfo,
ResolutionString,
SearchSymbolResultItem,
} from '@public/charting_library/charting_library'
export const SUPPORTED_RESOLUTIONS = [
'1',
'3',
'5',
'15',
'30',
'60',
'120',
'240',
'1D',
'1W',
] as const
type BaseBar = {
low: number
high: number
open: number
close: number
}
type KlineBar = BaseBar & {
volume: number
timestamp: number
}
type TradingViewBar = BaseBar & {
time: number
}
type Bar = KlineBar & TradingViewBar
type SymbolInfo = LibrarySymbolInfo & {
address: string
}
const lastBarsCache = new Map()
const configurationData = {
supported_resolutions: [
'1',
'3',
'5',
'15',
'30',
'60',
'120',
'240',
'1D',
'1W',
],
supported_resolutions: SUPPORTED_RESOLUTIONS,
intraday_multipliers: ['1', '3', '5', '15', '30', '60', '120', '240'],
exchanges: [],
}
@ -29,42 +59,101 @@ const configurationData = {
// return data.data.tokens
// }
export const queryBars = async (
tokenAddress: string,
resolution: typeof SUPPORTED_RESOLUTIONS[number],
periodParams: {
firstDataRequest: boolean
from: number
to: number
}
): Promise<Bar[]> => {
const { from, to } = periodParams
const urlParameters = {
address: tokenAddress,
type: parseResolution(resolution),
time_from: from,
time_to: to,
}
const query = Object.keys(urlParameters)
.map(
(name: string) =>
`${name}=${encodeURIComponent((urlParameters as any)[name])}`
)
.join('&')
const data = await makeApiRequest(`defi/ohlcv/pair?${query}`)
if (!data.success || data.data.items.length === 0) {
return []
}
let bars: Bar[] = []
for (const bar of data.data.items) {
if (bar.unixTime >= from && bar.unixTime < to) {
const timestamp = bar.unixTime * 1000
bars = [
...bars,
{
time: timestamp,
low: bar.l,
high: bar.h,
open: bar.o,
close: bar.c,
volume: bar.v,
timestamp,
},
]
}
}
return bars
}
export default {
onReady: (callback: any) => {
console.log('[onReady]: Method call')
setTimeout(() => callback(configurationData))
onReady: (callback: (configuration: DatafeedConfiguration) => void) => {
// console.log('[onReady]: Method call')
setTimeout(() => callback(configurationData as any))
},
searchSymbols: async (
_userInput: any,
_exchange: any,
_symbolType: any,
_onResultReadyCallback: any
_userInput: string,
_exchange: string,
_symbolType: string,
_onResultReadyCallback: (items: SearchSymbolResultItem[]) => void
) => {
return
},
resolveSymbol: async (
symbolAddress: any,
onSymbolResolvedCallback: any,
_onResolveErrorCallback: any,
_extension: any
symbolAddress: string,
onSymbolResolvedCallback: (symbolInfo: SymbolInfo) => void
// _onResolveErrorCallback: any,
// _extension: any
) => {
console.log('[resolveSymbol]: Method call', symbolAddress)
// console.log(
// '[resolveSymbol]: Method call',
// symbolAddress,
// onSymbolResolvedCallback
// )
// const symbols = await getAllSymbols()
// let symbolItem = symbols.find((item: any) => item.address === symbolAddress)
// console.log('========symbols:', symbolItem, symbols)
let symbolItem: any
let symbolItem:
| {
address: string
type: string
symbol: string
}
| undefined
if (!symbolItem) {
symbolItem = {
address: symbolAddress,
type: 'pair',
symbol: '',
}
}
const ticker = mangoStore.getState().selectedMarket.name
const symbolInfo = {
const symbolInfo: SymbolInfo = {
address: symbolItem.address,
ticker: symbolItem.address,
name: symbolItem.symbol || symbolItem.address,
@ -75,89 +164,78 @@ export default {
minmov: 1,
pricescale: 100,
has_intraday: true,
has_no_volume: true,
has_weekly_and_monthly: false,
supported_resolutions: configurationData.supported_resolutions,
supported_resolutions: configurationData.supported_resolutions as any,
intraday_multipliers: configurationData.intraday_multipliers,
volume_precision: 2,
data_status: 'streaming',
full_name: '',
exchange: '',
listed_exchange: '',
format: 'price',
}
console.log('[resolveSymbol]: Symbol resolved', symbolAddress)
// console.log('[resolveSymbol]: Symbol resolved', symbolAddress)
onSymbolResolvedCallback(symbolInfo)
},
getBars: async (
symbolInfo: any,
resolution: any,
periodParams: any,
onHistoryCallback: any,
onErrorCallback: any
symbolInfo: SymbolInfo,
resolution: ResolutionString,
periodParams: {
countBack: number
firstDataRequest: boolean
from: number
to: number
},
onHistoryCallback: (
bars: Bar[],
t: {
noData: boolean
}
) => void,
onErrorCallback: (e: any) => void
) => {
const { from, to, firstDataRequest } = periodParams
console.log('[getBars]: Method call', symbolInfo, resolution, from, to)
const urlParameters = {
address: symbolInfo.address,
type: parseResolution(resolution),
time_from: from,
time_to: to,
}
const query = Object.keys(urlParameters)
.map(
(name: any) =>
`${name}=${encodeURIComponent((urlParameters as any)[name])}`
)
.join('&')
try {
const data = await makeApiRequest(`defi/ohlcv/pair?${query}`)
if (!data.success || data.data.items.length === 0) {
const { firstDataRequest } = periodParams
const bars = await queryBars(
symbolInfo.address,
resolution as any,
periodParams
)
if (!bars || bars.length === 0) {
// "noData" should be set if there is no data in the requested period.
onHistoryCallback([], {
noData: true,
})
return
}
let bars: any = []
data.data.items.forEach((bar: any) => {
if (bar.unixTime >= from && bar.unixTime < to) {
bars = [
...bars,
{
time: bar.unixTime * 1000,
low: bar.l,
high: bar.h,
open: bar.o,
close: bar.c,
},
]
}
})
if (firstDataRequest) {
lastBarsCache.set(symbolInfo.address, {
...bars[bars.length - 1],
})
}
console.log(`[getBars]: returned ${bars.length} bar(s)`)
// console.log(`[getBars]: returned ${bars.length} bar(s)`)
onHistoryCallback(bars, {
noData: false,
})
} catch (error) {
console.log('[getBars]: Get error', error)
console.warn('[getBars]: Get error', error)
onErrorCallback(error)
}
},
subscribeBars: (
symbolInfo: any,
resolution: any,
onRealtimeCallback: any,
subscriberUID: any,
onResetCacheNeededCallback: any
symbolInfo: SymbolInfo,
resolution: string,
onRealtimeCallback: (data: any) => void,
subscriberUID: string,
onResetCacheNeededCallback: () => void
) => {
console.log(
'[subscribeBars]: Method call with subscriberUID:',
subscriberUID
)
// console.log(
// '[subscribeBars]: Method call with subscriberUID:',
// subscriberUID
// )
subscribeOnStream(
symbolInfo,
resolution,
@ -169,7 +247,7 @@ export default {
},
unsubscribeBars: () => {
console.log('[unsubscribeBars]')
console.warn('[unsubscribeBars]')
unsubscribeFromStream()
},
}

View File

@ -38,7 +38,7 @@ socket.addEventListener('message', (msg) => {
close: data.data.c,
volume: data.data.v,
}
console.log('[socket] Generate new bar')
// console.log('[socket] Generate new bar')
} else {
bar = {
...lastBar,
@ -47,7 +47,7 @@ socket.addEventListener('message', (msg) => {
close: data.data.c,
volume: data.data.v,
}
console.log('[socket] Update the latest bar by price')
// console.log('[socket] Update the latest bar by price')
}
subscriptionItem.lastBar = bar

View File

@ -21,7 +21,11 @@ import {
INPUT_TOKEN_DEFAULT,
} from './../utils/constants'
import { notify } from './../utils/notifications'
import { floorToDecimal, formatFixedDecimals } from './../utils/numbers'
import {
floorToDecimal,
formatDecimal,
formatFixedDecimals,
} from './../utils/numbers'
import ActionTokenList from './account/ActionTokenList'
import ButtonGroup from './forms/ButtonGroup'
import Label from './forms/Label'
@ -39,6 +43,8 @@ import useJupiterMints from 'hooks/useJupiterMints'
import useMangoGroup from 'hooks/useMangoGroup'
import TokenVaultWarnings from '@components/shared/TokenVaultWarnings'
import { useWallet } from '@solana/wallet-adapter-react'
import { useEnhancedWallet } from './wallet/EnhancedWalletProvider'
import AmountWithValue from './shared/AmountWithValue'
interface BorrowFormProps {
onSuccess: () => void
@ -58,6 +64,7 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
const { mangoTokens } = useJupiterMints()
const { mangoAccount } = useMangoAccount()
const { connected } = useWallet()
const { handleConnect } = useEnhancedWallet()
const bank = useMemo(() => {
const group = mangoStore.getState().group
@ -85,10 +92,11 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
const tokenBalance = useMemo(() => {
if (!bank || !mangoAccount) return new Decimal(0)
return floorToDecimal(
const balance = floorToDecimal(
mangoAccount.getTokenBalanceUi(bank),
bank.mintDecimals
)
return balance.gt(0) ? balance : new Decimal(0)
}, [bank, mangoAccount])
const isBorrow = parseFloat(inputAmount) > tokenBalance.toNumber()
@ -104,16 +112,19 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
)
const setMax = useCallback(() => {
setInputAmount(tokenMax.toFixed())
if (!bank) return
setInputAmount(
floorToDecimal(Number(tokenMax), bank.mintDecimals).toFixed()
)
handleSizePercentage('100')
}, [tokenMax, handleSizePercentage])
}, [bank, tokenMax, handleSizePercentage])
const handleSelectToken = (token: string) => {
setSelectedToken(token)
setShowTokenList(false)
}
const handleWithdraw = async () => {
const handleBorrow = async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
@ -203,14 +214,14 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
<ArrowLeftIcon className={`h-6 w-6`} />
</button>
<h2 className="mb-4 text-center text-lg">{t('select-borrow-token')}</h2>
<div className="grid auto-cols-fr grid-flow-col px-4 pb-2">
<div className="">
<div className="flex items-center px-4 pb-2">
<div className="w-1/4">
<p className="text-xs">{t('token')}</p>
</div>
<div className="text-right">
<div className="w-1/4 text-right">
<p className="text-xs">{t('borrow-rate')}</p>
</div>
<div className="text-right">
<div className="w-1/2 text-right">
<p className="whitespace-nowrap text-xs">{t('max-borrow')}</p>
</div>
</div>
@ -236,16 +247,21 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
/>
</div>
) : null}
{bank ? <TokenVaultWarnings bank={bank} /> : null}
{bank ? <TokenVaultWarnings bank={bank} type="borrow" /> : null}
<div className="grid grid-cols-2">
<div className="col-span-2 flex justify-between">
<Label text={`${t('borrow')} ${t('token')}`} />
<MaxAmountButton
className="mb-2"
label={t('max')}
onClick={setMax}
value={tokenMax.toFixed()}
/>
{bank ? (
<MaxAmountButton
className="mb-2"
label={t('max')}
onClick={setMax}
value={floorToDecimal(
Number(tokenMax),
bank.mintDecimals
).toFixed()}
/>
) : null}
</div>
<div className="col-span-1 rounded-lg rounded-r-none border border-r-0 border-th-input-border bg-th-input-bkg">
<button
@ -277,7 +293,7 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
allowNegative={false}
isNumericString={true}
decimalScale={bank?.mintDecimals || 6}
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl tracking-wider text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
placeholder="0.00"
value={inputAmount}
onValueChange={handleInputChange}
@ -306,70 +322,56 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
</div> */}
</div>
{bank ? (
<div className="my-6 space-y-2 border-y border-th-bkg-3 px-2 py-4">
<div className="my-6 space-y-1.5 border-y border-th-bkg-3 px-2 py-4">
<HealthImpactTokenChange
mintPk={bank.mint}
uiAmount={Number(inputAmount)}
/>
{tokenBalance ? (
<div className="flex justify-between">
<p>{t('withdraw-amount')}</p>
<p className="font-mono text-th-fgd-2">
{isBorrow ? (
<>
{tokenBalance.toNumber()}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
bank.uiPrice * tokenBalance.toNumber(),
true
)}
</span>
</>
) : inputAmount ? (
<>
{inputAmount}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
bank.uiPrice * parseFloat(inputAmount),
true
)}
)
</span>
</>
) : (
<>
0{' '}
<span className="text-xs text-th-fgd-3">($0.00)</span>
</>
<div className="flex justify-between">
<p>{t('withdraw-amount')}</p>
{isBorrow ? (
<AmountWithValue
amount={formatDecimal(
Number(tokenBalance),
bank.mintDecimals
)}
</p>
</div>
) : null}
value={formatFixedDecimals(
bank.uiPrice * tokenBalance.toNumber(),
true
)}
/>
) : inputAmount ? (
<AmountWithValue
amount={formatDecimal(
Number(inputAmount),
bank.mintDecimals
)}
value={formatFixedDecimals(
bank.uiPrice * parseFloat(inputAmount),
true
)}
/>
) : (
<AmountWithValue amount="0" value="$0.00" />
)}
</div>
<div className="flex justify-between">
<p>{t('borrow-amount')}</p>
<p className="font-mono text-th-fgd-2">
{isBorrow ? (
<>
{parseFloat(inputAmount) - tokenBalance.toNumber()}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
bank.uiPrice *
(parseFloat(inputAmount) -
tokenBalance.toNumber()),
true
)}
)
</span>
</>
) : (
<>
0 <span className="text-xs text-th-fgd-3">($0.00)</span>
</>
)}
</p>
{isBorrow ? (
<AmountWithValue
amount={formatDecimal(
Number(inputAmount) - Number(tokenBalance),
bank.mintDecimals
)}
value={formatFixedDecimals(
bank.uiPrice *
(parseFloat(inputAmount) - tokenBalance.toNumber()),
true
)}
/>
) : (
<AmountWithValue amount="0" value="$0.00" />
)}
</div>
<div className="flex justify-between">
<Tooltip content={t('loan-origination-fee-tooltip')}>
@ -378,23 +380,29 @@ function BorrowForm({ onSuccess, token }: BorrowFormProps) {
</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">
{isBorrow
? formatFixedDecimals(
bank.uiPrice *
bank.loanOriginationFeeRate.toNumber() *
{isBorrow ? (
<>
{formatDecimal(
bank.loanOriginationFeeRate.toNumber() *
(parseFloat(inputAmount) - tokenBalance.toNumber()),
true
)
: '$0.00'}
bank.mintDecimals
)}{' '}
<span className="font-body text-th-fgd-4">
{bank.name}
</span>
</>
) : (
'N/A'
)}
</p>
</div>
</div>
) : null}
</div>
<Button
onClick={handleWithdraw}
onClick={connected ? handleBorrow : handleConnect}
className="flex w-full items-center justify-center"
disabled={!inputAmount || showInsufficientBalance || !connected}
disabled={connected && (!inputAmount || showInsufficientBalance)}
size="large"
>
{!connected ? (

View File

@ -1,4 +1,4 @@
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { Bank, toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import {
ArrowDownTrayIcon,
ArrowLeftIcon,
@ -20,7 +20,11 @@ import {
INPUT_TOKEN_DEFAULT,
} from './../utils/constants'
import { notify } from './../utils/notifications'
import { floorToDecimal, formatFixedDecimals } from './../utils/numbers'
import {
floorToDecimal,
formatDecimal,
formatFixedDecimals,
} from './../utils/numbers'
import { TokenAccount } from './../utils/tokens'
import ActionTokenList from './account/ActionTokenList'
import ButtonGroup from './forms/ButtonGroup'
@ -36,6 +40,9 @@ import HealthImpactTokenChange from '@components/HealthImpactTokenChange'
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
import useJupiterMints from 'hooks/useJupiterMints'
import useMangoGroup from 'hooks/useMangoGroup'
import { useEnhancedWallet } from './wallet/EnhancedWalletProvider'
import useSolBalance from 'hooks/useSolBalance'
import AmountWithValue from './shared/AmountWithValue'
interface DepositFormProps {
onSuccess: () => void
@ -63,6 +70,27 @@ export const walletBalanceForToken = (
}
}
export const useAlphaMax = (inputAmount: string, bank: Bank | undefined) => {
const exceedsAlphaMax = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const group = mangoStore.getState().group
if (!group || !mangoAccount) return
if (
mangoAccount.owner.toString() ===
'8SSLjXBEVk9nesbhi9UMCA32uijbVBUqWoKPPQPTekzt'
)
return false
const accountValue = toUiDecimalsForQuote(
mangoAccount.getEquity(group).toNumber()
)
return (
parseFloat(inputAmount) * (bank?.uiPrice || 1) + accountValue >
ALPHA_DEPOSIT_LIMIT || accountValue > ALPHA_DEPOSIT_LIMIT
)
}, [inputAmount, bank])
return exceedsAlphaMax
}
function DepositForm({ onSuccess, token }: DepositFormProps) {
const { t } = useTranslation('common')
const { group } = useMangoGroup()
@ -74,11 +102,14 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
const [showTokenList, setShowTokenList] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const { mangoTokens } = useJupiterMints()
const { handleConnect } = useEnhancedWallet()
const { maxSolDeposit } = useSolBalance()
const bank = useMemo(() => {
const group = mangoStore.getState().group
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken])
const exceedsAlphaMax = useAlphaMax(inputAmount, bank)
const logoUri = useMemo(() => {
let logoURI
@ -98,7 +129,9 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
}, [walletTokens, selectedToken])
const setMax = useCallback(() => {
setInputAmount(tokenMax.maxAmount.toString())
setInputAmount(
floorToDecimal(tokenMax.maxAmount, tokenMax.maxDecimals).toFixed()
)
setSizePercentage('100')
}, [tokenMax])
@ -176,25 +209,9 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
return banks
}, [group?.banksMapByName, walletTokens])
const exceedsAlphaMax = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const group = mangoStore.getState().group
if (!group || !mangoAccount) return
if (
mangoAccount.owner.toString() ===
'8SSLjXBEVk9nesbhi9UMCA32uijbVBUqWoKPPQPTekzt'
)
return false
const accountValue = toUiDecimalsForQuote(
mangoAccount.getEquity(group).toNumber()
)
return (
parseFloat(inputAmount) * (bank?.uiPrice || 1) + accountValue >
ALPHA_DEPOSIT_LIMIT || accountValue > ALPHA_DEPOSIT_LIMIT
)
}, [inputAmount, bank])
const showInsufficientBalance = tokenMax.maxAmount < Number(inputAmount)
const showInsufficientBalance =
tokenMax.maxAmount < Number(inputAmount) ||
(selectedToken === 'SOL' && maxSolDeposit <= 0)
return (
<>
@ -211,14 +228,14 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
<h2 className="mb-4 text-center text-lg">
{t('select-deposit-token')}
</h2>
<div className="grid auto-cols-fr grid-flow-col px-4 pb-2">
<div className="text-left">
<div className="flex items-center px-4 pb-2">
<div className="w-1/4 text-left">
<p className="text-xs">{t('token')}</p>
</div>
<div className="text-right">
<div className="w-1/4 text-right">
<p className="text-xs">{t('deposit-rate')}</p>
</div>
<div className="text-right">
<div className="w-1/2 text-right">
<p className="whitespace-nowrap text-xs">{t('wallet-balance')}</p>
</div>
</div>
@ -243,6 +260,7 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
/>
<SolBalanceWarnings
amount={inputAmount}
className="mt-2"
setAmount={setInputAmount}
selectedToken={selectedToken}
/>
@ -289,7 +307,7 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
allowNegative={false}
isNumericString={true}
decimalScale={bank?.mintDecimals || 6}
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl tracking-wider text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) => {
@ -310,35 +328,31 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
/>
</div>
</div>
<div className="my-6 space-y-1.5 border-y border-th-bkg-3 px-2 py-4 text-sm ">
<HealthImpactTokenChange
mintPk={bank!.mint}
uiAmount={Number(inputAmount)}
isDeposit
/>
<div className="flex justify-between">
<p>{t('deposit-amount')}</p>
<p className="font-mono text-th-fgd-2">
{bank?.uiPrice && inputAmount ? (
<>
{inputAmount}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
bank.uiPrice * Number(inputAmount),
true
)}
)
</span>
</>
{bank ? (
<div className="my-6 space-y-1.5 border-y border-th-bkg-3 px-2 py-4 text-sm ">
<HealthImpactTokenChange
mintPk={bank.mint}
uiAmount={Number(inputAmount)}
isDeposit
/>
<div className="flex justify-between">
<p>{t('deposit-amount')}</p>
{inputAmount ? (
<AmountWithValue
amount={formatDecimal(
Number(inputAmount),
bank.mintDecimals
)}
value={formatFixedDecimals(
bank.uiPrice * Number(inputAmount),
true
)}
/>
) : (
<>
0 <span className="text-xs text-th-fgd-3">($0.00)</span>
</>
<AmountWithValue amount="0" value="$0.00" />
)}
</p>
</div>
{/* <div className="flex justify-between">
</div>
{/* <div className="flex justify-between">
<div className="flex items-center">
<Tooltip content={t('asset-weight-desc')}>
<p className="tooltip-underline">{t('asset-weight')}</p>
@ -346,29 +360,28 @@ function DepositForm({ onSuccess, token }: DepositFormProps) {
</div>
<p className="font-mono">{bank!.initAssetWeight.toFixed(2)}x</p>
</div> */}
<div className="flex justify-between">
<Tooltip content={t('tooltip-collateral-value')}>
<p className="tooltip-underline">{t('collateral-value')}</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(
bank!.uiPrice! *
Number(inputAmount) *
Number(bank!.initAssetWeight),
true
)}
</p>
<div className="flex justify-between">
<Tooltip content={t('tooltip-collateral-value')}>
<p className="tooltip-underline">{t('collateral-value')}</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">
{formatFixedDecimals(
bank.uiPrice *
Number(inputAmount) *
Number(bank.initAssetWeight),
true
)}
</p>
</div>
</div>
</div>
) : null}
</div>
<Button
onClick={handleDeposit}
onClick={connected ? handleDeposit : handleConnect}
className="flex w-full items-center justify-center"
disabled={
!inputAmount ||
exceedsAlphaMax ||
showInsufficientBalance ||
!connected
connected &&
(!inputAmount || exceedsAlphaMax || showInsufficientBalance)
}
size="large"
>

View File

@ -79,7 +79,11 @@ const Layout = ({ children }: { children: ReactNode }) => {
}`}
/>
</button>
<div className={`h-full ${!isCollapsed ? 'overflow-y-auto' : ''}`}>
<div
className={`thin-scroll h-full ${
!isCollapsed ? 'overflow-y-auto' : ''
}`}
>
<SideNav collapsed={isCollapsed} />
</div>
</div>

View File

@ -1,129 +0,0 @@
import { Fragment, useState } from 'react'
import {
CheckCircleIcon,
ChevronDownIcon,
PlusCircleIcon,
} from '@heroicons/react/20/solid'
import { Popover, Transition } from '@headlessui/react'
import { MangoAccount } from '@blockworks-foundation/mango-v4'
import mangoStore from '@store/mangoStore'
import { LinkButton } from './shared/Button'
import CreateAccountModal from './modals/CreateAccountModal'
import { useLocalStorageStringState } from '../hooks/useLocalStorageState'
import { LAST_ACCOUNT_KEY } from '../utils/constants'
import { useTranslation } from 'next-i18next'
import { retryFn } from '../utils'
import Loading from './shared/Loading'
const MangoAccountsList = ({
mangoAccount,
}: {
mangoAccount: MangoAccount | undefined
}) => {
const { t } = useTranslation('common')
const mangoAccounts = mangoStore((s) => s.mangoAccounts)
const actions = mangoStore((s) => s.actions)
const loading = mangoStore((s) => s.mangoAccount.initialLoad)
const [showNewAccountModal, setShowNewAccountModal] = useState(false)
const [, setLastAccountViewed] = useLocalStorageStringState(LAST_ACCOUNT_KEY)
const handleSelectMangoAccount = async (acc: MangoAccount) => {
const set = mangoStore.getState().set
const client = mangoStore.getState().client
const group = mangoStore.getState().group
if (!group) return
set((s) => {
s.activityFeed.feed = []
s.activityFeed.loading = true
})
try {
const reloadedMangoAccount = await retryFn(() => acc.reload(client))
set((s) => {
s.mangoAccount.current = reloadedMangoAccount
})
setLastAccountViewed(acc.publicKey.toString())
actions.fetchOpenOrders(acc)
} catch (e) {
console.warn('Error selecting account', e)
}
}
return (
<div id="account-step-two">
<Popover>
{({ open }) => (
<>
<Popover.Button className="flex w-full items-center rounded-none text-th-fgd-1 hover:text-th-active">
<div className="mr-2">
<p className="text-right text-xs">{t('accounts')}</p>
<p className="text-left text-sm font-bold text-th-fgd-1">
{mangoAccount ? mangoAccount.name : 'No Accounts'}
</p>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} mt-0.5 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</Popover.Button>
<div className="relative">
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel className="absolute top-[13.5px] -right-7 z-20 mr-4 w-56 rounded-md rounded-t-none border border-th-bkg-2 bg-th-bkg-2 p-4 text-th-fgd-3">
{loading ? (
<Loading />
) : mangoAccounts.length ? (
mangoAccounts.map((acc) => (
<div key={acc.publicKey.toString()}>
<button
onClick={() => handleSelectMangoAccount(acc)}
className="default-transition mb-3 flex w-full items-center justify-between border-b border-th-bkg-4 pb-3 hover:text-th-fgd-1"
>
{acc.name}
{acc.publicKey.toString() ===
mangoAccount?.publicKey.toString() ? (
<CheckCircleIcon className="h-5 w-5 text-th-up" />
) : null}
</button>
</div>
))
) : (
<p className="mb-4 text-center text-sm">
Create your first account 😎
</p>
)}
<div>
<LinkButton
className="w-full justify-center"
onClick={() => setShowNewAccountModal(true)}
>
<PlusCircleIcon className="h-5 w-5" />
<span className="ml-2">New subaccount</span>
</LinkButton>
</div>
</Popover.Panel>
</Transition>
</div>
</>
)}
</Popover>
{showNewAccountModal ? (
<CreateAccountModal
isOpen={showNewAccountModal}
onClose={() => setShowNewAccountModal(false)}
/>
) : null}
</div>
)
}
export default MangoAccountsList

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect } from 'react'
import mangoStore from '@store/mangoStore'
import { PublicKey } from '@solana/web3.js'
import { Keypair, PublicKey } from '@solana/web3.js'
import { useRouter } from 'next/router'
import { MangoAccount } from '@blockworks-foundation/mango-v4'
import useMangoAccount from 'hooks/useMangoAccount'
@ -12,7 +12,8 @@ const actions = mangoStore.getState().actions
const HydrateStore = () => {
const router = useRouter()
const { name: marketName } = router.query
const { mangoAccount } = useMangoAccount()
const { mangoAccountPk, mangoAccountAddress } = useMangoAccount()
const connection = mangoStore((s) => s.connection)
const fetchData = useCallback(async () => {
await actions.fetchGroup()
@ -31,15 +32,32 @@ const HydrateStore = () => {
fetchData()
}, 15000)
useInterval(() => {
if (mangoAccountAddress) {
actions.fetchOpenOrders()
}
}, 30000)
// The websocket library solana/web3.js uses closes its websocket connection when the subscription list
// is empty after opening its first time, preventing subsequent subscriptions from receiving responses.
// This is a hack to prevent the list from every getting empty
useEffect(() => {
const id = connection.onAccountChange(new Keypair().publicKey, () => {
return
})
return () => {
connection.removeAccountChangeListener(id)
}
}, [connection])
// watch selected Mango Account for changes
useEffect(() => {
const connection = mangoStore.getState().connection
const client = mangoStore.getState().client
if (!mangoAccount) return
if (!mangoAccountPk) return
const subscriptionId = connection.onAccountChange(
mangoAccount.publicKey,
mangoAccountPk,
async (info, context) => {
if (info?.lamports === 0) return
@ -80,7 +98,7 @@ const HydrateStore = () => {
return () => {
connection.removeAccountChangeListener(subscriptionId)
}
}, [mangoAccount])
}, [connection, mangoAccountPk])
return null
}
@ -103,10 +121,12 @@ const ReadOnlyMangoAccount = () => {
const pk = new PublicKey(ma)
const readOnlyMangoAccount = await client.getMangoAccount(pk)
await readOnlyMangoAccount.reloadAccountData(client)
await actions.fetchOpenOrders(readOnlyMangoAccount)
set((state) => {
state.mangoAccount.current = readOnlyMangoAccount
state.mangoAccount.initialLoad = false
})
actions.fetchTradeHistory()
} catch (error) {
console.error('error', error)
}

View File

@ -3,7 +3,6 @@ import {
ArrowLeftIcon,
ChevronDownIcon,
ExclamationCircleIcon,
LinkIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/20/solid'
import { Wallet } from '@project-serum/anchor'
@ -15,7 +14,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import NumberFormat, { NumberFormatValues } from 'react-number-format'
import mangoStore from '@store/mangoStore'
import { notify } from './../utils/notifications'
import { floorToDecimal, formatFixedDecimals } from './../utils/numbers'
import {
floorToDecimal,
formatDecimal,
formatFixedDecimals,
} from './../utils/numbers'
import ActionTokenList from './account/ActionTokenList'
import ButtonGroup from './forms/ButtonGroup'
import Label from './forms/Label'
@ -25,12 +28,17 @@ import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions'
import { withValueLimit } from './swap/SwapForm'
import MaxAmountButton from '@components/shared/MaxAmountButton'
import HealthImpactTokenChange from '@components/HealthImpactTokenChange'
import { walletBalanceForToken } from './DepositForm'
import { useAlphaMax, walletBalanceForToken } from './DepositForm'
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
import useMangoAccount from 'hooks/useMangoAccount'
import useJupiterMints from 'hooks/useJupiterMints'
import useMangoGroup from 'hooks/useMangoGroup'
import { ACCOUNT_ACTION_MODAL_INNER_HEIGHT } from 'utils/constants'
import {
ACCOUNT_ACTION_MODAL_INNER_HEIGHT,
INPUT_TOKEN_DEFAULT,
} from 'utils/constants'
import ConnectEmptyState from './shared/ConnectEmptyState'
import AmountWithValue from './shared/AmountWithValue'
interface RepayFormProps {
onSuccess: () => void
@ -43,7 +51,9 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
const { mangoAccount } = useMangoAccount()
const [inputAmount, setInputAmount] = useState('')
const [submitting, setSubmitting] = useState(false)
const [selectedToken, setSelectedToken] = useState(token)
const [selectedToken, setSelectedToken] = useState(
token || INPUT_TOKEN_DEFAULT
)
const [showTokenList, setShowTokenList] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const { mangoTokens } = useJupiterMints()
@ -51,7 +61,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
const bank = useMemo(() => {
const group = mangoStore.getState().group
return selectedToken ? group?.banksMapByName.get(selectedToken)?.[0] : null
return group?.banksMapByName.get(selectedToken)?.[0]
}, [selectedToken])
const logoUri = useMemo(() => {
@ -82,20 +92,22 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
}, [bank, mangoAccount])
const setMax = useCallback(() => {
setInputAmount(borrowAmount.toFixed(bank?.mintDecimals))
if (!bank) return
setInputAmount(floorToDecimal(borrowAmount, bank.mintDecimals).toFixed())
setSizePercentage('100')
}, [bank, borrowAmount])
const handleSizePercentage = useCallback(
(percentage: string) => {
if (!bank) return
setSizePercentage(percentage)
let amount: Decimal | number = new Decimal(borrowAmount)
.mul(percentage)
.div(100)
amount = floorToDecimal(amount, bank!.mintDecimals).toNumber()
amount = floorToDecimal(amount, bank.mintDecimals).toNumber()
setInputAmount(amount.toFixed(bank?.mintDecimals))
setInputAmount(amount.toFixed(bank.mintDecimals))
},
[bank, borrowAmount]
)
@ -172,6 +184,8 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
}
}, [token, banks])
const exceedsAlphaMax = useAlphaMax(inputAmount, bank)
const showInsufficientBalance = walletBalance.maxAmount < Number(inputAmount)
return banks.length ? (
@ -187,11 +201,11 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
<ArrowLeftIcon className={`h-6 w-6`} />
</button>
<h2 className="mb-4 text-center text-lg">{t('select-repay-token')}</h2>
<div className="grid auto-cols-fr grid-flow-col px-4 pb-2">
<div className="text-left">
<div className="flex items-center px-4 pb-2">
<div className="w-1/2 text-left">
<p className="text-xs">{t('token')}</p>
</div>
<div className="text-right">
<div className="w-1/2 text-right">
<p className="whitespace-nowrap text-xs">{t('amount-owed')}</p>
</div>
</div>
@ -208,13 +222,12 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
style={{ height: ACCOUNT_ACTION_MODAL_INNER_HEIGHT }}
>
<div>
<div className="-mt-2 mb-2">
<SolBalanceWarnings
amount={inputAmount}
setAmount={setInputAmount}
selectedToken={selectedToken}
/>
</div>
<SolBalanceWarnings
amount={inputAmount}
className="mb-4"
setAmount={setInputAmount}
selectedToken={selectedToken}
/>
<div className="grid grid-cols-2">
<div className="col-span-2 flex justify-between">
<Label text={`${t('repay')} ${t('token')}`} />
@ -255,7 +268,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
allowNegative={false}
isNumericString={true}
decimalScale={bank?.mintDecimals || 6}
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl tracking-wider text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) => {
@ -276,56 +289,53 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
/>
</div>
</div>
<div className="my-6 space-y-1.5 border-y border-th-bkg-3 px-2 py-4 text-sm ">
{bank ? (
{bank ? (
<div className="my-6 space-y-1.5 border-y border-th-bkg-3 px-2 py-4 text-sm ">
<HealthImpactTokenChange
mintPk={bank.mint}
uiAmount={Number(inputAmount)}
isDeposit
/>
) : null}
<div className="flex justify-between">
<p>{t('repayment-amount')}</p>
<p className="font-mono text-th-fgd-2">
{bank?.uiPrice && inputAmount ? (
<>
{inputAmount}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
bank.uiPrice * Number(inputAmount),
true
)}
)
</span>
</>
<div className="flex justify-between">
<p>{t('repayment-amount')}</p>
{inputAmount ? (
<AmountWithValue
amount={formatDecimal(
Number(inputAmount),
bank.mintDecimals
)}
value={formatFixedDecimals(
bank.uiPrice * Number(inputAmount),
true
)}
/>
) : (
<>
0 <span className="text-xs text-th-fgd-3">($0.00)</span>
</>
<AmountWithValue amount="0" value="$0.00" />
)}
</p>
</div>
<div className="flex justify-between">
<div className="flex items-center">
<p>{t('outstanding-balance')}</p>
</div>
<p className="font-mono text-th-fgd-2">
{floorToDecimal(
borrowAmount - Number(inputAmount),
walletBalance.maxDecimals
).toNumber()}{' '}
<span className="font-body text-th-fgd-4">
{selectedToken}
</span>
</p>
<div className="flex justify-between">
<div className="flex items-center">
<p>{t('outstanding-balance')}</p>
</div>
<p className="font-mono text-th-fgd-2">
{floorToDecimal(
borrowAmount - Number(inputAmount),
walletBalance.maxDecimals
).toFixed()}{' '}
<span className="font-body text-th-fgd-4">
{selectedToken}
</span>
</p>
</div>
</div>
</div>
) : null}
</div>
<Button
onClick={() => handleDeposit(inputAmount)}
className="flex w-full items-center justify-center"
disabled={!inputAmount || showInsufficientBalance}
disabled={
!inputAmount || showInsufficientBalance || exceedsAlphaMax
}
size="large"
>
{submitting ? (
@ -349,8 +359,7 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
</>
) : !connected ? (
<div className="flex h-[356px] flex-col items-center justify-center">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>Connect to repay your borrows</p>
<ConnectEmptyState text="Connect to repay your borrows" />
</div>
) : (
<div className="flex h-[356px] flex-col items-center justify-center">

View File

@ -10,10 +10,11 @@ import {
Cog8ToothIcon,
ArrowsRightLeftIcon,
ArrowTrendingUpIcon,
XMarkIcon,
} from '@heroicons/react/20/solid'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { Fragment, ReactNode, useEffect, useState } from 'react'
import { Fragment, ReactNode, useState } from 'react'
import { Disclosure, Popover, Transition } from '@headlessui/react'
import MangoAccountSummary from './account/MangoAccountSummary'
import Tooltip from './shared/Tooltip'
@ -23,6 +24,7 @@ import mangoStore from '@store/mangoStore'
import HealthHeart from './account/HealthHeart'
import useMangoAccount from 'hooks/useMangoAccount'
import { useTheme } from 'next-themes'
import { IconButton } from './shared/Button'
const SideNav = ({ collapsed }: { collapsed: boolean }) => {
const { t } = useTranslation('common')
@ -152,9 +154,13 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
size={32}
/>
}
isOpen
panelTitle={`${mangoAccount?.name || ''} ${t('account')}`}
title={
<div className="w-24 text-left">
<p className="mb-0.5 whitespace-nowrap text-xs">Health Check</p>
<p className="mb-0.5 whitespace-nowrap text-xs">
{t('account')}
</p>
<p className="truncate whitespace-nowrap text-sm font-bold text-th-fgd-1">
{mangoAccount
? mangoAccount.name
@ -166,6 +172,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
}
alignBottom
hideIconBg
showClose
>
<div className="px-4 py-2">
<MangoAccountSummary />
@ -253,6 +260,9 @@ export const ExpandableMenuItem = ({
collapsed,
hideIconBg,
icon,
panelTitle,
isOpen,
showClose,
title,
}: {
alignBottom?: boolean
@ -260,26 +270,14 @@ export const ExpandableMenuItem = ({
collapsed: boolean
hideIconBg?: boolean
icon: ReactNode
panelTitle?: string
isOpen?: boolean
showClose?: boolean
title: string | ReactNode
}) => {
const [showMenu, setShowMenu] = useState(false)
const [showMenu, setShowMenu] = useState(isOpen || false)
const { theme } = useTheme()
const onHoverMenu = (open: boolean, action: string) => {
if (
(!open && action === 'onMouseEnter') ||
(open && action === 'onMouseLeave')
) {
setShowMenu(!open)
}
}
useEffect(() => {
if (collapsed) {
setShowMenu(false)
}
}, [collapsed])
const toggleMenu = () => {
setShowMenu(!showMenu)
}
@ -287,12 +285,6 @@ export const ExpandableMenuItem = ({
return collapsed ? (
<Popover>
<div
onMouseEnter={
!alignBottom ? () => onHoverMenu(showMenu, 'onMouseEnter') : undefined
}
onMouseLeave={
!alignBottom ? () => onHoverMenu(showMenu, 'onMouseLeave') : undefined
}
className={`relative z-30 ${alignBottom ? '' : 'px-4 py-2'}`}
role="button"
>
@ -329,6 +321,20 @@ export const ExpandableMenuItem = ({
}`}
>
<div className="rounded-md rounded-l-none bg-th-bkg-2 py-2">
<div className="flex items-center justify-between pl-4 pr-2">
{panelTitle ? (
<h3 className="text-sm font-bold">{panelTitle}</h3>
) : null}
{showClose ? (
<IconButton
onClick={() => setShowMenu(false)}
hideBg
size="small"
>
<XMarkIcon className="h-5 w-5" />
</IconButton>
) : null}
</div>
{children}
</div>
</Popover.Panel>

View File

@ -31,6 +31,11 @@ import { Table, Td, Th, TrBody, TrHead } from './shared/TableElements'
import useMangoGroup from 'hooks/useMangoGroup'
import DepositWithdrawModal from './modals/DepositWithdrawModal'
import BorrowRepayModal from './modals/BorrowRepayModal'
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'
import { USDC_MINT } from 'utils/constants'
import { PublicKey } from '@solana/web3.js'
import ActionsLinkButton from './account/ActionsLinkButton'
import AmountWithValue from './shared/AmountWithValue'
const TokenList = () => {
const { t } = useTranslation(['common', 'token', 'trade'])
@ -54,18 +59,23 @@ const TokenList = () => {
value,
}))
const sortedBanks = mangoAccount
? rawBanks.sort(
(a, b) =>
Math.abs(
mangoAccount?.getTokenBalanceUi(b.value[0]) *
b.value[0].uiPrice!
) -
Math.abs(
mangoAccount?.getTokenBalanceUi(a.value[0]) *
a.value[0].uiPrice!
)
)
: rawBanks
? rawBanks.sort((a, b) => {
const aBalance = Math.abs(
mangoAccount.getTokenBalanceUi(a.value[0]) * a.value[0].uiPrice
)
const bBalance = Math.abs(
mangoAccount.getTokenBalanceUi(b.value[0]) * b.value[0].uiPrice
)
if (aBalance > bBalance) return -1
if (aBalance < bBalance) return 1
const aName = a.value[0].name
const bName = b.value[0].name
if (aName > bName) return 1
if (aName < bName) return -1
return 1
})
: rawBanks.sort((a, b) => a.key.localeCompare(b.key))
return mangoAccount && !showZeroBalances
? sortedBanks.filter(
@ -155,12 +165,12 @@ const TokenList = () => {
)
const interestAmount = hasInterestEarned
? hasInterestEarned.borrow_interest +
? hasInterestEarned.borrow_interest * -1 +
hasInterestEarned.deposit_interest
: 0
const interestValue = hasInterestEarned
? hasInterestEarned.borrow_interest_usd +
? hasInterestEarned.borrow_interest_usd * -1 +
hasInterestEarned.deposit_interest_usd
: 0.0
@ -180,48 +190,69 @@ const TokenList = () => {
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)}
</div>
<p className="font-body tracking-wide">{bank.name}</p>
<p className="font-body tracking-wider">{bank.name}</p>
</div>
</Td>
<Td className="text-right">
<p>{tokenBalance}</p>
<p className="text-sm text-th-fgd-4">
{tokenBalance
? `${formatFixedDecimals(
tokenBalance * oraclePrice!,
true
)}`
: '$0.00'}
</p>
{tokenBalance ? (
<AmountWithValue
amount={formatDecimal(tokenBalance, bank.mintDecimals)}
value={formatFixedDecimals(
tokenBalance * oraclePrice,
true,
true
)}
stacked
/>
) : (
<AmountWithValue amount="0" value="$0.00" stacked />
)}
</Td>
<Td className="text-right">
<p>{inOrders}</p>
<p className="text-sm text-th-fgd-4">
{formatFixedDecimals(inOrders * oraclePrice!, true)}
</p>
{inOrders ? (
<AmountWithValue
amount={formatDecimal(inOrders, bank.mintDecimals)}
value={formatFixedDecimals(
inOrders * oraclePrice,
true,
true
)}
stacked
/>
) : (
<AmountWithValue amount="0" value="$0.00" stacked />
)}
</Td>
<Td className="text-right">
<p>
{unsettled ? unsettled.toFixed(bank.mintDecimals) : 0}
</p>
<p className="text-sm text-th-fgd-4">
{formatFixedDecimals(unsettled * oraclePrice!, true)}
</p>
{unsettled ? (
<AmountWithValue
amount={formatDecimal(unsettled, bank.mintDecimals)}
value={formatFixedDecimals(
unsettled * oraclePrice,
true,
true
)}
stacked
/>
) : (
<AmountWithValue amount="0" value="$0.00" stacked />
)}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{interestAmount
? interestAmount.toFixed(bank.mintDecimals)
: 0}
</p>
<p className="text-sm text-th-fgd-4">
{formatFixedDecimals(interestValue, true)}
</p>
<AmountWithValue
amount={
interestAmount
? formatDecimal(interestAmount, bank.mintDecimals)
: '0'
}
value={formatFixedDecimals(interestValue, true, true)}
stacked
/>
</div>
</Td>
<Td>
<div className="flex justify-end space-x-2">
<div className="flex justify-end space-x-1.5">
<p className="text-th-up">
{formatDecimal(bank.getDepositRateUi(), 2, {
fixed: true,
@ -291,11 +322,12 @@ const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
)
const interestAmount = hasInterestEarned
? hasInterestEarned.borrow_interest + hasInterestEarned.deposit_interest
? hasInterestEarned.borrow_interest * -1 +
hasInterestEarned.deposit_interest
: 0
const interestValue = hasInterestEarned
? hasInterestEarned.borrow_interest_usd +
? hasInterestEarned.borrow_interest_usd * -1 +
hasInterestEarned.deposit_interest_usd
: 0.0
@ -331,7 +363,9 @@ const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
<span className="mr-1 font-body text-th-fgd-4">
{t('balance')}:
</span>
{tokenBalance}
{tokenBalance
? formatDecimal(tokenBalance, bank.mintDecimals)
: '0'}
</p>
</div>
</div>
@ -363,34 +397,32 @@ const MobileTokenListItem = ({ bank }: { bank: Bank }) => {
<div className="mt-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4">
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('trade:in-orders')}</p>
<div className="flex font-mono">
<p className="text-th-fgd-2">{inOrders}</p>
<p className="ml-1 text-th-fgd-4">
({formatFixedDecimals(inOrders * oraclePrice!, true)})
</p>
</div>
<AmountWithValue
amount={
inOrders ? formatDecimal(inOrders, bank.mintDecimals) : '0'
}
value={formatFixedDecimals(inOrders * oraclePrice, true, true)}
/>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('trade:unsettled')}</p>
<div className="flex font-mono">
<p className="text-th-fgd-2">
{unsettled ? unsettled.toFixed(bank.mintDecimals) : 0}
</p>
<p className="ml-1 text-th-fgd-4">
({formatFixedDecimals(unsettled * oraclePrice!, true)})
</p>
</div>
<AmountWithValue
amount={
unsettled ? formatDecimal(unsettled, bank.mintDecimals) : '0'
}
value={formatFixedDecimals(unsettled * oraclePrice, true, true)}
/>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('interest-earned-paid')}</p>
<div className="flex font-mono">
<p className="text-th-fgd-2">
{floorToDecimal(interestAmount, bank.mintDecimals).toNumber()}
</p>
<p className="ml-1 text-th-fgd-4">
({formatFixedDecimals(interestValue, true)})
</p>
</div>
<AmountWithValue
amount={
interestAmount
? formatDecimal(interestAmount, bank.mintDecimals)
: '0'
}
value={formatFixedDecimals(interestValue, true, true)}
/>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('rates')}</p>
@ -432,10 +464,18 @@ const ActionsMenu = ({
const [showBorrowModal, setShowBorrowModal] = useState(false)
const [showRepayModal, setShowRepayModal] = useState(false)
const [selectedToken, setSelectedToken] = useState('')
// const set = mangoStore.getState().set
// const router = useRouter()
// const { asPath } = router
const set = mangoStore.getState().set
const router = useRouter()
const { mangoTokens } = useJupiterMints()
const spotMarkets = mangoStore((s) => s.serumMarkets)
const { connected } = useWallet()
const spotMarket = useMemo(() => {
return spotMarkets.find((m) => {
const base = m.name.split('/')[0]
return base.toUpperCase() === bank.name.toUpperCase()
})
}, [spotMarkets])
const handleShowActionModals = useCallback(
(token: string, action: 'borrow' | 'deposit' | 'withdraw' | 'repay') => {
@ -451,31 +491,53 @@ const ActionsMenu = ({
[]
)
// const handleBuy = useCallback(() => {
// const outputTokenInfo = mangoTokens.find(
// (t: any) => t.address === bank.mint.toString()
// )
// set((s) => {
// s.swap.outputBank = bank
// s.swap.outputTokenInfo = outputTokenInfo
// })
// if (asPath === '/') {
// router.push('/swap', undefined, { shallow: true })
// }
// }, [bank, router, asPath, set, mangoTokens])
const balance = useMemo(() => {
if (!mangoAccount || !bank) return 0
return mangoAccount.getTokenBalanceUi(bank)
}, [bank, mangoAccount])
// const handleSell = useCallback(() => {
// const inputTokenInfo = mangoTokens.find(
// (t: any) => t.address === bank.mint.toString()
// )
// set((s) => {
// s.swap.inputBank = bank
// s.swap.inputTokenInfo = inputTokenInfo
// })
// if (asPath === '/') {
// router.push('/swap', undefined, { shallow: true })
// }
// }, [router, asPath, set, bank, mangoTokens])
const handleSwap = useCallback(() => {
const tokenInfo = mangoTokens.find(
(t: any) => t.address === bank.mint.toString()
)
const group = mangoStore.getState().group
if (balance && balance > 0) {
if (tokenInfo?.symbol === 'SOL') {
const usdcTokenInfo = mangoTokens.find(
(t: any) => t.address === USDC_MINT
)
const usdcBank = group?.getFirstBankByMint(new PublicKey(USDC_MINT))
set((s) => {
s.swap.inputBank = usdcBank
s.swap.inputTokenInfo = usdcTokenInfo
})
}
set((s) => {
s.swap.inputBank = bank
s.swap.inputTokenInfo = tokenInfo
})
} else {
if (tokenInfo?.symbol === 'USDC') {
const solTokenInfo = mangoTokens.find(
(t: any) => t.address === WRAPPED_SOL_MINT.toString()
)
const solBank = group?.getFirstBankByMint(WRAPPED_SOL_MINT)
set((s) => {
s.swap.inputBank = solBank
s.swap.inputTokenInfo = solTokenInfo
})
}
set((s) => {
s.swap.outputBank = bank
s.swap.outputTokenInfo = tokenInfo
})
}
router.push('/swap', undefined, { shallow: true })
}, [bank, router, set, mangoTokens, mangoAccount])
const handleTrade = useCallback(() => {
router.push(`/trade?name=${spotMarket?.name}`, undefined, { shallow: true })
}, [spotMarket, router])
const logoURI = useMemo(() => {
if (!bank || !mangoTokens?.length) return ''
@ -494,63 +556,60 @@ const ActionsMenu = ({
return (
<>
<IconDropMenu
icon={<EllipsisHorizontalIcon className="h-5 w-5" />}
postion="leftBottom"
>
<div className="flex items-center justify-center border-b border-th-bkg-3 pb-2">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image alt="" width="20" height="20" src={logoURI || ''} />
{mangoAccount && !connected ? null : (
<IconDropMenu
icon={<EllipsisHorizontalIcon className="h-5 w-5" />}
postion="leftBottom"
>
<div className="flex items-center justify-center border-b border-th-bkg-3 pb-2">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image alt="" width="20" height="20" src={logoURI || ''} />
</div>
<p className="font-body tracking-wider">
{formatTokenSymbol(bank.name)}
</p>
</div>
<p className="font-body tracking-wide">
{formatTokenSymbol(bank.name)}
</p>
</div>
<LinkButton
className="w-full text-left font-normal no-underline md:hover:text-th-fgd-1"
disabled={!mangoAccount}
onClick={() => handleShowActionModals(bank.name, 'deposit')}
>
{t('deposit')}
</LinkButton>
{hasBorrow ? (
<LinkButton
className="w-full text-left font-normal no-underline md:hover:text-th-fgd-1"
disabled={!mangoAccount}
onClick={() => handleShowActionModals(bank.name, 'repay')}
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'deposit')}
>
{t('repay')}
</LinkButton>
) : null}
<LinkButton
className="w-full text-left font-normal no-underline md:hover:text-th-fgd-1"
disabled={!mangoAccount}
onClick={() => handleShowActionModals(bank.name, 'withdraw')}
>
{t('withdraw')}
</LinkButton>
<LinkButton
className="w-full text-left font-normal no-underline md:hover:text-th-fgd-1"
disabled={!mangoAccount}
onClick={() => handleShowActionModals(bank.name, 'borrow')}
>
{t('borrow')}
</LinkButton>
{/* <LinkButton
className="w-full text-left"
disabled={!mangoAccount}
onClick={handleBuy}
>
{t('buy')}
</LinkButton>
<LinkButton
className="w-full text-left"
disabled={!mangoAccount}
onClick={handleSell}
>
{t('sell')}
</LinkButton> */}
</IconDropMenu>
{t('deposit')}
</ActionsLinkButton>
{hasBorrow ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'repay')}
>
{t('repay')}
</ActionsLinkButton>
) : null}
{balance && balance > 0 ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'withdraw')}
>
{t('withdraw')}
</ActionsLinkButton>
) : null}
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => handleShowActionModals(bank.name, 'borrow')}
>
{t('borrow')}
</ActionsLinkButton>
<ActionsLinkButton mangoAccount={mangoAccount!} onClick={handleSwap}>
{t('swap')}
</ActionsLinkButton>
{spotMarket ? (
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={handleTrade}
>
{t('trade')}
</ActionsLinkButton>
) : null}
</IconDropMenu>
)}
{showDepositModal ? (
<DepositWithdrawModal
action="deposit"

View File

@ -3,6 +3,7 @@ import {
ArrowLeftIcon,
ArrowRightIcon,
ExclamationTriangleIcon,
EyeIcon,
UsersIcon,
} from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
@ -93,11 +94,22 @@ const TopBar = () => {
alt="next"
/>
{!connected ? (
<span className="hidden items-center md:flex">
<WalletIcon className="h-5 w-5 text-th-fgd-3" />
<span className="ml-2">{t('connect-helper')}</span>
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
</span>
mangoAccount ? (
<span className="hidden items-center md:flex">
<EyeIcon className="h-5 w-5 text-th-fgd-3" />
<span className="ml-2">
{t('unowned-helper', {
accountPk: abbreviateAddress(mangoAccount.publicKey),
})}
</span>
</span>
) : (
<span className="hidden items-center md:flex">
<WalletIcon className="h-5 w-5 text-th-fgd-3" />
<span className="ml-2">{t('connect-helper')}</span>
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
</span>
)
) : null}
</span>
{!isOnline ? (
@ -112,7 +124,7 @@ const TopBar = () => {
{/* <div className="px-3 md:px-4">
<ThemeSwitcher />
</div> */}
{!connected && isMobile ? null : (
{(mangoAccount && !connected) || (!connected && isMobile) ? null : (
<Button
onClick={() => handleDepositWithdrawModal('deposit')}
secondary

View File

@ -18,7 +18,11 @@ import {
INPUT_TOKEN_DEFAULT,
} from './../utils/constants'
import { notify } from './../utils/notifications'
import { floorToDecimal, formatFixedDecimals } from './../utils/numbers'
import {
floorToDecimal,
formatDecimal,
formatFixedDecimals,
} from './../utils/numbers'
import ActionTokenList from './account/ActionTokenList'
import ButtonGroup from './forms/ButtonGroup'
import Label from './forms/Label'
@ -35,6 +39,8 @@ import useJupiterMints from 'hooks/useJupiterMints'
import useMangoGroup from 'hooks/useMangoGroup'
import TokenVaultWarnings from '@components/shared/TokenVaultWarnings'
import { useWallet } from '@solana/wallet-adapter-react'
import { useEnhancedWallet } from './wallet/EnhancedWalletProvider'
import AmountWithValue from './shared/AmountWithValue'
interface WithdrawFormProps {
onSuccess: () => void
@ -54,6 +60,7 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
const { mangoTokens } = useJupiterMints()
const { mangoAccount } = useMangoAccount()
const { connected } = useWallet()
const { handleConnect } = useEnhancedWallet()
const bank = useMemo(() => {
const group = mangoStore.getState().group
@ -81,11 +88,12 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
const handleSizePercentage = useCallback(
(percentage: string) => {
if (!bank) return
setSizePercentage(percentage)
const amount = tokenMax.mul(Number(percentage) / 100)
setInputAmount(amount.toFixed())
setInputAmount(floorToDecimal(amount, bank.mintDecimals).toFixed())
},
[tokenMax]
[bank, tokenMax]
)
const handleWithdraw = useCallback(async () => {
@ -181,11 +189,11 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
<h2 className="mb-4 text-center text-lg">
{t('select-withdraw-token')}
</h2>
<div className="grid auto-cols-fr grid-flow-col px-4 pb-2">
<div className="text-left">
<div className="flex items-center px-4 pb-2">
<div className="w-1/2 text-left">
<p className="text-xs">{t('token')}</p>
</div>
<div className="flex justify-end">
<div className="w-1/2 text-right">
<p className="text-xs">{t('available-balance')}</p>
</div>
</div>
@ -210,16 +218,21 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
/>
</div>
) : null}
{bank ? <TokenVaultWarnings bank={bank} /> : null}
{bank ? <TokenVaultWarnings bank={bank} type="withdraw" /> : null}
<div className="grid grid-cols-2">
<div className="col-span-2 flex justify-between">
<Label text={`${t('withdraw')} ${t('token')}`} />
<MaxAmountButton
className="mb-2"
label={t('max')}
onClick={() => handleSizePercentage('100')}
value={tokenMax.toString()}
/>
{bank ? (
<MaxAmountButton
className="mb-2"
label={t('max')}
onClick={() => handleSizePercentage('100')}
value={floorToDecimal(
Number(tokenMax),
bank.mintDecimals
).toFixed()}
/>
) : null}
</div>
<div className="col-span-1 rounded-lg rounded-r-none border border-r-0 border-th-input-border bg-th-input-bkg">
<button
@ -251,7 +264,7 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
allowNegative={false}
isNumericString={true}
decimalScale={bank?.mintDecimals || 6}
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl tracking-wider text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
placeholder="0.00"
value={inputAmount}
onValueChange={(e: NumberFormatValues) =>
@ -272,44 +285,39 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
/>
</div>
</div>
<div className="my-6 space-y-2 border-y border-th-bkg-3 px-2 py-4">
<HealthImpactTokenChange
mintPk={bank!.mint}
uiAmount={Number(inputAmount)}
/>
<div className="flex justify-between">
<p>{t('withdraw-amount')}</p>
<p className="font-mono text-th-fgd-2">
{bank?.uiPrice && inputAmount ? (
<>
{inputAmount}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
bank.uiPrice * Number(inputAmount),
true
)}
)
</span>
</>
{bank ? (
<div className="my-6 space-y-1.5 border-y border-th-bkg-3 px-2 py-4">
<HealthImpactTokenChange
mintPk={bank.mint}
uiAmount={Number(inputAmount)}
/>
<div className="flex justify-between">
<p>{t('withdraw-amount')}</p>
{inputAmount ? (
<AmountWithValue
amount={formatDecimal(
Number(inputAmount),
bank.mintDecimals
)}
value={formatFixedDecimals(
bank.uiPrice * Number(inputAmount),
true
)}
/>
) : (
<>
0 <span className="text-xs text-th-fgd-3">($0.00)</span>
</>
<AmountWithValue amount="0" value="$0.00" />
)}
</p>
</div>
</div>
</div>
) : null}
</div>
<Button
onClick={handleWithdraw}
onClick={connected ? handleWithdraw : handleConnect}
className="flex w-full items-center justify-center"
size="large"
disabled={
!inputAmount ||
showInsufficientBalance ||
initHealth <= 0 ||
!connected
connected &&
(!inputAmount || showInsufficientBalance || initHealth <= 0)
}
>
{!connected ? (

View File

@ -1,5 +1,5 @@
import { Fragment, ReactNode, useState } from 'react'
import Button, { LinkButton } from '../shared/Button'
import { Fragment, useState } from 'react'
import Button from '../shared/Button'
import {
ArrowDownRightIcon,
ArrowUpLeftIcon,
@ -22,6 +22,7 @@ import BorrowRepayModal from '@components/modals/BorrowRepayModal'
import { useWallet } from '@solana/wallet-adapter-react'
import CreateAccountModal from '@components/modals/CreateAccountModal'
import { Menu, Transition } from '@headlessui/react'
import ActionsLinkButton from './ActionsLinkButton'
export const handleCopyAddress = (
mangoAccount: MangoAccount,
@ -45,17 +46,6 @@ const AccountActions = () => {
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const { connected } = useWallet()
// this doesn't work for detecting spot borrows as it includes perp liabs. was only using it to make the repay button have primary styles so could delete
// const hasBorrows = useMemo(() => {
// if (!mangoAccount || !group) return false
// return (
// toUiDecimalsForQuote(
// mangoAccount.getLiabsValue(group, HealthType.init).toNumber()
// ) >= 1
// )
// }, [mangoAccount, group])
const handleBorrowModal = () => {
if (!connected || mangoAccount) {
setShowBorrowModal(true)
@ -66,100 +56,102 @@ const AccountActions = () => {
return (
<>
<div className="flex items-center space-x-2">
<Button
className="flex w-1/3 items-center justify-center sm:w-auto"
disabled={!mangoAccount}
onClick={() => setShowRepayModal(true)}
// secondary={!hasBorrows}
secondary
>
<ArrowDownRightIcon className="mr-2 h-5 w-5" />
{t('repay')}
</Button>
<Button
className="flex w-1/3 items-center justify-center sm:w-auto"
onClick={handleBorrowModal}
secondary
>
<ArrowUpLeftIcon className="mr-2 h-5 w-5" />
{t('borrow')}
</Button>
<Menu>
{({ open }) => (
<div className="relative w-1/3 sm:w-auto">
<Menu.Button
className={`default-transition w-full focus:outline-none`}
>
<Button
className="flex w-full items-center justify-center"
secondary
{mangoAccount && !connected ? null : (
<div className="flex items-center space-x-2">
<Button
className="flex w-1/3 items-center justify-center md:w-auto"
disabled={!mangoAccount}
onClick={() => setShowRepayModal(true)}
secondary
>
<ArrowDownRightIcon className="mr-2 h-5 w-5" />
{t('repay')}
</Button>
<Button
className="flex w-1/3 items-center justify-center md:w-auto"
onClick={handleBorrowModal}
secondary
>
<ArrowUpLeftIcon className="mr-2 h-5 w-5" />
{t('borrow')}
</Button>
<Menu>
{({ open }) => (
<div className="relative w-1/3 md:w-auto">
<Menu.Button
className={`default-transition w-full focus:outline-none`}
as="div"
>
<WrenchIcon className="mr-2 h-4 w-4" />
{t('actions')}
</Button>
</Menu.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition ease-in duration-200"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Menu.Items className="absolute right-0 top-10 mt-1 space-y-1.5 rounded-md bg-th-bkg-2 px-4 py-2.5">
<Menu.Item>
<ActionsButton
mangoAccount={mangoAccount!}
onClick={() =>
handleCopyAddress(
mangoAccount!,
t('copy-address-success', {
pk: abbreviateAddress(mangoAccount!.publicKey),
})
)
}
>
<DocumentDuplicateIcon className="h-4 w-4" />
<span className="ml-2">{t('copy-address')}</span>
</ActionsButton>
</Menu.Item>
<Menu.Item>
<ActionsButton
mangoAccount={mangoAccount!}
onClick={() => setShowEditAccountModal(true)}
>
<PencilIcon className="h-4 w-4" />
<span className="ml-2">{t('edit-account')}</span>
</ActionsButton>
</Menu.Item>
<Menu.Item>
<ActionsButton
mangoAccount={mangoAccount!}
onClick={() => setShowDelegateModal(true)}
>
<UsersIcon className="h-4 w-4" />
<span className="ml-2">{t('delegate-account')}</span>
</ActionsButton>
</Menu.Item>
<Menu.Item>
<ActionsButton
mangoAccount={mangoAccount!}
onClick={() => setShowCloseAccountModal(true)}
>
<TrashIcon className="h-4 w-4" />
<span className="ml-2">{t('close-account')}</span>
</ActionsButton>
</Menu.Item>
</Menu.Items>
</Transition>
</div>
)}
</Menu>
</div>
<Button
className="flex w-full items-center justify-center"
secondary
>
<WrenchIcon className="mr-2 h-4 w-4" />
{t('actions')}
</Button>
</Menu.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition ease-in duration-200"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Menu.Items className="absolute right-0 top-10 mt-1 space-y-1.5 rounded-md bg-th-bkg-2 px-4 py-2.5">
<Menu.Item>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() =>
handleCopyAddress(
mangoAccount!,
t('copy-address-success', {
pk: abbreviateAddress(mangoAccount!.publicKey),
})
)
}
>
<DocumentDuplicateIcon className="h-4 w-4" />
<span className="ml-2">{t('copy-address')}</span>
</ActionsLinkButton>
</Menu.Item>
<Menu.Item>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => setShowEditAccountModal(true)}
>
<PencilIcon className="h-4 w-4" />
<span className="ml-2">{t('edit-account')}</span>
</ActionsLinkButton>
</Menu.Item>
<Menu.Item>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => setShowDelegateModal(true)}
>
<UsersIcon className="h-4 w-4" />
<span className="ml-2">{t('delegate-account')}</span>
</ActionsLinkButton>
</Menu.Item>
<Menu.Item>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => setShowCloseAccountModal(true)}
>
<TrashIcon className="h-4 w-4" />
<span className="ml-2">{t('close-account')}</span>
</ActionsLinkButton>
</Menu.Item>
</Menu.Items>
</Transition>
</div>
)}
</Menu>
</div>
)}
{showCloseAccountModal ? (
<CloseAccountModal
isOpen={showCloseAccountModal}
@ -203,23 +195,3 @@ const AccountActions = () => {
}
export default AccountActions
const ActionsButton = ({
children,
mangoAccount,
onClick,
}: {
children: ReactNode
mangoAccount: MangoAccount
onClick: () => void
}) => {
return (
<LinkButton
className="whitespace-nowrap font-normal no-underline md:hover:text-th-fgd-1"
disabled={!mangoAccount}
onClick={onClick}
>
{children}
</LinkButton>
)
}

View File

@ -1,7 +1,4 @@
import {
MangoAccount,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { useMemo, useState } from 'react'
import mangoStore from '@store/mangoStore'
@ -15,39 +12,41 @@ const AccountChart = ({
chartToShow,
data,
hideChart,
mangoAccount,
yKey,
}: {
chartToShow: string
data: Array<any>
hideChart: () => void
mangoAccount: MangoAccount
yKey: string
}) => {
const { t } = useTranslation('common')
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const [daysToShow, setDaysToShow] = useState<string>('1')
const loading = mangoStore((s) => s.mangoAccount.stats.performance.loading)
const handleDaysToShow = async (days: string) => {
await actions.fetchAccountPerformance(
mangoAccount.publicKey.toString(),
parseInt(days)
)
setDaysToShow(days)
const mangoAccount = mangoStore.getState().mangoAccount.current
if (mangoAccount) {
await actions.fetchAccountPerformance(
mangoAccount.publicKey.toString(),
parseInt(days)
)
setDaysToShow(days)
}
}
const currentValue = useMemo(() => {
if (chartToShow === 'account-value') {
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const group = mangoStore.getState().group
if (group && mangoAccount && chartToShow === 'account-value') {
const currentAccountValue = toUiDecimalsForQuote(
mangoAccount.getEquity(group!)!.toNumber()
mangoAccount.getEquity(group).toNumber()
)
const time = Date.now()
return [{ account_equity: currentAccountValue, time: time }]
}
return []
}, [chartToShow, mangoAccount])
}, [chartToShow])
return (
<DetailedAreaChart

View File

@ -56,8 +56,8 @@ const AccountPage = () => {
const { t } = useTranslation('common')
// const { connected } = useWallet()
const group = mangoStore.getState().group
const { mangoAccount } = useMangoAccount()
const actions = mangoStore((s) => s.actions)
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const actions = mangoStore.getState().actions
const loadPerformanceData = mangoStore(
(s) => s.mangoAccount.stats.performance.loading
)
@ -76,7 +76,7 @@ const AccountPage = () => {
const [showExpandChart, setShowExpandChart] = useState<boolean>(false)
const { theme } = useTheme()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const isMobile = width ? width < breakpoints.md : false
// const tourSettings = mangoStore((s) => s.settings.tours)
// const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY)
const [animationSettings] = useLocalStorageState(
@ -85,12 +85,11 @@ const AccountPage = () => {
)
useEffect(() => {
if (mangoAccount) {
const pubKey = mangoAccount.publicKey.toString()
actions.fetchAccountPerformance(pubKey, 1)
actions.fetchAccountInterestTotals(pubKey)
if (mangoAccountAddress) {
actions.fetchAccountPerformance(mangoAccountAddress, 1)
actions.fetchAccountInterestTotals(mangoAccountAddress)
}
}, [actions, mangoAccount])
}, [actions, mangoAccountAddress])
useEffect(() => {
if (mangoAccount && performanceData.length && !chartToShow) {
@ -222,7 +221,7 @@ const AccountPage = () => {
return !chartToShow ? (
<>
<div className="flex flex-col border-b-0 border-th-bkg-3 px-6 py-3 lg:flex-row lg:items-center lg:justify-between lg:border-b">
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-6">
<div className="flex flex-col md:flex-row md:items-center md:space-x-6">
<div id="account-step-three">
<Tooltip
maxWidth="20rem"
@ -243,7 +242,7 @@ const AccountPage = () => {
play
delay={0.05}
duration={1}
numbers={formatFixedDecimals(accountValue, true)}
numbers={formatFixedDecimals(accountValue, true, true)}
/>
) : (
<FlipNumbers
@ -256,7 +255,7 @@ const AccountPage = () => {
/>
)
) : (
<span>{formatFixedDecimals(accountValue, true)}</span>
<span>{formatFixedDecimals(accountValue, true, true)}</span>
)}
</div>
<div className="flex items-center space-x-1.5">
@ -267,7 +266,7 @@ const AccountPage = () => {
{!loadPerformanceData ? (
mangoAccount && performanceData.length ? (
<div
className="relative mt-4 flex h-44 items-end sm:mt-0 sm:h-24 sm:w-48"
className="relative mt-4 flex h-44 items-end md:mt-0 md:h-24 md:w-48"
onMouseEnter={() =>
onHoverMenu(showExpandChart, 'onMouseEnter')
}
@ -308,8 +307,8 @@ const AccountPage = () => {
</div>
) : null
) : (
<SheenLoader className="mt-4 flex flex-1 sm:mt-0">
<div className="h-40 w-full rounded-md bg-th-bkg-2 sm:h-24 sm:w-48" />
<SheenLoader className="mt-4 flex flex-1 md:mt-0">
<div className="h-40 w-full rounded-md bg-th-bkg-2 md:h-24 md:w-48" />
</SheenLoader>
)}
</div>
@ -318,7 +317,7 @@ const AccountPage = () => {
</div>
</div>
<div className="grid grid-cols-5 border-b border-th-bkg-3">
<div className="col-span-4 flex border-t border-th-bkg-3 py-3 pl-6 lg:col-span-1 lg:border-t-0">
<div className="col-span-5 flex border-t border-th-bkg-3 py-3 pl-6 lg:col-span-1 lg:border-t-0">
<div id="account-step-four">
<Tooltip
maxWidth="20rem"
@ -364,7 +363,7 @@ const AccountPage = () => {
</p>
</div>
</div>
<div className="col-span-4 flex border-t border-th-bkg-3 py-3 pl-6 lg:col-span-1 lg:border-l lg:border-t-0">
<div className="col-span-5 flex border-t border-th-bkg-3 py-3 pl-6 lg:col-span-1 lg:border-l lg:border-t-0">
<div id="account-step-five">
<Tooltip
content="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."
@ -382,6 +381,7 @@ const AccountPage = () => {
toUiDecimalsForQuote(
mangoAccount.getCollateralValue(group).toNumber()
),
false,
true
)
: `$${(0).toFixed(2)}`}
@ -402,6 +402,7 @@ const AccountPage = () => {
.getAssetsValue(group, HealthType.init)
.toNumber()
),
false,
true
)
: `$${(0).toFixed(2)}`}
@ -413,7 +414,7 @@ const AccountPage = () => {
<div className="col-span-5 flex border-t border-th-bkg-3 py-3 pl-6 lg:col-span-1 lg:border-l lg:border-t-0">
<div id="account-step-six">
<Tooltip
content="Total position size divided by total collateral."
content="Total assets value divided by account equity value."
maxWidth="20rem"
placement="bottom"
delay={250}
@ -428,7 +429,7 @@ const AccountPage = () => {
</div>
</div>
<button
className={`col-span-4 flex items-center justify-between border-t border-th-bkg-3 py-3 pl-6 pr-4 lg:col-span-1 lg:border-l lg:border-t-0 ${
className={`col-span-5 flex items-center justify-between border-t border-th-bkg-3 py-3 pl-6 pr-4 lg:col-span-1 lg:border-l lg:border-t-0 ${
performanceData.length > 4
? 'default-transition cursor-pointer md:hover:bg-th-bkg-2'
: 'cursor-default'
@ -446,7 +447,7 @@ const AccountPage = () => {
</p>
</Tooltip>
<p className="mt-1 mb-0.5 text-left text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
{formatFixedDecimals(accountPnl, true)}
{formatFixedDecimals(accountPnl, true, true)}
</p>
<div className="flex space-x-1">
<Change change={oneDayPnlChange} prefix="$" size="small" />
@ -458,7 +459,7 @@ const AccountPage = () => {
) : null}
</button>
<button
className={`col-span-4 flex items-center justify-between border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left lg:col-span-1 lg:border-l lg:border-t-0 ${
className={`col-span-5 flex items-center justify-between border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left lg:col-span-1 lg:border-l lg:border-t-0 ${
interestTotalValue > 1 || interestTotalValue < -1
? 'default-transition cursor-pointer md:hover:bg-th-bkg-2'
: 'cursor-default'
@ -477,7 +478,7 @@ const AccountPage = () => {
</p>
</Tooltip>
<p className="mt-1 mb-0.5 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
{formatFixedDecimals(interestTotalValue, true)}
{formatFixedDecimals(interestTotalValue, true, true)}
</p>
<div className="flex space-x-1">
<Change change={oneDayInterestChange} prefix="$" size="small" />
@ -501,7 +502,6 @@ const AccountPage = () => {
chartToShow="account-value"
data={performanceData}
hideChart={handleHideChart}
mangoAccount={mangoAccount!}
yKey="account_equity"
/>
) : chartToShow === 'pnl' ? (
@ -509,7 +509,6 @@ const AccountPage = () => {
chartToShow="pnl"
data={performanceData}
hideChart={handleHideChart}
mangoAccount={mangoAccount!}
yKey="pnl"
/>
) : (
@ -522,7 +521,6 @@ const AccountPage = () => {
time: d.time,
}))}
hideChart={handleHideChart}
mangoAccount={mangoAccount!}
yKey="interest_value"
/>
)}

View File

@ -1,5 +1,4 @@
import { useMemo, useState } from 'react'
import mangoStore from '@store/mangoStore'
import TabButtons from '../shared/TabButtons'
import TokenList from '../TokenList'
import SwapHistoryTable from '../swap/SwapHistoryTable'
@ -43,8 +42,6 @@ const AccountTabs = () => {
}
const TabContent = ({ activeTab }: { activeTab: string }) => {
const swapHistory = mangoStore((s) => s.mangoAccount.stats.swapHistory.data)
const loading = mangoStore((s) => s.mangoAccount.stats.swapHistory.loading)
const unsettledSpotBalances = useUnsettledSpotBalances()
const unsettledPerpPositions = useUnsettledPerpPositions()
switch (activeTab) {
@ -53,7 +50,7 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
case 'activity:activity':
return <ActivityFeed />
case 'swap:swap-history':
return <SwapHistoryTable swapHistory={swapHistory} loading={loading} />
return <SwapHistoryTable />
case 'trade:unsettled':
return (
<UnsettledTrades

View File

@ -30,11 +30,15 @@ const ActionTokenItem = ({
return (
<button
className="default-transition flex grid w-full auto-cols-fr grid-flow-col items-center rounded-md border border-th-bkg-4 bg-th-bkg-1 px-4 py-3 disabled:cursor-not-allowed disabled:opacity-30 md:hover:border-th-fgd-4 md:disabled:hover:border-th-bkg-4"
className="default-transition flex w-full items-center rounded-md border border-th-bkg-4 bg-th-bkg-1 px-4 py-3 disabled:cursor-not-allowed disabled:opacity-30 md:hover:border-th-fgd-4 md:disabled:hover:border-th-bkg-4"
onClick={() => onSelect(name)}
disabled={customValue <= 0}
>
<div className="flex items-center">
<div
className={`flex ${
!showBorrowRates && !showDepositRates ? 'w-1/2' : 'w-1/4'
} items-center`}
>
<div className="mr-2.5 flex flex-shrink-0 items-center">
<Image
alt=""
@ -46,21 +50,23 @@ const ActionTokenItem = ({
<p className="text-th-fgd-1">{name}</p>
</div>
{showDepositRates ? (
<div className="text-right font-mono">
<div className="w-1/4 text-right font-mono">
<p className="text-th-up">
{formatDecimal(bank.getDepositRate().toNumber(), 2)}%
</p>
</div>
) : null}
{showBorrowRates ? (
<div className="text-right font-mono">
<div className="w-1/4 text-right font-mono">
<p className="text-th-down">
{formatDecimal(bank.getBorrowRate().toNumber(), 2)}%
</p>
</div>
) : null}
<div className="text-right">
<p className="font-mono text-th-fgd-1">{formatDecimal(customValue)}</p>
<div className="w-1/2 pl-3 text-right">
<p className="truncate font-mono text-th-fgd-1">
{formatDecimal(customValue)}
</p>
</div>
</button>
)

View File

@ -0,0 +1,25 @@
import { MangoAccount } from '@blockworks-foundation/mango-v4'
import { LinkButton } from '@components/shared/Button'
import { ReactNode } from 'react'
const ActionsLinkButton = ({
children,
mangoAccount,
onClick,
}: {
children: ReactNode
mangoAccount: MangoAccount
onClick: () => void
}) => {
return (
<LinkButton
className="w-full whitespace-nowrap text-left font-normal no-underline md:hover:text-th-fgd-1"
disabled={!mangoAccount}
onClick={onClick}
>
{children}
</LinkButton>
)
}
export default ActionsLinkButton

View File

@ -1,4 +1,3 @@
import Checkbox from '@components/forms/Checkbox'
import MangoDateRangePicker from '@components/forms/DateRangePicker'
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
@ -26,14 +25,6 @@ import { PREFERRED_EXPLORER_KEY } from 'utils/constants'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import ActivityFeedTable from './ActivityFeedTable'
interface Filters {
deposit: boolean
liquidate_token_with_token: boolean
perp_trade: boolean
swap: boolean
withdraw: boolean
}
interface AdvancedFilters {
symbol: string[]
'start-date': string
@ -42,14 +33,6 @@ interface AdvancedFilters {
'usd-upper': string
}
const DEFAULT_FILTERS = {
deposit: true,
liquidate_token_with_token: true,
perp_trade: true,
swap: true,
withdraw: true,
}
const DEFAULT_ADVANCED_FILTERS = {
symbol: [],
'start-date': '',
@ -60,47 +43,33 @@ const DEFAULT_ADVANCED_FILTERS = {
const DEFAULT_PARAMS = [
'deposit',
'liquidate_token_with_token',
'swap',
'perp_trade',
'liquidate_token_with_token',
'openbook_trade',
'swap',
'withdraw',
]
const ActivityFeed = () => {
const activityFeed = mangoStore((s) => s.activityFeed.feed)
const actions = mangoStore((s) => s.actions)
const { mangoAccount } = useMangoAccount()
const actions = mangoStore.getState().actions
const { mangoAccountAddress } = useMangoAccount()
const [showActivityDetail, setShowActivityDetail] = useState(null)
const [filters, setFilters] = useState<Filters>(DEFAULT_FILTERS)
const [advancedFilters, setAdvancedFilters] = useState<AdvancedFilters>(
DEFAULT_ADVANCED_FILTERS
)
const [params, setParams] = useState<string[]>(DEFAULT_PARAMS)
useEffect(() => {
if (mangoAccount) {
const pubKey = mangoAccount.publicKey.toString()
actions.fetchActivityFeed(pubKey)
if (mangoAccountAddress) {
actions.fetchActivityFeed(mangoAccountAddress)
}
}, [actions, mangoAccount])
}, [actions, mangoAccountAddress])
const handleShowActivityDetails = (activity: any) => {
setShowActivityDetail(activity)
}
const updateFilters = (e: ChangeEvent<HTMLInputElement>, filter: string) => {
setFilters({ ...filters, [filter]: e.target.checked })
let newParams: string[] = DEFAULT_PARAMS
if (params.includes(filter)) {
newParams = params.filter((p) => p !== filter)
} else {
newParams = [...params, filter]
}
setParams(newParams)
}
const advancedParamsString = useMemo(() => {
let advancedParams = ''
Object.entries(advancedFilters).map((entry) => {
@ -112,7 +81,7 @@ const ActivityFeed = () => {
}, [advancedFilters])
const queryParams = useMemo(() => {
return params.length === 5
return !params.length || params.length === 6
? advancedParamsString
: `&activity-type=${params.toString()}${advancedParamsString}`
}, [advancedParamsString, params])
@ -120,11 +89,9 @@ const ActivityFeed = () => {
return !showActivityDetail ? (
<>
<ActivityFilters
filters={filters}
setFilters={setFilters}
updateFilters={updateFilters}
filters={params}
setFilters={setParams}
params={queryParams}
setParams={setParams}
advancedFilters={advancedFilters}
setAdvancedFilters={setAdvancedFilters}
/>
@ -147,29 +114,24 @@ export default ActivityFeed
const ActivityFilters = ({
filters,
setFilters,
updateFilters,
params,
setParams,
advancedFilters,
setAdvancedFilters,
}: {
filters: Filters
setFilters: (x: Filters) => void
updateFilters: (e: ChangeEvent<HTMLInputElement>, filter: string) => void
filters: string[]
setFilters: (x: string[]) => void
params: string
setParams: (x: string[]) => void
advancedFilters: AdvancedFilters
setAdvancedFilters: (x: AdvancedFilters) => void
}) => {
const { t } = useTranslation(['common', 'activity'])
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const loadActivityFeed = mangoStore((s) => s.activityFeed.loading)
const { mangoAccount } = useMangoAccount()
const { mangoAccountAddress } = useMangoAccount()
const [showMobileFilters, setShowMobileFilters] = useState(false)
const [hasFilters, setHasFilters] = useState(false)
const handleUpdateResults = useCallback(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const set = mangoStore.getState().set
if (params) {
setHasFilters(true)
@ -180,33 +142,31 @@ const ActivityFilters = ({
s.activityFeed.feed = []
s.activityFeed.loading = true
})
if (mangoAccount) {
actions.fetchActivityFeed(mangoAccount.publicKey.toString(), 0, params)
if (mangoAccountAddress) {
actions.fetchActivityFeed(mangoAccountAddress, 0, params)
}
}, [actions, params])
}, [actions, params, mangoAccountAddress])
const handleResetFilters = useCallback(async () => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const set = mangoStore.getState().set
setHasFilters(false)
set((s) => {
s.activityFeed.feed = []
s.activityFeed.loading = true
})
if (mangoAccount) {
await actions.fetchActivityFeed(mangoAccount.publicKey.toString())
if (mangoAccountAddress) {
await actions.fetchActivityFeed(mangoAccountAddress)
setAdvancedFilters(DEFAULT_ADVANCED_FILTERS)
setFilters(DEFAULT_FILTERS)
setParams(DEFAULT_PARAMS)
setFilters(DEFAULT_PARAMS)
}
}, [actions])
}, [actions, mangoAccountAddress])
const handleUpdateMobileResults = () => {
handleUpdateResults()
setShowMobileFilters(false)
}
return mangoAccount ? (
return mangoAccountAddress ? (
<Disclosure>
<div className="relative">
{hasFilters ? (
@ -225,7 +185,7 @@ const ActivityFilters = ({
<div
onClick={() => setShowMobileFilters(!showMobileFilters)}
role="button"
className={`default-transition w-full bg-th-bkg-2 p-4 hover:bg-th-bkg-3 md:px-6`}
className={`default-transition h-12 w-full bg-th-bkg-2 px-4 hover:bg-th-bkg-3 md:px-6`}
>
<Disclosure.Button
className={`flex h-full w-full items-center justify-between rounded-none`}
@ -245,16 +205,11 @@ const ActivityFilters = ({
</div>
</div>
<Disclosure.Panel className="bg-th-bkg-2 px-6 pb-6">
<div className="py-4">
<Label text={t('activity:activity-type')} />
<ActivityTypeFiltersForm
filters={filters}
updateFilters={updateFilters}
/>
</div>
<AdvancedFiltersForm
<FiltersForm
advancedFilters={advancedFilters}
setAdvancedFilters={setAdvancedFilters}
filters={filters}
setFilters={setFilters}
/>
<Button
className="w-full md:w-auto"
@ -268,69 +223,19 @@ const ActivityFilters = ({
) : null
}
const ActivityTypeFiltersForm = ({
filters,
updateFilters,
}: {
filters: Filters
updateFilters: (e: ChangeEvent<HTMLInputElement>, filter: string) => void
}) => {
const { t } = useTranslation('activity')
return (
<div className="flex w-full flex-col space-y-3 md:flex-row md:space-y-0">
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.deposit}
onChange={(e) => updateFilters(e, 'deposit')}
>
<span className="text-sm">{t('deposits')}</span>
</Checkbox>
</div>
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.withdraw}
onChange={(e) => updateFilters(e, 'withdraw')}
>
<span className="text-sm">{t('withdrawals')}</span>
</Checkbox>
</div>
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.swap}
onChange={(e) => updateFilters(e, 'swap')}
>
<span className="text-sm">{t('swaps')}</span>
</Checkbox>
</div>
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.perp_trade}
onChange={(e) => updateFilters(e, 'perp_trade')}
>
<span className="text-sm">{t('perps')}</span>
</Checkbox>
</div>
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.liquidate_token_with_token}
onChange={(e) => updateFilters(e, 'liquidate_token_with_token')}
>
<span className="text-sm">{t('liquidations')}</span>
</Checkbox>
</div>
</div>
)
}
interface AdvancedFiltersFormProps {
interface FiltersFormProps {
advancedFilters: any
setAdvancedFilters: (x: any) => void
filters: string[]
setFilters: (x: string[]) => void
}
const AdvancedFiltersForm = ({
const FiltersForm = ({
advancedFilters,
setAdvancedFilters,
}: AdvancedFiltersFormProps) => {
filters,
setFilters,
}: FiltersFormProps) => {
const { t } = useTranslation(['common', 'activity'])
const { group } = useMangoGroup()
const [dateFrom, setDateFrom] = useState<Date | null>(null)
@ -352,13 +257,21 @@ const AdvancedFiltersForm = ({
}
}, [])
const toggleOption = (option: string) => {
const toggleTypeOption = (option: string) => {
if (filters.includes(option)) {
setFilters(filters.filter((opt) => opt !== option))
} else {
setFilters(filters.concat(option))
}
}
const toggleSymbolOption = (option: string) => {
setAdvancedFilters((prevSelected: any) => {
const newSelections = prevSelected.symbol ? [...prevSelected.symbol] : []
if (newSelections.includes(option)) {
return {
...prevSelected,
symbol: newSelections.filter((item) => item != option),
symbol: newSelections.filter((item) => item !== option),
}
} else {
newSelections.push(option)
@ -390,8 +303,8 @@ const AdvancedFiltersForm = ({
if (valueFrom && valueTo) {
setAdvancedFilters({
...advancedFilters,
'usd-lower': valueFrom,
'usd-upper': valueTo,
'usd-lower': Math.floor(valueFrom),
'usd-upper': Math.ceil(valueTo),
})
} else {
setAdvancedFilters({
@ -404,13 +317,23 @@ const AdvancedFiltersForm = ({
return (
<>
<Label text={t('tokens')} />
<div className="w-full lg:w-1/2 lg:pr-4">
<MultiSelectDropdown
options={symbols}
selected={advancedFilters.symbol || []}
toggleOption={toggleOption}
/>
<div className="grid grid-cols-2 gap-x-8 pt-4">
<div className="col-span-2 lg:col-span-1">
<Label text={t('activity:activity-type')} />
<MultiSelectDropdown
options={DEFAULT_PARAMS}
selected={filters}
toggleOption={toggleTypeOption}
/>
</div>
<div className="col-span-2 lg:col-span-1">
<Label text={t('tokens')} />
<MultiSelectDropdown
options={symbols}
selected={advancedFilters.symbol || []}
toggleOption={toggleSymbolOption}
/>
</div>
</div>
<div className="my-4 w-full">
<MangoDateRangePicker
@ -424,7 +347,7 @@ const AdvancedFiltersForm = ({
<div className="w-full">
<Label text={t('activity:value-from')} />
<Input
type="text"
type="number"
placeholder="0.00"
value={valueFrom}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -438,7 +361,7 @@ const AdvancedFiltersForm = ({
<div className="w-full">
<Label text={t('activity:value-to')} />
<Input
type="text"
type="number"
placeholder="0.00"
value={valueTo || ''}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -506,10 +429,8 @@ const ActivityDetails = ({
<p className="mb-0.5 text-sm">{t('activity:asset-liquidated')}</p>
<p className="font-mono text-th-fgd-1">
{formatDecimal(asset_amount)}{' '}
<span className="font-body tracking-wide">{asset_symbol}</span>
<span className="ml-2 font-body tracking-wide text-th-fgd-3">
at
</span>{' '}
<span className="font-body tracking-wider">{asset_symbol}</span>
<span className="ml-2 font-body text-th-fgd-3">at</span>{' '}
{formatFixedDecimals(asset_price, true)}
</p>
<p className="font-mono text-xs text-th-fgd-3">
@ -520,10 +441,8 @@ const ActivityDetails = ({
<p className="mb-0.5 text-sm">{t('activity:asset-returned')}</p>
<p className="font-mono text-th-fgd-1">
{formatDecimal(liab_amount)}{' '}
<span className="font-body tracking-wide">{liab_symbol}</span>
<span className="ml-2 font-body tracking-wide text-th-fgd-3">
at
</span>{' '}
<span className="font-body tracking-wider">{liab_symbol}</span>
<span className="ml-2 font-body text-th-fgd-3">at</span>{' '}
{formatFixedDecimals(liab_price, true)}
</p>
<p className="font-mono text-xs text-th-fgd-3">

View File

@ -7,7 +7,6 @@ import { Transition } from '@headlessui/react'
import {
ChevronDownIcon,
ChevronRightIcon,
LinkIcon,
NoSymbolIcon,
} from '@heroicons/react/20/solid'
import mangoStore, { LiquidationFeedItem } from '@store/mangoStore'
@ -22,6 +21,13 @@ import { PREFERRED_EXPLORER_KEY } from 'utils/constants'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import { breakpoints } from 'utils/theme'
const formatFee = (value: number) => {
return value.toLocaleString(undefined, {
minimumSignificantDigits: 1,
maximumSignificantDigits: 1,
})
}
const ActivityFeedTable = ({
activityFeed,
handleShowActivityDetails,
@ -32,8 +38,8 @@ const ActivityFeedTable = ({
params: string
}) => {
const { t } = useTranslation(['common', 'activity'])
const { mangoAccount } = useMangoAccount()
const actions = mangoStore((s) => s.actions)
const { mangoAccountAddress } = useMangoAccount()
const actions = mangoStore.getState().actions
const loadActivityFeed = mangoStore((s) => s.activityFeed.loading)
const [offset, setOffset] = useState(0)
const [preferredExplorer] = useLocalStorageState(
@ -44,19 +50,14 @@ const ActivityFeedTable = ({
const showTableView = width ? width > breakpoints.md : false
const handleShowMore = useCallback(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const set = mangoStore.getState().set
set((s) => {
s.activityFeed.loading = true
})
if (!mangoAccount) return
if (!mangoAccountAddress) return
setOffset(offset + 25)
actions.fetchActivityFeed(
mangoAccount.publicKey.toString(),
offset + 25,
params
)
}, [actions, offset, params])
actions.fetchActivityFeed(mangoAccountAddress, offset + 25, params)
}, [actions, offset, params, mangoAccountAddress])
const getCreditAndDebit = (activity: any) => {
const { activity_type } = activity
@ -107,6 +108,18 @@ const ActivityFeedTable = ({
credit = { value: quantity, symbol: perp_market }
debit = { value: formatDecimal(quantity * price * -1), symbol: 'USDC' }
}
if (activity_type === 'openbook_trade') {
const { side, price, size, base_symbol, quote_symbol } =
activity.activity_details
credit =
side === 'buy'
? { value: formatDecimal(size), symbol: base_symbol }
: { value: formatDecimal(size * price), symbol: quote_symbol }
debit =
side === 'buy'
? { value: formatDecimal(size * price * -1), symbol: quote_symbol }
: { value: formatDecimal(size * -1), symbol: base_symbol }
}
return { credit, debit }
}
@ -136,166 +149,183 @@ const ActivityFeedTable = ({
activity.activity_details
value = quantity * price + maker_fee + taker_fee
}
if (activity_type === 'openbook_trade') {
const { price, size } = activity.activity_details
value = price * size
}
return value
}
return mangoAccount ? (
activityFeed.length || loadActivityFeed ? (
<>
{showTableView ? (
<Table className="min-w-full">
<thead className="sticky top-0 z-10">
<TrHead>
<Th className="bg-th-bkg-1 text-left">{t('date')}</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:activity')}
</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:credit')}
</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:debit')}
</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:activity-value')}
</Th>
<Th className="bg-th-bkg-1 text-right">{t('explorer')}</Th>
</TrHead>
</thead>
<tbody>
{activityFeed.map((activity: any) => {
const { activity_type, block_datetime } = activity
const { signature } = activity.activity_details
const isLiquidation =
activity_type === 'liquidate_token_with_token'
const activityName = isLiquidation
? 'spot-liquidation'
: activity_type
const amounts = getCreditAndDebit(activity)
const value = getValue(activity)
return (
<TrBody
key={signature}
className={`default-transition text-sm hover:bg-th-bkg-2 ${
isLiquidation ? 'cursor-pointer' : ''
const getFee = (activity: any) => {
const { activity_type } = activity
let fee = '0'
if (activity_type === 'swap') {
const { loan_origination_fee, swap_in_symbol } = activity.activity_details
fee = loan_origination_fee
? `${formatFee(loan_origination_fee)} ${swap_in_symbol}`
: '0'
}
if (activity_type === 'perp_trade') {
const { maker_fee, taker_fee } = activity.activity_details
fee = `${formatFee(maker_fee + taker_fee)} USDC`
}
if (activity_type === 'openbook_trade') {
const { fee_cost, quote_symbol } = activity.activity_details
fee = `${formatFee(fee_cost)} ${quote_symbol}`
}
return fee
}
return mangoAccountAddress && (activityFeed.length || loadActivityFeed) ? (
<>
{showTableView ? (
<Table className="min-w-full">
<thead className="sticky top-0 z-10">
<TrHead>
<Th className="bg-th-bkg-1 text-left">{t('date')}</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:activity')}
</Th>
<Th className="bg-th-bkg-1 text-right">{t('activity:credit')}</Th>
<Th className="bg-th-bkg-1 text-right">{t('activity:debit')}</Th>
<Th className="bg-th-bkg-1 text-right">{t('fee')}</Th>
<Th className="bg-th-bkg-1 text-right">
{t('activity:activity-value')}
</Th>
<Th className="bg-th-bkg-1 text-right">{t('explorer')}</Th>
</TrHead>
</thead>
<tbody>
{activityFeed.map((activity: any) => {
const { activity_type, block_datetime } = activity
const { signature } = activity.activity_details
const isLiquidation =
activity_type === 'liquidate_token_with_token'
const isOpenbook = activity_type === 'openbook_trade'
const amounts = getCreditAndDebit(activity)
const value = getValue(activity)
const fee = getFee(activity)
return (
<TrBody
key={signature}
className={`default-transition text-sm hover:bg-th-bkg-2 ${
isLiquidation ? 'cursor-pointer' : ''
}`}
onClick={
isLiquidation
? () => handleShowActivityDetails(activity)
: undefined
}
>
<Td>
<p className="font-body tracking-wider">
{dayjs(block_datetime).format('ddd D MMM')}
</p>
<p className="text-xs text-th-fgd-3">
{dayjs(block_datetime).format('h:mma')}
</p>
</Td>
<Td className="text-right">
{t(`activity:${activity_type}`)}
</Td>
<Td className="text-right font-mono">
{amounts.credit.value}{' '}
<span className="font-body text-th-fgd-3">
{amounts.credit.symbol}
</span>
</Td>
<Td className="text-right font-mono">
{amounts.debit.value}{' '}
<span className="font-body text-th-fgd-3">
{amounts.debit.symbol}
</span>
</Td>
<Td className="text-right font-mono">{fee}</Td>
<Td
className={`text-right font-mono ${
activity_type === 'swap' ||
activity_type === 'perp_trade' ||
isOpenbook
? 'text-th-fgd-2'
: value >= 0
? 'text-th-up'
: 'text-th-down'
}`}
onClick={
isLiquidation
? () => handleShowActivityDetails(activity)
: undefined
}
>
<Td>
<p className="font-body tracking-wide">
{dayjs(block_datetime).format('ddd D MMM')}
</p>
<p className="text-xs text-th-fgd-3">
{dayjs(block_datetime).format('h:mma')}
</p>
</Td>
<Td className="text-right">
{t(`activity:${activityName}`)}
</Td>
<Td className="text-right font-mono">
{amounts.credit.value}{' '}
<span className="font-body tracking-wide text-th-fgd-3">
{amounts.credit.symbol}
</span>
</Td>
<Td className="text-right font-mono">
{amounts.debit.value}{' '}
<span className="font-body tracking-wide text-th-fgd-3">
{amounts.debit.symbol}
</span>
</Td>
<Td
className={`text-right font-mono ${
activityName === 'swap' || activityName === 'perp_trade'
? 'text-th-fgd-2'
: value >= 0
? 'text-th-up'
: 'text-th-down'
}`}
>
{value > 0 &&
activityName !== 'swap' &&
activityName !== 'perp_trade'
? '+'
: ''}
{formatFixedDecimals(value, true)}
</Td>
<Td>
{activity_type !== 'liquidate_token_with_token' ? (
<div className="flex items-center justify-end">
<Tooltip
content={`View on ${t(
`settings:${preferredExplorer.name}`
)}`}
placement="top-end"
{value > 0 &&
activity_type !== 'swap' &&
activity_type !== 'perp_trade' &&
!isOpenbook
? '+'
: ''}
{formatFixedDecimals(value, true)}
</Td>
<Td>
{activity_type !== 'liquidate_token_with_token' ? (
<div className="flex items-center justify-end">
<Tooltip
content={`View on ${t(
`settings:${preferredExplorer.name}`
)}`}
placement="top-end"
>
<a
href={`${preferredExplorer.url}${signature}`}
target="_blank"
rel="noopener noreferrer"
>
<a
href={`${preferredExplorer.url}${signature}`}
target="_blank"
rel="noopener noreferrer"
>
<div className="h-6 w-6">
<Image
alt=""
width="24"
height="24"
src={`/explorer-logos/${preferredExplorer.name}.png`}
/>
</div>
</a>
</Tooltip>
</div>
) : (
<div className="flex items-center justify-end">
<ChevronRightIcon className="h-6 w-6 text-th-fgd-3" />
</div>
)}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div>
{activityFeed.map((activity: any) => (
<MobileActivityFeedItem
activity={activity}
getValue={getValue}
key={activity.activity_details.signature}
/>
))}
</div>
)}
{loadActivityFeed ? (
<div className="mt-4 space-y-1.5">
{[...Array(4)].map((x, i) => (
<SheenLoader className="mx-4 flex flex-1 md:mx-6" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : null}
{activityFeed.length && activityFeed.length % 25 === 0 ? (
<div className="flex justify-center pt-6">
<LinkButton onClick={handleShowMore}>Show More</LinkButton>
</div>
) : null}
</>
) : (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-1 h-6 w-6 text-th-fgd-4" />
<p>{t('activity:no-activity')}</p>
</div>
)
<div className="h-6 w-6">
<Image
alt=""
width="24"
height="24"
src={`/explorer-logos/${preferredExplorer.name}.png`}
/>
</div>
</a>
</Tooltip>
</div>
) : (
<div className="flex items-center justify-end">
<ChevronRightIcon className="h-6 w-6 text-th-fgd-3" />
</div>
)}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div>
{activityFeed.map((activity: any) => (
<MobileActivityFeedItem
activity={activity}
getValue={getValue}
key={activity.activity_details.signature}
/>
))}
</div>
)}
{loadActivityFeed ? (
<div className="mt-4 space-y-1.5">
{[...Array(4)].map((x, i) => (
<SheenLoader className="mx-4 flex flex-1 md:mx-6" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : null}
{activityFeed.length && activityFeed.length % 25 === 0 ? (
<div className="flex justify-center pt-6">
<LinkButton onClick={handleShowMore}>Show More</LinkButton>
</div>
) : null}
</>
) : (
<div className="flex flex-col items-center p-8">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('activity:connect-activity')}</p>
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('activity:no-activity')}</p>
</div>
)
}
@ -320,7 +350,6 @@ const MobileActivityFeedItem = ({
const isLiquidation = activity_type === 'liquidate_token_with_token'
const isSwap = activity_type === 'swap'
const isPerp = activity_type === 'perp_trade'
const activityName = isLiquidation ? 'spot-liquidation' : activity_type
const value = getValue(activity)
return (
<div key={signature} className="border-b border-th-bkg-3 p-4">
@ -336,7 +365,7 @@ const MobileActivityFeedItem = ({
<div className="flex items-center space-x-4">
<div>
<p className="text-right text-xs">
{t(`activity:${activityName}`)}
{t(`activity:${activity_type}`)}
</p>
<p className="text-right font-mono text-sm text-th-fgd-1">
{isLiquidation ? (
@ -349,7 +378,7 @@ const MobileActivityFeedItem = ({
{ maximumFractionDigits: 6 }
)}
</span>
<span className="font-body tracking-wide text-th-fgd-3">
<span className="font-body text-th-fgd-3">
{activity.activity_details.swap_in_symbol}
</span>
<span className="mx-1 font-body text-th-fgd-3">for</span>
@ -359,7 +388,7 @@ const MobileActivityFeedItem = ({
{ maximumFractionDigits: 6 }
)}
</span>
<span className="font-body tracking-wide text-th-fgd-3">
<span className="font-body text-th-fgd-3">
{activity.activity_details.swap_out_symbol}
</span>
</>
@ -388,7 +417,7 @@ const MobileActivityFeedItem = ({
<span className="mr-1">
{activity.activity_details.quantity}
</span>
<span className="font-body tracking-wide text-th-fgd-3">
<span className="font-body text-th-fgd-3">
{activity.activity_details.symbol}
</span>
</>
@ -439,12 +468,10 @@ const MobileActivityFeedItem = ({
<p className="mb-0.5 text-sm">{t('activity:asset-liquidated')}</p>
<p className="font-mono text-sm text-th-fgd-1">
{formatDecimal(activity.activity_details.asset_amount)}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{activity.activity_details.asset_symbol}
</span>
<span className="ml-2 font-body tracking-wide text-th-fgd-3">
at
</span>{' '}
<span className="ml-2 font-body text-th-fgd-3">at</span>{' '}
{formatFixedDecimals(activity.activity_details.asset_price, true)}
</p>
<p className="font-mono text-xs text-th-fgd-3">
@ -459,12 +486,10 @@ const MobileActivityFeedItem = ({
<p className="mb-0.5 text-sm">{t('activity:asset-returned')}</p>
<p className="font-mono text-sm text-th-fgd-1">
{formatDecimal(activity.activity_details.liab_amount)}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{activity.activity_details.liab_symbol}
</span>
<span className="ml-2 font-body tracking-wide text-th-fgd-3">
at
</span>{' '}
<span className="ml-2 font-body text-th-fgd-3">at</span>{' '}
{formatFixedDecimals(activity.activity_details.liab_price, true)}
</p>
<p className="font-mono text-xs text-th-fgd-3">

View File

@ -59,7 +59,7 @@ const CreateAccountForm = ({
8 // perpOoCount
)
if (tx) {
const pk = wallet!.adapter.publicKey
const pk = wallet.adapter.publicKey
const mangoAccounts = await client.getMangoAccountsForOwner(group, pk!)
const reloadedMangoAccounts = await Promise.all(
mangoAccounts.map((ma) => ma.reloadAccountData(client))

View File

@ -3,7 +3,7 @@ import {
HealthType,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import { formatDecimal, formatFixedDecimals } from '../../utils/numbers'
import { formatFixedDecimals } from '../../utils/numbers'
import { useTranslation } from 'next-i18next'
import useMangoAccount from 'hooks/useMangoAccount'
@ -40,15 +40,15 @@ const MangoAccountSummary = () => {
<div>
<p className="text-sm text-th-fgd-3">{t('account-value')}</p>
<p className="font-mono text-sm text-th-fgd-1">
$
{group && mangoAccount
? formatDecimal(
? formatFixedDecimals(
toUiDecimalsForQuote(
mangoAccount.getEquity(group).toNumber()
),
2
false,
true
)
: (0).toFixed(2)}
: '$0.00'}
</p>
</div>
<div>
@ -59,9 +59,10 @@ const MangoAccountSummary = () => {
toUiDecimalsForQuote(
mangoAccount.getCollateralValue(group).toNumber()
),
false,
true
)
: `$${(0).toFixed(2)}`}
: '$0.00'}
</p>
</div>
<div>
@ -74,9 +75,10 @@ const MangoAccountSummary = () => {
.getAssetsValue(group, HealthType.init)
.toNumber()
),
false,
true
)
: `$${(0).toFixed(2)}`}
: '$0.00'}
</p>
</div>
{/* <div>

View File

@ -39,15 +39,15 @@ const Checkbox = ({
/>
<div
className={`${
checked && !disabled && !halfState ? 'bg-th-fgd-4' : 'bg-th-bkg-3'
checked && !disabled && !halfState ? 'bg-th-active' : 'bg-th-bkg-4'
} default-transition flex h-4 w-4 flex-shrink-0 cursor-pointer items-center justify-center rounded`}
>
{halfState ? (
<div className="mb-0.5 font-bold"></div>
<div className="mb-0.5 font-bold text-th-bkg-1"></div>
) : (
<CheckIcon
className={`${checked ? 'block' : 'hidden'} h-4 w-4 ${
disabled ? 'text-th-bkg-4' : 'text-th-bkg-1'
disabled ? 'text-th-fgd-4' : 'text-th-bkg-1'
}`}
/>
)}

View File

@ -8,12 +8,14 @@ const MultiSelectDropdown = ({
options,
selected,
toggleOption,
placeholder,
}: {
options: string[]
selected: string[]
toggleOption: (x: string) => void
placeholder?: string
}) => {
const { t } = useTranslation('common')
const { t } = useTranslation(['activity', 'common'])
return (
<Popover className="relative w-full min-w-[120px]">
{({ open }) => (
@ -25,10 +27,15 @@ const MultiSelectDropdown = ({
>
<div className={`flex items-center justify-between`}>
{selected.length ? (
<span>{selected.toString().replace(/,/g, ', ')}</span>
<span>
{selected
.map((v) => t(v))
.toString()
.replace(/,/g, ', ')}
</span>
) : (
<span className="text-th-fgd-4">
{t('activity:select-tokens')}
{placeholder || t('common:select')}
</span>
)}
<ChevronDownIcon
@ -61,7 +68,7 @@ const MultiSelectDropdown = ({
key={option}
onChange={() => toggleOption(option)}
>
{option}
{t(option)}
</Checkbox>
)
})}

View File

@ -19,7 +19,6 @@ import { useTranslation } from 'next-i18next'
import { retryFn } from '../../utils'
import Loading from '../shared/Loading'
import Modal from '@components/shared/Modal'
import { formatFixedDecimals } from 'utils/numbers'
import CreateAccountForm from '@components/account/CreateAccountForm'
import { EnterRightExitLeft } from '@components/shared/Transitions'
import { useRouter } from 'next/router'
@ -47,6 +46,7 @@ const MangoAccountsListModal = ({
const [, setLastAccountViewed] = useLocalStorageStringState(LAST_ACCOUNT_KEY)
const router = useRouter()
const { asPath } = useRouter()
const [submitting, setSubmitting] = useState('')
const handleSelectMangoAccount = async (acc: MangoAccount) => {
const set = mangoStore.getState().set
@ -56,13 +56,14 @@ const MangoAccountsListModal = ({
s.activityFeed.feed = []
s.activityFeed.loading = true
})
setSubmitting(acc.publicKey.toString())
try {
const reloadedMangoAccount = await retryFn(() => acc.reload(client))
actions.fetchOpenOrders(reloadedMangoAccount)
set((s) => {
s.mangoAccount.current = reloadedMangoAccount
})
actions.fetchTradeHistory()
setLastAccountViewed(acc.publicKey.toString())
} catch (e) {
console.warn('Error selecting account', e)
@ -73,6 +74,7 @@ const MangoAccountsListModal = ({
})
} finally {
onClose()
setSubmitting('')
}
}
@ -94,10 +96,9 @@ const MangoAccountsListModal = ({
) : mangoAccounts.length ? (
<div className="thin-scroll mt-4 max-h-[280px] space-y-2 overflow-y-auto">
{mangoAccounts.map((acc) => {
const accountValue = formatFixedDecimals(
toUiDecimalsForQuote(Number(acc.getEquity(group!))),
true
)
const accountValue = toUiDecimalsForQuote(
Number(acc.getEquity(group!))
).toFixed(2)
const maintHealth = acc.getHealthRatioUi(
group!,
HealthType.maint
@ -113,8 +114,10 @@ const MangoAccountsListModal = ({
>
<div className="flex w-full items-center justify-between">
<div className="flex items-center space-x-2.5">
{acc.publicKey.toString() ===
mangoAccount?.publicKey.toString() ? (
{submitting === acc.publicKey.toString() ? (
<Loading className="h-4 w-4" />
) : acc.publicKey.toString() ===
mangoAccount?.publicKey.toString() ? (
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-th-success">
<CheckIcon className="h-4 w-4 text-th-bkg-1" />
</div>
@ -150,7 +153,7 @@ const MangoAccountsListModal = ({
</div>
<div className="flex">
<span className="text-sm text-th-fgd-2">
{accountValue}
${accountValue}
</span>
<span className="mx-2 text-th-fgd-4">|</span>
<div

View File

@ -26,7 +26,11 @@ import {
} from 'react'
import { ALPHA_DEPOSIT_LIMIT } from 'utils/constants'
import { notify } from 'utils/notifications'
import { floorToDecimal, formatFixedDecimals } from 'utils/numbers'
import {
floorToDecimal,
formatDecimal,
formatFixedDecimals,
} from 'utils/numbers'
import ActionTokenList from '../account/ActionTokenList'
import ButtonGroup from '../forms/ButtonGroup'
import Input from '../forms/Input'
@ -34,8 +38,8 @@ import Label from '../forms/Label'
import WalletIcon from '../icons/WalletIcon'
import { walletBalanceForToken } from '../DepositForm'
import ParticlesBackground from '../ParticlesBackground'
import EditNftProfilePic from '../profile/EditNftProfilePic'
import EditProfileForm from '../profile/EditProfileForm'
// import EditNftProfilePic from '../profile/EditNftProfilePic'
// import EditProfileForm from '../profile/EditProfileForm'
import Button, { LinkButton } from '../shared/Button'
import InlineNotification from '../shared/InlineNotification'
import Loading from '../shared/Loading'
@ -43,6 +47,8 @@ import MaxAmountButton from '../shared/MaxAmountButton'
import SolBalanceWarnings from '../shared/SolBalanceWarnings'
import { useEnhancedWallet } from '../wallet/EnhancedWalletProvider'
import Modal from '../shared/Modal'
import NumberFormat, { NumberFormatValues } from 'react-number-format'
import { withValueLimit } from '@components/swap/SwapForm'
const UserSetupModal = ({
isOpen,
@ -63,7 +69,7 @@ const UserSetupModal = ({
const [depositAmount, setDepositAmount] = useState('')
const [submitDeposit, setSubmitDeposit] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const [showEditProfilePic, setShowEditProfilePic] = useState(false)
// const [showEditProfilePic, setShowEditProfilePic] = useState(false)
const walletTokens = mangoStore((s) => s.wallet.tokens)
const { handleConnect } = useEnhancedWallet()
const { maxSolDeposit } = useSolBalance()
@ -135,8 +141,9 @@ const UserSetupModal = ({
})
await actions.reloadMangoAccount()
setShowSetupStep(4)
setSubmitDeposit(false)
onClose()
// setShowSetupStep(4)
} catch (e: any) {
notify({
title: 'Transaction failed',
@ -203,7 +210,9 @@ const UserSetupModal = ({
return { amount: 0, decimals: 0 }
}, [banks, depositToken])
const showInsufficientBalance = tokenMax.amount < Number(depositAmount)
const showInsufficientBalance =
tokenMax.amount < Number(depositAmount) ||
(depositToken === 'SOL' && maxSolDeposit <= 0)
const handleSizePercentage = useCallback(
(percentage: string) => {
@ -223,11 +232,14 @@ const UserSetupModal = ({
}
return (
<Modal isOpen={isOpen} onClose={onClose} fullScreen>
<Modal isOpen={isOpen} onClose={onClose} fullScreen disableOutsideClose>
<div className="radial-gradient-bg grid h-screen overflow-auto text-left lg:grid-cols-2">
<img
className="absolute -bottom-6 right-0 hidden h-auto w-[53%] lg:block xl:w-[57%]"
src="/images/trade.png"
src="/images/trade@0.75x.png"
srcSet="/images/trade@0.75x.png 1157w, /images/trade@1x.png 1542w,
/images/trade@2x.png 3084w"
sizes="(max-width: 1600px) 1157px, (max-width: 2500px) 1542px, 3084px"
alt="next"
/>
<img
@ -238,7 +250,7 @@ const UserSetupModal = ({
<div className="absolute top-0 left-0 z-10 flex h-1.5 w-full flex-grow bg-th-bkg-3">
<div
style={{
width: `${(showSetupStep / 4) * 100}%`,
width: `${(showSetupStep / 3) * 100}%`,
}}
className="flex bg-th-active transition-all duration-700 ease-out"
/>
@ -254,10 +266,6 @@ const UserSetupModal = ({
<CheckCircleIcon className="h-5 w-5 text-th-success" />
<p className="text-base">{t('onboarding:bullet-1')}</p>
</div>
{/* <div className="flex items-center space-x-2">
<CheckCircleIcon className="h-5 w-5 text-th-success" />
<p className="text-base">Deeply liquid markets</p>
</div> */}
<div className="flex items-center space-x-2">
<CheckCircleIcon className="h-5 w-5 text-th-success" />
<p className="text-base">{t('onboarding:bullet-2')}</p>
@ -316,7 +324,7 @@ const UserSetupModal = ({
))}
</div>
<Button
className="mt-10 flex w-44 items-center justify-center"
className="mt-10 flex items-center justify-center"
onClick={handleConnect}
size="large"
>
@ -347,7 +355,7 @@ const UserSetupModal = ({
{t('onboarding:create-account-desc')}
</p>
</div>
<div className="pb-4">
<div className="mb-4">
<Label text={t('account-name')} optional />
<Input
type="text"
@ -361,14 +369,15 @@ const UserSetupModal = ({
charLimit={30}
/>
</div>
<div>
<SolBalanceWarnings className="mt-4" />
<div className="mt-2">
<InlineNotification
type="info"
desc={t('insufficient-sol')}
/>
<div className="mt-10">
<Button
className="mb-6 flex w-44 items-center justify-center"
className="mb-6 flex items-center justify-center"
disabled={maxSolDeposit <= 0}
onClick={handleCreateAccount}
size="large"
@ -381,7 +390,6 @@ const UserSetupModal = ({
</div>
)}
</Button>
<SolBalanceWarnings />
<LinkButton onClick={onClose}>
<span className="default-transition text-th-fgd-4 underline md:hover:text-th-fgd-3 md:hover:no-underline">
{t('onboarding:skip')}
@ -406,6 +414,7 @@ const UserSetupModal = ({
/>
<SolBalanceWarnings
amount={depositAmount}
className="mt-4"
setAmount={setDepositAmount}
selectedToken={depositToken}
/>
@ -414,16 +423,16 @@ const UserSetupModal = ({
<Label text={t('amount')} />
<MaxAmountButton
className="mb-2"
disabled={depositToken === 'SOL' && maxSolDeposit <= 0}
label="Wallet Max"
onClick={() =>
label="Max"
onClick={() => {
setDepositAmount(
floorToDecimal(
tokenMax.amount,
tokenMax.decimals
).toFixed()
)
}
setSizePercentage('100')
}}
value={floorToDecimal(
tokenMax.amount,
tokenMax.decimals
@ -432,7 +441,7 @@ const UserSetupModal = ({
</div>
<div className="mb-6 grid grid-cols-2">
<button
className="col-span-1 flex items-center rounded-lg rounded-r-none border border-r-0 border-th-bkg-4 bg-transparent px-4 hover:bg-transparent"
className="col-span-1 flex items-center rounded-lg rounded-r-none border border-r-0 border-th-input-border bg-th-input-bkg px-4"
onClick={() => setDepositToken('')}
>
<div className="flex w-full items-center justify-between">
@ -450,56 +459,67 @@ const UserSetupModal = ({
<PencilIcon className="ml-2 h-5 w-5 text-th-fgd-3" />
</div>
</button>
<Input
className="col-span-1 w-full rounded-lg rounded-l-none border border-th-bkg-4 bg-transparent p-3 text-right text-xl font-bold tracking-wider text-th-fgd-1 focus:outline-none"
type="text"
name="deposit"
id="deposit"
<NumberFormat
name="amountIn"
id="amountIn"
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={tokenMax.decimals || 6}
className="w-full rounded-lg rounded-l-none border border-th-input-border bg-th-input-bkg p-3 text-right font-mono text-xl text-th-fgd-1 focus:border-th-input-border-hover focus:outline-none md:hover:border-th-input-border-hover"
placeholder="0.00"
value={depositAmount}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setDepositAmount(e.target.value)
}
onValueChange={(e: NumberFormatValues) => {
setDepositAmount(
!Number.isNaN(Number(e.value)) ? e.value : ''
)
}}
isAllowed={withValueLimit}
/>
<div className="col-span-2 mt-2">
<ButtonGroup
activeValue={sizePercentage}
disabled={depositToken === 'SOL' && maxSolDeposit <= 0}
onChange={(p) => handleSizePercentage(p)}
values={['10', '25', '50', '75', '100']}
unit="%"
/>
</div>
</div>
<div className="mb-10 border-y border-th-bkg-3">
<div className="flex justify-between px-2 py-4">
<p>{t('deposit-amount')}</p>
<p className="font-mono text-th-fgd-2">
{depositBank?.uiPrice && depositAmount ? (
<>
{depositAmount}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
depositBank.uiPrice * Number(depositAmount),
true
)}
)
</span>
</>
) : (
<>
0{' '}
<span className="text-xs text-th-fgd-3">
($0.00)
</span>
</>
)}
</p>
{depositBank ? (
<div className="border-y border-th-bkg-3">
<div className="flex justify-between px-2 py-4">
<p>{t('deposit-amount')}</p>
<p className="font-mono text-th-fgd-2">
{depositAmount ? (
<>
{formatDecimal(
Number(depositAmount),
depositBank.mintDecimals
)}{' '}
<span className="text-xs text-th-fgd-3">
(
{formatFixedDecimals(
depositBank.uiPrice * Number(depositAmount),
true
)}
)
</span>
</>
) : (
<>
0{' '}
<span className="text-xs text-th-fgd-3">
($0.00)
</span>
</>
)}
</p>
</div>
</div>
</div>
) : null}
<Button
className="mb-6 flex w-44 items-center justify-center"
className="mb-6 mt-10 flex items-center justify-center"
disabled={
!depositAmount ||
!depositToken ||
@ -525,7 +545,7 @@ const UserSetupModal = ({
</div>
)}
</Button>
<LinkButton onClick={() => setShowSetupStep(4)}>
<LinkButton onClick={onClose}>
<span className="default-transition text-th-fgd-4 underline md:hover:text-th-fgd-3 md:hover:no-underline">
{t('onboarding:skip')}
</span>
@ -536,14 +556,14 @@ const UserSetupModal = ({
className="thin-scroll absolute top-36 w-full overflow-auto"
style={{ height: 'calc(100vh - 380px)' }}
>
<div className="grid auto-cols-fr grid-flow-col px-4 pb-2">
<div className="">
<div className="flex items-center px-4 pb-2">
<div className="w-1/4">
<p className="text-xs">{t('token')}</p>
</div>
<div className="text-right">
<div className="w-1/4 text-right">
<p className="text-xs">{t('deposit-rate')}</p>
</div>
<div className="text-right">
<div className="w-1/2 text-right">
<p className="whitespace-nowrap text-xs">
{t('wallet-balance')}
</p>
@ -561,7 +581,7 @@ const UserSetupModal = ({
</div>
) : null}
</UserSetupTransition>
<UserSetupTransition delay show={showSetupStep === 4}>
{/* <UserSetupTransition delay show={showSetupStep === 4}>
{showSetupStep === 4 ? (
<div className="relative">
<h2 className="mb-4 font-display text-3xl tracking-normal md:text-5xl lg:text-6xl">
@ -594,7 +614,7 @@ const UserSetupModal = ({
</UserSetupTransition>
</div>
) : null}
</UserSetupTransition>
</UserSetupTransition> */}
</div>
<div className="col-span-1 hidden h-screen lg:block">
<ParticlesBackground />

View File

@ -26,7 +26,7 @@ const EditNftProfilePic = ({ onClose }: { onClose: () => void }) => {
const nftsLoading = mangoStore((s) => s.wallet.nfts.loading)
const connection = mangoStore((s) => s.connection)
const [selectedProfile, setSelectedProfile] = useState<string>('')
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const profile = mangoStore((s) => s.profile.details)
useEffect(() => {

View File

@ -36,7 +36,7 @@ const EditProfileForm = ({
const [loadUniquenessCheck, setLoadUniquenessCheck] = useState(false)
const [loadUpdateProfile, setLoadUpdateProfile] = useState(false)
const [updateError, setUpdateError] = useState('')
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const validateProfileNameUniqueness = async (name: string) => {
try {

View File

@ -109,13 +109,20 @@ const DisplaySettings = () => {
</div> */}
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<p className="mb-2 md:mb-0">{t('settings:notification-position')}</p>
<div className="w-full min-w-[330px] md:w-[480px] md:pl-4">
<ButtonGroup
activeValue={notificationPosition}
<div className="w-full min-w-[140px] md:w-auto">
<Select
value={t(`settings:${notificationPosition}`)}
onChange={(p) => setNotificationPosition(p)}
values={NOTIFICATION_POSITIONS}
names={NOTIFICATION_POSITIONS.map((val) => t(`settings:${val}`))}
/>
className="w-full"
>
{NOTIFICATION_POSITIONS.map((val) => (
<Select.Option key={val} value={t(`settings:${val}`)}>
<div className="flex w-full items-center justify-between">
{t(`settings:${val}`)}
</div>
</Select.Option>
))}
</Select>
</div>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">

View File

@ -21,7 +21,7 @@ const RPC_URLS = [
const RpcSettings = () => {
const { t } = useTranslation('settings')
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const [customUrl, setCustomUrl] = useState('')
const [showCustomForm, setShowCustomForm] = useState(false)
const [rpcEndpointProvider, setRpcEndpointProvider] = useLocalStorageState(
@ -67,7 +67,7 @@ const RpcSettings = () => {
<h2 className="mb-4 text-base">{t('rpc')}</h2>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<p className="mb-2 md:mb-0">{t('rpc-provider')}</p>
<div className="w-full min-w-[240px] md:w-[340px] md:pl-4">
<div className="w-full min-w-[160px] md:w-auto md:pl-4">
<ButtonGroup
activeValue={rpcEndpoint.label}
onChange={(v) => handleSetEndpointProvider(v)}

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js'
const AmountWithValue = ({
amount,
value,
stacked,
}: {
amount: Decimal | number | string
value: string
stacked?: boolean
}) => {
return (
<p className={`font-mono text-th-fgd-2 ${stacked ? 'text-right' : ''}`}>
<>
{amount}{' '}
<span className={`text-th-fgd-4 ${stacked ? 'block' : ''}`}>
{value}
</span>
</>
</p>
)
}
export default AmountWithValue

View File

@ -21,6 +21,7 @@ import { LinkButton } from './Button'
import { Table, Td, Th, TrBody, TrHead } from './TableElements'
import useSelectedMarket from 'hooks/useSelectedMarket'
import useMangoGroup from 'hooks/useMangoGroup'
import AmountWithValue from './AmountWithValue'
const BalancesTable = () => {
const { t } = useTranslation(['common', 'trade'])
@ -46,12 +47,15 @@ const BalancesTable = () => {
Math.abs(b.balance! * b.value[0].uiPrice) -
Math.abs(a.balance! * a.value[0].uiPrice)
)
.filter(
(c) =>
.filter((c) => {
return (
Math.abs(
floorToDecimal(c.balance!, c.value[0].mintDecimals).toNumber()
) > 0
)
) > 0 ||
spotBalances[c.value[0].mint.toString()]?.unsettled > 0 ||
spotBalances[c.value[0].mint.toString()]?.inOrders > 0
)
})
: rawBanks
return sortedBanks
@ -104,22 +108,41 @@ const BalancesTable = () => {
{mangoAccount
? `${formatFixedDecimals(
mangoAccount.getTokenBalanceUi(bank) * bank.uiPrice,
false,
true
)}`
: '$0.00'}
</p>
</Td>
<Td className="text-right font-mono">
<p>{inOrders}</p>
<p className="text-sm text-th-fgd-4">
{formatFixedDecimals(inOrders * bank.uiPrice, true)}
</p>
<Td className="text-right">
<AmountWithValue
amount={
inOrders
? formatDecimal(Number(inOrders), bank.mintDecimals)
: '0'
}
value={formatFixedDecimals(
inOrders * bank.uiPrice,
true,
true
)}
stacked
/>
</Td>
<Td className="text-right font-mono">
<p>{unsettled ? unsettled.toFixed(bank.mintDecimals) : 0}</p>
<p className="text-sm text-th-fgd-4">
{formatFixedDecimals(unsettled * bank.uiPrice, true)}
</p>
<Td className="text-right">
<AmountWithValue
amount={
unsettled
? formatDecimal(Number(unsettled), bank.mintDecimals)
: '0'
}
value={formatFixedDecimals(
unsettled * bank.uiPrice,
true,
true
)}
stacked
/>
</Td>
</TrBody>
)
@ -160,14 +183,13 @@ const BalancesTable = () => {
<div className="mb-0.5 flex justify-end space-x-1.5">
<Balance bank={bank} />
<span className="text-sm text-th-fgd-4">
(
{mangoAccount
? `${formatFixedDecimals(
mangoAccount.getTokenBalanceUi(bank) * bank.uiPrice,
false,
true
)}`
: '$0.00'}
)
</span>
</div>
<div className="flex space-x-2">
@ -298,12 +320,7 @@ const Balance = ({ bank }: { bank: Bank }) => {
{asPath.includes('/trade') && isBaseOrQuote ? (
<LinkButton
className="font-normal underline-offset-4"
onClick={() =>
handleTradeFormBalanceClick(
parseFloat(formatDecimal(balance, bank.mintDecimals)),
isBaseOrQuote
)
}
onClick={() => handleTradeFormBalanceClick(balance, isBaseOrQuote)}
>
{formatDecimal(balance, bank.mintDecimals)}
</LinkButton>
@ -312,7 +329,7 @@ const Balance = ({ bank }: { bank: Bank }) => {
className="font-normal underline-offset-4"
onClick={() =>
handleSwapFormBalanceClick(
parseFloat(formatDecimal(balance, bank.mintDecimals))
floorToDecimal(balance, bank.mintDecimals).toNumber()
)
}
>

View File

@ -29,7 +29,7 @@ const Button: FunctionComponent<ButtonCombinedProps> = ({
<button
onClick={onClick}
disabled={disabled}
className={`whitespace-nowrap rounded-md ${
className={`rounded-md ${
secondary
? 'border border-th-button md:hover:border-th-button-hover'
: 'bg-th-button md:hover:bg-th-button-hover'

View File

@ -1,5 +1,5 @@
import { MinusSmallIcon } from '@heroicons/react/20/solid'
import { formatFixedDecimals } from 'utils/numbers'
import { formatDecimal } from 'utils/numbers'
import { DownTriangle, UpTriangle } from './DirectionTriangles'
const Change = ({
@ -42,9 +42,7 @@ const Change = ({
}`}
>
{prefix ? prefix : ''}
{isNaN(change)
? '0.00'
: formatFixedDecimals(Math.abs(change), false, true)}
{isNaN(change) ? '0.00' : formatDecimal(Math.abs(change), 2)}
{suffix ? suffix : ''}
</p>
</div>

View File

@ -0,0 +1,24 @@
import WalletIcon from '@components/icons/WalletIcon'
import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider'
import { LinkIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import Button from './Button'
const ConnectEmptyState = ({ text }: { text: string }) => {
const { t } = useTranslation('common')
const { handleConnect } = useEnhancedWallet()
return (
<div className="flex flex-col items-center">
<WalletIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p className="mb-4">{text}</p>
<Button onClick={handleConnect}>
<div className="flex items-center">
<LinkIcon className="mr-2 h-5 w-5" />
{t('connect')}
</div>
</Button>
</div>
)
}
export default ConnectEmptyState

View File

@ -102,7 +102,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
const flipGradientCoords = useMemo(() => {
if (!data.length) return
return data[0][yKey] <= 0 && data[data.length - 1][yKey] < data[0][yKey]
return data[0][yKey] <= 0 && data[data.length - 1][yKey] <= 0
}, [data])
return (
@ -147,17 +147,19 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
height={small ? 24 : 40}
width={small ? 17 : 30}
play
numbers={
prefix +
formatFixedDecimals(mouseData[yKey] ?? 0) +
suffix
}
numbers={`${
mouseData[yKey] < 0 ? '-' : ''
}${prefix}${formatFixedDecimals(
Math.abs(mouseData[yKey])
)}${suffix}`}
/>
) : (
<span>
{prefix +
formatFixedDecimals(mouseData[yKey] ?? 0) +
suffix}
{`${
mouseData[yKey] < 0 ? '-' : ''
}${prefix}${formatFixedDecimals(
Math.abs(mouseData[yKey])
)}${suffix}`}
</span>
)}
{!hideChange ? (
@ -192,17 +194,19 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
height={small ? 24 : 40}
width={small ? 17 : 30}
play
numbers={
prefix +
formatFixedDecimals(data[data.length - 1][yKey]) +
suffix
}
numbers={`${
data[data.length - 1][yKey] < 0 ? '-' : ''
}${prefix}${formatFixedDecimals(
Math.abs(data[data.length - 1][yKey])
)}${suffix}`}
/>
) : (
<span>
{prefix +
formatFixedDecimals(data[data.length - 1][yKey]) +
suffix}
{`${
data[data.length - 1][yKey] < 0 ? '-' : ''
}${prefix}${formatFixedDecimals(
Math.abs(data[data.length - 1][yKey])
)}${suffix}`}
</span>
)}
{!hideChange ? (

View File

@ -18,14 +18,19 @@ function Modal({
onClose,
hideClose,
}: ModalProps) {
const handleClose = () => {
if (disableOutsideClose) return
onClose()
}
return (
<Dialog
open={isOpen}
onClose={onClose}
onClose={handleClose}
className="relative z-40 overflow-y-auto"
>
<div
className={`fixed inset-0 backdrop-brightness-50 ${
className={`fixed inset-0 backdrop-brightness-[0.3] ${
disableOutsideClose ? 'pointer-events-none' : ''
}`}
aria-hidden="true"

View File

@ -123,13 +123,20 @@ const Notification = ({ notification }: { notification: Notification }) => {
let parsedTitle: string | undefined
if (description) {
if (
description?.includes('Timed out awaiting') ||
description?.includes('was not confirmed')
description.includes('Timed out awaiting') ||
description.includes('was not confirmed')
) {
parsedTitle = 'Transaction status unknown'
}
}
let parsedDescription = description
if (
description?.includes('{"err":{"InstructionError":[2,{"Custom":6001}]}}')
) {
parsedDescription = 'Your max slippage tolerance was exceeded'
}
// if the notification is a success, then hide the confirming tx notification with the same txid
useEffect(() => {
if ((type === 'error' || type === 'success') && txid) {
@ -247,11 +254,11 @@ const Notification = ({ notification }: { notification: Notification }) => {
</div>
<div className={`ml-2 flex-1`}>
<p className={`text-th-fgd-1`}>{parsedTitle || title}</p>
{description ? (
{parsedDescription ? (
<p
className={`mb-0 mt-0.5 break-all text-sm leading-tight text-th-fgd-4`}
>
{description}
{parsedDescription}
</p>
) : null}
{txid ? (

View File

@ -12,17 +12,13 @@ const SideBadge: FunctionComponent<SideBadgeProps> = ({ side }) => {
return (
<div
className={`inline-block rounded uppercase ${
side === 'buy' || side === 'long' || side === PerpOrderSide.bid
side === 'buy' || side === 'long' || side === 0
? 'text-th-up md:border md:border-th-up'
: 'text-th-down md:border md:border-th-down'
}
uppercase md:-my-0.5 md:px-1.5 md:py-0.5 md:text-xs`}
>
{typeof side === 'string'
? t(side)
: side === PerpOrderSide.bid
? 'Buy'
: 'Sell'}
{typeof side === 'string' ? t(side) : side === 0 ? 'Buy' : 'Sell'}
</div>
)
}

View File

@ -14,10 +14,10 @@ const SimpleAreaChart = ({
xKey: string
yKey: string
}) => {
const flipGradientCoords = useMemo(
() => data[0][yKey] <= 0 && data[data.length - 1][yKey] < data[0][yKey],
[data]
)
const flipGradientCoords = useMemo(() => {
if (!data.length) return
return data[0][yKey] <= 0 && data[data.length - 1][yKey] <= 0
}, [data])
return (
<ResponsiveContainer width="100%" height="100%">
@ -35,7 +35,6 @@ const SimpleAreaChart = ({
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey={yKey}
stroke={color}

View File

@ -6,10 +6,12 @@ import InlineNotification from './InlineNotification'
const SolBalanceWarnings = ({
amount,
className,
setAmount,
selectedToken,
}: {
amount?: string
className?: string
setAmount?: (a: string) => void
selectedToken?: string
}) => {
@ -54,11 +56,11 @@ const SolBalanceWarnings = ({
}, [maxSolDeposit])
return showLowSolWarning ? (
<div className="mt-2">
<div className={className}>
<InlineNotification type="warning" desc={t('deposit-more-sol')} />
</div>
) : showMaxSolWarning ? (
<div className="mt-2">
<div className={className}>
<InlineNotification
type="info"
desc={`SOL deposits are restricted to leave ${MIN_SOL_BALANCE} SOL in your wallet for sending transactions`}

View File

@ -65,7 +65,9 @@ export const TableDateDisplay = ({
showSeconds?: boolean
}) => (
<>
<p className="text-th-fgd-2">{dayjs(date).format('DD MMM YYYY')}</p>
<p className="tracking-normal text-th-fgd-2">
{dayjs(date).format('DD MMM YYYY')}
</p>
<p className="text-xs text-th-fgd-4">
{dayjs(date).format(showSeconds ? 'h:mm:ssa' : 'h:mma')}
</p>

View File

@ -1,48 +1,59 @@
import { Bank } from '@blockworks-foundation/mango-v4'
import {
getMaxWithdrawForBank,
useTokenMax,
} from '@components/swap/useTokenMax'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup'
import Link from 'next/link'
import { useMemo } from 'react'
import { floorToDecimal } from 'utils/numbers'
import InlineNotification from './InlineNotification'
const TokenVaultWarnings = ({ bank }: { bank: Bank }) => {
const TokenVaultWarnings = ({
bank,
type,
}: {
bank: Bank
type: 'borrow' | 'swap' | 'withdraw'
}) => {
const { mangoAccount } = useMangoAccount()
const { group } = useMangoGroup()
const balance = useMemo(() => {
if (!mangoAccount || !group) return 0
const { amountWithBorrow: swapBorrowMax } = useTokenMax()
const [maxWithdraw, maxBorrow] = useMemo(() => {
if (!mangoAccount || !group) return [0, 0]
const maxWithdraw = getMaxWithdrawForBank(group, bank, mangoAccount)
const maxBorrow = mangoAccount.getMaxWithdrawWithBorrowForTokenUi(
group,
bank.mint
)
console.log('xyx', maxBorrow / bank.minVaultToDepositsRatio)
return maxBorrow
return [maxWithdraw, maxBorrow]
}, [bank, mangoAccount, group])
const vaultBalance = useMemo(() => {
if (!group) return 0
return floorToDecimal(
group.getTokenVaultBalanceByMintUi(bank.mint),
bank.mintDecimals
).toNumber()
const availableVaultBalance = useMemo(() => {
if (!bank || !group) return 0
const vaultBalance = group.getTokenVaultBalanceByMintUi(bank.mint)
const vaultDeposits = bank.uiDeposits()
const available =
vaultBalance - vaultDeposits * bank.minVaultToDepositsRatio
return available
}, [bank, group])
// return !vaultBalance ? (
// <InlineNotification
// type="warning"
// desc={`${bank.name} vault is too low or fully utilized`}
// />
// ) : mangoAccount && balance! > vaultBalance ? (
// <InlineNotification
// type="warning"
// desc={`Available ${bank.name} vault balance is lower than your balance`}
// />
// ) : null
const showWarning = useMemo(() => {
if (!bank || !group) return false
if (
(type === 'borrow' && maxBorrow > availableVaultBalance) ||
(type === 'swap' && swapBorrowMax.toNumber() > availableVaultBalance) ||
(type === 'withdraw' && maxWithdraw > availableVaultBalance)
) {
return true
}
return false
}, [bank, group, type])
return mangoAccount &&
balance / bank.minVaultToDepositsRatio > vaultBalance ? (
return showWarning ? (
<div className="mb-4">
<InlineNotification
type="warning"
@ -52,7 +63,7 @@ const TokenVaultWarnings = ({ bank }: { bank: Bank }) => {
<Link href="/stats" className="underline hover:no-underline">
vault balance
</Link>{' '}
is low and impacting the maximum amount you can withdraw/borrow.
is low and impacting the maximum amount you can <span>{type}</span>
</div>
}
/>

View File

@ -85,7 +85,7 @@ const PerpMarketsTable = ({
<Td>
<div className="flex items-center">
<MarketLogos market={market} />
<p className="font-body tracking-wide">{market.name}</p>
<p className="font-body tracking-wider">{market.name}</p>
</div>
</Td>
<Td>

View File

@ -50,7 +50,7 @@ const SpotMarketsTable = () => {
const coingeckoData = coingeckoPrices.find((asset) =>
bank?.name === 'soETH'
? asset.symbol === 'ETH'
: asset.symbol === bank?.name
: asset.symbol.toUpperCase() === bank?.name.toUpperCase()
)
const change = coingeckoData
@ -67,12 +67,16 @@ const SpotMarketsTable = () => {
<Td>
<div className="flex items-center">
<MarketLogos market={market} />
<p className="font-body tracking-wide">{market.name}</p>
<p className="font-body tracking-wider">{market.name}</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>{formatFixedDecimals(oraclePrice!, true)}</p>
<p>
{oraclePrice
? formatFixedDecimals(oraclePrice, true)
: ''}
</p>
</div>
</Td>
<Td>

View File

@ -15,7 +15,7 @@ const TABS =
const StatsPage = () => {
const [activeTab, setActiveTab] = useState('tokens')
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const { group } = useMangoGroup()
useEffect(() => {

View File

@ -19,10 +19,11 @@ import useJupiterMints from 'hooks/useJupiterMints'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import useMangoGroup from 'hooks/useMangoGroup'
import mangoStore from '@store/mangoStore'
import AmountWithValue from '@components/shared/AmountWithValue'
const TokenStats = () => {
const { t } = useTranslation(['common', 'token'])
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const initialStatsLoad = mangoStore((s) => s.tokenStats.initialLoad)
const [showTokenDetails, setShowTokenDetails] = useState('')
const { group } = useMangoGroup()
@ -43,7 +44,7 @@ const TokenStats = () => {
key,
value,
}))
return rawBanks
return rawBanks.sort((a, b) => a.key.localeCompare(b.key))
}
return []
}, [group])
@ -65,6 +66,11 @@ const TokenStats = () => {
<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-right">
<Tooltip content="The amount available to borrow">
<span className="tooltip-underline">{t('available')}</span>
</Tooltip>
</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.">
@ -83,18 +89,13 @@ const TokenStats = () => {
</Th>
<Th>
<div className="flex justify-end text-right">
<Tooltip content={t('asset-weight-desc')}>
<Tooltip content={t('asset-liability-weight-desc')}>
<span className="tooltip-underline">
{t('asset-weight')}
{t('asset-liability-weight')}
</span>
</Tooltip>
</div>
</Th>
<Th>
<div className="flex items-center justify-end">
<span className="text-right">{t('liability-weight')}</span>
</div>
</Th>
</TrHead>
</thead>
<tbody>
@ -107,6 +108,11 @@ const TokenStats = () => {
(t) => t.address === bank.mint.toString()
)?.logoURI
}
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
const price = bank.uiPrice
const available =
deposits - deposits * bank.minVaultToDepositsRatio - borrows
return (
<TrBody key={key}>
@ -119,21 +125,39 @@ const TokenStats = () => {
<QuestionMarkCircleIcon className="h-6 w-6 text-th-fgd-3" />
)}
</div>
<p className="font-body tracking-wide">{bank.name}</p>
<p className="font-body tracking-wider">{bank.name}</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>{formatFixedDecimals(bank.uiDeposits())}</p>
<p>{formatFixedDecimals(deposits)}</p>
<p className="text-th-fgd-4">
{formatFixedDecimals(deposits * price, true, true)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>{formatFixedDecimals(bank.uiBorrows())}</p>
<p>{formatFixedDecimals(borrows)}</p>
<p className="text-th-fgd-4">
{formatFixedDecimals(borrows * price, true, true)}
</p>
</div>
</Td>
<Td>
<div className="flex justify-end space-x-2">
<div className="flex flex-col text-right">
<p>
{available > 0 ? formatFixedDecimals(available) : '0'}
</p>
<p className="text-th-fgd-4">
{available > 0
? formatFixedDecimals(available * price, false, true)
: '$0.00'}
</p>
</div>
</Td>
<Td>
<div className="flex justify-end space-x-1.5">
<p className="text-th-up">
{formatDecimal(bank.getDepositRateUi(), 2, {
fixed: true,
@ -164,12 +188,9 @@ const TokenStats = () => {
</div>
</Td>
<Td>
<div className="text-right">
<div className="flex justify-end space-x-1.5 text-right">
<p>{bank.initAssetWeight.toFixed(2)}</p>
</div>
</Td>
<Td>
<div className="text-right">
<span className="text-th-fgd-4">|</span>
<p>{bank.initLiabWeight.toFixed(2)}</p>
</div>
</Td>
@ -198,6 +219,11 @@ const TokenStats = () => {
(t) => t.address === bank.mint.toString()
)?.logoURI
}
const deposits = bank.uiDeposits()
const borrows = bank.uiBorrows()
const price = bank.uiPrice
const available =
deposits - deposits * bank.minVaultToDepositsRatio - borrows
return (
<div key={key} className="border-b border-th-bkg-3 px-6 py-4">
<div className="flex items-center justify-between">
@ -213,18 +239,28 @@ const TokenStats = () => {
</div>
<div className="flex items-center space-x-4">
<div>
<p className="text-right text-xs">
<p className="mb-0.5 text-right text-xs">
{t('total-deposits')}
</p>
<p className="text-right font-mono text-th-fgd-1">
{formatFixedDecimals(bank.uiDeposits())}
</p>
<AmountWithValue
amount={formatFixedDecimals(deposits)}
value={formatFixedDecimals(
deposits * price,
true,
true
)}
stacked
/>
</div>
<div>
<p className="text-right text-xs">{t('total-borrows')}</p>
<p className="text-right font-mono text-th-fgd-1">
{formatFixedDecimals(bank.uiBorrows())}
<p className="mb-0.5 text-right text-xs">
{t('total-borrows')}
</p>
<AmountWithValue
amount={formatFixedDecimals(borrows)}
value={formatFixedDecimals(borrows * price, true, true)}
stacked
/>
</div>
<IconButton
onClick={() => handleShowTokenDetails(bank.name)}
@ -279,20 +315,41 @@ const TokenStats = () => {
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('asset-weight')}
</p>
<p className="font-mono text-th-fgd-1">
{bank.initAssetWeight.toFixed(2)}
<Tooltip content="The amount available to borrow">
<p className="tooltip-underline text-xs text-th-fgd-3">
{t('available')}
</p>
</Tooltip>
<p className="text-th-fgd-1">
{available > 0 ? formatFixedDecimals(available) : '0'}{' '}
<span className="text-th-fgd-4">
(
{available > 0
? formatFixedDecimals(
available * price,
false,
true
)
: '$0.00'}
)
</span>
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('liability-weight')}
</p>
<p className="font-mono text-th-fgd-1">
{bank.initLiabWeight.toFixed(2)}
</p>
<Tooltip content={t('asset-liability-weight-desc')}>
<p className="tooltip-underline text-xs text-th-fgd-3">
{t('asset-liability-weight')}
</p>
</Tooltip>
<div className="flex space-x-1.5 text-right font-mono">
<p className="text-th-fgd-1">
{bank.initAssetWeight.toFixed(2)}
</p>
<span className="text-th-fgd-4">|</span>
<p className="text-th-fgd-1">
{bank.initLiabWeight.toFixed(2)}
</p>
</div>
</div>
<div className="col-span-1">
<LinkButton

View File

@ -1,65 +0,0 @@
import Tooltip from '@components/shared/Tooltip'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import { useTokenMax } from './useTokenMax'
const BorrowInfo = ({
amount,
useMargin,
}: {
amount: number
useMargin: boolean
}) => {
const { t } = useTranslation('common')
const { amount: tokenMax } = useTokenMax(useMargin)
const inputBank = mangoStore((s) => s.swap.inputBank)
const tokenMaxAsNumber = tokenMax.toNumber()
const borrowAmount = amount - tokenMaxAsNumber
return amount > tokenMaxAsNumber && inputBank ? (
<div className="space-y-2">
<div className="flex justify-between">
<Tooltip
content={
tokenMaxAsNumber
? t('swap:tooltip-borrow-balance', {
balance: formatFixedDecimals(tokenMaxAsNumber),
borrowAmount: formatFixedDecimals(borrowAmount),
token: inputBank.name,
})
: t('swap:tooltip-borrow-no-balance', {
borrowAmount: formatFixedDecimals(borrowAmount),
token: inputBank.name,
})
}
delay={250}
>
<p className="tooltip-underline text-sm text-th-fgd-3">
{t('borrow-amount')}
</p>
</Tooltip>
<p className="text-right font-mono text-sm text-th-fgd-3">
{formatFixedDecimals(borrowAmount)}
<span className="font-body"> {inputBank.name}</span>
</p>
</div>
<div className="flex justify-between">
<Tooltip content={t('tooltip-borrow-rate')} delay={250}>
<p className="tooltip-underline text-sm text-th-fgd-3">
{t('borrow-rate')}
</p>
</Tooltip>
<p className="text-right font-mono text-sm text-th-down">
{formatDecimal(inputBank.getBorrowRateUi(), 2, {
fixed: true,
})}
%
</p>
</div>
</div>
) : null
}
export default BorrowInfo

View File

@ -1,6 +1,7 @@
import MaxAmountButton from '@components/shared/MaxAmountButton'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import { floorToDecimal } from 'utils/numbers'
import { useTokenMax } from './useTokenMax'
const MaxSwapAmount = ({
@ -20,20 +21,29 @@ const MaxSwapAmount = ({
if (mangoAccountLoading) return null
const maxBalanceValue = floorToDecimal(
tokenMax.toNumber(),
decimals
).toFixed()
const maxBorrowValue = floorToDecimal(
amountWithBorrow.toNumber(),
decimals
).toFixed()
return (
<div className="flex flex-wrap justify-end pl-6 text-xs">
<MaxAmountButton
className="mb-0.5"
label="Bal"
onClick={() => setAmountIn(tokenMax.toFixed(decimals))}
value={tokenMax.toFixed()}
onClick={() => setAmountIn(maxBalanceValue)}
value={maxBalanceValue}
/>
{useMargin ? (
<MaxAmountButton
className="mb-0.5 ml-2"
label={t('max')}
onClick={() => setAmountIn(amountWithBorrow.toFixed(decimals))}
value={amountWithBorrow.toFixed()}
onClick={() => setAmountIn(maxBorrowValue)}
value={maxBorrowValue}
/>
) : null}
</div>

View File

@ -1,10 +1,11 @@
import { useState, useCallback, useEffect, useMemo } from 'react'
import { PublicKey } from '@solana/web3.js'
import {
AdjustmentsHorizontalIcon,
ArrowDownIcon,
Cog8ToothIcon,
ExclamationCircleIcon,
LinkIcon,
PencilIcon,
} from '@heroicons/react/20/solid'
import NumberFormat, {
NumberFormatValues,
@ -19,11 +20,10 @@ import useDebounce from '../shared/useDebounce'
import { useTranslation } from 'next-i18next'
import SwapFormTokenList from './SwapFormTokenList'
import { Transition } from '@headlessui/react'
import Button from '../shared/Button'
import Button, { IconButton } from '../shared/Button'
import Loading from '../shared/Loading'
import { EnterBottomExitBottom } from '../shared/Transitions'
import useJupiterRoutes from './useJupiterRoutes'
import SlippageSettings from './SlippageSettings'
import SheenLoader from '../shared/SheenLoader'
import { HealthType } from '@blockworks-foundation/mango-v4'
import {
@ -45,7 +45,8 @@ import TokenVaultWarnings from '@components/shared/TokenVaultWarnings'
import MaxSwapAmount from './MaxSwapAmount'
import PercentageSelectButtons from './PercentageSelectButtons'
import useIpAddress from 'hooks/useIpAddress'
import Checkbox from '@components/forms/Checkbox'
import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider'
import SwapSettings from './SwapSettings'
const MAX_DIGITS = 11
export const withValueLimit = (values: NumberFormatValues): boolean => {
@ -261,19 +262,12 @@ const SwapForm = () => {
return !!amountInAsDecimal.toNumber() && connected && !selectedRoute
}, [amountInAsDecimal, connected, selectedRoute])
const handleSetMargin = () => {
set((s) => {
s.swap.margin = !s.swap.margin
})
}
return (
<ContentBox
hidePadding
// showBackground
className="relative overflow-hidden border-x-0 md:border-l md:border-r-0 md:border-t-0 md:border-b-0"
>
<div className="">
<div>
<Transition
className="absolute top-0 left-0 z-10 h-full w-full bg-th-bkg-1 pb-0"
show={showConfirm}
@ -312,9 +306,19 @@ const SwapForm = () => {
className="thin-scroll absolute bottom-0 left-0 z-10 h-full w-full overflow-auto bg-th-bkg-1 p-6 pb-0"
show={showSettings}
>
<SlippageSettings onClose={() => setShowSettings(false)} />
<SwapSettings onClose={() => setShowSettings(false)} />
</EnterBottomExitBottom>
<div className="p-6">
<div className="relative p-6 pt-10">
<div className="absolute right-2 top-2">
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => setShowSettings(true)}
size="small"
>
<Cog8ToothIcon className="h-5 w-5" />
</IconButton>
</div>
<div className="mb-2 flex items-end justify-between">
<p className="text-th-fgd-2 lg:text-base">{t('swap:pay')}</p>
<MaxSwapAmount
@ -427,19 +431,19 @@ const SwapForm = () => {
}
/>
) : (
<div className="mt-6 mb-4 flex-grow">
<div className="flex">
<Button disabled className="flex-grow">
<span>
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '(Unknown)',
})}
</span>
</Button>
</div>
</div>
<Button
disabled
className="mt-6 mb-4 w-full leading-tight"
size="large"
>
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
{group && inputBank ? <TokenVaultWarnings bank={inputBank} /> : null}
{group && inputBank ? (
<TokenVaultWarnings bank={inputBank} type="swap" />
) : null}
<div className="space-y-2">
<div id="swap-step-four">
<HealthImpact maintProjectedHealth={maintProjectedHealth} />
@ -454,22 +458,21 @@ const SwapForm = () => {
: ''}
</p>
</div>
</div>
</div>
<div className="flex items-center justify-between border-t border-th-bkg-3 px-6 py-4">
<p>{`${t('swap')} ${t('settings')}`}</p>
<div className="flex items-center space-x-5">
<Checkbox checked={useMargin} onChange={handleSetMargin}>
<span className="text-xs text-th-fgd-3">{t('trade:margin')}</span>
</Checkbox>
<div id="swap-step-one">
<button
className="default-transition flex items-center text-th-fgd-3 focus:outline-none md:hover:text-th-active"
onClick={() => setShowSettings(true)}
>
<AdjustmentsHorizontalIcon className="mr-1.5 h-4 w-4" />
<span className="text-xs">{slippage}%</span>
</button>
<div className="flex justify-between">
<p className="text-sm text-th-fgd-3">{t('swap:max-slippage')}</p>
<div className="flex items-center space-x-1">
<p className="text-right font-mono text-sm text-th-fgd-2">
{slippage}%
</p>
<IconButton
className="text-th-fgd-3"
hideBg
onClick={() => setShowSettings(true)}
size="small"
>
<PencilIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
</div>
</div>
@ -498,17 +501,20 @@ const SwapFormSubmitButton = ({
const { t } = useTranslation('common')
const { connected } = useWallet()
const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin)
const { handleConnect } = useEnhancedWallet()
const showInsufficientBalance = useMargin
? amountWithBorrow.lt(amountIn)
: tokenMax.lt(amountIn)
const disabled =
!amountIn.toNumber() || !connected || showInsufficientBalance || !amountOut
connected && (!amountIn.toNumber() || showInsufficientBalance || !amountOut)
const onClick = connected ? () => setShowConfirm(true) : handleConnect
return (
<Button
onClick={() => setShowConfirm(true)}
onClick={onClick}
className="mt-6 mb-4 flex w-full items-center justify-center text-base"
disabled={disabled}
size="large"

View File

@ -10,6 +10,7 @@ import useMangoAccount from 'hooks/useMangoAccount'
import useJupiterMints from 'hooks/useJupiterMints'
import useMangoGroup from 'hooks/useMangoGroup'
import { PublicKey } from '@solana/web3.js'
import { formatDecimal } from 'utils/numbers'
// const generateSearchTerm = (item: Token, searchValue: string) => {
// const normalizedSearchValue = searchValue.toLowerCase()
@ -69,11 +70,14 @@ const TokenItem = ({
</div>
</div>
</div>
{type === 'input' ? (
<p className="text-sm text-th-fgd-2">
{type === 'input' &&
token.amount &&
token.amountWithBorrow &&
token.decimals ? (
<p className="font-mono text-sm text-th-fgd-2">
{useMargin
? token.amountWithBorrow?.toString()
: token.amount?.toString()}
? formatDecimal(token.amountWithBorrow.toNumber(), token.decimals)
: formatDecimal(token.amount.toNumber(), token.decimals)}
</p>
) : null}
</button>
@ -156,6 +160,7 @@ const SwapFormTokenList = ({
amountWithBorrow: new Decimal(0),
}))
.filter((token) => (token.symbol === inputBank?.name ? false : true))
.sort((a, b) => a.symbol.localeCompare(b.symbol))
return filteredTokens
} else {
return []

View File

@ -2,7 +2,6 @@ import { Fragment, useEffect, useState } from 'react'
import {
ArrowRightIcon,
ChevronDownIcon,
LinkIcon,
NoSymbolIcon,
} from '@heroicons/react/20/solid'
import dayjs from 'dayjs'
@ -13,9 +12,10 @@ import { useViewport } from '../../hooks/useViewport'
import { IconButton } from '../shared/Button'
import { Transition } from '@headlessui/react'
import SheenLoader from '../shared/SheenLoader'
import mangoStore, { SwapHistoryItem } from '@store/mangoStore'
import mangoStore from '@store/mangoStore'
import {
countLeadingZeros,
formatDecimal,
formatFixedDecimals,
trimDecimals,
} from '../../utils/numbers'
@ -25,23 +25,19 @@ import Tooltip from '@components/shared/Tooltip'
import { formatTokenSymbol } from 'utils/tokens'
import useJupiterMints from 'hooks/useJupiterMints'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import { useWallet } from '@solana/wallet-adapter-react'
import { EXPLORERS } from '@components/settings/PreferredExplorerSettings'
import useMangoAccount from 'hooks/useMangoAccount'
const SwapHistoryTable = ({
swapHistory,
loading,
}: {
swapHistory: SwapHistoryItem[]
loading: boolean
}) => {
const SwapHistoryTable = () => {
const { t } = useTranslation(['common', 'settings', 'swap'])
const { connected } = useWallet()
const swapHistory = mangoStore((s) => s.mangoAccount.stats.swapHistory.data)
const initialLoad = mangoStore(
(s) => s.mangoAccount.stats.swapHistory.initialLoad
)
const { mangoTokens } = useJupiterMints()
const [showSwapDetails, setSwapDetails] = useState('')
const actions = mangoStore((s) => s.actions)
const { mangoAccount } = useMangoAccount()
const actions = mangoStore.getState().actions
const { mangoAccountAddress } = useMangoAccount()
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const [preferredExplorer] = useLocalStorageState(
@ -50,191 +46,30 @@ const SwapHistoryTable = ({
)
useEffect(() => {
if (mangoAccount) {
actions.fetchSwapHistory(mangoAccount.publicKey.toString())
if (mangoAccountAddress) {
actions.fetchSwapHistory(mangoAccountAddress)
}
}, [actions, mangoAccount])
}, [actions, mangoAccountAddress])
const handleShowSwapDetails = (signature: string) => {
showSwapDetails ? setSwapDetails('') : setSwapDetails(signature)
}
return connected ? (
!loading ? (
swapHistory.length ? (
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('date')}</Th>
<Th className="w-1/3 text-left">{t('swap')}</Th>
<Th className="text-right">{t('value')}</Th>
<Th className="text-right">{t('borrow')}</Th>
<Th className="text-right">{t('borrow-fee')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{swapHistory.map((h) => {
const {
block_datetime,
signature,
swap_in_amount,
swap_in_loan_origination_fee,
swap_in_price_usd,
swap_in_symbol,
swap_out_amount,
loan,
loan_origination_fee,
swap_out_price_usd,
swap_out_symbol,
} = h
const borrowAmount =
loan > 0
? `${trimDecimals(loan, countLeadingZeros(loan) + 2)}`
: 0
const borrowFee =
swap_in_loan_origination_fee > 0
? swap_in_loan_origination_fee.toFixed(4)
: loan_origination_fee > 0
? loan_origination_fee.toFixed(4)
: 0
let baseLogoURI
let quoteLogoURI
const inSymbol = formatTokenSymbol(swap_in_symbol)
const outSymbol = formatTokenSymbol(swap_out_symbol)
if (mangoTokens.length) {
baseLogoURI = mangoTokens.find(
(t) => t.symbol === inSymbol
)?.logoURI
quoteLogoURI = mangoTokens.find(
(t) => t.symbol === outSymbol
)?.logoURI
}
const inDecimals = countLeadingZeros(swap_in_amount)
const outDecimals = countLeadingZeros(swap_out_amount) + 2
return (
<TrBody key={signature}>
<Td>
<p className="font-body tracking-wide">
{dayjs(block_datetime).format('ddd D MMM')}
</p>
<p className="font-body text-xs tracking-wide text-th-fgd-3">
{dayjs(block_datetime).format('h:mma')}
</p>
</Td>
<Td className="w-1/3">
<div className="flex items-center justify-between">
<div className="flex w-1/2 items-center">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image
alt=""
width="24"
height="24"
src={baseLogoURI || ''}
/>
</div>
<div>
<p className="whitespace-nowrap">
{`${swap_in_amount.toFixed(inDecimals)}`}
<span className="ml-1 font-body tracking-wide text-th-fgd-3">
{inSymbol}
</span>
</p>
<p className="text-xs text-th-fgd-3">
<span className="font-body text-th-fgd-4">
{t('price')}:
</span>{' '}
{formatFixedDecimals(swap_in_price_usd, true)}
</p>
</div>
</div>
<ArrowRightIcon className="mx-4 h-4 w-4 flex-shrink-0 text-th-fgd-4" />
<div className="flex w-1/2 items-center">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image
alt=""
width="24"
height="24"
src={quoteLogoURI || ''}
/>
</div>
<div>
<p className="whitespace-nowrap">
{`${trimDecimals(swap_out_amount, outDecimals)}`}
<span className="ml-1 font-body tracking-wide text-th-fgd-3">
{outSymbol}
</span>
</p>
<p className="text-xs text-th-fgd-3">
<span className="font-body text-th-fgd-4">
{t('price')}:
</span>{' '}
{formatFixedDecimals(swap_out_price_usd, true)}
</p>
</div>
</div>
</div>
</Td>
<Td>
<p className="text-right">
{formatFixedDecimals(
swap_out_price_usd * swap_out_amount,
true
)}
</p>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{borrowAmount}
<span className="ml-1 font-body tracking-wide text-th-fgd-3">
{inSymbol}
</span>
</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>${borrowFee}</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
<Tooltip
content={`View on ${t(
`settings:${preferredExplorer.name}`
)}`}
placement="top-end"
>
<a
href={`${preferredExplorer.url}${signature}`}
target="_blank"
rel="noopener noreferrer"
>
<div className="h-6 w-6">
<Image
alt=""
width="24"
height="24"
src={`/explorer-logos/${preferredExplorer.name}.png`}
/>
</div>
</a>
</Tooltip>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div>
return initialLoad ? (
mangoAccountAddress && swapHistory.length ? (
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('date')}</Th>
<Th className="w-1/3 text-left">{t('swap')}</Th>
<Th className="text-right">{t('value')}</Th>
<Th className="text-right">{t('borrow')}</Th>
<Th className="text-right">{t('borrow-fee')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{swapHistory.map((h) => {
const {
block_datetime,
@ -249,7 +84,6 @@ const SwapHistoryTable = ({
swap_out_price_usd,
swap_out_symbol,
} = h
const borrowAmount =
loan > 0
? `${trimDecimals(loan, countLeadingZeros(loan) + 2)}`
@ -266,6 +100,7 @@ const SwapHistoryTable = ({
const inSymbol = formatTokenSymbol(swap_in_symbol)
const outSymbol = formatTokenSymbol(swap_out_symbol)
console.log('mangoTokens', mangoTokens)
if (mangoTokens.length) {
baseLogoURI = mangoTokens.find(
@ -276,11 +111,21 @@ const SwapHistoryTable = ({
)?.logoURI
}
const inDecimals = countLeadingZeros(swap_in_amount)
const outDecimals = countLeadingZeros(swap_out_amount) + 2
return (
<div key={signature} className="border-b border-th-bkg-3 p-4">
<div className="flex items-center justify-between">
<TrBody key={signature}>
<Td>
<p className="font-body tracking-wider">
{dayjs(block_datetime).format('ddd D MMM')}
</p>
<p className="font-body text-xs text-th-fgd-3">
{dayjs(block_datetime).format('h:mma')}
</p>
</Td>
<Td className="w-1/3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="flex w-1/2 items-center">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image
alt=""
@ -290,13 +135,13 @@ const SwapHistoryTable = ({
/>
</div>
<div>
<p className="whitespace-nowrap font-mono text-th-fgd-1">
{swap_in_amount.toFixed(2)}{' '}
<span className="font-body text-th-fgd-3">
<p className="whitespace-nowrap">
{`${formatDecimal(swap_in_amount, inDecimals)}`}
<span className="ml-1 font-body text-th-fgd-3">
{inSymbol}
</span>
</p>
<p className="font-mono text-xs text-th-fgd-3">
<p className="text-xs text-th-fgd-3">
<span className="font-body text-th-fgd-4">
{t('price')}:
</span>{' '}
@ -305,7 +150,7 @@ const SwapHistoryTable = ({
</div>
</div>
<ArrowRightIcon className="mx-4 h-4 w-4 flex-shrink-0 text-th-fgd-4" />
<div className="flex items-center">
<div className="flex w-1/2 items-center">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image
alt=""
@ -315,116 +160,261 @@ const SwapHistoryTable = ({
/>
</div>
<div>
<p className="whitespace-nowrap font-mono text-th-fgd-1">
{swap_out_amount.toFixed(2)}{' '}
<span className="font-body text-th-fgd-3">
<p className="whitespace-nowrap">
{`${formatDecimal(swap_out_amount, outDecimals)}`}
<span className="ml-1 font-body text-th-fgd-3">
{outSymbol}
</span>
</p>
<p className="font-mono text-xs text-th-fgd-3">
<span className="font-body text-th-fgd-4">
{t('price')}:
</span>{' '}
<p className="text-xs text-th-fgd-4">
<span className="font-body">{t('price')}:</span>{' '}
{formatFixedDecimals(swap_out_price_usd, true)}
</p>
</div>
</div>
</div>
<IconButton
onClick={() => handleShowSwapDetails(signature)}
>
<ChevronDownIcon
className={`${
showSwapDetails === signature
? 'rotate-180'
: 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-1`}
/>
</IconButton>
</div>
<Transition
appear={true}
show={showSwapDetails === signature}
as={Fragment}
enter="transition ease-in duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-out"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="mt-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4">
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('date')}</p>
<p className="text-th-fgd-1">
{dayjs(block_datetime).format('ddd D MMM')}
</p>
<p className="text-xs text-th-fgd-3">
{dayjs(block_datetime).format('h:mma')}
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('borrow')}</p>
<p className="font-mono text-th-fgd-1">
{borrowAmount}{' '}
<span className="font-body text-th-fgd-3">
{inSymbol}
</span>
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('borrow-fee')}
</p>
<p className="font-mono text-th-fgd-1">${borrowFee}</p>
</div>
<div className="col-span-1">
<p className="mb-0.5 text-xs text-th-fgd-3">
{t('transaction')}
</p>
</Td>
<Td>
<p className="text-right">
{formatFixedDecimals(
swap_out_price_usd * swap_out_amount,
false,
true
)}
</p>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{borrowAmount}
<span className="ml-1 font-body text-th-fgd-3">
{inSymbol}
</span>
</p>
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{borrowFee}
<span className="ml-1 font-body text-th-fgd-3">
{inSymbol}
</span>
</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
<Tooltip
content={`View on ${t(
`settings:${preferredExplorer.name}`
)}`}
placement="top-end"
>
<a
className="default-transition flex items-center text-th-fgd-1 hover:text-th-fgd-3"
href={`${preferredExplorer.url}${signature}`}
target="_blank"
rel="noopener noreferrer"
>
<Image
alt=""
width="20"
height="20"
src={`/explorer-logos/${preferredExplorer.name}.png`}
/>
<span className="ml-1.5">{`View on ${t(
`settings:${preferredExplorer.name}`
)}`}</span>
<div className="h-6 w-6">
<Image
alt=""
width="24"
height="24"
src={`/explorer-logos/${preferredExplorer.name}.png`}
/>
</div>
</a>
</div>
</Tooltip>
</div>
</Transition>
</div>
</Td>
</TrBody>
)
})}
</div>
)
</tbody>
</Table>
) : (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('swap:no-history')}</p>
<div>
{swapHistory.map((h) => {
const {
block_datetime,
signature,
swap_in_amount,
swap_in_loan_origination_fee,
swap_in_price_usd,
swap_in_symbol,
swap_out_amount,
loan,
loan_origination_fee,
swap_out_price_usd,
swap_out_symbol,
} = h
const borrowAmount =
loan > 0
? `${trimDecimals(loan, countLeadingZeros(loan) + 2)}`
: 0
const borrowFee =
swap_in_loan_origination_fee > 0
? swap_in_loan_origination_fee.toFixed(4)
: loan_origination_fee > 0
? loan_origination_fee.toFixed(4)
: 0
let baseLogoURI
let quoteLogoURI
const inSymbol = formatTokenSymbol(swap_in_symbol)
const outSymbol = formatTokenSymbol(swap_out_symbol)
if (mangoTokens.length) {
baseLogoURI = mangoTokens.find(
(t) => t.symbol === inSymbol
)?.logoURI
quoteLogoURI = mangoTokens.find(
(t) => t.symbol === outSymbol
)?.logoURI
}
return (
<div key={signature} className="border-b border-th-bkg-3 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image
alt=""
width="24"
height="24"
src={baseLogoURI || ''}
/>
</div>
<div>
<p className="whitespace-nowrap font-mono text-th-fgd-1">
{swap_in_amount.toFixed(2)}{' '}
<span className="font-body text-th-fgd-3">
{inSymbol}
</span>
</p>
<p className="font-mono text-xs text-th-fgd-3">
<span className="font-body text-th-fgd-4">
{t('price')}:
</span>{' '}
{formatFixedDecimals(swap_in_price_usd, true)}
</p>
</div>
</div>
<ArrowRightIcon className="mx-4 h-4 w-4 flex-shrink-0 text-th-fgd-4" />
<div className="flex items-center">
<div className="mr-2 flex flex-shrink-0 items-center">
<Image
alt=""
width="24"
height="24"
src={quoteLogoURI || ''}
/>
</div>
<div>
<p className="whitespace-nowrap font-mono text-th-fgd-1">
{swap_out_amount.toFixed(2)}{' '}
<span className="font-body text-th-fgd-3">
{outSymbol}
</span>
</p>
<p className="font-mono text-xs text-th-fgd-3">
<span className="font-body text-th-fgd-4">
{t('price')}:
</span>{' '}
{formatFixedDecimals(swap_out_price_usd, true)}
</p>
</div>
</div>
</div>
<IconButton onClick={() => handleShowSwapDetails(signature)}>
<ChevronDownIcon
className={`${
showSwapDetails === signature
? 'rotate-180'
: 'rotate-360'
} h-6 w-6 flex-shrink-0 text-th-fgd-1`}
/>
</IconButton>
</div>
<Transition
appear={true}
show={showSwapDetails === signature}
as={Fragment}
enter="transition ease-in duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-out"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="mt-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pt-4">
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('date')}</p>
<p className="text-th-fgd-1">
{dayjs(block_datetime).format('ddd D MMM')}
</p>
<p className="text-xs text-th-fgd-3">
{dayjs(block_datetime).format('h:mma')}
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('borrow')}</p>
<p className="font-mono text-th-fgd-1">
{borrowAmount}{' '}
<span className="font-body text-th-fgd-3">
{inSymbol}
</span>
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('borrow-fee')}</p>
<p className="font-mono text-th-fgd-1">${borrowFee}</p>
</div>
<div className="col-span-1">
<p className="mb-0.5 text-xs text-th-fgd-3">
{t('transaction')}
</p>
<a
className="default-transition flex items-center text-th-fgd-1 hover:text-th-fgd-3"
href={`${preferredExplorer.url}${signature}`}
target="_blank"
rel="noopener noreferrer"
>
<Image
alt=""
width="20"
height="20"
src={`/explorer-logos/${preferredExplorer.name}.png`}
/>
<span className="ml-1.5">{`View on ${t(
`settings:${preferredExplorer.name}`
)}`}</span>
</a>
</div>
</div>
</Transition>
</div>
)
})}
</div>
)
) : (
<div className="mt-4 space-y-1.5">
{[...Array(4)].map((x, i) => (
<SheenLoader className="mx-4 flex flex-1 md:mx-6" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('swap:no-history')}</p>
</div>
)
) : (
<div className="flex flex-col items-center p-8">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('swap:connect-swap')}</p>
<div className="mt-4 space-y-1.5">
{[...Array(4)].map((x, i) => (
<SheenLoader className="mx-4 flex flex-1 md:mx-6" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
)
}

View File

@ -9,8 +9,6 @@ const SwapInfoTabs = () => {
const [selectedTab, setSelectedTab] = useState('balances')
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
const { mangoAccount } = useMangoAccount()
const swapHistory = mangoStore((s) => s.mangoAccount.stats.swapHistory.data)
const loading = mangoStore((s) => s.mangoAccount.stats.swapHistory.loading)
const tabsWithCount: [string, number][] = useMemo(() => {
return [
@ -30,9 +28,7 @@ const SwapInfoTabs = () => {
/>
</div>
{selectedTab === 'balances' ? <SwapTradeBalances /> : null}
{selectedTab === 'swap:swap-history' ? (
<SwapHistoryTable swapHistory={swapHistory} loading={loading} />
) : null}
{selectedTab === 'swap:swap-history' ? <SwapHistoryTable /> : null}
</div>
)
}

View File

@ -214,7 +214,7 @@ const SwapReviewRouteInfo = ({
const connection = mangoStore.getState().connection
if (!mangoAccount || !group || !inputBank || !outputBank) return
setSubmitting(true)
const [ixs, alts] = await fetchJupiterTransaction(
connection,
selectedRoute,
@ -225,7 +225,6 @@ const SwapReviewRouteInfo = ({
)
try {
setSubmitting(true)
const tx = await client.marginTrade({
group,
mangoAccount,
@ -295,7 +294,11 @@ const SwapReviewRouteInfo = ({
: new Decimal(0)
}, [coingeckoPrices, amountIn, amountOut])
return routes?.length && selectedRoute && outputTokenInfo && amountOut ? (
return routes?.length &&
selectedRoute &&
inputTokenInfo &&
outputTokenInfo &&
amountOut ? (
<div className="thin-scroll flex h-full flex-col justify-between overflow-y-auto">
<div>
<IconButton
@ -315,7 +318,7 @@ const SwapReviewRouteInfo = ({
alt=""
width="40"
height="40"
src={outputTokenInfo.logoURI}
src={outputTokenInfo?.logoURI}
/>
</div>
</div>
@ -323,12 +326,12 @@ const SwapReviewRouteInfo = ({
<span className="mr-1 font-mono text-th-fgd-1">{`${formatFixedDecimals(
amountIn.toNumber()
)}`}</span>{' '}
{inputTokenInfo!.symbol}
{inputTokenInfo?.symbol}
<ArrowRightIcon className="mx-2 h-5 w-5 text-th-fgd-4" />
<span className="mr-1 font-mono text-th-fgd-1">{`${formatFixedDecimals(
amountOut.toNumber()
)}`}</span>{' '}
{`${outputTokenInfo.symbol}`}
{`${outputTokenInfo?.symbol}`}
</p>
</div>
</div>
@ -341,23 +344,23 @@ const SwapReviewRouteInfo = ({
{swapRate ? (
<>
1{' '}
<span className="font-body tracking-wide">
{inputTokenInfo!.name} {' '}
<span className="font-body tracking-wider">
{inputTokenInfo?.name} {' '}
</span>
{formatFixedDecimals(amountOut.div(amountIn).toNumber())}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{outputTokenInfo?.symbol}
</span>
</>
) : (
<>
1{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{outputTokenInfo?.symbol} {' '}
</span>
{formatFixedDecimals(amountIn.div(amountOut).toNumber())}{' '}
<span className="font-body tracking-wide">
{inputTokenInfo!.symbol}
<span className="font-body tracking-wider">
{inputTokenInfo?.symbol}
</span>
</>
)}
@ -378,7 +381,7 @@ const SwapReviewRouteInfo = ({
}`}
>
{Decimal.abs(coinGeckoPriceDifference).toFixed(1)}%{' '}
<span className="font-body tracking-wide text-th-fgd-3">{`${
<span className="font-body text-th-fgd-3">{`${
coinGeckoPriceDifference.lte(0)
? 'cheaper'
: 'more expensive'
@ -392,14 +395,15 @@ const SwapReviewRouteInfo = ({
<p className="text-sm text-th-fgd-3">
{t('swap:minimum-received')}
</p>
{outputTokenInfo?.decimals ? (
{outputTokenInfo?.decimals &&
selectedRoute?.otherAmountThreshold ? (
<p className="text-right font-mono text-sm text-th-fgd-2">
{formatDecimal(
selectedRoute?.otherAmountThreshold /
selectedRoute.otherAmountThreshold /
10 ** outputTokenInfo.decimals || 1,
outputTokenInfo.decimals
)}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{outputTokenInfo?.symbol}
</span>
</p>
@ -442,7 +446,7 @@ const SwapReviewRouteInfo = ({
</Tooltip>
<p className="text-right font-mono text-sm text-th-fgd-2">
~{formatFixedDecimals(borrowAmount)}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{inputTokenInfo?.symbol}
</span>
</p>
@ -523,7 +527,7 @@ const SwapReviewRouteInfo = ({
.mul(inputBank!.loanOriginationFeeRate.toFixed())
.toNumber()
)}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{inputBank!.name}
</span>{' '}
(
@ -561,7 +565,7 @@ const SwapReviewRouteInfo = ({
info.lpFee?.amount /
Math.pow(10, feeToken.decimals)
).toFixed(6)}{' '}
<span className="font-body tracking-wide">
<span className="font-body tracking-wider">
{feeToken?.symbol}
</span>{' '}
(
@ -591,7 +595,7 @@ const SwapReviewRouteInfo = ({
setSelectedRoute={setSelectedRoute}
selectedRoute={selectedRoute}
routes={routes}
inputTokenSymbol={inputTokenInfo!.name}
inputTokenSymbol={inputTokenInfo?.name}
outputTokenInfo={outputTokenInfo}
/>
) : null}

View File

@ -1,6 +1,6 @@
import { XMarkIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import mangoStore from '@store/mangoStore'
import ButtonGroup from '../forms/ButtonGroup'
import Input from '../forms/Input'
@ -10,13 +10,19 @@ import Button, { IconButton, LinkButton } from '../shared/Button'
const slippagePresets = ['0.1', '0.5', '1', '2']
const SwapSettings = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation('common')
const { t } = useTranslation(['common', 'settings', 'swap'])
const margin = mangoStore((s) => s.swap.margin)
const slippage = mangoStore((s) => s.swap.slippage)
const set = mangoStore((s) => s.set)
const [showCustomSlippageForm, setShowCustomSlippageForm] = useState(false)
const [inputValue, setInputValue] = useState('')
const [inputValue, setInputValue] = useState(slippage.toString())
useEffect(() => {
if (!slippagePresets.includes(slippage.toString())) {
setShowCustomSlippageForm(true)
}
}, [])
const handleSetMargin = () => {
set((s) => {
@ -39,11 +45,11 @@ const SwapSettings = ({ onClose }: { onClose: () => void }) => {
<div className="mt-4">
<div className="mb-2 flex justify-between">
<p className="text-th-fgd-2">{`${t('max')} ${t('swap:slippage')}`}</p>
<p className="text-th-fgd-2">{t('swap:max-slippage')}</p>
<LinkButton
onClick={() => setShowCustomSlippageForm(!showCustomSlippageForm)}
>
{showCustomSlippageForm ? 'Preset' : t('settings:custom')}
{showCustomSlippageForm ? t('swap:preset') : t('settings:custom')}
</LinkButton>
</div>
{showCustomSlippageForm ? (

View File

@ -37,11 +37,13 @@ const CustomizedLabel = ({
x,
y,
value,
index,
}: {
chartData: any[]
x?: number
y?: string | number
value?: number
index?: number
}) => {
const { width } = useViewport()
const [min, max] = useMemo(() => {
@ -52,7 +54,13 @@ const CustomizedLabel = ({
return ['', '']
}, [chartData])
if (value === min || value === max) {
const [minIndex, maxIndex] = useMemo(() => {
const minIndex = chartData.findIndex((d) => d.price === min)
const maxIndex = chartData.findIndex((d) => d.price === max)
return [minIndex, maxIndex]
}, [min, max, chartData])
if (value && (minIndex === index || maxIndex === index)) {
return (
<Text
x={x}

View File

@ -22,11 +22,21 @@ const useJupiterSwapData = () => {
}
}, [inputBank, outputBank, mangoTokens])
let inputCoingeckoId = inputTokenInfo?.extensions?.coingeckoId
let outputCoingeckoId = outputTokenInfo?.extensions?.coingeckoId
if (inputBank?.name.toLocaleLowerCase() === 'dai') {
inputCoingeckoId = 'dai'
}
if (outputBank?.name.toLocaleLowerCase() === 'dai') {
outputCoingeckoId = 'dai'
}
return {
inputTokenInfo,
inputCoingeckoId: inputTokenInfo?.extensions?.coingeckoId,
inputCoingeckoId,
outputTokenInfo,
outputCoingeckoId: outputTokenInfo?.extensions?.coingeckoId,
outputCoingeckoId,
}
}

View File

@ -21,6 +21,8 @@ export const getMaxWithdrawForBank = (
)
const maxWithdraw = allowBorrow
? Decimal.min(vaultBalance, maxBorrow)
: bank.initAssetWeight.toNumber() === 0
? Decimal.min(accountBalance, vaultBalance)
: Decimal.min(accountBalance, vaultBalance, maxBorrow)
return Decimal.max(0, maxWithdraw)
}
@ -43,10 +45,10 @@ export const getTokenInMax = (
}
}
const inputTokenBalance = floorToDecimal(
mangoAccount.getTokenBalanceUi(inputBank),
inputBank.mintDecimals
const inputTokenBalance = new Decimal(
mangoAccount.getTokenBalanceUi(inputBank)
)
const maxAmountWithoutMargin = inputTokenBalance.gt(0)
? inputTokenBalance
: new Decimal(0)

View File

@ -4,6 +4,7 @@ import useMangoGroup from 'hooks/useMangoGroup'
import { useTranslation } from 'next-i18next'
import dynamic from 'next/dynamic'
import { useEffect, useMemo, useState } from 'react'
import { numberCompacter } from 'utils/numbers'
const DetailedAreaChart = dynamic(
() => import('@components/shared/DetailedAreaChart'),
{ ssr: false }
@ -16,7 +17,7 @@ const ChartTabs = ({ token }: { token: string }) => {
const tokenStats = mangoStore((s) => s.tokenStats.data)
const initialStatsLoad = mangoStore((s) => s.tokenStats.initialLoad)
const loadingTokenStats = mangoStore((s) => s.tokenStats.loading)
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const { group } = useMangoGroup()
useEffect(() => {
@ -63,7 +64,7 @@ const ChartTabs = ({ token }: { token: string }) => {
// domain={[0, 'dataMax']}
loading={loadingTokenStats}
small
tickFormat={(x) => x.toFixed(2)}
tickFormat={(x) => numberCompacter.format(x)}
title={`${token} ${t('token:deposits')}`}
xKey="date_hour"
yKey={'total_deposits'}
@ -107,7 +108,7 @@ const ChartTabs = ({ token }: { token: string }) => {
// domain={[0, 'dataMax']}
loading={loadingTokenStats}
small
tickFormat={(x) => x.toFixed(2)}
tickFormat={(x) => numberCompacter.format(x)}
title={`${token} ${t('token:borrows')}`}
xKey="date_hour"
yKey={'total_borrows'}

View File

@ -111,32 +111,34 @@ const CoingeckoStats = ({
return (
<>
<div className="border-b border-th-bkg-3 py-4 px-6">
<h2 className="mb-1 text-xl">About {bank.name}</h2>
<div className="flex items-end">
<p
className={`${
showFullDesc ? 'h-full' : 'h-5'
} max-w-[720px] overflow-hidden`}
ref={descWidthRef}
>
{parse(coingeckoData.description.en)}
</p>
{width === 720 ? (
<span
className="default-transition ml-4 flex cursor-pointer items-end font-normal underline hover:text-th-fgd-2 md:hover:no-underline"
onClick={() => setShowFullDesc(!showFullDesc)}
{coingeckoData?.description?.en?.length ? (
<div className="border-b border-th-bkg-3 py-4 px-6">
<h2 className="mb-1 text-xl">About {bank.name}</h2>
<div className="flex items-end">
<p
className={`${
showFullDesc ? 'h-full' : 'h-5'
} max-w-[720px] overflow-hidden`}
ref={descWidthRef}
>
{showFullDesc ? 'Less' : 'More'}
<ArrowSmallUpIcon
className={`h-5 w-5 ${
showFullDesc ? 'rotate-360' : 'rotate-180'
} default-transition`}
/>
</span>
) : null}
{parse(coingeckoData.description.en)}
</p>
{width === 720 ? (
<span
className="default-transition ml-4 flex cursor-pointer items-end font-normal underline hover:text-th-fgd-2 md:hover:no-underline"
onClick={() => setShowFullDesc(!showFullDesc)}
>
{showFullDesc ? 'Less' : 'More'}
<ArrowSmallUpIcon
className={`h-5 w-5 ${
showFullDesc ? 'rotate-360' : 'rotate-180'
} default-transition`}
/>
</span>
) : null}
</div>
</div>
</div>
) : null}
{!loadingChart ? (
coingeckoTokenPrices.length ? (
<>

View File

@ -3,6 +3,7 @@ import { useTheme } from 'next-themes'
import { useMemo } from 'react'
import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'
import { COLORS } from 'styles/colors'
import { formatFixedDecimals } from 'utils/numbers'
const PriceChart = ({
prices,
@ -70,7 +71,7 @@ const PriceChart = ({
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickFormatter={(x) => `$${x.toFixed(2)}`}
tickFormatter={(x) => formatFixedDecimals(x, true)}
tickLine={false}
/>
</AreaChart>

View File

@ -90,8 +90,8 @@ const AccountOnboardingTour = () => {
},
{
selector: '#account-step-eleven',
title: t('health-check'),
description: t('health-check-desc'),
title: t('account-summary'),
description: t('account-summary-desc'),
orientationPreferences: [CardinalOrientation.EASTSOUTH],
movingTarget: true,
},

View File

@ -17,7 +17,7 @@ const CustomTooltip = ({
const { title, description } = tourLogic!.stepContent
const { next, prev, close, allSteps, stepIndex } = tourLogic!
const { publicKey } = useWallet()
const actions = mangoStore((s) => s.actions)
const actions = mangoStore.getState().actions
const tourSettings = mangoStore((s) => s.settings.tours)
const [loading, setLoading] = useState(false)

View File

@ -22,7 +22,7 @@ import { notify } from 'utils/notifications'
import SpotSlider from './SpotSlider'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import Image from 'next/legacy/image'
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
import { LinkIcon, QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
import Loading from '@components/shared/Loading'
import TabUnderline from '@components/shared/TabUnderline'
import PerpSlider from './PerpSlider'
@ -42,6 +42,8 @@ import useMangoAccount from 'hooks/useMangoAccount'
import MaxSizeButton from './MaxSizeButton'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import { Howl } from 'howler'
import { useWallet } from '@solana/wallet-adapter-react'
import { useEnhancedWallet } from '@components/wallet/EnhancedWalletProvider'
const set = mangoStore.getState().set
@ -64,6 +66,8 @@ const AdvancedTradeForm = () => {
SOUND_SETTINGS_KEY,
INITIAL_SOUND_SETTINGS
)
const { connected } = useWallet()
const { handleConnect } = useEnhancedWallet()
const baseSymbol = useMemo(() => {
return selectedMarket?.name.split(/-|\//)[0]
@ -188,9 +192,9 @@ const AdvancedTradeForm = () => {
})
}, [])
const tickDecimals = useMemo(() => {
const [tickDecimals, tickSize] = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return 1
if (!group || !selectedMarket) return [1, 0.1]
let tickSize: number
if (selectedMarket instanceof Serum3Market) {
const market = group.getSerum3ExternalMarket(
@ -200,7 +204,8 @@ const AdvancedTradeForm = () => {
} else {
tickSize = selectedMarket.tickSize
}
return getDecimalCount(tickSize)
const tickDecimals = getDecimalCount(tickSize)
return [tickDecimals, tickSize]
}, [selectedMarket])
/*
@ -337,19 +342,20 @@ const AdvancedTradeForm = () => {
}
}, [])
const minOrderDecimals = useMemo(() => {
const [minOrderDecimals, minOrderSize] = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket) return 1
let minOrderDecimals = 1
if (!group || !selectedMarket) return [1, 0.1]
let minOrderSize: number
if (selectedMarket instanceof Serum3Market) {
const market = group.getSerum3ExternalMarket(
selectedMarket.serumMarketExternal
)
minOrderDecimals = getDecimalCount(market.minOrderSize)
minOrderSize = market.minOrderSize
} else {
minOrderDecimals = getDecimalCount(selectedMarket.minOrderSize)
minOrderSize = selectedMarket.minOrderSize
}
return minOrderDecimals
const minOrderDecimals = getDecimalCount(minOrderSize)
return [minOrderDecimals, minOrderSize]
}, [selectedMarket])
return (
@ -363,7 +369,7 @@ const AdvancedTradeForm = () => {
/>
</div>
<div className="px-3 md:px-4">
<SolBalanceWarnings />
<SolBalanceWarnings className="mt-4" />
</div>
<div className="mt-1 px-2 md:mt-3 md:px-4">
<p className="mb-2 text-xs">{t('trade:order-type')}</p>
@ -478,6 +484,7 @@ const AdvancedTradeForm = () => {
<SpotSlider
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
step={tradeForm.side === 'buy' ? tickSize : minOrderSize}
/>
) : (
<SpotButtonGroup
@ -554,16 +561,23 @@ const AdvancedTradeForm = () => {
<div className="mt-6 mb-4 flex px-3 md:px-4">
{ipAllowed ? (
<Button
onClick={handlePlaceOrder}
className={`flex w-full items-center justify-center text-white ${
tradeForm.side === 'buy'
? 'bg-th-up-dark md:hover:bg-th-up'
: 'bg-th-down-dark md:hover:bg-th-down'
onClick={connected ? handlePlaceOrder : handleConnect}
className={`flex w-full items-center justify-center ${
!connected
? ''
: tradeForm.side === 'buy'
? 'bg-th-up-dark text-white md:hover:bg-th-up'
: 'bg-th-down-dark text-white md:hover:bg-th-down'
}`}
disabled={!tradeForm.baseSize}
disabled={connected && !tradeForm.baseSize}
size="large"
>
{!placingOrder ? (
{!connected ? (
<div className="flex items-center">
<LinkIcon className="mr-2 h-5 w-5" />
{t('connect')}
</div>
) : !placingOrder ? (
<span className="capitalize">
{t('trade:place-order', { side: tradeForm.side })}
</span>
@ -575,17 +589,11 @@ const AdvancedTradeForm = () => {
)}
</Button>
) : (
<div className="flex-grow">
<div className="flex">
<Button disabled className="flex-grow">
<span>
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '(Unknown)',
})}
</span>
</Button>
</div>
</div>
<Button disabled className="w-full leading-tight" size="large">
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
</div>
<TradeSummary mangoAccount={mangoAccount} />

View File

@ -6,7 +6,7 @@ import useMangoAccount from 'hooks/useMangoAccount'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import { useCallback, useMemo } from 'react'
import { trimDecimals } from 'utils/numbers'
import { floorToDecimal } from 'utils/numbers'
const MaxSizeButton = ({
minOrderDecimals,
@ -18,7 +18,7 @@ const MaxSizeButton = ({
const { t } = useTranslation(['common', 'trade'])
const { mangoAccount } = useMangoAccount()
const { selectedMarket, price: oraclePrice } = useSelectedMarket()
const tradeForm = mangoStore((s) => s.tradeForm)
const { price, side, tradeType } = mangoStore((s) => s.tradeForm)
const leverageMax = useMemo(() => {
const group = mangoStore.getState().group
@ -26,7 +26,7 @@ const MaxSizeButton = ({
try {
if (selectedMarket instanceof Serum3Market) {
if (tradeForm.side === 'buy') {
if (side === 'buy') {
return mangoAccount.getMaxQuoteForSerum3BidUi(
group,
selectedMarket.serumMarketExternal
@ -38,7 +38,7 @@ const MaxSizeButton = ({
)
}
} else {
if (tradeForm.side === 'buy') {
if (side === 'buy') {
return mangoAccount.getMaxQuoteForPerpBidUi(
group,
selectedMarket.perpMarketIndex
@ -54,65 +54,60 @@ const MaxSizeButton = ({
console.error('Error calculating max leverage: spot btn group: ', e)
return 0
}
}, [mangoAccount, tradeForm.side, selectedMarket])
}, [mangoAccount, side, selectedMarket])
const handleMax = useCallback(() => {
const set = mangoStore.getState().set
set((state) => {
if (tradeForm.side === 'buy') {
state.tradeForm.quoteSize = trimDecimals(
if (side === 'buy') {
state.tradeForm.quoteSize = floorToDecimal(
leverageMax,
tickDecimals
).toFixed(tickDecimals)
if (tradeForm.tradeType === 'Market' || !tradeForm.price) {
state.tradeForm.baseSize = trimDecimals(
).toFixed()
if (tradeType === 'Market' || !price) {
state.tradeForm.baseSize = floorToDecimal(
leverageMax / oraclePrice,
minOrderDecimals
).toFixed(minOrderDecimals)
).toFixed()
} else {
state.tradeForm.baseSize = trimDecimals(
leverageMax / parseFloat(tradeForm.price),
state.tradeForm.baseSize = floorToDecimal(
leverageMax / parseFloat(price),
minOrderDecimals
).toFixed(minOrderDecimals)
).toFixed()
}
} else {
state.tradeForm.baseSize = trimDecimals(
state.tradeForm.baseSize = floorToDecimal(
leverageMax,
tickDecimals
).toFixed(tickDecimals)
if (tradeForm.tradeType === 'Market' || !tradeForm.price) {
state.tradeForm.quoteSize = trimDecimals(
).toFixed()
if (tradeType === 'Market' || !price) {
state.tradeForm.quoteSize = floorToDecimal(
leverageMax * oraclePrice,
minOrderDecimals
).toFixed(minOrderDecimals)
).toFixed()
} else {
state.tradeForm.quoteSize = trimDecimals(
leverageMax * parseFloat(tradeForm.price),
state.tradeForm.quoteSize = floorToDecimal(
leverageMax * parseFloat(price),
minOrderDecimals
).toFixed(minOrderDecimals)
).toFixed()
}
}
})
}, [leverageMax, tradeForm])
}, [leverageMax, price, side, tradeType])
const maxAmount = useMemo(() => {
if (!tradeForm.price) return '0'
if (tradeForm.side === 'buy') {
return trimDecimals(
leverageMax / parseFloat(tradeForm.price),
tickDecimals
).toFixed(tickDecimals)
const tradePrice = tradeType === 'Market' ? oraclePrice : Number(price)
if (side === 'buy') {
return floorToDecimal(leverageMax / tradePrice, tickDecimals).toFixed()
} else {
return trimDecimals(leverageMax, minOrderDecimals).toFixed(
minOrderDecimals
)
return floorToDecimal(leverageMax, minOrderDecimals).toFixed()
}
}, [leverageMax, minOrderDecimals, tickDecimals, tradeForm])
}, [leverageMax, minOrderDecimals, tickDecimals, price, side, tradeType])
return (
<div className="mb-2 mt-3 flex items-center justify-between">
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
<FadeInFadeOut show={!!tradeForm.price}>
<FadeInFadeOut show={!!price}>
<MaxAmountButton
className="text-xs"
label={t('max')}

View File

@ -15,7 +15,6 @@ import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import {
CheckIcon,
LinkIcon,
NoSymbolIcon,
PencilIcon,
TrashIcon,
@ -25,6 +24,7 @@ import { Order } from '@project-serum/serum/lib/market'
import { useWallet } from '@solana/wallet-adapter-react'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'next-i18next'
import { ChangeEvent, useCallback, useState } from 'react'
@ -35,7 +35,6 @@ import TableMarketName from './TableMarketName'
const OpenOrders = () => {
const { t } = useTranslation(['common', 'trade'])
const { connected } = useWallet()
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
const [cancelId, setCancelId] = useState<string>('')
const [modifyOrderId, setModifyOrderId] = useState<string | undefined>(
@ -46,6 +45,8 @@ const OpenOrders = () => {
const [modifiedOrderPrice, setModifiedOrderPrice] = useState('')
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const { mangoAccount } = useMangoAccount()
const { connected } = useWallet()
const findSerum3MarketPkInOpenOrders = (o: Order): string | undefined => {
const openOrders = mangoStore.getState().mangoAccount.openOrders
@ -224,110 +225,108 @@ const OpenOrders = () => {
setModifiedOrderPrice('')
}
return connected ? (
Object.values(openOrders).flat().length ? (
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="w-[16.67%] text-left">{t('market')}</Th>
<Th className="w-[16.67%] text-right">{t('trade:side')}</Th>
<Th className="w-[16.67%] text-right">{t('trade:size')}</Th>
<Th className="w-[16.67%] text-right">{t('price')}</Th>
<Th className="w-[16.67%] text-right">{t('value')}</Th>
<Th className="w-[16.67%] text-right"></Th>
</TrHead>
</thead>
<tbody>
{Object.entries(openOrders)
.map(([marketPk, orders]) => {
return orders.map((o) => {
const group = mangoStore.getState().group!
let market: PerpMarket | Serum3Market
let tickSize: number
let minOrderSize: number
let quoteSymbol
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
quoteSymbol = group.getFirstBankByTokenIndex(
market.settleTokenIndex
).name
tickSize = market.tickSize
minOrderSize = market.minOrderSize
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk)
)
quoteSymbol = group.getFirstBankByTokenIndex(
market!.quoteTokenIndex
).name
const serumMarket = group.getSerum3ExternalMarket(
market.serumMarketExternal
)
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
}
return (
<TrBody
key={`${o.side}${o.size}${o.price}`}
className="my-1 p-2"
>
<Td className="w-[16.67%]">
<TableMarketName market={market} />
</Td>
<Td className="w-[16.67%] text-right">
<SideBadge side={o.side} />
</Td>
{modifyOrderId !== o.orderId.toString() ? (
<>
<Td className="w-[16.67%] text-right font-mono">
{o.size.toLocaleString(undefined, {
maximumFractionDigits:
getDecimalCount(minOrderSize),
})}
</Td>
<Td className="w-[16.67%] whitespace-nowrap text-right">
<span className="font-mono">
{o.price.toLocaleString(undefined, {
minimumFractionDigits:
getDecimalCount(tickSize),
maximumFractionDigits:
getDecimalCount(tickSize),
})}{' '}
<span className="font-body tracking-wide text-th-fgd-4">
{quoteSymbol}
</span>
return mangoAccount && Object.values(openOrders).flat().length ? (
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="w-[16.67%] text-left">{t('market')}</Th>
<Th className="w-[16.67%] text-right">{t('trade:side')}</Th>
<Th className="w-[16.67%] text-right">{t('trade:size')}</Th>
<Th className="w-[16.67%] text-right">{t('price')}</Th>
<Th className="w-[16.67%] text-right">{t('value')}</Th>
{connected ? <Th className="w-[16.67%] text-right"></Th> : null}
</TrHead>
</thead>
<tbody>
{Object.entries(openOrders)
.map(([marketPk, orders]) => {
return orders.map((o) => {
const group = mangoStore.getState().group!
let market: PerpMarket | Serum3Market
let tickSize: number
let minOrderSize: number
let quoteSymbol
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
quoteSymbol = group.getFirstBankByTokenIndex(
market.settleTokenIndex
).name
tickSize = market.tickSize
minOrderSize = market.minOrderSize
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk)
)
quoteSymbol = group.getFirstBankByTokenIndex(
market!.quoteTokenIndex
).name
const serumMarket = group.getSerum3ExternalMarket(
market.serumMarketExternal
)
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
}
return (
<TrBody
key={`${o.side}${o.size}${o.price}`}
className="my-1 p-2"
>
<Td className="w-[16.67%]">
<TableMarketName market={market} />
</Td>
<Td className="w-[16.67%] text-right">
<SideBadge side={o.side} />
</Td>
{modifyOrderId !== o.orderId.toString() ? (
<>
<Td className="w-[16.67%] text-right font-mono">
{o.size.toLocaleString(undefined, {
maximumFractionDigits:
getDecimalCount(minOrderSize),
})}
</Td>
<Td className="w-[16.67%] whitespace-nowrap text-right">
<span className="font-mono">
{o.price.toLocaleString(undefined, {
minimumFractionDigits: getDecimalCount(tickSize),
maximumFractionDigits: getDecimalCount(tickSize),
})}{' '}
<span className="font-body text-th-fgd-4">
{quoteSymbol}
</span>
</Td>
</>
) : (
<>
<Td className="w-[16.67%]">
<Input
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderSize}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderSize(e.target.value)
}
/>
</Td>
<Td className="w-[16.67%]">
<Input
autoFocus
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderPrice}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderPrice(e.target.value)
}
/>
</Td>
</>
)}
<Td className="w-[16.67%] text-right">
{formatFixedDecimals(o.size * o.price, true)}
</Td>
</span>
</Td>
</>
) : (
<>
<Td className="w-[16.67%]">
<Input
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderSize}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderSize(e.target.value)
}
/>
</Td>
<Td className="w-[16.67%]">
<Input
autoFocus
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderPrice}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderPrice(e.target.value)
}
/>
</Td>
</>
)}
<Td className="w-[16.67%] text-right">
{formatFixedDecimals(o.size * o.price, true, true)}
</Td>
{connected ? (
<Td className="w-[16.67%]">
<div className="flex justify-end space-x-2">
{modifyOrderId !== o.orderId.toString() ? (
@ -378,101 +377,103 @@ const OpenOrders = () => {
)}
</div>
</Td>
</TrBody>
)
})
) : null}
</TrBody>
)
})
.flat()}
</tbody>
</Table>
) : (
<div>
{Object.entries(openOrders).map(([marketPk, orders]) => {
return orders.map((o) => {
const group = mangoStore.getState().group!
let market: PerpMarket | Serum3Market
let tickSize: number
let minOrderSize: number
let quoteSymbol: string
let baseSymbol: string
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
baseSymbol = market.name.split('-')[0]
quoteSymbol = group.getFirstBankByTokenIndex(
market.settleTokenIndex
).name
tickSize = market.tickSize
minOrderSize = market.minOrderSize
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk)
)
baseSymbol = market.name.split('/')[0]
quoteSymbol = group.getFirstBankByTokenIndex(
market!.quoteTokenIndex
).name
const serumMarket = group.getSerum3ExternalMarket(
market.serumMarketExternal
)
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
}
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
key={`${o.side}${o.size}${o.price}`}
>
<div>
<TableMarketName market={market} />
{modifyOrderId !== o.orderId.toString() ? (
<div className="mt-1 flex items-center space-x-1">
<SideBadge side={o.side} />
<p className="text-th-fgd-4">
<span className="font-mono text-th-fgd-3">
{o.size.toLocaleString(undefined, {
maximumFractionDigits:
getDecimalCount(minOrderSize),
})}
</span>{' '}
{baseSymbol}
{' for '}
<span className="font-mono text-th-fgd-3">
{o.price.toLocaleString(undefined, {
minimumFractionDigits: getDecimalCount(tickSize),
maximumFractionDigits: getDecimalCount(tickSize),
})}
</span>{' '}
{quoteSymbol}
</p>
})
.flat()}
</tbody>
</Table>
) : (
<div>
{Object.entries(openOrders).map(([marketPk, orders]) => {
return orders.map((o) => {
const group = mangoStore.getState().group!
let market: PerpMarket | Serum3Market
let tickSize: number
let minOrderSize: number
let quoteSymbol: string
let baseSymbol: string
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
baseSymbol = market.name.split('-')[0]
quoteSymbol = group.getFirstBankByTokenIndex(
market.settleTokenIndex
).name
tickSize = market.tickSize
minOrderSize = market.minOrderSize
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk)
)
baseSymbol = market.name.split('/')[0]
quoteSymbol = group.getFirstBankByTokenIndex(
market!.quoteTokenIndex
).name
const serumMarket = group.getSerum3ExternalMarket(
market.serumMarketExternal
)
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
}
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
key={`${o.side}${o.size}${o.price}`}
>
<div>
<TableMarketName market={market} />
{modifyOrderId !== o.orderId.toString() ? (
<div className="mt-1 flex items-center space-x-1">
<SideBadge side={o.side} />
<p className="text-th-fgd-4">
<span className="font-mono text-th-fgd-3">
{o.size.toLocaleString(undefined, {
maximumFractionDigits:
getDecimalCount(minOrderSize),
})}
</span>{' '}
{baseSymbol}
{' for '}
<span className="font-mono text-th-fgd-3">
{o.price.toLocaleString(undefined, {
minimumFractionDigits: getDecimalCount(tickSize),
maximumFractionDigits: getDecimalCount(tickSize),
})}
</span>{' '}
{quoteSymbol}
</p>
</div>
) : (
<div className="mt-2 flex space-x-4">
<div>
<p className="text-xs">{t('trade:size')}</p>
<Input
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderSize}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderSize(e.target.value)
}
/>
</div>
) : (
<div className="mt-2 flex space-x-4">
<div>
<p className="text-xs">{t('trade:size')}</p>
<Input
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderSize}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderSize(e.target.value)
}
/>
</div>
<div>
<p className="text-xs">{t('price')}</p>
<Input
autoFocus
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderPrice}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderPrice(e.target.value)
}
/>
</div>
<div>
<p className="text-xs">{t('price')}</p>
<Input
autoFocus
className="default-transition h-7 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-active focus:outline-none"
type="text"
value={modifiedOrderPrice}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setModifiedOrderPrice(e.target.value)
}
/>
</div>
)}
</div>
</div>
)}
</div>
{connected ? (
<div className="flex items-center space-x-3 pl-8">
<div className="flex items-center space-x-2">
{modifyOrderId !== o.orderId.toString() ? (
@ -513,22 +514,17 @@ const OpenOrders = () => {
)}
</div>
</div>
</div>
)
})
})}
</div>
)
) : (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-orders')}</p>
) : null}
</div>
)
})
})}
</div>
)
) : (
<div className="flex flex-col items-center p-8">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:connect-orders')}</p>
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-orders')}</p>
</div>
)
}

View File

@ -24,6 +24,8 @@ import {
} from '@blockworks-foundation/mango-v4'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import { ArrowPathIcon } from '@heroicons/react/20/solid'
import { sleep } from 'utils'
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
const depth = 40
@ -406,6 +408,13 @@ const Orderbook = () => {
// return () => clearTimeout(id)
}, [verticallyCenterOrderbook])
const resetOrderbook = useCallback(async () => {
setShowBuys(true)
setShowSells(true)
await sleep(300)
verticallyCenterOrderbook()
}, [])
const onGroupSizeChange = useCallback((groupSize: number) => {
setGrouping(groupSize)
}, [])
@ -417,10 +426,10 @@ const Orderbook = () => {
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-th-bkg-3 px-4 py-2">
<div id="trade-step-three" className="flex items-center space-x-2">
<div id="trade-step-three" className="flex items-center space-x-1.5">
<Tooltip
content={showBuys ? t('trade:hide-bids') : t('trade:show-bids')}
placement="top"
placement="bottom"
>
<button
className={`rounded ${
@ -434,7 +443,7 @@ const Orderbook = () => {
</Tooltip>
<Tooltip
content={showSells ? t('trade:hide-asks') : t('trade:show-asks')}
placement="top"
placement="bottom"
>
<button
className={`rounded ${
@ -446,10 +455,24 @@ const Orderbook = () => {
<OrderbookIcon className="h-4 w-4" side="sell" />
</button>
</Tooltip>
<Tooltip content={'Reset and center orderbook'} placement="bottom">
<button
className={`rounded ${
showSells ? 'bg-th-bkg-3' : 'bg-th-bkg-2'
} default-transition flex h-6 w-6 items-center justify-center hover:border-th-fgd-4 focus:outline-none disabled:cursor-not-allowed`}
onClick={resetOrderbook}
>
<ArrowPathIcon className="h-4 w-4" />
</button>
</Tooltip>
</div>
{market ? (
<div id="trade-step-four">
<Tooltip content={t('trade:grouping')} placement="top" delay={250}>
<Tooltip
content={t('trade:grouping')}
placement="bottom"
delay={250}
>
<GroupSize
tickSize={market.tickSize}
onChange={onGroupSizeChange}

View File

@ -80,7 +80,7 @@ const PerpButtonGroup = ({
)
return (
<div className="w-full px-4">
<div className="w-full px-3 md:px-4">
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}

View File

@ -1,9 +1,9 @@
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { LinkButton } from '@components/shared/Button'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import { LinkIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import { NoSymbolIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
@ -19,10 +19,10 @@ import TableMarketName from './TableMarketName'
const PerpPositions = () => {
const { t } = useTranslation(['common', 'trade'])
const { connected } = useWallet()
const { group } = useMangoGroup()
const perpPositions = mangoStore((s) => s.mangoAccount.perpPositions)
const { selectedMarket } = useSelectedMarket()
const { mangoAccount } = useMangoAccount()
const handlePositionClick = (positionSize: number) => {
const tradeForm = mangoStore.getState().tradeForm
@ -56,109 +56,98 @@ const PerpPositions = () => {
p.basePositionLots.toNumber()
)
return connected ? (
openPerpPositions.length ? (
<div>
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('notional')}</Th>
<Th className="text-right">{t('trade:entry-price')}</Th>
<Th className="text-right">Redeemable PnL</Th>
<Th className="text-right">Realized PnL</Th>
</TrHead>
</thead>
<tbody>
{openPerpPositions.map((position) => {
const market = group.getPerpMarketByMarketIndex(
position.marketIndex
)
const basePosition = position.getBasePositionUi(market)
const trimmedBasePosition = trimDecimals(
basePosition,
getDecimalCount(market.minOrderSize)
)
const isSelectedMarket =
selectedMarket instanceof PerpMarket &&
selectedMarket.perpMarketIndex === position.marketIndex
return mangoAccount && openPerpPositions.length ? (
<div>
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('trade:notional')}</Th>
<Th className="text-right">{t('trade:entry-price')}</Th>
<Th className="text-right">Redeemable PnL</Th>
<Th className="text-right">Realized PnL</Th>
</TrHead>
</thead>
<tbody>
{openPerpPositions.map((position) => {
const market = group.getPerpMarketByMarketIndex(
position.marketIndex
)
const basePosition = position.getBasePositionUi(market)
const trimmedBasePosition = trimDecimals(
basePosition,
getDecimalCount(market.minOrderSize)
)
const isSelectedMarket =
selectedMarket instanceof PerpMarket &&
selectedMarket.perpMarketIndex === position.marketIndex
if (!basePosition) return null
if (!basePosition) return null
const unsettledPnl = position.getEquityUi(group, market)
const unsettledPnl = position.getEquityUi(group, market)
return (
<TrBody key={`${position.marketIndex}`} className="my-1 p-2">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right">
<PerpSideBadge basePosition={basePosition} />
</Td>
<Td className="text-right">
<p className="flex justify-end">
{isSelectedMarket ? (
<LinkButton
onClick={() =>
handlePositionClick(trimmedBasePosition)
}
>
{Math.abs(trimmedBasePosition)}
</LinkButton>
) : (
Math.abs(trimmedBasePosition)
)}
</p>
</Td>
<Td className="text-right font-mono">
<div>
$
{Math.abs(trimmedBasePosition * market._uiPrice).toFixed(
2
)}
</div>
</Td>
<Td className="text-right font-mono">
<div>
$
{numberFormat.format(
position.getAverageEntryPriceUi(market)
)}
</div>
</Td>
<Td
className={`text-right font-mono ${
unsettledPnl > 0 ? 'text-th-up' : 'text-th-down'
}`}
>
<div>${formatFixedDecimals(unsettledPnl)}</div>
</Td>
<Td className="text-right">
<div>
$
{/* {numberFormat.format(
return (
<TrBody key={`${position.marketIndex}`} className="my-1 p-2">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right">
<PerpSideBadge basePosition={basePosition} />
</Td>
<Td className="text-right">
<p className="flex justify-end">
{isSelectedMarket ? (
<LinkButton
onClick={() => handlePositionClick(trimmedBasePosition)}
>
{Math.abs(trimmedBasePosition)}
</LinkButton>
) : (
Math.abs(trimmedBasePosition)
)}
</p>
</Td>
<Td className="text-right font-mono">
<div>
$
{Math.abs(trimmedBasePosition * market._uiPrice).toFixed(2)}
</div>
</Td>
<Td className="text-right font-mono">
<div>
$
{numberFormat.format(
position.getAverageEntryPriceUi(market)
)}
</div>
</Td>
<Td
className={`text-right font-mono ${
unsettledPnl > 0 ? 'text-th-up' : 'text-th-down'
}`}
>
<div>{formatFixedDecimals(unsettledPnl, true)}</div>
</Td>
<Td className="text-right">
<div>
$
{/* {numberFormat.format(
position.perpSpotTransfers.toNumber()
)} */}
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
) : (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-positions')}</p>
</div>
)
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
) : (
<div className="flex flex-col items-center p-8">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:connect-positions')}</p>
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-positions')}</p>
</div>
)
}

View File

@ -83,7 +83,7 @@ const PerpSlider = ({
)
return (
<div className="w-full px-4">
<div className="w-full px-3 md:px-4">
<LeverageSlider
amount={
tradeForm.side === 'buy'

View File

@ -1,7 +1,6 @@
import useInterval from '@components/shared/useInterval'
import mangoStore from '@store/mangoStore'
import { useEffect, useMemo, useRef } from 'react'
import isEqual from 'lodash/isEqual'
import { useEffect, useMemo } from 'react'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import Decimal from 'decimal.js'
import { ChartTradeType } from 'types'
@ -14,6 +13,7 @@ import { SOUND_SETTINGS_KEY } from 'utils/constants'
import { SpeakerWaveIcon, SpeakerXMarkIcon } from '@heroicons/react/20/solid'
import Tooltip from '@components/shared/Tooltip'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import usePrevious from '@components/shared/usePrevious'
const buySound = new Howl({
src: ['/sounds/trade-buy.mp3'],
@ -25,15 +25,30 @@ const sellSound = new Howl({
})
const RecentTrades = () => {
// const [trades, setTrades] = useState<any[]>([])
const { t } = useTranslation(['common', 'trade'])
const fills = mangoStore((s) => s.selectedMarket.fills)
const [soundSettings, setSoundSettings] = useLocalStorageState(
SOUND_SETTINGS_KEY,
INITIAL_SOUND_SETTINGS
)
const currentFillsData = useRef<any>([])
const nextFillsData = useRef<any>([])
const previousFills = usePrevious(fills)
useEffect(() => {
if (!soundSettings['recent-trades']) return
if (fills.length && previousFills && previousFills.length) {
const latestFill: ChartTradeType = fills[0]
const previousFill: ChartTradeType = previousFills[0]
if (previousFill.orderId.toString() !== latestFill.orderId.toString()) {
const side =
latestFill.side || (latestFill.takerSide === 1 ? 'bid' : 'ask')
if (['buy', 'bid'].includes(side)) {
buySound.play()
} else {
sellSound.play()
}
}
}
}, [fills, previousFills, soundSettings])
const { selectedMarket, serumOrPerpMarket: market } = useSelectedMarket()
@ -45,33 +60,6 @@ const RecentTrades = () => {
return selectedMarket?.name.split(/-|\//)[1]
}, [selectedMarket])
// needs more testing
useEffect(() => {
if (soundSettings['recent-trades']) {
mangoStore.subscribe(
(state) => [state.selectedMarket.fills],
(fills) => (nextFillsData.current = fills[0])
)
}
}, [soundSettings['recent-trades']])
// needs more testing
useInterval(() => {
if (soundSettings['recent-trades']) {
if (fills.length) {
currentFillsData.current = fills
}
if (
nextFillsData.current.length &&
!isEqual(currentFillsData.current, nextFillsData.current)
) {
nextFillsData.current[0].side === 'buy'
? buySound.play()
: sellSound.play()
}
}
}, 1000)
// const fetchRecentTrades = useCallback(async () => {
// if (!market) return
@ -109,9 +97,27 @@ const RecentTrades = () => {
actions.loadMarketFills()
}, 5000)
const [buyRatio, sellRatio] = useMemo(() => {
if (!fills.length) return [0, 0]
const vol = fills.reduce(
(a: { buys: number; sells: number }, c: any) => {
if (c.side === 'buy' || c.takerSide === 1) {
a.buys = a.buys + c.size
} else {
a.sells = a.sells + c.size
}
return a
},
{ buys: 0, sells: 0 }
)
const totalVol = vol.buys + vol.sells
return [vol.buys / totalVol, vol.sells / totalVol]
}, [fills])
return (
<div className="thin-scroll h-full overflow-y-scroll">
<div className="flex justify-end border-b border-th-bkg-3 px-2 py-1">
<div className="flex items-center justify-between border-b border-th-bkg-3 py-1 px-2">
<Tooltip content={t('trade:trade-sounds-tooltip')} delay={250}>
<IconButton
onClick={() =>
@ -130,6 +136,17 @@ const RecentTrades = () => {
)}
</IconButton>
</Tooltip>
<span className="text-xs text-th-fgd-4">
{t('trade:buys')}:{' '}
<span className="font-mono text-th-up">
{(buyRatio * 100).toFixed(1)}%
</span>
<span className="px-2">|</span>
{t('trade:sells')}:{' '}
<span className="font-mono text-th-down">
{(sellRatio * 100).toFixed(1)}%
</span>
</span>
</div>
<div className="px-2">
<table className="min-w-full">
@ -147,7 +164,8 @@ const RecentTrades = () => {
<tbody>
{!!fills.length &&
fills.map((trade: ChartTradeType, i: number) => {
const side = trade.side || trade.takerSide === 1 ? 'bid' : 'ask'
const side =
trade.side || (trade.takerSide === 1 ? 'bid' : 'ask')
// const price =
// typeof trade.price === 'number'
@ -174,8 +192,8 @@ const RecentTrades = () => {
<td
className={`pb-1.5 text-right ${
['buy', 'bid'].includes(side)
? `text-th-up`
: `text-th-down`
? 'text-th-up'
: 'text-th-down'
}`}
>
{formattedPrice.toFixed()}

View File

@ -4,7 +4,7 @@ import useMarkPrice from 'hooks/useMarkPrice'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import { useMemo } from 'react'
import { notify } from 'utils/notifications'
// import { notify } from 'utils/notifications'
import { calculateSlippage } from 'utils/tradeForm'
const Slippage = () => {
@ -25,7 +25,8 @@ const Slippage = () => {
)
}
} catch (e) {
notify({ type: 'info', title: 'Unable to calculate slippage' })
console.error({ type: 'info', title: 'Unable to calculate slippage' })
return 100_000
}
return 0
}, [tradeForm, markPrice, selectedMarket])
@ -44,7 +45,7 @@ const Slippage = () => {
: 'text-th-error'
}`}
>
{slippage.toFixed(2)}%
{slippage === 100_000 ? 'Unavailable' : `${slippage.toFixed(2)}&`}
</p>
</div>
) : null

View File

@ -79,7 +79,7 @@ const SpotButtonGroup = ({
)
return (
<div className="w-full px-4">
<div className="w-full px-3 md:px-4">
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}

View File

@ -9,9 +9,11 @@ import { trimDecimals } from 'utils/numbers'
const SpotSlider = ({
minOrderDecimals,
tickDecimals,
step,
}: {
minOrderDecimals: number
tickDecimals: number
step: number
}) => {
const side = mangoStore((s) => s.tradeForm.side)
const { selectedMarket, price: marketPrice } = useSelectedMarket()
@ -76,7 +78,7 @@ const SpotSlider = ({
)
return (
<div className="w-full px-4">
<div className="w-full px-3 md:px-4">
<LeverageSlider
amount={
tradeForm.side === 'buy'
@ -85,7 +87,7 @@ const SpotSlider = ({
}
leverageMax={leverageMax}
onChange={handleSlide}
step={0.01}
step={step}
/>
</div>
)

View File

@ -1,5 +1,4 @@
import { I80F48, PerpMarket } from '@blockworks-foundation/mango-v4'
import InlineNotification from '@components/shared/InlineNotification'
import SideBadge from '@components/shared/SideBadge'
import {
Table,
@ -9,15 +8,14 @@ import {
TrBody,
TrHead,
} from '@components/shared/TableElements'
import { LinkIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import { NoSymbolIcon } from '@heroicons/react/20/solid'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useViewport } from 'hooks/useViewport'
import { useMemo } from 'react'
import { formatDecimal } from 'utils/numbers'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import { breakpoints } from 'utils/theme'
import TableMarketName from './TableMarketName'
@ -30,8 +28,8 @@ const byTimestamp = (a: any, b: any) => {
const reverseSide = (side: string) => (side === 'buy' ? 'sell' : 'buy')
const parsedPerpEvent = (mangoAccountPk: PublicKey, event: any) => {
const maker = event.maker.toString() === mangoAccountPk.toString()
const parsedPerpEvent = (mangoAccountAddress: string, event: any) => {
const maker = event.maker.toString() === mangoAccountAddress
const orderId = maker ? event.makerOrderId : event.takerOrderId
const value = event.quantity * event.price
const feeRate = maker
@ -69,14 +67,17 @@ const parsedSerumEvent = (event: any) => {
}
}
const formatTradeHistory = (mangoAccountPk: PublicKey, tradeHistory: any[]) => {
const formatTradeHistory = (
mangoAccountAddress: string,
tradeHistory: any[]
) => {
return tradeHistory
.flat()
.map((event) => {
if (event.eventFlags || event.nativeQuantityPaid) {
return parsedSerumEvent(event)
} else if (event.maker) {
return parsedPerpEvent(mangoAccountPk, event)
return parsedPerpEvent(mangoAccountAddress, event)
} else {
return event
}
@ -85,29 +86,33 @@ const formatTradeHistory = (mangoAccountPk: PublicKey, tradeHistory: any[]) => {
}
const TradeHistory = () => {
const { connected } = useWallet()
const group = mangoStore.getState().group
const { selectedMarket } = useSelectedMarket()
const { mangoAccount } = useMangoAccount()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const fills = mangoStore((s) => s.selectedMarket.fills)
const tradeHistory = mangoStore((s) => s.mangoAccount.tradeHistory)
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const openOrderOwner = useMemo(() => {
if (!mangoAccount || !selectedMarket) return
try {
if (selectedMarket instanceof PerpMarket) {
return mangoAccount.publicKey
} else {
if (selectedMarket instanceof PerpMarket) {
return mangoAccount.publicKey
} else {
try {
return mangoAccount.getSerum3OoAccount(selectedMarket.marketIndex)
.address
} catch {
console.warn(
'Unable to find OO account for mkt index',
selectedMarket.marketIndex
)
}
} catch (e) {
console.error('Error loading open order account for trade history: ', e)
}
}, [mangoAccount, selectedMarket])
const tradeHistoryFromEventQueue = useMemo(() => {
if (!mangoAccount || !selectedMarket) return []
const eventQueueFillsForAccount = useMemo(() => {
if (!mangoAccountAddress || !selectedMarket) return []
const mangoAccountFills = fills
.filter((fill: any) => {
@ -124,117 +129,151 @@ const TradeHistory = () => {
})
.map((fill: any) => ({ ...fill, marketName: selectedMarket.name }))
return formatTradeHistory(mangoAccount.publicKey, mangoAccountFills)
}, [selectedMarket, mangoAccount, openOrderOwner])
return formatTradeHistory(mangoAccountAddress, mangoAccountFills)
}, [selectedMarket, mangoAccountAddress, openOrderOwner, fills])
if (!selectedMarket) return null
const combinedTradeHistory = useMemo(() => {
let newFills = []
if (eventQueueFillsForAccount?.length) {
console.log('eventQueueFillsForAccount', eventQueueFillsForAccount)
return connected ? (
tradeHistoryFromEventQueue.length ? (
showTableView ? (
<div>
<Table>
<thead>
<TrHead>
<Th className="text-left">Market</Th>
<Th className="text-right">Side</Th>
<Th className="text-right">Size</Th>
<Th className="text-right">Price</Th>
<Th className="text-right">Value</Th>
<Th className="text-right">Fee</Th>
{selectedMarket instanceof PerpMarket ? (
<Th className="text-right">Time</Th>
) : null}
</TrHead>
</thead>
<tbody>
{tradeHistoryFromEventQueue.map((trade: any) => {
return (
<TrBody key={`${trade.marketIndex}`} className="my-1 p-2">
<Td className="">
<TableMarketName market={selectedMarket} />
</Td>
<Td className="text-right">
<SideBadge side={trade.side} />
</Td>
<Td className="text-right font-mono">{trade.size}</Td>
<Td className="text-right font-mono">
{formatDecimal(trade.price)}
</Td>
<Td className="text-right font-mono">
${trade.value.toFixed(2)}
</Td>
<Td className="text-right">
<span className="font-mono">{trade.feeCost}</span>
<p className="font-body text-xs text-th-fgd-4">{`${
trade.liquidity ? trade.liquidity : ''
}`}</p>
</Td>
{selectedMarket instanceof PerpMarket ? (
<Td className="whitespace-nowrap text-right font-mono">
<TableDateDisplay
date={trade.timestamp.toNumber() * 1000}
showSeconds
/>
</Td>
) : null}
</TrBody>
newFills = eventQueueFillsForAccount.filter((fill) => {
return !tradeHistory.find((t) => {
if (t.order_id) {
return t.order_id === fill.orderId?.toString()
}
// else {
// return t.seq_num === fill.seqNum?.toString()
// }
})
})
}
return [...newFills, ...tradeHistory]
}, [eventQueueFillsForAccount, tradeHistory])
console.log('trade history', tradeHistory)
if (!selectedMarket || !group) return null
return mangoAccount && combinedTradeHistory.length ? (
showTableView ? (
<div>
<Table>
<thead>
<TrHead>
<Th className="text-left">Market</Th>
<Th className="text-right">Side</Th>
<Th className="text-right">Size</Th>
<Th className="text-right">Price</Th>
<Th className="text-right">Value</Th>
<Th className="text-right">Fee</Th>
<Th className="text-right">Time</Th>
</TrHead>
</thead>
<tbody>
{combinedTradeHistory.map((trade: any) => {
let market
if ('market' in trade) {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(trade.market)
)
})}
</tbody>
</Table>
<div className="px-6 py-4">
<InlineNotification
type="info"
desc="During the Mango V4 alpha, only your recent Openbook trades will be displayed here. Full trade history will be available shortly."
/>
</div>
</div>
) : (
<div>
{tradeHistoryFromEventQueue.map((trade: any) => {
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
key={`${trade.marketIndex}`}
>
<div>
<TableMarketName market={selectedMarket} />
<div className="mt-1 flex items-center space-x-1">
} else if ('perp_market' in trade) {
market = group.getPerpMarketByName(trade.perp_market)
} else {
market = selectedMarket
}
let makerTaker = trade.liquidity
if ('maker' in trade) {
makerTaker = trade.maker ? 'Maker' : 'Taker'
if (trade.taker === mangoAccount.publicKey.toString()) {
makerTaker = 'Taker'
}
}
const size = trade.size || trade.quantity
let fee
if (trade.fee_cost || trade.feeCost) {
fee = trade.fee_cost || trade.feeCost
} else {
fee =
trade.maker === mangoAccount.publicKey.toString()
? trade.maker_fee
: trade.taker_fee
}
return (
<TrBody
key={`${trade.signature || trade.marketIndex}${size}`}
className="my-1 p-2"
>
<Td className="">
<TableMarketName market={market} />
</Td>
<Td className="text-right">
<SideBadge side={trade.side} />
<p className="text-th-fgd-4">
<span className="font-mono text-th-fgd-3">
{trade.size}
</span>
{' for '}
<span className="font-mono text-th-fgd-3">
{formatDecimal(trade.price)}
</span>
</Td>
<Td className="text-right font-mono">{size}</Td>
<Td className="text-right font-mono">
{formatDecimal(trade.price)}
</Td>
<Td className="text-right font-mono">
{formatFixedDecimals(trade.price * size)}
</Td>
<Td className="text-right">
<span className="font-mono">{formatDecimal(fee)}</span>
<p className="font-body text-xs text-th-fgd-4">
{makerTaker}
</p>
</div>
</div>
<p className="font-mono">${trade.value.toFixed(2)}</p>
</div>
)
})}
</div>
)
</Td>
<Td className="whitespace-nowrap text-right">
{trade.block_datetime ? (
<TableDateDisplay
date={trade.block_datetime}
showSeconds
/>
) : (
'Recent'
)}
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
) : (
<div className="flex flex-col items-center justify-center px-6 pb-8 pt-4">
<div className="mb-8 w-full">
<InlineNotification
type="info"
desc="During the Mango V4 alpha, only your recent Openbook trades will be displayed here. Full trade history will be available shortly."
/>
</div>
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>No trade history for {selectedMarket?.name}</p>
<div>
{eventQueueFillsForAccount.map((trade: any) => {
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
key={`${trade.marketIndex}`}
>
<div>
<TableMarketName market={selectedMarket} />
<div className="mt-1 flex items-center space-x-1">
<SideBadge side={trade.side} />
<p className="text-th-fgd-4">
<span className="font-mono text-th-fgd-3">
{trade.size}
</span>
{' for '}
<span className="font-mono text-th-fgd-3">
{formatDecimal(trade.price)}
</span>
</p>
</div>
</div>
<p className="font-mono">${trade.value.toFixed(2)}</p>
</div>
)
})}
</div>
)
) : (
<div className="flex flex-col items-center p-8">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>Connect to view your trade history</p>
<div className="flex flex-col items-center justify-center px-6 pb-8 pt-4">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>No trade history</p>
</div>
)
}

View File

@ -18,7 +18,7 @@ const TradeInfoTabs = () => {
const unsettledSpotBalances = useUnsettledSpotBalances()
const unsettledPerpPositions = useUnsettledPerpPositions()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false
const isMobile = width ? width < breakpoints['2xl'] : false
const tabsWithCount: [string, number][] = useMemo(() => {
const unsettledTradeCount =
@ -38,7 +38,7 @@ const TradeInfoTabs = () => {
return (
<div className="hide-scroll h-full overflow-y-scroll pb-5">
<div className="hide-scroll sticky top-0 z-10 overflow-x-auto border-b border-th-bkg-3">
<div className="hide-scroll sticky top-0 z-20 overflow-x-auto border-b border-th-bkg-3">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}

View File

@ -69,7 +69,7 @@ const TradeSummary = ({
: simulatedHealthRatio < 0
? 0
: Math.trunc(simulatedHealthRatio)
}, [group, mangoAccount, selectedMarket, tradeForm.baseSize, tradeForm.side])
}, [group, mangoAccount, selectedMarket, tradeForm])
return (
<div className="space-y-2 px-3 md:px-4">
@ -79,6 +79,7 @@ const TradeSummary = ({
{tradeForm.price && tradeForm.baseSize
? formatFixedDecimals(
parseFloat(tradeForm.price) * parseFloat(tradeForm.baseSize),
false,
true
)
: '0.00'}
@ -93,8 +94,9 @@ const TradeSummary = ({
{group && mangoAccount
? formatFixedDecimals(
toUiDecimalsForQuote(
mangoAccount.getCollateralValue(group)!.toNumber()
mangoAccount.getCollateralValue(group).toNumber()
),
false,
true
)
: ''}

View File

@ -1,7 +1,6 @@
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
import mangoStore from '@store/mangoStore'
import klinecharts, { init, dispose } from 'klinecharts'
import axios from 'axios'
import { useViewport } from 'hooks/useViewport'
import usePrevious from '@components/shared/usePrevious'
import Modal from '@components/shared/Modal'
@ -11,7 +10,6 @@ import {
CHART_QUERY,
DEFAULT_MAIN_INDICATORS,
DEFAULT_SUB_INDICATOR,
HISTORY,
mainTechnicalIndicatorTypes,
MAIN_INDICATOR_CLASS,
ONE_DAY_SECONDS,
@ -20,11 +18,11 @@ import {
} from 'utils/kLineChart'
import Loading from '@components/shared/Loading'
import clsx from 'clsx'
import { API_URL, BE_API_KEY } from 'apis/birdeye/helpers'
import { useTheme } from 'next-themes'
import { COLORS } from 'styles/colors'
import { IconButton } from '@components/shared/Button'
import { ArrowsPointingOutIcon, XMarkIcon } from '@heroicons/react/20/solid'
import { queryBars } from 'apis/birdeye/datafeed'
const UPDATE_INTERVAL = 10000
@ -60,24 +58,17 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
...baseQuery,
time_from: from,
}
const response = await axios.get(`${API_URL}defi/ohlcv/pair`, {
params: query,
headers: {
'X-API-KEY': BE_API_KEY,
},
const response = await queryBars(query.address, query.type, {
firstDataRequest: false,
from: query.time_from,
to: query.time_to,
})
const newData = response.data.data.items as HISTORY[]
const dataSize = newData.length
const dataSize = response.length
const dataList = []
for (let i = 0; i < dataSize; i++) {
const row = newData[i]
const row = response[i]
const kLineModel = {
open: row.o,
low: row.l,
high: row.h,
close: row.c,
volume: row.v,
timestamp: row.unixTime * 1000,
...row,
}
dataList.push(kLineModel)
}

View File

@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'
import { PublicKey } from '@solana/web3.js'
import { IconButton } from '@components/shared/Button'
import { notify } from 'utils/notifications'
import { CheckIcon, LinkIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import { CheckIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
import Tooltip from '@components/shared/Tooltip'
import Loading from '@components/shared/Loading'
import { useViewport } from 'hooks/useViewport'
@ -12,8 +12,10 @@ import { breakpoints } from 'utils/theme'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import useMangoGroup from 'hooks/useMangoGroup'
import { PerpMarket, PerpPosition } from '@blockworks-foundation/mango-v4'
import { useWallet } from '@solana/wallet-adapter-react'
import TableMarketName from './TableMarketName'
import useMangoAccount from 'hooks/useMangoAccount'
import { useWallet } from '@solana/wallet-adapter-react'
import { formatDecimal } from 'utils/numbers'
const UnsettledTrades = ({
unsettledSpotBalances,
@ -23,11 +25,12 @@ const UnsettledTrades = ({
unsettledPerpPositions: PerpPosition[]
}) => {
const { t } = useTranslation(['common', 'trade'])
const { connected } = useWallet()
const { group } = useMangoGroup()
const [settleMktAddress, setSettleMktAddress] = useState<string>('')
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const { mangoAccount } = useMangoAccount()
const { connected } = useWallet()
const handleSettleSerumFunds = useCallback(async (mktAddress: string) => {
const client = mangoStore.getState().client
@ -90,13 +93,6 @@ const UnsettledTrades = ({
?.getEquityUi(group, market) || 0,
}))
.sort((a, b) => sign * (a.pnl - b.pnl))
console.log(
'pnl',
filteredAccounts.map((m) => [
m.mangoAccount.publicKey.toString(),
m.pnl,
])
)
const profitableAccount =
mangoAccountPnl >= 0 ? mangoAccount : filteredAccounts[0].mangoAccount
@ -135,51 +131,48 @@ const UnsettledTrades = ({
if (!group) return null
return connected ? (
return mangoAccount &&
Object.values(unsettledSpotBalances).flat().concat(unsettledPerpPositions)
.length ? (
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="bg-th-bkg-1 text-left">{t('market')}</Th>
<Th className="bg-th-bkg-1 text-right">{t('trade:amount')}</Th>
<Th className="bg-th-bkg-1 text-right" />
</TrHead>
</thead>
<tbody>
{Object.entries(unsettledSpotBalances).map(([mktAddress]) => {
const market = group.getSerum3MarketByExternalMarket(
new PublicKey(mktAddress)
)
const base = market?.name.split('/')[0]
const quote = market?.name.split('/')[1]
showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="bg-th-bkg-1 text-left">{t('market')}</Th>
<Th className="bg-th-bkg-1 text-right">{t('trade:amount')}</Th>
{connected ? <Th className="bg-th-bkg-1 text-right" /> : null}
</TrHead>
</thead>
<tbody>
{Object.entries(unsettledSpotBalances).map(([mktAddress]) => {
const market = group.getSerum3MarketByExternalMarket(
new PublicKey(mktAddress)
)
const base = market?.name.split('/')[0]
const quote = market?.name.split('/')[1]
return (
<TrBody key={mktAddress} className="text-sm">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right font-mono">
<div className="flex justify-end">
{unsettledSpotBalances[mktAddress].base ? (
<div>
{unsettledSpotBalances[mktAddress].base}{' '}
<span className="font-body tracking-wide text-th-fgd-4">
{base}
</span>
</div>
) : null}
{unsettledSpotBalances[mktAddress].quote ? (
<div className="ml-4">
{unsettledSpotBalances[mktAddress].quote}{' '}
<span className="font-body tracking-wide text-th-fgd-4">
{quote}
</span>
</div>
) : null}
</div>
</Td>
return (
<TrBody key={mktAddress} className="text-sm">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right font-mono">
<div className="flex justify-end">
{unsettledSpotBalances[mktAddress].base ? (
<div>
{unsettledSpotBalances[mktAddress].base}{' '}
<span className="font-body text-th-fgd-4">{base}</span>
</div>
) : null}
{unsettledSpotBalances[mktAddress].quote ? (
<div className="ml-4">
{unsettledSpotBalances[mktAddress].quote}{' '}
<span className="font-body text-th-fgd-4">{quote}</span>
</div>
) : null}
</div>
</Td>
{connected ? (
<Td>
<div className="flex justify-end">
<Tooltip content={t('trade:settle-funds')}>
@ -196,74 +189,75 @@ const UnsettledTrades = ({
</Tooltip>
</div>
</Td>
</TrBody>
)
})}
{unsettledPerpPositions.map((position) => {
const market = group.getPerpMarketByMarketIndex(
position.marketIndex
)
return (
<TrBody key={position.marketIndex} className="text-sm">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right font-mono">
{position.getEquityUi(group, market)}
</Td>
<Td>
<div className="flex justify-end">
<Tooltip content={t('trade:settle-funds')}>
<IconButton
onClick={() => handleSettlePerpFunds(market)}
size="small"
>
{settleMktAddress === market.publicKey.toString() ? (
<Loading className="h-4 w-4" />
) : (
<CheckIcon className="h-4 w-4" />
)}
</IconButton>
</Tooltip>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="pb-20">
{Object.entries(unsettledSpotBalances).map(([mktAddress]) => {
const market = group.getSerum3MarketByExternalMarket(
new PublicKey(mktAddress)
) : null}
</TrBody>
)
})}
{unsettledPerpPositions.map((position) => {
const market = group.getPerpMarketByMarketIndex(
position.marketIndex
)
const base = market?.name.split('/')[0]
const quote = market?.name.split('/')[1]
return (
<div
key={mktAddress}
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
>
<TableMarketName market={market} />
<div className="flex items-center space-x-3">
{unsettledSpotBalances[mktAddress].base ? (
<span className="font-mono text-sm">
{unsettledSpotBalances[mktAddress].base}{' '}
<span className="font-body tracking-wide text-th-fgd-4">
{base}
</span>
</span>
) : null}
{unsettledSpotBalances[mktAddress].quote ? (
<span className="font-mono text-sm">
{unsettledSpotBalances[mktAddress].quote}{' '}
<span className="font-body tracking-wide text-th-fgd-4">
{quote}
</span>
</span>
) : null}
<TrBody key={position.marketIndex} className="text-sm">
<Td>
<TableMarketName market={market} />
</Td>
<Td className="text-right font-mono">
{formatDecimal(
position.getEquityUi(group, market),
market.baseDecimals
)}
</Td>
<Td>
<div className="flex justify-end">
<Tooltip content={t('trade:settle-funds')}>
<IconButton
onClick={() => handleSettlePerpFunds(market)}
size="small"
>
{settleMktAddress === market.publicKey.toString() ? (
<Loading className="h-4 w-4" />
) : (
<CheckIcon className="h-4 w-4" />
)}
</IconButton>
</Tooltip>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="pb-20">
{Object.entries(unsettledSpotBalances).map(([mktAddress]) => {
const market = group.getSerum3MarketByExternalMarket(
new PublicKey(mktAddress)
)
const base = market?.name.split('/')[0]
const quote = market?.name.split('/')[1]
return (
<div
key={mktAddress}
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
>
<TableMarketName market={market} />
<div className="flex items-center space-x-3">
{unsettledSpotBalances[mktAddress].base ? (
<span className="font-mono text-sm">
{unsettledSpotBalances[mktAddress].base}{' '}
<span className="font-body text-th-fgd-4">{base}</span>
</span>
) : null}
{unsettledSpotBalances[mktAddress].quote ? (
<span className="font-mono text-sm">
{unsettledSpotBalances[mktAddress].quote}{' '}
<span className="font-body text-th-fgd-4">{quote}</span>
</span>
) : null}
{connected ? (
<IconButton
onClick={() => handleSettleSerumFunds(mktAddress)}
>
@ -273,22 +267,17 @@ const UnsettledTrades = ({
<CheckIcon className="h-4 w-4" />
)}
</IconButton>
</div>
) : null}
</div>
)
})}
</div>
)
) : (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-unsettled')}</p>
</div>
)
})}
</div>
)
) : (
<div className="flex flex-col items-center p-8">
<LinkIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:connect-unsettled')}</p>
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-unsettled')}</p>
</div>
)
}

View File

@ -49,7 +49,7 @@ export const ConnectWalletButton: React.FC = () => {
{connecting ? <Loading className="h-4 w-4" /> : t('connect')}
</div>
<div className="text-xxs font-normal leading-3 tracking-wider text-th-fgd-3">
<div className="text-xxs font-normal leading-3 text-th-fgd-3">
{preselectedWalletName}
</div>
</div>

View File

@ -26,18 +26,18 @@ const ConnectedMenu = () => {
const [showMangoAccountsModal, setShowMangoAccountsModal] = useState(false)
const set = mangoStore((s) => s.set)
const actions = mangoStore((s) => s.actions)
const profileDetails = mangoStore((s) => s.profile.details)
const actions = mangoStore.getState().actions
// const profileDetails = mangoStore((s) => s.profile.details)
const loadProfileDetails = mangoStore((s) => s.profile.loadDetails)
const isMobile = width ? width < breakpoints.md : false
const onConnectFetchAccountData = async (wallet: Wallet) => {
if (!wallet) return
const actions = mangoStore.getState().actions
await actions.fetchMangoAccounts(wallet.adapter as unknown as AnchorWallet)
actions.fetchTourSettings(wallet.adapter.publicKey?.toString() as string)
actions.fetchWalletTokens(wallet.adapter as unknown as AnchorWallet)
actions.fetchTradeHistory()
}
const handleDisconnect = useCallback(() => {
@ -89,14 +89,17 @@ const ConnectedMenu = () => {
/>
{!loadProfileDetails && !isMobile ? (
<div className="ml-2.5 overflow-hidden text-left">
<p className="font-mono text-xs text-th-fgd-3">
<p className="text-xs text-th-fgd-3">
{wallet?.adapter.name}
</p>
<p className="truncate pr-2 text-sm font-bold text-th-fgd-1">
{publicKey ? abbreviateAddress(publicKey) : ''}
</p>
<p className="truncate pr-2 text-sm font-bold capitalize text-th-fgd-1">
{/* <p className="truncate pr-2 text-sm font-bold capitalize text-th-fgd-1">
{profileDetails?.profile_name
? profileDetails.profile_name
: 'Profile Unavailabe'}
</p>
</p> */}
</div>
) : null}
</div>
@ -120,7 +123,7 @@ const ConnectedMenu = () => {
>
<UserCircleIcon className="h-4 w-4" />
<div className="pl-2 text-left">
{t('profile:edit-profile')}
{t('profile:edit-profile-pic')}
</div>
</button>
</Menu.Item>

View File

@ -47,7 +47,9 @@ const useJupiterMints = (): {
(t) => t.address === WRAPPED_SOL_MINT.toString()
)
if (findSol) {
findSol.logoURI = '/icons/sol.svg'
if (findSol.logoURI !== '/icons/sol.svg') {
findSol.logoURI = '/icons/sol.svg'
}
}
}

View File

@ -1,12 +1,24 @@
import { MangoAccount } from '@blockworks-foundation/mango-v4'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import { useMemo } from 'react'
export default function useMangoAccount(): {
mangoAccount: MangoAccount | undefined
initialLoad: boolean
mangoAccountPk: PublicKey | undefined
mangoAccountAddress: string
} {
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const initialLoad = mangoStore((s) => s.mangoAccount.initialLoad)
return { mangoAccount, initialLoad }
const mangoAccountPk = useMemo(() => {
return mangoAccount?.publicKey
}, [mangoAccount?.publicKey])
const mangoAccountAddress = useMemo(() => {
return mangoAccountPk?.toString() || ''
}, [mangoAccountPk])
return { mangoAccount, initialLoad, mangoAccountAddress, mangoAccountPk }
}

View File

@ -1,8 +1,9 @@
import mangoStore from '@store/mangoStore'
import { useEffect } from 'react'
const set = mangoStore.getState().set
export default function useMarkPrice() {
const set = mangoStore((s) => s.set)
const markPrice = mangoStore((s) => s.selectedMarket.markPrice)
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
const fills = mangoStore((s) => s.selectedMarket.fills)
@ -28,7 +29,7 @@ export default function useMarkPrice() {
state.selectedMarket.markPrice = newMarkPrice
})
}
}, [orderbook, trades])
}, [orderbook, trades, markPrice])
return markPrice
}

View File

@ -7,7 +7,7 @@ const nextConfig = {
BROWSER: true,
},
images: {
domains: ['raw.githubusercontent.com'],
domains: ['raw.githubusercontent.com', 'arweave.net'],
},
reactStrictMode: true,
webpack: (config, opts) => {

View File

@ -12,7 +12,7 @@
"postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz"
},
"dependencies": {
"@blockworks-foundation/mango-v4": "https://github.com/blockworks-foundation/mango-v4.git#main",
"@blockworks-foundation/mango-v4": "https://github.com/blockworks-foundation/mango-v4.git#ts-client",
"@headlessui/react": "^1.6.6",
"@heroicons/react": "^2.0.10",
"@project-serum/anchor": "0.25.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
public/images/trade@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
public/images/trade@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

View File

@ -15,12 +15,14 @@
"liquidation-type": "Liquidation Type",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"liquidate_token_with_token": "Spot Liquidation",
"no-activity": "No account activity",
"openbook_trade": "Spot Trade",
"perps": "Perps",
"perp_trade": "Perp",
"perp_trade": "Perp Trade",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"spot-liquidation": "Spot Liquidation",
"spot-trade": "Spot Trade",
"swap": "Swap",
"swaps": "Swaps",
"update": "Update",

View File

@ -6,6 +6,7 @@
"account-balance": "Account Balance",
"account-name": "Account Name",
"account-name-desc": "Organize your accounts by giving them useful names",
"account-settings": "Account Settings",
"account-update-failed": "Failed to update account",
"account-update-success": "Account updated successfully",
"account-value": "Account Value",
@ -14,8 +15,10 @@
"add-new-account": "Add New Account",
"amount": "Amount",
"amount-owed": "Amount Owed",
"asset-liability-weight": "Asset/Liability Weights",
"asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).",
"asset-weight": "Asset Weight",
"asset-weight-desc": "The asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral.",
"asset-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral.",
"available": "Available",
"available-balance": "Available Balance",
"balance": "Balance",
@ -24,7 +27,7 @@
"borrow-amount": "Borrow Amount",
"borrow-fee": "Borrow Fee",
"borrow-funds": "Borrow Funds",
"borrow-rate": "Borrow Rate (APR)",
"borrow-rate": "Borrow APR",
"buy": "Buy",
"cancel": "Cancel",
"chart-unavailable": "Chart Unavailable",
@ -53,10 +56,10 @@
"delegate-placeholder": "Enter a wallet address to delegate to",
"deposit": "Deposit",
"deposit-amount": "Deposit Amount",
"deposit-more-sol": "Your SOL wallet balance is low. Add more to pay for transactions",
"deposit-rate": "Deposit Rate (APR)",
"deposit-more-sol": "Your SOL wallet balance is too low. Add more to pay for transactions",
"deposit-rate": "Deposit APR",
"disconnect": "Disconnect",
"edit-account": "Edit Account",
"edit-account": "Edit Account Name",
"edit-profile-image": "Edit Profile Image",
"explorer": "Explorer",
"fee": "Fee",
@ -98,6 +101,7 @@
"repayment-amount": "Repayment Amount",
"rolling-change": "24h Change",
"save": "Save",
"select": "Select",
"select-borrow-token": "Select Borrow Token",
"select-deposit-token": "Select Deposit Token",
"select-repay-token": "Select Repay Token",
@ -127,6 +131,7 @@
"trade-history": "Trade History",
"transaction": "Transaction",
"unavailable": "Unavailable",
"unowned-helper": "Currently viewing account {{accountPk}}",
"update": "Update",
"update-delegate": "Update Delegate",
"updating-account-name": "Updating Account Name...",
@ -137,7 +142,6 @@
"wallet-balance": "Wallet Balance",
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"account-settings": "Account Settings"
"withdraw-amount": "Withdraw Amount"
}

View File

@ -7,8 +7,8 @@
"free-collateral-desc": "The amount of capital you have to trade or borrow against. When your free collateral reaches $0 you won't be able to make withdrawals.",
"health": "Health",
"health-desc": "If your account health reaches 0% your account will be liquidated. You can increase the health of your account by making a deposit.",
"health-check": "Health Check",
"health-check-desc": "Check the health of your account from any screen in the app. A green heart represents good health, orange okay and red poor.",
"account-summary": "Account Summary",
"account-summary-desc": "Check your key account information from any screen in the app.",
"health-impact": "Health Impact",
"health-impact-desc": "Projects the health of your account before you make a swap. The first value is your current account health and the second, your projected account health.",
"interest-earned": "Interest Earned",

View File

@ -1,6 +1,6 @@
{
"bullet-1": "Fully permissionless",
"bullet-2": "Up to 20x leverage",
"bullet-2": "Up to 10x leverage",
"bullet-3": "Automatically earn interest on your deposits",
"bullet-4": "Borrow tokens with many collateral options",
"choose-wallet": "Choose Wallet",

View File

@ -7,9 +7,11 @@
"hide-fees": "Hide Fees",
"insufficient-balance": "Insufficient {{symbol}} Balance",
"insufficient-collateral": "Insufficient Collateral",
"max-slippage": "Max Slippage",
"minimum-received": "Minimum Received",
"no-history": "No swap history",
"pay": "You Pay",
"preset": "Preset",
"price-impact": "Price Impact",
"rate": "Rate",
"receive": "You Receive",

View File

@ -2,6 +2,7 @@
"amount": "Amount",
"base": "Base",
"book": "Book",
"buys": "Buys",
"cancel-order-error": "Failed to cancel order",
"connect-orders": "Connect to view your open orders",
"connect-positions": "Connect to view your perp positions",
@ -21,6 +22,7 @@
"no-orders": "No open orders",
"no-positions": "No perp positions",
"no-unsettled": "No unsettled funds",
"notional": "Notional",
"open-interest": "Open Interest",
"oracle-price": "Oracle Price",
"order-error": "Failed to place order",
@ -31,6 +33,7 @@
"place-order": "Place {{side}} Order",
"placing-order": "Placing Order",
"quote": "Quote",
"sells": "Sells",
"settle-funds": "Settle Funds",
"settle-funds-error": "Failed to settle funds",
"show-asks": "Show Asks",

View File

@ -15,12 +15,14 @@
"liquidation-type": "Liquidation Type",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"liquidate_token_with_token": "Spot Liquidation",
"no-activity": "No account activity",
"openbook_trade": "Spot Trade",
"perps": "Perps",
"perp_trade": "Perp",
"perp_trade": "Perp Trade",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"spot-liquidation": "Spot Liquidation",
"spot-trade": "Spot Trade",
"swap": "Swap",
"swaps": "Swaps",
"update": "Update",

View File

@ -6,6 +6,7 @@
"account-balance": "Account Balance",
"account-name": "Account Name",
"account-name-desc": "Organize your accounts by giving them useful names",
"account-settings": "Account Settings",
"account-update-failed": "Failed to update account",
"account-update-success": "Account updated successfully",
"account-value": "Account Value",
@ -14,8 +15,10 @@
"add-new-account": "Add New Account",
"amount": "Amount",
"amount-owed": "Amount Owed",
"asset-liability-weight": "Asset/Liability Weights",
"asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).",
"asset-weight": "Asset Weight",
"asset-weight-desc": "The asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral.",
"asset-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral.",
"available": "Available",
"available-balance": "Available Balance",
"balance": "Balance",
@ -24,7 +27,7 @@
"borrow-amount": "Borrow Amount",
"borrow-fee": "Borrow Fee",
"borrow-funds": "Borrow Funds",
"borrow-rate": "Borrow Rate (APR)",
"borrow-rate": "Borrow APR",
"buy": "Buy",
"cancel": "Cancel",
"chart-unavailable": "Chart Unavailable",
@ -53,10 +56,10 @@
"delegate-placeholder": "Enter a wallet address to delegate to",
"deposit": "Deposit",
"deposit-amount": "Deposit Amount",
"deposit-more-sol": "Your SOL wallet balance is low. Add more to pay for transactions",
"deposit-rate": "Deposit Rate (APR)",
"deposit-more-sol": "Your SOL wallet balance is too low. Add more to pay for transactions",
"deposit-rate": "Deposit APR",
"disconnect": "Disconnect",
"edit-account": "Edit Account",
"edit-account": "Edit Account Name",
"edit-profile-image": "Edit Profile Image",
"explorer": "Explorer",
"fee": "Fee",
@ -98,6 +101,7 @@
"repayment-amount": "Repayment Amount",
"rolling-change": "24h Change",
"save": "Save",
"select": "Select",
"select-borrow-token": "Select Borrow Token",
"select-deposit-token": "Select Deposit Token",
"select-repay-token": "Select Repay Token",
@ -127,6 +131,7 @@
"trade-history": "Trade History",
"transaction": "Transaction",
"unavailable": "Unavailable",
"unowned-helper": "Currently viewing account {{accountPk}}",
"update": "Update",
"update-delegate": "Update Delegate",
"updating-account-name": "Updating Account Name...",
@ -137,7 +142,6 @@
"wallet-balance": "Wallet Balance",
"wallet-disconnected": "Disconnected from wallet",
"withdraw": "Withdraw",
"withdraw-amount": "Withdraw Amount",
"account-settings": "Account Settings"
"withdraw-amount": "Withdraw Amount"
}

View File

@ -7,8 +7,8 @@
"free-collateral-desc": "The amount of capital you have to trade or borrow against. When your free collateral reaches $0 you won't be able to make withdrawals.",
"health": "Health",
"health-desc": "If your account health reaches 0% your account will be liquidated. You can increase the health of your account by making a deposit.",
"health-check": "Health Check",
"health-check-desc": "Check the health of your account from any screen in the app. A green heart represents good health, orange okay and red poor.",
"account-summary": "Account Summary",
"account-summary-desc": "Check your key account information from any screen in the app.",
"health-impact": "Health Impact",
"health-impact-desc": "Projects the health of your account before you make a swap. The first value is your current account health and the second, your projected account health.",
"interest-earned": "Interest Earned",

View File

@ -1,6 +1,6 @@
{
"bullet-1": "Fully permissionless",
"bullet-2": "Up to 20x leverage",
"bullet-2": "Up to 10x leverage",
"bullet-3": "Automatically earn interest on your deposits",
"bullet-4": "Borrow tokens with many collateral options",
"choose-wallet": "Choose Wallet",

Some files were not shown because too many files have changed in this diff Show More