From 8a6ef664cca5055fdeac1a8ef5817c74646a6ab2 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:05:04 -0500 Subject: [PATCH 01/94] chore: WIP bridge controller, most tests passing --- .gitignore | 3 + README.md | 1 + packages/bridge-controller/CHANGELOG.md | 8 + packages/bridge-controller/README.md | 15 + packages/bridge-controller/jest.config.js | 26 + packages/bridge-controller/package.json | 86 ++ .../src/bridge-controller.test.ts | 700 ++++++++++++++ .../src/bridge-controller.ts | 363 +++++++ .../bridge-controller/src/constants/chains.ts | 158 ++++ .../bridge-controller/src/constants/index.ts | 81 ++ .../bridge-controller/src/constants/swaps.ts | 1 + .../bridge-controller/src/constants/tokens.ts | 144 +++ .../src/test/mock-quotes-erc20-erc20.json | 248 +++++ .../src/test/mock-quotes-erc20-native.json | 894 ++++++++++++++++++ .../test/mock-quotes-native-erc20-eth.json | 258 +++++ .../src/test/mock-quotes-native-erc20.json | 294 ++++++ .../bridge-controller/src/test/provider.ts | 7 + packages/bridge-controller/src/test/utils.ts | 3 + packages/bridge-controller/src/types.ts | 263 ++++++ .../src/utils/balance.test.ts | 138 +++ .../bridge-controller/src/utils/balance.ts | 56 ++ .../bridge-controller/src/utils/fetch.test.ts | 322 +++++++ packages/bridge-controller/src/utils/fetch.ts | 169 ++++ .../bridge-controller/src/utils/index.test.ts | 40 + packages/bridge-controller/src/utils/index.ts | 70 ++ packages/bridge-controller/src/utils/quote.ts | 36 + .../bridge-controller/src/utils/validators.ts | 143 +++ .../bridge-controller/tsconfig.build.json | 19 + packages/bridge-controller/tsconfig.json | 15 + packages/bridge-controller/typedoc.json | 7 + yarn.lock | 2 +- 31 files changed, 4569 insertions(+), 1 deletion(-) create mode 100644 packages/bridge-controller/CHANGELOG.md create mode 100644 packages/bridge-controller/README.md create mode 100644 packages/bridge-controller/jest.config.js create mode 100644 packages/bridge-controller/package.json create mode 100644 packages/bridge-controller/src/bridge-controller.test.ts create mode 100644 packages/bridge-controller/src/bridge-controller.ts create mode 100644 packages/bridge-controller/src/constants/chains.ts create mode 100644 packages/bridge-controller/src/constants/index.ts create mode 100644 packages/bridge-controller/src/constants/swaps.ts create mode 100644 packages/bridge-controller/src/constants/tokens.ts create mode 100644 packages/bridge-controller/src/test/mock-quotes-erc20-erc20.json create mode 100644 packages/bridge-controller/src/test/mock-quotes-erc20-native.json create mode 100644 packages/bridge-controller/src/test/mock-quotes-native-erc20-eth.json create mode 100644 packages/bridge-controller/src/test/mock-quotes-native-erc20.json create mode 100644 packages/bridge-controller/src/test/provider.ts create mode 100644 packages/bridge-controller/src/test/utils.ts create mode 100644 packages/bridge-controller/src/types.ts create mode 100644 packages/bridge-controller/src/utils/balance.test.ts create mode 100644 packages/bridge-controller/src/utils/balance.ts create mode 100644 packages/bridge-controller/src/utils/fetch.test.ts create mode 100644 packages/bridge-controller/src/utils/fetch.ts create mode 100644 packages/bridge-controller/src/utils/index.test.ts create mode 100644 packages/bridge-controller/src/utils/index.ts create mode 100644 packages/bridge-controller/src/utils/quote.ts create mode 100644 packages/bridge-controller/src/utils/validators.ts create mode 100644 packages/bridge-controller/tsconfig.build.json create mode 100644 packages/bridge-controller/tsconfig.json create mode 100644 packages/bridge-controller/typedoc.json diff --git a/.gitignore b/.gitignore index 5043addaa41..c7251737c86 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ scripts/coverage # typescript packages/*/*.tsbuildinfo + +# vscode +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 9645d28513e..be9f2d7225e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/approval-controller`](packages/approval-controller) - [`@metamask/assets-controllers`](packages/assets-controllers) - [`@metamask/base-controller`](packages/base-controller) +- [`@metamask/bridge-controller`](packages/bridge-controller) - [`@metamask/build-utils`](packages/build-utils) - [`@metamask/composable-controller`](packages/composable-controller) - [`@metamask/controller-utils`](packages/controller-utils) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md new file mode 100644 index 00000000000..11bddf32c5b --- /dev/null +++ b/packages/bridge-controller/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] diff --git a/packages/bridge-controller/README.md b/packages/bridge-controller/README.md new file mode 100644 index 00000000000..adb050aedec --- /dev/null +++ b/packages/bridge-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/bridge-controller` + +Manages bridge-related quote fetching functionality for MetaMask. + +## Installation + +`yarn add @metamask/bridge-controller` + +or + +`npm install @metamask/bridge-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js new file mode 100644 index 00000000000..d5f34b1825e --- /dev/null +++ b/packages/bridge-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 91.07, + functions: 97.51, + lines: 98.12, + statements: 98.03, + }, + }, +}); diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json new file mode 100644 index 00000000000..d6453225034 --- /dev/null +++ b/packages/bridge-controller/package.json @@ -0,0 +1,86 @@ +{ + "name": "@metamask/bridge-controller", + "version": "1.0.0", + "description": "Manages bridge-related quote fetching functionality for MetaMask", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/bridge-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/bridge-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/bridge-controller", + "publish:preview": "yarn npm publish --tag preview", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" + }, + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@metamask/base-controller": "^7.0.0", + "@metamask/controller-utils": "^11.4.0", + "@metamask/metamask-eth-abis": "^3.1.1", + "@metamask/network-controller": "^22.1.1", + "@metamask/polling-controller": "^12.0.1", + "@metamask/transaction-controller": "^43.0.0", + "@metamask/utils": "^10.0.1", + "ethers": "5.7.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eth-json-rpc-provider": "^4.1.6", + "@metamask/json-rpc-engine": "^10.0.0", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "lodash": "^4.17.21", + "nock": "^13.5.4", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/keyring-controller": "^19.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts new file mode 100644 index 00000000000..5ca7dd18523 --- /dev/null +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -0,0 +1,700 @@ +import nock from 'nock'; +import { bigIntToHex } from '@metamask/utils'; +import { BRIDGE_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; +import { CHAIN_IDS } from './constants/chains'; +import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; +import { flushPromises } from './test/utils'; +import * as fetchUtils from './utils/fetch'; +import * as balanceUtils from './utils/balance'; +import mockBridgeQuotesErc20Native from './test/mock-quotes-erc20-native.json'; +import mockBridgeQuotesNativeErc20 from './test/mock-quotes-native-erc20.json'; +import mockBridgeQuotesNativeErc20Eth from './test/mock-quotes-native-erc20-eth.json'; +import BridgeController from './bridge-controller'; +import { BridgeControllerMessenger, QuoteResponse } from './types'; + +const EMPTY_INIT_STATE = { + bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, +}; + +const messengerMock = { + call: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + publish: jest.fn(), +} as unknown as jest.Mocked; + +jest.mock('@ethersproject/contracts', () => { + return { + Contract: jest.fn(() => ({ + allowance: jest.fn(() => '100000000000000000000'), + })), + }; +}); + +jest.mock('@ethersproject/providers', () => { + return { + Web3Provider: jest.fn(), + }; +}); +const getLayer1GasFeeMock = jest.fn(); + +describe('BridgeController', function () { + let bridgeController: BridgeController; + + beforeAll(function () { + bridgeController = new BridgeController({ + messenger: messengerMock, + getLayer1GasFee: getLayer1GasFeeMock, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + + nock(BRIDGE_API_BASE_URL) + .get('/getAllFeatureFlags') + .reply(200, { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 3, + support: true, + chains: { + '10': { + isActiveSrc: true, + isActiveDest: false, + }, + '534352': { + isActiveSrc: true, + isActiveDest: false, + }, + '137': { + isActiveSrc: false, + isActiveDest: true, + }, + '42161': { + isActiveSrc: false, + isActiveDest: true, + }, + }, + }, + 'approval-gas-multiplier': { + '137': 1.1, + '42161': 1.2, + '10': 1.3, + '534352': 1.4, + }, + 'bridge-gas-multiplier': { + '137': 2.1, + '42161': 2.2, + '10': 2.3, + '534352': 2.4, + }, + }); + nock(BRIDGE_API_BASE_URL) + .get('/getTokens?chainId=10') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + aggregators: ['lifl', 'socket'], + }, + { + address: '0x1291478912', + symbol: 'DEF', + decimals: 16, + }, + ]); + nock(SWAPS_API_V2_BASE_URL) + .get('/networks/10/topAssets') + .reply(200, [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + }, + ]); + bridgeController.resetState(); + }); + + it('constructor should setup correctly', function () { + expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + }); + + it('setBridgeFeatureFlags should fetch and set the bridge feature flags', async function () { + const expectedFeatureFlagsResponse = { + extensionConfig: { + maxRefreshCount: 3, + refreshRate: 3, + support: true, + chains: { + [CHAIN_IDS.OPTIMISM]: { isActiveSrc: true, isActiveDest: false }, + [CHAIN_IDS.SCROLL]: { isActiveSrc: true, isActiveDest: false }, + [CHAIN_IDS.POLYGON]: { isActiveSrc: false, isActiveDest: true }, + [CHAIN_IDS.ARBITRUM]: { isActiveSrc: false, isActiveDest: true }, + }, + }, + }; + expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + + const setIntervalLengthSpy = jest.spyOn( + bridgeController, + 'setIntervalLength', + ); + + await bridgeController.setBridgeFeatureFlags(); + expect(bridgeController.state.bridgeState.bridgeFeatureFlags).toStrictEqual( + expectedFeatureFlagsResponse, + ); + expect(setIntervalLengthSpy).toHaveBeenCalledTimes(1); + expect(setIntervalLengthSpy).toHaveBeenCalledWith(3); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + bridgeFeatureFlags: expectedFeatureFlagsResponse, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + }); + + it('updateBridgeQuoteRequestParams should update the quoteRequest state', function () { + bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: 10, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + destChainId: undefined, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: undefined, + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + srcTokenAmount: '100000', + destTokenAddress: '0x123', + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + + bridgeController.updateBridgeQuoteRequestParams({ + srcTokenAddress: '0x2ABC', + }); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x2ABC', + walletAddress: undefined, + }); + + bridgeController.resetState(); + expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + }); + }); + + it('updateBridgeQuoteRequestParams should trigger quote polling if request is valid', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(true); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(mockBridgeQuotesNativeErc20Eth as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ] as never); + }, 10000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementationOnce(async () => { + return await new Promise((_, reject) => { + return setTimeout(() => { + reject(new Error('Network error')); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: expect.anything(), + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: false, + }, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: false, + }, + expect.any(AbortSignal), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + undefined, + ); + + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + }), + ); + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched ?? 0; + expect(firstFetchTime).toBeGreaterThan(0); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: [ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ], + quotesLoadingStatus: 1, + quoteFetchError: undefined, + quotesRefreshCount: 2, + }), + ); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(secondFetchTime).toBeGreaterThan(firstFetchTime); + + // After 3nd fetch throws an error + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: false }, + quotes: [ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ], + quotesLoadingStatus: 2, + quoteFetchError: 'Network error', + quotesRefreshCount: 3, + }), + ); + secondFetchTime && + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toBeGreaterThan(secondFetchTime); + + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); + }); + + it('updateBridgeQuoteRequestParams should only poll once if insufficientBal=true', async function () { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(false); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(mockBridgeQuotesNativeErc20Eth as never); + }, 5000); + }); + }); + + fetchBridgeQuotesSpy.mockImplementation(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve([ + ...mockBridgeQuotesNativeErc20Eth, + ...mockBridgeQuotesNativeErc20Eth, + ] as never); + }, 10000); + }); + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: expect.anything(), + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: true, + }, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesInitialLoadTime: undefined, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: true, + }, + expect.any(AbortSignal), + ); + expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( + undefined, + ); + + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + quotesInitialLoadTime: 11000, + }), + ); + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched ?? 0; + expect(firstFetchTime).toBeGreaterThan(0); + + // After 2nd fetch + jest.advanceTimersByTime(50000); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + quotesInitialLoadTime: 11000, + }), + ); + const secondFetchTime = + bridgeController.state.bridgeState.quotesLastFetched; + expect(secondFetchTime).toStrictEqual(firstFetchTime); + expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); + }); + + it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () { + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + bridgeController.updateBridgeQuoteRequestParams({ + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + }); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).not.toHaveBeenCalled(); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { + srcChainId: 1, + slippage: 0.5, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + walletAddress: undefined, + destChainId: 10, + destTokenAddress: '0x123', + }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + }); + + describe('getBridgeERC20Allowance', () => { + it('should return the atomic allowance of the ERC20 token contract', async () => { + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const allowance = await bridgeController.getBridgeERC20Allowance( + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + '0xa', + ); + expect(allowance).toBe('100000000000000000000'); + }); + }); + + it.each([ + [ + 'should append l1GasFees if srcChain is 10 and srcToken is erc20', + mockBridgeQuotesErc20Native, + bigIntToHex(BigInt('2608710388388') * 2n), + 12, + ], + [ + 'should append l1GasFees if srcChain is 10 and srcToken is native', + mockBridgeQuotesNativeErc20, + bigIntToHex(BigInt('2608710388388') * 2n), + 2, + ], + [ + 'should not append l1GasFees if srcChain is not 10', + mockBridgeQuotesNativeErc20Eth, + undefined, + 0, + ], + ])( + 'updateBridgeQuoteRequestParams: %s', + async ( + _: string, + quoteResponse: QuoteResponse[], + l1GasFeesInHexWei: string, + getLayer1GasFeeMockCallCount: number, + ) => { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(false); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + getLayer1GasFeeMock.mockResolvedValue('0x25F63418AA4'); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementationOnce(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(quoteResponse as never); + }, 1000); + }); + }); + + const quoteParams = { + srcChainId: 10, + destChainId: 1, + srcTokenAddress: '0x4200000000000000000000000000000000000006', + destTokenAddress: '0x0000000000000000000000000000000000000000', + srcTokenAmount: '991250000000000000', + }; + const quoteRequest = { + ...quoteParams, + slippage: 0.5, + walletAddress: '0x123', + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: expect.anything(), + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: true, + }, + }); + + expect(bridgeController.state.bridgeState).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + }), + ); + + // // Loading state + jest.advanceTimersByTime(500); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( + { + ...quoteRequest, + insufficientBal: true, + }, + expect.any(AbortSignal), + ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toStrictEqual(undefined); + + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: [], + quotesLoadingStatus: 0, + }), + ); + + // After first fetch + jest.advanceTimersByTime(1500); + await flushPromises(); + const { quotes } = bridgeController.state.bridgeState; + expect(bridgeController.state.bridgeState).toEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + }), + ); + quotes.forEach((quote) => { + const expectedQuote = l1GasFeesInHexWei + ? { ...quote, l1GasFeesInHexWei } + : quote; + expect(quote).toStrictEqual(expectedQuote); + }); + + const firstFetchTime = + bridgeController.state.bridgeState.quotesLastFetched ?? 0; + expect(firstFetchTime).toBeGreaterThan(0); + + expect(getLayer1GasFeeMock).toHaveBeenCalledTimes( + getLayer1GasFeeMockCallCount, + ); + }, + ); +}); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts new file mode 100644 index 00000000000..e2a836406e4 --- /dev/null +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -0,0 +1,363 @@ +import { add0x, Hex, numberToHex } from '@metamask/utils'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { NetworkClientId } from '@metamask/network-controller'; +import { StateMetadata } from '@metamask/base-controller'; +import { Contract } from '@ethersproject/contracts'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import { Web3Provider } from '@ethersproject/providers'; +import { BigNumber } from '@ethersproject/bignumber'; +import { TransactionParams } from '@metamask/transaction-controller'; +import type { ChainId } from '@metamask/controller-utils'; +import { + fetchBridgeFeatureFlags, + fetchBridgeQuotes, +} from './utils/fetch'; +import { + sumHexes, +} from './utils'; +import { + type L1GasFees, + type QuoteRequest, + type QuoteResponse, + type TxData, + type BridgeControllerState, + BridgeFeatureFlagsKey, + RequestStatus, +} from './types'; +import { isValidQuoteRequest } from './utils/quote'; +import { hasSufficientBalance } from './utils/balance'; +import { CHAIN_IDS } from './constants/chains'; +import { REFRESH_INTERVAL_MS } from './constants'; +import { + BRIDGE_CONTROLLER_NAME, + DEFAULT_BRIDGE_CONTROLLER_STATE, + METABRIDGE_CHAIN_TO_ADDRESS_MAP, +} from './constants'; +import type { BridgeControllerMessenger } from './types'; + +const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { + bridgeState: { + persist: false, + anonymous: false, + }, +}; + +const RESET_STATE_ABORT_MESSAGE = 'Reset controller state'; + +/** The input to start polling for the {@link BridgeController} */ +type BridgePollingInput = { + networkClientId: NetworkClientId; + updatedQuoteRequest: QuoteRequest; +}; + +export default class BridgeController extends StaticIntervalPollingController()< + typeof BRIDGE_CONTROLLER_NAME, + { bridgeState: BridgeControllerState }, + BridgeControllerMessenger +> { + #abortController: AbortController | undefined; + + #quotesFirstFetched: number | undefined; + + #getLayer1GasFee: (params: { + transactionParams: TransactionParams; + chainId: ChainId; + }) => Promise; + + constructor({ + messenger, + getLayer1GasFee, + }: { + messenger: BridgeControllerMessenger; + getLayer1GasFee: (params: { + transactionParams: TransactionParams; + chainId: ChainId; + }) => Promise; + }) { + super({ + name: BRIDGE_CONTROLLER_NAME, + metadata, + messenger, + state: { + bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, + }, + }); + + this.setIntervalLength(REFRESH_INTERVAL_MS); + + this.#abortController = new AbortController(); + // Register action handlers + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, + this.setBridgeFeatureFlags.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:updateBridgeQuoteRequestParams`, + this.updateBridgeQuoteRequestParams.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:resetState`, + this.resetState.bind(this), + ); + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:getBridgeERC20Allowance`, + this.getBridgeERC20Allowance.bind(this), + ); + + this.#getLayer1GasFee = getLayer1GasFee; + } + + _executePoll = async (pollingInput: BridgePollingInput) => { + await this.#fetchBridgeQuotes(pollingInput); + }; + + updateBridgeQuoteRequestParams = async ( + paramsToUpdate: Partial, + ) => { + this.stopAllPolling(); + this.#abortController?.abort('Quote request updated'); + + const { bridgeState } = this.state; + const updatedQuoteRequest = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, + ...paramsToUpdate, + }; + + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quoteRequest: updatedQuoteRequest, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, + quotesLoadingStatus: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + quoteFetchError: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError, + quotesRefreshCount: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount, + quotesInitialLoadTime: + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesInitialLoadTime, + }; + }); + + if (isValidQuoteRequest(updatedQuoteRequest)) { + this.#quotesFirstFetched = Date.now(); + const walletAddress = this.#getSelectedAccount().address; + const srcChainIdInHex = numberToHex(updatedQuoteRequest.srcChainId); + + const insufficientBal = + paramsToUpdate.insufficientBal || + !(await this.#hasSufficientBalance(updatedQuoteRequest)); + + const networkClientId = this.#getSelectedNetworkClientId(srcChainIdInHex); + this.startPolling({ + networkClientId, + updatedQuoteRequest: { + ...updatedQuoteRequest, + walletAddress, + insufficientBal, + }, + }); + } + }; + + #hasSufficientBalance = async (quoteRequest: QuoteRequest) => { + const walletAddress = this.#getSelectedAccount().address; + const srcChainIdInHex = numberToHex(quoteRequest.srcChainId); + const provider = this.#getSelectedNetworkClient()?.provider; + + return ( + provider && + (await hasSufficientBalance( + provider, + walletAddress, + quoteRequest.srcTokenAddress, + quoteRequest.srcTokenAmount, + srcChainIdInHex, + )) + ); + }; + + resetState = () => { + this.stopAllPolling(); + this.#abortController?.abort(RESET_STATE_ABORT_MESSAGE); + + this.update((_state) => { + _state.bridgeState = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE, + quotes: [], + bridgeFeatureFlags: _state.bridgeState.bridgeFeatureFlags, + }; + }); + }; + + setBridgeFeatureFlags = async () => { + const { bridgeState } = this.state; + const bridgeFeatureFlags = await fetchBridgeFeatureFlags(); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; + }); + this.setIntervalLength( + bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].refreshRate, + ); + }; + + #fetchBridgeQuotes = async ({ + networkClientId: _networkClientId, + updatedQuoteRequest, + }: BridgePollingInput) => { + this.#abortController?.abort('New quote request'); + this.#abortController = new AbortController(); + if (updatedQuoteRequest.srcChainId === updatedQuoteRequest.destChainId) { + return; + } + const { bridgeState } = this.state; + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quotesLoadingStatus: RequestStatus.LOADING, + quoteRequest: updatedQuoteRequest, + quoteFetchError: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError, + }; + }); + + try { + const quotes = await fetchBridgeQuotes( + updatedQuoteRequest, + this.#abortController.signal, + ); + + const quotesWithL1GasFees = await this.#appendL1GasFees(quotes); + + this.update((_state) => { + _state.bridgeState = { + ..._state.bridgeState, + quotes: quotesWithL1GasFees, + quotesLoadingStatus: RequestStatus.FETCHED, + }; + }); + } catch (error) { + const isAbortError = (error as Error).name === 'AbortError'; + const isAbortedDueToReset = error === RESET_STATE_ABORT_MESSAGE; + if (isAbortedDueToReset || isAbortError) { + return; + } + + this.update((_state) => { + _state.bridgeState = { + ...bridgeState, + quoteFetchError: + error instanceof Error ? error.message : 'Unknown error', + quotesLoadingStatus: RequestStatus.ERROR, + }; + }); + console.log('Failed to fetch bridge quotes', error); + } finally { + const { maxRefreshCount } = + bridgeState.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG]; + + const updatedQuotesRefreshCount = bridgeState.quotesRefreshCount + 1; + // Stop polling if the maximum number of refreshes has been reached + if ( + updatedQuoteRequest.insufficientBal || + (!updatedQuoteRequest.insufficientBal && + updatedQuotesRefreshCount >= maxRefreshCount) + ) { + this.stopAllPolling(); + } + + // Update quote fetching stats + const quotesLastFetched = Date.now(); + this.update((_state) => { + _state.bridgeState = { + ..._state.bridgeState, + quotesInitialLoadTime: + updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched + ? quotesLastFetched - this.#quotesFirstFetched + : bridgeState.quotesInitialLoadTime, + quotesLastFetched, + quotesRefreshCount: updatedQuotesRefreshCount, + }; + }); + } + }; + + #appendL1GasFees = async ( + quotes: QuoteResponse[], + ): Promise<(QuoteResponse & L1GasFees)[]> => { + return await Promise.all( + quotes.map(async (quoteResponse) => { + const { quote, trade, approval } = quoteResponse; + const chainId = numberToHex(quote.srcChainId) as ChainId; + if ( + [CHAIN_IDS.OPTIMISM.toString(), CHAIN_IDS.BASE.toString()].includes( + chainId, + ) + ) { + const getTxParams = (txData: TxData) => ({ + from: txData.from, + to: txData.to, + value: txData.value, + data: txData.data, + gasLimit: txData.gasLimit?.toString(), + }); + const approvalL1GasFees = approval + ? await this.#getLayer1GasFee({ + transactionParams: getTxParams(approval), + chainId, + }) + : '0'; + const tradeL1GasFees = await this.#getLayer1GasFee({ + transactionParams: getTxParams(trade), + chainId, + }); + return { + ...quoteResponse, + l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees), + }; + } + return quoteResponse; + }), + ); + }; + + #getSelectedAccount() { + return this.messagingSystem.call('AccountsController:getSelectedAccount'); + } + + #getSelectedNetworkClient() { + return this.messagingSystem.call( + 'NetworkController:getSelectedNetworkClient', + ); + } + + #getSelectedNetworkClientId(chainId: Hex) { + return this.messagingSystem.call( + 'NetworkController:findNetworkClientIdByChainId', + chainId, + ); + } + + /** + * + * @param contractAddress - The address of the ERC20 token contract + * @param chainId - The hex chain ID of the bridge network + * @returns The atomic allowance of the ERC20 token contract + */ + getBridgeERC20Allowance = async ( + contractAddress: string, + chainId: Hex, + ): Promise => { + const provider = this.#getSelectedNetworkClient()?.provider; + if (!provider) { + throw new Error('No provider found'); + } + + const web3Provider = new Web3Provider(provider); + const contract = new Contract(contractAddress, abiERC20, web3Provider); + const { address: walletAddress } = this.#getSelectedAccount(); + const allowance = await contract.allowance( + walletAddress, + METABRIDGE_CHAIN_TO_ADDRESS_MAP[chainId], + ); + return BigNumber.from(allowance).toString(); + }; +} diff --git a/packages/bridge-controller/src/constants/chains.ts b/packages/bridge-controller/src/constants/chains.ts new file mode 100644 index 00000000000..af16bcbad1e --- /dev/null +++ b/packages/bridge-controller/src/constants/chains.ts @@ -0,0 +1,158 @@ +/** + * An object containing all of the chain ids for networks both built in and + * those that we have added custom code to support our feature set. + */ +export const CHAIN_IDS = { + MAINNET: '0x1', + GOERLI: '0x5', + LOCALHOST: '0x539', + BSC: '0x38', + BSC_TESTNET: '0x61', + OPTIMISM: '0xa', + OPTIMISM_TESTNET: '0xaa37dc', + OPTIMISM_GOERLI: '0x1a4', + BASE: '0x2105', + BASE_TESTNET: '0x14a33', + OPBNB: '0xcc', + OPBNB_TESTNET: '0x15eb', + POLYGON: '0x89', + POLYGON_TESTNET: '0x13881', + AVALANCHE: '0xa86a', + AVALANCHE_TESTNET: '0xa869', + FANTOM: '0xfa', + FANTOM_TESTNET: '0xfa2', + CELO: '0xa4ec', + ARBITRUM: '0xa4b1', + HARMONY: '0x63564c40', + PALM: '0x2a15c308d', + SEPOLIA: '0xaa36a7', + HOLESKY: '0x4268', + LINEA_GOERLI: '0xe704', + LINEA_SEPOLIA: '0xe705', + AMOY: '0x13882', + BASE_SEPOLIA: '0x14a34', + BLAST_SEPOLIA: '0xa0c71fd', + OPTIMISM_SEPOLIA: '0xaa37dc', + PALM_TESTNET: '0x2a15c3083', + CELO_TESTNET: '0xaef3', + ZK_SYNC_ERA_TESTNET: '0x12c', + MANTA_SEPOLIA: '0x138b', + UNICHAIN_SEPOLIA: '0x515', + LINEA_MAINNET: '0xe708', + AURORA: '0x4e454152', + MOONBEAM: '0x504', + MOONBEAM_TESTNET: '0x507', + MOONRIVER: '0x505', + CRONOS: '0x19', + GNOSIS: '0x64', + ZKSYNC_ERA: '0x144', + TEST_ETH: '0x539', + ARBITRUM_GOERLI: '0x66eed', + BLAST: '0x13e31', + FILECOIN: '0x13a', + POLYGON_ZKEVM: '0x44d', + SCROLL: '0x82750', + SCROLL_SEPOLIA: '0x8274f', + WETHIO: '0x4e', + CHZ: '0x15b38', + NUMBERS: '0x290b', + SEI: '0x531', + APE_TESTNET: '0x8157', + APE_MAINNET: '0x8173', + BERACHAIN: '0x138d5', + METACHAIN_ONE: '0x1b6e6', + ARBITRUM_SEPOLIA: '0x66eee', + NEAR: '0x18d', + NEAR_TESTNET: '0x18e', + B3: '0x208d', + B3_TESTNET: '0x7c9', + GRAVITY_ALPHA_MAINNET: '0x659', + GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', + LISK: '0x46f', + LISK_SEPOLIA: '0x106a', + INK_SEPOLIA: '0xba5eD', + INK: '0xdef1', + MODE_SEPOLIA: '0x397', + MODE: '0x868b', +} as const; + +export const NETWORK_TYPES = { + GOERLI: 'goerli', + LOCALHOST: 'localhost', + MAINNET: 'mainnet', + RPC: 'rpc', + SEPOLIA: 'sepolia', + LINEA_GOERLI: 'linea-goerli', + LINEA_SEPOLIA: 'linea-sepolia', + LINEA_MAINNET: 'linea-mainnet', +} as const; + +export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet'; +export const GOERLI_DISPLAY_NAME = 'Goerli'; +export const SEPOLIA_DISPLAY_NAME = 'Sepolia'; +export const LINEA_GOERLI_DISPLAY_NAME = 'Linea Goerli'; +export const LINEA_SEPOLIA_DISPLAY_NAME = 'Linea Sepolia'; +export const LINEA_MAINNET_DISPLAY_NAME = 'Linea Mainnet'; +export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545'; +export const BSC_DISPLAY_NAME = 'Binance Smart Chain'; +export const POLYGON_DISPLAY_NAME = 'Polygon'; +export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain'; +export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One'; +export const BNB_DISPLAY_NAME = 'BNB Chain'; +export const OPTIMISM_DISPLAY_NAME = 'OP Mainnet'; +export const FANTOM_DISPLAY_NAME = 'Fantom Opera'; +export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0'; +export const PALM_DISPLAY_NAME = 'Palm'; +export const CELO_DISPLAY_NAME = 'Celo Mainnet'; +export const GNOSIS_DISPLAY_NAME = 'Gnosis'; +export const ZK_SYNC_ERA_DISPLAY_NAME = 'zkSync Era Mainnet'; +export const BASE_DISPLAY_NAME = 'Base Mainnet'; +export const AURORA_DISPLAY_NAME = 'Aurora Mainnet'; +export const CRONOS_DISPLAY_NAME = 'Cronos'; +export const POLYGON_ZKEVM_DISPLAY_NAME = 'Polygon zkEVM'; +export const MOONBEAM_DISPLAY_NAME = 'Moonbeam'; +export const MOONRIVER_DISPLAY_NAME = 'Moonriver'; +export const SCROLL_DISPLAY_NAME = 'Scroll'; +export const SCROLL_SEPOLIA_DISPLAY_NAME = 'Scroll Sepolia'; +export const OP_BNB_DISPLAY_NAME = 'opBNB'; +export const BERACHAIN_DISPLAY_NAME = 'Berachain Artio'; +export const METACHAIN_ONE_DISPLAY_NAME = 'Metachain One Mainnet'; +export const LISK_DISPLAY_NAME = 'Lisk'; +export const LISK_SEPOLIA_DISPLAY_NAME = 'Lisk Sepolia'; +export const INK_SEPOLIA_DISPLAY_NAME = 'Ink Sepolia'; +export const INK_DISPLAY_NAME = 'Ink Mainnet'; +export const SONEIUM_DISPLAY_NAME = 'Soneium Mainnet'; +export const MODE_SEPOLIA_DISPLAY_NAME = 'Mode Sepolia'; +export const MODE_DISPLAY_NAME = 'Mode Mainnet'; + +export const NETWORK_TO_NAME_MAP = { + [NETWORK_TYPES.GOERLI]: GOERLI_DISPLAY_NAME, + [NETWORK_TYPES.MAINNET]: MAINNET_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_GOERLI]: LINEA_GOERLI_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_SEPOLIA]: LINEA_SEPOLIA_DISPLAY_NAME, + [NETWORK_TYPES.LINEA_MAINNET]: LINEA_MAINNET_DISPLAY_NAME, + [NETWORK_TYPES.LOCALHOST]: LOCALHOST_DISPLAY_NAME, + [NETWORK_TYPES.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DISPLAY_NAME, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DISPLAY_NAME, + [CHAIN_IDS.BSC]: BSC_DISPLAY_NAME, + [CHAIN_IDS.BASE]: BASE_DISPLAY_NAME, + [CHAIN_IDS.GOERLI]: GOERLI_DISPLAY_NAME, + [CHAIN_IDS.MAINNET]: MAINNET_DISPLAY_NAME, + [CHAIN_IDS.LINEA_GOERLI]: LINEA_GOERLI_DISPLAY_NAME, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_MAINNET_DISPLAY_NAME, + [CHAIN_IDS.LINEA_SEPOLIA]: LINEA_SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.LOCALHOST]: LOCALHOST_DISPLAY_NAME, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DISPLAY_NAME, + [CHAIN_IDS.POLYGON]: POLYGON_DISPLAY_NAME, + [CHAIN_IDS.SCROLL]: SCROLL_DISPLAY_NAME, + [CHAIN_IDS.SCROLL_SEPOLIA]: SCROLL_SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.SEPOLIA]: SEPOLIA_DISPLAY_NAME, + [CHAIN_IDS.OPBNB]: OP_BNB_DISPLAY_NAME, + [CHAIN_IDS.ZKSYNC_ERA]: ZK_SYNC_ERA_DISPLAY_NAME, + [CHAIN_IDS.BERACHAIN]: BERACHAIN_DISPLAY_NAME, + [CHAIN_IDS.METACHAIN_ONE]: METACHAIN_ONE_DISPLAY_NAME, + [CHAIN_IDS.LISK]: LISK_DISPLAY_NAME, + [CHAIN_IDS.LISK_SEPOLIA]: LISK_SEPOLIA_DISPLAY_NAME, +} as const; \ No newline at end of file diff --git a/packages/bridge-controller/src/constants/index.ts b/packages/bridge-controller/src/constants/index.ts new file mode 100644 index 00000000000..2d9e58a3086 --- /dev/null +++ b/packages/bridge-controller/src/constants/index.ts @@ -0,0 +1,81 @@ +import { zeroAddress } from 'ethereumjs-util'; +import type { Hex } from '@metamask/utils'; +import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './chains'; +import { BridgeFeatureFlagsKey, BridgeControllerState } from '../types'; + +// TODO read from feature flags +export const ALLOWED_BRIDGE_CHAIN_IDS = [ + CHAIN_IDS.MAINNET, + CHAIN_IDS.BSC, + CHAIN_IDS.POLYGON, + CHAIN_IDS.ZKSYNC_ERA, + CHAIN_IDS.AVALANCHE, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.ARBITRUM, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.BASE, +]; + +export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; + +export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; +export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; +export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS + ? BRIDGE_DEV_API_BASE_URL + : BRIDGE_PROD_API_BASE_URL; + +export const BRIDGE_CLIENT_ID = 'extension'; + +export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; +export const METABRIDGE_ETHEREUM_ADDRESS = + '0x0439e60F02a8900a951603950d8D4527f400C3f1'; +export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour +export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it + +export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; +export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; + +export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< + AllowedBridgeChainIds, + string +> = { + [CHAIN_IDS.MAINNET]: 'Ethereum', + [CHAIN_IDS.LINEA_MAINNET]: 'Linea', + [CHAIN_IDS.POLYGON]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], + [CHAIN_IDS.AVALANCHE]: 'Avalanche', + [CHAIN_IDS.BSC]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], + [CHAIN_IDS.ARBITRUM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], + [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], + [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', + [CHAIN_IDS.BASE]: 'Base', +}; +export const BRIDGE_MM_FEE_RATE = 0.875; +export const REFRESH_INTERVAL_MS = 30 * 1000; +export const DEFAULT_MAX_REFRESH_COUNT = 5; + +export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; +export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, + support: false, + chains: {}, + }, + }, + quoteRequest: { + walletAddress: undefined, + srcTokenAddress: zeroAddress(), + slippage: BRIDGE_DEFAULT_SLIPPAGE, + }, + quotesInitialLoadTime: undefined, + quotes: [], + quotesLastFetched: undefined, + quotesLoadingStatus: undefined, + quoteFetchError: undefined, + quotesRefreshCount: 0, +}; + +export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { + [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, +}; \ No newline at end of file diff --git a/packages/bridge-controller/src/constants/swaps.ts b/packages/bridge-controller/src/constants/swaps.ts new file mode 100644 index 00000000000..f226425bd17 --- /dev/null +++ b/packages/bridge-controller/src/constants/swaps.ts @@ -0,0 +1 @@ +export const SWAPS_API_V2_BASE_URL = 'https://swap.api.cx.metamask.io'; diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts new file mode 100644 index 00000000000..6189be45b0a --- /dev/null +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -0,0 +1,144 @@ +import { CHAIN_IDS } from "./chains"; + +export type SwapsTokenObject = { + /** + * The symbol of token object + */ + symbol: string; + /** + * The name for the network + */ + name: string; + /** + * An address that the metaswap-api recognizes as the default token + */ + address: string; + /** + * Number of digits after decimal point + */ + decimals: number; + /** + * URL for token icon + */ + iconUrl: string; +}; + +const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; + +export const CURRENCY_SYMBOLS = { + ARBITRUM: 'ETH', + AVALANCHE: 'AVAX', + BNB: 'BNB', + BUSD: 'BUSD', + CELO: 'CELO', + DAI: 'DAI', + GNOSIS: 'XDAI', + ETH: 'ETH', + FANTOM: 'FTM', + HARMONY: 'ONE', + PALM: 'PALM', + MATIC: 'MATIC', + POL: 'POL', + TEST_ETH: 'TESTETH', + USDC: 'USDC', + USDT: 'USDT', + WETH: 'WETH', + OPTIMISM: 'ETH', + CRONOS: 'CRO', + GLIMMER: 'GLMR', + MOONRIVER: 'MOVR', + ONE: 'ONE', +} as const; + +export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.ETH, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +}; + +export const BNB_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.BNB, + name: 'Binance Coin', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const MATIC_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.POL, + name: 'Polygon', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const AVAX_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.AVALANCHE, + name: 'Avalanche', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const TEST_ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.TEST_ETH, + name: 'Test Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const GOERLI_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.ETH, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const SEPOLIA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: CURRENCY_SYMBOLS.ETH, + name: 'Ether', + address: DEFAULT_TOKEN_ADDRESS, + decimals: 18, + iconUrl: '', +} as const; + +export const ARBITRUM_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const OPTIMISM_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const ZKSYNC_ERA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const LINEA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +export const BASE_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + ...ETH_SWAPS_TOKEN_OBJECT, +} as const; + +const SWAPS_TESTNET_CHAIN_ID = '0x539'; + +export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { + [CHAIN_IDS.MAINNET]: ETH_SWAPS_TOKEN_OBJECT, + [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.BSC]: BNB_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.POLYGON]: MATIC_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.GOERLI]: GOERLI_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.SEPOLIA]: GOERLI_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.AVALANCHE]: AVAX_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, +} as const; \ No newline at end of file diff --git a/packages/bridge-controller/src/test/mock-quotes-erc20-erc20.json b/packages/bridge-controller/src/test/mock-quotes-erc20-erc20.json new file mode 100644 index 00000000000..8b589aa85e1 --- /dev/null +++ b/packages/bridge-controller/src/test/mock-quotes-erc20-erc20.json @@ -0,0 +1,248 @@ +[ + { + "quote": { + "requestId": "90ae8e69-f03a-4cf6-bab7-ed4e3431eb37", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + }, + "srcTokenAmount": "14000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "13984280", + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["across"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "14000000", + "destAmount": "13984280" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "approval": { + "chainId": 10, + "to": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000d59f80", + "gasLimit": 61865 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x038d7ea4c68000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000890000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000004a0c3540448000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000019d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284792ebcb90000000000000000000000000000000000000000000000000000000000d59f80000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000454000000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c335900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000d55a40000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000067041c47000000000000000000000000000000000000000000000000000000006704704d00000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef765753be7f7a64d5509974b0d678e1e3149b02f42c7402906f9888136205038026f20b3f6df2899044cab41d632bc7a6c35debd40516df85de6f194aeb05b72cb9ea4d5ce0f7c56c91a79536331112f1a846dc641c", + "gasLimit": 287227 + }, + "estimatedProcessingTimeInSeconds": 60 + }, + { + "quote": { + "requestId": "0b6caac9-456d-47e6-8982-1945ae81ae82", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + }, + "srcTokenAmount": "14000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "13800000", + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "chainId": 10, + "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["celercircle"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "cctp", + "displayName": "Circle CCTP", + "icon": "https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "14000000", + "destAmount": "13800000" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "approval": { + "chainId": 10, + "to": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000000000000d59f80", + "gasLimit": 61865 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x038d7ea4c68000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004400000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a500000000000000000000000000000000000000000000000000000000000000890000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000002e4c3540448000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e4b7dfe9d00000000000000000000000000000000000000000000000000000000000d59f8000000000000000000000000000000000000000000000000000000000000000c4000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000138bc5930d51a475e4669db259f69e61ca33803675e76540f062a76af8cbaef4672c9926e56d6a8c29a263de3ee8f734ad760461c448f82fdccdd8c2360fffba1b", + "gasLimit": 343079 + }, + "estimatedProcessingTimeInSeconds": 1560 + } +] diff --git a/packages/bridge-controller/src/test/mock-quotes-erc20-native.json b/packages/bridge-controller/src/test/mock-quotes-erc20-native.json new file mode 100644 index 00000000000..cd4a1963c6f --- /dev/null +++ b/packages/bridge-controller/src/test/mock-quotes-erc20-native.json @@ -0,0 +1,894 @@ +[ + { + "quote": { + "requestId": "a63df72a-75ae-4416-a8ab-aff02596c75c", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "991225000000000000", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["stargate"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "stargate", + "displayName": "StargateV2 (Fast mode)", + "icon": "https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3136", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "991225000000000000" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x1c8598b5db2e", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000564a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003804bdedbea3f94faf8c8fac5ec841251d96cf5e64e8706ada4688877885e5249520000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d0000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000043ccfd60b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000001c8598b5db2e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000759e000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c83dc7c11df600d7293f778cb365d3dfcc1ffa2221cf5447a8f2ea407a97792135d9f585ecb68916479dfa1f071f169cbe1cfec831b5ad01f4e4caa09204e5181c", + "gasLimit": 641446 + }, + "estimatedProcessingTimeInSeconds": 64 + }, + { + "quote": { + "requestId": "aad73198-a64d-4310-b12d-9dcc81c412e2", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "991147696728676903", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["celer"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "celer", + "displayName": "Celer cBridge", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/cbridge.svg" + }, + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "991147696728676903" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000e7bf43c55551b1036e796e7fd3b125d1f9903e2e000000000000000000000000e7bf43c55551b1036e796e7fd3b125d1f9903e2e000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000050f68486970f93a855b27794b8141d32a89a1e0a5ef360034a2f60a4b917c188380000a4b1420000000000000000000000000000000000000600000000000000000dc1a09f859b20002c03873900002777000000000000000000000000000000002d68122053030bf8df41a8bb8c6f0a9de411c7d94eed376b7d91234e1585fd9f77dcf974dd25160d0c2c16c8382d8aa85b0edd429edff19b4d4cdcf50d0a9d4d1c", + "gasLimit": 203352 + }, + "estimatedProcessingTimeInSeconds": 53 + }, + { + "quote": { + "requestId": "6cfd4952-c9b2-4aec-9349-af39c212f84b", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "991112862890876485", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["across"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png" + }, + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "991112862890876485" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000000902340ab8f6a57ef0c43231b98141d32a89a1e0a5ef360034a2f60a4b917c18838420000000000000000000000000000000000000600000000000000000dc1a09f859b20000000a4b100007dd39298f9ad673645ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b710000000000000000000000000000000088d06e7971021eee573a0ab6bc3e22039fc1c5ded5d12c4cf2b6311f47f909e06197aa8b2f647ae78ae33a6ea5d23f7c951c0e1686abecd01d7c796990d56f391c", + "gasLimit": 177423 + }, + "estimatedProcessingTimeInSeconds": 15 + }, + { + "quote": { + "requestId": "2c2ba7d8-3922-4081-9f27-63b7d5cc1986", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + }, + "destChainId": 42161, + "destTokenAmount": "990221346602370184", + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "symbol": "WETH", + "decimals": 18, + "name": "Wrapped ETH", + "coinKey": "WETH", + "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png", + "priceUSD": "3136", + "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["hop"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "hop", + "displayName": "Hop", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/hop.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 10, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3136", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3135.46", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "990221346602370184" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000484ca360ae0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000001168a464edd170000000000000000000000000000000000000000000000000dac6213fc70c84400000000000000000000000000000000000000000000000000000000673a3b080000000000000000000000000000000000000000000000000dac6213fc70c84400000000000000000000000000000000000000000000000000000000673a3b0800000000000000000000000086ca30bef97fb651b8d866d45503684b90cb3312000000000000000000000000710bda329b2a6224e4b44833de30f38e7f81d5640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067997b63db4b9059d22e50750707b46a6d48dfbb32e50d85fc3bff1170ed9ca30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003686f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d0000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000043ccfd60b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000099d00cde1f22e8afd37d7f103ec3c6c1eb835ace46e502ec8c5ab51413e539461b89c0e26892efd1de1cbfe4222b5589e76231080252197507cce4fb72a30b031b", + "gasLimit": 547501 + }, + "estimatedProcessingTimeInSeconds": 24.159 + }, + { + "quote": { + "requestId": "a77bc7b2-e8c8-4463-89db-5dd239d6aacc", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "name": "Wrapped Ether", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/WETH", + "logoURI": "https://media.socket.tech/tokens/all/WETH", + "chainAgnosticId": "ETH" + }, + "srcTokenAmount": "991250000000000000", + "destChainId": 42161, + "destAsset": { + "chainId": 42161, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "destTokenAmount": "991147696728676903", + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "name": "Wrapped Ether", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/WETH", + "logoURI": "https://media.socket.tech/tokens/all/WETH", + "chainAgnosticId": "ETH" + } + } + }, + "bridgeId": "socket", + "bridges": ["celer"], + "steps": [ + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "celer", + "displayName": "Celer", + "icon": "https://socketicons.s3.amazonaws.com/Celer+Light.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "name": "Wrapped Ether", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/WETH", + "logoURI": "https://media.socket.tech/tokens/all/WETH", + "chainAgnosticId": "ETH" + }, + "destAsset": { + "chainId": 42161, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcAmount": "991250000000000000", + "destAmount": "991147696728676903" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a5187000000000000000000000000000000000000000000000000000000000000004c0000001252106ce9141d32a89a1e0a5ef360034a2f60a4b917c18838420000000000000000000000000000000000000600000000000000000dc1a09f859b20000000a4b1245fa5dd00002777000000000000000000000000000000000000000022be703a074ef6089a301c364c2bbf391d51067ea5cd91515c9ec5421cdaabb23451cd2086f3ebe3e19ff138f3a9be154dcae6033838cc5fabeeb0d260b075cb1c", + "gasLimit": 182048 + }, + "estimatedProcessingTimeInSeconds": 360 + }, + { + "quote": { + "requestId": "4f2154d9b330221b2ad461adf63acc2c", + "srcChainId": 10, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "id": "10_0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + "volatility": 2, + "axelarNetworkSymbol": "WETH", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg" + }, + "destChainId": 42161, + "destTokenAmount": "989989428114299041", + "destAsset": { + "id": "42161_0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": "ETH", + "address": "0x0000000000000000000000000000000000000000", + "chainId": 42161, + "name": "ETH", + "decimals": 18, + "usdPrice": 3133.259355489038, + "coingeckoId": "ethereum", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg", + "volatility": 2, + "axelarNetworkSymbol": "ETH", + "subGraphIds": ["chainflip-bridge"], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "id": "10_0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + "volatility": 2, + "axelarNetworkSymbol": "WETH", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg" + } + } + }, + "bridgeId": "squid", + "bridges": ["axelar"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "destChainId": 10, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "10_0x4200000000000000000000000000000000000006", + "symbol": "WETH", + "address": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg", + "axelarNetworkSymbol": "WETH", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg" + }, + "destAsset": { + "id": "10_0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "chainId": 10, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc", "cctp-uusdc-optimism-to-noble"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "srcAmount": "991250000000000000", + "destAmount": "3100880215" + }, + { + "action": "swap", + "srcChainId": 10, + "destChainId": 10, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "10_0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "chainId": 10, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc", "cctp-uusdc-optimism-to-noble"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "10_0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "symbol": "USDC.e", + "address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "chainId": 10, + "name": "USDC.e", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC.e", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "srcAmount": "3100880215", + "destAmount": "3101045779" + }, + { + "action": "swap", + "srcChainId": 10, + "destChainId": 10, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "10_0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "symbol": "USDC.e", + "address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607", + "chainId": 10, + "name": "USDC.e", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC.e", + "subGraphIds": [], + "enabled": true, + "subGraphOnly": false, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "10_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 10, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "srcAmount": "3101045779", + "destAmount": "3101521947" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 42161, + "protocol": { + "name": "axelar", + "displayName": "Axelar" + }, + "srcAsset": { + "id": "10_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 10, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "42161_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 42161, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "srcAmount": "3101521947", + "destAmount": "3101521947" + }, + { + "action": "swap", + "srcChainId": 42161, + "destChainId": 42161, + "protocol": { + "name": "Pancakeswap V3", + "displayName": "Pancakeswap V3" + }, + "srcAsset": { + "id": "42161_0xeb466342c4d449bc9f53a865d5cb90586f405215", + "symbol": "USDC.axl", + "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215", + "chainId": 42161, + "name": " USDC (Axelar)", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "interchainTokenId": null, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "axlUSDC", + "subGraphOnly": false, + "subGraphIds": ["uusdc"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "symbol": "USDC", + "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "chainId": 42161, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": [ + "uusdc", + "cctp-uusdc-arbitrum-to-noble", + "chainflip-bridge" + ], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "srcAmount": "3101521947", + "destAmount": "3100543869" + }, + { + "action": "swap", + "srcChainId": 42161, + "destChainId": 42161, + "protocol": { + "name": "Uniswap V3", + "displayName": "Uniswap V3" + }, + "srcAsset": { + "id": "42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "symbol": "USDC", + "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", + "chainId": 42161, + "name": "USDC", + "decimals": 6, + "usdPrice": 1.0003003590332982, + "coingeckoId": "usd-coin", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg", + "axelarNetworkSymbol": "USDC", + "subGraphOnly": false, + "subGraphIds": [ + "uusdc", + "cctp-uusdc-arbitrum-to-noble", + "chainflip-bridge" + ], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg" + }, + "destAsset": { + "id": "42161_0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "symbol": "WETH", + "address": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "chainId": 42161, + "name": "Wrapped ETH", + "decimals": 18, + "usdPrice": 3135.9632118339764, + "interchainTokenId": null, + "coingeckoId": "weth", + "type": "evm", + "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg", + "axelarNetworkSymbol": "WETH", + "subGraphOnly": false, + "subGraphIds": ["arbitrum-weth-wei"], + "enabled": true, + "active": true, + "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg" + }, + "srcAmount": "3100543869", + "destAmount": "989989428114299041" + } + ] + }, + "approval": { + "chainId": 10, + "to": "0x4200000000000000000000000000000000000006", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x00", + "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "gasLimit": 29122 + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x4653ce53e6b1", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000e73717569644164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b60000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000001a14846a1bc600000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000ce00000000000000000000000000000000000000000000000000000000000000d200000000000000000000000000000000000000000000000000000000000000d600000000000000000000000000000000000000000000000000000000000000dc0000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000098000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf00000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000b8833d8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8d3ad5700000000000000000000000000000000000000000000000000000000b8c346b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d66600000000000000000000000000000000000000000000000000000000b8d6341300000000000000000000000000000000000000000000000000000000b8ca89fa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000761786c55534443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008417262697472756d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307863653136463639333735353230616230313337376365374238386635424138433438463844363636000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c100000000000000000000000000000000000000000000000000000000000000040000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a8000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8dd781b00000000000000000000000000000000000000000000000000000000b8bb9ee30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f40521500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8ce8b7d0000000000000000000000000000000000000000000000000db72b79f837011c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000000004f2154d9b330221b2ad461adf63acc2c000000000000000000000000000000004f2154d9b330221b2ad461adf63acc2c0000000000000000000000003c17c95cdb5887c334bfae85750ce00e1a720a76eff35e60db6c9f3b8384a6d63db3c56f1ce6545b50ba2f250429055ca77e7e6203ddd65a7a4d89ae1af3d61b1c", + "gasLimit": 710342 + }, + "estimatedProcessingTimeInSeconds": 20 + } +] diff --git a/packages/bridge-controller/src/test/mock-quotes-native-erc20-eth.json b/packages/bridge-controller/src/test/mock-quotes-native-erc20-eth.json new file mode 100644 index 00000000000..0afd77760e7 --- /dev/null +++ b/packages/bridge-controller/src/test/mock-quotes-native-erc20-eth.json @@ -0,0 +1,258 @@ +[ + { + "quote": { + "requestId": "34c4136d-8558-4d87-bdea-eef8d2d30d6d", + "srcChainId": 1, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destChainId": 42161, + "destTokenAmount": "3104367033", + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["across"], + "steps": [ + { + "action": "swap", + "srcChainId": 1, + "destChainId": 1, + "protocol": { + "name": "0x", + "displayName": "0x", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "3104701473" + }, + { + "action": "bridge", + "srcChainId": 1, + "destChainId": 42161, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png" + }, + "srcAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "3104701473", + "destAmount": "3104367033" + } + ] + }, + "trade": { + "chainId": 1, + "to": "0x0439e60F02a8900a951603950d8D4527f400C3f1", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x0de0b6b3a7640000", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c696669416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de51520000000000000000000000000000000000000000000000000000000000000a003a3f733200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000094027363a1fac5600d1f7e8a4c50087ff1f32a09359512d2379d46b331c6033cc7b000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000b8211d6e000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066163726f73730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005c42213bc0b00000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004e41fff991f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b909399a00000000000000000000000000000000000000000000000000000000000000a094cc69295a8f2a3016ede239627ab300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a15600000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f4710000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2010001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000005000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000620541d325b000000000000000000000000000000000000000000000000000000000673656d70000000000000000000000000000000000000000000000000000000000000080ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b71dcbfe555f9a744b18195d9b52032871d6f3c5a558275c08a71c2b6214801f5161be976f49181b854a3ebcbe1f2b896133b03314a5ff2746e6494c43e59d0c9ee1c", + "gasLimit": 540076 + }, + "estimatedProcessingTimeInSeconds": 45 + }, + { + "quote": { + "requestId": "5bf0f2f0-655c-4e13-a545-1ebad6f9d2bc", + "srcChainId": 1, + "srcTokenAmount": "991250000000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destChainId": 42161, + "destTokenAmount": "3104601473", + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "feeData": { + "metabridge": { + "amount": "8750000000000000", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + } + } + }, + "bridgeId": "lifi", + "bridges": ["celercircle"], + "steps": [ + { + "action": "swap", + "srcChainId": 1, + "destChainId": 1, + "protocol": { + "name": "0x", + "displayName": "0x", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "chainId": 1, + "symbol": "ETH", + "decimals": 18, + "name": "ETH", + "coinKey": "ETH", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + "priceUSD": "3145.41", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" + }, + "destAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "991250000000000000", + "destAmount": "3104701473" + }, + { + "action": "bridge", + "srcChainId": 1, + "destChainId": 42161, + "protocol": { + "name": "celercircle", + "displayName": "Circle CCTP", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png" + }, + "srcAsset": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "chainId": 1, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9997000899730081", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "destAsset": { + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "chainId": 42161, + "symbol": "USDC", + "decimals": 6, + "name": "USD Coin", + "coinKey": "USDC", + "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "priceUSD": "0.9998000399920016", + "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" + }, + "srcAmount": "3104701473", + "destAmount": "3104601473" + } + ] + }, + "trade": { + "chainId": 1, + "to": "0x0439e60F02a8900a951603950d8D4527f400C3f1", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x0de0b6b3a7640000", + "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c696669416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000009248fab066300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200b431adcab44c6fe13ade53dbd3b714f57922ab5b776924a913685ad0fe680f6c000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000b8211d6e000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b63656c6572636972636c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005c42213bc0b00000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004e41fff991f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b909399a00000000000000000000000000000000000000000000000000000000000000a0c0452b52ecb7cf70409b16cd627ab300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a15600000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f4710000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2010001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000005000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047896dca097909ba9db4c9631bce0e53090bce14a9b7d203e21fa80cee7a16fa049aa1ef7d663c2ec3148e698e01774b62ddedc9c2dcd21994e549cd6f318f971b", + "gasLimit": 682910 + }, + "estimatedProcessingTimeInSeconds": 1029.717 + } +] diff --git a/packages/bridge-controller/src/test/mock-quotes-native-erc20.json b/packages/bridge-controller/src/test/mock-quotes-native-erc20.json new file mode 100644 index 00000000000..f7efe7950ba --- /dev/null +++ b/packages/bridge-controller/src/test/mock-quotes-native-erc20.json @@ -0,0 +1,294 @@ +[ + { + "quote": { + "requestId": "381c23bc-e3e4-48fe-bc53-257471e388ad", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcTokenAmount": "9912500000000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "24438902", + "feeData": { + "metabridge": { + "amount": "87500000000000", + "asset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["across"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "protocol": { + "name": "zerox", + "displayName": "0x", + "icon": "https://media.socket.tech/dexes/0x.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://assets.polygon.technology/tokenAssets/eth.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/eth.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "srcAmount": "9912500000000000", + "destAmount": "24456223" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "across", + "displayName": "Across", + "icon": "https://miro.medium.com/max/800/1*PN_F5yW4VMBgs_xX-fsyzQ.png" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "24456223", + "destAmount": "24438902" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x27147114878000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f600000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000e2037c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000d64123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000019d0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd0000000000000000000000001000000000000000000000000000000000000011000000000000000000000000000000000000000021582def464917822ff6092c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000043a900000000000000000000000000000000000000000000000000000000000000c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000174e7be000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000067041c47000000000000000000000000000000000000000000000000000000006704704d00000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef765753be7f7a64d5509974b0d678e1e3149b02f41fec59a4aef7d9ac92ee5eeaf293cb28c2261e7fd322723a97cb83762f7302296636026e52849fdad0f9db6e1640f914660e6b13f5b1a29345344c8c5687abbf1b", + "gasLimit": 610414 + }, + "estimatedProcessingTimeInSeconds": 60 + }, + { + "quote": { + "requestId": "4277a368-40d7-4e82-aa67-74f29dc5f98a", + "srcChainId": 10, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + }, + "srcTokenAmount": "9912500000000000", + "destChainId": 137, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://media.socket.tech/tokens/all/USDC", + "logoURI": "https://media.socket.tech/tokens/all/USDC", + "chainAgnosticId": "USDC" + }, + "destTokenAmount": "24256223", + "feeData": { + "metabridge": { + "amount": "87500000000000", + "asset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://media.socket.tech/tokens/all/ETH", + "logoURI": "https://media.socket.tech/tokens/all/ETH", + "chainAgnosticId": null + } + } + }, + "bridgeId": "socket", + "bridges": ["celercircle"], + "steps": [ + { + "action": "swap", + "srcChainId": 10, + "protocol": { + "name": "zerox", + "displayName": "0x", + "icon": "https://media.socket.tech/dexes/0x.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ethereum", + "decimals": 18, + "icon": "https://assets.polygon.technology/tokenAssets/eth.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/eth.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "srcAmount": "9912500000000000", + "destAmount": "24456223" + }, + { + "action": "bridge", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "cctp", + "displayName": "Circle CCTP", + "icon": "https://movricons.s3.ap-south-1.amazonaws.com/CCTP.svg" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "symbol": "USDC", + "name": "USD Coin", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": null + }, + "destAsset": { + "chainId": 137, + "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "symbol": "USDC", + "name": "Native USD Coin (POS)", + "decimals": 6, + "icon": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "logoURI": "https://assets.polygon.technology/tokenAssets/usdc.svg", + "chainAgnosticId": "USDC" + }, + "srcAmount": "24456223", + "destAmount": "24256223" + } + ], + "refuel": { + "action": "refuel", + "srcChainId": 10, + "destChainId": 137, + "protocol": { + "name": "refuel", + "displayName": "Refuel", + "icon": "" + }, + "srcAsset": { + "chainId": 10, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "ETH", + "name": "Ether", + "decimals": 18 + }, + "destAsset": { + "chainId": 137, + "address": "0x0000000000000000000000000000000000000000", + "symbol": "MATIC", + "name": "Matic", + "decimals": 18 + }, + "srcAmount": "1000000000000000", + "destAmount": "4405865573929566208" + } + }, + "trade": { + "chainId": 10, + "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e", + "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838", + "value": "0x27147114878000", + "data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000c6437c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc4123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd00000000000000000000000010000000000000000000000000000000000000110000000000000000000000000000000000000000974132b87a5cb75e32f034280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000030d4000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f9e43204a24f476db20f2518722627a122d31a1bc7c63fc15412e6a327295a9460b76bea5bb53b1f73fa6a15811055f6bada592d2e9e6c8cf48a855ce6968951c", + "gasLimit": 664389 + }, + "estimatedProcessingTimeInSeconds": 15 + } +] diff --git a/packages/bridge-controller/src/test/provider.ts b/packages/bridge-controller/src/test/provider.ts new file mode 100644 index 00000000000..8a9fc40b2ee --- /dev/null +++ b/packages/bridge-controller/src/test/provider.ts @@ -0,0 +1,7 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; + +export const createMockProvider = () => { + const engine = new JsonRpcEngine(); + return new SafeEventEmitterProvider({ engine }); +} \ No newline at end of file diff --git a/packages/bridge-controller/src/test/utils.ts b/packages/bridge-controller/src/test/utils.ts new file mode 100644 index 00000000000..28deec265fe --- /dev/null +++ b/packages/bridge-controller/src/test/utils.ts @@ -0,0 +1,3 @@ +export function flushPromises() { + return new Promise(jest.requireActual('timers').setImmediate); +} diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts new file mode 100644 index 00000000000..3fa9ddc6e33 --- /dev/null +++ b/packages/bridge-controller/src/types.ts @@ -0,0 +1,263 @@ +import type { Hex } from '@metamask/utils'; +import type { BigNumber } from 'bignumber.js'; +import { + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import { + NetworkControllerFindNetworkClientIdByChainIdAction, + NetworkControllerGetSelectedNetworkClientAction, +} from '@metamask/network-controller'; +import BridgeController from './bridge-controller'; +import { BRIDGE_CONTROLLER_NAME } from './constants'; + +/** + * The types of assets that a user can send + * + * @type {AssetTypes} + */ +export enum AssetType { + /** The native asset for the current network, such as ETH */ + native = 'NATIVE', + /** An ERC20 token */ + token = 'TOKEN', + /** An ERC721 or ERC1155 token. */ + NFT = 'NFT', + /** + * A transaction interacting with a contract that isn't a token method + * interaction will be marked as dealing with an unknown asset type. + */ + unknown = 'UNKNOWN', +} + +export type ChainConfiguration = { + isActiveSrc: boolean; + isActiveDest: boolean; +}; + +export type L1GasFees = { + l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller +}; +// Values derived from the quote response +// valueInCurrency values are calculated based on the user's selected currency + +export type QuoteMetadata = { + gasFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; + totalNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // estimatedGasFees + relayerFees + totalMaxNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // maxGasFees + relayerFees + toTokenAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; + adjustedReturn: { valueInCurrency: BigNumber | null }; // destTokenAmount - totalNetworkFee + sentAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; // srcTokenAmount + metabridgeFee + swapRate: BigNumber; // destTokenAmount / sentAmount + cost: { valueInCurrency: BigNumber | null }; // sentAmount - adjustedReturn +}; +// Sort order set by the user + +export enum SortOrder { + COST_ASC = 'cost_ascending', + ETA_ASC = 'time_descending', +} + +export type BridgeToken = { + type: AssetType.native | AssetType.token; + address: string; + symbol: string; + image: string; + decimals: number; + chainId: Hex; + balance: string; // raw balance + string: string | undefined; // normalized balance as a stringified number + tokenFiatAmount?: number | null; +} | null; +// Types copied from Metabridge API + +export enum BridgeFlag { + EXTENSION_CONFIG = 'extension-config', +} +type DecimalChainId = string; +export type GasMultiplierByChainId = Record; + +export type FeatureFlagResponse = { + [BridgeFlag.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; + }; +}; + +export type BridgeAsset = { + chainId: ChainId; + address: string; + symbol: string; + name: string; + decimals: number; + icon?: string; +}; + +export type QuoteRequest = { + walletAddress: string; + destWalletAddress?: string; + srcChainId: ChainId; + destChainId: ChainId; + srcTokenAddress: string; + destTokenAddress: string; + /** + * This is the amount sent, in atomic amount + */ + srcTokenAmount: string; + slippage: number; + aggIds?: string[]; + bridgeIds?: string[]; + insufficientBal?: boolean; + resetApproval?: boolean; + refuel?: boolean; +}; +type Protocol = { + name: string; + displayName?: string; + icon?: string; +}; +enum ActionTypes { + BRIDGE = 'bridge', + SWAP = 'swap', + REFUEL = 'refuel', +} +type Step = { + action: ActionTypes; + srcChainId: ChainId; + destChainId?: ChainId; + srcAsset: BridgeAsset; + destAsset: BridgeAsset; + srcAmount: string; + destAmount: string; + protocol: Protocol; +}; +type RefuelData = Step; + +export type Quote = { + requestId: string; + srcChainId: ChainId; + srcAsset: BridgeAsset; + // Some tokens have a fee of 0, so sometimes it's equal to amount sent + srcTokenAmount: string; // Atomic amount, the amount sent - fees + destChainId: ChainId; + destAsset: BridgeAsset; + destTokenAmount: string; // Atomic amount, the amount received + feeData: Record & + Partial>; + bridgeId: string; + bridges: string[]; + steps: Step[]; + refuel?: RefuelData; +}; + +export type QuoteResponse = { + quote: Quote; + approval: TxData | null; + trade: TxData; + estimatedProcessingTimeInSeconds: number; +}; + +export enum ChainId { + ETH = 1, + OPTIMISM = 10, + BSC = 56, + POLYGON = 137, + ZKSYNC = 324, + BASE = 8453, + ARBITRUM = 42161, + AVALANCHE = 43114, + LINEA = 59144, +} + +export enum FeeType { + METABRIDGE = 'metabridge', + REFUEL = 'refuel', +} +export type FeeData = { + amount: string; + asset: BridgeAsset; +}; +export type TxData = { + chainId: ChainId; + to: string; + from: string; + value: string; + data: string; + gasLimit: number | null; +}; +export enum BridgeFeatureFlagsKey { + EXTENSION_CONFIG = 'extensionConfig', +} + +export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: number; + maxRefreshCount: number; + support: boolean; + chains: Record; + }; +}; +export enum RequestStatus { + LOADING, + FETCHED, + ERROR, +} +export enum BridgeUserAction { + SELECT_DEST_NETWORK = 'selectDestNetwork', + UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', +} +export enum BridgeBackgroundAction { + SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', + RESET_STATE = 'resetState', + GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', +} +export type BridgeControllerState = { + bridgeFeatureFlags: BridgeFeatureFlags; + quoteRequest: Partial; + quotes: (QuoteResponse & L1GasFees)[]; + quotesInitialLoadTime?: number; + quotesLastFetched?: number; + quotesLoadingStatus?: RequestStatus; + quoteFetchError?: string; + quotesRefreshCount: number; +}; + +type BridgeControllerAction = { + type: `${typeof BRIDGE_CONTROLLER_NAME}:${FunctionName}`; + handler: BridgeController[FunctionName]; +}; + +// Maps to BridgeController function names +type BridgeControllerActions = + | BridgeControllerAction + | BridgeControllerAction + | BridgeControllerAction + | BridgeControllerAction; + +type BridgeControllerEvents = ControllerStateChangeEvent< + typeof BRIDGE_CONTROLLER_NAME, + BridgeControllerState +>; + +type AllowedActions = + | AccountsControllerGetSelectedAccountAction['type'] + | NetworkControllerGetSelectedNetworkClientAction['type'] + | NetworkControllerFindNetworkClientIdByChainIdAction['type']; +type AllowedEvents = never; + +/** + * The messenger for the BridgeController. + */ +export type BridgeControllerMessenger = RestrictedControllerMessenger< + typeof BRIDGE_CONTROLLER_NAME, + | BridgeControllerActions + | AccountsControllerGetSelectedAccountAction + | NetworkControllerGetSelectedNetworkClientAction + | NetworkControllerFindNetworkClientIdByChainIdAction, + BridgeControllerEvents, + AllowedActions, + AllowedEvents +>; diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts new file mode 100644 index 00000000000..f3a7b0159c2 --- /dev/null +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -0,0 +1,138 @@ +import { BigNumber } from 'ethers'; +import { zeroAddress } from 'ethereumjs-util'; +import { Contract } from '@ethersproject/contracts'; +import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import * as balanceUtils from './balance'; +import { createMockProvider } from '../test/provider'; + +declare global { + var ethereumProvider: SafeEventEmitterProvider; +} + +const mockGetBalance = jest.fn(); +jest.mock('@ethersproject/providers', () => { + return { + Web3Provider: jest.fn().mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }), + }; +}); + +jest.mock('@ethersproject/contracts'); + +describe('balance', () => { + beforeEach(() => { + jest.clearAllMocks(); + global.ethereumProvider = createMockProvider(); + }); + + describe('calcLatestSrcBalance', () => { + it('should return the ERC20 token balance', async () => { + const mockBalanceOf = jest.fn().mockResolvedValueOnce(BigNumber.from('100')); + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + expect( + await balanceUtils.calcLatestSrcBalance( + global.ethereumProvider, + '0x123', + '0x456', + '0x789', + ), + ).toStrictEqual(BigNumber.from(100)); + expect(mockBalanceOf).toHaveBeenCalledTimes(1); + expect(mockBalanceOf).toHaveBeenCalledWith( + '0x123', + ); + }); + + it('should return the native asset balance', async () => { + mockGetBalance.mockImplementation(() => { + return BigNumber.from(100); + }); + + expect( + await balanceUtils.calcLatestSrcBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', + zeroAddress(), + '0x789', + ), + ).toStrictEqual(BigNumber.from(100)); + expect(mockGetBalance).toHaveBeenCalledTimes(1); + expect(mockGetBalance).toHaveBeenCalledWith( + '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', + ); + }); + + it('should return undefined if token address and chainId are undefined', async () => { + const mockFetchTokenBalance = jest.spyOn(balanceUtils, 'fetchTokenBalance'); + expect( + await balanceUtils.calcLatestSrcBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', + undefined as never, + undefined as never, + ), + ).toStrictEqual(undefined); + expect(mockFetchTokenBalance).not.toHaveBeenCalled(); + expect(mockGetBalance).not.toHaveBeenCalled(); + }); + }); + + describe('hasSufficientBalance', () => { + it('should return true if user has sufficient balance', async () => { + mockGetBalance.mockImplementation(() => { + return BigNumber.from('10000000000000000000'); + }); + + const mockBalanceOf = jest.fn().mockResolvedValueOnce(BigNumber.from('10000000000000000001')); + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + zeroAddress(), + '10000000000000000000', + '0x1', + ), + ).toBe(true); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + '10000000000000000000', + '0x1', + ), + ).toBe(true); + }); + + it('should return false if user has native assets but insufficient ERC20 src tokens', async () => { + mockGetBalance.mockImplementation(() => { + return BigNumber.from('10000000000000000000'); + }); + const mockFetchTokenBalance = jest.spyOn(balanceUtils, 'fetchTokenBalance'); + mockFetchTokenBalance.mockResolvedValueOnce( + BigNumber.from('9000000000000000000'), + ); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + '10000000000000000000', + '0x1', + ), + ).toBe(false); + }); + }); +}); diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts new file mode 100644 index 00000000000..6ce64a4f393 --- /dev/null +++ b/packages/bridge-controller/src/utils/balance.ts @@ -0,0 +1,56 @@ +import { Web3Provider } from '@ethersproject/providers'; +import type { Provider } from '@metamask/network-controller'; +import { Hex } from '@metamask/utils'; +import { Contract } from '@ethersproject/contracts'; +import { zeroAddress } from 'ethereumjs-util'; +import { getAddress } from 'ethers/lib/utils'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import { BigNumber } from 'ethers'; + +export async function fetchTokenBalance( + address: string, + userAddress: string, + provider: Provider, +): Promise { + const ethersProvider = new Web3Provider(provider); + const tokenContract = new Contract(address, abiERC20, ethersProvider); + const tokenBalancePromise = tokenContract + ? tokenContract.balanceOf(userAddress) + : Promise.resolve(); + return await tokenBalancePromise; +} + +export const calcLatestSrcBalance = async ( + provider: Provider, + selectedAddress: string, + tokenAddress: string, + chainId: Hex, +): Promise => { + if (tokenAddress && chainId) { + if (tokenAddress === zeroAddress()) { + const ethersProvider = new Web3Provider(provider); + return await ethersProvider.getBalance(getAddress(selectedAddress)) + } + return await fetchTokenBalance(tokenAddress, selectedAddress, provider); + } + return undefined; +}; + +export const hasSufficientBalance = async ( + provider: Provider, + selectedAddress: string, + tokenAddress: string, + fromTokenAmount: string, + chainId: Hex, +) => { + const srcTokenBalance = await calcLatestSrcBalance( + provider, + selectedAddress, + tokenAddress, + chainId, + ); + + return ( + srcTokenBalance?.gte(fromTokenAmount) ?? false + ); +}; diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts new file mode 100644 index 00000000000..a01bb8fa3e3 --- /dev/null +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -0,0 +1,322 @@ +import { zeroAddress } from 'ethereumjs-util'; +import { handleFetch } from '@metamask/controller-utils'; +import { CHAIN_IDS } from '../constants/chains'; +import mockBridgeQuotesErc20Erc20 from '../test/mock-quotes-erc20-erc20.json'; +import mockBridgeQuotesNativeErc20 from '../test/mock-quotes-native-erc20.json'; +import { + fetchBridgeFeatureFlags, + fetchBridgeQuotes, + fetchBridgeTokens, +} from './fetch'; + +jest.mock('@metamask/controller-utils', () => ({ + ...jest.requireActual('@metamask/controller-utils'), + handleFetch: jest.fn(), +})); + +describe('Bridge utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchBridgeFeatureFlags', () => { + it('should fetch bridge feature flags successfully', async () => { + const mockResponse = { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + support: true, + chains: { + '1': { + isActiveSrc: true, + isActiveDest: true, + }, + '10': { + isActiveSrc: true, + isActiveDest: false, + }, + '59144': { + isActiveSrc: true, + isActiveDest: true, + }, + '120': { + isActiveSrc: true, + isActiveDest: false, + }, + '137': { + isActiveSrc: false, + isActiveDest: true, + }, + '11111': { + isActiveSrc: false, + isActiveDest: true, + }, + }, + }, + }; + + (handleFetch as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags(); + + expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getAllFeatureFlags', { + headers: { 'X-Client-Id': 'extension' }, + }); + + expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 1, + refreshRate: 3, + support: true, + chains: { + [CHAIN_IDS.MAINNET]: { + isActiveSrc: true, + isActiveDest: true, + }, + [CHAIN_IDS.OPTIMISM]: { + isActiveSrc: true, + isActiveDest: false, + }, + [CHAIN_IDS.LINEA_MAINNET]: { + isActiveSrc: true, + isActiveDest: true, + }, + '0x78': { + isActiveSrc: true, + isActiveDest: false, + }, + [CHAIN_IDS.POLYGON]: { + isActiveSrc: false, + isActiveDest: true, + }, + '0x2b67': { + isActiveSrc: false, + isActiveDest: true, + }, + }, + }, + }); + }); + + it('should use fallback bridge feature flags if response is unexpected', async () => { + const mockResponse = { + 'extension-config': { + refreshRate: 3, + maxRefreshCount: 1, + support: 25, + chains: { + a: { + isActiveSrc: 1, + isActiveDest: 'test', + }, + '2': { + isActiveSrc: 'test', + isActiveDest: 2, + }, + }, + }, + }; + + (handleFetch as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeFeatureFlags(); + + expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getAllFeatureFlags', { + headers: { 'X-Client-Id': 'extension' }, + }); + + expect(result).toStrictEqual({ + extensionConfig: { + maxRefreshCount: 5, + refreshRate: 30000, + support: false, + chains: {}, + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (handleFetch as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeFeatureFlags()).rejects.toThrow(mockError); + }); + }); + + describe('fetchBridgeTokens', () => { + it('should fetch bridge tokens successfully', async () => { + const mockResponse = [ + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'ABC', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + decimals: 16, + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + decimals: 16, + symbol: 'DEF', + aggregators: ['lifi'], + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f987', + symbol: 'DEF', + }, + { + address: '0x124', + symbol: 'JKL', + decimals: 16, + }, + ]; + + (handleFetch as jest.Mock).mockResolvedValue(mockResponse); + + const result = await fetchBridgeTokens('0xa'); + + expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getTokens?chainId=10', { + headers: { 'X-Client-Id': 'extension' }, + }); + + console.log(result); + + expect(result).toStrictEqual({ + '0x0000000000000000000000000000000000000000': { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + iconUrl: '', + name: 'Ether', + symbol: 'ETH', + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + decimals: 16, + symbol: 'DEF', + aggregators: ['lifi'], + }, + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 16, + symbol: 'ABC', + }, + }); + }); + + it('should handle fetch error', async () => { + const mockError = new Error('Failed to fetch'); + + (handleFetch as jest.Mock).mockRejectedValue(mockError); + + await expect(fetchBridgeTokens('0xa')).rejects.toThrow(mockError); + }); + }); + + describe('fetchBridgeQuotes', () => { + it('should fetch bridge quotes successfully, no approvals', async () => { + (handleFetch as jest.Mock).mockResolvedValue( + mockBridgeQuotesNativeErc20, + ); + const { signal } = new AbortController(); + + const result = await fetchBridgeQuotes( + { + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: zeroAddress(), + destTokenAddress: zeroAddress(), + srcTokenAmount: '20000', + slippage: 0.5, + }, + signal, + ); + + expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { + headers: { 'X-Client-Id': 'extension' }, + signal, + }); + + expect(result).toStrictEqual(mockBridgeQuotesNativeErc20); + }); + + it('should fetch bridge quotes successfully, with approvals', async () => { + (handleFetch as jest.Mock).mockResolvedValue([ + ...mockBridgeQuotesErc20Erc20, + { ...mockBridgeQuotesErc20Erc20[0], approval: null }, + { ...mockBridgeQuotesErc20Erc20[0], trade: null }, + ]); + const { signal } = new AbortController(); + + const result = await fetchBridgeQuotes( + { + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: zeroAddress(), + destTokenAddress: zeroAddress(), + srcTokenAmount: '20000', + slippage: 0.5, + }, + signal, + ); + + expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { + headers: { 'X-Client-Id': 'extension' }, + signal, + }); + + expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); + }); + + it('should filter out malformed bridge quotes', async () => { + (handleFetch as jest.Mock).mockResolvedValue([ + ...mockBridgeQuotesErc20Erc20, + ...mockBridgeQuotesErc20Erc20.map( + ({ quote, ...restOfQuote }) => restOfQuote, + ), + { + ...mockBridgeQuotesErc20Erc20[0], + quote: { + srcAsset: { + ...mockBridgeQuotesErc20Erc20[0].quote.srcAsset, + decimals: undefined, + }, + }, + }, + { + ...mockBridgeQuotesErc20Erc20[1], + quote: { + srcAsset: { + ...mockBridgeQuotesErc20Erc20[1].quote.destAsset, + address: undefined, + }, + }, + }, + ]); + const { signal } = new AbortController(); + + const result = await fetchBridgeQuotes( + { + walletAddress: '0x123', + srcChainId: 1, + destChainId: 10, + srcTokenAddress: zeroAddress(), + destTokenAddress: zeroAddress(), + srcTokenAmount: '20000', + slippage: 0.5, + }, + signal, + ); + + expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { + headers: { 'X-Client-Id': 'extension' }, + signal, + }); + + expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); + }); + }); +}); diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts new file mode 100644 index 00000000000..150b7c466c5 --- /dev/null +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -0,0 +1,169 @@ +import { Duration, Hex, hexToNumber, numberToHex } from '@metamask/utils'; +import { + BRIDGE_API_BASE_URL, + BRIDGE_CLIENT_ID, + REFRESH_INTERVAL_MS, +} from '../constants'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SwapsTokenObject, +} from '../constants/tokens'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from './index'; +import { + BridgeFlag, + FeatureFlagResponse, + FeeData, + FeeType, + Quote, + QuoteRequest, + QuoteResponse, + TxData, + BridgeFeatureFlagsKey, + BridgeFeatureFlags, +} from '../types'; +import { + FEATURE_FLAG_VALIDATORS, + QUOTE_VALIDATORS, + TX_DATA_VALIDATORS, + TOKEN_VALIDATORS, + validateResponse, + QUOTE_RESPONSE_VALIDATORS, + FEE_DATA_VALIDATORS, +} from './validators'; +import { handleFetch } from '@metamask/controller-utils'; + + +const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; +// TODO put this back in once we have a fetchWithCache equivalent +// const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; + +export async function fetchBridgeFeatureFlags(): Promise { + const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; + const rawFeatureFlags = await handleFetch(url, { + headers: CLIENT_ID_HEADER, + }); + + if ( + validateResponse( + FEATURE_FLAG_VALIDATORS, + rawFeatureFlags, + url, + ) + ) { + return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + ...rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG], + chains: Object.entries( + rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG].chains, + ).reduce( + (acc, [chainId, value]) => ({ + ...acc, + [numberToHex(Number(chainId))]: value, + }), + {}, + ), + }, + }; + } + + return { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: 5, + support: false, + chains: {}, + }, + }; +} + +/** + * Returns a list of enabled (unblocked) tokens + * */ +export async function fetchBridgeTokens( + chainId: Hex, +): Promise> { + // TODO make token api v2 call + const url = `${BRIDGE_API_BASE_URL}/getTokens?chainId=${hexToNumber( + chainId, + )}`; + + // TODO we will need to cache these. In Extension fetchWithCache is used. This is due to the following: + // If we allow selecting dest networks which the user has not imported, + // note that the Assets controller won't be able to provide tokens. In extension we fetch+cache the token list from bridge-api to handle this + const tokens = await handleFetch(url, { + headers: CLIENT_ID_HEADER, + }); + + const nativeToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + const transformedTokens: Record = {}; + if (nativeToken) { + transformedTokens[nativeToken.address] = nativeToken; + } + + tokens.forEach((token: unknown) => { + if ( + validateResponse(TOKEN_VALIDATORS, token, url, false) && + !( + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) + ) + ) { + transformedTokens[token.address] = token; + } + }); + return transformedTokens; +} + +// Returns a list of bridge tx quotes +export async function fetchBridgeQuotes( + request: QuoteRequest, + signal: AbortSignal, +): Promise { + const queryParams = new URLSearchParams({ + walletAddress: request.walletAddress, + srcChainId: request.srcChainId.toString(), + destChainId: request.destChainId.toString(), + srcTokenAddress: request.srcTokenAddress, + destTokenAddress: request.destTokenAddress, + srcTokenAmount: request.srcTokenAmount, + slippage: request.slippage.toString(), + insufficientBal: request.insufficientBal ? 'true' : 'false', + resetApproval: request.resetApproval ? 'true' : 'false', + }); + const url = `${BRIDGE_API_BASE_URL}/getQuote?${queryParams}`; + const quotes = await handleFetch(url, { + headers: CLIENT_ID_HEADER, + signal, + }); + + const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => { + const { quote, approval, trade } = quoteResponse; + return ( + validateResponse( + QUOTE_RESPONSE_VALIDATORS, + quoteResponse, + url, + ) && + validateResponse(QUOTE_VALIDATORS, quote, url) && + validateResponse(TOKEN_VALIDATORS, quote.srcAsset, url) && + validateResponse(TOKEN_VALIDATORS, quote.destAsset, url) && + validateResponse(TX_DATA_VALIDATORS, trade, url) && + validateResponse( + FEE_DATA_VALIDATORS, + quote.feeData[FeeType.METABRIDGE], + url, + ) && + (approval + ? validateResponse(TX_DATA_VALIDATORS, approval, url) + : true) + ); + }); + return filteredQuotes; +} \ No newline at end of file diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts new file mode 100644 index 00000000000..c6fc9eebb07 --- /dev/null +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -0,0 +1,40 @@ +import { sumHexes } from './index'; + +describe('Bridge utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('sumHexes', () => { + it('returns 0x0 for empty input', () => { + expect(sumHexes()).toBe('0x0'); + }); + + it('returns same value for single input', () => { + expect(sumHexes('0xff')).toBe('0xff'); + expect(sumHexes('0x0')).toBe('0x0'); + expect(sumHexes('0x1')).toBe('0x1'); + }); + + it('correctly sums two hex values', () => { + expect(sumHexes('0x1', '0x1')).toBe('0x2'); + expect(sumHexes('0xff', '0x1')).toBe('0x100'); + expect(sumHexes('0x0', '0xff')).toBe('0xff'); + }); + + it('correctly sums multiple hex values', () => { + expect(sumHexes('0x1', '0x2', '0x3')).toBe('0x6'); + expect(sumHexes('0xff', '0xff', '0x2')).toBe('0x200'); + expect(sumHexes('0x0', '0x0', '0x0')).toBe('0x0'); + }); + + it('handles large numbers', () => { + expect(sumHexes('0xffffffff', '0x1')).toBe('0x100000000'); + expect(sumHexes('0xffffffff', '0xffffffff')).toBe('0x1fffffffe'); + }); + + it('throws for invalid hex strings', () => { + expect(() => sumHexes('0xg')).toThrow(); + }); + }); +}); diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/index.ts new file mode 100644 index 00000000000..23c31e912c1 --- /dev/null +++ b/packages/bridge-controller/src/utils/index.ts @@ -0,0 +1,70 @@ +import { Contract } from '@ethersproject/contracts'; +import { Hex } from '@metamask/utils'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import { + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, +} from '../constants'; +import { CHAIN_IDS } from '../constants/chains'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; + +/** + * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum + * + * @returns The txParam data that will reset allowance to 0, combine it with the approval tx params received from Bridge API + */ +export const getEthUsdtResetData = () => { + const UsdtContractInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) + .interface; + const data = UsdtContractInterface.encodeFunctionData('approve', [ + METABRIDGE_ETHEREUM_ADDRESS, + '0', + ]); + + return data; +}; + +export const isEthUsdt = (chainId: Hex, address: string) => + chainId === CHAIN_IDS.MAINNET && + address.toLowerCase() === ETH_USDT_ADDRESS.toLowerCase(); + +export const sumHexes = (...hexStrings: string[]): Hex => { + if (hexStrings.length === 0) { + return '0x0'; + } + + const sum = hexStrings.reduce((acc, hex) => acc + BigInt(hex), BigInt(0)); + return `0x${sum.toString(16)}`; +} + +/** + * Checks whether the provided address is strictly equal to the address for + * the default swaps token of the provided chain. + * + * @param {string} address - The string to compare to the default token address + * @param {Hex} chainId - The hex encoded chain ID of the default swaps token to check + * @returns {boolean} Whether the address is the provided chain's default token address + */ +export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { + if (!address || !chainId) { + return false; + } + + return address === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP]?.address; +} + +/** + * Checks whether the provided symbol is strictly equal to the symbol for + * the default swaps token of the provided chain. + * + * @param {string} symbol - The string to compare to the default token symbol + * @param {Hex} chainId - The hex encoded chain ID of the default swaps token to check + * @returns {boolean} Whether the symbol is the provided chain's default token symbol + */ +export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { + if (!symbol || !chainId) { + return false; + } + + return symbol === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP]?.symbol; +} diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts new file mode 100644 index 00000000000..25238c7e540 --- /dev/null +++ b/packages/bridge-controller/src/utils/quote.ts @@ -0,0 +1,36 @@ +import type { QuoteRequest } from '../types'; + +export const isValidQuoteRequest = ( + partialRequest: Partial, + requireAmount = true, +): partialRequest is QuoteRequest => { + const STRING_FIELDS = ['srcTokenAddress', 'destTokenAddress']; + if (requireAmount) { + STRING_FIELDS.push('srcTokenAmount'); + } + const NUMBER_FIELDS = ['srcChainId', 'destChainId', 'slippage']; + + return ( + STRING_FIELDS.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'string' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + partialRequest[field as keyof typeof partialRequest] !== '' && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + NUMBER_FIELDS.every( + (field) => + field in partialRequest && + typeof partialRequest[field as keyof typeof partialRequest] === + 'number' && + partialRequest[field as keyof typeof partialRequest] !== undefined && + !isNaN(Number(partialRequest[field as keyof typeof partialRequest])) && + partialRequest[field as keyof typeof partialRequest] !== null, + ) && + (requireAmount + ? Boolean((partialRequest.srcTokenAmount ?? '').match(/^[1-9]\d*$/u)) + : true) + ); +}; diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts new file mode 100644 index 00000000000..9ab432bdad1 --- /dev/null +++ b/packages/bridge-controller/src/utils/validators.ts @@ -0,0 +1,143 @@ +import { isStrictHexString } from '@metamask/utils'; +import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; +import { BridgeAsset, BridgeFlag, FeatureFlagResponse, FeeData, Quote, QuoteResponse, TxData } from '../types'; +import { SwapsTokenObject } from '../constants/tokens'; + +export const truthyString = (string: string) => Boolean(string?.length); +export const truthyDigitString = (string: string) => + truthyString(string) && Boolean(string.match(/^\d+$/u)); + +export const isValidNumber = (v: unknown): v is number => typeof v === 'number'; +const isValidObject = (v: unknown): v is object => + typeof v === 'object' && v !== null; +const isValidString = (v: unknown): v is string => + typeof v === 'string' && v.length > 0; +const isValidHexAddress = (v: unknown) => + isValidString(v) && isValidHexAddress_(v, { allowNonPrefixed: false }); + +type Validator = { + property: keyof ExpectedResponse; + type: string; + validator?: (value: unknown) => boolean; +}; + +export const validateData = ( + validators: Validator[], + object: unknown, + urlUsed: string, + logError = true, +): object is ExpectedResponse => { + return validators.every(({ property, type, validator }) => { + const types = type.split('|'); + const propertyString = String(property); + + const valid = + isValidObject(object) && + types.some((_type) => typeof object[propertyString as keyof typeof object] === _type) && + (!validator || validator(object[propertyString as keyof typeof object])); + + if (!valid && logError) { + const value = isValidObject(object) ? object[propertyString as keyof typeof object] : undefined; + const type = isValidObject(object) ? typeof object[propertyString as keyof typeof object] : 'undefined'; + + console.error( + `response to GET ${urlUsed} invalid for property ${String(property)}; value was:`, + value, + '| type was: ', + type, + ); + } + return valid; + }); +}; + +export const validateResponse = ( + validators: Validator[], + data: unknown, + urlUsed: string, + logError = true, +): data is ExpectedResponse => { + return validateData(validators, data, urlUsed, logError); +}; + +export const FEATURE_FLAG_VALIDATORS = [ + { + property: BridgeFlag.EXTENSION_CONFIG, + type: 'object', + validator: ( + v: unknown, + ): v is Pick => + isValidObject(v) && + 'refreshRate' in v && + isValidNumber(v.refreshRate) && + 'maxRefreshCount' in v && + isValidNumber(v.maxRefreshCount) && + 'chains' in v && + isValidObject(v.chains) && + Object.values(v.chains).every((chain) => isValidObject(chain)) && + Object.values(v.chains).every( + (chain) => + 'isActiveSrc' in chain && + 'isActiveDest' in chain && + typeof chain.isActiveSrc === 'boolean' && + typeof chain.isActiveDest === 'boolean', + ), + }, +]; + +export const TOKEN_AGGREGATOR_VALIDATORS = [ + { + property: 'aggregators', + type: 'object', + validator: (v: unknown): v is number[] => + isValidObject(v) && Object.values(v).every(isValidString), + }, +]; + +export const TOKEN_VALIDATORS: Validator[] = [ + { property: 'decimals', type: 'number' }, + { property: 'address', type: 'string', validator: isValidHexAddress }, + { + property: 'symbol', + type: 'string', + validator: (v: unknown) => isValidString(v) && v.length <= 12, + }, +]; + +export const QUOTE_RESPONSE_VALIDATORS: Validator[] = [ + { property: 'quote', type: 'object', validator: isValidObject }, + { property: 'estimatedProcessingTimeInSeconds', type: 'number' }, + { + property: 'approval', + type: 'object|undefined', + validator: (v: unknown) => v === undefined || isValidObject(v), + }, + { property: 'trade', type: 'object', validator: isValidObject }, +]; + +export const QUOTE_VALIDATORS: Validator[] = [ + { property: 'requestId', type: 'string' }, + { property: 'srcTokenAmount', type: 'string' }, + { property: 'destTokenAmount', type: 'string' }, + { property: 'bridgeId', type: 'string' }, + { property: 'bridges', type: 'object', validator: isValidObject }, + { property: 'srcChainId', type: 'number' }, + { property: 'destChainId', type: 'number' }, + { property: 'srcAsset', type: 'object', validator: isValidObject }, + { property: 'destAsset', type: 'object', validator: isValidObject }, + { property: 'feeData', type: 'object', validator: isValidObject }, +]; + +export const FEE_DATA_VALIDATORS: Validator[] = [ + { property: 'amount', type: 'string', validator: (v: unknown) => truthyDigitString(String(v)) }, + { property: 'asset', type: 'object', validator: isValidObject }, +]; + +export const TX_DATA_VALIDATORS: Validator[] = [ + { property: 'chainId', type: 'number' }, + { property: 'value', type: 'string', validator: isStrictHexString }, + { property: 'gasLimit', type: 'number' }, + { property: 'to', type: 'string', validator: isValidHexAddress }, + { property: 'from', type: 'string', validator: isValidHexAddress }, + { property: 'data', type: 'string', validator: isStrictHexString }, +]; diff --git a/packages/bridge-controller/tsconfig.build.json b/packages/bridge-controller/tsconfig.build.json new file mode 100644 index 00000000000..5d38b996867 --- /dev/null +++ b/packages/bridge-controller/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../approval-controller/tsconfig.build.json" }, + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../preferences-controller/tsconfig.build.json" }, + { "path": "../polling-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json new file mode 100644 index 00000000000..aab3ff34504 --- /dev/null +++ b/packages/bridge-controller/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "resolveJsonModule": true + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../polling-controller" }, + { "path": "../network-controller" }, + { "path": "../transaction-controller" } + ], + "include": ["../../types", "./src", "tsconfig.build.json", "tsconfig.json"] +} diff --git a/packages/bridge-controller/typedoc.json b/packages/bridge-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/bridge-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/yarn.lock b/yarn.lock index f6a87dc72a9..820eed9fffc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13826,4 +13826,4 @@ __metadata: "@types/yoga-layout": "npm:1.9.2" checksum: 10/fe36fadae9b30710083f76c73e87479c2eb291ff7c560c35a9e2b8eb78f43882ace63cc80cdaecae98ee2e4168e1bf84dc65b2f5ae1bfa31df37603c46683bd6 languageName: node - linkType: hard + linkType: hard \ No newline at end of file From 553b3d68bcb024bdd63a6030c8e3cdef36c29a5e Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:17:38 -0500 Subject: [PATCH 02/94] chore: remove logs --- packages/bridge-controller/src/utils/fetch.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index a01bb8fa3e3..39f6a95bdf2 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -181,8 +181,6 @@ describe('Bridge utils', () => { headers: { 'X-Client-Id': 'extension' }, }); - console.log(result); - expect(result).toStrictEqual({ '0x0000000000000000000000000000000000000000': { address: '0x0000000000000000000000000000000000000000', From c3705ffff9f61d38d86a3f15f02098246fbc8ecf Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:53:19 -0500 Subject: [PATCH 03/94] chore: clean up --- packages/bridge-controller/jest.config.js | 3 ++ .../bridge-controller/jest.environment.js | 17 ++++++++ packages/bridge-controller/package.json | 4 +- .../src/bridge-controller.ts | 41 +++++++++---------- .../bridge-controller/src/constants/chains.ts | 2 +- .../bridge-controller/src/constants/index.ts | 8 ++-- .../bridge-controller/src/constants/tokens.ts | 4 +- .../bridge-controller/src/test/provider.ts | 4 +- packages/bridge-controller/src/types.ts | 16 ++++---- packages/bridge-controller/tsconfig.json | 2 +- 10 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 packages/bridge-controller/jest.environment.js diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js index d5f34b1825e..a226e79eb7f 100644 --- a/packages/bridge-controller/jest.config.js +++ b/packages/bridge-controller/jest.config.js @@ -23,4 +23,7 @@ module.exports = merge(baseConfig, { statements: 98.03, }, }, + + // We rely on `window` to make requests + testEnvironment: '/jest.environment.js', }); diff --git a/packages/bridge-controller/jest.environment.js b/packages/bridge-controller/jest.environment.js new file mode 100644 index 00000000000..b77d5478109 --- /dev/null +++ b/packages/bridge-controller/jest.environment.js @@ -0,0 +1,17 @@ +const JSDOMEnvironment = require('jest-environment-jsdom'); + +// Custom test environment copied from https://github.com/jsdom/jsdom/issues/2524 +// in order to add TextEncoder to jsdom. TextEncoder is expected by jose. + +module.exports = class CustomTestEnvironment extends JSDOMEnvironment { + async setup() { + await super.setup(); + if (typeof this.global.TextEncoder === 'undefined') { + const { TextEncoder, TextDecoder } = require('util'); + this.global.TextEncoder = TextEncoder; + this.global.TextDecoder = TextDecoder; + this.global.ArrayBuffer = ArrayBuffer; + this.global.Uint8Array = Uint8Array; + } + } +}; diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index d6453225034..70748b1fba2 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -57,6 +57,7 @@ "@metamask/polling-controller": "^12.0.1", "@metamask/transaction-controller": "^43.0.0", "@metamask/utils": "^10.0.1", + "ethereumjs-util": "^7.0.10", "ethers": "5.7.0" }, "devDependencies": { @@ -73,9 +74,6 @@ "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.2.2" }, - "peerDependencies": { - "@metamask/keyring-controller": "^19.0.0" - }, "engines": { "node": "^18.18 || >=20" }, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index e2a836406e4..9932ae43b16 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -1,20 +1,22 @@ -import { add0x, Hex, numberToHex } from '@metamask/utils'; -import { StaticIntervalPollingController } from '@metamask/polling-controller'; -import { NetworkClientId } from '@metamask/network-controller'; -import { StateMetadata } from '@metamask/base-controller'; +import { BigNumber } from '@ethersproject/bignumber'; import { Contract } from '@ethersproject/contracts'; -import { abiERC20 } from '@metamask/metamask-eth-abis'; import { Web3Provider } from '@ethersproject/providers'; -import { BigNumber } from '@ethersproject/bignumber'; -import { TransactionParams } from '@metamask/transaction-controller'; +import type { StateMetadata } from '@metamask/base-controller'; import type { ChainId } from '@metamask/controller-utils'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { NetworkClientId } from '@metamask/network-controller'; +import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import type { TransactionParams } from '@metamask/transaction-controller'; +import { add0x, numberToHex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import { REFRESH_INTERVAL_MS } from './constants'; import { - fetchBridgeFeatureFlags, - fetchBridgeQuotes, -} from './utils/fetch'; -import { - sumHexes, -} from './utils'; + BRIDGE_CONTROLLER_NAME, + DEFAULT_BRIDGE_CONTROLLER_STATE, + METABRIDGE_CHAIN_TO_ADDRESS_MAP, +} from './constants'; +import { CHAIN_IDS } from './constants/chains'; import { type L1GasFees, type QuoteRequest, @@ -24,16 +26,11 @@ import { BridgeFeatureFlagsKey, RequestStatus, } from './types'; -import { isValidQuoteRequest } from './utils/quote'; -import { hasSufficientBalance } from './utils/balance'; -import { CHAIN_IDS } from './constants/chains'; -import { REFRESH_INTERVAL_MS } from './constants'; -import { - BRIDGE_CONTROLLER_NAME, - DEFAULT_BRIDGE_CONTROLLER_STATE, - METABRIDGE_CHAIN_TO_ADDRESS_MAP, -} from './constants'; import type { BridgeControllerMessenger } from './types'; +import { sumHexes } from './utils'; +import { hasSufficientBalance } from './utils/balance'; +import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; +import { isValidQuoteRequest } from './utils/quote'; const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { bridgeState: { diff --git a/packages/bridge-controller/src/constants/chains.ts b/packages/bridge-controller/src/constants/chains.ts index af16bcbad1e..ae79b6c1929 100644 --- a/packages/bridge-controller/src/constants/chains.ts +++ b/packages/bridge-controller/src/constants/chains.ts @@ -155,4 +155,4 @@ export const NETWORK_TO_NAME_MAP = { [CHAIN_IDS.METACHAIN_ONE]: METACHAIN_ONE_DISPLAY_NAME, [CHAIN_IDS.LISK]: LISK_DISPLAY_NAME, [CHAIN_IDS.LISK_SEPOLIA]: LISK_SEPOLIA_DISPLAY_NAME, -} as const; \ No newline at end of file +} as const; diff --git a/packages/bridge-controller/src/constants/index.ts b/packages/bridge-controller/src/constants/index.ts index 2d9e58a3086..218a4ee9335 100644 --- a/packages/bridge-controller/src/constants/index.ts +++ b/packages/bridge-controller/src/constants/index.ts @@ -1,7 +1,9 @@ -import { zeroAddress } from 'ethereumjs-util'; import type { Hex } from '@metamask/utils'; +import { zeroAddress } from 'ethereumjs-util'; + import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './chains'; -import { BridgeFeatureFlagsKey, BridgeControllerState } from '../types'; +import type { BridgeControllerState } from '../types'; +import { BridgeFeatureFlagsKey } from '../types'; // TODO read from feature flags export const ALLOWED_BRIDGE_CHAIN_IDS = [ @@ -78,4 +80,4 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, -}; \ No newline at end of file +}; diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 6189be45b0a..be67ca8ccd8 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,4 +1,4 @@ -import { CHAIN_IDS } from "./chains"; +import { CHAIN_IDS } from './chains'; export type SwapsTokenObject = { /** @@ -141,4 +141,4 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, [CHAIN_IDS.LINEA_MAINNET]: LINEA_SWAPS_TOKEN_OBJECT, [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, -} as const; \ No newline at end of file +} as const; diff --git a/packages/bridge-controller/src/test/provider.ts b/packages/bridge-controller/src/test/provider.ts index 8a9fc40b2ee..467bf6d6ba2 100644 --- a/packages/bridge-controller/src/test/provider.ts +++ b/packages/bridge-controller/src/test/provider.ts @@ -1,7 +1,7 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; export const createMockProvider = () => { const engine = new JsonRpcEngine(); return new SafeEventEmitterProvider({ engine }); -} \ No newline at end of file +}; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 3fa9ddc6e33..2cd63df8903 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -1,21 +1,21 @@ -import type { Hex } from '@metamask/utils'; -import type { BigNumber } from 'bignumber.js'; -import { +import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import type { ControllerStateChangeEvent, RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; -import { +import type { NetworkControllerFindNetworkClientIdByChainIdAction, NetworkControllerGetSelectedNetworkClientAction, } from '@metamask/network-controller'; -import BridgeController from './bridge-controller'; -import { BRIDGE_CONTROLLER_NAME } from './constants'; +import type { Hex } from '@metamask/utils'; +import type { BigNumber } from 'bignumber.js'; + +import type BridgeController from './bridge-controller'; +import type { BRIDGE_CONTROLLER_NAME } from './constants'; /** * The types of assets that a user can send * - * @type {AssetTypes} */ export enum AssetType { /** The native asset for the current network, such as ETH */ diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index aab3ff34504..ba5b6b86c55 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -11,5 +11,5 @@ { "path": "../network-controller" }, { "path": "../transaction-controller" } ], - "include": ["../../types", "./src", "tsconfig.build.json", "tsconfig.json"] + "include": ["../../types", "./src"] } From 482ceaf592af60ef1d97dc7ac93fedb296042bfb Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:55:47 -0500 Subject: [PATCH 04/94] chore: clean up --- .../bridge-controller/src/utils/validators.ts | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 9ab432bdad1..56d8f93a47a 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -1,7 +1,15 @@ -import { isStrictHexString } from '@metamask/utils'; import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; -import { BridgeAsset, BridgeFlag, FeatureFlagResponse, FeeData, Quote, QuoteResponse, TxData } from '../types'; -import { SwapsTokenObject } from '../constants/tokens'; +import { isStrictHexString } from '@metamask/utils'; + +import type { SwapsTokenObject } from '../constants/tokens'; +import type { + FeatureFlagResponse, + FeeData, + Quote, + QuoteResponse, + TxData, +} from '../types'; +import { BridgeFlag } from '../types'; export const truthyString = (string: string) => Boolean(string?.length); export const truthyDigitString = (string: string) => @@ -33,18 +41,25 @@ export const validateData = ( const valid = isValidObject(object) && - types.some((_type) => typeof object[propertyString as keyof typeof object] === _type) && + types.some( + (_type) => + typeof object[propertyString as keyof typeof object] === _type, + ) && (!validator || validator(object[propertyString as keyof typeof object])); if (!valid && logError) { - const value = isValidObject(object) ? object[propertyString as keyof typeof object] : undefined; - const type = isValidObject(object) ? typeof object[propertyString as keyof typeof object] : 'undefined'; + const value = isValidObject(object) + ? object[propertyString as keyof typeof object] + : undefined; + const typeString = isValidObject(object) + ? typeof object[propertyString as keyof typeof object] + : 'undefined'; console.error( `response to GET ${urlUsed} invalid for property ${String(property)}; value was:`, value, '| type was: ', - type, + typeString, ); } return valid; @@ -129,7 +144,11 @@ export const QUOTE_VALIDATORS: Validator[] = [ ]; export const FEE_DATA_VALIDATORS: Validator[] = [ - { property: 'amount', type: 'string', validator: (v: unknown) => truthyDigitString(String(v)) }, + { + property: 'amount', + type: 'string', + validator: (v: unknown) => truthyDigitString(String(v)), + }, { property: 'asset', type: 'object', validator: isValidObject }, ]; From 934d937bee95120b7040e58d7ee29a985333d0eb Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:58:51 -0500 Subject: [PATCH 05/94] chore: add jest environment jsdom package --- packages/bridge-controller/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 70748b1fba2..b06bbe0cfd4 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -67,6 +67,7 @@ "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", "lodash": "^4.17.21", "nock": "^13.5.4", "ts-jest": "^27.1.4", From e95b7c1f166cc51b2f379cd089c6c2c52ac30941 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:10:17 -0500 Subject: [PATCH 06/94] chore: fix lint issues --- .../src/bridge-controller.test.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 5ca7dd18523..787e9b4a7cc 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1,16 +1,20 @@ -import nock from 'nock'; import { bigIntToHex } from '@metamask/utils'; -import { BRIDGE_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; +import nock from 'nock'; + +import BridgeController from './bridge-controller'; +import { + BRIDGE_API_BASE_URL, + DEFAULT_BRIDGE_CONTROLLER_STATE, +} from './constants'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; -import { flushPromises } from './test/utils'; -import * as fetchUtils from './utils/fetch'; -import * as balanceUtils from './utils/balance'; import mockBridgeQuotesErc20Native from './test/mock-quotes-erc20-native.json'; -import mockBridgeQuotesNativeErc20 from './test/mock-quotes-native-erc20.json'; import mockBridgeQuotesNativeErc20Eth from './test/mock-quotes-native-erc20-eth.json'; -import BridgeController from './bridge-controller'; -import { BridgeControllerMessenger, QuoteResponse } from './types'; +import mockBridgeQuotesNativeErc20 from './test/mock-quotes-native-erc20.json'; +import { flushPromises } from './test/utils'; +import type { BridgeControllerMessenger, QuoteResponse } from './types'; +import * as balanceUtils from './utils/balance'; +import * as fetchUtils from './utils/fetch'; const EMPTY_INIT_STATE = { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -314,9 +318,9 @@ describe('BridgeController', function () { }, expect.any(AbortSignal), ); - expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( - undefined, - ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toBeUndefined(); expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ @@ -465,9 +469,9 @@ describe('BridgeController', function () { }, expect.any(AbortSignal), ); - expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( - undefined, - ); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + ).toBeUndefined(); expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ @@ -660,7 +664,7 @@ describe('BridgeController', function () { ); expect( bridgeController.state.bridgeState.quotesLastFetched, - ).toStrictEqual(undefined); + ).toBeUndefined(); expect(bridgeController.state.bridgeState).toEqual( expect.objectContaining({ From c97c078eaa83484379afb05bbf7df016f41953cd Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:12:05 -0500 Subject: [PATCH 07/94] chore: bump version of controller utils --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index b06bbe0cfd4..a73e4fb52c0 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -51,7 +51,7 @@ "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.2", "@metamask/base-controller": "^7.0.0", - "@metamask/controller-utils": "^11.4.0", + "@metamask/controller-utils": "^11.4.5", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/network-controller": "^22.1.1", "@metamask/polling-controller": "^12.0.1", From 7e197cd7a0374bb149d06e690a6e066acb5fa9c0 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:31:00 -0500 Subject: [PATCH 08/94] chore: fix lint issue --- .../bridge-controller/src/utils/balance.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts index 6ce64a4f393..c78d99d6cd3 100644 --- a/packages/bridge-controller/src/utils/balance.ts +++ b/packages/bridge-controller/src/utils/balance.ts @@ -1,24 +1,24 @@ +import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Provider } from '@metamask/network-controller'; -import { Hex } from '@metamask/utils'; -import { Contract } from '@ethersproject/contracts'; +import type { Hex } from '@metamask/utils'; import { zeroAddress } from 'ethereumjs-util'; +import type { BigNumber } from 'ethers'; import { getAddress } from 'ethers/lib/utils'; -import { abiERC20 } from '@metamask/metamask-eth-abis'; -import { BigNumber } from 'ethers'; -export async function fetchTokenBalance( +export const fetchTokenBalance = async ( address: string, userAddress: string, provider: Provider, -): Promise { +): Promise => { const ethersProvider = new Web3Provider(provider); const tokenContract = new Contract(address, abiERC20, ethersProvider); const tokenBalancePromise = tokenContract ? tokenContract.balanceOf(userAddress) : Promise.resolve(); return await tokenBalancePromise; -} +}; export const calcLatestSrcBalance = async ( provider: Provider, @@ -29,7 +29,7 @@ export const calcLatestSrcBalance = async ( if (tokenAddress && chainId) { if (tokenAddress === zeroAddress()) { const ethersProvider = new Web3Provider(provider); - return await ethersProvider.getBalance(getAddress(selectedAddress)) + return await ethersProvider.getBalance(getAddress(selectedAddress)); } return await fetchTokenBalance(tokenAddress, selectedAddress, provider); } @@ -50,7 +50,5 @@ export const hasSufficientBalance = async ( chainId, ); - return ( - srcTokenBalance?.gte(fromTokenAmount) ?? false - ); + return srcTokenBalance?.gte(fromTokenAmount) ?? false; }; From 1dc1bb3fe98c2c8c1b5626140193f159fcb89292 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:33:56 -0500 Subject: [PATCH 09/94] fix: broken tests --- .../src/utils/balance.test.ts | 73 +++++++++++++------ 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts index f3a7b0159c2..e06cbb60389 100644 --- a/packages/bridge-controller/src/utils/balance.test.ts +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -1,25 +1,18 @@ -import { BigNumber } from 'ethers'; -import { zeroAddress } from 'ethereumjs-util'; import { Contract } from '@ethersproject/contracts'; -import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { Web3Provider } from '@ethersproject/providers'; +import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { zeroAddress } from 'ethereumjs-util'; +import { BigNumber } from 'ethers'; + import * as balanceUtils from './balance'; import { createMockProvider } from '../test/provider'; declare global { + // eslint-disable-next-line no-var var ethereumProvider: SafeEventEmitterProvider; } -const mockGetBalance = jest.fn(); -jest.mock('@ethersproject/providers', () => { - return { - Web3Provider: jest.fn().mockImplementation(() => { - return { - getBalance: mockGetBalance, - }; - }), - }; -}); - +jest.mock('@ethersproject/providers'); jest.mock('@ethersproject/contracts'); describe('balance', () => { @@ -30,7 +23,9 @@ describe('balance', () => { describe('calcLatestSrcBalance', () => { it('should return the ERC20 token balance', async () => { - const mockBalanceOf = jest.fn().mockResolvedValueOnce(BigNumber.from('100')); + const mockBalanceOf = jest + .fn() + .mockResolvedValueOnce(BigNumber.from('100')); (Contract as unknown as jest.Mock).mockImplementation(() => ({ balanceOf: mockBalanceOf, })); @@ -44,12 +39,17 @@ describe('balance', () => { ), ).toStrictEqual(BigNumber.from(100)); expect(mockBalanceOf).toHaveBeenCalledTimes(1); - expect(mockBalanceOf).toHaveBeenCalledWith( - '0x123', - ); + expect(mockBalanceOf).toHaveBeenCalledWith('0x123'); }); it('should return the native asset balance', async () => { + const mockGetBalance = jest.fn(); + (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + mockGetBalance.mockImplementation(() => { return BigNumber.from(100); }); @@ -69,7 +69,17 @@ describe('balance', () => { }); it('should return undefined if token address and chainId are undefined', async () => { - const mockFetchTokenBalance = jest.spyOn(balanceUtils, 'fetchTokenBalance'); + const mockGetBalance = jest.fn(); + (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + + const mockFetchTokenBalance = jest.spyOn( + balanceUtils, + 'fetchTokenBalance', + ); expect( await balanceUtils.calcLatestSrcBalance( global.ethereumProvider, @@ -77,7 +87,7 @@ describe('balance', () => { undefined as never, undefined as never, ), - ).toStrictEqual(undefined); + ).toBeUndefined(); expect(mockFetchTokenBalance).not.toHaveBeenCalled(); expect(mockGetBalance).not.toHaveBeenCalled(); }); @@ -85,11 +95,20 @@ describe('balance', () => { describe('hasSufficientBalance', () => { it('should return true if user has sufficient balance', async () => { + const mockGetBalance = jest.fn(); + (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + mockGetBalance.mockImplementation(() => { return BigNumber.from('10000000000000000000'); }); - const mockBalanceOf = jest.fn().mockResolvedValueOnce(BigNumber.from('10000000000000000001')); + const mockBalanceOf = jest + .fn() + .mockResolvedValueOnce(BigNumber.from('10000000000000000001')); (Contract as unknown as jest.Mock).mockImplementation(() => ({ balanceOf: mockBalanceOf, })); @@ -116,10 +135,20 @@ describe('balance', () => { }); it('should return false if user has native assets but insufficient ERC20 src tokens', async () => { + const mockGetBalance = jest.fn(); + (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + return { + getBalance: mockGetBalance, + }; + }); + mockGetBalance.mockImplementation(() => { return BigNumber.from('10000000000000000000'); }); - const mockFetchTokenBalance = jest.spyOn(balanceUtils, 'fetchTokenBalance'); + const mockFetchTokenBalance = jest.spyOn( + balanceUtils, + 'fetchTokenBalance', + ); mockFetchTokenBalance.mockResolvedValueOnce( BigNumber.from('9000000000000000000'), ); From 6ceefa4c20ed4fb022256f5c05640ebdf66086cc Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:56:40 -0500 Subject: [PATCH 10/94] fix: broken tests --- .../src/bridge-controller.test.ts | 31 ++++++++++--------- .../src/bridge-controller.ts | 6 +++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 787e9b4a7cc..a5cbc0d8dc3 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1,3 +1,4 @@ +import type { Hex } from '@metamask/utils'; import { bigIntToHex } from '@metamask/utils'; import nock from 'nock'; @@ -165,8 +166,8 @@ describe('BridgeController', function () { ); }); - it('updateBridgeQuoteRequestParams should update the quoteRequest state', function () { - bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); + it('updateBridgeQuoteRequestParams should update the quoteRequest state', async function () { + await bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ srcChainId: 1, slippage: 0.5, @@ -174,7 +175,7 @@ describe('BridgeController', function () { walletAddress: undefined, }); - bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); + await bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ destChainId: 10, slippage: 0.5, @@ -182,7 +183,9 @@ describe('BridgeController', function () { walletAddress: undefined, }); - bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined }); + await bridgeController.updateBridgeQuoteRequestParams({ + destChainId: undefined, + }); expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ destChainId: undefined, slippage: 0.5, @@ -190,7 +193,7 @@ describe('BridgeController', function () { walletAddress: undefined, }); - bridgeController.updateBridgeQuoteRequestParams({ + await bridgeController.updateBridgeQuoteRequestParams({ srcTokenAddress: undefined, }); expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ @@ -199,7 +202,7 @@ describe('BridgeController', function () { walletAddress: undefined, }); - bridgeController.updateBridgeQuoteRequestParams({ + await bridgeController.updateBridgeQuoteRequestParams({ srcTokenAmount: '100000', destTokenAddress: '0x123', slippage: 0.5, @@ -214,7 +217,7 @@ describe('BridgeController', function () { walletAddress: undefined, }); - bridgeController.updateBridgeQuoteRequestParams({ + await bridgeController.updateBridgeQuoteRequestParams({ srcTokenAddress: '0x2ABC', }); expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ @@ -516,7 +519,7 @@ describe('BridgeController', function () { expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); }); - it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () { + it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', async function () { const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); messengerMock.call.mockReturnValue({ @@ -524,7 +527,7 @@ describe('BridgeController', function () { provider: jest.fn(), } as never); - bridgeController.updateBridgeQuoteRequestParams({ + await bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1, destChainId: 10, srcTokenAddress: '0x0000000000000000000000000000000000000000', @@ -570,28 +573,28 @@ describe('BridgeController', function () { it.each([ [ 'should append l1GasFees if srcChain is 10 and srcToken is erc20', - mockBridgeQuotesErc20Native, + mockBridgeQuotesErc20Native as QuoteResponse[], bigIntToHex(BigInt('2608710388388') * 2n), 12, ], [ 'should append l1GasFees if srcChain is 10 and srcToken is native', - mockBridgeQuotesNativeErc20, + mockBridgeQuotesNativeErc20 as unknown as QuoteResponse[], bigIntToHex(BigInt('2608710388388') * 2n), 2, ], [ 'should not append l1GasFees if srcChain is not 10', - mockBridgeQuotesNativeErc20Eth, + mockBridgeQuotesNativeErc20Eth as unknown as QuoteResponse[], undefined, 0, ], ])( 'updateBridgeQuoteRequestParams: %s', async ( - _: string, + _testTitle: string, quoteResponse: QuoteResponse[], - l1GasFeesInHexWei: string, + l1GasFeesInHexWei: Hex | undefined, getLayer1GasFeeMockCallCount: number, ) => { jest.useFakeTimers(); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 9932ae43b16..255e0299068 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -219,7 +219,11 @@ export default class BridgeController extends StaticIntervalPollingController
Date: Wed, 5 Feb 2025 14:56:50 -0500 Subject: [PATCH 11/94] chore: remove unused var --- packages/bridge-controller/src/bridge-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 255e0299068..9eab5195a09 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -7,7 +7,7 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { NetworkClientId } from '@metamask/network-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { TransactionParams } from '@metamask/transaction-controller'; -import { add0x, numberToHex } from '@metamask/utils'; +import { numberToHex } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { REFRESH_INTERVAL_MS } from './constants'; From ecde95be81b895a3d1f712e8f58496c649bcd9e9 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:21:21 -0500 Subject: [PATCH 12/94] fix: broken tests --- .../src/bridge-controller.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index a5cbc0d8dc3..332fc535324 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1,3 +1,4 @@ +import { Contract } from '@ethersproject/contracts'; import type { Hex } from '@metamask/utils'; import { bigIntToHex } from '@metamask/utils'; import nock from 'nock'; @@ -28,13 +29,7 @@ const messengerMock = { publish: jest.fn(), } as unknown as jest.Mocked; -jest.mock('@ethersproject/contracts', () => { - return { - Contract: jest.fn(() => ({ - allowance: jest.fn(() => '100000000000000000000'), - })), - }; -}); +jest.mock('@ethersproject/contracts'); jest.mock('@ethersproject/providers', () => { return { @@ -557,6 +552,10 @@ describe('BridgeController', function () { describe('getBridgeERC20Allowance', () => { it('should return the atomic allowance of the ERC20 token contract', async () => { + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + allowance: jest.fn(() => '100000000000000000000'), + })); + messengerMock.call.mockReturnValue({ address: '0x123', provider: jest.fn(), @@ -580,7 +579,7 @@ describe('BridgeController', function () { [ 'should append l1GasFees if srcChain is 10 and srcToken is native', mockBridgeQuotesNativeErc20 as unknown as QuoteResponse[], - bigIntToHex(BigInt('2608710388388') * 2n), + bigIntToHex(BigInt('2608710388388')), 2, ], [ From 835ac4402f3c2a2a9c4f7842d98409ab8c49f82f Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:23:17 -0500 Subject: [PATCH 13/94] chore: adjust coverage --- packages/bridge-controller/jest.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js index a226e79eb7f..1ccd267b2e2 100644 --- a/packages/bridge-controller/jest.config.js +++ b/packages/bridge-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 91.07, - functions: 97.51, - lines: 98.12, - statements: 98.03, + branches: 85, + functions: 95, + lines: 95, + statements: 95, }, }, From 7b83a3bb549983d93b458b527b6d6393df11ae59 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:23:45 -0500 Subject: [PATCH 14/94] chore: fix lint issues --- packages/bridge-controller/src/utils/index.ts | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/index.ts index 23c31e912c1..55e79513ce5 100644 --- a/packages/bridge-controller/src/utils/index.ts +++ b/packages/bridge-controller/src/utils/index.ts @@ -1,10 +1,8 @@ import { Contract } from '@ethersproject/contracts'; -import { Hex } from '@metamask/utils'; import { abiERC20 } from '@metamask/metamask-eth-abis'; -import { - ETH_USDT_ADDRESS, - METABRIDGE_ETHEREUM_ADDRESS, -} from '../constants'; +import type { Hex } from '@metamask/utils'; + +import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS } from '../constants'; import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; @@ -35,36 +33,46 @@ export const sumHexes = (...hexStrings: string[]): Hex => { const sum = hexStrings.reduce((acc, hex) => acc + BigInt(hex), BigInt(0)); return `0x${sum.toString(16)}`; -} +}; /** * Checks whether the provided address is strictly equal to the address for * the default swaps token of the provided chain. * - * @param {string} address - The string to compare to the default token address - * @param {Hex} chainId - The hex encoded chain ID of the default swaps token to check - * @returns {boolean} Whether the address is the provided chain's default token address + * @param address - The string to compare to the default token address + * @param chainId - The hex encoded chain ID of the default swaps token to check + * @returns Whether the address is the provided chain's default token address */ export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { if (!address || !chainId) { return false; } - return address === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP]?.address; -} + return ( + address === + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]?.address + ); +}; /** * Checks whether the provided symbol is strictly equal to the symbol for * the default swaps token of the provided chain. * - * @param {string} symbol - The string to compare to the default token symbol - * @param {Hex} chainId - The hex encoded chain ID of the default swaps token to check - * @returns {boolean} Whether the symbol is the provided chain's default token symbol + * @param symbol - The string to compare to the default token symbol + * @param chainId - The hex encoded chain ID of the default swaps token to check + * @returns Whether the symbol is the provided chain's default token symbol */ export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { if (!symbol || !chainId) { return false; } - return symbol === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP]?.symbol; -} + return ( + symbol === + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]?.symbol + ); +}; From 40770a682ac90b91314f688e3bb6f349c8f4c040 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:24:02 -0500 Subject: [PATCH 15/94] chore: fix lint issues --- packages/bridge-controller/src/utils/index.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts index c6fc9eebb07..cc04085988a 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -1,4 +1,4 @@ -import { sumHexes } from './index'; +import { sumHexes } from '.'; describe('Bridge utils', () => { beforeEach(() => { @@ -9,30 +9,30 @@ describe('Bridge utils', () => { it('returns 0x0 for empty input', () => { expect(sumHexes()).toBe('0x0'); }); - + it('returns same value for single input', () => { expect(sumHexes('0xff')).toBe('0xff'); expect(sumHexes('0x0')).toBe('0x0'); expect(sumHexes('0x1')).toBe('0x1'); }); - + it('correctly sums two hex values', () => { expect(sumHexes('0x1', '0x1')).toBe('0x2'); expect(sumHexes('0xff', '0x1')).toBe('0x100'); expect(sumHexes('0x0', '0xff')).toBe('0xff'); }); - + it('correctly sums multiple hex values', () => { expect(sumHexes('0x1', '0x2', '0x3')).toBe('0x6'); expect(sumHexes('0xff', '0xff', '0x2')).toBe('0x200'); expect(sumHexes('0x0', '0x0', '0x0')).toBe('0x0'); }); - + it('handles large numbers', () => { expect(sumHexes('0xffffffff', '0x1')).toBe('0x100000000'); expect(sumHexes('0xffffffff', '0xffffffff')).toBe('0x1fffffffe'); }); - + it('throws for invalid hex strings', () => { expect(() => sumHexes('0xg')).toThrow(); }); From 386e48109ec709b87bb822a8601f054ab7140ee7 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:28:48 -0500 Subject: [PATCH 16/94] chore: improve test --- packages/bridge-controller/src/utils/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts index cc04085988a..d16b7756c89 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -34,7 +34,7 @@ describe('Bridge utils', () => { }); it('throws for invalid hex strings', () => { - expect(() => sumHexes('0xg')).toThrow(); + expect(() => sumHexes('0xg')).toThrow('Cannot convert 0xg to a BigInt'); }); }); }); From f4f6991329751458dea5c6141ac3896359f50898 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:33:09 -0500 Subject: [PATCH 17/94] chore: add tests --- .../bridge-controller/src/utils/index.test.ts | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts index d16b7756c89..cf5609c15c7 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -1,4 +1,17 @@ -import { sumHexes } from '.'; +import { Contract } from '@ethersproject/contracts'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; +import type { Hex } from '@metamask/utils'; + +import { + getEthUsdtResetData, + isEthUsdt, + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, + sumHexes, +} from '.'; +import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS } from '../constants'; +import { CHAIN_IDS } from '../constants/chains'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; describe('Bridge utils', () => { beforeEach(() => { @@ -37,4 +50,86 @@ describe('Bridge utils', () => { expect(() => sumHexes('0xg')).toThrow('Cannot convert 0xg to a BigInt'); }); }); + + describe('getEthUsdtResetData', () => { + it('returns correct encoded function data for USDT approval reset', () => { + const expectedInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) + .interface; + const expectedData = expectedInterface.encodeFunctionData('approve', [ + METABRIDGE_ETHEREUM_ADDRESS, + '0', + ]); + + expect(getEthUsdtResetData()).toBe(expectedData); + }); + }); + + describe('isEthUsdt', () => { + it('returns true for ETH USDT address on mainnet', () => { + expect(isEthUsdt(CHAIN_IDS.MAINNET, ETH_USDT_ADDRESS)).toBe(true); + expect(isEthUsdt(CHAIN_IDS.MAINNET, ETH_USDT_ADDRESS.toUpperCase())).toBe( + true, + ); + }); + + it('returns false for non-mainnet chain', () => { + expect(isEthUsdt(CHAIN_IDS.GOERLI, ETH_USDT_ADDRESS)).toBe(false); + }); + + it('returns false for different address on mainnet', () => { + expect(isEthUsdt(CHAIN_IDS.MAINNET, METABRIDGE_ETHEREUM_ADDRESS)).toBe( + false, + ); + }); + }); + + describe('isSwapsDefaultTokenAddress', () => { + it('returns true for default token address of given chain', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + const defaultToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + expect(isSwapsDefaultTokenAddress(defaultToken.address, chainId)).toBe( + true, + ); + }); + + it('returns false for non-default token address', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenAddress('0x1234', chainId)).toBe(false); + }); + + it('returns false for invalid inputs', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenAddress('', chainId)).toBe(false); + expect(isSwapsDefaultTokenAddress('0x1234', '' as Hex)).toBe(false); + }); + }); + + describe('isSwapsDefaultTokenSymbol', () => { + it('returns true for default token symbol of given chain', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + const defaultToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; + + expect(isSwapsDefaultTokenSymbol(defaultToken.symbol, chainId)).toBe( + true, + ); + }); + + it('returns false for non-default token symbol', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenSymbol('FAKE', chainId)).toBe(false); + }); + + it('returns false for invalid inputs', () => { + const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; + expect(isSwapsDefaultTokenSymbol('', chainId)).toBe(false); + expect(isSwapsDefaultTokenSymbol('ETH', '' as Hex)).toBe(false); + }); + }); }); From 4c01fe3adb7261539e713011c1f3c458c25507f5 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:45:40 -0500 Subject: [PATCH 18/94] chore: add test --- .../src/utils/balance.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts index e06cbb60389..6b1b44a4404 100644 --- a/packages/bridge-controller/src/utils/balance.test.ts +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -163,5 +163,27 @@ describe('balance', () => { ), ).toBe(false); }); + + it('should return false if source token balance is undefined', async () => { + const mockBalanceOf = jest.fn().mockResolvedValueOnce(undefined); + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + expect( + await balanceUtils.hasSufficientBalance( + global.ethereumProvider, + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', + '10000000000000000000', + '0x1', + ), + ).toBe(false); + + expect(mockBalanceOf).toHaveBeenCalledTimes(1); + expect(mockBalanceOf).toHaveBeenCalledWith( + '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', + ); + }); }); }); From c8646058e19aab135fc501ac6433fbb0306765ec Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:55:14 -0500 Subject: [PATCH 19/94] fix: incorrect import --- packages/bridge-controller/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 2cd63df8903..7196ce74fbf 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -1,7 +1,7 @@ import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import type { ControllerStateChangeEvent, - RestrictedControllerMessenger, + RestrictedMessenger, } from '@metamask/base-controller'; import type { NetworkControllerFindNetworkClientIdByChainIdAction, @@ -251,7 +251,7 @@ type AllowedEvents = never; /** * The messenger for the BridgeController. */ -export type BridgeControllerMessenger = RestrictedControllerMessenger< +export type BridgeControllerMessenger = RestrictedMessenger< typeof BRIDGE_CONTROLLER_NAME, | BridgeControllerActions | AccountsControllerGetSelectedAccountAction From e8ec4b069f757f4fc0b4fd5f8d4aa959868f3339 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:56:56 -0500 Subject: [PATCH 20/94] chore: update changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 11bddf32c5b..bd379b7049f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -6,3 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added + +- Initial release From ca667019320c9a4ede001de0791dacf0df128c0b Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:10:41 -0500 Subject: [PATCH 21/94] fix: lint errors --- .../src/bridge-controller.test.ts | 8 +- packages/bridge-controller/src/test/utils.ts | 5 +- packages/bridge-controller/src/utils/fetch.ts | 74 +++++++++++-------- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 332fc535324..5f752c3ccb8 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -378,10 +378,10 @@ describe('BridgeController', function () { quotesRefreshCount: 3, }), ); - secondFetchTime && - expect( - bridgeController.state.bridgeState.quotesLastFetched, - ).toBeGreaterThan(secondFetchTime); + expect( + bridgeController.state.bridgeState.quotesLastFetched, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ).toBeGreaterThan(secondFetchTime!); expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); diff --git a/packages/bridge-controller/src/test/utils.ts b/packages/bridge-controller/src/test/utils.ts index 28deec265fe..065602096c5 100644 --- a/packages/bridge-controller/src/test/utils.ts +++ b/packages/bridge-controller/src/test/utils.ts @@ -1,3 +1,2 @@ -export function flushPromises() { - return new Promise(jest.requireActual('timers').setImmediate); -} +export const flushPromises = () => + new Promise((resolve) => jest.requireActual('timers').setImmediate(resolve)); diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 150b7c466c5..6dd0a55cc0c 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -1,45 +1,44 @@ -import { Duration, Hex, hexToNumber, numberToHex } from '@metamask/utils'; +import { handleFetch } from '@metamask/controller-utils'; +import type { Hex } from '@metamask/utils'; +import { hexToNumber, numberToHex } from '@metamask/utils'; + +import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol } from '.'; +import { + FEATURE_FLAG_VALIDATORS, + QUOTE_VALIDATORS, + TX_DATA_VALIDATORS, + TOKEN_VALIDATORS, + validateResponse, + QUOTE_RESPONSE_VALIDATORS, + FEE_DATA_VALIDATORS, +} from './validators'; import { BRIDGE_API_BASE_URL, BRIDGE_CLIENT_ID, REFRESH_INTERVAL_MS, } from '../constants'; -import { - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, - SwapsTokenObject, -} from '../constants/tokens'; -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from './index'; -import { - BridgeFlag, +import type { SwapsTokenObject } from '../constants/tokens'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; +import type { FeatureFlagResponse, FeeData, - FeeType, Quote, QuoteRequest, QuoteResponse, TxData, - BridgeFeatureFlagsKey, BridgeFeatureFlags, } from '../types'; -import { - FEATURE_FLAG_VALIDATORS, - QUOTE_VALIDATORS, - TX_DATA_VALIDATORS, - TOKEN_VALIDATORS, - validateResponse, - QUOTE_RESPONSE_VALIDATORS, - FEE_DATA_VALIDATORS, -} from './validators'; -import { handleFetch } from '@metamask/controller-utils'; - +import { BridgeFlag, FeeType, BridgeFeatureFlagsKey } from '../types'; const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; // TODO put this back in once we have a fetchWithCache equivalent // const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; +/** + * Fetches the bridge feature flags + * + * @returns The bridge feature flags + */ export async function fetchBridgeFeatureFlags(): Promise { const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; const rawFeatureFlags = await handleFetch(url, { @@ -81,7 +80,10 @@ export async function fetchBridgeFeatureFlags(): Promise { /** * Returns a list of enabled (unblocked) tokens - * */ + * + * @param chainId - The chain ID to fetch tokens for + * @returns A list of enabled (unblocked) tokens + */ export async function fetchBridgeTokens( chainId: Hex, ): Promise> { @@ -91,7 +93,7 @@ export async function fetchBridgeTokens( )}`; // TODO we will need to cache these. In Extension fetchWithCache is used. This is due to the following: - // If we allow selecting dest networks which the user has not imported, + // If we allow selecting dest networks which the user has not imported, // note that the Assets controller won't be able to provide tokens. In extension we fetch+cache the token list from bridge-api to handle this const tokens = await handleFetch(url, { headers: CLIENT_ID_HEADER, @@ -122,6 +124,12 @@ export async function fetchBridgeTokens( } // Returns a list of bridge tx quotes +/** + * + * @param request - The quote request + * @param signal - The abort signal + * @returns A list of bridge tx quotes + */ export async function fetchBridgeQuotes( request: QuoteRequest, signal: AbortSignal, @@ -152,8 +160,16 @@ export async function fetchBridgeQuotes( url, ) && validateResponse(QUOTE_VALIDATORS, quote, url) && - validateResponse(TOKEN_VALIDATORS, quote.srcAsset, url) && - validateResponse(TOKEN_VALIDATORS, quote.destAsset, url) && + validateResponse( + TOKEN_VALIDATORS, + quote.srcAsset, + url, + ) && + validateResponse( + TOKEN_VALIDATORS, + quote.destAsset, + url, + ) && validateResponse(TX_DATA_VALIDATORS, trade, url) && validateResponse( FEE_DATA_VALIDATORS, @@ -166,4 +182,4 @@ export async function fetchBridgeQuotes( ); }); return filteredQuotes; -} \ No newline at end of file +} From 318ae4ea72fec64adbe83d3ed7ed1d60429ffd40 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:15:14 -0500 Subject: [PATCH 22/94] chore: remove unneeded code --- packages/bridge-controller/jest.config.js | 3 --- packages/bridge-controller/jest.environment.js | 17 ----------------- 2 files changed, 20 deletions(-) delete mode 100644 packages/bridge-controller/jest.environment.js diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js index 1ccd267b2e2..eae426c4c23 100644 --- a/packages/bridge-controller/jest.config.js +++ b/packages/bridge-controller/jest.config.js @@ -23,7 +23,4 @@ module.exports = merge(baseConfig, { statements: 95, }, }, - - // We rely on `window` to make requests - testEnvironment: '/jest.environment.js', }); diff --git a/packages/bridge-controller/jest.environment.js b/packages/bridge-controller/jest.environment.js deleted file mode 100644 index b77d5478109..00000000000 --- a/packages/bridge-controller/jest.environment.js +++ /dev/null @@ -1,17 +0,0 @@ -const JSDOMEnvironment = require('jest-environment-jsdom'); - -// Custom test environment copied from https://github.com/jsdom/jsdom/issues/2524 -// in order to add TextEncoder to jsdom. TextEncoder is expected by jose. - -module.exports = class CustomTestEnvironment extends JSDOMEnvironment { - async setup() { - await super.setup(); - if (typeof this.global.TextEncoder === 'undefined') { - const { TextEncoder, TextDecoder } = require('util'); - this.global.TextEncoder = TextEncoder; - this.global.TextDecoder = TextDecoder; - this.global.ArrayBuffer = ArrayBuffer; - this.global.Uint8Array = Uint8Array; - } - } -}; From 4a7f5261d82221c0cefea281663387ea7f483c85 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:16:23 -0500 Subject: [PATCH 23/94] fix: lint errors --- .../src/bridge-controller.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 5f752c3ccb8..3682eda965a 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -331,7 +331,7 @@ describe('BridgeController', function () { // After first fetch jest.advanceTimersByTime(10000); await flushPromises(); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: mockBridgeQuotesNativeErc20Eth, @@ -345,7 +345,7 @@ describe('BridgeController', function () { // After 2nd fetch jest.advanceTimersByTime(50000); await flushPromises(); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [ @@ -366,7 +366,7 @@ describe('BridgeController', function () { jest.advanceTimersByTime(50000); await flushPromises(); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [ @@ -471,7 +471,7 @@ describe('BridgeController', function () { bridgeController.state.bridgeState.quotesLastFetched, ).toBeUndefined(); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: true }, quotes: [], @@ -482,7 +482,7 @@ describe('BridgeController', function () { // After first fetch jest.advanceTimersByTime(10000); await flushPromises(); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: true }, quotes: mockBridgeQuotesNativeErc20Eth, @@ -499,7 +499,7 @@ describe('BridgeController', function () { jest.advanceTimersByTime(50000); await flushPromises(); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: true }, quotes: mockBridgeQuotesNativeErc20Eth, @@ -668,7 +668,7 @@ describe('BridgeController', function () { bridgeController.state.bridgeState.quotesLastFetched, ).toBeUndefined(); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: true }, quotes: [], @@ -680,7 +680,7 @@ describe('BridgeController', function () { jest.advanceTimersByTime(1500); await flushPromises(); const { quotes } = bridgeController.state.bridgeState; - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: true }, quotesLoadingStatus: 1, From 63d7a30ddff51bb465b65084cdf901774587bb3c Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:23:45 -0500 Subject: [PATCH 24/94] fix: lint errors --- .../src/bridge-controller.test.ts | 22 +++--- .../src/bridge-controller.ts | 8 +- .../bridge-controller/src/utils/fetch.test.ts | 73 ++++++++++++------- 3 files changed, 59 insertions(+), 44 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 3682eda965a..c4fc1dcce2f 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -263,7 +263,7 @@ describe('BridgeController', function () { }); fetchBridgeQuotesSpy.mockImplementationOnce(async () => { - return await new Promise((_, reject) => { + return await new Promise((_resolve, reject) => { return setTimeout(() => { reject(new Error('Network error')); }, 10000); @@ -320,7 +320,7 @@ describe('BridgeController', function () { bridgeController.state.bridgeState.quotesLastFetched, ).toBeUndefined(); - expect(bridgeController.state.bridgeState).toEqual( + expect(bridgeController.state.bridgeState).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: false }, quotes: [], @@ -338,8 +338,7 @@ describe('BridgeController', function () { quotesLoadingStatus: 1, }), ); - const firstFetchTime = - bridgeController.state.bridgeState.quotesLastFetched ?? 0; + const firstFetchTime = bridgeController.state.bridgeState.quotesLastFetched; expect(firstFetchTime).toBeGreaterThan(0); // After 2nd fetch @@ -360,7 +359,8 @@ describe('BridgeController', function () { expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); const secondFetchTime = bridgeController.state.bridgeState.quotesLastFetched; - expect(secondFetchTime).toBeGreaterThan(firstFetchTime); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(secondFetchTime).toBeGreaterThan(firstFetchTime!); // After 3nd fetch throws an error jest.advanceTimersByTime(50000); @@ -491,8 +491,7 @@ describe('BridgeController', function () { quotesInitialLoadTime: 11000, }), ); - const firstFetchTime = - bridgeController.state.bridgeState.quotesLastFetched ?? 0; + const firstFetchTime = bridgeController.state.bridgeState.quotesLastFetched; expect(firstFetchTime).toBeGreaterThan(0); // After 2nd fetch @@ -688,14 +687,13 @@ describe('BridgeController', function () { }), ); quotes.forEach((quote) => { - const expectedQuote = l1GasFeesInHexWei - ? { ...quote, l1GasFeesInHexWei } - : quote; - expect(quote).toStrictEqual(expectedQuote); + const expectedQuote = { ...quote, l1GasFeesInHexWei }; + // eslint-disable-next-line jest/prefer-strict-equal + expect(quote).toEqual(expectedQuote); }); const firstFetchTime = - bridgeController.state.bridgeState.quotesLastFetched ?? 0; + bridgeController.state.bridgeState.quotesLastFetched; expect(firstFetchTime).toBeGreaterThan(0); expect(getLayer1GasFeeMock).toHaveBeenCalledTimes( diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 9eab5195a09..7d7bab5ad2b 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -56,7 +56,7 @@ export default class BridgeController extends StaticIntervalPollingController
Promise; @@ -156,7 +156,7 @@ export default class BridgeController extends StaticIntervalPollingController
{ + readonly #hasSufficientBalance = async (quoteRequest: QuoteRequest) => { const walletAddress = this.#getSelectedAccount().address; const srcChainIdInHex = numberToHex(quoteRequest.srcChainId); const provider = this.#getSelectedNetworkClient()?.provider; @@ -197,7 +197,7 @@ export default class BridgeController extends StaticIntervalPollingController
{ @@ -281,7 +281,7 @@ export default class BridgeController extends StaticIntervalPollingController
=> { return await Promise.all( diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 39f6a95bdf2..6798f852f59 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -1,13 +1,14 @@ -import { zeroAddress } from 'ethereumjs-util'; import { handleFetch } from '@metamask/controller-utils'; -import { CHAIN_IDS } from '../constants/chains'; -import mockBridgeQuotesErc20Erc20 from '../test/mock-quotes-erc20-erc20.json'; -import mockBridgeQuotesNativeErc20 from '../test/mock-quotes-native-erc20.json'; +import { zeroAddress } from 'ethereumjs-util'; + import { fetchBridgeFeatureFlags, fetchBridgeQuotes, fetchBridgeTokens, } from './fetch'; +import { CHAIN_IDS } from '../constants/chains'; +import mockBridgeQuotesErc20Erc20 from '../test/mock-quotes-erc20-erc20.json'; +import mockBridgeQuotesNativeErc20 from '../test/mock-quotes-native-erc20.json'; jest.mock('@metamask/controller-utils', () => ({ ...jest.requireActual('@metamask/controller-utils'), @@ -59,9 +60,12 @@ describe('Bridge utils', () => { const result = await fetchBridgeFeatureFlags(); - expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getAllFeatureFlags', { - headers: { 'X-Client-Id': 'extension' }, - }); + expect(handleFetch).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + { + headers: { 'X-Client-Id': 'extension' }, + }, + ); expect(result).toStrictEqual({ extensionConfig: { @@ -121,9 +125,12 @@ describe('Bridge utils', () => { const result = await fetchBridgeFeatureFlags(); - expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getAllFeatureFlags', { - headers: { 'X-Client-Id': 'extension' }, - }); + expect(handleFetch).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', + { + headers: { 'X-Client-Id': 'extension' }, + }, + ); expect(result).toStrictEqual({ extensionConfig: { @@ -177,9 +184,12 @@ describe('Bridge utils', () => { const result = await fetchBridgeTokens('0xa'); - expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getTokens?chainId=10', { - headers: { 'X-Client-Id': 'extension' }, - }); + expect(handleFetch).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', + { + headers: { 'X-Client-Id': 'extension' }, + }, + ); expect(result).toStrictEqual({ '0x0000000000000000000000000000000000000000': { @@ -214,9 +224,7 @@ describe('Bridge utils', () => { describe('fetchBridgeQuotes', () => { it('should fetch bridge quotes successfully, no approvals', async () => { - (handleFetch as jest.Mock).mockResolvedValue( - mockBridgeQuotesNativeErc20, - ); + (handleFetch as jest.Mock).mockResolvedValue(mockBridgeQuotesNativeErc20); const { signal } = new AbortController(); const result = await fetchBridgeQuotes( @@ -232,10 +240,13 @@ describe('Bridge utils', () => { signal, ); - expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { - headers: { 'X-Client-Id': 'extension' }, - signal, - }); + expect(handleFetch).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + { + headers: { 'X-Client-Id': 'extension' }, + signal, + }, + ); expect(result).toStrictEqual(mockBridgeQuotesNativeErc20); }); @@ -261,10 +272,13 @@ describe('Bridge utils', () => { signal, ); - expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { - headers: { 'X-Client-Id': 'extension' }, - signal, - }); + expect(handleFetch).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + { + headers: { 'X-Client-Id': 'extension' }, + signal, + }, + ); expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); }); @@ -309,10 +323,13 @@ describe('Bridge utils', () => { signal, ); - expect(handleFetch).toHaveBeenCalledWith('https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { - headers: { 'X-Client-Id': 'extension' }, - signal, - }); + expect(handleFetch).toHaveBeenCalledWith( + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + { + headers: { 'X-Client-Id': 'extension' }, + signal, + }, + ); expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); }); From c303bb4a36fc8284200e953c1b883b88faca09bf Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:25:52 -0500 Subject: [PATCH 25/94] chore: bump coverage --- packages/bridge-controller/jest.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js index eae426c4c23..c8fc07a0653 100644 --- a/packages/bridge-controller/jest.config.js +++ b/packages/bridge-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 85, - functions: 95, - lines: 95, - statements: 95, + branches: 89, + functions: 98, + lines: 98, + statements: 98, }, }, }); From a19aaaabe100b7914aac1f0f5fe295f98c9811c2 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:37:24 -0500 Subject: [PATCH 26/94] chore: add license --- packages/bridge-controller/LICENSE | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/bridge-controller/LICENSE diff --git a/packages/bridge-controller/LICENSE b/packages/bridge-controller/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/bridge-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE From 5772b3cea341717b136387014a5cef314058cb0e Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:06:39 -0500 Subject: [PATCH 27/94] fix: invalid deps --- packages/bridge-controller/package.json | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index a73e4fb52c0..ef42f6f0a7e 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -50,20 +50,20 @@ "@ethersproject/bignumber": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@metamask/base-controller": "^7.0.0", - "@metamask/controller-utils": "^11.4.5", + "@metamask/base-controller": "^7.1.1", + "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", - "@metamask/network-controller": "^22.1.1", - "@metamask/polling-controller": "^12.0.1", - "@metamask/transaction-controller": "^43.0.0", + "@metamask/network-controller": "^22.2.0", + "@metamask/polling-controller": "^12.0.2", + "@metamask/transaction-controller": "^45.0.0", "@metamask/utils": "^10.0.1", "ethereumjs-util": "^7.0.10", "ethers": "5.7.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/eth-json-rpc-provider": "^4.1.6", - "@metamask/json-rpc-engine": "^10.0.0", + "@metamask/eth-json-rpc-provider": "^4.1.8", + "@metamask/json-rpc-engine": "^10.0.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -75,6 +75,10 @@ "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.2.2" }, + "peerDependencies": { + "@metamask/network-controller": "^22.0.0", + "@metamask/transaction-controller": "^45.0.0" + }, "engines": { "node": "^18.18 || >=20" }, From 5db90c025aa9be3ed17c7e940b575dace1df48f7 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:15:49 -0500 Subject: [PATCH 28/94] chore: align @ethersproject/contracts version --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index ef42f6f0a7e..cf61e0aaaeb 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -49,7 +49,7 @@ "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/contracts": "^5.7.0", - "@ethersproject/providers": "^5.7.2", + "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^7.1.1", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", From 65eef1659c9d0ad0ffd16a290081ab8b7eec086f Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:16:38 -0500 Subject: [PATCH 29/94] chore: align more package versions --- packages/bridge-controller/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index cf61e0aaaeb..f7fc77ec553 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -56,7 +56,7 @@ "@metamask/network-controller": "^22.2.0", "@metamask/polling-controller": "^12.0.2", "@metamask/transaction-controller": "^45.0.0", - "@metamask/utils": "^10.0.1", + "@metamask/utils": "^11.1.0", "ethereumjs-util": "^7.0.10", "ethers": "5.7.0" }, @@ -69,7 +69,7 @@ "jest": "^27.5.1", "jest-environment-jsdom": "^27.5.1", "lodash": "^4.17.21", - "nock": "^13.5.4", + "nock": "^13.3.1", "ts-jest": "^27.1.4", "typedoc": "^0.24.8", "typedoc-plugin-missing-exports": "^2.0.0", From 1ee940a58719f2d95a991f8d2b8936e1e6b7f889 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:49:02 -0500 Subject: [PATCH 30/94] chore: migrate to ethers 6 --- packages/bridge-controller/package.json | 5 +- .../src/bridge-controller.test.ts | 10 ++-- .../src/bridge-controller.ts | 12 ++--- .../src/utils/balance.test.ts | 47 +++++++++---------- .../bridge-controller/src/utils/balance.ts | 15 +++--- 5 files changed, 39 insertions(+), 50 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index f7fc77ec553..78daefdb02c 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -47,9 +47,6 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/contracts": "^5.7.0", - "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^7.1.1", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", @@ -58,7 +55,7 @@ "@metamask/transaction-controller": "^45.0.0", "@metamask/utils": "^11.1.0", "ethereumjs-util": "^7.0.10", - "ethers": "5.7.0" + "ethers": "6.12.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index c4fc1dcce2f..ab72a81e0cc 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1,6 +1,6 @@ -import { Contract } from '@ethersproject/contracts'; import type { Hex } from '@metamask/utils'; import { bigIntToHex } from '@metamask/utils'; +import { Contract } from 'ethers'; import nock from 'nock'; import BridgeController from './bridge-controller'; @@ -29,11 +29,11 @@ const messengerMock = { publish: jest.fn(), } as unknown as jest.Mocked; -jest.mock('@ethersproject/contracts'); - -jest.mock('@ethersproject/providers', () => { +jest.mock('ethers', () => { return { - Web3Provider: jest.fn(), + ...jest.requireActual('ethers'), + Contract: jest.fn(), + BrowserProvider: jest.fn(), }; }); const getLayer1GasFeeMock = jest.fn(); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 7d7bab5ad2b..a10c955ab2c 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -1,6 +1,3 @@ -import { BigNumber } from '@ethersproject/bignumber'; -import { Contract } from '@ethersproject/contracts'; -import { Web3Provider } from '@ethersproject/providers'; import type { StateMetadata } from '@metamask/base-controller'; import type { ChainId } from '@metamask/controller-utils'; import { abiERC20 } from '@metamask/metamask-eth-abis'; @@ -9,6 +6,7 @@ import { StaticIntervalPollingController } from '@metamask/polling-controller'; import type { TransactionParams } from '@metamask/transaction-controller'; import { numberToHex } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; +import { BrowserProvider, Contract } from 'ethers'; import { REFRESH_INTERVAL_MS } from './constants'; import { @@ -352,13 +350,13 @@ export default class BridgeController extends StaticIntervalPollingController
{ + return { + ...jest.requireActual('ethers'), + Contract: jest.fn(), + BrowserProvider: jest.fn(), + }; +}); describe('balance', () => { beforeEach(() => { @@ -23,9 +26,7 @@ describe('balance', () => { describe('calcLatestSrcBalance', () => { it('should return the ERC20 token balance', async () => { - const mockBalanceOf = jest - .fn() - .mockResolvedValueOnce(BigNumber.from('100')); + const mockBalanceOf = jest.fn().mockResolvedValueOnce(BigInt(100)); (Contract as unknown as jest.Mock).mockImplementation(() => ({ balanceOf: mockBalanceOf, })); @@ -37,23 +38,21 @@ describe('balance', () => { '0x456', '0x789', ), - ).toStrictEqual(BigNumber.from(100)); + ).toStrictEqual(BigInt(100)); expect(mockBalanceOf).toHaveBeenCalledTimes(1); expect(mockBalanceOf).toHaveBeenCalledWith('0x123'); }); it('should return the native asset balance', async () => { - const mockGetBalance = jest.fn(); - (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + const mockGetBalance = jest.fn().mockImplementation(() => { + return BigInt(100); + }); + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { return { getBalance: mockGetBalance, }; }); - mockGetBalance.mockImplementation(() => { - return BigNumber.from(100); - }); - expect( await balanceUtils.calcLatestSrcBalance( global.ethereumProvider, @@ -61,7 +60,7 @@ describe('balance', () => { zeroAddress(), '0x789', ), - ).toStrictEqual(BigNumber.from(100)); + ).toStrictEqual(BigInt(100)); expect(mockGetBalance).toHaveBeenCalledTimes(1); expect(mockGetBalance).toHaveBeenCalledWith( '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', @@ -70,7 +69,7 @@ describe('balance', () => { it('should return undefined if token address and chainId are undefined', async () => { const mockGetBalance = jest.fn(); - (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { return { getBalance: mockGetBalance, }; @@ -96,19 +95,19 @@ describe('balance', () => { describe('hasSufficientBalance', () => { it('should return true if user has sufficient balance', async () => { const mockGetBalance = jest.fn(); - (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { return { getBalance: mockGetBalance, }; }); mockGetBalance.mockImplementation(() => { - return BigNumber.from('10000000000000000000'); + return BigInt(10000000000000000000); }); const mockBalanceOf = jest .fn() - .mockResolvedValueOnce(BigNumber.from('10000000000000000001')); + .mockResolvedValueOnce(BigInt('10000000000000000001')); (Contract as unknown as jest.Mock).mockImplementation(() => ({ balanceOf: mockBalanceOf, })); @@ -136,22 +135,20 @@ describe('balance', () => { it('should return false if user has native assets but insufficient ERC20 src tokens', async () => { const mockGetBalance = jest.fn(); - (Web3Provider as unknown as jest.Mock).mockImplementation(() => { + (BrowserProvider as unknown as jest.Mock).mockImplementation(() => { return { getBalance: mockGetBalance, }; }); mockGetBalance.mockImplementation(() => { - return BigNumber.from('10000000000000000000'); + return BigInt(10000000000000000000); }); const mockFetchTokenBalance = jest.spyOn( balanceUtils, 'fetchTokenBalance', ); - mockFetchTokenBalance.mockResolvedValueOnce( - BigNumber.from('9000000000000000000'), - ); + mockFetchTokenBalance.mockResolvedValueOnce(BigInt(9000000000000000000)); expect( await balanceUtils.hasSufficientBalance( diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts index c78d99d6cd3..084c12c87e3 100644 --- a/packages/bridge-controller/src/utils/balance.ts +++ b/packages/bridge-controller/src/utils/balance.ts @@ -1,18 +1,15 @@ -import { Contract } from '@ethersproject/contracts'; -import { Web3Provider } from '@ethersproject/providers'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Provider } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { zeroAddress } from 'ethereumjs-util'; -import type { BigNumber } from 'ethers'; -import { getAddress } from 'ethers/lib/utils'; +import { BrowserProvider, Contract, getAddress } from 'ethers'; export const fetchTokenBalance = async ( address: string, userAddress: string, provider: Provider, -): Promise => { - const ethersProvider = new Web3Provider(provider); +): Promise => { + const ethersProvider = new BrowserProvider(provider); const tokenContract = new Contract(address, abiERC20, ethersProvider); const tokenBalancePromise = tokenContract ? tokenContract.balanceOf(userAddress) @@ -25,10 +22,10 @@ export const calcLatestSrcBalance = async ( selectedAddress: string, tokenAddress: string, chainId: Hex, -): Promise => { +): Promise => { if (tokenAddress && chainId) { if (tokenAddress === zeroAddress()) { - const ethersProvider = new Web3Provider(provider); + const ethersProvider = new BrowserProvider(provider); return await ethersProvider.getBalance(getAddress(selectedAddress)); } return await fetchTokenBalance(tokenAddress, selectedAddress, provider); @@ -50,5 +47,5 @@ export const hasSufficientBalance = async ( chainId, ); - return srcTokenBalance?.gte(fromTokenAmount) ?? false; + return srcTokenBalance ? srcTokenBalance >= BigInt(fromTokenAmount) : false; }; From 92dea7e756923ea6b2498ab6fcfcc9372548c32a Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:55:20 -0500 Subject: [PATCH 31/94] chore: align ethers version --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 78daefdb02c..ee5e92182b5 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -55,7 +55,7 @@ "@metamask/transaction-controller": "^45.0.0", "@metamask/utils": "^11.1.0", "ethereumjs-util": "^7.0.10", - "ethers": "6.12.0" + "ethers": "^6.12.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", From a86d46f18020de54ce065c214f0c82f80f1b1d30 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:02:55 -0500 Subject: [PATCH 32/94] chore: update changelog with git diffs --- packages/bridge-controller/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index bd379b7049f..83ec1d578fe 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -10,3 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release + +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@1.0.0...HEAD +[1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/bridge-controller@1.0.0 From e245d4f3c913baa963749ccd02d87683dbf02245 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:03:38 -0500 Subject: [PATCH 33/94] chore: update changelog --- packages/bridge-controller/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 83ec1d578fe..bb775b6b9d4 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] + ### Added - Initial release From 33c5cc615e6b507f82e3b97eb202a23affde171b Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:09:01 -0500 Subject: [PATCH 34/94] fix: incorrect import --- packages/bridge-controller/src/utils/index.test.ts | 2 +- packages/bridge-controller/src/utils/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts index cf5609c15c7..af4e6cbb3b5 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -1,6 +1,6 @@ -import { Contract } from '@ethersproject/contracts'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Hex } from '@metamask/utils'; +import { Contract } from 'ethers'; import { getEthUsdtResetData, diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/index.ts index 55e79513ce5..8129f450f6e 100644 --- a/packages/bridge-controller/src/utils/index.ts +++ b/packages/bridge-controller/src/utils/index.ts @@ -1,6 +1,6 @@ -import { Contract } from '@ethersproject/contracts'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Hex } from '@metamask/utils'; +import { Contract } from 'ethers'; import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS } from '../constants'; import { CHAIN_IDS } from '../constants/chains'; From 4c01335bd63b32ad9f61f3fc4a3731c06201ec0e Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:13:11 -0500 Subject: [PATCH 35/94] chore: remove unneeded link --- packages/bridge-controller/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index bb775b6b9d4..a968177e3fa 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -14,4 +14,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@1.0.0...HEAD -[1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/bridge-controller@1.0.0 From 0d0e9ace7b9dac451255c286ea4a8ff93de8dc61 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:16:49 -0500 Subject: [PATCH 36/94] chore: update teams.json --- teams.json | 1 + 1 file changed, 1 insertion(+) diff --git a/teams.json b/teams.json index 7060b959eaf..31b7f607cac 100644 --- a/teams.json +++ b/teams.json @@ -5,6 +5,7 @@ "metamask/approval-controller": "team-confirmations", "metamask/assets-controllers": "team-assets", "metamask/base-controller": "team-wallet-framework", + "metamask/bridge-controller": "team-swaps,team-bridge", "metamask/build-utils": "team-wallet-framework", "metamask/composable-controller": "team-wallet-framework", "metamask/controller-utils": "team-wallet-framework", From d1903036d08697e1e062473ba2341361189121ac Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:25:34 -0500 Subject: [PATCH 37/94] chore: remove 1.0.0 section --- packages/bridge-controller/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index a968177e3fa..e2199d8724c 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.0.0] - ### Added - Initial release From 88ad488ff9d014e19eab69e60ab9b80571e69449 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:26:23 -0500 Subject: [PATCH 38/94] Revert "chore: remove 1.0.0 section" This reverts commit 2c96d62c4c5cddcea8b41dcda027052f47aa5333. --- packages/bridge-controller/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index e2199d8724c..a968177e3fa 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] + ### Added - Initial release From 3b7432aea096d2bb38a748b50ae432c3d99b44ed Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:30:55 -0500 Subject: [PATCH 39/94] fix: changelog lint --- packages/bridge-controller/CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index a968177e3fa..8fcf72c699c 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,10 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.0.0] - ### Added - Initial release -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@1.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/ From d421a2e15547536815e2523b97a8a7002e784238 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:22:47 -0500 Subject: [PATCH 40/94] chore: export BridgeController, types, constants, utils --- packages/bridge-controller/src/index.ts | 124 ++++++++++++++++++++++++ packages/bridge-controller/src/types.ts | 24 +++-- 2 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 packages/bridge-controller/src/index.ts diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts new file mode 100644 index 00000000000..c64fb015f56 --- /dev/null +++ b/packages/bridge-controller/src/index.ts @@ -0,0 +1,124 @@ +export { default } from './bridge-controller'; + +export type { + AssetType, + ChainConfiguration, + L1GasFees, + QuoteMetadata, + SortOrder, + BridgeToken, + BridgeFlag, + GasMultiplierByChainId, + FeatureFlagResponse, + BridgeAsset, + QuoteRequest, + Protocol, + ActionTypes, + Step, + RefuelData, + Quote, + QuoteResponse, + ChainId, + FeeType, + FeeData, + TxData, + BridgeFeatureFlagsKey, + BridgeFeatureFlags, + RequestStatus, + BridgeUserAction, + BridgeBackgroundAction, + BridgeControllerState, + BridgeControllerAction, + BridgeControllerActions, + BridgeControllerEvents, + AllowedActions, + AllowedEvents, + BridgeControllerMessenger, +} from './types'; + +export { + ALLOWED_BRIDGE_CHAIN_IDS, + BRIDGE_API_BASE_URL, + BRIDGE_CLIENT_ID, + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, + BRIDGE_QUOTE_MAX_ETA_SECONDS, + BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, + BRIDGE_PREFERRED_GAS_ESTIMATE, + BRIDGE_DEFAULT_SLIPPAGE, + NETWORK_TO_SHORT_NETWORK_NAME_MAP, + BRIDGE_MM_FEE_RATE, + REFRESH_INTERVAL_MS, + DEFAULT_MAX_REFRESH_COUNT, + BRIDGE_CONTROLLER_NAME, + DEFAULT_BRIDGE_CONTROLLER_STATE, + METABRIDGE_CHAIN_TO_ADDRESS_MAP, +} from './constants'; + +export type { AllowedBridgeChainIds } from './constants'; + +export { + CHAIN_IDS, + NETWORK_TYPES, + MAINNET_DISPLAY_NAME, + GOERLI_DISPLAY_NAME, + SEPOLIA_DISPLAY_NAME, + LINEA_GOERLI_DISPLAY_NAME, + LINEA_SEPOLIA_DISPLAY_NAME, + LINEA_MAINNET_DISPLAY_NAME, + LOCALHOST_DISPLAY_NAME, + BSC_DISPLAY_NAME, + POLYGON_DISPLAY_NAME, + AVALANCHE_DISPLAY_NAME, + ARBITRUM_DISPLAY_NAME, + BNB_DISPLAY_NAME, + OPTIMISM_DISPLAY_NAME, + FANTOM_DISPLAY_NAME, + HARMONY_DISPLAY_NAME, + PALM_DISPLAY_NAME, + CELO_DISPLAY_NAME, + GNOSIS_DISPLAY_NAME, + ZK_SYNC_ERA_DISPLAY_NAME, + BASE_DISPLAY_NAME, + AURORA_DISPLAY_NAME, + CRONOS_DISPLAY_NAME, + POLYGON_ZKEVM_DISPLAY_NAME, + MOONBEAM_DISPLAY_NAME, + MOONRIVER_DISPLAY_NAME, + SCROLL_DISPLAY_NAME, + SCROLL_SEPOLIA_DISPLAY_NAME, + OP_BNB_DISPLAY_NAME, + BERACHAIN_DISPLAY_NAME, + METACHAIN_ONE_DISPLAY_NAME, + LISK_DISPLAY_NAME, + LISK_SEPOLIA_DISPLAY_NAME, + INK_SEPOLIA_DISPLAY_NAME, + INK_DISPLAY_NAME, + SONEIUM_DISPLAY_NAME, + MODE_SEPOLIA_DISPLAY_NAME, + MODE_DISPLAY_NAME, + NETWORK_TO_NAME_MAP, +} from './constants/chains'; + +export { + CURRENCY_SYMBOLS, + ETH_SWAPS_TOKEN_OBJECT, + BNB_SWAPS_TOKEN_OBJECT, + MATIC_SWAPS_TOKEN_OBJECT, + AVAX_SWAPS_TOKEN_OBJECT, + TEST_ETH_SWAPS_TOKEN_OBJECT, + GOERLI_SWAPS_TOKEN_OBJECT, + SEPOLIA_SWAPS_TOKEN_OBJECT, + ARBITRUM_SWAPS_TOKEN_OBJECT, + OPTIMISM_SWAPS_TOKEN_OBJECT, + ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, + LINEA_SWAPS_TOKEN_OBJECT, + BASE_SWAPS_TOKEN_OBJECT, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, +} from './constants/tokens'; + +export type { SwapsTokenObject } from './constants/tokens'; + +export { SWAPS_API_V2_BASE_URL } from './constants/swaps'; + +export { getEthUsdtResetData, isEthUsdt } from './utils'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 7196ce74fbf..3e86e2cf169 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -114,17 +114,20 @@ export type QuoteRequest = { resetApproval?: boolean; refuel?: boolean; }; -type Protocol = { + +export type Protocol = { name: string; displayName?: string; icon?: string; }; -enum ActionTypes { + +export enum ActionTypes { BRIDGE = 'bridge', SWAP = 'swap', REFUEL = 'refuel', } -type Step = { + +export type Step = { action: ActionTypes; srcChainId: ChainId; destChainId?: ChainId; @@ -134,7 +137,8 @@ type Step = { destAmount: string; protocol: Protocol; }; -type RefuelData = Step; + +export type RefuelData = Step; export type Quote = { requestId: string; @@ -225,28 +229,30 @@ export type BridgeControllerState = { quotesRefreshCount: number; }; -type BridgeControllerAction = { +export type BridgeControllerAction< + FunctionName extends keyof BridgeController, +> = { type: `${typeof BRIDGE_CONTROLLER_NAME}:${FunctionName}`; handler: BridgeController[FunctionName]; }; // Maps to BridgeController function names -type BridgeControllerActions = +export type BridgeControllerActions = | BridgeControllerAction | BridgeControllerAction | BridgeControllerAction | BridgeControllerAction; -type BridgeControllerEvents = ControllerStateChangeEvent< +export type BridgeControllerEvents = ControllerStateChangeEvent< typeof BRIDGE_CONTROLLER_NAME, BridgeControllerState >; -type AllowedActions = +export type AllowedActions = | AccountsControllerGetSelectedAccountAction['type'] | NetworkControllerGetSelectedNetworkClientAction['type'] | NetworkControllerFindNetworkClientIdByChainIdAction['type']; -type AllowedEvents = never; +export type AllowedEvents = never; /** * The messenger for the BridgeController. From 0872c452f282d6c44a6217baeffbc02232d7807c Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:25:30 -0500 Subject: [PATCH 41/94] chore: simplify exports --- packages/bridge-controller/src/index.ts | 43 ------------------------- 1 file changed, 43 deletions(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index c64fb015f56..7e571f1fb9e 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -57,49 +57,6 @@ export { export type { AllowedBridgeChainIds } from './constants'; -export { - CHAIN_IDS, - NETWORK_TYPES, - MAINNET_DISPLAY_NAME, - GOERLI_DISPLAY_NAME, - SEPOLIA_DISPLAY_NAME, - LINEA_GOERLI_DISPLAY_NAME, - LINEA_SEPOLIA_DISPLAY_NAME, - LINEA_MAINNET_DISPLAY_NAME, - LOCALHOST_DISPLAY_NAME, - BSC_DISPLAY_NAME, - POLYGON_DISPLAY_NAME, - AVALANCHE_DISPLAY_NAME, - ARBITRUM_DISPLAY_NAME, - BNB_DISPLAY_NAME, - OPTIMISM_DISPLAY_NAME, - FANTOM_DISPLAY_NAME, - HARMONY_DISPLAY_NAME, - PALM_DISPLAY_NAME, - CELO_DISPLAY_NAME, - GNOSIS_DISPLAY_NAME, - ZK_SYNC_ERA_DISPLAY_NAME, - BASE_DISPLAY_NAME, - AURORA_DISPLAY_NAME, - CRONOS_DISPLAY_NAME, - POLYGON_ZKEVM_DISPLAY_NAME, - MOONBEAM_DISPLAY_NAME, - MOONRIVER_DISPLAY_NAME, - SCROLL_DISPLAY_NAME, - SCROLL_SEPOLIA_DISPLAY_NAME, - OP_BNB_DISPLAY_NAME, - BERACHAIN_DISPLAY_NAME, - METACHAIN_ONE_DISPLAY_NAME, - LISK_DISPLAY_NAME, - LISK_SEPOLIA_DISPLAY_NAME, - INK_SEPOLIA_DISPLAY_NAME, - INK_DISPLAY_NAME, - SONEIUM_DISPLAY_NAME, - MODE_SEPOLIA_DISPLAY_NAME, - MODE_DISPLAY_NAME, - NETWORK_TO_NAME_MAP, -} from './constants/chains'; - export { CURRENCY_SYMBOLS, ETH_SWAPS_TOKEN_OBJECT, From 0f67bcf965bbbdde4c37876e03ea92ad71999565 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:35:15 -0500 Subject: [PATCH 42/94] feat: add dynamic bridge API base URL configuration --- .../src/bridge-controller.test.ts | 10 ++++------ .../bridge-controller/src/constants/index.ts | 3 --- packages/bridge-controller/src/index.ts | 3 +-- packages/bridge-controller/src/utils/fetch.ts | 18 +++++++++--------- packages/bridge-controller/src/utils/index.ts | 19 ++++++++++++++++++- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index ab72a81e0cc..d0de71bddcc 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -4,10 +4,7 @@ import { Contract } from 'ethers'; import nock from 'nock'; import BridgeController from './bridge-controller'; -import { - BRIDGE_API_BASE_URL, - DEFAULT_BRIDGE_CONTROLLER_STATE, -} from './constants'; +import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import mockBridgeQuotesErc20Native from './test/mock-quotes-erc20-native.json'; @@ -15,6 +12,7 @@ import mockBridgeQuotesNativeErc20Eth from './test/mock-quotes-native-erc20-eth. import mockBridgeQuotesNativeErc20 from './test/mock-quotes-native-erc20.json'; import { flushPromises } from './test/utils'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; +import { getBridgeApiBaseUrl } from './utils'; import * as balanceUtils from './utils/balance'; import * as fetchUtils from './utils/fetch'; @@ -52,7 +50,7 @@ describe('BridgeController', function () { jest.clearAllMocks(); jest.clearAllTimers(); - nock(BRIDGE_API_BASE_URL) + nock(getBridgeApiBaseUrl()) .get('/getAllFeatureFlags') .reply(200, { 'extension-config': { @@ -91,7 +89,7 @@ describe('BridgeController', function () { '534352': 2.4, }, }); - nock(BRIDGE_API_BASE_URL) + nock(getBridgeApiBaseUrl()) .get('/getTokens?chainId=10') .reply(200, [ { diff --git a/packages/bridge-controller/src/constants/index.ts b/packages/bridge-controller/src/constants/index.ts index 218a4ee9335..3ce1d2379ca 100644 --- a/packages/bridge-controller/src/constants/index.ts +++ b/packages/bridge-controller/src/constants/index.ts @@ -22,9 +22,6 @@ export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; -export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS - ? BRIDGE_DEV_API_BASE_URL - : BRIDGE_PROD_API_BASE_URL; export const BRIDGE_CLIENT_ID = 'extension'; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 7e571f1fb9e..cd3928cf5c2 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -38,7 +38,6 @@ export type { export { ALLOWED_BRIDGE_CHAIN_IDS, - BRIDGE_API_BASE_URL, BRIDGE_CLIENT_ID, ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS, @@ -78,4 +77,4 @@ export type { SwapsTokenObject } from './constants/tokens'; export { SWAPS_API_V2_BASE_URL } from './constants/swaps'; -export { getEthUsdtResetData, isEthUsdt } from './utils'; +export { getEthUsdtResetData, isEthUsdt, getBridgeApiBaseUrl } from './utils'; diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 6dd0a55cc0c..46e51bd24ad 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -2,7 +2,11 @@ import { handleFetch } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; import { hexToNumber, numberToHex } from '@metamask/utils'; -import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol } from '.'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, + getBridgeApiBaseUrl, +} from '.'; import { FEATURE_FLAG_VALIDATORS, QUOTE_VALIDATORS, @@ -12,11 +16,7 @@ import { QUOTE_RESPONSE_VALIDATORS, FEE_DATA_VALIDATORS, } from './validators'; -import { - BRIDGE_API_BASE_URL, - BRIDGE_CLIENT_ID, - REFRESH_INTERVAL_MS, -} from '../constants'; +import { BRIDGE_CLIENT_ID, REFRESH_INTERVAL_MS } from '../constants'; import type { SwapsTokenObject } from '../constants/tokens'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { @@ -40,7 +40,7 @@ const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; * @returns The bridge feature flags */ export async function fetchBridgeFeatureFlags(): Promise { - const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; + const url = `${getBridgeApiBaseUrl()}/getAllFeatureFlags`; const rawFeatureFlags = await handleFetch(url, { headers: CLIENT_ID_HEADER, }); @@ -88,7 +88,7 @@ export async function fetchBridgeTokens( chainId: Hex, ): Promise> { // TODO make token api v2 call - const url = `${BRIDGE_API_BASE_URL}/getTokens?chainId=${hexToNumber( + const url = `${getBridgeApiBaseUrl()}/getTokens?chainId=${hexToNumber( chainId, )}`; @@ -145,7 +145,7 @@ export async function fetchBridgeQuotes( insufficientBal: request.insufficientBal ? 'true' : 'false', resetApproval: request.resetApproval ? 'true' : 'false', }); - const url = `${BRIDGE_API_BASE_URL}/getQuote?${queryParams}`; + const url = `${getBridgeApiBaseUrl()}/getQuote?${queryParams}`; const quotes = await handleFetch(url, { headers: CLIENT_ID_HEADER, signal, diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/index.ts index 8129f450f6e..533638b29b3 100644 --- a/packages/bridge-controller/src/utils/index.ts +++ b/packages/bridge-controller/src/utils/index.ts @@ -2,10 +2,27 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Hex } from '@metamask/utils'; import { Contract } from 'ethers'; -import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS } from '../constants'; +import { + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, +} from '../constants'; import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; +export const getBridgeApiBaseUrl = () => { + if (process.env.BRIDGE_API_CUSTOM_URL) { + return process.env.BRIDGE_API_CUSTOM_URL; + } + + if (process.env.BRIDGE_USE_DEV_APIS) { + return BRIDGE_DEV_API_BASE_URL; + } + + return BRIDGE_PROD_API_BASE_URL; +}; + /** * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum * From 07d7e31a54b6fbb97b8cf7287d182e5a57d8d491 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:36:50 -0500 Subject: [PATCH 43/94] fix: update bridge API environment variable name --- packages/bridge-controller/src/utils/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/index.ts index 533638b29b3..ba59b8867c2 100644 --- a/packages/bridge-controller/src/utils/index.ts +++ b/packages/bridge-controller/src/utils/index.ts @@ -12,8 +12,8 @@ import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; export const getBridgeApiBaseUrl = () => { - if (process.env.BRIDGE_API_CUSTOM_URL) { - return process.env.BRIDGE_API_CUSTOM_URL; + if (process.env.BRIDGE_CUSTOM_API_BASE_URL) { + return process.env.BRIDGE_CUSTOM_API_BASE_URL; } if (process.env.BRIDGE_USE_DEV_APIS) { From 8577f699fb3c062a8defe5691b7f53e9bd163723 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:41:57 -0500 Subject: [PATCH 44/94] fix: lint errors --- .../bridge-controller/src/utils/index.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts index af4e6cbb3b5..beacf3e8944 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable n/no-process-env */ import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Hex } from '@metamask/utils'; import { Contract } from 'ethers'; @@ -8,8 +9,13 @@ import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, sumHexes, + getBridgeApiBaseUrl, } from '.'; import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS } from '../constants'; +import { + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, +} from '../constants'; import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; @@ -132,4 +138,30 @@ describe('Bridge utils', () => { expect(isSwapsDefaultTokenSymbol('ETH', '' as Hex)).toBe(false); }); }); + + describe('getBridgeApiBaseUrl', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('returns custom API URL when BRIDGE_CUSTOM_API_BASE_URL is set', () => { + process.env.BRIDGE_CUSTOM_API_BASE_URL = 'https://custom-api.example.com'; + expect(getBridgeApiBaseUrl()).toBe('https://custom-api.example.com'); + }); + + it('returns dev API URL when BRIDGE_USE_DEV_APIS is set', () => { + process.env.BRIDGE_USE_DEV_APIS = 'true'; + expect(getBridgeApiBaseUrl()).toBe(BRIDGE_DEV_API_BASE_URL); + }); + + it('returns prod API URL by default', () => { + expect(getBridgeApiBaseUrl()).toBe(BRIDGE_PROD_API_BASE_URL); + }); + }); }); From df81faaf85251bb86af68078a2d79be2c386813f Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:53:22 -0500 Subject: [PATCH 45/94] chore: add CODEOWNERS for bridge-controller --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 159a42b2133..789e3ec20d9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,6 +37,9 @@ ## Snaps Team /packages/rate-limit-controller @MetaMask/snaps-devs +## Swaps-Bridge Team +/packages/bridge-controller @MetaMask/swaps-engineers + ## Portfolio Team /packages/token-search-discovery-controller @MetaMask/portfolio From 1e23a492e1aad5084d0edfc847cde7ae4957dde8 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:17:49 -0500 Subject: [PATCH 46/94] chore: revert .vscode in gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c7251737c86..6c1e52eb80d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,4 @@ scripts/coverage !.yarn/versions # typescript -packages/*/*.tsbuildinfo - -# vscode -.vscode \ No newline at end of file +packages/*/*.tsbuildinfo \ No newline at end of file From 2e7045b7366d4ad05d61c329c92b1c53eae7d9fb Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:18:59 -0500 Subject: [PATCH 47/94] chore: add package release related lines for bridge controller --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 789e3ec20d9..fcfb57e5fde 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -115,4 +115,5 @@ /packages/multichain-transactions-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/wallet-framework-engineers /packages/token-search-discovery-controller/package.json @MetaMask/portfolio @MetaMask/wallet-framework-engineers /packages/token-search-discovery-controller/CHANGELOG.md @MetaMask/portfolio @MetaMask/wallet-framework-engineers - +/packages/bridge-controller/package.json @MetaMask/swaps-engineers @MetaMask/wallet-framework-engineers +/packages/bridge-controller/CHANGELOG.md @MetaMask/swaps-engineers @MetaMask/wallet-framework-engineers \ No newline at end of file From 8fe137ceb7ec444b4bfebc2baf4e726632d48e94 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 05:20:09 +0900 Subject: [PATCH 48/94] Update packages/bridge-controller/src/utils/fetch.test.ts Co-authored-by: Elliot Winkler --- packages/bridge-controller/src/utils/fetch.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 6798f852f59..dcf36bfba98 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -16,10 +16,6 @@ jest.mock('@metamask/controller-utils', () => ({ })); describe('Bridge utils', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('fetchBridgeFeatureFlags', () => { it('should fetch bridge feature flags successfully', async () => { const mockResponse = { From bcc439880d3a1d7e30470d42a2897a677c6b6aa3 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 05:20:46 +0900 Subject: [PATCH 49/94] Update packages/bridge-controller/src/utils/fetch.test.ts Co-authored-by: Elliot Winkler --- packages/bridge-controller/src/utils/fetch.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index dcf36bfba98..b1af7d26ce8 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -16,10 +16,6 @@ jest.mock('@metamask/controller-utils', () => ({ })); describe('Bridge utils', () => { - describe('fetchBridgeFeatureFlags', () => { - it('should fetch bridge feature flags successfully', async () => { - const mockResponse = { - 'extension-config': { refreshRate: 3, maxRefreshCount: 1, support: true, From 085f29d93f07b9d6d3ebe5a6652872a9908d1c70 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:29:47 -0500 Subject: [PATCH 50/94] Revert "Update packages/bridge-controller/src/utils/fetch.test.ts" This reverts commit ee8a1c891b18d043bb097c26121f71bf020a3324. --- packages/bridge-controller/src/utils/fetch.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index b1af7d26ce8..dcf36bfba98 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -16,6 +16,10 @@ jest.mock('@metamask/controller-utils', () => ({ })); describe('Bridge utils', () => { + describe('fetchBridgeFeatureFlags', () => { + it('should fetch bridge feature flags successfully', async () => { + const mockResponse = { + 'extension-config': { refreshRate: 3, maxRefreshCount: 1, support: true, From 5a5e97ae102bc58577c0e1d40d64842e7af42566 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 05:30:25 +0900 Subject: [PATCH 51/94] Update packages/bridge-controller/package.json Co-authored-by: Elliot Winkler --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index ee5e92182b5..9795c38e0ea 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/bridge-controller", - "version": "1.0.0", + "version": "0.0.0", "description": "Manages bridge-related quote fetching functionality for MetaMask", "keywords": [ "MetaMask", From 7f945bee4c9fe6fd7bc8e7ae958f31c982d013e5 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:37:08 -0500 Subject: [PATCH 52/94] chore: use flushPromises from root tests/helpers instead --- packages/bridge-controller/src/bridge-controller.test.ts | 2 +- packages/bridge-controller/src/test/utils.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 packages/bridge-controller/src/test/utils.ts diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index d0de71bddcc..14cc3085d71 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -10,11 +10,11 @@ import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import mockBridgeQuotesErc20Native from './test/mock-quotes-erc20-native.json'; import mockBridgeQuotesNativeErc20Eth from './test/mock-quotes-native-erc20-eth.json'; import mockBridgeQuotesNativeErc20 from './test/mock-quotes-native-erc20.json'; -import { flushPromises } from './test/utils'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; import { getBridgeApiBaseUrl } from './utils'; import * as balanceUtils from './utils/balance'; import * as fetchUtils from './utils/fetch'; +import { flushPromises } from '../../../tests/helpers'; const EMPTY_INIT_STATE = { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, diff --git a/packages/bridge-controller/src/test/utils.ts b/packages/bridge-controller/src/test/utils.ts deleted file mode 100644 index 065602096c5..00000000000 --- a/packages/bridge-controller/src/test/utils.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const flushPromises = () => - new Promise((resolve) => jest.requireActual('timers').setImmediate(resolve)); From 5f7edf18179fb91063ec5552b1e21d1d92481cb0 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 06:39:11 +0900 Subject: [PATCH 53/94] Update packages/bridge-controller/src/constants/chains.ts Co-authored-by: Elliot Winkler --- packages/bridge-controller/src/constants/chains.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/constants/chains.ts b/packages/bridge-controller/src/constants/chains.ts index ae79b6c1929..790e9a8ccdf 100644 --- a/packages/bridge-controller/src/constants/chains.ts +++ b/packages/bridge-controller/src/constants/chains.ts @@ -80,7 +80,6 @@ export const NETWORK_TYPES = { GOERLI: 'goerli', LOCALHOST: 'localhost', MAINNET: 'mainnet', - RPC: 'rpc', SEPOLIA: 'sepolia', LINEA_GOERLI: 'linea-goerli', LINEA_SEPOLIA: 'linea-sepolia', From f450d735fd90753101915109caa21a98e83890ba Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:46:02 -0500 Subject: [PATCH 54/94] chore: move mock files into root test folder --- packages/bridge-controller/src/bridge-controller.test.ts | 6 +++--- packages/bridge-controller/src/utils/fetch.test.ts | 4 ++-- .../bridge-controller}/mock-quotes-erc20-erc20.json | 0 .../bridge-controller}/mock-quotes-erc20-native.json | 0 .../bridge-controller}/mock-quotes-native-erc20-eth.json | 0 .../bridge-controller}/mock-quotes-native-erc20.json | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename {packages/bridge-controller/src/test => tests/bridge-controller}/mock-quotes-erc20-erc20.json (100%) rename {packages/bridge-controller/src/test => tests/bridge-controller}/mock-quotes-erc20-native.json (100%) rename {packages/bridge-controller/src/test => tests/bridge-controller}/mock-quotes-native-erc20-eth.json (100%) rename {packages/bridge-controller/src/test => tests/bridge-controller}/mock-quotes-native-erc20.json (100%) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 14cc3085d71..58468326cf0 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -7,13 +7,13 @@ import BridgeController from './bridge-controller'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; -import mockBridgeQuotesErc20Native from './test/mock-quotes-erc20-native.json'; -import mockBridgeQuotesNativeErc20Eth from './test/mock-quotes-native-erc20-eth.json'; -import mockBridgeQuotesNativeErc20 from './test/mock-quotes-native-erc20.json'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; import { getBridgeApiBaseUrl } from './utils'; import * as balanceUtils from './utils/balance'; import * as fetchUtils from './utils/fetch'; +import mockBridgeQuotesErc20Native from '../../../tests/bridge-controller/mock-quotes-erc20-native.json'; +import mockBridgeQuotesNativeErc20Eth from '../../../tests/bridge-controller/mock-quotes-native-erc20-eth.json'; +import mockBridgeQuotesNativeErc20 from '../../../tests/bridge-controller/mock-quotes-native-erc20.json'; import { flushPromises } from '../../../tests/helpers'; const EMPTY_INIT_STATE = { diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index dcf36bfba98..065eb50b203 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -6,9 +6,9 @@ import { fetchBridgeQuotes, fetchBridgeTokens, } from './fetch'; +import mockBridgeQuotesErc20Erc20 from '../../../../tests/bridge-controller/mock-quotes-erc20-erc20.json'; +import mockBridgeQuotesNativeErc20 from '../../../../tests/bridge-controller/mock-quotes-native-erc20.json'; import { CHAIN_IDS } from '../constants/chains'; -import mockBridgeQuotesErc20Erc20 from '../test/mock-quotes-erc20-erc20.json'; -import mockBridgeQuotesNativeErc20 from '../test/mock-quotes-native-erc20.json'; jest.mock('@metamask/controller-utils', () => ({ ...jest.requireActual('@metamask/controller-utils'), diff --git a/packages/bridge-controller/src/test/mock-quotes-erc20-erc20.json b/tests/bridge-controller/mock-quotes-erc20-erc20.json similarity index 100% rename from packages/bridge-controller/src/test/mock-quotes-erc20-erc20.json rename to tests/bridge-controller/mock-quotes-erc20-erc20.json diff --git a/packages/bridge-controller/src/test/mock-quotes-erc20-native.json b/tests/bridge-controller/mock-quotes-erc20-native.json similarity index 100% rename from packages/bridge-controller/src/test/mock-quotes-erc20-native.json rename to tests/bridge-controller/mock-quotes-erc20-native.json diff --git a/packages/bridge-controller/src/test/mock-quotes-native-erc20-eth.json b/tests/bridge-controller/mock-quotes-native-erc20-eth.json similarity index 100% rename from packages/bridge-controller/src/test/mock-quotes-native-erc20-eth.json rename to tests/bridge-controller/mock-quotes-native-erc20-eth.json diff --git a/packages/bridge-controller/src/test/mock-quotes-native-erc20.json b/tests/bridge-controller/mock-quotes-native-erc20.json similarity index 100% rename from packages/bridge-controller/src/test/mock-quotes-native-erc20.json rename to tests/bridge-controller/mock-quotes-native-erc20.json From 1cf35854c9bd7f1b1a7d80c8b591ff8d3baeade0 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 06:47:37 +0900 Subject: [PATCH 55/94] Update packages/bridge-controller/package.json Co-authored-by: Elliot Winkler --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 9795c38e0ea..b0b66e7ad0a 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -74,7 +74,7 @@ }, "peerDependencies": { "@metamask/network-controller": "^22.0.0", - "@metamask/transaction-controller": "^45.0.0" + "@metamask/accounts-controller": "^23.0.0" }, "engines": { "node": "^18.18 || >=20" From f038584f992cb21d691c010eaf287c02de4638fa Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:53:39 -0500 Subject: [PATCH 56/94] chore: move controller we depend on for communiciation from deps to devDeps, it's already in peerDeps --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index b0b66e7ad0a..61c73245a1d 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -50,7 +50,6 @@ "@metamask/base-controller": "^7.1.1", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", - "@metamask/network-controller": "^22.2.0", "@metamask/polling-controller": "^12.0.2", "@metamask/transaction-controller": "^45.0.0", "@metamask/utils": "^11.1.0", @@ -61,6 +60,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/json-rpc-engine": "^10.0.3", + "@metamask/network-controller": "^22.2.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", From bb9ac4c4b014bcc1c05f23c3dc9e9039b8c5e185 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 06:54:46 +0900 Subject: [PATCH 57/94] Update packages/bridge-controller/package.json Co-authored-by: Elliot Winkler --- packages/bridge-controller/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 61c73245a1d..f7912c6713f 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -57,6 +57,7 @@ "ethers": "^6.12.0" }, "devDependencies": { + "@metamask/accounts-controller": "^23.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/json-rpc-engine": "^10.0.3", From 9e8368e306d8345cb47fef20b5574944d5b0da3f Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:01:06 -0500 Subject: [PATCH 58/94] chore: use FakeProvider from root test folder --- packages/bridge-controller/src/test/provider.ts | 7 ------- packages/bridge-controller/src/utils/balance.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 packages/bridge-controller/src/test/provider.ts diff --git a/packages/bridge-controller/src/test/provider.ts b/packages/bridge-controller/src/test/provider.ts deleted file mode 100644 index 467bf6d6ba2..00000000000 --- a/packages/bridge-controller/src/test/provider.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; - -export const createMockProvider = () => { - const engine = new JsonRpcEngine(); - return new SafeEventEmitterProvider({ engine }); -}; diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts index 8def5a5c4a2..9ee97911572 100644 --- a/packages/bridge-controller/src/utils/balance.test.ts +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -3,7 +3,7 @@ import { zeroAddress } from 'ethereumjs-util'; import { BrowserProvider, Contract } from 'ethers'; import * as balanceUtils from './balance'; -import { createMockProvider } from '../test/provider'; +import { FakeProvider } from '../../../../tests/fake-provider'; declare global { // eslint-disable-next-line no-var @@ -21,7 +21,7 @@ jest.mock('ethers', () => { describe('balance', () => { beforeEach(() => { jest.clearAllMocks(); - global.ethereumProvider = createMockProvider(); + global.ethereumProvider = new FakeProvider(); }); describe('calcLatestSrcBalance', () => { From ff675657853b4b76c0103655583234eb2b3837ba Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:04:04 -0500 Subject: [PATCH 59/94] chore: replace ethereumjs-util with ethers ZeroAddress --- packages/bridge-controller/package.json | 5 ++--- packages/bridge-controller/src/constants/index.ts | 4 ++-- .../bridge-controller/src/utils/balance.test.ts | 6 +++--- packages/bridge-controller/src/utils/balance.ts | 5 ++--- packages/bridge-controller/src/utils/fetch.test.ts | 14 +++++++------- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index f7912c6713f..7cafe1e37ec 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -53,7 +53,6 @@ "@metamask/polling-controller": "^12.0.2", "@metamask/transaction-controller": "^45.0.0", "@metamask/utils": "^11.1.0", - "ethereumjs-util": "^7.0.10", "ethers": "^6.12.0" }, "devDependencies": { @@ -74,8 +73,8 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/network-controller": "^22.0.0", - "@metamask/accounts-controller": "^23.0.0" + "@metamask/accounts-controller": "^23.0.0", + "@metamask/network-controller": "^22.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/bridge-controller/src/constants/index.ts b/packages/bridge-controller/src/constants/index.ts index 3ce1d2379ca..984ccf24ce0 100644 --- a/packages/bridge-controller/src/constants/index.ts +++ b/packages/bridge-controller/src/constants/index.ts @@ -1,5 +1,5 @@ import type { Hex } from '@metamask/utils'; -import { zeroAddress } from 'ethereumjs-util'; +import { ZeroAddress } from 'ethers'; import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './chains'; import type { BridgeControllerState } from '../types'; @@ -64,7 +64,7 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { }, quoteRequest: { walletAddress: undefined, - srcTokenAddress: zeroAddress(), + srcTokenAddress: ZeroAddress, slippage: BRIDGE_DEFAULT_SLIPPAGE, }, quotesInitialLoadTime: undefined, diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts index 9ee97911572..b7d6aebc93b 100644 --- a/packages/bridge-controller/src/utils/balance.test.ts +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -1,5 +1,5 @@ import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; -import { zeroAddress } from 'ethereumjs-util'; +import { ZeroAddress } from 'ethers'; import { BrowserProvider, Contract } from 'ethers'; import * as balanceUtils from './balance'; @@ -57,7 +57,7 @@ describe('balance', () => { await balanceUtils.calcLatestSrcBalance( global.ethereumProvider, '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', - zeroAddress(), + ZeroAddress, '0x789', ), ).toStrictEqual(BigInt(100)); @@ -116,7 +116,7 @@ describe('balance', () => { await balanceUtils.hasSufficientBalance( global.ethereumProvider, '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', - zeroAddress(), + ZeroAddress, '10000000000000000000', '0x1', ), diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts index 084c12c87e3..34a30369d83 100644 --- a/packages/bridge-controller/src/utils/balance.ts +++ b/packages/bridge-controller/src/utils/balance.ts @@ -1,8 +1,7 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Provider } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; -import { zeroAddress } from 'ethereumjs-util'; -import { BrowserProvider, Contract, getAddress } from 'ethers'; +import { BrowserProvider, Contract, getAddress, ZeroAddress } from 'ethers'; export const fetchTokenBalance = async ( address: string, @@ -24,7 +23,7 @@ export const calcLatestSrcBalance = async ( chainId: Hex, ): Promise => { if (tokenAddress && chainId) { - if (tokenAddress === zeroAddress()) { + if (tokenAddress === ZeroAddress) { const ethersProvider = new BrowserProvider(provider); return await ethersProvider.getBalance(getAddress(selectedAddress)); } diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 065eb50b203..7ff257c7231 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -1,5 +1,5 @@ import { handleFetch } from '@metamask/controller-utils'; -import { zeroAddress } from 'ethereumjs-util'; +import { ZeroAddress } from 'ethers'; import { fetchBridgeFeatureFlags, @@ -228,8 +228,8 @@ describe('Bridge utils', () => { walletAddress: '0x123', srcChainId: 1, destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), + srcTokenAddress: ZeroAddress, + destTokenAddress: ZeroAddress, srcTokenAmount: '20000', slippage: 0.5, }, @@ -260,8 +260,8 @@ describe('Bridge utils', () => { walletAddress: '0x123', srcChainId: 1, destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), + srcTokenAddress: ZeroAddress, + destTokenAddress: ZeroAddress, srcTokenAmount: '20000', slippage: 0.5, }, @@ -311,8 +311,8 @@ describe('Bridge utils', () => { walletAddress: '0x123', srcChainId: 1, destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), + srcTokenAddress: ZeroAddress, + destTokenAddress: ZeroAddress, srcTokenAmount: '20000', slippage: 0.5, }, From 9a993cf77b51847b59b726665a903934b3bcdf6f Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:08:53 +0900 Subject: [PATCH 60/94] Update packages/bridge-controller/src/index.ts Co-authored-by: Elliot Winkler --- packages/bridge-controller/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index cd3928cf5c2..ee469f6a0ee 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -49,7 +49,6 @@ export { BRIDGE_MM_FEE_RATE, REFRESH_INTERVAL_MS, DEFAULT_MAX_REFRESH_COUNT, - BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, METABRIDGE_CHAIN_TO_ADDRESS_MAP, } from './constants'; From b416235aa79c1303baf7796d7f54a378d14a7b93 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Sat, 8 Feb 2025 07:10:28 +0900 Subject: [PATCH 61/94] Update packages/bridge-controller/src/index.ts Co-authored-by: Elliot Winkler --- packages/bridge-controller/src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index ee469f6a0ee..55c11d1be90 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -31,8 +31,6 @@ export type { BridgeControllerAction, BridgeControllerActions, BridgeControllerEvents, - AllowedActions, - AllowedEvents, BridgeControllerMessenger, } from './types'; From a97a19b3527cc28c27d106f7f16227ba139c7eb5 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:18:58 -0500 Subject: [PATCH 62/94] chore: update network client retrieval in bridge controller --- packages/bridge-controller/src/bridge-controller.ts | 9 +++++++-- packages/bridge-controller/src/types.ts | 13 ++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index a10c955ab2c..7eb1697e5a4 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -323,9 +323,14 @@ export default class BridgeController extends StaticIntervalPollingController
Date: Fri, 7 Feb 2025 17:20:17 -0500 Subject: [PATCH 63/94] chore: change BridgeController to named export --- packages/bridge-controller/src/bridge-controller.ts | 2 +- packages/bridge-controller/src/index.ts | 2 +- packages/bridge-controller/src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 7eb1697e5a4..12e4f903cc3 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -45,7 +45,7 @@ type BridgePollingInput = { updatedQuoteRequest: QuoteRequest; }; -export default class BridgeController extends StaticIntervalPollingController()< +export class BridgeController extends StaticIntervalPollingController()< typeof BRIDGE_CONTROLLER_NAME, { bridgeState: BridgeControllerState }, BridgeControllerMessenger diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 55c11d1be90..7e84fcb8b47 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -1,4 +1,4 @@ -export { default } from './bridge-controller'; +export { BridgeController } from './bridge-controller'; export type { AssetType, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 761e9048061..af63242a86f 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -11,7 +11,7 @@ import type { import type { Hex } from '@metamask/utils'; import type { BigNumber } from 'bignumber.js'; -import type BridgeController from './bridge-controller'; +import type { BridgeController } from './bridge-controller'; import type { BRIDGE_CONTROLLER_NAME } from './constants'; /** From 4fd76a91d366b7d61b879a760f49da6388797d31 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:30:16 -0500 Subject: [PATCH 64/94] fix: broken import --- packages/bridge-controller/src/bridge-controller.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 58468326cf0..4e29e9e6117 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -3,7 +3,7 @@ import { bigIntToHex } from '@metamask/utils'; import { Contract } from 'ethers'; import nock from 'nock'; -import BridgeController from './bridge-controller'; +import { BridgeController } from './bridge-controller'; import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; From 1436e3dbeba893c67cd361e59d6c474e3b477225 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:39:20 -0500 Subject: [PATCH 65/94] feat: add optional initial state to BridgeController constructor --- packages/bridge-controller/src/bridge-controller.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 12e4f903cc3..67618fbfc88 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -61,9 +61,11 @@ export class BridgeController extends StaticIntervalPollingController; getLayer1GasFee: (params: { transactionParams: TransactionParams; chainId: ChainId; @@ -74,7 +76,10 @@ export class BridgeController extends StaticIntervalPollingController Date: Tue, 11 Feb 2025 10:53:53 -0500 Subject: [PATCH 66/94] chore: extract default bridge controller state method --- packages/bridge-controller/src/bridge-controller.ts | 4 ++-- packages/bridge-controller/src/utils/index.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 67618fbfc88..a0d056e544a 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -25,7 +25,7 @@ import { RequestStatus, } from './types'; import type { BridgeControllerMessenger } from './types'; -import { sumHexes } from './utils'; +import { getDefaultBridgeControllerState, sumHexes } from './utils'; import { hasSufficientBalance } from './utils/balance'; import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; import { isValidQuoteRequest } from './utils/quote'; @@ -77,7 +77,7 @@ export class BridgeController extends StaticIntervalPollingController { + return DEFAULT_BRIDGE_CONTROLLER_STATE; +}; export const getBridgeApiBaseUrl = () => { if (process.env.BRIDGE_CUSTOM_API_BASE_URL) { From 1b9f6eb13deaecb04faba71a26035ad8cae1c2fc Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:04:59 -0500 Subject: [PATCH 67/94] chore: set state directly in updates rather than making fresh objs --- .../src/bridge-controller.ts | 89 ++++++++----------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index a0d056e544a..75b8af8465d 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -117,25 +117,24 @@ export class BridgeController extends StaticIntervalPollingController { - _state.bridgeState = { - ...bridgeState, - quoteRequest: updatedQuoteRequest, - quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, - quotesLoadingStatus: - DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, - quoteFetchError: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError, - quotesRefreshCount: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount, - quotesInitialLoadTime: - DEFAULT_BRIDGE_CONTROLLER_STATE.quotesInitialLoadTime, - }; + this.update((state) => { + state.bridgeState.quoteRequest = updatedQuoteRequest; + state.bridgeState.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes; + state.bridgeState.quotesLastFetched = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched; + state.bridgeState.quotesLoadingStatus = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus; + state.bridgeState.quoteFetchError = + DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError; + state.bridgeState.quotesRefreshCount = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount; + state.bridgeState.quotesInitialLoadTime = + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesInitialLoadTime; }); if (isValidQuoteRequest(updatedQuoteRequest)) { @@ -180,20 +179,19 @@ export class BridgeController extends StaticIntervalPollingController { - _state.bridgeState = { + this.update((state) => { + state.bridgeState = { ...DEFAULT_BRIDGE_CONTROLLER_STATE, quotes: [], - bridgeFeatureFlags: _state.bridgeState.bridgeFeatureFlags, + bridgeFeatureFlags: state.bridgeState.bridgeFeatureFlags, }; }); }; setBridgeFeatureFlags = async () => { - const { bridgeState } = this.state; const bridgeFeatureFlags = await fetchBridgeFeatureFlags(); - this.update((_state) => { - _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; + this.update((state) => { + state.bridgeState.bridgeFeatureFlags = bridgeFeatureFlags; }); this.setIntervalLength( bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].refreshRate, @@ -204,19 +202,17 @@ export class BridgeController extends StaticIntervalPollingController { + const { bridgeState } = this.state; this.#abortController?.abort('New quote request'); this.#abortController = new AbortController(); if (updatedQuoteRequest.srcChainId === updatedQuoteRequest.destChainId) { return; } - const { bridgeState } = this.state; - this.update((_state) => { - _state.bridgeState = { - ...bridgeState, - quotesLoadingStatus: RequestStatus.LOADING, - quoteRequest: updatedQuoteRequest, - quoteFetchError: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError, - }; + this.update((state) => { + state.bridgeState.quotesLoadingStatus = RequestStatus.LOADING; + state.bridgeState.quoteRequest = updatedQuoteRequest; + state.bridgeState.quoteFetchError = + DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError; }); try { @@ -231,12 +227,9 @@ export class BridgeController extends StaticIntervalPollingController { - _state.bridgeState = { - ..._state.bridgeState, - quotes: quotesWithL1GasFees, - quotesLoadingStatus: RequestStatus.FETCHED, - }; + this.update((state) => { + state.bridgeState.quotes = quotesWithL1GasFees; + state.bridgeState.quotesLoadingStatus = RequestStatus.FETCHED; }); } catch (error) { const isAbortError = (error as Error).name === 'AbortError'; @@ -245,13 +238,10 @@ export class BridgeController extends StaticIntervalPollingController { - _state.bridgeState = { - ...bridgeState, - quoteFetchError: - error instanceof Error ? error.message : 'Unknown error', - quotesLoadingStatus: RequestStatus.ERROR, - }; + this.update((state) => { + state.bridgeState.quoteFetchError = + error instanceof Error ? error.message : 'Unknown error'; + state.bridgeState.quotesLoadingStatus = RequestStatus.ERROR; }); console.log('Failed to fetch bridge quotes', error); } finally { @@ -270,16 +260,13 @@ export class BridgeController extends StaticIntervalPollingController { - _state.bridgeState = { - ..._state.bridgeState, - quotesInitialLoadTime: - updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched - ? quotesLastFetched - this.#quotesFirstFetched - : bridgeState.quotesInitialLoadTime, - quotesLastFetched, - quotesRefreshCount: updatedQuotesRefreshCount, - }; + this.update((state) => { + state.bridgeState.quotesInitialLoadTime = + updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched + ? quotesLastFetched - this.#quotesFirstFetched + : bridgeState.quotesInitialLoadTime; + state.bridgeState.quotesLastFetched = quotesLastFetched; + state.bridgeState.quotesRefreshCount = updatedQuotesRefreshCount; }); } }; From abf2f21ee0fee2e0fa2f6c95df681b692c39eca0 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:06:33 -0500 Subject: [PATCH 68/94] refactor: improve quote request validation with consistent variable naming --- packages/bridge-controller/src/utils/quote.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index 25238c7e540..8ea616fd345 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -4,14 +4,14 @@ export const isValidQuoteRequest = ( partialRequest: Partial, requireAmount = true, ): partialRequest is QuoteRequest => { - const STRING_FIELDS = ['srcTokenAddress', 'destTokenAddress']; + const stringFields = ['srcTokenAddress', 'destTokenAddress']; if (requireAmount) { - STRING_FIELDS.push('srcTokenAmount'); + stringFields.push('srcTokenAmount'); } - const NUMBER_FIELDS = ['srcChainId', 'destChainId', 'slippage']; + const numberFields = ['srcChainId', 'destChainId', 'slippage']; return ( - STRING_FIELDS.every( + stringFields.every( (field) => field in partialRequest && typeof partialRequest[field as keyof typeof partialRequest] === @@ -20,7 +20,7 @@ export const isValidQuoteRequest = ( partialRequest[field as keyof typeof partialRequest] !== '' && partialRequest[field as keyof typeof partialRequest] !== null, ) && - NUMBER_FIELDS.every( + numberFields.every( (field) => field in partialRequest && typeof partialRequest[field as keyof typeof partialRequest] === From e6597a12265d3afa955c255cb147b4faa91e51b8 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 01:07:07 +0900 Subject: [PATCH 69/94] Update packages/bridge-controller/tsconfig.json Co-authored-by: Elliot Winkler --- packages/bridge-controller/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index ba5b6b86c55..3f93de1f5e6 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -5,10 +5,11 @@ "resolveJsonModule": true }, "references": [ + { "path": "../accounts-controller" }, { "path": "../base-controller" }, { "path": "../controller-utils" }, - { "path": "../polling-controller" }, { "path": "../network-controller" }, + { "path": "../polling-controller" }, { "path": "../transaction-controller" } ], "include": ["../../types", "./src"] From 80be6b5e666bfe56b25d1f39723336050b6f3251 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 01:07:16 +0900 Subject: [PATCH 70/94] Update packages/bridge-controller/tsconfig.build.json Co-authored-by: Elliot Winkler --- packages/bridge-controller/tsconfig.build.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/tsconfig.build.json b/packages/bridge-controller/tsconfig.build.json index 5d38b996867..b62ec3ff054 100644 --- a/packages/bridge-controller/tsconfig.build.json +++ b/packages/bridge-controller/tsconfig.build.json @@ -7,13 +7,11 @@ }, "references": [ { "path": "../accounts-controller/tsconfig.build.json" }, - { "path": "../approval-controller/tsconfig.build.json" }, { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, - { "path": "../keyring-controller/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, - { "path": "../preferences-controller/tsconfig.build.json" }, - { "path": "../polling-controller/tsconfig.build.json" } + { "path": "../polling-controller/tsconfig.build.json" }, + { "path": "../transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } From 7ca25052046eb904353553546c8d928ae384cab5 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:55:17 -0500 Subject: [PATCH 71/94] chore: reorganize constants, no more index --- .../src/bridge-controller.test.ts | 2 +- .../src/bridge-controller.ts | 6 +- .../bridge-controller/src/constants/bridge.ts | 65 +++++++++++++++ .../bridge-controller/src/constants/chains.ts | 16 ++++ .../bridge-controller/src/constants/index.ts | 80 ------------------- packages/bridge-controller/src/index.ts | 26 +----- packages/bridge-controller/src/types.ts | 2 +- packages/bridge-controller/src/utils/fetch.ts | 7 +- .../bridge-controller/src/utils/index.test.ts | 7 +- packages/bridge-controller/src/utils/index.ts | 4 +- 10 files changed, 102 insertions(+), 113 deletions(-) create mode 100644 packages/bridge-controller/src/constants/bridge.ts diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 4e29e9e6117..c22b47068df 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -4,7 +4,7 @@ import { Contract } from 'ethers'; import nock from 'nock'; import { BridgeController } from './bridge-controller'; -import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; +import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 75b8af8465d..fe63f3ceca4 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -8,12 +8,12 @@ import { numberToHex } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { BrowserProvider, Contract } from 'ethers'; -import { REFRESH_INTERVAL_MS } from './constants'; +import { REFRESH_INTERVAL_MS } from './constants/bridge'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, - METABRIDGE_CHAIN_TO_ADDRESS_MAP, -} from './constants'; + METABRIDGE_CHAIN_TO_ADDRESS_MAP +} from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; import { type L1GasFees, diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts new file mode 100644 index 00000000000..20c64e972a6 --- /dev/null +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -0,0 +1,65 @@ +import type { Hex } from '@metamask/utils'; +import { ZeroAddress } from 'ethers'; + +import { CHAIN_IDS } from './chains'; +import type { BridgeControllerState } from '../types'; +import { BridgeFeatureFlagsKey } from '../types'; + +// TODO read from feature flags +export const ALLOWED_BRIDGE_CHAIN_IDS = [ + CHAIN_IDS.MAINNET, + CHAIN_IDS.BSC, + CHAIN_IDS.POLYGON, + CHAIN_IDS.ZKSYNC_ERA, + CHAIN_IDS.AVALANCHE, + CHAIN_IDS.OPTIMISM, + CHAIN_IDS.ARBITRUM, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.BASE, +]; + +export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; + +export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; +export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; +export const BRIDGE_CLIENT_ID_EXTENSION = 'extension'; +export const BRIDGE_CLIENT_ID_MOBILE = 'mobile'; + +export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; +export const METABRIDGE_ETHEREUM_ADDRESS = + '0x0439e60F02a8900a951603950d8D4527f400C3f1'; +export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour +export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it + +export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; +export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; +export const BRIDGE_MM_FEE_RATE = 0.875; +export const REFRESH_INTERVAL_MS = 30 * 1000; +export const DEFAULT_MAX_REFRESH_COUNT = 5; + +export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; +export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { + refreshRate: REFRESH_INTERVAL_MS, + maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, + support: false, + chains: {}, + }, + }, + quoteRequest: { + walletAddress: undefined, + srcTokenAddress: ZeroAddress, + slippage: BRIDGE_DEFAULT_SLIPPAGE, + }, + quotesInitialLoadTime: undefined, + quotes: [], + quotesLastFetched: undefined, + quotesLoadingStatus: undefined, + quoteFetchError: undefined, + quotesRefreshCount: 0, +}; + +export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { + [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, +}; diff --git a/packages/bridge-controller/src/constants/chains.ts b/packages/bridge-controller/src/constants/chains.ts index 790e9a8ccdf..abf24411276 100644 --- a/packages/bridge-controller/src/constants/chains.ts +++ b/packages/bridge-controller/src/constants/chains.ts @@ -1,3 +1,5 @@ +import type { AllowedBridgeChainIds } from './bridge'; + /** * An object containing all of the chain ids for networks both built in and * those that we have added custom code to support our feature set. @@ -155,3 +157,17 @@ export const NETWORK_TO_NAME_MAP = { [CHAIN_IDS.LISK]: LISK_DISPLAY_NAME, [CHAIN_IDS.LISK_SEPOLIA]: LISK_SEPOLIA_DISPLAY_NAME, } as const; +export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< + AllowedBridgeChainIds, + string +> = { + [CHAIN_IDS.MAINNET]: 'Ethereum', + [CHAIN_IDS.LINEA_MAINNET]: 'Linea', + [CHAIN_IDS.POLYGON]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], + [CHAIN_IDS.AVALANCHE]: 'Avalanche', + [CHAIN_IDS.BSC]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], + [CHAIN_IDS.ARBITRUM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], + [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], + [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', + [CHAIN_IDS.BASE]: 'Base', +}; diff --git a/packages/bridge-controller/src/constants/index.ts b/packages/bridge-controller/src/constants/index.ts index 984ccf24ce0..e69de29bb2d 100644 --- a/packages/bridge-controller/src/constants/index.ts +++ b/packages/bridge-controller/src/constants/index.ts @@ -1,80 +0,0 @@ -import type { Hex } from '@metamask/utils'; -import { ZeroAddress } from 'ethers'; - -import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './chains'; -import type { BridgeControllerState } from '../types'; -import { BridgeFeatureFlagsKey } from '../types'; - -// TODO read from feature flags -export const ALLOWED_BRIDGE_CHAIN_IDS = [ - CHAIN_IDS.MAINNET, - CHAIN_IDS.BSC, - CHAIN_IDS.POLYGON, - CHAIN_IDS.ZKSYNC_ERA, - CHAIN_IDS.AVALANCHE, - CHAIN_IDS.OPTIMISM, - CHAIN_IDS.ARBITRUM, - CHAIN_IDS.LINEA_MAINNET, - CHAIN_IDS.BASE, -]; - -export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; - -export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; -export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; - -export const BRIDGE_CLIENT_ID = 'extension'; - -export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; -export const METABRIDGE_ETHEREUM_ADDRESS = - '0x0439e60F02a8900a951603950d8D4527f400C3f1'; -export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour -export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it - -export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; -export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; - -export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< - AllowedBridgeChainIds, - string -> = { - [CHAIN_IDS.MAINNET]: 'Ethereum', - [CHAIN_IDS.LINEA_MAINNET]: 'Linea', - [CHAIN_IDS.POLYGON]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], - [CHAIN_IDS.AVALANCHE]: 'Avalanche', - [CHAIN_IDS.BSC]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], - [CHAIN_IDS.ARBITRUM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], - [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], - [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', - [CHAIN_IDS.BASE]: 'Base', -}; -export const BRIDGE_MM_FEE_RATE = 0.875; -export const REFRESH_INTERVAL_MS = 30 * 1000; -export const DEFAULT_MAX_REFRESH_COUNT = 5; - -export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; -export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { - bridgeFeatureFlags: { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: REFRESH_INTERVAL_MS, - maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, - support: false, - chains: {}, - }, - }, - quoteRequest: { - walletAddress: undefined, - srcTokenAddress: ZeroAddress, - slippage: BRIDGE_DEFAULT_SLIPPAGE, - }, - quotesInitialLoadTime: undefined, - quotes: [], - quotesLastFetched: undefined, - quotesLoadingStatus: undefined, - quoteFetchError: undefined, - quotesRefreshCount: 0, -}; - -export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { - [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, -}; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 7e84fcb8b47..299d584afea 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -36,39 +36,21 @@ export type { export { ALLOWED_BRIDGE_CHAIN_IDS, - BRIDGE_CLIENT_ID, - ETH_USDT_ADDRESS, + BRIDGE_CLIENT_ID_EXTENSION, + BRIDGE_CLIENT_ID_MOBILE, METABRIDGE_ETHEREUM_ADDRESS, BRIDGE_QUOTE_MAX_ETA_SECONDS, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, BRIDGE_PREFERRED_GAS_ESTIMATE, BRIDGE_DEFAULT_SLIPPAGE, - NETWORK_TO_SHORT_NETWORK_NAME_MAP, BRIDGE_MM_FEE_RATE, REFRESH_INTERVAL_MS, DEFAULT_MAX_REFRESH_COUNT, DEFAULT_BRIDGE_CONTROLLER_STATE, METABRIDGE_CHAIN_TO_ADDRESS_MAP, -} from './constants'; +} from './constants/bridge'; -export type { AllowedBridgeChainIds } from './constants'; - -export { - CURRENCY_SYMBOLS, - ETH_SWAPS_TOKEN_OBJECT, - BNB_SWAPS_TOKEN_OBJECT, - MATIC_SWAPS_TOKEN_OBJECT, - AVAX_SWAPS_TOKEN_OBJECT, - TEST_ETH_SWAPS_TOKEN_OBJECT, - GOERLI_SWAPS_TOKEN_OBJECT, - SEPOLIA_SWAPS_TOKEN_OBJECT, - ARBITRUM_SWAPS_TOKEN_OBJECT, - OPTIMISM_SWAPS_TOKEN_OBJECT, - ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, - LINEA_SWAPS_TOKEN_OBJECT, - BASE_SWAPS_TOKEN_OBJECT, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, -} from './constants/tokens'; +export type { AllowedBridgeChainIds } from './constants/bridge'; export type { SwapsTokenObject } from './constants/tokens'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index af63242a86f..f47bc3c651f 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -12,7 +12,7 @@ import type { Hex } from '@metamask/utils'; import type { BigNumber } from 'bignumber.js'; import type { BridgeController } from './bridge-controller'; -import type { BRIDGE_CONTROLLER_NAME } from './constants'; +import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge'; /** * The types of assets that a user can send diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 46e51bd24ad..f50ab23d5e4 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -16,7 +16,10 @@ import { QUOTE_RESPONSE_VALIDATORS, FEE_DATA_VALIDATORS, } from './validators'; -import { BRIDGE_CLIENT_ID, REFRESH_INTERVAL_MS } from '../constants'; +import { + REFRESH_INTERVAL_MS, + BRIDGE_CLIENT_ID_EXTENSION, +} from '../constants/bridge'; import type { SwapsTokenObject } from '../constants/tokens'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { @@ -30,7 +33,7 @@ import type { } from '../types'; import { BridgeFlag, FeeType, BridgeFeatureFlagsKey } from '../types'; -const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; +const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID_EXTENSION }; // TODO put this back in once we have a fetchWithCache equivalent // const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/index.test.ts index beacf3e8944..eeb2d3dbff6 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/index.test.ts @@ -11,11 +11,14 @@ import { sumHexes, getBridgeApiBaseUrl, } from '.'; -import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS } from '../constants'; +import { + ETH_USDT_ADDRESS, + METABRIDGE_ETHEREUM_ADDRESS, +} from '../constants/bridge'; import { BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, -} from '../constants'; +} from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/index.ts index 90e6b3ced69..502772ec3a5 100644 --- a/packages/bridge-controller/src/utils/index.ts +++ b/packages/bridge-controller/src/utils/index.ts @@ -3,12 +3,12 @@ import type { Hex } from '@metamask/utils'; import { Contract } from 'ethers'; import { + DEFAULT_BRIDGE_CONTROLLER_STATE, BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, - DEFAULT_BRIDGE_CONTROLLER_STATE, ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS, -} from '../constants'; +} from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { BridgeControllerState } from '../types'; From 44618a3492a22b70e6864661fe0b35fb60ef8764 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:55:23 -0500 Subject: [PATCH 72/94] chore: delete constants index --- packages/bridge-controller/src/constants/index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/bridge-controller/src/constants/index.ts diff --git a/packages/bridge-controller/src/constants/index.ts b/packages/bridge-controller/src/constants/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 From c7525e4318236a40ad7369e6bd9f0e018e066cb1 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 12:57:05 -0500 Subject: [PATCH 73/94] chore: remove bridge utils index and put into bridge utils file --- packages/bridge-controller/src/bridge-controller.test.ts | 2 +- packages/bridge-controller/src/bridge-controller.ts | 4 ++-- packages/bridge-controller/src/index.ts | 6 +++++- .../src/utils/{index.test.ts => bridge.test.ts} | 2 +- .../bridge-controller/src/utils/{index.ts => bridge.ts} | 6 +++--- packages/bridge-controller/src/utils/fetch.ts | 4 ++-- 6 files changed, 14 insertions(+), 10 deletions(-) rename packages/bridge-controller/src/utils/{index.test.ts => bridge.test.ts} (99%) rename packages/bridge-controller/src/utils/{index.ts => bridge.ts} (100%) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index c22b47068df..f8b8f41e4fc 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -8,7 +8,7 @@ import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; -import { getBridgeApiBaseUrl } from './utils'; +import { getBridgeApiBaseUrl } from './utils/bridge'; import * as balanceUtils from './utils/balance'; import * as fetchUtils from './utils/fetch'; import mockBridgeQuotesErc20Native from '../../../tests/bridge-controller/mock-quotes-erc20-native.json'; diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index fe63f3ceca4..9f12bcd6464 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -12,7 +12,7 @@ import { REFRESH_INTERVAL_MS } from './constants/bridge'; import { BRIDGE_CONTROLLER_NAME, DEFAULT_BRIDGE_CONTROLLER_STATE, - METABRIDGE_CHAIN_TO_ADDRESS_MAP + METABRIDGE_CHAIN_TO_ADDRESS_MAP, } from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; import { @@ -25,8 +25,8 @@ import { RequestStatus, } from './types'; import type { BridgeControllerMessenger } from './types'; -import { getDefaultBridgeControllerState, sumHexes } from './utils'; import { hasSufficientBalance } from './utils/balance'; +import { getDefaultBridgeControllerState, sumHexes } from './utils/bridge'; import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; import { isValidQuoteRequest } from './utils/quote'; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 299d584afea..5418412b21a 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -56,4 +56,8 @@ export type { SwapsTokenObject } from './constants/tokens'; export { SWAPS_API_V2_BASE_URL } from './constants/swaps'; -export { getEthUsdtResetData, isEthUsdt, getBridgeApiBaseUrl } from './utils'; +export { + getEthUsdtResetData, + isEthUsdt, + getBridgeApiBaseUrl, +} from './utils/bridge'; diff --git a/packages/bridge-controller/src/utils/index.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts similarity index 99% rename from packages/bridge-controller/src/utils/index.test.ts rename to packages/bridge-controller/src/utils/bridge.test.ts index eeb2d3dbff6..013e9bf63eb 100644 --- a/packages/bridge-controller/src/utils/index.test.ts +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -10,7 +10,7 @@ import { isSwapsDefaultTokenSymbol, sumHexes, getBridgeApiBaseUrl, -} from '.'; +} from './bridge'; import { ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS, diff --git a/packages/bridge-controller/src/utils/index.ts b/packages/bridge-controller/src/utils/bridge.ts similarity index 100% rename from packages/bridge-controller/src/utils/index.ts rename to packages/bridge-controller/src/utils/bridge.ts index 502772ec3a5..b152c84ef09 100644 --- a/packages/bridge-controller/src/utils/index.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -28,12 +28,12 @@ export const getBridgeApiBaseUrl = () => { return BRIDGE_PROD_API_BASE_URL; }; - /** * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum * * @returns The txParam data that will reset allowance to 0, combine it with the approval tx params received from Bridge API */ + export const getEthUsdtResetData = () => { const UsdtContractInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) .interface; @@ -57,7 +57,6 @@ export const sumHexes = (...hexStrings: string[]): Hex => { const sum = hexStrings.reduce((acc, hex) => acc + BigInt(hex), BigInt(0)); return `0x${sum.toString(16)}`; }; - /** * Checks whether the provided address is strictly equal to the address for * the default swaps token of the provided chain. @@ -66,6 +65,7 @@ export const sumHexes = (...hexStrings: string[]): Hex => { * @param chainId - The hex encoded chain ID of the default swaps token to check * @returns Whether the address is the provided chain's default token address */ + export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { if (!address || !chainId) { return false; @@ -78,7 +78,6 @@ export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { ]?.address ); }; - /** * Checks whether the provided symbol is strictly equal to the symbol for * the default swaps token of the provided chain. @@ -87,6 +86,7 @@ export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { * @param chainId - The hex encoded chain ID of the default swaps token to check * @returns Whether the symbol is the provided chain's default token symbol */ + export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { if (!symbol || !chainId) { return false; diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index f50ab23d5e4..a139ccf545b 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -5,8 +5,8 @@ import { hexToNumber, numberToHex } from '@metamask/utils'; import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, - getBridgeApiBaseUrl, -} from '.'; + getBridgeApiBaseUrl +} from './bridge'; import { FEATURE_FLAG_VALIDATORS, QUOTE_VALIDATORS, From c5c48a539e5c96142eafe2c99063c82fc4acd842 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:10:04 -0500 Subject: [PATCH 74/94] feat: add platform specific client ID to bridge controller constructor --- .../src/bridge-controller.test.ts | 11 ++++++-- .../src/bridge-controller.ts | 12 ++++++--- .../bridge-controller/src/utils/fetch.test.ts | 18 +++++++++---- packages/bridge-controller/src/utils/fetch.ts | 27 ++++++++++++------- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index f8b8f41e4fc..ebea02678c3 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -4,12 +4,15 @@ import { Contract } from 'ethers'; import nock from 'nock'; import { BridgeController } from './bridge-controller'; -import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants/bridge'; +import { + BRIDGE_CLIENT_ID_EXTENSION, + DEFAULT_BRIDGE_CONTROLLER_STATE, +} from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; import type { BridgeControllerMessenger, QuoteResponse } from './types'; -import { getBridgeApiBaseUrl } from './utils/bridge'; import * as balanceUtils from './utils/balance'; +import { getBridgeApiBaseUrl } from './utils/bridge'; import * as fetchUtils from './utils/fetch'; import mockBridgeQuotesErc20Native from '../../../tests/bridge-controller/mock-quotes-erc20-native.json'; import mockBridgeQuotesNativeErc20Eth from '../../../tests/bridge-controller/mock-quotes-native-erc20-eth.json'; @@ -43,6 +46,7 @@ describe('BridgeController', function () { bridgeController = new BridgeController({ messenger: messengerMock, getLayer1GasFee: getLayer1GasFeeMock, + clientId: BRIDGE_CLIENT_ID_EXTENSION, }); }); @@ -313,6 +317,7 @@ describe('BridgeController', function () { insufficientBal: false, }, expect.any(AbortSignal), + BRIDGE_CLIENT_ID_EXTENSION, ); expect( bridgeController.state.bridgeState.quotesLastFetched, @@ -464,6 +469,7 @@ describe('BridgeController', function () { insufficientBal: true, }, expect.any(AbortSignal), + BRIDGE_CLIENT_ID_EXTENSION, ); expect( bridgeController.state.bridgeState.quotesLastFetched, @@ -660,6 +666,7 @@ describe('BridgeController', function () { insufficientBal: true, }, expect.any(AbortSignal), + BRIDGE_CLIENT_ID_EXTENSION, ); expect( bridgeController.state.bridgeState.quotesLastFetched, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 9f12bcd6464..bf3c3f5870c 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -54,6 +54,8 @@ export class BridgeController extends StaticIntervalPollingController; + clientId: string; getLayer1GasFee: (params: { transactionParams: TransactionParams; chainId: ChainId; @@ -86,6 +90,9 @@ export class BridgeController extends StaticIntervalPollingController { @@ -189,7 +194,7 @@ export class BridgeController extends StaticIntervalPollingController { - const bridgeFeatureFlags = await fetchBridgeFeatureFlags(); + const bridgeFeatureFlags = await fetchBridgeFeatureFlags(this.#clientId); this.update((state) => { state.bridgeState.bridgeFeatureFlags = bridgeFeatureFlags; }); @@ -223,6 +228,7 @@ export class BridgeController extends StaticIntervalPollingController ({ @@ -54,7 +55,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockResolvedValue(mockResponse); - const result = await fetchBridgeFeatureFlags(); + const result = await fetchBridgeFeatureFlags(BRIDGE_CLIENT_ID_EXTENSION); expect(handleFetch).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', @@ -119,7 +120,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockResolvedValue(mockResponse); - const result = await fetchBridgeFeatureFlags(); + const result = await fetchBridgeFeatureFlags(BRIDGE_CLIENT_ID_EXTENSION); expect(handleFetch).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', @@ -143,7 +144,9 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockRejectedValue(mockError); - await expect(fetchBridgeFeatureFlags()).rejects.toThrow(mockError); + await expect( + fetchBridgeFeatureFlags(BRIDGE_CLIENT_ID_EXTENSION), + ).rejects.toThrow(mockError); }); }); @@ -178,7 +181,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockResolvedValue(mockResponse); - const result = await fetchBridgeTokens('0xa'); + const result = await fetchBridgeTokens('0xa', BRIDGE_CLIENT_ID_EXTENSION); expect(handleFetch).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', @@ -214,7 +217,9 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockRejectedValue(mockError); - await expect(fetchBridgeTokens('0xa')).rejects.toThrow(mockError); + await expect( + fetchBridgeTokens('0xa', BRIDGE_CLIENT_ID_EXTENSION), + ).rejects.toThrow(mockError); }); }); @@ -234,6 +239,7 @@ describe('Bridge utils', () => { slippage: 0.5, }, signal, + BRIDGE_CLIENT_ID_EXTENSION, ); expect(handleFetch).toHaveBeenCalledWith( @@ -266,6 +272,7 @@ describe('Bridge utils', () => { slippage: 0.5, }, signal, + BRIDGE_CLIENT_ID_EXTENSION, ); expect(handleFetch).toHaveBeenCalledWith( @@ -317,6 +324,7 @@ describe('Bridge utils', () => { slippage: 0.5, }, signal, + BRIDGE_CLIENT_ID_EXTENSION, ); expect(handleFetch).toHaveBeenCalledWith( diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index a139ccf545b..f9a66916fbc 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -5,7 +5,7 @@ import { hexToNumber, numberToHex } from '@metamask/utils'; import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, - getBridgeApiBaseUrl + getBridgeApiBaseUrl, } from './bridge'; import { FEATURE_FLAG_VALIDATORS, @@ -16,10 +16,7 @@ import { QUOTE_RESPONSE_VALIDATORS, FEE_DATA_VALIDATORS, } from './validators'; -import { - REFRESH_INTERVAL_MS, - BRIDGE_CLIENT_ID_EXTENSION, -} from '../constants/bridge'; +import { REFRESH_INTERVAL_MS } from '../constants/bridge'; import type { SwapsTokenObject } from '../constants/tokens'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { @@ -33,19 +30,25 @@ import type { } from '../types'; import { BridgeFlag, FeeType, BridgeFeatureFlagsKey } from '../types'; -const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID_EXTENSION }; // TODO put this back in once we have a fetchWithCache equivalent // const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; +export const getClientIdHeader = (clientId: string) => ({ + 'X-Client-Id': clientId, +}); + /** * Fetches the bridge feature flags * + * @param clientId - The client ID for metrics * @returns The bridge feature flags */ -export async function fetchBridgeFeatureFlags(): Promise { +export async function fetchBridgeFeatureFlags( + clientId: string, +): Promise { const url = `${getBridgeApiBaseUrl()}/getAllFeatureFlags`; const rawFeatureFlags = await handleFetch(url, { - headers: CLIENT_ID_HEADER, + headers: getClientIdHeader(clientId), }); if ( @@ -85,10 +88,12 @@ export async function fetchBridgeFeatureFlags(): Promise { * Returns a list of enabled (unblocked) tokens * * @param chainId - The chain ID to fetch tokens for + * @param clientId - The client ID for metrics * @returns A list of enabled (unblocked) tokens */ export async function fetchBridgeTokens( chainId: Hex, + clientId: string, ): Promise> { // TODO make token api v2 call const url = `${getBridgeApiBaseUrl()}/getTokens?chainId=${hexToNumber( @@ -99,7 +104,7 @@ export async function fetchBridgeTokens( // If we allow selecting dest networks which the user has not imported, // note that the Assets controller won't be able to provide tokens. In extension we fetch+cache the token list from bridge-api to handle this const tokens = await handleFetch(url, { - headers: CLIENT_ID_HEADER, + headers: getClientIdHeader(clientId), }); const nativeToken = @@ -131,11 +136,13 @@ export async function fetchBridgeTokens( * * @param request - The quote request * @param signal - The abort signal + * @param clientId - The client ID for metrics * @returns A list of bridge tx quotes */ export async function fetchBridgeQuotes( request: QuoteRequest, signal: AbortSignal, + clientId: string, ): Promise { const queryParams = new URLSearchParams({ walletAddress: request.walletAddress, @@ -150,7 +157,7 @@ export async function fetchBridgeQuotes( }); const url = `${getBridgeApiBaseUrl()}/getQuote?${queryParams}`; const quotes = await handleFetch(url, { - headers: CLIENT_ID_HEADER, + headers: getClientIdHeader(clientId), signal, }); From 58e20e04c275a0894685b186e227ed154333fe36 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:12:50 -0500 Subject: [PATCH 75/94] chore: remove unneeded export --- packages/bridge-controller/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 5418412b21a..2359187c7b0 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -38,7 +38,6 @@ export { ALLOWED_BRIDGE_CHAIN_IDS, BRIDGE_CLIENT_ID_EXTENSION, BRIDGE_CLIENT_ID_MOBILE, - METABRIDGE_ETHEREUM_ADDRESS, BRIDGE_QUOTE_MAX_ETA_SECONDS, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, BRIDGE_PREFERRED_GAS_ESTIMATE, From 9eccf38b53bc834ec8e85181b5a2a8f80b46945a Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:13:39 -0500 Subject: [PATCH 76/94] chore: bump deps --- packages/bridge-controller/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 7cafe1e37ec..26876c74eef 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -50,17 +50,17 @@ "@metamask/base-controller": "^7.1.1", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", - "@metamask/polling-controller": "^12.0.2", - "@metamask/transaction-controller": "^45.0.0", + "@metamask/polling-controller": "^12.0.3", + "@metamask/transaction-controller": "^45.1.0", "@metamask/utils": "^11.1.0", "ethers": "^6.12.0" }, "devDependencies": { - "@metamask/accounts-controller": "^23.0.0", + "@metamask/accounts-controller": "^23.0.1", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/json-rpc-engine": "^10.0.3", - "@metamask/network-controller": "^22.2.0", + "@metamask/network-controller": "^22.2.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", From 6f9d5fc5072399b2203475d627f8a93f5ae5d231 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:16:36 -0500 Subject: [PATCH 77/94] chore: bump base controller dep --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 26876c74eef..75e3511c03f 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.1", + "@metamask/base-controller": "^8.0.0", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.3", From 8eebb185816dc4576e483f6e82146cfcd5f49ca3 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:18:24 -0500 Subject: [PATCH 78/94] Revert "chore: bump base controller dep" This reverts commit 65ef8ed35791a9d838a10e8c84f0f2c1877ae0fe. --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 75e3511c03f..26876c74eef 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^8.0.0", + "@metamask/base-controller": "^7.1.1", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.3", From 283f59a41029ead92cd855a962bb9f5357669b3d Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:26:19 -0500 Subject: [PATCH 79/94] refactor: convert bridge client ID to enum --- .../src/bridge-controller.test.ts | 10 +++++----- .../bridge-controller/src/bridge-controller.ts | 3 ++- .../bridge-controller/src/constants/bridge.ts | 7 +++++-- .../bridge-controller/src/utils/fetch.test.ts | 18 +++++++++--------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index ebea02678c3..f688d5b0735 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -5,7 +5,7 @@ import nock from 'nock'; import { BridgeController } from './bridge-controller'; import { - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId, DEFAULT_BRIDGE_CONTROLLER_STATE, } from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; @@ -46,7 +46,7 @@ describe('BridgeController', function () { bridgeController = new BridgeController({ messenger: messengerMock, getLayer1GasFee: getLayer1GasFeeMock, - clientId: BRIDGE_CLIENT_ID_EXTENSION, + clientId: BridgeClientId.EXTENSION, }); }); @@ -317,7 +317,7 @@ describe('BridgeController', function () { insufficientBal: false, }, expect.any(AbortSignal), - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId.EXTENSION, ); expect( bridgeController.state.bridgeState.quotesLastFetched, @@ -469,7 +469,7 @@ describe('BridgeController', function () { insufficientBal: true, }, expect.any(AbortSignal), - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId.EXTENSION, ); expect( bridgeController.state.bridgeState.quotesLastFetched, @@ -666,7 +666,7 @@ describe('BridgeController', function () { insufficientBal: true, }, expect.any(AbortSignal), - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId.EXTENSION, ); expect( bridgeController.state.bridgeState.quotesLastFetched, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index bf3c3f5870c..9f21834fc68 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -8,6 +8,7 @@ import { numberToHex } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { BrowserProvider, Contract } from 'ethers'; +import type { BridgeClientId } from './constants/bridge'; import { REFRESH_INTERVAL_MS } from './constants/bridge'; import { BRIDGE_CONTROLLER_NAME, @@ -69,7 +70,7 @@ export class BridgeController extends StaticIntervalPollingController; - clientId: string; + clientId: BridgeClientId; getLayer1GasFee: (params: { transactionParams: TransactionParams; chainId: ChainId; diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index 20c64e972a6..2fc2500b19c 100644 --- a/packages/bridge-controller/src/constants/bridge.ts +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -22,8 +22,11 @@ export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; -export const BRIDGE_CLIENT_ID_EXTENSION = 'extension'; -export const BRIDGE_CLIENT_ID_MOBILE = 'mobile'; + +export enum BridgeClientId { + EXTENSION = 'extension', + MOBILE = 'mobile', +} export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; export const METABRIDGE_ETHEREUM_ADDRESS = diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 7a6e01c2525..c8133526de6 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -8,7 +8,7 @@ import { } from './fetch'; import mockBridgeQuotesErc20Erc20 from '../../../../tests/bridge-controller/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../../../tests/bridge-controller/mock-quotes-native-erc20.json'; -import { BRIDGE_CLIENT_ID_EXTENSION } from '../constants/bridge'; +import { BridgeClientId } from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains'; jest.mock('@metamask/controller-utils', () => ({ @@ -55,7 +55,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockResolvedValue(mockResponse); - const result = await fetchBridgeFeatureFlags(BRIDGE_CLIENT_ID_EXTENSION); + const result = await fetchBridgeFeatureFlags(BridgeClientId.EXTENSION); expect(handleFetch).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', @@ -120,7 +120,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockResolvedValue(mockResponse); - const result = await fetchBridgeFeatureFlags(BRIDGE_CLIENT_ID_EXTENSION); + const result = await fetchBridgeFeatureFlags(BridgeClientId.EXTENSION); expect(handleFetch).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', @@ -145,7 +145,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockRejectedValue(mockError); await expect( - fetchBridgeFeatureFlags(BRIDGE_CLIENT_ID_EXTENSION), + fetchBridgeFeatureFlags(BridgeClientId.EXTENSION), ).rejects.toThrow(mockError); }); }); @@ -181,7 +181,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockResolvedValue(mockResponse); - const result = await fetchBridgeTokens('0xa', BRIDGE_CLIENT_ID_EXTENSION); + const result = await fetchBridgeTokens('0xa', BridgeClientId.EXTENSION); expect(handleFetch).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', @@ -218,7 +218,7 @@ describe('Bridge utils', () => { (handleFetch as jest.Mock).mockRejectedValue(mockError); await expect( - fetchBridgeTokens('0xa', BRIDGE_CLIENT_ID_EXTENSION), + fetchBridgeTokens('0xa', BridgeClientId.EXTENSION), ).rejects.toThrow(mockError); }); }); @@ -239,7 +239,7 @@ describe('Bridge utils', () => { slippage: 0.5, }, signal, - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId.EXTENSION, ); expect(handleFetch).toHaveBeenCalledWith( @@ -272,7 +272,7 @@ describe('Bridge utils', () => { slippage: 0.5, }, signal, - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId.EXTENSION, ); expect(handleFetch).toHaveBeenCalledWith( @@ -324,7 +324,7 @@ describe('Bridge utils', () => { slippage: 0.5, }, signal, - BRIDGE_CLIENT_ID_EXTENSION, + BridgeClientId.EXTENSION, ); expect(handleFetch).toHaveBeenCalledWith( From bec4cda844429dc0759a3edcd92f5d18b88d1e4f Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:28:54 -0500 Subject: [PATCH 80/94] fix: incorrect export --- packages/bridge-controller/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 2359187c7b0..415682821fe 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -36,8 +36,7 @@ export type { export { ALLOWED_BRIDGE_CHAIN_IDS, - BRIDGE_CLIENT_ID_EXTENSION, - BRIDGE_CLIENT_ID_MOBILE, + BridgeClientId, BRIDGE_QUOTE_MAX_ETA_SECONDS, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, BRIDGE_PREFERRED_GAS_ESTIMATE, From c95203279ef15948ba38cc25402e845a0f60874d Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:35:43 -0500 Subject: [PATCH 81/94] chore: add more tests for should not fetch quotes if source and destination chains are the same --- .../src/bridge-controller.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index f688d5b0735..45401e908d2 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -706,4 +706,37 @@ describe('BridgeController', function () { ); }, ); + + it('should not fetch quotes if source and destination chains are the same', async () => { + jest.useFakeTimers(); + const fetchBridgeQuotesSpy = jest.spyOn(fetchUtils, 'fetchBridgeQuotes'); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(true); + + const quoteParams = { + srcChainId: 1, + destChainId: 1, // Same chain ID + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + // Advance timers to trigger fetch + jest.advanceTimersByTime(1000); + await flushPromises(); + + expect(fetchBridgeQuotesSpy).not.toHaveBeenCalled(); + expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); + expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe( + DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, + ); + }); }); From c070e8ceadb7064655fdabd812ec428f0a69e7ba Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:44:06 -0500 Subject: [PATCH 82/94] chore: add test for should throw an error when no provider is found --- .../src/bridge-controller.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 45401e908d2..8dc295786ca 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -570,6 +570,35 @@ describe('BridgeController', function () { ); expect(allowance).toBe('100000000000000000000'); }); + + it('should throw an error when no provider is found', async () => { + // Setup + const mockMessenger = { + call: jest.fn().mockImplementation((methodName) => { + if (methodName === 'NetworkController:getNetworkClientById') { + return { provider: null }; + } + if (methodName === 'NetworkController:getState') { + return { selectedNetworkClientId: 'testNetworkClientId' }; + } + return undefined; + }), + registerActionHandler: jest.fn(), + publish: jest.fn(), + registerInitialEventPayload: jest.fn(), + } as unknown as jest.Mocked; + + const controller = new BridgeController({ + messenger: mockMessenger, + clientId: BridgeClientId.EXTENSION, + getLayer1GasFee: jest.fn(), + }); + + // Test + await expect( + controller.getBridgeERC20Allowance('0xContractAddress', '0x1'), + ).rejects.toThrow('No provider found'); + }); }); it.each([ From 68011590d3f9c031d4e703f51bc3561e2c3b23f7 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:48:21 -0500 Subject: [PATCH 83/94] chore: add tests for should handle abort signals in fetchBridgeQuotes --- .../src/bridge-controller.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 8dc295786ca..5d01752dfbd 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -768,4 +768,54 @@ describe('BridgeController', function () { DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, ); }); + + it('should handle abort signals in fetchBridgeQuotes', async () => { + jest.useFakeTimers(); + const fetchBridgeQuotesSpy = jest.spyOn(fetchUtils, 'fetchBridgeQuotes'); + messengerMock.call.mockReturnValue({ + address: '0x123', + provider: jest.fn(), + } as never); + + jest.spyOn(balanceUtils, 'hasSufficientBalance').mockResolvedValue(true); + + // Mock fetchBridgeQuotes to throw AbortError + fetchBridgeQuotesSpy.mockImplementation(async () => { + const error = new Error('Aborted'); + error.name = 'AbortError'; + throw error; + }); + + const quoteParams = { + srcChainId: 1, + destChainId: 10, + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x123', + srcTokenAmount: '1000000000000000000', + }; + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + // Advance timers to trigger fetch + jest.advanceTimersByTime(1000); + await flushPromises(); + + // Verify state wasn't updated due to abort + expect(bridgeController.state.bridgeState.quoteFetchError).toBeUndefined(); + expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe(0); + expect(bridgeController.state.bridgeState.quotes).toEqual([]); + + // Test reset abort + fetchBridgeQuotesSpy.mockRejectedValueOnce('Reset controller state'); + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + jest.advanceTimersByTime(1000); + await flushPromises(); + + // Verify state wasn't updated due to reset + expect(bridgeController.state.bridgeState.quoteFetchError).toBeUndefined(); + expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe(0); + expect(bridgeController.state.bridgeState.quotes).toEqual([]); + }); }); From 7af59796c84c80a0c0c179dc24154a67e8af6aa1 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:52:17 -0500 Subject: [PATCH 84/94] feat: add tests for fetchTokenBalance utility function --- .../src/utils/balance.test.ts | 63 +++++++++++++++++++ .../bridge-controller/src/utils/balance.ts | 9 +-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/bridge-controller/src/utils/balance.test.ts b/packages/bridge-controller/src/utils/balance.test.ts index b7d6aebc93b..a8f4d6569f9 100644 --- a/packages/bridge-controller/src/utils/balance.test.ts +++ b/packages/bridge-controller/src/utils/balance.test.ts @@ -1,8 +1,10 @@ import type { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; +import { abiERC20 } from '@metamask/metamask-eth-abis'; import { ZeroAddress } from 'ethers'; import { BrowserProvider, Contract } from 'ethers'; import * as balanceUtils from './balance'; +import { fetchTokenBalance } from './balance'; import { FakeProvider } from '../../../../tests/fake-provider'; declare global { @@ -184,3 +186,64 @@ describe('balance', () => { }); }); }); + +describe('fetchTokenBalance', () => { + let mockProvider: SafeEventEmitterProvider; + const mockAddress = '0x1234567890123456789012345678901234567890'; + const mockUserAddress = '0x9876543210987654321098765432109876543210'; + const mockBalance = BigInt(1000); + + beforeEach(() => { + jest.clearAllMocks(); + mockProvider = new FakeProvider(); + + // Mock BrowserProvider + (BrowserProvider as jest.Mock).mockImplementation(() => ({ + // Add any provider methods needed + })); + }); + + it('should fetch token balance when contract is valid', async () => { + // Mock Contract + const mockBalanceOf = jest.fn().mockResolvedValue(mockBalance); + (Contract as jest.Mock).mockImplementation(() => ({ + balanceOf: mockBalanceOf, + })); + + const result = await fetchTokenBalance( + mockAddress, + mockUserAddress, + mockProvider, + ); + + expect(BrowserProvider).toHaveBeenCalledWith(mockProvider); + expect(Contract).toHaveBeenCalledWith( + mockAddress, + abiERC20, + expect.anything(), + ); + expect(mockBalanceOf).toHaveBeenCalledWith(mockUserAddress); + expect(result).toBe(mockBalance); + }); + + it('should return undefined when contract is invalid', async () => { + // Mock Contract to return an object without balanceOf method + (Contract as jest.Mock).mockImplementation(() => ({ + // Empty object without balanceOf method + })); + + const result = await fetchTokenBalance( + mockAddress, + mockUserAddress, + mockProvider, + ); + + expect(BrowserProvider).toHaveBeenCalledWith(mockProvider); + expect(Contract).toHaveBeenCalledWith( + mockAddress, + abiERC20, + expect.anything(), + ); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts index 34a30369d83..2788423f2df 100644 --- a/packages/bridge-controller/src/utils/balance.ts +++ b/packages/bridge-controller/src/utils/balance.ts @@ -7,12 +7,13 @@ export const fetchTokenBalance = async ( address: string, userAddress: string, provider: Provider, -): Promise => { +): Promise => { const ethersProvider = new BrowserProvider(provider); const tokenContract = new Contract(address, abiERC20, ethersProvider); - const tokenBalancePromise = tokenContract - ? tokenContract.balanceOf(userAddress) - : Promise.resolve(); + const tokenBalancePromise = + typeof tokenContract?.balanceOf === 'function' + ? tokenContract.balanceOf(userAddress) + : Promise.resolve(undefined); return await tokenBalancePromise; }; From 1f5ea8fe54837db999f6116ea555f326f93ea9a5 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:14:43 -0500 Subject: [PATCH 85/94] refactor: simplify BridgeControllerMessenger type definition --- packages/bridge-controller/src/types.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index f47bc3c651f..192bb8f98d2 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -250,10 +250,10 @@ export type BridgeControllerEvents = ControllerStateChangeEvent< >; export type AllowedActions = - | AccountsControllerGetSelectedAccountAction['type'] - | NetworkControllerFindNetworkClientIdByChainIdAction['type'] - | NetworkControllerGetStateAction['type'] - | NetworkControllerGetNetworkClientByIdAction['type']; + | AccountsControllerGetSelectedAccountAction + | NetworkControllerFindNetworkClientIdByChainIdAction + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction; export type AllowedEvents = never; /** @@ -261,12 +261,8 @@ export type AllowedEvents = never; */ export type BridgeControllerMessenger = RestrictedMessenger< typeof BRIDGE_CONTROLLER_NAME, - | BridgeControllerActions - | AccountsControllerGetSelectedAccountAction - | NetworkControllerFindNetworkClientIdByChainIdAction - | NetworkControllerGetStateAction - | NetworkControllerGetNetworkClientByIdAction, - BridgeControllerEvents, - AllowedActions, - AllowedEvents + BridgeControllerActions | AllowedActions, + BridgeControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] >; From 161f66939b2c950ee49a83b64e9f0a9152842663 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:22:39 -0500 Subject: [PATCH 86/94] chore: increase coverage thresholds for bridge controller tests --- packages/bridge-controller/jest.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bridge-controller/jest.config.js b/packages/bridge-controller/jest.config.js index c8fc07a0653..d67e30322b8 100644 --- a/packages/bridge-controller/jest.config.js +++ b/packages/bridge-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 89, + branches: 93, functions: 98, - lines: 98, - statements: 98, + lines: 99, + statements: 99, }, }, }); From e78dab77f8548302c9ab3c41e68fee3269fbbbec Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:17:56 -0500 Subject: [PATCH 87/94] chore: update yarn lock --- yarn.lock | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 820eed9fffc..2988600518e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2580,6 +2580,38 @@ __metadata: languageName: unknown linkType: soft +"@metamask/bridge-controller@workspace:packages/bridge-controller": + version: 0.0.0-use.local + resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" + dependencies: + "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.5.0" + "@metamask/eth-json-rpc-provider": "npm:^4.1.8" + "@metamask/json-rpc-engine": "npm:^10.0.3" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/network-controller": "npm:^22.2.1" + "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/transaction-controller": "npm:^45.1.0" + "@metamask/utils": "npm:^11.1.0" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + ethers: "npm:^6.12.0" + jest: "npm:^27.5.1" + jest-environment-jsdom: "npm:^27.5.1" + lodash: "npm:^4.17.21" + nock: "npm:^13.3.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/accounts-controller": ^23.0.0 + "@metamask/network-controller": ^22.0.0 + languageName: unknown + linkType: soft + "@metamask/browser-passworder@npm:^4.3.0": version: 4.3.0 resolution: "@metamask/browser-passworder@npm:4.3.0" @@ -13826,4 +13858,4 @@ __metadata: "@types/yoga-layout": "npm:1.9.2" checksum: 10/fe36fadae9b30710083f76c73e87479c2eb291ff7c560c35a9e2b8eb78f43882ace63cc80cdaecae98ee2e4168e1bf84dc65b2f5ae1bfa31df37603c46683bd6 languageName: node - linkType: hard \ No newline at end of file + linkType: hard From 8cdc9eb640e141a8b2f5df356129de81e7c1b3a0 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:24:50 -0500 Subject: [PATCH 88/94] fix: lint errors --- packages/bridge-controller/src/bridge-controller.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 5d01752dfbd..9ea314667e0 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -575,9 +575,11 @@ describe('BridgeController', function () { // Setup const mockMessenger = { call: jest.fn().mockImplementation((methodName) => { + // eslint-disable-next-line jest/no-conditional-in-test if (methodName === 'NetworkController:getNetworkClientById') { return { provider: null }; } + // eslint-disable-next-line jest/no-conditional-in-test if (methodName === 'NetworkController:getState') { return { selectedNetworkClientId: 'testNetworkClientId' }; } @@ -803,7 +805,7 @@ describe('BridgeController', function () { // Verify state wasn't updated due to abort expect(bridgeController.state.bridgeState.quoteFetchError).toBeUndefined(); expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe(0); - expect(bridgeController.state.bridgeState.quotes).toEqual([]); + expect(bridgeController.state.bridgeState.quotes).toStrictEqual([]); // Test reset abort fetchBridgeQuotesSpy.mockRejectedValueOnce('Reset controller state'); @@ -816,6 +818,6 @@ describe('BridgeController', function () { // Verify state wasn't updated due to reset expect(bridgeController.state.bridgeState.quoteFetchError).toBeUndefined(); expect(bridgeController.state.bridgeState.quotesLoadingStatus).toBe(0); - expect(bridgeController.state.bridgeState.quotes).toEqual([]); + expect(bridgeController.state.bridgeState.quotes).toStrictEqual([]); }); }); From 64ba6ea7fe0d59c4d216662535dcd644be121f4c Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:31:40 -0500 Subject: [PATCH 89/94] chore: update yarn lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 2988600518e..3c0c631f9ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2340,7 +2340,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^23.1.0, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^23.0.1, @metamask/accounts-controller@npm:^23.1.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: From 741b2d69a9711f139d20eeece07b36678c9ecaf7 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:39:57 -0500 Subject: [PATCH 90/94] chore: bump versions --- packages/bridge-controller/package.json | 4 ++-- yarn.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 26876c74eef..81b27e23d53 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -47,7 +47,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^7.1.1", + "@metamask/base-controller": "^8.0.0", "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.3", @@ -56,7 +56,7 @@ "ethers": "^6.12.0" }, "devDependencies": { - "@metamask/accounts-controller": "^23.0.1", + "@metamask/accounts-controller": "^23.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/json-rpc-engine": "^10.0.3", diff --git a/yarn.lock b/yarn.lock index 3c0c631f9ba..53cf9d1554f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2340,7 +2340,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^23.0.1, @metamask/accounts-controller@npm:^23.1.0, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^23.1.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -2584,9 +2584,9 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" dependencies: - "@metamask/accounts-controller": "npm:^23.0.1" + "@metamask/accounts-controller": "npm:^23.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.1" + "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.5.0" "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/json-rpc-engine": "npm:^10.0.3" From 83f6059ffcfe5900aacb4996979602e3e64f8527 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:45:38 -0500 Subject: [PATCH 91/94] fix: package deps --- packages/bridge-controller/package.json | 5 +++-- yarn.lock | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 81b27e23d53..7b4a0b7571a 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -51,7 +51,6 @@ "@metamask/controller-utils": "^11.5.0", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/polling-controller": "^12.0.3", - "@metamask/transaction-controller": "^45.1.0", "@metamask/utils": "^11.1.0", "ethers": "^6.12.0" }, @@ -61,6 +60,7 @@ "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/json-rpc-engine": "^10.0.3", "@metamask/network-controller": "^22.2.1", + "@metamask/transaction-controller": "^45.1.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -74,7 +74,8 @@ }, "peerDependencies": { "@metamask/accounts-controller": "^23.0.0", - "@metamask/network-controller": "^22.0.0" + "@metamask/network-controller": "^22.0.0", + "@metamask/transaction-controller": "^45.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index 53cf9d1554f..d4a3d7152ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2609,6 +2609,7 @@ __metadata: peerDependencies: "@metamask/accounts-controller": ^23.0.0 "@metamask/network-controller": ^22.0.0 + "@metamask/transaction-controller": ^45.0.0 languageName: unknown linkType: soft From 6d213108c269a0d0641e90517dad20558e6f821d Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:19:14 -0500 Subject: [PATCH 92/94] feat: allow consumers to pass in their own fetch function to bridgeController constructor --- .../src/bridge-controller.test.ts | 7 +++ .../src/bridge-controller.ts | 13 ++++- packages/bridge-controller/src/types.ts | 6 ++ .../bridge-controller/src/utils/fetch.test.ts | 57 +++++++++++-------- packages/bridge-controller/src/utils/fetch.ts | 14 +++-- 5 files changed, 67 insertions(+), 30 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 9ea314667e0..051fb041b56 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -18,6 +18,7 @@ import mockBridgeQuotesErc20Native from '../../../tests/bridge-controller/mock-q import mockBridgeQuotesNativeErc20Eth from '../../../tests/bridge-controller/mock-quotes-native-erc20-eth.json'; import mockBridgeQuotesNativeErc20 from '../../../tests/bridge-controller/mock-quotes-native-erc20.json'; import { flushPromises } from '../../../tests/helpers'; +import { handleFetch } from '../../controller-utils/src'; const EMPTY_INIT_STATE = { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -38,6 +39,7 @@ jest.mock('ethers', () => { }; }); const getLayer1GasFeeMock = jest.fn(); +const mockFetchFn = handleFetch; describe('BridgeController', function () { let bridgeController: BridgeController; @@ -47,6 +49,7 @@ describe('BridgeController', function () { messenger: messengerMock, getLayer1GasFee: getLayer1GasFeeMock, clientId: BridgeClientId.EXTENSION, + fetchFn: mockFetchFn, }); }); @@ -318,6 +321,7 @@ describe('BridgeController', function () { }, expect.any(AbortSignal), BridgeClientId.EXTENSION, + mockFetchFn, ); expect( bridgeController.state.bridgeState.quotesLastFetched, @@ -470,6 +474,7 @@ describe('BridgeController', function () { }, expect.any(AbortSignal), BridgeClientId.EXTENSION, + mockFetchFn, ); expect( bridgeController.state.bridgeState.quotesLastFetched, @@ -594,6 +599,7 @@ describe('BridgeController', function () { messenger: mockMessenger, clientId: BridgeClientId.EXTENSION, getLayer1GasFee: jest.fn(), + fetchFn: mockFetchFn, }); // Test @@ -698,6 +704,7 @@ describe('BridgeController', function () { }, expect.any(AbortSignal), BridgeClientId.EXTENSION, + mockFetchFn, ); expect( bridgeController.state.bridgeState.quotesLastFetched, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 9f21834fc68..7788d2b7561 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -25,7 +25,7 @@ import { BridgeFeatureFlagsKey, RequestStatus, } from './types'; -import type { BridgeControllerMessenger } from './types'; +import type { BridgeControllerMessenger, FetchFunction } from './types'; import { hasSufficientBalance } from './utils/balance'; import { getDefaultBridgeControllerState, sumHexes } from './utils/bridge'; import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; @@ -62,11 +62,14 @@ export class BridgeController extends StaticIntervalPollingController Promise; + readonly #fetchFn: FetchFunction; + constructor({ messenger, state, clientId, getLayer1GasFee, + fetchFn, }: { messenger: BridgeControllerMessenger; state?: Partial; @@ -75,6 +78,7 @@ export class BridgeController extends StaticIntervalPollingController Promise; + fetchFn: FetchFunction; }) { super({ name: BRIDGE_CONTROLLER_NAME, @@ -93,6 +97,7 @@ export class BridgeController extends StaticIntervalPollingController { - const bridgeFeatureFlags = await fetchBridgeFeatureFlags(this.#clientId); + const bridgeFeatureFlags = await fetchBridgeFeatureFlags( + this.#clientId, + this.#fetchFn, + ); this.update((state) => { state.bridgeState.bridgeFeatureFlags = bridgeFeatureFlags; }); @@ -230,6 +238,7 @@ export class BridgeController extends StaticIntervalPollingController Promise; + /** * The types of assets that a user can send * diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index c8133526de6..8cd3501b819 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -1,4 +1,3 @@ -import { handleFetch } from '@metamask/controller-utils'; import { ZeroAddress } from 'ethers'; import { @@ -11,10 +10,7 @@ import mockBridgeQuotesNativeErc20 from '../../../../tests/bridge-controller/moc import { BridgeClientId } from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains'; -jest.mock('@metamask/controller-utils', () => ({ - ...jest.requireActual('@metamask/controller-utils'), - handleFetch: jest.fn(), -})); +const mockFetchFn = jest.fn(); describe('Bridge utils', () => { describe('fetchBridgeFeatureFlags', () => { @@ -53,11 +49,14 @@ describe('Bridge utils', () => { }, }; - (handleFetch as jest.Mock).mockResolvedValue(mockResponse); + mockFetchFn.mockResolvedValue(mockResponse); - const result = await fetchBridgeFeatureFlags(BridgeClientId.EXTENSION); + const result = await fetchBridgeFeatureFlags( + BridgeClientId.EXTENSION, + mockFetchFn, + ); - expect(handleFetch).toHaveBeenCalledWith( + expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', { headers: { 'X-Client-Id': 'extension' }, @@ -118,11 +117,14 @@ describe('Bridge utils', () => { }, }; - (handleFetch as jest.Mock).mockResolvedValue(mockResponse); + mockFetchFn.mockResolvedValue(mockResponse); - const result = await fetchBridgeFeatureFlags(BridgeClientId.EXTENSION); + const result = await fetchBridgeFeatureFlags( + BridgeClientId.EXTENSION, + mockFetchFn, + ); - expect(handleFetch).toHaveBeenCalledWith( + expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', { headers: { 'X-Client-Id': 'extension' }, @@ -142,10 +144,10 @@ describe('Bridge utils', () => { it('should handle fetch error', async () => { const mockError = new Error('Failed to fetch'); - (handleFetch as jest.Mock).mockRejectedValue(mockError); + mockFetchFn.mockRejectedValue(mockError); await expect( - fetchBridgeFeatureFlags(BridgeClientId.EXTENSION), + fetchBridgeFeatureFlags(BridgeClientId.EXTENSION, mockFetchFn), ).rejects.toThrow(mockError); }); }); @@ -179,11 +181,15 @@ describe('Bridge utils', () => { }, ]; - (handleFetch as jest.Mock).mockResolvedValue(mockResponse); + mockFetchFn.mockResolvedValue(mockResponse); - const result = await fetchBridgeTokens('0xa', BridgeClientId.EXTENSION); + const result = await fetchBridgeTokens( + '0xa', + BridgeClientId.EXTENSION, + mockFetchFn, + ); - expect(handleFetch).toHaveBeenCalledWith( + expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', { headers: { 'X-Client-Id': 'extension' }, @@ -215,17 +221,17 @@ describe('Bridge utils', () => { it('should handle fetch error', async () => { const mockError = new Error('Failed to fetch'); - (handleFetch as jest.Mock).mockRejectedValue(mockError); + mockFetchFn.mockRejectedValue(mockError); await expect( - fetchBridgeTokens('0xa', BridgeClientId.EXTENSION), + fetchBridgeTokens('0xa', BridgeClientId.EXTENSION, mockFetchFn), ).rejects.toThrow(mockError); }); }); describe('fetchBridgeQuotes', () => { it('should fetch bridge quotes successfully, no approvals', async () => { - (handleFetch as jest.Mock).mockResolvedValue(mockBridgeQuotesNativeErc20); + mockFetchFn.mockResolvedValue(mockBridgeQuotesNativeErc20); const { signal } = new AbortController(); const result = await fetchBridgeQuotes( @@ -240,9 +246,10 @@ describe('Bridge utils', () => { }, signal, BridgeClientId.EXTENSION, + mockFetchFn, ); - expect(handleFetch).toHaveBeenCalledWith( + expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { headers: { 'X-Client-Id': 'extension' }, @@ -254,7 +261,7 @@ describe('Bridge utils', () => { }); it('should fetch bridge quotes successfully, with approvals', async () => { - (handleFetch as jest.Mock).mockResolvedValue([ + mockFetchFn.mockResolvedValue([ ...mockBridgeQuotesErc20Erc20, { ...mockBridgeQuotesErc20Erc20[0], approval: null }, { ...mockBridgeQuotesErc20Erc20[0], trade: null }, @@ -273,9 +280,10 @@ describe('Bridge utils', () => { }, signal, BridgeClientId.EXTENSION, + mockFetchFn, ); - expect(handleFetch).toHaveBeenCalledWith( + expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { headers: { 'X-Client-Id': 'extension' }, @@ -287,7 +295,7 @@ describe('Bridge utils', () => { }); it('should filter out malformed bridge quotes', async () => { - (handleFetch as jest.Mock).mockResolvedValue([ + mockFetchFn.mockResolvedValue([ ...mockBridgeQuotesErc20Erc20, ...mockBridgeQuotesErc20Erc20.map( ({ quote, ...restOfQuote }) => restOfQuote, @@ -325,9 +333,10 @@ describe('Bridge utils', () => { }, signal, BridgeClientId.EXTENSION, + mockFetchFn, ); - expect(handleFetch).toHaveBeenCalledWith( + expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', { headers: { 'X-Client-Id': 'extension' }, diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index f9a66916fbc..674b7bcf452 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -1,4 +1,3 @@ -import { handleFetch } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; import { hexToNumber, numberToHex } from '@metamask/utils'; @@ -27,6 +26,7 @@ import type { QuoteResponse, TxData, BridgeFeatureFlags, + FetchFunction, } from '../types'; import { BridgeFlag, FeeType, BridgeFeatureFlagsKey } from '../types'; @@ -41,13 +41,15 @@ export const getClientIdHeader = (clientId: string) => ({ * Fetches the bridge feature flags * * @param clientId - The client ID for metrics + * @param fetchFn - The fetch function to use * @returns The bridge feature flags */ export async function fetchBridgeFeatureFlags( clientId: string, + fetchFn: FetchFunction, ): Promise { const url = `${getBridgeApiBaseUrl()}/getAllFeatureFlags`; - const rawFeatureFlags = await handleFetch(url, { + const rawFeatureFlags = await fetchFn(url, { headers: getClientIdHeader(clientId), }); @@ -89,11 +91,13 @@ export async function fetchBridgeFeatureFlags( * * @param chainId - The chain ID to fetch tokens for * @param clientId - The client ID for metrics + * @param fetchFn - The fetch function to use * @returns A list of enabled (unblocked) tokens */ export async function fetchBridgeTokens( chainId: Hex, clientId: string, + fetchFn: FetchFunction, ): Promise> { // TODO make token api v2 call const url = `${getBridgeApiBaseUrl()}/getTokens?chainId=${hexToNumber( @@ -103,7 +107,7 @@ export async function fetchBridgeTokens( // TODO we will need to cache these. In Extension fetchWithCache is used. This is due to the following: // If we allow selecting dest networks which the user has not imported, // note that the Assets controller won't be able to provide tokens. In extension we fetch+cache the token list from bridge-api to handle this - const tokens = await handleFetch(url, { + const tokens = await fetchFn(url, { headers: getClientIdHeader(clientId), }); @@ -137,12 +141,14 @@ export async function fetchBridgeTokens( * @param request - The quote request * @param signal - The abort signal * @param clientId - The client ID for metrics + * @param fetchFn - The fetch function to use * @returns A list of bridge tx quotes */ export async function fetchBridgeQuotes( request: QuoteRequest, signal: AbortSignal, clientId: string, + fetchFn: FetchFunction, ): Promise { const queryParams = new URLSearchParams({ walletAddress: request.walletAddress, @@ -156,7 +162,7 @@ export async function fetchBridgeQuotes( resetApproval: request.resetApproval ? 'true' : 'false', }); const url = `${getBridgeApiBaseUrl()}/getQuote?${queryParams}`; - const quotes = await handleFetch(url, { + const quotes = await fetchFn(url, { headers: getClientIdHeader(clientId), signal, }); From 411424ee759d11c76798e73507b1cd0e6ec90fbe Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:18:22 -0500 Subject: [PATCH 93/94] chore: move mock jsons inside bridge controller package --- packages/bridge-controller/src/bridge-controller.test.ts | 6 +++--- .../bridge-controller/tests}/mock-quotes-erc20-erc20.json | 0 .../bridge-controller/tests}/mock-quotes-erc20-native.json | 0 .../tests}/mock-quotes-native-erc20-eth.json | 0 .../bridge-controller/tests}/mock-quotes-native-erc20.json | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {tests/bridge-controller => packages/bridge-controller/tests}/mock-quotes-erc20-erc20.json (100%) rename {tests/bridge-controller => packages/bridge-controller/tests}/mock-quotes-erc20-native.json (100%) rename {tests/bridge-controller => packages/bridge-controller/tests}/mock-quotes-native-erc20-eth.json (100%) rename {tests/bridge-controller => packages/bridge-controller/tests}/mock-quotes-native-erc20.json (100%) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 051fb041b56..3bc04fcc973 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -14,11 +14,11 @@ import type { BridgeControllerMessenger, QuoteResponse } from './types'; import * as balanceUtils from './utils/balance'; import { getBridgeApiBaseUrl } from './utils/bridge'; import * as fetchUtils from './utils/fetch'; -import mockBridgeQuotesErc20Native from '../../../tests/bridge-controller/mock-quotes-erc20-native.json'; -import mockBridgeQuotesNativeErc20Eth from '../../../tests/bridge-controller/mock-quotes-native-erc20-eth.json'; -import mockBridgeQuotesNativeErc20 from '../../../tests/bridge-controller/mock-quotes-native-erc20.json'; import { flushPromises } from '../../../tests/helpers'; import { handleFetch } from '../../controller-utils/src'; +import mockBridgeQuotesErc20Native from '../tests/mock-quotes-erc20-native.json'; +import mockBridgeQuotesNativeErc20Eth from '../tests/mock-quotes-native-erc20-eth.json'; +import mockBridgeQuotesNativeErc20 from '../tests/mock-quotes-native-erc20.json'; const EMPTY_INIT_STATE = { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, diff --git a/tests/bridge-controller/mock-quotes-erc20-erc20.json b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json similarity index 100% rename from tests/bridge-controller/mock-quotes-erc20-erc20.json rename to packages/bridge-controller/tests/mock-quotes-erc20-erc20.json diff --git a/tests/bridge-controller/mock-quotes-erc20-native.json b/packages/bridge-controller/tests/mock-quotes-erc20-native.json similarity index 100% rename from tests/bridge-controller/mock-quotes-erc20-native.json rename to packages/bridge-controller/tests/mock-quotes-erc20-native.json diff --git a/tests/bridge-controller/mock-quotes-native-erc20-eth.json b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json similarity index 100% rename from tests/bridge-controller/mock-quotes-native-erc20-eth.json rename to packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json diff --git a/tests/bridge-controller/mock-quotes-native-erc20.json b/packages/bridge-controller/tests/mock-quotes-native-erc20.json similarity index 100% rename from tests/bridge-controller/mock-quotes-native-erc20.json rename to packages/bridge-controller/tests/mock-quotes-native-erc20.json From 76c617dbed29f5277f0ec7ea7a2b0765015e3263 Mon Sep 17 00:00:00 2001 From: IF <139582705+infiniteflower@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:37:34 -0500 Subject: [PATCH 94/94] fix: broken import --- packages/bridge-controller/src/utils/fetch.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 8cd3501b819..a287e15af4b 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -5,8 +5,8 @@ import { fetchBridgeQuotes, fetchBridgeTokens, } from './fetch'; -import mockBridgeQuotesErc20Erc20 from '../../../../tests/bridge-controller/mock-quotes-erc20-erc20.json'; -import mockBridgeQuotesNativeErc20 from '../../../../tests/bridge-controller/mock-quotes-native-erc20.json'; +import mockBridgeQuotesErc20Erc20 from '../../tests/mock-quotes-erc20-erc20.json'; +import mockBridgeQuotesNativeErc20 from '../../tests/mock-quotes-native-erc20.json'; import { BridgeClientId } from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains';