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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +144 -7
  2. package/dist/account/builder.d.ts.map +1 -1
  3. package/dist/account/builder.js +32 -25
  4. package/dist/account/builder.js.map +1 -1
  5. package/dist/account/builder.test.js +73 -21
  6. package/dist/account/builder.test.js.map +1 -1
  7. package/dist/account/layout.d.ts +31 -0
  8. package/dist/account/layout.d.ts.map +1 -0
  9. package/dist/account/layout.js +31 -0
  10. package/dist/account/layout.js.map +1 -0
  11. package/dist/account/masm/account-components/auth.d.ts +1 -4
  12. package/dist/account/masm/account-components/auth.d.ts.map +1 -1
  13. package/dist/account/masm/account-components/auth.js +36 -53
  14. package/dist/account/masm/account-components/auth.js.map +1 -1
  15. package/dist/account/masm/index.d.ts +0 -1
  16. package/dist/account/masm/index.d.ts.map +1 -1
  17. package/dist/account/masm/index.js +0 -1
  18. package/dist/account/masm/index.js.map +1 -1
  19. package/dist/account/storage.d.ts +4 -0
  20. package/dist/account/storage.d.ts.map +1 -1
  21. package/dist/account/storage.js +9 -20
  22. package/dist/account/storage.js.map +1 -1
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +5 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/client.test.js +68 -15
  27. package/dist/client.test.js.map +1 -1
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/inspector.d.ts +59 -1
  33. package/dist/inspector.d.ts.map +1 -1
  34. package/dist/inspector.js +157 -29
  35. package/dist/inspector.js.map +1 -1
  36. package/dist/inspector.test.js +246 -37
  37. package/dist/inspector.test.js.map +1 -1
  38. package/dist/multisig.d.ts +106 -32
  39. package/dist/multisig.d.ts.map +1 -1
  40. package/dist/multisig.js +289 -95
  41. package/dist/multisig.js.map +1 -1
  42. package/dist/multisig.test.js +581 -58
  43. package/dist/multisig.test.js.map +1 -1
  44. package/dist/procedures.d.ts +6 -7
  45. package/dist/procedures.d.ts.map +1 -1
  46. package/dist/procedures.js +6 -7
  47. package/dist/procedures.js.map +1 -1
  48. package/dist/proposal/metadata.d.ts.map +1 -1
  49. package/dist/proposal/metadata.js +13 -2
  50. package/dist/proposal/metadata.js.map +1 -1
  51. package/dist/proposal/metadata.test.js +57 -0
  52. package/dist/proposal/metadata.test.js.map +1 -1
  53. package/dist/prover/workflow.d.ts +3 -2
  54. package/dist/prover/workflow.d.ts.map +1 -1
  55. package/dist/prover/workflow.js +5 -2
  56. package/dist/prover/workflow.js.map +1 -1
  57. package/dist/prover/workflow.test.js +6 -4
  58. package/dist/prover/workflow.test.js.map +1 -1
  59. package/dist/transaction/index.d.ts +1 -1
  60. package/dist/transaction/index.d.ts.map +1 -1
  61. package/dist/transaction/index.js +1 -1
  62. package/dist/transaction/index.js.map +1 -1
  63. package/dist/transaction/p2id.d.ts +17 -6
  64. package/dist/transaction/p2id.d.ts.map +1 -1
  65. package/dist/transaction/p2id.js +22 -31
  66. package/dist/transaction/p2id.js.map +1 -1
  67. package/dist/transaction/p2id.test.js +62 -24
  68. package/dist/transaction/p2id.test.js.map +1 -1
  69. package/dist/transaction/summary.d.ts +45 -2
  70. package/dist/transaction/summary.d.ts.map +1 -1
  71. package/dist/transaction/summary.js +42 -2
  72. package/dist/transaction/summary.js.map +1 -1
  73. package/dist/transaction/summary.test.d.ts +2 -0
  74. package/dist/transaction/summary.test.d.ts.map +1 -0
  75. package/dist/transaction/summary.test.js +26 -0
  76. package/dist/transaction/summary.test.js.map +1 -0
  77. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  78. package/dist/transaction/updateGuardian.js +16 -18
  79. package/dist/transaction/updateGuardian.js.map +1 -1
  80. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  81. package/dist/transaction/updateProcedureThreshold.js +15 -17
  82. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  83. package/dist/transaction/updateSigners.d.ts +6 -1
  84. package/dist/transaction/updateSigners.d.ts.map +1 -1
  85. package/dist/transaction/updateSigners.js +20 -17
  86. package/dist/transaction/updateSigners.js.map +1 -1
  87. package/dist/transaction.d.ts +2 -2
  88. package/dist/transaction.d.ts.map +1 -1
  89. package/dist/transaction.js +1 -1
  90. package/dist/transaction.js.map +1 -1
  91. package/dist/types/proposal.d.ts +28 -4
  92. package/dist/types/proposal.d.ts.map +1 -1
  93. package/dist/types/proposal.js +18 -0
  94. package/dist/types/proposal.js.map +1 -1
  95. package/dist/types.d.ts +0 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/utils/signature.d.ts +11 -7
  98. package/dist/utils/signature.d.ts.map +1 -1
  99. package/dist/utils/signature.js +24 -58
  100. package/dist/utils/signature.js.map +1 -1
  101. package/dist/utils/word.d.ts +7 -0
  102. package/dist/utils/word.d.ts.map +1 -1
  103. package/dist/utils/word.js +15 -0
  104. package/dist/utils/word.js.map +1 -1
  105. package/masm/account_components/auth/guarded_multisig.masm +42 -0
  106. package/package.json +7 -4
  107. package/src/account/builder.test.ts +111 -45
  108. package/src/account/builder.ts +45 -33
  109. package/src/account/layout.ts +33 -0
  110. package/src/account/masm/account-components/auth.ts +36 -56
  111. package/src/account/masm/index.ts +0 -1
  112. package/src/account/storage.ts +9 -22
  113. package/src/client.test.ts +80 -15
  114. package/src/client.ts +5 -3
  115. package/src/index.ts +26 -1
  116. package/src/inspector.test.ts +330 -38
  117. package/src/inspector.ts +196 -33
  118. package/src/multisig.test.ts +679 -63
  119. package/src/multisig.ts +361 -107
  120. package/src/procedures.ts +6 -7
  121. package/src/proposal/metadata.test.ts +76 -0
  122. package/src/proposal/metadata.ts +13 -2
  123. package/src/prover/workflow.test.ts +9 -4
  124. package/src/prover/workflow.ts +15 -3
  125. package/src/transaction/index.ts +7 -1
  126. package/src/transaction/p2id.test.ts +112 -31
  127. package/src/transaction/p2id.ts +39 -38
  128. package/src/transaction/summary.test.ts +32 -0
  129. package/src/transaction/summary.ts +83 -4
  130. package/src/transaction/updateGuardian.ts +15 -25
  131. package/src/transaction/updateProcedureThreshold.ts +13 -29
  132. package/src/transaction/updateSigners.ts +38 -30
  133. package/src/transaction.ts +8 -1
  134. package/src/types/proposal.ts +43 -4
  135. package/src/types.ts +0 -1
  136. package/src/utils/signature.ts +32 -65
  137. package/src/utils/word.ts +17 -0
  138. package/dist/account/masm/auth.d.ts +0 -5
  139. package/dist/account/masm/auth.d.ts.map +0 -1
  140. package/dist/account/masm/auth.js +0 -1509
  141. package/dist/account/masm/auth.js.map +0 -1
  142. package/masm/account_components/auth/multisig.masm +0 -12
  143. package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
  144. package/masm/account_components/auth/multisig_guardian.masm +0 -16
  145. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
  146. package/masm/auth/guardian.masm +0 -199
  147. package/masm/auth/guardian_ecdsa.masm +0 -195
  148. package/masm/auth/multisig.masm +0 -554
  149. package/masm/auth/multisig_ecdsa.masm +0 -554
  150. package/src/account/masm/auth.ts +0 -1512
package/src/procedures.ts CHANGED
@@ -11,13 +11,12 @@
11
11
  * human-readable encoding and should not be copied into this table.
12
12
  */
13
13
  export const PROCEDURE_ROOTS = {
14
- update_signers: '0x34963b067dbba634e57b416bc2f2a9a8d4ac24147f40b2900148c9ba44774274',
15
- update_procedure_threshold: '0xec74c4b96ce593c11017ae54dec9c0ae5e0d242e8b3074eb3908d961300aed67',
16
- auth_tx: '0x0708020dce7b91b61116e3eb27e5d686e129a83df3c540e0a7693b4523814e72',
17
- update_guardian: '0xeceb1f2c2d7d20312dbaf091e9a27a2b63f9fcba120948043069793a5715bc96',
18
- verify_guardian: '0xe6a8a62d37117f55a79b5345aa3d263ab16e973d486bac9a1612663dfdecf82d',
19
- send_asset: '0xfb1c73d10de1954e9e8948964e3e77cf4e33759d2e012cb00eb10c50f2974eb4',
20
- receive_asset: '0x6170fd6d682d91777b551fd866258f43cc657f1291f8f071500f4e56e9c153da',
14
+ update_signers: '0xa261cfd3c8791ac5abe1e78e14eade2f20789d73ab1c23c430418de59bc3380e',
15
+ update_procedure_threshold: '0x97587c61d49313b1d5a3c8b7437e0080e67ed9bd9d3e7206bcae562f934ccd03',
16
+ auth_tx: '0xa6aa6f69d9358535272ba433cd48d20628a5c69598e00c6dd01a22e83a5f15df',
17
+ update_guardian: '0x0a614ff7c81a561cbd2a4c2d9482031a7a841ca5de33349daed23a9d871b3675',
18
+ send_asset: '0x595bc83258726a66bd904912cfd5186c07cbd902dfbc115b7d6bc8105efc57e3',
19
+ receive_asset: '0x34a56dd18f6fe5aab63198b9dcfc6467e793ebabb37d56b994b902504635da13',
21
20
  } as const;
22
21
 
23
22
  /**
@@ -157,3 +157,79 @@ describe('ProposalMetadataCodec p2id noteType (issue #322)', () => {
157
157
  expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported noteType/);
158
158
  });
159
159
  });
160
+
161
+ describe('ProposalMetadataCodec p2id P2IDE heights (issue #366)', () => {
162
+ const baseWire: GuardianProposalMetadata = {
163
+ proposalType: 'p2id',
164
+ recipientId: '0xrecipient',
165
+ faucetId: '0xfaucet',
166
+ amount: '1000',
167
+ };
168
+
169
+ it('round-trips reclaim and timelock heights through the codec', () => {
170
+ const md = ProposalMetadataCodec.fromGuardian({
171
+ ...baseWire,
172
+ reclaimHeight: 12345,
173
+ timelockHeight: 700,
174
+ }) as P2IdProposalMetadata;
175
+ expect(md.reclaimHeight).toBe(12345);
176
+ expect(md.timelockHeight).toBe(700);
177
+
178
+ const wire = ProposalMetadataCodec.toGuardian(md);
179
+ expect(wire.reclaimHeight).toBe(12345);
180
+ expect(wire.timelockHeight).toBe(700);
181
+ });
182
+
183
+ it('leaves heights absent for plain P2ID proposals (pre-#366 wire shape)', () => {
184
+ const md = ProposalMetadataCodec.fromGuardian(baseWire) as P2IdProposalMetadata;
185
+ expect(md.reclaimHeight).toBeUndefined();
186
+ expect(md.timelockHeight).toBeUndefined();
187
+
188
+ const wire = ProposalMetadataCodec.toGuardian(md);
189
+ expect(wire.reclaimHeight).toBeUndefined();
190
+ expect(wire.timelockHeight).toBeUndefined();
191
+ });
192
+
193
+ it('fromGuardian rejects a zero height (encodes "no constraint" on-chain)', () => {
194
+ expect(() =>
195
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, reclaimHeight: 0 }),
196
+ ).toThrow(/unsupported reclaimHeight/);
197
+ });
198
+
199
+ it('fromGuardian rejects a non-integer or out-of-range height', () => {
200
+ expect(() =>
201
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, timelockHeight: 1.5 }),
202
+ ).toThrow(/unsupported timelockHeight/);
203
+ expect(() =>
204
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, reclaimHeight: 0x1_0000_0000 }),
205
+ ).toThrow(/unsupported reclaimHeight/);
206
+ });
207
+
208
+ it('validate rejects an invalid height', () => {
209
+ const md = {
210
+ proposalType: 'p2id',
211
+ description: '',
212
+ recipientId: '0xrecipient',
213
+ faucetId: '0xfaucet',
214
+ amount: '1000',
215
+ reclaimHeight: -1,
216
+ } as unknown as P2IdProposalMetadata;
217
+ expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported reclaimHeight/);
218
+ });
219
+
220
+ it('toGuardian rejects an invalid height instead of emitting it on the wire', () => {
221
+ const base = {
222
+ proposalType: 'p2id',
223
+ description: '',
224
+ recipientId: '0xrecipient',
225
+ faucetId: '0xfaucet',
226
+ amount: '1000',
227
+ };
228
+ expect(() =>
229
+ ProposalMetadataCodec.toGuardian({ ...base, reclaimHeight: 0 } as P2IdProposalMetadata),
230
+ ).toThrow(/unsupported reclaimHeight/);
231
+ expect(() =>
232
+ ProposalMetadataCodec.toGuardian({ ...base, timelockHeight: 1.5 } as P2IdProposalMetadata),
233
+ ).toThrow(/unsupported timelockHeight/);
234
+ });
235
+ });
@@ -1,7 +1,7 @@
1
1
  import type { ProposalMetadata as GuardianProposalMetadata } from '@openzeppelin/guardian-client';
2
2
  import type { ProposalMetadata } from '../types.js';
3
3
  import { isProcedureName } from '../procedures.js';
4
- import { isP2idNoteVisibility } from '../types/proposal.js';
4
+ import { isP2idNoteVisibility, parseP2ideHeight } from '../types/proposal.js';
5
5
 
6
6
  export class ProposalMetadataCodec {
7
7
  static toGuardian(metadata: ProposalMetadata): GuardianProposalMetadata {
@@ -10,6 +10,7 @@ export class ProposalMetadataCodec {
10
10
  description: metadata.description,
11
11
  salt: metadata.saltHex,
12
12
  requiredSignatures: metadata.requiredSignatures,
13
+ chainAnchor: metadata.chainAnchor,
13
14
  };
14
15
 
15
16
  switch (metadata.proposalType) {
@@ -30,6 +31,11 @@ export class ProposalMetadataCodec {
30
31
  // keeps the pre-#322 wire shape and matches the Rust encoder (which
31
32
  // round-trips through the NoteType enum). Absent => public.
32
33
  noteType: metadata.noteType === 'private' ? 'private' : undefined,
34
+ // Validated on encode too: the create path already rejects bad
35
+ // heights when building the note, but metadata handed directly to
36
+ // the codec must not reach the wire with a value cosigners reject.
37
+ reclaimHeight: parseP2ideHeight('reclaimHeight', metadata.reclaimHeight),
38
+ timelockHeight: parseP2ideHeight('timelockHeight', metadata.timelockHeight),
33
39
  };
34
40
  case 'switch_guardian':
35
41
  return {
@@ -68,6 +74,7 @@ export class ProposalMetadataCodec {
68
74
  description: guardian.description ?? '',
69
75
  saltHex: guardian.salt,
70
76
  requiredSignatures: guardian.requiredSignatures,
77
+ chainAnchor: guardian.chainAnchor,
71
78
  };
72
79
 
73
80
  switch (guardian.proposalType) {
@@ -87,6 +94,8 @@ export class ProposalMetadataCodec {
87
94
  faucetId: guardian.faucetId,
88
95
  amount: guardian.amount,
89
96
  noteType: guardian.noteType,
97
+ reclaimHeight: parseP2ideHeight('reclaimHeight', guardian.reclaimHeight),
98
+ timelockHeight: parseP2ideHeight('timelockHeight', guardian.timelockHeight),
90
99
  };
91
100
  case 'consume_notes':
92
101
  if (!guardian.noteIds || guardian.noteIds.length === 0) {
@@ -138,7 +147,7 @@ export class ProposalMetadataCodec {
138
147
  };
139
148
  default:
140
149
  // Any proposal type the SDK does not model collapses to the 'custom'
141
- // bucket while preserving the original label (issue #266).
150
+ // bucket while preserving the original label.
142
151
  return {
143
152
  ...base,
144
153
  proposalType: 'custom',
@@ -182,6 +191,8 @@ export class ProposalMetadataCodec {
182
191
  if (metadata.noteType !== undefined && !isP2idNoteVisibility(metadata.noteType)) {
183
192
  throw new Error(`p2id proposal has unsupported noteType '${metadata.noteType}'`);
184
193
  }
194
+ parseP2ideHeight('reclaimHeight', metadata.reclaimHeight);
195
+ parseP2ideHeight('timelockHeight', metadata.timelockHeight);
185
196
  return metadata;
186
197
  case 'custom':
187
198
  // Custom proposals are opaque to the SDK; nothing to validate beyond
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AccountId,
3
+ ChainAnchor,
3
4
  MidenClient,
4
5
  TransactionProver,
5
6
  TransactionRequest,
@@ -13,6 +14,8 @@ function asType<T>(value: unknown): T {
13
14
  return value as T;
14
15
  }
15
16
 
17
+ const anchor = asType<ChainAnchor>({ marker: 'anchor' });
18
+
16
19
  describe('ProverWorkflow', () => {
17
20
  it('executes once, retries proof with fresh provers, submits once, and applies once', async () => {
18
21
  const transient = Object.assign(new Error('temporarily unavailable'), {
@@ -47,12 +50,14 @@ describe('ProverWorkflow', () => {
47
50
  runtime,
48
51
  );
49
52
 
50
- await workflow.submit(
53
+ await workflow.submitAt(
51
54
  asType<AccountId>({}),
52
55
  asType<TransactionRequest>({}),
56
+ anchor,
53
57
  );
54
58
 
55
59
  expect(executeRequest).toHaveBeenCalledTimes(1);
60
+ expect(executeRequest.mock.calls[0][2]).toEqual({ anchor });
56
61
  expect(prove).toHaveBeenCalledTimes(2);
57
62
  expect(provers).toHaveLength(2);
58
63
  expect(provers[0]).not.toBe(provers[1]);
@@ -83,7 +88,7 @@ describe('ProverWorkflow', () => {
83
88
  );
84
89
 
85
90
  await expect(
86
- workflow.submit(asType<AccountId>({}), asType<TransactionRequest>({})),
91
+ workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor),
87
92
  ).rejects.toBe(final);
88
93
  expect(prove).toHaveBeenCalledTimes(2);
89
94
  expect(runtime.sleep).toHaveBeenCalledTimes(1);
@@ -113,7 +118,7 @@ describe('ProverWorkflow', () => {
113
118
  );
114
119
 
115
120
  await expect(
116
- workflow.submit(asType<AccountId>({}), asType<TransactionRequest>({})),
121
+ workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor),
117
122
  ).rejects.toBe(rateLimited);
118
123
  expect(prove).toHaveBeenCalledTimes(1);
119
124
  expect(submit).toHaveBeenCalledTimes(1);
@@ -133,7 +138,7 @@ describe('ProverWorkflow', () => {
133
138
  createProver: () => undefined,
134
139
  });
135
140
 
136
- await workflow.submit(asType<AccountId>({}), asType<TransactionRequest>({}));
141
+ await workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor);
137
142
 
138
143
  expect(prove).toHaveBeenCalledWith();
139
144
  });
@@ -1,4 +1,9 @@
1
- import type { AccountId, MidenClient, TransactionRequest } from '@miden-sdk/miden-sdk';
1
+ import type {
2
+ AccountId,
3
+ ChainAnchor,
4
+ MidenClient,
5
+ TransactionRequest,
6
+ } from '@miden-sdk/miden-sdk';
2
7
  import type { ResolvedProverConfig } from './config.js';
3
8
  import type { RetryRuntime } from '../retry/runtime.js';
4
9
  import { proveWithRetry } from './retry.js';
@@ -10,8 +15,15 @@ export class ProverWorkflow {
10
15
  private readonly runtime?: RetryRuntime,
11
16
  ) {}
12
17
 
13
- async submit(accountId: AccountId, request: TransactionRequest): Promise<void> {
14
- const execution = await this.client.transactions.executeRequest(accountId, request);
18
+ /** Proves, submits, and applies a request at its signed chain anchor. */
19
+ async submitAt(
20
+ accountId: AccountId,
21
+ request: TransactionRequest,
22
+ anchor: ChainAnchor,
23
+ ): Promise<void> {
24
+ const execution = await this.client.transactions.executeRequest(accountId, request, {
25
+ anchor,
26
+ });
15
27
  const proof = await proveWithRetry(execution, this.config, this.runtime);
16
28
  const submission = await proof.submit();
17
29
  await submission.apply();
@@ -1,4 +1,10 @@
1
- export { executeForSummary } from './summary.js';
1
+ export {
2
+ chainAnchorFromBase64,
3
+ chainAnchorToBase64,
4
+ executeForSummary,
5
+ executeForSummaryAt,
6
+ summarySalt,
7
+ } from './summary.js';
2
8
  export { buildUpdateSignersTransactionRequest } from './updateSigners.js';
3
9
  export { buildUpdateProcedureThresholdTransactionRequest } from './updateProcedureThreshold.js';
4
10
  export { buildUpdateGuardianTransactionRequest } from './updateGuardian.js';
@@ -2,13 +2,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type { Word } from '@miden-sdk/miden-sdk';
3
3
 
4
4
  const {
5
- mockFromVaultKey,
6
5
  mockFungibleAssetConstructor,
7
6
  mockHashElements,
8
7
  mockNormalizeHexWord,
9
8
  mockRandomWord,
10
9
  mockWordFromHex,
11
10
  noteMetadataCalls,
11
+ noteRecipientCalls,
12
+ noteStorageCalls,
12
13
  saltFelts,
13
14
  } = vi.hoisted(() => {
14
15
  const saltFelts = [
@@ -19,13 +20,10 @@ const {
19
20
  ];
20
21
 
21
22
  return {
22
- mockFromVaultKey: vi.fn((vaultKey: unknown, amount: bigint) => ({
23
- kind: 'asset-from-vault-key',
24
- vaultKey,
25
- amount,
26
- })),
27
23
  mockFungibleAssetConstructor: vi.fn(),
28
24
  noteMetadataCalls: [] as unknown[][],
25
+ noteRecipientCalls: [] as unknown[][],
26
+ noteStorageCalls: [] as unknown[][],
29
27
  mockHashElements: vi.fn().mockReturnValue({ toString: () => 'serial' }),
30
28
  mockNormalizeHexWord: vi.fn((hex: string) => hex),
31
29
  mockRandomWord: vi.fn().mockReturnValue({
@@ -71,7 +69,9 @@ vi.mock('@miden-sdk/miden-sdk', () => {
71
69
  }
72
70
 
73
71
  class NoteStorage {
74
- constructor(_inputs: FeltArray) {}
72
+ constructor(inputs: FeltArray) {
73
+ noteStorageCalls.push([inputs]);
74
+ }
75
75
  }
76
76
 
77
77
  class NoteMetadata {
@@ -86,10 +86,12 @@ vi.mock('@miden-sdk/miden-sdk', () => {
86
86
 
87
87
  class NoteRecipient {
88
88
  constructor(
89
- _serialNum: unknown,
90
- _noteScript: unknown,
91
- _noteInputs: unknown,
92
- ) {}
89
+ serialNum: unknown,
90
+ noteScript: unknown,
91
+ noteInputs: unknown,
92
+ ) {
93
+ noteRecipientCalls.push([serialNum, noteScript, noteInputs]);
94
+ }
93
95
  }
94
96
 
95
97
  class Note {
@@ -104,8 +106,6 @@ vi.mock('@miden-sdk/miden-sdk', () => {
104
106
  constructor(faucet: unknown, amount: bigint) {
105
107
  mockFungibleAssetConstructor(faucet, amount);
106
108
  }
107
-
108
- static fromVaultKey = mockFromVaultKey;
109
109
  }
110
110
 
111
111
  class NoteArray {
@@ -152,6 +152,7 @@ vi.mock('@miden-sdk/miden-sdk', () => {
152
152
  NoteStorage,
153
153
  NoteScript: {
154
154
  p2id: vi.fn(() => ({ kind: 'p2id-script' })),
155
+ p2ide: vi.fn(() => ({ kind: 'p2ide-script' })),
155
156
  },
156
157
  NoteTag: {
157
158
  withAccountTarget: vi.fn(() => ({ kind: 'tag' })),
@@ -182,31 +183,20 @@ vi.mock('../utils/random.js', () => ({
182
183
  }));
183
184
 
184
185
  import { buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata } from './p2id.js';
185
- import type { Account } from '@miden-sdk/miden-sdk';
186
+ import { NoteType } from '@miden-sdk/miden-sdk';
186
187
 
187
188
  const FAUCET_ID = '0x7bfb0f38b0fafa103f86a805594171';
188
189
 
189
- const mockAccount = {
190
- vault: () => ({
191
- fungibleAssets: () => [
192
- {
193
- faucetId: () => ({ toString: () => FAUCET_ID }),
194
- vaultKey: () => ({ kind: 'vault-key' }),
195
- },
196
- ],
197
- }),
198
- } as unknown as Account;
199
- import { NoteType } from '@miden-sdk/miden-sdk';
200
-
201
190
  describe('buildP2idTransactionRequest', () => {
202
191
  beforeEach(() => {
203
- mockFromVaultKey.mockClear();
204
192
  mockFungibleAssetConstructor.mockClear();
205
193
  mockHashElements.mockClear();
206
194
  mockNormalizeHexWord.mockClear();
207
195
  mockRandomWord.mockClear();
208
196
  mockWordFromHex.mockClear();
209
197
  noteMetadataCalls.length = 0;
198
+ noteRecipientCalls.length = 0;
199
+ noteStorageCalls.length = 0;
210
200
  });
211
201
 
212
202
  it('derives serial number from salt felts plus four zero felts', () => {
@@ -217,7 +207,6 @@ describe('buildP2idTransactionRequest', () => {
217
207
  '0x8a65fc5a39e4cd106d648e3eb4ab5f',
218
208
  FAUCET_ID,
219
209
  10n,
220
- mockAccount,
221
210
  { salt },
222
211
  );
223
212
 
@@ -239,28 +228,120 @@ describe('buildP2idTransactionRequest', () => {
239
228
  buildP2idTransactionRequest(
240
229
  '0x7bfb0f38b0fafa103f86a805594170',
241
230
  '0x8a65fc5a39e4cd106d648e3eb4ab5f',
242
- '0x7bfb0f38b0fafa103f86a805594171',
231
+ FAUCET_ID,
243
232
  10n,
244
- mockAccount,
245
233
  );
246
234
 
247
235
  expect(noteMetadataCalls).toHaveLength(1);
248
236
  expect(noteMetadataCalls[0][1]).toBe(NoteType.Public);
249
237
  });
250
238
 
239
+ it('builds the asset from the faucet id, whose callback flag it carries since Miden 0.16', () => {
240
+ buildP2idTransactionRequest(
241
+ '0x7bfb0f38b0fafa103f86a805594170',
242
+ '0x8a65fc5a39e4cd106d648e3eb4ab5f',
243
+ FAUCET_ID,
244
+ 10n,
245
+ );
246
+
247
+ expect(mockFungibleAssetConstructor).toHaveBeenCalledTimes(1);
248
+ const [faucet, amount] = mockFungibleAssetConstructor.mock.calls[0] as [{ hex: string }, bigint];
249
+ expect(faucet.hex).toBe(FAUCET_ID);
250
+ expect(amount).toBe(10n);
251
+ });
252
+
251
253
  it('threads the requested noteType into the note metadata (issue #322)', () => {
252
254
  buildP2idTransactionRequest(
253
255
  '0x7bfb0f38b0fafa103f86a805594170',
254
256
  '0x8a65fc5a39e4cd106d648e3eb4ab5f',
255
257
  '0x7bfb0f38b0fafa103f86a805594171',
256
258
  10n,
257
- mockAccount,
258
259
  { noteType: NoteType.Private },
259
260
  );
260
261
 
261
262
  expect(noteMetadataCalls).toHaveLength(1);
262
263
  expect(noteMetadataCalls[0][1]).toBe(NoteType.Private);
263
264
  });
265
+
266
+ it('builds a plain P2ID note without heights: p2id script, 2 storage items', () => {
267
+ buildP2idTransactionRequest(
268
+ '0x7bfb0f38b0fafa103f86a805594170',
269
+ '0x8a65fc5a39e4cd106d648e3eb4ab5f',
270
+ FAUCET_ID,
271
+ 10n,
272
+ );
273
+
274
+ expect(noteRecipientCalls).toHaveLength(1);
275
+ expect(noteRecipientCalls[0][1]).toEqual({ kind: 'p2id-script' });
276
+ const [storageInputs] = noteStorageCalls[0] as [{ values: unknown[] }];
277
+ expect(storageInputs.values).toEqual([2, 1]);
278
+ });
279
+
280
+ it('builds a P2IDE note when reclaimHeight is set (issue #366)', () => {
281
+ buildP2idTransactionRequest(
282
+ '0x7bfb0f38b0fafa103f86a805594170',
283
+ '0x8a65fc5a39e4cd106d648e3eb4ab5f',
284
+ FAUCET_ID,
285
+ 10n,
286
+ { reclaimHeight: 12345 },
287
+ );
288
+
289
+ expect(noteRecipientCalls).toHaveLength(1);
290
+ expect(noteRecipientCalls[0][1]).toEqual({ kind: 'p2ide-script' });
291
+
292
+ // P2IDE storage layout: [suffix, prefix, reclaim, timelock], 0 = unset.
293
+ const [storageInputs] = noteStorageCalls[0] as [{ values: unknown[] }];
294
+ expect(storageInputs.values).toHaveLength(4);
295
+ expect(storageInputs.values.slice(0, 2)).toEqual([2, 1]);
296
+ expect((storageInputs.values[2] as { value: bigint }).value).toBe(12345n);
297
+ expect((storageInputs.values[3] as { value: bigint }).value).toBe(0n);
298
+ });
299
+
300
+ it('builds a P2IDE note when only timelockHeight is set (issue #366)', () => {
301
+ buildP2idTransactionRequest(
302
+ '0x7bfb0f38b0fafa103f86a805594170',
303
+ '0x8a65fc5a39e4cd106d648e3eb4ab5f',
304
+ FAUCET_ID,
305
+ 10n,
306
+ { timelockHeight: 777 },
307
+ );
308
+
309
+ expect(noteRecipientCalls[0][1]).toEqual({ kind: 'p2ide-script' });
310
+ const [storageInputs] = noteStorageCalls[0] as [{ values: unknown[] }];
311
+ expect((storageInputs.values[2] as { value: bigint }).value).toBe(0n);
312
+ expect((storageInputs.values[3] as { value: bigint }).value).toBe(777n);
313
+ });
314
+
315
+ it('carries both heights into the P2IDE storage (issue #366)', () => {
316
+ buildP2idTransactionRequest(
317
+ '0x7bfb0f38b0fafa103f86a805594170',
318
+ '0x8a65fc5a39e4cd106d648e3eb4ab5f',
319
+ FAUCET_ID,
320
+ 10n,
321
+ { reclaimHeight: 500, timelockHeight: 400 },
322
+ );
323
+
324
+ const [storageInputs] = noteStorageCalls[0] as [{ values: unknown[] }];
325
+ expect((storageInputs.values[2] as { value: bigint }).value).toBe(500n);
326
+ expect((storageInputs.values[3] as { value: bigint }).value).toBe(400n);
327
+ });
328
+
329
+ it.each([
330
+ ['zero', 0],
331
+ ['negative', -5],
332
+ ['fractional', 1.5],
333
+ ['above u32::MAX', 0x1_0000_0000],
334
+ ])('rejects a %s height instead of building a divergent note (issue #366)', (_label, height) => {
335
+ expect(() =>
336
+ buildP2idTransactionRequest(
337
+ '0x7bfb0f38b0fafa103f86a805594170',
338
+ '0x8a65fc5a39e4cd106d648e3eb4ab5f',
339
+ FAUCET_ID,
340
+ 10n,
341
+ { reclaimHeight: height },
342
+ ),
343
+ ).toThrow(/unsupported reclaimHeight/);
344
+ });
264
345
  });
265
346
 
266
347
  describe('parseP2idNoteType', () => {
@@ -1,6 +1,7 @@
1
- import type { Account, TransactionRequest, Word } from '@miden-sdk/miden-sdk';
1
+ import type { TransactionRequest, Word } from '@miden-sdk/miden-sdk';
2
2
  import {
3
3
  AccountId,
4
+ Felt,
4
5
  FeltArray,
5
6
  FungibleAsset,
6
7
  MidenArrays,
@@ -20,8 +21,21 @@ import { randomWord } from '../utils/random.js';
20
21
  import { normalizeHexWord } from '../utils/encoding.js';
21
22
  import type { SignatureOptions } from './options.js';
22
23
  import type { P2idNoteVisibility } from '../types/proposal.js';
24
+ import { parseP2ideHeight } from '../types/proposal.js';
23
25
 
24
- export interface P2idTransactionOptions extends SignatureOptions {
26
+ /**
27
+ * P2IDE execution constraints (issue #366). Presence of either height builds
28
+ * a P2IDE note instead of a plain P2ID note, mirroring the Miden SDK's
29
+ * `reclaimAfter`/`timelockUntil` semantics on `SendOptions`.
30
+ */
31
+ export interface P2ideHeightOptions {
32
+ /** Absolute block height at which the sender may reclaim the note. */
33
+ reclaimHeight?: number;
34
+ /** Absolute block height before which the note cannot be consumed. */
35
+ timelockHeight?: number;
36
+ }
37
+
38
+ export interface P2idTransactionOptions extends SignatureOptions, P2ideHeightOptions {
25
39
  /** Visibility of the created note. Defaults to `NoteType.Public`. */
26
40
  noteType?: NoteType;
27
41
  }
@@ -60,47 +74,35 @@ export function deriveP2idSerialNumber(salt: Word): Word {
60
74
  ]));
61
75
  }
62
76
 
63
- /**
64
- * Builds the fungible asset to transfer, sourcing the callback flag from the held asset.
65
- *
66
- * In Miden 0.15 the callback flag is part of the vault key, so rebuilding the asset from
67
- * `faucet`/`amount` with the default flag would not match the held asset and the transfer
68
- * would abort. When the faucet is absent the default flag is used, surfacing the
69
- * missing-asset error during execution.
70
- */
71
- function resolveFungibleAssetFromVault(
72
- account: Account,
73
- faucet: AccountId,
74
- amount: bigint,
75
- ): FungibleAsset {
76
- const faucetHex = faucet.toString();
77
- const assetFromVault = account
78
- .vault()
79
- .fungibleAssets()
80
- .find(asset => asset.faucetId().toString() === faucetHex);
81
-
82
- if (!assetFromVault) {
83
- return new FungibleAsset(faucet, amount);
84
- }
85
-
86
- return FungibleAsset.fromVaultKey(assetFromVault.vaultKey(), amount);
87
- }
88
-
89
77
  function buildP2idNote(
90
78
  sender: AccountId,
91
79
  recipient: AccountId,
92
80
  noteAssets: NoteAssets,
93
81
  noteType: NoteType,
94
82
  saltHex: string,
83
+ heights: P2ideHeightOptions = {},
95
84
  ): Note {
96
85
  const salt = WordType.fromHex(normalizeHexWord(saltHex));
97
86
  const serialNum = deriveP2idSerialNumber(salt);
98
87
 
99
- const noteScript = NoteScript.p2id();
100
- const noteStorage = new NoteStorage(new FeltArray([
88
+ const reclaimHeight = parseP2ideHeight('reclaimHeight', heights.reclaimHeight);
89
+ const timelockHeight = parseP2ideHeight('timelockHeight', heights.timelockHeight);
90
+ const isP2ide = reclaimHeight !== undefined || timelockHeight !== undefined;
91
+
92
+ // P2IDE storage layout (miden-standards `P2ideNoteStorage`): the P2ID
93
+ // storage plus reclaim/timelock heights as felts, 0 encoding "unset".
94
+ const noteScript = isP2ide ? NoteScript.p2ide() : NoteScript.p2id();
95
+ const storageFelts = [
101
96
  recipient.suffix(),
102
97
  recipient.prefix(),
103
- ]));
98
+ ];
99
+ if (isP2ide) {
100
+ storageFelts.push(
101
+ new Felt(BigInt(reclaimHeight ?? 0)),
102
+ new Felt(BigInt(timelockHeight ?? 0)),
103
+ );
104
+ }
105
+ const noteStorage = new NoteStorage(new FeltArray(storageFelts));
104
106
 
105
107
  const noteRecipient = new NoteRecipient(serialNum, noteScript, noteStorage);
106
108
  const noteTag = NoteTag.withAccountTarget(recipient);
@@ -117,26 +119,26 @@ function buildP2idNote(
117
119
  /**
118
120
  * Rebuilds the P2ID note a proposal creates, from its metadata fields. The
119
121
  * note is deterministic in the salt, so the resulting ID matches the note the
120
- * proposal produces on execution (issue #356). The asset is derived from the
121
- * account's current vault state, so call this before the transfer executes.
122
+ * proposal produces on execution. Since Miden 0.16, the asset
123
+ * callback flag is encoded in the faucet account ID.
122
124
  */
123
125
  export function buildP2idNoteFromMetadata(
124
126
  senderId: string,
125
127
  recipientId: string,
126
128
  faucetId: string,
127
129
  amount: bigint,
128
- account: Account,
129
130
  noteType: NoteType,
130
131
  saltHex: string,
132
+ heights: P2ideHeightOptions = {},
131
133
  ): Note {
132
134
  const sender = AccountId.fromHex(senderId);
133
135
  const recipient = AccountId.fromHex(recipientId);
134
136
  const faucet = AccountId.fromHex(faucetId);
135
137
 
136
- const asset = resolveFungibleAssetFromVault(account, faucet, amount);
138
+ const asset = new FungibleAsset(faucet, amount);
137
139
  const noteAssets = new NoteAssets([asset]);
138
140
 
139
- return buildP2idNote(sender, recipient, noteAssets, noteType, saltHex);
141
+ return buildP2idNote(sender, recipient, noteAssets, noteType, saltHex, heights);
140
142
  }
141
143
 
142
144
  export function buildP2idTransactionRequest(
@@ -144,7 +146,6 @@ export function buildP2idTransactionRequest(
144
146
  recipientId: string,
145
147
  faucetId: string,
146
148
  amount: bigint,
147
- account: Account,
148
149
  options: P2idTransactionOptions = {},
149
150
  ): { request: TransactionRequest; salt: Word } {
150
151
  const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
@@ -154,9 +155,9 @@ export function buildP2idTransactionRequest(
154
155
  recipientId,
155
156
  faucetId,
156
157
  amount,
157
- account,
158
158
  options.noteType ?? NoteType.Public,
159
159
  authSaltHex,
160
+ { reclaimHeight: options.reclaimHeight, timelockHeight: options.timelockHeight },
160
161
  );
161
162
 
162
163
  const outputNotes = new MidenArrays.NoteArray([note]);
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('@miden-sdk/miden-sdk', () => ({
4
+ AccountId: { fromHex: vi.fn() },
5
+ Word: {
6
+ newFromFelts: vi.fn((felts: unknown[]) => ({ felts })),
7
+ },
8
+ }));
9
+
10
+ vi.mock('../raw-client.js', () => ({
11
+ getRawMidenClient: vi.fn(),
12
+ }));
13
+
14
+ const { summarySalt } = await import('./summary.js');
15
+
16
+ describe('summarySalt', () => {
17
+ it('reads the salt from the trailing four user params', () => {
18
+ const summary = {
19
+ userParams: () => [0, 0, 0, 11, 22, 33, 44],
20
+ } as never;
21
+
22
+ expect(summarySalt(summary)).toEqual({ felts: [11, 22, 33, 44] });
23
+ });
24
+
25
+ it('ignores the leading three user params the auth component zeroes', () => {
26
+ const summary = {
27
+ userParams: () => [7, 8, 9, 11, 22, 33, 44],
28
+ } as never;
29
+
30
+ expect(summarySalt(summary)).toEqual({ felts: [11, 22, 33, 44] });
31
+ });
32
+ });