@solard/cli 0.2.3 → 0.2.7

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,922 +1,2515 @@
1
- #!/usr/bin/env bun
2
- import { listScripts, runScript, type TokenRow } from "@solard/sdk";
3
-
4
- function emit(value: string): void {
5
- process.stdout.write(value);
6
- }
7
-
8
- const OWL = "🦉";
9
- type Flags = Map<string, string>;
10
- function args(input: string[]): { values: string[]; flags: Flags } {
11
- const values: string[] = [],
12
- flags = new Map<string, string>();
13
- for (let i = 0; i < input.length; i++) {
14
- const current = input[i]!;
15
- if (current === "-w") {
16
- flags.set("wallet", input[++i] ?? "");
17
- continue;
18
- }
19
- if (current.startsWith("--")) {
20
- const [key, inline] = current.slice(2).split("=", 2);
21
- if (inline != null) flags.set(key!, inline);
22
- else if (input[i + 1] && !input[i + 1]!.startsWith("-"))
23
- flags.set(key!, input[++i]!);
24
- else flags.set(key!, "true");
25
- continue;
26
- }
27
- values.push(current);
28
- }
29
- return { values, flags };
30
- }
31
- function need(flags: Flags, key: string): string {
32
- const value = flags.get(key);
33
- if (!value || value === "true") throw new Error(`Missing --${key} <value>`);
34
- return value;
35
- }
36
- function int(flags: Flags, key: string, fallback?: number): number | undefined {
37
- const value = flags.get(key);
38
- if (!value) return fallback;
39
- const parsed = Number(value);
40
- if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${value}`);
41
- return parsed;
42
- }
43
- function json(value: unknown): string {
44
- return JSON.stringify(
45
- value,
46
- (_, item) => (typeof item === "bigint" ? item.toString() : item),
47
- 2,
48
- );
49
- }
50
- function formatError(error: unknown): string {
51
- if (error instanceof Error) {
52
- const code =
53
- "code" in error &&
54
- typeof (error as Error & { code?: unknown }).code === "string"
55
- ? ` [${String((error as Error & { code?: unknown }).code)}]`
56
- : "";
57
- const message = error.message.trim();
58
- const summary = `${error.name}${code}${message ? `: ${message}` : " (no message)"}`;
59
- return error.stack && error.stack !== summary
60
- ? `${summary}\n${error.stack}`
61
- : summary;
62
- }
63
- try {
64
- const encoded = json(error);
65
- return encoded && encoded !== "undefined" ? encoded : String(error);
66
- } catch {
67
- return String(error);
68
- }
69
- }
70
- function duration(value: string | undefined, fallbackMs: number): number {
71
- if (!value) return fallbackMs;
72
- const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i.exec(value.trim());
73
- if (!match)
74
- throw new Error(`Invalid duration: ${value}. Use e.g. 500ms, 5s, 15m, 1h.`);
75
- const n = Number(match[1]);
76
- const scale = ({ ms: 1, s: 1_000, m: 60_000, h: 3_600_000 } as const)[
77
- (match[2]?.toLowerCase() ?? "ms") as "ms" | "s" | "m" | "h"
78
- ];
79
- return Math.floor(n * scale);
80
- }
81
- function formatPrice(value: number | null | undefined): string {
82
- if (value == null || !Number.isFinite(value)) return "n/a";
83
- return value >= 0.001
84
- ? value.toFixed(9).replace(/0+$/, "").replace(/\.$/, "")
85
- : value.toExponential(6);
86
- }
87
-
88
- function csv(value: string | undefined): string[] {
89
- return (value ?? "")
90
- .split(",")
91
- .map((item) => item.trim())
92
- .filter(Boolean);
93
- }
94
- function targetWallets(
95
- slrd: { groupWallets(name: string): unknown[] },
96
- flags: Flags,
97
- usage: string,
98
- ): { mode: "wallet" | "wallets" | "group"; refs: string[]; group?: string } {
99
- const wallet = flags.get("wallet");
100
- const wallets = csv(flags.get("wallets"));
101
- const group = flags.get("group");
102
- const selected = [Boolean(wallet), wallets.length > 0, Boolean(group)].filter(
103
- Boolean,
104
- ).length;
105
- if (selected !== 1)
106
- throw new Error(
107
- `${usage} Supply exactly one of --wallet, --wallets, or --group.`,
108
- );
109
- if (wallet) return { mode: "wallet", refs: [wallet] };
110
- if (wallets.length > 0) return { mode: "wallets", refs: wallets };
111
- return {
112
- mode: "group",
113
- refs: slrd.groupWallets(group!).map((ref) => String(ref)),
114
- group,
115
- };
116
- }
117
- function help(): string {
118
- return `${OWL} slrd — multi-wallet Solana CLI + SDK for traders and AI agents
119
-
120
- Wallets and tokens
121
- slrd wallet create [name] Generate and persist an encrypted Solana wallet
122
- slrd import <private_key> [name]
123
- cat key.json | slrd import --stdin [name]
124
- slrd wallets [--token <token>] [--addresses-only] Show SOL and registered-token balances
125
- slrd token <token_ca> [name] [--metadata-json <json>]
126
- slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
127
- slrd token refresh <token|ca>
128
- slrd tokens
129
-
130
- Metadata and launching
131
- slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--alias <name>] [--live] [--skip-simulation]
132
- [--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
133
- slrd metadata upload --image <local_path> --name <name> --symbol <symbol> --description <text> [--provider pump-frontend|pinata]
134
- [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
135
- slrd deploy pump --wallet <wallet> --name <name> --symbol <symbol> (--uri <metadata_uri> | --image <local_path> --description <text>) [--alias <name>] [--live]
136
- [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
137
-
138
- Prices
139
- slrd quote buy <token|ca> --sol <amount> Inspect buy quote without submitting
140
- slrd price <token|ca> Sample current venue price
141
- slrd price average <token|ca> --period 15m Average stored samples in a period
142
- slrd price watch <token|ca...> [--interval 1s] [--period 1m]
143
-
144
- Trading
145
- slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
146
- slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
147
- slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
148
- slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
149
- slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
150
- slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
151
-
152
- Scripts (strategies stay outside the kernel)
153
- slrd scripts List scripts registered in slrd.config.ts
154
- slrd run <name-or-path> [script flags...] Execute a script that imports slrd
155
- slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
156
-
157
- Groups and agents
158
- slrd group create <name> [description]
159
- slrd group add <group> <wallet> [weight_bps]
160
- slrd group add-many <group> <wallet1,wallet2,...>
161
- slrd group show <group>
162
- slrd group list
163
- slrd buy <token|ca> --group <name> --sol <amount> --sender jito Submit group buys as Jito bundles, five tx per bundle
164
- slrd sell <token|ca> --group <name> --sender jito Submit group sells as Jito bundles, five tx per bundle
165
- slrd agent create <name> --wallet <wallet> [--config-json <json>]
166
- slrd agent list
167
-
168
- Watching
169
- slrd watch token <token|ca> [label]
170
- slrd watch wallet <wallet> [label]
171
- slrd watch program <address> [label]
172
- slrd watch list
173
-
174
- Transactions and ALTs
175
- slrd history
176
- slrd jito tip-accounts [--endpoint <block-engine-url>]
177
- slrd alt add <address> [label]
178
- slrd alt list
179
- slrd alt create --wallet <wallet>
180
- slrd alt extend <address> --wallet <wallet> <account...>
181
-
182
- Environment
183
- SLRD_DB_PATH shared SDK/CLI database; default ./slrd.db (SOLARD_DB_PATH alias supported)
184
- SLRD_MASTER_KEY required to create/import/decrypt stored wallets
185
- RPC_ENDPOINT required only for chain operations
186
- HELIUS_SENDER_URL regional/global Helius Sender endpoint used by launch scripts
187
- HELIUS_RPC_URL RPC endpoint used for ordinary Helius-RPC buyer lane
188
- HELIUS_TIP_ACCOUNT, HELIUS_TIP_LAMPORTS, HELIUS_PRIORITY_MICRO_LAMPORTS launch transport policy
189
- SLRD_LAUNCH_* launch resend/timeout policy; shared environment, not per-token metadata
190
- JITO_BLOCK_ENGINE_URL only when explicitly selecting the separate Jito sender
191
- SLRD_METADATA_UPLOADER metadata provider; default pump-frontend; optional pinata
192
- PUMP_IPFS_ENDPOINT browser-facing Pump metadata upload endpoint; default https://pump.fun/api/ipfs
193
- PINATA_JWT required only with --provider pinata or explicit pinata fallback
194
- IPFS_PUBLIC_GATEWAY optional Pinata returned metadata gateway; default https://ipfs.io/ipfs
195
- `;
196
- }
197
-
198
- async function main() {
199
- const [command, ...rest] = process.argv.slice(2);
200
- const { values, flags } = args(rest);
201
- if (!command || command === "help" || command === "--help") {
202
- emit(help());
203
- return;
204
- }
205
- if (command === "version" || command === "--version" || command === "-v") {
206
- const { readFileSync } = await import("node:fs");
207
- const { join } = await import("node:path");
208
- const pkg = JSON.parse(
209
- readFileSync(join(import.meta.dir, "..", "package.json"), "utf8"),
210
- ) as { name: string; version: string };
211
- emit(`${OWL} ${pkg.name} ${pkg.version} (bun ${Bun.version})\n`);
212
- return;
213
- }
214
- if (command === "vanity") {
215
- const {
216
- generateMintKeypairWithSuffix,
217
- saveMintKeypairFile,
218
- cleanVanitySuffix,
219
- } = await import("@solard/sdk");
220
- const suffix = cleanVanitySuffix(need(flags, "suffix"));
221
- const out = need(flags, "out");
222
- const count = int(flags, "count", 1)!;
223
- const results = [];
224
- for (let i = 0; i < count; i++) {
225
- const found = await generateMintKeypairWithSuffix({
226
- suffix,
227
- workers: int(flags, "workers"),
228
- maxAttempts: int(flags, "max-attempts", 25_000_000)!,
229
- timeoutMs: int(flags, "timeout-ms", 0)!,
230
- reportEvery: int(flags, "report-every", 1_000_000)!,
231
- onProgress: (p) =>
232
- emit(
233
- `${OWL} grinding ${p.suffix}: ${p.attempts} attempts, ${p.ratePerSecond}/s\n`,
234
- ),
235
- });
236
- const path = count === 1 ? out : out.replace(/(\.json)?$/, `-${i + 1}$1`);
237
- const saved = saveMintKeypairFile(path, found.mint, {
238
- force: flags.has("force"),
239
- });
240
- results.push({
241
- address: found.mint.publicKey.toBase58(),
242
- keypairPath: saved,
243
- attempts: found.attempts,
244
- elapsedMs: found.elapsedMs,
245
- launchArguments: ["--mint-keypair", saved, "--mint-suffix", suffix],
246
- });
247
- }
248
- emit(json(count === 1 ? results[0] : results) + "\n");
249
- return;
250
- }
251
- if (command === "spam-buy" || command === "buy-spam") {
252
- const { runPumpSpamBuyFromArgs } = await import("./pump/spam-buy-cli.ts");
253
- await runPumpSpamBuyFromArgs(rest);
254
- return;
255
- }
256
- if (command === "buy" && flags.has("spam")) {
257
- const { runPumpSpamBuyFromArgs } = await import("./pump/spam-buy-cli.ts");
258
- await runPumpSpamBuyFromArgs(rest);
259
- return;
260
- }
261
- if (command === "scripts") {
262
- emit(json(await listScripts()) + "\n");
263
- return;
264
- }
265
- if (command === "run") {
266
- const [script, ...scriptArgs] = rest;
267
- if (!script)
268
- throw new Error("Usage: slrd run <name-or-path> [script flags...]");
269
- process.exitCode = await runScript(script, scriptArgs);
270
- return;
271
- }
272
- if (
273
- command === "launch" &&
274
- (values[0] === "pump" || values[0] === "pump-token")
275
- ) {
276
- const { runPumpTokenLaunchFromArgs } =
277
- await import("./pump/token-launch-cli.ts");
278
- await runPumpTokenLaunchFromArgs(rest.slice(1), {
279
- defaultSubmitMode: "after-deploy-processed",
280
- defaultDeploymentPriorityMicroLamports: 500_000,
281
- defaultBuyerPriorityMicroLamports: 1_500_000,
282
- defaultSlippageBps: 1_500,
283
- persistOnLive: true,
284
- report: (label, value) => emit(`${label}: ${json(value)}\n`),
285
- });
286
- return;
287
- }
288
- if (command === "jito" && values[0] === "tip-accounts") {
289
- const endpoint = (
290
- flags.get("endpoint") ??
291
- process.env.JITO_BLOCK_ENGINE_URL ??
292
- "https://mainnet.block-engine.jito.wtf"
293
- ).replace(/\/$/, "");
294
- const { getJitoTipAccounts } = await import("@solard/sdk");
295
- const tipAccounts = await getJitoTipAccounts(endpoint);
296
- emit(json({ endpoint, tipAccounts }) + "\n");
297
- return;
298
- }
299
- if (command === "metadata" && values[0] === "upload") {
300
- const { uploadPumpMetadata } = await import("@solard/sdk");
301
- const uploaded = await uploadPumpMetadata(
302
- {
303
- imagePath: need(flags, "image"),
304
- name: need(flags, "name"),
305
- symbol: need(flags, "symbol"),
306
- description: need(flags, "description"),
307
- twitter: flags.get("twitter"),
308
- telegram: flags.get("telegram"),
309
- website: flags.get("website"),
310
- video: flags.get("video"),
311
- showName: !flags.has("hide-name"),
312
- },
313
- {
314
- provider: (flags.get("provider") ??
315
- process.env.SLRD_METADATA_UPLOADER ??
316
- "pump-frontend") as "pump-frontend" | "pinata",
317
- endpoint: flags.get("endpoint"),
318
- fallback: flags.get("fallback") === "pinata" ? "pinata" : null,
319
- },
320
- );
321
- emit(json(uploaded) + "\n");
322
- return;
323
- }
324
- const [{ sol, formatRaw }, { shortKey }, { createTraderSolard }] =
325
- await Promise.all([
326
- import("@solard/sdk"),
327
- import("@solard/sdk"),
328
- import("@solard/sdk"),
329
- ]);
330
- const slrd = createTraderSolard();
331
- try {
332
- if (command === "wallet" && values[0] === "create") {
333
- const wallet = slrd.createWallet(values[1]);
334
- emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
335
- return;
336
- }
337
- if (command === "export") {
338
- const ref = values[0];
339
- if (!ref)
340
- throw new Error(
341
- "Usage: slrd export <wallet|address> [--json] [--out <path>]",
342
- );
343
- const bs58 = (await import("bs58")).default;
344
- const { signer, row } = slrd.wallets.signer(ref);
345
- const secret = flags.has("json")
346
- ? JSON.stringify(Array.from(signer.secretKey))
347
- : bs58.encode(signer.secretKey);
348
-
349
- const out = flags.get("out");
350
- if (out && out !== "true") {
351
- const { writeFileSync, mkdirSync } = await import("node:fs");
352
- const { dirname, resolve } = await import("node:path");
353
- const path = resolve(out);
354
- mkdirSync(dirname(path), { recursive: true });
355
- writeFileSync(path, `${secret}\n`, { mode: 0o600, flag: "wx" });
356
- emit(
357
- `${OWL} exported @${row?.name ?? "?"} ${signer.publicKey.toBase58()} -> ${path}\n`,
358
- );
359
- return;
360
- }
361
-
362
- // Secret to stdout only; metadata to stderr so piping stays clean.
363
- process.stderr.write(
364
- `${OWL} @${row?.name ?? "?"} ${signer.publicKey.toBase58()} (${flags.has("json") ? "keypair-json" : "base58"})\n`,
365
- );
366
- process.stdout.write(`${secret}\n`);
367
- return;
368
- }
369
- if (command === "import") {
370
- const input = flags.has("stdin")
371
- ? (await new Response(Bun.stdin.stream()).text()).trim()
372
- : values[0];
373
- if (!input)
374
- throw new Error(
375
- "Usage: slrd import <private_key> [name] [--force] or cat key.json | slrd import --stdin [name] [--force]",
376
- );
377
- const name = flags.has("stdin") ? values[0] : values[1];
378
- const wallet = slrd.importWallet(input, name, {
379
- overwrite: flags.has("force") || flags.has("overwrite"),
380
- });
381
- emit(`${OWL} imported @${wallet.name} ${wallet.address}\n`);
382
- return;
383
- }
384
- if (command === "wallets") {
385
- const wallets = slrd.wallets.list();
386
- if (flags.has("addresses-only")) {
387
- for (const wallet of wallets)
388
- emit(`@${wallet.name}\t${wallet.address}\n`);
389
- return;
390
- }
391
- const selectedTokens = flags.get("token")
392
- ? [slrd.resolveToken(flags.get("token")!)]
393
- : slrd.tokens.list();
394
- for (const wallet of wallets) {
395
- const balances = await slrd.walletBalances(wallet, selectedTokens);
396
- const holdings = balances.tokenBalances
397
- .filter(
398
- (balance) =>
399
- balance.amountRaw > 0n ||
400
- flags.has("show-zero") ||
401
- Boolean(flags.get("token")),
402
- )
403
- .map(
404
- (balance) =>
405
- `${balance.token.symbol ? "$" + balance.token.symbol : (balance.token.name ?? shortKey(balance.token.mint))}=${formatRaw(balance.amountRaw, balance.decimals)}`,
406
- )
407
- .join(" ");
408
- emit(
409
- `@${wallet.name}\t${wallet.address}\tSOL=${formatRaw(balances.solLamports, 9)}${holdings ? ` ${holdings}` : ""}\n`,
410
- );
411
- }
412
- return;
413
- }
414
- if (command === "deploy") {
415
- const launchpad = values[0] ?? "pump";
416
- const wallet = need(flags, "wallet");
417
- const name = need(flags, "name");
418
- const symbol = need(flags, "symbol");
419
- let uri = flags.get("uri");
420
- let uploadedMetadata: unknown = null;
421
- if (!uri || uri === "true") {
422
- if (!flags.get("image"))
423
- throw new Error(
424
- "Provide --uri <metadata_uri> or --image <local_path> --description <text>",
425
- );
426
- const { uploadPumpMetadata } = await import("@solard/sdk");
427
- uploadedMetadata = await uploadPumpMetadata(
428
- {
429
- imagePath: need(flags, "image"),
430
- name,
431
- symbol,
432
- description: need(flags, "description"),
433
- twitter: flags.get("twitter"),
434
- telegram: flags.get("telegram"),
435
- website: flags.get("website"),
436
- video: flags.get("video"),
437
- showName: !flags.has("hide-name"),
438
- },
439
- {
440
- provider: (flags.get("provider") ??
441
- process.env.SLRD_METADATA_UPLOADER ??
442
- "pump-frontend") as "pump-frontend" | "pinata",
443
- endpoint: flags.get("endpoint"),
444
- fallback: flags.get("fallback") === "pinata" ? "pinata" : null,
445
- },
446
- );
447
- uri = (uploadedMetadata as { metadataUri: string }).metadataUri;
448
- }
449
- const creator = flags.get("creator")
450
- ? slrd.resolveWallet(flags.get("creator")!).address
451
- : slrd.signer(wallet).publicKey;
452
- const deployment = await slrd.prepareTokenDeployment(launchpad, wallet, {
453
- name,
454
- symbol,
455
- uri,
456
- creator,
457
- mayhemMode: flags.has("mayhem"),
458
- cashback: flags.has("cashback"),
459
- });
460
- const plan = await slrd
461
- .transaction(wallet)
462
- .addMany(deployment.instructions, {
463
- kind: "deploy-token",
464
- mint: deployment.mint.publicKey,
465
- meta: { launchpad, name, symbol },
466
- })
467
- .withSigner(deployment.mint)
468
- .build();
469
- const header = {
470
- launchpad,
471
- mint: deployment.mint.publicKey.toBase58(),
472
- name,
473
- symbol,
474
- uri,
475
- uploadedMetadata,
476
- live: flags.has("live"),
477
- };
478
- if (!flags.has("live")) {
479
- const simulation = await slrd.simulatePlan(plan);
480
- emit(json({ ...header, simulation }) + "\n");
481
- return;
482
- }
483
- const receipt = await slrd.sendPlan(
484
- plan,
485
- flags.get("sender") ?? "rpc",
486
- `deploy:${launchpad}`,
487
- {
488
- skipSimulation: flags.has("skip-simulation"),
489
- skipPreflight: flags.has("skip-simulation"),
490
- },
491
- );
492
- const token = slrd.persistPreparedDeployment(
493
- deployment,
494
- flags.get("alias"),
495
- );
496
- emit(json({ ...header, receipt, token }) + "\n");
497
- return;
498
- }
499
- if (command === "token" && values[0] !== "set" && values[0] !== "refresh") {
500
- const [mint, name] = values;
501
- if (!mint) throw new Error("Usage: slrd token <token_ca> [name]");
502
- const metadata = flags.get("metadata-json");
503
- const token = await slrd.addToken(
504
- mint,
505
- name,
506
- metadata ? { metadataJson: metadata } : {},
507
- );
508
- emit(
509
- `${OWL} token ${token.name ?? token.symbol ?? "-"} ${token.mint} venue=${token.venueHint}\n`,
510
- );
511
- return;
512
- }
513
- if (command === "token" && values[0] === "refresh") {
514
- const ref = values[1];
515
- if (!ref) throw new Error("Usage: slrd token refresh <token|ca>");
516
- emit(json(await slrd.refreshToken(ref)) + "\n");
517
- return;
518
- }
519
- if (command === "token" && values[0] === "set") {
520
- const ref = values[1];
521
- if (!ref) throw new Error("Usage: slrd token set <token|ca> [flags]");
522
- const patch: Partial<TokenRow> = {};
523
- if (flags.get("pool")) patch.pool = flags.get("pool");
524
- if (flags.get("quote-mint")) patch.quoteMint = flags.get("quote-mint");
525
- if (flags.get("quote-program"))
526
- patch.quoteTokenProgram = flags.get("quote-program");
527
- if (flags.get("metadata-json"))
528
- patch.metadataJson = flags.get("metadata-json");
529
- const token = slrd.configureToken(ref, patch);
530
- emit(`${OWL} updated ${token.name ?? shortKey(token.mint)}\n`);
531
- return;
532
- }
533
- if (command === "tokens") {
534
- for (const token of slrd.tokens.list())
535
- emit(
536
- `${token.name ?? "-"}\t${token.symbol ? "$" + token.symbol : "-"}\t${token.mint}\t${token.venueHint}\n`,
537
- );
538
- return;
539
- }
540
- if (command === "quote" && values[0] === "buy") {
541
- const tokenRef = values[1];
542
- if (!tokenRef)
543
- throw new Error(
544
- "Usage: slrd quote buy <token|ca> --sol <amount> [--slippage-bps 1500]",
545
- );
546
- const spend = need(flags, "sol");
547
- const result = await slrd.quoteBuy(
548
- tokenRef,
549
- sol(spend),
550
- int(flags, "slippage-bps", 1500),
551
- );
552
- emit(
553
- json({
554
- token: {
555
- mint: result.token.mint,
556
- name: result.token.name,
557
- symbol: result.token.symbol,
558
- },
559
- venue: result.venue,
560
- quoteAsset:
561
- result.quoteAsset.kind === "native-sol"
562
- ? "SOL"
563
- : result.quoteAsset.mint.toBase58(),
564
- spendRaw: result.quote.inputRaw,
565
- expectedOutputRaw: result.quote.expectedOutputRaw,
566
- minimumOutputRaw: result.quote.minimumOutputRaw,
567
- meta: result.quote.meta ?? {},
568
- }) + "\n",
569
- );
570
- return;
571
- }
572
- if (command === "price" && values[0] === "average") {
573
- const tokenRef = values[1];
574
- if (!tokenRef)
575
- throw new Error("Usage: slrd price average <token|ca> --period 15m");
576
- const token = slrd.resolveToken(tokenRef);
577
- const window = slrd.averagePrice(
578
- token,
579
- duration(flags.get("period"), 60_000),
580
- );
581
- emit(
582
- `${token.symbol ? "$" + token.symbol : (token.name ?? shortKey(token.mint))} samples=${window.samples} avg=${formatPrice(window.averagePriceQuotePerToken)} min=${formatPrice(window.minimumPriceQuotePerToken)} max=${formatPrice(window.maximumPriceQuotePerToken)} last=${formatPrice(window.lastPriceQuotePerToken)}\n`,
583
- );
584
- return;
585
- }
586
- if (command === "price" && values[0] === "watch") {
587
- const tokenRefs = values.slice(1);
588
- if (tokenRefs.length === 0)
589
- throw new Error(
590
- "Usage: slrd price watch <token|ca...> [--interval 1s] [--period 1m]",
591
- );
592
- const controller = new AbortController();
593
- process.once("SIGINT", () => controller.abort());
594
- for await (const event of slrd.watchPrices(tokenRefs, {
595
- intervalMs: duration(flags.get("interval"), 1_000),
596
- averagePeriodMs: duration(flags.get("period"), 60_000),
597
- signal: controller.signal,
598
- })) {
599
- const label = event.token.symbol
600
- ? "$" + event.token.symbol
601
- : (event.token.name ?? shortKey(event.token.mint));
602
- emit(
603
- `${new Date(event.sample.capturedAtMs).toISOString()} ${label} venue=${event.sample.venue} price=${formatPrice(event.sample.priceQuotePerToken)} avg=${formatPrice(event.average.averagePriceQuotePerToken)} samples=${event.average.samples}\n`,
604
- );
605
- }
606
- return;
607
- }
608
- if (command === "price") {
609
- const tokenRef = values[0];
610
- if (!tokenRef) throw new Error("Usage: slrd price <token|ca>");
611
- const token = slrd.resolveToken(tokenRef);
612
- const sample = await slrd.samplePrice(token);
613
- const label = token.symbol
614
- ? "$" + token.symbol
615
- : (token.name ?? shortKey(token.mint));
616
- emit(
617
- `${label} venue=${sample.venue} price=${formatPrice(sample.priceQuotePerToken)} quote=${sample.quoteAsset.kind === "native-sol" ? "SOL" : shortKey(sample.quoteAsset.mint.toBase58())} captured=${new Date(sample.capturedAtMs).toISOString()}\n`,
618
- );
619
- return;
620
- }
621
- if (command === "buy") {
622
- const token = values[0];
623
- if (!token)
624
- throw new Error(
625
- "Usage: slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) --sol <amount>",
626
- );
627
- const amount = flags.get("sol");
628
- if (!amount) throw new Error("Buy requires explicit --sol <amount>");
629
- const targets = targetWallets(
630
- slrd,
631
- flags,
632
- "Usage: slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) --sol <amount>.",
633
- );
634
- const options = {
635
- slippageBps: int(flags, "slippage-bps", 1500),
636
- via: flags.get("sender") ?? "rpc",
637
- skipSimulation: flags.has("skip-simulation"),
638
- skipPreflight:
639
- flags.has("skip-preflight") || flags.has("skip-simulation"),
640
- };
641
- if (flags.has("simulate-only")) {
642
- const plans =
643
- targets.refs.length === 1
644
- ? [
645
- await slrd
646
- .tx(targets.refs[0]!)
647
- .buy(token, sol(amount), options)
648
- .build(),
649
- ]
650
- : await slrd
651
- .composeMany(targets.refs)
652
- .buy(token, sol(amount), options)
653
- .build();
654
- const results = await Promise.all(
655
- plans.map((plan) => slrd.simulatePlan(plan)),
656
- );
657
- emit(json({ mode: "simulation", target: targets, results }) + "\n");
658
- return;
659
- }
660
- const receipts =
661
- targets.refs.length === 1
662
- ? await slrd.buy(token, targets.refs[0]!, sol(amount), options)
663
- : await slrd.buyMany(token, targets.refs, sol(amount), options);
664
- emit(json(receipts) + "\n");
665
- return;
666
- }
667
- if (command === "sell") {
668
- const token = values[0];
669
- if (!token)
670
- throw new Error(
671
- "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>)",
672
- );
673
- const targets = targetWallets(
674
- slrd,
675
- flags,
676
- "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
677
- );
678
- const sellOptions = {
679
- bps: int(flags, "bps", 10000),
680
- slippageBps: int(flags, "slippage-bps", 1500),
681
- via: flags.get("sender") ?? "rpc",
682
- skipSimulation: flags.has("skip-simulation"),
683
- skipPreflight:
684
- flags.has("skip-preflight") || flags.has("skip-simulation"),
685
- };
686
- if (flags.has("simulate-only")) {
687
- const plans =
688
- targets.refs.length === 1
689
- ? [await slrd.tx(targets.refs[0]!).sell(token, sellOptions).build()]
690
- : await slrd
691
- .composeMany(targets.refs)
692
- .sell(token, sellOptions)
693
- .build();
694
- const results = await Promise.all(
695
- plans.map((plan) => slrd.simulatePlan(plan)),
696
- );
697
- emit(json({ mode: "simulation", target: targets, results }) + "\n");
698
- return;
699
- }
700
- const receipts =
701
- targets.refs.length === 1
702
- ? await slrd.sell(token, targets.refs[0]!, sellOptions)
703
- : await slrd.sellMany(token, targets.refs, sellOptions);
704
- emit(json(receipts) + "\n");
705
- return;
706
- }
707
- if (
708
- command === "unwrap-wsol" ||
709
- (command === "unwrap" && values[0] === "wsol")
710
- ) {
711
- const targets = targetWallets(
712
- slrd,
713
- flags,
714
- "Usage: slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
715
- );
716
- const destination = flags.get("destination");
717
- if (destination && targets.refs.length !== 1)
718
- throw new Error(
719
- "--destination is only supported with a single --wallet unwrap.",
720
- );
721
- const unwrapOptions = {
722
- via: flags.get("sender") ?? "rpc",
723
- destination,
724
- skipMissing: flags.has("ignore-missing") || flags.has("skip-missing"),
725
- skipSimulation: flags.has("skip-simulation"),
726
- skipPreflight:
727
- flags.has("skip-preflight") || flags.has("skip-simulation"),
728
- };
729
- if (flags.has("simulate-only")) {
730
- const plans =
731
- targets.refs.length === 1
732
- ? [
733
- await slrd
734
- .tx(targets.refs[0]!)
735
- .unwrapWsol({
736
- destination,
737
- skipMissing: unwrapOptions.skipMissing,
738
- })
739
- .build(),
740
- ]
741
- : await slrd
742
- .composeMany(targets.refs)
743
- .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
744
- .build();
745
- const results = await Promise.all(
746
- plans.map((plan) => slrd.simulatePlan(plan)),
747
- );
748
- emit(json({ mode: "simulation", target: targets, results }) + "\n");
749
- return;
750
- }
751
- const receipts =
752
- targets.refs.length === 1
753
- ? await slrd.unwrapWsol(targets.refs[0]!, unwrapOptions)
754
- : await slrd.unwrapWsolMany(targets.refs, unwrapOptions);
755
- emit(json(receipts) + "\n");
756
- return;
757
- }
758
- if (command === "claim") {
759
- const token = values[0];
760
- if (!token)
761
- throw new Error("Usage: slrd claim <token|ca> --wallet <wallet>");
762
- emit(
763
- json(
764
- await slrd.claim(token, need(flags, "wallet"), {
765
- via: flags.get("sender") ?? "rpc",
766
- }),
767
- ) + "\n",
768
- );
769
- return;
770
- }
771
- if (command === "history") {
772
- emit(json(slrd.executions.history()) + "\n");
773
- return;
774
- }
775
- if (command === "group" && values[0] === "create") {
776
- const name = values[1];
777
- if (!name)
778
- throw new Error("Usage: slrd group create <name> [description]");
779
- emit(
780
- json(slrd.groups.create(name, values.slice(2).join(" ") || undefined)) +
781
- "\n",
782
- );
783
- return;
784
- }
785
- if (command === "group" && values[0] === "add") {
786
- const [_, group, wallet, weight] = values;
787
- if (!group || !wallet)
788
- throw new Error("Usage: slrd group add <group> <wallet> [weight_bps]");
789
- const resolved = slrd.resolveWallet(wallet);
790
- emit(
791
- json(
792
- slrd.groups.addWallet(
793
- group,
794
- resolved.address.toBase58(),
795
- weight ? Number(weight) : 10000,
796
- ),
797
- ) + "\n",
798
- );
799
- return;
800
- }
801
- if (command === "group" && values[0] === "add-many") {
802
- const group = values[1];
803
- const raw = values[2];
804
- if (!group || !raw)
805
- throw new Error(
806
- "Usage: slrd group add-many <group> <wallet1,wallet2,...>",
807
- );
808
- slrd.groups.create(group);
809
- const members = raw
810
- .split(",")
811
- .map((value) => value.trim())
812
- .filter(Boolean)
813
- .map((ref) => {
814
- const resolved = slrd.resolveWallet(ref);
815
- return slrd.groups.addWallet(
816
- group,
817
- resolved.address.toBase58(),
818
- 10000,
819
- );
820
- });
821
- emit(json({ group, added: members.length, members }) + "\n");
822
- return;
823
- }
824
- if (command === "group" && values[0] === "show") {
825
- const group = values[1];
826
- if (!group) throw new Error("Usage: slrd group show <group>");
827
- const members = slrd.groups.wallets(group).map((row) => {
828
- const wallet = slrd.resolveWallet(row.walletAddress);
829
- return {
830
- name: wallet.row?.name ?? null,
831
- address: wallet.address.toBase58(),
832
- weightBps: row.weightBps,
833
- };
834
- });
835
- emit(json({ group, members }) + "\n");
836
- return;
837
- }
838
- if (command === "group" && values[0] === "list") {
839
- emit(json(slrd.groups.list()) + "\n");
840
- return;
841
- }
842
- if (command === "agent" && values[0] === "create") {
843
- const name = values[1];
844
- if (!name)
845
- throw new Error(
846
- "Usage: slrd agent create <name> --wallet <wallet> [--config-json <json>]",
847
- );
848
- const supplied = flags.get("config-json")
849
- ? (JSON.parse(flags.get("config-json")!) as Record<string, unknown>)
850
- : {};
851
- const wallet = need(flags, "wallet");
852
- const agent = slrd.configureAgent(name, { ...supplied, wallet });
853
- emit(json(agent.row) + "\n");
854
- return;
855
- }
856
- if (command === "agent" && values[0] === "list") {
857
- emit(json(slrd.listAgents()) + "\n");
858
- return;
859
- }
860
- if (command === "watch" && values[0] === "token") {
861
- const ref = values[1];
862
- if (!ref) throw new Error("Usage: slrd watch token <token|ca> [label]");
863
- emit(json(slrd.watchToken(ref, values[2])) + "\n");
864
- return;
865
- }
866
- if (command === "watch" && values[0] === "wallet") {
867
- const ref = values[1];
868
- if (!ref) throw new Error("Usage: slrd watch wallet <wallet> [label]");
869
- emit(json(slrd.watchWallet(ref, values[2])) + "\n");
870
- return;
871
- }
872
- if (command === "watch" && values[0] === "program") {
873
- const address = values[1];
874
- if (!address)
875
- throw new Error("Usage: slrd watch program <address> [label]");
876
- emit(json(slrd.watchProgram(address, values[2])) + "\n");
877
- return;
878
- }
879
- if (command === "watch" && values[0] === "list") {
880
- emit(json(slrd.db.watches.select().where({ isActive: 1 }).all()) + "\n");
881
- return;
882
- }
883
- if (command === "alt" && values[0] === "add") {
884
- if (!values[1]) throw new Error("Usage: slrd alt add <address> [label]");
885
- emit(json(slrd.alts.register(values[1], values[2])) + "\n");
886
- return;
887
- }
888
- if (command === "alt" && values[0] === "list") {
889
- emit(json(slrd.alts.list()) + "\n");
890
- return;
891
- }
892
- if (command === "alt" && values[0] === "create") {
893
- emit(json(await slrd.createAlt(need(flags, "wallet"))) + "\n");
894
- return;
895
- }
896
- if (command === "alt" && values[0] === "extend") {
897
- if (!values[1] || values.length < 3)
898
- throw new Error(
899
- "Usage: slrd alt extend <address> --wallet <wallet> <account...>",
900
- );
901
- emit(
902
- json(
903
- await slrd.extendAlt(
904
- values[1],
905
- need(flags, "wallet"),
906
- values.slice(2),
907
- ),
908
- ) + "\n",
909
- );
910
- return;
911
- }
912
- throw new Error(`Unknown command: ${command}\n\n${help()}`);
913
- } finally {
914
- slrd.close();
915
- }
916
- }
917
- main().catch((error) => {
918
- emit(`${OWL} error: ${formatError(error)}\n`);
919
- process.exitCode = 1;
920
- });
921
-
922
- export { formatCliError } from "./pump/token-launch-cli.ts";
1
+ #!/usr/bin/env bun
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";
26
+
27
+ function emit(value: string): void {
28
+ process.stdout.write(value);
29
+ }
30
+
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
+ }
64
+ type Flags = Map<string, string>;
65
+ function args(input: string[]): { values: string[]; flags: Flags } {
66
+ const values: string[] = [],
67
+ flags = new Map<string, string>();
68
+ for (let i = 0; i < input.length; i++) {
69
+ const current = input[i]!;
70
+ if (current === "-w") {
71
+ flags.set("wallet", input[++i] ?? "");
72
+ continue;
73
+ }
74
+ if (current.startsWith("--")) {
75
+ const [key, inline] = current.slice(2).split("=", 2);
76
+ if (inline != null) flags.set(key!, inline);
77
+ else if (input[i + 1] && !input[i + 1]!.startsWith("-"))
78
+ flags.set(key!, input[++i]!);
79
+ else flags.set(key!, "true");
80
+ continue;
81
+ }
82
+ values.push(current);
83
+ }
84
+ return { values, flags };
85
+ }
86
+ function need(flags: Flags, key: string): string {
87
+ const value = flags.get(key);
88
+ if (!value || value === "true") throw new Error(`Missing --${key} <value>`);
89
+ return value;
90
+ }
91
+ function int(flags: Flags, key: string, fallback?: number): number | undefined {
92
+ const value = flags.get(key);
93
+ if (!value) return fallback;
94
+ const parsed = Number(value);
95
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid --${key}: ${value}`);
96
+ return parsed;
97
+ }
98
+ function json(value: unknown): string {
99
+ return JSON.stringify(
100
+ value,
101
+ (_, item) => (typeof item === "bigint" ? item.toString() : item),
102
+ 2,
103
+ );
104
+ }
105
+ function formatError(error: unknown): string {
106
+ if (error instanceof Error) {
107
+ const code =
108
+ "code" in error &&
109
+ typeof (error as Error & { code?: unknown }).code === "string"
110
+ ? ` [${String((error as Error & { code?: unknown }).code)}]`
111
+ : "";
112
+ const message = error.message.trim();
113
+ const summary = `${error.name}${code}${message ? `: ${message}` : " (no message)"}`;
114
+ return error.stack && error.stack !== summary
115
+ ? `${summary}\n${error.stack}`
116
+ : summary;
117
+ }
118
+ try {
119
+ const encoded = json(error);
120
+ return encoded && encoded !== "undefined" ? encoded : String(error);
121
+ } catch {
122
+ return String(error);
123
+ }
124
+ }
125
+ function duration(value: string | undefined, fallbackMs: number): number {
126
+ if (!value) return fallbackMs;
127
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i.exec(value.trim());
128
+ if (!match)
129
+ throw new Error(`Invalid duration: ${value}. Use e.g. 500ms, 5s, 15m, 1h.`);
130
+ const n = Number(match[1]);
131
+ const scale = ({ ms: 1, s: 1_000, m: 60_000, h: 3_600_000 } as const)[
132
+ (match[2]?.toLowerCase() ?? "ms") as "ms" | "s" | "m" | "h"
133
+ ];
134
+ return Math.floor(n * scale);
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
+
217
+ function formatPrice(value: number | null | undefined): string {
218
+ if (value == null || !Number.isFinite(value)) return "n/a";
219
+ return value >= 0.001
220
+ ? value.toFixed(9).replace(/0+$/, "").replace(/\.$/, "")
221
+ : value.toExponential(6);
222
+ }
223
+
224
+ function csv(value: string | undefined): string[] {
225
+ return (value ?? "")
226
+ .split(",")
227
+ .map((item) => item.trim())
228
+ .filter(Boolean);
229
+ }
230
+ function targetWallets(
231
+ slrd: { groupWallets(name: string): unknown[] },
232
+ flags: Flags,
233
+ usage: string,
234
+ ): { mode: "wallet" | "wallets" | "group"; refs: string[]; group?: string } {
235
+ const wallet = flags.get("wallet");
236
+ const wallets = csv(flags.get("wallets"));
237
+ const group = flags.get("group");
238
+ const selected = [Boolean(wallet), wallets.length > 0, Boolean(group)].filter(
239
+ Boolean,
240
+ ).length;
241
+ if (selected !== 1)
242
+ throw new Error(
243
+ `${usage} Supply exactly one of --wallet, --wallets, or --group.`,
244
+ );
245
+ if (wallet) return { mode: "wallet", refs: [wallet] };
246
+ if (wallets.length > 0) return { mode: "wallets", refs: wallets };
247
+ return {
248
+ mode: "group",
249
+ refs: slrd.groupWallets(group!).map((ref) => String(ref)),
250
+ group,
251
+ };
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
+
298
+ function help(): string {
299
+ return `${OWL} slrd — multi-wallet Solana CLI + SDK for traders and AI agents
300
+
301
+ Wallets and tokens
302
+ slrd wallet create [name] Generate and persist an encrypted Solana wallet
303
+ slrd import <private_key> [name]
304
+ cat key.json | slrd import --stdin [name]
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
316
+ slrd token <token_ca> [name] [--metadata-json <json>]
317
+ slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
318
+ slrd token refresh <token|ca>
319
+ slrd tokens
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
+
328
+ Metadata and launching
329
+ slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--alias <name>] [--live] [--skip-simulation]
330
+ [--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
331
+ slrd metadata upload --image <local_path> --name <name> --symbol <symbol> --description <text> [--provider pump-frontend|pinata]
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]
336
+ slrd deploy pump --wallet <wallet> --name <name> --symbol <symbol> (--uri <metadata_uri> | --image <local_path> --description <text>) [--alias <name>] [--live]
337
+ [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
338
+
339
+ Prices
340
+ slrd quote buy <token|ca> --sol <amount> Inspect buy quote without submitting
341
+ slrd price <token|ca> Sample current venue price
342
+ slrd price average <token|ca> --period 15m Average stored samples in a period
343
+ slrd price watch <token|ca...> [--interval 1s] [--period 1m]
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
+
364
+ Trading
365
+ slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live] Quote or execute SOL -> token through Jupiter
366
+ slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
367
+ slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
368
+ slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
369
+ slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
370
+ slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--continue-on-error] [--simulate-only]
371
+ slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
372
+
373
+ Scripts (strategies stay outside the kernel)
374
+ slrd scripts List scripts registered in slrd.config.ts
375
+ slrd run <name-or-path> [script flags...] Execute a script that imports slrd
376
+ slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
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
+
395
+ Groups and agents
396
+ slrd group create <name> [description]
397
+ slrd group add <group> <wallet> [weight_bps]
398
+ slrd group add-many <group> <wallet1,wallet2,...>
399
+ slrd group show <group>
400
+ slrd group list
401
+ slrd buy <token|ca> --group <name> --sol <amount> --sender jito Submit group buys as Jito bundles, five tx per bundle
402
+ slrd sell <token|ca> --group <name> --sender jito Submit group sells as Jito bundles, five tx per bundle
403
+ slrd agent create <name> --wallet <wallet> [--config-json <json>]
404
+ slrd agent list
405
+
406
+ Watching
407
+ slrd watch token <token|ca> [label]
408
+ slrd watch wallet <wallet> [label]
409
+ slrd watch program <address> [label]
410
+ slrd watch list
411
+
412
+ Transactions and ALTs
413
+ slrd history
414
+ slrd jito tip-accounts [--endpoint <block-engine-url>]
415
+ slrd alt add <address> [label]
416
+ slrd alt list
417
+ slrd alt create --wallet <wallet>
418
+ slrd alt extend <address> --wallet <wallet> <account...>
419
+
420
+ Environment
421
+ SLRD_DB_PATH shared SDK/CLI database; default ./slrd.db (SOLARD_DB_PATH alias supported)
422
+ SLRD_MASTER_KEY required to create/import/decrypt stored wallets
423
+ RPC_ENDPOINT required only for chain operations
424
+ HELIUS_SENDER_URL regional/global Helius Sender endpoint used by launch scripts
425
+ HELIUS_RPC_URL RPC endpoint used for ordinary Helius-RPC buyer lane
426
+ HELIUS_TIP_ACCOUNT, HELIUS_TIP_LAMPORTS, HELIUS_PRIORITY_MICRO_LAMPORTS launch transport policy
427
+ SLRD_LAUNCH_* launch resend/timeout policy; shared environment, not per-token metadata
428
+ JITO_BLOCK_ENGINE_URL only when explicitly selecting the separate Jito sender
429
+ SLRD_METADATA_UPLOADER metadata provider; default pump-frontend; optional pinata
430
+ PUMP_IPFS_ENDPOINT browser-facing Pump metadata upload endpoint; default https://pump.fun/api/ipfs
431
+ PINATA_JWT required only with --provider pinata or explicit pinata fallback
432
+ IPFS_PUBLIC_GATEWAY optional Pinata returned metadata gateway; default https://ipfs.io/ipfs
433
+ `;
434
+ }
435
+
436
+ async function main() {
437
+ const [command, ...rest] = process.argv.slice(2);
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
+
495
+ if (!command || command === "help" || command === "--help") {
496
+ emit(help());
497
+ return;
498
+ }
499
+ if (command === "version" || command === "--version" || command === "-v") {
500
+ const { readFileSync } = await import("node:fs");
501
+ const { join } = await import("node:path");
502
+ const pkg = JSON.parse(
503
+ readFileSync(join(import.meta.dir, "..", "package.json"), "utf8"),
504
+ ) as { name: string; version: string };
505
+ emit(`${OWL} ${pkg.name} ${pkg.version} (bun ${Bun.version})\n`);
506
+ return;
507
+ }
508
+ if (command === "vanity") {
509
+ const {
510
+ addVanityMintToPool,
511
+ cleanVanitySuffix,
512
+ defaultVanityMaxAttempts,
513
+ generateMintKeypairWithSuffix,
514
+ listVanityMintPool,
515
+ releaseVanityMintReservation,
516
+ saveMintKeypairFile,
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
+
588
+ const suffix = cleanVanitySuffix(need(flags, "suffix"));
589
+ const out = need(flags, "out");
590
+ const count = int(flags, "count", 1)!;
591
+ const results = [];
592
+ for (let i = 0; i < count; i++) {
593
+ const found = await generateMintKeypairWithSuffix({
594
+ suffix,
595
+ workers: int(flags, "workers"),
596
+ maxAttempts: int(
597
+ flags,
598
+ "max-attempts",
599
+ defaultVanityMaxAttempts(suffix),
600
+ )!,
601
+ timeoutMs: int(flags, "timeout-ms", 0)!,
602
+ reportEvery: int(flags, "report-every", 1_000_000)!,
603
+ onProgress: (p) =>
604
+ emit(
605
+ `${OWL} grinding ${p.suffix}: ${p.attempts} attempts, ${p.ratePerSecond}/s\n`,
606
+ ),
607
+ });
608
+ const path = count === 1 ? out : out.replace(/(\.json)?$/, `-${i + 1}$1`);
609
+ const saved = saveMintKeypairFile(path, found.mint, {
610
+ force: flags.has("force"),
611
+ });
612
+ results.push({
613
+ address: found.mint.publicKey.toBase58(),
614
+ keypairPath: saved,
615
+ attempts: found.attempts,
616
+ elapsedMs: found.elapsedMs,
617
+ launchArguments: ["--mint-keypair", saved, "--mint-suffix", suffix],
618
+ });
619
+ }
620
+ emit(json(count === 1 ? results[0] : results) + "\n");
621
+ return;
622
+ }
623
+ if (command === "spam-buy" || command === "buy-spam") {
624
+ const { runPumpSpamBuyFromArgs } = await import("./pump/spam-buy-cli.ts");
625
+ await runPumpSpamBuyFromArgs(rest);
626
+ return;
627
+ }
628
+ if (command === "buy" && flags.has("spam")) {
629
+ const { runPumpSpamBuyFromArgs } = await import("./pump/spam-buy-cli.ts");
630
+ await runPumpSpamBuyFromArgs(rest);
631
+ return;
632
+ }
633
+ if (command === "scripts") {
634
+ emit(json(await listScripts()) + "\n");
635
+ return;
636
+ }
637
+ if (command === "run") {
638
+ const [script, ...scriptArgs] = rest;
639
+ if (!script)
640
+ throw new Error("Usage: slrd run <name-or-path> [script flags...]");
641
+ process.exitCode = await runScript(script, scriptArgs);
642
+ return;
643
+ }
644
+ if (
645
+ command === "launch" &&
646
+ (values[0] === "pump" || values[0] === "pump-token")
647
+ ) {
648
+ const { runPumpTokenLaunchFromArgs } =
649
+ await import("./pump/token-launch-cli.ts");
650
+ await runPumpTokenLaunchFromArgs(rest.slice(1), {
651
+ defaultSubmitMode: "after-deploy-processed",
652
+ defaultDeploymentPriorityMicroLamports: 500_000,
653
+ defaultBuyerPriorityMicroLamports: 1_500_000,
654
+ defaultSlippageBps: 1_500,
655
+ persistOnLive: true,
656
+ report: (label, value) => emit(`${label}: ${json(value)}\n`),
657
+ });
658
+ return;
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
+ }
672
+ if (command === "jito" && values[0] === "tip-accounts") {
673
+ const endpoint = (
674
+ flags.get("endpoint") ??
675
+ process.env.JITO_BLOCK_ENGINE_URL ??
676
+ "https://mainnet.block-engine.jito.wtf"
677
+ ).replace(/\/$/, "");
678
+ const { getJitoTipAccounts } = await import("@solard/sdk");
679
+ const tipAccounts = await getJitoTipAccounts(endpoint);
680
+ emit(json({ endpoint, tipAccounts }) + "\n");
681
+ return;
682
+ }
683
+ if (command === "metadata" && values[0] === "upload") {
684
+ const { uploadPumpMetadata } = await import("@solard/sdk");
685
+ const uploaded = await uploadPumpMetadata(
686
+ {
687
+ imagePath: need(flags, "image"),
688
+ name: need(flags, "name"),
689
+ symbol: need(flags, "symbol"),
690
+ description: need(flags, "description"),
691
+ twitter: flags.get("twitter"),
692
+ telegram: flags.get("telegram"),
693
+ website: flags.get("website"),
694
+ video: flags.get("video"),
695
+ showName: !flags.has("hide-name"),
696
+ },
697
+ {
698
+ provider: (flags.get("provider") ??
699
+ process.env.SLRD_METADATA_UPLOADER ??
700
+ "pump-frontend") as "pump-frontend" | "pinata",
701
+ endpoint: flags.get("endpoint"),
702
+ fallback: flags.get("fallback") === "pinata" ? "pinata" : null,
703
+ },
704
+ );
705
+ emit(json(uploaded) + "\n");
706
+ return;
707
+ }
708
+ const [{ sol, formatRaw }, { shortKey }, { createTraderSolard }] =
709
+ await Promise.all([
710
+ import("@solard/sdk"),
711
+ import("@solard/sdk"),
712
+ import("@solard/sdk"),
713
+ ]);
714
+ const slrd = createTraderSolard();
715
+ try {
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") {
843
+ const wallet = slrd.createWallet(values[1]);
844
+ emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
845
+ return;
846
+ }
847
+ if (command === "export") {
848
+ const ref = values[0];
849
+ if (!ref)
850
+ throw new Error(
851
+ "Usage: slrd export <wallet|address> [--json] [--out <path>]",
852
+ );
853
+ const bs58 = (await import("bs58")).default;
854
+ const { signer, row } = slrd.wallets.signer(ref);
855
+ const secret = flags.has("json")
856
+ ? JSON.stringify(Array.from(signer.secretKey))
857
+ : bs58.encode(signer.secretKey);
858
+
859
+ const out = flags.get("out");
860
+ if (out && out !== "true") {
861
+ const { writeFileSync, mkdirSync } = await import("node:fs");
862
+ const { dirname, resolve } = await import("node:path");
863
+ const path = resolve(out);
864
+ mkdirSync(dirname(path), { recursive: true });
865
+ writeFileSync(path, `${secret}\n`, { mode: 0o600, flag: "wx" });
866
+ emit(
867
+ `${OWL} exported @${row?.name ?? "?"} ${signer.publicKey.toBase58()} -> ${path}\n`,
868
+ );
869
+ return;
870
+ }
871
+
872
+ // Secret to stdout only; metadata to stderr so piping stays clean.
873
+ process.stderr.write(
874
+ `${OWL} @${row?.name ?? "?"} ${signer.publicKey.toBase58()} (${flags.has("json") ? "keypair-json" : "base58"})\n`,
875
+ );
876
+ process.stdout.write(`${secret}\n`);
877
+ return;
878
+ }
879
+ if (command === "import") {
880
+ const input = flags.has("stdin")
881
+ ? (await new Response(Bun.stdin.stream()).text()).trim()
882
+ : values[0];
883
+ if (!input)
884
+ throw new Error(
885
+ "Usage: slrd import <private_key> [name] [--force] or cat key.json | slrd import --stdin [name] [--force]",
886
+ );
887
+ const name = flags.has("stdin") ? values[0] : values[1];
888
+ const wallet = slrd.importWallet(input, name, {
889
+ overwrite: flags.has("force") || flags.has("overwrite"),
890
+ });
891
+ emit(`${OWL} imported @${wallet.name} ${wallet.address}\n`);
892
+ return;
893
+ }
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
+
925
+ if (flags.has("addresses-only")) {
926
+ for (const wallet of wallets)
927
+ emit(`@${wallet.name}\t${wallet.address}\n`);
928
+ return;
929
+ }
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");
1412
+ emit(
1413
+ "TIME NATIVE Δ SOL FEE SOL WSOL Δ TOKEN DELTAS STATUS SIGNATURE\n",
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
+ }
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
+ );
1448
+ return;
1449
+ }
1450
+ if (command === "deploy") {
1451
+ const launchpad = values[0] ?? "pump";
1452
+ const wallet = need(flags, "wallet");
1453
+ const name = need(flags, "name");
1454
+ const symbol = need(flags, "symbol");
1455
+ let uri = flags.get("uri");
1456
+ let uploadedMetadata: unknown = null;
1457
+ if (!uri || uri === "true") {
1458
+ if (!flags.get("image"))
1459
+ throw new Error(
1460
+ "Provide --uri <metadata_uri> or --image <local_path> --description <text>",
1461
+ );
1462
+ const { uploadPumpMetadata } = await import("@solard/sdk");
1463
+ uploadedMetadata = await uploadPumpMetadata(
1464
+ {
1465
+ imagePath: need(flags, "image"),
1466
+ name,
1467
+ symbol,
1468
+ description: need(flags, "description"),
1469
+ twitter: flags.get("twitter"),
1470
+ telegram: flags.get("telegram"),
1471
+ website: flags.get("website"),
1472
+ video: flags.get("video"),
1473
+ showName: !flags.has("hide-name"),
1474
+ },
1475
+ {
1476
+ provider: (flags.get("provider") ??
1477
+ process.env.SLRD_METADATA_UPLOADER ??
1478
+ "pump-frontend") as "pump-frontend" | "pinata",
1479
+ endpoint: flags.get("endpoint"),
1480
+ fallback: flags.get("fallback") === "pinata" ? "pinata" : null,
1481
+ },
1482
+ );
1483
+ uri = (uploadedMetadata as { metadataUri: string }).metadataUri;
1484
+ }
1485
+ const creator = flags.get("creator")
1486
+ ? slrd.resolveWallet(flags.get("creator")!).address
1487
+ : slrd.signer(wallet).publicKey;
1488
+ const deployment = await slrd.prepareTokenDeployment(launchpad, wallet, {
1489
+ name,
1490
+ symbol,
1491
+ uri,
1492
+ creator,
1493
+ mayhemMode: flags.has("mayhem"),
1494
+ cashback: flags.has("cashback"),
1495
+ });
1496
+ const plan = await slrd
1497
+ .transaction(wallet)
1498
+ .addMany(deployment.instructions, {
1499
+ kind: "deploy-token",
1500
+ mint: deployment.mint.publicKey,
1501
+ meta: { launchpad, name, symbol },
1502
+ })
1503
+ .withSigner(deployment.mint)
1504
+ .build();
1505
+ const header = {
1506
+ launchpad,
1507
+ mint: deployment.mint.publicKey.toBase58(),
1508
+ name,
1509
+ symbol,
1510
+ uri,
1511
+ uploadedMetadata,
1512
+ live: flags.has("live"),
1513
+ };
1514
+ if (!flags.has("live")) {
1515
+ const simulation = await slrd.simulatePlan(plan);
1516
+ emit(json({ ...header, simulation }) + "\n");
1517
+ return;
1518
+ }
1519
+ const receipt = await slrd.sendPlan(
1520
+ plan,
1521
+ flags.get("sender") ?? "rpc",
1522
+ `deploy:${launchpad}`,
1523
+ {
1524
+ skipSimulation: flags.has("skip-simulation"),
1525
+ skipPreflight: flags.has("skip-simulation"),
1526
+ },
1527
+ );
1528
+ const token = slrd.persistPreparedDeployment(
1529
+ deployment,
1530
+ flags.get("alias"),
1531
+ );
1532
+ emit(json({ ...header, receipt, token }) + "\n");
1533
+ return;
1534
+ }
1535
+ if (command === "token" && values[0] !== "set" && values[0] !== "refresh") {
1536
+ const [mint, name] = values;
1537
+ if (!mint) throw new Error("Usage: slrd token <token_ca> [name]");
1538
+ const metadata = flags.get("metadata-json");
1539
+ const token = await slrd.addToken(
1540
+ mint,
1541
+ name,
1542
+ metadata ? { metadataJson: metadata } : {},
1543
+ );
1544
+ emit(
1545
+ `${OWL} token ${token.name ?? token.symbol ?? "-"} ${token.mint} venue=${token.venueHint}\n`,
1546
+ );
1547
+ return;
1548
+ }
1549
+ if (command === "token" && values[0] === "refresh") {
1550
+ const ref = values[1];
1551
+ if (!ref) throw new Error("Usage: slrd token refresh <token|ca>");
1552
+ emit(json(await slrd.refreshToken(ref)) + "\n");
1553
+ return;
1554
+ }
1555
+ if (command === "token" && values[0] === "set") {
1556
+ const ref = values[1];
1557
+ if (!ref) throw new Error("Usage: slrd token set <token|ca> [flags]");
1558
+ const patch: Partial<TokenRow> = {};
1559
+ if (flags.get("pool")) patch.pool = flags.get("pool");
1560
+ if (flags.get("quote-mint")) patch.quoteMint = flags.get("quote-mint");
1561
+ if (flags.get("quote-program"))
1562
+ patch.quoteTokenProgram = flags.get("quote-program");
1563
+ if (flags.get("metadata-json"))
1564
+ patch.metadataJson = flags.get("metadata-json");
1565
+ const token = slrd.configureToken(ref, patch);
1566
+ emit(`${OWL} updated ${token.name ?? shortKey(token.mint)}\n`);
1567
+ return;
1568
+ }
1569
+ if (command === "tokens") {
1570
+ for (const token of slrd.tokens.list())
1571
+ emit(
1572
+ `${token.name ?? "-"}\t${token.symbol ? "$" + token.symbol : "-"}\t${token.mint}\t${token.venueHint}\n`,
1573
+ );
1574
+ return;
1575
+ }
1576
+ if (command === "quote" && values[0] === "buy") {
1577
+ const tokenRef = values[1];
1578
+ if (!tokenRef)
1579
+ throw new Error(
1580
+ "Usage: slrd quote buy <token|ca> --sol <amount> [--slippage-bps 1500]",
1581
+ );
1582
+ const spend = need(flags, "sol");
1583
+ const result = await slrd.quoteBuy(
1584
+ tokenRef,
1585
+ sol(spend),
1586
+ int(flags, "slippage-bps", 1500),
1587
+ );
1588
+ emit(
1589
+ json({
1590
+ token: {
1591
+ mint: result.token.mint,
1592
+ name: result.token.name,
1593
+ symbol: result.token.symbol,
1594
+ },
1595
+ venue: result.venue,
1596
+ quoteAsset:
1597
+ result.quoteAsset.kind === "native-sol"
1598
+ ? "SOL"
1599
+ : result.quoteAsset.mint.toBase58(),
1600
+ spendRaw: result.quote.inputRaw,
1601
+ expectedOutputRaw: result.quote.expectedOutputRaw,
1602
+ minimumOutputRaw: result.quote.minimumOutputRaw,
1603
+ meta: result.quote.meta ?? {},
1604
+ }) + "\n",
1605
+ );
1606
+ return;
1607
+ }
1608
+ if (command === "price" && values[0] === "average") {
1609
+ const tokenRef = values[1];
1610
+ if (!tokenRef)
1611
+ throw new Error("Usage: slrd price average <token|ca> --period 15m");
1612
+ const token = slrd.resolveToken(tokenRef);
1613
+ const window = slrd.averagePrice(
1614
+ token,
1615
+ duration(flags.get("period"), 60_000),
1616
+ );
1617
+ emit(
1618
+ `${token.symbol ? "$" + token.symbol : (token.name ?? shortKey(token.mint))} samples=${window.samples} avg=${formatPrice(window.averagePriceQuotePerToken)} min=${formatPrice(window.minimumPriceQuotePerToken)} max=${formatPrice(window.maximumPriceQuotePerToken)} last=${formatPrice(window.lastPriceQuotePerToken)}\n`,
1619
+ );
1620
+ return;
1621
+ }
1622
+ if (command === "price" && values[0] === "watch") {
1623
+ const tokenRefs = values.slice(1);
1624
+ if (tokenRefs.length === 0)
1625
+ throw new Error(
1626
+ "Usage: slrd price watch <token|ca...> [--interval 1s] [--period 1m]",
1627
+ );
1628
+ const controller = new AbortController();
1629
+ process.once("SIGINT", () => controller.abort());
1630
+ for await (const event of slrd.watchPrices(tokenRefs, {
1631
+ intervalMs: duration(flags.get("interval"), 1_000),
1632
+ averagePeriodMs: duration(flags.get("period"), 60_000),
1633
+ signal: controller.signal,
1634
+ })) {
1635
+ const label = event.token.symbol
1636
+ ? "$" + event.token.symbol
1637
+ : (event.token.name ?? shortKey(event.token.mint));
1638
+ emit(
1639
+ `${new Date(event.sample.capturedAtMs).toISOString()} ${label} venue=${event.sample.venue} price=${formatPrice(event.sample.priceQuotePerToken)} avg=${formatPrice(event.average.averagePriceQuotePerToken)} samples=${event.average.samples}\n`,
1640
+ );
1641
+ }
1642
+ return;
1643
+ }
1644
+ if (command === "price") {
1645
+ const tokenRef = values[0];
1646
+ if (!tokenRef) throw new Error("Usage: slrd price <token|ca>");
1647
+ const token = slrd.resolveToken(tokenRef);
1648
+ const sample = await slrd.samplePrice(token);
1649
+ const label = token.symbol
1650
+ ? "$" + token.symbol
1651
+ : (token.name ?? shortKey(token.mint));
1652
+ emit(
1653
+ `${label} venue=${sample.venue} price=${formatPrice(sample.priceQuotePerToken)} quote=${sample.quoteAsset.kind === "native-sol" ? "SOL" : shortKey(sample.quoteAsset.mint.toBase58())} captured=${new Date(sample.capturedAtMs).toISOString()}\n`,
1654
+ );
1655
+ return;
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
+
2115
+ if (command === "buy") {
2116
+ const token = values[0];
2117
+ if (!token)
2118
+ throw new Error(
2119
+ "Usage: slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) --sol <amount>",
2120
+ );
2121
+ const amount = flags.get("sol");
2122
+ if (!amount) throw new Error("Buy requires explicit --sol <amount>");
2123
+ const targets = targetWallets(
2124
+ slrd,
2125
+ flags,
2126
+ "Usage: slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>) --sol <amount>.",
2127
+ );
2128
+ const options = {
2129
+ slippageBps: int(flags, "slippage-bps", 1500),
2130
+ via: flags.get("sender") ?? "rpc",
2131
+ skipSimulation: flags.has("skip-simulation"),
2132
+ skipPreflight:
2133
+ flags.has("skip-preflight") || flags.has("skip-simulation"),
2134
+ };
2135
+ if (flags.has("simulate-only")) {
2136
+ const plans =
2137
+ targets.refs.length === 1
2138
+ ? [
2139
+ await slrd
2140
+ .tx(targets.refs[0]!)
2141
+ .buy(token, sol(amount), options)
2142
+ .build(),
2143
+ ]
2144
+ : await slrd
2145
+ .composeMany(targets.refs)
2146
+ .buy(token, sol(amount), options)
2147
+ .build();
2148
+ const results = await Promise.all(
2149
+ plans.map((plan) => slrd.simulatePlan(plan)),
2150
+ );
2151
+ emit(json({ mode: "simulation", target: targets, results }) + "\n");
2152
+ return;
2153
+ }
2154
+ const receipts =
2155
+ targets.refs.length === 1
2156
+ ? await slrd.buy(token, targets.refs[0]!, sol(amount), options)
2157
+ : await slrd.buyMany(token, targets.refs, sol(amount), options);
2158
+ emit(json(receipts) + "\n");
2159
+ return;
2160
+ }
2161
+ if (command === "sell") {
2162
+ const token = values[0];
2163
+ if (!token)
2164
+ throw new Error(
2165
+ "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>)",
2166
+ );
2167
+ const targets = targetWallets(
2168
+ slrd,
2169
+ flags,
2170
+ "Usage: slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2171
+ );
2172
+ const sellOptions = {
2173
+ bps: int(flags, "bps", 10000),
2174
+ slippageBps: int(flags, "slippage-bps", 1500),
2175
+ via: flags.get("sender") ?? "rpc",
2176
+ skipSimulation: flags.has("skip-simulation"),
2177
+ skipPreflight:
2178
+ flags.has("skip-preflight") || flags.has("skip-simulation"),
2179
+ };
2180
+ if (flags.has("simulate-only")) {
2181
+ const plans =
2182
+ targets.refs.length === 1
2183
+ ? [await slrd.tx(targets.refs[0]!).sell(token, sellOptions).build()]
2184
+ : await slrd
2185
+ .composeMany(targets.refs)
2186
+ .sell(token, sellOptions)
2187
+ .build();
2188
+ const results = await Promise.all(
2189
+ plans.map((plan) => slrd.simulatePlan(plan)),
2190
+ );
2191
+ emit(json({ mode: "simulation", target: targets, results }) + "\n");
2192
+ return;
2193
+ }
2194
+ const receipts =
2195
+ targets.refs.length === 1
2196
+ ? await slrd.sell(token, targets.refs[0]!, sellOptions)
2197
+ : await slrd.sellMany(token, targets.refs, sellOptions);
2198
+ emit(json(receipts) + "\n");
2199
+ return;
2200
+ }
2201
+ if (
2202
+ command === "unwrap-wsol" ||
2203
+ (command === "unwrap" && values[0] === "wsol")
2204
+ ) {
2205
+ const targets = targetWallets(
2206
+ slrd,
2207
+ flags,
2208
+ "Usage: slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <group>).",
2209
+ );
2210
+ const destination = flags.get("destination");
2211
+ if (destination && targets.refs.length !== 1)
2212
+ throw new Error(
2213
+ "--destination is only supported with a single --wallet unwrap.",
2214
+ );
2215
+ const unwrapOptions = {
2216
+ via: flags.get("sender") ?? "rpc",
2217
+ destination,
2218
+ skipMissing: flags.has("ignore-missing") || flags.has("skip-missing"),
2219
+ skipSimulation: flags.has("skip-simulation"),
2220
+ skipPreflight:
2221
+ flags.has("skip-preflight") || flags.has("skip-simulation"),
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.
2321
+ if (flags.has("simulate-only")) {
2322
+ const plans =
2323
+ targets.refs.length === 1
2324
+ ? [
2325
+ await slrd
2326
+ .tx(targets.refs[0]!)
2327
+ .unwrapWsol({
2328
+ destination,
2329
+ skipMissing: unwrapOptions.skipMissing,
2330
+ })
2331
+ .build(),
2332
+ ]
2333
+ : await slrd
2334
+ .composeMany(targets.refs)
2335
+ .unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
2336
+ .build();
2337
+ const results = await Promise.all(
2338
+ plans.map((plan) => slrd.simulatePlan(plan)),
2339
+ );
2340
+ emit(json({ mode: "simulation", target: targets, results }) + "\n");
2341
+ return;
2342
+ }
2343
+
2344
+ const receipts =
2345
+ targets.refs.length === 1
2346
+ ? await slrd.unwrapWsol(targets.refs[0]!, unwrapOptions)
2347
+ : await slrd.unwrapWsolMany(targets.refs, unwrapOptions);
2348
+ emit(json(receipts) + "\n");
2349
+ return;
2350
+ }
2351
+ if (command === "claim") {
2352
+ const token = values[0];
2353
+ if (!token)
2354
+ throw new Error("Usage: slrd claim <token|ca> --wallet <wallet>");
2355
+ emit(
2356
+ json(
2357
+ await slrd.claim(token, need(flags, "wallet"), {
2358
+ via: flags.get("sender") ?? "rpc",
2359
+ }),
2360
+ ) + "\n",
2361
+ );
2362
+ return;
2363
+ }
2364
+ if (command === "history") {
2365
+ emit(json(slrd.executions.history()) + "\n");
2366
+ return;
2367
+ }
2368
+ if (command === "group" && values[0] === "create") {
2369
+ const name = values[1];
2370
+ if (!name)
2371
+ throw new Error("Usage: slrd group create <name> [description]");
2372
+ emit(
2373
+ json(slrd.groups.create(name, values.slice(2).join(" ") || undefined)) +
2374
+ "\n",
2375
+ );
2376
+ return;
2377
+ }
2378
+ if (command === "group" && values[0] === "add") {
2379
+ const [_, group, wallet, weight] = values;
2380
+ if (!group || !wallet)
2381
+ throw new Error("Usage: slrd group add <group> <wallet> [weight_bps]");
2382
+ const resolved = slrd.resolveWallet(wallet);
2383
+ emit(
2384
+ json(
2385
+ slrd.groups.addWallet(
2386
+ group,
2387
+ resolved.address.toBase58(),
2388
+ weight ? Number(weight) : 10000,
2389
+ ),
2390
+ ) + "\n",
2391
+ );
2392
+ return;
2393
+ }
2394
+ if (command === "group" && values[0] === "add-many") {
2395
+ const group = values[1];
2396
+ const raw = values[2];
2397
+ if (!group || !raw)
2398
+ throw new Error(
2399
+ "Usage: slrd group add-many <group> <wallet1,wallet2,...>",
2400
+ );
2401
+ slrd.groups.create(group);
2402
+ const members = raw
2403
+ .split(",")
2404
+ .map((value) => value.trim())
2405
+ .filter(Boolean)
2406
+ .map((ref) => {
2407
+ const resolved = slrd.resolveWallet(ref);
2408
+ return slrd.groups.addWallet(
2409
+ group,
2410
+ resolved.address.toBase58(),
2411
+ 10000,
2412
+ );
2413
+ });
2414
+ emit(json({ group, added: members.length, members }) + "\n");
2415
+ return;
2416
+ }
2417
+ if (command === "group" && values[0] === "show") {
2418
+ const group = values[1];
2419
+ if (!group) throw new Error("Usage: slrd group show <group>");
2420
+ const members = slrd.groups.wallets(group).map((row) => {
2421
+ const wallet = slrd.resolveWallet(row.walletAddress);
2422
+ return {
2423
+ name: wallet.row?.name ?? null,
2424
+ address: wallet.address.toBase58(),
2425
+ weightBps: row.weightBps,
2426
+ };
2427
+ });
2428
+ emit(json({ group, members }) + "\n");
2429
+ return;
2430
+ }
2431
+ if (command === "group" && values[0] === "list") {
2432
+ emit(json(slrd.groups.list()) + "\n");
2433
+ return;
2434
+ }
2435
+ if (command === "agent" && values[0] === "create") {
2436
+ const name = values[1];
2437
+ if (!name)
2438
+ throw new Error(
2439
+ "Usage: slrd agent create <name> --wallet <wallet> [--config-json <json>]",
2440
+ );
2441
+ const supplied = flags.get("config-json")
2442
+ ? (JSON.parse(flags.get("config-json")!) as Record<string, unknown>)
2443
+ : {};
2444
+ const wallet = need(flags, "wallet");
2445
+ const agent = slrd.configureAgent(name, { ...supplied, wallet });
2446
+ emit(json(agent.row) + "\n");
2447
+ return;
2448
+ }
2449
+ if (command === "agent" && values[0] === "list") {
2450
+ emit(json(slrd.listAgents()) + "\n");
2451
+ return;
2452
+ }
2453
+ if (command === "watch" && values[0] === "token") {
2454
+ const ref = values[1];
2455
+ if (!ref) throw new Error("Usage: slrd watch token <token|ca> [label]");
2456
+ emit(json(slrd.watchToken(ref, values[2])) + "\n");
2457
+ return;
2458
+ }
2459
+ if (command === "watch" && values[0] === "wallet") {
2460
+ const ref = values[1];
2461
+ if (!ref) throw new Error("Usage: slrd watch wallet <wallet> [label]");
2462
+ emit(json(slrd.watchWallet(ref, values[2])) + "\n");
2463
+ return;
2464
+ }
2465
+ if (command === "watch" && values[0] === "program") {
2466
+ const address = values[1];
2467
+ if (!address)
2468
+ throw new Error("Usage: slrd watch program <address> [label]");
2469
+ emit(json(slrd.watchProgram(address, values[2])) + "\n");
2470
+ return;
2471
+ }
2472
+ if (command === "watch" && values[0] === "list") {
2473
+ emit(json(slrd.db.watches.select().where({ isActive: 1 }).all()) + "\n");
2474
+ return;
2475
+ }
2476
+ if (command === "alt" && values[0] === "add") {
2477
+ if (!values[1]) throw new Error("Usage: slrd alt add <address> [label]");
2478
+ emit(json(slrd.alts.register(values[1], values[2])) + "\n");
2479
+ return;
2480
+ }
2481
+ if (command === "alt" && values[0] === "list") {
2482
+ emit(json(slrd.alts.list()) + "\n");
2483
+ return;
2484
+ }
2485
+ if (command === "alt" && values[0] === "create") {
2486
+ emit(json(await slrd.createAlt(need(flags, "wallet"))) + "\n");
2487
+ return;
2488
+ }
2489
+ if (command === "alt" && values[0] === "extend") {
2490
+ if (!values[1] || values.length < 3)
2491
+ throw new Error(
2492
+ "Usage: slrd alt extend <address> --wallet <wallet> <account...>",
2493
+ );
2494
+ emit(
2495
+ json(
2496
+ await slrd.extendAlt(
2497
+ values[1],
2498
+ need(flags, "wallet"),
2499
+ values.slice(2),
2500
+ ),
2501
+ ) + "\n",
2502
+ );
2503
+ return;
2504
+ }
2505
+ throw new Error(`Unknown command: ${command}\n\n${help()}`);
2506
+ } finally {
2507
+ slrd.close();
2508
+ }
2509
+ }
2510
+ main().catch((error) => {
2511
+ emit(`${OWL} error: ${formatError(error)}\n`);
2512
+ process.exitCode = 1;
2513
+ });
2514
+
2515
+ export { formatCliError } from "./pump/token-launch-cli.ts";