@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
@@ -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)],
@@ -1826,7 +2266,7 @@ describe('Multisig', () => {
1826
2266
  // Feature 009: only a conforming { code, message, meta } envelope is
1827
2267
  // folded into the error message; raw text bodies are dropped.
1828
2268
  text: async () => JSON.stringify({
1829
- code: 'GUARDIAN_PROPOSAL_NOT_FOUND',
2269
+ code: 'proposal_not_found',
1830
2270
  message: 'Proposal not found',
1831
2271
  meta: { retryable: false },
1832
2272
  }),
@@ -1855,6 +2295,7 @@ describe('Multisig', () => {
1855
2295
  ],
1856
2296
  metadata: {
1857
2297
  proposalType: 'add_signer',
2298
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1858
2299
  targetThreshold: 1,
1859
2300
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1860
2301
  description: '',
@@ -1886,6 +2327,7 @@ describe('Multisig', () => {
1886
2327
  ],
1887
2328
  metadata: {
1888
2329
  proposalType: 'change_threshold',
2330
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1889
2331
  targetThreshold: 1,
1890
2332
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1891
2333
  description: '',
@@ -1924,6 +2366,7 @@ describe('Multisig', () => {
1924
2366
  ],
1925
2367
  metadata: {
1926
2368
  proposalType: 'change_threshold',
2369
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1927
2370
  targetThreshold: 1,
1928
2371
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1929
2372
  description: '',
@@ -1945,6 +2388,7 @@ describe('Multisig', () => {
1945
2388
  signatures: [],
1946
2389
  metadata: {
1947
2390
  proposalType: 'add_signer',
2391
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
1948
2392
  targetThreshold: 2,
1949
2393
  targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1950
2394
  description: '',
@@ -2010,6 +2454,7 @@ describe('Multisig', () => {
2010
2454
  ],
2011
2455
  metadata: {
2012
2456
  proposalType: 'change_threshold',
2457
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2013
2458
  targetThreshold: 1,
2014
2459
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2015
2460
  description: '',
@@ -2026,6 +2471,7 @@ describe('Multisig', () => {
2026
2471
  signatures: [],
2027
2472
  metadata: {
2028
2473
  proposal_type: 'change_threshold',
2474
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2029
2475
  target_threshold: 1,
2030
2476
  signer_commitments: ['0x' + 'a'.repeat(64)],
2031
2477
  },
@@ -2051,8 +2497,14 @@ describe('Multisig', () => {
2051
2497
  await expect(multisig.createTransactionProposalRequest(requestedProposalId)).resolves.toBe(finalRequest);
2052
2498
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2053
2499
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2054
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
2055
- 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);
2056
2508
  expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2057
2509
  expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
2058
2510
  expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
@@ -2090,6 +2542,7 @@ describe('Multisig', () => {
2090
2542
  ],
2091
2543
  metadata: {
2092
2544
  proposalType: 'switch_guardian',
2545
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2093
2546
  newGuardianPubkey,
2094
2547
  newGuardianEndpoint: 'http://new-guardian.com',
2095
2548
  description: '',
@@ -2132,6 +2585,7 @@ describe('Multisig', () => {
2132
2585
  signatures: [],
2133
2586
  metadata: {
2134
2587
  proposal_type: 'add_signer',
2588
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2135
2589
  description: '',
2136
2590
  target_threshold: 2,
2137
2591
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -2166,7 +2620,7 @@ describe('Multisig', () => {
2166
2620
  };
2167
2621
  const multisig = createTestMultisig(config);
2168
2622
  const proposalId = '0x' + 'c'.repeat(64);
2169
- vi.mocked(executeForSummary).mockResolvedValueOnce({
2623
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
2170
2624
  toCommitment: () => ({
2171
2625
  toHex: () => '0x' + 'd'.repeat(64),
2172
2626
  }),
@@ -2186,6 +2640,7 @@ describe('Multisig', () => {
2186
2640
  ],
2187
2641
  metadata: {
2188
2642
  proposalType: 'change_threshold',
2643
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2189
2644
  targetThreshold: 1,
2190
2645
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2191
2646
  description: '',
@@ -2217,6 +2672,7 @@ describe('Multisig', () => {
2217
2672
  ],
2218
2673
  metadata: {
2219
2674
  proposalType: 'switch_guardian',
2675
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2220
2676
  newGuardianPubkey: '0x' + '1'.repeat(64),
2221
2677
  newGuardianEndpoint: 'http://new-guardian.com',
2222
2678
  description: '',
@@ -2257,6 +2713,7 @@ describe('Multisig', () => {
2257
2713
  ],
2258
2714
  metadata: {
2259
2715
  proposalType: 'switch_guardian',
2716
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2260
2717
  newGuardianPubkey: '0x' + '1'.repeat(64),
2261
2718
  newGuardianEndpoint: 'http://new-guardian.com',
2262
2719
  description: '',
@@ -2324,6 +2781,7 @@ describe('Multisig', () => {
2324
2781
  ],
2325
2782
  metadata: {
2326
2783
  proposalType: 'change_threshold',
2784
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2327
2785
  targetThreshold: 1,
2328
2786
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2329
2787
  description: '',
@@ -2340,6 +2798,7 @@ describe('Multisig', () => {
2340
2798
  signatures: [],
2341
2799
  metadata: {
2342
2800
  proposal_type: 'change_threshold',
2801
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2343
2802
  target_threshold: 1,
2344
2803
  signer_commitments: ['0x' + 'a'.repeat(64)],
2345
2804
  },
@@ -2410,6 +2869,7 @@ describe('Multisig', () => {
2410
2869
  ],
2411
2870
  metadata: {
2412
2871
  proposalType: 'switch_guardian',
2872
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2413
2873
  newGuardianPubkey: '0x' + '1'.repeat(64),
2414
2874
  newGuardianEndpoint: 'http://new-guardian.com',
2415
2875
  description: '',
@@ -2446,6 +2906,7 @@ describe('Multisig', () => {
2446
2906
  signatures: [],
2447
2907
  metadata: {
2448
2908
  proposal_type: 'add_signer',
2909
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2449
2910
  description: '',
2450
2911
  target_threshold: 2,
2451
2912
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
@@ -2473,7 +2934,7 @@ describe('Multisig', () => {
2473
2934
  // Proposal ID is mocked to return 'c'.repeat(64)
2474
2935
  await expect(multisig.executeProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
2475
2936
  });
2476
- it('should fail when GUARDIAN ack signature is missing (selector ON)', async () => {
2937
+ it('should fail when GUARDIAN ack signature is missing', async () => {
2477
2938
  const config = {
2478
2939
  threshold: 1,
2479
2940
  signerCommitments: ['0x' + 'a'.repeat(64)],
@@ -2489,6 +2950,7 @@ describe('Multisig', () => {
2489
2950
  signatures: [],
2490
2951
  metadata: {
2491
2952
  proposal_type: 'add_signer',
2953
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2492
2954
  description: '',
2493
2955
  target_threshold: 1,
2494
2956
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -2570,6 +3032,7 @@ describe('Multisig', () => {
2570
3032
  ],
2571
3033
  metadata: {
2572
3034
  proposalType: 'change_threshold',
3035
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2573
3036
  targetThreshold: 1,
2574
3037
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2575
3038
  description: '',
@@ -2586,6 +3049,7 @@ describe('Multisig', () => {
2586
3049
  signatures: [],
2587
3050
  metadata: {
2588
3051
  proposal_type: 'change_threshold',
3052
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2589
3053
  target_threshold: 1,
2590
3054
  signer_commitments: ['0x' + 'a'.repeat(64)],
2591
3055
  },
@@ -2626,8 +3090,14 @@ describe('Multisig', () => {
2626
3090
  expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2627
3091
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2628
3092
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2629
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
2630
- 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);
2631
3101
  });
2632
3102
  it('should execute imported ECDSA proposals with scheme-aware advice', async () => {
2633
3103
  const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
@@ -2666,6 +3136,7 @@ describe('Multisig', () => {
2666
3136
  ],
2667
3137
  metadata: {
2668
3138
  proposalType: 'change_threshold',
3139
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2669
3140
  targetThreshold: 1,
2670
3141
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
2671
3142
  description: '',
@@ -2682,6 +3153,7 @@ describe('Multisig', () => {
2682
3153
  signatures: [],
2683
3154
  metadata: {
2684
3155
  proposal_type: 'change_threshold',
3156
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2685
3157
  target_threshold: 1,
2686
3158
  signer_commitments: ['0x' + 'a'.repeat(64)],
2687
3159
  },
@@ -2711,8 +3183,14 @@ describe('Multisig', () => {
2711
3183
  expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2712
3184
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2713
3185
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2714
- expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
2715
- 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);
2716
3194
  });
2717
3195
  it('should verify switch_guardian endpoint commitment before execution', async () => {
2718
3196
  const config = {
@@ -2738,6 +3216,7 @@ describe('Multisig', () => {
2738
3216
  ],
2739
3217
  metadata: {
2740
3218
  proposalType: 'switch_guardian',
3219
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2741
3220
  newGuardianPubkey,
2742
3221
  newGuardianEndpoint: 'http://new-guardian.com',
2743
3222
  description: '',
@@ -2759,6 +3238,7 @@ describe('Multisig', () => {
2759
3238
  signatures: [],
2760
3239
  metadata: {
2761
3240
  proposal_type: 'switch_guardian',
3241
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
2762
3242
  new_guardian_pubkey: newGuardianPubkey,
2763
3243
  new_guardian_endpoint: 'http://new-guardian.com',
2764
3244
  },
@@ -2818,6 +3298,7 @@ describe('Multisig', () => {
2818
3298
  ],
2819
3299
  metadata: {
2820
3300
  proposalType: 'switch_guardian',
3301
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2821
3302
  newGuardianPubkey,
2822
3303
  newGuardianEndpoint: 'http://new-guardian.com',
2823
3304
  description: '',
@@ -2865,6 +3346,7 @@ describe('Multisig', () => {
2865
3346
  ],
2866
3347
  metadata: {
2867
3348
  proposalType: 'switch_guardian',
3349
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2868
3350
  newGuardianPubkey: '0x' + '1'.repeat(64),
2869
3351
  newGuardianEndpoint: 'http://new-guardian.com',
2870
3352
  description: '',
@@ -2905,6 +3387,7 @@ describe('Multisig', () => {
2905
3387
  ],
2906
3388
  metadata: {
2907
3389
  proposalType: 'switch_guardian',
3390
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2908
3391
  newGuardianPubkey: '0x' + '1'.repeat(64),
2909
3392
  newGuardianEndpoint: 'http://new-guardian.com',
2910
3393
  description: '',
@@ -2950,6 +3433,7 @@ describe('Multisig', () => {
2950
3433
  ],
2951
3434
  metadata: {
2952
3435
  proposalType: 'switch_guardian',
3436
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
2953
3437
  newGuardianPubkey: '0x' + '1'.repeat(64),
2954
3438
  newGuardianEndpoint: 'http://new-guardian.com',
2955
3439
  description: '',
@@ -2975,9 +3459,32 @@ describe('Multisig', () => {
2975
3459
  .mockRejectedValueOnce(transient)
2976
3460
  .mockRejectedValueOnce(transient)
2977
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
+ });
2978
3485
  vi.useFakeTimers();
2979
3486
  try {
2980
- const submission = multisig.submitTransaction({});
3487
+ const submission = multisig.submitTransaction('0x' + 'c'.repeat(64), {});
2981
3488
  await vi.runAllTimersAsync();
2982
3489
  await submission;
2983
3490
  }
@@ -3002,6 +3509,7 @@ describe('Multisig', () => {
3002
3509
  signatures: [],
3003
3510
  metadata: {
3004
3511
  proposal_type: proposalType,
3512
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3005
3513
  description: '',
3006
3514
  },
3007
3515
  },
@@ -3032,6 +3540,7 @@ describe('Multisig', () => {
3032
3540
  };
3033
3541
  builtinDelta.delta_payload.metadata = {
3034
3542
  proposal_type: 'change_threshold',
3543
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3035
3544
  description: '',
3036
3545
  target_threshold: 1,
3037
3546
  signer_commitments: ['0x' + 'a'.repeat(64)],
@@ -3064,7 +3573,7 @@ describe('Multisig', () => {
3064
3573
  const multisig = createTestMultisig(config);
3065
3574
  // Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
3066
3575
  // Make the binding request derive a different commitment so the check fails.
3067
- vi.mocked(executeForSummary).mockResolvedValueOnce({
3576
+ vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
3068
3577
  toCommitment: () => ({
3069
3578
  toHex: () => '0x' + '9'.repeat(64),
3070
3579
  }),
@@ -3115,6 +3624,7 @@ describe('Multisig', () => {
3115
3624
  signatures: [],
3116
3625
  metadata: {
3117
3626
  proposal_type: 'add_signer',
3627
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3118
3628
  target_threshold: 2,
3119
3629
  signer_commitments: ['0x1', '0x2'],
3120
3630
  },
@@ -3135,6 +3645,7 @@ describe('Multisig', () => {
3135
3645
  });
3136
3646
  const proposal = await multisig.createProposal(1, 'AQID', {
3137
3647
  proposalType: 'add_signer',
3648
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3138
3649
  targetThreshold: 2,
3139
3650
  targetSignerCommitments: ['0x1', '0x2'],
3140
3651
  description: '',
@@ -3169,6 +3680,7 @@ describe('Multisig', () => {
3169
3680
  signatures: [],
3170
3681
  metadata: {
3171
3682
  proposal_type: 'p2id',
3683
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3172
3684
  recipient_id: '0xrecipient',
3173
3685
  faucet_id: '0xfaucet',
3174
3686
  amount: '100',
@@ -3208,6 +3720,7 @@ describe('Multisig', () => {
3208
3720
  signatures: [],
3209
3721
  metadata: {
3210
3722
  proposal_type: 'add_signer',
3723
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3211
3724
  target_threshold: 2,
3212
3725
  signer_commitments: ['0x1', '0x2'],
3213
3726
  description: '',
@@ -3229,6 +3742,7 @@ describe('Multisig', () => {
3229
3742
  });
3230
3743
  const proposal = await multisig.createProposal(1, 'AQID', {
3231
3744
  proposalType: 'consume_notes',
3745
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3232
3746
  noteIds: ['0xnote1', '0xnote2'],
3233
3747
  description: '',
3234
3748
  });
@@ -3250,6 +3764,7 @@ describe('Multisig', () => {
3250
3764
  signatures: [],
3251
3765
  metadata: {
3252
3766
  proposal_type: 'add_signer',
3767
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3253
3768
  target_threshold: 1,
3254
3769
  signer_commitments: ['0x' + 'a'.repeat(64)],
3255
3770
  description: '',
@@ -3271,6 +3786,7 @@ describe('Multisig', () => {
3271
3786
  });
3272
3787
  const proposal = await multisig.createProposal(1, 'AQID', {
3273
3788
  proposalType: 'p2id',
3789
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3274
3790
  recipientId: '0xrecipient',
3275
3791
  faucetId: '0xfaucet',
3276
3792
  amount: '100',
@@ -3294,6 +3810,7 @@ describe('Multisig', () => {
3294
3810
  signatures: [],
3295
3811
  metadata: {
3296
3812
  proposalType: 'add_signer',
3813
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3297
3814
  targetThreshold: 2,
3298
3815
  targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3299
3816
  description: '',
@@ -3315,6 +3832,7 @@ describe('Multisig', () => {
3315
3832
  });
3316
3833
  const proposal = await multisig.createProposal(1, 'AQID', {
3317
3834
  proposalType: 'switch_guardian',
3835
+ chainAnchor: MOCK_CHAIN_ANCHOR_B64,
3318
3836
  newGuardianPubkey: '0xnewpubkey',
3319
3837
  newGuardianEndpoint: 'http://new-guardian.com',
3320
3838
  description: '',
@@ -3341,6 +3859,7 @@ describe('Multisig', () => {
3341
3859
  signatures: [],
3342
3860
  metadata: {
3343
3861
  proposal_type: 'add_signer',
3862
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3344
3863
  target_threshold: 2,
3345
3864
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3346
3865
  description: '',
@@ -3374,6 +3893,7 @@ describe('Multisig', () => {
3374
3893
  ...mockProposalsPending[0].delta_payload,
3375
3894
  metadata: {
3376
3895
  proposal_type: 'add_signer',
3896
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3377
3897
  target_threshold: 2,
3378
3898
  signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3379
3899
  description: '',
@@ -3465,6 +3985,7 @@ describe('Multisig', () => {
3465
3985
  signatures: [],
3466
3986
  metadata: {
3467
3987
  proposal_type: 'change_threshold',
3988
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3468
3989
  target_threshold: 3,
3469
3990
  signer_commitments: ['0xa', '0xb', '0xc'],
3470
3991
  salt: '0xlegacysalt',
@@ -3509,6 +4030,7 @@ describe('Multisig', () => {
3509
4030
  signatures: [],
3510
4031
  metadata: {
3511
4032
  proposal_type: 'p2id',
4033
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3512
4034
  recipient_id: '0xrecipient',
3513
4035
  faucet_id: '0xfaucet',
3514
4036
  amount: '12345',
@@ -3559,6 +4081,7 @@ describe('Multisig', () => {
3559
4081
  signatures: [],
3560
4082
  metadata: {
3561
4083
  proposal_type: 'switch_guardian',
4084
+ chain_anchor: MOCK_CHAIN_ANCHOR_B64,
3562
4085
  new_guardian_pubkey: '0xnewpubkey',
3563
4086
  new_guardian_endpoint: 'http://new-guardian.com',
3564
4087
  salt: '0xsalt',