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, + })); });