@solard/cli 0.2.6 → 0.2.8
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/README.md +13 -13
- package/package.json +2 -2
- package/src/index.ts +1621 -32
- package/src/meteora-commands.ts +2356 -0
- package/src/pump/token-launch-cli.ts +118 -18
- package/src/pump/vamp-cli.ts +363 -0
package/src/index.ts
CHANGED
|
@@ -1,11 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
addExternalContact,
|
|
4
|
+
configureSolardMeasure,
|
|
5
|
+
createSolardMeasureCollector,
|
|
6
|
+
executeRegistrySolSweep,
|
|
7
|
+
executeRegistryTokenLiquidation,
|
|
8
|
+
findExternalContact,
|
|
9
|
+
getSolardRpcStats,
|
|
10
|
+
listExternalContacts,
|
|
11
|
+
loadWalletAssetPortfolio,
|
|
12
|
+
planRegistrySolSweep,
|
|
13
|
+
planRegistryTokenLiquidation,
|
|
14
|
+
removeExternalContact,
|
|
15
|
+
resetSolardRpcStats,
|
|
16
|
+
resolveTokenMintForPolicy,
|
|
17
|
+
runScript,
|
|
18
|
+
listScripts,
|
|
19
|
+
simulateRegistrySolSweep,
|
|
20
|
+
simulateRegistryTokenLiquidation,
|
|
21
|
+
wrappedSolAta,
|
|
22
|
+
type TokenRow,
|
|
23
|
+
} from "@solard/sdk";
|
|
24
|
+
|
|
25
|
+
import { handleMeteoraCommand } from "./meteora-commands.ts";
|
|
3
26
|
|
|
4
27
|
function emit(value: string): void {
|
|
5
28
|
process.stdout.write(value);
|
|
6
29
|
}
|
|
7
30
|
|
|
8
31
|
const OWL = "🦉";
|
|
32
|
+
const NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
33
|
+
const CANONICAL_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
34
|
+
|
|
35
|
+
async function quoteJupiterSwap(
|
|
36
|
+
args: Record<string, unknown>,
|
|
37
|
+
): Promise<unknown> {
|
|
38
|
+
const sdk = (await import("@solard/sdk")) as Record<string, unknown>;
|
|
39
|
+
const fn = sdk.quoteJupiterSwap;
|
|
40
|
+
if (typeof fn !== "function") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"@solard/sdk does not export quoteJupiterSwap required by `slrd swap`.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return await (fn as (input: Record<string, unknown>) => Promise<unknown>)(
|
|
46
|
+
args,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function executeJupiterSwap(
|
|
51
|
+
args: Record<string, unknown>,
|
|
52
|
+
): Promise<unknown> {
|
|
53
|
+
const sdk = (await import("@solard/sdk")) as Record<string, unknown>;
|
|
54
|
+
const fn = sdk.executeJupiterSwap;
|
|
55
|
+
if (typeof fn !== "function") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"@solard/sdk does not export executeJupiterSwap required by `slrd swap`.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return await (fn as (input: Record<string, unknown>) => Promise<unknown>)(
|
|
61
|
+
args,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
9
64
|
type Flags = Map<string, string>;
|
|
10
65
|
function args(input: string[]): { values: string[]; flags: Flags } {
|
|
11
66
|
const values: string[] = [],
|
|
@@ -78,6 +133,87 @@ function duration(value: string | undefined, fallbackMs: number): number {
|
|
|
78
133
|
];
|
|
79
134
|
return Math.floor(n * scale);
|
|
80
135
|
}
|
|
136
|
+
|
|
137
|
+
function txAccountKeys(tx: any): string[] {
|
|
138
|
+
const message = tx?.transaction?.message;
|
|
139
|
+
if (!message) return [];
|
|
140
|
+
const toStringKey = (value: any): string => {
|
|
141
|
+
if (!value) return "";
|
|
142
|
+
if (typeof value === "string") return value;
|
|
143
|
+
if (typeof value?.toBase58 === "function") return value.toBase58();
|
|
144
|
+
if (typeof value?.pubkey?.toBase58 === "function")
|
|
145
|
+
return value.pubkey.toBase58();
|
|
146
|
+
if (typeof value?.pubkey === "string") return value.pubkey;
|
|
147
|
+
return String(value);
|
|
148
|
+
};
|
|
149
|
+
if (Array.isArray(message.accountKeys))
|
|
150
|
+
return message.accountKeys.map(toStringKey);
|
|
151
|
+
if (Array.isArray(message.staticAccountKeys)) {
|
|
152
|
+
const keys = message.staticAccountKeys.map(toStringKey);
|
|
153
|
+
const loaded = tx?.meta?.loadedAddresses;
|
|
154
|
+
if (Array.isArray(loaded?.writable))
|
|
155
|
+
keys.push(...loaded.writable.map(toStringKey));
|
|
156
|
+
if (Array.isArray(loaded?.readonly))
|
|
157
|
+
keys.push(...loaded.readonly.map(toStringKey));
|
|
158
|
+
return keys;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
if (typeof message.getAccountKeys === "function") {
|
|
162
|
+
const accountKeys = message.getAccountKeys({
|
|
163
|
+
accountKeysFromLookups: tx?.meta?.loadedAddresses,
|
|
164
|
+
});
|
|
165
|
+
const keys: string[] = [];
|
|
166
|
+
for (let i = 0; i < Number(accountKeys?.length ?? 0); i += 1)
|
|
167
|
+
keys.push(toStringKey(accountKeys.get(i)));
|
|
168
|
+
return keys;
|
|
169
|
+
}
|
|
170
|
+
} catch {}
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function txWalletTokenBalances(
|
|
175
|
+
tx: any,
|
|
176
|
+
owner: string,
|
|
177
|
+
): Map<string, { decimals: number; pre: bigint; post: bigint }> {
|
|
178
|
+
const out = new Map<
|
|
179
|
+
string,
|
|
180
|
+
{ decimals: number; pre: bigint; post: bigint }
|
|
181
|
+
>();
|
|
182
|
+
const add = (side: "pre" | "post", rows: any[] | null | undefined) => {
|
|
183
|
+
for (const row of rows ?? []) {
|
|
184
|
+
const rowOwner =
|
|
185
|
+
typeof row?.owner === "string"
|
|
186
|
+
? row.owner
|
|
187
|
+
: typeof row?.owner?.toBase58 === "function"
|
|
188
|
+
? row.owner.toBase58()
|
|
189
|
+
: "";
|
|
190
|
+
if (rowOwner !== owner || typeof row?.mint !== "string") continue;
|
|
191
|
+
const amount = row?.uiTokenAmount?.amount;
|
|
192
|
+
const decimals = Number(row?.uiTokenAmount?.decimals ?? 0);
|
|
193
|
+
if (typeof amount !== "string" || !/^\d+$/.test(amount)) continue;
|
|
194
|
+
const existing = out.get(row.mint) ?? { decimals, pre: 0n, post: 0n };
|
|
195
|
+
existing.decimals = decimals;
|
|
196
|
+
existing[side] += BigInt(amount);
|
|
197
|
+
out.set(row.mint, existing);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
add("pre", tx?.meta?.preTokenBalances);
|
|
201
|
+
add("post", tx?.meta?.postTokenBalances);
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function formatSignedRaw(value: bigint, decimals: number): string {
|
|
206
|
+
const sign = value > 0n ? "+" : value < 0n ? "-" : "";
|
|
207
|
+
const abs = value < 0n ? -value : value;
|
|
208
|
+
return `${sign}${formatRaw(abs, decimals)}`;
|
|
209
|
+
}
|
|
210
|
+
function formatDurationMs(value: number): string {
|
|
211
|
+
if (!Number.isFinite(value) || value < 0) return "0ms";
|
|
212
|
+
if (value < 1_000) return `${value.toFixed(value < 10 ? 1 : 0)}ms`;
|
|
213
|
+
if (value < 60_000) return `${(value / 1_000).toFixed(2)}s`;
|
|
214
|
+
return `${(value / 60_000).toFixed(2)}m`;
|
|
215
|
+
}
|
|
216
|
+
|
|
81
217
|
function formatPrice(value: number | null | undefined): string {
|
|
82
218
|
if (value == null || !Number.isFinite(value)) return "n/a";
|
|
83
219
|
return value >= 0.001
|
|
@@ -114,25 +250,89 @@ function targetWallets(
|
|
|
114
250
|
group,
|
|
115
251
|
};
|
|
116
252
|
}
|
|
253
|
+
function resolveDestinationRef(
|
|
254
|
+
slrd: { resolveWallet(ref: string): { address: { toBase58(): string } } },
|
|
255
|
+
value: string,
|
|
256
|
+
): {
|
|
257
|
+
input: string;
|
|
258
|
+
address: string;
|
|
259
|
+
contactName?: string;
|
|
260
|
+
walletName?: string;
|
|
261
|
+
} {
|
|
262
|
+
const input = value.trim();
|
|
263
|
+
const contact = findExternalContact(input);
|
|
264
|
+
|
|
265
|
+
let walletAddress: string | null = null;
|
|
266
|
+
try {
|
|
267
|
+
walletAddress = slrd.resolveWallet(input).address.toBase58();
|
|
268
|
+
} catch {
|
|
269
|
+
walletAddress = null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (contact && walletAddress && contact.address !== walletAddress) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Ambiguous destination ${input}: external contact @${contact.name} points to ${contact.address}, ` +
|
|
275
|
+
`but a stored signing wallet resolves to ${walletAddress}. Rename one of them.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (contact) {
|
|
280
|
+
return {
|
|
281
|
+
input,
|
|
282
|
+
address: contact.address,
|
|
283
|
+
contactName: contact.name,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (walletAddress) {
|
|
288
|
+
return {
|
|
289
|
+
input,
|
|
290
|
+
address: walletAddress,
|
|
291
|
+
walletName: input.replace(/^@/, ""),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { input, address: input };
|
|
296
|
+
}
|
|
297
|
+
|
|
117
298
|
function help(): string {
|
|
118
299
|
return `${OWL} slrd — multi-wallet Solana CLI + SDK for traders and AI agents
|
|
119
300
|
|
|
120
301
|
Wallets and tokens
|
|
121
|
-
slrd
|
|
122
|
-
slrd wallet create [name] Alias for wallets create
|
|
302
|
+
slrd wallet create [name] Generate and persist an encrypted Solana wallet
|
|
123
303
|
slrd import <private_key> [name]
|
|
124
304
|
cat key.json | slrd import --stdin [name]
|
|
125
|
-
|
|
305
|
+
|
|
306
|
+
External contacts (public addresses only; never signing wallets or group members)
|
|
307
|
+
slrd contact add <name> <address> [--force]
|
|
308
|
+
slrd contact list
|
|
309
|
+
slrd contact show <name|address>
|
|
310
|
+
slrd contact remove <name|address>
|
|
311
|
+
slrd wallets [--wallet <wallet>] [--group <name>] [--token <token> | --tokens] [--token-totals] [--only-with-tokens] [--rpc-concurrency <n>] [--rpc-delay-ms <n>] [--addresses-only]
|
|
312
|
+
Show SOL by default; token scans are opt-in
|
|
313
|
+
slrd balances --wallet <wallet> [--token <token>] [--show-zero] Show SOL + all SPL token balances for one wallet
|
|
314
|
+
slrd fees --wallet <wallet> [--since 30m] [--limit 100] Sum actual on-chain transaction fees paid by this wallet
|
|
315
|
+
slrd sol-flow --wallet <wallet> [--since 30m] [--limit 100] Audit native SOL + WSOL + SPL deltas from actual transactions
|
|
126
316
|
slrd token <token_ca> [name] [--metadata-json <json>]
|
|
127
317
|
slrd token set <token|ca> [--pool <address>] [--quote-mint <mint>] [--quote-program <program>] [--metadata-json <json>]
|
|
128
318
|
slrd token refresh <token|ca>
|
|
129
319
|
slrd tokens
|
|
130
320
|
|
|
321
|
+
Vanity mints
|
|
322
|
+
slrd vanity --suffix pump --out .\\mint.json [--count <n>]
|
|
323
|
+
slrd vanity pool generate --suffix pump --count <n> [--max-attempts <n>] Generate encrypted mint keypairs into the canonical DB
|
|
324
|
+
slrd vanity pool list [--suffix pump] [--status available|reserved|used]
|
|
325
|
+
slrd vanity pool release <mint-address> Release an ambiguous/failed launch reservation
|
|
326
|
+
launch/vamp: add --mint-pool pump [--mint-pool-address <address>] to consume a pooled mint
|
|
327
|
+
|
|
131
328
|
Metadata and launching
|
|
132
329
|
slrd launch pump --creator <wallet> (--uri <metadata_uri> | --metadata <json> | --image <path> --description <text>) [--alias <name>] [--live] [--skip-simulation]
|
|
133
330
|
[--deployment-sender helius-rpc|helius-fast] [--helius-tip-sol 0.01]
|
|
134
331
|
slrd metadata upload --image <local_path> --name <name> --symbol <symbol> --description <text> [--provider pump-frontend|pinata]
|
|
135
332
|
[--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
|
|
333
|
+
slrd vamp <source-mint> --creator <wallet> [--name <name>] [--symbol <symbol>] [--image <path-or-url>] [--description <text>]
|
|
334
|
+
[--website <url>] [--twitter <url>] [--telegram <url>] [--video <url>] [--uri <metadata-uri>]
|
|
335
|
+
[--buy-plan <file>] [--mint-pool pump] [--submit-mode jito-bundle] [--live] [--skip-simulation]
|
|
136
336
|
slrd deploy pump --wallet <wallet> --name <name> --symbol <symbol> (--uri <metadata_uri> | --image <local_path> --description <text>) [--alias <name>] [--live]
|
|
137
337
|
[--twitter <url>] [--telegram <url>] [--website <url>] [--video <url>] [--hide-name]
|
|
138
338
|
|
|
@@ -142,12 +342,32 @@ Prices
|
|
|
142
342
|
slrd price average <token|ca> --period 15m Average stored samples in a period
|
|
143
343
|
slrd price watch <token|ca...> [--interval 1s] [--period 1m]
|
|
144
344
|
|
|
345
|
+
Transfers and consolidation
|
|
346
|
+
slrd transfer <contact|wallet|address> --wallet <source-wallet> --sol <amount> [--simulate-only]
|
|
347
|
+
slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]
|
|
348
|
+
Without --wallets, sweep considers all stored signing wallets
|
|
349
|
+
|
|
350
|
+
Token liquidation
|
|
351
|
+
slrd liquidate tokens --except <token|mint> [--wallets <a,b,...>] [--slippage-bps 1500] [--no-jupiter] [--simulate | --live]
|
|
352
|
+
Sell supported tokens except protected mint(s); unwrap WSOL; close unprotected zero-balance token accounts and reclaim rent
|
|
353
|
+
|
|
354
|
+
RPC
|
|
355
|
+
All Solard JSON-RPC traffic is globally rate-limited to 5 req/s by default.
|
|
356
|
+
Override only when your provider allows it: SLRD_RPC_MAX_RPS=<n>
|
|
357
|
+
Jupiter fallback: keyless 0.5 req/s, API-key default 1 req/s; override SLRD_JUPITER_MAX_RPS=<n>
|
|
358
|
+
|
|
359
|
+
Diagnostics
|
|
360
|
+
Commands collect measure-fn telemetry without streaming it to the terminal.
|
|
361
|
+
--measure Print top aggregate timings after the normal command result
|
|
362
|
+
--measure-stream Restore raw live measure-fn output for low-level debugging
|
|
363
|
+
|
|
145
364
|
Trading
|
|
365
|
+
slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live] Quote or execute SOL -> token through Jupiter
|
|
146
366
|
slrd buy <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) --sol <amount> [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
|
|
147
367
|
slrd buy <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) --spam [--live]
|
|
148
368
|
slrd spam-buy [pump] <future-mint> (--wallet <wallet> | --group <name>) (--sol <amount> | --lamports <amount> | --min-bps <n> --max-bps <n>) [--sender <id>] [--live]
|
|
149
369
|
slrd sell <token|ca> (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--bps 10000] [--slippage-bps 1500] [--sender rpc|helius|jito] [--simulate-only]
|
|
150
|
-
slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--simulate-only]
|
|
370
|
+
slrd unwrap-wsol (--wallet <wallet> | --wallets <w1,w2> | --group <name>) [--sender rpc|helius|jito] [--ignore-missing] [--continue-on-error] [--simulate-only]
|
|
151
371
|
slrd claim <token|ca> --wallet <wallet> [--sender rpc|helius|jito]
|
|
152
372
|
|
|
153
373
|
Scripts (strategies stay outside the kernel)
|
|
@@ -155,6 +375,23 @@ Scripts (strategies stay outside the kernel)
|
|
|
155
375
|
slrd run <name-or-path> [script flags...] Execute a script that imports slrd
|
|
156
376
|
slrd run snipe --name <exact_name> --group <group> --sol 0.05 --sender jito
|
|
157
377
|
|
|
378
|
+
Meteora DLMM
|
|
379
|
+
slrd meteora discover --timeframe 30m --sort fee-active-tvl --limit 20
|
|
380
|
+
slrd meteora opportunities --timeframe 30m --sort flow-inactive --min-inactive-pct 70 --limit 20
|
|
381
|
+
slrd meteora token-pools <mint|symbol> --timeframe 30m
|
|
382
|
+
slrd meteora pools --timeframe 30m --sort fee-tvl --limit 20
|
|
383
|
+
slrd meteora pool <pool> [--timeframe 30m]
|
|
384
|
+
slrd meteora candles <pool> [--timeframe 5m]
|
|
385
|
+
slrd meteora positions --wallet <wallet|address>
|
|
386
|
+
slrd meteora open <pool> --wallet <wallet> --sol 0.1 --bins 40 [--strategy spot] [--live]
|
|
387
|
+
slrd meteora move <position> --wallet <wallet> [--bins 10] [--live]
|
|
388
|
+
slrd meteora migrate <position> --wallet <wallet> --to-pool <pool> [--bins 10] [--live]
|
|
389
|
+
slrd meteora add|remove|claim|close <position> --wallet <wallet> [--live]
|
|
390
|
+
slrd meteora close-all <pool> --wallet <wallet> [--live]
|
|
391
|
+
slrd meteora quote <pool> (--in-x N|--in-y N|--out-x N|--out-y N)
|
|
392
|
+
slrd meteora swap <pool> --wallet <wallet> (--in-x N|--in-y N|--out-x N|--out-y N) [--live]
|
|
393
|
+
slrd meteora help Full Meteora command reference
|
|
394
|
+
|
|
158
395
|
Groups and agents
|
|
159
396
|
slrd group create <name> [description]
|
|
160
397
|
slrd group add <group> <wallet> [weight_bps]
|
|
@@ -181,7 +418,7 @@ Transactions and ALTs
|
|
|
181
418
|
slrd alt extend <address> --wallet <wallet> <account...>
|
|
182
419
|
|
|
183
420
|
Environment
|
|
184
|
-
SLRD_DB_PATH shared SDK/CLI database; default
|
|
421
|
+
SLRD_DB_PATH shared SDK/CLI database; default ./slrd.db (SOLARD_DB_PATH alias supported)
|
|
185
422
|
SLRD_MASTER_KEY required to create/import/decrypt stored wallets
|
|
186
423
|
RPC_ENDPOINT required only for chain operations
|
|
187
424
|
HELIUS_SENDER_URL regional/global Helius Sender endpoint used by launch scripts
|
|
@@ -199,6 +436,62 @@ Environment
|
|
|
199
436
|
async function main() {
|
|
200
437
|
const [command, ...rest] = process.argv.slice(2);
|
|
201
438
|
const { values, flags } = args(rest);
|
|
439
|
+
|
|
440
|
+
const commandStartedAt = performance.now();
|
|
441
|
+
const measureCollector = createSolardMeasureCollector();
|
|
442
|
+
resetSolardRpcStats();
|
|
443
|
+
|
|
444
|
+
// Keep measurement active, but route events away from terminal output.
|
|
445
|
+
// --measure adds a detailed aggregate at the end; --measure-stream restores
|
|
446
|
+
// measure-fn's raw live logger for low-level debugging.
|
|
447
|
+
const streamMeasures =
|
|
448
|
+
flags.has("measure-stream") ||
|
|
449
|
+
process.env.SLRD_MEASURE_STREAM === "1" ||
|
|
450
|
+
process.env.SLRD_MEASURE_STREAM === "true";
|
|
451
|
+
configureSolardMeasure(
|
|
452
|
+
streamMeasures
|
|
453
|
+
? { silent: false, logger: null }
|
|
454
|
+
: { silent: false, logger: measureCollector.logger },
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
let perfPrinted = false;
|
|
458
|
+
const printPerfSummary = () => {
|
|
459
|
+
if (perfPrinted) return;
|
|
460
|
+
perfPrinted = true;
|
|
461
|
+
|
|
462
|
+
const measures = measureCollector.snapshot();
|
|
463
|
+
const rpc = getSolardRpcStats();
|
|
464
|
+
const wallMs = Math.max(0, performance.now() - commandStartedAt);
|
|
465
|
+
|
|
466
|
+
if (
|
|
467
|
+
measures.completed === 0 &&
|
|
468
|
+
measures.annotations === 0 &&
|
|
469
|
+
rpc.requestStarts === 0
|
|
470
|
+
) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
emit(
|
|
475
|
+
`PERF wall=${formatDurationMs(wallMs)} ` +
|
|
476
|
+
`measure=${measures.completed} (${measures.errors} err) ` +
|
|
477
|
+
`rpc=${rpc.requestStarts} 429=${rpc.rateLimited429} ` +
|
|
478
|
+
`retries=${rpc.retries429} limit=${rpc.maxRps}/s\n`,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (flags.has("measure") && !streamMeasures) {
|
|
482
|
+
for (const row of measures.labels.slice(0, 10)) {
|
|
483
|
+
emit(
|
|
484
|
+
` ${row.label} calls=${row.calls} ` +
|
|
485
|
+
`total=${formatDurationMs(row.totalMs)} ` +
|
|
486
|
+
`max=${formatDurationMs(row.maxMs)}` +
|
|
487
|
+
`${row.errors ? ` errors=${row.errors}` : ""}\n`,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
process.once("beforeExit", printPerfSummary);
|
|
494
|
+
|
|
202
495
|
if (!command || command === "help" || command === "--help") {
|
|
203
496
|
emit(help());
|
|
204
497
|
return;
|
|
@@ -214,10 +507,84 @@ async function main() {
|
|
|
214
507
|
}
|
|
215
508
|
if (command === "vanity") {
|
|
216
509
|
const {
|
|
510
|
+
addVanityMintToPool,
|
|
511
|
+
cleanVanitySuffix,
|
|
512
|
+
defaultVanityMaxAttempts,
|
|
217
513
|
generateMintKeypairWithSuffix,
|
|
514
|
+
listVanityMintPool,
|
|
515
|
+
releaseVanityMintReservation,
|
|
218
516
|
saveMintKeypairFile,
|
|
219
|
-
cleanVanitySuffix,
|
|
220
517
|
} = await import("@solard/sdk");
|
|
518
|
+
|
|
519
|
+
if (values[0] === "pool") {
|
|
520
|
+
const action = values[1] ?? "list";
|
|
521
|
+
|
|
522
|
+
if (action === "generate") {
|
|
523
|
+
const suffix = cleanVanitySuffix(need(flags, "suffix"));
|
|
524
|
+
const count = int(flags, "count", 1)!;
|
|
525
|
+
if (count <= 0) throw new Error("--count must be greater than zero");
|
|
526
|
+
const results = [];
|
|
527
|
+
for (let i = 0; i < count; i++) {
|
|
528
|
+
const found = await generateMintKeypairWithSuffix({
|
|
529
|
+
suffix,
|
|
530
|
+
workers: int(flags, "workers"),
|
|
531
|
+
maxAttempts: int(
|
|
532
|
+
flags,
|
|
533
|
+
"max-attempts",
|
|
534
|
+
defaultVanityMaxAttempts(suffix),
|
|
535
|
+
)!,
|
|
536
|
+
timeoutMs: int(flags, "timeout-ms", 0)!,
|
|
537
|
+
reportEvery: int(flags, "report-every", 1_000_000)!,
|
|
538
|
+
onProgress: (p) =>
|
|
539
|
+
emit(
|
|
540
|
+
`${OWL} pool grinding ${p.suffix} [${i + 1}/${count}]: ${p.attempts} attempts, ${p.ratePerSecond}/s\n`,
|
|
541
|
+
),
|
|
542
|
+
});
|
|
543
|
+
results.push({
|
|
544
|
+
...addVanityMintToPool(found.mint, suffix),
|
|
545
|
+
attempts: found.attempts,
|
|
546
|
+
elapsedMs: found.elapsedMs,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
emit(json(results) + "\n");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (action === "list") {
|
|
554
|
+
const status = flags.get("status") as
|
|
555
|
+
"available" | "reserved" | "used" | undefined;
|
|
556
|
+
if (
|
|
557
|
+
status &&
|
|
558
|
+
status !== "available" &&
|
|
559
|
+
status !== "reserved" &&
|
|
560
|
+
status !== "used"
|
|
561
|
+
) {
|
|
562
|
+
throw new Error("--status must be available, reserved, or used");
|
|
563
|
+
}
|
|
564
|
+
emit(
|
|
565
|
+
json(
|
|
566
|
+
listVanityMintPool({
|
|
567
|
+
suffix: flags.get("suffix"),
|
|
568
|
+
status,
|
|
569
|
+
}),
|
|
570
|
+
) + "\n",
|
|
571
|
+
);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (action === "release") {
|
|
576
|
+
const address = values[2] ?? flags.get("address");
|
|
577
|
+
if (!address)
|
|
578
|
+
throw new Error("Usage: slrd vanity pool release <mint-address>");
|
|
579
|
+
emit(json(releaseVanityMintReservation(address)) + "\n");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
throw new Error(
|
|
584
|
+
"Usage: slrd vanity pool <generate|list|release> [options]",
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
221
588
|
const suffix = cleanVanitySuffix(need(flags, "suffix"));
|
|
222
589
|
const out = need(flags, "out");
|
|
223
590
|
const count = int(flags, "count", 1)!;
|
|
@@ -226,7 +593,11 @@ async function main() {
|
|
|
226
593
|
const found = await generateMintKeypairWithSuffix({
|
|
227
594
|
suffix,
|
|
228
595
|
workers: int(flags, "workers"),
|
|
229
|
-
maxAttempts: int(
|
|
596
|
+
maxAttempts: int(
|
|
597
|
+
flags,
|
|
598
|
+
"max-attempts",
|
|
599
|
+
defaultVanityMaxAttempts(suffix),
|
|
600
|
+
)!,
|
|
230
601
|
timeoutMs: int(flags, "timeout-ms", 0)!,
|
|
231
602
|
reportEvery: int(flags, "report-every", 1_000_000)!,
|
|
232
603
|
onProgress: (p) =>
|
|
@@ -286,6 +657,18 @@ async function main() {
|
|
|
286
657
|
});
|
|
287
658
|
return;
|
|
288
659
|
}
|
|
660
|
+
if (command === "vamp") {
|
|
661
|
+
const { runPumpVampFromArgs } = await import("./pump/vamp-cli.ts");
|
|
662
|
+
await runPumpVampFromArgs(rest, {
|
|
663
|
+
defaultSubmitMode: "after-deploy-processed",
|
|
664
|
+
defaultDeploymentPriorityMicroLamports: 500_000,
|
|
665
|
+
defaultBuyerPriorityMicroLamports: 1_500_000,
|
|
666
|
+
defaultSlippageBps: 1_500,
|
|
667
|
+
persistOnLive: true,
|
|
668
|
+
report: (label, value) => emit(`${label}: ${json(value)}\n`),
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
289
672
|
if (command === "jito" && values[0] === "tip-accounts") {
|
|
290
673
|
const endpoint = (
|
|
291
674
|
flags.get("endpoint") ??
|
|
@@ -330,10 +713,133 @@ async function main() {
|
|
|
330
713
|
]);
|
|
331
714
|
const slrd = createTraderSolard();
|
|
332
715
|
try {
|
|
333
|
-
if (
|
|
334
|
-
(
|
|
335
|
-
|
|
336
|
-
|
|
716
|
+
if (command === "meteora") {
|
|
717
|
+
await handleMeteoraCommand({ slrd, values, flags, emit });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (command === "swap") {
|
|
722
|
+
const outputRef = values[0]?.trim();
|
|
723
|
+
if (!outputRef) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
"Usage: slrd swap <token|mint> --wallet <wallet> --sol <amount> [--live]",
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const wallet = need(flags, "wallet");
|
|
730
|
+
const amountSol = need(flags, "sol");
|
|
731
|
+
const amountRaw = sol(amountSol).raw;
|
|
732
|
+
if (amountRaw <= 0n) throw new Error("--sol must be greater than zero");
|
|
733
|
+
|
|
734
|
+
const normalizedOutput = outputRef.replace(/^\$/, "").trim();
|
|
735
|
+
const outputMint =
|
|
736
|
+
normalizedOutput.toUpperCase() === "USDC"
|
|
737
|
+
? CANONICAL_USDC_MINT
|
|
738
|
+
: resolveTokenMintForPolicy(slrd, normalizedOutput);
|
|
739
|
+
|
|
740
|
+
if (outputMint === NATIVE_SOL_MINT) {
|
|
741
|
+
throw new Error("Swap output is SOL; choose a different token mint.");
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!flags.has("live")) {
|
|
745
|
+
const quote = await quoteJupiterSwap({
|
|
746
|
+
inputMint: NATIVE_SOL_MINT,
|
|
747
|
+
outputMint,
|
|
748
|
+
amountRaw,
|
|
749
|
+
});
|
|
750
|
+
emit(
|
|
751
|
+
json({
|
|
752
|
+
mode: "quote",
|
|
753
|
+
wallet,
|
|
754
|
+
input: { symbol: "SOL", mint: NATIVE_SOL_MINT, amountSol },
|
|
755
|
+
output: { ref: outputRef, mint: outputMint },
|
|
756
|
+
quote,
|
|
757
|
+
hint: "Re-run with --live to execute this swap.",
|
|
758
|
+
}) + "\n",
|
|
759
|
+
);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const signer = slrd.signer(wallet);
|
|
764
|
+
const result = await executeJupiterSwap({
|
|
765
|
+
inputMint: NATIVE_SOL_MINT,
|
|
766
|
+
outputMint,
|
|
767
|
+
amountRaw,
|
|
768
|
+
signer,
|
|
769
|
+
});
|
|
770
|
+
emit(
|
|
771
|
+
json({
|
|
772
|
+
mode: "live",
|
|
773
|
+
wallet,
|
|
774
|
+
input: { symbol: "SOL", mint: NATIVE_SOL_MINT, amountSol },
|
|
775
|
+
output: { ref: outputRef, mint: outputMint },
|
|
776
|
+
result,
|
|
777
|
+
}) + "\n",
|
|
778
|
+
);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (command === "contact" || command === "contacts") {
|
|
782
|
+
const action = values[0] ?? "list";
|
|
783
|
+
|
|
784
|
+
if (action === "add") {
|
|
785
|
+
const name = values[1];
|
|
786
|
+
const address = values[2];
|
|
787
|
+
if (!name || !address) {
|
|
788
|
+
throw new Error("Usage: slrd contact add <name> <address> [--force]");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
let walletCollision: string | null = null;
|
|
792
|
+
try {
|
|
793
|
+
walletCollision = slrd.resolveWallet(name).address.toBase58();
|
|
794
|
+
} catch {
|
|
795
|
+
walletCollision = null;
|
|
796
|
+
}
|
|
797
|
+
if (walletCollision && walletCollision !== address) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
`Cannot create external contact @${name}: a stored signing wallet already uses that name (${walletCollision}).`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const contact = addExternalContact(name, address, {
|
|
804
|
+
overwrite: flags.has("force") || flags.has("overwrite"),
|
|
805
|
+
});
|
|
806
|
+
emit(`${OWL} contact @${contact.name}\t${contact.address}\n`);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (action === "list") {
|
|
811
|
+
const contacts = listExternalContacts();
|
|
812
|
+
if (flags.has("json")) {
|
|
813
|
+
emit(json(contacts) + "\n");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
for (const contact of contacts) {
|
|
817
|
+
emit(`@${contact.name}\t${contact.address}\n`);
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (action === "show") {
|
|
823
|
+
const ref = values[1];
|
|
824
|
+
if (!ref) throw new Error("Usage: slrd contact show <name|address>");
|
|
825
|
+
const contact = findExternalContact(ref);
|
|
826
|
+
if (!contact) throw new Error(`Unknown external contact: ${ref}`);
|
|
827
|
+
emit(json(contact) + "\n");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (action === "remove" || action === "delete" || action === "rm") {
|
|
832
|
+
const ref = values[1];
|
|
833
|
+
if (!ref) throw new Error("Usage: slrd contact remove <name|address>");
|
|
834
|
+
const removed = removeExternalContact(ref);
|
|
835
|
+
emit(`${OWL} removed contact @${removed.name}\t${removed.address}\n`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
throw new Error("Usage: slrd contact <add|list|show|remove> [arguments]");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (command === "wallet" && values[0] === "create") {
|
|
337
843
|
const wallet = slrd.createWallet(values[1]);
|
|
338
844
|
emit(`${OWL} created @${wallet.name} ${wallet.address}\n`);
|
|
339
845
|
return;
|
|
@@ -385,34 +891,560 @@ async function main() {
|
|
|
385
891
|
emit(`${OWL} imported @${wallet.name} ${wallet.address}\n`);
|
|
386
892
|
return;
|
|
387
893
|
}
|
|
388
|
-
if (command === "wallets") {
|
|
389
|
-
|
|
894
|
+
if (command === "wallets" || command === "balances") {
|
|
895
|
+
if (
|
|
896
|
+
flags.get("token") &&
|
|
897
|
+
(flags.has("tokens") || flags.has("all-tokens"))
|
|
898
|
+
) {
|
|
899
|
+
throw new Error("Use either --token <token> or --tokens, not both.");
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let wallets = slrd.wallets.list();
|
|
903
|
+
const singleWalletRef =
|
|
904
|
+
flags.get("wallet") ?? (command === "balances" ? values[0] : undefined);
|
|
905
|
+
if (singleWalletRef) {
|
|
906
|
+
const wanted = slrd.resolveWallet(singleWalletRef).address.toBase58();
|
|
907
|
+
wallets = wallets.filter(
|
|
908
|
+
(wallet) => slrd.resolveWallet(wallet).address.toBase58() === wanted,
|
|
909
|
+
);
|
|
910
|
+
if (wallets.length === 0)
|
|
911
|
+
throw new Error(`Wallet ${singleWalletRef} is not a stored wallet`);
|
|
912
|
+
}
|
|
913
|
+
if (command === "balances" && !singleWalletRef)
|
|
914
|
+
throw new Error("Usage: slrd balances --wallet <wallet>");
|
|
915
|
+
const group = flags.get("group");
|
|
916
|
+
if (group) {
|
|
917
|
+
const addresses = new Set(
|
|
918
|
+
slrd
|
|
919
|
+
.groupWallets(group)
|
|
920
|
+
.map((ref) => slrd.resolveWallet(ref).address.toBase58()),
|
|
921
|
+
);
|
|
922
|
+
wallets = wallets.filter((wallet) => addresses.has(wallet.address));
|
|
923
|
+
}
|
|
924
|
+
|
|
390
925
|
if (flags.has("addresses-only")) {
|
|
391
926
|
for (const wallet of wallets)
|
|
392
927
|
emit(`@${wallet.name}\t${wallet.address}\n`);
|
|
393
928
|
return;
|
|
394
929
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
.
|
|
930
|
+
|
|
931
|
+
const allTokens =
|
|
932
|
+
command === "balances" ||
|
|
933
|
+
flags.has("tokens") ||
|
|
934
|
+
flags.has("all-tokens");
|
|
935
|
+
const tokenRef = flags.get("token");
|
|
936
|
+
if (!allTokens && !tokenRef) {
|
|
937
|
+
// Cheap native-SOL-only default: <=100 wallets per RPC call.
|
|
938
|
+
const rows: Array<{
|
|
939
|
+
wallet: (typeof wallets)[number];
|
|
940
|
+
solLamports: bigint;
|
|
941
|
+
}> = [];
|
|
942
|
+
const connection = slrd.connection();
|
|
943
|
+
const batchSize = 100;
|
|
944
|
+
|
|
945
|
+
for (let offset = 0; offset < wallets.length; offset += batchSize) {
|
|
946
|
+
const batch = wallets.slice(offset, offset + batchSize);
|
|
947
|
+
const addresses = batch.map(
|
|
948
|
+
(wallet) => slrd.resolveWallet(wallet).address,
|
|
949
|
+
);
|
|
950
|
+
const accounts = await connection.getMultipleAccountsInfo(
|
|
951
|
+
addresses,
|
|
952
|
+
"confirmed",
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
956
|
+
rows.push({
|
|
957
|
+
wallet: batch[index]!,
|
|
958
|
+
solLamports: BigInt(accounts[index]?.lamports ?? 0),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
rows.sort((left, right) =>
|
|
964
|
+
left.solLamports === right.solLamports
|
|
965
|
+
? left.wallet.name.localeCompare(right.wallet.name)
|
|
966
|
+
: left.solLamports > right.solLamports
|
|
967
|
+
? -1
|
|
968
|
+
: 1,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const totalSol = rows.reduce((sum, row) => sum + row.solLamports, 0n);
|
|
972
|
+
emit(`TOTAL\tSOL=${formatRaw(totalSol, 9)}\tWALLETS=${rows.length}\n`);
|
|
973
|
+
for (const { wallet, solLamports } of rows) {
|
|
974
|
+
emit(
|
|
975
|
+
`@${wallet.name}\t${wallet.address}\tSOL=${formatRaw(solLamports, 9)}\n`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Owner-centric token discovery. This never refreshes/upserts the token
|
|
982
|
+
// registry, so `wallets --tokens` stays read-only and its stdout remains
|
|
983
|
+
// actual portfolio output instead of measurement noise.
|
|
984
|
+
const portfolio = await loadWalletAssetPortfolio(slrd, {
|
|
985
|
+
walletRefs: wallets.map((wallet) => wallet.address),
|
|
986
|
+
includeZero: flags.has("show-zero"),
|
|
987
|
+
concurrency: Math.max(
|
|
988
|
+
1,
|
|
989
|
+
Math.min(8, Math.trunc(int(flags, "rpc-concurrency", 1) ?? 1)),
|
|
990
|
+
),
|
|
991
|
+
requestDelayMs: Math.max(
|
|
992
|
+
0,
|
|
993
|
+
Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
|
|
994
|
+
),
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const selectedMint = tokenRef ? slrd.resolveToken(tokenRef).mint : null;
|
|
998
|
+
|
|
999
|
+
const visibleRows = portfolio.rows.map((row) => ({
|
|
1000
|
+
...row,
|
|
1001
|
+
tokenHoldings: selectedMint
|
|
1002
|
+
? row.tokenHoldings.filter((holding) => holding.mint === selectedMint)
|
|
1003
|
+
: row.tokenHoldings,
|
|
1004
|
+
}));
|
|
1005
|
+
|
|
1006
|
+
const visibleTotals = new Map<
|
|
1007
|
+
string,
|
|
1008
|
+
{
|
|
1009
|
+
mint: string;
|
|
1010
|
+
name: string | null;
|
|
1011
|
+
symbol: string | null;
|
|
1012
|
+
decimals: number;
|
|
1013
|
+
amountRaw: bigint;
|
|
1014
|
+
}
|
|
1015
|
+
>();
|
|
1016
|
+
for (const row of visibleRows) {
|
|
1017
|
+
for (const holding of row.tokenHoldings) {
|
|
1018
|
+
if (holding.amountRaw <= 0n && !flags.has("show-zero")) continue;
|
|
1019
|
+
const existing = visibleTotals.get(holding.mint);
|
|
1020
|
+
if (existing) existing.amountRaw += holding.amountRaw;
|
|
1021
|
+
else
|
|
1022
|
+
visibleTotals.set(holding.mint, {
|
|
1023
|
+
mint: holding.mint,
|
|
1024
|
+
name: holding.name,
|
|
1025
|
+
symbol: holding.symbol,
|
|
1026
|
+
decimals: holding.decimals,
|
|
1027
|
+
amountRaw: holding.amountRaw,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const holdingCount = visibleRows.reduce(
|
|
1033
|
+
(sum, row) =>
|
|
1034
|
+
sum +
|
|
1035
|
+
row.tokenHoldings.filter(
|
|
1036
|
+
(holding) => holding.amountRaw > 0n || flags.has("show-zero"),
|
|
1037
|
+
).length,
|
|
1038
|
+
0,
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
emit(
|
|
1042
|
+
`PORTFOLIO ${visibleRows.length} wallets | ` +
|
|
1043
|
+
`${formatRaw(portfolio.totalSolLamports, 9)} SOL | ` +
|
|
1044
|
+
`${holdingCount} token holdings | ${visibleTotals.size} mints` +
|
|
1045
|
+
`${portfolio.tokenScanErrorCount ? ` | ${portfolio.tokenScanErrorCount} scan errors` : ""}\n`,
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
if (flags.has("token-totals")) {
|
|
1049
|
+
const aggregateRows = [...visibleTotals.values()].sort((left, right) =>
|
|
1050
|
+
(left.symbol ?? left.name ?? left.mint).localeCompare(
|
|
1051
|
+
right.symbol ?? right.name ?? right.mint,
|
|
1052
|
+
),
|
|
1053
|
+
);
|
|
1054
|
+
emit("\nTOKEN TOTALS\n");
|
|
1055
|
+
for (const total of aggregateRows) {
|
|
1056
|
+
const label = total.symbol
|
|
1057
|
+
? total.symbol
|
|
1058
|
+
: (total.name ?? shortKey(total.mint));
|
|
1059
|
+
emit(
|
|
1060
|
+
` ${label.padEnd(14)} ${formatRaw(total.amountRaw, total.decimals).padStart(18)} ${total.mint}\n`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
for (const row of visibleRows) {
|
|
1066
|
+
const holdings = row.tokenHoldings.filter(
|
|
1067
|
+
(holding) =>
|
|
1068
|
+
holding.amountRaw > 0n ||
|
|
1069
|
+
flags.has("show-zero") ||
|
|
1070
|
+
Boolean(tokenRef),
|
|
1071
|
+
);
|
|
1072
|
+
if (
|
|
1073
|
+
flags.has("only-with-tokens") &&
|
|
1074
|
+
holdings.every((holding) => holding.amountRaw <= 0n)
|
|
1075
|
+
) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
emit(
|
|
1080
|
+
`\n@${row.walletName} SOL ${formatRaw(row.solLamports, 9)} ` +
|
|
1081
|
+
`${holdings.filter((holding) => holding.amountRaw > 0n).length} token(s)\n`,
|
|
1082
|
+
);
|
|
1083
|
+
emit(` ${row.walletAddress}\n`);
|
|
1084
|
+
|
|
1085
|
+
for (const holding of holdings) {
|
|
1086
|
+
const label = holding.symbol
|
|
1087
|
+
? holding.symbol
|
|
1088
|
+
: (holding.name ?? shortKey(holding.mint));
|
|
1089
|
+
emit(
|
|
1090
|
+
` ${label.padEnd(14)} ${holding.amountUi.padStart(18)} ${holding.mint}\n`,
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (!row.tokenScanComplete) {
|
|
1095
|
+
emit(
|
|
1096
|
+
` ! token scan partial (${row.tokenScanErrors.length} error(s))\n`,
|
|
1097
|
+
);
|
|
1098
|
+
if (flags.has("verbose")) {
|
|
1099
|
+
for (const error of row.tokenScanErrors) {
|
|
1100
|
+
process.stderr.write(
|
|
1101
|
+
`${OWL} token scan @${row.walletName}: ${error}\n`,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (command === "fees" || (command === "tx" && values[0] === "fees")) {
|
|
1110
|
+
const walletRef = need(flags, "wallet");
|
|
1111
|
+
const wallet = slrd.resolveWallet(walletRef);
|
|
1112
|
+
const address = wallet.address;
|
|
1113
|
+
const addressText = address.toBase58();
|
|
1114
|
+
const limit = Math.max(
|
|
1115
|
+
1,
|
|
1116
|
+
Math.min(1_000, Math.trunc(int(flags, "limit", 100) ?? 100)),
|
|
1117
|
+
);
|
|
1118
|
+
const sinceMs = duration(flags.get("since"), 30 * 60_000);
|
|
1119
|
+
const cutoff = Date.now() - sinceMs;
|
|
1120
|
+
const connection = slrd.connection();
|
|
1121
|
+
const signatures = await connection.getSignaturesForAddress(
|
|
1122
|
+
address,
|
|
1123
|
+
{ limit },
|
|
1124
|
+
"confirmed",
|
|
1125
|
+
);
|
|
1126
|
+
const recent = signatures.filter(
|
|
1127
|
+
(entry) => entry.blockTime == null || entry.blockTime * 1_000 >= cutoff,
|
|
1128
|
+
);
|
|
1129
|
+
let totalLamports = 0n;
|
|
1130
|
+
let feeMetaFound = 0;
|
|
1131
|
+
let paidByWalletCount = 0;
|
|
1132
|
+
let failed = 0;
|
|
1133
|
+
const rows: Array<{
|
|
1134
|
+
signature: string;
|
|
1135
|
+
blockTime: number | null | undefined;
|
|
1136
|
+
feeLamports: number | null;
|
|
1137
|
+
paidByWallet: boolean;
|
|
1138
|
+
err: unknown;
|
|
1139
|
+
}> = [];
|
|
1140
|
+
for (const entry of recent) {
|
|
1141
|
+
try {
|
|
1142
|
+
const tx = await connection.getTransaction(entry.signature, {
|
|
1143
|
+
commitment: "confirmed",
|
|
1144
|
+
maxSupportedTransactionVersion: 0,
|
|
1145
|
+
});
|
|
1146
|
+
const fee = tx?.meta?.fee;
|
|
1147
|
+
const payer = txAccountKeys(tx)[0] ?? "";
|
|
1148
|
+
const paidByWallet = payer === addressText;
|
|
1149
|
+
if (typeof fee === "number" && Number.isFinite(fee)) {
|
|
1150
|
+
feeMetaFound += 1;
|
|
1151
|
+
if (paidByWallet) {
|
|
1152
|
+
totalLamports += BigInt(Math.trunc(fee));
|
|
1153
|
+
paidByWalletCount += 1;
|
|
1154
|
+
}
|
|
1155
|
+
rows.push({
|
|
1156
|
+
signature: entry.signature,
|
|
1157
|
+
blockTime: entry.blockTime,
|
|
1158
|
+
feeLamports: Math.trunc(fee),
|
|
1159
|
+
paidByWallet,
|
|
1160
|
+
err: entry.err,
|
|
1161
|
+
});
|
|
1162
|
+
} else {
|
|
1163
|
+
rows.push({
|
|
1164
|
+
signature: entry.signature,
|
|
1165
|
+
blockTime: entry.blockTime,
|
|
1166
|
+
feeLamports: null,
|
|
1167
|
+
paidByWallet,
|
|
1168
|
+
err: entry.err,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
} catch {
|
|
1172
|
+
failed += 1;
|
|
1173
|
+
rows.push({
|
|
1174
|
+
signature: entry.signature,
|
|
1175
|
+
blockTime: entry.blockTime,
|
|
1176
|
+
feeLamports: null,
|
|
1177
|
+
paidByWallet: false,
|
|
1178
|
+
err: entry.err,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
emit(
|
|
1183
|
+
`FEES @${wallet.row?.name ?? walletRef} ${addressText}\n` +
|
|
1184
|
+
`Window: ${flags.get("since") ?? "30m"} transactions=${recent.length} fee-meta=${feeMetaFound} wallet-paid=${paidByWalletCount} unavailable=${recent.length - feeMetaFound}\n` +
|
|
1185
|
+
`Actual network fees paid by wallet: ${(Number(totalLamports) / 1e9).toFixed(9)} SOL (${totalLamports.toString()} lamports)\n`,
|
|
1186
|
+
);
|
|
1187
|
+
if (!flags.has("summary-only")) {
|
|
1188
|
+
emit(
|
|
1189
|
+
"\nTIME FEE SOL PAYER STATUS SIGNATURE\n",
|
|
1190
|
+
);
|
|
1191
|
+
for (const row of rows) {
|
|
1192
|
+
const time = row.blockTime
|
|
1193
|
+
? new Date(row.blockTime * 1_000).toISOString()
|
|
1194
|
+
: "-";
|
|
1195
|
+
const feeSol =
|
|
1196
|
+
row.feeLamports == null ? "-" : (row.feeLamports / 1e9).toFixed(9);
|
|
1197
|
+
emit(
|
|
1198
|
+
`${time.padEnd(25)} ${feeSol.padStart(12)} ${(row.paidByWallet ? "YOU" : "OTHER").padEnd(6)} ${(row.err ? "FAILED" : "OK").padEnd(7)} ${row.signature}\n`,
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (failed > 0)
|
|
1203
|
+
process.stderr.write(
|
|
1204
|
+
`${OWL} ${failed} transaction lookup(s) failed; rerun after RPC history catches up.\n`,
|
|
1205
|
+
);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (command === "sol-flow" || (command === "tx" && values[0] === "flow")) {
|
|
1209
|
+
const walletRef = need(flags, "wallet");
|
|
1210
|
+
const wallet = slrd.resolveWallet(walletRef);
|
|
1211
|
+
const address = wallet.address;
|
|
1212
|
+
const addressText = address.toBase58();
|
|
1213
|
+
const limit = Math.max(
|
|
1214
|
+
1,
|
|
1215
|
+
Math.min(1_000, Math.trunc(int(flags, "limit", 100) ?? 100)),
|
|
1216
|
+
);
|
|
1217
|
+
const sinceMs = duration(flags.get("since"), 30 * 60_000);
|
|
1218
|
+
const cutoff = Date.now() - sinceMs;
|
|
1219
|
+
const connection = slrd.connection();
|
|
1220
|
+
const signatures = await connection.getSignaturesForAddress(
|
|
1221
|
+
address,
|
|
1222
|
+
{ limit },
|
|
1223
|
+
"confirmed",
|
|
1224
|
+
);
|
|
1225
|
+
const recent = signatures.filter(
|
|
1226
|
+
(entry) => entry.blockTime == null || entry.blockTime * 1_000 >= cutoff,
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
type FlowRow = {
|
|
1230
|
+
signature: string;
|
|
1231
|
+
blockTime: number | null | undefined;
|
|
1232
|
+
err: unknown;
|
|
1233
|
+
nativeDeltaLamports: bigint | null;
|
|
1234
|
+
feeLamports: bigint;
|
|
1235
|
+
feePaidByWallet: boolean;
|
|
1236
|
+
wsolDeltaRaw: bigint;
|
|
1237
|
+
tokenDeltas: Array<{
|
|
1238
|
+
mint: string;
|
|
1239
|
+
decimals: number;
|
|
1240
|
+
deltaRaw: bigint;
|
|
1241
|
+
}>;
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
const rows: FlowRow[] = [];
|
|
1245
|
+
let nativeDeltaLamports = 0n;
|
|
1246
|
+
let nativeAvailable = 0;
|
|
1247
|
+
let walletFeeLamports = 0n;
|
|
1248
|
+
let wsolDeltaRaw = 0n;
|
|
1249
|
+
let failed = 0;
|
|
1250
|
+
const tokenTotals = new Map<
|
|
1251
|
+
string,
|
|
1252
|
+
{ decimals: number; deltaRaw: bigint }
|
|
1253
|
+
>();
|
|
1254
|
+
|
|
1255
|
+
for (const entry of recent) {
|
|
1256
|
+
try {
|
|
1257
|
+
const tx = await connection.getTransaction(entry.signature, {
|
|
1258
|
+
commitment: "confirmed",
|
|
1259
|
+
maxSupportedTransactionVersion: 0,
|
|
1260
|
+
});
|
|
1261
|
+
if (!tx?.meta) {
|
|
1262
|
+
rows.push({
|
|
1263
|
+
signature: entry.signature,
|
|
1264
|
+
blockTime: entry.blockTime,
|
|
1265
|
+
err: entry.err,
|
|
1266
|
+
nativeDeltaLamports: null,
|
|
1267
|
+
feeLamports: 0n,
|
|
1268
|
+
feePaidByWallet: false,
|
|
1269
|
+
wsolDeltaRaw: 0n,
|
|
1270
|
+
tokenDeltas: [],
|
|
1271
|
+
});
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const keys = txAccountKeys(tx);
|
|
1276
|
+
const walletIndex = keys.indexOf(addressText);
|
|
1277
|
+
let txNativeDelta: bigint | null = null;
|
|
1278
|
+
if (
|
|
1279
|
+
walletIndex >= 0 &&
|
|
1280
|
+
typeof tx.meta.preBalances?.[walletIndex] === "number" &&
|
|
1281
|
+
typeof tx.meta.postBalances?.[walletIndex] === "number"
|
|
1282
|
+
) {
|
|
1283
|
+
txNativeDelta =
|
|
1284
|
+
BigInt(Math.trunc(tx.meta.postBalances[walletIndex]!)) -
|
|
1285
|
+
BigInt(Math.trunc(tx.meta.preBalances[walletIndex]!));
|
|
1286
|
+
nativeDeltaLamports += txNativeDelta;
|
|
1287
|
+
nativeAvailable += 1;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const fee =
|
|
1291
|
+
typeof tx.meta.fee === "number" && Number.isFinite(tx.meta.fee)
|
|
1292
|
+
? BigInt(Math.trunc(tx.meta.fee))
|
|
1293
|
+
: 0n;
|
|
1294
|
+
const feePaidByWallet = keys[0] === addressText;
|
|
1295
|
+
if (feePaidByWallet) walletFeeLamports += fee;
|
|
1296
|
+
|
|
1297
|
+
const balances = txWalletTokenBalances(tx, addressText);
|
|
1298
|
+
const tokenDeltas: Array<{
|
|
1299
|
+
mint: string;
|
|
1300
|
+
decimals: number;
|
|
1301
|
+
deltaRaw: bigint;
|
|
1302
|
+
}> = [];
|
|
1303
|
+
let txWsol = 0n;
|
|
1304
|
+
for (const [mint, balance] of balances.entries()) {
|
|
1305
|
+
const deltaRaw = balance.post - balance.pre;
|
|
1306
|
+
if (deltaRaw === 0n) continue;
|
|
1307
|
+
tokenDeltas.push({ mint, decimals: balance.decimals, deltaRaw });
|
|
1308
|
+
const total = tokenTotals.get(mint) ?? {
|
|
1309
|
+
decimals: balance.decimals,
|
|
1310
|
+
deltaRaw: 0n,
|
|
1311
|
+
};
|
|
1312
|
+
total.decimals = balance.decimals;
|
|
1313
|
+
total.deltaRaw += deltaRaw;
|
|
1314
|
+
tokenTotals.set(mint, total);
|
|
1315
|
+
if (mint === NATIVE_SOL_MINT) {
|
|
1316
|
+
txWsol += deltaRaw;
|
|
1317
|
+
wsolDeltaRaw += deltaRaw;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
rows.push({
|
|
1322
|
+
signature: entry.signature,
|
|
1323
|
+
blockTime: entry.blockTime,
|
|
1324
|
+
err: entry.err,
|
|
1325
|
+
nativeDeltaLamports: txNativeDelta,
|
|
1326
|
+
feeLamports: fee,
|
|
1327
|
+
feePaidByWallet,
|
|
1328
|
+
wsolDeltaRaw: txWsol,
|
|
1329
|
+
tokenDeltas,
|
|
1330
|
+
});
|
|
1331
|
+
} catch {
|
|
1332
|
+
failed += 1;
|
|
1333
|
+
rows.push({
|
|
1334
|
+
signature: entry.signature,
|
|
1335
|
+
blockTime: entry.blockTime,
|
|
1336
|
+
err: entry.err,
|
|
1337
|
+
nativeDeltaLamports: null,
|
|
1338
|
+
feeLamports: 0n,
|
|
1339
|
+
feePaidByWallet: false,
|
|
1340
|
+
wsolDeltaRaw: 0n,
|
|
1341
|
+
tokenDeltas: [],
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const nonFeeNativeDelta = nativeDeltaLamports + walletFeeLamports;
|
|
1347
|
+
const nativePlusWsol = nativeDeltaLamports + wsolDeltaRaw;
|
|
1348
|
+
const tokenSummary = [...tokenTotals.entries()]
|
|
1349
|
+
.filter(([, value]) => value.deltaRaw !== 0n)
|
|
1350
|
+
.sort((left, right) => {
|
|
1351
|
+
const leftAbs =
|
|
1352
|
+
left[1].deltaRaw < 0n ? -left[1].deltaRaw : left[1].deltaRaw;
|
|
1353
|
+
const rightAbs =
|
|
1354
|
+
right[1].deltaRaw < 0n ? -right[1].deltaRaw : right[1].deltaRaw;
|
|
1355
|
+
return leftAbs === rightAbs
|
|
1356
|
+
? left[0].localeCompare(right[0])
|
|
1357
|
+
: leftAbs > rightAbs
|
|
1358
|
+
? -1
|
|
1359
|
+
: 1;
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
if (flags.has("json")) {
|
|
1363
|
+
emit(
|
|
1364
|
+
json({
|
|
1365
|
+
wallet: addressText,
|
|
1366
|
+
window: flags.get("since") ?? "30m",
|
|
1367
|
+
transactions: recent.length,
|
|
1368
|
+
nativeAvailable,
|
|
1369
|
+
nativeDeltaLamports,
|
|
1370
|
+
walletFeeLamports,
|
|
1371
|
+
nonFeeNativeDeltaLamports: nonFeeNativeDelta,
|
|
1372
|
+
wsolDeltaRaw,
|
|
1373
|
+
nativePlusWsolLamports: nativePlusWsol,
|
|
1374
|
+
tokenDeltas: tokenSummary.map(([mint, value]) => ({
|
|
1375
|
+
mint,
|
|
1376
|
+
...value,
|
|
1377
|
+
})),
|
|
1378
|
+
rows,
|
|
1379
|
+
failedLookups: failed,
|
|
1380
|
+
}) + "\n",
|
|
1381
|
+
);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
emit(
|
|
1386
|
+
`SOL FLOW @${wallet.row?.name ?? walletRef} ${addressText}\n` +
|
|
1387
|
+
`Window: ${flags.get("since") ?? "30m"} transactions=${recent.length} native-meta=${nativeAvailable} unavailable=${recent.length - nativeAvailable}\n` +
|
|
1388
|
+
`Native SOL delta: ${formatSignedRaw(nativeDeltaLamports, 9)} SOL\n` +
|
|
1389
|
+
`Network fees paid by wallet: -${formatRaw(walletFeeLamports, 9)} SOL\n` +
|
|
1390
|
+
`Native delta excluding fees: ${formatSignedRaw(nonFeeNativeDelta, 9)} SOL\n` +
|
|
1391
|
+
`WSOL token-account delta: ${formatSignedRaw(wsolDeltaRaw, 9)} WSOL\n` +
|
|
1392
|
+
`Native + WSOL wallet delta: ${formatSignedRaw(nativePlusWsol, 9)} SOL-like\n`,
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
if (tokenSummary.length > 0) {
|
|
1396
|
+
emit("\nNET SPL TOKEN DELTAS IN WALLET\n");
|
|
1397
|
+
for (const [mint, value] of tokenSummary) {
|
|
1398
|
+
const label =
|
|
1399
|
+
mint === NATIVE_SOL_MINT
|
|
1400
|
+
? "WSOL"
|
|
1401
|
+
: mint === CANONICAL_USDC_MINT
|
|
1402
|
+
? "USDC"
|
|
1403
|
+
: shortKey(mint);
|
|
1404
|
+
emit(
|
|
1405
|
+
` ${label.padEnd(12)} ${formatSignedRaw(value.deltaRaw, value.decimals).padStart(20)} ${mint}\n`,
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (!flags.has("summary-only")) {
|
|
1411
|
+
emit("\nPER TRANSACTION\n");
|
|
412
1412
|
emit(
|
|
413
|
-
|
|
1413
|
+
"TIME NATIVE Δ SOL FEE SOL WSOL Δ TOKEN DELTAS STATUS SIGNATURE\n",
|
|
414
1414
|
);
|
|
1415
|
+
for (const row of rows) {
|
|
1416
|
+
const time = row.blockTime
|
|
1417
|
+
? new Date(row.blockTime * 1_000).toISOString()
|
|
1418
|
+
: "-";
|
|
1419
|
+
const native =
|
|
1420
|
+
row.nativeDeltaLamports == null
|
|
1421
|
+
? "-"
|
|
1422
|
+
: formatSignedRaw(row.nativeDeltaLamports, 9);
|
|
1423
|
+
const fee = row.feePaidByWallet ? formatRaw(row.feeLamports, 9) : "0";
|
|
1424
|
+
const wsol = formatSignedRaw(row.wsolDeltaRaw, 9);
|
|
1425
|
+
const other =
|
|
1426
|
+
row.tokenDeltas
|
|
1427
|
+
.filter((delta) => delta.mint !== NATIVE_SOL_MINT)
|
|
1428
|
+
.slice(0, 2)
|
|
1429
|
+
.map(
|
|
1430
|
+
(delta) =>
|
|
1431
|
+
`${shortKey(delta.mint)} ${formatSignedRaw(delta.deltaRaw, delta.decimals)}`,
|
|
1432
|
+
)
|
|
1433
|
+
.join(", ") || "-";
|
|
1434
|
+
emit(
|
|
1435
|
+
`${time.padEnd(25)} ${native.padStart(13)} ${fee.padStart(10)} ${wsol.padStart(11)} ${other.padEnd(35)} ${(row.err ? "FAILED" : "OK").padEnd(7)} ${row.signature}\n`,
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
415
1438
|
}
|
|
1439
|
+
|
|
1440
|
+
emit(
|
|
1441
|
+
"\nNative SOL delta is a cash-flow number, not P&L: LP deposits, SOL->token swaps, account rent, and WSOL wrapping can all reduce it without being network fees.\n" +
|
|
1442
|
+
"Use the per-transaction rows to find the large negative native-SOL transactions; their token deltas show what the wallet received or deployed.\n",
|
|
1443
|
+
);
|
|
1444
|
+
if (failed > 0)
|
|
1445
|
+
process.stderr.write(
|
|
1446
|
+
`${OWL} ${failed} transaction lookup(s) failed; rerun after RPC history catches up.\n`,
|
|
1447
|
+
);
|
|
416
1448
|
return;
|
|
417
1449
|
}
|
|
418
1450
|
if (command === "deploy") {
|
|
@@ -622,6 +1654,464 @@ async function main() {
|
|
|
622
1654
|
);
|
|
623
1655
|
return;
|
|
624
1656
|
}
|
|
1657
|
+
if (command === "liquidate" && (values[0] ?? "tokens") === "tokens") {
|
|
1658
|
+
const except = csv(flags.get("except"));
|
|
1659
|
+
if (except.length === 0) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
"Usage: slrd liquidate tokens --except <token|mint>[,<token|mint>...] [--wallets <a,b,...>] [--simulate | --live]",
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
if (flags.has("simulate") && flags.has("live")) {
|
|
1665
|
+
throw new Error("Use either --simulate or --live, not both");
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const mode = flags.has("live")
|
|
1669
|
+
? "LIVE"
|
|
1670
|
+
: flags.has("simulate")
|
|
1671
|
+
? "SIMULATE"
|
|
1672
|
+
: "PLAN";
|
|
1673
|
+
|
|
1674
|
+
emit(`LIQUIDATE ${mode}\n`);
|
|
1675
|
+
for (const ref of except) {
|
|
1676
|
+
const mint = resolveTokenMintForPolicy(slrd, ref);
|
|
1677
|
+
emit(`PROTECT ${ref} -> ${mint}\n`);
|
|
1678
|
+
}
|
|
1679
|
+
emit(
|
|
1680
|
+
`RPC hard limit ${process.env.SLRD_RPC_MAX_RPS ?? "5"} req/s\n`,
|
|
1681
|
+
);
|
|
1682
|
+
|
|
1683
|
+
const showAction = (
|
|
1684
|
+
prefix: string,
|
|
1685
|
+
index: number,
|
|
1686
|
+
total: number,
|
|
1687
|
+
action: {
|
|
1688
|
+
walletName: string;
|
|
1689
|
+
mint: string;
|
|
1690
|
+
symbol: string | null;
|
|
1691
|
+
name: string | null;
|
|
1692
|
+
amountUi: string;
|
|
1693
|
+
kind: string;
|
|
1694
|
+
},
|
|
1695
|
+
) => {
|
|
1696
|
+
const label = action.symbol ?? action.name ?? shortKey(action.mint);
|
|
1697
|
+
emit(
|
|
1698
|
+
`${prefix} ${index}/${total} @${action.walletName} ` +
|
|
1699
|
+
`${action.kind === "unwrap-wsol" ? "WSOL" : label} ${action.amountUi}` +
|
|
1700
|
+
`${action.kind === "close-empty" ? " [close empty account]" : ""}\n`,
|
|
1701
|
+
);
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
const options = {
|
|
1705
|
+
except,
|
|
1706
|
+
walletRefs: csv(flags.get("wallets")),
|
|
1707
|
+
slippageBps: int(flags, "slippage-bps", 1500) ?? 1500,
|
|
1708
|
+
via: flags.get("sender") ?? "rpc",
|
|
1709
|
+
delayMs: int(flags, "delay-ms", 0) ?? 0,
|
|
1710
|
+
routeDelayMs: int(flags, "route-delay-ms", 0) ?? 0,
|
|
1711
|
+
portfolioConcurrency: 1,
|
|
1712
|
+
portfolioDelayMs: 0,
|
|
1713
|
+
jupiterFallback: !flags.has("no-jupiter"),
|
|
1714
|
+
onProgress: (event: any) => {
|
|
1715
|
+
if (event.stage === "portfolio-start") {
|
|
1716
|
+
emit("SCAN wallet token accounts...\n");
|
|
1717
|
+
} else if (event.stage === "portfolio-done") {
|
|
1718
|
+
emit(
|
|
1719
|
+
`SCAN ${event.wallets} wallets, ${event.holdings} holdings, ` +
|
|
1720
|
+
`${event.distinctMints} mints\n`,
|
|
1721
|
+
);
|
|
1722
|
+
} else if (event.stage === "route") {
|
|
1723
|
+
emit(
|
|
1724
|
+
`ROUTE ${event.index}/${event.total} ${shortKey(event.mint)}\n`,
|
|
1725
|
+
);
|
|
1726
|
+
} else if (event.stage === "action-start") {
|
|
1727
|
+
showAction(
|
|
1728
|
+
flags.has("simulate") ? "SIM " : "DO ",
|
|
1729
|
+
event.index,
|
|
1730
|
+
event.total,
|
|
1731
|
+
event.action,
|
|
1732
|
+
);
|
|
1733
|
+
} else if (event.stage === "action-done") {
|
|
1734
|
+
showAction("OK ", event.index, event.total, event.action);
|
|
1735
|
+
} else if (event.stage === "action-error") {
|
|
1736
|
+
showAction("FAIL", event.index, event.total, event.action);
|
|
1737
|
+
emit(` ${event.error}\n`);
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const plan = await planRegistryTokenLiquidation(slrd, options);
|
|
1743
|
+
emit(
|
|
1744
|
+
`PLAN native=${plan.totals.sell} jupiter=${plan.totals.jupiterSell} ` +
|
|
1745
|
+
`unwrap=${plan.totals.unwrapWsol} close-empty-now=${plan.totals.closeEmpty} ` +
|
|
1746
|
+
`keep=${plan.totals.keepProtected} unsupported=${plan.totals.skipUnsupported}\n`,
|
|
1747
|
+
);
|
|
1748
|
+
|
|
1749
|
+
if (!flags.has("simulate") && !flags.has("live")) {
|
|
1750
|
+
const actionable = plan.actions.filter(
|
|
1751
|
+
(action) =>
|
|
1752
|
+
action.kind === "sell" ||
|
|
1753
|
+
action.kind === "jupiter-sell" ||
|
|
1754
|
+
action.kind === "unwrap-wsol" ||
|
|
1755
|
+
action.kind === "close-empty",
|
|
1756
|
+
);
|
|
1757
|
+
for (const action of actionable) {
|
|
1758
|
+
const label = action.symbol ?? action.name ?? shortKey(action.mint);
|
|
1759
|
+
const verb =
|
|
1760
|
+
action.kind === "unwrap-wsol"
|
|
1761
|
+
? "UNWRAP"
|
|
1762
|
+
: action.kind === "close-empty"
|
|
1763
|
+
? "CLOSE"
|
|
1764
|
+
: action.kind === "jupiter-sell"
|
|
1765
|
+
? "JUPITER"
|
|
1766
|
+
: "SELL";
|
|
1767
|
+
emit(
|
|
1768
|
+
`${verb.padEnd(7)} @${action.walletName.padEnd(16)} ` +
|
|
1769
|
+
`${label.padEnd(14)} ${action.amountUi}` +
|
|
1770
|
+
`${action.venue ? ` ${action.venue}` : ""}\n`,
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
emit(
|
|
1775
|
+
`KEEP ${plan.totals.keepProtected} protected SLRD holding(s)\n` +
|
|
1776
|
+
`SKIP ${plan.totals.skipUnsupported} unsupported/no-liquidity holding(s)`,
|
|
1777
|
+
);
|
|
1778
|
+
if (plan.totals.skipUnsupported > 0) {
|
|
1779
|
+
emit(flags.has("verbose") ? "\n" : " (use --verbose to list)\n");
|
|
1780
|
+
if (flags.has("verbose")) {
|
|
1781
|
+
for (const action of plan.actions.filter(
|
|
1782
|
+
(item) => item.kind === "skip-unsupported",
|
|
1783
|
+
)) {
|
|
1784
|
+
const label =
|
|
1785
|
+
action.symbol ?? action.name ?? shortKey(action.mint);
|
|
1786
|
+
emit(
|
|
1787
|
+
`SKIP @${action.walletName.padEnd(16)} ` +
|
|
1788
|
+
`${label.padEnd(14)} ${action.amountUi} ${action.reason ?? "unsupported"}\n`,
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
emit("\n");
|
|
1794
|
+
}
|
|
1795
|
+
emit(
|
|
1796
|
+
`AUTO-CLOSE after successful liquidation, the command re-scans on-chain state and closes every unprotected token account that is actually zero.\n`,
|
|
1797
|
+
);
|
|
1798
|
+
emit(`\n${OWL} plan only. Use --simulate, then --live.\n`);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const results = flags.has("simulate")
|
|
1803
|
+
? await simulateRegistryTokenLiquidation(slrd, plan, options)
|
|
1804
|
+
: await executeRegistryTokenLiquidation(slrd, plan, options);
|
|
1805
|
+
|
|
1806
|
+
const failed = results.filter((result) => Boolean(result.error)).length;
|
|
1807
|
+
const succeeded = results.filter((result) => !result.error);
|
|
1808
|
+
const soldNative = succeeded.filter(
|
|
1809
|
+
(result) => result.action.kind === "sell",
|
|
1810
|
+
).length;
|
|
1811
|
+
const soldJupiter = succeeded.filter(
|
|
1812
|
+
(result) => result.action.kind === "jupiter-sell",
|
|
1813
|
+
).length;
|
|
1814
|
+
const unwrapped = succeeded.filter(
|
|
1815
|
+
(result) => result.action.kind === "unwrap-wsol",
|
|
1816
|
+
).length;
|
|
1817
|
+
const closedEmpty = succeeded.filter(
|
|
1818
|
+
(result) => result.action.kind === "close-empty",
|
|
1819
|
+
).length;
|
|
1820
|
+
const reclaimedRentLamports = succeeded
|
|
1821
|
+
.filter((result) => result.action.kind === "close-empty")
|
|
1822
|
+
.reduce((sum, result) => sum + (result.action.rentLamports ?? 0n), 0n);
|
|
1823
|
+
emit(
|
|
1824
|
+
`DONE ok=${succeeded.length} failed=${failed} ` +
|
|
1825
|
+
`native-sold=${soldNative} jupiter-sold=${soldJupiter} ` +
|
|
1826
|
+
`unwrapped=${unwrapped} closed-empty=${closedEmpty} ` +
|
|
1827
|
+
`reclaimed-rent≈${(Number(reclaimedRentLamports) / 1e9).toFixed(6)} SOL\n`,
|
|
1828
|
+
);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (command === "sweep" && values[0] === "sol") {
|
|
1833
|
+
const destinationInput = flags.get("to") ?? values[1];
|
|
1834
|
+
if (!destinationInput || destinationInput === "true") {
|
|
1835
|
+
throw new Error(
|
|
1836
|
+
"Usage: slrd sweep sol --to <contact|wallet|address> [--wallets <a,b,...>] [--exclude-group <group>] [--exclude-prefix <prefix>] [--keep <wallet=SOL,...>] [--keep-if-tokens <SOL> | --keep-if-token <token>=<SOL>] [--simulate | --live] [--json]",
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const destination = resolveDestinationRef(slrd, destinationInput);
|
|
1841
|
+
|
|
1842
|
+
const keepSolByWallet: Record<string, string> = {};
|
|
1843
|
+
const keep = flags.get("keep");
|
|
1844
|
+
if (keep && keep !== "true") {
|
|
1845
|
+
for (const entry of keep.split(",")) {
|
|
1846
|
+
const [wallet, amount, ...extra] = entry.split("=");
|
|
1847
|
+
if (!wallet?.trim() || !amount?.trim() || extra.length) {
|
|
1848
|
+
throw new Error(
|
|
1849
|
+
`Invalid --keep entry ${entry}; expected wallet=SOL`,
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
keepSolByWallet[wallet.trim()] = amount.trim();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const excludeGroups = csv(flags.get("exclude-group"));
|
|
1857
|
+
const excludePrefixes = csv(flags.get("exclude-prefix"));
|
|
1858
|
+
const includeWallets = csv(flags.get("wallets"));
|
|
1859
|
+
|
|
1860
|
+
const keepIfTokenRaw = flags.get("keep-if-token");
|
|
1861
|
+
let keepSolIfToken: { token: string; sol: string } | undefined;
|
|
1862
|
+
if (keepIfTokenRaw && keepIfTokenRaw !== "true") {
|
|
1863
|
+
const separator = keepIfTokenRaw.lastIndexOf("=");
|
|
1864
|
+
if (separator <= 0 || separator >= keepIfTokenRaw.length - 1) {
|
|
1865
|
+
throw new Error(
|
|
1866
|
+
"--keep-if-token expects <token|mint>=<SOL>, for example slrd=0.1",
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
keepSolIfToken = {
|
|
1870
|
+
token: keepIfTokenRaw.slice(0, separator).trim(),
|
|
1871
|
+
sol: keepIfTokenRaw.slice(separator + 1).trim(),
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
if (flags.get("keep-if-tokens") && keepSolIfToken) {
|
|
1875
|
+
throw new Error(
|
|
1876
|
+
"Use either --keep-if-tokens or --keep-if-token, not both",
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
const sweepMode = flags.has("live")
|
|
1881
|
+
? "LIVE"
|
|
1882
|
+
: flags.has("simulate")
|
|
1883
|
+
? "SIMULATE"
|
|
1884
|
+
: "PLAN";
|
|
1885
|
+
if (!flags.has("json")) {
|
|
1886
|
+
emit(`SWEEP ${sweepMode} to=${destination.address}\n`);
|
|
1887
|
+
emit(
|
|
1888
|
+
`RPC hard limit ${process.env.SLRD_RPC_MAX_RPS ?? "5"} req/s\n`,
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const options = {
|
|
1893
|
+
destination: destination.address,
|
|
1894
|
+
excludeGroups,
|
|
1895
|
+
excludePrefixes,
|
|
1896
|
+
includeWallets: includeWallets.length ? includeWallets : undefined,
|
|
1897
|
+
keepSolByWallet,
|
|
1898
|
+
defaultKeepSol: flags.get("default-keep-sol") ?? "0",
|
|
1899
|
+
keepSolIfTokens: flags.get("keep-if-tokens"),
|
|
1900
|
+
keepSolIfToken,
|
|
1901
|
+
tokenScanConcurrency: Math.max(
|
|
1902
|
+
1,
|
|
1903
|
+
Math.min(8, Math.trunc(int(flags, "rpc-concurrency", 1) ?? 1)),
|
|
1904
|
+
),
|
|
1905
|
+
tokenScanDelayMs: Math.max(
|
|
1906
|
+
0,
|
|
1907
|
+
Math.trunc(int(flags, "rpc-delay-ms", 75) ?? 75),
|
|
1908
|
+
),
|
|
1909
|
+
delayMs: int(flags, "delay-ms", 0) ?? 0,
|
|
1910
|
+
onProgress: flags.has("json")
|
|
1911
|
+
? undefined
|
|
1912
|
+
: (event: any) => {
|
|
1913
|
+
if (event.stage === "plan-start") {
|
|
1914
|
+
emit(`PLAN ${event.wallets} candidate wallets\n`);
|
|
1915
|
+
} else if (event.stage === "plan-ready") {
|
|
1916
|
+
emit(
|
|
1917
|
+
`PLAN ${event.sendWallets}/${event.wallets} will send, ` +
|
|
1918
|
+
`${formatRaw(event.totalSendLamports, 9)} SOL total\n`,
|
|
1919
|
+
);
|
|
1920
|
+
} else if (event.stage === "wallet-start") {
|
|
1921
|
+
emit(
|
|
1922
|
+
`SEND ${event.index}/${event.total} @${event.row.walletName} ` +
|
|
1923
|
+
`${formatRaw(event.row.sendLamports, 9)} SOL\n`,
|
|
1924
|
+
);
|
|
1925
|
+
} else if (event.stage === "wallet-done") {
|
|
1926
|
+
emit(
|
|
1927
|
+
`OK ${event.index}/${event.total} @${event.row.walletName}\n`,
|
|
1928
|
+
);
|
|
1929
|
+
} else if (event.stage === "wallet-error") {
|
|
1930
|
+
emit(
|
|
1931
|
+
`FAIL ${event.index}/${event.total} @${event.row.walletName} ` +
|
|
1932
|
+
`${event.error}\n`,
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
},
|
|
1936
|
+
};
|
|
1937
|
+
const plan = await planRegistrySolSweep(slrd, options);
|
|
1938
|
+
|
|
1939
|
+
const printablePlan = {
|
|
1940
|
+
mode: flags.has("live")
|
|
1941
|
+
? "live"
|
|
1942
|
+
: flags.has("simulate")
|
|
1943
|
+
? "simulate"
|
|
1944
|
+
: "plan",
|
|
1945
|
+
destination: {
|
|
1946
|
+
input: destination.input,
|
|
1947
|
+
address: plan.destination,
|
|
1948
|
+
contact: destination.contactName ?? null,
|
|
1949
|
+
wallet: destination.walletName ?? null,
|
|
1950
|
+
},
|
|
1951
|
+
totalSendSol: formatRaw(plan.totalSendLamports, 9),
|
|
1952
|
+
rows: plan.rows.map((row) => ({
|
|
1953
|
+
wallet: row.walletName,
|
|
1954
|
+
address: row.walletAddress,
|
|
1955
|
+
balanceSol: formatRaw(row.balanceLamports, 9),
|
|
1956
|
+
keepSol: formatRaw(row.keepLamports, 9),
|
|
1957
|
+
feeSol: formatRaw(row.feeLamports, 9),
|
|
1958
|
+
sendSol: formatRaw(row.sendLamports, 9),
|
|
1959
|
+
tokenHoldings: row.tokenHoldingCount,
|
|
1960
|
+
tokenScanComplete: row.tokenScanComplete,
|
|
1961
|
+
reserveReason: row.reserveReason ?? null,
|
|
1962
|
+
reserveTokenMint: row.reserveTokenMint ?? null,
|
|
1963
|
+
reserveTokenAmountRaw: row.reserveTokenAmountRaw?.toString() ?? null,
|
|
1964
|
+
skippedReason: row.skippedReason ?? null,
|
|
1965
|
+
})),
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
if (!flags.has("simulate") && !flags.has("live")) {
|
|
1969
|
+
if (flags.has("json")) {
|
|
1970
|
+
emit(json(printablePlan) + "\n");
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
emit(
|
|
1975
|
+
`SWEEP PLAN to=${plan.destination} ` +
|
|
1976
|
+
`send=${formatRaw(plan.totalSendLamports, 9)} SOL ` +
|
|
1977
|
+
`wallets=${plan.rows.filter((row) => row.sendLamports > 0n).length}/${plan.rows.length}\n`,
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1980
|
+
for (const row of plan.rows) {
|
|
1981
|
+
if (row.sendLamports <= 0n && !flags.has("show-skipped")) continue;
|
|
1982
|
+
emit(
|
|
1983
|
+
`${row.sendLamports > 0n ? "SEND" : "SKIP"} ` +
|
|
1984
|
+
`@${row.walletName.padEnd(18)} ` +
|
|
1985
|
+
`balance=${formatRaw(row.balanceLamports, 9).padStart(12)} ` +
|
|
1986
|
+
`keep=${formatRaw(row.keepLamports, 9).padStart(5)} ` +
|
|
1987
|
+
`send=${formatRaw(row.sendLamports, 9).padStart(12)}` +
|
|
1988
|
+
`${row.reserveReason === "specific-token" ? " keep-token=yes" : ""}` +
|
|
1989
|
+
`${row.skippedReason ? ` ${row.skippedReason}` : ""}\n`,
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
emit(
|
|
1994
|
+
`\n${OWL} plan only; no signer was decrypted and nothing was submitted. ` +
|
|
1995
|
+
`Use --json for full details, then --simulate or --live.\n`,
|
|
1996
|
+
);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (flags.has("simulate") && flags.has("live")) {
|
|
2001
|
+
throw new Error("Use either --simulate or --live, not both");
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
if (flags.has("simulate")) {
|
|
2005
|
+
const results = await simulateRegistrySolSweep(slrd, plan, options);
|
|
2006
|
+
if (flags.has("json")) {
|
|
2007
|
+
emit(json({ ...printablePlan, results }) + "\n");
|
|
2008
|
+
} else {
|
|
2009
|
+
const failed = results.filter(
|
|
2010
|
+
(result) => result.error || result.simulation?.success === false,
|
|
2011
|
+
).length;
|
|
2012
|
+
emit(
|
|
2013
|
+
`SWEEP SIMULATION attempted=${results.length} ` +
|
|
2014
|
+
`ok=${results.length - failed} failed=${failed} ` +
|
|
2015
|
+
`to=${plan.destination}\n`,
|
|
2016
|
+
);
|
|
2017
|
+
for (const result of results) {
|
|
2018
|
+
if (!result.error && result.simulation?.success !== false) continue;
|
|
2019
|
+
emit(
|
|
2020
|
+
`FAIL @${result.row.walletName} ` +
|
|
2021
|
+
`${result.error ?? "simulation failed"}\n`,
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const receipts = await executeRegistrySolSweep(slrd, plan, options);
|
|
2029
|
+
if (flags.has("json")) {
|
|
2030
|
+
emit(json({ ...printablePlan, receipts }) + "\n");
|
|
2031
|
+
} else {
|
|
2032
|
+
const failed = receipts.filter((result) =>
|
|
2033
|
+
Boolean(result.error),
|
|
2034
|
+
).length;
|
|
2035
|
+
emit(
|
|
2036
|
+
`SWEEP LIVE attempted=${receipts.length} ` +
|
|
2037
|
+
`ok=${receipts.length - failed} failed=${failed} ` +
|
|
2038
|
+
`to=${plan.destination}\n`,
|
|
2039
|
+
);
|
|
2040
|
+
for (const result of receipts) {
|
|
2041
|
+
if (!result.error) continue;
|
|
2042
|
+
emit(`FAIL @${result.row.walletName} ${result.error}\n`);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (command === "transfer" || command === "send-sol") {
|
|
2049
|
+
const recipientInput = values[0] === "sol" ? values[1] : values[0];
|
|
2050
|
+
if (!recipientInput) {
|
|
2051
|
+
throw new Error(
|
|
2052
|
+
"Usage: slrd transfer <contact|wallet|address> --wallet <wallet> --sol <amount> [--sender rpc|helius|jito] [--simulate-only]",
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
const recipient = resolveDestinationRef(slrd, recipientInput);
|
|
2057
|
+
const wallet = need(flags, "wallet");
|
|
2058
|
+
const amount = need(flags, "sol");
|
|
2059
|
+
const via = flags.get("sender") ?? "rpc";
|
|
2060
|
+
|
|
2061
|
+
const cuLimit = Number(flags.get("cu-limit") ?? "10000");
|
|
2062
|
+
const priorityMicroLamports = Number(
|
|
2063
|
+
flags.get("priority-micro-lamports") ?? "0",
|
|
2064
|
+
);
|
|
2065
|
+
if (!Number.isInteger(cuLimit) || cuLimit <= 0)
|
|
2066
|
+
throw new Error("--cu-limit must be a positive integer");
|
|
2067
|
+
if (
|
|
2068
|
+
!Number.isInteger(priorityMicroLamports) ||
|
|
2069
|
+
priorityMicroLamports < 0
|
|
2070
|
+
) {
|
|
2071
|
+
throw new Error(
|
|
2072
|
+
"--priority-micro-lamports must be a non-negative integer",
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const composer = slrd
|
|
2077
|
+
.tx(wallet)
|
|
2078
|
+
.transferSol(recipient.address, sol(amount))
|
|
2079
|
+
.priorityFee({
|
|
2080
|
+
cuLimit,
|
|
2081
|
+
microLamports: priorityMicroLamports,
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
if (flags.has("simulate-only")) {
|
|
2085
|
+
const plan = await composer.build();
|
|
2086
|
+
const result = await slrd.simulatePlan(plan);
|
|
2087
|
+
emit(
|
|
2088
|
+
json({
|
|
2089
|
+
mode: "simulation",
|
|
2090
|
+
wallet,
|
|
2091
|
+
recipient,
|
|
2092
|
+
sol: amount,
|
|
2093
|
+
result,
|
|
2094
|
+
}) + "\n",
|
|
2095
|
+
);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
const receipt = await composer.send({
|
|
2100
|
+
via,
|
|
2101
|
+
kind: "transfer-sol",
|
|
2102
|
+
skipSimulation: flags.has("skip-simulation"),
|
|
2103
|
+
skipPreflight:
|
|
2104
|
+
flags.has("skip-preflight") || flags.has("skip-simulation"),
|
|
2105
|
+
});
|
|
2106
|
+
emit(
|
|
2107
|
+
json({
|
|
2108
|
+
...receipt,
|
|
2109
|
+
recipient,
|
|
2110
|
+
}) + "\n",
|
|
2111
|
+
);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
625
2115
|
if (command === "buy") {
|
|
626
2116
|
const token = values[0];
|
|
627
2117
|
if (!token)
|
|
@@ -730,6 +2220,104 @@ async function main() {
|
|
|
730
2220
|
skipPreflight:
|
|
731
2221
|
flags.has("skip-preflight") || flags.has("skip-simulation"),
|
|
732
2222
|
};
|
|
2223
|
+
|
|
2224
|
+
const multiWallet = targets.refs.length > 1;
|
|
2225
|
+
const failFast = flags.has("fail-fast");
|
|
2226
|
+
|
|
2227
|
+
// Multi-wallet/group unwrap is resilient by default. Do not let one
|
|
2228
|
+
// undecryptable wallet prevent unrelated wallets from recovering WSOL.
|
|
2229
|
+
// With --ignore-missing, probe the WSOL ATA using only the public wallet
|
|
2230
|
+
// address BEFORE asking the vault to decrypt the signer.
|
|
2231
|
+
if (multiWallet && !failFast) {
|
|
2232
|
+
const results: Array<Record<string, unknown>> = [];
|
|
2233
|
+
|
|
2234
|
+
for (const walletRef of targets.refs) {
|
|
2235
|
+
const resolved = slrd.resolveWallet(walletRef);
|
|
2236
|
+
const address = resolved.address.toBase58();
|
|
2237
|
+
const label = resolved.row?.name ? `@${resolved.row.name}` : address;
|
|
2238
|
+
|
|
2239
|
+
if (unwrapOptions.skipMissing) {
|
|
2240
|
+
const wsolAccount = wrappedSolAta(resolved.address);
|
|
2241
|
+
const account = await slrd
|
|
2242
|
+
.connection()
|
|
2243
|
+
.getAccountInfo(wsolAccount, "confirmed");
|
|
2244
|
+
|
|
2245
|
+
if (!account) {
|
|
2246
|
+
results.push({
|
|
2247
|
+
wallet: walletRef,
|
|
2248
|
+
address,
|
|
2249
|
+
ok: true,
|
|
2250
|
+
skipped: "missing-wsol",
|
|
2251
|
+
wsolAccount: wsolAccount.toBase58(),
|
|
2252
|
+
});
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
try {
|
|
2258
|
+
if (flags.has("simulate-only")) {
|
|
2259
|
+
const plan = await slrd
|
|
2260
|
+
.tx(walletRef)
|
|
2261
|
+
.unwrapWsol({ skipMissing: unwrapOptions.skipMissing })
|
|
2262
|
+
.build();
|
|
2263
|
+
const simulation = await slrd.simulatePlan(plan);
|
|
2264
|
+
results.push({
|
|
2265
|
+
wallet: walletRef,
|
|
2266
|
+
address,
|
|
2267
|
+
ok: simulation.success,
|
|
2268
|
+
mode: "simulation",
|
|
2269
|
+
simulation,
|
|
2270
|
+
});
|
|
2271
|
+
} else {
|
|
2272
|
+
const receipt = await slrd.unwrapWsol(walletRef, unwrapOptions);
|
|
2273
|
+
results.push({
|
|
2274
|
+
wallet: walletRef,
|
|
2275
|
+
address,
|
|
2276
|
+
ok: true,
|
|
2277
|
+
receipt,
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
const message =
|
|
2282
|
+
error instanceof Error ? error.message : String(error);
|
|
2283
|
+
results.push({
|
|
2284
|
+
wallet: walletRef,
|
|
2285
|
+
address,
|
|
2286
|
+
ok: false,
|
|
2287
|
+
error: message,
|
|
2288
|
+
});
|
|
2289
|
+
process.stderr.write(
|
|
2290
|
+
`${OWL} unwrap-wsol ${label} (${address}) failed: ${message}\n`,
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const succeeded = results.filter(
|
|
2296
|
+
(row) => row.ok === true && !row.skipped,
|
|
2297
|
+
).length;
|
|
2298
|
+
const skipped = results.filter((row) => Boolean(row.skipped)).length;
|
|
2299
|
+
const failed = results.filter((row) => row.ok === false).length;
|
|
2300
|
+
|
|
2301
|
+
emit(
|
|
2302
|
+
json({
|
|
2303
|
+
mode: flags.has("simulate-only")
|
|
2304
|
+
? "wallet-by-wallet-simulation"
|
|
2305
|
+
: "wallet-by-wallet",
|
|
2306
|
+
target: targets,
|
|
2307
|
+
summary: {
|
|
2308
|
+
total: results.length,
|
|
2309
|
+
succeeded,
|
|
2310
|
+
skipped,
|
|
2311
|
+
failed,
|
|
2312
|
+
},
|
|
2313
|
+
results,
|
|
2314
|
+
}) + "\n",
|
|
2315
|
+
);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Single-wallet unwrap remains fail-fast. --fail-fast also preserves the
|
|
2320
|
+
// old batch behavior for callers that explicitly want all-or-nothing.
|
|
733
2321
|
if (flags.has("simulate-only")) {
|
|
734
2322
|
const plans =
|
|
735
2323
|
targets.refs.length === 1
|
|
@@ -752,6 +2340,7 @@ async function main() {
|
|
|
752
2340
|
emit(json({ mode: "simulation", target: targets, results }) + "\n");
|
|
753
2341
|
return;
|
|
754
2342
|
}
|
|
2343
|
+
|
|
755
2344
|
const receipts =
|
|
756
2345
|
targets.refs.length === 1
|
|
757
2346
|
? await slrd.unwrapWsol(targets.refs[0]!, unwrapOptions)
|