@effectstream/midnight-contracts 0.100.20 → 0.100.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/midnight-contracts",
3
- "version": "0.100.20",
3
+ "version": "0.100.21",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -17,27 +17,26 @@
17
17
  "./deploy": "./src/deploy.ts",
18
18
  "./wallet-info": "./src/get-wallet-info.ts",
19
19
  "./midnight-env": "./src/midnight-env.ts",
20
+ "./ledger-from-tx-state": "./src/ledger-from-tx-state.ts",
20
21
  "./types": "./src/types.ts"
21
22
  },
22
23
  "dependencies": {
23
- "@effectstream/utils": "0.100.20",
24
+ "@effectstream/utils": "0.100.21",
24
25
  "@scure/bip39": "^2.0.1",
25
26
  "dotenv": "^16.4.7",
26
- "@midnight-ntwrk/wallet": "5.0.0",
27
- "@midnight-ntwrk/wallet-api": "5.0.0",
28
27
  "@midnight-ntwrk/zswap": "4.0.0",
29
28
  "@midnight-ntwrk/ledger": "4.0.0",
30
29
  "rxjs": "^7.8.2",
31
30
  "@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
32
- "@midnight-ntwrk/midnight-js-network-id": "4.0.2",
33
- "@midnight-ntwrk/midnight-js-types": "4.0.2",
34
- "@midnight-ntwrk/midnight-js-utils": "4.0.2",
31
+ "@midnight-ntwrk/midnight-js-network-id": "4.0.4",
32
+ "@midnight-ntwrk/midnight-js-types": "4.0.4",
33
+ "@midnight-ntwrk/midnight-js-utils": "4.0.4",
35
34
  "@midnight-ntwrk/onchain-runtime-v3": "3.0.0",
36
- "@midnight-ntwrk/midnight-js-contracts": "4.0.2",
37
- "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
38
- "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
39
- "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
40
- "@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
35
+ "@midnight-ntwrk/midnight-js-contracts": "4.0.4",
36
+ "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
37
+ "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.4",
38
+ "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.4",
39
+ "@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.4",
41
40
  "@midnight-ntwrk/wallet-sdk-abstractions": "2.0.0",
42
41
  "@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
43
42
  "@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
package/src/deploy.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  // TODO Remove references to "src/managed" as this is not standard.
2
2
 
3
+ // Single WASM instance for ContractState / ContractMaintenanceAuthority.
4
+ import "@midnight-ntwrk/onchain-runtime-v3";
5
+
3
6
  import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
4
7
  import { Buffer } from "node:buffer";
5
8
  import { statSync, readdirSync } from "node:fs";
@@ -18,7 +18,6 @@ import {
18
18
  LedgerParameters,
19
19
  ZswapSecretKeys,
20
20
  shieldedToken,
21
- type UnprovenTransaction,
22
21
  } from "@midnight-ntwrk/ledger-v8";
23
22
 
24
23
  const log = console;
@@ -166,6 +165,150 @@ export function safeStringifyProgress(value: unknown): string {
166
165
  }
167
166
  }
168
167
 
168
+ /** Read spendable dust from façade state or a dust sub-wallet emission. */
169
+ export function resolveFacadeDustBalance(
170
+ facadeOrDustState: unknown,
171
+ at: Date = new Date(),
172
+ ): bigint {
173
+ const d =
174
+ (facadeOrDustState as { dust?: unknown } | null | undefined)?.dust ??
175
+ facadeOrDustState;
176
+ if (d === undefined || d === null || typeof d !== "object") return 0n;
177
+ let w = 0n;
178
+ let b = 0n;
179
+ try {
180
+ const fn = (d as { walletBalance?: (t: Date) => unknown }).walletBalance;
181
+ if (typeof fn === "function") {
182
+ const v = fn.call(d, at);
183
+ if (typeof v === "bigint") w = v;
184
+ }
185
+ } catch {
186
+ /* ignore transient read errors until dust subtree fully wires */
187
+ }
188
+ try {
189
+ const fn = (d as { balance?: (t: Date) => unknown }).balance;
190
+ if (typeof fn === "function") {
191
+ const v = fn.call(d, at);
192
+ if (typeof v === "bigint") b = v;
193
+ }
194
+ } catch {
195
+ /* ignore */
196
+ }
197
+ return w > b ? w : b;
198
+ }
199
+
200
+ function resolveDustHeartbeatPollMs(waitNonZero: boolean, override?: number): number {
201
+ if (override !== undefined) return Math.max(0, Math.floor(override));
202
+ if (!waitNonZero) return 0;
203
+ const raw = getEnv("MIDNIGHT_WAIT_DUST_POLL_INTERVAL_MS");
204
+ if (raw !== undefined && raw !== "") {
205
+ if (raw === "0") return 0;
206
+ const n = Number(raw);
207
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
208
+ }
209
+ return 7500;
210
+ }
211
+
212
+ function dustWalletState$(wallet: WalletFacade): Rx.Observable<unknown> {
213
+ const dust = (wallet.dust as { state?: Rx.Observable<unknown> }).state;
214
+ if (!dust || typeof dust.pipe !== "function") {
215
+ throw new Error("Dust wallet state stream is not available");
216
+ }
217
+ return dust;
218
+ }
219
+
220
+ /** Heartbeat over dust sub-wallet — safe after dust-only stops shielded/unshielded. */
221
+ function dustWalletStateWithHeartbeat(
222
+ wallet: WalletFacade,
223
+ pollMs: number,
224
+ ): Rx.Observable<unknown> {
225
+ const dust$ = dustWalletState$(wallet);
226
+ if (pollMs <= 0) return dust$;
227
+ return Rx.combineLatest([
228
+ dust$,
229
+ Rx.interval(pollMs).pipe(Rx.startWith(-1 as const)),
230
+ ]).pipe(Rx.map(([s]) => s));
231
+ }
232
+
233
+ function dustProgressFromState(ds: unknown): {
234
+ appliedIndex: bigint;
235
+ highestRelevantWalletIndex: bigint;
236
+ isConnected: boolean;
237
+ } | null {
238
+ const progress =
239
+ (ds as { state?: { progress?: unknown } })?.state?.progress ??
240
+ (ds as { progress?: unknown })?.progress;
241
+ if (!progress || typeof progress !== "object") return null;
242
+ const p = progress as {
243
+ appliedIndex?: bigint;
244
+ highestRelevantWalletIndex?: bigint;
245
+ isConnected?: boolean;
246
+ isStrictlyComplete?: () => boolean;
247
+ isCompleteWithin?: (gap: bigint) => boolean;
248
+ };
249
+ return {
250
+ appliedIndex: p.appliedIndex ?? 0n,
251
+ highestRelevantWalletIndex: p.highestRelevantWalletIndex ?? 0n,
252
+ isConnected: p.isConnected ?? false,
253
+ };
254
+ }
255
+
256
+ function isDustProgressCaughtUp(ds: unknown): boolean {
257
+ const progress =
258
+ (ds as { state?: { progress?: unknown } })?.state?.progress ??
259
+ (ds as { progress?: unknown })?.progress;
260
+ if (!progress || typeof progress !== "object") return false;
261
+ const p = progress as {
262
+ appliedIndex?: bigint;
263
+ highestRelevantWalletIndex?: bigint;
264
+ isStrictlyComplete?: () => boolean;
265
+ };
266
+ if (typeof p.isStrictlyComplete === "function" && p.isStrictlyComplete()) return true;
267
+ const applied = p.appliedIndex ?? 0n;
268
+ const target = p.highestRelevantWalletIndex ?? 0n;
269
+ if (applied === 0n && target === 0n) return true;
270
+ return target > 0n && applied >= target;
271
+ }
272
+
273
+ /** Wait until dust applied index reaches chain tip (not just DUST_COMPLETION_GAP). */
274
+ async function waitForDustWalletProgressCatchUp(
275
+ wallet: WalletFacade,
276
+ timeoutMs: number,
277
+ ): Promise<void> {
278
+ if (timeoutMs <= 0) return;
279
+ const startedAt = Date.now();
280
+ await Rx.firstValueFrom(
281
+ dustWalletState$(wallet).pipe(
282
+ Rx.tap((ds) => {
283
+ const p = dustProgressFromState(ds);
284
+ if (!p) return;
285
+ const elapsedSec = ((Date.now() - startedAt) / 1000).toFixed(0);
286
+ log.info(
287
+ `Dust catch-up (${elapsedSec}s): ${p.appliedIndex}/${p.highestRelevantWalletIndex} connected=${p.isConnected}`,
288
+ );
289
+ }),
290
+ Rx.filter((ds) => isDustProgressCaughtUp(ds)),
291
+ Rx.timeout({
292
+ first: timeoutMs,
293
+ with: () =>
294
+ Rx.throwError(
295
+ () => new Error(`Dust wallet sync catch-up timeout after ${timeoutMs}ms`),
296
+ ),
297
+ }),
298
+ ),
299
+ );
300
+ log.info("Dust wallet sync catch-up complete");
301
+ }
302
+
303
+ /** Stop shielded + unshielded sync after dust is ready (dust-only ergonomics). */
304
+ export async function suspendAuxWalletSyncForFees(wallet: WalletFacade): Promise<void> {
305
+ log.info("Stopping shielded and unshielded wallet sync (dust-only mode)");
306
+ await Promise.all([
307
+ (wallet.shielded as { stop?: () => Promise<void> }).stop?.(),
308
+ (wallet.unshielded as { stop?: () => Promise<void> }).stop?.(),
309
+ ]);
310
+ }
311
+
169
312
  /**
170
313
  * Wait for wallet to be synced and funded
171
314
  */
@@ -250,10 +393,7 @@ export async function syncAndWaitForFunds(
250
393
 
251
394
  let dustBalance = 0n;
252
395
  try {
253
- // Current SDK exposes walletBalance(time); older versions used balance(time).
254
- // Try both so this stays portable across SDK upgrades.
255
- const now = new Date();
256
- dustBalance = state.dust?.walletBalance?.(now) ?? state.dust?.balance?.(now) ?? 0n;
396
+ dustBalance = resolveFacadeDustBalance(state, new Date());
257
397
  } catch (_err) {
258
398
  log.warn("Could not read dust balance from synced state; continuing with dustBalance=0");
259
399
  }
@@ -262,11 +402,25 @@ export async function syncAndWaitForFunds(
262
402
  }
263
403
 
264
404
  /**
265
- * Wait for dust wallet sync and return dust balance if available.
405
+ * Wait for dust wallet sync and return dust balance.
406
+ *
407
+ * Reads **`wallet.dust.state`** directly so dust-only mode (shielded/unshielded
408
+ * stopped) does not wedge on façade `combineLatest`.
409
+ *
410
+ * When `waitNonZero` is true, first waits for dust appliedIndex to reach tip,
411
+ * then polls the dust sub-wallet (heartbeat default 7500ms).
266
412
  */
267
413
  export async function waitForDustFunds(
268
414
  wallet: WalletFacade,
269
- optionsOrTimeout?: number | { timeoutMs?: number; waitNonZero?: boolean }
415
+ optionsOrTimeout?:
416
+ | number
417
+ | {
418
+ timeoutMs?: number;
419
+ waitNonZero?: boolean;
420
+ dustPollIntervalMs?: number;
421
+ /** When true with waitNonZero, skip catch-up wait (e.g. fresh 0/0 chain). */
422
+ skipCatchUp?: boolean;
423
+ },
270
424
  ): Promise<bigint> {
271
425
  log.info("Waiting for dust wallet to sync and receive funds...");
272
426
 
@@ -276,51 +430,62 @@ export async function waitForDustFunds(
276
430
 
277
431
  const syncTimeoutMs = options?.timeoutMs ?? resolveWalletSyncTimeoutMs();
278
432
  const waitNonZero = options?.waitNonZero ?? false;
433
+ const pollMs = resolveDustHeartbeatPollMs(waitNonZero, options?.dustPollIntervalMs);
434
+ const deadline = Date.now() + syncTimeoutMs;
435
+ const remainingMs = () => Math.max(0, deadline - Date.now());
436
+
437
+ if (pollMs > 0) {
438
+ log.info(`Dust wait: dust sub-wallet heartbeat every ${pollMs}ms (disable with MIDNIGHT_WAIT_DUST_POLL_INTERVAL_MS=0)`);
439
+ }
279
440
  const dustSyncStartedAt = Date.now();
280
441
 
281
- const dustWallet = (wallet as any).dust;
282
- if (!dustWallet || !dustWallet.state) {
283
- log.warn("Dust wallet state not available; skipping dust balance wait.");
284
- return 0n;
442
+ if (waitNonZero && !options?.skipCatchUp && remainingMs() > 0) {
443
+ try {
444
+ await waitForDustWalletProgressCatchUp(wallet, remainingMs());
445
+ } catch (e) {
446
+ log.warn(
447
+ `Dust catch-up incomplete (${e instanceof Error ? e.message : String(e)}); continuing balance wait`,
448
+ );
449
+ }
285
450
  }
286
451
 
452
+ const balanceBudgetMs = remainingMs();
453
+ if (balanceBudgetMs <= 0) {
454
+ throw new Error(`Dust wallet sync timeout after ${syncTimeoutMs}ms`);
455
+ }
456
+
457
+ const state$ = dustWalletStateWithHeartbeat(wallet, pollMs);
458
+ const sampled$ =
459
+ pollMs > 0
460
+ ? state$
461
+ : state$.pipe(Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS));
462
+
287
463
  const dustBalance = (await Rx.firstValueFrom(
288
- (dustWallet.state as Rx.Observable<any>).pipe(
289
- Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS),
290
- Rx.tap((state: any) => {
464
+ sampled$.pipe(
465
+ Rx.tap((ds: unknown) => {
291
466
  try {
292
467
  const elapsedSec = ((Date.now() - dustSyncStartedAt) / 1000).toFixed(0);
293
- const complete = state.state?.progress?.isStrictlyComplete();
294
- log.info(`Dust wallet sync progress (${elapsedSec}s): complete=${complete ?? "unknown"}`);
295
- } catch (_err) {
296
- // ignore logging errors
297
- }
298
- }),
299
- Rx.filter((state: any) => {
300
- try {
301
- return state.state?.progress?.isStrictlyComplete() === true;
302
- } catch (_err) {
303
- return false;
304
- }
305
- }),
306
- Rx.map((state: any) => {
307
- try {
308
- if (typeof state.balance === "function") {
309
- return state.balance(new Date()) as bigint;
310
- }
311
- } catch (_err) {
312
- // fall through
468
+ const at = new Date();
469
+ const p = dustProgressFromState(ds);
470
+ const bal = resolveFacadeDustBalance(ds, at);
471
+ log.info(
472
+ `Dust wallet sync progress (${elapsedSec}s): ` +
473
+ `${p?.appliedIndex ?? "?"}/${p?.highestRelevantWalletIndex ?? "?"} ` +
474
+ `balance=${bal}`,
475
+ );
476
+ } catch {
477
+ /* ignore */
313
478
  }
314
- return 0n;
315
479
  }),
480
+ Rx.map((ds: unknown) => resolveFacadeDustBalance(ds)),
481
+ Rx.filter((balance: bigint) => !waitNonZero || balance > 0n),
316
482
  Rx.timeout({
317
- each: syncTimeoutMs,
483
+ first: balanceBudgetMs,
318
484
  with: () =>
319
485
  Rx.throwError(
320
486
  () => new Error(`Dust wallet sync timeout after ${syncTimeoutMs}ms`)
321
487
  ),
322
488
  }),
323
- Rx.filter((balance: bigint) => !waitNonZero || balance > 0n),
324
489
  Rx.tap((balance: bigint) => {
325
490
  if (balance > 0n) log.info(`Dust wallet balance: ${balance}`);
326
491
  })
@@ -344,6 +509,11 @@ export interface DustSyncWithRetryOptions {
344
509
  networkId: NetworkId.NetworkId;
345
510
  /** Per-emission stall timeout in ms. Default: 60_000 (1 minute). */
346
511
  stallTimeoutMs?: number;
512
+ /**
513
+ * How long to wait for a non-zero façade dust balance after dust sync.
514
+ * Default: {@link resolveWalletSyncTimeoutMs} (not `stallTimeoutMs`).
515
+ */
516
+ balanceWaitTimeoutMs?: number;
347
517
  /** Maximum retry attempts on stall. Default: 5. */
348
518
  maxRetries?: number;
349
519
  /** Throttle interval for state emissions in ms. Default: 10_000 (10s). */
@@ -374,6 +544,7 @@ export async function waitForDustFundsWithRetry(
374
544
  seed,
375
545
  networkId,
376
546
  stallTimeoutMs = DUST_STALL_TIMEOUT_MS,
547
+ balanceWaitTimeoutMs = resolveWalletSyncTimeoutMs(),
377
548
  maxRetries = DUST_MAX_RETRIES,
378
549
  throttleMs = CONSTANTS.WALLET_SYNC_THROTTLE_MS,
379
550
  syncMode = 'dust-only',
@@ -440,6 +611,7 @@ export async function waitForDustFundsWithRetry(
440
611
  networkId,
441
612
  syncMode,
442
613
  stateToRestore,
614
+ { stopAuxWalletsImmediately: syncMode !== 'dust-only' },
443
615
  );
444
616
  }
445
617
 
@@ -531,20 +703,44 @@ export async function waitForDustFundsWithRetry(
531
703
  `connected=${syncProgress?.isConnected ?? "?"}`,
532
704
  );
533
705
 
534
- // Sync complete — extract balance and save state
706
+ // Sync complete — read balance from dust sub-wallet (not façade).
535
707
  let dustBalance = 0n;
536
708
  try {
537
- if (typeof dustSyncedState.balance === "function") {
538
- dustBalance = dustSyncedState.balance(new Date()) as bigint;
539
- } else if (typeof dustSyncedState.walletBalance === "function") {
540
- dustBalance = dustSyncedState.walletBalance(new Date()) as bigint;
541
- }
709
+ dustBalance = await waitForDustFunds(walletResult!.wallet, {
710
+ timeoutMs: balanceWaitTimeoutMs,
711
+ waitNonZero: true,
712
+ });
542
713
  } catch {
543
- // ignore balance extraction errors
714
+ // ignore — will return 0n
544
715
  }
545
716
 
546
717
  log.info(`Dust sync complete (attempt ${attempt}). Balance: ${dustBalance}`);
547
718
 
719
+ if (dustBalance === 0n) {
720
+ log.info("No dust after sync; attempting unshielded NIGHT registration...");
721
+ try {
722
+ if (await registerNightForDust(walletResult!)) {
723
+ dustBalance = await waitForDustFunds(walletResult!.wallet, {
724
+ timeoutMs: balanceWaitTimeoutMs,
725
+ waitNonZero: true,
726
+ });
727
+ log.info(`Dust balance after registration: ${dustBalance}`);
728
+ }
729
+ } catch (e) {
730
+ log.warn(
731
+ `NIGHT→dust registration failed: ${e instanceof Error ? e.message : String(e)}`,
732
+ );
733
+ }
734
+ }
735
+
736
+ if (dustBalance === 0n) {
737
+ log.warn("Dust balance still 0 — wallet needs dust or unshielded NIGHT to register.");
738
+ }
739
+
740
+ if (syncMode === 'dust-only') {
741
+ await suspendAuxWalletSyncForFees(walletResult!.wallet);
742
+ }
743
+
548
744
  // Save final state to disk
549
745
  await serializeAndSave(walletResult);
550
746
 
@@ -587,9 +783,21 @@ export async function waitForDustFundsWithRetry(
587
783
  }
588
784
 
589
785
  export function getInitialDustState(
590
- dustWallet: any
591
- ): Promise<any> {
592
- return Rx.firstValueFrom(dustWallet.state);
786
+ dustWallet: { state: Rx.Observable<unknown> },
787
+ options?: { timeoutMs?: number },
788
+ ): Promise<unknown> {
789
+ const obs = dustWallet.state;
790
+ if (options?.timeoutMs && options.timeoutMs > 0) {
791
+ return Rx.firstValueFrom(
792
+ obs.pipe(
793
+ Rx.timeout({
794
+ first: options.timeoutMs,
795
+ with: () => Rx.throwError(() => new Error("Timeout reading dust wallet state")),
796
+ }),
797
+ ),
798
+ );
799
+ }
800
+ return Rx.firstValueFrom(obs);
593
801
  }
594
802
 
595
803
  /**
@@ -664,6 +872,7 @@ export async function buildWalletFacade(
664
872
  networkId: NetworkId.NetworkId,
665
873
  syncMode: WalletSyncMode = 'all',
666
874
  dustSerializedState?: string | null,
875
+ options?: { stopAuxWalletsImmediately?: boolean },
667
876
  ): Promise<WalletResult> {
668
877
  const shieldedSeed = deriveSeedForRole(seed, Roles.Zswap);
669
878
  const dustSeed = deriveSeedForRole(seed, Roles.Dust);
@@ -689,17 +898,18 @@ export async function buildWalletFacade(
689
898
 
690
899
  await wallet.start(zswapSecretKeys, dustSecretKey);
691
900
 
692
- if (syncMode === 'dust-only') {
693
- log.info("Stopping shielded and unshielded wallet sync (dust-only mode)");
694
- await Promise.all([
695
- (wallet.shielded as any).stop?.(),
696
- (wallet.unshielded as any).stop?.(),
697
- ]);
901
+ const stopAuxNow =
902
+ syncMode === 'dust-only' && (options?.stopAuxWalletsImmediately ?? true);
903
+ if (stopAuxNow) {
904
+ await suspendAuxWalletSyncForFees(wallet);
698
905
  }
699
906
 
700
907
  const unshieldedAddress = unshieldedKeystore.getBech32Address().asString();
701
- const dustState = await getInitialDustState(wallet.dust);
702
- const dustAddress = MidnightBech32m.encode(networkId, dustState.address).asString();
908
+ const dustState = await getInitialDustState(wallet.dust as { state: Rx.Observable<unknown> });
909
+ const dustAddress = MidnightBech32m.encode(
910
+ networkId,
911
+ (dustState as { address: unknown }).address,
912
+ ).asString();
703
913
 
704
914
  return {
705
915
  wallet,
@@ -759,9 +969,7 @@ export async function registerNightForDust(
759
969
 
760
970
  const state = await Rx.firstValueFrom(
761
971
  walletResult.wallet.state().pipe(
762
-
763
972
  Rx.filter((s: any) => {
764
- // Only require unshielded+dust sync for dust registration (shielded is not needed)
765
973
  const dustSynced = s.dust?.state?.progress?.isStrictlyComplete() ?? false;
766
974
  const unshieldedSynced = s.unshielded?.progress?.isStrictlyComplete() ?? false;
767
975
  return dustSynced && unshieldedSynced;
@@ -791,37 +999,17 @@ export async function registerNightForDust(
791
999
  (payload: Uint8Array) => walletResult.unshieldedKeystore.signData(payload)
792
1000
  );
793
1001
 
794
- const signedRecipe: UnprovenTransaction = await (walletResult.wallet as any).signUnprovenTransaction(
795
- recipe.transaction,
796
- (payload: Uint8Array) => walletResult.unshieldedKeystore.signData(payload),
797
- );
798
-
799
1002
  log.info("Submitting dust registration transaction...");
800
1003
  const txId = await walletResult.wallet.submitTransaction(
801
- await (walletResult.wallet as any).finalizeTransaction(signedRecipe)
1004
+ await (walletResult.wallet as any).finalizeRecipe(recipe),
802
1005
  );
803
1006
  log.info(`Dust registration submitted with tx id: ${txId}`);
804
1007
 
805
1008
  log.info("Waiting for dust to be generated...");
806
- // Current SDK exposes walletBalance(time); older versions used balance(time).
807
- // Try both so this stays portable across SDK upgrades.
808
- const readDustBalance = (s: any): bigint => {
809
- const now = new Date();
810
- return s.dust?.walletBalance?.(now) ?? s.dust?.balance?.(now) ?? 0n;
811
- };
812
- await Rx.firstValueFrom(
813
- walletResult.wallet.state().pipe(
814
- Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS),
815
- Rx.tap((s: any) => {
816
- log.info(`Current dust balance: ${readDustBalance(s)}`);
817
- }),
818
- Rx.filter((s: any) => readDustBalance(s) > 0n),
819
- Rx.timeout({
820
- each: resolveWalletSyncTimeoutMs(),
821
- with: () => Rx.throwError(() => new Error("Timeout waiting for dust generation"))
822
- })
823
- )
824
- );
1009
+ await waitForDustFunds(walletResult.wallet, {
1010
+ waitNonZero: true,
1011
+ timeoutMs: resolveWalletSyncTimeoutMs(),
1012
+ });
825
1013
 
826
1014
  log.info("Dust registration complete!");
827
1015
  return true;
@@ -973,9 +1161,7 @@ async function main() {
973
1161
 
974
1162
  try {
975
1163
  const state = await Rx.firstValueFrom(walletResult.wallet.state());
976
- // Current SDK exposes walletBalance(time); older versions used balance(time).
977
- const now = new Date();
978
- dustBalance = (state as any).dust?.walletBalance?.(now) ?? (state as any).dust?.balance?.(now) ?? 0n;
1164
+ dustBalance = resolveFacadeDustBalance(state, new Date());
979
1165
 
980
1166
  if (dustBalance === 0n) {
981
1167
  log.info("Dust balance is 0. Attempting to sync dust wallet...");
@@ -0,0 +1,26 @@
1
+ export type MidnightLedgerFn = (state: unknown) => unknown;
2
+
3
+ export type MidnightContractStateDeserializer = {
4
+ deserialize: (bytes: Uint8Array) => { data: { state: unknown } };
5
+ };
6
+
7
+ /**
8
+ * Build a sync-safe ledger parser for raw transaction contract state hex.
9
+ *
10
+ * Pass `ContractState` from the app's `@midnight-ntwrk/compact-runtime` so the
11
+ * deserialized `StateValue` matches the WASM module used by generated
12
+ * `contract.ledger()` helpers (required when @effectstream/sync resolves
13
+ * dependencies from a different node_modules tree than the app template).
14
+ */
15
+ export function midnightLedgerFromTxStateHex(
16
+ ledger: MidnightLedgerFn,
17
+ contractState: MidnightContractStateDeserializer,
18
+ ): (rawHexState: string) => Record<string, unknown> {
19
+ return (rawHexState: string) => {
20
+ const byteState = new Uint8Array(
21
+ rawHexState.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)),
22
+ );
23
+ const stateValue = contractState.deserialize(byteState).data.state;
24
+ return ledger(stateValue) as Record<string, unknown>;
25
+ };
26
+ }
@@ -1,5 +1,4 @@
1
1
  import type { NetworkId } from "@midnight-ntwrk/wallet-sdk-abstractions";
2
- import { mnemonicToSeed } from "@scure/bip39";
3
2
  import { Buffer } from "node:buffer";
4
3
  import { getEnv } from "@effectstream/utils/runtime";
5
4
 
@@ -69,6 +68,7 @@ let walletSeed: string;
69
68
  if (env("MIDNIGHT_WALLET_SEED")) {
70
69
  walletSeed = env("MIDNIGHT_WALLET_SEED");
71
70
  } else if (env("MIDNIGHT_WALLET_MNEMONIC")) {
71
+ const { mnemonicToSeed } = await import("@scure/bip39");
72
72
  walletSeed = Buffer.from(await mnemonicToSeed(env("MIDNIGHT_WALLET_MNEMONIC"))).toString('hex');
73
73
  } else {
74
74
  walletSeed = selectedNetworkConfig.genesisWalletSeed;
package/src/mod.ts CHANGED
@@ -8,6 +8,8 @@ export type {
8
8
  } from "./types.ts";
9
9
  export {
10
10
  resolveWalletSyncTimeoutMs,
11
+ suspendAuxWalletSyncForFees,
12
+ resolveFacadeDustBalance,
11
13
  safeStringifyProgress,
12
14
  syncAndWaitForFunds,
13
15
  buildWalletFacade,
@@ -19,7 +21,10 @@ export {
19
21
  saveDustState,
20
22
  loadDustState,
21
23
  } from "./get-wallet-info.ts";
22
- export type { WalletSyncMode, DustSyncWithRetryOptions } from "./get-wallet-info.ts";
24
+ export type {
25
+ WalletSyncMode,
26
+ DustSyncWithRetryOptions,
27
+ } from "./get-wallet-info.ts";
23
28
  export { CONSTANTS } from "./constants.ts";
24
29
  export {
25
30
  buildWalletAndWaitForFunds,
@@ -27,4 +32,4 @@ export {
27
32
  } from "./build-wallet.ts";
28
33
  export { readMidnightContract } from "./read-contract.ts";
29
34
  export { configureMidnightNodeProviders } from "./providers.ts";
30
- export { midnightNetworkConfig } from "./midnight-env.ts";
35
+ export { midnightNetworkConfig } from "./midnight-env.ts";