@openzeppelin/miden-multisig-client 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/README.md +98 -119
  2. package/dist/account/builder.d.ts +18 -0
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +59 -29
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/builder.test.js +108 -32
  7. package/dist/account/builder.test.js.map +1 -1
  8. package/dist/account/index.d.ts +1 -1
  9. package/dist/account/index.d.ts.map +1 -1
  10. package/dist/account/index.js +1 -1
  11. package/dist/account/index.js.map +1 -1
  12. package/dist/account/masm/account-components/auth.d.ts +5 -0
  13. package/dist/account/masm/account-components/auth.d.ts.map +1 -0
  14. package/dist/account/masm/account-components/auth.js +67 -0
  15. package/dist/account/masm/account-components/auth.js.map +1 -0
  16. package/dist/account/masm/auth.d.ts +5 -0
  17. package/dist/account/masm/auth.d.ts.map +1 -0
  18. package/dist/account/masm/auth.js +1395 -0
  19. package/dist/account/masm/auth.js.map +1 -0
  20. package/dist/account/masm/index.d.ts +3 -0
  21. package/dist/account/masm/index.d.ts.map +1 -0
  22. package/dist/account/masm/index.js +5 -0
  23. package/dist/account/masm/index.js.map +1 -0
  24. package/dist/account/masm.d.ts +1 -4
  25. package/dist/account/masm.d.ts.map +1 -1
  26. package/dist/account/masm.js +3 -1034
  27. package/dist/account/masm.js.map +1 -1
  28. package/dist/account/storage.d.ts +2 -2
  29. package/dist/account/storage.d.ts.map +1 -1
  30. package/dist/account/storage.js +18 -16
  31. package/dist/account/storage.js.map +1 -1
  32. package/dist/client.d.ts +67 -9
  33. package/dist/client.d.ts.map +1 -1
  34. package/dist/client.js +73 -22
  35. package/dist/client.js.map +1 -1
  36. package/dist/client.test.js +31 -32
  37. package/dist/client.test.js.map +1 -1
  38. package/dist/index.d.ts +54 -12
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +53 -9
  41. package/dist/index.js.map +1 -1
  42. package/dist/inspector.d.ts +32 -6
  43. package/dist/inspector.d.ts.map +1 -1
  44. package/dist/inspector.js +58 -23
  45. package/dist/inspector.js.map +1 -1
  46. package/dist/inspector.test.js +18 -18
  47. package/dist/inspector.test.js.map +1 -1
  48. package/dist/multisig/proposal/execution.d.ts +4 -4
  49. package/dist/multisig/proposal/execution.d.ts.map +1 -1
  50. package/dist/multisig/proposal/execution.js +42 -31
  51. package/dist/multisig/proposal/execution.js.map +1 -1
  52. package/dist/multisig/proposal/parser.d.ts +3 -3
  53. package/dist/multisig/proposal/parser.d.ts.map +1 -1
  54. package/dist/multisig/proposal/parser.js +23 -23
  55. package/dist/multisig/proposal/parser.js.map +1 -1
  56. package/dist/multisig/signing.d.ts +3 -3
  57. package/dist/multisig/signing.d.ts.map +1 -1
  58. package/dist/multisig/signing.js +3 -3
  59. package/dist/multisig/signing.js.map +1 -1
  60. package/dist/multisig.d.ts +188 -48
  61. package/dist/multisig.d.ts.map +1 -1
  62. package/dist/multisig.js +650 -308
  63. package/dist/multisig.js.map +1 -1
  64. package/dist/multisig.test.js +1680 -241
  65. package/dist/multisig.test.js.map +1 -1
  66. package/dist/procedures.d.ts +46 -24
  67. package/dist/procedures.d.ts.map +1 -1
  68. package/dist/procedures.js +46 -21
  69. package/dist/procedures.js.map +1 -1
  70. package/dist/proposal/factory.d.ts +18 -0
  71. package/dist/proposal/factory.d.ts.map +1 -0
  72. package/dist/proposal/factory.js +99 -0
  73. package/dist/proposal/factory.js.map +1 -0
  74. package/dist/proposal/metadata.d.ts +5 -12
  75. package/dist/proposal/metadata.d.ts.map +1 -1
  76. package/dist/proposal/metadata.js +118 -174
  77. package/dist/proposal/metadata.js.map +1 -1
  78. package/dist/proposal/signatures.d.ts +10 -0
  79. package/dist/proposal/signatures.d.ts.map +1 -0
  80. package/dist/proposal/signatures.js +54 -0
  81. package/dist/proposal/signatures.js.map +1 -0
  82. package/dist/signer.d.ts +2 -0
  83. package/dist/signer.d.ts.map +1 -1
  84. package/dist/signer.js +2 -0
  85. package/dist/signer.js.map +1 -1
  86. package/dist/signers/ecdsa.d.ts +3 -0
  87. package/dist/signers/ecdsa.d.ts.map +1 -1
  88. package/dist/signers/ecdsa.js +8 -4
  89. package/dist/signers/ecdsa.js.map +1 -1
  90. package/dist/signers/ecdsa.test.d.ts +2 -0
  91. package/dist/signers/ecdsa.test.d.ts.map +1 -0
  92. package/dist/signers/ecdsa.test.js +65 -0
  93. package/dist/signers/ecdsa.test.js.map +1 -0
  94. package/dist/signers/falcon.d.ts +3 -0
  95. package/dist/signers/falcon.d.ts.map +1 -1
  96. package/dist/signers/falcon.js +8 -4
  97. package/dist/signers/falcon.js.map +1 -1
  98. package/dist/signers/falcon.test.js +12 -0
  99. package/dist/signers/falcon.test.js.map +1 -1
  100. package/dist/signers/miden-wallet.d.ts +3 -1
  101. package/dist/signers/miden-wallet.d.ts.map +1 -1
  102. package/dist/signers/miden-wallet.js +11 -2
  103. package/dist/signers/miden-wallet.js.map +1 -1
  104. package/dist/signers/miden-wallet.test.js +26 -0
  105. package/dist/signers/miden-wallet.test.js.map +1 -1
  106. package/dist/signers/para.d.ts +3 -3
  107. package/dist/signers/para.d.ts.map +1 -1
  108. package/dist/signers/para.js +15 -6
  109. package/dist/signers/para.js.map +1 -1
  110. package/dist/signers/para.test.js +39 -0
  111. package/dist/signers/para.test.js.map +1 -1
  112. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  113. package/dist/transaction/consumeNotes.js +1 -0
  114. package/dist/transaction/consumeNotes.js.map +1 -1
  115. package/dist/transaction/index.d.ts +2 -1
  116. package/dist/transaction/index.d.ts.map +1 -1
  117. package/dist/transaction/index.js +2 -1
  118. package/dist/transaction/index.js.map +1 -1
  119. package/dist/transaction/options.d.ts +1 -1
  120. package/dist/transaction/options.d.ts.map +1 -1
  121. package/dist/transaction/p2id.d.ts +1 -0
  122. package/dist/transaction/p2id.d.ts.map +1 -1
  123. package/dist/transaction/p2id.js +9 -3
  124. package/dist/transaction/p2id.js.map +1 -1
  125. package/dist/transaction/updateGuardian.d.ts +7 -0
  126. package/dist/transaction/updateGuardian.d.ts.map +1 -0
  127. package/dist/transaction/{updatePsm.js → updateGuardian.js} +15 -29
  128. package/dist/transaction/updateGuardian.js.map +1 -0
  129. package/dist/transaction/updateProcedureThreshold.d.ts +9 -0
  130. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -0
  131. package/dist/transaction/updateProcedureThreshold.js +55 -0
  132. package/dist/transaction/updateProcedureThreshold.js.map +1 -0
  133. package/dist/transaction/updateSigners.d.ts.map +1 -1
  134. package/dist/transaction/updateSigners.js +5 -23
  135. package/dist/transaction/updateSigners.js.map +1 -1
  136. package/dist/transaction.d.ts +2 -1
  137. package/dist/transaction.d.ts.map +1 -1
  138. package/dist/transaction.js +2 -1
  139. package/dist/transaction.js.map +1 -1
  140. package/dist/types/proposal.d.ts +32 -15
  141. package/dist/types/proposal.d.ts.map +1 -1
  142. package/dist/types.d.ts +24 -7
  143. package/dist/types.d.ts.map +1 -1
  144. package/dist/utils/digest.d.ts +5 -0
  145. package/dist/utils/digest.d.ts.map +1 -1
  146. package/dist/utils/digest.js +34 -1
  147. package/dist/utils/digest.js.map +1 -1
  148. package/dist/utils/digest.test.d.ts +2 -0
  149. package/dist/utils/digest.test.d.ts.map +1 -0
  150. package/dist/utils/digest.test.js +48 -0
  151. package/dist/utils/digest.test.js.map +1 -0
  152. package/dist/utils/signature.d.ts +3 -1
  153. package/dist/utils/signature.d.ts.map +1 -1
  154. package/dist/utils/signature.js +31 -6
  155. package/dist/utils/signature.js.map +1 -1
  156. package/dist/utils/word.test.js +1 -52
  157. package/dist/utils/word.test.js.map +1 -1
  158. package/masm/account_components/auth/multisig.masm +13 -0
  159. package/masm/account_components/auth/multisig_ecdsa.masm +13 -0
  160. package/masm/account_components/auth/multisig_guardian.masm +17 -0
  161. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +17 -0
  162. package/masm/auth/guardian.masm +177 -0
  163. package/masm/auth/guardian_ecdsa.masm +178 -0
  164. package/masm/{multisig.masm → auth/multisig.masm} +149 -55
  165. package/masm/{multisig_ecdsa.masm → auth/multisig_ecdsa.masm} +148 -56
  166. package/package.json +11 -9
  167. package/src/account/builder.test.ts +165 -0
  168. package/src/account/builder.ts +71 -34
  169. package/src/account/index.ts +1 -1
  170. package/src/account/masm/account-components/auth.ts +70 -0
  171. package/src/account/masm/auth.ts +1398 -0
  172. package/src/account/masm/index.ts +5 -0
  173. package/src/account/masm.ts +3 -1040
  174. package/src/account/storage.ts +18 -16
  175. package/src/client.test.ts +31 -35
  176. package/src/client.ts +98 -26
  177. package/src/index.ts +84 -22
  178. package/src/inspector.test.ts +17 -17
  179. package/src/inspector.ts +63 -29
  180. package/src/multisig/proposal/execution.ts +66 -49
  181. package/src/multisig/proposal/parser.ts +26 -26
  182. package/src/multisig/signing.ts +4 -4
  183. package/src/multisig.test.ts +2097 -248
  184. package/src/multisig.ts +882 -388
  185. package/src/procedures.ts +50 -26
  186. package/src/proposal/factory.ts +171 -0
  187. package/src/proposal/metadata.ts +165 -0
  188. package/src/proposal/signatures.ts +68 -0
  189. package/src/signer.ts +2 -0
  190. package/src/signers/ecdsa.test.ts +80 -0
  191. package/src/signers/ecdsa.ts +15 -4
  192. package/src/signers/falcon.test.ts +19 -0
  193. package/src/signers/falcon.ts +16 -5
  194. package/src/signers/miden-wallet.test.ts +47 -0
  195. package/src/signers/miden-wallet.ts +18 -1
  196. package/src/signers/para.test.ts +57 -0
  197. package/src/signers/para.ts +26 -7
  198. package/src/transaction/consumeNotes.ts +1 -0
  199. package/src/transaction/index.ts +2 -2
  200. package/src/transaction/options.ts +1 -2
  201. package/src/transaction/p2id.test.ts +190 -0
  202. package/src/transaction/p2id.ts +11 -5
  203. package/src/transaction/rpoRandomCoin.test.ts +64 -0
  204. package/src/transaction/rpoRandomCoin.ts +24 -0
  205. package/src/transaction/{updatePsm.ts → updateGuardian.ts} +19 -32
  206. package/src/transaction/updateProcedureThreshold.ts +97 -0
  207. package/src/transaction/updateSigners.ts +12 -27
  208. package/src/transaction.ts +5 -3
  209. package/src/types/proposal.ts +43 -11
  210. package/src/types.ts +29 -11
  211. package/src/utils/digest.test.ts +55 -0
  212. package/src/utils/digest.ts +51 -1
  213. package/src/utils/signature.ts +47 -9
  214. package/src/utils/word.test.ts +1 -55
  215. package/dist/account.d.ts +0 -104
  216. package/dist/account.d.ts.map +0 -1
  217. package/dist/account.js +0 -136
  218. package/dist/account.js.map +0 -1
  219. package/dist/builder.d.ts +0 -54
  220. package/dist/builder.d.ts.map +0 -1
  221. package/dist/builder.js +0 -78
  222. package/dist/builder.js.map +0 -1
  223. package/dist/http.d.ts +0 -64
  224. package/dist/http.d.ts.map +0 -1
  225. package/dist/http.js +0 -162
  226. package/dist/http.js.map +0 -1
  227. package/dist/keystore.d.ts +0 -65
  228. package/dist/keystore.d.ts.map +0 -1
  229. package/dist/keystore.js +0 -121
  230. package/dist/keystore.js.map +0 -1
  231. package/dist/miden.d.ts +0 -9
  232. package/dist/miden.d.ts.map +0 -1
  233. package/dist/miden.js +0 -26
  234. package/dist/miden.js.map +0 -1
  235. package/dist/multisig/metadata.d.ts +0 -18
  236. package/dist/multisig/metadata.d.ts.map +0 -1
  237. package/dist/multisig/metadata.js +0 -100
  238. package/dist/multisig/metadata.js.map +0 -1
  239. package/dist/multisig/metadata.test.d.ts +0 -2
  240. package/dist/multisig/metadata.test.d.ts.map +0 -1
  241. package/dist/multisig/metadata.test.js +0 -323
  242. package/dist/multisig/metadata.test.js.map +0 -1
  243. package/dist/proposal/exported.d.ts +0 -12
  244. package/dist/proposal/exported.d.ts.map +0 -1
  245. package/dist/proposal/exported.js +0 -88
  246. package/dist/proposal/exported.js.map +0 -1
  247. package/dist/signer.test.d.ts +0 -2
  248. package/dist/signer.test.d.ts.map +0 -1
  249. package/dist/signer.test.js +0 -123
  250. package/dist/signer.test.js.map +0 -1
  251. package/dist/transaction/p2id.interop.test.d.ts +0 -2
  252. package/dist/transaction/p2id.interop.test.d.ts.map +0 -1
  253. package/dist/transaction/p2id.interop.test.js +0 -36
  254. package/dist/transaction/p2id.interop.test.js.map +0 -1
  255. package/dist/transaction/updatePsm.d.ts +0 -7
  256. package/dist/transaction/updatePsm.d.ts.map +0 -1
  257. package/dist/transaction/updatePsm.js.map +0 -1
  258. package/dist/transport/http.d.ts +0 -64
  259. package/dist/transport/http.d.ts.map +0 -1
  260. package/dist/transport/http.js +0 -162
  261. package/dist/transport/http.js.map +0 -1
  262. package/dist/transport/index.d.ts +0 -5
  263. package/dist/transport/index.d.ts.map +0 -1
  264. package/dist/transport/index.js +0 -5
  265. package/dist/transport/index.js.map +0 -1
  266. package/dist/utils/state-validation.d.ts +0 -5
  267. package/dist/utils/state-validation.d.ts.map +0 -1
  268. package/dist/utils/state-validation.js +0 -30
  269. package/dist/utils/state-validation.js.map +0 -1
  270. package/dist/utils/validation.d.ts +0 -6
  271. package/dist/utils/validation.d.ts.map +0 -1
  272. package/dist/utils/validation.js +0 -52
  273. package/dist/utils/validation.js.map +0 -1
  274. package/masm/psm.masm +0 -178
  275. package/masm/psm_ecdsa.masm +0 -179
@@ -1,8 +1,17 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { Multisig } from './multisig.js';
3
- import { PsmHttpClient } from '@openzeppelin/psm-client';
3
+ import { GuardianHttpClient } from '@openzeppelin/guardian-client';
4
+ import { buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildUpdateSignersTransactionRequest, executeForSummary, } from './transaction.js';
5
+ const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig } = vi.hoisted(() => ({
6
+ mockRpcGetAccountDetails: vi.fn(),
7
+ mockAccountDeserialize: vi.fn(),
8
+ mockDetectConfig: vi.fn(),
9
+ }));
4
10
  // Mock the Miden SDK
5
11
  vi.mock('@miden-sdk/miden-sdk', () => ({
12
+ Account: {
13
+ deserialize: mockAccountDeserialize,
14
+ },
6
15
  AccountId: {
7
16
  fromHex: vi.fn((hex) => ({ toString: () => hex })),
8
17
  },
@@ -37,6 +46,10 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
37
46
  toHex: () => '0x' + 'e'.repeat(64),
38
47
  }),
39
48
  },
49
+ Endpoint: vi.fn().mockImplementation((url) => ({ url })),
50
+ RpcClient: vi.fn().mockImplementation(() => ({
51
+ getAccountDetails: mockRpcGetAccountDetails,
52
+ })),
40
53
  }));
41
54
  // Mock transaction module
42
55
  vi.mock('./transaction.js', () => ({
@@ -46,7 +59,12 @@ vi.mock('./transaction.js', () => ({
46
59
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
47
60
  configHash: { toHex: () => '0x' + 'e'.repeat(64) },
48
61
  }),
49
- buildUpdatePsmTransactionRequest: vi.fn().mockResolvedValue({
62
+ buildUpdateProcedureThresholdTransactionRequest: vi.fn().mockResolvedValue({
63
+ request: {},
64
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
65
+ configHash: { toHex: () => '0x' + 'e'.repeat(64) },
66
+ }),
67
+ buildUpdateGuardianTransactionRequest: vi.fn().mockResolvedValue({
50
68
  request: {},
51
69
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
52
70
  }),
@@ -59,43 +77,83 @@ vi.mock('./transaction.js', () => ({
59
77
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
60
78
  }),
61
79
  }));
62
- vi.mock('./utils/signature.js', () => ({
63
- buildSignatureAdviceEntry: vi.fn().mockReturnValue({
64
- key: { toHex: () => '0x' + 'f'.repeat(64) },
65
- values: [1, 2, 3],
66
- }),
67
- signatureHexToBytes: vi.fn((hex) => new Uint8Array([0, 1, 2, 3])),
68
- }));
80
+ vi.mock('./utils/signature.js', async () => {
81
+ const actual = await vi.importActual('./utils/signature.js');
82
+ return {
83
+ ...actual,
84
+ buildSignatureAdviceEntry: vi.fn().mockImplementation((signerCommitment) => ({
85
+ key: { toHex: () => signerCommitment.toHex ? signerCommitment.toHex() : '0x' + 'f'.repeat(64) },
86
+ values: [1, 2, 3],
87
+ })),
88
+ signatureHexToBytes: vi.fn((hex) => new Uint8Array([0, 1, 2, 3])),
89
+ };
90
+ });
69
91
  vi.mock('./utils/encoding.js', async () => {
70
92
  const actual = await vi.importActual('./utils/encoding.js');
71
93
  return {
72
- ensureHexPrefix: actual.ensureHexPrefix,
73
- normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').padStart(64, '0')),
74
- bytesToHex: actual.bytesToHex,
75
- hexToBytes: actual.hexToBytes,
76
- uint8ArrayToBase64: actual.uint8ArrayToBase64,
77
- base64ToUint8Array: actual.base64ToUint8Array,
94
+ ...actual,
95
+ normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').toLowerCase().padStart(64, '0')),
78
96
  };
79
97
  });
80
- // Mock fetch for PSM client
98
+ vi.mock('./inspector.js', () => ({
99
+ AccountInspector: {
100
+ fromAccount: mockDetectConfig,
101
+ },
102
+ }));
103
+ // Mock fetch for GUARDIAN client
81
104
  const mockFetch = vi.fn();
82
105
  vi.stubGlobal('fetch', mockFetch);
106
+ function mockedAccount(commitmentHex, nonce = 0) {
107
+ return {
108
+ commitment: () => ({
109
+ toHex: () => commitmentHex,
110
+ }),
111
+ nonce: () => ({
112
+ asInt: () => BigInt(nonce),
113
+ }),
114
+ };
115
+ }
83
116
  describe('Multisig', () => {
84
- let psm;
117
+ let guardian;
85
118
  let mockSigner;
86
119
  let mockAccount;
87
120
  let mockWebClient;
88
121
  beforeEach(() => {
89
122
  mockFetch.mockReset();
90
- psm = new PsmHttpClient('http://localhost:3000');
123
+ vi.mocked(executeForSummary).mockResolvedValue({
124
+ toCommitment: () => ({
125
+ toHex: () => '0x' + 'c'.repeat(64),
126
+ }),
127
+ serialize: () => new Uint8Array([1, 2, 3]),
128
+ });
129
+ mockRpcGetAccountDetails.mockReset();
130
+ mockAccountDeserialize.mockReset();
131
+ mockRpcGetAccountDetails.mockResolvedValue({
132
+ commitment: () => ({
133
+ toHex: () => '0x' + 'b'.repeat(64),
134
+ }),
135
+ });
136
+ mockAccountDeserialize.mockReturnValue(mockedAccount('0x' + 'b'.repeat(64), 1));
137
+ mockDetectConfig.mockReset();
138
+ mockDetectConfig.mockReturnValue({
139
+ threshold: 1,
140
+ numSigners: 1,
141
+ signerCommitments: ['0x' + 'a'.repeat(64)],
142
+ guardianEnabled: true,
143
+ guardianCommitment: '0x' + 'c'.repeat(64),
144
+ vaultBalances: [],
145
+ procedureThresholds: new Map(),
146
+ });
147
+ guardian = new GuardianHttpClient('http://localhost:3000');
91
148
  mockSigner = {
92
- scheme: 'falcon',
93
149
  commitment: '0x' + '1'.repeat(64),
94
150
  publicKey: '0x' + '2'.repeat(64),
151
+ scheme: 'falcon',
95
152
  signAccountIdWithTimestamp: vi.fn().mockResolvedValue('0x' + 'a'.repeat(128)),
96
- signCommitment: vi.fn().mockResolvedValue('0x' + 'b'.repeat(128)),
153
+ signRequest: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
154
+ signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
97
155
  };
98
- psm.setSigner(mockSigner);
156
+ guardian.setSigner(mockSigner);
99
157
  mockAccount = {
100
158
  id: () => ({
101
159
  toString: () => '0x' + 'a'.repeat(30),
@@ -111,6 +169,8 @@ describe('Multisig', () => {
111
169
  applyTransaction: vi.fn(),
112
170
  getConsumableNotes: vi.fn().mockResolvedValue([]),
113
171
  syncState: vi.fn(),
172
+ getAccount: vi.fn().mockResolvedValue(null),
173
+ newAccount: vi.fn(),
114
174
  };
115
175
  });
116
176
  describe('constructor', () => {
@@ -118,23 +178,23 @@ describe('Multisig', () => {
118
178
  const config = {
119
179
  threshold: 2,
120
180
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
121
- psmCommitment: '0x' + 'c'.repeat(64),
181
+ guardianCommitment: '0x' + 'c'.repeat(64),
122
182
  };
123
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
183
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
124
184
  expect(multisig.threshold).toBe(2);
125
185
  expect(multisig.signerCommitments).toEqual(config.signerCommitments);
126
- expect(multisig.psmCommitment).toBe(config.psmCommitment);
186
+ expect(multisig.guardianCommitment).toBe(config.guardianCommitment);
127
187
  expect(multisig.account).toBe(mockAccount);
128
188
  });
129
- it('should create Multisig without account (loaded)', () => {
189
+ it('should create Multisig with explicit accountId override', () => {
130
190
  const config = {
131
191
  threshold: 2,
132
192
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
133
- psmCommitment: '0x' + 'c'.repeat(64),
193
+ guardianCommitment: '0x' + 'c'.repeat(64),
134
194
  };
135
195
  const accountId = '0x' + 'd'.repeat(30);
136
- const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, accountId);
137
- expect(multisig.account).toBeNull();
196
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, accountId);
197
+ expect(multisig.account).toBe(mockAccount);
138
198
  expect(multisig.accountId).toBe(accountId);
139
199
  });
140
200
  });
@@ -143,19 +203,19 @@ describe('Multisig', () => {
143
203
  const config = {
144
204
  threshold: 1,
145
205
  signerCommitments: ['0x' + 'a'.repeat(64)],
146
- psmCommitment: '0x' + 'c'.repeat(64),
206
+ guardianCommitment: '0x' + 'c'.repeat(64),
147
207
  };
148
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
208
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
149
209
  expect(multisig.accountId).toBe('0x' + 'a'.repeat(30));
150
210
  });
151
- it('should return provided account ID for loaded multisig', () => {
211
+ it('should return provided account ID when constructor override is set', () => {
152
212
  const config = {
153
213
  threshold: 1,
154
214
  signerCommitments: ['0x' + 'a'.repeat(64)],
155
- psmCommitment: '0x' + 'c'.repeat(64),
215
+ guardianCommitment: '0x' + 'c'.repeat(64),
156
216
  };
157
217
  const accountId = '0x' + 'e'.repeat(30);
158
- const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, accountId);
218
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, accountId);
159
219
  expect(multisig.accountId).toBe(accountId);
160
220
  });
161
221
  });
@@ -164,20 +224,20 @@ describe('Multisig', () => {
164
224
  const config = {
165
225
  threshold: 1,
166
226
  signerCommitments: ['0x' + 'a'.repeat(64)],
167
- psmCommitment: '0x' + 'c'.repeat(64),
227
+ guardianCommitment: '0x' + 'c'.repeat(64),
168
228
  };
169
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
229
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
170
230
  expect(multisig.signerCommitment).toBe(mockSigner.commitment);
171
231
  });
172
232
  });
173
233
  describe('fetchState', () => {
174
- it('should fetch account state from PSM', async () => {
234
+ it('should fetch account state from GUARDIAN', async () => {
175
235
  const config = {
176
236
  threshold: 1,
177
237
  signerCommitments: ['0x' + 'a'.repeat(64)],
178
- psmCommitment: '0x' + 'c'.repeat(64),
238
+ guardianCommitment: '0x' + 'c'.repeat(64),
179
239
  };
180
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
240
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
181
241
  mockFetch.mockResolvedValueOnce({
182
242
  ok: true,
183
243
  json: async () => ({
@@ -194,14 +254,244 @@ describe('Multisig', () => {
194
254
  expect(state.stateDataBase64).toBe('base64state');
195
255
  });
196
256
  });
197
- describe('registerOnPsm', () => {
198
- it('should register account on PSM', async () => {
257
+ describe('syncState', () => {
258
+ it('should overwrite local state when account is missing locally', async () => {
259
+ const config = {
260
+ threshold: 1,
261
+ signerCommitments: ['0x' + 'a'.repeat(64)],
262
+ guardianCommitment: '0x' + 'c'.repeat(64),
263
+ };
264
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
265
+ mockWebClient.getAccount.mockResolvedValueOnce(null);
266
+ mockFetch.mockResolvedValueOnce({
267
+ ok: true,
268
+ json: async () => ({
269
+ account_id: multisig.accountId,
270
+ commitment: '0x' + 'b'.repeat(64),
271
+ state_json: { data: 'AQID' },
272
+ created_at: '2024-01-01T00:00:00Z',
273
+ updated_at: '2024-01-02T00:00:00Z',
274
+ }),
275
+ });
276
+ await multisig.syncState();
277
+ expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
278
+ expect(mockRpcGetAccountDetails).toHaveBeenCalledTimes(1);
279
+ });
280
+ it('should overwrite local state when incoming commitment matches on-chain commitment', async () => {
281
+ const config = {
282
+ threshold: 1,
283
+ signerCommitments: ['0x' + 'a'.repeat(64)],
284
+ guardianCommitment: '0x' + 'c'.repeat(64),
285
+ };
286
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
287
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
288
+ mockRpcGetAccountDetails.mockResolvedValueOnce({
289
+ commitment: () => ({
290
+ toHex: () => '0x' + 'b'.repeat(64),
291
+ }),
292
+ });
293
+ mockFetch.mockResolvedValueOnce({
294
+ ok: true,
295
+ json: async () => ({
296
+ account_id: multisig.accountId,
297
+ commitment: '0x' + 'b'.repeat(64),
298
+ state_json: { data: 'AQID' },
299
+ created_at: '2024-01-01T00:00:00Z',
300
+ updated_at: '2024-01-02T00:00:00Z',
301
+ }),
302
+ });
303
+ await multisig.syncState();
304
+ expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
305
+ });
306
+ it('refreshes multisig config from synced account state', async () => {
307
+ const config = {
308
+ threshold: 1,
309
+ signerCommitments: ['0x' + 'a'.repeat(64)],
310
+ guardianCommitment: '0x' + 'c'.repeat(64),
311
+ };
312
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
313
+ mockWebClient.getAccount.mockResolvedValueOnce({
314
+ commitment: () => ({
315
+ toHex: () => '0x' + 'b'.repeat(64),
316
+ }),
317
+ });
318
+ mockFetch.mockResolvedValueOnce({
319
+ ok: true,
320
+ json: async () => ({
321
+ account_id: multisig.accountId,
322
+ commitment: '0x' + 'b'.repeat(64),
323
+ state_json: { data: 'AQID' },
324
+ created_at: '2024-01-01T00:00:00Z',
325
+ updated_at: '2024-01-02T00:00:00Z',
326
+ }),
327
+ });
328
+ mockDetectConfig.mockReturnValueOnce({
329
+ threshold: 2,
330
+ numSigners: 2,
331
+ signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
332
+ guardianEnabled: true,
333
+ guardianCommitment: '0x' + 'd'.repeat(64),
334
+ vaultBalances: [],
335
+ procedureThresholds: new Map(),
336
+ });
337
+ await multisig.syncState();
338
+ expect(multisig.threshold).toBe(2);
339
+ expect(multisig.signerCommitments).toEqual([
340
+ '0x' + '1'.repeat(64),
341
+ '0x' + '2'.repeat(64),
342
+ ]);
343
+ expect(multisig.guardianCommitment).toBe('0x' + 'd'.repeat(64));
344
+ expect(mockWebClient.newAccount).not.toHaveBeenCalled();
345
+ });
346
+ it('should overwrite local state when account is not found on-chain', async () => {
347
+ const config = {
348
+ threshold: 1,
349
+ signerCommitments: ['0x' + 'a'.repeat(64)],
350
+ guardianCommitment: '0x' + 'c'.repeat(64),
351
+ };
352
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
353
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
354
+ mockRpcGetAccountDetails.mockRejectedValueOnce(new Error('No account header record found for given ID'));
355
+ mockFetch.mockResolvedValueOnce({
356
+ ok: true,
357
+ json: async () => ({
358
+ account_id: multisig.accountId,
359
+ commitment: '0x' + 'b'.repeat(64),
360
+ state_json: { data: 'AQID' },
361
+ created_at: '2024-01-01T00:00:00Z',
362
+ updated_at: '2024-01-02T00:00:00Z',
363
+ }),
364
+ });
365
+ await multisig.syncState();
366
+ expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
367
+ });
368
+ it('should throw when incoming commitment does not match on-chain commitment', async () => {
369
+ const config = {
370
+ threshold: 1,
371
+ signerCommitments: ['0x' + 'a'.repeat(64)],
372
+ guardianCommitment: '0x' + 'c'.repeat(64),
373
+ };
374
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
375
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
376
+ mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 1));
377
+ mockRpcGetAccountDetails.mockResolvedValueOnce({
378
+ commitment: () => ({
379
+ toHex: () => '0x' + 'c'.repeat(64),
380
+ }),
381
+ });
382
+ mockFetch.mockResolvedValueOnce({
383
+ ok: true,
384
+ json: async () => ({
385
+ account_id: multisig.accountId,
386
+ commitment: '0x' + 'b'.repeat(64),
387
+ state_json: { data: 'AQID' },
388
+ created_at: '2024-01-01T00:00:00Z',
389
+ updated_at: '2024-01-02T00:00:00Z',
390
+ }),
391
+ });
392
+ await expect(multisig.syncState()).rejects.toThrow('Refusing to overwrite local state');
393
+ expect(mockWebClient.newAccount).not.toHaveBeenCalled();
394
+ });
395
+ it('should throw when incoming state nonce is lower than local nonce', async () => {
396
+ const config = {
397
+ threshold: 1,
398
+ signerCommitments: ['0x' + 'a'.repeat(64)],
399
+ guardianCommitment: '0x' + 'c'.repeat(64),
400
+ };
401
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
402
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 3));
403
+ mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
404
+ mockFetch.mockResolvedValueOnce({
405
+ ok: true,
406
+ json: async () => ({
407
+ account_id: multisig.accountId,
408
+ commitment: '0x' + 'b'.repeat(64),
409
+ state_json: { data: 'AQID' },
410
+ created_at: '2024-01-01T00:00:00Z',
411
+ updated_at: '2024-01-02T00:00:00Z',
412
+ }),
413
+ });
414
+ await expect(multisig.syncState()).rejects.toThrow('incoming nonce 2 is not greater than local nonce 3');
415
+ expect(mockWebClient.newAccount).not.toHaveBeenCalled();
416
+ });
417
+ it('should throw when incoming state nonce equals local nonce but commitment differs', async () => {
418
+ const config = {
419
+ threshold: 1,
420
+ signerCommitments: ['0x' + 'a'.repeat(64)],
421
+ guardianCommitment: '0x' + 'c'.repeat(64),
422
+ };
423
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
424
+ mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 2));
425
+ mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
426
+ mockFetch.mockResolvedValueOnce({
427
+ ok: true,
428
+ json: async () => ({
429
+ account_id: multisig.accountId,
430
+ commitment: '0x' + 'b'.repeat(64),
431
+ state_json: { data: 'AQID' },
432
+ created_at: '2024-01-01T00:00:00Z',
433
+ updated_at: '2024-01-02T00:00:00Z',
434
+ }),
435
+ });
436
+ await expect(multisig.syncState()).rejects.toThrow('incoming nonce 2 is not greater than local nonce 2');
437
+ expect(mockWebClient.newAccount).not.toHaveBeenCalled();
438
+ });
439
+ });
440
+ describe('verifyStateCommitment', () => {
441
+ it('should pass when local and on-chain commitments match', async () => {
442
+ const config = {
443
+ threshold: 1,
444
+ signerCommitments: ['0x' + 'a'.repeat(64)],
445
+ guardianCommitment: '0x' + 'c'.repeat(64),
446
+ };
447
+ mockWebClient.getAccount.mockResolvedValueOnce({
448
+ commitment: () => ({
449
+ toHex: () => '0x' + 'b'.repeat(64),
450
+ }),
451
+ });
452
+ const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
453
+ await expect(multisigWithRpc.verifyStateCommitment()).resolves.toMatchObject({
454
+ accountId: multisigWithRpc.accountId,
455
+ });
456
+ });
457
+ it('should throw when local account state is missing', async () => {
458
+ const config = {
459
+ threshold: 1,
460
+ signerCommitments: ['0x' + 'a'.repeat(64)],
461
+ guardianCommitment: '0x' + 'c'.repeat(64),
462
+ };
463
+ mockWebClient.getAccount.mockResolvedValueOnce(null);
464
+ const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
465
+ await expect(multisigWithRpc.verifyStateCommitment()).rejects.toThrow('Local account state not found');
466
+ });
467
+ it('should throw when local and on-chain commitments differ', async () => {
468
+ const config = {
469
+ threshold: 1,
470
+ signerCommitments: ['0x' + 'a'.repeat(64)],
471
+ guardianCommitment: '0x' + 'c'.repeat(64),
472
+ };
473
+ mockWebClient.getAccount.mockResolvedValueOnce({
474
+ commitment: () => ({
475
+ toHex: () => '0x' + 'f'.repeat(64),
476
+ }),
477
+ });
478
+ mockRpcGetAccountDetails.mockResolvedValueOnce({
479
+ commitment: () => ({
480
+ toHex: () => '0x' + 'b'.repeat(64),
481
+ }),
482
+ });
483
+ const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
484
+ await expect(multisigWithRpc.verifyStateCommitment()).rejects.toThrow('Local account commitment does not match on-chain commitment');
485
+ });
486
+ });
487
+ describe('registerOnGuardian', () => {
488
+ it('should register account on GUARDIAN', async () => {
199
489
  const config = {
200
490
  threshold: 1,
201
491
  signerCommitments: ['0x' + 'a'.repeat(64)],
202
- psmCommitment: '0x' + 'c'.repeat(64),
492
+ guardianCommitment: '0x' + 'c'.repeat(64),
203
493
  };
204
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
494
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
205
495
  mockFetch.mockResolvedValueOnce({
206
496
  ok: true,
207
497
  json: async () => ({
@@ -210,24 +500,45 @@ describe('Multisig', () => {
210
500
  ack_pubkey: '0x' + 'd'.repeat(64),
211
501
  }),
212
502
  });
213
- await expect(multisig.registerOnPsm()).resolves.toBeUndefined();
503
+ await expect(multisig.registerOnGuardian()).resolves.toBeUndefined();
214
504
  });
215
- it('should throw when no account and no initial state', async () => {
505
+ it('should register ECDSA accounts with MidenEcdsa auth', async () => {
216
506
  const config = {
217
507
  threshold: 1,
218
508
  signerCommitments: ['0x' + 'a'.repeat(64)],
219
- psmCommitment: '0x' + 'c'.repeat(64),
509
+ guardianCommitment: '0x' + 'c'.repeat(64),
220
510
  };
221
- const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, '0x' + 'e'.repeat(30));
222
- await expect(multisig.registerOnPsm()).rejects.toThrow('Cannot register on PSM');
511
+ const ecdsaSigner = {
512
+ ...mockSigner,
513
+ publicKey: '0x' + '2'.repeat(66),
514
+ scheme: 'ecdsa',
515
+ };
516
+ guardian.setSigner(ecdsaSigner);
517
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
518
+ mockFetch.mockResolvedValueOnce({
519
+ ok: true,
520
+ json: async () => ({
521
+ success: true,
522
+ message: 'Account configured',
523
+ ack_pubkey: '0x' + 'd'.repeat(66),
524
+ }),
525
+ });
526
+ await expect(multisig.registerOnGuardian()).resolves.toBeUndefined();
527
+ const [, requestInit] = mockFetch.mock.calls[0];
528
+ const body = JSON.parse(String(requestInit.body));
529
+ expect(body.auth).toEqual({
530
+ MidenEcdsa: {
531
+ cosigner_commitments: config.signerCommitments,
532
+ },
533
+ });
223
534
  });
224
- it('should accept initial state base64', async () => {
535
+ it('should accept explicit initial state base64', async () => {
225
536
  const config = {
226
537
  threshold: 1,
227
538
  signerCommitments: ['0x' + 'a'.repeat(64)],
228
- psmCommitment: '0x' + 'c'.repeat(64),
539
+ guardianCommitment: '0x' + 'c'.repeat(64),
229
540
  };
230
- const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, '0x' + 'e'.repeat(30));
541
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, '0x' + 'e'.repeat(30));
231
542
  mockFetch.mockResolvedValueOnce({
232
543
  ok: true,
233
544
  json: async () => ({
@@ -235,15 +546,15 @@ describe('Multisig', () => {
235
546
  message: 'Account configured',
236
547
  }),
237
548
  });
238
- await expect(multisig.registerOnPsm('base64initialstate')).resolves.toBeUndefined();
549
+ await expect(multisig.registerOnGuardian('base64initialstate')).resolves.toBeUndefined();
239
550
  });
240
- it('should throw on PSM registration failure', async () => {
551
+ it('should throw on GUARDIAN registration failure', async () => {
241
552
  const config = {
242
553
  threshold: 1,
243
554
  signerCommitments: ['0x' + 'a'.repeat(64)],
244
- psmCommitment: '0x' + 'c'.repeat(64),
555
+ guardianCommitment: '0x' + 'c'.repeat(64),
245
556
  };
246
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
557
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
247
558
  mockFetch.mockResolvedValueOnce({
248
559
  ok: true,
249
560
  json: async () => ({
@@ -251,17 +562,17 @@ describe('Multisig', () => {
251
562
  message: 'Account already exists',
252
563
  }),
253
564
  });
254
- await expect(multisig.registerOnPsm()).rejects.toThrow('Failed to register on PSM');
565
+ await expect(multisig.registerOnGuardian()).rejects.toThrow('Failed to register on GUARDIAN');
255
566
  });
256
567
  });
257
- describe('syncTransactionProposals', () => {
258
- it('should sync proposals from PSM', async () => {
568
+ describe('syncProposals', () => {
569
+ it('should sync proposals from GUARDIAN', async () => {
259
570
  const config = {
260
571
  threshold: 2,
261
572
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
262
- psmCommitment: '0x' + 'c'.repeat(64),
573
+ guardianCommitment: '0x' + 'c'.repeat(64),
263
574
  };
264
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
575
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
265
576
  const mockProposals = [
266
577
  {
267
578
  account_id: '0x' + 'a'.repeat(30),
@@ -283,7 +594,7 @@ describe('Multisig', () => {
283
594
  proposer_id: '0x' + 'c'.repeat(64),
284
595
  cosigner_sigs: [
285
596
  {
286
- signer_id: '0x' + 'd'.repeat(64),
597
+ signer_id: '0x' + 'a'.repeat(64),
287
598
  signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
288
599
  timestamp: '2024-01-01T00:00:00Z',
289
600
  },
@@ -295,19 +606,18 @@ describe('Multisig', () => {
295
606
  ok: true,
296
607
  json: async () => ({ proposals: mockProposals }),
297
608
  });
298
- const proposals = await multisig.syncTransactionProposals();
609
+ const proposals = await multisig.syncProposals();
299
610
  expect(proposals.length).toBe(1);
300
611
  expect(proposals[0].nonce).toBe(1);
301
- expect(proposals[0].status.type).toBe('pending');
302
- expect(proposals[0].commitment).toBe(proposals[0].id);
612
+ expect(proposals[0].status).toBe('pending');
303
613
  });
304
614
  it('should return ready status when enough signatures', async () => {
305
615
  const config = {
306
616
  threshold: 1, // Only 1 signature needed
307
617
  signerCommitments: ['0x' + 'a'.repeat(64)],
308
- psmCommitment: '0x' + 'c'.repeat(64),
618
+ guardianCommitment: '0x' + 'c'.repeat(64),
309
619
  };
310
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
620
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
311
621
  const mockProposals = [
312
622
  {
313
623
  account_id: '0x' + 'a'.repeat(30),
@@ -329,7 +639,7 @@ describe('Multisig', () => {
329
639
  proposer_id: '0x' + 'c'.repeat(64),
330
640
  cosigner_sigs: [
331
641
  {
332
- signer_id: '0x' + 'd'.repeat(64),
642
+ signer_id: '0x' + 'a'.repeat(64),
333
643
  signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
334
644
  timestamp: '2024-01-01T00:00:00Z',
335
645
  },
@@ -341,99 +651,571 @@ describe('Multisig', () => {
341
651
  ok: true,
342
652
  json: async () => ({ proposals: mockProposals }),
343
653
  });
344
- const proposals = await multisig.syncTransactionProposals();
345
- expect(proposals[0].status.type).toBe('ready');
346
- });
347
- });
348
- describe('listTransactionProposals', () => {
349
- it('should return empty list initially', () => {
350
- const config = {
351
- threshold: 1,
352
- signerCommitments: ['0x' + 'a'.repeat(64)],
353
- psmCommitment: '0x' + 'c'.repeat(64),
354
- };
355
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
356
- expect(multisig.listTransactionProposals()).toEqual([]);
654
+ const proposals = await multisig.syncProposals();
655
+ expect(proposals[0].status).toBe('ready');
357
656
  });
358
- });
359
- describe('createProposal', () => {
360
- it('should create a new proposal', async () => {
657
+ it('should reject proposals whose metadata does not match tx_summary', async () => {
361
658
  const config = {
362
659
  threshold: 1,
363
660
  signerCommitments: ['0x' + 'a'.repeat(64)],
364
- psmCommitment: '0x' + 'c'.repeat(64),
365
- };
366
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
367
- const mockDelta = {
368
- account_id: '0x' + 'a'.repeat(30),
369
- nonce: 1,
370
- prev_commitment: '0x' + 'b'.repeat(64),
371
- delta_payload: {
372
- tx_summary: { data: 'base64summary' },
373
- signatures: [],
374
- },
375
- status: {
376
- status: 'pending',
377
- timestamp: '2024-01-01T00:00:00Z',
378
- proposer_id: '0x' + 'c'.repeat(64),
379
- cosigner_sigs: [],
380
- },
661
+ guardianCommitment: '0x' + 'c'.repeat(64),
381
662
  };
663
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
382
664
  mockFetch.mockResolvedValueOnce({
383
665
  ok: true,
384
666
  json: async () => ({
385
- delta: mockDelta,
386
- commitment: '0x' + 'd'.repeat(64),
667
+ proposals: [
668
+ {
669
+ account_id: '0x' + 'a'.repeat(30),
670
+ nonce: 1,
671
+ prev_commitment: '0x' + 'b'.repeat(64),
672
+ delta_payload: {
673
+ tx_summary: { data: 'AQID' },
674
+ signatures: [],
675
+ metadata: {
676
+ proposal_type: 'add_signer',
677
+ target_threshold: 1,
678
+ signer_commitments: ['0x' + 'a'.repeat(64)],
679
+ description: '',
680
+ },
681
+ },
682
+ status: {
683
+ status: 'pending',
684
+ timestamp: '2024-01-01T00:00:00Z',
685
+ proposer_id: '0x' + 'c'.repeat(64),
686
+ cosigner_sigs: [],
687
+ },
688
+ },
689
+ ],
387
690
  }),
388
691
  });
389
- const proposal = await multisig.createProposal(1, 'AQID', {
390
- proposalType: 'add_signer',
391
- targetThreshold: 1,
392
- targetSignerCommitments: ['0x' + 'a'.repeat(64)],
393
- description: '',
692
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
693
+ toCommitment: () => ({
694
+ toHex: () => '0x' + 'f'.repeat(64),
695
+ }),
394
696
  });
395
- expect(proposal.nonce).toBe(1);
396
- expect(proposal.id).toBe('0x' + 'd'.repeat(64));
697
+ await expect(multisig.syncProposals()).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
397
698
  });
398
- });
399
- describe('signTransactionProposal', () => {
400
- it('should sign a proposal', async () => {
699
+ it('should reject non-32-byte signer IDs from GUARDIAN proposals', async () => {
401
700
  const config = {
402
701
  threshold: 1,
403
702
  signerCommitments: ['0x' + 'a'.repeat(64)],
404
- psmCommitment: '0x' + 'c'.repeat(64),
703
+ guardianCommitment: '0x' + 'c'.repeat(64),
405
704
  };
406
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
407
- // First create a proposal
408
- const mockDelta = {
409
- account_id: '0x' + 'a'.repeat(30),
410
- nonce: 1,
411
- prev_commitment: '0x' + 'b'.repeat(64),
412
- delta_payload: {
413
- tx_summary: { data: 'AQID' },
414
- signatures: [],
415
- },
416
- status: {
417
- status: 'pending',
418
- timestamp: '2024-01-01T00:00:00Z',
419
- proposer_id: '0x' + 'c'.repeat(64),
420
- cosigner_sigs: [],
705
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
706
+ const mockProposals = [
707
+ {
708
+ account_id: '0x' + 'a'.repeat(30),
709
+ nonce: 1,
710
+ prev_commitment: '0x' + 'b'.repeat(64),
711
+ delta_payload: {
712
+ tx_summary: { data: 'AQID' },
713
+ signatures: [],
714
+ metadata: {
715
+ proposal_type: 'add_signer',
716
+ target_threshold: 1,
717
+ signer_commitments: ['0x' + 'a'.repeat(64)],
718
+ description: '',
719
+ },
720
+ },
721
+ status: {
722
+ status: 'pending',
723
+ timestamp: '2024-01-01T00:00:00Z',
724
+ proposer_id: '0x' + 'c'.repeat(64),
725
+ cosigner_sigs: [
726
+ {
727
+ signer_id: '0x1',
728
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
729
+ timestamp: '2024-01-01T00:00:00Z',
730
+ },
731
+ ],
732
+ },
421
733
  },
422
- };
734
+ ];
423
735
  mockFetch.mockResolvedValueOnce({
424
736
  ok: true,
425
- json: async () => ({
426
- delta: mockDelta,
427
- commitment: '0x' + 'd'.repeat(64),
428
- }),
737
+ json: async () => ({ proposals: mockProposals }),
429
738
  });
430
- await multisig.createProposal(1, 'AQID', {
431
- proposalType: 'add_signer',
739
+ await expect(multisig.syncProposals()).rejects.toThrow('expected signerId as 32-byte hex');
740
+ });
741
+ it('should reject duplicate normalized signer IDs from GUARDIAN proposals', async () => {
742
+ const config = {
743
+ threshold: 2,
744
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
745
+ guardianCommitment: '0x' + 'c'.repeat(64),
746
+ };
747
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
748
+ const mockProposals = [
749
+ {
750
+ account_id: '0x' + 'a'.repeat(30),
751
+ nonce: 1,
752
+ prev_commitment: '0x' + 'b'.repeat(64),
753
+ delta_payload: {
754
+ tx_summary: { data: 'AQID' },
755
+ signatures: [],
756
+ metadata: {
757
+ proposal_type: 'add_signer',
758
+ target_threshold: 2,
759
+ signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
760
+ description: '',
761
+ },
762
+ },
763
+ status: {
764
+ status: 'pending',
765
+ timestamp: '2024-01-01T00:00:00Z',
766
+ proposer_id: '0x' + 'c'.repeat(64),
767
+ cosigner_sigs: [
768
+ {
769
+ signer_id: '0x' + 'A'.repeat(64),
770
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
771
+ timestamp: '2024-01-01T00:00:00Z',
772
+ },
773
+ {
774
+ signer_id: '0x' + 'a'.repeat(64),
775
+ signature: { scheme: 'falcon', signature: '0x' + 'f'.repeat(128) },
776
+ timestamp: '2024-01-01T00:00:01Z',
777
+ },
778
+ ],
779
+ },
780
+ },
781
+ ];
782
+ mockFetch.mockResolvedValueOnce({
783
+ ok: true,
784
+ json: async () => ({ proposals: mockProposals }),
785
+ });
786
+ await expect(multisig.syncProposals()).rejects.toThrow('duplicate signatures for signer');
787
+ });
788
+ });
789
+ describe('listProposals', () => {
790
+ it('should return empty list initially', () => {
791
+ const config = {
792
+ threshold: 1,
793
+ signerCommitments: ['0x' + 'a'.repeat(64)],
794
+ guardianCommitment: '0x' + 'c'.repeat(64),
795
+ };
796
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
797
+ expect(multisig.listProposals()).toEqual([]);
798
+ });
799
+ });
800
+ describe('createProposal', () => {
801
+ it('should create a new proposal', async () => {
802
+ const config = {
803
+ threshold: 1,
804
+ signerCommitments: ['0x' + 'a'.repeat(64)],
805
+ guardianCommitment: '0x' + 'c'.repeat(64),
806
+ };
807
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
808
+ const mockDelta = {
809
+ account_id: '0x' + 'a'.repeat(30),
810
+ nonce: 1,
811
+ prev_commitment: '0x' + 'b'.repeat(64),
812
+ delta_payload: {
813
+ tx_summary: { data: 'AQID' },
814
+ signatures: [],
815
+ },
816
+ status: {
817
+ status: 'pending',
818
+ timestamp: '2024-01-01T00:00:00Z',
819
+ proposer_id: '0x' + 'c'.repeat(64),
820
+ cosigner_sigs: [],
821
+ },
822
+ };
823
+ mockFetch.mockResolvedValueOnce({
824
+ ok: true,
825
+ json: async () => ({
826
+ delta: mockDelta,
827
+ commitment: '0x' + 'c'.repeat(64),
828
+ }),
829
+ });
830
+ const proposal = await multisig.createProposal(1, 'AQID', {
831
+ proposalType: 'add_signer',
832
+ targetThreshold: 1,
833
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
834
+ description: '',
835
+ });
836
+ expect(proposal.nonce).toBe(1);
837
+ expect(proposal.id).toBe('0x' + 'c'.repeat(64));
838
+ });
839
+ it('should reject a mismatched returned commitment', async () => {
840
+ const config = {
841
+ threshold: 1,
842
+ signerCommitments: ['0x' + 'a'.repeat(64)],
843
+ guardianCommitment: '0x' + 'c'.repeat(64),
844
+ };
845
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
846
+ const mockDelta = {
847
+ account_id: '0x' + 'a'.repeat(30),
848
+ nonce: 1,
849
+ prev_commitment: '0x' + 'b'.repeat(64),
850
+ delta_payload: {
851
+ tx_summary: { data: 'AQID' },
852
+ signatures: [],
853
+ },
854
+ status: {
855
+ status: 'pending',
856
+ timestamp: '2024-01-01T00:00:00Z',
857
+ proposer_id: '0x' + 'c'.repeat(64),
858
+ cosigner_sigs: [],
859
+ },
860
+ };
861
+ mockFetch.mockResolvedValueOnce({
862
+ ok: true,
863
+ json: async () => ({
864
+ delta: mockDelta,
865
+ commitment: '0x' + 'd'.repeat(64),
866
+ }),
867
+ });
868
+ await expect(multisig.createProposal(1, 'AQID', {
869
+ proposalType: 'add_signer',
870
+ targetThreshold: 1,
871
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
872
+ description: '',
873
+ })).rejects.toThrow('Invalid proposal: commitment 0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd does not match tx_summary 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc');
874
+ });
875
+ it('should reject a response whose tx_summary does not match the provided metadata', async () => {
876
+ const config = {
877
+ threshold: 1,
878
+ signerCommitments: ['0x' + 'a'.repeat(64)],
879
+ guardianCommitment: '0x' + 'c'.repeat(64),
880
+ };
881
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
882
+ const mockDelta = {
883
+ account_id: '0x' + 'a'.repeat(30),
884
+ nonce: 1,
885
+ prev_commitment: '0x' + 'b'.repeat(64),
886
+ delta_payload: {
887
+ tx_summary: { data: 'AQID' },
888
+ signatures: [],
889
+ },
890
+ status: {
891
+ status: 'pending',
892
+ timestamp: '2024-01-01T00:00:00Z',
893
+ proposer_id: '0x' + 'c'.repeat(64),
894
+ cosigner_sigs: [],
895
+ },
896
+ };
897
+ mockFetch.mockResolvedValueOnce({
898
+ ok: true,
899
+ json: async () => ({
900
+ delta: mockDelta,
901
+ commitment: '0x' + 'c'.repeat(64),
902
+ }),
903
+ });
904
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
905
+ toCommitment: () => ({
906
+ toHex: () => '0x' + 'f'.repeat(64),
907
+ }),
908
+ });
909
+ await expect(multisig.createProposal(1, 'AQID', {
910
+ proposalType: 'add_signer',
911
+ targetThreshold: 1,
912
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
913
+ description: '',
914
+ })).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
915
+ });
916
+ });
917
+ describe('createP2idProposal', () => {
918
+ it('should include the faucet asset in the proposal description', async () => {
919
+ const { executeForSummary } = await import('./transaction.js');
920
+ vi.mocked(executeForSummary).mockResolvedValue({
921
+ toCommitment: () => ({
922
+ toHex: () => '0x' + 'c'.repeat(64),
923
+ }),
924
+ serialize: () => new Uint8Array([1, 2, 3]),
925
+ });
926
+ const config = {
927
+ threshold: 1,
928
+ signerCommitments: ['0x' + 'a'.repeat(64)],
929
+ guardianCommitment: '0x' + 'c'.repeat(64),
930
+ };
931
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
932
+ const mockDelta = {
933
+ account_id: '0x' + 'a'.repeat(30),
934
+ nonce: 1,
935
+ prev_commitment: '0x' + 'b'.repeat(64),
936
+ delta_payload: {
937
+ tx_summary: { data: 'AQID' },
938
+ signatures: [],
939
+ metadata: {
940
+ proposal_type: 'p2id',
941
+ recipient_id: '0xrecipient',
942
+ faucet_id: '0xfaucet',
943
+ amount: '100',
944
+ description: '',
945
+ },
946
+ },
947
+ status: {
948
+ status: 'pending',
949
+ timestamp: '2024-01-01T00:00:00Z',
950
+ proposer_id: '0x' + 'c'.repeat(64),
951
+ cosigner_sigs: [],
952
+ },
953
+ };
954
+ mockFetch.mockResolvedValueOnce({
955
+ ok: true,
956
+ json: async () => ({
957
+ delta: mockDelta,
958
+ commitment: '0x' + 'c'.repeat(64),
959
+ }),
960
+ });
961
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1);
962
+ expect(proposal.metadata.description).toBe('Send 100 of asset 0xfaucet... to 0xrecipien...');
963
+ });
964
+ });
965
+ describe('createChangeThresholdProposal', () => {
966
+ it('passes the signer scheme to update-signers requests', async () => {
967
+ vi.mocked(executeForSummary).mockResolvedValue({
968
+ toCommitment: () => ({
969
+ toHex: () => '0x' + 'c'.repeat(64),
970
+ }),
971
+ serialize: () => new Uint8Array([1, 2, 3]),
972
+ });
973
+ const ecdsaSigner = {
974
+ ...mockSigner,
975
+ publicKey: '0x' + '2'.repeat(66),
976
+ scheme: 'ecdsa',
977
+ };
978
+ guardian.setSigner(ecdsaSigner);
979
+ const config = {
980
+ threshold: 1,
981
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
982
+ guardianCommitment: '0x' + 'c'.repeat(64),
983
+ };
984
+ const mockDelta = {
985
+ account_id: '0x' + 'a'.repeat(30),
986
+ nonce: 1,
987
+ prev_commitment: '0x' + 'b'.repeat(64),
988
+ delta_payload: {
989
+ tx_summary: { data: 'AQID' },
990
+ signatures: [],
991
+ metadata: {
992
+ proposal_type: 'change_threshold',
993
+ target_threshold: 2,
994
+ description: '',
995
+ },
996
+ },
997
+ status: {
998
+ status: 'pending',
999
+ timestamp: '2024-01-01T00:00:00Z',
1000
+ proposer_id: '0x' + 'c'.repeat(64),
1001
+ cosigner_sigs: [],
1002
+ },
1003
+ };
1004
+ mockFetch.mockResolvedValueOnce({
1005
+ ok: true,
1006
+ json: async () => ({
1007
+ delta: mockDelta,
1008
+ commitment: '0x' + 'c'.repeat(64),
1009
+ }),
1010
+ });
1011
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1012
+ await multisig.createChangeThresholdProposal(2, 1);
1013
+ expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 2, config.signerCommitments, { signatureScheme: 'ecdsa' });
1014
+ });
1015
+ });
1016
+ describe('createSwitchGuardianProposal', () => {
1017
+ it('should verify new endpoint commitment before creating proposal', async () => {
1018
+ vi.mocked(executeForSummary).mockResolvedValue({
1019
+ serialize: () => new Uint8Array([1, 2, 3]),
1020
+ });
1021
+ const config = {
1022
+ threshold: 1,
1023
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1024
+ guardianCommitment: '0x' + 'c'.repeat(64),
1025
+ };
1026
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1027
+ const newGuardianPubkey = '0x' + '1'.repeat(64);
1028
+ mockFetch.mockResolvedValueOnce({
1029
+ ok: true,
1030
+ json: async () => ({ commitment: newGuardianPubkey }),
1031
+ });
1032
+ const proposal = await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianPubkey);
1033
+ expect(proposal.metadata?.proposalType).toBe('switch_guardian');
1034
+ if (proposal.metadata?.proposalType === 'switch_guardian') {
1035
+ expect(proposal.metadata.newGuardianEndpoint).toBe('http://new-guardian.com');
1036
+ }
1037
+ expect(mockFetch).toHaveBeenCalledWith('http://new-guardian.com/pubkey?scheme=falcon', expect.objectContaining({ method: 'GET' }));
1038
+ });
1039
+ it('should reject switch proposal when endpoint commitment does not match', async () => {
1040
+ vi.mocked(executeForSummary).mockResolvedValue({
1041
+ serialize: () => new Uint8Array([1, 2, 3]),
1042
+ });
1043
+ const config = {
1044
+ threshold: 1,
1045
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1046
+ guardianCommitment: '0x' + 'c'.repeat(64),
1047
+ };
1048
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1049
+ mockFetch.mockResolvedValueOnce({
1050
+ ok: true,
1051
+ json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
1052
+ });
1053
+ await expect(multisig.createSwitchGuardianProposal('http://new-guardian.com', '0x' + '1'.repeat(64))).rejects.toThrow('Refusing to use GUARDIAN endpoint');
1054
+ });
1055
+ it('should use the signer scheme when resolving new GUARDIAN commitments', async () => {
1056
+ vi.mocked(executeForSummary).mockResolvedValue({
1057
+ serialize: () => new Uint8Array([1, 2, 3]),
1058
+ });
1059
+ const ecdsaSigner = {
1060
+ ...mockSigner,
1061
+ publicKey: '0x' + '2'.repeat(66),
1062
+ scheme: 'ecdsa',
1063
+ };
1064
+ guardian.setSigner(ecdsaSigner);
1065
+ const config = {
1066
+ threshold: 1,
1067
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1068
+ guardianCommitment: '0x' + 'c'.repeat(64),
1069
+ };
1070
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1071
+ const newGuardianCommitment = '0x' + '1'.repeat(64);
1072
+ mockFetch.mockResolvedValueOnce({
1073
+ ok: true,
1074
+ json: async () => ({ commitment: newGuardianCommitment }),
1075
+ });
1076
+ await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianCommitment);
1077
+ expect(mockFetch).toHaveBeenCalledWith('http://new-guardian.com/pubkey?scheme=ecdsa', expect.objectContaining({ method: 'GET' }));
1078
+ expect(buildUpdateGuardianTransactionRequest).toHaveBeenCalledWith(mockWebClient, newGuardianCommitment, { signatureScheme: 'ecdsa' });
1079
+ });
1080
+ });
1081
+ describe('createUpdateProcedureThresholdProposal', () => {
1082
+ it('should create procedure-threshold update proposals', async () => {
1083
+ vi.mocked(executeForSummary).mockResolvedValue({
1084
+ toCommitment: () => ({
1085
+ toHex: () => '0x' + 'c'.repeat(64),
1086
+ }),
1087
+ serialize: () => new Uint8Array([1, 2, 3]),
1088
+ });
1089
+ const config = {
1090
+ threshold: 2,
1091
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1092
+ guardianCommitment: '0x' + 'c'.repeat(64),
1093
+ };
1094
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1095
+ const mockDelta = {
1096
+ account_id: '0x' + 'a'.repeat(30),
1097
+ nonce: 1,
1098
+ prev_commitment: '0x' + 'b'.repeat(64),
1099
+ delta_payload: {
1100
+ tx_summary: { data: 'AQID' },
1101
+ signatures: [],
1102
+ metadata: {
1103
+ proposal_type: 'update_procedure_threshold',
1104
+ target_threshold: 1,
1105
+ target_procedure: 'send_asset',
1106
+ description: '',
1107
+ },
1108
+ },
1109
+ status: {
1110
+ status: 'pending',
1111
+ timestamp: '2024-01-01T00:00:00Z',
1112
+ proposer_id: '0x' + 'c'.repeat(64),
1113
+ cosigner_sigs: [],
1114
+ },
1115
+ };
1116
+ mockFetch.mockResolvedValueOnce({
1117
+ ok: true,
1118
+ json: async () => ({
1119
+ delta: mockDelta,
1120
+ commitment: '0x' + 'c'.repeat(64),
1121
+ }),
1122
+ });
1123
+ const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
1124
+ expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'falcon' });
1125
+ expect(proposal.metadata.proposalType).toBe('update_procedure_threshold');
1126
+ if (proposal.metadata.proposalType === 'update_procedure_threshold') {
1127
+ expect(proposal.metadata.targetProcedure).toBe('send_asset');
1128
+ expect(proposal.metadata.targetThreshold).toBe(1);
1129
+ }
1130
+ });
1131
+ it('passes the signer scheme to ECDSA procedure-threshold updates', async () => {
1132
+ vi.mocked(executeForSummary).mockResolvedValue({
1133
+ toCommitment: () => ({
1134
+ toHex: () => '0x' + 'c'.repeat(64),
1135
+ }),
1136
+ serialize: () => new Uint8Array([1, 2, 3]),
1137
+ });
1138
+ const ecdsaSigner = {
1139
+ ...mockSigner,
1140
+ publicKey: '0x' + '2'.repeat(66),
1141
+ scheme: 'ecdsa',
1142
+ };
1143
+ guardian.setSigner(ecdsaSigner);
1144
+ const config = {
1145
+ threshold: 2,
1146
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1147
+ guardianCommitment: '0x' + 'c'.repeat(64),
1148
+ };
1149
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1150
+ const mockDelta = {
1151
+ account_id: '0x' + 'a'.repeat(30),
1152
+ nonce: 1,
1153
+ prev_commitment: '0x' + 'b'.repeat(64),
1154
+ delta_payload: {
1155
+ tx_summary: { data: 'AQID' },
1156
+ signatures: [],
1157
+ metadata: {
1158
+ proposal_type: 'update_procedure_threshold',
1159
+ target_threshold: 1,
1160
+ target_procedure: 'send_asset',
1161
+ description: '',
1162
+ },
1163
+ },
1164
+ status: {
1165
+ status: 'pending',
1166
+ timestamp: '2024-01-01T00:00:00Z',
1167
+ proposer_id: '0x' + 'c'.repeat(64),
1168
+ cosigner_sigs: [],
1169
+ },
1170
+ };
1171
+ mockFetch.mockResolvedValueOnce({
1172
+ ok: true,
1173
+ json: async () => ({
1174
+ delta: mockDelta,
1175
+ commitment: '0x' + 'c'.repeat(64),
1176
+ }),
1177
+ });
1178
+ await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
1179
+ expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'ecdsa' });
1180
+ });
1181
+ });
1182
+ describe('signProposal', () => {
1183
+ it('should sign a proposal', async () => {
1184
+ const config = {
1185
+ threshold: 1,
1186
+ signerCommitments: [mockSigner.commitment],
1187
+ guardianCommitment: '0x' + 'c'.repeat(64),
1188
+ };
1189
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1190
+ // First create a proposal
1191
+ const mockDelta = {
1192
+ account_id: '0x' + 'a'.repeat(30),
1193
+ nonce: 1,
1194
+ prev_commitment: '0x' + 'b'.repeat(64),
1195
+ delta_payload: {
1196
+ tx_summary: { data: 'AQID' },
1197
+ signatures: [],
1198
+ },
1199
+ status: {
1200
+ status: 'pending',
1201
+ timestamp: '2024-01-01T00:00:00Z',
1202
+ proposer_id: '0x' + 'c'.repeat(64),
1203
+ cosigner_sigs: [],
1204
+ },
1205
+ };
1206
+ mockFetch.mockResolvedValueOnce({
1207
+ ok: true,
1208
+ json: async () => ({
1209
+ delta: mockDelta,
1210
+ commitment: '0x' + 'c'.repeat(64),
1211
+ }),
1212
+ });
1213
+ await multisig.createProposal(1, 'AQID', {
1214
+ proposalType: 'add_signer',
432
1215
  targetThreshold: 1,
433
1216
  targetSignerCommitments: ['0x' + 'a'.repeat(64)],
434
1217
  description: '',
435
1218
  });
436
- // Now sign it
437
1219
  const signedDelta = {
438
1220
  ...mockDelta,
439
1221
  status: {
@@ -458,33 +1240,173 @@ describe('Multisig', () => {
458
1240
  },
459
1241
  },
460
1242
  };
461
- // Mock for signDeltaProposal
462
- mockFetch.mockResolvedValueOnce({
463
- ok: true,
464
- json: async () => signedDelta,
1243
+ mockFetch.mockResolvedValueOnce({
1244
+ ok: true,
1245
+ json: async () => signedDelta,
1246
+ });
1247
+ const proposalId = '0x' + 'c'.repeat(64);
1248
+ const signedProposal = await multisig.signProposal(proposalId);
1249
+ expect(mockSigner.signCommitment).toHaveBeenCalledWith(proposalId);
1250
+ expect(signedProposal.signatures.length).toBe(1);
1251
+ });
1252
+ it('should reject signing when metadata does not match tx_summary', async () => {
1253
+ const config = {
1254
+ threshold: 1,
1255
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1256
+ guardianCommitment: '0x' + 'c'.repeat(64),
1257
+ };
1258
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1259
+ const mockDelta = {
1260
+ account_id: '0x' + 'a'.repeat(30),
1261
+ nonce: 1,
1262
+ prev_commitment: '0x' + 'b'.repeat(64),
1263
+ delta_payload: {
1264
+ tx_summary: { data: 'AQID' },
1265
+ signatures: [],
1266
+ },
1267
+ status: {
1268
+ status: 'pending',
1269
+ timestamp: '2024-01-01T00:00:00Z',
1270
+ proposer_id: '0x' + 'c'.repeat(64),
1271
+ cosigner_sigs: [],
1272
+ },
1273
+ };
1274
+ mockFetch.mockResolvedValueOnce({
1275
+ ok: true,
1276
+ json: async () => ({
1277
+ delta: mockDelta,
1278
+ commitment: '0x' + 'c'.repeat(64),
1279
+ }),
1280
+ });
1281
+ await multisig.createProposal(1, 'AQID', {
1282
+ proposalType: 'add_signer',
1283
+ targetThreshold: 1,
1284
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1285
+ description: '',
1286
+ });
1287
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
1288
+ toCommitment: () => ({
1289
+ toHex: () => '0x' + 'f'.repeat(64),
1290
+ }),
1291
+ });
1292
+ await expect(multisig.signProposal('0x' + 'c'.repeat(64))).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
1293
+ });
1294
+ it('should reject proposals for a different account before signing', async () => {
1295
+ const config = {
1296
+ threshold: 1,
1297
+ signerCommitments: [mockSigner.commitment],
1298
+ guardianCommitment: '0x' + 'c'.repeat(64),
1299
+ };
1300
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1301
+ const proposalId = '0x' + 'd'.repeat(64);
1302
+ mockFetch.mockResolvedValueOnce({
1303
+ ok: true,
1304
+ json: async () => ({
1305
+ proposals: [
1306
+ {
1307
+ account_id: '0x' + 'f'.repeat(30),
1308
+ nonce: 1,
1309
+ prev_commitment: '0x' + 'b'.repeat(64),
1310
+ delta_payload: {
1311
+ tx_summary: { data: 'AQID' },
1312
+ signatures: [],
1313
+ metadata: {
1314
+ proposal_type: 'add_signer',
1315
+ description: '',
1316
+ target_threshold: 1,
1317
+ signer_commitments: [mockSigner.commitment],
1318
+ },
1319
+ },
1320
+ status: {
1321
+ status: 'pending',
1322
+ timestamp: '2024-01-01T00:00:00Z',
1323
+ proposer_id: '0x' + 'c'.repeat(64),
1324
+ cosigner_sigs: [],
1325
+ },
1326
+ },
1327
+ ],
1328
+ }),
1329
+ });
1330
+ await expect(multisig.signProposal(proposalId)).rejects.toThrow('Proposal is for a different account: 0x' + 'f'.repeat(30));
1331
+ expect(mockSigner.signCommitment).not.toHaveBeenCalled();
1332
+ });
1333
+ });
1334
+ describe('importProposal', () => {
1335
+ it('should reject imported proposals whose metadata does not match tx_summary', async () => {
1336
+ const config = {
1337
+ threshold: 1,
1338
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1339
+ guardianCommitment: '0x' + 'c'.repeat(64),
1340
+ };
1341
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1342
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
1343
+ toCommitment: () => ({
1344
+ toHex: () => '0x' + 'f'.repeat(64),
1345
+ }),
1346
+ });
1347
+ await expect(multisig.importProposal(JSON.stringify({
1348
+ accountId: '0x' + 'a'.repeat(30),
1349
+ nonce: 1,
1350
+ commitment: '0x' + 'c'.repeat(64),
1351
+ txSummaryBase64: 'AQID',
1352
+ signatures: [],
1353
+ metadata: {
1354
+ proposalType: 'add_signer',
1355
+ targetThreshold: 1,
1356
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1357
+ description: '',
1358
+ },
1359
+ }))).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
1360
+ });
1361
+ });
1362
+ describe('signProposalOffline', () => {
1363
+ it('should reject signing imported proposals whose metadata does not match tx_summary', async () => {
1364
+ const config = {
1365
+ threshold: 1,
1366
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1367
+ guardianCommitment: '0x' + 'c'.repeat(64),
1368
+ };
1369
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1370
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
1371
+ toCommitment: () => ({
1372
+ toHex: () => '0x' + 'c'.repeat(64),
1373
+ }),
465
1374
  });
466
- // Mock for auto-sync (syncTransactionProposals)
467
- mockFetch.mockResolvedValueOnce({
468
- ok: true,
469
- json: async () => ({ proposals: [signedDelta] }),
1375
+ const proposal = await multisig.importProposal(JSON.stringify({
1376
+ accountId: '0x' + 'a'.repeat(30),
1377
+ nonce: 1,
1378
+ commitment: '0x' + 'c'.repeat(64),
1379
+ txSummaryBase64: 'AQID',
1380
+ signatures: [],
1381
+ metadata: {
1382
+ proposalType: 'add_signer',
1383
+ targetThreshold: 1,
1384
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1385
+ description: '',
1386
+ },
1387
+ }));
1388
+ proposal.metadata = {
1389
+ proposalType: 'add_signer',
1390
+ targetThreshold: 2,
1391
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1392
+ description: '',
1393
+ };
1394
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
1395
+ toCommitment: () => ({
1396
+ toHex: () => '0x' + 'f'.repeat(64),
1397
+ }),
470
1398
  });
471
- const proposalId = '0x' + 'd'.repeat(64);
472
- const proposals = await multisig.signTransactionProposal(proposalId);
473
- expect(mockSigner.signCommitment).toHaveBeenCalledWith(proposalId);
474
- expect(proposals.length).toBeGreaterThanOrEqual(1);
475
- expect(proposals[0].signatures.length).toBe(1);
476
- expect(proposals[0].commitment).toBe(proposals[0].id);
1399
+ await expect(multisig.signProposalOffline(proposal.id)).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
477
1400
  });
478
1401
  });
479
- describe('exportTransactionProposalToJson', () => {
480
- it('should export proposal to JSON from local cache', async () => {
1402
+ describe('exportProposal', () => {
1403
+ it('should export proposal for offline signing', async () => {
481
1404
  const config = {
482
1405
  threshold: 2,
483
1406
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
484
- psmCommitment: '0x' + 'c'.repeat(64),
1407
+ guardianCommitment: '0x' + 'c'.repeat(64),
485
1408
  };
486
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
487
- // First sync proposals to populate local cache
1409
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
488
1410
  const mockProposals = [
489
1411
  {
490
1412
  account_id: '0x' + 'a'.repeat(30),
@@ -506,7 +1428,7 @@ describe('Multisig', () => {
506
1428
  proposer_id: '0x' + 'c'.repeat(64),
507
1429
  cosigner_sigs: [
508
1430
  {
509
- signer_id: '0x' + 'd'.repeat(64),
1431
+ signer_id: '0x' + 'a'.repeat(64),
510
1432
  signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
511
1433
  timestamp: '2024-01-01T00:00:00Z',
512
1434
  },
@@ -516,45 +1438,223 @@ describe('Multisig', () => {
516
1438
  ];
517
1439
  mockFetch.mockResolvedValueOnce({
518
1440
  ok: true,
519
- json: async () => ({ proposals: mockProposals }),
1441
+ json: async () => mockProposals[0],
520
1442
  });
521
- await multisig.syncTransactionProposals();
522
1443
  // The proposal ID is computed from tx_summary, which is mocked to return 'c'.repeat(64)
523
- const json = multisig.exportTransactionProposalToJson('0x' + 'c'.repeat(64));
524
- const exported = JSON.parse(json);
1444
+ const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
525
1445
  expect(exported.accountId).toBe('0x' + 'a'.repeat(30));
526
1446
  expect(exported.nonce).toBe(1);
527
1447
  expect(exported.txSummaryBase64).toBe('AQID');
528
1448
  expect(exported.signatures.length).toBe(1);
529
- expect(exported.commitment).toBe('0x' + 'c'.repeat(64));
530
1449
  });
531
- it('should throw if proposal not found in cache', () => {
1450
+ it('should preserve ECDSA signature metadata in exported proposals', async () => {
1451
+ const config = {
1452
+ threshold: 2,
1453
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1454
+ guardianCommitment: '0x' + 'c'.repeat(64),
1455
+ };
1456
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1457
+ const publicKey = '0x' + 'd'.repeat(66);
1458
+ mockFetch.mockResolvedValueOnce({
1459
+ ok: true,
1460
+ json: async () => ({
1461
+ account_id: '0x' + 'a'.repeat(30),
1462
+ nonce: 1,
1463
+ prev_commitment: '0x' + 'b'.repeat(64),
1464
+ delta_payload: {
1465
+ tx_summary: { data: 'AQID' },
1466
+ signatures: [],
1467
+ metadata: {
1468
+ proposal_type: 'change_threshold',
1469
+ description: '',
1470
+ target_threshold: 2,
1471
+ signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1472
+ },
1473
+ },
1474
+ status: {
1475
+ status: 'pending',
1476
+ timestamp: '2024-01-01T00:00:00Z',
1477
+ proposer_id: '0x' + 'c'.repeat(64),
1478
+ cosigner_sigs: [
1479
+ {
1480
+ signer_id: '0x' + 'a'.repeat(64),
1481
+ signature: {
1482
+ scheme: 'ecdsa',
1483
+ signature: '0x' + 'e'.repeat(130),
1484
+ public_key: publicKey,
1485
+ },
1486
+ timestamp: '2024-01-01T00:00:00Z',
1487
+ },
1488
+ ],
1489
+ },
1490
+ }),
1491
+ });
1492
+ const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
1493
+ expect(exported.signatures).toEqual([
1494
+ {
1495
+ commitment: '0x' + 'a'.repeat(64),
1496
+ signatureHex: '0x' + 'e'.repeat(130),
1497
+ scheme: 'ecdsa',
1498
+ publicKey,
1499
+ timestamp: '2024-01-01T00:00:00Z',
1500
+ },
1501
+ ]);
1502
+ });
1503
+ it('should throw if proposal not found', async () => {
1504
+ const config = {
1505
+ threshold: 1,
1506
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1507
+ guardianCommitment: '0x' + 'c'.repeat(64),
1508
+ };
1509
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1510
+ mockFetch.mockResolvedValueOnce({
1511
+ ok: false,
1512
+ status: 404,
1513
+ statusText: 'Not Found',
1514
+ text: async () => 'Proposal not found',
1515
+ });
1516
+ await expect(multisig.exportProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
1517
+ });
1518
+ });
1519
+ describe('importProposal', () => {
1520
+ it('should reject imported signatures with non-32-byte signer IDs', async () => {
1521
+ const config = {
1522
+ threshold: 1,
1523
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1524
+ guardianCommitment: '0x' + 'c'.repeat(64),
1525
+ };
1526
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1527
+ const exported = {
1528
+ accountId: multisig.accountId,
1529
+ nonce: 1,
1530
+ commitment: '0x' + 'c'.repeat(64),
1531
+ txSummaryBase64: 'AQID',
1532
+ signatures: [
1533
+ {
1534
+ commitment: '0x1',
1535
+ signatureHex: '0x' + 'b'.repeat(128),
1536
+ },
1537
+ ],
1538
+ metadata: {
1539
+ proposalType: 'add_signer',
1540
+ targetThreshold: 1,
1541
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1542
+ description: '',
1543
+ },
1544
+ };
1545
+ await expect(multisig.importProposal(JSON.stringify(exported))).rejects.toThrow('expected signerId as 32-byte hex');
1546
+ });
1547
+ it('should preserve ECDSA imported signature metadata', async () => {
1548
+ const config = {
1549
+ threshold: 1,
1550
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1551
+ guardianCommitment: '0x' + 'c'.repeat(64),
1552
+ };
1553
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1554
+ const publicKey = '0x' + 'd'.repeat(66);
1555
+ const proposal = await multisig.importProposal(JSON.stringify({
1556
+ accountId: multisig.accountId,
1557
+ nonce: 1,
1558
+ commitment: '0x' + 'c'.repeat(64),
1559
+ txSummaryBase64: 'AQID',
1560
+ signatures: [
1561
+ {
1562
+ commitment: '0x' + 'a'.repeat(64),
1563
+ signatureHex: '0x' + 'b'.repeat(130),
1564
+ scheme: 'ecdsa',
1565
+ publicKey,
1566
+ timestamp: '2024-01-01T00:00:00Z',
1567
+ },
1568
+ ],
1569
+ metadata: {
1570
+ proposalType: 'change_threshold',
1571
+ targetThreshold: 1,
1572
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1573
+ description: '',
1574
+ },
1575
+ }));
1576
+ expect(proposal.signatures).toEqual([
1577
+ {
1578
+ signerId: '0x' + 'a'.repeat(64),
1579
+ signature: {
1580
+ scheme: 'ecdsa',
1581
+ signature: '0x' + 'b'.repeat(130),
1582
+ publicKey,
1583
+ },
1584
+ timestamp: '2024-01-01T00:00:00Z',
1585
+ },
1586
+ ]);
1587
+ });
1588
+ it('should reject imported ECDSA signatures without a public key', async () => {
532
1589
  const config = {
533
1590
  threshold: 1,
534
1591
  signerCommitments: ['0x' + 'a'.repeat(64)],
535
- psmCommitment: '0x' + 'c'.repeat(64),
1592
+ guardianCommitment: '0x' + 'c'.repeat(64),
1593
+ };
1594
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1595
+ await expect(multisig.importProposal(JSON.stringify({
1596
+ accountId: multisig.accountId,
1597
+ nonce: 1,
1598
+ commitment: '0x' + 'c'.repeat(64),
1599
+ txSummaryBase64: 'AQID',
1600
+ signatures: [
1601
+ {
1602
+ commitment: '0x' + 'a'.repeat(64),
1603
+ signatureHex: '0x' + 'b'.repeat(130),
1604
+ scheme: 'ecdsa',
1605
+ },
1606
+ ],
1607
+ metadata: {
1608
+ proposalType: 'change_threshold',
1609
+ targetThreshold: 1,
1610
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1611
+ description: '',
1612
+ },
1613
+ }))).rejects.toThrow('ECDSA signature for 0x' + 'a'.repeat(64) + ' is missing publicKey');
1614
+ });
1615
+ it('should reject offline signing if an imported proposal account is changed', async () => {
1616
+ const config = {
1617
+ threshold: 2,
1618
+ signerCommitments: ['0x' + 'a'.repeat(64), mockSigner.commitment],
1619
+ guardianCommitment: '0x' + 'c'.repeat(64),
536
1620
  };
537
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
538
- expect(() => multisig.exportTransactionProposalToJson('0x' + 'nonexistent'.repeat(5))).toThrow('Proposal not found');
1621
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1622
+ const exported = {
1623
+ accountId: multisig.accountId,
1624
+ nonce: 1,
1625
+ commitment: '0x' + 'c'.repeat(64),
1626
+ txSummaryBase64: 'AQID',
1627
+ signatures: [],
1628
+ metadata: {
1629
+ proposalType: 'add_signer',
1630
+ targetThreshold: 2,
1631
+ targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1632
+ description: '',
1633
+ },
1634
+ };
1635
+ const proposal = await multisig.importProposal(JSON.stringify(exported));
1636
+ proposal.accountId = '0x' + 'f'.repeat(30);
1637
+ await expect(multisig.signProposalOffline(proposal.id)).rejects.toThrow('Proposal is for a different account: 0x' + 'f'.repeat(30));
1638
+ expect(mockSigner.signCommitment).not.toHaveBeenCalled();
539
1639
  });
540
1640
  });
541
- describe('executeTransactionProposal', () => {
1641
+ describe('executeProposal', () => {
542
1642
  it('should throw if proposal not found locally', async () => {
543
1643
  const config = {
544
1644
  threshold: 1,
545
1645
  signerCommitments: ['0x' + 'a'.repeat(64)],
546
- psmCommitment: '0x' + 'c'.repeat(64),
1646
+ guardianCommitment: '0x' + 'c'.repeat(64),
547
1647
  };
548
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
549
- await expect(multisig.executeTransactionProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
1648
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1649
+ await expect(multisig.executeProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
550
1650
  });
551
1651
  it('should throw if proposal is still pending', async () => {
552
1652
  const config = {
553
1653
  threshold: 2, // Need 2 signatures
554
1654
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
555
- psmCommitment: '0x' + 'c'.repeat(64),
1655
+ guardianCommitment: '0x' + 'c'.repeat(64),
556
1656
  };
557
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
1657
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
558
1658
  // Sync with pending proposal (only 1 signature)
559
1659
  const mockProposals = [
560
1660
  {
@@ -577,7 +1677,7 @@ describe('Multisig', () => {
577
1677
  proposer_id: '0x' + 'c'.repeat(64),
578
1678
  cosigner_sigs: [
579
1679
  {
580
- signer_id: '0x' + 'd'.repeat(64),
1680
+ signer_id: '0x' + 'a'.repeat(64),
581
1681
  signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
582
1682
  timestamp: '2024-01-01T00:00:00Z',
583
1683
  },
@@ -589,17 +1689,17 @@ describe('Multisig', () => {
589
1689
  ok: true,
590
1690
  json: async () => ({ proposals: mockProposals }),
591
1691
  });
592
- await multisig.syncTransactionProposals();
1692
+ await multisig.syncProposals();
593
1693
  // Proposal ID is mocked to return 'c'.repeat(64)
594
- await expect(multisig.executeTransactionProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
1694
+ await expect(multisig.executeProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
595
1695
  });
596
- it('should fail when PSM ack signature is missing (selector ON)', async () => {
1696
+ it('should fail when GUARDIAN ack signature is missing (selector ON)', async () => {
597
1697
  const config = {
598
1698
  threshold: 1,
599
1699
  signerCommitments: ['0x' + 'a'.repeat(64)],
600
- psmCommitment: '0x' + 'c'.repeat(64),
1700
+ guardianCommitment: '0x' + 'c'.repeat(64),
601
1701
  };
602
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
1702
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
603
1703
  const readyDelta = {
604
1704
  account_id: '0x' + 'a'.repeat(30),
605
1705
  nonce: 1,
@@ -620,7 +1720,7 @@ describe('Multisig', () => {
620
1720
  proposer_id: '0x' + 'c'.repeat(64),
621
1721
  cosigner_sigs: [
622
1722
  {
623
- signer_id: '0x' + 'd'.repeat(64),
1723
+ signer_id: '0x' + 'a'.repeat(64),
624
1724
  signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
625
1725
  timestamp: '2024-01-01T00:00:00Z',
626
1726
  },
@@ -633,18 +1733,353 @@ describe('Multisig', () => {
633
1733
  ok: true,
634
1734
  json: async () => ({ proposals: [readyDelta] }),
635
1735
  });
636
- await multisig.syncTransactionProposals();
637
- // executeProposal: getDeltaProposals
1736
+ await multisig.syncProposals();
1737
+ // executeProposal: getDeltaProposal
638
1738
  mockFetch.mockResolvedValueOnce({
639
1739
  ok: true,
640
- json: async () => ({ proposals: [readyDelta] }),
1740
+ json: async () => readyDelta,
641
1741
  });
642
1742
  // executeProposal: pushDelta without ack_sig
643
1743
  mockFetch.mockResolvedValueOnce({
644
1744
  ok: true,
645
1745
  json: async () => ({ ...readyDelta, ack_sig: null }),
646
1746
  });
647
- await expect(multisig.executeTransactionProposal(proposalId)).rejects.toThrow('PSM did not return acknowledgment signature');
1747
+ await expect(multisig.executeProposal(proposalId)).rejects.toThrow('GUARDIAN did not return acknowledgment signature');
1748
+ });
1749
+ it('should encode ECDSA proposal and ack signatures with scheme-aware advice', async () => {
1750
+ const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
1751
+ vi.mocked(signatureHexToBytes).mockClear();
1752
+ vi.mocked(buildSignatureAdviceEntry).mockClear();
1753
+ const config = {
1754
+ threshold: 1,
1755
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1756
+ guardianCommitment: '0x' + 'c'.repeat(64),
1757
+ guardianPublicKey: '0x' + '1'.repeat(66),
1758
+ };
1759
+ const ecdsaSigner = {
1760
+ ...mockSigner,
1761
+ scheme: 'ecdsa',
1762
+ publicKey: '0x' + '2'.repeat(66),
1763
+ };
1764
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1765
+ const proposalId = '0x' + 'c'.repeat(64);
1766
+ const cosignerPubkey = '0x' + '3'.repeat(66);
1767
+ const ackPubkey = '0x' + '4'.repeat(66);
1768
+ const cosignerSignature = '0x' + '5'.repeat(130);
1769
+ const ackSignature = '0x' + '6'.repeat(130);
1770
+ multisig.proposals.set(proposalId, {
1771
+ id: proposalId,
1772
+ accountId: multisig.accountId,
1773
+ nonce: 1,
1774
+ status: 'ready',
1775
+ txSummary: 'AQID',
1776
+ signatures: [
1777
+ {
1778
+ signerId: '0x' + 'a'.repeat(64),
1779
+ signature: {
1780
+ scheme: 'ecdsa',
1781
+ signature: cosignerSignature,
1782
+ publicKey: cosignerPubkey,
1783
+ },
1784
+ timestamp: '2024-01-01T00:00:00Z',
1785
+ },
1786
+ ],
1787
+ metadata: {
1788
+ proposalType: 'change_threshold',
1789
+ targetThreshold: 1,
1790
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1791
+ description: '',
1792
+ },
1793
+ });
1794
+ mockFetch.mockResolvedValueOnce({
1795
+ ok: true,
1796
+ json: async () => ({
1797
+ account_id: multisig.accountId,
1798
+ nonce: 1,
1799
+ prev_commitment: '0x' + 'b'.repeat(64),
1800
+ delta_payload: {
1801
+ tx_summary: { data: 'AQID' },
1802
+ signatures: [],
1803
+ metadata: {
1804
+ proposal_type: 'change_threshold',
1805
+ target_threshold: 1,
1806
+ signer_commitments: ['0x' + 'a'.repeat(64)],
1807
+ },
1808
+ },
1809
+ status: {
1810
+ status: 'pending',
1811
+ timestamp: '2024-01-01T00:00:00Z',
1812
+ proposer_id: '0x' + 'a'.repeat(64),
1813
+ cosigner_sigs: [],
1814
+ },
1815
+ }),
1816
+ });
1817
+ mockFetch.mockResolvedValueOnce({
1818
+ ok: true,
1819
+ json: async () => ({
1820
+ account_id: multisig.accountId,
1821
+ nonce: 1,
1822
+ ack_sig: ackSignature,
1823
+ ack_pubkey: ackPubkey,
1824
+ ack_scheme: 'ecdsa',
1825
+ }),
1826
+ });
1827
+ mockWebClient.executeTransaction.mockResolvedValueOnce({});
1828
+ mockWebClient.proveTransaction.mockResolvedValueOnce({});
1829
+ mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
1830
+ mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
1831
+ await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
1832
+ expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
1833
+ expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
1834
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
1835
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
1836
+ });
1837
+ it('should execute imported ECDSA proposals with scheme-aware advice', async () => {
1838
+ const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
1839
+ vi.mocked(signatureHexToBytes).mockClear();
1840
+ vi.mocked(buildSignatureAdviceEntry).mockClear();
1841
+ const config = {
1842
+ threshold: 1,
1843
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1844
+ guardianCommitment: '0x' + 'c'.repeat(64),
1845
+ guardianPublicKey: '0x' + '1'.repeat(66),
1846
+ };
1847
+ const ecdsaSigner = {
1848
+ ...mockSigner,
1849
+ scheme: 'ecdsa',
1850
+ publicKey: '0x' + '2'.repeat(66),
1851
+ };
1852
+ const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1853
+ const proposalId = '0x' + 'c'.repeat(64);
1854
+ const cosignerPubkey = '0x' + '3'.repeat(66);
1855
+ const ackPubkey = '0x' + '4'.repeat(66);
1856
+ const cosignerSignature = '0x' + '5'.repeat(130);
1857
+ const ackSignature = '0x' + '6'.repeat(130);
1858
+ await multisig.importProposal(JSON.stringify({
1859
+ accountId: multisig.accountId,
1860
+ nonce: 1,
1861
+ commitment: proposalId,
1862
+ txSummaryBase64: 'AQID',
1863
+ signatures: [
1864
+ {
1865
+ commitment: '0x' + 'a'.repeat(64),
1866
+ signatureHex: cosignerSignature,
1867
+ scheme: 'ecdsa',
1868
+ publicKey: cosignerPubkey,
1869
+ timestamp: '2024-01-01T00:00:00Z',
1870
+ },
1871
+ ],
1872
+ metadata: {
1873
+ proposalType: 'change_threshold',
1874
+ targetThreshold: 1,
1875
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
1876
+ description: '',
1877
+ },
1878
+ }));
1879
+ mockFetch.mockResolvedValueOnce({
1880
+ ok: true,
1881
+ json: async () => ({
1882
+ account_id: multisig.accountId,
1883
+ nonce: 1,
1884
+ prev_commitment: '0x' + 'b'.repeat(64),
1885
+ delta_payload: {
1886
+ tx_summary: { data: 'AQID' },
1887
+ signatures: [],
1888
+ metadata: {
1889
+ proposal_type: 'change_threshold',
1890
+ target_threshold: 1,
1891
+ signer_commitments: ['0x' + 'a'.repeat(64)],
1892
+ },
1893
+ },
1894
+ status: {
1895
+ status: 'pending',
1896
+ timestamp: '2024-01-01T00:00:00Z',
1897
+ proposer_id: '0x' + 'a'.repeat(64),
1898
+ cosigner_sigs: [],
1899
+ },
1900
+ }),
1901
+ });
1902
+ mockFetch.mockResolvedValueOnce({
1903
+ ok: true,
1904
+ json: async () => ({
1905
+ account_id: multisig.accountId,
1906
+ nonce: 1,
1907
+ ack_sig: ackSignature,
1908
+ ack_pubkey: ackPubkey,
1909
+ ack_scheme: 'ecdsa',
1910
+ }),
1911
+ });
1912
+ mockWebClient.executeTransaction.mockResolvedValueOnce({});
1913
+ mockWebClient.proveTransaction.mockResolvedValueOnce({});
1914
+ mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
1915
+ mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
1916
+ await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
1917
+ expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
1918
+ expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
1919
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
1920
+ expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
1921
+ });
1922
+ it('should verify switch_guardian endpoint commitment before execution', async () => {
1923
+ const config = {
1924
+ threshold: 1,
1925
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1926
+ guardianCommitment: '0x' + 'c'.repeat(64),
1927
+ };
1928
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1929
+ const proposalId = '0x' + 'c'.repeat(64);
1930
+ const newGuardianPubkey = '0x' + '1'.repeat(64);
1931
+ multisig.proposals.set(proposalId, {
1932
+ id: proposalId,
1933
+ accountId: multisig.accountId,
1934
+ nonce: 1,
1935
+ status: 'ready',
1936
+ txSummary: 'AQID',
1937
+ signatures: [
1938
+ {
1939
+ signerId: '0x' + 'a'.repeat(64),
1940
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
1941
+ timestamp: '2024-01-01T00:00:00Z',
1942
+ },
1943
+ ],
1944
+ metadata: {
1945
+ proposalType: 'switch_guardian',
1946
+ newGuardianPubkey,
1947
+ newGuardianEndpoint: 'http://new-guardian.com',
1948
+ description: '',
1949
+ },
1950
+ });
1951
+ mockFetch.mockResolvedValueOnce({
1952
+ ok: true,
1953
+ json: async () => ({ commitment: newGuardianPubkey }),
1954
+ });
1955
+ mockWebClient.getAccount.mockResolvedValueOnce({
1956
+ serialize: () => new Uint8Array([1, 2, 3]),
1957
+ });
1958
+ mockFetch.mockResolvedValueOnce({
1959
+ ok: true,
1960
+ json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
1961
+ });
1962
+ mockWebClient.executeTransaction.mockResolvedValueOnce({});
1963
+ mockWebClient.proveTransaction.mockResolvedValueOnce({});
1964
+ mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
1965
+ mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
1966
+ await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
1967
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
1968
+ });
1969
+ it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {
1970
+ const config = {
1971
+ threshold: 1,
1972
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1973
+ guardianCommitment: '0x' + 'c'.repeat(64),
1974
+ };
1975
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1976
+ const proposalId = '0x' + 'c'.repeat(64);
1977
+ multisig.proposals.set(proposalId, {
1978
+ id: proposalId,
1979
+ accountId: multisig.accountId,
1980
+ nonce: 1,
1981
+ status: 'ready',
1982
+ txSummary: 'AQID',
1983
+ signatures: [
1984
+ {
1985
+ signerId: '0x' + 'a'.repeat(64),
1986
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
1987
+ timestamp: '2024-01-01T00:00:00Z',
1988
+ },
1989
+ ],
1990
+ metadata: {
1991
+ proposalType: 'switch_guardian',
1992
+ newGuardianPubkey: '0x' + '1'.repeat(64),
1993
+ newGuardianEndpoint: 'http://new-guardian.com',
1994
+ description: '',
1995
+ },
1996
+ });
1997
+ mockFetch.mockResolvedValueOnce({
1998
+ ok: true,
1999
+ json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
2000
+ });
2001
+ await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Refusing to use GUARDIAN endpoint');
2002
+ expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
2003
+ });
2004
+ it('should reject duplicate normalized signer IDs during execution', async () => {
2005
+ const config = {
2006
+ threshold: 2,
2007
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2008
+ guardianCommitment: '0x' + 'c'.repeat(64),
2009
+ };
2010
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2011
+ const proposalId = '0x' + 'c'.repeat(64);
2012
+ multisig.proposals.set(proposalId, {
2013
+ id: proposalId,
2014
+ accountId: multisig.accountId,
2015
+ nonce: 1,
2016
+ status: 'ready',
2017
+ txSummary: 'AQID',
2018
+ signatures: [
2019
+ {
2020
+ signerId: '0x' + 'a'.repeat(64),
2021
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2022
+ timestamp: '2024-01-01T00:00:00Z',
2023
+ },
2024
+ {
2025
+ signerId: '0x' + 'A'.repeat(64),
2026
+ signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
2027
+ timestamp: '2024-01-01T00:00:01Z',
2028
+ },
2029
+ ],
2030
+ metadata: {
2031
+ proposalType: 'switch_guardian',
2032
+ newGuardianPubkey: '0x' + '1'.repeat(64),
2033
+ newGuardianEndpoint: 'http://new-guardian.com',
2034
+ description: '',
2035
+ },
2036
+ });
2037
+ await expect(multisig.executeProposal(proposalId)).rejects.toThrow('duplicate signatures for signer');
2038
+ });
2039
+ it('should reject advice-map key collisions during execution', async () => {
2040
+ const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
2041
+ vi.mocked(buildSignatureAdviceEntry)
2042
+ .mockImplementationOnce(() => ({
2043
+ key: { toHex: () => '0x' + 'f'.repeat(64) },
2044
+ values: [1, 2, 3],
2045
+ }))
2046
+ .mockImplementationOnce(() => ({
2047
+ key: { toHex: () => '0x' + 'f'.repeat(64) },
2048
+ values: [1, 2, 3],
2049
+ }));
2050
+ const config = {
2051
+ threshold: 2,
2052
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
2053
+ guardianCommitment: '0x' + 'c'.repeat(64),
2054
+ };
2055
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2056
+ const proposalId = '0x' + 'c'.repeat(64);
2057
+ multisig.proposals.set(proposalId, {
2058
+ id: proposalId,
2059
+ accountId: multisig.accountId,
2060
+ nonce: 1,
2061
+ status: 'ready',
2062
+ txSummary: 'AQID',
2063
+ signatures: [
2064
+ {
2065
+ signerId: '0x' + 'a'.repeat(64),
2066
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
2067
+ timestamp: '2024-01-01T00:00:00Z',
2068
+ },
2069
+ {
2070
+ signerId: '0x' + 'b'.repeat(64),
2071
+ signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
2072
+ timestamp: '2024-01-01T00:00:01Z',
2073
+ },
2074
+ ],
2075
+ metadata: {
2076
+ proposalType: 'switch_guardian',
2077
+ newGuardianPubkey: '0x' + '1'.repeat(64),
2078
+ newGuardianEndpoint: 'http://new-guardian.com',
2079
+ description: '',
2080
+ },
2081
+ });
2082
+ await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Duplicate advice-map key detected');
648
2083
  });
649
2084
  });
650
2085
  describe('proposal metadata preservation', () => {
@@ -652,9 +2087,9 @@ describe('Multisig', () => {
652
2087
  const config = {
653
2088
  threshold: 1,
654
2089
  signerCommitments: ['0x' + 'a'.repeat(64)],
655
- psmCommitment: '0x' + 'c'.repeat(64),
2090
+ guardianCommitment: '0x' + 'c'.repeat(64),
656
2091
  };
657
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2092
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
658
2093
  // Create a proposal with metadata
659
2094
  const mockDelta = {
660
2095
  account_id: '0x' + 'a'.repeat(30),
@@ -663,7 +2098,11 @@ describe('Multisig', () => {
663
2098
  delta_payload: {
664
2099
  tx_summary: { data: 'AQID' },
665
2100
  signatures: [],
666
- metadata: { proposal_type: 'add_signer' },
2101
+ metadata: {
2102
+ proposal_type: 'add_signer',
2103
+ target_threshold: 2,
2104
+ signer_commitments: ['0x1', '0x2'],
2105
+ },
667
2106
  },
668
2107
  status: {
669
2108
  status: 'pending',
@@ -676,7 +2115,7 @@ describe('Multisig', () => {
676
2115
  ok: true,
677
2116
  json: async () => ({
678
2117
  delta: mockDelta,
679
- commitment: '0x' + 'd'.repeat(64),
2118
+ commitment: '0x' + 'c'.repeat(64),
680
2119
  }),
681
2120
  });
682
2121
  const proposal = await multisig.createProposal(1, 'AQID', {
@@ -693,17 +2132,17 @@ describe('Multisig', () => {
693
2132
  proposals: [mockDelta],
694
2133
  }),
695
2134
  });
696
- const syncedProposals = await multisig.syncTransactionProposals();
2135
+ const syncedProposals = await multisig.syncProposals();
697
2136
  const syncedProposal = syncedProposals.find(p => p.nonce === 1);
698
2137
  expect(syncedProposal?.metadata?.proposalType).toBe('add_signer');
699
2138
  });
700
- it('should use PSM metadata for new proposals from other signers', async () => {
2139
+ it('should use GUARDIAN metadata for new proposals from other signers', async () => {
701
2140
  const config = {
702
2141
  threshold: 2,
703
2142
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
704
- psmCommitment: '0x' + 'c'.repeat(64),
2143
+ guardianCommitment: '0x' + 'c'.repeat(64),
705
2144
  };
706
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2145
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
707
2146
  // Sync proposals - no local proposals exist
708
2147
  const mockProposals = [
709
2148
  {
@@ -732,7 +2171,7 @@ describe('Multisig', () => {
732
2171
  ok: true,
733
2172
  json: async () => ({ proposals: mockProposals }),
734
2173
  });
735
- const proposals = await multisig.syncTransactionProposals();
2174
+ const proposals = await multisig.syncProposals();
736
2175
  expect(proposals.length).toBe(1);
737
2176
  expect(proposals[0].metadata?.proposalType).toBe('p2id');
738
2177
  });
@@ -742,9 +2181,9 @@ describe('Multisig', () => {
742
2181
  const config = {
743
2182
  threshold: 1,
744
2183
  signerCommitments: ['0x' + 'a'.repeat(64)],
745
- psmCommitment: '0x' + 'c'.repeat(64),
2184
+ guardianCommitment: '0x' + 'c'.repeat(64),
746
2185
  };
747
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2186
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
748
2187
  const mockDelta = {
749
2188
  account_id: '0x' + 'a'.repeat(30),
750
2189
  nonce: 1,
@@ -770,7 +2209,7 @@ describe('Multisig', () => {
770
2209
  ok: true,
771
2210
  json: async () => ({
772
2211
  delta: mockDelta,
773
- commitment: '0x' + 'd'.repeat(64),
2212
+ commitment: '0x' + 'c'.repeat(64),
774
2213
  }),
775
2214
  });
776
2215
  const proposal = await multisig.createProposal(1, 'AQID', {
@@ -784,9 +2223,9 @@ describe('Multisig', () => {
784
2223
  const config = {
785
2224
  threshold: 1,
786
2225
  signerCommitments: ['0x' + 'a'.repeat(64)],
787
- psmCommitment: '0x' + 'c'.repeat(64),
2226
+ guardianCommitment: '0x' + 'c'.repeat(64),
788
2227
  };
789
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2228
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
790
2229
  const mockDelta = {
791
2230
  account_id: '0x' + 'a'.repeat(30),
792
2231
  nonce: 1,
@@ -812,7 +2251,7 @@ describe('Multisig', () => {
812
2251
  ok: true,
813
2252
  json: async () => ({
814
2253
  delta: mockDelta,
815
- commitment: '0x' + 'd'.repeat(64),
2254
+ commitment: '0x' + 'c'.repeat(64),
816
2255
  }),
817
2256
  });
818
2257
  const proposal = await multisig.createProposal(1, 'AQID', {
@@ -824,13 +2263,13 @@ describe('Multisig', () => {
824
2263
  });
825
2264
  expect(proposal.metadata?.proposalType).toBe('p2id');
826
2265
  });
827
- it('should create switch_psm proposal', async () => {
2266
+ it('should create switch_guardian proposal', async () => {
828
2267
  const config = {
829
2268
  threshold: 1,
830
2269
  signerCommitments: ['0x' + 'a'.repeat(64)],
831
- psmCommitment: '0x' + 'c'.repeat(64),
2270
+ guardianCommitment: '0x' + 'c'.repeat(64),
832
2271
  };
833
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2272
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
834
2273
  const mockDelta = {
835
2274
  account_id: '0x' + 'a'.repeat(30),
836
2275
  nonce: 1,
@@ -856,16 +2295,16 @@ describe('Multisig', () => {
856
2295
  ok: true,
857
2296
  json: async () => ({
858
2297
  delta: mockDelta,
859
- commitment: '0x' + 'd'.repeat(64),
2298
+ commitment: '0x' + 'c'.repeat(64),
860
2299
  }),
861
2300
  });
862
2301
  const proposal = await multisig.createProposal(1, 'AQID', {
863
- proposalType: 'switch_psm',
864
- newPsmPubkey: '0xnewpubkey',
865
- newPsmEndpoint: 'http://new-psm.com',
2302
+ proposalType: 'switch_guardian',
2303
+ newGuardianPubkey: '0xnewpubkey',
2304
+ newGuardianEndpoint: 'http://new-guardian.com',
866
2305
  description: '',
867
2306
  });
868
- expect(proposal.metadata?.proposalType).toBe('switch_psm');
2307
+ expect(proposal.metadata?.proposalType).toBe('switch_guardian');
869
2308
  });
870
2309
  });
871
2310
  describe('proposal status transitions', () => {
@@ -873,9 +2312,9 @@ describe('Multisig', () => {
873
2312
  const config = {
874
2313
  threshold: 2,
875
2314
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
876
- psmCommitment: '0x' + 'c'.repeat(64),
2315
+ guardianCommitment: '0x' + 'c'.repeat(64),
877
2316
  };
878
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2317
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
879
2318
  // First sync with 1 signature (pending)
880
2319
  const mockProposalsPending = [
881
2320
  {
@@ -910,8 +2349,8 @@ describe('Multisig', () => {
910
2349
  ok: true,
911
2350
  json: async () => ({ proposals: mockProposalsPending }),
912
2351
  });
913
- let proposals = await multisig.syncTransactionProposals();
914
- expect(proposals[0].status.type).toBe('pending');
2352
+ let proposals = await multisig.syncProposals();
2353
+ expect(proposals[0].status).toBe('pending');
915
2354
  // Second sync with 2 signatures (ready)
916
2355
  const mockProposalsReady = [
917
2356
  {
@@ -948,8 +2387,8 @@ describe('Multisig', () => {
948
2387
  ok: true,
949
2388
  json: async () => ({ proposals: mockProposalsReady }),
950
2389
  });
951
- proposals = await multisig.syncTransactionProposals();
952
- expect(proposals[0].status.type).toBe('ready');
2390
+ proposals = await multisig.syncProposals();
2391
+ expect(proposals[0].status).toBe('ready');
953
2392
  });
954
2393
  });
955
2394
  describe('getters', () => {
@@ -957,9 +2396,9 @@ describe('Multisig', () => {
957
2396
  const config = {
958
2397
  threshold: 3,
959
2398
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'c'.repeat(64)],
960
- psmCommitment: '0x' + 'd'.repeat(64),
2399
+ guardianCommitment: '0x' + 'd'.repeat(64),
961
2400
  };
962
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2401
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
963
2402
  expect(multisig.threshold).toBe(3);
964
2403
  });
965
2404
  it('should expose signerCommitments', () => {
@@ -967,28 +2406,28 @@ describe('Multisig', () => {
967
2406
  const config = {
968
2407
  threshold: 2,
969
2408
  signerCommitments,
970
- psmCommitment: '0x' + 'd'.repeat(64),
2409
+ guardianCommitment: '0x' + 'd'.repeat(64),
971
2410
  };
972
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2411
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
973
2412
  expect(multisig.signerCommitments).toEqual(signerCommitments);
974
2413
  });
975
- it('should expose psmCommitment', () => {
976
- const psmCommitment = '0x' + 'psm'.repeat(20);
2414
+ it('should expose guardianCommitment', () => {
2415
+ const guardianCommitment = '0x' + 'guardian'.repeat(20);
977
2416
  const config = {
978
2417
  threshold: 1,
979
2418
  signerCommitments: ['0x' + 'a'.repeat(64)],
980
- psmCommitment,
2419
+ guardianCommitment,
981
2420
  };
982
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
983
- expect(multisig.psmCommitment).toBe(psmCommitment);
2421
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2422
+ expect(multisig.guardianCommitment).toBe(guardianCommitment);
984
2423
  });
985
2424
  it('should expose account when provided', () => {
986
2425
  const config = {
987
2426
  threshold: 1,
988
2427
  signerCommitments: ['0x' + 'a'.repeat(64)],
989
- psmCommitment: '0x' + 'd'.repeat(64),
2428
+ guardianCommitment: '0x' + 'd'.repeat(64),
990
2429
  };
991
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2430
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
992
2431
  expect(multisig.account).toBe(mockAccount);
993
2432
  });
994
2433
  });
@@ -997,10 +2436,10 @@ describe('Multisig', () => {
997
2436
  const config = {
998
2437
  threshold: 2,
999
2438
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
1000
- psmCommitment: '0x' + 'c'.repeat(64),
2439
+ guardianCommitment: '0x' + 'c'.repeat(64),
1001
2440
  };
1002
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
1003
- // Simulates a PSM response with canonical snake_case metadata
2441
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2442
+ // Simulates a GUARDIAN response with canonical snake_case metadata
1004
2443
  const rustProposals = [
1005
2444
  {
1006
2445
  account_id: '0x' + 'a'.repeat(30),
@@ -1028,7 +2467,7 @@ describe('Multisig', () => {
1028
2467
  ok: true,
1029
2468
  json: async () => ({ proposals: rustProposals }),
1030
2469
  });
1031
- const proposals = await multisig.syncTransactionProposals();
2470
+ const proposals = await multisig.syncProposals();
1032
2471
  expect(proposals.length).toBe(1);
1033
2472
  // The TS client should normalize snake_case to camelCase
1034
2473
  expect(proposals[0].metadata?.proposalType).toBe('change_threshold');
@@ -1041,9 +2480,9 @@ describe('Multisig', () => {
1041
2480
  const config = {
1042
2481
  threshold: 1,
1043
2482
  signerCommitments: ['0x' + 'a'.repeat(64)],
1044
- psmCommitment: '0x' + 'c'.repeat(64),
2483
+ guardianCommitment: '0x' + 'c'.repeat(64),
1045
2484
  };
1046
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
2485
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1047
2486
  // P2ID proposal with canonical snake_case fields
1048
2487
  const p2idProposals = [
1049
2488
  {
@@ -1079,7 +2518,7 @@ describe('Multisig', () => {
1079
2518
  ok: true,
1080
2519
  json: async () => ({ proposals: p2idProposals }),
1081
2520
  });
1082
- const proposals = await multisig.syncTransactionProposals();
2521
+ const proposals = await multisig.syncProposals();
1083
2522
  expect(proposals.length).toBe(1);
1084
2523
  expect(proposals[0].metadata?.proposalType).toBe('p2id');
1085
2524
  if (proposals[0].metadata?.proposalType === 'p2id') {
@@ -1088,14 +2527,14 @@ describe('Multisig', () => {
1088
2527
  expect(proposals[0].metadata.amount).toBe('12345');
1089
2528
  }
1090
2529
  });
1091
- it('should parse switch_psm proposal with snake_case fields', async () => {
2530
+ it('should parse switch_guardian proposal with snake_case fields', async () => {
1092
2531
  const config = {
1093
2532
  threshold: 1,
1094
2533
  signerCommitments: ['0x' + 'a'.repeat(64)],
1095
- psmCommitment: '0x' + 'c'.repeat(64),
2534
+ guardianCommitment: '0x' + 'c'.repeat(64),
1096
2535
  };
1097
- const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
1098
- const switchPsmProposals = [
2536
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2537
+ const switchGuardianProposals = [
1099
2538
  {
1100
2539
  account_id: '0x' + 'a'.repeat(30),
1101
2540
  nonce: 1,
@@ -1104,9 +2543,9 @@ describe('Multisig', () => {
1104
2543
  tx_summary: { data: 'AQID' },
1105
2544
  signatures: [],
1106
2545
  metadata: {
1107
- proposal_type: 'switch_psm',
1108
- new_psm_pubkey: '0xnewpubkey',
1109
- new_psm_endpoint: 'http://new-psm.com',
2546
+ proposal_type: 'switch_guardian',
2547
+ new_guardian_pubkey: '0xnewpubkey',
2548
+ new_guardian_endpoint: 'http://new-guardian.com',
1110
2549
  salt: '0xsalt',
1111
2550
  },
1112
2551
  },
@@ -1120,14 +2559,14 @@ describe('Multisig', () => {
1120
2559
  ];
1121
2560
  mockFetch.mockResolvedValueOnce({
1122
2561
  ok: true,
1123
- json: async () => ({ proposals: switchPsmProposals }),
2562
+ json: async () => ({ proposals: switchGuardianProposals }),
1124
2563
  });
1125
- const proposals = await multisig.syncTransactionProposals();
2564
+ const proposals = await multisig.syncProposals();
1126
2565
  expect(proposals.length).toBe(1);
1127
- expect(proposals[0].metadata?.proposalType).toBe('switch_psm');
1128
- if (proposals[0].metadata?.proposalType === 'switch_psm') {
1129
- expect(proposals[0].metadata.newPsmPubkey).toBe('0xnewpubkey');
1130
- expect(proposals[0].metadata.newPsmEndpoint).toBe('http://new-psm.com');
2566
+ expect(proposals[0].metadata?.proposalType).toBe('switch_guardian');
2567
+ if (proposals[0].metadata?.proposalType === 'switch_guardian') {
2568
+ expect(proposals[0].metadata.newGuardianPubkey).toBe('0xnewpubkey');
2569
+ expect(proposals[0].metadata.newGuardianEndpoint).toBe('http://new-guardian.com');
1131
2570
  }
1132
2571
  });
1133
2572
  });