@openzeppelin/miden-multisig-client 0.17.0 → 0.18.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.
Files changed (151) hide show
  1. package/README.md +171 -46
  2. package/dist/account/builder.d.ts +4 -4
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +17 -7
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/layout.d.ts +5 -5
  7. package/dist/account/layout.d.ts.map +1 -1
  8. package/dist/account/layout.js +5 -5
  9. package/dist/account/layout.js.map +1 -1
  10. package/dist/client.d.ts +18 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +79 -6
  13. package/dist/client.js.map +1 -1
  14. package/dist/index.d.ts +6 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -5
  17. package/dist/index.js.map +1 -1
  18. package/dist/multisig/authArgErrors.d.ts +28 -31
  19. package/dist/multisig/authArgErrors.d.ts.map +1 -1
  20. package/dist/multisig/authArgErrors.js +42 -46
  21. package/dist/multisig/authArgErrors.js.map +1 -1
  22. package/dist/multisig/consumeNotesErrors.d.ts +13 -1
  23. package/dist/multisig/consumeNotesErrors.d.ts.map +1 -1
  24. package/dist/multisig/consumeNotesErrors.js +17 -0
  25. package/dist/multisig/consumeNotesErrors.js.map +1 -1
  26. package/dist/multisig/signing.d.ts +1 -1
  27. package/dist/multisig/signing.d.ts.map +1 -1
  28. package/dist/multisig/signing.js +8 -3
  29. package/dist/multisig/signing.js.map +1 -1
  30. package/dist/multisig.d.ts +145 -18
  31. package/dist/multisig.d.ts.map +1 -1
  32. package/dist/multisig.js +493 -162
  33. package/dist/multisig.js.map +1 -1
  34. package/dist/procedures.d.ts +7 -7
  35. package/dist/procedures.js +7 -7
  36. package/dist/proposal/factory.d.ts.map +1 -1
  37. package/dist/proposal/factory.js +7 -0
  38. package/dist/proposal/factory.js.map +1 -1
  39. package/dist/prover/workflow.d.ts +7 -3
  40. package/dist/prover/workflow.d.ts.map +1 -1
  41. package/dist/prover/workflow.js +7 -5
  42. package/dist/prover/workflow.js.map +1 -1
  43. package/dist/raw-client.d.ts +1 -0
  44. package/dist/raw-client.d.ts.map +1 -1
  45. package/dist/raw-client.js +10 -2
  46. package/dist/raw-client.js.map +1 -1
  47. package/dist/recovery/publicNoteBackfill.js +1 -1
  48. package/dist/recovery/publicNoteBackfill.js.map +1 -1
  49. package/dist/retry/classify.d.ts +3 -0
  50. package/dist/retry/classify.d.ts.map +1 -1
  51. package/dist/retry/classify.js +2 -2
  52. package/dist/retry/classify.js.map +1 -1
  53. package/dist/signer.d.ts +1 -0
  54. package/dist/signer.d.ts.map +1 -1
  55. package/dist/signer.js +1 -0
  56. package/dist/signer.js.map +1 -1
  57. package/dist/signers/index.d.ts +1 -0
  58. package/dist/signers/index.d.ts.map +1 -1
  59. package/dist/signers/index.js +1 -0
  60. package/dist/signers/index.js.map +1 -1
  61. package/dist/signers/ledger.d.ts +25 -0
  62. package/dist/signers/ledger.d.ts.map +1 -0
  63. package/dist/signers/ledger.js +96 -0
  64. package/dist/signers/ledger.js.map +1 -0
  65. package/dist/state/adopt.d.ts +45 -0
  66. package/dist/state/adopt.d.ts.map +1 -0
  67. package/dist/state/adopt.js +101 -0
  68. package/dist/state/adopt.js.map +1 -0
  69. package/dist/transaction/authArgs.d.ts +57 -0
  70. package/dist/transaction/authArgs.d.ts.map +1 -0
  71. package/dist/transaction/authArgs.js +108 -0
  72. package/dist/transaction/authArgs.js.map +1 -0
  73. package/dist/transaction/consumeNotes.d.ts +9 -5
  74. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  75. package/dist/transaction/consumeNotes.js +8 -23
  76. package/dist/transaction/consumeNotes.js.map +1 -1
  77. package/dist/transaction/noteAuthentication.d.ts +39 -0
  78. package/dist/transaction/noteAuthentication.d.ts.map +1 -0
  79. package/dist/transaction/noteAuthentication.js +94 -0
  80. package/dist/transaction/noteAuthentication.js.map +1 -0
  81. package/dist/transaction/options.d.ts +25 -0
  82. package/dist/transaction/options.d.ts.map +1 -1
  83. package/dist/transaction/p2id.d.ts +3 -2
  84. package/dist/transaction/p2id.d.ts.map +1 -1
  85. package/dist/transaction/p2id.js +36 -27
  86. package/dist/transaction/p2id.js.map +1 -1
  87. package/dist/transaction/summary.d.ts +126 -22
  88. package/dist/transaction/summary.d.ts.map +1 -1
  89. package/dist/transaction/summary.js +164 -22
  90. package/dist/transaction/summary.js.map +1 -1
  91. package/dist/transaction/updateGuardian.d.ts +3 -3
  92. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  93. package/dist/transaction/updateGuardian.js +5 -16
  94. package/dist/transaction/updateGuardian.js.map +1 -1
  95. package/dist/transaction/updateProcedureThreshold.d.ts +3 -3
  96. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  97. package/dist/transaction/updateProcedureThreshold.js +6 -16
  98. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  99. package/dist/transaction/updateSigners.d.ts +3 -3
  100. package/dist/transaction/updateSigners.d.ts.map +1 -1
  101. package/dist/transaction/updateSigners.js +9 -16
  102. package/dist/transaction/updateSigners.js.map +1 -1
  103. package/dist/transaction.d.ts +3 -2
  104. package/dist/transaction.d.ts.map +1 -1
  105. package/dist/transaction.js +3 -2
  106. package/dist/transaction.js.map +1 -1
  107. package/dist/types/proposal.d.ts +37 -5
  108. package/dist/types/proposal.d.ts.map +1 -1
  109. package/dist/types/proposal.js +8 -0
  110. package/dist/types/proposal.js.map +1 -1
  111. package/dist/utils/eip712.d.ts +80 -0
  112. package/dist/utils/eip712.d.ts.map +1 -0
  113. package/dist/utils/eip712.js +49 -0
  114. package/dist/utils/eip712.js.map +1 -0
  115. package/dist/utils/signature.d.ts +4 -0
  116. package/dist/utils/signature.d.ts.map +1 -1
  117. package/dist/utils/signature.js +49 -1
  118. package/dist/utils/signature.js.map +1 -1
  119. package/package.json +11 -6
  120. package/src/account/builder.ts +18 -7
  121. package/src/account/layout.ts +5 -5
  122. package/src/client.ts +94 -6
  123. package/src/index.ts +24 -3
  124. package/src/multisig/authArgErrors.ts +47 -53
  125. package/src/multisig/consumeNotesErrors.ts +20 -1
  126. package/src/multisig/signing.ts +8 -2
  127. package/src/multisig.ts +614 -205
  128. package/src/procedures.ts +7 -7
  129. package/src/proposal/factory.ts +7 -0
  130. package/src/prover/workflow.ts +7 -10
  131. package/src/raw-client.ts +11 -7
  132. package/src/recovery/publicNoteBackfill.ts +1 -1
  133. package/src/retry/classify.ts +3 -3
  134. package/src/signer.ts +1 -0
  135. package/src/signers/index.ts +1 -0
  136. package/src/signers/ledger.ts +122 -0
  137. package/src/state/adopt.ts +132 -0
  138. package/src/transaction/authArgs.ts +142 -0
  139. package/src/transaction/consumeNotes.ts +23 -30
  140. package/src/transaction/noteAuthentication.ts +136 -0
  141. package/src/transaction/options.ts +27 -0
  142. package/src/transaction/p2id.ts +45 -34
  143. package/src/transaction/summary.ts +239 -30
  144. package/src/transaction/updateGuardian.ts +8 -22
  145. package/src/transaction/updateProcedureThreshold.ts +8 -20
  146. package/src/transaction/updateSigners.ts +11 -22
  147. package/src/transaction.ts +18 -1
  148. package/src/types/proposal.ts +36 -5
  149. package/src/utils/eip712.ts +57 -0
  150. package/src/utils/signature.ts +57 -0
  151. package/src/prover/test-node.d.ts +0 -6
@@ -5,23 +5,85 @@ import type {
5
5
  WasmWebClient,
6
6
  } from '@miden-sdk/miden-sdk';
7
7
  import { AccountId, ChainAnchor, Word } from '@miden-sdk/miden-sdk';
8
+ import { BoundBlockNotDeclaredError } from '../multisig/authArgErrors.js';
8
9
  import { getRawMidenClient } from '../raw-client.js';
9
- import { base64ToUint8Array, uint8ArrayToBase64 } from '../utils/encoding.js';
10
+ import { base64ToUint8Array, normalizeHexWord, uint8ArrayToBase64 } from '../utils/encoding.js';
11
+ import { requestBoundBlockNum } from './authArgs.js';
10
12
 
11
13
  /**
12
- * Index of the first user param carrying the auth args. The guarded-multisig
13
- * auth component zeroes user params 0-2 and fills 3-6 with the auth args, matching
14
- * `push.0.0.0` ahead of `multisig::auth_tx` in `guarded_multisig.masm`.
14
+ * Layout of the six user params a multisig auth component binds into the
15
+ * transaction summary since protocol 0.17: the approval expiration block (or
16
+ * zero for an approval that never expires), a zero, then the four salt felts.
15
17
  */
16
- const AUTH_ARG_USER_PARAM_OFFSET = 3;
18
+ const APPROVAL_EXPIRATION_USER_PARAM_INDEX = 0;
19
+ const SALT_USER_PARAM_OFFSET = 2;
17
20
 
18
21
  /**
19
- * Captures a `ChainAnchor` for the request at the current sync height and
20
- * executes the transaction against it to obtain the summary awaiting
21
- * authorization. The anchor is returned alongside the summary so the proposer
22
- * can ship it with the signed data; cosigners and the executor then reproduce
23
- * the summary — which binds the reference block commitment since protocol
24
- * 0.16 — with {@link executeForSummaryAt} regardless of their own sync height.
22
+ * The summary binds the block the request's auth args name, and the anchor
23
+ * the store's sync height at capture. A sync landing between the build and the
24
+ * capture leaves them one block apart, and every cosigner's anchor check would
25
+ * then fail on a proposal nothing else is wrong with. Caught here, before the
26
+ * proposal is pushed, so the proposer rebuilds instead.
27
+ */
28
+ export class SummaryAnchorMismatchError extends Error {
29
+ readonly retryable = true;
30
+
31
+ constructor(details: { anchorCommitmentHex: string; summaryBlockCommitmentHex: string }) {
32
+ super(
33
+ `the transaction summary binds block commitment ${details.summaryBlockCommitmentHex} but ` +
34
+ `the captured chain anchor is ${details.anchorCommitmentHex}; a sync landed between ` +
35
+ 'building the request and capturing its anchor, so rebuild the request and retry',
36
+ );
37
+ this.name = 'SummaryAnchorMismatchError';
38
+ }
39
+ }
40
+
41
+ /**
42
+ * The Miden client synced and its node still has not produced the block a
43
+ * proposal binds, so the proposal cannot execute at this client's tip yet.
44
+ * Worth retrying once the node catches up.
45
+ */
46
+ export class ChainBehindBoundBlockError extends Error {
47
+ readonly retryable = true;
48
+ readonly syncHeight: number;
49
+ readonly boundBlockNum: number;
50
+
51
+ constructor(details: { syncHeight: number; boundBlockNum: number }) {
52
+ super(
53
+ `the Miden client synced to block ${details.syncHeight}, below block ` +
54
+ `${details.boundBlockNum} the proposal binds; its node has not reached that block yet`,
55
+ );
56
+ this.name = 'ChainBehindBoundBlockError';
57
+ this.syncHeight = details.syncHeight;
58
+ this.boundBlockNum = details.boundBlockNum;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Whether a failed re-execution came from chain state this client can catch up
64
+ * with rather than from the proposal itself: a node that has not reached the
65
+ * bound block yet, or account state the node pruned because this client had
66
+ * not synced recently. Either clears on a later attempt, which syncs first.
67
+ */
68
+ export function isStaleChainError(error: unknown): boolean {
69
+ if (error instanceof ChainBehindBoundBlockError) {
70
+ return true;
71
+ }
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ return message.includes('has been pruned');
74
+ }
75
+
76
+ /**
77
+ * Derives the summary awaiting authorization for a proposal the caller is
78
+ * creating now, and captures a `ChainAnchor` at the current sync height to ship
79
+ * with it.
80
+ *
81
+ * The summary is derived at the chain tip, like every other execution of a
82
+ * multisig proposal (see {@link executeForSummaryAtTip}). The anchor still
83
+ * travels in the proposal: it names the block the request's auth args bind,
84
+ * which is how a rebuild learns that block, and 0.18.0-rc.1 clients re-execute
85
+ * at it. A proposer builds at the sync height the anchor is captured at, and
86
+ * the check below is what makes that hold.
25
87
  */
26
88
  export function executeForSummary(
27
89
  client: MidenClient,
@@ -41,18 +103,143 @@ export async function executeForSummary(
41
103
  txRequest: TransactionRequest,
42
104
  midenRpcEndpoint?: string,
43
105
  ): Promise<{ summary: TransactionSummary; anchor: ChainAnchor }> {
44
- const acc = AccountId.fromHex(accountId);
45
106
  const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
46
107
  const anchor = await rawClient.chainAnchorForRequest(txRequest);
47
- const summary = await rawClient.executeForSummaryAt(acc, txRequest, anchor);
108
+ let summary: TransactionSummary;
109
+ try {
110
+ summary = await executeForSummaryAtTip(rawClient, accountId, txRequest);
111
+ } catch (error) {
112
+ anchor.free();
113
+ throw error;
114
+ }
115
+
116
+ const anchorCommitment = anchor.commitment();
117
+ const summaryBlockCommitment = summary.blockCommitment();
118
+ const anchorCommitmentHex = normalizeHexWord(anchorCommitment.toHex());
119
+ const summaryBlockCommitmentHex = normalizeHexWord(summaryBlockCommitment.toHex());
120
+ anchorCommitment.free?.();
121
+ summaryBlockCommitment.free?.();
122
+ if (anchorCommitmentHex !== summaryBlockCommitmentHex) {
123
+ anchor.free();
124
+ throw new SummaryAnchorMismatchError({ anchorCommitmentHex, summaryBlockCommitmentHex });
125
+ }
48
126
  return { summary, anchor };
49
127
  }
50
128
 
129
+ /**
130
+ * Executes a multisig request at the chain tip to obtain the summary awaiting
131
+ * authorization. This is how cosigners and the executor reproduce a proposal's
132
+ * summary, whatever block they have synced to.
133
+ *
134
+ * Since protocol 0.17 a multisig summary binds the block its auth args name
135
+ * (the bound block), not the block the transaction executes against, so it
136
+ * reproduces at any later tip once the bound block is in the transaction's
137
+ * partial blockchain. The request declares it through `withBlockNumbers`, and
138
+ * foreign accounts, the fee faucet among them, load at the tip. Re-executing at
139
+ * the proposal's anchor instead loads them at the bound block, which a node
140
+ * prunes about 50 blocks later (issue #462).
141
+ *
142
+ * The client has to have synced to at least the bound block. When it has not,
143
+ * this syncs once before executing.
144
+ *
145
+ * @throws BoundBlockNotDeclaredError when the request binds a block in its
146
+ * multisig auth args without declaring it.
147
+ */
148
+ export function executeForSummaryAtTip(
149
+ client: MidenClient,
150
+ accountId: string,
151
+ txRequest: TransactionRequest,
152
+ midenRpcEndpoint: string,
153
+ ): Promise<TransactionSummary>;
154
+ export function executeForSummaryAtTip(
155
+ client: WasmWebClient,
156
+ accountId: string,
157
+ txRequest: TransactionRequest,
158
+ midenRpcEndpoint?: string,
159
+ ): Promise<TransactionSummary>;
160
+ export async function executeForSummaryAtTip(
161
+ client: MidenClient | WasmWebClient,
162
+ accountId: string,
163
+ txRequest: TransactionRequest,
164
+ midenRpcEndpoint?: string,
165
+ ): Promise<TransactionSummary> {
166
+ const rawClient = await getRawMidenClient(client, midenRpcEndpoint);
167
+ await prepareTipExecution(rawClient, txRequest);
168
+ return rawClient.executeForSummary(AccountId.fromHex(accountId), txRequest);
169
+ }
170
+
171
+ /**
172
+ * Gets `client` ready to execute `request` at the chain tip: checks the
173
+ * request declares the block its multisig auth args bind, and syncs to that
174
+ * block (see {@link syncToBoundBlock}).
175
+ *
176
+ * @throws BoundBlockNotDeclaredError when the request binds a block in its
177
+ * multisig auth args without declaring it.
178
+ */
179
+ export async function prepareTipExecution(
180
+ client: WasmWebClient,
181
+ request: TransactionRequest,
182
+ syncState?: () => Promise<unknown>,
183
+ ): Promise<void> {
184
+ const boundBlockNum = requireDeclaredBoundBlock(request);
185
+ if (boundBlockNum !== undefined) {
186
+ await syncToBoundBlock(client, boundBlockNum, syncState);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * The block `request`'s multisig auth args bind, after checking the request
192
+ * declares it. `undefined` for a request without multisig auth args, which
193
+ * has no bound block to declare.
194
+ *
195
+ * @throws BoundBlockNotDeclaredError when the block is bound but not declared.
196
+ */
197
+ export function requireDeclaredBoundBlock(request: TransactionRequest): number | undefined {
198
+ const boundBlockNum = requestBoundBlockNum(request);
199
+ if (boundBlockNum !== undefined && !request.blockNumbers().includes(boundBlockNum)) {
200
+ throw new BoundBlockNotDeclaredError(boundBlockNum);
201
+ }
202
+ return boundBlockNum;
203
+ }
204
+
205
+ /**
206
+ * Syncs `client` once when its sync height is below `blockNum`, the block a
207
+ * proposal binds. Execution at a tip below it fails with "requested block N is
208
+ * after transaction reference block M", and a store that has never synced (a
209
+ * cosigner that has only just loaded the account) holds no header to rebuild
210
+ * the request from. `syncState` lets a caller wrap the sync in its own retry
211
+ * policy.
212
+ *
213
+ * This does not make a store that is already past the bound block current. An
214
+ * execution loads foreign accounts, the fee faucet among them, at the store's
215
+ * sync height, which a node prunes about 50 blocks later, so the multisig
216
+ * entry points that re-execute a proposal sync the chain first.
217
+ *
218
+ * @throws ChainBehindBoundBlockError when the node has not reached the block.
219
+ */
220
+ export async function syncToBoundBlock(
221
+ client: WasmWebClient,
222
+ blockNum: number,
223
+ syncState: () => Promise<unknown> = () => client.syncState(),
224
+ ): Promise<void> {
225
+ if ((await client.getSyncHeight()) >= blockNum) {
226
+ return;
227
+ }
228
+ await syncState();
229
+ const syncHeight = await client.getSyncHeight();
230
+ if (syncHeight < blockNum) {
231
+ throw new ChainBehindBoundBlockError({ syncHeight, boundBlockNum: blockNum });
232
+ }
233
+ }
234
+
51
235
  /**
52
236
  * Executes a transaction at the given `ChainAnchor`'s reference block to
53
- * obtain the summary awaiting authorization — the anchored counterpart of
54
- * {@link executeForSummary} for cosigners and executors holding a proposal's
55
- * anchor.
237
+ * obtain the summary awaiting authorization.
238
+ *
239
+ * For a summary that binds the reference block, such as a single-signature
240
+ * one. A multisig proposal's summary binds its bound block instead and is
241
+ * reproduced with {@link executeForSummaryAtTip}: re-executing it at an anchor
242
+ * fails once the node prunes the anchor block's account state.
56
243
  */
57
244
  export function executeForSummaryAt(
58
245
  client: MidenClient,
@@ -91,26 +278,48 @@ export function chainAnchorToBase64(anchor: ChainAnchor): string {
91
278
  * Deserializes a `ChainAnchor` from its base64 wire form. `ChainAnchor`
92
279
  * deserialization validates the header/chain consistency internally, so a
93
280
  * decoded anchor only needs its block commitment checked against the signed
94
- * transaction summary before it is safe to execute against.
281
+ * transaction summary before the block it names is taken as the one the
282
+ * summary binds.
95
283
  */
96
284
  export function chainAnchorFromBase64(anchorBase64: string): ChainAnchor {
97
285
  return ChainAnchor.deserialize(base64ToUint8Array(anchorBase64));
98
286
  }
99
287
 
100
288
  /**
101
- * Reads the auth args back out of a transaction summary.
102
- *
103
- * Since miden-protocol 0.16-rc the summary binds seven user-defined elements
104
- * instead of a dedicated salt word. The guarded-multisig auth component zeroes
105
- * the leading three and passes the auth args as the trailing four, so the auth
106
- * args are the tail of `userParams()`.
289
+ * The block a proposal's `chainAnchor` names, which is the block its summary
290
+ * binds: a custom producer rebuilds its request at this block. Decodes the
291
+ * anchor for the one number and frees it.
292
+ */
293
+ export function chainAnchorBlockNum(anchorBase64: string): number {
294
+ const anchor = chainAnchorFromBase64(anchorBase64);
295
+ try {
296
+ return anchor.blockNum();
297
+ } finally {
298
+ anchor.free();
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Reads the salt a multisig transaction summary binds.
107
304
  *
108
- * This is the auth-arg word, *not* the proposal salt. When the request declares
109
- * a fee conversion salt, miden-client uses it to commit the native conversion
110
- * info under `hash(CONVERSION_INFO || SALT)`. That commitment is not invertible
111
- * to the salt. Keep the salt alongside the proposal — `ProposalMetadata.saltHex`
112
- * — rather than trying to recover it from the summary.
305
+ * Since protocol 0.17 the multisig auth components bind the salt itself into
306
+ * the summary's user params rather than a commitment derived from it, so the
307
+ * value cosigners signed over is readable again. A proposal still carries the
308
+ * salt in its metadata, because a request has to be rebuilt before any summary
309
+ * exists; this reader is the cross-check that the two agree.
310
+ */
311
+ export function summarySalt(summary: TransactionSummary): Word {
312
+ return Word.newFromFelts(
313
+ summary.userParams().slice(SALT_USER_PARAM_OFFSET, SALT_USER_PARAM_OFFSET + 4),
314
+ );
315
+ }
316
+
317
+ /**
318
+ * Reads the block at which the approvers' signatures stop authorizing the
319
+ * transaction, or `undefined` for an approval that never expires, which is
320
+ * what this package's builders produce.
113
321
  */
114
- export function summaryAuthArg(summary: TransactionSummary): Word {
115
- return Word.newFromFelts(summary.userParams().slice(AUTH_ARG_USER_PARAM_OFFSET));
322
+ export function summaryApprovalExpirationBlockNum(summary: TransactionSummary): number | undefined {
323
+ const value = summary.userParams()[APPROVAL_EXPIRATION_USER_PARAM_INDEX].asInt();
324
+ return value === 0n ? undefined : Number(value);
116
325
  }
@@ -1,17 +1,15 @@
1
1
  import {
2
2
  type MidenClient,
3
3
  TransactionRequest,
4
- TransactionRequestBuilder,
5
4
  TransactionScript,
6
5
  type WasmWebClient,
7
6
  Word,
8
- Word as WordType,
9
7
  } from '@miden-sdk/miden-sdk';
10
8
  import { compileTxScript } from '../raw-client.js';
11
9
  import { normalizeHexWord } from '../utils/encoding.js';
12
- import { randomWord } from '../utils/random.js';
13
10
  import { authSchemeId } from '../utils/signature.js';
14
- import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
11
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
12
+ import type { MidenClientMultisigRequestOptions, MultisigRequestOptions } from './options.js';
15
13
  import type { SignatureScheme } from '../types.js';
16
14
 
17
15
  async function buildUpdateGuardianScript(
@@ -44,17 +42,17 @@ end
44
42
  export function buildUpdateGuardianTransactionRequest(
45
43
  client: MidenClient,
46
44
  newGuardianPubkey: string,
47
- options: MidenClientSignatureOptions,
45
+ options: MidenClientMultisigRequestOptions,
48
46
  ): Promise<{ request: TransactionRequest; salt: Word }>;
49
47
  export function buildUpdateGuardianTransactionRequest(
50
48
  client: WasmWebClient,
51
49
  newGuardianPubkey: string,
52
- options?: SignatureOptions,
50
+ options: MultisigRequestOptions,
53
51
  ): Promise<{ request: TransactionRequest; salt: Word }>;
54
52
  export async function buildUpdateGuardianTransactionRequest(
55
53
  client: MidenClient | WasmWebClient,
56
54
  newGuardianPubkey: string,
57
- options: SignatureOptions = {},
55
+ options: MultisigRequestOptions,
58
56
  ): Promise<{ request: TransactionRequest; salt: Word }> {
59
57
  const signatureScheme = options.signatureScheme ?? 'falcon';
60
58
  const script = await buildUpdateGuardianScript(
@@ -64,24 +62,12 @@ export async function buildUpdateGuardianTransactionRequest(
64
62
  options.midenRpcEndpoint,
65
63
  );
66
64
 
67
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
68
- const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
69
-
70
- let txBuilder = new TransactionRequestBuilder();
71
- txBuilder = txBuilder.withCustomScript(script);
72
- txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
73
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
74
- // so the handle stays ours to release once the builder has read it.
75
- authSaltForBuilder.free?.();
65
+ const { builder, saltHex } = await multisigRequestBuilder(client, options);
66
+ let txBuilder = builder.withCustomScript(script);
76
67
 
77
68
  if (options.signatureAdviceMap) {
78
69
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
79
70
  }
80
71
 
81
- const authSaltForReturn = WordType.fromHex(normalizeHexWord(authSaltHex));
82
-
83
- return {
84
- request: txBuilder.build(),
85
- salt: authSaltForReturn,
86
- };
72
+ return buildMultisigRequest(txBuilder, saltHex, options.accountId);
87
73
  }
@@ -4,7 +4,6 @@ import {
4
4
  type MidenClient,
5
5
  Poseidon2,
6
6
  TransactionRequest,
7
- TransactionRequestBuilder,
8
7
  TransactionScript,
9
8
  type WasmWebClient,
10
9
  Word,
@@ -13,8 +12,8 @@ import {
13
12
  import { getProcedureRoot, type ProcedureName } from '../procedures.js';
14
13
  import { compileTxScript } from '../raw-client.js';
15
14
  import { normalizeHexWord } from '../utils/encoding.js';
16
- import { randomWord } from '../utils/random.js';
17
- import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
15
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
16
+ import type { MidenClientMultisigRequestOptions, MultisigRequestOptions } from './options.js';
18
17
 
19
18
  function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: number): Felt[] {
20
19
  const procedureRoot = WordType.fromHex(normalizeHexWord(getProcedureRoot(procedure)));
@@ -66,19 +65,19 @@ export function buildUpdateProcedureThresholdTransactionRequest(
66
65
  client: MidenClient,
67
66
  procedure: ProcedureName,
68
67
  threshold: number,
69
- options: MidenClientSignatureOptions,
68
+ options: MidenClientMultisigRequestOptions,
70
69
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
71
70
  export function buildUpdateProcedureThresholdTransactionRequest(
72
71
  client: WasmWebClient,
73
72
  procedure: ProcedureName,
74
73
  threshold: number,
75
- options?: SignatureOptions,
74
+ options: MultisigRequestOptions,
76
75
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
77
76
  export async function buildUpdateProcedureThresholdTransactionRequest(
78
77
  client: MidenClient | WasmWebClient,
79
78
  procedure: ProcedureName,
80
79
  threshold: number,
81
- options: SignatureOptions = {},
80
+ options: MultisigRequestOptions,
82
81
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
83
82
  const configHash = buildProcedureThresholdConfigHash(procedure, threshold);
84
83
 
@@ -88,23 +87,12 @@ export async function buildUpdateProcedureThresholdTransactionRequest(
88
87
  threshold,
89
88
  options.midenRpcEndpoint,
90
89
  );
91
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
92
- const authSalt = WordType.fromHex(normalizeHexWord(authSaltHex));
93
-
94
- let txBuilder = new TransactionRequestBuilder();
95
- txBuilder = txBuilder.withCustomScript(script);
96
- txBuilder = txBuilder.withFeeConversionSalt(authSalt);
97
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
98
- // so the handle stays ours to release once the builder has read it.
99
- authSalt.free?.();
90
+ const { builder, saltHex } = await multisigRequestBuilder(client, options);
91
+ let txBuilder = builder.withCustomScript(script);
100
92
 
101
93
  if (options.signatureAdviceMap) {
102
94
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
103
95
  }
104
96
 
105
- return {
106
- request: txBuilder.build(),
107
- salt: WordType.fromHex(normalizeHexWord(authSaltHex)),
108
- configHash,
109
- };
97
+ return { ...buildMultisigRequest(txBuilder, saltHex, options.accountId), configHash };
110
98
  }
@@ -5,7 +5,6 @@ import {
5
5
  type MidenClient,
6
6
  Poseidon2,
7
7
  TransactionRequest,
8
- TransactionRequestBuilder,
9
8
  TransactionScript,
10
9
  type WasmWebClient,
11
10
  Word,
@@ -13,9 +12,9 @@ import {
13
12
  } from '@miden-sdk/miden-sdk';
14
13
  import { compileTxScript } from '../raw-client.js';
15
14
  import { normalizeHexWord } from '../utils/encoding.js';
16
- import { randomWord } from '../utils/random.js';
17
15
  import { authSchemeId } from '../utils/signature.js';
18
- import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
16
+ import { buildMultisigRequest, multisigRequestBuilder } from './authArgs.js';
17
+ import type { MidenClientMultisigRequestOptions, MultisigRequestOptions } from './options.js';
19
18
  import type { SignatureScheme } from '../types.js';
20
19
 
21
20
  function buildMultisigConfigFelts(
@@ -77,19 +76,19 @@ export function buildUpdateSignersTransactionRequest(
77
76
  client: MidenClient,
78
77
  threshold: number,
79
78
  signerCommitments: string[],
80
- options: MidenClientSignatureOptions,
79
+ options: MidenClientMultisigRequestOptions,
81
80
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
82
81
  export function buildUpdateSignersTransactionRequest(
83
82
  client: WasmWebClient,
84
83
  threshold: number,
85
84
  signerCommitments: string[],
86
- options?: SignatureOptions,
85
+ options: MultisigRequestOptions,
87
86
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }>;
88
87
  export async function buildUpdateSignersTransactionRequest(
89
88
  client: MidenClient | WasmWebClient,
90
89
  threshold: number,
91
90
  signerCommitments: string[],
92
- options: SignatureOptions = {},
91
+ options: MultisigRequestOptions,
93
92
  ): Promise<{ request: TransactionRequest; salt: Word; configHash: Word }> {
94
93
  const signatureScheme = options.signatureScheme ?? 'falcon';
95
94
  const { configHash: configHashForAdvice, payload } = buildMultisigConfigAdvice(
@@ -115,28 +114,18 @@ export async function buildUpdateSignersTransactionRequest(
115
114
 
116
115
  const script = await buildUpdateSignersScript(client, options.midenRpcEndpoint);
117
116
 
118
- const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
119
-
120
- const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
121
-
122
- let txBuilder = new TransactionRequestBuilder();
123
- txBuilder = txBuilder.withCustomScript(script);
124
- txBuilder = txBuilder.withScriptArg(configHashForScript);
125
- txBuilder = txBuilder.extendAdviceMap(advice);
126
- txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
127
- // Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
128
- // so the handle stays ours to release once the builder has read it.
129
- authSaltForBuilder.free?.();
117
+ const { builder, saltHex } = await multisigRequestBuilder(client, options);
118
+ let txBuilder = builder
119
+ .withCustomScript(script)
120
+ .withScriptArg(configHashForScript)
121
+ .extendAdviceMap(advice);
130
122
 
131
123
  if (options.signatureAdviceMap) {
132
124
  txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
133
125
  }
134
126
 
135
- const authSaltForReturn = WordType.fromHex(normalizeHexWord(authSaltHex));
136
-
137
127
  return {
138
- request: txBuilder.build(),
139
- salt: authSaltForReturn,
128
+ ...buildMultisigRequest(txBuilder, saltHex, options.accountId),
140
129
  configHash: configHashForReturn,
141
130
  };
142
131
  }
@@ -1,12 +1,29 @@
1
+ export {
2
+ MAX_APPROVAL_EXPIRATION_DELTA,
3
+ buildMultisigRequest,
4
+ multisigRequestBuilder,
5
+ requestBoundBlockNum,
6
+ requestSaltHex,
7
+ } from './transaction/authArgs.js';
1
8
  export {
2
9
  buildConsumeNotesTransactionRequest,
10
+ buildConsumeNotesTransactionRequestFromNotes,
3
11
  } from './transaction/consumeNotes.js';
4
12
  export {
13
+ ChainBehindBoundBlockError,
14
+ chainAnchorBlockNum,
5
15
  chainAnchorFromBase64,
6
16
  chainAnchorToBase64,
7
17
  executeForSummary,
8
18
  executeForSummaryAt,
9
- summaryAuthArg,
19
+ executeForSummaryAtTip,
20
+ prepareTipExecution,
21
+ isStaleChainError,
22
+ requireDeclaredBoundBlock,
23
+ syncToBoundBlock,
24
+ summaryApprovalExpirationBlockNum,
25
+ summarySalt,
26
+ SummaryAnchorMismatchError,
10
27
  } from './transaction/summary.js';
11
28
  export {
12
29
  buildP2idNoteFromMetadata,
@@ -38,11 +38,11 @@ interface BaseProposalMetadata {
38
38
  saltHex?: string;
39
39
  requiredSignatures?: number;
40
40
  /**
41
- * Base64-serialized Miden `ChainAnchor` pinning the reference block the
42
- * proposal's transaction summary was built at. Required to verify or
43
- * execute the proposal: since protocol 0.16 the signed summary binds the
44
- * reference block commitment, so it only reproduces when re-executed at
45
- * that block.
41
+ * Base64-serialized Miden `ChainAnchor` at the block the proposal's
42
+ * transaction summary binds, the proposer's sync height when it built the
43
+ * request. Required, and checked against the summary's block commitment: a
44
+ * rebuild binds the block it names. The proposal executes at the chain tip,
45
+ * not at the anchor; 0.18.0-rc.1 clients still re-execute at it.
46
46
  */
47
47
  chainAnchor?: string;
48
48
  }
@@ -162,6 +162,36 @@ export interface Proposal {
162
162
  txSummary: string;
163
163
  signatures: ProposalSignatureEntry[];
164
164
  metadata: ProposalMetadata;
165
+ /**
166
+ * Result of the last summary-binding check on this value. Only the check
167
+ * itself writes `verified`; a freshly parsed or imported proposal is
168
+ * `unchecked`. `syncProposals` surfaces failed proposals instead of
169
+ * failing wholesale (issue #462); `signProposal` and `executeProposal`
170
+ * re-verify and refuse them.
171
+ */
172
+ verification: ProposalVerification;
173
+ }
174
+
175
+ /**
176
+ * Outcome of checking a proposal's metadata against its signed summary.
177
+ * `failed.retryable` is true when the failure came from a transient node or
178
+ * RPC error, or from chain state this client had not caught up with, so the
179
+ * same proposal may verify on a later sync; false when the proposal itself
180
+ * cannot be reproduced (tampered metadata, for example) and it has to be
181
+ * re-proposed.
182
+ */
183
+ export type ProposalVerification =
184
+ | { status: 'unchecked' }
185
+ | { status: 'verified' }
186
+ | { status: 'failed'; retryable: boolean; message: string };
187
+
188
+ /**
189
+ * True when the proposal is both verified and has met its signature
190
+ * threshold, i.e. it can be executed. `status` alone keeps meaning
191
+ * "threshold met": a fully signed proposal can still be dead.
192
+ */
193
+ export function isProposalActionable(proposal: Proposal): boolean {
194
+ return proposal.verification.status === 'verified' && proposal.status === 'ready';
165
195
  }
166
196
 
167
197
  export interface TransactionProposal {
@@ -185,6 +215,7 @@ export interface ExportedProposal {
185
215
  signatureHex: string;
186
216
  scheme?: SignatureScheme;
187
217
  publicKey?: string;
218
+ messageFormat?: 'eip712';
188
219
  timestamp?: string;
189
220
  }>;
190
221
  metadata: ProposalMetadata;