@openzeppelin/miden-multisig-client 0.15.2 → 0.16.0

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 (98) hide show
  1. package/README.md +25 -1
  2. package/dist/account/builder.d.ts +2 -1
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +1 -0
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/builder.test.js +2 -2
  7. package/dist/account/builder.test.js.map +1 -1
  8. package/dist/client.d.ts +9 -5
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +5 -5
  11. package/dist/client.js.map +1 -1
  12. package/dist/client.test.js +46 -15
  13. package/dist/client.test.js.map +1 -1
  14. package/dist/connectivity.d.ts +38 -0
  15. package/dist/connectivity.d.ts.map +1 -0
  16. package/dist/connectivity.js +97 -0
  17. package/dist/connectivity.js.map +1 -0
  18. package/dist/connectivity.test.d.ts +2 -0
  19. package/dist/connectivity.test.d.ts.map +1 -0
  20. package/dist/connectivity.test.js +61 -0
  21. package/dist/connectivity.test.js.map +1 -0
  22. package/dist/index.d.ts +9 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/multisig.d.ts +68 -7
  27. package/dist/multisig.d.ts.map +1 -1
  28. package/dist/multisig.js +90 -18
  29. package/dist/multisig.js.map +1 -1
  30. package/dist/multisig.test.js +214 -79
  31. package/dist/multisig.test.js.map +1 -1
  32. package/dist/proposal/metadata.d.ts.map +1 -1
  33. package/dist/proposal/metadata.js +12 -0
  34. package/dist/proposal/metadata.js.map +1 -1
  35. package/dist/proposal/metadata.test.js +49 -0
  36. package/dist/proposal/metadata.test.js.map +1 -1
  37. package/dist/raw-client.d.ts +2 -2
  38. package/dist/raw-client.d.ts.map +1 -1
  39. package/dist/raw-client.js +14 -4
  40. package/dist/raw-client.js.map +1 -1
  41. package/dist/raw-client.test.js +25 -3
  42. package/dist/raw-client.test.js.map +1 -1
  43. package/dist/transaction/consumeNotes.d.ts +6 -2
  44. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  45. package/dist/transaction/consumeNotes.js +0 -4
  46. package/dist/transaction/consumeNotes.js.map +1 -1
  47. package/dist/transaction/options.d.ts +3 -0
  48. package/dist/transaction/options.d.ts.map +1 -1
  49. package/dist/transaction/p2id.d.ts +20 -2
  50. package/dist/transaction/p2id.d.ts.map +1 -1
  51. package/dist/transaction/p2id.js +46 -3
  52. package/dist/transaction/p2id.js.map +1 -1
  53. package/dist/transaction/p2id.test.js +66 -6
  54. package/dist/transaction/p2id.test.js.map +1 -1
  55. package/dist/transaction/summary.d.ts +2 -1
  56. package/dist/transaction/summary.d.ts.map +1 -1
  57. package/dist/transaction/summary.js.map +1 -1
  58. package/dist/transaction/updateGuardian.d.ts +6 -2
  59. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  60. package/dist/transaction/updateGuardian.js.map +1 -1
  61. package/dist/transaction/updateProcedureThreshold.d.ts +7 -2
  62. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  63. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  64. package/dist/transaction/updateSigners.d.ts +7 -2
  65. package/dist/transaction/updateSigners.d.ts.map +1 -1
  66. package/dist/transaction/updateSigners.js.map +1 -1
  67. package/dist/transaction.d.ts +1 -1
  68. package/dist/transaction.d.ts.map +1 -1
  69. package/dist/transaction.js +1 -1
  70. package/dist/transaction.js.map +1 -1
  71. package/dist/types/proposal.d.ts +5 -0
  72. package/dist/types/proposal.d.ts.map +1 -1
  73. package/dist/types/proposal.js +3 -0
  74. package/dist/types/proposal.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/account/builder.test.ts +19 -11
  77. package/src/account/builder.ts +2 -1
  78. package/src/client.test.ts +67 -15
  79. package/src/client.ts +18 -10
  80. package/src/connectivity.test.ts +67 -0
  81. package/src/connectivity.ts +111 -0
  82. package/src/index.ts +20 -1
  83. package/src/multisig.test.ts +267 -80
  84. package/src/multisig.ts +109 -20
  85. package/src/proposal/metadata.test.ts +60 -0
  86. package/src/proposal/metadata.ts +14 -0
  87. package/src/raw-client.test.ts +41 -3
  88. package/src/raw-client.ts +15 -5
  89. package/src/transaction/consumeNotes.ts +11 -1
  90. package/src/transaction/options.ts +4 -0
  91. package/src/transaction/p2id.test.ts +98 -8
  92. package/src/transaction/p2id.ts +64 -4
  93. package/src/transaction/summary.ts +12 -0
  94. package/src/transaction/updateGuardian.ts +11 -1
  95. package/src/transaction/updateProcedureThreshold.ts +13 -1
  96. package/src/transaction/updateSigners.ts +13 -1
  97. package/src/transaction.ts +3 -0
  98. package/src/types/proposal.ts +9 -0
package/src/multisig.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * for proposal management.
6
6
  */
7
7
 
8
- import { GuardianHttpClient, type DeltaObject, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
8
+ import { GuardianHttpClient, type AbandonCandidateResponse, type AbandonStatus, type DeltaObject, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
9
9
  import type {
10
10
  ConsumableNote,
11
11
  ExportedProposal,
@@ -29,6 +29,7 @@ import {
29
29
  Endpoint,
30
30
  FeltArray,
31
31
  Note,
32
+ NoteType,
32
33
  RpcClient,
33
34
  Signature,
34
35
  TransactionRequest,
@@ -42,6 +43,8 @@ import {
42
43
  buildUpdateGuardianTransactionRequest,
43
44
  buildConsumeNotesTransactionRequest,
44
45
  buildP2idTransactionRequest,
46
+ parseP2idNoteType,
47
+ p2idNoteTypeToMetadata,
45
48
  } from './transaction.js';
46
49
  import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
47
50
  import {
@@ -73,7 +76,11 @@ import { AccountInspector } from './inspector.js';
73
76
  import { ProposalFactory } from './proposal/factory.js';
74
77
  import { ProposalMetadataCodec } from './proposal/metadata.js';
75
78
  import { ProposalSignatures } from './proposal/signatures.js';
76
- import { getRawMidenClient, getTransactionProver } from './raw-client.js';
79
+ import {
80
+ getRawMidenClient,
81
+ getTransactionProver,
82
+ requireMidenRpcEndpoint,
83
+ } from './raw-client.js';
77
84
 
78
85
  /**
79
86
  * Result of fetching account state from GUARDIAN.
@@ -138,7 +145,7 @@ export class Multisig {
138
145
  private readonly rawClientPromise: Promise<WasmWebClient>;
139
146
  private readonly transactionProver: TransactionProver | null;
140
147
  private readonly _accountId: string;
141
- private readonly midenRpcEndpoint?: string;
148
+ private readonly midenRpcEndpoint: string;
142
149
  private proposals: Map<string, Proposal> = new Map();
143
150
 
144
151
  constructor(
@@ -147,8 +154,8 @@ export class Multisig {
147
154
  guardian: GuardianHttpClient,
148
155
  signer: Signer,
149
156
  midenClient: MidenClient,
150
- accountId?: string,
151
- midenRpcEndpoint?: string
157
+ accountId: string | undefined,
158
+ midenRpcEndpoint: string
152
159
  ) {
153
160
  this.account = account;
154
161
  this.threshold = config.threshold;
@@ -162,15 +169,12 @@ export class Multisig {
162
169
  this.signer = signer;
163
170
  this.midenClient = midenClient;
164
171
  this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
165
- this.midenRpcEndpoint = midenRpcEndpoint;
166
- this.rawClientPromise = getRawMidenClient(midenClient, midenRpcEndpoint);
172
+ this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
167
173
  this.transactionProver = getTransactionProver(midenClient);
174
+ this.rawClientPromise = getRawMidenClient(midenClient, this.midenRpcEndpoint);
168
175
  }
169
176
 
170
177
  private getMidenRpcEndpoint(): string {
171
- if (!this.midenRpcEndpoint) {
172
- throw new Error('Missing Miden RPC endpoint in MultisigClient configuration');
173
- }
174
178
  return this.midenRpcEndpoint;
175
179
  }
176
180
 
@@ -213,6 +217,19 @@ export class Multisig {
213
217
  return this.signer.commitment;
214
218
  }
215
219
 
220
+ /**
221
+ * Resolve the account from the web client's store, falling back to the
222
+ * `account` snapshot when the store has no record.
223
+ *
224
+ * Transaction execution reads the store, and other flows (e.g. consume-notes
225
+ * finalize) update it without refreshing the snapshot, so vault lookups must
226
+ * source from the store to see the same state execution will.
227
+ */
228
+ async getStoreAccount(): Promise<Account> {
229
+ const webClient = await this.getRawClient();
230
+ return (await webClient.getAccount(AccountId.fromHex(this._accountId))) ?? this.account;
231
+ }
232
+
216
233
  /**
217
234
  * Maps a proposal type to the procedure that determines its threshold.
218
235
  */
@@ -286,7 +303,13 @@ export class Multisig {
286
303
  * Sync account state from GUARDIAN into the local Miden client store.
287
304
  *
288
305
  * If the GUARDIAN commitment differs from the local commitment (or the account
289
- * is missing locally), the local store is overwritten with the GUARDIAN state.
306
+ * is missing locally) and the GUARDIAN state is safe to import, the local store
307
+ * is overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
308
+ * local — e.g. the pushed execution delta has not been canonicalized yet
309
+ * (see OpenZeppelin/guardian#316) — the local state is already ahead and
310
+ * on-chain-verifiable, so it is kept as authoritative. Either way, config is
311
+ * refreshed from the resulting account so callers reading `Multisig.account`
312
+ * (e.g. the UI) observe the current state instead of a stale snapshot.
290
313
  */
291
314
  async syncState(): Promise<AccountState> {
292
315
  const state = await this.fetchState();
@@ -303,9 +326,10 @@ export class Multisig {
303
326
  if (!localAccount || localCommitment !== guardianCommitment) {
304
327
  const accountBytes = base64ToUint8Array(state.stateDataBase64);
305
328
  const incomingAccount = Account.deserialize(accountBytes);
306
- await this.ensureSafeToOverwriteLocalState(incomingAccount, localAccount);
307
- await webClient.newAccount(incomingAccount, true);
308
- accountForConfigRefresh = incomingAccount;
329
+ if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount)) {
330
+ await webClient.newAccount(incomingAccount, true);
331
+ accountForConfigRefresh = incomingAccount;
332
+ }
309
333
  }
310
334
 
311
335
  this.refreshConfigFromAccount(accountForConfigRefresh);
@@ -344,17 +368,37 @@ export class Multisig {
344
368
  };
345
369
  }
346
370
 
347
- private async ensureSafeToOverwriteLocalState(
371
+ /**
372
+ * Decide whether GUARDIAN-provided state may overwrite the local store.
373
+ *
374
+ * Returns `false` — rather than throwing — when the GUARDIAN state is simply
375
+ * *behind* local (lower nonce). That happens whenever the execution delta the
376
+ * client pushed has not been canonicalized by the GUARDIAN's background worker
377
+ * yet (see OpenZeppelin/guardian#316), or permanently if that candidate was
378
+ * discarded (#312 / #319). In that case the local account is already ahead and
379
+ * is independently verifiable against chain (`verifyStateCommitment`), so it is
380
+ * authoritative and must be kept, not clobbered; the caller keeps local and
381
+ * refreshes config from it.
382
+ *
383
+ * Still throws for genuine divergence: an incoming state at the *same* nonce as
384
+ * local but a different commitment, or an incoming state whose commitment does
385
+ * not match the on-chain commitment.
386
+ */
387
+ private async isSafeToOverwriteLocalState(
348
388
  incomingAccount: Account,
349
389
  localAccount?: Account,
350
- ): Promise<void> {
390
+ ): Promise<boolean> {
351
391
  if (localAccount) {
352
392
  const localNonce = localAccount.nonce().asInt();
353
393
  const incomingNonce = incomingAccount.nonce().asInt();
354
394
 
355
- if (incomingNonce <= localNonce) {
395
+ if (incomingNonce < localNonce) {
396
+ return false;
397
+ }
398
+
399
+ if (incomingNonce === localNonce) {
356
400
  throw new Error(
357
- `Refusing to overwrite local state: incoming nonce ${incomingNonce.toString()} is not greater than local nonce ${localNonce.toString()} for account ${this._accountId}`
401
+ `Refusing to overwrite local state: incoming nonce ${incomingNonce.toString()} equals local nonce ${localNonce.toString()} but commitments differ for account ${this._accountId}`
358
402
  );
359
403
  }
360
404
  }
@@ -362,7 +406,7 @@ export class Multisig {
362
406
  const accountId = AccountId.fromHex(this._accountId);
363
407
  const onChainCommitment = await this.getOnChainCommitment(accountId);
364
408
  if (!onChainCommitment) {
365
- return;
409
+ return true;
366
410
  }
367
411
 
368
412
  const incomingCommitment = normalizeHexWord(incomingAccount.to_commitment().toHex());
@@ -371,6 +415,8 @@ export class Multisig {
371
415
  `Refusing to overwrite local state: incoming commitment does not match on-chain commitment for account ${this._accountId}`
372
416
  );
373
417
  }
418
+
419
+ return true;
374
420
  }
375
421
 
376
422
  private async getOnChainCommitment(accountId: AccountId): Promise<string | null> {
@@ -807,23 +853,30 @@ export class Multisig {
807
853
  * @param faucetId - Faucet/token account ID (hex string)
808
854
  * @param amount - Amount to send
809
855
  * @param nonce - Optional proposal nonce (defaults to Date.now())
856
+ * @param options - Optional settings; `noteType` selects the created note's
857
+ * visibility (defaults to `NoteType.Public`, issue #322)
810
858
  */
811
859
  async createP2idProposal(
812
860
  recipientId: string,
813
861
  faucetId: string,
814
862
  amount: bigint,
815
863
  nonce?: number,
864
+ options: { noteType?: NoteType } = {},
816
865
  ): Promise<Proposal> {
817
866
  const webClient = await this.getRawClient();
818
867
  if (amount <= 0n) {
819
868
  throw new Error('Amount must be greater than 0');
820
869
  }
821
870
 
871
+ const account = await this.getStoreAccount();
872
+
822
873
  const { request, salt } = buildP2idTransactionRequest(
823
874
  this._accountId,
824
875
  recipientId,
825
876
  faucetId,
826
877
  amount,
878
+ account,
879
+ { noteType: options.noteType },
827
880
  );
828
881
 
829
882
  const summary = await executeForSummary(webClient, this._accountId, request);
@@ -837,6 +890,8 @@ export class Multisig {
837
890
  recipientId,
838
891
  faucetId,
839
892
  amount: amount.toString(),
893
+ // Omitted for public notes so the wire shape matches pre-#322 proposals.
894
+ noteType: p2idNoteTypeToMetadata(options.noteType),
840
895
  description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
841
896
  };
842
897
 
@@ -902,6 +957,38 @@ export class Multisig {
902
957
  *
903
958
  * @param proposalId - The proposal commitment/ID (this is also what gets signed)
904
959
  */
960
+ /**
961
+ * Request abandonment of a pending canonicalization candidate whose
962
+ * transaction will never land on-chain (issue #319) — e.g. after an
963
+ * approved transaction died client-side (RPC submit failure, prover
964
+ * timeout, crash).
965
+ *
966
+ * Records an abandon *intent* on GUARDIAN: the account stays locked
967
+ * until the guardian's canonicalization worker confirms over a short
968
+ * quarantine (typically well under a minute) that the transaction did
969
+ * not land, then releases the account. Poll {@link abandonStatus} for
970
+ * the resolution.
971
+ *
972
+ * `nonce` pins the exact candidate to release; it is the nonce the
973
+ * proposal was pushed with. Retries are idempotent and preserve the
974
+ * original request timestamp. Refused with `GUARDIAN_CANDIDATE_LANDED`
975
+ * (409) when the transaction actually landed.
976
+ */
977
+ async abandonCandidate(nonce: number): Promise<AbandonCandidateResponse> {
978
+ return this.guardian.abandonCandidate(this._accountId, nonce);
979
+ }
980
+
981
+ /**
982
+ * Poll the resolution of an abandon request made with
983
+ * {@link abandonCandidate}: `'waiting'` while the quarantine runs,
984
+ * `'landed'` if the transaction landed after all, `'abandoned'` once
985
+ * the account is released, `'unexpected'` for any state no abandon
986
+ * flow produces.
987
+ */
988
+ async abandonStatus(nonce: number): Promise<AbandonStatus> {
989
+ return this.guardian.abandonStatus(this._accountId, nonce);
990
+ }
991
+
905
992
  async signProposal(proposalId: string): Promise<Proposal> {
906
993
  const normalizedProposalId = normalizeHexWord(proposalId);
907
994
  const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
@@ -1678,12 +1765,14 @@ export class Multisig {
1678
1765
  throw new UnsupportedMetadataVersionError(version);
1679
1766
  }
1680
1767
  case 'p2id': {
1768
+ const account = await this.getStoreAccount();
1681
1769
  const { request } = buildP2idTransactionRequest(
1682
1770
  this._accountId,
1683
1771
  metadata.recipientId,
1684
1772
  metadata.faucetId,
1685
1773
  BigInt(metadata.amount),
1686
- { salt, signatureAdviceMap }
1774
+ account,
1775
+ { salt, signatureAdviceMap, noteType: parseP2idNoteType(metadata.noteType) }
1687
1776
  );
1688
1777
  return request;
1689
1778
  }
@@ -4,6 +4,7 @@ import { ProposalMetadataCodec } from './metadata.js';
4
4
  import type {
5
5
  ConsumeNotesProposalMetadata,
6
6
  CustomProposalMetadata,
7
+ P2IdProposalMetadata,
7
8
  } from '../types/proposal.js';
8
9
 
9
10
  describe('ProposalMetadataCodec consume_notes v2 round-trip (issue #229)', () => {
@@ -97,3 +98,62 @@ describe('ProposalMetadataCodec custom proposal types (issue #266)', () => {
97
98
  expect(back.targetThreshold).toBe(2);
98
99
  });
99
100
  });
101
+
102
+ describe('ProposalMetadataCodec p2id noteType (issue #322)', () => {
103
+ const baseWire: GuardianProposalMetadata = {
104
+ proposalType: 'p2id',
105
+ recipientId: '0xrecipient',
106
+ faucetId: '0xfaucet',
107
+ amount: '1000',
108
+ };
109
+
110
+ it('round-trips a private noteType through the codec', () => {
111
+ const md = ProposalMetadataCodec.fromGuardian({
112
+ ...baseWire,
113
+ noteType: 'private',
114
+ }) as P2IdProposalMetadata;
115
+ expect(md.noteType).toBe('private');
116
+
117
+ const wire = ProposalMetadataCodec.toGuardian(md);
118
+ expect(wire.noteType).toBe('private');
119
+ });
120
+
121
+ it('leaves noteType absent for legacy proposals (=> public)', () => {
122
+ const md = ProposalMetadataCodec.fromGuardian(baseWire) as P2IdProposalMetadata;
123
+ expect(md.noteType).toBeUndefined();
124
+ expect(ProposalMetadataCodec.toGuardian(md).noteType).toBeUndefined();
125
+ });
126
+
127
+ it('canonicalizes an explicit public noteType to absent on encode', () => {
128
+ const md = {
129
+ proposalType: 'p2id',
130
+ description: '',
131
+ recipientId: '0xrecipient',
132
+ faucetId: '0xfaucet',
133
+ amount: '1000',
134
+ noteType: 'public',
135
+ } as P2IdProposalMetadata;
136
+
137
+ // toGuardian omits the field so a public note keeps the pre-#322 wire
138
+ // shape and matches the Rust encoder, even if handed an explicit 'public'.
139
+ expect(ProposalMetadataCodec.toGuardian(md).noteType).toBeUndefined();
140
+ });
141
+
142
+ it('fromGuardian rejects an unsupported noteType', () => {
143
+ expect(() =>
144
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, noteType: 'encrypted' }),
145
+ ).toThrow(/unsupported noteType/);
146
+ });
147
+
148
+ it('validate rejects an unsupported noteType', () => {
149
+ const md = {
150
+ proposalType: 'p2id',
151
+ description: '',
152
+ recipientId: '0xrecipient',
153
+ faucetId: '0xfaucet',
154
+ amount: '1000',
155
+ noteType: 'encrypted',
156
+ } as unknown as P2IdProposalMetadata;
157
+ expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported noteType/);
158
+ });
159
+ });
@@ -1,6 +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
5
 
5
6
  export class ProposalMetadataCodec {
6
7
  static toGuardian(metadata: ProposalMetadata): GuardianProposalMetadata {
@@ -25,6 +26,10 @@ export class ProposalMetadataCodec {
25
26
  recipientId: metadata.recipientId,
26
27
  faucetId: metadata.faucetId,
27
28
  amount: metadata.amount,
29
+ // Canonicalize: emit note_type only when private, so a public note
30
+ // keeps the pre-#322 wire shape and matches the Rust encoder (which
31
+ // round-trips through the NoteType enum). Absent => public.
32
+ noteType: metadata.noteType === 'private' ? 'private' : undefined,
28
33
  };
29
34
  case 'switch_guardian':
30
35
  return {
@@ -70,12 +75,18 @@ export class ProposalMetadataCodec {
70
75
  if (!guardian.recipientId || !guardian.faucetId || !guardian.amount) {
71
76
  throw new Error('p2id proposal is missing required metadata fields');
72
77
  }
78
+ if (guardian.noteType !== undefined && !isP2idNoteVisibility(guardian.noteType)) {
79
+ throw new Error(
80
+ `p2id proposal has unsupported noteType '${guardian.noteType}': expected 'public' or 'private'`,
81
+ );
82
+ }
73
83
  return {
74
84
  ...base,
75
85
  proposalType: 'p2id',
76
86
  recipientId: guardian.recipientId,
77
87
  faucetId: guardian.faucetId,
78
88
  amount: guardian.amount,
89
+ noteType: guardian.noteType,
79
90
  };
80
91
  case 'consume_notes':
81
92
  if (!guardian.noteIds || guardian.noteIds.length === 0) {
@@ -168,6 +179,9 @@ export class ProposalMetadataCodec {
168
179
  if (!metadata.recipientId || !metadata.faucetId || !metadata.amount) {
169
180
  throw new Error('p2id proposal metadata is incomplete');
170
181
  }
182
+ if (metadata.noteType !== undefined && !isP2idNoteVisibility(metadata.noteType)) {
183
+ throw new Error(`p2id proposal has unsupported noteType '${metadata.noteType}'`);
184
+ }
171
185
  return metadata;
172
186
  case 'custom':
173
187
  // Custom proposals are opaque to the SDK; nothing to validate beyond
@@ -14,7 +14,7 @@ import {
14
14
  compileTxScript,
15
15
  getRawMidenClient,
16
16
  getTransactionProver,
17
- resolveMidenRpcEndpoint,
17
+ requireConfigValue,
18
18
  } from './raw-client.js';
19
19
 
20
20
  describe('raw-client', () => {
@@ -22,8 +22,46 @@ describe('raw-client', () => {
22
22
  mockCreateClient.mockReset();
23
23
  });
24
24
 
25
- it('defaults RPC endpoint resolution to devnet', () => {
26
- expect(resolveMidenRpcEndpoint()).toBe('https://rpc.devnet.miden.io');
25
+ it.each([undefined, null, 42, {}, []])(
26
+ 'rejects non-string configuration value %j consistently',
27
+ (value) => {
28
+ expect(() => requireConfigValue('guardianEndpoint', value)).toThrow(
29
+ 'missing required configuration: guardianEndpoint',
30
+ );
31
+ },
32
+ );
33
+
34
+ it('trims surrounding whitespace from configuration values', () => {
35
+ expect(requireConfigValue('guardianEndpoint', ' http://localhost:3000\n')).toBe(
36
+ 'http://localhost:3000',
37
+ );
38
+ });
39
+
40
+ it('rejects shadow client creation without an RPC endpoint', async () => {
41
+ const client = {
42
+ accounts: {},
43
+ sync: vi.fn(),
44
+ defaultProver: null,
45
+ storeIdentifier: vi.fn(() => 'browser-db'),
46
+ };
47
+
48
+ await expect(getRawMidenClient(client as any)).rejects.toThrow(
49
+ 'missing required configuration: midenRpcEndpoint',
50
+ );
51
+ await expect(getRawMidenClient(client as any, ' ')).rejects.toThrow(
52
+ 'missing required configuration: midenRpcEndpoint',
53
+ );
54
+ expect(mockCreateClient).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it('returns an injected raw web client without needing an endpoint', async () => {
58
+ const rawClient = {
59
+ executeTransaction: vi.fn(),
60
+ proveTransaction: vi.fn(),
61
+ };
62
+
63
+ await expect(getRawMidenClient(rawClient as any)).resolves.toBe(rawClient);
64
+ expect(mockCreateClient).not.toHaveBeenCalled();
27
65
  });
28
66
 
29
67
  it('returns the default prover from a public MidenClient', () => {
package/src/raw-client.ts CHANGED
@@ -5,8 +5,6 @@ import {
5
5
  WasmWebClient,
6
6
  } from '@miden-sdk/miden-sdk';
7
7
 
8
- export const DEFAULT_MIDEN_RPC_URL = 'https://rpc.devnet.miden.io';
9
-
10
8
  export type RawClientSource = MidenClient | WasmWebClient;
11
9
  export interface ScriptLibrarySource {
12
10
  namespace: string;
@@ -16,8 +14,19 @@ export interface ScriptLibrarySource {
16
14
 
17
15
  const rawClientCache = new WeakMap<MidenClient, Promise<WasmWebClient>>();
18
16
 
19
- export function resolveMidenRpcEndpoint(endpoint?: string): string {
20
- return endpoint ?? DEFAULT_MIDEN_RPC_URL;
17
+ export function requireConfigValue(field: string, value?: unknown): string {
18
+ if (typeof value !== 'string') {
19
+ throw new Error(`missing required configuration: ${field}`);
20
+ }
21
+ const normalizedValue = value.trim();
22
+ if (normalizedValue === '') {
23
+ throw new Error(`missing required configuration: ${field}`);
24
+ }
25
+ return normalizedValue;
26
+ }
27
+
28
+ export function requireMidenRpcEndpoint(endpoint?: string): string {
29
+ return requireConfigValue('midenRpcEndpoint', endpoint);
21
30
  }
22
31
 
23
32
  function isPublicMidenClient(client: RawClientSource): client is MidenClient {
@@ -37,8 +46,9 @@ export async function getRawMidenClient(
37
46
  return cached;
38
47
  }
39
48
 
49
+ const endpoint = requireMidenRpcEndpoint(rpcUrl);
40
50
  const rawClient = WasmWebClient.createClient(
41
- resolveMidenRpcEndpoint(rpcUrl),
51
+ endpoint,
42
52
  undefined,
43
53
  undefined,
44
54
  await client.storeIdentifier(),
@@ -15,7 +15,7 @@ import { LegacyConsumeNotesNoteMissingError } from '../multisig/consumeNotesErro
15
15
  import { getRawMidenClient } from '../raw-client.js';
16
16
  import { normalizeHexWord } from '../utils/encoding.js';
17
17
  import { randomWord } from '../utils/random.js';
18
- import type { SignatureOptions } from './options.js';
18
+ import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
19
19
 
20
20
  /**
21
21
  * Build a consume-notes request from loaded `Note` objects (no local-store
@@ -57,6 +57,16 @@ export function buildConsumeNotesTransactionRequestFromNotes(
57
57
  * Legacy/creation adapter: fetches notes from the local store and delegates
58
58
  * to the from-notes variant. v2 verification MUST NOT call this.
59
59
  */
60
+ export function buildConsumeNotesTransactionRequest(
61
+ client: MidenClient,
62
+ noteIds: string[],
63
+ options: MidenClientSignatureOptions,
64
+ ): Promise<{ request: TransactionRequest; salt: Word }>;
65
+ export function buildConsumeNotesTransactionRequest(
66
+ client: WasmWebClient,
67
+ noteIds: string[],
68
+ options?: SignatureOptions,
69
+ ): Promise<{ request: TransactionRequest; salt: Word }>;
60
70
  export async function buildConsumeNotesTransactionRequest(
61
71
  client: MidenClient | WasmWebClient,
62
72
  noteIds: string[],
@@ -7,3 +7,7 @@ export interface SignatureOptions {
7
7
  signatureScheme?: SignatureScheme;
8
8
  midenRpcEndpoint?: string;
9
9
  }
10
+
11
+ export interface MidenClientSignatureOptions extends SignatureOptions {
12
+ midenRpcEndpoint: string;
13
+ }