Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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))
Comment thread
salimtb marked this conversation as resolved.
Outdated

## [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,
};
Comment thread
bfullam marked this conversation as resolved.
}
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;
Comment thread
salimtb marked this conversation as resolved.

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