@openzeppelin/miden-multisig-client 0.16.2 → 0.17.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +144 -7
  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.map +1 -1
  24. package/dist/client.js +5 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/client.test.js +68 -15
  27. package/dist/client.test.js.map +1 -1
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/inspector.d.ts +59 -1
  33. package/dist/inspector.d.ts.map +1 -1
  34. package/dist/inspector.js +157 -29
  35. package/dist/inspector.js.map +1 -1
  36. package/dist/inspector.test.js +246 -37
  37. package/dist/inspector.test.js.map +1 -1
  38. package/dist/multisig.d.ts +106 -32
  39. package/dist/multisig.d.ts.map +1 -1
  40. package/dist/multisig.js +289 -95
  41. package/dist/multisig.js.map +1 -1
  42. package/dist/multisig.test.js +581 -58
  43. package/dist/multisig.test.js.map +1 -1
  44. package/dist/procedures.d.ts +6 -7
  45. package/dist/procedures.d.ts.map +1 -1
  46. package/dist/procedures.js +6 -7
  47. package/dist/procedures.js.map +1 -1
  48. package/dist/proposal/metadata.d.ts.map +1 -1
  49. package/dist/proposal/metadata.js +13 -2
  50. package/dist/proposal/metadata.js.map +1 -1
  51. package/dist/proposal/metadata.test.js +57 -0
  52. package/dist/proposal/metadata.test.js.map +1 -1
  53. package/dist/prover/workflow.d.ts +3 -2
  54. package/dist/prover/workflow.d.ts.map +1 -1
  55. package/dist/prover/workflow.js +5 -2
  56. package/dist/prover/workflow.js.map +1 -1
  57. package/dist/prover/workflow.test.js +6 -4
  58. package/dist/prover/workflow.test.js.map +1 -1
  59. package/dist/transaction/index.d.ts +1 -1
  60. package/dist/transaction/index.d.ts.map +1 -1
  61. package/dist/transaction/index.js +1 -1
  62. package/dist/transaction/index.js.map +1 -1
  63. package/dist/transaction/p2id.d.ts +17 -6
  64. package/dist/transaction/p2id.d.ts.map +1 -1
  65. package/dist/transaction/p2id.js +22 -31
  66. package/dist/transaction/p2id.js.map +1 -1
  67. package/dist/transaction/p2id.test.js +62 -24
  68. package/dist/transaction/p2id.test.js.map +1 -1
  69. package/dist/transaction/summary.d.ts +45 -2
  70. package/dist/transaction/summary.d.ts.map +1 -1
  71. package/dist/transaction/summary.js +42 -2
  72. package/dist/transaction/summary.js.map +1 -1
  73. package/dist/transaction/summary.test.d.ts +2 -0
  74. package/dist/transaction/summary.test.d.ts.map +1 -0
  75. package/dist/transaction/summary.test.js +26 -0
  76. package/dist/transaction/summary.test.js.map +1 -0
  77. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  78. package/dist/transaction/updateGuardian.js +16 -18
  79. package/dist/transaction/updateGuardian.js.map +1 -1
  80. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  81. package/dist/transaction/updateProcedureThreshold.js +15 -17
  82. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  83. package/dist/transaction/updateSigners.d.ts +6 -1
  84. package/dist/transaction/updateSigners.d.ts.map +1 -1
  85. package/dist/transaction/updateSigners.js +20 -17
  86. package/dist/transaction/updateSigners.js.map +1 -1
  87. package/dist/transaction.d.ts +2 -2
  88. package/dist/transaction.d.ts.map +1 -1
  89. package/dist/transaction.js +1 -1
  90. package/dist/transaction.js.map +1 -1
  91. package/dist/types/proposal.d.ts +28 -4
  92. package/dist/types/proposal.d.ts.map +1 -1
  93. package/dist/types/proposal.js +18 -0
  94. package/dist/types/proposal.js.map +1 -1
  95. package/dist/types.d.ts +0 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/utils/signature.d.ts +11 -7
  98. package/dist/utils/signature.d.ts.map +1 -1
  99. package/dist/utils/signature.js +24 -58
  100. package/dist/utils/signature.js.map +1 -1
  101. package/dist/utils/word.d.ts +7 -0
  102. package/dist/utils/word.d.ts.map +1 -1
  103. package/dist/utils/word.js +15 -0
  104. package/dist/utils/word.js.map +1 -1
  105. package/masm/account_components/auth/guarded_multisig.masm +42 -0
  106. package/package.json +7 -4
  107. package/src/account/builder.test.ts +111 -45
  108. package/src/account/builder.ts +45 -33
  109. package/src/account/layout.ts +33 -0
  110. package/src/account/masm/account-components/auth.ts +36 -56
  111. package/src/account/masm/index.ts +0 -1
  112. package/src/account/storage.ts +9 -22
  113. package/src/client.test.ts +80 -15
  114. package/src/client.ts +5 -3
  115. package/src/index.ts +26 -1
  116. package/src/inspector.test.ts +330 -38
  117. package/src/inspector.ts +196 -33
  118. package/src/multisig.test.ts +679 -63
  119. package/src/multisig.ts +361 -107
  120. package/src/procedures.ts +6 -7
  121. package/src/proposal/metadata.test.ts +76 -0
  122. package/src/proposal/metadata.ts +13 -2
  123. package/src/prover/workflow.test.ts +9 -4
  124. package/src/prover/workflow.ts +15 -3
  125. package/src/transaction/index.ts +7 -1
  126. package/src/transaction/p2id.test.ts +112 -31
  127. package/src/transaction/p2id.ts +39 -38
  128. package/src/transaction/summary.test.ts +32 -0
  129. package/src/transaction/summary.ts +83 -4
  130. package/src/transaction/updateGuardian.ts +15 -25
  131. package/src/transaction/updateProcedureThreshold.ts +13 -29
  132. package/src/transaction/updateSigners.ts +38 -30
  133. package/src/transaction.ts +8 -1
  134. package/src/types/proposal.ts +43 -4
  135. package/src/types.ts +0 -1
  136. package/src/utils/signature.ts +32 -65
  137. package/src/utils/word.ts +17 -0
  138. package/dist/account/masm/auth.d.ts +0 -5
  139. package/dist/account/masm/auth.d.ts.map +0 -1
  140. package/dist/account/masm/auth.js +0 -1509
  141. package/dist/account/masm/auth.js.map +0 -1
  142. package/masm/account_components/auth/multisig.masm +0 -12
  143. package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
  144. package/masm/account_components/auth/multisig_guardian.masm +0 -16
  145. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
  146. package/masm/auth/guardian.masm +0 -199
  147. package/masm/auth/guardian_ecdsa.masm +0 -195
  148. package/masm/auth/multisig.masm +0 -554
  149. package/masm/auth/multisig_ecdsa.masm +0 -554
  150. package/src/account/masm/auth.ts +0 -1512
@@ -5,16 +5,38 @@ import {
5
5
  buildUpdateProcedureThresholdTransactionRequest,
6
6
  buildUpdateGuardianTransactionRequest,
7
7
  buildUpdateSignersTransactionRequest,
8
+ chainAnchorFromBase64,
8
9
  executeForSummary,
10
+ executeForSummaryAt,
9
11
  } from './transaction.js';
10
12
 
11
- const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig, mockNoteFileDeserialize } = vi.hoisted(() => ({
13
+ const {
14
+ mockRpcGetAccountDetails,
15
+ mockAccountDeserialize,
16
+ mockDetectConfig,
17
+ mockNoteFileDeserialize,
18
+ mockGetSignerCommitments,
19
+ mockGetGuardianCommitment,
20
+ } = vi.hoisted(() => ({
12
21
  mockRpcGetAccountDetails: vi.fn(),
13
22
  mockAccountDeserialize: vi.fn(),
14
23
  mockDetectConfig: vi.fn(),
15
24
  mockNoteFileDeserialize: vi.fn(),
25
+ mockGetSignerCommitments: vi.fn(),
26
+ mockGetGuardianCommitment: vi.fn(),
16
27
  }));
17
28
 
29
+ const { MOCK_CHAIN_ANCHOR_B64, createMockChainAnchor } = vi.hoisted(() => {
30
+ const MOCK_CHAIN_ANCHOR_B64 = 'bW9jay1jaGFpbi1hbmNob3I=';
31
+ const createMockChainAnchor = () =>
32
+ ({
33
+ commitment: () => ({ toHex: () => '0x' + 'b'.repeat(64) }),
34
+ free: () => {},
35
+ serialize: () => new Uint8Array([9, 9, 9]),
36
+ }) as never;
37
+ return { MOCK_CHAIN_ANCHOR_B64, createMockChainAnchor };
38
+ });
39
+
18
40
  // Mock the Miden SDK
19
41
  vi.mock('@miden-sdk/miden-sdk', () => ({
20
42
  Account: {
@@ -40,9 +62,10 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
40
62
  toCommitment: () => ({
41
63
  toHex: () => '0x' + 'c'.repeat(64),
42
64
  }),
43
- salt: () => ({
44
- toHex: () => '0x' + 'd'.repeat(64),
65
+ blockCommitment: () => ({
66
+ toHex: () => '0x' + 'b'.repeat(64),
45
67
  }),
68
+ userParams: () => [0, 0, 0, 1, 2, 3, 4],
46
69
  serialize: () => new Uint8Array([1, 2, 3]),
47
70
  }),
48
71
  },
@@ -78,6 +101,12 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
78
101
  // Mock transaction module
79
102
  vi.mock('./transaction.js', () => ({
80
103
  executeForSummary: vi.fn(),
104
+ executeForSummaryAt: vi.fn(),
105
+ chainAnchorToBase64: vi.fn(() => MOCK_CHAIN_ANCHOR_B64),
106
+ chainAnchorFromBase64: vi.fn(() => createMockChainAnchor()),
107
+ summarySalt: vi.fn(() => ({
108
+ toHex: () => '0x' + 'd'.repeat(64),
109
+ })),
81
110
  buildUpdateSignersTransactionRequest: vi.fn().mockResolvedValue({
82
111
  request: {},
83
112
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
@@ -122,6 +151,9 @@ vi.mock('./utils/signature.js', async () => {
122
151
  values: [1, 2, 3],
123
152
  })),
124
153
  signatureHexToBytes: vi.fn((hex: string) => new Uint8Array([0, 1, 2, 3])),
154
+ // These tests use synthetic signature bytes to exercise advice routing;
155
+ // recoverability is covered by tests/ecdsa-advice-encoding.test.ts.
156
+ assertEcdsaSignatureRecoverable: vi.fn(),
125
157
  };
126
158
  });
127
159
 
@@ -133,11 +165,19 @@ vi.mock('./utils/encoding.js', async () => {
133
165
  };
134
166
  });
135
167
 
136
- vi.mock('./inspector.js', () => ({
137
- AccountInspector: {
138
- fromAccount: mockDetectConfig,
139
- },
140
- }));
168
+ // Keep the real assertCompleteDetectedConfig so refreshConfigFromAccount's
169
+ // fail-closed validation is exercised.
170
+ vi.mock('./inspector.js', async (importOriginal) => {
171
+ const actual = await importOriginal<typeof import('./inspector.js')>();
172
+ return {
173
+ ...actual,
174
+ AccountInspector: {
175
+ fromAccount: mockDetectConfig,
176
+ getSignerPublicKeyCommitments: mockGetSignerCommitments,
177
+ getGuardianPublicKeyCommitment: mockGetGuardianCommitment,
178
+ },
179
+ };
180
+ });
141
181
 
142
182
  // Mock fetch for GUARDIAN client
143
183
  const mockFetch = vi.fn();
@@ -186,6 +226,15 @@ describe('Multisig', () => {
186
226
  beforeEach(() => {
187
227
  mockFetch.mockReset();
188
228
  vi.mocked(executeForSummary).mockResolvedValue({
229
+ summary: {
230
+ toCommitment: () => ({
231
+ toHex: () => '0x' + 'c'.repeat(64),
232
+ }),
233
+ serialize: () => new Uint8Array([1, 2, 3]),
234
+ },
235
+ anchor: createMockChainAnchor(),
236
+ } as any);
237
+ vi.mocked(executeForSummaryAt).mockResolvedValue({
189
238
  toCommitment: () => ({
190
239
  toHex: () => '0x' + 'c'.repeat(64),
191
240
  }),
@@ -204,7 +253,6 @@ describe('Multisig', () => {
204
253
  threshold: 1,
205
254
  numSigners: 1,
206
255
  signerCommitments: ['0x' + 'a'.repeat(64)],
207
- guardianEnabled: true,
208
256
  guardianCommitment: '0x' + 'c'.repeat(64),
209
257
  vaultBalances: [],
210
258
  procedureThresholds: new Map(),
@@ -350,6 +398,38 @@ describe('Multisig', () => {
350
398
  });
351
399
  });
352
400
 
401
+ describe('history (issue #413)', () => {
402
+ it('delegates to the guardian client with the account id and options', async () => {
403
+ const config = {
404
+ threshold: 1,
405
+ signerCommitments: ['0x' + 'a'.repeat(64)],
406
+ guardianCommitment: '0x' + 'c'.repeat(64),
407
+ };
408
+ const multisig = createTestMultisig(config);
409
+
410
+ const page = {
411
+ entries: [
412
+ {
413
+ nonce: 3,
414
+ status: 'canonical' as const,
415
+ timestamp: '2026-08-01T12:00:03Z',
416
+ newCommitment: '0x' + 'b'.repeat(64),
417
+ inputNotes: [],
418
+ outputNotes: [],
419
+ decodeWarnings: [],
420
+ },
421
+ ],
422
+ nextCursor: 'cursor-token',
423
+ };
424
+ const spy = vi.spyOn(guardian, 'getDeltaHistory').mockResolvedValue(page);
425
+
426
+ const result = await multisig.deltaHistory({ limit: 5, cursor: 'prev' });
427
+
428
+ expect(result).toBe(page);
429
+ expect(spy).toHaveBeenCalledWith('0x' + 'a'.repeat(30), { limit: 5, cursor: 'prev' });
430
+ });
431
+ });
432
+
353
433
  describe('signerCommitment', () => {
354
434
  it('should return signer commitment', () => {
355
435
  const config = {
@@ -363,6 +443,56 @@ describe('Multisig', () => {
363
443
  });
364
444
  });
365
445
 
446
+ describe('getSignerPublicKeyCommitments (issue #306)', () => {
447
+ const config = {
448
+ threshold: 1,
449
+ signerCommitments: ['0x' + 'a'.repeat(64)],
450
+ guardianCommitment: '0x' + 'c'.repeat(64),
451
+ };
452
+
453
+ it('reads commitments from the store-backed account', async () => {
454
+ const storeAccount = mockedAccount('0x' + 'b'.repeat(64), 1);
455
+ mockWebClient.getAccount.mockResolvedValueOnce(storeAccount);
456
+ const expected = ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)];
457
+ mockGetSignerCommitments.mockReturnValueOnce(expected);
458
+
459
+ const multisig = createTestMultisig(config);
460
+ const commitments = await multisig.getSignerPublicKeyCommitments();
461
+
462
+ expect(commitments).toEqual(expected);
463
+ expect(mockGetSignerCommitments).toHaveBeenCalledWith(storeAccount);
464
+ });
465
+
466
+ it('falls back to the account snapshot when the store has no record', async () => {
467
+ mockWebClient.getAccount.mockResolvedValueOnce(null);
468
+ mockGetSignerCommitments.mockReturnValueOnce(['0x' + '3'.repeat(64)]);
469
+
470
+ const multisig = createTestMultisig(config);
471
+ await multisig.getSignerPublicKeyCommitments();
472
+
473
+ expect(mockGetSignerCommitments).toHaveBeenCalledWith(mockAccount);
474
+ });
475
+ });
476
+
477
+ describe('getGuardianPublicKeyCommitment (issue #306)', () => {
478
+ it('reads the guardian commitment from the store-backed account', async () => {
479
+ const storeAccount = mockedAccount('0x' + 'b'.repeat(64), 1);
480
+ mockWebClient.getAccount.mockResolvedValueOnce(storeAccount);
481
+ mockGetGuardianCommitment.mockReturnValueOnce('0x' + '4'.repeat(64));
482
+
483
+ const multisig = createTestMultisig({
484
+ threshold: 1,
485
+ signerCommitments: ['0x' + 'a'.repeat(64)],
486
+ guardianCommitment: '0x' + 'c'.repeat(64),
487
+ });
488
+
489
+ const commitment = await multisig.getGuardianPublicKeyCommitment();
490
+
491
+ expect(commitment).toBe('0x' + '4'.repeat(64));
492
+ expect(mockGetGuardianCommitment).toHaveBeenCalledWith(storeAccount);
493
+ });
494
+ });
495
+
366
496
  describe('fetchState', () => {
367
497
  it('should fetch account state from GUARDIAN', async () => {
368
498
  const config = {
@@ -499,7 +629,6 @@ describe('Multisig', () => {
499
629
  threshold: 2,
500
630
  numSigners: 2,
501
631
  signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
502
- guardianEnabled: true,
503
632
  guardianCommitment: '0x' + 'd'.repeat(64),
504
633
  vaultBalances: [],
505
634
  procedureThresholds: new Map(),
@@ -516,6 +645,44 @@ describe('Multisig', () => {
516
645
  expect(mockWebClient.newAccount).not.toHaveBeenCalled();
517
646
  });
518
647
 
648
+ it('keeps the previous config when a refresh reads an incomplete signer set (issue #306 review)', async () => {
649
+ const config = {
650
+ threshold: 1,
651
+ signerCommitments: ['0x' + 'a'.repeat(64)],
652
+ guardianCommitment: '0x' + 'c'.repeat(64),
653
+ };
654
+ const multisig = createTestMultisig(config, mockSigner, '0x' + 'a'.repeat(30));
655
+
656
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
657
+ mockFetch.mockResolvedValueOnce({
658
+ ok: true,
659
+ json: async () => ({
660
+ account_id: multisig.accountId,
661
+ commitment: '0x' + 'b'.repeat(64),
662
+ state_json: { data: 'AQID' },
663
+ created_at: '2024-01-01T00:00:00Z',
664
+ updated_at: '2024-01-02T00:00:00Z',
665
+ }),
666
+ });
667
+ // Storage reports 3 signers but only 1 entry was readable: adopting
668
+ // this would let membership proposals rewrite the on-chain set without
669
+ // the omitted keys. The refresh must keep the previous config instead.
670
+ mockDetectConfig.mockReturnValueOnce({
671
+ threshold: 2,
672
+ numSigners: 3,
673
+ signerCommitments: ['0x' + '1'.repeat(64)],
674
+ guardianCommitment: '0x' + 'd'.repeat(64),
675
+ vaultBalances: [],
676
+ procedureThresholds: new Map(),
677
+ });
678
+
679
+ await multisig.syncState();
680
+
681
+ expect(multisig.threshold).toBe(1);
682
+ expect(multisig.signerCommitments).toEqual(['0x' + 'a'.repeat(64)]);
683
+ expect(multisig.guardianCommitment).toBe('0x' + 'c'.repeat(64));
684
+ });
685
+
519
686
  it('should overwrite local state when account is not found on-chain', async () => {
520
687
  const config = {
521
688
  threshold: 1,
@@ -923,6 +1090,7 @@ describe('Multisig', () => {
923
1090
  signatures: [],
924
1091
  metadata: {
925
1092
  proposal_type: 'add_signer',
1093
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
926
1094
  target_threshold: 1,
927
1095
  signer_commitments: ['0x' + 'a'.repeat(64)],
928
1096
  description: '',
@@ -974,6 +1142,7 @@ describe('Multisig', () => {
974
1142
  signatures: [],
975
1143
  metadata: {
976
1144
  proposal_type: 'add_signer',
1145
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
977
1146
  target_threshold: 1,
978
1147
  signer_commitments: ['0x' + 'a'.repeat(64)],
979
1148
  description: '',
@@ -1026,6 +1195,7 @@ describe('Multisig', () => {
1026
1195
  signatures: [],
1027
1196
  metadata: {
1028
1197
  proposal_type: 'add_signer',
1198
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1029
1199
  target_threshold: 1,
1030
1200
  signer_commitments: ['0x' + 'a'.repeat(64)],
1031
1201
  description: '',
@@ -1042,7 +1212,7 @@ describe('Multisig', () => {
1042
1212
  }),
1043
1213
  });
1044
1214
 
1045
- vi.mocked(executeForSummary).mockResolvedValueOnce({
1215
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1046
1216
  toCommitment: () => ({
1047
1217
  toHex: () => '0x' + 'f'.repeat(64),
1048
1218
  }),
@@ -1053,6 +1223,65 @@ describe('Multisig', () => {
1053
1223
  );
1054
1224
  });
1055
1225
 
1226
+ /// A structurally valid anchor pinned to the wrong block must be rejected
1227
+ /// before anything executes against it: its commitment disagrees with the
1228
+ /// block commitment signed into the tx_summary.
1229
+ it('should reject a proposal whose chain anchor does not match the summary block commitment', async () => {
1230
+ const config = {
1231
+ threshold: 1,
1232
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1233
+ guardianCommitment: '0x' + 'c'.repeat(64),
1234
+ };
1235
+
1236
+ const multisig = createTestMultisig(config);
1237
+
1238
+ mockFetch.mockResolvedValueOnce({
1239
+ ok: true,
1240
+ json: async () => ({
1241
+ proposals: [
1242
+ {
1243
+ account_id: '0x' + 'a'.repeat(30),
1244
+ nonce: 1,
1245
+ prev_commitment: '0x' + 'b'.repeat(64),
1246
+ delta_payload: {
1247
+ tx_summary: { data: 'AQID' },
1248
+ signatures: [],
1249
+ metadata: {
1250
+ proposal_type: 'add_signer',
1251
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1252
+ target_threshold: 1,
1253
+ signer_commitments: ['0x' + 'a'.repeat(64)],
1254
+ description: '',
1255
+ },
1256
+ },
1257
+ status: {
1258
+ status: 'pending',
1259
+ timestamp: '2024-01-01T00:00:00Z',
1260
+ proposer_id: '0x' + 'c'.repeat(64),
1261
+ cosigner_sigs: [],
1262
+ },
1263
+ },
1264
+ ],
1265
+ }),
1266
+ });
1267
+
1268
+ // Deserializes fine, but pins a different block than the one bound into
1269
+ // the summary (mock summary blockCommitment is 'b' * 64).
1270
+ const freed = vi.fn();
1271
+ vi.mocked(chainAnchorFromBase64).mockReturnValueOnce({
1272
+ commitment: () => ({ toHex: () => '0x' + 'e'.repeat(64) }),
1273
+ free: freed,
1274
+ serialize: () => new Uint8Array([9, 9, 9]),
1275
+ } as never);
1276
+
1277
+ const reExecutionsBefore = vi.mocked(executeForSummaryAt).mock.calls.length;
1278
+ await expect(multisig.syncProposals()).rejects.toThrow(
1279
+ 'chain anchor does not match the block commitment bound into the tx_summary'
1280
+ );
1281
+ expect(vi.mocked(executeForSummaryAt).mock.calls.length).toBe(reExecutionsBefore);
1282
+ expect(freed).toHaveBeenCalledTimes(1);
1283
+ });
1284
+
1056
1285
  it('should reject non-32-byte signer IDs from GUARDIAN proposals', async () => {
1057
1286
  const config = {
1058
1287
  threshold: 1,
@@ -1072,6 +1301,7 @@ describe('Multisig', () => {
1072
1301
  signatures: [],
1073
1302
  metadata: {
1074
1303
  proposal_type: 'add_signer',
1304
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1075
1305
  target_threshold: 1,
1076
1306
  signer_commitments: ['0x' + 'a'.repeat(64)],
1077
1307
  description: '',
@@ -1119,6 +1349,7 @@ describe('Multisig', () => {
1119
1349
  signatures: [],
1120
1350
  metadata: {
1121
1351
  proposal_type: 'add_signer',
1352
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1122
1353
  target_threshold: 2,
1123
1354
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1124
1355
  description: '',
@@ -1202,6 +1433,7 @@ describe('Multisig', () => {
1202
1433
 
1203
1434
  const proposal = await multisig.createProposal(1, 'AQID', {
1204
1435
  proposalType: 'add_signer',
1436
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1205
1437
  targetThreshold: 1,
1206
1438
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1207
1439
  description: '',
@@ -1247,6 +1479,7 @@ describe('Multisig', () => {
1247
1479
  await expect(
1248
1480
  multisig.createProposal(1, 'AQID', {
1249
1481
  proposalType: 'add_signer',
1482
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1250
1483
  targetThreshold: 1,
1251
1484
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1252
1485
  description: '',
@@ -1289,7 +1522,7 @@ describe('Multisig', () => {
1289
1522
  }),
1290
1523
  });
1291
1524
 
1292
- vi.mocked(executeForSummary).mockResolvedValueOnce({
1525
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
1293
1526
  toCommitment: () => ({
1294
1527
  toHex: () => '0x' + 'f'.repeat(64),
1295
1528
  }),
@@ -1298,6 +1531,7 @@ describe('Multisig', () => {
1298
1531
  await expect(
1299
1532
  multisig.createProposal(1, 'AQID', {
1300
1533
  proposalType: 'add_signer',
1534
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1301
1535
  targetThreshold: 1,
1302
1536
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1303
1537
  description: '',
@@ -1310,10 +1544,13 @@ describe('Multisig', () => {
1310
1544
  it('should include the faucet asset in the proposal description', async () => {
1311
1545
  const { executeForSummary } = await import('./transaction.js');
1312
1546
  vi.mocked(executeForSummary).mockResolvedValue({
1313
- toCommitment: () => ({
1314
- toHex: () => '0x' + 'c'.repeat(64),
1315
- }),
1316
- serialize: () => new Uint8Array([1, 2, 3]),
1547
+ summary: {
1548
+ toCommitment: () => ({
1549
+ toHex: () => '0x' + 'c'.repeat(64),
1550
+ }),
1551
+ serialize: () => new Uint8Array([1, 2, 3]),
1552
+ },
1553
+ anchor: createMockChainAnchor(),
1317
1554
  } as any);
1318
1555
 
1319
1556
  const config = {
@@ -1333,6 +1570,7 @@ describe('Multisig', () => {
1333
1570
  signatures: [],
1334
1571
  metadata: {
1335
1572
  proposal_type: 'p2id',
1573
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1336
1574
  recipient_id: '0xrecipient',
1337
1575
  faucet_id: '0xfaucet',
1338
1576
  amount: '100',
@@ -1355,7 +1593,7 @@ describe('Multisig', () => {
1355
1593
  }),
1356
1594
  });
1357
1595
 
1358
- const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1);
1596
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, { nonce: 1 });
1359
1597
 
1360
1598
  expect(proposal.metadata.description).toBe('Send 100 of asset 0xfaucet... to 0xrecipien...');
1361
1599
  });
@@ -1364,10 +1602,13 @@ describe('Multisig', () => {
1364
1602
  const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
1365
1603
  const { NoteType } = await import('@miden-sdk/miden-sdk');
1366
1604
  vi.mocked(executeForSummary).mockResolvedValue({
1367
- toCommitment: () => ({
1368
- toHex: () => '0x' + 'c'.repeat(64),
1369
- }),
1370
- serialize: () => new Uint8Array([1, 2, 3]),
1605
+ summary: {
1606
+ toCommitment: () => ({
1607
+ toHex: () => '0x' + 'c'.repeat(64),
1608
+ }),
1609
+ serialize: () => new Uint8Array([1, 2, 3]),
1610
+ },
1611
+ anchor: createMockChainAnchor(),
1371
1612
  } as any);
1372
1613
 
1373
1614
  const config = {
@@ -1387,6 +1628,7 @@ describe('Multisig', () => {
1387
1628
  signatures: [],
1388
1629
  metadata: {
1389
1630
  proposal_type: 'p2id',
1631
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1390
1632
  recipient_id: '0xrecipient',
1391
1633
  faucet_id: '0xfaucet',
1392
1634
  amount: '100',
@@ -1410,7 +1652,8 @@ describe('Multisig', () => {
1410
1652
  }),
1411
1653
  });
1412
1654
 
1413
- const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1, {
1655
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, {
1656
+ nonce: 1,
1414
1657
  noteType: NoteType.Private,
1415
1658
  });
1416
1659
 
@@ -1420,12 +1663,11 @@ describe('Multisig', () => {
1420
1663
  '0xrecipient',
1421
1664
  '0xfaucet',
1422
1665
  100n,
1423
- expect.anything(),
1424
1666
  { noteType: NoteType.Private },
1425
1667
  );
1426
1668
  // ...and the rebuild-from-metadata path parses note_type back to Private.
1427
1669
  const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1)!;
1428
- expect(lastCall[5]).toMatchObject({ noteType: NoteType.Private });
1670
+ expect(lastCall[4]).toMatchObject({ noteType: NoteType.Private });
1429
1671
 
1430
1672
  // The pushed wire metadata carries note_type so cosigners rebuild the
1431
1673
  // same private note at verification/execution.
@@ -1435,6 +1677,137 @@ describe('Multisig', () => {
1435
1677
  expect(proposal.metadata.proposalType).toBe('p2id');
1436
1678
  expect((proposal.metadata as { noteType?: string }).noteType).toBe('private');
1437
1679
  });
1680
+
1681
+ it('threads P2IDE reclaim/timelock heights into the request and wire metadata (issue #366)', async () => {
1682
+ const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
1683
+ vi.mocked(executeForSummary).mockResolvedValue({
1684
+ summary: {
1685
+ toCommitment: () => ({
1686
+ toHex: () => '0x' + 'c'.repeat(64),
1687
+ }),
1688
+ serialize: () => new Uint8Array([1, 2, 3]),
1689
+ },
1690
+ anchor: createMockChainAnchor(),
1691
+ } as any);
1692
+
1693
+ const config = {
1694
+ threshold: 1,
1695
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1696
+ guardianCommitment: '0x' + 'c'.repeat(64),
1697
+ };
1698
+
1699
+ const multisig = createTestMultisig(config);
1700
+
1701
+ const mockDelta = {
1702
+ account_id: '0x' + 'a'.repeat(30),
1703
+ nonce: 1,
1704
+ prev_commitment: '0x' + 'b'.repeat(64),
1705
+ delta_payload: {
1706
+ tx_summary: { data: 'AQID' },
1707
+ signatures: [],
1708
+ metadata: {
1709
+ proposal_type: 'p2id',
1710
+ recipient_id: '0xrecipient',
1711
+ faucet_id: '0xfaucet',
1712
+ amount: '100',
1713
+ reclaim_height: 12345,
1714
+ timelock_height: 700,
1715
+ description: '',
1716
+ },
1717
+ },
1718
+ status: {
1719
+ status: 'pending',
1720
+ timestamp: '2024-01-01T00:00:00Z',
1721
+ proposer_id: '0x' + 'c'.repeat(64),
1722
+ cosigner_sigs: [],
1723
+ },
1724
+ };
1725
+
1726
+ mockFetch.mockResolvedValueOnce({
1727
+ ok: true,
1728
+ json: async () => ({
1729
+ delta: mockDelta,
1730
+ commitment: '0x' + 'c'.repeat(64),
1731
+ }),
1732
+ });
1733
+
1734
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, {
1735
+ nonce: 1,
1736
+ reclaimHeight: 12345,
1737
+ timelockHeight: 700,
1738
+ });
1739
+
1740
+ // Propose path builds the P2IDE note from the heights...
1741
+ const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1)!;
1742
+ expect(lastCall[4]).toMatchObject({ reclaimHeight: 12345, timelockHeight: 700 });
1743
+
1744
+ // ...and the pushed wire metadata carries the heights so cosigners
1745
+ // rebuild the same P2IDE note at verification/execution.
1746
+ const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)![1].body as string);
1747
+ expect(pushBody.delta_payload.metadata.reclaim_height).toBe(12345);
1748
+ expect(pushBody.delta_payload.metadata.timelock_height).toBe(700);
1749
+
1750
+ expect(proposal.metadata.proposalType).toBe('p2id');
1751
+ expect(proposal.metadata).toMatchObject({ reclaimHeight: 12345, timelockHeight: 700 });
1752
+ });
1753
+
1754
+ it('omits the heights from wire metadata for a plain P2ID send (pre-#366 shape)', async () => {
1755
+ const { executeForSummary } = await import('./transaction.js');
1756
+ vi.mocked(executeForSummary).mockResolvedValue({
1757
+ summary: {
1758
+ toCommitment: () => ({
1759
+ toHex: () => '0x' + 'c'.repeat(64),
1760
+ }),
1761
+ serialize: () => new Uint8Array([1, 2, 3]),
1762
+ },
1763
+ anchor: createMockChainAnchor(),
1764
+ } as any);
1765
+
1766
+ const config = {
1767
+ threshold: 1,
1768
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1769
+ guardianCommitment: '0x' + 'c'.repeat(64),
1770
+ };
1771
+
1772
+ const multisig = createTestMultisig(config);
1773
+
1774
+ const mockDelta = {
1775
+ account_id: '0x' + 'a'.repeat(30),
1776
+ nonce: 1,
1777
+ prev_commitment: '0x' + 'b'.repeat(64),
1778
+ delta_payload: {
1779
+ tx_summary: { data: 'AQID' },
1780
+ signatures: [],
1781
+ metadata: {
1782
+ proposal_type: 'p2id',
1783
+ recipient_id: '0xrecipient',
1784
+ faucet_id: '0xfaucet',
1785
+ amount: '100',
1786
+ description: '',
1787
+ },
1788
+ },
1789
+ status: {
1790
+ status: 'pending',
1791
+ timestamp: '2024-01-01T00:00:00Z',
1792
+ proposer_id: '0x' + 'c'.repeat(64),
1793
+ cosigner_sigs: [],
1794
+ },
1795
+ };
1796
+
1797
+ mockFetch.mockResolvedValueOnce({
1798
+ ok: true,
1799
+ json: async () => ({
1800
+ delta: mockDelta,
1801
+ commitment: '0x' + 'c'.repeat(64),
1802
+ }),
1803
+ });
1804
+
1805
+ await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, { nonce: 1 });
1806
+
1807
+ const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)![1].body as string);
1808
+ expect('reclaim_height' in pushBody.delta_payload.metadata).toBe(false);
1809
+ expect('timelock_height' in pushBody.delta_payload.metadata).toBe(false);
1810
+ });
1438
1811
  });
1439
1812
 
1440
1813
  describe('exportNoteToBytes / importNoteFromBytes (issue #356)', () => {
@@ -1561,6 +1934,7 @@ describe('Multisig', () => {
1561
1934
  const proposal = {
1562
1935
  metadata: {
1563
1936
  proposalType: 'p2id',
1937
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1564
1938
  recipientId: '0x' + 'b'.repeat(30),
1565
1939
  faucetId: '0x' + 'c'.repeat(30),
1566
1940
  amount: '100',
@@ -1588,10 +1962,13 @@ describe('Multisig', () => {
1588
1962
  describe('createChangeThresholdProposal', () => {
1589
1963
  it('passes the signer scheme to update-signers requests', async () => {
1590
1964
  vi.mocked(executeForSummary).mockResolvedValue({
1591
- toCommitment: () => ({
1592
- toHex: () => '0x' + 'c'.repeat(64),
1593
- }),
1594
- serialize: () => new Uint8Array([1, 2, 3]),
1965
+ summary: {
1966
+ toCommitment: () => ({
1967
+ toHex: () => '0x' + 'c'.repeat(64),
1968
+ }),
1969
+ serialize: () => new Uint8Array([1, 2, 3]),
1970
+ },
1971
+ anchor: createMockChainAnchor(),
1595
1972
  } as any);
1596
1973
 
1597
1974
  const ecdsaSigner: Signer = {
@@ -1616,6 +1993,7 @@ describe('Multisig', () => {
1616
1993
  signatures: [],
1617
1994
  metadata: {
1618
1995
  proposal_type: 'change_threshold',
1996
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1619
1997
  target_threshold: 2,
1620
1998
  description: '',
1621
1999
  },
@@ -1637,7 +2015,7 @@ describe('Multisig', () => {
1637
2015
  });
1638
2016
 
1639
2017
  const multisig = createTestMultisig(config, ecdsaSigner);
1640
- await multisig.createChangeThresholdProposal(2, 1);
2018
+ await multisig.createChangeThresholdProposal(2, { nonce: 1 });
1641
2019
 
1642
2020
  expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(
1643
2021
  mockWebClient,
@@ -1648,10 +2026,153 @@ describe('Multisig', () => {
1648
2026
  });
1649
2027
  });
1650
2028
 
2029
+ describe('createAddSignerProposal / createRemoveSignerProposal', () => {
2030
+ const config = {
2031
+ threshold: 2,
2032
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'd'.repeat(64)],
2033
+ guardianCommitment: '0x' + 'c'.repeat(64),
2034
+ };
2035
+
2036
+ const mockPushResponse = (proposalType: string) => {
2037
+ mockFetch.mockResolvedValueOnce({
2038
+ ok: true,
2039
+ json: async () => ({
2040
+ delta: {
2041
+ account_id: '0x' + 'a'.repeat(30),
2042
+ nonce: 1,
2043
+ prev_commitment: '0x' + 'b'.repeat(64),
2044
+ delta_payload: {
2045
+ tx_summary: { data: 'AQID' },
2046
+ signatures: [],
2047
+ metadata: { proposal_type: proposalType, description: '' },
2048
+ },
2049
+ status: {
2050
+ status: 'pending',
2051
+ timestamp: '2024-01-01T00:00:00Z',
2052
+ proposer_id: '0x' + 'c'.repeat(64),
2053
+ cosigner_sigs: [],
2054
+ },
2055
+ },
2056
+ commitment: '0x' + 'c'.repeat(64),
2057
+ }),
2058
+ });
2059
+ };
2060
+
2061
+ beforeEach(() => {
2062
+ vi.mocked(executeForSummary).mockResolvedValue({
2063
+ summary: {
2064
+ toCommitment: () => ({ toHex: () => '0x' + 'c'.repeat(64) }),
2065
+ serialize: () => new Uint8Array([1, 2, 3]),
2066
+ },
2067
+ anchor: createMockChainAnchor(),
2068
+ } as any);
2069
+ });
2070
+
2071
+ it('add: passes newThreshold from the options bag and appends the commitment', async () => {
2072
+ mockPushResponse('add_signer');
2073
+ const newCommitment = '0x' + 'e'.repeat(64);
2074
+
2075
+ const multisig = createTestMultisig(config);
2076
+ await multisig.createAddSignerProposal(newCommitment, { nonce: 1, newThreshold: 3 });
2077
+
2078
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(
2079
+ mockWebClient,
2080
+ 3,
2081
+ [...config.signerCommitments, newCommitment],
2082
+ { signatureScheme: mockSigner.scheme },
2083
+ );
2084
+ });
2085
+
2086
+ it('add: defaults to the current threshold when newThreshold is omitted', async () => {
2087
+ mockPushResponse('add_signer');
2088
+
2089
+ const multisig = createTestMultisig(config);
2090
+ await multisig.createAddSignerProposal('0x' + 'e'.repeat(64), { nonce: 1 });
2091
+
2092
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(
2093
+ mockWebClient,
2094
+ config.threshold,
2095
+ expect.any(Array),
2096
+ { signatureScheme: mockSigner.scheme },
2097
+ );
2098
+ });
2099
+
2100
+ it('remove: passes newThreshold from the options bag and drops the commitment', async () => {
2101
+ mockPushResponse('remove_signer');
2102
+
2103
+ const multisig = createTestMultisig(config);
2104
+ await multisig.createRemoveSignerProposal('0x' + 'd'.repeat(64), { nonce: 1, newThreshold: 1 });
2105
+
2106
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(
2107
+ mockWebClient,
2108
+ 1,
2109
+ [config.signerCommitments[0], config.signerCommitments[1]],
2110
+ { signatureScheme: mockSigner.scheme },
2111
+ );
2112
+ });
2113
+
2114
+ it('remove: defaults to min(current threshold, remaining signers) when newThreshold is omitted', async () => {
2115
+ mockPushResponse('remove_signer');
2116
+
2117
+ const multisig = createTestMultisig(config);
2118
+ await multisig.createRemoveSignerProposal('0x' + 'd'.repeat(64), { nonce: 1 });
2119
+
2120
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(
2121
+ mockWebClient,
2122
+ 2,
2123
+ expect.any(Array),
2124
+ { signatureScheme: mockSigner.scheme },
2125
+ );
2126
+ });
2127
+ });
2128
+
2129
+ describe('legacy positional-caller guard (issue #387)', () => {
2130
+ const config = {
2131
+ threshold: 1,
2132
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2133
+ guardianCommitment: '0x' + 'c'.repeat(64),
2134
+ };
2135
+
2136
+ it('rejects a legacy positional nonce where the options bag is expected', async () => {
2137
+ const multisig = createTestMultisig(config);
2138
+
2139
+ await expect(
2140
+ multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1 as any),
2141
+ ).rejects.toThrow(/issue #387/);
2142
+ await expect(
2143
+ multisig.createConsumeNotesProposal(['0x1'], 1 as any),
2144
+ ).rejects.toThrow(/issue #387/);
2145
+ await expect(
2146
+ multisig.createAddSignerProposal('0x' + 'e'.repeat(64), 1 as any),
2147
+ ).rejects.toThrow(/issue #387/);
2148
+ await expect(
2149
+ multisig.createCustomProposal(new Uint8Array([1]), 'label', 1 as any),
2150
+ ).rejects.toThrow(/issue #387/);
2151
+ });
2152
+
2153
+ it('rejects a legacy trailing argument after the options slot', async () => {
2154
+ const multisig = createTestMultisig(config);
2155
+
2156
+ // Pre-#387 pattern: createP2idProposal(r, f, amount, nonceHole, { noteType })
2157
+ await expect(
2158
+ (multisig.createP2idProposal as any)('0xrecipient', '0xfaucet', 100n, undefined, {
2159
+ noteType: 'private',
2160
+ }),
2161
+ ).rejects.toThrow(/issue #387/);
2162
+ // Pre-#387 pattern: createAddSignerProposal(commitment, nonceHole, newThreshold)
2163
+ await expect(
2164
+ (multisig.createAddSignerProposal as any)('0x' + 'e'.repeat(64), undefined, 3),
2165
+ ).rejects.toThrow(/issue #387/);
2166
+ });
2167
+ });
2168
+
1651
2169
  describe('createSwitchGuardianProposal', () => {
1652
2170
  it('should verify new endpoint commitment before creating proposal', async () => {
1653
2171
  vi.mocked(executeForSummary).mockResolvedValue({
1654
- serialize: () => new Uint8Array([1, 2, 3]),
2172
+ summary: {
2173
+ serialize: () => new Uint8Array([1, 2, 3]),
2174
+ },
2175
+ anchor: createMockChainAnchor(),
1655
2176
  } as any);
1656
2177
 
1657
2178
  const config = {
@@ -1700,7 +2221,10 @@ describe('Multisig', () => {
1700
2221
 
1701
2222
  it('should reject switch proposal when endpoint commitment does not match', async () => {
1702
2223
  vi.mocked(executeForSummary).mockResolvedValue({
1703
- serialize: () => new Uint8Array([1, 2, 3]),
2224
+ summary: {
2225
+ serialize: () => new Uint8Array([1, 2, 3]),
2226
+ },
2227
+ anchor: createMockChainAnchor(),
1704
2228
  } as any);
1705
2229
 
1706
2230
  const config = {
@@ -1723,7 +2247,10 @@ describe('Multisig', () => {
1723
2247
 
1724
2248
  it('should use the signer scheme when resolving new GUARDIAN commitments', async () => {
1725
2249
  vi.mocked(executeForSummary).mockResolvedValue({
1726
- serialize: () => new Uint8Array([1, 2, 3]),
2250
+ summary: {
2251
+ serialize: () => new Uint8Array([1, 2, 3]),
2252
+ },
2253
+ anchor: createMockChainAnchor(),
1727
2254
  } as any);
1728
2255
 
1729
2256
  const ecdsaSigner: Signer = {
@@ -1782,10 +2309,13 @@ describe('Multisig', () => {
1782
2309
  describe('createUpdateProcedureThresholdProposal', () => {
1783
2310
  it('should create procedure-threshold update proposals', async () => {
1784
2311
  vi.mocked(executeForSummary).mockResolvedValue({
1785
- toCommitment: () => ({
1786
- toHex: () => '0x' + 'c'.repeat(64),
1787
- }),
1788
- serialize: () => new Uint8Array([1, 2, 3]),
2312
+ summary: {
2313
+ toCommitment: () => ({
2314
+ toHex: () => '0x' + 'c'.repeat(64),
2315
+ }),
2316
+ serialize: () => new Uint8Array([1, 2, 3]),
2317
+ },
2318
+ anchor: createMockChainAnchor(),
1789
2319
  } as any);
1790
2320
 
1791
2321
  const config = {
@@ -1805,6 +2335,7 @@ describe('Multisig', () => {
1805
2335
  signatures: [],
1806
2336
  metadata: {
1807
2337
  proposal_type: 'update_procedure_threshold',
2338
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1808
2339
  target_threshold: 1,
1809
2340
  target_procedure: 'send_asset',
1810
2341
  description: '',
@@ -1826,7 +2357,7 @@ describe('Multisig', () => {
1826
2357
  }),
1827
2358
  });
1828
2359
 
1829
- const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
2360
+ const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, { nonce: 1 });
1830
2361
 
1831
2362
  expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(
1832
2363
  mockWebClient,
@@ -1843,10 +2374,13 @@ describe('Multisig', () => {
1843
2374
 
1844
2375
  it('passes the signer scheme to ECDSA procedure-threshold updates', async () => {
1845
2376
  vi.mocked(executeForSummary).mockResolvedValue({
1846
- toCommitment: () => ({
1847
- toHex: () => '0x' + 'c'.repeat(64),
1848
- }),
1849
- serialize: () => new Uint8Array([1, 2, 3]),
2377
+ summary: {
2378
+ toCommitment: () => ({
2379
+ toHex: () => '0x' + 'c'.repeat(64),
2380
+ }),
2381
+ serialize: () => new Uint8Array([1, 2, 3]),
2382
+ },
2383
+ anchor: createMockChainAnchor(),
1850
2384
  } as any);
1851
2385
 
1852
2386
  const ecdsaSigner: Signer = {
@@ -1873,6 +2407,7 @@ describe('Multisig', () => {
1873
2407
  signatures: [],
1874
2408
  metadata: {
1875
2409
  proposal_type: 'update_procedure_threshold',
2410
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1876
2411
  target_threshold: 1,
1877
2412
  target_procedure: 'send_asset',
1878
2413
  description: '',
@@ -1894,7 +2429,7 @@ describe('Multisig', () => {
1894
2429
  }),
1895
2430
  });
1896
2431
 
1897
- await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
2432
+ await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, { nonce: 1 });
1898
2433
 
1899
2434
  expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(
1900
2435
  mockWebClient,
@@ -1942,6 +2477,7 @@ describe('Multisig', () => {
1942
2477
 
1943
2478
  await multisig.createProposal(1, 'AQID', {
1944
2479
  proposalType: 'add_signer',
2480
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1945
2481
  targetThreshold: 1,
1946
2482
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1947
2483
  description: '',
@@ -1965,6 +2501,7 @@ describe('Multisig', () => {
1965
2501
  ...mockDelta.delta_payload,
1966
2502
  metadata: {
1967
2503
  proposal_type: 'add_signer',
2504
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
1968
2505
  description: '',
1969
2506
  target_threshold: 1,
1970
2507
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -2019,12 +2556,13 @@ describe('Multisig', () => {
2019
2556
 
2020
2557
  await multisig.createProposal(1, 'AQID', {
2021
2558
  proposalType: 'add_signer',
2559
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2022
2560
  targetThreshold: 1,
2023
2561
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2024
2562
  description: '',
2025
2563
  });
2026
2564
 
2027
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2565
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2028
2566
  toCommitment: () => ({
2029
2567
  toHex: () => '0x' + 'f'.repeat(64),
2030
2568
  }),
@@ -2058,6 +2596,7 @@ describe('Multisig', () => {
2058
2596
  signatures: [],
2059
2597
  metadata: {
2060
2598
  proposal_type: 'add_signer',
2599
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2061
2600
  description: '',
2062
2601
  target_threshold: 1,
2063
2602
  signer_commitments: [mockSigner.commitment],
@@ -2091,7 +2630,7 @@ describe('Multisig', () => {
2091
2630
 
2092
2631
  const multisig = createTestMultisig(config);
2093
2632
 
2094
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2633
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2095
2634
  toCommitment: () => ({
2096
2635
  toHex: () => '0x' + 'f'.repeat(64),
2097
2636
  }),
@@ -2107,6 +2646,7 @@ describe('Multisig', () => {
2107
2646
  signatures: [],
2108
2647
  metadata: {
2109
2648
  proposalType: 'add_signer',
2649
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2110
2650
  targetThreshold: 1,
2111
2651
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2112
2652
  description: '',
@@ -2127,7 +2667,7 @@ describe('Multisig', () => {
2127
2667
 
2128
2668
  const multisig = createTestMultisig(config);
2129
2669
 
2130
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2670
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2131
2671
  toCommitment: () => ({
2132
2672
  toHex: () => '0x' + 'c'.repeat(64),
2133
2673
  }),
@@ -2142,6 +2682,7 @@ describe('Multisig', () => {
2142
2682
  signatures: [],
2143
2683
  metadata: {
2144
2684
  proposalType: 'add_signer',
2685
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2145
2686
  targetThreshold: 1,
2146
2687
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2147
2688
  description: '',
@@ -2151,12 +2692,13 @@ describe('Multisig', () => {
2151
2692
 
2152
2693
  proposal.metadata = {
2153
2694
  proposalType: 'add_signer',
2695
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2154
2696
  targetThreshold: 2,
2155
2697
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2156
2698
  description: '',
2157
2699
  };
2158
2700
 
2159
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2701
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2160
2702
  toCommitment: () => ({
2161
2703
  toHex: () => '0x' + 'f'.repeat(64),
2162
2704
  }),
@@ -2188,6 +2730,7 @@ describe('Multisig', () => {
2188
2730
  signatures: [],
2189
2731
  metadata: {
2190
2732
  proposal_type: 'add_signer',
2733
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2191
2734
  description: '',
2192
2735
  target_threshold: 1,
2193
2736
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -2243,6 +2786,7 @@ describe('Multisig', () => {
2243
2786
  signatures: [],
2244
2787
  metadata: {
2245
2788
  proposal_type: 'change_threshold',
2789
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2246
2790
  description: '',
2247
2791
  target_threshold: 2,
2248
2792
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -2298,7 +2842,7 @@ describe('Multisig', () => {
2298
2842
  // folded into the error message; raw text bodies are dropped.
2299
2843
  text: async () =>
2300
2844
  JSON.stringify({
2301
- code: 'GUARDIAN_PROPOSAL_NOT_FOUND',
2845
+ code: 'proposal_not_found',
2302
2846
  message: 'Proposal not found',
2303
2847
  meta: { retryable: false },
2304
2848
  }),
@@ -2333,6 +2877,7 @@ describe('Multisig', () => {
2333
2877
  ],
2334
2878
  metadata: {
2335
2879
  proposalType: 'add_signer' as const,
2880
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2336
2881
  targetThreshold: 1,
2337
2882
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2338
2883
  description: '',
@@ -2371,6 +2916,7 @@ describe('Multisig', () => {
2371
2916
  ],
2372
2917
  metadata: {
2373
2918
  proposalType: 'change_threshold',
2919
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2374
2920
  targetThreshold: 1,
2375
2921
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2376
2922
  description: '',
@@ -2416,6 +2962,7 @@ describe('Multisig', () => {
2416
2962
  ],
2417
2963
  metadata: {
2418
2964
  proposalType: 'change_threshold',
2965
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2419
2966
  targetThreshold: 1,
2420
2967
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2421
2968
  description: '',
@@ -2442,6 +2989,7 @@ describe('Multisig', () => {
2442
2989
  signatures: [],
2443
2990
  metadata: {
2444
2991
  proposalType: 'add_signer' as const,
2992
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2445
2993
  targetThreshold: 2,
2446
2994
  targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2447
2995
  description: '',
@@ -2517,6 +3065,7 @@ describe('Multisig', () => {
2517
3065
  ],
2518
3066
  metadata: {
2519
3067
  proposalType: 'change_threshold',
3068
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2520
3069
  targetThreshold: 1,
2521
3070
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2522
3071
  description: '',
@@ -2534,6 +3083,7 @@ describe('Multisig', () => {
2534
3083
  signatures: [],
2535
3084
  metadata: {
2536
3085
  proposal_type: 'change_threshold',
3086
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2537
3087
  target_threshold: 1,
2538
3088
  signer_commitments: ['0x' + 'a'.repeat(64)],
2539
3089
  },
@@ -2576,17 +3126,19 @@ describe('Multisig', () => {
2576
3126
  expect.anything(),
2577
3127
  expect.anything(),
2578
3128
  expect.anything(),
2579
- cosignerPubkey,
2580
- cosignerSignature,
2581
3129
  );
2582
3130
  expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(
2583
3131
  2,
2584
3132
  expect.anything(),
2585
3133
  expect.anything(),
2586
3134
  expect.anything(),
2587
- ackPubkey,
2588
- ackSignature,
2589
3135
  );
3136
+ // The advice payload now comes from the SDK, so the routing assertion is
3137
+ // the commitment each entry is keyed on: cosigner first, GUARDIAN ack
3138
+ // second. Swapping the two entries must not pass.
3139
+ const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
3140
+ expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
3141
+ expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
2590
3142
  expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2591
3143
  expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
2592
3144
  expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
@@ -2628,6 +3180,7 @@ describe('Multisig', () => {
2628
3180
  ],
2629
3181
  metadata: {
2630
3182
  proposalType: 'switch_guardian',
3183
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2631
3184
  newGuardianPubkey,
2632
3185
  newGuardianEndpoint: 'http://new-guardian.com',
2633
3186
  description: '',
@@ -2681,6 +3234,7 @@ describe('Multisig', () => {
2681
3234
  signatures: [],
2682
3235
  metadata: {
2683
3236
  proposal_type: 'add_signer',
3237
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2684
3238
  description: '',
2685
3239
  target_threshold: 2,
2686
3240
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -2723,7 +3277,7 @@ describe('Multisig', () => {
2723
3277
  const multisig = createTestMultisig(config);
2724
3278
  const proposalId = '0x' + 'c'.repeat(64);
2725
3279
 
2726
- vi.mocked(executeForSummary).mockResolvedValueOnce({
3280
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2727
3281
  toCommitment: () => ({
2728
3282
  toHex: () => '0x' + 'd'.repeat(64),
2729
3283
  }),
@@ -2744,6 +3298,7 @@ describe('Multisig', () => {
2744
3298
  ],
2745
3299
  metadata: {
2746
3300
  proposalType: 'change_threshold',
3301
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2747
3302
  targetThreshold: 1,
2748
3303
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2749
3304
  description: '',
@@ -2781,6 +3336,7 @@ describe('Multisig', () => {
2781
3336
  ],
2782
3337
  metadata: {
2783
3338
  proposalType: 'switch_guardian',
3339
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2784
3340
  newGuardianPubkey: '0x' + '1'.repeat(64),
2785
3341
  newGuardianEndpoint: 'http://new-guardian.com',
2786
3342
  description: '',
@@ -2828,6 +3384,7 @@ describe('Multisig', () => {
2828
3384
  ],
2829
3385
  metadata: {
2830
3386
  proposalType: 'switch_guardian',
3387
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2831
3388
  newGuardianPubkey: '0x' + '1'.repeat(64),
2832
3389
  newGuardianEndpoint: 'http://new-guardian.com',
2833
3390
  description: '',
@@ -2908,6 +3465,7 @@ describe('Multisig', () => {
2908
3465
  ],
2909
3466
  metadata: {
2910
3467
  proposalType: 'change_threshold',
3468
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2911
3469
  targetThreshold: 1,
2912
3470
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2913
3471
  description: '',
@@ -2925,6 +3483,7 @@ describe('Multisig', () => {
2925
3483
  signatures: [],
2926
3484
  metadata: {
2927
3485
  proposal_type: 'change_threshold',
3486
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2928
3487
  target_threshold: 1,
2929
3488
  signer_commitments: ['0x' + 'a'.repeat(64)],
2930
3489
  },
@@ -2999,6 +3558,7 @@ describe('Multisig', () => {
2999
3558
  ],
3000
3559
  metadata: {
3001
3560
  proposalType: 'switch_guardian',
3561
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3002
3562
  newGuardianPubkey: '0x' + '1'.repeat(64),
3003
3563
  newGuardianEndpoint: 'http://new-guardian.com',
3004
3564
  description: '',
@@ -3046,6 +3606,7 @@ describe('Multisig', () => {
3046
3606
  signatures: [],
3047
3607
  metadata: {
3048
3608
  proposal_type: 'add_signer',
3609
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3049
3610
  description: '',
3050
3611
  target_threshold: 2,
3051
3612
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -3079,7 +3640,7 @@ describe('Multisig', () => {
3079
3640
  ).rejects.toThrow('not ready for execution');
3080
3641
  });
3081
3642
 
3082
- it('should fail when GUARDIAN ack signature is missing (selector ON)', async () => {
3643
+ it('should fail when GUARDIAN ack signature is missing', async () => {
3083
3644
  const config = {
3084
3645
  threshold: 1,
3085
3646
  signerCommitments: ['0x' + 'a'.repeat(64)],
@@ -3097,6 +3658,7 @@ describe('Multisig', () => {
3097
3658
  signatures: [],
3098
3659
  metadata: {
3099
3660
  proposal_type: 'add_signer',
3661
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3100
3662
  description: '',
3101
3663
  target_threshold: 1,
3102
3664
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -3189,6 +3751,7 @@ describe('Multisig', () => {
3189
3751
  ],
3190
3752
  metadata: {
3191
3753
  proposalType: 'change_threshold',
3754
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3192
3755
  targetThreshold: 1,
3193
3756
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
3194
3757
  description: '',
@@ -3206,6 +3769,7 @@ describe('Multisig', () => {
3206
3769
  signatures: [],
3207
3770
  metadata: {
3208
3771
  proposal_type: 'change_threshold',
3772
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3209
3773
  target_threshold: 1,
3210
3774
  signer_commitments: ['0x' + 'a'.repeat(64)],
3211
3775
  },
@@ -3259,17 +3823,19 @@ describe('Multisig', () => {
3259
3823
  expect.anything(),
3260
3824
  expect.anything(),
3261
3825
  expect.anything(),
3262
- cosignerPubkey,
3263
- cosignerSignature,
3264
3826
  );
3265
3827
  expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(
3266
3828
  2,
3267
3829
  expect.anything(),
3268
3830
  expect.anything(),
3269
3831
  expect.anything(),
3270
- ackPubkey,
3271
- ackSignature,
3272
3832
  );
3833
+ // The advice payload now comes from the SDK, so the routing assertion is
3834
+ // the commitment each entry is keyed on: cosigner first, GUARDIAN ack
3835
+ // second. Swapping the two entries must not pass.
3836
+ const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
3837
+ expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
3838
+ expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
3273
3839
  });
3274
3840
 
3275
3841
  it('should execute imported ECDSA proposals with scheme-aware advice', async () => {
@@ -3314,6 +3880,7 @@ describe('Multisig', () => {
3314
3880
  ],
3315
3881
  metadata: {
3316
3882
  proposalType: 'change_threshold',
3883
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3317
3884
  targetThreshold: 1,
3318
3885
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
3319
3886
  description: '',
@@ -3332,6 +3899,7 @@ describe('Multisig', () => {
3332
3899
  signatures: [],
3333
3900
  metadata: {
3334
3901
  proposal_type: 'change_threshold',
3902
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3335
3903
  target_threshold: 1,
3336
3904
  signer_commitments: ['0x' + 'a'.repeat(64)],
3337
3905
  },
@@ -3375,17 +3943,19 @@ describe('Multisig', () => {
3375
3943
  expect.anything(),
3376
3944
  expect.anything(),
3377
3945
  expect.anything(),
3378
- cosignerPubkey,
3379
- cosignerSignature,
3380
3946
  );
3381
3947
  expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(
3382
3948
  2,
3383
3949
  expect.anything(),
3384
3950
  expect.anything(),
3385
3951
  expect.anything(),
3386
- ackPubkey,
3387
- ackSignature,
3388
3952
  );
3953
+ // The advice payload now comes from the SDK, so the routing assertion is
3954
+ // the commitment each entry is keyed on: cosigner first, GUARDIAN ack
3955
+ // second. Swapping the two entries must not pass.
3956
+ const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
3957
+ expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
3958
+ expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
3389
3959
  });
3390
3960
 
3391
3961
  it('should verify switch_guardian endpoint commitment before execution', async () => {
@@ -3414,6 +3984,7 @@ describe('Multisig', () => {
3414
3984
  ],
3415
3985
  metadata: {
3416
3986
  proposalType: 'switch_guardian',
3987
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3417
3988
  newGuardianPubkey,
3418
3989
  newGuardianEndpoint: 'http://new-guardian.com',
3419
3990
  description: '',
@@ -3436,6 +4007,7 @@ describe('Multisig', () => {
3436
4007
  signatures: [],
3437
4008
  metadata: {
3438
4009
  proposal_type: 'switch_guardian',
4010
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3439
4011
  new_guardian_pubkey: newGuardianPubkey,
3440
4012
  new_guardian_endpoint: 'http://new-guardian.com',
3441
4013
  },
@@ -3498,6 +4070,7 @@ describe('Multisig', () => {
3498
4070
  ],
3499
4071
  metadata: {
3500
4072
  proposalType: 'switch_guardian',
4073
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3501
4074
  newGuardianPubkey,
3502
4075
  newGuardianEndpoint: 'http://new-guardian.com',
3503
4076
  description: '',
@@ -3549,6 +4122,7 @@ describe('Multisig', () => {
3549
4122
  ],
3550
4123
  metadata: {
3551
4124
  proposalType: 'switch_guardian',
4125
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3552
4126
  newGuardianPubkey: '0x' + '1'.repeat(64),
3553
4127
  newGuardianEndpoint: 'http://new-guardian.com',
3554
4128
  description: '',
@@ -3596,6 +4170,7 @@ describe('Multisig', () => {
3596
4170
  ],
3597
4171
  metadata: {
3598
4172
  proposalType: 'switch_guardian',
4173
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3599
4174
  newGuardianPubkey: '0x' + '1'.repeat(64),
3600
4175
  newGuardianEndpoint: 'http://new-guardian.com',
3601
4176
  description: '',
@@ -3648,6 +4223,7 @@ describe('Multisig', () => {
3648
4223
  ],
3649
4224
  metadata: {
3650
4225
  proposalType: 'switch_guardian',
4226
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3651
4227
  newGuardianPubkey: '0x' + '1'.repeat(64),
3652
4228
  newGuardianEndpoint: 'http://new-guardian.com',
3653
4229
  description: '',
@@ -3683,9 +4259,33 @@ describe('Multisig', () => {
3683
4259
  .mockRejectedValueOnce(transient)
3684
4260
  .mockResolvedValueOnce({});
3685
4261
 
4262
+ mockFetch.mockResolvedValueOnce({
4263
+ ok: true,
4264
+ json: async () => ({
4265
+ account_id: '0x' + 'a'.repeat(30),
4266
+ nonce: 1,
4267
+ prev_commitment: '0x' + 'b'.repeat(64),
4268
+ delta_payload: {
4269
+ tx_summary: { data: 'AQID' },
4270
+ signatures: [],
4271
+ metadata: {
4272
+ proposal_type: 'b2agg',
4273
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4274
+ description: '',
4275
+ },
4276
+ },
4277
+ status: {
4278
+ status: 'pending',
4279
+ timestamp: '2024-01-01T00:00:00Z',
4280
+ proposer_id: '0x' + 'c'.repeat(64),
4281
+ cosigner_sigs: [],
4282
+ },
4283
+ }),
4284
+ });
4285
+
3686
4286
  vi.useFakeTimers();
3687
4287
  try {
3688
- const submission = multisig.submitTransaction({} as never);
4288
+ const submission = multisig.submitTransaction('0x' + 'c'.repeat(64), {} as never);
3689
4289
  await vi.runAllTimersAsync();
3690
4290
  await submission;
3691
4291
  } finally {
@@ -3715,6 +4315,7 @@ describe('Multisig', () => {
3715
4315
  signatures: [],
3716
4316
  metadata: {
3717
4317
  proposal_type: proposalType,
4318
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3718
4319
  description: '',
3719
4320
  },
3720
4321
  },
@@ -3748,6 +4349,7 @@ describe('Multisig', () => {
3748
4349
  };
3749
4350
  builtinDelta.delta_payload.metadata = {
3750
4351
  proposal_type: 'change_threshold',
4352
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3751
4353
  description: '',
3752
4354
  target_threshold: 1,
3753
4355
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -3791,7 +4393,7 @@ describe('Multisig', () => {
3791
4393
 
3792
4394
  // Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
3793
4395
  // Make the binding request derive a different commitment so the check fails.
3794
- vi.mocked(executeForSummary).mockResolvedValueOnce({
4396
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
3795
4397
  toCommitment: () => ({
3796
4398
  toHex: () => '0x' + '9'.repeat(64),
3797
4399
  }),
@@ -3855,6 +4457,7 @@ describe('Multisig', () => {
3855
4457
  signatures: [],
3856
4458
  metadata: {
3857
4459
  proposal_type: 'add_signer',
4460
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3858
4461
  target_threshold: 2,
3859
4462
  signer_commitments: ['0x1', '0x2'],
3860
4463
  },
@@ -3877,6 +4480,7 @@ describe('Multisig', () => {
3877
4480
 
3878
4481
  const proposal = await multisig.createProposal(1, 'AQID', {
3879
4482
  proposalType: 'add_signer',
4483
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3880
4484
  targetThreshold: 2,
3881
4485
  targetSignerCommitments: ['0x1', '0x2'],
3882
4486
  description: '',
@@ -3918,6 +4522,7 @@ describe('Multisig', () => {
3918
4522
  signatures: [],
3919
4523
  metadata: {
3920
4524
  proposal_type: 'p2id',
4525
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3921
4526
  recipient_id: '0xrecipient',
3922
4527
  faucet_id: '0xfaucet',
3923
4528
  amount: '100',
@@ -3963,6 +4568,7 @@ describe('Multisig', () => {
3963
4568
  signatures: [],
3964
4569
  metadata: {
3965
4570
  proposal_type: 'add_signer',
4571
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3966
4572
  target_threshold: 2,
3967
4573
  signer_commitments: ['0x1', '0x2'],
3968
4574
  description: '',
@@ -3986,6 +4592,7 @@ describe('Multisig', () => {
3986
4592
 
3987
4593
  const proposal = await multisig.createProposal(1, 'AQID', {
3988
4594
  proposalType: 'consume_notes',
4595
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3989
4596
  noteIds: ['0xnote1', '0xnote2'],
3990
4597
  description: '',
3991
4598
  });
@@ -4011,6 +4618,7 @@ describe('Multisig', () => {
4011
4618
  signatures: [],
4012
4619
  metadata: {
4013
4620
  proposal_type: 'add_signer',
4621
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4014
4622
  target_threshold: 1,
4015
4623
  signer_commitments: ['0x' + 'a'.repeat(64)],
4016
4624
  description: '',
@@ -4034,6 +4642,7 @@ describe('Multisig', () => {
4034
4642
 
4035
4643
  const proposal = await multisig.createProposal(1, 'AQID', {
4036
4644
  proposalType: 'p2id',
4645
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
4037
4646
  recipientId: '0xrecipient',
4038
4647
  faucetId: '0xfaucet',
4039
4648
  amount: '100',
@@ -4061,6 +4670,7 @@ describe('Multisig', () => {
4061
4670
  signatures: [],
4062
4671
  metadata: {
4063
4672
  proposalType: 'add_signer',
4673
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
4064
4674
  targetThreshold: 2,
4065
4675
  targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
4066
4676
  description: '',
@@ -4084,6 +4694,7 @@ describe('Multisig', () => {
4084
4694
 
4085
4695
  const proposal = await multisig.createProposal(1, 'AQID', {
4086
4696
  proposalType: 'switch_guardian',
4697
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
4087
4698
  newGuardianPubkey: '0xnewpubkey',
4088
4699
  newGuardianEndpoint: 'http://new-guardian.com',
4089
4700
  description: '',
@@ -4114,6 +4725,7 @@ describe('Multisig', () => {
4114
4725
  signatures: [],
4115
4726
  metadata: {
4116
4727
  proposal_type: 'add_signer',
4728
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4117
4729
  target_threshold: 2,
4118
4730
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
4119
4731
  description: '',
@@ -4150,6 +4762,7 @@ describe('Multisig', () => {
4150
4762
  ...mockProposalsPending[0].delta_payload,
4151
4763
  metadata: {
4152
4764
  proposal_type: 'add_signer',
4765
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4153
4766
  target_threshold: 2,
4154
4767
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
4155
4768
  description: '',
@@ -4254,6 +4867,7 @@ describe('Multisig', () => {
4254
4867
  signatures: [],
4255
4868
  metadata: {
4256
4869
  proposal_type: 'change_threshold',
4870
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4257
4871
  target_threshold: 3,
4258
4872
  signer_commitments: ['0xa', '0xb', '0xc'],
4259
4873
  salt: '0xlegacysalt',
@@ -4304,6 +4918,7 @@ describe('Multisig', () => {
4304
4918
  signatures: [],
4305
4919
  metadata: {
4306
4920
  proposal_type: 'p2id',
4921
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4307
4922
  recipient_id: '0xrecipient',
4308
4923
  faucet_id: '0xfaucet',
4309
4924
  amount: '12345',
@@ -4360,6 +4975,7 @@ describe('Multisig', () => {
4360
4975
  signatures: [],
4361
4976
  metadata: {
4362
4977
  proposal_type: 'switch_guardian',
4978
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
4363
4979
  new_guardian_pubkey: '0xnewpubkey',
4364
4980
  new_guardian_endpoint: 'http://new-guardian.com',
4365
4981
  salt: '0xsalt',