@openzeppelin/guardian-client 0.18.3 → 0.19.0-rc.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 +127 -9
- package/dist/auth-request.d.ts +13 -0
- package/dist/auth-request.d.ts.map +1 -1
- package/dist/auth-request.js +26 -5
- package/dist/auth-request.js.map +1 -1
- package/dist/conversion.d.ts +8 -2
- package/dist/conversion.d.ts.map +1 -1
- package/dist/conversion.js +46 -3
- package/dist/conversion.js.map +1 -1
- package/dist/error-codes.d.ts +3 -5
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +23 -4
- package/dist/error-codes.js.map +1 -1
- package/dist/execution.d.ts +99 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +124 -0
- package/dist/execution.js.map +1 -0
- package/dist/http.d.ts +81 -5
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +179 -27
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/request-envelope.d.ts +22 -0
- package/dist/request-envelope.d.ts.map +1 -0
- package/dist/request-envelope.js +51 -0
- package/dist/request-envelope.js.map +1 -0
- package/dist/server-types.d.ts +33 -0
- package/dist/server-types.d.ts.map +1 -1
- package/dist/session-request.d.ts +104 -0
- package/dist/session-request.d.ts.map +1 -0
- package/dist/session-request.js +164 -0
- package/dist/session-request.js.map +1 -0
- package/dist/types.d.ts +124 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/auth-request.test.ts +12 -0
- package/src/auth-request.ts +37 -5
- package/src/conversion.test.ts +98 -0
- package/src/conversion.ts +59 -3
- package/src/dependencies.test.ts +25 -0
- package/src/error-codes.test.ts +14 -1
- package/src/error-codes.ts +23 -4
- package/src/execution.test.ts +145 -0
- package/src/execution.ts +206 -0
- package/src/http.test.ts +752 -19
- package/src/http.ts +244 -30
- package/src/index.ts +50 -1
- package/src/request-envelope.test.ts +30 -0
- package/src/request-envelope.ts +68 -0
- package/src/server-types.ts +35 -1
- package/src/session-request.test.ts +178 -0
- package/src/session-request.ts +191 -0
- package/src/types.ts +129 -4
package/src/http.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_REQUEST_AUTH_FORMAT,
|
|
3
|
+
SessionOperation,
|
|
4
|
+
isSessionWrite,
|
|
5
|
+
} from './session-request.js';
|
|
1
6
|
import {
|
|
2
7
|
type GuardianErrorCode,
|
|
3
8
|
normalizeGuardianErrorCode,
|
|
@@ -24,10 +29,17 @@ import type {
|
|
|
24
29
|
SignatureScheme,
|
|
25
30
|
Signer,
|
|
26
31
|
StateObject,
|
|
32
|
+
StateTail,
|
|
27
33
|
StatusResponse,
|
|
28
34
|
CanonicalNonce,
|
|
29
35
|
} from './types.js';
|
|
30
36
|
import { RequestAuthPayload } from './auth-request.js';
|
|
37
|
+
import { fromServerExecution, fromServerExecutionCapability } from './execution.js';
|
|
38
|
+
import type {
|
|
39
|
+
ProposalExecution,
|
|
40
|
+
ServerCurrentExecution,
|
|
41
|
+
ServerProposalExecution,
|
|
42
|
+
} from './execution.js';
|
|
31
43
|
import type {
|
|
32
44
|
ServerAbandonCandidateRequest,
|
|
33
45
|
ServerAbandonCandidateResponse,
|
|
@@ -39,6 +51,7 @@ import type {
|
|
|
39
51
|
ServerProposalsResponse,
|
|
40
52
|
ServerPubkeyResponse,
|
|
41
53
|
ServerStateObject,
|
|
54
|
+
ServerStateTail,
|
|
42
55
|
ServerConfigureResponse,
|
|
43
56
|
ServerCreateSessionResponse,
|
|
44
57
|
ServerPushDeltaResponse,
|
|
@@ -55,6 +68,7 @@ import {
|
|
|
55
68
|
fromServerHistoryPage,
|
|
56
69
|
fromServerLookupResponse,
|
|
57
70
|
fromServerStateObject,
|
|
71
|
+
fromServerStateTail,
|
|
58
72
|
toServerConfigureRequest,
|
|
59
73
|
toServerCreateSessionRequest,
|
|
60
74
|
toServerDeltaProposalRequest,
|
|
@@ -84,6 +98,33 @@ export interface GuardianErrorMeta {
|
|
|
84
98
|
* `code === 'signature_scheme_not_allowed'`.
|
|
85
99
|
*/
|
|
86
100
|
allowedSchemes?: string[];
|
|
101
|
+
/**
|
|
102
|
+
* The proposal whose execution holds the account. Present only when
|
|
103
|
+
* `code === 'execution_conflict'`.
|
|
104
|
+
*/
|
|
105
|
+
blockingProposalId?: string;
|
|
106
|
+
/**
|
|
107
|
+
* The proposal type GUARDIAN refuses to execute because the client that finishes its
|
|
108
|
+
* follow-up work must execute it. Present only when `code === 'proposal_executes_locally'`.
|
|
109
|
+
*/
|
|
110
|
+
proposalType?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Why GUARDIAN leaves the proposal to local execution. Present only when
|
|
113
|
+
* `code === 'proposal_executes_locally'`: `switch_guardian` for a guardian switch.
|
|
114
|
+
*/
|
|
115
|
+
reason?: LocalExecutionReason;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Why GUARDIAN refuses to execute a proposal and leaves it to local execution. */
|
|
119
|
+
export type LocalExecutionReason = 'switch_guardian';
|
|
120
|
+
|
|
121
|
+
function localExecutionReason(value: unknown): LocalExecutionReason | undefined {
|
|
122
|
+
switch (value) {
|
|
123
|
+
case 'switch_guardian':
|
|
124
|
+
return value;
|
|
125
|
+
default:
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
87
128
|
}
|
|
88
129
|
|
|
89
130
|
interface ParsedGuardianError {
|
|
@@ -142,6 +183,12 @@ function parseGuardianErrorBody(body: string): ParsedGuardianError | undefined {
|
|
|
142
183
|
) {
|
|
143
184
|
meta.allowedSchemes = rawMeta.allowed_schemes;
|
|
144
185
|
}
|
|
186
|
+
if (typeof rawMeta.blocking_proposal_id === 'string') {
|
|
187
|
+
meta.blockingProposalId = rawMeta.blocking_proposal_id;
|
|
188
|
+
}
|
|
189
|
+
if (typeof rawMeta.proposal_type === 'string') meta.proposalType = rawMeta.proposal_type;
|
|
190
|
+
const reason = localExecutionReason(rawMeta.reason);
|
|
191
|
+
if (reason !== undefined) meta.reason = reason;
|
|
145
192
|
if (typeof rawMeta.paused_reason === 'string' || rawMeta.paused_reason === null) {
|
|
146
193
|
meta.pausedReason = rawMeta.paused_reason as string | null;
|
|
147
194
|
}
|
|
@@ -242,6 +289,9 @@ function parseRetryAfterSeconds(header: string | null | undefined): number | und
|
|
|
242
289
|
*/
|
|
243
290
|
const SESSION_EXPIRY_MARGIN_MS = 30_000;
|
|
244
291
|
|
|
292
|
+
/** Replay rejections a request is re-signed and resent for. */
|
|
293
|
+
const REPLAY_RETRIES = 2;
|
|
294
|
+
|
|
245
295
|
/** Codes that end a session-signed request's session for good. */
|
|
246
296
|
const SESSION_END_REASONS: Record<string, SessionEndReason> = {
|
|
247
297
|
session_expired: 'expired',
|
|
@@ -253,13 +303,13 @@ const SESSION_END_REASONS: Record<string, SessionEndReason> = {
|
|
|
253
303
|
* Which key signs a per-account request. Mirrors the server: requests are
|
|
254
304
|
* wallet-signed unless the route is session-eligible.
|
|
255
305
|
*/
|
|
256
|
-
type AuthMode =
|
|
306
|
+
type AuthMode = SessionOperation | 'wallet';
|
|
257
307
|
|
|
258
308
|
function authHeaders(
|
|
259
309
|
publicKey: string,
|
|
260
310
|
signature: string,
|
|
261
311
|
timestamp: number,
|
|
262
|
-
format?: 'eip712' |
|
|
312
|
+
format?: 'eip712' | typeof SESSION_REQUEST_AUTH_FORMAT
|
|
263
313
|
): Record<string, string> {
|
|
264
314
|
return {
|
|
265
315
|
'x-pubkey': publicKey,
|
|
@@ -301,8 +351,8 @@ export class GuardianHttpClient {
|
|
|
301
351
|
|
|
302
352
|
/**
|
|
303
353
|
* Sign session-eligible requests (reads, proposal list/get, proposal
|
|
304
|
-
* create/sign) with a registered delegated signer instead of
|
|
305
|
-
* Every other route, including `configure`,
|
|
354
|
+
* create/sign, `pushDelta`) with a registered delegated signer instead of
|
|
355
|
+
* the wallet. Every other route, including `configure`,
|
|
306
356
|
* `abandonCandidate`, account lookup and `revokeAllSessions`, uses the
|
|
307
357
|
* wallet signer. Pass `null` to stop using the session.
|
|
308
358
|
*/
|
|
@@ -434,10 +484,16 @@ export class GuardianHttpClient {
|
|
|
434
484
|
environment: data.environment,
|
|
435
485
|
startedAt: data.started_at,
|
|
436
486
|
uptimeSeconds: data.uptime_seconds,
|
|
487
|
+
...(data.execution === undefined || data.execution === null
|
|
488
|
+
? {}
|
|
489
|
+
: { execution: fromServerExecutionCapability(data.execution) }),
|
|
437
490
|
...(data.sessions
|
|
438
491
|
? {
|
|
439
492
|
sessions: {
|
|
440
493
|
maxTtlSeconds: data.sessions.max_ttl_seconds,
|
|
494
|
+
...(data.sessions.request_auth_version === undefined
|
|
495
|
+
? {}
|
|
496
|
+
: { requestAuthVersion: data.sessions.request_auth_version }),
|
|
441
497
|
},
|
|
442
498
|
}
|
|
443
499
|
: {}),
|
|
@@ -460,11 +516,35 @@ export class GuardianHttpClient {
|
|
|
460
516
|
const params = new URLSearchParams(requestQuery);
|
|
461
517
|
const response = await this.fetchAuthenticated(`/state?${params}`, {
|
|
462
518
|
method: 'GET',
|
|
463
|
-
}, accountId, requestQuery,
|
|
519
|
+
}, accountId, requestQuery, SessionOperation.GetState);
|
|
464
520
|
const server = (await response.json()) as ServerStateObject;
|
|
465
521
|
return fromServerStateObject(server);
|
|
466
522
|
}
|
|
467
523
|
|
|
524
|
+
/**
|
|
525
|
+
* The queue tail (`GET /state/tail`, issue #517): the state a new delta or
|
|
526
|
+
* proposal must build on, and the queued candidates that lead to it. With
|
|
527
|
+
* nothing queued it is the canonical state and `queue` is empty. Otherwise
|
|
528
|
+
* it is not on chain until the queued candidates land, and if one never
|
|
529
|
+
* does, the tail is abandoned with it. `acceptsSubmissions` is false while
|
|
530
|
+
* the queue would refuse anything built on the tail with
|
|
531
|
+
* `conflict_pending_delta` (a full queue, which at the default depth of 1 is
|
|
532
|
+
* any queue; a tail that changes the signers or the guardian key; a
|
|
533
|
+
* candidate a server before 0.19.0 queued under another label): wait for
|
|
534
|
+
* the queue to drain instead of building. Each read replays the queue on the
|
|
535
|
+
* server, so read it before building on the account; poll
|
|
536
|
+
* `getCanonicalNonce` instead. A server before 0.19.0 answers 404.
|
|
537
|
+
*/
|
|
538
|
+
async getStateTail(accountId: string): Promise<StateTail> {
|
|
539
|
+
const requestQuery = { account_id: accountId };
|
|
540
|
+
const params = new URLSearchParams(requestQuery);
|
|
541
|
+
const response = await this.fetchAuthenticated(`/state/tail?${params}`, {
|
|
542
|
+
method: 'GET',
|
|
543
|
+
}, accountId, requestQuery, SessionOperation.GetStateTail);
|
|
544
|
+
const server = (await response.json()) as ServerStateTail;
|
|
545
|
+
return fromServerStateTail(server);
|
|
546
|
+
}
|
|
547
|
+
|
|
468
548
|
/**
|
|
469
549
|
* Nonce and commitment of the latest canonical state, without the state
|
|
470
550
|
* blob (`GET /state/nonce`). A client can skip `getState` when the
|
|
@@ -477,7 +557,7 @@ export class GuardianHttpClient {
|
|
|
477
557
|
const params = new URLSearchParams(requestQuery);
|
|
478
558
|
const response = await this.fetchAuthenticated(`/state/nonce?${params}`, {
|
|
479
559
|
method: 'GET',
|
|
480
|
-
}, accountId, requestQuery,
|
|
560
|
+
}, accountId, requestQuery, SessionOperation.GetCanonicalNonce);
|
|
481
561
|
const server = (await response.json()) as ServerCanonicalNonceResponse;
|
|
482
562
|
return fromServerCanonicalNonce(server);
|
|
483
563
|
}
|
|
@@ -504,7 +584,7 @@ export class GuardianHttpClient {
|
|
|
504
584
|
const params = new URLSearchParams(requestQuery);
|
|
505
585
|
const response = await this.fetchAuthenticated(`/delta/proposal?${params}`, {
|
|
506
586
|
method: 'GET',
|
|
507
|
-
}, accountId, requestQuery,
|
|
587
|
+
}, accountId, requestQuery, SessionOperation.GetDeltaProposals);
|
|
508
588
|
const data = (await response.json()) as ServerProposalsResponse;
|
|
509
589
|
return data.proposals.map(fromServerDeltaObject);
|
|
510
590
|
}
|
|
@@ -514,7 +594,7 @@ export class GuardianHttpClient {
|
|
|
514
594
|
const params = new URLSearchParams(requestQuery);
|
|
515
595
|
const response = await this.fetchAuthenticated(`/delta/proposal/single?${params}`, {
|
|
516
596
|
method: 'GET',
|
|
517
|
-
}, accountId, requestQuery,
|
|
597
|
+
}, accountId, requestQuery, SessionOperation.GetDeltaProposal);
|
|
518
598
|
const data = (await response.json()) as ServerDeltaObject;
|
|
519
599
|
return fromServerDeltaObject(data);
|
|
520
600
|
}
|
|
@@ -524,7 +604,7 @@ export class GuardianHttpClient {
|
|
|
524
604
|
const response = await this.fetchAuthenticated('/delta/proposal', {
|
|
525
605
|
method: 'POST',
|
|
526
606
|
body: JSON.stringify(serverRequest),
|
|
527
|
-
}, request.accountId, serverRequest,
|
|
607
|
+
}, request.accountId, serverRequest, SessionOperation.PushDeltaProposal);
|
|
528
608
|
const server = (await response.json()) as ServerDeltaProposalResponse;
|
|
529
609
|
return {
|
|
530
610
|
delta: fromServerDeltaObject(server.delta),
|
|
@@ -532,6 +612,48 @@ export class GuardianHttpClient {
|
|
|
532
612
|
};
|
|
533
613
|
}
|
|
534
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Ask Guardian to prove and submit a threshold-met proposal. Resolves once the request is
|
|
617
|
+
* accepted; poll {@link getDeltaProposalExecution} for the outcome. Repeating the request while
|
|
618
|
+
* the execution runs returns it with `newlyAccepted: false`.
|
|
619
|
+
*
|
|
620
|
+
* Signed by the session while one is set, over the payload tagged as `ExecuteDeltaProposal`:
|
|
621
|
+
* the body names the same fields as a status read, and the tag keeps a signed read from
|
|
622
|
+
* authenticating an execution request.
|
|
623
|
+
*/
|
|
624
|
+
async executeDeltaProposal(accountId: string, proposalId: string): Promise<ProposalExecution> {
|
|
625
|
+
const serverRequest = {
|
|
626
|
+
account_id: accountId,
|
|
627
|
+
proposal_id: proposalId,
|
|
628
|
+
};
|
|
629
|
+
const response = await this.fetchSigned('/delta/proposal/execution', {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
body: JSON.stringify(serverRequest),
|
|
632
|
+
}, accountId, RequestAuthPayload.forOperation('ExecuteDeltaProposal', serverRequest), SessionOperation.ExecuteDeltaProposal);
|
|
633
|
+
return fromServerExecution((await response.json()) as ServerProposalExecution);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** The latest execution attempt of a proposal. */
|
|
637
|
+
async getDeltaProposalExecution(accountId: string, proposalId: string): Promise<ProposalExecution> {
|
|
638
|
+
const requestQuery = { account_id: accountId, proposal_id: proposalId };
|
|
639
|
+
const params = new URLSearchParams(requestQuery);
|
|
640
|
+
const response = await this.fetchAuthenticated(`/delta/proposal/execution?${params}`, {
|
|
641
|
+
method: 'GET',
|
|
642
|
+
}, accountId, requestQuery, SessionOperation.GetDeltaProposalExecution);
|
|
643
|
+
return fromServerExecution((await response.json()) as ServerProposalExecution);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** The account's in-flight execution, or `null`. A finished execution is not in flight. */
|
|
647
|
+
async getCurrentExecution(accountId: string): Promise<ProposalExecution | null> {
|
|
648
|
+
const requestQuery = { account_id: accountId };
|
|
649
|
+
const params = new URLSearchParams(requestQuery);
|
|
650
|
+
const response = await this.fetchAuthenticated(`/delta/execution/current?${params}`, {
|
|
651
|
+
method: 'GET',
|
|
652
|
+
}, accountId, requestQuery, SessionOperation.GetCurrentExecution);
|
|
653
|
+
const server = (await response.json()) as ServerCurrentExecution;
|
|
654
|
+
return server.execution === null ? null : fromServerExecution(server.execution);
|
|
655
|
+
}
|
|
656
|
+
|
|
535
657
|
/**
|
|
536
658
|
* Request abandonment of a pending canonicalization candidate whose
|
|
537
659
|
* transaction will never land on-chain (issue #319).
|
|
@@ -544,9 +666,22 @@ export class GuardianHttpClient {
|
|
|
544
666
|
* for the resolution. Refused with `GUARDIAN_CANDIDATE_LANDED` (409)
|
|
545
667
|
* when the transaction actually landed. Retries are idempotent and
|
|
546
668
|
* preserve the original request timestamp.
|
|
669
|
+
*
|
|
670
|
+
* `proposalId` names the transaction: its summary's commitment, which
|
|
671
|
+
* is the proposal id of a multisig proposal. A Guardian execution can
|
|
672
|
+
* archive the delta and take its nonce; the abandon then still answers
|
|
673
|
+
* for this transaction.
|
|
547
674
|
*/
|
|
548
|
-
async abandonCandidate(
|
|
549
|
-
|
|
675
|
+
async abandonCandidate(
|
|
676
|
+
accountId: string,
|
|
677
|
+
nonce: number,
|
|
678
|
+
proposalId: string,
|
|
679
|
+
): Promise<AbandonCandidateResponse> {
|
|
680
|
+
const serverRequest: ServerAbandonCandidateRequest = {
|
|
681
|
+
account_id: accountId,
|
|
682
|
+
nonce,
|
|
683
|
+
proposal_id: proposalId,
|
|
684
|
+
};
|
|
550
685
|
// Abandoning changes the account's canonical chain: always the wallet.
|
|
551
686
|
const response = await this.fetchAuthenticated('/delta/candidate/abandon', {
|
|
552
687
|
method: 'POST',
|
|
@@ -558,6 +693,7 @@ export class GuardianHttpClient {
|
|
|
558
693
|
nonce: server.nonce,
|
|
559
694
|
state: server.state,
|
|
560
695
|
abandonRequestedAt: server.abandon_requested_at,
|
|
696
|
+
commitment: server.commitment,
|
|
561
697
|
};
|
|
562
698
|
}
|
|
563
699
|
|
|
@@ -572,8 +708,14 @@ export class GuardianHttpClient {
|
|
|
572
708
|
* delta until its retention TTL expires — sync and check chain before
|
|
573
709
|
* replacing it), `'unexpected'` for any state no abandon flow
|
|
574
710
|
* produces (including a missing delta).
|
|
711
|
+
*
|
|
712
|
+
* `commitment` is the one {@link abandonCandidate} returned. Another
|
|
713
|
+
* delta can take the nonce (a Guardian execution archives the abandoned
|
|
714
|
+
* one): once it is canonical the abandoned transaction can no longer
|
|
715
|
+
* land (`'abandoned'`), and while it is a candidate the nonce is still
|
|
716
|
+
* contested (`'waiting'`).
|
|
575
717
|
*/
|
|
576
|
-
async abandonStatus(accountId: string, nonce: number): Promise<AbandonStatus> {
|
|
718
|
+
async abandonStatus(accountId: string, nonce: number, commitment: string): Promise<AbandonStatus> {
|
|
577
719
|
let delta: DeltaObject;
|
|
578
720
|
try {
|
|
579
721
|
delta = await this.getDelta(accountId, nonce);
|
|
@@ -583,6 +725,16 @@ export class GuardianHttpClient {
|
|
|
583
725
|
}
|
|
584
726
|
throw e;
|
|
585
727
|
}
|
|
728
|
+
if (delta.newCommitment?.toLowerCase() !== commitment.toLowerCase()) {
|
|
729
|
+
switch (delta.status.status) {
|
|
730
|
+
case 'canonical':
|
|
731
|
+
return 'abandoned';
|
|
732
|
+
case 'candidate':
|
|
733
|
+
return 'waiting';
|
|
734
|
+
default:
|
|
735
|
+
return 'unexpected';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
586
738
|
switch (delta.status.status) {
|
|
587
739
|
case 'candidate':
|
|
588
740
|
return 'waiting';
|
|
@@ -607,18 +759,35 @@ export class GuardianHttpClient {
|
|
|
607
759
|
const response = await this.fetchAuthenticated('/delta/proposal', {
|
|
608
760
|
method: 'PUT',
|
|
609
761
|
body: JSON.stringify(serverRequest),
|
|
610
|
-
}, request.accountId, serverRequest,
|
|
762
|
+
}, request.accountId, serverRequest, SessionOperation.SignDeltaProposal);
|
|
611
763
|
const server = (await response.json()) as ServerDeltaObject;
|
|
612
764
|
return fromServerDeltaObject(server);
|
|
613
765
|
}
|
|
614
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Push a delta for Guardian's acknowledgement. Signed by the session while
|
|
769
|
+
* one is set: Guardian acknowledges a multisig delta only once its
|
|
770
|
+
* proposal carries enough verified cosigner approvals, which the wallet
|
|
771
|
+
* signs. A Guardian that takes this route only from the wallet (an older
|
|
772
|
+
* release, or an account without a multisig state) answers
|
|
773
|
+
* `wallet_signature_required`, and the wallet signs the push again; the
|
|
774
|
+
* session stays in use for every other route.
|
|
775
|
+
*/
|
|
615
776
|
async pushDelta(delta: ExecutionDelta): Promise<PushDeltaResponse> {
|
|
616
777
|
const serverDelta = toServerExecutionDelta(delta);
|
|
617
|
-
|
|
618
|
-
const response = await this.fetchAuthenticated(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
778
|
+
const init = { method: 'POST', body: JSON.stringify(serverDelta) };
|
|
779
|
+
const response = await this.fetchAuthenticated(
|
|
780
|
+
'/delta',
|
|
781
|
+
init,
|
|
782
|
+
delta.accountId,
|
|
783
|
+
serverDelta,
|
|
784
|
+
SessionOperation.PushDelta
|
|
785
|
+
).catch((err: unknown) => {
|
|
786
|
+
if (err instanceof GuardianHttpError && err.code === 'wallet_signature_required') {
|
|
787
|
+
return this.fetchAuthenticated('/delta', init, delta.accountId, serverDelta, 'wallet');
|
|
788
|
+
}
|
|
789
|
+
throw err;
|
|
790
|
+
});
|
|
622
791
|
const server = (await response.json()) as ServerPushDeltaResponse;
|
|
623
792
|
return {
|
|
624
793
|
accountId: server.account_id,
|
|
@@ -642,7 +811,7 @@ export class GuardianHttpClient {
|
|
|
642
811
|
const params = new URLSearchParams(requestQuery);
|
|
643
812
|
const response = await this.fetchAuthenticated(`/delta?${params}`, {
|
|
644
813
|
method: 'GET',
|
|
645
|
-
}, accountId, requestPayload,
|
|
814
|
+
}, accountId, requestPayload, SessionOperation.GetDelta);
|
|
646
815
|
const server = (await response.json()) as ServerDeltaObject;
|
|
647
816
|
return fromServerDeltaObject(server);
|
|
648
817
|
}
|
|
@@ -659,7 +828,7 @@ export class GuardianHttpClient {
|
|
|
659
828
|
const params = new URLSearchParams(requestQuery);
|
|
660
829
|
const response = await this.fetchAuthenticated(`/delta/since?${params}`, {
|
|
661
830
|
method: 'GET',
|
|
662
|
-
}, accountId, requestPayload,
|
|
831
|
+
}, accountId, requestPayload, SessionOperation.GetDeltaSince);
|
|
663
832
|
const server = (await response.json()) as ServerDeltaObject;
|
|
664
833
|
return fromServerDeltaObject(server);
|
|
665
834
|
}
|
|
@@ -686,7 +855,7 @@ export class GuardianHttpClient {
|
|
|
686
855
|
const params = new URLSearchParams(requestPayload);
|
|
687
856
|
const response = await this.fetchAuthenticated(`/delta/history?${params}`, {
|
|
688
857
|
method: 'GET',
|
|
689
|
-
}, accountId, requestPayload,
|
|
858
|
+
}, accountId, requestPayload, SessionOperation.GetDeltaHistory);
|
|
690
859
|
const server = (await response.json()) as ServerHistoryPage;
|
|
691
860
|
return fromServerHistoryPage(server);
|
|
692
861
|
}
|
|
@@ -753,20 +922,65 @@ export class GuardianHttpClient {
|
|
|
753
922
|
init: RequestInit,
|
|
754
923
|
accountId: string,
|
|
755
924
|
requestPayload: unknown,
|
|
756
|
-
mode: AuthMode = 'wallet'
|
|
757
|
-
retries = 2
|
|
925
|
+
mode: AuthMode = 'wallet'
|
|
758
926
|
): Promise<Response> {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
927
|
+
return this.fetchSigned(path, init, accountId, RequestAuthPayload.fromRequest(requestPayload), mode);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private async fetchSigned(
|
|
931
|
+
path: string,
|
|
932
|
+
init: RequestInit,
|
|
933
|
+
accountId: string,
|
|
934
|
+
authPayload: RequestAuthPayload,
|
|
935
|
+
mode: AuthMode
|
|
936
|
+
): Promise<Response> {
|
|
937
|
+
for (;;) {
|
|
938
|
+
const session = mode === 'wallet' ? null : this.getSession();
|
|
939
|
+
const response =
|
|
940
|
+
session && mode !== 'wallet' && isSessionWrite(mode)
|
|
941
|
+
? await session.writeSequencer.run(accountId, () =>
|
|
942
|
+
this.sendSigned(path, init, accountId, authPayload, mode, session)
|
|
943
|
+
)
|
|
944
|
+
: await this.sendSigned(path, init, accountId, authPayload, mode, session);
|
|
945
|
+
if (response) {
|
|
946
|
+
return response;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Signs with `session` (the wallet when null) and retries replay
|
|
953
|
+
* rejections. Returns null, without sending, once `session` is no longer
|
|
954
|
+
* this client's session: a session write only ever signs within the write
|
|
955
|
+
* turn of the session it holds, and the caller starts over with the
|
|
956
|
+
* current one.
|
|
957
|
+
*/
|
|
958
|
+
private async sendSigned(
|
|
959
|
+
path: string,
|
|
960
|
+
init: RequestInit,
|
|
961
|
+
accountId: string,
|
|
962
|
+
authPayload: RequestAuthPayload,
|
|
963
|
+
mode: AuthMode,
|
|
964
|
+
session: SessionRequestSigner | null,
|
|
965
|
+
retries = REPLAY_RETRIES
|
|
966
|
+
): Promise<Response | null> {
|
|
967
|
+
if (session && this.getSession() !== session) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const timestamp = session
|
|
971
|
+
? await session.nextTimestamp(accountId, retries < REPLAY_RETRIES)
|
|
972
|
+
: this.nextTimestamp();
|
|
973
|
+
const headers = session && mode !== 'wallet'
|
|
763
974
|
? authHeaders(
|
|
764
975
|
session.publicKey,
|
|
765
|
-
await session.signRequest(accountId, timestamp, authPayload),
|
|
976
|
+
await session.signRequest(mode, accountId, timestamp, authPayload),
|
|
766
977
|
timestamp,
|
|
767
|
-
|
|
978
|
+
SESSION_REQUEST_AUTH_FORMAT
|
|
768
979
|
)
|
|
769
980
|
: await this.walletAuthHeaders(accountId, timestamp, authPayload);
|
|
981
|
+
if (session && this.getSession() !== session) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
770
984
|
|
|
771
985
|
try {
|
|
772
986
|
return await this.fetch(path, {
|
|
@@ -799,7 +1013,7 @@ export class GuardianHttpClient {
|
|
|
799
1013
|
err.code === 'authentication_replay'
|
|
800
1014
|
) {
|
|
801
1015
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
802
|
-
return this.
|
|
1016
|
+
return this.sendSigned(path, init, accountId, authPayload, mode, session, retries - 1);
|
|
803
1017
|
}
|
|
804
1018
|
throw err;
|
|
805
1019
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { GuardianHttpClient, GuardianHttpError } from './http.js';
|
|
2
|
-
export type { GuardianErrorMeta } from './http.js';
|
|
2
|
+
export type { GuardianErrorMeta, LocalExecutionReason } from './http.js';
|
|
3
3
|
export {
|
|
4
4
|
GUARDIAN_ERROR_CODES,
|
|
5
5
|
isGuardianErrorCode,
|
|
@@ -7,6 +7,52 @@ export {
|
|
|
7
7
|
} from './error-codes.js';
|
|
8
8
|
export type { GuardianErrorCode } from './error-codes.js';
|
|
9
9
|
export { RequestAuthPayload } from './auth-request.js';
|
|
10
|
+
export {
|
|
11
|
+
SESSION_REQUEST_AUTH_FORMAT,
|
|
12
|
+
SESSION_REQUEST_ENVELOPE_LENGTH,
|
|
13
|
+
SESSION_REQUEST_VERSION,
|
|
14
|
+
SESSION_TIMESTAMP_MAX_AHEAD_MS,
|
|
15
|
+
SessionOperation,
|
|
16
|
+
SessionTimestamps,
|
|
17
|
+
WriteSequencer,
|
|
18
|
+
isSessionWrite,
|
|
19
|
+
SessionTransport,
|
|
20
|
+
UnsupportedAuthVersionError,
|
|
21
|
+
assertSupportedSessionPolicy,
|
|
22
|
+
sessionRequestEnvelope,
|
|
23
|
+
} from './session-request.js';
|
|
24
|
+
export type { SignedOperation } from './auth-request.js';
|
|
25
|
+
export {
|
|
26
|
+
ENVELOPE_FORMAT_VERSION,
|
|
27
|
+
PROTOCOL_LINE,
|
|
28
|
+
decodeTransactionRequestEnvelope,
|
|
29
|
+
sealTransactionRequest,
|
|
30
|
+
} from './request-envelope.js';
|
|
31
|
+
export type { TransactionRequestEnvelope } from './request-envelope.js';
|
|
32
|
+
export {
|
|
33
|
+
EXECUTION_FAILURE_CODES,
|
|
34
|
+
EXECUTION_STATES,
|
|
35
|
+
EXECUTION_UNAVAILABLE_REASONS,
|
|
36
|
+
EXPIRATION_BOUNDS,
|
|
37
|
+
FOREIGN_ACCOUNT_UNAVAILABLE_REASONS,
|
|
38
|
+
PLAIN_EXECUTION_FAILURE_CODES,
|
|
39
|
+
REQUEST_INVALID_REASONS,
|
|
40
|
+
fromServerExecution,
|
|
41
|
+
fromServerExecutionCapability,
|
|
42
|
+
isTerminalExecutionState,
|
|
43
|
+
} from './execution.js';
|
|
44
|
+
export type {
|
|
45
|
+
ExecutionFailure,
|
|
46
|
+
ExecutionFailureCode,
|
|
47
|
+
ExecutionState,
|
|
48
|
+
ExecutionUnavailableReason,
|
|
49
|
+
ExpirationBound,
|
|
50
|
+
ForeignAccountUnavailableReason,
|
|
51
|
+
PlainExecutionFailureCode,
|
|
52
|
+
ProposalExecution,
|
|
53
|
+
RequestInvalidReason,
|
|
54
|
+
ServerExecutionCapability,
|
|
55
|
+
} from './execution.js';
|
|
10
56
|
|
|
11
57
|
export type {
|
|
12
58
|
AbandonCandidateResponse,
|
|
@@ -18,11 +64,14 @@ export type {
|
|
|
18
64
|
SignatureScheme,
|
|
19
65
|
CosignerSignature,
|
|
20
66
|
AuthConfig,
|
|
67
|
+
MidenCosignerConfig,
|
|
21
68
|
CanonicalNonce,
|
|
22
69
|
DeltaStatus,
|
|
23
70
|
DeltaObject,
|
|
24
71
|
ExecutionDelta,
|
|
25
72
|
StateObject,
|
|
73
|
+
StateTail,
|
|
74
|
+
QueuedCandidate,
|
|
26
75
|
ProposalType,
|
|
27
76
|
ProposalMetadata,
|
|
28
77
|
ConfigureRequest,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { PROTOCOL_LINE, sealTransactionRequest } from './request-envelope.js';
|
|
6
|
+
|
|
7
|
+
describe('sealTransactionRequest', () => {
|
|
8
|
+
it('seals bytes with this SDK\'s protocol line, checksum and base64 body', async () => {
|
|
9
|
+
const envelope = await sealTransactionRequest(new TextEncoder().encode('abc'));
|
|
10
|
+
expect(envelope).toEqual({
|
|
11
|
+
format_version: 1,
|
|
12
|
+
protocol_line: '0.17',
|
|
13
|
+
checksum: '0xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
|
14
|
+
bytes: 'YWJj',
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('the shared envelope fixture', () => {
|
|
20
|
+
it('seals to the shared vector for this SDK\'s protocol line', async () => {
|
|
21
|
+
const fixture = JSON.parse(
|
|
22
|
+
readFileSync(
|
|
23
|
+
join(dirname(fileURLToPath(import.meta.url)), '../../../fixtures/miden-multisig-client/request-envelope.json'),
|
|
24
|
+
'utf8',
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
const bytes = Uint8Array.from(Buffer.from(fixture.bytes_hex, 'hex'));
|
|
28
|
+
expect(await sealTransactionRequest(bytes)).toEqual(fixture.sealed[PROTOCOL_LINE]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The envelope a Guardian-executable proposal stores its serialized `TransactionRequest` in.
|
|
3
|
+
* Mirrors `guardian_shared::request_envelope`; the same bytes seal to the same envelope in both
|
|
4
|
+
* SDKs, apart from the protocol line each one declares.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const ENVELOPE_FORMAT_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
/** The Miden protocol line this SDK serializes requests for, as `MAJOR.MINOR`: that of its web SDK pin. */
|
|
10
|
+
export const PROTOCOL_LINE = '0.17';
|
|
11
|
+
|
|
12
|
+
/** Wire form: snake_case, exactly as the server stores it. */
|
|
13
|
+
export interface TransactionRequestEnvelope {
|
|
14
|
+
format_version: number;
|
|
15
|
+
protocol_line: string;
|
|
16
|
+
/** `0x`-prefixed lowercase SHA-256 of the raw bytes. */
|
|
17
|
+
checksum: string;
|
|
18
|
+
/** Base64 of the raw bytes. */
|
|
19
|
+
bytes: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toBase64(bytes: Uint8Array): string {
|
|
23
|
+
let binary = '';
|
|
24
|
+
for (const byte of bytes) {
|
|
25
|
+
binary += String.fromCharCode(byte);
|
|
26
|
+
}
|
|
27
|
+
return btoa(binary);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
31
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', new Uint8Array(bytes)));
|
|
32
|
+
return `0x${Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Wraps freshly serialized request bytes for storage with a proposal. */
|
|
36
|
+
export async function sealTransactionRequest(bytes: Uint8Array): Promise<TransactionRequestEnvelope> {
|
|
37
|
+
return {
|
|
38
|
+
format_version: ENVELOPE_FORMAT_VERSION,
|
|
39
|
+
protocol_line: PROTOCOL_LINE,
|
|
40
|
+
checksum: await sha256Hex(bytes),
|
|
41
|
+
bytes: toBase64(bytes),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function envelopeField<T>(record: Record<string, unknown>, key: keyof TransactionRequestEnvelope, accepts: (value: unknown) => value is T): T {
|
|
46
|
+
const value = record[key];
|
|
47
|
+
if (!accepts(value)) {
|
|
48
|
+
throw new Error(`Guardian returned a transaction request envelope with an invalid ${key}: ${JSON.stringify(value)}`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isString = (value: unknown): value is string => typeof value === 'string';
|
|
54
|
+
const isFormatVersion = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0;
|
|
55
|
+
|
|
56
|
+
/** Decodes a stored envelope as the server returns it, refusing a missing or mistyped field. */
|
|
57
|
+
export function decodeTransactionRequestEnvelope(value: unknown): TransactionRequestEnvelope {
|
|
58
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
59
|
+
throw new Error(`Guardian returned a transaction request envelope that is not an object: ${JSON.stringify(value)}`);
|
|
60
|
+
}
|
|
61
|
+
const record = value as Record<string, unknown>;
|
|
62
|
+
return {
|
|
63
|
+
format_version: envelopeField(record, 'format_version', isFormatVersion),
|
|
64
|
+
protocol_line: envelopeField(record, 'protocol_line', isString),
|
|
65
|
+
checksum: envelopeField(record, 'checksum', isString),
|
|
66
|
+
bytes: envelopeField(record, 'bytes', isString),
|
|
67
|
+
};
|
|
68
|
+
}
|