@openzeppelin/guardian-client 0.18.3 → 0.19.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 +66 -5
- 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.map +1 -1
- package/dist/conversion.js +12 -2
- 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 +21 -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 +57 -4
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +111 -13
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 +20 -0
- package/dist/server-types.d.ts.map +1 -1
- package/dist/types.d.ts +48 -1
- 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 +81 -0
- package/src/conversion.ts +17 -2
- package/src/dependencies.test.ts +25 -0
- package/src/error-codes.test.ts +14 -1
- package/src/error-codes.ts +21 -4
- package/src/execution.test.ts +145 -0
- package/src/execution.ts +206 -0
- package/src/http.test.ts +430 -14
- package/src/http.ts +153 -13
- package/src/index.ts +34 -1
- package/src/request-envelope.test.ts +30 -0
- package/src/request-envelope.ts +68 -0
- package/src/server-types.ts +20 -1
- package/src/types.ts +50 -1
package/src/http.ts
CHANGED
|
@@ -28,6 +28,12 @@ import type {
|
|
|
28
28
|
CanonicalNonce,
|
|
29
29
|
} from './types.js';
|
|
30
30
|
import { RequestAuthPayload } from './auth-request.js';
|
|
31
|
+
import { fromServerExecution, fromServerExecutionCapability } from './execution.js';
|
|
32
|
+
import type {
|
|
33
|
+
ProposalExecution,
|
|
34
|
+
ServerCurrentExecution,
|
|
35
|
+
ServerProposalExecution,
|
|
36
|
+
} from './execution.js';
|
|
31
37
|
import type {
|
|
32
38
|
ServerAbandonCandidateRequest,
|
|
33
39
|
ServerAbandonCandidateResponse,
|
|
@@ -84,6 +90,33 @@ export interface GuardianErrorMeta {
|
|
|
84
90
|
* `code === 'signature_scheme_not_allowed'`.
|
|
85
91
|
*/
|
|
86
92
|
allowedSchemes?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* The proposal whose execution holds the account. Present only when
|
|
95
|
+
* `code === 'execution_conflict'`.
|
|
96
|
+
*/
|
|
97
|
+
blockingProposalId?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The proposal type GUARDIAN refuses to execute because the client that finishes its
|
|
100
|
+
* follow-up work must execute it. Present only when `code === 'proposal_executes_locally'`.
|
|
101
|
+
*/
|
|
102
|
+
proposalType?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Why GUARDIAN leaves the proposal to local execution. Present only when
|
|
105
|
+
* `code === 'proposal_executes_locally'`: `switch_guardian` for a guardian switch.
|
|
106
|
+
*/
|
|
107
|
+
reason?: LocalExecutionReason;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Why GUARDIAN refuses to execute a proposal and leaves it to local execution. */
|
|
111
|
+
export type LocalExecutionReason = 'switch_guardian';
|
|
112
|
+
|
|
113
|
+
function localExecutionReason(value: unknown): LocalExecutionReason | undefined {
|
|
114
|
+
switch (value) {
|
|
115
|
+
case 'switch_guardian':
|
|
116
|
+
return value;
|
|
117
|
+
default:
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
interface ParsedGuardianError {
|
|
@@ -142,6 +175,12 @@ function parseGuardianErrorBody(body: string): ParsedGuardianError | undefined {
|
|
|
142
175
|
) {
|
|
143
176
|
meta.allowedSchemes = rawMeta.allowed_schemes;
|
|
144
177
|
}
|
|
178
|
+
if (typeof rawMeta.blocking_proposal_id === 'string') {
|
|
179
|
+
meta.blockingProposalId = rawMeta.blocking_proposal_id;
|
|
180
|
+
}
|
|
181
|
+
if (typeof rawMeta.proposal_type === 'string') meta.proposalType = rawMeta.proposal_type;
|
|
182
|
+
const reason = localExecutionReason(rawMeta.reason);
|
|
183
|
+
if (reason !== undefined) meta.reason = reason;
|
|
145
184
|
if (typeof rawMeta.paused_reason === 'string' || rawMeta.paused_reason === null) {
|
|
146
185
|
meta.pausedReason = rawMeta.paused_reason as string | null;
|
|
147
186
|
}
|
|
@@ -301,8 +340,8 @@ export class GuardianHttpClient {
|
|
|
301
340
|
|
|
302
341
|
/**
|
|
303
342
|
* 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`,
|
|
343
|
+
* create/sign, `pushDelta`) with a registered delegated signer instead of
|
|
344
|
+
* the wallet. Every other route, including `configure`,
|
|
306
345
|
* `abandonCandidate`, account lookup and `revokeAllSessions`, uses the
|
|
307
346
|
* wallet signer. Pass `null` to stop using the session.
|
|
308
347
|
*/
|
|
@@ -434,6 +473,9 @@ export class GuardianHttpClient {
|
|
|
434
473
|
environment: data.environment,
|
|
435
474
|
startedAt: data.started_at,
|
|
436
475
|
uptimeSeconds: data.uptime_seconds,
|
|
476
|
+
...(data.execution === undefined || data.execution === null
|
|
477
|
+
? {}
|
|
478
|
+
: { execution: fromServerExecutionCapability(data.execution) }),
|
|
437
479
|
...(data.sessions
|
|
438
480
|
? {
|
|
439
481
|
sessions: {
|
|
@@ -532,6 +574,48 @@ export class GuardianHttpClient {
|
|
|
532
574
|
};
|
|
533
575
|
}
|
|
534
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Ask Guardian to prove and submit a threshold-met proposal. Resolves once the request is
|
|
579
|
+
* accepted; poll {@link getDeltaProposalExecution} for the outcome. Repeating the request while
|
|
580
|
+
* the execution runs returns it with `newlyAccepted: false`.
|
|
581
|
+
*
|
|
582
|
+
* Signed by the session while one is set, over the payload tagged as `ExecuteDeltaProposal`:
|
|
583
|
+
* the body names the same fields as a status read, and the tag keeps a signed read from
|
|
584
|
+
* authenticating an execution request.
|
|
585
|
+
*/
|
|
586
|
+
async executeDeltaProposal(accountId: string, proposalId: string): Promise<ProposalExecution> {
|
|
587
|
+
const serverRequest = {
|
|
588
|
+
account_id: accountId,
|
|
589
|
+
proposal_id: proposalId,
|
|
590
|
+
};
|
|
591
|
+
const response = await this.fetchSigned('/delta/proposal/execution', {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
body: JSON.stringify(serverRequest),
|
|
594
|
+
}, accountId, RequestAuthPayload.forOperation('ExecuteDeltaProposal', serverRequest), 'session');
|
|
595
|
+
return fromServerExecution((await response.json()) as ServerProposalExecution);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** The latest execution attempt of a proposal. */
|
|
599
|
+
async getDeltaProposalExecution(accountId: string, proposalId: string): Promise<ProposalExecution> {
|
|
600
|
+
const requestQuery = { account_id: accountId, proposal_id: proposalId };
|
|
601
|
+
const params = new URLSearchParams(requestQuery);
|
|
602
|
+
const response = await this.fetchAuthenticated(`/delta/proposal/execution?${params}`, {
|
|
603
|
+
method: 'GET',
|
|
604
|
+
}, accountId, requestQuery, 'session');
|
|
605
|
+
return fromServerExecution((await response.json()) as ServerProposalExecution);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** The account's in-flight execution, or `null`. A finished execution is not in flight. */
|
|
609
|
+
async getCurrentExecution(accountId: string): Promise<ProposalExecution | null> {
|
|
610
|
+
const requestQuery = { account_id: accountId };
|
|
611
|
+
const params = new URLSearchParams(requestQuery);
|
|
612
|
+
const response = await this.fetchAuthenticated(`/delta/execution/current?${params}`, {
|
|
613
|
+
method: 'GET',
|
|
614
|
+
}, accountId, requestQuery, 'session');
|
|
615
|
+
const server = (await response.json()) as ServerCurrentExecution;
|
|
616
|
+
return server.execution === null ? null : fromServerExecution(server.execution);
|
|
617
|
+
}
|
|
618
|
+
|
|
535
619
|
/**
|
|
536
620
|
* Request abandonment of a pending canonicalization candidate whose
|
|
537
621
|
* transaction will never land on-chain (issue #319).
|
|
@@ -544,9 +628,22 @@ export class GuardianHttpClient {
|
|
|
544
628
|
* for the resolution. Refused with `GUARDIAN_CANDIDATE_LANDED` (409)
|
|
545
629
|
* when the transaction actually landed. Retries are idempotent and
|
|
546
630
|
* preserve the original request timestamp.
|
|
631
|
+
*
|
|
632
|
+
* `proposalId` names the transaction: its summary's commitment, which
|
|
633
|
+
* is the proposal id of a multisig proposal. A Guardian execution can
|
|
634
|
+
* archive the delta and take its nonce; the abandon then still answers
|
|
635
|
+
* for this transaction.
|
|
547
636
|
*/
|
|
548
|
-
async abandonCandidate(
|
|
549
|
-
|
|
637
|
+
async abandonCandidate(
|
|
638
|
+
accountId: string,
|
|
639
|
+
nonce: number,
|
|
640
|
+
proposalId: string,
|
|
641
|
+
): Promise<AbandonCandidateResponse> {
|
|
642
|
+
const serverRequest: ServerAbandonCandidateRequest = {
|
|
643
|
+
account_id: accountId,
|
|
644
|
+
nonce,
|
|
645
|
+
proposal_id: proposalId,
|
|
646
|
+
};
|
|
550
647
|
// Abandoning changes the account's canonical chain: always the wallet.
|
|
551
648
|
const response = await this.fetchAuthenticated('/delta/candidate/abandon', {
|
|
552
649
|
method: 'POST',
|
|
@@ -558,6 +655,7 @@ export class GuardianHttpClient {
|
|
|
558
655
|
nonce: server.nonce,
|
|
559
656
|
state: server.state,
|
|
560
657
|
abandonRequestedAt: server.abandon_requested_at,
|
|
658
|
+
commitment: server.commitment,
|
|
561
659
|
};
|
|
562
660
|
}
|
|
563
661
|
|
|
@@ -572,8 +670,14 @@ export class GuardianHttpClient {
|
|
|
572
670
|
* delta until its retention TTL expires — sync and check chain before
|
|
573
671
|
* replacing it), `'unexpected'` for any state no abandon flow
|
|
574
672
|
* produces (including a missing delta).
|
|
673
|
+
*
|
|
674
|
+
* `commitment` is the one {@link abandonCandidate} returned. Another
|
|
675
|
+
* delta can take the nonce (a Guardian execution archives the abandoned
|
|
676
|
+
* one): once it is canonical the abandoned transaction can no longer
|
|
677
|
+
* land (`'abandoned'`), and while it is a candidate the nonce is still
|
|
678
|
+
* contested (`'waiting'`).
|
|
575
679
|
*/
|
|
576
|
-
async abandonStatus(accountId: string, nonce: number): Promise<AbandonStatus> {
|
|
680
|
+
async abandonStatus(accountId: string, nonce: number, commitment: string): Promise<AbandonStatus> {
|
|
577
681
|
let delta: DeltaObject;
|
|
578
682
|
try {
|
|
579
683
|
delta = await this.getDelta(accountId, nonce);
|
|
@@ -583,6 +687,16 @@ export class GuardianHttpClient {
|
|
|
583
687
|
}
|
|
584
688
|
throw e;
|
|
585
689
|
}
|
|
690
|
+
if (delta.newCommitment?.toLowerCase() !== commitment.toLowerCase()) {
|
|
691
|
+
switch (delta.status.status) {
|
|
692
|
+
case 'canonical':
|
|
693
|
+
return 'abandoned';
|
|
694
|
+
case 'candidate':
|
|
695
|
+
return 'waiting';
|
|
696
|
+
default:
|
|
697
|
+
return 'unexpected';
|
|
698
|
+
}
|
|
699
|
+
}
|
|
586
700
|
switch (delta.status.status) {
|
|
587
701
|
case 'candidate':
|
|
588
702
|
return 'waiting';
|
|
@@ -612,13 +726,30 @@ export class GuardianHttpClient {
|
|
|
612
726
|
return fromServerDeltaObject(server);
|
|
613
727
|
}
|
|
614
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Push a delta for Guardian's acknowledgement. Signed by the session while
|
|
731
|
+
* one is set: Guardian acknowledges a multisig delta only once its
|
|
732
|
+
* proposal carries enough verified cosigner approvals, which the wallet
|
|
733
|
+
* signs. A Guardian that takes this route only from the wallet (an older
|
|
734
|
+
* release, or an account without a multisig state) answers
|
|
735
|
+
* `wallet_signature_required`, and the wallet signs the push again; the
|
|
736
|
+
* session stays in use for every other route.
|
|
737
|
+
*/
|
|
615
738
|
async pushDelta(delta: ExecutionDelta): Promise<PushDeltaResponse> {
|
|
616
739
|
const serverDelta = toServerExecutionDelta(delta);
|
|
617
|
-
|
|
618
|
-
const response = await this.fetchAuthenticated(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
740
|
+
const init = { method: 'POST', body: JSON.stringify(serverDelta) };
|
|
741
|
+
const response = await this.fetchAuthenticated(
|
|
742
|
+
'/delta',
|
|
743
|
+
init,
|
|
744
|
+
delta.accountId,
|
|
745
|
+
serverDelta,
|
|
746
|
+
'session'
|
|
747
|
+
).catch((err: unknown) => {
|
|
748
|
+
if (err instanceof GuardianHttpError && err.code === 'wallet_signature_required') {
|
|
749
|
+
return this.fetchAuthenticated('/delta', init, delta.accountId, serverDelta, 'wallet');
|
|
750
|
+
}
|
|
751
|
+
throw err;
|
|
752
|
+
});
|
|
622
753
|
const server = (await response.json()) as ServerPushDeltaResponse;
|
|
623
754
|
return {
|
|
624
755
|
accountId: server.account_id,
|
|
@@ -753,11 +884,20 @@ export class GuardianHttpClient {
|
|
|
753
884
|
init: RequestInit,
|
|
754
885
|
accountId: string,
|
|
755
886
|
requestPayload: unknown,
|
|
756
|
-
mode: AuthMode = 'wallet'
|
|
887
|
+
mode: AuthMode = 'wallet'
|
|
888
|
+
): Promise<Response> {
|
|
889
|
+
return this.fetchSigned(path, init, accountId, RequestAuthPayload.fromRequest(requestPayload), mode);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private async fetchSigned(
|
|
893
|
+
path: string,
|
|
894
|
+
init: RequestInit,
|
|
895
|
+
accountId: string,
|
|
896
|
+
authPayload: RequestAuthPayload,
|
|
897
|
+
mode: AuthMode,
|
|
757
898
|
retries = 2
|
|
758
899
|
): Promise<Response> {
|
|
759
900
|
const timestamp = this.nextTimestamp();
|
|
760
|
-
const authPayload = RequestAuthPayload.fromRequest(requestPayload);
|
|
761
901
|
const session = mode === 'session' ? this.getSession() : null;
|
|
762
902
|
const headers = session
|
|
763
903
|
? authHeaders(
|
|
@@ -799,7 +939,7 @@ export class GuardianHttpClient {
|
|
|
799
939
|
err.code === 'authentication_replay'
|
|
800
940
|
) {
|
|
801
941
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
802
|
-
return this.
|
|
942
|
+
return this.fetchSigned(path, init, accountId, authPayload, mode, retries - 1);
|
|
803
943
|
}
|
|
804
944
|
throw err;
|
|
805
945
|
}
|
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,38 @@ 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 type { SignedOperation } from './auth-request.js';
|
|
11
|
+
export {
|
|
12
|
+
ENVELOPE_FORMAT_VERSION,
|
|
13
|
+
PROTOCOL_LINE,
|
|
14
|
+
decodeTransactionRequestEnvelope,
|
|
15
|
+
sealTransactionRequest,
|
|
16
|
+
} from './request-envelope.js';
|
|
17
|
+
export type { TransactionRequestEnvelope } from './request-envelope.js';
|
|
18
|
+
export {
|
|
19
|
+
EXECUTION_FAILURE_CODES,
|
|
20
|
+
EXECUTION_STATES,
|
|
21
|
+
EXECUTION_UNAVAILABLE_REASONS,
|
|
22
|
+
EXPIRATION_BOUNDS,
|
|
23
|
+
FOREIGN_ACCOUNT_UNAVAILABLE_REASONS,
|
|
24
|
+
PLAIN_EXECUTION_FAILURE_CODES,
|
|
25
|
+
REQUEST_INVALID_REASONS,
|
|
26
|
+
fromServerExecution,
|
|
27
|
+
fromServerExecutionCapability,
|
|
28
|
+
isTerminalExecutionState,
|
|
29
|
+
} from './execution.js';
|
|
30
|
+
export type {
|
|
31
|
+
ExecutionFailure,
|
|
32
|
+
ExecutionFailureCode,
|
|
33
|
+
ExecutionState,
|
|
34
|
+
ExecutionUnavailableReason,
|
|
35
|
+
ExpirationBound,
|
|
36
|
+
ForeignAccountUnavailableReason,
|
|
37
|
+
PlainExecutionFailureCode,
|
|
38
|
+
ProposalExecution,
|
|
39
|
+
RequestInvalidReason,
|
|
40
|
+
ServerExecutionCapability,
|
|
41
|
+
} from './execution.js';
|
|
10
42
|
|
|
11
43
|
export type {
|
|
12
44
|
AbandonCandidateResponse,
|
|
@@ -18,6 +50,7 @@ export type {
|
|
|
18
50
|
SignatureScheme,
|
|
19
51
|
CosignerSignature,
|
|
20
52
|
AuthConfig,
|
|
53
|
+
MidenCosignerConfig,
|
|
21
54
|
CanonicalNonce,
|
|
22
55
|
DeltaStatus,
|
|
23
56
|
DeltaObject,
|
|
@@ -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 { sealTransactionRequest } from './request-envelope.js';
|
|
6
|
+
|
|
7
|
+
describe('sealTransactionRequest', () => {
|
|
8
|
+
it('produces the envelope the Rust SDK and the server produce for the same bytes', 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 envelope the Rust SDK and the server produce', 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);
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const ENVELOPE_FORMAT_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
/** The Miden protocol line requests are serialized for, as `MAJOR.MINOR`. */
|
|
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
|
+
}
|
package/src/server-types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { TransactionRequestEnvelope } from './request-envelope.js';
|
|
2
|
+
import type { ServerExecutionCapabilityWire } from './execution.js';
|
|
3
|
+
|
|
1
4
|
export interface ServerFalconSignature {
|
|
2
5
|
scheme: 'falcon';
|
|
3
6
|
signature: string;
|
|
@@ -43,6 +46,11 @@ export interface ServerProposalMetadata {
|
|
|
43
46
|
target_threshold?: number;
|
|
44
47
|
required_signatures?: number;
|
|
45
48
|
signer_commitments?: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Target signer schemes (`"falcon"` / `"ecdsa"`), index-aligned with
|
|
51
|
+
* `signer_commitments` (issue #563). Proposer-supplied and stored as sent.
|
|
52
|
+
*/
|
|
53
|
+
signer_schemes?: string[];
|
|
46
54
|
target_procedure?: string;
|
|
47
55
|
salt?: string;
|
|
48
56
|
description?: string;
|
|
@@ -78,6 +86,7 @@ export interface ServerDeltaObject {
|
|
|
78
86
|
data?: string;
|
|
79
87
|
signatures?: Array<{ signer_id: string; signature: ServerProposalSignature }>;
|
|
80
88
|
metadata?: ServerProposalMetadata;
|
|
89
|
+
transaction_request?: TransactionRequestEnvelope;
|
|
81
90
|
};
|
|
82
91
|
ack_sig?: string;
|
|
83
92
|
ack_pubkey?: string;
|
|
@@ -121,7 +130,13 @@ export interface ServerCanonicalNonceResponse {
|
|
|
121
130
|
|
|
122
131
|
export type ServerAuthConfig =
|
|
123
132
|
| { MidenFalconRpo: { cosigner_commitments: string[] } }
|
|
124
|
-
| { MidenEcdsa: { cosigner_commitments: string[] } }
|
|
133
|
+
| { MidenEcdsa: { cosigner_commitments: string[] } }
|
|
134
|
+
| {
|
|
135
|
+
Miden: {
|
|
136
|
+
cosigners: { commitment: string; scheme: 'falcon' | 'ecdsa' }[];
|
|
137
|
+
guardian_scheme: 'falcon' | 'ecdsa';
|
|
138
|
+
};
|
|
139
|
+
};
|
|
125
140
|
|
|
126
141
|
export interface ServerConfigureRequest {
|
|
127
142
|
account_id: string;
|
|
@@ -143,6 +158,7 @@ export interface ServerDeltaProposalRequest {
|
|
|
143
158
|
tx_summary: { data: string };
|
|
144
159
|
signatures: Array<{ signer_id: string; signature: ServerProposalSignature }>;
|
|
145
160
|
metadata?: ServerProposalMetadata;
|
|
161
|
+
transaction_request?: TransactionRequestEnvelope;
|
|
146
162
|
};
|
|
147
163
|
}
|
|
148
164
|
|
|
@@ -158,6 +174,7 @@ export interface ServerProposalsResponse {
|
|
|
158
174
|
export interface ServerAbandonCandidateRequest {
|
|
159
175
|
account_id: string;
|
|
160
176
|
nonce: number;
|
|
177
|
+
proposal_id: string;
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
export interface ServerAbandonCandidateResponse {
|
|
@@ -165,6 +182,7 @@ export interface ServerAbandonCandidateResponse {
|
|
|
165
182
|
nonce: number;
|
|
166
183
|
state: 'pending' | 'abandoned' | 'retained';
|
|
167
184
|
abandon_requested_at?: string;
|
|
185
|
+
commitment: string;
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
export interface ServerSignProposalRequest {
|
|
@@ -185,6 +203,7 @@ export interface ServerStatusResponse {
|
|
|
185
203
|
environment: string;
|
|
186
204
|
started_at: string;
|
|
187
205
|
uptime_seconds: number;
|
|
206
|
+
execution?: ServerExecutionCapabilityWire | null;
|
|
188
207
|
sessions?: {
|
|
189
208
|
max_ttl_seconds: number;
|
|
190
209
|
};
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { TransactionRequestEnvelope } from './request-envelope.js';
|
|
2
|
+
|
|
1
3
|
import type { RequestAuthPayload } from './auth-request.js';
|
|
4
|
+
import type { ServerExecutionCapability } from './execution.js';
|
|
2
5
|
|
|
3
6
|
export interface Signer {
|
|
4
7
|
readonly commitment: string;
|
|
@@ -153,6 +156,12 @@ export type ProposalSignature = FalconSignature | EcdsaSignature;
|
|
|
153
156
|
|
|
154
157
|
export type SignatureScheme = 'falcon' | 'ecdsa';
|
|
155
158
|
|
|
159
|
+
/** One cosigner of a {@link AuthConfig} `Miden` auth and the scheme Guardian verifies its signatures under. */
|
|
160
|
+
export interface MidenCosignerConfig {
|
|
161
|
+
commitment: string;
|
|
162
|
+
scheme: SignatureScheme;
|
|
163
|
+
}
|
|
164
|
+
|
|
156
165
|
export type AuthConfig =
|
|
157
166
|
| {
|
|
158
167
|
MidenFalconRpo: {
|
|
@@ -163,6 +172,18 @@ export type AuthConfig =
|
|
|
163
172
|
MidenEcdsa: {
|
|
164
173
|
cosigner_commitments: string[];
|
|
165
174
|
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Cosigners that each carry their own scheme, and the GUARDIAN's scheme
|
|
178
|
+
* (issue #563). Send it only for an account whose cosigners and GUARDIAN
|
|
179
|
+
* do not all share one scheme; servers before #563 reject it, and the
|
|
180
|
+
* single-scheme variants keep serving the accounts that do.
|
|
181
|
+
*/
|
|
182
|
+
| {
|
|
183
|
+
Miden: {
|
|
184
|
+
cosigners: MidenCosignerConfig[];
|
|
185
|
+
guardian_scheme: SignatureScheme;
|
|
186
|
+
};
|
|
166
187
|
};
|
|
167
188
|
|
|
168
189
|
export interface CosignerSignature {
|
|
@@ -206,6 +227,15 @@ export interface ProposalMetadata {
|
|
|
206
227
|
targetThreshold?: number;
|
|
207
228
|
requiredSignatures?: number;
|
|
208
229
|
signerCommitments?: string[];
|
|
230
|
+
/**
|
|
231
|
+
* Target signer schemes (`"falcon"` / `"ecdsa"`), index-aligned with
|
|
232
|
+
* `signerCommitments`, for an update-signers proposal whose signers do not
|
|
233
|
+
* all share one scheme (issue #563). Absent => every target signer keeps the
|
|
234
|
+
* proposer's scheme. GUARDIAN stores the proposer's values as sent, so a
|
|
235
|
+
* consumer validates them, as it validates the rest of the metadata against
|
|
236
|
+
* the transaction summary, before relying on them.
|
|
237
|
+
*/
|
|
238
|
+
signerSchemes?: string[];
|
|
209
239
|
targetProcedure?: string;
|
|
210
240
|
salt?: string;
|
|
211
241
|
description?: string;
|
|
@@ -245,6 +275,8 @@ export interface DeltaObject {
|
|
|
245
275
|
txSummary: { data: string };
|
|
246
276
|
signatures: Array<{ signerId: string; signature: ProposalSignature }>;
|
|
247
277
|
metadata?: ProposalMetadata;
|
|
278
|
+
/** The request a Guardian-executable proposal stores. Absent on a self-executed one. */
|
|
279
|
+
transactionRequest?: TransactionRequestEnvelope;
|
|
248
280
|
};
|
|
249
281
|
ackSig?: string;
|
|
250
282
|
ackPubkey?: string;
|
|
@@ -268,6 +300,7 @@ export interface StateObject {
|
|
|
268
300
|
stateJson: { data: string };
|
|
269
301
|
createdAt: string;
|
|
270
302
|
updatedAt: string;
|
|
303
|
+
/** `falcon` or `ecdsa` when the account's cosigners and GUARDIAN share that scheme, `mixed` when they do not. */
|
|
271
304
|
authScheme?: string;
|
|
272
305
|
}
|
|
273
306
|
|
|
@@ -310,6 +343,11 @@ export interface StatusResponse {
|
|
|
310
343
|
environment: string;
|
|
311
344
|
startedAt: string;
|
|
312
345
|
uptimeSeconds: number;
|
|
346
|
+
/**
|
|
347
|
+
* Whether the server accepts Guardian execution requests. Absent on a Guardian that predates
|
|
348
|
+
* Guardian execution: it does not offer it.
|
|
349
|
+
*/
|
|
350
|
+
execution?: ServerExecutionCapability;
|
|
313
351
|
/** Present on every Guardian that accepts session grants; absent on older servers. */
|
|
314
352
|
sessions?: SessionsStatus;
|
|
315
353
|
}
|
|
@@ -321,6 +359,8 @@ export interface DeltaProposalRequest {
|
|
|
321
359
|
txSummary: { data: string };
|
|
322
360
|
signatures: Array<{ signerId: string; signature: ProposalSignature }>;
|
|
323
361
|
metadata?: ProposalMetadata;
|
|
362
|
+
/** The request a Guardian-executable proposal stores. Omit for a self-executed one. */
|
|
363
|
+
transactionRequest?: TransactionRequestEnvelope;
|
|
324
364
|
};
|
|
325
365
|
}
|
|
326
366
|
|
|
@@ -357,13 +397,22 @@ export interface AbandonCandidateResponse {
|
|
|
357
397
|
* return the original timestamp; absent once resolved.
|
|
358
398
|
*/
|
|
359
399
|
abandonRequestedAt?: string;
|
|
400
|
+
/**
|
|
401
|
+
* The abandoned delta's `new_commitment`. Another delta can later take
|
|
402
|
+
* the same nonce (a Guardian execution archives the abandoned one), so
|
|
403
|
+
* pass this to `abandonStatus` to poll for this delta.
|
|
404
|
+
*/
|
|
405
|
+
commitment: string;
|
|
360
406
|
}
|
|
361
407
|
|
|
362
408
|
/**
|
|
363
409
|
* Resolution of an abandon request, as observed via the delta feed.
|
|
364
410
|
* `'retained'` means the account slot is released but the on-chain
|
|
365
411
|
* outcome is still uncertain — "unlocked but unresolved", never to be
|
|
366
|
-
* read as "the transaction did not land".
|
|
412
|
+
* read as "the transaction did not land". When another delta has taken
|
|
413
|
+
* the nonce, `'abandoned'` means it is canonical (the abandoned
|
|
414
|
+
* transaction can no longer land) and `'waiting'` that it is still a
|
|
415
|
+
* candidate.
|
|
367
416
|
*/
|
|
368
417
|
export type AbandonStatus = 'waiting' | 'landed' | 'abandoned' | 'retained' | 'unexpected';
|
|
369
418
|
|