@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.
- package/README.md +27 -1
- package/dist/cli.js +222 -4
- package/dist/commands/balances.d.ts +5 -0
- package/dist/commands/balances.js +113 -0
- package/dist/commands/fees.d.ts +18 -0
- package/dist/commands/fees.js +793 -0
- package/dist/commands/launch.d.ts +13 -0
- package/dist/commands/launch.js +174 -0
- package/dist/commands/llm.d.ts +11 -0
- package/dist/commands/llm.js +319 -3
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +464 -147
- package/dist/commands/profile.d.ts +26 -0
- package/dist/commands/profile.js +183 -0
- package/dist/commands/prompt.js +5 -0
- package/dist/commands/sign.js +3 -0
- package/dist/commands/sounds.d.ts +12 -0
- package/dist/commands/sounds.js +262 -0
- package/dist/commands/submit.js +14 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lib/api.d.ts +213 -0
- package/dist/lib/api.js +177 -3
- package/dist/lib/cesp/engine.d.ts +13 -0
- package/dist/lib/cesp/engine.js +132 -0
- package/dist/lib/cesp/player.d.ts +6 -0
- package/dist/lib/cesp/player.js +50 -0
- package/dist/lib/cesp/types.d.ts +38 -0
- package/dist/lib/cesp/types.js +2 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +3 -0
- package/package.json +4 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/cancel.d.ts.map +0 -1
- package/dist/commands/cancel.js.map +0 -1
- package/dist/commands/capabilities.d.ts.map +0 -1
- package/dist/commands/capabilities.js.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/llm.d.ts.map +0 -1
- package/dist/commands/llm.js.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/prompt.d.ts.map +0 -1
- package/dist/commands/prompt.js.map +0 -1
- package/dist/commands/sign.d.ts.map +0 -1
- package/dist/commands/sign.js.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/submit.d.ts.map +0 -1
- package/dist/commands/submit.js.map +0 -1
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/api.d.ts.map +0 -1
- package/dist/lib/api.js.map +0 -1
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/output.d.ts.map +0 -1
- 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
|