@openzeppelin/guardian-client 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +16 -0
  2. package/dist/conversion.d.ts.map +1 -1
  3. package/dist/conversion.js +4 -2
  4. package/dist/conversion.js.map +1 -1
  5. package/dist/error-codes.d.ts +30 -0
  6. package/dist/error-codes.d.ts.map +1 -0
  7. package/dist/error-codes.js +81 -0
  8. package/dist/error-codes.js.map +1 -0
  9. package/dist/http.d.ts +62 -10
  10. package/dist/http.d.ts.map +1 -1
  11. package/dist/http.js +146 -29
  12. package/dist/http.js.map +1 -1
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/server-types.d.ts +13 -0
  18. package/dist/server-types.d.ts.map +1 -1
  19. package/dist/types.d.ts +21 -0
  20. package/dist/types.d.ts.map +1 -1
  21. package/package.json +1 -1
  22. package/src/conversion.test.ts +16 -0
  23. package/src/conversion.ts +4 -2
  24. package/src/error-codes.test.ts +69 -0
  25. package/src/error-codes.ts +91 -0
  26. package/src/http.test.ts +216 -29
  27. package/src/http.ts +180 -29
  28. package/src/index.ts +9 -0
  29. package/src/server-types.ts +15 -1
  30. package/src/types.ts +23 -1
  31. package/dist/auth-request.test.d.ts +0 -2
  32. package/dist/auth-request.test.d.ts.map +0 -1
  33. package/dist/auth-request.test.js +0 -10
  34. package/dist/auth-request.test.js.map +0 -1
  35. package/dist/conversion.test.d.ts +0 -2
  36. package/dist/conversion.test.d.ts.map +0 -1
  37. package/dist/conversion.test.js +0 -332
  38. package/dist/conversion.test.js.map +0 -1
  39. package/dist/http.test.d.ts +0 -2
  40. package/dist/http.test.d.ts.map +0 -1
  41. package/dist/http.test.js +0 -750
  42. package/dist/http.test.js.map +0 -1
package/dist/http.test.js DELETED
@@ -1,750 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { GuardianHttpClient, GuardianHttpError } from './http.js';
3
- // Mock fetch globally
4
- const mockFetch = vi.fn();
5
- vi.stubGlobal('fetch', mockFetch);
6
- // Mock signer for authenticated requests
7
- const mockSigner = {
8
- commitment: '0x' + '1'.repeat(64),
9
- publicKey: '0x' + '2'.repeat(64),
10
- scheme: 'falcon',
11
- signAccountIdWithTimestamp: vi.fn().mockResolvedValue('0x' + 'a'.repeat(128)),
12
- signRequest: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
13
- signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
14
- };
15
- describe('GuardianHttpClient', () => {
16
- let client;
17
- beforeEach(() => {
18
- client = new GuardianHttpClient('http://localhost:3000');
19
- mockFetch.mockReset();
20
- });
21
- afterEach(() => {
22
- vi.clearAllMocks();
23
- });
24
- describe('constructor', () => {
25
- it('should create client with baseUrl', () => {
26
- const c = new GuardianHttpClient('http://example.com:8080');
27
- expect(c).toBeInstanceOf(GuardianHttpClient);
28
- });
29
- });
30
- describe('getPubkey', () => {
31
- it('should return server public key', async () => {
32
- const expectedPubkey = '0x' + 'abc123'.repeat(10);
33
- mockFetch.mockResolvedValueOnce({
34
- ok: true,
35
- json: async () => ({ commitment: expectedPubkey }),
36
- });
37
- const pubkey = await client.getPubkey();
38
- expect(pubkey).toEqual({ commitment: expectedPubkey, pubkey: undefined });
39
- expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/pubkey', expect.objectContaining({
40
- method: 'GET',
41
- headers: expect.objectContaining({
42
- 'Content-Type': 'application/json',
43
- }),
44
- }));
45
- });
46
- it('should throw GuardianHttpError on non-ok response', async () => {
47
- mockFetch.mockResolvedValueOnce({
48
- ok: false,
49
- status: 500,
50
- statusText: 'Internal Server Error',
51
- text: async () => 'Server error message',
52
- });
53
- const error = await client.getPubkey().catch((e) => e);
54
- expect(error).toBeInstanceOf(GuardianHttpError);
55
- expect(error.status).toBe(500);
56
- expect(error.statusText).toBe('Internal Server Error');
57
- // Non-JSON body: no typed envelope fields.
58
- expect(error.code).toBeNull();
59
- expect(error.releasedAt).toBeNull();
60
- });
61
- it('exposes code and released_at from a GUARDIAN_ACCOUNT_RELEASED envelope', async () => {
62
- mockFetch.mockResolvedValueOnce({
63
- ok: false,
64
- status: 409,
65
- statusText: 'Conflict',
66
- text: async () => JSON.stringify({
67
- success: false,
68
- code: 'GUARDIAN_ACCOUNT_RELEASED',
69
- error: 'Account was released',
70
- retryable: false,
71
- released_at: '2026-07-06T10:00:00Z',
72
- }),
73
- });
74
- const error = await client.getPubkey().catch((e) => e);
75
- expect(error).toBeInstanceOf(GuardianHttpError);
76
- expect(error.status).toBe(409);
77
- expect(error.code).toBe('GUARDIAN_ACCOUNT_RELEASED');
78
- expect(error.releasedAt).toBe('2026-07-06T10:00:00Z');
79
- });
80
- it('exposes code without releasedAt for other envelope errors', async () => {
81
- mockFetch.mockResolvedValueOnce({
82
- ok: false,
83
- status: 404,
84
- statusText: 'Not Found',
85
- text: async () => JSON.stringify({
86
- success: false,
87
- code: 'account_not_found',
88
- error: "Account '0xabc' not found",
89
- }),
90
- });
91
- const error = await client.getPubkey().catch((e) => e);
92
- expect(error.code).toBe('account_not_found');
93
- expect(error.releasedAt).toBeNull();
94
- });
95
- });
96
- describe('getStatus', () => {
97
- it('maps the server status response to camelCase', async () => {
98
- mockFetch.mockResolvedValueOnce({
99
- ok: true,
100
- json: async () => ({
101
- status: 'ok',
102
- version: '0.1.0',
103
- git_commit: 'abc123def456',
104
- environment: 'devnet',
105
- started_at: '2026-06-17T10:00:00Z',
106
- uptime_seconds: 3600,
107
- }),
108
- });
109
- const status = await client.getStatus();
110
- expect(status).toEqual({
111
- status: 'ok',
112
- version: '0.1.0',
113
- gitCommit: 'abc123def456',
114
- environment: 'devnet',
115
- startedAt: '2026-06-17T10:00:00Z',
116
- uptimeSeconds: 3600,
117
- });
118
- expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/status', expect.objectContaining({
119
- method: 'GET',
120
- headers: expect.objectContaining({
121
- 'Content-Type': 'application/json',
122
- }),
123
- }));
124
- });
125
- it('should throw GuardianHttpError on non-ok response', async () => {
126
- mockFetch.mockResolvedValueOnce({
127
- ok: false,
128
- status: 503,
129
- statusText: 'Service Unavailable',
130
- text: async () => 'down',
131
- });
132
- const error = await client.getStatus().catch((e) => e);
133
- expect(error).toBeInstanceOf(GuardianHttpError);
134
- expect(error.status).toBe(503);
135
- });
136
- });
137
- describe('configure', () => {
138
- it('should configure account with authentication', async () => {
139
- client.setSigner(mockSigner);
140
- // Server returns snake_case
141
- const serverResponse = {
142
- success: true,
143
- message: 'Account configured',
144
- ack_pubkey: '0x' + 'c'.repeat(64),
145
- };
146
- mockFetch.mockResolvedValueOnce({
147
- ok: true,
148
- json: async () => serverResponse,
149
- });
150
- // Client API uses camelCase
151
- const request = {
152
- accountId: '0x' + 'd'.repeat(30),
153
- auth: {
154
- MidenFalconRpo: {
155
- cosigner_commitments: ['0x' + 'e'.repeat(64)],
156
- },
157
- },
158
- initialState: { data: 'base64data', accountId: '0x' + 'd'.repeat(30) },
159
- };
160
- const response = await client.configure(request);
161
- // Client returns camelCase
162
- const expectedResponse = {
163
- success: true,
164
- message: 'Account configured',
165
- ackPubkey: '0x' + 'c'.repeat(64),
166
- };
167
- expect(response).toEqual(expectedResponse);
168
- // Wire format is snake_case
169
- expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/configure', expect.objectContaining({
170
- method: 'POST',
171
- body: JSON.stringify({
172
- account_id: '0x' + 'd'.repeat(30),
173
- auth: { MidenFalconRpo: { cosigner_commitments: ['0x' + 'e'.repeat(64)] } },
174
- initial_state: { data: 'base64data', account_id: '0x' + 'd'.repeat(30) },
175
- }),
176
- headers: expect.objectContaining({
177
- 'x-pubkey': mockSigner.publicKey,
178
- 'x-signature': expect.any(String),
179
- }),
180
- }));
181
- });
182
- it('should throw error when no signer configured', async () => {
183
- const request = {
184
- accountId: '0x' + 'd'.repeat(30),
185
- auth: { MidenFalconRpo: { cosigner_commitments: [] } },
186
- initialState: { data: 'base64data', accountId: '0x' + 'd'.repeat(30) },
187
- };
188
- await expect(client.configure(request)).rejects.toThrow('No signer configured');
189
- });
190
- });
191
- describe('getState', () => {
192
- it('should get account state with authentication', async () => {
193
- client.setSigner(mockSigner);
194
- // Server returns snake_case
195
- const serverState = {
196
- account_id: '0x' + 'a'.repeat(30),
197
- commitment: '0x' + 'b'.repeat(64),
198
- state_json: { data: 'base64state' },
199
- created_at: '2024-01-01T00:00:00Z',
200
- updated_at: '2024-01-02T00:00:00Z',
201
- };
202
- mockFetch.mockResolvedValueOnce({
203
- ok: true,
204
- json: async () => serverState,
205
- });
206
- const accountId = '0x' + 'a'.repeat(30);
207
- const state = await client.getState(accountId);
208
- // Client returns camelCase
209
- const expectedState = {
210
- accountId: '0x' + 'a'.repeat(30),
211
- commitment: '0x' + 'b'.repeat(64),
212
- stateJson: { data: 'base64state' },
213
- createdAt: '2024-01-01T00:00:00Z',
214
- updatedAt: '2024-01-02T00:00:00Z',
215
- };
216
- expect(state).toEqual(expectedState);
217
- expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/state?'), expect.objectContaining({
218
- method: 'GET',
219
- headers: expect.objectContaining({
220
- 'x-pubkey': mockSigner.publicKey,
221
- }),
222
- }));
223
- });
224
- });
225
- describe('getDeltaProposals', () => {
226
- it('should get delta proposals for account', async () => {
227
- client.setSigner(mockSigner);
228
- // Server returns snake_case
229
- const serverProposals = [
230
- {
231
- account_id: '0x' + 'a'.repeat(30),
232
- nonce: 1,
233
- prev_commitment: '0x' + 'b'.repeat(64),
234
- delta_payload: {
235
- tx_summary: { data: 'base64summary' },
236
- signatures: [],
237
- },
238
- status: {
239
- status: 'pending',
240
- timestamp: '2024-01-01T00:00:00Z',
241
- proposer_id: '0x' + 'c'.repeat(64),
242
- cosigner_sigs: [],
243
- },
244
- },
245
- ];
246
- mockFetch.mockResolvedValueOnce({
247
- ok: true,
248
- json: async () => ({ proposals: serverProposals }),
249
- });
250
- const accountId = '0x' + 'a'.repeat(30);
251
- const result = await client.getDeltaProposals(accountId);
252
- // Client returns camelCase
253
- const expectedProposals = [
254
- {
255
- accountId: '0x' + 'a'.repeat(30),
256
- nonce: 1,
257
- prevCommitment: '0x' + 'b'.repeat(64),
258
- newCommitment: undefined,
259
- deltaPayload: {
260
- txSummary: { data: 'base64summary' },
261
- signatures: [],
262
- metadata: undefined,
263
- },
264
- ackSig: undefined,
265
- status: {
266
- status: 'pending',
267
- timestamp: '2024-01-01T00:00:00Z',
268
- proposerId: '0x' + 'c'.repeat(64),
269
- cosignerSigs: [],
270
- },
271
- },
272
- ];
273
- expect(result).toEqual(expectedProposals);
274
- expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/delta/proposal?'), expect.objectContaining({ method: 'GET' }));
275
- });
276
- });
277
- describe('getDeltaProposal', () => {
278
- it('should get a single delta proposal by commitment', async () => {
279
- client.setSigner(mockSigner);
280
- const serverProposal = {
281
- account_id: '0x' + 'a'.repeat(30),
282
- nonce: 1,
283
- prev_commitment: '0x' + 'b'.repeat(64),
284
- delta_payload: {
285
- tx_summary: { data: 'base64summary' },
286
- signatures: [],
287
- metadata: { proposal_type: 'change_threshold', target_threshold: 2, signer_commitments: [] },
288
- },
289
- status: {
290
- status: 'pending',
291
- timestamp: '2024-01-01T00:00:00Z',
292
- proposer_id: '0x' + 'c'.repeat(64),
293
- cosigner_sigs: [],
294
- },
295
- };
296
- mockFetch.mockResolvedValueOnce({
297
- ok: true,
298
- json: async () => serverProposal,
299
- });
300
- const accountId = '0x' + 'a'.repeat(30);
301
- const commitment = '0x' + 'd'.repeat(64);
302
- const proposal = await client.getDeltaProposal(accountId, commitment);
303
- expect(proposal.accountId).toBe(accountId);
304
- expect(proposal.nonce).toBe(1);
305
- expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/delta/proposal/single?'), expect.objectContaining({ method: 'GET' }));
306
- });
307
- });
308
- describe('pushDeltaProposal', () => {
309
- it('should push a new delta proposal', async () => {
310
- client.setSigner(mockSigner);
311
- // Server returns snake_case
312
- const serverResponse = {
313
- delta: {
314
- account_id: '0x' + 'a'.repeat(30),
315
- nonce: 1,
316
- prev_commitment: '0x' + 'b'.repeat(64),
317
- delta_payload: {
318
- tx_summary: { data: 'base64summary' },
319
- signatures: [],
320
- },
321
- status: {
322
- status: 'pending',
323
- timestamp: '2024-01-01T00:00:00Z',
324
- proposer_id: '0x' + 'c'.repeat(64),
325
- cosigner_sigs: [],
326
- },
327
- },
328
- commitment: '0x' + 'd'.repeat(64),
329
- };
330
- mockFetch.mockResolvedValueOnce({
331
- ok: true,
332
- json: async () => serverResponse,
333
- });
334
- // Client API uses camelCase
335
- const request = {
336
- accountId: '0x' + 'a'.repeat(30),
337
- nonce: 1,
338
- deltaPayload: {
339
- txSummary: { data: 'base64summary' },
340
- signatures: [],
341
- },
342
- };
343
- const result = await client.pushDeltaProposal(request);
344
- // Client returns camelCase
345
- const expectedResponse = {
346
- delta: {
347
- accountId: '0x' + 'a'.repeat(30),
348
- nonce: 1,
349
- prevCommitment: '0x' + 'b'.repeat(64),
350
- newCommitment: undefined,
351
- deltaPayload: {
352
- txSummary: { data: 'base64summary' },
353
- signatures: [],
354
- metadata: undefined,
355
- },
356
- ackSig: undefined,
357
- status: {
358
- status: 'pending',
359
- timestamp: '2024-01-01T00:00:00Z',
360
- proposerId: '0x' + 'c'.repeat(64),
361
- cosignerSigs: [],
362
- },
363
- },
364
- commitment: '0x' + 'd'.repeat(64),
365
- };
366
- expect(result).toEqual(expectedResponse);
367
- // Wire format is snake_case
368
- expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/delta/proposal', expect.objectContaining({
369
- method: 'POST',
370
- body: JSON.stringify({
371
- account_id: '0x' + 'a'.repeat(30),
372
- nonce: 1,
373
- delta_payload: {
374
- tx_summary: { data: 'base64summary' },
375
- signatures: [],
376
- },
377
- }),
378
- }));
379
- });
380
- });
381
- describe('signDeltaProposal', () => {
382
- it('should sign a delta proposal', async () => {
383
- client.setSigner(mockSigner);
384
- // Server returns snake_case
385
- const serverDelta = {
386
- account_id: '0x' + 'a'.repeat(30),
387
- nonce: 1,
388
- prev_commitment: '0x' + 'b'.repeat(64),
389
- delta_payload: {
390
- tx_summary: { data: 'base64summary' },
391
- signatures: [{ signer_id: '0x' + 'c'.repeat(64), signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) } }],
392
- },
393
- status: {
394
- status: 'pending',
395
- timestamp: '2024-01-01T00:00:00Z',
396
- proposer_id: '0x' + 'c'.repeat(64),
397
- cosigner_sigs: [
398
- {
399
- signer_id: '0x' + 'c'.repeat(64),
400
- signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
401
- timestamp: '2024-01-01T00:00:00Z',
402
- },
403
- ],
404
- },
405
- };
406
- mockFetch.mockResolvedValueOnce({
407
- ok: true,
408
- json: async () => serverDelta,
409
- });
410
- // Client API uses camelCase
411
- const request = {
412
- accountId: '0x' + 'a'.repeat(30),
413
- commitment: '0x' + 'e'.repeat(64),
414
- signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
415
- };
416
- const result = await client.signDeltaProposal(request);
417
- // Client returns camelCase
418
- const expectedDelta = {
419
- accountId: '0x' + 'a'.repeat(30),
420
- nonce: 1,
421
- prevCommitment: '0x' + 'b'.repeat(64),
422
- newCommitment: undefined,
423
- deltaPayload: {
424
- txSummary: { data: 'base64summary' },
425
- signatures: [{ signerId: '0x' + 'c'.repeat(64), signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) } }],
426
- metadata: undefined,
427
- },
428
- ackSig: undefined,
429
- status: {
430
- status: 'pending',
431
- timestamp: '2024-01-01T00:00:00Z',
432
- proposerId: '0x' + 'c'.repeat(64),
433
- cosignerSigs: [
434
- {
435
- signerId: '0x' + 'c'.repeat(64),
436
- signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
437
- timestamp: '2024-01-01T00:00:00Z',
438
- },
439
- ],
440
- },
441
- };
442
- expect(result).toEqual(expectedDelta);
443
- // Wire format is snake_case
444
- expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/delta/proposal', expect.objectContaining({
445
- method: 'PUT',
446
- body: JSON.stringify({
447
- account_id: '0x' + 'a'.repeat(30),
448
- commitment: '0x' + 'e'.repeat(64),
449
- signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
450
- }),
451
- }));
452
- });
453
- });
454
- describe('pushDelta', () => {
455
- it('should push a delta for execution and return ack signature', async () => {
456
- client.setSigner(mockSigner);
457
- // Server returns snake_case - execution delta response has raw delta_payload
458
- const serverResponse = {
459
- account_id: '0x' + 'a'.repeat(30),
460
- nonce: 1,
461
- prev_commitment: '0x' + 'b'.repeat(64),
462
- new_commitment: '0x' + 'd'.repeat(64),
463
- delta_payload: { data: 'base64summary' },
464
- ack_sig: '0x' + 'f'.repeat(128),
465
- status: {
466
- status: 'candidate',
467
- timestamp: '2024-01-01T00:00:00Z',
468
- },
469
- };
470
- mockFetch.mockResolvedValueOnce({
471
- ok: true,
472
- json: async () => serverResponse,
473
- });
474
- // Client API uses camelCase
475
- const executionDelta = {
476
- accountId: '0x' + 'a'.repeat(30),
477
- nonce: 1,
478
- prevCommitment: '0x' + 'b'.repeat(64),
479
- deltaPayload: { data: 'base64summary' },
480
- status: {
481
- status: 'pending',
482
- timestamp: '2024-01-01T00:00:00Z',
483
- proposerId: '0x' + 'c'.repeat(64),
484
- cosignerSigs: [],
485
- },
486
- };
487
- const result = await client.pushDelta(executionDelta);
488
- // PushDeltaResponse only includes essential fields for execution
489
- expect(result.accountId).toBe('0x' + 'a'.repeat(30));
490
- expect(result.nonce).toBe(1);
491
- expect(result.newCommitment).toBe('0x' + 'd'.repeat(64));
492
- expect(result.ackSig).toBe('0x' + 'f'.repeat(128));
493
- expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/delta', expect.objectContaining({
494
- method: 'POST',
495
- }));
496
- });
497
- });
498
- describe('getDelta', () => {
499
- it('should get a specific delta by nonce', async () => {
500
- client.setSigner(mockSigner);
501
- // Server returns snake_case
502
- const serverDelta = {
503
- account_id: '0x' + 'a'.repeat(30),
504
- nonce: 5,
505
- prev_commitment: '0x' + 'b'.repeat(64),
506
- delta_payload: {
507
- tx_summary: { data: 'base64summary' },
508
- signatures: [],
509
- },
510
- status: {
511
- status: 'canonical',
512
- timestamp: '2024-01-01T00:00:00Z',
513
- },
514
- };
515
- mockFetch.mockResolvedValueOnce({
516
- ok: true,
517
- json: async () => serverDelta,
518
- });
519
- const result = await client.getDelta('0x' + 'a'.repeat(30), 5);
520
- // Client returns camelCase
521
- expect(result.accountId).toBe('0x' + 'a'.repeat(30));
522
- expect(result.nonce).toBe(5);
523
- expect(result.prevCommitment).toBe('0x' + 'b'.repeat(64));
524
- expect(result.status.status).toBe('canonical');
525
- expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/delta?'), expect.objectContaining({ method: 'GET' }));
526
- });
527
- });
528
- describe('getDeltaSince', () => {
529
- it('should get merged delta since a nonce', async () => {
530
- client.setSigner(mockSigner);
531
- // Server returns snake_case
532
- const serverDelta = {
533
- account_id: '0x' + 'a'.repeat(30),
534
- nonce: 10,
535
- prev_commitment: '0x' + 'b'.repeat(64),
536
- delta_payload: {
537
- tx_summary: { data: 'base64mergeddata' },
538
- signatures: [],
539
- },
540
- status: {
541
- status: 'canonical',
542
- timestamp: '2024-01-01T00:00:00Z',
543
- },
544
- };
545
- mockFetch.mockResolvedValueOnce({
546
- ok: true,
547
- json: async () => serverDelta,
548
- });
549
- const result = await client.getDeltaSince('0x' + 'a'.repeat(30), 5);
550
- // Client returns camelCase
551
- expect(result.accountId).toBe('0x' + 'a'.repeat(30));
552
- expect(result.nonce).toBe(10);
553
- expect(result.deltaPayload.txSummary.data).toBe('base64mergeddata');
554
- expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/delta/since?'), expect.objectContaining({ method: 'GET' }));
555
- });
556
- });
557
- // --- lookupAccountByKeyCommitment -----
558
- describe('lookupAccountByKeyCommitment', () => {
559
- const keyCommitmentHex = '0x' + 'aa'.repeat(32);
560
- function makeLookupSigner() {
561
- return {
562
- commitment: keyCommitmentHex,
563
- publicKey: '0x' + 'bb'.repeat(897),
564
- scheme: 'falcon',
565
- signAccountIdWithTimestamp: vi.fn(),
566
- signRequest: vi.fn(),
567
- signCommitment: vi.fn(),
568
- signLookupMessage: vi.fn().mockResolvedValue('0x' + 'cc'.repeat(762)),
569
- };
570
- }
571
- it('returns the parsed accounts list on a happy path', async () => {
572
- const signer = makeLookupSigner();
573
- client.setSigner(signer);
574
- mockFetch.mockResolvedValueOnce({
575
- ok: true,
576
- json: async () => ({
577
- accounts: [{ account_id: '0x7bfb0f38b0fafa103f86a805594170' }],
578
- }),
579
- });
580
- const result = await client.lookupAccountByKeyCommitment(keyCommitmentHex);
581
- expect(result.accounts).toHaveLength(1);
582
- expect(result.accounts[0].accountId).toBe('0x7bfb0f38b0fafa103f86a805594170');
583
- expect(signer.signLookupMessage).toHaveBeenCalledTimes(1);
584
- expect(signer.signLookupMessage).toHaveBeenCalledWith(keyCommitmentHex, expect.any(Number));
585
- });
586
- it('treats an empty list as a successful response, not an error', async () => {
587
- const signer = makeLookupSigner();
588
- client.setSigner(signer);
589
- mockFetch.mockResolvedValueOnce({
590
- ok: true,
591
- json: async () => ({ accounts: [] }),
592
- });
593
- const result = await client.lookupAccountByKeyCommitment(keyCommitmentHex);
594
- expect(result.accounts).toEqual([]);
595
- });
596
- it('returns multi-match results in order', async () => {
597
- const signer = makeLookupSigner();
598
- client.setSigner(signer);
599
- mockFetch.mockResolvedValueOnce({
600
- ok: true,
601
- json: async () => ({
602
- accounts: [
603
- { account_id: '0xaaa1' },
604
- { account_id: '0xbbb2' },
605
- ],
606
- }),
607
- });
608
- const result = await client.lookupAccountByKeyCommitment(keyCommitmentHex);
609
- expect(result.accounts.map((a) => a.accountId)).toEqual(['0xaaa1', '0xbbb2']);
610
- });
611
- it('attaches x-pubkey, x-signature, x-timestamp headers and uses /state/lookup with the query string', async () => {
612
- const signer = makeLookupSigner();
613
- client.setSigner(signer);
614
- mockFetch.mockResolvedValueOnce({
615
- ok: true,
616
- json: async () => ({ accounts: [] }),
617
- });
618
- await client.lookupAccountByKeyCommitment(keyCommitmentHex);
619
- expect(mockFetch).toHaveBeenCalledTimes(1);
620
- const [url, init] = mockFetch.mock.calls[0];
621
- expect(url).toBe(`http://localhost:3000/state/lookup?key_commitment=${encodeURIComponent(keyCommitmentHex)}`);
622
- expect(init.method).toBe('GET');
623
- expect(init.headers['x-pubkey']).toBe(signer.publicKey);
624
- expect(init.headers['x-signature']).toMatch(/^0x/);
625
- expect(init.headers['x-timestamp']).toMatch(/^\d+$/);
626
- });
627
- it('throws a clear error when no signer is configured', async () => {
628
- // No setSigner() call.
629
- await expect(client.lookupAccountByKeyCommitment(keyCommitmentHex)).rejects.toThrow(/No signer configured/);
630
- expect(mockFetch).not.toHaveBeenCalled();
631
- });
632
- it('throws a clear error when the signer does not implement signLookupMessage', async () => {
633
- // Default mockSigner from the outer describe does NOT implement
634
- // signLookupMessage — the HTTP client must reject up front rather than
635
- // sending a request the server will reject.
636
- client.setSigner(mockSigner);
637
- await expect(client.lookupAccountByKeyCommitment(keyCommitmentHex)).rejects.toThrow(/signLookupMessage/);
638
- expect(mockFetch).not.toHaveBeenCalled();
639
- });
640
- it('propagates HTTP errors from the server through GuardianHttpError', async () => {
641
- const signer = makeLookupSigner();
642
- client.setSigner(signer);
643
- mockFetch.mockResolvedValueOnce({
644
- ok: false,
645
- status: 401,
646
- statusText: 'Unauthorized',
647
- text: async () => '{"code":"authentication_failed","error":"..."}',
648
- });
649
- const err = await client
650
- .lookupAccountByKeyCommitment(keyCommitmentHex)
651
- .catch((e) => e);
652
- expect(err).toBeInstanceOf(GuardianHttpError);
653
- expect(err.status).toBe(401);
654
- });
655
- it('rejects malformed server responses (missing accounts array)', async () => {
656
- const signer = makeLookupSigner();
657
- client.setSigner(signer);
658
- mockFetch.mockResolvedValueOnce({
659
- ok: true,
660
- json: async () => ({}),
661
- });
662
- await expect(client.lookupAccountByKeyCommitment(keyCommitmentHex)).rejects.toThrow(/Malformed/);
663
- });
664
- });
665
- });
666
- describe('GuardianHttpError', () => {
667
- it('should create error with status, statusText, and body', () => {
668
- const error = new GuardianHttpError(404, 'Not Found', 'Resource not found');
669
- expect(error).toBeInstanceOf(Error);
670
- expect(error).toBeInstanceOf(GuardianHttpError);
671
- expect(error.status).toBe(404);
672
- expect(error.statusText).toBe('Not Found');
673
- expect(error.body).toBe('Resource not found');
674
- expect(error.message).toContain('404');
675
- expect(error.message).toContain('Not Found');
676
- expect(error.name).toBe('GuardianHttpError');
677
- });
678
- describe('error envelope contract (account-paused path)', () => {
679
- let client;
680
- beforeEach(() => {
681
- client = new GuardianHttpClient('http://localhost:3000');
682
- mockFetch.mockReset();
683
- });
684
- it('surfaces 409 GUARDIAN_ACCOUNT_PAUSED with a parseable error envelope on pushDeltaProposal', async () => {
685
- client.setSigner(mockSigner);
686
- // The server's GuardianError::AccountPaused → IntoResponse contract.
687
- // Locks client/server in lockstep: a regression to the legacy
688
- // "(400, {delta: {account_id: 'error text'}})" shape would break this.
689
- const envelope = {
690
- success: false,
691
- code: 'GUARDIAN_ACCOUNT_PAUSED',
692
- error: 'Account is paused: compliance review',
693
- paused_at: '2026-05-20T10:00:00Z',
694
- paused_reason: 'compliance review',
695
- };
696
- mockFetch.mockResolvedValueOnce({
697
- ok: false,
698
- status: 409,
699
- statusText: 'Conflict',
700
- text: async () => JSON.stringify(envelope),
701
- });
702
- const request = {
703
- accountId: '0x' + 'a'.repeat(30),
704
- nonce: 1,
705
- deltaPayload: { txSummary: { data: '' }, signatures: [] },
706
- };
707
- const error = await client
708
- .pushDeltaProposal(request)
709
- .catch((e) => e);
710
- expect(error).toBeInstanceOf(GuardianHttpError);
711
- expect(error.status).toBe(409);
712
- const parsed = JSON.parse(error.body);
713
- expect(parsed.success).toBe(false);
714
- expect(parsed.code).toBe('GUARDIAN_ACCOUNT_PAUSED');
715
- expect(typeof parsed.error).toBe('string');
716
- expect(parsed.paused_at).toBe('2026-05-20T10:00:00Z');
717
- expect(parsed.paused_reason).toBe('compliance review');
718
- // Negative assertion: legacy "stuff error into delta.account_id" shape.
719
- expect(parsed.delta).toBeUndefined();
720
- });
721
- it('surfaces 401 AUTHENTICATION_FAILED with a parseable error envelope', async () => {
722
- client.setSigner(mockSigner);
723
- const envelope = {
724
- success: false,
725
- code: 'authentication_failed',
726
- error: 'Invalid signature',
727
- };
728
- mockFetch.mockResolvedValueOnce({
729
- ok: false,
730
- status: 401,
731
- statusText: 'Unauthorized',
732
- text: async () => JSON.stringify(envelope),
733
- });
734
- const error = await client
735
- .pushDeltaProposal({
736
- accountId: '0x' + 'a'.repeat(30),
737
- nonce: 1,
738
- deltaPayload: { txSummary: { data: '' }, signatures: [] },
739
- })
740
- .catch((e) => e);
741
- expect(error).toBeInstanceOf(GuardianHttpError);
742
- expect(error.status).toBe(401);
743
- const parsed = JSON.parse(error.body);
744
- expect(parsed.success).toBe(false);
745
- expect(parsed.code).toBe('authentication_failed');
746
- expect(parsed.delta).toBeUndefined();
747
- });
748
- });
749
- });
750
- //# sourceMappingURL=http.test.js.map