apow-cli 0.1.4 → 0.3.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.
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.startDashboardServer = startDashboardServer;
40
+ const http = __importStar(require("node:http"));
41
+ const node_fs_1 = require("node:fs");
42
+ const node_path_1 = require("node:path");
43
+ const viem_1 = require("viem");
44
+ const chains_1 = require("viem/chains");
45
+ const dashboard_html_1 = require("./dashboard-html");
46
+ const AgentCoin_json_1 = __importDefault(require("./abi/AgentCoin.json"));
47
+ const MiningAgent_json_1 = __importDefault(require("./abi/MiningAgent.json"));
48
+ const AgentCoinAbi = AgentCoin_json_1.default;
49
+ const MiningAgentAbi = MiningAgent_json_1.default;
50
+ const RARITY_LABELS = ["Common", "Uncommon", "Rare", "Epic", "Mythic"];
51
+ const ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
52
+ const DEFAULT_RPC = "https://mainnet.base.org";
53
+ const FLEETS_PATH = (0, node_path_1.join)(process.env.HOME ?? "", ".apow", "fleets.json");
54
+ // --- Wallet / Fleet loading ---
55
+ function isAddress(s) {
56
+ return ADDR_RE.test(s);
57
+ }
58
+ function extractArray(path) {
59
+ const raw = (0, node_fs_1.readFileSync)(path, "utf8");
60
+ const data = JSON.parse(raw);
61
+ if (!Array.isArray(data))
62
+ return [];
63
+ return data.filter((a) => typeof a === "string" && isAddress(a));
64
+ }
65
+ function extractSolkek(path) {
66
+ const raw = (0, node_fs_1.readFileSync)(path, "utf8");
67
+ const data = JSON.parse(raw);
68
+ const addrs = [];
69
+ if (data.master?.address && isAddress(data.master.address)) {
70
+ addrs.push(data.master.address);
71
+ }
72
+ if (Array.isArray(data.miners)) {
73
+ for (const m of data.miners) {
74
+ if (m.address && isAddress(m.address)) {
75
+ addrs.push(m.address);
76
+ }
77
+ }
78
+ }
79
+ return addrs;
80
+ }
81
+ function extractRigdirs(dir) {
82
+ const addrs = [];
83
+ const entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (!entry.isDirectory() || !entry.name.startsWith("rig"))
86
+ continue;
87
+ const rigFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(dir, entry.name));
88
+ for (const file of rigFiles) {
89
+ const match = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
90
+ if (match && isAddress(match[1])) {
91
+ addrs.push(match[1]);
92
+ }
93
+ }
94
+ }
95
+ return addrs;
96
+ }
97
+ function extractWalletfiles(dir) {
98
+ const addrs = [];
99
+ const files = (0, node_fs_1.readdirSync)(dir);
100
+ for (const file of files) {
101
+ const match = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
102
+ if (match && isAddress(match[1])) {
103
+ addrs.push(match[1]);
104
+ }
105
+ }
106
+ return addrs;
107
+ }
108
+ function getFleets(walletsPath) {
109
+ try {
110
+ const raw = (0, node_fs_1.readFileSync)(FLEETS_PATH, "utf8");
111
+ const configs = JSON.parse(raw);
112
+ return configs.map((cfg) => {
113
+ let addresses = [];
114
+ try {
115
+ switch (cfg.type) {
116
+ case "array":
117
+ addresses = extractArray(cfg.path);
118
+ break;
119
+ case "solkek":
120
+ addresses = extractSolkek(cfg.path);
121
+ break;
122
+ case "rigdirs":
123
+ addresses = extractRigdirs(cfg.path);
124
+ break;
125
+ case "walletfiles":
126
+ addresses = extractWalletfiles(cfg.path);
127
+ break;
128
+ }
129
+ }
130
+ catch {
131
+ // Skip broken fleet sources silently
132
+ }
133
+ return { name: cfg.name, addresses };
134
+ });
135
+ }
136
+ catch {
137
+ return [{ name: "Main", addresses: getWalletAddresses(walletsPath) }];
138
+ }
139
+ }
140
+ function getWalletAddresses(walletsPath) {
141
+ try {
142
+ const raw = (0, node_fs_1.readFileSync)(walletsPath, "utf8");
143
+ const data = JSON.parse(raw);
144
+ if (Array.isArray(data)) {
145
+ return data.filter((addr) => typeof addr === "string" && ADDR_RE.test(addr));
146
+ }
147
+ return [];
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ }
153
+ function getAddressesForFleet(fleetName, walletsPath) {
154
+ if (!fleetName || fleetName === "All") {
155
+ const fleets = getFleets(walletsPath);
156
+ const seen = new Set();
157
+ const all = [];
158
+ for (const f of fleets) {
159
+ for (const addr of f.addresses) {
160
+ const lower = addr.toLowerCase();
161
+ if (!seen.has(lower)) {
162
+ seen.add(lower);
163
+ all.push(addr);
164
+ }
165
+ }
166
+ }
167
+ return all;
168
+ }
169
+ const fleets = getFleets(walletsPath);
170
+ const fleet = fleets.find((f) => f.name === fleetName);
171
+ if (fleet)
172
+ return fleet.addresses;
173
+ return getWalletAddresses(walletsPath);
174
+ }
175
+ // --- Art parsing ---
176
+ function parseArtFromTokenUri(raw) {
177
+ if (!raw?.startsWith("data:application/json;base64,"))
178
+ return "";
179
+ try {
180
+ const json = JSON.parse(Buffer.from(raw.slice(29), "base64").toString("utf8"));
181
+ return json.image ?? "";
182
+ }
183
+ catch {
184
+ return "";
185
+ }
186
+ }
187
+ // --- Server ---
188
+ function startDashboardServer(opts) {
189
+ const { port, walletsPath, rpcUrl, miningAgentAddress, agentCoinAddress } = opts;
190
+ const publicClient = (0, viem_1.createPublicClient)({
191
+ chain: chains_1.base,
192
+ transport: (0, viem_1.http)(rpcUrl),
193
+ });
194
+ const artCache = new Map();
195
+ const htmlPage = (0, dashboard_html_1.getDashboardHtml)();
196
+ async function handleWallets(fleetParam) {
197
+ const addresses = getAddressesForFleet(fleetParam, walletsPath);
198
+ if (addresses.length === 0)
199
+ return "[]";
200
+ // Phase 1: ETH balance + AGENT balance + NFT count
201
+ const phase1Contracts = addresses.flatMap((addr) => [
202
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "balanceOf", args: [addr] },
203
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "balanceOf", args: [addr] },
204
+ ]);
205
+ const [balances, multicallResults] = await Promise.all([
206
+ Promise.all(addresses.map((addr) => publicClient.getBalance({ address: addr }).catch(() => 0n))),
207
+ publicClient.multicall({ contracts: phase1Contracts }),
208
+ ]);
209
+ const walletInfos = addresses.map((addr, i) => ({
210
+ address: addr,
211
+ ethBalance: balances[i],
212
+ agentBalance: multicallResults[i * 2].result ?? 0n,
213
+ nftCount: Number(multicallResults[i * 2 + 1].result ?? 0n),
214
+ }));
215
+ // Phase 2: token IDs
216
+ const tokenIdContracts = [];
217
+ const tokenIdMap = [];
218
+ for (let wi = 0; wi < walletInfos.length; wi++) {
219
+ const info = walletInfos[wi];
220
+ for (let mi = 0; mi < info.nftCount; mi++) {
221
+ tokenIdContracts.push({
222
+ address: miningAgentAddress,
223
+ abi: MiningAgentAbi,
224
+ functionName: "tokenOfOwnerByIndex",
225
+ args: [info.address, BigInt(mi)],
226
+ });
227
+ tokenIdMap.push({ walletIdx: wi });
228
+ }
229
+ }
230
+ const tokenIds = [];
231
+ const validTokenMap = [];
232
+ if (tokenIdContracts.length > 0) {
233
+ const tokenIdResults = await publicClient.multicall({ contracts: tokenIdContracts });
234
+ for (let i = 0; i < tokenIdResults.length; i++) {
235
+ const r = tokenIdResults[i];
236
+ if (r.status === "success" && r.result != null) {
237
+ validTokenMap.push(i);
238
+ tokenIds.push(r.result);
239
+ }
240
+ }
241
+ }
242
+ // No miners — return early
243
+ if (tokenIds.length === 0) {
244
+ const wallets = walletInfos.map((info) => ({
245
+ address: info.address,
246
+ ethBalance: (0, viem_1.formatEther)(info.ethBalance),
247
+ agentBalance: (0, viem_1.formatEther)(info.agentBalance),
248
+ miners: [],
249
+ }));
250
+ return JSON.stringify(wallets);
251
+ }
252
+ // Phase 3: miner stats + art
253
+ const uncachedTokenIds = tokenIds.filter((id) => !artCache.has(id.toString()));
254
+ const FIELDS_PER_TOKEN = 5;
255
+ const detailContracts = tokenIds.flatMap((tokenId) => [
256
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "rarity", args: [tokenId] },
257
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "hashpower", args: [tokenId] },
258
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenMineCount", args: [tokenId] },
259
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenEarnings", args: [tokenId] },
260
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "mintBlock", args: [tokenId] },
261
+ ]);
262
+ const detailResults = await publicClient.multicall({ contracts: detailContracts });
263
+ // Batch art URI calls for uncached tokens — use small chunks to avoid RPC response size limits
264
+ const ART_CHUNK_SIZE = 2;
265
+ const artResults = [];
266
+ for (let i = 0; i < uncachedTokenIds.length; i += ART_CHUNK_SIZE) {
267
+ const chunk = uncachedTokenIds.slice(i, i + ART_CHUNK_SIZE);
268
+ const artContracts = chunk.map((tokenId) => ({
269
+ address: miningAgentAddress,
270
+ abi: MiningAgentAbi,
271
+ functionName: "tokenURI",
272
+ args: [tokenId],
273
+ }));
274
+ try {
275
+ const chunkResults = await publicClient.multicall({ contracts: artContracts });
276
+ artResults.push(...chunkResults);
277
+ }
278
+ catch {
279
+ // Push failure entries so indexing stays aligned
280
+ for (let j = 0; j < chunk.length; j++) {
281
+ artResults.push({ status: "failure" });
282
+ }
283
+ }
284
+ }
285
+ // Populate art cache
286
+ for (let i = 0; i < uncachedTokenIds.length; i++) {
287
+ const r = artResults[i];
288
+ if (r && r.status === "success" && r.result) {
289
+ const imageUri = parseArtFromTokenUri(r.result);
290
+ if (imageUri)
291
+ artCache.set(uncachedTokenIds[i].toString(), imageUri);
292
+ }
293
+ }
294
+ // Build wallet data
295
+ const minersByWallet = new Map();
296
+ for (let ti = 0; ti < tokenIds.length; ti++) {
297
+ const originalIdx = validTokenMap[ti];
298
+ const { walletIdx } = tokenIdMap[originalIdx];
299
+ const b = ti * FIELDS_PER_TOKEN;
300
+ const rarity = Number(detailResults[b].result ?? 0n);
301
+ const hashpower = Number(detailResults[b + 1].result ?? 100n);
302
+ const mineCount = detailResults[b + 2].result ?? 0n;
303
+ const earnings = detailResults[b + 3].result ?? 0n;
304
+ const mintBlock = detailResults[b + 4].result ?? 0n;
305
+ const tid = tokenIds[ti].toString();
306
+ const miner = {
307
+ tokenId: tid,
308
+ rarity,
309
+ rarityLabel: RARITY_LABELS[rarity] ?? `Tier ${rarity}`,
310
+ hashpower,
311
+ mineCount: mineCount.toString(),
312
+ earnings: (0, viem_1.formatEther)(earnings),
313
+ mintBlock: mintBlock.toString(),
314
+ imageUri: artCache.get(tid),
315
+ };
316
+ if (!minersByWallet.has(walletIdx))
317
+ minersByWallet.set(walletIdx, []);
318
+ minersByWallet.get(walletIdx).push(miner);
319
+ }
320
+ const wallets = walletInfos.map((info, i) => ({
321
+ address: info.address,
322
+ ethBalance: (0, viem_1.formatEther)(info.ethBalance),
323
+ agentBalance: (0, viem_1.formatEther)(info.agentBalance),
324
+ miners: minersByWallet.get(i) ?? [],
325
+ }));
326
+ return JSON.stringify(wallets);
327
+ }
328
+ async function handleNetwork() {
329
+ const results = await publicClient.multicall({
330
+ contracts: [
331
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "totalMines" },
332
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "totalMinted" },
333
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "miningTarget" },
334
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "MINEABLE_SUPPLY" },
335
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "ERA_INTERVAL" },
336
+ ],
337
+ });
338
+ const totalMines = results[0].result;
339
+ const totalMinted = results[1].result;
340
+ const miningTarget = results[2].result;
341
+ const mineableSupply = results[3].result;
342
+ const eraInterval = results[4].result;
343
+ const era = totalMines / eraInterval;
344
+ const minesUntilNextEra = eraInterval - (totalMines % eraInterval);
345
+ const supplyPct = Number((totalMinted * 10000n) / mineableSupply) / 100;
346
+ let baseReward = 3;
347
+ for (let i = 0n; i < era; i++) {
348
+ baseReward *= 0.9;
349
+ }
350
+ const nextEraReward = baseReward * 0.9;
351
+ const targetLog = Math.log2(Number(miningTarget));
352
+ const difficulty = targetLog > 250 ? "very easy" :
353
+ targetLog > 240 ? "easy" :
354
+ targetLog > 220 ? "moderate" :
355
+ targetLog > 200 ? "hard" : "very hard";
356
+ return JSON.stringify({
357
+ totalMines: totalMines.toString(),
358
+ totalMinted: (0, viem_1.formatEther)(totalMinted),
359
+ mineableSupply: (0, viem_1.formatEther)(mineableSupply),
360
+ era: Number(era),
361
+ minesUntilNextEra: minesUntilNextEra.toString(),
362
+ baseReward,
363
+ nextEraReward,
364
+ difficulty,
365
+ supplyPct,
366
+ });
367
+ }
368
+ function handleFleets() {
369
+ const fleets = getFleets(walletsPath);
370
+ return JSON.stringify(fleets.map((f) => ({ name: f.name, walletCount: f.addresses.length })));
371
+ }
372
+ function handleConfig() {
373
+ const rpcIsDefault = rpcUrl === DEFAULT_RPC;
374
+ const walletCount = getWalletAddresses(walletsPath).length;
375
+ return JSON.stringify({ rpcIsDefault, walletCount });
376
+ }
377
+ function jsonResponse(res, body, status = 200) {
378
+ res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
379
+ res.end(body);
380
+ }
381
+ function errorResponse(res, message, status = 500) {
382
+ jsonResponse(res, JSON.stringify({ error: message }), status);
383
+ }
384
+ const server = http.createServer(async (req, res) => {
385
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
386
+ const pathname = url.pathname;
387
+ try {
388
+ if (pathname === "/" || pathname === "") {
389
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
390
+ res.end(htmlPage);
391
+ return;
392
+ }
393
+ if (pathname === "/api/wallets") {
394
+ const fleet = url.searchParams.get("fleet");
395
+ const body = await handleWallets(fleet);
396
+ jsonResponse(res, body);
397
+ return;
398
+ }
399
+ if (pathname === "/api/network") {
400
+ const body = await handleNetwork();
401
+ jsonResponse(res, body);
402
+ return;
403
+ }
404
+ if (pathname === "/api/fleets") {
405
+ const body = handleFleets();
406
+ jsonResponse(res, body);
407
+ return;
408
+ }
409
+ if (pathname === "/api/config") {
410
+ const body = handleConfig();
411
+ jsonResponse(res, body);
412
+ return;
413
+ }
414
+ res.writeHead(404, { "Content-Type": "text/plain" });
415
+ res.end("Not Found");
416
+ }
417
+ catch (err) {
418
+ const message = err instanceof Error ? err.message : "Unknown error";
419
+ errorResponse(res, message);
420
+ }
421
+ });
422
+ server.listen(port);
423
+ return server;
424
+ }