diff --git a/package.json b/package.json index 6ed2cf358..7826db0af 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@avalabs/avalanche-module": "0.11.2", "@avalabs/avalanchejs": "4.1.0-alpha.7", "@avalabs/bitcoin-module": "0.11.2", - "@avalabs/bridge-unified": "0.0.0-feat-ab-missing-features-20241105143650", + "@avalabs/bridge-unified": "3.1.0", "@avalabs/core-bridge-sdk": "3.1.0-alpha.10", "@avalabs/core-chains-sdk": "3.1.0-alpha.10", "@avalabs/core-coingecko-sdk": "3.1.0-alpha.10", diff --git a/src/background/services/featureFlags/models.ts b/src/background/services/featureFlags/models.ts index 0a16b7bf7..7e5da49e8 100644 --- a/src/background/services/featureFlags/models.ts +++ b/src/background/services/featureFlags/models.ts @@ -30,6 +30,10 @@ export enum FeatureGates { SEEEDLESS_MFA_SETTINGS = 'seedless-mfa-settings', SEEDLESS_OPTIONAL_MFA = 'seedless-optional-mfa', UNIFIED_BRIDGE_CCTP = 'unified-bridge-cctp', + UNIFIED_BRIDGE_ICTT = 'unified-bridge-ictt', + UNIFIED_BRIDGE_AB_EVM = 'unified-bridge-ab-evm', + UNIFIED_BRIDGE_AB_AVA_TO_BTC = 'unified-bridge-ab-ava-to-btc', + UNIFIED_BRIDGE_AB_BTC_TO_AVA = 'unified-bridge-ab-btc-to-ava', DEBANK_TRANSACTION_PARSING = 'debank-transaction-parsing', DEBANK_TRANSACTION_PRE_EXECUTION = 'debank-transaction-pre-execution', PRIMARY_ACCOUNT_REMOVAL = 'primary-account-removal', @@ -76,6 +80,10 @@ export const DISABLED_FLAG_VALUES: FeatureFlags = { [FeatureGates.SEEEDLESS_MFA_SETTINGS]: false, [FeatureGates.SEEDLESS_OPTIONAL_MFA]: false, [FeatureGates.UNIFIED_BRIDGE_CCTP]: false, + [FeatureGates.UNIFIED_BRIDGE_ICTT]: false, + [FeatureGates.UNIFIED_BRIDGE_AB_EVM]: false, + [FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: false, + [FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: false, [FeatureGates.DEBANK_TRANSACTION_PARSING]: false, [FeatureGates.DEBANK_TRANSACTION_PRE_EXECUTION]: false, [FeatureGates.PRIMARY_ACCOUNT_REMOVAL]: false, @@ -121,6 +129,10 @@ export const DEFAULT_FLAGS: FeatureFlags = { [FeatureGates.SEEEDLESS_MFA_SETTINGS]: true, [FeatureGates.SEEDLESS_OPTIONAL_MFA]: true, [FeatureGates.UNIFIED_BRIDGE_CCTP]: true, + [FeatureGates.UNIFIED_BRIDGE_ICTT]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true, [FeatureGates.DEBANK_TRANSACTION_PARSING]: false, [FeatureGates.DEBANK_TRANSACTION_PRE_EXECUTION]: false, [FeatureGates.PRIMARY_ACCOUNT_REMOVAL]: true, diff --git a/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts b/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts index 82e049270..6510ad03c 100644 --- a/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts +++ b/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts @@ -29,6 +29,7 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { isMainnet: jest.fn(), getNetwork: jest.fn(), getProviderForNetwork: jest.fn(), + getBitcoinProvider: jest.fn(), sendTransaction: jest.fn(), } as any; @@ -41,6 +42,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { featureFlags: { [FeatureGates.IMPORT_FIREBLOCKS]: true, [FeatureGates.UNIFIED_BRIDGE_CCTP]: true, + [FeatureGates.UNIFIED_BRIDGE_ICTT]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true, }, addListener: jest.fn(), } as any; @@ -64,6 +69,7 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { networkService.getNetwork.mockImplementation(async (chainId) => ({ chainId, })); + networkService.getBitcoinProvider.mockResolvedValue({} as any); }); it('creates core instance with proper environment', async () => { @@ -118,6 +124,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { // Toggle an irrelevant flag off mockFeatureFlagChanges({ [FeatureGates.UNIFIED_BRIDGE_CCTP]: true, + [FeatureGates.UNIFIED_BRIDGE_ICTT]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true, [FeatureGates.IMPORT_FIREBLOCKS]: false, }); @@ -127,6 +137,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { // Toggle a relevant flag off mockFeatureFlagChanges({ [FeatureGates.UNIFIED_BRIDGE_CCTP]: false, + [FeatureGates.UNIFIED_BRIDGE_ICTT]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true, [FeatureGates.IMPORT_FIREBLOCKS]: false, }); @@ -180,30 +194,13 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { }); new UnifiedBridgeService(networkService, storageService, flagsService); + await jest.runAllTimersAsync(); await jest.runAllTicks(); - expect(getEnabledBridgeServices).toHaveBeenCalledTimes(1); + expect(getEnabledBridgeServices).toHaveBeenCalledTimes(4); expect(wait).toHaveBeenNthCalledWith(1, 2000); - - jest.advanceTimersByTime(2000); - await jest.runOnlyPendingTimers(); - await jest.runAllTicks(); - - expect(getEnabledBridgeServices).toHaveBeenCalledTimes(2); expect(wait).toHaveBeenNthCalledWith(2, 4000); - - jest.advanceTimersByTime(4000); - await jest.runOnlyPendingTimers(); - await jest.runAllTicks(); - - expect(getEnabledBridgeServices).toHaveBeenCalledTimes(3); expect(wait).toHaveBeenNthCalledWith(3, 8000); - - jest.advanceTimersByTime(8000); - await jest.runOnlyPendingTimers(); - await jest.runAllTicks(); - - expect(getEnabledBridgeServices).toHaveBeenCalledTimes(4); expect(createUnifiedBridgeService).toHaveBeenCalled(); }); }); diff --git a/src/background/services/unifiedBridge/UnifiedBridgeService.ts b/src/background/services/unifiedBridge/UnifiedBridgeService.ts index 9eee25b6c..85cfc205d 100644 --- a/src/background/services/unifiedBridge/UnifiedBridgeService.ts +++ b/src/background/services/unifiedBridge/UnifiedBridgeService.ts @@ -34,6 +34,7 @@ import { import sentryCaptureException, { SentryExceptionTypes, } from '@src/monitoring/sentryCaptureException'; +import { getEnabledBridgeTypes } from '@src/utils/getEnabledBridgeTypes'; @singleton() export class UnifiedBridgeService implements OnStorageReady { @@ -115,14 +116,9 @@ export class UnifiedBridgeService implements OnStorageReady { #getBridgeInitializers( bitcoinProvider: BitcoinProvider ): BridgeInitializer[] { - // TODO: feature flag those - return [ - BridgeType.CCTP, - BridgeType.ICTT_ERC20_ERC20, - BridgeType.AVALANCHE_EVM, - BridgeType.AVALANCHE_AVA_BTC, - BridgeType.AVALANCHE_BTC_AVA, - ].map((type) => this.#getInitializerForBridgeType(type, bitcoinProvider)); + return getEnabledBridgeTypes(this.#flagStates).map((type) => + this.#getInitializerForBridgeType(type, bitcoinProvider) + ); } #getInitializerForBridgeType( diff --git a/src/background/services/unifiedBridge/models.ts b/src/background/services/unifiedBridge/models.ts index dbb4a52ed..4efc38f53 100644 --- a/src/background/services/unifiedBridge/models.ts +++ b/src/background/services/unifiedBridge/models.ts @@ -19,7 +19,13 @@ export type UnifiedBridgeState = { pendingTransfers: Record; }; -export const UNIFIED_BRIDGE_TRACKED_FLAGS = [FeatureGates.UNIFIED_BRIDGE_CCTP]; +export const UNIFIED_BRIDGE_TRACKED_FLAGS = [ + FeatureGates.UNIFIED_BRIDGE_CCTP, + FeatureGates.UNIFIED_BRIDGE_ICTT, + FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC, + FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA, + FeatureGates.UNIFIED_BRIDGE_AB_EVM, +]; export const UNIFIED_BRIDGE_DEFAULT_STATE: UnifiedBridgeState = { pendingTransfers: {}, diff --git a/src/components/common/CustomFees.tsx b/src/components/common/CustomFees.tsx index cf2bb9c1c..59807d304 100644 --- a/src/components/common/CustomFees.tsx +++ b/src/components/common/CustomFees.tsx @@ -511,7 +511,7 @@ export function CustomFees({ diff --git a/src/contexts/FeatureFlagsProvider.tsx b/src/contexts/FeatureFlagsProvider.tsx index 9bd5b3557..489d74b8e 100644 --- a/src/contexts/FeatureFlagsProvider.tsx +++ b/src/contexts/FeatureFlagsProvider.tsx @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import { DEFAULT_FLAGS, FeatureGates, @@ -30,7 +31,13 @@ export function FeatureFlagsContextProvider({ children }: { children: any }) { map((evt) => evt.value) ) .subscribe((result) => { - setFeatureFlags(result); + setFeatureFlags((prevFlags) => { + if (isEqual(prevFlags, result)) { + // Prevent re-renders when nothing changed + return prevFlags; + } + return result; + }); }); return () => { subscription.unsubscribe(); diff --git a/src/contexts/UnifiedBridgeProvider.test.tsx b/src/contexts/UnifiedBridgeProvider.test.tsx index eeba66c8c..4a0abd352 100644 --- a/src/contexts/UnifiedBridgeProvider.test.tsx +++ b/src/contexts/UnifiedBridgeProvider.test.tsx @@ -1,8 +1,8 @@ import { matchingPayload, render, waitFor } from '@src/tests/test-utils'; import { createRef, forwardRef, useImperativeHandle } from 'react'; import { - BridgeService, BridgeSignatureReason, + BridgeStepDetails, BridgeType, Environment, TokenType, @@ -25,7 +25,6 @@ import { chainIdToCaip } from '@src/utils/caipConversion'; import { CommonError } from '@src/utils/errors'; import { UnifiedBridgeError } from '@src/background/services/unifiedBridge/models'; import { RpcMethod } from '@avalabs/vm-module-types'; -import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; const ACTIVE_ACCOUNT_ADDRESS = 'addressC'; @@ -105,11 +104,16 @@ describe('contexts/UnifiedBridgeProvider', () => { ethereumProvider: { waitForTransaction: jest.fn(), }, + bitcoinProvider: {}, } as any; const featureFlagsContext = { featureFlags: { [FeatureGates.UNIFIED_BRIDGE_CCTP]: true, + [FeatureGates.UNIFIED_BRIDGE_ICTT]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]: true, + [FeatureGates.UNIFIED_BRIDGE_AB_EVM]: true, }, } as any; @@ -140,7 +144,7 @@ describe('contexts/UnifiedBridgeProvider', () => { jest.resetAllMocks(); core = { - getAssets: async () => ({ + getAssets: () => ({ [chainIdToCaip(ethereum.chainId)]: [ethUSDC], [chainIdToCaip(avalanche.chainId)]: [avaxUSDC], }), @@ -206,23 +210,61 @@ describe('contexts/UnifiedBridgeProvider', () => { ); }); - it('initializes without AvalancheBridge and ICTT', async () => { - const serviceMap = new Map([[BridgeType.CCTP, {} as BridgeService]]); - jest.mocked(getEnabledBridgeServices).mockResolvedValueOnce(serviceMap); - + it('uses proper signers for different bridge types', async () => { getBridgeProvider(); - await waitFor(() => - expect(getEnabledBridgeServices).toHaveBeenCalledWith(Environment.PROD, [ - BridgeType.AVALANCHE_EVM, - BridgeType.ICTT_ERC20_ERC20, - ]) - ); - await waitFor(() => - expect(createUnifiedBridgeService).toHaveBeenCalledWith( - matchingPayload({ enabledBridgeServices: serviceMap }) - ) - ); + await waitFor(async () => { + expect(getEnabledBridgeServices).toHaveBeenCalledWith( + Environment.PROD, + expect.any(Array) + ); + const initializers = jest.mocked(getEnabledBridgeServices).mock + .lastCall?.[1]; + + expect(initializers?.length).toEqual(5); + }); + + await waitFor(async () => { + const initializers = jest.mocked(getEnabledBridgeServices).mock + .lastCall![1]; + + const step: BridgeStepDetails = { + currentSignature: 1, + requiredSignatures: 2, + currentSignatureReason: BridgeSignatureReason.AllowanceApproval, + }; + + for (const { signer, type } of initializers) { + const isBtc = type === BridgeType.AVALANCHE_BTC_AVA; + const tx = isBtc + ? { inputs: [], outputs: [] } + : ({ + from: `0x${type}`, + to: `0x${type}`, + data: `0x${type}`, + } as any); + + await signer.sign(tx, async () => '0x' as const, step); + + expect(requestFn).toHaveBeenLastCalledWith( + { + method: + type === BridgeType.AVALANCHE_BTC_AVA + ? RpcMethod.BITCOIN_SIGN_TRANSACTION + : RpcMethod.ETH_SEND_TRANSACTION, + params: isBtc ? tx : [tx], + }, + { + alert: { + notice: 'You will be prompted {{remaining}} more time(s).', + title: 'This operation requires {{total}} approvals.', + type: 'info', + }, + customApprovalScreenTitle: 'Confirm Bridge', + } + ); + } + }); }); describe('transferAsset()', () => { @@ -254,94 +296,6 @@ describe('contexts/UnifiedBridgeProvider', () => { } }); }); - - it('uses the SDK to transfer assets and sends eth_sendTransaction requests to the wallet', async () => { - const transfer = { sourceTxHash: '0xTransferTxHash' } as any; - const allowanceTx = { - from: ACTIVE_ACCOUNT_ADDRESS, - to: '0xUsdcAllowanceContract', - data: '0x1234', - } as any; - const transferTx = { - from: ACTIVE_ACCOUNT_ADDRESS, - to: '0xUsdcTransferContract', - data: '0x1234', - } as any; - - jest - .mocked(core.transferAsset) - .mockImplementation(async ({ onStepChange, sign }) => { - // Simulate double-approval flow for complete test - - onStepChange?.({ - currentSignature: 1, - requiredSignatures: 2, - currentSignatureReason: BridgeSignatureReason.AllowanceApproval, - }); - - await sign?.(allowanceTx, async () => '0xApprovalTxHash'); - - onStepChange?.({ - currentSignature: 2, - requiredSignatures: 2, - currentSignatureReason: BridgeSignatureReason.TokensTransfer, - }); - - await sign?.(transferTx, async () => transfer.sourceTxHash); - - return transfer; - }); - - const provider = getBridgeProvider(); - - await waitFor(async () => { - expect( - await provider.current?.transferAsset('USDC', 1000n, 'eip155:1') - ).toEqual(transfer.sourceTxHash); - - expect(core.transferAsset).toHaveBeenCalledWith({ - asset: expect.objectContaining({ symbol: 'USDC' }), - fromAddress: accountsContext.accounts.active.addressC, - amount: 1000n, - targetChain: expect.objectContaining({ - chainId: chainIdToCaip(ethereum.chainId), - }), - sourceChain: expect.objectContaining({ - chainId: chainIdToCaip(avalanche.chainId), - }), - onStepChange: expect.any(Function), - sign: expect.any(Function), - }); - - expect(requestFn).toHaveBeenCalledWith( - { - method: RpcMethod.ETH_SEND_TRANSACTION, - params: [{ ...allowanceTx }], - }, - { - customApprovalScreenTitle: 'Confirm Bridge', - alert: { - type: 'info', - title: 'This operation requires {{total}} approvals.', - notice: 'You will be prompted {{remaining}} more time(s).', - }, - } - ); - expect(requestFn).toHaveBeenCalledWith( - { - method: RpcMethod.ETH_SEND_TRANSACTION, - params: [{ ...transferTx }], - }, - { - customApprovalScreenTitle: 'Confirm Bridge', - } - ); - expect(requestFn).toHaveBeenCalledWith({ - method: ExtensionRequest.UNIFIED_BRIDGE_TRACK_TRANSFER, - params: [transfer], - }); - }); - }); }); describe('estimateTransferGas()', () => { diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index ea4ba8fa1..c1f8d2d22 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -42,7 +42,6 @@ import { useConnectionContext } from './ConnectionProvider'; import { CommonError, ErrorCode } from '@src/utils/errors'; import { useTranslation } from 'react-i18next'; import { useFeatureFlagContext } from './FeatureFlagsProvider'; -import { FeatureGates } from '@src/background/services/featureFlags/models'; import { useAccountsContext } from './AccountsProvider'; import { JsonRpcApiProvider } from 'ethers'; import { getProviderForNetwork } from '@src/utils/network/getProviderForNetwork'; @@ -52,6 +51,7 @@ import { lowerCaseKeys } from '@src/utils/lowerCaseKeys'; import { RpcMethod } from '@avalabs/vm-module-types'; import { isBitcoinCaipId } from '@src/utils/caipConversion'; import { Account } from '@src/background/services/accounts/models'; +import { getEnabledBridgeTypes } from '@src/utils/getEnabledBridgeTypes'; export interface UnifiedBridgeContext { estimateTransferGas( @@ -143,22 +143,10 @@ export function UnifiedBridgeProvider({ UNIFIED_BRIDGE_DEFAULT_STATE ); const { featureFlags } = useFeatureFlagContext(); - const isCCTPEnabled = featureFlags[FeatureGates.UNIFIED_BRIDGE_CCTP]; - const enabledBridgeTypes = useMemo(() => { - const enabled: BridgeType[] = []; - - if (isCCTPEnabled) { - enabled.push(BridgeType.CCTP); - } - - // TODO: feature flag those - enabled.push(BridgeType.ICTT_ERC20_ERC20); - enabled.push(BridgeType.AVALANCHE_EVM); - enabled.push(BridgeType.AVALANCHE_AVA_BTC); - enabled.push(BridgeType.AVALANCHE_BTC_AVA); - - return enabled; - }, [isCCTPEnabled]); + const enabledBridgeTypes = useMemo( + () => getEnabledBridgeTypes(featureFlags), + [featureFlags] + ); const environment = useMemo(() => { if (typeof activeNetwork?.isTestnet !== 'boolean') { diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 95eccf392..2abaab97a 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -128,6 +128,7 @@ "Bridge initialization failed": "Bridge initialization failed", "Bridge not available": "Bridge not available", "Bridging from Avalanche to Bitcoin takes approximately . Please see\n the FAQ for additional info.": "Bridging from Avalanche to Bitcoin takes approximately . Please see\n the FAQ for additional info.", + "Bridging this token pair utilizes Avalanche Interchain Messaging. Bridge FAQs": "Bridging this token pair utilizes Avalanche Interchain Messaging. Bridge FAQs", "Bridging...": "Bridging...", "Browse Files": "Browse Files", "Buy": "Buy", @@ -413,8 +414,8 @@ "Instant": "Instant", "Insufficient balance": "Insufficient balance", "Insufficient balance for fee": "Insufficient balance for fee", - "Insufficient balance to cover gas costs.
Please add {{tokenSymbol}}.": "Insufficient balance to cover gas costs.
Please add {{tokenSymbol}}.", "Insufficient balance to cover gas costs.
Please add {{token}}.": "Insufficient balance to cover gas costs.
Please add {{token}}.", + "Insufficient balance to cover gas costs. Please add {{tokenSymbol}}.": "Insufficient balance to cover gas costs. Please add {{tokenSymbol}}.", "Insufficient balance.": "Insufficient balance.", "Insufficient funds": "Insufficient funds", "Insurance Buyer": "Insurance Buyer", @@ -814,6 +815,7 @@ "The BTC address could not be found for the connected vault account. Ensure your vault account has BTC wallet with a SEGWIT address format configured.": "The BTC address could not be found for the connected vault account. Ensure your vault account has BTC wallet with a SEGWIT address format configured.", "The Base Fee is set by the network and changes frequently. Any difference between the set Max Base Fee and the actual Base Fee will be refunded.": "The Base Fee is set by the network and changes frequently. Any difference between the set Max Base Fee and the actual Base Fee will be refunded.", "The Priority Fee is an incentive paid to network operators to prioritize processing of this transaction.": "The Priority Fee is an incentive paid to network operators to prioritize processing of this transaction.", + "The active account does not support Bitcoin.": "The active account does not support Bitcoin.", "The amount cannot be lower than the bridging fee": "The amount cannot be lower than the bridging fee", "The bridging fee is unknown": "The bridging fee is unknown", "The inputs of this transaction are greater than the output. Continuing will cause you to lose funds associated with this UTXO.": "The inputs of this transaction are greater than the output. Continuing will cause you to lose funds associated with this UTXO.", @@ -925,6 +927,7 @@ "Unlocked Staked": "Unlocked Staked", "Unlocked Unstaked": "Unlocked Unstaked", "Unsupported Version": "Unsupported Version", + "Unsupported account": "Unsupported account", "Unsupported network": "Unsupported network", "Unsupported token": "Unsupported token", "Update": "Update", diff --git a/src/pages/Bridge/Bridge.tsx b/src/pages/Bridge/Bridge.tsx index 68a5858c9..d8de52c09 100644 --- a/src/pages/Bridge/Bridge.tsx +++ b/src/pages/Bridge/Bridge.tsx @@ -24,12 +24,15 @@ import { useErrorMessage } from '@src/hooks/useErrorMessage'; import { isBitcoinNetwork } from '@src/background/services/network/utils/isBitcoinNetwork'; import { useLiveBalance } from '@src/hooks/useLiveBalance'; import { NetworkWithCaipId } from '@src/background/services/network/models'; +import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; import { useBridge } from './hooks/useBridge'; import { BridgeForm } from './components/BridgeForm'; import { BridgeUnknownNetwork } from './components/BridgeUnknownNetwork'; import { useBridgeTxHandling } from './hooks/useBridgeTxHandling'; import { BridgeFormSkeleton } from './components/BridgeFormSkeleton'; +import { BridgeSanctions } from './components/BridgeSanctions'; +import { isAddressBlockedError } from './utils/isAddressBlockedError'; const POLLED_BALANCES = [TokenType.NATIVE, TokenType.ERC20]; @@ -65,6 +68,7 @@ export function Bridge() { const history = useHistory(); const { captureEncrypted } = useAnalyticsContext(); + const { networkFee } = useNetworkFeeContext(); const { getPageHistoryData, setNavigationHistoryData } = usePageHistory(); const getTranslatedError = useErrorMessage(); @@ -155,6 +159,8 @@ export function Bridge() { bridgeFee, ]); + const [isAddressBlocked, setIsAddressBlocked] = useState(false); + const onFailure = useCallback( (transferError: unknown) => { setBridgeError(t('There was a problem with the transfer')); @@ -164,6 +170,11 @@ export function Bridge() { targetBlockchain: targetChain?.caipId, }); + if (isAddressBlockedError(transferError)) { + setIsAddressBlocked(true); + return; + } + const { title, hint } = getTranslatedError(transferError); toast.custom( @@ -269,7 +280,6 @@ export function Bridge() { receiveAmount, setTargetChain, possibleTargetChains, - loading: false, // TODO: load balances bridgableTokens, sourceBalance, }; @@ -291,17 +301,9 @@ export function Bridge() { ); } - // TODO: implement in UnifiedBridge SDK? - // if ( - // activeAccount && - // isAddressBlocklisted({ - // addressEVM: activeAccount.addressC, - // addressBTC: activeAccount.addressBTC, - // bridgeConfig, - // }) - // ) { - // return ; - // } + if (isAddressBlocked) { + return ; + } if (isReady && transferableAssets.length === 0) { return ( @@ -322,7 +324,11 @@ export function Bridge() { > {t('Bridge')} - {isReady ? : } + {isReady && networkFee ? ( + + ) : ( + + )}
); } diff --git a/src/pages/Bridge/components/BridgeForm.tsx b/src/pages/Bridge/components/BridgeForm.tsx index 31ee03383..483f920cb 100644 --- a/src/pages/Bridge/components/BridgeForm.tsx +++ b/src/pages/Bridge/components/BridgeForm.tsx @@ -45,7 +45,7 @@ import { BridgeTypeFootnote } from './BridgeTypeFootnote'; import { BridgeOptions } from '../models'; import { isBitcoinNetwork } from '@src/background/services/network/utils/isBitcoinNetwork'; import { CustomFees } from '@src/components/common/CustomFees'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; +import { NetworkFee } from '@src/background/services/networkFee/models'; export type BridgeFormProps = ReturnType & { isPending: boolean; @@ -53,13 +53,13 @@ export type BridgeFormProps = ReturnType & { // Generic props isAmountTooLow: boolean; setIsAmountTooLow: Dispatch>; + networkFee: NetworkFee; price?: number; - loading: boolean; sourceBalance?: Exclude; bridgeError: string; - setBridgeError: (err: string) => void; + setBridgeError: Dispatch>; setNavigationHistoryData: (data: NavigationHistoryDataState) => void; onTransfer: (bridgeOptions: BridgeOptions) => void; @@ -76,11 +76,11 @@ export const BridgeForm = ({ minimum, maximum, receiveAmount, + networkFee, sourceBalance, targetChain, setTargetChain, price, - loading, estimateGas, bridgeError, setBridgeError, @@ -100,7 +100,6 @@ export const BridgeForm = ({ const formRef = useRef(null); const { setNetwork, network } = useNetworkContext(); - const { networkFee } = useNetworkFeeContext(); const { currencyFormatter, currency } = useSettingsContext(); const { sendTokenSelectedAnalytics, sendAmountEnteredAnalytics } = useSendAnalyticsData(); @@ -108,6 +107,7 @@ export const BridgeForm = ({ const { capture } = useAnalyticsContext(); const [isTokenSelectOpen, setIsTokenSelectOpen] = useState(false); + const [feeRate, setFeeRate] = useState(networkFee.low.maxFee); const denomination = useMemo(() => { if (!sourceBalance) { @@ -145,7 +145,25 @@ export const BridgeForm = ({ } }, [estimateGas, amount, isAmountTooLow]); - const hasEnoughForNetworkFee = useHasEnoughForGas(neededGas); + useEffect(() => { + if (typeof maximum === 'bigint' && amount && amount > maximum) { + const errorMessage = t('Insufficient balance'); + + setBridgeError((prevError) => { + if (prevError === errorMessage) { + return prevError; + } + + capture('BridgeTokenSelectError', { + errorMessage, + }); + + return errorMessage; + }); + } + }, [amount, capture, maximum, setBridgeError, t]); + + const hasEnoughForNetworkFee = useHasEnoughForGas(amount, feeRate, neededGas); const errorTooltipContent = useMemo(() => { if (!hasEnoughForNetworkFee) { @@ -213,34 +231,8 @@ export const BridgeForm = ({ setAmount(value.bigint); sendAmountEnteredAnalytics('Bridge'); - - // When there is no balance for given token, maximum is undefined - if (!maximum || (maximum && value.bigint && value.bigint > maximum)) { - const errorMessage = t('Insufficient balance'); - - if (errorMessage === bridgeError) { - return; - } - - setBridgeError(errorMessage); - capture('BridgeTokenSelectError', { - errorMessage, - }); - return; - } - setBridgeError(''); }, - [ - asset, - bridgeError, - capture, - setBridgeError, - maximum, - sendAmountEnteredAnalytics, - setAmount, - setNavigationHistoryData, - t, - ] + [asset, sendAmountEnteredAnalytics, setAmount, setNavigationHistoryData] ); const handleSelect = useCallback( @@ -293,7 +285,6 @@ export const BridgeForm = ({ const disableTransfer = useMemo( () => bridgeError.length > 0 || - loading || isPending || isAmountTooLow || !amount || @@ -305,21 +296,13 @@ export const BridgeForm = ({ hasEnoughForNetworkFee, isAmountTooLow, isPending, - loading, networkFee, ] ); - - const [feeRate, setFeeRate] = useState(networkFee?.low.maxFee); // NOTE: we operate on the assumption that UnifiedBridge SDK will // use the first matching bridge from the `destinations` array const [bridgeType] = asset?.destinations[targetChain?.caipId ?? ''] ?? []; - - if (!network || !networkFee) { - return null; // TODO: loading screen - } - - const withFeeBox = isBitcoinNetwork(network); + const withFeeBox = network ? isBitcoinNetwork(network) : false; return ( <> @@ -396,7 +379,6 @@ export const BridgeForm = ({ setIsTokenSelectOpen(!isTokenSelectOpen); }} isOpen={isTokenSelectOpen} - isValueLoading={loading} setIsOpen={setIsTokenSelectOpen} padding="0 16px 8px" skipHandleMaxAmount @@ -592,7 +574,7 @@ export const BridgeForm = ({ withFeeBox && feeRate ? { price: feeRate } : undefined, }) } - isLoading={loading || isPending} + isLoading={isPending} > {isPending ? t('Bridging...') : t('Bridge')} diff --git a/src/pages/Bridge/hooks/useHasEnoughtForGas.ts b/src/pages/Bridge/hooks/useHasEnoughtForGas.ts index a87aac64c..c70008ff7 100644 --- a/src/pages/Bridge/hooks/useHasEnoughtForGas.ts +++ b/src/pages/Bridge/hooks/useHasEnoughtForGas.ts @@ -1,17 +1,25 @@ +import { useEffect, useState } from 'react'; import { TokenType } from '@avalabs/vm-module-types'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; + import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -import Big from 'big.js'; -import { useEffect, useState } from 'react'; -export const useHasEnoughForGas = (gasLimit?: bigint): boolean => { +export const useHasEnoughForGas = ( + sendAmount?: bigint, + feeRate?: bigint, + gasLimit?: bigint +): boolean => { const tokens = useTokensWithBalances(); - const { networkFee } = useNetworkFeeContext(); const [hasEnough, setHasEnough] = useState(true); useEffect(() => { - if (!tokens || !networkFee || !gasLimit) return; + if ( + !tokens || + !gasLimit || + typeof sendAmount !== 'bigint' || + typeof feeRate !== 'bigint' + ) + return; const token = tokens.find((x) => x.type === TokenType.NATIVE); // If the native token has no blance, we do not have enough @@ -22,14 +30,13 @@ export const useHasEnoughForGas = (gasLimit?: bigint): boolean => { // get gasPrice of network const balance = token && token.balance; - const estimatedGasCost = networkFee.low.maxFeePerGas * gasLimit; + const estimatedGasCost = feeRate * gasLimit; + // check if balance > gasPrice if (balance && estimatedGasCost) { - setHasEnough( - new Big(balance.toString()).gte(estimatedGasCost.toString()) - ); + setHasEnough(balance > sendAmount + estimatedGasCost); } - }, [tokens, networkFee, gasLimit]); + }, [tokens, feeRate, sendAmount, gasLimit]); return hasEnough; }; diff --git a/src/pages/Bridge/utils/isAddressBlockedError.ts b/src/pages/Bridge/utils/isAddressBlockedError.ts new file mode 100644 index 000000000..194fd9f17 --- /dev/null +++ b/src/pages/Bridge/utils/isAddressBlockedError.ts @@ -0,0 +1,9 @@ +import { ErrorReason } from '@avalabs/bridge-unified'; + +export const isAddressBlockedError = (err?: unknown) => { + return ( + !!err && + err instanceof Error && + err.message === ErrorReason.ADDRESS_IS_BLOCKED + ); +}; diff --git a/src/utils/getEnabledBridgeTypes.ts b/src/utils/getEnabledBridgeTypes.ts new file mode 100644 index 000000000..9570c4d5a --- /dev/null +++ b/src/utils/getEnabledBridgeTypes.ts @@ -0,0 +1,27 @@ +import { BridgeType } from '@avalabs/bridge-unified'; +import { + FeatureFlags, + FeatureGates, +} from '@src/background/services/featureFlags/models'; + +export const getEnabledBridgeTypes = (featureFlags: Partial) => { + const enabled: BridgeType[] = []; + + if (featureFlags[FeatureGates.UNIFIED_BRIDGE_CCTP]) { + enabled.push(BridgeType.CCTP); + } + if (featureFlags[FeatureGates.UNIFIED_BRIDGE_ICTT]) { + enabled.push(BridgeType.ICTT_ERC20_ERC20); + } + if (featureFlags[FeatureGates.UNIFIED_BRIDGE_AB_EVM]) { + enabled.push(BridgeType.AVALANCHE_EVM); + } + if (featureFlags[FeatureGates.UNIFIED_BRIDGE_AB_BTC_TO_AVA]) { + enabled.push(BridgeType.AVALANCHE_BTC_AVA); + } + if (featureFlags[FeatureGates.UNIFIED_BRIDGE_AB_AVA_TO_BTC]) { + enabled.push(BridgeType.AVALANCHE_AVA_BTC); + } + + return enabled; +}; diff --git a/yarn.lock b/yarn.lock index cb646bf13..efcf9bd60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,10 +74,10 @@ bn.js "5.2.1" zod "3.23.8" -"@avalabs/bridge-unified@0.0.0-feat-ab-missing-features-20241105143650": - version "0.0.0-feat-ab-missing-features-20241105143650" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-ab-missing-features-20241105143650.tgz#090865160e9db31f62323af2319a17a88f56ab56" - integrity sha512-Yeu8LbCGV9pmPnNp/huHUN0YHV63rqBFFf809mP9AhKILSRNTaYZPEnE7RhRBq+O4T5Nvv8/1L9j9YzPwuanYg== +"@avalabs/bridge-unified@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-3.1.0.tgz#563be60cb5c1399b9cfea457fd82fee5fe43177c" + integrity sha512-1+bEf3wy6m0DLDmF5nQCfCHDnCN3hlBt9424B0gGgRnC5RaoSNHudJSBrNxR3UrI1kAZQDAlFfqGSxm2qmjH2A== dependencies: "@noble/hashes" "1.5.0" "@scure/base" "1.1.9"