@solard/cli 0.2.17 → 0.2.20

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/src/index.ts CHANGED
@@ -122,7 +122,7 @@ function friendlyCliError(error: unknown): string | null {
122
122
  if (code === "MISSING_CONFIG" && error.message.includes("SLRD_MASTER_KEY")) {
123
123
  return `${error.message}\n\nEnter the wallet password interactively, or set SLRD_MASTER_KEY for automation/CI.`;
124
124
  }
125
- return null;
125
+ return error.message.trim() || error.name;
126
126
  }
127
127
  function duration(value: string | undefined, fallbackMs: number): number {
128
128
  if (!value) return fallbackMs;
@@ -279,6 +279,8 @@ function commandNeedsSigningVault(
279
279
  if (values[0] === "resume") return true;
280
280
  return flags.has("live");
281
281
  }
282
+ if (command === "reclaim") return flags.has("live") || flags.has("simulate");
283
+ if (command === "unwrap" && values[0] === "wsol") return true;
282
284
  if (
283
285
  [
284
286
  "import",
@@ -317,12 +319,21 @@ function commandNeedsSigningVault(
317
319
  "spam-buy",
318
320
  "buy-spam",
319
321
  "run",
322
+ "strategy",
320
323
  ].includes(command)
321
324
  )
322
325
  return true;
323
326
  return false;
324
327
  }
325
328
 
329
+ function sweepUsage(): string {
330
+ return (
331
+ "Usage: slrd sweep --to <contact|wallet|address> --below <SOL> " +
332
+ "[--wallets <a,b,...>] [--keep <wallet=SOL,...>] [--simulate | --live] [--json]\n" +
333
+ "Compatibility: slrd sweep sol --to <destination> --max-balance-sol <SOL>"
334
+ );
335
+ }
336
+
326
337
  async function storedWalletCount(): Promise<number> {
327
338
  const { createTraderSolard } = await import("@solard/core");
328
339
  const probe = createTraderSolard();
@@ -355,7 +366,9 @@ External contacts (public addresses only; never signing wallets or group members
355
366
  slrd token <token_ca> [name] [--metadata-json <json>]
356
367
  slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
357
368
  slrd token refresh <token|ca>
358
- slrd token backfill <ca> [--replace] [--confirmed] [--batch-size 100] [--rpc-concurrency 3] [--max-signatures N] [--json]
369
+ slrd token backfill <ca> [--max-signatures N] [--json] Index signatures only; no transaction hydration by default
370
+ slrd token backfill <ca> --materialize [--price-sample-ms 1000] [--replace] [--rpc-concurrency 2]
371
+ slrd token backfill <ca> --exact [--replace] [--rpc-concurrency 2]
359
372
  Durable exact trades + sparse 1s candle materialization from creation
360
373
  slrd token trades <ca> [--from-start] [--min-sol N] [--owner <wallet>] [--side buy|sell] [--limit N] [--json]
361
374
  slrd token analyze <ca> [--top N] [--json]
@@ -372,8 +385,18 @@ Vanity mints
372
385
  slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
373
386
  launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
374
387
 
375
- Pump pairs
388
+ Shared market feed
389
+ slrd feed serve [--host 127.0.0.1] [--port 8788] [--rpc-rps 5] [--rpc-read-rps 2] [--max-fallback-subs 200]
390
+ One upstream Pump/PumpSwap/LaunchLab feed shared by watchers, agents, and future UI clients
391
+
392
+ Launch discovery
393
+ slrd launch watch [--venue pump,raydium-launchlab] [--min-mcap 5000] [--ath-step-pct 20] [--track-ttl 30m] [--feed ws://127.0.0.1:8788/ws] [--show-new] [--prices] [--json]
394
+ Consume the shared feed; ignore Pump Mayhem and notify on first $5k cross and each +20% notified ATH step
395
+
396
+ Pump discovery
376
397
  slrd pump pairs [--json] Read Pump-supported quote mints from the on-chain Global account
398
+ slrd pump watch [--min-mcap 5000] [--ath-step-pct 20] [--track-ttl 30m] [--feed ws://127.0.0.1:8788/ws] [--show-new] [--prices] [--json]
399
+ Compatibility alias for: slrd launch watch --venue pump
377
400
 
378
401
  Metadata and launching
379
402
  slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--pair SOL|USDC|<custom-quote-mint>] [--beneficiary <wallet|address>] [--alias <name>] [--live] [--skip-simulation]
@@ -401,11 +424,12 @@ Backtesting
401
424
  Transfers and consolidation
402
425
  slrd transfer <contact|wallet|address> --wallet <source-wallet> --sol <amount> [--simulate-only]
403
426
  slrd transfer <contact|wallet|address> --wallet <source-wallet> --token <USDC|mint> --amount <ui> [--simulate-only]
404
- slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--max-balance-sol <SOL>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]
405
- Without --wallets, sweep considers all stored signing wallets
427
+ slrd sweep --to <contact|wallet|address> --below <SOL> [--wallets <a,b,...>] [--keep <wallet=SOL,...>] [--simulate | --live] [--json]
428
+ Sweeps stored wallets strictly below the threshold; preview is the default
429
+ slrd sweep sol ... Compatibility alias; --max-balance-sol remains accepted
406
430
 
407
431
  Token liquidation
408
- slrd liquidate tokens [--except <token|mint>] [--wallets <a,b,...>] [--except-wallet <wallet>] [--except-wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--burn-unsellable] [--simulate | --live]
432
+ slrd liquidate tokens [--except <token|mint>] [--wallets <a,b,...>] [--except-wallet <wallet>] [--except-wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--burn-unsellable] [--rounds 3] [--simulate | --live]
409
433
  Sell all supported tokens unless protected with --except; excluded wallets are not scanned or touched
410
434
 
411
435
  RPC
@@ -426,7 +450,12 @@ Trading
426
450
  slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
427
451
  slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
428
452
  slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--venue auto|native|jupiter] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
429
- slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--continue-on-error] [--simulate-only]
453
+ slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
454
+ Close every owned WSOL token account, including non-ATA WSOL accounts
455
+ slrd reclaim inspect --all-wallets List Loader-v3 buffers/programs controlled by stored wallets
456
+ slrd reclaim buffers --all-wallets [--simulate | --live]
457
+ slrd reclaim programs --all-wallets [--simulate | --live --confirm-program-close]
458
+ slrd reclaim all --all-wallets [--simulate | --live --confirm-program-close]
430
459
  slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
431
460
  slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
432
461
  slrd transfer-many status <stable-id>
@@ -441,10 +470,14 @@ Trading
441
470
  slrd rewards claim <token|ca> --wallet <fee-payer> [--id <stable-claim-id> --basis <reward-basis.json>]
442
471
  slrd rewards claim-status <stable-claim-id>
443
472
 
444
- Scripts (strategies stay outside the kernel)
473
+ Scripts and event strategies
445
474
  slrd scripts List scripts registered in slrd.config.ts
446
475
  slrd run <name-or-path> [script flags...] Execute a script that imports slrd
447
476
  slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
477
+ slrd strategy run <file.ts> --token <mint|alias> --wallet <wallet> [--params <json>] [--feed ws://127.0.0.1:8788/ws] [--live]
478
+ Run an onPrice strategy from the shared feed; wallet/execution state remains isolated per agent
479
+ slrd strategy backtest <file.ts> --token <mint> [--params <json>] [--capital-sol 1] [--price-sample 5s] [--sol-usd N] [--json]
480
+ Replay the same strategy file over durable token history
448
481
 
449
482
  Meteora DLMM
450
483
  slrd meteora discover --timeframe 30m --sort fee-active-tvl --limit 20
@@ -481,6 +514,9 @@ Watching
481
514
  slrd watch list
482
515
 
483
516
  Transactions and ALTs
517
+ slrd tx stream --wallet <wallet|address> [--finalized] [--jsonl]
518
+ slrd tx stream --wallets <a,b,c> [--finalized] [--jsonl]
519
+ Stream every transaction mentioning the selected wallets until Ctrl+C
484
520
  slrd history
485
521
  slrd jito tip-accounts [--endpoint <block-engine-url>]
486
522
  slrd alt add <address> [label]
@@ -782,6 +818,24 @@ async function main() {
782
818
  process.exitCode = await runScript(script, scriptArgs);
783
819
  return;
784
820
  }
821
+ if (command === "feed" && values[0] === "serve") {
822
+ const { runPriceFeedServerCommand } =
823
+ await import("./price-feed-server.ts");
824
+ await runPriceFeedServerCommand({ flags, emit });
825
+ return;
826
+ }
827
+ if (command === "strategy" && values[0] === "backtest") {
828
+ const { runStrategyBacktestCommand } =
829
+ await import("./strategy-backtest-command.ts");
830
+ await runStrategyBacktestCommand({ values, flags, emit });
831
+ return;
832
+ }
833
+ if (command === "strategy" && values[0] === "run") {
834
+ const { runTradeStrategyCommand } =
835
+ await import("./trade-strategy-command.ts");
836
+ await runTradeStrategyCommand({ values, flags, emit });
837
+ return;
838
+ }
785
839
  if (
786
840
  (command === "prepare" && values[0] === "pump") ||
787
841
  (command === "launch" &&
@@ -794,6 +848,11 @@ async function main() {
794
848
  await runPumpExternalDeploymentFromArgs(externalArgs);
795
849
  return;
796
850
  }
851
+ if (command === "launch" && values[0] === "watch") {
852
+ const { runLaunchWatchCommand } = await import("./launch-watch-command.ts");
853
+ await runLaunchWatchCommand({ flags, emit });
854
+ return;
855
+ }
797
856
  if (
798
857
  command === "launch" &&
799
858
  (values[0] === "pump" || values[0] === "pump-token")
@@ -822,6 +881,11 @@ async function main() {
822
881
  });
823
882
  return;
824
883
  }
884
+ if (command === "pump" && values[0] === "watch") {
885
+ const { runPumpWatchCommand } = await import("./pump-watch-command.ts");
886
+ await runPumpWatchCommand({ flags, emit });
887
+ return;
888
+ }
825
889
  if (command === "jito" && values[0] === "tip-accounts") {
826
890
  const endpoint = (
827
891
  flags.get("endpoint") ??
@@ -870,6 +934,12 @@ async function main() {
870
934
  return;
871
935
  }
872
936
 
937
+ if (command === "reclaim") {
938
+ const { runReclaimCommand } = await import("./reclaim-command.ts");
939
+ await runReclaimCommand({ slrd, values, flags, emit });
940
+ return;
941
+ }
942
+
873
943
  if (command === "meteora") {
874
944
  await handleMeteoraCommand({ slrd, values, flags, emit });
875
945
  return;
@@ -1303,7 +1373,7 @@ async function main() {
1303
1373
  try {
1304
1374
  const tx = await connection.getTransaction(entry.signature, {
1305
1375
  commitment: "confirmed",
1306
- maxSupportedTransactionVersion: 0,
1376
+ maxSupportedTransactionVersion: 1,
1307
1377
  });
1308
1378
  const fee = tx?.meta?.fee;
1309
1379
  const payer = txAccountKeys(tx)[0] ?? "";
@@ -1367,6 +1437,328 @@ async function main() {
1367
1437
  );
1368
1438
  return;
1369
1439
  }
1440
+ if (command === "tx" && values[0] === "stream") {
1441
+ const walletFlag = flags.get("wallet");
1442
+ const walletRefs = [
1443
+ ...(walletFlag && walletFlag !== "true" ? [walletFlag] : []),
1444
+ ...csv(flags.get("wallets")),
1445
+ ];
1446
+ if (walletRefs.length === 0) {
1447
+ throw new Error(
1448
+ "Usage: slrd tx stream --wallet <wallet|address> | --wallets <a,b,c> [--finalized] [--jsonl]",
1449
+ );
1450
+ }
1451
+
1452
+ const { PublicKey } = await import("@solana/web3.js");
1453
+ const connection = slrd.connection();
1454
+ const commitment = flags.has("finalized") ? "finalized" : "confirmed";
1455
+ const enrichAttempts = Math.max(
1456
+ 1,
1457
+ Math.min(20, Math.trunc(int(flags, "enrich-attempts", 8) ?? 8)),
1458
+ );
1459
+ const enrichDelayMs = Math.max(
1460
+ 50,
1461
+ Math.trunc(int(flags, "enrich-delay-ms", 250) ?? 250),
1462
+ );
1463
+
1464
+ type StreamWallet = {
1465
+ ref: string;
1466
+ name: string | null;
1467
+ address: string;
1468
+ publicKey: InstanceType<typeof PublicKey>;
1469
+ };
1470
+
1471
+ const byAddress = new Map<string, StreamWallet>();
1472
+ for (const ref of walletRefs) {
1473
+ let name: string | null = null;
1474
+ let publicKey: InstanceType<typeof PublicKey>;
1475
+ try {
1476
+ const resolved = slrd.resolveWallet(ref);
1477
+ publicKey = resolved.address;
1478
+ name = resolved.row?.name ?? null;
1479
+ } catch {
1480
+ try {
1481
+ publicKey = new PublicKey(ref);
1482
+ } catch {
1483
+ throw new Error(`Unknown wallet or invalid Solana address: ${ref}`);
1484
+ }
1485
+ }
1486
+ const address = publicKey.toBase58();
1487
+ if (!byAddress.has(address)) {
1488
+ byAddress.set(address, { ref, name, address, publicKey });
1489
+ }
1490
+ }
1491
+ const watched = [...byAddress.values()];
1492
+ const watchedAddresses = new Set(watched.map((row) => row.address));
1493
+ const labelFor = (row: StreamWallet) =>
1494
+ row.name ? `@${row.name}` : shortKey(row.address);
1495
+
1496
+ type PendingSignature = {
1497
+ signature: string;
1498
+ observedSlot: number | null;
1499
+ };
1500
+
1501
+ const queue: PendingSignature[] = [];
1502
+ const queued = new Set<string>();
1503
+ const seen = new Set<string>();
1504
+ const seenOrder: string[] = [];
1505
+ const listenerIds: number[] = [];
1506
+ let processing = false;
1507
+ let stopped = false;
1508
+ let stopResolve: (() => void) | null = null;
1509
+
1510
+ const rememberSeen = (signature: string) => {
1511
+ if (seen.has(signature)) return;
1512
+ seen.add(signature);
1513
+ seenOrder.push(signature);
1514
+ while (seenOrder.length > 20_000) {
1515
+ const oldest = seenOrder.shift();
1516
+ if (oldest) seen.delete(oldest);
1517
+ }
1518
+ };
1519
+
1520
+ const transactionWithRetry = async (signature: string) => {
1521
+ for (let attempt = 1; attempt <= enrichAttempts; attempt += 1) {
1522
+ const tx = await connection.getTransaction(signature, {
1523
+ commitment,
1524
+ maxSupportedTransactionVersion: 1,
1525
+ });
1526
+ if (tx) return tx;
1527
+ if (attempt < enrichAttempts) {
1528
+ await new Promise((resolve) => setTimeout(resolve, enrichDelayMs));
1529
+ }
1530
+ }
1531
+ return null;
1532
+ };
1533
+
1534
+ const printTransaction = async (
1535
+ item: PendingSignature,
1536
+ ): Promise<void> => {
1537
+ const tx = await transactionWithRetry(item.signature);
1538
+ if (!tx?.meta) {
1539
+ const unavailable = {
1540
+ type: "transaction",
1541
+ signature: item.signature,
1542
+ slot: item.observedSlot,
1543
+ commitment,
1544
+ status: "unavailable",
1545
+ wallets: [],
1546
+ };
1547
+ if (flags.has("jsonl")) emit(JSON.stringify(unavailable) + "\n");
1548
+ else
1549
+ emit(
1550
+ `${new Date().toISOString()} UNAVAILABLE ${item.signature} slot=${item.observedSlot ?? "?"}\n`,
1551
+ );
1552
+ return;
1553
+ }
1554
+
1555
+ const keys = txAccountKeys(tx);
1556
+ const keySet = new Set(keys);
1557
+ const touched = watched.filter((row) => keySet.has(row.address));
1558
+ if (touched.length === 0) return;
1559
+
1560
+ const feeLamports =
1561
+ typeof tx.meta.fee === "number" && Number.isFinite(tx.meta.fee)
1562
+ ? BigInt(Math.trunc(tx.meta.fee))
1563
+ : 0n;
1564
+ const payer = keys[0] ?? null;
1565
+ const status = tx.meta.err ? "failed" : "confirmed";
1566
+ const at =
1567
+ tx.blockTime == null
1568
+ ? new Date().toISOString()
1569
+ : new Date(tx.blockTime * 1_000).toISOString();
1570
+
1571
+ const walletRows = touched.map((wallet) => {
1572
+ const walletIndex = keys.indexOf(wallet.address);
1573
+ let nativeDeltaLamports: bigint | null = null;
1574
+ if (
1575
+ walletIndex >= 0 &&
1576
+ typeof tx.meta!.preBalances?.[walletIndex] === "number" &&
1577
+ typeof tx.meta!.postBalances?.[walletIndex] === "number"
1578
+ ) {
1579
+ nativeDeltaLamports =
1580
+ BigInt(Math.trunc(tx.meta!.postBalances[walletIndex]!)) -
1581
+ BigInt(Math.trunc(tx.meta!.preBalances[walletIndex]!));
1582
+ }
1583
+
1584
+ const tokenDeltas = [...txWalletTokenBalances(tx, wallet.address)]
1585
+ .map(([mint, balance]) => ({
1586
+ mint,
1587
+ decimals: balance.decimals,
1588
+ deltaRaw: balance.post - balance.pre,
1589
+ }))
1590
+ .filter((row) => row.deltaRaw !== 0n)
1591
+ .sort((left, right) => {
1592
+ const leftAbs =
1593
+ left.deltaRaw < 0n ? -left.deltaRaw : left.deltaRaw;
1594
+ const rightAbs =
1595
+ right.deltaRaw < 0n ? -right.deltaRaw : right.deltaRaw;
1596
+ return leftAbs === rightAbs
1597
+ ? left.mint.localeCompare(right.mint)
1598
+ : leftAbs > rightAbs
1599
+ ? -1
1600
+ : 1;
1601
+ });
1602
+
1603
+ return {
1604
+ ref: wallet.ref,
1605
+ name: wallet.name,
1606
+ address: wallet.address,
1607
+ nativeDeltaLamports,
1608
+ feePaidLamports: payer === wallet.address ? feeLamports : 0n,
1609
+ tokenDeltas,
1610
+ };
1611
+ });
1612
+
1613
+ if (flags.has("jsonl")) {
1614
+ emit(
1615
+ JSON.stringify(
1616
+ {
1617
+ type: "transaction",
1618
+ at,
1619
+ signature: item.signature,
1620
+ slot: tx.slot ?? item.observedSlot,
1621
+ commitment,
1622
+ status,
1623
+ error: tx.meta.err ?? null,
1624
+ feeLamports,
1625
+ feePayer: payer,
1626
+ wallets: walletRows,
1627
+ },
1628
+ (_, value) =>
1629
+ typeof value === "bigint" ? value.toString() : value,
1630
+ ) + "\n",
1631
+ );
1632
+ return;
1633
+ }
1634
+
1635
+ emit(
1636
+ `\n${at} ${status.toUpperCase()} slot=${tx.slot ?? item.observedSlot ?? "?"}\n` +
1637
+ `SIG ${item.signature}\n` +
1638
+ `FEE ${formatRaw(feeLamports, 9)} SOL payer=${payer ?? "?"}\n`,
1639
+ );
1640
+
1641
+ for (const row of walletRows) {
1642
+ const wallet = byAddress.get(row.address)!;
1643
+ emit(` ${labelFor(wallet)} ${row.address}\n`);
1644
+ emit(
1645
+ ` SOL ${
1646
+ row.nativeDeltaLamports == null
1647
+ ? "n/a"
1648
+ : `${formatSignedRaw(row.nativeDeltaLamports, 9)} SOL`
1649
+ }${
1650
+ row.feePaidLamports > 0n
1651
+ ? ` (paid fee ${formatRaw(row.feePaidLamports, 9)} SOL)`
1652
+ : ""
1653
+ }\n`,
1654
+ );
1655
+ if (row.tokenDeltas.length === 0) {
1656
+ emit(" TOKEN no wallet-owned SPL balance delta\n");
1657
+ } else {
1658
+ for (const delta of row.tokenDeltas) {
1659
+ const tokenLabel =
1660
+ delta.mint === NATIVE_SOL_MINT
1661
+ ? "WSOL"
1662
+ : delta.mint === CANONICAL_USDC_MINT
1663
+ ? "USDC"
1664
+ : shortKey(delta.mint);
1665
+ emit(
1666
+ ` TOKEN ${formatSignedRaw(delta.deltaRaw, delta.decimals)} ${tokenLabel} ${delta.mint}\n`,
1667
+ );
1668
+ }
1669
+ }
1670
+ }
1671
+
1672
+ if (tx.meta.err) {
1673
+ emit(` ERROR ${JSON.stringify(tx.meta.err)}\n`);
1674
+ }
1675
+ };
1676
+
1677
+ const drain = async (): Promise<void> => {
1678
+ if (processing) return;
1679
+ processing = true;
1680
+ try {
1681
+ while (!stopped && queue.length > 0) {
1682
+ const item = queue.shift()!;
1683
+ queued.delete(item.signature);
1684
+ if (seen.has(item.signature)) continue;
1685
+ try {
1686
+ await printTransaction(item);
1687
+ } catch (error) {
1688
+ const message =
1689
+ error instanceof Error ? error.message : String(error);
1690
+ if (flags.has("jsonl")) {
1691
+ emit(
1692
+ JSON.stringify({
1693
+ type: "stream-error",
1694
+ signature: item.signature,
1695
+ error: message,
1696
+ }) + "\n",
1697
+ );
1698
+ } else {
1699
+ process.stderr.write(
1700
+ `${OWL} tx stream ${item.signature}: ${message}\n`,
1701
+ );
1702
+ }
1703
+ } finally {
1704
+ rememberSeen(item.signature);
1705
+ }
1706
+ }
1707
+ } finally {
1708
+ processing = false;
1709
+ if (!stopped && queue.length > 0) void drain();
1710
+ }
1711
+ };
1712
+
1713
+ const enqueue = (signature: string, observedSlot: number | null) => {
1714
+ if (stopped || seen.has(signature) || queued.has(signature)) return;
1715
+ queued.add(signature);
1716
+ queue.push({ signature, observedSlot });
1717
+ void drain();
1718
+ };
1719
+
1720
+ if (!flags.has("jsonl")) {
1721
+ emit(
1722
+ `TX STREAM wallets=${watched.length} commitment=${commitment} Ctrl+C to stop\n`,
1723
+ );
1724
+ for (const wallet of watched) {
1725
+ emit(` ${labelFor(wallet)} ${wallet.address}\n`);
1726
+ }
1727
+ emit("\n");
1728
+ }
1729
+
1730
+ for (const wallet of watched) {
1731
+ const id = connection.onLogs(
1732
+ wallet.publicKey,
1733
+ (logs, context) => {
1734
+ // Stream failed transactions too. The transaction metadata below
1735
+ // is the authoritative status/error source.
1736
+ enqueue(logs.signature, context?.slot ?? null);
1737
+ },
1738
+ commitment,
1739
+ );
1740
+ listenerIds.push(id);
1741
+ }
1742
+
1743
+ await new Promise<void>((resolve) => {
1744
+ stopResolve = resolve;
1745
+ const stop = () => {
1746
+ if (stopped) return;
1747
+ stopped = true;
1748
+ resolve();
1749
+ };
1750
+ process.once("SIGINT", stop);
1751
+ process.once("SIGTERM", stop);
1752
+ });
1753
+
1754
+ await Promise.allSettled(
1755
+ listenerIds.map((id) => connection.removeOnLogsListener(id)),
1756
+ );
1757
+
1758
+ if (!flags.has("jsonl")) emit("TX STREAM STOPPED\n");
1759
+ return;
1760
+ }
1761
+
1370
1762
  if (command === "sol-flow" || (command === "tx" && values[0] === "flow")) {
1371
1763
  const walletRef = need(flags, "wallet");
1372
1764
  const wallet = slrd.resolveWallet(walletRef);
@@ -1418,7 +1810,7 @@ async function main() {
1418
1810
  try {
1419
1811
  const tx = await connection.getTransaction(entry.signature, {
1420
1812
  commitment: "confirmed",
1421
- maxSupportedTransactionVersion: 0,
1813
+ maxSupportedTransactionVersion: 1,
1422
1814
  });
1423
1815
  if (!tx?.meta) {
1424
1816
  rows.push({
@@ -1894,7 +2286,6 @@ async function main() {
1894
2286
  : flags.has("simulate")
1895
2287
  ? "SIMULATE"
1896
2288
  : "PLAN";
1897
-
1898
2289
  emit(`LIQUIDATE ${mode}\n`);
1899
2290
  for (const ref of except) {
1900
2291
  const mint = resolveTokenMintForPolicy(slrd, ref);
@@ -1960,6 +2351,15 @@ async function main() {
1960
2351
  emit(
1961
2352
  `ROUTE ${event.index}/${event.total} ${shortKey(event.mint)}\n`,
1962
2353
  );
2354
+ } else if (event.stage === "consolidation-start") {
2355
+ showAction("MOVE", event.index, event.total, event.action);
2356
+ emit(
2357
+ ` consolidate ${event.accounts} non-ATA account(s), raw=${event.amountRaw.toString()} -> ATA\n`,
2358
+ );
2359
+ } else if (event.stage === "consolidation-done") {
2360
+ emit(
2361
+ `MOVED ${event.index}/${event.total} @${event.action.walletName} ${shortKey(event.action.mint)} accounts=${event.accounts} signatures=${event.signatures.length}\n`,
2362
+ );
1963
2363
  } else if (event.stage === "action-start") {
1964
2364
  showAction(
1965
2365
  flags.has("simulate") ? "SIM " : "DO ",
@@ -1983,12 +2383,19 @@ async function main() {
1983
2383
  },
1984
2384
  };
1985
2385
 
1986
- const plan = await planRegistryTokenLiquidation(slrd, options);
1987
- emit(
1988
- `PLAN native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
1989
- `unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
1990
- `keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
1991
- );
2386
+ const emitPlan = (
2387
+ plan: Awaited<ReturnType<typeof planRegistryTokenLiquidation>>,
2388
+ prefix = "PLAN ",
2389
+ ) => {
2390
+ emit(
2391
+ `${prefix} native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
2392
+ `unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
2393
+ `keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
2394
+ );
2395
+ };
2396
+
2397
+ let plan = await planRegistryTokenLiquidation(slrd, options);
2398
+ emitPlan(plan);
1992
2399
 
1993
2400
  if (!flags.has("simulate") && !flags.has("live")) {
1994
2401
  const actionable = plan.actions.filter(
@@ -2043,9 +2450,43 @@ async function main() {
2043
2450
  return;
2044
2451
  }
2045
2452
 
2046
- const results = flags.has("simulate")
2047
- ? await simulateRegistryTokenLiquidation(slrd, plan, options)
2048
- : await executeRegistryTokenLiquidation(slrd, plan, options);
2453
+ let results: Awaited<ReturnType<typeof executeRegistryTokenLiquidation>> =
2454
+ [];
2455
+ let finalPlan = plan;
2456
+
2457
+ if (flags.has("simulate")) {
2458
+ results = await simulateRegistryTokenLiquidation(slrd, plan, options);
2459
+ } else {
2460
+ const maxRounds = Math.max(
2461
+ 1,
2462
+ Math.min(6, int(flags, "rounds", 3) ?? 3),
2463
+ );
2464
+ for (let round = 1; round <= maxRounds; round += 1) {
2465
+ if (round > 1) {
2466
+ emit(`RESCAN liquidation round ${round}/${maxRounds}\n`);
2467
+ emitPlan(plan, "PLAN-R ");
2468
+ }
2469
+ results.push(
2470
+ ...(await executeRegistryTokenLiquidation(slrd, plan, options)),
2471
+ );
2472
+ finalPlan = await planRegistryTokenLiquidation(slrd, options);
2473
+ const retryable = finalPlan.actions.filter(
2474
+ (action) =>
2475
+ action.amountRaw > 0n &&
2476
+ (action.kind === "sell" ||
2477
+ action.kind === "jupiter-sell" ||
2478
+ action.kind === "unwrap-wsol" ||
2479
+ (options.burnUnsellable && action.kind === "skip-unsupported")),
2480
+ );
2481
+ if (!retryable.length) break;
2482
+ if (round < maxRounds) {
2483
+ emit(
2484
+ `RESIDUAL ${retryable.length} positive holding(s) appeared/remain after round ${round}; rescanning and retrying\n`,
2485
+ );
2486
+ plan = finalPlan;
2487
+ }
2488
+ }
2489
+ }
2049
2490
 
2050
2491
  const failed = results.filter((result) => Boolean(result.error)).length;
2051
2492
  const succeeded = results.filter((result) => !result.error);
@@ -2064,22 +2505,79 @@ async function main() {
2064
2505
  const reclaimedRentLamports = succeeded
2065
2506
  .filter((result) => result.action.kind === "close-empty")
2066
2507
  .reduce((sum, result) => sum + (result.action.rentLamports ?? 0n), 0n);
2508
+
2509
+ const residual = flags.has("live")
2510
+ ? finalPlan.actions.filter(
2511
+ (action) =>
2512
+ action.amountRaw > 0n &&
2513
+ action.kind !== "keep-protected" &&
2514
+ action.kind !== "close-empty",
2515
+ )
2516
+ : [];
2517
+
2067
2518
  emit(
2068
2519
  `DONE ok=${succeeded.length} failed=${failed} ` +
2069
2520
  `native-sold=${soldNative} jupiter-sold=${soldJupiter} ` +
2070
2521
  `unwrapped=${unwrapped} closed-empty=${closedEmpty} ` +
2522
+ `residual=${residual.length} ` +
2071
2523
  `reclaimed-rent≈${(Number(reclaimedRentLamports) / 1e9).toFixed(6)} SOL\n`,
2072
2524
  );
2525
+
2526
+ if (residual.length) {
2527
+ for (const action of residual.slice(0, 12)) {
2528
+ const label = action.symbol ?? action.name ?? shortKey(action.mint);
2529
+ emit(
2530
+ `REMAIN @${action.walletName} ${label} ${action.amountUi} ` +
2531
+ `${action.mint}${action.reason ? ` ${action.reason}` : ""}\n`,
2532
+ );
2533
+ }
2534
+ if (residual.length > 12)
2535
+ emit(`REMAIN ... ${residual.length - 12} more holding(s)\n`);
2536
+ emit(
2537
+ `INCOMPLETE positive unprotected token balances remain after the final fresh scan\n`,
2538
+ );
2539
+ process.exitCode = 1;
2540
+ }
2073
2541
  return;
2074
2542
  }
2075
2543
 
2076
- if (command === "sweep" && values[0] === "sol") {
2077
- const destinationInput = flags.get("to") ?? values[1];
2544
+ if (command === "sweep") {
2545
+ const legacySolSyntax = values[0]?.toLowerCase() === "sol";
2546
+ const positionalDestination = legacySolSyntax ? values[1] : values[0];
2547
+ const extraValues = legacySolSyntax ? values.slice(2) : values.slice(1);
2548
+ if (extraValues.length > 0) {
2549
+ throw new Error(
2550
+ `${sweepUsage()}\nUnexpected argument: ${extraValues[0]}`,
2551
+ );
2552
+ }
2553
+
2554
+ const destinationInput = flags.get("to") ?? positionalDestination;
2078
2555
  if (!destinationInput || destinationInput === "true") {
2556
+ throw new Error(sweepUsage());
2557
+ }
2558
+
2559
+ const below = flags.get("below");
2560
+ const maxBalance = flags.get("max-balance-sol");
2561
+ if (below === "true") {
2562
+ throw new Error(`--below requires a SOL amount.\n${sweepUsage()}`);
2563
+ }
2564
+ if (maxBalance === "true") {
2079
2565
  throw new Error(
2080
- "Usage: slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--max-balance-sol <SOL>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]",
2566
+ `--max-balance-sol requires a SOL amount.\n${sweepUsage()}`,
2081
2567
  );
2082
2568
  }
2569
+ if (
2570
+ below != null &&
2571
+ maxBalance != null &&
2572
+ maxBalance !== "true" &&
2573
+ below !== maxBalance
2574
+ ) {
2575
+ throw new Error("Use either --below or --max-balance-sol, not both.");
2576
+ }
2577
+ const maxBalanceSol = below ?? maxBalance;
2578
+ if (!legacySolSyntax && maxBalanceSol == null) {
2579
+ throw new Error(`--below is required for slrd sweep.\n${sweepUsage()}`);
2580
+ }
2083
2581
 
2084
2582
  const destination = resolveDestinationRef(slrd, destinationInput);
2085
2583
 
@@ -2120,16 +2618,26 @@ async function main() {
2120
2618
  "Use either --keep-if-tokens or --keep-if-token, not both",
2121
2619
  );
2122
2620
  }
2621
+ if (flags.has("simulate") && flags.has("live")) {
2622
+ throw new Error("Use either --simulate or --live, not both");
2623
+ }
2123
2624
 
2124
2625
  const sweepMode = flags.has("live")
2125
2626
  ? "LIVE"
2126
2627
  : flags.has("simulate")
2127
2628
  ? "SIMULATE"
2128
2629
  : "PLAN";
2129
- if (!flags.has("json")) {
2130
- emit(`SWEEP ${sweepMode} to=${destination.address}\n`);
2630
+
2631
+ if (!flags.has("json") && (flags.has("live") || flags.has("simulate"))) {
2632
+ const initialDestinationLabel = destination.contactName
2633
+ ? `${destination.contactName} (${destination.address})`
2634
+ : destination.walletName
2635
+ ? `${destination.walletName} (${destination.address})`
2636
+ : destination.address;
2637
+ emit(`SWEEP ${sweepMode} -> ${initialDestinationLabel}\n`);
2638
+ if (maxBalanceSol != null) emit(`Threshold: < ${maxBalanceSol} SOL\n`);
2131
2639
  emit(
2132
- `RPC hard limit ${process.env.SLRD_RPC_MAX_RPS ?? "5"} req/s\n`,
2640
+ "PLAN reading confirmed wallet balances and estimating transfer fee...\n",
2133
2641
  );
2134
2642
  }
2135
2643
 
@@ -2138,7 +2646,7 @@ async function main() {
2138
2646
  excludeGroups,
2139
2647
  excludePrefixes,
2140
2648
  includeWallets: includeWallets.length ? includeWallets : undefined,
2141
- maxBalanceSol: flags.get("max-balance-sol"),
2649
+ maxBalanceSol,
2142
2650
  keepSolByWallet,
2143
2651
  defaultKeepSol: flags.get("default-keep-sol") ?? "0",
2144
2652
  keepSolIfTokens: flags.get("keep-if-tokens"),
@@ -2152,34 +2660,56 @@ async function main() {
2152
2660
  Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
2153
2661
  ),
2154
2662
  delayMs: int(flags, "delay-ms", 0) ?? 0,
2155
- onProgress: flags.has("json")
2156
- ? undefined
2157
- : (event: any) => {
2158
- if (event.stage === "plan-start") {
2159
- emit(`PLAN ${event.wallets} candidate wallets\n`);
2160
- } else if (event.stage === "plan-ready") {
2161
- emit(
2162
- `PLAN ${event.sendWallets}/${event.wallets} will send, ` +
2163
- `${formatRaw(event.totalSendLamports, 9)} SOL total\n`,
2164
- );
2165
- } else if (event.stage === "wallet-start") {
2166
- emit(
2167
- `SEND ${event.index}/${event.total} @${event.row.walletName} ` +
2168
- `${formatRaw(event.row.sendLamports, 9)} SOL\n`,
2169
- );
2170
- } else if (event.stage === "wallet-done") {
2171
- emit(
2172
- `OK ${event.index}/${event.total} @${event.row.walletName}\n`,
2173
- );
2174
- } else if (event.stage === "wallet-error") {
2175
- emit(
2176
- `FAIL ${event.index}/${event.total} @${event.row.walletName} ` +
2177
- `${event.error}\n`,
2178
- );
2663
+ maxAttempts: Math.max(
2664
+ 1,
2665
+ Math.trunc(int(flags, "sweep-attempts", 3) ?? 3),
2666
+ ),
2667
+ confirmationTimeoutMs: Math.max(
2668
+ 1_000,
2669
+ Math.trunc(int(flags, "confirm-timeout-ms", 15_000) ?? 15_000),
2670
+ ),
2671
+ confirmationPollMs: Math.max(
2672
+ 250,
2673
+ Math.trunc(int(flags, "confirm-poll-ms", 750) ?? 750),
2674
+ ),
2675
+ onProgress:
2676
+ !flags.has("json") && flags.has("live")
2677
+ ? (event: any) => {
2678
+ if (event.stage === "wallet-start") {
2679
+ emit(
2680
+ `SEND ${event.index}/${event.total} @${event.row.walletName} ` +
2681
+ `${formatRaw(event.row.sendLamports, 9)} SOL\n`,
2682
+ );
2683
+ } else if (event.stage === "wallet-pending") {
2684
+ emit(
2685
+ `WAIT ${event.index}/${event.total} @${event.row.walletName} ` +
2686
+ `attempt=${event.attempt}/${event.maxAttempts} ` +
2687
+ `${event.reason}\n`,
2688
+ );
2689
+ } else if (event.stage === "wallet-done") {
2690
+ emit(
2691
+ `OK ${event.index}/${event.total} @${event.row.walletName} ` +
2692
+ `remaining=${formatRaw(event.remainingLamports, 9)} SOL` +
2693
+ `${event.receipt?.status ? ` status=${event.receipt.status}` : " state=already-settled"}\n`,
2694
+ );
2695
+ } else if (event.stage === "wallet-error") {
2696
+ emit(
2697
+ `FAIL ${event.index}/${event.total} @${event.row.walletName} ` +
2698
+ `${event.error}\n`,
2699
+ );
2700
+ }
2179
2701
  }
2180
- },
2702
+ : undefined,
2181
2703
  };
2182
2704
  const plan = await planRegistrySolSweep(slrd, options);
2705
+ const sendRows = plan.rows.filter(
2706
+ (row) => row.sendLamports > 0n && !row.skippedReason,
2707
+ );
2708
+ const destinationLabel = destination.contactName
2709
+ ? `${destination.contactName} (${plan.destination})`
2710
+ : destination.walletName
2711
+ ? `${destination.walletName} (${plan.destination})`
2712
+ : plan.destination;
2183
2713
 
2184
2714
  const printablePlan = {
2185
2715
  mode: flags.has("live")
@@ -2193,6 +2723,7 @@ async function main() {
2193
2723
  contact: destination.contactName ?? null,
2194
2724
  wallet: destination.walletName ?? null,
2195
2725
  },
2726
+ belowSol: maxBalanceSol ?? null,
2196
2727
  totalSendSol: formatRaw(plan.totalSendLamports, 9),
2197
2728
  rows: plan.rows.map((row) => ({
2198
2729
  wallet: row.walletName,
@@ -2216,37 +2747,40 @@ async function main() {
2216
2747
  return;
2217
2748
  }
2218
2749
 
2750
+ emit(`SWEEP SOL -> ${destinationLabel}\n`);
2751
+ if (maxBalanceSol != null) emit(`Threshold: < ${maxBalanceSol} SOL\n`);
2219
2752
  emit(
2220
- `SWEEP PLAN to=${plan.destination} ` +
2221
- `send=${formatRaw(plan.totalSendLamports, 9)} SOL ` +
2222
- `wallets=${plan.rows.filter((row) => row.sendLamports > 0n).length}/${plan.rows.length}\n`,
2753
+ `Scanned: ${plan.rows.length} Send: ${sendRows.length} ` +
2754
+ `Skipped: ${plan.rows.length - sendRows.length} ` +
2755
+ `Total: ${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2223
2756
  );
2224
2757
 
2225
2758
  for (const row of plan.rows) {
2226
2759
  if (row.sendLamports <= 0n && !flags.has("show-skipped")) continue;
2227
- emit(
2228
- `${row.sendLamports > 0n ? "SEND" : "SKIP"} ` +
2229
- `@${row.walletName.padEnd(18)} ` +
2230
- `balance=${formatRaw(row.balanceLamports, 9).padStart(12)} ` +
2231
- `keep=${formatRaw(row.keepLamports, 9).padStart(5)} ` +
2232
- `send=${formatRaw(row.sendLamports, 9).padStart(12)}` +
2233
- `${row.reserveReason === "specific-token" ? " keep-token=yes" : ""}` +
2234
- `${row.skippedReason ? ` ${row.skippedReason}` : ""}\n`,
2235
- );
2760
+ if (row.sendLamports > 0n) {
2761
+ emit(
2762
+ ` @${row.walletName} ${formatRaw(row.balanceLamports, 9)} -> ` +
2763
+ `${formatRaw(row.sendLamports, 9)} SOL\n`,
2764
+ );
2765
+ } else {
2766
+ emit(
2767
+ ` @${row.walletName} ${formatRaw(row.balanceLamports, 9)} ` +
2768
+ `SKIP ${row.skippedReason ?? "not-sendable"}\n`,
2769
+ );
2770
+ }
2236
2771
  }
2237
2772
 
2238
- emit(
2239
- `\n${OWL} plan only; no signer was decrypted and nothing was submitted. ` +
2240
- `Use --json for full details, then --simulate or --live.\n`,
2241
- );
2773
+ emit(`\nPreview only. Add --simulate to verify or --live to send.\n`);
2242
2774
  return;
2243
2775
  }
2244
2776
 
2245
- if (flags.has("simulate") && flags.has("live")) {
2246
- throw new Error("Use either --simulate or --live, not both");
2247
- }
2248
-
2249
2777
  if (flags.has("simulate")) {
2778
+ if (!flags.has("json")) {
2779
+ emit(
2780
+ `PLAN eligible=${sendRows.length}/${plan.rows.length} ` +
2781
+ `total=${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2782
+ );
2783
+ }
2250
2784
  const results = await simulateRegistrySolSweep(slrd, plan, options);
2251
2785
  if (flags.has("json")) {
2252
2786
  emit(json({ ...printablePlan, results }) + "\n");
@@ -2254,22 +2788,33 @@ async function main() {
2254
2788
  const failed = results.filter(
2255
2789
  (result) => result.error || result.simulation?.success === false,
2256
2790
  ).length;
2791
+ emit(`SWEEP SIMULATION -> ${destinationLabel}\n`);
2257
2792
  emit(
2258
- `SWEEP SIMULATION attempted=${results.length} ` +
2259
- `ok=${results.length - failed} failed=${failed} ` +
2260
- `to=${plan.destination}\n`,
2793
+ `Attempted: ${results.length} OK: ${results.length - failed} ` +
2794
+ `Failed: ${failed} Total: ${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2261
2795
  );
2262
2796
  for (const result of results) {
2263
2797
  if (!result.error && result.simulation?.success !== false) continue;
2264
2798
  emit(
2265
- `FAIL @${result.row.walletName} ` +
2799
+ ` FAIL @${result.row.walletName} ` +
2266
2800
  `${result.error ?? "simulation failed"}\n`,
2267
2801
  );
2268
2802
  }
2803
+ emit("No transactions submitted.\n");
2269
2804
  }
2270
2805
  return;
2271
2806
  }
2272
2807
 
2808
+ if (!flags.has("json")) {
2809
+ emit(
2810
+ `PLAN eligible=${sendRows.length}/${plan.rows.length} ` +
2811
+ `total=${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2812
+ );
2813
+ if (sendRows.length === 0) {
2814
+ emit("Nothing to sweep.\n");
2815
+ }
2816
+ }
2817
+
2273
2818
  const receipts = await executeRegistrySolSweep(slrd, plan, options);
2274
2819
  if (flags.has("json")) {
2275
2820
  emit(json({ ...printablePlan, receipts }) + "\n");
@@ -2277,14 +2822,14 @@ async function main() {
2277
2822
  const failed = receipts.filter((result) =>
2278
2823
  Boolean(result.error),
2279
2824
  ).length;
2825
+ emit(`SWEEP COMPLETE -> ${destinationLabel}\n`);
2280
2826
  emit(
2281
- `SWEEP LIVE attempted=${receipts.length} ` +
2282
- `ok=${receipts.length - failed} failed=${failed} ` +
2283
- `to=${plan.destination}\n`,
2827
+ `Attempted: ${receipts.length} OK: ${receipts.length - failed} ` +
2828
+ `Failed: ${failed} Planned total: ${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2284
2829
  );
2285
2830
  for (const result of receipts) {
2286
2831
  if (!result.error) continue;
2287
- emit(`FAIL @${result.row.walletName} ${result.error}\n`);
2832
+ emit(` FAIL @${result.row.walletName} ${result.error}\n`);
2288
2833
  }
2289
2834
  }
2290
2835
  return;
@@ -2310,150 +2855,183 @@ async function main() {
2310
2855
  command === "unwrap-wsol" ||
2311
2856
  (command === "unwrap" && values[0] === "wsol")
2312
2857
  ) {
2313
- const targets = targetWallets(
2314
- slrd,
2315
- flags,
2316
- "Usage: slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2317
- );
2858
+ const allWallets = flags.has("all-wallets");
2859
+ const explicitSelectors = [
2860
+ Boolean(flags.get("wallet")),
2861
+ csv(flags.get("wallets")).length > 0,
2862
+ Boolean(flags.get("group")),
2863
+ ].filter(Boolean).length;
2864
+ if (allWallets && explicitSelectors > 0)
2865
+ throw new Error(
2866
+ "Use --all-wallets or a specific wallet selector, not both.",
2867
+ );
2868
+ const targets = allWallets
2869
+ ? {
2870
+ mode: "all-wallets" as const,
2871
+ refs: slrd.wallets.list().map((wallet) => wallet.name),
2872
+ }
2873
+ : targetWallets(
2874
+ slrd,
2875
+ flags,
2876
+ "Usage: slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2877
+ );
2318
2878
  const destination = flags.get("destination");
2319
2879
  if (destination && targets.refs.length !== 1)
2320
2880
  throw new Error(
2321
2881
  "--destination is only supported with a single --wallet unwrap.",
2322
2882
  );
2323
- const unwrapOptions = {
2324
- via: flags.get("sender") ?? "rpc",
2325
- destination,
2326
- skipMissing: flags.has("ignore-missing") || flags.has("skip-missing"),
2327
- skipSimulation: flags.has("skip-simulation"),
2328
- skipPreflight:
2329
- flags.has("skip-preflight") || flags.has("skip-simulation"),
2330
- };
2331
-
2332
- const multiWallet = targets.refs.length > 1;
2883
+ const skipMissing =
2884
+ allWallets || flags.has("ignore-missing") || flags.has("skip-missing");
2885
+ const via = flags.get("sender") ?? "rpc";
2886
+ const skipSimulation = flags.has("skip-simulation");
2887
+ const skipPreflight = flags.has("skip-preflight") || skipSimulation;
2333
2888
  const failFast = flags.has("fail-fast");
2889
+ const results: Array<Record<string, unknown>> = [];
2334
2890
 
2335
- // Multi-wallet/group unwrap is resilient by default. Do not let one
2336
- // undecryptable wallet prevent unrelated wallets from recovering WSOL.
2337
- // With --ignore-missing, probe the WSOL ATA using only the public wallet
2338
- // address BEFORE asking the vault to decrypt the signer.
2339
- if (multiWallet && !failFast) {
2340
- const results: Array<Record<string, unknown>> = [];
2341
-
2342
- for (const walletRef of targets.refs) {
2343
- const resolved = slrd.resolveWallet(walletRef);
2344
- const address = resolved.address.toBase58();
2345
- const label = resolved.row?.name ? `@${resolved.row.name}` : address;
2346
-
2347
- if (unwrapOptions.skipMissing) {
2348
- const wsolAccount = wrappedSolAta(resolved.address);
2349
- const account = await slrd
2350
- .connection()
2351
- .getAccountInfo(wsolAccount, "confirmed");
2352
-
2353
- if (!account) {
2354
- results.push({
2355
- wallet: walletRef,
2356
- address,
2357
- ok: true,
2358
- skipped: "missing-wsol",
2359
- wsolAccount: wsolAccount.toBase58(),
2891
+ for (const walletRef of targets.refs) {
2892
+ const resolved = slrd.resolveWallet(walletRef);
2893
+ const address = resolved.address.toBase58();
2894
+ const label = resolved.row?.name ? `@${resolved.row.name}` : address;
2895
+ try {
2896
+ const accounts = (await slrd.tokenAccounts(address)).filter(
2897
+ (account) => account.mint === NATIVE_SOL_MINT,
2898
+ );
2899
+ if (!accounts.length) {
2900
+ if (!skipMissing && targets.refs.length === 1)
2901
+ throw new Error(`No WSOL token accounts exist for ${address}`);
2902
+ results.push({
2903
+ wallet: walletRef,
2904
+ address,
2905
+ ok: true,
2906
+ skipped: "missing-wsol",
2907
+ });
2908
+ continue;
2909
+ }
2910
+
2911
+ let closed = 0;
2912
+ let unwrappedRaw = 0n;
2913
+ const accountResults: Array<Record<string, unknown>> = [];
2914
+ for (const account of accounts) {
2915
+ if (account.closeAuthority && account.closeAuthority !== address) {
2916
+ const error = `WSOL account ${account.address} has close authority ${account.closeAuthority}`;
2917
+ accountResults.push({
2918
+ account: account.address,
2919
+ ok: false,
2920
+ error,
2360
2921
  });
2922
+ if (failFast) throw new Error(error);
2361
2923
  continue;
2362
2924
  }
2363
- }
2364
-
2365
- try {
2366
- if (flags.has("simulate-only")) {
2367
- const plan = await slrd
2925
+ try {
2926
+ const composer = slrd
2368
2927
  .tx(walletRef)
2369
- .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
2370
- .build();
2371
- const simulation = await slrd.simulatePlan(plan);
2372
- results.push({
2373
- wallet: walletRef,
2374
- address,
2375
- ok: simulation.success,
2376
- mode: "simulation",
2377
- simulation,
2378
- });
2379
- } else {
2380
- const receipt = await slrd.unwrapWsol(walletRef, unwrapOptions);
2381
- results.push({
2382
- wallet: walletRef,
2383
- address,
2384
- ok: true,
2385
- receipt,
2928
+ .closeTokenAccountAddress(
2929
+ account.address,
2930
+ account.tokenProgram,
2931
+ {
2932
+ destination,
2933
+ },
2934
+ );
2935
+ if (flags.has("simulate-only")) {
2936
+ const plan = await composer.build();
2937
+ const simulation = await slrd.simulatePlan(plan);
2938
+ if (!simulation.success) throw new Error("simulation failed");
2939
+ accountResults.push({
2940
+ account: account.address,
2941
+ amountRaw: account.amountRaw,
2942
+ ok: true,
2943
+ mode: "simulation",
2944
+ simulation,
2945
+ });
2946
+ } else {
2947
+ const receipt = await composer.send({
2948
+ via,
2949
+ kind: "unwrap-wsol:account",
2950
+ skipSimulation,
2951
+ skipPreflight,
2952
+ });
2953
+ const { PublicKey } = await import("@solana/web3.js");
2954
+ let closedOnChain = false;
2955
+ for (let attempt = 0; attempt < 6; attempt += 1) {
2956
+ const current = await slrd
2957
+ .connection()
2958
+ .getAccountInfo(
2959
+ new PublicKey(account.address),
2960
+ "confirmed",
2961
+ );
2962
+ if (!current || current.lamports === 0) {
2963
+ closedOnChain = true;
2964
+ break;
2965
+ }
2966
+ if (attempt < 5)
2967
+ await new Promise((resolve) => setTimeout(resolve, 500));
2968
+ }
2969
+ if (!closedOnChain)
2970
+ throw new Error(
2971
+ `WSOL account ${account.address} still exists after close`,
2972
+ );
2973
+ accountResults.push({
2974
+ account: account.address,
2975
+ amountRaw: account.amountRaw,
2976
+ ok: true,
2977
+ receipt,
2978
+ });
2979
+ }
2980
+ closed += 1;
2981
+ unwrappedRaw += BigInt(account.amountRaw);
2982
+ } catch (error) {
2983
+ const message =
2984
+ error instanceof Error ? error.message : String(error);
2985
+ accountResults.push({
2986
+ account: account.address,
2987
+ ok: false,
2988
+ error: message,
2386
2989
  });
2990
+ if (failFast) throw error;
2387
2991
  }
2388
- } catch (error) {
2389
- const message =
2390
- error instanceof Error ? error.message : String(error);
2391
- results.push({
2392
- wallet: walletRef,
2393
- address,
2394
- ok: false,
2395
- error: message,
2396
- });
2397
- process.stderr.write(
2398
- `${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
2399
- );
2400
2992
  }
2993
+ const failedAccounts = accountResults.filter(
2994
+ (row) => row.ok === false,
2995
+ ).length;
2996
+ results.push({
2997
+ wallet: walletRef,
2998
+ address,
2999
+ ok: failedAccounts === 0,
3000
+ closed,
3001
+ failedAccounts,
3002
+ unwrappedRaw,
3003
+ accounts: accountResults,
3004
+ });
3005
+ } catch (error) {
3006
+ const message =
3007
+ error instanceof Error ? error.message : String(error);
3008
+ results.push({
3009
+ wallet: walletRef,
3010
+ address,
3011
+ ok: false,
3012
+ error: message,
3013
+ });
3014
+ process.stderr.write(
3015
+ `${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
3016
+ );
3017
+ if (failFast) throw error;
2401
3018
  }
2402
-
2403
- const succeeded = results.filter(
2404
- (row) => row.ok === true && !row.skipped,
2405
- ).length;
2406
- const skipped = results.filter((row) => Boolean(row.skipped)).length;
2407
- const failed = results.filter((row) => row.ok === false).length;
2408
-
2409
- emit(
2410
- json({
2411
- mode: flags.has("simulate-only")
2412
- ? "wallet-by-wallet-simulation"
2413
- : "wallet-by-wallet",
2414
- target: targets,
2415
- summary: {
2416
- total: results.length,
2417
- succeeded,
2418
- skipped,
2419
- failed,
2420
- },
2421
- results,
2422
- }) + "\n",
2423
- );
2424
- return;
2425
- }
2426
-
2427
- // Single-wallet unwrap remains fail-fast. --fail-fast also preserves the
2428
- // old batch behavior for callers that explicitly want all-or-nothing.
2429
- if (flags.has("simulate-only")) {
2430
- const plans =
2431
- targets.refs.length === 1
2432
- ? [
2433
- await slrd
2434
- .tx(targets.refs[0]!)
2435
- .unwrapWsol({
2436
- destination,
2437
- skipMissing: unwrapOptions.skipMissing,
2438
- })
2439
- .build(),
2440
- ]
2441
- : await slrd
2442
- .composeMany(targets.refs)
2443
- .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
2444
- .build();
2445
- const results = await Promise.all(
2446
- plans.map((plan) => slrd.simulatePlan(plan)),
2447
- );
2448
- emit(json({ mode: "simulation", target: targets, results }) + "\n");
2449
- return;
2450
3019
  }
2451
3020
 
2452
- const receipts =
2453
- targets.refs.length === 1
2454
- ? await slrd.unwrapWsol(targets.refs[0]!, unwrapOptions)
2455
- : await slrd.unwrapWsolMany(targets.refs, unwrapOptions);
2456
- emit(json(receipts) + "\n");
3021
+ const succeeded = results.filter(
3022
+ (row) => row.ok === true && !row.skipped,
3023
+ ).length;
3024
+ const skipped = results.filter((row) => Boolean(row.skipped)).length;
3025
+ const failed = results.filter((row) => row.ok === false).length;
3026
+ emit(
3027
+ json({
3028
+ mode: flags.has("simulate-only") ? "simulation" : "live",
3029
+ target: targets,
3030
+ summary: { total: results.length, succeeded, skipped, failed },
3031
+ results,
3032
+ }) + "\n",
3033
+ );
3034
+ if (failed) process.exitCode = 1;
2457
3035
  return;
2458
3036
  }
2459
3037
  if (command === "rewards") {
@@ -2742,7 +3320,9 @@ async function main() {
2742
3320
  );
2743
3321
  return;
2744
3322
  }
2745
- throw new Error(`Unknown command: ${command}\n\n${help()}`);
3323
+ throw new Error(
3324
+ `Unknown command: ${command}. Run "slrd help" for available commands.`,
3325
+ );
2746
3326
  } finally {
2747
3327
  slrd.close();
2748
3328
  }