apow-cli 0.2.0 → 0.3.1
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/.env.example +2 -1
- package/README.md +12 -2
- package/dist/abi/MiningAgent.json +17 -0
- package/dist/dashboard-html.js +333 -0
- package/dist/dashboard.js +478 -0
- package/dist/index.js +191 -0
- package/package.json +1 -1
- package/skill.md +13 -2
package/.env.example
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Wallet
|
|
2
2
|
PRIVATE_KEY=
|
|
3
3
|
|
|
4
|
-
# RPC
|
|
4
|
+
# RPC — The public default works but is unreliable for sustained mining.
|
|
5
|
+
# We strongly recommend a free Alchemy endpoint: https://www.alchemy.com/ (no credit card)
|
|
5
6
|
RPC_URL=https://mainnet.base.org
|
|
6
7
|
|
|
7
8
|
# LLM Provider (openai / anthropic / gemini / ollama / claude-code / codex)
|
package/README.md
CHANGED
|
@@ -16,6 +16,16 @@ Or run directly:
|
|
|
16
16
|
npx apow-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Important: RPC Endpoint
|
|
20
|
+
|
|
21
|
+
> **The default public RPC (`mainnet.base.org`) is unreliable for mining.** It rate-limits aggressively and will cause frequent transaction failures. **We strongly recommend getting a free Alchemy endpoint** before you start:
|
|
22
|
+
>
|
|
23
|
+
> 1. Sign up at [alchemy.com](https://www.alchemy.com/) (free, no credit card)
|
|
24
|
+
> 2. Create an app → Chain: **Base** → Network: **Base Mainnet**
|
|
25
|
+
> 3. Copy the HTTPS URL → set `RPC_URL` in your `.env`
|
|
26
|
+
>
|
|
27
|
+
> Alchemy's free tier gives you 300M compute units/month — more than enough for mining. See [RPC Recommendations](skill.md#rpc-recommendations) for other free options.
|
|
28
|
+
|
|
19
29
|
## For AI Agents
|
|
20
30
|
|
|
21
31
|
Any AI agent can go from zero to actively mining in 5 steps — no human interaction except funding the wallet.
|
|
@@ -28,7 +38,7 @@ npx apow-cli wallet new
|
|
|
28
38
|
# 2. Write .env (no interactive prompts)
|
|
29
39
|
cat > .env << 'EOF'
|
|
30
40
|
PRIVATE_KEY=0x<from step 1>
|
|
31
|
-
RPC_URL=https://mainnet.base.org
|
|
41
|
+
RPC_URL=https://mainnet.base.org # UNRELIABLE — get a free Alchemy URL (see above)
|
|
32
42
|
LLM_PROVIDER=openai
|
|
33
43
|
LLM_MODEL=gpt-4o-mini
|
|
34
44
|
LLM_API_KEY=<your key>
|
|
@@ -97,7 +107,7 @@ Create a `.env` file or use `apow setup`:
|
|
|
97
107
|
|
|
98
108
|
```bash
|
|
99
109
|
PRIVATE_KEY=0x... # Your wallet private key
|
|
100
|
-
RPC_URL=https://mainnet.base.org
|
|
110
|
+
RPC_URL=https://mainnet.base.org # UNRELIABLE — strongly recommend a free Alchemy URL instead (see above)
|
|
101
111
|
LLM_PROVIDER=openai # openai | anthropic | gemini | ollama | claude-code | codex
|
|
102
112
|
LLM_MODEL=gpt-4o-mini
|
|
103
113
|
LLM_API_KEY=sk-...
|
|
@@ -182,5 +182,22 @@
|
|
|
182
182
|
"type": "uint256"
|
|
183
183
|
}
|
|
184
184
|
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"type": "function",
|
|
188
|
+
"name": "tokenURI",
|
|
189
|
+
"stateMutability": "view",
|
|
190
|
+
"inputs": [
|
|
191
|
+
{
|
|
192
|
+
"name": "tokenId",
|
|
193
|
+
"type": "uint256"
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
"outputs": [
|
|
197
|
+
{
|
|
198
|
+
"name": "",
|
|
199
|
+
"type": "string"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
185
202
|
}
|
|
186
203
|
]
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDashboardHtml = getDashboardHtml;
|
|
4
|
+
function getDashboardHtml() {
|
|
5
|
+
return `<!DOCTYPE html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<title>APoW Dashboard</title>
|
|
11
|
+
<style>
|
|
12
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
13
|
+
:root{
|
|
14
|
+
--bg:#0a0a0a;--card:#141414;--card-border:#161616;
|
|
15
|
+
--text:#e5e5e5;--text-dim:#737373;--accent:#0052FF;
|
|
16
|
+
--rarity-common:#a1a1aa;--rarity-uncommon:#4ade80;
|
|
17
|
+
--rarity-rare:#60a5fa;--rarity-epic:#a78bfa;--rarity-mythic:#fbbf24;
|
|
18
|
+
}
|
|
19
|
+
body{background:var(--bg);color:var(--text);font-family:SFMono-Regular,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace;font-size:13px;line-height:1.4}
|
|
20
|
+
a{color:inherit;text-decoration:none}
|
|
21
|
+
.container{min-height:100vh;padding:12px}
|
|
22
|
+
/* Header */
|
|
23
|
+
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
|
|
24
|
+
.header h1{font-size:14px;font-weight:700;letter-spacing:.05em}
|
|
25
|
+
.status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-dim)}
|
|
26
|
+
.pulse{display:inline-block;height:6px;width:6px;border-radius:50%;background:var(--accent);animation:pulse 1.5s infinite}
|
|
27
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
28
|
+
/* Banners */
|
|
29
|
+
.rpc-warning{border-radius:6px;border:1px solid #7c2d12;background:rgba(69,26,3,.3);padding:8px 12px;font-size:12px;color:#fdba74;margin-bottom:12px}
|
|
30
|
+
.rpc-warning a{text-decoration:underline}
|
|
31
|
+
.rpc-warning a:hover{color:#fed7aa}
|
|
32
|
+
.error-banner{border-radius:6px;border:1px solid #7f1d1d;background:rgba(69,10,10,.3);padding:8px 12px;font-size:12px;color:#f87171;margin-bottom:12px}
|
|
33
|
+
/* Fleet tabs */
|
|
34
|
+
.fleet-tabs{display:flex;align-items:center;gap:4px;margin-bottom:12px;overflow-x:auto}
|
|
35
|
+
.fleet-tab{padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;white-space:nowrap;border:1px solid transparent;background:var(--card);color:var(--text-dim);transition:all .15s}
|
|
36
|
+
.fleet-tab:hover{color:var(--text)}
|
|
37
|
+
.fleet-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
38
|
+
/* Stats grid */
|
|
39
|
+
.stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;background:var(--card-border);border-radius:8px;overflow:hidden;margin-bottom:12px}
|
|
40
|
+
@media(min-width:640px){.stats-grid{grid-template-columns:repeat(5,1fr)}}
|
|
41
|
+
@media(min-width:1024px){.stats-grid{grid-template-columns:repeat(12,1fr)}}
|
|
42
|
+
.stat-cell{background:var(--card);padding:8px 12px}
|
|
43
|
+
.stat-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
|
|
44
|
+
.stat-value{font-size:14px;font-weight:600}
|
|
45
|
+
.stat-value.highlight{color:var(--accent)}
|
|
46
|
+
.stat-sub{font-size:10px;color:var(--text-dim)}
|
|
47
|
+
/* Wallet grid */
|
|
48
|
+
.wallet-grid{display:grid;grid-template-columns:1fr;gap:8px}
|
|
49
|
+
@media(min-width:640px){.wallet-grid{grid-template-columns:repeat(2,1fr)}}
|
|
50
|
+
@media(min-width:1024px){.wallet-grid{grid-template-columns:repeat(3,1fr)}}
|
|
51
|
+
@media(min-width:1280px){.wallet-grid{grid-template-columns:repeat(4,1fr)}}
|
|
52
|
+
.wallet-card{border-radius:6px;border:1px solid var(--card-border);background:var(--card);overflow:hidden}
|
|
53
|
+
.wallet-card.active{border-color:var(--accent)}
|
|
54
|
+
.wallet-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid var(--card-border)}
|
|
55
|
+
.wallet-addr{font-size:11px;font-weight:500;color:var(--text-dim);transition:color .15s}
|
|
56
|
+
.wallet-addr:hover{color:var(--accent)}
|
|
57
|
+
.wallet-stats{display:flex;align-items:center;gap:12px;font-size:11px}
|
|
58
|
+
.wallet-stats .hp{color:var(--rarity-uncommon)}
|
|
59
|
+
.wallet-stats .dim{color:var(--text-dim)}
|
|
60
|
+
.wallet-stats .agent-hl{color:var(--accent)}
|
|
61
|
+
.miners-wrap{display:flex;flex-wrap:wrap;gap:8px;padding:8px 10px}
|
|
62
|
+
.miner-thumb{display:flex;align-items:center;gap:4px;text-decoration:none}
|
|
63
|
+
.miner-thumb:hover .miner-img{box-shadow:0 0 0 1px var(--accent)}
|
|
64
|
+
.miner-thumb:hover .miner-id{color:var(--accent)}
|
|
65
|
+
.miner-img{height:40px;width:40px;border-radius:4px;transition:box-shadow .15s}
|
|
66
|
+
.miner-placeholder{height:40px;width:40px;border-radius:4px;background:rgba(255,255,255,.05);animation:pulse 1.5s infinite}
|
|
67
|
+
.miner-id{font-size:9px;color:var(--text-dim);transition:color .15s}
|
|
68
|
+
.no-miners{padding:8px 10px;font-size:10px;color:var(--text-dim)}
|
|
69
|
+
/* Empty state */
|
|
70
|
+
.empty-state{border-radius:6px;border:1px solid var(--card-border);background:var(--card);padding:16px 20px;font-size:12px;color:var(--text-dim)}
|
|
71
|
+
.empty-state h2{font-size:14px;color:var(--text);text-align:center;margin-bottom:12px}
|
|
72
|
+
.empty-state code{color:var(--accent)}
|
|
73
|
+
.empty-state .cmds{margin:8px 0 0 8px;line-height:2}
|
|
74
|
+
.empty-state .hint{margin-top:12px;font-size:10px}
|
|
75
|
+
/* Loading */
|
|
76
|
+
.loading{text-align:center;padding:80px 0;color:var(--text-dim)}
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<div class="container">
|
|
81
|
+
<div class="header">
|
|
82
|
+
<h1>APoW DASHBOARD</h1>
|
|
83
|
+
<div class="status"><span class="pulse" id="statusDot"></span><span id="statusText">Loading...</span></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div id="rpcWarning" class="rpc-warning" style="display:none">
|
|
86
|
+
Using public Base RPC (mainnet.base.org) — this is unreliable for dashboards with many wallets.
|
|
87
|
+
Get a free dedicated endpoint at <a href="https://www.alchemy.com/base" target="_blank" rel="noopener noreferrer">alchemy.com</a> for reliable data.
|
|
88
|
+
</div>
|
|
89
|
+
<div id="errorBanner" class="error-banner" style="display:none">Failed to fetch data. Check RPC connection.</div>
|
|
90
|
+
<div id="fleetTabs" class="fleet-tabs" style="display:none"></div>
|
|
91
|
+
<div id="statsGrid" class="stats-grid" style="display:none"></div>
|
|
92
|
+
<div id="walletGrid" class="wallet-grid"></div>
|
|
93
|
+
<div id="emptyState" class="empty-state" style="display:none">
|
|
94
|
+
<h2>No wallets detected.</h2>
|
|
95
|
+
<p>To add wallets:</p>
|
|
96
|
+
<div class="cmds">
|
|
97
|
+
<div><code>apow dashboard add <address></code> <span>— add a specific address</span></div>
|
|
98
|
+
<div><code>apow dashboard scan</code> <span>— auto-detect from wallet files in current dir</span></div>
|
|
99
|
+
</div>
|
|
100
|
+
<p class="hint">Wallets are also auto-detected from your .env PRIVATE_KEY on dashboard start.</p>
|
|
101
|
+
</div>
|
|
102
|
+
<div id="loading" class="loading">Loading...</div>
|
|
103
|
+
</div>
|
|
104
|
+
<script>
|
|
105
|
+
(function(){
|
|
106
|
+
var activeFleet = 'All';
|
|
107
|
+
var balanceHistory = [];
|
|
108
|
+
var prevMines = {};
|
|
109
|
+
var activeWallets = {};
|
|
110
|
+
var lastSeen = {};
|
|
111
|
+
var pollCount = 0;
|
|
112
|
+
|
|
113
|
+
function fetchJson(url) {
|
|
114
|
+
return fetch(url).then(function(r) {
|
|
115
|
+
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
|
|
116
|
+
return r.json();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fmt(n, d) { return Number(n).toLocaleString(undefined, { maximumFractionDigits: d !== undefined ? d : 1 }); }
|
|
121
|
+
function fmtFixed(n, d) { return Number(n).toFixed(d); }
|
|
122
|
+
|
|
123
|
+
function statCellHtml(label, value, opts) {
|
|
124
|
+
opts = opts || {};
|
|
125
|
+
var cls = 'stat-value' + (opts.highlight ? ' highlight' : '');
|
|
126
|
+
var sub = opts.sub ? '<div class="stat-sub">' + opts.sub + '</div>' : '';
|
|
127
|
+
return '<div class="stat-cell"><div class="stat-label">' + label + '</div><div class="' + cls + '">' + value + '</div>' + sub + '</div>';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function shortAddr(addr) { return addr.slice(0, 6) + '...' + addr.slice(-4); }
|
|
131
|
+
|
|
132
|
+
function escapeHtml(s) {
|
|
133
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function updateMiningRate(totalAgent) {
|
|
137
|
+
if (totalAgent <= 0) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
|
|
138
|
+
var now = Date.now();
|
|
139
|
+
var h = balanceHistory;
|
|
140
|
+
if (h.length === 0 || h[h.length - 1].agent !== totalAgent) {
|
|
141
|
+
h.push({ agent: totalAgent, timestamp: now });
|
|
142
|
+
}
|
|
143
|
+
if (h.length > 120) h.splice(0, h.length - 120);
|
|
144
|
+
if (h.length < 2) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
|
|
145
|
+
var first = h[0], last = h[h.length - 1];
|
|
146
|
+
var elapsedMin = (last.timestamp - first.timestamp) / 60000;
|
|
147
|
+
if (elapsedMin < 0.5) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
|
|
148
|
+
var gained = last.agent - first.agent;
|
|
149
|
+
var perMin = gained / elapsedMin;
|
|
150
|
+
return { perMin: perMin, perHour: perMin * 60, sessionGain: gained, sessionMinutes: Math.floor(elapsedMin) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updateActiveWallets(wallets) {
|
|
154
|
+
if (!wallets) return;
|
|
155
|
+
pollCount++;
|
|
156
|
+
// Skip first 2 polls to establish stable baseline (avoids false positives on load/refresh)
|
|
157
|
+
if (pollCount <= 2) {
|
|
158
|
+
for (var i = 0; i < wallets.length; i++) {
|
|
159
|
+
var w = wallets[i];
|
|
160
|
+
var totalMines = 0;
|
|
161
|
+
for (var j = 0; j < w.miners.length; j++) totalMines += Number(w.miners[j].mineCount);
|
|
162
|
+
prevMines[w.address] = totalMines;
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
var now = Date.now();
|
|
167
|
+
for (var i = 0; i < wallets.length; i++) {
|
|
168
|
+
var w = wallets[i];
|
|
169
|
+
var totalMines = 0;
|
|
170
|
+
for (var j = 0; j < w.miners.length; j++) totalMines += Number(w.miners[j].mineCount);
|
|
171
|
+
var prev = prevMines[w.address];
|
|
172
|
+
if (prev !== undefined && totalMines > prev) {
|
|
173
|
+
activeWallets[w.address] = true;
|
|
174
|
+
lastSeen[w.address] = now;
|
|
175
|
+
}
|
|
176
|
+
if (lastSeen[w.address] && now - lastSeen[w.address] > 300000) {
|
|
177
|
+
delete activeWallets[w.address];
|
|
178
|
+
}
|
|
179
|
+
prevMines[w.address] = totalMines;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderFleetTabs(fleets) {
|
|
184
|
+
var el = document.getElementById('fleetTabs');
|
|
185
|
+
if (!fleets || fleets.length <= 1) { el.style.display = 'none'; return; }
|
|
186
|
+
el.style.display = 'flex';
|
|
187
|
+
var totalCount = 0;
|
|
188
|
+
for (var i = 0; i < fleets.length; i++) totalCount += fleets[i].walletCount;
|
|
189
|
+
var html = '<div class="fleet-tab' + (activeFleet === 'All' ? ' active' : '') + '" data-fleet="All">All (' + totalCount + ')</div>';
|
|
190
|
+
for (var i = 0; i < fleets.length; i++) {
|
|
191
|
+
var f = fleets[i];
|
|
192
|
+
html += '<div class="fleet-tab' + (activeFleet === f.name ? ' active' : '') + '" data-fleet="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + ' (' + f.walletCount + ')</div>';
|
|
193
|
+
}
|
|
194
|
+
el.innerHTML = html;
|
|
195
|
+
var tabs = el.querySelectorAll('.fleet-tab');
|
|
196
|
+
for (var t = 0; t < tabs.length; t++) {
|
|
197
|
+
tabs[t].addEventListener('click', function() {
|
|
198
|
+
activeFleet = this.getAttribute('data-fleet');
|
|
199
|
+
refresh();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderStats(wallets, network) {
|
|
205
|
+
var el = document.getElementById('statsGrid');
|
|
206
|
+
if (!wallets && !network) { el.style.display = 'none'; return; }
|
|
207
|
+
el.style.display = 'grid';
|
|
208
|
+
var totalAgent = 0, totalEth = 0, totalMiners = 0, totalHashpower = 0;
|
|
209
|
+
if (wallets) {
|
|
210
|
+
for (var i = 0; i < wallets.length; i++) {
|
|
211
|
+
totalAgent += Number(wallets[i].agentBalance);
|
|
212
|
+
totalEth += Number(wallets[i].ethBalance);
|
|
213
|
+
totalMiners += wallets[i].miners.length;
|
|
214
|
+
for (var j = 0; j < wallets[i].miners.length; j++) totalHashpower += wallets[i].miners[j].hashpower;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var rate = updateMiningRate(totalAgent);
|
|
218
|
+
var html = '';
|
|
219
|
+
html += statCellHtml('TOTAL AGENT', fmt(totalAgent, 1), { highlight: true });
|
|
220
|
+
html += statCellHtml('TOTAL ETH', fmtFixed(totalEth, 4));
|
|
221
|
+
html += statCellHtml('WALLETS', wallets ? String(wallets.length) : '\\u2014');
|
|
222
|
+
html += statCellHtml('MINERS', String(totalMiners));
|
|
223
|
+
html += statCellHtml('HASHPOWER', fmtFixed(totalHashpower / 100, 1) + 'x');
|
|
224
|
+
html += statCellHtml('AGENT/MIN', rate.perMin !== null ? fmtFixed(rate.perMin, 2) : '\\u2014', { highlight: rate.perMin !== null && rate.perMin > 0 });
|
|
225
|
+
html += statCellHtml('AGENT/HR', rate.perHour !== null ? fmtFixed(rate.perHour, 1) : '\\u2014', {
|
|
226
|
+
highlight: rate.perHour !== null && rate.perHour > 0,
|
|
227
|
+
sub: rate.sessionGain !== null ? '+' + fmtFixed(rate.sessionGain, 1) + ' in ' + rate.sessionMinutes + 'm' : undefined
|
|
228
|
+
});
|
|
229
|
+
if (network) {
|
|
230
|
+
html += statCellHtml('ERA', String(network.era), { sub: fmt(network.minesUntilNextEra, 0) + ' to next' });
|
|
231
|
+
html += statCellHtml('BASE REWARD', fmtFixed(network.baseReward, 2) + ' AGENT/mine');
|
|
232
|
+
html += statCellHtml('SUPPLY', fmtFixed(network.supplyPct, 2) + '%');
|
|
233
|
+
html += statCellHtml('DIFFICULTY', String(network.difficulty));
|
|
234
|
+
html += statCellHtml('NETWORK MINES', fmt(network.totalMines, 0));
|
|
235
|
+
}
|
|
236
|
+
el.innerHTML = html;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderWallets(wallets) {
|
|
240
|
+
var grid = document.getElementById('walletGrid');
|
|
241
|
+
var empty = document.getElementById('emptyState');
|
|
242
|
+
if (!wallets || wallets.length === 0) {
|
|
243
|
+
grid.innerHTML = '';
|
|
244
|
+
if (wallets) empty.style.display = 'block';
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
empty.style.display = 'none';
|
|
248
|
+
wallets = wallets.slice().sort(function(a, b) { return Number(b.agentBalance) - Number(a.agentBalance); });
|
|
249
|
+
var html = '';
|
|
250
|
+
for (var i = 0; i < wallets.length; i++) {
|
|
251
|
+
var w = wallets[i];
|
|
252
|
+
var isActive = !!activeWallets[w.address];
|
|
253
|
+
var hasAgent = Number(w.agentBalance) > 0;
|
|
254
|
+
var walletHp = 0;
|
|
255
|
+
for (var j = 0; j < w.miners.length; j++) walletHp += w.miners[j].hashpower;
|
|
256
|
+
html += '<div class="wallet-card' + (isActive ? ' active' : '') + '">';
|
|
257
|
+
html += '<div class="wallet-header">';
|
|
258
|
+
html += '<a class="wallet-addr" href="https://basescan.org/address/' + w.address + '" target="_blank" rel="noopener noreferrer">' + shortAddr(w.address) + '</a>';
|
|
259
|
+
html += '<div class="wallet-stats">';
|
|
260
|
+
if (w.miners.length > 0) html += '<span class="hp">' + fmtFixed(walletHp / 100, 1) + 'x</span>';
|
|
261
|
+
html += '<span><span class="dim">E </span>' + fmtFixed(Number(w.ethBalance), 4) + '</span>';
|
|
262
|
+
html += '<span' + (hasAgent ? ' class="agent-hl"' : '') + '><span class="dim">A </span>' + fmt(Number(w.agentBalance), 1) + '</span>';
|
|
263
|
+
html += '</div></div>';
|
|
264
|
+
if (w.miners.length > 0) {
|
|
265
|
+
html += '<div class="miners-wrap">';
|
|
266
|
+
for (var j = 0; j < w.miners.length; j++) {
|
|
267
|
+
var m = w.miners[j];
|
|
268
|
+
var osUrl = 'https://opensea.io/item/base/0xb7cad3ca5f2bd8aec2eb67d6e8d448099b3bc03d/' + m.tokenId;
|
|
269
|
+
html += '<a class="miner-thumb" href="' + osUrl + '" target="_blank" rel="noopener noreferrer">';
|
|
270
|
+
if (m.imageUri) {
|
|
271
|
+
html += '<img class="miner-img" src="' + escapeHtml(m.imageUri) + '" alt="#' + m.tokenId + '">';
|
|
272
|
+
} else {
|
|
273
|
+
html += '<div class="miner-placeholder"></div>';
|
|
274
|
+
}
|
|
275
|
+
html += '<span class="miner-id">#' + m.tokenId + '</span></a>';
|
|
276
|
+
}
|
|
277
|
+
html += '</div>';
|
|
278
|
+
} else {
|
|
279
|
+
html += '<div class="no-miners">No miners</div>';
|
|
280
|
+
}
|
|
281
|
+
html += '</div>';
|
|
282
|
+
}
|
|
283
|
+
grid.innerHTML = html;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var networkData = null;
|
|
287
|
+
var walletsData = null;
|
|
288
|
+
var hasError = false;
|
|
289
|
+
|
|
290
|
+
function setStatus(refreshing) {
|
|
291
|
+
document.getElementById('statusDot').style.display = refreshing ? 'inline-block' : 'none';
|
|
292
|
+
document.getElementById('statusText').textContent = refreshing ? 'Refreshing...' : 'Live';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function refresh() {
|
|
296
|
+
setStatus(true);
|
|
297
|
+
var fleetParam = encodeURIComponent(activeFleet);
|
|
298
|
+
Promise.all([
|
|
299
|
+
fetchJson('/api/network').catch(function(e) { return null; }),
|
|
300
|
+
fetchJson('/api/wallets?fleet=' + fleetParam).catch(function(e) { return null; }),
|
|
301
|
+
fetchJson('/api/fleets').catch(function(e) { return null; }),
|
|
302
|
+
fetchJson('/api/config').catch(function(e) { return null; })
|
|
303
|
+
]).then(function(results) {
|
|
304
|
+
document.getElementById('loading').style.display = 'none';
|
|
305
|
+
var net = results[0], wal = results[1], fleets = results[2], cfg = results[3];
|
|
306
|
+
hasError = !net && !wal;
|
|
307
|
+
document.getElementById('errorBanner').style.display = hasError ? 'block' : 'none';
|
|
308
|
+
if (net) networkData = net;
|
|
309
|
+
if (wal && !wal.error) {
|
|
310
|
+
walletsData = wal;
|
|
311
|
+
updateActiveWallets(wal);
|
|
312
|
+
}
|
|
313
|
+
if (cfg) {
|
|
314
|
+
document.getElementById('rpcWarning').style.display = cfg.rpcIsDefault ? 'block' : 'none';
|
|
315
|
+
}
|
|
316
|
+
renderFleetTabs(fleets);
|
|
317
|
+
renderStats(walletsData, networkData);
|
|
318
|
+
renderWallets(walletsData);
|
|
319
|
+
setStatus(false);
|
|
320
|
+
}).catch(function() {
|
|
321
|
+
document.getElementById('loading').style.display = 'none';
|
|
322
|
+
document.getElementById('errorBanner').style.display = 'block';
|
|
323
|
+
setStatus(false);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
refresh();
|
|
328
|
+
setInterval(refresh, 30000);
|
|
329
|
+
})();
|
|
330
|
+
</script>
|
|
331
|
+
</body>
|
|
332
|
+
</html>`;
|
|
333
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
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
|
+
// 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
|
+
}
|
|
250
|
+
async function handleWallets(fleetParam) {
|
|
251
|
+
const addresses = getAddressesForFleet(fleetParam, walletsPath);
|
|
252
|
+
if (addresses.length === 0)
|
|
253
|
+
return "[]";
|
|
254
|
+
// Phase 1: ETH balance + AGENT balance + NFT count (chunked)
|
|
255
|
+
const phase1Contracts = addresses.flatMap((addr) => [
|
|
256
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "balanceOf", args: [addr] },
|
|
257
|
+
{ address: miningAgentAddress, abi: MiningAgentAbi, functionName: "balanceOf", args: [addr] },
|
|
258
|
+
]);
|
|
259
|
+
const [balances, multicallResults] = await Promise.all([
|
|
260
|
+
Promise.all(addresses.map((addr) => publicClient.getBalance({ address: addr }).catch(() => 0n))),
|
|
261
|
+
chunkedMulticall(phase1Contracts),
|
|
262
|
+
]);
|
|
263
|
+
const walletInfos = addresses.map((addr, i) => ({
|
|
264
|
+
address: addr,
|
|
265
|
+
ethBalance: balances[i],
|
|
266
|
+
agentBalance: multicallResults[i * 2]?.result ?? 0n,
|
|
267
|
+
nftCount: Number(multicallResults[i * 2 + 1]?.result ?? 0n),
|
|
268
|
+
}));
|
|
269
|
+
// Phase 2: token IDs (chunked)
|
|
270
|
+
const tokenIdContracts = [];
|
|
271
|
+
const tokenIdMap = [];
|
|
272
|
+
for (let wi = 0; wi < walletInfos.length; wi++) {
|
|
273
|
+
const info = walletInfos[wi];
|
|
274
|
+
for (let mi = 0; mi < info.nftCount; mi++) {
|
|
275
|
+
tokenIdContracts.push({
|
|
276
|
+
address: miningAgentAddress,
|
|
277
|
+
abi: MiningAgentAbi,
|
|
278
|
+
functionName: "tokenOfOwnerByIndex",
|
|
279
|
+
args: [info.address, BigInt(mi)],
|
|
280
|
+
});
|
|
281
|
+
tokenIdMap.push({ walletIdx: wi });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const tokenIds = [];
|
|
285
|
+
const validTokenMap = [];
|
|
286
|
+
if (tokenIdContracts.length > 0) {
|
|
287
|
+
const tokenIdResults = await chunkedMulticall(tokenIdContracts);
|
|
288
|
+
for (let i = 0; i < tokenIdResults.length; i++) {
|
|
289
|
+
const r = tokenIdResults[i];
|
|
290
|
+
if (r.status === "success" && r.result != null) {
|
|
291
|
+
validTokenMap.push(i);
|
|
292
|
+
tokenIds.push(r.result);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// No miners — return early
|
|
297
|
+
if (tokenIds.length === 0) {
|
|
298
|
+
const wallets = walletInfos.map((info) => ({
|
|
299
|
+
address: info.address,
|
|
300
|
+
ethBalance: (0, viem_1.formatEther)(info.ethBalance),
|
|
301
|
+
agentBalance: (0, viem_1.formatEther)(info.agentBalance),
|
|
302
|
+
miners: [],
|
|
303
|
+
}));
|
|
304
|
+
return JSON.stringify(wallets);
|
|
305
|
+
}
|
|
306
|
+
// Phase 3: miner stats (chunked) + art
|
|
307
|
+
const uncachedTokenIds = tokenIds.filter((id) => !artCache.has(id.toString()));
|
|
308
|
+
const FIELDS_PER_TOKEN = 5;
|
|
309
|
+
const detailContracts = tokenIds.flatMap((tokenId) => [
|
|
310
|
+
{ address: miningAgentAddress, abi: MiningAgentAbi, functionName: "rarity", args: [tokenId] },
|
|
311
|
+
{ address: miningAgentAddress, abi: MiningAgentAbi, functionName: "hashpower", args: [tokenId] },
|
|
312
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenMineCount", args: [tokenId] },
|
|
313
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenEarnings", args: [tokenId] },
|
|
314
|
+
{ address: miningAgentAddress, abi: MiningAgentAbi, functionName: "mintBlock", args: [tokenId] },
|
|
315
|
+
]);
|
|
316
|
+
const detailResults = await chunkedMulticall(detailContracts);
|
|
317
|
+
// Batch art URI calls for uncached tokens — small chunks (tokenURI returns ~41KB each)
|
|
318
|
+
const ART_CHUNK_SIZE = 2;
|
|
319
|
+
const artResults = [];
|
|
320
|
+
for (let i = 0; i < uncachedTokenIds.length; i += ART_CHUNK_SIZE) {
|
|
321
|
+
const chunk = uncachedTokenIds.slice(i, i + ART_CHUNK_SIZE);
|
|
322
|
+
const artContracts = chunk.map((tokenId) => ({
|
|
323
|
+
address: miningAgentAddress,
|
|
324
|
+
abi: MiningAgentAbi,
|
|
325
|
+
functionName: "tokenURI",
|
|
326
|
+
args: [tokenId],
|
|
327
|
+
}));
|
|
328
|
+
try {
|
|
329
|
+
const chunkResults = await publicClient.multicall({ contracts: artContracts });
|
|
330
|
+
artResults.push(...chunkResults);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
334
|
+
artResults.push({ status: "failure" });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Populate art cache
|
|
339
|
+
for (let i = 0; i < uncachedTokenIds.length; i++) {
|
|
340
|
+
const r = artResults[i];
|
|
341
|
+
if (r && r.status === "success" && r.result) {
|
|
342
|
+
const imageUri = parseArtFromTokenUri(r.result);
|
|
343
|
+
if (imageUri)
|
|
344
|
+
artCache.set(uncachedTokenIds[i].toString(), imageUri);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Build wallet data
|
|
348
|
+
const minersByWallet = new Map();
|
|
349
|
+
for (let ti = 0; ti < tokenIds.length; ti++) {
|
|
350
|
+
const originalIdx = validTokenMap[ti];
|
|
351
|
+
const { walletIdx } = tokenIdMap[originalIdx];
|
|
352
|
+
const b = ti * FIELDS_PER_TOKEN;
|
|
353
|
+
const rarity = Number(detailResults[b].result ?? 0n);
|
|
354
|
+
const hashpower = Number(detailResults[b + 1].result ?? 100n);
|
|
355
|
+
const mineCount = detailResults[b + 2].result ?? 0n;
|
|
356
|
+
const earnings = detailResults[b + 3].result ?? 0n;
|
|
357
|
+
const mintBlock = detailResults[b + 4].result ?? 0n;
|
|
358
|
+
const tid = tokenIds[ti].toString();
|
|
359
|
+
const miner = {
|
|
360
|
+
tokenId: tid,
|
|
361
|
+
rarity,
|
|
362
|
+
rarityLabel: RARITY_LABELS[rarity] ?? `Tier ${rarity}`,
|
|
363
|
+
hashpower,
|
|
364
|
+
mineCount: mineCount.toString(),
|
|
365
|
+
earnings: (0, viem_1.formatEther)(earnings),
|
|
366
|
+
mintBlock: mintBlock.toString(),
|
|
367
|
+
imageUri: artCache.get(tid),
|
|
368
|
+
};
|
|
369
|
+
if (!minersByWallet.has(walletIdx))
|
|
370
|
+
minersByWallet.set(walletIdx, []);
|
|
371
|
+
minersByWallet.get(walletIdx).push(miner);
|
|
372
|
+
}
|
|
373
|
+
const wallets = walletInfos.map((info, i) => ({
|
|
374
|
+
address: info.address,
|
|
375
|
+
ethBalance: (0, viem_1.formatEther)(info.ethBalance),
|
|
376
|
+
agentBalance: (0, viem_1.formatEther)(info.agentBalance),
|
|
377
|
+
miners: minersByWallet.get(i) ?? [],
|
|
378
|
+
}));
|
|
379
|
+
return JSON.stringify(wallets);
|
|
380
|
+
}
|
|
381
|
+
async function handleNetwork() {
|
|
382
|
+
const results = await publicClient.multicall({
|
|
383
|
+
contracts: [
|
|
384
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "totalMines" },
|
|
385
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "totalMinted" },
|
|
386
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "miningTarget" },
|
|
387
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "MINEABLE_SUPPLY" },
|
|
388
|
+
{ address: agentCoinAddress, abi: AgentCoinAbi, functionName: "ERA_INTERVAL" },
|
|
389
|
+
],
|
|
390
|
+
});
|
|
391
|
+
const totalMines = results[0].result;
|
|
392
|
+
const totalMinted = results[1].result;
|
|
393
|
+
const miningTarget = results[2].result;
|
|
394
|
+
const mineableSupply = results[3].result;
|
|
395
|
+
const eraInterval = results[4].result;
|
|
396
|
+
const era = totalMines / eraInterval;
|
|
397
|
+
const minesUntilNextEra = eraInterval - (totalMines % eraInterval);
|
|
398
|
+
const supplyPct = Number((totalMinted * 10000n) / mineableSupply) / 100;
|
|
399
|
+
let baseReward = 3;
|
|
400
|
+
for (let i = 0n; i < era; i++) {
|
|
401
|
+
baseReward *= 0.9;
|
|
402
|
+
}
|
|
403
|
+
const nextEraReward = baseReward * 0.9;
|
|
404
|
+
const targetLog = Math.log2(Number(miningTarget));
|
|
405
|
+
const difficulty = targetLog > 250 ? "very easy" :
|
|
406
|
+
targetLog > 240 ? "easy" :
|
|
407
|
+
targetLog > 220 ? "moderate" :
|
|
408
|
+
targetLog > 200 ? "hard" : "very hard";
|
|
409
|
+
return JSON.stringify({
|
|
410
|
+
totalMines: totalMines.toString(),
|
|
411
|
+
totalMinted: (0, viem_1.formatEther)(totalMinted),
|
|
412
|
+
mineableSupply: (0, viem_1.formatEther)(mineableSupply),
|
|
413
|
+
era: Number(era),
|
|
414
|
+
minesUntilNextEra: minesUntilNextEra.toString(),
|
|
415
|
+
baseReward,
|
|
416
|
+
nextEraReward,
|
|
417
|
+
difficulty,
|
|
418
|
+
supplyPct,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function handleFleets() {
|
|
422
|
+
const fleets = getFleets(walletsPath);
|
|
423
|
+
return JSON.stringify(fleets.map((f) => ({ name: f.name, walletCount: f.addresses.length })));
|
|
424
|
+
}
|
|
425
|
+
function handleConfig() {
|
|
426
|
+
const rpcIsDefault = rpcUrl === DEFAULT_RPC;
|
|
427
|
+
const walletCount = getWalletAddresses(walletsPath).length;
|
|
428
|
+
return JSON.stringify({ rpcIsDefault, walletCount });
|
|
429
|
+
}
|
|
430
|
+
function jsonResponse(res, body, status = 200) {
|
|
431
|
+
res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
432
|
+
res.end(body);
|
|
433
|
+
}
|
|
434
|
+
function errorResponse(res, message, status = 500) {
|
|
435
|
+
jsonResponse(res, JSON.stringify({ error: message }), status);
|
|
436
|
+
}
|
|
437
|
+
const server = http.createServer(async (req, res) => {
|
|
438
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
439
|
+
const pathname = url.pathname;
|
|
440
|
+
try {
|
|
441
|
+
if (pathname === "/" || pathname === "") {
|
|
442
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
443
|
+
res.end(htmlPage);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (pathname === "/api/wallets") {
|
|
447
|
+
const fleet = url.searchParams.get("fleet");
|
|
448
|
+
const cacheKey = `wallets:${fleet ?? "All"}`;
|
|
449
|
+
const body = await cachedHandler(cacheKey, () => handleWallets(fleet));
|
|
450
|
+
jsonResponse(res, body);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (pathname === "/api/network") {
|
|
454
|
+
const body = await cachedHandler("network", () => handleNetwork());
|
|
455
|
+
jsonResponse(res, body);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (pathname === "/api/fleets") {
|
|
459
|
+
const body = handleFleets();
|
|
460
|
+
jsonResponse(res, body);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (pathname === "/api/config") {
|
|
464
|
+
const body = handleConfig();
|
|
465
|
+
jsonResponse(res, body);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
469
|
+
res.end("Not Found");
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
473
|
+
errorResponse(res, message);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
server.listen(port);
|
|
477
|
+
return server;
|
|
478
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
const node_fs_1 = require("node:fs");
|
|
41
41
|
const node_path_1 = require("node:path");
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
43
|
const commander_1 = require("commander");
|
|
43
44
|
const viem_1 = require("viem");
|
|
44
45
|
const MiningAgent_json_1 = __importDefault(require("./abi/MiningAgent.json"));
|
|
@@ -470,8 +471,198 @@ async function main() {
|
|
|
470
471
|
console.log(` Tx: ${ui.dim((0, explorer_1.txUrl)(receipt.transactionHash))}`);
|
|
471
472
|
console.log("");
|
|
472
473
|
});
|
|
474
|
+
// --- Dashboard commands ---
|
|
475
|
+
const dashboardCmd = program
|
|
476
|
+
.command("dashboard")
|
|
477
|
+
.description("Multi-wallet mining dashboard");
|
|
478
|
+
dashboardCmd
|
|
479
|
+
.command("start", { isDefault: true })
|
|
480
|
+
.description("Launch the dashboard web UI")
|
|
481
|
+
.action(async () => {
|
|
482
|
+
const walletsPath = getWalletsPath();
|
|
483
|
+
// Seed wallets.json if it doesn't exist
|
|
484
|
+
if (!(0, node_fs_1.existsSync)(walletsPath)) {
|
|
485
|
+
const walletsDir = (0, node_path_1.join)(process.env.HOME ?? "", ".apow");
|
|
486
|
+
if (!(0, node_fs_1.existsSync)(walletsDir))
|
|
487
|
+
(0, node_fs_1.mkdirSync)(walletsDir, { recursive: true });
|
|
488
|
+
const initial = wallet_1.account ? [wallet_1.account.address] : [];
|
|
489
|
+
(0, node_fs_1.writeFileSync)(walletsPath, JSON.stringify(initial, null, 2), "utf8");
|
|
490
|
+
if (wallet_1.account) {
|
|
491
|
+
ui.ok(`Seeded ${walletsPath} with ${wallet_1.account.address.slice(0, 6)}...${wallet_1.account.address.slice(-4)}`);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
ui.ok(`Created ${walletsPath} (empty — add wallets with: apow dashboard add <address>)`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Auto-detect wallets from CWD
|
|
498
|
+
const { addresses, newCount } = detectWallets(process.cwd());
|
|
499
|
+
if (newCount > 0) {
|
|
500
|
+
ui.ok(`Detected ${addresses.length} wallets (${newCount} new)`);
|
|
501
|
+
}
|
|
502
|
+
else if (addresses.length > 0) {
|
|
503
|
+
console.log(` ${ui.dim(`${addresses.length} wallets loaded`)}`);
|
|
504
|
+
}
|
|
505
|
+
const { startDashboardServer } = await Promise.resolve().then(() => __importStar(require("./dashboard")));
|
|
506
|
+
console.log("");
|
|
507
|
+
console.log(` ${ui.bold("APoW Dashboard")} starting on http://localhost:3847`);
|
|
508
|
+
console.log(` ${ui.dim("Press Ctrl+C to stop")}`);
|
|
509
|
+
console.log("");
|
|
510
|
+
const server = startDashboardServer({
|
|
511
|
+
port: 3847,
|
|
512
|
+
walletsPath,
|
|
513
|
+
rpcUrl: config_1.config.rpcUrl,
|
|
514
|
+
miningAgentAddress: config_1.config.miningAgentAddress,
|
|
515
|
+
agentCoinAddress: config_1.config.agentCoinAddress,
|
|
516
|
+
});
|
|
517
|
+
// Open browser after short delay (server starts instantly)
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
520
|
+
(0, node_child_process_1.spawn)(openCmd, ["http://localhost:3847"], { stdio: "ignore" });
|
|
521
|
+
}, 500);
|
|
522
|
+
// Wait for SIGINT
|
|
523
|
+
await new Promise((resolve) => {
|
|
524
|
+
process.on("SIGINT", () => {
|
|
525
|
+
server.close();
|
|
526
|
+
resolve();
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
dashboardCmd
|
|
531
|
+
.command("add <address>")
|
|
532
|
+
.description("Add a wallet address to monitor")
|
|
533
|
+
.action((address) => {
|
|
534
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
535
|
+
ui.error("Invalid address. Must be 0x + 40 hex characters.");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const walletsPath = getWalletsPath();
|
|
539
|
+
const wallets = loadWallets(walletsPath);
|
|
540
|
+
const lower = address.toLowerCase();
|
|
541
|
+
if (wallets.some((w) => w.toLowerCase() === lower)) {
|
|
542
|
+
ui.warn("Address already monitored.");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
wallets.push(address);
|
|
546
|
+
saveWallets(walletsPath, wallets);
|
|
547
|
+
ui.ok(`Added ${address.slice(0, 6)}...${address.slice(-4)} (${wallets.length} wallets total)`);
|
|
548
|
+
});
|
|
549
|
+
dashboardCmd
|
|
550
|
+
.command("remove <address>")
|
|
551
|
+
.description("Remove a wallet address from monitoring")
|
|
552
|
+
.action((address) => {
|
|
553
|
+
const walletsPath = getWalletsPath();
|
|
554
|
+
const wallets = loadWallets(walletsPath);
|
|
555
|
+
const lower = address.toLowerCase();
|
|
556
|
+
const filtered = wallets.filter((w) => w.toLowerCase() !== lower);
|
|
557
|
+
if (filtered.length === wallets.length) {
|
|
558
|
+
ui.warn("Address not found in wallet list.");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
saveWallets(walletsPath, filtered);
|
|
562
|
+
ui.ok(`Removed ${address.slice(0, 6)}...${address.slice(-4)} (${filtered.length} wallets remaining)`);
|
|
563
|
+
});
|
|
564
|
+
dashboardCmd
|
|
565
|
+
.command("scan [dir]")
|
|
566
|
+
.description("Auto-detect wallets from wallet-0x*.txt files in a directory")
|
|
567
|
+
.action((dir) => {
|
|
568
|
+
const scanDir = dir ?? process.cwd();
|
|
569
|
+
const { addresses, newCount } = detectWallets(scanDir);
|
|
570
|
+
console.log("");
|
|
571
|
+
if (addresses.length === 0) {
|
|
572
|
+
console.log(` No wallets found in ${scanDir}`);
|
|
573
|
+
console.log(` ${ui.dim("Expected files named wallet-0x<address>.txt")}`);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
console.log(` ${ui.bold("Detected Wallets")} (${newCount} new, ${addresses.length} total)`);
|
|
577
|
+
console.log("");
|
|
578
|
+
for (const addr of addresses) {
|
|
579
|
+
console.log(` ${addr}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
console.log("");
|
|
583
|
+
});
|
|
584
|
+
dashboardCmd
|
|
585
|
+
.command("wallets")
|
|
586
|
+
.description("List monitored wallet addresses")
|
|
587
|
+
.action(() => {
|
|
588
|
+
const walletsPath = getWalletsPath();
|
|
589
|
+
const wallets = loadWallets(walletsPath);
|
|
590
|
+
if (wallets.length === 0) {
|
|
591
|
+
console.log(" No wallets configured. Run: apow dashboard add <address>");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
console.log("");
|
|
595
|
+
console.log(` ${ui.bold("Monitored Wallets")} (${wallets.length})`);
|
|
596
|
+
console.log("");
|
|
597
|
+
for (const w of wallets) {
|
|
598
|
+
console.log(` ${w}`);
|
|
599
|
+
}
|
|
600
|
+
console.log("");
|
|
601
|
+
});
|
|
473
602
|
await program.parseAsync(process.argv);
|
|
474
603
|
}
|
|
604
|
+
function getWalletsPath() {
|
|
605
|
+
return (0, node_path_1.join)(process.env.HOME ?? "", ".apow", "wallets.json");
|
|
606
|
+
}
|
|
607
|
+
function loadWallets(path) {
|
|
608
|
+
try {
|
|
609
|
+
const raw = (0, node_fs_1.readFileSync)(path, "utf8");
|
|
610
|
+
const data = JSON.parse(raw);
|
|
611
|
+
return Array.isArray(data) ? data.filter((a) => typeof a === "string") : [];
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function saveWallets(path, wallets) {
|
|
618
|
+
const dir = (0, node_path_1.join)(process.env.HOME ?? "", ".apow");
|
|
619
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
620
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
621
|
+
(0, node_fs_1.writeFileSync)(path, JSON.stringify(wallets, null, 2), "utf8");
|
|
622
|
+
}
|
|
623
|
+
function detectWallets(scanDir) {
|
|
624
|
+
const walletsPath = getWalletsPath();
|
|
625
|
+
const existing = loadWallets(walletsPath);
|
|
626
|
+
const seen = new Set(existing.map((a) => a.toLowerCase()));
|
|
627
|
+
const detected = [];
|
|
628
|
+
// Scan scanDir for wallet-0x*.txt files
|
|
629
|
+
try {
|
|
630
|
+
const entries = (0, node_fs_1.readdirSync)(scanDir, { withFileTypes: true });
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
if (entry.isFile()) {
|
|
633
|
+
const match = entry.name.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
|
|
634
|
+
if (match && !seen.has(match[1].toLowerCase())) {
|
|
635
|
+
detected.push(match[1]);
|
|
636
|
+
seen.add(match[1].toLowerCase());
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Scan rig*/wallet-0x*.txt subdirectories
|
|
640
|
+
if (entry.isDirectory() && entry.name.startsWith("rig")) {
|
|
641
|
+
try {
|
|
642
|
+
const rigFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(scanDir, entry.name));
|
|
643
|
+
for (const file of rigFiles) {
|
|
644
|
+
const m = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
|
|
645
|
+
if (m && !seen.has(m[1].toLowerCase())) {
|
|
646
|
+
detected.push(m[1]);
|
|
647
|
+
seen.add(m[1].toLowerCase());
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// rig dir not readable — skip
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// scanDir not readable
|
|
659
|
+
}
|
|
660
|
+
const merged = [...existing, ...detected];
|
|
661
|
+
if (detected.length > 0) {
|
|
662
|
+
saveWallets(walletsPath, merged);
|
|
663
|
+
}
|
|
664
|
+
return { addresses: merged, newCount: detected.length };
|
|
665
|
+
}
|
|
475
666
|
main().catch((error) => {
|
|
476
667
|
const message = error instanceof Error ? error.message : String(error);
|
|
477
668
|
ui.error(message);
|
package/package.json
CHANGED
package/skill.md
CHANGED
|
@@ -23,6 +23,15 @@ metadata:
|
|
|
23
23
|
configure the miner, mint, and mine. The only step that requires your user is funding the wallet
|
|
24
24
|
with ETH on Base. Everything else you handle yourself.
|
|
25
25
|
|
|
26
|
+
> **IMPORTANT — You need a dedicated RPC endpoint.** The default public RPC (`https://mainnet.base.org`) has aggressive rate limits and **will cause minting and mining to fail**. Before starting, get a free Alchemy endpoint:
|
|
27
|
+
>
|
|
28
|
+
> 1. Go to [alchemy.com](https://www.alchemy.com/) and sign up (free, no credit card)
|
|
29
|
+
> 2. Click **Create new app** → Name: `apow-miner` → Chain: **Base** → Network: **Base Mainnet**
|
|
30
|
+
> 3. Copy the **HTTPS** URL: `https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY`
|
|
31
|
+
> 4. Set `RPC_URL` to this URL in your `.env`
|
|
32
|
+
>
|
|
33
|
+
> Alchemy's free tier (300M compute units/month) is more than enough. See [RPC Recommendations](#rpc-recommendations) for alternatives.
|
|
34
|
+
|
|
26
35
|
---
|
|
27
36
|
|
|
28
37
|
## Quick Start (TL;DR)
|
|
@@ -56,6 +65,7 @@ npx apow-cli wallet new
|
|
|
56
65
|
# 2. Write .env directly (no interactive prompts needed)
|
|
57
66
|
# IMPORTANT: Use an API-based provider (openai/anthropic/gemini), NOT claude-code/codex.
|
|
58
67
|
# Session-based providers are too slow for the 20-second mint challenge window.
|
|
68
|
+
# IMPORTANT: The public Base RPC is unreliable — get a free Alchemy URL (see RPC Recommendations).
|
|
59
69
|
cat > .env << 'EOF'
|
|
60
70
|
PRIVATE_KEY=0x<from step 1>
|
|
61
71
|
RPC_URL=https://mainnet.base.org
|
|
@@ -243,7 +253,8 @@ LLM_MODEL=gpt-4o-mini
|
|
|
243
253
|
|
|
244
254
|
# === Network ===
|
|
245
255
|
|
|
246
|
-
# Base RPC endpoint
|
|
256
|
+
# Base RPC endpoint — the public default is unreliable for sustained mining.
|
|
257
|
+
# Strongly recommend a free Alchemy key: https://www.alchemy.com/ (no credit card)
|
|
247
258
|
RPC_URL=https://mainnet.base.org
|
|
248
259
|
|
|
249
260
|
# Chain: "base" | "baseSepolia" (auto-detected from RPC_URL if omitted)
|
|
@@ -260,7 +271,7 @@ CHAIN=base
|
|
|
260
271
|
| `LLM_PROVIDER` | No | `openai` | LLM provider: `openai`, `anthropic`, `ollama`, `gemini`, `claude-code`, or `codex` |
|
|
261
272
|
| `LLM_API_KEY` | Conditional | -- | API key. Falls back to `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` per provider. Not needed for `ollama`, `claude-code`, or `codex` |
|
|
262
273
|
| `LLM_MODEL` | No | `gpt-4o-mini` | Model identifier passed to the provider |
|
|
263
|
-
| `RPC_URL` |
|
|
274
|
+
| `RPC_URL` | **Strongly recommended** | `https://mainnet.base.org` | Base JSON-RPC endpoint. **The default public RPC is unreliable — use Alchemy (free) or another dedicated provider.** |
|
|
264
275
|
| `CHAIN` | No | `base` | Network selector; auto-detects `baseSepolia` if RPC URL contains "sepolia" |
|
|
265
276
|
| `SOLANA_RPC_URL` | No | `https://api.mainnet-beta.solana.com` | Solana RPC endpoint (only for `apow fund --solana`) |
|
|
266
277
|
| `SQUID_INTEGRATOR_ID` | No | -- | Squid Router integrator ID for deposit address flow (free at [squidrouter.com](https://app.squidrouter.com/)) |
|