@solard/cli 0.2.19 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 @@ 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
+ Launch discovery
389
+ slrd launch watch [--venue pump,raydium-launchlab] [--min-mcap 5000] [--ath-step-pct 20] [--track-ttl 30m] [--show-new] [--prices] [--json]
390
+ Subscribe directly to launch/trade logs; ignore Pump Mayhem and notify on first $5k cross and each +20% notified ATH step
391
+
392
+ Pump discovery
384
393
  slrd pump pairs [--json] Read Pump-supported quote mints from the on-chain Global account
394
+ slrd pump watch [--min-mcap 5000] [--ath-step-pct 20] [--track-ttl 30m] [--show-new] [--prices] [--json]
395
+ Compatibility alias for: slrd launch watch --venue pump
385
396
 
386
397
  Metadata and launching
387
398
  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 +425,7 @@ Transfers and consolidation
414
425
  slrd sweep sol ... Compatibility alias; --max-balance-sol remains accepted
415
426
 
416
427
  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]
428
+ 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
429
  Sell all supported tokens unless protected with --except; excluded wallets are not scanned or touched
419
430
 
420
431
  RPC
@@ -435,7 +446,12 @@ Trading
435
446
  slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
436
447
  slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
437
448
  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]
449
+ slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
450
+ Close every owned WSOL token account, including non-ATA WSOL accounts
451
+ slrd reclaim inspect --all-wallets List Loader-v3 buffers/programs controlled by stored wallets
452
+ slrd reclaim buffers --all-wallets [--simulate | --live]
453
+ slrd reclaim programs --all-wallets [--simulate | --live --confirm-program-close]
454
+ slrd reclaim all --all-wallets [--simulate | --live --confirm-program-close]
439
455
  slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
440
456
  slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
441
457
  slrd transfer-many status <stable-id>
@@ -450,10 +466,14 @@ Trading
450
466
  slrd rewards claim <token|ca> --wallet <fee-payer> [--id <stable-claim-id> --basis <reward-basis.json>]
451
467
  slrd rewards claim-status <stable-claim-id>
452
468
 
453
- Scripts (strategies stay outside the kernel)
469
+ Scripts and event strategies
454
470
  slrd scripts List scripts registered in slrd.config.ts
455
471
  slrd run <name-or-path> [script flags...] Execute a script that imports slrd
456
472
  slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
473
+ slrd strategy run <file.ts> --token <mint|alias> --wallet <wallet> [--params <json>] [--live]
474
+ Run an onPrice strategy from direct token trade subscriptions; wallet/execution state remains isolated per agent
475
+ slrd strategy backtest <file.ts> --token <mint> [--params <json>] [--capital-sol 1] [--price-sample 5s] [--sol-usd N] [--json]
476
+ Replay the same strategy file over durable token history
457
477
 
458
478
  Meteora DLMM
459
479
  slrd meteora discover --timeframe 30m --sort fee-active-tvl --limit 20
@@ -490,6 +510,9 @@ Watching
490
510
  slrd watch list
491
511
 
492
512
  Transactions and ALTs
513
+ slrd tx stream --wallet <wallet|address> [--finalized] [--jsonl]
514
+ slrd tx stream --wallets <a,b,c> [--finalized] [--jsonl]
515
+ Stream every transaction mentioning the selected wallets until Ctrl+C
493
516
  slrd history
494
517
  slrd jito tip-accounts [--endpoint <block-engine-url>]
495
518
  slrd alt add <address> [label]
@@ -791,6 +814,18 @@ async function main() {
791
814
  process.exitCode = await runScript(script, scriptArgs);
792
815
  return;
793
816
  }
817
+ if (command === "strategy" && values[0] === "backtest") {
818
+ const { runStrategyBacktestCommand } =
819
+ await import("./strategy-backtest-command.ts");
820
+ await runStrategyBacktestCommand({ values, flags, emit });
821
+ return;
822
+ }
823
+ if (command === "strategy" && values[0] === "run") {
824
+ const { runTradeStrategyCommand } =
825
+ await import("./trade-strategy-command.ts");
826
+ await runTradeStrategyCommand({ values, flags, emit });
827
+ return;
828
+ }
794
829
  if (
795
830
  (command === "prepare" && values[0] === "pump") ||
796
831
  (command === "launch" &&
@@ -803,6 +838,11 @@ async function main() {
803
838
  await runPumpExternalDeploymentFromArgs(externalArgs);
804
839
  return;
805
840
  }
841
+ if (command === "launch" && values[0] === "watch") {
842
+ const { runLaunchWatchCommand } = await import("./launch-watch-command.ts");
843
+ await runLaunchWatchCommand({ flags, emit });
844
+ return;
845
+ }
806
846
  if (
807
847
  command === "launch" &&
808
848
  (values[0] === "pump" || values[0] === "pump-token")
@@ -831,6 +871,11 @@ async function main() {
831
871
  });
832
872
  return;
833
873
  }
874
+ if (command === "pump" && values[0] === "watch") {
875
+ const { runPumpWatchCommand } = await import("./pump-watch-command.ts");
876
+ await runPumpWatchCommand({ flags, emit });
877
+ return;
878
+ }
834
879
  if (command === "jito" && values[0] === "tip-accounts") {
835
880
  const endpoint = (
836
881
  flags.get("endpoint") ??
@@ -879,6 +924,12 @@ async function main() {
879
924
  return;
880
925
  }
881
926
 
927
+ if (command === "reclaim") {
928
+ const { runReclaimCommand } = await import("./reclaim-command.ts");
929
+ await runReclaimCommand({ slrd, values, flags, emit });
930
+ return;
931
+ }
932
+
882
933
  if (command === "meteora") {
883
934
  await handleMeteoraCommand({ slrd, values, flags, emit });
884
935
  return;
@@ -1312,7 +1363,7 @@ async function main() {
1312
1363
  try {
1313
1364
  const tx = await connection.getTransaction(entry.signature, {
1314
1365
  commitment: "confirmed",
1315
- maxSupportedTransactionVersion: 0,
1366
+ maxSupportedTransactionVersion: 1,
1316
1367
  });
1317
1368
  const fee = tx?.meta?.fee;
1318
1369
  const payer = txAccountKeys(tx)[0] ?? "";
@@ -1376,6 +1427,328 @@ async function main() {
1376
1427
  );
1377
1428
  return;
1378
1429
  }
1430
+ if (command === "tx" && values[0] === "stream") {
1431
+ const walletFlag = flags.get("wallet");
1432
+ const walletRefs = [
1433
+ ...(walletFlag && walletFlag !== "true" ? [walletFlag] : []),
1434
+ ...csv(flags.get("wallets")),
1435
+ ];
1436
+ if (walletRefs.length === 0) {
1437
+ throw new Error(
1438
+ "Usage: slrd tx stream --wallet <wallet|address> | --wallets <a,b,c> [--finalized] [--jsonl]",
1439
+ );
1440
+ }
1441
+
1442
+ const { PublicKey } = await import("@solana/web3.js");
1443
+ const connection = slrd.connection();
1444
+ const commitment = flags.has("finalized") ? "finalized" : "confirmed";
1445
+ const enrichAttempts = Math.max(
1446
+ 1,
1447
+ Math.min(20, Math.trunc(int(flags, "enrich-attempts", 8) ?? 8)),
1448
+ );
1449
+ const enrichDelayMs = Math.max(
1450
+ 50,
1451
+ Math.trunc(int(flags, "enrich-delay-ms", 250) ?? 250),
1452
+ );
1453
+
1454
+ type StreamWallet = {
1455
+ ref: string;
1456
+ name: string | null;
1457
+ address: string;
1458
+ publicKey: InstanceType<typeof PublicKey>;
1459
+ };
1460
+
1461
+ const byAddress = new Map<string, StreamWallet>();
1462
+ for (const ref of walletRefs) {
1463
+ let name: string | null = null;
1464
+ let publicKey: InstanceType<typeof PublicKey>;
1465
+ try {
1466
+ const resolved = slrd.resolveWallet(ref);
1467
+ publicKey = resolved.address;
1468
+ name = resolved.row?.name ?? null;
1469
+ } catch {
1470
+ try {
1471
+ publicKey = new PublicKey(ref);
1472
+ } catch {
1473
+ throw new Error(`Unknown wallet or invalid Solana address: ${ref}`);
1474
+ }
1475
+ }
1476
+ const address = publicKey.toBase58();
1477
+ if (!byAddress.has(address)) {
1478
+ byAddress.set(address, { ref, name, address, publicKey });
1479
+ }
1480
+ }
1481
+ const watched = [...byAddress.values()];
1482
+ const watchedAddresses = new Set(watched.map((row) => row.address));
1483
+ const labelFor = (row: StreamWallet) =>
1484
+ row.name ? `@${row.name}` : shortKey(row.address);
1485
+
1486
+ type PendingSignature = {
1487
+ signature: string;
1488
+ observedSlot: number | null;
1489
+ };
1490
+
1491
+ const queue: PendingSignature[] = [];
1492
+ const queued = new Set<string>();
1493
+ const seen = new Set<string>();
1494
+ const seenOrder: string[] = [];
1495
+ const listenerIds: number[] = [];
1496
+ let processing = false;
1497
+ let stopped = false;
1498
+ let stopResolve: (() => void) | null = null;
1499
+
1500
+ const rememberSeen = (signature: string) => {
1501
+ if (seen.has(signature)) return;
1502
+ seen.add(signature);
1503
+ seenOrder.push(signature);
1504
+ while (seenOrder.length > 20_000) {
1505
+ const oldest = seenOrder.shift();
1506
+ if (oldest) seen.delete(oldest);
1507
+ }
1508
+ };
1509
+
1510
+ const transactionWithRetry = async (signature: string) => {
1511
+ for (let attempt = 1; attempt <= enrichAttempts; attempt += 1) {
1512
+ const tx = await connection.getTransaction(signature, {
1513
+ commitment,
1514
+ maxSupportedTransactionVersion: 1,
1515
+ });
1516
+ if (tx) return tx;
1517
+ if (attempt < enrichAttempts) {
1518
+ await new Promise((resolve) => setTimeout(resolve, enrichDelayMs));
1519
+ }
1520
+ }
1521
+ return null;
1522
+ };
1523
+
1524
+ const printTransaction = async (
1525
+ item: PendingSignature,
1526
+ ): Promise<void> => {
1527
+ const tx = await transactionWithRetry(item.signature);
1528
+ if (!tx?.meta) {
1529
+ const unavailable = {
1530
+ type: "transaction",
1531
+ signature: item.signature,
1532
+ slot: item.observedSlot,
1533
+ commitment,
1534
+ status: "unavailable",
1535
+ wallets: [],
1536
+ };
1537
+ if (flags.has("jsonl")) emit(JSON.stringify(unavailable) + "\n");
1538
+ else
1539
+ emit(
1540
+ `${new Date().toISOString()} UNAVAILABLE ${item.signature} slot=${item.observedSlot ?? "?"}\n`,
1541
+ );
1542
+ return;
1543
+ }
1544
+
1545
+ const keys = txAccountKeys(tx);
1546
+ const keySet = new Set(keys);
1547
+ const touched = watched.filter((row) => keySet.has(row.address));
1548
+ if (touched.length === 0) return;
1549
+
1550
+ const feeLamports =
1551
+ typeof tx.meta.fee === "number" && Number.isFinite(tx.meta.fee)
1552
+ ? BigInt(Math.trunc(tx.meta.fee))
1553
+ : 0n;
1554
+ const payer = keys[0] ?? null;
1555
+ const status = tx.meta.err ? "failed" : "confirmed";
1556
+ const at =
1557
+ tx.blockTime == null
1558
+ ? new Date().toISOString()
1559
+ : new Date(tx.blockTime * 1_000).toISOString();
1560
+
1561
+ const walletRows = touched.map((wallet) => {
1562
+ const walletIndex = keys.indexOf(wallet.address);
1563
+ let nativeDeltaLamports: bigint | null = null;
1564
+ if (
1565
+ walletIndex >= 0 &&
1566
+ typeof tx.meta!.preBalances?.[walletIndex] === "number" &&
1567
+ typeof tx.meta!.postBalances?.[walletIndex] === "number"
1568
+ ) {
1569
+ nativeDeltaLamports =
1570
+ BigInt(Math.trunc(tx.meta!.postBalances[walletIndex]!)) -
1571
+ BigInt(Math.trunc(tx.meta!.preBalances[walletIndex]!));
1572
+ }
1573
+
1574
+ const tokenDeltas = [...txWalletTokenBalances(tx, wallet.address)]
1575
+ .map(([mint, balance]) => ({
1576
+ mint,
1577
+ decimals: balance.decimals,
1578
+ deltaRaw: balance.post - balance.pre,
1579
+ }))
1580
+ .filter((row) => row.deltaRaw !== 0n)
1581
+ .sort((left, right) => {
1582
+ const leftAbs =
1583
+ left.deltaRaw < 0n ? -left.deltaRaw : left.deltaRaw;
1584
+ const rightAbs =
1585
+ right.deltaRaw < 0n ? -right.deltaRaw : right.deltaRaw;
1586
+ return leftAbs === rightAbs
1587
+ ? left.mint.localeCompare(right.mint)
1588
+ : leftAbs > rightAbs
1589
+ ? -1
1590
+ : 1;
1591
+ });
1592
+
1593
+ return {
1594
+ ref: wallet.ref,
1595
+ name: wallet.name,
1596
+ address: wallet.address,
1597
+ nativeDeltaLamports,
1598
+ feePaidLamports: payer === wallet.address ? feeLamports : 0n,
1599
+ tokenDeltas,
1600
+ };
1601
+ });
1602
+
1603
+ if (flags.has("jsonl")) {
1604
+ emit(
1605
+ JSON.stringify(
1606
+ {
1607
+ type: "transaction",
1608
+ at,
1609
+ signature: item.signature,
1610
+ slot: tx.slot ?? item.observedSlot,
1611
+ commitment,
1612
+ status,
1613
+ error: tx.meta.err ?? null,
1614
+ feeLamports,
1615
+ feePayer: payer,
1616
+ wallets: walletRows,
1617
+ },
1618
+ (_, value) =>
1619
+ typeof value === "bigint" ? value.toString() : value,
1620
+ ) + "\n",
1621
+ );
1622
+ return;
1623
+ }
1624
+
1625
+ emit(
1626
+ `\n${at} ${status.toUpperCase()} slot=${tx.slot ?? item.observedSlot ?? "?"}\n` +
1627
+ `SIG ${item.signature}\n` +
1628
+ `FEE ${formatRaw(feeLamports, 9)} SOL payer=${payer ?? "?"}\n`,
1629
+ );
1630
+
1631
+ for (const row of walletRows) {
1632
+ const wallet = byAddress.get(row.address)!;
1633
+ emit(` ${labelFor(wallet)} ${row.address}\n`);
1634
+ emit(
1635
+ ` SOL ${
1636
+ row.nativeDeltaLamports == null
1637
+ ? "n/a"
1638
+ : `${formatSignedRaw(row.nativeDeltaLamports, 9)} SOL`
1639
+ }${
1640
+ row.feePaidLamports > 0n
1641
+ ? ` (paid fee ${formatRaw(row.feePaidLamports, 9)} SOL)`
1642
+ : ""
1643
+ }\n`,
1644
+ );
1645
+ if (row.tokenDeltas.length === 0) {
1646
+ emit(" TOKEN no wallet-owned SPL balance delta\n");
1647
+ } else {
1648
+ for (const delta of row.tokenDeltas) {
1649
+ const tokenLabel =
1650
+ delta.mint === NATIVE_SOL_MINT
1651
+ ? "WSOL"
1652
+ : delta.mint === CANONICAL_USDC_MINT
1653
+ ? "USDC"
1654
+ : shortKey(delta.mint);
1655
+ emit(
1656
+ ` TOKEN ${formatSignedRaw(delta.deltaRaw, delta.decimals)} ${tokenLabel} ${delta.mint}\n`,
1657
+ );
1658
+ }
1659
+ }
1660
+ }
1661
+
1662
+ if (tx.meta.err) {
1663
+ emit(` ERROR ${JSON.stringify(tx.meta.err)}\n`);
1664
+ }
1665
+ };
1666
+
1667
+ const drain = async (): Promise<void> => {
1668
+ if (processing) return;
1669
+ processing = true;
1670
+ try {
1671
+ while (!stopped && queue.length > 0) {
1672
+ const item = queue.shift()!;
1673
+ queued.delete(item.signature);
1674
+ if (seen.has(item.signature)) continue;
1675
+ try {
1676
+ await printTransaction(item);
1677
+ } catch (error) {
1678
+ const message =
1679
+ error instanceof Error ? error.message : String(error);
1680
+ if (flags.has("jsonl")) {
1681
+ emit(
1682
+ JSON.stringify({
1683
+ type: "stream-error",
1684
+ signature: item.signature,
1685
+ error: message,
1686
+ }) + "\n",
1687
+ );
1688
+ } else {
1689
+ process.stderr.write(
1690
+ `${OWL} tx stream ${item.signature}: ${message}\n`,
1691
+ );
1692
+ }
1693
+ } finally {
1694
+ rememberSeen(item.signature);
1695
+ }
1696
+ }
1697
+ } finally {
1698
+ processing = false;
1699
+ if (!stopped && queue.length > 0) void drain();
1700
+ }
1701
+ };
1702
+
1703
+ const enqueue = (signature: string, observedSlot: number | null) => {
1704
+ if (stopped || seen.has(signature) || queued.has(signature)) return;
1705
+ queued.add(signature);
1706
+ queue.push({ signature, observedSlot });
1707
+ void drain();
1708
+ };
1709
+
1710
+ if (!flags.has("jsonl")) {
1711
+ emit(
1712
+ `TX STREAM wallets=${watched.length} commitment=${commitment} Ctrl+C to stop\n`,
1713
+ );
1714
+ for (const wallet of watched) {
1715
+ emit(` ${labelFor(wallet)} ${wallet.address}\n`);
1716
+ }
1717
+ emit("\n");
1718
+ }
1719
+
1720
+ for (const wallet of watched) {
1721
+ const id = connection.onLogs(
1722
+ wallet.publicKey,
1723
+ (logs, context) => {
1724
+ // Stream failed transactions too. The transaction metadata below
1725
+ // is the authoritative status/error source.
1726
+ enqueue(logs.signature, context?.slot ?? null);
1727
+ },
1728
+ commitment,
1729
+ );
1730
+ listenerIds.push(id);
1731
+ }
1732
+
1733
+ await new Promise<void>((resolve) => {
1734
+ stopResolve = resolve;
1735
+ const stop = () => {
1736
+ if (stopped) return;
1737
+ stopped = true;
1738
+ resolve();
1739
+ };
1740
+ process.once("SIGINT", stop);
1741
+ process.once("SIGTERM", stop);
1742
+ });
1743
+
1744
+ await Promise.allSettled(
1745
+ listenerIds.map((id) => connection.removeOnLogsListener(id)),
1746
+ );
1747
+
1748
+ if (!flags.has("jsonl")) emit("TX STREAM STOPPED\n");
1749
+ return;
1750
+ }
1751
+
1379
1752
  if (command === "sol-flow" || (command === "tx" && values[0] === "flow")) {
1380
1753
  const walletRef = need(flags, "wallet");
1381
1754
  const wallet = slrd.resolveWallet(walletRef);
@@ -1427,7 +1800,7 @@ async function main() {
1427
1800
  try {
1428
1801
  const tx = await connection.getTransaction(entry.signature, {
1429
1802
  commitment: "confirmed",
1430
- maxSupportedTransactionVersion: 0,
1803
+ maxSupportedTransactionVersion: 1,
1431
1804
  });
1432
1805
  if (!tx?.meta) {
1433
1806
  rows.push({
@@ -1903,7 +2276,6 @@ async function main() {
1903
2276
  : flags.has("simulate")
1904
2277
  ? "SIMULATE"
1905
2278
  : "PLAN";
1906
-
1907
2279
  emit(`LIQUIDATE ${mode}\n`);
1908
2280
  for (const ref of except) {
1909
2281
  const mint = resolveTokenMintForPolicy(slrd, ref);
@@ -1969,6 +2341,15 @@ async function main() {
1969
2341
  emit(
1970
2342
  `ROUTE ${event.index}/${event.total} ${shortKey(event.mint)}\n`,
1971
2343
  );
2344
+ } else if (event.stage === "consolidation-start") {
2345
+ showAction("MOVE", event.index, event.total, event.action);
2346
+ emit(
2347
+ ` consolidate ${event.accounts} non-ATA account(s), raw=${event.amountRaw.toString()} -> ATA\n`,
2348
+ );
2349
+ } else if (event.stage === "consolidation-done") {
2350
+ emit(
2351
+ `MOVED ${event.index}/${event.total} @${event.action.walletName} ${shortKey(event.action.mint)} accounts=${event.accounts} signatures=${event.signatures.length}\n`,
2352
+ );
1972
2353
  } else if (event.stage === "action-start") {
1973
2354
  showAction(
1974
2355
  flags.has("simulate") ? "SIM " : "DO ",
@@ -1992,12 +2373,19 @@ async function main() {
1992
2373
  },
1993
2374
  };
1994
2375
 
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
- );
2376
+ const emitPlan = (
2377
+ plan: Awaited<ReturnType<typeof planRegistryTokenLiquidation>>,
2378
+ prefix = "PLAN ",
2379
+ ) => {
2380
+ emit(
2381
+ `${prefix} native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
2382
+ `unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
2383
+ `keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
2384
+ );
2385
+ };
2386
+
2387
+ let plan = await planRegistryTokenLiquidation(slrd, options);
2388
+ emitPlan(plan);
2001
2389
 
2002
2390
  if (!flags.has("simulate") && !flags.has("live")) {
2003
2391
  const actionable = plan.actions.filter(
@@ -2052,9 +2440,43 @@ async function main() {
2052
2440
  return;
2053
2441
  }
2054
2442
 
2055
- const results = flags.has("simulate")
2056
- ? await simulateRegistryTokenLiquidation(slrd, plan, options)
2057
- : await executeRegistryTokenLiquidation(slrd, plan, options);
2443
+ let results: Awaited<ReturnType<typeof executeRegistryTokenLiquidation>> =
2444
+ [];
2445
+ let finalPlan = plan;
2446
+
2447
+ if (flags.has("simulate")) {
2448
+ results = await simulateRegistryTokenLiquidation(slrd, plan, options);
2449
+ } else {
2450
+ const maxRounds = Math.max(
2451
+ 1,
2452
+ Math.min(6, int(flags, "rounds", 3) ?? 3),
2453
+ );
2454
+ for (let round = 1; round <= maxRounds; round += 1) {
2455
+ if (round > 1) {
2456
+ emit(`RESCAN liquidation round ${round}/${maxRounds}\n`);
2457
+ emitPlan(plan, "PLAN-R ");
2458
+ }
2459
+ results.push(
2460
+ ...(await executeRegistryTokenLiquidation(slrd, plan, options)),
2461
+ );
2462
+ finalPlan = await planRegistryTokenLiquidation(slrd, options);
2463
+ const retryable = finalPlan.actions.filter(
2464
+ (action) =>
2465
+ action.amountRaw > 0n &&
2466
+ (action.kind === "sell" ||
2467
+ action.kind === "jupiter-sell" ||
2468
+ action.kind === "unwrap-wsol" ||
2469
+ (options.burnUnsellable && action.kind === "skip-unsupported")),
2470
+ );
2471
+ if (!retryable.length) break;
2472
+ if (round < maxRounds) {
2473
+ emit(
2474
+ `RESIDUAL ${retryable.length} positive holding(s) appeared/remain after round ${round}; rescanning and retrying\n`,
2475
+ );
2476
+ plan = finalPlan;
2477
+ }
2478
+ }
2479
+ }
2058
2480
 
2059
2481
  const failed = results.filter((result) => Boolean(result.error)).length;
2060
2482
  const succeeded = results.filter((result) => !result.error);
@@ -2073,12 +2495,39 @@ async function main() {
2073
2495
  const reclaimedRentLamports = succeeded
2074
2496
  .filter((result) => result.action.kind === "close-empty")
2075
2497
  .reduce((sum, result) => sum + (result.action.rentLamports ?? 0n), 0n);
2498
+
2499
+ const residual = flags.has("live")
2500
+ ? finalPlan.actions.filter(
2501
+ (action) =>
2502
+ action.amountRaw > 0n &&
2503
+ action.kind !== "keep-protected" &&
2504
+ action.kind !== "close-empty",
2505
+ )
2506
+ : [];
2507
+
2076
2508
  emit(
2077
2509
  `DONE ok=${succeeded.length} failed=${failed} ` +
2078
2510
  `native-sold=${soldNative} jupiter-sold=${soldJupiter} ` +
2079
2511
  `unwrapped=${unwrapped} closed-empty=${closedEmpty} ` +
2512
+ `residual=${residual.length} ` +
2080
2513
  `reclaimed-rent≈${(Number(reclaimedRentLamports) / 1e9).toFixed(6)} SOL\n`,
2081
2514
  );
2515
+
2516
+ if (residual.length) {
2517
+ for (const action of residual.slice(0, 12)) {
2518
+ const label = action.symbol ?? action.name ?? shortKey(action.mint);
2519
+ emit(
2520
+ `REMAIN @${action.walletName} ${label} ${action.amountUi} ` +
2521
+ `${action.mint}${action.reason ? ` ${action.reason}` : ""}\n`,
2522
+ );
2523
+ }
2524
+ if (residual.length > 12)
2525
+ emit(`REMAIN ... ${residual.length - 12} more holding(s)\n`);
2526
+ emit(
2527
+ `INCOMPLETE positive unprotected token balances remain after the final fresh scan\n`,
2528
+ );
2529
+ process.exitCode = 1;
2530
+ }
2082
2531
  return;
2083
2532
  }
2084
2533
 
@@ -2163,6 +2612,25 @@ async function main() {
2163
2612
  throw new Error("Use either --simulate or --live, not both");
2164
2613
  }
2165
2614
 
2615
+ const sweepMode = flags.has("live")
2616
+ ? "LIVE"
2617
+ : flags.has("simulate")
2618
+ ? "SIMULATE"
2619
+ : "PLAN";
2620
+
2621
+ if (!flags.has("json") && (flags.has("live") || flags.has("simulate"))) {
2622
+ const initialDestinationLabel = destination.contactName
2623
+ ? `${destination.contactName} (${destination.address})`
2624
+ : destination.walletName
2625
+ ? `${destination.walletName} (${destination.address})`
2626
+ : destination.address;
2627
+ emit(`SWEEP ${sweepMode} -> ${initialDestinationLabel}\n`);
2628
+ if (maxBalanceSol != null) emit(`Threshold: < ${maxBalanceSol} SOL\n`);
2629
+ emit(
2630
+ "PLAN reading confirmed wallet balances and estimating transfer fee...\n",
2631
+ );
2632
+ }
2633
+
2166
2634
  const options = {
2167
2635
  destination: destination.address,
2168
2636
  excludeGroups,
@@ -2182,6 +2650,18 @@ async function main() {
2182
2650
  Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
2183
2651
  ),
2184
2652
  delayMs: int(flags, "delay-ms", 0) ?? 0,
2653
+ maxAttempts: Math.max(
2654
+ 1,
2655
+ Math.trunc(int(flags, "sweep-attempts", 3) ?? 3),
2656
+ ),
2657
+ confirmationTimeoutMs: Math.max(
2658
+ 1_000,
2659
+ Math.trunc(int(flags, "confirm-timeout-ms", 15_000) ?? 15_000),
2660
+ ),
2661
+ confirmationPollMs: Math.max(
2662
+ 250,
2663
+ Math.trunc(int(flags, "confirm-poll-ms", 750) ?? 750),
2664
+ ),
2185
2665
  onProgress:
2186
2666
  !flags.has("json") && flags.has("live")
2187
2667
  ? (event: any) => {
@@ -2190,9 +2670,17 @@ async function main() {
2190
2670
  `SEND ${event.index}/${event.total} @${event.row.walletName} ` +
2191
2671
  `${formatRaw(event.row.sendLamports, 9)} SOL\n`,
2192
2672
  );
2673
+ } else if (event.stage === "wallet-pending") {
2674
+ emit(
2675
+ `WAIT ${event.index}/${event.total} @${event.row.walletName} ` +
2676
+ `attempt=${event.attempt}/${event.maxAttempts} ` +
2677
+ `${event.reason}\n`,
2678
+ );
2193
2679
  } else if (event.stage === "wallet-done") {
2194
2680
  emit(
2195
- `OK ${event.index}/${event.total} @${event.row.walletName}\n`,
2681
+ `OK ${event.index}/${event.total} @${event.row.walletName} ` +
2682
+ `remaining=${formatRaw(event.remainingLamports, 9)} SOL` +
2683
+ `${event.receipt?.status ? ` status=${event.receipt.status}` : " state=already-settled"}\n`,
2196
2684
  );
2197
2685
  } else if (event.stage === "wallet-error") {
2198
2686
  emit(
@@ -2204,7 +2692,9 @@ async function main() {
2204
2692
  : undefined,
2205
2693
  };
2206
2694
  const plan = await planRegistrySolSweep(slrd, options);
2207
- const sendRows = plan.rows.filter((row) => row.sendLamports > 0n);
2695
+ const sendRows = plan.rows.filter(
2696
+ (row) => row.sendLamports > 0n && !row.skippedReason,
2697
+ );
2208
2698
  const destinationLabel = destination.contactName
2209
2699
  ? `${destination.contactName} (${plan.destination})`
2210
2700
  : destination.walletName
@@ -2275,6 +2765,12 @@ async function main() {
2275
2765
  }
2276
2766
 
2277
2767
  if (flags.has("simulate")) {
2768
+ if (!flags.has("json")) {
2769
+ emit(
2770
+ `PLAN eligible=${sendRows.length}/${plan.rows.length} ` +
2771
+ `total=${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2772
+ );
2773
+ }
2278
2774
  const results = await simulateRegistrySolSweep(slrd, plan, options);
2279
2775
  if (flags.has("json")) {
2280
2776
  emit(json({ ...printablePlan, results }) + "\n");
@@ -2299,6 +2795,16 @@ async function main() {
2299
2795
  return;
2300
2796
  }
2301
2797
 
2798
+ if (!flags.has("json")) {
2799
+ emit(
2800
+ `PLAN eligible=${sendRows.length}/${plan.rows.length} ` +
2801
+ `total=${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
2802
+ );
2803
+ if (sendRows.length === 0) {
2804
+ emit("Nothing to sweep.\n");
2805
+ }
2806
+ }
2807
+
2302
2808
  const receipts = await executeRegistrySolSweep(slrd, plan, options);
2303
2809
  if (flags.has("json")) {
2304
2810
  emit(json({ ...printablePlan, receipts }) + "\n");
@@ -2339,150 +2845,183 @@ async function main() {
2339
2845
  command === "unwrap-wsol" ||
2340
2846
  (command === "unwrap" && values[0] === "wsol")
2341
2847
  ) {
2342
- const targets = targetWallets(
2343
- slrd,
2344
- flags,
2345
- "Usage: slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2346
- );
2848
+ const allWallets = flags.has("all-wallets");
2849
+ const explicitSelectors = [
2850
+ Boolean(flags.get("wallet")),
2851
+ csv(flags.get("wallets")).length > 0,
2852
+ Boolean(flags.get("group")),
2853
+ ].filter(Boolean).length;
2854
+ if (allWallets && explicitSelectors > 0)
2855
+ throw new Error(
2856
+ "Use --all-wallets or a specific wallet selector, not both.",
2857
+ );
2858
+ const targets = allWallets
2859
+ ? {
2860
+ mode: "all-wallets" as const,
2861
+ refs: slrd.wallets.list().map((wallet) => wallet.name),
2862
+ }
2863
+ : targetWallets(
2864
+ slrd,
2865
+ flags,
2866
+ "Usage: slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2867
+ );
2347
2868
  const destination = flags.get("destination");
2348
2869
  if (destination && targets.refs.length !== 1)
2349
2870
  throw new Error(
2350
2871
  "--destination is only supported with a single --wallet unwrap.",
2351
2872
  );
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;
2873
+ const skipMissing =
2874
+ allWallets || flags.has("ignore-missing") || flags.has("skip-missing");
2875
+ const via = flags.get("sender") ?? "rpc";
2876
+ const skipSimulation = flags.has("skip-simulation");
2877
+ const skipPreflight = flags.has("skip-preflight") || skipSimulation;
2362
2878
  const failFast = flags.has("fail-fast");
2879
+ const results: Array<Record<string, unknown>> = [];
2363
2880
 
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(),
2881
+ for (const walletRef of targets.refs) {
2882
+ const resolved = slrd.resolveWallet(walletRef);
2883
+ const address = resolved.address.toBase58();
2884
+ const label = resolved.row?.name ? `@${resolved.row.name}` : address;
2885
+ try {
2886
+ const accounts = (await slrd.tokenAccounts(address)).filter(
2887
+ (account) => account.mint === NATIVE_SOL_MINT,
2888
+ );
2889
+ if (!accounts.length) {
2890
+ if (!skipMissing && targets.refs.length === 1)
2891
+ throw new Error(`No WSOL token accounts exist for ${address}`);
2892
+ results.push({
2893
+ wallet: walletRef,
2894
+ address,
2895
+ ok: true,
2896
+ skipped: "missing-wsol",
2897
+ });
2898
+ continue;
2899
+ }
2900
+
2901
+ let closed = 0;
2902
+ let unwrappedRaw = 0n;
2903
+ const accountResults: Array<Record<string, unknown>> = [];
2904
+ for (const account of accounts) {
2905
+ if (account.closeAuthority && account.closeAuthority !== address) {
2906
+ const error = `WSOL account ${account.address} has close authority ${account.closeAuthority}`;
2907
+ accountResults.push({
2908
+ account: account.address,
2909
+ ok: false,
2910
+ error,
2389
2911
  });
2912
+ if (failFast) throw new Error(error);
2390
2913
  continue;
2391
2914
  }
2392
- }
2393
-
2394
- try {
2395
- if (flags.has("simulate-only")) {
2396
- const plan = await slrd
2915
+ try {
2916
+ const composer = slrd
2397
2917
  .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,
2918
+ .closeTokenAccountAddress(
2919
+ account.address,
2920
+ account.tokenProgram,
2921
+ {
2922
+ destination,
2923
+ },
2924
+ );
2925
+ if (flags.has("simulate-only")) {
2926
+ const plan = await composer.build();
2927
+ const simulation = await slrd.simulatePlan(plan);
2928
+ if (!simulation.success) throw new Error("simulation failed");
2929
+ accountResults.push({
2930
+ account: account.address,
2931
+ amountRaw: account.amountRaw,
2932
+ ok: true,
2933
+ mode: "simulation",
2934
+ simulation,
2935
+ });
2936
+ } else {
2937
+ const receipt = await composer.send({
2938
+ via,
2939
+ kind: "unwrap-wsol:account",
2940
+ skipSimulation,
2941
+ skipPreflight,
2942
+ });
2943
+ const { PublicKey } = await import("@solana/web3.js");
2944
+ let closedOnChain = false;
2945
+ for (let attempt = 0; attempt < 6; attempt += 1) {
2946
+ const current = await slrd
2947
+ .connection()
2948
+ .getAccountInfo(
2949
+ new PublicKey(account.address),
2950
+ "confirmed",
2951
+ );
2952
+ if (!current || current.lamports === 0) {
2953
+ closedOnChain = true;
2954
+ break;
2955
+ }
2956
+ if (attempt < 5)
2957
+ await new Promise((resolve) => setTimeout(resolve, 500));
2958
+ }
2959
+ if (!closedOnChain)
2960
+ throw new Error(
2961
+ `WSOL account ${account.address} still exists after close`,
2962
+ );
2963
+ accountResults.push({
2964
+ account: account.address,
2965
+ amountRaw: account.amountRaw,
2966
+ ok: true,
2967
+ receipt,
2968
+ });
2969
+ }
2970
+ closed += 1;
2971
+ unwrappedRaw += BigInt(account.amountRaw);
2972
+ } catch (error) {
2973
+ const message =
2974
+ error instanceof Error ? error.message : String(error);
2975
+ accountResults.push({
2976
+ account: account.address,
2977
+ ok: false,
2978
+ error: message,
2415
2979
  });
2980
+ if (failFast) throw error;
2416
2981
  }
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
2982
  }
2983
+ const failedAccounts = accountResults.filter(
2984
+ (row) => row.ok === false,
2985
+ ).length;
2986
+ results.push({
2987
+ wallet: walletRef,
2988
+ address,
2989
+ ok: failedAccounts === 0,
2990
+ closed,
2991
+ failedAccounts,
2992
+ unwrappedRaw,
2993
+ accounts: accountResults,
2994
+ });
2995
+ } catch (error) {
2996
+ const message =
2997
+ error instanceof Error ? error.message : String(error);
2998
+ results.push({
2999
+ wallet: walletRef,
3000
+ address,
3001
+ ok: false,
3002
+ error: message,
3003
+ });
3004
+ process.stderr.write(
3005
+ `${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
3006
+ );
3007
+ if (failFast) throw error;
2430
3008
  }
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
3009
  }
2480
3010
 
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");
3011
+ const succeeded = results.filter(
3012
+ (row) => row.ok === true && !row.skipped,
3013
+ ).length;
3014
+ const skipped = results.filter((row) => Boolean(row.skipped)).length;
3015
+ const failed = results.filter((row) => row.ok === false).length;
3016
+ emit(
3017
+ json({
3018
+ mode: flags.has("simulate-only") ? "simulation" : "live",
3019
+ target: targets,
3020
+ summary: { total: results.length, succeeded, skipped, failed },
3021
+ results,
3022
+ }) + "\n",
3023
+ );
3024
+ if (failed) process.exitCode = 1;
2486
3025
  return;
2487
3026
  }
2488
3027
  if (command === "rewards") {