Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions packages/ramps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add `RampsController.createAutoramp(request, options?)` method and the `RampsController:createAutoramp` messenger action (plus the exported `RampsControllerCreateAutorampAction` and `CreateAutorampRequest` types). It resolves the MoonPay `customer_id` from Profile Sync (`AuthenticationController:getSessionProfile`) via `NeoBankService:getCustomerByExternalId`, injects it into the request (overwriting any caller-supplied `customer_id`), forwards the body to `NeoBankService:createAutoramp`, and applies the returned snapshot to local state. Throws when the wallet is not signed in or no MoonPay customer is mapped to the external id. ([#9853](https://github.com/MetaMask/core/pull/9853))
- Add the exported `RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS` constant listing the other-controller actions (`AuthenticationController:getSessionProfile`, `KeyringController:signPersonalMessage`) that hosts must delegate to the `RampsController` messenger to enable autoramp creation and Money Account wallet registration. ([#9853](https://github.com/MetaMask/core/pull/9853))
- Add NeoBankService Pix / autoramp quote client methods and messenger actions, targeting the neobank-proxy `/neobank` prefix on the Ramp API host: `registerPixAddress`, `getAutorampQuote`, `createAutoramp`, `getAutorampQuoteForAutoramp`, `attachAutorampQuote`, and `getCustomerByExternalId`. Pix/quote helpers return parsed proxy JSON; `createAutoramp` maps autoramp-shaped responses via `mapNeoBankAutorampToRemoteSnapshot` (same as `getAutoramp`). Optional `Idempotency-Key` is supported on mutating calls. ([#9853](https://github.com/MetaMask/core/pull/9853))
- Export `TERMINAL_ORDER_STATUSES` and `isTerminalOrderStatus()` so consuming clients can share the controller's terminal order status set instead of maintaining duplicate copies. ([#9679](https://github.com/MetaMask/core/pull/9679), [#9853](https://github.com/MetaMask/core/pull/9853))
- Add `RampsController.registerMoneyAccountWallet({ address })` method and the `RampsController:registerMoneyAccountWallet` messenger action (moved from `@metamask/kyc-controller`). Resolves the MoonPay Iron customer id via Profile Sync → neobank-proxy external-id lookup, signs the Monad ownership message via `KeyringController:signPersonalMessage`, and registers the self-hosted wallet through the neobank-proxy — including `409` disambiguation, transient-failure reconciliation, and UTC date rollover re-signing ([#9850](https://github.com/MetaMask/core/pull/9850), [#9847](https://github.com/MetaMask/core/pull/9847), [#9853](https://github.com/MetaMask/core/pull/9853))
- Add `NeoBankService.getMoonpayCustomerId`, `NeoBankService.getWalletRegistrationStatus`, and `NeoBankService.registerSelfHostedWallet` methods and messenger actions, targeting the transparent neobank routes (`GET /neobank/customers/{external_id}/external`, `GET /neobank/addresses/crypto/{customer_id}`, `POST /neobank/addresses/crypto/selfhosted`) with client-side Monad filtering, `Idempotency-Key` support, and upstream error bodies mirrored 1:1. ([#9853](https://github.com/MetaMask/core/pull/9853))
- Export the wallet registration types (`SelfHostedRegistration`, `RegistrationStatus`, `RegistrationOutcome`, `WalletRegistrationError`, `WalletRegistrationErrorKind`, `MoneyAccountWalletRegistrationResult`) and `buildOwnershipMessage` (moved from `@metamask/kyc-controller`). ([#9853](https://github.com/MetaMask/core/pull/9853))
- Add `RampsController.createAutoramp(request, options?)` method and the `RampsController:createAutoramp` messenger action (plus the exported `RampsControllerCreateAutorampAction` and `CreateAutorampRequest` types). It resolves the MoonPay `customer_id` from Profile Sync (`AuthenticationController:getSessionProfile`) via `NeoBankService:getCustomerByExternalId`, injects it into the request (overwriting any caller-supplied `customer_id`), forwards the body to `NeoBankService:createAutoramp`, and applies the returned snapshot to local state. Throws when the wallet is not signed in or no MoonPay customer is mapped to the external id. ([#9930](https://github.com/MetaMask/core/pull/9930))
- Add the exported `RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS` constant listing the other-controller actions (`KycController:getCustomerIdentity`, `AuthenticationController:getSessionProfile`, `KeyringController:signPersonalMessage`) that hosts must delegate to the `RampsController` messenger to enable autoramp creation and Money Account wallet registration. ([#9930](https://github.com/MetaMask/core/pull/9930), [#9931](https://github.com/MetaMask/core/pull/9931))
- Export `KycControllerGetCustomerIdentityAction` so hosts can type the KYC messenger action they delegate onto `RampsController` without depending on `@metamask/kyc-controller`. ([#9931](https://github.com/MetaMask/core/pull/9931))
- Add NeoBankService Pix / autoramp quote client methods and messenger actions, targeting the neobank-proxy `/neobank` prefix on the Ramp API host: `registerPixAddress`, `getAutorampQuote`, `createAutoramp`, `getAutorampQuoteForAutoramp`, `attachAutorampQuote`, and `getCustomerByExternalId`. Pix/quote helpers return parsed proxy JSON; `createAutoramp` maps autoramp-shaped responses via `mapNeoBankAutorampToRemoteSnapshot` (same as `getAutoramp`). Optional `Idempotency-Key` is supported on mutating calls. ([#9930](https://github.com/MetaMask/core/pull/9930))
- Export `TERMINAL_ORDER_STATUSES` and `isTerminalOrderStatus()` so consuming clients can share the controller's terminal order status set instead of maintaining duplicate copies. ([#9679](https://github.com/MetaMask/core/pull/9679), [#9930](https://github.com/MetaMask/core/pull/9930))
- Add `RampsController.registerMoneyAccountWallet({ address })` method and the `RampsController:registerMoneyAccountWallet` messenger action (moved from `@metamask/kyc-controller`). Resolves the MoonPay Iron customer id via Profile Sync → neobank-proxy external-id lookup, signs the Monad ownership message via `KeyringController:signPersonalMessage`, and registers the self-hosted wallet through the neobank-proxy — including `409` disambiguation, transient-failure reconciliation, and UTC date rollover re-signing ([#9930](https://github.com/MetaMask/core/pull/9930))
- Add `NeoBankService.getMoonpayCustomerId`, `NeoBankService.getWalletRegistrationStatus`, and `NeoBankService.registerSelfHostedWallet` methods and messenger actions, targeting the transparent neobank routes (`GET /neobank/customers/{external_id}/external`, `GET /neobank/addresses/crypto/{customer_id}`, `POST /neobank/addresses/crypto/selfhosted`) with client-side Monad filtering, `Idempotency-Key` support, and upstream error bodies mirrored 1:1. ([#9930](https://github.com/MetaMask/core/pull/9930))
- Export the wallet registration types (`SelfHostedRegistration`, `RegistrationStatus`, `RegistrationOutcome`, `WalletRegistrationError`, `WalletRegistrationErrorKind`, `MoneyAccountWalletRegistrationResult`) and `buildOwnershipMessage` (moved from `@metamask/kyc-controller`). ([#9930](https://github.com/MetaMask/core/pull/9930))

### Changed

- Point `NeoBankService.getAutoramp` at `GET /neobank/autoramps/{id}` (neobank-proxy global `/neobank` prefix) instead of `/api/v2/autoramps/{id}`, so Core matches the proxy that ships. ([#9853](https://github.com/MetaMask/core/pull/9853))
- Resolve autoramp / Money Account wallet-registration customer id from `KycController:getCustomerIdentity` when a session identity is present (vendor-scoped `{ vendor, id }`), falling back to Profile Sync + `NeoBankService:getCustomerByExternalId` (prefer `canonicalProfileId`, else `profileId`). Hosts that enable autoramp creation / wallet registration must delegate `KycController:getCustomerIdentity` on the `RampsController` messenger (`RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS`). ([#9931](https://github.com/MetaMask/core/pull/9931))
- Point `NeoBankService.getAutoramp` at `GET /neobank/autoramps/{id}` (neobank-proxy global `/neobank` prefix) instead of `/api/v2/autoramps/{id}`, so Core matches the proxy that ships. ([#9930](https://github.com/MetaMask/core/pull/9930))
- Bump `@metamask/remote-feature-flag-controller` from `^5.0.0` to `^6.0.0` ([#9945](https://github.com/MetaMask/core/pull/9945))

### Fixed

- Keep the local `customerId` / `walletAddress` when a remote autoramp snapshot omits or blanks them. The proxy sends empty identity fields on partial status pushes, and `applyAutorampRemoteStatus` / `mapNeoBankAutorampToRemoteSnapshot` treated those as a clear, wiping valid local values during refresh-on-load and websocket pushes. ([#9861](https://github.com/MetaMask/core/pull/9861), [#9853](https://github.com/MetaMask/core/pull/9853))

### Changed

- Bump `@metamask/remote-feature-flag-controller` from `^5.0.0` to `^6.0.0` ([#9945](https://github.com/MetaMask/core/pull/9945))
- Keep the local `customerId` / `walletAddress` when a remote autoramp snapshot omits or blanks them. The proxy sends empty identity fields on partial status pushes, and `applyAutorampRemoteStatus` / `mapNeoBankAutorampToRemoteSnapshot` treated those as a clear, wiping valid local values during refresh-on-load and websocket pushes. ([#9930](https://github.com/MetaMask/core/pull/9930))

## [20.0.0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ export type RampsControllerAddAutorampAction = {
*
* The vendor `customer_id` is not accepted from callers: it is resolved via
* {@link RampsController.resolveAutorampCustomerId} and injected into the
* request. This keeps the sensitive customer id owned by Profile Sync /
* the neo-bank proxy and avoids requiring the UI to know or plumb it.
* request. This keeps the sensitive customer id owned by KYC / Profile Sync
* / the neo-bank proxy and avoids requiring the UI to know or plumb it.
*
* @param request - CreateAutoramp payload (any `customer_id` is overwritten).
* @param options - Optional idempotency key forwarded to the proxy.
Expand All @@ -317,8 +317,8 @@ export type RampsControllerCreateAutorampAction = {
*
* Consumers provide only the Monad address. The controller resolves the
* vendor customer id via {@link RampsController.resolveAutorampCustomerId}
* (Profile Sync → neobank-proxy external-id lookup) before the first
* list/lookup because list requires `customer_id`
* (KYC session identity, else Profile Sync → neobank-proxy external-id
* lookup) before the first list/lookup because list requires `customer_id`
* in the path. Message construction, EIP-191 signing, submission, and
* ambiguous-write reconciliation stay internal to this controller.
*
Expand Down
128 changes: 128 additions & 0 deletions packages/ramps-controller/src/RampsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9206,6 +9206,83 @@ describe('RampsController', () => {
});
});

it('prefers the KYC session identity over Profile Sync lookup', async () => {
await withController(async ({ controller, rootMessenger }) => {
spyOnGetCustomerIdentity(rootMessenger, {
vendor: 'iron',
id: 'kyc-cust-1',
});
const getSessionProfile = jest.fn();
rootMessenger.registerActionHandler(
'AuthenticationController:getSessionProfile',
getSessionProfile,
);
const getCustomerByExternalId = jest.fn();
rootMessenger.registerActionHandler(
'NeoBankService:getCustomerByExternalId',
getCustomerByExternalId,
);
const createAutoramp = jest.fn().mockResolvedValue({
id: 'ar-new',
customerId: 'kyc-cust-1',
walletAddress: '0xabc',
status: AutorampStatus.Created,
});
rootMessenger.registerActionHandler(
'NeoBankService:createAutoramp',
createAutoramp,
);

const created = await controller.createAutoramp({
customer_id: 'attacker-supplied',
});

expect(createAutoramp).toHaveBeenCalledWith(
{ customer_id: 'kyc-cust-1' },
{},
);
expect(created.customerId).toBe('kyc-cust-1');
expect(getSessionProfile).not.toHaveBeenCalled();
expect(getCustomerByExternalId).not.toHaveBeenCalled();
});
});

it('falls back to Profile Sync when KYC identity has an empty id', async () => {
await withController(async ({ controller, rootMessenger }) => {
spyOnGetCustomerIdentity(rootMessenger, { vendor: 'iron', id: '' });
rootMessenger.registerActionHandler(
'AuthenticationController:getSessionProfile',
async () =>
({
identifierId: 'id-1',
profileId: 'profile-1',
canonicalProfileId: 'canonical-1',
metaMetricsId: 'mm-1',
}) as never,
);
const getCustomerByExternalId = jest
.fn()
.mockResolvedValue({ id: 'cust-fallback' });
rootMessenger.registerActionHandler(
'NeoBankService:getCustomerByExternalId',
getCustomerByExternalId,
);
rootMessenger.registerActionHandler(
'NeoBankService:createAutoramp',
async () => ({
id: 'ar-new',
customerId: 'cust-fallback',
walletAddress: '0xabc',
status: AutorampStatus.Created,
}),
);

await controller.createAutoramp({});

expect(getCustomerByExternalId).toHaveBeenCalledWith('canonical-1');
});
});

it('skips failed refreshes when refreshing all autoramps', async () => {
await withController(async ({ controller, rootMessenger }) => {
rootMessenger.registerActionHandler(
Expand Down Expand Up @@ -9505,6 +9582,7 @@ describe('RampsController', () => {
};

type WalletRegistrationHandlers = {
getCustomerIdentity: jest.Mock;
getSessionProfile: jest.Mock;
getCustomerByExternalId: jest.Mock;
getWalletRegistrationStatus: jest.Mock;
Expand All @@ -9523,6 +9601,7 @@ describe('RampsController', () => {
rootMessenger: RootMessenger,
): WalletRegistrationHandlers {
const handlers: WalletRegistrationHandlers = {
getCustomerIdentity: spyOnGetCustomerIdentity(rootMessenger, null),
getSessionProfile: jest.fn().mockResolvedValue({
identifierId: 'id-1',
profileId: 'profile-1',
Expand Down Expand Up @@ -9638,12 +9717,32 @@ describe('RampsController', () => {

await controller.registerMoneyAccountWallet({ address: '0xabc' });

expect(handlers.getCustomerIdentity).toHaveBeenCalled();
expect(handlers.getCustomerByExternalId).toHaveBeenCalledWith(
'canonical-1',
);
});
});

it('prefers the KYC session identity over Profile Sync lookup', async () => {
await withController(async ({ controller, rootMessenger }) => {
const handlers = registerWalletRegistrationHandlers(rootMessenger);
handlers.getCustomerIdentity.mockReturnValue({
vendor: 'iron',
id: 'kyc-cust-1',
});

await controller.registerMoneyAccountWallet({ address: '0xabc' });

expect(handlers.getWalletRegistrationStatus).toHaveBeenCalledWith({
customerId: 'kyc-cust-1',
address: '0xabc',
});
expect(handlers.getSessionProfile).not.toHaveBeenCalled();
expect(handlers.getCustomerByExternalId).not.toHaveBeenCalled();
});
});

it('reconciles an ambiguous conflict as already registered', async () => {
await withController(async ({ controller, rootMessenger }) => {
const handlers = registerWalletRegistrationHandlers(rootMessenger);
Expand Down Expand Up @@ -12613,6 +12712,13 @@ function getRootMessenger(): RootMessenger {
'RampsService:getDefaultRedirectCallbackUrl',
() => STAGING_REDIRECT_CALLBACK_URL,
);
// Default: no KYC session, so autoramp / wallet registration fall back to
// Profile Sync → neo-bank external-id lookup. Tests that need a session
// identity call `spyOnGetCustomerIdentity`.
rootMessenger.registerActionHandler(
'KycController:getCustomerIdentity',
() => null,
);
return rootMessenger;
}

Expand Down Expand Up @@ -12640,6 +12746,28 @@ function spyOnDefaultRedirectCallbackUrl(
return handler;
}

/**
* Replaces the default `KycController:getCustomerIdentity` handler with a spy.
*
* @param rootMessenger - The root messenger to re-register the handler on.
* @param identity - Session identity to return, or `null` when none is captured.
* @returns The spy standing in for the KYC controller method.
*/
function spyOnGetCustomerIdentity(
rootMessenger: RootMessenger,
identity: { vendor: string; id: string } | null,
): jest.Mock<{ vendor: string; id: string } | null, []> {
const handler = jest.fn<{ vendor: string; id: string } | null, []>(
() => identity,
);
rootMessenger.unregisterActionHandler('KycController:getCustomerIdentity');
rootMessenger.registerActionHandler(
'KycController:getCustomerIdentity',
handler,
);
return handler;
}

/**
* Constructs the messenger for the controller under test.
*
Expand Down
Loading