@openzeppelin/miden-multisig-client 0.13.0 → 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.
Files changed (273) hide show
  1. package/README.md +98 -119
  2. package/dist/account/builder.d.ts +18 -0
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +59 -29
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/builder.test.d.ts +2 -0
  7. package/dist/account/builder.test.d.ts.map +1 -0
  8. package/dist/account/builder.test.js +115 -0
  9. package/dist/account/builder.test.js.map +1 -0
  10. package/dist/account/index.d.ts +1 -1
  11. package/dist/account/index.d.ts.map +1 -1
  12. package/dist/account/index.js +1 -1
  13. package/dist/account/index.js.map +1 -1
  14. package/dist/account/masm/account-components/auth.d.ts +5 -0
  15. package/dist/account/masm/account-components/auth.d.ts.map +1 -0
  16. package/dist/account/masm/account-components/auth.js +67 -0
  17. package/dist/account/masm/account-components/auth.js.map +1 -0
  18. package/dist/account/masm/auth.d.ts +5 -0
  19. package/dist/account/masm/auth.d.ts.map +1 -0
  20. package/dist/account/masm/auth.js +1395 -0
  21. package/dist/account/masm/auth.js.map +1 -0
  22. package/dist/account/masm/index.d.ts +3 -0
  23. package/dist/account/masm/index.d.ts.map +1 -0
  24. package/dist/account/masm/index.js +5 -0
  25. package/dist/account/masm/index.js.map +1 -0
  26. package/dist/account/masm.d.ts +1 -4
  27. package/dist/account/masm.d.ts.map +1 -1
  28. package/dist/account/masm.js +3 -1034
  29. package/dist/account/masm.js.map +1 -1
  30. package/dist/account/storage.d.ts +2 -2
  31. package/dist/account/storage.d.ts.map +1 -1
  32. package/dist/account/storage.js +18 -16
  33. package/dist/account/storage.js.map +1 -1
  34. package/dist/client.d.ts +67 -9
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +73 -22
  37. package/dist/client.js.map +1 -1
  38. package/dist/client.test.js +31 -32
  39. package/dist/client.test.js.map +1 -1
  40. package/dist/index.d.ts +54 -12
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +53 -9
  43. package/dist/index.js.map +1 -1
  44. package/dist/inspector.d.ts +32 -6
  45. package/dist/inspector.d.ts.map +1 -1
  46. package/dist/inspector.js +58 -23
  47. package/dist/inspector.js.map +1 -1
  48. package/dist/inspector.test.js +18 -18
  49. package/dist/inspector.test.js.map +1 -1
  50. package/dist/multisig/proposal/execution.d.ts +6 -5
  51. package/dist/multisig/proposal/execution.d.ts.map +1 -1
  52. package/dist/multisig/proposal/execution.js +49 -27
  53. package/dist/multisig/proposal/execution.js.map +1 -1
  54. package/dist/multisig/proposal/parser.d.ts +3 -3
  55. package/dist/multisig/proposal/parser.d.ts.map +1 -1
  56. package/dist/multisig/proposal/parser.js +23 -23
  57. package/dist/multisig/proposal/parser.js.map +1 -1
  58. package/dist/multisig/signing.d.ts +3 -3
  59. package/dist/multisig/signing.d.ts.map +1 -1
  60. package/dist/multisig/signing.js +3 -3
  61. package/dist/multisig/signing.js.map +1 -1
  62. package/dist/multisig.d.ts +187 -46
  63. package/dist/multisig.d.ts.map +1 -1
  64. package/dist/multisig.js +650 -289
  65. package/dist/multisig.js.map +1 -1
  66. package/dist/multisig.test.js +1680 -241
  67. package/dist/multisig.test.js.map +1 -1
  68. package/dist/procedures.d.ts +46 -24
  69. package/dist/procedures.d.ts.map +1 -1
  70. package/dist/procedures.js +46 -21
  71. package/dist/procedures.js.map +1 -1
  72. package/dist/proposal/factory.d.ts +18 -0
  73. package/dist/proposal/factory.d.ts.map +1 -0
  74. package/dist/proposal/factory.js +99 -0
  75. package/dist/proposal/factory.js.map +1 -0
  76. package/dist/proposal/metadata.d.ts +8 -0
  77. package/dist/proposal/metadata.d.ts.map +1 -0
  78. package/dist/proposal/metadata.js +156 -0
  79. package/dist/proposal/metadata.js.map +1 -0
  80. package/dist/proposal/signatures.d.ts +10 -0
  81. package/dist/proposal/signatures.d.ts.map +1 -0
  82. package/dist/proposal/signatures.js +54 -0
  83. package/dist/proposal/signatures.js.map +1 -0
  84. package/dist/signer.d.ts +2 -0
  85. package/dist/signer.d.ts.map +1 -1
  86. package/dist/signer.js +2 -0
  87. package/dist/signer.js.map +1 -1
  88. package/dist/signers/ecdsa.d.ts +3 -0
  89. package/dist/signers/ecdsa.d.ts.map +1 -1
  90. package/dist/signers/ecdsa.js +8 -4
  91. package/dist/signers/ecdsa.js.map +1 -1
  92. package/dist/signers/ecdsa.test.d.ts +2 -0
  93. package/dist/signers/ecdsa.test.d.ts.map +1 -0
  94. package/dist/signers/ecdsa.test.js +65 -0
  95. package/dist/signers/ecdsa.test.js.map +1 -0
  96. package/dist/signers/falcon.d.ts +3 -0
  97. package/dist/signers/falcon.d.ts.map +1 -1
  98. package/dist/signers/falcon.js +8 -4
  99. package/dist/signers/falcon.js.map +1 -1
  100. package/dist/signers/falcon.test.js +12 -0
  101. package/dist/signers/falcon.test.js.map +1 -1
  102. package/dist/signers/miden-wallet.d.ts +3 -1
  103. package/dist/signers/miden-wallet.d.ts.map +1 -1
  104. package/dist/signers/miden-wallet.js +11 -2
  105. package/dist/signers/miden-wallet.js.map +1 -1
  106. package/dist/signers/miden-wallet.test.js +26 -0
  107. package/dist/signers/miden-wallet.test.js.map +1 -1
  108. package/dist/signers/para.d.ts +3 -3
  109. package/dist/signers/para.d.ts.map +1 -1
  110. package/dist/signers/para.js +15 -6
  111. package/dist/signers/para.js.map +1 -1
  112. package/dist/signers/para.test.js +39 -0
  113. package/dist/signers/para.test.js.map +1 -1
  114. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  115. package/dist/transaction/consumeNotes.js +1 -0
  116. package/dist/transaction/consumeNotes.js.map +1 -1
  117. package/dist/transaction/index.d.ts +2 -1
  118. package/dist/transaction/index.d.ts.map +1 -1
  119. package/dist/transaction/index.js +2 -1
  120. package/dist/transaction/index.js.map +1 -1
  121. package/dist/transaction/options.d.ts +1 -1
  122. package/dist/transaction/options.d.ts.map +1 -1
  123. package/dist/transaction/p2id.d.ts +1 -0
  124. package/dist/transaction/p2id.d.ts.map +1 -1
  125. package/dist/transaction/p2id.js +9 -3
  126. package/dist/transaction/p2id.js.map +1 -1
  127. package/dist/transaction/p2id.test.d.ts +2 -0
  128. package/dist/transaction/p2id.test.d.ts.map +1 -0
  129. package/dist/transaction/p2id.test.js +138 -0
  130. package/dist/transaction/p2id.test.js.map +1 -0
  131. package/dist/transaction/rpoRandomCoin.d.ts +7 -0
  132. package/dist/transaction/rpoRandomCoin.d.ts.map +1 -0
  133. package/dist/transaction/rpoRandomCoin.js +17 -0
  134. package/dist/transaction/rpoRandomCoin.js.map +1 -0
  135. package/dist/transaction/rpoRandomCoin.test.d.ts +2 -0
  136. package/dist/transaction/rpoRandomCoin.test.d.ts.map +1 -0
  137. package/dist/transaction/rpoRandomCoin.test.js +52 -0
  138. package/dist/transaction/rpoRandomCoin.test.js.map +1 -0
  139. package/dist/transaction/updateGuardian.d.ts +7 -0
  140. package/dist/transaction/updateGuardian.d.ts.map +1 -0
  141. package/dist/transaction/{updatePsm.js → updateGuardian.js} +15 -29
  142. package/dist/transaction/updateGuardian.js.map +1 -0
  143. package/dist/transaction/updateProcedureThreshold.d.ts +9 -0
  144. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -0
  145. package/dist/transaction/updateProcedureThreshold.js +55 -0
  146. package/dist/transaction/updateProcedureThreshold.js.map +1 -0
  147. package/dist/transaction/updateSigners.d.ts.map +1 -1
  148. package/dist/transaction/updateSigners.js +5 -23
  149. package/dist/transaction/updateSigners.js.map +1 -1
  150. package/dist/transaction.d.ts +2 -1
  151. package/dist/transaction.d.ts.map +1 -1
  152. package/dist/transaction.js +2 -1
  153. package/dist/transaction.js.map +1 -1
  154. package/dist/types/proposal.d.ts +32 -15
  155. package/dist/types/proposal.d.ts.map +1 -1
  156. package/dist/types.d.ts +24 -7
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/utils/digest.d.ts +5 -0
  159. package/dist/utils/digest.d.ts.map +1 -1
  160. package/dist/utils/digest.js +34 -1
  161. package/dist/utils/digest.js.map +1 -1
  162. package/dist/utils/digest.test.d.ts +2 -0
  163. package/dist/utils/digest.test.d.ts.map +1 -0
  164. package/dist/utils/digest.test.js +48 -0
  165. package/dist/utils/digest.test.js.map +1 -0
  166. package/dist/utils/signature.d.ts +3 -1
  167. package/dist/utils/signature.d.ts.map +1 -1
  168. package/dist/utils/signature.js +31 -6
  169. package/dist/utils/signature.js.map +1 -1
  170. package/dist/utils/word.test.js +1 -52
  171. package/dist/utils/word.test.js.map +1 -1
  172. package/masm/account_components/auth/multisig.masm +13 -0
  173. package/masm/account_components/auth/multisig_ecdsa.masm +13 -0
  174. package/masm/account_components/auth/multisig_guardian.masm +17 -0
  175. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +17 -0
  176. package/masm/auth/guardian.masm +177 -0
  177. package/masm/auth/guardian_ecdsa.masm +178 -0
  178. package/masm/{multisig.masm → auth/multisig.masm} +149 -55
  179. package/masm/{multisig_ecdsa.masm → auth/multisig_ecdsa.masm} +148 -56
  180. package/package.json +11 -9
  181. package/src/account/builder.test.ts +165 -0
  182. package/src/account/builder.ts +71 -34
  183. package/src/account/index.ts +1 -1
  184. package/src/account/masm/account-components/auth.ts +70 -0
  185. package/src/account/masm/auth.ts +1398 -0
  186. package/src/account/masm/index.ts +5 -0
  187. package/src/account/masm.ts +3 -1040
  188. package/src/account/storage.ts +18 -16
  189. package/src/client.test.ts +31 -35
  190. package/src/client.ts +98 -26
  191. package/src/index.ts +84 -22
  192. package/src/inspector.test.ts +17 -17
  193. package/src/inspector.ts +63 -29
  194. package/src/multisig/proposal/execution.ts +102 -41
  195. package/src/multisig/proposal/parser.ts +26 -26
  196. package/src/multisig/signing.ts +4 -4
  197. package/src/multisig.test.ts +2097 -248
  198. package/src/multisig.ts +882 -365
  199. package/src/procedures.ts +50 -26
  200. package/src/proposal/factory.ts +171 -0
  201. package/src/proposal/metadata.ts +165 -0
  202. package/src/proposal/signatures.ts +68 -0
  203. package/src/signer.ts +2 -0
  204. package/src/signers/ecdsa.test.ts +80 -0
  205. package/src/signers/ecdsa.ts +15 -4
  206. package/src/signers/falcon.test.ts +19 -0
  207. package/src/signers/falcon.ts +16 -5
  208. package/src/signers/miden-wallet.test.ts +47 -0
  209. package/src/signers/miden-wallet.ts +18 -1
  210. package/src/signers/para.test.ts +57 -0
  211. package/src/signers/para.ts +26 -7
  212. package/src/transaction/consumeNotes.ts +1 -0
  213. package/src/transaction/index.ts +2 -2
  214. package/src/transaction/options.ts +1 -2
  215. package/src/transaction/p2id.test.ts +190 -0
  216. package/src/transaction/p2id.ts +11 -5
  217. package/src/transaction/rpoRandomCoin.test.ts +64 -0
  218. package/src/transaction/rpoRandomCoin.ts +24 -0
  219. package/src/transaction/{updatePsm.ts → updateGuardian.ts} +19 -32
  220. package/src/transaction/updateProcedureThreshold.ts +97 -0
  221. package/src/transaction/updateSigners.ts +12 -27
  222. package/src/transaction.ts +5 -3
  223. package/src/types/proposal.ts +43 -11
  224. package/src/types.ts +29 -11
  225. package/src/utils/digest.test.ts +55 -0
  226. package/src/utils/digest.ts +51 -1
  227. package/src/utils/signature.ts +47 -9
  228. package/src/utils/word.test.ts +1 -55
  229. package/dist/account.d.ts +0 -104
  230. package/dist/account.d.ts.map +0 -1
  231. package/dist/account.js +0 -136
  232. package/dist/account.js.map +0 -1
  233. package/dist/builder.d.ts +0 -54
  234. package/dist/builder.d.ts.map +0 -1
  235. package/dist/builder.js +0 -78
  236. package/dist/builder.js.map +0 -1
  237. package/dist/http.d.ts +0 -64
  238. package/dist/http.d.ts.map +0 -1
  239. package/dist/http.js +0 -162
  240. package/dist/http.js.map +0 -1
  241. package/dist/keystore.d.ts +0 -65
  242. package/dist/keystore.d.ts.map +0 -1
  243. package/dist/keystore.js +0 -121
  244. package/dist/keystore.js.map +0 -1
  245. package/dist/miden.d.ts +0 -9
  246. package/dist/miden.d.ts.map +0 -1
  247. package/dist/miden.js +0 -26
  248. package/dist/miden.js.map +0 -1
  249. package/dist/multisig/metadata.d.ts +0 -18
  250. package/dist/multisig/metadata.d.ts.map +0 -1
  251. package/dist/multisig/metadata.js +0 -100
  252. package/dist/multisig/metadata.js.map +0 -1
  253. package/dist/multisig/metadata.test.d.ts +0 -2
  254. package/dist/multisig/metadata.test.d.ts.map +0 -1
  255. package/dist/multisig/metadata.test.js +0 -323
  256. package/dist/multisig/metadata.test.js.map +0 -1
  257. package/dist/signer.test.d.ts +0 -2
  258. package/dist/signer.test.d.ts.map +0 -1
  259. package/dist/signer.test.js +0 -117
  260. package/dist/signer.test.js.map +0 -1
  261. package/dist/transaction/updatePsm.d.ts +0 -7
  262. package/dist/transaction/updatePsm.d.ts.map +0 -1
  263. package/dist/transaction/updatePsm.js.map +0 -1
  264. package/dist/transport/http.d.ts +0 -64
  265. package/dist/transport/http.d.ts.map +0 -1
  266. package/dist/transport/http.js +0 -162
  267. package/dist/transport/http.js.map +0 -1
  268. package/dist/transport/index.d.ts +0 -5
  269. package/dist/transport/index.d.ts.map +0 -1
  270. package/dist/transport/index.js +0 -5
  271. package/dist/transport/index.js.map +0 -1
  272. package/masm/psm.masm +0 -178
  273. package/masm/psm_ecdsa.masm +0 -179
package/dist/multisig.js CHANGED
@@ -1,49 +1,85 @@
1
- import { AccountInspector } from './inspector.js';
2
- import { Account, AccountId, } from '@miden-sdk/miden-sdk';
3
- import { executeForSummary, buildUpdateSignersTransactionRequest, buildUpdatePsmTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idTransactionRequest, } from './transaction.js';
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 { buildPsmMetadata, deltaToProposal, resolveMetadata, signatureRequirementForProposal, } from './multisig/proposal/parser.js';
7
- import { toPsmSignature, buildPsmSignatureFromSigner, } from './multisig/signing.js';
8
- import { executeProposalWorkflow } from './multisig/proposal/execution.js';
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
- psmCommitment;
14
- psmPublicKey;
25
+ guardianCommitment;
15
26
  procedureThresholds;
16
- signatureScheme;
17
- psm;
27
+ guardianPublicKey;
28
+ guardian;
18
29
  signer;
19
30
  webClient;
20
31
  _accountId;
32
+ midenRpcEndpoint;
21
33
  proposals = new Map();
22
- constructor(account, config, psm, signer, webClient, accountId) {
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.psmCommitment = config.psmCommitment;
30
- this.psmPublicKey = config.psmPublicKey;
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.psm = psm;
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
- setPsmPublicKey(pubkey) {
45
- this.psmPublicKey = pubkey;
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 'switch_psm':
58
- return 'update_psm';
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
- setPsmClient(psmClient) {
74
- this.psm = psmClient;
75
- this.psm.setSigner(this.signer);
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.psm.getState(this._accountId);
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
- const psmCommitment = normalizeHexWord(state.commitment);
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 !== psmCommitment) {
157
+ if (!localAccount || localCommitment !== guardianCommitment) {
97
158
  const accountBytes = base64ToUint8Array(state.stateDataBase64);
98
- const account = Account.deserialize(accountBytes);
99
- await this.webClient.newAccount(account, true);
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 registerOnPsm(initialStateBase64) {
104
- if (!this.account && !initialStateBase64) {
105
- throw new Error('Cannot register on PSM: no account available and no initial state provided');
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
- let stateData;
108
- if (initialStateBase64) {
109
- stateData = initialStateBase64;
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
- else {
112
- const accountBytes = this.account.serialize();
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
- const auth = this.signer.scheme === 'ecdsa'
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
- auth,
121
- initialState: { data: stateData, accountId: this._accountId },
122
- });
123
- if (!response.success) {
124
- throw new Error(`Failed to register on PSM: ${response.message}`);
125
- }
126
- if (response.ackCommitment && this.psmCommitment) {
127
- const onChain = normalizeHexWord(this.psmCommitment);
128
- const server = normalizeHexWord(response.ackCommitment);
129
- if (onChain !== server) {
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
- async syncAll() {
135
- const proposals = await this.syncTransactionProposals();
136
- const state = await this.syncState();
137
- const notes = await this.getConsumableNotes();
138
- const config = AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
139
- this.threshold = config.threshold;
140
- this.signerCommitments = config.signerCommitments;
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 getAccountConfig() {
148
- const state = await this.syncState();
149
- return AccountInspector.fromBase64(state.stateDataBase64, this.signatureScheme);
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
- async switchPsm(psmClient) {
152
- const accountId = AccountId.fromHex(this._accountId);
153
- const localAccount = await this.webClient.getAccount(accountId);
154
- if (localAccount) {
155
- this.account = localAccount;
156
- const config = AccountInspector.fromAccount(localAccount, this.signatureScheme);
157
- if (config.psmCommitment) {
158
- this.psmCommitment = config.psmCommitment;
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.threshold = config.threshold;
161
- this.signerCommitments = config.signerCommitments;
162
- this.procedureThresholds = config.procedureThresholds;
242
+ this.procedureThresholds = new Map(detected.procedureThresholds);
243
+ }
244
+ catch (error) {
245
+ console.warn('Failed to refresh multisig config from account state', error);
163
246
  }
164
- this.setPsmClient(psmClient);
165
- const accountBytes = this.account.serialize();
166
- const stateData = uint8ArrayToBase64(accountBytes);
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
- ? { MidenEcdsa: { cosigner_commitments: this.signerCommitments } }
169
- : { MidenFalconRpo: { cosigner_commitments: this.signerCommitments } };
170
- const response = await this.psm.configure({
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 PSM: ${response.message}`);
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
- async syncTransactionProposals() {
186
- const deltas = await this.psm.getDeltaProposals(this._accountId);
187
- const serverProposalIds = new Set();
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 resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
193
- if (!resolvedMetadata) {
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
- listTransactionProposals() {
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 psmMetadata = buildPsmMetadata(metadata);
220
- const response = await this.psm.pushDeltaProposal({
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: psmMetadata,
315
+ metadata: guardianMetadata,
227
316
  },
228
317
  });
229
- const proposal = deltaToProposal({
230
- delta: response.delta,
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
- async createAddSignerProposal(newCommitment, options) {
239
- const targetThreshold = options?.newThreshold ?? this.threshold;
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.signatureScheme });
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.createAndSyncFromRequest(request, metadata, options?.nonce);
345
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
250
346
  }
251
- async createRemoveSignerProposal(signerToRemove, options) {
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 signerExists = this.signerCommitments.some((c) => c.toLowerCase() === normalizedRemove);
254
- if (!signerExists) {
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 = options?.newThreshold ?? Math.min(this.threshold, targetSignerCommitments.length);
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.signatureScheme });
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.createAndSyncFromRequest(request, metadata, options?.nonce);
379
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
274
380
  }
275
- async createChangeThresholdProposal(newThreshold, options) {
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.signatureScheme });
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.createAndSyncFromRequest(request, metadata, options?.nonce);
406
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
407
+ }
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 });
420
+ const summary = await executeForSummary(this.webClient, this._accountId, request);
421
+ const summaryBase64 = uint8ArrayToBase64(summary.serialize());
422
+ const proposalNonce = nonce ?? Date.now();
423
+ const action = targetThreshold === 0
424
+ ? `Clear threshold override for ${targetProcedure}`
425
+ : `Set ${targetProcedure} threshold override to ${targetThreshold}`;
426
+ const metadata = {
427
+ proposalType: 'update_procedure_threshold',
428
+ targetProcedure,
429
+ targetThreshold,
430
+ saltHex: salt.toHex(),
431
+ requiredSignatures: this.getEffectiveThreshold('update_procedure_threshold'),
432
+ description: action,
433
+ };
434
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
291
435
  }
292
- async createSwitchPsmProposal(newPsmEndpoint, newPsmPubkey, options) {
293
- const { request, salt } = await buildUpdatePsmTransactionRequest(this.webClient, newPsmPubkey, { signatureScheme: this.signatureScheme });
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 });
294
446
  const summary = await executeForSummary(this.webClient, this._accountId, request);
295
447
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
296
- const proposalNonce = options?.nonce ?? Date.now();
448
+ const proposalNonce = nonce ?? Date.now();
297
449
  const metadata = {
298
- proposalType: 'switch_psm',
450
+ proposalType: 'switch_guardian',
299
451
  saltHex: salt.toHex(),
300
- newPsmPubkey,
301
- newPsmEndpoint,
302
- description: `Switch PSM to ${newPsmEndpoint}`,
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: { type: 'pending', signaturesCollected: 0, signaturesRequired: this.threshold, signers: [] },
462
+ status: 'pending',
311
463
  txSummary: summaryBase64,
312
464
  signatures: [],
313
465
  metadata,
314
466
  };
315
467
  this.proposals.set(proposal.id, proposal);
316
- const proposals = this.listTransactionProposals();
317
- return { proposal, proposals };
468
+ return proposal;
318
469
  }
319
- async createConsumeNotesProposal(noteIds, options) {
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.createAndSyncFromRequest(request, metadata, options?.nonce);
491
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
331
492
  }
332
- async createSendProposal(recipientId, faucetId, amount, options) {
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.createAndSyncFromRequest(request, metadata, options?.nonce);
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
- async signTransactionProposal(commitment) {
373
- const existingProposal = this.proposals.get(commitment);
374
- const signature = await buildPsmSignatureFromSigner(this.signer, commitment);
375
- const delta = await this.psm.signDeltaProposal({
376
- accountId: this._accountId,
377
- commitment,
378
- signature,
379
- });
380
- const resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
381
- if (!resolvedMetadata) {
382
- throw new Error('Missing proposal metadata');
383
- }
384
- const proposal = deltaToProposal({
385
- delta,
386
- proposalId: commitment,
387
- metadata: resolvedMetadata,
388
- signaturesRequired: signatureRequirementForProposal(resolvedMetadata, this.threshold, (proposalType) => this.getEffectiveThreshold(proposalType)),
389
- existingSignatures: existingProposal?.signatures,
390
- });
391
- this.proposals.set(proposal.id, proposal);
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 resolvedMetadata = resolveMetadata(delta, existingProposal?.metadata);
405
- if (!resolvedMetadata) {
406
- throw new Error('Missing proposal metadata');
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;
583
+ }
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);
417
591
  }
418
- async executeTransactionProposal(commitment) {
419
- const proposal = this.proposals.get(commitment);
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: ${commitment}`);
600
+ throw new Error(`Proposal not found: ${proposalId}`);
422
601
  }
423
- await executeProposalWorkflow({
424
- proposal,
425
- accountId: this._accountId,
426
- threshold: this.threshold,
427
- signerCommitments: this.signerCommitments,
428
- psmCommitment: this.psmCommitment,
429
- psmPublicKey: this.psmPublicKey,
430
- signatureScheme: this.signatureScheme,
431
- getEffectiveThreshold: (proposalType) => this.getEffectiveThreshold(proposalType),
432
- psm: this.psm,
433
- webClient: this.webClient,
434
- });
435
- proposal.status = { type: 'finalized' };
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';
729
+ }
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
+ };
436
754
  }
437
- exportTransactionProposalToJson(commitment) {
438
- const proposal = this.proposals.get(commitment);
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: ${commitment}`);
764
+ throw new Error(`Proposal not found in local cache: ${proposalId}`);
441
765
  }
442
766
  const exported = {
443
767
  accountId: proposal.accountId,
@@ -447,96 +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
- 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) {
457
789
  const exported = JSON.parse(json);
458
- if (!exported.accountId || !exported.txSummaryBase64 || !exported.commitment) {
790
+ if (!exported.accountId || !exported.txSummaryBase64 || !exported.commitment || !exported.metadata) {
459
791
  throw new Error('Invalid proposal JSON: missing required fields');
460
792
  }
461
- if (exported.accountId.toLowerCase() !== this._accountId.toLowerCase()) {
462
- throw new Error(`Proposal is for a different account: ${exported.accountId}`);
463
- }
464
- const computedCommitment = computeCommitmentFromTxSummary(exported.txSummaryBase64);
465
- if (computedCommitment !== exported.commitment) {
466
- throw new Error('Invalid proposal: commitment does not match tx_summary');
467
- }
468
- const metadata = exported.metadata ?? {
469
- proposalType: 'unknown',
470
- description: '',
471
- };
472
- const signaturesCollected = exported.signatures.length;
473
- const signaturesRequired = this.getEffectiveThreshold(metadata.proposalType);
474
- const status = signaturesCollected >= signaturesRequired
475
- ? { type: 'ready' }
476
- : {
477
- type: 'pending',
478
- signaturesCollected,
479
- signaturesRequired,
480
- signers: exported.signatures.map((s) => s.commitment),
481
- };
482
- const proposal = {
483
- id: exported.commitment,
484
- commitment: exported.commitment,
485
- accountId: exported.accountId,
486
- nonce: exported.nonce,
487
- status,
488
- txSummary: exported.txSummaryBase64,
489
- signatures: exported.signatures.map((s) => ({
490
- signerId: s.commitment,
491
- signature: { scheme: this.signer.scheme, signature: s.signatureHex },
492
- timestamp: s.timestamp || new Date().toISOString(),
493
- })),
494
- metadata,
495
- };
793
+ const proposal = this.proposalFactory().fromExported(exported);
794
+ await this.verifyProposalMetadataBinding(proposal);
496
795
  this.proposals.set(proposal.id, proposal);
497
- const proposals = this.listTransactionProposals();
498
- return { proposal, proposals };
796
+ return proposal;
499
797
  }
500
- async signTransactionProposalOffline(commitment) {
501
- const proposal = this.proposals.get(commitment);
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);
502
807
  if (!proposal) {
503
- throw new Error(`Proposal not found: ${commitment}`);
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}`);
504
820
  }
505
- const alreadySigned = proposal.signatures.some((s) => s.signerId.toLowerCase() === this.signer.commitment.toLowerCase());
821
+ // Check if already signed
822
+ const alreadySigned = existingSignatures.hasSigner(signerCommitment);
506
823
  if (alreadySigned) {
507
824
  throw new Error('You have already signed this proposal');
508
825
  }
509
- const signatureHex = await this.signer.signCommitment(commitment);
510
- const sigEntry = toPsmSignature(this.signer.scheme, signatureHex, this.signer.publicKey);
511
- proposal.signatures.push({
512
- signerId: this.signer.commitment,
513
- signature: sigEntry,
514
- timestamp: new Date().toISOString(),
515
- });
516
- const signaturesCollected = proposal.signatures.length;
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
517
841
  const proposalType = proposal.metadata?.proposalType;
518
- const effectiveThreshold = proposalType
842
+ const signaturesRequired = proposalType
519
843
  ? this.getEffectiveThreshold(proposalType)
520
844
  : this.threshold;
521
- if (signaturesCollected >= effectiveThreshold) {
522
- proposal.status = { type: 'ready' };
523
- }
524
- else if (proposal.status.type === 'pending') {
525
- proposal.status = {
526
- type: 'pending',
527
- signaturesCollected,
528
- signaturesRequired: effectiveThreshold,
529
- signers: proposal.signatures.map((s) => s.signerId),
530
- };
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}`);
531
854
  }
532
- return this.exportTransactionProposalToJson(commitment);
855
+ return txSummaryCommitment;
533
856
  }
534
- async createAndSyncFromRequest(request, metadata, nonce) {
535
- const summary = await executeForSummary(this.webClient, this._accountId, request);
536
- const summaryBase64 = uint8ArrayToBase64(summary.serialize());
537
- const proposalNonce = nonce ?? Date.now();
538
- const proposal = await this.createProposal(proposalNonce, summaryBase64, metadata);
539
- return { proposal, proposals: this.listTransactionProposals() };
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
+ }
540
901
  }
541
902
  }
542
903
  //# sourceMappingURL=multisig.js.map