@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 +2 -2
- package/src/fairfun-commands.ts +85 -25
- package/src/index.ts +2 -1
- package/src/vault-onboarding.ts +48 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solard/cli",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
21
|
+
"@solard/sdk": "0.2.15",
|
|
22
22
|
"bs58": "^6.0.0",
|
|
23
23
|
"@solana/web3.js": "^1.98.4"
|
|
24
24
|
},
|
package/src/fairfun-commands.ts
CHANGED
|
@@ -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
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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>
|
|
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
|
package/src/vault-onboarding.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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> {
|