From c2a43912e379859553c0842bff14b63b39029323 Mon Sep 17 00:00:00 2001 From: Kriys94 Date: Mon, 12 Oct 2026 11:31:30 +0200 Subject: [PATCH 1/2] fix(assets-controller): remove staked balance in fast source --- oxlint-suppressions.json | 8 -- packages/assets-controller/CHANGELOG.md | 2 + .../assets-controller/src/AssetsController.ts | 17 ++-- .../AccountsApiDataSource.test.ts | 8 +- .../src/data-sources/AccountsApiDataSource.ts | 86 ++++++------------- .../data-sources/StakedBalanceDataSource.ts | 4 +- .../services/StakedBalanceFetcher.ts | 4 +- ...tering.accounts-api-v6.integration.test.ts | 8 -- ...c-spam-token-filtering.integration.test.ts | 8 -- .../pipeline/buildFastFetchSources.test.ts | 24 ++---- .../src/pipeline/buildFastFetchSources.ts | 25 ++---- 11 files changed, 64 insertions(+), 130 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 7df780443d1..54fd69caeb4 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -737,9 +737,6 @@ "packages/assets-controller/src/AssetsController.ts": { "typescript/no-unnecessary-type-assertion": { "count": 1 - }, - "typescript/no-unsafe-assignment": { - "count": 5 } }, "packages/assets-controller/src/__fixtures__/MockAssetControllerMessenger.ts": { @@ -933,11 +930,6 @@ "count": 2 } }, - "packages/assets-controller/src/pipeline/buildFastFetchSources.test.ts": { - "no-unused-vars": { - "count": 1 - } - }, "packages/assets-controller/src/pipeline/executeAssetsPipeline.ts": { "no-unused-vars": { "count": 1 diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 80f84594cc7..5490d4b683a 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/transaction-controller` from `^72.2.0` to `^72.2.1` ([#10817](https://github.com/MetaMask/core/pull/10817)) - Bump `@metamask/accounts-controller` from `^40.0.0` to `^40.1.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) - Bump `@metamask/keyring-controller` from `^28.1.0` to `^28.2.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) +- Poll staked balances every hour instead of every 3 minutes. Subscribe still fetches immediately, and a confirmed transaction that touches the staking contract still refreshes the balance right away ([#10782](https://github.com/MetaMask/core/pull/10782)) +- Remove staked balances from the fast asset pipeline ([#10782](https://github.com/MetaMask/core/pull/10782)) ## [19.0.0] diff --git a/packages/assets-controller/src/AssetsController.ts b/packages/assets-controller/src/AssetsController.ts index 17d814a2b2e..e1873113d3f 100644 --- a/packages/assets-controller/src/AssetsController.ts +++ b/packages/assets-controller/src/AssetsController.ts @@ -1239,7 +1239,7 @@ export class AssetsController extends BaseController< this.#handleNetworkAdded(networkConfiguration.chainId); this.#refreshAssetsAfterNetworkAdded( networkConfiguration.chainId, - ).catch((error) => { + ).catch((error: unknown) => { log('Failed to refresh assets after network added', { error }); }); }, @@ -1396,7 +1396,7 @@ export class AssetsController extends BaseController< chainIds: [caipChainId], forceUpdate: true, bypassServerCache: true, - }).catch((error) => { + }).catch((error: unknown) => { log('Failed to refresh assets after transaction event', { error }); }); } @@ -1634,6 +1634,7 @@ export class AssetsController extends BaseController< */ bypassServerCache?: boolean; dataTypes?: DataType[]; + /** Specific CAIP-19 asset IDs whose prices should be refreshed. */ assetsForPriceUpdate?: Caip19AssetId[]; }, ): Promise>> { @@ -1733,7 +1734,6 @@ export class AssetsController extends BaseController< const fastSources = buildFastFetchSources( { accountsApiDataSource: this.#accountsApiDataSource, - stakedBalanceDataSource: this.#stakedBalanceDataSource, customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware, rpcFallbackMiddleware: this.#rpcFallbackMiddleware, detectionMiddleware: this.#detectionMiddleware, @@ -1777,7 +1777,6 @@ export class AssetsController extends BaseController< const fastSources = buildFastFetchSources( { accountsApiDataSource: this.#accountsApiDataSource, - stakedBalanceDataSource: this.#stakedBalanceDataSource, customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware, rpcFallbackMiddleware: this.#rpcFallbackMiddleware, detectionMiddleware: this.#detectionMiddleware, @@ -1955,7 +1954,7 @@ export class AssetsController extends BaseController< }); await this.#updateState(response); }, - }).catch((error) => log('Background pipeline failed', { error })); + }).catch((error: unknown) => log('Background pipeline failed', { error })); } #emitFastFetchTraces({ @@ -2601,7 +2600,7 @@ export class AssetsController extends BaseController< dataTypes: ['price'], chainIds, assetsForPriceUpdate, - }).catch((error) => { + }).catch((error: unknown) => { log('Failed to fetch missing prices', { error }); }); } @@ -3756,6 +3755,10 @@ export class AssetsController extends BaseController< /** * Subscribe to staked balance updates. + * This is the only staked-balance path: subscribe fetches immediately, then + * polls, and the data source refreshes again when a confirmed transaction + * touches the staking contract. The fast fetch lane does not query it. + * * Unlike regular balance data sources, the staked balance data source provides * supplementary data and does not participate in chain-claiming. * @@ -4235,7 +4238,7 @@ export class AssetsController extends BaseController< this.getAssets(accounts, { forceUpdate: true, dataTypes: ['balance', 'metadata'], - }).catch((error) => { + }).catch((error: unknown) => { log('Failed to refresh assets after network change', { error }); }); } diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts index e7d0cde3470..58dcdd8cac9 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts @@ -1796,7 +1796,7 @@ describe('AccountsApiDataSource', () => { controller.destroy(); }); - it('middleware removes handled chains from next request', async () => { + it('middleware keeps every requested chain for later lane stages', async () => { const { controller } = await setupController({ supportedChains: [1] }); const next = jest.fn().mockResolvedValue(undefined); @@ -1809,7 +1809,7 @@ describe('AccountsApiDataSource', () => { expect(next).toHaveBeenCalledWith( expect.objectContaining({ request: expect.objectContaining({ - chainIds: [CHAIN_POLYGON], + chainIds: [CHAIN_MAINNET, CHAIN_POLYGON], }), }), ); @@ -2266,7 +2266,7 @@ describe('AccountsApiDataSource', () => { controller.destroy(); }); - it('claims handled chains on v6 when tokenDetectionEnabled is false', async () => { + it('keeps requested chains on v6 when tokenDetectionEnabled is false', async () => { const { controller } = await setupController({ remoteFeatureFlags: { assetsAccountsApiV6: true }, tokenDetectionEnabled: (): boolean => false, @@ -2294,7 +2294,7 @@ describe('AccountsApiDataSource', () => { expect(nextFn).toHaveBeenCalledWith( expect.objectContaining({ request: expect.objectContaining({ - chainIds: [], + chainIds: [CHAIN_MAINNET], }), }), ); diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts index 4a9e0f67c94..c4fd7363ee8 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts @@ -913,9 +913,10 @@ export class AccountsApiDataSource extends AbstractDataSource< * Get the middleware for fetching balances via Accounts API. * This middleware: * - Supports multiple accounts in a single request - * - Uses unprocessedNetworks from API response to determine what to pass to next middleware - * - Merges response into context - * - Removes handled chains from request for next middleware + * - Fetches only chains this source currently supports + * - Merges that response into context + * - Leaves `request.chainIds` unchanged so later stages (RPC fallback, + * detection, prices) still see every chain the caller asked for * * @returns The middleware function for the assets pipeline. */ @@ -940,12 +941,19 @@ export class AccountsApiDataSource extends AbstractDataSource< return next(context); } - let successfullyHandledChains: ChainId[] = []; + const chainsToFetch = this.#getChainsToFetch(request); + if (chainsToFetch.length === 0) { + return next(context); + } try { - const response = await this.fetch(request); + const response = await this.fetch({ + ...request, + chainIds: chainsToFetch, + }); - // Merge response into context + // Merge response into context. The caller's chain list stays intact: + // the RPC fallback re-reads omitted tokens on chains this fetch answered. if (response.assetsBalance) { context.response.assetsBalance ??= {}; for (const [accountId, accountBalances] of Object.entries( @@ -957,44 +965,10 @@ export class AccountsApiDataSource extends AbstractDataSource< }; } } - - // Determine successfully handled chains (exclude unprocessed/error chains) - const unprocessedChains = new Set(Object.keys(response.errors ?? {})); - successfullyHandledChains = request.chainIds.filter( - (chainId) => !unprocessedChains.has(chainId), - ); - - // When token detection is off and we filtered out all balance data (e.g. new - // account with empty state), do not claim any chain as handled so that RPC - // middleware can still process them and fetch native balances (ETH, MATIC, etc.). - if ( - !this.#tokenDetectionEnabled() && - (!response.assetsBalance || - Object.keys(response.assetsBalance).length === 0) - ) { - successfullyHandledChains = []; - } } catch (error) { log('Middleware fetch failed', { error }); - successfullyHandledChains = []; } - // Remove successfully handled chains from request for next middleware - if (successfullyHandledChains.length > 0) { - const remainingChains = request.chainIds.filter( - (chainId) => !successfullyHandledChains.includes(chainId), - ); - - return next({ - ...context, - request: { - ...request, - chainIds: remainingChains, - }, - }); - } - - // No chains handled - pass context unchanged return next(context); }; @@ -1011,12 +985,19 @@ export class AccountsApiDataSource extends AbstractDataSource< return next(context); } - let remainingChains = request.chainIds; + const chainsToFetch = this.#getChainsToFetch(request); + if (chainsToFetch.length === 0) { + return next(context); + } try { - const response = await this.fetch(request); + const response = await this.fetch({ + ...request, + chainIds: chainsToFetch, + }); - // Merge response into context + // Merge response into context. Failed chains stay on `response.errors`; + // the RPC fallback retries those. The caller's chain list is unchanged. if (response.assetsBalance) { context.response.assetsBalance ??= {}; for (const [accountId, accountBalances] of Object.entries( @@ -1034,30 +1015,17 @@ export class AccountsApiDataSource extends AbstractDataSource< updateMode: 'full', }; - const errors = response.errors ?? {}; - remainingChains = request.chainIds.filter((chainId) => errors[chainId]); - - // Remaining chains are exactly the failed ones; RPC fallback retries them. - if (remainingChains.length > 0) { + if (response.errors && Object.keys(response.errors).length > 0) { context.response.errors = { ...context.response.errors, - ...Object.fromEntries( - remainingChains.map((chainId) => [chainId, errors[chainId]]), - ), + ...response.errors, }; } } catch (error) { log('Middleware fetch failed', { error }); - remainingChains = request.chainIds; } - return next({ - ...context, - request: { - ...request, - chainIds: remainingChains, - }, - }); + return next(context); }; // ============================================================================ diff --git a/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts b/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts index a9d7d6f4a75..65d5a0c1528 100644 --- a/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts +++ b/packages/assets-controller/src/data-sources/StakedBalanceDataSource.ts @@ -38,7 +38,7 @@ import { } from './evm-rpc-services/index.js'; const CONTROLLER_NAME = 'StakedBalanceDataSource'; -const DEFAULT_POLL_INTERVAL = 180_000; // 3 minutes +const DEFAULT_POLL_INTERVAL = 3_600_000; // 1 hour /** Metadata for staked ETH (same symbol and decimals as native ETH). */ const STAKED_ETH_METADATA: AssetMetadata = { @@ -54,7 +54,7 @@ const log = createModuleLogger(projectLogger, CONTROLLER_NAME); export type StakedBalanceDataSourceConfig = { /** Whether staked balance fetching is enabled (default: true). */ enabled?: boolean; - /** Polling interval in ms (default: 180s / 3 min). */ + /** Polling interval in ms (default: 1 hour). */ pollInterval?: number; }; diff --git a/packages/assets-controller/src/data-sources/evm-rpc-services/services/StakedBalanceFetcher.ts b/packages/assets-controller/src/data-sources/evm-rpc-services/services/StakedBalanceFetcher.ts index 4ccce33fb8c..4068237ba99 100644 --- a/packages/assets-controller/src/data-sources/evm-rpc-services/services/StakedBalanceFetcher.ts +++ b/packages/assets-controller/src/data-sources/evm-rpc-services/services/StakedBalanceFetcher.ts @@ -70,13 +70,13 @@ export const STAKING_INTERFACE = new Interface(STAKING_CONTRACT_ABI); const STAKING_DECIMALS = 18; export type StakedBalanceFetcherConfig = { - /** Polling interval in ms (default: 180s) */ + /** Polling interval in ms (default: 1 hour) */ pollingInterval?: number; /** Returns the network provider for the given chain. Required for fetchStakedBalance. */ getNetworkProvider?: (chainId: ChainId) => Web3Provider | undefined; }; -const DEFAULT_STAKED_BALANCE_INTERVAL = 180_000; // 3 minutes +const DEFAULT_STAKED_BALANCE_INTERVAL = 3_600_000; // 1 hour export class StakedBalanceFetcher extends StaticIntervalPollingControllerOnly() { readonly #providerGetter?: (chainId: ChainId) => Web3Provider | undefined; diff --git a/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.accounts-api-v6.integration.test.ts b/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.accounts-api-v6.integration.test.ts index db46373cb3a..397f85cfd03 100644 --- a/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.accounts-api-v6.integration.test.ts +++ b/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.accounts-api-v6.integration.test.ts @@ -27,7 +27,6 @@ import { withZeroedTimestamps } from '../__fixtures__/test-utils.js'; import { AccountsApiDataSource } from '../data-sources/AccountsApiDataSource.js'; import { PriceDataSource } from '../data-sources/PriceDataSource.js'; import { RpcDataSource } from '../data-sources/RpcDataSource.js'; -import { StakedBalanceDataSource } from '../data-sources/StakedBalanceDataSource.js'; import { TokenDataSource } from '../data-sources/TokenDataSource.js'; import { CustomAssetGraduationMiddleware } from '../middlewares/CustomAssetGraduationMiddleware.js'; import { DetectionMiddleware } from '../middlewares/DetectionMiddleware.js'; @@ -115,11 +114,6 @@ async function runPipeline( isBalanceV6Enabled: (): boolean => true, }); - const stakedBalanceDataSource = new StakedBalanceDataSource({ - messenger: assetsControllerMessenger, - onActiveChainsUpdated: jest.fn(), - }); - const rpcDataSource = new RpcDataSource({ messenger: assetsControllerMessenger, queryApiClient, @@ -174,7 +168,6 @@ async function runPipeline( const sources = buildFastFetchSources( { accountsApiDataSource, - stakedBalanceDataSource, customAssetGraduationMiddleware: new CustomAssetGraduationMiddleware({ getSelectedAccountId: (): AccountId => BSC_SPAM_ACCOUNT_ID, removeCustomAsset: (): void => { @@ -207,7 +200,6 @@ async function runPipeline( }); accountsApiDataSource.destroy(); - stakedBalanceDataSource.destroy(); rpcDataSource.destroy(); queryApiClient.clear(); diff --git a/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.integration.test.ts b/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.integration.test.ts index a682e968559..9eda3c89b23 100644 --- a/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.integration.test.ts +++ b/packages/assets-controller/src/pipeline/buildFastFetchSources.bsc-spam-token-filtering.integration.test.ts @@ -25,7 +25,6 @@ import { createTestApiClient } from '../__fixtures__/mockTokenApi.js'; import { AccountsApiDataSource } from '../data-sources/AccountsApiDataSource.js'; import { PriceDataSource } from '../data-sources/PriceDataSource.js'; import { RpcDataSource } from '../data-sources/RpcDataSource.js'; -import { StakedBalanceDataSource } from '../data-sources/StakedBalanceDataSource.js'; import { TokenDataSource } from '../data-sources/TokenDataSource.js'; import { CustomAssetGraduationMiddleware } from '../middlewares/CustomAssetGraduationMiddleware.js'; import { DetectionMiddleware } from '../middlewares/DetectionMiddleware.js'; @@ -104,11 +103,6 @@ async function runPipeline( }), }); - const stakedBalanceDataSource = new StakedBalanceDataSource({ - messenger: assetsControllerMessenger, - onActiveChainsUpdated: jest.fn(), - }); - const rpcDataSource = new RpcDataSource({ messenger: assetsControllerMessenger, queryApiClient, @@ -160,7 +154,6 @@ async function runPipeline( const sources = buildFastFetchSources( { accountsApiDataSource, - stakedBalanceDataSource, customAssetGraduationMiddleware: new CustomAssetGraduationMiddleware({ getSelectedAccountId: (): AccountId => BSC_SPAM_ACCOUNT_ID, removeCustomAsset: (): void => { @@ -189,7 +182,6 @@ async function runPipeline( }); accountsApiDataSource.destroy(); - stakedBalanceDataSource.destroy(); rpcDataSource.destroy(); queryApiClient.clear(); diff --git a/packages/assets-controller/src/pipeline/buildFastFetchSources.test.ts b/packages/assets-controller/src/pipeline/buildFastFetchSources.test.ts index 56c23068db8..c01fef711b4 100644 --- a/packages/assets-controller/src/pipeline/buildFastFetchSources.test.ts +++ b/packages/assets-controller/src/pipeline/buildFastFetchSources.test.ts @@ -1,5 +1,4 @@ -import type { BalanceSource } from '../middlewares/ParallelMiddleware.js'; -import type { AssetsDataSource, ChainId, Middleware } from '../types.js'; +import type { AssetsDataSource } from '../types.js'; import { buildFastFetchSources } from './buildFastFetchSources.js'; import type { FastFetchSources } from './buildFastFetchSources.js'; @@ -10,17 +9,9 @@ function stubSource(name: string): AssetsDataSource { }; } -function stubBalanceSource(name: string): BalanceSource { - return { - ...stubSource(name), - getActiveChainsSync: (): ChainId[] => [], - }; -} - function buildSources(): FastFetchSources { return { - accountsApiDataSource: stubBalanceSource('AccountsApiDataSource'), - stakedBalanceDataSource: stubBalanceSource('StakedBalanceDataSource'), + accountsApiDataSource: stubSource('AccountsApiDataSource'), customAssetGraduationMiddleware: stubSource( 'CustomAssetGraduationMiddleware', ), @@ -39,7 +30,7 @@ describe('buildFastFetchSources', () => { isBasicFunctionality: true, includeCustomAssetGraduation: true, expected: [ - 'ParallelBalanceMiddleware', + 'AccountsApiDataSource', 'CustomAssetGraduationMiddleware', 'RpcFallbackMiddleware', 'DetectionMiddleware', @@ -52,18 +43,19 @@ describe('buildFastFetchSources', () => { // The Accounts API v6 lane resolves pins through `includeAssetIds`. includeCustomAssetGraduation: false, expected: [ - 'ParallelBalanceMiddleware', + 'AccountsApiDataSource', 'RpcFallbackMiddleware', 'DetectionMiddleware', 'ParallelMiddleware', ], }, { - title: 'runs only the staking balance and detection', + title: 'runs nothing', isBasicFunctionality: false, includeCustomAssetGraduation: true, - // No network-backed source may run when the user has opted out. - expected: ['StakedBalanceDataSource', 'DetectionMiddleware'], + // No balance source runs, so detection has nothing to scan and no + // token or price source to feed. The RPC background lane covers this. + expected: [], }, ])( '$title', diff --git a/packages/assets-controller/src/pipeline/buildFastFetchSources.ts b/packages/assets-controller/src/pipeline/buildFastFetchSources.ts index 8b5f59ed643..dee8259266a 100644 --- a/packages/assets-controller/src/pipeline/buildFastFetchSources.ts +++ b/packages/assets-controller/src/pipeline/buildFastFetchSources.ts @@ -1,8 +1,4 @@ -import { - createParallelBalanceMiddleware, - createParallelMiddleware, -} from '../middlewares/ParallelMiddleware.js'; -import type { BalanceSource } from '../middlewares/ParallelMiddleware.js'; +import { createParallelMiddleware } from '../middlewares/ParallelMiddleware.js'; import type { AssetsDataSource } from '../types.js'; /** @@ -13,8 +9,7 @@ import type { AssetsDataSource } from '../types.js'; * actually run, not which are supplied. */ export type FastFetchSources = { - accountsApiDataSource: BalanceSource; - stakedBalanceDataSource: BalanceSource; + accountsApiDataSource: AssetsDataSource; customAssetGraduationMiddleware: AssetsDataSource; rpcFallbackMiddleware: AssetsDataSource; detectionMiddleware: AssetsDataSource; @@ -23,7 +18,7 @@ export type FastFetchSources = { }; /** - * Compose the fast fetch lane: balances in parallel → custom-asset graduation → + * Compose the fast fetch lane: Accounts API balances → custom-asset graduation → * RPC fallback → detection → token metadata and prices in parallel. * * Snap and RPC balance sources are deliberately absent — the controller runs @@ -38,8 +33,10 @@ export type FastFetchSources = { * * @param sources - The sources to place into the lane. * @param options - Lane options. - * @param options.isBasicFunctionality - When false, only the staking balance and - * detection run; no network-backed source is used. + * @param options.isBasicFunctionality - When false, the lane is empty. No + * balance source runs here, so detection would have nothing to scan and no + * token or price source to feed. The RPC background lane fetches balances and + * runs detection after them. * @param options.includeCustomAssetGraduation - `true` on the Accounts API v5 * lane. The v6 lane never graduates custom assets: it sends the pins to the * endpoint as `includeAssetIds`, and a chain that comes back without all of @@ -55,7 +52,6 @@ export function buildFastFetchSources( ): AssetsDataSource[] { const { accountsApiDataSource, - stakedBalanceDataSource, customAssetGraduationMiddleware, rpcFallbackMiddleware, detectionMiddleware, @@ -64,14 +60,11 @@ export function buildFastFetchSources( } = sources; if (!options.isBasicFunctionality) { - return [stakedBalanceDataSource, detectionMiddleware]; + return []; } return [ - createParallelBalanceMiddleware([ - accountsApiDataSource, - stakedBalanceDataSource, - ]), + accountsApiDataSource, ...(options.includeCustomAssetGraduation ? [customAssetGraduationMiddleware] : []), From 3002511624faef7f33f0f8d8c474b51f141688f1 Mon Sep 17 00:00:00 2001 From: Kriys94 Date: Thu, 15 Oct 2026 18:22:36 +0200 Subject: [PATCH 2/2] fix v5 fallback --- packages/assets-controller/CHANGELOG.md | 4 ++ .../AccountsApiDataSource.test.ts | 39 +++++++++++++++++++ .../src/data-sources/AccountsApiDataSource.ts | 7 ++++ 3 files changed, 50 insertions(+) diff --git a/packages/assets-controller/CHANGELOG.md b/packages/assets-controller/CHANGELOG.md index 5490d4b683a..d38456d5b1d 100644 --- a/packages/assets-controller/CHANGELOG.md +++ b/packages/assets-controller/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Poll staked balances every hour instead of every 3 minutes. Subscribe still fetches immediately, and a confirmed transaction that touches the staking contract still refreshes the balance right away ([#10782](https://github.com/MetaMask/core/pull/10782)) - Remove staked balances from the fast asset pipeline ([#10782](https://github.com/MetaMask/core/pull/10782)) +### Fixed + +- Copy Accounts API v5 fetch errors onto the asset pipeline context so unprocessed and timed-out chains are retried by RPC fallback and the slow pipeline ([#10782](https://github.com/MetaMask/core/pull/10782)) + ## [19.0.0] ### Added diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts index 58dcdd8cac9..f50d358c2ab 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.test.ts @@ -1753,6 +1753,45 @@ describe('AccountsApiDataSource', () => { controller.destroy(); }); + it('middleware copies v5 unprocessed-chain errors onto the pipeline context', async () => { + const { controller } = await setupController({ + unprocessedNetworks: ['eip155:1'], + }); + + const next = jest.fn().mockResolvedValue(undefined); + const context = createMiddlewareContext({ + response: { errors: { [CHAIN_POLYGON]: 'existing' } }, + }); + + await controller.assetsMiddleware(context, next); + + expect(context.response.errors?.[CHAIN_MAINNET]).toBe( + 'Unprocessed by Accounts API', + ); + expect(context.response.errors?.[CHAIN_POLYGON]).toBe('existing'); + + controller.destroy(); + }); + + it('middleware copies timed-out v5 chains onto the pipeline context', async () => { + const { controller, apiClient } = await setupController({ + fetchTimeoutMs: 10, + }); + + apiClient.accounts.fetchV5MultiAccountBalances.mockImplementationOnce( + () => new Promise(() => undefined), + ); + + const next = jest.fn().mockResolvedValue(undefined); + const context = createMiddlewareContext(); + + await controller.assetsMiddleware(context, next); + + expect(context.response.errors?.[CHAIN_MAINNET]).toContain('timed out'); + + controller.destroy(); + }); + it('middleware forwards full updateMode from v6 fetches', async () => { const { controller } = await setupController({ remoteFeatureFlags: { assetsAccountsApiV6: true }, diff --git a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts index c4fd7363ee8..1894998a418 100644 --- a/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts +++ b/packages/assets-controller/src/data-sources/AccountsApiDataSource.ts @@ -965,6 +965,13 @@ export class AccountsApiDataSource extends AbstractDataSource< }; } } + + if (response.errors && Object.keys(response.errors).length > 0) { + context.response.errors = { + ...context.response.errors, + ...response.errors, + }; + } } catch (error) { log('Middleware fetch failed', { error }); }