@openzeppelin/miden-multisig-client 0.12.6 → 0.12.7
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.
- package/README.md +70 -75
- package/dist/account/builder.d.ts +0 -18
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +16 -24
- package/dist/account/builder.js.map +1 -1
- package/dist/account/masm.d.ts +2 -0
- package/dist/account/masm.d.ts.map +1 -1
- package/dist/account/masm.js +428 -1
- package/dist/account/masm.js.map +1 -1
- package/dist/account/storage.d.ts.map +1 -1
- package/dist/account/storage.js +5 -6
- package/dist/account/storage.js.map +1 -1
- package/dist/client.d.ts +5 -57
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -53
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +28 -0
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +3 -46
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -45
- package/dist/index.js.map +1 -1
- package/dist/inspector.d.ts +4 -30
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +13 -43
- package/dist/inspector.js.map +1 -1
- package/dist/multisig/helpers.d.ts.map +1 -1
- package/dist/multisig/helpers.js.map +1 -1
- package/dist/multisig.d.ts +39 -161
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +190 -241
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +41 -31
- package/dist/multisig.test.js.map +1 -1
- package/dist/procedures.d.ts +20 -47
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +17 -50
- package/dist/procedures.js.map +1 -1
- package/dist/signer.d.ts +11 -26
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +41 -26
- package/dist/signer.js.map +1 -1
- package/dist/transaction/options.d.ts +2 -0
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/updatePsm.d.ts.map +1 -1
- package/dist/transaction/updatePsm.js +21 -3
- package/dist/transaction/updatePsm.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +21 -3
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +0 -2
- package/dist/transaction.js.map +1 -1
- package/dist/transaction.test.js +5 -0
- package/dist/transaction.test.js.map +1 -1
- package/dist/types/proposal.d.ts +16 -8
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types.d.ts +18 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/signature.d.ts +10 -2
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +77 -5
- package/dist/utils/signature.js.map +1 -1
- package/dist/utils/signature.test.js +16 -1
- package/dist/utils/signature.test.js.map +1 -1
- package/masm/multisig_ecdsa.masm +424 -0
- package/masm/psm_ecdsa.masm +179 -0
- package/package.json +2 -2
- package/src/account/builder.ts +18 -25
- package/src/account/masm.ts +433 -2
- package/src/account/storage.ts +5 -6
- package/src/client.test.ts +36 -0
- package/src/client.ts +10 -59
- package/src/index.ts +13 -62
- package/src/inspector.ts +15 -44
- package/src/multisig/helpers.ts +1 -2
- package/src/multisig.test.ts +45 -34
- package/src/multisig.ts +246 -273
- package/src/procedures.ts +21 -56
- package/src/signer.ts +48 -28
- package/src/transaction/options.ts +2 -0
- package/src/transaction/updatePsm.ts +24 -3
- package/src/transaction/updateSigners.ts +25 -3
- package/src/transaction.test.ts +6 -0
- package/src/transaction.ts +0 -2
- package/src/types/proposal.ts +15 -8
- package/src/types.ts +26 -16
- package/src/utils/signature.test.ts +24 -1
- package/src/utils/signature.ts +86 -4
package/src/multisig.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
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
|
-
|
|
8
|
-
import { PsmHttpClient, type DeltaObject, type DeltaStatus, type FalconSignature, type Signer, type AuthConfig, type StateObject, type ProposalMetadata as PsmProposalMetadata } from '@openzeppelin/psm-client';
|
|
1
|
+
import { PsmHttpClient, type DeltaObject, type DeltaStatus, type ProposalSignature, type Signer, type AuthConfig, type StateObject, type ProposalMetadata as PsmProposalMetadata } from '@openzeppelin/psm-client';
|
|
9
2
|
import type {
|
|
10
3
|
ConsumableNote,
|
|
11
|
-
|
|
4
|
+
ExportedTransactionProposal,
|
|
12
5
|
MultisigConfig,
|
|
13
6
|
NoteAsset,
|
|
14
|
-
|
|
7
|
+
TransactionProposal,
|
|
15
8
|
ProposalMetadata,
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
TransactionProposalSignature,
|
|
10
|
+
TransactionProposalStatus,
|
|
18
11
|
ProposalType,
|
|
12
|
+
SignTransactionProposalParams,
|
|
13
|
+
SyncResult,
|
|
14
|
+
TransactionProposalResult,
|
|
19
15
|
} from './types.js';
|
|
20
16
|
import type { ProcedureName } from './procedures.js';
|
|
17
|
+
import { AccountInspector, type DetectedMultisigConfig } from './inspector.js';
|
|
21
18
|
import type { WebClient, TransactionRequest } from '@demox-labs/miden-sdk';
|
|
22
19
|
import {
|
|
23
20
|
Account,
|
|
@@ -40,38 +37,32 @@ import {
|
|
|
40
37
|
uint8ArrayToBase64,
|
|
41
38
|
normalizeHexWord,
|
|
42
39
|
} from './utils/encoding.js';
|
|
43
|
-
import { buildSignatureAdviceEntry, signatureHexToBytes } from './utils/signature.js';
|
|
40
|
+
import { buildSignatureAdviceEntry, signatureHexToBytes, tryComputeEcdsaCommitmentHex } from './utils/signature.js';
|
|
44
41
|
import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
|
|
45
42
|
|
|
46
|
-
/**
|
|
47
|
-
* Result of fetching account state from PSM.
|
|
48
|
-
*/
|
|
49
43
|
export interface AccountState {
|
|
50
|
-
/** Account ID */
|
|
51
44
|
accountId: string;
|
|
52
|
-
/** Current commitment */
|
|
53
45
|
commitment: string;
|
|
54
|
-
/** Raw state data (base64-encoded serialized account) */
|
|
55
46
|
stateDataBase64: string;
|
|
56
47
|
createdAt: string;
|
|
57
48
|
updatedAt: string;
|
|
49
|
+
authScheme?: string;
|
|
58
50
|
}
|
|
59
51
|
|
|
60
|
-
/**
|
|
61
|
-
* Represents a multisig account with PSM integration.
|
|
62
|
-
*/
|
|
63
52
|
export class Multisig {
|
|
64
53
|
readonly account: Account | null;
|
|
65
54
|
readonly threshold: number;
|
|
66
55
|
readonly signerCommitments: string[];
|
|
67
56
|
readonly psmCommitment: string;
|
|
57
|
+
psmPublicKey?: string;
|
|
68
58
|
readonly procedureThresholds: Map<ProcedureName, number>;
|
|
59
|
+
readonly signatureScheme: Signer['scheme'];
|
|
69
60
|
|
|
70
61
|
private psm: PsmHttpClient;
|
|
71
62
|
private readonly signer: Signer;
|
|
72
63
|
private readonly webClient: WebClient;
|
|
73
64
|
private readonly _accountId: string;
|
|
74
|
-
private proposals: Map<string,
|
|
65
|
+
private proposals: Map<string, TransactionProposal> = new Map();
|
|
75
66
|
|
|
76
67
|
constructor(
|
|
77
68
|
account: Account | null,
|
|
@@ -81,10 +72,17 @@ export class Multisig {
|
|
|
81
72
|
webClient: WebClient,
|
|
82
73
|
accountId?: string
|
|
83
74
|
) {
|
|
75
|
+
if (config.signatureScheme && config.signatureScheme !== signer.scheme) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`signature scheme mismatch: config=${config.signatureScheme} signer=${signer.scheme}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
84
80
|
this.account = account;
|
|
85
81
|
this.threshold = config.threshold;
|
|
86
82
|
this.signerCommitments = config.signerCommitments;
|
|
87
83
|
this.psmCommitment = config.psmCommitment;
|
|
84
|
+
this.psmPublicKey = config.psmPublicKey;
|
|
85
|
+
this.signatureScheme = config.signatureScheme ?? signer.scheme;
|
|
88
86
|
this.procedureThresholds = new Map(
|
|
89
87
|
(config.procedureThresholds ?? []).map((pt) => [pt.procedure, pt.threshold])
|
|
90
88
|
);
|
|
@@ -94,19 +92,18 @@ export class Multisig {
|
|
|
94
92
|
this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
/** The account ID as a string */
|
|
98
95
|
get accountId(): string {
|
|
99
96
|
return this._accountId;
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
/** The signer's commitment */
|
|
103
99
|
get signerCommitment(): string {
|
|
104
100
|
return this.signer.commitment;
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
setPsmPublicKey(pubkey?: string): void {
|
|
104
|
+
this.psmPublicKey = pubkey;
|
|
105
|
+
}
|
|
106
|
+
|
|
110
107
|
private getProposalProcedure(proposalType: ProposalType): ProcedureName | null {
|
|
111
108
|
switch (proposalType) {
|
|
112
109
|
case 'p2id':
|
|
@@ -124,13 +121,6 @@ export class Multisig {
|
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
/**
|
|
128
|
-
* Get the effective threshold for a given proposal type.
|
|
129
|
-
* Returns the procedure-specific threshold if configured, otherwise the default threshold.
|
|
130
|
-
*
|
|
131
|
-
* @param proposalType - The type of proposal
|
|
132
|
-
* @returns The threshold that applies to this proposal type
|
|
133
|
-
*/
|
|
134
124
|
getEffectiveThreshold(proposalType: ProposalType): number {
|
|
135
125
|
if (this.procedureThresholds.size === 0) {
|
|
136
126
|
return this.threshold;
|
|
@@ -144,21 +134,11 @@ export class Multisig {
|
|
|
144
134
|
return this.procedureThresholds.get(procedure) ?? this.threshold;
|
|
145
135
|
}
|
|
146
136
|
|
|
147
|
-
/**
|
|
148
|
-
* Update the PSM client used by this Multisig instance.
|
|
149
|
-
*
|
|
150
|
-
* @param psmClient - The new PSM HTTP client
|
|
151
|
-
*/
|
|
152
137
|
setPsmClient(psmClient: PsmHttpClient): void {
|
|
153
138
|
this.psm = psmClient;
|
|
154
139
|
this.psm.setSigner(this.signer);
|
|
155
140
|
}
|
|
156
141
|
|
|
157
|
-
/**
|
|
158
|
-
* Fetch the current account state from PSM.
|
|
159
|
-
*
|
|
160
|
-
* @returns The account state including commitment and serialized data
|
|
161
|
-
*/
|
|
162
142
|
async fetchState(): Promise<AccountState> {
|
|
163
143
|
const state: StateObject = await this.psm.getState(this._accountId);
|
|
164
144
|
|
|
@@ -168,15 +148,10 @@ export class Multisig {
|
|
|
168
148
|
stateDataBase64: state.stateJson.data,
|
|
169
149
|
createdAt: state.createdAt,
|
|
170
150
|
updatedAt: state.updatedAt,
|
|
151
|
+
authScheme: state.authScheme,
|
|
171
152
|
};
|
|
172
153
|
}
|
|
173
154
|
|
|
174
|
-
/**
|
|
175
|
-
* Sync account state from PSM into the local WebClient store.
|
|
176
|
-
*
|
|
177
|
-
* If the PSM commitment differs from the local commitment (or the account
|
|
178
|
-
* is missing locally), the local store is overwritten with the PSM state.
|
|
179
|
-
*/
|
|
180
155
|
async syncState(): Promise<AccountState> {
|
|
181
156
|
const state = await this.fetchState();
|
|
182
157
|
const accountId = AccountId.fromHex(this._accountId);
|
|
@@ -196,20 +171,11 @@ export class Multisig {
|
|
|
196
171
|
return state;
|
|
197
172
|
}
|
|
198
173
|
|
|
199
|
-
/**
|
|
200
|
-
* Register this multisig account on the PSM server.
|
|
201
|
-
*
|
|
202
|
-
* The initial state must be the serialized Account bytes (base64-encoded).
|
|
203
|
-
* If not provided, the account's serialize() method is used.
|
|
204
|
-
*
|
|
205
|
-
* @param initialStateBase64 - Optional base64-encoded serialized Account.¡
|
|
206
|
-
*/
|
|
207
174
|
async registerOnPsm(initialStateBase64?: string): Promise<void> {
|
|
208
175
|
if (!this.account && !initialStateBase64) {
|
|
209
176
|
throw new Error('Cannot register on PSM: no account available and no initial state provided');
|
|
210
177
|
}
|
|
211
178
|
|
|
212
|
-
// Serialize the account to bytes and base64-encode
|
|
213
179
|
let stateData: string;
|
|
214
180
|
if (initialStateBase64) {
|
|
215
181
|
stateData = initialStateBase64;
|
|
@@ -218,11 +184,9 @@ export class Multisig {
|
|
|
218
184
|
stateData = uint8ArrayToBase64(accountBytes);
|
|
219
185
|
}
|
|
220
186
|
|
|
221
|
-
const auth: AuthConfig =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
},
|
|
225
|
-
};
|
|
187
|
+
const auth: AuthConfig = this.signer.scheme === 'ecdsa'
|
|
188
|
+
? { MidenEcdsa: { cosigner_commitments: this.signerCommitments } }
|
|
189
|
+
: { MidenFalconRpo: { cosigner_commitments: this.signerCommitments } };
|
|
226
190
|
|
|
227
191
|
const response = await this.psm.configure({
|
|
228
192
|
accountId: this._accountId,
|
|
@@ -233,16 +197,45 @@ export class Multisig {
|
|
|
233
197
|
if (!response.success) {
|
|
234
198
|
throw new Error(`Failed to register on PSM: ${response.message}`);
|
|
235
199
|
}
|
|
200
|
+
|
|
201
|
+
if (response.ackCommitment && this.psmCommitment) {
|
|
202
|
+
const onChain = normalizeHexWord(this.psmCommitment);
|
|
203
|
+
const server = normalizeHexWord(response.ackCommitment);
|
|
204
|
+
if (onChain !== server) {
|
|
205
|
+
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.`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async syncAll(): Promise<SyncResult> {
|
|
214
|
+
const proposals = await this.syncTransactionProposals();
|
|
215
|
+
const state = await this.syncState();
|
|
216
|
+
const notes = await this.getConsumableNotes();
|
|
217
|
+
const config = AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
|
|
218
|
+
return { proposals, state, notes, config };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getAccountConfig(): Promise<DetectedMultisigConfig> {
|
|
222
|
+
const state = await this.syncState();
|
|
223
|
+
return AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
|
|
236
224
|
}
|
|
237
225
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
226
|
+
async switchPsm(psmClient: PsmHttpClient): Promise<void> {
|
|
227
|
+
this.setPsmClient(psmClient);
|
|
228
|
+
const state = await this.fetchState();
|
|
229
|
+
await this.registerOnPsm(state.stateDataBase64);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async syncTransactionProposals(): Promise<TransactionProposal[]> {
|
|
242
233
|
const deltas = await this.psm.getDeltaProposals(this._accountId);
|
|
243
234
|
|
|
235
|
+
const serverProposalIds = new Set<string>();
|
|
244
236
|
for (const delta of deltas) {
|
|
245
237
|
const proposalId = computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data);
|
|
238
|
+
serverProposalIds.add(proposalId);
|
|
246
239
|
const existingProposal = this.proposals.get(proposalId);
|
|
247
240
|
|
|
248
241
|
const resolvedMetadata =
|
|
@@ -259,24 +252,22 @@ export class Multisig {
|
|
|
259
252
|
this.proposals.set(proposal.id, proposal);
|
|
260
253
|
}
|
|
261
254
|
|
|
255
|
+
for (const [id, proposal] of this.proposals) {
|
|
256
|
+
if (serverProposalIds.has(id)) continue;
|
|
257
|
+
const isLocalOnly = proposal.metadata?.proposalType === 'switch_psm' && proposal.status.type !== 'finalized';
|
|
258
|
+
if (!isLocalOnly) {
|
|
259
|
+
this.proposals.delete(id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
262
263
|
return Array.from(this.proposals.values());
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
* List all known proposals
|
|
267
|
-
*/
|
|
268
|
-
listProposals(): Proposal[] {
|
|
266
|
+
listTransactionProposals(): TransactionProposal[] {
|
|
269
267
|
return Array.from(this.proposals.values());
|
|
270
268
|
}
|
|
271
269
|
|
|
272
|
-
|
|
273
|
-
* Create a new proposal.
|
|
274
|
-
*
|
|
275
|
-
* @param nonce - The nonce for this transaction
|
|
276
|
-
* @param txSummaryBase64 - Base64-encoded transaction summary
|
|
277
|
-
* @param metadata - Optional metadata for execution (target config, salt, etc.)
|
|
278
|
-
*/
|
|
279
|
-
async createProposal(nonce: number, txSummaryBase64: string, metadata: ProposalMetadata): Promise<Proposal> {
|
|
270
|
+
async createProposal(nonce: number, txSummaryBase64: string, metadata: ProposalMetadata): Promise<TransactionProposal> {
|
|
280
271
|
const psmMetadata = this.buildPsmMetadata(metadata);
|
|
281
272
|
|
|
282
273
|
const response = await this.psm.pushDeltaProposal({
|
|
@@ -295,30 +286,23 @@ export class Multisig {
|
|
|
295
286
|
return proposal;
|
|
296
287
|
}
|
|
297
288
|
|
|
298
|
-
/**
|
|
299
|
-
* Create an "add signer" proposal.
|
|
300
|
-
*
|
|
301
|
-
* @param newCommitment - Commitment of the new signer (hex)
|
|
302
|
-
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
303
|
-
* @param newThreshold - Optional new threshold (defaults to current threshold)
|
|
304
|
-
*/
|
|
305
289
|
async createAddSignerProposal(
|
|
306
290
|
newCommitment: string,
|
|
307
|
-
nonce?: number,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const targetThreshold = newThreshold ?? this.threshold;
|
|
291
|
+
options?: { nonce?: number; newThreshold?: number },
|
|
292
|
+
): Promise<TransactionProposalResult> {
|
|
293
|
+
const targetThreshold = options?.newThreshold ?? this.threshold;
|
|
311
294
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
312
295
|
|
|
313
296
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
314
297
|
this.webClient,
|
|
315
298
|
targetThreshold,
|
|
316
299
|
targetSignerCommitments,
|
|
300
|
+
{ signatureScheme: this.signatureScheme }
|
|
317
301
|
);
|
|
318
302
|
|
|
319
303
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
320
304
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
321
|
-
const proposalNonce = nonce ?? Date.now();
|
|
305
|
+
const proposalNonce = options?.nonce ?? Date.now();
|
|
322
306
|
|
|
323
307
|
const metadata: ProposalMetadata = {
|
|
324
308
|
proposalType: 'add_signer',
|
|
@@ -328,21 +312,14 @@ export class Multisig {
|
|
|
328
312
|
description: `Add signer ${newCommitment.slice(0, 10)}...`,
|
|
329
313
|
};
|
|
330
314
|
|
|
331
|
-
|
|
315
|
+
const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
316
|
+
return this.syncAfterCreate(proposal);
|
|
332
317
|
}
|
|
333
318
|
|
|
334
|
-
/**
|
|
335
|
-
* Create a "remove signer" proposal by executing the update_signers script to summary.
|
|
336
|
-
*
|
|
337
|
-
* @param signerToRemove - Commitment of the signer to remove (hex)
|
|
338
|
-
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
339
|
-
* @param newThreshold - Optional new threshold (defaults to min of current threshold and new signer count)
|
|
340
|
-
*/
|
|
341
319
|
async createRemoveSignerProposal(
|
|
342
320
|
signerToRemove: string,
|
|
343
|
-
nonce?: number,
|
|
344
|
-
|
|
345
|
-
): Promise<Proposal> {
|
|
321
|
+
options?: { nonce?: number; newThreshold?: number },
|
|
322
|
+
): Promise<TransactionProposalResult> {
|
|
346
323
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
347
324
|
const signerExists = this.signerCommitments.some(
|
|
348
325
|
(c) => c.toLowerCase() === normalizedRemove
|
|
@@ -359,7 +336,7 @@ export class Multisig {
|
|
|
359
336
|
throw new Error('Cannot remove the last signer');
|
|
360
337
|
}
|
|
361
338
|
|
|
362
|
-
const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
339
|
+
const targetThreshold = options?.newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
363
340
|
|
|
364
341
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
365
342
|
throw new Error(
|
|
@@ -371,11 +348,12 @@ export class Multisig {
|
|
|
371
348
|
this.webClient,
|
|
372
349
|
targetThreshold,
|
|
373
350
|
targetSignerCommitments,
|
|
351
|
+
{ signatureScheme: this.signatureScheme }
|
|
374
352
|
);
|
|
375
353
|
|
|
376
354
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
377
355
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
378
|
-
const proposalNonce = nonce ?? Date.now();
|
|
356
|
+
const proposalNonce = options?.nonce ?? Date.now();
|
|
379
357
|
|
|
380
358
|
const metadata: ProposalMetadata = {
|
|
381
359
|
proposalType: 'remove_signer',
|
|
@@ -385,19 +363,14 @@ export class Multisig {
|
|
|
385
363
|
description: `Remove signer ${signerToRemove.slice(0, 10)}...`,
|
|
386
364
|
};
|
|
387
365
|
|
|
388
|
-
|
|
366
|
+
const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
367
|
+
return this.syncAfterCreate(proposal);
|
|
389
368
|
}
|
|
390
369
|
|
|
391
|
-
/**
|
|
392
|
-
* Create a "change threshold" proposal.
|
|
393
|
-
*
|
|
394
|
-
* @param newThreshold - The new threshold value
|
|
395
|
-
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
396
|
-
*/
|
|
397
370
|
async createChangeThresholdProposal(
|
|
398
371
|
newThreshold: number,
|
|
399
|
-
nonce?: number,
|
|
400
|
-
): Promise<
|
|
372
|
+
options?: { nonce?: number },
|
|
373
|
+
): Promise<TransactionProposalResult> {
|
|
401
374
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
402
375
|
throw new Error(
|
|
403
376
|
`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`
|
|
@@ -412,11 +385,12 @@ export class Multisig {
|
|
|
412
385
|
this.webClient,
|
|
413
386
|
newThreshold,
|
|
414
387
|
this.signerCommitments,
|
|
388
|
+
{ signatureScheme: this.signatureScheme }
|
|
415
389
|
);
|
|
416
390
|
|
|
417
391
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
418
392
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
419
|
-
const proposalNonce = nonce ?? Date.now();
|
|
393
|
+
const proposalNonce = options?.nonce ?? Date.now();
|
|
420
394
|
|
|
421
395
|
const metadata: ProposalMetadata = {
|
|
422
396
|
proposalType: 'change_threshold',
|
|
@@ -426,29 +400,24 @@ export class Multisig {
|
|
|
426
400
|
description: `Change threshold from ${this.threshold} to ${newThreshold}`,
|
|
427
401
|
};
|
|
428
402
|
|
|
429
|
-
|
|
403
|
+
const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
404
|
+
return this.syncAfterCreate(proposal);
|
|
430
405
|
}
|
|
431
406
|
|
|
432
|
-
/**
|
|
433
|
-
* Create a "switch PSM" proposal to change the PSM provider.
|
|
434
|
-
*
|
|
435
|
-
* @param newPsmEndpoint - The new PSM server endpoint URL
|
|
436
|
-
* @param newPsmPubkey - The new PSM server's public key commitment (hex)
|
|
437
|
-
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
438
|
-
*/
|
|
439
407
|
async createSwitchPsmProposal(
|
|
440
408
|
newPsmEndpoint: string,
|
|
441
409
|
newPsmPubkey: string,
|
|
442
|
-
nonce?: number,
|
|
443
|
-
): Promise<
|
|
410
|
+
options?: { nonce?: number },
|
|
411
|
+
): Promise<TransactionProposalResult> {
|
|
444
412
|
const { request, salt } = await buildUpdatePsmTransactionRequest(
|
|
445
413
|
this.webClient,
|
|
446
414
|
newPsmPubkey,
|
|
415
|
+
{ signatureScheme: this.signatureScheme }
|
|
447
416
|
);
|
|
448
417
|
|
|
449
418
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
450
419
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
451
|
-
const proposalNonce = nonce ?? Date.now();
|
|
420
|
+
const proposalNonce = options?.nonce ?? Date.now();
|
|
452
421
|
|
|
453
422
|
const metadata: ProposalMetadata = {
|
|
454
423
|
proposalType: 'switch_psm',
|
|
@@ -459,8 +428,9 @@ export class Multisig {
|
|
|
459
428
|
};
|
|
460
429
|
|
|
461
430
|
const proposalId = computeCommitmentFromTxSummary(summaryBase64);
|
|
462
|
-
const proposal:
|
|
431
|
+
const proposal: TransactionProposal = {
|
|
463
432
|
id: proposalId,
|
|
433
|
+
commitment: proposalId,
|
|
464
434
|
accountId: this._accountId,
|
|
465
435
|
nonce: proposalNonce,
|
|
466
436
|
status: { type: 'pending', signaturesCollected: 0, signaturesRequired: this.threshold, signers: [] },
|
|
@@ -470,19 +440,14 @@ export class Multisig {
|
|
|
470
440
|
};
|
|
471
441
|
|
|
472
442
|
this.proposals.set(proposal.id, proposal);
|
|
473
|
-
|
|
443
|
+
const proposals = this.listTransactionProposals();
|
|
444
|
+
return { proposal, proposals };
|
|
474
445
|
}
|
|
475
446
|
|
|
476
|
-
/**
|
|
477
|
-
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
478
|
-
*
|
|
479
|
-
* @param noteIds - IDs of the notes to consume (hex strings)
|
|
480
|
-
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
481
|
-
*/
|
|
482
447
|
async createConsumeNotesProposal(
|
|
483
448
|
noteIds: string[],
|
|
484
|
-
nonce?: number,
|
|
485
|
-
): Promise<
|
|
449
|
+
options?: { nonce?: number },
|
|
450
|
+
): Promise<TransactionProposalResult> {
|
|
486
451
|
if (noteIds.length === 0) {
|
|
487
452
|
throw new Error('At least one note ID is required');
|
|
488
453
|
}
|
|
@@ -491,7 +456,7 @@ export class Multisig {
|
|
|
491
456
|
|
|
492
457
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
493
458
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
494
|
-
const proposalNonce = nonce ?? Date.now();
|
|
459
|
+
const proposalNonce = options?.nonce ?? Date.now();
|
|
495
460
|
|
|
496
461
|
const metadata: ProposalMetadata = {
|
|
497
462
|
proposalType: 'consume_notes',
|
|
@@ -500,23 +465,16 @@ export class Multisig {
|
|
|
500
465
|
description: `Consume ${noteIds.length} note(s)`,
|
|
501
466
|
};
|
|
502
467
|
|
|
503
|
-
|
|
468
|
+
const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
469
|
+
return this.syncAfterCreate(proposal);
|
|
504
470
|
}
|
|
505
471
|
|
|
506
|
-
|
|
507
|
-
* Create a P2ID proposal to send funds to another account.
|
|
508
|
-
*
|
|
509
|
-
* @param recipientId - Account ID of the recipient (hex string)
|
|
510
|
-
* @param faucetId - Faucet/token account ID (hex string)
|
|
511
|
-
* @param amount - Amount to send
|
|
512
|
-
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
513
|
-
*/
|
|
514
|
-
async createP2idProposal(
|
|
472
|
+
async createSendProposal(
|
|
515
473
|
recipientId: string,
|
|
516
474
|
faucetId: string,
|
|
517
475
|
amount: bigint,
|
|
518
|
-
nonce?: number,
|
|
519
|
-
): Promise<
|
|
476
|
+
options?: { nonce?: number },
|
|
477
|
+
): Promise<TransactionProposalResult> {
|
|
520
478
|
if (amount <= 0n) {
|
|
521
479
|
throw new Error('Amount must be greater than 0');
|
|
522
480
|
}
|
|
@@ -530,7 +488,7 @@ export class Multisig {
|
|
|
530
488
|
|
|
531
489
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
532
490
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
533
|
-
const proposalNonce = nonce ?? Date.now();
|
|
491
|
+
const proposalNonce = options?.nonce ?? Date.now();
|
|
534
492
|
|
|
535
493
|
const metadata: ProposalMetadata = {
|
|
536
494
|
proposalType: 'p2id',
|
|
@@ -541,28 +499,19 @@ export class Multisig {
|
|
|
541
499
|
description: `Send ${amount} to ${recipientId.slice(0, 10)}...`,
|
|
542
500
|
};
|
|
543
501
|
|
|
544
|
-
|
|
502
|
+
const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
503
|
+
return this.syncAfterCreate(proposal);
|
|
545
504
|
}
|
|
546
505
|
|
|
547
|
-
/**
|
|
548
|
-
* Get notes that can be consumed by this multisig account.
|
|
549
|
-
*
|
|
550
|
-
* Returns a list of notes that are committed on-chain and can be consumed
|
|
551
|
-
* immediately by the multisig account.
|
|
552
|
-
*/
|
|
553
506
|
async getConsumableNotes(): Promise<ConsumableNote[]> {
|
|
554
507
|
const accountId = AccountId.fromHex(this._accountId);
|
|
555
508
|
|
|
556
|
-
// Get consumable notes for this account
|
|
557
509
|
const consumableRecords = await this.webClient.getConsumableNotes(accountId);
|
|
558
|
-
|
|
559
|
-
// Convert to our simplified ConsumableNote type
|
|
560
510
|
const notes: ConsumableNote[] = [];
|
|
561
511
|
for (const record of consumableRecords) {
|
|
562
512
|
const inputNote = record.inputNoteRecord();
|
|
563
513
|
const consumability = record.noteConsumability();
|
|
564
514
|
|
|
565
|
-
// Only include notes that can be consumed now (consumableAfterBlock is undefined/null)
|
|
566
515
|
const canConsumeNow = consumability.some(
|
|
567
516
|
(c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
|
|
568
517
|
c.consumableAfterBlock() === undefined
|
|
@@ -573,7 +522,6 @@ export class Multisig {
|
|
|
573
522
|
const details = inputNote.details();
|
|
574
523
|
const fungibleAssets = details.assets().fungibleAssets();
|
|
575
524
|
|
|
576
|
-
// Extract assets
|
|
577
525
|
const assets: NoteAsset[] = [];
|
|
578
526
|
for (const asset of fungibleAssets) {
|
|
579
527
|
assets.push({
|
|
@@ -589,31 +537,22 @@ export class Multisig {
|
|
|
589
537
|
return notes;
|
|
590
538
|
}
|
|
591
539
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
async signProposal(proposalId: string): Promise<Proposal> {
|
|
601
|
-
const existingProposal = this.proposals.get(proposalId);
|
|
602
|
-
|
|
603
|
-
const signatureHex = this.signer.signCommitment(proposalId);
|
|
604
|
-
|
|
605
|
-
const signature: FalconSignature = {
|
|
606
|
-
scheme: 'falcon',
|
|
607
|
-
signature: signatureHex,
|
|
608
|
-
};
|
|
540
|
+
async signTransactionProposal(commitment: string): Promise<TransactionProposal[]> {
|
|
541
|
+
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 };
|
|
609
548
|
|
|
610
549
|
const delta = await this.psm.signDeltaProposal({
|
|
611
550
|
accountId: this._accountId,
|
|
612
|
-
commitment
|
|
551
|
+
commitment,
|
|
613
552
|
signature,
|
|
614
553
|
});
|
|
615
554
|
|
|
616
|
-
const proposal = this.deltaToProposal(delta,
|
|
555
|
+
const proposal = this.deltaToProposal(delta, commitment, undefined, existingProposal?.signatures);
|
|
617
556
|
|
|
618
557
|
if (existingProposal?.metadata) {
|
|
619
558
|
proposal.metadata = existingProposal.metadata;
|
|
@@ -621,18 +560,42 @@ export class Multisig {
|
|
|
621
560
|
|
|
622
561
|
this.proposals.set(proposal.id, proposal);
|
|
623
562
|
|
|
624
|
-
return
|
|
563
|
+
return this.syncTransactionProposals();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async signTransactionProposalExternal(
|
|
567
|
+
params: SignTransactionProposalParams,
|
|
568
|
+
): Promise<TransactionProposal[]> {
|
|
569
|
+
const { commitment, signature: signatureHex, publicKey, scheme } = params;
|
|
570
|
+
const resolvedScheme = scheme ?? this.signatureScheme;
|
|
571
|
+
|
|
572
|
+
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 };
|
|
577
|
+
|
|
578
|
+
const delta = await this.psm.signDeltaProposal({
|
|
579
|
+
accountId: this._accountId,
|
|
580
|
+
commitment,
|
|
581
|
+
signature,
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const proposal = this.deltaToProposal(delta, commitment, undefined, existingProposal?.signatures);
|
|
585
|
+
|
|
586
|
+
if (existingProposal?.metadata) {
|
|
587
|
+
proposal.metadata = existingProposal.metadata;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.proposals.set(proposal.id, proposal);
|
|
591
|
+
|
|
592
|
+
return this.syncTransactionProposals();
|
|
625
593
|
}
|
|
626
594
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
*
|
|
630
|
-
* @param proposalId - The proposal commitment/ID
|
|
631
|
-
*/
|
|
632
|
-
async executeProposal(proposalId: string): Promise<void> {
|
|
633
|
-
const proposal = this.proposals.get(proposalId);
|
|
595
|
+
async executeTransactionProposal(commitment: string): Promise<void> {
|
|
596
|
+
const proposal = this.proposals.get(commitment);
|
|
634
597
|
if (!proposal) {
|
|
635
|
-
throw new Error(`Proposal not found: ${
|
|
598
|
+
throw new Error(`Proposal not found: ${commitment}`);
|
|
636
599
|
}
|
|
637
600
|
|
|
638
601
|
const proposalType = proposal.metadata?.proposalType;
|
|
@@ -654,11 +617,11 @@ export class Multisig {
|
|
|
654
617
|
} else {
|
|
655
618
|
const deltas = await this.psm.getDeltaProposals(this._accountId);
|
|
656
619
|
delta = deltas.find(
|
|
657
|
-
(d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) ===
|
|
620
|
+
(d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) === commitment
|
|
658
621
|
);
|
|
659
622
|
|
|
660
623
|
if (!delta) {
|
|
661
|
-
throw new Error(`Proposal not found on server: ${
|
|
624
|
+
throw new Error(`Proposal not found on server: ${commitment}`);
|
|
662
625
|
}
|
|
663
626
|
txSummaryBase64 = delta.deltaPayload.txSummary.data;
|
|
664
627
|
}
|
|
@@ -669,16 +632,38 @@ export class Multisig {
|
|
|
669
632
|
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
670
633
|
|
|
671
634
|
const adviceMap = new AdviceMap();
|
|
635
|
+
const normalizedSignerCommitments = new Set(
|
|
636
|
+
this.signerCommitments.map((c) => normalizeHexWord(c))
|
|
637
|
+
);
|
|
672
638
|
|
|
673
639
|
for (const cosignerSig of proposal.signatures) {
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
);
|
|
676
657
|
const signature = Signature.deserialize(sigBytes);
|
|
677
658
|
const txCommitment = Word.fromHex(normalizeHexWord(txCommitmentHex));
|
|
659
|
+
|
|
660
|
+
const isEcdsa = cosignerSig.signature.scheme === 'ecdsa' && cosignerSig.signature.publicKey;
|
|
678
661
|
const { key, values } = buildSignatureAdviceEntry(
|
|
679
662
|
signerCommitment,
|
|
680
663
|
txCommitment,
|
|
681
|
-
signature
|
|
664
|
+
signature,
|
|
665
|
+
isEcdsa ? cosignerSig.signature.publicKey : undefined,
|
|
666
|
+
isEcdsa ? cosignerSig.signature.signature : undefined,
|
|
682
667
|
);
|
|
683
668
|
adviceMap.insert(key, new FeltArray(values));
|
|
684
669
|
}
|
|
@@ -695,14 +680,27 @@ export class Multisig {
|
|
|
695
680
|
throw new Error('PSM did not return acknowledgment signature');
|
|
696
681
|
}
|
|
697
682
|
|
|
698
|
-
const
|
|
699
|
-
const
|
|
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);
|
|
700
695
|
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
701
696
|
const txCommitmentForAck = Word.fromHex(normalizeHexWord(txCommitmentHex));
|
|
697
|
+
const isAckEcdsa = psmAckScheme === 'ecdsa' && psmAckPubkey;
|
|
702
698
|
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
703
699
|
psmCommitment,
|
|
704
700
|
txCommitmentForAck,
|
|
705
|
-
ackSignature
|
|
701
|
+
ackSignature,
|
|
702
|
+
isAckEcdsa ? psmAckPubkey : undefined,
|
|
703
|
+
isAckEcdsa ? ackSigHex : undefined,
|
|
706
704
|
);
|
|
707
705
|
adviceMap.insert(ackKey, new FeltArray(ackValues));
|
|
708
706
|
}
|
|
@@ -732,7 +730,11 @@ export class Multisig {
|
|
|
732
730
|
const { request } = await buildUpdatePsmTransactionRequest(
|
|
733
731
|
this.webClient,
|
|
734
732
|
metadata.newPsmPubkey,
|
|
735
|
-
{
|
|
733
|
+
{
|
|
734
|
+
salt: Word.fromHex(normalizeHexWord(saltHex)),
|
|
735
|
+
signatureAdviceMap: adviceMap,
|
|
736
|
+
signatureScheme: this.signatureScheme,
|
|
737
|
+
},
|
|
736
738
|
);
|
|
737
739
|
finalRequest = request;
|
|
738
740
|
break;
|
|
@@ -759,7 +761,11 @@ export class Multisig {
|
|
|
759
761
|
this.webClient,
|
|
760
762
|
metadata.targetThreshold,
|
|
761
763
|
metadata.targetSignerCommitments,
|
|
762
|
-
{
|
|
764
|
+
{
|
|
765
|
+
salt: Word.fromHex(normalizeHexWord(saltHex)),
|
|
766
|
+
signatureAdviceMap: adviceMap,
|
|
767
|
+
signatureScheme: this.signatureScheme,
|
|
768
|
+
},
|
|
763
769
|
);
|
|
764
770
|
finalRequest = request;
|
|
765
771
|
break;
|
|
@@ -775,47 +781,13 @@ export class Multisig {
|
|
|
775
781
|
proposal.status = { type: 'finalized' };
|
|
776
782
|
}
|
|
777
783
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
*/
|
|
781
|
-
async exportProposal(proposalId: string): Promise<ExportedProposal> {
|
|
782
|
-
const deltas = await this.psm.getDeltaProposals(this._accountId);
|
|
783
|
-
const delta = deltas.find((d) => computeCommitmentFromTxSummary(d.deltaPayload.txSummary.data) === proposalId);
|
|
784
|
-
|
|
785
|
-
if (!delta) {
|
|
786
|
-
throw new Error(`Proposal not found: ${proposalId}`);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const signatures =
|
|
790
|
-
delta.status.status === 'pending'
|
|
791
|
-
? delta.status.cosignerSigs.map((s) => ({
|
|
792
|
-
commitment: s.signerId,
|
|
793
|
-
signatureHex: s.signature.signature,
|
|
794
|
-
}))
|
|
795
|
-
: [];
|
|
796
|
-
|
|
797
|
-
return {
|
|
798
|
-
accountId: delta.accountId,
|
|
799
|
-
nonce: delta.nonce,
|
|
800
|
-
commitment: proposalId,
|
|
801
|
-
txSummaryBase64: delta.deltaPayload.txSummary.data,
|
|
802
|
-
signatures,
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* Export a proposal to JSON for side-channel sharing.
|
|
808
|
-
*
|
|
809
|
-
* @param proposalId - The proposal commitment/ID
|
|
810
|
-
* @returns JSON string that can be shared and imported by other signers
|
|
811
|
-
*/
|
|
812
|
-
exportProposalToJson(proposalId: string): string {
|
|
813
|
-
const proposal = this.proposals.get(proposalId);
|
|
784
|
+
exportTransactionProposalToJson(commitment: string): string {
|
|
785
|
+
const proposal = this.proposals.get(commitment);
|
|
814
786
|
if (!proposal) {
|
|
815
|
-
throw new Error(`Proposal not found in local cache: ${
|
|
787
|
+
throw new Error(`Proposal not found in local cache: ${commitment}`);
|
|
816
788
|
}
|
|
817
789
|
|
|
818
|
-
const exported:
|
|
790
|
+
const exported: ExportedTransactionProposal = {
|
|
819
791
|
accountId: proposal.accountId,
|
|
820
792
|
nonce: proposal.nonce,
|
|
821
793
|
commitment: proposal.id,
|
|
@@ -831,14 +803,8 @@ export class Multisig {
|
|
|
831
803
|
return JSON.stringify(exported, null, 2);
|
|
832
804
|
}
|
|
833
805
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
*
|
|
837
|
-
* @param json - JSON string from exportProposalToJson
|
|
838
|
-
* @returns The imported proposal
|
|
839
|
-
*/
|
|
840
|
-
importProposal(json: string): Proposal {
|
|
841
|
-
const exported: ExportedProposal = JSON.parse(json);
|
|
806
|
+
importTransactionProposal(json: string): TransactionProposalResult {
|
|
807
|
+
const exported: ExportedTransactionProposal = JSON.parse(json);
|
|
842
808
|
|
|
843
809
|
if (!exported.accountId || !exported.txSummaryBase64 || !exported.commitment) {
|
|
844
810
|
throw new Error('Invalid proposal JSON: missing required fields');
|
|
@@ -860,7 +826,7 @@ export class Multisig {
|
|
|
860
826
|
|
|
861
827
|
const signaturesCollected = exported.signatures.length;
|
|
862
828
|
const signaturesRequired = this.getEffectiveThreshold(metadata.proposalType);
|
|
863
|
-
const status:
|
|
829
|
+
const status: TransactionProposalStatus = signaturesCollected >= signaturesRequired
|
|
864
830
|
? { type: 'ready' }
|
|
865
831
|
: {
|
|
866
832
|
type: 'pending',
|
|
@@ -869,38 +835,32 @@ export class Multisig {
|
|
|
869
835
|
signers: exported.signatures.map((s) => s.commitment),
|
|
870
836
|
};
|
|
871
837
|
|
|
872
|
-
const proposal:
|
|
838
|
+
const proposal: TransactionProposal = {
|
|
873
839
|
id: exported.commitment,
|
|
840
|
+
commitment: exported.commitment,
|
|
874
841
|
accountId: exported.accountId,
|
|
875
842
|
nonce: exported.nonce,
|
|
876
843
|
status,
|
|
877
844
|
txSummary: exported.txSummaryBase64,
|
|
878
845
|
signatures: exported.signatures.map((s) => ({
|
|
879
846
|
signerId: s.commitment,
|
|
880
|
-
signature: { scheme:
|
|
847
|
+
signature: { scheme: this.signer.scheme, signature: s.signatureHex },
|
|
881
848
|
timestamp: s.timestamp || new Date().toISOString(),
|
|
882
849
|
})),
|
|
883
850
|
metadata,
|
|
884
851
|
};
|
|
885
852
|
|
|
886
853
|
this.proposals.set(proposal.id, proposal);
|
|
887
|
-
|
|
888
|
-
return proposal;
|
|
854
|
+
const proposals = this.listTransactionProposals();
|
|
855
|
+
return { proposal, proposals };
|
|
889
856
|
}
|
|
890
857
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
*
|
|
894
|
-
* @param proposalId - The proposal commitment/ID
|
|
895
|
-
* @returns Updated JSON string with the new signature included
|
|
896
|
-
*/
|
|
897
|
-
signProposalOffline(proposalId: string): string {
|
|
898
|
-
const proposal = this.proposals.get(proposalId);
|
|
858
|
+
signTransactionProposalOffline(commitment: string): string {
|
|
859
|
+
const proposal = this.proposals.get(commitment);
|
|
899
860
|
if (!proposal) {
|
|
900
|
-
throw new Error(`Proposal not found: ${
|
|
861
|
+
throw new Error(`Proposal not found: ${commitment}`);
|
|
901
862
|
}
|
|
902
863
|
|
|
903
|
-
// Check if already signed
|
|
904
864
|
const alreadySigned = proposal.signatures.some(
|
|
905
865
|
(s) => s.signerId.toLowerCase() === this.signer.commitment.toLowerCase()
|
|
906
866
|
);
|
|
@@ -908,17 +868,17 @@ export class Multisig {
|
|
|
908
868
|
throw new Error('You have already signed this proposal');
|
|
909
869
|
}
|
|
910
870
|
|
|
911
|
-
|
|
912
|
-
const signatureHex = this.signer.signCommitment(proposalId);
|
|
871
|
+
const signatureHex = this.signer.signCommitment(commitment);
|
|
913
872
|
|
|
914
|
-
|
|
873
|
+
const sigEntry: TransactionProposalSignature['signature'] = this.signer.scheme === 'ecdsa'
|
|
874
|
+
? { scheme: 'ecdsa', signature: signatureHex, publicKey: this.signer.publicKey }
|
|
875
|
+
: { scheme: 'falcon', signature: signatureHex };
|
|
915
876
|
proposal.signatures.push({
|
|
916
877
|
signerId: this.signer.commitment,
|
|
917
|
-
signature:
|
|
878
|
+
signature: sigEntry,
|
|
918
879
|
timestamp: new Date().toISOString(),
|
|
919
880
|
});
|
|
920
881
|
|
|
921
|
-
// Update status
|
|
922
882
|
const signaturesCollected = proposal.signatures.length;
|
|
923
883
|
const proposalType = proposal.metadata?.proposalType;
|
|
924
884
|
const effectiveThreshold = proposalType
|
|
@@ -936,16 +896,28 @@ export class Multisig {
|
|
|
936
896
|
};
|
|
937
897
|
}
|
|
938
898
|
|
|
939
|
-
|
|
940
|
-
|
|
899
|
+
return this.exportTransactionProposalToJson(commitment);
|
|
900
|
+
}
|
|
901
|
+
|
|
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 };
|
|
941
913
|
}
|
|
942
914
|
|
|
943
915
|
private deltaToProposal(
|
|
944
916
|
delta: DeltaObject,
|
|
945
917
|
proposalId: string,
|
|
946
918
|
metadata?: ProposalMetadata,
|
|
947
|
-
existingSignatures?:
|
|
948
|
-
):
|
|
919
|
+
existingSignatures?: TransactionProposalSignature[],
|
|
920
|
+
): TransactionProposal {
|
|
949
921
|
const resolvedMetadata: ProposalMetadata | undefined =
|
|
950
922
|
metadata ??
|
|
951
923
|
(delta.deltaPayload.metadata ? this.fromPsmMetadata(delta.deltaPayload.metadata) : undefined);
|
|
@@ -964,7 +936,7 @@ export class Multisig {
|
|
|
964
936
|
}))
|
|
965
937
|
: [];
|
|
966
938
|
|
|
967
|
-
const signaturesMap = new Map<string,
|
|
939
|
+
const signaturesMap = new Map<string, TransactionProposalSignature>();
|
|
968
940
|
for (const sig of existingSignatures ?? []) {
|
|
969
941
|
signaturesMap.set(sig.signerId, sig);
|
|
970
942
|
}
|
|
@@ -975,6 +947,7 @@ export class Multisig {
|
|
|
975
947
|
|
|
976
948
|
return {
|
|
977
949
|
id: proposalId,
|
|
950
|
+
commitment: proposalId,
|
|
978
951
|
accountId: delta.accountId,
|
|
979
952
|
nonce: delta.nonce,
|
|
980
953
|
status,
|
|
@@ -1070,7 +1043,7 @@ export class Multisig {
|
|
|
1070
1043
|
}
|
|
1071
1044
|
}
|
|
1072
1045
|
|
|
1073
|
-
private deltaStatusToProposalStatus(status: DeltaStatus, proposalType?: ProposalType):
|
|
1046
|
+
private deltaStatusToProposalStatus(status: DeltaStatus, proposalType?: ProposalType): TransactionProposalStatus {
|
|
1074
1047
|
switch (status.status) {
|
|
1075
1048
|
case 'pending': {
|
|
1076
1049
|
const signaturesCollected = status.cosignerSigs.length;
|