From d4d03881eca50d833f0098f10ed192ea11b622d0 Mon Sep 17 00:00:00 2001 From: kev1n-peters <96065607+kev1n-peters@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:52:50 -0500 Subject: [PATCH] solana gas dropoff rent min to fresh wallets (#1082) Co-authored-by: Evan Gray --- sdk/src/contexts/solana/context.ts | 13 ++- wormhole-connect/src/utils/routes/operator.ts | 33 +++++++- .../src/views/Bridge/NativeGasSlider.tsx | 79 +++++++++++++------ 3 files changed, 98 insertions(+), 27 deletions(-) diff --git a/sdk/src/contexts/solana/context.ts b/sdk/src/contexts/solana/context.ts index 8c3ec54f0..22c951da2 100644 --- a/sdk/src/contexts/solana/context.ts +++ b/sdk/src/contexts/solana/context.ts @@ -1040,7 +1040,18 @@ export class SolanaContext< BigNumber.from(amount).toBigInt(), decimals, ); - return BigNumber.from(nativeTokenAmount); + // an non-existent account cannot be sent less than the rent exempt amount + // in order to create the wallet, it must be sent at least the rent exemption minimum + const acctExists = + (await this.connection!.getAccountInfo(new PublicKey(walletAddress))) !== + null; + if (acctExists) return BigNumber.from(nativeTokenAmount); + const minBalance = await this.connection!.getMinimumBalanceForRentExemption( + 0, + ); + return nativeTokenAmount > minBalance + ? BigNumber.from(nativeTokenAmount) + : BigNumber.from(0); } async calculateMaxSwapAmount( diff --git a/wormhole-connect/src/utils/routes/operator.ts b/wormhole-connect/src/utils/routes/operator.ts index 8a36395dd..ac2610b23 100644 --- a/wormhole-connect/src/utils/routes/operator.ts +++ b/wormhole-connect/src/utils/routes/operator.ts @@ -1,3 +1,4 @@ +import { PublicKey } from '@solana/web3.js'; import { ChainId, ChainName, @@ -12,7 +13,14 @@ import { RelayRoute } from './relay'; // import { HashflowRoute } from './hashflow'; import { CCTPRelayRoute } from './cctpRelay'; import { CosmosGatewayRoute } from './cosmosGateway'; -import { ParsedMessage, PayloadType, getMessage, isEvmChain, wh } from '../sdk'; +import { + ParsedMessage, + PayloadType, + getMessage, + isEvmChain, + solanaContext, + wh, +} from '../sdk'; import { isCosmWasmChain } from '../cosmos'; import RouteAbstract from './routeAbstract'; import { @@ -484,6 +492,29 @@ export class Operator { return r.maxSwapAmount(destChain, token, walletAddress); } + async minSwapAmountNative( + route: Route, + destChain: ChainName | ChainId, + token: TokenId, + walletAddress: string, + ): Promise { + const chainName = wh.toChainName(destChain); + if (chainName === 'solana') { + const context = solanaContext(); + // an non-existent account cannot be sent less than the rent exempt amount + // in order to create the wallet, it must be sent at least the rent exemption minimum + const acctExists = + (await context.connection!.getAccountInfo( + new PublicKey(walletAddress), + )) !== null; + if (acctExists) return BigNumber.from(0); + const minBalance = + await context.connection!.getMinimumBalanceForRentExemption(0); + return BigNumber.from(minBalance); + } + return BigNumber.from(0); + } + tryFetchRedeemTx( route: Route, txData: UnsignedMessage, diff --git a/wormhole-connect/src/views/Bridge/NativeGasSlider.tsx b/wormhole-connect/src/views/Bridge/NativeGasSlider.tsx index ac6bb148f..a64693c47 100644 --- a/wormhole-connect/src/views/Bridge/NativeGasSlider.tsx +++ b/wormhole-connect/src/views/Bridge/NativeGasSlider.tsx @@ -1,6 +1,6 @@ import Slider, { SliderThumb } from '@mui/material/Slider'; import { styled } from '@mui/material/styles'; -import { BigNumber, utils } from 'ethers'; +import { utils } from 'ethers'; import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; @@ -88,6 +88,7 @@ function formatAmount(amount?: number): number { const INITIAL_STATE = { disabled: false, + min: 0, max: 0, nativeGas: 0, token: formatAmount(), @@ -137,7 +138,7 @@ function GasSlider(props: { disabled: boolean }) { if (actualMaxSwap) { // address the bug that the swapAmount='maxSwapAmount' results in a 'minimumSendAmount' // that could be higher than 'amount' (due to the buffer packed into minimumSendAmount) - // not a perfect fix for all posible 'getMinSendAmount' functions - but valid for linear ones + // not a perfect fix for all possible 'getMinSendAmount' functions - but valid for linear ones // // For example, if 'amount' is 1.1, and relayerFee is 1, then 'amountNum' (the receive amount) is // 0.1, and 'actualMaxSwap' is 0.1. @@ -178,18 +179,23 @@ function GasSlider(props: { disabled: boolean }) { !RouteOperator.getRoute(route).NATIVE_GAS_DROPOFF_SUPPORTED || !receivingWallet.address || !receivingToken - ) + ) { return; + } - const tokenId = receivingToken.tokenId!; - RouteOperator.maxSwapAmount( - route, - toChain, - tokenId, - receivingWallet.address, - ) - .then((res: BigNumber) => { - if (!res) { + let cancelled = false; + (async () => { + const tokenId = receivingToken.tokenId!; + + try { + const maxSwapAmount = await RouteOperator.maxSwapAmount( + route, + toChain, + tokenId, + receivingWallet.address, + ); + if (cancelled) return; + if (!maxSwapAmount) { dispatch(setMaxSwapAmt(undefined)); return; } @@ -197,11 +203,11 @@ function GasSlider(props: { disabled: boolean }) { wh.toChainId(toChain), tokenId, ); - const amt = toDecimals(res, toChainDecimals, 6); + const amt = toDecimals(maxSwapAmount, toChainDecimals, 6); dispatch(setMaxSwapAmt(Number.parseFloat(amt))); - }) - .catch((e) => { - if (e.message.includes('swap rate not set')) { + } catch (e: any) { + if (cancelled) return; + if (e.message?.includes('swap rate not set')) { if (route === Route.CCTPRelay) { dispatch(setTransferRoute(Route.CCTPManual)); } else { @@ -210,13 +216,32 @@ function GasSlider(props: { disabled: boolean }) { } else { throw e; } - }); + } - // get conversion rate of token - const { gasToken } = CHAINS[toChain]!; - getConversion(token, gasToken).then((res: number) => { - setState((prevState) => ({ ...prevState, conversionRate: res })); - }); + // get conversion rate of token + const { gasToken } = CHAINS[toChain]!; + const conversionRate = await getConversion(token, gasToken); + if (cancelled) return; + const minNative = await RouteOperator.minSwapAmountNative( + route, + toChain, + tokenId, + receivingWallet.address, + ); + const minNativeAdjusted = Number.parseFloat( + toDecimals( + minNative, + getTokenDecimals(wh.toChainId(toChain), 'native'), + ), + ); + if (cancelled) return; + const min = conversionRate ? minNativeAdjusted / conversionRate : 0; + setState((prevState) => ({ ...prevState, conversionRate, min })); + })(); + + return () => { + cancelled = true; + }; }, [ sendingToken, receivingToken, @@ -255,9 +280,10 @@ function GasSlider(props: { disabled: boolean }) { // compute amounts on change const handleChange = (e: any) => { if (!amountNum || !state.conversionRate) return; - const newGasAmount = e.target.value * state.conversionRate; - const newTokenAmount = amountNum - e.target.value; - const swapAmount = e.target.value; + const value = e.target.value < state.min ? 0 : e.target.value; + const newGasAmount = value * state.conversionRate; + const newTokenAmount = amountNum - value; + const swapAmount = value; const conversion = { nativeGas: formatAmount(newGasAmount), token: formatAmount(newTokenAmount), @@ -368,6 +394,9 @@ function GasSlider(props: { disabled: boolean }) { } valueLabelDisplay="auto" onChange={handleChange} + marks={ + state.min ? [{ value: state.min, label: 'Min' }] : undefined + } />