@bankr/cli 0.1.0-beta.6 → 0.1.0

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.
Files changed (66) hide show
  1. package/README.md +27 -1
  2. package/dist/cli.js +222 -4
  3. package/dist/commands/balances.d.ts +5 -0
  4. package/dist/commands/balances.js +113 -0
  5. package/dist/commands/fees.d.ts +18 -0
  6. package/dist/commands/fees.js +793 -0
  7. package/dist/commands/launch.d.ts +13 -0
  8. package/dist/commands/launch.js +174 -0
  9. package/dist/commands/llm.d.ts +11 -0
  10. package/dist/commands/llm.js +319 -3
  11. package/dist/commands/login.d.ts +9 -0
  12. package/dist/commands/login.js +464 -147
  13. package/dist/commands/profile.d.ts +26 -0
  14. package/dist/commands/profile.js +183 -0
  15. package/dist/commands/prompt.js +5 -0
  16. package/dist/commands/sign.js +3 -0
  17. package/dist/commands/sounds.d.ts +12 -0
  18. package/dist/commands/sounds.js +262 -0
  19. package/dist/commands/submit.js +14 -4
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +1 -1
  22. package/dist/lib/api.d.ts +213 -0
  23. package/dist/lib/api.js +177 -3
  24. package/dist/lib/cesp/engine.d.ts +13 -0
  25. package/dist/lib/cesp/engine.js +132 -0
  26. package/dist/lib/cesp/player.d.ts +6 -0
  27. package/dist/lib/cesp/player.js +50 -0
  28. package/dist/lib/cesp/types.d.ts +38 -0
  29. package/dist/lib/cesp/types.js +2 -0
  30. package/dist/lib/config.d.ts +4 -0
  31. package/dist/lib/config.js +1 -0
  32. package/dist/lib/output.d.ts +1 -0
  33. package/dist/lib/output.js +3 -0
  34. package/package.json +4 -2
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/cli.js.map +0 -1
  37. package/dist/commands/cancel.d.ts.map +0 -1
  38. package/dist/commands/cancel.js.map +0 -1
  39. package/dist/commands/capabilities.d.ts.map +0 -1
  40. package/dist/commands/capabilities.js.map +0 -1
  41. package/dist/commands/config.d.ts.map +0 -1
  42. package/dist/commands/config.js.map +0 -1
  43. package/dist/commands/llm.d.ts.map +0 -1
  44. package/dist/commands/llm.js.map +0 -1
  45. package/dist/commands/login.d.ts.map +0 -1
  46. package/dist/commands/login.js.map +0 -1
  47. package/dist/commands/logout.d.ts.map +0 -1
  48. package/dist/commands/logout.js.map +0 -1
  49. package/dist/commands/prompt.d.ts.map +0 -1
  50. package/dist/commands/prompt.js.map +0 -1
  51. package/dist/commands/sign.d.ts.map +0 -1
  52. package/dist/commands/sign.js.map +0 -1
  53. package/dist/commands/status.d.ts.map +0 -1
  54. package/dist/commands/status.js.map +0 -1
  55. package/dist/commands/submit.d.ts.map +0 -1
  56. package/dist/commands/submit.js.map +0 -1
  57. package/dist/commands/whoami.d.ts.map +0 -1
  58. package/dist/commands/whoami.js.map +0 -1
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/lib/api.d.ts.map +0 -1
  62. package/dist/lib/api.js.map +0 -1
  63. package/dist/lib/config.d.ts.map +0 -1
  64. package/dist/lib/config.js.map +0 -1
  65. package/dist/lib/output.d.ts.map +0 -1
  66. package/dist/lib/output.js.map +0 -1
@@ -0,0 +1,793 @@
1
+ import chalk from "chalk";
2
+ import { confirm, password, checkbox } from "@inquirer/prompts";
3
+ import { isAddress } from "viem";
4
+ import { getCreatorFees, getBeneficiaryFeesStreaming, getTokenCreatorFees, getUserInfo, buildDopplerClaimTx, submit, } from "../lib/api.js";
5
+ import { emitCESP } from "../lib/cesp/engine.js";
6
+ import { requireApiKey } from "../lib/config.js";
7
+ import * as output from "../lib/output.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Constants
10
+ // ---------------------------------------------------------------------------
11
+ const BRAND = chalk.hex("#FF613D");
12
+ const BRAND_BOLD = BRAND.bold;
13
+ const GREEN = chalk.green;
14
+ const BRIGHT_GREEN = chalk.greenBright;
15
+ const CYAN = chalk.cyan;
16
+ const BRIGHT_CYAN = chalk.cyanBright;
17
+ const YELLOW = chalk.yellowBright;
18
+ const MAGENTA = chalk.magentaBright;
19
+ const DIM = chalk.dim;
20
+ const BOLD = chalk.bold;
21
+ const WHITE = chalk.whiteBright;
22
+ const CHART_HEIGHT = 16;
23
+ const CHART_WIDTH = 58;
24
+ const MIN_DAYS = 1;
25
+ const MAX_DAYS = 90;
26
+ const DEFAULT_DAYS = 30;
27
+ // ---------------------------------------------------------------------------
28
+ // Chart helpers (ported from development/scripts/daily-usdc-earnings.ts)
29
+ // ---------------------------------------------------------------------------
30
+ const SPARK_CHARS = [
31
+ "\u2581",
32
+ "\u2582",
33
+ "\u2583",
34
+ "\u2584",
35
+ "\u2585",
36
+ "\u2586",
37
+ "\u2587",
38
+ "\u2588",
39
+ ];
40
+ function sparkline(values) {
41
+ if (values.length === 0)
42
+ return "";
43
+ const max = Math.max(...values);
44
+ const min = Math.min(...values);
45
+ const range = max - min || 1;
46
+ return values
47
+ .map((v) => {
48
+ const idx = Math.round(((v - min) / range) * (SPARK_CHARS.length - 1));
49
+ return SPARK_CHARS[idx];
50
+ })
51
+ .join("");
52
+ }
53
+ function colorForValue(value, max) {
54
+ const ratio = max > 0 ? value / max : 0;
55
+ if (ratio >= 0.75)
56
+ return BRIGHT_GREEN;
57
+ if (ratio >= 0.5)
58
+ return GREEN;
59
+ if (ratio >= 0.25)
60
+ return CYAN;
61
+ return DIM;
62
+ }
63
+ function sectionBox(title, boxWidth) {
64
+ const top = BRAND_BOLD("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
65
+ const pad = Math.max(0, boxWidth - 2 - title.length);
66
+ const mid = `${BRAND_BOLD("\u2551")} ${BOLD(WHITE(title))}${" ".repeat(pad)} ${BRAND_BOLD("\u2551")}`;
67
+ const bot = BRAND_BOLD("\u255a" + "\u2550".repeat(boxWidth) + "\u255d");
68
+ return [top, mid, bot];
69
+ }
70
+ function drawChart(dailyData) {
71
+ if (dailyData.length === 0)
72
+ return " No data to chart.";
73
+ const values = dailyData.map((d) => d.amount);
74
+ const max = Math.max(...values);
75
+ const lines = [];
76
+ const boxWidth = CHART_WIDTH + 14;
77
+ lines.push("");
78
+ for (const l of sectionBox("DAILY FEE EARNINGS (WETH)", boxWidth)) {
79
+ lines.push(` ${l}`);
80
+ }
81
+ lines.push("");
82
+ const chartRows = [];
83
+ for (let row = CHART_HEIGHT; row >= 0; row--) {
84
+ const label = row === CHART_HEIGHT
85
+ ? max.toFixed(4)
86
+ : row === 0
87
+ ? "0"
88
+ : row === Math.floor(CHART_HEIGHT / 2)
89
+ ? (max / 2).toFixed(4)
90
+ : "";
91
+ const paddedLabel = label.padStart(12);
92
+ // Sample data to fit chart width
93
+ const step = Math.max(1, Math.floor(dailyData.length / CHART_WIDTH));
94
+ const sampledValues = [];
95
+ for (let i = 0; i < dailyData.length; i += step) {
96
+ sampledValues.push(values[i]);
97
+ }
98
+ while (sampledValues.length < CHART_WIDTH) {
99
+ sampledValues.push(0);
100
+ }
101
+ let rowStr = "";
102
+ for (let col = 0; col < Math.min(sampledValues.length, CHART_WIDTH); col++) {
103
+ const val = sampledValues[col];
104
+ const barHeight = max > 0 ? (val / max) * CHART_HEIGHT : 0;
105
+ const color = colorForValue(val, max);
106
+ if (barHeight >= row && row > 0) {
107
+ if (barHeight - row < 1 && barHeight >= row) {
108
+ rowStr += color("\u2584");
109
+ }
110
+ else {
111
+ rowStr += color("\u2588");
112
+ }
113
+ }
114
+ else if (row === 0) {
115
+ rowStr += DIM("\u2500");
116
+ }
117
+ else {
118
+ rowStr += " ";
119
+ }
120
+ }
121
+ const yLabel = DIM(paddedLabel);
122
+ const separator = row === 0 ? DIM("\u253c") : DIM("\u2502");
123
+ chartRows.push(` ${yLabel} ${separator}${rowStr}`);
124
+ }
125
+ lines.push(...chartRows);
126
+ // X-axis labels
127
+ const xAxisPadding = " ".repeat(14);
128
+ if (dailyData.length > 0) {
129
+ const first = dailyData[0].date.slice(5);
130
+ const last = dailyData[dailyData.length - 1].date.slice(5);
131
+ const mid = dailyData[Math.floor(dailyData.length / 2)].date.slice(5);
132
+ const gap1 = Math.floor(CHART_WIDTH / 2) - first.length;
133
+ const gap2 = CHART_WIDTH - Math.floor(CHART_WIDTH / 2) - mid.length;
134
+ lines.push(` ${xAxisPadding} ${DIM(first)}${" ".repeat(Math.max(1, gap1))}${DIM(mid)}${" ".repeat(Math.max(1, gap2))}${DIM(last)}`);
135
+ }
136
+ return lines.join("\n");
137
+ }
138
+ function drawTable(dailyData, total) {
139
+ const lines = [];
140
+ const nonZero = dailyData.filter((d) => d.amount > 0);
141
+ if (nonZero.length === 0)
142
+ return "";
143
+ const max = Math.max(...nonZero.map((d) => d.amount));
144
+ // Column inner widths (content between │ borders, including padding spaces)
145
+ const col1 = 14; // " YYYY-MM-DD "
146
+ const col2 = 16; // " XX.XXXXXX "
147
+ const barMax = 30;
148
+ const pctWidth = 6; // "XX.X%" padded
149
+ const col3 = 1 + barMax + 1 + pctWidth + 1; // " bar pct% " = 39
150
+ const hr = (l, m, r) => YELLOW(l +
151
+ "\u2500".repeat(col1) +
152
+ m +
153
+ "\u2500".repeat(col2) +
154
+ m +
155
+ "\u2500".repeat(col3) +
156
+ r);
157
+ lines.push("");
158
+ lines.push(` ${hr("\u250c", "\u252c", "\u2510")}`);
159
+ // Header row — pad each cell to its column width
160
+ const h1 = (" Date" + " ".repeat(col1)).slice(0, col1);
161
+ const h2 = (" WETH Earned" + " ".repeat(col2)).slice(0, col2);
162
+ const h3 = (" Distribution" + " ".repeat(col3)).slice(0, col3);
163
+ lines.push(` ${YELLOW("\u2502")}${BOLD(WHITE(h1))}${YELLOW("\u2502")}${BOLD(WHITE(h2))}${YELLOW("\u2502")}${BOLD(WHITE(h3))}${YELLOW("\u2502")}`);
164
+ lines.push(` ${hr("\u251c", "\u253c", "\u2524")}`);
165
+ for (const { date, amount } of nonZero) {
166
+ const barWidth = max > 0 ? Math.max(1, Math.round((amount / max) * barMax)) : 0;
167
+ const color = colorForValue(amount, max);
168
+ const bar = color("\u2588".repeat(barWidth));
169
+ const pctStr = (total > 0 ? ((amount / total) * 100).toFixed(1) : "0.0") + "%";
170
+ const amountStr = amount.toFixed(6);
171
+ // Col1: " date " (14 chars)
172
+ const c1 = ` ${date}` + " ".repeat(Math.max(0, col1 - 1 - date.length));
173
+ // Col2: " amountPadded " (16 chars)
174
+ const c2 = ` ${amountStr.padStart(14)} `;
175
+ // Col3: " bar spaces pct% " (col3 chars)
176
+ const barArea = barWidth + Math.max(0, barMax - barWidth); // always barMax
177
+ const c3Pad = Math.max(0, col3 - 1 - barArea - 1 - pctStr.length - 1);
178
+ const c3 = ` ${bar}${" ".repeat(Math.max(0, barMax - barWidth))} ${DIM(pctStr)}${" ".repeat(c3Pad)} `;
179
+ lines.push(` ${YELLOW("\u2502")}${CYAN(c1)}${YELLOW("\u2502")}${BRIGHT_GREEN(c2)}${YELLOW("\u2502")}${c3}${YELLOW("\u2502")}`);
180
+ }
181
+ lines.push(` ${hr("\u2514", "\u2534", "\u2518")}`);
182
+ return lines.join("\n");
183
+ }
184
+ function statCard(title, value, subtitle, color) {
185
+ const inner = 22;
186
+ const top = color("\u250c" + "\u2500".repeat(inner) + "\u2510");
187
+ const titlePad = Math.max(0, inner - 1 - title.length);
188
+ const titleLine = `${color("\u2502")} ${DIM(title)}${" ".repeat(titlePad)}${color("\u2502")}`;
189
+ const valuePad = Math.max(0, inner - 1 - value.length);
190
+ const valueLine = `${color("\u2502")} ${BOLD(WHITE(value))}${" ".repeat(valuePad)}${color("\u2502")}`;
191
+ const subPad = Math.max(0, inner - 1 - subtitle.length);
192
+ const subLine = `${color("\u2502")} ${DIM(subtitle)}${" ".repeat(subPad)}${color("\u2502")}`;
193
+ const bottom = color("\u2514" + "\u2500".repeat(inner) + "\u2518");
194
+ return [top, titleLine, valueLine, subLine, bottom];
195
+ }
196
+ function renderStatRow(cards) {
197
+ const lines = [];
198
+ const height = cards[0].length;
199
+ for (let i = 0; i < height; i++) {
200
+ lines.push(" " + cards.map((c) => c[i]).join(" "));
201
+ }
202
+ return lines.join("\n");
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // Rendering
206
+ // ---------------------------------------------------------------------------
207
+ function renderDashboard(data) {
208
+ // Banner
209
+ const bannerTitle = "B A N K R \u00b7 F E E D A S H B O A R D";
210
+ const bannerBox = sectionBox(bannerTitle, bannerTitle.length + 4);
211
+ for (const l of bannerBox) {
212
+ console.log(` ${l}`);
213
+ }
214
+ console.log(`${DIM(` Wallet: ${data.address}`)}`);
215
+ console.log(`${DIM(` Chain: Base \u00b7 Tokens: ${data.tokens.length} \u00b7 Period: ${data.days} days`)}`);
216
+ if (data.tokens.length === 0) {
217
+ console.log(`\n ${DIM("No tokens found where this wallet is a creator beneficiary.")}`);
218
+ console.log("");
219
+ return;
220
+ }
221
+ // Claimable summary cards
222
+ console.log("");
223
+ console.log(renderStatRow([
224
+ statCard("CLAIMABLE WETH", data.totals.claimableWeth, "pending", MAGENTA),
225
+ statCard("CLAIMED WETH", data.totals.claimedWeth, `${data.totals.claimCount} claims (${data.days}d)`, BRIGHT_CYAN),
226
+ ]));
227
+ // Daily chart
228
+ const dailyData = data.dailyEarnings.map((d) => ({
229
+ date: d.date,
230
+ amount: Number(d.weth),
231
+ }));
232
+ const amounts = dailyData.map((d) => d.amount);
233
+ const total = amounts.reduce((a, b) => a + b, 0);
234
+ if (total > 0) {
235
+ console.log(drawChart(dailyData));
236
+ // Stats row
237
+ const avg = amounts.length > 0 ? total / amounts.length : 0;
238
+ const maxDay = dailyData.reduce((best, d) => (d.amount > best.amount ? d : best), dailyData[0]);
239
+ const activeDays = amounts.filter((a) => a > 0).length;
240
+ const streak = (() => {
241
+ let s = 0;
242
+ for (let i = amounts.length - 1; i >= 0; i--) {
243
+ if (amounts[i] > 0)
244
+ s++;
245
+ else
246
+ break;
247
+ }
248
+ return s;
249
+ })();
250
+ console.log("");
251
+ console.log(renderStatRow([
252
+ statCard("TOTAL EARNED", total.toFixed(6), `${data.days} days`, MAGENTA),
253
+ statCard("DAILY AVERAGE", avg.toFixed(6), "per day", BRIGHT_CYAN),
254
+ statCard("BEST DAY", maxDay.amount.toFixed(6), maxDay.date, YELLOW),
255
+ ]));
256
+ console.log("");
257
+ console.log(renderStatRow([
258
+ statCard("ACTIVE DAYS", `${activeDays} / ${data.days}`, "with earnings", GREEN),
259
+ statCard("CURRENT STREAK", `${streak} day${streak !== 1 ? "s" : ""}`, "consecutive", MAGENTA),
260
+ ]));
261
+ // Sparkline
262
+ console.log("");
263
+ console.log(` ${DIM("Sparkline:")} ${BRIGHT_GREEN(sparkline(amounts))}`);
264
+ // Table
265
+ console.log(drawTable(dailyData, total));
266
+ // Projections
267
+ const projectedMonthly = avg * 30;
268
+ const projectedYearly = avg * 365;
269
+ console.log("");
270
+ console.log(` ${BRAND_BOLD("Projections (based on daily avg):")}`);
271
+ console.log(` ${DIM("Monthly:")} ${BOLD(BRIGHT_GREEN(projectedMonthly.toFixed(6) + " WETH"))}`);
272
+ console.log(` ${DIM("Yearly:")} ${BOLD(BRIGHT_GREEN(projectedYearly.toFixed(4) + " WETH"))}`);
273
+ }
274
+ // Per-token breakdown
275
+ console.log("");
276
+ console.log(` ${BRAND_BOLD("\u2550".repeat(62))}`);
277
+ console.log(` ${BRAND_BOLD("PER-TOKEN BREAKDOWN")}`);
278
+ console.log(` ${BRAND_BOLD("\u2550".repeat(62))}`);
279
+ console.log("");
280
+ for (const token of data.tokens) {
281
+ const wethIsToken0 = token.token0Label === "WETH";
282
+ const claimableWeth = wethIsToken0
283
+ ? token.claimable.token0
284
+ : token.claimable.token1;
285
+ const claimableToken = wethIsToken0
286
+ ? token.claimable.token1
287
+ : token.claimable.token0;
288
+ const claimedWeth = wethIsToken0
289
+ ? token.claimed.token0
290
+ : token.claimed.token1;
291
+ const claimedToken = wethIsToken0
292
+ ? token.claimed.token1
293
+ : token.claimed.token0;
294
+ const tokenSymbol = wethIsToken0 ? token.token1Label : token.token0Label;
295
+ const sourceLabel = token.source === "clanker" ? DIM(" [Clanker]") : DIM(" [Doppler]");
296
+ console.log(` ${BOLD(WHITE(token.name))} ${DIM("($" + token.symbol + ")")}${sourceLabel}`);
297
+ const shareInfo = token.source === "clanker"
298
+ ? `${DIM("Creator")} ${DIM("\u00b7 Token:")} ${DIM(token.tokenAddress)}`
299
+ : `${DIM("Share:")} ${token.share} ${DIM("\u00b7 Token:")} ${DIM(token.tokenAddress)}`;
300
+ console.log(` ${shareInfo}`);
301
+ console.log(` ${DIM("Claimable:")} ${BRIGHT_GREEN(claimableWeth.padStart(12))} WETH ${DIM("|")} ${BRIGHT_GREEN(claimableToken.padStart(18))} ${tokenSymbol}`);
302
+ console.log(` ${DIM("Claimed:")} ${WHITE(claimedWeth.padStart(12))} WETH ${DIM("|")} ${WHITE(claimedToken.padStart(18))} ${tokenSymbol} ${DIM(`(${token.claimed.count} claims)`)}`);
303
+ console.log("");
304
+ }
305
+ console.log(` ${BRAND_BOLD("\u2550".repeat(62))}`);
306
+ console.log("");
307
+ }
308
+ // Subcommand names that Commander may accidentally pass as the address arg
309
+ const FEES_SUBCOMMANDS = ["claim", "claim-wallet"];
310
+ export async function feesCommand(address, opts) {
311
+ // Commander may route "bankr fees claim-wallet" here with address="claim-wallet"
312
+ // instead of to the subcommand. Bail out and let Commander re-route.
313
+ if (address && FEES_SUBCOMMANDS.includes(address)) {
314
+ return;
315
+ }
316
+ // Validate days
317
+ let days;
318
+ if (opts.days) {
319
+ days = parseInt(opts.days, 10);
320
+ if (isNaN(days) || days < MIN_DAYS || days > MAX_DAYS) {
321
+ output.error(`Days must be between ${MIN_DAYS} and ${MAX_DAYS}`);
322
+ process.exit(1);
323
+ }
324
+ }
325
+ // --token flag: look up fees by token address (no wallet needed)
326
+ if (opts.token) {
327
+ const spin = output.spinner("Fetching token creator fee data...");
328
+ try {
329
+ const data = await getTokenCreatorFees(opts.token, days);
330
+ spin.succeed("Fee data loaded");
331
+ emitCESP("task.complete");
332
+ if (opts.json) {
333
+ console.log(JSON.stringify(data, null, 2));
334
+ return;
335
+ }
336
+ renderDashboard(data);
337
+ }
338
+ catch (err) {
339
+ spin.fail("Failed to fetch token fee data");
340
+ emitCESP("task.error");
341
+ output.error(err.message);
342
+ process.exit(1);
343
+ }
344
+ return;
345
+ }
346
+ // Resolve wallet address
347
+ let walletAddress = address;
348
+ if (!walletAddress) {
349
+ requireApiKey();
350
+ const spin = output.spinner("Fetching wallet info...");
351
+ try {
352
+ const info = await getUserInfo();
353
+ const evmWallet = info.wallets.find((w) => w.chain === "evm");
354
+ if (!evmWallet) {
355
+ spin.fail("No EVM wallet found for this account");
356
+ process.exit(1);
357
+ }
358
+ walletAddress = evmWallet.address;
359
+ spin.succeed(`Wallet: ${walletAddress}`);
360
+ }
361
+ catch (err) {
362
+ spin.fail("Failed to fetch wallet info");
363
+ output.error(err.message);
364
+ process.exit(1);
365
+ }
366
+ }
367
+ // Fetch data
368
+ const spin = output.spinner("Fetching creator fee data...");
369
+ try {
370
+ const data = await getCreatorFees(walletAddress, days);
371
+ spin.succeed("Fee data loaded");
372
+ emitCESP("task.complete");
373
+ if (opts.json) {
374
+ console.log(JSON.stringify(data, null, 2));
375
+ return;
376
+ }
377
+ renderDashboard(data);
378
+ }
379
+ catch (err) {
380
+ spin.fail("Failed to fetch fee data");
381
+ emitCESP("task.error");
382
+ output.error(err.message);
383
+ process.exit(1);
384
+ }
385
+ }
386
+ export async function feesClaimCommand(tokenAddress, opts) {
387
+ requireApiKey();
388
+ // Step 1: Build claim transaction
389
+ const buildSpin = output.spinner("Building claim transaction...");
390
+ let claimTx;
391
+ try {
392
+ claimTx = await buildDopplerClaimTx(tokenAddress);
393
+ buildSpin.succeed(claimTx.description);
394
+ }
395
+ catch (err) {
396
+ buildSpin.fail("Failed to build claim transaction");
397
+ emitCESP("task.error");
398
+ output.error(err.message);
399
+ process.exit(1);
400
+ }
401
+ // Step 2: Confirm
402
+ if (!opts.yes) {
403
+ console.log();
404
+ output.keyValue("To", claimTx.to);
405
+ output.keyValue("Chain ID", String(claimTx.chainId));
406
+ output.keyValue("Action", claimTx.description);
407
+ console.log();
408
+ const proceed = await confirm({
409
+ message: "Submit this claim transaction?",
410
+ default: true,
411
+ theme: output.bankrTheme,
412
+ });
413
+ if (!proceed) {
414
+ output.dim("Claim cancelled.");
415
+ return;
416
+ }
417
+ }
418
+ // Step 3: Submit via /agent/submit
419
+ const submitSpin = output.spinner("Submitting claim transaction...");
420
+ const startTime = Date.now();
421
+ try {
422
+ const result = await submit({
423
+ transaction: {
424
+ to: claimTx.to,
425
+ chainId: claimTx.chainId,
426
+ data: claimTx.data,
427
+ },
428
+ description: claimTx.description,
429
+ waitForConfirmation: true,
430
+ });
431
+ const elapsed = output.formatDuration(Date.now() - startTime);
432
+ if (result.success) {
433
+ submitSpin.succeed(`Fees claimed in ${elapsed}`);
434
+ emitCESP("task.complete");
435
+ console.log();
436
+ if (result.transactionHash) {
437
+ output.keyValue("Transaction", result.transactionHash);
438
+ }
439
+ if (result.status) {
440
+ output.keyValue("Status", output.formatStatus(result.status === "success" ? "completed" : result.status));
441
+ }
442
+ output.keyValue("Signer", result.signer);
443
+ console.log();
444
+ output.dim(`View on Basescan: https://basescan.org/tx/${result.transactionHash}`);
445
+ }
446
+ else {
447
+ submitSpin.fail(`Claim failed after ${elapsed}`);
448
+ emitCESP("task.error");
449
+ output.error(result.error || "Transaction failed");
450
+ process.exit(1);
451
+ }
452
+ }
453
+ catch (err) {
454
+ submitSpin.fail("Claim submission failed");
455
+ emitCESP("task.error");
456
+ output.error(err.message);
457
+ process.exit(1);
458
+ }
459
+ }
460
+ // ---------------------------------------------------------------------------
461
+ // claim-wallet: Claim fees using an external private key (no Bankr account)
462
+ // ---------------------------------------------------------------------------
463
+ const DEFAULT_BASE_RPC = "https://mainnet.base.org";
464
+ export async function feesClaimWalletCommand(address, opts) {
465
+ // Commander may pass the subcommand name as the address arg — discard it
466
+ if (address && !isAddress(address)) {
467
+ address = undefined;
468
+ }
469
+ // Load .env from cwd if present (no dependency needed)
470
+ try {
471
+ const { readFileSync, existsSync } = await import("node:fs");
472
+ const { resolve } = await import("node:path");
473
+ const envPath = resolve(process.cwd(), ".env");
474
+ if (existsSync(envPath)) {
475
+ const envContent = readFileSync(envPath, "utf-8");
476
+ for (const line of envContent.split("\n")) {
477
+ const trimmed = line.trim();
478
+ if (!trimmed || trimmed.startsWith("#"))
479
+ continue;
480
+ const eqIdx = trimmed.indexOf("=");
481
+ if (eqIdx === -1)
482
+ continue;
483
+ const key = trimmed.slice(0, eqIdx).trim();
484
+ const raw = trimmed.slice(eqIdx + 1).trim();
485
+ // Strip surrounding quotes
486
+ const val = raw.replace(/^["']|["']$/g, "");
487
+ if (!process.env[key]) {
488
+ process.env[key] = val;
489
+ }
490
+ }
491
+ }
492
+ }
493
+ catch {
494
+ // Ignore .env read errors
495
+ }
496
+ // Step 1: Get private key (flag > env var > .env > interactive prompt)
497
+ let privateKey;
498
+ if (opts.privateKey) {
499
+ privateKey = opts.privateKey;
500
+ }
501
+ else if (process.env.BANKR_PRIVATE_KEY) {
502
+ privateKey = process.env.BANKR_PRIVATE_KEY.trim();
503
+ }
504
+ else if (!address) {
505
+ output.error("Address required when no private key is available. Provide an address argument or set BANKR_PRIVATE_KEY in .env");
506
+ process.exit(1);
507
+ }
508
+ else {
509
+ privateKey = await password({
510
+ message: "Enter private key for this wallet (0x...):",
511
+ mask: "*",
512
+ theme: output.bankrTheme,
513
+ });
514
+ }
515
+ privateKey = privateKey.trim();
516
+ if (!privateKey.startsWith("0x")) {
517
+ privateKey = `0x${privateKey}`;
518
+ }
519
+ // Step 2: Derive account from key
520
+ const { privateKeyToAccount } = await import("viem/accounts");
521
+ let account;
522
+ try {
523
+ account = privateKeyToAccount(privateKey);
524
+ }
525
+ catch {
526
+ output.error("Invalid private key format");
527
+ process.exit(1);
528
+ }
529
+ // Step 3: Resolve address — derive from key if not provided, verify match if provided
530
+ if (!address) {
531
+ address = account.address;
532
+ output.dim(`Using address from private key: ${address}`);
533
+ }
534
+ else {
535
+ if (!isAddress(address)) {
536
+ output.error("Invalid EVM address");
537
+ process.exit(1);
538
+ }
539
+ if (account.address.toLowerCase() !== address.toLowerCase()) {
540
+ output.error(`Private key does not match address ${address}. Derived: ${account.address}`);
541
+ process.exit(1);
542
+ }
543
+ }
544
+ console.log();
545
+ output.keyValue("Wallet", account.address);
546
+ // Step 4: Fetch claimable fees with streaming progress
547
+ const fetchSpin = output.spinner("Scanning launches for claimable fees...");
548
+ let allTokens;
549
+ try {
550
+ const result = await getBeneficiaryFeesStreaming(address, (progress) => {
551
+ fetchSpin.text = `Scanning pools... ${progress.scanned}/${progress.total} (${progress.found ?? 0} with claimable fees)`;
552
+ });
553
+ fetchSpin.succeed(`Scanned ${result.totalLaunches} launches — ${result.poolsWithShares} pools with shares, ${result.tokens.length} with claimable fees`);
554
+ allTokens = result.tokens;
555
+ }
556
+ catch (err) {
557
+ fetchSpin.fail("Failed to fetch fee data");
558
+ output.error(err.message);
559
+ process.exit(1);
560
+ }
561
+ // Step 5: Filter to tokens with claimable fees (API already filters, but
562
+ // double-check for any rounding edge cases)
563
+ const parseAmount = (s) => parseFloat(s.replace(/^[<>]/, "")) || 0;
564
+ const claimableTokens = allTokens.filter((token) => {
565
+ const wethIsToken0 = token.token0Label === "WETH";
566
+ const claimableWeth = wethIsToken0
567
+ ? token.claimable.token0
568
+ : token.claimable.token1;
569
+ const claimableToken = wethIsToken0
570
+ ? token.claimable.token1
571
+ : token.claimable.token0;
572
+ return parseAmount(claimableWeth) > 0 || parseAmount(claimableToken) > 0;
573
+ });
574
+ if (claimableTokens.length === 0) {
575
+ console.log();
576
+ output.dim("No claimable fees found for this wallet.");
577
+ return;
578
+ }
579
+ // Show total claimable WETH
580
+ const totalWeth = claimableTokens.reduce((sum, token) => {
581
+ const wethIsToken0 = token.token0Label === "WETH";
582
+ const wethStr = wethIsToken0
583
+ ? token.claimable.token0
584
+ : token.claimable.token1;
585
+ return sum + parseAmount(wethStr);
586
+ }, 0);
587
+ console.log();
588
+ output.keyValue("Total claimable", `${totalWeth.toFixed(6)} WETH across ${claimableTokens.length} token${claimableTokens.length !== 1 ? "s" : ""}`);
589
+ // Step 6: Select tokens
590
+ let selectedTokens;
591
+ if (opts.all) {
592
+ selectedTokens = claimableTokens;
593
+ }
594
+ else {
595
+ const choices = claimableTokens.map((token) => {
596
+ const wethIsToken0 = token.token0Label === "WETH";
597
+ const claimableWeth = wethIsToken0
598
+ ? token.claimable.token0
599
+ : token.claimable.token1;
600
+ return {
601
+ name: `${token.name} ($${token.symbol}) - ${claimableWeth} WETH`,
602
+ value: token.tokenAddress,
603
+ checked: true,
604
+ };
605
+ });
606
+ const selectedAddresses = await checkbox({
607
+ message: "Select tokens to claim fees for:",
608
+ choices,
609
+ theme: output.bankrTheme,
610
+ });
611
+ if (selectedAddresses.length === 0) {
612
+ output.dim("No tokens selected.");
613
+ return;
614
+ }
615
+ selectedTokens = claimableTokens.filter((t) => selectedAddresses.includes(t.tokenAddress));
616
+ }
617
+ // Step 7: Build transactions locally (like claim-doppler-fees.ts script)
618
+ const { createPublicClient, createWalletClient, http, encodeFunctionData } = await import("viem");
619
+ const { base } = await import("viem/chains");
620
+ const rpcUrl = opts.rpc || DEFAULT_BASE_RPC;
621
+ const transport = http(rpcUrl);
622
+ const publicClient = createPublicClient({ chain: base, transport });
623
+ const walletClient = createWalletClient({
624
+ account,
625
+ chain: base,
626
+ transport,
627
+ });
628
+ const collectFeesAbi = [
629
+ {
630
+ type: "function",
631
+ name: "collectFees",
632
+ stateMutability: "nonpayable",
633
+ inputs: [{ name: "poolId", type: "bytes32" }],
634
+ outputs: [
635
+ { name: "fees0", type: "uint128" },
636
+ { name: "fees1", type: "uint128" },
637
+ ],
638
+ },
639
+ ];
640
+ // Filter to tokens with initializer (required for local tx building)
641
+ const buildableTokens = selectedTokens.filter((t) => t.initializer);
642
+ if (buildableTokens.length < selectedTokens.length) {
643
+ output.warn(`${selectedTokens.length - buildableTokens.length} token(s) missing initializer data — skipped`);
644
+ }
645
+ if (buildableTokens.length === 0) {
646
+ output.error("No transactions could be built");
647
+ process.exit(1);
648
+ }
649
+ // Encode calldata locally (instant, no RPC)
650
+ const txData = buildableTokens.map((t) => ({
651
+ token: t,
652
+ to: t.initializer,
653
+ data: encodeFunctionData({
654
+ abi: collectFeesAbi,
655
+ functionName: "collectFees",
656
+ args: [t.poolId],
657
+ }),
658
+ }));
659
+ // Estimate gas for ALL tokens in parallel
660
+ const buildSpin = output.spinner(`Estimating gas for ${txData.length} transaction${txData.length !== 1 ? "s" : ""}...`);
661
+ const GAS_BUFFER = 130n; // 30% buffer
662
+ const gasEstimates = await Promise.all(txData.map(async ({ token, to, data }) => {
663
+ try {
664
+ const gas = await publicClient.estimateGas({
665
+ account: account.address,
666
+ to,
667
+ data,
668
+ });
669
+ return (gas * GAS_BUFFER) / 100n;
670
+ }
671
+ catch (err) {
672
+ output.warn(`${token.symbol}: gas estimate failed — ${err.message}`);
673
+ return null;
674
+ }
675
+ }));
676
+ // Filter out failed gas estimates
677
+ const readyTxs = txData
678
+ .map((td, i) => ({ ...td, gas: gasEstimates[i] }))
679
+ .filter((td) => td.gas !== null);
680
+ if (readyTxs.length === 0) {
681
+ buildSpin.fail("All gas estimates failed");
682
+ process.exit(1);
683
+ }
684
+ buildSpin.succeed(`Prepared ${readyTxs.length} transaction${readyTxs.length !== 1 ? "s" : ""}`);
685
+ // Step 8: Show summary and confirm
686
+ const [feeData] = await Promise.all([publicClient.estimateFeesPerGas()]);
687
+ console.log();
688
+ console.log(` ${BRAND_BOLD("Transactions to submit:")}`);
689
+ console.log();
690
+ for (const tx of readyTxs) {
691
+ const gasWei = tx.gas * (feeData.maxFeePerGas ?? 0n);
692
+ const gasCostEth = Number(gasWei) / 1e18;
693
+ console.log(` ${BOLD(WHITE(tx.token.name))} ${DIM(`($${tx.token.symbol})`)}`);
694
+ console.log(` ${DIM("To:")} ${tx.to} ${DIM("Gas:")} ~${gasCostEth.toFixed(6)} ETH`);
695
+ console.log();
696
+ }
697
+ if (!opts.yes) {
698
+ const proceed = await confirm({
699
+ message: `Submit ${readyTxs.length} claim transaction${readyTxs.length !== 1 ? "s" : ""}?`,
700
+ default: true,
701
+ theme: output.bankrTheme,
702
+ });
703
+ if (!proceed) {
704
+ output.dim("Cancelled.");
705
+ return;
706
+ }
707
+ }
708
+ // Step 9: Fire all txs with explicit nonce, then confirm in parallel
709
+ const startTime = Date.now();
710
+ let successCount = 0;
711
+ let failCount = 0;
712
+ const sendSpin = output.spinner("Preparing transactions...");
713
+ const baseNonce = await publicClient.getTransactionCount({
714
+ address: account.address,
715
+ blockTag: "pending",
716
+ });
717
+ let nonce = baseNonce;
718
+ const pendingTxs = [];
719
+ for (const tx of readyTxs) {
720
+ sendSpin.text = `Sending ${pendingTxs.length + 1}/${readyTxs.length}: ${tx.token.symbol}...`;
721
+ try {
722
+ const hash = await walletClient.sendTransaction({
723
+ to: tx.to,
724
+ data: tx.data,
725
+ gas: tx.gas,
726
+ maxFeePerGas: feeData.maxFeePerGas,
727
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
728
+ nonce: nonce++,
729
+ });
730
+ pendingTxs.push({ token: tx.token, hash });
731
+ }
732
+ catch (err) {
733
+ sendSpin.fail(`${tx.token.symbol}: Send failed — ${err.message}`);
734
+ emitCESP("task.error");
735
+ failCount++;
736
+ if (readyTxs.indexOf(tx) < readyTxs.length - 1) {
737
+ output.warn("Stopping remaining sends to avoid nonce gaps.");
738
+ }
739
+ break;
740
+ }
741
+ }
742
+ if (pendingTxs.length > 0) {
743
+ sendSpin.succeed(`Sent ${pendingTxs.length} transaction${pendingTxs.length !== 1 ? "s" : ""}`);
744
+ }
745
+ else {
746
+ sendSpin.fail("No transactions were sent.");
747
+ }
748
+ // Wait for all receipts in parallel
749
+ if (pendingTxs.length > 0) {
750
+ const confirmSpin = output.spinner(`Waiting for ${pendingTxs.length} confirmation${pendingTxs.length !== 1 ? "s" : ""}...`);
751
+ const results = await Promise.all(pendingTxs.map(async ({ token, hash }) => {
752
+ try {
753
+ const receipt = await publicClient.waitForTransactionReceipt({
754
+ hash,
755
+ });
756
+ return { token, hash, status: receipt.status };
757
+ }
758
+ catch (err) {
759
+ return {
760
+ token,
761
+ hash,
762
+ status: "failed",
763
+ error: err.message,
764
+ };
765
+ }
766
+ }));
767
+ confirmSpin.stop();
768
+ for (const r of results) {
769
+ if (r.status === "success") {
770
+ output.success(`${r.token.symbol}: Claimed ${DIM(`tx: ${r.hash.slice(0, 10)}...${r.hash.slice(-8)}`)}`);
771
+ output.dim(` https://basescan.org/tx/${r.hash}`);
772
+ successCount++;
773
+ }
774
+ else {
775
+ const reason = "error" in r ? r.error : "Transaction reverted";
776
+ output.error(`${r.token.symbol}: ${reason}`);
777
+ output.dim(` https://basescan.org/tx/${r.hash}`);
778
+ failCount++;
779
+ }
780
+ }
781
+ }
782
+ // Step 10: Summary
783
+ const elapsed = output.formatDuration(Date.now() - startTime);
784
+ console.log();
785
+ if (successCount > 0) {
786
+ emitCESP("task.complete");
787
+ output.success(`Claimed fees for ${successCount} token${successCount !== 1 ? "s" : ""} in ${elapsed}`);
788
+ }
789
+ if (failCount > 0) {
790
+ output.error(`${failCount} claim${failCount !== 1 ? "s" : ""} failed`);
791
+ }
792
+ }
793
+ //# sourceMappingURL=fees.js.map