2023-07-16 20:41:13 -07:00
|
|
|
|
import mangoStore from '@store/mangoStore'
|
|
|
|
|
import NumberFormat, {
|
|
|
|
|
NumberFormatValues,
|
|
|
|
|
SourceInfo,
|
|
|
|
|
} from 'react-number-format'
|
|
|
|
|
import {
|
2023-07-24 19:35:27 -07:00
|
|
|
|
DEFAULT_CHECKBOX_SETTINGS,
|
2023-07-16 20:41:13 -07:00
|
|
|
|
INPUT_PREFIX_CLASSNAMES,
|
|
|
|
|
INPUT_SUFFIX_CLASSNAMES,
|
|
|
|
|
} from './AdvancedTradeForm'
|
|
|
|
|
import LogoWithFallback from '@components/shared/LogoWithFallback'
|
|
|
|
|
import { LinkIcon, QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
|
|
|
|
|
import useSelectedMarket from 'hooks/useSelectedMarket'
|
|
|
|
|
import { useWallet } from '@solana/wallet-adapter-react'
|
|
|
|
|
import useIpAddress from 'hooks/useIpAddress'
|
|
|
|
|
import { useTranslation } from 'next-i18next'
|
2023-08-25 10:52:00 -07:00
|
|
|
|
import { FormEvent, useCallback, useMemo, useState } from 'react'
|
2023-07-16 20:41:13 -07:00
|
|
|
|
import Loading from '@components/shared/Loading'
|
|
|
|
|
import Button from '@components/shared/Button'
|
|
|
|
|
import Image from 'next/image'
|
|
|
|
|
import useQuoteRoutes from '@components/swap/useQuoteRoutes'
|
2023-09-12 21:44:48 -07:00
|
|
|
|
import {
|
|
|
|
|
HealthType,
|
|
|
|
|
PerpMarket,
|
|
|
|
|
Serum3Market,
|
|
|
|
|
} from '@blockworks-foundation/mango-v4'
|
2023-07-16 20:41:13 -07:00
|
|
|
|
import Decimal from 'decimal.js'
|
|
|
|
|
import { notify } from 'utils/notifications'
|
|
|
|
|
import * as sentry from '@sentry/nextjs'
|
|
|
|
|
import { isMangoError } from 'types'
|
|
|
|
|
import SwapSlider from '@components/swap/SwapSlider'
|
|
|
|
|
import PercentageSelectButtons from '@components/swap/PercentageSelectButtons'
|
2023-07-24 19:35:27 -07:00
|
|
|
|
import { SIZE_INPUT_UI_KEY, TRADE_CHECKBOXES_KEY } from 'utils/constants'
|
2023-07-16 20:41:13 -07:00
|
|
|
|
import useLocalStorageState from 'hooks/useLocalStorageState'
|
2023-07-18 11:55:13 -07:00
|
|
|
|
import useUnownedAccount from 'hooks/useUnownedAccount'
|
|
|
|
|
import HealthImpact from '@components/shared/HealthImpact'
|
|
|
|
|
import Tooltip from '@components/shared/Tooltip'
|
2023-07-24 19:35:27 -07:00
|
|
|
|
import Checkbox from '@components/forms/Checkbox'
|
2023-08-25 09:46:45 -07:00
|
|
|
|
// import MaxMarketSwapAmount from './MaxMarketSwapAmount'
|
2023-09-12 21:44:48 -07:00
|
|
|
|
import {
|
|
|
|
|
floorToDecimal,
|
|
|
|
|
formatNumericValue,
|
|
|
|
|
getDecimalCount,
|
|
|
|
|
} from 'utils/numbers'
|
2023-07-24 19:35:27 -07:00
|
|
|
|
import { formatTokenSymbol } from 'utils/tokens'
|
|
|
|
|
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
2023-07-24 19:56:28 -07:00
|
|
|
|
import { useTokenMax } from '@components/swap/useTokenMax'
|
2023-07-24 20:15:00 -07:00
|
|
|
|
import SheenLoader from '@components/shared/SheenLoader'
|
2023-08-13 21:26:39 -07:00
|
|
|
|
import { fetchJupiterTransaction } from '@components/swap/SwapReviewRouteInfo'
|
2023-09-12 21:44:48 -07:00
|
|
|
|
import MaxMarketTradeAmount from './MaxMarketTradeAmount'
|
2023-10-24 11:51:18 -07:00
|
|
|
|
import useMangoAccount from 'hooks/useMangoAccount'
|
2023-07-16 20:41:13 -07:00
|
|
|
|
|
|
|
|
|
const set = mangoStore.getState().set
|
|
|
|
|
|
|
|
|
|
function stringToNumberOrZero(s: string): number {
|
|
|
|
|
const n = parseFloat(s)
|
|
|
|
|
if (isNaN(n)) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function SpotMarketOrderSwapForm() {
|
2023-07-18 11:55:13 -07:00
|
|
|
|
const { t } = useTranslation()
|
2023-07-24 19:56:28 -07:00
|
|
|
|
const { baseSize, quoteSize, side } = mangoStore((s) => s.tradeForm)
|
2023-07-18 11:55:13 -07:00
|
|
|
|
const { isUnownedAccount } = useUnownedAccount()
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const [placingOrder, setPlacingOrder] = useState(false)
|
2023-10-26 21:30:17 -07:00
|
|
|
|
const [isDraggingSlider, setIsDraggingSlider] = useState(false)
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const { ipAllowed, ipCountry } = useIpAddress()
|
2023-07-20 16:07:59 -07:00
|
|
|
|
const { connected, publicKey, connect } = useWallet()
|
2023-10-24 11:51:18 -07:00
|
|
|
|
const { mangoAccount } = useMangoAccount()
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
|
2023-07-24 19:35:27 -07:00
|
|
|
|
const [savedCheckboxSettings, setSavedCheckboxSettings] =
|
|
|
|
|
useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS)
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const {
|
|
|
|
|
selectedMarket,
|
|
|
|
|
price: oraclePrice,
|
|
|
|
|
baseLogoURI,
|
|
|
|
|
baseSymbol,
|
|
|
|
|
quoteLogoURI,
|
|
|
|
|
quoteSymbol,
|
|
|
|
|
serumOrPerpMarket,
|
|
|
|
|
} = useSelectedMarket()
|
2023-07-24 19:56:28 -07:00
|
|
|
|
const { amount: tokenMax, amountWithBorrow } = useTokenMax(
|
|
|
|
|
savedCheckboxSettings.margin,
|
|
|
|
|
)
|
2023-07-16 20:41:13 -07:00
|
|
|
|
|
|
|
|
|
const handleBaseSizeChange = useCallback(
|
|
|
|
|
(e: NumberFormatValues, info: SourceInfo) => {
|
|
|
|
|
if (info.source !== 'event') return
|
2023-09-12 21:44:48 -07:00
|
|
|
|
console.log(e.value)
|
2023-07-16 20:41:13 -07:00
|
|
|
|
set((s) => {
|
|
|
|
|
const price =
|
|
|
|
|
s.tradeForm.tradeType === 'Market'
|
|
|
|
|
? oraclePrice
|
|
|
|
|
: Number(s.tradeForm.price)
|
|
|
|
|
|
|
|
|
|
s.tradeForm.baseSize = e.value
|
|
|
|
|
if (price && e.value !== '' && !Number.isNaN(Number(e.value))) {
|
|
|
|
|
s.tradeForm.quoteSize = new Decimal(price).mul(e.value).toFixed()
|
|
|
|
|
} else {
|
|
|
|
|
s.tradeForm.quoteSize = ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
|
[oraclePrice],
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const handleQuoteSizeChange = useCallback(
|
|
|
|
|
(e: NumberFormatValues, info: SourceInfo) => {
|
|
|
|
|
if (info.source !== 'event') return
|
|
|
|
|
set((s) => {
|
|
|
|
|
const price =
|
|
|
|
|
s.tradeForm.tradeType === 'Market'
|
|
|
|
|
? oraclePrice
|
|
|
|
|
: Number(s.tradeForm.price)
|
|
|
|
|
|
|
|
|
|
s.tradeForm.quoteSize = e.value
|
|
|
|
|
if (price && e.value !== '' && !Number.isNaN(Number(e.value))) {
|
|
|
|
|
s.tradeForm.baseSize = new Decimal(e.value).div(price).toFixed()
|
|
|
|
|
} else {
|
|
|
|
|
s.tradeForm.baseSize = ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
|
[oraclePrice],
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
|
2023-09-12 21:44:48 -07:00
|
|
|
|
const handleMaxAmount = useCallback(
|
|
|
|
|
(useMargin: boolean) => {
|
|
|
|
|
const group = mangoStore.getState().group
|
|
|
|
|
if (
|
|
|
|
|
!group ||
|
|
|
|
|
!serumOrPerpMarket ||
|
|
|
|
|
serumOrPerpMarket instanceof PerpMarket
|
|
|
|
|
)
|
|
|
|
|
return { max: new Decimal(0), decimals: 6 }
|
|
|
|
|
|
|
|
|
|
const max = useMargin ? amountWithBorrow : tokenMax
|
|
|
|
|
const decimals = getDecimalCount(serumOrPerpMarket.minOrderSize)
|
|
|
|
|
if (side === 'sell') {
|
|
|
|
|
return { max, decimals }
|
|
|
|
|
} else {
|
|
|
|
|
const baseMax = max.div(new Decimal(oraclePrice))
|
|
|
|
|
return { max: baseMax, decimals }
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[amountWithBorrow, oraclePrice, serumOrPerpMarket, side, tokenMax],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const setMaxFromButton = useCallback(
|
|
|
|
|
(amount: string) => {
|
|
|
|
|
handleBaseSizeChange(
|
|
|
|
|
{ value: amount } as NumberFormatValues,
|
|
|
|
|
{ source: 'event' } as SourceInfo,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
[handleBaseSizeChange],
|
|
|
|
|
)
|
|
|
|
|
|
2023-10-26 21:30:17 -07:00
|
|
|
|
const handleSliderDrag = useCallback(() => {
|
|
|
|
|
if (!isDraggingSlider) {
|
|
|
|
|
setIsDraggingSlider(true)
|
|
|
|
|
}
|
|
|
|
|
}, [isDraggingSlider])
|
|
|
|
|
|
|
|
|
|
const handleSliderDragEnd = useCallback(() => {
|
|
|
|
|
if (isDraggingSlider) {
|
|
|
|
|
setIsDraggingSlider(false)
|
|
|
|
|
}
|
|
|
|
|
}, [isDraggingSlider])
|
|
|
|
|
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const setAmountFromSlider = useCallback(
|
|
|
|
|
(amount: string) => {
|
|
|
|
|
if (side === 'buy') {
|
|
|
|
|
handleQuoteSizeChange(
|
|
|
|
|
{ value: amount } as NumberFormatValues,
|
2023-07-21 11:47:53 -07:00
|
|
|
|
{ source: 'event' } as SourceInfo,
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
handleBaseSizeChange(
|
|
|
|
|
{ value: amount } as NumberFormatValues,
|
2023-07-21 11:47:53 -07:00
|
|
|
|
{ source: 'event' } as SourceInfo,
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
|
[side, handleBaseSizeChange, handleQuoteSizeChange],
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const [inputBank, outputBank] = useMemo(() => {
|
|
|
|
|
const group = mangoStore.getState().group
|
|
|
|
|
if (!group || !(selectedMarket instanceof Serum3Market)) return []
|
|
|
|
|
|
|
|
|
|
const quoteBank = group?.getFirstBankByTokenIndex(
|
2023-07-21 11:47:53 -07:00
|
|
|
|
selectedMarket.quoteTokenIndex,
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
const baseBank = group.getFirstBankByTokenIndex(
|
2023-07-21 11:47:53 -07:00
|
|
|
|
selectedMarket.baseTokenIndex,
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (side === 'buy') {
|
|
|
|
|
set((s) => {
|
|
|
|
|
s.swap.inputBank = quoteBank
|
|
|
|
|
s.swap.outputBank = baseBank
|
|
|
|
|
})
|
|
|
|
|
return [quoteBank, baseBank]
|
|
|
|
|
} else {
|
|
|
|
|
set((s) => {
|
|
|
|
|
s.swap.inputBank = baseBank
|
|
|
|
|
s.swap.outputBank = quoteBank
|
|
|
|
|
})
|
|
|
|
|
return [baseBank, quoteBank]
|
|
|
|
|
}
|
|
|
|
|
}, [selectedMarket, side])
|
|
|
|
|
|
2023-08-25 09:46:45 -07:00
|
|
|
|
const slippage = mangoStore.getState().swap.slippage
|
2023-10-26 21:30:17 -07:00
|
|
|
|
const jupiterQuoteAmount = side === 'buy' ? quoteSize : baseSize
|
|
|
|
|
const { bestRoute: selectedRoute, isInitialLoading: loadingRoute } =
|
|
|
|
|
useQuoteRoutes({
|
|
|
|
|
inputMint: inputBank?.mint.toString() || '',
|
|
|
|
|
outputMint: outputBank?.mint.toString() || '',
|
|
|
|
|
amount: jupiterQuoteAmount,
|
|
|
|
|
slippage,
|
|
|
|
|
swapMode: 'ExactIn',
|
|
|
|
|
wallet: publicKey?.toBase58(),
|
|
|
|
|
mangoAccount,
|
|
|
|
|
mode: 'JUPITER',
|
|
|
|
|
enabled: () =>
|
|
|
|
|
!!(
|
|
|
|
|
inputBank?.mint &&
|
|
|
|
|
outputBank?.mint &&
|
|
|
|
|
jupiterQuoteAmount &&
|
|
|
|
|
!isDraggingSlider
|
|
|
|
|
),
|
|
|
|
|
})
|
2023-07-16 20:41:13 -07:00
|
|
|
|
|
2023-08-25 10:52:00 -07:00
|
|
|
|
const handlePlaceOrder = useCallback(async () => {
|
|
|
|
|
const client = mangoStore.getState().client
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const group = mangoStore.getState().group
|
|
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
2023-08-25 10:52:00 -07:00
|
|
|
|
const { baseSize, quoteSize, side } = mangoStore.getState().tradeForm
|
|
|
|
|
const actions = mangoStore.getState().actions
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const connection = mangoStore.getState().connection
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!mangoAccount ||
|
|
|
|
|
!group ||
|
|
|
|
|
!inputBank ||
|
|
|
|
|
!outputBank ||
|
|
|
|
|
!publicKey ||
|
|
|
|
|
!selectedRoute
|
2023-07-18 11:55:13 -07:00
|
|
|
|
)
|
2023-07-16 20:41:13 -07:00
|
|
|
|
return
|
|
|
|
|
|
2023-08-25 10:52:00 -07:00
|
|
|
|
setPlacingOrder(true)
|
|
|
|
|
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const [ixs, alts] = await fetchJupiterTransaction(
|
|
|
|
|
connection,
|
|
|
|
|
selectedRoute,
|
|
|
|
|
publicKey,
|
|
|
|
|
slippage,
|
|
|
|
|
inputBank.mint,
|
2023-07-21 11:47:53 -07:00
|
|
|
|
outputBank.mint,
|
2023-07-16 20:41:13 -07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try {
|
2023-08-12 11:40:09 -07:00
|
|
|
|
const { signature: tx, slot } = await client.marginTrade({
|
2023-07-16 20:41:13 -07:00
|
|
|
|
group,
|
|
|
|
|
mangoAccount,
|
|
|
|
|
inputMintPk: inputBank.mint,
|
|
|
|
|
amountIn:
|
|
|
|
|
side === 'buy'
|
|
|
|
|
? stringToNumberOrZero(quoteSize)
|
|
|
|
|
: stringToNumberOrZero(baseSize),
|
|
|
|
|
outputMintPk: outputBank.mint,
|
|
|
|
|
userDefinedInstructions: ixs,
|
|
|
|
|
userDefinedAlts: alts,
|
|
|
|
|
flashLoanType: { swap: {} },
|
|
|
|
|
})
|
|
|
|
|
set((s) => {
|
|
|
|
|
s.successAnimation.swap = true
|
|
|
|
|
})
|
|
|
|
|
// if (soundSettings['swap-success']) {
|
|
|
|
|
// successSound.play()
|
|
|
|
|
// }
|
|
|
|
|
notify({
|
|
|
|
|
title: 'Transaction confirmed',
|
|
|
|
|
type: 'success',
|
|
|
|
|
txid: tx,
|
|
|
|
|
noSound: true,
|
|
|
|
|
})
|
|
|
|
|
actions.fetchGroup()
|
|
|
|
|
actions.fetchSwapHistory(mangoAccount.publicKey.toString(), 30000)
|
2023-08-12 11:40:09 -07:00
|
|
|
|
await actions.reloadMangoAccount(slot)
|
2023-07-16 20:41:13 -07:00
|
|
|
|
set((s) => {
|
|
|
|
|
s.tradeForm.baseSize = ''
|
|
|
|
|
s.tradeForm.quoteSize = ''
|
|
|
|
|
})
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('onSwap error: ', e)
|
|
|
|
|
sentry.captureException(e)
|
|
|
|
|
if (isMangoError(e)) {
|
|
|
|
|
notify({
|
|
|
|
|
title: 'Transaction failed',
|
|
|
|
|
description: e.message,
|
|
|
|
|
txid: e?.txid,
|
|
|
|
|
type: 'error',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setPlacingOrder(false)
|
|
|
|
|
}
|
2023-08-25 10:52:00 -07:00
|
|
|
|
}, [inputBank, outputBank, publicKey, selectedRoute])
|
2023-07-16 20:41:13 -07:00
|
|
|
|
|
|
|
|
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
|
|
|
e.preventDefault()
|
2023-07-20 16:07:59 -07:00
|
|
|
|
connected ? handlePlaceOrder() : connect()
|
2023-07-16 20:41:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-18 11:55:13 -07:00
|
|
|
|
const maintProjectedHealth = useMemo(() => {
|
|
|
|
|
const group = mangoStore.getState().group
|
|
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
|
|
|
|
if (!inputBank || !mangoAccount || !outputBank || !group) return 0
|
|
|
|
|
|
|
|
|
|
const simulatedHealthRatio =
|
|
|
|
|
mangoAccount.simHealthRatioWithTokenPositionUiChanges(
|
|
|
|
|
group,
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
mintPk: inputBank.mint,
|
|
|
|
|
uiTokenAmount:
|
|
|
|
|
(side === 'buy'
|
|
|
|
|
? stringToNumberOrZero(quoteSize)
|
|
|
|
|
: stringToNumberOrZero(baseSize)) * -1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
mintPk: outputBank.mint,
|
|
|
|
|
uiTokenAmount:
|
|
|
|
|
side === 'buy'
|
|
|
|
|
? stringToNumberOrZero(baseSize)
|
|
|
|
|
: stringToNumberOrZero(quoteSize),
|
|
|
|
|
},
|
|
|
|
|
],
|
2023-07-21 11:47:53 -07:00
|
|
|
|
HealthType.maint,
|
2023-07-18 11:55:13 -07:00
|
|
|
|
)
|
|
|
|
|
return simulatedHealthRatio > 100
|
|
|
|
|
? 100
|
|
|
|
|
: simulatedHealthRatio < 0
|
|
|
|
|
? 0
|
|
|
|
|
: Math.trunc(simulatedHealthRatio)
|
|
|
|
|
}, [inputBank, outputBank, baseSize, quoteSize, side])
|
|
|
|
|
|
2023-07-24 19:35:27 -07:00
|
|
|
|
const [balance, borrowAmount] = useMemo(() => {
|
|
|
|
|
if (!inputBank) return [0, 0]
|
|
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
|
|
|
|
if (!mangoAccount) return [0, 0]
|
|
|
|
|
let borrowAmount
|
|
|
|
|
const balance = mangoAccount.getTokenDepositsUi(inputBank)
|
|
|
|
|
if (side === 'buy') {
|
|
|
|
|
const remainingBalance = balance - parseFloat(quoteSize)
|
|
|
|
|
borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
|
|
|
|
|
} else {
|
|
|
|
|
const remainingBalance = balance - parseFloat(baseSize)
|
|
|
|
|
borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [balance, borrowAmount]
|
|
|
|
|
}, [baseSize, inputBank, quoteSize])
|
|
|
|
|
|
|
|
|
|
const orderValue = useMemo(() => {
|
2023-07-24 19:56:28 -07:00
|
|
|
|
if (
|
|
|
|
|
!inputBank ||
|
|
|
|
|
!outputBank ||
|
|
|
|
|
!oraclePrice ||
|
|
|
|
|
!baseSize ||
|
|
|
|
|
isNaN(parseFloat(baseSize))
|
|
|
|
|
)
|
2023-07-24 19:35:27 -07:00
|
|
|
|
return 0
|
2023-07-24 19:56:28 -07:00
|
|
|
|
|
|
|
|
|
const quotePriceDecimal =
|
|
|
|
|
side === 'buy'
|
|
|
|
|
? new Decimal(inputBank.uiPrice)
|
|
|
|
|
: new Decimal(outputBank.uiPrice)
|
2023-07-24 19:35:27 -07:00
|
|
|
|
const basePriceDecimal = new Decimal(oraclePrice)
|
|
|
|
|
const sizeDecimal = new Decimal(baseSize)
|
|
|
|
|
return floorToDecimal(
|
|
|
|
|
basePriceDecimal.mul(quotePriceDecimal).mul(sizeDecimal),
|
|
|
|
|
2,
|
|
|
|
|
)
|
2023-07-24 19:56:28 -07:00
|
|
|
|
}, [baseSize, inputBank, outputBank, oraclePrice, side])
|
|
|
|
|
|
|
|
|
|
const tooMuchSize = useMemo(() => {
|
|
|
|
|
if (!baseSize || !quoteSize || !amountWithBorrow || !tokenMax) return false
|
|
|
|
|
const size = side === 'buy' ? new Decimal(quoteSize) : new Decimal(baseSize)
|
|
|
|
|
const useMargin = savedCheckboxSettings.margin
|
|
|
|
|
return useMargin ? size.gt(amountWithBorrow) : size.gt(tokenMax)
|
|
|
|
|
}, [
|
|
|
|
|
amountWithBorrow,
|
|
|
|
|
baseSize,
|
|
|
|
|
quoteSize,
|
|
|
|
|
side,
|
|
|
|
|
tokenMax,
|
|
|
|
|
savedCheckboxSettings.margin,
|
|
|
|
|
])
|
2023-07-24 19:35:27 -07:00
|
|
|
|
|
2023-07-16 20:41:13 -07:00
|
|
|
|
const disabled =
|
2023-07-24 19:56:28 -07:00
|
|
|
|
(connected && (!baseSize || !oraclePrice)) ||
|
2023-07-16 20:41:13 -07:00
|
|
|
|
!serumOrPerpMarket ||
|
2023-08-25 10:52:00 -07:00
|
|
|
|
loadingRoute ||
|
2023-07-24 19:56:28 -07:00
|
|
|
|
tooMuchSize
|
2023-07-16 20:41:13 -07:00
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<form onSubmit={(e) => handleSubmit(e)}>
|
|
|
|
|
<div className="mt-3 px-3 md:px-4">
|
2023-09-12 21:44:48 -07:00
|
|
|
|
<div className="mb-2 mt-3 flex items-center justify-between">
|
|
|
|
|
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
|
|
|
|
|
{!isUnownedAccount ? (
|
|
|
|
|
<MaxMarketTradeAmount
|
|
|
|
|
useMargin={savedCheckboxSettings.margin}
|
|
|
|
|
setAmountIn={setMaxFromButton}
|
|
|
|
|
maxAmount={handleMaxAmount}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<NumberFormat
|
|
|
|
|
inputMode="decimal"
|
|
|
|
|
thousandSeparator=","
|
|
|
|
|
allowNegative={false}
|
|
|
|
|
isNumericString={true}
|
|
|
|
|
decimalScale={
|
|
|
|
|
side === 'buy'
|
|
|
|
|
? outputBank?.mintDecimals
|
|
|
|
|
: inputBank?.mintDecimals
|
|
|
|
|
}
|
|
|
|
|
name="base"
|
|
|
|
|
id="base"
|
|
|
|
|
className="relative flex w-full items-center rounded-md rounded-b-none border border-th-input-border bg-th-input-bkg p-2 pl-9 font-mono text-sm font-bold text-th-fgd-1 focus:z-10 focus:border-th-fgd-4 focus:outline-none md:hover:z-10 md:hover:border-th-input-border-hover md:hover:focus:border-th-fgd-4 lg:text-base"
|
|
|
|
|
placeholder="0.00"
|
|
|
|
|
value={baseSize}
|
|
|
|
|
onValueChange={handleBaseSizeChange}
|
|
|
|
|
/>
|
|
|
|
|
<div className={`z-10 ${INPUT_PREFIX_CLASSNAMES}`}>
|
|
|
|
|
<LogoWithFallback
|
|
|
|
|
alt=""
|
|
|
|
|
className="drop-shadow-md"
|
|
|
|
|
width={'24'}
|
|
|
|
|
height={'24'}
|
|
|
|
|
src={baseLogoURI || `/icons/${baseSymbol?.toLowerCase()}.svg`}
|
|
|
|
|
fallback={
|
|
|
|
|
<QuestionMarkCircleIcon
|
|
|
|
|
className={`h-5 w-5 text-th-fgd-3`}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={`z-10 ${INPUT_SUFFIX_CLASSNAMES}`}>
|
|
|
|
|
{baseSymbol}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
{quoteLogoURI ? (
|
|
|
|
|
<div className={INPUT_PREFIX_CLASSNAMES}>
|
|
|
|
|
<Image alt="" width="20" height="20" src={quoteLogoURI} />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className={INPUT_PREFIX_CLASSNAMES}>
|
|
|
|
|
<QuestionMarkCircleIcon className="h-5 w-5 text-th-fgd-3" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<NumberFormat
|
|
|
|
|
inputMode="decimal"
|
|
|
|
|
thousandSeparator=","
|
|
|
|
|
allowNegative={false}
|
|
|
|
|
isNumericString={true}
|
|
|
|
|
decimalScale={
|
|
|
|
|
side === 'buy'
|
|
|
|
|
? inputBank?.mintDecimals
|
|
|
|
|
: outputBank?.mintDecimals
|
|
|
|
|
}
|
|
|
|
|
name="quote"
|
|
|
|
|
id="quote"
|
2023-08-25 10:52:00 -07:00
|
|
|
|
className="mt-[-1px] flex w-full items-center rounded-md rounded-t-none border border-th-input-border bg-th-input-bkg p-2 pl-9 font-mono text-sm font-bold text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus:border-th-fgd-4 lg:text-base"
|
2023-07-16 20:41:13 -07:00
|
|
|
|
placeholder="0.00"
|
|
|
|
|
value={quoteSize}
|
|
|
|
|
onValueChange={handleQuoteSizeChange}
|
|
|
|
|
/>
|
|
|
|
|
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{swapFormSizeUi === 'slider' ? (
|
2023-07-24 20:08:43 -07:00
|
|
|
|
<div className="mt-2">
|
|
|
|
|
<SwapSlider
|
|
|
|
|
useMargin={savedCheckboxSettings.margin}
|
|
|
|
|
amount={
|
|
|
|
|
side === 'buy'
|
|
|
|
|
? stringToNumberOrZero(quoteSize)
|
|
|
|
|
: stringToNumberOrZero(baseSize)
|
|
|
|
|
}
|
|
|
|
|
onChange={setAmountFromSlider}
|
|
|
|
|
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
|
2023-09-07 00:59:04 -07:00
|
|
|
|
maxAmount={useTokenMax}
|
2023-10-26 21:30:17 -07:00
|
|
|
|
handleStartDrag={handleSliderDrag}
|
|
|
|
|
handleEndDrag={handleSliderDragEnd}
|
2023-07-24 20:08:43 -07:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
|
) : (
|
|
|
|
|
<PercentageSelectButtons
|
|
|
|
|
amountIn={side === 'buy' ? quoteSize : baseSize}
|
2023-07-18 11:55:13 -07:00
|
|
|
|
setAmountIn={setAmountFromSlider}
|
2023-07-24 19:35:27 -07:00
|
|
|
|
useMargin={savedCheckboxSettings.margin}
|
|
|
|
|
values={['25', '50', '75', '100']}
|
2023-07-16 20:41:13 -07:00
|
|
|
|
/>
|
|
|
|
|
)}
|
2023-07-24 19:35:27 -07:00
|
|
|
|
<div className="mt-4">
|
2023-07-18 11:55:13 -07:00
|
|
|
|
<Tooltip
|
2023-07-24 19:35:27 -07:00
|
|
|
|
className="hidden md:block"
|
|
|
|
|
delay={100}
|
|
|
|
|
placement="left"
|
|
|
|
|
content={t('trade:tooltip-enable-margin')}
|
2023-07-18 11:55:13 -07:00
|
|
|
|
>
|
2023-07-24 19:35:27 -07:00
|
|
|
|
<Checkbox
|
|
|
|
|
checked={savedCheckboxSettings.margin}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setSavedCheckboxSettings({
|
|
|
|
|
...savedCheckboxSettings,
|
|
|
|
|
margin: e.target.checked,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{t('trade:margin')}
|
|
|
|
|
</Checkbox>
|
2023-07-18 11:55:13 -07:00
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
2023-08-25 10:52:00 -07:00
|
|
|
|
<div className="mb-4 mt-6 flex">
|
2023-07-24 19:35:27 -07:00
|
|
|
|
{ipAllowed ? (
|
|
|
|
|
<Button
|
|
|
|
|
className={`flex w-full items-center justify-center ${
|
|
|
|
|
!connected
|
|
|
|
|
? ''
|
|
|
|
|
: side === 'buy'
|
|
|
|
|
? 'bg-th-up-dark text-white md:hover:bg-th-up-dark md:hover:brightness-90'
|
|
|
|
|
: 'bg-th-down-dark text-white md:hover:bg-th-down-dark md:hover:brightness-90'
|
|
|
|
|
}`}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
size="large"
|
|
|
|
|
type="submit"
|
|
|
|
|
>
|
2023-08-25 10:52:00 -07:00
|
|
|
|
{loadingRoute ? (
|
2023-07-24 19:35:27 -07:00
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Loading />
|
|
|
|
|
<span className="hidden sm:block">
|
|
|
|
|
{t('common:fetching-route')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : !connected ? (
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<LinkIcon className="mr-2 h-5 w-5" />
|
|
|
|
|
{t('connect')}
|
|
|
|
|
</div>
|
2023-07-24 19:56:28 -07:00
|
|
|
|
) : tooMuchSize ? (
|
|
|
|
|
<span>
|
|
|
|
|
{t('swap:insufficient-balance', {
|
|
|
|
|
symbol: '',
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
2023-07-24 19:35:27 -07:00
|
|
|
|
) : !placingOrder ? (
|
|
|
|
|
<span>
|
|
|
|
|
{t('trade:place-order', {
|
|
|
|
|
side: side === 'buy' ? t('buy') : t('sell'),
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Loading />
|
|
|
|
|
<span className="hidden sm:block">
|
|
|
|
|
{t('trade:placing-order')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button disabled className="w-full leading-tight" size="large">
|
|
|
|
|
{t('country-not-allowed', {
|
|
|
|
|
country: ipCountry ? `(${ipCountry})` : '',
|
2023-07-18 11:55:13 -07:00
|
|
|
|
})}
|
2023-07-24 19:35:27 -07:00
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<p>{t('trade:order-value')}</p>
|
|
|
|
|
<p className="font-mono text-th-fgd-2">
|
|
|
|
|
{orderValue ? (
|
|
|
|
|
<FormatNumericValue value={orderValue} isUsd />
|
|
|
|
|
) : (
|
|
|
|
|
'–'
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
|
|
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={
|
|
|
|
|
<>
|
|
|
|
|
<p>
|
|
|
|
|
The price impact is the difference observed between the
|
|
|
|
|
total value of the entry tokens swapped and the
|
|
|
|
|
destination tokens obtained.
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-1">
|
|
|
|
|
The bigger the trade is, the bigger the price impact can
|
|
|
|
|
be.
|
|
|
|
|
</p>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<p className="tooltip-underline">{t('swap:price-impact')}</p>
|
|
|
|
|
</Tooltip>
|
2023-08-25 10:52:00 -07:00
|
|
|
|
{loadingRoute ? (
|
2023-07-24 20:15:00 -07:00
|
|
|
|
<SheenLoader>
|
|
|
|
|
<div className="h-3.5 w-12 bg-th-bkg-2" />
|
|
|
|
|
</SheenLoader>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-right font-mono text-th-fgd-2">
|
|
|
|
|
{selectedRoute
|
|
|
|
|
? selectedRoute?.priceImpactPct * 100 < 0.1
|
|
|
|
|
? '<0.1%'
|
|
|
|
|
: `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%`
|
|
|
|
|
: '-'}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2023-07-24 19:35:27 -07:00
|
|
|
|
</div>
|
|
|
|
|
{borrowAmount && inputBank ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={
|
|
|
|
|
balance
|
|
|
|
|
? t('trade:tooltip-borrow-balance', {
|
|
|
|
|
balance: formatNumericValue(balance),
|
|
|
|
|
borrowAmount: formatNumericValue(borrowAmount),
|
|
|
|
|
token: formatTokenSymbol(inputBank.name),
|
|
|
|
|
rate: formatNumericValue(
|
|
|
|
|
inputBank.getBorrowRateUi(),
|
|
|
|
|
2,
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
: t('trade:tooltip-borrow-no-balance', {
|
|
|
|
|
borrowAmount: formatNumericValue(borrowAmount),
|
|
|
|
|
token: formatTokenSymbol(inputBank.name),
|
|
|
|
|
rate: formatNumericValue(
|
|
|
|
|
inputBank.getBorrowRateUi(),
|
|
|
|
|
2,
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
delay={100}
|
|
|
|
|
>
|
|
|
|
|
<p className="tooltip-underline">{t('borrow-amount')}</p>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<p className="text-right font-mono text-th-fgd-2">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={borrowAmount}
|
|
|
|
|
decimals={inputBank.mintDecimals}
|
|
|
|
|
/>{' '}
|
|
|
|
|
<span className="font-body text-th-fgd-4">
|
|
|
|
|
{formatTokenSymbol(inputBank.name)}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={t('loan-origination-fee-tooltip', {
|
|
|
|
|
fee: `${(
|
|
|
|
|
inputBank.loanOriginationFeeRate.toNumber() * 100
|
|
|
|
|
).toFixed(3)}%`,
|
|
|
|
|
})}
|
|
|
|
|
delay={100}
|
|
|
|
|
>
|
|
|
|
|
<p className="tooltip-underline">
|
|
|
|
|
{t('loan-origination-fee')}
|
|
|
|
|
</p>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<p className="text-right font-mono text-th-fgd-2">
|
|
|
|
|
<FormatNumericValue
|
|
|
|
|
value={
|
|
|
|
|
borrowAmount *
|
|
|
|
|
inputBank.loanOriginationFeeRate.toNumber()
|
|
|
|
|
}
|
|
|
|
|
decimals={inputBank.mintDecimals}
|
|
|
|
|
/>{' '}
|
|
|
|
|
<span className="font-body text-th-fgd-4">
|
|
|
|
|
{formatTokenSymbol(inputBank.name)}
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
<div className="flex items-center justify-between text-xs">
|
|
|
|
|
<p className="pr-2 text-th-fgd-3">{t('common:route')}</p>
|
2023-08-25 10:52:00 -07:00
|
|
|
|
{loadingRoute ? (
|
2023-07-24 20:15:00 -07:00
|
|
|
|
<SheenLoader>
|
|
|
|
|
<div className="h-3.5 w-20 bg-th-bkg-2" />
|
|
|
|
|
</SheenLoader>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center overflow-hidden text-th-fgd-2">
|
|
|
|
|
<Tooltip
|
2023-11-14 09:38:17 -08:00
|
|
|
|
content={selectedRoute?.routePlan?.map((info, index) => {
|
2023-07-24 19:35:27 -07:00
|
|
|
|
let includeSeparator = false
|
|
|
|
|
if (
|
2023-11-14 09:38:17 -08:00
|
|
|
|
selectedRoute?.routePlan &&
|
|
|
|
|
selectedRoute?.routePlan?.length > 1 &&
|
|
|
|
|
index !== selectedRoute?.routePlan?.length - 1
|
2023-07-24 19:35:27 -07:00
|
|
|
|
) {
|
|
|
|
|
includeSeparator = true
|
|
|
|
|
}
|
|
|
|
|
return (
|
2023-10-16 21:12:46 -07:00
|
|
|
|
<span key={index}>{`${info?.swapInfo.label} ${
|
2023-07-24 19:35:27 -07:00
|
|
|
|
includeSeparator ? 'x ' : ''
|
|
|
|
|
}`}</span>
|
|
|
|
|
)
|
|
|
|
|
})}
|
2023-07-24 20:15:00 -07:00
|
|
|
|
>
|
2023-08-17 16:47:11 -07:00
|
|
|
|
<div className="tooltip-underline max-w-[140px] truncate whitespace-nowrap">
|
2023-11-14 09:38:17 -08:00
|
|
|
|
{selectedRoute?.routePlan?.map((info, index) => {
|
2023-07-24 20:15:00 -07:00
|
|
|
|
let includeSeparator = false
|
|
|
|
|
if (
|
2023-11-14 09:38:17 -08:00
|
|
|
|
selectedRoute?.routePlan &&
|
|
|
|
|
selectedRoute?.routePlan?.length > 1 &&
|
|
|
|
|
index !== selectedRoute?.routePlan?.length - 1
|
2023-07-24 20:15:00 -07:00
|
|
|
|
) {
|
|
|
|
|
includeSeparator = true
|
|
|
|
|
}
|
|
|
|
|
return (
|
2023-10-16 21:12:46 -07:00
|
|
|
|
<span key={index}>{`${info?.swapInfo.label} ${
|
2023-07-24 20:15:00 -07:00
|
|
|
|
includeSeparator ? 'x ' : ''
|
|
|
|
|
}`}</span>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2023-07-18 11:55:13 -07:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2023-07-16 20:41:13 -07:00
|
|
|
|
</form>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|