From 34a8da03cd105714714bb85082346bcde5a8d384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 6 Sep 2024 08:09:13 +0200 Subject: [PATCH 01/11] chore: upgrade unified bridge sdk --- package.json | 2 +- src/contexts/UnifiedBridgeProvider.tsx | 88 +++++++++++++++++--------- yarn.lock | 39 ++++++------ 3 files changed, 78 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 8af32a29..a2e1d4fc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@avalabs/avalanchejs": "4.0.5", "@avalabs/bitcoin-module": "0.5.0", - "@avalabs/bridge-unified": "2.1.0", + "@avalabs/bridge-unified": "0.0.0-fix-make-sign-optional-for-gas-20240905074019", "@avalabs/core-bridge-sdk": "3.1.0-alpha.4", "@avalabs/core-chains-sdk": "3.1.0-alpha.4", "@avalabs/core-coingecko-sdk": "3.1.0-alpha.4", diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index 3fd1c121..46a2185b 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -16,6 +16,8 @@ import { TokenType, createUnifiedBridgeService, BridgeTransfer, + getEnabledBridgeServices, + BridgeServicesMap, } from '@avalabs/bridge-unified'; import { ethErrors } from 'eth-rpc-errors'; import { filter, map } from 'rxjs'; @@ -125,25 +127,55 @@ export function UnifiedBridgeProvider({ ); const { featureFlags } = useFeatureFlagContext(); const isCCTPDisabled = !featureFlags[FeatureGates.UNIFIED_BRIDGE_CCTP]; - const disabledBridges = useMemo( + const disabledBridgeTypes = useMemo( () => (isCCTPDisabled ? [BridgeType.CCTP] : []), [isCCTPDisabled] ); + const environment = useMemo(() => { + if (typeof activeNetwork?.isTestnet !== 'boolean') { + return null; + } + + return activeNetwork.isTestnet ? Environment.TEST : Environment.PROD; + }, []); + + const [enabledBridgeServices, setEnabledBridgeServices] = + useState(); + + useEffect(() => { + if (!environment) { + return; + } + + let isMounted = true; + + getEnabledBridgeServices(environment, disabledBridgeTypes) + .then((bridges) => { + if (isMounted) setEnabledBridgeServices(bridges); + }) + .catch((err) => { + console.log('Unable to initialize bridge services', err); + if (isMounted) setEnabledBridgeServices(undefined); + }); + + return () => { + isMounted = false; + }; + }, [environment, disabledBridgeTypes]); + // Memoize the core instance of Unified Bridge based on the current // network environment & feature flags configuration - const core = useMemo( - () => - activeNetwork - ? createUnifiedBridgeService({ - environment: activeNetwork.isTestnet - ? Environment.TEST - : Environment.PROD, - disabledBridgeTypes: disabledBridges, - }) - : null, - [activeNetwork, disabledBridges] - ); + const core = useMemo(() => { + if (!environment || !enabledBridgeServices) { + return null; + } + + return createUnifiedBridgeService({ + environment, + enabledBridgeServices, + }); + }, [environment, enabledBridgeServices]); // Whenever core instance is re-created, initialize it and update assets useEffect(() => { @@ -153,12 +185,12 @@ export function UnifiedBridgeProvider({ let isMounted = true; - core.init().then(async () => { + core.getAssets().then((chainAssetsMap) => { if (!isMounted) { return; } - setAssets(await core.getAssets()); + setAssets(chainAssetsMap); }); return () => { @@ -167,7 +199,7 @@ export function UnifiedBridgeProvider({ }, [core]); const buildChain = useCallback( - (chainId: number) => { + (chainId: number): Chain => { const network = getNetwork(chainId); assert(network, CommonError.UnknownNetwork); @@ -221,14 +253,16 @@ export function UnifiedBridgeProvider({ }, [events, request]); const supportsAsset = useCallback( - (lookupAddress: string, targetChainId: number) => { + (lookupAddressOrSymbol: string, targetChainId: number) => { if (!activeNetwork) { return false; } const sourceAssets = assets[getNetworkCaipId(activeNetwork)] ?? []; - const asset = sourceAssets.find(({ address }) => { - return lookupAddress === address; + const asset = sourceAssets.find((token) => { + return token.type === TokenType.NATIVE + ? token.symbol === lookupAddressOrSymbol + : token.address === lookupAddressOrSymbol; }); if (!asset) { @@ -300,8 +334,7 @@ export function UnifiedBridgeProvider({ assert(activeNetwork, CommonError.NoActiveNetwork); const asset = getAsset(symbol, activeNetwork.chainId); - - assert(asset?.address, UnifiedBridgeError.UnknownAsset); + assert(asset, UnifiedBridgeError.UnknownAsset); const feeMap = await core.getFees({ asset, @@ -310,17 +343,10 @@ export function UnifiedBridgeProvider({ sourceChain: buildChain(activeNetwork.chainId), }); - const fee = feeMap[asset.address]; - - if (typeof fee !== 'bigint') { - throw ethErrors.rpc.invalidRequest({ - data: { - reason: UnifiedBridgeError.InvalidFee, - }, - }); - } + const identifier = + asset.type === TokenType.NATIVE ? asset.symbol : asset.address; - return fee; + return feeMap[identifier] ?? 0n; }, [activeNetwork, core, buildChain, getAsset] ); diff --git a/yarn.lock b/yarn.lock index 5d0b13e0..18dd1291 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,14 +56,15 @@ bn.js "5.2.1" zod "3.23.8" -"@avalabs/bridge-unified@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-2.1.0.tgz#2b247cf46df387aeac8633b016419a056e12567b" - integrity sha512-s0lmYoTqi3J9mtA5Ja6qOKLDT/XMsv8D9wGNaWS+bBTOePf0nMDxRZ8k+BhEBikR3upGuQY8gtOEl2j9vkxkrA== +"@avalabs/bridge-unified@0.0.0-fix-make-sign-optional-for-gas-20240905074019": + version "0.0.0-fix-make-sign-optional-for-gas-20240905074019" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-fix-make-sign-optional-for-gas-20240905074019.tgz#543a62a639ba36a7624ed4dca22cfbc47720a3eb" + integrity sha512-a1I1kWMe4u9MZsNvBFQahNtPHefFL2RMeT+oLfbR6CClHrDyvcPURdt4mV+oV9oDSSAwK2Lg28cdfxokfkdo3A== dependencies: abitype "0.9.3" lodash "4.17.21" - viem "1.19.8" + viem "2.11.1" + zod "3.23.8" "@avalabs/core-bridge-sdk@3.1.0-alpha.4": version "3.1.0-alpha.4" @@ -6912,10 +6913,10 @@ abitype@0.9.3: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.3.tgz#294d25288ee683d72baf4e1fed757034e3c8c277" integrity sha512-dz4qCQLurx97FQhnb/EIYTk/ldQ+oafEDUqC0VVIeQS1Q48/YWt/9YNfMmp9SLFqN41ktxny3c8aYxHjmFIB/w== -abitype@0.9.8: - version "0.9.8" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" - integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== +abitype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" + integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== abort-controller@^3.0.0: version "3.0.0" @@ -13238,10 +13239,10 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -isows@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" - integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== +isows@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" + integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== isstream@~0.1.2: version "0.1.2" @@ -20591,18 +20592,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -viem@1.19.8: - version "1.19.8" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.19.8.tgz#036c4a5f76adf8fcbcc279c82ee5bdf398d82adf" - integrity sha512-AgoZcLej7u4Zw/ojDsDWfonPSoz07EirrvXVndAMG78ikHf7VnRbxkFACAFg2SgNbCXmr5+ePHpgd53dMVIZ4Q== +viem@2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.11.1.tgz#03ff205734c21748b7a0df8e5f956db61ebaf1c5" + integrity sha512-4iypXhxWkXoWO45XStxQvhj/vYZ5+3AtSEngHiOnuHOSNoiIsYfDvTMoTiAO53PeyYvExyVzJc9BFwejGHh7pg== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.2.0" "@noble/hashes" "1.3.2" "@scure/bip32" "1.3.2" "@scure/bip39" "1.2.1" - abitype "0.9.8" - isows "1.0.3" + abitype "1.0.0" + isows "1.0.4" ws "8.13.0" vinyl-fs@^3.0.1: From 814151cf2bf654a97b7a5047eb21d8e5f6048495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 10 Sep 2024 12:34:17 +0200 Subject: [PATCH 02/11] chore: make extension compatible with new UnifiedBridge SDK --- package.json | 2 +- .../history/HistoryServiceBridgeHelper.ts | 6 +- .../services/history/HistoryServiceGlacier.ts | 8 +- .../UnifiedBridgeService.test.ts | 20 +++- .../unifiedBridge/UnifiedBridgeService.ts | 100 ++++++++++++------ .../services/unifiedBridge/models.ts | 2 - src/contexts/UnifiedBridgeProvider.tsx | 52 ++++++--- src/localization/locales/en/translation.json | 1 + src/pages/Bridge/Bridge.tsx | 29 ++--- src/pages/Bridge/BridgeTransactionStatus.tsx | 15 ++- src/pages/Bridge/components/BridgeForm.tsx | 46 +++++--- .../Bridge/components/BridgeFormUnified.tsx | 6 +- src/pages/Bridge/hooks/useBridge.ts | 5 +- src/pages/Bridge/hooks/useBridgeAmounts.ts | 2 +- .../Bridge/hooks/useBridgeTransferStatus.ts | 8 +- .../hooks/useLogoUriForBridgeTransaction.ts | 3 +- src/pages/Bridge/hooks/useUnifiedBridge.ts | 12 +-- src/pages/Bridge/utils/getBalances.ts | 11 +- src/pages/Bridge/utils/getTokenAddress.ts | 3 +- src/pages/Wallet/WalletRecentTxs.tsx | 3 +- .../InProgressBridgeActivityCard.tsx | 22 +++- .../components/History/useBlockchainNames.ts | 18 ++-- src/utils/bridge/getBridgedAssetSymbol.ts | 13 +++ src/utils/lowerCaseKeys.ts | 5 + yarn.lock | 8 +- 25 files changed, 263 insertions(+), 137 deletions(-) create mode 100644 src/utils/bridge/getBridgedAssetSymbol.ts create mode 100644 src/utils/lowerCaseKeys.ts diff --git a/package.json b/package.json index a2e1d4fc..b25b0508 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@avalabs/avalanchejs": "4.0.5", "@avalabs/bitcoin-module": "0.5.0", - "@avalabs/bridge-unified": "0.0.0-fix-make-sign-optional-for-gas-20240905074019", + "@avalabs/bridge-unified": "0.0.0-feat-is-bridge-address-20240909115936", "@avalabs/core-bridge-sdk": "3.1.0-alpha.4", "@avalabs/core-chains-sdk": "3.1.0-alpha.4", "@avalabs/core-coingecko-sdk": "3.1.0-alpha.4", diff --git a/src/background/services/history/HistoryServiceBridgeHelper.ts b/src/background/services/history/HistoryServiceBridgeHelper.ts index 86a00087..106fa8e2 100644 --- a/src/background/services/history/HistoryServiceBridgeHelper.ts +++ b/src/background/services/history/HistoryServiceBridgeHelper.ts @@ -27,16 +27,12 @@ export class HistoryServiceBridgeHelper { const config = this.bridgeService.bridgeConfig; const ethereumAssets = config?.config?.critical.assets; const bitcoinAssets = config?.config?.criticalBitcoin?.bitcoinAssets; - const unifiedBridgeAddresses = this.unifiedBridgeService.state.addresses; if (!ethereumAssets || !bitcoinAssets) { return false; } - if ( - unifiedBridgeAddresses.includes(tx.from.toLowerCase()) || - unifiedBridgeAddresses.includes(tx.to.toLowerCase()) - ) { + if (this.unifiedBridgeService.isBridgeAddress(tx.from, tx.to)) { return true; } diff --git a/src/background/services/history/HistoryServiceGlacier.ts b/src/background/services/history/HistoryServiceGlacier.ts index 3f9bcc4e..65451a6c 100644 --- a/src/background/services/history/HistoryServiceGlacier.ts +++ b/src/background/services/history/HistoryServiceGlacier.ts @@ -90,10 +90,10 @@ export class HistoryServiceGlacier { return false; } - return [ - ETHEREUM_ADDRESS, - ...this.unifiedBridgeService.state.addresses, - ].includes(address.toLowerCase()); + return ( + ETHEREUM_ADDRESS === address.toLowerCase() || + this.unifiedBridgeService.isBridgeAddress(address) + ); } private getHistoryItemCategories( diff --git a/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts b/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts index 62814172..9eca295c 100644 --- a/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts +++ b/src/background/services/unifiedBridge/UnifiedBridgeService.test.ts @@ -1,6 +1,7 @@ import { Environment, createUnifiedBridgeService, + getEnabledBridgeServices, } from '@avalabs/bridge-unified'; import { UnifiedBridgeService } from './UnifiedBridgeService'; import { FeatureGates } from '../featureFlags/models'; @@ -49,10 +50,10 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { getAssets: jest.fn().mockResolvedValue({}), trackTransfer, transferAsset, - init: jest.fn().mockResolvedValue(undefined), } as any; jest.mocked(createUnifiedBridgeService).mockReturnValue(core); + jest.mocked(getEnabledBridgeServices).mockResolvedValue({} as any); networkService.isMainnet.mockReturnValue(false); networkService.getNetwork.mockImplementation(async (chainId) => ({ @@ -62,11 +63,13 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { new UnifiedBridgeService(networkService, storageService, flagsService); }); - it('creates core instance with proper environment', () => { + it('creates core instance with proper environment', async () => { networkService.isMainnet.mockReturnValue(true); new UnifiedBridgeService(networkService, storageService, flagsService); + await new Promise(process.nextTick); // Await getEnabledBridgeServices() call + expect(createUnifiedBridgeService).toHaveBeenCalledWith( expect.objectContaining({ environment: Environment.PROD, @@ -84,20 +87,25 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { ); }); - it('recreates the core instance on testnet mode switch', () => { + it('recreates the core instance on testnet mode switch', async () => { const mockTestnetModeChange = ( networkService.developerModeChanged.add as jest.Mock ).mock.lastCall[0]; + await new Promise(process.nextTick); // Await getEnabledBridgeServices() call expect(createUnifiedBridgeService).toHaveBeenCalledTimes(1); + mockTestnetModeChange(); + + await new Promise(process.nextTick); // Await getEnabledBridgeServices() call expect(createUnifiedBridgeService).toHaveBeenCalledTimes(2); }); - it('recreates the core instance when certain feature flags are toggled', () => { + it('recreates the core instance when certain feature flags are toggled', async () => { const mockFeatureFlagChanges = (flagsService.addListener as jest.Mock).mock .lastCall[1]; + await new Promise(process.nextTick); // Await getEnabledBridgeServices() call expect(createUnifiedBridgeService).toHaveBeenCalledTimes(1); // Toggle an irrelevant flag off @@ -105,6 +113,8 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { [FeatureGates.UNIFIED_BRIDGE_CCTP]: true, [FeatureGates.IMPORT_FIREBLOCKS]: false, }); + + await new Promise(process.nextTick); // Await getEnabledBridgeServices() call expect(createUnifiedBridgeService).toHaveBeenCalledTimes(1); // Toggle a relevant flag off @@ -112,6 +122,8 @@ describe('src/background/services/unifiedBridge/UnifiedBridgeService', () => { [FeatureGates.UNIFIED_BRIDGE_CCTP]: false, [FeatureGates.IMPORT_FIREBLOCKS]: false, }); + + await new Promise(process.nextTick); // Await getEnabledBridgeServices() call expect(createUnifiedBridgeService).toHaveBeenCalledTimes(2); }); diff --git a/src/background/services/unifiedBridge/UnifiedBridgeService.ts b/src/background/services/unifiedBridge/UnifiedBridgeService.ts index 17487fc9..c98bb33a 100644 --- a/src/background/services/unifiedBridge/UnifiedBridgeService.ts +++ b/src/background/services/unifiedBridge/UnifiedBridgeService.ts @@ -4,9 +4,12 @@ import { BridgeType, createUnifiedBridgeService, Environment, + getEnabledBridgeServices, } from '@avalabs/bridge-unified'; +import { wait } from '@avalabs/core-utils-sdk'; import EventEmitter from 'events'; +import { getExponentialBackoffDelay } from '@src/utils/exponentialBackoff'; import { OnStorageReady } from '@src/background/runtime/lifecycleCallbacks'; import { NetworkService } from '../network/NetworkService'; import { StorageService } from '../storage/StorageService'; @@ -30,9 +33,10 @@ import sentryCaptureException, { @singleton() export class UnifiedBridgeService implements OnStorageReady { - #core: ReturnType; + #core?: ReturnType; #eventEmitter = new EventEmitter(); #state = UNIFIED_BRIDGE_DEFAULT_STATE; + #failedInitAttempts = 0; // We'll re-create the #core instance when one of these flags is toggled. #flagStates: Partial = {}; @@ -49,11 +53,11 @@ export class UnifiedBridgeService implements OnStorageReady { this.#flagStates = this.#getTrackedFlags( this.featureFlagService.featureFlags ); - this.#core = this.#createService(); + this.#recreateService(); // When testnet mode is toggled, we need to recreate the instance. this.networkService.developerModeChanged.add(() => { - this.#core = this.#createService(); + this.#recreateService(); }); // When some of the feature flags change, we need to recreate the instance. @@ -65,7 +69,7 @@ export class UnifiedBridgeService implements OnStorageReady { if (JSON.stringify(newFlags) !== JSON.stringify(this.#flagStates)) { this.#flagStates = newFlags; - this.#core = this.#createService(); + this.#recreateService(); } } ); @@ -104,51 +108,77 @@ export class UnifiedBridgeService implements OnStorageReady { }); } - #updateBridgeAddresses() { - const addresses: string[] = []; - - this.#core.bridges.forEach((bridge) => { - if (bridge.config) { - addresses.push( - ...bridge.config.map( - ({ tokenRouterAddress }) => tokenRouterAddress as string - ) - ); - } - }); - - this.#saveState({ - ...this.#state, - addresses, - }); - } - #getDisabledBridges(): BridgeType[] { const bridges: BridgeType[] = []; if (!this.#flagStates[FeatureGates.UNIFIED_BRIDGE_CCTP]) { bridges.push(BridgeType.CCTP); } - return bridges; } - #createService() { - const core = createUnifiedBridgeService({ - environment: this.networkService.isMainnet() - ? Environment.PROD - : Environment.TEST, - disabledBridgeTypes: this.#getDisabledBridges(), - }); - core.init().then(async () => { - this.#updateBridgeAddresses(); + async #recreateService() { + const environment = this.networkService.isMainnet() + ? Environment.PROD + : Environment.TEST; + + try { + this.#core = createUnifiedBridgeService({ + environment, + enabledBridgeServices: await getEnabledBridgeServices( + environment, + this.#getDisabledBridges() + ), + }); + this.#failedInitAttempts = 0; this.#trackPendingTransfers(); - }); + } catch (err: any) { + // If it failed, it's most likely a network issue. + // Wait a bit and try again. + this.#failedInitAttempts += 1; + + const delay = getExponentialBackoffDelay({ + attempt: this.#failedInitAttempts, + startsAfter: 1, + }); + + sentryCaptureException(err, SentryExceptionTypes.UNIFIED_BRIDGE); + console.log( + `Initialization of UnifiedBridgeService failed, attempt #${ + this.#failedInitAttempts + }. Retry in ${delay / 1000}s` + ); + + await wait(delay); - return core; + // Do not attempt again if it succeded in the meantime + // (e.g. user switched developer mode or feature flags updated) + if (this.#failedInitAttempts > 0) { + this.#recreateService(); + } + } + } + + isBridgeAddress(...addresses: string[]): boolean { + if (!this.#core) { + return false; + } + + return this.#core.isBridgeAddress(...addresses); } trackTransfer(bridgeTransfer: BridgeTransfer) { + if (!this.#core) { + // Just log that this happened. This is edge-casey, but technically possible. + sentryCaptureException( + new Error( + `UnifiedBridge - tracking attempted with no service insantiated.` + ), + SentryExceptionTypes.UNIFIED_BRIDGE + ); + return; + } + const { result } = this.#core.trackTransfer({ bridgeTransfer, updateListener: async (transfer) => { diff --git a/src/background/services/unifiedBridge/models.ts b/src/background/services/unifiedBridge/models.ts index 0861cd01..700a2b87 100644 --- a/src/background/services/unifiedBridge/models.ts +++ b/src/background/services/unifiedBridge/models.ts @@ -16,14 +16,12 @@ export enum UnifiedBridgeError { export type UnifiedBridgeState = { pendingTransfers: Record; - addresses: string[]; }; export const UNIFIED_BRIDGE_TRACKED_FLAGS = [FeatureGates.UNIFIED_BRIDGE_CCTP]; export const UNIFIED_BRIDGE_DEFAULT_STATE: UnifiedBridgeState = { pendingTransfers: {}, - addresses: [], }; export const UNIFIED_BRIDGE_STATE_STORAGE_KEY = 'UNIFIED_BRIDGE_STATE'; diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index 46a2185b..aa6a0f77 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -47,6 +47,7 @@ import { NetworkVMType } from '@avalabs/core-chains-sdk'; import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; import { EthSendTransactionHandler } from '@src/background/services/wallet/handlers/eth_sendTransaction'; import { UnifiedBridgeTrackTransfer } from '@src/background/services/unifiedBridge/handlers/unifiedBridgeTrackTransfer'; +import { lowerCaseKeys } from '@src/utils/lowerCaseKeys'; export interface UnifiedBridgeContext { estimateTransferGas( @@ -54,7 +55,7 @@ export interface UnifiedBridgeContext { amount: bigint, targetChainId: number ): Promise; - getAssetAddressOnTargetChain( + getAssetIdentifierOnTargetChain( symbol?: string, chainId?: number ): string | undefined; @@ -63,6 +64,7 @@ export interface UnifiedBridgeContext { amount: bigint, targetChainId: number ): Promise; + isBridgeAddress(...addresses: string[]): boolean; supportsAsset(address: string, targetChainId: number): boolean; transferAsset( symbol: string, @@ -79,7 +81,7 @@ const DEFAULT_STATE = { estimateTransferGas() { throw new Error('Bridge not ready'); }, - getAssetAddressOnTargetChain() { + getAssetIdentifierOnTargetChain() { return undefined; }, getErrorMessage() { @@ -88,6 +90,9 @@ const DEFAULT_STATE = { supportsAsset() { return false; }, + isBridgeAddress() { + return false; + }, transferAsset() { throw new Error('Bridge not ready'); }, @@ -128,7 +133,10 @@ export function UnifiedBridgeProvider({ const { featureFlags } = useFeatureFlagContext(); const isCCTPDisabled = !featureFlags[FeatureGates.UNIFIED_BRIDGE_CCTP]; const disabledBridgeTypes = useMemo( - () => (isCCTPDisabled ? [BridgeType.CCTP] : []), + () => + isCCTPDisabled + ? [BridgeType.CCTP, BridgeType.ICTT_ERC20_ERC20] + : [BridgeType.ICTT_ERC20_ERC20], [isCCTPDisabled] ); @@ -138,7 +146,7 @@ export function UnifiedBridgeProvider({ } return activeNetwork.isTestnet ? Environment.TEST : Environment.PROD; - }, []); + }, [activeNetwork?.isTestnet]); const [enabledBridgeServices, setEnabledBridgeServices] = useState(); @@ -336,17 +344,19 @@ export function UnifiedBridgeProvider({ const asset = getAsset(symbol, activeNetwork.chainId); assert(asset, UnifiedBridgeError.UnknownAsset); - const feeMap = await core.getFees({ - asset, - amount, - targetChain: buildChain(targetChainId), - sourceChain: buildChain(activeNetwork.chainId), - }); + const feeMap = lowerCaseKeys( + await core.getFees({ + asset, + amount, + targetChain: buildChain(targetChainId), + sourceChain: buildChain(activeNetwork.chainId), + }) + ); const identifier = asset.type === TokenType.NATIVE ? asset.symbol : asset.address; - return feeMap[identifier] ?? 0n; + return feeMap[identifier.toLowerCase()] ?? 0n; }, [activeNetwork, core, buildChain, getAsset] ); @@ -380,7 +390,7 @@ export function UnifiedBridgeProvider({ [activeNetwork, core, buildParams, getAsset] ); - const getAssetAddressOnTargetChain = useCallback( + const getAssetIdentifierOnTargetChain = useCallback( (symbol?: string, targetChainId?: number) => { if (!symbol || !targetChainId) { return; @@ -388,7 +398,11 @@ export function UnifiedBridgeProvider({ const asset = getAsset(symbol, targetChainId); - return asset?.address; + if (!asset) { + return; + } + + return asset.type === TokenType.NATIVE ? asset.symbol : asset.address; }, [getAsset] ); @@ -502,13 +516,23 @@ export function UnifiedBridgeProvider({ [t] ); + const isBridgeAddress = useCallback( + (...addresses: string[]) => { + assert(core, CommonError.Unknown); + + return core.isBridgeAddress(...addresses); + }, + [core] + ); + return ( (); + const [currentAssetIdentifier, setCurrentAssetIdentifier] = + useState(); const { amount, setAmount, bridgeFee, provider, minimum, targetChainId } = - useBridge(currentAssetAddress); + useBridge(currentAssetIdentifier); const { bridgeConfig, @@ -66,7 +67,7 @@ export function Bridge() { const bridgeConfigError = bridgeConfig.error; const { t } = useTranslation(); const availableBlockchains = useAvailableBlockchains(); - const { getAssetAddressOnTargetChain } = useUnifiedBridgeContext(); + const { getAssetIdentifierOnTargetChain } = useUnifiedBridgeContext(); const { isFunctionAvailable } = useIsFunctionAvailable(FunctionNames.BRIDGE); @@ -140,7 +141,7 @@ export function Bridge() { // that also calls setCurrentAsset :( const timer = setTimeout(() => { setCurrentAsset(symbol); - setCurrentAssetAddress( + setCurrentAssetIdentifier( bridgePageHistoryData.selectedTokenAddress ?? '' ); }, 1); @@ -153,7 +154,7 @@ export function Bridge() { bridgePageHistoryData.selectedToken, bridgePageHistoryData.selectedTokenAddress, currentAsset, - currentAssetAddress, + currentAssetIdentifier, setCurrentAsset, sourceAssets, bridgeConfig, @@ -161,8 +162,7 @@ export function Bridge() { targetBlockchain, ]); - const isAmountTooLow = - amount && !amount.eq(BIG_ZERO) && amount.lt(minimum || BIG_ZERO); + const [isAmountTooLow, setIsAmountTooLow] = useState(false); const onInitiated = useCallback(() => { captureEncrypted('BridgeTransferStarted', { @@ -253,12 +253,12 @@ export function Bridge() { if (blockChainNetwork) { setNetwork(blockChainNetwork); - const assetAddressOnOppositeChain = getAssetAddressOnTargetChain( + const assetAddressOnOppositeChain = getAssetIdentifierOnTargetChain( currentAsset, blockChainNetwork.chainId ); - setCurrentAssetAddress(assetAddressOnOppositeChain); + setCurrentAssetIdentifier(assetAddressOnOppositeChain); setNavigationHistoryData({ selectedTokenAddress: assetAddressOnOppositeChain, selectedToken: currentAsset, @@ -273,14 +273,14 @@ export function Bridge() { [ amount, bridgeConfig, - getAssetAddressOnTargetChain, + getAssetIdentifierOnTargetChain, currentAsset, networks, setAmount, setNavigationHistoryData, setNetwork, setBridgeError, - setCurrentAssetAddress, + setCurrentAssetIdentifier, ] ); @@ -326,13 +326,14 @@ export function Bridge() { availableBlockchains, bridgeError, isAmountTooLow, + setIsAmountTooLow, provider, setAmount, setBridgeError, - setCurrentAssetAddress, + setCurrentAssetIdentifier, setNavigationHistoryData, targetNetwork, - currentAssetAddress, + currentAssetIdentifier, }; if ( @@ -348,7 +349,7 @@ export function Bridge() { onBackClick={() => { // We need to reset the current asset when the user purposefully navigates away from Bridge. // That's because this kind of action will clear the data we saved in NavigationHistoryService, - // therefore leaving us with no "currentAssetAddress", without which we cannot distinguish between + // therefore leaving us with no "currentAssetIdentifier", without which we cannot distinguish between // USDC and USDC.e // Closing & reopening of the extension will still work & load the previous form values, // because this action does not clear the data in NavigationHistoryService. diff --git a/src/pages/Bridge/BridgeTransactionStatus.tsx b/src/pages/Bridge/BridgeTransactionStatus.tsx index c87013de..7fbcbfd3 100644 --- a/src/pages/Bridge/BridgeTransactionStatus.tsx +++ b/src/pages/Bridge/BridgeTransactionStatus.tsx @@ -80,7 +80,14 @@ const BridgeTransactionStatus = () => { [params.txHash, bridgeTransactions] ); - const coingeckoId = useCoinGeckoId(bridgeTransaction?.symbol); + const symbol = useMemo( + () => + isUnifiedBridgeTransfer(bridgeTransaction) + ? bridgeTransaction.asset.symbol + : bridgeTransaction?.symbol, + [bridgeTransaction] + ); + const coingeckoId = useCoinGeckoId(symbol); const logoUri = useLogoUriForBridgeTransaction(bridgeTransaction); const { networks } = useNetworkContext(); @@ -157,7 +164,7 @@ const BridgeTransactionStatus = () => { ? errorMessage : t(`You transferred {{amount}} {{symbol}}!`, { amount, - symbol: bridgeTransaction.symbol, + symbol, })} , { duration: Infinity } @@ -247,7 +254,7 @@ const BridgeTransactionStatus = () => { variant="h6" sx={{ ml: 1, color: 'text.secondary' }} > - {bridgeTransaction.symbol} + {symbol} @@ -589,7 +596,7 @@ const BridgeTransactionStatus = () => { / { isUnifiedBridgeTransfer(bridgeTransaction) - ? bridgeTransaction.requiredTargetConfirmationCount + ? bridgeTransaction.targetRequiredConfirmationCount : 1 // With legacy Avalanche Bridge, we just need 1 confirmation on the destination network } diff --git a/src/pages/Bridge/components/BridgeForm.tsx b/src/pages/Bridge/components/BridgeForm.tsx index b6c27238..00721a80 100644 --- a/src/pages/Bridge/components/BridgeForm.tsx +++ b/src/pages/Bridge/components/BridgeForm.tsx @@ -12,7 +12,15 @@ import { Typography, useTheme, } from '@avalabs/core-k2-components'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Asset, @@ -68,15 +76,16 @@ export type BridgeFormProps = { isPending: boolean; // Generic props - currentAssetAddress?: string; + currentAssetIdentifier?: string; provider: BridgeProviders; amount: Big; isAmountTooLow: boolean; + setIsAmountTooLow: Dispatch>; availableBlockchains: Blockchain[]; targetNetwork?: Network; bridgeError: string; setBridgeError: (err: string) => void; - setCurrentAssetAddress: (assetAddress?: string) => void; + setCurrentAssetIdentifier: (assetAddress?: string) => void; setNavigationHistoryData: (data: NavigationHistoryDataState) => void; setAmount: (amount: Big) => void; onTransfer: () => void; @@ -84,10 +93,11 @@ export type BridgeFormProps = { }; export const BridgeForm = ({ - currentAssetAddress, + currentAssetIdentifier, provider, amount, isAmountTooLow, + setIsAmountTooLow, availableBlockchains, minimum, maximum, @@ -100,7 +110,7 @@ export const BridgeForm = ({ estimateGas, bridgeError, setBridgeError, - setCurrentAssetAddress, + setCurrentAssetIdentifier, setNavigationHistoryData, setAmount, onTransfer, @@ -122,7 +132,7 @@ export const BridgeForm = ({ const { setNetwork, networks } = useNetworkContext(); const { currencyFormatter, currency } = useSettingsContext(); - const { getAssetAddressOnTargetChain } = useUnifiedBridgeContext(); + const { getAssetIdentifierOnTargetChain } = useUnifiedBridgeContext(); const { getTokenSymbolOnNetwork } = useGetTokenSymbolOnNetwork(); const { sendTokenSelectedAnalytics, sendAmountEnteredAnalytics } = useSendAnalyticsData(); @@ -204,6 +214,14 @@ export const BridgeForm = ({ const [neededGas, setNeededGas] = useState(0n); + useEffect(() => { + if (minimum && amount.lt(minimum)) { + setIsAmountTooLow(true); + } else { + setIsAmountTooLow(false); + } + }, [minimum, amount, setIsAmountTooLow]); + useEffect(() => { let isMounted = true; @@ -285,7 +303,7 @@ export const BridgeForm = ({ (value: { bn: BN; amount: string }) => { const bigValue = bnToBig(value.bn, denomination); setNavigationHistoryData({ - selectedTokenAddress: currentAssetAddress, + selectedTokenAddress: currentAssetIdentifier, selectedToken: currentAsset, inputAmount: bigValue, }); @@ -313,7 +331,7 @@ export const BridgeForm = ({ bridgeError, capture, currentAsset, - currentAssetAddress, + currentAssetIdentifier, setBridgeError, denomination, maximum, @@ -329,7 +347,7 @@ export const BridgeForm = ({ const symbol = token.symbol; const address = getTokenAddress(token); - setCurrentAssetAddress(address); + setCurrentAssetIdentifier(address); setNavigationHistoryData({ selectedToken: symbol, selectedTokenAddress: address, @@ -351,7 +369,7 @@ export const BridgeForm = ({ sendTokenSelectedAnalytics, setAmount, setCurrentAsset, - setCurrentAssetAddress, + setCurrentAssetIdentifier, setNavigationHistoryData, ] ); @@ -366,12 +384,12 @@ export const BridgeForm = ({ ); if (blockChainNetwork) { - const assetAddressOnOppositeChain = getAssetAddressOnTargetChain( + const assetAddressOnOppositeChain = getAssetIdentifierOnTargetChain( currentAsset, blockChainNetwork.chainId ); - setCurrentAssetAddress(assetAddressOnOppositeChain); + setCurrentAssetIdentifier(assetAddressOnOppositeChain); setNavigationHistoryData({ selectedTokenAddress: assetAddressOnOppositeChain, selectedToken: currentAsset, @@ -384,11 +402,11 @@ export const BridgeForm = ({ } }, [ bridgeConfig, - getAssetAddressOnTargetChain, + getAssetIdentifierOnTargetChain, networks, setNetwork, setBridgeError, - setCurrentAssetAddress, + setCurrentAssetIdentifier, currentAsset, setAmount, setNavigationHistoryData, diff --git a/src/pages/Bridge/components/BridgeFormUnified.tsx b/src/pages/Bridge/components/BridgeFormUnified.tsx index 1403612c..7e5bc04f 100644 --- a/src/pages/Bridge/components/BridgeFormUnified.tsx +++ b/src/pages/Bridge/components/BridgeFormUnified.tsx @@ -11,7 +11,7 @@ type BridgeFormUnifiedProps = Omit< > & { amount: Big; targetChainId: number; - currentAssetAddress?: string; + currentAssetIdentifier?: string; onInitiated: () => void; onSuccess: (txHash: string) => void; @@ -26,12 +26,12 @@ export const BridgeFormUnified = ({ onRejected, ...props }: BridgeFormUnifiedProps) => { - const { amount, targetChainId, currentAssetAddress } = props; + const { amount, targetChainId, currentAssetIdentifier } = props; const { transfer, ...bridge } = useUnifiedBridge( amount, targetChainId, - currentAssetAddress + currentAssetIdentifier ); const { onTransfer, isPending } = useBridgeTxHandling({ diff --git a/src/pages/Bridge/hooks/useBridge.ts b/src/pages/Bridge/hooks/useBridge.ts index f8b750b9..4f87d9e7 100644 --- a/src/pages/Bridge/hooks/useBridge.ts +++ b/src/pages/Bridge/hooks/useBridge.ts @@ -57,7 +57,7 @@ export enum BridgeProviders { Unified, } -export function useBridge(currentAssetAddress?: string): Bridge { +export function useBridge(currentAssetIdentifier?: string): Bridge { const { targetBlockchain } = useBridgeSDK(); const { supportsAsset } = useUnifiedBridgeContext(); @@ -99,7 +99,8 @@ export function useBridge(currentAssetAddress?: string): Bridge { bridgeFee, targetChainId, provider: - currentAssetAddress && supportsAsset(currentAssetAddress, targetChainId) + currentAssetIdentifier && + supportsAsset(currentAssetIdentifier, targetChainId) ? BridgeProviders.Unified : BridgeProviders.Avalanche, }; diff --git a/src/pages/Bridge/hooks/useBridgeAmounts.ts b/src/pages/Bridge/hooks/useBridgeAmounts.ts index 2bd60bfb..9e73e506 100644 --- a/src/pages/Bridge/hooks/useBridgeAmounts.ts +++ b/src/pages/Bridge/hooks/useBridgeAmounts.ts @@ -39,7 +39,7 @@ export const useBridgeAmounts = ( return { amount: isUnifiedBridgeTransfer(bridgeTx) - ? bigintToBig(bridgeTx.amount, bridgeTx.amountDecimals) + ? bigintToBig(bridgeTx.amount, bridgeTx.asset.decimals) : bridgeTx?.amount, sourceNetworkFee, targetNetworkFee, diff --git a/src/pages/Bridge/hooks/useBridgeTransferStatus.ts b/src/pages/Bridge/hooks/useBridgeTransferStatus.ts index c320488e..7a2112eb 100644 --- a/src/pages/Bridge/hooks/useBridgeTransferStatus.ts +++ b/src/pages/Bridge/hooks/useBridgeTransferStatus.ts @@ -22,15 +22,15 @@ export const useBridgeTransferStatus = ( // cap the current confirmations so we don't go over sourceCurrentConfirmations: Math.min( bridgeTx.sourceConfirmationCount, - bridgeTx.requiredSourceConfirmationCount + bridgeTx.sourceRequiredConfirmationCount ), targetCurrentConfirmations: Math.min( bridgeTx.targetConfirmationCount, - bridgeTx.requiredTargetConfirmationCount + bridgeTx.targetRequiredConfirmationCount ), // with Unified Bridge, the SDK provides info about the target confirmations - sourceRequiredConfirmations: bridgeTx.requiredSourceConfirmationCount, - targetRequiredConfirmations: bridgeTx.requiredTargetConfirmationCount, + sourceRequiredConfirmations: bridgeTx.sourceRequiredConfirmationCount, + targetRequiredConfirmations: bridgeTx.targetRequiredConfirmationCount, }; } diff --git a/src/pages/Bridge/hooks/useLogoUriForBridgeTransaction.ts b/src/pages/Bridge/hooks/useLogoUriForBridgeTransaction.ts index 5fff2af6..feed5ba8 100644 --- a/src/pages/Bridge/hooks/useLogoUriForBridgeTransaction.ts +++ b/src/pages/Bridge/hooks/useLogoUriForBridgeTransaction.ts @@ -8,6 +8,7 @@ import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; import { findTokenForAsset } from '../utils/findTokenForAsset'; import { networkToBlockchain } from '../utils/blockchainConversion'; import { caipToChainId } from '@src/utils/caipConversion'; +import { getBridgedAssetSymbol } from '@src/utils/bridge/getBridgedAssetSymbol'; export function useLogoUriForBridgeTransaction( bridgeTransaction: BridgeTransaction | BridgeTransfer | undefined @@ -50,7 +51,7 @@ export function useLogoUriForBridgeTransaction( } const token = findTokenForAsset( - bridgeTransaction.symbol, + getBridgedAssetSymbol(bridgeTransaction), targetBlockchain, tokens ); diff --git a/src/pages/Bridge/hooks/useUnifiedBridge.ts b/src/pages/Bridge/hooks/useUnifiedBridge.ts index 47bf55fb..835ff5e9 100644 --- a/src/pages/Bridge/hooks/useUnifiedBridge.ts +++ b/src/pages/Bridge/hooks/useUnifiedBridge.ts @@ -24,7 +24,7 @@ import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; export function useUnifiedBridge( amount: Big, targetChainId: number, - currentAssetAddress?: string + currentAssetIdentifier?: string ): BridgeAdapter { const { currentAsset, @@ -48,14 +48,14 @@ export function useUnifiedBridge( isEthereum ? Blockchain.ETHEREUM : Blockchain.AVALANCHE ); const sourceBalance = useMemo(() => { - if (!currentAsset || !currentAssetAddress || !network) { + if (!currentAsset || !currentAssetIdentifier || !network) { return undefined; } return assetsWithBalances.find(({ asset }) => { return isUnifiedBridgeAsset(asset) && asset.symbol === currentAsset; }); - }, [network, assetsWithBalances, currentAssetAddress, currentAsset]); + }, [network, assetsWithBalances, currentAssetIdentifier, currentAsset]); useEffect(() => { if (!maximum && sourceBalance?.balance) { @@ -69,9 +69,9 @@ export function useUnifiedBridge( if ( currentAsset && currentAssetData && - currentAssetAddress && + currentAssetIdentifier && amount && - supportsAsset(currentAssetAddress, targetChainId) + supportsAsset(currentAssetIdentifier, targetChainId) ) { const hasAmount = amount && !amount.eq(BIG_ZERO); @@ -103,7 +103,7 @@ export function useUnifiedBridge( }, [ currentAsset, currentAssetData, - currentAssetAddress, + currentAssetIdentifier, amount, targetChainId, getFee, diff --git a/src/pages/Bridge/utils/getBalances.ts b/src/pages/Bridge/utils/getBalances.ts index 892ad607..60aa8d22 100644 --- a/src/pages/Bridge/utils/getBalances.ts +++ b/src/pages/Bridge/utils/getBalances.ts @@ -6,7 +6,10 @@ import { TokenType, getTokenPrice, } from '@src/background/services/balances/models'; -import { BridgeAsset } from '@avalabs/bridge-unified'; +import { + BridgeAsset, + TokenType as UnifiedTokenType, +} from '@avalabs/bridge-unified'; import { isUnifiedBridgeAsset } from './isUnifiedBridgeAsset'; import { normalizeBalance } from '@src/utils/normalizeBalance'; @@ -34,7 +37,11 @@ export function getBalances( return assets.map((asset) => { const symbol = asset.symbol; const token = isUnifiedBridgeAsset(asset) - ? tokensByAddress[asset.address?.toLowerCase() ?? asset.symbol] + ? tokensByAddress[ + asset.type === UnifiedTokenType.NATIVE + ? asset.symbol.toLowerCase() + : asset.address.toLowerCase() + ] : isNativeAsset(asset) ? tokensByAddress[asset.symbol.toLowerCase()] : isBtcAsset(asset) diff --git a/src/pages/Bridge/utils/getTokenAddress.ts b/src/pages/Bridge/utils/getTokenAddress.ts index 2eb98251..77cf0403 100644 --- a/src/pages/Bridge/utils/getTokenAddress.ts +++ b/src/pages/Bridge/utils/getTokenAddress.ts @@ -1,10 +1,11 @@ import { isEthAsset } from '@avalabs/core-bridge-sdk'; import { AssetBalance } from '../models'; import { isUnifiedBridgeAsset } from './isUnifiedBridgeAsset'; +import { TokenType } from '@avalabs/bridge-unified'; export const getTokenAddress = (token: AssetBalance): string => { if (isUnifiedBridgeAsset(token.asset)) { - return token.asset.address ?? ''; + return token.asset.type === TokenType.ERC20 ? token.asset.address : ''; } else if (isEthAsset(token.asset)) { return token.asset.nativeContractAddress; } diff --git a/src/pages/Wallet/WalletRecentTxs.tsx b/src/pages/Wallet/WalletRecentTxs.tsx index 6d4979e2..8c007eef 100644 --- a/src/pages/Wallet/WalletRecentTxs.tsx +++ b/src/pages/Wallet/WalletRecentTxs.tsx @@ -42,6 +42,7 @@ import { import { isXchainNetwork } from '@src/background/services/network/utils/isAvalancheXchainNetwork'; import { getAddressForChain } from '@src/utils/getAddressForChain'; import { XchainActivityCard } from './components/History/components/ActivityCard/XchainActivityCard'; +import { getBridgedAssetSymbol } from '@src/utils/bridge/getBridgedAssetSymbol'; type WalletRecentTxsProps = { isEmbedded?: boolean; @@ -145,7 +146,7 @@ export function WalletRecentTxs({ const filteredBridgeTransactions = tokenSymbolFilter ? Object.values(bridgeTransactions).filter( - (tx) => tx.symbol === tokenSymbolFilter + (tx) => getBridgedAssetSymbol(tx) === tokenSymbolFilter ) : bridgeTransactions; diff --git a/src/pages/Wallet/components/History/components/InProgressBridge/InProgressBridgeActivityCard.tsx b/src/pages/Wallet/components/History/components/InProgressBridge/InProgressBridgeActivityCard.tsx index 1b9ad6c6..ba5354d8 100644 --- a/src/pages/Wallet/components/History/components/InProgressBridge/InProgressBridgeActivityCard.tsx +++ b/src/pages/Wallet/components/History/components/InProgressBridge/InProgressBridgeActivityCard.tsx @@ -65,7 +65,7 @@ export function InProgressBridgeActivityCard({ if (isUnifiedBridgeTransfer(tx)) { const totalConfirmationsRequired = - tx.requiredSourceConfirmationCount + tx.requiredTargetConfirmationCount; + tx.sourceRequiredConfirmationCount + tx.targetRequiredConfirmationCount; const totalConfirmationsObtained = tx.sourceConfirmationCount + tx.targetConfirmationCount; @@ -85,9 +85,13 @@ export function InProgressBridgeActivityCard({ return (currentCount / confirmationCount) * 100; }, [tx]); + const symbol = useMemo( + () => (isUnifiedBridgeTransfer(tx) ? tx.asset.symbol : tx?.symbol), + [tx] + ); const amount = useMemo(() => { if (isUnifiedBridgeTransfer(tx)) { - return bigintToBig(tx.amount, tx.amountDecimals); + return bigintToBig(tx.amount, tx.asset.decimals); } return tx.amount; @@ -114,7 +118,7 @@ export function InProgressBridgeActivityCard({ {isSuccessful ? t(`You transferred {{amount}} {{symbol}}`, { amount, - symbol: tx.symbol, + symbol, }) : tx.errorCode ? getErrorMessage(tx.errorCode) @@ -127,7 +131,15 @@ export function InProgressBridgeActivityCard({ } removeBridgeTransaction(tx.sourceTxHash); - }, [removeBridgeTransaction, t, toastShown, tx, amount, getErrorMessage]); + }, [ + removeBridgeTransaction, + t, + toastShown, + tx, + amount, + getErrorMessage, + symbol, + ]); const errorCode = isUnifiedBridgeTransfer(tx) ? tx.errorCode : undefined; const hasError = typeof errorCode !== 'undefined'; @@ -188,7 +200,7 @@ export function InProgressBridgeActivityCard({ variant="body2" sx={{ color: theme.palette.primary.dark }} > - {tx.symbol} + {symbol} diff --git a/src/pages/Wallet/components/History/useBlockchainNames.ts b/src/pages/Wallet/components/History/useBlockchainNames.ts index fb410d0c..4c36a931 100644 --- a/src/pages/Wallet/components/History/useBlockchainNames.ts +++ b/src/pages/Wallet/components/History/useBlockchainNames.ts @@ -10,13 +10,12 @@ import { isPendingBridgeTransaction, } from '@src/utils/bridgeTransactionUtils'; import { caipToChainId } from '@src/utils/caipConversion'; +import { getBridgedAssetSymbol } from '@src/utils/bridge/getBridgedAssetSymbol'; export function useBlockchainNames( item: TxHistoryItem | BridgeTransaction | BridgeTransfer ) { - const { - state: { addresses }, - } = useUnifiedBridgeContext(); + const { isBridgeAddress } = useUnifiedBridgeContext(); const pending = isPendingBridgeTransaction(item); if (pending) { @@ -35,7 +34,7 @@ export function useBlockchainNames( } const isToAvalanche = isTxToAvalanche(item); - const txBlockchain = getTxBlockchain(item, addresses); + const txBlockchain = getTxBlockchain(item, isBridgeAddress); return { sourceBlockchain: isToAvalanche ? txBlockchain : 'Avalanche', @@ -83,9 +82,11 @@ function isTxToAvalanche( function getTxBlockchain( tx: TxHistoryItem | BridgeTransaction | BridgeTransfer, - addresses: string[] + isBridgeAddress: (...addresses: string[]) => boolean ) { - const symbol = isBridgeTransaction(tx) ? tx.symbol : tx.tokens?.[0]?.symbol; + const symbol = isBridgeTransaction(tx) + ? getBridgedAssetSymbol(tx) + : tx.tokens?.[0]?.symbol; const ethereum = 'Ethereum'; const bitcoin = 'Bitcoin'; @@ -98,10 +99,7 @@ function getTxBlockchain( return ethereum; } - if ( - addresses.includes(tx.to.toLowerCase()) || - addresses.includes(tx.from.toLowerCase()) - ) { + if (isBridgeAddress(tx.to, tx.from)) { return ethereum; } } diff --git a/src/utils/bridge/getBridgedAssetSymbol.ts b/src/utils/bridge/getBridgedAssetSymbol.ts new file mode 100644 index 00000000..b578e873 --- /dev/null +++ b/src/utils/bridge/getBridgedAssetSymbol.ts @@ -0,0 +1,13 @@ +import { BridgeTransfer } from '@avalabs/bridge-unified'; +import { BridgeTransaction } from '@avalabs/core-bridge-sdk'; +import { isUnifiedBridgeTransfer } from '@src/pages/Bridge/utils/isUnifiedBridgeTransfer'; + +export const getBridgedAssetSymbol = ( + tx: BridgeTransfer | BridgeTransaction +): string => { + if (isUnifiedBridgeTransfer(tx)) { + return tx.asset.symbol; + } + + return tx.symbol; +}; diff --git a/src/utils/lowerCaseKeys.ts b/src/utils/lowerCaseKeys.ts new file mode 100644 index 00000000..c47c3136 --- /dev/null +++ b/src/utils/lowerCaseKeys.ts @@ -0,0 +1,5 @@ +export const lowerCaseKeys = (obj: Record): Record => { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]) + ); +}; diff --git a/yarn.lock b/yarn.lock index 18dd1291..56876614 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,10 +56,10 @@ bn.js "5.2.1" zod "3.23.8" -"@avalabs/bridge-unified@0.0.0-fix-make-sign-optional-for-gas-20240905074019": - version "0.0.0-fix-make-sign-optional-for-gas-20240905074019" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-fix-make-sign-optional-for-gas-20240905074019.tgz#543a62a639ba36a7624ed4dca22cfbc47720a3eb" - integrity sha512-a1I1kWMe4u9MZsNvBFQahNtPHefFL2RMeT+oLfbR6CClHrDyvcPURdt4mV+oV9oDSSAwK2Lg28cdfxokfkdo3A== +"@avalabs/bridge-unified@0.0.0-feat-is-bridge-address-20240909115936": + version "0.0.0-feat-is-bridge-address-20240909115936" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-is-bridge-address-20240909115936.tgz#1529f648d301db2bda671d46e8f3d5d6edb2a492" + integrity sha512-Y1AsYR3SKHJVvak6C8IyE68SFLKMmNj3U9hGLNTaB6zR6DyewGuHQByB5wJpVeompd+LHjZl7T0+juY8scneGQ== dependencies: abitype "0.9.3" lodash "4.17.21" From d774bfa0484dcbea735217974a890600d5e29439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 10 Sep 2024 12:34:43 +0200 Subject: [PATCH 03/11] chore: implement schema migration for pending CCTP transfers --- .../migrations/unified_bridge_v2.test.ts | 173 ++++++++++++++++++ .../migrations/unified_bridge_v2.ts | 115 ++++++++++++ .../storage/schemaMigrations/schemaMap.ts | 6 + 3 files changed, 294 insertions(+) create mode 100644 src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.test.ts create mode 100644 src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts diff --git a/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.test.ts b/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.test.ts new file mode 100644 index 00000000..dadcc8fd --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.test.ts @@ -0,0 +1,173 @@ +import Joi from 'joi'; + +import unified_bridge_v2 from './unified_bridge_v2'; + +describe('background/services/storage/schemaMigrations/migrations/unified_bridge_v2', () => { + const pendingTransfers = { + '0xEthToAvalanche': { + type: 'cctp', + environment: 'production', + fromAddress: '0xSourceAddress', + toAddress: '0xTargetAddress', + amount: 5000000, + amountDecimals: 6, + symbol: 'USDC', + bridgeFee: 2000000, + sourceChain: { + chainName: 'Ethereum', + chainId: 'eip155:1', + }, + sourceStartedAt: 1725963300000, + sourceTxHash: '0xSourceTxHash', + sourceNetworkFee: 100_000_000, + sourceConfirmationCount: 1, + requiredSourceConfirmationCount: 6, + + targetChain: { + chainName: 'Avalanche C-Chain', + chainId: 'eip155:43114', + }, + targetConfirmationCount: 0, + requiredTargetConfirmationCount: 2, + }, + '0xAvalancheToEth': { + type: 'cctp', + environment: 'production', + fromAddress: '0xSourceAddress', + toAddress: '0xTargetAddress', + amount: 5000000, + amountDecimals: 6, + symbol: 'USDC', + bridgeFee: 2000000, + sourceChain: { + chainName: 'Avalanche C-Chain', + chainId: 'eip155:43114', + }, + sourceStartedAt: 1725963300000, + sourceTxHash: '0xSourceTxHash', + sourceNetworkFee: 100_000_000, + sourceConfirmationCount: 8, + requiredSourceConfirmationCount: 6, + targetChain: { + chainName: 'Ethereum', + chainId: 'eip155:1', + }, + targetConfirmationCount: 2, + requiredTargetConfirmationCount: 4, + startBlockNumber: 1234567, + }, + }; + + const stateWithPendingTransfers = { + pendingTransfers, + addresses: ['0xTargetAddress'], + }; + + it('accepts correct inputs', () => { + expect( + unified_bridge_v2.previousSchema.validate(stateWithPendingTransfers) + ).toEqual({ + error: undefined, + value: stateWithPendingTransfers, + }); + }); + + it('rejects incorrect inputs', () => { + const stateWithMalformedPendingTransfers = { + pendingTransfers: [{ what: 'is', that: '?' }], + }; + + const result = unified_bridge_v2.previousSchema.validate({ + pendingTransfers: [{ what: 'is', that: '?' }], + }); + + expect(result).toEqual({ + value: stateWithMalformedPendingTransfers, + error: expect.any(Joi.ValidationError), + }); + }); + + it('migrates to v2 successfully', async () => { + expect(await unified_bridge_v2.up(stateWithPendingTransfers)).toStrictEqual( + { + pendingTransfers: { + '0xEthToAvalanche': { + type: 'cctp', + environment: 'production', + fromAddress: '0xSourceAddress', + toAddress: '0xTargetAddress', + amount: 5000000, + asset: { + decimals: 6, + symbol: 'USDC', + name: 'USD Coin', + type: 'erc20', + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + }, + bridgeFee: 2000000, + sourceChain: { + chainName: 'Ethereum', + chainId: 'eip155:1', + }, + sourceStartedAt: 1725963300000, + sourceTxHash: '0xSourceTxHash', + sourceNetworkFee: 100_000_000, + sourceConfirmationCount: 1, + sourceRequiredConfirmationCount: 6, + targetChain: { + chainName: 'Avalanche C-Chain', + chainId: 'eip155:43114', + }, + targetConfirmationCount: 0, + targetRequiredConfirmationCount: 2, + targetStartBlockNumber: undefined, + }, + '0xAvalancheToEth': { + type: 'cctp', + environment: 'production', + fromAddress: '0xSourceAddress', + toAddress: '0xTargetAddress', + amount: 5000000, + asset: { + decimals: 6, + symbol: 'USDC', + name: 'USD Coin', + type: 'erc20', + address: '0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e', + }, + bridgeFee: 2000000, + sourceChain: { + chainName: 'Avalanche C-Chain', + chainId: 'eip155:43114', + }, + sourceStartedAt: 1725963300000, + sourceTxHash: '0xSourceTxHash', + sourceNetworkFee: 100_000_000, + sourceConfirmationCount: 8, + sourceRequiredConfirmationCount: 6, + targetChain: { + chainName: 'Ethereum', + chainId: 'eip155:1', + }, + targetConfirmationCount: 2, + targetRequiredConfirmationCount: 4, + targetStartBlockNumber: 1234567, + }, + }, + version: 2, + } + ); + + expect( + await unified_bridge_v2.up({ addresses: ['0xTargetAddress'] }) + ).toStrictEqual({ + pendingTransfers: {}, + version: 2, + }); + + expect(await unified_bridge_v2.up({ pendingTransfers: {} })).toStrictEqual({ + pendingTransfers: {}, + version: 2, + }); + }); +}); diff --git a/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts b/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts new file mode 100644 index 00000000..ddd02ea2 --- /dev/null +++ b/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts @@ -0,0 +1,115 @@ +import Joi from 'joi'; + +const VERSION = 2; + +// Schemas & types below only list the relevant properties +// that actually changed. The rest is untouched & untyped. +type LegacyBridgeTransfer = { + amountDecimals: number; + symbol: string; + sourceChain: { + chainId: string; + }; + requiredSourceConfirmationCount: number; + requiredTargetConfirmationCount: number; + startBlockNumber?: number; +}; + +type NewBridgeTransfer = { + asset: { + type: 'erc20'; + address?: string; + name: string; + symbol: string; + decimals: number; + }; + sourceRequiredConfirmationCount: number; + targetRequiredConfirmationCount: number; + targetStartBlockNumber?: number; +}; + +type PreviousSchema = { + pendingTransfers?: Record; + addresses?: string[]; +}; + +type NewSchema = { + pendingTransfers?: Record; +}; + +const previousSchema = Joi.object({ + addresses: Joi.array().items(Joi.string()).optional(), + pendingTransfers: Joi.object().pattern( + Joi.string(), + Joi.object({ + amountDecimals: Joi.number(), + symbol: Joi.string(), + requiredSourceConfirmationCount: Joi.number(), + requiredTargetConfirmationCount: Joi.number(), + startBlockNumber: Joi.number(), + sourceChain: Joi.object({ chainId: Joi.string() }).unknown(true), + }).unknown(true) + ), +}); + +const getUsdcAddressByChainId = (caipId: string) => { + switch (caipId) { + case 'eip155:1': // Ethereum Mainnet + return '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; + + case 'eip155:43114': // C-Chain Mainnet + return '0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e'; + + case 'eip155:11155111': // Ethereum Sepolia + return '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; + + case 'eip155:43113': // C-Chain Fuji + return '0x5425890298aed601595a70ab815c96711a31bc65'; + } +}; + +const up = async ( + unifiedBridgeState: PreviousSchema +): Promise => { + const { pendingTransfers: oldTransfers } = unifiedBridgeState; + + const newTransfers: Record = {}; + + if (oldTransfers !== undefined) { + for (const [id, transfer] of Object.entries(oldTransfers)) { + const { + amountDecimals, + symbol, + requiredSourceConfirmationCount, + requiredTargetConfirmationCount, + startBlockNumber, + ...rest + } = transfer; + + newTransfers[id] = { + ...rest, + asset: { + // Prior to this schema upgrad, only USDC was possible to bridge (via CCTP) + decimals: amountDecimals, + symbol, + type: 'erc20', + name: 'USD Coin', + address: getUsdcAddressByChainId(rest.sourceChain.chainId), + }, + sourceRequiredConfirmationCount: requiredSourceConfirmationCount, + targetRequiredConfirmationCount: requiredTargetConfirmationCount, + targetStartBlockNumber: startBlockNumber, + }; + } + } + + return { + pendingTransfers: newTransfers, + version: VERSION, + }; +}; + +export default { + previousSchema, + up, +}; diff --git a/src/background/services/storage/schemaMigrations/schemaMap.ts b/src/background/services/storage/schemaMigrations/schemaMap.ts index 160ce8a7..6ae4a641 100644 --- a/src/background/services/storage/schemaMigrations/schemaMap.ts +++ b/src/background/services/storage/schemaMigrations/schemaMap.ts @@ -16,6 +16,8 @@ import { BALANCES_CACHE_KEY } from '../../balances/models'; import balances_v2 from './migrations/balances_v2'; import network_v3 from './migrations/network_v3'; import network_v4 from './migrations/network_v4'; +import { UNIFIED_BRIDGE_STATE_STORAGE_KEY } from '../../unifiedBridge/models'; +import unified_bridge_v2 from './migrations/unified_bridge_v2'; export type Migration = { previousSchema: Joi.Schema; @@ -108,4 +110,8 @@ export const SCHEMA_MAP = { }, ], }, + [UNIFIED_BRIDGE_STATE_STORAGE_KEY]: { + latestVersion: 2, + migrations: [{ version: 2, migration: unified_bridge_v2 }], + }, } as const; From d408cc0f6b2718a1efeac28d2a74e717147d6bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 10 Sep 2024 12:55:54 +0200 Subject: [PATCH 04/11] fix: schema migration bigint support --- .../migrations/unified_bridge_v2.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts b/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts index ddd02ea2..64097ae1 100644 --- a/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts +++ b/src/background/services/storage/schemaMigrations/migrations/unified_bridge_v2.ts @@ -46,7 +46,20 @@ const previousSchema = Joi.object({ symbol: Joi.string(), requiredSourceConfirmationCount: Joi.number(), requiredTargetConfirmationCount: Joi.number(), - startBlockNumber: Joi.number(), + startBlockNumber: Joi.custom((value) => { + // Joi has no support for BigInt + if ( + typeof value !== 'number' && + typeof value !== 'bigint' && + typeof value !== 'undefined' + ) { + return new Error( + `Expected bigint or number, received ${typeof value}` + ); + } + + return value; + }, 'bigint'), sourceChain: Joi.object({ chainId: Joi.string() }).unknown(true), }).unknown(true) ), From 8608f371e928f1a8f26992e2afa947dc14a5d4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 10 Sep 2024 13:03:42 +0200 Subject: [PATCH 05/11] chore: cleanup translations --- src/localization/locales/en/translation.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index 8b74fde5..37ae4336 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -415,7 +415,6 @@ "Insufficient funds": "Insufficient funds", "Insurance Buyer": "Insurance Buyer", "Internal error": "Internal error", - "Internal error occured. Try restarting your browser.": "Internal error occured. Try restarting your browser.", "Internal error. Please try again": "Internal error. Please try again", "Invalid Password": "Invalid Password", "Invalid QR Code": "Invalid QR Code", From 2fe74b3fb1289c0e43470346b18652099a4d4d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 10 Sep 2024 13:33:18 +0200 Subject: [PATCH 06/11] test: update failing tests --- .../services/history/HistoryServiceGlacier.test.ts | 3 ++- src/contexts/UnifiedBridgeProvider.test.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/background/services/history/HistoryServiceGlacier.test.ts b/src/background/services/history/HistoryServiceGlacier.test.ts index ff1e8f03..394af009 100644 --- a/src/background/services/history/HistoryServiceGlacier.test.ts +++ b/src/background/services/history/HistoryServiceGlacier.test.ts @@ -76,7 +76,7 @@ const mockedAccountsService = { }, } as any; const mockedUnifiedBridgeService = { - state: { addresses: [] }, + isBridgeAddress: jest.fn(), } as any; const senderAddress = 'Sender Address'; @@ -334,6 +334,7 @@ describe('background/services/history/HistoryServiceGlacier.test.ts', () => { ], }) ); + mockedUnifiedBridgeService.isBridgeAddress.mockReturnValue(false); (Glacier as jest.Mock).mockImplementation(() => { return { evmTransactions: { diff --git a/src/contexts/UnifiedBridgeProvider.test.tsx b/src/contexts/UnifiedBridgeProvider.test.tsx index d3ee521e..8bc2be6c 100644 --- a/src/contexts/UnifiedBridgeProvider.test.tsx +++ b/src/contexts/UnifiedBridgeProvider.test.tsx @@ -5,6 +5,7 @@ import { BridgeType, TokenType, createUnifiedBridgeService, + getEnabledBridgeServices, } from '@avalabs/bridge-unified'; import { useConnectionContext } from './ConnectionProvider'; @@ -133,7 +134,6 @@ describe('contexts/UnifiedBridgeProvider', () => { jest.resetAllMocks(); core = { - init: jest.fn().mockResolvedValue(undefined), getAssets: async () => ({ [chainIdToCaip(ethereum.chainId)]: [ethUSDC], [chainIdToCaip(avalanche.chainId)]: [avaxUSDC], @@ -144,7 +144,7 @@ describe('contexts/UnifiedBridgeProvider', () => { } as unknown as ReturnType; jest.mocked(createUnifiedBridgeService).mockReturnValue(core); - + jest.mocked(getEnabledBridgeServices).mockResolvedValue({} as any); // Mock events flow eventsFn.mockReturnValue({ pipe: jest.fn().mockReturnValue({ @@ -195,9 +195,9 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.transferAsset('USDC', 1000n, 1); + await provider.current?.transferAsset('USDCC', 1000n, 1); } catch (err: any) { - expect(err.data.reason).toEqual(UnifiedBridgeError.UnknownAsset); + expect(err.data?.reason).toEqual(UnifiedBridgeError.UnknownAsset); } }); }); @@ -368,7 +368,7 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.getFee('USDC', 1000n, 1); + await provider.current?.getFee('USDCc', 1000n, 1); } catch (err: any) { expect(err.data.reason).toEqual(UnifiedBridgeError.UnknownAsset); } From af180f2a2847f5a7a655697d0d2061f729ae77a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 26 Sep 2024 14:22:59 +0200 Subject: [PATCH 07/11] refactor: update consumer-sdk, isBridgeAddress -> isBridgeTx --- package.json | 2 +- .../history/HistoryServiceBridgeHelper.ts | 2 +- .../history/HistoryServiceGlacier.test.ts | 4 ++-- .../services/history/HistoryServiceGlacier.ts | 18 ++++++++---------- .../unifiedBridge/UnifiedBridgeService.ts | 5 +++-- src/contexts/UnifiedBridgeProvider.tsx | 13 +++++++------ .../components/History/useBlockchainNames.ts | 10 +++++----- yarn.lock | 8 ++++---- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index b25b0508..4065bee2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@avalabs/avalanchejs": "4.0.5", "@avalabs/bitcoin-module": "0.5.0", - "@avalabs/bridge-unified": "0.0.0-feat-is-bridge-address-20240909115936", + "@avalabs/bridge-unified": "0.0.0-feat-is-bridge-address-20240926120228", "@avalabs/core-bridge-sdk": "3.1.0-alpha.4", "@avalabs/core-chains-sdk": "3.1.0-alpha.4", "@avalabs/core-coingecko-sdk": "3.1.0-alpha.4", diff --git a/src/background/services/history/HistoryServiceBridgeHelper.ts b/src/background/services/history/HistoryServiceBridgeHelper.ts index 106fa8e2..bec22fe7 100644 --- a/src/background/services/history/HistoryServiceBridgeHelper.ts +++ b/src/background/services/history/HistoryServiceBridgeHelper.ts @@ -32,7 +32,7 @@ export class HistoryServiceBridgeHelper { return false; } - if (this.unifiedBridgeService.isBridgeAddress(tx.from, tx.to)) { + if (this.unifiedBridgeService.isBridgeTx(tx)) { return true; } diff --git a/src/background/services/history/HistoryServiceGlacier.test.ts b/src/background/services/history/HistoryServiceGlacier.test.ts index 394af009..7735ba57 100644 --- a/src/background/services/history/HistoryServiceGlacier.test.ts +++ b/src/background/services/history/HistoryServiceGlacier.test.ts @@ -76,7 +76,7 @@ const mockedAccountsService = { }, } as any; const mockedUnifiedBridgeService = { - isBridgeAddress: jest.fn(), + isBridgeTx: jest.fn(), } as any; const senderAddress = 'Sender Address'; @@ -334,7 +334,7 @@ describe('background/services/history/HistoryServiceGlacier.test.ts', () => { ], }) ); - mockedUnifiedBridgeService.isBridgeAddress.mockReturnValue(false); + mockedUnifiedBridgeService.isBridgeTx.mockReturnValue(false); (Glacier as jest.Mock).mockImplementation(() => { return { evmTransactions: { diff --git a/src/background/services/history/HistoryServiceGlacier.ts b/src/background/services/history/HistoryServiceGlacier.ts index 65451a6c..d3acc5e3 100644 --- a/src/background/services/history/HistoryServiceGlacier.ts +++ b/src/background/services/history/HistoryServiceGlacier.ts @@ -85,14 +85,11 @@ export class HistoryServiceGlacier { return method; } - private isBridgeAddress(address?: string) { - if (!address) { - return false; - } - + private isBridgeTx({ from, to }: { from: string; to: string }) { return ( - ETHEREUM_ADDRESS === address.toLowerCase() || - this.unifiedBridgeService.isBridgeAddress(address) + ETHEREUM_ADDRESS === to?.toLowerCase() || + ETHEREUM_ADDRESS === from?.toLowerCase() || + this.unifiedBridgeService.isBridgeTx({ from: from ?? '', to: to ?? '' }) ); } @@ -103,9 +100,10 @@ export class HistoryServiceGlacier { const nativeOnly = !erc20Transfers && !erc721Transfers; const method = this.parseRawMethod(nativeTransaction.method?.methodName); - const isBridge = - this.isBridgeAddress(erc20Transfers?.[0]?.from?.address) || - this.isBridgeAddress(erc20Transfers?.[0]?.to?.address); + const isBridge = this.isBridgeTx({ + from: erc20Transfers?.[0]?.from?.address ?? '', + to: erc20Transfers?.[0]?.to?.address ?? '', + }); const isSwap = method.toLowerCase().includes('swap'); const isNativeSend = diff --git a/src/background/services/unifiedBridge/UnifiedBridgeService.ts b/src/background/services/unifiedBridge/UnifiedBridgeService.ts index c98bb33a..7ce3222b 100644 --- a/src/background/services/unifiedBridge/UnifiedBridgeService.ts +++ b/src/background/services/unifiedBridge/UnifiedBridgeService.ts @@ -5,6 +5,7 @@ import { createUnifiedBridgeService, Environment, getEnabledBridgeServices, + IsBridgeTxParams, } from '@avalabs/bridge-unified'; import { wait } from '@avalabs/core-utils-sdk'; import EventEmitter from 'events'; @@ -159,12 +160,12 @@ export class UnifiedBridgeService implements OnStorageReady { } } - isBridgeAddress(...addresses: string[]): boolean { + isBridgeTx(txInfo: IsBridgeTxParams): boolean { if (!this.#core) { return false; } - return this.#core.isBridgeAddress(...addresses); + return this.#core.isBridgeTx(txInfo); } trackTransfer(bridgeTransfer: BridgeTransfer) { diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index aa6a0f77..bf622dd0 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -18,6 +18,7 @@ import { BridgeTransfer, getEnabledBridgeServices, BridgeServicesMap, + IsBridgeTxParams, } from '@avalabs/bridge-unified'; import { ethErrors } from 'eth-rpc-errors'; import { filter, map } from 'rxjs'; @@ -64,7 +65,7 @@ export interface UnifiedBridgeContext { amount: bigint, targetChainId: number ): Promise; - isBridgeAddress(...addresses: string[]): boolean; + isBridgeTx(txInfo: IsBridgeTxParams): boolean; supportsAsset(address: string, targetChainId: number): boolean; transferAsset( symbol: string, @@ -90,7 +91,7 @@ const DEFAULT_STATE = { supportsAsset() { return false; }, - isBridgeAddress() { + isBridgeTx() { return false; }, transferAsset() { @@ -516,11 +517,11 @@ export function UnifiedBridgeProvider({ [t] ); - const isBridgeAddress = useCallback( - (...addresses: string[]) => { + const isBridgeTx = useCallback( + (txInfo: IsBridgeTxParams) => { assert(core, CommonError.Unknown); - return core.isBridgeAddress(...addresses); + return core.isBridgeTx(txInfo); }, [core] ); @@ -531,7 +532,7 @@ export function UnifiedBridgeProvider({ estimateTransferGas, getErrorMessage, state, - isBridgeAddress, + isBridgeTx, getAssetIdentifierOnTargetChain, getFee, supportsAsset, diff --git a/src/pages/Wallet/components/History/useBlockchainNames.ts b/src/pages/Wallet/components/History/useBlockchainNames.ts index 4c36a931..3952db78 100644 --- a/src/pages/Wallet/components/History/useBlockchainNames.ts +++ b/src/pages/Wallet/components/History/useBlockchainNames.ts @@ -1,5 +1,5 @@ import { Blockchain, BridgeTransaction } from '@avalabs/core-bridge-sdk'; -import { BridgeTransfer } from '@avalabs/bridge-unified'; +import { BridgeTransfer, IsBridgeTxParams } from '@avalabs/bridge-unified'; import { BITCOIN_NETWORK, ChainId } from '@avalabs/core-chains-sdk'; import { TxHistoryItem } from '@src/background/services/history/models'; import { isEthereumChainId } from '@src/background/services/network/utils/isEthereumNetwork'; @@ -15,7 +15,7 @@ import { getBridgedAssetSymbol } from '@src/utils/bridge/getBridgedAssetSymbol'; export function useBlockchainNames( item: TxHistoryItem | BridgeTransaction | BridgeTransfer ) { - const { isBridgeAddress } = useUnifiedBridgeContext(); + const { isBridgeTx } = useUnifiedBridgeContext(); const pending = isPendingBridgeTransaction(item); if (pending) { @@ -34,7 +34,7 @@ export function useBlockchainNames( } const isToAvalanche = isTxToAvalanche(item); - const txBlockchain = getTxBlockchain(item, isBridgeAddress); + const txBlockchain = getTxBlockchain(item, isBridgeTx); return { sourceBlockchain: isToAvalanche ? txBlockchain : 'Avalanche', @@ -82,7 +82,7 @@ function isTxToAvalanche( function getTxBlockchain( tx: TxHistoryItem | BridgeTransaction | BridgeTransfer, - isBridgeAddress: (...addresses: string[]) => boolean + isBridgeTx: (txInfo: IsBridgeTxParams) => boolean ) { const symbol = isBridgeTransaction(tx) ? getBridgedAssetSymbol(tx) @@ -99,7 +99,7 @@ function getTxBlockchain( return ethereum; } - if (isBridgeAddress(tx.to, tx.from)) { + if (isBridgeTx(tx)) { return ethereum; } } diff --git a/yarn.lock b/yarn.lock index 56876614..fcfe6f3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,10 +56,10 @@ bn.js "5.2.1" zod "3.23.8" -"@avalabs/bridge-unified@0.0.0-feat-is-bridge-address-20240909115936": - version "0.0.0-feat-is-bridge-address-20240909115936" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-is-bridge-address-20240909115936.tgz#1529f648d301db2bda671d46e8f3d5d6edb2a492" - integrity sha512-Y1AsYR3SKHJVvak6C8IyE68SFLKMmNj3U9hGLNTaB6zR6DyewGuHQByB5wJpVeompd+LHjZl7T0+juY8scneGQ== +"@avalabs/bridge-unified@0.0.0-feat-is-bridge-address-20240926120228": + version "0.0.0-feat-is-bridge-address-20240926120228" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-is-bridge-address-20240926120228.tgz#5cd14896a519579f2de50c7c3bd7ce28835f756c" + integrity sha512-0k7S9Ft0QvX7En7BUv7uoKfkplWv5eQDzxEIGWkMVfvAS/89aaUY8cGbyIg0hG29ogQi811bcCZIDEo05rEiGg== dependencies: abitype "0.9.3" lodash "4.17.21" From 50d431070cc78e06c3bd0b28134f6c2eab56fb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Wed, 2 Oct 2024 15:11:49 +0200 Subject: [PATCH 08/11] test: update HistoryService tests --- src/background/services/history/HistoryService.test.ts | 5 ++--- src/background/services/history/HistoryService.ts | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/background/services/history/HistoryService.test.ts b/src/background/services/history/HistoryService.test.ts index 300d6833..7b220633 100644 --- a/src/background/services/history/HistoryService.test.ts +++ b/src/background/services/history/HistoryService.test.ts @@ -41,9 +41,7 @@ describe('src/background/services/history/HistoryService.ts', () => { isBridgeTransactionBTC: jest.fn(), } as any; const unifiedBridgeServiceMock = { - state: { - addresses: [], - }, + isBridgeTx: jest.fn().mockReturnValue(false), } as any; const txHistoryItem: TxHistoryItem = { @@ -187,6 +185,7 @@ describe('src/background/services/history/HistoryService.ts', () => { ]); }); it('should return results with an pchain transaction', async () => { + jest.mocked(unifiedBridgeServiceMock.isBridgeTx).mockReturnValue(false); jest.mocked(moduleManagereMock.loadModuleByNetwork).mockResolvedValue({ getTransactionHistory: jest.fn(() => { return { diff --git a/src/background/services/history/HistoryService.ts b/src/background/services/history/HistoryService.ts index 15293fee..370e93d6 100644 --- a/src/background/services/history/HistoryService.ts +++ b/src/background/services/history/HistoryService.ts @@ -69,7 +69,8 @@ export class HistoryService { } return ( this.#isBridgeAddress(transaction.from) || - this.#isBridgeAddress(transaction.to) + this.#isBridgeAddress(transaction.to) || + this.unifiedBridgeService.isBridgeTx(transaction) ); } @@ -95,9 +96,6 @@ export class HistoryService { return false; } - return [ - ETHEREUM_ADDRESS, - ...this.unifiedBridgeService.state.addresses, - ].includes(address.toLowerCase()); + return ETHEREUM_ADDRESS === address.toLowerCase(); } } From d6c19a6f0b585a4c6ce83c8df1df2e0878a43f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Wed, 2 Oct 2024 13:47:22 +0200 Subject: [PATCH 09/11] fix: show balances for custom tokens too --- .../services/balances/BalancesService.ts | 15 +++++++++++---- src/background/services/settings/models.ts | 4 +++- src/hooks/useTokensWithBalances.ts | 7 ++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/background/services/balances/BalancesService.ts b/src/background/services/balances/BalancesService.ts index 91d9748d..f616a71d 100644 --- a/src/background/services/balances/BalancesService.ts +++ b/src/background/services/balances/BalancesService.ts @@ -6,7 +6,11 @@ import { ModuleManager } from '@src/background/vmModules/ModuleManager'; import { SettingsService } from '../settings/SettingsService'; import { getPriceChangeValues } from './utils/getPriceChangeValues'; import * as Sentry from '@sentry/browser'; -import { NetworkVMType, TokenWithBalance } from '@avalabs/vm-module-types'; +import { + NetworkVMType, + TokenType, + TokenWithBalance, +} from '@avalabs/vm-module-types'; @singleton() export class BalancesService { @@ -30,9 +34,11 @@ export class BalancesService { ); const module = await this.moduleManager.loadModuleByNetwork(network); - const currency = ( - await this.settingsService.getSettings() - ).currency.toLowerCase(); + const settings = await this.settingsService.getSettings(); + const currency = settings.currency.toLowerCase(); + const customTokens = Object.values( + settings.customTokens[network.chainId] ?? {} + ).map((t) => ({ ...t, type: TokenType.ERC20 as const })); const rawBalances = await module.getBalances({ // TODO: Use public key and module.getAddress instead to make this more modular @@ -56,6 +62,7 @@ export class BalancesService { .filter((address): address is string => !!address), network, currency, + customTokens, }); // Apply price changes data, VM Modules don't do this yet diff --git a/src/background/services/settings/models.ts b/src/background/services/settings/models.ts index 012e1854..c0e40004 100644 --- a/src/background/services/settings/models.ts +++ b/src/background/services/settings/models.ts @@ -48,7 +48,9 @@ export type AddCustomTokenData = { }; type CustomTokens = { - [chain: string]: NetworkContractToken; + [chain: string]: { + [tokenAddress: string]: NetworkContractToken; + }; }; export interface SettingsState { diff --git a/src/hooks/useTokensWithBalances.ts b/src/hooks/useTokensWithBalances.ts index d0af068b..d713fabb 100644 --- a/src/hooks/useTokensWithBalances.ts +++ b/src/hooks/useTokensWithBalances.ts @@ -60,18 +60,19 @@ export const useTokensWithBalances = ( if (!network?.chainId) { return {}; } - const customTokenForActiveNetwork = customTokens[network?.chainId]; - if (!customTokenForActiveNetwork) { + const customTokensForActiveNetwork = customTokens[network?.chainId]; + if (!customTokensForActiveNetwork) { return {}; } - return Object.entries(customTokenForActiveNetwork).reduce<{ + return Object.entries(customTokensForActiveNetwork).reduce<{ [address: string]: TokenWithBalance; }>((acc, [address, tokenData]) => { acc[address] = { ...tokenData, type: TokenType.ERC20, balance: 0n, + balanceDisplayValue: '0', }; return acc; From 66838c8326735ffd264466cac950297638a87c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 4 Oct 2024 09:36:23 +0200 Subject: [PATCH 10/11] refactor: bye avalanche bridge, hello unified bridge --- package.json | 2 +- src/components/common/TokenEllipsis.tsx | 6 +- src/components/common/TokenSelect.tsx | 25 +- src/components/common/TokenSelector.tsx | 10 +- src/contexts/UnifiedBridgeProvider.tsx | 87 +-- src/hooks/useDisplayTokenList.ts | 75 +-- src/pages/Bridge/Bridge.tsx | 374 +++++------- src/pages/Bridge/BridgeTransactionStatus.tsx | 19 +- src/pages/Bridge/components/BridgeConfirm.tsx | 31 - src/pages/Bridge/components/BridgeForm.tsx | 559 ++++++------------ .../Bridge/components/BridgeFormAVAX.tsx | 53 -- src/pages/Bridge/components/BridgeFormBTC.tsx | 50 -- src/pages/Bridge/components/BridgeFormETH.tsx | 49 -- .../Bridge/components/BridgeFormSkeleton.tsx | 71 +++ .../Bridge/components/BridgeFormUnified.tsx | 53 -- .../Bridge/components/BridgeTypeFootnote.tsx | 74 +++ .../components/BridgeUnknownNetwork.tsx | 35 +- .../Bridge/components/NetworkSelector.tsx | 155 +++-- src/pages/Bridge/hooks/useAssetBalancesEVM.ts | 146 ----- .../Bridge/hooks/useAvailableBlockchains.ts | 51 -- .../Bridge/hooks/useAvalancheBridge.test.ts | 213 ------- src/pages/Bridge/hooks/useAvalancheBridge.ts | 80 --- src/pages/Bridge/hooks/useBridge.ts | 283 ++++++--- src/pages/Bridge/hooks/useBtcBridge.test.ts | 250 -------- src/pages/Bridge/hooks/useBtcBridge.ts | 263 -------- src/pages/Bridge/hooks/useEthBridge.test.ts | 228 ------- src/pages/Bridge/hooks/useEthBridge.ts | 134 ----- src/pages/Bridge/hooks/useHasEnoughtForGas.ts | 2 +- .../hooks/useSetBridgeChainFromNetwork.ts | 19 - src/pages/Bridge/hooks/useUnifiedBridge.ts | 187 ------ src/pages/Bridge/models.ts | 16 +- .../Bridge/utils/findMatchingBridgeAsset.ts | 27 + src/pages/Bridge/utils/getBalances.ts | 65 -- src/pages/Bridge/utils/getTokenAddress.ts | 13 - .../Bridge/utils/isUnifiedBridgeAsset.ts | 5 - yarn.lock | 10 +- 36 files changed, 910 insertions(+), 2810 deletions(-) delete mode 100644 src/pages/Bridge/components/BridgeConfirm.tsx delete mode 100644 src/pages/Bridge/components/BridgeFormAVAX.tsx delete mode 100644 src/pages/Bridge/components/BridgeFormBTC.tsx delete mode 100644 src/pages/Bridge/components/BridgeFormETH.tsx create mode 100644 src/pages/Bridge/components/BridgeFormSkeleton.tsx delete mode 100644 src/pages/Bridge/components/BridgeFormUnified.tsx create mode 100644 src/pages/Bridge/components/BridgeTypeFootnote.tsx delete mode 100644 src/pages/Bridge/hooks/useAssetBalancesEVM.ts delete mode 100644 src/pages/Bridge/hooks/useAvailableBlockchains.ts delete mode 100644 src/pages/Bridge/hooks/useAvalancheBridge.test.ts delete mode 100644 src/pages/Bridge/hooks/useAvalancheBridge.ts delete mode 100644 src/pages/Bridge/hooks/useBtcBridge.test.ts delete mode 100644 src/pages/Bridge/hooks/useBtcBridge.ts delete mode 100644 src/pages/Bridge/hooks/useEthBridge.test.ts delete mode 100644 src/pages/Bridge/hooks/useEthBridge.ts delete mode 100644 src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts delete mode 100644 src/pages/Bridge/hooks/useUnifiedBridge.ts create mode 100644 src/pages/Bridge/utils/findMatchingBridgeAsset.ts delete mode 100644 src/pages/Bridge/utils/getBalances.ts delete mode 100644 src/pages/Bridge/utils/getTokenAddress.ts delete mode 100644 src/pages/Bridge/utils/isUnifiedBridgeAsset.ts diff --git a/package.json b/package.json index cecf59b1..abae4ffc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@avalabs/avalanche-module": "0.7.0", "@avalabs/avalanchejs": "4.0.5", - "@avalabs/bridge-unified": "0.0.0-feat-is-bridge-address-20240926120228", + "@avalabs/bridge-unified": "0.0.0-feat-ictt-configs-20241002113306", "@avalabs/bitcoin-module": "0.7.0", "@avalabs/core-bridge-sdk": "3.1.0-alpha.4", "@avalabs/core-chains-sdk": "3.1.0-alpha.4", diff --git a/src/components/common/TokenEllipsis.tsx b/src/components/common/TokenEllipsis.tsx index 71ba1a96..5f07353e 100644 --- a/src/components/common/TokenEllipsis.tsx +++ b/src/components/common/TokenEllipsis.tsx @@ -1,11 +1,12 @@ import { PropsWithChildren } from 'react'; -import { Tooltip } from '@avalabs/core-k2-components'; +import { SxProps, Tooltip } from '@avalabs/core-k2-components'; import { truncateAddress } from '@avalabs/core-utils-sdk'; interface TokenEllipsisProps { maxLength: number; text: string; className?: string; + sx?: SxProps; } function isTruncated(maxLength, text) { @@ -16,6 +17,7 @@ export function TokenEllipsis({ maxLength, text, className, + sx, }: PropsWithChildren) { const name = text.length <= maxLength ? text : truncateAddress(text, maxLength / 2); @@ -26,7 +28,7 @@ export function TokenEllipsis({ title={text} disableHoverListener={!isTruncated(maxLength, text)} disableFocusListener={!isTruncated(maxLength, text)} - sx={{ cursor: 'pointer' }} + sx={sx ?? { cursor: 'pointer' }} > <>{name} diff --git a/src/components/common/TokenSelect.tsx b/src/components/common/TokenSelect.tsx index 78873009..8b081232 100644 --- a/src/components/common/TokenSelect.tsx +++ b/src/components/common/TokenSelect.tsx @@ -9,7 +9,6 @@ import { } from 'react'; import { useSettingsContext } from '@src/contexts/SettingsProvider'; import { ContainedDropdown } from '@src/components/common/ContainedDropdown'; -import { AssetBalance } from '@src/pages/Bridge/models'; import EthLogo from '@src/images/tokens/eth.png'; import { hasUnconfirmedBTCBalance, @@ -45,7 +44,6 @@ const InputContainer = styled(Card)` align-items: center; padding: 8px 16px; background: ${({ theme }) => theme.palette.grey[850]}; - cursor: pointer; display: flex; `; @@ -71,7 +69,7 @@ const StyledDropdownMenuItem = styled(DropdownItem)` interface TokenSelectProps { selectedToken?: TokenWithBalance | null; - onTokenChange(token: TokenWithBalance | AssetBalance): void; + onTokenChange(token: TokenWithBalance): void; maxAmount?: bigint; inputAmount?: bigint; onInputAmountChange?(data: { amount: string; bigint: bigint }): void; @@ -83,7 +81,6 @@ interface TokenSelectProps { label?: string; selectorLabel?: string; tokensList?: TokenWithBalance[]; - bridgeTokensList?: AssetBalance[]; isValueLoading?: boolean; hideErrorMessage?: boolean; skipHandleMaxAmount?: boolean; @@ -107,7 +104,6 @@ export function TokenSelect({ isValueLoading, hideErrorMessage, skipHandleMaxAmount, - bridgeTokensList, setIsOpen, containerRef, withMaxButton = true, @@ -140,13 +136,10 @@ export function TokenSelect({ }, [onInputAmountChange, maxAmountString] ); - const hideTokenDropdown = - (bridgeTokensList && bridgeTokensList.length < 2) || - (tokensList && tokensList.length < 2); + const hideTokenDropdown = tokensList && tokensList.length < 2; const displayTokenList = useDisplaytokenlist({ tokensList, - bridgeTokensList, searchQuery, }); @@ -187,9 +180,8 @@ export function TokenSelect({ useEffect(() => { // when only one token is present, auto select it - const tokens = bridgeTokensList ?? tokensList; - const hasOnlyOneToken = tokens?.length === 1; - const theOnlyToken = hasOnlyOneToken ? tokens[0] : undefined; + const hasOnlyOneToken = tokensList?.length === 1; + const theOnlyToken = hasOnlyOneToken ? tokensList[0] : undefined; const isOnlyTokenNotSelected = theOnlyToken && theOnlyToken?.symbol !== selectedToken?.symbol; @@ -198,17 +190,16 @@ export function TokenSelect({ return; } // when selected token is not supported, clear it - const supportedSymbols = - tokens?.flatMap((tok) => [tok.symbol, tok.symbolOnNetwork]) ?? []; + const supportedSymbols = tokensList?.flatMap((tok) => tok.symbol) ?? []; if ( selectedToken && - tokens?.[0] && + tokensList?.[0] && !supportedSymbols.includes(selectedToken.symbol) ) { - onTokenChange(tokens[0]); + onTokenChange(tokensList[0]); } - }, [bridgeTokensList, tokensList, onTokenChange, selectedToken]); + }, [tokensList, onTokenChange, selectedToken]); const rowRenderer = useCallback( ({ key, index, style }) => { diff --git a/src/components/common/TokenSelector.tsx b/src/components/common/TokenSelector.tsx index b0fd8f61..dfbfe200 100644 --- a/src/components/common/TokenSelector.tsx +++ b/src/components/common/TokenSelector.tsx @@ -30,12 +30,12 @@ export function TokenSelector({ const { t } = useTranslation(); return ( @@ -51,7 +51,11 @@ export function TokenSelector({ <> {token.icon} - + {!hideCaretIcon ? ( isOpen ? ( diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index bf622dd0..f5ef6701 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -24,7 +24,6 @@ import { ethErrors } from 'eth-rpc-errors'; import { filter, map } from 'rxjs'; import { ExtensionRequest } from '@src/background/connections/extensionConnection/models'; -import { chainIdToCaip } from '@src/utils/caipConversion'; import { UNIFIED_BRIDGE_DEFAULT_STATE, UnifiedBridgeError, @@ -54,30 +53,34 @@ export interface UnifiedBridgeContext { estimateTransferGas( symbol: string, amount: bigint, - targetChainId: number + targetChainId: string ): Promise; getAssetIdentifierOnTargetChain( symbol?: string, - chainId?: number + chainId?: string ): string | undefined; getFee( symbol: string, amount: bigint, - targetChainId: number + targetChainId: string ): Promise; isBridgeTx(txInfo: IsBridgeTxParams): boolean; - supportsAsset(address: string, targetChainId: number): boolean; + supportsAsset(address: string, targetChainId: string): boolean; transferAsset( symbol: string, amount: bigint, - targetChainId: number + targetChainId: string ): Promise; getErrorMessage(errorCode: UnifiedBridgeErrorCode): string; transferableAssets: BridgeAsset[]; state: UnifiedBridgeState; + assets: ChainAssetMap; + availableChainIds: string[]; + isReady: boolean; } const DEFAULT_STATE = { + assets: {}, state: UNIFIED_BRIDGE_DEFAULT_STATE, estimateTransferGas() { throw new Error('Bridge not ready'); @@ -101,6 +104,8 @@ const DEFAULT_STATE = { throw new Error('Bridge not ready'); }, transferableAssets: [], + availableChainIds: [], + isReady: false, }; const UnifiedBridgeContext = createContext(DEFAULT_STATE); @@ -131,15 +136,26 @@ export function UnifiedBridgeProvider({ const [state, setState] = useState( UNIFIED_BRIDGE_DEFAULT_STATE ); + const [isReady, setIsReady] = useState(false); const { featureFlags } = useFeatureFlagContext(); const isCCTPDisabled = !featureFlags[FeatureGates.UNIFIED_BRIDGE_CCTP]; - const disabledBridgeTypes = useMemo( - () => - isCCTPDisabled - ? [BridgeType.CCTP, BridgeType.ICTT_ERC20_ERC20] - : [BridgeType.ICTT_ERC20_ERC20], - [isCCTPDisabled] - ); + const isICTTDisabled = false; // TODO: feature flag it + const isABDisabled = false; // TODO: feature flag it + const disabledBridgeTypes = useMemo(() => { + const disabledBridges: BridgeType[] = []; + + if (isCCTPDisabled) { + disabledBridges.push(BridgeType.CCTP); + } + if (isICTTDisabled) { + disabledBridges.push(BridgeType.ICTT_ERC20_ERC20); + } + if (isABDisabled) { + disabledBridges.push(BridgeType.AVALANCHE_EVM); + } + + return disabledBridges; + }, [isCCTPDisabled, isABDisabled, isICTTDisabled]); const environment = useMemo(() => { if (typeof activeNetwork?.isTestnet !== 'boolean') { @@ -200,6 +216,7 @@ export function UnifiedBridgeProvider({ } setAssets(chainAssetsMap); + setIsReady(true); }); return () => { @@ -207,14 +224,16 @@ export function UnifiedBridgeProvider({ }; }, [core]); + const availableChainIds = useMemo(() => Object.keys(assets ?? {}), [assets]); + const buildChain = useCallback( - (chainId: number): Chain => { - const network = getNetwork(chainId); + (caipId: string): Chain => { + const network = getNetwork(caipId); assert(network, CommonError.UnknownNetwork); return { - chainId: chainIdToCaip(network.chainId), + chainId: network.caipId, chainName: network.chainName, rpcUrl: network.rpcUrl, networkToken: { @@ -234,10 +253,7 @@ export function UnifiedBridgeProvider({ return []; } - // UnifiedBridge SDK returns the chain IDs in CAIP2 format. - // This is good, but we need to translate it to numeric chain ids - // until we make the switch in extension: - return assets[getNetworkCaipId(activeNetwork)] ?? []; + return assets[activeNetwork.caipId] ?? []; }, [activeNetwork, assets]); useEffect(() => { @@ -262,7 +278,7 @@ export function UnifiedBridgeProvider({ }, [events, request]); const supportsAsset = useCallback( - (lookupAddressOrSymbol: string, targetChainId: number) => { + (lookupAddressOrSymbol: string, targetChainId: string) => { if (!activeNetwork) { return false; } @@ -278,14 +294,14 @@ export function UnifiedBridgeProvider({ return false; } - return chainIdToCaip(targetChainId) in asset.destinations; + return targetChainId in asset.destinations; }, [assets, activeNetwork] ); const getAsset = useCallback( - (symbol: string, chainId: number) => { - const chainAssets = assets[chainIdToCaip(chainId)] ?? []; + (symbol: string, chainId: string) => { + const chainAssets = assets[chainId] ?? []; const asset = chainAssets.find( ({ symbol: assetSymbol }) => assetSymbol === symbol @@ -298,7 +314,7 @@ export function UnifiedBridgeProvider({ const buildParams = useCallback( ( - targetChainId: number + targetChainId: string ): { sourceChain: Chain; sourceChainId: number; @@ -313,7 +329,7 @@ export function UnifiedBridgeProvider({ UnifiedBridgeError.UnsupportedNetwork ); - const sourceChain = buildChain(activeNetwork.chainId); + const sourceChain = buildChain(activeNetwork.caipId); const targetChain = buildChain(targetChainId); const provider = getProviderForNetwork( @@ -337,12 +353,12 @@ export function UnifiedBridgeProvider({ async ( symbol: string, amount: bigint, - targetChainId: number + targetChainId: string ): Promise => { assert(core, CommonError.Unknown); assert(activeNetwork, CommonError.NoActiveNetwork); - const asset = getAsset(symbol, activeNetwork.chainId); + const asset = getAsset(symbol, activeNetwork.caipId); assert(asset, UnifiedBridgeError.UnknownAsset); const feeMap = lowerCaseKeys( @@ -350,7 +366,7 @@ export function UnifiedBridgeProvider({ asset, amount, targetChain: buildChain(targetChainId), - sourceChain: buildChain(activeNetwork.chainId), + sourceChain: buildChain(activeNetwork.caipId), }) ); @@ -366,12 +382,12 @@ export function UnifiedBridgeProvider({ async ( symbol: string, amount: bigint, - targetChainId: number + targetChainId: string ): Promise => { assert(core, CommonError.Unknown); assert(activeNetwork, CommonError.NoActiveNetwork); - const asset = getAsset(symbol, activeNetwork.chainId); + const asset = getAsset(symbol, activeNetwork.caipId); assert(asset, UnifiedBridgeError.UnknownAsset); @@ -392,7 +408,7 @@ export function UnifiedBridgeProvider({ ); const getAssetIdentifierOnTargetChain = useCallback( - (symbol?: string, targetChainId?: number) => { + (symbol?: string, targetChainId?: string) => { if (!symbol || !targetChainId) { return; } @@ -419,11 +435,11 @@ export function UnifiedBridgeProvider({ ); const transferAsset = useCallback( - async (symbol: string, amount: bigint, targetChainId: number) => { + async (symbol: string, amount: bigint, targetChainId: string) => { assert(core, CommonError.Unknown); assert(activeNetwork, CommonError.NoActiveNetwork); - const asset = getAsset(symbol, activeNetwork.chainId); + const asset = getAsset(symbol, activeNetwork.caipId); assert(asset, UnifiedBridgeError.UnknownAsset); @@ -529,8 +545,11 @@ export function UnifiedBridgeProvider({ return ( { const displayTokenList: DisplayToken[] = useMemo(() => { - const initialList = [ - ...(tokensList - ? tokensList - .filter((token) => - searchQuery.length - ? token.name - .toLowerCase() - .includes(searchQuery.toLowerCase()) || - token.symbol.toLowerCase().includes(searchQuery.toLowerCase()) - : true - ) - .map((token): DisplayToken => { - return { - name: token.name, - symbol: token.symbol, - displayValue: token.balanceDisplayValue ?? '', - token, - decimals: isNFT(token) ? 0 : token.decimals, - }; - }) - : []), - ...(bridgeTokensList - ? bridgeTokensList - .filter((token) => - searchQuery.length - ? token.symbol - .toLowerCase() - .includes(searchQuery.toLowerCase()) || - token.symbolOnNetwork - ?.toLowerCase() - .includes(searchQuery.toLocaleLowerCase()) - : true - ) - .map((token): DisplayToken => { - return { - name: token.symbolOnNetwork || token.symbol, - symbol: token.asset.symbol, - displayValue: formatBalance(token.balance), - token, - decimals: isUnifiedBridgeAsset(token.asset) - ? token.asset.decimals - : token.asset.denomination, - }; - }) - : []), - ]; + const initialList = (tokensList ?? []) + .filter((token) => + searchQuery.length + ? token.name.toLowerCase().includes(searchQuery.toLowerCase()) || + token.symbol.toLowerCase().includes(searchQuery.toLowerCase()) + : true + ) + .map((token): DisplayToken => { + return { + name: token.name, + symbol: token.symbol, + displayValue: token.balanceDisplayValue ?? '', + token, + decimals: isNFT(token) ? 0 : token.decimals, + }; + }); const [tokensWithBalance, tokensWithoutBalance]: DisplayToken[][] = partition(initialList, (token) => { @@ -107,6 +68,6 @@ export const useDisplaytokenlist = ({ return tokenOne.name.localeCompare(tokenTwo.name); }), ]; - }, [tokensList, bridgeTokensList, searchQuery]); + }, [tokensList, searchQuery]); return displayTokenList; }; diff --git a/src/pages/Bridge/Bridge.tsx b/src/pages/Bridge/Bridge.tsx index 6c4f017a..8035bad9 100644 --- a/src/pages/Bridge/Bridge.tsx +++ b/src/pages/Bridge/Bridge.tsx @@ -1,27 +1,5 @@ -import { - BIG_ZERO, - Blockchain, - useBridgeSDK, - isAddressBlocklisted, -} from '@avalabs/core-bridge-sdk'; -import { PageTitle } from '@src/components/common/PageTitle'; -import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { BridgeProviders, useBridge } from './hooks/useBridge'; -import { FunctionIsOffline } from '@src/components/common/FunctionIsOffline'; -import { usePageHistory } from '@src/hooks/usePageHistory'; -import { useSyncBridgeConfig } from './hooks/useSyncBridgeConfig'; -import Big from 'big.js'; -import { useSetBridgeChainFromNetwork } from './hooks/useSetBridgeChainFromNetwork'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { BridgeSanctions } from './components/BridgeSanctions'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { - blockchainToNetwork, - networkToBlockchain, -} from './utils/blockchainConversion'; -import { useAvailableBlockchains } from './hooks/useAvailableBlockchains'; import { useTranslation } from 'react-i18next'; import { Button, @@ -30,44 +8,53 @@ import { Typography, toast, } from '@avalabs/core-k2-components'; + +import { PageTitle } from '@src/components/common/PageTitle'; +import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; +import { FunctionIsOffline } from '@src/components/common/FunctionIsOffline'; +import { usePageHistory } from '@src/hooks/usePageHistory'; +import { useAccountsContext } from '@src/contexts/AccountsProvider'; +import { useNetworkContext } from '@src/contexts/NetworkProvider'; import { FunctionNames, useIsFunctionAvailable, } from '@src/hooks/useIsFunctionAvailable'; 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 { BridgeFormETH } from './components/BridgeFormETH'; -import { BridgeFormAVAX } from './components/BridgeFormAVAX'; -import { BridgeFormBTC } from './components/BridgeFormBTC'; -import { BridgeFormUnified } from './components/BridgeFormUnified'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; +import { useBridge } from './hooks/useBridge'; +import { BridgeForm } from './components/BridgeForm'; import { BridgeUnknownNetwork } from './components/BridgeUnknownNetwork'; -import { useLiveBalance } from '@src/hooks/useLiveBalance'; +import { useBridgeTxHandling } from './hooks/useBridgeTxHandling'; +import { BridgeFormSkeleton } from './components/BridgeFormSkeleton'; export function Bridge() { useLiveBalance(); // Make sure we always use the latest balances. - useSyncBridgeConfig(); // keep bridge config up-to-date - useSetBridgeChainFromNetwork(); - - const [currentAssetIdentifier, setCurrentAssetIdentifier] = - useState(); - const { amount, setAmount, bridgeFee, provider, minimum, targetChainId } = - useBridge(currentAssetIdentifier); const { - bridgeConfig, - currentAsset, - setCurrentAsset, - currentBlockchain, - setCurrentBlockchain, - targetBlockchain, - sourceAssets, - } = useBridgeSDK(); - const bridgeConfigError = bridgeConfig.error; + amount, + setAmount, + bridgableTokens, + availableChainIds, + bridgeFee, + estimateGas, + isReady, + minimum, + maximum, + receiveAmount, + setTargetChain, + possibleTargetChains, + asset, + setAsset, + targetChain, + transferableAssets, + sourceBalance, + transfer, + } = useBridge(); + const { t } = useTranslation(); - const availableBlockchains = useAvailableBlockchains(); - const { getAssetIdentifierOnTargetChain } = useUnifiedBridgeContext(); const { isFunctionAvailable } = useIsFunctionAvailable(FunctionNames.BRIDGE); @@ -81,7 +68,7 @@ export function Bridge() { const { accounts: { active: activeAccount }, } = useAccountsContext(); - const { network, setNetwork, networks } = useNetworkContext(); + const { network, setNetwork } = useNetworkContext(); const activeAddress = useMemo( () => @@ -93,98 +80,72 @@ export function Bridge() { [activeAccount?.addressBTC, activeAccount?.addressC, network] ); - const targetNetwork = useMemo(() => { - if (targetBlockchain) { - return blockchainToNetwork(targetBlockchain, networks, bridgeConfig); - } - }, [bridgeConfig, networks, targetBlockchain]); - const bridgePageHistoryData: { selectedToken?: string; - inputAmount?: Big; - selectedTokenAddress?: string; + inputAmount?: string; } = getPageHistoryData(); - // derive blockchain/network from network useEffect(() => { - const networkBlockchain = networkToBlockchain(network); - if (currentBlockchain !== networkBlockchain) { - setCurrentBlockchain(networkBlockchain); - } - }, [network, currentBlockchain, setCurrentBlockchain]); + if (!asset && bridgePageHistoryData.selectedToken) { + const matchingAsset = transferableAssets.find( + (a) => a.symbol === bridgePageHistoryData.selectedToken + ); - // Set source blockchain & amount from page storage - useEffect(() => { - if (!amount && bridgePageHistoryData.inputAmount) { - setAmount(new Big(bridgePageHistoryData.inputAmount)); + if (matchingAsset) { + setAsset(matchingAsset); + } + } + if (typeof amount !== 'bigint' && bridgePageHistoryData.inputAmount) { + setAmount(BigInt(bridgePageHistoryData.inputAmount)); } }, [ amount, + asset, + setAsset, + transferableAssets, bridgePageHistoryData.inputAmount, + bridgePageHistoryData.selectedToken, setAmount, - networks, - setNetwork, ]); - // Set token from page storage useEffect(() => { - const sourceSymbols = Object.keys(sourceAssets); - const symbol = bridgePageHistoryData.selectedToken; - - if ( - symbol && - !currentAsset && - sourceSymbols.length && - sourceSymbols.includes(symbol) // make sure we have the selected token available on the network to prevent an infinite loop - ) { - // Workaround for a race condition with useEffect in BridgeSDKProvider - // that also calls setCurrentAsset :( - const timer = setTimeout(() => { - setCurrentAsset(symbol); - setCurrentAssetIdentifier( - bridgePageHistoryData.selectedTokenAddress ?? '' - ); - }, 1); - - return () => { - clearTimeout(timer); - }; + const sourceSymbols = transferableAssets.map(({ symbol }) => symbol); + const prevSymbol = bridgePageHistoryData.selectedToken; + + if (prevSymbol && sourceSymbols.length) { + const prevAsset = transferableAssets.find( + ({ symbol }) => symbol === prevSymbol + ); + + if (prevAsset) { + setAsset(prevAsset); + } } - }, [ - bridgePageHistoryData.selectedToken, - bridgePageHistoryData.selectedTokenAddress, - currentAsset, - currentAssetIdentifier, - setCurrentAsset, - sourceAssets, - bridgeConfig, - networks, - targetBlockchain, - ]); + }, [bridgePageHistoryData.selectedToken, setAsset, transferableAssets]); const [isAmountTooLow, setIsAmountTooLow] = useState(false); const onInitiated = useCallback(() => { captureEncrypted('BridgeTransferStarted', { address: activeAddress, - sourceBlockchain: currentBlockchain, - targetBlockchain, + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, }); - }, [captureEncrypted, activeAddress, currentBlockchain, targetBlockchain]); + }, [captureEncrypted, activeAddress, network?.caipId, targetChain?.caipId]); const onRejected = useCallback(() => { captureEncrypted('BridgeTransferRequestUserRejectedError', { address: activeAddress, - sourceBlockchain: currentBlockchain, - targetBlockchain, - fee: bridgeFee?.toNumber(), + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, + fee: Number(bridgeFee ?? 0), }); }, [ activeAddress, - bridgeFee, captureEncrypted, - currentBlockchain, - targetBlockchain, + network?.caipId, + targetChain?.caipId, + bridgeFee, ]); const onFailure = useCallback( @@ -192,8 +153,8 @@ export function Bridge() { setBridgeError(t('There was a problem with the transfer')); captureEncrypted('BridgeTransferRequestError', { address: activeAddress, - sourceBlockchain: currentBlockchain, - targetBlockchain, + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, }); const { title, hint } = getTranslatedError(transferError); @@ -211,10 +172,10 @@ export function Bridge() { [ activeAddress, captureEncrypted, - currentBlockchain, - targetBlockchain, getTranslatedError, + network?.caipId, t, + targetChain?.caipId, ] ); @@ -223,72 +184,90 @@ export function Bridge() { captureEncrypted('BridgeTransferRequestSucceeded', { address: activeAddress, txHash: hash, - sourceBlockchain: currentBlockchain, - targetBlockchain, + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, }); const timestamp = Date.now(); // Navigate to transaction status page history.push( - `/bridge/transaction-status/${currentBlockchain}/${hash}/${timestamp}` + `/bridge/transaction-status/${network?.caipId}/${hash}/${timestamp}` ); }, [ activeAddress, captureEncrypted, - currentBlockchain, history, - targetBlockchain, + network?.caipId, + targetChain?.caipId, ] ); - const handleBlockchainChange = useCallback( - (blockchain: Blockchain) => { - const blockChainNetwork = blockchainToNetwork( - blockchain, - networks, - bridgeConfig - ); - - if (blockChainNetwork) { - setNetwork(blockChainNetwork); - const assetAddressOnOppositeChain = getAssetIdentifierOnTargetChain( - currentAsset, - blockChainNetwork.chainId - ); - - setCurrentAssetIdentifier(assetAddressOnOppositeChain); - setNavigationHistoryData({ - selectedTokenAddress: assetAddressOnOppositeChain, - selectedToken: currentAsset, - inputAmount: amount, - }); - } + const handleSourceChainChange = useCallback( + (chain: NetworkWithCaipId) => { + setNetwork(chain); + setNavigationHistoryData({ + selectedToken: asset ? asset.symbol : undefined, + inputAmount: amount, + }); // Reset because a denomination change will change its value - setAmount(BIG_ZERO); + setAmount(0n); setBridgeError(''); }, [ amount, - bridgeConfig, - getAssetIdentifierOnTargetChain, - currentAsset, - networks, + asset, setAmount, setNavigationHistoryData, setNetwork, setBridgeError, - setCurrentAssetIdentifier, ] ); - if ( - bridgeConfigError || - !isFunctionAvailable || - availableBlockchains.length < 2 // we need at least to blockchains to bridge between - ) { + const { onTransfer, isPending } = useBridgeTxHandling({ + transfer, + onInitiated, + onSuccess, + onFailure, + onRejected, + }); + + const formProps = { + onInitiated, + onSuccess, + onFailure, + onRejected, + handleSourceChainChange, + amount, + bridgeError, + isAmountTooLow, + isReady, + asset, + setAsset, + availableChainIds, + transferableAssets, + transfer, + onTransfer, + isPending, + setIsAmountTooLow, + setAmount, + setBridgeError, + setNavigationHistoryData, + targetChain, + estimateGas, + minimum, + maximum, + receiveAmount, + setTargetChain, + possibleTargetChains, + loading: false, // TODO: load balances + bridgableTokens, + sourceBalance, + }; + + if (!isFunctionAvailable) { return ( - - - - {/* To section */} - - - - - {t('To')} - - - {targetNetwork ? targetNetwork.chainName : ''} - - - } sx={{ rowGap: 2 }} /> - - 0} + unmountOnExit + mountOnEnter + > + + {t('Switch')}}> + + + + - {formattedReceiveAmount} - + 0} + mountOnEnter + unmountOnExit + > + + - {t('Estimated')} + {t('To')} - - + + } sx={{ rowGap: 2 }} /> + + - {formattedReceiveAmountCurrency} - + {t('Receive')} + + {formattedReceiveAmount} + + + + {t('Estimated')} + + + + {formattedReceiveAmountCurrency} + + - - + + @@ -658,54 +537,8 @@ export const BridgeForm = ({ gap: 2, }} > - {/* FIXME: Unified SDK can handle multiple bridges, but for now it's just the CCTP */} - {provider === BridgeProviders.Unified && ( - - {t('Powered by')} - - Circle - - ), - }} - /> - } - > - - - + {asset && targetChain && ( + )} + ); diff --git a/src/pages/Bridge/components/NetworkSelector.tsx b/src/pages/Bridge/components/NetworkSelector.tsx index 046a9440..d6a37812 100644 --- a/src/pages/Bridge/components/NetworkSelector.tsx +++ b/src/pages/Bridge/components/NetworkSelector.tsx @@ -1,119 +1,64 @@ -import { Blockchain } from '@avalabs/core-bridge-sdk'; -import { AvaxTokenIcon } from '@src/components/icons/AvaxTokenIcon'; -import { BitcoinLogo } from '@src/components/icons/BitcoinLogo'; import { useCallback, useRef, useState } from 'react'; -import { blockchainDisplayNameMap } from '../models'; import { Button, CheckIcon, ChevronDownIcon, ChevronUpIcon, - EthereumColorIcon, Menu, MenuItem, Stack, Typography, } from '@avalabs/core-k2-components'; +import { useTranslation } from 'react-i18next'; +import { useNetworkContext } from '@src/contexts/NetworkProvider'; +import { NetworkWithCaipId } from '@src/background/services/network/models'; interface NetworkSelectorProps { testId?: string; disabled?: boolean; - selected: Blockchain; - onSelect?: (blockchain: Blockchain) => void; - chains: Blockchain[]; + selected?: NetworkWithCaipId; + onSelect?: (blockchain: NetworkWithCaipId) => void; + chainIds: string[]; } -const getBlockChainLogo = (blockchain: Blockchain) => { - switch (blockchain) { - case Blockchain.AVALANCHE: - return ; - case Blockchain.ETHEREUM: - return ; - case Blockchain.BITCOIN: - return ; - default: - return <>; - } -}; - export function NetworkSelector({ testId, disabled, selected, onSelect, - chains, + chainIds, }: NetworkSelectorProps) { + const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const selectButtonRef = useRef(null); - - const selectedDisplayValue = blockchainDisplayNameMap.get(selected); + const { getNetwork } = useNetworkContext(); const handleClose = useCallback( - (blockchain: Blockchain) => { + (network: NetworkWithCaipId) => { setIsOpen(false); - onSelect?.(blockchain); + onSelect?.(network); }, [onSelect] ); - const getMenuItem = useCallback( - (dataId: string, blockchain: Blockchain) => { - if (!chains.includes(blockchain)) { - return null; - } - - return ( - { - handleClose(blockchain); - }} - disableRipple - sx={{ minHeight: 'auto', py: 1 }} - > - - - {getBlockChainLogo(blockchain)} - - {blockchainDisplayNameMap.get(blockchain)} - - - - {selected === blockchain && } - - - ); - }, - [chains, handleClose, selected] - ); - return ( - {getMenuItem('bridge-avax-chain-option', Blockchain.AVALANCHE)} - {getMenuItem('bridge-eth-chain-option', Blockchain.ETHEREUM)} - {getMenuItem('bridge-btc-chain-option', Blockchain.BITCOIN)} + {chainIds + .map((chainId) => getNetwork(chainId)) + .filter((n): n is NetworkWithCaipId => typeof n !== 'undefined') + .map((network) => { + return ( + { + handleClose(network); + }} + disableRipple + sx={{ minHeight: 'auto', py: 1 }} + > + + + + {network.chainName} + + + {selected === network && } + + + ); + })} ); diff --git a/src/pages/Bridge/hooks/useAssetBalancesEVM.ts b/src/pages/Bridge/hooks/useAssetBalancesEVM.ts deleted file mode 100644 index 1ba0bcd4..00000000 --- a/src/pages/Bridge/hooks/useAssetBalancesEVM.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { - Asset, - Blockchain, - EthereumAssets, - useBridgeSDK, - useGetTokenSymbolOnNetwork, -} from '@avalabs/core-bridge-sdk'; -import { uniqBy } from 'lodash'; - -import { getBalances } from '../utils/getBalances'; -import { AssetBalance } from '../models'; -import { useMemo } from 'react'; -import { useFeatureFlagContext } from '@src/contexts/FeatureFlagsProvider'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -import { FeatureGates } from '@src/background/services/featureFlags/models'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { AccountType } from '@src/background/services/accounts/models'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; - -import { isUnifiedBridgeAsset } from '../utils/isUnifiedBridgeAsset'; - -/** - * Get for the current chain. - * Get a list of bridge supported assets with the balances of the current blockchain. - * The list is sorted by balance. - */ -export function useAssetBalancesEVM( - chain: Blockchain.AVALANCHE | Blockchain.ETHEREUM, - selectedAsset?: Asset -): { - assetsWithBalances: AssetBalance[]; -} { - const { featureFlags } = useFeatureFlagContext(); - const { - accounts: { active: activeAccount }, - } = useAccountsContext(); - - const { avalancheAssets, ethereumAssets, currentBlockchain } = useBridgeSDK(); - const { transferableAssets: unifiedBridgeAssets } = useUnifiedBridgeContext(); - - const { getTokenSymbolOnNetwork } = useGetTokenSymbolOnNetwork(); - - const tokens = useTokensWithBalances({ - forceShowTokensWithoutBalances: true, - }); - - // For balances on the Avalanche side, for all bridge assets on avalanche - const balances = useMemo(() => { - const isAvalanche = - chain === Blockchain.AVALANCHE || - currentBlockchain === Blockchain.AVALANCHE; - const isEthereum = - chain === Blockchain.ETHEREUM || - currentBlockchain === Blockchain.ETHEREUM; - if (!isAvalanche && !isEthereum) { - return []; - } - - const filteredEthereumAssets: EthereumAssets = Object.keys(ethereumAssets) - .filter((key) => ethereumAssets[key]?.symbol !== 'BUSD') // do not allow BUSD.e onboardings - .filter((key) => ethereumAssets[key]?.symbol !== 'USDC') // do not use Legacy Bridge for USDC onboardings - .reduce((obj, key) => { - obj[key] = ethereumAssets[key]; - return obj; - }, {}); - - const abAssets = Object.values( - isAvalanche ? avalancheAssets : filteredEthereumAssets - ); - - if (!abAssets.length) { - return []; - } - - const allAssets = selectedAsset - ? [selectedAsset] - : // Deduplicate the assets since both Unified & legacy SDKs could allow bridging the same assets. - // unifiedBridgeAssets go first so that they're not the ones removed (we prefer Unified bridge over legacy) - uniqBy([...unifiedBridgeAssets, ...abAssets], (asset) => - isUnifiedBridgeAsset(asset) - ? asset.symbol - : getTokenSymbolOnNetwork(asset.symbol, chain) - ); - - const availableAssets = allAssets.filter((asset) => { - if (chain === Blockchain.AVALANCHE) { - if (isUnifiedBridgeAsset(asset)) { - return featureFlags[FeatureGates.BRIDGE_ETH]; - } - - const { nativeNetwork } = asset; - - if ( - nativeNetwork === Blockchain.ETHEREUM && - !featureFlags[FeatureGates.BRIDGE_ETH] - ) { - // ETH is not available filter ETH tokens out - return false; - } - if (nativeNetwork === Blockchain.BITCOIN) { - // Filter out BTC tokens if BTC bridge is not available, or - // the active account was imported via WalletConnect (the BTC address is unknown). - - const isBtcSupportedByActiveAccount = - activeAccount?.addressBTC && - activeAccount?.type !== AccountType.WALLET_CONNECT; - - return ( - featureFlags[FeatureGates.BRIDGE_BTC] && - isBtcSupportedByActiveAccount - ); - } - } - - // no further filtering is needed since it's not possible to bridge between eth and btc - return true; - }); - - return getBalances(availableAssets, tokens).map((token) => { - return { - ...token, - symbolOnNetwork: isUnifiedBridgeAsset(token.asset) - ? token.asset.symbol - : getTokenSymbolOnNetwork(token.symbol, chain), - }; - }); - }, [ - chain, - currentBlockchain, - selectedAsset, - avalancheAssets, - ethereumAssets, - tokens, - featureFlags, - getTokenSymbolOnNetwork, - activeAccount?.type, - activeAccount?.addressBTC, - unifiedBridgeAssets, - ]); - - const assetsWithBalances = balances.sort( - (asset1, asset2) => asset2.balance?.cmp(asset1.balance || 0) || 0 - ); - - return { assetsWithBalances }; -} diff --git a/src/pages/Bridge/hooks/useAvailableBlockchains.ts b/src/pages/Bridge/hooks/useAvailableBlockchains.ts deleted file mode 100644 index f6ffa10d..00000000 --- a/src/pages/Bridge/hooks/useAvailableBlockchains.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Blockchain } from '@avalabs/core-bridge-sdk'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useFeatureFlagContext } from '@src/contexts/FeatureFlagsProvider'; -import { useEffect, useState } from 'react'; -import { SUPPORTED_CHAINS } from '../models'; -import { FeatureGates } from '@src/background/services/featureFlags/models'; -import { - isFireblocksAccount, - isWalletConnectAccount, -} from '@src/background/services/accounts/utils/typeGuards'; -import isFireblocksApiSupported from '@src/background/services/fireblocks/utils/isFireblocksApiSupported'; - -export function useAvailableBlockchains() { - const { featureFlags } = useFeatureFlagContext(); - const { - accounts: { active: activeAccount }, - } = useAccountsContext(); - const [availableBlockchains, setAvailableBlockchains] = - useState(SUPPORTED_CHAINS); - - // Remove chains turned off by the feature flags and - // switch chain in case the selected one is not available - useEffect(() => { - const availableChains = SUPPORTED_CHAINS.filter((chain) => { - switch (chain) { - case Blockchain.BITCOIN: - if (!featureFlags[FeatureGates.BRIDGE_BTC]) { - return false; - } - - if (isWalletConnectAccount(activeAccount)) { - return false; - } - - if (isFireblocksAccount(activeAccount)) { - return isFireblocksApiSupported(activeAccount); - } - - return true; - case Blockchain.ETHEREUM: - return featureFlags[FeatureGates.BRIDGE_ETH]; - default: - return true; - } - }); - - setAvailableBlockchains(availableChains); - }, [featureFlags, activeAccount]); - - return availableBlockchains; -} diff --git a/src/pages/Bridge/hooks/useAvalancheBridge.test.ts b/src/pages/Bridge/hooks/useAvalancheBridge.test.ts deleted file mode 100644 index 07aebaf9..00000000 --- a/src/pages/Bridge/hooks/useAvalancheBridge.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import Big from 'big.js'; -import { - useBridgeSDK, - Blockchain, - AssetType, - isNativeAsset, -} from '@avalabs/core-bridge-sdk'; -import { act, renderHook } from '@testing-library/react-hooks'; - -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; - -import { useAvalancheBridge } from './useAvalancheBridge'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (k) => k, - }), -})); -jest.mock('@avalabs/core-bridge-sdk'); -jest.mock('./useAssetBalancesEVM'); -jest.mock('@src/contexts/BridgeProvider'); -jest.mock('@src/contexts/AccountsProvider'); -jest.mock('@src/contexts/NetworkFeeProvider'); -jest.mock('@src/contexts/ConnectionProvider'); - -describe('src/pages/Bridge/hooks/useAvalancheBridge', () => { - let requestFn = jest.fn(); - let createBridgeTransactionFn = jest.fn(); - let transferEVMAssetFn = jest.fn(); - - const highFee = 20n; - const lowFee = 8n; - - const currentAssetData = { - assetType: AssetType.BTC, - denomination: 8, - wrappedAssetSymbol: 'BTC.b', - tokenName: 'Bitcoin', - symbol: 'BTC', - nativeNetwork: Blockchain.BITCOIN, - }; - - const btcWithBalance = { - symbol: 'BTC', - asset: currentAssetData, - balance: new Big('0.1'), - price: 60_000, - }; - - beforeEach(() => { - jest.resetAllMocks(); - - requestFn = jest.fn(); - transferEVMAssetFn = jest.fn(); - createBridgeTransactionFn = jest.fn(); - - jest.mocked(useConnectionContext).mockReturnValue({ - request: requestFn, - } as any); - - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressC: 'user-c-address', - addressBTC: 'user-btc-address', - }, - }, - } as any); - - jest.mocked(useAssetBalancesEVM).mockReturnValue({ - assetsWithBalances: [btcWithBalance], - } as any); - - jest.mocked(useBridgeContext).mockReturnValue({ - createBridgeTransaction: createBridgeTransactionFn, - transferEVMAsset: transferEVMAssetFn, - } as any); - - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: 'BTC', - currentAssetData, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.AVALANCHE, - targetBlockchain: Blockchain.BITCOIN, - } as any); - - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: { - high: { - maxFee: highFee, - }, - low: { - maxFee: lowFee, - }, - displayDecimals: 8, - }, - } as any); - }); - - it('provides maximum transfer amount', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - // Wait for the state to be set - await new Promise(process.nextTick); - - expect(hook.current.maximum).toEqual(btcWithBalance.balance); - }); - - describe('transfer()', () => { - beforeEach(() => { - jest.mocked(isNativeAsset).mockReturnValue(true); - }); - - describe('when no asset is selected', () => { - beforeEach(() => { - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: '', - currentAssetData: undefined, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.ETHEREUM, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'No asset selected' - ); - }); - }); - }); - - it("calls the provider's transfer function", async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - const hash = await hook.current.transfer(); - - expect(transferEVMAssetFn).toHaveBeenCalledWith( - amount, - currentAssetData - ); - - expect(hash).toEqual(fakeHash); - }); - }); - - it('tracks the bridge transaction', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useAvalancheBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - await hook.current.transfer(); - - expect(createBridgeTransactionFn).toHaveBeenCalledWith({ - sourceChain: Blockchain.AVALANCHE, - sourceTxHash: fakeHash, - sourceStartedAt: expect.any(Number), - targetChain: Blockchain.BITCOIN, - amount, - symbol: 'BTC', - }); - }); - }); - }); -}); diff --git a/src/pages/Bridge/hooks/useAvalancheBridge.ts b/src/pages/Bridge/hooks/useAvalancheBridge.ts deleted file mode 100644 index 01cf8c36..00000000 --- a/src/pages/Bridge/hooks/useAvalancheBridge.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { BIG_ZERO, Blockchain, useBridgeSDK } from '@avalabs/core-bridge-sdk'; -import { BridgeAdapter } from './useBridge'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { useCallback } from 'react'; -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import Big from 'big.js'; - -/** - * Hook for when the source is Avalanche - */ -export function useAvalancheBridge( - amount: Big, - bridgeFee: Big, - minimum: Big -): BridgeAdapter { - const { - targetBlockchain, - currentBlockchain, - setTransactionDetails, - currentAssetData, - } = useBridgeSDK(); - - const { createBridgeTransaction, transferEVMAsset, estimateGas } = - useBridgeContext(); - - const isAvalancheBridge = currentBlockchain === Blockchain.AVALANCHE; - - const { assetsWithBalances: selectedAssetWithBalances } = useAssetBalancesEVM( - Blockchain.AVALANCHE, - isAvalancheBridge ? currentAssetData : undefined - ); - const sourceBalance = selectedAssetWithBalances[0]; - - const { assetsWithBalances } = useAssetBalancesEVM(Blockchain.AVALANCHE); - - const maximum = sourceBalance?.balance || BIG_ZERO; - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO; - - const transfer = useCallback(async () => { - if (!currentAssetData) { - throw new Error('No asset selected'); - } - - const timestamp = Date.now(); - const result = await transferEVMAsset(amount, currentAssetData); - - setTransactionDetails({ - tokenSymbol: currentAssetData.symbol, - amount, - }); - - createBridgeTransaction({ - sourceChain: Blockchain.AVALANCHE, - sourceTxHash: result.hash, - sourceStartedAt: timestamp, - targetChain: targetBlockchain, - amount, - symbol: currentAssetData.symbol, - }); - - return result.hash; - }, [ - amount, - createBridgeTransaction, - currentAssetData, - setTransactionDetails, - targetBlockchain, - transferEVMAsset, - ]); - - return { - sourceBalance, - assetsWithBalances, - receiveAmount, - maximum, - price: sourceBalance?.price, - estimateGas, - transfer, - }; -} diff --git a/src/pages/Bridge/hooks/useBridge.ts b/src/pages/Bridge/hooks/useBridge.ts index 4f87d9e7..82482df2 100644 --- a/src/pages/Bridge/hooks/useBridge.ts +++ b/src/pages/Bridge/hooks/useBridge.ts @@ -1,107 +1,206 @@ -import Big from 'big.js'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { - BIG_ZERO, - Blockchain, - useBridgeSDK, - useBridgeFeeEstimate, - WrapStatus, - useMinimumTransferAmount, - Asset, -} from '@avalabs/core-bridge-sdk'; -import { useMemo, useState } from 'react'; - -import { AssetBalance } from '../models'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; + BridgeAsset, + TokenType as BridgeTokenType, +} from '@avalabs/bridge-unified'; +import { + NftTokenWithBalance, + TokenType, + TokenWithBalance, +} from '@avalabs/vm-module-types'; + +import { NetworkWithCaipId } from '@src/background/services/network/models'; import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { BridgeStepDetails } from '@avalabs/bridge-unified'; - -export interface BridgeAdapter { - address?: string; - sourceBalance?: AssetBalance; - targetBalance?: AssetBalance; - assetsWithBalances?: AssetBalance[]; - loading?: boolean; - networkFee?: Big; - bridgeFee?: Big; - /** Amount minus network and bridge fees */ - receiveAmount?: Big; - /** Maximum transfer amount */ - maximum?: Big; - /** Minimum transfer amount */ - minimum?: Big; - /** Price for the current asset & currency code */ - price?: number; - wrapStatus?: WrapStatus; - txHash?: string; - /** - * Transfer funds to the target blockchain - * @returns the transaction hash - */ - transfer: () => Promise; - estimateGas(amount: Big, asset?: Asset): Promise; - bridgeStep?: BridgeStepDetails; -} +import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; +import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; +import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; +import { isNFT } from '@src/background/services/balances/nft/utils/isNFT'; + +import { findMatchingBridgeAsset } from '../utils/findMatchingBridgeAsset'; interface Bridge { - amount: Big; - setAmount: (amount: Big) => void; - bridgeFee: Big; - provider: BridgeProviders; - minimum: Big; - targetChainId: number; + amount?: bigint; + setAmount: (amount: bigint) => void; + availableChainIds: string[]; + estimateGas: () => Promise; + isReady: boolean; + targetChain?: NetworkWithCaipId; + setTargetChain: (targetChain: NetworkWithCaipId) => void; + asset?: BridgeAsset; + setAsset: (asset: BridgeAsset) => void; + sourceBalance?: Exclude; + possibleTargetChains: string[]; + minimum?: bigint; + maximum?: bigint; + receiveAmount?: bigint; + bridgeFee?: bigint; + bridgableTokens: Exclude[]; + transferableAssets: BridgeAsset[]; + transfer: () => Promise; } -export enum BridgeProviders { - Avalanche, - Unified, -} +export function useBridge(): Bridge { + const { network, getNetwork } = useNetworkContext(); + const { capture } = useAnalyticsContext(); + const { + availableChainIds, + estimateTransferGas, + getFee, + isReady, + transferableAssets, + transferAsset, + } = useUnifiedBridgeContext(); + const [amount, setAmount] = useState(); + const [asset, setAsset] = useState(); + const firstTargetChainId = Object.keys(asset?.destinations ?? {})[0] ?? ''; + const [targetChain, setTargetChain] = useState( + firstTargetChainId ? getNetwork(firstTargetChainId) : undefined + ); + + const [receiveAmount, setReceiveAmount] = useState(); + const [maximum, setMaximum] = useState(); + const [minimum, setMinimum] = useState(); + const [bridgeFee, setBridgeFee] = useState(); + const balances = useTokensWithBalances({ + chainId: network?.chainId, + forceShowTokensWithoutBalances: true, + }); + + const bridgableTokens = useMemo(() => { + const nonNFTs = balances.filter( + (t): t is Exclude => !isNFT(t) + ); + + return nonNFTs.filter((t) => + findMatchingBridgeAsset(transferableAssets, t) + ); + }, [balances, transferableAssets]); + + const sourceBalance = useMemo(() => { + if (!asset) { + return; + } + + return bridgableTokens.find((token) => { + if ( + asset.type === BridgeTokenType.NATIVE && + token.type === TokenType.NATIVE + ) { + return asset.symbol.toLowerCase() === token.symbol.toLowerCase(); + } + + if ( + asset.type === BridgeTokenType.ERC20 && + token.type === TokenType.ERC20 + ) { + return asset.address.toLowerCase() === token.address.toLowerCase(); + } + + return false; + }); + }, [asset, bridgableTokens]); + + const possibleTargetChains = useMemo(() => { + return Object.keys(asset?.destinations ?? {}); + }, [asset?.destinations]); + + useEffect(() => { + let isMounted = true; + + if (asset && amount && targetChain) { + getFee(asset.symbol, amount, targetChain.caipId).then((fee) => { + if (!isMounted) { + return; + } + + setBridgeFee(fee); + setMinimum(fee); + setReceiveAmount(amount - fee); + }); + + setMaximum(sourceBalance?.balance); + } + + return () => { + isMounted = false; + }; + }, [amount, asset, getFee, targetChain, sourceBalance?.balance]); + + const estimateGas = useCallback(async () => { + if (!asset?.symbol || !amount || !targetChain?.caipId) { + return 0n; + } + + return estimateTransferGas(asset.symbol, amount, targetChain?.caipId); + }, [estimateTransferGas, targetChain?.caipId, asset?.symbol, amount]); + + const transfer = useCallback(async () => { + capture('unifedBridgeTransferStarted', { + bridgeType: 'CCTP', // TODO: no longer CCTP only + sourceBlockchain: network?.caipId, + targetBlockchain: targetChain?.caipId, + }); + + if (!amount) { + throw new Error('No amount chosen'); + } + + if (!asset) { + throw new Error('No asset chosen'); + } + + if (!network?.caipId) { + throw new Error('No source chain chosen'); + } + + if (!targetChain?.caipId) { + throw new Error('No target chain chosen'); + } + + const hash = await transferAsset(asset.symbol, amount, targetChain?.caipId); + + return hash; + }, [ + amount, + asset, + targetChain?.caipId, + transferAsset, + capture, + network?.caipId, + ]); -export function useBridge(currentAssetIdentifier?: string): Bridge { - const { targetBlockchain } = useBridgeSDK(); - const { supportsAsset } = useUnifiedBridgeContext(); - - const [amount, setAmount] = useState(BIG_ZERO); - - const bridgeFee = useBridgeFeeEstimate(amount) || BIG_ZERO; - const minimum = useMinimumTransferAmount(amount); - const { isDeveloperMode } = useNetworkContext(); - - const targetChainId = useMemo(() => { - switch (targetBlockchain) { - case Blockchain.ETHEREUM: - return isDeveloperMode - ? ChainId.ETHEREUM_TEST_SEPOLIA - : ChainId.ETHEREUM_HOMESTEAD; - - case Blockchain.AVALANCHE: - return isDeveloperMode - ? ChainId.AVALANCHE_TESTNET_ID - : ChainId.AVALANCHE_MAINNET_ID; - - case Blockchain.BITCOIN: - return isDeveloperMode ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN; - - default: - // NOTE: this will only happen for Ethereum and is safe for now, - // since we're only using this piece of code for Unified Bridge (CCTP). - // Needs revisiting when we migrate Avalanche Bridge to @avalabs/bridge-unified package. - return isDeveloperMode - ? ChainId.ETHEREUM_TEST_SEPOLIA - : ChainId.ETHEREUM_HOMESTEAD; + useEffect(() => { + if (targetChain && possibleTargetChains.includes(targetChain.caipId)) { + return; } - }, [isDeveloperMode, targetBlockchain]); + + if (possibleTargetChains[0]) { + const foundChain = getNetwork(possibleTargetChains[0]); + + if (foundChain) { + setTargetChain(foundChain); + } + } + }, [getNetwork, targetChain, possibleTargetChains]); return { amount, setAmount, - minimum, + bridgableTokens: bridgableTokens, + availableChainIds, bridgeFee, - targetChainId, - provider: - currentAssetIdentifier && - supportsAsset(currentAssetIdentifier, targetChainId) - ? BridgeProviders.Unified - : BridgeProviders.Avalanche, + estimateGas, + isReady, + minimum, + maximum, + receiveAmount, + asset, + setAsset, + sourceBalance, + targetChain, + setTargetChain, + possibleTargetChains, + transferableAssets, + transfer, }; } diff --git a/src/pages/Bridge/hooks/useBtcBridge.test.ts b/src/pages/Bridge/hooks/useBtcBridge.test.ts deleted file mode 100644 index 7d4f5a64..00000000 --- a/src/pages/Bridge/hooks/useBtcBridge.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -import Big from 'big.js'; -import { BN } from 'bn.js'; -import { act, renderHook } from '@testing-library/react-hooks'; -import { - useBridgeConfig, - useBridgeSDK, - getBtcAsset, - Blockchain, - btcToSatoshi, -} from '@avalabs/core-bridge-sdk'; - -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; -import { DAppProviderRequest } from '@src/background/connections/dAppConnection/models'; - -import { useBtcBridge } from './useBtcBridge'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (k) => k, - }), -})); -jest.mock('@avalabs/core-bridge-sdk'); -jest.mock('@src/contexts/BridgeProvider'); -jest.mock('@src/contexts/AccountsProvider'); -jest.mock('@src/contexts/NetworkFeeProvider'); -jest.mock('@src/contexts/ConnectionProvider'); -jest.mock('@src/hooks/useTokensWithBalances'); - -describe('src/pages/Bridge/hooks/useBtcBridge', () => { - let requestFn = jest.fn(); - let createBridgeTransactionFn = jest.fn(); - - const highFee = 20n; - const lowFee = 8n; - - beforeEach(() => { - jest.resetAllMocks(); - - requestFn = jest.fn(); - createBridgeTransactionFn = jest.fn(); - - jest.mocked(useConnectionContext).mockReturnValue({ - request: requestFn, - } as any); - - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressBTC: 'user-btc-address', - }, - }, - } as any); - - jest.mocked(useTokensWithBalances).mockReturnValue([ - { - symbol: 'BTC', - decimals: 8, - balance: new BN('10000000', 8), - } as any, - ]); - - jest.mocked(useBridgeContext).mockReturnValue({ - createBridgeTransaction: createBridgeTransactionFn, - } as any); - - jest.mocked(getBtcAsset).mockReturnValue({ - symbol: 'BTC', - denomination: 8, - tokenName: 'Bitcoin', - } as any); - - jest.mocked(useBridgeSDK).mockReturnValue({ - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.BITCOIN, - } as any); - - jest.mocked(useBridgeConfig).mockReturnValue({ - config: { - criticalBitcoin: { - walletAddresses: { - btc: 'bridge-btc-address', - avalanche: 'bridge-avax-address', - }, - }, - }, - } as any); - - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: { - high: { - maxFee: highFee, - }, - low: { - maxFee: lowFee, - }, - displayDecimals: 8, - }, - } as any); - }); - - describe('transfer()', () => { - describe('when active account has no BTC address', () => { - beforeEach(() => { - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressC: 'user-c-address', - addressBTC: '', - }, - }, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Unsupported account' - ); - }); - }); - }); - - describe('when bridge config is not loaded yet', () => { - beforeEach(() => { - jest.mocked(useBridgeConfig).mockReturnValue({ config: undefined }); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Bridge not ready' - ); - }); - }); - }); - - describe('when fee rate is not loaded yet', () => { - beforeEach(() => { - jest.mocked(useNetworkFeeContext).mockReturnValue({ - currentFeeInfo: null, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Bridge not ready' - ); - }); - }); - }); - - describe('when fee rate is not loaded yet', () => { - beforeEach(() => { - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: null, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Bridge not ready' - ); - }); - }); - }); - - describe('when amount is not provided', () => { - it('throws error', async () => { - const amount = new Big('0'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'Amount not provided' - ); - }); - }); - }); - - it('sends a bitcoin_sendTransaction request with proper parameters', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - const fakeHash = '0xTxHash'; - - requestFn.mockResolvedValue(fakeHash); - - await act(async () => { - const hash = await hook.current.transfer(); - - expect(requestFn).toHaveBeenCalledWith( - { - method: DAppProviderRequest.BITCOIN_SEND_TRANSACTION, - params: { - from: 'user-btc-address', - to: 'bridge-btc-address', - amount: btcToSatoshi(amount), - feeRate: Number(highFee), - }, - }, - { - customApprovalScreenTitle: 'Confirm Bridge', - } - ); - - expect(hash).toEqual(fakeHash); - }); - }); - - it('tracks the bridge transaction', async () => { - const amount = new Big('0.1'); - const { result: hook } = renderHook(() => useBtcBridge(amount)); - - const fakeHash = '0xTxHash'; - - requestFn.mockResolvedValue(fakeHash); - - await act(async () => { - await hook.current.transfer(); - - expect(createBridgeTransactionFn).toHaveBeenCalledWith({ - sourceChain: Blockchain.BITCOIN, - sourceTxHash: fakeHash, - sourceStartedAt: expect.any(Number), - targetChain: Blockchain.AVALANCHE, - amount, - symbol: 'BTC', - }); - }); - }); - }); -}); diff --git a/src/pages/Bridge/hooks/useBtcBridge.ts b/src/pages/Bridge/hooks/useBtcBridge.ts deleted file mode 100644 index c95d42c8..00000000 --- a/src/pages/Bridge/hooks/useBtcBridge.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { - BIG_ZERO, - Blockchain, - btcToSatoshi, - getBtcAsset, - getBtcTransactionDetails, - satoshiToBtc, - useBridgeConfig, - useBridgeSDK, -} from '@avalabs/core-bridge-sdk'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { BitcoinSendTransactionParams } from '@avalabs/bitcoin-module'; -import { RpcMethod, TokenWithBalanceBTC } from '@avalabs/vm-module-types'; -import { - BitcoinInputUTXOWithOptionalScript, - getMaxTransferAmount, -} from '@avalabs/core-wallets-sdk'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import Big from 'big.js'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { AssetBalance } from '../models'; -import { BridgeAdapter } from './useBridge'; -import { TransactionPriority } from '@src/background/services/networkFee/models'; -import { useTokensWithBalances } from '@src/hooks/useTokensWithBalances'; - -import { getBtcInputUtxos } from '@src/utils/send/btcSendUtils'; -import { useTranslation } from 'react-i18next'; -import { normalizeBalance } from '@src/utils/normalizeBalance'; - -/** - * Hook for Bitcoin to Avalanche transactions - */ -export function useBtcBridge(amountInBtc: Big): BridgeAdapter { - const { setTransactionDetails, currentBlockchain } = useBridgeSDK(); - const isBitcoinBridge = currentBlockchain === Blockchain.BITCOIN; - const { t } = useTranslation(); - const { request } = useConnectionContext(); - const { bitcoinProvider, isDeveloperMode } = useNetworkContext(); - const { networkFee: currentFeeInfo } = useNetworkFeeContext(); - const { config } = useBridgeConfig(); - const { createBridgeTransaction } = useBridgeContext(); - const btcTokens = useTokensWithBalances({ - forceShowTokensWithoutBalances: true, - chainId: isDeveloperMode ? ChainId.BITCOIN_TESTNET : ChainId.BITCOIN, - }); - const { - accounts: { active: activeAccount }, - } = useAccountsContext(); - - const [btcBalance, setBtcBalance] = useState(); - const [utxos, setUtxos] = useState([]); - - const btcToken = useMemo( - () => - btcTokens.find((tok): tok is TokenWithBalanceBTC => tok.symbol === 'BTC'), - [btcTokens] - ); - - // Update the fee rate so we're able to calculate the - // max. bridgable amount for the main bridge screen - const feeRate: number = useMemo(() => { - if (!currentFeeInfo) { - return 0; - } - - // Because BTC testnet fees are super high recently, - // defaulting to cheaper preset makes testing easier. - const preset: TransactionPriority = isDeveloperMode ? 'low' : 'high'; - - return Number(currentFeeInfo[preset].maxFee); - }, [currentFeeInfo, isDeveloperMode]); - - // Calculate the maximum bridgable BTC amount whwnever - const maximum = useMemo(() => { - if (!feeRate || !config || !activeAccount?.addressBTC) { - return Big(0); - } - - const maxAmt = getMaxTransferAmount( - utxos, - // As long as the address type is the same (P2WPKH) it should not matter. - config.criticalBitcoin.walletAddresses.btc, - activeAccount.addressBTC, - feeRate - ); - - return satoshiToBtc(maxAmt); - }, [utxos, config, feeRate, activeAccount?.addressBTC]); - - /** Amount minus network and bridge fees (in BTC) */ - const [receiveAmount, setReceiveAmount] = useState(); - const loading = !btcBalance || !currentFeeInfo || !feeRate; - const amountInSatoshis = btcToSatoshi(amountInBtc); - - const btcAsset = config && getBtcAsset(config); - const assetsWithBalances = btcBalance ? [btcBalance] : []; - - // Update balances for the UI - useEffect(() => { - if (!isBitcoinBridge || !btcAsset || !btcToken) { - return; - } - - setBtcBalance({ - symbol: btcAsset.symbol, - asset: btcAsset, - balance: normalizeBalance(btcToken.balance, btcToken.decimals), - logoUri: btcToken.logoUri, - price: btcToken.priceInCurrency, - unconfirmedBalance: btcToken.unconfirmedBalance - ? normalizeBalance(btcToken.unconfirmedBalance, btcToken.decimals) - : new Big(0), - }); - }, [btcToken, btcAsset, isBitcoinBridge]); - - // Filter UTXOs whenever balance or fee rate is updated - // so we can calculate the max. bridgable amount. - useEffect(() => { - let isMounted = true; - - if (!bitcoinProvider || !feeRate || !btcToken) { - return; - } - - getBtcInputUtxos(bitcoinProvider, btcToken, feeRate) - .then((_utxos) => { - if (isMounted) { - setUtxos(_utxos); - } - }) - .catch((err) => { - console.error(err); - if (isMounted) { - setUtxos([]); - } - }); - - return () => { - isMounted = false; - }; - }, [bitcoinProvider, btcToken, feeRate]); - - useEffect(() => { - if (!isBitcoinBridge || !config || !activeAccount?.addressBTC || !utxos) { - return; - } - - try { - const btcTx = getBtcTransactionDetails( - config, - activeAccount.addressBTC, - utxos, - amountInSatoshis, - feeRate - ); - - setReceiveAmount(satoshiToBtc(btcTx.receiveAmount)); - } catch (error) { - // getBtcTransaction throws an error when the amount is too low or too high - // so set these to 0 - setReceiveAmount(BIG_ZERO); - } - }, [ - amountInSatoshis, - activeAccount?.addressBTC, - config, - isBitcoinBridge, - utxos, - feeRate, - ]); - - const transferBTC = useCallback(async () => { - if (!config || !feeRate) { - throw new Error('Bridge not ready'); - } - - if (amountInBtc.lte(0)) { - throw new Error('Amount not provided'); - } - - if (!activeAccount?.addressBTC) { - throw new Error('Unsupported account'); - } - - const symbol = 'BTC'; - const hash = await request( - { - method: RpcMethod.BITCOIN_SEND_TRANSACTION, - params: { - from: activeAccount.addressBTC, - to: config.criticalBitcoin.walletAddresses.btc, - feeRate, - amount: btcToSatoshi(amountInBtc), - }, - }, - { customApprovalScreenTitle: t('Confirm Bridge') } - ); - - setTransactionDetails({ - tokenSymbol: symbol, - amount: amountInBtc, - }); - createBridgeTransaction({ - sourceChain: Blockchain.BITCOIN, - sourceTxHash: hash, - sourceStartedAt: Date.now(), - targetChain: Blockchain.AVALANCHE, - amount: amountInBtc, - symbol, - }); - - return hash; - }, [ - request, - activeAccount?.addressBTC, - t, - amountInBtc, - config, - createBridgeTransaction, - feeRate, - setTransactionDetails, - ]); - - const estimateGas = useCallback( - async (amount: Big) => { - if (!config || !activeAccount?.addressBTC) { - return; - } - - // Bitcoin's formula for fee is `transactionByteLength * feeRate`. - // By setting the feeRate here to 1, we'll receive the transaction's byte length, - // which is what we need to have the dynamic fee calculations in the UI. - // Think of the byteLength as gasLimit for EVM transactions. - const fakeFeeRate = 1; - const { fee: byteLength } = getBtcTransactionDetails( - config, - activeAccount.addressBTC, - utxos, - btcToSatoshi(amount), - fakeFeeRate - ); - - return BigInt(byteLength); - }, - [activeAccount?.addressBTC, config, utxos] - ); - - return { - address: activeAccount?.addressBTC, - sourceBalance: btcBalance, - assetsWithBalances, - loading, - receiveAmount, - maximum, - price: btcBalance?.price, - estimateGas, - transfer: transferBTC, - }; -} diff --git a/src/pages/Bridge/hooks/useEthBridge.test.ts b/src/pages/Bridge/hooks/useEthBridge.test.ts deleted file mode 100644 index 184ca508..00000000 --- a/src/pages/Bridge/hooks/useEthBridge.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import Big from 'big.js'; -import { - useBridgeSDK, - Blockchain, - AssetType, - isNativeAsset, - getMaxTransferAmount, -} from '@avalabs/core-bridge-sdk'; -import { act, renderHook } from '@testing-library/react-hooks'; - -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useAccountsContext } from '@src/contexts/AccountsProvider'; -import { useNetworkFeeContext } from '@src/contexts/NetworkFeeProvider'; -import { useConnectionContext } from '@src/contexts/ConnectionProvider'; - -import { useEthBridge } from './useEthBridge'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; - -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (k) => k, - }), -})); -jest.mock('@avalabs/core-bridge-sdk'); -jest.mock('./useAssetBalancesEVM'); -jest.mock('@src/contexts/BridgeProvider'); -jest.mock('@src/contexts/NetworkProvider'); -jest.mock('@src/contexts/AccountsProvider'); -jest.mock('@src/contexts/NetworkFeeProvider'); -jest.mock('@src/contexts/ConnectionProvider'); - -describe('src/pages/Bridge/hooks/useEthBridge', () => { - let requestFn = jest.fn(); - let createBridgeTransactionFn = jest.fn(); - let transferEVMAssetFn = jest.fn(); - - const highFee = 20n; - const lowFee = 8n; - - const currentAssetData = { - assetType: AssetType.NATIVE, - denomination: 18, - wrappedAssetSymbol: 'WETH', - tokenName: 'Ethereum', - symbol: 'ETH', - nativeNetwork: Blockchain.ETHEREUM, - }; - - beforeEach(() => { - jest.resetAllMocks(); - - requestFn = jest.fn(); - transferEVMAssetFn = jest.fn(); - createBridgeTransactionFn = jest.fn(); - - jest.mocked(getMaxTransferAmount).mockResolvedValue(new Big('0.01')); - - jest.mocked(useConnectionContext).mockReturnValue({ - request: requestFn, - } as any); - - jest.mocked(useAccountsContext).mockReturnValue({ - accounts: { - active: { - addressC: 'user-c-address', - addressBTC: 'user-btc-address', - }, - }, - } as any); - - jest.mocked(useAssetBalancesEVM).mockReturnValue({ - assetsWithBalances: [ - { - symbol: 'ETH', - asset: currentAssetData, - balance: new Big('0.1'), - price: 2000, - }, - ], - } as any); - - jest.mocked(useBridgeContext).mockReturnValue({ - createBridgeTransaction: createBridgeTransactionFn, - transferEVMAsset: transferEVMAssetFn, - } as any); - - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: 'ETH', - currentAssetData, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.ETHEREUM, - } as any); - - jest.mocked(useNetworkFeeContext).mockReturnValue({ - networkFee: { - high: { - maxFee: highFee, - }, - low: { - maxFee: lowFee, - }, - displayDecimals: 8, - }, - } as any); - - jest.mocked(useNetworkContext).mockReturnValue({ - ethereumProvider: {}, - } as any); - }); - - it('provides maximum transfer amount', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const fakeMax = new Big('0.9'); - jest.mocked(getMaxTransferAmount).mockResolvedValue(fakeMax); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - await act(async () => { - expect(getMaxTransferAmount).toHaveBeenCalled(); - }); - - // Wait for the state to be set - await new Promise(process.nextTick); - - expect(hook.current.maximum).toEqual(fakeMax); - }); - - describe('transfer()', () => { - beforeEach(() => { - jest.mocked(isNativeAsset).mockReturnValue(true); - }); - - describe('when no asset is selected', () => { - beforeEach(() => { - jest.mocked(useBridgeSDK).mockReturnValue({ - bridgeConfig: { - config: {}, - }, - currentAsset: '', - currentAssetData: undefined, - setTransactionDetails: jest.fn(), - currentBlockchain: Blockchain.ETHEREUM, - } as any); - }); - - it('throws error', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - await act(async () => { - await expect(hook.current.transfer()).rejects.toThrow( - 'No asset selected' - ); - }); - }); - }); - - it("calls the provider's transfer function", async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - const hash = await hook.current.transfer(); - - expect(transferEVMAssetFn).toHaveBeenCalledWith( - amount, - currentAssetData - ); - - expect(hash).toEqual(fakeHash); - }); - }); - - it('tracks the bridge transaction', async () => { - const amount = new Big('0.1'); - const fee = new Big('0.0003'); - const minimum = new Big('0.0006'); - - const { result: hook } = renderHook(() => - useEthBridge(amount, fee, minimum) - ); - - const fakeHash = '0xHash'; - - transferEVMAssetFn.mockResolvedValue({ - hash: fakeHash, - }); - - await act(async () => { - await hook.current.transfer(); - - expect(createBridgeTransactionFn).toHaveBeenCalledWith({ - sourceChain: Blockchain.ETHEREUM, - sourceTxHash: fakeHash, - sourceStartedAt: expect.any(Number), - targetChain: Blockchain.AVALANCHE, - amount, - symbol: 'WETH', - }); - }); - }); - }); -}); diff --git a/src/pages/Bridge/hooks/useEthBridge.ts b/src/pages/Bridge/hooks/useEthBridge.ts deleted file mode 100644 index 59d74f04..00000000 --- a/src/pages/Bridge/hooks/useEthBridge.ts +++ /dev/null @@ -1,134 +0,0 @@ -import Big from 'big.js'; -import { - BIG_ZERO, - Blockchain, - getAssets, - getMaxTransferAmount, - isNativeAsset, - useBridgeSDK, -} from '@avalabs/core-bridge-sdk'; -import { useBridgeContext } from '@src/contexts/BridgeProvider'; -import { useCallback, useEffect, useState } from 'react'; -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { BridgeAdapter } from './useBridge'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; - -/** - * Hook for when the bridge source chain is Ethereum - */ -export function useEthBridge( - amount: Big, - bridgeFee: Big, - minimum: Big -): BridgeAdapter { - const { - currentAsset, - currentAssetData, - bridgeConfig, - setTransactionDetails, - currentBlockchain, - } = useBridgeSDK(); - const [maximum, setMaximum] = useState(undefined); - const isEthereumBridge = currentBlockchain === Blockchain.ETHEREUM; - - const { createBridgeTransaction, transferEVMAsset, estimateGas } = - useBridgeContext(); - const { assetsWithBalances } = useAssetBalancesEVM(Blockchain.ETHEREUM); - const { ethereumProvider } = useNetworkContext(); - const sourceBalance = assetsWithBalances.find( - ({ asset }) => asset.symbol === currentAsset - ); - - const receiveAmount = amount.gt(minimum) ? amount.minus(bridgeFee) : BIG_ZERO; - - useEffect(() => { - if ( - !currentAsset || - !isEthereumBridge || - !bridgeConfig.config || - !sourceBalance?.balance || - !ethereumProvider - ) { - return; - } - - const ethereumAssets = getAssets(currentBlockchain, bridgeConfig.config); - let isMounted = true; - - // Estimating gas can take a couple seconds - reset it before calculating - // so we don't use a stale value. - setMaximum(undefined); - - getMaxTransferAmount({ - currentBlockchain, - balance: sourceBalance.balance, - currentAsset, - assets: ethereumAssets, - provider: ethereumProvider, - config: bridgeConfig.config, - }).then((max) => { - if (!isMounted) { - return; - } - - setMaximum(max ?? undefined); - }); - - return () => { - isMounted = false; - }; - }, [ - bridgeConfig?.config, - currentAsset, - currentBlockchain, - ethereumProvider, - isEthereumBridge, - sourceBalance?.balance, - ]); - - const transfer = useCallback(async () => { - if (!currentAssetData) { - throw new Error('No asset selected'); - } - - const timestamp = Date.now(); - - const symbol = isNativeAsset(currentAssetData) - ? currentAssetData.wrappedAssetSymbol - : currentAsset || ''; - - const result = await transferEVMAsset(amount, currentAssetData); - - setTransactionDetails({ - tokenSymbol: symbol, - amount, - }); - createBridgeTransaction({ - sourceChain: Blockchain.ETHEREUM, - sourceTxHash: result.hash, - sourceStartedAt: timestamp, - targetChain: Blockchain.AVALANCHE, - amount, - symbol, - }); - - return result.hash; - }, [ - amount, - currentAssetData, - createBridgeTransaction, - currentAsset, - setTransactionDetails, - transferEVMAsset, - ]); - - return { - sourceBalance, - assetsWithBalances, - receiveAmount, - maximum, - price: sourceBalance?.price, - estimateGas, - transfer, - }; -} diff --git a/src/pages/Bridge/hooks/useHasEnoughtForGas.ts b/src/pages/Bridge/hooks/useHasEnoughtForGas.ts index c9a61f84..5edb33e7 100644 --- a/src/pages/Bridge/hooks/useHasEnoughtForGas.ts +++ b/src/pages/Bridge/hooks/useHasEnoughtForGas.ts @@ -22,7 +22,7 @@ export const useHasEnoughForGas = (gasLimit?: bigint): boolean => { // get gasPrice of network const balance = token && token.balance; - const estimatedGasCost = networkFee.low.maxFee * gasLimit; + const estimatedGasCost = (networkFee.low.maxFee / 2n) * gasLimit; // check if balance > gasPrice if (balance && estimatedGasCost) { setHasEnough( diff --git a/src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts b/src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts deleted file mode 100644 index 7fe4446b..00000000 --- a/src/pages/Bridge/hooks/useSetBridgeChainFromNetwork.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Blockchain, useBridgeSDK } from '@avalabs/core-bridge-sdk'; -import { ChainId } from '@avalabs/core-chains-sdk'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; -import { useEffect } from 'react'; - -export const useSetBridgeChainFromNetwork = () => { - const { network } = useNetworkContext(); - const { setCurrentBlockchain } = useBridgeSDK(); - - useEffect(() => { - switch (network?.chainId) { - case ChainId.BITCOIN: - case ChainId.BITCOIN_TESTNET: - setCurrentBlockchain(Blockchain.BITCOIN); - } - // Run once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); -}; diff --git a/src/pages/Bridge/hooks/useUnifiedBridge.ts b/src/pages/Bridge/hooks/useUnifiedBridge.ts deleted file mode 100644 index 835ff5e9..00000000 --- a/src/pages/Bridge/hooks/useUnifiedBridge.ts +++ /dev/null @@ -1,187 +0,0 @@ -import Big from 'big.js'; -import { bigToBigInt } from '@avalabs/core-utils-sdk'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { - Asset, - BIG_ZERO, - Blockchain, - isNativeAsset, - useBridgeSDK, -} from '@avalabs/core-bridge-sdk'; - -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; -import { bigintToBig } from '@src/utils/bigintToBig'; -import { useNetworkContext } from '@src/contexts/NetworkProvider'; - -import { useAssetBalancesEVM } from './useAssetBalancesEVM'; -import { BridgeAdapter } from './useBridge'; -import { isUnifiedBridgeAsset } from '../utils/isUnifiedBridgeAsset'; -import { useAnalyticsContext } from '@src/contexts/AnalyticsProvider'; - -/** - * Hook for when the Unified Bridge SDK can handle the transfer - */ -export function useUnifiedBridge( - amount: Big, - targetChainId: number, - currentAssetIdentifier?: string -): BridgeAdapter { - const { - currentAsset, - currentAssetData, - setTransactionDetails, - currentBlockchain, - targetBlockchain, - } = useBridgeSDK(); - const { network } = useNetworkContext(); - const { capture } = useAnalyticsContext(); - const { estimateTransferGas, getFee, transferAsset, supportsAsset } = - useUnifiedBridgeContext(); - - const [receiveAmount, setReceiveAmount] = useState(); - const [maximum, setMaximum] = useState(); - const [minimum, setMinimum] = useState(); - const [bridgeFee, setBridgeFee] = useState(); - - const isEthereum = currentBlockchain === Blockchain.ETHEREUM; - const { assetsWithBalances } = useAssetBalancesEVM( - isEthereum ? Blockchain.ETHEREUM : Blockchain.AVALANCHE - ); - const sourceBalance = useMemo(() => { - if (!currentAsset || !currentAssetIdentifier || !network) { - return undefined; - } - - return assetsWithBalances.find(({ asset }) => { - return isUnifiedBridgeAsset(asset) && asset.symbol === currentAsset; - }); - }, [network, assetsWithBalances, currentAssetIdentifier, currentAsset]); - - useEffect(() => { - if (!maximum && sourceBalance?.balance) { - setMaximum(sourceBalance.balance); - } - }, [maximum, sourceBalance]); - - useEffect(() => { - let isMounted = true; - - if ( - currentAsset && - currentAssetData && - currentAssetIdentifier && - amount && - supportsAsset(currentAssetIdentifier, targetChainId) - ) { - const hasAmount = amount && !amount.eq(BIG_ZERO); - - if (hasAmount) { - getFee( - currentAsset, - bigToBigInt(amount, currentAssetData.denomination), - targetChainId - ).then((fee) => { - if (!isMounted) { - return; - } - - const feeBig = bigintToBig(fee, currentAssetData.denomination); - setBridgeFee(feeBig); - setMinimum(feeBig); - setReceiveAmount(amount.sub(feeBig)); - }); - } - - if (sourceBalance?.balance) { - setMaximum(sourceBalance.balance); - } - } - - return () => { - isMounted = false; - }; - }, [ - currentAsset, - currentAssetData, - currentAssetIdentifier, - amount, - targetChainId, - getFee, - sourceBalance?.balance, - supportsAsset, - ]); - - const estimateGas = useCallback( - (transferAmount: Big, asset: Asset) => { - if (!asset) { - throw new Error('No asset data'); - } - - const symbol = isNativeAsset(asset) - ? asset.wrappedAssetSymbol - : asset.symbol; - - return estimateTransferGas( - symbol, - bigToBigInt(transferAmount, asset.denomination), - targetChainId - ); - }, - [estimateTransferGas, targetChainId] - ); - - const transfer = useCallback(async () => { - capture('unifedBridgeTransferStarted', { - bridgeType: 'CCTP', - sourceBlockchain: currentBlockchain, - targetBlockchain, - }); - - if (!currentAsset) { - throw new Error('No asset chosen'); - } - - if (!currentAssetData) { - throw new Error('No asset data'); - } - - const symbol = isNativeAsset(currentAssetData) - ? currentAssetData.wrappedAssetSymbol - : currentAsset || ''; - - const hash = await transferAsset( - currentAsset, - bigToBigInt(amount, currentAssetData.denomination), - targetChainId - ); - - setTransactionDetails({ - tokenSymbol: symbol, - amount, - }); - - return hash; - }, [ - amount, - currentAssetData, - currentAsset, - setTransactionDetails, - transferAsset, - targetChainId, - capture, - currentBlockchain, - targetBlockchain, - ]); - - return { - sourceBalance, - estimateGas, - assetsWithBalances, - receiveAmount, - bridgeFee, - maximum, - minimum, - price: sourceBalance?.price, - transfer, - }; -} diff --git a/src/pages/Bridge/models.ts b/src/pages/Bridge/models.ts index f5905c95..a83c80e8 100644 --- a/src/pages/Bridge/models.ts +++ b/src/pages/Bridge/models.ts @@ -1,26 +1,12 @@ -import { Asset, Blockchain } from '@avalabs/core-bridge-sdk'; import { BridgeAsset } from '@avalabs/bridge-unified'; import Big from 'big.js'; export interface AssetBalance { symbol: string; - asset: Asset | BridgeAsset; + asset: BridgeAsset; balance: Big | undefined; symbolOnNetwork?: string; logoUri?: string; price?: number; unconfirmedBalance?: Big; } - -export const blockchainDisplayNameMap = new Map([ - [Blockchain.AVALANCHE, 'Avalanche C-Chain'], - [Blockchain.ETHEREUM, 'Ethereum'], - [Blockchain.BITCOIN, 'Bitcoin'], - [Blockchain.UNKNOWN, ''], -]); - -export const SUPPORTED_CHAINS = [ - Blockchain.AVALANCHE, - Blockchain.ETHEREUM, - Blockchain.BITCOIN, -]; diff --git a/src/pages/Bridge/utils/findMatchingBridgeAsset.ts b/src/pages/Bridge/utils/findMatchingBridgeAsset.ts new file mode 100644 index 00000000..39ad7481 --- /dev/null +++ b/src/pages/Bridge/utils/findMatchingBridgeAsset.ts @@ -0,0 +1,27 @@ +import { + BridgeAsset, + TokenType as BridgeTokenType, +} from '@avalabs/bridge-unified'; + +import { + NftTokenWithBalance, + TokenType, + TokenWithBalance, +} from '@avalabs/vm-module-types'; + +export const findMatchingBridgeAsset = ( + assets: BridgeAsset[], + token: Exclude +): BridgeAsset | undefined => { + return assets.find((a) => { + if (a.type === BridgeTokenType.NATIVE && token.type === TokenType.NATIVE) { + return a.symbol.toLowerCase() === a.symbol.toLowerCase(); + } + + if (a.type === BridgeTokenType.ERC20 && token.type === TokenType.ERC20) { + return a.address.toLowerCase() === token.address.toLowerCase(); + } + + return false; + }); +}; diff --git a/src/pages/Bridge/utils/getBalances.ts b/src/pages/Bridge/utils/getBalances.ts deleted file mode 100644 index 7cadd0dd..00000000 --- a/src/pages/Bridge/utils/getBalances.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Asset, isBtcAsset, isNativeAsset } from '@avalabs/core-bridge-sdk'; -import { AssetBalance } from '@src/pages/Bridge/models'; -import { - BridgeAsset, - TokenType as UnifiedTokenType, -} from '@avalabs/bridge-unified'; -import { isUnifiedBridgeAsset } from './isUnifiedBridgeAsset'; -import { normalizeBalance } from '@src/utils/normalizeBalance'; -import { - NetworkTokenWithBalance, - TokenType, - TokenWithBalance, - TokenWithBalanceERC20, -} from '@avalabs/vm-module-types'; - -/** - * Get balances of wrapped erc20 tokens on Avalanche - * @param assets - * @param tokens - */ -export function getBalances( - assets: Array, - tokens: TokenWithBalance[] -): AssetBalance[] { - const tokensByAddress = tokens.reduce<{ - [address: string]: - | TokenWithBalanceERC20 - | NetworkTokenWithBalance - | undefined; - }>((tokensMap, token) => { - if (token.type !== TokenType.ERC20 && token.type !== TokenType.NATIVE) { - return tokensMap; - } - - if (token.type !== TokenType.ERC20) { - tokensMap[token.symbol.toLowerCase()] = token; - return tokensMap; - } - // Need to convert the keys to lowercase because they are mixed case, and this messes up or comparison function - tokensMap[token.address.toLowerCase()] = token; - return tokensMap; - }, {}); - - return assets.map((asset) => { - const symbol = asset.symbol; - const token = isUnifiedBridgeAsset(asset) - ? tokensByAddress[ - asset.type === UnifiedTokenType.NATIVE - ? asset.symbol.toLowerCase() - : asset.address.toLowerCase() - ] - : isNativeAsset(asset) - ? tokensByAddress[asset.symbol.toLowerCase()] - : isBtcAsset(asset) - ? tokensByAddress[asset.wrappedContractAddress.toLowerCase()] - : tokensByAddress[asset.wrappedContractAddress?.toLowerCase()] || - tokensByAddress[asset.nativeContractAddress?.toLowerCase()]; - - const balance = token && normalizeBalance(token.balance, token.decimals); - const logoUri = token?.logoUri; - const price = token?.priceInCurrency; - - return { symbol, asset, balance, logoUri, price }; - }); -} diff --git a/src/pages/Bridge/utils/getTokenAddress.ts b/src/pages/Bridge/utils/getTokenAddress.ts deleted file mode 100644 index 77cf0403..00000000 --- a/src/pages/Bridge/utils/getTokenAddress.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isEthAsset } from '@avalabs/core-bridge-sdk'; -import { AssetBalance } from '../models'; -import { isUnifiedBridgeAsset } from './isUnifiedBridgeAsset'; -import { TokenType } from '@avalabs/bridge-unified'; - -export const getTokenAddress = (token: AssetBalance): string => { - if (isUnifiedBridgeAsset(token.asset)) { - return token.asset.type === TokenType.ERC20 ? token.asset.address : ''; - } else if (isEthAsset(token.asset)) { - return token.asset.nativeContractAddress; - } - return ''; -}; diff --git a/src/pages/Bridge/utils/isUnifiedBridgeAsset.ts b/src/pages/Bridge/utils/isUnifiedBridgeAsset.ts deleted file mode 100644 index c4ae8328..00000000 --- a/src/pages/Bridge/utils/isUnifiedBridgeAsset.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { BridgeAsset } from '@avalabs/bridge-unified'; - -export const isUnifiedBridgeAsset = (asset: unknown): asset is BridgeAsset => { - return asset !== null && typeof asset === 'object' && 'destinations' in asset; -}; diff --git a/yarn.lock b/yarn.lock index e57a1998..221fc0ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,11 +74,13 @@ bn.js "5.2.1" zod "3.23.8" -"@avalabs/bridge-unified@0.0.0-feat-is-bridge-address-20240926120228": - version "0.0.0-feat-is-bridge-address-20240926120228" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-is-bridge-address-20240926120228.tgz#5cd14896a519579f2de50c7c3bd7ce28835f756c" - integrity sha512-0k7S9Ft0QvX7En7BUv7uoKfkplWv5eQDzxEIGWkMVfvAS/89aaUY8cGbyIg0hG29ogQi811bcCZIDEo05rEiGg== +"@avalabs/bridge-unified@0.0.0-feat-ictt-configs-20241002113306": + version "0.0.0-feat-ictt-configs-20241002113306" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-ictt-configs-20241002113306.tgz#15ef3581189fdcf0209c9286ed1e11fce8ab6f51" + integrity sha512-u1t4vV1cc5LH8v7ddraTOJkt1sKmopI7t81tCo5rKMc96eBOVjQjTniXCxG0c1E/BID0QgQcL5rTC1e861C/YA== dependencies: + "@noble/hashes" "1.3.3" + "@scure/base" "1.1.5" abitype "0.9.3" lodash "4.17.21" viem "2.11.1" From 987870c655714a3419407d49312f5a4780c60afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Tue, 8 Oct 2024 14:41:48 +0200 Subject: [PATCH 11/11] chore: use analyzeTx() from UnifiedBridge SDK --- package.json | 2 +- .../services/bridge/BridgeService.test.ts | 8 +- .../services/bridge/BridgeService.ts | 2 +- .../handlers/avalanche_bridgeAsset.test.ts | 4 +- .../bridge/handlers/avalanche_bridgeAsset.ts | 2 +- .../services/history/HistoryService.test.ts | 43 ++++--- .../services/history/HistoryService.ts | 45 ++++---- .../history/HistoryServiceBridgeHelper.ts | 93 --------------- src/background/services/history/models.ts | 51 +-------- .../history/utils/isTxHistoryItem.test.ts | 8 +- .../services/history/utils/isTxHistoryItem.ts | 2 +- .../unifiedBridge/UnifiedBridgeService.ts | 11 +- src/contexts/UnifiedBridgeProvider.test.tsx | 32 ++++-- src/contexts/UnifiedBridgeProvider.tsx | 17 +-- src/localization/locales/en/translation.json | 4 +- src/pages/Wallet/WalletRecentTxs.tsx | 14 ++- .../components/ActivityCard/ActivityCard.tsx | 2 +- .../ActivityCard/ActivityCardIcon.tsx | 2 +- .../ActivityCard/ActivityCardSummary.tsx | 8 +- .../components/History/useBlockchainNames.ts | 107 ++++-------------- yarn.lock | 8 +- 21 files changed, 150 insertions(+), 315 deletions(-) delete mode 100644 src/background/services/history/HistoryServiceBridgeHelper.ts diff --git a/package.json b/package.json index abae4ffc..6e32dd0f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@avalabs/avalanche-module": "0.7.0", "@avalabs/avalanchejs": "4.0.5", - "@avalabs/bridge-unified": "0.0.0-feat-ictt-configs-20241002113306", + "@avalabs/bridge-unified": "0.0.0-feat-ictt-configs-20241008120400", "@avalabs/bitcoin-module": "0.7.0", "@avalabs/core-bridge-sdk": "3.1.0-alpha.4", "@avalabs/core-chains-sdk": "3.1.0-alpha.4", diff --git a/src/background/services/bridge/BridgeService.test.ts b/src/background/services/bridge/BridgeService.test.ts index 302cb528..966687a6 100644 --- a/src/background/services/bridge/BridgeService.test.ts +++ b/src/background/services/bridge/BridgeService.test.ts @@ -54,9 +54,11 @@ const addressBTC = 'tb01234'; const networkBalancesService = { getBalancesForNetworks: async () => ({ - [ChainId.BITCOIN_TESTNET]: { - [addressBTC]: { - BTC: {}, + tokens: { + [ChainId.BITCOIN_TESTNET]: { + [addressBTC]: { + BTC: {}, + }, }, }, }), diff --git a/src/background/services/bridge/BridgeService.ts b/src/background/services/bridge/BridgeService.ts index 1001ff31..f31578eb 100644 --- a/src/background/services/bridge/BridgeService.ts +++ b/src/background/services/bridge/BridgeService.ts @@ -197,7 +197,7 @@ export class BridgeService implements OnLock, OnStorageReady { [this.accountsService.activeAccount] ); - const token = balances[btcNetwork.chainId]?.[addressBtc]?.[ + const token = balances.tokens[btcNetwork.chainId]?.[addressBtc]?.[ 'BTC' ] as TokenWithBalanceBTC; diff --git a/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts b/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts index 782287a8..ef68d573 100644 --- a/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts +++ b/src/background/services/bridge/handlers/avalanche_bridgeAsset.test.ts @@ -167,7 +167,9 @@ describe('background/services/bridge/handlers/avalanche_bridgeAsset', () => { networkServiceMock.getBitcoinProvider.mockResolvedValue({ waitForTx: jest.fn().mockResolvedValue(btcResult), }); - balanceAggregatorServiceMock.getBalancesForNetworks.mockResolvedValue({}); + balanceAggregatorServiceMock.getBalancesForNetworks.mockResolvedValue({ + tokens: {}, + }); jest.mocked(openApprovalWindow).mockResolvedValue({} as any); jest.mocked(getAssets).mockReturnValue({ BTC: btcAsset, diff --git a/src/background/services/bridge/handlers/avalanche_bridgeAsset.ts b/src/background/services/bridge/handlers/avalanche_bridgeAsset.ts index 26d894f7..542aa01c 100644 --- a/src/background/services/bridge/handlers/avalanche_bridgeAsset.ts +++ b/src/background/services/bridge/handlers/avalanche_bridgeAsset.ts @@ -310,7 +310,7 @@ export class AvalancheBridgeAsset extends DAppRequestHandler 0 ); - const token = balances[network.chainId]?.[addressBTC]?.[ + const token = balances.tokens[network.chainId]?.[addressBTC]?.[ 'BTC' ] as TokenWithBalanceBTC; diff --git a/src/background/services/history/HistoryService.test.ts b/src/background/services/history/HistoryService.test.ts index 7b220633..36f578bf 100644 --- a/src/background/services/history/HistoryService.test.ts +++ b/src/background/services/history/HistoryService.test.ts @@ -4,6 +4,7 @@ import { TxHistoryItem } from './models'; import { TokenType } from '@avalabs/vm-module-types'; import { TransactionType } from '@avalabs/vm-module-types'; import { ETHEREUM_ADDRESS } from '@src/utils/bridgeTransactionUtils'; +import { BridgeType } from '@avalabs/bridge-unified'; describe('src/background/services/history/HistoryService.ts', () => { let service: HistoryService; @@ -37,15 +38,14 @@ describe('src/background/services/history/HistoryService.ts', () => { addressAVM: 'addressBtc', }, } as any; - const bridgeHistoryHelperServiceMock = { - isBridgeTransactionBTC: jest.fn(), - } as any; const unifiedBridgeServiceMock = { - isBridgeTx: jest.fn().mockReturnValue(false), + analyzeTx: jest.fn(), } as any; const txHistoryItem: TxHistoryItem = { - isBridge: false, + bridgeAnalysis: { + isBridgeTx: false, + }, isContractCall: true, isIncoming: false, isOutgoing: true, @@ -70,7 +70,9 @@ describe('src/background/services/history/HistoryService.ts', () => { }; const btcTxHistoryItem: TxHistoryItem = { - isBridge: false, + bridgeAnalysis: { + isBridgeTx: false, + }, isContractCall: true, isIncoming: false, isOutgoing: true, @@ -100,9 +102,12 @@ describe('src/background/services/history/HistoryService.ts', () => { service = new HistoryService( moduleManagereMock, accountsServiceMock, - bridgeHistoryHelperServiceMock, unifiedBridgeServiceMock ); + + jest + .mocked(unifiedBridgeServiceMock.analyzeTx) + .mockReturnValue({ isBridgeTx: false }); }); it('should return empty array when network is not supported', async () => { @@ -135,9 +140,6 @@ describe('src/background/services/history/HistoryService.ts', () => { return { transactions: [btcTxHistoryItem] }; }), }); - jest - .mocked(bridgeHistoryHelperServiceMock.isBridgeTransactionBTC) - .mockReturnValue(false); const result = await service.getTxHistory({ ...network1, vmName: NetworkVMType.BITCOIN, @@ -151,18 +153,21 @@ describe('src/background/services/history/HistoryService.ts', () => { return { transactions: [btcTxHistoryItem] }; }), }); - jest - .mocked(bridgeHistoryHelperServiceMock.isBridgeTransactionBTC) - .mockReturnValue(true); const result = await service.getTxHistory({ ...network1, vmName: NetworkVMType.BITCOIN, caipId: 'bip122:000000000019d6689c085ae165831e93', }); - expect(result).toEqual([{ ...btcTxHistoryItem, isBridge: true }]); + expect(result).toEqual([ + { ...btcTxHistoryItem, bridgeAnalysis: { isBridgeTx: true } }, + ]); }); it('should return results with an ETH bridge transaction', async () => { + jest.mocked(unifiedBridgeServiceMock.analyzeTx).mockReturnValue({ + isBridgeTx: true, + bridgeType: BridgeType.AVALANCHE_EVM, + }); jest.mocked(moduleManagereMock.loadModuleByNetwork).mockResolvedValue({ getTransactionHistory: jest.fn(() => { return { @@ -181,11 +186,17 @@ describe('src/background/services/history/HistoryService.ts', () => { caipId: 'caip', }); expect(result).toEqual([ - { ...txHistoryItem, isBridge: true, from: ETHEREUM_ADDRESS }, + { + ...txHistoryItem, + bridgeAnalysis: { isBridgeTx: true }, + from: ETHEREUM_ADDRESS, + }, ]); }); it('should return results with an pchain transaction', async () => { - jest.mocked(unifiedBridgeServiceMock.isBridgeTx).mockReturnValue(false); + jest + .mocked(unifiedBridgeServiceMock.analyzeTx) + .mockReturnValue({ isBridgeTx: false }); jest.mocked(moduleManagereMock.loadModuleByNetwork).mockResolvedValue({ getTransactionHistory: jest.fn(() => { return { diff --git a/src/background/services/history/HistoryService.ts b/src/background/services/history/HistoryService.ts index 370e93d6..d44b474f 100644 --- a/src/background/services/history/HistoryService.ts +++ b/src/background/services/history/HistoryService.ts @@ -4,21 +4,19 @@ import { NetworkWithCaipId } from '../network/models'; import { ModuleManager } from '@src/background/vmModules/ModuleManager'; import { AccountsService } from '../accounts/AccountsService'; import { TxHistoryItem } from './models'; -import { HistoryServiceBridgeHelper } from './HistoryServiceBridgeHelper'; import { Transaction } from '@avalabs/vm-module-types'; -import { ETHEREUM_ADDRESS } from '@src/utils/bridgeTransactionUtils'; import { UnifiedBridgeService } from '../unifiedBridge/UnifiedBridgeService'; import { resolve } from '@src/utils/promiseResolver'; import sentryCaptureException, { SentryExceptionTypes, } from '@src/monitoring/sentryCaptureException'; +import { AnalyzeTxParams } from '@avalabs/bridge-unified'; @singleton() export class HistoryService { constructor( private moduleManager: ModuleManager, private accountsService: AccountsService, - private bridgeHistoryHelperService: HistoryServiceBridgeHelper, private unifiedBridgeService: UnifiedBridgeService ) {} @@ -52,26 +50,31 @@ export class HistoryService { }); const txHistoryItem = transactions.map((transaction) => { - const isBridge = this.#getIsBirdge(network, transaction); + const result = this.#analyze(network, transaction); const vmType = network.vmName; - return { ...transaction, vmType, isBridge }; + return { + ...transaction, + vmType, + bridgeAnalysis: result, + }; }) as TxHistoryItem[]; return txHistoryItem; } - #getIsBirdge(network: NetworkWithCaipId, transaction: Transaction) { - if (network.vmName === NetworkVMType.BITCOIN) { - return this.bridgeHistoryHelperService.isBridgeTransactionBTC([ - transaction.from, - transaction.to, - ]); - } - return ( - this.#isBridgeAddress(transaction.from) || - this.#isBridgeAddress(transaction.to) || - this.unifiedBridgeService.isBridgeTx(transaction) - ); + #analyze(network: NetworkWithCaipId, transaction: Transaction) { + const params: AnalyzeTxParams = { + from: transaction.from as `0x${string}`, + to: transaction.to as `0x${string}`, + chainId: network.caipId, + tokenTransfers: transaction.tokens.map((transfer) => ({ + symbol: transfer.symbol, + from: (transfer.from?.address ?? transaction.from) as `0x${string}`, + to: (transfer.to?.address ?? transaction.to) as `0x${string}`, + })), + }; + + return this.unifiedBridgeService.analyzeTx(params); } #getAddress(network: NetworkWithCaipId) { @@ -90,12 +93,4 @@ export class HistoryService { return undefined; } } - - #isBridgeAddress(address?: string) { - if (!address) { - return false; - } - - return ETHEREUM_ADDRESS === address.toLowerCase(); - } } diff --git a/src/background/services/history/HistoryServiceBridgeHelper.ts b/src/background/services/history/HistoryServiceBridgeHelper.ts deleted file mode 100644 index bec22fe7..00000000 --- a/src/background/services/history/HistoryServiceBridgeHelper.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ETHEREUM_ADDRESS } from '@src/utils/bridgeTransactionUtils'; -import { BridgeService } from '../bridge/BridgeService'; -import { singleton } from 'tsyringe'; -import { Erc20Tx } from '@avalabs/core-snowtrace-sdk'; -import { Network } from '@avalabs/core-chains-sdk'; -import { isEthereumNetwork } from '../network/utils/isEthereumNetwork'; -import { UnifiedBridgeService } from '../unifiedBridge/UnifiedBridgeService'; - -@singleton() -export class HistoryServiceBridgeHelper { - constructor( - private bridgeService: BridgeService, - private unifiedBridgeService: UnifiedBridgeService - ) {} - - /** - * Checking if the transaction is a bridge transaction with Ethereum - * - * If the tx is TransactionERC20 - * to or from has to be ETHEREUM_ADDRESS - * and the contractAddress of the transaction has to match one of the addresses from EthereumConfigAssets - * - * If the tx is BitcoinHistoryTx - * it should be false since we currently do not support bridge between Bitcoin and Ethereum - */ - isBridgeTransactionEVM(tx: Erc20Tx, network: Network): boolean { - const config = this.bridgeService.bridgeConfig; - const ethereumAssets = config?.config?.critical.assets; - const bitcoinAssets = config?.config?.criticalBitcoin?.bitcoinAssets; - - if (!ethereumAssets || !bitcoinAssets) { - return false; - } - - if (this.unifiedBridgeService.isBridgeTx(tx)) { - return true; - } - - if (isEthereumNetwork(network)) { - const ethBridgeAddress = config.config?.critical.walletAddresses.ethereum; - return ( - tx.to.toLowerCase() === ethBridgeAddress || - tx.from.toLowerCase() === ethBridgeAddress - ); - } else { - return ( - Object.values(ethereumAssets).some(({ wrappedContractAddress }) => { - return ( - wrappedContractAddress.toLowerCase() === - tx.contractAddress.toLowerCase() && - (tx.to === ETHEREUM_ADDRESS || tx.from === ETHEREUM_ADDRESS) - ); - }) || - Object.values(bitcoinAssets).some( - ({ wrappedContractAddress }) => - wrappedContractAddress.toLowerCase() === - tx.contractAddress.toLowerCase() - ) - ); - } - } - - /** - * Checking if the transaction is a bridge transaction with Bitcoin - * - * If the tx is TransactionERC20 - * The contractAddress from the transaction has to match one of the addresses from BitcoinConfigAssets - * - * If the tx is BitcoinHistoryTx - * addresses is an array of addresses to or from. - * It should include - * config.criticalBitcoin?.walletAddresses.btc or - * config.criticalBitcoin?.walletAddresses.avalanche - */ - isBridgeTransactionBTC(addresses: string[]): boolean { - const config = this.bridgeService.bridgeConfig; - const bitcoinWalletAddresses = - config?.config?.criticalBitcoin?.walletAddresses; - - if (!bitcoinWalletAddresses) { - return false; - } - - return addresses.some((address) => { - return [ - bitcoinWalletAddresses.btc, - bitcoinWalletAddresses.avalanche, - ].some( - (walletAddress) => address.toLowerCase() === walletAddress.toLowerCase() - ); - }); - } -} diff --git a/src/background/services/history/models.ts b/src/background/services/history/models.ts index e2bafafa..5caab75f 100644 --- a/src/background/services/history/models.ts +++ b/src/background/services/history/models.ts @@ -1,52 +1,7 @@ -import { - NetworkVMType, - Transaction, - TransactionType, -} from '@avalabs/vm-module-types'; +import { AnalyzeTxResult } from '@avalabs/bridge-unified'; +import { NetworkVMType, Transaction } from '@avalabs/vm-module-types'; export interface TxHistoryItem extends Transaction { - isBridge: boolean; + bridgeAnalysis: AnalyzeTxResult; vmType?: NetworkVMType; } - -export const NonContractCallTypes = [ - TransactionType.BRIDGE, - TransactionType.SEND, - TransactionType.RECEIVE, - TransactionType.TRANSFER, -]; - -export interface HistoryItemCategories { - isBridge: boolean; - isSwap: boolean; - isNativeSend: boolean; - isNativeReceive: boolean; - isNFTPurchase: boolean; - isApprove: boolean; - isTransfer: boolean; - isAirdrop: boolean; - isUnwrap: boolean; - isFillOrder: boolean; - isContractCall: boolean; - method: string; - type: TransactionType; -} - -export interface SubnetHistoryItem { - hash: string; - status: number; - gasPrice: number; - gasUsed: number; - timestamp: number; - from: string; - to: string; - value: string; - method: string; - type: number; - block: number; - toContract?: { - name: string; - symbol: string; - decimals: number; - }; -} diff --git a/src/background/services/history/utils/isTxHistoryItem.test.ts b/src/background/services/history/utils/isTxHistoryItem.test.ts index f59dfd22..8d755542 100644 --- a/src/background/services/history/utils/isTxHistoryItem.test.ts +++ b/src/background/services/history/utils/isTxHistoryItem.test.ts @@ -9,7 +9,9 @@ import { TransactionType } from '@avalabs/vm-module-types'; describe('src/background/services/history/utils/isTxHistoryItem.ts', () => { const txHistoryItem: TxHistoryItem = { - isBridge: false, + bridgeAnalysis: { + isBridgeTx: false, + }, isContractCall: true, isIncoming: false, isOutgoing: true, @@ -32,7 +34,9 @@ describe('src/background/services/history/utils/isTxHistoryItem.ts', () => { txType: TransactionType.SEND, }; const pchainTxHistoryItem: TxHistoryItem = { - isBridge: false, + bridgeAnalysis: { + isBridgeTx: false, + }, isContractCall: true, isIncoming: false, isOutgoing: true, diff --git a/src/background/services/history/utils/isTxHistoryItem.ts b/src/background/services/history/utils/isTxHistoryItem.ts index dfcd3b73..1305488b 100644 --- a/src/background/services/history/utils/isTxHistoryItem.ts +++ b/src/background/services/history/utils/isTxHistoryItem.ts @@ -1,7 +1,7 @@ import { TxHistoryItem } from '../models'; export function isTxHistoryItem(tx: TxHistoryItem): tx is TxHistoryItem { - if ('isBridge' in tx && tx.vmType !== 'AVM' && tx.vmType !== 'PVM') { + if ('bridgeAnalysis' in tx && tx.vmType !== 'AVM' && tx.vmType !== 'PVM') { return true; } return false; diff --git a/src/background/services/unifiedBridge/UnifiedBridgeService.ts b/src/background/services/unifiedBridge/UnifiedBridgeService.ts index 7ce3222b..42510380 100644 --- a/src/background/services/unifiedBridge/UnifiedBridgeService.ts +++ b/src/background/services/unifiedBridge/UnifiedBridgeService.ts @@ -1,11 +1,12 @@ import { singleton } from 'tsyringe'; import { + AnalyzeTxParams, + AnalyzeTxResult, BridgeTransfer, BridgeType, createUnifiedBridgeService, Environment, getEnabledBridgeServices, - IsBridgeTxParams, } from '@avalabs/bridge-unified'; import { wait } from '@avalabs/core-utils-sdk'; import EventEmitter from 'events'; @@ -160,12 +161,14 @@ export class UnifiedBridgeService implements OnStorageReady { } } - isBridgeTx(txInfo: IsBridgeTxParams): boolean { + analyzeTx(txInfo: AnalyzeTxParams): AnalyzeTxResult { if (!this.#core) { - return false; + return { + isBridgeTx: false, + }; } - return this.#core.isBridgeTx(txInfo); + return this.#core.analyzeTx(txInfo); } trackTransfer(bridgeTransfer: BridgeTransfer) { diff --git a/src/contexts/UnifiedBridgeProvider.test.tsx b/src/contexts/UnifiedBridgeProvider.test.tsx index 8bc2be6c..3b5189a0 100644 --- a/src/contexts/UnifiedBridgeProvider.test.tsx +++ b/src/contexts/UnifiedBridgeProvider.test.tsx @@ -183,7 +183,7 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.transferAsset('USDC', 1000n, 1); + await provider.current?.transferAsset('USDC', 1000n, 'eip155:1'); } catch (err: any) { expect(err.data.reason).toEqual(CommonError.Unknown); } @@ -195,7 +195,7 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.transferAsset('USDCC', 1000n, 1); + await provider.current?.transferAsset('USDCC', 1000n, 'eip155:1'); } catch (err: any) { expect(err.data?.reason).toEqual(UnifiedBridgeError.UnknownAsset); } @@ -242,9 +242,9 @@ describe('contexts/UnifiedBridgeProvider', () => { const provider = getBridgeProvider(); await waitFor(async () => { - expect(await provider.current?.transferAsset('USDC', 1000n, 1)).toEqual( - transfer.sourceTxHash - ); + expect( + await provider.current?.transferAsset('USDC', 1000n, 'eip155:1') + ).toEqual(transfer.sourceTxHash); expect(core.transferAsset).toHaveBeenCalledWith({ asset: expect.objectContaining({ symbol: 'USDC' }), @@ -301,7 +301,11 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.estimateTransferGas('USDC', 1000n, 1); + await provider.current?.estimateTransferGas( + 'USDC', + 1000n, + 'eip155:1' + ); } catch (err: any) { expect(err.data.reason).toEqual(CommonError.Unknown); } @@ -313,7 +317,11 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.estimateTransferGas('USDC', 1000n, 1); + await provider.current?.estimateTransferGas( + 'USDC', + 1000n, + 'eip155:1' + ); } catch (err: any) { expect(err.data.reason).toEqual(UnifiedBridgeError.UnknownAsset); } @@ -327,7 +335,7 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { expect( - await provider.current?.estimateTransferGas('USDC', 1000n, 1) + await provider.current?.estimateTransferGas('USDC', 1000n, 'eip155:1') ).toEqual(555n); expect(core.estimateGas).toHaveBeenCalledWith({ @@ -356,7 +364,7 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.getFee('USDC', 1000n, 1); + await provider.current?.getFee('USDC', 1000n, 'eip155:1'); } catch (err: any) { expect(err.data.reason).toEqual(CommonError.Unknown); } @@ -368,7 +376,7 @@ describe('contexts/UnifiedBridgeProvider', () => { await waitFor(async () => { try { - await provider.current?.getFee('USDCc', 1000n, 1); + await provider.current?.getFee('USDCc', 1000n, 'eip155:1'); } catch (err: any) { expect(err.data.reason).toEqual(UnifiedBridgeError.UnknownAsset); } @@ -382,7 +390,9 @@ describe('contexts/UnifiedBridgeProvider', () => { const provider = getBridgeProvider(); await waitFor(async () => { - expect(await provider.current?.getFee('USDC', 1000n, 1)).toEqual(300n); + expect( + await provider.current?.getFee('USDC', 1000n, 'eip155:1') + ).toEqual(300n); expect(core.getFees).toHaveBeenCalledWith({ asset: expect.objectContaining({ symbol: 'USDC' }), diff --git a/src/contexts/UnifiedBridgeProvider.tsx b/src/contexts/UnifiedBridgeProvider.tsx index f5ef6701..16534476 100644 --- a/src/contexts/UnifiedBridgeProvider.tsx +++ b/src/contexts/UnifiedBridgeProvider.tsx @@ -18,7 +18,8 @@ import { BridgeTransfer, getEnabledBridgeServices, BridgeServicesMap, - IsBridgeTxParams, + AnalyzeTxParams, + AnalyzeTxResult, } from '@avalabs/bridge-unified'; import { ethErrors } from 'eth-rpc-errors'; import { filter, map } from 'rxjs'; @@ -64,7 +65,7 @@ export interface UnifiedBridgeContext { amount: bigint, targetChainId: string ): Promise; - isBridgeTx(txInfo: IsBridgeTxParams): boolean; + analyzeTx(txInfo: AnalyzeTxParams): AnalyzeTxResult; supportsAsset(address: string, targetChainId: string): boolean; transferAsset( symbol: string, @@ -94,8 +95,8 @@ const DEFAULT_STATE = { supportsAsset() { return false; }, - isBridgeTx() { - return false; + analyzeTx(): AnalyzeTxResult { + return { isBridgeTx: false }; }, transferAsset() { throw new Error('Bridge not ready'); @@ -533,11 +534,11 @@ export function UnifiedBridgeProvider({ [t] ); - const isBridgeTx = useCallback( - (txInfo: IsBridgeTxParams) => { + const analyzeTx = useCallback( + (txInfo: AnalyzeTxParams) => { assert(core, CommonError.Unknown); - return core.isBridgeTx(txInfo); + return core.analyzeTx(txInfo); }, [core] ); @@ -551,7 +552,7 @@ export function UnifiedBridgeProvider({ getErrorMessage, isReady, state, - isBridgeTx, + analyzeTx, getAssetIdentifierOnTargetChain, getFee, supportsAsset, diff --git a/src/localization/locales/en/translation.json b/src/localization/locales/en/translation.json index a95468c0..02b59d68 100644 --- a/src/localization/locales/en/translation.json +++ b/src/localization/locales/en/translation.json @@ -713,6 +713,7 @@ "Select a derivation path to see your derived addresses.": "Select a derivation path to see your derived addresses.", "Select derivation path": "Select derivation path", "Select one of the available verification methods below to proceed.": "Select one of the available verification methods below to proceed.", + "Select target chain": "Select target chain", "Select the Core wallet.": "Select the Core wallet.", "Select the first word": "Select the first word", "Select the word that comes after": "Select the word that comes after", @@ -784,7 +785,6 @@ "Swap transaction succeeded! 🎉": "Swap transaction succeeded! 🎉", "Swiss Franc": "Swiss Franc", "Switch": "Switch", - "Switch to {{chainName}}": "Switch to {{chainName}}", "Switch to {{chainName}} Network?": "Switch to {{chainName}} Network?", "Switch to {{name}}?": "Switch to {{name}}?", "Symbol": "Symbol", @@ -885,6 +885,7 @@ "Try typing the information again or go back to the account manager.": "Try typing the information again or go back to the account manager.", "Turkish": "Turkish", "URI": "URI", + "USDC is routed through Circle's Cross-Chain Transfer Protocol. Bridge FAQs": "USDC is routed through Circle's Cross-Chain Transfer Protocol. Bridge FAQs", "Unable to connect. View the troubleshoot guide here": "Unable to connect. View the troubleshoot guide here", "Unable to connect?": "Unable to connect?", "Unable to set TOTP configuration": "Unable to set TOTP configuration", @@ -1017,6 +1018,5 @@ "{{functionName}} is currently unavailable.": "{{functionName}} is currently unavailable.", "{{length}} Bytes": "{{length}} Bytes", "{{name}} successfully removed!": "{{name}} successfully removed!", - "{{symbol}} is routed through {{bridgeName}}. Bridge FAQs": "{{symbol}} is routed through {{bridgeName}}. Bridge FAQs", "{{walletName}} Added": "{{walletName}} Added" } diff --git a/src/pages/Wallet/WalletRecentTxs.tsx b/src/pages/Wallet/WalletRecentTxs.tsx index bb808e78..cad78958 100644 --- a/src/pages/Wallet/WalletRecentTxs.tsx +++ b/src/pages/Wallet/WalletRecentTxs.tsx @@ -190,7 +190,11 @@ export function WalletRecentTxs({ } function shouldTxBeKept(tx: TxHistoryItem) { - if (isTxHistoryItem(tx) && tx.isBridge && isPendingBridge(tx)) { + if ( + isTxHistoryItem(tx) && + tx.bridgeAnalysis.isBridgeTx && + isPendingBridge(tx) + ) { return false; } return true; @@ -214,12 +218,16 @@ export function WalletRecentTxs({ if (filter === FilterType.ALL) { return true; } else if (filter === FilterType.BRIDGE) { - return tx.txType === TransactionType.BRIDGE || tx.isBridge; + return ( + tx.txType === TransactionType.BRIDGE || tx.bridgeAnalysis.isBridgeTx + ); } else if (filter === FilterType.SWAP) { return tx.txType === TransactionType.SWAP; } else if (filter === FilterType.CONTRACT_CALL) { return ( - tx.isContractCall && !tx.isBridge && tx.txType !== TransactionType.SWAP + tx.isContractCall && + !tx.bridgeAnalysis.isBridgeTx && + tx.txType !== TransactionType.SWAP ); } else if (filter === FilterType.INCOMING) { return tx.isIncoming; diff --git a/src/pages/Wallet/components/History/components/ActivityCard/ActivityCard.tsx b/src/pages/Wallet/components/History/components/ActivityCard/ActivityCard.tsx index d7f7b656..95a7b441 100644 --- a/src/pages/Wallet/components/History/components/ActivityCard/ActivityCard.tsx +++ b/src/pages/Wallet/components/History/components/ActivityCard/ActivityCard.tsx @@ -83,7 +83,7 @@ export function ActivityCard({ historyItem }: ActivityCardProp) { const txTitle = useMemo(() => { if (network) { - if (historyItem.isBridge) { + if (historyItem.bridgeAnalysis.isBridgeTx) { return t('Bridge'); } if ( diff --git a/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardIcon.tsx b/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardIcon.tsx index ec586f12..6eae9082 100644 --- a/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardIcon.tsx +++ b/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardIcon.tsx @@ -48,7 +48,7 @@ export function ActivityCardIcon({ historyItem }: ActivityCardProp) { ); - if (historyItem.isBridge) { + if (historyItem.bridgeAnalysis.isBridgeTx) { setTxIcon(); return; } diff --git a/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardSummary.tsx b/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardSummary.tsx index d472ec34..f1554460 100644 --- a/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardSummary.tsx +++ b/src/pages/Wallet/components/History/components/ActivityCard/ActivityCardSummary.tsx @@ -14,13 +14,17 @@ export function ActivityCardSummary({ historyItem }: ActivityCardProp) { useBlockchainNames(historyItem); const { t } = useTranslation(); - if (historyItem.txType === TransactionType.BRIDGE || historyItem.isBridge) { + if ( + historyItem.txType === TransactionType.BRIDGE || + historyItem.bridgeAnalysis.isBridgeTx + ) { return ( ({ color: theme.palette.primary.dark })} > - {sourceBlockchain} -> {targetBlockchain} + {sourceBlockchain ?? t('Unknown')} ->{' '} + {targetBlockchain ?? t('Unknown')} ); } else if (historyItem.txType === TransactionType.SWAP) { diff --git a/src/pages/Wallet/components/History/useBlockchainNames.ts b/src/pages/Wallet/components/History/useBlockchainNames.ts index 3952db78..0d348ad9 100644 --- a/src/pages/Wallet/components/History/useBlockchainNames.ts +++ b/src/pages/Wallet/components/History/useBlockchainNames.ts @@ -1,22 +1,15 @@ -import { Blockchain, BridgeTransaction } from '@avalabs/core-bridge-sdk'; -import { BridgeTransfer, IsBridgeTxParams } from '@avalabs/bridge-unified'; -import { BITCOIN_NETWORK, ChainId } from '@avalabs/core-chains-sdk'; +import { BridgeTransaction } from '@avalabs/core-bridge-sdk'; +import { BridgeTransfer } from '@avalabs/bridge-unified'; + import { TxHistoryItem } from '@src/background/services/history/models'; -import { isEthereumChainId } from '@src/background/services/network/utils/isEthereumNetwork'; -import { useUnifiedBridgeContext } from '@src/contexts/UnifiedBridgeProvider'; -import { isUnifiedBridgeTransfer } from '@src/pages/Bridge/utils/isUnifiedBridgeTransfer'; -import { - ETHEREUM_ADDRESS, - isPendingBridgeTransaction, -} from '@src/utils/bridgeTransactionUtils'; -import { caipToChainId } from '@src/utils/caipConversion'; -import { getBridgedAssetSymbol } from '@src/utils/bridge/getBridgedAssetSymbol'; +import { isPendingBridgeTransaction } from '@src/utils/bridgeTransactionUtils'; +import { useNetworkContext } from '@src/contexts/NetworkProvider'; export function useBlockchainNames( item: TxHistoryItem | BridgeTransaction | BridgeTransfer ) { - const { isBridgeTx } = useUnifiedBridgeContext(); const pending = isPendingBridgeTransaction(item); + const { getNetwork } = useNetworkContext(); if (pending) { return { @@ -33,85 +26,25 @@ export function useBlockchainNames( }; } - const isToAvalanche = isTxToAvalanche(item); - const txBlockchain = getTxBlockchain(item, isBridgeTx); + if (!item.bridgeAnalysis.isBridgeTx) { + return { + sourceBlockchain: undefined, + targetBlockchain: undefined, + }; + } + + const { sourceChainId, targetChainId } = item.bridgeAnalysis; return { - sourceBlockchain: isToAvalanche ? txBlockchain : 'Avalanche', - targetBlockchain: isToAvalanche ? 'Avalanche' : txBlockchain, + sourceBlockchain: sourceChainId + ? getNetwork(sourceChainId)?.chainName ?? sourceChainId + : undefined, + targetBlockchain: targetChainId + ? getNetwork(targetChainId)?.chainName ?? targetChainId + : undefined, }; } function titleCase(name: string) { return (name[0] || '').toUpperCase() + name.slice(1); } - -function isBridgeTransaction( - tx: TxHistoryItem | BridgeTransaction | BridgeTransfer -): tx is BridgeTransaction | BridgeTransfer { - return 'targetChain' in tx; -} - -function isTxToAvalanche( - tx: TxHistoryItem | BridgeTransaction | BridgeTransfer -): boolean { - if (isBridgeTransaction(tx)) { - if (isUnifiedBridgeTransfer(tx)) { - return ( - caipToChainId(tx.targetChain.chainId) === ChainId.ETHEREUM_HOMESTEAD || - caipToChainId(tx.targetChain.chainId) === ChainId.ETHEREUM_TEST_GOERLY - ); - } - - return tx.targetChain === Blockchain.AVALANCHE; - } else if (tx.from === ETHEREUM_ADDRESS) { - return true; - } else if (tx.isOutgoing) { - return ( - tx.chainId !== ChainId.AVALANCHE_MAINNET_ID.toString() && - tx.chainId !== ChainId.AVALANCHE_TESTNET_ID.toString() - ); - } else if (tx.isIncoming) { - return ( - tx.chainId === ChainId.AVALANCHE_MAINNET_ID.toString() || - tx.chainId === ChainId.AVALANCHE_TESTNET_ID.toString() - ); - } - return false; -} - -function getTxBlockchain( - tx: TxHistoryItem | BridgeTransaction | BridgeTransfer, - isBridgeTx: (txInfo: IsBridgeTxParams) => boolean -) { - const symbol = isBridgeTransaction(tx) - ? getBridgedAssetSymbol(tx) - : tx.tokens?.[0]?.symbol; - const ethereum = 'Ethereum'; - const bitcoin = 'Bitcoin'; - - if (symbol === BITCOIN_NETWORK.networkToken.symbol) { - return bitcoin; - } - - if (!isBridgeTransaction(tx)) { - if (isEthereumChainId(Number(tx.chainId))) { - return ethereum; - } - - if (isBridgeTx(tx)) { - return ethereum; - } - } - - const symbolPostfix = symbol?.split('.')[1]; - - switch (symbolPostfix) { - case 'e': - return ethereum; - case 'b': - return bitcoin; - default: - return 'N/A'; - } -} diff --git a/yarn.lock b/yarn.lock index 221fc0ab..e0f45a08 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-ictt-configs-20241002113306": - version "0.0.0-feat-ictt-configs-20241002113306" - resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-ictt-configs-20241002113306.tgz#15ef3581189fdcf0209c9286ed1e11fce8ab6f51" - integrity sha512-u1t4vV1cc5LH8v7ddraTOJkt1sKmopI7t81tCo5rKMc96eBOVjQjTniXCxG0c1E/BID0QgQcL5rTC1e861C/YA== +"@avalabs/bridge-unified@0.0.0-feat-ictt-configs-20241008120400": + version "0.0.0-feat-ictt-configs-20241008120400" + resolved "https://registry.yarnpkg.com/@avalabs/bridge-unified/-/bridge-unified-0.0.0-feat-ictt-configs-20241008120400.tgz#6451a8db94a6d7c320f243e83e74f6293b87ac9a" + integrity sha512-lUfC4n/R4F6ZqkI4mx5paf3yPNIEecBfZfrkJCMjXmy8I0lhiPQbjUC2/wraX/72W64Y/dyp0AIeiLfG9/RLiA== dependencies: "@noble/hashes" "1.3.3" "@scure/base" "1.1.5"