Skip to content

feat: add status tracking to RampsController ResourceState#8116

Open
AxelGes wants to merge 17 commits intomainfrom
feat/ramps-controller-resource-status
Open

feat: add status tracking to RampsController ResourceState#8116
AxelGes wants to merge 17 commits intomainfrom
feat/ramps-controller-resource-status

Conversation

@AxelGes
Copy link
Contributor

@AxelGes AxelGes commented Mar 4, 2026

Explanation

This PR adds explicit status tracking to the ResourceState type in @metamask/ramps-controller. Previously, the controller used only isLoading and error fields, making it impossible to distinguish between:

  1. An uninitialized resource (never fetched)
  2. A successfully fetched resource with no data

This ambiguity caused false positive "token unavailable" errors in MetaMask Mobile when payment methods were still loading.

Changes

RampsController.ts

  • Added status: \${RequestStatus}`field toResourceState` type
  • Updated createDefaultResourceState to initialize with status: RequestStatus.IDLE
  • Added private #setResourceStatus method to update resource status
  • Wired status transitions in executeRequest:
    • loading when fetch starts
    • success when fetch completes successfully
    • error when fetch fails

The status field uses the existing RequestStatus enum from RequestCache.ts ('idle' | 'loading' | 'success' | 'error'), already exported in the package's public API.

References

Checklist

  • Tests pass
  • Linter passes
  • No breaking changes to existing controller consumers (status is optional via migration)

Note

Medium Risk
Moderate risk because it changes the persisted/public controller state shape and refactors request lifecycle logic around concurrency/abort paths, which could affect UI state transitions if incorrect.

Overview
Adds a status field to ResourceState (initialized to idle) so consumers can distinguish never fetched from fetched successfully with empty data, and wires this through default state creation and resource resets.

Updates executeRequest to set resource status to loading on cache misses and to commit success/error when the last in-flight request completes, using a new per-resource terminal-status tracker to avoid stale concurrent requests regressing status (and clearing this tracker on region-change dependent-request aborts). Transak resource fetch helpers are updated to set loading/success/error statuses, and tests are expanded/updated (including getWidgetUrl returning null for undefined URLs but preserving empty strings, plus new provider mocks for order polling tests).

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

Add explicit status field to ResourceState to distinguish between uninitialized and empty-fetched states. The default idle/loading/success/error statuses are now tracked per resource and set explicitly via #setResourceStatus during fetch cycles in executeRequest.

This eliminates ambiguity when determining if a resource has been fetched vs is still in its initial state, fixing false positive "token unavailable" errors in mobile when payment methods are actually still loading.
AxelGes added a commit to MetaMask/metamask-mobile that referenced this pull request Mar 4, 2026
…ble errors

Replace local ref-based loading tracking with paymentMethodsStatus from RampsController. The controller now exposes explicit 'idle'/'loading'/'success'/'error' status per resource, eliminating the need for component-level workarounds.

Changes:
- Expose status from useRampsPaymentMethods and thread through useRampsController
- Replace hasSeenLoadingRef/prevProviderIdRef/prevSettledAssetIdRef with direct paymentMethodsStatus === RequestStatus.SUCCESS check
- Add migration 124 to backfill status: 'idle' on persisted RampsController state
- Make selectors tolerant of missing status field for backward compatibility
- Update all tests to mock paymentMethodsStatus

Depends on: MetaMask/core#8116
- Add missing status field reset in resetResource function to fix bugbot issue
- Update test helpers (createResourceState, createDefaultResourceState) to include status field
- Add createMockProvider helper to generate complete Provider objects with all required fields
- Fix test mocks to use proper Provider type instead of partial objects
- Update changelog with new status field feature
@AxelGes AxelGes requested a review from a team as a code owner March 5, 2026 00:14
- Extend #updateResourceField to accept 'status' field, removing the
  duplicate update logic in #setResourceStatus
- Move SUCCESS/ERROR status transitions to the finally block behind the
  same ref-count guard (next === 0) used by isLoading, so status and
  isLoading never disagree during concurrent requests
AxelGes added 2 commits March 4, 2026 21:47
… branch coverage

BuyWidget.url is typed as string (non-optional), so the ?? null was dead
code that prevented 100% branch coverage.
…idle inconsistency

When isResultCurrent() returns false, terminalStatus was never assigned, leaving
status stuck at 'loading' while isLoading was set back to false. Fall back to
RequestStatus.IDLE so the two fields always stay in sync.
…nsistent states

Replace separate #setResourceLoading and #setResourceStatus calls with a single
#setResourceLoadingAndStatus that writes both fields inside one this.update() call,
so subscribers never observe a snapshot where isLoading and status disagree.
@AxelGes
Copy link
Contributor Author

AxelGes commented Mar 5, 2026

@metamaskbot release-preview

@AxelGes
Copy link
Contributor Author

AxelGes commented Mar 5, 2026

@metamaskbot publish-preview

@github-actions
Copy link
Contributor

github-actions bot commented Mar 5, 2026

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": "4.1.1-preview-93519965c",
  "@metamask-previews/accounts-controller": "36.0.1-preview-93519965c",
  "@metamask-previews/address-book-controller": "7.0.1-preview-93519965c",
  "@metamask-previews/ai-controllers": "0.1.0-preview-93519965c",
  "@metamask-previews/analytics-controller": "1.0.0-preview-93519965c",
  "@metamask-previews/analytics-data-regulation-controller": "0.0.0-preview-93519965c",
  "@metamask-previews/announcement-controller": "8.0.0-preview-93519965c",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-93519965c",
  "@metamask-previews/approval-controller": "8.0.0-preview-93519965c",
  "@metamask-previews/assets-controller": "2.2.0-preview-93519965c",
  "@metamask-previews/assets-controllers": "100.1.0-preview-93519965c",
  "@metamask-previews/base-controller": "9.0.0-preview-93519965c",
  "@metamask-previews/base-data-service": "0.0.0-preview-93519965c",
  "@metamask-previews/bridge-controller": "68.0.0-preview-93519965c",
  "@metamask-previews/bridge-status-controller": "68.0.0-preview-93519965c",
  "@metamask-previews/build-utils": "3.0.4-preview-93519965c",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-93519965c",
  "@metamask-previews/claims-controller": "0.4.2-preview-93519965c",
  "@metamask-previews/client-controller": "1.0.0-preview-93519965c",
  "@metamask-previews/compliance-controller": "1.0.1-preview-93519965c",
  "@metamask-previews/composable-controller": "12.0.0-preview-93519965c",
  "@metamask-previews/config-registry-controller": "0.0.0-preview-93519965c",
  "@metamask-previews/connectivity-controller": "0.1.0-preview-93519965c",
  "@metamask-previews/controller-utils": "11.19.0-preview-93519965c",
  "@metamask-previews/core-backend": "6.0.0-preview-93519965c",
  "@metamask-previews/delegation-controller": "2.0.1-preview-93519965c",
  "@metamask-previews/earn-controller": "11.1.1-preview-93519965c",
  "@metamask-previews/eip-5792-middleware": "3.0.0-preview-93519965c",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-93519965c",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-93519965c",
  "@metamask-previews/ens-controller": "19.0.3-preview-93519965c",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-93519965c",
  "@metamask-previews/eth-block-tracker": "15.0.1-preview-93519965c",
  "@metamask-previews/eth-json-rpc-middleware": "23.1.0-preview-93519965c",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-93519965c",
  "@metamask-previews/foundryup": "1.0.1-preview-93519965c",
  "@metamask-previews/gas-fee-controller": "26.0.3-preview-93519965c",
  "@metamask-previews/gator-permissions-controller": "2.0.0-preview-93519965c",
  "@metamask-previews/geolocation-controller": "0.1.0-preview-93519965c",
  "@metamask-previews/json-rpc-engine": "10.2.3-preview-93519965c",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-93519965c",
  "@metamask-previews/keyring-controller": "25.1.0-preview-93519965c",
  "@metamask-previews/logging-controller": "7.0.1-preview-93519965c",
  "@metamask-previews/message-manager": "14.1.0-preview-93519965c",
  "@metamask-previews/messenger": "0.3.0-preview-93519965c",
  "@metamask-previews/multichain-account-service": "7.0.0-preview-93519965c",
  "@metamask-previews/multichain-api-middleware": "1.2.7-preview-93519965c",
  "@metamask-previews/multichain-network-controller": "3.0.4-preview-93519965c",
  "@metamask-previews/multichain-transactions-controller": "7.0.1-preview-93519965c",
  "@metamask-previews/name-controller": "9.0.0-preview-93519965c",
  "@metamask-previews/network-controller": "30.0.0-preview-93519965c",
  "@metamask-previews/network-enablement-controller": "4.2.0-preview-93519965c",
  "@metamask-previews/notification-services-controller": "22.0.0-preview-93519965c",
  "@metamask-previews/permission-controller": "12.2.0-preview-93519965c",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-93519965c",
  "@metamask-previews/perps-controller": "1.0.0-preview-93519965c",
  "@metamask-previews/phishing-controller": "16.3.0-preview-93519965c",
  "@metamask-previews/polling-controller": "16.0.3-preview-93519965c",
  "@metamask-previews/preferences-controller": "22.1.0-preview-93519965c",
  "@metamask-previews/profile-metrics-controller": "3.0.1-preview-93519965c",
  "@metamask-previews/profile-sync-controller": "27.1.0-preview-93519965c",
  "@metamask-previews/ramps-controller": "10.2.0-preview-93519965c",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-93519965c",
  "@metamask-previews/remote-feature-flag-controller": "4.1.0-preview-93519965c",
  "@metamask-previews/sample-controllers": "4.0.3-preview-93519965c",
  "@metamask-previews/seedless-onboarding-controller": "8.1.0-preview-93519965c",
  "@metamask-previews/selected-network-controller": "26.0.3-preview-93519965c",
  "@metamask-previews/shield-controller": "5.0.1-preview-93519965c",
  "@metamask-previews/signature-controller": "39.0.4-preview-93519965c",
  "@metamask-previews/storage-service": "1.0.0-preview-93519965c",
  "@metamask-previews/subscription-controller": "6.0.0-preview-93519965c",
  "@metamask-previews/transaction-controller": "62.20.0-preview-93519965c",
  "@metamask-previews/transaction-pay-controller": "16.2.0-preview-93519965c",
  "@metamask-previews/user-operation-controller": "41.0.3-preview-93519965c"
}

@AxelGes AxelGes enabled auto-merge March 5, 2026 14:28
AxelGes added 2 commits March 5, 2026 12:42
…tructor

Persisted resource states (countries, providers, tokens) pre-dating the
status field would come back from storage without it, leaving status
undefined and defeating the idle/loading/success/error distinction.

Deep-merge each persisted resource state with defaults in the constructor
so that any missing fields are backfilled, while explicit values
(including null, for the existing error-path test) are preserved.
The test title said "returns null" but the assertion checked for an empty
string. The ?? operator does not coalesce empty strings, so the function
correctly returns '' — the title was wrong, not the assertion.
…buyQuote, kycRequirement)

The status field was only wired for top-level resources via executeRequest
but native provider resources directly mutated isLoading/error without ever
updating status, leaving it permanently 'idle'. Also backfill native provider
and paymentMethods resource states in the constructor for persisted state
migration.
amitabh94
amitabh94 previously approved these changes Mar 5, 2026
…s to idle

When multiple concurrent requests share a resourceType but different cache
keys, the last-to-finish request now correctly preserves any terminal status
(success/error) recorded by earlier current results rather than falling back
to IDLE. Introduces #resourceTerminalStatus map to track the best status
across all in-flight requests for a resource, cleared after the final request
commits the status to state.
AxelGes and others added 2 commits March 5, 2026 21:10
…atus

Persisted-state migration is a consumer responsibility (handled by
migration 125 in metamask-mobile). The controller has no migration
runner and should not paper over missing fields — that's the wrong
layer for this concern.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

… region change

Without this fix, a successful request recording SUCCESS into
#resourceTerminalStatus before a region change aborts concurrent in-flight
requests would cause that stale SUCCESS to persist. The next request batch
for the same resource would then inherit it, preventing ERROR from being
recorded even when the new request failed.
if (next === 0) {
this.#pendingResourceCount.delete(resourceType);
this.#setResourceLoading(resourceType, false);
// Use the best terminal status recorded by any current result across
Copy link
Contributor

Choose a reason for hiding this comment

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

I know cursor commented on this but I wanted to understand if there would actually be a case where there will be concurrent requests and we need this ? My concern is that we may be over engineering for this case here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants