@openzeppelin/miden-multisig-client 0.14.8 → 0.14.9-rc.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.
@@ -1,11 +1,21 @@
1
- import type {
2
- ProposalSignature,
3
- ProposalType as GuardianProposalType,
4
- SignatureScheme,
5
- } from '@openzeppelin/guardian-client';
1
+ import type { ProposalSignature, SignatureScheme } from '@openzeppelin/guardian-client';
6
2
  import type { ProcedureName } from '../procedures.js';
7
3
 
8
- export type ProposalType = Exclude<GuardianProposalType, 'custom'>;
4
+ /**
5
+ * Closed set of proposal types the multisig SDK models behaviorally, plus the
6
+ * `'custom'` bucket for any server-defined type the SDK does not model (issue
7
+ * #266). Defined explicitly (not derived from the now-arbitrary guardian-client
8
+ * wire union) so the exhaustive switches in the metadata codec stay sound.
9
+ */
10
+ export type ProposalType =
11
+ | 'add_signer'
12
+ | 'remove_signer'
13
+ | 'change_threshold'
14
+ | 'update_procedure_threshold'
15
+ | 'switch_guardian'
16
+ | 'consume_notes'
17
+ | 'p2id'
18
+ | 'custom';
9
19
 
10
20
  export type ProposalStatus = 'pending' | 'ready' | 'finalized';
11
21
 
@@ -79,8 +89,13 @@ export interface P2IdProposalMetadata extends BaseProposalMetadata {
79
89
  amount: string;
80
90
  }
81
91
 
82
- export interface UnknownProposalMetadata extends BaseProposalMetadata {
83
- proposalType: 'unknown';
92
+ export interface CustomProposalMetadata extends BaseProposalMetadata {
93
+ proposalType: 'custom';
94
+ /** Original server-defined proposal label, e.g. "b2agg" (issue #266). Mirrors
95
+ * Rust `ProposalMetadata.proposal_type`; it is what lets a custom proposal
96
+ * round-trip back to GUARDIAN/export, so it is required in the domain model.
97
+ * Any wire-level optionality is resolved in the parser/codec boundary. */
98
+ rawProposalType: string;
84
99
  }
85
100
 
86
101
  export type ProposalMetadata =
@@ -89,7 +104,7 @@ export type ProposalMetadata =
89
104
  | UpdateProcedureThresholdProposalMetadata
90
105
  | ConsumeNotesProposalMetadata
91
106
  | P2IdProposalMetadata
92
- | UnknownProposalMetadata;
107
+ | CustomProposalMetadata;
93
108
 
94
109
  export interface Proposal {
95
110
  id: string;
@@ -1,385 +0,0 @@
1
- import { type DeltaObject, type GuardianHttpClient } from '@openzeppelin/guardian-client';
2
- import type {
3
- MidenClient,
4
- TransactionProver,
5
- TransactionRequest,
6
- } from '@miden-sdk/miden-sdk';
7
- import {
8
- AdviceMap,
9
- FeltArray,
10
- Signature,
11
- TransactionSummary,
12
- Word,
13
- } from '@miden-sdk/miden-sdk';
14
- import type { ProposalType, TransactionProposal } from '../../types.js';
15
- import {
16
- buildConsumeNotesTransactionRequest,
17
- buildP2idTransactionRequest,
18
- buildUpdateGuardianTransactionRequest,
19
- buildUpdateProcedureThresholdTransactionRequest,
20
- buildUpdateSignersTransactionRequest,
21
- } from '../../transaction.js';
22
- import { base64ToUint8Array, normalizeHexWord } from '../../utils/encoding.js';
23
- import {
24
- buildSignatureAdviceEntry,
25
- signatureHexToBytes,
26
- tryComputeEcdsaCommitmentHex,
27
- } from '../../utils/signature.js';
28
- import { computeCommitmentFromTxSummary } from '../helpers.js';
29
-
30
- interface ResolveExecutionSourceResult {
31
- delta?: DeltaObject;
32
- txSummaryBase64: string;
33
- }
34
-
35
- interface PrepareExecutionResult {
36
- txSummary: TransactionSummary;
37
- saltHex: string;
38
- txCommitmentHex: string;
39
- }
40
-
41
- interface ExecuteProposalWorkflowParams {
42
- proposal: TransactionProposal;
43
- accountId: string;
44
- threshold: number;
45
- signerCommitments: string[];
46
- guardianCommitment: string;
47
- guardianPublicKey?: string;
48
- signatureScheme: 'falcon' | 'ecdsa';
49
- getEffectiveThreshold: (proposalType: ProposalType) => number;
50
- guardian: GuardianHttpClient;
51
- midenClient: MidenClient;
52
- transactionProver?: TransactionProver | null;
53
- }
54
-
55
- export async function executeProposalWorkflow(
56
- params: ExecuteProposalWorkflowParams,
57
- ): Promise<void> {
58
- ensureProposalReady(
59
- params.proposal,
60
- params.threshold,
61
- params.getEffectiveThreshold,
62
- );
63
-
64
- const isSwitchGuardian = params.proposal.metadata.proposalType === 'switch_guardian';
65
- const executionSource = await resolveExecutionSource(
66
- params.guardian,
67
- params.accountId,
68
- params.proposal,
69
- isSwitchGuardian,
70
- );
71
- const executionData = prepareExecutionData(executionSource.txSummaryBase64);
72
-
73
- const adviceMap = buildCosignerAdviceMap(
74
- params.proposal,
75
- params.signerCommitments,
76
- executionData.txCommitmentHex,
77
- );
78
-
79
- if (!isSwitchGuardian && executionSource.delta) {
80
- await appendGuardianAckAdvice(
81
- params.guardian,
82
- executionSource.delta,
83
- params.guardianCommitment,
84
- params.guardianPublicKey,
85
- params.signatureScheme,
86
- executionData.txCommitmentHex,
87
- adviceMap,
88
- );
89
- }
90
-
91
- const finalRequest = await buildFinalRequest(
92
- params,
93
- executionData.saltHex,
94
- adviceMap,
95
- );
96
- await submitTransaction(
97
- params.midenClient,
98
- params.accountId,
99
- finalRequest,
100
- );
101
- }
102
-
103
-
104
- export async function createTransactionProposalRequest(
105
- params: ExecuteProposalWorkflowParams,
106
- ): Promise<TransactionRequest> {
107
- ensureProposalReady(
108
- params.proposal,
109
- params.threshold,
110
- params.getEffectiveThreshold,
111
- );
112
-
113
- const isSwitchGuardian = params.proposal.metadata.proposalType === 'switch_guardian';
114
- const executionSource = await resolveExecutionSource(
115
- params.guardian,
116
- params.accountId,
117
- params.proposal,
118
- isSwitchGuardian,
119
- );
120
- const executionData = prepareExecutionData(executionSource.txSummaryBase64);
121
-
122
- const adviceMap = buildCosignerAdviceMap(
123
- params.proposal,
124
- params.signerCommitments,
125
- executionData.txCommitmentHex,
126
- );
127
-
128
- if (!isSwitchGuardian && executionSource.delta) {
129
- await appendGuardianAckAdvice(
130
- params.guardian,
131
- executionSource.delta,
132
- params.guardianCommitment,
133
- params.guardianPublicKey,
134
- params.signatureScheme,
135
- executionData.txCommitmentHex,
136
- adviceMap,
137
- );
138
- }
139
-
140
- return await buildFinalRequest(
141
- params,
142
- executionData.saltHex,
143
- adviceMap,
144
- );
145
- }
146
-
147
- function ensureProposalReady(
148
- proposal: TransactionProposal,
149
- defaultThreshold: number,
150
- getEffectiveThreshold: (proposalType: ProposalType) => number,
151
- ): void {
152
- const proposalType = proposal.metadata?.proposalType;
153
- const effectiveThreshold = proposalType
154
- ? getEffectiveThreshold(proposalType)
155
- : defaultThreshold;
156
-
157
- if (proposal.signatures.length < effectiveThreshold) {
158
- throw new Error('Proposal is not ready for execution. Still pending signatures.');
159
- }
160
- }
161
-
162
- async function resolveExecutionSource(
163
- guardian: GuardianHttpClient,
164
- accountId: string,
165
- proposal: TransactionProposal,
166
- isSwitchGuardian: boolean,
167
- ): Promise<ResolveExecutionSourceResult> {
168
- if (isSwitchGuardian) {
169
- return { txSummaryBase64: proposal.txSummary };
170
- }
171
-
172
- const deltas = await guardian.getDeltaProposals(accountId);
173
- const delta = deltas.find(
174
- (d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) === proposal.commitment,
175
- );
176
-
177
- if (!delta) {
178
- throw new Error(`Proposal not found on server: ${proposal.commitment}`);
179
- }
180
-
181
- return {
182
- delta,
183
- txSummaryBase64: delta.deltaPayload.txSummary.data,
184
- };
185
- }
186
-
187
- function prepareExecutionData(txSummaryBase64: string): PrepareExecutionResult {
188
- const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
189
- const txSummary = TransactionSummary.deserialize(txSummaryBytes);
190
- const saltHex = txSummary.salt().toHex();
191
- const txCommitmentHex = txSummary.toCommitment().toHex();
192
- return { txSummary, saltHex, txCommitmentHex };
193
- }
194
-
195
- function buildCosignerAdviceMap(
196
- proposal: TransactionProposal,
197
- signerCommitments: string[],
198
- txCommitmentHex: string,
199
- ): AdviceMap {
200
- const adviceMap = new AdviceMap();
201
- const normalizedSignerCommitments = new Set(
202
- signerCommitments.map((c) => normalizeHexWord(c)),
203
- );
204
-
205
- for (const cosignerSig of proposal.signatures) {
206
- let signerCommitmentHex = normalizeHexWord(cosignerSig.signerId);
207
- if (cosignerSig.signature.scheme === 'ecdsa' && cosignerSig.signature.publicKey) {
208
- const derived = tryComputeEcdsaCommitmentHex(cosignerSig.signature.publicKey);
209
- if (derived && derived !== signerCommitmentHex) {
210
- if (!normalizedSignerCommitments.has(derived)) {
211
- throw new Error(
212
- `ECDSA public key commitment mismatch: derived commitment ${derived} is not in signerCommitments.`,
213
- );
214
- }
215
- signerCommitmentHex = derived;
216
- }
217
- }
218
-
219
- const signerCommitment = Word.fromHex(signerCommitmentHex);
220
- const sigBytes = signatureHexToBytes(
221
- cosignerSig.signature.signature,
222
- cosignerSig.signature.scheme,
223
- );
224
- const signature = Signature.deserialize(sigBytes);
225
- const txCommitment = Word.fromHex(normalizeHexWord(txCommitmentHex));
226
- const ecdsaPublicKey =
227
- cosignerSig.signature.scheme === 'ecdsa'
228
- ? cosignerSig.signature.publicKey
229
- : undefined;
230
- const isEcdsa = cosignerSig.signature.scheme === 'ecdsa' && Boolean(ecdsaPublicKey);
231
-
232
- const { key, values } = buildSignatureAdviceEntry(
233
- signerCommitment,
234
- txCommitment,
235
- signature,
236
- ecdsaPublicKey,
237
- isEcdsa ? cosignerSig.signature.signature : undefined,
238
- );
239
- adviceMap.insert(key, new FeltArray(values));
240
- }
241
-
242
- return adviceMap;
243
- }
244
-
245
- async function appendGuardianAckAdvice(
246
- guardian: GuardianHttpClient,
247
- delta: DeltaObject,
248
- guardianCommitmentHex: string,
249
- guardianPublicKey: string | undefined,
250
- defaultAckScheme: 'falcon' | 'ecdsa',
251
- txCommitmentHex: string,
252
- adviceMap: AdviceMap,
253
- ): Promise<void> {
254
- const executionDelta = {
255
- ...delta,
256
- deltaPayload: delta.deltaPayload.txSummary,
257
- };
258
-
259
- const pushResult = await guardian.pushDelta(executionDelta);
260
- const ackSigHex = pushResult.ackSig;
261
- if (!ackSigHex) {
262
- throw new Error('GUARDIAN did not return acknowledgment signature');
263
- }
264
-
265
- const guardianAckScheme: 'ecdsa' | 'falcon' =
266
- (pushResult.ackScheme as 'ecdsa' | 'falcon') || defaultAckScheme;
267
- const ackPubkey = pushResult.ackPubkey || guardianPublicKey;
268
- const normalizedGuardianCommitment = normalizeHexWord(guardianCommitmentHex);
269
-
270
- if (guardianAckScheme === 'ecdsa' && ackPubkey) {
271
- const derived = tryComputeEcdsaCommitmentHex(ackPubkey);
272
- if (derived && derived !== normalizedGuardianCommitment) {
273
- throw new Error('GUARDIAN public key commitment mismatch');
274
- }
275
- }
276
-
277
- const guardianCommitment = Word.fromHex(normalizedGuardianCommitment);
278
- const ackSigBytes = signatureHexToBytes(ackSigHex, guardianAckScheme);
279
- const ackSignature = Signature.deserialize(ackSigBytes);
280
- const txCommitment = Word.fromHex(normalizeHexWord(txCommitmentHex));
281
- const isAckEcdsa = guardianAckScheme === 'ecdsa' && ackPubkey;
282
- const { key, values } = buildSignatureAdviceEntry(
283
- guardianCommitment,
284
- txCommitment,
285
- ackSignature,
286
- isAckEcdsa ? ackPubkey : undefined,
287
- isAckEcdsa ? ackSigHex : undefined,
288
- );
289
- adviceMap.insert(key, new FeltArray(values));
290
- }
291
-
292
- async function buildFinalRequest(
293
- params: ExecuteProposalWorkflowParams,
294
- saltHex: string,
295
- adviceMap: AdviceMap,
296
- ): Promise<TransactionRequest> {
297
- const metadata = params.proposal.metadata;
298
- const normalizedSalt = Word.fromHex(normalizeHexWord(saltHex));
299
-
300
- switch (metadata.proposalType) {
301
- case 'consume_notes': {
302
- if (!metadata.noteIds || metadata.noteIds.length === 0) {
303
- throw new Error(
304
- 'Proposal missing noteIds. Was it created with createConsumeNotesProposal?',
305
- );
306
- }
307
- const { request } = await buildConsumeNotesTransactionRequest(
308
- params.midenClient,
309
- metadata.noteIds,
310
- { salt: normalizedSalt, signatureAdviceMap: adviceMap },
311
- );
312
- return request;
313
- }
314
- case 'switch_guardian': {
315
- if (!metadata.newGuardianPubkey) {
316
- throw new Error(
317
- 'Proposal missing newGuardianPubkey. Was it created with createSwitchGuardianProposal?',
318
- );
319
- }
320
- const { request } = await buildUpdateGuardianTransactionRequest(
321
- params.midenClient,
322
- metadata.newGuardianPubkey,
323
- {
324
- salt: normalizedSalt,
325
- signatureAdviceMap: adviceMap,
326
- signatureScheme: params.signatureScheme,
327
- },
328
- );
329
- return request;
330
- }
331
- case 'update_procedure_threshold': {
332
- const { request } = await buildUpdateProcedureThresholdTransactionRequest(
333
- params.midenClient,
334
- metadata.targetProcedure,
335
- metadata.targetThreshold,
336
- {
337
- salt: normalizedSalt,
338
- signatureAdviceMap: adviceMap,
339
- signatureScheme: params.signatureScheme,
340
- },
341
- );
342
- return request;
343
- }
344
- case 'p2id': {
345
- if (!metadata.recipientId || !metadata.faucetId || !metadata.amount) {
346
- throw new Error(
347
- 'Proposal missing P2ID metadata (recipientId, faucetId, amount). Was it created with createP2idProposal?',
348
- );
349
- }
350
- const { request } = buildP2idTransactionRequest(
351
- params.accountId,
352
- metadata.recipientId,
353
- metadata.faucetId,
354
- BigInt(metadata.amount),
355
- { salt: normalizedSalt, signatureAdviceMap: adviceMap },
356
- );
357
- return request;
358
- }
359
- case 'unknown':
360
- throw new Error(
361
- 'Cannot execute proposal with unknown type. The proposal must have been imported without proper metadata.',
362
- );
363
- default: {
364
- const { request } = await buildUpdateSignersTransactionRequest(
365
- params.midenClient,
366
- metadata.targetThreshold,
367
- metadata.targetSignerCommitments,
368
- {
369
- salt: normalizedSalt,
370
- signatureAdviceMap: adviceMap,
371
- signatureScheme: params.signatureScheme,
372
- },
373
- );
374
- return request;
375
- }
376
- }
377
- }
378
-
379
- async function submitTransaction(
380
- midenClient: MidenClient,
381
- accountIdHex: string,
382
- request: TransactionRequest,
383
- ): Promise<void> {
384
- await midenClient.transactions.submit(accountIdHex, request);
385
- }
@@ -1,191 +0,0 @@
1
- import type {
2
- DeltaObject,
3
- DeltaStatus,
4
- ProposalMetadata as GuardianProposalMetadata,
5
- } from '@openzeppelin/guardian-client';
6
- import type {
7
- ProposalMetadata,
8
- ProposalType,
9
- TransactionProposal,
10
- TransactionProposalSignature,
11
- TransactionProposalStatus,
12
- } from '../../types.js';
13
-
14
- export function buildGuardianMetadata(metadata: ProposalMetadata): GuardianProposalMetadata {
15
- const base: GuardianProposalMetadata = {
16
- proposalType: metadata.proposalType,
17
- description: metadata.description,
18
- salt: metadata.saltHex,
19
- };
20
-
21
- switch (metadata.proposalType) {
22
- case 'consume_notes':
23
- return {
24
- ...base,
25
- noteIds: metadata.noteIds,
26
- };
27
- case 'p2id':
28
- return {
29
- ...base,
30
- recipientId: metadata.recipientId,
31
- faucetId: metadata.faucetId,
32
- amount: metadata.amount,
33
- };
34
- case 'switch_guardian':
35
- return {
36
- ...base,
37
- targetThreshold: metadata.targetThreshold,
38
- signerCommitments: metadata.targetSignerCommitments,
39
- newGuardianPubkey: metadata.newGuardianPubkey,
40
- newGuardianEndpoint: metadata.newGuardianEndpoint,
41
- };
42
- case 'add_signer':
43
- case 'remove_signer':
44
- case 'change_threshold':
45
- return {
46
- ...base,
47
- targetThreshold: metadata.targetThreshold,
48
- signerCommitments: metadata.targetSignerCommitments,
49
- };
50
- case 'unknown':
51
- return base;
52
- default:
53
- return base;
54
- }
55
- }
56
-
57
- export function fromGuardianMetadata(guardian: GuardianProposalMetadata): ProposalMetadata | undefined {
58
- if (!guardian.proposalType) return undefined;
59
-
60
- const base = {
61
- description: guardian.description ?? '',
62
- saltHex: guardian.salt,
63
- };
64
-
65
- switch (guardian.proposalType) {
66
- case 'p2id':
67
- return {
68
- ...base,
69
- proposalType: 'p2id',
70
- recipientId: guardian.recipientId ?? '',
71
- faucetId: guardian.faucetId ?? '',
72
- amount: guardian.amount ?? '0',
73
- };
74
- case 'consume_notes':
75
- return {
76
- ...base,
77
- proposalType: 'consume_notes',
78
- noteIds: guardian.noteIds ?? [],
79
- };
80
- case 'switch_guardian':
81
- return {
82
- ...base,
83
- proposalType: 'switch_guardian',
84
- newGuardianPubkey: guardian.newGuardianPubkey ?? '',
85
- newGuardianEndpoint: guardian.newGuardianEndpoint,
86
- targetThreshold: guardian.targetThreshold,
87
- targetSignerCommitments: guardian.signerCommitments,
88
- };
89
- case 'add_signer':
90
- case 'remove_signer':
91
- case 'change_threshold':
92
- return {
93
- ...base,
94
- proposalType: guardian.proposalType,
95
- targetThreshold: guardian.targetThreshold ?? 0,
96
- targetSignerCommitments: guardian.signerCommitments ?? [],
97
- };
98
- default:
99
- return undefined;
100
- }
101
- }
102
-
103
- export function deltaStatusToProposalStatus(
104
- status: DeltaStatus,
105
- signaturesRequired: number,
106
- ): TransactionProposalStatus {
107
- switch (status.status) {
108
- case 'pending': {
109
- const signaturesCollected = status.cosignerSigs.length;
110
- if (signaturesCollected >= signaturesRequired) {
111
- return { type: 'ready' };
112
- }
113
- return {
114
- type: 'pending',
115
- signaturesCollected,
116
- signaturesRequired,
117
- signers: status.cosignerSigs.map((s) => s.signerId),
118
- };
119
- }
120
- case 'candidate':
121
- return { type: 'ready' };
122
- case 'canonical':
123
- case 'discarded':
124
- return { type: 'finalized' };
125
- }
126
- }
127
-
128
- interface DeltaToProposalParams {
129
- delta: DeltaObject;
130
- proposalId: string;
131
- metadata: ProposalMetadata;
132
- signaturesRequired: number;
133
- existingSignatures?: TransactionProposalSignature[];
134
- }
135
-
136
- export function deltaToProposal({
137
- delta,
138
- proposalId,
139
- metadata,
140
- signaturesRequired,
141
- existingSignatures,
142
- }: DeltaToProposalParams): TransactionProposal {
143
- const status = deltaStatusToProposalStatus(delta.status, signaturesRequired);
144
-
145
- const signaturesFromStatus =
146
- delta.status.status === 'pending'
147
- ? delta.status.cosignerSigs.map((s) => ({
148
- signerId: s.signerId,
149
- signature: s.signature,
150
- timestamp: s.timestamp,
151
- }))
152
- : [];
153
-
154
- const signaturesMap = new Map<string, TransactionProposalSignature>();
155
- for (const sig of existingSignatures ?? []) {
156
- signaturesMap.set(sig.signerId, sig);
157
- }
158
- for (const sig of signaturesFromStatus) {
159
- signaturesMap.set(sig.signerId, sig);
160
- }
161
- const signatures = Array.from(signaturesMap.values());
162
-
163
- return {
164
- id: proposalId,
165
- commitment: proposalId,
166
- accountId: delta.accountId,
167
- nonce: delta.nonce,
168
- status,
169
- txSummary: delta.deltaPayload.txSummary.data,
170
- signatures,
171
- metadata,
172
- };
173
- }
174
-
175
- export function resolveMetadata(
176
- delta: DeltaObject,
177
- existingMetadata?: ProposalMetadata,
178
- ): ProposalMetadata | undefined {
179
- if (existingMetadata) return existingMetadata;
180
- if (!delta.deltaPayload.metadata) return undefined;
181
- return fromGuardianMetadata(delta.deltaPayload.metadata);
182
- }
183
-
184
- export function signatureRequirementForProposal(
185
- metadata: ProposalMetadata,
186
- defaultThreshold: number,
187
- getEffectiveThreshold: (proposalType: ProposalType) => number,
188
- ): number {
189
- if (metadata.proposalType === 'unknown') return defaultThreshold;
190
- return getEffectiveThreshold(metadata.proposalType);
191
- }