Skip to content

Commit 65563c4

Browse files
authored
Add EIP-8050 Test Coverage for Prolink Utilities (#189)
* Add EIP-8050 test coverage for prolink utilities * Apply formatting
1 parent 5eb05e3 commit 65563c4

File tree

11 files changed

+1794
-31
lines changed

11 files changed

+1794
-31
lines changed

packages/account-sdk/src/interface/public-utilities/prolink/index.node.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,11 @@ export async function decodeProlink(payload: string): Promise<ProlinkDecoded> {
177177
throw new Error('wallet_sendCalls requires chainId');
178178
}
179179

180-
const params = decodeWalletSendCalls(rpcPayload.body.value, rpcPayload.chainId);
180+
const params = decodeWalletSendCalls(rpcPayload.body.value, rpcPayload.chainId, capabilities);
181181

182182
return {
183183
method: 'wallet_sendCalls',
184184
params: [params],
185-
chainId: rpcPayload.chainId,
186-
capabilities,
187185
};
188186
}
189187

@@ -196,13 +194,11 @@ export async function decodeProlink(payload: string): Promise<ProlinkDecoded> {
196194
throw new Error('wallet_sign requires chainId');
197195
}
198196

199-
const params = decodeWalletSign(rpcPayload.body.value, rpcPayload.chainId);
197+
const params = decodeWalletSign(rpcPayload.body.value, rpcPayload.chainId, capabilities);
200198

201199
return {
202200
method: 'wallet_sign',
203201
params: [params],
204-
chainId: rpcPayload.chainId,
205-
capabilities,
206202
};
207203
}
208204

packages/account-sdk/src/interface/public-utilities/prolink/index.test.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ describe('prolink end-to-end', () => {
2929

3030
const decoded = await decodeProlink(encoded);
3131
expect(decoded.method).toBe('wallet_sendCalls');
32-
expect(decoded.chainId).toBe(8453);
3332
expect(Array.isArray(decoded.params)).toBe(true);
3433

35-
const params = (decoded.params as Array<{ calls: unknown[] }>)[0];
34+
const params = (decoded.params as Array<{ chainId: string; calls: unknown[] }>)[0];
35+
expect(params.chainId).toBe('0x2105'); // hex string
3636
expect(params.calls.length).toBe(1);
3737
});
3838

@@ -58,7 +58,8 @@ describe('prolink end-to-end', () => {
5858
const decoded = await decodeProlink(encoded);
5959

6060
expect(decoded.method).toBe('wallet_sendCalls');
61-
expect(decoded.chainId).toBe(1);
61+
const params = (decoded.params as Array<{ chainId: string }>)[0];
62+
expect(params.chainId).toBe('0x1');
6263
});
6364

6465
it('should encode and decode with capabilities', async () => {
@@ -88,8 +89,9 @@ describe('prolink end-to-end', () => {
8889
const encoded = await encodeProlink(request);
8990
const decoded = await decodeProlink(encoded);
9091

91-
expect(decoded.capabilities).toBeDefined();
92-
expect(decoded.capabilities?.dataCallback).toEqual({
92+
const params = (decoded.params as Array<{ capabilities?: Record<string, unknown> }>)[0];
93+
expect(params.capabilities).toBeDefined();
94+
expect(params.capabilities?.dataCallback).toEqual({
9395
callbackURL: 'https://example.com',
9496
events: ['initiated'],
9597
});
@@ -147,9 +149,10 @@ describe('prolink end-to-end', () => {
147149

148150
const decoded = await decodeProlink(encoded);
149151
expect(decoded.method).toBe('wallet_sign');
150-
expect(decoded.chainId).toBe(84532);
151-
152-
const params = (decoded.params as Array<{ data: { primaryType: string } }>)[0];
152+
const params = (
153+
decoded.params as Array<{ chainId: string; data: { primaryType: string } }>
154+
)[0];
155+
expect(params.chainId).toBe('0x14a34'); // hex string for 84532
153156
expect(params.data.primaryType).toBe('SpendPermission');
154157
});
155158
});
@@ -295,10 +298,16 @@ describe('prolink end-to-end', () => {
295298
const decoded = await decodeProlink(encoded);
296299

297300
expect(decoded.method).toBe(request.method);
298-
expect(decoded.chainId).toBe(8453);
299-
expect(decoded.capabilities).toEqual(request.capabilities);
300-
301-
const params = (decoded.params as Array<{ version?: string; from?: string }>)[0];
301+
const params = (
302+
decoded.params as Array<{
303+
version?: string;
304+
from?: string;
305+
chainId: string;
306+
capabilities?: Record<string, unknown>;
307+
}>
308+
)[0];
309+
expect(params.chainId).toBe('0x2105'); // hex string for 8453
310+
expect(params.capabilities).toEqual(request.capabilities);
302311
expect(params.version).toBe('2.0');
303312
expect(params.from?.toLowerCase()).toBe(request.params[0].from.toLowerCase());
304313
});

packages/account-sdk/src/interface/public-utilities/prolink/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,11 @@ export async function decodeProlink(payload: string): Promise<ProlinkDecoded> {
176176
throw new Error('wallet_sendCalls requires chainId');
177177
}
178178

179-
const params = decodeWalletSendCalls(rpcPayload.body.value, rpcPayload.chainId);
179+
const params = decodeWalletSendCalls(rpcPayload.body.value, rpcPayload.chainId, capabilities);
180180

181181
return {
182182
method: 'wallet_sendCalls',
183183
params: [params],
184-
chainId: rpcPayload.chainId,
185-
capabilities,
186184
};
187185
}
188186

@@ -195,13 +193,11 @@ export async function decodeProlink(payload: string): Promise<ProlinkDecoded> {
195193
throw new Error('wallet_sign requires chainId');
196194
}
197195

198-
const params = decodeWalletSign(rpcPayload.body.value, rpcPayload.chainId);
196+
const params = decodeWalletSign(rpcPayload.body.value, rpcPayload.chainId, capabilities);
199197

200198
return {
201199
method: 'wallet_sign',
202200
params: [params],
203-
chainId: rpcPayload.chainId,
204-
capabilities,
205201
};
206202
}
207203

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Copyright (c) 2018-2025 Coinbase, Inc. <https://www.coinbase.com/>
2+
3+
import { describe, expect, it } from 'vitest';
4+
import { decodeGenericRpc, encodeGenericRpc } from './generic.js';
5+
6+
describe('generic JSON-RPC shortcut (EIP-8050 Shortcut 0)', () => {
7+
describe('encoding', () => {
8+
it('should encode method name correctly', () => {
9+
const encoded = encodeGenericRpc('eth_sendTransaction', []);
10+
expect(encoded.method).toBe('eth_sendTransaction');
11+
});
12+
13+
it('should encode params as UTF-8 JSON bytes', () => {
14+
const params = [{ to: '0x1234', value: '0x100' }];
15+
const encoded = encodeGenericRpc('eth_sendTransaction', params);
16+
17+
const decoded = new TextDecoder().decode(encoded.paramsJson);
18+
expect(JSON.parse(decoded)).toEqual(params);
19+
});
20+
21+
it('should set rpc version to 2.0', () => {
22+
const encoded = encodeGenericRpc('eth_chainId', []);
23+
expect(encoded.rpcVersion).toBe('2.0');
24+
});
25+
26+
it('should handle array params', () => {
27+
const params = ['0x1234', 'latest'];
28+
const encoded = encodeGenericRpc('eth_getBalance', params);
29+
30+
const decoded = JSON.parse(new TextDecoder().decode(encoded.paramsJson));
31+
expect(decoded).toEqual(params);
32+
});
33+
34+
it('should handle object params (JSON-RPC 2.0 named params)', () => {
35+
const params = { address: '0x1234', blockTag: 'latest' };
36+
const encoded = encodeGenericRpc('eth_getBalance', params);
37+
38+
const decoded = JSON.parse(new TextDecoder().decode(encoded.paramsJson));
39+
expect(decoded).toEqual(params);
40+
});
41+
42+
it('should handle null params', () => {
43+
const encoded = encodeGenericRpc('eth_chainId', null);
44+
45+
const decoded = JSON.parse(new TextDecoder().decode(encoded.paramsJson));
46+
expect(decoded).toBeNull();
47+
});
48+
49+
it('should handle undefined params', () => {
50+
const encoded = encodeGenericRpc('eth_chainId', undefined);
51+
52+
const decoded = new TextDecoder().decode(encoded.paramsJson);
53+
// JSON.stringify(undefined) returns undefined (not a string), but in practice
54+
// we get the string "undefined" which is not valid JSON
55+
// This behavior depends on implementation; let's check what actually happens
56+
expect(decoded).toBeDefined();
57+
});
58+
59+
it('should handle complex nested params', () => {
60+
const params = {
61+
nested: {
62+
array: [1, 2, 3],
63+
object: { a: 'b' },
64+
},
65+
boolean: true,
66+
number: 42,
67+
string: 'test',
68+
};
69+
const encoded = encodeGenericRpc('custom_method', params);
70+
71+
const decoded = JSON.parse(new TextDecoder().decode(encoded.paramsJson));
72+
expect(decoded).toEqual(params);
73+
});
74+
});
75+
76+
describe('decoding', () => {
77+
it('should decode method name', () => {
78+
const encoded = encodeGenericRpc('eth_sendTransaction', []);
79+
const decoded = decodeGenericRpc(encoded);
80+
81+
expect(decoded.method).toBe('eth_sendTransaction');
82+
});
83+
84+
it('should decode array params', () => {
85+
const params = ['0x1234', 'latest'];
86+
const encoded = encodeGenericRpc('eth_getBalance', params);
87+
const decoded = decodeGenericRpc(encoded);
88+
89+
expect(decoded.params).toEqual(params);
90+
});
91+
92+
it('should decode object params', () => {
93+
const params = { address: '0x1234', blockTag: 'latest' };
94+
const encoded = encodeGenericRpc('eth_getBalance', params);
95+
const decoded = decodeGenericRpc(encoded);
96+
97+
expect(decoded.params).toEqual(params);
98+
});
99+
100+
it('should throw on invalid JSON', () => {
101+
const invalidPayload = {
102+
method: 'test',
103+
paramsJson: new Uint8Array([0xff, 0xfe, 0xfd]), // Invalid UTF-8
104+
rpcVersion: '2.0',
105+
};
106+
107+
expect(() => decodeGenericRpc(invalidPayload)).toThrow(/Failed to parse params JSON/);
108+
});
109+
110+
it('should throw on malformed JSON', () => {
111+
const malformedPayload = {
112+
method: 'test',
113+
paramsJson: new TextEncoder().encode('{ invalid json }'),
114+
rpcVersion: '2.0',
115+
};
116+
117+
expect(() => decodeGenericRpc(malformedPayload)).toThrow(/Failed to parse params JSON/);
118+
});
119+
});
120+
121+
describe('roundtrip', () => {
122+
it('should roundtrip eth_sendTransaction', () => {
123+
const method = 'eth_sendTransaction';
124+
const params = [
125+
{
126+
from: '0x1111111111111111111111111111111111111111',
127+
to: '0x2222222222222222222222222222222222222222',
128+
value: '0x100',
129+
data: '0x1234',
130+
},
131+
];
132+
133+
const encoded = encodeGenericRpc(method, params);
134+
const decoded = decodeGenericRpc(encoded);
135+
136+
expect(decoded.method).toBe(method);
137+
expect(decoded.params).toEqual(params);
138+
});
139+
140+
it('should roundtrip eth_call with complex params', () => {
141+
const method = 'eth_call';
142+
const params = [
143+
{
144+
to: '0x1234567890123456789012345678901234567890',
145+
data: '0xa9059cbb0000000000000000000000001234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000000a',
146+
},
147+
'latest',
148+
];
149+
150+
const encoded = encodeGenericRpc(method, params);
151+
const decoded = decodeGenericRpc(encoded);
152+
153+
expect(decoded.method).toBe(method);
154+
expect(decoded.params).toEqual(params);
155+
});
156+
157+
it('should roundtrip wallet_sendCalls (fallback scenario)', () => {
158+
// Even wallet_sendCalls can be encoded via generic shortcut
159+
const method = 'wallet_sendCalls';
160+
const params = [
161+
{
162+
version: '1.0',
163+
chainId: '0x1',
164+
calls: [
165+
{
166+
to: '0x1234567890123456789012345678901234567890',
167+
data: '0x',
168+
value: '0x100',
169+
},
170+
],
171+
},
172+
];
173+
174+
const encoded = encodeGenericRpc(method, params);
175+
const decoded = decodeGenericRpc(encoded);
176+
177+
expect(decoded.method).toBe(method);
178+
expect(decoded.params).toEqual(params);
179+
});
180+
181+
it('should roundtrip empty params array', () => {
182+
const method = 'eth_chainId';
183+
const params: unknown[] = [];
184+
185+
const encoded = encodeGenericRpc(method, params);
186+
const decoded = decodeGenericRpc(encoded);
187+
188+
expect(decoded.method).toBe(method);
189+
expect(decoded.params).toEqual(params);
190+
});
191+
});
192+
193+
describe('method name handling', () => {
194+
// Per spec: "method: case-sensitive, MUST match the target RPC interface"
195+
196+
it('should preserve case sensitivity', () => {
197+
const encoded = encodeGenericRpc('ETH_sendTransaction', []);
198+
const decoded = decodeGenericRpc(encoded);
199+
200+
expect(decoded.method).toBe('ETH_sendTransaction');
201+
});
202+
203+
it('should handle underscores in method names', () => {
204+
const encoded = encodeGenericRpc('wallet_send_calls', []);
205+
const decoded = decodeGenericRpc(encoded);
206+
207+
expect(decoded.method).toBe('wallet_send_calls');
208+
});
209+
210+
it('should handle namespaced methods', () => {
211+
const encoded = encodeGenericRpc('debug_traceTransaction', []);
212+
const decoded = decodeGenericRpc(encoded);
213+
214+
expect(decoded.method).toBe('debug_traceTransaction');
215+
});
216+
});
217+
218+
describe('JSON encoding edge cases', () => {
219+
it('should handle Unicode strings', () => {
220+
const params = { message: '你好世界 🌍' };
221+
const encoded = encodeGenericRpc('custom_method', params);
222+
const decoded = decodeGenericRpc(encoded);
223+
224+
expect(decoded.params).toEqual(params);
225+
});
226+
227+
it('should handle special characters', () => {
228+
const params = { special: '\\n\\t\\"' };
229+
const encoded = encodeGenericRpc('custom_method', params);
230+
const decoded = decodeGenericRpc(encoded);
231+
232+
expect(decoded.params).toEqual(params);
233+
});
234+
235+
it('should handle large numbers as strings', () => {
236+
// BigInt values should be passed as strings to avoid precision loss
237+
const params = {
238+
amount: '115792089237316195423570985008687907853269984665640564039457584007913129639935',
239+
};
240+
const encoded = encodeGenericRpc('custom_method', params);
241+
const decoded = decodeGenericRpc(encoded);
242+
243+
expect(decoded.params).toEqual(params);
244+
});
245+
246+
it('should handle boolean values', () => {
247+
const params = { flag: true, other: false };
248+
const encoded = encodeGenericRpc('custom_method', params);
249+
const decoded = decodeGenericRpc(encoded);
250+
251+
expect(decoded.params).toEqual(params);
252+
});
253+
});
254+
});

0 commit comments

Comments
 (0)