@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solard/cli",
3
- "version": "0.2.16",
3
+ "version": "0.2.19",
4
4
  "description": "Command-line interface for the Solard SDK.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -18,9 +18,10 @@
18
18
  "bun": ">=1.1.0"
19
19
  },
20
20
  "dependencies": {
21
- "@solard/sdk": "0.2.16",
21
+ "@solard/core": "0.2.19",
22
22
  "bs58": "^6.0.0",
23
- "@solana/web3.js": "^1.98.4"
23
+ "@solana/web3.js": "^1.98.4",
24
+ "measure-fn": "5.2.0"
24
25
  },
25
26
  "scripts": {
26
27
  "typecheck": "tsc --noEmit -p ../../tsconfig.json"
@@ -0,0 +1,621 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { PublicKey } from "@solana/web3.js";
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
+ };
10
+
11
+ export type AccountingCliFlags = Map<string, string>;
12
+ type Emit = (value: string) => void;
13
+
14
+ function json(value: unknown): string {
15
+ return JSON.stringify(
16
+ value,
17
+ (_, item) =>
18
+ typeof item === "bigint"
19
+ ? item.toString()
20
+ : item instanceof Map
21
+ ? Object.fromEntries(item)
22
+ : item,
23
+ 2,
24
+ );
25
+ }
26
+
27
+ function flag(flags: AccountingCliFlags, key: string): string | undefined {
28
+ const value = flags.get(key);
29
+ return value && value !== "true" ? value : undefined;
30
+ }
31
+
32
+ function bool(flags: AccountingCliFlags, key: string): boolean {
33
+ return flags.has(key) && flags.get(key) !== "false";
34
+ }
35
+
36
+ function csv(value: string | undefined): string[] {
37
+ return (value ?? "")
38
+ .split(",")
39
+ .map((item) => item.trim())
40
+ .filter(Boolean);
41
+ }
42
+
43
+ function positiveInteger(
44
+ value: string | undefined,
45
+ fallback?: number,
46
+ ): number | undefined {
47
+ if (value == null) return fallback;
48
+ const parsed = Number(value);
49
+ if (!Number.isInteger(parsed) || parsed <= 0)
50
+ throw new Error(`Expected a positive integer, received ${value}`);
51
+ return parsed;
52
+ }
53
+
54
+ function nonNegativeInteger(
55
+ value: string | undefined,
56
+ fallback?: number,
57
+ ): number | undefined {
58
+ if (value == null) return fallback;
59
+ const parsed = Number(value);
60
+ if (!Number.isInteger(parsed) || parsed < 0)
61
+ throw new Error(`Expected a non-negative integer, received ${value}`);
62
+ return parsed;
63
+ }
64
+
65
+ function raw(value: string | undefined, name: string): bigint | undefined {
66
+ if (value == null) return undefined;
67
+ if (!/^\d+$/.test(value))
68
+ throw new Error(`${name} must be a raw integer amount`);
69
+ return BigInt(value);
70
+ }
71
+
72
+ async function ensureToken(slrd: Solard, ref: string) {
73
+ try {
74
+ return slrd.resolveToken(ref);
75
+ } catch {
76
+ return await slrd.addToken(new PublicKey(ref).toBase58());
77
+ }
78
+ }
79
+
80
+ function requireLiveGate(): void {
81
+ const enabled = [
82
+ process.env.SOLARD_ENABLE_LIVE_TRADES,
83
+ process.env.SLRD_ENABLE_LIVE_TRADES,
84
+ ].some((value) => /^(1|true|yes)$/i.test(value?.trim() ?? ""));
85
+ if (!enabled) {
86
+ throw new Error(
87
+ "Live reward transfers require SOLARD_ENABLE_LIVE_TRADES=1 as well as --live.",
88
+ );
89
+ }
90
+ }
91
+
92
+ function allocationsFile(
93
+ path: string,
94
+ ): Array<{ id?: string; recipient: string; amountRaw: bigint }> {
95
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
96
+ const rows = Array.isArray(parsed)
97
+ ? parsed
98
+ : parsed &&
99
+ typeof parsed === "object" &&
100
+ Array.isArray((parsed as any).allocations)
101
+ ? (parsed as any).allocations
102
+ : null;
103
+ if (!rows)
104
+ throw new Error(
105
+ "Transfer file must be a JSON array or { allocations: [...] }",
106
+ );
107
+ return rows.map((row: any, index: number) => {
108
+ if (!row || typeof row !== "object")
109
+ throw new Error(`Transfer allocation ${index + 1} must be an object`);
110
+ const recipient = String(row.recipient ?? row.address ?? "").trim();
111
+ const amountText = String(row.amountRaw ?? row.raw ?? "").trim();
112
+ if (!recipient)
113
+ throw new Error(`Transfer allocation ${index + 1} has no recipient`);
114
+ if (!/^\d+$/.test(amountText) || BigInt(amountText) <= 0n)
115
+ throw new Error(`Transfer allocation ${index + 1} has invalid amountRaw`);
116
+ return {
117
+ id: row.id == null ? undefined : String(row.id),
118
+ recipient: new PublicKey(recipient).toBase58(),
119
+ amountRaw: BigInt(amountText),
120
+ };
121
+ });
122
+ }
123
+
124
+ function rewardSnapshotFile(path: string): FairfunEntitlementSnapshot {
125
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as any;
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) => {
132
+ if (!row || typeof row !== "object")
133
+ throw new Error(
134
+ `Reward snapshot recipient ${index + 1} must be an object`,
135
+ );
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();
144
+ if (!wallet)
145
+ throw new Error(`Reward snapshot recipient ${index + 1} requires wallet`);
146
+ if (!/^\d+$/.test(entitledRaw))
147
+ throw new Error(
148
+ `Reward snapshot recipient ${index + 1} entitledRaw must be a raw integer`,
149
+ );
150
+ return {
151
+ wallet: new PublicKey(wallet).toBase58(),
152
+ entitledRaw,
153
+ };
154
+ });
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");
162
+ const observedAtMs =
163
+ Array.isArray(parsed) || parsed.observedAtMs == null
164
+ ? null
165
+ : Number(parsed.observedAtMs);
166
+ if (
167
+ observedAtMs != null &&
168
+ (!Number.isFinite(observedAtMs) || observedAtMs < 0)
169
+ )
170
+ throw new Error(
171
+ "Reward snapshot observedAtMs must be a non-negative number",
172
+ );
173
+ return { recipients, totalEntitledRaw, observedAtMs };
174
+ }
175
+
176
+ export async function handleAccountingCommand(args: {
177
+ command: string | undefined;
178
+ values: string[];
179
+ flags: AccountingCliFlags;
180
+ slrd: Solard;
181
+ emit: Emit;
182
+ }): Promise<boolean> {
183
+ const { command, values, flags, slrd, emit } = args;
184
+
185
+ if (command === "holders") {
186
+ const ref = values[0];
187
+ if (!ref)
188
+ throw new Error(
189
+ "Usage: slrd holders <token|ca> [--exclude a,b] [--min-raw N] [--json]",
190
+ );
191
+ const token = await ensureToken(slrd, ref);
192
+ const snapshot = await slrd.snapshotHolders(token.mint, {
193
+ excludeOwners: csv(flag(flags, "exclude")),
194
+ minimumRaw: raw(flag(flags, "min-raw"), "--min-raw"),
195
+ });
196
+ if (bool(flags, "json")) {
197
+ emit(json(snapshot) + "\n");
198
+ return true;
199
+ }
200
+ emit(
201
+ `HOLDERS ${snapshot.mint}\n` +
202
+ `slot=${snapshot.slot} accounts=${snapshot.tokenAccounts} holders=${snapshot.holderCount} eligible=${snapshot.eligibleHolderCount}\n` +
203
+ `eligibleRaw=${snapshot.eligibleTotalRaw} excludedRaw=${snapshot.excludedTotalRaw}\n\n` +
204
+ `SHARE % AMOUNT OWNER\n`,
205
+ );
206
+ for (const holder of snapshot.holders) {
207
+ emit(
208
+ `${holder.shareBps.toFixed(4).padStart(8)} ${holder.amountUi.padStart(24)} ${holder.owner}\n`,
209
+ );
210
+ }
211
+ if (snapshot.excluded.length) {
212
+ emit("\nEXCLUDED\n");
213
+ for (const holder of snapshot.excluded)
214
+ emit(
215
+ `${holder.reason.padEnd(16)} ${holder.amountUi.padStart(20)} ${holder.owner}\n`,
216
+ );
217
+ }
218
+ return true;
219
+ }
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
+
274
+ if (command === "events") {
275
+ const ref = values[0];
276
+ if (!ref)
277
+ throw new Error(
278
+ "Usage: slrd events <token|ca> [--all|--swaps|--transfers|--creates|--swaps-only|--transfers-only] [--jsonl]",
279
+ );
280
+ const token = await ensureToken(slrd, ref);
281
+ const controller = new AbortController();
282
+ const stop = () => controller.abort();
283
+ process.once("SIGINT", stop);
284
+ process.once("SIGTERM", stop);
285
+
286
+ const explicit =
287
+ bool(flags, "all") ||
288
+ bool(flags, "swaps") ||
289
+ bool(flags, "transfers") ||
290
+ bool(flags, "creates");
291
+ let swaps = true;
292
+ let transfers = true;
293
+ let creates = false;
294
+ if (bool(flags, "swaps-only")) {
295
+ swaps = true;
296
+ transfers = false;
297
+ creates = false;
298
+ } else if (bool(flags, "transfers-only")) {
299
+ swaps = false;
300
+ transfers = true;
301
+ creates = false;
302
+ } else if (explicit) {
303
+ const all = bool(flags, "all");
304
+ swaps = all || bool(flags, "swaps");
305
+ transfers = all || bool(flags, "transfers");
306
+ creates = all || bool(flags, "creates");
307
+ }
308
+
309
+ const subscription = await slrd.subscribeTokenEvents(token.mint, {
310
+ swaps,
311
+ transfers,
312
+ creates,
313
+ commitment: bool(flags, "finalized") ? "finalized" : "confirmed",
314
+ signal: controller.signal,
315
+ });
316
+ if (!bool(flags, "jsonl")) {
317
+ emit(
318
+ `EVENTS ${subscription.mint}\n` +
319
+ `addresses=${subscription.addresses.join(",")}\n` +
320
+ `transfer-coverage=${subscription.transferCoverage} Ctrl+C to stop\n\n`,
321
+ );
322
+ }
323
+ try {
324
+ for await (const event of subscription) {
325
+ if (bool(flags, "jsonl")) {
326
+ emit(
327
+ JSON.stringify(event, (_, item) =>
328
+ typeof item === "bigint" ? item.toString() : item,
329
+ ) + "\n",
330
+ );
331
+ continue;
332
+ }
333
+ if (event.type === "swap") {
334
+ emit(
335
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} SWAP ${event.venue} ${event.side.toUpperCase()} ` +
336
+ `${event.tokenAmountRaw} tokenRaw quote=${event.quoteAmountRaw ?? "?"} ${event.quoteMint} trader=${event.trader ?? "?"} sig=${event.signature}\n`,
337
+ );
338
+ } else if (event.type === "create") {
339
+ emit(
340
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} CREATE ` +
341
+ `${event.symbol ?? "?"} ${event.name ?? "?"} creator=${event.creator ?? "?"} quote=${event.quoteMint} sig=${event.signature}\n`,
342
+ );
343
+ } else {
344
+ emit(
345
+ `${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} ${event.movement.toUpperCase()} ${event.amountRaw} ` +
346
+ `${event.sourceOwner ?? event.sourceTokenAccount} -> ${event.destinationOwner ?? event.destinationTokenAccount} sig=${event.signature}\n`,
347
+ );
348
+ }
349
+ }
350
+ } finally {
351
+ process.off("SIGINT", stop);
352
+ process.off("SIGTERM", stop);
353
+ await subscription.close();
354
+ }
355
+ return true;
356
+ }
357
+
358
+ if (command === "transfer-many" && values[0] === "status") {
359
+ const id = values[1];
360
+ if (!id)
361
+ throw new Error("Usage: slrd transfer-many status <distribution-id>");
362
+ emit(json(slrd.getTransferManyStatus(id)) + "\n");
363
+ return true;
364
+ }
365
+
366
+ if (command === "transfer-many" && values[0] === "resume") {
367
+ const id = values[1];
368
+ if (!id)
369
+ throw new Error(
370
+ "Usage: slrd transfer-many resume <distribution-id> [--sender rpc]",
371
+ );
372
+ requireLiveGate();
373
+ const state = await slrd.resumeTransferMany(id, {
374
+ via: flag(flags, "sender") ?? "rpc",
375
+ priorityMicroLamports: nonNegativeInteger(
376
+ flag(flags, "priority-micro-lamports"),
377
+ 0,
378
+ ),
379
+ maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
380
+ skipSimulation: bool(flags, "skip-simulation"),
381
+ skipPreflight: bool(flags, "skip-preflight"),
382
+ });
383
+ emit(json(state) + "\n");
384
+ return true;
385
+ }
386
+
387
+ if (command === "transfer-many") {
388
+ const wallet = flag(flags, "wallet");
389
+ const file = flag(flags, "file");
390
+ if (!wallet || !file)
391
+ throw new Error(
392
+ "Usage: slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]",
393
+ );
394
+ const token = flag(flags, "token");
395
+ const asset = bool(flags, "sol") ? "SOL" : token;
396
+ if (!asset || (bool(flags, "sol") && token))
397
+ throw new Error("Supply exactly one of --token <mint> or --sol");
398
+ const allocations = allocationsFile(file);
399
+ const common = {
400
+ wallet,
401
+ asset,
402
+ allocations,
403
+ priorityMicroLamports: nonNegativeInteger(
404
+ flag(flags, "priority-micro-lamports"),
405
+ 0,
406
+ ),
407
+ maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
408
+ };
409
+ const plan = await slrd.planTransferMany(common);
410
+ if (!bool(flags, "live")) {
411
+ emit(
412
+ json({
413
+ live: false,
414
+ id: flag(flags, "id") ?? null,
415
+ payer: plan.payer,
416
+ asset: plan.asset,
417
+ totalRaw: plan.totalRaw,
418
+ allocations: plan.allocationCount,
419
+ batches: plan.batches.map((batch) => ({
420
+ index: batch.index,
421
+ recipients: batch.allocations.length,
422
+ allocationIds: batch.allocations.map((row) => row.id),
423
+ totalRaw: batch.totalRaw,
424
+ serializedSize: batch.estimatedSerializedSize,
425
+ })),
426
+ packetLimit: plan.packetLimit,
427
+ }) + "\n",
428
+ );
429
+ return true;
430
+ }
431
+ const id = flag(flags, "id");
432
+ if (!id) {
433
+ throw new Error(
434
+ "Live transfer-many requires --id <stable-distribution-id>. " +
435
+ "This prevents ambiguous retries from double-paying recipients.",
436
+ );
437
+ }
438
+ if (allocations.some((row) => !row.id?.trim())) {
439
+ throw new Error(
440
+ "Live durable transfer-many requires an explicit stable id on every allocation in the JSON file.",
441
+ );
442
+ }
443
+ requireLiveGate();
444
+ const state = await slrd.sendTransferMany({
445
+ ...common,
446
+ id,
447
+ via: flag(flags, "sender") ?? "rpc",
448
+ skipSimulation: bool(flags, "skip-simulation"),
449
+ skipPreflight: bool(flags, "skip-preflight"),
450
+ });
451
+ emit(json(state) + "\n");
452
+ return true;
453
+ }
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
+
524
+ if (command === "rewards" && values[0] === "status") {
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");
529
+ return true;
530
+ }
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
+
565
+ if (command === "rewards" && values[0] === "distribute") {
566
+ const ref = values[1];
567
+ const wallet = flag(flags, "wallet");
568
+ const snapshotPath = flag(flags, "snapshot");
569
+ if (!ref || !wallet || !snapshotPath)
570
+ throw new Error(
571
+ "Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> --snapshot <snapshot.json> [--reward-mint <mint>] [--reserve-raw N] [--live]",
572
+ );
573
+ const token = await ensureToken(slrd, ref);
574
+ const snapshot = rewardSnapshotFile(snapshotPath);
575
+ const common = {
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
+ })),
583
+ reserveRaw: raw(flag(flags, "reserve-raw"), "--reserve-raw") ?? 0n,
584
+ maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
585
+ };
586
+ if (!bool(flags, "live")) {
587
+ const plan = await slrd.distributions.plan(common);
588
+ emit(
589
+ json({
590
+ live: false,
591
+ id: plan.id,
592
+ sourceWallet: plan.sourceWallet,
593
+ asset: plan.asset,
594
+ totalEntitledRaw: plan.totalEntitledRaw,
595
+ totalConfirmedPaidRaw: plan.totalConfirmedPaidRaw,
596
+ totalOutstandingRaw: plan.totalOutstandingRaw,
597
+ sourceBalanceRaw: plan.sourceBalanceRaw,
598
+ availableRaw: plan.availableRaw,
599
+ reserveRaw: plan.reserveRaw,
600
+ outstanding: plan.outstanding,
601
+ pending: plan.pending,
602
+ nextBatch: plan.nextPayments.length
603
+ ? { payments: plan.nextPayments }
604
+ : null,
605
+ }) + "\n",
606
+ );
607
+ return true;
608
+ }
609
+ requireLiveGate();
610
+ const state = await slrd.distributions.execute({
611
+ ...common,
612
+ via: flag(flags, "sender") ?? "rpc",
613
+ skipSimulation: bool(flags, "skip-simulation"),
614
+ skipPreflight: bool(flags, "skip-preflight"),
615
+ });
616
+ emit(json(state) + "\n");
617
+ return true;
618
+ }
619
+
620
+ return false;
621
+ }
@@ -12,7 +12,7 @@ import {
12
12
  type HistoricalResearchBatchResult,
13
13
  type HistoricalResearchStrategy,
14
14
  type HistoricalResearchToken,
15
- } from "@solard/sdk";
15
+ } from "@solard/core";
16
16
 
17
17
  type Flags = Map<string, string>;
18
18
  type Emit = (value: string) => void;
@@ -9,7 +9,7 @@ import {
9
9
  type TargetWeightPolicy,
10
10
  type TokenAthDipProfitBacktestResult,
11
11
  type TokenTargetWeightBacktestResult,
12
- } from "@solard/sdk";
12
+ } from "@solard/core";
13
13
 
14
14
  type Flags = Map<string, string>;
15
15
  type Emit = (value: string) => void;
@@ -7,7 +7,7 @@ import {
7
7
  sol,
8
8
  type SenderId,
9
9
  type Solard,
10
- } from "@solard/sdk";
10
+ } from "@solard/core";
11
11
 
12
12
  import { resolveTradeTargets, type TradeTargets } from "./trade-targets.ts";
13
13
  import { summarizeTradeRoute, type TradeRouteSummary } from "./trade-result.ts";
@@ -11,7 +11,7 @@ import {
11
11
  type JupiterSwapQuote,
12
12
  type Solard,
13
13
  type TradeAsset,
14
- } from "@solard/sdk";
14
+ } from "@solard/core";
15
15
 
16
16
  export type JupiterSwapCliFlags = ReadonlyMap<string, string>;
17
17
 
@@ -6,7 +6,7 @@ import {
6
6
  resolveTradeRoute,
7
7
  type SenderId,
8
8
  type Solard,
9
- } from "@solard/sdk";
9
+ } from "@solard/core";
10
10
 
11
11
  import { resolveTradeTargets, type TradeTargets } from "./trade-targets.ts";
12
12
  import { summarizeTradeRoute, type TradeRouteSummary } from "./trade-result.ts";