@solard/cli 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,11 +1,66 @@
1
1
  #!/usr/bin/env bun
2
- import { listScripts, runScript, type TokenRow } from "@solard/sdk";
2
+ import {
3
+ addExternalContact,
4
+ configureSolardMeasure,
5
+ createSolardMeasureCollector,
6
+ executeRegistrySolSweep,
7
+ executeRegistryTokenLiquidation,
8
+ findExternalContact,
9
+ getSolardRpcStats,
10
+ listExternalContacts,
11
+ loadWalletAssetPortfolio,
12
+ planRegistrySolSweep,
13
+ planRegistryTokenLiquidation,
14
+ removeExternalContact,
15
+ resetSolardRpcStats,
16
+ resolveTokenMintForPolicy,
17
+ runScript,
18
+ listScripts,
19
+ simulateRegistrySolSweep,
20
+ simulateRegistryTokenLiquidation,
21
+ wrappedSolAta,
22
+ type TokenRow,
23
+ } from "@solard/sdk";
24
+
25
+ import { handleMeteoraCommand } from "./meteora-commands.ts";
3
26
 
4
27
  function emit(value: string): void {
5
28
  process.stdout.write(value);
6
29
  }
7
30
 
8
31
  const OWL = "🦉";
32
+ const NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112";
33
+ const CANONICAL_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
34
+
35
+ async function quoteJupiterSwap(
36
+ args: Record<string, unknown>,
37
+ ): Promise<unknown> {
38
+ const sdk = (await import("@solard/sdk")) as Record<string, unknown>;
39
+ const fn = sdk.quoteJupiterSwap;
40
+ if (typeof fn !== "function") {
41
+ throw new Error(
42
+ "@solard/sdk does not export quoteJupiterSwap required by `slrd swap`.",
43
+ );
44
+ }
45
+ return await (fn as (input: Record<string, unknown>) => Promise<unknown>)(
46
+ args,
47
+ );
48
+ }
49
+
50
+ async function executeJupiterSwap(
51
+ args: Record<string, unknown>,
52
+ ): Promise<unknown> {
53
+ const sdk = (await import("@solard/sdk")) as Record<string, unknown>;
54
+ const fn = sdk.executeJupiterSwap;
55
+ if (typeof fn !== "function") {
56
+ throw new Error(
57
+ "@solard/sdk does not export executeJupiterSwap required by `slrd swap`.",
58
+ );
59
+ }
60
+ return await (fn as (input: Record<string, unknown>) => Promise<unknown>)(
61
+ args,
62
+ );
63
+ }
9
64
  type Flags = Map<string, string>;
10
65
  function args(input: string[]): { values: string[]; flags: Flags } {
11
66
  const values: string[] = [],
@@ -78,6 +133,87 @@ function duration(value: string | undefined, fallbackMs: number): number {
78
133
  ];
79
134
  return Math.floor(n * scale);
80
135
  }
136
+
137
+ function txAccountKeys(tx: any): string[] {
138
+ const message = tx?.transaction?.message;
139
+ if (!message) return [];
140
+ const toStringKey = (value: any): string => {
141
+ if (!value) return "";
142
+ if (typeof value === "string") return value;
143
+ if (typeof value?.toBase58 === "function") return value.toBase58();
144
+ if (typeof value?.pubkey?.toBase58 === "function")
145
+ return value.pubkey.toBase58();
146
+ if (typeof value?.pubkey === "string") return value.pubkey;
147
+ return String(value);
148
+ };
149
+ if (Array.isArray(message.accountKeys))
150
+ return message.accountKeys.map(toStringKey);
151
+ if (Array.isArray(message.staticAccountKeys)) {
152
+ const keys = message.staticAccountKeys.map(toStringKey);
153
+ const loaded = tx?.meta?.loadedAddresses;
154
+ if (Array.isArray(loaded?.writable))
155
+ keys.push(...loaded.writable.map(toStringKey));
156
+ if (Array.isArray(loaded?.readonly))
157
+ keys.push(...loaded.readonly.map(toStringKey));
158
+ return keys;
159
+ }
160
+ try {
161
+ if (typeof message.getAccountKeys === "function") {
162
+ const accountKeys = message.getAccountKeys({
163
+ accountKeysFromLookups: tx?.meta?.loadedAddresses,
164
+ });
165
+ const keys: string[] = [];
166
+ for (let i = 0; i < Number(accountKeys?.length ?? 0); i += 1)
167
+ keys.push(toStringKey(accountKeys.get(i)));
168
+ return keys;
169
+ }
170
+ } catch {}
171
+ return [];
172
+ }
173
+
174
+ function txWalletTokenBalances(
175
+ tx: any,
176
+ owner: string,
177
+ ): Map<string, { decimals: number; pre: bigint; post: bigint }> {
178
+ const out = new Map<
179
+ string,
180
+ { decimals: number; pre: bigint; post: bigint }
181
+ >();
182
+ const add = (side: "pre" | "post", rows: any[] | null | undefined) => {
183
+ for (const row of rows ?? []) {
184
+ const rowOwner =
185
+ typeof row?.owner === "string"
186
+ ? row.owner
187
+ : typeof row?.owner?.toBase58 === "function"
188
+ ? row.owner.toBase58()
189
+ : "";
190
+ if (rowOwner !== owner || typeof row?.mint !== "string") continue;
191
+ const amount = row?.uiTokenAmount?.amount;
192
+ const decimals = Number(row?.uiTokenAmount?.decimals ?? 0);
193
+ if (typeof amount !== "string" || !/^\d+$/.test(amount)) continue;
194
+ const existing = out.get(row.mint) ?? { decimals, pre: 0n, post: 0n };
195
+ existing.decimals = decimals;
196
+ existing[side] += BigInt(amount);
197
+ out.set(row.mint, existing);
198
+ }
199
+ };
200
+ add("pre", tx?.meta?.preTokenBalances);
201
+ add("post", tx?.meta?.postTokenBalances);
202
+ return out;
203
+ }
204
+
205
+ function formatSignedRaw(value: bigint, decimals: number): string {
206
+ const sign = value > 0n ? "+" : value < 0n ? "-" : "";
207
+ const abs = value < 0n ? -value : value;
208
+ return `${sign}${formatRaw(abs, decimals)}`;
209
+ }
210
+ function formatDurationMs(value: number): string {
211
+ if (!Number.isFinite(value) || value < 0) return "0ms";
212
+ if (value < 1_000) return `${value.toFixed(value < 10 ? 1 : 0)}ms`;
213
+ if (value < 60_000) return `${(value / 1_000).toFixed(2)}s`;
214
+ return `${(value / 60_000).toFixed(2)}m`;
215
+ }
216
+
81
217
  function formatPrice(value: number | null | undefined): string {
82
218
  if (value == null || !Number.isFinite(value)) return "n/a";
83
219
  return value >= 0.001
@@ -114,25 +250,89 @@ function targetWallets(
114
250
  group,
115
251
  };
116
252
  }
253
+ function resolveDestinationRef(
254
+ slrd: { resolveWallet(ref: string): { address: { toBase58(): string } } },
255
+ value: string,
256
+ ): {
257
+ input: string;
258
+ address: string;
259
+ contactName?: string;
260
+ walletName?: string;
261
+ } {
262
+ const input = value.trim();
263
+ const contact = findExternalContact(input);
264
+
265
+ let walletAddress: string | null = null;
266
+ try {
267
+ walletAddress = slrd.resolveWallet(input).address.toBase58();
268
+ } catch {
269
+ walletAddress = null;
270
+ }
271
+
272
+ if (contact && walletAddress && contact.address !== walletAddress) {
273
+ throw new Error(
274
+ `Ambiguous destination ${input}: external contact @${contact.name} points to ${contact.address}, ` +
275
+ `but a stored signing wallet resolves to ${walletAddress}. Rename one of them.`,
276
+ );
277
+ }
278
+
279
+ if (contact) {
280
+ return {
281
+ input,
282
+ address: contact.address,
283
+ contactName: contact.name,
284
+ };
285
+ }
286
+
287
+ if (walletAddress) {
288
+ return {
289
+ input,
290
+ address: walletAddress,
291
+ walletName: input.replace(/^@/, ""),
292
+ };
293
+ }
294
+
295
+ return { input, address: input };
296
+ }
297
+
117
298
  function help(): string {
118
299
  return `${OWL} slrd — multi-wallet Solana CLI + SDK for traders and AI agents
119
300
 
120
301
  Wallets and tokens
121
- slrd wallets create [name] Generate and persist an encrypted Solana wallet
122
- slrd wallet create [name] Alias for wallets create
302
+ slrd wallet create [name] Generate and persist an encrypted Solana wallet
123
303
  slrd import <private_key> [name]
124
304
  cat key.json | slrd import --stdin [name]
125
- slrd wallets [--token <token>] [--addresses-only] Show SOL and registered-token balances
305
+
306
+ External contacts (public addresses only; never signing wallets or group members)
307
+ slrd contact add <name> <address> [--force]
308
+ slrd contact list
309
+ slrd contact show <name|address>
310
+ slrd contact remove <name|address>
311
+ slrd wallets [--wallet <wallet>] [--group <name>] [--token <token> | --tokens] [--token-totals] [--only-with-tokens] [--rpc-concurrency <n>] [--rpc-delay-ms <n>] [--addresses-only]
312
+ Show SOL by default; token scans are opt-in
313
+ slrd balances --wallet <wallet> [--token <token>] [--show-zero] Show SOL + all SPL token balances for one wallet
314
+ slrd fees --wallet <wallet> [--since 30m] [--limit 100] Sum actual on-chain transaction fees paid by this wallet
315
+ slrd sol-flow --wallet <wallet> [--since 30m] [--limit 100] Audit native SOL + WSOL + SPL deltas from actual transactions
126
316
  slrd token <token_ca> [name] [--metadata-json <json>]
127
317
  slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
128
318
  slrd token refresh <token|ca>
129
319
  slrd tokens
130
320
 
321
+ Vanity mints
322
+ slrd vanity --suffix pump --out .\\mint.json [--count <n>]
323
+ slrd vanity pool generate --suffix pump --count <n> [--max-attempts <n>] Generate encrypted mint keypairs into the canonical DB
324
+ slrd vanity pool list [--suffix pump] [--status available|reserved|used]
325
+ slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
326
+ launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
327
+
131
328
  Metadata and launching
132
329
  slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--alias <name>] [--live] [--skip-simulation]
133
330
  [--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
134
331
  slrd metadata upload --image <local_path> --name <name> --symbol <symbol> --description <text> [--provider pump-frontend|pinata]
135
332
  [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
333
+ slrd vamp <source-mint> --creator <wallet> [--name <name>] [--symbol <symbol>] [--image <path-or-url>] [--description <text>]
334
+ [--website <url>] [--twitter <url>] [--telegram <url>] [--video <url>] [--uri <metadata-uri>]
335
+ [--buy-plan <file>] [--mint-pool pump] [--submit-mode jito-bundle] [--live] [--skip-simulation]
136
336
  slrd deploy pump --wallet <wallet> --name <name> --symbol <symbol> (--uri <metadata_uri> | --image <local_path> --description <text>) [--alias <name>] [--live]
137
337
  [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
138
338
 
@@ -142,12 +342,32 @@ Prices
142
342
  slrd price average <token|ca> --period 15m Average stored samples in a period
143
343
  slrd price watch <token|ca...> [--interval 1s] [--period 1m]
144
344
 
345
+ Transfers and consolidation
346
+ slrd transfer <contact|wallet|address> --wallet <source-wallet> --sol <amount> [--simulate-only]
347
+ slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]
348
+ Without --wallets, sweep considers all stored signing wallets
349
+
350
+ Token liquidation
351
+ slrd liquidate tokens --except <token|mint> [--wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--simulate | --live]
352
+ Sell supported tokens except protected mint(s); unwrap WSOL; close unprotected zero-balance token accounts and reclaim rent
353
+
354
+ RPC
355
+ All Solard JSON-RPC traffic is globally rate-limited to 5 req/s by default.
356
+ Override only when your provider allows it: SLRD_RPC_MAX_RPS=<n>
357
+ Jupiter fallback: keyless 0.5 req/s, API-key default 1 req/s; override SLRD_JUPITER_MAX_RPS=<n>
358
+
359
+ Diagnostics
360
+ Commands collect measure-fn telemetry without streaming it to the terminal.
361
+ --measure Print top aggregate timings after the normal command result
362
+ --measure-stream Restore raw live measure-fn output for low-level debugging
363
+
145
364
  Trading
365
+ slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live] Quote or execute SOL -> token through Jupiter
146
366
  slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
147
367
  slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
148
368
  slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
149
369
  slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
150
- slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
370
+ slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--continue-on-error] [--simulate-only]
151
371
  slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
152
372
 
153
373
  Scripts (strategies stay outside the kernel)
@@ -155,6 +375,23 @@ Scripts (strategies stay outside the kernel)
155
375
  slrd run <name-or-path> [script flags...] Execute a script that imports slrd
156
376
  slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
157
377
 
378
+ Meteora DLMM
379
+ slrd meteora discover --timeframe 30m --sort fee-active-tvl --limit 20
380
+ slrd meteora opportunities --timeframe 30m --sort flow-inactive --min-inactive-pct 70 --limit 20
381
+ slrd meteora token-pools <mint|symbol> --timeframe 30m
382
+ slrd meteora pools --timeframe 30m --sort fee-tvl --limit 20
383
+ slrd meteora pool <pool> [--timeframe 30m]
384
+ slrd meteora candles <pool> [--timeframe 5m]
385
+ slrd meteora positions --wallet <wallet|address>
386
+ slrd meteora open <pool> --wallet <wallet> --sol 0.1 --bins 40 [--strategy spot] [--live]
387
+ slrd meteora move <position> --wallet <wallet> [--bins 10] [--live]
388
+ slrd meteora migrate <position> --wallet <wallet> --to-pool <pool> [--bins 10] [--live]
389
+ slrd meteora add|remove|claim|close <position> --wallet <wallet> [--live]
390
+ slrd meteora close-all <pool> --wallet <wallet> [--live]
391
+ slrd meteora quote <pool> (--in-x N|--in-y N|--out-x N|--out-y N)
392
+ slrd meteora swap <pool> --wallet <wallet> (--in-x N|--in-y N|--out-x N|--out-y N) [--live]
393
+ slrd meteora help Full Meteora command reference
394
+
158
395
  Groups and agents
159
396
  slrd group create <name> [description]
160
397
  slrd group add <group> <wallet> [weight_bps]
@@ -181,7 +418,7 @@ Transactions and ALTs
181
418
  slrd alt extend <address> --wallet <wallet> <account...>
182
419
 
183
420
  Environment
184
- SLRD_DB_PATH shared SDK/CLI database; default ~/.solard/solard.sqlite (SOLARD_DB_PATH alias supported)
421
+ SLRD_DB_PATH shared SDK/CLI database; default ./slrd.db (SOLARD_DB_PATH alias supported)
185
422
  SLRD_MASTER_KEY required to create/import/decrypt stored wallets
186
423
  RPC_ENDPOINT required only for chain operations
187
424
  HELIUS_SENDER_URL regional/global Helius Sender endpoint used by launch scripts
@@ -199,6 +436,62 @@ Environment
199
436
  async function main() {
200
437
  const [command, ...rest] = process.argv.slice(2);
201
438
  const { values, flags } = args(rest);
439
+
440
+ const commandStartedAt = performance.now();
441
+ const measureCollector = createSolardMeasureCollector();
442
+ resetSolardRpcStats();
443
+
444
+ // Keep measurement active, but route events away from terminal output.
445
+ // --measure adds a detailed aggregate at the end; --measure-stream restores
446
+ // measure-fn's raw live logger for low-level debugging.
447
+ const streamMeasures =
448
+ flags.has("measure-stream") ||
449
+ process.env.SLRD_MEASURE_STREAM === "1" ||
450
+ process.env.SLRD_MEASURE_STREAM === "true";
451
+ configureSolardMeasure(
452
+ streamMeasures
453
+ ? { silent: false, logger: null }
454
+ : { silent: false, logger: measureCollector.logger },
455
+ );
456
+
457
+ let perfPrinted = false;
458
+ const printPerfSummary = () => {
459
+ if (perfPrinted) return;
460
+ perfPrinted = true;
461
+
462
+ const measures = measureCollector.snapshot();
463
+ const rpc = getSolardRpcStats();
464
+ const wallMs = Math.max(0, performance.now() - commandStartedAt);
465
+
466
+ if (
467
+ measures.completed === 0 &&
468
+ measures.annotations === 0 &&
469
+ rpc.requestStarts === 0
470
+ ) {
471
+ return;
472
+ }
473
+
474
+ emit(
475
+ `PERF wall=${formatDurationMs(wallMs)} ` +
476
+ `measure=${measures.completed} (${measures.errors} err) ` +
477
+ `rpc=${rpc.requestStarts} 429=${rpc.rateLimited429} ` +
478
+ `retries=${rpc.retries429} limit=${rpc.maxRps}/s\n`,
479
+ );
480
+
481
+ if (flags.has("measure") && !streamMeasures) {
482
+ for (const row of measures.labels.slice(0, 10)) {
483
+ emit(
484
+ ` ${row.label} calls=${row.calls} ` +
485
+ `total=${formatDurationMs(row.totalMs)} ` +
486
+ `max=${formatDurationMs(row.maxMs)}` +
487
+ `${row.errors ? ` errors=${row.errors}` : ""}\n`,
488
+ );
489
+ }
490
+ }
491
+ };
492
+
493
+ process.once("beforeExit", printPerfSummary);
494
+
202
495
  if (!command || command === "help" || command === "--help") {
203
496
  emit(help());
204
497
  return;
@@ -214,10 +507,84 @@ async function main() {
214
507
  }
215
508
  if (command === "vanity") {
216
509
  const {
510
+ addVanityMintToPool,
511
+ cleanVanitySuffix,
512
+ defaultVanityMaxAttempts,
217
513
  generateMintKeypairWithSuffix,
514
+ listVanityMintPool,
515
+ releaseVanityMintReservation,
218
516
  saveMintKeypairFile,
219
- cleanVanitySuffix,
220
517
  } = await import("@solard/sdk");
518
+
519
+ if (values[0] === "pool") {
520
+ const action = values[1] ?? "list";
521
+
522
+ if (action === "generate") {
523
+ const suffix = cleanVanitySuffix(need(flags, "suffix"));
524
+ const count = int(flags, "count", 1)!;
525
+ if (count <= 0) throw new Error("--count must be greater than zero");
526
+ const results = [];
527
+ for (let i = 0; i < count; i++) {
528
+ const found = await generateMintKeypairWithSuffix({
529
+ suffix,
530
+ workers: int(flags, "workers"),
531
+ maxAttempts: int(
532
+ flags,
533
+ "max-attempts",
534
+ defaultVanityMaxAttempts(suffix),
535
+ )!,
536
+ timeoutMs: int(flags, "timeout-ms", 0)!,
537
+ reportEvery: int(flags, "report-every", 1_000_000)!,
538
+ onProgress: (p) =>
539
+ emit(
540
+ `${OWL} pool grinding ${p.suffix} [${i + 1}/${count}]: ${p.attempts} attempts, ${p.ratePerSecond}/s\n`,
541
+ ),
542
+ });
543
+ results.push({
544
+ ...addVanityMintToPool(found.mint, suffix),
545
+ attempts: found.attempts,
546
+ elapsedMs: found.elapsedMs,
547
+ });
548
+ }
549
+ emit(json(results) + "\n");
550
+ return;
551
+ }
552
+
553
+ if (action === "list") {
554
+ const status = flags.get("status") as
555
+ "available" | "reserved" | "used" | undefined;
556
+ if (
557
+ status &&
558
+ status !== "available" &&
559
+ status !== "reserved" &&
560
+ status !== "used"
561
+ ) {
562
+ throw new Error("--status must be available, reserved, or used");
563
+ }
564
+ emit(
565
+ json(
566
+ listVanityMintPool({
567
+ suffix: flags.get("suffix"),
568
+ status,
569
+ }),
570
+ ) + "\n",
571
+ );
572
+ return;
573
+ }
574
+
575
+ if (action === "release") {
576
+ const address = values[2] ?? flags.get("address");
577
+ if (!address)
578
+ throw new Error("Usage: slrd vanity pool release <mint-address>");
579
+ emit(json(releaseVanityMintReservation(address)) + "\n");
580
+ return;
581
+ }
582
+
583
+ throw new Error(
584
+ "Usage: slrd vanity pool <generate|list|release> [options]",
585
+ );
586
+ }
587
+
221
588
  const suffix = cleanVanitySuffix(need(flags, "suffix"));
222
589
  const out = need(flags, "out");
223
590
  const count = int(flags, "count", 1)!;
@@ -226,7 +593,11 @@ async function main() {
226
593
  const found = await generateMintKeypairWithSuffix({
227
594
  suffix,
228
595
  workers: int(flags, "workers"),
229
- maxAttempts: int(flags, "max-attempts", 25_000_000)!,
596
+ maxAttempts: int(
597
+ flags,
598
+ "max-attempts",
599
+ defaultVanityMaxAttempts(suffix),
600
+ )!,
230
601
  timeoutMs: int(flags, "timeout-ms", 0)!,
231
602
  reportEvery: int(flags, "report-every", 1_000_000)!,
232
603
  onProgress: (p) =>
@@ -286,6 +657,18 @@ async function main() {
286
657
  });
287
658
  return;
288
659
  }
660
+ if (command === "vamp") {
661
+ const { runPumpVampFromArgs } = await import("./pump/vamp-cli.ts");
662
+ await runPumpVampFromArgs(rest, {
663
+ defaultSubmitMode: "after-deploy-processed",
664
+ defaultDeploymentPriorityMicroLamports: 500_000,
665
+ defaultBuyerPriorityMicroLamports: 1_500_000,
666
+ defaultSlippageBps: 1_500,
667
+ persistOnLive: true,
668
+ report: (label, value) => emit(`${label}: ${json(value)}\n`),
669
+ });
670
+ return;
671
+ }
289
672
  if (command === "jito" && values[0] === "tip-accounts") {
290
673
  const endpoint = (
291
674
  flags.get("endpoint") ??
@@ -330,10 +713,133 @@ async function main() {
330
713
  ]);
331
714
  const slrd = createTraderSolard();
332
715
  try {
333
- if (
334
- (command === "wallet" || command === "wallets") &&
335
- values[0] === "create"
336
- ) {
716
+ if (command === "meteora") {
717
+ await handleMeteoraCommand({ slrd, values, flags, emit });
718
+ return;
719
+ }
720
+
721
+ if (command === "swap") {
722
+ const outputRef = values[0]?.trim();
723
+ if (!outputRef) {
724
+ throw new Error(
725
+ "Usage: slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live]",
726
+ );
727
+ }
728
+
729
+ const wallet = need(flags, "wallet");
730
+ const amountSol = need(flags, "sol");
731
+ const amountRaw = sol(amountSol).raw;
732
+ if (amountRaw <= 0n) throw new Error("--sol must be greater than zero");
733
+
734
+ const normalizedOutput = outputRef.replace(/^\$/, "").trim();
735
+ const outputMint =
736
+ normalizedOutput.toUpperCase() === "USDC"
737
+ ? CANONICAL_USDC_MINT
738
+ : resolveTokenMintForPolicy(slrd, normalizedOutput);
739
+
740
+ if (outputMint === NATIVE_SOL_MINT) {
741
+ throw new Error("Swap output is SOL; choose a different token mint.");
742
+ }
743
+
744
+ if (!flags.has("live")) {
745
+ const quote = await quoteJupiterSwap({
746
+ inputMint: NATIVE_SOL_MINT,
747
+ outputMint,
748
+ amountRaw,
749
+ });
750
+ emit(
751
+ json({
752
+ mode: "quote",
753
+ wallet,
754
+ input: { symbol: "SOL", mint: NATIVE_SOL_MINT, amountSol },
755
+ output: { ref: outputRef, mint: outputMint },
756
+ quote,
757
+ hint: "Re-run with --live to execute this swap.",
758
+ }) + "\n",
759
+ );
760
+ return;
761
+ }
762
+
763
+ const signer = slrd.signer(wallet);
764
+ const result = await executeJupiterSwap({
765
+ inputMint: NATIVE_SOL_MINT,
766
+ outputMint,
767
+ amountRaw,
768
+ signer,
769
+ });
770
+ emit(
771
+ json({
772
+ mode: "live",
773
+ wallet,
774
+ input: { symbol: "SOL", mint: NATIVE_SOL_MINT, amountSol },
775
+ output: { ref: outputRef, mint: outputMint },
776
+ result,
777
+ }) + "\n",
778
+ );
779
+ return;
780
+ }
781
+ if (command === "contact" || command === "contacts") {
782
+ const action = values[0] ?? "list";
783
+
784
+ if (action === "add") {
785
+ const name = values[1];
786
+ const address = values[2];
787
+ if (!name || !address) {
788
+ throw new Error("Usage: slrd contact add <name> <address> [--force]");
789
+ }
790
+
791
+ let walletCollision: string | null = null;
792
+ try {
793
+ walletCollision = slrd.resolveWallet(name).address.toBase58();
794
+ } catch {
795
+ walletCollision = null;
796
+ }
797
+ if (walletCollision && walletCollision !== address) {
798
+ throw new Error(
799
+ `Cannot create external contact @${name}: a stored signing wallet already uses that name (${walletCollision}).`,
800
+ );
801
+ }
802
+
803
+ const contact = addExternalContact(name, address, {
804
+ overwrite: flags.has("force") || flags.has("overwrite"),
805
+ });
806
+ emit(`${OWL} contact @${contact.name}\t${contact.address}\n`);
807
+ return;
808
+ }
809
+
810
+ if (action === "list") {
811
+ const contacts = listExternalContacts();
812
+ if (flags.has("json")) {
813
+ emit(json(contacts) + "\n");
814
+ return;
815
+ }
816
+ for (const contact of contacts) {
817
+ emit(`@${contact.name}\t${contact.address}\n`);
818
+ }
819
+ return;
820
+ }
821
+
822
+ if (action === "show") {
823
+ const ref = values[1];
824
+ if (!ref) throw new Error("Usage: slrd contact show <name|address>");
825
+ const contact = findExternalContact(ref);
826
+ if (!contact) throw new Error(`Unknown external contact: ${ref}`);
827
+ emit(json(contact) + "\n");
828
+ return;
829
+ }
830
+
831
+ if (action === "remove" || action === "delete" || action === "rm") {
832
+ const ref = values[1];
833
+ if (!ref) throw new Error("Usage: slrd contact remove <name|address>");
834
+ const removed = removeExternalContact(ref);
835
+ emit(`${OWL} removed contact @${removed.name}\t${removed.address}\n`);
836
+ return;
837
+ }
838
+
839
+ throw new Error("Usage: slrd contact <add|list|show|remove> [arguments]");
840
+ }
841
+
842
+ if (command === "wallet" && values[0] === "create") {
337
843
  const wallet = slrd.createWallet(values[1]);
338
844
  emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
339
845
  return;
@@ -385,34 +891,560 @@ async function main() {
385
891
  emit(`${OWL} imported @${wallet.name} ${wallet.address}\n`);
386
892
  return;
387
893
  }
388
- if (command === "wallets") {
389
- const wallets = slrd.wallets.list();
894
+ if (command === "wallets" || command === "balances") {
895
+ if (
896
+ flags.get("token") &&
897
+ (flags.has("tokens") || flags.has("all-tokens"))
898
+ ) {
899
+ throw new Error("Use either --token <token> or --tokens, not both.");
900
+ }
901
+
902
+ let wallets = slrd.wallets.list();
903
+ const singleWalletRef =
904
+ flags.get("wallet") ?? (command === "balances" ? values[0] : undefined);
905
+ if (singleWalletRef) {
906
+ const wanted = slrd.resolveWallet(singleWalletRef).address.toBase58();
907
+ wallets = wallets.filter(
908
+ (wallet) => slrd.resolveWallet(wallet).address.toBase58() === wanted,
909
+ );
910
+ if (wallets.length === 0)
911
+ throw new Error(`Wallet ${singleWalletRef} is not a stored wallet`);
912
+ }
913
+ if (command === "balances" && !singleWalletRef)
914
+ throw new Error("Usage: slrd balances --wallet <wallet>");
915
+ const group = flags.get("group");
916
+ if (group) {
917
+ const addresses = new Set(
918
+ slrd
919
+ .groupWallets(group)
920
+ .map((ref) => slrd.resolveWallet(ref).address.toBase58()),
921
+ );
922
+ wallets = wallets.filter((wallet) => addresses.has(wallet.address));
923
+ }
924
+
390
925
  if (flags.has("addresses-only")) {
391
926
  for (const wallet of wallets)
392
927
  emit(`@${wallet.name}\t${wallet.address}\n`);
393
928
  return;
394
929
  }
395
- const selectedTokens = flags.get("token")
396
- ? [slrd.resolveToken(flags.get("token")!)]
397
- : slrd.tokens.list();
398
- for (const wallet of wallets) {
399
- const balances = await slrd.walletBalances(wallet, selectedTokens);
400
- const holdings = balances.tokenBalances
401
- .filter(
402
- (balance) =>
403
- balance.amountRaw > 0n ||
404
- flags.has("show-zero") ||
405
- Boolean(flags.get("token")),
406
- )
407
- .map(
408
- (balance) =>
409
- `${balance.token.symbol ? "$" + balance.token.symbol : (balance.token.name ?? shortKey(balance.token.mint))}=${formatRaw(balance.amountRaw, balance.decimals)}`,
410
- )
411
- .join(" ");
930
+
931
+ const allTokens =
932
+ command === "balances" ||
933
+ flags.has("tokens") ||
934
+ flags.has("all-tokens");
935
+ const tokenRef = flags.get("token");
936
+ if (!allTokens && !tokenRef) {
937
+ // Cheap native-SOL-only default: <=100 wallets per RPC call.
938
+ const rows: Array<{
939
+ wallet: (typeof wallets)[number];
940
+ solLamports: bigint;
941
+ }> = [];
942
+ const connection = slrd.connection();
943
+ const batchSize = 100;
944
+
945
+ for (let offset = 0; offset < wallets.length; offset += batchSize) {
946
+ const batch = wallets.slice(offset, offset + batchSize);
947
+ const addresses = batch.map(
948
+ (wallet) => slrd.resolveWallet(wallet).address,
949
+ );
950
+ const accounts = await connection.getMultipleAccountsInfo(
951
+ addresses,
952
+ "confirmed",
953
+ );
954
+
955
+ for (let index = 0; index < batch.length; index += 1) {
956
+ rows.push({
957
+ wallet: batch[index]!,
958
+ solLamports: BigInt(accounts[index]?.lamports ?? 0),
959
+ });
960
+ }
961
+ }
962
+
963
+ rows.sort((left, right) =>
964
+ left.solLamports === right.solLamports
965
+ ? left.wallet.name.localeCompare(right.wallet.name)
966
+ : left.solLamports > right.solLamports
967
+ ? -1
968
+ : 1,
969
+ );
970
+
971
+ const totalSol = rows.reduce((sum, row) => sum + row.solLamports, 0n);
972
+ emit(`TOTAL\tSOL=${formatRaw(totalSol, 9)}\tWALLETS=${rows.length}\n`);
973
+ for (const { wallet, solLamports } of rows) {
974
+ emit(
975
+ `@${wallet.name}\t${wallet.address}\tSOL=${formatRaw(solLamports, 9)}\n`,
976
+ );
977
+ }
978
+ return;
979
+ }
980
+
981
+ // Owner-centric token discovery. This never refreshes/upserts the token
982
+ // registry, so `wallets --tokens` stays read-only and its stdout remains
983
+ // actual portfolio output instead of measurement noise.
984
+ const portfolio = await loadWalletAssetPortfolio(slrd, {
985
+ walletRefs: wallets.map((wallet) => wallet.address),
986
+ includeZero: flags.has("show-zero"),
987
+ concurrency: Math.max(
988
+ 1,
989
+ Math.min(8, Math.trunc(int(flags, "rpc-concurrency", 1) ?? 1)),
990
+ ),
991
+ requestDelayMs: Math.max(
992
+ 0,
993
+ Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
994
+ ),
995
+ });
996
+
997
+ const selectedMint = tokenRef ? slrd.resolveToken(tokenRef).mint : null;
998
+
999
+ const visibleRows = portfolio.rows.map((row) => ({
1000
+ ...row,
1001
+ tokenHoldings: selectedMint
1002
+ ? row.tokenHoldings.filter((holding) => holding.mint === selectedMint)
1003
+ : row.tokenHoldings,
1004
+ }));
1005
+
1006
+ const visibleTotals = new Map<
1007
+ string,
1008
+ {
1009
+ mint: string;
1010
+ name: string | null;
1011
+ symbol: string | null;
1012
+ decimals: number;
1013
+ amountRaw: bigint;
1014
+ }
1015
+ >();
1016
+ for (const row of visibleRows) {
1017
+ for (const holding of row.tokenHoldings) {
1018
+ if (holding.amountRaw <= 0n && !flags.has("show-zero")) continue;
1019
+ const existing = visibleTotals.get(holding.mint);
1020
+ if (existing) existing.amountRaw += holding.amountRaw;
1021
+ else
1022
+ visibleTotals.set(holding.mint, {
1023
+ mint: holding.mint,
1024
+ name: holding.name,
1025
+ symbol: holding.symbol,
1026
+ decimals: holding.decimals,
1027
+ amountRaw: holding.amountRaw,
1028
+ });
1029
+ }
1030
+ }
1031
+
1032
+ const holdingCount = visibleRows.reduce(
1033
+ (sum, row) =>
1034
+ sum +
1035
+ row.tokenHoldings.filter(
1036
+ (holding) => holding.amountRaw > 0n || flags.has("show-zero"),
1037
+ ).length,
1038
+ 0,
1039
+ );
1040
+
1041
+ emit(
1042
+ `PORTFOLIO ${visibleRows.length} wallets | ` +
1043
+ `${formatRaw(portfolio.totalSolLamports, 9)} SOL | ` +
1044
+ `${holdingCount} token holdings | ${visibleTotals.size} mints` +
1045
+ `${portfolio.tokenScanErrorCount ? ` | ${portfolio.tokenScanErrorCount} scan errors` : ""}\n`,
1046
+ );
1047
+
1048
+ if (flags.has("token-totals")) {
1049
+ const aggregateRows = [...visibleTotals.values()].sort((left, right) =>
1050
+ (left.symbol ?? left.name ?? left.mint).localeCompare(
1051
+ right.symbol ?? right.name ?? right.mint,
1052
+ ),
1053
+ );
1054
+ emit("\nTOKEN TOTALS\n");
1055
+ for (const total of aggregateRows) {
1056
+ const label = total.symbol
1057
+ ? total.symbol
1058
+ : (total.name ?? shortKey(total.mint));
1059
+ emit(
1060
+ ` ${label.padEnd(14)} ${formatRaw(total.amountRaw, total.decimals).padStart(18)} ${total.mint}\n`,
1061
+ );
1062
+ }
1063
+ }
1064
+
1065
+ for (const row of visibleRows) {
1066
+ const holdings = row.tokenHoldings.filter(
1067
+ (holding) =>
1068
+ holding.amountRaw > 0n ||
1069
+ flags.has("show-zero") ||
1070
+ Boolean(tokenRef),
1071
+ );
1072
+ if (
1073
+ flags.has("only-with-tokens") &&
1074
+ holdings.every((holding) => holding.amountRaw <= 0n)
1075
+ ) {
1076
+ continue;
1077
+ }
1078
+
1079
+ emit(
1080
+ `\n@${row.walletName} SOL ${formatRaw(row.solLamports, 9)} ` +
1081
+ `${holdings.filter((holding) => holding.amountRaw > 0n).length} token(s)\n`,
1082
+ );
1083
+ emit(` ${row.walletAddress}\n`);
1084
+
1085
+ for (const holding of holdings) {
1086
+ const label = holding.symbol
1087
+ ? holding.symbol
1088
+ : (holding.name ?? shortKey(holding.mint));
1089
+ emit(
1090
+ ` ${label.padEnd(14)} ${holding.amountUi.padStart(18)} ${holding.mint}\n`,
1091
+ );
1092
+ }
1093
+
1094
+ if (!row.tokenScanComplete) {
1095
+ emit(
1096
+ ` ! token scan partial (${row.tokenScanErrors.length} error(s))\n`,
1097
+ );
1098
+ if (flags.has("verbose")) {
1099
+ for (const error of row.tokenScanErrors) {
1100
+ process.stderr.write(
1101
+ `${OWL} token scan @${row.walletName}: ${error}\n`,
1102
+ );
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+ return;
1108
+ }
1109
+ if (command === "fees" || (command === "tx" && values[0] === "fees")) {
1110
+ const walletRef = need(flags, "wallet");
1111
+ const wallet = slrd.resolveWallet(walletRef);
1112
+ const address = wallet.address;
1113
+ const addressText = address.toBase58();
1114
+ const limit = Math.max(
1115
+ 1,
1116
+ Math.min(1_000, Math.trunc(int(flags, "limit", 100) ?? 100)),
1117
+ );
1118
+ const sinceMs = duration(flags.get("since"), 30 * 60_000);
1119
+ const cutoff = Date.now() - sinceMs;
1120
+ const connection = slrd.connection();
1121
+ const signatures = await connection.getSignaturesForAddress(
1122
+ address,
1123
+ { limit },
1124
+ "confirmed",
1125
+ );
1126
+ const recent = signatures.filter(
1127
+ (entry) => entry.blockTime == null || entry.blockTime * 1_000 >= cutoff,
1128
+ );
1129
+ let totalLamports = 0n;
1130
+ let feeMetaFound = 0;
1131
+ let paidByWalletCount = 0;
1132
+ let failed = 0;
1133
+ const rows: Array<{
1134
+ signature: string;
1135
+ blockTime: number | null | undefined;
1136
+ feeLamports: number | null;
1137
+ paidByWallet: boolean;
1138
+ err: unknown;
1139
+ }> = [];
1140
+ for (const entry of recent) {
1141
+ try {
1142
+ const tx = await connection.getTransaction(entry.signature, {
1143
+ commitment: "confirmed",
1144
+ maxSupportedTransactionVersion: 0,
1145
+ });
1146
+ const fee = tx?.meta?.fee;
1147
+ const payer = txAccountKeys(tx)[0] ?? "";
1148
+ const paidByWallet = payer === addressText;
1149
+ if (typeof fee === "number" && Number.isFinite(fee)) {
1150
+ feeMetaFound += 1;
1151
+ if (paidByWallet) {
1152
+ totalLamports += BigInt(Math.trunc(fee));
1153
+ paidByWalletCount += 1;
1154
+ }
1155
+ rows.push({
1156
+ signature: entry.signature,
1157
+ blockTime: entry.blockTime,
1158
+ feeLamports: Math.trunc(fee),
1159
+ paidByWallet,
1160
+ err: entry.err,
1161
+ });
1162
+ } else {
1163
+ rows.push({
1164
+ signature: entry.signature,
1165
+ blockTime: entry.blockTime,
1166
+ feeLamports: null,
1167
+ paidByWallet,
1168
+ err: entry.err,
1169
+ });
1170
+ }
1171
+ } catch {
1172
+ failed += 1;
1173
+ rows.push({
1174
+ signature: entry.signature,
1175
+ blockTime: entry.blockTime,
1176
+ feeLamports: null,
1177
+ paidByWallet: false,
1178
+ err: entry.err,
1179
+ });
1180
+ }
1181
+ }
1182
+ emit(
1183
+ `FEES @${wallet.row?.name ?? walletRef} ${addressText}\n` +
1184
+ `Window: ${flags.get("since") ?? "30m"} transactions=${recent.length} fee-meta=${feeMetaFound} wallet-paid=${paidByWalletCount} unavailable=${recent.length - feeMetaFound}\n` +
1185
+ `Actual network fees paid by wallet: ${(Number(totalLamports) / 1e9).toFixed(9)} SOL (${totalLamports.toString()} lamports)\n`,
1186
+ );
1187
+ if (!flags.has("summary-only")) {
1188
+ emit(
1189
+ "\nTIME FEE SOL PAYER STATUS SIGNATURE\n",
1190
+ );
1191
+ for (const row of rows) {
1192
+ const time = row.blockTime
1193
+ ? new Date(row.blockTime * 1_000).toISOString()
1194
+ : "-";
1195
+ const feeSol =
1196
+ row.feeLamports == null ? "-" : (row.feeLamports / 1e9).toFixed(9);
1197
+ emit(
1198
+ `${time.padEnd(25)} ${feeSol.padStart(12)} ${(row.paidByWallet ? "YOU" : "OTHER").padEnd(6)} ${(row.err ? "FAILED" : "OK").padEnd(7)} ${row.signature}\n`,
1199
+ );
1200
+ }
1201
+ }
1202
+ if (failed > 0)
1203
+ process.stderr.write(
1204
+ `${OWL} ${failed} transaction lookup(s) failed; rerun after RPC history catches up.\n`,
1205
+ );
1206
+ return;
1207
+ }
1208
+ if (command === "sol-flow" || (command === "tx" && values[0] === "flow")) {
1209
+ const walletRef = need(flags, "wallet");
1210
+ const wallet = slrd.resolveWallet(walletRef);
1211
+ const address = wallet.address;
1212
+ const addressText = address.toBase58();
1213
+ const limit = Math.max(
1214
+ 1,
1215
+ Math.min(1_000, Math.trunc(int(flags, "limit", 100) ?? 100)),
1216
+ );
1217
+ const sinceMs = duration(flags.get("since"), 30 * 60_000);
1218
+ const cutoff = Date.now() - sinceMs;
1219
+ const connection = slrd.connection();
1220
+ const signatures = await connection.getSignaturesForAddress(
1221
+ address,
1222
+ { limit },
1223
+ "confirmed",
1224
+ );
1225
+ const recent = signatures.filter(
1226
+ (entry) => entry.blockTime == null || entry.blockTime * 1_000 >= cutoff,
1227
+ );
1228
+
1229
+ type FlowRow = {
1230
+ signature: string;
1231
+ blockTime: number | null | undefined;
1232
+ err: unknown;
1233
+ nativeDeltaLamports: bigint | null;
1234
+ feeLamports: bigint;
1235
+ feePaidByWallet: boolean;
1236
+ wsolDeltaRaw: bigint;
1237
+ tokenDeltas: Array<{
1238
+ mint: string;
1239
+ decimals: number;
1240
+ deltaRaw: bigint;
1241
+ }>;
1242
+ };
1243
+
1244
+ const rows: FlowRow[] = [];
1245
+ let nativeDeltaLamports = 0n;
1246
+ let nativeAvailable = 0;
1247
+ let walletFeeLamports = 0n;
1248
+ let wsolDeltaRaw = 0n;
1249
+ let failed = 0;
1250
+ const tokenTotals = new Map<
1251
+ string,
1252
+ { decimals: number; deltaRaw: bigint }
1253
+ >();
1254
+
1255
+ for (const entry of recent) {
1256
+ try {
1257
+ const tx = await connection.getTransaction(entry.signature, {
1258
+ commitment: "confirmed",
1259
+ maxSupportedTransactionVersion: 0,
1260
+ });
1261
+ if (!tx?.meta) {
1262
+ rows.push({
1263
+ signature: entry.signature,
1264
+ blockTime: entry.blockTime,
1265
+ err: entry.err,
1266
+ nativeDeltaLamports: null,
1267
+ feeLamports: 0n,
1268
+ feePaidByWallet: false,
1269
+ wsolDeltaRaw: 0n,
1270
+ tokenDeltas: [],
1271
+ });
1272
+ continue;
1273
+ }
1274
+
1275
+ const keys = txAccountKeys(tx);
1276
+ const walletIndex = keys.indexOf(addressText);
1277
+ let txNativeDelta: bigint | null = null;
1278
+ if (
1279
+ walletIndex >= 0 &&
1280
+ typeof tx.meta.preBalances?.[walletIndex] === "number" &&
1281
+ typeof tx.meta.postBalances?.[walletIndex] === "number"
1282
+ ) {
1283
+ txNativeDelta =
1284
+ BigInt(Math.trunc(tx.meta.postBalances[walletIndex]!)) -
1285
+ BigInt(Math.trunc(tx.meta.preBalances[walletIndex]!));
1286
+ nativeDeltaLamports += txNativeDelta;
1287
+ nativeAvailable += 1;
1288
+ }
1289
+
1290
+ const fee =
1291
+ typeof tx.meta.fee === "number" && Number.isFinite(tx.meta.fee)
1292
+ ? BigInt(Math.trunc(tx.meta.fee))
1293
+ : 0n;
1294
+ const feePaidByWallet = keys[0] === addressText;
1295
+ if (feePaidByWallet) walletFeeLamports += fee;
1296
+
1297
+ const balances = txWalletTokenBalances(tx, addressText);
1298
+ const tokenDeltas: Array<{
1299
+ mint: string;
1300
+ decimals: number;
1301
+ deltaRaw: bigint;
1302
+ }> = [];
1303
+ let txWsol = 0n;
1304
+ for (const [mint, balance] of balances.entries()) {
1305
+ const deltaRaw = balance.post - balance.pre;
1306
+ if (deltaRaw === 0n) continue;
1307
+ tokenDeltas.push({ mint, decimals: balance.decimals, deltaRaw });
1308
+ const total = tokenTotals.get(mint) ?? {
1309
+ decimals: balance.decimals,
1310
+ deltaRaw: 0n,
1311
+ };
1312
+ total.decimals = balance.decimals;
1313
+ total.deltaRaw += deltaRaw;
1314
+ tokenTotals.set(mint, total);
1315
+ if (mint === NATIVE_SOL_MINT) {
1316
+ txWsol += deltaRaw;
1317
+ wsolDeltaRaw += deltaRaw;
1318
+ }
1319
+ }
1320
+
1321
+ rows.push({
1322
+ signature: entry.signature,
1323
+ blockTime: entry.blockTime,
1324
+ err: entry.err,
1325
+ nativeDeltaLamports: txNativeDelta,
1326
+ feeLamports: fee,
1327
+ feePaidByWallet,
1328
+ wsolDeltaRaw: txWsol,
1329
+ tokenDeltas,
1330
+ });
1331
+ } catch {
1332
+ failed += 1;
1333
+ rows.push({
1334
+ signature: entry.signature,
1335
+ blockTime: entry.blockTime,
1336
+ err: entry.err,
1337
+ nativeDeltaLamports: null,
1338
+ feeLamports: 0n,
1339
+ feePaidByWallet: false,
1340
+ wsolDeltaRaw: 0n,
1341
+ tokenDeltas: [],
1342
+ });
1343
+ }
1344
+ }
1345
+
1346
+ const nonFeeNativeDelta = nativeDeltaLamports + walletFeeLamports;
1347
+ const nativePlusWsol = nativeDeltaLamports + wsolDeltaRaw;
1348
+ const tokenSummary = [...tokenTotals.entries()]
1349
+ .filter(([, value]) => value.deltaRaw !== 0n)
1350
+ .sort((left, right) => {
1351
+ const leftAbs =
1352
+ left[1].deltaRaw < 0n ? -left[1].deltaRaw : left[1].deltaRaw;
1353
+ const rightAbs =
1354
+ right[1].deltaRaw < 0n ? -right[1].deltaRaw : right[1].deltaRaw;
1355
+ return leftAbs === rightAbs
1356
+ ? left[0].localeCompare(right[0])
1357
+ : leftAbs > rightAbs
1358
+ ? -1
1359
+ : 1;
1360
+ });
1361
+
1362
+ if (flags.has("json")) {
1363
+ emit(
1364
+ json({
1365
+ wallet: addressText,
1366
+ window: flags.get("since") ?? "30m",
1367
+ transactions: recent.length,
1368
+ nativeAvailable,
1369
+ nativeDeltaLamports,
1370
+ walletFeeLamports,
1371
+ nonFeeNativeDeltaLamports: nonFeeNativeDelta,
1372
+ wsolDeltaRaw,
1373
+ nativePlusWsolLamports: nativePlusWsol,
1374
+ tokenDeltas: tokenSummary.map(([mint, value]) => ({
1375
+ mint,
1376
+ ...value,
1377
+ })),
1378
+ rows,
1379
+ failedLookups: failed,
1380
+ }) + "\n",
1381
+ );
1382
+ return;
1383
+ }
1384
+
1385
+ emit(
1386
+ `SOL FLOW @${wallet.row?.name ?? walletRef} ${addressText}\n` +
1387
+ `Window: ${flags.get("since") ?? "30m"} transactions=${recent.length} native-meta=${nativeAvailable} unavailable=${recent.length - nativeAvailable}\n` +
1388
+ `Native SOL delta: ${formatSignedRaw(nativeDeltaLamports, 9)} SOL\n` +
1389
+ `Network fees paid by wallet: -${formatRaw(walletFeeLamports, 9)} SOL\n` +
1390
+ `Native delta excluding fees: ${formatSignedRaw(nonFeeNativeDelta, 9)} SOL\n` +
1391
+ `WSOL token-account delta: ${formatSignedRaw(wsolDeltaRaw, 9)} WSOL\n` +
1392
+ `Native + WSOL wallet delta: ${formatSignedRaw(nativePlusWsol, 9)} SOL-like\n`,
1393
+ );
1394
+
1395
+ if (tokenSummary.length > 0) {
1396
+ emit("\nNET SPL TOKEN DELTAS IN WALLET\n");
1397
+ for (const [mint, value] of tokenSummary) {
1398
+ const label =
1399
+ mint === NATIVE_SOL_MINT
1400
+ ? "WSOL"
1401
+ : mint === CANONICAL_USDC_MINT
1402
+ ? "USDC"
1403
+ : shortKey(mint);
1404
+ emit(
1405
+ ` ${label.padEnd(12)} ${formatSignedRaw(value.deltaRaw, value.decimals).padStart(20)} ${mint}\n`,
1406
+ );
1407
+ }
1408
+ }
1409
+
1410
+ if (!flags.has("summary-only")) {
1411
+ emit("\nPER TRANSACTION\n");
412
1412
  emit(
413
- `@${wallet.name}\t${wallet.address}\tSOL=${formatRaw(balances.solLamports, 9)}${holdings ? ` ${holdings}` : ""}\n`,
1413
+ "TIME NATIVE Δ SOL FEE SOL WSOL Δ TOKEN DELTAS STATUS SIGNATURE\n",
414
1414
  );
1415
+ for (const row of rows) {
1416
+ const time = row.blockTime
1417
+ ? new Date(row.blockTime * 1_000).toISOString()
1418
+ : "-";
1419
+ const native =
1420
+ row.nativeDeltaLamports == null
1421
+ ? "-"
1422
+ : formatSignedRaw(row.nativeDeltaLamports, 9);
1423
+ const fee = row.feePaidByWallet ? formatRaw(row.feeLamports, 9) : "0";
1424
+ const wsol = formatSignedRaw(row.wsolDeltaRaw, 9);
1425
+ const other =
1426
+ row.tokenDeltas
1427
+ .filter((delta) => delta.mint !== NATIVE_SOL_MINT)
1428
+ .slice(0, 2)
1429
+ .map(
1430
+ (delta) =>
1431
+ `${shortKey(delta.mint)} ${formatSignedRaw(delta.deltaRaw, delta.decimals)}`,
1432
+ )
1433
+ .join(", ") || "-";
1434
+ emit(
1435
+ `${time.padEnd(25)} ${native.padStart(13)} ${fee.padStart(10)} ${wsol.padStart(11)} ${other.padEnd(35)} ${(row.err ? "FAILED" : "OK").padEnd(7)} ${row.signature}\n`,
1436
+ );
1437
+ }
415
1438
  }
1439
+
1440
+ emit(
1441
+ "\nNative SOL delta is a cash-flow number, not P&L: LP deposits, SOL->token swaps, account rent, and WSOL wrapping can all reduce it without being network fees.\n" +
1442
+ "Use the per-transaction rows to find the large negative native-SOL transactions; their token deltas show what the wallet received or deployed.\n",
1443
+ );
1444
+ if (failed > 0)
1445
+ process.stderr.write(
1446
+ `${OWL} ${failed} transaction lookup(s) failed; rerun after RPC history catches up.\n`,
1447
+ );
416
1448
  return;
417
1449
  }
418
1450
  if (command === "deploy") {
@@ -622,6 +1654,464 @@ async function main() {
622
1654
  );
623
1655
  return;
624
1656
  }
1657
+ if (command === "liquidate" && (values[0] ?? "tokens") === "tokens") {
1658
+ const except = csv(flags.get("except"));
1659
+ if (except.length === 0) {
1660
+ throw new Error(
1661
+ "Usage: slrd liquidate tokens --except <token|mint>[,<token|mint>...] [--wallets <a,b,...>] [--simulate | --live]",
1662
+ );
1663
+ }
1664
+ if (flags.has("simulate") && flags.has("live")) {
1665
+ throw new Error("Use either --simulate or --live, not both");
1666
+ }
1667
+
1668
+ const mode = flags.has("live")
1669
+ ? "LIVE"
1670
+ : flags.has("simulate")
1671
+ ? "SIMULATE"
1672
+ : "PLAN";
1673
+
1674
+ emit(`LIQUIDATE ${mode}\n`);
1675
+ for (const ref of except) {
1676
+ const mint = resolveTokenMintForPolicy(slrd, ref);
1677
+ emit(`PROTECT ${ref} -> ${mint}\n`);
1678
+ }
1679
+ emit(
1680
+ `RPC hard limit ${process.env.SLRD_RPC_MAX_RPS ?? "5"} req/s\n`,
1681
+ );
1682
+
1683
+ const showAction = (
1684
+ prefix: string,
1685
+ index: number,
1686
+ total: number,
1687
+ action: {
1688
+ walletName: string;
1689
+ mint: string;
1690
+ symbol: string | null;
1691
+ name: string | null;
1692
+ amountUi: string;
1693
+ kind: string;
1694
+ },
1695
+ ) => {
1696
+ const label = action.symbol ?? action.name ?? shortKey(action.mint);
1697
+ emit(
1698
+ `${prefix} ${index}/${total} @${action.walletName} ` +
1699
+ `${action.kind === "unwrap-wsol" ? "WSOL" : label} ${action.amountUi}` +
1700
+ `${action.kind === "close-empty" ? " [close empty account]" : ""}\n`,
1701
+ );
1702
+ };
1703
+
1704
+ const options = {
1705
+ except,
1706
+ walletRefs: csv(flags.get("wallets")),
1707
+ slippageBps: int(flags, "slippage-bps", 1500) ?? 1500,
1708
+ via: flags.get("sender") ?? "rpc",
1709
+ delayMs: int(flags, "delay-ms", 0) ?? 0,
1710
+ routeDelayMs: int(flags, "route-delay-ms", 0) ?? 0,
1711
+ portfolioConcurrency: 1,
1712
+ portfolioDelayMs: 0,
1713
+ jupiterFallback: !flags.has("no-jupiter"),
1714
+ onProgress: (event: any) => {
1715
+ if (event.stage === "portfolio-start") {
1716
+ emit("SCAN wallet token accounts...\n");
1717
+ } else if (event.stage === "portfolio-done") {
1718
+ emit(
1719
+ `SCAN ${event.wallets} wallets, ${event.holdings} holdings, ` +
1720
+ `${event.distinctMints} mints\n`,
1721
+ );
1722
+ } else if (event.stage === "route") {
1723
+ emit(
1724
+ `ROUTE ${event.index}/${event.total} ${shortKey(event.mint)}\n`,
1725
+ );
1726
+ } else if (event.stage === "action-start") {
1727
+ showAction(
1728
+ flags.has("simulate") ? "SIM " : "DO ",
1729
+ event.index,
1730
+ event.total,
1731
+ event.action,
1732
+ );
1733
+ } else if (event.stage === "action-done") {
1734
+ showAction("OK ", event.index, event.total, event.action);
1735
+ } else if (event.stage === "action-error") {
1736
+ showAction("FAIL", event.index, event.total, event.action);
1737
+ emit(` ${event.error}\n`);
1738
+ }
1739
+ },
1740
+ };
1741
+
1742
+ const plan = await planRegistryTokenLiquidation(slrd, options);
1743
+ emit(
1744
+ `PLAN native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
1745
+ `unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
1746
+ `keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
1747
+ );
1748
+
1749
+ if (!flags.has("simulate") && !flags.has("live")) {
1750
+ const actionable = plan.actions.filter(
1751
+ (action) =>
1752
+ action.kind === "sell" ||
1753
+ action.kind === "jupiter-sell" ||
1754
+ action.kind === "unwrap-wsol" ||
1755
+ action.kind === "close-empty",
1756
+ );
1757
+ for (const action of actionable) {
1758
+ const label = action.symbol ?? action.name ?? shortKey(action.mint);
1759
+ const verb =
1760
+ action.kind === "unwrap-wsol"
1761
+ ? "UNWRAP"
1762
+ : action.kind === "close-empty"
1763
+ ? "CLOSE"
1764
+ : action.kind === "jupiter-sell"
1765
+ ? "JUPITER"
1766
+ : "SELL";
1767
+ emit(
1768
+ `${verb.padEnd(7)} @${action.walletName.padEnd(16)} ` +
1769
+ `${label.padEnd(14)} ${action.amountUi}` +
1770
+ `${action.venue ? ` ${action.venue}` : ""}\n`,
1771
+ );
1772
+ }
1773
+
1774
+ emit(
1775
+ `KEEP ${plan.totals.keepProtected} protected SLRD holding(s)\n` +
1776
+ `SKIP ${plan.totals.skipUnsupported} unsupported/no-liquidity holding(s)`,
1777
+ );
1778
+ if (plan.totals.skipUnsupported > 0) {
1779
+ emit(flags.has("verbose") ? "\n" : " (use --verbose to list)\n");
1780
+ if (flags.has("verbose")) {
1781
+ for (const action of plan.actions.filter(
1782
+ (item) => item.kind === "skip-unsupported",
1783
+ )) {
1784
+ const label =
1785
+ action.symbol ?? action.name ?? shortKey(action.mint);
1786
+ emit(
1787
+ `SKIP @${action.walletName.padEnd(16)} ` +
1788
+ `${label.padEnd(14)} ${action.amountUi} ${action.reason ?? "unsupported"}\n`,
1789
+ );
1790
+ }
1791
+ }
1792
+ } else {
1793
+ emit("\n");
1794
+ }
1795
+ emit(
1796
+ `AUTO-CLOSE after successful liquidation, the command re-scans on-chain state and closes every unprotected token account that is actually zero.\n`,
1797
+ );
1798
+ emit(`\n${OWL} plan only. Use --simulate, then --live.\n`);
1799
+ return;
1800
+ }
1801
+
1802
+ const results = flags.has("simulate")
1803
+ ? await simulateRegistryTokenLiquidation(slrd, plan, options)
1804
+ : await executeRegistryTokenLiquidation(slrd, plan, options);
1805
+
1806
+ const failed = results.filter((result) => Boolean(result.error)).length;
1807
+ const succeeded = results.filter((result) => !result.error);
1808
+ const soldNative = succeeded.filter(
1809
+ (result) => result.action.kind === "sell",
1810
+ ).length;
1811
+ const soldJupiter = succeeded.filter(
1812
+ (result) => result.action.kind === "jupiter-sell",
1813
+ ).length;
1814
+ const unwrapped = succeeded.filter(
1815
+ (result) => result.action.kind === "unwrap-wsol",
1816
+ ).length;
1817
+ const closedEmpty = succeeded.filter(
1818
+ (result) => result.action.kind === "close-empty",
1819
+ ).length;
1820
+ const reclaimedRentLamports = succeeded
1821
+ .filter((result) => result.action.kind === "close-empty")
1822
+ .reduce((sum, result) => sum + (result.action.rentLamports ?? 0n), 0n);
1823
+ emit(
1824
+ `DONE ok=${succeeded.length} failed=${failed} ` +
1825
+ `native-sold=${soldNative} jupiter-sold=${soldJupiter} ` +
1826
+ `unwrapped=${unwrapped} closed-empty=${closedEmpty} ` +
1827
+ `reclaimed-rent≈${(Number(reclaimedRentLamports) / 1e9).toFixed(6)} SOL\n`,
1828
+ );
1829
+ return;
1830
+ }
1831
+
1832
+ if (command === "sweep" && values[0] === "sol") {
1833
+ const destinationInput = flags.get("to") ?? values[1];
1834
+ if (!destinationInput || destinationInput === "true") {
1835
+ throw new Error(
1836
+ "Usage: slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]",
1837
+ );
1838
+ }
1839
+
1840
+ const destination = resolveDestinationRef(slrd, destinationInput);
1841
+
1842
+ const keepSolByWallet: Record<string, string> = {};
1843
+ const keep = flags.get("keep");
1844
+ if (keep && keep !== "true") {
1845
+ for (const entry of keep.split(",")) {
1846
+ const [wallet, amount, ...extra] = entry.split("=");
1847
+ if (!wallet?.trim() || !amount?.trim() || extra.length) {
1848
+ throw new Error(
1849
+ `Invalid --keep entry ${entry}; expected wallet=SOL`,
1850
+ );
1851
+ }
1852
+ keepSolByWallet[wallet.trim()] = amount.trim();
1853
+ }
1854
+ }
1855
+
1856
+ const excludeGroups = csv(flags.get("exclude-group"));
1857
+ const excludePrefixes = csv(flags.get("exclude-prefix"));
1858
+ const includeWallets = csv(flags.get("wallets"));
1859
+
1860
+ const keepIfTokenRaw = flags.get("keep-if-token");
1861
+ let keepSolIfToken: { token: string; sol: string } | undefined;
1862
+ if (keepIfTokenRaw && keepIfTokenRaw !== "true") {
1863
+ const separator = keepIfTokenRaw.lastIndexOf("=");
1864
+ if (separator <= 0 || separator >= keepIfTokenRaw.length - 1) {
1865
+ throw new Error(
1866
+ "--keep-if-token expects <token|mint>=<SOL>, for example slrd=0.1",
1867
+ );
1868
+ }
1869
+ keepSolIfToken = {
1870
+ token: keepIfTokenRaw.slice(0, separator).trim(),
1871
+ sol: keepIfTokenRaw.slice(separator + 1).trim(),
1872
+ };
1873
+ }
1874
+ if (flags.get("keep-if-tokens") && keepSolIfToken) {
1875
+ throw new Error(
1876
+ "Use either --keep-if-tokens or --keep-if-token, not both",
1877
+ );
1878
+ }
1879
+
1880
+ const sweepMode = flags.has("live")
1881
+ ? "LIVE"
1882
+ : flags.has("simulate")
1883
+ ? "SIMULATE"
1884
+ : "PLAN";
1885
+ if (!flags.has("json")) {
1886
+ emit(`SWEEP ${sweepMode} to=${destination.address}\n`);
1887
+ emit(
1888
+ `RPC hard limit ${process.env.SLRD_RPC_MAX_RPS ?? "5"} req/s\n`,
1889
+ );
1890
+ }
1891
+
1892
+ const options = {
1893
+ destination: destination.address,
1894
+ excludeGroups,
1895
+ excludePrefixes,
1896
+ includeWallets: includeWallets.length ? includeWallets : undefined,
1897
+ keepSolByWallet,
1898
+ defaultKeepSol: flags.get("default-keep-sol") ?? "0",
1899
+ keepSolIfTokens: flags.get("keep-if-tokens"),
1900
+ keepSolIfToken,
1901
+ tokenScanConcurrency: Math.max(
1902
+ 1,
1903
+ Math.min(8, Math.trunc(int(flags, "rpc-concurrency", 1) ?? 1)),
1904
+ ),
1905
+ tokenScanDelayMs: Math.max(
1906
+ 0,
1907
+ Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
1908
+ ),
1909
+ delayMs: int(flags, "delay-ms", 0) ?? 0,
1910
+ onProgress: flags.has("json")
1911
+ ? undefined
1912
+ : (event: any) => {
1913
+ if (event.stage === "plan-start") {
1914
+ emit(`PLAN ${event.wallets} candidate wallets\n`);
1915
+ } else if (event.stage === "plan-ready") {
1916
+ emit(
1917
+ `PLAN ${event.sendWallets}/${event.wallets} will send, ` +
1918
+ `${formatRaw(event.totalSendLamports, 9)} SOL total\n`,
1919
+ );
1920
+ } else if (event.stage === "wallet-start") {
1921
+ emit(
1922
+ `SEND ${event.index}/${event.total} @${event.row.walletName} ` +
1923
+ `${formatRaw(event.row.sendLamports, 9)} SOL\n`,
1924
+ );
1925
+ } else if (event.stage === "wallet-done") {
1926
+ emit(
1927
+ `OK ${event.index}/${event.total} @${event.row.walletName}\n`,
1928
+ );
1929
+ } else if (event.stage === "wallet-error") {
1930
+ emit(
1931
+ `FAIL ${event.index}/${event.total} @${event.row.walletName} ` +
1932
+ `${event.error}\n`,
1933
+ );
1934
+ }
1935
+ },
1936
+ };
1937
+ const plan = await planRegistrySolSweep(slrd, options);
1938
+
1939
+ const printablePlan = {
1940
+ mode: flags.has("live")
1941
+ ? "live"
1942
+ : flags.has("simulate")
1943
+ ? "simulate"
1944
+ : "plan",
1945
+ destination: {
1946
+ input: destination.input,
1947
+ address: plan.destination,
1948
+ contact: destination.contactName ?? null,
1949
+ wallet: destination.walletName ?? null,
1950
+ },
1951
+ totalSendSol: formatRaw(plan.totalSendLamports, 9),
1952
+ rows: plan.rows.map((row) => ({
1953
+ wallet: row.walletName,
1954
+ address: row.walletAddress,
1955
+ balanceSol: formatRaw(row.balanceLamports, 9),
1956
+ keepSol: formatRaw(row.keepLamports, 9),
1957
+ feeSol: formatRaw(row.feeLamports, 9),
1958
+ sendSol: formatRaw(row.sendLamports, 9),
1959
+ tokenHoldings: row.tokenHoldingCount,
1960
+ tokenScanComplete: row.tokenScanComplete,
1961
+ reserveReason: row.reserveReason ?? null,
1962
+ reserveTokenMint: row.reserveTokenMint ?? null,
1963
+ reserveTokenAmountRaw: row.reserveTokenAmountRaw?.toString() ?? null,
1964
+ skippedReason: row.skippedReason ?? null,
1965
+ })),
1966
+ };
1967
+
1968
+ if (!flags.has("simulate") && !flags.has("live")) {
1969
+ if (flags.has("json")) {
1970
+ emit(json(printablePlan) + "\n");
1971
+ return;
1972
+ }
1973
+
1974
+ emit(
1975
+ `SWEEP PLAN to=${plan.destination} ` +
1976
+ `send=${formatRaw(plan.totalSendLamports, 9)} SOL ` +
1977
+ `wallets=${plan.rows.filter((row) => row.sendLamports > 0n).length}/${plan.rows.length}\n`,
1978
+ );
1979
+
1980
+ for (const row of plan.rows) {
1981
+ if (row.sendLamports <= 0n && !flags.has("show-skipped")) continue;
1982
+ emit(
1983
+ `${row.sendLamports > 0n ? "SEND" : "SKIP"} ` +
1984
+ `@${row.walletName.padEnd(18)} ` +
1985
+ `balance=${formatRaw(row.balanceLamports, 9).padStart(12)} ` +
1986
+ `keep=${formatRaw(row.keepLamports, 9).padStart(5)} ` +
1987
+ `send=${formatRaw(row.sendLamports, 9).padStart(12)}` +
1988
+ `${row.reserveReason === "specific-token" ? " keep-token=yes" : ""}` +
1989
+ `${row.skippedReason ? ` ${row.skippedReason}` : ""}\n`,
1990
+ );
1991
+ }
1992
+
1993
+ emit(
1994
+ `\n${OWL} plan only; no signer was decrypted and nothing was submitted. ` +
1995
+ `Use --json for full details, then --simulate or --live.\n`,
1996
+ );
1997
+ return;
1998
+ }
1999
+
2000
+ if (flags.has("simulate") && flags.has("live")) {
2001
+ throw new Error("Use either --simulate or --live, not both");
2002
+ }
2003
+
2004
+ if (flags.has("simulate")) {
2005
+ const results = await simulateRegistrySolSweep(slrd, plan, options);
2006
+ if (flags.has("json")) {
2007
+ emit(json({ ...printablePlan, results }) + "\n");
2008
+ } else {
2009
+ const failed = results.filter(
2010
+ (result) => result.error || result.simulation?.success === false,
2011
+ ).length;
2012
+ emit(
2013
+ `SWEEP SIMULATION attempted=${results.length} ` +
2014
+ `ok=${results.length - failed} failed=${failed} ` +
2015
+ `to=${plan.destination}\n`,
2016
+ );
2017
+ for (const result of results) {
2018
+ if (!result.error && result.simulation?.success !== false) continue;
2019
+ emit(
2020
+ `FAIL @${result.row.walletName} ` +
2021
+ `${result.error ?? "simulation failed"}\n`,
2022
+ );
2023
+ }
2024
+ }
2025
+ return;
2026
+ }
2027
+
2028
+ const receipts = await executeRegistrySolSweep(slrd, plan, options);
2029
+ if (flags.has("json")) {
2030
+ emit(json({ ...printablePlan, receipts }) + "\n");
2031
+ } else {
2032
+ const failed = receipts.filter((result) =>
2033
+ Boolean(result.error),
2034
+ ).length;
2035
+ emit(
2036
+ `SWEEP LIVE attempted=${receipts.length} ` +
2037
+ `ok=${receipts.length - failed} failed=${failed} ` +
2038
+ `to=${plan.destination}\n`,
2039
+ );
2040
+ for (const result of receipts) {
2041
+ if (!result.error) continue;
2042
+ emit(`FAIL @${result.row.walletName} ${result.error}\n`);
2043
+ }
2044
+ }
2045
+ return;
2046
+ }
2047
+
2048
+ if (command === "transfer" || command === "send-sol") {
2049
+ const recipientInput = values[0] === "sol" ? values[1] : values[0];
2050
+ if (!recipientInput) {
2051
+ throw new Error(
2052
+ "Usage: slrd transfer <contact|wallet|address> --wallet <wallet> --sol <amount> [--sender rpc|helius|jito] [--simulate-only]",
2053
+ );
2054
+ }
2055
+
2056
+ const recipient = resolveDestinationRef(slrd, recipientInput);
2057
+ const wallet = need(flags, "wallet");
2058
+ const amount = need(flags, "sol");
2059
+ const via = flags.get("sender") ?? "rpc";
2060
+
2061
+ const cuLimit = Number(flags.get("cu-limit") ?? "10000");
2062
+ const priorityMicroLamports = Number(
2063
+ flags.get("priority-micro-lamports") ?? "0",
2064
+ );
2065
+ if (!Number.isInteger(cuLimit) || cuLimit <= 0)
2066
+ throw new Error("--cu-limit must be a positive integer");
2067
+ if (
2068
+ !Number.isInteger(priorityMicroLamports) ||
2069
+ priorityMicroLamports < 0
2070
+ ) {
2071
+ throw new Error(
2072
+ "--priority-micro-lamports must be a non-negative integer",
2073
+ );
2074
+ }
2075
+
2076
+ const composer = slrd
2077
+ .tx(wallet)
2078
+ .transferSol(recipient.address, sol(amount))
2079
+ .priorityFee({
2080
+ cuLimit,
2081
+ microLamports: priorityMicroLamports,
2082
+ });
2083
+
2084
+ if (flags.has("simulate-only")) {
2085
+ const plan = await composer.build();
2086
+ const result = await slrd.simulatePlan(plan);
2087
+ emit(
2088
+ json({
2089
+ mode: "simulation",
2090
+ wallet,
2091
+ recipient,
2092
+ sol: amount,
2093
+ result,
2094
+ }) + "\n",
2095
+ );
2096
+ return;
2097
+ }
2098
+
2099
+ const receipt = await composer.send({
2100
+ via,
2101
+ kind: "transfer-sol",
2102
+ skipSimulation: flags.has("skip-simulation"),
2103
+ skipPreflight:
2104
+ flags.has("skip-preflight") || flags.has("skip-simulation"),
2105
+ });
2106
+ emit(
2107
+ json({
2108
+ ...receipt,
2109
+ recipient,
2110
+ }) + "\n",
2111
+ );
2112
+ return;
2113
+ }
2114
+
625
2115
  if (command === "buy") {
626
2116
  const token = values[0];
627
2117
  if (!token)
@@ -730,6 +2220,104 @@ async function main() {
730
2220
  skipPreflight:
731
2221
  flags.has("skip-preflight") || flags.has("skip-simulation"),
732
2222
  };
2223
+
2224
+ const multiWallet = targets.refs.length > 1;
2225
+ const failFast = flags.has("fail-fast");
2226
+
2227
+ // Multi-wallet/group unwrap is resilient by default. Do not let one
2228
+ // undecryptable wallet prevent unrelated wallets from recovering WSOL.
2229
+ // With --ignore-missing, probe the WSOL ATA using only the public wallet
2230
+ // address BEFORE asking the vault to decrypt the signer.
2231
+ if (multiWallet && !failFast) {
2232
+ const results: Array<Record<string, unknown>> = [];
2233
+
2234
+ for (const walletRef of targets.refs) {
2235
+ const resolved = slrd.resolveWallet(walletRef);
2236
+ const address = resolved.address.toBase58();
2237
+ const label = resolved.row?.name ? `@${resolved.row.name}` : address;
2238
+
2239
+ if (unwrapOptions.skipMissing) {
2240
+ const wsolAccount = wrappedSolAta(resolved.address);
2241
+ const account = await slrd
2242
+ .connection()
2243
+ .getAccountInfo(wsolAccount, "confirmed");
2244
+
2245
+ if (!account) {
2246
+ results.push({
2247
+ wallet: walletRef,
2248
+ address,
2249
+ ok: true,
2250
+ skipped: "missing-wsol",
2251
+ wsolAccount: wsolAccount.toBase58(),
2252
+ });
2253
+ continue;
2254
+ }
2255
+ }
2256
+
2257
+ try {
2258
+ if (flags.has("simulate-only")) {
2259
+ const plan = await slrd
2260
+ .tx(walletRef)
2261
+ .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
2262
+ .build();
2263
+ const simulation = await slrd.simulatePlan(plan);
2264
+ results.push({
2265
+ wallet: walletRef,
2266
+ address,
2267
+ ok: simulation.success,
2268
+ mode: "simulation",
2269
+ simulation,
2270
+ });
2271
+ } else {
2272
+ const receipt = await slrd.unwrapWsol(walletRef, unwrapOptions);
2273
+ results.push({
2274
+ wallet: walletRef,
2275
+ address,
2276
+ ok: true,
2277
+ receipt,
2278
+ });
2279
+ }
2280
+ } catch (error) {
2281
+ const message =
2282
+ error instanceof Error ? error.message : String(error);
2283
+ results.push({
2284
+ wallet: walletRef,
2285
+ address,
2286
+ ok: false,
2287
+ error: message,
2288
+ });
2289
+ process.stderr.write(
2290
+ `${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
2291
+ );
2292
+ }
2293
+ }
2294
+
2295
+ const succeeded = results.filter(
2296
+ (row) => row.ok === true && !row.skipped,
2297
+ ).length;
2298
+ const skipped = results.filter((row) => Boolean(row.skipped)).length;
2299
+ const failed = results.filter((row) => row.ok === false).length;
2300
+
2301
+ emit(
2302
+ json({
2303
+ mode: flags.has("simulate-only")
2304
+ ? "wallet-by-wallet-simulation"
2305
+ : "wallet-by-wallet",
2306
+ target: targets,
2307
+ summary: {
2308
+ total: results.length,
2309
+ succeeded,
2310
+ skipped,
2311
+ failed,
2312
+ },
2313
+ results,
2314
+ }) + "\n",
2315
+ );
2316
+ return;
2317
+ }
2318
+
2319
+ // Single-wallet unwrap remains fail-fast. --fail-fast also preserves the
2320
+ // old batch behavior for callers that explicitly want all-or-nothing.
733
2321
  if (flags.has("simulate-only")) {
734
2322
  const plans =
735
2323
  targets.refs.length === 1
@@ -752,6 +2340,7 @@ async function main() {
752
2340
  emit(json({ mode: "simulation", target: targets, results }) + "\n");
753
2341
  return;
754
2342
  }
2343
+
755
2344
  const receipts =
756
2345
  targets.refs.length === 1
757
2346
  ? await slrd.unwrapWsol(targets.refs[0]!, unwrapOptions)