apow-cli 0.3.0 → 0.3.2

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.
@@ -8,6 +8,7 @@ function getDashboardHtml() {
8
8
  <meta charset="utf-8">
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1">
10
10
  <title>APoW Dashboard</title>
11
+ <link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAFNUExURQAAAABS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBS/wBR/xNf/32n/3ql/yFp/9zo/9rm/x9n/3ym/////3mk/wBQ/yFo/9fk//r8/6fD/6jE/wBP/+3z/zZ3/zh4/+7z/wRV/yds/yxw/0F+/73S/zJ0/77T/9jl/0B9/0yF/+Xt//D1//f5/0yG/wtZ/6G//+Tt/+vx/+zy/+Dq/+Lr/+rx/6LA/wNU/xpk/xxl/5K1/+3y/0uF/xpj/5G0/xtl/yNq/zl5/9jk/7nP/ypu/yRq/7rQ/yVr/8rb/+nw//7///P3/+fv/+jv/8vb/3um//H1/wJT/5i5/1OK/yht/1SL/+/0/5e5/x1l/9Xj/6PB/z17/z58//H2/wlY/3ai/5y8/3Ge/wZW/3Sg/569/+rqE9AAAAATdFJOUwAAAitvsNz0/jma4fwbjOw3xUOydetBAAAAAWJLR0Qd6wNxkQAAAAd0SU1FB+oDERUwG5vARuoAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjYtMDMtMTdUMjE6NDg6MjIrMDA6MDDtKLHnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI2LTAzLTE3VDIxOjQ4OjIyKzAwOjAwnHUJWwAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNi0wMy0xN1QyMTo0ODoyNyswMDowMJlYByMAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAABwklEQVQ4y4VTWVvCMBAM5T5FKKjAUkGpICoeXHKIgIrigQhe4AleKPr/H22btKT4fTgv3WYnk2R3FiERGgGMVqc3GE0mo0Gv0zLiClIgps0Wq81NYLNazAzFEEK7Y8qtwpTDrjCEwDnt/oNpJ2EIH5dBWWZZJTS4JIa4n8p7PBRD0hDOp/RZ7wxLnWIXCYyDys/O+fwUw8EIBDN1fzYAEOSot5g1iLFQ+XkfQIiWsDBIa1UJhMMqCasW6WwqgYVFlYRNh/RyzHGRAPBL0RgEI5wiokdyDZbjK6s+WEsk1mFjcyW+LNcCGcn+rSSfAkjzfBogxSczRMOITDjIbufyAIXizk6xAFDK7WbxukkmlCvVPdg/qFWrtQwPh0eVskwgR1TqxydwelZvNOpn5xCK1ivyEdIl2eZFq9CGy6trAVeX0C60bpocviR+5m2+A9BplSS0xDgfIM+UCtX1x+8gdt98kNB8fILnuL+LC4VLHen1YbfMEZRfoN+LkFLjZrFeSL0q5ePe3sHLkmbhdn8MPr+Gox4Nv38GH6TdsmEoM47+JMOoLDdmbMlyKtOqgE07Zns67xoNxsTB+X/0/h/eieP/CwWOXVaTpQ0oAAAAAElFTkSuQmCC">
11
12
  <style>
12
13
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
14
  :root{
@@ -108,6 +109,7 @@ a{color:inherit;text-decoration:none}
108
109
  var prevMines = {};
109
110
  var activeWallets = {};
110
111
  var lastSeen = {};
112
+ var pollCount = 0;
111
113
 
112
114
  function fetchJson(url) {
113
115
  return fetch(url).then(function(r) {
@@ -151,6 +153,17 @@ a{color:inherit;text-decoration:none}
151
153
 
152
154
  function updateActiveWallets(wallets) {
153
155
  if (!wallets) return;
156
+ pollCount++;
157
+ // Skip first 2 polls to establish stable baseline (avoids false positives on load/refresh)
158
+ if (pollCount <= 2) {
159
+ for (var i = 0; i < wallets.length; i++) {
160
+ var w = wallets[i];
161
+ var totalMines = 0;
162
+ for (var j = 0; j < w.miners.length; j++) totalMines += Number(w.miners[j].mineCount);
163
+ prevMines[w.address] = totalMines;
164
+ }
165
+ return;
166
+ }
154
167
  var now = Date.now();
155
168
  for (var i = 0; i < wallets.length; i++) {
156
169
  var w = wallets[i];
package/dist/dashboard.js CHANGED
@@ -193,26 +193,80 @@ function startDashboardServer(opts) {
193
193
  });
194
194
  const artCache = new Map();
195
195
  const htmlPage = (0, dashboard_html_1.getDashboardHtml)();
196
+ // Response cache with TTL — always serve cached data, refresh in background
197
+ const responseCache = new Map();
198
+ const pendingFetches = new Map();
199
+ const CACHE_TTL = 25_000; // 25s — slightly less than client's 30s poll
200
+ async function cachedHandler(key, handler) {
201
+ const cached = responseCache.get(key);
202
+ const now = Date.now();
203
+ const isStale = !cached || (now - cached.ts > CACHE_TTL);
204
+ if (isStale && !pendingFetches.has(key)) {
205
+ // Fetch fresh data (non-blocking if we have stale data to return)
206
+ const fetchPromise = handler()
207
+ .then((result) => {
208
+ responseCache.set(key, { data: result, ts: Date.now() });
209
+ pendingFetches.delete(key);
210
+ return result;
211
+ })
212
+ .catch((err) => {
213
+ pendingFetches.delete(key);
214
+ if (cached)
215
+ return cached.data;
216
+ throw err;
217
+ });
218
+ pendingFetches.set(key, fetchPromise);
219
+ // No cached data yet — must wait for first fetch
220
+ if (!cached)
221
+ return fetchPromise;
222
+ }
223
+ // If we have a pending fetch and no cache, wait for it
224
+ if (!cached && pendingFetches.has(key)) {
225
+ return pendingFetches.get(key);
226
+ }
227
+ // Return cached data immediately (even if stale — background refresh handles it)
228
+ return cached ? cached.data : "{}";
229
+ }
230
+ const MULTICALL_CHUNK = 30; // max calls per multicall batch
231
+ async function chunkedMulticall(contracts) {
232
+ if (contracts.length <= MULTICALL_CHUNK) {
233
+ return publicClient.multicall({ contracts });
234
+ }
235
+ const results = [];
236
+ for (let i = 0; i < contracts.length; i += MULTICALL_CHUNK) {
237
+ const chunk = contracts.slice(i, i + MULTICALL_CHUNK);
238
+ try {
239
+ const chunkResults = await publicClient.multicall({ contracts: chunk });
240
+ results.push(...chunkResults);
241
+ }
242
+ catch {
243
+ for (let j = 0; j < chunk.length; j++) {
244
+ results.push({ status: "failure" });
245
+ }
246
+ }
247
+ }
248
+ return results;
249
+ }
196
250
  async function handleWallets(fleetParam) {
197
251
  const addresses = getAddressesForFleet(fleetParam, walletsPath);
198
252
  if (addresses.length === 0)
199
253
  return "[]";
200
- // Phase 1: ETH balance + AGENT balance + NFT count
254
+ // Phase 1: ETH balance + AGENT balance + NFT count (chunked)
201
255
  const phase1Contracts = addresses.flatMap((addr) => [
202
256
  { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "balanceOf", args: [addr] },
203
257
  { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "balanceOf", args: [addr] },
204
258
  ]);
205
259
  const [balances, multicallResults] = await Promise.all([
206
260
  Promise.all(addresses.map((addr) => publicClient.getBalance({ address: addr }).catch(() => 0n))),
207
- publicClient.multicall({ contracts: phase1Contracts }),
261
+ chunkedMulticall(phase1Contracts),
208
262
  ]);
209
263
  const walletInfos = addresses.map((addr, i) => ({
210
264
  address: addr,
211
265
  ethBalance: balances[i],
212
- agentBalance: multicallResults[i * 2].result ?? 0n,
213
- nftCount: Number(multicallResults[i * 2 + 1].result ?? 0n),
266
+ agentBalance: multicallResults[i * 2]?.result ?? 0n,
267
+ nftCount: Number(multicallResults[i * 2 + 1]?.result ?? 0n),
214
268
  }));
215
- // Phase 2: token IDs
269
+ // Phase 2: token IDs (chunked)
216
270
  const tokenIdContracts = [];
217
271
  const tokenIdMap = [];
218
272
  for (let wi = 0; wi < walletInfos.length; wi++) {
@@ -230,7 +284,7 @@ function startDashboardServer(opts) {
230
284
  const tokenIds = [];
231
285
  const validTokenMap = [];
232
286
  if (tokenIdContracts.length > 0) {
233
- const tokenIdResults = await publicClient.multicall({ contracts: tokenIdContracts });
287
+ const tokenIdResults = await chunkedMulticall(tokenIdContracts);
234
288
  for (let i = 0; i < tokenIdResults.length; i++) {
235
289
  const r = tokenIdResults[i];
236
290
  if (r.status === "success" && r.result != null) {
@@ -249,7 +303,7 @@ function startDashboardServer(opts) {
249
303
  }));
250
304
  return JSON.stringify(wallets);
251
305
  }
252
- // Phase 3: miner stats + art
306
+ // Phase 3: miner stats (chunked) + art
253
307
  const uncachedTokenIds = tokenIds.filter((id) => !artCache.has(id.toString()));
254
308
  const FIELDS_PER_TOKEN = 5;
255
309
  const detailContracts = tokenIds.flatMap((tokenId) => [
@@ -259,8 +313,8 @@ function startDashboardServer(opts) {
259
313
  { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenEarnings", args: [tokenId] },
260
314
  { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "mintBlock", args: [tokenId] },
261
315
  ]);
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
316
+ const detailResults = await chunkedMulticall(detailContracts);
317
+ // Batch art URI calls for uncached tokens — small chunks (tokenURI returns ~41KB each)
264
318
  const ART_CHUNK_SIZE = 2;
265
319
  const artResults = [];
266
320
  for (let i = 0; i < uncachedTokenIds.length; i += ART_CHUNK_SIZE) {
@@ -276,7 +330,6 @@ function startDashboardServer(opts) {
276
330
  artResults.push(...chunkResults);
277
331
  }
278
332
  catch {
279
- // Push failure entries so indexing stays aligned
280
333
  for (let j = 0; j < chunk.length; j++) {
281
334
  artResults.push({ status: "failure" });
282
335
  }
@@ -392,12 +445,13 @@ function startDashboardServer(opts) {
392
445
  }
393
446
  if (pathname === "/api/wallets") {
394
447
  const fleet = url.searchParams.get("fleet");
395
- const body = await handleWallets(fleet);
448
+ const cacheKey = `wallets:${fleet ?? "All"}`;
449
+ const body = await cachedHandler(cacheKey, () => handleWallets(fleet));
396
450
  jsonResponse(res, body);
397
451
  return;
398
452
  }
399
453
  if (pathname === "/api/network") {
400
- const body = await handleNetwork();
454
+ const body = await cachedHandler("network", () => handleNetwork());
401
455
  jsonResponse(res, body);
402
456
  return;
403
457
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apow-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Mine AGENT tokens on Base L2 with AI-powered proof of work",
5
5
  "keywords": [
6
6
  "apow",