@openzeppelin/guardian-client 0.18.2 → 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.
Files changed (49) hide show
  1. package/README.md +66 -5
  2. package/dist/auth-request.d.ts +13 -0
  3. package/dist/auth-request.d.ts.map +1 -1
  4. package/dist/auth-request.js +26 -5
  5. package/dist/auth-request.js.map +1 -1
  6. package/dist/conversion.d.ts.map +1 -1
  7. package/dist/conversion.js +12 -2
  8. package/dist/conversion.js.map +1 -1
  9. package/dist/error-codes.d.ts +3 -5
  10. package/dist/error-codes.d.ts.map +1 -1
  11. package/dist/error-codes.js +21 -4
  12. package/dist/error-codes.js.map +1 -1
  13. package/dist/execution.d.ts +99 -0
  14. package/dist/execution.d.ts.map +1 -0
  15. package/dist/execution.js +124 -0
  16. package/dist/execution.js.map +1 -0
  17. package/dist/http.d.ts +57 -4
  18. package/dist/http.d.ts.map +1 -1
  19. package/dist/http.js +111 -13
  20. package/dist/http.js.map +1 -1
  21. package/dist/index.d.ts +7 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/request-envelope.d.ts +22 -0
  26. package/dist/request-envelope.d.ts.map +1 -0
  27. package/dist/request-envelope.js +51 -0
  28. package/dist/request-envelope.js.map +1 -0
  29. package/dist/server-types.d.ts +20 -0
  30. package/dist/server-types.d.ts.map +1 -1
  31. package/dist/types.d.ts +48 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/auth-request.test.ts +12 -0
  35. package/src/auth-request.ts +37 -5
  36. package/src/conversion.test.ts +81 -0
  37. package/src/conversion.ts +17 -2
  38. package/src/dependencies.test.ts +25 -0
  39. package/src/error-codes.test.ts +14 -1
  40. package/src/error-codes.ts +21 -4
  41. package/src/execution.test.ts +145 -0
  42. package/src/execution.ts +206 -0
  43. package/src/http.test.ts +430 -14
  44. package/src/http.ts +153 -13
  45. package/src/index.ts +34 -1
  46. package/src/request-envelope.test.ts +30 -0
  47. package/src/request-envelope.ts +68 -0
  48. package/src/server-types.ts +20 -1
  49. 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 the wallet.
305
- * Every other route, including `configure`, `pushDelta`,
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(accountId: string, nonce: number): Promise<AbandonCandidateResponse> {
549
- const serverRequest: ServerAbandonCandidateRequest = { account_id: accountId, nonce };
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
- // Guardian's ACK commits the account to this transition: always the wallet.
618
- const response = await this.fetchAuthenticated('/delta', {
619
- method: 'POST',
620
- body: JSON.stringify(serverDelta),
621
- }, delta.accountId, serverDelta);
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.fetchAuthenticated(path, init, accountId, requestPayload, mode, retries - 1);
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
+ }
@@ -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