Skip to content

feat: optimize Transaction Pay amount quote latency#9543

Draft
pedronfigueiredo wants to merge 3 commits into
mainfrom
pnf/money-account-quote-latency-poc
Draft

feat: optimize Transaction Pay amount quote latency#9543
pedronfigueiredo wants to merge 3 commits into
mainfrom
pnf/money-account-quote-latency-poc

Conversation

@pedronfigueiredo

@pedronfigueiredo pedronfigueiredo commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Explanation

Context

Updating the amount of a Money Account deposit currently fans one logical user action out through multiple nested transaction updates, parent calldata rebuilds, gas/simulation preparation, controller publications, and quote generations. In the reference iOS simulator trace, Update/Done to an executable quote took 8.503 seconds:

Segment Baseline
Update/Done → Relay request 3,393.546 ms
Relay request 4,107.940 ms
Relay response → executable quote 1,001.045 ms
Total 8,502.531 ms

Relay is a third-party request and its latency varies independently. The client-controlled portions are therefore the primary optimization target.

What this PR changes

This proof of concept adds generic controller foundations for an explicit, coherent amount-update-and-quote pipeline.

TransactionController

  • Adds beginAtomicBatchUpdate to apply required assets and all nested calldata updates in one canonical state publication.
  • Regenerates parent atomic-batch calldata once and starts one gas preparation generation.
  • Assigns a monotonic transaction revision and binds preparation results to that revision.
  • Prevents stale gas and simulation results from overwriting a newer revision.
  • Keeps the existing single-update API intact for existing consumers.

TransactionPayController

  • Adds updateAmount, backed by a consumer-provided prepareTransactionAmount callback, so transaction-specific amount conversion and calldata construction remain outside the generic controller.
  • Deduplicates identical in-flight intents and aborts/suppresses superseded generations.
  • Avoids the existing TransactionController listener starting a duplicate quote for the explicit update.
  • Carries revision-bound local preparation through quote generation and publishes only a quote matching the current transaction revision.
  • Separates raw Relay fetching from the preparation-aware normalization path so vendor waiting can overlap local work without publishing stale executable data.

The result is one coherent revision, one local preparation generation, and one quote generation for the standard path rather than intermediate revisions repeatedly triggering work.

Why start with Money Account deposits?

Money Account deposits are a useful first adopter because they expose the amplification clearly: an amount change updates both approval and deposit calldata, requires previewDeposit, and then feeds Transaction Pay and EIP-7702/delegation preparation. The flow is narrow enough to validate behind a Mobile feature flag while retaining the legacy path as a kill switch.

The APIs in this PR are intentionally not Money Account-specific. TransactionController accepts generic required assets and nested transaction updates, while TransactionPayController delegates transaction-specific preparation to an injected callback. Other Transaction Pay flows can adopt the same orchestration later once their patch completeness, strategy semantics, and freshness requirements are validated.

Benchmark result

A follow-up React Native DevTools trace used the same inferred Update/Done and executable-commit anchors:

Segment Baseline PoC Change
Update/Done → Relay request 3,393.546 ms 1,512.891 ms -55.4%
Relay request 4,107.940 ms 5,118.617 ms +24.6%
Relay response → executable quote 1,001.045 ms 805.860 ms -19.5%
Observed total 8,502.531 ms 7,437.368 ms -12.5%

The Relay request regressed by 1.011 seconds in the second sample even though this PR does not optimize the Relay backend. Treating Relay as the same 4.108-second contribution in both traces gives a normalized PoC total of 6,426.691 ms, a 2,075.840 ms / 24.4% end-to-end improvement. Client-controlled time fell from 4,394.591 ms to 2,318.751 ms, a 47.2% reduction.

Structural trace evidence supports the client improvement:

  • 29 → 19 requests in the measured window
  • 18 → 11 requests before Relay
  • two → one eth_estimateGas calls
  • 1,640 ms → 483 ms of pre-request React scheduler time
  • 1,091 ms → 313 ms of pre-request render time

These are two individual simulator traces, not production p50/p95 measurements. Relay latency should continue to be reported separately and old/new runs should be alternated over multiple samples.

Correctness and rollout constraints

  • The adopting callback must return the complete nested update set before a canonical revision is committed.
  • Required assets and nested calldata are committed together or not at all.
  • Late local preparation and quote results are rejected by intent/revision checks.
  • Existing listener-driven behavior remains available to non-adopting flows.
  • The Mobile integration is gated and retains the legacy path.
  • Security/privacy review is still required before rollout to confirm whether Sentinel gates execution only or must also gate vendor disclosure. A Sentinel-blocked revision must never become executable.

Future work

  • Start live balance and safe capability work earlier and concurrently with transaction-specific preparation.
  • Deduplicate or advance remaining capability/metadata reads.
  • Add explicit spans around delegation, request construction, normalization, and state publication.
  • Reduce the remaining response-to-executable publication/render cycles.
  • Collect alternating old/new samples and release profiles on physical iOS and Android devices.
  • Validate and incrementally adopt the generic pipeline in other Transaction Pay flows.

Validation

  • yarn workspace @metamask/transaction-controller run jest --no-coverage src/TransactionController.test.ts --runInBand — 249 passed
  • yarn workspace @metamask/transaction-pay-controller run jest --no-coverage src/TransactionPayController.test.ts src/strategy/relay/relay-quotes.test.ts src/utils/quotes.test.ts --runInBand — 260 passed
  • Both changed workspaces build successfully.
  • Messenger action type checks pass for both controllers.
  • yarn changelog:validate passes.
  • ESLint passes for all changed TypeScript files.

References

  • Mobile first-adopter PR: MetaMask/metamask-mobile#33433
  • Baseline trace SHA-256: 28e517a73d33db3aa63470f9bc55bdbb47fc61060b09227dce3c5df2d4ae13a9
  • PoC trace SHA-256: e80c86da8913924a228627627d851ab75965174a30c7607cee1416917aa2c34f

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
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

@pedronfigueiredo pedronfigueiredo force-pushed the pnf/money-account-quote-latency-poc branch from d284b3c to 732f0f7 Compare July 16, 2026 16:26
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.

1 participant