@openzeppelin/guardian-client 0.13.2
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 +140 -0
- package/dist/auth-request.d.ts +10 -0
- package/dist/auth-request.d.ts.map +1 -0
- package/dist/auth-request.js +38 -0
- package/dist/auth-request.js.map +1 -0
- package/dist/auth-request.test.d.ts +2 -0
- package/dist/auth-request.test.d.ts.map +1 -0
- package/dist/auth-request.test.js +10 -0
- package/dist/auth-request.test.js.map +1 -0
- package/dist/conversion.d.ts +18 -0
- package/dist/conversion.d.ts.map +1 -0
- package/dist/conversion.js +187 -0
- package/dist/conversion.js.map +1 -0
- package/dist/conversion.test.d.ts +2 -0
- package/dist/conversion.test.d.ts.map +1 -0
- package/dist/conversion.test.js +317 -0
- package/dist/conversion.test.js.map +1 -0
- package/dist/http.d.ts +33 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +191 -0
- package/dist/http.js.map +1 -0
- package/dist/http.test.d.ts +2 -0
- package/dist/http.test.d.ts.map +1 -0
- package/dist/http.test.js +493 -0
- package/dist/http.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server-types.d.ts +150 -0
- package/dist/server-types.d.ts.map +1 -0
- package/dist/server-types.js +2 -0
- package/dist/server-types.js.map +1 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
- package/src/auth-request.test.ts +11 -0
- package/src/auth-request.ts +49 -0
- package/src/conversion.test.ts +400 -0
- package/src/conversion.ts +247 -0
- package/src/http.test.ts +597 -0
- package/src/http.ts +244 -0
- package/src/index.ts +25 -0
- package/src/server-types.ts +142 -0
- package/src/types.ts +167 -0
package/src/http.test.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { GuardianHttpClient, GuardianHttpError } from './http.js';
|
|
3
|
+
import type { Signer, ConfigureResponse, StateObject, DeltaObject, DeltaProposalResponse } from './types.js';
|
|
4
|
+
|
|
5
|
+
// Mock fetch globally
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
8
|
+
|
|
9
|
+
// Mock signer for authenticated requests
|
|
10
|
+
const mockSigner: Signer = {
|
|
11
|
+
commitment: '0x' + '1'.repeat(64),
|
|
12
|
+
publicKey: '0x' + '2'.repeat(64),
|
|
13
|
+
scheme: 'falcon',
|
|
14
|
+
signAccountIdWithTimestamp: vi.fn().mockResolvedValue('0x' + 'a'.repeat(128)),
|
|
15
|
+
signRequest: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
|
|
16
|
+
signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('GuardianHttpClient', () => {
|
|
20
|
+
let client: GuardianHttpClient;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
client = new GuardianHttpClient('http://localhost:3000');
|
|
24
|
+
mockFetch.mockReset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('constructor', () => {
|
|
32
|
+
it('should create client with baseUrl', () => {
|
|
33
|
+
const c = new GuardianHttpClient('http://example.com:8080');
|
|
34
|
+
expect(c).toBeInstanceOf(GuardianHttpClient);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getPubkey', () => {
|
|
39
|
+
it('should return server public key', async () => {
|
|
40
|
+
const expectedPubkey = '0x' + 'abc123'.repeat(10);
|
|
41
|
+
mockFetch.mockResolvedValueOnce({
|
|
42
|
+
ok: true,
|
|
43
|
+
json: async () => ({ commitment: expectedPubkey }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const pubkey = await client.getPubkey();
|
|
47
|
+
|
|
48
|
+
expect(pubkey).toEqual({ commitment: expectedPubkey, pubkey: undefined });
|
|
49
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
50
|
+
'http://localhost:3000/pubkey',
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
method: 'GET',
|
|
53
|
+
headers: expect.objectContaining({
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should throw GuardianHttpError on non-ok response', async () => {
|
|
61
|
+
mockFetch.mockResolvedValueOnce({
|
|
62
|
+
ok: false,
|
|
63
|
+
status: 500,
|
|
64
|
+
statusText: 'Internal Server Error',
|
|
65
|
+
text: async () => 'Server error message',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const error = await client.getPubkey().catch((e) => e);
|
|
69
|
+
expect(error).toBeInstanceOf(GuardianHttpError);
|
|
70
|
+
expect(error.status).toBe(500);
|
|
71
|
+
expect(error.statusText).toBe('Internal Server Error');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('configure', () => {
|
|
76
|
+
it('should configure account with authentication', async () => {
|
|
77
|
+
client.setSigner(mockSigner);
|
|
78
|
+
|
|
79
|
+
// Server returns snake_case
|
|
80
|
+
const serverResponse = {
|
|
81
|
+
success: true,
|
|
82
|
+
message: 'Account configured',
|
|
83
|
+
ack_pubkey: '0x' + 'c'.repeat(64),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
mockFetch.mockResolvedValueOnce({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: async () => serverResponse,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Client API uses camelCase
|
|
92
|
+
const request = {
|
|
93
|
+
accountId: '0x' + 'd'.repeat(30),
|
|
94
|
+
auth: {
|
|
95
|
+
MidenFalconRpo: {
|
|
96
|
+
cosigner_commitments: ['0x' + 'e'.repeat(64)],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
initialState: { data: 'base64data', accountId: '0x' + 'd'.repeat(30) },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const response = await client.configure(request);
|
|
103
|
+
|
|
104
|
+
// Client returns camelCase
|
|
105
|
+
const expectedResponse: ConfigureResponse = {
|
|
106
|
+
success: true,
|
|
107
|
+
message: 'Account configured',
|
|
108
|
+
ackPubkey: '0x' + 'c'.repeat(64),
|
|
109
|
+
};
|
|
110
|
+
expect(response).toEqual(expectedResponse);
|
|
111
|
+
|
|
112
|
+
// Wire format is snake_case
|
|
113
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
114
|
+
'http://localhost:3000/configure',
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
account_id: '0x' + 'd'.repeat(30),
|
|
119
|
+
auth: { MidenFalconRpo: { cosigner_commitments: ['0x' + 'e'.repeat(64)] } },
|
|
120
|
+
initial_state: { data: 'base64data', account_id: '0x' + 'd'.repeat(30) },
|
|
121
|
+
}),
|
|
122
|
+
headers: expect.objectContaining({
|
|
123
|
+
'x-pubkey': mockSigner.publicKey,
|
|
124
|
+
'x-signature': expect.any(String),
|
|
125
|
+
}),
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should throw error when no signer configured', async () => {
|
|
131
|
+
const request = {
|
|
132
|
+
accountId: '0x' + 'd'.repeat(30),
|
|
133
|
+
auth: { MidenFalconRpo: { cosigner_commitments: [] } },
|
|
134
|
+
initialState: { data: 'base64data', accountId: '0x' + 'd'.repeat(30) },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await expect(client.configure(request)).rejects.toThrow('No signer configured');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('getState', () => {
|
|
142
|
+
it('should get account state with authentication', async () => {
|
|
143
|
+
client.setSigner(mockSigner);
|
|
144
|
+
|
|
145
|
+
// Server returns snake_case
|
|
146
|
+
const serverState = {
|
|
147
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
148
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
149
|
+
state_json: { data: 'base64state' },
|
|
150
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
151
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
mockFetch.mockResolvedValueOnce({
|
|
155
|
+
ok: true,
|
|
156
|
+
json: async () => serverState,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const accountId = '0x' + 'a'.repeat(30);
|
|
160
|
+
const state = await client.getState(accountId);
|
|
161
|
+
|
|
162
|
+
// Client returns camelCase
|
|
163
|
+
const expectedState: StateObject = {
|
|
164
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
165
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
166
|
+
stateJson: { data: 'base64state' },
|
|
167
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
168
|
+
updatedAt: '2024-01-02T00:00:00Z',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(state).toEqual(expectedState);
|
|
172
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
173
|
+
expect.stringContaining('/state?'),
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
method: 'GET',
|
|
176
|
+
headers: expect.objectContaining({
|
|
177
|
+
'x-pubkey': mockSigner.publicKey,
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('getDeltaProposals', () => {
|
|
185
|
+
it('should get delta proposals for account', async () => {
|
|
186
|
+
client.setSigner(mockSigner);
|
|
187
|
+
|
|
188
|
+
// Server returns snake_case
|
|
189
|
+
const serverProposals = [
|
|
190
|
+
{
|
|
191
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
192
|
+
nonce: 1,
|
|
193
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
194
|
+
delta_payload: {
|
|
195
|
+
tx_summary: { data: 'base64summary' },
|
|
196
|
+
signatures: [],
|
|
197
|
+
},
|
|
198
|
+
status: {
|
|
199
|
+
status: 'pending',
|
|
200
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
201
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
202
|
+
cosigner_sigs: [],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
mockFetch.mockResolvedValueOnce({
|
|
208
|
+
ok: true,
|
|
209
|
+
json: async () => ({ proposals: serverProposals }),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const accountId = '0x' + 'a'.repeat(30);
|
|
213
|
+
const result = await client.getDeltaProposals(accountId);
|
|
214
|
+
|
|
215
|
+
// Client returns camelCase
|
|
216
|
+
const expectedProposals: DeltaObject[] = [
|
|
217
|
+
{
|
|
218
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
219
|
+
nonce: 1,
|
|
220
|
+
prevCommitment: '0x' + 'b'.repeat(64),
|
|
221
|
+
newCommitment: undefined,
|
|
222
|
+
deltaPayload: {
|
|
223
|
+
txSummary: { data: 'base64summary' },
|
|
224
|
+
signatures: [],
|
|
225
|
+
metadata: undefined,
|
|
226
|
+
},
|
|
227
|
+
ackSig: undefined,
|
|
228
|
+
status: {
|
|
229
|
+
status: 'pending',
|
|
230
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
231
|
+
proposerId: '0x' + 'c'.repeat(64),
|
|
232
|
+
cosignerSigs: [],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
expect(result).toEqual(expectedProposals);
|
|
238
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
239
|
+
expect.stringContaining('/delta/proposal?'),
|
|
240
|
+
expect.objectContaining({ method: 'GET' })
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('getDeltaProposal', () => {
|
|
246
|
+
it('should get a single delta proposal by commitment', async () => {
|
|
247
|
+
client.setSigner(mockSigner);
|
|
248
|
+
|
|
249
|
+
const serverProposal = {
|
|
250
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
251
|
+
nonce: 1,
|
|
252
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
253
|
+
delta_payload: {
|
|
254
|
+
tx_summary: { data: 'base64summary' },
|
|
255
|
+
signatures: [],
|
|
256
|
+
metadata: { proposal_type: 'change_threshold' as const, target_threshold: 2, signer_commitments: [] },
|
|
257
|
+
},
|
|
258
|
+
status: {
|
|
259
|
+
status: 'pending' as const,
|
|
260
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
261
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
262
|
+
cosigner_sigs: [],
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
mockFetch.mockResolvedValueOnce({
|
|
267
|
+
ok: true,
|
|
268
|
+
json: async () => serverProposal,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const accountId = '0x' + 'a'.repeat(30);
|
|
272
|
+
const commitment = '0x' + 'd'.repeat(64);
|
|
273
|
+
const proposal = await client.getDeltaProposal(accountId, commitment);
|
|
274
|
+
|
|
275
|
+
expect(proposal.accountId).toBe(accountId);
|
|
276
|
+
expect(proposal.nonce).toBe(1);
|
|
277
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
278
|
+
expect.stringContaining('/delta/proposal/single?'),
|
|
279
|
+
expect.objectContaining({ method: 'GET' }),
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('pushDeltaProposal', () => {
|
|
285
|
+
it('should push a new delta proposal', async () => {
|
|
286
|
+
client.setSigner(mockSigner);
|
|
287
|
+
|
|
288
|
+
// Server returns snake_case
|
|
289
|
+
const serverResponse = {
|
|
290
|
+
delta: {
|
|
291
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
292
|
+
nonce: 1,
|
|
293
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
294
|
+
delta_payload: {
|
|
295
|
+
tx_summary: { data: 'base64summary' },
|
|
296
|
+
signatures: [],
|
|
297
|
+
},
|
|
298
|
+
status: {
|
|
299
|
+
status: 'pending',
|
|
300
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
301
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
302
|
+
cosigner_sigs: [],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
commitment: '0x' + 'd'.repeat(64),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
mockFetch.mockResolvedValueOnce({
|
|
309
|
+
ok: true,
|
|
310
|
+
json: async () => serverResponse,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Client API uses camelCase
|
|
314
|
+
const request = {
|
|
315
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
316
|
+
nonce: 1,
|
|
317
|
+
deltaPayload: {
|
|
318
|
+
txSummary: { data: 'base64summary' },
|
|
319
|
+
signatures: [],
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const result = await client.pushDeltaProposal(request);
|
|
324
|
+
|
|
325
|
+
// Client returns camelCase
|
|
326
|
+
const expectedResponse: DeltaProposalResponse = {
|
|
327
|
+
delta: {
|
|
328
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
329
|
+
nonce: 1,
|
|
330
|
+
prevCommitment: '0x' + 'b'.repeat(64),
|
|
331
|
+
newCommitment: undefined,
|
|
332
|
+
deltaPayload: {
|
|
333
|
+
txSummary: { data: 'base64summary' },
|
|
334
|
+
signatures: [],
|
|
335
|
+
metadata: undefined,
|
|
336
|
+
},
|
|
337
|
+
ackSig: undefined,
|
|
338
|
+
status: {
|
|
339
|
+
status: 'pending',
|
|
340
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
341
|
+
proposerId: '0x' + 'c'.repeat(64),
|
|
342
|
+
cosignerSigs: [],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
commitment: '0x' + 'd'.repeat(64),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
expect(result).toEqual(expectedResponse);
|
|
349
|
+
|
|
350
|
+
// Wire format is snake_case
|
|
351
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
352
|
+
'http://localhost:3000/delta/proposal',
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
method: 'POST',
|
|
355
|
+
body: JSON.stringify({
|
|
356
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
357
|
+
nonce: 1,
|
|
358
|
+
delta_payload: {
|
|
359
|
+
tx_summary: { data: 'base64summary' },
|
|
360
|
+
signatures: [],
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('signDeltaProposal', () => {
|
|
369
|
+
it('should sign a delta proposal', async () => {
|
|
370
|
+
client.setSigner(mockSigner);
|
|
371
|
+
|
|
372
|
+
// Server returns snake_case
|
|
373
|
+
const serverDelta = {
|
|
374
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
375
|
+
nonce: 1,
|
|
376
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
377
|
+
delta_payload: {
|
|
378
|
+
tx_summary: { data: 'base64summary' },
|
|
379
|
+
signatures: [{ signer_id: '0x' + 'c'.repeat(64), signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) } }],
|
|
380
|
+
},
|
|
381
|
+
status: {
|
|
382
|
+
status: 'pending',
|
|
383
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
384
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
385
|
+
cosigner_sigs: [
|
|
386
|
+
{
|
|
387
|
+
signer_id: '0x' + 'c'.repeat(64),
|
|
388
|
+
signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
|
|
389
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
mockFetch.mockResolvedValueOnce({
|
|
396
|
+
ok: true,
|
|
397
|
+
json: async () => serverDelta,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Client API uses camelCase
|
|
401
|
+
const request = {
|
|
402
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
403
|
+
commitment: '0x' + 'e'.repeat(64),
|
|
404
|
+
signature: { scheme: 'falcon' as const, signature: '0x' + 'd'.repeat(128) },
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const result = await client.signDeltaProposal(request);
|
|
408
|
+
|
|
409
|
+
// Client returns camelCase
|
|
410
|
+
const expectedDelta: DeltaObject = {
|
|
411
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
412
|
+
nonce: 1,
|
|
413
|
+
prevCommitment: '0x' + 'b'.repeat(64),
|
|
414
|
+
newCommitment: undefined,
|
|
415
|
+
deltaPayload: {
|
|
416
|
+
txSummary: { data: 'base64summary' },
|
|
417
|
+
signatures: [{ signerId: '0x' + 'c'.repeat(64), signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) } }],
|
|
418
|
+
metadata: undefined,
|
|
419
|
+
},
|
|
420
|
+
ackSig: undefined,
|
|
421
|
+
status: {
|
|
422
|
+
status: 'pending',
|
|
423
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
424
|
+
proposerId: '0x' + 'c'.repeat(64),
|
|
425
|
+
cosignerSigs: [
|
|
426
|
+
{
|
|
427
|
+
signerId: '0x' + 'c'.repeat(64),
|
|
428
|
+
signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
|
|
429
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
expect(result).toEqual(expectedDelta);
|
|
436
|
+
|
|
437
|
+
// Wire format is snake_case
|
|
438
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
439
|
+
'http://localhost:3000/delta/proposal',
|
|
440
|
+
expect.objectContaining({
|
|
441
|
+
method: 'PUT',
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
444
|
+
commitment: '0x' + 'e'.repeat(64),
|
|
445
|
+
signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
|
|
446
|
+
}),
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('pushDelta', () => {
|
|
453
|
+
it('should push a delta for execution and return ack signature', async () => {
|
|
454
|
+
client.setSigner(mockSigner);
|
|
455
|
+
|
|
456
|
+
// Server returns snake_case - execution delta response has raw delta_payload
|
|
457
|
+
const serverResponse = {
|
|
458
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
459
|
+
nonce: 1,
|
|
460
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
461
|
+
new_commitment: '0x' + 'd'.repeat(64),
|
|
462
|
+
delta_payload: { data: 'base64summary' },
|
|
463
|
+
ack_sig: '0x' + 'f'.repeat(128),
|
|
464
|
+
status: {
|
|
465
|
+
status: 'candidate',
|
|
466
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
mockFetch.mockResolvedValueOnce({
|
|
471
|
+
ok: true,
|
|
472
|
+
json: async () => serverResponse,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Client API uses camelCase
|
|
476
|
+
const executionDelta = {
|
|
477
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
478
|
+
nonce: 1,
|
|
479
|
+
prevCommitment: '0x' + 'b'.repeat(64),
|
|
480
|
+
deltaPayload: { data: 'base64summary' },
|
|
481
|
+
status: {
|
|
482
|
+
status: 'pending' as const,
|
|
483
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
484
|
+
proposerId: '0x' + 'c'.repeat(64),
|
|
485
|
+
cosignerSigs: [],
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const result = await client.pushDelta(executionDelta);
|
|
490
|
+
|
|
491
|
+
// PushDeltaResponse only includes essential fields for execution
|
|
492
|
+
expect(result.accountId).toBe('0x' + 'a'.repeat(30));
|
|
493
|
+
expect(result.nonce).toBe(1);
|
|
494
|
+
expect(result.newCommitment).toBe('0x' + 'd'.repeat(64));
|
|
495
|
+
expect(result.ackSig).toBe('0x' + 'f'.repeat(128));
|
|
496
|
+
|
|
497
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
498
|
+
'http://localhost:3000/delta',
|
|
499
|
+
expect.objectContaining({
|
|
500
|
+
method: 'POST',
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('getDelta', () => {
|
|
507
|
+
it('should get a specific delta by nonce', async () => {
|
|
508
|
+
client.setSigner(mockSigner);
|
|
509
|
+
|
|
510
|
+
// Server returns snake_case
|
|
511
|
+
const serverDelta = {
|
|
512
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
513
|
+
nonce: 5,
|
|
514
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
515
|
+
delta_payload: {
|
|
516
|
+
tx_summary: { data: 'base64summary' },
|
|
517
|
+
signatures: [],
|
|
518
|
+
},
|
|
519
|
+
status: {
|
|
520
|
+
status: 'canonical',
|
|
521
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
mockFetch.mockResolvedValueOnce({
|
|
526
|
+
ok: true,
|
|
527
|
+
json: async () => serverDelta,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const result = await client.getDelta('0x' + 'a'.repeat(30), 5);
|
|
531
|
+
|
|
532
|
+
// Client returns camelCase
|
|
533
|
+
expect(result.accountId).toBe('0x' + 'a'.repeat(30));
|
|
534
|
+
expect(result.nonce).toBe(5);
|
|
535
|
+
expect(result.prevCommitment).toBe('0x' + 'b'.repeat(64));
|
|
536
|
+
expect(result.status.status).toBe('canonical');
|
|
537
|
+
|
|
538
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
539
|
+
expect.stringContaining('/delta?'),
|
|
540
|
+
expect.objectContaining({ method: 'GET' })
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
describe('getDeltaSince', () => {
|
|
546
|
+
it('should get merged delta since a nonce', async () => {
|
|
547
|
+
client.setSigner(mockSigner);
|
|
548
|
+
|
|
549
|
+
// Server returns snake_case
|
|
550
|
+
const serverDelta = {
|
|
551
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
552
|
+
nonce: 10,
|
|
553
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
554
|
+
delta_payload: {
|
|
555
|
+
tx_summary: { data: 'base64mergeddata' },
|
|
556
|
+
signatures: [],
|
|
557
|
+
},
|
|
558
|
+
status: {
|
|
559
|
+
status: 'canonical',
|
|
560
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
mockFetch.mockResolvedValueOnce({
|
|
565
|
+
ok: true,
|
|
566
|
+
json: async () => serverDelta,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const result = await client.getDeltaSince('0x' + 'a'.repeat(30), 5);
|
|
570
|
+
|
|
571
|
+
// Client returns camelCase
|
|
572
|
+
expect(result.accountId).toBe('0x' + 'a'.repeat(30));
|
|
573
|
+
expect(result.nonce).toBe(10);
|
|
574
|
+
expect(result.deltaPayload.txSummary.data).toBe('base64mergeddata');
|
|
575
|
+
|
|
576
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
577
|
+
expect.stringContaining('/delta/since?'),
|
|
578
|
+
expect.objectContaining({ method: 'GET' })
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe('GuardianHttpError', () => {
|
|
585
|
+
it('should create error with status, statusText, and body', () => {
|
|
586
|
+
const error = new GuardianHttpError(404, 'Not Found', 'Resource not found');
|
|
587
|
+
|
|
588
|
+
expect(error).toBeInstanceOf(Error);
|
|
589
|
+
expect(error).toBeInstanceOf(GuardianHttpError);
|
|
590
|
+
expect(error.status).toBe(404);
|
|
591
|
+
expect(error.statusText).toBe('Not Found');
|
|
592
|
+
expect(error.body).toBe('Resource not found');
|
|
593
|
+
expect(error.message).toContain('404');
|
|
594
|
+
expect(error.message).toContain('Not Found');
|
|
595
|
+
expect(error.name).toBe('GuardianHttpError');
|
|
596
|
+
});
|
|
597
|
+
});
|