Skip to content
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@metamask/eth-sig-util": "^8.1.2",
"@metamask/json-rpc-engine": "^10.0.2",
"@metamask/rpc-errors": "^7.0.2",
"@metamask/superstruct": "^3.1.0",
"@metamask/utils": "^11.1.0",
"@types/bn.js": "^5.1.5",
"bn.js": "^5.2.1",
Expand Down
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ export * from './block-ref';
export * from './block-tracker-inspector';
export * from './fetch';
export * from './inflight-cache';
export type {
GetCallsStatusParams,
GetCallsStatusReceipt,
GetCallsStatusResult,
GetTransactionReceiptsByBatchIdHook,
} from './methods/wallet-get-calls-status';
export type {
GetCapabilitiesHook,
GetCapabilitiesParams,
GetCapabilitiesResult,
} from './methods/wallet-get-capabilities';
export type {
ProcessSendCallsHook,
SendCalls,
SendCallsParams,
} from './methods/wallet-send-calls';
export * from './providerAsMiddleware';
export * from './retryOnEmpty';
export * from './wallet';
145 changes: 145 additions & 0 deletions src/methods/wallet-get-calls-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
import { klona } from 'klona';

import type {
GetCallsStatusParams,
GetCallsStatusResult,
GetTransactionReceiptsByBatchIdHook,
} from './wallet-get-calls-status';
import { walletGetCallsStatus } from './wallet-get-calls-status';

const ID_MOCK = '1234-5678';

const RECEIPT_MOCK = {
logs: [
{
address: '0x123abc123abc123abc123abc123abc123abc123a',
data: '0x123abc',
topics: ['0x123abc'],
},
],
status: '0x1',
chainId: '0x1',
blockHash: '0x123abc',
blockNumber: '0x1',
gasUsed: '0x1',
transactionHash: '0x123abc',
};

const REQUEST_MOCK = {
params: [ID_MOCK],
} as unknown as JsonRpcRequest<GetCallsStatusParams>;

describe('wallet_getCallsStatus', () => {
let request: JsonRpcRequest<GetCallsStatusParams>;
let params: GetCallsStatusParams;
let response: PendingJsonRpcResponse<GetCallsStatusResult>;
let getTransactionReceiptsByBatchIdMock: jest.MockedFunction<GetTransactionReceiptsByBatchIdHook>;

async function callMethod() {
return walletGetCallsStatus(request, response, {
getTransactionReceiptsByBatchId: getTransactionReceiptsByBatchIdMock,
});
}

beforeEach(() => {
jest.resetAllMocks();

request = klona(REQUEST_MOCK);
params = request.params as GetCallsStatusParams;
response = {} as PendingJsonRpcResponse<GetCallsStatusResult>;

getTransactionReceiptsByBatchIdMock = jest
.fn()
.mockResolvedValue([RECEIPT_MOCK, RECEIPT_MOCK]);
});

it('calls hook', async () => {
await callMethod();
expect(getTransactionReceiptsByBatchIdMock).toHaveBeenCalledWith(
params[0],
request,
);
});

it('returns confirmed status if all receipts available', async () => {
await callMethod();
expect(response.result?.status).toBe('CONFIRMED');
});

it('returns pending status if missing receipts', async () => {
getTransactionReceiptsByBatchIdMock = jest
.fn()
.mockResolvedValue([RECEIPT_MOCK, undefined]);

await callMethod();
expect(response.result?.status).toBe('PENDING');
expect(response.result?.receipts).toBeNull();
});

it('returns receipts', async () => {
await callMethod();

expect(response.result?.receipts).toStrictEqual([
RECEIPT_MOCK,
RECEIPT_MOCK,
]);
});

it('returns null if no receipts', async () => {
getTransactionReceiptsByBatchIdMock = jest.fn().mockResolvedValue([]);

await callMethod();
expect(response.result).toBeNull();
});

it('throws if no hook', async () => {
await expect(
walletGetCallsStatus(request, response, {}),
).rejects.toMatchInlineSnapshot(`[Error: Method not supported.]`);
});

it('throws if no params', async () => {
request.params = undefined;

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

Expected an array, but received: undefined]
`);
});

it('throws if wrong type', async () => {
params[0] = 123 as never;

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

0 - Expected a string, but received: 123]
`);
});

it('throws if empty', async () => {
params[0] = '';

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

0 - Expected a nonempty string but received an empty one]
`);
});

it('removes excess properties from receipts', async () => {
getTransactionReceiptsByBatchIdMock.mockResolvedValue([
{
...RECEIPT_MOCK,
extra: 'value1',
logs: [{ ...RECEIPT_MOCK.logs[0], extra2: 'value2' }],
} as never,
]);

await callMethod();

expect(response.result?.receipts).toStrictEqual([RECEIPT_MOCK]);
});
});
85 changes: 85 additions & 0 deletions src/methods/wallet-get-calls-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { rpcErrors } from '@metamask/rpc-errors';
import type { Infer } from '@metamask/superstruct';
import {
nonempty,
optional,
mask,
string,
array,
object,
tuple,
} from '@metamask/superstruct';
import type {
Json,
JsonRpcRequest,
PendingJsonRpcResponse,
} from '@metamask/utils';
import { HexChecksumAddressStruct, StrictHexStruct } from '@metamask/utils';

import { validateParams } from '../utils/validation';

const GetCallsStatusStruct = tuple([nonempty(string())]);

const GetCallsStatusReceiptStruct = object({
logs: optional(
array(
object({
address: optional(HexChecksumAddressStruct),
data: optional(StrictHexStruct),
topics: optional(array(StrictHexStruct)),
}),
),
),
status: optional(StrictHexStruct),
chainId: optional(StrictHexStruct),
blockHash: optional(StrictHexStruct),
blockNumber: optional(StrictHexStruct),
gasUsed: optional(StrictHexStruct),
transactionHash: optional(StrictHexStruct),
});

export type GetCallsStatusParams = Infer<typeof GetCallsStatusStruct>;
export type GetCallsStatusReceipt = Infer<typeof GetCallsStatusReceiptStruct>;

export type GetCallsStatusResult = {
status: 'PENDING' | 'CONFIRMED';
receipts?: GetCallsStatusReceipt[];
};

export type GetTransactionReceiptsByBatchIdHook = (
batchId: string,
req: JsonRpcRequest,
) => Promise<GetCallsStatusReceipt[]>;

export async function walletGetCallsStatus(
req: JsonRpcRequest,
res: PendingJsonRpcResponse<Json>,
{
getTransactionReceiptsByBatchId,
}: {
getTransactionReceiptsByBatchId?: GetTransactionReceiptsByBatchIdHook;
},
): Promise<void> {
if (!getTransactionReceiptsByBatchId) {
throw rpcErrors.methodNotSupported();
}

validateParams(req.params, GetCallsStatusStruct);

const batchId = req.params[0];
const rawReceipts = await getTransactionReceiptsByBatchId(batchId, req);

if (!rawReceipts.length) {
res.result = null;
return;
}

const isComplete = rawReceipts.every((receipt) => Boolean(receipt));
const status = isComplete ? 'CONFIRMED' : 'PENDING';

const receipts = isComplete
? rawReceipts.map((receipt) => mask(receipt, GetCallsStatusReceiptStruct))
: null;

res.result = { status, receipts };
}
101 changes: 101 additions & 0 deletions src/methods/wallet-get-capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
import { klona } from 'klona';

import type {
GetCapabilitiesHook,
GetCapabilitiesParams,
GetCapabilitiesResult,
} from './wallet-get-capabilities';
import { walletGetCapabilities } from './wallet-get-capabilities';

const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a';

const RESULT_MOCK = {
testCapability: {
testKey: 'testValue',
},
};

const REQUEST_MOCK = {
params: [ADDRESS_MOCK],
} as unknown as JsonRpcRequest<GetCapabilitiesParams>;

describe('wallet_getCapabilities', () => {
let request: JsonRpcRequest<GetCapabilitiesParams>;
let params: GetCapabilitiesParams;
let response: PendingJsonRpcResponse<GetCapabilitiesResult>;
let getCapabilitiesMock: jest.MockedFunction<GetCapabilitiesHook>;

async function callMethod() {
return walletGetCapabilities(request, response, {
getCapabilities: getCapabilitiesMock,
});
}

beforeEach(() => {
jest.resetAllMocks();

request = klona(REQUEST_MOCK);
params = request.params as GetCapabilitiesParams;
response = {} as PendingJsonRpcResponse<GetCapabilitiesResult>;

getCapabilitiesMock = jest.fn().mockResolvedValue(RESULT_MOCK);
});

it('calls hook', async () => {
await callMethod();
expect(getCapabilitiesMock).toHaveBeenCalledWith(params[0], request);
});

it('returns capabilities from hook', async () => {
await callMethod();

expect(response.result).toStrictEqual(RESULT_MOCK);
});

it('throws if no hook', async () => {
await expect(
walletGetCapabilities(request, response, {}),
).rejects.toMatchInlineSnapshot(`[Error: Method not supported.]`);
});

it('throws if no params', async () => {
request.params = undefined;

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

Expected an array, but received: undefined]
`);
});

it('throws if wrong type', async () => {
params[0] = 123 as never;

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

0 - Expected a string, but received: 123]
`);
});

it('throws if not hex', async () => {
params[0] = 'test' as never;

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

0 - Expected a string matching \`/^0x[0-9a-fA-F]{40}$/\` but received "test"]
`);
});

it('throws if wrong length', async () => {
params[0] = '0x123' as never;

await expect(callMethod()).rejects.toMatchInlineSnapshot(`
[Error: Invalid params

0 - Expected a string matching \`/^0x[0-9a-fA-F]{40}$/\` but received "0x123"]
`);
});
});
Loading
Loading