@solard/cli 0.2.12 → 0.2.14

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,21 @@ 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> [--claim-first|--amount-raw N] [--id <epoch>] [--live]
425
+ slrd rewards status <distribution-id>
426
+ slrd rewards inspect <token|ca> Show claim source, quote asset, estimated accrued creator reward, and payout address
427
+ slrd rewards claim <token|ca> --wallet <fee-payer> Claim creator rewards; on-chain beneficiary receives the reward
372
428
 
373
429
  Scripts (strategies stay outside the kernel)
374
430
  slrd scripts List scripts registered in slrd.config.ts
@@ -418,8 +474,8 @@ Transactions and ALTs
418
474
  slrd alt extend <address> --wallet <wallet> <account...>
419
475
 
420
476
  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
477
+ SLRD_DB_PATH shared SDK/CLI database; default ~/.solard/solard.sqlite (SOLARD_DB_PATH alias supported)
478
+ SLRD_MASTER_KEY optional explicit override for CI/automation; otherwise signing commands prompt each invocation and never persist the password
423
479
  RPC_ENDPOINT required only for chain operations
424
480
  HELIUS_SENDER_URL regional/global Helius Sender endpoint used by launch scripts
425
481
  HELIUS_RPC_URL RPC endpoint used for ordinary Helius-RPC buyer lane
@@ -450,10 +506,71 @@ async function main() {
450
506
  process.env.SLRD_MEASURE_STREAM === "true";
451
507
  configureSolardMeasure(
452
508
  streamMeasures
453
- ? { silent: false, logger: null }
454
- : { silent: false, logger: measureCollector.logger },
509
+ ? {
510
+ silent: false,
511
+ // Middleware mode: accumulate the structured event and then delegate
512
+ // to measure-fn's built-in logger. The collector itself does not own
513
+ // formatting or streaming.
514
+ logger: measureCollector.middleware,
515
+ }
516
+ : {
517
+ silent: false,
518
+ // Replacement mode: accumulate without printing every span.
519
+ logger: measureCollector.logger,
520
+ },
455
521
  );
456
522
 
523
+ const vaultCommand = command === "setup";
524
+ const needsVault = commandNeedsSigningVault(command, values, flags);
525
+ if (vaultCommand && flags.has("status")) {
526
+ const status = cliVaultStatus();
527
+ if (flags.has("json")) {
528
+ emit(json(status) + "\n");
529
+ } else {
530
+ emit(
531
+ `${OWL} wallet password mode\n` +
532
+ ` mode ephemeral per command\n` +
533
+ ` password persisted no\n` +
534
+ ` env override ${status.environmentOverride ? "yes" : "no"}\n`,
535
+ );
536
+ }
537
+ return;
538
+ }
539
+
540
+ if (vaultCommand) {
541
+ const count = await storedWalletCount();
542
+ if (count === 0 && !process.env.SLRD_MASTER_KEY?.trim()) {
543
+ emit(
544
+ `${OWL} no wallet password is stored by Solard.\n` +
545
+ ` No signing wallets exist yet. Run: slrd wallet create [name]\n` +
546
+ ` That command will ask for a password twice and use it only for that process.\n`,
547
+ );
548
+ return;
549
+ }
550
+ const result = await ensureCliVaultReady({
551
+ existingWalletCount: count,
552
+ allowCreate: false,
553
+ });
554
+ emit(
555
+ `${OWL} wallet password accepted source=${result.source}\n` +
556
+ ` Nothing was written to disk; a future invocation will prompt again unless SLRD_MASTER_KEY is set.\n`,
557
+ );
558
+ return;
559
+ }
560
+
561
+ if (needsVault && !process.env.SLRD_MASTER_KEY?.trim()) {
562
+ const allowCreate =
563
+ (command === "wallet" && values[0] === "create") ||
564
+ command === "import" ||
565
+ (command === "vanity" &&
566
+ values[0] === "pool" &&
567
+ values[1] === "generate");
568
+ await ensureCliVaultReady({
569
+ existingWalletCount: await storedWalletCount(),
570
+ allowCreate,
571
+ });
572
+ }
573
+
457
574
  let perfPrinted = false;
458
575
  const printPerfSummary = () => {
459
576
  if (perfPrinted) return;
@@ -475,7 +592,10 @@ async function main() {
475
592
  `PERF wall=${formatDurationMs(wallMs)} ` +
476
593
  `measure=${measures.completed} (${measures.errors} err) ` +
477
594
  `rpc=${rpc.requestStarts} 429=${rpc.rateLimited429} ` +
478
- `retries=${rpc.retries429} limit=${rpc.maxRps}/s\n`,
595
+ `neterr=${rpc.networkErrors} ` +
596
+ `retries=${rpc.retries429 + rpc.retriesNetwork} ` +
597
+ `(429=${rpc.retries429},net=${rpc.retriesNetwork}) ` +
598
+ `limit=${rpc.maxRps}/s\n`,
479
599
  );
480
600
 
481
601
  if (flags.has("measure") && !streamMeasures) {
@@ -630,6 +750,11 @@ async function main() {
630
750
  await runPumpSpamBuyFromArgs(rest);
631
751
  return;
632
752
  }
753
+ if (command === "backtest") {
754
+ const { runBacktestCommand } = await import("./backtest-command.ts");
755
+ await runBacktestCommand({ values, flags, emit });
756
+ return;
757
+ }
633
758
  if (command === "scripts") {
634
759
  emit(json(await listScripts()) + "\n");
635
760
  return;
@@ -641,6 +766,18 @@ async function main() {
641
766
  process.exitCode = await runScript(script, scriptArgs);
642
767
  return;
643
768
  }
769
+ if (
770
+ (command === "prepare" && values[0] === "pump") ||
771
+ (command === "launch" &&
772
+ (values[0] === "pump" || values[0] === "pump-token") &&
773
+ flags.has("prepare"))
774
+ ) {
775
+ const { runPumpExternalDeploymentFromArgs } =
776
+ await import("./pump/external-deployment-cli.ts");
777
+ const externalArgs = command === "prepare" ? rest.slice(1) : rest.slice(1);
778
+ await runPumpExternalDeploymentFromArgs(externalArgs);
779
+ return;
780
+ }
644
781
  if (
645
782
  command === "launch" &&
646
783
  (values[0] === "pump" || values[0] === "pump-token")
@@ -713,69 +850,19 @@ async function main() {
713
850
  ]);
714
851
  const slrd = createTraderSolard();
715
852
  try {
853
+ if (await handleFairfunCommand({ command, values, flags, slrd, emit })) {
854
+ return;
855
+ }
856
+
716
857
  if (command === "meteora") {
717
858
  await handleMeteoraCommand({ slrd, values, flags, emit });
718
859
  return;
719
860
  }
720
861
 
721
862
  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
- );
863
+ const { runJupiterSwapCommand } =
864
+ await import("./commands/jupiter-swap.ts");
865
+ emit(json(await runJupiterSwapCommand({ slrd, values, flags })) + "\n");
779
866
  return;
780
867
  }
781
868
  if (command === "contact" || command === "contacts") {
@@ -840,8 +927,26 @@ async function main() {
840
927
  }
841
928
 
842
929
  if (command === "wallet" && values[0] === "create") {
843
- const wallet = slrd.createWallet(values[1]);
844
- emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
930
+ const firstWallet = slrd.wallets.list().length === 0;
931
+ const wallet = slrd.createWallet(
932
+ values[1] ?? (firstWallet ? "main" : undefined),
933
+ );
934
+ if (flags.has("json")) {
935
+ emit(json(wallet) + "\n");
936
+ } else if (process.stdout.isTTY) {
937
+ emit(
938
+ `\n╭─ ${OWL} wallet ready ──────────────────────────────────╮\n` +
939
+ ` name @${wallet.name}\n` +
940
+ ` address ${wallet.address}\n` +
941
+ ` storage encrypted in Solard's local wallet database\n` +
942
+ ` password NOT saved; you will enter it again next invocation\n` +
943
+ `╰──────────────────────────────────────────────────────╯\n` +
944
+ `\nNext: slrd balances --wallet ${wallet.name}\n` +
945
+ `Backup: slrd export ${wallet.name} --out .\\${wallet.name}-key.txt\n`,
946
+ );
947
+ } else {
948
+ emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
949
+ }
845
950
  return;
846
951
  }
847
952
  if (command === "export") {
@@ -1482,14 +1587,39 @@ async function main() {
1482
1587
  );
1483
1588
  uri = (uploadedMetadata as { metadataUri: string }).metadataUri;
1484
1589
  }
1485
- const creator = flags.get("creator")
1486
- ? slrd.resolveWallet(flags.get("creator")!).address
1487
- : slrd.signer(wallet).publicKey;
1590
+ const payer = slrd.signer(wallet).publicKey;
1591
+ const beneficiaryInput = flags.get("beneficiary") ?? flags.get("creator");
1592
+ let creator = payer;
1593
+ if (beneficiaryInput && beneficiaryInput !== "true") {
1594
+ try {
1595
+ creator = slrd.resolveWallet(beneficiaryInput).address;
1596
+ } catch {
1597
+ const contact = findExternalContact(beneficiaryInput);
1598
+ const { PublicKey } = await import("@solana/web3.js");
1599
+ creator = new PublicKey(contact?.address ?? beneficiaryInput);
1600
+ }
1601
+ }
1602
+ const quoteAsset =
1603
+ launchpad === "pump"
1604
+ ? await resolvePumpQuoteAsset(
1605
+ slrd.connection(),
1606
+ flags.get("pair") ?? flags.get("quote-mint") ?? "SOL",
1607
+ )
1608
+ : undefined;
1609
+ if (
1610
+ launchpad !== "pump" &&
1611
+ (flags.has("pair") || flags.has("beneficiary"))
1612
+ ) {
1613
+ throw new Error(
1614
+ "--pair and --beneficiary are currently defined for Pump create_v2 deployment only.",
1615
+ );
1616
+ }
1488
1617
  const deployment = await slrd.prepareTokenDeployment(launchpad, wallet, {
1489
1618
  name,
1490
1619
  symbol,
1491
1620
  uri,
1492
1621
  creator,
1622
+ quoteAsset,
1493
1623
  mayhemMode: flags.has("mayhem"),
1494
1624
  cashback: flags.has("cashback"),
1495
1625
  });
@@ -1508,6 +1638,12 @@ async function main() {
1508
1638
  name,
1509
1639
  symbol,
1510
1640
  uri,
1641
+ beneficiary: creator.toBase58(),
1642
+ pair:
1643
+ quoteAsset?.kind === "native-sol"
1644
+ ? "SOL"
1645
+ : (quoteAsset?.mint.toBase58() ?? null),
1646
+ quoteMint: quoteAsset?.mint.toBase58() ?? null,
1511
1647
  uploadedMetadata,
1512
1648
  live: flags.has("live"),
1513
1649
  };
@@ -1532,7 +1668,31 @@ async function main() {
1532
1668
  emit(json({ ...header, receipt, token }) + "\n");
1533
1669
  return;
1534
1670
  }
1535
- if (command === "token" && values[0] !== "set" && values[0] !== "refresh") {
1671
+ if (
1672
+ command === "token" &&
1673
+ (values[0] === "backfill" ||
1674
+ values[0] === "trades" ||
1675
+ values[0] === "analyze")
1676
+ ) {
1677
+ const { runTokenHistoryCommand } =
1678
+ await import("./token-history-command.ts");
1679
+ await runTokenHistoryCommand({
1680
+ slrd,
1681
+ action: values[0]!,
1682
+ values: values.slice(1),
1683
+ flags,
1684
+ emit,
1685
+ });
1686
+ return;
1687
+ }
1688
+ if (
1689
+ command === "token" &&
1690
+ values[0] !== "set" &&
1691
+ values[0] !== "refresh" &&
1692
+ values[0] !== "backfill" &&
1693
+ values[0] !== "trades" &&
1694
+ values[0] !== "analyze"
1695
+ ) {
1536
1696
  const [mint, name] = values;
1537
1697
  if (!mint) throw new Error("Usage: slrd token <token_ca> [name]");
1538
1698
  const metadata = flags.get("metadata-json");
@@ -1658,7 +1818,7 @@ async function main() {
1658
1818
  const except = csv(flags.get("except"));
1659
1819
  if (except.length === 0) {
1660
1820
  throw new Error(
1661
- "Usage: slrd liquidate tokens --except <token|mint>[,<token|mint>...] [--wallets <a,b,...>] [--simulate | --live]",
1821
+ "Usage: slrd liquidate tokens --except <token|mint>[,<token|mint>...] [--wallets <a,b,...>] [--burn-unsellable] [--simulate | --live]",
1662
1822
  );
1663
1823
  }
1664
1824
  if (flags.has("simulate") && flags.has("live")) {
@@ -1711,6 +1871,7 @@ async function main() {
1711
1871
  portfolioConcurrency: 1,
1712
1872
  portfolioDelayMs: 0,
1713
1873
  jupiterFallback: !flags.has("no-jupiter"),
1874
+ burnUnsellable: flags.has("burn-unsellable"),
1714
1875
  onProgress: (event: any) => {
1715
1876
  if (event.stage === "portfolio-start") {
1716
1877
  emit("SCAN wallet token accounts...\n");
@@ -1833,7 +1994,7 @@ async function main() {
1833
1994
  const destinationInput = flags.get("to") ?? values[1];
1834
1995
  if (!destinationInput || destinationInput === "true") {
1835
1996
  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]",
1997
+ "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
1998
  );
1838
1999
  }
1839
2000
 
@@ -1894,6 +2055,7 @@ async function main() {
1894
2055
  excludeGroups,
1895
2056
  excludePrefixes,
1896
2057
  includeWallets: includeWallets.length ? includeWallets : undefined,
2058
+ maxBalanceSol: flags.get("max-balance-sol"),
1897
2059
  keepSolByWallet,
1898
2060
  defaultKeepSol: flags.get("default-keep-sol") ?? "0",
1899
2061
  keepSolIfTokens: flags.get("keep-if-tokens"),
@@ -2046,156 +2208,19 @@ async function main() {
2046
2208
  }
2047
2209
 
2048
2210
  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
- );
2211
+ const { runTransferCommand } = await import("./commands/transfer.ts");
2212
+ emit(json(await runTransferCommand({ slrd, values, flags })) + "\n");
2112
2213
  return;
2113
2214
  }
2114
2215
 
2115
2216
  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");
2217
+ const { runSmartBuyCommand } = await import("./commands/buy.ts");
2218
+ emit(json(await runSmartBuyCommand({ slrd, values, flags })) + "\n");
2159
2219
  return;
2160
2220
  }
2161
2221
  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");
2222
+ const { runSmartSellCommand } = await import("./commands/sell.ts");
2223
+ emit(json(await runSmartSellCommand({ slrd, values, flags })) + "\n");
2199
2224
  return;
2200
2225
  }
2201
2226
  if (
@@ -2348,6 +2373,74 @@ async function main() {
2348
2373
  emit(json(receipts) + "\n");
2349
2374
  return;
2350
2375
  }
2376
+ if (command === "rewards") {
2377
+ const action = values[0] ?? "inspect";
2378
+ const tokenRef = values[1];
2379
+ if (!tokenRef)
2380
+ throw new Error(
2381
+ "Usage: slrd rewards <inspect|claim> <token|ca> [--wallet <fee-payer>]",
2382
+ );
2383
+
2384
+ if (action === "claim") {
2385
+ const wallet = need(flags, "wallet");
2386
+ const receipt = await slrd.claim(tokenRef, wallet, {
2387
+ via: flags.get("sender") ?? "rpc",
2388
+ });
2389
+ emit(
2390
+ json({
2391
+ action: "claim",
2392
+ token: tokenRef,
2393
+ feePayer: wallet,
2394
+ receipt,
2395
+ }) + "\n",
2396
+ );
2397
+ return;
2398
+ }
2399
+
2400
+ if (action === "inspect") {
2401
+ let token = slrd.resolveToken(tokenRef);
2402
+ try {
2403
+ token = await slrd.refreshToken(token);
2404
+ } catch {
2405
+ // A registered token can still expose a useful claim plan when one
2406
+ // enrichment source is temporarily unavailable.
2407
+ }
2408
+ const { PublicKey } = await import("@solana/web3.js");
2409
+ const probeUser = token.creator
2410
+ ? new PublicKey(token.creator)
2411
+ : new PublicKey(token.mint);
2412
+ const plan = await slrd.resolveClaim(token, probeUser);
2413
+ const payoutAddress =
2414
+ typeof plan.meta?.payoutAddress === "string"
2415
+ ? plan.meta.payoutAddress
2416
+ : token.creator;
2417
+ const output = {
2418
+ action: "inspect",
2419
+ token: token.mint,
2420
+ source: plan.source,
2421
+ payoutAddress: payoutAddress ?? null,
2422
+ quote: {
2423
+ kind: plan.quoteAsset.kind,
2424
+ mint: plan.quoteAsset.mint.toBase58(),
2425
+ decimals: plan.quoteAsset.decimals,
2426
+ tokenProgram: plan.quoteAsset.tokenProgram.toBase58(),
2427
+ },
2428
+ estimatedClaimRaw: plan.estimatedClaimRaw.toString(),
2429
+ estimatedClaimUi: formatRaw(
2430
+ plan.estimatedClaimRaw,
2431
+ plan.quoteAsset.decimals,
2432
+ ),
2433
+ spendableByProbeRaw: plan.spendableByUserRaw.toString(),
2434
+ sharingConfig: plan.meta?.sharingConfig ?? null,
2435
+ note: "Claim is permissionless where supported; --wallet is the transaction fee payer. The configured on-chain beneficiary receives the reward.",
2436
+ };
2437
+ emit(json(output) + "\n");
2438
+ return;
2439
+ }
2440
+
2441
+ throw new Error("Usage: slrd rewards <inspect|claim> <token|ca>");
2442
+ }
2443
+
2351
2444
  if (command === "claim") {
2352
2445
  const token = values[0];
2353
2446
  if (!token)
@@ -2508,7 +2601,10 @@ async function main() {
2508
2601
  }
2509
2602
  }
2510
2603
  main().catch((error) => {
2511
- emit(`${OWL} error: ${formatError(error)}\n`);
2604
+ const friendly = friendlyCliError(error);
2605
+ emit(
2606
+ friendly ? `${OWL} ${friendly}\n` : `${OWL} error: ${formatError(error)}\n`,
2607
+ );
2512
2608
  process.exitCode = 1;
2513
2609
  });
2514
2610