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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const USER_STORAGE_FEATURE_NAMES = {
notifications: 'notifications',
accounts: 'accounts_v2',
addressBook: 'addressBook',
rampsAutoramps: 'rampsAutoramps',
};

export type UserStorageGenericFeatureName = string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* This file is auto generated.
* Do not edit manually.
*/

import type { NeoBankService } from './NeoBankService.js';

/**
* Fetches an autoramp account via the Ramp API proxy of
* MoonPay `GET /api/autoramps/{autoramp_id}`.
*
* @param autorampId - MoonPay / Ramp API autoramp id.
* @returns Remote snapshot for controller apply/refresh.
*/
export type NeoBankServiceGetAutorampAction = {
type: `NeoBankService:getAutoramp`;
handler: NeoBankService['getAutoramp'];
};

/**
* Union of all NeoBankService action types.
*/
export type NeoBankServiceMethodActions = NeoBankServiceGetAutorampAction;
80 changes: 80 additions & 0 deletions packages/ramps-controller/src/NeoBankService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import nock from 'nock';

import {
mapNeoBankAutorampToRemoteSnapshot,
NeoBankService,
} from './NeoBankService.js';
import type { NeoBankServiceMessenger } from './NeoBankService.js';
import { RampsEnvironment } from './RampsService.js';
import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger';
import type { MockAnyNamespace } from '@metamask/messenger';

describe('NeoBankService', () => {
describe('mapNeoBankAutorampToRemoteSnapshot', () => {
it('maps MoonPay-shaped fields into a remote snapshot', () => {
expect(
mapNeoBankAutorampToRemoteSnapshot({
id: 'ar-1',
customer_id: 'cust-1',
status: 'Approved',
wallet_address: '0xabc',
deposit_rails: [{ type: 'Iban' }],
}),
).toStrictEqual({
id: 'ar-1',
customerId: 'cust-1',
walletAddress: '0xabc',
status: 'Approved',
depositRailsSummary: { ready: true },
});
});
});

describe('getAutoramp', () => {
it('GETs the proxied autoramp endpoint with bearer auth', async () => {

Check failure on line 34 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 rootMessenger = new Messenger({
namespace: MOCK_ANY_NAMESPACE as MockAnyNamespace,
});
rootMessenger.registerActionHandler(
'AuthenticationController:getBearerToken',
async () => 'test-token',
);

const messenger = new Messenger({
namespace: 'NeoBankService',
parent: rootMessenger,
}) as unknown as NeoBankServiceMessenger;
rootMessenger.delegate({
messenger,
actions: ['AuthenticationController:getBearerToken'],
});

const scope = nock('https://on-ramp.uat-api.cx.metamask.io')
.get(/\/api\/v2\/autoramps\/ar-1/u)
.matchHeader('Authorization', 'Bearer test-token')
.reply(200, {
id: 'ar-1',
customer_id: 'cust-1',
status: 'Authorized',
wallet_address: '0xabc',
});

const service = new NeoBankService({
messenger,
environment: RampsEnvironment.Staging,
context: 'test',
fetch: globalThis.fetch.bind(globalThis),
});

const snapshot = await service.getAutoramp('ar-1');

expect(scope.isDone()).toBe(true);
expect(snapshot).toMatchObject({
id: 'ar-1',
customerId: 'cust-1',
status: 'Authorized',
walletAddress: '0xabc',
});
});
});
});
246 changes: 246 additions & 0 deletions packages/ramps-controller/src/NeoBankService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import type {
CreateServicePolicyOptions,
ServicePolicy,
} from '@metamask/controller-utils';
import { createServicePolicy, HttpError } from '@metamask/controller-utils';
import type { Messenger } from '@metamask/messenger';
import type { AuthenticationController } from '@metamask/profile-sync-controller';

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

/**
* Name of the NeoBankService messenger namespace.
*/
export const serviceName = 'NeoBankService';

/**
* Raw autoramp payload from the MetaMask Ramp API neo-bank proxy.
* Shape mirrors MoonPay Enterprise `GET /api/autoramps/{autoramp_id}`.
* The Ramp API handles partner auth / headers; the client only sends the
* MetaMask bearer token.
*/
export type NeoBankAutorampResponse = {
id: string;
customer_id: string;

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

View workflow job for this annotation

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

Type Property name `customer_id` must match one of the following formats: camelCase
status: string;
/**
* Destination wallet when present on the proxy response.
* Field name may evolve with the Ramp API contract.
*/
wallet_address?: string;

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

View workflow job for this annotation

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

Type Property name `wallet_address` must match one of the following formats: camelCase
recipient_account?: {

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

View workflow job for this annotation

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

Type Property name `recipient_account` must match one of the following formats: camelCase
address?: string;
};
deposit_rails?: unknown[];

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

View workflow job for this annotation

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

Type Property name `deposit_rails` must match one of the following formats: camelCase
};

const MESSENGER_EXPOSED_METHODS = ['getAutoramp'] as const;

/**
* Actions that {@link NeoBankService} exposes to other consumers.
*/
export type NeoBankServiceActions = NeoBankServiceMethodActions;

type AllowedActions =
AuthenticationController.AuthenticationControllerGetBearerTokenAction;

export type NeoBankServiceEvents = never;

type AllowedEvents = never;

/**
* The messenger restricted to actions and events accessed by
* {@link NeoBankService}.
*/
export type NeoBankServiceMessenger = Messenger<
typeof serviceName,
NeoBankServiceActions | AllowedActions,
NeoBankServiceEvents | AllowedEvents
>;

/**
* Builds an `/api/v2/...` path for the Ramp API neo-bank proxy.
*
* @param path - Path under the versioned API root (no leading slash).
* @param version - API version segment.
* @returns Versioned API path.
*/
function getApiPath(path: string, version: string = 'v2'): string {
return `api/${version}/${path.replace(/^\//u, '')}`;
}

/**
* Resolves the Ramp API host for neo-bank calls (same hosts as {@link RampsService}).
*
* @param environment - Ramp environment.
* @returns Base URL.
*/
function getBaseUrl(environment: RampsEnvironment): string {
switch (environment) {
case RampsEnvironment.Production:
return 'https://on-ramp.api.cx.metamask.io';
case RampsEnvironment.Staging:
return 'https://on-ramp.uat-api.cx.metamask.io';
case RampsEnvironment.Development:
return 'https://on-ramp.dev-api.cx.metamask.io';
case RampsEnvironment.Local:
return 'http://localhost:3000';
default:
throw new Error(`Invalid environment: ${String(environment)}`);
}
}

/**
* Maps a Ramp API / MoonPay-shaped autoramp response into the local remote snapshot.
*
* @param response - Proxy response body.
* @returns Snapshot consumed by {@link applyAutorampRemoteStatus}.
*/
export function mapNeoBankAutorampToRemoteSnapshot(
response: NeoBankAutorampResponse,
): AutorampRemoteSnapshot {
const depositRails = response.deposit_rails;
const hasDepositRails = Array.isArray(depositRails) && depositRails.length > 0;
const depositRailsSummary: AutorampDepositRailsSummary | undefined =
hasDepositRails || response.status === 'Approved'
? {
ready: response.status === 'Approved' && hasDepositRails,
}
: undefined;

return {
id: response.id,
customerId: response.customer_id,
walletAddress:
response.wallet_address ?? response.recipient_account?.address,
status: response.status,
depositRailsSummary,
};
}

/**
* Client for MetaMask Ramp API neo-bank endpoints (MoonPay Enterprise proxy).
*
* Lives alongside {@link RampsService} and {@link TransakService}. Authentication
* and MoonPay partner headers are handled by the Ramp API — this service only
* attaches the MetaMask user bearer token.
*/
export class NeoBankService {
readonly name: typeof serviceName;

readonly #messenger: NeoBankServiceMessenger;

readonly #fetch: typeof fetch;

readonly #policy: ServicePolicy;

readonly #environment: RampsEnvironment;

readonly #context: string;

readonly #baseUrlOverride?: string;

constructor({
messenger,
environment = RampsEnvironment.Staging,
context,
fetch: fetchFunction,
policyOptions = {},
baseUrlOverride,
}: {
messenger: NeoBankServiceMessenger;
environment?: RampsEnvironment;
context: string;
fetch: typeof fetch;
policyOptions?: CreateServicePolicyOptions;
baseUrlOverride?: string;
}) {
this.name = serviceName;
this.#messenger = messenger;
this.#fetch = fetchFunction;
this.#policy = createServicePolicy(policyOptions);
this.#environment = environment;
this.#context = context;
this.#baseUrlOverride = baseUrlOverride;

this.#messenger.registerMethodActionHandlers(
this,
MESSENGER_EXPOSED_METHODS,
);
}

#getBaseUrl(): string {
if (this.#baseUrlOverride) {
return this.#baseUrlOverride;
}
return getBaseUrl(this.#environment);
}

async #getRequestHeaders(): Promise<Record<string, string>> {
const bearerToken = await this.#messenger.call(
'AuthenticationController:getBearerToken',
);
return {
Authorization: `Bearer ${bearerToken}`,
};
}

/**
* Fetches an autoramp account via the Ramp API proxy of
* MoonPay `GET /api/autoramps/{autoramp_id}`.
*
* @param autorampId - MoonPay / Ramp API autoramp id.
* @returns Remote snapshot for controller apply/refresh.
*/
async getAutoramp(autorampId: string): Promise<AutorampRemoteSnapshot> {
const url = new URL(
getApiPath(`autoramps/${encodeURIComponent(autorampId)}`),
this.#getBaseUrl(),
);
url.searchParams.set('sdk', RAMPS_SDK_VERSION);
url.searchParams.set('controller', packageJson.version);
url.searchParams.set('context', this.#context);

const response = await this.#policy.execute(async () => {
const headers = await this.#getRequestHeaders();
const fetchResponse = await this.#fetch(url, { headers });
if (!fetchResponse.ok) {
throw new HttpError(
fetchResponse.status,
`Fetching '${url.toString()}' failed with status '${fetchResponse.status}'`,
);
}
return fetchResponse.json() as Promise<NeoBankAutorampResponse>;
});

if (!response || typeof response !== 'object' || !response.id) {
throw new Error('Malformed response received from neo-bank autoramp API');
}

return mapNeoBankAutorampToRemoteSnapshot(response);
}

onRetry(
listener: Parameters<ServicePolicy['onRetry']>[0],
): ReturnType<ServicePolicy['onRetry']> {
return this.#policy.onRetry(listener);
}

onBreak(
listener: Parameters<ServicePolicy['onBreak']>[0],
): ReturnType<ServicePolicy['onBreak']> {
return this.#policy.onBreak(listener);
}

onDegraded(
listener: Parameters<ServicePolicy['onDegraded']>[0],
): ReturnType<ServicePolicy['onDegraded']> {
return this.#policy.onDegraded(listener);
}
}
Loading
Loading