@solard/cli 0.2.14 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solard/cli",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
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.15",
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[];
@@ -319,20 +376,21 @@ export async function handleFairfunCommand(args: {
319
376
  if (command === "rewards" && values[0] === "distribute") {
320
377
  const ref = values[1];
321
378
  const wallet = flag(flags, "wallet");
322
- if (!ref || !wallet)
379
+ const id = flag(flags, "id");
380
+ const snapshotPath = flag(flags, "snapshot");
381
+ if (!ref || !wallet || !id || !snapshotPath)
323
382
  throw new Error(
324
- "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> [--claim-first | --amount-raw N] [--reward-mint <mint>] [--id <epoch>] [--live]",
383
+ "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--reserve-raw N] [--live]",
325
384
  );
326
385
  const token = await ensureToken(slrd, ref);
386
+ const snapshot = rewardSnapshotFile(snapshotPath);
327
387
  const common = {
388
+ id,
328
389
  token: token.mint,
329
390
  wallet,
330
391
  rewardMint: flag(flags, "reward-mint"),
331
- amountRaw: raw(flag(flags, "amount-raw"), "--amount-raw"),
332
- claimFirst: bool(flags, "claim-first"),
392
+ snapshot,
333
393
  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
394
  maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
337
395
  };
338
396
  if (!bool(flags, "live")) {
@@ -340,35 +398,37 @@ export async function handleFairfunCommand(args: {
340
398
  emit(
341
399
  json({
342
400
  live: false,
401
+ version: plan.version,
343
402
  id: plan.id,
403
+ snapshotId: plan.snapshotId,
344
404
  tokenMint: plan.tokenMint,
345
405
  sourceWallet: plan.sourceWallet,
346
- claimFirst: plan.claimFirst,
347
406
  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
- })),
407
+ totalClaimedRaw: plan.totalClaimedRaw,
408
+ totalEntitledRaw: plan.totalEntitledRaw,
409
+ totalConfirmedPaidRaw: plan.totalConfirmedPaidRaw,
410
+ totalOutstandingRaw: plan.totalOutstandingRaw,
411
+ sourceBalanceRaw: plan.sourceBalanceRaw,
412
+ availableRaw: plan.availableRaw,
413
+ reserveRaw: plan.reserveRaw,
414
+ outstanding: plan.outstanding,
415
+ pending: plan.pending,
416
+ nextBatch: plan.transferPlan?.batches[0]
417
+ ? {
418
+ recipients: plan.transferPlan.batches[0].allocations.length,
419
+ totalRaw: plan.transferPlan.batches[0].totalRaw,
420
+ serializedSize:
421
+ plan.transferPlan.batches[0].estimatedSerializedSize,
422
+ payments: plan.nextPayments,
423
+ }
424
+ : null,
359
425
  }) + "\n",
360
426
  );
361
427
  return true;
362
428
  }
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
429
  requireLiveGate();
369
430
  const state = await slrd.distributeHolderRewards({
370
431
  ...common,
371
- id,
372
432
  via: flag(flags, "sender") ?? "rpc",
373
433
  skipSimulation: bool(flags, "skip-simulation"),
374
434
  skipPreflight: bool(flags, "skip-preflight"),
package/src/index.ts CHANGED
@@ -421,7 +421,8 @@ Trading
421
421
  slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
422
422
  slrd transfer-many status <stable-id>
423
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]
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
425
426
  slrd rewards status <distribution-id>
426
427
  slrd rewards inspect <token|ca> Show claim source, quote asset, estimated accrued creator reward, and payout address
427
428
  slrd rewards claim <token|ca> --wallet <fee-payer> Claim creator rewards; on-chain beneficiary receives the reward
@@ -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,42 @@ 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();
121
+ const wallets = probe.db.wallets
122
+ .select()
123
+ .orderBy("id", "asc")
124
+ .all() as Array<{
125
+ name: string;
126
+ address: string;
127
+ }>;
110
128
  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 {
129
+ const failures: Array<{ name: string; address: string }> = [];
130
+ let decrypted = 0;
131
+ for (const wallet of wallets) {
132
+ try {
133
+ probe.wallets.signer(wallet.address);
134
+ decrypted += 1;
135
+ } catch {
136
+ failures.push({ name: wallet.name, address: wallet.address });
137
+ }
138
+ }
139
+ if (!failures.length) return;
116
140
  clearMasterKey();
117
- throw new VaultOnboardingError(
118
- "That wallet password could not decrypt the stored wallets.",
141
+ if (decrypted === 0) {
142
+ throw new VaultOnboardingError(
143
+ "That wallet password could not decrypt the stored wallet database.",
144
+ );
145
+ }
146
+ const affected = failures
147
+ .map((wallet) => `@${wallet.name} ${wallet.address}`)
148
+ .join(", ");
149
+ throw new VaultIntegrityError(
150
+ `Wallet database integrity check failed: one master password must decrypt every stored signing wallet. ` +
151
+ `Decrypted ${decrypted}/${wallets.length}; undecryptable: ${affected}`,
119
152
  );
120
153
  } finally {
121
154
  probe.close();
@@ -126,9 +159,10 @@ async function unlockExistingWallets(): Promise<string> {
126
159
  for (let attempt = 1; attempt <= 3; attempt += 1) {
127
160
  const password = await promptHidden("Wallet password: ");
128
161
  try {
129
- await validateExistingWalletPassword(password);
162
+ await validateAllStoredWallets(password);
130
163
  return password;
131
164
  } catch (error) {
165
+ if (error instanceof VaultIntegrityError) throw error;
132
166
  if (attempt === 3) throw error;
133
167
  process.stdout.write("Incorrect password. Try again.\n\n");
134
168
  }
@@ -136,18 +170,13 @@ async function unlockExistingWallets(): Promise<string> {
136
170
  throw new VaultOnboardingError("Unable to unlock the stored wallets.");
137
171
  }
138
172
 
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
173
  export async function ensureCliVaultReady(options: {
146
174
  existingWalletCount: number;
147
175
  allowCreate?: boolean;
148
176
  }): Promise<VaultReadyResult> {
149
- const existing = process.env.SLRD_MASTER_KEY?.trim();
177
+ const existing = environmentMasterKey();
150
178
  if (existing) {
179
+ await validateAllStoredWallets(existing);
151
180
  return {
152
181
  source: "env",
153
182
  created: false,
@@ -197,13 +226,12 @@ export function cliVaultStatus(): {
197
226
  } {
198
227
  return {
199
228
  mode: "ephemeral-password",
200
- environmentOverride: Boolean(process.env.SLRD_MASTER_KEY?.trim()),
229
+ environmentOverride: environmentMasterKey() != null,
201
230
  passwordPersisted: false,
202
231
  remembered: false,
203
232
  };
204
233
  }
205
234
 
206
- /** Optional helper for tests/tools that want a visible prompt without a signing command. */
207
235
  export async function promptForWalletPassword(
208
236
  existingWalletCount: number,
209
237
  ): Promise<void> {