@solard/cli 0.2.13 → 0.2.15

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
@@ -13,6 +13,7 @@ import {
13
13
  planRegistryTokenLiquidation,
14
14
  removeExternalContact,
15
15
  resetSolardRpcStats,
16
+ resolvePumpQuoteAsset,
16
17
  resolveTokenMintForPolicy,
17
18
  runScript,
18
19
  listScripts,
@@ -23,6 +24,13 @@ import {
23
24
  } from "@solard/sdk";
24
25
 
25
26
  import { handleMeteoraCommand } from "./meteora-commands.ts";
27
+ import { handleFairfunCommand } from "./fairfun-commands.ts";
28
+ import { resolveDestinationRef } from "./refs.ts";
29
+ import {
30
+ VaultOnboardingError,
31
+ cliVaultStatus,
32
+ ensureCliVaultReady,
33
+ } from "./vault-onboarding.ts";
26
34
 
27
35
  function emit(value: string): void {
28
36
  process.stdout.write(value);
@@ -32,35 +40,6 @@ const OWL = "🦉";
32
40
  const NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112";
33
41
  const CANONICAL_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
34
42
 
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
43
  type Flags = Map<string, string>;
65
44
  function args(input: string[]): { values: string[]; flags: Flags } {
66
45
  const values: string[] = [],
@@ -122,6 +101,24 @@ function formatError(error: unknown): string {
122
101
  return String(error);
123
102
  }
124
103
  }
104
+
105
+ function friendlyCliError(error: unknown): string | null {
106
+ const debug =
107
+ process.argv.includes("--debug") ||
108
+ process.env.SLRD_DEBUG === "1" ||
109
+ process.env.SOLARD_DEBUG === "1";
110
+ if (debug || !(error instanceof Error)) return null;
111
+ const code =
112
+ "code" in error &&
113
+ typeof (error as Error & { code?: unknown }).code === "string"
114
+ ? String((error as Error & { code?: unknown }).code)
115
+ : null;
116
+ if (error instanceof VaultOnboardingError) return error.message;
117
+ if (code === "MISSING_CONFIG" && error.message.includes("SLRD_MASTER_KEY")) {
118
+ return `${error.message}\n\nEnter the wallet password interactively, or set SLRD_MASTER_KEY for automation/CI.`;
119
+ }
120
+ return null;
121
+ }
125
122
  function duration(value: string | undefined, fallbackMs: number): number {
126
123
  if (!value) return fallbackMs;
127
124
  const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i.exec(value.trim());
@@ -202,10 +199,22 @@ function txWalletTokenBalances(
202
199
  return out;
203
200
  }
204
201
 
202
+ function formatRawLocal(value: bigint, decimals: number): string {
203
+ const unit = 10n ** BigInt(Math.max(0, decimals));
204
+ const whole = value / unit;
205
+ const fraction = value % unit;
206
+ if (decimals <= 0 || fraction === 0n) return whole.toString();
207
+ const fractionText = fraction
208
+ .toString()
209
+ .padStart(decimals, "0")
210
+ .replace(/0+$/, "");
211
+ return `${whole.toString()}.${fractionText}`;
212
+ }
213
+
205
214
  function formatSignedRaw(value: bigint, decimals: number): string {
206
215
  const sign = value > 0n ? "+" : value < 0n ? "-" : "";
207
216
  const abs = value < 0n ? -value : value;
208
- return `${sign}${formatRaw(abs, decimals)}`;
217
+ return `${sign}${formatRawLocal(abs, decimals)}`;
209
218
  }
210
219
  function formatDurationMs(value: number): string {
211
220
  if (!Number.isFinite(value) || value < 0) return "0ms";
@@ -250,55 +259,80 @@ function targetWallets(
250
259
  group,
251
260
  };
252
261
  }
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
- );
262
+ function commandNeedsSigningVault(
263
+ command: string | undefined,
264
+ values: string[],
265
+ flags: Flags,
266
+ ): boolean {
267
+ if (!command) return false;
268
+ if (command === "wallet" && values[0] === "create") return true;
269
+ if (command === "rewards" && values[0] === "claim") return true;
270
+ if (command === "rewards" && values[0] === "distribute")
271
+ return flags.has("live");
272
+ if (command === "transfer-many") {
273
+ if (values[0] === "status") return false;
274
+ if (values[0] === "resume") return true;
275
+ return flags.has("live");
277
276
  }
277
+ if (
278
+ [
279
+ "import",
280
+ "export",
281
+ "transfer",
282
+ "send-sol",
283
+ "buy",
284
+ "sell",
285
+ "unwrap-wsol",
286
+ "claim",
287
+ ].includes(command)
288
+ )
289
+ return true;
290
+ if (command === "alt" && (values[0] === "create" || values[0] === "extend"))
291
+ return true;
292
+ if (command === "vanity" && values[0] === "pool" && values[1] === "generate")
293
+ return true;
294
+
295
+ // Local Pump launch planning/building resolves a real signing wallet even in
296
+ // dry-run mode. External-wallet preparation is deliberately public-key-only.
297
+ if (
298
+ command === "launch" &&
299
+ (values[0] === "pump" || values[0] === "pump-token")
300
+ )
301
+ return !flags.has("prepare");
302
+ if (command === "deploy" || command === "vamp") return true;
278
303
 
279
- if (contact) {
280
- return {
281
- input,
282
- address: contact.address,
283
- contactName: contact.name,
284
- };
285
- }
304
+ if (
305
+ flags.has("live") &&
306
+ [
307
+ "swap",
308
+ "sweep",
309
+ "liquidate",
310
+ "meteora",
311
+ "raydium",
312
+ "spam-buy",
313
+ "buy-spam",
314
+ "run",
315
+ ].includes(command)
316
+ )
317
+ return true;
318
+ return false;
319
+ }
286
320
 
287
- if (walletAddress) {
288
- return {
289
- input,
290
- address: walletAddress,
291
- walletName: input.replace(/^@/, ""),
292
- };
321
+ async function storedWalletCount(): Promise<number> {
322
+ const { createTraderSolard } = await import("@solard/sdk");
323
+ const probe = createTraderSolard();
324
+ try {
325
+ return probe.wallets.list().length;
326
+ } finally {
327
+ probe.close();
293
328
  }
294
-
295
- return { input, address: input };
296
329
  }
297
330
 
298
331
  function help(): string {
299
332
  return `${OWL} slrd — multi-wallet Solana CLI + SDK for traders and AI agents
300
333
 
301
334
  Wallets and tokens
335
+ slrd setup [--status] Check ephemeral wallet-password mode; no password is persisted
302
336
  slrd wallet create [name] Generate and persist an encrypted Solana wallet
303
337
  slrd import <private_key> [name]
304
338
  cat key.json | slrd import --stdin [name]
@@ -316,7 +350,13 @@ External contacts (public addresses only; never signing wallets or group members
316
350
  slrd token <token_ca> [name] [--metadata-json <json>]
317
351
  slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
318
352
  slrd token refresh <token|ca>
353
+ slrd token backfill <ca> [--replace] [--confirmed] [--batch-size 100] [--rpc-concurrency 3] [--max-signatures N] [--json]
354
+ Durable exact trades + sparse 1s candle materialization from creation
355
+ slrd token trades <ca> [--from-start] [--min-sol N] [--owner <wallet>] [--side buy|sell] [--limit N] [--json]
356
+ slrd token analyze <ca> [--top N] [--json]
319
357
  slrd tokens
358
+ slrd holders <token|ca> [--exclude <a,b>] [--min-raw N] [--json] Complete on-chain holder snapshot
359
+ slrd events <token|ca> [--swaps-only|--transfers-only] [--jsonl] Live typed Pump/PumpSwap + transfer stream
320
360
 
321
361
  Vanity mints
322
362
  slrd vanity --suffix pump --out .\\mint.json [--count <n>]
@@ -326,14 +366,16 @@ Vanity mints
326
366
  launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
327
367
 
328
368
  Metadata and launching
329
- slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--alias <name>] [--live] [--skip-simulation]
369
+ slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--pair SOL|USDC|<custom-quote-mint>] [--beneficiary <wallet|address>] [--alias <name>] [--live] [--skip-simulation]
330
370
  [--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
371
+ slrd prepare pump --payer <phantom-address> --name <name> --symbol <symbol> --uri <metadata_uri> [--pair SOL|USDC|<custom-quote-mint>] [--beneficiary <address>] [--out prepared.json]
372
+ Build + mint-partial-sign only; payer remains unsigned and nothing is broadcast
331
373
  slrd metadata upload --image <local_path> --name <name> --symbol <symbol> --description <text> [--provider pump-frontend|pinata]
332
374
  [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
333
375
  slrd vamp <source-mint> --creator <wallet> [--name <name>] [--symbol <symbol>] [--image <path-or-url>] [--description <text>]
334
376
  [--website <url>] [--twitter <url>] [--telegram <url>] [--video <url>] [--uri <metadata-uri>]
335
377
  [--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]
378
+ slrd deploy pump --wallet <wallet> --name <name> --symbol <symbol> (--uri <metadata_uri> | --image <local_path> --description <text>) [--pair SOL|USDC|<custom-quote-mint>] [--beneficiary <wallet|address>] [--alias <name>] [--live]
337
379
  [--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
338
380
 
339
381
  Prices
@@ -342,18 +384,24 @@ Prices
342
384
  slrd price average <token|ca> --period 15m Average stored samples in a period
343
385
  slrd price watch <token|ca...> [--interval 1s] [--period 1m]
344
386
 
387
+ Backtesting
388
+ slrd backtest <mint> --dip-pct 20 --profit-pct 40 --buy-sol 0.1 [--from creation|migration|<time>] [--to <time>] [--capital-sol 5] [--slippage-bps N] [--venue-fee-bps N] [--network-fee-sol N] [--latency-ms N] [--require-from-start] [--exact-trades] [--ledger[=<csv>]] [--json]
389
+ Replays sparse 1s candles by default; --exact-trades uses durable market fills
390
+
345
391
  Transfers and consolidation
346
392
  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]
393
+ slrd transfer <contact|wallet|address> --wallet <source-wallet> --token <USDC|mint> --amount <ui> [--simulate-only]
394
+ slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--max-balance-sol <SOL>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]
348
395
  Without --wallets, sweep considers all stored signing wallets
349
396
 
350
397
  Token liquidation
351
- slrd liquidate tokens --except <token|mint> [--wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--simulate | --live]
398
+ slrd liquidate tokens --except <token|mint> [--wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--burn-unsellable] [--simulate | --live]
352
399
  Sell supported tokens except protected mint(s); unwrap WSOL; close unprotected zero-balance token accounts and reclaim rent
353
400
 
354
401
  RPC
355
402
  All Solard JSON-RPC traffic is globally rate-limited to 5 req/s by default.
356
403
  Override only when your provider allows it: SLRD_RPC_MAX_RPS=<n>
404
+ Fetch-level network failures (ECONNRESET, socket close, etc.) retry 4 times by default; SLRD_RPC_NETWORK_RETRIES=<n>.
357
405
  Jupiter fallback: keyless 0.5 req/s, API-key default 1 req/s; override SLRD_JUPITER_MAX_RPS=<n>
358
406
 
359
407
  Diagnostics
@@ -362,13 +410,22 @@ Diagnostics
362
410
  --measure-stream Restore raw live measure-fn output for low-level debugging
363
411
 
364
412
  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]
413
+ slrd swap --from <SOL|token|mint> --to <SOL|token|mint> --amount <ui> --wallet <wallet> [--live] Jupiter exact-input any -> any; quote-only unless --live
414
+ slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live] Compatibility: SOL -> token
415
+ slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--venue auto|native|jupiter] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
367
416
  slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
368
417
  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]
418
+ slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--venue auto|native|jupiter] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
370
419
  slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--continue-on-error] [--simulate-only]
371
420
  slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
421
+ slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
422
+ slrd transfer-many status <stable-id>
423
+ slrd transfer-many resume <stable-id> [--sender rpc|helius]
424
+ slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--live]
425
+ Snapshot contains cumulative totalClaimedRaw + recipients[].entitledRaw; Solard tracks confirmed payouts
426
+ slrd rewards status <distribution-id>
427
+ slrd rewards inspect <token|ca> Show claim source, quote asset, estimated accrued creator reward, and payout address
428
+ slrd rewards claim <token|ca> --wallet <fee-payer> Claim creator rewards; on-chain beneficiary receives the reward
372
429
 
373
430
  Scripts (strategies stay outside the kernel)
374
431
  slrd scripts List scripts registered in slrd.config.ts
@@ -418,8 +475,8 @@ Transactions and ALTs
418
475
  slrd alt extend <address> --wallet <wallet> <account...>
419
476
 
420
477
  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
478
+ SLRD_DB_PATH shared SDK/CLI database; default ~/.solard/solard.sqlite (SOLARD_DB_PATH alias supported)
479
+ SLRD_MASTER_KEY optional explicit override for CI/automation; otherwise signing commands prompt each invocation and never persist the password
423
480
  RPC_ENDPOINT required only for chain operations
424
481
  HELIUS_SENDER_URL regional/global Helius Sender endpoint used by launch scripts
425
482
  HELIUS_RPC_URL RPC endpoint used for ordinary Helius-RPC buyer lane
@@ -450,10 +507,71 @@ async function main() {
450
507
  process.env.SLRD_MEASURE_STREAM === "true";
451
508
  configureSolardMeasure(
452
509
  streamMeasures
453
- ? { silent: false, logger: null }
454
- : { silent: false, logger: measureCollector.logger },
510
+ ? {
511
+ silent: false,
512
+ // Middleware mode: accumulate the structured event and then delegate
513
+ // to measure-fn's built-in logger. The collector itself does not own
514
+ // formatting or streaming.
515
+ logger: measureCollector.middleware,
516
+ }
517
+ : {
518
+ silent: false,
519
+ // Replacement mode: accumulate without printing every span.
520
+ logger: measureCollector.logger,
521
+ },
455
522
  );
456
523
 
524
+ const vaultCommand = command === "setup";
525
+ const needsVault = commandNeedsSigningVault(command, values, flags);
526
+ if (vaultCommand && flags.has("status")) {
527
+ const status = cliVaultStatus();
528
+ if (flags.has("json")) {
529
+ emit(json(status) + "\n");
530
+ } else {
531
+ emit(
532
+ `${OWL} wallet password mode\n` +
533
+ ` mode ephemeral per command\n` +
534
+ ` password persisted no\n` +
535
+ ` env override ${status.environmentOverride ? "yes" : "no"}\n`,
536
+ );
537
+ }
538
+ return;
539
+ }
540
+
541
+ if (vaultCommand) {
542
+ const count = await storedWalletCount();
543
+ if (count === 0 && !process.env.SLRD_MASTER_KEY?.trim()) {
544
+ emit(
545
+ `${OWL} no wallet password is stored by Solard.\n` +
546
+ ` No signing wallets exist yet. Run: slrd wallet create [name]\n` +
547
+ ` That command will ask for a password twice and use it only for that process.\n`,
548
+ );
549
+ return;
550
+ }
551
+ const result = await ensureCliVaultReady({
552
+ existingWalletCount: count,
553
+ allowCreate: false,
554
+ });
555
+ emit(
556
+ `${OWL} wallet password accepted source=${result.source}\n` +
557
+ ` Nothing was written to disk; a future invocation will prompt again unless SLRD_MASTER_KEY is set.\n`,
558
+ );
559
+ return;
560
+ }
561
+
562
+ if (needsVault && !process.env.SLRD_MASTER_KEY?.trim()) {
563
+ const allowCreate =
564
+ (command === "wallet" && values[0] === "create") ||
565
+ command === "import" ||
566
+ (command === "vanity" &&
567
+ values[0] === "pool" &&
568
+ values[1] === "generate");
569
+ await ensureCliVaultReady({
570
+ existingWalletCount: await storedWalletCount(),
571
+ allowCreate,
572
+ });
573
+ }
574
+
457
575
  let perfPrinted = false;
458
576
  const printPerfSummary = () => {
459
577
  if (perfPrinted) return;
@@ -475,7 +593,10 @@ async function main() {
475
593
  `PERF wall=${formatDurationMs(wallMs)} ` +
476
594
  `measure=${measures.completed} (${measures.errors} err) ` +
477
595
  `rpc=${rpc.requestStarts} 429=${rpc.rateLimited429} ` +
478
- `retries=${rpc.retries429} limit=${rpc.maxRps}/s\n`,
596
+ `neterr=${rpc.networkErrors} ` +
597
+ `retries=${rpc.retries429 + rpc.retriesNetwork} ` +
598
+ `(429=${rpc.retries429},net=${rpc.retriesNetwork}) ` +
599
+ `limit=${rpc.maxRps}/s\n`,
479
600
  );
480
601
 
481
602
  if (flags.has("measure") && !streamMeasures) {
@@ -630,6 +751,11 @@ async function main() {
630
751
  await runPumpSpamBuyFromArgs(rest);
631
752
  return;
632
753
  }
754
+ if (command === "backtest") {
755
+ const { runBacktestCommand } = await import("./backtest-command.ts");
756
+ await runBacktestCommand({ values, flags, emit });
757
+ return;
758
+ }
633
759
  if (command === "scripts") {
634
760
  emit(json(await listScripts()) + "\n");
635
761
  return;
@@ -641,6 +767,18 @@ async function main() {
641
767
  process.exitCode = await runScript(script, scriptArgs);
642
768
  return;
643
769
  }
770
+ if (
771
+ (command === "prepare" && values[0] === "pump") ||
772
+ (command === "launch" &&
773
+ (values[0] === "pump" || values[0] === "pump-token") &&
774
+ flags.has("prepare"))
775
+ ) {
776
+ const { runPumpExternalDeploymentFromArgs } =
777
+ await import("./pump/external-deployment-cli.ts");
778
+ const externalArgs = command === "prepare" ? rest.slice(1) : rest.slice(1);
779
+ await runPumpExternalDeploymentFromArgs(externalArgs);
780
+ return;
781
+ }
644
782
  if (
645
783
  command === "launch" &&
646
784
  (values[0] === "pump" || values[0] === "pump-token")
@@ -713,69 +851,19 @@ async function main() {
713
851
  ]);
714
852
  const slrd = createTraderSolard();
715
853
  try {
854
+ if (await handleFairfunCommand({ command, values, flags, slrd, emit })) {
855
+ return;
856
+ }
857
+
716
858
  if (command === "meteora") {
717
859
  await handleMeteoraCommand({ slrd, values, flags, emit });
718
860
  return;
719
861
  }
720
862
 
721
863
  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
- );
864
+ const { runJupiterSwapCommand } =
865
+ await import("./commands/jupiter-swap.ts");
866
+ emit(json(await runJupiterSwapCommand({ slrd, values, flags })) + "\n");
779
867
  return;
780
868
  }
781
869
  if (command === "contact" || command === "contacts") {
@@ -840,8 +928,26 @@ async function main() {
840
928
  }
841
929
 
842
930
  if (command === "wallet" && values[0] === "create") {
843
- const wallet = slrd.createWallet(values[1]);
844
- emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
931
+ const firstWallet = slrd.wallets.list().length === 0;
932
+ const wallet = slrd.createWallet(
933
+ values[1] ?? (firstWallet ? "main" : undefined),
934
+ );
935
+ if (flags.has("json")) {
936
+ emit(json(wallet) + "\n");
937
+ } else if (process.stdout.isTTY) {
938
+ emit(
939
+ `\n╭─ ${OWL} wallet ready ──────────────────────────────────╮\n` +
940
+ ` name @${wallet.name}\n` +
941
+ ` address ${wallet.address}\n` +
942
+ ` storage encrypted in Solard's local wallet database\n` +
943
+ ` password NOT saved; you will enter it again next invocation\n` +
944
+ `╰──────────────────────────────────────────────────────╯\n` +
945
+ `\nNext: slrd balances --wallet ${wallet.name}\n` +
946
+ `Backup: slrd export ${wallet.name} --out .\\${wallet.name}-key.txt\n`,
947
+ );
948
+ } else {
949
+ emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
950
+ }
845
951
  return;
846
952
  }
847
953
  if (command === "export") {
@@ -1482,14 +1588,39 @@ async function main() {
1482
1588
  );
1483
1589
  uri = (uploadedMetadata as { metadataUri: string }).metadataUri;
1484
1590
  }
1485
- const creator = flags.get("creator")
1486
- ? slrd.resolveWallet(flags.get("creator")!).address
1487
- : slrd.signer(wallet).publicKey;
1591
+ const payer = slrd.signer(wallet).publicKey;
1592
+ const beneficiaryInput = flags.get("beneficiary") ?? flags.get("creator");
1593
+ let creator = payer;
1594
+ if (beneficiaryInput && beneficiaryInput !== "true") {
1595
+ try {
1596
+ creator = slrd.resolveWallet(beneficiaryInput).address;
1597
+ } catch {
1598
+ const contact = findExternalContact(beneficiaryInput);
1599
+ const { PublicKey } = await import("@solana/web3.js");
1600
+ creator = new PublicKey(contact?.address ?? beneficiaryInput);
1601
+ }
1602
+ }
1603
+ const quoteAsset =
1604
+ launchpad === "pump"
1605
+ ? await resolvePumpQuoteAsset(
1606
+ slrd.connection(),
1607
+ flags.get("pair") ?? flags.get("quote-mint") ?? "SOL",
1608
+ )
1609
+ : undefined;
1610
+ if (
1611
+ launchpad !== "pump" &&
1612
+ (flags.has("pair") || flags.has("beneficiary"))
1613
+ ) {
1614
+ throw new Error(
1615
+ "--pair and --beneficiary are currently defined for Pump create_v2 deployment only.",
1616
+ );
1617
+ }
1488
1618
  const deployment = await slrd.prepareTokenDeployment(launchpad, wallet, {
1489
1619
  name,
1490
1620
  symbol,
1491
1621
  uri,
1492
1622
  creator,
1623
+ quoteAsset,
1493
1624
  mayhemMode: flags.has("mayhem"),
1494
1625
  cashback: flags.has("cashback"),
1495
1626
  });
@@ -1508,6 +1639,12 @@ async function main() {
1508
1639
  name,
1509
1640
  symbol,
1510
1641
  uri,
1642
+ beneficiary: creator.toBase58(),
1643
+ pair:
1644
+ quoteAsset?.kind === "native-sol"
1645
+ ? "SOL"
1646
+ : (quoteAsset?.mint.toBase58() ?? null),
1647
+ quoteMint: quoteAsset?.mint.toBase58() ?? null,
1511
1648
  uploadedMetadata,
1512
1649
  live: flags.has("live"),
1513
1650
  };
@@ -1532,7 +1669,31 @@ async function main() {
1532
1669
  emit(json({ ...header, receipt, token }) + "\n");
1533
1670
  return;
1534
1671
  }
1535
- if (command === "token" && values[0] !== "set" && values[0] !== "refresh") {
1672
+ if (
1673
+ command === "token" &&
1674
+ (values[0] === "backfill" ||
1675
+ values[0] === "trades" ||
1676
+ values[0] === "analyze")
1677
+ ) {
1678
+ const { runTokenHistoryCommand } =
1679
+ await import("./token-history-command.ts");
1680
+ await runTokenHistoryCommand({
1681
+ slrd,
1682
+ action: values[0]!,
1683
+ values: values.slice(1),
1684
+ flags,
1685
+ emit,
1686
+ });
1687
+ return;
1688
+ }
1689
+ if (
1690
+ command === "token" &&
1691
+ values[0] !== "set" &&
1692
+ values[0] !== "refresh" &&
1693
+ values[0] !== "backfill" &&
1694
+ values[0] !== "trades" &&
1695
+ values[0] !== "analyze"
1696
+ ) {
1536
1697
  const [mint, name] = values;
1537
1698
  if (!mint) throw new Error("Usage: slrd token <token_ca> [name]");
1538
1699
  const metadata = flags.get("metadata-json");
@@ -1658,7 +1819,7 @@ async function main() {
1658
1819
  const except = csv(flags.get("except"));
1659
1820
  if (except.length === 0) {
1660
1821
  throw new Error(
1661
- "Usage: slrd liquidate tokens --except <token|mint>[,<token|mint>...] [--wallets <a,b,...>] [--simulate | --live]",
1822
+ "Usage: slrd liquidate tokens --except <token|mint>[,<token|mint>...] [--wallets <a,b,...>] [--burn-unsellable] [--simulate | --live]",
1662
1823
  );
1663
1824
  }
1664
1825
  if (flags.has("simulate") && flags.has("live")) {
@@ -1711,6 +1872,7 @@ async function main() {
1711
1872
  portfolioConcurrency: 1,
1712
1873
  portfolioDelayMs: 0,
1713
1874
  jupiterFallback: !flags.has("no-jupiter"),
1875
+ burnUnsellable: flags.has("burn-unsellable"),
1714
1876
  onProgress: (event: any) => {
1715
1877
  if (event.stage === "portfolio-start") {
1716
1878
  emit("SCAN wallet token accounts...\n");
@@ -1833,7 +1995,7 @@ async function main() {
1833
1995
  const destinationInput = flags.get("to") ?? values[1];
1834
1996
  if (!destinationInput || destinationInput === "true") {
1835
1997
  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]",
1998
+ "Usage: slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--max-balance-sol <SOL>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]",
1837
1999
  );
1838
2000
  }
1839
2001
 
@@ -1894,6 +2056,7 @@ async function main() {
1894
2056
  excludeGroups,
1895
2057
  excludePrefixes,
1896
2058
  includeWallets: includeWallets.length ? includeWallets : undefined,
2059
+ maxBalanceSol: flags.get("max-balance-sol"),
1897
2060
  keepSolByWallet,
1898
2061
  defaultKeepSol: flags.get("default-keep-sol") ?? "0",
1899
2062
  keepSolIfTokens: flags.get("keep-if-tokens"),
@@ -2046,156 +2209,19 @@ async function main() {
2046
2209
  }
2047
2210
 
2048
2211
  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
- );
2212
+ const { runTransferCommand } = await import("./commands/transfer.ts");
2213
+ emit(json(await runTransferCommand({ slrd, values, flags })) + "\n");
2112
2214
  return;
2113
2215
  }
2114
2216
 
2115
2217
  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");
2218
+ const { runSmartBuyCommand } = await import("./commands/buy.ts");
2219
+ emit(json(await runSmartBuyCommand({ slrd, values, flags })) + "\n");
2159
2220
  return;
2160
2221
  }
2161
2222
  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");
2223
+ const { runSmartSellCommand } = await import("./commands/sell.ts");
2224
+ emit(json(await runSmartSellCommand({ slrd, values, flags })) + "\n");
2199
2225
  return;
2200
2226
  }
2201
2227
  if (
@@ -2348,6 +2374,74 @@ async function main() {
2348
2374
  emit(json(receipts) + "\n");
2349
2375
  return;
2350
2376
  }
2377
+ if (command === "rewards") {
2378
+ const action = values[0] ?? "inspect";
2379
+ const tokenRef = values[1];
2380
+ if (!tokenRef)
2381
+ throw new Error(
2382
+ "Usage: slrd rewards <inspect|claim> <token|ca> [--wallet <fee-payer>]",
2383
+ );
2384
+
2385
+ if (action === "claim") {
2386
+ const wallet = need(flags, "wallet");
2387
+ const receipt = await slrd.claim(tokenRef, wallet, {
2388
+ via: flags.get("sender") ?? "rpc",
2389
+ });
2390
+ emit(
2391
+ json({
2392
+ action: "claim",
2393
+ token: tokenRef,
2394
+ feePayer: wallet,
2395
+ receipt,
2396
+ }) + "\n",
2397
+ );
2398
+ return;
2399
+ }
2400
+
2401
+ if (action === "inspect") {
2402
+ let token = slrd.resolveToken(tokenRef);
2403
+ try {
2404
+ token = await slrd.refreshToken(token);
2405
+ } catch {
2406
+ // A registered token can still expose a useful claim plan when one
2407
+ // enrichment source is temporarily unavailable.
2408
+ }
2409
+ const { PublicKey } = await import("@solana/web3.js");
2410
+ const probeUser = token.creator
2411
+ ? new PublicKey(token.creator)
2412
+ : new PublicKey(token.mint);
2413
+ const plan = await slrd.resolveClaim(token, probeUser);
2414
+ const payoutAddress =
2415
+ typeof plan.meta?.payoutAddress === "string"
2416
+ ? plan.meta.payoutAddress
2417
+ : token.creator;
2418
+ const output = {
2419
+ action: "inspect",
2420
+ token: token.mint,
2421
+ source: plan.source,
2422
+ payoutAddress: payoutAddress ?? null,
2423
+ quote: {
2424
+ kind: plan.quoteAsset.kind,
2425
+ mint: plan.quoteAsset.mint.toBase58(),
2426
+ decimals: plan.quoteAsset.decimals,
2427
+ tokenProgram: plan.quoteAsset.tokenProgram.toBase58(),
2428
+ },
2429
+ estimatedClaimRaw: plan.estimatedClaimRaw.toString(),
2430
+ estimatedClaimUi: formatRaw(
2431
+ plan.estimatedClaimRaw,
2432
+ plan.quoteAsset.decimals,
2433
+ ),
2434
+ spendableByProbeRaw: plan.spendableByUserRaw.toString(),
2435
+ sharingConfig: plan.meta?.sharingConfig ?? null,
2436
+ note: "Claim is permissionless where supported; --wallet is the transaction fee payer. The configured on-chain beneficiary receives the reward.",
2437
+ };
2438
+ emit(json(output) + "\n");
2439
+ return;
2440
+ }
2441
+
2442
+ throw new Error("Usage: slrd rewards <inspect|claim> <token|ca>");
2443
+ }
2444
+
2351
2445
  if (command === "claim") {
2352
2446
  const token = values[0];
2353
2447
  if (!token)
@@ -2508,7 +2602,10 @@ async function main() {
2508
2602
  }
2509
2603
  }
2510
2604
  main().catch((error) => {
2511
- emit(`${OWL} error: ${formatError(error)}\n`);
2605
+ const friendly = friendlyCliError(error);
2606
+ emit(
2607
+ friendly ? `${OWL} ${friendly}\n` : `${OWL} error: ${formatError(error)}\n`,
2608
+ );
2512
2609
  process.exitCode = 1;
2513
2610
  });
2514
2611