diff --git a/.release/.changeset/hot-knives-pay.md b/.release/.changeset/hot-knives-pay.md
new file mode 100644
index 00000000..3f2a69df
--- /dev/null
+++ b/.release/.changeset/hot-knives-pay.md
@@ -0,0 +1,5 @@
+---
+"@bnb-chain/canonical-bridge-widget": patch
+---
+
+feat: Send confirm popup
diff --git a/.release/.changeset/pre.json b/.release/.changeset/pre.json
new file mode 100644
index 00000000..07072fdc
--- /dev/null
+++ b/.release/.changeset/pre.json
@@ -0,0 +1,11 @@
+{
+ "mode": "pre",
+ "tag": "alpha",
+ "initialVersions": {
+ "@bnb-chain/canonical-bridge-sdk": "0.4.6",
+ "@bnb-chain/canonical-bridge-widget": "0.5.16"
+ },
+ "changesets": [
+ "hot-knives-pay"
+ ]
+}
diff --git a/apps/canonical-bridge-ui/core/components/ThemeProvider/index.tsx b/apps/canonical-bridge-ui/core/components/ThemeProvider/index.tsx
index 5e900bfb..5f1d2a65 100644
--- a/apps/canonical-bridge-ui/core/components/ThemeProvider/index.tsx
+++ b/apps/canonical-bridge-ui/core/components/ThemeProvider/index.tsx
@@ -24,6 +24,9 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
global: ({ colorMode }: { colorMode: ColorMode }) => ({
body: {
bg: theme.colors[colorMode].background[3],
+ '.bccb-widget-transaction-summary-modal-overlap': {
+ opacity: '0.88 !important',
+ },
},
}),
},
diff --git a/packages/canonical-bridge-widget/CHANGELOG.md b/packages/canonical-bridge-widget/CHANGELOG.md
index a425fd23..ef3cbadf 100644
--- a/packages/canonical-bridge-widget/CHANGELOG.md
+++ b/packages/canonical-bridge-widget/CHANGELOG.md
@@ -1,5 +1,11 @@
# @bnb-chain/canonical-bridge-widget
+## 0.5.17-alpha.0
+
+### Patch Changes
+
+- feat: Send confirm popup
+
## 0.5.16
### Patch Changes
diff --git a/packages/canonical-bridge-widget/package.json b/packages/canonical-bridge-widget/package.json
index 78bb0e38..7f6b0a8a 100644
--- a/packages/canonical-bridge-widget/package.json
+++ b/packages/canonical-bridge-widget/package.json
@@ -1,6 +1,6 @@
{
"name": "@bnb-chain/canonical-bridge-widget",
- "version": "0.5.16",
+ "version": "0.5.17-alpha.0",
"description": "canonical bridge widget",
"author": "bnb-chain",
"private": false,
diff --git a/packages/canonical-bridge-widget/src/core/components/icons/InfoIcon.tsx b/packages/canonical-bridge-widget/src/core/components/icons/InfoIcon.tsx
index 52658ebd..8f6e1b59 100644
--- a/packages/canonical-bridge-widget/src/core/components/icons/InfoIcon.tsx
+++ b/packages/canonical-bridge-widget/src/core/components/icons/InfoIcon.tsx
@@ -1,6 +1,11 @@
import { Icon, IconProps } from '@bnb-chain/space';
-export function InfoIcon(props: IconProps) {
+interface InfoIconProps extends IconProps {
+ iconcolor?: string;
+ iconbgcolor?: string;
+}
+
+export function InfoIcon(props: InfoIconProps) {
return (
);
diff --git a/packages/canonical-bridge-widget/src/core/components/icons/TransferToIcon.tsx b/packages/canonical-bridge-widget/src/core/components/icons/TransferToIcon.tsx
index 387276b0..a1db9df8 100644
--- a/packages/canonical-bridge-widget/src/core/components/icons/TransferToIcon.tsx
+++ b/packages/canonical-bridge-widget/src/core/components/icons/TransferToIcon.tsx
@@ -1,6 +1,10 @@
import { Icon, IconProps } from '@bnb-chain/space';
-export function TransferToIcon(props: IconProps) {
+interface TransferToIconProps extends IconProps {
+ iconopacity?: string;
+}
+
+export function TransferToIcon(props: TransferToIconProps) {
return (
);
diff --git a/packages/canonical-bridge-widget/src/core/locales/en.ts b/packages/canonical-bridge-widget/src/core/locales/en.ts
index ccda9559..06225390 100644
--- a/packages/canonical-bridge-widget/src/core/locales/en.ts
+++ b/packages/canonical-bridge-widget/src/core/locales/en.ts
@@ -62,6 +62,12 @@ export const en = {
'transfer.button.switch-network': 'Switch Network in Wallet',
'transfer.button.wallet-connect': 'Connect Wallet',
'transfer.button.switch-wallet': 'Switch Wallet',
+ 'transfer.button.confirm-summary': 'Confirm Transfer',
+ 'transfer.button.confirm-loading': 'Reloading Quotation',
+
+ 'transfer.warning.confirm.to.address': 'Please double check the received token address:',
+ 'transfer.warning.sol.balance':
+ 'At least {min} SOL is required to proceed with this transaction.',
'modal.approve.title': 'Approve Token',
'modal.approve.desc.1': 'Please approve at least ',
@@ -81,6 +87,13 @@ export const en = {
'modal.confirm.title': 'Waiting for Confirmation',
'modal.confirm.desc': 'Confirm this transaction in your wallet',
+ 'modal.summary.title': 'Confirm Transaction',
+
+ 'modal.quote.error.title': 'Failed to Get the Quotation',
+ 'modal.quote.error.desc':
+ 'We’ve encountered an unknown issue on this route. Please try again later.',
+ 'modal.quote.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/modules/aggregator/adapters/cBridge/components/CBridgeOption.tsx b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/cBridge/components/CBridgeOption.tsx
index 05755e7b..7914ddba 100644
--- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/cBridge/components/CBridgeOption.tsx
+++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/cBridge/components/CBridgeOption.tsx
@@ -25,7 +25,6 @@ export const CBridgeOption = () => {
const estimatedAmount = useAppSelector((state) => state.transfer.estimatedAmount);
const sendValue = useAppSelector((state) => state.transfer.sendValue);
const routeError = useAppSelector((state) => state.transfer.routeError);
- const routeFees = useAppSelector((state) => state.transfer.routeFees);
const receiveAmt = useMemo(() => {
return estimatedAmount &&
@@ -89,12 +88,7 @@ export const CBridgeOption = () => {
toTokenInfo={toTokenInfo?.['cBridge']}
/>
-
+
{
const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
const estimatedAmount = useAppSelector((state) => state.transfer.estimatedAmount);
const routeError = useAppSelector((state) => state.transfer.routeError);
- const routeFees = useAppSelector((state) => state.transfer.routeFees);
const receiveAmt = useMemo(() => {
return estimatedAmount?.['deBridge'] &&
@@ -79,12 +78,7 @@ export const DeBridgeOption = ({}: DeBridgeOptionProps) => {
toTokenInfo={toTokenInfo?.['deBridge']}
/>
-
+
);
diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx
index 301610bc..bdf8638e 100644
--- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx
+++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/layerZero/components/LayerZeroOption.tsx
@@ -22,7 +22,6 @@ export const LayerZeroOption = () => {
const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
const estimatedAmount = useAppSelector((state) => state.transfer.estimatedAmount);
const routeError = useAppSelector((state) => state.transfer.routeError);
- const routeFees = useAppSelector((state) => state.transfer.routeFees);
const receiveAmt = useMemo(() => {
return estimatedAmount &&
@@ -33,6 +32,7 @@ export const LayerZeroOption = () => {
? `${formatNumber(
Number(formatUnits(BigInt(estimatedAmount?.['layerZero']), getToDecimals()['layerZero'])),
8,
+ false,
)}`
: '--';
}, [estimatedAmount, toTokenInfo, sendValue, getToDecimals]);
@@ -70,12 +70,7 @@ export const LayerZeroOption = () => {
toTokenInfo={toTokenInfo?.['layerZero']}
/>
-
+
);
diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/components/MesonOption.tsx b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/components/MesonOption.tsx
index c2607ad4..f4e5e692 100644
--- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/components/MesonOption.tsx
+++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/components/MesonOption.tsx
@@ -19,7 +19,6 @@ export const MesonOption = () => {
const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
const estimatedAmount = useAppSelector((state) => state.transfer.estimatedAmount);
const routeError = useAppSelector((state) => state.transfer.routeError);
- const routeFees = useAppSelector((state) => state.transfer.routeFees);
const fromChain = useAppSelector((state) => state.transfer.fromChain);
const receiveAmt = useMemo(() => {
@@ -55,12 +54,7 @@ export const MesonOption = () => {
-
+
{/* {
const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
const estimatedAmount = useAppSelector((state) => state.transfer.estimatedAmount);
const routeError = useAppSelector((state) => state.transfer.routeError);
- const routeFees = useAppSelector((state) => state.transfer.routeFees);
const receiveAmt = useMemo(() => {
return estimatedAmount &&
@@ -79,12 +78,7 @@ export const StarGateOption = () => {
toTokenInfo={toTokenInfo?.['stargate']}
/>
-
+
void }) => {
+ const fromChain = useAppSelector((state) => state.transfer.fromChain);
+ const toChain = useAppSelector((state) => state.transfer.toChain);
+ const selectedToken = useAppSelector((state) => state.transfer.selectedToken);
+ const sendValue = useAppSelector((state) => state.transfer.sendValue);
+ const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
+
+ const handleFailure = useCallback(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (e: any) => {
+ reportEvent({
+ id: 'transaction_bridge_fail',
+ params: {
+ item_category: fromChain?.name,
+ item_category2: toChain?.name,
+ token: selectedToken?.displaySymbol,
+ value: sendValue,
+ item_variant: transferActionInfo?.bridgeType,
+ message: JSON.stringify(e.message || e),
+ page_location: JSON.stringify(e.message || e),
+ },
+ });
+ onOpenFailedModal();
+ },
+ [
+ fromChain?.name,
+ onOpenFailedModal,
+ selectedToken?.displaySymbol,
+ sendValue,
+ toChain?.name,
+ transferActionInfo?.bridgeType,
+ ],
+ );
+
+ return { handleFailure };
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/BridgeRoutes.tsx b/packages/canonical-bridge-widget/src/modules/transfer/BridgeRoutes.tsx
index 14571fc6..538bdfce 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/BridgeRoutes.tsx
+++ b/packages/canonical-bridge-widget/src/modules/transfer/BridgeRoutes.tsx
@@ -1,10 +1,17 @@
import { useBreakpointValue, useIntl } from '@bnb-chain/space';
+import { useEffect } from 'react';
import { TransferOverview } from '@/modules/transfer/components/TransferOverview';
import { RoutesModal } from '@/modules/transfer/components/TransferOverview/modal/RoutesModal';
import { useBridgeConfig } from '@/CanonicalBridgeProvider';
import { useAppDispatch, useAppSelector } from '@/modules/store/StoreProvider';
-import { setIsRoutesModalOpen } from '@/modules/transfer/action';
+import {
+ setIsGlobalFeeLoading,
+ setIsManuallyReload,
+ setIsRefreshing,
+ setIsRoutesModalOpen,
+} from '@/modules/transfer/action';
+import { TriggerType, useLoadingBridgeFees } from '@/modules/transfer/hooks/useLoadingBridgeFees';
export function BridgeRoutes() {
const { formatMessage } = useIntl();
@@ -12,7 +19,59 @@ export function BridgeRoutes() {
const isBase = useBreakpointValue({ base: true, lg: false }) ?? false;
const { routeContentBottom } = useBridgeConfig();
+ const { loadingBridgeFees } = useLoadingBridgeFees();
+ const bridgeConfig = useBridgeConfig();
const isRoutesModalOpen = useAppSelector((state) => state.transfer.isRoutesModalOpen);
+ const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
+ const isManuallyReload = useAppSelector((state) => state.transfer.isManuallyReload);
+
+ // Load estimated bridge fees every 30 seconds when there is bridge route available
+ useEffect(() => {
+ let mount = true;
+ if (!mount) return;
+ if (transferActionInfo) {
+ const params = {
+ triggerType: 'refresh' as TriggerType,
+ };
+
+ let interval = setInterval(() => {
+ dispatch(setIsGlobalFeeLoading(true));
+ loadingBridgeFees(params);
+ }, bridgeConfig.http.refetchingInterval ?? 30000);
+
+ // Stop and restart fee loading
+ if (isManuallyReload === true) {
+ dispatch(setIsManuallyReload(false));
+ dispatch(setIsRefreshing(true));
+ if (interval) {
+ clearInterval(interval);
+ dispatch(setIsGlobalFeeLoading(true));
+ loadingBridgeFees(params);
+ dispatch(setIsRefreshing(false));
+ interval = setInterval(() => {
+ dispatch(setIsGlobalFeeLoading(true));
+ loadingBridgeFees(params);
+ }, bridgeConfig.http?.refetchingInterval ?? 30000);
+ }
+ }
+
+ return () => {
+ mount = false;
+ interval && clearInterval(interval);
+ dispatch(setIsManuallyReload(false));
+ };
+ } else {
+ dispatch(setIsManuallyReload(false));
+ mount = false;
+ dispatch(setIsManuallyReload(false));
+ }
+ }, [
+ transferActionInfo,
+ loadingBridgeFees,
+ dispatch,
+ bridgeConfig.http.refetchingInterval,
+ isManuallyReload,
+ ]);
if (isBase) {
return (
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/action.ts b/packages/canonical-bridge-widget/src/modules/transfer/action.ts
index 27b696a9..c502f17f 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/action.ts
+++ b/packages/canonical-bridge-widget/src/modules/transfer/action.ts
@@ -47,3 +47,14 @@ export const setIsToAddressChecked = createAction(
'transfer/setIsRoutesModalOpen',
);
+
+export const setIsManuallyReload = createAction(
+ 'transfer/setIsManuallyReload',
+);
+
+export const setIsFailedGetQuoteModalOpen = createAction<
+ ITransferState['isFailedGetQuoteModalOpen']
+>('transfer/setIsFailedGetQuoteModalOpen');
+export const setIsSummaryModalOpen = createAction(
+ 'transfer/setIsSummaryModalOpen',
+);
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/RefreshingButton.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/RefreshingButton.tsx
index 1c740f5f..ffa9c408 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/RefreshingButton.tsx
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/RefreshingButton.tsx
@@ -1,72 +1,23 @@
-import { useEffect, useState } from 'react';
-import { Box, BoxProps, useColorMode, useTheme } from '@bnb-chain/space';
+import { Box, BoxProps, IconProps, useColorMode, useTheme } from '@bnb-chain/space';
import { useAppDispatch, useAppSelector } from '@/modules/store/StoreProvider';
-import { setIsGlobalFeeLoading, setIsRefreshing } from '@/modules/transfer/action';
-import { TriggerType, useLoadingBridgeFees } from '@/modules/transfer/hooks/useLoadingBridgeFees';
+import { setIsManuallyReload, setIsRefreshing } from '@/modules/transfer/action';
import { RefreshingIcon } from '@/modules/transfer/components/LoadingImg/RefreshingIcon';
import { useBridgeConfig } from '@/index';
-export const RefreshingButton = (props: BoxProps) => {
+export const RefreshingButton = ({
+ iconProps,
+ boxProps,
+}: {
+ iconProps?: IconProps;
+ boxProps?: BoxProps;
+}) => {
const { colorMode } = useColorMode();
- const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
- const isRefreshing = useAppSelector((state) => state.transfer.isRefreshing);
- const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
const theme = useTheme();
const dispatch = useAppDispatch();
- const [isButtonPressed, setIsButtonPressed] = useState(false);
-
- const { loadingBridgeFees } = useLoadingBridgeFees();
- const bridgeConfig = useBridgeConfig();
-
- // Load estimated bridge fees every 30 seconds when there is bridge route available
- useEffect(() => {
- let mount = true;
- if (!mount) return;
- if (transferActionInfo) {
- const params = {
- triggerType: 'refresh' as TriggerType,
- };
-
- let interval = setInterval(() => {
- dispatch(setIsGlobalFeeLoading(true));
- loadingBridgeFees(params);
- }, bridgeConfig.http.refetchingInterval ?? 30000);
-
- // Stop and restart fee loading
- if (isButtonPressed === true) {
- dispatch(setIsRefreshing(true));
- if (interval) {
- clearInterval(interval);
- dispatch(setIsGlobalFeeLoading(true));
- loadingBridgeFees(params);
- dispatch(setIsRefreshing(false));
- interval = setInterval(() => {
- dispatch(setIsGlobalFeeLoading(true));
- loadingBridgeFees(params);
- }, bridgeConfig.http?.refetchingInterval ?? 30000);
- }
- setIsButtonPressed(false);
- }
-
- return () => {
- mount = false;
- interval && clearInterval(interval);
- setIsButtonPressed(false);
- };
- } else {
- return () => {
- mount = false;
- setIsButtonPressed(false);
- };
- }
- }, [
- transferActionInfo,
- loadingBridgeFees,
- dispatch,
- isButtonPressed,
- bridgeConfig.http.refetchingInterval,
- ]);
+ const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
+ const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
+ const isRefreshing = useAppSelector((state) => state.transfer.isRefreshing);
const { refreshingIcon } = useBridgeConfig();
@@ -82,12 +33,12 @@ export const RefreshingButton = (props: BoxProps) => {
: theme.colors[colorMode].button.refresh.text,
}}
onClick={() => {
- setIsButtonPressed(true);
+ dispatch(setIsManuallyReload(true));
dispatch(setIsRefreshing(true));
}}
- {...props}
+ {...boxProps}
>
- {refreshingIcon ?? }
+ {refreshingIcon ?? }
) : null;
};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferButton.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferButton.tsx
index c7273459..6b51a0a0 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferButton.tsx
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferButton.tsx
@@ -1,71 +1,50 @@
/* eslint-disable no-console */
import { Button, Flex, useColorMode, useIntl, useTheme } from '@bnb-chain/space';
import { useCallback, useState } from 'react';
-import { useAccount, useBytecode, usePublicClient, useSignMessage, useWalletClient } from 'wagmi';
-import { formatUnits, parseUnits } from 'viem';
+import { useAccount, useBytecode, usePublicClient, useWalletClient } from 'wagmi';
+import { formatUnits } from 'viem';
import { useWallet as useTronWallet } from '@tronweb3/tronwallet-adapter-react-hooks';
-import { useConnection } from '@solana/wallet-adapter-react';
-import { useWallet as useSolanaWallet } from '@solana/wallet-adapter-react';
-import { VersionedTransaction } from '@solana/web3.js';
import { useAppSelector } from '@/modules/store/StoreProvider';
import { useGetAllowance } from '@/core/contract/hooks/useGetAllowance';
-import { useCBridgeTransferParams } from '@/modules/aggregator/adapters/cBridge/hooks/useCBridgeTransferParams';
-import { useBridgeSDK } from '@/core/hooks/useBridgeSDK';
import { reportEvent } from '@/core/utils/gtm';
import { useGetTronAllowance } from '@/modules/aggregator/adapters/meson/hooks/useGetTronAllowance';
import { useTronTransferInfo } from '@/modules/transfer/hooks/tron/useTronTransferInfo';
-import { utf8ToHex } from '@/core/utils/string';
import { useTronContract } from '@/modules/aggregator/adapters/meson/hooks/useTronContract';
import { useSolanaTransferInfo } from '@/modules/transfer/hooks/solana/useSolanaTransferInfo';
import { useTronAccount } from '@/modules/wallet/hooks/useTronAccount';
-import { useWaitForTxReceipt } from '@/core/hooks/useWaitForTxReceipt';
-import {
- CBRIDGE_ENDPOINT,
- DEBRIDGE_ENDPOINT,
- MESON_ENDPOINT,
- STARGATE_ENDPOINT,
-} from '@/core/constants';
+import { useHandleTxFailure } from '@/modules/aggregator/hooks/useHandleTxFailure';
export function TransferButton({
- onOpenSubmittedModal,
onOpenFailedModal,
onOpenApproveModal,
- onOpenConfirmingModal,
+ onOpenSummaryModal,
onCloseConfirmingModal,
- setHash,
setChosenBridge,
}: {
- onOpenSubmittedModal: () => void;
onOpenFailedModal: () => void;
onOpenApproveModal: () => void;
- onOpenConfirmingModal: () => void;
+ onOpenSummaryModal: () => void;
onCloseConfirmingModal: () => void;
- setHash: (hash: string | null) => void;
+
setChosenBridge: (bridge: string | null) => void;
}) {
const { data: walletClient } = useWalletClient();
- const { args: cBridgeArgs } = useCBridgeTransferParams();
- const bridgeSDK = useBridgeSDK();
const { formatMessage } = useIntl();
const theme = useTheme();
const { colorMode } = useColorMode();
const { address } = useAccount();
- const { address: tronAddress, signTransaction } = useTronWallet();
+ const { address: tronAddress } = useTronWallet();
const { isTronAvailableToAccount, isTronTransfer } = useTronTransferInfo();
- const { signMessageAsync } = useSignMessage();
const { isSolanaTransfer, isSolanaAvailableToAccount } = useSolanaTransferInfo();
- const { connection } = useConnection();
- const { sendTransaction: sendSolanaTransaction } = useSolanaWallet();
const sendValue = useAppSelector((state) => state.transfer.sendValue);
const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
const selectedToken = useAppSelector((state) => state.transfer.selectedToken);
const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
const isTransferable = useAppSelector((state) => state.transfer.isTransferable);
- const toToken = useAppSelector((state) => state.transfer.toToken);
const fromChain = useAppSelector((state) => state.transfer.fromChain);
const toChain = useAppSelector((state) => state.transfer.toChain);
const toAccount = useAppSelector((state) => state.transfer.toAccount);
@@ -73,7 +52,6 @@ export function TransferButton({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const publicClient = usePublicClient({ chainId: fromChain?.id }) as any;
- const toPublicClient = usePublicClient({ chainId: toChain?.id }) as any;
const [isLoading, setIsLoading] = useState(false);
const { allowance } = useGetAllowance({
@@ -86,10 +64,11 @@ export function TransferButton({
chainId: toChain?.id,
});
+ const { handleFailure } = useHandleTxFailure({ onOpenFailedModal });
+
const tronAllowance = useGetTronAllowance();
const { isConnected: isEvmConnected } = useAccount();
const { isConnected: isTronConnected } = useTronAccount();
- const { waitForTxReceipt } = useWaitForTxReceipt();
const isApproveNeeded =
(fromChain?.chainType === 'evm' &&
@@ -104,7 +83,7 @@ export function TransferButton({
Number(formatUnits(tronAllowance, selectedToken?.meson?.raw?.decimals || 6))) ||
(fromChain?.chainType === 'solana' && false);
- const sendTx = useCallback(async () => {
+ const onConfirmSummary = useCallback(async () => {
if (
!selectedToken ||
!transferActionInfo?.bridgeType ||
@@ -122,24 +101,7 @@ export function TransferButton({
) {
return;
}
- const handleFailure = (e: any) => {
- reportEvent({
- id: 'transaction_bridge_fail',
- params: {
- item_category: fromChain?.name,
- item_category2: toChain?.name,
- token: selectedToken.displaySymbol,
- value: sendValue,
- item_variant: transferActionInfo?.bridgeType,
- message: JSON.stringify(e.message || e),
- page_location: JSON.stringify(e.message || e),
- },
- });
- onOpenFailedModal();
- };
-
try {
- setHash(null);
setChosenBridge('');
setIsLoading(true);
if (
@@ -167,379 +129,8 @@ export function TransferButton({
return;
}
- onOpenConfirmingModal();
-
- reportEvent({
- id: 'click_bridge_goal',
- params: {
- item_name: 'Send',
- },
- });
-
- if (transferActionInfo.bridgeType === 'cBridge' && cBridgeArgs && fromChain && address) {
- try {
- const isValidToken = await bridgeSDK.cBridge.validateCBridgeToken({
- isPegged: selectedToken.isPegged,
- fromChainId: fromChain.id,
- fromTokenAddress: selectedToken?.cBridge?.raw?.token.address as `0x${string}`,
- fromTokenSymbol: selectedToken?.cBridge?.raw?.token?.symbol as string,
- fromTokenDecimals: selectedToken.cBridge?.raw?.token.decimal as number,
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- toChainId: toChain?.id,
- toTokenAddress: toToken?.cBridge?.raw?.token.address as `0x${string}`,
- toTokenSymbol: toToken?.cBridge?.raw?.token.symbol,
- toTokenDecimals: toToken?.cBridge?.raw?.token.decimal as number,
- amount: Number(sendValue),
- cBridgeEndpoint: `${CBRIDGE_ENDPOINT}/getTransferConfigsForAll`,
- });
-
- if (!isValidToken) {
- handleFailure({
- fromTokenAddress: selectedToken.address as `0x${string}`,
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- fromChainId: fromChain.id,
- isPegged: selectedToken.isPegged,
- fromTokenSymbol: selectedToken.symbol,
- toChainId: toChain?.id,
- toTokenAddress: toToken?.cBridge?.raw?.token.address as `0x${string}`,
- toTokenSymbol: toToken?.cBridge?.raw?.token.symbol,
- decimals: selectedToken.decimals,
- amount: Number(sendValue),
- message: `(Token Validation Failed) - Invalid cBridge token!!`,
- });
- return;
- }
- const cBridgeHash = await bridgeSDK.cBridge.sendToken({
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- walletClient: walletClient as any,
- publicClient,
- bridgeAddress: transferActionInfo.bridgeAddress as string,
- fromChainId: fromChain?.id,
- isPegged: selectedToken.isPegged,
- address,
- peggedConfig: selectedToken?.cBridge?.peggedConfig,
- args: cBridgeArgs.args,
- });
- await waitForTxReceipt({
- publicClient,
- hash: cBridgeHash,
- });
- if (cBridgeHash) {
- reportEvent({
- id: 'transaction_bridge_success',
- params: {
- item_category: fromChain?.name,
- item_category2: toChain?.name,
- token: selectedToken.displaySymbol,
- value: sendValue,
- item_variant: 'cBridge',
- },
- });
- onCloseConfirmingModal();
- setHash(cBridgeHash);
- setChosenBridge('cBridge');
- onOpenSubmittedModal();
- }
- // eslint-disable-next-line no-console
- console.log('cBridge tx', cBridgeHash);
- } catch (e) {
- // eslint-disable-next-line no-console
- console.log(e);
- handleFailure(e);
- }
- } else if (transferActionInfo.bridgeType === 'deBridge') {
- try {
- let deBridgeHash: string | undefined;
- const isValidToken = await bridgeSDK.deBridge.validateDeBridgeToken({
- fromChainId: fromChain?.id,
- toChainId: toChain?.id,
- fromTokenSymbol: selectedToken.symbol,
- fromTokenAddress: selectedToken.deBridge?.raw?.address as `0x${string}`,
- fromTokenDecimals: selectedToken.deBridge?.raw?.decimals as number,
- toTokenSymbol: toToken?.deBridge?.raw?.symbol,
- toTokenAddress: toToken?.deBridge?.raw?.address as `0x${string}`,
- toTokenDecimals: toToken?.deBridge?.raw?.decimals as number,
- amount: Number(sendValue),
- fromChainType: fromChain?.chainType,
- fromBridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- toChainType: toChain?.chainType,
- deBridgeEndpoint: DEBRIDGE_ENDPOINT,
- });
- if (!isValidToken) {
- handleFailure({
- message: '(Token Validation Failed) - Invalid deBridge token!!',
- fromChainId: fromChain?.id,
- tokenSymbol: selectedToken.symbol,
- tokenAddress: selectedToken.address as `0x${string}`,
- });
- return;
- }
- if (fromChain?.chainType === 'evm' && transferActionInfo.value && address) {
- deBridgeHash = await bridgeSDK.deBridge.sendToken({
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- walletClient: walletClient as any,
- bridgeAddress: transferActionInfo.bridgeAddress as string,
- data: transferActionInfo.data as `0x${string}`,
- amount: BigInt(transferActionInfo.value),
- address,
- });
- await waitForTxReceipt({
- publicClient,
- hash: deBridgeHash,
- });
- }
-
- if (fromChain?.chainType === 'solana') {
- const { blockhash } = await connection.getLatestBlockhash();
- const data = (transferActionInfo.data as string)?.slice(2);
- const tx = VersionedTransaction.deserialize(Buffer.from(data, 'hex'));
-
- tx.message.recentBlockhash = blockhash;
- deBridgeHash = await sendSolanaTransaction(tx, connection);
-
- console.log('---solana---');
- console.log('blockhash: ', blockhash);
- console.log('data:', data);
- console.log('tx:', tx);
- console.log('hash:', deBridgeHash);
- }
-
- if (deBridgeHash) {
- reportEvent({
- id: 'transaction_bridge_success',
- params: {
- item_category: fromChain?.name,
- item_category2: toChain?.name,
- token: selectedToken.displaySymbol,
- value: sendValue,
- item_variant: 'deBridge',
- },
- });
- onCloseConfirmingModal();
- setChosenBridge('deBridge');
- setHash(deBridgeHash);
- onOpenSubmittedModal();
- }
- } catch (e) {
- // eslint-disable-next-line no-console
- console.log(e);
- handleFailure(e);
- }
- } else if (transferActionInfo.bridgeType === 'stargate' && address) {
- const isValidToken = await bridgeSDK.stargate.validateStargateToken({
- fromBridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- toBridgeAddress: toToken?.stargate?.raw?.address as `0x${string}`,
- fromTokenAddress: selectedToken?.stargate?.raw?.token?.address as `0x${string}`,
- fromTokenSymbol: selectedToken?.stargate?.raw?.token?.symbol as string,
- fromTokenDecimals: selectedToken?.stargate?.raw?.token?.decimals as number,
- fromChainId: fromChain?.id,
- toTokenAddress: toToken?.stargate?.raw?.token?.address as `0x${string}`,
- toTokenSymbol: toToken?.stargate?.raw?.token?.symbol as string,
- toTokenDecimals: toToken?.stargate?.raw?.token?.decimals as number,
- toChainId: toChain?.id,
- amount: Number(sendValue),
- dstEndpointId: toToken?.stargate?.raw?.endpointID as number,
- toPublicClient,
- fromPublicClient: publicClient,
- stargateEndpoint: STARGATE_ENDPOINT,
- });
- if (!isValidToken) {
- handleFailure({
- messages: '(Token Validation Failed) - Invalid Stargate token!!',
- fromChainId: fromChain?.id,
- tokenAddress: selectedToken.address as `0x${string}`,
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- tokenSymbol: selectedToken.symbol,
- });
- return;
- }
- const stargateHash = await bridgeSDK.stargate.sendToken({
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- walletClient: walletClient as any,
- publicClient,
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- tokenAddress: selectedToken.address as `0x${string}`,
- endPointId: toToken?.stargate?.raw?.endpointID as number,
- receiver: address,
- amount: parseUnits(sendValue, selectedToken.decimals),
- });
- if (stargateHash) {
- reportEvent({
- id: 'transaction_bridge_success',
- params: {
- item_category: fromChain?.name,
- item_category2: toChain?.name,
- token: selectedToken.displaySymbol,
- value: sendValue,
- item_variant: 'stargate',
- },
- });
- onCloseConfirmingModal();
- setChosenBridge('stargate');
- setHash(stargateHash);
- onOpenSubmittedModal();
- }
- } else if (transferActionInfo.bridgeType === 'layerZero' && address) {
- // check layerZero token address
- const isValidToken = await bridgeSDK.layerZero.validateLayerZeroToken({
- fromPublicClient: publicClient,
- toPublicClient,
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- fromTokenAddress: selectedToken.layerZero?.raw?.address as `0x${string}`,
- fromTokenSymbol: selectedToken.layerZero?.raw?.symbol as string,
- fromTokenDecimals: selectedToken.layerZero?.raw?.decimals as number,
- toTokenAddress: toToken?.layerZero?.raw?.address as `0x${string}`,
- toTokenDecimals: toToken?.layerZero?.raw?.decimals as number,
- toTokenSymbol: toToken?.layerZero?.raw?.symbol as string,
- toBridgeAddress: toToken?.layerZero?.raw?.bridgeAddress as `0x${string}`,
- dstEndpoint: toToken?.layerZero?.raw?.endpointID as number,
- amount: Number(sendValue),
- });
- if (!isValidToken) {
- handleFailure({
- messages: '(Token Validation Failed) - Invalid LayerZero token!!',
- fromChainId: fromChain?.id,
- tokenAddress: selectedToken.layerZero?.raw?.address as `0x${string}`,
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- tokenSymbol: selectedToken.symbol,
- });
- return;
- }
- const layerZeroHash = await bridgeSDK.layerZero.sendToken({
- bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
- dstEndpoint: toToken?.layerZero?.raw?.endpointID as number,
- userAddress: address,
- amount: parseUnits(sendValue, selectedToken.decimals),
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- walletClient: walletClient as any,
- publicClient,
- });
- if (layerZeroHash) {
- reportEvent({
- id: 'transaction_bridge_success',
- params: {
- item_category: fromChain?.name,
- item_category2: toChain?.name,
- token: selectedToken.displaySymbol,
- value: sendValue,
- item_variant: 'layerZero',
- },
- });
- onCloseConfirmingModal();
- setChosenBridge('layerZero');
- setHash(layerZeroHash);
- onOpenSubmittedModal();
- }
- } else if (transferActionInfo.bridgeType === 'meson') {
- const isValidToken = await bridgeSDK.meson.validateMesonToken({
- fromChainId: fromChain?.id,
- toChainId: toChain?.id,
- fromTokenAddress:
- selectedToken.meson?.raw?.addr ?? '0x0000000000000000000000000000000000000000',
- fromTokenSymbol: selectedToken.meson?.raw?.id as string,
- fromTokenDecimals: selectedToken.meson?.raw?.decimals as number,
- fromChainType: fromChain?.chainType,
- toChainType: toChain?.chainType,
- toTokenAddress: toToken?.meson?.raw?.addr ?? '0x0000000000000000000000000000000000000000',
- toTokenSymbol: toToken?.meson?.raw?.id,
- toTokenDecimals: toToken?.meson?.raw?.decimals,
- amount: Number(sendValue),
- mesonEndpoint: MESON_ENDPOINT,
- });
- if (!isValidToken) {
- handleFailure({
- message: '(Token Validation Failed) Invalid Meson token!!',
- fromChainId: fromChain?.id,
- tokenAddress: selectedToken.address as `0x${string}`,
- tokenSymbol: selectedToken.symbol,
- });
- return;
- }
- let fromAddress = '';
- let toAddress = '';
- let msg = '';
- let signature = '';
-
- if (fromChain?.chainType === 'tron' && tronAddress) {
- fromAddress = tronAddress;
- } else if (fromChain?.chainType !== 'tron' && address) {
- fromAddress = address;
- }
-
- if (isTronTransfer && isTronAvailableToAccount && toAccount?.address) {
- toAddress = toAccount.address;
- } else if (address) {
- toAddress = address;
- }
-
- // get unsigned message
- const unsignedMessage = await bridgeSDK.meson.getUnsignedMessage({
- fromToken: `${fromChain?.meson?.raw?.id}:${selectedToken?.meson?.raw?.id}`,
- toToken: `${toChain?.meson?.raw?.id}:${toToken?.meson?.raw?.id}`,
- amount: sendValue,
- fromAddress: fromAddress,
- recipient: toAddress,
- });
-
- if (unsignedMessage?.result) {
- const result = unsignedMessage.result;
- const encodedData = result.encoded;
- const message = result.signingRequest.message;
-
- if (fromChain?.chainType === 'tron') {
- const hexTronHeader = utf8ToHex('\x19TRON Signed Message:\n32');
- msg = message.replace(hexTronHeader, '');
- } else {
- const hexEthHeader = utf8ToHex('\x19Ethereum Signed Message:\n52');
- msg = message.replace(hexEthHeader, '');
- }
-
- if (fromChain?.chainType != 'tron') {
- signature = await signMessageAsync({
- account: address,
- message: {
- raw: msg as `0x${string}`,
- },
- });
- } else {
- // TODO
- signature = String(await signTransaction(msg as any));
- }
-
- const swapId = await bridgeSDK.meson.sendToken({
- fromAddress: fromAddress,
- recipient: toAddress,
- signature: signature,
- encodedData: encodedData,
- });
-
- // eslint-disable-next-line no-console
- console.log('Meson swap id', swapId);
- if (swapId?.result?.swapId) {
- setChosenBridge('meson');
- setHash(swapId?.result?.swapId);
- }
- if (swapId?.error) {
- throw new Error(swapId?.error.message);
- }
-
- reportEvent({
- id: 'transaction_bridge_success',
- params: {
- item_category: fromChain?.name,
- item_category2: toChain?.name,
- token: selectedToken.displaySymbol,
- value: sendValue,
- item_variant: 'meson',
- },
- });
-
- onCloseConfirmingModal();
- onOpenSubmittedModal();
- } else {
- throw new Error(unsignedMessage?.error.message);
- }
- }
+ onOpenSummaryModal();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
// eslint-disable-next-line no-console
console.error(e, e.message);
@@ -552,49 +143,23 @@ export function TransferButton({
selectedToken,
transferActionInfo?.bridgeType,
transferActionInfo?.bridgeAddress,
- transferActionInfo?.value,
- transferActionInfo?.data,
fromChain,
walletClient,
publicClient,
- toPublicClient,
address,
allowance,
isEvmConnected,
isTronConnected,
tronAddress,
tronAllowance,
- toChain?.name,
- toChain?.meson?.raw?.id,
sendValue,
- onOpenFailedModal,
- setHash,
+
setChosenBridge,
isApproveNeeded,
- onOpenConfirmingModal,
- cBridgeArgs,
+ onOpenSummaryModal,
onOpenApproveModal,
- bridgeSDK.cBridge,
- bridgeSDK.deBridge,
- bridgeSDK.stargate,
- bridgeSDK.layerZero,
- bridgeSDK.meson,
onCloseConfirmingModal,
- onOpenSubmittedModal,
- connection,
- sendSolanaTransaction,
- toToken?.stargate?.raw,
- toToken?.layerZero?.raw,
- toToken?.meson?.raw,
- toToken?.cBridge?.raw,
- toToken?.deBridge?.raw,
- toChain?.id,
- toChain?.chainType,
- isTronTransfer,
- isTronAvailableToAccount,
- toAccount.address,
- signMessageAsync,
- signTransaction,
+ handleFailure,
]);
const isDisabled =
@@ -627,7 +192,7 @@ export function TransferButton({
bg: theme.colors[colorMode].button.brand.hover,
_disabled: { bg: theme.colors[colorMode].button.disabled },
}}
- onClick={sendTx}
+ onClick={onConfirmSummary}
isDisabled={isDisabled}
>
{isApproveNeeded
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx
new file mode 100644
index 00000000..53c6a717
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Button/TransferConfirmButton.tsx
@@ -0,0 +1,565 @@
+/* eslint-disable no-console */
+import { Button, Flex, useColorMode, useIntl, useTheme } from '@bnb-chain/space';
+import { useCallback, useState } from 'react';
+import { useAccount, usePublicClient, useSignMessage, useWalletClient } from 'wagmi';
+import { parseUnits } from 'viem';
+import { useWallet as useTronWallet } from '@tronweb3/tronwallet-adapter-react-hooks';
+import { useConnection } from '@solana/wallet-adapter-react';
+import { useWallet as useSolanaWallet } from '@solana/wallet-adapter-react';
+import { VersionedTransaction } from '@solana/web3.js';
+
+import { useAppSelector } from '@/modules/store/StoreProvider';
+import { useGetAllowance } from '@/core/contract/hooks/useGetAllowance';
+import { useCBridgeTransferParams } from '@/modules/aggregator/adapters/cBridge/hooks/useCBridgeTransferParams';
+import { useBridgeSDK } from '@/core/hooks/useBridgeSDK';
+import { reportEvent } from '@/core/utils/gtm';
+import { useGetTronAllowance } from '@/modules/aggregator/adapters/meson/hooks/useGetTronAllowance';
+import { useTronTransferInfo } from '@/modules/transfer/hooks/tron/useTronTransferInfo';
+import { utf8ToHex } from '@/core/utils/string';
+import { useTronAccount } from '@/modules/wallet/hooks/useTronAccount';
+import { useWaitForTxReceipt } from '@/core/hooks/useWaitForTxReceipt';
+import {
+ CBRIDGE_ENDPOINT,
+ DEBRIDGE_ENDPOINT,
+ MESON_ENDPOINT,
+ STARGATE_ENDPOINT,
+} from '@/core/constants';
+import { useHandleTxFailure } from '@/modules/aggregator/hooks/useHandleTxFailure';
+
+export const TransferConfirmButton = ({
+ onClose,
+ onOpenSubmittedModal,
+ onOpenFailedModal,
+ onOpenConfirmingModal,
+ onCloseConfirmingModal,
+ setHash,
+ setChosenBridge,
+}: {
+ onClose: () => void;
+ onOpenSubmittedModal: () => void;
+ onOpenFailedModal: () => void;
+ onOpenConfirmingModal: () => void;
+ onCloseConfirmingModal: () => void;
+ setHash: (hash: string | null) => void;
+ setChosenBridge: (bridge: string | null) => void;
+}) => {
+ const { data: walletClient } = useWalletClient();
+ const { args: cBridgeArgs } = useCBridgeTransferParams();
+ const bridgeSDK = useBridgeSDK();
+ const { formatMessage } = useIntl();
+ const theme = useTheme();
+ const { colorMode } = useColorMode();
+
+ const { address } = useAccount();
+ const { address: tronAddress, signTransaction } = useTronWallet();
+ const { isTronAvailableToAccount, isTronTransfer } = useTronTransferInfo();
+ const { signMessageAsync } = useSignMessage();
+ const { handleFailure } = useHandleTxFailure({ onOpenFailedModal });
+
+ const { connection } = useConnection();
+ const { sendTransaction: sendSolanaTransaction } = useSolanaWallet();
+
+ const sendValue = useAppSelector((state) => state.transfer.sendValue);
+ const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
+ const selectedToken = useAppSelector((state) => state.transfer.selectedToken);
+ const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
+ const isTransferable = useAppSelector((state) => state.transfer.isTransferable);
+ const toToken = useAppSelector((state) => state.transfer.toToken);
+ const fromChain = useAppSelector((state) => state.transfer.fromChain);
+ const toChain = useAppSelector((state) => state.transfer.toChain);
+ const toAccount = useAppSelector((state) => state.transfer.toAccount);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const publicClient = usePublicClient({ chainId: fromChain?.id }) as any;
+ const toPublicClient = usePublicClient({ chainId: toChain?.id }) as any;
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { allowance } = useGetAllowance({
+ tokenAddress: selectedToken?.address as `0x${string}`,
+ sender: transferActionInfo?.bridgeAddress as `0x${string}`,
+ });
+
+ const tronAllowance = useGetTronAllowance();
+ const { isConnected: isEvmConnected } = useAccount();
+ const { isConnected: isTronConnected } = useTronAccount();
+ const { waitForTxReceipt } = useWaitForTxReceipt();
+
+ const sendTx = useCallback(async () => {
+ if (
+ !selectedToken ||
+ !transferActionInfo?.bridgeType ||
+ (!transferActionInfo?.bridgeAddress && fromChain?.chainType !== 'solana') ||
+ ((!walletClient ||
+ !publicClient ||
+ !address ||
+ (allowance === null &&
+ selectedToken?.address !== '0x0000000000000000000000000000000000000000') ||
+ !isEvmConnected) &&
+ fromChain?.chainType !== 'tron' &&
+ fromChain?.chainType !== 'solana') ||
+ ((!isTronConnected || !tronAddress || tronAllowance === null) &&
+ fromChain?.chainType === 'tron')
+ ) {
+ return;
+ }
+
+ try {
+ setHash(null);
+ setChosenBridge('');
+ setIsLoading(true);
+
+ onClose(); // Close summary modal
+ onOpenConfirmingModal();
+
+ reportEvent({
+ id: 'click_bridge_goal',
+ params: {
+ item_name: 'Send',
+ },
+ });
+
+ if (transferActionInfo.bridgeType === 'cBridge' && cBridgeArgs && fromChain && address) {
+ try {
+ const isValidToken = await bridgeSDK.cBridge.validateCBridgeToken({
+ isPegged: selectedToken.isPegged,
+ fromChainId: fromChain.id,
+ fromTokenAddress: selectedToken?.cBridge?.raw?.token.address as `0x${string}`,
+ fromTokenSymbol: selectedToken?.cBridge?.raw?.token?.symbol as string,
+ fromTokenDecimals: selectedToken.cBridge?.raw?.token.decimal as number,
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ toChainId: toChain?.id,
+ toTokenAddress: toToken?.cBridge?.raw?.token.address as `0x${string}`,
+ toTokenSymbol: toToken?.cBridge?.raw?.token.symbol,
+ toTokenDecimals: toToken?.cBridge?.raw?.token.decimal as number,
+ amount: Number(sendValue),
+ cBridgeEndpoint: `${CBRIDGE_ENDPOINT}/getTransferConfigsForAll`,
+ });
+
+ if (!isValidToken) {
+ handleFailure({
+ fromTokenAddress: selectedToken.address as `0x${string}`,
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ fromChainId: fromChain.id,
+ isPegged: selectedToken.isPegged,
+ fromTokenSymbol: selectedToken.symbol,
+ toChainId: toChain?.id,
+ toTokenAddress: toToken?.cBridge?.raw?.token.address as `0x${string}`,
+ toTokenSymbol: toToken?.cBridge?.raw?.token.symbol,
+ decimals: selectedToken.decimals,
+ amount: Number(sendValue),
+ message: `(Token Validation Failed) - Invalid cBridge token!!`,
+ });
+ return;
+ }
+ const cBridgeHash = await bridgeSDK.cBridge.sendToken({
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ walletClient: walletClient as any,
+ publicClient,
+ bridgeAddress: transferActionInfo.bridgeAddress as string,
+ fromChainId: fromChain?.id,
+ isPegged: selectedToken.isPegged,
+ address,
+ peggedConfig: selectedToken?.cBridge?.peggedConfig,
+ args: cBridgeArgs.args,
+ });
+ await waitForTxReceipt({
+ publicClient,
+ hash: cBridgeHash,
+ });
+ if (cBridgeHash) {
+ reportEvent({
+ id: 'transaction_bridge_success',
+ params: {
+ item_category: fromChain?.name,
+ item_category2: toChain?.name,
+ token: selectedToken.displaySymbol,
+ value: sendValue,
+ item_variant: 'cBridge',
+ },
+ });
+ onCloseConfirmingModal();
+ setHash(cBridgeHash);
+ setChosenBridge('cBridge');
+ onOpenSubmittedModal();
+ }
+ // eslint-disable-next-line no-console
+ console.log('cBridge tx', cBridgeHash);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log(e);
+ handleFailure(e);
+ }
+ } else if (transferActionInfo.bridgeType === 'deBridge') {
+ try {
+ let deBridgeHash: string | undefined;
+ const isValidToken = await bridgeSDK.deBridge.validateDeBridgeToken({
+ fromChainId: fromChain?.id,
+ toChainId: toChain?.id,
+ fromTokenSymbol: selectedToken.symbol,
+ fromTokenAddress: selectedToken.deBridge?.raw?.address as `0x${string}`,
+ fromTokenDecimals: selectedToken.deBridge?.raw?.decimals as number,
+ toTokenSymbol: toToken?.deBridge?.raw?.symbol,
+ toTokenAddress: toToken?.deBridge?.raw?.address as `0x${string}`,
+ toTokenDecimals: toToken?.deBridge?.raw?.decimals as number,
+ amount: Number(sendValue),
+ fromChainType: fromChain?.chainType,
+ fromBridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ toChainType: toChain?.chainType,
+ deBridgeEndpoint: DEBRIDGE_ENDPOINT,
+ });
+ if (!isValidToken) {
+ handleFailure({
+ message: '(Token Validation Failed) - Invalid deBridge token!!',
+ fromChainId: fromChain?.id,
+ tokenSymbol: selectedToken.symbol,
+ tokenAddress: selectedToken.address as `0x${string}`,
+ });
+ return;
+ }
+ if (fromChain?.chainType === 'evm' && transferActionInfo.value && address) {
+ deBridgeHash = await bridgeSDK.deBridge.sendToken({
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ walletClient: walletClient as any,
+ bridgeAddress: transferActionInfo.bridgeAddress as string,
+ data: transferActionInfo.data as `0x${string}`,
+ amount: BigInt(transferActionInfo.value),
+ address,
+ });
+ await waitForTxReceipt({
+ publicClient,
+ hash: deBridgeHash,
+ });
+ }
+
+ if (fromChain?.chainType === 'solana') {
+ const { blockhash } = await connection.getLatestBlockhash();
+ const data = (transferActionInfo.data as string)?.slice(2);
+ const tx = VersionedTransaction.deserialize(Buffer.from(data, 'hex'));
+
+ tx.message.recentBlockhash = blockhash;
+ deBridgeHash = await sendSolanaTransaction(tx, connection);
+
+ console.log('---solana---');
+ console.log('blockhash: ', blockhash);
+ console.log('data:', data);
+ console.log('tx:', tx);
+ console.log('hash:', deBridgeHash);
+ }
+
+ if (deBridgeHash) {
+ reportEvent({
+ id: 'transaction_bridge_success',
+ params: {
+ item_category: fromChain?.name,
+ item_category2: toChain?.name,
+ token: selectedToken.displaySymbol,
+ value: sendValue,
+ item_variant: 'deBridge',
+ },
+ });
+ onCloseConfirmingModal();
+ setChosenBridge('deBridge');
+ setHash(deBridgeHash);
+ onOpenSubmittedModal();
+ }
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log(e);
+ handleFailure(e);
+ }
+ } else if (transferActionInfo.bridgeType === 'stargate' && address) {
+ const isValidToken = await bridgeSDK.stargate.validateStargateToken({
+ fromBridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ toBridgeAddress: toToken?.stargate?.raw?.address as `0x${string}`,
+ fromTokenAddress: selectedToken?.stargate?.raw?.token?.address as `0x${string}`,
+ fromTokenSymbol: selectedToken?.stargate?.raw?.token?.symbol as string,
+ fromTokenDecimals: selectedToken?.stargate?.raw?.token?.decimals as number,
+ fromChainId: fromChain?.id,
+ toTokenAddress: toToken?.stargate?.raw?.token?.address as `0x${string}`,
+ toTokenSymbol: toToken?.stargate?.raw?.token?.symbol as string,
+ toTokenDecimals: toToken?.stargate?.raw?.token?.decimals as number,
+ toChainId: toChain?.id,
+ amount: Number(sendValue),
+ dstEndpointId: toToken?.stargate?.raw?.endpointID as number,
+ toPublicClient,
+ fromPublicClient: publicClient,
+ stargateEndpoint: STARGATE_ENDPOINT,
+ });
+ if (!isValidToken) {
+ handleFailure({
+ messages: '(Token Validation Failed) - Invalid Stargate token!!',
+ fromChainId: fromChain?.id,
+ tokenAddress: selectedToken.address as `0x${string}`,
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ tokenSymbol: selectedToken.symbol,
+ });
+ return;
+ }
+ const stargateHash = await bridgeSDK.stargate.sendToken({
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ walletClient: walletClient as any,
+ publicClient,
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ tokenAddress: selectedToken.address as `0x${string}`,
+ endPointId: toToken?.stargate?.raw?.endpointID as number,
+ receiver: address,
+ amount: parseUnits(sendValue, selectedToken.decimals),
+ });
+ if (stargateHash) {
+ reportEvent({
+ id: 'transaction_bridge_success',
+ params: {
+ item_category: fromChain?.name,
+ item_category2: toChain?.name,
+ token: selectedToken.displaySymbol,
+ value: sendValue,
+ item_variant: 'stargate',
+ },
+ });
+ onCloseConfirmingModal();
+ setChosenBridge('stargate');
+ setHash(stargateHash);
+ onOpenSubmittedModal();
+ }
+ } else if (transferActionInfo.bridgeType === 'layerZero' && address) {
+ // check layerZero token address
+ const isValidToken = await bridgeSDK.layerZero.validateLayerZeroToken({
+ fromPublicClient: publicClient,
+ toPublicClient,
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ fromTokenAddress: selectedToken.layerZero?.raw?.address as `0x${string}`,
+ fromTokenSymbol: selectedToken.layerZero?.raw?.symbol as string,
+ fromTokenDecimals: selectedToken.layerZero?.raw?.decimals as number,
+ toTokenAddress: toToken?.layerZero?.raw?.address as `0x${string}`,
+ toTokenDecimals: toToken?.layerZero?.raw?.decimals as number,
+ toTokenSymbol: toToken?.layerZero?.raw?.symbol as string,
+ toBridgeAddress: toToken?.layerZero?.raw?.bridgeAddress as `0x${string}`,
+ dstEndpoint: toToken?.layerZero?.raw?.endpointID as number,
+ amount: Number(sendValue),
+ });
+ if (!isValidToken) {
+ handleFailure({
+ messages: '(Token Validation Failed) - Invalid LayerZero token!!',
+ fromChainId: fromChain?.id,
+ tokenAddress: selectedToken.layerZero?.raw?.address as `0x${string}`,
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ tokenSymbol: selectedToken.symbol,
+ });
+ return;
+ }
+ const layerZeroHash = await bridgeSDK.layerZero.sendToken({
+ bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`,
+ dstEndpoint: toToken?.layerZero?.raw?.endpointID as number,
+ userAddress: address,
+ amount: parseUnits(sendValue, selectedToken.decimals),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ walletClient: walletClient as any,
+ publicClient,
+ });
+ if (layerZeroHash) {
+ reportEvent({
+ id: 'transaction_bridge_success',
+ params: {
+ item_category: fromChain?.name,
+ item_category2: toChain?.name,
+ token: selectedToken.displaySymbol,
+ value: sendValue,
+ item_variant: 'layerZero',
+ },
+ });
+ onCloseConfirmingModal();
+ setChosenBridge('layerZero');
+ setHash(layerZeroHash);
+ onOpenSubmittedModal();
+ }
+ } else if (transferActionInfo.bridgeType === 'meson') {
+ const isValidToken = await bridgeSDK.meson.validateMesonToken({
+ fromChainId: fromChain?.id,
+ toChainId: toChain?.id,
+ fromTokenAddress:
+ selectedToken.meson?.raw?.addr ?? '0x0000000000000000000000000000000000000000',
+ fromTokenSymbol: selectedToken.meson?.raw?.id as string,
+ fromTokenDecimals: selectedToken.meson?.raw?.decimals as number,
+ fromChainType: fromChain?.chainType,
+ toChainType: toChain?.chainType,
+ toTokenAddress: toToken?.meson?.raw?.addr ?? '0x0000000000000000000000000000000000000000',
+ toTokenSymbol: toToken?.meson?.raw?.id,
+ toTokenDecimals: toToken?.meson?.raw?.decimals,
+ amount: Number(sendValue),
+ mesonEndpoint: MESON_ENDPOINT,
+ });
+ if (!isValidToken) {
+ handleFailure({
+ message: '(Token Validation Failed) Invalid Meson token!!',
+ fromChainId: fromChain?.id,
+ tokenAddress: selectedToken.address as `0x${string}`,
+ tokenSymbol: selectedToken.symbol,
+ });
+ return;
+ }
+ let fromAddress = '';
+ let toAddress = '';
+ let msg = '';
+ let signature = '';
+
+ if (fromChain?.chainType === 'tron' && tronAddress) {
+ fromAddress = tronAddress;
+ } else if (fromChain?.chainType !== 'tron' && address) {
+ fromAddress = address;
+ }
+
+ if (isTronTransfer && isTronAvailableToAccount && toAccount?.address) {
+ toAddress = toAccount.address;
+ } else if (address) {
+ toAddress = address;
+ }
+
+ // get unsigned message
+ const unsignedMessage = await bridgeSDK.meson.getUnsignedMessage({
+ fromToken: `${fromChain?.meson?.raw?.id}:${selectedToken?.meson?.raw?.id}`,
+ toToken: `${toChain?.meson?.raw?.id}:${toToken?.meson?.raw?.id}`,
+ amount: sendValue,
+ fromAddress: fromAddress,
+ recipient: toAddress,
+ });
+
+ if (unsignedMessage?.result) {
+ const result = unsignedMessage.result;
+ const encodedData = result.encoded;
+ const message = result.signingRequest.message;
+
+ if (fromChain?.chainType === 'tron') {
+ const hexTronHeader = utf8ToHex('\x19TRON Signed Message:\n32');
+ msg = message.replace(hexTronHeader, '');
+ } else {
+ const hexEthHeader = utf8ToHex('\x19Ethereum Signed Message:\n52');
+ msg = message.replace(hexEthHeader, '');
+ }
+
+ if (fromChain?.chainType != 'tron') {
+ signature = await signMessageAsync({
+ account: address,
+ message: {
+ raw: msg as `0x${string}`,
+ },
+ });
+ } else {
+ // TODO
+ signature = String(await signTransaction(msg as any));
+ }
+
+ const swapId = await bridgeSDK.meson.sendToken({
+ fromAddress: fromAddress,
+ recipient: toAddress,
+ signature: signature,
+ encodedData: encodedData,
+ });
+
+ // eslint-disable-next-line no-console
+ console.log('Meson swap id', swapId);
+ if (swapId?.result?.swapId) {
+ setChosenBridge('meson');
+ setHash(swapId?.result?.swapId);
+ }
+ if (swapId?.error) {
+ throw new Error(swapId?.error.message);
+ }
+
+ reportEvent({
+ id: 'transaction_bridge_success',
+ params: {
+ item_category: fromChain?.name,
+ item_category2: toChain?.name,
+ token: selectedToken.displaySymbol,
+ value: sendValue,
+ item_variant: 'meson',
+ },
+ });
+
+ onCloseConfirmingModal();
+ onOpenSubmittedModal();
+ } else {
+ throw new Error(unsignedMessage?.error.message);
+ }
+ }
+ } catch (e: any) {
+ // eslint-disable-next-line no-console
+ console.error(e, e.message);
+ handleFailure(e);
+ } finally {
+ onCloseConfirmingModal();
+ setIsLoading(false);
+ }
+ }, [
+ onClose,
+ selectedToken,
+ transferActionInfo?.bridgeType,
+ transferActionInfo?.bridgeAddress,
+ transferActionInfo?.value,
+ transferActionInfo?.data,
+ fromChain,
+ walletClient,
+ publicClient,
+ toPublicClient,
+ address,
+ allowance,
+ isEvmConnected,
+ isTronConnected,
+ tronAddress,
+ tronAllowance,
+ toChain?.name,
+ toChain?.meson?.raw?.id,
+ sendValue,
+ onOpenFailedModal,
+ setHash,
+ setChosenBridge,
+ onOpenConfirmingModal,
+ cBridgeArgs,
+ bridgeSDK.cBridge,
+ bridgeSDK.deBridge,
+ bridgeSDK.stargate,
+ bridgeSDK.layerZero,
+ bridgeSDK.meson,
+ onCloseConfirmingModal,
+ onOpenSubmittedModal,
+ connection,
+ sendSolanaTransaction,
+ toToken?.stargate?.raw,
+ toToken?.layerZero?.raw,
+ toToken?.meson?.raw,
+ toToken?.cBridge?.raw,
+ toToken?.deBridge?.raw,
+ toChain?.id,
+ toChain?.chainType,
+ isTronTransfer,
+ isTronAvailableToAccount,
+ toAccount.address,
+ signMessageAsync,
+ signTransaction,
+ handleFailure,
+ ]);
+
+ const isFeeLoading = isLoading || isGlobalFeeLoading || !transferActionInfo || !isTransferable;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/LoadingImg/RefreshingIcon.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/LoadingImg/RefreshingIcon.tsx
index f6ed536e..6783e8c6 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/components/LoadingImg/RefreshingIcon.tsx
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/LoadingImg/RefreshingIcon.tsx
@@ -6,6 +6,7 @@ import { useAppSelector } from '@/modules/store/StoreProvider';
export const RefreshingIcon = (props: IconProps) => {
const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
const isRefreshing = useAppSelector((state) => state.transfer.isRefreshing);
+ const randomStr = Math.random().toString(36).substring(7);
return (
{
strokeDashoffset={128.76}
/>
{
>
-
+
) => {
+ const { ...restProps } = props;
+ const { formatMessage } = useIntl();
+
+ return (
+
+ );
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/FeeSummary.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/FeeSummary.tsx
new file mode 100644
index 00000000..361279e0
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/FeeSummary.tsx
@@ -0,0 +1,30 @@
+import { Box, Flex, Skeleton, useColorMode, useTheme } from '@bnb-chain/space';
+import { useMemo } from 'react';
+
+import { FeesInfo } from '@/modules/transfer/components/TransferOverview/RouteInfo/FeesInfo';
+import { useAppSelector } from '@/modules/store/StoreProvider';
+import { EstimatedArrivalTime } from '@/modules/transfer/components/TransferOverview/RouteInfo/EstimatedArrivalTime';
+
+export const FeeSummary = () => {
+ const theme = useTheme();
+ const { colorMode } = useColorMode();
+ const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
+ const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
+ const bridgeType = useMemo(() => transferActionInfo?.bridgeType, [transferActionInfo]);
+
+ return (
+
+ {isGlobalFeeLoading ? (
+
+
+
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ );
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/TokenInfo.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/TokenInfo.tsx
new file mode 100644
index 00000000..4ce37773
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/TokenInfo.tsx
@@ -0,0 +1,55 @@
+import { Box, Flex, Skeleton, theme, useColorMode } from '@bnb-chain/space';
+
+import { IconImage } from '@/core/components/IconImage';
+import { useAppSelector } from '@/modules/store/StoreProvider';
+
+export const TokenInfo = ({
+ chainIconUrl,
+ tokenIconUrl,
+ chainName,
+ amount,
+ tokenSymbol,
+}: {
+ chainIconUrl?: string;
+ tokenIconUrl?: string;
+ chainName?: string;
+ amount?: string;
+ tokenSymbol?: string;
+}) => {
+ const { colorMode } = useColorMode();
+ const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
+
+ return (
+
+
+
+
+
+
+
+ {chainName}
+
+
+ {isGlobalFeeLoading ? (
+
+ ) : (
+
+ {amount ?? '--'} {tokenSymbol}
+
+ )}
+
+ );
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/TransferSummary.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/TransferSummary.tsx
new file mode 100644
index 00000000..fe3d1e39
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/TransferSummary.tsx
@@ -0,0 +1,89 @@
+import { Flex, Link, useBreakpointValue, useColorMode, useIntl, useTheme } from '@bnb-chain/space';
+import { useMemo } from 'react';
+
+import { useAppSelector } from '@/modules/store/StoreProvider';
+import { useGetReceiveAmount } from '@/modules/transfer/hooks/useGetReceiveAmount';
+import { useToTokenInfo } from '@/modules/transfer/hooks/useToTokenInfo';
+import { TransferToIcon } from '@/core/components/icons/TransferToIcon';
+import { TokenInfo } from '@/modules/transfer/components/Modal/TransactionSummaryModal/TokenInfo';
+import { formatTokenUrl } from '@/core/utils/string';
+import { WarningMessage } from '@/modules/transfer/components/TransferWarningMessage/WarningMessage';
+import { formatAppAddress } from '@/core/utils/address';
+
+export const TransferSummary = () => {
+ const { colorMode } = useColorMode();
+ const theme = useTheme();
+ const { getSortedReceiveAmount } = useGetReceiveAmount();
+ const { formatMessage } = useIntl();
+ const isBase = useBreakpointValue({ base: true, md: false }) ?? false;
+
+ const fromChain = useAppSelector((state) => state.transfer.fromChain);
+ const toChain = useAppSelector((state) => state.transfer.toChain);
+ const selectedToken = useAppSelector((state) => state.transfer.selectedToken);
+ const sendValue = useAppSelector((state) => state.transfer.sendValue);
+ const transferActionInfo = useAppSelector((state) => state.transfer.transferActionInfo);
+ const { toTokenInfo } = useToTokenInfo();
+
+ const receiveAmt = useMemo(() => {
+ if (!Number(sendValue)) return null;
+ if (transferActionInfo && transferActionInfo.bridgeType) {
+ const bridgeType = transferActionInfo.bridgeType;
+ const receiveValue = getSortedReceiveAmount();
+ return Number(receiveValue[bridgeType].value);
+ }
+ return null;
+ }, [getSortedReceiveAmount, transferActionInfo, sendValue]);
+
+ return (
+
+
+
+
+
+ {formatMessage({ id: 'transfer.warning.confirm.to.address' })}
+
+ {isBase
+ ? formatAppAddress({ address: toTokenInfo?.address, isTruncated: true })
+ : toTokenInfo?.address}
+
+
+ }
+ />
+
+ );
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/index.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/index.tsx
new file mode 100644
index 00000000..b85ded40
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/Modal/TransactionSummaryModal/index.tsx
@@ -0,0 +1,123 @@
+import { CloseIcon } from '@bnb-chain/icons';
+import {
+ Flex,
+ Modal,
+ ModalContent,
+ ModalOverlay,
+ SkeletonCircle,
+ useColorMode,
+ useIntl,
+ useTheme,
+} from '@bnb-chain/space';
+
+import { TransferSummary } from '@/modules/transfer/components/Modal/TransactionSummaryModal/TransferSummary';
+import { TransferConfirmButton } from '@/modules/transfer/components/Button/TransferConfirmButton';
+import { FeeSummary } from '@/modules/transfer/components/Modal/TransactionSummaryModal/FeeSummary';
+import { RefreshingButton } from '@/modules/transfer/components/Button/RefreshingButton';
+import { useAppSelector } from '@/modules/store/StoreProvider';
+
+interface ITransactionSummaryModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onOpenSubmittedModal: () => void;
+ onOpenFailedModal: () => void;
+ onOpenApproveModal: () => void;
+ onOpenConfirmingModal: () => void;
+ onCloseConfirmingModal: () => void;
+ setHash: (hash: string | null) => void;
+ setChosenBridge: (bridge: string | null) => void;
+}
+
+export function TransactionSummaryModal(props: ITransactionSummaryModalProps) {
+ const {
+ isOpen,
+ onClose,
+ onOpenSubmittedModal,
+ onOpenFailedModal,
+ onOpenConfirmingModal,
+ onCloseConfirmingModal,
+ setHash,
+ setChosenBridge,
+ } = props;
+
+ const theme = useTheme();
+ const { colorMode } = useColorMode();
+ const { formatMessage } = useIntl();
+ const isGlobalLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
+
+ return (
+
+
+
+
+
+ {isGlobalLoading ? (
+
+ ) : (
+
+ )}
+
+ {formatMessage({ id: 'modal.summary.title' })}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/ReceiveInfo/index.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/ReceiveInfo/index.tsx
index b29f63ab..71b7b5c8 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/components/ReceiveInfo/index.tsx
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/ReceiveInfo/index.tsx
@@ -52,7 +52,6 @@ export const ReceiveInfo = ({ onOpen }: ReceiveInfoProps) => {
const isGlobalFeeLoading = useAppSelector((state) => state.transfer.isGlobalFeeLoading);
const sendValue = useAppSelector((state) => state.transfer.sendValue);
const selectedToken = useAppSelector((state) => state.transfer.selectedToken);
- const routeFees = useAppSelector((state) => state.transfer.routeFees);
const estimatedAmount = useAppSelector((state) => state.transfer.estimatedAmount);
const isBase = useBreakpointValue({ base: true, lg: false }) ?? false;
@@ -73,28 +72,6 @@ export const ReceiveInfo = ({ onOpen }: ReceiveInfoProps) => {
const { allowedSendAmount: STAllowedSendAmount, isAllowSendError: STIsAllowSendError } =
useGetStargateFees();
- const feeDetails = useMemo(() => {
- let feeContent = '';
- const feeBreakdown = [];
- if (bridgeType === 'cBridge' && routeFees?.['cBridge']) {
- feeContent = routeFees?.['cBridge'].summary;
- feeBreakdown.push(...routeFees?.['cBridge'].breakdown);
- } else if (bridgeType === 'deBridge' && routeFees?.['deBridge']) {
- feeContent = routeFees?.['deBridge'].summary;
- feeBreakdown.push(...routeFees?.['deBridge'].breakdown);
- } else if (bridgeType === 'stargate' && routeFees?.['stargate']) {
- feeContent = routeFees?.['stargate'].summary;
- feeBreakdown.push(...routeFees?.['stargate'].breakdown);
- } else if (bridgeType === 'layerZero' && routeFees?.['layerZero']) {
- feeContent = routeFees?.['layerZero'].summary;
- feeBreakdown.push(...routeFees?.['layerZero'].breakdown);
- } else if (bridgeType === 'meson' && routeFees?.['meson']) {
- feeContent = routeFees?.['meson'].summary;
- feeBreakdown.push(...routeFees?.['meson'].breakdown);
- }
- return { summary: feeContent ? feeContent : '--', breakdown: feeBreakdown };
- }, [bridgeType, routeFees]);
-
const allowedAmtContent = useMemo(() => {
if (cBridgeAllowedAmt && transferActionInfo?.bridgeType === 'cBridge') {
return cBridgeAllowedAmt;
@@ -200,13 +177,19 @@ export const ReceiveInfo = ({ onOpen }: ReceiveInfoProps) => {
},
}}
>
-
+
}
{bridgeType && (
{
)}
-
+
{
const [hash, setHash] = useState(null);
const [chosenBridge, setChosenBridge] = useState(null);
+ const { formatMessage } = useIntl();
+
+ const isFailedGetQuoteModalOpen = useAppSelector(
+ (state) => state.transfer.isFailedGetQuoteModalOpen,
+ );
+ const isSummaryModalOpen = useAppSelector((state) => state.transfer.isSummaryModalOpen);
const {
isOpen: isSubmittedModalOpen,
@@ -32,7 +45,8 @@ export const TransferButtonGroup = () => {
onOpen: onOpenConfirmingModal,
onClose: onCloseConfirmingModal,
} = useDisclosure();
-
+ const { onCloseFailedGetQuoteModal } = useFailGetQuoteModal();
+ const { onCloseSummaryModal, onOpenSummaryModal } = useSummaryModal();
return (
<>
{
>
+
{
onCloseConfirmingModal={onCloseConfirmingModal}
/>
+
+
>
);
};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/TransferOverview/RouteInfo/FeesInfo.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/TransferOverview/RouteInfo/FeesInfo.tsx
index 836fc034..1225c56e 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/components/TransferOverview/RouteInfo/FeesInfo.tsx
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/TransferOverview/RouteInfo/FeesInfo.tsx
@@ -1,21 +1,44 @@
import { Box, useColorMode, useIntl, useTheme } from '@bnb-chain/space';
+import { useMemo } from 'react';
import { FeesIcon } from '@/core/components/icons/FeesIcon';
import { FeeBreakdown } from '@/modules/transfer/components/TransferOverview/RouteInfo/FeeBreakdown';
import { InfoTooltip } from '@/core/components/InfoTooltip';
-import { IFeeBreakDown } from '@/modules/transfer/types';
+import { useAppSelector } from '@/modules/store/StoreProvider';
interface FeesInfoProps {
- summary?: string;
- breakdown?: IFeeBreakDown;
bridgeType?: string;
isError?: boolean;
}
-export const FeesInfo = ({ summary, breakdown, bridgeType, isError }: FeesInfoProps) => {
+export const FeesInfo = ({ bridgeType, isError }: FeesInfoProps) => {
const theme = useTheme();
const { colorMode } = useColorMode();
const { formatMessage } = useIntl();
+
+ const routeFees = useAppSelector((state) => state.transfer.routeFees);
+
+ const feeDetails = useMemo(() => {
+ let feeContent = '';
+ const feeBreakdown = [];
+ if (bridgeType === 'cBridge' && routeFees?.['cBridge']) {
+ feeContent = routeFees?.['cBridge'].summary;
+ feeBreakdown.push(...routeFees?.['cBridge'].breakdown);
+ } else if (bridgeType === 'deBridge' && routeFees?.['deBridge']) {
+ feeContent = routeFees?.['deBridge'].summary;
+ feeBreakdown.push(...routeFees?.['deBridge'].breakdown);
+ } else if (bridgeType === 'stargate' && routeFees?.['stargate']) {
+ feeContent = routeFees?.['stargate'].summary;
+ feeBreakdown.push(...routeFees?.['stargate'].breakdown);
+ } else if (bridgeType === 'layerZero' && routeFees?.['layerZero']) {
+ feeContent = routeFees?.['layerZero'].summary;
+ feeBreakdown.push(...routeFees?.['layerZero'].breakdown);
+ } else if (bridgeType === 'meson' && routeFees?.['meson']) {
+ feeContent = routeFees?.['meson'].summary;
+ feeBreakdown.push(...routeFees?.['meson'].breakdown);
+ }
+ return { summary: feeContent ? feeContent : '--', breakdown: feeBreakdown };
+ }, [bridgeType, routeFees]);
return (
- {summary}
+ {feeDetails.summary}
0
- ? breakdown.map((fee, index) => {
+ feeDetails.breakdown && feeDetails.breakdown?.length > 0
+ ? feeDetails.breakdown.map((fee, index) => {
return fee.value !== '0' && fee.value !== null ? (
{
+ const { colorMode } = useColorMode();
+ const theme = useTheme();
+ return (
+
+
+ {text}
+
+ );
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/components/TransferWarningMessage/index.tsx b/packages/canonical-bridge-widget/src/modules/transfer/components/TransferWarningMessage/index.tsx
new file mode 100644
index 00000000..15c32f46
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/components/TransferWarningMessage/index.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { MIN_SOL_TO_ENABLED_TX } from '@/core/constants';
+import { useAppSelector } from '@/modules/store/StoreProvider';
+import { useSolanaBalance } from '@/modules/wallet/hooks/useSolanaBalance';
+import { WarningMessage } from '@/modules/transfer/components/TransferWarningMessage/WarningMessage';
+
+interface ITransferWarningMessageProps {
+ text: React.ReactNode;
+}
+
+export const TransferWarningMessage = ({ text, ...restProps }: ITransferWarningMessageProps) => {
+ const { data } = useSolanaBalance();
+ const solBalance = Number(data?.formatted);
+ const fromChain = useAppSelector((state) => state.transfer.fromChain);
+
+ if (fromChain?.chainType === 'solana' && solBalance < MIN_SOL_TO_ENABLED_TX) {
+ return ;
+ }
+ return null;
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFailGetQuoteModal.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFailGetQuoteModal.ts
new file mode 100644
index 00000000..824f664d
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useFailGetQuoteModal.ts
@@ -0,0 +1,20 @@
+import { useCallback } from 'react';
+
+import { useAppDispatch } from '@/modules/store/StoreProvider';
+import { setIsFailedGetQuoteModalOpen } from '@/modules/transfer/action';
+
+export const useFailGetQuoteModal = () => {
+ const dispatch = useAppDispatch();
+
+ const onOpenFailedGetQuoteModal = useCallback(() => {
+ dispatch(setIsFailedGetQuoteModalOpen(true));
+ }, [dispatch]);
+
+ const onCloseFailedGetQuoteModal = useCallback(() => {
+ dispatch(setIsFailedGetQuoteModalOpen(false));
+ }, [dispatch]);
+ return {
+ onOpenFailedGetQuoteModal,
+ onCloseFailedGetQuoteModal,
+ };
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useSummaryModal.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useSummaryModal.ts
new file mode 100644
index 00000000..fdfad764
--- /dev/null
+++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/modal/useSummaryModal.ts
@@ -0,0 +1,20 @@
+import { useCallback } from 'react';
+
+import { useAppDispatch } from '@/modules/store/StoreProvider';
+import { setIsSummaryModalOpen } from '@/modules/transfer/action';
+
+export const useSummaryModal = () => {
+ const dispatch = useAppDispatch();
+
+ const onOpenSummaryModal = useCallback(() => {
+ dispatch(setIsSummaryModalOpen(true));
+ }, [dispatch]);
+
+ const onCloseSummaryModal = useCallback(() => {
+ dispatch(setIsSummaryModalOpen(false));
+ }, [dispatch]);
+ return {
+ onOpenSummaryModal,
+ onCloseSummaryModal,
+ };
+};
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts
index 27044a96..e8313224 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts
+++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useInputValidation.ts
@@ -32,15 +32,18 @@ export const useInputValidation = () => {
if (!decimal || !value) {
return null;
}
+ // check if send amount is smaller than lowest possible token amount
if (Number(value) < Math.pow(10, -decimal)) {
return {
text: `The amount is too small. Please enter a valid amount to transfer.`,
isError: true,
};
}
+ // check if send amount is greater than token balance
if (!!balance && value > balance) {
return { text: `You have insufficient balance`, isError: true };
}
+ // check Stargate max amount
if (estimatedAmount?.stargate && bridgeType === 'stargate' && value) {
const stargateMax = formatUnits(estimatedAmount.stargate[0].maxAmountLD, decimal);
if (value > Number(stargateMax)) {
@@ -54,7 +57,7 @@ export const useInputValidation = () => {
if (!!balance) {
if (fromChain?.chainType === 'solana' && solBalance < MIN_SOL_TO_ENABLED_TX) {
return {
- text: `You should have at least ${MIN_SOL_TO_ENABLED_TX} SOL in your balance to perform this trade.`,
+ text: ``, // Error message has been moved to send button section
isError: true,
};
} else {
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePreSelectRoute.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePreSelectRoute.ts
index b29678cb..6fa22b93 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePreSelectRoute.ts
+++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/usePreSelectRoute.ts
@@ -4,11 +4,12 @@ import { useCallback } from 'react';
import { useAppDispatch, useAppSelector } from '@/modules/store/StoreProvider';
import { setTransferActionInfo } from '@/modules/transfer/action';
import { useCBridgeTransferParams } from '@/modules/aggregator/adapters/cBridge/hooks/useCBridgeTransferParams';
+import { useFailGetQuoteModal } from '@/modules/transfer/hooks/modal/useFailGetQuoteModal';
export const usePreSelectRoute = () => {
const dispatch = useAppDispatch();
const { bridgeAddress: cBridgeAddress } = useCBridgeTransferParams();
-
+ const { onOpenFailedGetQuoteModal } = useFailGetQuoteModal();
const selectedToken = useAppSelector((state) => state.transfer.selectedToken);
const fromChain = useAppSelector((state) => state.transfer.fromChain);
@@ -63,6 +64,9 @@ export const usePreSelectRoute = () => {
bridgeAddress: fromChain?.meson?.raw?.address as `0x${string}`,
}),
);
+ } else {
+ // Can not find the route
+ onOpenFailedGetQuoteModal();
}
},
[
@@ -71,6 +75,7 @@ export const usePreSelectRoute = () => {
selectedToken?.stargate?.raw?.address,
cBridgeAddress,
fromChain,
+ onOpenFailedGetQuoteModal,
],
);
diff --git a/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts b/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts
index 39b63aa9..cc36ffab 100644
--- a/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts
+++ b/packages/canonical-bridge-widget/src/modules/transfer/reducer.ts
@@ -24,10 +24,13 @@ export interface ITransferState {
estimatedAmount?: IEstimatedAmount;
routeFees?: IRouteFees;
isToAddressChecked?: boolean;
+ isManuallyReload: boolean;
toAccount: {
address?: string;
};
isRoutesModalOpen: boolean;
+ isFailedGetQuoteModalOpen: boolean;
+ isSummaryModalOpen: boolean;
}
const initStates: ITransferState = {
@@ -52,6 +55,9 @@ const initStates: ITransferState = {
address: '',
},
isRoutesModalOpen: false,
+ isManuallyReload: false,
+ isFailedGetQuoteModalOpen: false,
+ isSummaryModalOpen: false,
};
export default createReducer(initStates, (builder) => {
@@ -137,4 +143,17 @@ export default createReducer(initStates, (builder) => {
...state,
isRoutesModalOpen: payload,
}));
+ builder.addCase(actions.setIsManuallyReload, (state, { payload }) => ({
+ ...state,
+ isManuallyReload: payload,
+ }));
+
+ builder.addCase(actions.setIsFailedGetQuoteModalOpen, (state, { payload }) => ({
+ ...state,
+ isFailedGetQuoteModalOpen: payload,
+ }));
+ builder.addCase(actions.setIsSummaryModalOpen, (state, { payload }) => ({
+ ...state,
+ isSummaryModalOpen: payload,
+ }));
});