@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.
- package/README.md +16 -0
- package/dist/conversion.d.ts.map +1 -1
- package/dist/conversion.js +4 -2
- package/dist/conversion.js.map +1 -1
- package/dist/error-codes.d.ts +30 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +81 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/http.d.ts +62 -10
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +146 -29
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server-types.d.ts +13 -0
- package/dist/server-types.d.ts.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/conversion.test.ts +16 -0
- package/src/conversion.ts +4 -2
- package/src/error-codes.test.ts +69 -0
- package/src/error-codes.ts +91 -0
- package/src/http.test.ts +216 -29
- package/src/http.ts +180 -29
- package/src/index.ts +9 -0
- package/src/server-types.ts +15 -1
- package/src/types.ts +23 -1
- package/dist/auth-request.test.d.ts +0 -2
- package/dist/auth-request.test.d.ts.map +0 -1
- package/dist/auth-request.test.js +0 -10
- package/dist/auth-request.test.js.map +0 -1
- package/dist/conversion.test.d.ts +0 -2
- package/dist/conversion.test.d.ts.map +0 -1
- package/dist/conversion.test.js +0 -332
- package/dist/conversion.test.js.map +0 -1
- package/dist/http.test.d.ts +0 -2
- package/dist/http.test.d.ts.map +0 -1
- package/dist/http.test.js +0 -750
- 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
|