@solard/cli 0.2.33 → 0.2.40

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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
- import { configure } from "measure-fn";
2
+ import { configure } from "measure-fn";
3
+ import { excludeWalletGroups } from "./wallet-group-filter.ts";
3
4
  import {
4
5
  addExternalContact,
5
6
  createSolardMeasureCollector,
@@ -23,6 +24,7 @@ import {
23
24
  type TokenRow,
24
25
  } from "@solard/core";
25
26
 
27
+ import { activateRpc, effectiveRpc, loadRpcConfig, saveRpcConfig, rpcUrl, safeUrl, probeHttp, probeWs } from "./rpc-management.ts";
26
28
  import { handleMeteoraCommand } from "./meteora-commands.ts";
27
29
  import { handleAccountingCommand } from "./accounting-commands.ts";
28
30
  import { resolveDestinationRef } from "./refs.ts";
@@ -358,7 +360,9 @@ External contacts (public addresses only; never signing wallets or group members
358
360
  slrd contact list
359
361
  slrd contact show <name|address>
360
362
  slrd contact remove <name|address>
361
- slrd wallets [--wallet <wallet>] [--group <name>] [--token <token> | --tokens] [--token-totals] [--only-with-tokens] [--rpc-concurrency <n>] [--rpc-delay-ms <n>] [--addresses-only]
363
+ slrd rpc add http|ws <url> [--group <quota-group>] [--rps N] [--concurrency N]
364
+ slrd rpc list|test|status|auto on|off|remove http|ws <url>
365
+ slrd wallets [--wallet <wallet>] [--group <name>] [--except <group1,group2>] [--token <token> | --tokens] [--token-totals] [--only-with-tokens] [--rpc-concurrency <n>] [--rpc-delay-ms <n>] [--addresses-only]
362
366
  Show SOL by default; token scans are opt-in
363
367
  slrd balances --wallet <wallet> [--token <token>] [--show-zero] Show SOL + all SPL token balances for one wallet
364
368
  slrd fees --wallet <wallet> [--since 30m] [--limit 100] Sum actual on-chain transaction fees paid by this wallet
@@ -442,10 +446,10 @@ Diagnostics
442
446
  Trading
443
447
  slrd swap --from <SOL|token|mint> --to <SOL|token|mint> --amount <ui> --wallet <wallet> [--live] Jupiter exact-input any -> any; quote-only unless --live
444
448
  slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live] Compatibility: SOL -> token
445
- slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--venue auto|native|jupiter] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
449
+ slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--max-price-sol <SOL/token>] [--venue auto|native|jupiter] [--slippage-bps 1500 | --min-out <amount> | --min-out-raw <units>] [--sender rpc|helius|jito] [--simulate-only]
446
450
  slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
447
451
  slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
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]
452
+ slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--venue auto|native|jupiter] [--slippage-bps 1500 | --min-out <amount> | --min-out-raw <units>] [--sender rpc|helius|jito] [--simulate-only]
449
453
  Native buy/sell and transfer fees: --cu-limit N --priority-micro-lamports N (fixed), or --fee-percentile N --trade-attempts N --fee-multiplier N --max-priority-fee-lamports N (automatic)
450
454
  slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
451
455
  Close every owned WSOL token account, including non-ATA WSOL accounts
@@ -542,6 +546,58 @@ async function main() {
542
546
  const [command, ...rest] = process.argv.slice(2);
543
547
  const { values, flags } = args(rest);
544
548
 
549
+ if (command === "rpc") {
550
+ const action = values[0] ?? "list";
551
+ if (action === "add" || action === "remove") {
552
+ const transport = values[1]; const raw = values[2];
553
+ if (!raw || (transport !== "http" && transport !== "ws")) throw new Error("Usage: slrd rpc add|remove http|ws <url>");
554
+ const url = rpcUrl(raw, transport);
555
+ const config = loadRpcConfig();
556
+ if (transport === "http") {
557
+ const index = config.http.findIndex(row => row.url === url);
558
+ if (action === "remove") { if (index < 0) throw new Error("RPC not configured"); config.http.splice(index, 1); }
559
+ else if (index < 0) {
560
+ const hostname = new URL(url).hostname;
561
+ const group = flags.get("group") ?? hostname;
562
+ const rps = int(flags, "rps", 5)!, concurrency = int(flags, "concurrency", 3)!;
563
+ if (!/^[a-zA-Z0-9._:-]{1,100}$/.test(group) || !Number.isSafeInteger(rps) || rps < 1 || !Number.isSafeInteger(concurrency) || concurrency < 1)
564
+ throw new Error("Invalid RPC provider limits");
565
+ config.http.push({ url, group, rps, concurrency });
566
+ }
567
+ } else {
568
+ const index = config.websocket.indexOf(url);
569
+ if (action === "remove") { if (index < 0) throw new Error("WebSocket not configured"); config.websocket.splice(index, 1); }
570
+ else if (index < 0) config.websocket.push(url);
571
+ }
572
+ saveRpcConfig(config); emit("RPC configuration saved; credentials redacted\n"); return;
573
+ }
574
+ if (action === "auto") {
575
+ if (values[1] !== "on" && values[1] !== "off") throw new Error("Usage: slrd rpc auto on|off");
576
+ const config = loadRpcConfig(); config.auto = values[1] === "on"; saveRpcConfig(config);
577
+ emit(`RPC automatic selection ${values[1]}\n`); return;
578
+ }
579
+ if (action === "list" || action === "status" || action === "test") {
580
+ const effective = effectiveRpc(flags);
581
+ const saved = effective.saved;
582
+ const output: Record<string, unknown> = {
583
+ source: effective.source, auto: saved.auto,
584
+ saved: { http: saved.http.map(row => ({ ...row, url: safeUrl(row.url) })), websocket: saved.websocket.map(safeUrl) },
585
+ effective: { http: effective.http.map(safeUrl), websocket: effective.websocket.map(safeUrl) },
586
+ aggregateRps: saved.aggregateRps,
587
+ };
588
+ if (action !== "list") {
589
+ const health = [];
590
+ for (const url of effective.http) health.push(await probeHttp(url));
591
+ for (const url of effective.websocket) health.push(await probeWs(url));
592
+ output.health = health;
593
+ output.active = health.find(row => row.state === "healthy")?.endpoint ?? null;
594
+ if (!effective.http.length || health.every(row => row.state !== "healthy")) process.exitCode = 1;
595
+ }
596
+ emit(json(output) + "\n"); return;
597
+ }
598
+ throw new Error("Usage: slrd rpc add|remove|list|test|status|auto");
599
+ }
600
+ activateRpc(flags);
545
601
  const commandStartedAt = performance.now();
546
602
  const measureCollector = createSolardMeasureCollector();
547
603
  resetSolardRpcStats();
@@ -1146,7 +1202,12 @@ async function main() {
1146
1202
  wallets = wallets.filter((wallet) => addresses.has(wallet.address));
1147
1203
  }
1148
1204
 
1149
- if (flags.has("addresses-only")) {
1205
+ if (flags.has("except")) {
1206
+ const value=flags.get("except");
1207
+ if(!value || value==="true")throw Error("Missing --except <group1,group2>");
1208
+ wallets=excludeWalletGroups(wallets,csv(value),slrd.db.groupWallets.select().all(),new Set(slrd.groups.list().map(group=>group.name)));
1209
+ }
1210
+ if (flags.has("addresses-only")) {
1150
1211
  for (const wallet of wallets)
1151
1212
  emit(`@${wallet.name}\t${wallet.address}\n`);
1152
1213
  return;
@@ -1161,7 +1222,7 @@ async function main() {
1161
1222
  // Cheap native-SOL-only default: <=100 wallets per RPC call.
1162
1223
  const rows: Array<{
1163
1224
  wallet: (typeof wallets)[number];
1164
- solLamports: bigint;
1225
+ solLamports: bigint | null;
1165
1226
  }> = [];
1166
1227
  const connection = slrd.connection();
1167
1228
  const batchSize = 100;
@@ -1171,34 +1232,27 @@ async function main() {
1171
1232
  const addresses = batch.map(
1172
1233
  (wallet) => slrd.resolveWallet(wallet).address,
1173
1234
  );
1174
- const accounts = await connection.getMultipleAccountsInfo(
1175
- addresses,
1176
- "confirmed",
1177
- );
1178
-
1179
- for (let index = 0; index < batch.length; index += 1) {
1180
- rows.push({
1181
- wallet: batch[index]!,
1182
- solLamports: BigInt(accounts[index]?.lamports ?? 0),
1235
+ try {
1236
+ const accounts = await connection.getMultipleAccountsInfo(addresses, "confirmed");
1237
+ if (accounts.length !== batch.length) throw new Error("Truncated account response");
1238
+ for (let index = 0; index < batch.length; index++) rows.push({
1239
+ wallet: batch[index]!, solLamports: BigInt(accounts[index]?.lamports ?? 0),
1183
1240
  });
1241
+ } catch {
1242
+ for (const wallet of batch) rows.push({ wallet, solLamports: null });
1184
1243
  }
1185
1244
  }
1186
1245
 
1187
- rows.sort((left, right) =>
1188
- left.solLamports === right.solLamports
1189
- ? left.wallet.name.localeCompare(right.wallet.name)
1190
- : left.solLamports > right.solLamports
1191
- ? -1
1192
- : 1,
1193
- );
1246
+ rows.sort((a,b) => a.solLamports === b.solLamports
1247
+ ? a.wallet.name.localeCompare(b.wallet.name)
1248
+ : a.solLamports === null ? 1 : b.solLamports === null ? -1
1249
+ : a.solLamports > b.solLamports ? -1 : 1);
1250
+ const unavailable = rows.filter(row => row.solLamports === null).length;
1251
+ const total = rows.reduce((sum,row) => sum + (row.solLamports ?? 0n), 0n);
1252
+ emit(`TOTAL\tSOL=${unavailable ? "N/A (partial)" : formatRaw(total,9)}\tWALLETS=${rows.length}\tUNAVAILABLE=${unavailable}\n`);
1253
+ for (const { wallet, solLamports } of rows) emit(
1254
+ `@${wallet.name}\t${wallet.address}\tSOL=${solLamports === null ? "N/A (RPC unavailable)" : formatRaw(solLamports,9)}\n`);
1194
1255
 
1195
- const totalSol = rows.reduce((sum, row) => sum + row.solLamports, 0n);
1196
- emit(`TOTAL\tSOL=${formatRaw(totalSol, 9)}\tWALLETS=${rows.length}\n`);
1197
- for (const { wallet, solLamports } of rows) {
1198
- emit(
1199
- `@${wallet.name}\t${wallet.address}\tSOL=${formatRaw(solLamports, 9)}\n`,
1200
- );
1201
- }
1202
1256
  return;
1203
1257
  }
1204
1258
 
@@ -2328,7 +2382,7 @@ async function main() {
2328
2382
  delayMs: int(flags, "delay-ms", 0) ?? 0,
2329
2383
  routeDelayMs: int(flags, "route-delay-ms", 0) ?? 0,
2330
2384
  portfolioConcurrency: 1,
2331
- portfolioDelayMs: 0,
2385
+ portfolioDelayMs: Math.max(0, int(flags, "rpc-delay-ms", 2500) ?? 2500),
2332
2386
  jupiterFallback: !flags.has("no-jupiter"),
2333
2387
  burnUnsellable: flags.has("burn-unsellable"),
2334
2388
  onProgress: (event: any) => {
@@ -2386,8 +2440,27 @@ async function main() {
2386
2440
  );
2387
2441
  };
2388
2442
 
2389
- let plan = await planRegistryTokenLiquidation(slrd, options);
2443
+ const { retryLiquidationRead, runLiquidationWallets } = await import("./liquidation-wallets.ts");
2444
+ const cleanupWallets = new Set<string>();
2445
+ const runWallet = async (walletRef: string, phase: "sales" | "cleanup" = "sales") => {
2446
+ const walletOptions = { ...options, walletRefs: [walletRef], cleanup: phase === "cleanup" };
2447
+
2448
+ const readPlan = () => retryLiquidationRead(
2449
+ () => planRegistryTokenLiquidation(slrd, walletOptions),
2450
+ (attempt, delayMs) => emit(`BACKOFF @${walletRef} read retry=${attempt} delay=${delayMs}ms\n`),
2451
+ );
2452
+ let plan = await readPlan();
2453
+ if (plan.actions.some(action => action.kind === "close-empty")) cleanupWallets.add(walletRef);
2454
+ if (phase === "cleanup") plan = { ...plan, actions: plan.actions.filter(action => action.kind === "close-empty") };
2390
2455
  emitPlan(plan);
2456
+ if (flags.has("live") && phase === "sales" && !plan.actions.some(action => action.amountRaw > 0n)) {
2457
+ emit(`SELL-PASS @${walletRef} no positive holdings; rent cleanup queued separately\n`);
2458
+ return;
2459
+ }
2460
+ if (plan.actions.length === 0) {
2461
+ emit(`EMPTY @${walletRef} verified token-account scan; no liquidation work\n`);
2462
+ return;
2463
+ }
2391
2464
 
2392
2465
  if (!flags.has("simulate") && !flags.has("live")) {
2393
2466
  const actionable = plan.actions.filter(
@@ -2447,7 +2520,7 @@ async function main() {
2447
2520
  let finalPlan = plan;
2448
2521
 
2449
2522
  if (flags.has("simulate")) {
2450
- results = await simulateRegistryTokenLiquidation(slrd, plan, options);
2523
+ results = await simulateRegistryTokenLiquidation(slrd, plan, walletOptions);
2451
2524
  } else {
2452
2525
  const maxRounds = Math.max(
2453
2526
  1,
@@ -2459,9 +2532,12 @@ async function main() {
2459
2532
  emitPlan(plan, "PLAN-R ");
2460
2533
  }
2461
2534
  results.push(
2462
- ...(await executeRegistryTokenLiquidation(slrd, plan, options)),
2535
+ ...(await executeRegistryTokenLiquidation(slrd, plan, walletOptions)),
2463
2536
  );
2464
- finalPlan = await planRegistryTokenLiquidation(slrd, options);
2537
+ finalPlan = await readPlan();
2538
+ if (finalPlan.actions.some(action => action.kind === "close-empty")) cleanupWallets.add(walletRef);
2539
+ if (phase === "cleanup") break;
2540
+ if (results.some(result => result.error)) break;
2465
2541
  const retryable = finalPlan.actions.filter(
2466
2542
  (action) =>
2467
2543
  action.amountRaw > 0n &&
@@ -2530,6 +2606,24 @@ async function main() {
2530
2606
  );
2531
2607
  process.exitCode = 1;
2532
2608
  }
2609
+ if (failed) { process.exitCode = 1; throw new Error(`${failed} liquidation action(s) failed; no automatic repeat`); }
2610
+ };
2611
+ const excluded = new Set(exceptWalletRefs.map(ref => slrd.resolveWallet(ref).address.toBase58()));
2612
+ const selected = (options.walletRefs.length ? options.walletRefs : slrd.listWallets().map(w => w.address))
2613
+ .filter(ref => !excluded.has(slrd.resolveWallet(ref).address.toBase58()));
2614
+ const failures = await runLiquidationWallets(selected, runWallet,
2615
+ (ref, index, total) => emit(`WALLET ${index}/${total} @${slrd.resolveWallet(ref).row?.name ?? ref}\n`),
2616
+ (ref, error) => emit(`WALLET-FAILED @${ref} ${error instanceof Error ? error.message : String(error)}\n`));
2617
+ if (failures.length) process.exitCode = 1;
2618
+ if (flags.has("live")) {
2619
+ emit(`CLEANUP-PASS selling pass finished; reclaiming rent afterward\n`);
2620
+ const cleanupFailures = await runLiquidationWallets([...cleanupWallets].filter(ref => !failures.includes(ref)),
2621
+ ref => runWallet(ref, "cleanup"),
2622
+ (ref, index, total) => emit(`CLEANUP-WALLET ${index}/${total} @${slrd.resolveWallet(ref).row?.name ?? ref}\n`),
2623
+ (ref, error) => emit(`CLEANUP-FAILED @${ref} ${error instanceof Error ? error.message : String(error)}\n`));
2624
+ if (cleanupFailures.length) process.exitCode = 1;
2625
+ }
2626
+ emit(`REGISTRY wallets=${selected.length} scan-or-execution-errors=${failures.length}\n`);
2533
2627
  return;
2534
2628
  }
2535
2629