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