Skip to content

Commit

Permalink
Merge pull request #288 from bnb-chain/feat/upperLimit0107
Browse files Browse the repository at this point in the history
feat: Fee loading timeout and stargate input decimal contraints
  • Loading branch information
wenty22 authored Jan 15, 2025
2 parents 4c55426 + b817255 commit 207a9d6
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 54 deletions.
1 change: 1 addition & 0 deletions apps/canonical-bridge-ui/core/env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
1 change: 1 addition & 0 deletions apps/canonical-bridge-ui/pages/mainnet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function BridgeWidget() {
http: {
serverEndpoint: env.SERVER_ENDPOINT,
deBridgeReferralCode: env.DEBRIDGE_REFERRAL_CODE,
feeReloadMaxTime: Number(env.FEE_RELOAD_MAX_TIME),
},
transfer: transferConfig,
onClickConnectWalletButton: onOpen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface IBridgeConfig {
http: {
refetchingInterval: number;
apiTimeOut: number;
feeReloadMaxTime?: number;
deBridgeAccessToken?: string;
deBridgeReferralCode?: string;
serverEndpoint?: string;
Expand Down
4 changes: 4 additions & 0 deletions packages/canonical-bridge-widget/src/core/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
8 changes: 8 additions & 0 deletions packages/canonical-bridge-widget/src/core/utils/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
}
2 changes: 2 additions & 0 deletions packages/canonical-bridge-widget/src/core/utils/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Original file line number Diff line number Diff line change
Expand Up @@ -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 send amount must be less than ${maxDecimals} digits`,
}),
);
return { isDisplayError: true };
}
if (!!Number(fees?.[2].amountReceivedLD)) {
if (fees?.[2].amountReceivedLD) {
args.minAmountLD = BigInt(fees[2].amountReceivedLD);
Expand Down Expand Up @@ -144,6 +153,7 @@ export const useGetStargateFees = () => {
isDisplayError = true;
}
}

try {
if (chain && fromChain?.id === chain?.id && address && selectedToken?.address) {
// gas fee
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const setIsSummaryModalOpen = createAction<ITransferState['isSummaryModal
'transfer/setIsSummaryModalOpen',
);

export const setIsFeeTimeoutModalOpen = createAction<ITransferState['isFeeTimeoutModalOpen']>(
'transfer/setIsFeeTimeoutModalOpen',
);

export const setRefreshAnimationProgress = createAction<ITransferState['refreshAnimationProgress']>(
'transfer/setRefreshAnimationProgress',
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useIntl } from '@bnb-chain/space';

import { StateModal, StateModalProps } from '@/core/components/StateModal';

export const FeeTimeoutModal = (props: Omit<StateModalProps, 'title'>) => {
const { ...restProps } = props;
const { formatMessage } = useIntl();

return (
<StateModal
className="bccb-widget-fee-timeout-modal"
type="error"
title={formatMessage({ id: 'modal.fee-timeout.error.title' })}
description={formatMessage({ id: 'modal.fee-timeout.error.desc' })}
buttonText={formatMessage({ id: 'modal.fee-timeout.error.button.close' })}
bodyProps={{
px: ['16px', '16px', '40px'],
}}
footerProps={{
pb: ['32px', '32px', '40px'],
px: ['16px', '16px', '40px'],
}}
{...restProps}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
Expand All @@ -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 {
Expand All @@ -46,6 +49,7 @@ export const TransferButtonGroup = () => {
onClose: onCloseConfirmingModal,
} = useDisclosure();
const { onCloseFailedGetQuoteModal } = useFailGetQuoteModal();
const { onCloseFeeTimeoutModal } = useFeeLoadTimeout();
const { onCloseSummaryModal, onOpenSummaryModal } = useSummaryModal();
return (
<>
Expand Down Expand Up @@ -99,6 +103,7 @@ export const TransferButtonGroup = () => {
isOpen={isFailedGetQuoteModalOpen}
onClose={onCloseFailedGetQuoteModal}
/>
<FeeTimeoutModal isOpen={isFeeTimeoutModalOpen} onClose={onCloseFeeTimeoutModal} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand All @@ -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();
Expand All @@ -80,6 +82,7 @@ export const useLoadingBridgeFees = () => {

const { mesonFeeSorting: _mesonFeeSorting } = useGetMesonFees();
const { onOpenFailedGetQuoteModal } = useFailGetQuoteModal();
const { onOpenFeeTimeoutModal } = useFeeLoadTimeout();
const mesonFeeSorting = useRef(_mesonFeeSorting);
mesonFeeSorting.current = _mesonFeeSorting;

Expand Down Expand Up @@ -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]',
Expand All @@ -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
Expand Down Expand Up @@ -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,
}),
);
}
},
[
Expand Down Expand Up @@ -485,6 +522,8 @@ export const useLoadingBridgeFees = () => {
nativeToken,
preSelectRoute,
onOpenFailedGetQuoteModal,
onOpenFeeTimeoutModal,
feeReloadMaxTime,
],
);

Expand Down
Loading

0 comments on commit 207a9d6

Please sign in to comment.