From 660e448198b0c3fe5cd2aee19f48ea5f1e5a375f Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 14 Jan 2025 16:49:50 +1000 Subject: [PATCH 1/4] feat: Fee loading timeout model --- apps/canonical-bridge-ui/core/env/index.ts | 1 + .../pages/mainnet/index.tsx | 1 + .../src/CanonicalBridgeProvider.tsx | 1 + .../src/core/locales/en.ts | 4 + .../src/core/utils/string.ts | 8 + .../src/core/utils/time.ts | 2 + .../src/modules/transfer/action.ts | 4 + .../Modal/FeeTimeoutModal/index.tsx | 26 ++++ .../components/TransferButtonGroup/index.tsx | 5 + .../transfer/hooks/modal/useFeeLoadTimeout.ts | 20 +++ .../transfer/hooks/useLoadingBridgeFees.ts | 147 +++++++++++------- .../src/modules/transfer/reducer.ts | 6 + 12 files changed, 171 insertions(+), 54 deletions(-) create mode 100644 packages/canonical-bridge-widget/src/modules/transfer/components/Modal/FeeTimeoutModal/index.tsx create mode 100644 packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFeeLoadTimeout.ts diff --git a/apps/canonical-bridge-ui/core/env/index.ts b/apps/canonical-bridge-ui/core/env/index.ts index c1c97c10..19b06f90 100644 --- a/apps/canonical-bridge-ui/core/env/index.ts +++ b/apps/canonical-bridge-ui/core/env/index.ts @@ -5,4 +5,5 @@ export const env = { SERVER_ENDPOINT: process.env.NEXT_PUBLIC_SERVER_ENDPOINT ?? '', WALLET_CONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? '', DEBRIDGE_REFERRAL_CODE: process.env.NEXT_PUBLIC_DEBRIDGE_REFERRAL_CODE ?? '', + FEE_RELOAD_MAX_TIME: process.env.NEXT_PUBLIC_FEE_RELOAD_MAX_TIME ?? 15000, }; diff --git a/apps/canonical-bridge-ui/pages/mainnet/index.tsx b/apps/canonical-bridge-ui/pages/mainnet/index.tsx index 5934c42e..c0f80188 100644 --- a/apps/canonical-bridge-ui/pages/mainnet/index.tsx +++ b/apps/canonical-bridge-ui/pages/mainnet/index.tsx @@ -39,6 +39,7 @@ function BridgeWidget() { http: { serverEndpoint: env.SERVER_ENDPOINT, deBridgeReferralCode: env.DEBRIDGE_REFERRAL_CODE, + feeReloadMaxTime: env.FEE_RELOAD_MAX_TIME, }, transfer: transferConfig, onClickConnectWalletButton: onOpen, diff --git a/packages/canonical-bridge-widget/src/CanonicalBridgeProvider.tsx b/packages/canonical-bridge-widget/src/CanonicalBridgeProvider.tsx index 95aade99..7f63a8d7 100644 --- a/packages/canonical-bridge-widget/src/CanonicalBridgeProvider.tsx +++ b/packages/canonical-bridge-widget/src/CanonicalBridgeProvider.tsx @@ -45,6 +45,7 @@ export interface IBridgeConfig { http: { refetchingInterval: number; apiTimeOut: number; + feeReloadMaxTime?: number; deBridgeAccessToken?: string; deBridgeReferralCode?: string; serverEndpoint?: string; diff --git a/packages/canonical-bridge-widget/src/core/locales/en.ts b/packages/canonical-bridge-widget/src/core/locales/en.ts index 7e8498df..48fd9f2a 100644 --- a/packages/canonical-bridge-widget/src/core/locales/en.ts +++ b/packages/canonical-bridge-widget/src/core/locales/en.ts @@ -99,6 +99,10 @@ export const en = { 'We’ve encountered an unknown issue on this route. Please try again later.', 'modal.quote.error.button.close': 'OK', + 'modal.fee-timeout.error.title': 'Failed to Find the Route!', + 'modal.fee-timeout.error.desc': 'We’ve encountered an unknown issue. Please try again later.', + 'modal.fee-timeout.error.button.close': 'OK', + 'select-modal.tag.incompatible': 'Incompatible', 'select-modal.search.no-result.title': 'No result found', 'select-modal.search.no-result.warning': diff --git a/packages/canonical-bridge-widget/src/core/utils/string.ts b/packages/canonical-bridge-widget/src/core/utils/string.ts index 2ca90979..12d04618 100644 --- a/packages/canonical-bridge-widget/src/core/utils/string.ts +++ b/packages/canonical-bridge-widget/src/core/utils/string.ts @@ -75,3 +75,11 @@ export function capitalizeFirst(text: string) { if (typeof text !== 'string') return ''; return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); } + +export function checkResponseResult(response: any) { + return response.every( + (route: any) => + (route.status === 'rejected' && route?.reason?.message?.includes('timeout')) || + (route.status === 'fulfilled' && route.value === null), + ); +} diff --git a/packages/canonical-bridge-widget/src/core/utils/time.ts b/packages/canonical-bridge-widget/src/core/utils/time.ts index dba680c5..a9fd4041 100644 --- a/packages/canonical-bridge-widget/src/core/utils/time.ts +++ b/packages/canonical-bridge-widget/src/core/utils/time.ts @@ -14,3 +14,5 @@ export function formatEstimatedTime(value?: string | number) { return `${finalMinutes}${finalMinutes > 1 ? ' mins' : ' min'}`; } + +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/canonical-bridge-widget/src/modules/transfer/action.ts b/packages/canonical-bridge-widget/src/modules/transfer/action.ts index be800b09..ca81f40f 100644 --- a/packages/canonical-bridge-widget/src/modules/transfer/action.ts +++ b/packages/canonical-bridge-widget/src/modules/transfer/action.ts @@ -61,6 +61,10 @@ export const setIsSummaryModalOpen = createAction( + 'transfer/setIsFeeTimeoutModalOpen', +); + export const setRefreshAnimationProgress = createAction( 'transfer/setRefreshAnimationProgress', ); diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/FeeTimeoutModal/index.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/FeeTimeoutModal/index.tsx new file mode 100644 index 00000000..6d276cf6 --- /dev/null +++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/FeeTimeoutModal/index.tsx @@ -0,0 +1,26 @@ +import { useIntl } from '@bnb-chain/space'; + +import { StateModal, StateModalProps } from '@/core/components/StateModal'; + +export const FeeTimeoutModal = (props: Omit) => { + const { ...restProps } = props; + const { formatMessage } = useIntl(); + + return ( + + ); +}; diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/TransferButtonGroup/index.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/TransferButtonGroup/index.tsx index 809f334c..21fbe7cd 100644 --- a/packages/canonical-bridge-widget/src/modules/transfer/components/TransferButtonGroup/index.tsx +++ b/packages/canonical-bridge-widget/src/modules/transfer/components/TransferButtonGroup/index.tsx @@ -11,9 +11,11 @@ import { TransactionSummaryModal } from '@/modules/transfer/components/Modal/Tra import { TransferWarningMessage } from '@/modules/transfer/components/TransferWarningMessage'; import { MIN_SOL_TO_ENABLED_TX } from '@/core/constants'; import { FailedToGetQuoteModal } from '@/modules/transfer/components/Modal/FailedToGetQuoteModal'; +import { FeeTimeoutModal } from '@/modules/transfer/components/Modal/FeeTimeoutModal'; import { useFailGetQuoteModal } from '@/modules/transfer/hooks/modal/useFailGetQuoteModal'; import { useAppSelector } from '@/modules/store/StoreProvider'; import { useSummaryModal } from '@/modules/transfer/hooks/modal/useSummaryModal'; +import { useFeeLoadTimeout } from '@/modules/transfer/hooks/modal/useFeeLoadTimeout'; export const TransferButtonGroup = () => { const [hash, setHash] = useState(null); @@ -23,6 +25,7 @@ export const TransferButtonGroup = () => { const isFailedGetQuoteModalOpen = useAppSelector( (state) => state.transfer.isFailedGetQuoteModalOpen, ); + const isFeeTimeoutModalOpen = useAppSelector((state) => state.transfer.isFeeTimeoutModalOpen); const isSummaryModalOpen = useAppSelector((state) => state.transfer.isSummaryModalOpen); const { @@ -46,6 +49,7 @@ export const TransferButtonGroup = () => { onClose: onCloseConfirmingModal, } = useDisclosure(); const { onCloseFailedGetQuoteModal } = useFailGetQuoteModal(); + const { onCloseFeeTimeoutModal } = useFeeLoadTimeout(); const { onCloseSummaryModal, onOpenSummaryModal } = useSummaryModal(); return ( <> @@ -99,6 +103,7 @@ export const TransferButtonGroup = () => { isOpen={isFailedGetQuoteModalOpen} onClose={onCloseFailedGetQuoteModal} /> + ); }; diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFeeLoadTimeout.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFeeLoadTimeout.ts new file mode 100644 index 00000000..4aeba9c2 --- /dev/null +++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFeeLoadTimeout.ts @@ -0,0 +1,20 @@ +import { useCallback } from 'react'; + +import { useAppDispatch } from '@/modules/store/StoreProvider'; +import { setIsFeeTimeoutModalOpen } from '@/modules/transfer/action'; + +export const useFeeLoadTimeout = () => { + const dispatch = useAppDispatch(); + + const onOpenFeeTimeoutModal = useCallback(() => { + dispatch(setIsFeeTimeoutModalOpen(true)); + }, [dispatch]); + + const onCloseFeeTimeoutModal = useCallback(() => { + dispatch(setIsFeeTimeoutModalOpen(false)); + }, [dispatch]); + return { + onOpenFeeTimeoutModal, + onCloseFeeTimeoutModal, + }; +}; diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useLoadingBridgeFees.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useLoadingBridgeFees.ts index 7516a5d3..f8f3eb80 100644 --- a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useLoadingBridgeFees.ts +++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useLoadingBridgeFees.ts @@ -21,7 +21,7 @@ import { DEFAULT_SOLANA_ADDRESS, DEFAULT_TRON_ADDRESS, } from '@/core/constants'; -import { toObject } from '@/core/utils/string'; +import { checkResponseResult, toObject } from '@/core/utils/string'; import { useGetCBridgeFees } from '@/modules/aggregator/adapters/cBridge/hooks/useGetCBridgeFees'; import { useGetDeBridgeFees } from '@/modules/aggregator/adapters/deBridge/hooks/useGetDeBridgeFees'; import { useGetStargateFees } from '@/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees'; @@ -36,6 +36,8 @@ import { useSolanaAccount } from '@/modules/wallet/hooks/useSolanaAccount'; import { useSolanaTransferInfo } from '@/modules/transfer/hooks/solana/useSolanaTransferInfo'; import { useIsWalletCompatible } from '@/modules/wallet/hooks/useIsWalletCompatible'; import { useFailGetQuoteModal } from '@/modules/transfer/hooks/modal/useFailGetQuoteModal'; +import { delay } from '@/core/utils/time'; +import { useFeeLoadTimeout } from '@/modules/transfer/hooks/modal/useFeeLoadTimeout'; let lastTime = Date.now(); @@ -54,7 +56,7 @@ export const useLoadingBridgeFees = () => { const bridgeSDK = useBridgeSDK(); const { - http: { deBridgeAccessToken, deBridgeReferralCode }, + http: { deBridgeAccessToken, deBridgeReferralCode, feeReloadMaxTime = 15000 }, } = useBridgeConfig(); const nativeToken = useGetNativeToken(); const { deBridgeFeeSorting: _deBridgeFeeSorting } = useGetDeBridgeFees(); @@ -80,6 +82,7 @@ export const useLoadingBridgeFees = () => { const { mesonFeeSorting: _mesonFeeSorting } = useGetMesonFees(); const { onOpenFailedGetQuoteModal } = useFailGetQuoteModal(); + const { onOpenFeeTimeoutModal } = useFeeLoadTimeout(); const mesonFeeSorting = useRef(_mesonFeeSorting); mesonFeeSorting.current = _mesonFeeSorting; @@ -143,61 +146,66 @@ export const useLoadingBridgeFees = () => { } }); try { + const loadFeeCoreFn = async () => { + return await bridgeSDK.loadBridgeFees({ + bridgeType: bridgeTypeList, + fromChainId: fromChain.id, + fromAccount: address || DEFAULT_ADDRESS, + toChainId: toChain?.id, + toToken, + sendValue: amount, + fromTokenSymbol: selectedToken.symbol, + publicClient, + endPointId: { + layerZeroV1: toToken?.layerZero?.raw?.endpointID, + layerZeroV2: toToken?.stargate?.raw?.endpointID, + }, + bridgeAddress: { + stargate: selectedToken?.stargate?.raw?.address as `0x${string}`, + layerZero: selectedToken?.layerZero?.raw?.bridgeAddress as `0x${string}`, + }, + isPegged: selectedToken?.isPegged, + slippage: max_slippage, + mesonOpts: { + fromToken: `${fromChain?.meson?.raw?.id}:${selectedToken?.meson?.raw?.id}`, + toToken: `${toChain?.meson?.raw?.id}:${toToken?.meson?.raw?.id}`, + amount: debouncedSendValue, + fromAddr: + fromChain?.chainType === 'tron' + ? tronAddress ?? DEFAULT_TRON_ADDRESS + : address ?? DEFAULT_ADDRESS, + }, + deBridgeOpts: { + fromChainId: fromChain.id, + fromTokenAddress: selectedToken.deBridge?.raw?.address as `0x${string}`, + amount, + toChainId: toChain?.id, + toTokenAddress: toToken?.deBridge?.raw?.address as `0x${string}`, + accesstoken: deBridgeAccessToken, + referralCode: deBridgeReferralCode, + userAddress: + fromChain.chainType === 'solana' + ? solanaAddress || DEFAULT_SOLANA_ADDRESS + : address || DEFAULT_ADDRESS, + toUserAddress: + fromChain.chainType === 'solana' + ? isSolanaAvailableToAccount + ? toAccountRef.current + : DEFAULT_ADDRESS + : toChain.chainType === 'solana' + ? isSolanaAvailableToAccount + ? toAccountRef.current + : DEFAULT_SOLANA_ADDRESS + : undefined, + }, + }); + }; + const amount = parseUnits(debouncedSendValue, selectedToken.decimals); const now = Date.now(); lastTime = now; - const response = await bridgeSDK.loadBridgeFees({ - bridgeType: bridgeTypeList, - fromChainId: fromChain.id, - fromAccount: address || DEFAULT_ADDRESS, - toChainId: toChain?.id, - toToken, - sendValue: amount, - fromTokenSymbol: selectedToken.symbol, - publicClient, - endPointId: { - layerZeroV1: toToken?.layerZero?.raw?.endpointID, - layerZeroV2: toToken?.stargate?.raw?.endpointID, - }, - bridgeAddress: { - stargate: selectedToken?.stargate?.raw?.address as `0x${string}`, - layerZero: selectedToken?.layerZero?.raw?.bridgeAddress as `0x${string}`, - }, - isPegged: selectedToken?.isPegged, - slippage: max_slippage, - mesonOpts: { - fromToken: `${fromChain?.meson?.raw?.id}:${selectedToken?.meson?.raw?.id}`, - toToken: `${toChain?.meson?.raw?.id}:${toToken?.meson?.raw?.id}`, - amount: debouncedSendValue, - fromAddr: - fromChain?.chainType === 'tron' - ? tronAddress ?? DEFAULT_TRON_ADDRESS - : address ?? DEFAULT_ADDRESS, - }, - deBridgeOpts: { - fromChainId: fromChain.id, - fromTokenAddress: selectedToken.deBridge?.raw?.address as `0x${string}`, - amount, - toChainId: toChain?.id, - toTokenAddress: toToken?.deBridge?.raw?.address as `0x${string}`, - accesstoken: deBridgeAccessToken, - referralCode: deBridgeReferralCode, - userAddress: - fromChain.chainType === 'solana' - ? solanaAddress || DEFAULT_SOLANA_ADDRESS - : address || DEFAULT_ADDRESS, - toUserAddress: - fromChain.chainType === 'solana' - ? isSolanaAvailableToAccount - ? toAccountRef.current - : DEFAULT_ADDRESS - : toChain.chainType === 'solana' - ? isSolanaAvailableToAccount - ? toAccountRef.current - : DEFAULT_SOLANA_ADDRESS - : undefined, - }, - }); + let response = undefined; + response = await loadFeeCoreFn(); // eslint-disable-next-line no-console console.log( 'API response deBridge[0], cBridge[1], stargate[2], layerZero[3], meson[4]', @@ -207,6 +215,26 @@ export const useLoadingBridgeFees = () => { return; } + const allFailedOrNull = checkResponseResult(response); + + // if all API return null or timeout, retry fee loading + if (allFailedOrNull) { + const startLoadingTime = Date.now(); + while (true) { + // eslint-disable-next-line + console.log(`reload start after ${Date.now() - startLoadingTime} seconds`); + response = await loadFeeCoreFn(); + const allFailedOrNull = checkResponseResult(response); + if (!allFailedOrNull) break; + if (Date.now() - startLoadingTime >= feeReloadMaxTime) { + onOpenFeeTimeoutModal(); + throw new Error(`Exceeded maximum retry time of ${feeReloadMaxTime / 1000} seconds`); + } + // Wait a bit before retrying + await delay(2000); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [debridgeEst, cbridgeEst, stargateEst, layerZeroEst, mesonEst] = response as any; // meson @@ -458,6 +486,15 @@ export const useLoadingBridgeFees = () => { // eslint-disable-next-line no-console console.log(error, error.message); dispatch(setIsGlobalFeeLoading(false)); + dispatch( + setEstimatedAmount({ + deBridge: undefined, + cBridge: undefined, + stargate: undefined, + layerZero: undefined, + meson: undefined, + }), + ); } }, [ @@ -485,6 +522,8 @@ export const useLoadingBridgeFees = () => { nativeToken, preSelectRoute, onOpenFailedGetQuoteModal, + onOpenFeeTimeoutModal, + feeReloadMaxTime, ], ); diff --git a/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts b/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts index 743e16e3..e4a0e746 100644 --- a/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts +++ b/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts @@ -32,6 +32,7 @@ export interface ITransferState { isRoutesModalOpen: boolean; toTokens: IBridgeToken[]; isFailedGetQuoteModalOpen: boolean; + isFeeTimeoutModalOpen: boolean; isSummaryModalOpen: boolean; refreshAnimationProgress: number; } @@ -61,6 +62,7 @@ const initStates: ITransferState = { toTokens: [], isManuallyReload: false, isFailedGetQuoteModalOpen: false, + isFeeTimeoutModalOpen: false, isSummaryModalOpen: false, refreshAnimationProgress: 0, }; @@ -163,6 +165,10 @@ export default createReducer(initStates, (builder) => { ...state, isFailedGetQuoteModalOpen: payload, })); + builder.addCase(actions.setIsFeeTimeoutModalOpen, (state, { payload }) => ({ + ...state, + isFeeTimeoutModalOpen: payload, + })); builder.addCase(actions.setIsSummaryModalOpen, (state, { payload }) => ({ ...state, isSummaryModalOpen: payload, From 273db6ff065234f2da8f71738dc1748d0e7855b9 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 14 Jan 2025 18:11:36 +1000 Subject: [PATCH 2/4] feat: Stargate max decimals error message --- .../adapters/stargate/hooks/useGetStarGateFees.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts index e6632085..de2f7507 100644 --- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts +++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts @@ -77,12 +77,21 @@ export const useGetStargateFees = () => { const receiver = address || DEFAULT_ADDRESS; const bridgeAddress = selectedToken?.stargate?.raw?.address as `0x${string}`; const decimal = selectedToken?.stargate?.raw?.token?.decimals ?? (18 as number); + const maxDecimals = selectedToken?.stargate?.raw?.sharedDecimals ?? 18; const allowedMin = Number(formatUnits(fees[0].minAmountLD, decimal)); const allowedMax = Number(formatUnits(fees[0].maxAmountLD, decimal)); const amount = parseUnits(sendValue, decimal); // Can not retrieve other fees if token amount is out of range if (Number(sendValue) >= allowedMin && Number(sendValue) <= allowedMax && !!args) { + if (sendValue.split('.')[1]?.length > maxDecimals) { + dispatch( + setRouteError({ + stargate: `The amount exceeds the maximum of ${maxDecimals} decimals`, + }), + ); + return { isDisplayError: true }; + } if (!!Number(fees?.[2].amountReceivedLD)) { if (fees?.[2].amountReceivedLD) { args.minAmountLD = BigInt(fees[2].amountReceivedLD); @@ -144,6 +153,7 @@ export const useGetStargateFees = () => { isDisplayError = true; } } + try { if (chain && fromChain?.id === chain?.id && address && selectedToken?.address) { // gas fee From 71dfd8b3a023541e8526bf883329e1cabc16be57 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 14 Jan 2025 18:45:18 +1000 Subject: [PATCH 3/4] fix: Fix type issue --- apps/canonical-bridge-ui/pages/mainnet/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/canonical-bridge-ui/pages/mainnet/index.tsx b/apps/canonical-bridge-ui/pages/mainnet/index.tsx index c0f80188..86099679 100644 --- a/apps/canonical-bridge-ui/pages/mainnet/index.tsx +++ b/apps/canonical-bridge-ui/pages/mainnet/index.tsx @@ -39,7 +39,7 @@ function BridgeWidget() { http: { serverEndpoint: env.SERVER_ENDPOINT, deBridgeReferralCode: env.DEBRIDGE_REFERRAL_CODE, - feeReloadMaxTime: env.FEE_RELOAD_MAX_TIME, + feeReloadMaxTime: Number(env.FEE_RELOAD_MAX_TIME), }, transfer: transferConfig, onClickConnectWalletButton: onOpen, From b817255fa6abacfbe847eb7919d535b63838e23a Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 14 Jan 2025 18:49:16 +1000 Subject: [PATCH 4/4] chore: Update stargate decimal error msg --- .../aggregator/adapters/stargate/hooks/useGetStarGateFees.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts index de2f7507..f8170ffa 100644 --- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts +++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/hooks/useGetStarGateFees.ts @@ -87,7 +87,7 @@ export const useGetStargateFees = () => { if (sendValue.split('.')[1]?.length > maxDecimals) { dispatch( setRouteError({ - stargate: `The amount exceeds the maximum of ${maxDecimals} decimals`, + stargate: `The send amount must be less than ${maxDecimals} digits`, }), ); return { isDisplayError: true };