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