@solard/cli 0.2.16 → 0.2.19

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.
@@ -1,4 +1,8 @@
1
- import type { TradeAsset, TradeRoute, TradeRouteResolution } from "@solard/sdk";
1
+ import type {
2
+ TradeAsset,
3
+ TradeRoute,
4
+ TradeRouteResolution,
5
+ } from "@solard/core";
2
6
 
3
7
  export type TradeRouteSummary = {
4
8
  route: TradeRoute;
@@ -1,4 +1,4 @@
1
- import type { Solard } from "@solard/sdk";
1
+ import type { Solard } from "@solard/core";
2
2
  import type { TradeCommandFlags } from "./trade-venue.ts";
3
3
 
4
4
  export type TradeTargets = {
@@ -1,4 +1,4 @@
1
- import type { VenuePreference } from "@solard/sdk";
1
+ import type { VenuePreference } from "@solard/core";
2
2
 
3
3
  export type TradeCommandFlags = ReadonlyMap<string, string>;
4
4
 
@@ -6,7 +6,7 @@ import {
6
6
  transferTokenIxs,
7
7
  type SenderId,
8
8
  type Solard,
9
- } from "@solard/sdk";
9
+ } from "@solard/core";
10
10
 
11
11
  import { resolveDestinationRef } from "../refs.ts";
12
12
 
@@ -1,6 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { PublicKey } from "@solana/web3.js";
3
- import type { HolderRewardEntitlementSnapshotInput, Solard } from "@solard/sdk";
3
+ import type { Solard } from "@solard/core";
4
+
5
+ type FairfunEntitlementSnapshot = {
6
+ recipients: Array<{ wallet: string; entitledRaw: string }>;
7
+ totalEntitledRaw?: string;
8
+ observedAtMs: number | null;
9
+ };
4
10
 
5
11
  export type FairfunCliFlags = Map<string, string>;
6
12
  type Emit = (value: string) => void;
@@ -8,7 +14,12 @@ type Emit = (value: string) => void;
8
14
  function json(value: unknown): string {
9
15
  return JSON.stringify(
10
16
  value,
11
- (_, item) => (typeof item === "bigint" ? item.toString() : item),
17
+ (_, item) =>
18
+ typeof item === "bigint"
19
+ ? item.toString()
20
+ : item instanceof Map
21
+ ? Object.fromEntries(item)
22
+ : item,
12
23
  2,
13
24
  );
14
25
  }
@@ -110,26 +121,26 @@ function allocationsFile(
110
121
  });
111
122
  }
112
123
 
113
- function rewardSnapshotFile(
114
- path: string,
115
- ): HolderRewardEntitlementSnapshotInput {
124
+ function rewardSnapshotFile(path: string): FairfunEntitlementSnapshot {
116
125
  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) => {
126
+ const rows = Array.isArray(parsed) ? parsed : parsed?.recipients;
127
+ if (!Array.isArray(rows))
128
+ throw new Error(
129
+ "Reward snapshot must be a JSON array or { recipients: [...] }",
130
+ );
131
+ const recipients = rows.map((row: any, index: number) => {
127
132
  if (!row || typeof row !== "object")
128
133
  throw new Error(
129
134
  `Reward snapshot recipient ${index + 1} must be an object`,
130
135
  );
131
- const wallet = String(row.wallet ?? "").trim();
132
- const entitledRaw = String(row.entitledRaw ?? "").trim();
136
+ const wallet = String(row.wallet ?? row.address ?? "").trim();
137
+ const entitledRaw = String(
138
+ row.entitledRaw ??
139
+ row.accumulatedRewardRaw ??
140
+ row.accumulatedRaw ??
141
+ row.rewardRaw ??
142
+ "",
143
+ ).trim();
133
144
  if (!wallet)
134
145
  throw new Error(`Reward snapshot recipient ${index + 1} requires wallet`);
135
146
  if (!/^\d+$/.test(entitledRaw))
@@ -141,11 +152,17 @@ function rewardSnapshotFile(
141
152
  entitledRaw,
142
153
  };
143
154
  });
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");
155
+ const totalInput = Array.isArray(parsed)
156
+ ? undefined
157
+ : (parsed.totalEntitledRaw ?? parsed.totalRewardRaw ?? undefined);
158
+ const totalEntitledRaw =
159
+ totalInput == null ? undefined : String(totalInput).trim();
160
+ if (totalEntitledRaw != null && !/^\d+$/.test(totalEntitledRaw))
161
+ throw new Error("Reward snapshot totalEntitledRaw must be a raw integer");
147
162
  const observedAtMs =
148
- parsed.observedAtMs == null ? null : Number(parsed.observedAtMs);
163
+ Array.isArray(parsed) || parsed.observedAtMs == null
164
+ ? null
165
+ : Number(parsed.observedAtMs);
149
166
  if (
150
167
  observedAtMs != null &&
151
168
  (!Number.isFinite(observedAtMs) || observedAtMs < 0)
@@ -153,18 +170,7 @@ function rewardSnapshotFile(
153
170
  throw new Error(
154
171
  "Reward snapshot observedAtMs must be a non-negative number",
155
172
  );
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
- };
173
+ return { recipients, totalEntitledRaw, observedAtMs };
168
174
  }
169
175
 
170
176
  export async function handleFairfunCommand(args: {
@@ -212,6 +218,59 @@ export async function handleFairfunCommand(args: {
212
218
  return true;
213
219
  }
214
220
 
221
+ if (command === "events" && values[0] === "history") {
222
+ const ref = values[1];
223
+ if (!ref)
224
+ throw new Error(
225
+ "Usage: slrd events history <token|ca> [--provider auto|solscan|rpc] [--from-slot N] [--to-slot N] [--slot-order] [--json|--jsonl]",
226
+ );
227
+ const token = await ensureToken(slrd, ref);
228
+ const provider = flag(flags, "provider") ?? "auto";
229
+ if (!new Set(["auto", "solscan", "rpc"]).has(provider))
230
+ throw new Error("--provider must be auto, solscan, or rpc");
231
+ const history = await slrd.events.history(token.mint, {
232
+ provider: provider as "auto" | "solscan" | "rpc",
233
+ fromSlot: nonNegativeInteger(flag(flags, "from-slot")),
234
+ toSlot: nonNegativeInteger(flag(flags, "to-slot")),
235
+ maxPages: positiveInteger(flag(flags, "max-pages")),
236
+ exactOrdering: !bool(flags, "slot-order"),
237
+ verifyCurrentBalances: !bool(flags, "no-verify"),
238
+ commitment: bool(flags, "confirmed") ? "confirmed" : "finalized",
239
+ });
240
+ if (bool(flags, "json")) {
241
+ emit(json({ coverage: history.coverage, events: history.events }) + "\n");
242
+ return true;
243
+ }
244
+ if (bool(flags, "jsonl")) {
245
+ for (const event of history.events) {
246
+ emit(
247
+ JSON.stringify(event, (_, item) =>
248
+ typeof item === "bigint" ? item.toString() : item,
249
+ ) + "\n",
250
+ );
251
+ }
252
+ emit(JSON.stringify({ type: "coverage", ...history.coverage }) + "\n");
253
+ return true;
254
+ }
255
+ emit(
256
+ `EVENT HISTORY ${history.mint}\n` +
257
+ `provider=${history.coverage.provider} coverage=${history.coverage.status} transfer-coverage=${history.coverage.transferCoverage} from-creation=${history.coverage.fromCreation} authoritative=${history.coverage.authoritativeForBalanceReplay}\n` +
258
+ `events=${history.events.length} slots=${history.coverage.firstEventSlot ?? "?"}..${history.coverage.lastEventSlot ?? "?"} ordering=${history.coverage.ordering}\n` +
259
+ `balance-check=${history.coverage.balanceVerification.matches == null ? "not-checked" : history.coverage.balanceVerification.matches ? "match" : "MISMATCH"}\n\n`,
260
+ );
261
+ for (const event of history.events) {
262
+ emit(
263
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} ${event.movement.toUpperCase()} ${event.amountRaw} ` +
264
+ `${event.sourceOwner ?? "MINT"} -> ${event.destinationOwner ?? "BURN"} slot=${event.slot} tx=${event.transactionIndex ?? "?"} sig=${event.signature}\n`,
265
+ );
266
+ }
267
+ if (history.coverage.warnings.length) {
268
+ emit("\nWARNINGS\n");
269
+ for (const warning of history.coverage.warnings) emit(`- ${warning}\n`);
270
+ }
271
+ return true;
272
+ }
273
+
215
274
  if (command === "events") {
216
275
  const ref = values[0];
217
276
  if (!ref)
@@ -283,7 +342,7 @@ export async function handleFairfunCommand(args: {
283
342
  );
284
343
  } else {
285
344
  emit(
286
- `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} TRANSFER ${event.amountRaw} ` +
345
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} ${event.movement.toUpperCase()} ${event.amountRaw} ` +
287
346
  `${event.sourceOwner ?? event.sourceTokenAccount} -> ${event.destinationOwner ?? event.destinationTokenAccount} sig=${event.signature}\n`,
288
347
  );
289
348
  }
@@ -393,46 +452,145 @@ export async function handleFairfunCommand(args: {
393
452
  return true;
394
453
  }
395
454
 
455
+ if (command === "rewards" && values[0] === "history") {
456
+ const ref = values[1];
457
+ if (!ref)
458
+ throw new Error(
459
+ "Usage: slrd rewards history <token|ca> [--wallet <reward-recipient>] [--provider auto|solscan|rpc] [--json|--jsonl]",
460
+ );
461
+ for (const key of [
462
+ "from-slot",
463
+ "to-slot",
464
+ "slot-order",
465
+ "no-verify",
466
+ "confirmed",
467
+ ]) {
468
+ if (flags.has(key)) {
469
+ throw new Error(
470
+ `--${key} is not supported by persistent replay; use slrd events history for bounded diagnostic history`,
471
+ );
472
+ }
473
+ }
474
+ const token = await ensureToken(slrd, ref);
475
+ const provider = flag(flags, "provider") ?? "auto";
476
+ if (!new Set(["auto", "solscan", "rpc"]).has(provider))
477
+ throw new Error("--provider must be auto, solscan, or rpc");
478
+ const history = await slrd.history.replay(token.mint, {
479
+ recipient: flag(flags, "wallet"),
480
+ provider: provider as "auto" | "solscan" | "rpc",
481
+ maxPages: positiveInteger(flag(flags, "max-pages")),
482
+ claimMaxPages: positiveInteger(flag(flags, "claim-max-pages")),
483
+ });
484
+ if (bool(flags, "json")) {
485
+ emit(
486
+ json({
487
+ mint: history.mint,
488
+ coverage: history.coverage,
489
+ items: history.items,
490
+ }) + "\n",
491
+ );
492
+ return true;
493
+ }
494
+ if (bool(flags, "jsonl")) {
495
+ for (const item of history.items) emit(json(item) + "\n");
496
+ emit(json({ type: "coverage", replay: history.coverage }) + "\n");
497
+ return true;
498
+ }
499
+ const transfers = history.items.filter((item) => item.trx !== "claim");
500
+ const claims = history.items.filter((item) => item.trx === "claim");
501
+ emit(
502
+ `REPLAY HISTORY ${history.mint}\n` +
503
+ `recipient=${"-"} authoritative=${history.coverage.complete} transfers=${transfers.length} claims=${claims.length} finalizedThroughSlot=${history.coverage.throughSlot}\n\n`,
504
+ );
505
+ for (const item of history.items) {
506
+ const event = item.raw;
507
+ if (event.type === "claim") {
508
+ emit(
509
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} CLAIM ${event.payout.amountRaw} ${event.payout.assetMint} attribution=${event.attribution} slot=${item.slot} tx=${item.transactionIndex ?? "?"} sig=${item.signature}\n`,
510
+ );
511
+ } else {
512
+ emit(
513
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} ${event.movement.toUpperCase()} ${event.amountRaw} ${event.sourceOwner ?? "MINT"} -> ${event.destinationOwner ?? "BURN"} slot=${item.slot} tx=${item.transactionIndex ?? "?"} sig=${item.signature}\n`,
514
+ );
515
+ }
516
+ }
517
+ if (history.coverage.warnings.length) {
518
+ emit("\nWARNINGS\n");
519
+ for (const warning of history.coverage.warnings) emit(`- ${warning}\n`);
520
+ }
521
+ return true;
522
+ }
523
+
396
524
  if (command === "rewards" && values[0] === "status") {
397
- const id = values[1];
398
- if (!id) throw new Error("Usage: slrd rewards status <distribution-id>");
399
- const { getHolderRewardDistributionState } = await import("@solard/sdk");
400
- emit(json(getHolderRewardDistributionState(slrd, id)) + "\n");
525
+ const ref = values[1];
526
+ if (!ref) throw new Error("Usage: slrd rewards status <token|ca>");
527
+ const token = await ensureToken(slrd, ref);
528
+ emit(json(slrd.distributions.status(`fairfun:${token.mint}`)) + "\n");
401
529
  return true;
402
530
  }
403
531
 
532
+ if (command === "rewards" && values[0] === "audit") {
533
+ const ref = values[1];
534
+ if (!ref)
535
+ throw new Error(
536
+ "Usage: slrd rewards audit <token|ca> [--holder <wallet>]",
537
+ );
538
+ const token = await ensureToken(slrd, ref);
539
+ const state = slrd.distributions.status(`fairfun:${token.mint}`);
540
+ const holder = flag(flags, "holder");
541
+ emit(
542
+ json(
543
+ holder && state
544
+ ? {
545
+ ...state,
546
+ recipients: state.recipients.filter(
547
+ (row) => row.recipient === holder,
548
+ ),
549
+ }
550
+ : state,
551
+ ) + "\n",
552
+ );
553
+ return true;
554
+ }
555
+
556
+ if (command === "rewards" && values[0] === "stop") {
557
+ const ref = values[1];
558
+ if (!ref) throw new Error("Usage: slrd rewards stop <token|ca>");
559
+ const token = await ensureToken(slrd, ref);
560
+ throw new Error(
561
+ `Distribution ${`fairfun:${token.mint}`} has no process-level stop state; stop the supervising process instead.`,
562
+ );
563
+ }
564
+
404
565
  if (command === "rewards" && values[0] === "distribute") {
405
566
  const ref = values[1];
406
567
  const wallet = flag(flags, "wallet");
407
- const id = flag(flags, "id");
408
568
  const snapshotPath = flag(flags, "snapshot");
409
- if (!ref || !wallet || !id || !snapshotPath)
569
+ if (!ref || !wallet || !snapshotPath)
410
570
  throw new Error(
411
- "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--reserve-raw N] [--live]",
571
+ "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> --snapshot <snapshot.json> [--reward-mint <mint>] [--reserve-raw N] [--live]",
412
572
  );
413
573
  const token = await ensureToken(slrd, ref);
414
574
  const snapshot = rewardSnapshotFile(snapshotPath);
415
575
  const common = {
416
- id,
417
- token: token.mint,
418
- wallet,
419
- rewardMint: flag(flags, "reward-mint"),
420
- snapshot,
576
+ id: flag(flags, "id") ?? `fairfun:${token.mint}`,
577
+ from: wallet,
578
+ asset: flag(flags, "reward-mint") ?? token.quoteMint ?? "SOL",
579
+ entitlements: snapshot.recipients.map((row) => ({
580
+ recipient: row.wallet,
581
+ entitledRaw: row.entitledRaw,
582
+ })),
421
583
  reserveRaw: raw(flag(flags, "reserve-raw"), "--reserve-raw") ?? 0n,
422
584
  maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
423
585
  };
424
586
  if (!bool(flags, "live")) {
425
- const plan = await slrd.planHolderRewards(common);
587
+ const plan = await slrd.distributions.plan(common);
426
588
  emit(
427
589
  json({
428
590
  live: false,
429
- version: plan.version,
430
591
  id: plan.id,
431
- snapshotId: plan.snapshotId,
432
- tokenMint: plan.tokenMint,
433
592
  sourceWallet: plan.sourceWallet,
434
- rewardAsset: plan.rewardAsset,
435
- totalClaimedRaw: plan.totalClaimedRaw,
593
+ asset: plan.asset,
436
594
  totalEntitledRaw: plan.totalEntitledRaw,
437
595
  totalConfirmedPaidRaw: plan.totalConfirmedPaidRaw,
438
596
  totalOutstandingRaw: plan.totalOutstandingRaw,
@@ -441,21 +599,15 @@ export async function handleFairfunCommand(args: {
441
599
  reserveRaw: plan.reserveRaw,
442
600
  outstanding: plan.outstanding,
443
601
  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
- }
602
+ nextBatch: plan.nextPayments.length
603
+ ? { payments: plan.nextPayments }
452
604
  : null,
453
605
  }) + "\n",
454
606
  );
455
607
  return true;
456
608
  }
457
609
  requireLiveGate();
458
- const state = await slrd.distributeHolderRewards({
610
+ const state = await slrd.distributions.execute({
459
611
  ...common,
460
612
  via: flag(flags, "sender") ?? "rpc",
461
613
  skipSimulation: bool(flags, "skip-simulation"),