@solard/cli 0.2.19 → 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
@@ -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,6 +319,7 @@ 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;
@@ -363,7 +366,9 @@ External contacts (public addresses only; never signing wallets or group members
363
366
  slrd token <token_ca> [name] [--metadata-json <json>]
364
367
  slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
365
368
  slrd token refresh <token|ca>
366
- 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]
367
372
  Durable exact trades + sparse 1s candle materialization from creation
368
373
  slrd token trades <ca> [--from-start] [--min-sol N] [--owner <wallet>] [--side buy|sell] [--limit N] [--json]
369
374
  slrd token analyze <ca> [--top N] [--json]
@@ -380,8 +385,18 @@ Vanity mints
380
385
  slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
381
386
  launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
382
387
 
383
- 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
384
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
385
400
 
386
401
  Metadata and launching
387
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]
@@ -414,7 +429,7 @@ Transfers and consolidation
414
429
  slrd sweep sol ... Compatibility alias; --max-balance-sol remains accepted
415
430
 
416
431
  Token liquidation
417
- 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]
418
433
  Sell all supported tokens unless protected with --except; excluded wallets are not scanned or touched
419
434
 
420
435
  RPC
@@ -435,7 +450,12 @@ Trading
435
450
  slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
436
451
  slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
437
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]
438
- 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]
439
459
  slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
440
460
  slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
441
461
  slrd transfer-many status <stable-id>
@@ -450,10 +470,14 @@ Trading
450
470
  slrd rewards claim <token|ca> --wallet <fee-payer> [--id <stable-claim-id> --basis <reward-basis.json>]
451
471
  slrd rewards claim-status <stable-claim-id>
452
472
 
453
- Scripts (strategies stay outside the kernel)
473
+ Scripts and event strategies
454
474
  slrd scripts List scripts registered in slrd.config.ts
455
475
  slrd run <name-or-path> [script flags...] Execute a script that imports slrd
456
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
457
481
 
458
482
  Meteora DLMM
459
483
  slrd meteora discover --timeframe 30m --sort fee-active-tvl --limit 20
@@ -490,6 +514,9 @@ Watching
490
514
  slrd watch list
491
515
 
492
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
493
520
  slrd history
494
521
  slrd jito tip-accounts [--endpoint <block-engine-url>]
495
522
  slrd alt add <address> [label]
@@ -791,6 +818,24 @@ async function main() {
791
818
  process.exitCode = await runScript(script, scriptArgs);
792
819
  return;
793
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
+ }
794
839
  if (
795
840
  (command === "prepare" && values[0] === "pump") ||
796
841
  (command === "launch" &&
@@ -803,6 +848,11 @@ async function main() {
803
848
  await runPumpExternalDeploymentFromArgs(externalArgs);
804
849
  return;
805
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
+ }
806
856
  if (
807
857
  command === "launch" &&
808
858
  (values[0] === "pump" || values[0] === "pump-token")
@@ -831,6 +881,11 @@ async function main() {
831
881
  });
832
882
  return;
833
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
+ }
834
889
  if (command === "jito" && values[0] === "tip-accounts") {
835
890
  const endpoint = (
836
891
  flags.get("endpoint") ??
@@ -879,6 +934,12 @@ async function main() {
879
934
  return;
880
935
  }
881
936
 
937
+ if (command === "reclaim") {
938
+ const { runReclaimCommand } = await import("./reclaim-command.ts");
939
+ await runReclaimCommand({ slrd, values, flags, emit });
940
+ return;
941
+ }
942
+
882
943
  if (command === "meteora") {
883
944
  await handleMeteoraCommand({ slrd, values, flags, emit });
884
945
  return;
@@ -1312,7 +1373,7 @@ async function main() {
1312
1373
  try {
1313
1374
  const tx = await connection.getTransaction(entry.signature, {
1314
1375
  commitment: "confirmed",
1315
- maxSupportedTransactionVersion: 0,
1376
+ maxSupportedTransactionVersion: 1,
1316
1377
  });
1317
1378
  const fee = tx?.meta?.fee;
1318
1379
  const payer = txAccountKeys(tx)[0] ?? "";
@@ -1376,6 +1437,328 @@ async function main() {
1376
1437
  );
1377
1438
  return;
1378
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
+
1379
1762
  if (command === "sol-flow" || (command === "tx" && values[0] === "flow")) {
1380
1763
  const walletRef = need(flags, "wallet");
1381
1764
  const wallet = slrd.resolveWallet(walletRef);
@@ -1427,7 +1810,7 @@ async function main() {
1427
1810
  try {
1428
1811
  const tx = await connection.getTransaction(entry.signature, {
1429
1812
  commitment: "confirmed",
1430
- maxSupportedTransactionVersion: 0,
1813
+ maxSupportedTransactionVersion: 1,
1431
1814
  });
1432
1815
  if (!tx?.meta) {
1433
1816
  rows.push({
@@ -1903,7 +2286,6 @@ async function main() {
1903
2286
  : flags.has("simulate")
1904
2287
  ? "SIMULATE"
1905
2288
  : "PLAN";
1906
-
1907
2289
  emit(`LIQUIDATE ${mode}\n`);
1908
2290
  for (const ref of except) {
1909
2291
  const mint = resolveTokenMintForPolicy(slrd, ref);
@@ -1969,6 +2351,15 @@ async function main() {
1969
2351
  emit(
1970
2352
  `ROUTE ${event.index}/${event.total} ${shortKey(event.mint)}\n`,
1971
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
+ );
1972
2363
  } else if (event.stage === "action-start") {
1973
2364
  showAction(
1974
2365
  flags.has("simulate") ? "SIM " : "DO ",
@@ -1992,12 +2383,19 @@ async function main() {
1992
2383
  },
1993
2384
  };
1994
2385
 
1995
- const plan = await planRegistryTokenLiquidation(slrd, options);
1996
- emit(
1997
- `PLAN native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
1998
- `unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
1999
- `keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
2000
- );
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);
2001
2399
 
2002
2400
  if (!flags.has("simulate") && !flags.has("live")) {
2003
2401
  const actionable = plan.actions.filter(
@@ -2052,9 +2450,43 @@ async function main() {
2052
2450
  return;
2053
2451
  }
2054
2452
 
2055
- const results = flags.has("simulate")
2056
- ? await simulateRegistryTokenLiquidation(slrd, plan, options)
2057
- : 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
+ }
2058
2490
 
2059
2491
  const failed = results.filter((result) => Boolean(result.error)).length;
2060
2492
  const succeeded = results.filter((result) => !result.error);
@@ -2073,12 +2505,39 @@ async function main() {
2073
2505
  const reclaimedRentLamports = succeeded
2074
2506
  .filter((result) => result.action.kind === "close-empty")
2075
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
+
2076
2518
  emit(
2077
2519
  `DONE ok=${succeeded.length} failed=${failed} ` +
2078
2520
  `native-sold=${soldNative} jupiter-sold=${soldJupiter} ` +
2079
2521
  `unwrapped=${unwrapped} closed-empty=${closedEmpty} ` +
2522
+ `residual=${residual.length} ` +
2080
2523
  `reclaimed-rent≈${(Number(reclaimedRentLamports) / 1e9).toFixed(6)} SOL\n`,
2081
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
+ }
2082
2541
  return;
2083
2542
  }
2084
2543
 
@@ -2163,6 +2622,25 @@ async function main() {
2163
2622
  throw new Error("Use either --simulate or --live, not both");
2164
2623
  }
2165
2624
 
2625
+ const sweepMode = flags.has("live")
2626
+ ? "LIVE"
2627
+ : flags.has("simulate")
2628
+ ? "SIMULATE"
2629
+ : "PLAN";
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`);
2639
+ emit(
2640
+ "PLAN reading confirmed wallet balances and estimating transfer fee...\n",
2641
+ );
2642
+ }
2643
+
2166
2644
  const options = {
2167
2645
  destination: destination.address,
2168
2646
  excludeGroups,
@@ -2182,6 +2660,18 @@ async function main() {
2182
2660
  Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
2183
2661
  ),
2184
2662
  delayMs: int(flags, "delay-ms", 0) ?? 0,
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
+ ),
2185
2675
  onProgress:
2186
2676
  !flags.has("json") && flags.has("live")
2187
2677
  ? (event: any) => {
@@ -2190,9 +2680,17 @@ async function main() {
2190
2680
  `SEND ${event.index}/${event.total} @${event.row.walletName} ` +
2191
2681
  `${formatRaw(event.row.sendLamports, 9)} SOL\n`,
2192
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
+ );
2193
2689
  } else if (event.stage === "wallet-done") {
2194
2690
  emit(
2195
- `OK ${event.index}/${event.total} @${event.row.walletName}\n`,
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`,
2196
2694
  );
2197
2695
  } else if (event.stage === "wallet-error") {
2198
2696
  emit(
@@ -2204,7 +2702,9 @@ async function main() {
2204
2702
  : undefined,
2205
2703
  };
2206
2704
  const plan = await planRegistrySolSweep(slrd, options);
2207
- const sendRows = plan.rows.filter((row) => row.sendLamports > 0n);
2705
+ const sendRows = plan.rows.filter(
2706
+ (row) => row.sendLamports > 0n && !row.skippedReason,
2707
+ );
2208
2708
  const destinationLabel = destination.contactName
2209
2709
  ? `${destination.contactName} (${plan.destination})`
2210
2710
  : destination.walletName
@@ -2275,6 +2775,12 @@ async function main() {
2275
2775
  }
2276
2776
 
2277
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
+ }
2278
2784
  const results = await simulateRegistrySolSweep(slrd, plan, options);
2279
2785
  if (flags.has("json")) {
2280
2786
  emit(json({ ...printablePlan, results }) + "\n");
@@ -2299,6 +2805,16 @@ async function main() {
2299
2805
  return;
2300
2806
  }
2301
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
+
2302
2818
  const receipts = await executeRegistrySolSweep(slrd, plan, options);
2303
2819
  if (flags.has("json")) {
2304
2820
  emit(json({ ...printablePlan, receipts }) + "\n");
@@ -2339,150 +2855,183 @@ async function main() {
2339
2855
  command === "unwrap-wsol" ||
2340
2856
  (command === "unwrap" && values[0] === "wsol")
2341
2857
  ) {
2342
- const targets = targetWallets(
2343
- slrd,
2344
- flags,
2345
- "Usage: slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2346
- );
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
+ );
2347
2878
  const destination = flags.get("destination");
2348
2879
  if (destination && targets.refs.length !== 1)
2349
2880
  throw new Error(
2350
2881
  "--destination is only supported with a single --wallet unwrap.",
2351
2882
  );
2352
- const unwrapOptions = {
2353
- via: flags.get("sender") ?? "rpc",
2354
- destination,
2355
- skipMissing: flags.has("ignore-missing") || flags.has("skip-missing"),
2356
- skipSimulation: flags.has("skip-simulation"),
2357
- skipPreflight:
2358
- flags.has("skip-preflight") || flags.has("skip-simulation"),
2359
- };
2360
-
2361
- 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;
2362
2888
  const failFast = flags.has("fail-fast");
2889
+ const results: Array<Record<string, unknown>> = [];
2363
2890
 
2364
- // Multi-wallet/group unwrap is resilient by default. Do not let one
2365
- // undecryptable wallet prevent unrelated wallets from recovering WSOL.
2366
- // With --ignore-missing, probe the WSOL ATA using only the public wallet
2367
- // address BEFORE asking the vault to decrypt the signer.
2368
- if (multiWallet && !failFast) {
2369
- const results: Array<Record<string, unknown>> = [];
2370
-
2371
- for (const walletRef of targets.refs) {
2372
- const resolved = slrd.resolveWallet(walletRef);
2373
- const address = resolved.address.toBase58();
2374
- const label = resolved.row?.name ? `@${resolved.row.name}` : address;
2375
-
2376
- if (unwrapOptions.skipMissing) {
2377
- const wsolAccount = wrappedSolAta(resolved.address);
2378
- const account = await slrd
2379
- .connection()
2380
- .getAccountInfo(wsolAccount, "confirmed");
2381
-
2382
- if (!account) {
2383
- results.push({
2384
- wallet: walletRef,
2385
- address,
2386
- ok: true,
2387
- skipped: "missing-wsol",
2388
- 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,
2389
2921
  });
2922
+ if (failFast) throw new Error(error);
2390
2923
  continue;
2391
2924
  }
2392
- }
2393
-
2394
- try {
2395
- if (flags.has("simulate-only")) {
2396
- const plan = await slrd
2925
+ try {
2926
+ const composer = slrd
2397
2927
  .tx(walletRef)
2398
- .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
2399
- .build();
2400
- const simulation = await slrd.simulatePlan(plan);
2401
- results.push({
2402
- wallet: walletRef,
2403
- address,
2404
- ok: simulation.success,
2405
- mode: "simulation",
2406
- simulation,
2407
- });
2408
- } else {
2409
- const receipt = await slrd.unwrapWsol(walletRef, unwrapOptions);
2410
- results.push({
2411
- wallet: walletRef,
2412
- address,
2413
- ok: true,
2414
- 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,
2415
2989
  });
2990
+ if (failFast) throw error;
2416
2991
  }
2417
- } catch (error) {
2418
- const message =
2419
- error instanceof Error ? error.message : String(error);
2420
- results.push({
2421
- wallet: walletRef,
2422
- address,
2423
- ok: false,
2424
- error: message,
2425
- });
2426
- process.stderr.write(
2427
- `${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
2428
- );
2429
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;
2430
3018
  }
2431
-
2432
- const succeeded = results.filter(
2433
- (row) => row.ok === true && !row.skipped,
2434
- ).length;
2435
- const skipped = results.filter((row) => Boolean(row.skipped)).length;
2436
- const failed = results.filter((row) => row.ok === false).length;
2437
-
2438
- emit(
2439
- json({
2440
- mode: flags.has("simulate-only")
2441
- ? "wallet-by-wallet-simulation"
2442
- : "wallet-by-wallet",
2443
- target: targets,
2444
- summary: {
2445
- total: results.length,
2446
- succeeded,
2447
- skipped,
2448
- failed,
2449
- },
2450
- results,
2451
- }) + "\n",
2452
- );
2453
- return;
2454
- }
2455
-
2456
- // Single-wallet unwrap remains fail-fast. --fail-fast also preserves the
2457
- // old batch behavior for callers that explicitly want all-or-nothing.
2458
- if (flags.has("simulate-only")) {
2459
- const plans =
2460
- targets.refs.length === 1
2461
- ? [
2462
- await slrd
2463
- .tx(targets.refs[0]!)
2464
- .unwrapWsol({
2465
- destination,
2466
- skipMissing: unwrapOptions.skipMissing,
2467
- })
2468
- .build(),
2469
- ]
2470
- : await slrd
2471
- .composeMany(targets.refs)
2472
- .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
2473
- .build();
2474
- const results = await Promise.all(
2475
- plans.map((plan) => slrd.simulatePlan(plan)),
2476
- );
2477
- emit(json({ mode: "simulation", target: targets, results }) + "\n");
2478
- return;
2479
3019
  }
2480
3020
 
2481
- const receipts =
2482
- targets.refs.length === 1
2483
- ? await slrd.unwrapWsol(targets.refs[0]!, unwrapOptions)
2484
- : await slrd.unwrapWsolMany(targets.refs, unwrapOptions);
2485
- 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;
2486
3035
  return;
2487
3036
  }
2488
3037
  if (command === "rewards") {