@solard/cli 0.2.15 → 0.2.17
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 +4 -3
- package/src/accounting-commands.ts +621 -0
- package/src/backtest-batch-command.ts +1 -1
- package/src/backtest-command.ts +1 -1
- package/src/commands/buy.ts +1 -1
- package/src/commands/jupiter-swap.ts +1 -1
- package/src/commands/sell.ts +1 -1
- package/src/commands/trade-result.ts +5 -1
- package/src/commands/trade-targets.ts +1 -1
- package/src/commands/trade-venue.ts +1 -1
- package/src/commands/transfer.ts +1 -1
- package/src/fairfun-commands.ts +245 -65
- package/src/index.ts +198 -52
- package/src/meteora-commands.ts +1 -1
- package/src/pump/external-deployment-cli.ts +1 -1
- package/src/pump/spam-buy-cli.ts +1 -1
- package/src/pump/token-launch-cli.ts +3 -3
- package/src/pump/vamp-cli.ts +1 -1
- package/src/raydium-commands.ts +1 -1
- package/src/refs.ts +1 -1
- package/src/token-history-command.ts +11 -33
- package/src/vault-onboarding.ts +6 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solard/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
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/
|
|
21
|
+
"@solard/core": "0.2.17",
|
|
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
|
+
}
|
package/src/backtest-command.ts
CHANGED
package/src/commands/buy.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
sol,
|
|
8
8
|
type SenderId,
|
|
9
9
|
type Solard,
|
|
10
|
-
} from "@solard/
|
|
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";
|
package/src/commands/sell.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resolveTradeRoute,
|
|
7
7
|
type SenderId,
|
|
8
8
|
type Solard,
|
|
9
|
-
} from "@solard/
|
|
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";
|