@openzeppelin/miden-multisig-client 0.13.1 → 0.13.2
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/src/multisig.ts
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
|
|
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
|
+
|
|
8
|
+
import { GuardianHttpClient, type DeltaObject, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
|
|
2
9
|
import type {
|
|
3
10
|
ConsumableNote,
|
|
4
|
-
|
|
11
|
+
ExportedProposal,
|
|
5
12
|
MultisigConfig,
|
|
6
13
|
NoteAsset,
|
|
7
|
-
|
|
14
|
+
Proposal,
|
|
8
15
|
ProposalMetadata,
|
|
9
|
-
TransactionProposalStatus,
|
|
10
16
|
ProposalType,
|
|
11
|
-
SignTransactionProposalParams,
|
|
12
|
-
SyncResult,
|
|
13
|
-
TransactionProposalResult,
|
|
14
17
|
} from './types.js';
|
|
15
18
|
import type { ProcedureName } from './procedures.js';
|
|
16
|
-
import { AccountInspector, type DetectedMultisigConfig } from './inspector.js';
|
|
17
19
|
import type { WebClient, TransactionRequest } from '@miden-sdk/miden-sdk';
|
|
18
20
|
import {
|
|
19
21
|
Account,
|
|
20
22
|
AccountId,
|
|
23
|
+
AdviceMap,
|
|
24
|
+
Endpoint,
|
|
25
|
+
FeltArray,
|
|
26
|
+
RpcClient,
|
|
27
|
+
Signature,
|
|
28
|
+
TransactionSummary,
|
|
29
|
+
Word,
|
|
21
30
|
} from '@miden-sdk/miden-sdk';
|
|
22
31
|
import {
|
|
23
32
|
executeForSummary,
|
|
24
33
|
buildUpdateSignersTransactionRequest,
|
|
25
|
-
|
|
34
|
+
buildUpdateProcedureThresholdTransactionRequest,
|
|
35
|
+
buildUpdateGuardianTransactionRequest,
|
|
26
36
|
buildConsumeNotesTransactionRequest,
|
|
27
37
|
buildP2idTransactionRequest,
|
|
28
38
|
} from './transaction.js';
|
|
@@ -31,84 +41,126 @@ import {
|
|
|
31
41
|
uint8ArrayToBase64,
|
|
32
42
|
normalizeHexWord,
|
|
33
43
|
} from './utils/encoding.js';
|
|
34
|
-
import { signatureHexToBytes } from './utils/signature.js';
|
|
35
|
-
import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
|
|
36
|
-
import {
|
|
37
|
-
buildPsmMetadata,
|
|
38
|
-
deltaToProposal,
|
|
39
|
-
resolveMetadata,
|
|
40
|
-
signatureRequirementForProposal,
|
|
41
|
-
} from './multisig/proposal/parser.js';
|
|
42
44
|
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
buildSignatureAdviceEntry,
|
|
46
|
+
normalizeSignerCommitment,
|
|
47
|
+
signatureHexToBytes,
|
|
48
|
+
tryComputeEcdsaCommitmentHex,
|
|
49
|
+
} from './utils/signature.js';
|
|
50
|
+
import { computeCommitmentFromTxSummary, accountIdToHex } from './multisig/helpers.js';
|
|
51
|
+
import { buildGuardianSignatureFromSigner } from './multisig/signing.js';
|
|
52
|
+
import { AccountInspector } from './inspector.js';
|
|
53
|
+
import { ProposalFactory } from './proposal/factory.js';
|
|
54
|
+
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
55
|
+
import { ProposalSignatures } from './proposal/signatures.js';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Result of fetching account state from GUARDIAN.
|
|
59
|
+
*/
|
|
48
60
|
export interface AccountState {
|
|
61
|
+
/** Account ID */
|
|
49
62
|
accountId: string;
|
|
63
|
+
/** Current commitment */
|
|
50
64
|
commitment: string;
|
|
65
|
+
/** Raw state data (base64-encoded serialized account) */
|
|
51
66
|
stateDataBase64: string;
|
|
52
67
|
createdAt: string;
|
|
53
68
|
updatedAt: string;
|
|
54
|
-
authScheme?: string;
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
export interface AccountStateVerificationResult {
|
|
72
|
+
accountId: string;
|
|
73
|
+
localCommitment: string;
|
|
74
|
+
onChainCommitment: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Represents a multisig account with GUARDIAN integration.
|
|
79
|
+
*/
|
|
57
80
|
export class Multisig {
|
|
58
|
-
account: Account
|
|
81
|
+
account: Account;
|
|
59
82
|
threshold: number;
|
|
60
83
|
signerCommitments: string[];
|
|
61
|
-
|
|
62
|
-
psmPublicKey?: string;
|
|
84
|
+
guardianCommitment: string;
|
|
63
85
|
procedureThresholds: Map<ProcedureName, number>;
|
|
64
|
-
|
|
86
|
+
guardianPublicKey?: string;
|
|
65
87
|
|
|
66
|
-
private
|
|
88
|
+
private guardian: GuardianHttpClient;
|
|
67
89
|
private readonly signer: Signer;
|
|
68
90
|
private readonly webClient: WebClient;
|
|
69
91
|
private readonly _accountId: string;
|
|
70
|
-
private
|
|
92
|
+
private readonly midenRpcEndpoint?: string;
|
|
93
|
+
private proposals: Map<string, Proposal> = new Map();
|
|
71
94
|
|
|
72
95
|
constructor(
|
|
73
|
-
account: Account
|
|
96
|
+
account: Account,
|
|
74
97
|
config: MultisigConfig,
|
|
75
|
-
|
|
98
|
+
guardian: GuardianHttpClient,
|
|
76
99
|
signer: Signer,
|
|
77
100
|
webClient: WebClient,
|
|
78
|
-
accountId?: string
|
|
101
|
+
accountId?: string,
|
|
102
|
+
midenRpcEndpoint?: string
|
|
79
103
|
) {
|
|
80
|
-
if (config.signatureScheme && config.signatureScheme !== signer.scheme) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`signature scheme mismatch: config=${config.signatureScheme} signer=${signer.scheme}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
104
|
this.account = account;
|
|
86
105
|
this.threshold = config.threshold;
|
|
87
106
|
this.signerCommitments = config.signerCommitments;
|
|
88
|
-
this.
|
|
89
|
-
this.
|
|
90
|
-
this.signatureScheme = config.signatureScheme ?? signer.scheme;
|
|
107
|
+
this.guardianCommitment = config.guardianCommitment;
|
|
108
|
+
this.guardianPublicKey = config.guardianPublicKey;
|
|
91
109
|
this.procedureThresholds = new Map(
|
|
92
110
|
(config.procedureThresholds ?? []).map((pt) => [pt.procedure, pt.threshold])
|
|
93
111
|
);
|
|
94
|
-
this.
|
|
112
|
+
this.guardian = guardian;
|
|
95
113
|
this.signer = signer;
|
|
96
114
|
this.webClient = webClient;
|
|
97
115
|
this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
|
|
116
|
+
this.midenRpcEndpoint = midenRpcEndpoint;
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
private getMidenRpcEndpoint(): string {
|
|
120
|
+
if (!this.midenRpcEndpoint) {
|
|
121
|
+
throw new Error('Missing Miden RPC endpoint in MultisigClient configuration');
|
|
122
|
+
}
|
|
123
|
+
return this.midenRpcEndpoint;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private proposalFactory(): ProposalFactory {
|
|
127
|
+
return new ProposalFactory({
|
|
128
|
+
accountId: this._accountId,
|
|
129
|
+
signerCommitments: this.signerCommitments,
|
|
130
|
+
resolveRequiredSignatures: (proposalType) => this.getEffectiveThreshold(proposalType),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async verifyGuardianEndpointCommitment(endpoint: string | undefined, expectedCommitment: string): Promise<void> {
|
|
135
|
+
if (!endpoint) {
|
|
136
|
+
throw new Error('Switch GUARDIAN proposal missing newGuardianEndpoint');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const endpointClient = new GuardianHttpClient(endpoint);
|
|
140
|
+
const fetchedPubkey = await endpointClient.getPubkey(this.signer.scheme);
|
|
141
|
+
const endpointCommitment = normalizeHexWord(fetchedPubkey.commitment);
|
|
142
|
+
const normalizedExpected = normalizeHexWord(expectedCommitment);
|
|
143
|
+
|
|
144
|
+
if (endpointCommitment !== normalizedExpected) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Refusing to use GUARDIAN endpoint ${endpoint}: endpoint pubkey commitment ${endpointCommitment} does not match expected ${normalizedExpected}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The account ID as a string */
|
|
100
152
|
get accountId(): string {
|
|
101
153
|
return this._accountId;
|
|
102
154
|
}
|
|
103
155
|
|
|
156
|
+
/** The signer's commitment */
|
|
104
157
|
get signerCommitment(): string {
|
|
105
158
|
return this.signer.commitment;
|
|
106
159
|
}
|
|
107
160
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Maps a proposal type to the procedure that determines its threshold.
|
|
163
|
+
*/
|
|
112
164
|
private getProposalProcedure(proposalType: ProposalType): ProcedureName | null {
|
|
113
165
|
switch (proposalType) {
|
|
114
166
|
case 'p2id':
|
|
@@ -119,13 +171,22 @@ export class Multisig {
|
|
|
119
171
|
case 'remove_signer':
|
|
120
172
|
case 'change_threshold':
|
|
121
173
|
return 'update_signers';
|
|
122
|
-
case '
|
|
123
|
-
return '
|
|
174
|
+
case 'update_procedure_threshold':
|
|
175
|
+
return 'update_procedure_threshold';
|
|
176
|
+
case 'switch_guardian':
|
|
177
|
+
return 'update_guardian';
|
|
124
178
|
default:
|
|
125
179
|
return null;
|
|
126
180
|
}
|
|
127
181
|
}
|
|
128
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Get the effective threshold for a given proposal type.
|
|
185
|
+
* Returns the procedure-specific threshold if configured, otherwise the default threshold.
|
|
186
|
+
*
|
|
187
|
+
* @param proposalType - The type of proposal
|
|
188
|
+
* @returns The threshold that applies to this proposal type
|
|
189
|
+
*/
|
|
129
190
|
getEffectiveThreshold(proposalType: ProposalType): number {
|
|
130
191
|
if (this.procedureThresholds.size === 0) {
|
|
131
192
|
return this.threshold;
|
|
@@ -139,13 +200,23 @@ export class Multisig {
|
|
|
139
200
|
return this.procedureThresholds.get(procedure) ?? this.threshold;
|
|
140
201
|
}
|
|
141
202
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Update the GUARDIAN client used by this Multisig instance.
|
|
205
|
+
*
|
|
206
|
+
* @param guardianClient - The new GUARDIAN HTTP client
|
|
207
|
+
*/
|
|
208
|
+
setGuardianClient(guardianClient: GuardianHttpClient): void {
|
|
209
|
+
this.guardian = guardianClient;
|
|
210
|
+
this.guardian.setSigner(this.signer);
|
|
145
211
|
}
|
|
146
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Fetch the current account state from GUARDIAN.
|
|
215
|
+
*
|
|
216
|
+
* @returns The account state including commitment and serialized data
|
|
217
|
+
*/
|
|
147
218
|
async fetchState(): Promise<AccountState> {
|
|
148
|
-
const state: StateObject = await this.
|
|
219
|
+
const state: StateObject = await this.guardian.getState(this._accountId);
|
|
149
220
|
|
|
150
221
|
return {
|
|
151
222
|
accountId: state.accountId,
|
|
@@ -153,246 +224,305 @@ export class Multisig {
|
|
|
153
224
|
stateDataBase64: state.stateJson.data,
|
|
154
225
|
createdAt: state.createdAt,
|
|
155
226
|
updatedAt: state.updatedAt,
|
|
156
|
-
authScheme: state.authScheme,
|
|
157
227
|
};
|
|
158
228
|
}
|
|
159
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Sync account state from GUARDIAN into the local WebClient store.
|
|
232
|
+
*
|
|
233
|
+
* If the GUARDIAN commitment differs from the local commitment (or the account
|
|
234
|
+
* is missing locally), the local store is overwritten with the GUARDIAN state.
|
|
235
|
+
*/
|
|
160
236
|
async syncState(): Promise<AccountState> {
|
|
161
237
|
const state = await this.fetchState();
|
|
162
238
|
const accountId = AccountId.fromHex(this._accountId);
|
|
163
239
|
const localAccount = await this.webClient.getAccount(accountId);
|
|
240
|
+
let accountForConfigRefresh: Account | null = localAccount ?? null;
|
|
164
241
|
|
|
165
|
-
const
|
|
242
|
+
const guardianCommitment = normalizeHexWord(state.commitment);
|
|
166
243
|
const localCommitment = localAccount
|
|
167
244
|
? normalizeHexWord(localAccount.commitment().toHex())
|
|
168
245
|
: null;
|
|
169
246
|
|
|
170
|
-
if (!localAccount || localCommitment !==
|
|
247
|
+
if (!localAccount || localCommitment !== guardianCommitment) {
|
|
171
248
|
const accountBytes = base64ToUint8Array(state.stateDataBase64);
|
|
172
|
-
const
|
|
173
|
-
await this.
|
|
249
|
+
const incomingAccount = Account.deserialize(accountBytes);
|
|
250
|
+
await this.ensureSafeToOverwriteLocalState(incomingAccount, localAccount);
|
|
251
|
+
await this.webClient.newAccount(incomingAccount, true);
|
|
252
|
+
accountForConfigRefresh = incomingAccount;
|
|
174
253
|
}
|
|
175
254
|
|
|
255
|
+
this.refreshConfigFromAccount(accountForConfigRefresh);
|
|
256
|
+
|
|
176
257
|
return state;
|
|
177
258
|
}
|
|
178
259
|
|
|
179
|
-
async
|
|
180
|
-
|
|
181
|
-
|
|
260
|
+
async verifyStateCommitment(): Promise<AccountStateVerificationResult> {
|
|
261
|
+
const accountId = AccountId.fromHex(this._accountId);
|
|
262
|
+
const localAccount = await this.webClient.getAccount(accountId);
|
|
263
|
+
|
|
264
|
+
if (!localAccount) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Local account state not found for account ${this._accountId}. Sync the account before verifying.`
|
|
267
|
+
);
|
|
182
268
|
}
|
|
183
269
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
stateData = uint8ArrayToBase64(accountBytes);
|
|
270
|
+
const localCommitment = normalizeHexWord(localAccount.commitment().toHex());
|
|
271
|
+
const onChainCommitment = await this.getOnChainCommitment(accountId);
|
|
272
|
+
|
|
273
|
+
if (!onChainCommitment) {
|
|
274
|
+
throw new Error(`On-chain account details not found for account ${this._accountId}`);
|
|
190
275
|
}
|
|
191
276
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
277
|
+
if (localCommitment !== onChainCommitment) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Local account commitment does not match on-chain commitment for account ${this._accountId}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
195
282
|
|
|
196
|
-
|
|
283
|
+
return {
|
|
197
284
|
accountId: this._accountId,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
285
|
+
localCommitment,
|
|
286
|
+
onChainCommitment,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
201
289
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
290
|
+
private async ensureSafeToOverwriteLocalState(
|
|
291
|
+
incomingAccount: Account,
|
|
292
|
+
localAccount?: Account,
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
if (localAccount) {
|
|
295
|
+
const localNonce = localAccount.nonce().asInt();
|
|
296
|
+
const incomingNonce = incomingAccount.nonce().asInt();
|
|
205
297
|
|
|
206
|
-
|
|
207
|
-
const onChain = normalizeHexWord(this.psmCommitment);
|
|
208
|
-
const server = normalizeHexWord(response.ackCommitment);
|
|
209
|
-
if (onChain !== server) {
|
|
298
|
+
if (incomingNonce <= localNonce) {
|
|
210
299
|
throw new Error(
|
|
211
|
-
`
|
|
300
|
+
`Refusing to overwrite local state: incoming nonce ${incomingNonce.toString()} is not greater than local nonce ${localNonce.toString()} for account ${this._accountId}`
|
|
212
301
|
);
|
|
213
302
|
}
|
|
214
303
|
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async syncAll(): Promise<SyncResult> {
|
|
218
|
-
const proposals = await this.syncTransactionProposals();
|
|
219
|
-
const state = await this.syncState();
|
|
220
|
-
const notes = await this.getConsumableNotes();
|
|
221
|
-
const config = AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
|
|
222
304
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
305
|
+
const accountId = AccountId.fromHex(this._accountId);
|
|
306
|
+
const onChainCommitment = await this.getOnChainCommitment(accountId);
|
|
307
|
+
if (!onChainCommitment) {
|
|
308
|
+
return;
|
|
227
309
|
}
|
|
228
|
-
this.procedureThresholds = config.procedureThresholds;
|
|
229
310
|
|
|
230
|
-
|
|
311
|
+
const incomingCommitment = normalizeHexWord(incomingAccount.commitment().toHex());
|
|
312
|
+
if (incomingCommitment !== onChainCommitment) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Refusing to overwrite local state: incoming commitment does not match on-chain commitment for account ${this._accountId}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
231
317
|
}
|
|
232
318
|
|
|
233
|
-
async
|
|
234
|
-
const
|
|
235
|
-
return AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
|
|
236
|
-
}
|
|
319
|
+
private async getOnChainCommitment(accountId: AccountId): Promise<string | null> {
|
|
320
|
+
const rpcClient = new RpcClient(new Endpoint(this.getMidenRpcEndpoint()));
|
|
237
321
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
322
|
+
try {
|
|
323
|
+
const accountDetails = await rpcClient.getAccountDetails(accountId);
|
|
324
|
+
// If the account is not found or its commitment is zero, means that the account is not deployed yet
|
|
325
|
+
if (!accountDetails) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const commitment = normalizeHexWord(accountDetails.commitment().toHex());
|
|
329
|
+
const zeroCommitment = `0x${'0'.repeat(64)}`;
|
|
330
|
+
if (commitment === zeroCommitment) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return commitment;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
336
|
+
if (
|
|
337
|
+
message.includes('null pointer passed to rust') ||
|
|
338
|
+
message.includes('No account header record found for given ID') ||
|
|
339
|
+
message.toLowerCase().includes('not found')
|
|
340
|
+
) {
|
|
341
|
+
return null;
|
|
246
342
|
}
|
|
247
|
-
|
|
248
|
-
this.signerCommitments = config.signerCommitments;
|
|
249
|
-
this.procedureThresholds = config.procedureThresholds;
|
|
343
|
+
throw error;
|
|
250
344
|
}
|
|
345
|
+
}
|
|
251
346
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
347
|
+
private refreshConfigFromAccount(account: Account | null): void {
|
|
348
|
+
if (!account) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
256
351
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
352
|
+
try {
|
|
353
|
+
const detected = AccountInspector.fromAccount(account);
|
|
354
|
+
this.account = account;
|
|
355
|
+
this.threshold = detected.threshold;
|
|
356
|
+
this.signerCommitments = detected.signerCommitments;
|
|
357
|
+
if (detected.guardianCommitment) {
|
|
358
|
+
this.guardianCommitment = detected.guardianCommitment;
|
|
359
|
+
}
|
|
360
|
+
this.procedureThresholds = new Map(detected.procedureThresholds);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn('Failed to refresh multisig config from account state', error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
260
365
|
|
|
261
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Register this multisig account on the GUARDIAN server.
|
|
368
|
+
*
|
|
369
|
+
* The initial state must be the serialized Account bytes (base64-encoded).
|
|
370
|
+
* If not provided, the account's serialize() method is used.
|
|
371
|
+
*
|
|
372
|
+
* @param initialStateBase64 - Optional base64-encoded serialized Account.¡
|
|
373
|
+
*/
|
|
374
|
+
async registerOnGuardian(initialStateBase64?: string): Promise<void> {
|
|
375
|
+
// Serialize the account to bytes and base64-encode
|
|
376
|
+
const stateData =
|
|
377
|
+
initialStateBase64 ?? uint8ArrayToBase64(this.account.serialize());
|
|
378
|
+
|
|
379
|
+
const auth: AuthConfig =
|
|
380
|
+
this.signer.scheme === 'ecdsa'
|
|
381
|
+
? {
|
|
382
|
+
MidenEcdsa: {
|
|
383
|
+
cosigner_commitments: this.signerCommitments,
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
: {
|
|
387
|
+
MidenFalconRpo: {
|
|
388
|
+
cosigner_commitments: this.signerCommitments,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const response = await this.guardian.configure({
|
|
262
393
|
accountId: this._accountId,
|
|
263
394
|
auth,
|
|
264
395
|
initialState: { data: stateData, accountId: this._accountId },
|
|
265
396
|
});
|
|
266
397
|
|
|
267
398
|
if (!response.success) {
|
|
268
|
-
throw new Error(`Failed to register on
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (response.ackCommitment) {
|
|
272
|
-
this.psmCommitment = normalizeHexWord(response.ackCommitment);
|
|
273
|
-
}
|
|
274
|
-
if (response.ackPubkey) {
|
|
275
|
-
this.psmPublicKey = response.ackPubkey;
|
|
399
|
+
throw new Error(`Failed to register on GUARDIAN: ${response.message}`);
|
|
276
400
|
}
|
|
277
401
|
}
|
|
278
402
|
|
|
279
|
-
|
|
280
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Sync proposals from the GUARDIAN server.
|
|
405
|
+
*/
|
|
406
|
+
async syncProposals(): Promise<Proposal[]> {
|
|
407
|
+
const deltas = await this.guardian.getDeltaProposals(this._accountId);
|
|
408
|
+
const factory = this.proposalFactory();
|
|
281
409
|
|
|
282
|
-
const serverProposalIds = new Set<string>();
|
|
283
410
|
for (const delta of deltas) {
|
|
284
|
-
const proposalId =
|
|
285
|
-
|
|
411
|
+
const proposalId = normalizeHexWord(
|
|
412
|
+
computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data)
|
|
413
|
+
);
|
|
286
414
|
const existingProposal = this.proposals.get(proposalId);
|
|
287
|
-
|
|
288
|
-
const resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
|
|
289
|
-
if (!resolvedMetadata) {
|
|
290
|
-
throw new Error('Missing proposal metadata from PSM');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const proposal = deltaToProposal({
|
|
415
|
+
const proposal = factory.fromDelta(
|
|
294
416
|
delta,
|
|
295
417
|
proposalId,
|
|
296
|
-
metadata
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
(proposalType) => this.getEffectiveThreshold(proposalType),
|
|
301
|
-
),
|
|
302
|
-
existingSignatures: existingProposal?.signatures,
|
|
303
|
-
});
|
|
418
|
+
existingProposal?.metadata,
|
|
419
|
+
existingProposal?.signatures ?? [],
|
|
420
|
+
);
|
|
421
|
+
await this.verifyProposalMetadataBinding(proposal);
|
|
304
422
|
|
|
305
423
|
this.proposals.set(proposal.id, proposal);
|
|
306
424
|
}
|
|
307
425
|
|
|
308
|
-
for (const [id, proposal] of this.proposals) {
|
|
309
|
-
if (serverProposalIds.has(id)) continue;
|
|
310
|
-
const isLocalOnly = proposal.metadata?.proposalType === 'switch_psm' && proposal.status.type !== 'finalized';
|
|
311
|
-
if (!isLocalOnly) {
|
|
312
|
-
this.proposals.delete(id);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
426
|
return Array.from(this.proposals.values());
|
|
317
427
|
}
|
|
318
428
|
|
|
319
|
-
|
|
429
|
+
/**
|
|
430
|
+
* List all known proposals
|
|
431
|
+
*/
|
|
432
|
+
listProposals(): Proposal[] {
|
|
320
433
|
return Array.from(this.proposals.values());
|
|
321
434
|
}
|
|
322
435
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Create a new proposal.
|
|
438
|
+
*
|
|
439
|
+
* @param nonce - The nonce for this transaction
|
|
440
|
+
* @param txSummaryBase64 - Base64-encoded transaction summary
|
|
441
|
+
* @param metadata - Optional metadata for execution (target config, salt, etc.)
|
|
442
|
+
*/
|
|
443
|
+
async createProposal(nonce: number, txSummaryBase64: string, metadata: ProposalMetadata): Promise<Proposal> {
|
|
444
|
+
const guardianMetadata = ProposalMetadataCodec.toGuardian(metadata);
|
|
445
|
+
|
|
446
|
+
const response = await this.guardian.pushDeltaProposal({
|
|
327
447
|
accountId: this._accountId,
|
|
328
448
|
nonce,
|
|
329
449
|
deltaPayload: {
|
|
330
450
|
txSummary: { data: txSummaryBase64 },
|
|
331
451
|
signatures: [],
|
|
332
|
-
metadata:
|
|
452
|
+
metadata: guardianMetadata,
|
|
333
453
|
},
|
|
334
454
|
});
|
|
335
455
|
|
|
336
|
-
const proposal =
|
|
337
|
-
|
|
338
|
-
proposalId: response.commitment,
|
|
339
|
-
metadata,
|
|
340
|
-
signaturesRequired: signatureRequirementForProposal(
|
|
341
|
-
metadata,
|
|
342
|
-
this.threshold,
|
|
343
|
-
(proposalType) => this.getEffectiveThreshold(proposalType),
|
|
344
|
-
),
|
|
345
|
-
});
|
|
456
|
+
const proposal = this.proposalFactory().fromDelta(response.delta, response.commitment, metadata);
|
|
457
|
+
await this.verifyProposalMetadataBinding(proposal);
|
|
346
458
|
this.proposals.set(proposal.id, proposal);
|
|
347
459
|
|
|
348
460
|
return proposal;
|
|
349
461
|
}
|
|
350
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Create an "add signer" proposal.
|
|
465
|
+
*
|
|
466
|
+
* @param newCommitment - Commitment of the new signer (hex)
|
|
467
|
+
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
468
|
+
* @param newThreshold - Optional new threshold (defaults to current threshold)
|
|
469
|
+
*/
|
|
351
470
|
async createAddSignerProposal(
|
|
352
471
|
newCommitment: string,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
472
|
+
nonce?: number,
|
|
473
|
+
newThreshold?: number,
|
|
474
|
+
): Promise<Proposal> {
|
|
475
|
+
const targetThreshold = newThreshold ?? this.threshold;
|
|
356
476
|
const targetSignerCommitments = [...this.signerCommitments, newCommitment];
|
|
357
477
|
|
|
358
478
|
const { request, salt } = await buildUpdateSignersTransactionRequest(
|
|
359
479
|
this.webClient,
|
|
360
480
|
targetThreshold,
|
|
361
481
|
targetSignerCommitments,
|
|
362
|
-
{ signatureScheme: this.
|
|
482
|
+
{ signatureScheme: this.signer.scheme },
|
|
363
483
|
);
|
|
364
484
|
|
|
485
|
+
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
486
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
487
|
+
const proposalNonce = nonce ?? Date.now();
|
|
488
|
+
|
|
365
489
|
const metadata: ProposalMetadata = {
|
|
366
490
|
proposalType: 'add_signer',
|
|
367
491
|
targetThreshold,
|
|
368
492
|
targetSignerCommitments,
|
|
369
493
|
saltHex: salt.toHex(),
|
|
494
|
+
requiredSignatures: this.getEffectiveThreshold('add_signer'),
|
|
370
495
|
description: `Add signer ${newCommitment.slice(0, 10)}...`,
|
|
371
496
|
};
|
|
372
|
-
|
|
497
|
+
|
|
498
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
373
499
|
}
|
|
374
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Create a "remove signer" proposal by executing the update_signers script to summary.
|
|
503
|
+
*
|
|
504
|
+
* @param signerToRemove - Commitment of the signer to remove (hex)
|
|
505
|
+
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
506
|
+
* @param newThreshold - Optional new threshold (defaults to min of current threshold and new signer count)
|
|
507
|
+
*/
|
|
375
508
|
async createRemoveSignerProposal(
|
|
376
509
|
signerToRemove: string,
|
|
377
|
-
|
|
378
|
-
|
|
510
|
+
nonce?: number,
|
|
511
|
+
newThreshold?: number,
|
|
512
|
+
): Promise<Proposal> {
|
|
379
513
|
const normalizedRemove = signerToRemove.toLowerCase();
|
|
380
|
-
const signerExists = this.signerCommitments.some(
|
|
381
|
-
(c) => c.toLowerCase() === normalizedRemove
|
|
382
|
-
);
|
|
383
|
-
if (!signerExists) {
|
|
384
|
-
throw new Error(`Signer ${signerToRemove} is not in the current signer list`);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
514
|
const targetSignerCommitments = this.signerCommitments.filter(
|
|
388
515
|
(c) => c.toLowerCase() !== normalizedRemove
|
|
389
516
|
);
|
|
517
|
+
if (targetSignerCommitments.length === this.signerCommitments.length) {
|
|
518
|
+
throw new Error(`Signer ${signerToRemove} is not in the current signer list`);
|
|
519
|
+
}
|
|
390
520
|
|
|
391
521
|
if (targetSignerCommitments.length === 0) {
|
|
392
522
|
throw new Error('Cannot remove the last signer');
|
|
393
523
|
}
|
|
394
524
|
|
|
395
|
-
const targetThreshold =
|
|
525
|
+
const targetThreshold = newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
|
|
396
526
|
|
|
397
527
|
if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
|
|
398
528
|
throw new Error(
|
|
@@ -404,23 +534,35 @@ export class Multisig {
|
|
|
404
534
|
this.webClient,
|
|
405
535
|
targetThreshold,
|
|
406
536
|
targetSignerCommitments,
|
|
407
|
-
{ signatureScheme: this.
|
|
537
|
+
{ signatureScheme: this.signer.scheme },
|
|
408
538
|
);
|
|
409
539
|
|
|
540
|
+
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
541
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
542
|
+
const proposalNonce = nonce ?? Date.now();
|
|
543
|
+
|
|
410
544
|
const metadata: ProposalMetadata = {
|
|
411
545
|
proposalType: 'remove_signer',
|
|
412
546
|
targetThreshold,
|
|
413
547
|
targetSignerCommitments,
|
|
414
548
|
saltHex: salt.toHex(),
|
|
549
|
+
requiredSignatures: this.getEffectiveThreshold('remove_signer'),
|
|
415
550
|
description: `Remove signer ${signerToRemove.slice(0, 10)}...`,
|
|
416
551
|
};
|
|
417
|
-
|
|
552
|
+
|
|
553
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
418
554
|
}
|
|
419
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Create a "change threshold" proposal.
|
|
558
|
+
*
|
|
559
|
+
* @param newThreshold - The new threshold value
|
|
560
|
+
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
561
|
+
*/
|
|
420
562
|
async createChangeThresholdProposal(
|
|
421
563
|
newThreshold: number,
|
|
422
|
-
|
|
423
|
-
): Promise<
|
|
564
|
+
nonce?: number,
|
|
565
|
+
): Promise<Proposal> {
|
|
424
566
|
if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
|
|
425
567
|
throw new Error(
|
|
426
568
|
`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`
|
|
@@ -435,84 +577,166 @@ export class Multisig {
|
|
|
435
577
|
this.webClient,
|
|
436
578
|
newThreshold,
|
|
437
579
|
this.signerCommitments,
|
|
438
|
-
{ signatureScheme: this.
|
|
580
|
+
{ signatureScheme: this.signer.scheme },
|
|
439
581
|
);
|
|
440
582
|
|
|
583
|
+
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
584
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
585
|
+
const proposalNonce = nonce ?? Date.now();
|
|
586
|
+
|
|
441
587
|
const metadata: ProposalMetadata = {
|
|
442
588
|
proposalType: 'change_threshold',
|
|
443
589
|
targetThreshold: newThreshold,
|
|
444
590
|
targetSignerCommitments: this.signerCommitments,
|
|
445
591
|
saltHex: salt.toHex(),
|
|
592
|
+
requiredSignatures: this.getEffectiveThreshold('change_threshold'),
|
|
446
593
|
description: `Change threshold from ${this.threshold} to ${newThreshold}`,
|
|
447
594
|
};
|
|
448
|
-
|
|
595
|
+
|
|
596
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
449
597
|
}
|
|
450
598
|
|
|
451
|
-
async
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
): Promise<
|
|
456
|
-
|
|
599
|
+
async createUpdateProcedureThresholdProposal(
|
|
600
|
+
targetProcedure: ProcedureName,
|
|
601
|
+
targetThreshold: number,
|
|
602
|
+
nonce?: number,
|
|
603
|
+
): Promise<Proposal> {
|
|
604
|
+
if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const currentOverride = this.procedureThresholds.get(targetProcedure);
|
|
611
|
+
if (targetThreshold === 0 && currentOverride === undefined) {
|
|
612
|
+
throw new Error(`Procedure ${targetProcedure} does not have an override to clear`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (currentOverride !== undefined && currentOverride === targetThreshold) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`Procedure ${targetProcedure} already has threshold override ${targetThreshold}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(
|
|
622
|
+
this.webClient,
|
|
623
|
+
targetProcedure,
|
|
624
|
+
targetThreshold,
|
|
625
|
+
{ signatureScheme: this.signer.scheme },
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
629
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
630
|
+
const proposalNonce = nonce ?? Date.now();
|
|
631
|
+
const action = targetThreshold === 0
|
|
632
|
+
? `Clear threshold override for ${targetProcedure}`
|
|
633
|
+
: `Set ${targetProcedure} threshold override to ${targetThreshold}`;
|
|
634
|
+
|
|
635
|
+
const metadata: ProposalMetadata = {
|
|
636
|
+
proposalType: 'update_procedure_threshold',
|
|
637
|
+
targetProcedure,
|
|
638
|
+
targetThreshold,
|
|
639
|
+
saltHex: salt.toHex(),
|
|
640
|
+
requiredSignatures: this.getEffectiveThreshold('update_procedure_threshold'),
|
|
641
|
+
description: action,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Create a "switch GUARDIAN" proposal to change the GUARDIAN provider.
|
|
649
|
+
*
|
|
650
|
+
* @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
|
|
651
|
+
* @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
|
|
652
|
+
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
653
|
+
*/
|
|
654
|
+
async createSwitchGuardianProposal(
|
|
655
|
+
newGuardianEndpoint: string,
|
|
656
|
+
newGuardianPubkey: string,
|
|
657
|
+
nonce?: number,
|
|
658
|
+
): Promise<Proposal> {
|
|
659
|
+
await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
|
|
660
|
+
|
|
661
|
+
const { request, salt } = await buildUpdateGuardianTransactionRequest(
|
|
457
662
|
this.webClient,
|
|
458
|
-
|
|
459
|
-
{ signatureScheme: this.
|
|
663
|
+
newGuardianPubkey,
|
|
664
|
+
{ signatureScheme: this.signer.scheme },
|
|
460
665
|
);
|
|
461
666
|
|
|
462
667
|
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
463
668
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
464
|
-
const proposalNonce =
|
|
669
|
+
const proposalNonce = nonce ?? Date.now();
|
|
465
670
|
|
|
466
671
|
const metadata: ProposalMetadata = {
|
|
467
|
-
proposalType: '
|
|
672
|
+
proposalType: 'switch_guardian',
|
|
468
673
|
saltHex: salt.toHex(),
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
674
|
+
requiredSignatures: this.getEffectiveThreshold('switch_guardian'),
|
|
675
|
+
newGuardianPubkey,
|
|
676
|
+
newGuardianEndpoint,
|
|
677
|
+
description: `Switch GUARDIAN to ${newGuardianEndpoint}`,
|
|
472
678
|
};
|
|
473
679
|
|
|
474
680
|
const proposalId = computeCommitmentFromTxSummary(summaryBase64);
|
|
475
|
-
const proposal:
|
|
681
|
+
const proposal: Proposal = {
|
|
476
682
|
id: proposalId,
|
|
477
|
-
commitment: proposalId,
|
|
478
683
|
accountId: this._accountId,
|
|
479
684
|
nonce: proposalNonce,
|
|
480
|
-
status:
|
|
685
|
+
status: 'pending',
|
|
481
686
|
txSummary: summaryBase64,
|
|
482
687
|
signatures: [],
|
|
483
688
|
metadata,
|
|
484
689
|
};
|
|
485
690
|
|
|
486
691
|
this.proposals.set(proposal.id, proposal);
|
|
487
|
-
|
|
488
|
-
return { proposal, proposals };
|
|
692
|
+
return proposal;
|
|
489
693
|
}
|
|
490
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Create a "consume notes" proposal to consume notes sent to the multisig account.
|
|
697
|
+
*
|
|
698
|
+
* @param noteIds - IDs of the notes to consume (hex strings)
|
|
699
|
+
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
700
|
+
*/
|
|
491
701
|
async createConsumeNotesProposal(
|
|
492
702
|
noteIds: string[],
|
|
493
|
-
|
|
494
|
-
): Promise<
|
|
703
|
+
nonce?: number,
|
|
704
|
+
): Promise<Proposal> {
|
|
495
705
|
if (noteIds.length === 0) {
|
|
496
706
|
throw new Error('At least one note ID is required');
|
|
497
707
|
}
|
|
498
708
|
|
|
499
709
|
const { request, salt } = await buildConsumeNotesTransactionRequest(this.webClient, noteIds);
|
|
500
710
|
|
|
711
|
+
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
712
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
713
|
+
const proposalNonce = nonce ?? Date.now();
|
|
714
|
+
|
|
501
715
|
const metadata: ProposalMetadata = {
|
|
502
716
|
proposalType: 'consume_notes',
|
|
503
717
|
noteIds,
|
|
504
718
|
saltHex: salt.toHex(),
|
|
719
|
+
requiredSignatures: this.getEffectiveThreshold('consume_notes'),
|
|
505
720
|
description: `Consume ${noteIds.length} note(s)`,
|
|
506
721
|
};
|
|
507
|
-
|
|
722
|
+
|
|
723
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
508
724
|
}
|
|
509
725
|
|
|
510
|
-
|
|
726
|
+
/**
|
|
727
|
+
* Create a P2ID proposal to send funds to another account.
|
|
728
|
+
*
|
|
729
|
+
* @param recipientId - Account ID of the recipient (hex string)
|
|
730
|
+
* @param faucetId - Faucet/token account ID (hex string)
|
|
731
|
+
* @param amount - Amount to send
|
|
732
|
+
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
733
|
+
*/
|
|
734
|
+
async createP2idProposal(
|
|
511
735
|
recipientId: string,
|
|
512
736
|
faucetId: string,
|
|
513
737
|
amount: bigint,
|
|
514
|
-
|
|
515
|
-
): Promise<
|
|
738
|
+
nonce?: number,
|
|
739
|
+
): Promise<Proposal> {
|
|
516
740
|
if (amount <= 0n) {
|
|
517
741
|
throw new Error('Amount must be greater than 0');
|
|
518
742
|
}
|
|
@@ -524,26 +748,42 @@ export class Multisig {
|
|
|
524
748
|
amount,
|
|
525
749
|
);
|
|
526
750
|
|
|
751
|
+
const summary = await executeForSummary(this.webClient, this._accountId, request);
|
|
752
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
753
|
+
const proposalNonce = nonce ?? Date.now();
|
|
754
|
+
|
|
527
755
|
const metadata: ProposalMetadata = {
|
|
528
756
|
proposalType: 'p2id',
|
|
529
757
|
saltHex: salt.toHex(),
|
|
758
|
+
requiredSignatures: this.getEffectiveThreshold('p2id'),
|
|
530
759
|
recipientId,
|
|
531
760
|
faucetId,
|
|
532
761
|
amount: amount.toString(),
|
|
533
|
-
description: `Send ${amount} to ${recipientId.slice(0, 10)}...`,
|
|
762
|
+
description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
|
|
534
763
|
};
|
|
535
|
-
|
|
764
|
+
|
|
765
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
536
766
|
}
|
|
537
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Get notes that can be consumed by this multisig account.
|
|
770
|
+
*
|
|
771
|
+
* Returns a list of notes that are committed on-chain and can be consumed
|
|
772
|
+
* immediately by the multisig account.
|
|
773
|
+
*/
|
|
538
774
|
async getConsumableNotes(): Promise<ConsumableNote[]> {
|
|
539
775
|
const accountId = AccountId.fromHex(this._accountId);
|
|
540
776
|
|
|
777
|
+
// Get consumable notes for this account
|
|
541
778
|
const consumableRecords = await this.webClient.getConsumableNotes(accountId);
|
|
779
|
+
|
|
780
|
+
// Convert to our simplified ConsumableNote type
|
|
542
781
|
const notes: ConsumableNote[] = [];
|
|
543
782
|
for (const record of consumableRecords) {
|
|
544
783
|
const inputNote = record.inputNoteRecord();
|
|
545
784
|
const consumability = record.noteConsumability();
|
|
546
785
|
|
|
786
|
+
// Only include notes that can be consumed now (consumableAfterBlock is undefined/null)
|
|
547
787
|
const canConsumeNow = consumability.some(
|
|
548
788
|
(c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
|
|
549
789
|
c.consumptionStatus().consumableAfterBlock() === undefined
|
|
@@ -554,6 +794,7 @@ export class Multisig {
|
|
|
554
794
|
const details = inputNote.details();
|
|
555
795
|
const fungibleAssets = details.assets().fungibleAssets();
|
|
556
796
|
|
|
797
|
+
// Extract assets
|
|
557
798
|
const assets: NoteAsset[] = [];
|
|
558
799
|
for (const asset of fungibleAssets) {
|
|
559
800
|
assets.push({
|
|
@@ -569,102 +810,304 @@ export class Multisig {
|
|
|
569
810
|
return notes;
|
|
570
811
|
}
|
|
571
812
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Sign a proposal.
|
|
815
|
+
*
|
|
816
|
+
* The proposalId is the tx_summary commitment hex, which is what gets signed.
|
|
817
|
+
* This matches the Rust client behavior where proposal.id == tx_summary.to_commitment().
|
|
818
|
+
*
|
|
819
|
+
* @param proposalId - The proposal commitment/ID (this is also what gets signed)
|
|
820
|
+
*/
|
|
821
|
+
async signProposal(proposalId: string): Promise<Proposal> {
|
|
822
|
+
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
823
|
+
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
824
|
+
if (!existingProposal) {
|
|
825
|
+
throw new Error(`Proposal not found: ${proposalId}`);
|
|
826
|
+
}
|
|
827
|
+
this.proposalFactory().assertAccountId(existingProposal.accountId);
|
|
828
|
+
const factory = this.proposalFactory();
|
|
829
|
+
const proposal = existingProposal;
|
|
830
|
+
|
|
831
|
+
const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
|
|
832
|
+
const signature: ProposalSignature = await buildGuardianSignatureFromSigner(
|
|
833
|
+
this.signer,
|
|
834
|
+
commitmentToSign,
|
|
835
|
+
);
|
|
575
836
|
|
|
576
|
-
const
|
|
837
|
+
const signedDelta = await this.guardian.signDeltaProposal({
|
|
577
838
|
accountId: this._accountId,
|
|
578
|
-
commitment,
|
|
839
|
+
commitment: normalizedProposalId,
|
|
579
840
|
signature,
|
|
580
841
|
});
|
|
581
842
|
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
metadata: resolvedMetadata,
|
|
590
|
-
signaturesRequired: signatureRequirementForProposal(
|
|
591
|
-
resolvedMetadata,
|
|
592
|
-
this.threshold,
|
|
593
|
-
(proposalType) => this.getEffectiveThreshold(proposalType),
|
|
594
|
-
),
|
|
595
|
-
existingSignatures: existingProposal?.signatures,
|
|
596
|
-
});
|
|
843
|
+
const signedProposal = factory.fromDelta(
|
|
844
|
+
signedDelta,
|
|
845
|
+
normalizedProposalId,
|
|
846
|
+
proposal.metadata,
|
|
847
|
+
proposal.signatures,
|
|
848
|
+
);
|
|
849
|
+
await this.verifyProposalMetadataBinding(signedProposal);
|
|
597
850
|
|
|
598
|
-
this.proposals.set(
|
|
851
|
+
this.proposals.set(signedProposal.id, signedProposal);
|
|
599
852
|
|
|
600
|
-
return
|
|
853
|
+
return signedProposal;
|
|
601
854
|
}
|
|
602
855
|
|
|
603
|
-
async
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
856
|
+
private async getProposalForSigning(
|
|
857
|
+
proposalId: string,
|
|
858
|
+
normalizedProposalId: string,
|
|
859
|
+
): Promise<Proposal | undefined> {
|
|
860
|
+
const cachedProposal = this.proposals.get(proposalId);
|
|
861
|
+
if (cachedProposal) {
|
|
862
|
+
return cachedProposal;
|
|
863
|
+
}
|
|
608
864
|
|
|
609
|
-
|
|
610
|
-
|
|
865
|
+
await this.syncProposals();
|
|
866
|
+
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
867
|
+
}
|
|
611
868
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
869
|
+
/**
|
|
870
|
+
* Execute a proposal that has enough signatures.
|
|
871
|
+
*
|
|
872
|
+
* @param proposalId - The proposal commitment/ID
|
|
873
|
+
*/
|
|
874
|
+
async executeProposal(proposalId: string): Promise<void> {
|
|
875
|
+
const proposal = this.proposals.get(proposalId);
|
|
876
|
+
if (!proposal) {
|
|
877
|
+
throw new Error(`Proposal not found: ${proposalId}`);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
await this.verifyProposalMetadataBinding(proposal);
|
|
881
|
+
|
|
882
|
+
const proposalType = proposal.metadata?.proposalType;
|
|
883
|
+
const effectiveThreshold = proposalType
|
|
884
|
+
? this.getEffectiveThreshold(proposalType)
|
|
885
|
+
: this.threshold;
|
|
886
|
+
|
|
887
|
+
const signatureContext = `Invalid proposal signatures for ${proposalId}`;
|
|
888
|
+
const signaturesForExecution = new ProposalSignatures(
|
|
889
|
+
proposal.signatures,
|
|
890
|
+
this.signerCommitments,
|
|
891
|
+
signatureContext,
|
|
892
|
+
).entries();
|
|
617
893
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
throw new Error('Missing proposal metadata');
|
|
894
|
+
if (signaturesForExecution.length < effectiveThreshold) {
|
|
895
|
+
throw new Error('Proposal is not ready for execution. Still pending signatures.');
|
|
621
896
|
}
|
|
622
|
-
const proposal = deltaToProposal({
|
|
623
|
-
delta,
|
|
624
|
-
proposalId: commitment,
|
|
625
|
-
metadata: resolvedMetadata,
|
|
626
|
-
signaturesRequired: signatureRequirementForProposal(
|
|
627
|
-
resolvedMetadata,
|
|
628
|
-
this.threshold,
|
|
629
|
-
(proposalType) => this.getEffectiveThreshold(proposalType),
|
|
630
|
-
),
|
|
631
|
-
existingSignatures: existingProposal?.signatures,
|
|
632
|
-
});
|
|
633
897
|
|
|
634
|
-
|
|
898
|
+
const isSwitchGuardian = proposalType === 'switch_guardian';
|
|
635
899
|
|
|
636
|
-
|
|
637
|
-
|
|
900
|
+
let txSummaryBase64: string;
|
|
901
|
+
let delta: DeltaObject | undefined;
|
|
638
902
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
903
|
+
if (isSwitchGuardian) {
|
|
904
|
+
txSummaryBase64 = proposal.txSummary;
|
|
905
|
+
} else {
|
|
906
|
+
delta = await this.guardian.getDeltaProposal(this._accountId, proposalId);
|
|
907
|
+
txSummaryBase64 = delta.deltaPayload.txSummary.data;
|
|
643
908
|
}
|
|
644
909
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
910
|
+
const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
|
|
911
|
+
const txSummary = TransactionSummary.deserialize(txSummaryBytes);
|
|
912
|
+
const saltHex = txSummary.salt().toHex();
|
|
913
|
+
const txCommitmentHex = txSummary.toCommitment().toHex();
|
|
914
|
+
const normalizedSignerCommitments = new Set(
|
|
915
|
+
this.signerCommitments.map((commitment) => normalizeHexWord(commitment)),
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
const adviceMap = new AdviceMap();
|
|
919
|
+
const adviceMapKeys = new Set<string>();
|
|
920
|
+
|
|
921
|
+
for (const cosignerSig of signaturesForExecution) {
|
|
922
|
+
let signerCommitmentHex = normalizeHexWord(cosignerSig.signerId);
|
|
923
|
+
const ecdsaPublicKey =
|
|
924
|
+
cosignerSig.signature.scheme === 'ecdsa'
|
|
925
|
+
? cosignerSig.signature.publicKey
|
|
926
|
+
: undefined;
|
|
927
|
+
|
|
928
|
+
if (cosignerSig.signature.scheme === 'ecdsa') {
|
|
929
|
+
if (!ecdsaPublicKey) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
`ECDSA proposal signature for ${signerCommitmentHex} is missing publicKey`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const derivedCommitment = tryComputeEcdsaCommitmentHex(ecdsaPublicKey);
|
|
936
|
+
if (derivedCommitment && derivedCommitment !== signerCommitmentHex) {
|
|
937
|
+
if (!normalizedSignerCommitments.has(derivedCommitment)) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`ECDSA public key commitment mismatch: derived commitment ${derivedCommitment} is not in signerCommitments.`,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
signerCommitmentHex = derivedCommitment;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const signerCommitment = Word.fromHex(signerCommitmentHex);
|
|
947
|
+
const sigBytes = signatureHexToBytes(
|
|
948
|
+
cosignerSig.signature.signature,
|
|
949
|
+
cosignerSig.signature.scheme,
|
|
950
|
+
);
|
|
951
|
+
const signature = Signature.deserialize(sigBytes);
|
|
952
|
+
const txCommitment = Word.fromHex(normalizeHexWord(txCommitmentHex));
|
|
953
|
+
const { key, values } = buildSignatureAdviceEntry(
|
|
954
|
+
signerCommitment,
|
|
955
|
+
txCommitment,
|
|
956
|
+
signature,
|
|
957
|
+
ecdsaPublicKey,
|
|
958
|
+
cosignerSig.signature.scheme === 'ecdsa'
|
|
959
|
+
? cosignerSig.signature.signature
|
|
960
|
+
: undefined,
|
|
961
|
+
);
|
|
962
|
+
const keyHex = normalizeHexWord(key.toHex());
|
|
963
|
+
if (adviceMapKeys.has(keyHex)) {
|
|
964
|
+
throw new Error(`Duplicate advice-map key detected for proposal ${proposalId}`);
|
|
965
|
+
}
|
|
966
|
+
adviceMapKeys.add(keyHex);
|
|
967
|
+
adviceMap.insert(key, new FeltArray(values));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (!isSwitchGuardian && delta) {
|
|
971
|
+
const executionDelta = {
|
|
972
|
+
...delta,
|
|
973
|
+
deltaPayload: delta.deltaPayload.txSummary,
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const pushResult = await this.guardian.pushDelta(executionDelta);
|
|
977
|
+
const ackSigHex = pushResult.ackSig;
|
|
978
|
+
if (!ackSigHex) {
|
|
979
|
+
throw new Error('GUARDIAN did not return acknowledgment signature');
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const guardianCommitment = Word.fromHex(normalizeHexWord(this.guardianCommitment));
|
|
983
|
+
const ackScheme = (pushResult.ackScheme as 'ecdsa' | 'falcon') || this.signer.scheme;
|
|
984
|
+
const ackPubkey = pushResult.ackPubkey || this.guardianPublicKey;
|
|
985
|
+
if (ackScheme === 'ecdsa' && !ackPubkey) {
|
|
986
|
+
throw new Error('GUARDIAN acknowledgment is missing ECDSA public key');
|
|
987
|
+
}
|
|
988
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
989
|
+
const derivedCommitment = tryComputeEcdsaCommitmentHex(ackPubkey);
|
|
990
|
+
if (derivedCommitment && derivedCommitment !== normalizeHexWord(this.guardianCommitment)) {
|
|
991
|
+
throw new Error('GUARDIAN public key commitment mismatch');
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
995
|
+
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
996
|
+
const txCommitmentForAck = Word.fromHex(normalizeHexWord(txCommitmentHex));
|
|
997
|
+
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
998
|
+
guardianCommitment,
|
|
999
|
+
txCommitmentForAck,
|
|
1000
|
+
ackSignature,
|
|
1001
|
+
ackScheme === 'ecdsa' ? ackPubkey : undefined,
|
|
1002
|
+
ackScheme === 'ecdsa' ? ackSigHex : undefined,
|
|
1003
|
+
);
|
|
1004
|
+
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1005
|
+
if (adviceMapKeys.has(ackKeyHex)) {
|
|
1006
|
+
throw new Error(`Duplicate advice-map key detected for GUARDIAN acknowledgment in proposal ${proposalId}`);
|
|
1007
|
+
}
|
|
1008
|
+
adviceMapKeys.add(ackKeyHex);
|
|
1009
|
+
adviceMap.insert(ackKey, new FeltArray(ackValues));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const metadata = proposal.metadata;
|
|
1013
|
+
if (!metadata) {
|
|
1014
|
+
throw new Error('Proposal missing metadata');
|
|
1015
|
+
}
|
|
1016
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
1017
|
+
await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
|
|
1018
|
+
}
|
|
1019
|
+
const executionSalt = Word.fromHex(normalizeHexWord(saltHex));
|
|
1020
|
+
const finalRequest = await this.buildTransactionRequestFromMetadata(
|
|
1021
|
+
metadata,
|
|
1022
|
+
executionSalt,
|
|
1023
|
+
adviceMap,
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
const accountId = AccountId.fromHex(this._accountId);
|
|
1027
|
+
const result = await this.webClient.executeTransaction(accountId, finalRequest);
|
|
1028
|
+
const proven = await this.webClient.proveTransaction(result, null);
|
|
1029
|
+
const submissionHeight = await this.webClient.submitProvenTransaction(proven, result);
|
|
1030
|
+
await this.webClient.applyTransaction(result, submissionHeight);
|
|
1031
|
+
|
|
1032
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
1033
|
+
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
1034
|
+
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
await this.webClient.syncState();
|
|
1039
|
+
|
|
1040
|
+
const updatedAccount = await this.webClient.getAccount(accountId);
|
|
1041
|
+
if (!updatedAccount) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`Updated account ${this._accountId} is missing from local client`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const updatedStateBase64 = uint8ArrayToBase64(updatedAccount.serialize());
|
|
1048
|
+
const nextGuardian = new GuardianHttpClient(metadata.newGuardianEndpoint);
|
|
1049
|
+
this.setGuardianClient(nextGuardian);
|
|
1050
|
+
this.guardianPublicKey = metadata.newGuardianPubkey;
|
|
1051
|
+
|
|
1052
|
+
await this.registerOnGuardian(updatedStateBase64);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`Transaction executed successfully but failed to register on new GUARDIAN: ${message}`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
657
1060
|
|
|
658
|
-
proposal.status =
|
|
1061
|
+
proposal.status = 'finalized';
|
|
659
1062
|
}
|
|
660
1063
|
|
|
661
|
-
|
|
662
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* Export a proposal for offline signing
|
|
1066
|
+
*/
|
|
1067
|
+
async exportProposal(proposalId: string): Promise<ExportedProposal> {
|
|
1068
|
+
const delta = await this.guardian.getDeltaProposal(this._accountId, proposalId);
|
|
1069
|
+
const existingProposal = this.proposals.get(proposalId);
|
|
1070
|
+
const proposal = this.proposalFactory().fromDelta(
|
|
1071
|
+
delta,
|
|
1072
|
+
proposalId,
|
|
1073
|
+
existingProposal?.metadata,
|
|
1074
|
+
existingProposal?.signatures ?? [],
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
const signatures =
|
|
1078
|
+
delta.status.status === 'pending'
|
|
1079
|
+
? delta.status.cosignerSigs.map((s) => ({
|
|
1080
|
+
commitment: s.signerId,
|
|
1081
|
+
signatureHex: s.signature.signature,
|
|
1082
|
+
scheme: s.signature.scheme,
|
|
1083
|
+
publicKey: s.signature.scheme === 'ecdsa' ? s.signature.publicKey : undefined,
|
|
1084
|
+
timestamp: s.timestamp,
|
|
1085
|
+
}))
|
|
1086
|
+
: [];
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
accountId: delta.accountId,
|
|
1090
|
+
nonce: delta.nonce,
|
|
1091
|
+
commitment: proposalId,
|
|
1092
|
+
txSummaryBase64: delta.deltaPayload.txSummary.data,
|
|
1093
|
+
signatures,
|
|
1094
|
+
metadata: proposal.metadata,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Export a proposal to JSON for side-channel sharing.
|
|
1100
|
+
*
|
|
1101
|
+
* @param proposalId - The proposal commitment/ID
|
|
1102
|
+
* @returns JSON string that can be shared and imported by other signers
|
|
1103
|
+
*/
|
|
1104
|
+
exportProposalToJson(proposalId: string): string {
|
|
1105
|
+
const proposal = this.proposals.get(proposalId);
|
|
663
1106
|
if (!proposal) {
|
|
664
|
-
throw new Error(`Proposal not found in local cache: ${
|
|
1107
|
+
throw new Error(`Proposal not found in local cache: ${proposalId}`);
|
|
665
1108
|
}
|
|
666
1109
|
|
|
667
|
-
const exported:
|
|
1110
|
+
const exported: ExportedProposal = {
|
|
668
1111
|
accountId: proposal.accountId,
|
|
669
1112
|
nonce: proposal.nonce,
|
|
670
1113
|
commitment: proposal.id,
|
|
@@ -672,6 +1115,8 @@ export class Multisig {
|
|
|
672
1115
|
signatures: proposal.signatures.map((s) => ({
|
|
673
1116
|
commitment: s.signerId,
|
|
674
1117
|
signatureHex: s.signature.signature,
|
|
1118
|
+
scheme: s.signature.scheme,
|
|
1119
|
+
publicKey: s.signature.scheme === 'ecdsa' ? s.signature.publicKey : undefined,
|
|
675
1120
|
timestamp: s.timestamp,
|
|
676
1121
|
})),
|
|
677
1122
|
metadata: proposal.metadata,
|
|
@@ -680,133 +1125,182 @@ export class Multisig {
|
|
|
680
1125
|
return JSON.stringify(exported, null, 2);
|
|
681
1126
|
}
|
|
682
1127
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
accountId: this._accountId,
|
|
693
|
-
threshold: this.threshold,
|
|
694
|
-
signerCommitments: this.signerCommitments,
|
|
695
|
-
psmCommitment: this.psmCommitment,
|
|
696
|
-
psmPublicKey: this.psmPublicKey,
|
|
697
|
-
signatureScheme: this.signatureScheme,
|
|
698
|
-
getEffectiveThreshold: (proposalType) => this.getEffectiveThreshold(proposalType),
|
|
699
|
-
psm: this.psm,
|
|
700
|
-
webClient: this.webClient,
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
return request;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
importTransactionProposal(json: string): TransactionProposalResult {
|
|
707
|
-
const exported: ExportedTransactionProposal = JSON.parse(json);
|
|
708
|
-
|
|
709
|
-
if (!exported.accountId || !exported.txSummaryBase64 || !exported.commitment) {
|
|
1128
|
+
/**
|
|
1129
|
+
* Import a proposal from JSON (exported via exportProposalToJson).
|
|
1130
|
+
*
|
|
1131
|
+
* @param json - JSON string from exportProposalToJson
|
|
1132
|
+
* @returns The imported proposal
|
|
1133
|
+
*/
|
|
1134
|
+
async importProposal(json: string): Promise<Proposal> {
|
|
1135
|
+
const exported: ExportedProposal = JSON.parse(json);
|
|
1136
|
+
if (!exported.accountId || !exported.txSummaryBase64 || !exported.commitment || !exported.metadata) {
|
|
710
1137
|
throw new Error('Invalid proposal JSON: missing required fields');
|
|
711
1138
|
}
|
|
712
1139
|
|
|
713
|
-
|
|
714
|
-
throw new Error(`Proposal is for a different account: ${exported.accountId}`);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const computedCommitment = computeCommitmentFromTxSummary(exported.txSummaryBase64);
|
|
718
|
-
if (computedCommitment !== exported.commitment) {
|
|
719
|
-
throw new Error('Invalid proposal: commitment does not match tx_summary');
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const metadata: ProposalMetadata = (exported.metadata as ProposalMetadata) ?? {
|
|
723
|
-
proposalType: 'unknown',
|
|
724
|
-
description: '',
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
const signaturesCollected = exported.signatures.length;
|
|
728
|
-
const signaturesRequired = this.getEffectiveThreshold(metadata.proposalType);
|
|
729
|
-
const status: TransactionProposalStatus = signaturesCollected >= signaturesRequired
|
|
730
|
-
? { type: 'ready' }
|
|
731
|
-
: {
|
|
732
|
-
type: 'pending',
|
|
733
|
-
signaturesCollected,
|
|
734
|
-
signaturesRequired,
|
|
735
|
-
signers: exported.signatures.map((s) => s.commitment),
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
const proposal: TransactionProposal = {
|
|
739
|
-
id: exported.commitment,
|
|
740
|
-
commitment: exported.commitment,
|
|
741
|
-
accountId: exported.accountId,
|
|
742
|
-
nonce: exported.nonce,
|
|
743
|
-
status,
|
|
744
|
-
txSummary: exported.txSummaryBase64,
|
|
745
|
-
signatures: exported.signatures.map((s) => ({
|
|
746
|
-
signerId: s.commitment,
|
|
747
|
-
signature: { scheme: this.signer.scheme, signature: s.signatureHex },
|
|
748
|
-
timestamp: s.timestamp || new Date().toISOString(),
|
|
749
|
-
})),
|
|
750
|
-
metadata,
|
|
751
|
-
};
|
|
1140
|
+
const proposal = this.proposalFactory().fromExported(exported);
|
|
752
1141
|
|
|
1142
|
+
await this.verifyProposalMetadataBinding(proposal);
|
|
753
1143
|
this.proposals.set(proposal.id, proposal);
|
|
754
|
-
|
|
755
|
-
return
|
|
1144
|
+
|
|
1145
|
+
return proposal;
|
|
756
1146
|
}
|
|
757
1147
|
|
|
758
|
-
|
|
759
|
-
|
|
1148
|
+
/**
|
|
1149
|
+
* Sign an imported proposal and return updated JSON for sharing..
|
|
1150
|
+
*
|
|
1151
|
+
* @param proposalId - The proposal commitment/ID
|
|
1152
|
+
* @returns Updated JSON string with the new signature included
|
|
1153
|
+
*/
|
|
1154
|
+
async signProposalOffline(proposalId: string): Promise<string> {
|
|
1155
|
+
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1156
|
+
const proposal = this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
760
1157
|
if (!proposal) {
|
|
761
|
-
throw new Error(`Proposal not found: ${
|
|
1158
|
+
throw new Error(`Proposal not found: ${proposalId}`);
|
|
762
1159
|
}
|
|
1160
|
+
this.proposalFactory().assertAccountId(proposal.accountId);
|
|
763
1161
|
|
|
764
|
-
const
|
|
765
|
-
|
|
1162
|
+
const localSignatureContext = `Invalid local proposal signatures for ${proposalId}`;
|
|
1163
|
+
const existingSignatures = new ProposalSignatures(
|
|
1164
|
+
proposal.signatures,
|
|
1165
|
+
this.signerCommitments,
|
|
1166
|
+
localSignatureContext,
|
|
766
1167
|
);
|
|
1168
|
+
let signerCommitment: string;
|
|
1169
|
+
try {
|
|
1170
|
+
signerCommitment = normalizeSignerCommitment(this.signer.commitment);
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1173
|
+
throw new Error(`Invalid local signer commitment: ${message}`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Check if already signed
|
|
1177
|
+
const alreadySigned = existingSignatures.hasSigner(signerCommitment);
|
|
767
1178
|
if (alreadySigned) {
|
|
768
1179
|
throw new Error('You have already signed this proposal');
|
|
769
1180
|
}
|
|
770
1181
|
|
|
771
|
-
const
|
|
1182
|
+
const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
|
|
772
1183
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1184
|
+
// Sign the commitment
|
|
1185
|
+
const signature = await buildGuardianSignatureFromSigner(this.signer, commitmentToSign);
|
|
1186
|
+
|
|
1187
|
+
// Add signature to local proposal
|
|
1188
|
+
const signatures = [
|
|
1189
|
+
...existingSignatures.entries(),
|
|
1190
|
+
{
|
|
1191
|
+
signerId: signerCommitment,
|
|
1192
|
+
signature,
|
|
1193
|
+
timestamp: new Date().toISOString(),
|
|
1194
|
+
},
|
|
1195
|
+
];
|
|
1196
|
+
const canonicalizedSignatures = new ProposalSignatures(
|
|
1197
|
+
signatures,
|
|
1198
|
+
this.signerCommitments,
|
|
1199
|
+
localSignatureContext,
|
|
1200
|
+
).entries();
|
|
1201
|
+
proposal.signatures = canonicalizedSignatures;
|
|
779
1202
|
|
|
780
|
-
|
|
1203
|
+
// Update status
|
|
781
1204
|
const proposalType = proposal.metadata?.proposalType;
|
|
782
|
-
const
|
|
1205
|
+
const signaturesRequired = proposalType
|
|
783
1206
|
? this.getEffectiveThreshold(proposalType)
|
|
784
1207
|
: this.threshold;
|
|
1208
|
+
proposal.status = proposal.signatures.length >= signaturesRequired ? 'ready' : 'pending';
|
|
785
1209
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1210
|
+
// Return updated JSON
|
|
1211
|
+
return this.exportProposalToJson(proposal.id);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private ensureProposalCommitmentMatchesSummary(proposal: Proposal): string {
|
|
1215
|
+
const proposalId = normalizeHexWord(proposal.id);
|
|
1216
|
+
const txSummaryCommitment = normalizeHexWord(
|
|
1217
|
+
computeCommitmentFromTxSummary(proposal.txSummary)
|
|
1218
|
+
);
|
|
1219
|
+
if (proposalId !== txSummaryCommitment) {
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
`Invalid proposal: id ${proposal.id} does not match tx_summary commitment ${txSummaryCommitment}`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
return txSummaryCommitment;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private async verifyProposalMetadataBinding(proposal: Proposal): Promise<string> {
|
|
1228
|
+
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
1229
|
+
if (proposal.metadata.proposalType === 'unknown') {
|
|
1230
|
+
throw new Error(`Cannot verify proposal metadata for unknown proposal type: ${proposal.id}`);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
1234
|
+
const salt = proposal.metadata.saltHex
|
|
1235
|
+
? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
|
|
1236
|
+
: summary.salt();
|
|
1237
|
+
|
|
1238
|
+
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
|
|
1239
|
+
const reconstructed = await executeForSummary(this.webClient, this._accountId, request);
|
|
1240
|
+
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
1241
|
+
|
|
1242
|
+
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
1243
|
+
throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
|
|
795
1244
|
}
|
|
796
1245
|
|
|
797
|
-
return
|
|
1246
|
+
return txSummaryCommitment;
|
|
798
1247
|
}
|
|
799
1248
|
|
|
800
|
-
private async
|
|
801
|
-
request: TransactionRequest,
|
|
1249
|
+
private async buildTransactionRequestFromMetadata(
|
|
802
1250
|
metadata: ProposalMetadata,
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1251
|
+
salt: Word,
|
|
1252
|
+
signatureAdviceMap?: AdviceMap,
|
|
1253
|
+
): Promise<TransactionRequest> {
|
|
1254
|
+
switch (metadata.proposalType) {
|
|
1255
|
+
case 'add_signer':
|
|
1256
|
+
case 'remove_signer':
|
|
1257
|
+
case 'change_threshold': {
|
|
1258
|
+
const { request } = await buildUpdateSignersTransactionRequest(
|
|
1259
|
+
this.webClient,
|
|
1260
|
+
metadata.targetThreshold,
|
|
1261
|
+
metadata.targetSignerCommitments,
|
|
1262
|
+
{ salt, signatureAdviceMap, signatureScheme: this.signer.scheme }
|
|
1263
|
+
);
|
|
1264
|
+
return request;
|
|
1265
|
+
}
|
|
1266
|
+
case 'switch_guardian': {
|
|
1267
|
+
const { request } = await buildUpdateGuardianTransactionRequest(
|
|
1268
|
+
this.webClient,
|
|
1269
|
+
metadata.newGuardianPubkey,
|
|
1270
|
+
{ salt, signatureAdviceMap, signatureScheme: this.signer.scheme }
|
|
1271
|
+
);
|
|
1272
|
+
return request;
|
|
1273
|
+
}
|
|
1274
|
+
case 'update_procedure_threshold': {
|
|
1275
|
+
const { request } = await buildUpdateProcedureThresholdTransactionRequest(
|
|
1276
|
+
this.webClient,
|
|
1277
|
+
metadata.targetProcedure,
|
|
1278
|
+
metadata.targetThreshold,
|
|
1279
|
+
{ salt, signatureAdviceMap, signatureScheme: this.signer.scheme }
|
|
1280
|
+
);
|
|
1281
|
+
return request;
|
|
1282
|
+
}
|
|
1283
|
+
case 'consume_notes': {
|
|
1284
|
+
const { request } = await buildConsumeNotesTransactionRequest(
|
|
1285
|
+
this.webClient,
|
|
1286
|
+
metadata.noteIds,
|
|
1287
|
+
{ salt, signatureAdviceMap }
|
|
1288
|
+
);
|
|
1289
|
+
return request;
|
|
1290
|
+
}
|
|
1291
|
+
case 'p2id': {
|
|
1292
|
+
const { request } = buildP2idTransactionRequest(
|
|
1293
|
+
this._accountId,
|
|
1294
|
+
metadata.recipientId,
|
|
1295
|
+
metadata.faucetId,
|
|
1296
|
+
BigInt(metadata.amount),
|
|
1297
|
+
{ salt, signatureAdviceMap }
|
|
1298
|
+
);
|
|
1299
|
+
return request;
|
|
1300
|
+
}
|
|
1301
|
+
case 'unknown':
|
|
1302
|
+
throw new Error('Unsupported proposal type: unknown');
|
|
1303
|
+
}
|
|
810
1304
|
}
|
|
811
1305
|
|
|
812
1306
|
}
|