From e13a839cbf1088db14f939b154df551f20555b7d Mon Sep 17 00:00:00 2001 From: Artur Sapek Date: Mon, 21 Oct 2024 12:09:50 -0400 Subject: [PATCH] use amount.Amount type to store balances --- wormhole-connect/src/config/index.ts | 4 +- .../src/hooks/useAmountValidation.ts | 28 +-- wormhole-connect/src/hooks/useComputeFees.ts | 8 +- wormhole-connect/src/hooks/useComputeQuote.ts | 9 +- .../src/hooks/useFetchSupportedRoutes.ts | 2 +- .../src/hooks/useGetTokenBalances.ts | 21 +- .../src/hooks/useRoutesQuotesBulk.ts | 10 +- .../src/hooks/useSortedRoutesWithQuotes.ts | 3 +- .../src/hooks/useUSDamountGetter.ts | 5 +- wormhole-connect/src/routes/index.ts | 1 - wormhole-connect/src/routes/operator.ts | 15 +- wormhole-connect/src/routes/sdkv2/route.ts | 216 ++---------------- wormhole-connect/src/routes/types.ts | 37 --- wormhole-connect/src/store/redeem.ts | 10 - wormhole-connect/src/store/transferInput.ts | 31 +-- wormhole-connect/src/telemetry/index.ts | 11 +- wormhole-connect/src/telemetry/types.ts | 4 +- wormhole-connect/src/utils/amount.ts | 24 -- wormhole-connect/src/utils/index.ts | 39 ++-- wormhole-connect/src/utils/sdkv2.ts | 42 ++-- .../src/utils/transferValidation.ts | 44 ++-- .../src/views/v2/Bridge/AmountInput/index.tsx | 38 +-- .../views/v2/Bridge/AssetPicker/TokenItem.tsx | 8 +- .../views/v2/Bridge/AssetPicker/TokenList.tsx | 5 +- .../v2/Bridge/ReviewTransaction/GasSlider.tsx | 11 +- .../v2/Bridge/ReviewTransaction/index.tsx | 20 +- .../views/v2/Bridge/Routes/SingleRoute.tsx | 28 +-- .../src/views/v2/Bridge/Routes/index.tsx | 24 -- .../src/views/v2/Bridge/index.tsx | 2 +- .../v2/Redeem/TransactionDetails/index.tsx | 8 +- .../src/views/v2/TxHistory/Item/index.tsx | 2 +- 31 files changed, 205 insertions(+), 505 deletions(-) delete mode 100644 wormhole-connect/src/routes/index.ts delete mode 100644 wormhole-connect/src/utils/amount.ts diff --git a/wormhole-connect/src/config/index.ts b/wormhole-connect/src/config/index.ts index 29806be18..ef30b4799 100644 --- a/wormhole-connect/src/config/index.ts +++ b/wormhole-connect/src/config/index.ts @@ -36,7 +36,7 @@ import sui from '@wormhole-foundation/sdk/sui'; import cosmwasm from '@wormhole-foundation/sdk/cosmwasm'; import algorand from '@wormhole-foundation/sdk/algorand'; import RouteOperator from 'routes/operator'; -import { getTokenDecimals, getWrappedTokenId } from 'utils'; +import { getTokenDecimals, getWrappedToken } from 'utils'; import { CHAIN_ORDER } from './constants'; import { getTokenBridgeWrappedTokenAddressSync } from 'utils/sdkv2'; import { createUiConfig } from './ui'; @@ -229,7 +229,7 @@ export async function newWormholeContextV2(): Promise> { const fa = getTokenBridgeWrappedTokenAddressSync(token, chain); if (fa) { tokenV2.address = fa.toString(); - tokenV2.decimals = getTokenDecimals(chain, getWrappedTokenId(token)); + tokenV2.decimals = getTokenDecimals(chain, getWrappedToken(token)); } else { continue; } diff --git a/wormhole-connect/src/hooks/useAmountValidation.ts b/wormhole-connect/src/hooks/useAmountValidation.ts index 1f3f7a741..c36703a80 100644 --- a/wormhole-connect/src/hooks/useAmountValidation.ts +++ b/wormhole-connect/src/hooks/useAmountValidation.ts @@ -12,7 +12,7 @@ type HookReturn = { }; type Props = { - balance?: string | null; + balance?: sdkAmount.Amount | null; routes: RouteState[]; quotesMap: Record; tokenSymbol: string; @@ -50,36 +50,30 @@ export const useAmountValidation = (props: Props): HookReturn => { [props.quotesMap], ); - const allRoutesFailed = useMemo( - () => props.routes.every((route) => !props.quotesMap[route.name]?.success), - [props.routes, props.quotesMap], - ); + const allRoutesFailed = useMemo(() => { + if (Object.keys(props.quotesMap).length === 0) { + return false; + } - const numAmount = Number.parseFloat(amount); + return props.routes.every((route) => { + return props.quotesMap[route.name]?.success === false; + }); + }, [props.routes, props.quotesMap]); // Don't show errors when no amount is set or it's loading - if (!amount || !numAmount || props.disabled) { + if (!amount || props.disabled) { return {}; } - // Input errors - if (Number.isNaN(numAmount)) { - return { - error: 'Amount must be a number.', - }; - } - // Balance errors if (props.balance) { - const balanceNum = Number.parseFloat(props.balance.replaceAll(',', '')); - if (numAmount > balanceNum) { + if (sdkAmount.units(amount) > sdkAmount.units(props.balance)) { return { error: 'Amount exceeds available balance.', }; } } - // All quotes fail. if (allRoutesFailed) { if (minAmount) { const formattedAmount = sdkAmount.display(minAmount); diff --git a/wormhole-connect/src/hooks/useComputeFees.ts b/wormhole-connect/src/hooks/useComputeFees.ts index 9af82dbcb..e525aba4b 100644 --- a/wormhole-connect/src/hooks/useComputeFees.ts +++ b/wormhole-connect/src/hooks/useComputeFees.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { Chain, finality } from '@wormhole-foundation/sdk-base'; +import { Chain, finality, amount as sdkAmount } from '@wormhole-foundation/sdk'; import config from 'config'; @@ -15,7 +15,7 @@ type Props = { destChain: Chain | undefined; destToken: string; route?: string; - amount: string; + amount: sdkAmount.Amount; toNativeToken: number; }; @@ -90,8 +90,8 @@ const useComputeFees = (props: Props): returnProps => { const receiveAmount = await config.routes .get(route) - .computeReceiveAmountWithFees( - Number.parseFloat(amount), + .computeReceiveAmount( + amount, sourceToken, destToken, sourceChain, diff --git a/wormhole-connect/src/hooks/useComputeQuote.ts b/wormhole-connect/src/hooks/useComputeQuote.ts index 6a054ce98..c0392d68e 100644 --- a/wormhole-connect/src/hooks/useComputeQuote.ts +++ b/wormhole-connect/src/hooks/useComputeQuote.ts @@ -14,7 +14,7 @@ type Props = { destChain: Chain | undefined; destToken: string; route?: string; - amount: string; + amount: sdkAmount.Amount; toNativeToken: number; }; @@ -54,13 +54,6 @@ const useComputeQuote = (props: Props): returnProps => { try { setIsFetching(true); - if (Number.isNaN(Number.parseFloat(amount))) { - dispatch(setReceiveAmount('0')); - dispatch(setReceiveNativeAmt(0)); - dispatch(setRelayerFee(undefined)); - return; - } - const quote = ( await config.routes.getQuotes([route], { amount, diff --git a/wormhole-connect/src/hooks/useFetchSupportedRoutes.ts b/wormhole-connect/src/hooks/useFetchSupportedRoutes.ts index a782fa387..607349f76 100644 --- a/wormhole-connect/src/hooks/useFetchSupportedRoutes.ts +++ b/wormhole-connect/src/hooks/useFetchSupportedRoutes.ts @@ -20,7 +20,7 @@ const useFetchSupportedRoutes = (): void => { const [debouncedAmount] = useDebounce(amount, 500); useEffect(() => { - if (!fromChain || !toChain || !token || !destToken) { + if (!fromChain || !toChain || !token || !destToken || !debouncedAmount) { dispatch(setRoutes([])); return; } diff --git a/wormhole-connect/src/hooks/useGetTokenBalances.ts b/wormhole-connect/src/hooks/useGetTokenBalances.ts index 6e4f03cdb..69f12a55b 100644 --- a/wormhole-connect/src/hooks/useGetTokenBalances.ts +++ b/wormhole-connect/src/hooks/useGetTokenBalances.ts @@ -5,13 +5,11 @@ import { useEffect, useState } from 'react'; import { accessBalance, Balances, updateBalances } from 'store/transferInput'; import config, { getWormholeContextV2 } from 'config'; import { TokenConfig } from 'config/types'; -import { formatAmount } from 'utils/amount'; import { chainToPlatform } from '@wormhole-foundation/sdk-base'; import { getTokenBridgeWrappedTokenAddress } from 'utils/sdkv2'; -import { Chain, TokenAddress } from '@wormhole-foundation/sdk'; +import { Chain, TokenAddress, amount } from '@wormhole-foundation/sdk'; +import { getTokenDecimals } from 'utils'; -// TODO: This hook shouldn't format amounts -// Instead the view should format and render accordingly const useGetTokenBalances = ( walletAddress: string, chain: Chain | undefined, @@ -75,8 +73,10 @@ const useGetTokenBalances = ( const tokenIdMapping: Record = {}; const tokenAddresses: string[] = []; for (const tokenConfig of needsUpdate) { + const decimals = getTokenDecimals(chain, tokenConfig); + updatedBalances[tokenConfig.key] = { - balance: '0', + balance: amount.fromBaseUnits(0n, decimals), lastUpdated: now, }; @@ -126,13 +126,12 @@ const useGetTokenBalances = ( for (const tokenAddress in result) { const tokenConfig = tokenIdMapping[tokenAddress]; - const balance = result[tokenAddress]; - let formatted: string | null = null; - if (balance !== null) { - formatted = formatAmount(chain, tokenConfig, balance); - } + const decimals = getTokenDecimals(chain, tokenConfig); + const bus = result[tokenAddress]; + const balance = amount.fromBaseUnits(bus ?? 0n, decimals); + updatedBalances[tokenConfig.key] = { - balance: formatted, + balance, lastUpdated: now, }; } diff --git a/wormhole-connect/src/hooks/useRoutesQuotesBulk.ts b/wormhole-connect/src/hooks/useRoutesQuotesBulk.ts index 1200e6e5a..b6c35f416 100644 --- a/wormhole-connect/src/hooks/useRoutesQuotesBulk.ts +++ b/wormhole-connect/src/hooks/useRoutesQuotesBulk.ts @@ -1,3 +1,4 @@ +import { amount as sdkAmount } from '@wormhole-foundation/sdk'; import { useState, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import type { RootState } from 'store'; @@ -19,7 +20,7 @@ type Params = { sourceToken: string; destChain?: Chain; destToken: string; - amount: string; + amount?: sdkAmount.Amount; nativeGas: number; }; @@ -62,8 +63,7 @@ const useRoutesQuotesBulk = (routes: string[], params: Params): HookReturn => { !params.sourceChain || !params.sourceToken || !params.destChain || - !params.destToken || - !parseFloat(params.amount) + !params.destToken ) { return; } @@ -127,7 +127,7 @@ const useRoutesQuotesBulk = (routes: string[], params: Params): HookReturn => { const quote = quotesMap[name]; if (quote !== undefined && quote.success) { const usdValueOut = calculateUSDPriceRaw( - amount.whole(quote.destinationToken.amount), + quote.destinationToken.amount, usdPrices.data, destTokenConfig, ); @@ -175,7 +175,7 @@ const useRoutesQuotesBulk = (routes: string[], params: Params): HookReturn => { sourceTokenConfig, ); const approxOutputUsdValue = calculateUSDPriceRaw( - amount.display(mayanQuote.destinationToken.amount), + mayanQuote.destinationToken.amount, usdPrices.data, config.tokens[params.destToken], ); diff --git a/wormhole-connect/src/hooks/useSortedRoutesWithQuotes.ts b/wormhole-connect/src/hooks/useSortedRoutesWithQuotes.ts index a0e9d2074..981f8bdfa 100644 --- a/wormhole-connect/src/hooks/useSortedRoutesWithQuotes.ts +++ b/wormhole-connect/src/hooks/useSortedRoutesWithQuotes.ts @@ -34,6 +34,7 @@ export const useSortedRoutesWithQuotes = (): HookReturn => { destToken, preferredRouteName, } = useSelector((state: RootState) => state.transferInput); + const { toNativeToken } = useSelector((state: RootState) => state.relay); const supportedRoutes = useMemo( @@ -55,7 +56,7 @@ export const useSortedRoutesWithQuotes = (): HookReturn => { destToken, nativeGas: toNativeToken, }), - [parseFloat(amount), fromChain, token, toChain, destToken, toNativeToken], + [amount, fromChain, token, toChain, destToken, toNativeToken], ); const { quotesMap, isFetching } = useRoutesQuotesBulk( diff --git a/wormhole-connect/src/hooks/useUSDamountGetter.ts b/wormhole-connect/src/hooks/useUSDamountGetter.ts index 43126fb75..680619d08 100644 --- a/wormhole-connect/src/hooks/useUSDamountGetter.ts +++ b/wormhole-connect/src/hooks/useUSDamountGetter.ts @@ -3,10 +3,11 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from 'store'; import { getTokenPrice } from 'utils'; +import { amount as sdkAmount } from '@wormhole-foundation/sdk'; export const useUSDamountGetter = (): ((args: { token: string; - amount: string; + amount: sdkAmount.Amount; }) => number | undefined) => { const { usdPrices: { data }, @@ -15,7 +16,7 @@ export const useUSDamountGetter = (): ((args: { return useCallback( ({ token, amount }) => { const prices = data || {}; - const numericAmount = Number(amount); + const numericAmount = sdkAmount.whole(amount); const tokenPrice = Number(getTokenPrice(prices, config.tokens[token])); const USDAmount = tokenPrice * numericAmount; diff --git a/wormhole-connect/src/routes/index.ts b/wormhole-connect/src/routes/index.ts deleted file mode 100644 index fcb073fef..000000000 --- a/wormhole-connect/src/routes/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './types'; diff --git a/wormhole-connect/src/routes/operator.ts b/wormhole-connect/src/routes/operator.ts index 776ca4b16..1f2d772f4 100644 --- a/wormhole-connect/src/routes/operator.ts +++ b/wormhole-connect/src/routes/operator.ts @@ -1,7 +1,12 @@ import config from 'config'; import { TokenConfig } from 'config/types'; -import { Chain, routes, TransactionId } from '@wormhole-foundation/sdk'; +import { + Chain, + routes, + TransactionId, + amount as sdkAmount, +} from '@wormhole-foundation/sdk'; import SDKv2Route from './sdkv2'; @@ -37,7 +42,7 @@ export interface QuoteParams { sourceToken: string; destChain: Chain; destToken: string; - amount: string; + amount: sdkAmount.Amount; nativeGas: number; } @@ -245,7 +250,11 @@ class QuoteCache { } quoteParamsKey(routeName: string, params: QuoteParams): string { - return `${routeName}:${params.sourceChain}:${params.sourceToken}:${params.destChain}:${params.destToken}:${params.amount}:${params.nativeGas}`; + return `${routeName}:${params.sourceChain}:${params.sourceToken}:${ + params.destChain + }:${params.destToken}:${sdkAmount.units(params.amount)}:${ + params.nativeGas + }`; } get(routeName: string, params: QuoteParams): QuoteResult | null { diff --git a/wormhole-connect/src/routes/sdkv2/route.ts b/wormhole-connect/src/routes/sdkv2/route.ts index c4d868793..2045df97a 100644 --- a/wormhole-connect/src/routes/sdkv2/route.ts +++ b/wormhole-connect/src/routes/sdkv2/route.ts @@ -13,25 +13,13 @@ import { } from '@wormhole-foundation/sdk'; import { TokenId as TokenIdV1 } from 'sdklegacy'; import { TokenConfig } from 'config/types'; -import { - TransferDestInfo, - TransferDestInfoBaseParams, - TransferDisplayData, - TransferInfoBaseParams, -} from 'routes/types'; -import { TokenPrices } from 'store/tokenPrices'; -import { - getTokenBridgeWrappedTokenAddressSync, - TransferInfo, -} from 'utils/sdkv2'; +import { getTokenBridgeWrappedTokenAddressSync } from 'utils/sdkv2'; import { SDKv2Signer } from './signer'; -import { amount } from '@wormhole-foundation/sdk'; +import { amount as sdkAmount } from '@wormhole-foundation/sdk'; import config, { getWormholeContextV2 } from 'config'; import { - calculateUSDPrice, - getDisplayName, getGasToken, getWrappedToken, getWrappedTokenId, @@ -39,8 +27,8 @@ import { isWrappedToken, } from 'utils'; import { TransferWallet } from 'utils/wallet'; -import { RelayerFee } from 'store/relay'; -import { toFixedDecimals } from 'utils/balance'; + +type Amount = sdkAmount.Amount; // =^o^= export class SDKv2Route { @@ -79,7 +67,7 @@ export class SDKv2Route { async isRouteSupported( sourceToken: string, destToken: string, - _amount: string, // Amount is validated later, when getting a quote + _amount: Amount, // Amount is validated later, when getting a quote fromChain: Chain, toChain: Chain, ): Promise { @@ -217,7 +205,7 @@ export class SDKv2Route { } async getQuote( - amount: string, + amount: Amount, sourceTokenV1: string, destTokenV1: string, sourceChain: Chain, @@ -240,7 +228,7 @@ export class SDKv2Route { const wh = await getWormholeContextV2(); const route = new this.rc(wh); const validationResult = await route.validate(req, { - amount, + amount: sdkAmount.display(amount), options, }); @@ -254,7 +242,7 @@ export class SDKv2Route { } async createRequest( - amount: string, + amount: Amount, sourceTokenV1: string, destTokenV1: string, sourceChain: Chain, @@ -299,22 +287,18 @@ export class SDKv2Route { } async computeReceiveAmount( - amountIn: number, + amountIn: Amount, sourceToken: string, destToken: string, fromChain: Chain | undefined, toChain: Chain | undefined, options?: routes.AutomaticTokenBridgeRoute.Options, - ): Promise { - if (isNaN(amountIn)) { - return 0; - } - + ): Promise { if (!fromChain || !toChain) throw new Error('Need both chains to get a quote from SDKv2'); const [, quote] = await this.getQuote( - amountIn.toString(), + amountIn, sourceToken, destToken, fromChain, @@ -323,36 +307,14 @@ export class SDKv2Route { ); if (quote.success) { - return amount.whole(quote.destinationToken.amount); + return quote.destinationToken.amount; } else { throw quote.error; } } - async computeReceiveAmountWithFees( - amount: number, - sourceToken: string, - destToken: string, - fromChain: Chain | undefined, - toChain: Chain | undefined, - options?: routes.AutomaticTokenBridgeRoute.Options, - ): Promise { - if (!fromChain || !toChain) - throw new Error('Need both chains to get a quote from SDKv2'); - - // TODO handle fees? - return this.computeReceiveAmount( - amount, - sourceToken, - destToken, - fromChain, - toChain, - options, - ); - } - async computeQuote( - amountIn: string, + amountIn: Amount, sourceToken: string, destToken: string, fromChain: Chain, @@ -380,7 +342,7 @@ export class SDKv2Route { public validate( token: TokenIdV1 | 'native', - amount: string, + amount: Amount, sendingChain: Chain, senderAddress: string, recipientChain: Chain, @@ -402,7 +364,7 @@ export class SDKv2Route { async send( sourceToken: TokenConfig, - amount: string, + amount: Amount, fromChain: Chain, senderAddress: string, toChain: Chain, @@ -411,7 +373,7 @@ export class SDKv2Route { options?: routes.AutomaticTokenBridgeRoute.Options, ): Promise<[routes.Route, routes.Receipt]> { const [route, quote, req] = await this.getQuote( - amount.toString(), + amount, sourceToken.key, destToken, fromChain, @@ -456,152 +418,6 @@ export class SDKv2Route { throw new Error('Never got a SourceInitiated state in receipt'); } - async getPreview( - token: TokenConfig, - destToken: TokenConfig, - amount: number, - sendingChain: Chain, - recipientChain: Chain, - sendingGasEst: string, - claimingGasEst: string, - receiveAmount: string, - tokenPrices: TokenPrices, - relayerFee?: RelayerFee, - receiveNativeAmt?: number, - ): Promise { - const displayData = [ - this.createDisplayItem( - 'Amount', - amount, - destToken, - tokenPrices, - recipientChain, - ), - ]; - - if (relayerFee) { - const { fee, tokenKey } = relayerFee; - displayData.push( - this.createDisplayItem( - 'Relayer fee', - fee, - config.tokens[tokenKey], - tokenPrices, - recipientChain, - ), - ); - } - - if (receiveNativeAmt) { - const destGasToken = - config.tokens[config.chains[recipientChain]?.gasToken || '']; - displayData.push( - this.createDisplayItem( - 'Native gas on destination', - receiveNativeAmt, - destGasToken, - tokenPrices, - recipientChain, - ), - ); - } - - return displayData; - } - - createDisplayItem( - title: string, - amount: number, - token: TokenConfig, - tokenPrices: TokenPrices, - chain: Chain, - ) { - return { - title, - value: `${ - !isNaN(amount) - ? Number(toFixedDecimals(amount.toFixed(18).toString(), 6)) - : '0' - } ${getDisplayName(token, chain)}`, - valueUSD: calculateUSDPrice(amount, tokenPrices, token), - }; - } - - async getTransferSourceInfo( - params: T, - ): Promise { - const txData = params.txData as TransferInfo; - const token = config.tokens[txData.tokenKey]; - const displayData = [ - this.createDisplayItem( - 'Amount', - Number(txData.amount), - token, - params.tokenPrices, - txData.toChain, - ), - ]; - const { relayerFee, toChain } = txData; - if (relayerFee) { - displayData.push( - this.createDisplayItem( - 'Relayer fee', - relayerFee.fee, - config.tokens[relayerFee.tokenKey], - params.tokenPrices, - toChain, - ), - ); - } - return displayData; - } - - async getTransferDestInfo( - params: T, - ): Promise { - const info: TransferDestInfo = { - route: this.rc.meta.name, - displayData: [], - }; - const txData = params.txData as TransferInfo; - const token = config.tokens[txData.receivedTokenKey]; - if (txData.receiveAmount) { - info.displayData.push( - this.createDisplayItem( - 'Amount', - Number(txData.receiveAmount), - token, - params.tokenPrices, - txData.toChain, - ), - ); - } - if (txData.receiveNativeAmount && txData.receiveNativeAmount > 0) { - info.displayData.push( - this.createDisplayItem( - 'Native gas amount', - Number(txData.receiveNativeAmount.toFixed(6)), - config.tokens[config.chains[txData.toChain]?.gasToken || ''], - params.tokenPrices, - txData.toChain, - ), - ); - } - return info; - } - - async getForeignAsset( - token: TokenIdV1, - chain: Chain, - destToken?: TokenConfig | undefined, - ): Promise { - return 'test'; - } - - tryFetchRedeemTx(txData: TransferInfo): Promise { - throw new Error('Method not implemented.'); - } - async resumeIfManual(tx: TransactionId): Promise { const wh = await getWormholeContextV2(); const route = new this.rc(wh); diff --git a/wormhole-connect/src/routes/types.ts b/wormhole-connect/src/routes/types.ts index fb336ad95..e69de29bb 100644 --- a/wormhole-connect/src/routes/types.ts +++ b/wormhole-connect/src/routes/types.ts @@ -1,37 +0,0 @@ -import { TransferInfo } from '../utils/sdkv2'; -import { TokenPrices } from 'store/tokenPrices'; -import { TokenId } from 'sdklegacy'; - -export interface TransferInfoBaseParams { - txData: TransferInfo; - tokenPrices: TokenPrices; -} - -export interface TransferDestInfoBaseParams { - txData: TransferInfo; - tokenPrices: TokenPrices; - receiveTx?: string; - gasEstimate?: string; -} - -export type Row = { - title: string; - value: string; - valueUSD?: string; -}; - -export interface NestedRow extends Row { - rows?: Row[]; -} - -export type TransferDestInfo = { - route: string; - displayData: TransferDisplayData; -}; - -export type TransferDisplayData = NestedRow[]; - -export interface RelayerFee { - fee: bigint; - feeToken: TokenId | 'native'; -} diff --git a/wormhole-connect/src/store/redeem.ts b/wormhole-connect/src/store/redeem.ts index b4a30a0dc..7f0739ec4 100644 --- a/wormhole-connect/src/store/redeem.ts +++ b/wormhole-connect/src/store/redeem.ts @@ -1,6 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { TransferInfo } from 'utils/sdkv2'; -import { TransferDestInfo } from 'routes'; export enum MessageType { BRIDGE = 1, @@ -15,7 +14,6 @@ export interface RedeemState { isVaaEnqueued: boolean; isInvalidVaa: boolean; route?: string; - transferDestInfo: TransferDestInfo | undefined; isResumeTx: boolean; timestamp: number; } @@ -28,7 +26,6 @@ const initialState: RedeemState = { isVaaEnqueued: false, isInvalidVaa: false, route: undefined, - transferDestInfo: undefined, isResumeTx: false, timestamp: 0, }; @@ -64,12 +61,6 @@ export const redeemSlice = createSlice({ ) => { state.isVaaEnqueued = payload; }, - setTransferDestInfo: ( - state: RedeemState, - { payload }: PayloadAction, - ) => { - state.transferDestInfo = payload; - }, clearRedeem: (state: RedeemState) => { Object.keys(state).forEach((key) => { // @ts-ignore @@ -101,7 +92,6 @@ export const { setTransferComplete, setIsVaaEnqueued, setInvalidVaa, - setTransferDestInfo, clearRedeem, setRoute, setIsResumeTx, diff --git a/wormhole-connect/src/store/transferInput.ts b/wormhole-connect/src/store/transferInput.ts index c744c8156..76a8874dd 100644 --- a/wormhole-connect/src/store/transferInput.ts +++ b/wormhole-connect/src/store/transferInput.ts @@ -15,11 +15,12 @@ import { getEmptyDataWrapper, receiveDataWrapper, } from './helpers'; -import { Chain } from '@wormhole-foundation/sdk'; +import { getTokenDecimals } from 'utils'; +import { Chain, amount } from '@wormhole-foundation/sdk'; export type Balance = { lastUpdated: number; - balance: string | null; + balance: amount.Amount | null; }; export type Balances = { [key: string]: Balance }; export type ChainBalances = { @@ -29,13 +30,6 @@ export type BalancesCache = { [key in Chain]?: ChainBalances }; type WalletAddress = string; export type WalletBalances = { [key: WalletAddress]: BalancesCache }; -export const formatStringAmount = (amountStr = '0'): string => { - const amountNum = parseFloat(amountStr); - return amountNum.toLocaleString('en', { - maximumFractionDigits: 4, - }); -}; - // for use in USDC or other tokens that have versions on many chains // returns token key export const getNativeVersionOfToken = ( @@ -102,7 +96,7 @@ export interface TransferInputState { toChain: Chain | undefined; token: string; destToken: string; - amount: string; + amount?: amount.Amount; receiveAmount: DataWrapper; route?: string; preferredRouteName?: string | undefined; @@ -140,7 +134,7 @@ function getInitialState(): TransferInputState { toChain: config.ui.defaultInputs?.toChain || undefined, token: config.ui.defaultInputs?.tokenKey || '', destToken: config.ui.defaultInputs?.toTokenKey || '', - amount: '', + amount: undefined, receiveAmount: getEmptyDataWrapper(), preferredRouteName: config.ui.defaultInputs?.preferredRouteName, route: undefined, @@ -168,7 +162,7 @@ const performModificationsIfFromChainChanged = (state: TransferInputState) => { (!tokenConfig.tokenId && tokenConfig.nativeChain !== fromChain) ) { state.token = ''; - state.amount = ''; + state.amount = undefined; } if ( tokenConfig.symbol === 'USDC' && @@ -272,7 +266,18 @@ export const transferInputSlice = createSlice({ state: TransferInputState, { payload }: PayloadAction, ) => { - state.amount = payload; + if (state.token && state.fromChain) { + const tokenConfig = config.tokens[state.token]; + const decimals = getTokenDecimals(state.fromChain, tokenConfig); + const parsed = amount.parse(payload, decimals); + if (amount.units(parsed) === 0n) { + state.amount = undefined; + } else { + state.amount = parsed; + } + } else { + console.warn(`Can't call setAmount without a fromChain and token`); + } }, setReceiveAmount: ( state: TransferInputState, diff --git a/wormhole-connect/src/telemetry/index.ts b/wormhole-connect/src/telemetry/index.ts index 2de9d8ccf..86a688c77 100644 --- a/wormhole-connect/src/telemetry/index.ts +++ b/wormhole-connect/src/telemetry/index.ts @@ -1,7 +1,7 @@ import config from 'config'; import { TokenDetails, TransferDetails } from './types'; -import { Chain } from '@wormhole-foundation/sdk'; +import { Chain, amount as sdkAmount } from '@wormhole-foundation/sdk'; export function getTokenDetails(token: string): TokenDetails { const tokenConfig = config.tokens[token]!; @@ -19,8 +19,11 @@ export function getTransferDetails( destToken: string, sourceChain: Chain, destChain: Chain, - amount: string, - getUSDAmount: (args: { token: string; amount: string }) => number | undefined, + amount: sdkAmount.Amount, + getUSDAmount: (args: { + token: string; + amount: sdkAmount.Amount; + }) => number | undefined, ): TransferDetails { return { route, @@ -28,7 +31,7 @@ export function getTransferDetails( toToken: getTokenDetails(destToken), fromChain: sourceChain, toChain: destChain, - amount: Number(amount), + amount, USDAmount: getUSDAmount({ token: sourceToken, amount }), }; } diff --git a/wormhole-connect/src/telemetry/types.ts b/wormhole-connect/src/telemetry/types.ts index 84a7bc7e2..fea040eb4 100644 --- a/wormhole-connect/src/telemetry/types.ts +++ b/wormhole-connect/src/telemetry/types.ts @@ -1,4 +1,4 @@ -import { Chain } from '@wormhole-foundation/sdk'; +import { Chain, amount as sdkAmount } from '@wormhole-foundation/sdk'; import { WormholeConnectConfig } from 'config/types'; import { TransferWallet } from 'utils/wallet'; @@ -15,7 +15,7 @@ export interface TransferDetails { toChain: Chain; txId?: string; USDAmount?: number; - amount?: number; + amount?: sdkAmount.Amount; } export type TransferEventType = diff --git a/wormhole-connect/src/utils/amount.ts b/wormhole-connect/src/utils/amount.ts deleted file mode 100644 index 91f10915b..000000000 --- a/wormhole-connect/src/utils/amount.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getTokenDecimals } from 'utils'; -import { amount, Chain } from '@wormhole-foundation/sdk'; -import { TokenConfig } from 'config/types'; - -export const formatAmount = ( - chain: Chain, - token: TokenConfig, - val: string | bigint | null, - truncate?: number, -): string | null => { - if (!val) { - return null; - } - - const decimals = getTokenDecimals(chain, token.tokenId); - - let balanceAmount = amount.fromBaseUnits(BigInt(val), decimals); - - if (truncate) { - balanceAmount = amount.truncate(balanceAmount, truncate); - } - - return amount.display(balanceAmount); -}; diff --git a/wormhole-connect/src/utils/index.ts b/wormhole-connect/src/utils/index.ts index d74cf57f6..64cfaf9dd 100644 --- a/wormhole-connect/src/utils/index.ts +++ b/wormhole-connect/src/utils/index.ts @@ -7,7 +7,11 @@ import config from 'config'; import { ChainConfig, TokenConfig } from 'config/types'; import { isGatewayChain } from './cosmos'; import { TokenPrices } from 'store/tokenPrices'; -import { Chain, chainToPlatform } from '@wormhole-foundation/sdk'; +import { + Chain, + chainToPlatform, + amount as sdkAmount, +} from '@wormhole-foundation/sdk'; import { getNativeVersionOfToken } from 'store/transferInput'; export const MAX_DECIMALS = 6; @@ -135,24 +139,18 @@ export function getGasToken(chain: Chain): TokenConfig { return gasToken; } -export function getTokenDecimals( - chain: Chain, - tokenId: TokenId | 'native' = 'native', -): number { +export function getTokenDecimals(chain: Chain, token: TokenConfig): number { const chainConfig = config.chains[chain]; if (!chainConfig) throw new Error(`chain config for ${chain} not found`); - if (tokenId === 'native') { + /* + if (token?.tokenId === 'native') { const { decimals } = getGasToken(chain); return decimals; } + */ - const tokenConfig = getTokenById(tokenId); - if (!tokenConfig) { - throw new Error('token config not found'); - } - - const { nativeChain, decimals } = tokenConfig; + const { nativeChain, decimals } = token; const platform = chainToPlatform(chain); const tokenPlatform = chainToPlatform(nativeChain); @@ -308,27 +306,26 @@ export const getUSDFormat = (price: number | undefined): string => { }; export const calculateUSDPriceRaw = ( - amount?: number | string, + amount?: sdkAmount.Amount | number, tokenPrices?: TokenPrices | null, token?: TokenConfig, ): number | undefined => { - if ( - typeof amount === 'undefined' || - amount === '' || - !tokenPrices || - !token - ) { + if (typeof amount === 'undefined' || !tokenPrices || !token) { return undefined; } const usdPrice = getTokenPrice(tokenPrices || {}, token) || 0; if (usdPrice > 0) { - return Number.parseFloat(`${amount}`) * usdPrice; + if (typeof amount === 'number') { + return amount * usdPrice; + } else { + return sdkAmount.whole(amount) * usdPrice; + } } }; export const calculateUSDPrice = ( - amount?: number | string, + amount?: sdkAmount.Amount | number, tokenPrices?: TokenPrices | null, token?: TokenConfig, ): string => { diff --git a/wormhole-connect/src/utils/sdkv2.ts b/wormhole-connect/src/utils/sdkv2.ts index 492b054fb..af71fa85b 100644 --- a/wormhole-connect/src/utils/sdkv2.ts +++ b/wormhole-connect/src/utils/sdkv2.ts @@ -21,7 +21,7 @@ import { NttRoute } from '@wormhole-foundation/sdk-route-ntt'; import { Connection } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js'; import * as splToken from '@solana/spl-token'; -import { getTokenDecimals, getWrappedTokenId } from 'utils'; +import { getTokenDecimals, getWrappedToken } from 'utils'; import { WORMSCAN } from 'config/constants'; // Used to represent an initiated transfer. Primarily for the Redeem view. @@ -33,7 +33,7 @@ export interface TransferInfo { sender?: string; recipient: string; - amount: string; + amount: amount.Amount; toChain: Chain; fromChain: Chain; @@ -45,12 +45,11 @@ export interface TransferInfo { // Destination token receivedTokenKey: string; - receiveAmount?: string; + receiveAmount?: amount.Amount; relayerFee?: RelayerFee; // Amount of native gas being received, in destination gas token units - // For example 1.0 is 1.0 ETH, not 1 wei - receiveNativeAmount?: number; + receiveNativeAmount?: amount.Amount; // ETA for the route this transfer was initiated on eta?: number; @@ -234,22 +233,23 @@ const parseTokenBridgeReceipt = async ( const fromChain = receipt.from; - const decimals = getTokenDecimals(fromChain, getWrappedTokenId(tokenV1)); + const decimals = getTokenDecimals(fromChain, getWrappedToken(tokenV1)); txData.tokenDecimals = decimals; - txData.amount = amount.display({ - amount: payload.token.amount.toString(), + txData.amount = amount.fromBaseUnits( + payload.token.amount, // VAAs are truncated to a max of 8 decimal places - decimals: Math.min(8, decimals), - }); + Math.min(8, decimals), + ); txData.tokenAddress = tokenAddress; txData.tokenKey = tokenV1.key; txData.receivedTokenKey = tokenV1.key; txData.receiveAmount = txData.amount; if (payload.payload?.toNativeTokenAmount) { - txData.receiveNativeAmount = Number( - amount.fmt(payload.payload.toNativeTokenAmount, Math.min(8, decimals)), + txData.receiveNativeAmount = amount.fromBaseUnits( + payload.payload.toNativeTokenAmount, + Math.min(8, decimals), ); } if (payload.payload?.targetRelayerFee) { @@ -323,16 +323,10 @@ const parseCCTPReceipt = async ( txData.tokenAddress = sourceTokenId.address.toString(); txData.tokenKey = usdcLegacy.key; - const decimals = getTokenDecimals( - receipt.from, - getWrappedTokenId(usdcLegacy), - ); + const decimals = getTokenDecimals(receipt.from, getWrappedToken(usdcLegacy)); txData.tokenDecimals = decimals; - txData.amount = amount.display({ - amount: payload.amount.toString(), - decimals, - }); + txData.amount = amount.fromBaseUnits(payload.amount, decimals); txData.receiveAmount = txData.amount; txData.sender = payload.messageSender.toNative(receipt.from).toString(); @@ -414,10 +408,10 @@ const parseNttReceipt = ( ? attestation : attestation.payload; const { trimmedAmount } = payload.nttManagerPayload.payload; - const amt = amount.display({ - amount: trimmedAmount.amount.toString(), - decimals: trimmedAmount.decimals, - }); + const amt = amount.fromBaseUnits( + trimmedAmount.amount, + trimmedAmount.decimals, + ); return { toChain: receipt.to, fromChain: receipt.from, diff --git a/wormhole-connect/src/utils/transferValidation.ts b/wormhole-connect/src/utils/transferValidation.ts index 25e3eac97..c8c412760 100644 --- a/wormhole-connect/src/utils/transferValidation.ts +++ b/wormhole-connect/src/utils/transferValidation.ts @@ -18,7 +18,7 @@ import { walletAcceptedChains } from './wallet'; import { useDispatch, useSelector } from 'react-redux'; import { useDebounce } from 'use-debounce'; import { DataWrapper } from 'store/helpers'; -import { Chain } from '@wormhole-foundation/sdk'; +import { Chain, amount as sdkAmount } from '@wormhole-foundation/sdk'; export const validateFromChain = (chain: Chain | undefined): ValidationErr => { if (!chain) return 'Select a source chain'; @@ -91,20 +91,23 @@ export const validateDestToken = ( }; export const validateAmount = ( - amount: string, - balance: string | null, - maxAmount: number, + amount: sdkAmount.Amount | undefined, + balance: sdkAmount.Amount | null, ): ValidationErr => { - if (amount === '') return ''; - const numAmount = Number.parseFloat(amount); - if (isNaN(numAmount)) return 'Amount must be a number'; - if (numAmount <= 0) return 'Amount must be greater than 0'; - if (balance) { - const b = Number.parseFloat(balance.replaceAll(',', '')); - if (numAmount > b) return 'Amount exceeds available balance.'; + if (!amount) return ''; + + // If user has selected chain, token, and has a balance entry, we can compare + // their amount input to their balance (using base units) + const amountBaseUnits = sdkAmount.units(amount); + if (amountBaseUnits === 0n) { + return 'Amount must be greater than 0'; } - if (numAmount > maxAmount) { - return `At the moment, amount cannot exceed ${maxAmount}`; + + if (balance) { + const balanceBaseUnits = sdkAmount.units(balance); + if (amountBaseUnits > balanceBaseUnits) { + return 'Amount exceeds available balance'; + } } return ''; }; @@ -159,13 +162,6 @@ export const validateReceiveAmount = ( return ''; }; -export const getMaxAmt = (route?: string): number => { - if (!route) return Infinity; - const r = config.routes.get(route); - if (!r) return Infinity; - return r.getMaxSendAmount(); -}; - export const getIsAutomatic = (route?: string): boolean => { if (!route) return false; const r = config.routes.get(route); @@ -197,7 +193,6 @@ export const validateAll = async ( fromChain, token, ); - const maxSendAmount = getMaxAmt(route); const baseValidations = { sendingWallet: await validateWallet(sending, fromChain), receivingWallet: await validateWallet(receiving, toChain), @@ -205,11 +200,7 @@ export const validateAll = async ( toChain: validateToChain(toChain, fromChain), token: validateToken(token, fromChain), destToken: validateDestToken(destToken, toChain, supportedDestTokens), - amount: validateAmount( - amount, - sendingTokenBalance?.balance || null, - maxSendAmount, - ), + amount: validateAmount(amount, sendingTokenBalance?.balance || null), toNativeToken: '', relayerFee: '', receiveAmount: '', @@ -262,7 +253,6 @@ export const validate = async ( transferInput.token && transferInput.destToken && transferInput.amount && - Number.parseFloat(transferInput.amount) >= 0 && transferInput.routeStates?.some((rs) => rs.supported) !== undefined ? true : false; diff --git a/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx b/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx index a5b3ca3af..fcbb86adb 100644 --- a/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx +++ b/wormhole-connect/src/views/v2/Bridge/AmountInput/index.tsx @@ -19,6 +19,7 @@ import InputAdornment from '@mui/material/InputAdornment'; import Stack from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; +import { amount as sdkAmount } from '@wormhole-foundation/sdk'; import AlertBannerV2 from 'components/v2/AlertBanner'; import useGetTokenBalances from 'hooks/useGetTokenBalances'; @@ -110,12 +111,15 @@ const AmountInput = (props: Props) => { const { sending: sendingWallet } = useSelector( (state: RootState) => state.wallet, ); + const { amount } = useSelector((state: RootState) => state.transferInput); - const { - fromChain: sourceChain, - token: sourceToken, - amount, - } = useSelector((state: RootState) => state.transferInput); + const [amountInput, setAmountInput] = useState( + amount ? sdkAmount.display(amount) : '', + ); + + const { fromChain: sourceChain, token: sourceToken } = useSelector( + (state: RootState) => state.transferInput, + ); const { balances, isFetching } = useGetTokenBalances( sendingWallet?.address || '', @@ -124,7 +128,7 @@ const AmountInput = (props: Props) => { ); const tokenBalance = useMemo( - () => balances?.[sourceToken]?.balance || '0', + () => balances?.[sourceToken]?.balance || null, [balances, sourceToken], ); @@ -151,21 +155,25 @@ const AmountInput = (props: Props) => { {isFetching ? ( ) : ( - // TODO AMOUNT HACK... fix amount formatting in amount.Amount balance refactor - {parseFloat(tokenBalance).toLocaleString('en', { - maximumFractionDigits: 6, - })} + {tokenBalance === null + ? '0' + : sdkAmount.display(sdkAmount.truncate(tokenBalance, 6))} )} ); }, [isInputDisabled, balances, tokenBalance, sendingWallet.address]); + const handleChange = useCallback((newValue: string): void => { + dispatch(setAmount(newValue)); + setAmountInput(newValue); + }, []); + const maxButton = useMemo(() => { return (