@solard/cli 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solard/cli",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Command-line interface for the Solard SDK.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -18,7 +18,7 @@
18
18
  "bun": ">=1.1.0"
19
19
  },
20
20
  "dependencies": {
21
- "@solard/sdk": "0.2.14",
21
+ "@solard/sdk": "0.2.16",
22
22
  "bs58": "^6.0.0",
23
23
  "@solana/web3.js": "^1.98.4"
24
24
  },
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { PublicKey } from "@solana/web3.js";
3
- import type { Solard } from "@solard/sdk";
3
+ import type { HolderRewardEntitlementSnapshotInput, Solard } from "@solard/sdk";
4
4
 
5
5
  export type FairfunCliFlags = Map<string, string>;
6
6
  type Emit = (value: string) => void;
@@ -110,6 +110,63 @@ function allocationsFile(
110
110
  });
111
111
  }
112
112
 
113
+ function rewardSnapshotFile(
114
+ path: string,
115
+ ): HolderRewardEntitlementSnapshotInput {
116
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as any;
117
+ if (!parsed || typeof parsed !== "object")
118
+ throw new Error("Reward snapshot must be a JSON object");
119
+ const id = String(parsed.id ?? parsed.snapshotId ?? "").trim();
120
+ if (!id) throw new Error("Reward snapshot requires id");
121
+ const totalClaimedRaw = String(parsed.totalClaimedRaw ?? "").trim();
122
+ if (!/^\d+$/.test(totalClaimedRaw))
123
+ throw new Error("Reward snapshot totalClaimedRaw must be a raw integer");
124
+ if (!Array.isArray(parsed.recipients))
125
+ throw new Error("Reward snapshot requires recipients: [...]");
126
+ const recipients = parsed.recipients.map((row: any, index: number) => {
127
+ if (!row || typeof row !== "object")
128
+ throw new Error(
129
+ `Reward snapshot recipient ${index + 1} must be an object`,
130
+ );
131
+ const wallet = String(row.wallet ?? "").trim();
132
+ const entitledRaw = String(row.entitledRaw ?? "").trim();
133
+ if (!wallet)
134
+ throw new Error(`Reward snapshot recipient ${index + 1} requires wallet`);
135
+ if (!/^\d+$/.test(entitledRaw))
136
+ throw new Error(
137
+ `Reward snapshot recipient ${index + 1} entitledRaw must be a raw integer`,
138
+ );
139
+ return {
140
+ wallet: new PublicKey(wallet).toBase58(),
141
+ entitledRaw,
142
+ };
143
+ });
144
+ const slot = parsed.slot == null ? null : Number(parsed.slot);
145
+ if (slot != null && (!Number.isInteger(slot) || slot < 0))
146
+ throw new Error("Reward snapshot slot must be a non-negative integer");
147
+ const observedAtMs =
148
+ parsed.observedAtMs == null ? null : Number(parsed.observedAtMs);
149
+ if (
150
+ observedAtMs != null &&
151
+ (!Number.isFinite(observedAtMs) || observedAtMs < 0)
152
+ )
153
+ throw new Error(
154
+ "Reward snapshot observedAtMs must be a non-negative number",
155
+ );
156
+ const claimSignature =
157
+ parsed.claimSignature == null
158
+ ? null
159
+ : String(parsed.claimSignature).trim() || null;
160
+ return {
161
+ id,
162
+ totalClaimedRaw,
163
+ recipients,
164
+ slot,
165
+ observedAtMs,
166
+ claimSignature,
167
+ };
168
+ }
169
+
113
170
  export async function handleFairfunCommand(args: {
114
171
  command: string | undefined;
115
172
  values: string[];
@@ -159,18 +216,41 @@ export async function handleFairfunCommand(args: {
159
216
  const ref = values[0];
160
217
  if (!ref)
161
218
  throw new Error(
162
- "Usage: slrd events <token|ca> [--swaps-only|--transfers-only] [--jsonl]",
219
+ "Usage: slrd events <token|ca> [--all|--swaps|--transfers|--creates|--swaps-only|--transfers-only] [--jsonl]",
163
220
  );
164
221
  const token = await ensureToken(slrd, ref);
165
222
  const controller = new AbortController();
166
223
  const stop = () => controller.abort();
167
224
  process.once("SIGINT", stop);
168
225
  process.once("SIGTERM", stop);
169
- const swaps = !bool(flags, "transfers-only");
170
- const transfers = !bool(flags, "swaps-only");
226
+
227
+ const explicit =
228
+ bool(flags, "all") ||
229
+ bool(flags, "swaps") ||
230
+ bool(flags, "transfers") ||
231
+ bool(flags, "creates");
232
+ let swaps = true;
233
+ let transfers = true;
234
+ let creates = false;
235
+ if (bool(flags, "swaps-only")) {
236
+ swaps = true;
237
+ transfers = false;
238
+ creates = false;
239
+ } else if (bool(flags, "transfers-only")) {
240
+ swaps = false;
241
+ transfers = true;
242
+ creates = false;
243
+ } else if (explicit) {
244
+ const all = bool(flags, "all");
245
+ swaps = all || bool(flags, "swaps");
246
+ transfers = all || bool(flags, "transfers");
247
+ creates = all || bool(flags, "creates");
248
+ }
249
+
171
250
  const subscription = await slrd.subscribeTokenEvents(token.mint, {
172
251
  swaps,
173
252
  transfers,
253
+ creates,
174
254
  commitment: bool(flags, "finalized") ? "finalized" : "confirmed",
175
255
  signal: controller.signal,
176
256
  });
@@ -194,7 +274,12 @@ export async function handleFairfunCommand(args: {
194
274
  if (event.type === "swap") {
195
275
  emit(
196
276
  `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} SWAP ${event.venue} ${event.side.toUpperCase()} ` +
197
- `${event.tokenAmountRaw} tokenRaw trader=${event.trader ?? "?"} sig=${event.signature}\n`,
277
+ `${event.tokenAmountRaw} tokenRaw quote=${event.quoteAmountRaw ?? "?"} ${event.quoteMint} trader=${event.trader ?? "?"} sig=${event.signature}\n`,
278
+ );
279
+ } else if (event.type === "create") {
280
+ emit(
281
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} CREATE ` +
282
+ `${event.symbol ?? "?"} ${event.name ?? "?"} creator=${event.creator ?? "?"} quote=${event.quoteMint} sig=${event.signature}\n`,
198
283
  );
199
284
  } else {
200
285
  emit(
@@ -319,20 +404,21 @@ export async function handleFairfunCommand(args: {
319
404
  if (command === "rewards" && values[0] === "distribute") {
320
405
  const ref = values[1];
321
406
  const wallet = flag(flags, "wallet");
322
- if (!ref || !wallet)
407
+ const id = flag(flags, "id");
408
+ const snapshotPath = flag(flags, "snapshot");
409
+ if (!ref || !wallet || !id || !snapshotPath)
323
410
  throw new Error(
324
- "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> [--claim-first | --amount-raw N] [--reward-mint <mint>] [--id <epoch>] [--live]",
411
+ "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--reserve-raw N] [--live]",
325
412
  );
326
413
  const token = await ensureToken(slrd, ref);
414
+ const snapshot = rewardSnapshotFile(snapshotPath);
327
415
  const common = {
416
+ id,
328
417
  token: token.mint,
329
418
  wallet,
330
419
  rewardMint: flag(flags, "reward-mint"),
331
- amountRaw: raw(flag(flags, "amount-raw"), "--amount-raw"),
332
- claimFirst: bool(flags, "claim-first"),
420
+ snapshot,
333
421
  reserveRaw: raw(flag(flags, "reserve-raw"), "--reserve-raw") ?? 0n,
334
- excludeOwners: csv(flag(flags, "exclude")),
335
- minimumHolderRaw: raw(flag(flags, "min-holder-raw"), "--min-holder-raw"),
336
422
  maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
337
423
  };
338
424
  if (!bool(flags, "live")) {
@@ -340,35 +426,37 @@ export async function handleFairfunCommand(args: {
340
426
  emit(
341
427
  json({
342
428
  live: false,
429
+ version: plan.version,
343
430
  id: plan.id,
431
+ snapshotId: plan.snapshotId,
344
432
  tokenMint: plan.tokenMint,
345
433
  sourceWallet: plan.sourceWallet,
346
- claimFirst: plan.claimFirst,
347
434
  rewardAsset: plan.rewardAsset,
348
- rewardAmountRaw: plan.rewardAmountRaw,
349
- snapshotSlot: plan.snapshot.slot,
350
- holders: plan.snapshot.eligibleHolderCount,
351
- allocations: plan.allocations.length,
352
- undistributedRemainderRaw: plan.undistributedRemainderRaw,
353
- batches: plan.transferPlan.batches.map((batch) => ({
354
- index: batch.index,
355
- recipients: batch.allocations.length,
356
- totalRaw: batch.totalRaw,
357
- serializedSize: batch.estimatedSerializedSize,
358
- })),
435
+ totalClaimedRaw: plan.totalClaimedRaw,
436
+ totalEntitledRaw: plan.totalEntitledRaw,
437
+ totalConfirmedPaidRaw: plan.totalConfirmedPaidRaw,
438
+ totalOutstandingRaw: plan.totalOutstandingRaw,
439
+ sourceBalanceRaw: plan.sourceBalanceRaw,
440
+ availableRaw: plan.availableRaw,
441
+ reserveRaw: plan.reserveRaw,
442
+ outstanding: plan.outstanding,
443
+ pending: plan.pending,
444
+ nextBatch: plan.transferPlan?.batches[0]
445
+ ? {
446
+ recipients: plan.transferPlan.batches[0].allocations.length,
447
+ totalRaw: plan.transferPlan.batches[0].totalRaw,
448
+ serializedSize:
449
+ plan.transferPlan.batches[0].estimatedSerializedSize,
450
+ payments: plan.nextPayments,
451
+ }
452
+ : null,
359
453
  }) + "\n",
360
454
  );
361
455
  return true;
362
456
  }
363
- const id = flag(flags, "id");
364
- if (!id)
365
- throw new Error(
366
- "Live holder distribution requires --id <stable-epoch-id> so retries are idempotent.",
367
- );
368
457
  requireLiveGate();
369
458
  const state = await slrd.distributeHolderRewards({
370
459
  ...common,
371
- id,
372
460
  via: flag(flags, "sender") ?? "rpc",
373
461
  skipSimulation: bool(flags, "skip-simulation"),
374
462
  skipPreflight: bool(flags, "skip-preflight"),
package/src/index.ts CHANGED
@@ -322,7 +322,7 @@ async function storedWalletCount(): Promise<number> {
322
322
  const { createTraderSolard } = await import("@solard/sdk");
323
323
  const probe = createTraderSolard();
324
324
  try {
325
- return probe.wallets.list().length;
325
+ return (probe.db.wallets.select().all() as unknown[]).length;
326
326
  } finally {
327
327
  probe.close();
328
328
  }
@@ -333,7 +333,7 @@ function help(): string {
333
333
 
334
334
  Wallets and tokens
335
335
  slrd setup [--status] Check ephemeral wallet-password mode; no password is persisted
336
- slrd wallet create [name] Generate and persist an encrypted Solana wallet
336
+ slrd wallet create [name] [--vanity <suffix>] Generate and persist an encrypted Solana wallet
337
337
  slrd import <private_key> [name]
338
338
  cat key.json | slrd import --stdin [name]
339
339
 
@@ -356,7 +356,7 @@ External contacts (public addresses only; never signing wallets or group members
356
356
  slrd token analyze <ca> [--top N] [--json]
357
357
  slrd tokens
358
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
359
+ slrd events <token|ca> [--all|--swaps|--transfers|--creates] [--jsonl] Live typed Pump/PumpSwap + transfer/create stream
360
360
 
361
361
  Vanity mints
362
362
  slrd vanity --suffix pump --out .\\mint.json [--count <n>]
@@ -365,6 +365,9 @@ Vanity mints
365
365
  slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
366
366
  launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
367
367
 
368
+ Pump pairs
369
+ slrd pump pairs [--json] Read Pump-supported quote mints from the on-chain Global account
370
+
368
371
  Metadata and launching
369
372
  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]
370
373
  [--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
@@ -421,7 +424,8 @@ Trading
421
424
  slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
422
425
  slrd transfer-many status <stable-id>
423
426
  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]
427
+ slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--live]
428
+ Snapshot contains cumulative totalClaimedRaw + recipients[].entitledRaw; Solard tracks confirmed payouts
425
429
  slrd rewards status <distribution-id>
426
430
  slrd rewards inspect <token|ca> Show claim source, quote asset, estimated accrued creator reward, and payout address
427
431
  slrd rewards claim <token|ca> --wallet <fee-payer> Claim creator rewards; on-chain beneficiary receives the reward
@@ -558,7 +562,7 @@ async function main() {
558
562
  return;
559
563
  }
560
564
 
561
- if (needsVault && !process.env.SLRD_MASTER_KEY?.trim()) {
565
+ if (needsVault) {
562
566
  const allowCreate =
563
567
  (command === "wallet" && values[0] === "create") ||
564
568
  command === "import" ||
@@ -859,6 +863,20 @@ async function main() {
859
863
  return;
860
864
  }
861
865
 
866
+ if (command === "pump" && values[0] === "pairs") {
867
+ const pairs = await slrd.pump.getSupportedPairs();
868
+ if (flags.has("json")) {
869
+ emit(json(pairs) + "\n");
870
+ } else {
871
+ for (const pair of pairs) {
872
+ emit(
873
+ `${(pair.symbol ?? "?").padEnd(12)} ${(pair.name ?? "?").padEnd(28)} ${pair.mint} decimals=${pair.decimals}\n`,
874
+ );
875
+ }
876
+ }
877
+ return;
878
+ }
879
+
862
880
  if (command === "swap") {
863
881
  const { runJupiterSwapCommand } =
864
882
  await import("./commands/jupiter-swap.ts");
@@ -928,16 +946,46 @@ async function main() {
928
946
 
929
947
  if (command === "wallet" && values[0] === "create") {
930
948
  const firstWallet = slrd.wallets.list().length === 0;
931
- const wallet = slrd.createWallet(
932
- values[1] ?? (firstWallet ? "main" : undefined),
933
- );
949
+ const name = values[1] ?? (firstWallet ? "main" : undefined);
950
+ const vanity = flags.has("vanity") ? need(flags, "vanity") : null;
951
+ const vanityResult = vanity
952
+ ? await slrd.createVanityWallet(name, {
953
+ suffix: vanity,
954
+ workers: int(flags, "workers"),
955
+ maxAttempts: int(flags, "max-attempts"),
956
+ timeoutMs: int(flags, "timeout-ms", 0),
957
+ reportEvery: int(flags, "report-every", 1_000_000),
958
+ onProgress: process.stderr.isTTY
959
+ ? (progress) =>
960
+ process.stderr.write(
961
+ `${OWL} wallet vanity ${progress.suffix}: ${progress.attempts} attempts, ${progress.ratePerSecond}/s\r`,
962
+ )
963
+ : undefined,
964
+ })
965
+ : null;
966
+ if (vanityResult && process.stderr.isTTY) process.stderr.write("\n");
967
+ const wallet = vanityResult?.wallet ?? slrd.createWallet(name);
968
+ const output = vanityResult
969
+ ? {
970
+ ...wallet,
971
+ vanity: {
972
+ suffix: vanityResult.suffix,
973
+ attempts: vanityResult.attempts,
974
+ elapsedMs: vanityResult.elapsedMs,
975
+ ratePerSecond: vanityResult.ratePerSecond,
976
+ },
977
+ }
978
+ : wallet;
934
979
  if (flags.has("json")) {
935
- emit(json(wallet) + "\n");
980
+ emit(json(output) + "\n");
936
981
  } else if (process.stdout.isTTY) {
937
982
  emit(
938
983
  `\n╭─ ${OWL} wallet ready ──────────────────────────────────╮\n` +
939
984
  ` name @${wallet.name}\n` +
940
985
  ` address ${wallet.address}\n` +
986
+ (vanityResult
987
+ ? ` vanity suffix=${vanityResult.suffix} attempts=${vanityResult.attempts}\n`
988
+ : "") +
941
989
  ` storage encrypted in Solard's local wallet database\n` +
942
990
  ` password NOT saved; you will enter it again next invocation\n` +
943
991
  `╰──────────────────────────────────────────────────────╯\n` +
@@ -1,5 +1,9 @@
1
1
  export class VaultOnboardingError extends Error {
2
- readonly name = "VaultOnboardingError";
2
+ readonly name: string = "VaultOnboardingError";
3
+ }
4
+
5
+ export class VaultIntegrityError extends VaultOnboardingError {
6
+ readonly name: string = "VaultIntegrityError";
3
7
  }
4
8
 
5
9
  export type VaultReadyResult = {
@@ -30,7 +34,10 @@ async function promptHidden(message: string): Promise<string> {
30
34
 
31
35
  return await new Promise<string>((resolve, reject) => {
32
36
  let value = "";
37
+ let settled = false;
33
38
  const cleanup = () => {
39
+ if (settled) return;
40
+ settled = true;
34
41
  input.off("data", onData);
35
42
  input.setRawMode(Boolean(wasRaw));
36
43
  input.pause();
@@ -67,6 +74,11 @@ function clearMasterKey(): void {
67
74
  delete process.env.SLRD_MASTER_KEY;
68
75
  }
69
76
 
77
+ function environmentMasterKey(): string | null {
78
+ const value = process.env.SLRD_MASTER_KEY;
79
+ return value != null && value.length > 0 ? value : null;
80
+ }
81
+
70
82
  async function confirmNewPassword(): Promise<string> {
71
83
  process.stdout.write(
72
84
  [
@@ -101,21 +113,25 @@ async function confirmNewPassword(): Promise<string> {
101
113
  );
102
114
  }
103
115
 
104
- async function validateExistingWalletPassword(password: string): Promise<void> {
116
+ async function validateAllStoredWallets(password: string): Promise<void> {
105
117
  setMasterKey(password);
106
118
  const { createTraderSolard } = await import("@solard/sdk");
107
119
  const probe = createTraderSolard();
108
120
  try {
109
- const wallets = probe.wallets.list();
110
- if (!wallets.length) return;
111
- // Decrypt one persisted wallet. All persisted wallets use the same process-level
112
- // SLRD_MASTER_KEY contract, so this is enough to catch a mistyped password before
113
- // the requested command begins doing work.
114
- probe.wallets.signer(wallets[0]!.address);
115
- } catch {
121
+ const integrity = probe.wallets.integrity();
122
+ if (!integrity.failures.length) return;
116
123
  clearMasterKey();
117
- throw new VaultOnboardingError(
118
- "That wallet password could not decrypt the stored wallets.",
124
+ if (integrity.decrypted === 0) {
125
+ throw new VaultOnboardingError(
126
+ "That wallet password could not decrypt the stored wallet database.",
127
+ );
128
+ }
129
+ const affected = integrity.failures
130
+ .map((wallet) => `@${wallet.name} ${wallet.address}`)
131
+ .join(", ");
132
+ throw new VaultIntegrityError(
133
+ `Wallet database integrity check failed: one master password must decrypt every stored signing wallet. ` +
134
+ `Decrypted ${integrity.decrypted}/${integrity.total}; undecryptable or mismatched: ${affected}`,
119
135
  );
120
136
  } finally {
121
137
  probe.close();
@@ -126,9 +142,10 @@ async function unlockExistingWallets(): Promise<string> {
126
142
  for (let attempt = 1; attempt <= 3; attempt += 1) {
127
143
  const password = await promptHidden("Wallet password: ");
128
144
  try {
129
- await validateExistingWalletPassword(password);
145
+ await validateAllStoredWallets(password);
130
146
  return password;
131
147
  } catch (error) {
148
+ if (error instanceof VaultIntegrityError) throw error;
132
149
  if (attempt === 3) throw error;
133
150
  process.stdout.write("Incorrect password. Try again.\n\n");
134
151
  }
@@ -136,18 +153,13 @@ async function unlockExistingWallets(): Promise<string> {
136
153
  throw new VaultOnboardingError("Unable to unlock the stored wallets.");
137
154
  }
138
155
 
139
- /**
140
- * Ensure the process has an SLRD_MASTER_KEY before a command that needs a local
141
- * signing wallet runs. Nothing is written to disk. The password/master-key value
142
- * exists only in process.env for this process (and intentionally inherited child
143
- * processes such as `slrd run ...`).
144
- */
145
156
  export async function ensureCliVaultReady(options: {
146
157
  existingWalletCount: number;
147
158
  allowCreate?: boolean;
148
159
  }): Promise<VaultReadyResult> {
149
- const existing = process.env.SLRD_MASTER_KEY?.trim();
160
+ const existing = environmentMasterKey();
150
161
  if (existing) {
162
+ await validateAllStoredWallets(existing);
151
163
  return {
152
164
  source: "env",
153
165
  created: false,
@@ -197,13 +209,12 @@ export function cliVaultStatus(): {
197
209
  } {
198
210
  return {
199
211
  mode: "ephemeral-password",
200
- environmentOverride: Boolean(process.env.SLRD_MASTER_KEY?.trim()),
212
+ environmentOverride: environmentMasterKey() != null,
201
213
  passwordPersisted: false,
202
214
  remembered: false,
203
215
  };
204
216
  }
205
217
 
206
- /** Optional helper for tests/tools that want a visible prompt without a signing command. */
207
218
  export async function promptForWalletPassword(
208
219
  existingWalletCount: number,
209
220
  ): Promise<void> {