From 04e4ed87931143ba9778650fd0f3e7f3b5a7beca Mon Sep 17 00:00:00 2001 From: chrisling-dev Date: Wed, 24 Jul 2024 15:05:49 +0800 Subject: [PATCH] feat: improved swap amount --- .../components/recipes/TokenSelectDialog.tsx | 4 +- .../widgets/chainflip-swap/FromAmount.tsx | 72 +++++++++++++++---- .../widgets/chainflip-swap/index.tsx | 39 ++++------ .../swap-modules/chainflip.swap-module.ts | 11 +-- .../swap-modules/common.swap-module.ts | 1 + .../widgets/chainflip-swap/swaps.api.ts | 3 +- apps/portal/src/hooks/useFastBalance.ts | 10 ++- apps/portal/src/hooks/useSubstrateBalance.ts | 15 ++-- packages/ui/package.json | 1 - 9 files changed, 101 insertions(+), 55 deletions(-) diff --git a/apps/portal/src/components/recipes/TokenSelectDialog.tsx b/apps/portal/src/components/recipes/TokenSelectDialog.tsx index 9081f5874..a0e2af7ff 100644 --- a/apps/portal/src/components/recipes/TokenSelectDialog.tsx +++ b/apps/portal/src/components/recipes/TokenSelectDialog.tsx @@ -23,13 +23,13 @@ const TokenSelectDialogItem = (props: TokenSelectDialogItemProps) => { const value = useMemo(() => { if (props.rates === undefined) return null - const balance = fastBalance?.balance ?? props.defaultBalanceDecimal + const balance = fastBalance?.balance?.transferrable ?? props.defaultBalanceDecimal if (balance === undefined) return return (props.rates * balance.toNumber()).toLocaleString(undefined, { currency, style: 'currency' }) }, [currency, fastBalance, props.defaultBalanceDecimal, props.rates]) const balanceUI = useMemo(() => { - const balance = fastBalance?.balance ?? props.defaultBalanceDecimal + const balance = fastBalance?.balance?.transferrable ?? props.defaultBalanceDecimal if (balance === undefined) return return balance.toLocaleString(undefined, { maximumFractionDigits: 4 }) }, [fastBalance?.balance, props.defaultBalanceDecimal]) diff --git a/apps/portal/src/components/widgets/chainflip-swap/FromAmount.tsx b/apps/portal/src/components/widgets/chainflip-swap/FromAmount.tsx index dc20e09f8..f496d8219 100644 --- a/apps/portal/src/components/widgets/chainflip-swap/FromAmount.tsx +++ b/apps/portal/src/components/widgets/chainflip-swap/FromAmount.tsx @@ -12,18 +12,25 @@ import { selectedCurrencyState } from '@/domains/balances' import { cn } from '@/lib/utils' import { useTokenRates } from '@talismn/balances-react' import { Decimal } from '@talismn/math' -import { CircularProgressIndicator, TextInput } from '@talismn/ui' +import { CircularProgressIndicator, TextInput, Tooltip } from '@talismn/ui' import { useAtom, useAtomValue } from 'jotai' +import { HelpCircle } from 'lucide-react' import { useCallback, useMemo } from 'react' +import { Link } from 'react-router-dom' import { useRecoilValue } from 'recoil' +const hardcodedGasBufferByTokenSymbol: Record = { + dot: 0.03, + eth: 0.01, // same as uniswap, they give a fixed 0.01 ETH buffer regardless of the chain +} + export const FromAmount: React.FC<{ // NOTE: we get this as a prop so we dont have to get this balance twice. The parent component also needs this to // check whether user has enough balance to swap - availableBalance: { balance: Decimal; loading: boolean } | null - wouldReapAccount?: boolean + availableBalance?: Decimal + stayAliveBalance?: Decimal insufficientBalance?: boolean -}> = ({ availableBalance, insufficientBalance }) => { +}> = ({ availableBalance, insufficientBalance, stayAliveBalance }) => { const [fromAmountInput, setFromAmountInput] = useAtom(fromAmountInputAtom) const [fromAsset, setFromAsset] = useAtom(fromAssetAtom) const [toAsset, setToAsset] = useAtom(toAssetAtom) @@ -52,16 +59,35 @@ export const FromAmount: React.FC<{ return +fromAmount.toString() * rateInCurrency }, [currency, fromAmount, fromAsset, rates]) + const accountWouldBeReaped = useMemo(() => { + if (!stayAliveBalance) return false + return stayAliveBalance < fromAmount + }, [fromAmount, stayAliveBalance]) + + const maxAfterGas = useMemo(() => { + if (!fromAsset || !availableBalance) return null + const idParts = fromAsset.id.split('-') + const assetType = idParts[idParts.length - 1] + if (assetType === 'native') { + const gasBuffer = hardcodedGasBufferByTokenSymbol[fromAsset.symbol.toLowerCase()] + if (gasBuffer) { + const gasBufferDecimal = Decimal.fromUserInputOrUndefined(gasBuffer?.toString(), fromAsset.decimals) + return Decimal.fromPlanck(availableBalance.planck - (gasBufferDecimal?.planck ?? 0n), fromAsset.decimals) + } + } + return availableBalance + }, [availableBalance, fromAsset]) + return ( + fromAsset && fromAddress ? ( + availableBalance ? ( + `Balance: ${availableBalance.toLocaleString()}` ) : ( - `Balance: ${availableBalance.balance.toLocaleString()}` + ) ) : null } @@ -74,6 +100,30 @@ export const FromAmount: React.FC<{

Insufficient balance

+ ) : accountWouldBeReaped ? ( +
+

+ Account would be reaped +

+ + + This amount will cause your balance to go below the network's{' '} + Existential Deposit, +
+ which would cause your account to be reaped. +
+ Any remaining funds in a reaped account cannot be recovered. +

+ } + > + + + +
+
) : null} } @@ -91,13 +141,11 @@ export const FromAmount: React.FC<{ type="number" trailingIcon={
- {availableBalance && !availableBalance.loading && ( + {maxAfterGas && maxAfterGas.planck > 0 && ( - setFromAmountInput( - Decimal.fromPlanck(availableBalance.balance.planck, availableBalance.balance.decimals).toString() - ) + setFromAmountInput(Decimal.fromPlanck(maxAfterGas.planck, maxAfterGas.decimals).toString()) } >

Max

diff --git a/apps/portal/src/components/widgets/chainflip-swap/index.tsx b/apps/portal/src/components/widgets/chainflip-swap/index.tsx index eb24c626f..a37007f77 100644 --- a/apps/portal/src/components/widgets/chainflip-swap/index.tsx +++ b/apps/portal/src/components/widgets/chainflip-swap/index.tsx @@ -26,7 +26,6 @@ import { } from './swaps.api' import { useFastBalance } from '@/hooks/useFastBalance' import { isAddress as isSubstrateAddress } from '@polkadot/util-crypto' -import { Decimal } from '@talismn/math' import { Button, Surface, TonalIconButton } from '@talismn/ui' import { Repeat } from '@talismn/web-icons' import { useAtom, useAtomValue, useSetAtom } from 'jotai' @@ -83,28 +82,10 @@ export const ChainFlipSwap: React.FC = () => { }, [fromAddress, fromAsset]) ) - const availableBalance = useMemo(() => { - if (!fromAddress || !fromToken || !fastBalance) return null - - if ( - fastBalance.balance === undefined || - fromToken.symbol.toLowerCase() !== fastBalance?.balance?.options?.currency?.toLowerCase() - ) { - return { - balance: Decimal.fromPlanck(0n, fromToken.decimals, { currency: fromToken.symbol }), - loading: true, - } - } - return { - balance: fastBalance?.balance ?? Decimal.fromPlanck(0n, fromToken.decimals, { currency: fromToken.symbol }), - loading: fastBalance?.balance === undefined, - } - }, [fastBalance, fromAddress, fromToken]) - const insufficientBalance = useMemo(() => { - if (!availableBalance || availableBalance.loading) return undefined - return fromAmount.planck > availableBalance.balance.planck - }, [availableBalance, fromAmount.planck]) + if (!fastBalance?.balance) return undefined + return fromAmount.planck > fastBalance.balance.transferrable.planck + }, [fastBalance, fromAmount.planck]) useEffect(() => { if (fromAmountInput.trim() !== '' && fromAsset && toAsset) setShouldFocusDetails(true) @@ -134,7 +115,11 @@ export const ChainFlipSwap: React.FC = () => {

Select Asset

- +
{ { loading={swapping} onClick={() => { setInfoTab('details') - if (quote.state === 'hasData' && quote.data) { - swap(quote.data.protocol) + if (quote.state === 'hasData' && quote.data && fastBalance?.balance) { + swap(quote.data.protocol, fromAmount.planck > fastBalance.balance.stayAlive.planck) } }} > diff --git a/apps/portal/src/components/widgets/chainflip-swap/swap-modules/chainflip.swap-module.ts b/apps/portal/src/components/widgets/chainflip-swap/swap-modules/chainflip.swap-module.ts index c652eeb56..eecd71ffd 100644 --- a/apps/portal/src/components/widgets/chainflip-swap/swap-modules/chainflip.swap-module.ts +++ b/apps/portal/src/components/widgets/chainflip-swap/swap-modules/chainflip.swap-module.ts @@ -222,7 +222,7 @@ const saveAddressForQuest = async (swapId: string, fromAddress: string) => { const swap: SwapFunction = async ( get: Getter, _: Setter, - { evmWalletClient, getSubstrateApi, substrateWallet } + { evmWalletClient, getSubstrateApi, substrateWallet, allowReap } ) => { const sdk = get(swapSdkAtom) const network = get(chainflipNetworkAtom) @@ -308,9 +308,10 @@ const swap: SwapFunction = async ( const polkadotRpc = get(polkadotRpcAtom) const polkadotApi = await getSubstrateApi(polkadotRpc) - await polkadotApi.tx.balances - .transferKeepAlive(depositAddress.depositAddress, depositAddress.amount) - .signAndSend(fromAddress, { signer, withSignedTransaction: true }) + await polkadotApi.tx.balances[allowReap ? 'transferAllowDeath' : 'transferKeepAlive']( + depositAddress.depositAddress, + depositAddress.amount + ).signAndSend(fromAddress, { signer, withSignedTransaction: true }) saveAddressForQuest(depositAddress.depositChannelId, fromAddress) return { protocol: PROTOCOL, data: { id: depositAddress.depositChannelId, network } } @@ -357,7 +358,7 @@ const getEstimateGasTx: GetEstimateGasTxFunction = async (get, { getSubstrateApi return { type: 'substrate', fromAddress, - tx: polkadotApi.tx.balances.transferKeepAlive(fromAddress, fromAmount.planck), + tx: polkadotApi.tx.balances.transferAllowDeath(fromAddress, fromAmount.planck), } } diff --git a/apps/portal/src/components/widgets/chainflip-swap/swap-modules/common.swap-module.ts b/apps/portal/src/components/widgets/chainflip-swap/swap-modules/common.swap-module.ts index 11272a06d..fb08427e8 100644 --- a/apps/portal/src/components/widgets/chainflip-swap/swap-modules/common.swap-module.ts +++ b/apps/portal/src/components/widgets/chainflip-swap/swap-modules/common.swap-module.ts @@ -33,6 +33,7 @@ type SwapProps = { evmWalletClient?: WalletClient substrateWallet?: BaseWallet getSubstrateApi: (rpc: string) => Promise + allowReap?: boolean toAmount: Decimal | null } diff --git a/apps/portal/src/components/widgets/chainflip-swap/swaps.api.ts b/apps/portal/src/components/widgets/chainflip-swap/swaps.api.ts index 8af702668..45b06ea7f 100644 --- a/apps/portal/src/components/widgets/chainflip-swap/swaps.api.ts +++ b/apps/portal/src/components/widgets/chainflip-swap/swaps.api.ts @@ -169,7 +169,7 @@ export const useSwap = () => { const swap = useAtomCallback( useCallback( - async (get, set, protocol: SupportedSwapProtocol) => { + async (get, set, protocol: SupportedSwapProtocol, allowReap = false) => { try { set(swappingAtom, true) const toAmount = await get(toAmountAtom) @@ -181,6 +181,7 @@ export const useSwap = () => { substrateWallet, getSubstrateApi, toAmount, + allowReap, }) // TODO: instead of just getting "swapped: boolean" // we should handle adding swap to activity generically so that diff --git a/apps/portal/src/hooks/useFastBalance.ts b/apps/portal/src/hooks/useFastBalance.ts index 9823936d3..fcb5b9e0c 100644 --- a/apps/portal/src/hooks/useFastBalance.ts +++ b/apps/portal/src/hooks/useFastBalance.ts @@ -35,10 +35,16 @@ export const useFastBalance = (props?: UseFastBalanceProps) => { balance: substrateBalance, } } + const ethBalance = evmBalance.data + ? Decimal.fromPlanck(evmBalance.data.value, evmBalance.data.decimals, { currency: evmBalance.data.symbol }) + : undefined return { ...props, - balance: evmBalance.data - ? Decimal.fromPlanck(evmBalance.data.value, evmBalance.data.decimals, { currency: evmBalance.data.symbol }) + balance: ethBalance + ? { + transferrable: ethBalance, + stayAlive: ethBalance, + } : undefined, } }, [evmBalance.data, props, substrateBalance]) diff --git a/apps/portal/src/hooks/useSubstrateBalance.ts b/apps/portal/src/hooks/useSubstrateBalance.ts index dfdb6769b..3d05a2cc9 100644 --- a/apps/portal/src/hooks/useSubstrateBalance.ts +++ b/apps/portal/src/hooks/useSubstrateBalance.ts @@ -11,8 +11,13 @@ export type UseSubstrateBalanceProps = { address: string } +type SubstrateBalance = { + transferrable: Decimal + stayAlive: Decimal +} + export const useSubstrateBalance = (props?: UseSubstrateBalanceProps) => { - const [balance, setBalance] = useState() + const [balance, setBalance] = useState() const unsubRef = useRef<() => void>() const chainsLoadable = useRecoilValueLoadable(chainsState) const api = useRecoilValueLoadable( @@ -34,14 +39,14 @@ export const useSubstrateBalance = (props?: UseSubstrateBalanceProps) => { const reserved = account.data.reserved.toBigInt() const frozen = account.data.frozen.toBigInt() const untouchable = BigMath.max(frozen - reserved, 0n) - // make sure untouchable is never less than the existentialDeposit - const untouchableOrEd = BigMath.max(untouchable, ed.toBigInt()) const free = account.data.free.toBigInt() - const transferableBN = BigMath.max(free - untouchableOrEd, 0n) + const transferableBN = BigMath.max(free - untouchable, 0n) const decimals = api.contents.registry.chainDecimals[0] ?? 10 const symbol = api.contents.registry.chainTokens[0] ?? 'DOT' - setBalance(Decimal.fromPlanck(transferableBN, decimals, { currency: symbol })) + const transferrable = Decimal.fromPlanck(transferableBN, decimals, { currency: symbol }) + const stayAlive = Decimal.fromPlanck(free - ed.toBigInt(), decimals, { currency: symbol }) + setBalance({ transferrable, stayAlive }) }) .then(unsub => { unsubRef.current = unsub diff --git a/packages/ui/package.json b/packages/ui/package.json index 3dce20c3c..56d8f4e3f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,7 +15,6 @@ "dev": "tsc --build --watch tsconfig.build.json", "build": "rm -rf build && tsc --build tsconfig.build.json", "lint": "eslint src", - "check-types": "tsc --noEmit", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" },