@openzeppelin/guardian-client 0.16.1 → 0.17.0-rc.1
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 +84 -0
- package/dist/conversion.d.ts +4 -2
- package/dist/conversion.d.ts.map +1 -1
- package/dist/conversion.js +37 -0
- package/dist/conversion.js.map +1 -1
- package/dist/error-codes.d.ts +1 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +1 -0
- package/dist/error-codes.js.map +1 -1
- package/dist/http.d.ts +25 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +66 -9
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server-types.d.ts +36 -0
- package/dist/server-types.d.ts.map +1 -1
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/conversion.test.ts +34 -0
- package/src/conversion.ts +46 -0
- package/src/error-codes.ts +1 -0
- package/src/http.test.ts +312 -6
- package/src/http.ts +78 -8
- package/src/index.ts +10 -0
- package/src/server-types.ts +43 -0
- package/src/types.ts +98 -0
package/src/http.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
DeltaProposalRequest,
|
|
12
12
|
DeltaProposalResponse,
|
|
13
13
|
ExecutionDelta,
|
|
14
|
+
HistoryOptions,
|
|
15
|
+
HistoryPage,
|
|
14
16
|
LookupResponse,
|
|
15
17
|
PubkeyResponse,
|
|
16
18
|
PushDeltaResponse,
|
|
@@ -26,6 +28,7 @@ import type {
|
|
|
26
28
|
ServerAbandonCandidateResponse,
|
|
27
29
|
ServerDeltaObject,
|
|
28
30
|
ServerDeltaProposalResponse,
|
|
31
|
+
ServerHistoryPage,
|
|
29
32
|
ServerLookupResponse,
|
|
30
33
|
ServerProposalsResponse,
|
|
31
34
|
ServerPubkeyResponse,
|
|
@@ -37,6 +40,7 @@ import type {
|
|
|
37
40
|
import {
|
|
38
41
|
fromServerConfigureResponse,
|
|
39
42
|
fromServerDeltaObject,
|
|
43
|
+
fromServerHistoryPage,
|
|
40
44
|
fromServerLookupResponse,
|
|
41
45
|
fromServerStateObject,
|
|
42
46
|
toServerConfigureRequest,
|
|
@@ -149,10 +153,13 @@ export class GuardianHttpError extends Error {
|
|
|
149
153
|
*/
|
|
150
154
|
public readonly releasedAt: string | null;
|
|
151
155
|
|
|
156
|
+
private readonly headerRetryAfterSecs?: number;
|
|
157
|
+
|
|
152
158
|
constructor(
|
|
153
159
|
public readonly status: number,
|
|
154
160
|
public readonly statusText: string,
|
|
155
|
-
public readonly body: string
|
|
161
|
+
public readonly body: string,
|
|
162
|
+
retryAfterHeader?: string | null
|
|
156
163
|
) {
|
|
157
164
|
// Only the parsed, user-safe message is folded into Error.message; the
|
|
158
165
|
// raw body (which may carry backend/proxy internals) stays on the `body`
|
|
@@ -165,7 +172,37 @@ export class GuardianHttpError extends Error {
|
|
|
165
172
|
this.userMessage = parsed?.message;
|
|
166
173
|
this.meta = parsed?.meta;
|
|
167
174
|
this.releasedAt = parsed?.meta.releasedAt ?? null;
|
|
175
|
+
this.headerRetryAfterSecs = parseRetryAfterSeconds(retryAfterHeader);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether the server marked this error safe to retry: `meta.retryable`
|
|
180
|
+
* from the envelope, falling back to the status class (429 rejections
|
|
181
|
+
* happen before any handler runs). Mirrors the Rust client's
|
|
182
|
+
* `ClientError::is_retryable`.
|
|
183
|
+
*/
|
|
184
|
+
isRetryable(): boolean {
|
|
185
|
+
return this.meta?.retryable ?? this.status === 429;
|
|
168
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Server-provided backoff hint in seconds: the `Retry-After` header,
|
|
190
|
+
* falling back to `meta.retryAfterSecs`. Unparseable values mean no
|
|
191
|
+
* hint. Mirrors the Rust client's `ClientError::retry_after`.
|
|
192
|
+
*/
|
|
193
|
+
retryAfterSecs(): number | undefined {
|
|
194
|
+
return this.headerRetryAfterSecs ?? this.meta?.retryAfterSecs;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Decimal digits only, mirroring the Rust client's `parse::<u64>()`: no
|
|
199
|
+
// signs, exponents, hex, or empty strings (`Number('')` is 0).
|
|
200
|
+
function parseRetryAfterSeconds(header: string | null | undefined): number | undefined {
|
|
201
|
+
if (typeof header !== 'string') return undefined;
|
|
202
|
+
const trimmed = header.trim();
|
|
203
|
+
if (!/^\d+$/.test(trimmed)) return undefined;
|
|
204
|
+
const secs = Number(trimmed);
|
|
205
|
+
return Number.isSafeInteger(secs) ? secs : undefined;
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
/**
|
|
@@ -419,6 +456,33 @@ export class GuardianHttpClient {
|
|
|
419
456
|
return fromServerDeltaObject(server);
|
|
420
457
|
}
|
|
421
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Fetch one page of the account's canonical delta history
|
|
461
|
+
* (issue #413), newest-first by nonce, with decoded input/output
|
|
462
|
+
* note summaries. Pass `options.cursor` from a previous page's
|
|
463
|
+
* `nextCursor` to resume; an absent `nextCursor` on the result means
|
|
464
|
+
* the feed is exhausted. Read-only: served while the account is
|
|
465
|
+
* paused. Only transactions pushed through Guardian appear.
|
|
466
|
+
*/
|
|
467
|
+
async getDeltaHistory(accountId: string, options: HistoryOptions = {}): Promise<HistoryPage> {
|
|
468
|
+
// Signed payload and query string must carry the same values: the
|
|
469
|
+
// server signs the canonical JSON of its query struct, where limit
|
|
470
|
+
// stays a string and omitted parameters are omitted keys.
|
|
471
|
+
const requestPayload: Record<string, string> = { account_id: accountId };
|
|
472
|
+
if (options.limit !== undefined) {
|
|
473
|
+
requestPayload.limit = options.limit.toString();
|
|
474
|
+
}
|
|
475
|
+
if (options.cursor !== undefined) {
|
|
476
|
+
requestPayload.cursor = options.cursor;
|
|
477
|
+
}
|
|
478
|
+
const params = new URLSearchParams(requestPayload);
|
|
479
|
+
const response = await this.fetchAuthenticated(`/delta/history?${params}`, {
|
|
480
|
+
method: 'GET',
|
|
481
|
+
}, accountId, requestPayload);
|
|
482
|
+
const server = (await response.json()) as ServerHistoryPage;
|
|
483
|
+
return fromServerHistoryPage(server);
|
|
484
|
+
}
|
|
485
|
+
|
|
422
486
|
private async fetch(path: string, init: RequestInit): Promise<Response> {
|
|
423
487
|
const url = `${this.baseUrl}${path}`;
|
|
424
488
|
const response = await fetch(url, {
|
|
@@ -431,7 +495,12 @@ export class GuardianHttpClient {
|
|
|
431
495
|
|
|
432
496
|
if (!response.ok) {
|
|
433
497
|
const body = await response.text();
|
|
434
|
-
throw new GuardianHttpError(
|
|
498
|
+
throw new GuardianHttpError(
|
|
499
|
+
response.status,
|
|
500
|
+
response.statusText,
|
|
501
|
+
body,
|
|
502
|
+
response.headers.get('Retry-After')
|
|
503
|
+
);
|
|
435
504
|
}
|
|
436
505
|
|
|
437
506
|
return response;
|
|
@@ -504,15 +573,16 @@ export class GuardianHttpClient {
|
|
|
504
573
|
},
|
|
505
574
|
});
|
|
506
575
|
} catch (err) {
|
|
507
|
-
// Replay rejections
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
576
|
+
// Replay rejections are transient: the request was correctly signed
|
|
577
|
+
// but lost the server's per-signer timestamp CAS. Retry with a fresh
|
|
578
|
+
// timestamp and signature, branching only on the dedicated
|
|
579
|
+
// `authentication_replay` code (issue #367); terminal authentication
|
|
580
|
+
// failures (invalid signature, clock outside the skew window) are
|
|
581
|
+
// never retried.
|
|
512
582
|
if (
|
|
513
583
|
retries > 0 &&
|
|
514
584
|
err instanceof GuardianHttpError &&
|
|
515
|
-
err.code === '
|
|
585
|
+
err.code === 'authentication_replay'
|
|
516
586
|
) {
|
|
517
587
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
518
588
|
return this.fetchAuthenticated(path, init, accountId, requestPayload, retries - 1);
|
package/src/index.ts
CHANGED
|
@@ -34,4 +34,14 @@ export type {
|
|
|
34
34
|
SignProposalRequest,
|
|
35
35
|
LookupAccount,
|
|
36
36
|
LookupResponse,
|
|
37
|
+
HistoryDecodeSection,
|
|
38
|
+
HistoryDecodeWarning,
|
|
39
|
+
HistoryEntry,
|
|
40
|
+
HistoryEntryStatus,
|
|
41
|
+
HistoryNote,
|
|
42
|
+
HistoryNoteAsset,
|
|
43
|
+
HistoryNoteTag,
|
|
44
|
+
HistoryNoteVisibility,
|
|
45
|
+
HistoryOptions,
|
|
46
|
+
HistoryPage,
|
|
37
47
|
} from './types.js';
|
package/src/server-types.ts
CHANGED
|
@@ -57,6 +57,12 @@ export interface ServerProposalMetadata {
|
|
|
57
57
|
amount?: string;
|
|
58
58
|
/** P2ID note visibility, "public" or "private" (issue #322). Absent => public. */
|
|
59
59
|
note_type?: string;
|
|
60
|
+
/** Base64-serialized Miden `ChainAnchor` pinning the proposal's reference block. */
|
|
61
|
+
chain_anchor?: string;
|
|
62
|
+
/** P2IDE reclaim block height (issue #366). Presence of either height means a P2IDE note. */
|
|
63
|
+
reclaim_height?: number;
|
|
64
|
+
/** P2IDE timelock block height (issue #366). */
|
|
65
|
+
timelock_height?: number;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export interface ServerDeltaObject {
|
|
@@ -179,3 +185,40 @@ export interface ServerLookupAccount {
|
|
|
179
185
|
export interface ServerLookupResponse {
|
|
180
186
|
accounts: ServerLookupAccount[];
|
|
181
187
|
}
|
|
188
|
+
|
|
189
|
+
// --- Delta history (issue #413) ---
|
|
190
|
+
|
|
191
|
+
export interface ServerHistoryNoteAsset {
|
|
192
|
+
asset_id: string;
|
|
193
|
+
kind: 'fungible' | 'non_fungible';
|
|
194
|
+
amount?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface ServerHistoryNote {
|
|
198
|
+
note_id: string;
|
|
199
|
+
tag: 'p2id' | 'p2ide' | 'pswap' | 'mint' | 'burn' | 'custom';
|
|
200
|
+
note_type: 'public' | 'private';
|
|
201
|
+
assets: ServerHistoryNoteAsset[];
|
|
202
|
+
sender?: string;
|
|
203
|
+
recipient?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ServerHistoryDecodeWarning {
|
|
207
|
+
section: 'tx_summary' | 'metadata' | 'input_notes' | 'output_notes' | 'vault' | 'storage';
|
|
208
|
+
reason: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ServerHistoryEntry {
|
|
212
|
+
nonce: number;
|
|
213
|
+
status: 'canonical';
|
|
214
|
+
timestamp: string;
|
|
215
|
+
new_commitment: string | null;
|
|
216
|
+
input_notes: ServerHistoryNote[];
|
|
217
|
+
output_notes: ServerHistoryNote[];
|
|
218
|
+
decode_warnings?: ServerHistoryDecodeWarning[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface ServerHistoryPage {
|
|
222
|
+
items: ServerHistoryEntry[];
|
|
223
|
+
next_cursor: string | null;
|
|
224
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -105,6 +105,17 @@ export interface ProposalMetadata {
|
|
|
105
105
|
amount?: string;
|
|
106
106
|
/** P2ID note visibility, "public" or "private" (issue #322). Absent => public. */
|
|
107
107
|
noteType?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Base64-serialized Miden `ChainAnchor` pinning the reference block the
|
|
110
|
+
* proposal's transaction summary was built at. Since protocol 0.16 the
|
|
111
|
+
* signed summary binds the reference block commitment, so cosigners and the
|
|
112
|
+
* executor need this anchor to reproduce the summary the proposer signed.
|
|
113
|
+
*/
|
|
114
|
+
chainAnchor?: string;
|
|
115
|
+
/** P2IDE reclaim block height (issue #366). Presence of either height means a P2IDE note. */
|
|
116
|
+
reclaimHeight?: number;
|
|
117
|
+
/** P2IDE timelock block height (issue #366). */
|
|
118
|
+
timelockHeight?: number;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
export interface DeltaObject {
|
|
@@ -244,3 +255,90 @@ export interface LookupAccount {
|
|
|
244
255
|
export interface LookupResponse {
|
|
245
256
|
accounts: LookupAccount[];
|
|
246
257
|
}
|
|
258
|
+
|
|
259
|
+
/** Note classification decoded from the on-chain note script. */
|
|
260
|
+
export type HistoryNoteTag = 'p2id' | 'p2ide' | 'pswap' | 'mint' | 'burn' | 'custom';
|
|
261
|
+
|
|
262
|
+
/** On-chain note visibility from the note metadata. */
|
|
263
|
+
export type HistoryNoteVisibility = 'public' | 'private';
|
|
264
|
+
|
|
265
|
+
/** Which section of the persisted payload failed to decode. */
|
|
266
|
+
export type HistoryDecodeSection =
|
|
267
|
+
| 'tx_summary'
|
|
268
|
+
| 'metadata'
|
|
269
|
+
| 'input_notes'
|
|
270
|
+
| 'output_notes'
|
|
271
|
+
| 'vault'
|
|
272
|
+
| 'storage';
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Delta lifecycle status of a history entry. Only `canonical` is
|
|
276
|
+
* emitted today; the set widens if the feed gains a status filter.
|
|
277
|
+
*/
|
|
278
|
+
export type HistoryEntryStatus = 'canonical';
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* One decoded asset inside a history note. `amount` is a base-10
|
|
282
|
+
* string for fungible assets, absent for non-fungible ones.
|
|
283
|
+
*/
|
|
284
|
+
export interface HistoryNoteAsset {
|
|
285
|
+
assetId: string;
|
|
286
|
+
kind: 'fungible' | 'non_fungible';
|
|
287
|
+
amount?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* One decoded note attached to a history entry. `sender` / `recipient`
|
|
292
|
+
* are account IDs when the note script exposes them.
|
|
293
|
+
*/
|
|
294
|
+
export interface HistoryNote {
|
|
295
|
+
noteId: string;
|
|
296
|
+
tag: HistoryNoteTag;
|
|
297
|
+
/** On-chain visibility from the note metadata. */
|
|
298
|
+
noteType: HistoryNoteVisibility;
|
|
299
|
+
assets: HistoryNoteAsset[];
|
|
300
|
+
sender?: string;
|
|
301
|
+
recipient?: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Why a history entry's note sections are empty: the persisted payload
|
|
306
|
+
* could not be decoded server-side (schema drift). The entry itself is
|
|
307
|
+
* still returned.
|
|
308
|
+
*/
|
|
309
|
+
export interface HistoryDecodeWarning {
|
|
310
|
+
section: HistoryDecodeSection;
|
|
311
|
+
reason: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** One canonical transaction in an account's history (issue #413). */
|
|
315
|
+
export interface HistoryEntry {
|
|
316
|
+
nonce: number;
|
|
317
|
+
/** Delta lifecycle status; always `canonical` today. */
|
|
318
|
+
status: HistoryEntryStatus;
|
|
319
|
+
/** RFC 3339 UTC timestamp at which the delta became canonical. */
|
|
320
|
+
timestamp: string;
|
|
321
|
+
/**
|
|
322
|
+
* Account commitment after this transaction; `undefined` when the
|
|
323
|
+
* stored row predates commitment recording.
|
|
324
|
+
*/
|
|
325
|
+
newCommitment?: string;
|
|
326
|
+
inputNotes: HistoryNote[];
|
|
327
|
+
outputNotes: HistoryNote[];
|
|
328
|
+
decodeWarnings: HistoryDecodeWarning[];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** One page of canonical delta history, newest-first by nonce. */
|
|
332
|
+
export interface HistoryPage {
|
|
333
|
+
entries: HistoryEntry[];
|
|
334
|
+
/** Opaque resume token; `undefined` when the feed is exhausted. */
|
|
335
|
+
nextCursor?: string;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Options for `getDeltaHistory`. */
|
|
339
|
+
export interface HistoryOptions {
|
|
340
|
+
/** Page size in `[1, 500]`; server default 50 when omitted. */
|
|
341
|
+
limit?: number;
|
|
342
|
+
/** Opaque `nextCursor` from a previous page. */
|
|
343
|
+
cursor?: string;
|
|
344
|
+
}
|