@openzeppelin/miden-multisig-client 0.12.6

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 (215) hide show
  1. package/README.md +189 -0
  2. package/dist/account/builder.d.ts +23 -0
  3. package/dist/account/builder.d.ts.map +1 -0
  4. package/dist/account/builder.js +85 -0
  5. package/dist/account/builder.js.map +1 -0
  6. package/dist/account/index.d.ts +6 -0
  7. package/dist/account/index.d.ts.map +1 -0
  8. package/dist/account/index.js +6 -0
  9. package/dist/account/index.js.map +1 -0
  10. package/dist/account/masm.d.ts +3 -0
  11. package/dist/account/masm.d.ts.map +1 -0
  12. package/dist/account/masm.js +605 -0
  13. package/dist/account/masm.js.map +1 -0
  14. package/dist/account/storage.d.ts +10 -0
  15. package/dist/account/storage.d.ts.map +1 -0
  16. package/dist/account/storage.js +56 -0
  17. package/dist/account/storage.js.map +1 -0
  18. package/dist/account.d.ts +104 -0
  19. package/dist/account.d.ts.map +1 -0
  20. package/dist/account.js +136 -0
  21. package/dist/account.js.map +1 -0
  22. package/dist/builder.d.ts +54 -0
  23. package/dist/builder.d.ts.map +1 -0
  24. package/dist/builder.js +78 -0
  25. package/dist/builder.js.map +1 -0
  26. package/dist/client.d.ts +73 -0
  27. package/dist/client.d.ts.map +1 -0
  28. package/dist/client.js +104 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/client.test.d.ts +2 -0
  31. package/dist/client.test.d.ts.map +1 -0
  32. package/dist/client.test.js +150 -0
  33. package/dist/client.test.js.map +1 -0
  34. package/dist/http.d.ts +64 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +162 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/index.d.ts +53 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +52 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/inspector.d.ts +49 -0
  43. package/dist/inspector.d.ts.map +1 -0
  44. package/dist/inspector.js +120 -0
  45. package/dist/inspector.js.map +1 -0
  46. package/dist/inspector.test.d.ts +2 -0
  47. package/dist/inspector.test.d.ts.map +1 -0
  48. package/dist/inspector.test.js +205 -0
  49. package/dist/inspector.test.js.map +1 -0
  50. package/dist/keystore.d.ts +65 -0
  51. package/dist/keystore.d.ts.map +1 -0
  52. package/dist/keystore.js +121 -0
  53. package/dist/keystore.js.map +1 -0
  54. package/dist/miden.d.ts +9 -0
  55. package/dist/miden.d.ts.map +1 -0
  56. package/dist/miden.js +26 -0
  57. package/dist/miden.js.map +1 -0
  58. package/dist/multisig/helpers.d.ts +4 -0
  59. package/dist/multisig/helpers.d.ts.map +1 -0
  60. package/dist/multisig/helpers.js +21 -0
  61. package/dist/multisig/helpers.js.map +1 -0
  62. package/dist/multisig/helpers.test.d.ts +2 -0
  63. package/dist/multisig/helpers.test.d.ts.map +1 -0
  64. package/dist/multisig/helpers.test.js +94 -0
  65. package/dist/multisig/helpers.test.js.map +1 -0
  66. package/dist/multisig/metadata.d.ts +18 -0
  67. package/dist/multisig/metadata.d.ts.map +1 -0
  68. package/dist/multisig/metadata.js +100 -0
  69. package/dist/multisig/metadata.js.map +1 -0
  70. package/dist/multisig/metadata.test.d.ts +2 -0
  71. package/dist/multisig/metadata.test.d.ts.map +1 -0
  72. package/dist/multisig/metadata.test.js +323 -0
  73. package/dist/multisig/metadata.test.js.map +1 -0
  74. package/dist/multisig.d.ts +199 -0
  75. package/dist/multisig.d.ts.map +1 -0
  76. package/dist/multisig.js +821 -0
  77. package/dist/multisig.js.map +1 -0
  78. package/dist/multisig.test.d.ts +2 -0
  79. package/dist/multisig.test.d.ts.map +1 -0
  80. package/dist/multisig.test.js +1121 -0
  81. package/dist/multisig.test.js.map +1 -0
  82. package/dist/procedures.d.ts +57 -0
  83. package/dist/procedures.d.ts.map +1 -0
  84. package/dist/procedures.js +62 -0
  85. package/dist/procedures.js.map +1 -0
  86. package/dist/signer.d.ts +36 -0
  87. package/dist/signer.d.ts.map +1 -0
  88. package/dist/signer.js +68 -0
  89. package/dist/signer.js.map +1 -0
  90. package/dist/signer.test.d.ts +2 -0
  91. package/dist/signer.test.d.ts.map +1 -0
  92. package/dist/signer.test.js +117 -0
  93. package/dist/signer.test.js.map +1 -0
  94. package/dist/transaction/consumeNotes.d.ts +7 -0
  95. package/dist/transaction/consumeNotes.d.ts.map +1 -0
  96. package/dist/transaction/consumeNotes.js +28 -0
  97. package/dist/transaction/consumeNotes.js.map +1 -0
  98. package/dist/transaction/index.d.ts +7 -0
  99. package/dist/transaction/index.d.ts.map +1 -0
  100. package/dist/transaction/index.js +6 -0
  101. package/dist/transaction/index.js.map +1 -0
  102. package/dist/transaction/options.d.ts +6 -0
  103. package/dist/transaction/options.d.ts.map +1 -0
  104. package/dist/transaction/options.js +2 -0
  105. package/dist/transaction/options.js.map +1 -0
  106. package/dist/transaction/p2id.d.ts +7 -0
  107. package/dist/transaction/p2id.d.ts.map +1 -0
  108. package/dist/transaction/p2id.js +43 -0
  109. package/dist/transaction/p2id.js.map +1 -0
  110. package/dist/transaction/summary.d.ts +3 -0
  111. package/dist/transaction/summary.d.ts.map +1 -0
  112. package/dist/transaction/summary.js +6 -0
  113. package/dist/transaction/summary.js.map +1 -0
  114. package/dist/transaction/updatePsm.d.ts +7 -0
  115. package/dist/transaction/updatePsm.d.ts.map +1 -0
  116. package/dist/transaction/updatePsm.js +43 -0
  117. package/dist/transaction/updatePsm.js.map +1 -0
  118. package/dist/transaction/updateSigners.d.ts +8 -0
  119. package/dist/transaction/updateSigners.d.ts.map +1 -0
  120. package/dist/transaction/updateSigners.js +60 -0
  121. package/dist/transaction/updateSigners.js.map +1 -0
  122. package/dist/transaction.d.ts +6 -0
  123. package/dist/transaction.d.ts.map +1 -0
  124. package/dist/transaction.js +8 -0
  125. package/dist/transaction.js.map +1 -0
  126. package/dist/transaction.test.d.ts +2 -0
  127. package/dist/transaction.test.d.ts.map +1 -0
  128. package/dist/transaction.test.js +117 -0
  129. package/dist/transaction.test.js.map +1 -0
  130. package/dist/transport/http.d.ts +64 -0
  131. package/dist/transport/http.d.ts.map +1 -0
  132. package/dist/transport/http.js +162 -0
  133. package/dist/transport/http.js.map +1 -0
  134. package/dist/transport/index.d.ts +5 -0
  135. package/dist/transport/index.d.ts.map +1 -0
  136. package/dist/transport/index.js +5 -0
  137. package/dist/transport/index.js.map +1 -0
  138. package/dist/types/proposal.d.ts +74 -0
  139. package/dist/types/proposal.d.ts.map +1 -0
  140. package/dist/types/proposal.js +2 -0
  141. package/dist/types/proposal.js.map +1 -0
  142. package/dist/types.d.ts +60 -0
  143. package/dist/types.d.ts.map +1 -0
  144. package/dist/types.js +2 -0
  145. package/dist/types.js.map +1 -0
  146. package/dist/utils/encoding.d.ts +7 -0
  147. package/dist/utils/encoding.d.ts.map +1 -0
  148. package/dist/utils/encoding.js +54 -0
  149. package/dist/utils/encoding.js.map +1 -0
  150. package/dist/utils/encoding.test.d.ts +2 -0
  151. package/dist/utils/encoding.test.d.ts.map +1 -0
  152. package/dist/utils/encoding.test.js +185 -0
  153. package/dist/utils/encoding.test.js.map +1 -0
  154. package/dist/utils/index.d.ts +5 -0
  155. package/dist/utils/index.d.ts.map +1 -0
  156. package/dist/utils/index.js +5 -0
  157. package/dist/utils/index.js.map +1 -0
  158. package/dist/utils/random.d.ts +3 -0
  159. package/dist/utils/random.d.ts.map +1 -0
  160. package/dist/utils/random.js +14 -0
  161. package/dist/utils/random.js.map +1 -0
  162. package/dist/utils/signature.d.ts +12 -0
  163. package/dist/utils/signature.d.ts.map +1 -0
  164. package/dist/utils/signature.js +28 -0
  165. package/dist/utils/signature.js.map +1 -0
  166. package/dist/utils/signature.test.d.ts +2 -0
  167. package/dist/utils/signature.test.d.ts.map +1 -0
  168. package/dist/utils/signature.test.js +149 -0
  169. package/dist/utils/signature.test.js.map +1 -0
  170. package/dist/utils/word.d.ts +4 -0
  171. package/dist/utils/word.d.ts.map +1 -0
  172. package/dist/utils/word.js +8 -0
  173. package/dist/utils/word.js.map +1 -0
  174. package/dist/utils/word.test.d.ts +2 -0
  175. package/dist/utils/word.test.d.ts.map +1 -0
  176. package/dist/utils/word.test.js +54 -0
  177. package/dist/utils/word.test.js.map +1 -0
  178. package/masm/multisig.masm +423 -0
  179. package/masm/psm.masm +178 -0
  180. package/package.json +42 -0
  181. package/src/account/builder.ts +112 -0
  182. package/src/account/index.ts +15 -0
  183. package/src/account/masm.ts +606 -0
  184. package/src/account/storage.ts +70 -0
  185. package/src/client.test.ts +177 -0
  186. package/src/client.ts +129 -0
  187. package/src/index.ts +118 -0
  188. package/src/inspector.test.ts +237 -0
  189. package/src/inspector.ts +142 -0
  190. package/src/multisig/helpers.test.ts +114 -0
  191. package/src/multisig/helpers.ts +23 -0
  192. package/src/multisig.test.ts +1294 -0
  193. package/src/multisig.ts +1097 -0
  194. package/src/procedures.ts +72 -0
  195. package/src/signer.test.ts +135 -0
  196. package/src/signer.ts +75 -0
  197. package/src/transaction/consumeNotes.ts +41 -0
  198. package/src/transaction/index.ts +7 -0
  199. package/src/transaction/options.ts +7 -0
  200. package/src/transaction/p2id.ts +105 -0
  201. package/src/transaction/summary.ts +12 -0
  202. package/src/transaction/updatePsm.ts +69 -0
  203. package/src/transaction/updateSigners.ts +96 -0
  204. package/src/transaction.test.ts +136 -0
  205. package/src/transaction.ts +16 -0
  206. package/src/types/proposal.ts +80 -0
  207. package/src/types.ts +81 -0
  208. package/src/utils/encoding.test.ts +233 -0
  209. package/src/utils/encoding.ts +60 -0
  210. package/src/utils/index.ts +5 -0
  211. package/src/utils/random.ts +15 -0
  212. package/src/utils/signature.test.ts +187 -0
  213. package/src/utils/signature.ts +36 -0
  214. package/src/utils/word.test.ts +64 -0
  215. package/src/utils/word.ts +11 -0
@@ -0,0 +1,821 @@
1
+ /**
2
+ * Multisig class representing a created or loaded multisig account.
3
+ *
4
+ * This class wraps a Miden SDK Account and provides PSM integration
5
+ * for proposal management.
6
+ */
7
+ import { Account, AccountId, AdviceMap, FeltArray, Signature, TransactionSummary, Word, } from '@demox-labs/miden-sdk';
8
+ import { executeForSummary, buildUpdateSignersTransactionRequest, buildUpdatePsmTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idTransactionRequest, } from './transaction.js';
9
+ import { base64ToUint8Array, uint8ArrayToBase64, normalizeHexWord, } from './utils/encoding.js';
10
+ import { buildSignatureAdviceEntry, signatureHexToBytes } from './utils/signature.js';
11
+ import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
12
+ /**
13
+ * Represents a multisig account with PSM integration.
14
+ */
15
+ export class Multisig {
16
+ account;
17
+ threshold;
18
+ signerCommitments;
19
+ psmCommitment;
20
+ procedureThresholds;
21
+ psm;
22
+ signer;
23
+ webClient;
24
+ _accountId;
25
+ proposals = new Map();
26
+ constructor(account, config, psm, signer, webClient, accountId) {
27
+ this.account = account;
28
+ this.threshold = config.threshold;
29
+ this.signerCommitments = config.signerCommitments;
30
+ this.psmCommitment = config.psmCommitment;
31
+ this.procedureThresholds = new Map((config.procedureThresholds ?? []).map((pt) => [pt.procedure, pt.threshold]));
32
+ this.psm = psm;
33
+ this.signer = signer;
34
+ this.webClient = webClient;
35
+ this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
36
+ }
37
+ /** The account ID as a string */
38
+ get accountId() {
39
+ return this._accountId;
40
+ }
41
+ /** The signer's commitment */
42
+ get signerCommitment() {
43
+ return this.signer.commitment;
44
+ }
45
+ /**
46
+ * Maps a proposal type to the procedure that determines its threshold.
47
+ */
48
+ getProposalProcedure(proposalType) {
49
+ switch (proposalType) {
50
+ case 'p2id':
51
+ return 'send_asset';
52
+ case 'consume_notes':
53
+ return 'receive_asset';
54
+ case 'add_signer':
55
+ case 'remove_signer':
56
+ case 'change_threshold':
57
+ return 'update_signers';
58
+ case 'switch_psm':
59
+ return 'update_psm';
60
+ default:
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Get the effective threshold for a given proposal type.
66
+ * Returns the procedure-specific threshold if configured, otherwise the default threshold.
67
+ *
68
+ * @param proposalType - The type of proposal
69
+ * @returns The threshold that applies to this proposal type
70
+ */
71
+ getEffectiveThreshold(proposalType) {
72
+ if (this.procedureThresholds.size === 0) {
73
+ return this.threshold;
74
+ }
75
+ const procedure = this.getProposalProcedure(proposalType);
76
+ if (!procedure) {
77
+ return this.threshold;
78
+ }
79
+ return this.procedureThresholds.get(procedure) ?? this.threshold;
80
+ }
81
+ /**
82
+ * Update the PSM client used by this Multisig instance.
83
+ *
84
+ * @param psmClient - The new PSM HTTP client
85
+ */
86
+ setPsmClient(psmClient) {
87
+ this.psm = psmClient;
88
+ this.psm.setSigner(this.signer);
89
+ }
90
+ /**
91
+ * Fetch the current account state from PSM.
92
+ *
93
+ * @returns The account state including commitment and serialized data
94
+ */
95
+ async fetchState() {
96
+ const state = await this.psm.getState(this._accountId);
97
+ return {
98
+ accountId: state.accountId,
99
+ commitment: state.commitment,
100
+ stateDataBase64: state.stateJson.data,
101
+ createdAt: state.createdAt,
102
+ updatedAt: state.updatedAt,
103
+ };
104
+ }
105
+ /**
106
+ * Sync account state from PSM into the local WebClient store.
107
+ *
108
+ * If the PSM commitment differs from the local commitment (or the account
109
+ * is missing locally), the local store is overwritten with the PSM state.
110
+ */
111
+ async syncState() {
112
+ const state = await this.fetchState();
113
+ const accountId = AccountId.fromHex(this._accountId);
114
+ const localAccount = await this.webClient.getAccount(accountId);
115
+ const psmCommitment = normalizeHexWord(state.commitment);
116
+ const localCommitment = localAccount
117
+ ? normalizeHexWord(localAccount.commitment().toHex())
118
+ : null;
119
+ if (!localAccount || localCommitment !== psmCommitment) {
120
+ const accountBytes = base64ToUint8Array(state.stateDataBase64);
121
+ const account = Account.deserialize(accountBytes);
122
+ await this.webClient.newAccount(account, true);
123
+ }
124
+ return state;
125
+ }
126
+ /**
127
+ * Register this multisig account on the PSM server.
128
+ *
129
+ * The initial state must be the serialized Account bytes (base64-encoded).
130
+ * If not provided, the account's serialize() method is used.
131
+ *
132
+ * @param initialStateBase64 - Optional base64-encoded serialized Account.¡
133
+ */
134
+ async registerOnPsm(initialStateBase64) {
135
+ if (!this.account && !initialStateBase64) {
136
+ throw new Error('Cannot register on PSM: no account available and no initial state provided');
137
+ }
138
+ // Serialize the account to bytes and base64-encode
139
+ let stateData;
140
+ if (initialStateBase64) {
141
+ stateData = initialStateBase64;
142
+ }
143
+ else {
144
+ const accountBytes = this.account.serialize();
145
+ stateData = uint8ArrayToBase64(accountBytes);
146
+ }
147
+ const auth = {
148
+ MidenFalconRpo: {
149
+ cosigner_commitments: this.signerCommitments,
150
+ },
151
+ };
152
+ const response = await this.psm.configure({
153
+ accountId: this._accountId,
154
+ auth,
155
+ initialState: { data: stateData, accountId: this._accountId },
156
+ });
157
+ if (!response.success) {
158
+ throw new Error(`Failed to register on PSM: ${response.message}`);
159
+ }
160
+ }
161
+ /**
162
+ * Sync proposals from the PSM server.
163
+ */
164
+ async syncProposals() {
165
+ const deltas = await this.psm.getDeltaProposals(this._accountId);
166
+ for (const delta of deltas) {
167
+ const proposalId = computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data);
168
+ const existingProposal = this.proposals.get(proposalId);
169
+ const resolvedMetadata = existingProposal?.metadata ??
170
+ (delta.deltaPayload.metadata
171
+ ? this.fromPsmMetadata(delta.deltaPayload.metadata)
172
+ : undefined);
173
+ if (!resolvedMetadata) {
174
+ throw new Error('Missing proposal metadata from PSM');
175
+ }
176
+ const proposal = this.deltaToProposal(delta, proposalId, resolvedMetadata, existingProposal?.signatures);
177
+ this.proposals.set(proposal.id, proposal);
178
+ }
179
+ return Array.from(this.proposals.values());
180
+ }
181
+ /**
182
+ * List all known proposals
183
+ */
184
+ listProposals() {
185
+ return Array.from(this.proposals.values());
186
+ }
187
+ /**
188
+ * Create a new proposal.
189
+ *
190
+ * @param nonce - The nonce for this transaction
191
+ * @param txSummaryBase64 - Base64-encoded transaction summary
192
+ * @param metadata - Optional metadata for execution (target config, salt, etc.)
193
+ */
194
+ async createProposal(nonce, txSummaryBase64, metadata) {
195
+ const psmMetadata = this.buildPsmMetadata(metadata);
196
+ const response = await this.psm.pushDeltaProposal({
197
+ accountId: this._accountId,
198
+ nonce,
199
+ deltaPayload: {
200
+ txSummary: { data: txSummaryBase64 },
201
+ signatures: [],
202
+ metadata: psmMetadata,
203
+ },
204
+ });
205
+ const proposal = this.deltaToProposal(response.delta, response.commitment, metadata);
206
+ this.proposals.set(proposal.id, proposal);
207
+ return proposal;
208
+ }
209
+ /**
210
+ * Create an "add signer" proposal.
211
+ *
212
+ * @param newCommitment - Commitment of the new signer (hex)
213
+ * @param nonce - Optional proposal nonce (defaults to Date.now())
214
+ * @param newThreshold - Optional new threshold (defaults to current threshold)
215
+ */
216
+ async createAddSignerProposal(newCommitment, nonce, newThreshold) {
217
+ const targetThreshold = newThreshold ?? this.threshold;
218
+ const targetSignerCommitments = [...this.signerCommitments, newCommitment];
219
+ const { request, salt } = await buildUpdateSignersTransactionRequest(this.webClient, targetThreshold, targetSignerCommitments);
220
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
221
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
222
+ const proposalNonce = nonce ?? Date.now();
223
+ const metadata = {
224
+ proposalType: 'add_signer',
225
+ targetThreshold,
226
+ targetSignerCommitments,
227
+ saltHex: salt.toHex(),
228
+ description: `Add signer ${newCommitment.slice(0, 10)}...`,
229
+ };
230
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
231
+ }
232
+ /**
233
+ * Create a "remove signer" proposal by executing the update_signers script to summary.
234
+ *
235
+ * @param signerToRemove - Commitment of the signer to remove (hex)
236
+ * @param nonce - Optional proposal nonce (defaults to Date.now())
237
+ * @param newThreshold - Optional new threshold (defaults to min of current threshold and new signer count)
238
+ */
239
+ async createRemoveSignerProposal(signerToRemove, nonce, newThreshold) {
240
+ const normalizedRemove = signerToRemove.toLowerCase();
241
+ const signerExists = this.signerCommitments.some((c) => c.toLowerCase() === normalizedRemove);
242
+ if (!signerExists) {
243
+ throw new Error(`Signer ${signerToRemove} is not in the current signer list`);
244
+ }
245
+ const targetSignerCommitments = this.signerCommitments.filter((c) => c.toLowerCase() !== normalizedRemove);
246
+ if (targetSignerCommitments.length === 0) {
247
+ throw new Error('Cannot remove the last signer');
248
+ }
249
+ const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
250
+ if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
251
+ throw new Error(`Invalid threshold ${targetThreshold}. Must be between 1 and ${targetSignerCommitments.length}`);
252
+ }
253
+ const { request, salt } = await buildUpdateSignersTransactionRequest(this.webClient, targetThreshold, targetSignerCommitments);
254
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
255
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
256
+ const proposalNonce = nonce ?? Date.now();
257
+ const metadata = {
258
+ proposalType: 'remove_signer',
259
+ targetThreshold,
260
+ targetSignerCommitments,
261
+ saltHex: salt.toHex(),
262
+ description: `Remove signer ${signerToRemove.slice(0, 10)}...`,
263
+ };
264
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
265
+ }
266
+ /**
267
+ * Create a "change threshold" proposal.
268
+ *
269
+ * @param newThreshold - The new threshold value
270
+ * @param nonce - Optional proposal nonce (defaults to Date.now())
271
+ */
272
+ async createChangeThresholdProposal(newThreshold, nonce) {
273
+ if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
274
+ throw new Error(`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`);
275
+ }
276
+ if (newThreshold === this.threshold) {
277
+ throw new Error('New threshold is the same as current threshold');
278
+ }
279
+ const { request, salt } = await buildUpdateSignersTransactionRequest(this.webClient, newThreshold, this.signerCommitments);
280
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
281
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
282
+ const proposalNonce = nonce ?? Date.now();
283
+ const metadata = {
284
+ proposalType: 'change_threshold',
285
+ targetThreshold: newThreshold,
286
+ targetSignerCommitments: this.signerCommitments,
287
+ saltHex: salt.toHex(),
288
+ description: `Change threshold from ${this.threshold} to ${newThreshold}`,
289
+ };
290
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
291
+ }
292
+ /**
293
+ * Create a "switch PSM" proposal to change the PSM provider.
294
+ *
295
+ * @param newPsmEndpoint - The new PSM server endpoint URL
296
+ * @param newPsmPubkey - The new PSM server's public key commitment (hex)
297
+ * @param nonce - Optional proposal nonce (defaults to Date.now())
298
+ */
299
+ async createSwitchPsmProposal(newPsmEndpoint, newPsmPubkey, nonce) {
300
+ const { request, salt } = await buildUpdatePsmTransactionRequest(this.webClient, newPsmPubkey);
301
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
302
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
303
+ const proposalNonce = nonce ?? Date.now();
304
+ const metadata = {
305
+ proposalType: 'switch_psm',
306
+ saltHex: salt.toHex(),
307
+ newPsmPubkey,
308
+ newPsmEndpoint,
309
+ description: `Switch PSM to ${newPsmEndpoint}`,
310
+ };
311
+ const proposalId = computeCommitmentFromTxSummary(summaryBase64);
312
+ const proposal = {
313
+ id: proposalId,
314
+ accountId: this._accountId,
315
+ nonce: proposalNonce,
316
+ status: { type: 'pending', signaturesCollected: 0, signaturesRequired: this.threshold, signers: [] },
317
+ txSummary: summaryBase64,
318
+ signatures: [],
319
+ metadata,
320
+ };
321
+ this.proposals.set(proposal.id, proposal);
322
+ return proposal;
323
+ }
324
+ /**
325
+ * Create a "consume notes" proposal to consume notes sent to the multisig account.
326
+ *
327
+ * @param noteIds - IDs of the notes to consume (hex strings)
328
+ * @param nonce - Optional proposal nonce (defaults to Date.now())
329
+ */
330
+ async createConsumeNotesProposal(noteIds, nonce) {
331
+ if (noteIds.length === 0) {
332
+ throw new Error('At least one note ID is required');
333
+ }
334
+ const { request, salt } = buildConsumeNotesTransactionRequest(noteIds);
335
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
336
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
337
+ const proposalNonce = nonce ?? Date.now();
338
+ const metadata = {
339
+ proposalType: 'consume_notes',
340
+ noteIds,
341
+ saltHex: salt.toHex(),
342
+ description: `Consume ${noteIds.length} note(s)`,
343
+ };
344
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
345
+ }
346
+ /**
347
+ * Create a P2ID proposal to send funds to another account.
348
+ *
349
+ * @param recipientId - Account ID of the recipient (hex string)
350
+ * @param faucetId - Faucet/token account ID (hex string)
351
+ * @param amount - Amount to send
352
+ * @param nonce - Optional proposal nonce (defaults to Date.now())
353
+ */
354
+ async createP2idProposal(recipientId, faucetId, amount, nonce) {
355
+ if (amount <= 0n) {
356
+ throw new Error('Amount must be greater than 0');
357
+ }
358
+ const { request, salt } = buildP2idTransactionRequest(this._accountId, recipientId, faucetId, amount);
359
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
360
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
361
+ const proposalNonce = nonce ?? Date.now();
362
+ const metadata = {
363
+ proposalType: 'p2id',
364
+ saltHex: salt.toHex(),
365
+ recipientId,
366
+ faucetId,
367
+ amount: amount.toString(),
368
+ description: `Send ${amount} to ${recipientId.slice(0, 10)}...`,
369
+ };
370
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
371
+ }
372
+ /**
373
+ * Get notes that can be consumed by this multisig account.
374
+ *
375
+ * Returns a list of notes that are committed on-chain and can be consumed
376
+ * immediately by the multisig account.
377
+ */
378
+ async getConsumableNotes() {
379
+ const accountId = AccountId.fromHex(this._accountId);
380
+ // Get consumable notes for this account
381
+ const consumableRecords = await this.webClient.getConsumableNotes(accountId);
382
+ // Convert to our simplified ConsumableNote type
383
+ const notes = [];
384
+ for (const record of consumableRecords) {
385
+ const inputNote = record.inputNoteRecord();
386
+ const consumability = record.noteConsumability();
387
+ // Only include notes that can be consumed now (consumableAfterBlock is undefined/null)
388
+ const canConsumeNow = consumability.some((c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
389
+ c.consumableAfterBlock() === undefined);
390
+ if (canConsumeNow) {
391
+ const noteId = inputNote.id().toString();
392
+ const details = inputNote.details();
393
+ const fungibleAssets = details.assets().fungibleAssets();
394
+ // Extract assets
395
+ const assets = [];
396
+ for (const asset of fungibleAssets) {
397
+ assets.push({
398
+ faucetId: asset.faucetId().toString(),
399
+ amount: asset.amount(),
400
+ });
401
+ }
402
+ notes.push({ id: noteId, assets });
403
+ }
404
+ }
405
+ return notes;
406
+ }
407
+ /**
408
+ * Sign a proposal.
409
+ *
410
+ * The proposalId is the tx_summary commitment hex, which is what gets signed.
411
+ * This matches the Rust client behavior where proposal.id == tx_summary.to_commitment().
412
+ *
413
+ * @param proposalId - The proposal commitment/ID (this is also what gets signed)
414
+ */
415
+ async signProposal(proposalId) {
416
+ const existingProposal = this.proposals.get(proposalId);
417
+ const signatureHex = this.signer.signCommitment(proposalId);
418
+ const signature = {
419
+ scheme: 'falcon',
420
+ signature: signatureHex,
421
+ };
422
+ const delta = await this.psm.signDeltaProposal({
423
+ accountId: this._accountId,
424
+ commitment: proposalId,
425
+ signature,
426
+ });
427
+ const proposal = this.deltaToProposal(delta, proposalId, undefined, existingProposal?.signatures);
428
+ if (existingProposal?.metadata) {
429
+ proposal.metadata = existingProposal.metadata;
430
+ }
431
+ this.proposals.set(proposal.id, proposal);
432
+ return proposal;
433
+ }
434
+ /**
435
+ * Execute a proposal that has enough signatures.
436
+ *
437
+ * @param proposalId - The proposal commitment/ID
438
+ */
439
+ async executeProposal(proposalId) {
440
+ const proposal = this.proposals.get(proposalId);
441
+ if (!proposal) {
442
+ throw new Error(`Proposal not found: ${proposalId}`);
443
+ }
444
+ const proposalType = proposal.metadata?.proposalType;
445
+ const effectiveThreshold = proposalType
446
+ ? this.getEffectiveThreshold(proposalType)
447
+ : this.threshold;
448
+ if (proposal.signatures.length < effectiveThreshold) {
449
+ throw new Error('Proposal is not ready for execution. Still pending signatures.');
450
+ }
451
+ const isSwitchPsm = proposalType === 'switch_psm';
452
+ let txSummaryBase64;
453
+ let delta;
454
+ if (isSwitchPsm) {
455
+ txSummaryBase64 = proposal.txSummary;
456
+ }
457
+ else {
458
+ const deltas = await this.psm.getDeltaProposals(this._accountId);
459
+ delta = deltas.find((d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) === proposalId);
460
+ if (!delta) {
461
+ throw new Error(`Proposal not found on server: ${proposalId}`);
462
+ }
463
+ txSummaryBase64 = delta.deltaPayload.txSummary.data;
464
+ }
465
+ const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
466
+ const txSummary = TransactionSummary.deserialize(txSummaryBytes);
467
+ const saltHex = txSummary.salt().toHex();
468
+ const txCommitmentHex = txSummary.toCommitment().toHex();
469
+ const adviceMap = new AdviceMap();
470
+ for (const cosignerSig of proposal.signatures) {
471
+ const signerCommitment = Word.fromHex(normalizeHexWord(cosignerSig.signerId));
472
+ const sigBytes = signatureHexToBytes(cosignerSig.signature.signature);
473
+ const signature = Signature.deserialize(sigBytes);
474
+ const txCommitment = Word.fromHex(normalizeHexWord(txCommitmentHex));
475
+ const { key, values } = buildSignatureAdviceEntry(signerCommitment, txCommitment, signature);
476
+ adviceMap.insert(key, new FeltArray(values));
477
+ }
478
+ if (!isSwitchPsm && delta) {
479
+ const executionDelta = {
480
+ ...delta,
481
+ deltaPayload: delta.deltaPayload.txSummary,
482
+ };
483
+ const pushResult = await this.psm.pushDelta(executionDelta);
484
+ const ackSigHex = pushResult.ackSig;
485
+ if (!ackSigHex) {
486
+ throw new Error('PSM did not return acknowledgment signature');
487
+ }
488
+ const psmCommitment = Word.fromHex(normalizeHexWord(this.psmCommitment));
489
+ const ackSigBytes = signatureHexToBytes(ackSigHex);
490
+ const ackSignature = Signature.deserialize(ackSigBytes);
491
+ const txCommitmentForAck = Word.fromHex(normalizeHexWord(txCommitmentHex));
492
+ const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(psmCommitment, txCommitmentForAck, ackSignature);
493
+ adviceMap.insert(ackKey, new FeltArray(ackValues));
494
+ }
495
+ const metadata = proposal.metadata;
496
+ if (!metadata) {
497
+ throw new Error('Proposal missing metadata');
498
+ }
499
+ let finalRequest;
500
+ switch (metadata.proposalType) {
501
+ case 'consume_notes': {
502
+ if (!metadata.noteIds || metadata.noteIds.length === 0) {
503
+ throw new Error('Proposal missing noteIds. Was it created with createConsumeNotesProposal?');
504
+ }
505
+ const { request } = buildConsumeNotesTransactionRequest(metadata.noteIds, { salt: Word.fromHex(normalizeHexWord(saltHex)), signatureAdviceMap: adviceMap });
506
+ finalRequest = request;
507
+ break;
508
+ }
509
+ case 'switch_psm': {
510
+ if (!metadata.newPsmPubkey) {
511
+ throw new Error('Proposal missing newPsmPubkey. Was it created with createSwitchPsmProposal?');
512
+ }
513
+ const { request } = await buildUpdatePsmTransactionRequest(this.webClient, metadata.newPsmPubkey, { salt: Word.fromHex(normalizeHexWord(saltHex)), signatureAdviceMap: adviceMap });
514
+ finalRequest = request;
515
+ break;
516
+ }
517
+ case 'p2id': {
518
+ if (!metadata.recipientId || !metadata.faucetId || !metadata.amount) {
519
+ throw new Error('Proposal missing P2ID metadata (recipientId, faucetId, amount). Was it created with createP2idProposal?');
520
+ }
521
+ const { request } = buildP2idTransactionRequest(this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), { salt: Word.fromHex(normalizeHexWord(saltHex)), signatureAdviceMap: adviceMap });
522
+ finalRequest = request;
523
+ break;
524
+ }
525
+ case 'unknown': {
526
+ throw new Error('Cannot execute proposal with unknown type. The proposal must have been imported without proper metadata.');
527
+ }
528
+ default: {
529
+ const { request } = await buildUpdateSignersTransactionRequest(this.webClient, metadata.targetThreshold, metadata.targetSignerCommitments, { salt: Word.fromHex(normalizeHexWord(saltHex)), signatureAdviceMap: adviceMap });
530
+ finalRequest = request;
531
+ break;
532
+ }
533
+ }
534
+ const accountId = AccountId.fromHex(this._accountId);
535
+ const result = await this.webClient.executeTransaction(accountId, finalRequest);
536
+ const proven = await this.webClient.proveTransaction(result, null);
537
+ const submissionHeight = await this.webClient.submitProvenTransaction(proven, result);
538
+ await this.webClient.applyTransaction(result, submissionHeight);
539
+ proposal.status = { type: 'finalized' };
540
+ }
541
+ /**
542
+ * Export a proposal for offline signing
543
+ */
544
+ async exportProposal(proposalId) {
545
+ const deltas = await this.psm.getDeltaProposals(this._accountId);
546
+ const delta = deltas.find((d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) === proposalId);
547
+ if (!delta) {
548
+ throw new Error(`Proposal not found: ${proposalId}`);
549
+ }
550
+ const signatures = delta.status.status === 'pending'
551
+ ? delta.status.cosignerSigs.map((s) => ({
552
+ commitment: s.signerId,
553
+ signatureHex: s.signature.signature,
554
+ }))
555
+ : [];
556
+ return {
557
+ accountId: delta.accountId,
558
+ nonce: delta.nonce,
559
+ commitment: proposalId,
560
+ txSummaryBase64: delta.deltaPayload.txSummary.data,
561
+ signatures,
562
+ };
563
+ }
564
+ /**
565
+ * Export a proposal to JSON for side-channel sharing.
566
+ *
567
+ * @param proposalId - The proposal commitment/ID
568
+ * @returns JSON string that can be shared and imported by other signers
569
+ */
570
+ exportProposalToJson(proposalId) {
571
+ const proposal = this.proposals.get(proposalId);
572
+ if (!proposal) {
573
+ throw new Error(`Proposal not found in local cache: ${proposalId}`);
574
+ }
575
+ const exported = {
576
+ accountId: proposal.accountId,
577
+ nonce: proposal.nonce,
578
+ commitment: proposal.id,
579
+ txSummaryBase64: proposal.txSummary,
580
+ signatures: proposal.signatures.map((s) => ({
581
+ commitment: s.signerId,
582
+ signatureHex: s.signature.signature,
583
+ timestamp: s.timestamp,
584
+ })),
585
+ metadata: proposal.metadata,
586
+ };
587
+ return JSON.stringify(exported, null, 2);
588
+ }
589
+ /**
590
+ * Import a proposal from JSON (exported via exportProposalToJson).
591
+ *
592
+ * @param json - JSON string from exportProposalToJson
593
+ * @returns The imported proposal
594
+ */
595
+ importProposal(json) {
596
+ const exported = JSON.parse(json);
597
+ if (!exported.accountId || !exported.txSummaryBase64 || !exported.commitment) {
598
+ throw new Error('Invalid proposal JSON: missing required fields');
599
+ }
600
+ if (exported.accountId.toLowerCase() !== this._accountId.toLowerCase()) {
601
+ throw new Error(`Proposal is for a different account: ${exported.accountId}`);
602
+ }
603
+ const computedCommitment = computeCommitmentFromTxSummary(exported.txSummaryBase64);
604
+ if (computedCommitment !== exported.commitment) {
605
+ throw new Error('Invalid proposal: commitment does not match tx_summary');
606
+ }
607
+ const metadata = exported.metadata ?? {
608
+ proposalType: 'unknown',
609
+ description: '',
610
+ };
611
+ const signaturesCollected = exported.signatures.length;
612
+ const signaturesRequired = this.getEffectiveThreshold(metadata.proposalType);
613
+ const status = signaturesCollected >= signaturesRequired
614
+ ? { type: 'ready' }
615
+ : {
616
+ type: 'pending',
617
+ signaturesCollected,
618
+ signaturesRequired,
619
+ signers: exported.signatures.map((s) => s.commitment),
620
+ };
621
+ const proposal = {
622
+ id: exported.commitment,
623
+ accountId: exported.accountId,
624
+ nonce: exported.nonce,
625
+ status,
626
+ txSummary: exported.txSummaryBase64,
627
+ signatures: exported.signatures.map((s) => ({
628
+ signerId: s.commitment,
629
+ signature: { scheme: 'falcon', signature: s.signatureHex },
630
+ timestamp: s.timestamp || new Date().toISOString(),
631
+ })),
632
+ metadata,
633
+ };
634
+ this.proposals.set(proposal.id, proposal);
635
+ return proposal;
636
+ }
637
+ /**
638
+ * Sign an imported proposal and return updated JSON for sharing..
639
+ *
640
+ * @param proposalId - The proposal commitment/ID
641
+ * @returns Updated JSON string with the new signature included
642
+ */
643
+ signProposalOffline(proposalId) {
644
+ const proposal = this.proposals.get(proposalId);
645
+ if (!proposal) {
646
+ throw new Error(`Proposal not found: ${proposalId}`);
647
+ }
648
+ // Check if already signed
649
+ const alreadySigned = proposal.signatures.some((s) => s.signerId.toLowerCase() === this.signer.commitment.toLowerCase());
650
+ if (alreadySigned) {
651
+ throw new Error('You have already signed this proposal');
652
+ }
653
+ // Sign the commitment
654
+ const signatureHex = this.signer.signCommitment(proposalId);
655
+ // Add signature to local proposal
656
+ proposal.signatures.push({
657
+ signerId: this.signer.commitment,
658
+ signature: { scheme: 'falcon', signature: signatureHex },
659
+ timestamp: new Date().toISOString(),
660
+ });
661
+ // Update status
662
+ const signaturesCollected = proposal.signatures.length;
663
+ const proposalType = proposal.metadata?.proposalType;
664
+ const effectiveThreshold = proposalType
665
+ ? this.getEffectiveThreshold(proposalType)
666
+ : this.threshold;
667
+ if (signaturesCollected >= effectiveThreshold) {
668
+ proposal.status = { type: 'ready' };
669
+ }
670
+ else if (proposal.status.type === 'pending') {
671
+ proposal.status = {
672
+ type: 'pending',
673
+ signaturesCollected,
674
+ signaturesRequired: effectiveThreshold,
675
+ signers: proposal.signatures.map((s) => s.signerId),
676
+ };
677
+ }
678
+ // Return updated JSON
679
+ return this.exportProposalToJson(proposalId);
680
+ }
681
+ deltaToProposal(delta, proposalId, metadata, existingSignatures) {
682
+ const resolvedMetadata = metadata ??
683
+ (delta.deltaPayload.metadata ? this.fromPsmMetadata(delta.deltaPayload.metadata) : undefined);
684
+ if (!resolvedMetadata) {
685
+ throw new Error('Missing proposal metadata');
686
+ }
687
+ const status = this.deltaStatusToProposalStatus(delta.status, resolvedMetadata.proposalType);
688
+ const signaturesFromStatus = delta.status.status === 'pending'
689
+ ? delta.status.cosignerSigs.map((s) => ({
690
+ signerId: s.signerId,
691
+ signature: s.signature,
692
+ timestamp: s.timestamp,
693
+ }))
694
+ : [];
695
+ const signaturesMap = new Map();
696
+ for (const sig of existingSignatures ?? []) {
697
+ signaturesMap.set(sig.signerId, sig);
698
+ }
699
+ for (const sig of signaturesFromStatus) {
700
+ signaturesMap.set(sig.signerId, sig);
701
+ }
702
+ const signatures = Array.from(signaturesMap.values());
703
+ return {
704
+ id: proposalId,
705
+ accountId: delta.accountId,
706
+ nonce: delta.nonce,
707
+ status,
708
+ txSummary: delta.deltaPayload.txSummary.data,
709
+ signatures,
710
+ metadata: resolvedMetadata,
711
+ };
712
+ }
713
+ buildPsmMetadata(metadata) {
714
+ const base = {
715
+ proposalType: metadata.proposalType,
716
+ description: metadata.description,
717
+ salt: metadata.saltHex,
718
+ };
719
+ switch (metadata.proposalType) {
720
+ case 'consume_notes':
721
+ return {
722
+ ...base,
723
+ noteIds: metadata.noteIds,
724
+ };
725
+ case 'p2id':
726
+ return {
727
+ ...base,
728
+ recipientId: metadata.recipientId,
729
+ faucetId: metadata.faucetId,
730
+ amount: metadata.amount,
731
+ };
732
+ case 'switch_psm':
733
+ return {
734
+ ...base,
735
+ targetThreshold: metadata.targetThreshold,
736
+ signerCommitments: metadata.targetSignerCommitments,
737
+ newPsmPubkey: metadata.newPsmPubkey,
738
+ newPsmEndpoint: metadata.newPsmEndpoint,
739
+ };
740
+ case 'add_signer':
741
+ case 'remove_signer':
742
+ case 'change_threshold':
743
+ return {
744
+ ...base,
745
+ targetThreshold: metadata.targetThreshold,
746
+ signerCommitments: metadata.targetSignerCommitments,
747
+ };
748
+ case 'unknown':
749
+ return base;
750
+ }
751
+ }
752
+ fromPsmMetadata(psm) {
753
+ if (!psm.proposalType)
754
+ return undefined;
755
+ const base = {
756
+ description: psm.description ?? '',
757
+ saltHex: psm.salt,
758
+ };
759
+ switch (psm.proposalType) {
760
+ case 'p2id':
761
+ return {
762
+ ...base,
763
+ proposalType: 'p2id',
764
+ recipientId: psm.recipientId ?? '',
765
+ faucetId: psm.faucetId ?? '',
766
+ amount: psm.amount ?? '0',
767
+ };
768
+ case 'consume_notes':
769
+ return {
770
+ ...base,
771
+ proposalType: 'consume_notes',
772
+ noteIds: psm.noteIds ?? [],
773
+ };
774
+ case 'switch_psm':
775
+ return {
776
+ ...base,
777
+ proposalType: 'switch_psm',
778
+ newPsmPubkey: psm.newPsmPubkey ?? '',
779
+ newPsmEndpoint: psm.newPsmEndpoint,
780
+ targetThreshold: psm.targetThreshold,
781
+ targetSignerCommitments: psm.signerCommitments,
782
+ };
783
+ case 'add_signer':
784
+ case 'remove_signer':
785
+ case 'change_threshold':
786
+ return {
787
+ ...base,
788
+ proposalType: psm.proposalType,
789
+ targetThreshold: psm.targetThreshold ?? 0,
790
+ targetSignerCommitments: psm.signerCommitments ?? [],
791
+ };
792
+ default:
793
+ return undefined;
794
+ }
795
+ }
796
+ deltaStatusToProposalStatus(status, proposalType) {
797
+ switch (status.status) {
798
+ case 'pending': {
799
+ const signaturesCollected = status.cosignerSigs.length;
800
+ const signaturesRequired = proposalType
801
+ ? this.getEffectiveThreshold(proposalType)
802
+ : this.threshold;
803
+ if (signaturesCollected >= signaturesRequired) {
804
+ return { type: 'ready' };
805
+ }
806
+ return {
807
+ type: 'pending',
808
+ signaturesCollected,
809
+ signaturesRequired,
810
+ signers: status.cosignerSigs.map((s) => s.signerId),
811
+ };
812
+ }
813
+ case 'candidate':
814
+ return { type: 'ready' };
815
+ case 'canonical':
816
+ case 'discarded':
817
+ return { type: 'finalized' };
818
+ }
819
+ }
820
+ }
821
+ //# sourceMappingURL=multisig.js.map