Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/bridge bridge status controller env config #5465

Merged
merged 5 commits into from
Mar 13, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/bridge-controller/src/bridge-controller.test.ts
Original file line number Diff line number Diff line change
@@ -6,13 +6,13 @@ import nock from 'nock';
import { BridgeController } from './bridge-controller';
import {
BridgeClientId,
BRIDGE_PROD_API_BASE_URL,
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 * as balanceUtils from './utils/balance';
import { getBridgeApiBaseUrl } from './utils/bridge';
import * as fetchUtils from './utils/fetch';
import { flushPromises } from '../../../tests/helpers';
import { handleFetch } from '../../controller-utils/src';
@@ -55,7 +55,7 @@ describe('BridgeController', function () {
jest.clearAllMocks();
jest.clearAllTimers();

nock(getBridgeApiBaseUrl())
nock(BRIDGE_PROD_API_BASE_URL)
.get('/getAllFeatureFlags')
.reply(200, {
'extension-config': {
@@ -117,7 +117,7 @@ describe('BridgeController', function () {
'534352': 2.4,
},
});
nock(getBridgeApiBaseUrl())
nock(BRIDGE_PROD_API_BASE_URL)
.get('/getTokens?chainId=10')
.reply(200, [
{
@@ -338,6 +338,7 @@ describe('BridgeController', function () {
expect.any(AbortSignal),
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);
expect(bridgeController.state.quotesLastFetched).toBeNull();

@@ -488,6 +489,7 @@ describe('BridgeController', function () {
expect.any(AbortSignal),
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);
expect(bridgeController.state.quotesLastFetched).toBeNull();

@@ -715,6 +717,7 @@ describe('BridgeController', function () {
expect.any(AbortSignal),
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);
expect(bridgeController.state.quotesLastFetched).toBeNull();

12 changes: 12 additions & 0 deletions packages/bridge-controller/src/bridge-controller.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import type { Hex } from '@metamask/utils';
import type { BridgeClientId } from './constants/bridge';
import {
BRIDGE_CONTROLLER_NAME,
BRIDGE_PROD_API_BASE_URL,
DEFAULT_BRIDGE_CONTROLLER_STATE,
METABRIDGE_CHAIN_TO_ADDRESS_MAP,
REFRESH_INTERVAL_MS,
@@ -95,12 +96,17 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll

readonly #fetchFn: FetchFunction;

readonly #config: {
customBridgeApiBaseUrl?: string;
};

constructor({
messenger,
state,
clientId,
getLayer1GasFee,
fetchFn,
config,
}: {
messenger: BridgeControllerMessenger;
state?: Partial<BridgeControllerState>;
@@ -110,6 +116,9 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
chainId: ChainId;
}) => Promise<string>;
fetchFn: FetchFunction;
config?: {
customBridgeApiBaseUrl?: string;
};
}) {
super({
name: BRIDGE_CONTROLLER_NAME,
@@ -127,6 +136,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
this.#getLayer1GasFee = getLayer1GasFee;
this.#clientId = clientId;
this.#fetchFn = fetchFn;
this.#config = config ?? {};

// Register action handlers
this.messagingSystem.registerActionHandler(
@@ -242,6 +252,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
const bridgeFeatureFlags = await fetchBridgeFeatureFlags(
this.#clientId,
this.#fetchFn,
this.#config.customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL,
);
this.update((state) => {
state.bridgeFeatureFlags = bridgeFeatureFlags;
@@ -278,6 +289,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
this.#abortController!.signal as AbortSignal,
this.#clientId,
this.#fetchFn,
this.#config.customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL,
);

const quotesWithL1GasFees = await this.#appendL1GasFees(quotes);
8 changes: 3 additions & 5 deletions packages/bridge-controller/src/index.ts
Original file line number Diff line number Diff line change
@@ -47,6 +47,8 @@ export {
DEFAULT_MAX_REFRESH_COUNT,
DEFAULT_BRIDGE_CONTROLLER_STATE,
METABRIDGE_CHAIN_TO_ADDRESS_MAP,
BRIDGE_DEV_API_BASE_URL,
BRIDGE_PROD_API_BASE_URL,
} from './constants/bridge';

export type { AllowedBridgeChainIds } from './constants/bridge';
@@ -55,8 +57,4 @@ export type { SwapsTokenObject } from './constants/tokens';

export { SWAPS_API_V2_BASE_URL } from './constants/swaps';

export {
getEthUsdtResetData,
isEthUsdt,
getBridgeApiBaseUrl,
} from './utils/bridge';
export { getEthUsdtResetData, isEthUsdt } from './utils/bridge';
32 changes: 0 additions & 32 deletions packages/bridge-controller/src/utils/bridge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable n/no-process-env */
import { Contract } from '@ethersproject/contracts';
import { abiERC20 } from '@metamask/metamask-eth-abis';
import type { Hex } from '@metamask/utils';
@@ -9,16 +8,11 @@ import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
sumHexes,
getBridgeApiBaseUrl,
} from './bridge';
import {
ETH_USDT_ADDRESS,
METABRIDGE_ETHEREUM_ADDRESS,
} from '../constants/bridge';
import {
BRIDGE_DEV_API_BASE_URL,
BRIDGE_PROD_API_BASE_URL,
} from '../constants/bridge';
import { CHAIN_IDS } from '../constants/chains';
import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens';

@@ -141,30 +135,4 @@ 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);
});
});
});
13 changes: 0 additions & 13 deletions packages/bridge-controller/src/utils/bridge.ts
Original file line number Diff line number Diff line change
@@ -4,8 +4,6 @@ import type { Hex } from '@metamask/utils';

import {
DEFAULT_BRIDGE_CONTROLLER_STATE,
BRIDGE_DEV_API_BASE_URL,
BRIDGE_PROD_API_BASE_URL,
ETH_USDT_ADDRESS,
METABRIDGE_ETHEREUM_ADDRESS,
} from '../constants/bridge';
@@ -17,17 +15,6 @@ export const getDefaultBridgeControllerState = (): BridgeControllerState => {
return DEFAULT_BRIDGE_CONTROLLER_STATE;
};

export const getBridgeApiBaseUrl = () => {
if (process.env.BRIDGE_CUSTOM_API_BASE_URL) {
return process.env.BRIDGE_CUSTOM_API_BASE_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
*
21 changes: 18 additions & 3 deletions packages/bridge-controller/src/utils/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import {
} from './fetch';
import mockBridgeQuotesErc20Erc20 from '../../tests/mock-quotes-erc20-erc20.json';
import mockBridgeQuotesNativeErc20 from '../../tests/mock-quotes-native-erc20.json';
import { BridgeClientId } from '../constants/bridge';
import { BridgeClientId, BRIDGE_PROD_API_BASE_URL } from '../constants/bridge';
import { CHAIN_IDS } from '../constants/chains';

const mockFetchFn = jest.fn();
@@ -56,6 +56,7 @@ describe('fetch', () => {
const result = await fetchBridgeFeatureFlags(
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);

expect(mockFetchFn).toHaveBeenCalledWith(
@@ -129,6 +130,7 @@ describe('fetch', () => {
const result = await fetchBridgeFeatureFlags(
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);

expect(mockFetchFn).toHaveBeenCalledWith(
@@ -156,7 +158,11 @@ describe('fetch', () => {
mockFetchFn.mockRejectedValue(mockError);

await expect(
fetchBridgeFeatureFlags(BridgeClientId.EXTENSION, mockFetchFn),
fetchBridgeFeatureFlags(
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
),
).rejects.toThrow(mockError);
});
});
@@ -196,6 +202,7 @@ describe('fetch', () => {
'0xa',
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);

expect(mockFetchFn).toHaveBeenCalledWith(
@@ -233,7 +240,12 @@ describe('fetch', () => {
mockFetchFn.mockRejectedValue(mockError);

await expect(
fetchBridgeTokens('0xa', BridgeClientId.EXTENSION, mockFetchFn),
fetchBridgeTokens(
'0xa',
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
),
).rejects.toThrow(mockError);
});
});
@@ -256,6 +268,7 @@ describe('fetch', () => {
signal,
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);

expect(mockFetchFn).toHaveBeenCalledWith(
@@ -290,6 +303,7 @@ describe('fetch', () => {
signal,
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);

expect(mockFetchFn).toHaveBeenCalledWith(
@@ -343,6 +357,7 @@ describe('fetch', () => {
signal,
BridgeClientId.EXTENSION,
mockFetchFn,
BRIDGE_PROD_API_BASE_URL,
);

expect(mockFetchFn).toHaveBeenCalledWith(
15 changes: 9 additions & 6 deletions packages/bridge-controller/src/utils/fetch.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ import { hexToNumber, numberToHex } from '@metamask/utils';
import {
isSwapsDefaultTokenAddress,
isSwapsDefaultTokenSymbol,
getBridgeApiBaseUrl,
} from './bridge';
import {
validateFeatureFlagsResponse,
@@ -35,13 +34,15 @@ export const getClientIdHeader = (clientId: string) => ({
*
* @param clientId - The client ID for metrics
* @param fetchFn - The fetch function to use
* @param bridgeApiBaseUrl - The base URL for the bridge API
* @returns The bridge feature flags
*/
export async function fetchBridgeFeatureFlags(
clientId: string,
fetchFn: FetchFunction,
bridgeApiBaseUrl: string,
): Promise<BridgeFeatureFlags> {
const url = `${getBridgeApiBaseUrl()}/getAllFeatureFlags`;
const url = `${bridgeApiBaseUrl}/getAllFeatureFlags`;
const rawFeatureFlags: unknown = await fetchFn(url, {
headers: getClientIdHeader(clientId),
});
@@ -82,17 +83,17 @@ 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
* @param bridgeApiBaseUrl - The base URL for the bridge API
* @returns A list of enabled (unblocked) tokens
*/
export async function fetchBridgeTokens(
chainId: Hex,
clientId: string,
fetchFn: FetchFunction,
bridgeApiBaseUrl: string,
): Promise<Record<string, SwapsTokenObject>> {
// TODO make token api v2 call
const url = `${getBridgeApiBaseUrl()}/getTokens?chainId=${hexToNumber(
chainId,
)}`;
const url = `${bridgeApiBaseUrl}/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,
@@ -132,13 +133,15 @@ export async function fetchBridgeTokens(
* @param signal - The abort signal
* @param clientId - The client ID for metrics
* @param fetchFn - The fetch function to use
* @param bridgeApiBaseUrl - The base URL for the bridge API
* @returns A list of bridge tx quotes
*/
export async function fetchBridgeQuotes(
request: QuoteRequest,
signal: AbortSignal,
clientId: string,
fetchFn: FetchFunction,
bridgeApiBaseUrl: string,
): Promise<QuoteResponse[]> {
const queryParams = new URLSearchParams({
walletAddress: request.walletAddress,
@@ -151,7 +154,7 @@ export async function fetchBridgeQuotes(
insufficientBal: request.insufficientBal ? 'true' : 'false',
resetApproval: request.resetApproval ? 'true' : 'false',
});
const url = `${getBridgeApiBaseUrl()}/getQuote?${queryParams}`;
const url = `${bridgeApiBaseUrl}/getQuote?${queryParams}`;
const quotes: unknown[] = await fetchFn(url, {
headers: getClientIdHeader(clientId),
signal,
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { StateMetadata } from '@metamask/base-controller';
import type { BridgeClientId } from '@metamask/bridge-controller';
import { BRIDGE_PROD_API_BASE_URL } from '@metamask/bridge-controller';
import { StaticIntervalPollingController } from '@metamask/polling-controller';
import { numberToHex, type Hex } from '@metamask/utils';

@@ -46,16 +47,24 @@ export class BridgeStatusController extends StaticIntervalPollingController<Brid

readonly #fetchFn: FetchFunction;

readonly #config: {
customBridgeApiBaseUrl?: string;
};

constructor({
messenger,
state,
clientId,
fetchFn,
config,
}: {
messenger: BridgeStatusControllerMessenger;
state?: Partial<BridgeStatusControllerState>;
clientId: BridgeClientId;
fetchFn: FetchFunction;
config?: {
customBridgeApiBaseUrl?: string;
};
}) {
super({
name: BRIDGE_STATUS_CONTROLLER_NAME,
@@ -70,6 +79,7 @@ export class BridgeStatusController extends StaticIntervalPollingController<Brid

this.#clientId = clientId;
this.#fetchFn = fetchFn;
this.#config = config ?? {};

// Register action handlers
this.messagingSystem.registerActionHandler(
@@ -242,6 +252,7 @@ export class BridgeStatusController extends StaticIntervalPollingController<Brid
statusRequest,
this.#clientId,
this.#fetchFn,
this.#config.customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL,
);
const newBridgeHistoryItem = {
...historyItem,
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { BridgeClientId, FeeType } from '@metamask/bridge-controller';
import {
BridgeClientId,
BRIDGE_PROD_API_BASE_URL,
FeeType,
} from '@metamask/bridge-controller';

import {
fetchBridgeTxStatus,
BRIDGE_STATUS_BASE_URL,
getBridgeStatusUrl,
getStatusRequestDto,
} from './bridge-status';
import type { StatusRequestWithSrcTxHash, FetchFunction } from '../types';
@@ -93,11 +97,12 @@ describe('utils', () => {
mockStatusRequest,
mockClientId,
mockFetch,
BRIDGE_PROD_API_BASE_URL,
);

// Verify the fetch was called with correct parameters
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining(BRIDGE_STATUS_BASE_URL),
expect.stringContaining(getBridgeStatusUrl(BRIDGE_PROD_API_BASE_URL)),
{
headers: { 'X-Client-Id': mockClientId },
},
@@ -125,7 +130,12 @@ describe('utils', () => {
.mockResolvedValue(invalidResponse);

await expect(
fetchBridgeTxStatus(mockStatusRequest, mockClientId, mockFetch),
fetchBridgeTxStatus(
mockStatusRequest,
mockClientId,
mockFetch,
BRIDGE_PROD_API_BASE_URL,
),
// eslint-disable-next-line jest/require-to-throw-message
).rejects.toThrow();
});
@@ -136,7 +146,12 @@ describe('utils', () => {
.mockRejectedValue(new Error('Network error'));

await expect(
fetchBridgeTxStatus(mockStatusRequest, mockClientId, mockFetch),
fetchBridgeTxStatus(
mockStatusRequest,
mockClientId,
mockFetch,
BRIDGE_PROD_API_BASE_URL,
),
).rejects.toThrow('Network error');
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Quote } from '@metamask/bridge-controller';
import { getBridgeApiBaseUrl } from '@metamask/bridge-controller';

import { validateBridgeStatusResponse } from './validators';
import type {
@@ -13,7 +12,8 @@ export const getClientIdHeader = (clientId: string) => ({
'X-Client-Id': clientId,
});

export const BRIDGE_STATUS_BASE_URL = `${getBridgeApiBaseUrl()}/getTxStatus`;
export const getBridgeStatusUrl = (bridgeApiBaseUrl: string) =>
`${bridgeApiBaseUrl}/getTxStatus`;

export const getStatusRequestDto = (
statusRequest: StatusRequestWithSrcTxHash,
@@ -40,12 +40,13 @@ export const fetchBridgeTxStatus = async (
statusRequest: StatusRequestWithSrcTxHash,
clientId: string,
fetchFn: FetchFunction,
bridgeApiBaseUrl: string,
): Promise<StatusResponse> => {
const statusRequestDto = getStatusRequestDto(statusRequest);
const params = new URLSearchParams(statusRequestDto);

// Fetch
const url = `${BRIDGE_STATUS_BASE_URL}?${params.toString()}`;
const url = `${getBridgeStatusUrl(bridgeApiBaseUrl)}?${params.toString()}`;

const rawTxStatus: unknown = await fetchFn(url, {
headers: getClientIdHeader(clientId),

Unchanged files with check annotations Beta

import { GasPricesController } from '@metamask/example-controllers';
import type { GasPricesControllerMessenger } from '@metamask/example-controllers';
import type {

Check warning on line 5 in examples/example-controllers/src/gas-prices-controller.test.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

`../../../packages/base-controller/tests/helpers` type import should occur after import of `./network-controller-types`
ExtractAvailableAction,
ExtractAvailableEvent,
} from '../../../packages/base-controller/tests/helpers';
/**
* The service object that is used to obtain gas prices.
*/
#gasPricesService: AbstractGasPricesService;

Check warning on line 196 in examples/example-controllers/src/gas-prices-controller.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

Member '#gasPricesService' is never reassigned; mark it as `readonly`
/**
* Constructs a new {@link GasPricesController}.
*/
async updateGasPrices() {
const { chainId } = this.messagingSystem.call('NetworkController:getState');
const gasPricesResponse = await this.#gasPricesService.fetchGasPrices(

Check warning on line 241 in examples/example-controllers/src/gas-prices-controller.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

Replace `·await·this.#gasPricesService.fetchGasPrices(⏎······chainId,⏎····` with `⏎······await·this.#gasPricesService.fetchGasPrices(chainId`
chainId,
);
this.update((state) => {
* ```
*/
export class GasPricesService {
#fetch: typeof fetch;

Check warning on line 41 in examples/example-controllers/src/gas-prices-service/gas-prices-service.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

Member '#fetch' is never reassigned; mark it as `readonly`
/**
* Constructs a new GasPricesService object.
this.#fetch = fetchFunction;
}
/**

Check warning on line 56 in examples/example-controllers/src/gas-prices-service/gas-prices-service.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

Missing JSDoc @returns declaration
* Makes a request to the API in order to retrieve gas prices for a particular
* chain.
*
ExtractAvailableEvent,
} from '../../../packages/base-controller/tests/helpers';
import { PROTOTYPE_POLLUTION_BLOCKLIST } from '../../../packages/controller-utils/src/util';
import type { PetNamesControllerMessenger } from './pet-names-controller';

Check warning on line 8 in examples/example-controllers/src/pet-names-controller.test.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

`./pet-names-controller` type import should occur before type import of `../../../packages/base-controller/tests/helpers`
import { PetNamesController } from './pet-names-controller';

Check warning on line 9 in examples/example-controllers/src/pet-names-controller.test.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

`./pet-names-controller` import should occur before type import of `../../../packages/base-controller/tests/helpers`
describe('PetNamesController', () => {
describe('constructor', () => {
import type { SnapControllerState } from '@metamask/snaps-controllers';
import { SnapStatus } from '@metamask/snaps-utils';
import type { CaipChainId } from '@metamask/utils';
import * as uuid from 'uuid';

Check warning on line 20 in packages/accounts-controller/src/AccountsController.test.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

No exported names found in module 'uuid'
import type { V4Options } from 'uuid';
import type {
import type { Hex } from '@metamask/utils';
/**
* @type ContactEntry

Check warning on line 17 in packages/address-book-controller/src/AddressBookController.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

'@type' is redundant when using a type system
*
* ContactEntry representation
* @property address - Hex address of a recipient account

Check warning on line 20 in packages/address-book-controller/src/AddressBookController.ts

GitHub Actions / Lint, build, and test / Lint (20.x)

'@Property' is redundant when using a type system
* @property name - Nickname associated with this address
* @property importTime - Data time when an account as created/imported
*/