Skip to content

feat(kyc-controller): register Money Account wallets - #9850

Draft
saustrie-consensys wants to merge 2 commits into
feat/kyc-controllerfrom
feat/moonpay-wallet-registration-stacked
Draft

feat(kyc-controller): register Money Account wallets#9850
saustrie-consensys wants to merge 2 commits into
feat/kyc-controllerfrom
feat/moonpay-wallet-registration-stacked

Conversation

@saustrie-consensys

@saustrie-consensys saustrie-consensys commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Explanation

This stacked PR adds MoonPay Iron self-hosted wallet registration to the unreleased @metamask/kyc-controller package from #9615. The POC is limited to Money Account on Monad.

Ramps and other consumers call one messenger action:

await messenger.call('KycController:registerMoneyAccountWallet', {
  address: moneyAccount.address,
});

The consumer does not pass an Iron customer ID, construct a registration service, build the ownership message, sign it, or manage retry state. KycController handles those details internally:

  1. Prefer the ephemeral this.state.moonpayCustomerId from MoonPay's hosted frame; otherwise resolve Iron's internal customer id via neobank-proxy GET /neobank/customers/{external_id}/external (MetaMask canonicalProfileId as external_id).
  2. Check whether the Monad address is already registered via GET /neobank/addresses/crypto/{customer_id}?filter=SelfHosted (Monad filtered client-side).
  3. Build the exact UTC-dated ownership message and sign it with KeyringController:signPersonalMessage.
  4. Submit via POST /neobank/addresses/crypto/selfhosted with a client-generated Idempotency-Key, and drive the internal state machine for conflict reconciliation, transient retries, and UTC rollover re-signing.

The low-level registration service, message builder, and state machine are implementation details. They are not exported as alternate consumer APIs. Typed registration errors remain exported so clients can classify failures if needed.

This replaces #9847. That PR targeted main and could not cleanly stack onto #9615 because the branches diverged from different points on main.

Backend contract (synced with onramp-api #1126)

Wallet signing is owned by Money Movement on onramp-api neobank-proxy (not kyc-api). This Core PR now targets the transparent neobank routes:

Capability Path Notes
Customer lookup GET /neobank/customers/{external_id}/external Caller supplies MetaMask canonical profile id; response Customer.id is Iron's customer UUID
List wallets GET /neobank/addresses/crypto/{customer_id}?filter=SelfHosted Caller supplies customer_id; Monad filtered in Core
Register POST /neobank/addresses/crypto/selfhosted MoonPay snake_case body as-is; client sends Idempotency-Key
Host on-ramp / neobank (neobankBaseUrl on KycService, falls back to KYC baseUrl) e.g. https://on-ramp.dev-api.cx.metamask.io
Errors Upstream status + plain-string/JSON body mirrored 1:1 No { code: 'iron_error', message } envelope
Auth MetaMask bearer token still attached Customer is not resolved server-side from Canonical JWT; external_id / customer_id come from the client

Backend PRs:

Recommended call site (vs #9848 NeoBankService)

Prefer the UI / product entrypoint calling KycController:registerMoneyAccountWallet, not NeoBankService.

Rationale:

  • #9848's NeoBankService is a thin Ramp API HTTP client (autoramp fetch + bearer). It does not own KYC session state, signing, or Travel Rule orchestration.
  • Wallet registration is Travel Rule / ownership proof owned by KYC (message build, signPersonalMessage, retries, moonpayCustomerId). Putting that inside NeoBank would double-orchestrate and place signing in the wrong layer.
  • Registration is a silent prerequisite before VBA/Pix in product flow, but the trigger should sit at the Money Account / Pix UI (or RampsController product method) once the user has a Money Account address and KYC context, not inside NeoBank's transport methods.
  • moonpayCustomerId is only reliably in KycController state after the MoonPay frame flow; NeoBank refresh/sync paths often run without that session.

Do not wire this into #9848's branch. Call from mobile/extension UI (or a later RampsController Money Account helper) with:

await messenger.call('KycController:registerMoneyAccountWallet', { address });
State machine
stateDiagram-v2
    [*] --> idle
    idle --> preparing: START

    preparing --> alreadyRegistered: LOOKUP_ACTIVE
    preparing --> registeredDisabled: LOOKUP_DISABLED
    preparing --> signing: LOOKUP_ABSENT
    preparing --> lookupUnavailable: LOOKUP_FAILED

    signing --> awaitingUnlock: WALLET_LOCKED
    awaitingUnlock --> signing: WALLET_UNLOCKED
    signing --> submitting: SIGN_OK
    signing --> cancelled: SIGN_REJECTED / CANCEL
    signing --> failedRetryable: SIGN_FAILED(retryable)
    signing --> failedTerminal: SIGN_FAILED(!retryable)

    submitting --> registered: SUBMIT_OK
    submitting --> disambiguate409: SUBMIT_CONFLICT
    submitting --> checkThenRetry: SUBMIT_TRANSIENT
    submitting --> signing: SUBMIT_VALIDATION(utcRollover & attempts < max)
    submitting --> failedTerminal: SUBMIT_VALIDATION(otherwise) / SUBMIT_TERMINAL
    submitting --> failedRetryable: SUBMIT_RATE_LIMITED

    disambiguate409 --> alreadyRegistered: LOOKUP_ACTIVE
    disambiguate409 --> registeredDisabled: LOOKUP_DISABLED
    disambiguate409 --> failedRetryable: LOOKUP_ABSENT
    disambiguate409 --> lookupUnavailable: LOOKUP_FAILED

    checkThenRetry --> alreadyRegistered: LOOKUP_ACTIVE
    checkThenRetry --> registeredDisabled: LOOKUP_DISABLED
    checkThenRetry --> signing: LOOKUP_ABSENT & attempts < max
    checkThenRetry --> failedRetryable: LOOKUP_ABSENT & attempts >= max
    checkThenRetry --> lookupUnavailable: LOOKUP_FAILED

    failedRetryable --> preparing: RETRY
    lookupUnavailable --> preparing: RETRY
    cancelled --> preparing: RETRY

    registered --> [*]
    alreadyRegistered --> [*]
    registeredDisabled --> [*]
    failedTerminal --> [*]
Loading

Contract confirmations from MoonPay/Iron

  • Money Account is an EIP-7702 upgraded EOA. Iron accepts its EIP-191 signature.
  • blockchain: "Monad" works in sandbox and is present in the pinned 2026-08-01 contract.
  • Address uniqueness is per customer per chain.
  • An existing SELF_ATTESTED address satisfies proof of ownership and is treated as registered.

Verification

  • Package tests pass with 100% statement, branch, function, and line coverage.
  • Package ESM and CommonJS builds pass.
  • ESLint, formatting, and generated messenger action checks pass.

Remaining client wiring blockers

  • Clients must pass neobankBaseUrl (on-ramp host) when constructing KycService so wallet routes do not hit the KYC API host.
  • AuthenticationController:getSessionProfile must be delegated to KycService messengers so fallback customer lookup can read canonicalProfileId.
  • Backend feat: add support for consensys zkevm network (Linea) #1126 must be deployed (or locally available) before end-to-end registration works against neobank-proxy.

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 an address-only KycController action that resolves the MoonPay customer, signs a Monad ownership proof, and registers the wallet through the MetaMask proxy.
@saustrie-consensys

Copy link
Copy Markdown
Contributor Author

Backend ownership update: wallet signing moved from kyc-api to Money Movement neobank-proxy.

This Core branch still uses the old /vendors/moonpay/* kyc-api paths. Follow-up needed to retarget WalletRegistrationService to /neobank/customers/... and /neobank/addresses/crypto/... on the on-ramp host (details in the updated PR body).

Retarget Money Account self-hosted wallet registration from kyc-api
/vendors/moonpay/* to onramp-api neobank-proxy /neobank/... so Core
matches Money Movement ownership and transparent proxy semantics.
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