@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/package.json +3 -3
- package/src/commands/buy.ts +30 -4
- package/src/commands/sell.ts +19 -4
- package/src/commands/trade-fees.test.ts +18 -6
- package/src/commands/trade-fees.ts +8 -2
- package/src/commands/trade-minimum.test.ts +50 -0
- package/src/commands/trade-minimum.ts +23 -0
- package/src/commands/transfer.test.ts +75 -18
- package/src/commands/transfer.ts +34 -6
- package/src/index.ts +128 -34
- package/src/launch-watch-command.ts +301 -301
- package/src/liquidation-wallets.test.ts +27 -0
- package/src/liquidation-wallets.ts +33 -0
- package/src/price-feed-client.ts +174 -174
- package/src/price-feed-protocol.ts +45 -45
- package/src/price-feed-server.architecture.test.ts +20 -20
- package/src/price-feed-server.ts +315 -315
- package/src/pump/token-launch-cli.ts +1 -1
- package/src/refs.ts +3 -1
- package/src/rpc-management.test.ts +108 -0
- package/src/rpc-management.ts +109 -0
- package/src/trade-strategy-command.ts +1219 -1219
- package/src/wallet-group-filter.test.ts +10 -0
- package/src/wallet-group-filter.ts +12 -0
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
|
|
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("
|
|
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
|
-
|
|
1175
|
-
addresses,
|
|
1176
|
-
"
|
|
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((
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
:
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
2535
|
+
...(await executeRegistryTokenLiquidation(slrd, plan, walletOptions)),
|
|
2463
2536
|
);
|
|
2464
|
-
finalPlan = await
|
|
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
|
|