feat(ramps): poll neo-bank deposits and emit status notifications by saustrie-consensys · Pull Request #10120 · MetaMask/core · GitHub
Skip to content

feat(ramps): poll neo-bank deposits and emit status notifications - #10120

Draft
saustrie-consensys wants to merge 5 commits into
mainfrom
saustrie/ramps-deposit-notifications
Draft

feat(ramps): poll neo-bank deposits and emit status notifications#10120
saustrie-consensys wants to merge 5 commits into
mainfrom
saustrie/ramps-deposit-notifications

Conversation

@saustrie-consensys

@saustrie-consensys saustrie-consensys commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Explanation

Part of the MM Neobank MVP0 (Brazil 🇧🇷) onramp flow. When a user funds a Money Account via Pix, the partner (Iron) pays out mUSD on Monad, and Core needs to notice those deposits so the app can react.

This adds emit-only deposit polling to RampsController (TRAM-3898, "poll for deposit and notifications"). The vault "sweep" (the original step 3) was removed from the ticket, so this PR only detects and reports: it takes no on-chain action, and vault sweeping stays with the backend.

How it works, reusing the autoramp/order patterns already in this package:

  • NeoBankService.getAutorampTransactions(autorampId) fetches a partner's deposit/transaction records, mapped via the exported mapNeoBankTransactionToRemoteSnapshot (accepts a bare array or a { transactions } envelope; rejects items missing id/status).
  • moneyAccountDeposit.ts is a thin local clone model mirroring autorampAccount.ts: a status enum plus a pure applyDepositRemoteStatus diff returning statusChanged / shouldNotify.
  • A sibling poll loop (startDepositPolling / stopDepositPolling, plus refreshDeposits for app-load catch-up) reuses the order poller's 30s interval and error backoff. Each cycle upserts a persisted state.deposits array and publishes RampsController:depositStatusChanged on status transitions. Only Approved autoramps (or ones with an in-flight local deposit) are polled, so an in-flight deposit keeps being tracked even if its route later goes terminal.
  • markDepositAsNotified dedupes repeat notifications for the same status; removeDeposit lets consumers prune the persisted list.

The client tracks the real MoonPay/Iron AutorampTransaction that the neobank-proxy (onramp-api PR #1124) forwards verbatim: GET /neobank/autoramp-transactions?autoramp_id={id}, returning an Iron PagedList (items array) with the payout hash nested at payout_crypto_transaction.transaction_hash, and the 8 AutorampTransactionStatus values. Because #1124 is unmerged and its own fixtures use a transitional data / flat transaction_hash shape, the mapper reads both envelopes (items then data then a bare array) and both hash locations (nested then flat) so it works whichever ships. Still mocked in unit tests until #1124 merges. Deferred to the mobile-safe DTO in TRAM-3925 (unstarted): the display field names (amount / currency / money-account, which MoonPay carries as structured objects), and cursor pagination — the poll reads the first page only for now.

Known behaviors worth a reviewer's eye

  • A deposit first observed already in a terminal state (app closed across the whole in-progress-to-terminal window) is recorded for display but fires no depositStatusChanged notification (there is no prior local state to have transitioned from). Whether catch-up should toast is a product question.
  • Unknown / renamed partner statuses fall back to a non-terminal in-progress value (FundsReviewInProgress), so an unrecognized status keeps polling rather than being mistaken for a terminal outcome — best-effort until TRAM-3925 pins the wire values.
  • state.deposits is a live mirror of the proxy for actively-polled (Approved) autoramps, so it grows with the user's deposit history. removeDeposit prunes local state, but a deposit under a still-polled autoramp re-syncs on the next poll (it only sticks once the autoramp is no longer pollable).

Hosts must delegate NeoBankService:getAutorampTransactions (added to RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS), or startDepositPolling / refreshDeposits reject with a messenger "handler has not been delegated" error.

References

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

Add emit-only Money Account deposit polling to RampsController for the MM
Neobank onramp flow (TRAM-3898, "poll for deposit and notifications"). A
sibling poll loop fetches each pollable autoramp's deposit/transaction records
from the neo-bank proxy, keeps a persisted state.deposits clone, and publishes
RampsController:depositStatusChanged on status transitions so the app can show
a toast or refresh the account screen. The poller takes no on-chain action;
vault sweeping stays with the backend.

- Add NeoBankService.getAutorampTransactions and the matching messenger action,
  mapping proxy transactions (bare array or a { transactions } envelope) via the
  exported mapNeoBankTransactionToRemoteSnapshot. Rejects items missing id/status.
- Add the moneyAccountDeposit model: MoneyAccountDeposit, MoneyAccountDepositStatus,
  the pure applyDepositRemoteStatus diff, and helpers, mirroring autorampAccount.
- Add startDepositPolling / stopDepositPolling / refreshDeposits, plus
  markDepositAsNotified (notify dedupe) and removeDeposit (prune persisted state),
  a persisted state.deposits array, and the depositStatusChanged event, reusing
  the order poller's 30s interval and error backoff.
- Poll only Approved autoramps, or ones with an in-flight local deposit, so an
  in-flight deposit keeps being tracked even if its route later goes terminal.
…trancy)

- Backfill the owning autoramp id onto deposit snapshots from the poll query
  key, so an in-flight deposit stays pollable once its route goes terminal even
  when the proxy omits autoramp_id.
- Only write state when a snapshot is new or materially changed (new `changed`
  flag on applyDepositRemoteStatus), so unchanged deposits no longer churn
  updatedAt / stateChange on every 30s poll for the life of the autoramp.
- Guard refreshDeposits with the same in-flight flag as the poll loop so an
  unlock catch-up cannot overlap the interval poll.
- Correct the removeDeposit doc: a deposit under an actively polled autoramp is
  a live mirror of the proxy and re-syncs; removal only sticks once the autoramp
  is no longer pollable.
- Note that a deposit first observed already-terminal is recorded for display
  without a notification (no prior local state to have transitioned from).
… contract

Replace the assumed transactions contract with the one shipping in onramp-api
PR #1124 (the neobank-proxy forwards raw MoonPay Enterprise verbatim):

- Route: GET /neobank/autoramp-transactions?autoramp_id={id} (flat, not the
  assumed nested /autoramps/{id}/transactions), returning a MoonPay PagedList;
  read the `data` array (single page for now, next_cursor pagination is a
  follow-up).
- Payout hash: flat `transaction_hash` (drop the invented
  payout_transaction_hash and nested payout.transaction_hash).
- Status: the real 8 AutorampTransactionStatus values (FundsReviewInProgress,
  ConversionInProgress, PayoutInProgress, Completed, Failed, RejectedAml,
  RejectedFraud, RejectedMinAmount). The three Rejected* are terminal and
  notable; an unknown status falls back to a non-terminal value. This fixes the
  prior invented enum, which treated rejections as non-terminal and would have
  polled them forever without ever notifying.
- Display fields (amount/currency/moneyAccountAddress) are intentionally left
  unmapped: MoonPay carries them as structured objects, and the mobile-safe DTO
  (TRAM-3925) will pin the wire names.
… hash)

The prior alignment matched onramp-api #1124's internal mock fixture
({ data, next_cursor } + flat transaction_hash), not the real MoonPay/Iron
AutorampTransaction the proxy forwards verbatim. Per Iron's OpenAPI spec the
list is a PagedList with an `items` array and the payout hash is nested at
`payout_crypto_transaction.transaction_hash`. Reading only `data` / a flat hash
would have thrown on every real response (no deposit ever tracked) and never
captured the payout hash.

Make the mapping tolerant of both, since the proxy PR is unmerged and TRAM-3925
will re-shape it again:
- Envelope: read `items` (real Iron), then `data` (proxy fixture), then a bare
  array.
- Payout hash: read nested `payout_crypto_transaction.transaction_hash`, then
  flat `transaction_hash`.
- Tests use the real `{ items, cursor, prev_cursor }` + nested hash as the
  primary case, with the `data` / flat / bare-array fallbacks covered too.
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