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"
}

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