@arkade-os/sdk 0.4.5 → 0.4.7

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/README.md CHANGED
@@ -310,21 +310,62 @@ const settleTxid = await wallet.settle({
310
310
 
311
311
  ### VTXO Management (Renewal & Recovery)
312
312
 
313
- VTXOs have an expiration time (batch expiry). The SDK provides the `VtxoManager` class to handle both:
313
+ VTXOs have an expiration time (batch expiry). The SDK provides the `VtxoManager` class to handle:
314
314
 
315
315
  - **Renewal**: Renew VTXOs before they expire to maintain unilateral control of the funds.
316
316
  - **Recovery**: Reclaim swept or expired VTXOs back to the wallet in case renewal window was missed.
317
+ - **Boarding UTXO Sweep**: Sweep expired boarding UTXOs back to a fresh boarding address to restart the timelock.
318
+
319
+ #### Settlement Configuration
320
+
321
+ The recommended way to configure `VtxoManager` is via `settlementConfig` on the wallet.
322
+ If you omit `settlementConfig`, settlement is enabled with the default behavior:
323
+ VTXO renewal at 3 days and boarding UTXO sweep enabled.
317
324
 
318
325
  ```typescript
319
- import { VtxoManager } from '@arkade-os/sdk'
326
+ const wallet = await Wallet.create({
327
+ identity,
328
+ arkServerUrl: 'https://mutinynet.arkade.sh',
329
+ // Enable settlement with defaults explicitly
330
+ settlementConfig: {},
331
+ })
332
+ ```
320
333
 
321
- // Create manager with optional renewal configuration
322
- const manager = new VtxoManager(wallet, {
323
- enabled: true, // Enable expiration monitoring
324
- thresholdMs: 24 * 60 * 60 * 1000 // Alert when 24h hours % of lifetime remains (default)
334
+ ```typescript
335
+ // Enable both VTXO renewal and boarding UTXO sweep
336
+ const wallet = await Wallet.create({
337
+ identity,
338
+ arkServerUrl: 'https://mutinynet.arkade.sh',
339
+ settlementConfig: {
340
+ vtxoThreshold: 86400, // renew when 24 hours remain (in seconds)
341
+ boardingUtxoSweep: true, // sweep expired boarding UTXOs
342
+ },
325
343
  })
326
344
  ```
327
345
 
346
+ ```typescript
347
+ // Explicitly disable all settlement
348
+ const wallet = await Wallet.create({
349
+ identity,
350
+ arkServerUrl: 'https://mutinynet.arkade.sh',
351
+ settlementConfig: false,
352
+ })
353
+ ```
354
+
355
+ Create the `VtxoManager` by passing the wallet and its settlement config:
356
+
357
+ ```typescript
358
+ import { VtxoManager } from '@arkade-os/sdk'
359
+
360
+ const manager = new VtxoManager(
361
+ wallet,
362
+ undefined, // deprecated renewalConfig
363
+ wallet.settlementConfig // new settlementConfig
364
+ )
365
+ ```
366
+
367
+ > **Migration from `renewalConfig`:** The old `renewalConfig` with `enabled` and `thresholdMs` (milliseconds) is still supported but deprecated. If both are provided, `settlementConfig` takes precedence. The new `vtxoThreshold` uses **seconds** instead of milliseconds.
368
+
328
369
  #### Renewal: Prevent Expiration
329
370
 
330
371
  Renew VTXOs before they expire to retain unilateral control of funds.
@@ -341,6 +382,26 @@ const expiringVtxos = await manager.getExpiringVtxos()
341
382
  const urgentlyExpiring = await manager.getExpiringVtxos(5_000)
342
383
  ```
343
384
 
385
+ #### Boarding UTXO Sweep
386
+
387
+ When a boarding UTXO's CSV timelock expires, it can no longer be onboarded into Ark cooperatively. The sweep feature detects these expired UTXOs and builds a raw on-chain transaction that spends them via the unilateral exit path back to a fresh boarding address, restarting the timelock.
388
+
389
+ - Multiple expired UTXOs are batched into a single transaction (many inputs, one output)
390
+ - A dust check ensures the sweep is skipped if fees would consume the entire value
391
+
392
+ ```typescript
393
+ // Check for expired boarding UTXOs
394
+ const expired = await manager.getExpiredBoardingUtxos()
395
+ console.log(`${expired.length} expired boarding UTXOs`)
396
+
397
+ // Sweep them back to a fresh boarding address (requires boardingUtxoSweep: true)
398
+ try {
399
+ const txid = await manager.sweepExpiredBoardingUtxos()
400
+ console.log('Swept expired boarding UTXOs:', txid)
401
+ } catch (e) {
402
+ // "No expired boarding UTXOs to sweep" or "Sweep not economical"
403
+ }
404
+ ```
344
405
 
345
406
  #### Recovery: Reclaim Swept VTXOs
346
407
 
@@ -121,13 +121,17 @@ const nLocktimeMinSeconds = 500000000n;
121
121
  function isSeconds(locktime) {
122
122
  return locktime >= nLocktimeMinSeconds;
123
123
  }
124
- function hasBoardingTxExpired(coin, boardingTimelock) {
124
+ function hasBoardingTxExpired(coin, boardingTimelock, chainTipHeight) {
125
125
  if (!coin.status.block_time)
126
126
  return false;
127
127
  if (boardingTimelock.value === 0n)
128
128
  return true;
129
- if (boardingTimelock.type === "blocks")
130
- return false; // TODO: handle get chain tip
129
+ if (boardingTimelock.type === "blocks") {
130
+ if (chainTipHeight === undefined || !coin.status.block_height)
131
+ return false;
132
+ return (BigInt(chainTipHeight - coin.status.block_height) >=
133
+ boardingTimelock.value);
134
+ }
131
135
  // validate expiry in terms of seconds
132
136
  const now = BigInt(Math.floor(Date.now() / 1000));
133
137
  const blockTime = BigInt(Math.floor(coin.status.block_time));
@@ -192,6 +192,7 @@ class ExpoWallet {
192
192
  catch {
193
193
  // expo-background-task not installed — nothing to unregister
194
194
  }
195
+ await this.wallet.dispose();
195
196
  }
196
197
  // ── IWallet delegation ───────────────────────────────────────────
197
198
  getAddress() {
@@ -580,6 +580,20 @@ class WalletMessageHandler {
580
580
  this.contractEventsSubscription();
581
581
  this.contractEventsSubscription = undefined;
582
582
  }
583
+ // Dispose the wallet to stop the ContractWatcher (and its polling
584
+ // intervals) before clearing the repositories, otherwise the poller
585
+ // will hit a closing IndexedDB connection.
586
+ try {
587
+ if (this.wallet) {
588
+ await this.wallet.dispose();
589
+ }
590
+ else {
591
+ await this.readonlyWallet.dispose();
592
+ }
593
+ }
594
+ catch (_) {
595
+ // best-effort teardown
596
+ }
583
597
  try {
584
598
  await this.walletRepository?.clear();
585
599
  }
@@ -1,16 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = exports.DEFAULT_THRESHOLD_MS = void 0;
3
+ exports.VtxoManager = exports.DEFAULT_SETTLEMENT_CONFIG = exports.DEFAULT_RENEWAL_CONFIG = exports.DEFAULT_THRESHOLD_SECONDS = exports.DEFAULT_THRESHOLD_MS = void 0;
4
4
  exports.isVtxoExpiringSoon = isVtxoExpiringSoon;
5
5
  exports.getExpiringAndRecoverableVtxos = getExpiringAndRecoverableVtxos;
6
6
  const _1 = require(".");
7
+ const arkTransaction_1 = require("../utils/arkTransaction");
8
+ const tapscript_1 = require("../script/tapscript");
9
+ const base_1 = require("@scure/base");
10
+ const base_2 = require("../script/base");
11
+ const transaction_1 = require("../utils/transaction");
12
+ const txSizeEstimator_1 = require("../utils/txSizeEstimator");
13
+ /** Type guard to check if a wallet has the properties needed for sweep operations. */
14
+ function isSweepCapable(wallet) {
15
+ return ("boardingTapscript" in wallet &&
16
+ "onchainProvider" in wallet &&
17
+ "network" in wallet);
18
+ }
19
+ /** Asserts that the wallet supports sweep operations, throwing a clear error if not. */
20
+ function assertSweepCapable(wallet) {
21
+ if (!isSweepCapable(wallet)) {
22
+ throw new Error("Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, and network");
23
+ }
24
+ }
7
25
  exports.DEFAULT_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
26
+ exports.DEFAULT_THRESHOLD_SECONDS = 3 * 24 * 60 * 60; // 3 days
8
27
  /**
9
28
  * Default renewal configuration values
29
+ * @deprecated Use DEFAULT_SETTLEMENT_CONFIG instead
10
30
  */
11
31
  exports.DEFAULT_RENEWAL_CONFIG = {
12
32
  thresholdMs: exports.DEFAULT_THRESHOLD_MS, // 3 days
13
33
  };
34
+ /**
35
+ * Default settlement configuration values
36
+ */
37
+ exports.DEFAULT_SETTLEMENT_CONFIG = {
38
+ vtxoThreshold: exports.DEFAULT_THRESHOLD_SECONDS,
39
+ boardingUtxoSweep: true,
40
+ pollIntervalMs: 60000,
41
+ };
42
+ /** Extracts the dust amount from the wallet, defaulting to 330 sats. */
14
43
  function getDustAmount(wallet) {
15
44
  return "dustAmount" in wallet ? wallet.dustAmount : 330n;
16
45
  }
@@ -157,9 +186,38 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
157
186
  * ```
158
187
  */
159
188
  class VtxoManager {
160
- constructor(wallet, renewalConfig) {
189
+ constructor(wallet,
190
+ /** @deprecated Use settlementConfig instead */
191
+ renewalConfig, settlementConfig) {
161
192
  this.wallet = wallet;
162
193
  this.renewalConfig = renewalConfig;
194
+ this.knownBoardingUtxos = new Set();
195
+ this.sweptBoardingUtxos = new Set();
196
+ this.pollInProgress = false;
197
+ // Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
198
+ if (settlementConfig !== undefined) {
199
+ this.settlementConfig = settlementConfig;
200
+ }
201
+ else if (renewalConfig && renewalConfig.enabled) {
202
+ this.settlementConfig = {
203
+ vtxoThreshold: renewalConfig.thresholdMs
204
+ ? renewalConfig.thresholdMs / 1000
205
+ : undefined,
206
+ };
207
+ }
208
+ else if (renewalConfig) {
209
+ // renewalConfig provided but not enabled → disabled
210
+ this.settlementConfig = false;
211
+ }
212
+ else {
213
+ // No config at all → enabled by default
214
+ this.settlementConfig = { ...exports.DEFAULT_SETTLEMENT_CONFIG };
215
+ }
216
+ this.contractEventsSubscriptionReady =
217
+ this.initializeSubscription().then((subscription) => {
218
+ this.contractEventsSubscription = subscription;
219
+ return subscription;
220
+ });
163
221
  }
164
222
  // ========== Recovery Methods ==========
165
223
  /**
@@ -271,10 +329,26 @@ class VtxoManager {
271
329
  * ```
272
330
  */
273
331
  async getExpiringVtxos(thresholdMs) {
332
+ // If settlementConfig is explicitly false and no override provided, renewal is disabled
333
+ if (this.settlementConfig === false && thresholdMs === undefined) {
334
+ return [];
335
+ }
274
336
  const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
275
- const threshold = thresholdMs ??
276
- this.renewalConfig?.thresholdMs ??
277
- exports.DEFAULT_RENEWAL_CONFIG.thresholdMs;
337
+ // Resolve threshold: method param > settlementConfig (seconds→ms) > renewalConfig > default
338
+ let threshold;
339
+ if (thresholdMs !== undefined) {
340
+ threshold = thresholdMs;
341
+ }
342
+ else if (this.settlementConfig !== false &&
343
+ this.settlementConfig &&
344
+ this.settlementConfig.vtxoThreshold !== undefined) {
345
+ threshold = this.settlementConfig.vtxoThreshold * 1000;
346
+ }
347
+ else {
348
+ threshold =
349
+ this.renewalConfig?.thresholdMs ??
350
+ exports.DEFAULT_RENEWAL_CONFIG.thresholdMs;
351
+ }
278
352
  return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet));
279
353
  }
280
354
  /**
@@ -304,7 +378,11 @@ class VtxoManager {
304
378
  */
305
379
  async renewVtxos(eventCallback) {
306
380
  // Get all VTXOs (including recoverable ones)
307
- const vtxos = await this.getExpiringVtxos();
381
+ // Use default threshold to bypass settlementConfig gate (manual API should always work)
382
+ const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
383
+ this.settlementConfig?.vtxoThreshold !== undefined
384
+ ? this.settlementConfig.vtxoThreshold * 1000
385
+ : exports.DEFAULT_RENEWAL_CONFIG.thresholdMs);
308
386
  if (vtxos.length === 0) {
309
387
  throw new Error("No VTXOs available to renew");
310
388
  }
@@ -326,5 +404,312 @@ class VtxoManager {
326
404
  ],
327
405
  }, eventCallback);
328
406
  }
407
+ // ========== Boarding UTXO Sweep Methods ==========
408
+ /**
409
+ * Get boarding UTXOs whose timelock has expired.
410
+ *
411
+ * These UTXOs can no longer be onboarded cooperatively via `settle()` and
412
+ * must be swept back to a fresh boarding address using the unilateral exit path.
413
+ *
414
+ * @returns Array of expired boarding UTXOs
415
+ *
416
+ * @example
417
+ * ```typescript
418
+ * const manager = new VtxoManager(wallet);
419
+ * const expired = await manager.getExpiredBoardingUtxos();
420
+ * if (expired.length > 0) {
421
+ * console.log(`${expired.length} expired boarding UTXOs to sweep`);
422
+ * }
423
+ * ```
424
+ */
425
+ async getExpiredBoardingUtxos() {
426
+ const boardingUtxos = await this.wallet.getBoardingUtxos();
427
+ const boardingTimelock = this.getBoardingTimelock();
428
+ // For block-based timelocks, fetch the chain tip height
429
+ let chainTipHeight;
430
+ if (boardingTimelock.type === "blocks") {
431
+ const tip = await this.getOnchainProvider().getChainTip();
432
+ chainTipHeight = tip.height;
433
+ }
434
+ return boardingUtxos.filter((utxo) => (0, arkTransaction_1.hasBoardingTxExpired)(utxo, boardingTimelock, chainTipHeight));
435
+ }
436
+ /**
437
+ * Sweep expired boarding UTXOs back to a fresh boarding address via
438
+ * the unilateral exit path (on-chain self-spend).
439
+ *
440
+ * This builds a raw on-chain transaction that:
441
+ * - Uses all expired boarding UTXOs as inputs (spent via the CSV exit script path)
442
+ * - Has a single output to the wallet's boarding address (restarts the timelock)
443
+ * - Batches multiple expired UTXOs into one transaction
444
+ * - Skips the sweep if the output after fees would be below dust
445
+ *
446
+ * No Ark server involvement is needed — this is a pure on-chain transaction.
447
+ *
448
+ * @returns The broadcast transaction ID
449
+ * @throws Error if no expired boarding UTXOs found
450
+ * @throws Error if output after fees is below dust (not economical to sweep)
451
+ * @throws Error if boarding UTXO sweep is not enabled in settlementConfig
452
+ *
453
+ * @example
454
+ * ```typescript
455
+ * const manager = new VtxoManager(wallet, undefined, {
456
+ * boardingUtxoSweep: true,
457
+ * });
458
+ *
459
+ * try {
460
+ * const txid = await manager.sweepExpiredBoardingUtxos();
461
+ * console.log('Swept expired boarding UTXOs:', txid);
462
+ * } catch (e) {
463
+ * console.log('No sweep needed or not economical');
464
+ * }
465
+ * ```
466
+ */
467
+ async sweepExpiredBoardingUtxos() {
468
+ const sweepEnabled = this.settlementConfig !== false &&
469
+ (this.settlementConfig?.boardingUtxoSweep ??
470
+ exports.DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
471
+ if (!sweepEnabled) {
472
+ throw new Error("Boarding UTXO sweep is not enabled in settlementConfig");
473
+ }
474
+ const allExpired = await this.getExpiredBoardingUtxos();
475
+ // Filter out UTXOs already swept (tx broadcast but not yet confirmed)
476
+ const expiredUtxos = allExpired.filter((u) => !this.sweptBoardingUtxos.has(`${u.txid}:${u.vout}`));
477
+ if (expiredUtxos.length === 0) {
478
+ throw new Error("No expired boarding UTXOs to sweep");
479
+ }
480
+ const boardingAddress = await this.wallet.getBoardingAddress();
481
+ // Get fee rate from onchain provider
482
+ const feeRate = (await this.getOnchainProvider().getFeeRate()) ?? 1;
483
+ // Get the exit tap leaf script for signing
484
+ const exitTapLeafScript = this.getBoardingExitLeaf();
485
+ // Estimate transaction size for fee calculation
486
+ const sequence = (0, base_2.getSequence)(exitTapLeafScript);
487
+ // TapLeafScript: [{version, internalKey, merklePath}, scriptWithVersion]
488
+ const leafScript = exitTapLeafScript[1];
489
+ const leafScriptSize = leafScript.length - 1; // minus version byte
490
+ const controlBlockSize = exitTapLeafScript[0].merklePath.length * 32;
491
+ // Exit path witness: 1 Schnorr signature (64 bytes)
492
+ const leafWitnessSize = 64;
493
+ const estimator = txSizeEstimator_1.TxWeightEstimator.create();
494
+ for (const _ of expiredUtxos) {
495
+ estimator.addTapscriptInput(leafWitnessSize, leafScriptSize, controlBlockSize);
496
+ }
497
+ estimator.addOutputAddress(boardingAddress, this.getNetwork());
498
+ const fee = Math.ceil(Number(estimator.vsize().value) * feeRate);
499
+ const totalValue = expiredUtxos.reduce((sum, utxo) => sum + BigInt(utxo.value), 0n);
500
+ const outputAmount = totalValue - BigInt(fee);
501
+ // Dust check: skip if output after fees is below dust
502
+ const dustAmount = getDustAmount(this.wallet);
503
+ if (outputAmount < dustAmount) {
504
+ throw new Error(`Sweep not economical: output ${outputAmount} sats after ${fee} sats fee is below dust (${dustAmount} sats)`);
505
+ }
506
+ // Build the raw transaction
507
+ const tx = new transaction_1.Transaction();
508
+ for (const utxo of expiredUtxos) {
509
+ tx.addInput({
510
+ txid: utxo.txid,
511
+ index: utxo.vout,
512
+ witnessUtxo: {
513
+ script: this.getBoardingOutputScript(),
514
+ amount: BigInt(utxo.value),
515
+ },
516
+ tapLeafScript: [exitTapLeafScript],
517
+ sequence,
518
+ });
519
+ }
520
+ tx.addOutputAddress(boardingAddress, outputAmount, this.getNetwork());
521
+ // Sign and finalize
522
+ const signedTx = await this.getIdentity().sign(tx);
523
+ signedTx.finalize();
524
+ // Broadcast
525
+ const txid = await this.getOnchainProvider().broadcastTransaction(signedTx.hex);
526
+ // Mark UTXOs as swept to prevent duplicate broadcasts on next poll
527
+ for (const u of expiredUtxos) {
528
+ this.sweptBoardingUtxos.add(`${u.txid}:${u.vout}`);
529
+ }
530
+ // Mark the sweep output as "known" so the next poll doesn't try to
531
+ // auto-settle it back into Ark (it lands at the same boarding address).
532
+ this.knownBoardingUtxos.add(`${txid}:0`);
533
+ return txid;
534
+ }
535
+ // ========== Private Helpers ==========
536
+ /** Asserts sweep capability and returns the typed wallet. */
537
+ getSweepWallet() {
538
+ assertSweepCapable(this.wallet);
539
+ return this.wallet;
540
+ }
541
+ /** Decodes the boarding tapscript exit path to extract the CSV timelock. */
542
+ getBoardingTimelock() {
543
+ const wallet = this.getSweepWallet();
544
+ const exitScript = tapscript_1.CSVMultisigTapscript.decode(base_1.hex.decode(wallet.boardingTapscript.exitScript));
545
+ return exitScript.params.timelock;
546
+ }
547
+ /** Returns the TapLeafScript for the boarding tapscript's exit (CSV) path. */
548
+ getBoardingExitLeaf() {
549
+ return this.getSweepWallet().boardingTapscript.exit();
550
+ }
551
+ /** Returns the pkScript (output script) of the boarding tapscript. */
552
+ getBoardingOutputScript() {
553
+ return this.getSweepWallet().boardingTapscript.pkScript;
554
+ }
555
+ /** Returns the on-chain provider for fee estimation and broadcasting. */
556
+ getOnchainProvider() {
557
+ return this.getSweepWallet().onchainProvider;
558
+ }
559
+ /** Returns the Bitcoin network configuration from the wallet. */
560
+ getNetwork() {
561
+ return this.getSweepWallet().network;
562
+ }
563
+ /** Returns the wallet's identity for transaction signing. */
564
+ getIdentity() {
565
+ return this.wallet.identity;
566
+ }
567
+ async initializeSubscription() {
568
+ if (this.settlementConfig === false) {
569
+ return undefined;
570
+ }
571
+ // Start polling for boarding UTXOs independently of contract manager
572
+ // SSE setup. Use a short delay to let the wallet finish construction.
573
+ setTimeout(() => this.startBoardingUtxoPoll(), 1000);
574
+ try {
575
+ const [delegatorManager, contractManager, destination] = await Promise.all([
576
+ this.wallet.getDelegatorManager(),
577
+ this.wallet.getContractManager(),
578
+ this.wallet.getAddress(),
579
+ ]);
580
+ const stopWatching = contractManager.onContractEvent((event) => {
581
+ if (event.type !== "vtxo_received") {
582
+ return;
583
+ }
584
+ this.renewVtxos().catch((e) => {
585
+ if (e instanceof Error) {
586
+ if (e.message.includes("No VTXOs available to renew")) {
587
+ // Not an error, just no VTXO eligible for renewal.
588
+ return;
589
+ }
590
+ if (e.message.includes("is below dust threshold")) {
591
+ // Not an error, just below dust threshold.
592
+ // As more VTXOs are received, the threshold will be raised.
593
+ return;
594
+ }
595
+ }
596
+ console.error("Error renewing VTXOs:", e);
597
+ });
598
+ delegatorManager
599
+ ?.delegate(event.vtxos, destination)
600
+ .catch((e) => {
601
+ console.error("Error delegating VTXOs:", e);
602
+ });
603
+ });
604
+ return stopWatching;
605
+ }
606
+ catch (e) {
607
+ console.error("Error renewing VTXOs from VtxoManager", e);
608
+ return undefined;
609
+ }
610
+ }
611
+ /**
612
+ * Starts a polling loop that:
613
+ * 1. Auto-settles new boarding UTXOs into Ark
614
+ * 2. Sweeps expired boarding UTXOs (when boardingUtxoSweep is enabled)
615
+ */
616
+ startBoardingUtxoPoll() {
617
+ if (this.settlementConfig === false)
618
+ return;
619
+ const intervalMs = this.settlementConfig.pollIntervalMs ??
620
+ exports.DEFAULT_SETTLEMENT_CONFIG.pollIntervalMs;
621
+ // Run once immediately, then on interval
622
+ this.pollBoardingUtxos();
623
+ this.pollIntervalId = setInterval(() => this.pollBoardingUtxos(), intervalMs);
624
+ }
625
+ async pollBoardingUtxos() {
626
+ // Guard: wallet must support boarding UTXO + sweep operations
627
+ if (!isSweepCapable(this.wallet))
628
+ return;
629
+ // Skip if a previous poll is still running
630
+ if (this.pollInProgress)
631
+ return;
632
+ this.pollInProgress = true;
633
+ try {
634
+ // Settle new (unexpired) UTXOs first, then sweep expired ones.
635
+ // Sequential to avoid racing for the same UTXOs.
636
+ try {
637
+ await this.settleBoardingUtxos();
638
+ }
639
+ catch (e) {
640
+ console.error("Error auto-settling boarding UTXOs:", e);
641
+ }
642
+ const sweepEnabled = this.settlementConfig !== false &&
643
+ (this.settlementConfig?.boardingUtxoSweep ??
644
+ exports.DEFAULT_SETTLEMENT_CONFIG.boardingUtxoSweep);
645
+ if (sweepEnabled) {
646
+ try {
647
+ await this.sweepExpiredBoardingUtxos();
648
+ }
649
+ catch (e) {
650
+ if (!(e instanceof Error) ||
651
+ !e.message.includes("No expired boarding UTXOs")) {
652
+ console.error("Error auto-sweeping boarding UTXOs:", e);
653
+ }
654
+ }
655
+ }
656
+ }
657
+ finally {
658
+ this.pollInProgress = false;
659
+ }
660
+ }
661
+ /**
662
+ * Auto-settle new (unexpired) boarding UTXOs into the Ark.
663
+ * Skips UTXOs that are already expired (those are handled by sweep).
664
+ * Only settles UTXOs not already in-flight (tracked in knownBoardingUtxos).
665
+ * UTXOs are marked as known only after a successful settle, so failed
666
+ * attempts will be retried on the next poll.
667
+ */
668
+ async settleBoardingUtxos() {
669
+ const boardingUtxos = await this.wallet.getBoardingUtxos();
670
+ // Exclude expired UTXOs — those should be swept, not settled.
671
+ // If we can't determine expired status, bail out entirely to avoid
672
+ // accidentally settling expired UTXOs (which would conflict with sweep).
673
+ let expiredSet;
674
+ try {
675
+ const expired = await this.getExpiredBoardingUtxos();
676
+ expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
677
+ }
678
+ catch {
679
+ return;
680
+ }
681
+ const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
682
+ !expiredSet.has(`${u.txid}:${u.vout}`));
683
+ if (unsettledUtxos.length === 0)
684
+ return;
685
+ const dustAmount = getDustAmount(this.wallet);
686
+ const totalAmount = unsettledUtxos.reduce((sum, u) => sum + BigInt(u.value), 0n);
687
+ if (totalAmount < dustAmount)
688
+ return;
689
+ const arkAddress = await this.wallet.getAddress();
690
+ await this.wallet.settle({
691
+ inputs: unsettledUtxos,
692
+ outputs: [{ address: arkAddress, amount: totalAmount }],
693
+ });
694
+ // Mark as known only after successful settle
695
+ for (const u of unsettledUtxos) {
696
+ this.knownBoardingUtxos.add(`${u.txid}:${u.vout}`);
697
+ }
698
+ }
699
+ async dispose() {
700
+ this.disposePromise ?? (this.disposePromise = (async () => {
701
+ if (this.pollIntervalId) {
702
+ clearInterval(this.pollIntervalId);
703
+ this.pollIntervalId = undefined;
704
+ }
705
+ const subscription = await this.contractEventsSubscriptionReady;
706
+ this.contractEventsSubscription = undefined;
707
+ subscription?.();
708
+ })());
709
+ return this.disposePromise;
710
+ }
711
+ async [Symbol.asyncDispose]() {
712
+ await this.dispose();
713
+ }
329
714
  }
330
715
  exports.VtxoManager = VtxoManager;