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