Skip to content

Conversation

@salimtb
Copy link
Contributor

@salimtb salimtb commented Nov 10, 2025

Explanation

What is the current state and why does it need to change?

Currently, when TokenDetectionController uses the Accounts API to detect tokens, API calls can hang indefinitely if the API is slow, unresponsive, or experiencing network issues. This causes the entire token detection process to freeze without any fallback mechanism, resulting in a poor user experience where tokens are never detected.

What is the solution and how does it work?

This PR adds 30-second timeout protection for Accounts API calls in TokenDetectionController:

  • Added ACCOUNTS_API_TIMEOUT_MS constant (30000ms) to define the timeout threshold

  • Wrapped Accounts API calls with Promise.race() between the actual API call and a timeout promise

  • When timeout occurs, the promise rejects and is caught, triggering an automatic fallback to RPC-based token detection

  • Properly cleans up the timeout in a finally block to prevent memory leaks

  • Includes error logging for debugging timeout and failure events

The timeout mechanism ensures that:

  1. If the API responds within 30 seconds, detection proceeds normally via the API

  2. If the API takes longer than 30 seconds, the timeout fires and RPC detection takes over

  3. Users always get token detection results, either via API or RPC fallback

Changes that might not be obvious

The timeout is implemented in the #attemptAccountAPIDetection private method, which wraps the existing #addDetectedTokensViaAPI call. This ensures the timeout protection applies to all Accounts API token detection flows without requiring changes to the core detection logic.

References

  • Improves reliability of token detection by preventing indefinite hangs

  • Ensures users always get token detection results through automatic RPC fallback

  • Related to ongoing work to improve Accounts API reliability and user experience

Checklist

  • I've updated the test suite for new or updated code as appropriate

    • Added comprehensive timeout test using fake timers to simulate 30-second timeout scenario

    • Test verifies that API is called, timeout triggers, RPC fallback occurs, and tokens are successfully added

  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate

    • Added inline comments explaining timeout mechanism and cleanup
  • I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary

    • Updated CHANGELOG.md with timeout protection fix
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

    • Not applicable: This is an internal improvement with no breaking changes

Technical Implementation Details

Key Files Changed

  1. TokenDetectionController.ts

    • Added ACCOUNTS_API_TIMEOUT_MS constant (30000ms)
    • Modified #attemptAccountAPIDetection method to implement timeout protection
    • Uses Promise.race() to race between API call and timeout promise
    • Includes proper cleanup in finally block to prevent memory leaks
  2. TokenDetectionController.test.ts

    • Added test case: 'should timeout and fallback to RPC when Accounts API call takes longer than 30 seconds'
    • Uses sinon.useFakeTimers() to simulate time advancement
    • Verifies complete flow: API call → timeout → RPC fallback → token addition

Code Flow

detectTokens()
  ↓
#attemptAccountAPIDetection()
  ↓
Promise.race([
  #addDetectedTokensViaAPI() ← actual API call
  timeout promise (30s)      ← timeout protection
])
  ↓
Success → continue with API results
  ↓
Timeout/Failure → fallback to RPC detection
  ↓
finally → cleanup timeout

Testing Strategy

The test uses fake timers to simulate the 30-second timeout without actually waiting 30 seconds:

  1. Mock API call to never resolve (simulates hanging request)
  2. Start detection process
  3. Advance fake timers by 30 seconds
  4. Verify timeout triggered
  5. Verify RPC fallback occurred
  6. Verify tokens were successfully added

Impact

User Experience

  • Before: Token detection could hang indefinitely, leaving users without their tokens
  • After: Token detection always completes within 30 seconds, with automatic RPC fallback

Performance

  • No performance impact on successful API calls (they complete normally)
  • Failed/slow API calls are cut off at 30 seconds instead of hanging forever
  • Memory leak prevention through proper timeout cleanup

Reliability

  • Significantly improves reliability of token detection
  • Provides graceful degradation when Accounts API is experiencing issues
  • Maintains existing functionality while adding safety net

Note

Adds a 30s timeout to Accounts API token detection with RPC fallback, and refactors balance fetchers to return unprocessedChainIds so controllers can retry unsupported chains via RPC.

  • Token Detection (TokenDetectionController):
    • Add ACCOUNTS_API_TIMEOUT_MS (30s) and wrap API calls with safelyExecuteWithTimeout; on timeout/failure, fall back to RPC.
    • Process Accounts API unprocessedNetworks and add those chains to RPC detection.
  • Balance Fetchers:
    • Change fetch() to return { balances, unprocessedChainIds } in AccountsApiBalanceFetcher and RpcBalanceFetcher.
    • Convert Accounts API unprocessedNetworks (decimal) to hex unprocessedChainIds; aggregate across batches.
    • For AccountTracker RPC fetcher wrapper, filter out staked entries when includeStakedAssets is false while preserving unprocessedChainIds.
  • Controllers:
    • Update AccountTrackerController and TokenBalancesController to consume new fetcher result shape and re-queue unprocessedChainIds for fallback fetchers.
  • Tests & Misc:
    • Update tests across fetchers/controllers for new return type and timeout/fallback behavior; add coverage for missing currencies in CurrencyRateController responses.
    • Update CHANGELOG.md to reflect timeout protection and unprocessed network handling.

Written by Cursor Bugbot for commit 7a813ba. This will update automatically on new commits. Configure here.

@salimtb salimtb changed the title Fix/force timeout after 30s token detection fix: force timeout after 30s token detection Nov 10, 2025
@salimtb
Copy link
Contributor Author

salimtb commented Nov 10, 2025

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "3.0.0-preview-d4351f75",
  "@metamask-previews/accounts-controller": "34.0.0-preview-d4351f75",
  "@metamask-previews/address-book-controller": "7.0.0-preview-d4351f75",
  "@metamask-previews/analytics-controller": "0.0.0-preview-d4351f75",
  "@metamask-previews/announcement-controller": "8.0.0-preview-d4351f75",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-d4351f75",
  "@metamask-previews/approval-controller": "8.0.0-preview-d4351f75",
  "@metamask-previews/assets-controllers": "88.0.0-preview-d4351f75",
  "@metamask-previews/base-controller": "9.0.0-preview-d4351f75",
  "@metamask-previews/bridge-controller": "60.0.0-preview-d4351f75",
  "@metamask-previews/bridge-status-controller": "60.0.0-preview-d4351f75",
  "@metamask-previews/build-utils": "3.0.4-preview-d4351f75",
  "@metamask-previews/chain-agnostic-permission": "1.2.2-preview-d4351f75",
  "@metamask-previews/claims-controller": "0.1.0-preview-d4351f75",
  "@metamask-previews/composable-controller": "12.0.0-preview-d4351f75",
  "@metamask-previews/controller-utils": "11.15.0-preview-d4351f75",
  "@metamask-previews/core-backend": "4.0.0-preview-d4351f75",
  "@metamask-previews/delegation-controller": "1.0.0-preview-d4351f75",
  "@metamask-previews/earn-controller": "10.0.0-preview-d4351f75",
  "@metamask-previews/eip-5792-middleware": "2.0.0-preview-d4351f75",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-d4351f75",
  "@metamask-previews/eip1193-permission-middleware": "1.0.2-preview-d4351f75",
  "@metamask-previews/ens-controller": "18.0.0-preview-d4351f75",
  "@metamask-previews/error-reporting-service": "3.0.0-preview-d4351f75",
  "@metamask-previews/eth-block-tracker": "14.0.0-preview-d4351f75",
  "@metamask-previews/eth-json-rpc-middleware": "21.0.0-preview-d4351f75",
  "@metamask-previews/eth-json-rpc-provider": "5.0.1-preview-d4351f75",
  "@metamask-previews/foundryup": "1.0.1-preview-d4351f75",
  "@metamask-previews/gas-fee-controller": "25.0.0-preview-d4351f75",
  "@metamask-previews/gator-permissions-controller": "0.4.0-preview-d4351f75",
  "@metamask-previews/json-rpc-engine": "10.1.1-preview-d4351f75",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-d4351f75",
  "@metamask-previews/keyring-controller": "24.0.0-preview-d4351f75",
  "@metamask-previews/logging-controller": "7.0.0-preview-d4351f75",
  "@metamask-previews/message-manager": "14.0.0-preview-d4351f75",
  "@metamask-previews/messenger": "0.3.0-preview-d4351f75",
  "@metamask-previews/multichain-account-service": "3.0.0-preview-d4351f75",
  "@metamask-previews/multichain-api-middleware": "1.2.4-preview-d4351f75",
  "@metamask-previews/multichain-network-controller": "2.0.0-preview-d4351f75",
  "@metamask-previews/multichain-transactions-controller": "6.0.0-preview-d4351f75",
  "@metamask-previews/name-controller": "9.0.0-preview-d4351f75",
  "@metamask-previews/network-controller": "25.0.0-preview-d4351f75",
  "@metamask-previews/network-enablement-controller": "3.1.0-preview-d4351f75",
  "@metamask-previews/notification-services-controller": "19.0.0-preview-d4351f75",
  "@metamask-previews/permission-controller": "12.1.0-preview-d4351f75",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-d4351f75",
  "@metamask-previews/phishing-controller": "15.0.0-preview-d4351f75",
  "@metamask-previews/polling-controller": "15.0.0-preview-d4351f75",
  "@metamask-previews/preferences-controller": "21.0.0-preview-d4351f75",
  "@metamask-previews/profile-sync-controller": "26.0.0-preview-d4351f75",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-d4351f75",
  "@metamask-previews/remote-feature-flag-controller": "2.0.0-preview-d4351f75",
  "@metamask-previews/sample-controllers": "3.0.0-preview-d4351f75",
  "@metamask-previews/seedless-onboarding-controller": "6.1.0-preview-d4351f75",
  "@metamask-previews/selected-network-controller": "25.0.0-preview-d4351f75",
  "@metamask-previews/shield-controller": "2.0.0-preview-d4351f75",
  "@metamask-previews/signature-controller": "36.0.0-preview-d4351f75",
  "@metamask-previews/subscription-controller": "3.3.0-preview-d4351f75",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-d4351f75",
  "@metamask-previews/transaction-controller": "61.1.0-preview-d4351f75",
  "@metamask-previews/transaction-pay-controller": "4.0.0-preview-d4351f75",
  "@metamask-previews/user-operation-controller": "40.0.0-preview-d4351f75"
}

@salimtb salimtb marked this pull request as ready for review November 12, 2025 07:57
@salimtb salimtb requested review from a team as code owners November 12, 2025 07:57
supportedNetworks,
});

return await Promise.race([apiCallPromise, timeoutPromise]);
Copy link
Contributor

@Prithpal-Sooriya Prithpal-Sooriya Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm can we reuse our existing utils? E.g.

  async #attemptAccountAPIDetection(
    chainsToDetectUsingAccountAPI: Hex[],
    addressToDetect: string,
    supportedNetworks: number[] | null,
  ) {
    // from controller-utils
    const result = await safelyExecuteWithTimeout(
      async () => {
        return this.#addDetectedTokensViaAPI({
          chainIds: chainsToDetectUsingAccountAPI,
          selectedAddress: addressToDetect,
          supportedNetworks,
        });
      },
      false,
      ACCOUNTS_API_TIMEOUT_MS,
    );

    if (!result) {
      return { result: 'failed' };
    }

    return result;
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh true , comment addressed here e5b43d0

@salimtb
Copy link
Contributor Author

salimtb commented Nov 12, 2025

@metamaskbot publish-preview

@salimtb salimtb merged commit d405c7d into main Nov 12, 2025
271 checks passed
@salimtb salimtb deleted the fix/force-timeout-after-30s-token-detection branch November 12, 2025 11:51
@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "3.0.0-preview-7a813ba4",
  "@metamask-previews/accounts-controller": "34.0.0-preview-7a813ba4",
  "@metamask-previews/address-book-controller": "7.0.0-preview-7a813ba4",
  "@metamask-previews/analytics-controller": "0.0.0-preview-7a813ba4",
  "@metamask-previews/announcement-controller": "8.0.0-preview-7a813ba4",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-7a813ba4",
  "@metamask-previews/approval-controller": "8.0.0-preview-7a813ba4",
  "@metamask-previews/assets-controllers": "88.0.0-preview-7a813ba4",
  "@metamask-previews/base-controller": "9.0.0-preview-7a813ba4",
  "@metamask-previews/bridge-controller": "60.0.0-preview-7a813ba4",
  "@metamask-previews/bridge-status-controller": "60.0.0-preview-7a813ba4",
  "@metamask-previews/build-utils": "3.0.4-preview-7a813ba4",
  "@metamask-previews/chain-agnostic-permission": "1.2.2-preview-7a813ba4",
  "@metamask-previews/claims-controller": "0.2.0-preview-7a813ba4",
  "@metamask-previews/composable-controller": "12.0.0-preview-7a813ba4",
  "@metamask-previews/controller-utils": "11.15.0-preview-7a813ba4",
  "@metamask-previews/core-backend": "4.0.0-preview-7a813ba4",
  "@metamask-previews/delegation-controller": "1.0.0-preview-7a813ba4",
  "@metamask-previews/earn-controller": "10.0.0-preview-7a813ba4",
  "@metamask-previews/eip-5792-middleware": "2.0.0-preview-7a813ba4",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-7a813ba4",
  "@metamask-previews/eip1193-permission-middleware": "1.0.2-preview-7a813ba4",
  "@metamask-previews/ens-controller": "18.0.0-preview-7a813ba4",
  "@metamask-previews/error-reporting-service": "3.0.0-preview-7a813ba4",
  "@metamask-previews/eth-block-tracker": "14.0.0-preview-7a813ba4",
  "@metamask-previews/eth-json-rpc-middleware": "21.0.0-preview-7a813ba4",
  "@metamask-previews/eth-json-rpc-provider": "5.0.1-preview-7a813ba4",
  "@metamask-previews/foundryup": "1.0.1-preview-7a813ba4",
  "@metamask-previews/gas-fee-controller": "25.0.0-preview-7a813ba4",
  "@metamask-previews/gator-permissions-controller": "0.4.0-preview-7a813ba4",
  "@metamask-previews/json-rpc-engine": "10.1.1-preview-7a813ba4",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-7a813ba4",
  "@metamask-previews/keyring-controller": "24.0.0-preview-7a813ba4",
  "@metamask-previews/logging-controller": "7.0.0-preview-7a813ba4",
  "@metamask-previews/message-manager": "14.0.0-preview-7a813ba4",
  "@metamask-previews/messenger": "0.3.0-preview-7a813ba4",
  "@metamask-previews/multichain-account-service": "3.0.0-preview-7a813ba4",
  "@metamask-previews/multichain-api-middleware": "1.2.4-preview-7a813ba4",
  "@metamask-previews/multichain-network-controller": "2.0.0-preview-7a813ba4",
  "@metamask-previews/multichain-transactions-controller": "6.0.0-preview-7a813ba4",
  "@metamask-previews/name-controller": "9.0.0-preview-7a813ba4",
  "@metamask-previews/network-controller": "25.0.0-preview-7a813ba4",
  "@metamask-previews/network-enablement-controller": "3.1.0-preview-7a813ba4",
  "@metamask-previews/notification-services-controller": "19.0.0-preview-7a813ba4",
  "@metamask-previews/permission-controller": "12.1.0-preview-7a813ba4",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-7a813ba4",
  "@metamask-previews/phishing-controller": "15.0.0-preview-7a813ba4",
  "@metamask-previews/polling-controller": "15.0.0-preview-7a813ba4",
  "@metamask-previews/preferences-controller": "21.0.0-preview-7a813ba4",
  "@metamask-previews/profile-sync-controller": "26.0.0-preview-7a813ba4",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-7a813ba4",
  "@metamask-previews/remote-feature-flag-controller": "2.0.0-preview-7a813ba4",
  "@metamask-previews/sample-controllers": "3.0.0-preview-7a813ba4",
  "@metamask-previews/seedless-onboarding-controller": "6.1.0-preview-7a813ba4",
  "@metamask-previews/selected-network-controller": "25.0.0-preview-7a813ba4",
  "@metamask-previews/shield-controller": "2.0.0-preview-7a813ba4",
  "@metamask-previews/signature-controller": "36.0.0-preview-7a813ba4",
  "@metamask-previews/subscription-controller": "4.2.0-preview-7a813ba4",
  "@metamask-previews/token-search-discovery-controller": "4.0.0-preview-7a813ba4",
  "@metamask-previews/transaction-controller": "61.2.0-preview-7a813ba4",
  "@metamask-previews/transaction-pay-controller": "4.0.0-preview-7a813ba4",
  "@metamask-previews/user-operation-controller": "40.0.0-preview-7a813ba4"
}

salimtb added a commit that referenced this pull request Nov 14, 2025
…7155)

## Explanation

### Current State and Problem

Previously, when the Accounts API failed or timed out in
`TokenBalancesController`, the error was caught within
`AccountsApiBalanceFetcher` and converted into balance entries with
`success: false`. While this seemed like graceful error handling, it
created a critical issue:

1. The `TokenBalancesController` would receive these `success: false`
entries as valid results
2. It would mark those chains as "processed" and remove them from
`remainingChains`
3. The RPC fallback fetcher would never attempt to fetch balances for
those chains
4. **Result:** Users would get no balance data when the API failed,
instead of falling back to RPC

Additionally, there was no timeout protection, so slow or hanging API
calls could delay balance updates indefinitely.

### Solution

This PR enables proper RPC fallback by:

1. **Adding timeout protection:** Wraps API calls with
`safelyExecuteWithTimeout` (30-second timeout) to prevent hanging
requests
2. **Propagating errors:** Removes the try-catch block that was
swallowing API errors, allowing them to propagate to
`TokenBalancesController`
3. **Leveraging existing fallback logic:** `TokenBalancesController`
already has proper error handling (lines 687-692) that catches errors
and moves to the next fetcher
4. **Simplifying the flow:** Removed the `apiError` flag and related
logic for generating `success: false` entries

When the API fails or times out:
- `safelyExecuteWithTimeout` returns `undefined`
- We detect this and throw an error: `'Accounts API request timed out or
failed'`
- `TokenBalancesController` catches it and automatically falls back to
RPC
- RPC fetcher handles **both** native balances and staked balances

### Key Changes

**`api-balance-fetcher.ts`:**
- Import and use `safelyExecuteWithTimeout` with 30-second timeout
- Remove try-catch that prevented error propagation
- Remove `apiError` flag logic
- Remove code that generated `success: false` entries when API failed
- Throw error when `apiResponse` is `undefined`

**`api-balance-fetcher.test.ts`:**
- Updated 2 tests to expect error propagation instead of graceful
handling
- Tests now verify that errors are thrown with message: `'Accounts API
request timed out or failed'`

### Non-Obvious Changes

The original try-catch was attempting to fetch staked balances even when
the API failed. This is no longer necessary because:
- When errors propagate to `TokenBalancesController`, it falls back to
RPC
- The RPC fetcher handles **all** balance types (native + staked +
tokens)
- This results in a simpler, more reliable flow

## References

- Related to #7106 (similar timeout protection added to
`TokenDetectionController`)
- Part of ongoing improvements to balance fetching reliability and
fallback mechanisms

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] 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 (N/A - no breaking changes)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds 30s timeout and error propagation in `AccountsApiBalanceFetcher`
so `TokenBalancesController` falls back to RPC on Accounts API
failures/timeouts; updates tests and changelog.
> 
> - **Balances fetching (`AccountsApiBalanceFetcher`)**:
> - Add 30s timeout via `safelyExecuteWithTimeout` for Accounts API
requests.
> - Remove internal try/catch and `apiError` logic; propagate failures.
> - Throw `'Accounts API request timed out or failed'` when API
fails/returns undefined to trigger RPC fallback.
> - Always add zero native entries only when API succeeds but omits
them; remove error-entry generation.
> - Preserve handling of `unprocessedNetworks` -> `unprocessedChainIds`.
> - **Tests** (`api-balance-fetcher.test.ts`):
> - Update expectations to assert thrown error on API failure (for RPC
fallback).
> - Add coverage for batching accumulation, integer-only balances, and
timeout-failure path.
> - **Changelog**:
> - Document RPC fallback enablement, 30s timeout, and related fixes in
`TokenBalancesController`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0f83296. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants