@openzeppelin/miden-multisig-client 0.12.7 → 0.13.0

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 (201) hide show
  1. package/README.md +42 -4
  2. package/dist/account/builder.d.ts +1 -1
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +7 -5
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/masm.d.ts +3 -3
  7. package/dist/account/masm.d.ts.map +1 -1
  8. package/dist/account/masm.js +101 -98
  9. package/dist/account/masm.js.map +1 -1
  10. package/dist/account/storage.d.ts +1 -1
  11. package/dist/account/storage.d.ts.map +1 -1
  12. package/dist/account/storage.js +17 -7
  13. package/dist/account/storage.js.map +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +2 -2
  17. package/dist/client.js.map +1 -1
  18. package/dist/client.test.js +4 -4
  19. package/dist/client.test.js.map +1 -1
  20. package/dist/index.d.ts +8 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +6 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/inspector.d.ts +1 -1
  25. package/dist/inspector.d.ts.map +1 -1
  26. package/dist/inspector.js +16 -10
  27. package/dist/inspector.js.map +1 -1
  28. package/dist/inspector.test.js +33 -22
  29. package/dist/inspector.test.js.map +1 -1
  30. package/dist/multisig/helpers.d.ts +1 -1
  31. package/dist/multisig/helpers.d.ts.map +1 -1
  32. package/dist/multisig/helpers.js +1 -1
  33. package/dist/multisig/helpers.js.map +1 -1
  34. package/dist/multisig/helpers.test.js +2 -2
  35. package/dist/multisig/helpers.test.js.map +1 -1
  36. package/dist/multisig/proposal/execution.d.ts +18 -0
  37. package/dist/multisig/proposal/execution.d.ts.map +1 -0
  38. package/dist/multisig/proposal/execution.js +147 -0
  39. package/dist/multisig/proposal/execution.js.map +1 -0
  40. package/dist/multisig/proposal/parser.d.ts +17 -0
  41. package/dist/multisig/proposal/parser.d.ts.map +1 -0
  42. package/dist/multisig/proposal/parser.js +147 -0
  43. package/dist/multisig/proposal/parser.js.map +1 -0
  44. package/dist/multisig/signing.d.ts +5 -0
  45. package/dist/multisig/signing.d.ts.map +1 -0
  46. package/dist/multisig/signing.js +14 -0
  47. package/dist/multisig/signing.js.map +1 -0
  48. package/dist/multisig.d.ts +9 -13
  49. package/dist/multisig.d.ts.map +1 -1
  50. package/dist/multisig.js +109 -337
  51. package/dist/multisig.js.map +1 -1
  52. package/dist/multisig.test.js +8 -4
  53. package/dist/multisig.test.js.map +1 -1
  54. package/dist/signer.d.ts +2 -20
  55. package/dist/signer.d.ts.map +1 -1
  56. package/dist/signer.js +2 -82
  57. package/dist/signer.js.map +1 -1
  58. package/dist/signer.test.js +24 -24
  59. package/dist/signer.test.js.map +1 -1
  60. package/dist/signers/ecdsa.d.ts +12 -0
  61. package/dist/signers/ecdsa.d.ts.map +1 -0
  62. package/dist/signers/ecdsa.js +30 -0
  63. package/dist/signers/ecdsa.js.map +1 -0
  64. package/dist/signers/falcon.d.ts +12 -0
  65. package/dist/signers/falcon.d.ts.map +1 -0
  66. package/dist/signers/falcon.js +31 -0
  67. package/dist/signers/falcon.js.map +1 -0
  68. package/dist/signers/falcon.test.d.ts +2 -0
  69. package/dist/signers/falcon.test.d.ts.map +1 -0
  70. package/dist/signers/falcon.test.js +113 -0
  71. package/dist/signers/falcon.test.js.map +1 -0
  72. package/dist/signers/index.d.ts +5 -0
  73. package/dist/signers/index.d.ts.map +1 -0
  74. package/dist/signers/index.js +5 -0
  75. package/dist/signers/index.js.map +1 -0
  76. package/dist/signers/miden-wallet.d.ts +16 -0
  77. package/dist/signers/miden-wallet.d.ts.map +1 -0
  78. package/dist/signers/miden-wallet.js +35 -0
  79. package/dist/signers/miden-wallet.js.map +1 -0
  80. package/dist/signers/miden-wallet.test.d.ts +2 -0
  81. package/dist/signers/miden-wallet.test.d.ts.map +1 -0
  82. package/dist/signers/miden-wallet.test.js +107 -0
  83. package/dist/signers/miden-wallet.test.js.map +1 -0
  84. package/dist/signers/para.d.ts +21 -0
  85. package/dist/signers/para.d.ts.map +1 -0
  86. package/dist/signers/para.js +43 -0
  87. package/dist/signers/para.js.map +1 -0
  88. package/dist/signers/para.test.d.ts +2 -0
  89. package/dist/signers/para.test.d.ts.map +1 -0
  90. package/dist/signers/para.test.js +88 -0
  91. package/dist/signers/para.test.js.map +1 -0
  92. package/dist/transaction/consumeNotes.d.ts +3 -3
  93. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  94. package/dist/transaction/consumeNotes.js +11 -7
  95. package/dist/transaction/consumeNotes.js.map +1 -1
  96. package/dist/transaction/options.d.ts +2 -2
  97. package/dist/transaction/options.d.ts.map +1 -1
  98. package/dist/transaction/p2id.d.ts +1 -1
  99. package/dist/transaction/p2id.d.ts.map +1 -1
  100. package/dist/transaction/p2id.js +5 -5
  101. package/dist/transaction/p2id.js.map +1 -1
  102. package/dist/transaction/summary.d.ts +1 -1
  103. package/dist/transaction/summary.d.ts.map +1 -1
  104. package/dist/transaction/summary.js +1 -1
  105. package/dist/transaction/summary.js.map +1 -1
  106. package/dist/transaction/updatePsm.d.ts +1 -1
  107. package/dist/transaction/updatePsm.d.ts.map +1 -1
  108. package/dist/transaction/updatePsm.js +5 -5
  109. package/dist/transaction/updatePsm.js.map +1 -1
  110. package/dist/transaction/updateSigners.d.ts +1 -1
  111. package/dist/transaction/updateSigners.d.ts.map +1 -1
  112. package/dist/transaction/updateSigners.js +5 -5
  113. package/dist/transaction/updateSigners.js.map +1 -1
  114. package/dist/transaction.test.js +1 -1
  115. package/dist/transaction.test.js.map +1 -1
  116. package/dist/types.d.ts +1 -1
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/utils/digest.d.ts +6 -0
  119. package/dist/utils/digest.d.ts.map +1 -0
  120. package/dist/utils/digest.js +22 -0
  121. package/dist/utils/digest.js.map +1 -0
  122. package/dist/utils/ecdsa.d.ts +8 -0
  123. package/dist/utils/ecdsa.d.ts.map +1 -0
  124. package/dist/utils/ecdsa.js +60 -0
  125. package/dist/utils/ecdsa.js.map +1 -0
  126. package/dist/utils/ecdsa.test.d.ts +2 -0
  127. package/dist/utils/ecdsa.test.d.ts.map +1 -0
  128. package/dist/utils/ecdsa.test.js +85 -0
  129. package/dist/utils/ecdsa.test.js.map +1 -0
  130. package/dist/utils/index.d.ts +1 -0
  131. package/dist/utils/index.d.ts.map +1 -1
  132. package/dist/utils/index.js +1 -0
  133. package/dist/utils/index.js.map +1 -1
  134. package/dist/utils/key.d.ts +10 -0
  135. package/dist/utils/key.d.ts.map +1 -0
  136. package/dist/utils/key.js +38 -0
  137. package/dist/utils/key.js.map +1 -0
  138. package/dist/utils/key.test.d.ts +2 -0
  139. package/dist/utils/key.test.d.ts.map +1 -0
  140. package/dist/utils/key.test.js +80 -0
  141. package/dist/utils/key.test.js.map +1 -0
  142. package/dist/utils/random.d.ts +1 -1
  143. package/dist/utils/random.d.ts.map +1 -1
  144. package/dist/utils/random.js +1 -1
  145. package/dist/utils/random.js.map +1 -1
  146. package/dist/utils/signature.d.ts +2 -1
  147. package/dist/utils/signature.d.ts.map +1 -1
  148. package/dist/utils/signature.js +6 -3
  149. package/dist/utils/signature.js.map +1 -1
  150. package/dist/utils/signature.test.js +11 -11
  151. package/dist/utils/signature.test.js.map +1 -1
  152. package/dist/utils/word.d.ts +6 -1
  153. package/dist/utils/word.d.ts.map +1 -1
  154. package/dist/utils/word.js +11 -0
  155. package/dist/utils/word.js.map +1 -1
  156. package/dist/utils/word.test.js +54 -3
  157. package/dist/utils/word.test.js.map +1 -1
  158. package/package.json +4 -5
  159. package/src/account/builder.ts +7 -5
  160. package/src/account/masm.ts +103 -99
  161. package/src/account/storage.ts +19 -7
  162. package/src/client.test.ts +4 -4
  163. package/src/client.ts +3 -3
  164. package/src/index.ts +8 -1
  165. package/src/inspector.test.ts +38 -25
  166. package/src/inspector.ts +18 -10
  167. package/src/multisig/helpers.test.ts +2 -2
  168. package/src/multisig/helpers.ts +2 -1
  169. package/src/multisig/proposal/execution.ts +320 -0
  170. package/src/multisig/proposal/parser.ts +191 -0
  171. package/src/multisig/signing.ts +24 -0
  172. package/src/multisig.test.ts +8 -4
  173. package/src/multisig.ts +153 -434
  174. package/src/signer.ts +2 -95
  175. package/src/signers/ecdsa.ts +35 -0
  176. package/src/{signer.test.ts → signers/falcon.test.ts} +29 -33
  177. package/src/signers/falcon.ts +36 -0
  178. package/src/signers/index.ts +4 -0
  179. package/src/signers/miden-wallet.test.ts +119 -0
  180. package/src/signers/miden-wallet.ts +49 -0
  181. package/src/signers/para.test.ts +103 -0
  182. package/src/signers/para.ts +60 -0
  183. package/src/transaction/consumeNotes.ts +14 -10
  184. package/src/transaction/options.ts +2 -2
  185. package/src/transaction/p2id.ts +3 -8
  186. package/src/transaction/summary.ts +2 -2
  187. package/src/transaction/updatePsm.ts +5 -6
  188. package/src/transaction/updateSigners.ts +5 -6
  189. package/src/transaction.test.ts +1 -1
  190. package/src/types.ts +1 -1
  191. package/src/utils/digest.ts +25 -0
  192. package/src/utils/ecdsa.test.ts +104 -0
  193. package/src/utils/ecdsa.ts +77 -0
  194. package/src/utils/index.ts +1 -0
  195. package/src/utils/key.test.ts +89 -0
  196. package/src/utils/key.ts +49 -0
  197. package/src/utils/random.ts +1 -1
  198. package/src/utils/signature.test.ts +11 -11
  199. package/src/utils/signature.ts +10 -3
  200. package/src/utils/word.test.ts +57 -3
  201. package/src/utils/word.ts +13 -1
package/src/multisig.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PsmHttpClient, type DeltaObject, type DeltaStatus, type ProposalSignature, type Signer, type AuthConfig, type StateObject, type ProposalMetadata as PsmProposalMetadata } from '@openzeppelin/psm-client';
1
+ import { PsmHttpClient, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/psm-client';
2
2
  import type {
3
3
  ConsumableNote,
4
4
  ExportedTransactionProposal,
@@ -6,7 +6,6 @@ import type {
6
6
  NoteAsset,
7
7
  TransactionProposal,
8
8
  ProposalMetadata,
9
- TransactionProposalSignature,
10
9
  TransactionProposalStatus,
11
10
  ProposalType,
12
11
  SignTransactionProposalParams,
@@ -15,16 +14,11 @@ import type {
15
14
  } from './types.js';
16
15
  import type { ProcedureName } from './procedures.js';
17
16
  import { AccountInspector, type DetectedMultisigConfig } from './inspector.js';
18
- import type { WebClient, TransactionRequest } from '@demox-labs/miden-sdk';
17
+ import type { WebClient, TransactionRequest } from '@miden-sdk/miden-sdk';
19
18
  import {
20
19
  Account,
21
20
  AccountId,
22
- AdviceMap,
23
- FeltArray,
24
- Signature,
25
- TransactionSummary,
26
- Word,
27
- } from '@demox-labs/miden-sdk';
21
+ } from '@miden-sdk/miden-sdk';
28
22
  import {
29
23
  executeForSummary,
30
24
  buildUpdateSignersTransactionRequest,
@@ -37,8 +31,19 @@ import {
37
31
  uint8ArrayToBase64,
38
32
  normalizeHexWord,
39
33
  } from './utils/encoding.js';
40
- import { buildSignatureAdviceEntry, signatureHexToBytes, tryComputeEcdsaCommitmentHex } from './utils/signature.js';
34
+ import { signatureHexToBytes } from './utils/signature.js';
41
35
  import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
36
+ import {
37
+ buildPsmMetadata,
38
+ deltaToProposal,
39
+ resolveMetadata,
40
+ signatureRequirementForProposal,
41
+ } from './multisig/proposal/parser.js';
42
+ import {
43
+ toPsmSignature,
44
+ buildPsmSignatureFromSigner,
45
+ } from './multisig/signing.js';
46
+ import { executeProposalWorkflow } from './multisig/proposal/execution.js';
42
47
 
43
48
  export interface AccountState {
44
49
  accountId: string;
@@ -50,12 +55,12 @@ export interface AccountState {
50
55
  }
51
56
 
52
57
  export class Multisig {
53
- readonly account: Account | null;
54
- readonly threshold: number;
55
- readonly signerCommitments: string[];
56
- readonly psmCommitment: string;
58
+ account: Account | null;
59
+ threshold: number;
60
+ signerCommitments: string[];
61
+ psmCommitment: string;
57
62
  psmPublicKey?: string;
58
- readonly procedureThresholds: Map<ProcedureName, number>;
63
+ procedureThresholds: Map<ProcedureName, number>;
59
64
  readonly signatureScheme: Signer['scheme'];
60
65
 
61
66
  private psm: PsmHttpClient;
@@ -203,8 +208,7 @@ export class Multisig {
203
208
  const server = normalizeHexWord(response.ackCommitment);
204
209
  if (onChain !== server) {
205
210
  throw new Error(
206
- `PSM commitment mismatch: on-chain=${onChain}, server=${server}. ` +
207
- `Re-create the account with getPubkey('${this.signatureScheme}') to get the correct PSM commitment.`
211
+ `PSM commitment mismatch: on-chain=${onChain}, server=${server}`
208
212
  );
209
213
  }
210
214
  }
@@ -215,6 +219,14 @@ export class Multisig {
215
219
  const state = await this.syncState();
216
220
  const notes = await this.getConsumableNotes();
217
221
  const config = AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
222
+
223
+ this.threshold = config.threshold;
224
+ this.signerCommitments = config.signerCommitments;
225
+ if (config.psmCommitment) {
226
+ this.psmCommitment = config.psmCommitment;
227
+ }
228
+ this.procedureThresholds = config.procedureThresholds;
229
+
218
230
  return { proposals, state, notes, config };
219
231
  }
220
232
 
@@ -224,9 +236,44 @@ export class Multisig {
224
236
  }
225
237
 
226
238
  async switchPsm(psmClient: PsmHttpClient): Promise<void> {
239
+ const accountId = AccountId.fromHex(this._accountId);
240
+ const localAccount = await this.webClient.getAccount(accountId);
241
+ if (localAccount) {
242
+ this.account = localAccount;
243
+ const config = AccountInspector.fromAccount(localAccount, this.signatureScheme);
244
+ if (config.psmCommitment) {
245
+ this.psmCommitment = config.psmCommitment;
246
+ }
247
+ this.threshold = config.threshold;
248
+ this.signerCommitments = config.signerCommitments;
249
+ this.procedureThresholds = config.procedureThresholds;
250
+ }
251
+
227
252
  this.setPsmClient(psmClient);
228
- const state = await this.fetchState();
229
- await this.registerOnPsm(state.stateDataBase64);
253
+
254
+ const accountBytes: Uint8Array = this.account!.serialize();
255
+ const stateData = uint8ArrayToBase64(accountBytes);
256
+
257
+ const auth: AuthConfig = this.signer.scheme === 'ecdsa'
258
+ ? { MidenEcdsa: { cosigner_commitments: this.signerCommitments } }
259
+ : { MidenFalconRpo: { cosigner_commitments: this.signerCommitments } };
260
+
261
+ const response = await this.psm.configure({
262
+ accountId: this._accountId,
263
+ auth,
264
+ initialState: { data: stateData, accountId: this._accountId },
265
+ });
266
+
267
+ if (!response.success) {
268
+ throw new Error(`Failed to register on PSM: ${response.message}`);
269
+ }
270
+
271
+ if (response.ackCommitment) {
272
+ this.psmCommitment = normalizeHexWord(response.ackCommitment);
273
+ }
274
+ if (response.ackPubkey) {
275
+ this.psmPublicKey = response.ackPubkey;
276
+ }
230
277
  }
231
278
 
232
279
  async syncTransactionProposals(): Promise<TransactionProposal[]> {
@@ -238,16 +285,22 @@ export class Multisig {
238
285
  serverProposalIds.add(proposalId);
239
286
  const existingProposal = this.proposals.get(proposalId);
240
287
 
241
- const resolvedMetadata =
242
- existingProposal?.metadata ??
243
- (delta.deltaPayload.metadata
244
- ? this.fromPsmMetadata(delta.deltaPayload.metadata)
245
- : undefined);
288
+ const resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
246
289
  if (!resolvedMetadata) {
247
290
  throw new Error('Missing proposal metadata from PSM');
248
291
  }
249
292
 
250
- const proposal = this.deltaToProposal(delta, proposalId, resolvedMetadata, existingProposal?.signatures);
293
+ const proposal = deltaToProposal({
294
+ delta,
295
+ proposalId,
296
+ metadata: resolvedMetadata,
297
+ signaturesRequired: signatureRequirementForProposal(
298
+ resolvedMetadata,
299
+ this.threshold,
300
+ (proposalType) => this.getEffectiveThreshold(proposalType),
301
+ ),
302
+ existingSignatures: existingProposal?.signatures,
303
+ });
251
304
 
252
305
  this.proposals.set(proposal.id, proposal);
253
306
  }
@@ -268,7 +321,7 @@ export class Multisig {
268
321
  }
269
322
 
270
323
  async createProposal(nonce: number, txSummaryBase64: string, metadata: ProposalMetadata): Promise<TransactionProposal> {
271
- const psmMetadata = this.buildPsmMetadata(metadata);
324
+ const psmMetadata = buildPsmMetadata(metadata);
272
325
 
273
326
  const response = await this.psm.pushDeltaProposal({
274
327
  accountId: this._accountId,
@@ -280,7 +333,16 @@ export class Multisig {
280
333
  },
281
334
  });
282
335
 
283
- const proposal = this.deltaToProposal(response.delta, response.commitment, metadata);
336
+ const proposal = deltaToProposal({
337
+ delta: response.delta,
338
+ proposalId: response.commitment,
339
+ metadata,
340
+ signaturesRequired: signatureRequirementForProposal(
341
+ metadata,
342
+ this.threshold,
343
+ (proposalType) => this.getEffectiveThreshold(proposalType),
344
+ ),
345
+ });
284
346
  this.proposals.set(proposal.id, proposal);
285
347
 
286
348
  return proposal;
@@ -300,10 +362,6 @@ export class Multisig {
300
362
  { signatureScheme: this.signatureScheme }
301
363
  );
302
364
 
303
- const summary = await executeForSummary(this.webClient, this._accountId, request);
304
- const summaryBase64 = uint8ArrayToBase64(summary.serialize());
305
- const proposalNonce = options?.nonce ?? Date.now();
306
-
307
365
  const metadata: ProposalMetadata = {
308
366
  proposalType: 'add_signer',
309
367
  targetThreshold,
@@ -311,9 +369,7 @@ export class Multisig {
311
369
  saltHex: salt.toHex(),
312
370
  description: `Add signer ${newCommitment.slice(0, 10)}...`,
313
371
  };
314
-
315
- const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
316
- return this.syncAfterCreate(proposal);
372
+ return this.createAndSyncFromRequest(request, metadata, options?.nonce);
317
373
  }
318
374
 
319
375
  async createRemoveSignerProposal(
@@ -351,10 +407,6 @@ export class Multisig {
351
407
  { signatureScheme: this.signatureScheme }
352
408
  );
353
409
 
354
- const summary = await executeForSummary(this.webClient, this._accountId, request);
355
- const summaryBase64 = uint8ArrayToBase64(summary.serialize());
356
- const proposalNonce = options?.nonce ?? Date.now();
357
-
358
410
  const metadata: ProposalMetadata = {
359
411
  proposalType: 'remove_signer',
360
412
  targetThreshold,
@@ -362,9 +414,7 @@ export class Multisig {
362
414
  saltHex: salt.toHex(),
363
415
  description: `Remove signer ${signerToRemove.slice(0, 10)}...`,
364
416
  };
365
-
366
- const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
367
- return this.syncAfterCreate(proposal);
417
+ return this.createAndSyncFromRequest(request, metadata, options?.nonce);
368
418
  }
369
419
 
370
420
  async createChangeThresholdProposal(
@@ -388,10 +438,6 @@ export class Multisig {
388
438
  { signatureScheme: this.signatureScheme }
389
439
  );
390
440
 
391
- const summary = await executeForSummary(this.webClient, this._accountId, request);
392
- const summaryBase64 = uint8ArrayToBase64(summary.serialize());
393
- const proposalNonce = options?.nonce ?? Date.now();
394
-
395
441
  const metadata: ProposalMetadata = {
396
442
  proposalType: 'change_threshold',
397
443
  targetThreshold: newThreshold,
@@ -399,9 +445,7 @@ export class Multisig {
399
445
  saltHex: salt.toHex(),
400
446
  description: `Change threshold from ${this.threshold} to ${newThreshold}`,
401
447
  };
402
-
403
- const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
404
- return this.syncAfterCreate(proposal);
448
+ return this.createAndSyncFromRequest(request, metadata, options?.nonce);
405
449
  }
406
450
 
407
451
  async createSwitchPsmProposal(
@@ -452,11 +496,7 @@ export class Multisig {
452
496
  throw new Error('At least one note ID is required');
453
497
  }
454
498
 
455
- const { request, salt } = buildConsumeNotesTransactionRequest(noteIds);
456
-
457
- const summary = await executeForSummary(this.webClient, this._accountId, request);
458
- const summaryBase64 = uint8ArrayToBase64(summary.serialize());
459
- const proposalNonce = options?.nonce ?? Date.now();
499
+ const { request, salt } = await buildConsumeNotesTransactionRequest(this.webClient, noteIds);
460
500
 
461
501
  const metadata: ProposalMetadata = {
462
502
  proposalType: 'consume_notes',
@@ -464,9 +504,7 @@ export class Multisig {
464
504
  saltHex: salt.toHex(),
465
505
  description: `Consume ${noteIds.length} note(s)`,
466
506
  };
467
-
468
- const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
469
- return this.syncAfterCreate(proposal);
507
+ return this.createAndSyncFromRequest(request, metadata, options?.nonce);
470
508
  }
471
509
 
472
510
  async createSendProposal(
@@ -486,10 +524,6 @@ export class Multisig {
486
524
  amount,
487
525
  );
488
526
 
489
- const summary = await executeForSummary(this.webClient, this._accountId, request);
490
- const summaryBase64 = uint8ArrayToBase64(summary.serialize());
491
- const proposalNonce = options?.nonce ?? Date.now();
492
-
493
527
  const metadata: ProposalMetadata = {
494
528
  proposalType: 'p2id',
495
529
  saltHex: salt.toHex(),
@@ -498,9 +532,7 @@ export class Multisig {
498
532
  amount: amount.toString(),
499
533
  description: `Send ${amount} to ${recipientId.slice(0, 10)}...`,
500
534
  };
501
-
502
- const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
503
- return this.syncAfterCreate(proposal);
535
+ return this.createAndSyncFromRequest(request, metadata, options?.nonce);
504
536
  }
505
537
 
506
538
  async getConsumableNotes(): Promise<ConsumableNote[]> {
@@ -514,7 +546,7 @@ export class Multisig {
514
546
 
515
547
  const canConsumeNow = consumability.some(
516
548
  (c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
517
- c.consumableAfterBlock() === undefined
549
+ c.consumptionStatus().consumableAfterBlock() === undefined
518
550
  );
519
551
 
520
552
  if (canConsumeNow) {
@@ -539,12 +571,7 @@ export class Multisig {
539
571
 
540
572
  async signTransactionProposal(commitment: string): Promise<TransactionProposal[]> {
541
573
  const existingProposal = this.proposals.get(commitment);
542
-
543
- const signatureHex = this.signer.signCommitment(commitment);
544
-
545
- const signature: ProposalSignature = this.signer.scheme === 'ecdsa'
546
- ? { scheme: 'ecdsa', signature: signatureHex, publicKey: this.signer.publicKey }
547
- : { scheme: 'falcon', signature: signatureHex };
574
+ const signature = await buildPsmSignatureFromSigner(this.signer, commitment);
548
575
 
549
576
  const delta = await this.psm.signDeltaProposal({
550
577
  accountId: this._accountId,
@@ -552,15 +579,25 @@ export class Multisig {
552
579
  signature,
553
580
  });
554
581
 
555
- const proposal = this.deltaToProposal(delta, commitment, undefined, existingProposal?.signatures);
556
-
557
- if (existingProposal?.metadata) {
558
- proposal.metadata = existingProposal.metadata;
582
+ const resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
583
+ if (!resolvedMetadata) {
584
+ throw new Error('Missing proposal metadata');
559
585
  }
586
+ const proposal = deltaToProposal({
587
+ delta,
588
+ proposalId: commitment,
589
+ metadata: resolvedMetadata,
590
+ signaturesRequired: signatureRequirementForProposal(
591
+ resolvedMetadata,
592
+ this.threshold,
593
+ (proposalType) => this.getEffectiveThreshold(proposalType),
594
+ ),
595
+ existingSignatures: existingProposal?.signatures,
596
+ });
560
597
 
561
598
  this.proposals.set(proposal.id, proposal);
562
599
 
563
- return this.syncTransactionProposals();
600
+ return this.listTransactionProposals();
564
601
  }
565
602
 
566
603
  async signTransactionProposalExternal(
@@ -570,10 +607,7 @@ export class Multisig {
570
607
  const resolvedScheme = scheme ?? this.signatureScheme;
571
608
 
572
609
  const existingProposal = this.proposals.get(commitment);
573
-
574
- const signature: ProposalSignature = resolvedScheme === 'ecdsa'
575
- ? { scheme: 'ecdsa', signature: signatureHex, publicKey: publicKey! }
576
- : { scheme: 'falcon', signature: signatureHex };
610
+ const signature = toPsmSignature(resolvedScheme, signatureHex, publicKey);
577
611
 
578
612
  const delta = await this.psm.signDeltaProposal({
579
613
  accountId: this._accountId,
@@ -581,15 +615,25 @@ export class Multisig {
581
615
  signature,
582
616
  });
583
617
 
584
- const proposal = this.deltaToProposal(delta, commitment, undefined, existingProposal?.signatures);
585
-
586
- if (existingProposal?.metadata) {
587
- proposal.metadata = existingProposal.metadata;
618
+ const resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
619
+ if (!resolvedMetadata) {
620
+ throw new Error('Missing proposal metadata');
588
621
  }
622
+ const proposal = deltaToProposal({
623
+ delta,
624
+ proposalId: commitment,
625
+ metadata: resolvedMetadata,
626
+ signaturesRequired: signatureRequirementForProposal(
627
+ resolvedMetadata,
628
+ this.threshold,
629
+ (proposalType) => this.getEffectiveThreshold(proposalType),
630
+ ),
631
+ existingSignatures: existingProposal?.signatures,
632
+ });
589
633
 
590
634
  this.proposals.set(proposal.id, proposal);
591
635
 
592
- return this.syncTransactionProposals();
636
+ return this.listTransactionProposals();
593
637
  }
594
638
 
595
639
  async executeTransactionProposal(commitment: string): Promise<void> {
@@ -598,185 +642,18 @@ export class Multisig {
598
642
  throw new Error(`Proposal not found: ${commitment}`);
599
643
  }
600
644
 
601
- const proposalType = proposal.metadata?.proposalType;
602
- const effectiveThreshold = proposalType
603
- ? this.getEffectiveThreshold(proposalType)
604
- : this.threshold;
605
-
606
- if (proposal.signatures.length < effectiveThreshold) {
607
- throw new Error('Proposal is not ready for execution. Still pending signatures.');
608
- }
609
-
610
- const isSwitchPsm = proposalType === 'switch_psm';
611
-
612
- let txSummaryBase64: string;
613
- let delta: DeltaObject | undefined;
614
-
615
- if (isSwitchPsm) {
616
- txSummaryBase64 = proposal.txSummary;
617
- } else {
618
- const deltas = await this.psm.getDeltaProposals(this._accountId);
619
- delta = deltas.find(
620
- (d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) === commitment
621
- );
622
-
623
- if (!delta) {
624
- throw new Error(`Proposal not found on server: ${commitment}`);
625
- }
626
- txSummaryBase64 = delta.deltaPayload.txSummary.data;
627
- }
628
-
629
- const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
630
- const txSummary = TransactionSummary.deserialize(txSummaryBytes);
631
- const saltHex = txSummary.salt().toHex();
632
- const txCommitmentHex = txSummary.toCommitment().toHex();
633
-
634
- const adviceMap = new AdviceMap();
635
- const normalizedSignerCommitments = new Set(
636
- this.signerCommitments.map((c) => normalizeHexWord(c))
637
- );
638
-
639
- for (const cosignerSig of proposal.signatures) {
640
- let signerCommitmentHex = normalizeHexWord(cosignerSig.signerId);
641
- if (cosignerSig.signature.scheme === 'ecdsa' && cosignerSig.signature.publicKey) {
642
- const derived = tryComputeEcdsaCommitmentHex(cosignerSig.signature.publicKey);
643
- if (derived && derived !== signerCommitmentHex) {
644
- if (!normalizedSignerCommitments.has(derived)) {
645
- throw new Error(
646
- `ECDSA public key commitment mismatch: derived commitment ${derived} is not in signerCommitments.`
647
- );
648
- }
649
- signerCommitmentHex = derived;
650
- }
651
- }
652
- const signerCommitment = Word.fromHex(signerCommitmentHex);
653
- const sigBytes = signatureHexToBytes(
654
- cosignerSig.signature.signature,
655
- cosignerSig.signature.scheme
656
- );
657
- const signature = Signature.deserialize(sigBytes);
658
- const txCommitment = Word.fromHex(normalizeHexWord(txCommitmentHex));
659
-
660
- const isEcdsa = cosignerSig.signature.scheme === 'ecdsa' && cosignerSig.signature.publicKey;
661
- const { key, values } = buildSignatureAdviceEntry(
662
- signerCommitment,
663
- txCommitment,
664
- signature,
665
- isEcdsa ? cosignerSig.signature.publicKey : undefined,
666
- isEcdsa ? cosignerSig.signature.signature : undefined,
667
- );
668
- adviceMap.insert(key, new FeltArray(values));
669
- }
670
-
671
- if (!isSwitchPsm && delta) {
672
- const executionDelta = {
673
- ...delta,
674
- deltaPayload: delta.deltaPayload.txSummary,
675
- };
676
-
677
- const pushResult = await this.psm.pushDelta(executionDelta);
678
- const ackSigHex = pushResult.ackSig;
679
- if (!ackSigHex) {
680
- throw new Error('PSM did not return acknowledgment signature');
681
- }
682
-
683
- const psmAckScheme: 'ecdsa' | 'falcon' = (pushResult.ackScheme as 'ecdsa' | 'falcon') || this.signatureScheme;
684
- const psmAckPubkey = pushResult.ackPubkey || this.psmPublicKey;
685
- const psmCommitmentHex = normalizeHexWord(this.psmCommitment);
686
-
687
- if (psmAckScheme === 'ecdsa' && psmAckPubkey) {
688
- const derived = tryComputeEcdsaCommitmentHex(psmAckPubkey);
689
- if (derived && derived !== psmCommitmentHex) {
690
- throw new Error(`PSM public key commitment mismatch`);
691
- }
692
- }
693
- const psmCommitment = Word.fromHex(psmCommitmentHex);
694
- const ackSigBytes = signatureHexToBytes(ackSigHex, psmAckScheme);
695
- const ackSignature = Signature.deserialize(ackSigBytes);
696
- const txCommitmentForAck = Word.fromHex(normalizeHexWord(txCommitmentHex));
697
- const isAckEcdsa = psmAckScheme === 'ecdsa' && psmAckPubkey;
698
- const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
699
- psmCommitment,
700
- txCommitmentForAck,
701
- ackSignature,
702
- isAckEcdsa ? psmAckPubkey : undefined,
703
- isAckEcdsa ? ackSigHex : undefined,
704
- );
705
- adviceMap.insert(ackKey, new FeltArray(ackValues));
706
- }
707
-
708
- const metadata = proposal.metadata;
709
- if (!metadata) {
710
- throw new Error('Proposal missing metadata');
711
- }
712
-
713
- let finalRequest: TransactionRequest;
714
- switch (metadata.proposalType) {
715
- case 'consume_notes': {
716
- if (!metadata.noteIds || metadata.noteIds.length === 0) {
717
- throw new Error('Proposal missing noteIds. Was it created with createConsumeNotesProposal?');
718
- }
719
- const { request } = buildConsumeNotesTransactionRequest(
720
- metadata.noteIds,
721
- { salt: Word.fromHex(normalizeHexWord(saltHex)), signatureAdviceMap: adviceMap },
722
- );
723
- finalRequest = request;
724
- break;
725
- }
726
- case 'switch_psm': {
727
- if (!metadata.newPsmPubkey) {
728
- throw new Error('Proposal missing newPsmPubkey. Was it created with createSwitchPsmProposal?');
729
- }
730
- const { request } = await buildUpdatePsmTransactionRequest(
731
- this.webClient,
732
- metadata.newPsmPubkey,
733
- {
734
- salt: Word.fromHex(normalizeHexWord(saltHex)),
735
- signatureAdviceMap: adviceMap,
736
- signatureScheme: this.signatureScheme,
737
- },
738
- );
739
- finalRequest = request;
740
- break;
741
- }
742
- case 'p2id': {
743
- if (!metadata.recipientId || !metadata.faucetId || !metadata.amount) {
744
- throw new Error('Proposal missing P2ID metadata (recipientId, faucetId, amount). Was it created with createP2idProposal?');
745
- }
746
- const { request } = buildP2idTransactionRequest(
747
- this._accountId,
748
- metadata.recipientId,
749
- metadata.faucetId,
750
- BigInt(metadata.amount),
751
- { salt: Word.fromHex(normalizeHexWord(saltHex)), signatureAdviceMap: adviceMap },
752
- );
753
- finalRequest = request;
754
- break;
755
- }
756
- case 'unknown': {
757
- throw new Error('Cannot execute proposal with unknown type. The proposal must have been imported without proper metadata.');
758
- }
759
- default: {
760
- const { request } = await buildUpdateSignersTransactionRequest(
761
- this.webClient,
762
- metadata.targetThreshold,
763
- metadata.targetSignerCommitments,
764
- {
765
- salt: Word.fromHex(normalizeHexWord(saltHex)),
766
- signatureAdviceMap: adviceMap,
767
- signatureScheme: this.signatureScheme,
768
- },
769
- );
770
- finalRequest = request;
771
- break;
772
- }
773
- }
774
-
775
- const accountId = AccountId.fromHex(this._accountId);
776
- const result = await this.webClient.executeTransaction(accountId, finalRequest);
777
- const proven = await this.webClient.proveTransaction(result, null);
778
- const submissionHeight = await this.webClient.submitProvenTransaction(proven, result);
779
- await this.webClient.applyTransaction(result, submissionHeight);
645
+ await executeProposalWorkflow({
646
+ proposal,
647
+ accountId: this._accountId,
648
+ threshold: this.threshold,
649
+ signerCommitments: this.signerCommitments,
650
+ psmCommitment: this.psmCommitment,
651
+ psmPublicKey: this.psmPublicKey,
652
+ signatureScheme: this.signatureScheme,
653
+ getEffectiveThreshold: (proposalType) => this.getEffectiveThreshold(proposalType),
654
+ psm: this.psm,
655
+ webClient: this.webClient,
656
+ });
780
657
 
781
658
  proposal.status = { type: 'finalized' };
782
659
  }
@@ -855,7 +732,7 @@ export class Multisig {
855
732
  return { proposal, proposals };
856
733
  }
857
734
 
858
- signTransactionProposalOffline(commitment: string): string {
735
+ async signTransactionProposalOffline(commitment: string): Promise<string> {
859
736
  const proposal = this.proposals.get(commitment);
860
737
  if (!proposal) {
861
738
  throw new Error(`Proposal not found: ${commitment}`);
@@ -868,11 +745,9 @@ export class Multisig {
868
745
  throw new Error('You have already signed this proposal');
869
746
  }
870
747
 
871
- const signatureHex = this.signer.signCommitment(commitment);
748
+ const signatureHex = await this.signer.signCommitment(commitment);
872
749
 
873
- const sigEntry: TransactionProposalSignature['signature'] = this.signer.scheme === 'ecdsa'
874
- ? { scheme: 'ecdsa', signature: signatureHex, publicKey: this.signer.publicKey }
875
- : { scheme: 'falcon', signature: signatureHex };
750
+ const sigEntry = toPsmSignature(this.signer.scheme, signatureHex, this.signer.publicKey);
876
751
  proposal.signatures.push({
877
752
  signerId: this.signer.commitment,
878
753
  signature: sigEntry,
@@ -899,172 +774,16 @@ export class Multisig {
899
774
  return this.exportTransactionProposalToJson(commitment);
900
775
  }
901
776
 
902
- private async syncAfterCreate(proposal: TransactionProposal): Promise<TransactionProposalResult> {
903
- let proposals: TransactionProposal[];
904
- try {
905
- proposals = await this.syncTransactionProposals();
906
- if (!proposals.find((p) => p.id === proposal.id)) {
907
- proposals = [...proposals, proposal];
908
- }
909
- } catch {
910
- proposals = this.listTransactionProposals();
911
- }
912
- return { proposal, proposals };
913
- }
914
-
915
- private deltaToProposal(
916
- delta: DeltaObject,
917
- proposalId: string,
918
- metadata?: ProposalMetadata,
919
- existingSignatures?: TransactionProposalSignature[],
920
- ): TransactionProposal {
921
- const resolvedMetadata: ProposalMetadata | undefined =
922
- metadata ??
923
- (delta.deltaPayload.metadata ? this.fromPsmMetadata(delta.deltaPayload.metadata) : undefined);
924
- if (!resolvedMetadata) {
925
- throw new Error('Missing proposal metadata');
926
- }
927
-
928
- const status = this.deltaStatusToProposalStatus(delta.status, resolvedMetadata.proposalType);
929
-
930
- const signaturesFromStatus =
931
- delta.status.status === 'pending'
932
- ? delta.status.cosignerSigs.map((s) => ({
933
- signerId: s.signerId,
934
- signature: s.signature,
935
- timestamp: s.timestamp,
936
- }))
937
- : [];
938
-
939
- const signaturesMap = new Map<string, TransactionProposalSignature>();
940
- for (const sig of existingSignatures ?? []) {
941
- signaturesMap.set(sig.signerId, sig);
942
- }
943
- for (const sig of signaturesFromStatus) {
944
- signaturesMap.set(sig.signerId, sig);
945
- }
946
- const signatures = Array.from(signaturesMap.values());
947
-
948
- return {
949
- id: proposalId,
950
- commitment: proposalId,
951
- accountId: delta.accountId,
952
- nonce: delta.nonce,
953
- status,
954
- txSummary: delta.deltaPayload.txSummary.data,
955
- signatures,
956
- metadata: resolvedMetadata,
957
- };
958
- }
959
-
960
- private buildPsmMetadata(metadata: ProposalMetadata): PsmProposalMetadata {
961
- const base: PsmProposalMetadata = {
962
- proposalType: metadata.proposalType,
963
- description: metadata.description,
964
- salt: metadata.saltHex,
965
- };
966
-
967
- switch (metadata.proposalType) {
968
- case 'consume_notes':
969
- return {
970
- ...base,
971
- noteIds: metadata.noteIds,
972
- };
973
- case 'p2id':
974
- return {
975
- ...base,
976
- recipientId: metadata.recipientId,
977
- faucetId: metadata.faucetId,
978
- amount: metadata.amount,
979
- };
980
- case 'switch_psm':
981
- return {
982
- ...base,
983
- targetThreshold: metadata.targetThreshold,
984
- signerCommitments: metadata.targetSignerCommitments,
985
- newPsmPubkey: metadata.newPsmPubkey,
986
- newPsmEndpoint: metadata.newPsmEndpoint,
987
- };
988
- case 'add_signer':
989
- case 'remove_signer':
990
- case 'change_threshold':
991
- return {
992
- ...base,
993
- targetThreshold: metadata.targetThreshold,
994
- signerCommitments: metadata.targetSignerCommitments,
995
- };
996
- case 'unknown':
997
- return base;
998
- }
999
- }
1000
-
1001
- private fromPsmMetadata(psm: PsmProposalMetadata): ProposalMetadata | undefined {
1002
- if (!psm.proposalType) return undefined;
1003
- const base = {
1004
- description: psm.description ?? '',
1005
- saltHex: psm.salt,
1006
- };
1007
-
1008
- switch (psm.proposalType) {
1009
- case 'p2id':
1010
- return {
1011
- ...base,
1012
- proposalType: 'p2id',
1013
- recipientId: psm.recipientId ?? '',
1014
- faucetId: psm.faucetId ?? '',
1015
- amount: psm.amount ?? '0',
1016
- };
1017
- case 'consume_notes':
1018
- return {
1019
- ...base,
1020
- proposalType: 'consume_notes',
1021
- noteIds: psm.noteIds ?? [],
1022
- };
1023
- case 'switch_psm':
1024
- return {
1025
- ...base,
1026
- proposalType: 'switch_psm',
1027
- newPsmPubkey: psm.newPsmPubkey ?? '',
1028
- newPsmEndpoint: psm.newPsmEndpoint,
1029
- targetThreshold: psm.targetThreshold,
1030
- targetSignerCommitments: psm.signerCommitments,
1031
- };
1032
- case 'add_signer':
1033
- case 'remove_signer':
1034
- case 'change_threshold':
1035
- return {
1036
- ...base,
1037
- proposalType: psm.proposalType,
1038
- targetThreshold: psm.targetThreshold ?? 0,
1039
- targetSignerCommitments: psm.signerCommitments ?? [],
1040
- };
1041
- default:
1042
- return undefined;
1043
- }
777
+ private async createAndSyncFromRequest(
778
+ request: TransactionRequest,
779
+ metadata: ProposalMetadata,
780
+ nonce?: number,
781
+ ): Promise<TransactionProposalResult> {
782
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
783
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
784
+ const proposalNonce = nonce ?? Date.now();
785
+ const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
786
+ return { proposal, proposals: this.listTransactionProposals() };
1044
787
  }
1045
788
 
1046
- private deltaStatusToProposalStatus(status: DeltaStatus, proposalType?: ProposalType): TransactionProposalStatus {
1047
- switch (status.status) {
1048
- case 'pending': {
1049
- const signaturesCollected = status.cosignerSigs.length;
1050
- const signaturesRequired = proposalType
1051
- ? this.getEffectiveThreshold(proposalType)
1052
- : this.threshold;
1053
- if (signaturesCollected >= signaturesRequired) {
1054
- return { type: 'ready' };
1055
- }
1056
- return {
1057
- type: 'pending',
1058
- signaturesCollected,
1059
- signaturesRequired,
1060
- signers: status.cosignerSigs.map((s) => s.signerId),
1061
- };
1062
- }
1063
- case 'candidate':
1064
- return { type: 'ready' };
1065
- case 'canonical':
1066
- case 'discarded':
1067
- return { type: 'finalized' };
1068
- }
1069
- }
1070
789
  }