@solard/cli 0.2.19 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.ts +695 -146
- 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
|
@@ -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> [--
|
|
369
|
+
slrd token backfill <ca> [--max-signatures N] [--json] Index signatures only; no transaction hydration by default
|
|
370
|
+
slrd token backfill <ca> --materialize [--price-sample-ms 1000] [--replace] [--rpc-concurrency 2]
|
|
371
|
+
slrd token backfill <ca> --exact [--replace] [--rpc-concurrency 2]
|
|
367
372
|
Durable exact trades + sparse 1s candle materialization from creation
|
|
368
373
|
slrd token trades <ca> [--from-start] [--min-sol N] [--owner <wallet>] [--side buy|sell] [--limit N] [--json]
|
|
369
374
|
slrd token analyze <ca> [--top N] [--json]
|
|
@@ -380,8 +385,18 @@ Vanity mints
|
|
|
380
385
|
slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
|
|
381
386
|
launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
|
|
382
387
|
|
|
383
|
-
|
|
388
|
+
Shared market feed
|
|
389
|
+
slrd feed serve [--host 127.0.0.1] [--port 8788] [--rpc-rps 5] [--rpc-read-rps 2] [--max-fallback-subs 200]
|
|
390
|
+
One upstream Pump/PumpSwap/LaunchLab feed shared by watchers, agents, and future UI clients
|
|
391
|
+
|
|
392
|
+
Launch discovery
|
|
393
|
+
slrd launch watch [--venue pump,raydium-launchlab] [--min-mcap 5000] [--ath-step-pct 20] [--track-ttl 30m] [--feed ws://127.0.0.1:8788/ws] [--show-new] [--prices] [--json]
|
|
394
|
+
Consume the shared feed; ignore Pump Mayhem and notify on first $5k cross and each +20% notified ATH step
|
|
395
|
+
|
|
396
|
+
Pump discovery
|
|
384
397
|
slrd pump pairs [--json] Read Pump-supported quote mints from the on-chain Global account
|
|
398
|
+
slrd pump watch [--min-mcap 5000] [--ath-step-pct 20] [--track-ttl 30m] [--feed ws://127.0.0.1:8788/ws] [--show-new] [--prices] [--json]
|
|
399
|
+
Compatibility alias for: slrd launch watch --venue pump
|
|
385
400
|
|
|
386
401
|
Metadata and launching
|
|
387
402
|
slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--pair SOL|USDC|<custom-quote-mint>] [--beneficiary <wallet|address>] [--alias <name>] [--live] [--skip-simulation]
|
|
@@ -414,7 +429,7 @@ Transfers and consolidation
|
|
|
414
429
|
slrd sweep sol ... Compatibility alias; --max-balance-sol remains accepted
|
|
415
430
|
|
|
416
431
|
Token liquidation
|
|
417
|
-
slrd liquidate tokens [--except <token|mint>] [--wallets <a,b,...>] [--except-wallet <wallet>] [--except-wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--burn-unsellable] [--simulate | --live]
|
|
432
|
+
slrd liquidate tokens [--except <token|mint>] [--wallets <a,b,...>] [--except-wallet <wallet>] [--except-wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--burn-unsellable] [--rounds 3] [--simulate | --live]
|
|
418
433
|
Sell all supported tokens unless protected with --except; excluded wallets are not scanned or touched
|
|
419
434
|
|
|
420
435
|
RPC
|
|
@@ -435,7 +450,12 @@ Trading
|
|
|
435
450
|
slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
|
|
436
451
|
slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
|
|
437
452
|
slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--venue auto|native|jupiter] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
|
|
438
|
-
slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--
|
|
453
|
+
slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
|
|
454
|
+
Close every owned WSOL token account, including non-ATA WSOL accounts
|
|
455
|
+
slrd reclaim inspect --all-wallets List Loader-v3 buffers/programs controlled by stored wallets
|
|
456
|
+
slrd reclaim buffers --all-wallets [--simulate | --live]
|
|
457
|
+
slrd reclaim programs --all-wallets [--simulate | --live --confirm-program-close]
|
|
458
|
+
slrd reclaim all --all-wallets [--simulate | --live --confirm-program-close]
|
|
439
459
|
slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
|
|
440
460
|
slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
|
|
441
461
|
slrd transfer-many status <stable-id>
|
|
@@ -450,10 +470,14 @@ Trading
|
|
|
450
470
|
slrd rewards claim <token|ca> --wallet <fee-payer> [--id <stable-claim-id> --basis <reward-basis.json>]
|
|
451
471
|
slrd rewards claim-status <stable-claim-id>
|
|
452
472
|
|
|
453
|
-
Scripts
|
|
473
|
+
Scripts and event strategies
|
|
454
474
|
slrd scripts List scripts registered in slrd.config.ts
|
|
455
475
|
slrd run <name-or-path> [script flags...] Execute a script that imports slrd
|
|
456
476
|
slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
|
|
477
|
+
slrd strategy run <file.ts> --token <mint|alias> --wallet <wallet> [--params <json>] [--feed ws://127.0.0.1:8788/ws] [--live]
|
|
478
|
+
Run an onPrice strategy from the shared feed; wallet/execution state remains isolated per agent
|
|
479
|
+
slrd strategy backtest <file.ts> --token <mint> [--params <json>] [--capital-sol 1] [--price-sample 5s] [--sol-usd N] [--json]
|
|
480
|
+
Replay the same strategy file over durable token history
|
|
457
481
|
|
|
458
482
|
Meteora DLMM
|
|
459
483
|
slrd meteora discover --timeframe 30m --sort fee-active-tvl --limit 20
|
|
@@ -490,6 +514,9 @@ Watching
|
|
|
490
514
|
slrd watch list
|
|
491
515
|
|
|
492
516
|
Transactions and ALTs
|
|
517
|
+
slrd tx stream --wallet <wallet|address> [--finalized] [--jsonl]
|
|
518
|
+
slrd tx stream --wallets <a,b,c> [--finalized] [--jsonl]
|
|
519
|
+
Stream every transaction mentioning the selected wallets until Ctrl+C
|
|
493
520
|
slrd history
|
|
494
521
|
slrd jito tip-accounts [--endpoint <block-engine-url>]
|
|
495
522
|
slrd alt add <address> [label]
|
|
@@ -791,6 +818,24 @@ async function main() {
|
|
|
791
818
|
process.exitCode = await runScript(script, scriptArgs);
|
|
792
819
|
return;
|
|
793
820
|
}
|
|
821
|
+
if (command === "feed" && values[0] === "serve") {
|
|
822
|
+
const { runPriceFeedServerCommand } =
|
|
823
|
+
await import("./price-feed-server.ts");
|
|
824
|
+
await runPriceFeedServerCommand({ flags, emit });
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (command === "strategy" && values[0] === "backtest") {
|
|
828
|
+
const { runStrategyBacktestCommand } =
|
|
829
|
+
await import("./strategy-backtest-command.ts");
|
|
830
|
+
await runStrategyBacktestCommand({ values, flags, emit });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (command === "strategy" && values[0] === "run") {
|
|
834
|
+
const { runTradeStrategyCommand } =
|
|
835
|
+
await import("./trade-strategy-command.ts");
|
|
836
|
+
await runTradeStrategyCommand({ values, flags, emit });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
794
839
|
if (
|
|
795
840
|
(command === "prepare" && values[0] === "pump") ||
|
|
796
841
|
(command === "launch" &&
|
|
@@ -803,6 +848,11 @@ async function main() {
|
|
|
803
848
|
await runPumpExternalDeploymentFromArgs(externalArgs);
|
|
804
849
|
return;
|
|
805
850
|
}
|
|
851
|
+
if (command === "launch" && values[0] === "watch") {
|
|
852
|
+
const { runLaunchWatchCommand } = await import("./launch-watch-command.ts");
|
|
853
|
+
await runLaunchWatchCommand({ flags, emit });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
806
856
|
if (
|
|
807
857
|
command === "launch" &&
|
|
808
858
|
(values[0] === "pump" || values[0] === "pump-token")
|
|
@@ -831,6 +881,11 @@ async function main() {
|
|
|
831
881
|
});
|
|
832
882
|
return;
|
|
833
883
|
}
|
|
884
|
+
if (command === "pump" && values[0] === "watch") {
|
|
885
|
+
const { runPumpWatchCommand } = await import("./pump-watch-command.ts");
|
|
886
|
+
await runPumpWatchCommand({ flags, emit });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
834
889
|
if (command === "jito" && values[0] === "tip-accounts") {
|
|
835
890
|
const endpoint = (
|
|
836
891
|
flags.get("endpoint") ??
|
|
@@ -879,6 +934,12 @@ async function main() {
|
|
|
879
934
|
return;
|
|
880
935
|
}
|
|
881
936
|
|
|
937
|
+
if (command === "reclaim") {
|
|
938
|
+
const { runReclaimCommand } = await import("./reclaim-command.ts");
|
|
939
|
+
await runReclaimCommand({ slrd, values, flags, emit });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
882
943
|
if (command === "meteora") {
|
|
883
944
|
await handleMeteoraCommand({ slrd, values, flags, emit });
|
|
884
945
|
return;
|
|
@@ -1312,7 +1373,7 @@ async function main() {
|
|
|
1312
1373
|
try {
|
|
1313
1374
|
const tx = await connection.getTransaction(entry.signature, {
|
|
1314
1375
|
commitment: "confirmed",
|
|
1315
|
-
maxSupportedTransactionVersion:
|
|
1376
|
+
maxSupportedTransactionVersion: 1,
|
|
1316
1377
|
});
|
|
1317
1378
|
const fee = tx?.meta?.fee;
|
|
1318
1379
|
const payer = txAccountKeys(tx)[0] ?? "";
|
|
@@ -1376,6 +1437,328 @@ async function main() {
|
|
|
1376
1437
|
);
|
|
1377
1438
|
return;
|
|
1378
1439
|
}
|
|
1440
|
+
if (command === "tx" && values[0] === "stream") {
|
|
1441
|
+
const walletFlag = flags.get("wallet");
|
|
1442
|
+
const walletRefs = [
|
|
1443
|
+
...(walletFlag && walletFlag !== "true" ? [walletFlag] : []),
|
|
1444
|
+
...csv(flags.get("wallets")),
|
|
1445
|
+
];
|
|
1446
|
+
if (walletRefs.length === 0) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
"Usage: slrd tx stream --wallet <wallet|address> | --wallets <a,b,c> [--finalized] [--jsonl]",
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
1453
|
+
const connection = slrd.connection();
|
|
1454
|
+
const commitment = flags.has("finalized") ? "finalized" : "confirmed";
|
|
1455
|
+
const enrichAttempts = Math.max(
|
|
1456
|
+
1,
|
|
1457
|
+
Math.min(20, Math.trunc(int(flags, "enrich-attempts", 8) ?? 8)),
|
|
1458
|
+
);
|
|
1459
|
+
const enrichDelayMs = Math.max(
|
|
1460
|
+
50,
|
|
1461
|
+
Math.trunc(int(flags, "enrich-delay-ms", 250) ?? 250),
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
type StreamWallet = {
|
|
1465
|
+
ref: string;
|
|
1466
|
+
name: string | null;
|
|
1467
|
+
address: string;
|
|
1468
|
+
publicKey: InstanceType<typeof PublicKey>;
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
const byAddress = new Map<string, StreamWallet>();
|
|
1472
|
+
for (const ref of walletRefs) {
|
|
1473
|
+
let name: string | null = null;
|
|
1474
|
+
let publicKey: InstanceType<typeof PublicKey>;
|
|
1475
|
+
try {
|
|
1476
|
+
const resolved = slrd.resolveWallet(ref);
|
|
1477
|
+
publicKey = resolved.address;
|
|
1478
|
+
name = resolved.row?.name ?? null;
|
|
1479
|
+
} catch {
|
|
1480
|
+
try {
|
|
1481
|
+
publicKey = new PublicKey(ref);
|
|
1482
|
+
} catch {
|
|
1483
|
+
throw new Error(`Unknown wallet or invalid Solana address: ${ref}`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const address = publicKey.toBase58();
|
|
1487
|
+
if (!byAddress.has(address)) {
|
|
1488
|
+
byAddress.set(address, { ref, name, address, publicKey });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const watched = [...byAddress.values()];
|
|
1492
|
+
const watchedAddresses = new Set(watched.map((row) => row.address));
|
|
1493
|
+
const labelFor = (row: StreamWallet) =>
|
|
1494
|
+
row.name ? `@${row.name}` : shortKey(row.address);
|
|
1495
|
+
|
|
1496
|
+
type PendingSignature = {
|
|
1497
|
+
signature: string;
|
|
1498
|
+
observedSlot: number | null;
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const queue: PendingSignature[] = [];
|
|
1502
|
+
const queued = new Set<string>();
|
|
1503
|
+
const seen = new Set<string>();
|
|
1504
|
+
const seenOrder: string[] = [];
|
|
1505
|
+
const listenerIds: number[] = [];
|
|
1506
|
+
let processing = false;
|
|
1507
|
+
let stopped = false;
|
|
1508
|
+
let stopResolve: (() => void) | null = null;
|
|
1509
|
+
|
|
1510
|
+
const rememberSeen = (signature: string) => {
|
|
1511
|
+
if (seen.has(signature)) return;
|
|
1512
|
+
seen.add(signature);
|
|
1513
|
+
seenOrder.push(signature);
|
|
1514
|
+
while (seenOrder.length > 20_000) {
|
|
1515
|
+
const oldest = seenOrder.shift();
|
|
1516
|
+
if (oldest) seen.delete(oldest);
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const transactionWithRetry = async (signature: string) => {
|
|
1521
|
+
for (let attempt = 1; attempt <= enrichAttempts; attempt += 1) {
|
|
1522
|
+
const tx = await connection.getTransaction(signature, {
|
|
1523
|
+
commitment,
|
|
1524
|
+
maxSupportedTransactionVersion: 1,
|
|
1525
|
+
});
|
|
1526
|
+
if (tx) return tx;
|
|
1527
|
+
if (attempt < enrichAttempts) {
|
|
1528
|
+
await new Promise((resolve) => setTimeout(resolve, enrichDelayMs));
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return null;
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
const printTransaction = async (
|
|
1535
|
+
item: PendingSignature,
|
|
1536
|
+
): Promise<void> => {
|
|
1537
|
+
const tx = await transactionWithRetry(item.signature);
|
|
1538
|
+
if (!tx?.meta) {
|
|
1539
|
+
const unavailable = {
|
|
1540
|
+
type: "transaction",
|
|
1541
|
+
signature: item.signature,
|
|
1542
|
+
slot: item.observedSlot,
|
|
1543
|
+
commitment,
|
|
1544
|
+
status: "unavailable",
|
|
1545
|
+
wallets: [],
|
|
1546
|
+
};
|
|
1547
|
+
if (flags.has("jsonl")) emit(JSON.stringify(unavailable) + "\n");
|
|
1548
|
+
else
|
|
1549
|
+
emit(
|
|
1550
|
+
`${new Date().toISOString()} UNAVAILABLE ${item.signature} slot=${item.observedSlot ?? "?"}\n`,
|
|
1551
|
+
);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const keys = txAccountKeys(tx);
|
|
1556
|
+
const keySet = new Set(keys);
|
|
1557
|
+
const touched = watched.filter((row) => keySet.has(row.address));
|
|
1558
|
+
if (touched.length === 0) return;
|
|
1559
|
+
|
|
1560
|
+
const feeLamports =
|
|
1561
|
+
typeof tx.meta.fee === "number" && Number.isFinite(tx.meta.fee)
|
|
1562
|
+
? BigInt(Math.trunc(tx.meta.fee))
|
|
1563
|
+
: 0n;
|
|
1564
|
+
const payer = keys[0] ?? null;
|
|
1565
|
+
const status = tx.meta.err ? "failed" : "confirmed";
|
|
1566
|
+
const at =
|
|
1567
|
+
tx.blockTime == null
|
|
1568
|
+
? new Date().toISOString()
|
|
1569
|
+
: new Date(tx.blockTime * 1_000).toISOString();
|
|
1570
|
+
|
|
1571
|
+
const walletRows = touched.map((wallet) => {
|
|
1572
|
+
const walletIndex = keys.indexOf(wallet.address);
|
|
1573
|
+
let nativeDeltaLamports: bigint | null = null;
|
|
1574
|
+
if (
|
|
1575
|
+
walletIndex >= 0 &&
|
|
1576
|
+
typeof tx.meta!.preBalances?.[walletIndex] === "number" &&
|
|
1577
|
+
typeof tx.meta!.postBalances?.[walletIndex] === "number"
|
|
1578
|
+
) {
|
|
1579
|
+
nativeDeltaLamports =
|
|
1580
|
+
BigInt(Math.trunc(tx.meta!.postBalances[walletIndex]!)) -
|
|
1581
|
+
BigInt(Math.trunc(tx.meta!.preBalances[walletIndex]!));
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const tokenDeltas = [...txWalletTokenBalances(tx, wallet.address)]
|
|
1585
|
+
.map(([mint, balance]) => ({
|
|
1586
|
+
mint,
|
|
1587
|
+
decimals: balance.decimals,
|
|
1588
|
+
deltaRaw: balance.post - balance.pre,
|
|
1589
|
+
}))
|
|
1590
|
+
.filter((row) => row.deltaRaw !== 0n)
|
|
1591
|
+
.sort((left, right) => {
|
|
1592
|
+
const leftAbs =
|
|
1593
|
+
left.deltaRaw < 0n ? -left.deltaRaw : left.deltaRaw;
|
|
1594
|
+
const rightAbs =
|
|
1595
|
+
right.deltaRaw < 0n ? -right.deltaRaw : right.deltaRaw;
|
|
1596
|
+
return leftAbs === rightAbs
|
|
1597
|
+
? left.mint.localeCompare(right.mint)
|
|
1598
|
+
: leftAbs > rightAbs
|
|
1599
|
+
? -1
|
|
1600
|
+
: 1;
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
return {
|
|
1604
|
+
ref: wallet.ref,
|
|
1605
|
+
name: wallet.name,
|
|
1606
|
+
address: wallet.address,
|
|
1607
|
+
nativeDeltaLamports,
|
|
1608
|
+
feePaidLamports: payer === wallet.address ? feeLamports : 0n,
|
|
1609
|
+
tokenDeltas,
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
if (flags.has("jsonl")) {
|
|
1614
|
+
emit(
|
|
1615
|
+
JSON.stringify(
|
|
1616
|
+
{
|
|
1617
|
+
type: "transaction",
|
|
1618
|
+
at,
|
|
1619
|
+
signature: item.signature,
|
|
1620
|
+
slot: tx.slot ?? item.observedSlot,
|
|
1621
|
+
commitment,
|
|
1622
|
+
status,
|
|
1623
|
+
error: tx.meta.err ?? null,
|
|
1624
|
+
feeLamports,
|
|
1625
|
+
feePayer: payer,
|
|
1626
|
+
wallets: walletRows,
|
|
1627
|
+
},
|
|
1628
|
+
(_, value) =>
|
|
1629
|
+
typeof value === "bigint" ? value.toString() : value,
|
|
1630
|
+
) + "\n",
|
|
1631
|
+
);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
emit(
|
|
1636
|
+
`\n${at} ${status.toUpperCase()} slot=${tx.slot ?? item.observedSlot ?? "?"}\n` +
|
|
1637
|
+
`SIG ${item.signature}\n` +
|
|
1638
|
+
`FEE ${formatRaw(feeLamports, 9)} SOL payer=${payer ?? "?"}\n`,
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
for (const row of walletRows) {
|
|
1642
|
+
const wallet = byAddress.get(row.address)!;
|
|
1643
|
+
emit(` ${labelFor(wallet)} ${row.address}\n`);
|
|
1644
|
+
emit(
|
|
1645
|
+
` SOL ${
|
|
1646
|
+
row.nativeDeltaLamports == null
|
|
1647
|
+
? "n/a"
|
|
1648
|
+
: `${formatSignedRaw(row.nativeDeltaLamports, 9)} SOL`
|
|
1649
|
+
}${
|
|
1650
|
+
row.feePaidLamports > 0n
|
|
1651
|
+
? ` (paid fee ${formatRaw(row.feePaidLamports, 9)} SOL)`
|
|
1652
|
+
: ""
|
|
1653
|
+
}\n`,
|
|
1654
|
+
);
|
|
1655
|
+
if (row.tokenDeltas.length === 0) {
|
|
1656
|
+
emit(" TOKEN no wallet-owned SPL balance delta\n");
|
|
1657
|
+
} else {
|
|
1658
|
+
for (const delta of row.tokenDeltas) {
|
|
1659
|
+
const tokenLabel =
|
|
1660
|
+
delta.mint === NATIVE_SOL_MINT
|
|
1661
|
+
? "WSOL"
|
|
1662
|
+
: delta.mint === CANONICAL_USDC_MINT
|
|
1663
|
+
? "USDC"
|
|
1664
|
+
: shortKey(delta.mint);
|
|
1665
|
+
emit(
|
|
1666
|
+
` TOKEN ${formatSignedRaw(delta.deltaRaw, delta.decimals)} ${tokenLabel} ${delta.mint}\n`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (tx.meta.err) {
|
|
1673
|
+
emit(` ERROR ${JSON.stringify(tx.meta.err)}\n`);
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
const drain = async (): Promise<void> => {
|
|
1678
|
+
if (processing) return;
|
|
1679
|
+
processing = true;
|
|
1680
|
+
try {
|
|
1681
|
+
while (!stopped && queue.length > 0) {
|
|
1682
|
+
const item = queue.shift()!;
|
|
1683
|
+
queued.delete(item.signature);
|
|
1684
|
+
if (seen.has(item.signature)) continue;
|
|
1685
|
+
try {
|
|
1686
|
+
await printTransaction(item);
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
const message =
|
|
1689
|
+
error instanceof Error ? error.message : String(error);
|
|
1690
|
+
if (flags.has("jsonl")) {
|
|
1691
|
+
emit(
|
|
1692
|
+
JSON.stringify({
|
|
1693
|
+
type: "stream-error",
|
|
1694
|
+
signature: item.signature,
|
|
1695
|
+
error: message,
|
|
1696
|
+
}) + "\n",
|
|
1697
|
+
);
|
|
1698
|
+
} else {
|
|
1699
|
+
process.stderr.write(
|
|
1700
|
+
`${OWL} tx stream ${item.signature}: ${message}\n`,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
} finally {
|
|
1704
|
+
rememberSeen(item.signature);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
} finally {
|
|
1708
|
+
processing = false;
|
|
1709
|
+
if (!stopped && queue.length > 0) void drain();
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
const enqueue = (signature: string, observedSlot: number | null) => {
|
|
1714
|
+
if (stopped || seen.has(signature) || queued.has(signature)) return;
|
|
1715
|
+
queued.add(signature);
|
|
1716
|
+
queue.push({ signature, observedSlot });
|
|
1717
|
+
void drain();
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
if (!flags.has("jsonl")) {
|
|
1721
|
+
emit(
|
|
1722
|
+
`TX STREAM wallets=${watched.length} commitment=${commitment} Ctrl+C to stop\n`,
|
|
1723
|
+
);
|
|
1724
|
+
for (const wallet of watched) {
|
|
1725
|
+
emit(` ${labelFor(wallet)} ${wallet.address}\n`);
|
|
1726
|
+
}
|
|
1727
|
+
emit("\n");
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
for (const wallet of watched) {
|
|
1731
|
+
const id = connection.onLogs(
|
|
1732
|
+
wallet.publicKey,
|
|
1733
|
+
(logs, context) => {
|
|
1734
|
+
// Stream failed transactions too. The transaction metadata below
|
|
1735
|
+
// is the authoritative status/error source.
|
|
1736
|
+
enqueue(logs.signature, context?.slot ?? null);
|
|
1737
|
+
},
|
|
1738
|
+
commitment,
|
|
1739
|
+
);
|
|
1740
|
+
listenerIds.push(id);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
await new Promise<void>((resolve) => {
|
|
1744
|
+
stopResolve = resolve;
|
|
1745
|
+
const stop = () => {
|
|
1746
|
+
if (stopped) return;
|
|
1747
|
+
stopped = true;
|
|
1748
|
+
resolve();
|
|
1749
|
+
};
|
|
1750
|
+
process.once("SIGINT", stop);
|
|
1751
|
+
process.once("SIGTERM", stop);
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
await Promise.allSettled(
|
|
1755
|
+
listenerIds.map((id) => connection.removeOnLogsListener(id)),
|
|
1756
|
+
);
|
|
1757
|
+
|
|
1758
|
+
if (!flags.has("jsonl")) emit("TX STREAM STOPPED\n");
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1379
1762
|
if (command === "sol-flow" || (command === "tx" && values[0] === "flow")) {
|
|
1380
1763
|
const walletRef = need(flags, "wallet");
|
|
1381
1764
|
const wallet = slrd.resolveWallet(walletRef);
|
|
@@ -1427,7 +1810,7 @@ async function main() {
|
|
|
1427
1810
|
try {
|
|
1428
1811
|
const tx = await connection.getTransaction(entry.signature, {
|
|
1429
1812
|
commitment: "confirmed",
|
|
1430
|
-
maxSupportedTransactionVersion:
|
|
1813
|
+
maxSupportedTransactionVersion: 1,
|
|
1431
1814
|
});
|
|
1432
1815
|
if (!tx?.meta) {
|
|
1433
1816
|
rows.push({
|
|
@@ -1903,7 +2286,6 @@ async function main() {
|
|
|
1903
2286
|
: flags.has("simulate")
|
|
1904
2287
|
? "SIMULATE"
|
|
1905
2288
|
: "PLAN";
|
|
1906
|
-
|
|
1907
2289
|
emit(`LIQUIDATE ${mode}\n`);
|
|
1908
2290
|
for (const ref of except) {
|
|
1909
2291
|
const mint = resolveTokenMintForPolicy(slrd, ref);
|
|
@@ -1969,6 +2351,15 @@ async function main() {
|
|
|
1969
2351
|
emit(
|
|
1970
2352
|
`ROUTE ${event.index}/${event.total} ${shortKey(event.mint)}\n`,
|
|
1971
2353
|
);
|
|
2354
|
+
} else if (event.stage === "consolidation-start") {
|
|
2355
|
+
showAction("MOVE", event.index, event.total, event.action);
|
|
2356
|
+
emit(
|
|
2357
|
+
` consolidate ${event.accounts} non-ATA account(s), raw=${event.amountRaw.toString()} -> ATA\n`,
|
|
2358
|
+
);
|
|
2359
|
+
} else if (event.stage === "consolidation-done") {
|
|
2360
|
+
emit(
|
|
2361
|
+
`MOVED ${event.index}/${event.total} @${event.action.walletName} ${shortKey(event.action.mint)} accounts=${event.accounts} signatures=${event.signatures.length}\n`,
|
|
2362
|
+
);
|
|
1972
2363
|
} else if (event.stage === "action-start") {
|
|
1973
2364
|
showAction(
|
|
1974
2365
|
flags.has("simulate") ? "SIM " : "DO ",
|
|
@@ -1992,12 +2383,19 @@ async function main() {
|
|
|
1992
2383
|
},
|
|
1993
2384
|
};
|
|
1994
2385
|
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2386
|
+
const emitPlan = (
|
|
2387
|
+
plan: Awaited<ReturnType<typeof planRegistryTokenLiquidation>>,
|
|
2388
|
+
prefix = "PLAN ",
|
|
2389
|
+
) => {
|
|
2390
|
+
emit(
|
|
2391
|
+
`${prefix} native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
|
|
2392
|
+
`unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
|
|
2393
|
+
`keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
|
|
2394
|
+
);
|
|
2395
|
+
};
|
|
2396
|
+
|
|
2397
|
+
let plan = await planRegistryTokenLiquidation(slrd, options);
|
|
2398
|
+
emitPlan(plan);
|
|
2001
2399
|
|
|
2002
2400
|
if (!flags.has("simulate") && !flags.has("live")) {
|
|
2003
2401
|
const actionable = plan.actions.filter(
|
|
@@ -2052,9 +2450,43 @@ async function main() {
|
|
|
2052
2450
|
return;
|
|
2053
2451
|
}
|
|
2054
2452
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2453
|
+
let results: Awaited<ReturnType<typeof executeRegistryTokenLiquidation>> =
|
|
2454
|
+
[];
|
|
2455
|
+
let finalPlan = plan;
|
|
2456
|
+
|
|
2457
|
+
if (flags.has("simulate")) {
|
|
2458
|
+
results = await simulateRegistryTokenLiquidation(slrd, plan, options);
|
|
2459
|
+
} else {
|
|
2460
|
+
const maxRounds = Math.max(
|
|
2461
|
+
1,
|
|
2462
|
+
Math.min(6, int(flags, "rounds", 3) ?? 3),
|
|
2463
|
+
);
|
|
2464
|
+
for (let round = 1; round <= maxRounds; round += 1) {
|
|
2465
|
+
if (round > 1) {
|
|
2466
|
+
emit(`RESCAN liquidation round ${round}/${maxRounds}\n`);
|
|
2467
|
+
emitPlan(plan, "PLAN-R ");
|
|
2468
|
+
}
|
|
2469
|
+
results.push(
|
|
2470
|
+
...(await executeRegistryTokenLiquidation(slrd, plan, options)),
|
|
2471
|
+
);
|
|
2472
|
+
finalPlan = await planRegistryTokenLiquidation(slrd, options);
|
|
2473
|
+
const retryable = finalPlan.actions.filter(
|
|
2474
|
+
(action) =>
|
|
2475
|
+
action.amountRaw > 0n &&
|
|
2476
|
+
(action.kind === "sell" ||
|
|
2477
|
+
action.kind === "jupiter-sell" ||
|
|
2478
|
+
action.kind === "unwrap-wsol" ||
|
|
2479
|
+
(options.burnUnsellable && action.kind === "skip-unsupported")),
|
|
2480
|
+
);
|
|
2481
|
+
if (!retryable.length) break;
|
|
2482
|
+
if (round < maxRounds) {
|
|
2483
|
+
emit(
|
|
2484
|
+
`RESIDUAL ${retryable.length} positive holding(s) appeared/remain after round ${round}; rescanning and retrying\n`,
|
|
2485
|
+
);
|
|
2486
|
+
plan = finalPlan;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2058
2490
|
|
|
2059
2491
|
const failed = results.filter((result) => Boolean(result.error)).length;
|
|
2060
2492
|
const succeeded = results.filter((result) => !result.error);
|
|
@@ -2073,12 +2505,39 @@ async function main() {
|
|
|
2073
2505
|
const reclaimedRentLamports = succeeded
|
|
2074
2506
|
.filter((result) => result.action.kind === "close-empty")
|
|
2075
2507
|
.reduce((sum, result) => sum + (result.action.rentLamports ?? 0n), 0n);
|
|
2508
|
+
|
|
2509
|
+
const residual = flags.has("live")
|
|
2510
|
+
? finalPlan.actions.filter(
|
|
2511
|
+
(action) =>
|
|
2512
|
+
action.amountRaw > 0n &&
|
|
2513
|
+
action.kind !== "keep-protected" &&
|
|
2514
|
+
action.kind !== "close-empty",
|
|
2515
|
+
)
|
|
2516
|
+
: [];
|
|
2517
|
+
|
|
2076
2518
|
emit(
|
|
2077
2519
|
`DONE ok=${succeeded.length} failed=${failed} ` +
|
|
2078
2520
|
`native-sold=${soldNative} jupiter-sold=${soldJupiter} ` +
|
|
2079
2521
|
`unwrapped=${unwrapped} closed-empty=${closedEmpty} ` +
|
|
2522
|
+
`residual=${residual.length} ` +
|
|
2080
2523
|
`reclaimed-rent≈${(Number(reclaimedRentLamports) / 1e9).toFixed(6)} SOL\n`,
|
|
2081
2524
|
);
|
|
2525
|
+
|
|
2526
|
+
if (residual.length) {
|
|
2527
|
+
for (const action of residual.slice(0, 12)) {
|
|
2528
|
+
const label = action.symbol ?? action.name ?? shortKey(action.mint);
|
|
2529
|
+
emit(
|
|
2530
|
+
`REMAIN @${action.walletName} ${label} ${action.amountUi} ` +
|
|
2531
|
+
`${action.mint}${action.reason ? ` ${action.reason}` : ""}\n`,
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
if (residual.length > 12)
|
|
2535
|
+
emit(`REMAIN ... ${residual.length - 12} more holding(s)\n`);
|
|
2536
|
+
emit(
|
|
2537
|
+
`INCOMPLETE positive unprotected token balances remain after the final fresh scan\n`,
|
|
2538
|
+
);
|
|
2539
|
+
process.exitCode = 1;
|
|
2540
|
+
}
|
|
2082
2541
|
return;
|
|
2083
2542
|
}
|
|
2084
2543
|
|
|
@@ -2163,6 +2622,25 @@ async function main() {
|
|
|
2163
2622
|
throw new Error("Use either --simulate or --live, not both");
|
|
2164
2623
|
}
|
|
2165
2624
|
|
|
2625
|
+
const sweepMode = flags.has("live")
|
|
2626
|
+
? "LIVE"
|
|
2627
|
+
: flags.has("simulate")
|
|
2628
|
+
? "SIMULATE"
|
|
2629
|
+
: "PLAN";
|
|
2630
|
+
|
|
2631
|
+
if (!flags.has("json") && (flags.has("live") || flags.has("simulate"))) {
|
|
2632
|
+
const initialDestinationLabel = destination.contactName
|
|
2633
|
+
? `${destination.contactName} (${destination.address})`
|
|
2634
|
+
: destination.walletName
|
|
2635
|
+
? `${destination.walletName} (${destination.address})`
|
|
2636
|
+
: destination.address;
|
|
2637
|
+
emit(`SWEEP ${sweepMode} -> ${initialDestinationLabel}\n`);
|
|
2638
|
+
if (maxBalanceSol != null) emit(`Threshold: < ${maxBalanceSol} SOL\n`);
|
|
2639
|
+
emit(
|
|
2640
|
+
"PLAN reading confirmed wallet balances and estimating transfer fee...\n",
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2166
2644
|
const options = {
|
|
2167
2645
|
destination: destination.address,
|
|
2168
2646
|
excludeGroups,
|
|
@@ -2182,6 +2660,18 @@ async function main() {
|
|
|
2182
2660
|
Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
|
|
2183
2661
|
),
|
|
2184
2662
|
delayMs: int(flags, "delay-ms", 0) ?? 0,
|
|
2663
|
+
maxAttempts: Math.max(
|
|
2664
|
+
1,
|
|
2665
|
+
Math.trunc(int(flags, "sweep-attempts", 3) ?? 3),
|
|
2666
|
+
),
|
|
2667
|
+
confirmationTimeoutMs: Math.max(
|
|
2668
|
+
1_000,
|
|
2669
|
+
Math.trunc(int(flags, "confirm-timeout-ms", 15_000) ?? 15_000),
|
|
2670
|
+
),
|
|
2671
|
+
confirmationPollMs: Math.max(
|
|
2672
|
+
250,
|
|
2673
|
+
Math.trunc(int(flags, "confirm-poll-ms", 750) ?? 750),
|
|
2674
|
+
),
|
|
2185
2675
|
onProgress:
|
|
2186
2676
|
!flags.has("json") && flags.has("live")
|
|
2187
2677
|
? (event: any) => {
|
|
@@ -2190,9 +2680,17 @@ async function main() {
|
|
|
2190
2680
|
`SEND ${event.index}/${event.total} @${event.row.walletName} ` +
|
|
2191
2681
|
`${formatRaw(event.row.sendLamports, 9)} SOL\n`,
|
|
2192
2682
|
);
|
|
2683
|
+
} else if (event.stage === "wallet-pending") {
|
|
2684
|
+
emit(
|
|
2685
|
+
`WAIT ${event.index}/${event.total} @${event.row.walletName} ` +
|
|
2686
|
+
`attempt=${event.attempt}/${event.maxAttempts} ` +
|
|
2687
|
+
`${event.reason}\n`,
|
|
2688
|
+
);
|
|
2193
2689
|
} else if (event.stage === "wallet-done") {
|
|
2194
2690
|
emit(
|
|
2195
|
-
`OK ${event.index}/${event.total} @${event.row.walletName}
|
|
2691
|
+
`OK ${event.index}/${event.total} @${event.row.walletName} ` +
|
|
2692
|
+
`remaining=${formatRaw(event.remainingLamports, 9)} SOL` +
|
|
2693
|
+
`${event.receipt?.status ? ` status=${event.receipt.status}` : " state=already-settled"}\n`,
|
|
2196
2694
|
);
|
|
2197
2695
|
} else if (event.stage === "wallet-error") {
|
|
2198
2696
|
emit(
|
|
@@ -2204,7 +2702,9 @@ async function main() {
|
|
|
2204
2702
|
: undefined,
|
|
2205
2703
|
};
|
|
2206
2704
|
const plan = await planRegistrySolSweep(slrd, options);
|
|
2207
|
-
const sendRows = plan.rows.filter(
|
|
2705
|
+
const sendRows = plan.rows.filter(
|
|
2706
|
+
(row) => row.sendLamports > 0n && !row.skippedReason,
|
|
2707
|
+
);
|
|
2208
2708
|
const destinationLabel = destination.contactName
|
|
2209
2709
|
? `${destination.contactName} (${plan.destination})`
|
|
2210
2710
|
: destination.walletName
|
|
@@ -2275,6 +2775,12 @@ async function main() {
|
|
|
2275
2775
|
}
|
|
2276
2776
|
|
|
2277
2777
|
if (flags.has("simulate")) {
|
|
2778
|
+
if (!flags.has("json")) {
|
|
2779
|
+
emit(
|
|
2780
|
+
`PLAN eligible=${sendRows.length}/${plan.rows.length} ` +
|
|
2781
|
+
`total=${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2278
2784
|
const results = await simulateRegistrySolSweep(slrd, plan, options);
|
|
2279
2785
|
if (flags.has("json")) {
|
|
2280
2786
|
emit(json({ ...printablePlan, results }) + "\n");
|
|
@@ -2299,6 +2805,16 @@ async function main() {
|
|
|
2299
2805
|
return;
|
|
2300
2806
|
}
|
|
2301
2807
|
|
|
2808
|
+
if (!flags.has("json")) {
|
|
2809
|
+
emit(
|
|
2810
|
+
`PLAN eligible=${sendRows.length}/${plan.rows.length} ` +
|
|
2811
|
+
`total=${formatRaw(plan.totalSendLamports, 9)} SOL\n`,
|
|
2812
|
+
);
|
|
2813
|
+
if (sendRows.length === 0) {
|
|
2814
|
+
emit("Nothing to sweep.\n");
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2302
2818
|
const receipts = await executeRegistrySolSweep(slrd, plan, options);
|
|
2303
2819
|
if (flags.has("json")) {
|
|
2304
2820
|
emit(json({ ...printablePlan, receipts }) + "\n");
|
|
@@ -2339,150 +2855,183 @@ async function main() {
|
|
|
2339
2855
|
command === "unwrap-wsol" ||
|
|
2340
2856
|
(command === "unwrap" && values[0] === "wsol")
|
|
2341
2857
|
) {
|
|
2342
|
-
const
|
|
2343
|
-
|
|
2344
|
-
flags,
|
|
2345
|
-
"
|
|
2346
|
-
|
|
2858
|
+
const allWallets = flags.has("all-wallets");
|
|
2859
|
+
const explicitSelectors = [
|
|
2860
|
+
Boolean(flags.get("wallet")),
|
|
2861
|
+
csv(flags.get("wallets")).length > 0,
|
|
2862
|
+
Boolean(flags.get("group")),
|
|
2863
|
+
].filter(Boolean).length;
|
|
2864
|
+
if (allWallets && explicitSelectors > 0)
|
|
2865
|
+
throw new Error(
|
|
2866
|
+
"Use --all-wallets or a specific wallet selector, not both.",
|
|
2867
|
+
);
|
|
2868
|
+
const targets = allWallets
|
|
2869
|
+
? {
|
|
2870
|
+
mode: "all-wallets" as const,
|
|
2871
|
+
refs: slrd.wallets.list().map((wallet) => wallet.name),
|
|
2872
|
+
}
|
|
2873
|
+
: targetWallets(
|
|
2874
|
+
slrd,
|
|
2875
|
+
flags,
|
|
2876
|
+
"Usage: slrd unwrap-wsol (--all-wallets | --wallet <wallet> | --wallets <w1,w2> | --group <group>).",
|
|
2877
|
+
);
|
|
2347
2878
|
const destination = flags.get("destination");
|
|
2348
2879
|
if (destination && targets.refs.length !== 1)
|
|
2349
2880
|
throw new Error(
|
|
2350
2881
|
"--destination is only supported with a single --wallet unwrap.",
|
|
2351
2882
|
);
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
skipPreflight:
|
|
2358
|
-
flags.has("skip-preflight") || flags.has("skip-simulation"),
|
|
2359
|
-
};
|
|
2360
|
-
|
|
2361
|
-
const multiWallet = targets.refs.length > 1;
|
|
2883
|
+
const skipMissing =
|
|
2884
|
+
allWallets || flags.has("ignore-missing") || flags.has("skip-missing");
|
|
2885
|
+
const via = flags.get("sender") ?? "rpc";
|
|
2886
|
+
const skipSimulation = flags.has("skip-simulation");
|
|
2887
|
+
const skipPreflight = flags.has("skip-preflight") || skipSimulation;
|
|
2362
2888
|
const failFast = flags.has("fail-fast");
|
|
2889
|
+
const results: Array<Record<string, unknown>> = [];
|
|
2363
2890
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2891
|
+
for (const walletRef of targets.refs) {
|
|
2892
|
+
const resolved = slrd.resolveWallet(walletRef);
|
|
2893
|
+
const address = resolved.address.toBase58();
|
|
2894
|
+
const label = resolved.row?.name ? `@${resolved.row.name}` : address;
|
|
2895
|
+
try {
|
|
2896
|
+
const accounts = (await slrd.tokenAccounts(address)).filter(
|
|
2897
|
+
(account) => account.mint === NATIVE_SOL_MINT,
|
|
2898
|
+
);
|
|
2899
|
+
if (!accounts.length) {
|
|
2900
|
+
if (!skipMissing && targets.refs.length === 1)
|
|
2901
|
+
throw new Error(`No WSOL token accounts exist for ${address}`);
|
|
2902
|
+
results.push({
|
|
2903
|
+
wallet: walletRef,
|
|
2904
|
+
address,
|
|
2905
|
+
ok: true,
|
|
2906
|
+
skipped: "missing-wsol",
|
|
2907
|
+
});
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
let closed = 0;
|
|
2912
|
+
let unwrappedRaw = 0n;
|
|
2913
|
+
const accountResults: Array<Record<string, unknown>> = [];
|
|
2914
|
+
for (const account of accounts) {
|
|
2915
|
+
if (account.closeAuthority && account.closeAuthority !== address) {
|
|
2916
|
+
const error = `WSOL account ${account.address} has close authority ${account.closeAuthority}`;
|
|
2917
|
+
accountResults.push({
|
|
2918
|
+
account: account.address,
|
|
2919
|
+
ok: false,
|
|
2920
|
+
error,
|
|
2389
2921
|
});
|
|
2922
|
+
if (failFast) throw new Error(error);
|
|
2390
2923
|
continue;
|
|
2391
2924
|
}
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
try {
|
|
2395
|
-
if (flags.has("simulate-only")) {
|
|
2396
|
-
const plan = await slrd
|
|
2925
|
+
try {
|
|
2926
|
+
const composer = slrd
|
|
2397
2927
|
.tx(walletRef)
|
|
2398
|
-
.
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2928
|
+
.closeTokenAccountAddress(
|
|
2929
|
+
account.address,
|
|
2930
|
+
account.tokenProgram,
|
|
2931
|
+
{
|
|
2932
|
+
destination,
|
|
2933
|
+
},
|
|
2934
|
+
);
|
|
2935
|
+
if (flags.has("simulate-only")) {
|
|
2936
|
+
const plan = await composer.build();
|
|
2937
|
+
const simulation = await slrd.simulatePlan(plan);
|
|
2938
|
+
if (!simulation.success) throw new Error("simulation failed");
|
|
2939
|
+
accountResults.push({
|
|
2940
|
+
account: account.address,
|
|
2941
|
+
amountRaw: account.amountRaw,
|
|
2942
|
+
ok: true,
|
|
2943
|
+
mode: "simulation",
|
|
2944
|
+
simulation,
|
|
2945
|
+
});
|
|
2946
|
+
} else {
|
|
2947
|
+
const receipt = await composer.send({
|
|
2948
|
+
via,
|
|
2949
|
+
kind: "unwrap-wsol:account",
|
|
2950
|
+
skipSimulation,
|
|
2951
|
+
skipPreflight,
|
|
2952
|
+
});
|
|
2953
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
2954
|
+
let closedOnChain = false;
|
|
2955
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
2956
|
+
const current = await slrd
|
|
2957
|
+
.connection()
|
|
2958
|
+
.getAccountInfo(
|
|
2959
|
+
new PublicKey(account.address),
|
|
2960
|
+
"confirmed",
|
|
2961
|
+
);
|
|
2962
|
+
if (!current || current.lamports === 0) {
|
|
2963
|
+
closedOnChain = true;
|
|
2964
|
+
break;
|
|
2965
|
+
}
|
|
2966
|
+
if (attempt < 5)
|
|
2967
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2968
|
+
}
|
|
2969
|
+
if (!closedOnChain)
|
|
2970
|
+
throw new Error(
|
|
2971
|
+
`WSOL account ${account.address} still exists after close`,
|
|
2972
|
+
);
|
|
2973
|
+
accountResults.push({
|
|
2974
|
+
account: account.address,
|
|
2975
|
+
amountRaw: account.amountRaw,
|
|
2976
|
+
ok: true,
|
|
2977
|
+
receipt,
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
closed += 1;
|
|
2981
|
+
unwrappedRaw += BigInt(account.amountRaw);
|
|
2982
|
+
} catch (error) {
|
|
2983
|
+
const message =
|
|
2984
|
+
error instanceof Error ? error.message : String(error);
|
|
2985
|
+
accountResults.push({
|
|
2986
|
+
account: account.address,
|
|
2987
|
+
ok: false,
|
|
2988
|
+
error: message,
|
|
2415
2989
|
});
|
|
2990
|
+
if (failFast) throw error;
|
|
2416
2991
|
}
|
|
2417
|
-
} catch (error) {
|
|
2418
|
-
const message =
|
|
2419
|
-
error instanceof Error ? error.message : String(error);
|
|
2420
|
-
results.push({
|
|
2421
|
-
wallet: walletRef,
|
|
2422
|
-
address,
|
|
2423
|
-
ok: false,
|
|
2424
|
-
error: message,
|
|
2425
|
-
});
|
|
2426
|
-
process.stderr.write(
|
|
2427
|
-
`${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
|
|
2428
|
-
);
|
|
2429
2992
|
}
|
|
2993
|
+
const failedAccounts = accountResults.filter(
|
|
2994
|
+
(row) => row.ok === false,
|
|
2995
|
+
).length;
|
|
2996
|
+
results.push({
|
|
2997
|
+
wallet: walletRef,
|
|
2998
|
+
address,
|
|
2999
|
+
ok: failedAccounts === 0,
|
|
3000
|
+
closed,
|
|
3001
|
+
failedAccounts,
|
|
3002
|
+
unwrappedRaw,
|
|
3003
|
+
accounts: accountResults,
|
|
3004
|
+
});
|
|
3005
|
+
} catch (error) {
|
|
3006
|
+
const message =
|
|
3007
|
+
error instanceof Error ? error.message : String(error);
|
|
3008
|
+
results.push({
|
|
3009
|
+
wallet: walletRef,
|
|
3010
|
+
address,
|
|
3011
|
+
ok: false,
|
|
3012
|
+
error: message,
|
|
3013
|
+
});
|
|
3014
|
+
process.stderr.write(
|
|
3015
|
+
`${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
|
|
3016
|
+
);
|
|
3017
|
+
if (failFast) throw error;
|
|
2430
3018
|
}
|
|
2431
|
-
|
|
2432
|
-
const succeeded = results.filter(
|
|
2433
|
-
(row) => row.ok === true && !row.skipped,
|
|
2434
|
-
).length;
|
|
2435
|
-
const skipped = results.filter((row) => Boolean(row.skipped)).length;
|
|
2436
|
-
const failed = results.filter((row) => row.ok === false).length;
|
|
2437
|
-
|
|
2438
|
-
emit(
|
|
2439
|
-
json({
|
|
2440
|
-
mode: flags.has("simulate-only")
|
|
2441
|
-
? "wallet-by-wallet-simulation"
|
|
2442
|
-
: "wallet-by-wallet",
|
|
2443
|
-
target: targets,
|
|
2444
|
-
summary: {
|
|
2445
|
-
total: results.length,
|
|
2446
|
-
succeeded,
|
|
2447
|
-
skipped,
|
|
2448
|
-
failed,
|
|
2449
|
-
},
|
|
2450
|
-
results,
|
|
2451
|
-
}) + "\n",
|
|
2452
|
-
);
|
|
2453
|
-
return;
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
// Single-wallet unwrap remains fail-fast. --fail-fast also preserves the
|
|
2457
|
-
// old batch behavior for callers that explicitly want all-or-nothing.
|
|
2458
|
-
if (flags.has("simulate-only")) {
|
|
2459
|
-
const plans =
|
|
2460
|
-
targets.refs.length === 1
|
|
2461
|
-
? [
|
|
2462
|
-
await slrd
|
|
2463
|
-
.tx(targets.refs[0]!)
|
|
2464
|
-
.unwrapWsol({
|
|
2465
|
-
destination,
|
|
2466
|
-
skipMissing: unwrapOptions.skipMissing,
|
|
2467
|
-
})
|
|
2468
|
-
.build(),
|
|
2469
|
-
]
|
|
2470
|
-
: await slrd
|
|
2471
|
-
.composeMany(targets.refs)
|
|
2472
|
-
.unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
|
|
2473
|
-
.build();
|
|
2474
|
-
const results = await Promise.all(
|
|
2475
|
-
plans.map((plan) => slrd.simulatePlan(plan)),
|
|
2476
|
-
);
|
|
2477
|
-
emit(json({ mode: "simulation", target: targets, results }) + "\n");
|
|
2478
|
-
return;
|
|
2479
3019
|
}
|
|
2480
3020
|
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
3021
|
+
const succeeded = results.filter(
|
|
3022
|
+
(row) => row.ok === true && !row.skipped,
|
|
3023
|
+
).length;
|
|
3024
|
+
const skipped = results.filter((row) => Boolean(row.skipped)).length;
|
|
3025
|
+
const failed = results.filter((row) => row.ok === false).length;
|
|
3026
|
+
emit(
|
|
3027
|
+
json({
|
|
3028
|
+
mode: flags.has("simulate-only") ? "simulation" : "live",
|
|
3029
|
+
target: targets,
|
|
3030
|
+
summary: { total: results.length, succeeded, skipped, failed },
|
|
3031
|
+
results,
|
|
3032
|
+
}) + "\n",
|
|
3033
|
+
);
|
|
3034
|
+
if (failed) process.exitCode = 1;
|
|
2486
3035
|
return;
|
|
2487
3036
|
}
|
|
2488
3037
|
if (command === "rewards") {
|