@openzeppelin/miden-multisig-client 0.16.1 → 0.17.0-rc.1

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 (207) hide show
  1. package/README.md +192 -11
  2. package/dist/account/builder.d.ts.map +1 -1
  3. package/dist/account/builder.js +32 -25
  4. package/dist/account/builder.js.map +1 -1
  5. package/dist/account/builder.test.js +73 -21
  6. package/dist/account/builder.test.js.map +1 -1
  7. package/dist/account/layout.d.ts +31 -0
  8. package/dist/account/layout.d.ts.map +1 -0
  9. package/dist/account/layout.js +31 -0
  10. package/dist/account/layout.js.map +1 -0
  11. package/dist/account/masm/account-components/auth.d.ts +1 -4
  12. package/dist/account/masm/account-components/auth.d.ts.map +1 -1
  13. package/dist/account/masm/account-components/auth.js +36 -53
  14. package/dist/account/masm/account-components/auth.js.map +1 -1
  15. package/dist/account/masm/index.d.ts +0 -1
  16. package/dist/account/masm/index.d.ts.map +1 -1
  17. package/dist/account/masm/index.js +0 -1
  18. package/dist/account/masm/index.js.map +1 -1
  19. package/dist/account/storage.d.ts +4 -0
  20. package/dist/account/storage.d.ts.map +1 -1
  21. package/dist/account/storage.js +9 -20
  22. package/dist/account/storage.js.map +1 -1
  23. package/dist/client.d.ts +4 -0
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +10 -5
  26. package/dist/client.js.map +1 -1
  27. package/dist/client.test.js +74 -16
  28. package/dist/client.test.js.map +1 -1
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/inspector.d.ts +59 -1
  34. package/dist/inspector.d.ts.map +1 -1
  35. package/dist/inspector.js +157 -29
  36. package/dist/inspector.js.map +1 -1
  37. package/dist/inspector.test.js +246 -37
  38. package/dist/inspector.test.js.map +1 -1
  39. package/dist/multisig.d.ts +109 -33
  40. package/dist/multisig.d.ts.map +1 -1
  41. package/dist/multisig.js +307 -104
  42. package/dist/multisig.js.map +1 -1
  43. package/dist/multisig.test.js +582 -58
  44. package/dist/multisig.test.js.map +1 -1
  45. package/dist/procedures.d.ts +6 -7
  46. package/dist/procedures.d.ts.map +1 -1
  47. package/dist/procedures.js +6 -7
  48. package/dist/procedures.js.map +1 -1
  49. package/dist/proposal/metadata.d.ts.map +1 -1
  50. package/dist/proposal/metadata.js +13 -2
  51. package/dist/proposal/metadata.js.map +1 -1
  52. package/dist/proposal/metadata.test.js +57 -0
  53. package/dist/proposal/metadata.test.js.map +1 -1
  54. package/dist/prover/errors.d.ts +5 -0
  55. package/dist/prover/errors.d.ts.map +1 -1
  56. package/dist/prover/errors.js +7 -155
  57. package/dist/prover/errors.js.map +1 -1
  58. package/dist/prover/errors.test.js +5 -0
  59. package/dist/prover/errors.test.js.map +1 -1
  60. package/dist/prover/retry.d.ts +1 -6
  61. package/dist/prover/retry.d.ts.map +1 -1
  62. package/dist/prover/retry.js +5 -28
  63. package/dist/prover/retry.js.map +1 -1
  64. package/dist/prover/retry.test.js +1 -1
  65. package/dist/prover/retry.test.js.map +1 -1
  66. package/dist/prover/workflow.d.ts +4 -3
  67. package/dist/prover/workflow.d.ts.map +1 -1
  68. package/dist/prover/workflow.js +5 -2
  69. package/dist/prover/workflow.js.map +1 -1
  70. package/dist/prover/workflow.test.js +28 -3
  71. package/dist/prover/workflow.test.js.map +1 -1
  72. package/dist/retry/classify.d.ts +16 -0
  73. package/dist/retry/classify.d.ts.map +1 -0
  74. package/dist/retry/classify.js +187 -0
  75. package/dist/retry/classify.js.map +1 -0
  76. package/dist/retry/runtime.d.ts +13 -0
  77. package/dist/retry/runtime.d.ts.map +1 -0
  78. package/dist/retry/runtime.js +33 -0
  79. package/dist/retry/runtime.js.map +1 -0
  80. package/dist/rpc/config.d.ts +11 -0
  81. package/dist/rpc/config.d.ts.map +1 -0
  82. package/dist/rpc/config.js +17 -0
  83. package/dist/rpc/config.js.map +1 -0
  84. package/dist/rpc/config.test.d.ts +2 -0
  85. package/dist/rpc/config.test.d.ts.map +1 -0
  86. package/dist/rpc/config.test.js +24 -0
  87. package/dist/rpc/config.test.js.map +1 -0
  88. package/dist/rpc/errors.d.ts +2 -0
  89. package/dist/rpc/errors.d.ts.map +1 -0
  90. package/dist/rpc/errors.js +11 -0
  91. package/dist/rpc/errors.js.map +1 -0
  92. package/dist/rpc/errors.test.d.ts +2 -0
  93. package/dist/rpc/errors.test.d.ts.map +1 -0
  94. package/dist/rpc/errors.test.js +34 -0
  95. package/dist/rpc/errors.test.js.map +1 -0
  96. package/dist/rpc/retry.d.ts +4 -0
  97. package/dist/rpc/retry.d.ts.map +1 -0
  98. package/dist/rpc/retry.js +6 -0
  99. package/dist/rpc/retry.js.map +1 -0
  100. package/dist/rpc/retry.test.d.ts +2 -0
  101. package/dist/rpc/retry.test.d.ts.map +1 -0
  102. package/dist/rpc/retry.test.js +98 -0
  103. package/dist/rpc/retry.test.js.map +1 -0
  104. package/dist/transaction/index.d.ts +1 -1
  105. package/dist/transaction/index.d.ts.map +1 -1
  106. package/dist/transaction/index.js +1 -1
  107. package/dist/transaction/index.js.map +1 -1
  108. package/dist/transaction/p2id.d.ts +17 -6
  109. package/dist/transaction/p2id.d.ts.map +1 -1
  110. package/dist/transaction/p2id.js +22 -31
  111. package/dist/transaction/p2id.js.map +1 -1
  112. package/dist/transaction/p2id.test.js +62 -24
  113. package/dist/transaction/p2id.test.js.map +1 -1
  114. package/dist/transaction/summary.d.ts +45 -2
  115. package/dist/transaction/summary.d.ts.map +1 -1
  116. package/dist/transaction/summary.js +42 -2
  117. package/dist/transaction/summary.js.map +1 -1
  118. package/dist/transaction/summary.test.d.ts +2 -0
  119. package/dist/transaction/summary.test.d.ts.map +1 -0
  120. package/dist/transaction/summary.test.js +26 -0
  121. package/dist/transaction/summary.test.js.map +1 -0
  122. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  123. package/dist/transaction/updateGuardian.js +16 -18
  124. package/dist/transaction/updateGuardian.js.map +1 -1
  125. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  126. package/dist/transaction/updateProcedureThreshold.js +15 -17
  127. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  128. package/dist/transaction/updateSigners.d.ts +6 -1
  129. package/dist/transaction/updateSigners.d.ts.map +1 -1
  130. package/dist/transaction/updateSigners.js +20 -17
  131. package/dist/transaction/updateSigners.js.map +1 -1
  132. package/dist/transaction.d.ts +2 -2
  133. package/dist/transaction.d.ts.map +1 -1
  134. package/dist/transaction.js +1 -1
  135. package/dist/transaction.js.map +1 -1
  136. package/dist/types/proposal.d.ts +28 -4
  137. package/dist/types/proposal.d.ts.map +1 -1
  138. package/dist/types/proposal.js +18 -0
  139. package/dist/types/proposal.js.map +1 -1
  140. package/dist/types.d.ts +0 -1
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/utils/signature.d.ts +11 -7
  143. package/dist/utils/signature.d.ts.map +1 -1
  144. package/dist/utils/signature.js +24 -58
  145. package/dist/utils/signature.js.map +1 -1
  146. package/dist/utils/word.d.ts +7 -0
  147. package/dist/utils/word.d.ts.map +1 -1
  148. package/dist/utils/word.js +15 -0
  149. package/dist/utils/word.js.map +1 -1
  150. package/masm/account_components/auth/guarded_multisig.masm +42 -0
  151. package/package.json +7 -4
  152. package/src/account/builder.test.ts +111 -45
  153. package/src/account/builder.ts +45 -33
  154. package/src/account/layout.ts +33 -0
  155. package/src/account/masm/account-components/auth.ts +36 -56
  156. package/src/account/masm/index.ts +0 -1
  157. package/src/account/storage.ts +9 -22
  158. package/src/client.test.ts +87 -16
  159. package/src/client.ts +16 -3
  160. package/src/index.ts +27 -1
  161. package/src/inspector.test.ts +330 -38
  162. package/src/inspector.ts +196 -33
  163. package/src/multisig.test.ts +680 -63
  164. package/src/multisig.ts +400 -115
  165. package/src/procedures.ts +6 -7
  166. package/src/proposal/metadata.test.ts +76 -0
  167. package/src/proposal/metadata.ts +13 -2
  168. package/src/prover/errors.test.ts +8 -0
  169. package/src/prover/errors.ts +7 -175
  170. package/src/prover/retry.test.ts +1 -1
  171. package/src/prover/retry.ts +10 -35
  172. package/src/prover/workflow.test.ts +40 -4
  173. package/src/prover/workflow.ts +16 -4
  174. package/src/retry/classify.ts +220 -0
  175. package/src/retry/runtime.ts +45 -0
  176. package/src/rpc/config.test.ts +45 -0
  177. package/src/rpc/config.ts +30 -0
  178. package/src/rpc/errors.test.ts +69 -0
  179. package/src/rpc/errors.ts +12 -0
  180. package/src/rpc/retry.test.ts +144 -0
  181. package/src/rpc/retry.ts +12 -0
  182. package/src/transaction/index.ts +7 -1
  183. package/src/transaction/p2id.test.ts +112 -31
  184. package/src/transaction/p2id.ts +39 -38
  185. package/src/transaction/summary.test.ts +32 -0
  186. package/src/transaction/summary.ts +83 -4
  187. package/src/transaction/updateGuardian.ts +15 -25
  188. package/src/transaction/updateProcedureThreshold.ts +13 -29
  189. package/src/transaction/updateSigners.ts +38 -30
  190. package/src/transaction.ts +8 -1
  191. package/src/types/proposal.ts +43 -4
  192. package/src/types.ts +0 -1
  193. package/src/utils/signature.ts +32 -65
  194. package/src/utils/word.ts +17 -0
  195. package/dist/account/masm/auth.d.ts +0 -5
  196. package/dist/account/masm/auth.d.ts.map +0 -1
  197. package/dist/account/masm/auth.js +0 -1509
  198. package/dist/account/masm/auth.js.map +0 -1
  199. package/masm/account_components/auth/multisig.masm +0 -12
  200. package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
  201. package/masm/account_components/auth/multisig_guardian.masm +0 -16
  202. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
  203. package/masm/auth/guardian.masm +0 -199
  204. package/masm/auth/guardian_ecdsa.masm +0 -195
  205. package/masm/auth/multisig.masm +0 -554
  206. package/masm/auth/multisig_ecdsa.masm +0 -554
  207. package/src/account/masm/auth.ts +0 -1512
@@ -1,13 +1,24 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { Multisig } from './multisig.js';
3
3
  import { GuardianHttpClient } from '@openzeppelin/guardian-client';
4
- import { buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildUpdateSignersTransactionRequest, executeForSummary, } from './transaction.js';
5
- const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig, mockNoteFileDeserialize } = vi.hoisted(() => ({
4
+ import { buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildUpdateSignersTransactionRequest, chainAnchorFromBase64, executeForSummary, executeForSummaryAt, } from './transaction.js';
5
+ const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig, mockNoteFileDeserialize, mockGetSignerCommitments, mockGetGuardianCommitment, } = vi.hoisted(() => ({
6
6
  mockRpcGetAccountDetails: vi.fn(),
7
7
  mockAccountDeserialize: vi.fn(),
8
8
  mockDetectConfig: vi.fn(),
9
9
  mockNoteFileDeserialize: vi.fn(),
10
+ mockGetSignerCommitments: vi.fn(),
11
+ mockGetGuardianCommitment: vi.fn(),
10
12
  }));
13
+ const { MOCK_CHAIN_ANCHOR_B64, createMockChainAnchor } = vi.hoisted(() => {
14
+ const MOCK_CHAIN_ANCHOR_B64 = 'bW9jay1jaGFpbi1hbmNob3I=';
15
+ const createMockChainAnchor = () => ({
16
+ commitment: () => ({ toHex: () => '0x' + 'b'.repeat(64) }),
17
+ free: () => { },
18
+ serialize: () => new Uint8Array([9, 9, 9]),
19
+ });
20
+ return { MOCK_CHAIN_ANCHOR_B64, createMockChainAnchor };
21
+ });
11
22
  // Mock the Miden SDK
12
23
  vi.mock('@miden-sdk/miden-sdk', () => ({
13
24
  Account: {
@@ -33,9 +44,10 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
33
44
  toCommitment: () => ({
34
45
  toHex: () => '0x' + 'c'.repeat(64),
35
46
  }),
36
- salt: () => ({
37
- toHex: () => '0x' + 'd'.repeat(64),
47
+ blockCommitment: () => ({
48
+ toHex: () => '0x' + 'b'.repeat(64),
38
49
  }),
50
+ userParams: () => [0, 0, 0, 1, 2, 3, 4],
39
51
  serialize: () => new Uint8Array([1, 2, 3]),
40
52
  }),
41
53
  },
@@ -70,6 +82,12 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
70
82
  // Mock transaction module
71
83
  vi.mock('./transaction.js', () => ({
72
84
  executeForSummary: vi.fn(),
85
+ executeForSummaryAt: vi.fn(),
86
+ chainAnchorToBase64: vi.fn(() => MOCK_CHAIN_ANCHOR_B64),
87
+ chainAnchorFromBase64: vi.fn(() => createMockChainAnchor()),
88
+ summarySalt: vi.fn(() => ({
89
+ toHex: () => '0x' + 'd'.repeat(64),
90
+ })),
73
91
  buildUpdateSignersTransactionRequest: vi.fn().mockResolvedValue({
74
92
  request: {},
75
93
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
@@ -115,6 +133,9 @@ vi.mock('./utils/signature.js', async () => {
115
133
  values: [1, 2, 3],
116
134
  })),
117
135
  signatureHexToBytes: vi.fn((hex) => new Uint8Array([0, 1, 2, 3])),
136
+ // These tests use synthetic signature bytes to exercise advice routing;
137
+ // recoverability is covered by tests/ecdsa-advice-encoding.test.ts.
138
+ assertEcdsaSignatureRecoverable: vi.fn(),
118
139
  };
119
140
  });
120
141
  vi.mock('./utils/encoding.js', async () => {
@@ -124,11 +145,19 @@ vi.mock('./utils/encoding.js', async () => {
124
145
  normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').toLowerCase().padStart(64, '0')),
125
146
  };
126
147
  });
127
- vi.mock('./inspector.js', () => ({
128
- AccountInspector: {
129
- fromAccount: mockDetectConfig,
130
- },
131
- }));
148
+ // Keep the real assertCompleteDetectedConfig so refreshConfigFromAccount's
149
+ // fail-closed validation is exercised.
150
+ vi.mock('./inspector.js', async (importOriginal) => {
151
+ const actual = await importOriginal();
152
+ return {
153
+ ...actual,
154
+ AccountInspector: {
155
+ fromAccount: mockDetectConfig,
156
+ getSignerPublicKeyCommitments: mockGetSignerCommitments,
157
+ getGuardianPublicKeyCommitment: mockGetGuardianCommitment,
158
+ },
159
+ };
160
+ });
132
161
  // Mock fetch for GUARDIAN client
133
162
  const mockFetch = vi.fn();
134
163
  vi.stubGlobal('fetch', mockFetch);
@@ -157,6 +186,15 @@ describe('Multisig', () => {
157
186
  beforeEach(() => {
158
187
  mockFetch.mockReset();
159
188
  vi.mocked(executeForSummary).mockResolvedValue({
189
+ summary: {
190
+ toCommitment: () => ({
191
+ toHex: () => '0x' + 'c'.repeat(64),
192
+ }),
193
+ serialize: () => new Uint8Array([1, 2, 3]),
194
+ },
195
+ anchor: createMockChainAnchor(),
196
+ });
197
+ vi.mocked(executeForSummaryAt).mockResolvedValue({
160
198
  toCommitment: () => ({
161
199
  toHex: () => '0x' + 'c'.repeat(64),
162
200
  }),
@@ -175,7 +213,6 @@ describe('Multisig', () => {
175
213
  threshold: 1,
176
214
  numSigners: 1,
177
215
  signerCommitments: ['0x' + 'a'.repeat(64)],
178
- guardianEnabled: true,
179
216
  guardianCommitment: '0x' + 'c'.repeat(64),
180
217
  vaultBalances: [],
181
218
  procedureThresholds: new Map(),
@@ -291,6 +328,34 @@ describe('Multisig', () => {
291
328
  expect(multisig.accountId).toBe(accountId);
292
329
  });
293
330
  });
331
+ describe('history (issue #413)', () => {
332
+ it('delegates to the guardian client with the account id and options', async () => {
333
+ const config = {
334
+ threshold: 1,
335
+ signerCommitments: ['0x' + 'a'.repeat(64)],
336
+ guardianCommitment: '0x' + 'c'.repeat(64),
337
+ };
338
+ const multisig = createTestMultisig(config);
339
+ const page = {
340
+ entries: [
341
+ {
342
+ nonce: 3,
343
+ status: 'canonical',
344
+ timestamp: '2026-08-01T12:00:03Z',
345
+ newCommitment: '0x' + 'b'.repeat(64),
346
+ inputNotes: [],
347
+ outputNotes: [],
348
+ decodeWarnings: [],
349
+ },
350
+ ],
351
+ nextCursor: 'cursor-token',
352
+ };
353
+ const spy = vi.spyOn(guardian, 'getDeltaHistory').mockResolvedValue(page);
354
+ const result = await multisig.deltaHistory({ limit: 5, cursor: 'prev' });
355
+ expect(result).toBe(page);
356
+ expect(spy).toHaveBeenCalledWith('0x' + 'a'.repeat(30), { limit: 5, cursor: 'prev' });
357
+ });
358
+ });
294
359
  describe('signerCommitment', () => {
295
360
  it('should return signer commitment', () => {
296
361
  const config = {
@@ -302,6 +367,45 @@ describe('Multisig', () => {
302
367
  expect(multisig.signerCommitment).toBe(mockSigner.commitment);
303
368
  });
304
369
  });
370
+ describe('getSignerPublicKeyCommitments (issue #306)', () => {
371
+ const config = {
372
+ threshold: 1,
373
+ signerCommitments: ['0x' + 'a'.repeat(64)],
374
+ guardianCommitment: '0x' + 'c'.repeat(64),
375
+ };
376
+ it('reads commitments from the store-backed account', async () => {
377
+ const storeAccount = mockedAccount('0x' + 'b'.repeat(64), 1);
378
+ mockWebClient.getAccount.mockResolvedValueOnce(storeAccount);
379
+ const expected = ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)];
380
+ mockGetSignerCommitments.mockReturnValueOnce(expected);
381
+ const multisig = createTestMultisig(config);
382
+ const commitments = await multisig.getSignerPublicKeyCommitments();
383
+ expect(commitments).toEqual(expected);
384
+ expect(mockGetSignerCommitments).toHaveBeenCalledWith(storeAccount);
385
+ });
386
+ it('falls back to the account snapshot when the store has no record', async () => {
387
+ mockWebClient.getAccount.mockResolvedValueOnce(null);
388
+ mockGetSignerCommitments.mockReturnValueOnce(['0x' + '3'.repeat(64)]);
389
+ const multisig = createTestMultisig(config);
390
+ await multisig.getSignerPublicKeyCommitments();
391
+ expect(mockGetSignerCommitments).toHaveBeenCalledWith(mockAccount);
392
+ });
393
+ });
394
+ describe('getGuardianPublicKeyCommitment (issue #306)', () => {
395
+ it('reads the guardian commitment from the store-backed account', async () => {
396
+ const storeAccount = mockedAccount('0x' + 'b'.repeat(64), 1);
397
+ mockWebClient.getAccount.mockResolvedValueOnce(storeAccount);
398
+ mockGetGuardianCommitment.mockReturnValueOnce('0x' + '4'.repeat(64));
399
+ const multisig = createTestMultisig({
400
+ threshold: 1,
401
+ signerCommitments: ['0x' + 'a'.repeat(64)],
402
+ guardianCommitment: '0x' + 'c'.repeat(64),
403
+ });
404
+ const commitment = await multisig.getGuardianPublicKeyCommitment();
405
+ expect(commitment).toBe('0x' + '4'.repeat(64));
406
+ expect(mockGetGuardianCommitment).toHaveBeenCalledWith(storeAccount);
407
+ });
408
+ });
305
409
  describe('fetchState', () => {
306
410
  it('should fetch account state from GUARDIAN', async () => {
307
411
  const config = {
@@ -397,7 +501,6 @@ describe('Multisig', () => {
397
501
  threshold: 2,
398
502
  numSigners: 2,
399
503
  signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
400
- guardianEnabled: true,
401
504
  guardianCommitment: '0x' + 'd'.repeat(64),
402
505
  vaultBalances: [],
403
506
  procedureThresholds: new Map(),
@@ -411,6 +514,40 @@ describe('Multisig', () => {
411
514
  expect(multisig.guardianCommitment).toBe('0x' + 'd'.repeat(64));
412
515
  expect(mockWebClient.newAccount).not.toHaveBeenCalled();
413
516
  });
517
+ it('keeps the previous config when a refresh reads an incomplete signer set (issue #306 review)', async () => {
518
+ const config = {
519
+ threshold: 1,
520
+ signerCommitments: ['0x' + 'a'.repeat(64)],
521
+ guardianCommitment: '0x' + 'c'.repeat(64),
522
+ };
523
+ const multisig = createTestMultisig(config, mockSigner, '0x' + 'a'.repeat(30));
524
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
525
+ mockFetch.mockResolvedValueOnce({
526
+ ok: true,
527
+ json: async () => ({
528
+ account_id: multisig.accountId,
529
+ commitment: '0x' + 'b'.repeat(64),
530
+ state_json: { data: 'AQID' },
531
+ created_at: '2024-01-01T00:00:00Z',
532
+ updated_at: '2024-01-02T00:00:00Z',
533
+ }),
534
+ });
535
+ // Storage reports 3 signers but only 1 entry was readable: adopting
536
+ // this would let membership proposals rewrite the on-chain set without
537
+ // the omitted keys. The refresh must keep the previous config instead.
538
+ mockDetectConfig.mockReturnValueOnce({
539
+ threshold: 2,
540
+ numSigners: 3,
541
+ signerCommitments: ['0x' + '1'.repeat(64)],
542
+ guardianCommitment: '0x' + 'd'.repeat(64),
543
+ vaultBalances: [],
544
+ procedureThresholds: new Map(),
545
+ });
546
+ await multisig.syncState();
547
+ expect(multisig.threshold).toBe(1);
548
+ expect(multisig.signerCommitments).toEqual(['0x' + 'a'.repeat(64)]);
549
+ expect(multisig.guardianCommitment).toBe('0x' + 'c'.repeat(64));
550
+ });
414
551
  it('should overwrite local state when account is not found on-chain', async () => {
415
552
  const config = {
416
553
  threshold: 1,
@@ -686,6 +823,7 @@ describe('Multisig', () => {
686
823
  signatures: [],
687
824
  metadata: {
688
825
  proposal_type: 'add_signer',
826
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
689
827
  target_threshold: 1,
690
828
  signer_commitments: ['0x' + 'a'.repeat(64)],
691
829
  description: '',
@@ -731,6 +869,7 @@ describe('Multisig', () => {
731
869
  signatures: [],
732
870
  metadata: {
733
871
  proposal_type: 'add_signer',
872
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
734
873
  target_threshold: 1,
735
874
  signer_commitments: ['0x' + 'a'.repeat(64)],
736
875
  description: '',
@@ -777,6 +916,7 @@ describe('Multisig', () => {
777
916
  signatures: [],
778
917
  metadata: {
779
918
  proposal_type: 'add_signer',
919
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
780
920
  target_threshold: 1,
781
921
  signer_commitments: ['0x' + 'a'.repeat(64)],
782
922
  description: '',
@@ -792,13 +932,65 @@ describe('Multisig', () => {
792
932
  ],
793
933
  }),
794
934
  });
795
- vi.mocked(executeForSummary).mockResolvedValueOnce({
935
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
796
936
  toCommitment: () => ({
797
937
  toHex: () => '0x' + 'f'.repeat(64),
798
938
  }),
799
939
  });
800
940
  await expect(multisig.syncProposals()).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
801
941
  });
942
+ /// A structurally valid anchor pinned to the wrong block must be rejected
943
+ /// before anything executes against it: its commitment disagrees with the
944
+ /// block commitment signed into the tx_summary.
945
+ it('should reject a proposal whose chain anchor does not match the summary block commitment', async () => {
946
+ const config = {
947
+ threshold: 1,
948
+ signerCommitments: ['0x' + 'a'.repeat(64)],
949
+ guardianCommitment: '0x' + 'c'.repeat(64),
950
+ };
951
+ const multisig = createTestMultisig(config);
952
+ mockFetch.mockResolvedValueOnce({
953
+ ok: true,
954
+ json: async () => ({
955
+ proposals: [
956
+ {
957
+ account_id: '0x' + 'a'.repeat(30),
958
+ nonce: 1,
959
+ prev_commitment: '0x' + 'b'.repeat(64),
960
+ delta_payload: {
961
+ tx_summary: { data: 'AQID' },
962
+ signatures: [],
963
+ metadata: {
964
+ proposal_type: 'add_signer',
965
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
966
+ target_threshold: 1,
967
+ signer_commitments: ['0x' + 'a'.repeat(64)],
968
+ description: '',
969
+ },
970
+ },
971
+ status: {
972
+ status: 'pending',
973
+ timestamp: '2024-01-01T00:00:00Z',
974
+ proposer_id: '0x' + 'c'.repeat(64),
975
+ cosigner_sigs: [],
976
+ },
977
+ },
978
+ ],
979
+ }),
980
+ });
981
+ // Deserializes fine, but pins a different block than the one bound into
982
+ // the summary (mock summary blockCommitment is 'b' * 64).
983
+ const freed = vi.fn();
984
+ vi.mocked(chainAnchorFromBase64).mockReturnValueOnce({
985
+ commitment: () => ({ toHex: () => '0x' + 'e'.repeat(64) }),
986
+ free: freed,
987
+ serialize: () => new Uint8Array([9, 9, 9]),
988
+ });
989
+ const reExecutionsBefore = vi.mocked(executeForSummaryAt).mock.calls.length;
990
+ await expect(multisig.syncProposals()).rejects.toThrow('chain anchor does not match the block commitment bound into the tx_summary');
991
+ expect(vi.mocked(executeForSummaryAt).mock.calls.length).toBe(reExecutionsBefore);
992
+ expect(freed).toHaveBeenCalledTimes(1);
993
+ });
802
994
  it('should reject non-32-byte signer IDs from GUARDIAN proposals', async () => {
803
995
  const config = {
804
996
  threshold: 1,
@@ -816,6 +1008,7 @@ describe('Multisig', () => {
816
1008
  signatures: [],
817
1009
  metadata: {
818
1010
  proposal_type: 'add_signer',
1011
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
819
1012
  target_threshold: 1,
820
1013
  signer_commitments: ['0x' + 'a'.repeat(64)],
821
1014
  description: '',
@@ -858,6 +1051,7 @@ describe('Multisig', () => {
858
1051
  signatures: [],
859
1052
  metadata: {
860
1053
  proposal_type: 'add_signer',
1054
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
861
1055
  target_threshold: 2,
862
1056
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
863
1057
  description: '',
@@ -932,6 +1126,7 @@ describe('Multisig', () => {
932
1126
  });
933
1127
  const proposal = await multisig.createProposal(1, 'AQID', {
934
1128
  proposalType: 'add_signer',
1129
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
935
1130
  targetThreshold: 1,
936
1131
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
937
1132
  description: '',
@@ -970,6 +1165,7 @@ describe('Multisig', () => {
970
1165
  });
971
1166
  await expect(multisig.createProposal(1, 'AQID', {
972
1167
  proposalType: 'add_signer',
1168
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
973
1169
  targetThreshold: 1,
974
1170
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
975
1171
  description: '',
@@ -1004,13 +1200,14 @@ describe('Multisig', () => {
1004
1200
  commitment: '0x' + 'c'.repeat(64),
1005
1201
  }),
1006
1202
  });
1007
- vi.mocked(executeForSummary).mockResolvedValueOnce({
1203
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1008
1204
  toCommitment: () => ({
1009
1205
  toHex: () => '0x' + 'f'.repeat(64),
1010
1206
  }),
1011
1207
  });
1012
1208
  await expect(multisig.createProposal(1, 'AQID', {
1013
1209
  proposalType: 'add_signer',
1210
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1014
1211
  targetThreshold: 1,
1015
1212
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1016
1213
  description: '',
@@ -1021,10 +1218,13 @@ describe('Multisig', () => {
1021
1218
  it('should include the faucet asset in the proposal description', async () => {
1022
1219
  const { executeForSummary } = await import('./transaction.js');
1023
1220
  vi.mocked(executeForSummary).mockResolvedValue({
1024
- toCommitment: () => ({
1025
- toHex: () => '0x' + 'c'.repeat(64),
1026
- }),
1027
- serialize: () => new Uint8Array([1, 2, 3]),
1221
+ summary: {
1222
+ toCommitment: () => ({
1223
+ toHex: () => '0x' + 'c'.repeat(64),
1224
+ }),
1225
+ serialize: () => new Uint8Array([1, 2, 3]),
1226
+ },
1227
+ anchor: createMockChainAnchor(),
1028
1228
  });
1029
1229
  const config = {
1030
1230
  threshold: 1,
@@ -1041,6 +1241,7 @@ describe('Multisig', () => {
1041
1241
  signatures: [],
1042
1242
  metadata: {
1043
1243
  proposal_type: 'p2id',
1244
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1044
1245
  recipient_id: '0xrecipient',
1045
1246
  faucet_id: '0xfaucet',
1046
1247
  amount: '100',
@@ -1061,17 +1262,20 @@ describe('Multisig', () => {
1061
1262
  commitment: '0x' + 'c'.repeat(64),
1062
1263
  }),
1063
1264
  });
1064
- const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1);
1265
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, { nonce: 1 });
1065
1266
  expect(proposal.metadata.description).toBe('Send 100 of asset 0xfaucet... to 0xrecipien...');
1066
1267
  });
1067
1268
  it('threads a private noteType into the request and wire metadata (issue #322)', async () => {
1068
1269
  const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
1069
1270
  const { NoteType } = await import('@miden-sdk/miden-sdk');
1070
1271
  vi.mocked(executeForSummary).mockResolvedValue({
1071
- toCommitment: () => ({
1072
- toHex: () => '0x' + 'c'.repeat(64),
1073
- }),
1074
- serialize: () => new Uint8Array([1, 2, 3]),
1272
+ summary: {
1273
+ toCommitment: () => ({
1274
+ toHex: () => '0x' + 'c'.repeat(64),
1275
+ }),
1276
+ serialize: () => new Uint8Array([1, 2, 3]),
1277
+ },
1278
+ anchor: createMockChainAnchor(),
1075
1279
  });
1076
1280
  const config = {
1077
1281
  threshold: 1,
@@ -1088,6 +1292,7 @@ describe('Multisig', () => {
1088
1292
  signatures: [],
1089
1293
  metadata: {
1090
1294
  proposal_type: 'p2id',
1295
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1091
1296
  recipient_id: '0xrecipient',
1092
1297
  faucet_id: '0xfaucet',
1093
1298
  amount: '100',
@@ -1109,14 +1314,15 @@ describe('Multisig', () => {
1109
1314
  commitment: '0x' + 'c'.repeat(64),
1110
1315
  }),
1111
1316
  });
1112
- const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1, {
1317
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, {
1318
+ nonce: 1,
1113
1319
  noteType: NoteType.Private,
1114
1320
  });
1115
1321
  // Propose path builds the private note...
1116
- expect(vi.mocked(buildP2idTransactionRequest)).toHaveBeenCalledWith(expect.any(String), '0xrecipient', '0xfaucet', 100n, expect.anything(), { noteType: NoteType.Private });
1322
+ expect(vi.mocked(buildP2idTransactionRequest)).toHaveBeenCalledWith(expect.any(String), '0xrecipient', '0xfaucet', 100n, { noteType: NoteType.Private });
1117
1323
  // ...and the rebuild-from-metadata path parses note_type back to Private.
1118
1324
  const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1);
1119
- expect(lastCall[5]).toMatchObject({ noteType: NoteType.Private });
1325
+ expect(lastCall[4]).toMatchObject({ noteType: NoteType.Private });
1120
1326
  // The pushed wire metadata carries note_type so cosigners rebuild the
1121
1327
  // same private note at verification/execution.
1122
1328
  const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)[1].body);
@@ -1124,6 +1330,121 @@ describe('Multisig', () => {
1124
1330
  expect(proposal.metadata.proposalType).toBe('p2id');
1125
1331
  expect(proposal.metadata.noteType).toBe('private');
1126
1332
  });
1333
+ it('threads P2IDE reclaim/timelock heights into the request and wire metadata (issue #366)', async () => {
1334
+ const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
1335
+ vi.mocked(executeForSummary).mockResolvedValue({
1336
+ summary: {
1337
+ toCommitment: () => ({
1338
+ toHex: () => '0x' + 'c'.repeat(64),
1339
+ }),
1340
+ serialize: () => new Uint8Array([1, 2, 3]),
1341
+ },
1342
+ anchor: createMockChainAnchor(),
1343
+ });
1344
+ const config = {
1345
+ threshold: 1,
1346
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1347
+ guardianCommitment: '0x' + 'c'.repeat(64),
1348
+ };
1349
+ const multisig = createTestMultisig(config);
1350
+ const mockDelta = {
1351
+ account_id: '0x' + 'a'.repeat(30),
1352
+ nonce: 1,
1353
+ prev_commitment: '0x' + 'b'.repeat(64),
1354
+ delta_payload: {
1355
+ tx_summary: { data: 'AQID' },
1356
+ signatures: [],
1357
+ metadata: {
1358
+ proposal_type: 'p2id',
1359
+ recipient_id: '0xrecipient',
1360
+ faucet_id: '0xfaucet',
1361
+ amount: '100',
1362
+ reclaim_height: 12345,
1363
+ timelock_height: 700,
1364
+ description: '',
1365
+ },
1366
+ },
1367
+ status: {
1368
+ status: 'pending',
1369
+ timestamp: '2024-01-01T00:00:00Z',
1370
+ proposer_id: '0x' + 'c'.repeat(64),
1371
+ cosigner_sigs: [],
1372
+ },
1373
+ };
1374
+ mockFetch.mockResolvedValueOnce({
1375
+ ok: true,
1376
+ json: async () => ({
1377
+ delta: mockDelta,
1378
+ commitment: '0x' + 'c'.repeat(64),
1379
+ }),
1380
+ });
1381
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, {
1382
+ nonce: 1,
1383
+ reclaimHeight: 12345,
1384
+ timelockHeight: 700,
1385
+ });
1386
+ // Propose path builds the P2IDE note from the heights...
1387
+ const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1);
1388
+ expect(lastCall[4]).toMatchObject({ reclaimHeight: 12345, timelockHeight: 700 });
1389
+ // ...and the pushed wire metadata carries the heights so cosigners
1390
+ // rebuild the same P2IDE note at verification/execution.
1391
+ const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)[1].body);
1392
+ expect(pushBody.delta_payload.metadata.reclaim_height).toBe(12345);
1393
+ expect(pushBody.delta_payload.metadata.timelock_height).toBe(700);
1394
+ expect(proposal.metadata.proposalType).toBe('p2id');
1395
+ expect(proposal.metadata).toMatchObject({ reclaimHeight: 12345, timelockHeight: 700 });
1396
+ });
1397
+ it('omits the heights from wire metadata for a plain P2ID send (pre-#366 shape)', async () => {
1398
+ const { executeForSummary } = await import('./transaction.js');
1399
+ vi.mocked(executeForSummary).mockResolvedValue({
1400
+ summary: {
1401
+ toCommitment: () => ({
1402
+ toHex: () => '0x' + 'c'.repeat(64),
1403
+ }),
1404
+ serialize: () => new Uint8Array([1, 2, 3]),
1405
+ },
1406
+ anchor: createMockChainAnchor(),
1407
+ });
1408
+ const config = {
1409
+ threshold: 1,
1410
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1411
+ guardianCommitment: '0x' + 'c'.repeat(64),
1412
+ };
1413
+ const multisig = createTestMultisig(config);
1414
+ const mockDelta = {
1415
+ account_id: '0x' + 'a'.repeat(30),
1416
+ nonce: 1,
1417
+ prev_commitment: '0x' + 'b'.repeat(64),
1418
+ delta_payload: {
1419
+ tx_summary: { data: 'AQID' },
1420
+ signatures: [],
1421
+ metadata: {
1422
+ proposal_type: 'p2id',
1423
+ recipient_id: '0xrecipient',
1424
+ faucet_id: '0xfaucet',
1425
+ amount: '100',
1426
+ description: '',
1427
+ },
1428
+ },
1429
+ status: {
1430
+ status: 'pending',
1431
+ timestamp: '2024-01-01T00:00:00Z',
1432
+ proposer_id: '0x' + 'c'.repeat(64),
1433
+ cosigner_sigs: [],
1434
+ },
1435
+ };
1436
+ mockFetch.mockResolvedValueOnce({
1437
+ ok: true,
1438
+ json: async () => ({
1439
+ delta: mockDelta,
1440
+ commitment: '0x' + 'c'.repeat(64),
1441
+ }),
1442
+ });
1443
+ await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, { nonce: 1 });
1444
+ const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)[1].body);
1445
+ expect('reclaim_height' in pushBody.delta_payload.metadata).toBe(false);
1446
+ expect('timelock_height' in pushBody.delta_payload.metadata).toBe(false);
1447
+ });
1127
1448
  });
1128
1449
  describe('exportNoteToBytes / importNoteFromBytes (issue #356)', () => {
1129
1450
  const config = {
@@ -1219,6 +1540,7 @@ describe('Multisig', () => {
1219
1540
  const proposal = {
1220
1541
  metadata: {
1221
1542
  proposalType: 'p2id',
1543
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1222
1544
  recipientId: '0x' + 'b'.repeat(30),
1223
1545
  faucetId: '0x' + 'c'.repeat(30),
1224
1546
  amount: '100',
@@ -1240,10 +1562,13 @@ describe('Multisig', () => {
1240
1562
  describe('createChangeThresholdProposal', () => {
1241
1563
  it('passes the signer scheme to update-signers requests', async () => {
1242
1564
  vi.mocked(executeForSummary).mockResolvedValue({
1243
- toCommitment: () => ({
1244
- toHex: () => '0x' + 'c'.repeat(64),
1245
- }),
1246
- serialize: () => new Uint8Array([1, 2, 3]),
1565
+ summary: {
1566
+ toCommitment: () => ({
1567
+ toHex: () => '0x' + 'c'.repeat(64),
1568
+ }),
1569
+ serialize: () => new Uint8Array([1, 2, 3]),
1570
+ },
1571
+ anchor: createMockChainAnchor(),
1247
1572
  });
1248
1573
  const ecdsaSigner = {
1249
1574
  ...mockSigner,
@@ -1265,6 +1590,7 @@ describe('Multisig', () => {
1265
1590
  signatures: [],
1266
1591
  metadata: {
1267
1592
  proposal_type: 'change_threshold',
1593
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1268
1594
  target_threshold: 2,
1269
1595
  description: '',
1270
1596
  },
@@ -1284,14 +1610,105 @@ describe('Multisig', () => {
1284
1610
  }),
1285
1611
  });
1286
1612
  const multisig = createTestMultisig(config, ecdsaSigner);
1287
- await multisig.createChangeThresholdProposal(2, 1);
1613
+ await multisig.createChangeThresholdProposal(2, { nonce: 1 });
1288
1614
  expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 2, config.signerCommitments, { signatureScheme: 'ecdsa' });
1289
1615
  });
1290
1616
  });
1617
+ describe('createAddSignerProposal / createRemoveSignerProposal', () => {
1618
+ const config = {
1619
+ threshold: 2,
1620
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'd'.repeat(64)],
1621
+ guardianCommitment: '0x' + 'c'.repeat(64),
1622
+ };
1623
+ const mockPushResponse = (proposalType) => {
1624
+ mockFetch.mockResolvedValueOnce({
1625
+ ok: true,
1626
+ json: async () => ({
1627
+ delta: {
1628
+ account_id: '0x' + 'a'.repeat(30),
1629
+ nonce: 1,
1630
+ prev_commitment: '0x' + 'b'.repeat(64),
1631
+ delta_payload: {
1632
+ tx_summary: { data: 'AQID' },
1633
+ signatures: [],
1634
+ metadata: { proposal_type: proposalType, description: '' },
1635
+ },
1636
+ status: {
1637
+ status: 'pending',
1638
+ timestamp: '2024-01-01T00:00:00Z',
1639
+ proposer_id: '0x' + 'c'.repeat(64),
1640
+ cosigner_sigs: [],
1641
+ },
1642
+ },
1643
+ commitment: '0x' + 'c'.repeat(64),
1644
+ }),
1645
+ });
1646
+ };
1647
+ beforeEach(() => {
1648
+ vi.mocked(executeForSummary).mockResolvedValue({
1649
+ summary: {
1650
+ toCommitment: () => ({ toHex: () => '0x' + 'c'.repeat(64) }),
1651
+ serialize: () => new Uint8Array([1, 2, 3]),
1652
+ },
1653
+ anchor: createMockChainAnchor(),
1654
+ });
1655
+ });
1656
+ it('add: passes newThreshold from the options bag and appends the commitment', async () => {
1657
+ mockPushResponse('add_signer');
1658
+ const newCommitment = '0x' + 'e'.repeat(64);
1659
+ const multisig = createTestMultisig(config);
1660
+ await multisig.createAddSignerProposal(newCommitment, { nonce: 1, newThreshold: 3 });
1661
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 3, [...config.signerCommitments, newCommitment], { signatureScheme: mockSigner.scheme });
1662
+ });
1663
+ it('add: defaults to the current threshold when newThreshold is omitted', async () => {
1664
+ mockPushResponse('add_signer');
1665
+ const multisig = createTestMultisig(config);
1666
+ await multisig.createAddSignerProposal('0x' + 'e'.repeat(64), { nonce: 1 });
1667
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, config.threshold, expect.any(Array), { signatureScheme: mockSigner.scheme });
1668
+ });
1669
+ it('remove: passes newThreshold from the options bag and drops the commitment', async () => {
1670
+ mockPushResponse('remove_signer');
1671
+ const multisig = createTestMultisig(config);
1672
+ await multisig.createRemoveSignerProposal('0x' + 'd'.repeat(64), { nonce: 1, newThreshold: 1 });
1673
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 1, [config.signerCommitments[0], config.signerCommitments[1]], { signatureScheme: mockSigner.scheme });
1674
+ });
1675
+ it('remove: defaults to min(current threshold, remaining signers) when newThreshold is omitted', async () => {
1676
+ mockPushResponse('remove_signer');
1677
+ const multisig = createTestMultisig(config);
1678
+ await multisig.createRemoveSignerProposal('0x' + 'd'.repeat(64), { nonce: 1 });
1679
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 2, expect.any(Array), { signatureScheme: mockSigner.scheme });
1680
+ });
1681
+ });
1682
+ describe('legacy positional-caller guard (issue #387)', () => {
1683
+ const config = {
1684
+ threshold: 1,
1685
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1686
+ guardianCommitment: '0x' + 'c'.repeat(64),
1687
+ };
1688
+ it('rejects a legacy positional nonce where the options bag is expected', async () => {
1689
+ const multisig = createTestMultisig(config);
1690
+ await expect(multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1)).rejects.toThrow(/issue #387/);
1691
+ await expect(multisig.createConsumeNotesProposal(['0x1'], 1)).rejects.toThrow(/issue #387/);
1692
+ await expect(multisig.createAddSignerProposal('0x' + 'e'.repeat(64), 1)).rejects.toThrow(/issue #387/);
1693
+ await expect(multisig.createCustomProposal(new Uint8Array([1]), 'label', 1)).rejects.toThrow(/issue #387/);
1694
+ });
1695
+ it('rejects a legacy trailing argument after the options slot', async () => {
1696
+ const multisig = createTestMultisig(config);
1697
+ // Pre-#387 pattern: createP2idProposal(r, f, amount, nonceHole, { noteType })
1698
+ await expect(multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, undefined, {
1699
+ noteType: 'private',
1700
+ })).rejects.toThrow(/issue #387/);
1701
+ // Pre-#387 pattern: createAddSignerProposal(commitment, nonceHole, newThreshold)
1702
+ await expect(multisig.createAddSignerProposal('0x' + 'e'.repeat(64), undefined, 3)).rejects.toThrow(/issue #387/);
1703
+ });
1704
+ });
1291
1705
  describe('createSwitchGuardianProposal', () => {
1292
1706
  it('should verify new endpoint commitment before creating proposal', async () => {
1293
1707
  vi.mocked(executeForSummary).mockResolvedValue({
1294
- serialize: () => new Uint8Array([1, 2, 3]),
1708
+ summary: {
1709
+ serialize: () => new Uint8Array([1, 2, 3]),
1710
+ },
1711
+ anchor: createMockChainAnchor(),
1295
1712
  });
1296
1713
  const config = {
1297
1714
  threshold: 1,
@@ -1331,7 +1748,10 @@ describe('Multisig', () => {
1331
1748
  });
1332
1749
  it('should reject switch proposal when endpoint commitment does not match', async () => {
1333
1750
  vi.mocked(executeForSummary).mockResolvedValue({
1334
- serialize: () => new Uint8Array([1, 2, 3]),
1751
+ summary: {
1752
+ serialize: () => new Uint8Array([1, 2, 3]),
1753
+ },
1754
+ anchor: createMockChainAnchor(),
1335
1755
  });
1336
1756
  const config = {
1337
1757
  threshold: 1,
@@ -1347,7 +1767,10 @@ describe('Multisig', () => {
1347
1767
  });
1348
1768
  it('should use the signer scheme when resolving new GUARDIAN commitments', async () => {
1349
1769
  vi.mocked(executeForSummary).mockResolvedValue({
1350
- serialize: () => new Uint8Array([1, 2, 3]),
1770
+ summary: {
1771
+ serialize: () => new Uint8Array([1, 2, 3]),
1772
+ },
1773
+ anchor: createMockChainAnchor(),
1351
1774
  });
1352
1775
  const ecdsaSigner = {
1353
1776
  ...mockSigner,
@@ -1392,10 +1815,13 @@ describe('Multisig', () => {
1392
1815
  describe('createUpdateProcedureThresholdProposal', () => {
1393
1816
  it('should create procedure-threshold update proposals', async () => {
1394
1817
  vi.mocked(executeForSummary).mockResolvedValue({
1395
- toCommitment: () => ({
1396
- toHex: () => '0x' + 'c'.repeat(64),
1397
- }),
1398
- serialize: () => new Uint8Array([1, 2, 3]),
1818
+ summary: {
1819
+ toCommitment: () => ({
1820
+ toHex: () => '0x' + 'c'.repeat(64),
1821
+ }),
1822
+ serialize: () => new Uint8Array([1, 2, 3]),
1823
+ },
1824
+ anchor: createMockChainAnchor(),
1399
1825
  });
1400
1826
  const config = {
1401
1827
  threshold: 2,
@@ -1412,6 +1838,7 @@ describe('Multisig', () => {
1412
1838
  signatures: [],
1413
1839
  metadata: {
1414
1840
  proposal_type: 'update_procedure_threshold',
1841
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1415
1842
  target_threshold: 1,
1416
1843
  target_procedure: 'send_asset',
1417
1844
  description: '',
@@ -1431,7 +1858,7 @@ describe('Multisig', () => {
1431
1858
  commitment: '0x' + 'c'.repeat(64),
1432
1859
  }),
1433
1860
  });
1434
- const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
1861
+ const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, { nonce: 1 });
1435
1862
  expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'falcon' });
1436
1863
  expect(proposal.metadata.proposalType).toBe('update_procedure_threshold');
1437
1864
  if (proposal.metadata.proposalType === 'update_procedure_threshold') {
@@ -1441,10 +1868,13 @@ describe('Multisig', () => {
1441
1868
  });
1442
1869
  it('passes the signer scheme to ECDSA procedure-threshold updates', async () => {
1443
1870
  vi.mocked(executeForSummary).mockResolvedValue({
1444
- toCommitment: () => ({
1445
- toHex: () => '0x' + 'c'.repeat(64),
1446
- }),
1447
- serialize: () => new Uint8Array([1, 2, 3]),
1871
+ summary: {
1872
+ toCommitment: () => ({
1873
+ toHex: () => '0x' + 'c'.repeat(64),
1874
+ }),
1875
+ serialize: () => new Uint8Array([1, 2, 3]),
1876
+ },
1877
+ anchor: createMockChainAnchor(),
1448
1878
  });
1449
1879
  const ecdsaSigner = {
1450
1880
  ...mockSigner,
@@ -1467,6 +1897,7 @@ describe('Multisig', () => {
1467
1897
  signatures: [],
1468
1898
  metadata: {
1469
1899
  proposal_type: 'update_procedure_threshold',
1900
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1470
1901
  target_threshold: 1,
1471
1902
  target_procedure: 'send_asset',
1472
1903
  description: '',
@@ -1486,7 +1917,7 @@ describe('Multisig', () => {
1486
1917
  commitment: '0x' + 'c'.repeat(64),
1487
1918
  }),
1488
1919
  });
1489
- await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
1920
+ await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, { nonce: 1 });
1490
1921
  expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'ecdsa' });
1491
1922
  });
1492
1923
  });
@@ -1523,6 +1954,7 @@ describe('Multisig', () => {
1523
1954
  });
1524
1955
  await multisig.createProposal(1, 'AQID', {
1525
1956
  proposalType: 'add_signer',
1957
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1526
1958
  targetThreshold: 1,
1527
1959
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1528
1960
  description: '',
@@ -1545,6 +1977,7 @@ describe('Multisig', () => {
1545
1977
  ...mockDelta.delta_payload,
1546
1978
  metadata: {
1547
1979
  proposal_type: 'add_signer',
1980
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1548
1981
  description: '',
1549
1982
  target_threshold: 1,
1550
1983
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -1591,11 +2024,12 @@ describe('Multisig', () => {
1591
2024
  });
1592
2025
  await multisig.createProposal(1, 'AQID', {
1593
2026
  proposalType: 'add_signer',
2027
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1594
2028
  targetThreshold: 1,
1595
2029
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1596
2030
  description: '',
1597
2031
  });
1598
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2032
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1599
2033
  toCommitment: () => ({
1600
2034
  toHex: () => '0x' + 'f'.repeat(64),
1601
2035
  }),
@@ -1623,6 +2057,7 @@ describe('Multisig', () => {
1623
2057
  signatures: [],
1624
2058
  metadata: {
1625
2059
  proposal_type: 'add_signer',
2060
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1626
2061
  description: '',
1627
2062
  target_threshold: 1,
1628
2063
  signer_commitments: [mockSigner.commitment],
@@ -1650,7 +2085,7 @@ describe('Multisig', () => {
1650
2085
  guardianCommitment: '0x' + 'c'.repeat(64),
1651
2086
  };
1652
2087
  const multisig = createTestMultisig(config);
1653
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2088
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1654
2089
  toCommitment: () => ({
1655
2090
  toHex: () => '0x' + 'f'.repeat(64),
1656
2091
  }),
@@ -1663,6 +2098,7 @@ describe('Multisig', () => {
1663
2098
  signatures: [],
1664
2099
  metadata: {
1665
2100
  proposalType: 'add_signer',
2101
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1666
2102
  targetThreshold: 1,
1667
2103
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1668
2104
  description: '',
@@ -1678,7 +2114,7 @@ describe('Multisig', () => {
1678
2114
  guardianCommitment: '0x' + 'c'.repeat(64),
1679
2115
  };
1680
2116
  const multisig = createTestMultisig(config);
1681
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2117
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1682
2118
  toCommitment: () => ({
1683
2119
  toHex: () => '0x' + 'c'.repeat(64),
1684
2120
  }),
@@ -1691,6 +2127,7 @@ describe('Multisig', () => {
1691
2127
  signatures: [],
1692
2128
  metadata: {
1693
2129
  proposalType: 'add_signer',
2130
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1694
2131
  targetThreshold: 1,
1695
2132
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1696
2133
  description: '',
@@ -1698,11 +2135,12 @@ describe('Multisig', () => {
1698
2135
  }));
1699
2136
  proposal.metadata = {
1700
2137
  proposalType: 'add_signer',
2138
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1701
2139
  targetThreshold: 2,
1702
2140
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1703
2141
  description: '',
1704
2142
  };
1705
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2143
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1706
2144
  toCommitment: () => ({
1707
2145
  toHex: () => '0x' + 'f'.repeat(64),
1708
2146
  }),
@@ -1728,6 +2166,7 @@ describe('Multisig', () => {
1728
2166
  signatures: [],
1729
2167
  metadata: {
1730
2168
  proposal_type: 'add_signer',
2169
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1731
2170
  description: '',
1732
2171
  target_threshold: 1,
1733
2172
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -1777,6 +2216,7 @@ describe('Multisig', () => {
1777
2216
  signatures: [],
1778
2217
  metadata: {
1779
2218
  proposal_type: 'change_threshold',
2219
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1780
2220
  description: '',
1781
2221
  target_threshold: 2,
1782
2222
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -1822,10 +2262,11 @@ describe('Multisig', () => {
1822
2262
  ok: false,
1823
2263
  status: 404,
1824
2264
  statusText: 'Not Found',
2265
+ headers: new Headers(),
1825
2266
  // Feature 009: only a conforming { code, message, meta } envelope is
1826
2267
  // folded into the error message; raw text bodies are dropped.
1827
2268
  text: async () => JSON.stringify({
1828
- code: 'GUARDIAN_PROPOSAL_NOT_FOUND',
2269
+ code: 'proposal_not_found',
1829
2270
  message: 'Proposal not found',
1830
2271
  meta: { retryable: false },
1831
2272
  }),
@@ -1854,6 +2295,7 @@ describe('Multisig', () => {
1854
2295
  ],
1855
2296
  metadata: {
1856
2297
  proposalType: 'add_signer',
2298
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1857
2299
  targetThreshold: 1,
1858
2300
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1859
2301
  description: '',
@@ -1885,6 +2327,7 @@ describe('Multisig', () => {
1885
2327
  ],
1886
2328
  metadata: {
1887
2329
  proposalType: 'change_threshold',
2330
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1888
2331
  targetThreshold: 1,
1889
2332
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1890
2333
  description: '',
@@ -1923,6 +2366,7 @@ describe('Multisig', () => {
1923
2366
  ],
1924
2367
  metadata: {
1925
2368
  proposalType: 'change_threshold',
2369
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1926
2370
  targetThreshold: 1,
1927
2371
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1928
2372
  description: '',
@@ -1944,6 +2388,7 @@ describe('Multisig', () => {
1944
2388
  signatures: [],
1945
2389
  metadata: {
1946
2390
  proposalType: 'add_signer',
2391
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1947
2392
  targetThreshold: 2,
1948
2393
  targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1949
2394
  description: '',
@@ -2009,6 +2454,7 @@ describe('Multisig', () => {
2009
2454
  ],
2010
2455
  metadata: {
2011
2456
  proposalType: 'change_threshold',
2457
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2012
2458
  targetThreshold: 1,
2013
2459
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2014
2460
  description: '',
@@ -2025,6 +2471,7 @@ describe('Multisig', () => {
2025
2471
  signatures: [],
2026
2472
  metadata: {
2027
2473
  proposal_type: 'change_threshold',
2474
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2028
2475
  target_threshold: 1,
2029
2476
  signer_commitments: ['0x' + 'a'.repeat(64)],
2030
2477
  },
@@ -2050,8 +2497,14 @@ describe('Multisig', () => {
2050
2497
  await expect(multisig.createTransactionProposalRequest(requestedProposalId)).resolves.toBe(finalRequest);
2051
2498
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2052
2499
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2053
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
2054
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
2500
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything());
2501
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything());
2502
+ // The advice payload now comes from the SDK, so the routing assertion is
2503
+ // the commitment each entry is keyed on: cosigner first, GUARDIAN ack
2504
+ // second. Swapping the two entries must not pass.
2505
+ const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
2506
+ expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
2507
+ expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
2055
2508
  expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2056
2509
  expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
2057
2510
  expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
@@ -2089,6 +2542,7 @@ describe('Multisig', () => {
2089
2542
  ],
2090
2543
  metadata: {
2091
2544
  proposalType: 'switch_guardian',
2545
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2092
2546
  newGuardianPubkey,
2093
2547
  newGuardianEndpoint: 'http://new-guardian.com',
2094
2548
  description: '',
@@ -2131,6 +2585,7 @@ describe('Multisig', () => {
2131
2585
  signatures: [],
2132
2586
  metadata: {
2133
2587
  proposal_type: 'add_signer',
2588
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2134
2589
  description: '',
2135
2590
  target_threshold: 2,
2136
2591
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -2165,7 +2620,7 @@ describe('Multisig', () => {
2165
2620
  };
2166
2621
  const multisig = createTestMultisig(config);
2167
2622
  const proposalId = '0x' + 'c'.repeat(64);
2168
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2623
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2169
2624
  toCommitment: () => ({
2170
2625
  toHex: () => '0x' + 'd'.repeat(64),
2171
2626
  }),
@@ -2185,6 +2640,7 @@ describe('Multisig', () => {
2185
2640
  ],
2186
2641
  metadata: {
2187
2642
  proposalType: 'change_threshold',
2643
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2188
2644
  targetThreshold: 1,
2189
2645
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2190
2646
  description: '',
@@ -2216,6 +2672,7 @@ describe('Multisig', () => {
2216
2672
  ],
2217
2673
  metadata: {
2218
2674
  proposalType: 'switch_guardian',
2675
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2219
2676
  newGuardianPubkey: '0x' + '1'.repeat(64),
2220
2677
  newGuardianEndpoint: 'http://new-guardian.com',
2221
2678
  description: '',
@@ -2256,6 +2713,7 @@ describe('Multisig', () => {
2256
2713
  ],
2257
2714
  metadata: {
2258
2715
  proposalType: 'switch_guardian',
2716
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2259
2717
  newGuardianPubkey: '0x' + '1'.repeat(64),
2260
2718
  newGuardianEndpoint: 'http://new-guardian.com',
2261
2719
  description: '',
@@ -2323,6 +2781,7 @@ describe('Multisig', () => {
2323
2781
  ],
2324
2782
  metadata: {
2325
2783
  proposalType: 'change_threshold',
2784
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2326
2785
  targetThreshold: 1,
2327
2786
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2328
2787
  description: '',
@@ -2339,6 +2798,7 @@ describe('Multisig', () => {
2339
2798
  signatures: [],
2340
2799
  metadata: {
2341
2800
  proposal_type: 'change_threshold',
2801
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2342
2802
  target_threshold: 1,
2343
2803
  signer_commitments: ['0x' + 'a'.repeat(64)],
2344
2804
  },
@@ -2409,6 +2869,7 @@ describe('Multisig', () => {
2409
2869
  ],
2410
2870
  metadata: {
2411
2871
  proposalType: 'switch_guardian',
2872
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2412
2873
  newGuardianPubkey: '0x' + '1'.repeat(64),
2413
2874
  newGuardianEndpoint: 'http://new-guardian.com',
2414
2875
  description: '',
@@ -2445,6 +2906,7 @@ describe('Multisig', () => {
2445
2906
  signatures: [],
2446
2907
  metadata: {
2447
2908
  proposal_type: 'add_signer',
2909
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2448
2910
  description: '',
2449
2911
  target_threshold: 2,
2450
2912
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -2472,7 +2934,7 @@ describe('Multisig', () => {
2472
2934
  // Proposal ID is mocked to return 'c'.repeat(64)
2473
2935
  await expect(multisig.executeProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
2474
2936
  });
2475
- it('should fail when GUARDIAN ack signature is missing (selector ON)', async () => {
2937
+ it('should fail when GUARDIAN ack signature is missing', async () => {
2476
2938
  const config = {
2477
2939
  threshold: 1,
2478
2940
  signerCommitments: ['0x' + 'a'.repeat(64)],
@@ -2488,6 +2950,7 @@ describe('Multisig', () => {
2488
2950
  signatures: [],
2489
2951
  metadata: {
2490
2952
  proposal_type: 'add_signer',
2953
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2491
2954
  description: '',
2492
2955
  target_threshold: 1,
2493
2956
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -2569,6 +3032,7 @@ describe('Multisig', () => {
2569
3032
  ],
2570
3033
  metadata: {
2571
3034
  proposalType: 'change_threshold',
3035
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2572
3036
  targetThreshold: 1,
2573
3037
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2574
3038
  description: '',
@@ -2585,6 +3049,7 @@ describe('Multisig', () => {
2585
3049
  signatures: [],
2586
3050
  metadata: {
2587
3051
  proposal_type: 'change_threshold',
3052
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2588
3053
  target_threshold: 1,
2589
3054
  signer_commitments: ['0x' + 'a'.repeat(64)],
2590
3055
  },
@@ -2625,8 +3090,14 @@ describe('Multisig', () => {
2625
3090
  expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2626
3091
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2627
3092
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2628
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
2629
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
3093
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything());
3094
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything());
3095
+ // The advice payload now comes from the SDK, so the routing assertion is
3096
+ // the commitment each entry is keyed on: cosigner first, GUARDIAN ack
3097
+ // second. Swapping the two entries must not pass.
3098
+ const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
3099
+ expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
3100
+ expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
2630
3101
  });
2631
3102
  it('should execute imported ECDSA proposals with scheme-aware advice', async () => {
2632
3103
  const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
@@ -2665,6 +3136,7 @@ describe('Multisig', () => {
2665
3136
  ],
2666
3137
  metadata: {
2667
3138
  proposalType: 'change_threshold',
3139
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2668
3140
  targetThreshold: 1,
2669
3141
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2670
3142
  description: '',
@@ -2681,6 +3153,7 @@ describe('Multisig', () => {
2681
3153
  signatures: [],
2682
3154
  metadata: {
2683
3155
  proposal_type: 'change_threshold',
3156
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2684
3157
  target_threshold: 1,
2685
3158
  signer_commitments: ['0x' + 'a'.repeat(64)],
2686
3159
  },
@@ -2710,8 +3183,14 @@ describe('Multisig', () => {
2710
3183
  expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2711
3184
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2712
3185
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2713
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
2714
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
3186
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything());
3187
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything());
3188
+ // The advice payload now comes from the SDK, so the routing assertion is
3189
+ // the commitment each entry is keyed on: cosigner first, GUARDIAN ack
3190
+ // second. Swapping the two entries must not pass.
3191
+ const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
3192
+ expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
3193
+ expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
2715
3194
  });
2716
3195
  it('should verify switch_guardian endpoint commitment before execution', async () => {
2717
3196
  const config = {
@@ -2737,6 +3216,7 @@ describe('Multisig', () => {
2737
3216
  ],
2738
3217
  metadata: {
2739
3218
  proposalType: 'switch_guardian',
3219
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2740
3220
  newGuardianPubkey,
2741
3221
  newGuardianEndpoint: 'http://new-guardian.com',
2742
3222
  description: '',
@@ -2758,6 +3238,7 @@ describe('Multisig', () => {
2758
3238
  signatures: [],
2759
3239
  metadata: {
2760
3240
  proposal_type: 'switch_guardian',
3241
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2761
3242
  new_guardian_pubkey: newGuardianPubkey,
2762
3243
  new_guardian_endpoint: 'http://new-guardian.com',
2763
3244
  },
@@ -2817,6 +3298,7 @@ describe('Multisig', () => {
2817
3298
  ],
2818
3299
  metadata: {
2819
3300
  proposalType: 'switch_guardian',
3301
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2820
3302
  newGuardianPubkey,
2821
3303
  newGuardianEndpoint: 'http://new-guardian.com',
2822
3304
  description: '',
@@ -2864,6 +3346,7 @@ describe('Multisig', () => {
2864
3346
  ],
2865
3347
  metadata: {
2866
3348
  proposalType: 'switch_guardian',
3349
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2867
3350
  newGuardianPubkey: '0x' + '1'.repeat(64),
2868
3351
  newGuardianEndpoint: 'http://new-guardian.com',
2869
3352
  description: '',
@@ -2904,6 +3387,7 @@ describe('Multisig', () => {
2904
3387
  ],
2905
3388
  metadata: {
2906
3389
  proposalType: 'switch_guardian',
3390
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2907
3391
  newGuardianPubkey: '0x' + '1'.repeat(64),
2908
3392
  newGuardianEndpoint: 'http://new-guardian.com',
2909
3393
  description: '',
@@ -2949,6 +3433,7 @@ describe('Multisig', () => {
2949
3433
  ],
2950
3434
  metadata: {
2951
3435
  proposalType: 'switch_guardian',
3436
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2952
3437
  newGuardianPubkey: '0x' + '1'.repeat(64),
2953
3438
  newGuardianEndpoint: 'http://new-guardian.com',
2954
3439
  description: '',
@@ -2974,9 +3459,32 @@ describe('Multisig', () => {
2974
3459
  .mockRejectedValueOnce(transient)
2975
3460
  .mockRejectedValueOnce(transient)
2976
3461
  .mockResolvedValueOnce({});
3462
+ mockFetch.mockResolvedValueOnce({
3463
+ ok: true,
3464
+ json: async () => ({
3465
+ account_id: '0x' + 'a'.repeat(30),
3466
+ nonce: 1,
3467
+ prev_commitment: '0x' + 'b'.repeat(64),
3468
+ delta_payload: {
3469
+ tx_summary: { data: 'AQID' },
3470
+ signatures: [],
3471
+ metadata: {
3472
+ proposal_type: 'b2agg',
3473
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3474
+ description: '',
3475
+ },
3476
+ },
3477
+ status: {
3478
+ status: 'pending',
3479
+ timestamp: '2024-01-01T00:00:00Z',
3480
+ proposer_id: '0x' + 'c'.repeat(64),
3481
+ cosigner_sigs: [],
3482
+ },
3483
+ }),
3484
+ });
2977
3485
  vi.useFakeTimers();
2978
3486
  try {
2979
- const submission = multisig.submitTransaction({});
3487
+ const submission = multisig.submitTransaction('0x' + 'c'.repeat(64), {});
2980
3488
  await vi.runAllTimersAsync();
2981
3489
  await submission;
2982
3490
  }
@@ -3001,6 +3509,7 @@ describe('Multisig', () => {
3001
3509
  signatures: [],
3002
3510
  metadata: {
3003
3511
  proposal_type: proposalType,
3512
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3004
3513
  description: '',
3005
3514
  },
3006
3515
  },
@@ -3031,6 +3540,7 @@ describe('Multisig', () => {
3031
3540
  };
3032
3541
  builtinDelta.delta_payload.metadata = {
3033
3542
  proposal_type: 'change_threshold',
3543
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3034
3544
  description: '',
3035
3545
  target_threshold: 1,
3036
3546
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -3063,7 +3573,7 @@ describe('Multisig', () => {
3063
3573
  const multisig = createTestMultisig(config);
3064
3574
  // Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
3065
3575
  // Make the binding request derive a different commitment so the check fails.
3066
- vi.mocked(executeForSummary).mockResolvedValueOnce({
3576
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
3067
3577
  toCommitment: () => ({
3068
3578
  toHex: () => '0x' + '9'.repeat(64),
3069
3579
  }),
@@ -3114,6 +3624,7 @@ describe('Multisig', () => {
3114
3624
  signatures: [],
3115
3625
  metadata: {
3116
3626
  proposal_type: 'add_signer',
3627
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3117
3628
  target_threshold: 2,
3118
3629
  signer_commitments: ['0x1', '0x2'],
3119
3630
  },
@@ -3134,6 +3645,7 @@ describe('Multisig', () => {
3134
3645
  });
3135
3646
  const proposal = await multisig.createProposal(1, 'AQID', {
3136
3647
  proposalType: 'add_signer',
3648
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3137
3649
  targetThreshold: 2,
3138
3650
  targetSignerCommitments: ['0x1', '0x2'],
3139
3651
  description: '',
@@ -3168,6 +3680,7 @@ describe('Multisig', () => {
3168
3680
  signatures: [],
3169
3681
  metadata: {
3170
3682
  proposal_type: 'p2id',
3683
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3171
3684
  recipient_id: '0xrecipient',
3172
3685
  faucet_id: '0xfaucet',
3173
3686
  amount: '100',
@@ -3207,6 +3720,7 @@ describe('Multisig', () => {
3207
3720
  signatures: [],
3208
3721
  metadata: {
3209
3722
  proposal_type: 'add_signer',
3723
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3210
3724
  target_threshold: 2,
3211
3725
  signer_commitments: ['0x1', '0x2'],
3212
3726
  description: '',
@@ -3228,6 +3742,7 @@ describe('Multisig', () => {
3228
3742
  });
3229
3743
  const proposal = await multisig.createProposal(1, 'AQID', {
3230
3744
  proposalType: 'consume_notes',
3745
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3231
3746
  noteIds: ['0xnote1', '0xnote2'],
3232
3747
  description: '',
3233
3748
  });
@@ -3249,6 +3764,7 @@ describe('Multisig', () => {
3249
3764
  signatures: [],
3250
3765
  metadata: {
3251
3766
  proposal_type: 'add_signer',
3767
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3252
3768
  target_threshold: 1,
3253
3769
  signer_commitments: ['0x' + 'a'.repeat(64)],
3254
3770
  description: '',
@@ -3270,6 +3786,7 @@ describe('Multisig', () => {
3270
3786
  });
3271
3787
  const proposal = await multisig.createProposal(1, 'AQID', {
3272
3788
  proposalType: 'p2id',
3789
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3273
3790
  recipientId: '0xrecipient',
3274
3791
  faucetId: '0xfaucet',
3275
3792
  amount: '100',
@@ -3293,6 +3810,7 @@ describe('Multisig', () => {
3293
3810
  signatures: [],
3294
3811
  metadata: {
3295
3812
  proposalType: 'add_signer',
3813
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3296
3814
  targetThreshold: 2,
3297
3815
  targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3298
3816
  description: '',
@@ -3314,6 +3832,7 @@ describe('Multisig', () => {
3314
3832
  });
3315
3833
  const proposal = await multisig.createProposal(1, 'AQID', {
3316
3834
  proposalType: 'switch_guardian',
3835
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3317
3836
  newGuardianPubkey: '0xnewpubkey',
3318
3837
  newGuardianEndpoint: 'http://new-guardian.com',
3319
3838
  description: '',
@@ -3340,6 +3859,7 @@ describe('Multisig', () => {
3340
3859
  signatures: [],
3341
3860
  metadata: {
3342
3861
  proposal_type: 'add_signer',
3862
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3343
3863
  target_threshold: 2,
3344
3864
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3345
3865
  description: '',
@@ -3373,6 +3893,7 @@ describe('Multisig', () => {
3373
3893
  ...mockProposalsPending[0].delta_payload,
3374
3894
  metadata: {
3375
3895
  proposal_type: 'add_signer',
3896
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3376
3897
  target_threshold: 2,
3377
3898
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3378
3899
  description: '',
@@ -3464,6 +3985,7 @@ describe('Multisig', () => {
3464
3985
  signatures: [],
3465
3986
  metadata: {
3466
3987
  proposal_type: 'change_threshold',
3988
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3467
3989
  target_threshold: 3,
3468
3990
  signer_commitments: ['0xa', '0xb', '0xc'],
3469
3991
  salt: '0xlegacysalt',
@@ -3508,6 +4030,7 @@ describe('Multisig', () => {
3508
4030
  signatures: [],
3509
4031
  metadata: {
3510
4032
  proposal_type: 'p2id',
4033
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3511
4034
  recipient_id: '0xrecipient',
3512
4035
  faucet_id: '0xfaucet',
3513
4036
  amount: '12345',
@@ -3558,6 +4081,7 @@ describe('Multisig', () => {
3558
4081
  signatures: [],
3559
4082
  metadata: {
3560
4083
  proposal_type: 'switch_guardian',
4084
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3561
4085
  new_guardian_pubkey: '0xnewpubkey',
3562
4086
  new_guardian_endpoint: 'http://new-guardian.com',
3563
4087
  salt: '0xsalt',