diff --git a/packages/canonical-bridge-widget/src/core/constants/index.ts b/packages/canonical-bridge-widget/src/core/constants/index.ts index e5189218..6604baee 100644 --- a/packages/canonical-bridge-widget/src/core/constants/index.ts +++ b/packages/canonical-bridge-widget/src/core/constants/index.ts @@ -15,6 +15,11 @@ export const DEFAULT_ADDRESS = '0x6836CbaCbBd1E798cC56802AC7d8BDf6Da0d0980'; export const DEFAULT_TRON_ADDRESS = 'TTb3A6ASFejJuGcM1UVcRCJA23WGiJKSiY'; export const DEFAULT_SOLANA_ADDRESS = 'J7JYXS8PMMBgfFKP1bqUu7mGgWyWUDL9xqfYujznc61r'; +export const CBRIDGE_ENDPOINT = 'https://cbridge-prod2.celer.app/v2'; +export const DEBRIDGE_ENDPOINT = 'https://deswap.debridge.finance/v1.0'; +export const STARGATE_ENDPOINT = 'https://mainnet.stargate-api.com/v1/metadata?version=v2'; +export const MESON_ENDPOINT = 'https://relayer.meson.fi/api/v1'; + export const nativeTokenMap = { 1: 'ETH', 10: 'ETH', diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/types.ts b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/types.ts index eaa25eb3..fc107639 100644 --- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/types.ts +++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/meson/types.ts @@ -13,3 +13,19 @@ export interface IMesonChain { address: string; // bridge address tokens: IMesonToken[]; } + +interface IMesonTokenLimits { + id: string; + decimals: number; + addr?: `0x${string}`; + min: string; + max: string; +} + +export interface IMesonTokenList { + id: string; + name: string; + chainId: `0x${string}`; + address: `0x${string}`; + tokens: IMesonTokenLimits[]; +} diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/const/index.ts b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/const/index.ts new file mode 100644 index 00000000..d1ae31e5 --- /dev/null +++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/const/index.ts @@ -0,0 +1,24 @@ +export const stargateChainKey: { [key: number]: string } = { + 1: 'ethereum', + 10: 'optimism', + 14: 'flare', + 56: 'bsc', + 137: 'polygon', + 250: 'fantom', + 1088: 'metis', + 1116: 'coredao', + 1329: 'sei', + 1625: 'gravity', + 2222: 'kava', + 5000: 'mantle', + 8217: 'klaytn', + 8453: 'base', + 8822: 'iota', + 42161: 'arbitrum', + 43114: 'avalanche', + 59144: 'linea', + 167000: 'taiko', + 534352: 'scroll', + 1313161554: 'aurora', + 1380012617: 'rarible', +}; diff --git a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/types.ts b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/types.ts index 95ca11c2..5e90f9de 100644 --- a/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/types.ts +++ b/packages/canonical-bridge-widget/src/modules/aggregator/adapters/stargate/types.ts @@ -39,3 +39,46 @@ export interface IStargateBusDriveSettings { maxWaitTime: number; passengersToDrive: number; } + +export interface IStargateBridgeTokenInfo { + stargateType: string; + address: `0x${string}`; + token: { + address: `0x${string}`; + decimals: number; + symbol: string; + }; + lpToken: { + address: `0x${string}`; + decimals: number; + symbol: string; + }; + farm: { + stargateStaking: { + address: `0x${string}`; + rewardTokens: [ + { + address: `0x${string}`; + decimals: number; + symbol: string; + }, + { + address: `0x${string}`; + decimals: number; + symbol: string; + }, + ]; + }; + }; + id: string; + assetId: string; + chainKey: string; + chainName: string; + tokenMessaging: `0x${string}`; + sharedDecimals: number; +} + +export interface IStargateTokenList { + v1: IStargateBridgeTokenInfo[]; + v2: IStargateBridgeTokenInfo[]; +} 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 91876e9f..1c97469d 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 @@ -20,6 +20,7 @@ import { useTronContract } from '@/modules/aggregator/adapters/meson/hooks/useTr import { useSolanaTransferInfo } from '@/modules/transfer/hooks/solana/useSolanaTransferInfo'; import { useTronAccount } from '@/modules/wallet/hooks/useTronAccount'; import { useWaitForTxReceipt } from '@/core/hooks/useWaitForTxReceipt'; +import { useValidateSendToken } from '@/modules/transfer/hooks/useSendTokenValidation'; export function TransferButton({ onOpenSubmittedModal, @@ -83,6 +84,8 @@ export function TransferButton({ const { isConnected: isEvmConnected } = useAccount(); const { isConnected: isTronConnected } = useTronAccount(); const { waitForTxReceipt } = useWaitForTxReceipt(); + const { validateCBridgeToken, validateDeBridgeToken, validateMesonToken, validateStargateToken } = + useValidateSendToken(); const isApproveNeeded = (fromChain?.chainType === 'evm' && @@ -171,6 +174,26 @@ export function TransferButton({ if (transferActionInfo.bridgeType === 'cBridge' && cBridgeArgs && fromChain && address) { try { + const isValid = await validateCBridgeToken({ + tokenAddress: selectedToken.address as `0x${string}`, + bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`, + fromChainId: fromChain.id, + isPegged: selectedToken.isPegged, + tokenSymbol: selectedToken.symbol, + toChainId: toChain?.id, + }); + if (!isValid) { + handleFailure({ + tokenAddress: selectedToken.address as `0x${string}`, + bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`, + fromChainId: fromChain.id, + isPegged: selectedToken.isPegged, + tokenSymbol: selectedToken.symbol, + toChainId: toChain?.id, + message: `Invalid cBridge token!!`, + }); + return; + } const cBridgeHash = await bridgeSDK.cBridge.sendToken({ // eslint-disable-next-line @typescript-eslint/no-explicit-any walletClient: walletClient as any, @@ -212,8 +235,21 @@ export function TransferButton({ } else if (transferActionInfo.bridgeType === 'deBridge') { try { let deBridgeHash: string | undefined; - if (fromChain?.chainType === 'evm' && transferActionInfo.value && address) { + const isValidToken = await validateDeBridgeToken({ + fromChainId: fromChain?.id, + tokenSymbol: selectedToken.symbol, + tokenAddress: selectedToken.address as `0x${string}`, + }); + if (!isValidToken) { + handleFailure({ + message: 'Invalid deBridge token!!', + fromChainId: fromChain?.id, + tokenSymbol: selectedToken.symbol, + tokenAddress: selectedToken.address as `0x${string}`, + }); + return; + } deBridgeHash = await bridgeSDK.deBridge.sendToken({ // eslint-disable-next-line @typescript-eslint/no-explicit-any walletClient: walletClient as any, @@ -265,6 +301,23 @@ export function TransferButton({ handleFailure(e); } } else if (transferActionInfo.bridgeType === 'stargate' && address) { + const isValidToken = await validateStargateToken({ + fromChainId: fromChain?.id, + tokenAddress: selectedToken.address as `0x${string}`, + bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`, + tokenSymbol: selectedToken.symbol, + }); + if (!isValidToken) { + handleFailure({ + messages: '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, @@ -318,6 +371,22 @@ export function TransferButton({ onOpenSubmittedModal(); } } else if (transferActionInfo.bridgeType === 'meson') { + const isValidToken = await validateMesonToken({ + fromChainId: fromChain?.id, + tokenAddress: selectedToken.address as `0x${string}`, + bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`, + tokenSymbol: selectedToken.symbol, + }); + if (!isValidToken) { + handleFailure({ + message: 'Invalid Meson token!!', + fromChainId: fromChain?.id, + tokenAddress: selectedToken.address as `0x${string}`, + bridgeAddress: transferActionInfo.bridgeAddress as `0x${string}`, + tokenSymbol: selectedToken.symbol, + }); + return; + } let fromAddress = ''; let toAddress = ''; let msg = ''; diff --git a/packages/canonical-bridge-widget/src/modules/transfer/hooks/useSendTokenValidation.ts b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useSendTokenValidation.ts new file mode 100644 index 00000000..4149adc5 --- /dev/null +++ b/packages/canonical-bridge-widget/src/modules/transfer/hooks/useSendTokenValidation.ts @@ -0,0 +1,239 @@ +/* eslint-disable no-console */ +import axios from 'axios'; + +import { + CBRIDGE_ENDPOINT, + DEBRIDGE_ENDPOINT, + MESON_ENDPOINT, + STARGATE_ENDPOINT, +} from '@/core/constants'; +import { ICBridgeTransferConfig, IDeBridgeToken, IStargateTokenList } from '@/modules/aggregator'; +import { IMesonTokenList } from '@/modules/aggregator/adapters/meson/types'; +import { stargateChainKey } from '@/modules/aggregator/adapters/stargate/const'; + +interface ICBridgeTokenValidateParams { + isPegged: boolean; + tokenAddress: `0x${string}`; + bridgeAddress: `0x${string}`; + fromChainId?: number; + toChainId?: number; + tokenSymbol: string; +} + +// deBridge only needs to check token address +interface IDeBridgeTokenValidateParams { + tokenAddress: `0x${string}`; + tokenSymbol: string; + fromChainId?: number; +} + +interface IStargateTokenValidateParams { + tokenAddress: `0x${string}`; + bridgeAddress: `0x${string}`; + fromChainId?: number; + tokenSymbol: string; +} + +interface IMesonTokenValidateParams { + fromChainId?: number; + tokenSymbol: string; + tokenAddress: `0x${string}`; + bridgeAddress: `0x${string}`; +} + +export const useValidateSendToken = () => { + // cBridge + const validateCBridgeToken = async ({ + isPegged, + fromChainId, + toChainId, + tokenAddress, + bridgeAddress, + tokenSymbol, + }: ICBridgeTokenValidateParams) => { + try { + if (!fromChainId || !toChainId || !tokenAddress || !bridgeAddress || !tokenSymbol) { + return false; + } + const { data: cBridgeConfig } = await axios.get( + `${CBRIDGE_ENDPOINT}/getTransferConfigsForAll`, + ); + if (!cBridgeConfig) return false; + if (isPegged === true) { + // pegged token + const peggedToken = cBridgeConfig.pegged_pair_configs.filter((pair) => { + return ( + (pair.pegged_deposit_contract_addr === bridgeAddress && + pair?.org_chain_id === fromChainId && + pair?.org_token.token.address === tokenAddress && + pair?.org_token.token.symbol === tokenSymbol && + pair?.pegged_chain_id === toChainId) || + (pair?.pegged_chain_id === fromChainId && + pair.pegged_burn_contract_addr === bridgeAddress && + pair.pegged_token.token.address === tokenAddress && + pair.pegged_token.token.symbol === tokenSymbol && + pair?.org_chain_id === toChainId) + ); + }); + if (!!peggedToken && peggedToken.length > 0) { + console.log('cBridge pegged token info matched', peggedToken); + return true; + } + console.log('Can not find cBridge pegged info'); + console.log('-- isPegged', isPegged); + console.log('-- fromChainId', fromChainId); + console.log('-- tokenAddress', tokenAddress); + console.log('-- bridgeAddress', bridgeAddress); + return false; + } else { + // bridge address + const addressInfo = cBridgeConfig.chains.filter((chain) => { + return chain.id === fromChainId && chain.contract_addr === bridgeAddress; + }); + // token address + const tokenInfo = cBridgeConfig.chain_token[fromChainId].token.filter((t) => { + return ( + t.token.address.toLowerCase() === tokenAddress.toLowerCase() && + t.token.symbol === tokenSymbol + ); + }); + if (addressInfo?.length > 0 && tokenInfo?.length > 0) { + console.log('cBridge pool info matched', addressInfo, tokenInfo); + return true; + } else { + console.log('Can not find cBridge pool info'); + console.log('-- isPegged', isPegged); + console.log('-- fromChainId', fromChainId); + console.log('-- tokenAddress', tokenAddress); + console.log('-- bridgeAddress', bridgeAddress); + return false; + } + } + } catch (error) { + // eslint-disable-next-line no-console + console.log('cBridge token address validation error', error); + return false; + } + }; + + // deBridge + const validateDeBridgeToken = async ({ + fromChainId, + tokenSymbol, + tokenAddress, + }: IDeBridgeTokenValidateParams) => { + try { + if (!fromChainId || !tokenAddress || !tokenSymbol) return false; + const { data: deBridgeConfig } = await axios.get<{ + tokens: { [key: string]: IDeBridgeToken }; + }>(`${DEBRIDGE_ENDPOINT}/token-list?chainId=${fromChainId}`); + + if (!deBridgeConfig?.tokens) return false; + const tokenInfo = deBridgeConfig.tokens[tokenAddress.toLowerCase()]; + if (!!tokenInfo && tokenInfo?.address === tokenAddress && tokenInfo?.symbol === tokenSymbol) { + console.log('deBridge token info matched', tokenInfo); + return true; + } + console.log('Could not find deBridge token info'); + console.log('-- fromChainId', fromChainId); + console.log('-- tokenSymbol', tokenSymbol); + console.log('-- tokenAddress', tokenAddress); + return false; + } catch (error: any) { + // eslint-disable-next-line no-console + console.log('deBridge token validation error', error); + return false; + } + }; + + // Stargate + const validateStargateToken = async ({ + fromChainId, + tokenAddress, + bridgeAddress, + tokenSymbol, + }: IStargateTokenValidateParams) => { + try { + if (!fromChainId || !tokenAddress || !bridgeAddress || !tokenSymbol) return false; + const { data: stargateConfig } = await axios.get<{ data: IStargateTokenList }>( + `${STARGATE_ENDPOINT}`, + ); + + // Get chain name by chain id + const chainKey = stargateChainKey[fromChainId] ?? ''; + + if (!chainKey) return false; + if (!stargateConfig) return false; + + const tokenInfo = stargateConfig.data?.v2?.filter((token) => { + return ( + token.chainKey === chainKey && + token.address.toLowerCase() === bridgeAddress.toLowerCase() && + token.token.symbol === tokenSymbol && + token.token.address.toLowerCase() === tokenAddress.toLowerCase() + ); + }); + + console.log('tokenInfo', tokenInfo, stargateConfig.data?.v2); + if (!!tokenInfo && tokenInfo.length > 0) { + console.log('Stargate token info matched', tokenInfo); + return true; + } + console.log('Could not find Stargate token info'); + console.log('-- fromChainId', fromChainId); + console.log('-- tokenAddress', tokenAddress); + console.log('-- bridgeAddress', bridgeAddress); + console.log('-- tokenSymbol', tokenSymbol); + return false; + } catch (error: any) { + console.log('Stargate token validation error', error); + return false; + } + }; + + // Meson + const validateMesonToken = async ({ + fromChainId, + tokenSymbol, + bridgeAddress, + tokenAddress, + }: IMesonTokenValidateParams) => { + try { + if (!fromChainId || !tokenAddress || !tokenSymbol || !bridgeAddress) return false; + const { data: mesonConfig } = await axios.get<{ result: IMesonTokenList[] }>( + `${MESON_ENDPOINT}/limits`, + ); + + const hexNum = fromChainId?.toString(16); + const chainInfo = mesonConfig.result.filter((chainInfo) => { + const tokenInfo = chainInfo.tokens.filter( + (token) => token?.addr === tokenAddress && token.id === tokenSymbol.toLowerCase(), + ); + if (!!tokenInfo && tokenInfo.length > 0) { + console.log('Meson token info', tokenInfo); + } + return ( + chainInfo.chainId === `0x${hexNum}` && + chainInfo.address === bridgeAddress && + tokenInfo?.length > 0 && + !!tokenInfo + ); + }); + if (!!chainInfo && chainInfo.length > 0) { + console.log('Meson chain info matched', chainInfo); + return true; + } + console.log('Could not find Meson token'); + console.log('-- fromChainId', fromChainId); + console.log('-- bridgeAddress', bridgeAddress); + console.log('-- tokenAddress', tokenAddress); + console.log('-- tokenSymbol', tokenSymbol); + return false; + } catch (error: any) { + console.log('Meson token validation error', error); + return false; + } + }; + + return { validateCBridgeToken, validateDeBridgeToken, validateStargateToken, validateMesonToken }; +};