@miden-sdk/miden-sdk 0.15.0-alpha.4 → 0.15.0-alpha.6

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 (59) hide show
  1. package/README.md +139 -9
  2. package/dist/mt/Cargo-C9UbiAcT.js +26202 -0
  3. package/dist/mt/Cargo-C9UbiAcT.js.map +1 -0
  4. package/dist/{api-types.d.ts → mt/api-types.d.ts} +160 -6
  5. package/dist/mt/assets/miden_client_web.wasm +0 -0
  6. package/dist/mt/crates/miden_client_web.d.ts +4821 -0
  7. package/dist/mt/eager.js +38 -0
  8. package/dist/mt/eager.js.map +1 -0
  9. package/dist/{index.d.ts → mt/index.d.ts} +6 -3
  10. package/dist/mt/index.js +3811 -0
  11. package/dist/mt/index.js.map +1 -0
  12. package/dist/{wasm.js → mt/wasm.js} +1 -1
  13. package/dist/mt/wasm.js.map +1 -0
  14. package/dist/mt/workerHelpers.js +28 -0
  15. package/dist/mt/workers/Cargo-C9UbiAcT-Z344cyB1.js +26203 -0
  16. package/dist/mt/workers/Cargo-C9UbiAcT-Z344cyB1.js.map +1 -0
  17. package/dist/mt/workers/assets/miden_client_web.wasm +0 -0
  18. package/dist/mt/workers/web-client-methods-worker.js +26996 -0
  19. package/dist/mt/workers/web-client-methods-worker.js.map +1 -0
  20. package/dist/{workers → mt/workers}/web-client-methods-worker.module.js +71 -2
  21. package/dist/mt/workers/web-client-methods-worker.module.js.map +1 -0
  22. package/dist/mt/workers/workerHelpers.js +28 -0
  23. package/dist/{Cargo-CVlXCH_2.js → st/Cargo-OZMlHpic.js} +721 -496
  24. package/dist/st/Cargo-OZMlHpic.js.map +1 -0
  25. package/dist/st/api-types.d.ts +1144 -0
  26. package/dist/{workers → st}/assets/miden_client_web.wasm +0 -0
  27. package/dist/{crates → st/crates}/miden_client_web.d.ts +357 -197
  28. package/dist/st/docs-entry.d.ts +38 -0
  29. package/dist/{eager.js → st/eager.js} +2 -2
  30. package/dist/st/eager.js.map +1 -0
  31. package/dist/st/index.d.ts +183 -0
  32. package/dist/{index.js → st/index.js} +723 -284
  33. package/dist/st/index.js.map +1 -0
  34. package/dist/st/wasm.js +23 -0
  35. package/dist/st/wasm.js.map +1 -0
  36. package/dist/{workers/Cargo-CVlXCH_2-CWA-5vlh.js → st/workers/Cargo-OZMlHpic-DZjvJlWc.js} +721 -496
  37. package/dist/st/workers/Cargo-OZMlHpic-DZjvJlWc.js.map +1 -0
  38. package/dist/{assets → st/workers/assets}/miden_client_web.wasm +0 -0
  39. package/dist/{workers → st/workers}/web-client-methods-worker.js +792 -498
  40. package/dist/st/workers/web-client-methods-worker.js.map +1 -0
  41. package/dist/st/workers/web-client-methods-worker.module.js +628 -0
  42. package/dist/st/workers/web-client-methods-worker.module.js.map +1 -0
  43. package/js/client.js +190 -7
  44. package/js/node/napi-compat.js +22 -3
  45. package/js/node-index.js +0 -1
  46. package/js/resources/accounts.js +4 -6
  47. package/js/resources/transactions.js +138 -1
  48. package/lazy/package.json +2 -2
  49. package/mt/lazy/package.json +4 -0
  50. package/mt/package.json +4 -0
  51. package/package.json +30 -15
  52. package/dist/Cargo-CVlXCH_2.js.map +0 -1
  53. package/dist/eager.js.map +0 -1
  54. package/dist/index.js.map +0 -1
  55. package/dist/wasm.js.map +0 -1
  56. package/dist/workers/Cargo-CVlXCH_2-CWA-5vlh.js.map +0 -1
  57. package/dist/workers/web-client-methods-worker.js.map +0 -1
  58. package/dist/workers/web-client-methods-worker.module.js.map +0 -1
  59. /package/dist/{docs-entry.d.ts → mt/docs-entry.d.ts} +0 -0
@@ -1,9 +1,10 @@
1
1
  import loadWasm from './wasm.js';
2
- export { Account, AccountArray, AccountBuilder, AccountBuilderResult, AccountCode, AccountComponent, AccountComponentCode, AccountDelta, AccountFile, AccountHeader, AccountId, AccountIdArray, AccountInterface, AccountProof, AccountReader, AccountStatus, AccountStorage, AccountStorageDelta, AccountStorageMode, AccountStorageRequirements, AccountVaultDelta, Address, AdviceInputs, AdviceMap, AssetVault, AuthFalcon512RpoMultisigConfig, AuthSecretKey, BasicFungibleFaucetComponent, BlockHeader, CodeBuilder, CommittedNote, ConsumableNoteRecord, Endpoint, ExecutedTransaction, Felt, FeltArray, FetchedAccount, FetchedNote, FlattenedU8Vec, ForeignAccount, ForeignAccountArray, FungibleAsset, FungibleAssetDelta, FungibleAssetDeltaItem, GetProceduresResultItem, InputNote, InputNoteRecord, InputNoteState, InputNotes, IntoUnderlyingByteSource, IntoUnderlyingSink, IntoUnderlyingSource, JsAccountUpdate, JsStateSyncUpdate, JsStorageMapEntry, JsStorageSlot, JsVaultAsset, Library, MerklePath, NetworkId, NetworkNoteStatusInfo, NetworkType, Note, NoteAndArgs, NoteAndArgsArray, NoteArray, NoteAssets, NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme, NoteConsumability, NoteConsumptionStatus, NoteDetails, NoteDetailsAndTag, NoteDetailsAndTagArray, NoteExecutionHint, NoteExportFormat, NoteFile, NoteFilter, NoteFilterTypes, NoteHeader, NoteId, NoteIdAndArgs, NoteIdAndArgsArray, NoteInclusionProof, NoteLocation, NoteMetadata, NoteRecipient, NoteRecipientArray, NoteScript, NoteStorage, NoteSyncBlock, NoteSyncInfo, NoteTag, NoteType, OutputNote, OutputNoteArray, OutputNoteRecord, OutputNoteState, OutputNotes, Package, PartialNote, Poseidon2, ProcedureThreshold, Program, ProvenTransaction, PublicKey, RpcClient, Rpo256, SerializedInputNoteData, SerializedOutputNoteData, SerializedTransactionData, Signature, SigningInputs, SigningInputsType, SlotAndKeys, SparseMerklePath, StorageMap, StorageMapEntry, StorageMapEntryJs, StorageMapInfo, StorageMapUpdate, StorageSlot, StorageSlotArray, SyncSummary, TestUtils, TokenSymbol, TransactionArgs, TransactionFilter, TransactionId, TransactionProver, TransactionRecord, TransactionRequest, TransactionRequestBuilder, TransactionResult, TransactionScript, TransactionScriptInputPair, TransactionScriptInputPairArray, TransactionStatus, TransactionStoreUpdate, TransactionSummary, WebClient, WebKeystoreApi, Word, createAuthFalcon512RpoMultisig, exportStore, importStore, initSync, setupLogging } from './Cargo-CVlXCH_2.js';
2
+ export { Account, AccountArray, AccountBuilder, AccountBuilderResult, AccountCode, AccountComponent, AccountComponentCode, AccountDelta, AccountFile, AccountHeader, AccountId, AccountIdArray, AccountInterface, AccountProof, AccountReader, AccountStatus, AccountStorage, AccountStorageDelta, AccountStorageMode, AccountStorageRequirements, AccountVaultDelta, Address, AdviceInputs, AdviceMap, AssetVault, AuthFalcon512RpoMultisigConfig, AuthSecretKey, BasicFungibleFaucetComponent, BlockHeader, CodeBuilder, CommittedNote, ConsumableNoteRecord, Endpoint, ExecutedTransaction, Felt, FeltArray, FetchedAccount, FetchedNote, FlattenedU8Vec, ForeignAccount, ForeignAccountArray, FungibleAsset, FungibleAssetDelta, FungibleAssetDeltaItem, GetProceduresResultItem, InputNote, InputNoteRecord, InputNoteState, InputNotes, IntoUnderlyingByteSource, IntoUnderlyingSink, IntoUnderlyingSource, JsAccountUpdate, JsStateSyncUpdate, JsStorageMapEntry, JsStorageSlot, JsVaultAsset, Library, MerklePath, NetworkId, NetworkNoteStatusInfo, NetworkType, Note, NoteAndArgs, NoteAndArgsArray, NoteArray, NoteAssets, NoteAttachment, NoteAttachmentScheme, NoteConsumability, NoteConsumptionStatus, NoteDetails, NoteDetailsAndTag, NoteDetailsAndTagArray, NoteExecutionHint, NoteExportFormat, NoteFile, NoteFilter, NoteFilterTypes, NoteHeader, NoteId, NoteIdAndArgs, NoteIdAndArgsArray, NoteInclusionProof, NoteLocation, NoteMetadata, NoteRecipient, NoteRecipientArray, NoteScript, NoteStorage, NoteSyncBlock, NoteSyncInfo, NoteTag, NoteType, OutputNote, OutputNoteArray, OutputNoteRecord, OutputNoteState, OutputNotes, Package, PartialNote, Poseidon2, ProcedureThreshold, Program, ProvenTransaction, PublicKey, RpcClient, Rpo256, SerializedInputNoteData, SerializedOutputNoteData, SerializedTransactionData, Signature, SigningInputs, SigningInputsType, SlotAndKeys, SparseMerklePath, StorageMap, StorageMapEntry, StorageMapEntryJs, StorageMapInfo, StorageMapUpdate, StorageSlot, StorageSlotArray, SyncSummary, TestUtils, TokenSymbol, TransactionArgs, TransactionFilter, TransactionId, TransactionProver, TransactionRecord, TransactionRequest, TransactionRequestBuilder, TransactionResult, TransactionScript, TransactionScriptInputPair, TransactionScriptInputPairArray, TransactionStatus, TransactionStoreUpdate, TransactionSummary, WebClient, WebKeystoreApi, Word, createAuthFalcon512RpoMultisig, exportStore, importStore, initSync, sequentialSumBench, setupLogging } from './Cargo-OZMlHpic.js';
3
3
 
4
4
  const WorkerAction = Object.freeze({
5
5
  INIT: "init",
6
6
  INIT_MOCK: "initMock",
7
+ INIT_THREAD_POOL: "initThreadPool",
7
8
  CALL_METHOD: "callMethod",
8
9
  EXECUTE_CALLBACK: "executeCallback",
9
10
  });
@@ -25,20 +26,24 @@ const MethodName = Object.freeze({
25
26
  SUBMIT_NEW_TRANSACTION_WITH_PROVER_MOCK: "submitNewTransactionWithProverMock",
26
27
  SYNC_STATE: "syncState",
27
28
  SYNC_STATE_MOCK: "syncStateMock",
29
+ SYNC_CHAIN: "syncChain",
30
+ SYNC_CHAIN_MOCK: "syncChainMock",
31
+ SYNC_NOTE_TRANSPORT: "syncNoteTransport",
32
+ SYNC_NOTE_TRANSPORT_MOCK: "syncNoteTransportMock",
28
33
  });
29
34
 
30
35
  /**
31
36
  * Sync Lock Module
32
37
  *
33
- * Provides coordination for concurrent syncState() calls using the Web Locks API
34
- * with an in-process mutex fallback for older browsers.
38
+ * Coordinates concurrent sync calls using the Web Locks API.
35
39
  *
36
40
  * Behavior:
37
- * - Uses "coalescing": if a sync is in progress, subsequent callers wait and receive
38
- * the same result
39
- * - Web Locks for cross-tab coordination (Chrome 69+, Safari 15.4+)
40
- * - In-process mutex fallback when Web Locks unavailable
41
- * - Optional timeout support
41
+ * - Same-method coalescing: if a sync of the same method is in progress,
42
+ * subsequent callers share its result promise
43
+ * - Different-method serialization: different methods (e.g. syncState vs
44
+ * syncNoteTransport) wait for each other via the Web Lock, or via an
45
+ * in-process per-dbId promise chain when Web Locks are unavailable
46
+ * - Web Locks also serialize across tabs (Chrome 69+, Safari 15.4+)
42
47
  */
43
48
 
44
49
  /**
@@ -52,195 +57,86 @@ function hasWebLocks() {
52
57
  );
53
58
  }
54
59
 
55
- /**
56
- * Internal state for tracking in-progress syncs and waiters per database.
57
- */
58
- const syncStates = new Map();
60
+ // Coalesce map keyed by `${dbId}:${methodId}` -> in-flight promise.
61
+ const inFlight = new Map();
59
62
 
60
- /**
61
- * Get or create sync state for a database.
62
- */
63
- function getSyncState(dbId) {
64
- let state = syncStates.get(dbId);
65
- if (!state) {
66
- state = {
67
- inProgress: false,
68
- result: null,
69
- error: null,
70
- waiters: [],
71
- releaseLock: null,
72
- syncGeneration: 0,
73
- };
74
- syncStates.set(dbId, state);
75
- }
76
- return state;
77
- }
63
+ // Per-dbId promise tail used to serialize cross-method calls when Web Locks
64
+ // are unavailable. Each new task chains onto the current tail so different
65
+ // methods on the same dbId run sequentially within the tab.
66
+ const fallbackTails = new Map();
78
67
 
79
68
  /**
80
- * Acquire a sync lock for the given database.
81
- *
82
- * If a sync is already in progress:
83
- * - Returns { acquired: false, coalescedResult } after waiting for the result
84
- *
85
- * If no sync is in progress:
86
- * - Returns { acquired: true } and the caller should perform the sync,
87
- * then call releaseSyncLock() or releaseSyncLockWithError()
69
+ * Build the coalesce-map key for an in-flight sync of `(dbId, methodId)`.
88
70
  *
89
- * @param {string} dbId - The database ID to lock
90
- * @param {number} timeoutMs - Optional timeout in milliseconds (0 = no timeout)
91
- * @returns {Promise<{acquired: boolean, coalescedResult?: any}>}
71
+ * @param {string} dbId
72
+ * @param {string} methodId
73
+ * @returns {string}
92
74
  */
93
- async function acquireSyncLock(dbId, timeoutMs = 0) {
94
- const state = getSyncState(dbId);
95
-
96
- // If a sync is already in progress, wait for it to complete (coalescing)
97
- if (state.inProgress) {
98
- return new Promise((resolve, reject) => {
99
- let timeoutId;
100
- if (timeoutMs > 0) {
101
- timeoutId = setTimeout(() => {
102
- const idx = state.waiters.findIndex((w) => w.resolve === onResult);
103
- if (idx !== -1) {
104
- state.waiters.splice(idx, 1);
105
- }
106
- reject(new Error("Sync lock acquisition timed out"));
107
- }, timeoutMs);
108
- }
109
-
110
- const onResult = (result) => {
111
- /* v8 ignore next 1 -- timeoutId only set when timeoutMs>0 AND another sync is in progress; combo rare in tests */
112
- if (timeoutId) clearTimeout(timeoutId);
113
- resolve({ acquired: false, coalescedResult: result });
114
- };
115
-
116
- const onError = (error) => {
117
- if (timeoutId) clearTimeout(timeoutId);
118
- reject(error);
119
- };
120
-
121
- state.waiters.push({ resolve: onResult, reject: onError });
122
- });
123
- }
124
-
125
- // Mark sync as in progress and increment generation
126
- state.inProgress = true;
127
- state.result = null;
128
- state.error = null;
129
- state.syncGeneration++;
130
- const currentGeneration = state.syncGeneration;
131
-
132
- // Try to acquire Web Lock if available
133
- if (hasWebLocks()) {
134
- const lockName = `miden-sync-${dbId}`;
135
-
136
- return new Promise((resolve, reject) => {
137
- let timeoutId;
138
- let timedOut = false;
139
-
140
- if (timeoutMs > 0) {
141
- timeoutId = setTimeout(() => {
142
- timedOut = true;
143
- if (state.syncGeneration === currentGeneration) {
144
- state.inProgress = false;
145
- const error = new Error("Sync lock acquisition timed out");
146
- for (const waiter of state.waiters) {
147
- waiter.reject(error);
148
- }
149
- state.waiters = [];
150
- }
151
- reject(new Error("Sync lock acquisition timed out"));
152
- }, timeoutMs);
153
- }
154
-
155
- navigator.locks
156
- .request(lockName, { mode: "exclusive" }, async () => {
157
- /* v8 ignore next 3 -- race: lock granted after timeout or newer generation */
158
- if (timedOut || state.syncGeneration !== currentGeneration) {
159
- return;
160
- }
161
-
162
- if (timeoutId) clearTimeout(timeoutId);
163
-
164
- return new Promise((releaseLock) => {
165
- state.releaseLock = releaseLock;
166
- resolve({ acquired: true });
167
- });
168
- })
169
- .catch((err) => {
170
- /* v8 ignore next 5 -- catch path requires Web Locks rejection combined with
171
- optional timeout; tested via "rejects when Web Locks request rejects" but
172
- the timeoutId-set branch needs Web Locks + timeout simultaneously */
173
- if (timeoutId) clearTimeout(timeoutId);
174
- if (state.syncGeneration === currentGeneration) {
175
- state.inProgress = false;
176
- }
177
- reject(err instanceof Error ? err : new Error(String(err)));
178
- });
179
- });
180
- } else {
181
- // Fallback: no Web Locks, just use in-process state
182
- return { acquired: true };
183
- }
75
+ function coalesceKey(dbId, methodId) {
76
+ return `${dbId}:${methodId}`;
184
77
  }
185
78
 
186
79
  /**
187
- * Release the sync lock with a successful result.
80
+ * Run `fn` while holding the per-db Web Lock. When Web Locks are unavailable,
81
+ * serializes `fn` against any other in-flight call on the same `dbId` via an
82
+ * in-process promise chain — the wasm-bindgen `WebClient` uses a synchronous
83
+ * `RefCell` for interior mutability in the browser, so overlapping
84
+ * cross-method borrows would throw "recursive use of an object detected
85
+ * which would lead to unsafe aliasing in rust".
188
86
  *
189
- * This notifies all waiting callers with the result and releases the lock.
190
- *
191
- * @param {string} dbId - The database ID
192
- * @param {any} result - The sync result to pass to waiters
87
+ * @param {string} dbId
88
+ * @param {() => Promise<T>} fn
89
+ * @returns {Promise<T>}
90
+ * @template T
193
91
  */
194
- function releaseSyncLock(dbId, result) {
195
- const state = getSyncState(dbId);
196
-
197
- if (!state.inProgress) {
198
- console.warn("releaseSyncLock called but no sync was in progress");
199
- return;
200
- }
201
-
202
- state.result = result;
203
- state.inProgress = false;
204
-
205
- for (const waiter of state.waiters) {
206
- waiter.resolve(result);
207
- }
208
- state.waiters = [];
209
-
210
- if (state.releaseLock) {
211
- state.releaseLock();
212
- state.releaseLock = null;
92
+ function runUnderLock(dbId, fn) {
93
+ if (!hasWebLocks()) {
94
+ const prev = fallbackTails.get(dbId) ?? Promise.resolve();
95
+ const next = prev.catch(() => {}).then(fn);
96
+ const guarded = next.catch(() => {});
97
+ fallbackTails.set(dbId, guarded);
98
+ guarded.then(() => {
99
+ // Drop the slot only if no successor chained onto this tail.
100
+ if (fallbackTails.get(dbId) === guarded) fallbackTails.delete(dbId);
101
+ });
102
+ return next;
213
103
  }
104
+ return navigator.locks.request(
105
+ `miden-sync-${dbId}`,
106
+ { mode: "exclusive" },
107
+ fn
108
+ );
214
109
  }
215
110
 
216
111
  /**
217
- * Release the sync lock due to an error.
112
+ * Run `fn` under the sync lock for (dbId, methodId).
218
113
  *
219
- * This notifies all waiting callers that the sync failed.
114
+ * Concurrent calls with the same (dbId, methodId) share the same promise
115
+ * (coalescing). Concurrent calls on the same dbId with different methodIds
116
+ * serialize via the Web Lock.
220
117
  *
221
- * @param {string} dbId - The database ID
222
- * @param {Error} error - The error to pass to waiters
118
+ * @param {string} dbId - Database ID
119
+ * @param {string} methodId - Method identifier (see MethodName constants)
120
+ * @param {() => Promise<T>} fn - Work to run under the lock
121
+ * @returns {Promise<T>}
223
122
  */
224
- function releaseSyncLockWithError(dbId, error) {
225
- const state = getSyncState(dbId);
226
-
227
- if (!state.inProgress) {
228
- console.warn("releaseSyncLockWithError called but no sync was in progress");
229
- return;
230
- }
231
-
232
- state.error = error;
233
- state.inProgress = false;
234
-
235
- for (const waiter of state.waiters) {
236
- waiter.reject(error);
237
- }
238
- state.waiters = [];
239
-
240
- if (state.releaseLock) {
241
- state.releaseLock();
242
- state.releaseLock = null;
243
- }
123
+ function withSyncLock(dbId, methodId, fn) {
124
+ const key = coalesceKey(dbId, methodId);
125
+
126
+ let work = inFlight.get(key);
127
+ if (!work) {
128
+ work = runUnderLock(dbId, fn);
129
+ inFlight.set(key, work);
130
+ // Swallow on the derived promise so a rejection here doesn't surface as
131
+ // an unhandled rejection; the caller still sees the error through `work`.
132
+ work
133
+ .finally(() => {
134
+ if (inFlight.get(key) === work) inFlight.delete(key);
135
+ })
136
+ .catch(() => {});
137
+ }
138
+
139
+ return work;
244
140
  }
245
141
 
246
142
  /**
@@ -504,6 +400,7 @@ class AccountsResource {
504
400
  return await this.#inner.newFaucet(
505
401
  storageMode,
506
402
  type === 1 || type === "NonFungibleFaucet",
403
+ opts.name ?? opts.symbol,
507
404
  opts.symbol,
508
405
  opts.decimals,
509
406
  BigInt(opts.maxSupply),
@@ -536,11 +433,9 @@ class AccountsResource {
536
433
  if (!opts.auth)
537
434
  throw new Error("Contract creation requires an 'auth' (AuthSecretKey)");
538
435
 
539
- // Default to immutable when type is omitted (safer for contracts)
540
- const mutable = opts.type === "MutableContract" || opts.type === 3;
541
- const accountTypeEnum = mutable
542
- ? wasm.AccountType.RegularAccountUpdatableCode
543
- : wasm.AccountType.RegularAccountImmutableCode;
436
+ // The 0.15 protocol has no code-mutability distinction, so the `type`
437
+ // ("ImmutableContract" / "MutableContract") only steers routing here; the
438
+ // account's on-chain visibility is set entirely by `storageMode`.
544
439
  const storageMode = resolveStorageMode(opts.storage ?? "public", wasm);
545
440
  const authComponent =
546
441
  wasm.AccountComponent.createAuthComponentFromSecretKey(opts.auth);
@@ -555,7 +450,6 @@ class AccountsResource {
555
450
  }
556
451
 
557
452
  let builder = new wasm.AccountBuilder(opts.seed)
558
- .accountType(accountTypeEnum)
559
453
  .storageMode(storageMode)
560
454
  .withAuthComponent(authComponent);
561
455
 
@@ -866,6 +760,72 @@ class TransactionsResource {
866
760
  return { txId, result };
867
761
  }
868
762
 
763
+ /** Create a partial-swap (PSWAP) note. See {@link PswapCreateOptions}. */
764
+ async pswapCreate(opts) {
765
+ this.#client.assertNotTerminated();
766
+ const wasm = await this.#getWasm();
767
+ const { accountId, request } = await this.#buildPswapCreateRequest(
768
+ opts,
769
+ wasm
770
+ );
771
+
772
+ const { txId, result } = await this.#submitOrSubmitWithProver(
773
+ accountId,
774
+ request,
775
+ opts.prover
776
+ );
777
+
778
+ if (opts.waitForConfirmation) {
779
+ await this.waitFor(txId.toHex(), { timeout: opts.timeout });
780
+ }
781
+
782
+ return { txId, result };
783
+ }
784
+
785
+ /** Consume (fully or partially fill) a PSWAP note. See {@link PswapConsumeOptions}. */
786
+ async pswapConsume(opts) {
787
+ this.#client.assertNotTerminated();
788
+ const wasm = await this.#getWasm();
789
+ const { accountId, request } = await this.#buildPswapConsumeRequest(
790
+ opts,
791
+ wasm
792
+ );
793
+
794
+ const { txId, result } = await this.#submitOrSubmitWithProver(
795
+ accountId,
796
+ request,
797
+ opts.prover
798
+ );
799
+
800
+ if (opts.waitForConfirmation) {
801
+ await this.waitFor(txId.toHex(), { timeout: opts.timeout });
802
+ }
803
+
804
+ return { txId, result };
805
+ }
806
+
807
+ /** Cancel a PSWAP note as its creator and reclaim the offered asset. See {@link PswapCancelOptions}. */
808
+ async pswapCancel(opts) {
809
+ this.#client.assertNotTerminated();
810
+ const wasm = await this.#getWasm();
811
+ const { accountId, request } = await this.#buildPswapCancelRequest(
812
+ opts,
813
+ wasm
814
+ );
815
+
816
+ const { txId, result } = await this.#submitOrSubmitWithProver(
817
+ accountId,
818
+ request,
819
+ opts.prover
820
+ );
821
+
822
+ if (opts.waitForConfirmation) {
823
+ await this.waitFor(txId.toHex(), { timeout: opts.timeout });
824
+ }
825
+
826
+ return { txId, result };
827
+ }
828
+
869
829
  async preview(opts) {
870
830
  this.#client.assertNotTerminated();
871
831
  const wasm = await this.#getWasm();
@@ -890,6 +850,27 @@ class TransactionsResource {
890
850
  ({ accountId, request } = await this.#buildSwapRequest(opts, wasm));
891
851
  break;
892
852
  }
853
+ case "pswapCreate": {
854
+ ({ accountId, request } = await this.#buildPswapCreateRequest(
855
+ opts,
856
+ wasm
857
+ ));
858
+ break;
859
+ }
860
+ case "pswapConsume": {
861
+ ({ accountId, request } = await this.#buildPswapConsumeRequest(
862
+ opts,
863
+ wasm
864
+ ));
865
+ break;
866
+ }
867
+ case "pswapCancel": {
868
+ ({ accountId, request } = await this.#buildPswapCancelRequest(
869
+ opts,
870
+ wasm
871
+ ));
872
+ break;
873
+ }
893
874
  case "custom": {
894
875
  accountId = resolveAccountRef(opts.account, wasm);
895
876
  request = opts.request;
@@ -1042,7 +1023,10 @@ class TransactionsResource {
1042
1023
  }
1043
1024
 
1044
1025
  try {
1045
- await this.#inner.syncStateWithTimeout(0);
1026
+ // Chain-only sync is sufficient: confirmation only needs on-chain
1027
+ // state, and skipping NTL keeps polling alive when the note
1028
+ // transport endpoint is unavailable.
1029
+ await this.#inner.syncChain();
1046
1030
  } catch {
1047
1031
  // Sync may fail transiently; continue polling
1048
1032
  }
@@ -1176,6 +1160,53 @@ class TransactionsResource {
1176
1160
  return { accountId, request };
1177
1161
  }
1178
1162
 
1163
+ async #buildPswapCreateRequest(opts, wasm) {
1164
+ const accountId = resolveAccountRef(opts.account, wasm);
1165
+ const offeredFaucetId = resolveAccountRef(opts.offer.token, wasm);
1166
+ const requestedFaucetId = resolveAccountRef(opts.request.token, wasm);
1167
+ const noteType = resolveNoteType(opts.type, wasm);
1168
+ const paybackNoteType = resolveNoteType(
1169
+ opts.paybackType ?? opts.type,
1170
+ wasm
1171
+ );
1172
+
1173
+ const request = await this.#inner.newPswapCreateTransactionRequest(
1174
+ accountId,
1175
+ offeredFaucetId,
1176
+ BigInt(opts.offer.amount),
1177
+ requestedFaucetId,
1178
+ BigInt(opts.request.amount),
1179
+ noteType,
1180
+ paybackNoteType
1181
+ );
1182
+ return { accountId, request };
1183
+ }
1184
+
1185
+ async #buildPswapConsumeRequest(opts, wasm) {
1186
+ const accountId = resolveAccountRef(opts.account, wasm);
1187
+ const note = await this.#resolveNoteInput(opts.note);
1188
+ const noteFillAmount = opts.noteFillAmount ?? 0n;
1189
+
1190
+ const request = await this.#inner.newPswapConsumeTransactionRequest(
1191
+ note,
1192
+ accountId,
1193
+ BigInt(opts.fillAmount),
1194
+ BigInt(noteFillAmount)
1195
+ );
1196
+ return { accountId, request };
1197
+ }
1198
+
1199
+ async #buildPswapCancelRequest(opts, wasm) {
1200
+ const accountId = resolveAccountRef(opts.account, wasm);
1201
+ const note = await this.#resolveNoteInput(opts.note);
1202
+
1203
+ const request = await this.#inner.newPswapCancelTransactionRequest(
1204
+ note,
1205
+ accountId
1206
+ );
1207
+ return { accountId, request };
1208
+ }
1209
+
1179
1210
  async #resolveNoteInput(input) {
1180
1211
  if (typeof input === "string") {
1181
1212
  const record = await this.#inner.getInputNote(input);
@@ -1565,6 +1596,80 @@ class MidenClient {
1565
1596
  this.keystore = new KeystoreResource(inner, this);
1566
1597
  }
1567
1598
 
1599
+ /**
1600
+ * Escape hatch: runs `fn` with exclusive access to the proxied JS
1601
+ * WebClient that backs this MidenClient.
1602
+ *
1603
+ * The proxy forwards missing properties to the underlying wasm-bindgen
1604
+ * `WebClient`, so `fn` can reach lower-level methods like
1605
+ * `executeTransaction`, `proveTransaction[WithProver]`,
1606
+ * `submitProvenTransaction`, `applyTransaction`,
1607
+ * `newSendTransactionRequest`, `newConsumeTransactionRequest`, etc.
1608
+ *
1609
+ * Intended for advanced consumers that need to split the bundled
1610
+ * execute → prove → submit → apply pipeline across contexts — for example,
1611
+ * a Chrome MV3 extension that runs `executeTransaction` in its service
1612
+ * worker, dispatches the prove step to a `chrome.offscreen` document
1613
+ * (where wasm-bindgen-rayon can spawn a real thread pool), then runs
1614
+ * `submitProvenTransaction` + `applyTransaction` back in the SW.
1615
+ *
1616
+ * The callback runs inside `_serializeWasmCall`, so the WASM RefCell is
1617
+ * held for the duration of `fn`. Concurrent SDK calls (sync, other
1618
+ * transactions, etc.) queue on the same chain and run after `fn`
1619
+ * settles. Without this serialization, raw inner-client access would
1620
+ * race the proxy's chain and trip wasm-bindgen's "recursive use of an
1621
+ * object detected" panic.
1622
+ *
1623
+ * Re-entrancy: while `fn` is running, the underlying client's
1624
+ * `_withInnerLockDepth` counter is bumped so that `_serializeWasmCall`
1625
+ * invocations made BY `fn` (or any proxy-dispatched method it calls)
1626
+ * run inline rather than enqueuing on the chain. Without this, every
1627
+ * `await inner.X(...)` inside `fn` would enqueue behind the outer
1628
+ * `_withInnerWebClient` slot which is itself awaiting `fn` —
1629
+ * a classic re-entrant-lock deadlock. The depth counter restores the
1630
+ * intent of the docstring above: the lock is held for the duration
1631
+ * of `fn`, and inner-client calls "borrow" that already-held lock
1632
+ * instead of trying to re-acquire it.
1633
+ *
1634
+ * SAFETY CONTRACT for re-entrancy: callers MUST hold an external
1635
+ * mutex preventing concurrent access to this same client instance
1636
+ * via other code paths during `fn`. The chain still serializes
1637
+ * against external callers — they queue behind the outer slot — but
1638
+ * if an external task runs during one of `fn`'s awaits and calls
1639
+ * into the SDK, it will see `_withInnerLockDepth > 0` and run
1640
+ * inline, racing wasm-bindgen's borrow check. The wallet pattern
1641
+ * (own outer mutex around `_withInnerWebClient`) satisfies this.
1642
+ *
1643
+ * Stability: marked `@internal`. The shape of the proxied client is
1644
+ * intentionally not part of the documented public API and may change
1645
+ * between SDK versions. If you depend on this method, pin the SDK
1646
+ * version and test the lower-level surface carefully on each upgrade.
1647
+ * If your use case is common enough to warrant a stable public API,
1648
+ * file an issue.
1649
+ *
1650
+ * @internal
1651
+ * @template T
1652
+ * @param {(inner: object) => Promise<T>} fn - Async callback receiving
1653
+ * the proxied JS WebClient. Must not return references that escape
1654
+ * the callback's lifetime (the lock is released on settle).
1655
+ * @returns {Promise<T>} The resolved value of `fn`.
1656
+ */
1657
+ _withInnerWebClient(fn) {
1658
+ this.assertNotTerminated();
1659
+ if (typeof fn !== "function") {
1660
+ throw new TypeError("_withInnerWebClient: fn must be a function");
1661
+ }
1662
+ const inner = this.#inner;
1663
+ return inner._serializeWasmCall(async () => {
1664
+ inner._withInnerLockDepth = (inner._withInnerLockDepth || 0) + 1;
1665
+ try {
1666
+ return await fn(inner);
1667
+ } finally {
1668
+ inner._withInnerLockDepth--;
1669
+ }
1670
+ });
1671
+ }
1672
+
1568
1673
  /**
1569
1674
  * Creates and initializes a new MidenClient.
1570
1675
  *
@@ -1593,6 +1698,13 @@ class MidenClient {
1593
1698
  const rpcUrl = resolveRpcUrl(options?.rpcUrl);
1594
1699
  const noteTransportUrl = resolveNoteTransportUrl(options?.noteTransportUrl);
1595
1700
 
1701
+ // `useWorker: false` opts out of the Web Worker shim that wraps every
1702
+ // WASM call. The shim exists to keep the main thread responsive in
1703
+ // browser/extension contexts, but it serializes the prover via
1704
+ // `TransactionProver.serialize()` — a format that has no encoding for
1705
+ // `newCallbackProver(jsFn)` and silently downgrades it to `"local"`.
1706
+ // Mobile/Tauri/native-prover consumers must pass `useWorker: false`.
1707
+ const useWorker = options?.useWorker;
1596
1708
  let inner;
1597
1709
  if (options?.keystore) {
1598
1710
  inner = await WebClientClass.createClientWithExternalKeystore(
@@ -1603,7 +1715,8 @@ class MidenClient {
1603
1715
  options.keystore.getKey,
1604
1716
  options.keystore.insertKey,
1605
1717
  options.keystore.sign,
1606
- options?.debugMode
1718
+ options?.debugMode,
1719
+ useWorker
1607
1720
  );
1608
1721
  } else {
1609
1722
  inner = await WebClientClass.createClient(
@@ -1611,7 +1724,8 @@ class MidenClient {
1611
1724
  noteTransportUrl,
1612
1725
  seed,
1613
1726
  options?.storeName,
1614
- options?.debugMode
1727
+ options?.debugMode,
1728
+ useWorker
1615
1729
  );
1616
1730
  }
1617
1731
 
@@ -1668,6 +1782,32 @@ class MidenClient {
1668
1782
  });
1669
1783
  }
1670
1784
 
1785
+ /**
1786
+ * Resolves once the WASM module is initialized and safe to use.
1787
+ *
1788
+ * Idempotent and shared across callers: the underlying loader memoizes the
1789
+ * in-flight promise, so concurrent `ready()` calls await the same
1790
+ * initialization and post-init callers resolve immediately from a cached
1791
+ * module. Safe to call from `MidenProvider`, tutorial helpers, and any
1792
+ * other consumer simultaneously.
1793
+ *
1794
+ * Useful on the `/lazy` entry (e.g. Next.js / Capacitor), where no
1795
+ * top-level await runs at import time. On the default (eager) entry this
1796
+ * is redundant — importing the module already awaits WASM — but calling it
1797
+ * is still harmless.
1798
+ *
1799
+ * @returns {Promise<void>} Resolves when WASM is initialized.
1800
+ */
1801
+ static async ready() {
1802
+ const getWasm = MidenClient._getWasmOrThrow;
1803
+ if (!getWasm) {
1804
+ throw new Error(
1805
+ "MidenClient not initialized. Import from the SDK package entry point."
1806
+ );
1807
+ }
1808
+ await getWasm();
1809
+ }
1810
+
1671
1811
  /**
1672
1812
  * Creates a mock client for testing.
1673
1813
  *
@@ -1703,15 +1843,34 @@ class MidenClient {
1703
1843
  }
1704
1844
 
1705
1845
  /**
1706
- * Syncs the client state with the Miden node.
1846
+ * Syncs the client: fetches private notes from the Note Transport Layer, then syncs on-chain
1847
+ * state with the Miden node. Fails fast on either.
1707
1848
  *
1708
- * @param {object} [opts] - Sync options.
1709
- * @param {number} [opts.timeout] - Timeout in milliseconds (0 = no timeout).
1710
1849
  * @returns {Promise<SyncSummary>} The sync summary.
1711
1850
  */
1712
- async sync(opts) {
1851
+ async sync() {
1713
1852
  this.assertNotTerminated();
1714
- return await this.#inner.syncStateWithTimeout(opts?.timeout ?? 0);
1853
+ return await this.#inner.syncState();
1854
+ }
1855
+
1856
+ /**
1857
+ * Syncs on-chain state only (no NTL fetch).
1858
+ *
1859
+ * @returns {Promise<SyncSummary>}
1860
+ */
1861
+ async syncChain() {
1862
+ this.assertNotTerminated();
1863
+ return await this.#inner.syncChain();
1864
+ }
1865
+
1866
+ /**
1867
+ * Fetches private notes from the Note Transport Layer.
1868
+ *
1869
+ * @returns {Promise<void>}
1870
+ */
1871
+ async syncNoteTransport() {
1872
+ this.assertNotTerminated();
1873
+ return await this.#inner.syncNoteTransport();
1715
1874
  }
1716
1875
 
1717
1876
  /**
@@ -1724,6 +1883,61 @@ class MidenClient {
1724
1883
  return await this.#inner.getSyncHeight();
1725
1884
  }
1726
1885
 
1886
+ /**
1887
+ * Resolves once every serialized WASM call that was already on the
1888
+ * internal `_serializeWasmCall` chain when `waitForIdle()` was called
1889
+ * (execute, submit, prove, apply, sync, or account creation) has
1890
+ * settled. Use this from callers that need to perform a non-WASM-side
1891
+ * action — e.g. clearing an in-memory auth key on wallet lock — after
1892
+ * the kernel finishes, so its auth callback doesn't race with the key
1893
+ * being cleared.
1894
+ *
1895
+ * Does NOT wait for calls enqueued after `waitForIdle()` returns —
1896
+ * intentional, so a caller can drain and proceed without being blocked
1897
+ * indefinitely by concurrent workload.
1898
+ *
1899
+ * Caveat for `syncState`: `syncStateWithTimeout` awaits the sync lock
1900
+ * (`acquireSyncLock`, which uses Web Locks) BEFORE putting its WASM
1901
+ * call onto the chain, so a `syncState` that is queued on the sync
1902
+ * lock — but has not yet begun its WASM phase — is not visible to
1903
+ * `waitForIdle` and will not be awaited. Other methods (`newWallet`,
1904
+ * `executeTransaction`, etc.) route through the chain synchronously
1905
+ * on call and are always observed.
1906
+ *
1907
+ * Safe to call at any time; returns immediately if nothing was in
1908
+ * flight.
1909
+ *
1910
+ * @returns {Promise<void>}
1911
+ */
1912
+ async waitForIdle() {
1913
+ this.assertNotTerminated();
1914
+ await this.#inner.waitForIdle();
1915
+ }
1916
+
1917
+ /**
1918
+ * Returns the raw JS value that the most recent sign-callback invocation
1919
+ * threw, or `null` if the last sign call succeeded (or no call has
1920
+ * happened yet).
1921
+ *
1922
+ * Useful for recovering structured metadata (e.g. a `reason: 'locked'`
1923
+ * property) that the kernel-level `auth::request` diagnostic would
1924
+ * otherwise erase. Call immediately after catching a failed
1925
+ * `transactions.submit` / `transactions.send` / `transactions.consume`.
1926
+ *
1927
+ * Meaningful only with `useWorker: false`: under the worker shim the
1928
+ * sign callback fires against the worker's WASM keystore, while this
1929
+ * accessor reads the main-thread instance — which never signed — so it
1930
+ * returns `null`. Consumers that need this signal (e.g. external
1931
+ * keystores with lock-aware sign callbacks) already require
1932
+ * `useWorker: false` for the callback to be reachable at all.
1933
+ *
1934
+ * @returns {any} The raw thrown value, or `null`.
1935
+ */
1936
+ lastAuthError() {
1937
+ this.assertNotTerminated();
1938
+ return this.#inner.lastAuthError();
1939
+ }
1940
+
1727
1941
  /**
1728
1942
  * Terminates the underlying Web Worker. After this, all method calls will throw.
1729
1943
  */
@@ -2319,7 +2533,6 @@ const NoteVisibility = Object.freeze({
2319
2533
  const StorageMode = Object.freeze({
2320
2534
  Public: "public",
2321
2535
  Private: "private",
2322
- Network: "network",
2323
2536
  });
2324
2537
 
2325
2538
  const Linking = Object.freeze({
@@ -2513,6 +2726,15 @@ class WebClient {
2513
2726
  * @param {string | undefined} [logLevel] - Optional log verbosity level
2514
2727
  * ("error", "warn", "info", "debug", "trace", "off", or "none").
2515
2728
  * When set, Rust tracing output is routed to the browser console.
2729
+ * @param {boolean} [useWorker=true] - When `false`, skip the Web Worker shim
2730
+ * and call the wasm-bindgen `WebClient` directly on the current thread.
2731
+ * The worker exists to keep the main thread responsive during WASM work
2732
+ * in browser/extension contexts, but it serializes the prover argument
2733
+ * via `TransactionProver.serialize()` — a format that has no encoding
2734
+ * for `newCallbackProver(jsFn)` and silently downgrades it to `"local"`.
2735
+ * Consumers that hand a `CallbackProver` (e.g. native iOS/Android plug-in
2736
+ * provers in Capacitor apps, or any other JS-side prover bridge) need
2737
+ * `useWorker: false` so the prover handle reaches the WASM binding intact.
2516
2738
  */
2517
2739
  constructor(
2518
2740
  rpcUrl,
@@ -2522,7 +2744,8 @@ class WebClient {
2522
2744
  getKeyCb,
2523
2745
  insertKeyCb,
2524
2746
  signCb,
2525
- logLevel
2747
+ logLevel,
2748
+ useWorker = true
2526
2749
  ) {
2527
2750
  this.rpcUrl = rpcUrl;
2528
2751
  this.noteTransportUrl = noteTransportUrl;
@@ -2532,9 +2755,12 @@ class WebClient {
2532
2755
  this.insertKeyCb = insertKeyCb;
2533
2756
  this.signCb = signCb;
2534
2757
  this.logLevel = logLevel;
2758
+ this.useWorker = useWorker !== false;
2535
2759
 
2536
- // Check if Web Workers are available.
2537
- if (typeof Worker !== "undefined") {
2760
+ // Check if Web Workers are available AND the caller didn't opt out via
2761
+ // `useWorker: false`. The opt-out is load-bearing for `CallbackProver`
2762
+ // consumers — see the constructor doc above.
2763
+ if (this.useWorker && typeof Worker !== "undefined") {
2538
2764
  console.log("WebClient: Web Workers are available.");
2539
2765
  // Pick between the module and classic worker variants at runtime — see
2540
2766
  // `WebClient.workerMode` below. Both branches keep the
@@ -2575,10 +2801,19 @@ class WebClient {
2575
2801
  this.loadedResolver = resolve;
2576
2802
  });
2577
2803
 
2578
- // Create a promise that resolves when the worker signals that it is fully initialized.
2579
- this.ready = new Promise((resolve) => {
2804
+ // Create a promise that resolves when the worker signals that it is
2805
+ // fully initialized, and rejects if initialization fails. Every
2806
+ // worker-forwarded method awaits `ready` first, so an init failure must
2807
+ // reject it — otherwise those calls would await a promise that never
2808
+ // settles and hang forever.
2809
+ this.ready = new Promise((resolve, reject) => {
2580
2810
  this.readyResolver = resolve;
2811
+ this.readyRejecter = reject;
2581
2812
  });
2813
+ // Init can fail before any caller awaits `ready`; this no-op handler
2814
+ // suppresses the unhandledrejection event without consuming the
2815
+ // rejection for real awaiters.
2816
+ this.ready.catch(() => {});
2582
2817
 
2583
2818
  // Listen for messages from the worker.
2584
2819
  this.worker.addEventListener("message", async (event) => {
@@ -2642,14 +2877,33 @@ class WebClient {
2642
2877
  } else {
2643
2878
  resolve(result);
2644
2879
  }
2880
+ return;
2881
+ }
2882
+
2883
+ // An error with no request attached comes from worker initialization
2884
+ // (INIT is the only requestId-less action that can fail). Reject
2885
+ // `ready` so queued and future method calls fail with the real cause
2886
+ // instead of awaiting forever.
2887
+ if (error && !requestId) {
2888
+ const workerError =
2889
+ error instanceof Error ? error : deserializeError(error);
2890
+ console.error(
2891
+ "WebClient: worker initialization failed:",
2892
+ workerError
2893
+ );
2894
+ this.readyRejecter(workerError);
2645
2895
  }
2646
2896
  });
2647
2897
 
2648
2898
  // Once the worker script has loaded, initialize the worker.
2649
2899
  this.loaded.then(() => this.initializeWorker());
2650
2900
  } else {
2651
- console.log("WebClient: Web Workers are not available.");
2652
- // Worker not available; set up fallback values.
2901
+ console.log(
2902
+ this.useWorker
2903
+ ? "WebClient: Web Workers are not available."
2904
+ : "WebClient: Web Worker shim disabled by caller (useWorker=false)."
2905
+ );
2906
+ // Worker not available or explicitly disabled; set up fallback values.
2653
2907
  this.worker = null;
2654
2908
  this.pendingRequests = null;
2655
2909
  this.loaded = Promise.resolve();
@@ -2665,25 +2919,99 @@ class WebClient {
2665
2919
  // would panic with "recursive use of an object detected" due to
2666
2920
  // wasm-bindgen's internal RefCell.
2667
2921
  this._wasmCallChain = Promise.resolve();
2922
+ // Depth counter for `_withInnerWebClient` re-entrancy. While > 0,
2923
+ // `_serializeWasmCall` runs its callback inline instead of queueing
2924
+ // it on the chain — see the comment on `_serializeWasmCall` for the
2925
+ // safety contract.
2926
+ this._withInnerLockDepth = 0;
2668
2927
  }
2669
2928
 
2670
2929
  /**
2671
2930
  * Serialize a WASM call that requires exclusive (&mut self) access.
2672
2931
  * Concurrent calls are queued and executed one at a time.
2673
2932
  *
2933
+ * Wraps both the direct (in-thread) path and the worker-dispatched path.
2934
+ * On the worker path this is redundant with the worker's own message queue,
2935
+ * but harmless (the chain resolves immediately on the main thread once the
2936
+ * worker's postMessage returns). On the direct path it is load-bearing —
2937
+ * without it, concurrent main-thread callers would panic with
2938
+ * "recursive use of an object detected" (wasm-bindgen's internal RefCell).
2939
+ *
2940
+ * Re-entrancy: when invoked from inside a `_withInnerWebClient(fn)`
2941
+ * callback — detected via `_withInnerLockDepth > 0` — `fn` runs inline
2942
+ * (no chain enqueue). The outer `_withInnerWebClient` invocation
2943
+ * already holds the chain via its own wrapping `_serializeWasmCall`,
2944
+ * so enqueueing the inner call would deadlock (the inner queues
2945
+ * behind the outer; the outer awaits the inner). The inline run is
2946
+ * safe because the chain still serializes against external callers
2947
+ * — they queue behind the outer call's chain slot, which only resolves
2948
+ * after `fn` (including all inline re-entries) settles. Callers of
2949
+ * `_withInnerWebClient` MUST hold an external mutex preventing
2950
+ * concurrent access via other code paths on this same instance during
2951
+ * the callback; without that, an external task running between two
2952
+ * awaits inside `fn` would race wasm-bindgen's borrow check.
2953
+ *
2674
2954
  * @param {() => Promise<any>} fn - The async function to execute.
2675
2955
  * @returns {Promise<any>} The result of fn.
2676
2956
  */
2677
2957
  _serializeWasmCall(fn) {
2958
+ if (this._withInnerLockDepth > 0) {
2959
+ return Promise.resolve().then(fn);
2960
+ }
2678
2961
  const result = this._wasmCallChain.catch(() => {}).then(fn);
2679
2962
  this._wasmCallChain = result.catch(() => {});
2680
2963
  return result;
2681
2964
  }
2682
2965
 
2966
+ /**
2967
+ * Returns a promise that resolves once every serialized WASM call that
2968
+ * was already on `_wasmCallChain` when `waitForIdle()` was called has
2969
+ * settled. Use this from callers that need to perform a non-WASM-side
2970
+ * action (e.g. clear an in-memory auth key) AFTER any in-flight
2971
+ * execute / submit / sync has completed, so the WASM kernel's auth
2972
+ * callback doesn't race with the key being cleared.
2973
+ *
2974
+ * Does NOT wait for calls enqueued after `waitForIdle()` returns —
2975
+ * this is intentional, so a caller can drain and then proceed without
2976
+ * being blocked indefinitely by a concurrent workload.
2977
+ *
2978
+ * Caveat for `syncState`: `syncStateWithTimeout` awaits
2979
+ * `acquireSyncLock` (Web Locks) BEFORE wrapping its WASM call in
2980
+ * `_serializeWasmCall`, so a sync that is queued on the sync lock but
2981
+ * has not yet reached its WASM phase is not on the chain and will not
2982
+ * be awaited. Every other serialized method (`executeTransaction`,
2983
+ * `newWallet`, `submitNewTransaction`, `proveTransaction`,
2984
+ * `applyTransaction`, and the proxy-fallback reads) routes through
2985
+ * the chain synchronously on call and is always observed.
2986
+ *
2987
+ * @returns {Promise<void>}
2988
+ */
2989
+ async waitForIdle() {
2990
+ // Chain on `_wasmCallChain`; by the time this resolves, any in-flight
2991
+ // serialized call has settled. Catch so the chain state doesn't leak.
2992
+ await this._wasmCallChain.catch(() => {});
2993
+ }
2994
+
2683
2995
  // TODO: This will soon conflict with some changes in main.
2684
2996
  // More context here:
2685
2997
  // https://github.com/0xMiden/miden-client/pull/1645?notification_referrer_id=NT_kwHOA1yg7NoAJVJlcG9zaXRvcnk7NjU5MzQzNzAyO0lzc3VlOzM3OTY4OTU1Nzk&notifications_query=is%3Aunread#discussion_r2696075480
2686
2998
  initializeWorker() {
2999
+ // Pass `numThreads` to the worker so it can call `wasm.initThreadPool(n)`
3000
+ // inside its OWN WASM instance — the SDK worker's instance is separate
3001
+ // from the main thread's, and rayon's global pool is per-instance.
3002
+ // Default: navigator.hardwareConcurrency (or 1 if unavailable for any
3003
+ // reason — e.g. the page isn't crossOriginIsolated, in which case the
3004
+ // worker will skip pool init and parallelism falls back to sequential).
3005
+ let numThreads = 1;
3006
+ try {
3007
+ if (
3008
+ typeof self !== "undefined" &&
3009
+ self.crossOriginIsolated &&
3010
+ navigator?.hardwareConcurrency
3011
+ ) {
3012
+ numThreads = navigator.hardwareConcurrency;
3013
+ }
3014
+ } catch {}
2687
3015
  this.worker.postMessage({
2688
3016
  action: WorkerAction.INIT,
2689
3017
  args: [
@@ -2695,6 +3023,7 @@ class WebClient {
2695
3023
  !!this.insertKeyCb,
2696
3024
  !!this.signCb,
2697
3025
  this.logLevel,
3026
+ numThreads,
2698
3027
  ],
2699
3028
  });
2700
3029
  }
@@ -2723,9 +3052,19 @@ class WebClient {
2723
3052
  * @param {string} seed - The seed for the account.
2724
3053
  * @param {string | undefined} network - Optional name for the store. Setting this allows multiple clients to be used in the same browser.
2725
3054
  * @param {string | undefined} logLevel - Optional log verbosity level ("error", "warn", "info", "debug", "trace", "off", or "none").
3055
+ * @param {boolean} [useWorker=true] - When `false`, bypass the Web Worker shim
3056
+ * and run WASM calls on the current thread. Required for `CallbackProver`
3057
+ * consumers (the worker path serializes the prover and loses the callback).
2726
3058
  * @returns {Promise<WebClient>} The fully initialized WebClient.
2727
3059
  */
2728
- static async createClient(rpcUrl, noteTransportUrl, seed, network, logLevel) {
3060
+ static async createClient(
3061
+ rpcUrl,
3062
+ noteTransportUrl,
3063
+ seed,
3064
+ network,
3065
+ logLevel,
3066
+ useWorker = true
3067
+ ) {
2729
3068
  // Construct the instance (synchronously).
2730
3069
  const instance = new WebClient(
2731
3070
  rpcUrl,
@@ -2735,7 +3074,8 @@ class WebClient {
2735
3074
  undefined,
2736
3075
  undefined,
2737
3076
  undefined,
2738
- logLevel
3077
+ logLevel,
3078
+ useWorker
2739
3079
  );
2740
3080
 
2741
3081
  // Set up logging on the main thread before creating the client.
@@ -2766,6 +3106,9 @@ class WebClient {
2766
3106
  * @param {Function | undefined} insertKeyCb - The insert key callback.
2767
3107
  * @param {Function | undefined} signCb - The sign callback.
2768
3108
  * @param {string | undefined} logLevel - Optional log verbosity level ("error", "warn", "info", "debug", "trace", "off", or "none").
3109
+ * @param {boolean} [useWorker=true] - When `false`, bypass the Web Worker shim
3110
+ * and run WASM calls on the current thread. Required for `CallbackProver`
3111
+ * consumers (the worker path serializes the prover and loses the callback).
2769
3112
  * @returns {Promise<WebClient>} The fully initialized WebClient.
2770
3113
  */
2771
3114
  static async createClientWithExternalKeystore(
@@ -2776,7 +3119,8 @@ class WebClient {
2776
3119
  getKeyCb,
2777
3120
  insertKeyCb,
2778
3121
  signCb,
2779
- logLevel
3122
+ logLevel,
3123
+ useWorker = true
2780
3124
  ) {
2781
3125
  // Construct the instance (synchronously).
2782
3126
  const instance = new WebClient(
@@ -2787,7 +3131,8 @@ class WebClient {
2787
3131
  getKeyCb,
2788
3132
  insertKeyCb,
2789
3133
  signCb,
2790
- logLevel
3134
+ logLevel,
3135
+ useWorker
2791
3136
  );
2792
3137
 
2793
3138
  // Set up logging on the main thread before creating the client.
@@ -2852,6 +3197,7 @@ class WebClient {
2852
3197
  async newFaucet(
2853
3198
  storageMode,
2854
3199
  nonFungible,
3200
+ tokenName,
2855
3201
  tokenSymbol,
2856
3202
  decimals,
2857
3203
  maxSupply,
@@ -2862,6 +3208,7 @@ class WebClient {
2862
3208
  return await wasmWebClient.newFaucet(
2863
3209
  storageMode,
2864
3210
  nonFungible,
3211
+ tokenName,
2865
3212
  tokenSymbol,
2866
3213
  decimals,
2867
3214
  maxSupply,
@@ -3029,7 +3376,7 @@ class WebClient {
3029
3376
  }
3030
3377
 
3031
3378
  /**
3032
- * Syncs the client state with the node.
3379
+ * Syncs the client (NTL followed by chain sync, failing fast on either).
3033
3380
  *
3034
3381
  * This method coordinates concurrent sync calls using the Web Locks API when available,
3035
3382
  * with an in-process mutex fallback for older browsers. If a sync is already in progress,
@@ -3038,58 +3385,76 @@ class WebClient {
3038
3385
  * @returns {Promise<SyncSummary>} The sync summary
3039
3386
  */
3040
3387
  async syncState() {
3041
- return this.syncStateWithTimeout(0);
3388
+ const dbId = this.storeName || "default";
3389
+ const methodId = MethodName.SYNC_STATE;
3390
+
3391
+ try {
3392
+ return await withSyncLock(dbId, methodId, async () => {
3393
+ if (!this.worker) {
3394
+ const wasmWebClient = await this.getWasmWebClient();
3395
+ return await wasmWebClient.syncStateImpl();
3396
+ }
3397
+ const wasm = await getWasmOrThrow();
3398
+ const serializedSyncSummaryBytes =
3399
+ await this.callMethodWithWorker(methodId);
3400
+ return wasm.SyncSummary.deserialize(
3401
+ new Uint8Array(serializedSyncSummaryBytes)
3402
+ );
3403
+ });
3404
+ } catch (error) {
3405
+ console.error("INDEX.JS: Error in syncState:", error);
3406
+ throw error;
3407
+ }
3042
3408
  }
3043
3409
 
3044
3410
  /**
3045
- * Syncs the client state with the node with an optional timeout.
3046
- *
3047
- * This method coordinates concurrent sync calls using the Web Locks API when available,
3048
- * with an in-process mutex fallback for older browsers. If a sync is already in progress,
3049
- * subsequent callers will wait and receive the same result (coalescing behavior).
3411
+ * Fetches private notes from the Note Transport Layer.
3050
3412
  *
3051
- * @param {number} timeoutMs - Timeout in milliseconds (0 = no timeout)
3052
- * @returns {Promise<SyncSummary>} The sync summary
3413
+ * @returns {Promise<void>}
3053
3414
  */
3054
- async syncStateWithTimeout(timeoutMs = 0) {
3055
- // Use storeName as the database ID for lock coordination
3415
+ async syncNoteTransport() {
3056
3416
  const dbId = this.storeName || "default";
3417
+ const methodId = MethodName.SYNC_NOTE_TRANSPORT;
3057
3418
 
3058
3419
  try {
3059
- // Acquire the sync lock (coordinates concurrent calls)
3060
- const lockHandle = await acquireSyncLock(dbId, timeoutMs);
3061
-
3062
- if (!lockHandle.acquired) {
3063
- // We're coalescing - return the result from the in-progress sync
3064
- return lockHandle.coalescedResult;
3065
- }
3066
-
3067
- // We acquired the lock - perform the sync
3068
- try {
3069
- let result;
3420
+ await withSyncLock(dbId, methodId, async () => {
3070
3421
  if (!this.worker) {
3071
3422
  const wasmWebClient = await this.getWasmWebClient();
3072
- result = await wasmWebClient.syncStateImpl();
3423
+ await wasmWebClient.syncNoteTransportImpl();
3073
3424
  } else {
3074
- const wasm = await getWasmOrThrow();
3075
- const serializedSyncSummaryBytes = await this.callMethodWithWorker(
3076
- MethodName.SYNC_STATE
3077
- );
3078
- result = wasm.SyncSummary.deserialize(
3079
- new Uint8Array(serializedSyncSummaryBytes)
3080
- );
3425
+ await this.callMethodWithWorker(methodId);
3081
3426
  }
3427
+ });
3428
+ } catch (error) {
3429
+ console.error("INDEX.JS: Error in syncNoteTransport:", error);
3430
+ throw error;
3431
+ }
3432
+ }
3082
3433
 
3083
- // Release the lock with the result
3084
- releaseSyncLock(dbId, result);
3085
- return result;
3086
- } catch (error) {
3087
- // Release the lock with the error
3088
- releaseSyncLockWithError(dbId, error);
3089
- throw error;
3090
- }
3434
+ /**
3435
+ * Syncs on-chain state only (no NTL fetch).
3436
+ *
3437
+ * @returns {Promise<SyncSummary>}
3438
+ */
3439
+ async syncChain() {
3440
+ const dbId = this.storeName || "default";
3441
+ const methodId = MethodName.SYNC_CHAIN;
3442
+
3443
+ try {
3444
+ return await withSyncLock(dbId, methodId, async () => {
3445
+ if (!this.worker) {
3446
+ const wasmWebClient = await this.getWasmWebClient();
3447
+ return await wasmWebClient.syncChainImpl();
3448
+ }
3449
+ const wasm = await getWasmOrThrow();
3450
+ const serializedSyncSummaryBytes =
3451
+ await this.callMethodWithWorker(methodId);
3452
+ return wasm.SyncSummary.deserialize(
3453
+ new Uint8Array(serializedSyncSummaryBytes)
3454
+ );
3455
+ });
3091
3456
  } catch (error) {
3092
- console.error("INDEX.JS: Error in syncState:", error);
3457
+ console.error("INDEX.JS: Error in syncChain:", error);
3093
3458
  throw error;
3094
3459
  }
3095
3460
  }
@@ -3125,9 +3490,23 @@ class MockWebClient extends WebClient {
3125
3490
  }
3126
3491
 
3127
3492
  initializeWorker() {
3493
+ // Pass `numThreads` exactly like the real INIT path: every prove runs
3494
+ // inside the worker's own WASM instance, and rayon's pool is
3495
+ // per-instance — without this, mock-client proving (including the
3496
+ // integration suite) silently runs single-threaded.
3497
+ let numThreads = 1;
3498
+ try {
3499
+ if (
3500
+ typeof self !== "undefined" &&
3501
+ self.crossOriginIsolated &&
3502
+ navigator?.hardwareConcurrency
3503
+ ) {
3504
+ numThreads = navigator.hardwareConcurrency;
3505
+ }
3506
+ } catch {}
3128
3507
  this.worker.postMessage({
3129
3508
  action: WorkerAction.INIT_MOCK,
3130
- args: [this.seed, this.logLevel],
3509
+ args: [this.seed, this.logLevel, numThreads],
3131
3510
  });
3132
3511
  }
3133
3512
 
@@ -3178,59 +3557,119 @@ class MockWebClient extends WebClient {
3178
3557
  * @returns {Promise<SyncSummary>} The sync summary
3179
3558
  */
3180
3559
  async syncState() {
3181
- return this.syncStateWithTimeout(0);
3560
+ const dbId = this.storeName || "mock";
3561
+ const methodId = MethodName.SYNC_STATE;
3562
+
3563
+ try {
3564
+ return await withSyncLock(dbId, methodId, async () => {
3565
+ const wasmWebClient = await this.getWasmWebClient();
3566
+
3567
+ if (!this.worker) {
3568
+ return await wasmWebClient.syncStateImpl();
3569
+ }
3570
+
3571
+ const serializedMockChain = (await wasmWebClient.serializeMockChain())
3572
+ .buffer;
3573
+ const serializedMockNoteTransportNode = (
3574
+ await wasmWebClient.serializeMockNoteTransportNode()
3575
+ ).buffer;
3576
+
3577
+ const wasm = await getWasmOrThrow();
3578
+ const serializedSyncSummaryBytes = await this.callMethodWithWorker(
3579
+ MethodName.SYNC_STATE_MOCK,
3580
+ serializedMockChain,
3581
+ serializedMockNoteTransportNode
3582
+ );
3583
+ return wasm.SyncSummary.deserialize(
3584
+ new Uint8Array(serializedSyncSummaryBytes)
3585
+ );
3586
+ });
3587
+ } catch (error) {
3588
+ console.error("INDEX.JS: Error in syncState:", error);
3589
+ throw error;
3590
+ }
3182
3591
  }
3183
3592
 
3184
3593
  /**
3185
- * Syncs the mock client state with an optional timeout.
3594
+ * Syncs only the on-chain mock state (no note transport fetch).
3186
3595
  *
3187
- * @param {number} timeoutMs - Timeout in milliseconds (0 = no timeout)
3188
- * @returns {Promise<SyncSummary>} The sync summary
3596
+ * In worker mode, the main-thread mock chain + note-transport-node state
3597
+ * is serialized and shipped to the worker before the sync, so a prior
3598
+ * `proveBlock()` on the main thread is reflected in the worker's WASM
3599
+ * client. The no-worker path uses the main-thread WASM client directly.
3600
+ *
3601
+ * @returns {Promise<SyncSummary>}
3189
3602
  */
3190
- async syncStateWithTimeout(timeoutMs = 0) {
3603
+ async syncChain() {
3191
3604
  const dbId = this.storeName || "mock";
3605
+ const methodId = MethodName.SYNC_CHAIN;
3192
3606
 
3193
3607
  try {
3194
- const lockHandle = await acquireSyncLock(dbId, timeoutMs);
3608
+ return await withSyncLock(dbId, methodId, async () => {
3609
+ const wasmWebClient = await this.getWasmWebClient();
3195
3610
 
3196
- if (!lockHandle.acquired) {
3197
- return lockHandle.coalescedResult;
3198
- }
3611
+ if (!this.worker) {
3612
+ return await wasmWebClient.syncChainImpl();
3613
+ }
3199
3614
 
3200
- try {
3201
- let result;
3615
+ const serializedMockChain = (await wasmWebClient.serializeMockChain())
3616
+ .buffer;
3617
+ const serializedMockNoteTransportNode = (
3618
+ await wasmWebClient.serializeMockNoteTransportNode()
3619
+ ).buffer;
3620
+
3621
+ const wasm = await getWasmOrThrow();
3622
+ const serializedSyncSummaryBytes = await this.callMethodWithWorker(
3623
+ MethodName.SYNC_CHAIN_MOCK,
3624
+ serializedMockChain,
3625
+ serializedMockNoteTransportNode
3626
+ );
3627
+ return wasm.SyncSummary.deserialize(
3628
+ new Uint8Array(serializedSyncSummaryBytes)
3629
+ );
3630
+ });
3631
+ } catch (error) {
3632
+ console.error("INDEX.JS: Error in syncChain:", error);
3633
+ throw error;
3634
+ }
3635
+ }
3636
+
3637
+ /**
3638
+ * Syncs only the mock note-transport state (no chain fetch).
3639
+ *
3640
+ * Mirrors {@link MockWebClient#syncChain}: in worker mode, the
3641
+ * main-thread mock chain + note-transport-node state is serialized
3642
+ * and shipped to the worker first.
3643
+ *
3644
+ * @returns {Promise<void>}
3645
+ */
3646
+ async syncNoteTransport() {
3647
+ const dbId = this.storeName || "mock";
3648
+ const methodId = MethodName.SYNC_NOTE_TRANSPORT;
3649
+
3650
+ try {
3651
+ await withSyncLock(dbId, methodId, async () => {
3202
3652
  const wasmWebClient = await this.getWasmWebClient();
3203
3653
 
3204
3654
  if (!this.worker) {
3205
- result = await wasmWebClient.syncStateImpl();
3206
- } else {
3207
- let serializedMockChain = (await wasmWebClient.serializeMockChain())
3208
- .buffer;
3209
- let serializedMockNoteTransportNode = (
3210
- await wasmWebClient.serializeMockNoteTransportNode()
3211
- ).buffer;
3212
-
3213
- const wasm = await getWasmOrThrow();
3214
-
3215
- const serializedSyncSummaryBytes = await this.callMethodWithWorker(
3216
- MethodName.SYNC_STATE_MOCK,
3217
- serializedMockChain,
3218
- serializedMockNoteTransportNode
3219
- );
3220
-
3221
- result = wasm.SyncSummary.deserialize(
3222
- new Uint8Array(serializedSyncSummaryBytes)
3223
- );
3655
+ await wasmWebClient.syncNoteTransportImpl();
3656
+ return;
3224
3657
  }
3225
3658
 
3226
- releaseSyncLock(dbId, result);
3227
- return result;
3228
- } catch (error) {
3229
- releaseSyncLockWithError(dbId, error);
3230
- throw error;
3231
- }
3659
+ const serializedMockChain = (await wasmWebClient.serializeMockChain())
3660
+ .buffer;
3661
+ const serializedMockNoteTransportNode = (
3662
+ await wasmWebClient.serializeMockNoteTransportNode()
3663
+ ).buffer;
3664
+
3665
+ await this.callMethodWithWorker(
3666
+ MethodName.SYNC_NOTE_TRANSPORT_MOCK,
3667
+ serializedMockChain,
3668
+ serializedMockNoteTransportNode
3669
+ );
3670
+ });
3232
3671
  } catch (error) {
3233
- console.error("INDEX.JS: Error in syncState:", error);
3672
+ console.error("INDEX.JS: Error in syncNoteTransport:", error);
3234
3673
  throw error;
3235
3674
  }
3236
3675
  }
@@ -3368,5 +3807,5 @@ MidenClient._MockWasmWebClient = MockWebClient;
3368
3807
  MidenClient._getWasmOrThrow = getWasmOrThrow;
3369
3808
  _setWebClient(WebClient);
3370
3809
 
3371
- export { AccountType, AuthScheme, CompilerResource, Linking, MidenArrays, MidenClient, MockWebClient as MockWasmWebClient, MockWebClient, NoteVisibility, StorageMode, StorageResult, StorageView, WebClient as WasmWebClient, buildSwapTag, createP2IDENote, createP2IDNote, getWasmOrThrow, wordToBigInt };
3810
+ export { AccountType, AuthScheme, CompilerResource, Linking, MidenArrays, MidenClient, MockWebClient as MockWasmWebClient, MockWebClient, NoteVisibility, StorageMode, StorageResult, StorageView, WebClient as WasmWebClient, buildSwapTag, createP2IDENote, createP2IDNote, getWasmOrThrow, withSyncLock, wordToBigInt };
3372
3811
  //# sourceMappingURL=index.js.map