@effectstream/midnight-contracts 0.104.0 → 0.200.1

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.
@@ -15,15 +15,19 @@ import {
15
15
  DustSecretKey,
16
16
  LedgerParameters,
17
17
  ZswapSecretKeys,
18
- shieldedToken,
19
- } from "@midnight-ntwrk/ledger-v8";
18
+ nativeToken,
19
+ } from "@midnightntwrk/ledger-v9";
20
20
 
21
21
  const log = console;
22
22
  import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
23
23
  import { Buffer } from "node:buffer";
24
24
  import * as Rx from "rxjs";
25
25
  import { HDWallet, Roles } from "@midnightntwrk/wallet-sdk-hd";
26
- import { type DefaultConfiguration, WalletFacade } from "@midnightntwrk/wallet-sdk-facade";
26
+ import {
27
+ type DefaultConfiguration,
28
+ type UnprovenTransactionRecipe,
29
+ WalletFacade,
30
+ } from "@midnightntwrk/wallet-sdk-facade";
27
31
  import { DustWallet } from "@midnightntwrk/wallet-sdk-dust-wallet";
28
32
  import {
29
33
  UnshieldedWallet,
@@ -37,7 +41,7 @@ import {
37
41
  NetworkId,
38
42
  TransactionHistoryStorage,
39
43
  } from "@midnightntwrk/wallet-sdk-abstractions";
40
- import { MidnightBech32m } from "@midnightntwrk/wallet-sdk-address-format";
44
+ import { DustAddress } from "@midnightntwrk/wallet-sdk-address-format";
41
45
  // No node:fs / node:path here — this module is reachable from BROWSER bundles
42
46
  // (MidnightLocal.connectFromSeed imports `./wallet-info` to build the wallet
43
47
  // facade), and browser bundlers shim those to empty/null modules. All disk I/O
@@ -79,7 +83,9 @@ function deriveSeedForRole(seed: string, role: DerivationRole): Uint8Array {
79
83
  throw new Error(`Key derivation out of bounds for role: ${role}`);
80
84
  }
81
85
 
82
- return Buffer.from(derivationResult.key);
86
+ const derivedKey = Buffer.from(derivationResult.key);
87
+ hdWalletResult.hdWallet.clear();
88
+ return derivedKey;
83
89
  }
84
90
 
85
91
  // ============================================================================
@@ -145,6 +151,67 @@ export function resolveFacadeDustBalance(
145
151
  return w > b ? w : b;
146
152
  }
147
153
 
154
+ /** Count spendable DUST UTXOs from a facade state or dust-wallet state. */
155
+ export function resolveFacadeDustAvailableCoins(
156
+ facadeOrDustState: unknown,
157
+ ): number {
158
+ const dust =
159
+ (facadeOrDustState as { dust?: unknown } | null | undefined)?.dust ??
160
+ facadeOrDustState;
161
+ if (dust === undefined || dust === null || typeof dust !== "object") return 0;
162
+ const availableCoins = (dust as { availableCoins?: unknown }).availableCoins;
163
+ return Array.isArray(availableCoins) ? availableCoins.length : 0;
164
+ }
165
+
166
+ export interface DustFundsReadiness {
167
+ /** Aggregate wallet balance, retained for diagnostics only. */
168
+ balance: bigint;
169
+ /** Coins the Ledger-v9 DUST wallet currently exposes as available. */
170
+ availableCoins: number;
171
+ /** Available coins that meet the caller's optional per-coin value floor. */
172
+ spendableCoins: number;
173
+ /** The spendability signal adapters must use instead of aggregate balance. */
174
+ ready: boolean;
175
+ }
176
+
177
+ /**
178
+ * Resolve Ledger-v9 DUST spendability without inferring it from aggregate
179
+ * balance. Regular wallet flows count every available coin; fee-balancing
180
+ * callers can provide a value floor so newly-created, value-insufficient coins
181
+ * do not advertise readiness prematurely.
182
+ */
183
+ export function resolveFacadeDustFundsReadiness(
184
+ facadeOrDustState: unknown,
185
+ minSpendableDustPerCoin?: bigint,
186
+ ): DustFundsReadiness {
187
+ const dust =
188
+ (facadeOrDustState as { dust?: unknown } | null | undefined)?.dust ??
189
+ facadeOrDustState;
190
+ const coins = dust && typeof dust === "object"
191
+ ? (dust as { availableCoins?: unknown }).availableCoins
192
+ : undefined;
193
+ const available = Array.isArray(coins) ? coins : [];
194
+ const spendableCoins = minSpendableDustPerCoin === undefined
195
+ ? available.length
196
+ : available.filter((coin) => {
197
+ try {
198
+ const generatedNow = (coin as { generatedNow?: bigint | string | number } | null)
199
+ ?.generatedNow;
200
+ return generatedNow !== undefined &&
201
+ BigInt(generatedNow) >= minSpendableDustPerCoin;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }).length;
206
+
207
+ return {
208
+ balance: resolveFacadeDustBalance(facadeOrDustState),
209
+ availableCoins: available.length,
210
+ spendableCoins,
211
+ ready: spendableCoins > 0,
212
+ };
213
+ }
214
+
148
215
  function resolveDustHeartbeatPollMs(waitNonZero: boolean, override?: number): number {
149
216
  if (override !== undefined) return Math.max(0, Math.floor(override));
150
217
  if (!waitNonZero) return 0;
@@ -276,8 +343,8 @@ export async function syncAndWaitForFunds(
276
343
  let latestState: any = null;
277
344
  const periodicLogger = setInterval(() => {
278
345
  if (!latestState) return;
279
- const shieldedSynced = latestState.shielded.state.progress.isStrictlyComplete();
280
- const dustSynced = latestState.dust.state.progress.isStrictlyComplete();
346
+ const shieldedSynced = latestState.shielded.progress.isStrictlyComplete();
347
+ const dustSynced = latestState.dust.progress.isStrictlyComplete();
281
348
  const unshieldedSynced = latestState.unshielded?.progress?.isStrictlyComplete() ?? false;
282
349
  log.info(
283
350
  `[wait] shielded=${shieldedSynced}, unshielded=${unshieldedSynced}, dust=${dustSynced}`
@@ -285,45 +352,51 @@ export async function syncAndWaitForFunds(
285
352
  }, CONSTANTS.WALLET_SYNC_THROTTLE_MS);
286
353
  const syncStartedAt = Date.now();
287
354
 
288
- const state = await Rx.firstValueFrom(
289
- wallet.state().pipe(
290
- Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS),
291
- Rx.tap((state: any) => {
292
- const elapsedSec = ((Date.now() - syncStartedAt) / 1000).toFixed(0);
293
- const shieldedSynced = state.shielded.state.progress.isStrictlyComplete();
294
- const dustSynced = state.dust.state.progress.isStrictlyComplete();
295
- const unshieldedSynced = state.unshielded?.progress?.isStrictlyComplete() ?? false;
296
- log.info(
297
- `Wallet sync progress (${elapsedSec}s): shielded=${shieldedSynced}, unshielded=${unshieldedSynced}, dust=${dustSynced}`
298
- );
299
- }),
300
- Rx.filter((state: any) => {
301
- const shieldedSynced = skipShielded || state.shielded.state.progress.isStrictlyComplete();
302
- const dustSynced = state.dust.state.progress.isStrictlyComplete();
303
- const unshieldedSynced = state.unshielded?.progress?.isStrictlyComplete() ?? false;
355
+ let state: any;
356
+ try {
357
+ state = await Rx.firstValueFrom(
358
+ wallet.state().pipe(
359
+ Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS),
360
+ Rx.tap((nextState: any) => {
361
+ latestState = nextState;
362
+ const elapsedSec = ((Date.now() - syncStartedAt) / 1000).toFixed(0);
363
+ const shieldedSynced = nextState.shielded.progress.isStrictlyComplete();
364
+ const dustSynced = nextState.dust.progress.isStrictlyComplete();
365
+ const unshieldedSynced = nextState.unshielded?.progress?.isStrictlyComplete() ?? false;
366
+ log.info(
367
+ `Wallet sync progress (${elapsedSec}s): shielded=${shieldedSynced}, unshielded=${unshieldedSynced}, dust=${dustSynced}`
368
+ );
369
+ }),
370
+ Rx.filter((nextState: any) => {
371
+ const shieldedSynced = skipShielded || nextState.shielded.progress.isStrictlyComplete();
372
+ const dustSynced = nextState.dust.progress.isStrictlyComplete();
373
+ const unshieldedSynced = nextState.unshielded?.progress?.isStrictlyComplete() ?? false;
304
374
 
305
- if (!shieldedSynced || !dustSynced || !unshieldedSynced) return false;
375
+ if (!shieldedSynced || !dustSynced || !unshieldedSynced) return false;
306
376
 
307
- if (waitNonZero && !skipShielded) {
308
- const tokenId = shieldedToken().tag;
309
- const shieldedBalance = state.shielded.balances[tokenId] ?? 0n;
310
- return shieldedBalance > 0n;
311
- }
377
+ if (waitNonZero && !skipShielded) {
378
+ const tokenId = nativeToken().raw;
379
+ const shieldedBalance = nextState.shielded.balances[tokenId] ?? 0n;
380
+ return shieldedBalance > 0n;
381
+ }
312
382
 
313
- return true;
314
- }),
315
- Rx.tap(() => log.info("Wallet sync complete")),
316
- Rx.timeout({
317
- each: syncTimeoutMs,
318
- with: () =>
319
- Rx.throwError(
320
- () => new Error(`Wallet sync timeout after ${syncTimeoutMs}ms`)
321
- ),
322
- })
323
- )
324
- );
383
+ return true;
384
+ }),
385
+ Rx.tap(() => log.info("Wallet sync complete")),
386
+ Rx.timeout({
387
+ each: syncTimeoutMs,
388
+ with: () =>
389
+ Rx.throwError(
390
+ () => new Error(`Wallet sync timeout after ${syncTimeoutMs}ms`)
391
+ ),
392
+ })
393
+ )
394
+ );
395
+ } finally {
396
+ clearInterval(periodicLogger);
397
+ }
325
398
 
326
- const tokenId = shieldedToken().tag;
399
+ const tokenId = nativeToken().raw;
327
400
 
328
401
  const shieldedBalancesObj = state.shielded.balances || {};
329
402
  const availableKeys = Object.keys(shieldedBalancesObj);
@@ -356,7 +429,9 @@ export async function syncAndWaitForFunds(
356
429
  * stopped) does not wedge on façade `combineLatest`.
357
430
  *
358
431
  * When `waitNonZero` is true, first waits for dust appliedIndex to reach tip,
359
- * then polls the dust sub-wallet (heartbeat default 7500ms).
432
+ * then polls the dust sub-wallet until at least one spendable DUST UTXO exists.
433
+ * A positive accrued balance is not sufficient because it can precede coin
434
+ * availability after NIGHT registration.
360
435
  */
361
436
  export async function waitForDustFunds(
362
437
  wallet: WalletFacade,
@@ -368,8 +443,10 @@ export async function waitForDustFunds(
368
443
  dustPollIntervalMs?: number;
369
444
  /** When true with waitNonZero, skip catch-up wait (e.g. fresh 0/0 chain). */
370
445
  skipCatchUp?: boolean;
446
+ /** Optional generated-value floor for a coin to count as spendable. */
447
+ minSpendableDustPerCoin?: bigint;
371
448
  },
372
- ): Promise<bigint> {
449
+ ): Promise<DustFundsReadiness> {
373
450
  log.info("Waiting for dust wallet to sync and receive funds...");
374
451
 
375
452
  const options = typeof optionsOrTimeout === 'number'
@@ -378,6 +455,7 @@ export async function waitForDustFunds(
378
455
 
379
456
  const syncTimeoutMs = options?.timeoutMs ?? resolveWalletSyncTimeoutMs();
380
457
  const waitNonZero = options?.waitNonZero ?? false;
458
+ const minSpendableDustPerCoin = options?.minSpendableDustPerCoin;
381
459
  const pollMs = resolveDustHeartbeatPollMs(waitNonZero, options?.dustPollIntervalMs);
382
460
  const deadline = Date.now() + syncTimeoutMs;
383
461
  const remainingMs = () => Math.max(0, deadline - Date.now());
@@ -408,7 +486,7 @@ export async function waitForDustFunds(
408
486
  ? state$
409
487
  : state$.pipe(Rx.throttleTime(CONSTANTS.WALLET_SYNC_THROTTLE_MS));
410
488
 
411
- const dustBalance = (await Rx.firstValueFrom(
489
+ const dustFunds = await Rx.firstValueFrom(
412
490
  sampled$.pipe(
413
491
  Rx.tap((ds: unknown) => {
414
492
  try {
@@ -416,17 +494,20 @@ export async function waitForDustFunds(
416
494
  const at = new Date();
417
495
  const p = dustProgressFromState(ds);
418
496
  const bal = resolveFacadeDustBalance(ds, at);
497
+ const availableCoins = resolveFacadeDustAvailableCoins(ds);
419
498
  log.info(
420
499
  `Dust wallet sync progress (${elapsedSec}s): ` +
421
500
  `${p?.appliedIndex ?? "?"}/${p?.highestRelevantWalletIndex ?? "?"} ` +
422
- `balance=${bal}`,
501
+ `balance=${bal} availableCoins=${availableCoins}`,
423
502
  );
424
503
  } catch {
425
504
  /* ignore */
426
505
  }
427
506
  }),
428
- Rx.map((ds: unknown) => resolveFacadeDustBalance(ds)),
429
- Rx.filter((balance: bigint) => !waitNonZero || balance > 0n),
507
+ Rx.map((ds: unknown) =>
508
+ resolveFacadeDustFundsReadiness(ds, minSpendableDustPerCoin)
509
+ ),
510
+ Rx.filter((funds) => !waitNonZero || funds.ready),
430
511
  Rx.timeout({
431
512
  first: balanceBudgetMs,
432
513
  with: () =>
@@ -434,13 +515,16 @@ export async function waitForDustFunds(
434
515
  () => new Error(`Dust wallet sync timeout after ${syncTimeoutMs}ms`)
435
516
  ),
436
517
  }),
437
- Rx.tap((balance: bigint) => {
438
- if (balance > 0n) log.info(`Dust wallet balance: ${balance}`);
518
+ Rx.tap((funds) => {
519
+ log.info(
520
+ `Dust wallet ready=${funds.ready} balance=${funds.balance} ` +
521
+ `availableCoins=${funds.availableCoins} spendableCoins=${funds.spendableCoins}`,
522
+ );
439
523
  })
440
524
  )
441
- )) as bigint;
525
+ );
442
526
 
443
- return dustBalance;
527
+ return dustFunds;
444
528
  }
445
529
 
446
530
  // ============================================================================
@@ -469,6 +553,16 @@ export interface DustSyncWithRetryOptions {
469
553
  syncMode?: WalletSyncMode;
470
554
  /** Base directory for dust state files (relative to CWD). Default: "dust-state". */
471
555
  dustStateDir?: string;
556
+ /** Optional generated-value floor for a coin to count as spendable. */
557
+ minSpendableDustPerCoin?: bigint;
558
+ }
559
+
560
+ export interface DustSyncWithRetryResult {
561
+ walletResult: WalletResult;
562
+ dustBalance: bigint;
563
+ availableDustCoins: number;
564
+ spendableDustCoins: number;
565
+ dustReady: boolean;
472
566
  }
473
567
 
474
568
  /**
@@ -486,7 +580,7 @@ export interface DustSyncWithRetryOptions {
486
580
  export async function waitForDustFundsWithRetry(
487
581
  options: DustSyncWithRetryOptions,
488
582
  existingWalletResult?: WalletResult,
489
- ): Promise<{ walletResult: WalletResult; dustBalance: bigint }> {
583
+ ): Promise<DustSyncWithRetryResult> {
490
584
  const {
491
585
  networkUrls,
492
586
  seed,
@@ -497,6 +591,7 @@ export async function waitForDustFundsWithRetry(
497
591
  throttleMs = CONSTANTS.WALLET_SYNC_THROTTLE_MS,
498
592
  syncMode = 'dust-only',
499
593
  dustStateDir = DEFAULT_DUST_STATE_DIR,
594
+ minSpendableDustPerCoin,
500
595
  } = options;
501
596
 
502
597
  const networkIdStr = String(networkId);
@@ -572,7 +667,13 @@ export async function waitForDustFundsWithRetry(
572
667
  const dustWallet = (walletResult.wallet as any).dust;
573
668
  if (!dustWallet || !dustWallet.state) {
574
669
  log.warn("Dust wallet state not available; skipping dust sync.");
575
- return { walletResult, dustBalance: 0n };
670
+ return {
671
+ walletResult,
672
+ dustBalance: 0n,
673
+ availableDustCoins: 0,
674
+ spendableDustCoins: 0,
675
+ dustReady: false,
676
+ };
576
677
  }
577
678
 
578
679
  // First, try the SDK's built-in waitForSyncedState with DUST_COMPLETION_GAP.
@@ -657,27 +758,42 @@ export async function waitForDustFundsWithRetry(
657
758
  );
658
759
 
659
760
  // Sync complete — read balance from dust sub-wallet (not façade).
660
- let dustBalance = 0n;
761
+ let dustFunds: DustFundsReadiness = {
762
+ balance: 0n,
763
+ availableCoins: 0,
764
+ spendableCoins: 0,
765
+ ready: false,
766
+ };
661
767
  try {
662
- dustBalance = await waitForDustFunds(walletResult!.wallet, {
768
+ dustFunds = await waitForDustFunds(walletResult!.wallet, {
663
769
  timeoutMs: balanceWaitTimeoutMs,
664
770
  waitNonZero: true,
771
+ minSpendableDustPerCoin,
665
772
  });
666
773
  } catch {
667
- // ignore — will return 0n
774
+ // ignore — explicit readiness remains false
668
775
  }
669
776
 
670
- log.info(`Dust sync complete (attempt ${attempt}). Balance: ${dustBalance}`);
777
+ log.info(
778
+ `Dust sync complete (attempt ${attempt}). ` +
779
+ `ready=${dustFunds.ready} balance=${dustFunds.balance} ` +
780
+ `availableCoins=${dustFunds.availableCoins} spendableCoins=${dustFunds.spendableCoins}`,
781
+ );
671
782
 
672
- if (dustBalance === 0n) {
783
+ if (!dustFunds.ready) {
673
784
  log.info("No dust after sync; attempting unshielded NIGHT registration...");
674
785
  try {
675
786
  if (await registerNightForDust(walletResult!)) {
676
- dustBalance = await waitForDustFunds(walletResult!.wallet, {
787
+ dustFunds = await waitForDustFunds(walletResult!.wallet, {
677
788
  timeoutMs: balanceWaitTimeoutMs,
678
789
  waitNonZero: true,
790
+ minSpendableDustPerCoin,
679
791
  });
680
- log.info(`Dust balance after registration: ${dustBalance}`);
792
+ log.info(
793
+ `Dust after registration: ready=${dustFunds.ready} ` +
794
+ `balance=${dustFunds.balance} availableCoins=${dustFunds.availableCoins} ` +
795
+ `spendableCoins=${dustFunds.spendableCoins}`,
796
+ );
681
797
  }
682
798
  } catch (e) {
683
799
  log.warn(
@@ -686,8 +802,8 @@ export async function waitForDustFundsWithRetry(
686
802
  }
687
803
  }
688
804
 
689
- if (dustBalance === 0n) {
690
- log.warn("Dust balance still 0 — wallet needs dust or unshielded NIGHT to register.");
805
+ if (!dustFunds.ready) {
806
+ log.warn("No spendable DUST coin — wallet needs DUST or unshielded NIGHT to register.");
691
807
  }
692
808
 
693
809
  if (syncMode === 'dust-only') {
@@ -697,7 +813,13 @@ export async function waitForDustFundsWithRetry(
697
813
  // Save final state to disk
698
814
  await serializeAndSave(walletResult);
699
815
 
700
- return { walletResult, dustBalance };
816
+ return {
817
+ walletResult,
818
+ dustBalance: dustFunds.balance,
819
+ availableDustCoins: dustFunds.availableCoins,
820
+ spendableDustCoins: dustFunds.spendableCoins,
821
+ dustReady: dustFunds.ready,
822
+ };
701
823
  } catch (e) {
702
824
  const isStall = e instanceof Error && e.message === "stall";
703
825
 
@@ -769,7 +891,7 @@ function createWalletConfiguration(
769
891
  relayURL: new URL(networkUrls.node.replace("http", "ws")),
770
892
  networkId: networkId,
771
893
  txHistoryStorage: new InMemoryTransactionHistoryStorage(
772
- TransactionHistoryStorage.TransactionHistoryCommonSchema,
894
+ TransactionHistoryStorage.TransactionHistoryEntryCommonSchema,
773
895
  ),
774
896
  costParameters: {
775
897
  additionalFeeOverhead: CONSTANTS.DUST_FEE_OVERHEAD,
@@ -845,7 +967,7 @@ function buildUnshieldedWallet(
845
967
  return UnshieldedWallet({
846
968
  ...config,
847
969
  txHistoryStorage: new InMemoryTransactionHistoryStorage(
848
- TransactionHistoryStorage.TransactionHistoryCommonSchema,
970
+ TransactionHistoryStorage.TransactionHistoryEntryCommonSchema,
849
971
  ),
850
972
  } as any).startWithPublicKey(PublicKey.fromKeyStore(keystore));
851
973
  }
@@ -866,26 +988,29 @@ export async function buildWalletFacade(
866
988
  dustSerializedState?: string | null,
867
989
  options?: { stopAuxWalletsImmediately?: boolean },
868
990
  ): Promise<WalletResult> {
991
+ setNetworkId(networkId);
869
992
  const shieldedSeed = deriveSeedForRole(seed, Roles.Zswap);
870
993
  const dustSeed = deriveSeedForRole(seed, Roles.Dust);
871
994
  const unshieldedSeed = deriveSeedForRole(seed, Roles.NightExternal);
872
995
 
873
996
  const config = createWalletConfiguration(networkUrls, networkId);
874
997
 
875
- const unshieldedKeystore = createKeystore(unshieldedSeed, networkId);
876
-
877
- const shieldedWallet = buildShieldedWallet(config, shieldedSeed);
878
- const dustWallet = buildDustWallet(config, dustSeed, dustSerializedState);
879
- const unshieldedWallet = buildUnshieldedWallet(config, unshieldedKeystore);
998
+ const unshieldedKeystore = createKeystore(
999
+ { kind: "schnorr", secret: unshieldedSeed },
1000
+ networkId,
1001
+ );
880
1002
 
881
1003
  const zswapSecretKeys = ZswapSecretKeys.fromSeed(shieldedSeed);
882
1004
  const dustSecretKey = DustSecretKey.fromSeed(dustSeed);
883
1005
 
884
1006
  const wallet: WalletFacade = await WalletFacade.init({
885
- configuration: config as any,
886
- shielded: () => shieldedWallet,
887
- unshielded: () => unshieldedWallet,
888
- dust: () => dustWallet,
1007
+ configuration: config,
1008
+ shielded: (walletConfig) =>
1009
+ buildShieldedWallet(walletConfig, shieldedSeed),
1010
+ unshielded: (walletConfig) =>
1011
+ buildUnshieldedWallet(walletConfig, unshieldedKeystore),
1012
+ dust: (walletConfig) =>
1013
+ buildDustWallet(walletConfig, dustSeed, dustSerializedState),
889
1014
  });
890
1015
 
891
1016
  await wallet.start(zswapSecretKeys, dustSecretKey);
@@ -897,11 +1022,10 @@ export async function buildWalletFacade(
897
1022
  }
898
1023
 
899
1024
  const unshieldedAddress = unshieldedKeystore.getBech32Address().asString();
900
- const dustState = await getInitialDustState(wallet.dust as { state: Rx.Observable<unknown> });
901
- const dustAddress = MidnightBech32m.encode(
1025
+ const dustAddress = DustAddress.encodePublicKey(
902
1026
  networkId,
903
- (dustState as { address: unknown }).address,
904
- ).asString();
1027
+ dustSecretKey.publicKey,
1028
+ );
905
1029
 
906
1030
  return {
907
1031
  wallet,
@@ -962,7 +1086,7 @@ export async function registerNightForDust(
962
1086
  const state = await Rx.firstValueFrom(
963
1087
  walletResult.wallet.state().pipe(
964
1088
  Rx.filter((s: any) => {
965
- const dustSynced = s.dust?.state?.progress?.isStrictlyComplete() ?? false;
1089
+ const dustSynced = s.dust?.progress?.isStrictlyComplete() ?? false;
966
1090
  const unshieldedSynced = s.unshielded?.progress?.isStrictlyComplete() ?? false;
967
1091
  return dustSynced && unshieldedSynced;
968
1092
  }),
@@ -978,22 +1102,45 @@ export async function registerNightForDust(
978
1102
 
979
1103
  if (unregisteredNightUtxos.length === 0) {
980
1104
  log.info("No unregistered unshielded Night UTXOs available.");
981
- const dustBalance = await waitForDustFunds(walletResult.wallet, { timeoutMs: 5000 });
982
- return dustBalance > 0n;
1105
+ try {
1106
+ await waitForDustFunds(walletResult.wallet, {
1107
+ timeoutMs: 5000,
1108
+ waitNonZero: true,
1109
+ });
1110
+ return true;
1111
+ } catch {
1112
+ return false;
1113
+ }
983
1114
  }
984
1115
 
985
1116
  log.info(`Found ${unregisteredNightUtxos.length} unregistered Night UTXOs. Registering for dust...`);
986
1117
 
987
1118
  try {
988
- const recipe = await (walletResult.wallet as any).registerNightUtxosForDustGeneration(
1119
+ const { fee } = await walletResult.wallet.estimateRegistration(
1120
+ unregisteredNightUtxos,
1121
+ );
1122
+ log.info(`Waiting for the NIGHT UTXOs to generate the registration fee: ${fee}`);
1123
+ await walletResult.wallet.waitForGeneratedDust(
1124
+ unregisteredNightUtxos,
1125
+ fee,
1126
+ { timeoutMs: resolveWalletSyncTimeoutMs() },
1127
+ );
1128
+
1129
+ const recipe: UnprovenTransactionRecipe =
1130
+ await walletResult.wallet.registerNightUtxosForDustGeneration(
989
1131
  unregisteredNightUtxos,
990
1132
  walletResult.unshieldedKeystore.getPublicKey(),
991
- (payload: Uint8Array) => walletResult.unshieldedKeystore.signData(payload)
1133
+ (payload: Uint8Array) => walletResult.unshieldedKeystore.signDataAsync(payload),
992
1134
  );
1135
+ if (recipe.type !== "UNPROVEN_TRANSACTION") {
1136
+ throw new Error(
1137
+ `Unexpected DUST registration recipe type: ${(recipe as { type?: unknown }).type}`,
1138
+ );
1139
+ }
993
1140
 
994
1141
  log.info("Submitting dust registration transaction...");
995
1142
  const txId = await walletResult.wallet.submitTransaction(
996
- await (walletResult.wallet as any).finalizeRecipe(recipe),
1143
+ await walletResult.wallet.finalizeRecipe(recipe),
997
1144
  );
998
1145
  log.info(`Dust registration submitted with tx id: ${txId}`);
999
1146
 
@@ -18,7 +18,7 @@ const env = (key: string | string[], fallback?: string): string => {
18
18
  throw new Error('Invalid key type');
19
19
  }
20
20
 
21
- type NetworkConfig = {
21
+ export type NetworkConfig = {
22
22
  indexer: string;
23
23
  indexerWS: string;
24
24
  node: string;
@@ -29,8 +29,8 @@ type NetworkConfig = {
29
29
 
30
30
  // Midnight Network default configurations
31
31
  const undeployedNetworkConfig: NetworkConfig = {
32
- indexer: "http://127.0.0.1:8088/api/v3/graphql",
33
- indexerWS: "ws://127.0.0.1:8088/api/v3/graphql/ws",
32
+ indexer: "http://127.0.0.1:8088/api/v4/graphql",
33
+ indexerWS: "ws://127.0.0.1:8088/api/v4/graphql/ws",
34
34
  node: "http://127.0.0.1:9944",
35
35
  networkId: "undeployed" as NetworkId.NetworkId,
36
36
  proofServer: "http://127.0.0.1:6300",
@@ -47,22 +47,20 @@ const deployedNetworkConfig = (networkId: NetworkId.NetworkId): NetworkConfig =>
47
47
  genesisWalletSeed: '',
48
48
  });
49
49
 
50
- const networkId = env("MIDNIGHT_NETWORK_ID") || "undeployed";
51
- let selectedNetworkConfig: NetworkConfig;
52
- switch (networkId) {
53
- case "undeployed":
54
- selectedNetworkConfig = undeployedNetworkConfig;
55
- break;
56
- case "mainnet":
57
- case "testnet":
58
- case "devnet":
59
- case "qanet":
60
- case "preview":
61
- case "preprod":
62
- default:
63
- selectedNetworkConfig = deployedNetworkConfig(networkId as NetworkId.NetworkId);
64
- break;
65
- }
50
+ /**
51
+ * Resolve defaults for every wallet-SDK network ID. Only `undeployed` uses
52
+ * loopback services; every deployed or future network ID follows the hosted
53
+ * Midnight endpoint convention instead of being restricted to `stagenet`.
54
+ */
55
+ export const defaultMidnightNetworkConfig = (
56
+ networkId: NetworkId.NetworkId,
57
+ ): NetworkConfig =>
58
+ networkId === "undeployed"
59
+ ? undeployedNetworkConfig
60
+ : deployedNetworkConfig(networkId);
61
+
62
+ const networkId = (env("MIDNIGHT_NETWORK_ID") || "undeployed") as NetworkId.NetworkId;
63
+ const selectedNetworkConfig = defaultMidnightNetworkConfig(networkId);
66
64
 
67
65
  let walletSeed: string;
68
66
  if (env("MIDNIGHT_WALLET_SEED")) {
package/src/mod.ts CHANGED
@@ -10,6 +10,8 @@ export {
10
10
  resolveWalletSyncTimeoutMs,
11
11
  suspendAuxWalletSyncForFees,
12
12
  resolveFacadeDustBalance,
13
+ resolveFacadeDustAvailableCoins,
14
+ resolveFacadeDustFundsReadiness,
13
15
  safeStringifyProgress,
14
16
  syncAndWaitForFunds,
15
17
  buildWalletFacade,
@@ -26,7 +28,9 @@ export {
26
28
  export { saveDustState, loadDustState } from "./dust-state.ts";
27
29
  export type {
28
30
  WalletSyncMode,
31
+ DustFundsReadiness,
29
32
  DustSyncWithRetryOptions,
33
+ DustSyncWithRetryResult,
30
34
  } from "./get-wallet-info.ts";
31
35
  export { CONSTANTS } from "./constants.ts";
32
36
  export {