Skip to content

Commit e601069

Browse files
feat: support updated EIP-5792 specification (#363)
Update the EIP-5792 support following changes to the specification. Specifically: - `wallet_sendCalls` - Support optional custom `id`. - Support optional `capabilities` per call. - Support `optional` property in capabilities. - Return object containing `id` and optional `capabilities`. - `wallet_getCallsStatus` - Expect hexadecimal `id`. - Add `id`, `version`, and optional `capabilities` to result. - Change `status` to number. - Provider current codes in `GetCallsStatusCode` enum. - Use more generic `GetCallsStatusHook`. - `wallet_getCapabilities` - Support optional `chainIds` argument.
1 parent 29b4007 commit e601069

8 files changed

+171
-132
lines changed

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ export * from './block-tracker-inspector';
55
export * from './fetch';
66
export * from './inflight-cache';
77
export type {
8+
GetCallsStatusHook,
89
GetCallsStatusParams,
9-
GetCallsStatusReceipt,
1010
GetCallsStatusResult,
11-
GetTransactionReceiptsByBatchIdHook,
1211
} from './methods/wallet-get-calls-status';
12+
export { GetCallsStatusCode } from './methods/wallet-get-calls-status';
1313
export type {
1414
GetCapabilitiesHook,
1515
GetCapabilitiesParams,
@@ -19,6 +19,7 @@ export type {
1919
ProcessSendCallsHook,
2020
SendCalls,
2121
SendCallsParams,
22+
SendCallsResult,
2223
} from './methods/wallet-send-calls';
2324
export * from './providerAsMiddleware';
2425
export * from './retryOnEmpty';
Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
1+
import type {
2+
Hex,
3+
JsonRpcRequest,
4+
PendingJsonRpcResponse,
5+
} from '@metamask/utils';
26
import { klona } from 'klona';
37

48
import type {
9+
GetCallsStatusHook,
510
GetCallsStatusParams,
611
GetCallsStatusResult,
7-
GetTransactionReceiptsByBatchIdHook,
812
} from './wallet-get-calls-status';
913
import { walletGetCallsStatus } from './wallet-get-calls-status';
1014

11-
const ID_MOCK = '1234-5678';
15+
const ID_MOCK = '0x12345678';
1216

1317
const RECEIPT_MOCK = {
1418
logs: [
@@ -30,15 +34,23 @@ const REQUEST_MOCK = {
3034
params: [ID_MOCK],
3135
} as unknown as JsonRpcRequest<GetCallsStatusParams>;
3236

37+
const RESULT_MOCK = {
38+
version: '1.0',
39+
id: ID_MOCK,
40+
chainId: '0x1',
41+
status: 1,
42+
receipts: [RECEIPT_MOCK, RECEIPT_MOCK],
43+
};
44+
3345
describe('wallet_getCallsStatus', () => {
3446
let request: JsonRpcRequest<GetCallsStatusParams>;
3547
let params: GetCallsStatusParams;
3648
let response: PendingJsonRpcResponse<GetCallsStatusResult>;
37-
let getTransactionReceiptsByBatchIdMock: jest.MockedFunction<GetTransactionReceiptsByBatchIdHook>;
49+
let getCallsStatusMock: jest.MockedFunction<GetCallsStatusHook>;
3850

3951
async function callMethod() {
4052
return walletGetCallsStatus(request, response, {
41-
getTransactionReceiptsByBatchId: getTransactionReceiptsByBatchIdMock,
53+
getCallsStatus: getCallsStatusMock,
4254
});
4355
}
4456

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

52-
getTransactionReceiptsByBatchIdMock = jest
53-
.fn()
54-
.mockResolvedValue([RECEIPT_MOCK, RECEIPT_MOCK]);
64+
getCallsStatusMock = jest.fn().mockResolvedValue(RESULT_MOCK);
5565
});
5666

5767
it('calls hook', async () => {
5868
await callMethod();
59-
expect(getTransactionReceiptsByBatchIdMock).toHaveBeenCalledWith(
60-
params[0],
61-
request,
62-
);
63-
});
64-
65-
it('returns confirmed status if all receipts available', async () => {
66-
await callMethod();
67-
expect(response.result?.status).toBe('CONFIRMED');
68-
});
69-
70-
it('returns pending status if missing receipts', async () => {
71-
getTransactionReceiptsByBatchIdMock = jest
72-
.fn()
73-
.mockResolvedValue([RECEIPT_MOCK, undefined]);
74-
75-
await callMethod();
76-
expect(response.result?.status).toBe('PENDING');
77-
expect(response.result?.receipts).toBeNull();
78-
});
79-
80-
it('returns receipts', async () => {
81-
await callMethod();
82-
83-
expect(response.result?.receipts).toStrictEqual([
84-
RECEIPT_MOCK,
85-
RECEIPT_MOCK,
86-
]);
69+
expect(getCallsStatusMock).toHaveBeenCalledWith(params[0], request);
8770
});
8871

89-
it('returns null if no receipts', async () => {
90-
getTransactionReceiptsByBatchIdMock = jest.fn().mockResolvedValue([]);
91-
72+
it('returns result from hook', async () => {
9273
await callMethod();
93-
expect(response.result).toBeNull();
74+
expect(response.result).toStrictEqual(RESULT_MOCK);
9475
});
9576

9677
it('throws if no hook', async () => {
@@ -119,27 +100,23 @@ describe('wallet_getCallsStatus', () => {
119100
`);
120101
});
121102

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

125106
await expect(callMethod()).rejects.toMatchInlineSnapshot(`
126107
[Error: Invalid params
127108
128-
0 - Expected a nonempty string but received an empty one]
109+
0 - Expected a string matching \`/^0x[0-9a-f]+$/\` but received "123"]
129110
`);
130111
});
131112

132-
it('removes excess properties from receipts', async () => {
133-
getTransactionReceiptsByBatchIdMock.mockResolvedValue([
134-
{
135-
...RECEIPT_MOCK,
136-
extra: 'value1',
137-
logs: [{ ...RECEIPT_MOCK.logs[0], extra2: 'value2' }],
138-
} as never,
139-
]);
113+
it('throws if address is empty', async () => {
114+
params[0] = '' as never;
140115

141-
await callMethod();
116+
await expect(callMethod()).rejects.toMatchInlineSnapshot(`
117+
[Error: Invalid params
142118
143-
expect(response.result?.receipts).toStrictEqual([RECEIPT_MOCK]);
119+
0 - Expected a string matching \`/^0x[0-9a-f]+$/\` but received ""]
120+
`);
144121
});
145122
});
Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,69 @@
11
import { rpcErrors } from '@metamask/rpc-errors';
22
import type { Infer } from '@metamask/superstruct';
3-
import {
4-
nonempty,
5-
optional,
6-
mask,
7-
string,
8-
array,
9-
object,
10-
tuple,
11-
} from '@metamask/superstruct';
3+
import { tuple } from '@metamask/superstruct';
124
import type {
5+
Hex,
136
Json,
147
JsonRpcRequest,
158
PendingJsonRpcResponse,
169
} from '@metamask/utils';
17-
import { HexChecksumAddressStruct, StrictHexStruct } from '@metamask/utils';
10+
import { StrictHexStruct } from '@metamask/utils';
1811

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

21-
const GetCallsStatusStruct = tuple([nonempty(string())]);
14+
const GetCallsStatusStruct = tuple([StrictHexStruct]);
2215

23-
const GetCallsStatusReceiptStruct = object({
24-
logs: optional(
25-
array(
26-
object({
27-
address: optional(HexChecksumAddressStruct),
28-
data: optional(StrictHexStruct),
29-
topics: optional(array(StrictHexStruct)),
30-
}),
31-
),
32-
),
33-
status: optional(StrictHexStruct),
34-
chainId: optional(StrictHexStruct),
35-
blockHash: optional(StrictHexStruct),
36-
blockNumber: optional(StrictHexStruct),
37-
gasUsed: optional(StrictHexStruct),
38-
transactionHash: optional(StrictHexStruct),
39-
});
16+
export enum GetCallsStatusCode {
17+
PENDING = 100,
18+
CONFIRMED = 200,
19+
FAILED_OFFCHAIN = 400,
20+
REVERTED = 500,
21+
REVERTED_PARTIAL = 600,
22+
}
4023

4124
export type GetCallsStatusParams = Infer<typeof GetCallsStatusStruct>;
42-
export type GetCallsStatusReceipt = Infer<typeof GetCallsStatusReceiptStruct>;
4325

4426
export type GetCallsStatusResult = {
45-
status: 'PENDING' | 'CONFIRMED';
46-
receipts?: GetCallsStatusReceipt[];
27+
version: string;
28+
id: Hex;
29+
chainId: Hex;
30+
status: number;
31+
receipts?: {
32+
logs: {
33+
address: Hex;
34+
data: Hex;
35+
topics: Hex[];
36+
}[];
37+
status: '0x0' | '0x1';
38+
blockHash: Hex;
39+
blockNumber: Hex;
40+
gasUsed: Hex;
41+
transactionHash: Hex;
42+
}[];
43+
capabilities?: Record<string, Json>;
4744
};
4845

49-
export type GetTransactionReceiptsByBatchIdHook = (
50-
batchId: string,
46+
export type GetCallsStatusHook = (
47+
id: GetCallsStatusParams[0],
5148
req: JsonRpcRequest,
52-
) => Promise<GetCallsStatusReceipt[]>;
49+
) => Promise<GetCallsStatusResult>;
5350

5451
export async function walletGetCallsStatus(
5552
req: JsonRpcRequest,
5653
res: PendingJsonRpcResponse<Json>,
5754
{
58-
getTransactionReceiptsByBatchId,
55+
getCallsStatus,
5956
}: {
60-
getTransactionReceiptsByBatchId?: GetTransactionReceiptsByBatchIdHook;
57+
getCallsStatus?: GetCallsStatusHook;
6158
},
6259
): Promise<void> {
63-
if (!getTransactionReceiptsByBatchId) {
60+
if (!getCallsStatus) {
6461
throw rpcErrors.methodNotSupported();
6562
}
6663

6764
validateParams(req.params, GetCallsStatusStruct);
6865

69-
const batchId = req.params[0];
70-
const rawReceipts = await getTransactionReceiptsByBatchId(batchId, req);
71-
72-
if (!rawReceipts.length) {
73-
res.result = null;
74-
return;
75-
}
76-
77-
const isComplete = rawReceipts.every((receipt) => Boolean(receipt));
78-
const status = isComplete ? 'CONFIRMED' : 'PENDING';
79-
80-
const receipts = isComplete
81-
? rawReceipts.map((receipt) => mask(receipt, GetCallsStatusReceiptStruct))
82-
: null;
66+
const id = req.params[0];
8367

84-
res.result = { status, receipts };
68+
res.result = await getCallsStatus(id, req);
8569
}

src/methods/wallet-get-capabilities.test.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
import { walletGetCapabilities } from './wallet-get-capabilities';
1010

1111
const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a';
12+
const CHAIN_ID_MOCK = '0x1';
13+
const CHAIN_ID_2_MOCK = '0x2';
1214

1315
const RESULT_MOCK = {
1416
testCapability: {
@@ -18,10 +20,10 @@ const RESULT_MOCK = {
1820

1921
const REQUEST_MOCK = {
2022
params: [ADDRESS_MOCK],
21-
} as unknown as JsonRpcRequest<GetCapabilitiesParams>;
23+
};
2224

2325
describe('wallet_getCapabilities', () => {
24-
let request: JsonRpcRequest<GetCapabilitiesParams>;
26+
let request: JsonRpcRequest;
2527
let params: GetCapabilitiesParams;
2628
let response: PendingJsonRpcResponse<GetCapabilitiesResult>;
2729
let getCapabilitiesMock: jest.MockedFunction<GetCapabilitiesHook>;
@@ -35,7 +37,7 @@ describe('wallet_getCapabilities', () => {
3537
beforeEach(() => {
3638
jest.resetAllMocks();
3739

38-
request = klona(REQUEST_MOCK);
40+
request = klona(REQUEST_MOCK) as JsonRpcRequest;
3941
params = request.params as GetCapabilitiesParams;
4042
response = {} as PendingJsonRpcResponse<GetCapabilitiesResult>;
4143

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

4547
it('calls hook', async () => {
4648
await callMethod();
47-
expect(getCapabilitiesMock).toHaveBeenCalledWith(params[0], request);
49+
expect(getCapabilitiesMock).toHaveBeenCalledWith(
50+
params[0],
51+
undefined,
52+
request,
53+
);
4854
});
4955

50-
it('returns capabilities from hook', async () => {
56+
it('calls hook with chain IDs', async () => {
57+
request.params = [ADDRESS_MOCK, [CHAIN_ID_MOCK, CHAIN_ID_2_MOCK]];
58+
5159
await callMethod();
5260

61+
expect(getCapabilitiesMock).toHaveBeenCalledWith(
62+
params[0],
63+
[CHAIN_ID_MOCK, CHAIN_ID_2_MOCK],
64+
request,
65+
);
66+
});
67+
68+
it('returns capabilities from hook', async () => {
69+
await callMethod();
5370
expect(response.result).toStrictEqual(RESULT_MOCK);
5471
});
5572

src/methods/wallet-get-capabilities.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import { rpcErrors } from '@metamask/rpc-errors';
22
import type { Infer } from '@metamask/superstruct';
3-
import { tuple } from '@metamask/superstruct';
3+
import { array, optional, tuple } from '@metamask/superstruct';
44
import type {
55
Hex,
66
Json,
77
JsonRpcRequest,
88
PendingJsonRpcResponse,
99
} from '@metamask/utils';
10-
import { HexChecksumAddressStruct } from '@metamask/utils';
10+
import { StrictHexStruct, HexChecksumAddressStruct } from '@metamask/utils';
1111

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

14-
const GetCapabilitiesStruct = tuple([HexChecksumAddressStruct]);
14+
const GetCapabilitiesStruct = tuple([
15+
HexChecksumAddressStruct,
16+
optional(array(StrictHexStruct)),
17+
]);
1518

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

1922
export type GetCapabilitiesHook = (
20-
address: Hex,
23+
address: GetCapabilitiesParams[0],
24+
chainIds: GetCapabilitiesParams[1],
2125
req: JsonRpcRequest,
2226
) => Promise<GetCapabilitiesResult>;
2327

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

3943
const address = req.params[0];
40-
const capabilities = await getCapabilities(address, req);
44+
const chainIds = req.params[1];
45+
const capabilities = await getCapabilities(address, chainIds, req);
4146

4247
res.result = capabilities;
4348
}

0 commit comments

Comments
 (0)