@solard/cli 0.2.14 → 0.2.16
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 +117 -29
- package/src/index.ts +57 -9
- package/src/vault-onboarding.ts +32 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solard/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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.16",
|
|
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[];
|
|
@@ -159,18 +216,41 @@ export async function handleFairfunCommand(args: {
|
|
|
159
216
|
const ref = values[0];
|
|
160
217
|
if (!ref)
|
|
161
218
|
throw new Error(
|
|
162
|
-
"Usage: slrd events <token|ca> [--swaps-only|--transfers-only] [--jsonl]",
|
|
219
|
+
"Usage: slrd events <token|ca> [--all|--swaps|--transfers|--creates|--swaps-only|--transfers-only] [--jsonl]",
|
|
163
220
|
);
|
|
164
221
|
const token = await ensureToken(slrd, ref);
|
|
165
222
|
const controller = new AbortController();
|
|
166
223
|
const stop = () => controller.abort();
|
|
167
224
|
process.once("SIGINT", stop);
|
|
168
225
|
process.once("SIGTERM", stop);
|
|
169
|
-
|
|
170
|
-
const
|
|
226
|
+
|
|
227
|
+
const explicit =
|
|
228
|
+
bool(flags, "all") ||
|
|
229
|
+
bool(flags, "swaps") ||
|
|
230
|
+
bool(flags, "transfers") ||
|
|
231
|
+
bool(flags, "creates");
|
|
232
|
+
let swaps = true;
|
|
233
|
+
let transfers = true;
|
|
234
|
+
let creates = false;
|
|
235
|
+
if (bool(flags, "swaps-only")) {
|
|
236
|
+
swaps = true;
|
|
237
|
+
transfers = false;
|
|
238
|
+
creates = false;
|
|
239
|
+
} else if (bool(flags, "transfers-only")) {
|
|
240
|
+
swaps = false;
|
|
241
|
+
transfers = true;
|
|
242
|
+
creates = false;
|
|
243
|
+
} else if (explicit) {
|
|
244
|
+
const all = bool(flags, "all");
|
|
245
|
+
swaps = all || bool(flags, "swaps");
|
|
246
|
+
transfers = all || bool(flags, "transfers");
|
|
247
|
+
creates = all || bool(flags, "creates");
|
|
248
|
+
}
|
|
249
|
+
|
|
171
250
|
const subscription = await slrd.subscribeTokenEvents(token.mint, {
|
|
172
251
|
swaps,
|
|
173
252
|
transfers,
|
|
253
|
+
creates,
|
|
174
254
|
commitment: bool(flags, "finalized") ? "finalized" : "confirmed",
|
|
175
255
|
signal: controller.signal,
|
|
176
256
|
});
|
|
@@ -194,7 +274,12 @@ export async function handleFairfunCommand(args: {
|
|
|
194
274
|
if (event.type === "swap") {
|
|
195
275
|
emit(
|
|
196
276
|
`${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} SWAP ${event.venue} ${event.side.toUpperCase()} ` +
|
|
197
|
-
`${event.tokenAmountRaw} tokenRaw trader=${event.trader ?? "?"} sig=${event.signature}\n`,
|
|
277
|
+
`${event.tokenAmountRaw} tokenRaw quote=${event.quoteAmountRaw ?? "?"} ${event.quoteMint} trader=${event.trader ?? "?"} sig=${event.signature}\n`,
|
|
278
|
+
);
|
|
279
|
+
} else if (event.type === "create") {
|
|
280
|
+
emit(
|
|
281
|
+
`${new Date(event.blockTimeMs ?? event.observedAtMs).toISOString()} CREATE ` +
|
|
282
|
+
`${event.symbol ?? "?"} ${event.name ?? "?"} creator=${event.creator ?? "?"} quote=${event.quoteMint} sig=${event.signature}\n`,
|
|
198
283
|
);
|
|
199
284
|
} else {
|
|
200
285
|
emit(
|
|
@@ -319,20 +404,21 @@ export async function handleFairfunCommand(args: {
|
|
|
319
404
|
if (command === "rewards" && values[0] === "distribute") {
|
|
320
405
|
const ref = values[1];
|
|
321
406
|
const wallet = flag(flags, "wallet");
|
|
322
|
-
|
|
407
|
+
const id = flag(flags, "id");
|
|
408
|
+
const snapshotPath = flag(flags, "snapshot");
|
|
409
|
+
if (!ref || !wallet || !id || !snapshotPath)
|
|
323
410
|
throw new Error(
|
|
324
|
-
"Usage: slrd rewards distribute <token|ca> --wallet <beneficiary>
|
|
411
|
+
"Usage: slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--reserve-raw N] [--live]",
|
|
325
412
|
);
|
|
326
413
|
const token = await ensureToken(slrd, ref);
|
|
414
|
+
const snapshot = rewardSnapshotFile(snapshotPath);
|
|
327
415
|
const common = {
|
|
416
|
+
id,
|
|
328
417
|
token: token.mint,
|
|
329
418
|
wallet,
|
|
330
419
|
rewardMint: flag(flags, "reward-mint"),
|
|
331
|
-
|
|
332
|
-
claimFirst: bool(flags, "claim-first"),
|
|
420
|
+
snapshot,
|
|
333
421
|
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
422
|
maxRecipientsPerTransaction: positiveInteger(flag(flags, "max-per-tx")),
|
|
337
423
|
};
|
|
338
424
|
if (!bool(flags, "live")) {
|
|
@@ -340,35 +426,37 @@ export async function handleFairfunCommand(args: {
|
|
|
340
426
|
emit(
|
|
341
427
|
json({
|
|
342
428
|
live: false,
|
|
429
|
+
version: plan.version,
|
|
343
430
|
id: plan.id,
|
|
431
|
+
snapshotId: plan.snapshotId,
|
|
344
432
|
tokenMint: plan.tokenMint,
|
|
345
433
|
sourceWallet: plan.sourceWallet,
|
|
346
|
-
claimFirst: plan.claimFirst,
|
|
347
434
|
rewardAsset: plan.rewardAsset,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
435
|
+
totalClaimedRaw: plan.totalClaimedRaw,
|
|
436
|
+
totalEntitledRaw: plan.totalEntitledRaw,
|
|
437
|
+
totalConfirmedPaidRaw: plan.totalConfirmedPaidRaw,
|
|
438
|
+
totalOutstandingRaw: plan.totalOutstandingRaw,
|
|
439
|
+
sourceBalanceRaw: plan.sourceBalanceRaw,
|
|
440
|
+
availableRaw: plan.availableRaw,
|
|
441
|
+
reserveRaw: plan.reserveRaw,
|
|
442
|
+
outstanding: plan.outstanding,
|
|
443
|
+
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
|
+
}
|
|
452
|
+
: null,
|
|
359
453
|
}) + "\n",
|
|
360
454
|
);
|
|
361
455
|
return true;
|
|
362
456
|
}
|
|
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
457
|
requireLiveGate();
|
|
369
458
|
const state = await slrd.distributeHolderRewards({
|
|
370
459
|
...common,
|
|
371
|
-
id,
|
|
372
460
|
via: flag(flags, "sender") ?? "rpc",
|
|
373
461
|
skipSimulation: bool(flags, "skip-simulation"),
|
|
374
462
|
skipPreflight: bool(flags, "skip-preflight"),
|
package/src/index.ts
CHANGED
|
@@ -322,7 +322,7 @@ async function storedWalletCount(): Promise<number> {
|
|
|
322
322
|
const { createTraderSolard } = await import("@solard/sdk");
|
|
323
323
|
const probe = createTraderSolard();
|
|
324
324
|
try {
|
|
325
|
-
return probe.wallets.
|
|
325
|
+
return (probe.db.wallets.select().all() as unknown[]).length;
|
|
326
326
|
} finally {
|
|
327
327
|
probe.close();
|
|
328
328
|
}
|
|
@@ -333,7 +333,7 @@ function help(): string {
|
|
|
333
333
|
|
|
334
334
|
Wallets and tokens
|
|
335
335
|
slrd setup [--status] Check ephemeral wallet-password mode; no password is persisted
|
|
336
|
-
slrd wallet create [name]
|
|
336
|
+
slrd wallet create [name] [--vanity <suffix>] Generate and persist an encrypted Solana wallet
|
|
337
337
|
slrd import <private_key> [name]
|
|
338
338
|
cat key.json | slrd import --stdin [name]
|
|
339
339
|
|
|
@@ -356,7 +356,7 @@ External contacts (public addresses only; never signing wallets or group members
|
|
|
356
356
|
slrd token analyze <ca> [--top N] [--json]
|
|
357
357
|
slrd tokens
|
|
358
358
|
slrd holders <token|ca> [--exclude <a,b>] [--min-raw N] [--json] Complete on-chain holder snapshot
|
|
359
|
-
slrd events <token|ca> [--swaps
|
|
359
|
+
slrd events <token|ca> [--all|--swaps|--transfers|--creates] [--jsonl] Live typed Pump/PumpSwap + transfer/create stream
|
|
360
360
|
|
|
361
361
|
Vanity mints
|
|
362
362
|
slrd vanity --suffix pump --out .\\mint.json [--count <n>]
|
|
@@ -365,6 +365,9 @@ Vanity mints
|
|
|
365
365
|
slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
|
|
366
366
|
launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
|
|
367
367
|
|
|
368
|
+
Pump pairs
|
|
369
|
+
slrd pump pairs [--json] Read Pump-supported quote mints from the on-chain Global account
|
|
370
|
+
|
|
368
371
|
Metadata and launching
|
|
369
372
|
slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--pair SOL|USDC|<custom-quote-mint>] [--beneficiary <wallet|address>] [--alias <name>] [--live] [--skip-simulation]
|
|
370
373
|
[--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
|
|
@@ -421,7 +424,8 @@ Trading
|
|
|
421
424
|
slrd transfer-many --wallet <wallet> (--token <mint>|--sol) --file <allocations.json> [--id <stable-id> --live]
|
|
422
425
|
slrd transfer-many status <stable-id>
|
|
423
426
|
slrd transfer-many resume <stable-id> [--sender rpc|helius]
|
|
424
|
-
slrd rewards distribute <token|ca> --wallet <beneficiary>
|
|
427
|
+
slrd rewards distribute <token|ca> --wallet <beneficiary> --id <ledger-id> --snapshot <snapshot.json> [--reward-mint <mint>] [--live]
|
|
428
|
+
Snapshot contains cumulative totalClaimedRaw + recipients[].entitledRaw; Solard tracks confirmed payouts
|
|
425
429
|
slrd rewards status <distribution-id>
|
|
426
430
|
slrd rewards inspect <token|ca> Show claim source, quote asset, estimated accrued creator reward, and payout address
|
|
427
431
|
slrd rewards claim <token|ca> --wallet <fee-payer> Claim creator rewards; on-chain beneficiary receives the reward
|
|
@@ -558,7 +562,7 @@ async function main() {
|
|
|
558
562
|
return;
|
|
559
563
|
}
|
|
560
564
|
|
|
561
|
-
if (needsVault
|
|
565
|
+
if (needsVault) {
|
|
562
566
|
const allowCreate =
|
|
563
567
|
(command === "wallet" && values[0] === "create") ||
|
|
564
568
|
command === "import" ||
|
|
@@ -859,6 +863,20 @@ async function main() {
|
|
|
859
863
|
return;
|
|
860
864
|
}
|
|
861
865
|
|
|
866
|
+
if (command === "pump" && values[0] === "pairs") {
|
|
867
|
+
const pairs = await slrd.pump.getSupportedPairs();
|
|
868
|
+
if (flags.has("json")) {
|
|
869
|
+
emit(json(pairs) + "\n");
|
|
870
|
+
} else {
|
|
871
|
+
for (const pair of pairs) {
|
|
872
|
+
emit(
|
|
873
|
+
`${(pair.symbol ?? "?").padEnd(12)} ${(pair.name ?? "?").padEnd(28)} ${pair.mint} decimals=${pair.decimals}\n`,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
862
880
|
if (command === "swap") {
|
|
863
881
|
const { runJupiterSwapCommand } =
|
|
864
882
|
await import("./commands/jupiter-swap.ts");
|
|
@@ -928,16 +946,46 @@ async function main() {
|
|
|
928
946
|
|
|
929
947
|
if (command === "wallet" && values[0] === "create") {
|
|
930
948
|
const firstWallet = slrd.wallets.list().length === 0;
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
949
|
+
const name = values[1] ?? (firstWallet ? "main" : undefined);
|
|
950
|
+
const vanity = flags.has("vanity") ? need(flags, "vanity") : null;
|
|
951
|
+
const vanityResult = vanity
|
|
952
|
+
? await slrd.createVanityWallet(name, {
|
|
953
|
+
suffix: vanity,
|
|
954
|
+
workers: int(flags, "workers"),
|
|
955
|
+
maxAttempts: int(flags, "max-attempts"),
|
|
956
|
+
timeoutMs: int(flags, "timeout-ms", 0),
|
|
957
|
+
reportEvery: int(flags, "report-every", 1_000_000),
|
|
958
|
+
onProgress: process.stderr.isTTY
|
|
959
|
+
? (progress) =>
|
|
960
|
+
process.stderr.write(
|
|
961
|
+
`${OWL} wallet vanity ${progress.suffix}: ${progress.attempts} attempts, ${progress.ratePerSecond}/s\r`,
|
|
962
|
+
)
|
|
963
|
+
: undefined,
|
|
964
|
+
})
|
|
965
|
+
: null;
|
|
966
|
+
if (vanityResult && process.stderr.isTTY) process.stderr.write("\n");
|
|
967
|
+
const wallet = vanityResult?.wallet ?? slrd.createWallet(name);
|
|
968
|
+
const output = vanityResult
|
|
969
|
+
? {
|
|
970
|
+
...wallet,
|
|
971
|
+
vanity: {
|
|
972
|
+
suffix: vanityResult.suffix,
|
|
973
|
+
attempts: vanityResult.attempts,
|
|
974
|
+
elapsedMs: vanityResult.elapsedMs,
|
|
975
|
+
ratePerSecond: vanityResult.ratePerSecond,
|
|
976
|
+
},
|
|
977
|
+
}
|
|
978
|
+
: wallet;
|
|
934
979
|
if (flags.has("json")) {
|
|
935
|
-
emit(json(
|
|
980
|
+
emit(json(output) + "\n");
|
|
936
981
|
} else if (process.stdout.isTTY) {
|
|
937
982
|
emit(
|
|
938
983
|
`\n╭─ ${OWL} wallet ready ──────────────────────────────────╮\n` +
|
|
939
984
|
` name @${wallet.name}\n` +
|
|
940
985
|
` address ${wallet.address}\n` +
|
|
986
|
+
(vanityResult
|
|
987
|
+
? ` vanity suffix=${vanityResult.suffix} attempts=${vanityResult.attempts}\n`
|
|
988
|
+
: "") +
|
|
941
989
|
` storage encrypted in Solard's local wallet database\n` +
|
|
942
990
|
` password NOT saved; you will enter it again next invocation\n` +
|
|
943
991
|
`╰──────────────────────────────────────────────────────╯\n` +
|
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,25 @@ 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
|
|
110
|
-
if (!
|
|
111
|
-
// Decrypt one persisted wallet. All persisted wallets use the same process-level
|
|
112
|
-
// SLRD_MASTER_KEY contract, so this is enough to catch a mistyped password before
|
|
113
|
-
// the requested command begins doing work.
|
|
114
|
-
probe.wallets.signer(wallets[0]!.address);
|
|
115
|
-
} catch {
|
|
121
|
+
const integrity = probe.wallets.integrity();
|
|
122
|
+
if (!integrity.failures.length) return;
|
|
116
123
|
clearMasterKey();
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
if (integrity.decrypted === 0) {
|
|
125
|
+
throw new VaultOnboardingError(
|
|
126
|
+
"That wallet password could not decrypt the stored wallet database.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const affected = integrity.failures
|
|
130
|
+
.map((wallet) => `@${wallet.name} ${wallet.address}`)
|
|
131
|
+
.join(", ");
|
|
132
|
+
throw new VaultIntegrityError(
|
|
133
|
+
`Wallet database integrity check failed: one master password must decrypt every stored signing wallet. ` +
|
|
134
|
+
`Decrypted ${integrity.decrypted}/${integrity.total}; undecryptable or mismatched: ${affected}`,
|
|
119
135
|
);
|
|
120
136
|
} finally {
|
|
121
137
|
probe.close();
|
|
@@ -126,9 +142,10 @@ async function unlockExistingWallets(): Promise<string> {
|
|
|
126
142
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
127
143
|
const password = await promptHidden("Wallet password: ");
|
|
128
144
|
try {
|
|
129
|
-
await
|
|
145
|
+
await validateAllStoredWallets(password);
|
|
130
146
|
return password;
|
|
131
147
|
} catch (error) {
|
|
148
|
+
if (error instanceof VaultIntegrityError) throw error;
|
|
132
149
|
if (attempt === 3) throw error;
|
|
133
150
|
process.stdout.write("Incorrect password. Try again.\n\n");
|
|
134
151
|
}
|
|
@@ -136,18 +153,13 @@ async function unlockExistingWallets(): Promise<string> {
|
|
|
136
153
|
throw new VaultOnboardingError("Unable to unlock the stored wallets.");
|
|
137
154
|
}
|
|
138
155
|
|
|
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
156
|
export async function ensureCliVaultReady(options: {
|
|
146
157
|
existingWalletCount: number;
|
|
147
158
|
allowCreate?: boolean;
|
|
148
159
|
}): Promise<VaultReadyResult> {
|
|
149
|
-
const existing =
|
|
160
|
+
const existing = environmentMasterKey();
|
|
150
161
|
if (existing) {
|
|
162
|
+
await validateAllStoredWallets(existing);
|
|
151
163
|
return {
|
|
152
164
|
source: "env",
|
|
153
165
|
created: false,
|
|
@@ -197,13 +209,12 @@ export function cliVaultStatus(): {
|
|
|
197
209
|
} {
|
|
198
210
|
return {
|
|
199
211
|
mode: "ephemeral-password",
|
|
200
|
-
environmentOverride:
|
|
212
|
+
environmentOverride: environmentMasterKey() != null,
|
|
201
213
|
passwordPersisted: false,
|
|
202
214
|
remembered: false,
|
|
203
215
|
};
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
/** Optional helper for tests/tools that want a visible prompt without a signing command. */
|
|
207
218
|
export async function promptForWalletPassword(
|
|
208
219
|
existingWalletCount: number,
|
|
209
220
|
): Promise<void> {
|