Skip to content
Open
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Optional constructor option `getUseAssetsControllerForRates`: when it returns true, exchange rates are read from the new `@metamask/assets-controller` (`AssetsController:getExchangeRatesForBridge`) instead of `MultichainAssetsRatesController`, `TokenRatesController`, and `CurrencyRateController`. ([#8090](https://github.com/MetaMask/core/pull/8090))

## [67.4.0]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/bridge-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@ethersproject/contracts": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@metamask/accounts-controller": "^36.0.1",
"@metamask/assets-controller": "^2.2.0",
"@metamask/assets-controllers": "^100.0.3",
"@metamask/base-controller": "^9.0.0",
"@metamask/controller-utils": "^11.19.0",
Expand Down
248 changes: 239 additions & 9 deletions packages/bridge-controller/src/bridge-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ const bridgeConfig = {
},
};

const metricsContext = {
token_symbol_source: 'ETH',
token_symbol_destination: 'USDC',
usd_amount_source: 100,
stx_enabled: true,
security_warnings: [],
warnings: [],
};

describe('BridgeController', function () {
let bridgeController: BridgeController;

Expand Down Expand Up @@ -144,6 +153,236 @@ describe('BridgeController', function () {
expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE);
});

describe('getExchangeRateSources and fetchAssetExchangeRates', function () {
it('calls MultichainAssetsRatesController, CurrencyRateController, and TokenRatesController when useAssetsControllerForRates is false', async function () {
jest.useFakeTimers();
const hasSufficientBalanceSpy = jest
.spyOn(balanceUtils, 'hasSufficientBalance')
.mockResolvedValue(true);

const getStateReturn = {
conversionRates: {},
currencyRates: {},
marketData: {},
currentCurrency: 'USD',
};
messengerMock.call.mockImplementation(
(actionType: string): ReturnType<BridgeControllerMessenger['call']> => {
if (actionType === 'AuthenticationController:getBearerToken') {
return 'AUTH_TOKEN';
}
if (
actionType === 'MultichainAssetsRatesController:getState' ||
actionType === 'CurrencyRateController:getState' ||
actionType === 'TokenRatesController:getState'
) {
return getStateReturn as never;
}
return {
remoteFeatureFlags: { bridgeConfig: { ...bridgeConfig } },
address: '0x123',
provider: jest.fn(),
} as never;
},
);

const selectIsAssetExchangeRateInStateSpy = jest.spyOn(
selectors,
'selectIsAssetExchangeRateInState',
);

await bridgeController.updateBridgeQuoteRequestParams(
{
srcChainId: '0x1',
destChainId: '0xa',
srcTokenAddress: '0x0000000000000000000000000000000000000000',
destTokenAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
srcTokenAmount: '1000000000000000000',
walletAddress: '0x123',
slippage: 0.5,
},
metricsContext,
);

jest.advanceTimersToNextTimer();
await flushPromises();

expect(messengerMock.call).toHaveBeenCalledWith(
'MultichainAssetsRatesController:getState',
);
expect(messengerMock.call).toHaveBeenCalledWith(
'CurrencyRateController:getState',
);
expect(messengerMock.call).toHaveBeenCalledWith(
'TokenRatesController:getState',
);
expect(messengerMock.call).not.toHaveBeenCalledWith(
'AssetsController:getExchangeRatesForBridge',
);

expect(selectIsAssetExchangeRateInStateSpy).toHaveBeenCalled();
const [firstCallSources] =
selectIsAssetExchangeRateInStateSpy.mock.calls[0];
expect(firstCallSources).toHaveProperty('assetExchangeRates');
expect(firstCallSources).toHaveProperty('conversionRates');
expect(firstCallSources).toHaveProperty('currencyRates');
expect(firstCallSources).toHaveProperty('marketData');

hasSufficientBalanceSpy.mockRestore();
selectIsAssetExchangeRateInStateSpy.mockRestore();
});

it('calls AssetsController:getExchangeRatesForBridge when getUseAssetsControllerForRates returns true', async function () {
jest.useFakeTimers();
const controllerWithAssetsRates = new BridgeController({
messenger: messengerMock,
getLayer1GasFee: getLayer1GasFeeMock,
clientId: BridgeClientId.EXTENSION,
clientVersion: '13.7.0',
fetchFn: mockFetchFn,
trackMetaMetricsFn,
getUseAssetsControllerForRates: (): boolean => true,
});
controllerWithAssetsRates.resetState();

const hasSufficientBalanceSpy = jest
.spyOn(balanceUtils, 'hasSufficientBalance')
.mockResolvedValue(true);

const bridgeRatesReturn = {
conversionRates: {},
currencyRates: {},
marketData: {},
currentCurrency: 'EUR',
};
messengerMock.call.mockImplementation(
(actionType: string): ReturnType<BridgeControllerMessenger['call']> => {
if (actionType === 'AuthenticationController:getBearerToken') {
return 'AUTH_TOKEN';
}
if (actionType === 'AssetsController:getExchangeRatesForBridge') {
return bridgeRatesReturn as never;
}
return {
remoteFeatureFlags: { bridgeConfig: { ...bridgeConfig } },
address: '0x123',
provider: jest.fn(),
} as never;
},
);

const selectIsAssetExchangeRateInStateSpy = jest.spyOn(
selectors,
'selectIsAssetExchangeRateInState',
);

await controllerWithAssetsRates.updateBridgeQuoteRequestParams(
{
srcChainId: '0x1',
destChainId: '0xa',
srcTokenAddress: '0x0000000000000000000000000000000000000000',
destTokenAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
srcTokenAmount: '1000000000000000000',
walletAddress: '0x123',
slippage: 0.5,
},
metricsContext,
);

jest.advanceTimersToNextTimer();
await flushPromises();

expect(messengerMock.call).toHaveBeenCalledWith(
'AssetsController:getExchangeRatesForBridge',
);
expect(messengerMock.call).not.toHaveBeenCalledWith(
'MultichainAssetsRatesController:getState',
);

expect(selectIsAssetExchangeRateInStateSpy).toHaveBeenCalled();
const [firstCallSources] =
selectIsAssetExchangeRateInStateSpy.mock.calls[0];
expect(firstCallSources).toHaveProperty('assetExchangeRates');
expect(firstCallSources).toHaveProperty('currentCurrency', 'EUR');

hasSufficientBalanceSpy.mockRestore();
selectIsAssetExchangeRateInStateSpy.mockRestore();
});

it('calls selectIsAssetExchangeRateInState with exchange rate sources, src chain/address, and dest chain/address', async function () {
jest.useFakeTimers();
const hasSufficientBalanceSpy = jest
.spyOn(balanceUtils, 'hasSufficientBalance')
.mockResolvedValue(true);

messengerMock.call.mockImplementation(
(actionType: string): ReturnType<BridgeControllerMessenger['call']> => {
if (actionType === 'AuthenticationController:getBearerToken') {
return 'AUTH_TOKEN';
}
if (
actionType === 'MultichainAssetsRatesController:getState' ||
actionType === 'CurrencyRateController:getState' ||
actionType === 'TokenRatesController:getState'
) {
return {
conversionRates: {},
currencyRates: {},
marketData: {},
currentCurrency: 'USD',
} as never;
}
return {
remoteFeatureFlags: { bridgeConfig: { ...bridgeConfig } },
address: '0x123',
provider: jest.fn(),
} as never;
},
);

const selectIsAssetExchangeRateInStateSpy = jest.spyOn(
selectors,
'selectIsAssetExchangeRateInState',
);

const quoteParams = {
srcChainId: '0x1',
destChainId: '0xa',
srcTokenAddress: '0x0000000000000000000000000000000000000000',
destTokenAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
srcTokenAmount: '1000000000000000000',
walletAddress: '0x123',
slippage: 0.5,
};

await bridgeController.updateBridgeQuoteRequestParams(
quoteParams,
metricsContext,
);

jest.advanceTimersToNextTimer();
await flushPromises();

expect(selectIsAssetExchangeRateInStateSpy).toHaveBeenCalledWith(
expect.objectContaining({
assetExchangeRates: expect.any(Object),
}),
quoteParams.srcChainId,
quoteParams.srcTokenAddress,
);
expect(selectIsAssetExchangeRateInStateSpy).toHaveBeenCalledWith(
expect.objectContaining({
assetExchangeRates: expect.any(Object),
}),
quoteParams.destChainId,
quoteParams.destTokenAddress,
);

hasSufficientBalanceSpy.mockRestore();
selectIsAssetExchangeRateInStateSpy.mockRestore();
});
});

it('setBridgeFeatureFlags should fetch and set the bridge feature flags', async function () {
const remoteFeatureFlagControllerState = {
cacheTimestamp: 1745515389440,
Expand Down Expand Up @@ -206,15 +445,6 @@ describe('BridgeController', function () {
expect(setIntervalLengthSpy).toHaveBeenCalledWith(3);
});

const metricsContext = {
token_symbol_source: 'ETH',
token_symbol_destination: 'USDC',
usd_amount_source: 100,
stx_enabled: true,
security_warnings: [],
warnings: [],
};

it('updateBridgeQuoteRequestParams should update the quoteRequest state', async function () {
messengerMock.call.mockReturnValue({
currentCurrency: 'usd',
Expand Down
38 changes: 33 additions & 5 deletions packages/bridge-controller/src/bridge-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import {
import { CHAIN_IDS } from './constants/chains';
import { SWAPS_CONTRACT_ADDRESSES } from './constants/swaps';
import { TraceName } from './constants/traces';
import { selectIsAssetExchangeRateInState } from './selectors';
import {
ExchangeRateSourcesForLookup,
selectIsAssetExchangeRateInState,
} from './selectors';
import { RequestStatus } from './types';
import type {
L1GasFees,
Expand Down Expand Up @@ -196,6 +199,14 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
customBridgeApiBaseUrl?: string;
};

/**
* Returns whether to use AssetsController for exchange rates.
* Set via constructor option getUseAssetsControllerForRates; defaults to false.
*
* @returns True when exchange rates should be read from AssetsController:getExchangeRatesForBridge.
*/
readonly #getUseAssetsControllerForRates: () => boolean;

constructor({
messenger,
state,
Expand All @@ -206,6 +217,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
config,
trackMetaMetricsFn,
traceFn,
getUseAssetsControllerForRates,
}: {
messenger: BridgeControllerMessenger;
state?: Partial<BridgeControllerState>;
Expand All @@ -224,6 +236,12 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
properties: CrossChainSwapsEventProperties<EventName>,
) => void;
traceFn?: TraceCallback;
/**
* When provided, called to determine whether to use AssetsController for exchange rates.
* When true, rates are read from AssetsController:getExchangeRatesForBridge instead of
* MultichainAssetsRatesController, TokenRatesController, and CurrencyRateController.
*/
getUseAssetsControllerForRates?: () => boolean;
}) {
super({
name: BRIDGE_CONTROLLER_NAME,
Expand All @@ -245,6 +263,8 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
this.#trackMetaMetricsFn = trackMetaMetricsFn;
this.#config = config ?? {};
this.#trace = traceFn ?? (((_request, fn) => fn?.()) as TraceCallback);
this.#getUseAssetsControllerForRates =
getUseAssetsControllerForRates ?? (() => false);

// Register action handlers
this.messenger.registerActionHandler(
Expand Down Expand Up @@ -399,7 +419,14 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
);
};

readonly #getExchangeRateSources = () => {
readonly #getExchangeRateSources = (): ExchangeRateSourcesForLookup => {
if (this.#getUseAssetsControllerForRates()) {
return {
...this.messenger.call('AssetsController:getExchangeRatesForBridge'),
historicalPrices: {},
...this.state,
};
}
return {
...this.messenger.call('MultichainAssetsRatesController:getState'),
...this.messenger.call('CurrencyRateController:getState'),
Expand Down Expand Up @@ -453,9 +480,10 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
);
}

const currency = this.messenger.call(
'CurrencyRateController:getState',
).currentCurrency;
const currency = this.#getUseAssetsControllerForRates()
? this.messenger.call('AssetsController:getExchangeRatesForBridge')
.currentCurrency
: this.messenger.call('CurrencyRateController:getState').currentCurrency;

if (assetIds.size === 0) {
return;
Expand Down
Loading
Loading