Skip to content

Commit 1690874

Browse files
authored
feat: fetch balances accout api array (#6487)
## Explanation Previously, the TokenBalancesController used a simple boolean useAccountAPI configuration that applied an all-or-nothing approach - either all chains would use the Accounts API for balance fetching, or none would. This lacked the flexibility needed for real-world scenarios where different chains have different levels of Accounts API support. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 89ebe0d commit 1690874

File tree

5 files changed

+285
-38
lines changed

5 files changed

+285
-38
lines changed

packages/assets-controllers/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING:** Replace `useAccountAPI` boolean with `accountsApiChainIds` array in `TokenBalancesController` for granular per-chain Accounts API configuration ([#6487](https://github.com/MetaMask/core/pull/6487))
13+
1014
## [74.3.3]
1115

1216
### Changed

packages/assets-controllers/src/AccountTrackerController.test.ts

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
AllowedEvents,
1717
} from './AccountTrackerController';
1818
import { AccountTrackerController } from './AccountTrackerController';
19+
import { AccountsApiBalanceFetcher } from './multi-chain-accounts-service/api-balance-fetcher';
1920
import { getTokenBalancesForMultipleAddresses } from './multicall';
2021
import { FakeProvider } from '../../../tests/fake-provider';
2122
import { advanceTime } from '../../../tests/helpers';
@@ -867,7 +868,7 @@ describe('AccountTrackerController', () => {
867868
},
868869
},
869870
},
870-
useAccountsAPI: false, // Disable API balance fetchers to force RPC usage
871+
accountsApiChainIds: [], // Disable API balance fetchers to force RPC usage
871872
},
872873
isMultiAccountBalancesEnabled: true,
873874
selectedAccount: ACCOUNT_1,
@@ -910,7 +911,7 @@ describe('AccountTrackerController', () => {
910911
await withController(
911912
{
912913
options: {
913-
useAccountsAPI: true,
914+
accountsApiChainIds: ['0x1'],
914915
// allowExternalServices not provided - should default to () => true (line 390)
915916
},
916917
isMultiAccountBalancesEnabled: true,
@@ -924,7 +925,7 @@ describe('AccountTrackerController', () => {
924925
// Refresh balances for mainnet (supported by API)
925926
await refresh(clock, ['mainnet']);
926927

927-
// Since allowExternalServices defaults to () => true (line 390), and useAccountsAPI is true,
928+
// Since allowExternalServices defaults to () => true (line 390), and accountsApiChainIds includes '0x1',
928929
// the API fetcher should be used, which means fetch should be called
929930
expect(fetchSpy).toHaveBeenCalled();
930931

@@ -943,7 +944,7 @@ describe('AccountTrackerController', () => {
943944
await withController(
944945
{
945946
options: {
946-
useAccountsAPI: true,
947+
accountsApiChainIds: ['0x1'],
947948
allowExternalServices: () => true, // Explicitly set to true
948949
},
949950
isMultiAccountBalancesEnabled: true,
@@ -957,7 +958,7 @@ describe('AccountTrackerController', () => {
957958
// Refresh balances for mainnet (supported by API)
958959
await refresh(clock, ['mainnet']);
959960

960-
// Since allowExternalServices is true and useAccountsAPI is true,
961+
// Since allowExternalServices is true and accountsApiChainIds includes '0x1',
961962
// the API fetcher should be used, which means fetch should be called
962963
expect(fetchSpy).toHaveBeenCalled();
963964

@@ -976,7 +977,7 @@ describe('AccountTrackerController', () => {
976977
await withController(
977978
{
978979
options: {
979-
useAccountsAPI: true,
980+
accountsApiChainIds: ['0x1'],
980981
allowExternalServices: () => false, // Explicitly set to false
981982
},
982983
isMultiAccountBalancesEnabled: true,
@@ -1001,6 +1002,79 @@ describe('AccountTrackerController', () => {
10011002
);
10021003
});
10031004
});
1005+
1006+
it('should continue to next fetcher when current fetcher supports no chains', async () => {
1007+
// Spy on the AccountsApiBalanceFetcher's supports method to return false
1008+
const supportsSpy = jest
1009+
.spyOn(AccountsApiBalanceFetcher.prototype, 'supports')
1010+
.mockReturnValue(false);
1011+
1012+
await withController(
1013+
{
1014+
options: {
1015+
accountsApiChainIds: ['0x1'], // Configure to use AccountsAPI for mainnet
1016+
allowExternalServices: () => true,
1017+
},
1018+
isMultiAccountBalancesEnabled: true,
1019+
selectedAccount: ACCOUNT_1,
1020+
listAccounts: [ACCOUNT_1],
1021+
},
1022+
async ({ controller, refresh }) => {
1023+
// Mock RPC query to return balance (this should be used since AccountsAPI supports nothing)
1024+
mockedQuery.mockResolvedValue('0x123456');
1025+
1026+
// Refresh balances for mainnet
1027+
await refresh(clock, ['mainnet']);
1028+
1029+
// Verify that the supports method was called (meaning we reached the continue logic)
1030+
expect(supportsSpy).toHaveBeenCalledWith('0x1');
1031+
1032+
// Verify that state was still updated via RPC fetcher fallback
1033+
expect(
1034+
controller.state.accountsByChainId['0x1'][CHECKSUM_ADDRESS_1]
1035+
.balance,
1036+
).toBeDefined();
1037+
1038+
supportsSpy.mockRestore();
1039+
},
1040+
);
1041+
});
1042+
1043+
it('should log warning when balance fetcher throws an error', async () => {
1044+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
1045+
1046+
// Mock AccountsApiBalanceFetcher to throw an error
1047+
const fetchSpy = jest
1048+
.spyOn(AccountsApiBalanceFetcher.prototype, 'fetch')
1049+
.mockRejectedValue(new Error('API request failed'));
1050+
1051+
await withController(
1052+
{
1053+
options: {
1054+
accountsApiChainIds: ['0x1'], // Configure to use AccountsAPI for mainnet
1055+
allowExternalServices: () => true,
1056+
},
1057+
isMultiAccountBalancesEnabled: true,
1058+
selectedAccount: ACCOUNT_1,
1059+
listAccounts: [ACCOUNT_1],
1060+
},
1061+
async ({ refresh }) => {
1062+
// Mock RPC query to return balance (fallback after API fails)
1063+
mockedQuery.mockResolvedValue('0x123456');
1064+
1065+
// Refresh balances for mainnet
1066+
await refresh(clock, ['mainnet']);
1067+
1068+
// Verify that console.warn was called with the error message
1069+
expect(consoleWarnSpy).toHaveBeenCalledWith(
1070+
expect.stringContaining('Balance fetcher failed for chains 0x1:'),
1071+
);
1072+
1073+
fetchSpy.mockRestore();
1074+
consoleWarnSpy.mockRestore();
1075+
},
1076+
);
1077+
});
10041078
});
10051079

10061080
describe('syncBalanceWithAddresses', () => {
@@ -1051,6 +1125,41 @@ describe('AccountTrackerController', () => {
10511125
},
10521126
);
10531127
});
1128+
1129+
it('should handle timeout in syncBalanceWithAddresses gracefully', async () => {
1130+
await withController(
1131+
{
1132+
isMultiAccountBalancesEnabled: true,
1133+
selectedAccount: ACCOUNT_1,
1134+
listAccounts: [],
1135+
},
1136+
async ({ controller }) => {
1137+
// Mock safelyExecuteWithTimeout to return undefined (timeout case)
1138+
mockedSafelyExecuteWithTimeout.mockImplementation(
1139+
async () => undefined, // Simulates timeout behavior
1140+
);
1141+
1142+
const result = await controller.syncBalanceWithAddresses([
1143+
ADDRESS_1,
1144+
ADDRESS_2,
1145+
]);
1146+
1147+
// Verify that the result is an empty object when all operations timeout
1148+
expect(result).toStrictEqual({});
1149+
1150+
// Restore the mock
1151+
mockedSafelyExecuteWithTimeout.mockImplementation(
1152+
async (operation: () => Promise<unknown>) => {
1153+
try {
1154+
return await operation();
1155+
} catch {
1156+
return undefined;
1157+
}
1158+
},
1159+
);
1160+
},
1161+
);
1162+
});
10541163
});
10551164

10561165
it('should call refresh every interval on polling', async () => {

packages/assets-controllers/src/AccountTrackerController.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
219219

220220
readonly #includeStakedAssets: boolean;
221221

222+
readonly #accountsApiChainIds: ChainIdHex[];
223+
222224
readonly #getStakedBalanceForChain: AssetsContractController['getStakedBalanceForChain'];
223225

224226
readonly #balanceFetchers: BalanceFetcher[];
@@ -232,7 +234,7 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
232234
* @param options.messenger - The controller messaging system.
233235
* @param options.getStakedBalanceForChain - The function to get the staked native asset balance for a chain.
234236
* @param options.includeStakedAssets - Whether to include staked assets in the account balances.
235-
* @param options.useAccountsAPI - Enable AccountsAPI strategy (if supported chain).
237+
* @param options.accountsApiChainIds - Array of chainIds that should use Accounts-API strategy (if supported by API).
236238
* @param options.allowExternalServices - Disable external HTTP calls (privacy / offline mode).
237239
*/
238240
constructor({
@@ -241,15 +243,15 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
241243
messenger,
242244
getStakedBalanceForChain,
243245
includeStakedAssets = false,
244-
useAccountsAPI = false,
246+
accountsApiChainIds = [],
245247
allowExternalServices = () => true,
246248
}: {
247249
interval?: number;
248250
state?: Partial<AccountTrackerControllerState>;
249251
messenger: AccountTrackerControllerMessenger;
250252
getStakedBalanceForChain: AssetsContractController['getStakedBalanceForChain'];
251253
includeStakedAssets?: boolean;
252-
useAccountsAPI?: boolean;
254+
accountsApiChainIds?: ChainIdHex[];
253255
allowExternalServices?: () => boolean;
254256
}) {
255257
const { selectedNetworkClientId } = messenger.call(
@@ -275,11 +277,12 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
275277
this.#getStakedBalanceForChain = getStakedBalanceForChain;
276278

277279
this.#includeStakedAssets = includeStakedAssets;
280+
this.#accountsApiChainIds = [...accountsApiChainIds];
278281

279282
// Initialize balance fetchers - Strategy order: API first, then RPC fallback
280283
this.#balanceFetchers = [
281-
...(useAccountsAPI && allowExternalServices()
282-
? [new AccountsApiBalanceFetcher('extension', this.#getProvider)]
284+
...(accountsApiChainIds.length > 0 && allowExternalServices()
285+
? [this.#createAccountsApiFetcher()]
283286
: []),
284287
createAccountTrackerRpcBalanceFetcher(
285288
this.#getProvider,
@@ -390,6 +393,31 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
390393
);
391394
};
392395

396+
/**
397+
* Creates an AccountsApiBalanceFetcher that only supports chains in the accountsApiChainIds array
398+
*
399+
* @returns A BalanceFetcher that wraps AccountsApiBalanceFetcher with chainId filtering
400+
*/
401+
readonly #createAccountsApiFetcher = (): BalanceFetcher => {
402+
const originalFetcher = new AccountsApiBalanceFetcher(
403+
'extension',
404+
this.#getProvider,
405+
);
406+
407+
return {
408+
supports: (chainId: ChainIdHex): boolean => {
409+
// Only support chains that are both:
410+
// 1. In our specified accountsApiChainIds array
411+
// 2. Actually supported by the AccountsApi
412+
return (
413+
this.#accountsApiChainIds.includes(chainId) &&
414+
originalFetcher.supports(chainId)
415+
);
416+
},
417+
fetch: originalFetcher.fetch.bind(originalFetcher),
418+
};
419+
};
420+
393421
/**
394422
* Resolves a networkClientId to a network client config
395423
* or globally selected network config if not provided

0 commit comments

Comments
 (0)