@solard/cli 0.2.13 → 0.2.14
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 +2 -2
- package/src/backtest-batch-command.ts +262 -0
- package/src/backtest-command.ts +533 -0
- package/src/commands/buy.ts +207 -0
- package/src/commands/jupiter-swap.test.ts +78 -0
- package/src/commands/jupiter-swap.ts +269 -0
- package/src/commands/sell.ts +239 -0
- package/src/commands/trade-result.ts +19 -0
- package/src/commands/trade-targets.ts +43 -0
- package/src/commands/trade-venue.test.ts +32 -0
- package/src/commands/trade-venue.ts +19 -0
- package/src/commands/transfer.ts +200 -0
- package/src/fairfun-commands.ts +381 -0
- package/src/index.ts +386 -290
- package/src/meteora-commands.ts +148 -6
- package/src/pump/external-deployment-cli.ts +136 -0
- package/src/pump/token-launch-cli.ts +82 -6
- package/src/raydium-commands.ts +398 -0
- package/src/refs.ts +56 -0
- package/src/token-history-command.ts +559 -0
- package/src/vault-onboarding.ts +214 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { PublicKey } from "@solana/web3.js";
|
|
3
|
+
import type { Solard } from "@solard/sdk";
|
|
4
|
+
|
|
5
|
+
export type FairfunCliFlags = Map<string, string>;
|
|
6
|
+
type Emit = (value: string) => void;
|
|
7
|
+
|
|
8
|
+
function json(value: unknown): string {
|
|
9
|
+
return JSON.stringify(
|
|
10
|
+
value,
|
|
11
|
+
(_, item) => (typeof item === "bigint" ? item.toString() : item),
|
|
12
|
+
2,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function flag(flags: FairfunCliFlags, key: string): string | undefined {
|
|
17
|
+
const value = flags.get(key);
|
|
18
|
+
return value && value !== "true" ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function bool(flags: FairfunCliFlags, key: string): boolean {
|
|
22
|
+
return flags.has(key) && flags.get(key) !== "false";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function csv(value: string | undefined): string[] {
|
|
26
|
+
return (value ?? "")
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((item) => item.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function positiveInteger(
|
|
33
|
+
value: string | undefined,
|
|
34
|
+
fallback?: number,
|
|
35
|
+
): number | undefined {
|
|
36
|
+
if (value == null) return fallback;
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
39
|
+
throw new Error(`Expected a positive integer, received ${value}`);
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nonNegativeInteger(
|
|
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 non-negative integer, received ${value}`);
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function raw(value: string | undefined, name: string): bigint | undefined {
|
|
55
|
+
if (value == null) return undefined;
|
|
56
|
+
if (!/^\d+$/.test(value))
|
|
57
|
+
throw new Error(`${name} must be a raw integer amount`);
|
|
58
|
+
return BigInt(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ensureToken(slrd: Solard, ref: string) {
|
|
62
|
+
try {
|
|
63
|
+
return slrd.resolveToken(ref);
|
|
64
|
+
} catch {
|
|
65
|
+
return await slrd.addToken(new PublicKey(ref).toBase58());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requireLiveGate(): void {
|
|
70
|
+
const enabled = [
|
|
71
|
+
process.env.SOLARD_ENABLE_LIVE_TRADES,
|
|
72
|
+
process.env.SLRD_ENABLE_LIVE_TRADES,
|
|
73
|
+
].some((value) => /^(1|true|yes)$/i.test(value?.trim() ?? ""));
|
|
74
|
+
if (!enabled) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Live Fairfun/reward transfers require SOLARD_ENABLE_LIVE_TRADES=1 as well as --live.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function allocationsFile(
|
|
82
|
+
path: string,
|
|
83
|
+
): Array<{ id?: string; recipient: string; amountRaw: bigint }> {
|
|
84
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
85
|
+
const rows = Array.isArray(parsed)
|
|
86
|
+
? parsed
|
|
87
|
+
: parsed &&
|
|
88
|
+
typeof parsed === "object" &&
|
|
89
|
+
Array.isArray((parsed as any).allocations)
|
|
90
|
+
? (parsed as any).allocations
|
|
91
|
+
: null;
|
|
92
|
+
if (!rows)
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Transfer file must be a JSON array or { allocations: [...] }",
|
|
95
|
+
);
|
|
96
|
+
return rows.map((row: any, index: number) => {
|
|
97
|
+
if (!row || typeof row !== "object")
|
|
98
|
+
throw new Error(`Transfer allocation ${index + 1} must be an object`);
|
|
99
|
+
const recipient = String(row.recipient ?? row.address ?? "").trim();
|
|
100
|
+
const amountText = String(row.amountRaw ?? row.raw ?? "").trim();
|
|
101
|
+
if (!recipient)
|
|
102
|
+
throw new Error(`Transfer allocation ${index + 1} has no recipient`);
|
|
103
|
+
if (!/^\d+$/.test(amountText) || BigInt(amountText) <= 0n)
|
|
104
|
+
throw new Error(`Transfer allocation ${index + 1} has invalid amountRaw`);
|
|
105
|
+
return {
|
|
106
|
+
id: row.id == null ? undefined : String(row.id),
|
|
107
|
+
recipient: new PublicKey(recipient).toBase58(),
|
|
108
|
+
amountRaw: BigInt(amountText),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function handleFairfunCommand(args: {
|
|
114
|
+
command: string | undefined;
|
|
115
|
+
values: string[];
|
|
116
|
+
flags: FairfunCliFlags;
|
|
117
|
+
slrd: Solard;
|
|
118
|
+
emit: Emit;
|
|
119
|
+
}): Promise<boolean> {
|
|
120
|
+
const { command, values, flags, slrd, emit } = args;
|
|
121
|
+
|
|
122
|
+
if (command === "holders") {
|
|
123
|
+
const ref = values[0];
|
|
124
|
+
if (!ref)
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Usage: slrd holders <token|ca> [--exclude a,b] [--min-raw N] [--json]",
|
|
127
|
+
);
|
|
128
|
+
const token = await ensureToken(slrd, ref);
|
|
129
|
+
const snapshot = await slrd.snapshotHolders(token.mint, {
|
|
130
|
+
excludeOwners: csv(flag(flags, "exclude")),
|
|
131
|
+
minimumRaw: raw(flag(flags, "min-raw"), "--min-raw"),
|
|
132
|
+
});
|
|
133
|
+
if (bool(flags, "json")) {
|
|
134
|
+
emit(json(snapshot) + "\n");
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
emit(
|
|
138
|
+
`HOLDERS ${snapshot.mint}\n` +
|
|
139
|
+
`slot=${snapshot.slot} accounts=${snapshot.tokenAccounts} holders=${snapshot.holderCount} eligible=${snapshot.eligibleHolderCount}\n` +
|
|
140
|
+
`eligibleRaw=${snapshot.eligibleTotalRaw} excludedRaw=${snapshot.excludedTotalRaw}\n\n` +
|
|
141
|
+
`SHARE % AMOUNT OWNER\n`,
|
|
142
|
+
);
|
|
143
|
+
for (const holder of snapshot.holders) {
|
|
144
|
+
emit(
|
|
145
|
+
`${holder.shareBps.toFixed(4).padStart(8)} ${holder.amountUi.padStart(24)} ${holder.owner}\n`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (snapshot.excluded.length) {
|
|
149
|
+
emit("\nEXCLUDED\n");
|
|
150
|
+
for (const holder of snapshot.excluded)
|
|
151
|
+
emit(
|
|
152
|
+
`${holder.reason.padEnd(16)} ${holder.amountUi.padStart(20)} ${holder.owner}\n`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (command === "events") {
|
|
159
|
+
const ref = values[0];
|
|
160
|
+
if (!ref)
|
|
161
|
+
throw new Error(
|
|
162
|
+
"Usage: slrd events <token|ca> [--swaps-only|--transfers-only] [--jsonl]",
|
|
163
|
+
);
|
|
164
|
+
const token = await ensureToken(slrd, ref);
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const stop = () => controller.abort();
|
|
167
|
+
process.once("SIGINT", stop);
|
|
168
|
+
process.once("SIGTERM", stop);
|
|
169
|
+
const swaps = !bool(flags, "transfers-only");
|
|
170
|
+
const transfers = !bool(flags, "swaps-only");
|
|
171
|
+
const subscription = await slrd.subscribeTokenEvents(token.mint, {
|
|
172
|
+
swaps,
|
|
173
|
+
transfers,
|
|
174
|
+
commitment: bool(flags, "finalized") ? "finalized" : "confirmed",
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
});
|
|
177
|
+
if (!bool(flags, "jsonl")) {
|
|
178
|
+
emit(
|
|
179
|
+
`EVENTS ${subscription.mint}\n` +
|
|
180
|
+
`addresses=${subscription.addresses.join(",")}\n` +
|
|
181
|
+
`transfer-coverage=${subscription.transferCoverage} Ctrl+C to stop\n\n`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
for await (const event of subscription) {
|
|
186
|
+
if (bool(flags, "jsonl")) {
|
|
187
|
+
emit(
|
|
188
|
+
JSON.stringify(event, (_, item) =>
|
|
189
|
+
typeof item === "bigint" ? item.toString() : item,
|
|
190
|
+
) + "\n",
|
|
191
|
+
);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (event.type === "swap") {
|
|
195
|
+
emit(
|
|
196
|
+
`${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} SWAP ${event.venue} ${event.side.toUpperCase()} ` +
|
|
197
|
+
`${event.tokenAmountRaw} tokenRaw trader=${event.trader ?? "?"} sig=${event.signature}\n`,
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
emit(
|
|
201
|
+
`${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} TRANSFER ${event.amountRaw} ` +
|
|
202
|
+
`${event.sourceOwner ?? event.sourceTokenAccount} -> ${event.destinationOwner ?? event.destinationTokenAccount} sig=${event.signature}\n`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
process.off("SIGINT", stop);
|
|
208
|
+
process.off("SIGTERM", stop);
|
|
209
|
+
await subscription.close();
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (command === "transfer-many" && values[0] === "status") {
|
|
215
|
+
const id = values[1];
|
|
216
|
+
if (!id)
|
|
217
|
+
throw new Error("Usage: slrd transfer-many status <distribution-id>");
|
|
218
|
+
emit(json(slrd.getTransferManyStatus(id)) + "\n");
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (command === "transfer-many" && values[0] === "resume") {
|
|
223
|
+
const id = values[1];
|
|
224
|
+
if (!id)
|
|
225
|
+
throw new Error(
|
|
226
|
+
"Usage: slrd transfer-many resume <distribution-id> [--sender rpc]",
|
|
227
|
+
);
|
|
228
|
+
requireLiveGate();
|
|
229
|
+
const state = await slrd.resumeTransferMany(id, {
|
|
230
|
+
via: flag(flags, "sender") ?? "rpc",
|
|
231
|
+
priorityMicroLamports: nonNegativeInteger(
|
|
232
|
+
flag(flags, "priority-micro-lamports"),
|
|
233
|
+
0,
|
|
234
|
+
),
|
|
235
|
+
maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
|
|
236
|
+
skipSimulation: bool(flags, "skip-simulation"),
|
|
237
|
+
skipPreflight: bool(flags, "skip-preflight"),
|
|
238
|
+
});
|
|
239
|
+
emit(json(state) + "\n");
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (command === "transfer-many") {
|
|
244
|
+
const wallet = flag(flags, "wallet");
|
|
245
|
+
const file = flag(flags, "file");
|
|
246
|
+
if (!wallet || !file)
|
|
247
|
+
throw new Error(
|
|
248
|
+
"Usage: slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]",
|
|
249
|
+
);
|
|
250
|
+
const token = flag(flags, "token");
|
|
251
|
+
const asset = bool(flags, "sol") ? "SOL" : token;
|
|
252
|
+
if (!asset || (bool(flags, "sol") && token))
|
|
253
|
+
throw new Error("Supply exactly one of --token <mint> or --sol");
|
|
254
|
+
const allocations = allocationsFile(file);
|
|
255
|
+
const common = {
|
|
256
|
+
wallet,
|
|
257
|
+
asset,
|
|
258
|
+
allocations,
|
|
259
|
+
priorityMicroLamports: nonNegativeInteger(
|
|
260
|
+
flag(flags, "priority-micro-lamports"),
|
|
261
|
+
0,
|
|
262
|
+
),
|
|
263
|
+
maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
|
|
264
|
+
};
|
|
265
|
+
const plan = await slrd.planTransferMany(common);
|
|
266
|
+
if (!bool(flags, "live")) {
|
|
267
|
+
emit(
|
|
268
|
+
json({
|
|
269
|
+
live: false,
|
|
270
|
+
id: flag(flags, "id") ?? null,
|
|
271
|
+
payer: plan.payer,
|
|
272
|
+
asset: plan.asset,
|
|
273
|
+
totalRaw: plan.totalRaw,
|
|
274
|
+
allocations: plan.allocationCount,
|
|
275
|
+
batches: plan.batches.map((batch) => ({
|
|
276
|
+
index: batch.index,
|
|
277
|
+
recipients: batch.allocations.length,
|
|
278
|
+
allocationIds: batch.allocations.map((row) => row.id),
|
|
279
|
+
totalRaw: batch.totalRaw,
|
|
280
|
+
serializedSize: batch.estimatedSerializedSize,
|
|
281
|
+
})),
|
|
282
|
+
packetLimit: plan.packetLimit,
|
|
283
|
+
}) + "\n",
|
|
284
|
+
);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
const id = flag(flags, "id");
|
|
288
|
+
if (!id) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"Live transfer-many requires --id <stable-distribution-id>. " +
|
|
291
|
+
"This prevents ambiguous retries from double-paying recipients.",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (allocations.some((row) => !row.id?.trim())) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
"Live durable transfer-many requires an explicit stable id on every allocation in the JSON file.",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
requireLiveGate();
|
|
300
|
+
const state = await slrd.sendTransferMany({
|
|
301
|
+
...common,
|
|
302
|
+
id,
|
|
303
|
+
via: flag(flags, "sender") ?? "rpc",
|
|
304
|
+
skipSimulation: bool(flags, "skip-simulation"),
|
|
305
|
+
skipPreflight: bool(flags, "skip-preflight"),
|
|
306
|
+
});
|
|
307
|
+
emit(json(state) + "\n");
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (command === "rewards" && values[0] === "status") {
|
|
312
|
+
const id = values[1];
|
|
313
|
+
if (!id) throw new Error("Usage: slrd rewards status <distribution-id>");
|
|
314
|
+
const { getHolderRewardDistributionState } = await import("@solard/sdk");
|
|
315
|
+
emit(json(getHolderRewardDistributionState(slrd, id)) + "\n");
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (command === "rewards" && values[0] === "distribute") {
|
|
320
|
+
const ref = values[1];
|
|
321
|
+
const wallet = flag(flags, "wallet");
|
|
322
|
+
if (!ref || !wallet)
|
|
323
|
+
throw new Error(
|
|
324
|
+
"Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> [--claim-first | --amount-raw N] [--reward-mint <mint>] [--id <epoch>] [--live]",
|
|
325
|
+
);
|
|
326
|
+
const token = await ensureToken(slrd, ref);
|
|
327
|
+
const common = {
|
|
328
|
+
token: token.mint,
|
|
329
|
+
wallet,
|
|
330
|
+
rewardMint: flag(flags, "reward-mint"),
|
|
331
|
+
amountRaw: raw(flag(flags, "amount-raw"), "--amount-raw"),
|
|
332
|
+
claimFirst: bool(flags, "claim-first"),
|
|
333
|
+
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
|
+
maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
|
|
337
|
+
};
|
|
338
|
+
if (!bool(flags, "live")) {
|
|
339
|
+
const plan = await slrd.planHolderRewards(common);
|
|
340
|
+
emit(
|
|
341
|
+
json({
|
|
342
|
+
live: false,
|
|
343
|
+
id: plan.id,
|
|
344
|
+
tokenMint: plan.tokenMint,
|
|
345
|
+
sourceWallet: plan.sourceWallet,
|
|
346
|
+
claimFirst: plan.claimFirst,
|
|
347
|
+
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
|
+
})),
|
|
359
|
+
}) + "\n",
|
|
360
|
+
);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
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
|
+
requireLiveGate();
|
|
369
|
+
const state = await slrd.distributeHolderRewards({
|
|
370
|
+
...common,
|
|
371
|
+
id,
|
|
372
|
+
via: flag(flags, "sender") ?? "rpc",
|
|
373
|
+
skipSimulation: bool(flags, "skip-simulation"),
|
|
374
|
+
skipPreflight: bool(flags, "skip-preflight"),
|
|
375
|
+
});
|
|
376
|
+
emit(json(state) + "\n");
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
}
|