wallet swap ui

This commit is contained in:
saml33 2023-09-18 22:55:37 +10:00
parent 9490635968
commit 140a0f9352
14 changed files with 732 additions and 73 deletions

View File

@ -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}

View File

@ -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

View File

@ -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>

View File

@ -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')}

View File

@ -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')

View File

@ -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"
>

View File

@ -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

View File

@ -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}
</>
)
}

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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": "你正在卖"

View File

@ -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": "你正在賣"