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
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ export * from './block-tracker-inspector';
export * from './fetch';
export * from './inflight-cache';
export type {
GetCallsStatusHook,
GetCallsStatusParams,
GetCallsStatusReceipt,
GetCallsStatusResult,
GetTransactionReceiptsByBatchIdHook,
} from './methods/wallet-get-calls-status';
export { GetCallsStatusCode } from './methods/wallet-get-calls-status';
export type {
GetCapabilitiesHook,
GetCapabilitiesParams,
Expand All @@ -19,6 +19,7 @@ export type {
ProcessSendCallsHook,
SendCalls,
SendCallsParams,
SendCallsResult,
} from './methods/wallet-send-calls';
export * from './providerAsMiddleware';
export * from './retryOnEmpty';
Expand Down
83 changes: 30 additions & 53 deletions src/methods/wallet-get-calls-status.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
import type {
Hex,
JsonRpcRequest,
PendingJsonRpcResponse,
} from '@metamask/utils';
import { klona } from 'klona';

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

const ID_MOCK = '1234-5678';
const ID_MOCK = '0x12345678';

const RECEIPT_MOCK = {
logs: [
Expand All @@ -30,15 +34,23 @@ const REQUEST_MOCK = {
params: [ID_MOCK],
} as unknown as JsonRpcRequest<GetCallsStatusParams>;

const RESULT_MOCK = {
version: '1.0',
id: ID_MOCK,
chainId: '0x1',
status: 1,
receipts: [RECEIPT_MOCK, RECEIPT_MOCK],
};

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

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

Expand All @@ -49,48 +61,17 @@ describe('wallet_getCallsStatus', () => {
params = request.params as GetCallsStatusParams;
response = {} as PendingJsonRpcResponse<GetCallsStatusResult>;

getTransactionReceiptsByBatchIdMock = jest
.fn()
.mockResolvedValue([RECEIPT_MOCK, RECEIPT_MOCK]);
getCallsStatusMock = jest.fn().mockResolvedValue(RESULT_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,
]);
expect(getCallsStatusMock).toHaveBeenCalledWith(params[0], request);
});

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

it('returns result from hook', async () => {
await callMethod();
expect(response.result).toBeNull();
expect(response.result).toStrictEqual(RESULT_MOCK);
});

it('throws if no hook', async () => {
Expand Down Expand Up @@ -119,27 +100,23 @@ describe('wallet_getCallsStatus', () => {
`);
});

it('throws if empty', async () => {
params[0] = '';
it('throws if address is not hex', async () => {
params[0] = '123' as Hex;

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

0 - Expected a nonempty string but received an empty one]
0 - Expected a string matching \`/^0x[0-9a-f]+$/\` but received "123"]
`);
});

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

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

expect(response.result?.receipts).toStrictEqual([RECEIPT_MOCK]);
0 - Expected a string matching \`/^0x[0-9a-f]+$/\` but received ""]
`);
});
});
88 changes: 36 additions & 52 deletions src/methods/wallet-get-calls-status.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,69 @@
import { rpcErrors } from '@metamask/rpc-errors';
import type { Infer } from '@metamask/superstruct';
import {
nonempty,
optional,
mask,
string,
array,
object,
tuple,
} from '@metamask/superstruct';
import { tuple } from '@metamask/superstruct';
import type {
Hex,
Json,
JsonRpcRequest,
PendingJsonRpcResponse,
} from '@metamask/utils';
import { HexChecksumAddressStruct, StrictHexStruct } from '@metamask/utils';
import { StrictHexStruct } from '@metamask/utils';

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

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

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 enum GetCallsStatusCode {
PENDING = 100,
CONFIRMED = 200,
FAILED_OFFCHAIN = 400,
REVERTED = 500,
REVERTED_PARTIAL = 600,
}

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

export type GetCallsStatusResult = {
status: 'PENDING' | 'CONFIRMED';
receipts?: GetCallsStatusReceipt[];
version: string;
id: Hex;
chainId: Hex;
status: number;
receipts?: {
logs: {
address: Hex;
data: Hex;
topics: Hex[];
}[];
status: '0x0' | '0x1';
blockHash: Hex;
blockNumber: Hex;
gasUsed: Hex;
transactionHash: Hex;
}[];
capabilities?: Record<string, Json>;
};

export type GetTransactionReceiptsByBatchIdHook = (
batchId: string,
export type GetCallsStatusHook = (
id: GetCallsStatusParams[0],
req: JsonRpcRequest,
) => Promise<GetCallsStatusReceipt[]>;
) => Promise<GetCallsStatusResult>;

export async function walletGetCallsStatus(
req: JsonRpcRequest,
res: PendingJsonRpcResponse<Json>,
{
getTransactionReceiptsByBatchId,
getCallsStatus,
}: {
getTransactionReceiptsByBatchId?: GetTransactionReceiptsByBatchIdHook;
getCallsStatus?: GetCallsStatusHook;
},
): Promise<void> {
if (!getTransactionReceiptsByBatchId) {
if (!getCallsStatus) {
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;
const id = req.params[0];

res.result = { status, receipts };
res.result = await getCallsStatus(id, req);
}
27 changes: 22 additions & 5 deletions src/methods/wallet-get-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
import { walletGetCapabilities } from './wallet-get-capabilities';

const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a';
const CHAIN_ID_MOCK = '0x1';
const CHAIN_ID_2_MOCK = '0x2';

const RESULT_MOCK = {
testCapability: {
Expand All @@ -18,10 +20,10 @@ const RESULT_MOCK = {

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

describe('wallet_getCapabilities', () => {
let request: JsonRpcRequest<GetCapabilitiesParams>;
let request: JsonRpcRequest;
let params: GetCapabilitiesParams;
let response: PendingJsonRpcResponse<GetCapabilitiesResult>;
let getCapabilitiesMock: jest.MockedFunction<GetCapabilitiesHook>;
Expand All @@ -35,7 +37,7 @@ describe('wallet_getCapabilities', () => {
beforeEach(() => {
jest.resetAllMocks();

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

Expand All @@ -44,12 +46,27 @@ describe('wallet_getCapabilities', () => {

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

it('returns capabilities from hook', async () => {
it('calls hook with chain IDs', async () => {
request.params = [ADDRESS_MOCK, [CHAIN_ID_MOCK, CHAIN_ID_2_MOCK]];

await callMethod();

expect(getCapabilitiesMock).toHaveBeenCalledWith(
params[0],
[CHAIN_ID_MOCK, CHAIN_ID_2_MOCK],
request,
);
});

it('returns capabilities from hook', async () => {
await callMethod();
expect(response.result).toStrictEqual(RESULT_MOCK);
});

Expand Down
15 changes: 10 additions & 5 deletions src/methods/wallet-get-capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { rpcErrors } from '@metamask/rpc-errors';
import type { Infer } from '@metamask/superstruct';
import { tuple } from '@metamask/superstruct';
import { array, optional, tuple } from '@metamask/superstruct';
import type {
Hex,
Json,
JsonRpcRequest,
PendingJsonRpcResponse,
} from '@metamask/utils';
import { HexChecksumAddressStruct } from '@metamask/utils';
import { StrictHexStruct, HexChecksumAddressStruct } from '@metamask/utils';

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

const GetCapabilitiesStruct = tuple([HexChecksumAddressStruct]);
const GetCapabilitiesStruct = tuple([
HexChecksumAddressStruct,
optional(array(StrictHexStruct)),
]);

export type GetCapabilitiesParams = Infer<typeof GetCapabilitiesStruct>;
export type GetCapabilitiesResult = Record<Hex, Record<string, Json>>;

export type GetCapabilitiesHook = (
address: Hex,
address: GetCapabilitiesParams[0],
chainIds: GetCapabilitiesParams[1],
req: JsonRpcRequest,
) => Promise<GetCapabilitiesResult>;

Expand All @@ -37,7 +41,8 @@ export async function walletGetCapabilities(
validateParams(req.params, GetCapabilitiesStruct);

const address = req.params[0];
const capabilities = await getCapabilities(address, req);
const chainIds = req.params[1];
const capabilities = await getCapabilities(address, chainIds, req);

res.result = capabilities;
}
Loading
Loading