@openzeppelin/miden-multisig-client 0.13.3 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +10 -13
  2. package/dist/account/builder.d.ts +3 -3
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +10 -10
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/builder.test.js +16 -14
  7. package/dist/account/builder.test.js.map +1 -1
  8. package/dist/account/masm/account-components/auth.d.ts +4 -4
  9. package/dist/account/masm/account-components/auth.d.ts.map +1 -1
  10. package/dist/account/masm/account-components/auth.js +8 -12
  11. package/dist/account/masm/account-components/auth.js.map +1 -1
  12. package/dist/account/masm/auth.d.ts +4 -4
  13. package/dist/account/masm/auth.d.ts.map +1 -1
  14. package/dist/account/masm/auth.js +226 -124
  15. package/dist/account/masm/auth.js.map +1 -1
  16. package/dist/account/storage.d.ts.map +1 -1
  17. package/dist/account/storage.js +24 -6
  18. package/dist/account/storage.js.map +1 -1
  19. package/dist/account/storage.test.d.ts +2 -0
  20. package/dist/account/storage.test.d.ts.map +1 -0
  21. package/dist/account/storage.test.js +73 -0
  22. package/dist/account/storage.test.js.map +1 -0
  23. package/dist/client.d.ts +9 -9
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +24 -14
  26. package/dist/client.js.map +1 -1
  27. package/dist/client.test.js +45 -8
  28. package/dist/client.test.js.map +1 -1
  29. package/dist/index.d.ts +6 -12
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +6 -12
  32. package/dist/index.js.map +1 -1
  33. package/dist/multisig/proposal/execution.d.ts +3 -2
  34. package/dist/multisig/proposal/execution.d.ts.map +1 -1
  35. package/dist/multisig/proposal/execution.js +8 -12
  36. package/dist/multisig/proposal/execution.js.map +1 -1
  37. package/dist/multisig.d.ts +10 -4
  38. package/dist/multisig.d.ts.map +1 -1
  39. package/dist/multisig.js +97 -71
  40. package/dist/multisig.js.map +1 -1
  41. package/dist/multisig.test.js +481 -29
  42. package/dist/multisig.test.js.map +1 -1
  43. package/dist/procedures.d.ts +8 -8
  44. package/dist/procedures.js +8 -8
  45. package/dist/raw-client.d.ts +13 -0
  46. package/dist/raw-client.d.ts.map +1 -0
  47. package/dist/raw-client.js +42 -0
  48. package/dist/raw-client.js.map +1 -0
  49. package/dist/raw-client.test.d.ts +2 -0
  50. package/dist/raw-client.test.d.ts.map +1 -0
  51. package/dist/raw-client.test.js +89 -0
  52. package/dist/raw-client.test.js.map +1 -0
  53. package/dist/signers/ecdsa.d.ts +3 -1
  54. package/dist/signers/ecdsa.d.ts.map +1 -1
  55. package/dist/signers/ecdsa.js +15 -1
  56. package/dist/signers/ecdsa.js.map +1 -1
  57. package/dist/signers/ecdsa.test.js +27 -4
  58. package/dist/signers/ecdsa.test.js.map +1 -1
  59. package/dist/signers/falcon.d.ts +3 -1
  60. package/dist/signers/falcon.d.ts.map +1 -1
  61. package/dist/signers/falcon.js +15 -1
  62. package/dist/signers/falcon.js.map +1 -1
  63. package/dist/signers/falcon.test.js +39 -3
  64. package/dist/signers/falcon.test.js.map +1 -1
  65. package/dist/transaction/consumeNotes.d.ts +2 -2
  66. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  67. package/dist/transaction/consumeNotes.js +5 -4
  68. package/dist/transaction/consumeNotes.js.map +1 -1
  69. package/dist/transaction/options.d.ts +1 -0
  70. package/dist/transaction/options.d.ts.map +1 -1
  71. package/dist/transaction/p2id.d.ts.map +1 -1
  72. package/dist/transaction/p2id.js +7 -10
  73. package/dist/transaction/p2id.js.map +1 -1
  74. package/dist/transaction/p2id.test.js +19 -9
  75. package/dist/transaction/p2id.test.js.map +1 -1
  76. package/dist/transaction/summary.d.ts +2 -2
  77. package/dist/transaction/summary.d.ts.map +1 -1
  78. package/dist/transaction/summary.js +4 -2
  79. package/dist/transaction/summary.js.map +1 -1
  80. package/dist/transaction/updateGuardian.d.ts +2 -2
  81. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  82. package/dist/transaction/updateGuardian.js +8 -11
  83. package/dist/transaction/updateGuardian.js.map +1 -1
  84. package/dist/transaction/updateProcedureThreshold.d.ts +2 -2
  85. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  86. package/dist/transaction/updateProcedureThreshold.js +8 -10
  87. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  88. package/dist/transaction/updateSigners.d.ts +2 -2
  89. package/dist/transaction/updateSigners.d.ts.map +1 -1
  90. package/dist/transaction/updateSigners.js +8 -10
  91. package/dist/transaction/updateSigners.js.map +1 -1
  92. package/dist/transaction.test.js +6 -6
  93. package/dist/transaction.test.js.map +1 -1
  94. package/dist/utils/key.d.ts.map +1 -1
  95. package/dist/utils/key.js +3 -3
  96. package/dist/utils/key.js.map +1 -1
  97. package/dist/utils/key.test.js +4 -2
  98. package/dist/utils/key.test.js.map +1 -1
  99. package/dist/utils/signature.d.ts +2 -0
  100. package/dist/utils/signature.d.ts.map +1 -1
  101. package/dist/utils/signature.js +10 -5
  102. package/dist/utils/signature.js.map +1 -1
  103. package/dist/utils/signature.test.js +9 -9
  104. package/dist/utils/signature.test.js.map +1 -1
  105. package/masm/account_components/auth/multisig.masm +2 -3
  106. package/masm/account_components/auth/multisig_ecdsa.masm +2 -3
  107. package/masm/account_components/auth/multisig_guardian.masm +2 -3
  108. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +2 -3
  109. package/masm/auth/guardian.masm +30 -8
  110. package/masm/auth/guardian_ecdsa.masm +24 -7
  111. package/masm/auth/multisig.masm +86 -55
  112. package/masm/auth/multisig_ecdsa.masm +86 -54
  113. package/package.json +3 -3
  114. package/src/account/builder.test.ts +16 -14
  115. package/src/account/builder.ts +13 -12
  116. package/src/account/masm/account-components/auth.ts +8 -12
  117. package/src/account/masm/auth.ts +226 -124
  118. package/src/account/storage.test.ts +90 -0
  119. package/src/account/storage.ts +27 -6
  120. package/src/client.test.ts +52 -8
  121. package/src/client.ts +39 -16
  122. package/src/index.ts +6 -12
  123. package/src/multisig/proposal/execution.ts +18 -14
  124. package/src/multisig.test.ts +572 -29
  125. package/src/multisig.ts +121 -83
  126. package/src/procedures.ts +8 -8
  127. package/src/raw-client.test.ts +133 -0
  128. package/src/raw-client.ts +75 -0
  129. package/src/signers/ecdsa.test.ts +35 -4
  130. package/src/signers/ecdsa.ts +18 -2
  131. package/src/signers/falcon.test.ts +50 -3
  132. package/src/signers/falcon.ts +18 -2
  133. package/src/transaction/consumeNotes.ts +16 -5
  134. package/src/transaction/options.ts +1 -0
  135. package/src/transaction/p2id.test.ts +19 -9
  136. package/src/transaction/p2id.ts +8 -13
  137. package/src/transaction/summary.ts +11 -4
  138. package/src/transaction/updateGuardian.ts +22 -14
  139. package/src/transaction/updateProcedureThreshold.ts +20 -15
  140. package/src/transaction/updateSigners.ts +22 -14
  141. package/src/transaction.test.ts +6 -6
  142. package/src/utils/key.test.ts +4 -2
  143. package/src/utils/key.ts +7 -3
  144. package/src/utils/signature.test.ts +10 -10
  145. package/src/utils/signature.ts +12 -5
@@ -48,7 +48,7 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
48
48
  insert: vi.fn(),
49
49
  })),
50
50
  FeltArray: vi.fn().mockImplementation((arr: any[]) => arr),
51
- Rpo256: {
51
+ Poseidon2: {
52
52
  hashElements: vi.fn().mockReturnValue({
53
53
  toHex: () => '0x' + 'e'.repeat(64),
54
54
  }),
@@ -121,6 +121,9 @@ function mockedAccount(commitmentHex: string, nonce = 0): any {
121
121
  commitment: () => ({
122
122
  toHex: () => commitmentHex,
123
123
  }),
124
+ to_commitment: () => ({
125
+ toHex: () => commitmentHex,
126
+ }),
124
127
  nonce: () => ({
125
128
  asInt: () => BigInt(nonce),
126
129
  }),
@@ -187,6 +190,11 @@ describe('Multisig', () => {
187
190
  proveTransaction: vi.fn(),
188
191
  submitProvenTransaction: vi.fn(),
189
192
  applyTransaction: vi.fn(),
193
+ submitNewTransaction: vi.fn(),
194
+ submitNewTransactionWithProver: vi.fn(),
195
+ transactions: {
196
+ submit: vi.fn(),
197
+ },
190
198
  getConsumableNotes: vi.fn().mockResolvedValue([]),
191
199
  syncState: vi.fn(),
192
200
  getAccount: vi.fn().mockResolvedValue(null),
@@ -384,11 +392,7 @@ describe('Multisig', () => {
384
392
  'https://rpc.devnet.miden.io'
385
393
  );
386
394
 
387
- mockWebClient.getAccount.mockResolvedValueOnce({
388
- commitment: () => ({
389
- toHex: () => '0x' + 'b'.repeat(64),
390
- }),
391
- });
395
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
392
396
  mockFetch.mockResolvedValueOnce({
393
397
  ok: true,
394
398
  json: async () => ({
@@ -576,11 +580,7 @@ describe('Multisig', () => {
576
580
  signerCommitments: ['0x' + 'a'.repeat(64)],
577
581
  guardianCommitment: '0x' + 'c'.repeat(64),
578
582
  };
579
- mockWebClient.getAccount.mockResolvedValueOnce({
580
- commitment: () => ({
581
- toHex: () => '0x' + 'b'.repeat(64),
582
- }),
583
- });
583
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
584
584
 
585
585
  const multisigWithRpc = new Multisig(
586
586
  mockAccount,
@@ -628,11 +628,7 @@ describe('Multisig', () => {
628
628
  signerCommitments: ['0x' + 'a'.repeat(64)],
629
629
  guardianCommitment: '0x' + 'c'.repeat(64),
630
630
  };
631
- mockWebClient.getAccount.mockResolvedValueOnce({
632
- commitment: () => ({
633
- toHex: () => '0x' + 'f'.repeat(64),
634
- }),
635
- });
631
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'f'.repeat(64), 0));
636
632
  mockRpcGetAccountDetails.mockResolvedValueOnce({
637
633
  commitment: () => ({
638
634
  toHex: () => '0x' + 'b'.repeat(64),
@@ -2048,6 +2044,561 @@ describe('Multisig', () => {
2048
2044
  });
2049
2045
  });
2050
2046
 
2047
+ describe('createTransactionProposalRequest', () => {
2048
+ it('should return a ready non-switch_guardian request without executing it', async () => {
2049
+ const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
2050
+ vi.mocked(signatureHexToBytes).mockClear();
2051
+ vi.mocked(buildSignatureAdviceEntry).mockClear();
2052
+
2053
+ const config = {
2054
+ threshold: 1,
2055
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2056
+ guardianCommitment: '0x' + 'c'.repeat(64),
2057
+ guardianPublicKey: '0x' + '1'.repeat(66),
2058
+ };
2059
+
2060
+ const ecdsaSigner: Signer = {
2061
+ ...mockSigner,
2062
+ scheme: 'ecdsa',
2063
+ publicKey: '0x' + '2'.repeat(66),
2064
+ };
2065
+
2066
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
2067
+ const cachedProposalId = '0x' + 'c'.repeat(64);
2068
+ const requestedProposalId = '0x' + 'C'.repeat(64);
2069
+ const cosignerPubkey = '0x' + '3'.repeat(66);
2070
+ const ackPubkey = '0x' + '4'.repeat(66);
2071
+ const cosignerSignature = '0x' + '5'.repeat(130);
2072
+ const ackSignature = '0x' + '6'.repeat(130);
2073
+ const finalRequest = { kind: 'final-change-threshold-request' };
2074
+
2075
+ vi.mocked(buildUpdateSignersTransactionRequest)
2076
+ .mockResolvedValueOnce({
2077
+ request: { kind: 'verify-change-threshold-request' },
2078
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
2079
+ configHash: { toHex: () => '0x' + 'e'.repeat(64) },
2080
+ } as any)
2081
+ .mockResolvedValueOnce({
2082
+ request: finalRequest,
2083
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
2084
+ configHash: { toHex: () => '0x' + 'e'.repeat(64) },
2085
+ } as any);
2086
+
2087
+ (multisig as any).proposals.set(cachedProposalId, {
2088
+ id: cachedProposalId,
2089
+ accountId: multisig.accountId,
2090
+ nonce: 1,
2091
+ status: 'ready',
2092
+ txSummary: 'AQID',
2093
+ signatures: [
2094
+ {
2095
+ signerId: '0x' + 'a'.repeat(64),
2096
+ signature: {
2097
+ scheme: 'ecdsa',
2098
+ signature: cosignerSignature,
2099
+ publicKey: cosignerPubkey,
2100
+ },
2101
+ timestamp: '2024-01-01T00:00:00Z',
2102
+ },
2103
+ ],
2104
+ metadata: {
2105
+ proposalType: 'change_threshold',
2106
+ targetThreshold: 1,
2107
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2108
+ description: '',
2109
+ },
2110
+ });
2111
+
2112
+ mockFetch.mockResolvedValueOnce({
2113
+ ok: true,
2114
+ json: async () => ({
2115
+ account_id: multisig.accountId,
2116
+ nonce: 1,
2117
+ prev_commitment: '0x' + 'b'.repeat(64),
2118
+ delta_payload: {
2119
+ tx_summary: { data: 'AQID' },
2120
+ signatures: [],
2121
+ metadata: {
2122
+ proposal_type: 'change_threshold',
2123
+ target_threshold: 1,
2124
+ signer_commitments: ['0x' + 'a'.repeat(64)],
2125
+ },
2126
+ },
2127
+ status: {
2128
+ status: 'pending',
2129
+ timestamp: '2024-01-01T00:00:00Z',
2130
+ proposer_id: '0x' + 'a'.repeat(64),
2131
+ cosigner_sigs: [],
2132
+ },
2133
+ }),
2134
+ });
2135
+ mockFetch.mockResolvedValueOnce({
2136
+ ok: true,
2137
+ json: async () => ({
2138
+ account_id: multisig.accountId,
2139
+ nonce: 1,
2140
+ ack_sig: ackSignature,
2141
+ ack_pubkey: ackPubkey,
2142
+ ack_scheme: 'ecdsa',
2143
+ }),
2144
+ });
2145
+
2146
+ await expect(
2147
+ multisig.createTransactionProposalRequest(requestedProposalId)
2148
+ ).resolves.toBe(finalRequest);
2149
+
2150
+ expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
2151
+ 1,
2152
+ cosignerSignature,
2153
+ 'ecdsa',
2154
+ );
2155
+ expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
2156
+ 2,
2157
+ ackSignature,
2158
+ 'ecdsa',
2159
+ );
2160
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(
2161
+ 1,
2162
+ expect.anything(),
2163
+ expect.anything(),
2164
+ expect.anything(),
2165
+ cosignerPubkey,
2166
+ cosignerSignature,
2167
+ );
2168
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(
2169
+ 2,
2170
+ expect.anything(),
2171
+ expect.anything(),
2172
+ expect.anything(),
2173
+ ackPubkey,
2174
+ ackSignature,
2175
+ );
2176
+ expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2177
+ expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
2178
+ expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
2179
+ expect(mockWebClient.applyTransaction).not.toHaveBeenCalled();
2180
+ });
2181
+
2182
+ it('should return a ready switch_guardian request without executing it', async () => {
2183
+ const config = {
2184
+ threshold: 1,
2185
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2186
+ guardianCommitment: '0x' + 'c'.repeat(64),
2187
+ };
2188
+
2189
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2190
+ const proposalId = '0x' + 'c'.repeat(64);
2191
+ const newGuardianPubkey = '0x' + '1'.repeat(64);
2192
+ const finalRequest = { kind: 'final-switch-guardian-request' };
2193
+
2194
+ vi.mocked(buildUpdateGuardianTransactionRequest)
2195
+ .mockResolvedValueOnce({
2196
+ request: { kind: 'verify-switch-guardian-request' },
2197
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
2198
+ } as any)
2199
+ .mockResolvedValueOnce({
2200
+ request: finalRequest,
2201
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
2202
+ } as any);
2203
+
2204
+ (multisig as any).proposals.set(proposalId, {
2205
+ id: proposalId,
2206
+ accountId: multisig.accountId,
2207
+ nonce: 1,
2208
+ status: 'ready',
2209
+ txSummary: 'AQID',
2210
+ signatures: [
2211
+ {
2212
+ signerId: '0x' + 'a'.repeat(64),
2213
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2214
+ timestamp: '2024-01-01T00:00:00Z',
2215
+ },
2216
+ ],
2217
+ metadata: {
2218
+ proposalType: 'switch_guardian',
2219
+ newGuardianPubkey,
2220
+ newGuardianEndpoint: 'http://new-guardian.com',
2221
+ description: '',
2222
+ },
2223
+ });
2224
+
2225
+ mockFetch.mockResolvedValueOnce({
2226
+ ok: true,
2227
+ json: async () => ({ commitment: newGuardianPubkey }),
2228
+ });
2229
+
2230
+ await expect(multisig.createTransactionProposalRequest(proposalId)).resolves.toBe(finalRequest);
2231
+
2232
+ expect(mockFetch).toHaveBeenCalledTimes(1);
2233
+ expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2234
+ expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
2235
+ expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
2236
+ expect(mockWebClient.applyTransaction).not.toHaveBeenCalled();
2237
+ });
2238
+
2239
+ it('should throw if proposal not found locally', async () => {
2240
+ const config = {
2241
+ threshold: 1,
2242
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2243
+ guardianCommitment: '0x' + 'c'.repeat(64),
2244
+ };
2245
+
2246
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2247
+
2248
+ await expect(
2249
+ multisig.createTransactionProposalRequest('0x' + 'nonexistent'.repeat(5))
2250
+ ).rejects.toThrow('Proposal not found');
2251
+ });
2252
+
2253
+ it('should throw if proposal is still pending', async () => {
2254
+ const config = {
2255
+ threshold: 2,
2256
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2257
+ guardianCommitment: '0x' + 'c'.repeat(64),
2258
+ };
2259
+
2260
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2261
+
2262
+ const mockProposals = [
2263
+ {
2264
+ account_id: '0x' + 'a'.repeat(30),
2265
+ nonce: 1,
2266
+ prev_commitment: '0x' + 'b'.repeat(64),
2267
+ delta_payload: {
2268
+ tx_summary: { data: 'AQID' },
2269
+ signatures: [],
2270
+ metadata: {
2271
+ proposal_type: 'add_signer',
2272
+ description: '',
2273
+ target_threshold: 2,
2274
+ signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2275
+ },
2276
+ },
2277
+ status: {
2278
+ status: 'pending',
2279
+ timestamp: '2024-01-01T00:00:00Z',
2280
+ proposer_id: '0x' + 'c'.repeat(64),
2281
+ cosigner_sigs: [
2282
+ {
2283
+ signer_id: '0x' + 'a'.repeat(64),
2284
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
2285
+ timestamp: '2024-01-01T00:00:00Z',
2286
+ },
2287
+ ],
2288
+ },
2289
+ },
2290
+ ];
2291
+
2292
+ mockFetch.mockResolvedValueOnce({
2293
+ ok: true,
2294
+ json: async () => ({ proposals: mockProposals }),
2295
+ });
2296
+
2297
+ await multisig.syncProposals();
2298
+
2299
+ await expect(
2300
+ multisig.createTransactionProposalRequest('0x' + 'c'.repeat(64))
2301
+ ).rejects.toThrow('not ready for execution');
2302
+ });
2303
+
2304
+ it('should throw when proposal metadata does not match tx_summary', async () => {
2305
+ const config = {
2306
+ threshold: 1,
2307
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2308
+ guardianCommitment: '0x' + 'c'.repeat(64),
2309
+ };
2310
+
2311
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2312
+ const proposalId = '0x' + 'c'.repeat(64);
2313
+
2314
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
2315
+ toCommitment: () => ({
2316
+ toHex: () => '0x' + 'd'.repeat(64),
2317
+ }),
2318
+ } as any);
2319
+
2320
+ (multisig as any).proposals.set(proposalId, {
2321
+ id: proposalId,
2322
+ accountId: multisig.accountId,
2323
+ nonce: 1,
2324
+ status: 'ready',
2325
+ txSummary: 'AQID',
2326
+ signatures: [
2327
+ {
2328
+ signerId: '0x' + 'a'.repeat(64),
2329
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2330
+ timestamp: '2024-01-01T00:00:00Z',
2331
+ },
2332
+ ],
2333
+ metadata: {
2334
+ proposalType: 'change_threshold',
2335
+ targetThreshold: 1,
2336
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2337
+ description: '',
2338
+ },
2339
+ });
2340
+
2341
+ await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow(
2342
+ `Invalid proposal: metadata does not match tx_summary for ${proposalId}`
2343
+ );
2344
+ expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2345
+ });
2346
+
2347
+ it('should reject switch_guardian requests when endpoint commitment mismatches', async () => {
2348
+ const config = {
2349
+ threshold: 1,
2350
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2351
+ guardianCommitment: '0x' + 'c'.repeat(64),
2352
+ };
2353
+
2354
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2355
+ const proposalId = '0x' + 'c'.repeat(64);
2356
+
2357
+ (multisig as any).proposals.set(proposalId, {
2358
+ id: proposalId,
2359
+ accountId: multisig.accountId,
2360
+ nonce: 1,
2361
+ status: 'ready',
2362
+ txSummary: 'AQID',
2363
+ signatures: [
2364
+ {
2365
+ signerId: '0x' + 'a'.repeat(64),
2366
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2367
+ timestamp: '2024-01-01T00:00:00Z',
2368
+ },
2369
+ ],
2370
+ metadata: {
2371
+ proposalType: 'switch_guardian',
2372
+ newGuardianPubkey: '0x' + '1'.repeat(64),
2373
+ newGuardianEndpoint: 'http://new-guardian.com',
2374
+ description: '',
2375
+ },
2376
+ });
2377
+
2378
+ mockFetch.mockResolvedValueOnce({
2379
+ ok: true,
2380
+ json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
2381
+ });
2382
+
2383
+ await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow(
2384
+ 'Refusing to use GUARDIAN endpoint'
2385
+ );
2386
+ expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2387
+ });
2388
+
2389
+ it('should reject duplicate normalized signer IDs during request creation', async () => {
2390
+ const config = {
2391
+ threshold: 2,
2392
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2393
+ guardianCommitment: '0x' + 'c'.repeat(64),
2394
+ };
2395
+
2396
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2397
+ const proposalId = '0x' + 'c'.repeat(64);
2398
+
2399
+ (multisig as any).proposals.set(proposalId, {
2400
+ id: proposalId,
2401
+ accountId: multisig.accountId,
2402
+ nonce: 1,
2403
+ status: 'ready',
2404
+ txSummary: 'AQID',
2405
+ signatures: [
2406
+ {
2407
+ signerId: '0x' + 'a'.repeat(64),
2408
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2409
+ timestamp: '2024-01-01T00:00:00Z',
2410
+ },
2411
+ {
2412
+ signerId: '0x' + 'A'.repeat(64),
2413
+ signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
2414
+ timestamp: '2024-01-01T00:00:01Z',
2415
+ },
2416
+ ],
2417
+ metadata: {
2418
+ proposalType: 'switch_guardian',
2419
+ newGuardianPubkey: '0x' + '1'.repeat(64),
2420
+ newGuardianEndpoint: 'http://new-guardian.com',
2421
+ description: '',
2422
+ },
2423
+ });
2424
+
2425
+ await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow(
2426
+ 'duplicate signatures for signer',
2427
+ );
2428
+ });
2429
+
2430
+ it('should build a fresh tx commitment word for each advice entry during request creation', async () => {
2431
+ const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
2432
+ const { Word } = await import('@miden-sdk/miden-sdk');
2433
+
2434
+ const originalAdviceImpl = vi.mocked(buildSignatureAdviceEntry).getMockImplementation();
2435
+ const originalWordFromHexImpl = vi.mocked(Word.fromHex).getMockImplementation();
2436
+
2437
+ try {
2438
+ vi.mocked(Word.fromHex).mockImplementation((hex: string) => {
2439
+ let consumed = false;
2440
+ return {
2441
+ toHex: () => hex,
2442
+ toFelts: () => {
2443
+ if (consumed) {
2444
+ throw new Error('Word already consumed');
2445
+ }
2446
+ consumed = true;
2447
+ return [1, 2, 3, 4];
2448
+ },
2449
+ } as any;
2450
+ });
2451
+
2452
+ vi.mocked(buildSignatureAdviceEntry).mockImplementation(
2453
+ (signerCommitment: any, message: any) => {
2454
+ message.toFelts();
2455
+ return {
2456
+ key: { toHex: () => signerCommitment.toHex() },
2457
+ values: [1, 2, 3],
2458
+ } as any;
2459
+ },
2460
+ );
2461
+
2462
+ const config = {
2463
+ threshold: 1,
2464
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2465
+ guardianCommitment: '0x' + 'c'.repeat(64),
2466
+ };
2467
+
2468
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2469
+ const proposalId = '0x' + 'c'.repeat(64);
2470
+ const finalRequest = { kind: 'fresh-message-word-request' };
2471
+
2472
+ vi.mocked(buildUpdateSignersTransactionRequest)
2473
+ .mockResolvedValueOnce({
2474
+ request: { kind: 'verify-change-threshold-request' },
2475
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
2476
+ configHash: { toHex: () => '0x' + 'e'.repeat(64) },
2477
+ } as any)
2478
+ .mockResolvedValueOnce({
2479
+ request: finalRequest,
2480
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
2481
+ configHash: { toHex: () => '0x' + 'e'.repeat(64) },
2482
+ } as any);
2483
+
2484
+ (multisig as any).proposals.set(proposalId, {
2485
+ id: proposalId,
2486
+ accountId: multisig.accountId,
2487
+ nonce: 1,
2488
+ status: 'ready',
2489
+ txSummary: 'AQID',
2490
+ signatures: [
2491
+ {
2492
+ signerId: '0x' + 'a'.repeat(64),
2493
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2494
+ timestamp: '2024-01-01T00:00:00Z',
2495
+ },
2496
+ ],
2497
+ metadata: {
2498
+ proposalType: 'change_threshold',
2499
+ targetThreshold: 1,
2500
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2501
+ description: '',
2502
+ },
2503
+ });
2504
+
2505
+ mockFetch.mockResolvedValueOnce({
2506
+ ok: true,
2507
+ json: async () => ({
2508
+ account_id: multisig.accountId,
2509
+ nonce: 1,
2510
+ prev_commitment: '0x' + 'b'.repeat(64),
2511
+ delta_payload: {
2512
+ tx_summary: { data: 'AQID' },
2513
+ signatures: [],
2514
+ metadata: {
2515
+ proposal_type: 'change_threshold',
2516
+ target_threshold: 1,
2517
+ signer_commitments: ['0x' + 'a'.repeat(64)],
2518
+ },
2519
+ },
2520
+ status: {
2521
+ status: 'pending',
2522
+ timestamp: '2024-01-01T00:00:00Z',
2523
+ proposer_id: '0x' + 'a'.repeat(64),
2524
+ cosigner_sigs: [],
2525
+ },
2526
+ }),
2527
+ });
2528
+ mockFetch.mockResolvedValueOnce({
2529
+ ok: true,
2530
+ json: async () => ({
2531
+ account_id: multisig.accountId,
2532
+ nonce: 1,
2533
+ ack_sig: '0x' + 'f'.repeat(128),
2534
+ ack_scheme: 'falcon',
2535
+ }),
2536
+ });
2537
+
2538
+ await expect(multisig.createTransactionProposalRequest(proposalId)).resolves.toBe(finalRequest);
2539
+ } finally {
2540
+ if (originalAdviceImpl) {
2541
+ vi.mocked(buildSignatureAdviceEntry).mockImplementation(originalAdviceImpl);
2542
+ }
2543
+ if (originalWordFromHexImpl) {
2544
+ vi.mocked(Word.fromHex).mockImplementation(originalWordFromHexImpl);
2545
+ }
2546
+ }
2547
+ });
2548
+
2549
+ it('should reject advice-map key collisions during request creation', async () => {
2550
+ const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
2551
+ vi.mocked(buildSignatureAdviceEntry)
2552
+ .mockImplementationOnce(() => ({
2553
+ key: { toHex: () => '0x' + 'f'.repeat(64) },
2554
+ values: [1, 2, 3],
2555
+ }) as any)
2556
+ .mockImplementationOnce(() => ({
2557
+ key: { toHex: () => '0x' + 'f'.repeat(64) },
2558
+ values: [1, 2, 3],
2559
+ }) as any);
2560
+
2561
+ const config = {
2562
+ threshold: 2,
2563
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2564
+ guardianCommitment: '0x' + 'c'.repeat(64),
2565
+ };
2566
+
2567
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2568
+ const proposalId = '0x' + 'c'.repeat(64);
2569
+
2570
+ (multisig as any).proposals.set(proposalId, {
2571
+ id: proposalId,
2572
+ accountId: multisig.accountId,
2573
+ nonce: 1,
2574
+ status: 'ready',
2575
+ txSummary: 'AQID',
2576
+ signatures: [
2577
+ {
2578
+ signerId: '0x' + 'a'.repeat(64),
2579
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2580
+ timestamp: '2024-01-01T00:00:00Z',
2581
+ },
2582
+ {
2583
+ signerId: '0x' + 'b'.repeat(64),
2584
+ signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
2585
+ timestamp: '2024-01-01T00:00:01Z',
2586
+ },
2587
+ ],
2588
+ metadata: {
2589
+ proposalType: 'switch_guardian',
2590
+ newGuardianPubkey: '0x' + '1'.repeat(64),
2591
+ newGuardianEndpoint: 'http://new-guardian.com',
2592
+ description: '',
2593
+ },
2594
+ });
2595
+
2596
+ await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow(
2597
+ 'Duplicate advice-map key detected',
2598
+ );
2599
+ });
2600
+ });
2601
+
2051
2602
  describe('executeProposal', () => {
2052
2603
  it('should throw if proposal not found locally', async () => {
2053
2604
  const config = {
@@ -2261,12 +2812,10 @@ describe('Multisig', () => {
2261
2812
  ack_scheme: 'ecdsa',
2262
2813
  }),
2263
2814
  });
2264
- mockWebClient.executeTransaction.mockResolvedValueOnce({});
2265
- mockWebClient.proveTransaction.mockResolvedValueOnce({});
2266
- mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
2267
- mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
2815
+ mockWebClient.transactions.submit.mockResolvedValueOnce({});
2268
2816
 
2269
2817
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2818
+ expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
2270
2819
 
2271
2820
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
2272
2821
  1,
@@ -2378,10 +2927,7 @@ describe('Multisig', () => {
2378
2927
  ack_scheme: 'ecdsa',
2379
2928
  }),
2380
2929
  });
2381
- mockWebClient.executeTransaction.mockResolvedValueOnce({});
2382
- mockWebClient.proveTransaction.mockResolvedValueOnce({});
2383
- mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
2384
- mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
2930
+ mockWebClient.transactions.submit.mockResolvedValueOnce({});
2385
2931
 
2386
2932
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2387
2933
 
@@ -2456,13 +3002,10 @@ describe('Multisig', () => {
2456
3002
  ok: true,
2457
3003
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
2458
3004
  });
2459
- mockWebClient.executeTransaction.mockResolvedValueOnce({});
2460
- mockWebClient.proveTransaction.mockResolvedValueOnce({});
2461
- mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
2462
- mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
3005
+ mockWebClient.transactions.submit.mockResolvedValueOnce({});
2463
3006
 
2464
3007
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2465
- expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3008
+ expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
2466
3009
  });
2467
3010
 
2468
3011
  it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {