@miden-sdk/miden-sdk 0.13.2 → 0.13.3
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.
- package/dist/{Cargo-e77f9a02.js → Cargo-0ed69232.js} +143 -130
- package/dist/Cargo-0ed69232.js.map +1 -0
- package/dist/assets/miden_client_web.wasm +0 -0
- package/dist/index.d.ts +29 -1
- package/dist/index.js +775 -290
- package/dist/index.js.map +1 -1
- package/dist/wasm.js +1 -1
- package/dist/workers/{Cargo-e77f9a02-e77f9a02.js → Cargo-0ed69232-0ed69232.js} +143 -130
- package/dist/workers/Cargo-0ed69232-0ed69232.js.map +1 -0
- package/dist/workers/assets/miden_client_web.wasm +0 -0
- package/dist/workers/web-client-methods-worker.js +1 -1
- package/dist/workers/web-client-methods-worker.js.map +1 -1
- package/package.json +2 -1
- package/dist/Cargo-e77f9a02.js.map +0 -1
- package/dist/workers/Cargo-e77f9a02-e77f9a02.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import loadWasm from './wasm.js';
|
|
2
|
-
export { Account, AccountArray, AccountBuilder, AccountBuilderResult, AccountCode, AccountComponent, AccountComponentCode, AccountDelta, AccountFile, AccountHeader, AccountId, AccountIdArray, AccountInterface, AccountStorage, AccountStorageDelta, AccountStorageMode, AccountStorageRequirements, AccountType, AccountVaultDelta, Address, AdviceInputs, AdviceMap, AssetVault, AuthFalcon512RpoMultisigConfig, AuthScheme, 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, NetworkType, Note, NoteAndArgs, NoteAndArgsArray, NoteAssets, NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme, NoteConsumability, NoteConsumptionStatus, NoteDetails, NoteDetailsAndTag, NoteDetailsAndTagArray, NoteExecutionHint, NoteFile, NoteFilter, NoteFilterTypes, NoteHeader, NoteId, NoteIdAndArgs, NoteIdAndArgsArray, NoteInclusionProof, NoteInputs, NoteLocation, NoteMetadata, NoteRecipient, NoteRecipientArray, NoteScript, NoteSyncInfo, NoteTag, NoteType, OutputNote, OutputNoteArray, OutputNoteRecord, OutputNoteState, OutputNotes, OutputNotesArray, Package, PartialNote, ProcedureThreshold, Program, ProvenTransaction, PublicKey, RpcClient, Rpo256, SerializedInputNoteData, SerializedOutputNoteData, SerializedTransactionData, Signature, SigningInputs, SigningInputsType, SlotAndKeys, SparseMerklePath, StorageMap, StorageSlot, StorageSlotArray, SyncSummary, TestUtils, TokenSymbol, TransactionArgs, TransactionFilter, TransactionId, TransactionProver, TransactionRecord, TransactionRequest, TransactionRequestBuilder, TransactionResult, TransactionScript, TransactionScriptInputPair, TransactionScriptInputPairArray, TransactionStatus, TransactionStoreUpdate, TransactionSummary, Word, createAuthFalcon512RpoMultisig, initSync, setupLogging } from './Cargo-
|
|
2
|
+
export { Account, AccountArray, AccountBuilder, AccountBuilderResult, AccountCode, AccountComponent, AccountComponentCode, AccountDelta, AccountFile, AccountHeader, AccountId, AccountIdArray, AccountInterface, AccountStorage, AccountStorageDelta, AccountStorageMode, AccountStorageRequirements, AccountType, AccountVaultDelta, Address, AdviceInputs, AdviceMap, AssetVault, AuthFalcon512RpoMultisigConfig, AuthScheme, 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, NetworkType, Note, NoteAndArgs, NoteAndArgsArray, NoteAssets, NoteAttachment, NoteAttachmentKind, NoteAttachmentScheme, NoteConsumability, NoteConsumptionStatus, NoteDetails, NoteDetailsAndTag, NoteDetailsAndTagArray, NoteExecutionHint, NoteFile, NoteFilter, NoteFilterTypes, NoteHeader, NoteId, NoteIdAndArgs, NoteIdAndArgsArray, NoteInclusionProof, NoteInputs, NoteLocation, NoteMetadata, NoteRecipient, NoteRecipientArray, NoteScript, NoteSyncInfo, NoteTag, NoteType, OutputNote, OutputNoteArray, OutputNoteRecord, OutputNoteState, OutputNotes, OutputNotesArray, Package, PartialNote, ProcedureThreshold, Program, ProvenTransaction, PublicKey, RpcClient, Rpo256, SerializedInputNoteData, SerializedOutputNoteData, SerializedTransactionData, Signature, SigningInputs, SigningInputsType, SlotAndKeys, SparseMerklePath, StorageMap, StorageSlot, StorageSlotArray, SyncSummary, TestUtils, TokenSymbol, TransactionArgs, TransactionFilter, TransactionId, TransactionProver, TransactionRecord, TransactionRequest, TransactionRequestBuilder, TransactionResult, TransactionScript, TransactionScriptInputPair, TransactionScriptInputPairArray, TransactionStatus, TransactionStoreUpdate, TransactionSummary, Word, createAuthFalcon512RpoMultisig, initSync, setupLogging } from './Cargo-0ed69232.js';
|
|
3
3
|
|
|
4
4
|
const WorkerAction = Object.freeze({
|
|
5
5
|
INIT: "init",
|
|
@@ -239,6 +239,242 @@ function releaseSyncLockWithError(dbId, error) {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/**
|
|
243
|
+
* A simple promise-chain mutex for serializing async operations.
|
|
244
|
+
*
|
|
245
|
+
* Ignores errors: if one operation throws, the next still runs (no deadlocks).
|
|
246
|
+
*/
|
|
247
|
+
class AsyncLock {
|
|
248
|
+
constructor() {
|
|
249
|
+
this._pending = Promise.resolve();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Queue `fn` so that it runs only after all previously queued operations
|
|
254
|
+
* have settled (resolved or rejected).
|
|
255
|
+
*
|
|
256
|
+
* @template T
|
|
257
|
+
* @param {() => Promise<T>} fn
|
|
258
|
+
* @returns {Promise<T>}
|
|
259
|
+
*/
|
|
260
|
+
runExclusive(fn) {
|
|
261
|
+
const run = this._pending.then(
|
|
262
|
+
() => fn(),
|
|
263
|
+
() => fn()
|
|
264
|
+
);
|
|
265
|
+
// Swallow the result/error so the chain itself never rejects
|
|
266
|
+
this._pending = run.then(
|
|
267
|
+
() => undefined,
|
|
268
|
+
() => undefined
|
|
269
|
+
);
|
|
270
|
+
return run;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Cross-Tab Write Lock Module
|
|
276
|
+
*
|
|
277
|
+
* Provides an exclusive write lock using the Web Locks API so that mutating
|
|
278
|
+
* operations on the same IndexedDB database are serialized across browser tabs.
|
|
279
|
+
*
|
|
280
|
+
* When the Web Locks API is unavailable the lock is a no-op — the in-process
|
|
281
|
+
* AsyncLock still protects against concurrent WASM access within a single tab.
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Execute `fn` while holding an exclusive cross-tab write lock for the given
|
|
287
|
+
* store. If the Web Locks API is not available, `fn` runs immediately.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} storeName - Logical database / store name.
|
|
290
|
+
* @param {() => Promise<T>} fn - The async work to perform under the lock.
|
|
291
|
+
* @param {number} [timeoutMs=0] - Optional timeout in milliseconds for
|
|
292
|
+
* acquiring the lock. 0 (default) means wait indefinitely. When the timeout
|
|
293
|
+
* fires the lock request is aborted and the returned promise rejects with an
|
|
294
|
+
* `AbortError`. Has no effect when the Web Locks API is unavailable.
|
|
295
|
+
* @returns {Promise<T>}
|
|
296
|
+
* @template T
|
|
297
|
+
*/
|
|
298
|
+
async function withWriteLock(storeName, fn, timeoutMs = 0) {
|
|
299
|
+
if (!hasWebLocks()) {
|
|
300
|
+
return fn();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const lockName = `miden-db-${storeName || "default"}`;
|
|
304
|
+
|
|
305
|
+
if (timeoutMs > 0) {
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
return await navigator.locks.request(
|
|
311
|
+
lockName,
|
|
312
|
+
{ mode: "exclusive", signal: controller.signal },
|
|
313
|
+
async () => {
|
|
314
|
+
clearTimeout(timeoutId);
|
|
315
|
+
return fn();
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
clearTimeout(timeoutId);
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return navigator.locks.request(lockName, { mode: "exclusive" }, async () => {
|
|
325
|
+
return fn();
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// WASM PROXY METHODS
|
|
330
|
+
// ================================================================================================
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Set of method names that are synchronous (non-async) on the WASM object
|
|
334
|
+
* and should NOT be wrapped with the async WASM lock. Wrapping them would
|
|
335
|
+
* turn their return type from T into Promise<T>, breaking callers that
|
|
336
|
+
* expect a synchronous return value.
|
|
337
|
+
*
|
|
338
|
+
* These methods may still mutate internal client state (e.g. RNG), but they
|
|
339
|
+
* complete in a single synchronous call without yielding to the event loop.
|
|
340
|
+
* JavaScript's single-threaded execution guarantees they cannot interleave
|
|
341
|
+
* with an in-progress async WASM call.
|
|
342
|
+
*
|
|
343
|
+
* When updating this set, check the Rust source for any `pub fn` (non-async)
|
|
344
|
+
* method on `impl WebClient` with #[wasm_bindgen] that is NOT already handled
|
|
345
|
+
* by an explicit wrapper method on the JS WebClient class.
|
|
346
|
+
*/
|
|
347
|
+
const SYNC_METHODS = new Set([
|
|
348
|
+
"newMintTransactionRequest",
|
|
349
|
+
"newSendTransactionRequest",
|
|
350
|
+
"newConsumeTransactionRequest",
|
|
351
|
+
"newSwapTransactionRequest",
|
|
352
|
+
"createCodeBuilder",
|
|
353
|
+
"buildSwapTag",
|
|
354
|
+
"setDebugMode",
|
|
355
|
+
"usesMockChain",
|
|
356
|
+
"serializeMockChain",
|
|
357
|
+
"serializeMockNoteTransportNode",
|
|
358
|
+
"proveBlock",
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Set of method names that mutate state. These are wrapped with the cross-tab
|
|
363
|
+
* write lock (Layer 2) when accessed through the Proxy fallback.
|
|
364
|
+
*/
|
|
365
|
+
const WRITE_METHODS = new Set([
|
|
366
|
+
"newAccount",
|
|
367
|
+
"importAccountFile",
|
|
368
|
+
"importAccountById",
|
|
369
|
+
"importPublicAccountFromSeed",
|
|
370
|
+
"importNoteFile",
|
|
371
|
+
"forceImportStore",
|
|
372
|
+
"addTag",
|
|
373
|
+
"removeTag",
|
|
374
|
+
"setSetting",
|
|
375
|
+
"removeSetting",
|
|
376
|
+
"insertAccountAddress",
|
|
377
|
+
"removeAccountAddress",
|
|
378
|
+
"sendPrivateNote",
|
|
379
|
+
// fetch*PrivateNotes fetches from the note transport AND writes to IndexedDB
|
|
380
|
+
"fetchPrivateNotes",
|
|
381
|
+
"fetchAllPrivateNotes",
|
|
382
|
+
"addAccountSecretKeyToWebStore",
|
|
383
|
+
"executeForSummary",
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Set of method names that are read-only (no state mutation). These are
|
|
388
|
+
* wrapped with the in-process WASM lock only (Layer 1).
|
|
389
|
+
*
|
|
390
|
+
* This set exists purely for the CI lint check (`check-method-classification`)
|
|
391
|
+
* which ensures every WASM export is explicitly classified — preventing new
|
|
392
|
+
* write methods from silently defaulting to read-only.
|
|
393
|
+
*
|
|
394
|
+
* The Proxy behaviour is unchanged: methods not in SYNC_METHODS or
|
|
395
|
+
* WRITE_METHODS already get WASM-lock-only wrapping.
|
|
396
|
+
*/
|
|
397
|
+
const READ_METHODS = new Set([
|
|
398
|
+
"getAccounts",
|
|
399
|
+
"getAccount",
|
|
400
|
+
"getAccountAuthByPubKeyCommitment",
|
|
401
|
+
"getPublicKeyCommitmentsOfAccount",
|
|
402
|
+
"getInputNotes",
|
|
403
|
+
"getInputNote",
|
|
404
|
+
"getOutputNotes",
|
|
405
|
+
"getOutputNote",
|
|
406
|
+
"getConsumableNotes",
|
|
407
|
+
"getTransactions",
|
|
408
|
+
"getSyncHeight",
|
|
409
|
+
"exportNoteFile",
|
|
410
|
+
"exportStore",
|
|
411
|
+
"exportAccountFile",
|
|
412
|
+
"listTags",
|
|
413
|
+
"getSetting",
|
|
414
|
+
"listSettingKeys",
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Module-level map tracking whether the current tab already holds the
|
|
419
|
+
* cross-tab Web Lock for a given store name. Keyed by storeName so that
|
|
420
|
+
* two WebClient instances targeting the same store correctly detect
|
|
421
|
+
* re-entrancy and skip re-acquiring the lock (which would deadlock).
|
|
422
|
+
*/
|
|
423
|
+
const _writeLockHeldByStore = new Map();
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create the Proxy that wraps a WebClient instance. The proxy:
|
|
427
|
+
* - Returns properties from the wrapper (instance) first.
|
|
428
|
+
* - Falls back to the underlying WASM WebClient, wrapping function calls
|
|
429
|
+
* through the in-process WASM lock (Layer 1) and, for write methods,
|
|
430
|
+
* the cross-tab write lock (Layer 2).
|
|
431
|
+
*/
|
|
432
|
+
function createClientProxy(instance) {
|
|
433
|
+
return new Proxy(instance, {
|
|
434
|
+
get(target, prop, receiver) {
|
|
435
|
+
// If the property exists on the wrapper, return it.
|
|
436
|
+
if (prop in target) {
|
|
437
|
+
return Reflect.get(target, prop, receiver);
|
|
438
|
+
}
|
|
439
|
+
// Otherwise, if the wasmWebClient has it, return that.
|
|
440
|
+
if (target.wasmWebClient && prop in target.wasmWebClient) {
|
|
441
|
+
const value = target.wasmWebClient[prop];
|
|
442
|
+
if (typeof value === "function") {
|
|
443
|
+
// Synchronous methods: call directly without async wrapping.
|
|
444
|
+
// These are pure-computation methods whose callers expect a
|
|
445
|
+
// synchronous return value.
|
|
446
|
+
if (SYNC_METHODS.has(prop)) {
|
|
447
|
+
return (...args) => value.apply(target.wasmWebClient, args);
|
|
448
|
+
} else if (WRITE_METHODS.has(prop)) {
|
|
449
|
+
// Write methods: cross-tab lock (outer) → WASM lock (inner)
|
|
450
|
+
return (...args) =>
|
|
451
|
+
target._withWrite(prop, () =>
|
|
452
|
+
target._wasmLock.runExclusive(() =>
|
|
453
|
+
value.apply(target.wasmWebClient, args)
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
} else if (READ_METHODS.has(prop)) {
|
|
457
|
+
// Read methods: WASM lock only
|
|
458
|
+
return (...args) =>
|
|
459
|
+
target._wasmLock.runExclusive(() =>
|
|
460
|
+
value.apply(target.wasmWebClient, args)
|
|
461
|
+
);
|
|
462
|
+
} else {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`Unclassified WASM method: "${prop}". Add it to SYNC_METHODS, WRITE_METHODS, or READ_METHODS.`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
return undefined;
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// WASM MODULE LOADING
|
|
476
|
+
// ================================================================================================
|
|
477
|
+
|
|
242
478
|
const buildTypedArraysExport = (exportObject) => {
|
|
243
479
|
return Object.entries(exportObject).reduce(
|
|
244
480
|
(exports, [exportName, _export]) => {
|
|
@@ -307,6 +543,10 @@ const getWasmOrThrow = async () => {
|
|
|
307
543
|
}
|
|
308
544
|
return module;
|
|
309
545
|
};
|
|
546
|
+
|
|
547
|
+
// WEB CLIENT
|
|
548
|
+
// ================================================================================================
|
|
549
|
+
|
|
310
550
|
/**
|
|
311
551
|
* WebClient is a wrapper around the underlying WASM WebClient object.
|
|
312
552
|
*
|
|
@@ -323,6 +563,18 @@ const getWasmOrThrow = async () => {
|
|
|
323
563
|
* 3. It employs a Proxy to forward any calls not designated for web worker computation
|
|
324
564
|
* directly to the underlying WASM WebClient instance.
|
|
325
565
|
*
|
|
566
|
+
* Concurrency safety is provided by three layers:
|
|
567
|
+
*
|
|
568
|
+
* - **Layer 1 (In-Process AsyncLock):** All main-thread WASM calls are serialized
|
|
569
|
+
* through `_wasmLock` to prevent "recursive use of an object detected" panics.
|
|
570
|
+
*
|
|
571
|
+
* - **Layer 2 (Cross-Tab Write Lock):** Mutating operations acquire an exclusive
|
|
572
|
+
* Web Lock (`miden-db-{storeName}`) so that writes from different tabs are
|
|
573
|
+
* serialized against the same IndexedDB database.
|
|
574
|
+
*
|
|
575
|
+
* - **Layer 3 (BroadcastChannel):** After every write, a notification is sent
|
|
576
|
+
* to all other tabs so they can refresh stale in-memory state.
|
|
577
|
+
*
|
|
326
578
|
* Additionally, the wrapper provides a static createClient function. This static method
|
|
327
579
|
* instantiates the WebClient object and ensures that the necessary createClient calls are
|
|
328
580
|
* performed both in the main thread and within the worker thread. This dual initialization
|
|
@@ -376,6 +628,55 @@ class WebClient {
|
|
|
376
628
|
this.signCb = signCb;
|
|
377
629
|
this.logLevel = logLevel;
|
|
378
630
|
|
|
631
|
+
// Layer 1: In-process WASM lock — serializes all main-thread WASM calls.
|
|
632
|
+
this._wasmLock = new AsyncLock();
|
|
633
|
+
|
|
634
|
+
// Layer 2 timeout: how long to wait to acquire the cross-tab write lock
|
|
635
|
+
// before aborting. Set to 0 to wait indefinitely. Can be overridden on
|
|
636
|
+
// the instance, e.g. client._writeLockTimeoutMs = 0.
|
|
637
|
+
this._writeLockTimeoutMs = 60_000;
|
|
638
|
+
|
|
639
|
+
// Layer 3: BroadcastChannel for cross-tab state-change notifications.
|
|
640
|
+
// If construction fails (e.g. unsupported WebView), Layer 3 is disabled
|
|
641
|
+
// but correctness is preserved: the cross-tab write lock (Layer 2) still
|
|
642
|
+
// prevents concurrent writes, and the next explicit user action will
|
|
643
|
+
// trigger a sync. Tabs just won't receive proactive refresh signals.
|
|
644
|
+
const channelName = `miden-state-${storeName || "default"}`;
|
|
645
|
+
try {
|
|
646
|
+
this._stateChannel =
|
|
647
|
+
typeof BroadcastChannel !== "undefined"
|
|
648
|
+
? new BroadcastChannel(channelName)
|
|
649
|
+
: null;
|
|
650
|
+
} catch {
|
|
651
|
+
this._stateChannel = null;
|
|
652
|
+
}
|
|
653
|
+
this._stateListeners = [];
|
|
654
|
+
if (this._stateChannel) {
|
|
655
|
+
this._stateChannel.onmessage = async (event) => {
|
|
656
|
+
// Ignore cross-tab messages until the client is fully initialized.
|
|
657
|
+
if (!this.wasmWebClient) return;
|
|
658
|
+
|
|
659
|
+
// Auto-sync: refresh in-memory Rust Client state from IndexedDB.
|
|
660
|
+
// Sync coalescing (in syncLock.js) ensures concurrent syncs share the
|
|
661
|
+
// same result, so rapid messages are handled without debouncing.
|
|
662
|
+
try {
|
|
663
|
+
await this.syncState();
|
|
664
|
+
} catch {
|
|
665
|
+
// Sync failure is non-fatal — the next explicit sync will retry.
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Invoke listeners AFTER syncState resolves so in-memory state
|
|
669
|
+
// is guaranteed fresh when callbacks run.
|
|
670
|
+
for (const listener of this._stateListeners) {
|
|
671
|
+
try {
|
|
672
|
+
listener(event.data);
|
|
673
|
+
} catch {
|
|
674
|
+
// Swallow listener errors.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
379
680
|
// Check if Web Workers are available.
|
|
380
681
|
if (typeof Worker !== "undefined") {
|
|
381
682
|
console.log("WebClient: Web Workers are available.");
|
|
@@ -479,6 +780,94 @@ class WebClient {
|
|
|
479
780
|
this.wasmWebClientPromise = null;
|
|
480
781
|
}
|
|
481
782
|
|
|
783
|
+
// CONCURRENCY HELPERS
|
|
784
|
+
// ================================================================================================
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Execute `fn` under the cross-tab write lock and broadcast a state-change
|
|
788
|
+
* notification when it completes. Safe to call re-entrantly within the same
|
|
789
|
+
* tab (the inner call skips the cross-tab lock since the outer call holds it).
|
|
790
|
+
*
|
|
791
|
+
* @param {string} operation - Name of the operation (for the broadcast payload).
|
|
792
|
+
* @param {() => Promise<T>} fn - The async work.
|
|
793
|
+
* @returns {Promise<T>}
|
|
794
|
+
* @template T
|
|
795
|
+
*/
|
|
796
|
+
async _withWrite(operation, fn) {
|
|
797
|
+
const storeName = this.storeName || "default";
|
|
798
|
+
|
|
799
|
+
if (_writeLockHeldByStore.get(storeName)) {
|
|
800
|
+
// This tab already holds the cross-tab Web Lock for this store —
|
|
801
|
+
// skip re-acquiring it to avoid deadlock. The outer call's lock
|
|
802
|
+
// still blocks other tabs. Works correctly even when two WebClient
|
|
803
|
+
// instances target the same store within the same tab.
|
|
804
|
+
return fn();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const result = await withWriteLock(
|
|
808
|
+
storeName,
|
|
809
|
+
async () => {
|
|
810
|
+
_writeLockHeldByStore.set(storeName, true);
|
|
811
|
+
try {
|
|
812
|
+
return await fn();
|
|
813
|
+
} finally {
|
|
814
|
+
_writeLockHeldByStore.set(storeName, false);
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
this._writeLockTimeoutMs
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
// Layer 3: notify other tabs. Skip for syncState — sync is not a
|
|
821
|
+
// user-facing mutation, and broadcasting it would cause a ping-pong
|
|
822
|
+
// loop (Tab A syncs → broadcasts → Tab B auto-syncs → broadcasts → …).
|
|
823
|
+
if (operation !== "syncState") {
|
|
824
|
+
this._broadcastStateChange(operation);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return result;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Send a state-change notification over the BroadcastChannel (Layer 3).
|
|
832
|
+
*
|
|
833
|
+
* @param {string} [operation] - Human-readable name of the operation.
|
|
834
|
+
*/
|
|
835
|
+
_broadcastStateChange(operation) {
|
|
836
|
+
if (this._stateChannel) {
|
|
837
|
+
try {
|
|
838
|
+
this._stateChannel.postMessage({
|
|
839
|
+
type: "stateChanged",
|
|
840
|
+
operation,
|
|
841
|
+
storeName: this.storeName || "default",
|
|
842
|
+
});
|
|
843
|
+
} catch {
|
|
844
|
+
// BroadcastChannel may be closed — ignore.
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Register a listener that is called when **another tab** mutates the same
|
|
851
|
+
* IndexedDB database (Layer 3). The WebClient automatically calls
|
|
852
|
+
* `syncState()` before invoking listeners, so the in-memory state is
|
|
853
|
+
* already refreshed when your callback runs. Use this for additional
|
|
854
|
+
* work like re-fetching accounts or updating UI.
|
|
855
|
+
*
|
|
856
|
+
* Returns an unsubscribe function.
|
|
857
|
+
*
|
|
858
|
+
* @param {(event: {type: string, operation?: string, storeName: string}) => void} callback
|
|
859
|
+
* @returns {() => void} Unsubscribe function.
|
|
860
|
+
*/
|
|
861
|
+
onStateChanged(callback) {
|
|
862
|
+
this._stateListeners.push(callback);
|
|
863
|
+
return () => {
|
|
864
|
+
this._stateListeners = this._stateListeners.filter((l) => l !== callback);
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// WORKER / WASM INITIALIZATION
|
|
869
|
+
// ================================================================================================
|
|
870
|
+
|
|
482
871
|
// TODO: This will soon conflict with some changes in main.
|
|
483
872
|
// More context here:
|
|
484
873
|
// https://github.com/0xMiden/miden-client/pull/1645?notification_referrer_id=NT_kwHOA1yg7NoAJVJlcG9zaXRvcnk7NjU5MzQzNzAyO0lzc3VlOzM3OTY4OTU1Nzk¬ifications_query=is%3Aunread#discussion_r2696075480
|
|
@@ -513,6 +902,9 @@ class WebClient {
|
|
|
513
902
|
return this.wasmWebClientPromise;
|
|
514
903
|
}
|
|
515
904
|
|
|
905
|
+
// FACTORY METHODS
|
|
906
|
+
// ================================================================================================
|
|
907
|
+
|
|
516
908
|
/**
|
|
517
909
|
* Factory method to create and initialize a WebClient instance.
|
|
518
910
|
* This method is async so you can await the asynchronous call to createClient().
|
|
@@ -550,24 +942,7 @@ class WebClient {
|
|
|
550
942
|
// Wait for the worker to be ready
|
|
551
943
|
await instance.ready;
|
|
552
944
|
|
|
553
|
-
|
|
554
|
-
return new Proxy(instance, {
|
|
555
|
-
get(target, prop, receiver) {
|
|
556
|
-
// If the property exists on the wrapper, return it.
|
|
557
|
-
if (prop in target) {
|
|
558
|
-
return Reflect.get(target, prop, receiver);
|
|
559
|
-
}
|
|
560
|
-
// Otherwise, if the wasmWebClient has it, return that.
|
|
561
|
-
if (target.wasmWebClient && prop in target.wasmWebClient) {
|
|
562
|
-
const value = target.wasmWebClient[prop];
|
|
563
|
-
if (typeof value === "function") {
|
|
564
|
-
return value.bind(target.wasmWebClient);
|
|
565
|
-
}
|
|
566
|
-
return value;
|
|
567
|
-
}
|
|
568
|
-
return undefined;
|
|
569
|
-
},
|
|
570
|
-
});
|
|
945
|
+
return createClientProxy(instance);
|
|
571
946
|
}
|
|
572
947
|
|
|
573
948
|
/**
|
|
@@ -625,24 +1000,8 @@ class WebClient {
|
|
|
625
1000
|
);
|
|
626
1001
|
|
|
627
1002
|
await instance.ready;
|
|
628
|
-
|
|
629
|
-
return
|
|
630
|
-
get(target, prop, receiver) {
|
|
631
|
-
// If the property exists on the wrapper, return it.
|
|
632
|
-
if (prop in target) {
|
|
633
|
-
return Reflect.get(target, prop, receiver);
|
|
634
|
-
}
|
|
635
|
-
// Otherwise, if the wasmWebClient has it, return that.
|
|
636
|
-
if (target.wasmWebClient && prop in target.wasmWebClient) {
|
|
637
|
-
const value = target.wasmWebClient[prop];
|
|
638
|
-
if (typeof value === "function") {
|
|
639
|
-
return value.bind(target.wasmWebClient);
|
|
640
|
-
}
|
|
641
|
-
return value;
|
|
642
|
-
}
|
|
643
|
-
return undefined;
|
|
644
|
-
},
|
|
645
|
-
});
|
|
1003
|
+
|
|
1004
|
+
return createClientProxy(instance);
|
|
646
1005
|
}
|
|
647
1006
|
|
|
648
1007
|
/**
|
|
@@ -668,33 +1027,38 @@ class WebClient {
|
|
|
668
1027
|
});
|
|
669
1028
|
}
|
|
670
1029
|
|
|
671
|
-
//
|
|
1030
|
+
// EXPLICITLY WRAPPED METHODS (Worker-Forwarded + Concurrency-Safe)
|
|
1031
|
+
// ================================================================================================
|
|
672
1032
|
|
|
673
1033
|
async newWallet(storageMode, mutable, authSchemeId, seed) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1034
|
+
return this._withWrite("newWallet", async () => {
|
|
1035
|
+
try {
|
|
1036
|
+
if (!this.worker) {
|
|
1037
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1038
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1039
|
+
return await wasmWebClient.newWallet(
|
|
1040
|
+
storageMode,
|
|
1041
|
+
mutable,
|
|
1042
|
+
authSchemeId,
|
|
1043
|
+
seed
|
|
1044
|
+
);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
const wasm = await getWasmOrThrow();
|
|
1048
|
+
const serializedStorageMode = storageMode.asStr();
|
|
1049
|
+
const serializedAccountBytes = await this.callMethodWithWorker(
|
|
1050
|
+
MethodName.NEW_WALLET,
|
|
1051
|
+
serializedStorageMode,
|
|
679
1052
|
mutable,
|
|
680
1053
|
authSchemeId,
|
|
681
1054
|
seed
|
|
682
1055
|
);
|
|
1056
|
+
return wasm.Account.deserialize(new Uint8Array(serializedAccountBytes));
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
console.error("INDEX.JS: Error in newWallet:", error);
|
|
1059
|
+
throw error;
|
|
683
1060
|
}
|
|
684
|
-
|
|
685
|
-
const serializedStorageMode = storageMode.asStr();
|
|
686
|
-
const serializedAccountBytes = await this.callMethodWithWorker(
|
|
687
|
-
MethodName.NEW_WALLET,
|
|
688
|
-
serializedStorageMode,
|
|
689
|
-
mutable,
|
|
690
|
-
authSchemeId,
|
|
691
|
-
seed
|
|
692
|
-
);
|
|
693
|
-
return wasm.Account.deserialize(new Uint8Array(serializedAccountBytes));
|
|
694
|
-
} catch (error) {
|
|
695
|
-
console.error("INDEX.JS: Error in newWallet:", error);
|
|
696
|
-
throw error;
|
|
697
|
-
}
|
|
1061
|
+
});
|
|
698
1062
|
}
|
|
699
1063
|
|
|
700
1064
|
async newFaucet(
|
|
@@ -705,134 +1069,157 @@ class WebClient {
|
|
|
705
1069
|
maxSupply,
|
|
706
1070
|
authSchemeId
|
|
707
1071
|
) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1072
|
+
return this._withWrite("newFaucet", async () => {
|
|
1073
|
+
try {
|
|
1074
|
+
if (!this.worker) {
|
|
1075
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1076
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1077
|
+
return await wasmWebClient.newFaucet(
|
|
1078
|
+
storageMode,
|
|
1079
|
+
nonFungible,
|
|
1080
|
+
tokenSymbol,
|
|
1081
|
+
decimals,
|
|
1082
|
+
maxSupply,
|
|
1083
|
+
authSchemeId
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const wasm = await getWasmOrThrow();
|
|
1088
|
+
const serializedStorageMode = storageMode.asStr();
|
|
1089
|
+
const serializedMaxSupply = maxSupply.toString();
|
|
1090
|
+
const serializedAccountBytes = await this.callMethodWithWorker(
|
|
1091
|
+
MethodName.NEW_FAUCET,
|
|
1092
|
+
serializedStorageMode,
|
|
713
1093
|
nonFungible,
|
|
714
1094
|
tokenSymbol,
|
|
715
1095
|
decimals,
|
|
716
|
-
|
|
1096
|
+
serializedMaxSupply,
|
|
717
1097
|
authSchemeId
|
|
718
1098
|
);
|
|
719
|
-
}
|
|
720
|
-
const wasm = await getWasmOrThrow();
|
|
721
|
-
const serializedStorageMode = storageMode.asStr();
|
|
722
|
-
const serializedMaxSupply = maxSupply.toString();
|
|
723
|
-
const serializedAccountBytes = await this.callMethodWithWorker(
|
|
724
|
-
MethodName.NEW_FAUCET,
|
|
725
|
-
serializedStorageMode,
|
|
726
|
-
nonFungible,
|
|
727
|
-
tokenSymbol,
|
|
728
|
-
decimals,
|
|
729
|
-
serializedMaxSupply,
|
|
730
|
-
authSchemeId
|
|
731
|
-
);
|
|
732
1099
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1100
|
+
return wasm.Account.deserialize(new Uint8Array(serializedAccountBytes));
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
console.error("INDEX.JS: Error in newFaucet:", error);
|
|
1103
|
+
throw error;
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
738
1106
|
}
|
|
739
1107
|
|
|
740
1108
|
async submitNewTransaction(accountId, transactionRequest) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1109
|
+
return this._withWrite("submitNewTransaction", async () => {
|
|
1110
|
+
try {
|
|
1111
|
+
if (!this.worker) {
|
|
1112
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1113
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1114
|
+
return await wasmWebClient.submitNewTransaction(
|
|
1115
|
+
accountId,
|
|
1116
|
+
transactionRequest
|
|
1117
|
+
);
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
749
1120
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1121
|
+
const wasm = await getWasmOrThrow();
|
|
1122
|
+
const serializedTransactionRequest = transactionRequest.serialize();
|
|
1123
|
+
const result = await this.callMethodWithWorker(
|
|
1124
|
+
MethodName.SUBMIT_NEW_TRANSACTION,
|
|
1125
|
+
accountId.toString(),
|
|
1126
|
+
serializedTransactionRequest
|
|
1127
|
+
);
|
|
757
1128
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1129
|
+
const transactionResult = wasm.TransactionResult.deserialize(
|
|
1130
|
+
new Uint8Array(result.serializedTransactionResult)
|
|
1131
|
+
);
|
|
761
1132
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1133
|
+
return transactionResult.id();
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
console.error("INDEX.JS: Error in submitNewTransaction:", error);
|
|
1136
|
+
throw error;
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
767
1139
|
}
|
|
768
1140
|
|
|
769
1141
|
async submitNewTransactionWithProver(accountId, transactionRequest, prover) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1142
|
+
return this._withWrite("submitNewTransactionWithProver", async () => {
|
|
1143
|
+
try {
|
|
1144
|
+
if (!this.worker) {
|
|
1145
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1146
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1147
|
+
return await wasmWebClient.submitNewTransactionWithProver(
|
|
1148
|
+
accountId,
|
|
1149
|
+
transactionRequest,
|
|
1150
|
+
prover
|
|
1151
|
+
);
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
779
1154
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1155
|
+
const wasm = await getWasmOrThrow();
|
|
1156
|
+
const serializedTransactionRequest = transactionRequest.serialize();
|
|
1157
|
+
const proverPayload = prover.serialize();
|
|
1158
|
+
const result = await this.callMethodWithWorker(
|
|
1159
|
+
MethodName.SUBMIT_NEW_TRANSACTION_WITH_PROVER,
|
|
1160
|
+
accountId.toString(),
|
|
1161
|
+
serializedTransactionRequest,
|
|
1162
|
+
proverPayload
|
|
1163
|
+
);
|
|
789
1164
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1165
|
+
const transactionResult = wasm.TransactionResult.deserialize(
|
|
1166
|
+
new Uint8Array(result.serializedTransactionResult)
|
|
1167
|
+
);
|
|
793
1168
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1169
|
+
return transactionResult.id();
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
console.error(
|
|
1172
|
+
"INDEX.JS: Error in submitNewTransactionWithProver:",
|
|
1173
|
+
error
|
|
1174
|
+
);
|
|
1175
|
+
throw error;
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
802
1178
|
}
|
|
803
1179
|
|
|
804
1180
|
async executeTransaction(accountId, transactionRequest) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1181
|
+
return this._withWrite("executeTransaction", async () => {
|
|
1182
|
+
try {
|
|
1183
|
+
if (!this.worker) {
|
|
1184
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1185
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1186
|
+
return await wasmWebClient.executeTransaction(
|
|
1187
|
+
accountId,
|
|
1188
|
+
transactionRequest
|
|
1189
|
+
);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
813
1192
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1193
|
+
const wasm = await getWasmOrThrow();
|
|
1194
|
+
const serializedTransactionRequest = transactionRequest.serialize();
|
|
1195
|
+
const serializedResultBytes = await this.callMethodWithWorker(
|
|
1196
|
+
MethodName.EXECUTE_TRANSACTION,
|
|
1197
|
+
accountId.toString(),
|
|
1198
|
+
serializedTransactionRequest
|
|
1199
|
+
);
|
|
821
1200
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1201
|
+
return wasm.TransactionResult.deserialize(
|
|
1202
|
+
new Uint8Array(serializedResultBytes)
|
|
1203
|
+
);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
console.error("INDEX.JS: Error in executeTransaction:", error);
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
829
1209
|
}
|
|
830
1210
|
|
|
1211
|
+
// proveTransaction is CPU-heavy but does NOT write to IndexedDB, so it only
|
|
1212
|
+
// needs the in-process WASM lock (Layer 1), not the cross-tab write lock.
|
|
831
1213
|
async proveTransaction(transactionResult, prover) {
|
|
832
1214
|
try {
|
|
833
1215
|
if (!this.worker) {
|
|
834
|
-
|
|
835
|
-
|
|
1216
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1217
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1218
|
+
return await wasmWebClient.proveTransaction(
|
|
1219
|
+
transactionResult,
|
|
1220
|
+
prover
|
|
1221
|
+
);
|
|
1222
|
+
});
|
|
836
1223
|
}
|
|
837
1224
|
|
|
838
1225
|
const wasm = await getWasmOrThrow();
|
|
@@ -854,6 +1241,52 @@ class WebClient {
|
|
|
854
1241
|
}
|
|
855
1242
|
}
|
|
856
1243
|
|
|
1244
|
+
// submitProvenTransaction and applyTransaction sync state before the write
|
|
1245
|
+
// to catch cross-tab writes that occurred between execute and submit.
|
|
1246
|
+
// This ensures the local IndexedDB view is fresh before submission.
|
|
1247
|
+
|
|
1248
|
+
async submitProvenTransaction(provenTransaction, transactionResult) {
|
|
1249
|
+
return this._withWrite("submitProvenTransaction", async () => {
|
|
1250
|
+
// Sync state to catch cross-tab writes that occurred since execute.
|
|
1251
|
+
// We call syncStateImpl directly (bypassing the sync lock) because we
|
|
1252
|
+
// already hold the write lock — calling syncState() would attempt to
|
|
1253
|
+
// acquire sync lock → write lock, which is the reverse of the normal
|
|
1254
|
+
// sync lock → write lock order and could deadlock across tabs.
|
|
1255
|
+
try {
|
|
1256
|
+
await this._wasmLock.runExclusive(async () => {
|
|
1257
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1258
|
+
await wasmWebClient.syncStateImpl();
|
|
1259
|
+
});
|
|
1260
|
+
} catch {
|
|
1261
|
+
// Sync failure is non-fatal — proceed with submission.
|
|
1262
|
+
// The node will reject truly invalid transactions.
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1266
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1267
|
+
return await wasmWebClient.submitProvenTransaction(
|
|
1268
|
+
provenTransaction,
|
|
1269
|
+
transactionResult
|
|
1270
|
+
);
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async applyTransaction(provenTransaction, transactionResult) {
|
|
1276
|
+
return this._withWrite("applyTransaction", async () => {
|
|
1277
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1278
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1279
|
+
return await wasmWebClient.applyTransaction(
|
|
1280
|
+
provenTransaction,
|
|
1281
|
+
transactionResult
|
|
1282
|
+
);
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// SYNC
|
|
1288
|
+
// ================================================================================================
|
|
1289
|
+
|
|
857
1290
|
/**
|
|
858
1291
|
* Syncs the client state with the node.
|
|
859
1292
|
*
|
|
@@ -861,6 +1294,9 @@ class WebClient {
|
|
|
861
1294
|
* with an in-process mutex fallback for older browsers. If a sync is already in progress,
|
|
862
1295
|
* subsequent callers will wait and receive the same result (coalescing behavior).
|
|
863
1296
|
*
|
|
1297
|
+
* Sync also acquires the cross-tab write lock (Layer 2) so that it does not
|
|
1298
|
+
* interleave with writes from other tabs.
|
|
1299
|
+
*
|
|
864
1300
|
* @returns {Promise<SyncSummary>} The sync summary
|
|
865
1301
|
*/
|
|
866
1302
|
async syncState() {
|
|
@@ -874,6 +1310,8 @@ class WebClient {
|
|
|
874
1310
|
* with an in-process mutex fallback for older browsers. If a sync is already in progress,
|
|
875
1311
|
* subsequent callers will wait and receive the same result (coalescing behavior).
|
|
876
1312
|
*
|
|
1313
|
+
* Lock nesting order: Sync Lock (coalescing, outer) → Write Lock → WASM Lock (inner).
|
|
1314
|
+
*
|
|
877
1315
|
* @param {number} timeoutMs - Timeout in milliseconds (0 = no timeout)
|
|
878
1316
|
* @returns {Promise<SyncSummary>} The sync summary
|
|
879
1317
|
*/
|
|
@@ -882,7 +1320,7 @@ class WebClient {
|
|
|
882
1320
|
const dbId = this.storeName || "default";
|
|
883
1321
|
|
|
884
1322
|
try {
|
|
885
|
-
// Acquire the sync lock (coordinates concurrent calls)
|
|
1323
|
+
// Acquire the sync lock (coordinates concurrent calls via coalescing)
|
|
886
1324
|
const lockHandle = await acquireSyncLock(dbId, timeoutMs);
|
|
887
1325
|
|
|
888
1326
|
if (!lockHandle.acquired) {
|
|
@@ -890,27 +1328,31 @@ class WebClient {
|
|
|
890
1328
|
return lockHandle.coalescedResult;
|
|
891
1329
|
}
|
|
892
1330
|
|
|
893
|
-
// We acquired the lock
|
|
1331
|
+
// We acquired the sync lock. Now acquire the write lock so that
|
|
1332
|
+
// sync doesn't interleave with writes from other tabs.
|
|
894
1333
|
try {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1334
|
+
const result = await this._withWrite("syncState", async () => {
|
|
1335
|
+
if (!this.worker) {
|
|
1336
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1337
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1338
|
+
return await wasmWebClient.syncStateImpl();
|
|
1339
|
+
});
|
|
1340
|
+
} else {
|
|
1341
|
+
const wasm = await getWasmOrThrow();
|
|
1342
|
+
const serializedSyncSummaryBytes = await this.callMethodWithWorker(
|
|
1343
|
+
MethodName.SYNC_STATE
|
|
1344
|
+
);
|
|
1345
|
+
return wasm.SyncSummary.deserialize(
|
|
1346
|
+
new Uint8Array(serializedSyncSummaryBytes)
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
908
1350
|
|
|
909
|
-
// Release the lock with the result
|
|
1351
|
+
// Release the sync lock with the result
|
|
910
1352
|
releaseSyncLock(dbId, result);
|
|
911
1353
|
return result;
|
|
912
1354
|
} catch (error) {
|
|
913
|
-
// Release the lock with the error
|
|
1355
|
+
// Release the sync lock with the error
|
|
914
1356
|
releaseSyncLockWithError(dbId, error);
|
|
915
1357
|
throw error;
|
|
916
1358
|
}
|
|
@@ -920,13 +1362,39 @@ class WebClient {
|
|
|
920
1362
|
}
|
|
921
1363
|
}
|
|
922
1364
|
|
|
1365
|
+
/**
|
|
1366
|
+
* Replace the sign callback on a live client instance.
|
|
1367
|
+
* This allows hot-swapping the signer without recreating the client.
|
|
1368
|
+
*
|
|
1369
|
+
* @param {(pubKey: Uint8Array, signingInputs: Uint8Array) => Promise<Uint8Array> | Uint8Array} signCb
|
|
1370
|
+
* - The new sign callback, or null/undefined to clear it.
|
|
1371
|
+
*/
|
|
1372
|
+
setSignCb(signCb) {
|
|
1373
|
+
this.signCb = signCb ?? null;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// LIFECYCLE
|
|
1377
|
+
// ================================================================================================
|
|
1378
|
+
|
|
923
1379
|
terminate() {
|
|
924
1380
|
if (this.worker) {
|
|
925
1381
|
this.worker.terminate();
|
|
926
1382
|
}
|
|
1383
|
+
if (this._stateChannel) {
|
|
1384
|
+
try {
|
|
1385
|
+
this._stateChannel.close();
|
|
1386
|
+
} catch {
|
|
1387
|
+
// Already closed — ignore.
|
|
1388
|
+
}
|
|
1389
|
+
this._stateChannel = null;
|
|
1390
|
+
}
|
|
1391
|
+
this._stateListeners = [];
|
|
927
1392
|
}
|
|
928
1393
|
}
|
|
929
1394
|
|
|
1395
|
+
// MOCK WEB CLIENT
|
|
1396
|
+
// ================================================================================================
|
|
1397
|
+
|
|
930
1398
|
class MockWebClient extends WebClient {
|
|
931
1399
|
constructor(seed, logLevel) {
|
|
932
1400
|
super(null, null, seed, "mock", undefined, undefined, undefined, logLevel);
|
|
@@ -973,24 +1441,7 @@ class MockWebClient extends WebClient {
|
|
|
973
1441
|
// Wait for the worker to be ready
|
|
974
1442
|
await instance.ready;
|
|
975
1443
|
|
|
976
|
-
|
|
977
|
-
return new Proxy(instance, {
|
|
978
|
-
get(target, prop, receiver) {
|
|
979
|
-
// If the property exists on the wrapper, return it.
|
|
980
|
-
if (prop in target) {
|
|
981
|
-
return Reflect.get(target, prop, receiver);
|
|
982
|
-
}
|
|
983
|
-
// Otherwise, if the wasmWebClient has it, return that.
|
|
984
|
-
if (target.wasmWebClient && prop in target.wasmWebClient) {
|
|
985
|
-
const value = target.wasmWebClient[prop];
|
|
986
|
-
if (typeof value === "function") {
|
|
987
|
-
return value.bind(target.wasmWebClient);
|
|
988
|
-
}
|
|
989
|
-
return value;
|
|
990
|
-
}
|
|
991
|
-
return undefined;
|
|
992
|
-
},
|
|
993
|
-
});
|
|
1444
|
+
return createClientProxy(instance);
|
|
994
1445
|
}
|
|
995
1446
|
|
|
996
1447
|
/**
|
|
@@ -1023,15 +1474,23 @@ class MockWebClient extends WebClient {
|
|
|
1023
1474
|
}
|
|
1024
1475
|
|
|
1025
1476
|
try {
|
|
1026
|
-
|
|
1027
|
-
|
|
1477
|
+
const result = await this._withWrite("syncState", async () => {
|
|
1478
|
+
if (!this.worker) {
|
|
1479
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1480
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1481
|
+
return wasmWebClient.syncStateImpl();
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1028
1484
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1485
|
+
const { serializedMockChain, serializedMockNoteTransportNode } =
|
|
1486
|
+
await this._wasmLock.runExclusive(async () => {
|
|
1487
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1488
|
+
return {
|
|
1489
|
+
serializedMockChain: wasmWebClient.serializeMockChain().buffer,
|
|
1490
|
+
serializedMockNoteTransportNode:
|
|
1491
|
+
wasmWebClient.serializeMockNoteTransportNode().buffer,
|
|
1492
|
+
};
|
|
1493
|
+
});
|
|
1035
1494
|
|
|
1036
1495
|
const wasm = await getWasmOrThrow();
|
|
1037
1496
|
|
|
@@ -1041,10 +1500,10 @@ class MockWebClient extends WebClient {
|
|
|
1041
1500
|
serializedMockNoteTransportNode
|
|
1042
1501
|
);
|
|
1043
1502
|
|
|
1044
|
-
|
|
1503
|
+
return wasm.SyncSummary.deserialize(
|
|
1045
1504
|
new Uint8Array(serializedSyncSummaryBytes)
|
|
1046
1505
|
);
|
|
1047
|
-
}
|
|
1506
|
+
});
|
|
1048
1507
|
|
|
1049
1508
|
releaseSyncLock(dbId, result);
|
|
1050
1509
|
return result;
|
|
@@ -1059,113 +1518,139 @@ class MockWebClient extends WebClient {
|
|
|
1059
1518
|
}
|
|
1060
1519
|
|
|
1061
1520
|
async submitNewTransaction(accountId, transactionRequest) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1521
|
+
return this._withWrite("submitNewTransaction", async () => {
|
|
1522
|
+
try {
|
|
1523
|
+
if (!this.worker) {
|
|
1524
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1525
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1526
|
+
return await wasmWebClient.submitNewTransaction(
|
|
1527
|
+
accountId,
|
|
1528
|
+
transactionRequest
|
|
1529
|
+
);
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1066
1532
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
serializedMockChain,
|
|
1079
|
-
serializedMockNoteTransportNode
|
|
1080
|
-
);
|
|
1533
|
+
const wasm = await getWasmOrThrow();
|
|
1534
|
+
const serializedTransactionRequest = transactionRequest.serialize();
|
|
1535
|
+
const { serializedMockChain, serializedMockNoteTransportNode } =
|
|
1536
|
+
await this._wasmLock.runExclusive(async () => {
|
|
1537
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1538
|
+
return {
|
|
1539
|
+
serializedMockChain: wasmWebClient.serializeMockChain().buffer,
|
|
1540
|
+
serializedMockNoteTransportNode:
|
|
1541
|
+
wasmWebClient.serializeMockNoteTransportNode().buffer,
|
|
1542
|
+
};
|
|
1543
|
+
});
|
|
1081
1544
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1545
|
+
const result = await this.callMethodWithWorker(
|
|
1546
|
+
MethodName.SUBMIT_NEW_TRANSACTION_MOCK,
|
|
1547
|
+
accountId.toString(),
|
|
1548
|
+
serializedTransactionRequest,
|
|
1549
|
+
serializedMockChain,
|
|
1550
|
+
serializedMockNoteTransportNode
|
|
1551
|
+
);
|
|
1086
1552
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1553
|
+
const newMockChain = new Uint8Array(result.serializedMockChain);
|
|
1554
|
+
const newMockNoteTransportNode = result.serializedMockNoteTransportNode
|
|
1555
|
+
? new Uint8Array(result.serializedMockNoteTransportNode)
|
|
1556
|
+
: undefined;
|
|
1090
1557
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1558
|
+
const transactionResult = wasm.TransactionResult.deserialize(
|
|
1559
|
+
new Uint8Array(result.serializedTransactionResult)
|
|
1560
|
+
);
|
|
1094
1561
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
this.seed,
|
|
1099
|
-
newMockChain,
|
|
1100
|
-
newMockNoteTransportNode
|
|
1101
|
-
);
|
|
1562
|
+
if (!(this instanceof MockWebClient)) {
|
|
1563
|
+
return transactionResult.id();
|
|
1564
|
+
}
|
|
1102
1565
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1566
|
+
this.wasmWebClient = new wasm.WebClient();
|
|
1567
|
+
this.wasmWebClientPromise = Promise.resolve(this.wasmWebClient);
|
|
1568
|
+
await this.wasmWebClient.createMockClient(
|
|
1569
|
+
this.seed,
|
|
1570
|
+
newMockChain,
|
|
1571
|
+
newMockNoteTransportNode
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
return transactionResult.id();
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
console.error("INDEX.JS: Error in submitNewTransaction:", error);
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1108
1580
|
}
|
|
1109
1581
|
|
|
1110
1582
|
async submitNewTransactionWithProver(accountId, transactionRequest, prover) {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1583
|
+
return this._withWrite("submitNewTransactionWithProver", async () => {
|
|
1584
|
+
try {
|
|
1585
|
+
if (!this.worker) {
|
|
1586
|
+
return await this._wasmLock.runExclusive(async () => {
|
|
1587
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1588
|
+
return await wasmWebClient.submitNewTransactionWithProver(
|
|
1589
|
+
accountId,
|
|
1590
|
+
transactionRequest,
|
|
1591
|
+
prover
|
|
1592
|
+
);
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const wasm = await getWasmOrThrow();
|
|
1597
|
+
const serializedTransactionRequest = transactionRequest.serialize();
|
|
1598
|
+
const proverPayload = prover.serialize();
|
|
1599
|
+
const { serializedMockChain, serializedMockNoteTransportNode } =
|
|
1600
|
+
await this._wasmLock.runExclusive(async () => {
|
|
1601
|
+
const wasmWebClient = await this.getWasmWebClient();
|
|
1602
|
+
return {
|
|
1603
|
+
serializedMockChain: wasmWebClient.serializeMockChain().buffer,
|
|
1604
|
+
serializedMockNoteTransportNode:
|
|
1605
|
+
wasmWebClient.serializeMockNoteTransportNode().buffer,
|
|
1606
|
+
};
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
const result = await this.callMethodWithWorker(
|
|
1610
|
+
MethodName.SUBMIT_NEW_TRANSACTION_WITH_PROVER_MOCK,
|
|
1611
|
+
accountId.toString(),
|
|
1612
|
+
serializedTransactionRequest,
|
|
1613
|
+
proverPayload,
|
|
1614
|
+
serializedMockChain,
|
|
1615
|
+
serializedMockNoteTransportNode
|
|
1117
1616
|
);
|
|
1118
|
-
}
|
|
1119
1617
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const serializedMockChain = wasmWebClient.serializeMockChain().buffer;
|
|
1125
|
-
const serializedMockNoteTransportNode =
|
|
1126
|
-
wasmWebClient.serializeMockNoteTransportNode().buffer;
|
|
1127
|
-
|
|
1128
|
-
const result = await this.callMethodWithWorker(
|
|
1129
|
-
MethodName.SUBMIT_NEW_TRANSACTION_WITH_PROVER_MOCK,
|
|
1130
|
-
accountId.toString(),
|
|
1131
|
-
serializedTransactionRequest,
|
|
1132
|
-
proverPayload,
|
|
1133
|
-
serializedMockChain,
|
|
1134
|
-
serializedMockNoteTransportNode
|
|
1135
|
-
);
|
|
1618
|
+
const newMockChain = new Uint8Array(result.serializedMockChain);
|
|
1619
|
+
const newMockNoteTransportNode = result.serializedMockNoteTransportNode
|
|
1620
|
+
? new Uint8Array(result.serializedMockNoteTransportNode)
|
|
1621
|
+
: undefined;
|
|
1136
1622
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
: undefined;
|
|
1623
|
+
const transactionResult = wasm.TransactionResult.deserialize(
|
|
1624
|
+
new Uint8Array(result.serializedTransactionResult)
|
|
1625
|
+
);
|
|
1141
1626
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1627
|
+
if (!(this instanceof MockWebClient)) {
|
|
1628
|
+
return transactionResult.id();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
this.wasmWebClient = new wasm.WebClient();
|
|
1632
|
+
this.wasmWebClientPromise = Promise.resolve(this.wasmWebClient);
|
|
1633
|
+
await this.wasmWebClient.createMockClient(
|
|
1634
|
+
this.seed,
|
|
1635
|
+
newMockChain,
|
|
1636
|
+
newMockNoteTransportNode
|
|
1637
|
+
);
|
|
1145
1638
|
|
|
1146
|
-
if (!(this instanceof MockWebClient)) {
|
|
1147
1639
|
return transactionResult.id();
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
console.error(
|
|
1642
|
+
"INDEX.JS: Error in submitNewTransactionWithProver:",
|
|
1643
|
+
error
|
|
1644
|
+
);
|
|
1645
|
+
throw error;
|
|
1148
1646
|
}
|
|
1149
|
-
|
|
1150
|
-
this.wasmWebClient = new wasm.WebClient();
|
|
1151
|
-
this.wasmWebClientPromise = Promise.resolve(this.wasmWebClient);
|
|
1152
|
-
await this.wasmWebClient.createMockClient(
|
|
1153
|
-
this.seed,
|
|
1154
|
-
newMockChain,
|
|
1155
|
-
newMockNoteTransportNode
|
|
1156
|
-
);
|
|
1157
|
-
|
|
1158
|
-
return transactionResult.id();
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
console.error(
|
|
1161
|
-
"INDEX.JS: Error in submitNewTransactionWithProver:",
|
|
1162
|
-
error
|
|
1163
|
-
);
|
|
1164
|
-
throw error;
|
|
1165
|
-
}
|
|
1647
|
+
});
|
|
1166
1648
|
}
|
|
1167
1649
|
}
|
|
1168
1650
|
|
|
1651
|
+
// STATICS
|
|
1652
|
+
// ================================================================================================
|
|
1653
|
+
|
|
1169
1654
|
function copyWebClientStatics(WasmWebClient) {
|
|
1170
1655
|
if (!WasmWebClient) {
|
|
1171
1656
|
return;
|