Skip to content
Closed
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
5 changes: 5 additions & 0 deletions packages/ramps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add Money Account deposit polling to `RampsController` (emit-only). New `startDepositPolling` / `stopDepositPolling` / `refreshDeposits` methods and messenger actions poll the neo-bank proxy for each pollable autoramp's transactions on the shared 30s interval, keep a persisted `state.deposits` clone, and publish the new `RampsController:depositStatusChanged` event (`{ deposit, previousStatus, shouldNotify }`) on status transitions. Only `Approved` autoramps (or ones with an in-flight local deposit) are polled. The poller takes no on-chain action; vault sweeping is owned by the backend. ([#10108](https://github.com/MetaMask/core/pull/10108))
- Also adds `markDepositAsNotified(depositId)` (dedupes repeat notifications for the same status) and `removeDeposit(depositId)` (lets consumers prune the persisted deposit list), each exposed as a messenger action.
- `RampsController` now calls `NeoBankService:getAutorampTransactions`, added to the exported `RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS`. Hosts that enumerate their delegated actions instead of spreading that constant must add it, or `startDepositPolling` / `refreshDeposits` reject with a messenger "handler has not been delegated" error.
- Add the `moneyAccountDeposit` model: `MoneyAccountDeposit`, `MoneyAccountDepositStatus`, `MoneyAccountDepositRemoteSnapshot`, the pure `applyDepositRemoteStatus` diff, and helpers (`normalizeDepositStatus`, `isTerminalDepositStatus`, `createMoneyAccountDeposit`, `markDepositNotified`, `TERMINAL_DEPOSIT_STATUSES`, `NOTABLE_DEPOSIT_STATUSES`). ([#10108](https://github.com/MetaMask/core/pull/10108))
- Add `NeoBankService.getAutorampTransactions(autorampId)` and the `NeoBankService:getAutorampTransactions` messenger action, which fetch deposit/transaction records from neobank-proxy `GET /neobank/autoramps/{id}/transactions` and map them via the exported `mapNeoBankTransactionToRemoteSnapshot` (accepting either a bare array or a `{ transactions }` envelope). ([#10108](https://github.com/MetaMask/core/pull/10108))
- 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.
- 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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export type NeoBankServiceGetAutorampAction = {
handler: NeoBankService['getAutoramp'];
};

/**
* Fetches deposit/transaction records for an autoramp via neobank-proxy
* `GET /neobank/autoramps/{autoramp_id}/transactions`.
*
* Used by the deposit poller to detect status changes (e.g. a payout settling
* on Monad). Route + response shape are assumed pending the proxy contract
* (onramp-api #1124).
*
* @param autorampId - MoonPay / Ramp API autoramp id.
* @returns Deposit snapshots for controller apply/refresh.
*/
export type NeoBankServiceGetAutorampTransactionsAction = {
type: `NeoBankService:getAutorampTransactions`;
handler: NeoBankService['getAutorampTransactions'];
};

/**
* Registers a Pix address via neobank-proxy `POST /neobank/addresses/pix`.
* Body is forwarded as opaque JSON (MoonPay address schema).
Expand Down Expand Up @@ -99,6 +115,7 @@ export type NeoBankServiceGetCustomerByExternalIdAction = {
*/
export type NeoBankServiceMethodActions =
| NeoBankServiceGetAutorampAction
| NeoBankServiceGetAutorampTransactionsAction
| NeoBankServiceRegisterPixAddressAction
| NeoBankServiceGetAutorampQuoteAction
| NeoBankServiceCreateAutorampAction
Expand Down
132 changes: 132 additions & 0 deletions packages/ramps-controller/src/NeoBankService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
mapNeoBankAutorampToRemoteSnapshot,
mapNeoBankTransactionToRemoteSnapshot,
NeoBankService,
} from './NeoBankService.js';
import type { NeoBankServiceMessenger } from './NeoBankService.js';
Expand All @@ -11,10 +12,10 @@

const STAGING_BASE = 'https://on-ramp.uat-api.cx.metamask.io';

/**

Check failure on line 15 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Missing JSDoc @PARAM "options.omitDefaults" declaration

Check failure on line 15 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Missing JSDoc @PARAM "options.baseUrlOverride" declaration

Check failure on line 15 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Missing JSDoc @PARAM "options.environment" declaration
* Builds a NeoBankService with AuthenticationController bearer auth stubbed.
*
* @param options - Optional constructor overrides. Pass `omitDefaults: true` to

Check failure on line 18 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Missing @PARAM "options.omitDefaults"

Check failure on line 18 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Missing @PARAM "options.baseUrlOverride"

Check failure on line 18 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Missing @PARAM "options.environment"
* exercise constructor defaulted parameters (`environment`, `policyOptions`).
* @returns Service instance for the test.
*/
Expand Down Expand Up @@ -61,7 +62,7 @@

describe('NeoBankService', () => {
afterEach(() => {
nock.cleanAll();

Check failure on line 65 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

Caution: `nock` also has a named export `cleanAll`. Check if you meant to write `import {cleanAll} from 'nock'` instead
});

describe('mapNeoBankAutorampToRemoteSnapshot', () => {
Expand Down Expand Up @@ -110,8 +111,139 @@
});
});

describe('mapNeoBankTransactionToRemoteSnapshot', () => {
it('maps proxy transaction fields into a deposit snapshot', () => {
expect(
mapNeoBankTransactionToRemoteSnapshot({
id: 'dep-1',
autoramp_id: 'ar-1',
status: 'Completed',
money_account_address: '0xaccount',
payout_transaction_hash: '0xpayout',
amount: '100.00',
currency: 'BRL',
}),
).toStrictEqual({
id: 'dep-1',
autorampId: 'ar-1',
moneyAccountAddress: '0xaccount',
status: 'Completed',
payoutTransactionHash: '0xpayout',
amount: '100.00',
currency: 'BRL',
});
});

it('falls back to a nested payout.transaction_hash', () => {
expect(
mapNeoBankTransactionToRemoteSnapshot({
id: 'dep-1',
status: 'Completed',
payout: { transaction_hash: '0xnested' },
}),
).toMatchObject({ payoutTransactionHash: '0xnested' });
});

it('leaves the payout hash undefined when the proxy omits it', () => {
expect(
mapNeoBankTransactionToRemoteSnapshot({
id: 'dep-1',
status: 'Pending',
}),
).toMatchObject({ payoutTransactionHash: undefined });
});
});

describe('getAutorampTransactions', () => {
it('fetches /neobank/autoramps/{id}/transactions and maps an array body', async () => {
const scope = nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1\/transactions/u)
.matchHeader('Authorization', 'Bearer test-token')
.reply(200, [
{
id: 'dep-1',
autoramp_id: 'ar-1',
status: 'Completed',
payout_transaction_hash: '0xpayout',
},
]);

const service = createService();
const snapshots = await service.getAutorampTransactions('ar-1');

expect(scope.isDone()).toBe(true);
expect(snapshots).toStrictEqual([
{
id: 'dep-1',
autorampId: 'ar-1',
moneyAccountAddress: undefined,
status: 'Completed',
payoutTransactionHash: '0xpayout',
amount: undefined,
currency: undefined,
},
]);
});

it('accepts a { transactions } envelope', async () => {
nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1\/transactions/u)
.reply(200, { transactions: [{ id: 'dep-1', status: 'Pending' }] });

const service = createService();
const snapshots = await service.getAutorampTransactions('ar-1');

expect(snapshots).toHaveLength(1);
expect(snapshots[0]).toMatchObject({ id: 'dep-1', status: 'Pending' });
});

it('throws HttpError when the proxy returns a non-2xx status', async () => {
nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1\/transactions/u)
.reply(500);

const service = createService();
await expect(service.getAutorampTransactions('ar-1')).rejects.toThrow(
/failed with status '500'/u,
);
});

it('throws when the response body is not a transaction list', async () => {
nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1\/transactions/u)
.reply(200, { nope: true });

const service = createService();
await expect(service.getAutorampTransactions('ar-1')).rejects.toThrow(
'Malformed response received from neo-bank transactions API',
);
});

it('throws when an item is missing an id', async () => {
nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1\/transactions/u)
.reply(200, [{ status: 'Pending' }]);

const service = createService();
await expect(service.getAutorampTransactions('ar-1')).rejects.toThrow(
'Malformed response received from neo-bank transactions API',
);
});

it('throws when an item is missing a status', async () => {
nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1\/transactions/u)
.reply(200, [{ id: 'dep-1' }]);

const service = createService();
await expect(service.getAutorampTransactions('ar-1')).rejects.toThrow(
'Malformed response received from neo-bank transactions API',
);
});
});

describe('getAutoramp', () => {
it('GETs /neobank/autoramps/{id} with bearer auth', async () => {

Check failure on line 246 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

`it`s should begin with lowercase
const scope = nock(STAGING_BASE)
.get(/\/neobank\/autoramps\/ar-1/u)
.matchHeader('Authorization', 'Bearer test-token')
Expand Down Expand Up @@ -156,7 +288,7 @@
});

describe('registerPixAddress', () => {
it('POSTs /neobank/addresses/pix with JSON body and bearer auth', async () => {

Check failure on line 291 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

`it`s should begin with lowercase
const body = {
type: 'Pix',
pix_key: 'user@example.com',
Expand Down Expand Up @@ -195,7 +327,7 @@
});

describe('getAutorampQuote', () => {
it('GETs /neobank/autoramps/quote with query params', async () => {

Check failure on line 330 in packages/ramps-controller/src/NeoBankService.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

`it`s should begin with lowercase
const scope = nock(STAGING_BASE)
.get('/neobank/autoramps/quote')
.query((query) => {
Expand Down
98 changes: 98 additions & 0 deletions packages/ramps-controller/src/NeoBankService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import type {
import { createServicePolicy, HttpError } from '@metamask/controller-utils';
import type { Messenger } from '@metamask/messenger';
import type { AuthenticationController } from '@metamask/profile-sync-controller';
import type { Hex } from '@metamask/utils';

import packageJson from '../package.json';
import type {
AutorampDepositRailsSummary,
AutorampRemoteSnapshot,
} from './autorampAccount.js';
import type { MoneyAccountDepositRemoteSnapshot } from './moneyAccountDeposit.js';
import type { NeoBankServiceMethodActions } from './NeoBankService-method-action-types.js';
import { RAMPS_SDK_VERSION, RampsEnvironment } from './RampsService.js';

Expand Down Expand Up @@ -40,6 +42,39 @@ export type NeoBankAutorampResponse = {
deposit_rails?: unknown[];
};

/**
* Raw deposit/transaction payload from the MetaMask Ramp API neo-bank proxy.
*
* Represents a single payment instance flowing through an autoramp (partner
* receives fiat, pays out mUSD on Monad to the Money Account). Field names mirror
* the assumed neobank-proxy transactions contract (onramp-api #1124) and may
* evolve — keep the mapper tolerant.
*/
/* eslint-disable @typescript-eslint/naming-convention -- snake_case proxy wire format */
export type NeoBankTransactionResponse = {
id: string;
status: string;
autoramp_id?: string;
money_account_address?: string;
/** Monad payout transaction hash when the payout has settled on-chain. */
payout_transaction_hash?: string;
/** Alternate nested location for the payout hash, if the proxy nests it. */
payout?: {
transaction_hash?: string;
};
amount?: string;
currency?: string;
};
/* eslint-enable @typescript-eslint/naming-convention */

/**
* Envelope returned by the neo-bank transactions endpoint. The proxy may return
* a bare array or wrap it under `transactions`; the mapper accepts both.
*/
export type NeoBankTransactionsResponse =
| NeoBankTransactionResponse[]
| { transactions?: NeoBankTransactionResponse[] };

/**
* Optional headers for neo-bank mutating requests.
*/
Expand All @@ -61,6 +96,7 @@ export type NeoBankQueryParams = Record<

const MESSENGER_EXPOSED_METHODS = [
'getAutoramp',
'getAutorampTransactions',
'registerPixAddress',
'getAutorampQuote',
'createAutoramp',
Expand Down Expand Up @@ -154,6 +190,29 @@ export function mapNeoBankAutorampToRemoteSnapshot(
};
}

/**
* Maps a neo-bank proxy transaction response into a local deposit snapshot.
*
* @param response - Single transaction from the proxy transactions endpoint.
* @returns Snapshot consumed by `applyDepositRemoteStatus`.
*/
export function mapNeoBankTransactionToRemoteSnapshot(
response: NeoBankTransactionResponse,
): MoneyAccountDepositRemoteSnapshot {
const payoutTransactionHash =
response.payout_transaction_hash ?? response.payout?.transaction_hash;

return {
id: response.id,
autorampId: response.autoramp_id,
moneyAccountAddress: response.money_account_address as Hex | undefined,
status: response.status,
payoutTransactionHash: payoutTransactionHash as Hex | undefined,
amount: response.amount,
currency: response.currency,
};
}

/**
* Client for MetaMask Ramp API neo-bank endpoints (MoonPay Enterprise proxy).
*
Expand Down Expand Up @@ -293,6 +352,25 @@ export class NeoBankService {
return mapNeoBankAutorampToRemoteSnapshot(response);
}

#mapTransactionsResponse(
response: NeoBankTransactionsResponse,
): MoneyAccountDepositRemoteSnapshot[] {
const list = Array.isArray(response) ? response : response?.transactions;
if (!Array.isArray(list)) {
throw new Error(
'Malformed response received from neo-bank transactions API',
);
}
return list.map((item) => {
if (!item || typeof item !== 'object' || !item.id || !item.status) {
throw new Error(
'Malformed response received from neo-bank transactions API',
);
}
return mapNeoBankTransactionToRemoteSnapshot(item);
});
}

/**
* Fetches an autoramp account via neobank-proxy
* `GET /neobank/autoramps/{autoramp_id}` (MoonPay
Expand All @@ -308,6 +386,26 @@ export class NeoBankService {
return this.#mapAutorampResponse(response);
}

/**
* Fetches deposit/transaction records for an autoramp via neobank-proxy
* `GET /neobank/autoramps/{autoramp_id}/transactions`.
*
* Used by the deposit poller to detect status changes (e.g. a payout settling
* on Monad). Route + response shape are assumed pending the proxy contract
* (onramp-api #1124).
*
* @param autorampId - MoonPay / Ramp API autoramp id.
* @returns Deposit snapshots for controller apply/refresh.
*/
async getAutorampTransactions(
autorampId: string,
): Promise<MoneyAccountDepositRemoteSnapshot[]> {
const response = await this.#getJson<NeoBankTransactionsResponse>(
`autoramps/${encodeURIComponent(autorampId)}/transactions`,
);
return this.#mapTransactionsResponse(response);
}

/**
* Registers a Pix address via neobank-proxy `POST /neobank/addresses/pix`.
* Body is forwarded as opaque JSON (MoonPay address schema).
Expand Down
Loading
Loading