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