wallet swap ui
This commit is contained in:
parent
9490635968
commit
140a0f9352
|
@ -70,7 +70,7 @@ const TabUnderline = <T extends Values>({
|
|||
<span className="relative">
|
||||
{names ? names[i] : t(`${value}`)}
|
||||
{value === 'trade:trigger-order' ? (
|
||||
<span className="absolute -right-5 -top-3 ml-2 rounded bg-th-active px-1 py-0.5 text-xxs font-bold uppercase leading-none text-th-bkg-1">
|
||||
<span className="absolute -right-5 -top-2.5 ml-2 rounded bg-th-active px-1 py-0.5 text-xxs font-bold uppercase leading-none text-th-bkg-1">
|
||||
beta
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
@ -7,14 +7,17 @@ import { CUSTOM_TOKEN_ICONS } from 'utils/constants'
|
|||
|
||||
const TokenLogo = ({
|
||||
bank,
|
||||
logoUrl,
|
||||
size,
|
||||
}: {
|
||||
bank: Bank | undefined
|
||||
logoUrl?: string
|
||||
size?: number
|
||||
}) => {
|
||||
const { mangoTokens } = useJupiterMints()
|
||||
|
||||
const logoUri = useMemo(() => {
|
||||
if (logoUrl) return logoUrl
|
||||
if (!bank) return ''
|
||||
const tokenSymbol = bank.name.toLowerCase()
|
||||
const hasCustomIcon = CUSTOM_TOKEN_ICONS[tokenSymbol]
|
||||
|
@ -26,7 +29,7 @@ const TokenLogo = ({
|
|||
)?.logoURI
|
||||
}
|
||||
return jupiterLogoURI
|
||||
}, [mangoTokens, bank])
|
||||
}, [mangoTokens, bank, logoUrl])
|
||||
|
||||
const logoSize = size ? size : 24
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||
import { floorToDecimal } from 'utils/numbers'
|
||||
import { useTokenMax } from './useTokenMax'
|
||||
|
||||
const DEFAULT_VALUES = ['10', '25', '50', '75', '100']
|
||||
export const DEFAULT_PERCENTAGE_VALUES = ['10', '25', '50', '75', '100']
|
||||
|
||||
const PercentageSelectButtons = ({
|
||||
amountIn,
|
||||
|
@ -53,7 +53,7 @@ const PercentageSelectButtons = ({
|
|||
<ButtonGroup
|
||||
activeValue={sizePercentage}
|
||||
onChange={(p) => handleSizePercentage(p)}
|
||||
values={values || DEFAULT_VALUES}
|
||||
values={values || DEFAULT_PERCENTAGE_VALUES}
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,7 @@ import useLocalStorageState from 'hooks/useLocalStorageState'
|
|||
import { SwapFormTokenListType } from './SwapFormTokenList'
|
||||
import { TriggerOrderTypes } from 'types'
|
||||
import TriggerSwapForm from './TriggerSwapForm'
|
||||
import WalletSwapForm from './WalletSwapForm'
|
||||
|
||||
const set = mangoStore.getState().set
|
||||
|
||||
|
@ -29,6 +30,7 @@ const SwapForm = () => {
|
|||
const [showTokenSelect, setShowTokenSelect] =
|
||||
useState<SwapFormTokenListType>()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [walletSwap, setWalletSwap] = useState(false)
|
||||
const [, setSavedSwapMargin] = useLocalStorageState<boolean>(
|
||||
SWAP_MARGIN_KEY,
|
||||
true,
|
||||
|
@ -150,7 +152,9 @@ const SwapForm = () => {
|
|||
<SwapFormTokenList
|
||||
onClose={() => setShowTokenSelect(undefined)}
|
||||
onTokenSelect={
|
||||
showTokenSelect === 'input' || showTokenSelect === 'reduce-input'
|
||||
showTokenSelect === 'input' ||
|
||||
showTokenSelect === 'reduce-input' ||
|
||||
showTokenSelect === 'wallet-input'
|
||||
? handleTokenInSelect
|
||||
: handleTokenOutSelect
|
||||
}
|
||||
|
@ -164,26 +168,44 @@ const SwapForm = () => {
|
|||
>
|
||||
<SwapSettings onClose={() => setShowSettings(false)} />
|
||||
</EnterBottomExitBottom>
|
||||
<div className="relative p-6">
|
||||
<div className="relative mb-6">
|
||||
<TabUnderline
|
||||
activeValue={swapOrTrigger}
|
||||
values={['swap', 'trade:trigger-order']}
|
||||
onChange={(v) => handleSwapOrTrigger(v)}
|
||||
/>
|
||||
</div>
|
||||
{swapOrTrigger === 'swap' ? (
|
||||
<MarketSwapForm setShowTokenSelect={setShowTokenSelect} />
|
||||
<div className="border-b border-th-bkg-3 px-6 py-3">
|
||||
<Switch
|
||||
checked={walletSwap}
|
||||
onChange={() => setWalletSwap(!walletSwap)}
|
||||
small
|
||||
>
|
||||
{t('swap:wallet-swap')}
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="relative p-6 pt-0">
|
||||
{walletSwap ? (
|
||||
<div className="pt-4">
|
||||
<WalletSwapForm setShowTokenSelect={setShowTokenSelect} />
|
||||
</div>
|
||||
) : (
|
||||
<TriggerSwapForm
|
||||
showTokenSelect={showTokenSelect}
|
||||
setShowTokenSelect={setShowTokenSelect}
|
||||
/>
|
||||
<>
|
||||
<div className="relative pb-2 pt-6">
|
||||
<TabUnderline
|
||||
activeValue={swapOrTrigger}
|
||||
values={['swap', 'trade:trigger-order']}
|
||||
onChange={(v) => handleSwapOrTrigger(v)}
|
||||
/>
|
||||
</div>
|
||||
{swapOrTrigger === 'swap' ? (
|
||||
<MarketSwapForm setShowTokenSelect={setShowTokenSelect} />
|
||||
) : (
|
||||
<TriggerSwapForm
|
||||
showTokenSelect={showTokenSelect}
|
||||
setShowTokenSelect={setShowTokenSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{inputBank ? (
|
||||
{inputBank && !walletSwap ? (
|
||||
<TokenVaultWarnings bank={inputBank} type="swap" />
|
||||
) : null}
|
||||
{inputBank &&
|
||||
!walletSwap &&
|
||||
inputBank.areBorrowsReduceOnly() &&
|
||||
inputBank.areDepositsReduceOnly() ? (
|
||||
<div className="pb-4">
|
||||
|
@ -196,6 +218,7 @@ const SwapForm = () => {
|
|||
</div>
|
||||
) : null}
|
||||
{outputBank &&
|
||||
!walletSwap &&
|
||||
outputBank.areBorrowsReduceOnly() &&
|
||||
outputBank.areDepositsReduceOnly() ? (
|
||||
<div className="pb-4">
|
||||
|
@ -208,24 +231,26 @@ const SwapForm = () => {
|
|||
</div>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<div id="swap-step-four">
|
||||
{!walletSwap ? (
|
||||
<HealthImpact maintProjectedHealth={maintProjectedHealth} />
|
||||
</div>
|
||||
) : null}
|
||||
{swapOrTrigger === 'swap' ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<Tooltip content={t('swap:tooltip-margin')}>
|
||||
<p className="tooltip-underline text-sm text-th-fgd-3">
|
||||
{t('swap:margin')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
<Switch
|
||||
className="text-th-fgd-3"
|
||||
checked={useMargin}
|
||||
onChange={handleSetMargin}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
{!walletSwap ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<Tooltip content={t('swap:tooltip-margin')}>
|
||||
<p className="tooltip-underline text-sm text-th-fgd-3">
|
||||
{t('swap:margin')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
<Switch
|
||||
className="text-th-fgd-3"
|
||||
checked={useMargin}
|
||||
onChange={handleSetMargin}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-th-fgd-3">
|
||||
{t('swap:max-slippage')}
|
||||
|
|
|
@ -15,12 +15,14 @@ import { formatTokenSymbol } from 'utils/tokens'
|
|||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import Input from '@components/forms/Input'
|
||||
import { getInputTokenBalance } from './TriggerSwapForm'
|
||||
import { walletBalanceForToken } from '@components/DepositForm'
|
||||
|
||||
export type SwapFormTokenListType =
|
||||
| 'input'
|
||||
| 'output'
|
||||
| 'reduce-input'
|
||||
| 'reduce-output'
|
||||
| 'wallet-input'
|
||||
| undefined
|
||||
|
||||
const generateSearchTerm = (item: Token, searchValue: string) => {
|
||||
|
@ -66,8 +68,19 @@ const TokenItem = ({
|
|||
const bank = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!group) return
|
||||
// if we want to let users swap from tokens not listed on Mango.
|
||||
|
||||
// if (type === 'wallet-input') {
|
||||
// const hasBank = Array.from(group.banksMapByName.values())
|
||||
// .map((b) => b[0].mint.toString())
|
||||
// .find((mint) => mint === address)
|
||||
// if (hasBank) {
|
||||
// return group.getFirstBankByMint(new PublicKey(address))
|
||||
// }
|
||||
// }
|
||||
|
||||
return group.getFirstBankByMint(new PublicKey(address))
|
||||
}, [address])
|
||||
}, [address, type])
|
||||
|
||||
const isReduceOnly = useMemo(() => {
|
||||
if (!bank) return false
|
||||
|
@ -84,7 +97,7 @@ const TokenItem = ({
|
|||
onClick={() => onSubmit(address)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
<TokenLogo bank={bank} logoUrl={!bank ? token.logoURI : ''} />
|
||||
<div className="ml-2.5">
|
||||
<p className="text-left text-th-fgd-2">
|
||||
{bank?.name ? formatTokenSymbol(bank.name) : symbol || 'unknown'}
|
||||
|
@ -109,23 +122,27 @@ const TokenItem = ({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{(type === 'input' || type === 'reduce-input') &&
|
||||
token.amount &&
|
||||
token.amountWithBorrow &&
|
||||
token.decimals ? (
|
||||
{type === 'input' || type === 'reduce-input' ? (
|
||||
<p className="font-mono text-sm text-th-fgd-2">
|
||||
{useMargin ? (
|
||||
<FormatNumericValue
|
||||
value={token.amountWithBorrow}
|
||||
value={token.amountWithBorrow || 0}
|
||||
decimals={token.decimals}
|
||||
/>
|
||||
) : (
|
||||
<FormatNumericValue
|
||||
value={token.amount}
|
||||
value={token.amount || 0}
|
||||
decimals={token.decimals}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
) : type === 'wallet-input' ? (
|
||||
<p className="font-mono text-sm text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={token.amount || 0}
|
||||
decimals={token.decimals}
|
||||
/>
|
||||
</p>
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -150,9 +167,10 @@ const SwapFormTokenList = ({
|
|||
}) => {
|
||||
const { t } = useTranslation(['common', 'search', 'swap'])
|
||||
const [search, setSearch] = useState('')
|
||||
const { mangoTokens } = useJupiterMints()
|
||||
const { mangoTokens, jupiterTokens } = useJupiterMints()
|
||||
const inputBank = mangoStore((s) => s.swap.inputBank)
|
||||
const outputBank = mangoStore((s) => s.swap.outputBank)
|
||||
const walletTokens = mangoStore((s) => s.wallet.tokens)
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const focusRef = useRef<HTMLInputElement>(null)
|
||||
|
@ -169,40 +187,38 @@ const SwapFormTokenList = ({
|
|||
|
||||
const tokenInfos: TokenInfoWithAmounts[] = useMemo(() => {
|
||||
if (
|
||||
mangoTokens?.length &&
|
||||
group &&
|
||||
mangoAccount &&
|
||||
outputBank &&
|
||||
inputBank &&
|
||||
type === 'input'
|
||||
) {
|
||||
!mangoTokens.length ||
|
||||
!group ||
|
||||
!mangoAccount ||
|
||||
!outputBank ||
|
||||
!inputBank
|
||||
)
|
||||
return []
|
||||
if (type === 'input') {
|
||||
const filteredSortedTokens = mangoTokens
|
||||
.map((token) => {
|
||||
const tokenPk = new PublicKey(token.address)
|
||||
const tokenBank = group.getFirstBankByMint(tokenPk)
|
||||
const max = getTokenInMax(
|
||||
mangoAccount,
|
||||
new PublicKey(token.address),
|
||||
tokenPk,
|
||||
outputBank.mint,
|
||||
group,
|
||||
useMargin,
|
||||
)
|
||||
return { ...token, ...max }
|
||||
const price = tokenBank.uiPrice
|
||||
return { ...token, ...max, price }
|
||||
})
|
||||
.filter((token) => (token.symbol === outputBank?.name ? false : true))
|
||||
.filter((token) => (token.symbol === outputBank.name ? false : true))
|
||||
.sort((a, b) =>
|
||||
useMargin
|
||||
? Number(b.amountWithBorrow) - Number(a.amountWithBorrow)
|
||||
: Number(b.amount) - Number(a.amount),
|
||||
? Number(b.amountWithBorrow.mul(b.price)) -
|
||||
Number(a.amountWithBorrow.mul(a.price))
|
||||
: Number(b.amount.mul(b.price)) - Number(a.amount.mul(a.price)),
|
||||
)
|
||||
|
||||
return filteredSortedTokens
|
||||
} else if (
|
||||
mangoTokens?.length &&
|
||||
group &&
|
||||
mangoAccount &&
|
||||
outputBank &&
|
||||
inputBank &&
|
||||
type === 'reduce-input'
|
||||
) {
|
||||
} else if (type === 'reduce-input') {
|
||||
const filteredSortedTokens = mangoTokens
|
||||
.map((token) => {
|
||||
const tokenBank = group.getFirstBankByMint(
|
||||
|
@ -220,25 +236,74 @@ const SwapFormTokenList = ({
|
|||
})
|
||||
.filter(
|
||||
(token) =>
|
||||
token.symbol !== outputBank?.name && token.absDollarValue > 0.0001,
|
||||
token.symbol !== outputBank.name && token.absDollarValue > 0.0001,
|
||||
)
|
||||
.sort((a, b) => b.absDollarValue - a.absDollarValue)
|
||||
|
||||
return filteredSortedTokens
|
||||
} else if (mangoTokens?.length) {
|
||||
} else if (type === 'wallet-input' && jupiterTokens.length) {
|
||||
// if we want to let users swap from tokens not listed on Mango. Some other changes are need to pass the mint to the swap function
|
||||
|
||||
// const jupiterTokensWithAmount = []
|
||||
// for (const token of jupiterTokens) {
|
||||
// const hasToken = walletTokens.find(
|
||||
// (t) => t.mint.toString() === token.address,
|
||||
// )
|
||||
// if (hasToken) {
|
||||
// jupiterTokensWithAmount.push({
|
||||
// ...token,
|
||||
// amount: new Decimal(hasToken.uiAmount),
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// const filteredSortedTokens = jupiterTokensWithAmount
|
||||
// .filter((token) =>
|
||||
// token.symbol !== outputBank.name && token.amount.gt(0) ? true : false,
|
||||
// )
|
||||
// .sort((a, b) => Number(b.amount) - Number(a.amount))
|
||||
|
||||
const filteredSortedTokens = mangoTokens
|
||||
.map((token) => {
|
||||
const tokenBank = group.getFirstBankByMint(
|
||||
new PublicKey(token.address),
|
||||
)
|
||||
const max = walletBalanceForToken(walletTokens, tokenBank.name)
|
||||
const price = tokenBank.uiPrice
|
||||
return {
|
||||
...token,
|
||||
amount: new Decimal(max.maxAmount),
|
||||
decimals: max.maxDecimals,
|
||||
price,
|
||||
}
|
||||
})
|
||||
.filter((token) => (token.symbol === outputBank.name ? false : true))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(b.amount.mul(b.price)) - Number(a.amount.mul(a.price)),
|
||||
)
|
||||
|
||||
return filteredSortedTokens
|
||||
} else if (mangoTokens.length) {
|
||||
const filteredTokens = mangoTokens
|
||||
.map((token) => ({
|
||||
...token,
|
||||
amount: new Decimal(0),
|
||||
amountWithBorrow: new Decimal(0),
|
||||
}))
|
||||
.filter((token) => (token.symbol === inputBank?.name ? false : true))
|
||||
.filter((token) => (token.symbol === inputBank.name ? false : true))
|
||||
.sort((a, b) => a.symbol.localeCompare(b.symbol))
|
||||
return filteredTokens
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}, [mangoTokens, inputBank, outputBank, mangoAccount, group, useMargin, type])
|
||||
} else return []
|
||||
}, [
|
||||
mangoTokens,
|
||||
jupiterTokens,
|
||||
inputBank,
|
||||
outputBank,
|
||||
mangoAccount,
|
||||
group,
|
||||
useMargin,
|
||||
type,
|
||||
])
|
||||
|
||||
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value)
|
||||
|
@ -254,7 +319,7 @@ const SwapFormTokenList = ({
|
|||
|
||||
const listTitle = useMemo(() => {
|
||||
if (!type) return ''
|
||||
if (type === 'input') {
|
||||
if (type === 'input' || type === 'wallet-input') {
|
||||
return t('swap:you-sell')
|
||||
} else if (type === 'output') {
|
||||
return t('swap:you-buy')
|
||||
|
|
|
@ -50,6 +50,7 @@ import TokenLogo from '@components/shared/TokenLogo'
|
|||
|
||||
type JupiterRouteInfoProps = {
|
||||
amountIn: Decimal
|
||||
isWalletSwap?: boolean
|
||||
onClose: () => void
|
||||
routes: RouteInfo[] | undefined
|
||||
selectedRoute: RouteInfo | undefined | null
|
||||
|
@ -183,6 +184,7 @@ const successSound = new Howl({
|
|||
|
||||
const SwapReviewRouteInfo = ({
|
||||
amountIn,
|
||||
isWalletSwap,
|
||||
onClose,
|
||||
routes,
|
||||
selectedRoute,
|
||||
|
@ -248,6 +250,41 @@ const SwapReviewRouteInfo = ({
|
|||
}
|
||||
}, [inputTokenInfo, outputTokenInfo])
|
||||
|
||||
const onWalletSwap = useCallback(async () => {
|
||||
if (!selectedRoute || !inputBank || !outputBank || !wallet.publicKey) return
|
||||
const client = mangoStore.getState().client
|
||||
const connection = mangoStore.getState().connection
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const [ixs] = await fetchJupiterTransaction(
|
||||
connection,
|
||||
selectedRoute,
|
||||
wallet.publicKey,
|
||||
slippage,
|
||||
inputBank.mint,
|
||||
outputBank.mint,
|
||||
)
|
||||
const tx = await client.sendAndConfirmTransaction(ixs)
|
||||
notify({
|
||||
title: 'Transaction confirmed',
|
||||
type: 'success',
|
||||
txid: tx.signature,
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('error swapping wallet tokens', e)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
onClose()
|
||||
}
|
||||
}, [
|
||||
inputBank,
|
||||
outputBank,
|
||||
onClose,
|
||||
selectedRoute,
|
||||
slippage,
|
||||
wallet.publicKey,
|
||||
])
|
||||
|
||||
const onSwap = useCallback(async () => {
|
||||
if (!selectedRoute) return
|
||||
try {
|
||||
|
@ -333,6 +370,8 @@ const SwapReviewRouteInfo = ({
|
|||
}
|
||||
}, [
|
||||
amountIn,
|
||||
inputBank,
|
||||
outputBank,
|
||||
onClose,
|
||||
selectedRoute,
|
||||
slippage,
|
||||
|
@ -340,6 +379,8 @@ const SwapReviewRouteInfo = ({
|
|||
wallet.publicKey,
|
||||
])
|
||||
|
||||
const onClick = isWalletSwap ? onWalletSwap : onSwap
|
||||
|
||||
const [balance, borrowAmount] = useMemo(() => {
|
||||
const mangoAccount = mangoStore.getState().mangoAccount.current
|
||||
const inputBank = mangoStore.getState().swap.inputBank
|
||||
|
@ -596,7 +637,7 @@ const SwapReviewRouteInfo = ({
|
|||
<div className="p-6">
|
||||
<div className="mb-4 flex items-center justify-center">
|
||||
<Button
|
||||
onClick={onSwap}
|
||||
onClick={onClick}
|
||||
className="flex w-full items-center justify-center text-base"
|
||||
size="large"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import TokenSelect from './TokenSelect'
|
||||
import NumberFormat, {
|
||||
NumberFormatValues,
|
||||
SourceInfo,
|
||||
} from 'react-number-format'
|
||||
import { formatCurrencyValue } from 'utils/numbers'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { INPUT_TOKEN_DEFAULT } from 'utils/constants'
|
||||
import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm'
|
||||
import InlineNotification from '@components/shared/InlineNotification'
|
||||
import { SwapFormTokenListType } from './SwapFormTokenList'
|
||||
import MaxAmountButton from '@components/shared/MaxAmountButton'
|
||||
|
||||
const WalletSellTokenInput = ({
|
||||
handleAmountInChange,
|
||||
setShowTokenSelect,
|
||||
handleMax,
|
||||
max,
|
||||
className,
|
||||
error,
|
||||
}: {
|
||||
handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void
|
||||
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
|
||||
handleMax: (amountIn: string) => void
|
||||
max: string
|
||||
className?: string
|
||||
error?: string
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { group } = useMangoGroup()
|
||||
const { inputBank, amountIn: amountInFormValue } = mangoStore((s) => s.swap)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid grid-cols-2 rounded-t-xl bg-th-bkg-2 p-3 pb-2 ${className}`}
|
||||
>
|
||||
<div className="col-span-2 mb-2 flex items-center justify-between">
|
||||
<p className="text-th-fgd-2">{t('sell')}</p>
|
||||
{inputBank ? (
|
||||
<span className="text-xs">
|
||||
<MaxAmountButton
|
||||
className="mb-0.5"
|
||||
decimals={inputBank.mintDecimals}
|
||||
label={t('wallet')}
|
||||
onClick={() => handleMax(max)}
|
||||
value={max}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<TokenSelect
|
||||
bank={
|
||||
inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)?.[0]
|
||||
}
|
||||
showTokenList={setShowTokenSelect}
|
||||
type="wallet-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative col-span-1">
|
||||
<NumberFormat
|
||||
inputMode="decimal"
|
||||
thousandSeparator=","
|
||||
allowNegative={false}
|
||||
isNumericString={true}
|
||||
decimalScale={inputBank?.mintDecimals || 6}
|
||||
name="amountIn"
|
||||
id="amountIn"
|
||||
className={NUMBER_FORMAT_CLASSNAMES}
|
||||
placeholder="0.00"
|
||||
value={amountInFormValue}
|
||||
onValueChange={handleAmountInChange}
|
||||
isAllowed={withValueLimit}
|
||||
/>
|
||||
{!isNaN(Number(amountInFormValue)) ? (
|
||||
<span className="absolute bottom-1.5 right-3 text-xxs text-th-fgd-4">
|
||||
{inputBank
|
||||
? formatCurrencyValue(
|
||||
inputBank.uiPrice * Number(amountInFormValue),
|
||||
)
|
||||
: '–'}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="col-span-2 mt-1 flex justify-center">
|
||||
<InlineNotification
|
||||
type="error"
|
||||
desc={error}
|
||||
hideBorder
|
||||
hidePadding
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletSellTokenInput
|
|
@ -0,0 +1,389 @@
|
|||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from 'react'
|
||||
import { ArrowDownIcon } from '@heroicons/react/20/solid'
|
||||
import { NumberFormatValues, SourceInfo } from 'react-number-format'
|
||||
import Decimal from 'decimal.js'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useDebounce from '../shared/useDebounce'
|
||||
import { MANGO_MINT, SIZE_INPUT_UI_KEY, USDC_MINT } from '../../utils/constants'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { RouteInfo } from 'types/jupiter'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { DEFAULT_PERCENTAGE_VALUES } from './PercentageSelectButtons'
|
||||
import BuyTokenInput from './BuyTokenInput'
|
||||
import Button from '@components/shared/Button'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import SwapReviewRouteInfo from './SwapReviewRouteInfo'
|
||||
import useIpAddress from 'hooks/useIpAddress'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useQuoteRoutes from './useQuoteRoutes'
|
||||
import Loading from '@components/shared/Loading'
|
||||
import InlineNotification from '@components/shared/InlineNotification'
|
||||
import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { floorToDecimal } from 'utils/numbers'
|
||||
import { SwapFormTokenListType } from './SwapFormTokenList'
|
||||
import WalletSellTokenInput from './WalletSellTokenInput'
|
||||
import { walletBalanceForToken } from '@components/DepositForm'
|
||||
import WalletSwapSlider from './WalletSwapSlider'
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
type WalletSwapFormProps = {
|
||||
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
|
||||
}
|
||||
|
||||
const MAX_DIGITS = 11
|
||||
export const withValueLimit = (values: NumberFormatValues): boolean => {
|
||||
return values.floatValue
|
||||
? values.floatValue.toFixed(0).length <= MAX_DIGITS
|
||||
: true
|
||||
}
|
||||
|
||||
export const NUMBER_FORMAT_CLASSNAMES =
|
||||
'w-full rounded-r-lg h-[56px] box-border pb-4 border-l border-th-bkg-2 bg-th-input-bkg px-3 text-right font-mono text-xl text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-1'
|
||||
|
||||
const set = mangoStore.getState().set
|
||||
|
||||
const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => {
|
||||
const { t } = useTranslation(['common', 'swap', 'trade'])
|
||||
//initial state is undefined null is returned on error
|
||||
const [selectedRoute, setSelectedRoute] = useState<RouteInfo | null>()
|
||||
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [sizePercentage, setSizePercentage] = useState('')
|
||||
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
|
||||
const {
|
||||
margin: useMargin,
|
||||
slippage,
|
||||
inputBank,
|
||||
outputBank,
|
||||
amountIn: amountInFormValue,
|
||||
amountOut: amountOutFormValue,
|
||||
swapMode,
|
||||
} = mangoStore((s) => s.swap)
|
||||
const [debouncedAmountIn] = useDebounce(amountInFormValue, 300)
|
||||
const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300)
|
||||
const { connected, publicKey } = useWallet()
|
||||
const { bestRoute, routes } = useQuoteRoutes({
|
||||
inputMint: inputBank?.mint.toString() || USDC_MINT,
|
||||
outputMint: outputBank?.mint.toString() || MANGO_MINT,
|
||||
amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut,
|
||||
slippage,
|
||||
swapMode,
|
||||
wallet: publicKey?.toBase58(),
|
||||
mode: 'JUPITER',
|
||||
})
|
||||
const { ipAllowed, ipCountry } = useIpAddress()
|
||||
|
||||
const walletTokens = mangoStore((s) => s.wallet.tokens)
|
||||
|
||||
const [walletMax, inputDecimals] = useMemo(() => {
|
||||
if (!inputBank) return ['0', 6]
|
||||
const walletBalance = walletBalanceForToken(walletTokens, inputBank.name)
|
||||
const max = floorToDecimal(
|
||||
walletBalance.maxAmount,
|
||||
walletBalance.maxDecimals,
|
||||
).toFixed()
|
||||
return [max, walletBalance.maxDecimals]
|
||||
}, [walletTokens, inputBank])
|
||||
|
||||
const amountInAsDecimal: Decimal | null = useMemo(() => {
|
||||
return Number(debouncedAmountIn)
|
||||
? new Decimal(debouncedAmountIn)
|
||||
: new Decimal(0)
|
||||
}, [debouncedAmountIn])
|
||||
|
||||
const amountOutAsDecimal: Decimal | null = useMemo(() => {
|
||||
return Number(debouncedAmountOut)
|
||||
? new Decimal(debouncedAmountOut)
|
||||
: new Decimal(0)
|
||||
}, [debouncedAmountOut])
|
||||
|
||||
const setAmountInFormValue = useCallback(
|
||||
(amountIn: string, setSwapMode?: boolean) => {
|
||||
set((s) => {
|
||||
s.swap.amountIn = amountIn
|
||||
if (!parseFloat(amountIn)) {
|
||||
s.swap.amountOut = ''
|
||||
}
|
||||
if (setSwapMode) {
|
||||
s.swap.swapMode = 'ExactIn'
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const setAmountOutFormValue = useCallback(
|
||||
(amountOut: string, setSwapMode?: boolean) => {
|
||||
set((s) => {
|
||||
s.swap.amountOut = amountOut
|
||||
if (!parseFloat(amountOut)) {
|
||||
s.swap.amountIn = ''
|
||||
}
|
||||
if (setSwapMode) {
|
||||
s.swap.swapMode = 'ExactOut'
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const handleAmountInChange = useCallback(
|
||||
(e: NumberFormatValues, info: SourceInfo) => {
|
||||
if (info.source !== 'event') return
|
||||
setAmountInFormValue(e.value)
|
||||
if (swapMode === 'ExactOut') {
|
||||
set((s) => {
|
||||
s.swap.swapMode = 'ExactIn'
|
||||
})
|
||||
}
|
||||
},
|
||||
[outputBank, setAmountInFormValue, swapMode],
|
||||
)
|
||||
|
||||
const handleAmountOutChange = useCallback(
|
||||
(e: NumberFormatValues, info: SourceInfo) => {
|
||||
if (info.source !== 'event') return
|
||||
if (swapMode === 'ExactIn') {
|
||||
set((s) => {
|
||||
s.swap.swapMode = 'ExactOut'
|
||||
})
|
||||
}
|
||||
setAmountOutFormValue(e.value)
|
||||
},
|
||||
[swapMode, setAmountOutFormValue],
|
||||
)
|
||||
|
||||
const handleMax = useCallback(
|
||||
(amountIn: string) => {
|
||||
setAmountInFormValue(amountIn, true)
|
||||
},
|
||||
[setAmountInFormValue],
|
||||
)
|
||||
|
||||
/*
|
||||
Once a route is returned from the Jupiter API, use the inAmount or outAmount
|
||||
depending on the swapMode and set those values in state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (typeof bestRoute !== 'undefined') {
|
||||
setSelectedRoute(bestRoute)
|
||||
|
||||
if (inputBank && swapMode === 'ExactOut' && bestRoute) {
|
||||
const inAmount = new Decimal(bestRoute!.inAmount)
|
||||
.div(10 ** inputBank.mintDecimals)
|
||||
.toString()
|
||||
setAmountInFormValue(inAmount)
|
||||
} else if (outputBank && swapMode === 'ExactIn' && bestRoute) {
|
||||
const outAmount = new Decimal(bestRoute!.outAmount)
|
||||
.div(10 ** outputBank.mintDecimals)
|
||||
.toString()
|
||||
setAmountOutFormValue(outAmount)
|
||||
}
|
||||
}
|
||||
}, [bestRoute, swapMode, inputBank, outputBank])
|
||||
|
||||
/*
|
||||
If the use margin setting is toggled, clear the form values
|
||||
*/
|
||||
useEffect(() => {
|
||||
setAmountInFormValue('')
|
||||
setAmountOutFormValue('')
|
||||
}, [useMargin, setAmountInFormValue, setAmountOutFormValue])
|
||||
|
||||
const handleSwitchTokens = useCallback(() => {
|
||||
if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) {
|
||||
setAmountInFormValue(amountOutAsDecimal.toString())
|
||||
}
|
||||
const inputBank = mangoStore.getState().swap.inputBank
|
||||
const outputBank = mangoStore.getState().swap.outputBank
|
||||
set((s) => {
|
||||
s.swap.inputBank = outputBank
|
||||
s.swap.outputBank = inputBank
|
||||
})
|
||||
setAnimateSwitchArrow(
|
||||
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1,
|
||||
)
|
||||
}, [setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal])
|
||||
|
||||
const loadingSwapDetails: boolean = useMemo(() => {
|
||||
return (
|
||||
!!(amountInAsDecimal.toNumber() || amountOutAsDecimal.toNumber()) &&
|
||||
connected &&
|
||||
typeof selectedRoute === 'undefined'
|
||||
)
|
||||
}, [amountInAsDecimal, amountOutAsDecimal, connected, selectedRoute])
|
||||
|
||||
const handleSizePercentage = (percentage: string) => {
|
||||
setSizePercentage(percentage)
|
||||
const walletMaxDecimal = new Decimal(walletMax)
|
||||
if (walletMaxDecimal.gt(0)) {
|
||||
let amount = walletMaxDecimal.mul(percentage).div(100)
|
||||
if (percentage !== '100') {
|
||||
amount = floorToDecimal(amount, inputDecimals)
|
||||
}
|
||||
setAmountInFormValue(amount.toFixed())
|
||||
} else {
|
||||
setAmountInFormValue('0')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Transition
|
||||
className="absolute right-0 top-0 z-10 h-full w-full bg-th-bkg-1 pb-0"
|
||||
show={showConfirm}
|
||||
enter="transition ease-in duration-300"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-out duration-300"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<SwapReviewRouteInfo
|
||||
amountIn={amountInAsDecimal}
|
||||
isWalletSwap
|
||||
onClose={() => setShowConfirm(false)}
|
||||
routes={routes}
|
||||
selectedRoute={selectedRoute}
|
||||
setSelectedRoute={setSelectedRoute}
|
||||
slippage={slippage}
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<InlineNotification type="info" desc={t('swap:wallet-swap-desc')} />
|
||||
</div>
|
||||
<WalletSellTokenInput
|
||||
handleAmountInChange={handleAmountInChange}
|
||||
setShowTokenSelect={setShowTokenSelect}
|
||||
handleMax={handleMax}
|
||||
max={walletMax}
|
||||
/>
|
||||
<div className="rounded-b-xl bg-th-bkg-2 p-3 pt-0">
|
||||
{swapFormSizeUi === 'slider' ? (
|
||||
<WalletSwapSlider
|
||||
amount={amountInAsDecimal.toNumber()}
|
||||
onChange={(v) => setAmountInFormValue(v, true)}
|
||||
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
|
||||
maxAmount={parseFloat(walletMax)}
|
||||
/>
|
||||
) : (
|
||||
<div className="col-span-2">
|
||||
<ButtonGroup
|
||||
activeValue={sizePercentage}
|
||||
onChange={(p) => handleSizePercentage(p)}
|
||||
values={DEFAULT_PERCENTAGE_VALUES}
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="my-2 flex justify-center">
|
||||
<button
|
||||
className="rounded-full border border-th-fgd-4 p-1.5 text-th-fgd-3 focus-visible:border-th-active md:hover:border-th-active md:hover:text-th-active"
|
||||
onClick={handleSwitchTokens}
|
||||
>
|
||||
<ArrowDownIcon
|
||||
className="h-5 w-5"
|
||||
style={
|
||||
animateSwitchArrow % 2 == 0
|
||||
? { transform: 'rotate(0deg)' }
|
||||
: { transform: 'rotate(360deg)' }
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<BuyTokenInput
|
||||
handleAmountOutChange={handleAmountOutChange}
|
||||
loading={loadingSwapDetails}
|
||||
setShowTokenSelect={setShowTokenSelect}
|
||||
/>
|
||||
{ipAllowed ? (
|
||||
<SwapFormSubmitButton
|
||||
loadingSwapDetails={loadingSwapDetails}
|
||||
useMargin={useMargin}
|
||||
selectedRoute={selectedRoute}
|
||||
setShowConfirm={setShowConfirm}
|
||||
amountIn={amountInAsDecimal}
|
||||
inputSymbol={inputBank?.name}
|
||||
amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
disabled
|
||||
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
|
||||
size="large"
|
||||
>
|
||||
{t('country-not-allowed', {
|
||||
country: ipCountry ? `(${ipCountry})` : '',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletSwapForm
|
||||
|
||||
const SwapFormSubmitButton = ({
|
||||
amountIn,
|
||||
amountOut,
|
||||
loadingSwapDetails,
|
||||
selectedRoute,
|
||||
setShowConfirm,
|
||||
}: {
|
||||
amountIn: Decimal
|
||||
amountOut: number | undefined
|
||||
inputSymbol: string | undefined
|
||||
loadingSwapDetails: boolean
|
||||
selectedRoute: RouteInfo | undefined | null
|
||||
setShowConfirm: (x: boolean) => void
|
||||
useMargin: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { connected } = useWallet()
|
||||
|
||||
const disabled =
|
||||
(connected && !amountIn.toNumber()) || !amountOut || !selectedRoute
|
||||
|
||||
return (
|
||||
<>
|
||||
{connected ? (
|
||||
<Button
|
||||
onClick={() => setShowConfirm(true)}
|
||||
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
|
||||
disabled={disabled}
|
||||
size="large"
|
||||
>
|
||||
{loadingSwapDetails ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<span>{t('swap:review-swap')}</span>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<SecondaryConnectButton
|
||||
className="mb-4 mt-6 flex w-full items-center justify-center"
|
||||
isLarge
|
||||
/>
|
||||
)}
|
||||
{selectedRoute === null && amountIn.gt(0) ? (
|
||||
<div className="mb-4">
|
||||
<InlineNotification type="error" desc={t('swap:no-swap-found')} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import LeverageSlider from '../shared/LeverageSlider'
|
||||
|
||||
const WalletSwapSlider = ({
|
||||
amount,
|
||||
onChange,
|
||||
step,
|
||||
maxAmount,
|
||||
}: {
|
||||
amount: number
|
||||
onChange: (x: string) => void
|
||||
step: number
|
||||
maxAmount: number
|
||||
}) => {
|
||||
return (
|
||||
<LeverageSlider
|
||||
amount={amount}
|
||||
leverageMax={maxAmount}
|
||||
onChange={onChange}
|
||||
step={step}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletSwapSlider
|
|
@ -51,6 +51,8 @@
|
|||
"tooltip-favorite-swap-remove": "Remove pair from favorites",
|
||||
"trigger-beta": "Trigger orders are in beta. Use with caution.",
|
||||
"use-margin": "Allow Margin",
|
||||
"wallet-swap": "Wallet Swap",
|
||||
"wallet-swap-desc": "Swap tokens within your wallet",
|
||||
"warning-no-collateral": "You have no free collateral",
|
||||
"you-buy": "You're buying",
|
||||
"you-sell": "You're selling"
|
||||
|
|
|
@ -49,6 +49,8 @@
|
|||
"tooltip-max-slippage": "If price slips beyond your maximum slippage your swap will not be executed",
|
||||
"trigger-beta": "Trigger orders are in beta. Use with caution.",
|
||||
"use-margin": "Allow Margin",
|
||||
"wallet-swap": "Wallet Swap",
|
||||
"wallet-swap-desc": "Swap tokens within your wallet",
|
||||
"warning-no-collateral": "You have no free collateral",
|
||||
"you-buy": "You're buying",
|
||||
"you-sell": "You're selling"
|
||||
|
|
|
@ -49,6 +49,8 @@
|
|||
"tooltip-max-slippage": "If price slips beyond your maximum slippage your swap will not be executed",
|
||||
"trigger-beta": "Trigger orders are in beta. Use with caution.",
|
||||
"use-margin": "Allow Margin",
|
||||
"wallet-swap": "Wallet Swap",
|
||||
"wallet-swap-desc": "Swap tokens within your wallet",
|
||||
"warning-no-collateral": "You have no free collateral",
|
||||
"you-buy": "You're buying",
|
||||
"you-sell": "You're selling"
|
||||
|
|
|
@ -51,6 +51,8 @@
|
|||
"tooltip-max-slippage": "如果价格下滑超过您的最大滑点,换币交易将不会被执行",
|
||||
"trigger-beta": "Trigger orders are in beta. Use with caution.",
|
||||
"use-margin": "允许杠杆",
|
||||
"wallet-swap": "Wallet Swap",
|
||||
"wallet-swap-desc": "Swap tokens within your wallet",
|
||||
"warning-no-collateral": "你没有可用质押品",
|
||||
"you-buy": "你正在买",
|
||||
"you-sell": "你正在卖"
|
||||
|
|
|
@ -51,6 +51,8 @@
|
|||
"tooltip-max-slippage": "如果價格下滑超過您的最大滑點,換幣交易將不會被執行",
|
||||
"trigger-beta": "Trigger orders are in beta. Use with caution.",
|
||||
"use-margin": "允許槓桿",
|
||||
"wallet-swap": "Wallet Swap",
|
||||
"wallet-swap-desc": "Swap tokens within your wallet",
|
||||
"warning-no-collateral": "你沒有可用質押品",
|
||||
"you-buy": "你正在買",
|
||||
"you-sell": "你正在賣"
|
||||
|
|
Loading…
Reference in New Issue