hypow 0.0.0-stage → 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/dist/cli.html +103 -0
- package/dist/cli.js +1025 -0
- package/dist/hypow-miner.js +63 -0
- package/dist/search-worker.js +35 -0
- package/package.json +34 -4
- package/README.md +0 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { getAddress as getAddress3, isAddress as isAddress3, parseEther as parseEther2 } from "viem";
|
|
7
|
+
|
|
8
|
+
// src/home.ts
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { getAddress, isAddress, isHex } from "viem";
|
|
13
|
+
import { generatePrivateKey } from "viem/accounts";
|
|
14
|
+
var PUBLIC = "public";
|
|
15
|
+
var home = () => process.env.HYPOW_HOME ?? join(homedir(), ".hypow", "mainnet");
|
|
16
|
+
function write(name, value) {
|
|
17
|
+
mkdirSync(home(), { recursive: true, mode: 448 });
|
|
18
|
+
writeFileSync(join(home(), name), `${value}
|
|
19
|
+
`, { mode: 384 });
|
|
20
|
+
}
|
|
21
|
+
function read(name) {
|
|
22
|
+
const path = join(home(), name);
|
|
23
|
+
return existsSync(path) ? readFileSync(path, "utf8").trim() : void 0;
|
|
24
|
+
}
|
|
25
|
+
function loadKey() {
|
|
26
|
+
const saved = read("key");
|
|
27
|
+
if (saved !== void 0) {
|
|
28
|
+
if (!isHex(saved) || saved.length !== 66) throw new Error(`${join(home(), "key")} is not a private key`);
|
|
29
|
+
return { key: saved, created: false };
|
|
30
|
+
}
|
|
31
|
+
const key = generatePrivateKey();
|
|
32
|
+
write("key", key);
|
|
33
|
+
return { key, created: true };
|
|
34
|
+
}
|
|
35
|
+
function loadOwner() {
|
|
36
|
+
const owner = read("owner");
|
|
37
|
+
return owner !== void 0 && isAddress(owner) ? getAddress(owner) : void 0;
|
|
38
|
+
}
|
|
39
|
+
function saveOwner(owner) {
|
|
40
|
+
write("owner", owner);
|
|
41
|
+
}
|
|
42
|
+
function loadRpc() {
|
|
43
|
+
const rpc = read("rpc");
|
|
44
|
+
return rpc === PUBLIC ? null : rpc;
|
|
45
|
+
}
|
|
46
|
+
function saveRpc(url) {
|
|
47
|
+
write("rpc", url ?? PUBLIC);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/log.ts
|
|
51
|
+
import { styleText } from "node:util";
|
|
52
|
+
var LABEL_STYLES = {
|
|
53
|
+
captured: "green",
|
|
54
|
+
ticket: "cyan",
|
|
55
|
+
error: "red",
|
|
56
|
+
lost: "red",
|
|
57
|
+
missed: "red",
|
|
58
|
+
"gas low": "yellow",
|
|
59
|
+
subaccounts: "yellow",
|
|
60
|
+
pooled: "yellow"
|
|
61
|
+
};
|
|
62
|
+
var BLOCK_STYLE = ["bold", "redBright"];
|
|
63
|
+
var ALARM_STYLE = ["bold", "red"];
|
|
64
|
+
var alarm = (label, message) => label.startsWith("\u26A0") || message.startsWith("\u26A0");
|
|
65
|
+
function paint(style, text) {
|
|
66
|
+
return styleText(style, text, { stream: process.stdout });
|
|
67
|
+
}
|
|
68
|
+
function format(label, message) {
|
|
69
|
+
const time = paint("dim", (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8));
|
|
70
|
+
const padded = label.padEnd(11);
|
|
71
|
+
if (label.startsWith("BLOCK")) return `${time} ${paint(BLOCK_STYLE, `${padded} ${message}`)}`;
|
|
72
|
+
if (alarm(label, message)) return `${time} ${paint(ALARM_STYLE, `${padded} ${message}`)}`;
|
|
73
|
+
const cut = message.indexOf(" \xB7 ");
|
|
74
|
+
const body = cut < 0 ? message : `${message.slice(0, cut)}${paint("dim", message.slice(cut))}`;
|
|
75
|
+
return `${time} ${paint(LABEL_STYLES[label] ?? "dim", padded)} ${body}`;
|
|
76
|
+
}
|
|
77
|
+
function line(label, message) {
|
|
78
|
+
console.log(format(label, message));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/mine.ts
|
|
82
|
+
import { extname } from "node:path";
|
|
83
|
+
import { Worker } from "node:worker_threads";
|
|
84
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
85
|
+
|
|
86
|
+
// src/authorize.ts
|
|
87
|
+
import { spawn } from "node:child_process";
|
|
88
|
+
import { randomBytes } from "node:crypto";
|
|
89
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
90
|
+
import { createServer } from "node:http";
|
|
91
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
92
|
+
import { encodeFunctionData, getAddress as getAddress2, isAddress as isAddress2, isHex as isHex2 } from "viem";
|
|
93
|
+
|
|
94
|
+
// src/chain.ts
|
|
95
|
+
import {
|
|
96
|
+
createPublicClient,
|
|
97
|
+
createWalletClient,
|
|
98
|
+
decodeAbiParameters,
|
|
99
|
+
defineChain,
|
|
100
|
+
encodeAbiParameters,
|
|
101
|
+
fallback,
|
|
102
|
+
http,
|
|
103
|
+
parseAbi,
|
|
104
|
+
parseEventLogs
|
|
105
|
+
} from "viem";
|
|
106
|
+
import { nonceManager, privateKeyToAccount } from "viem/accounts";
|
|
107
|
+
var minterAbi = parseAbi([
|
|
108
|
+
"function capture(address member, uint32[] assets) returns (uint128 k)",
|
|
109
|
+
"function spend(address owner, uint128 maxK) returns (uint64 round, uint128 k)",
|
|
110
|
+
"function settle(address owner, uint64 round, bytes signature, uint256 nonce) returns (uint256 reward)",
|
|
111
|
+
"function setSpender(address newSpender)",
|
|
112
|
+
"function setSpenderBySig(address owner, address newSpender, uint256 nonce, uint256 deadline, bytes signature)",
|
|
113
|
+
"function nonces(address) view returns (uint256)",
|
|
114
|
+
"function spender(address) view returns (address)",
|
|
115
|
+
"function creditDelegate(address) view returns (address)",
|
|
116
|
+
"function credits(address) view returns (uint128)",
|
|
117
|
+
"function draws(address, uint64) view returns (uint128)",
|
|
118
|
+
"function memberSlots(address, uint32) view returns (int64 lastSnapshotSzi, bool known, uint64 lastMarkPx)",
|
|
119
|
+
"function memberAssetsLength(address member) view returns (uint256)",
|
|
120
|
+
"function memberAssets(address member, uint256 index) view returns (uint32)",
|
|
121
|
+
"function token() view returns (address)",
|
|
122
|
+
"function difficulty() view returns (uint128)",
|
|
123
|
+
"function winCount() view returns (uint64)",
|
|
124
|
+
"function lastWonRound() view returns (uint64)",
|
|
125
|
+
"function wonBy(uint64 round) view returns (address owner, uint128 difficulty, uint128 reward)",
|
|
126
|
+
"event AssetRegistered(address indexed member, uint32 indexed asset, int64 baselineSzi)",
|
|
127
|
+
"event Captured(address indexed member, address indexed destination, uint128 cents)",
|
|
128
|
+
"event Spent(address indexed owner, uint64 indexed round, uint128 k, uint128 drawK)",
|
|
129
|
+
"event Won(address indexed owner, uint64 indexed round, uint64 indexed winCount, uint256 nonce, uint256 reward, address caller)"
|
|
130
|
+
]);
|
|
131
|
+
var MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
132
|
+
function clients(net, rpc) {
|
|
133
|
+
const own = rpc ? [rpc] : [];
|
|
134
|
+
const rpcs = [...own, ...net.rpcs];
|
|
135
|
+
const chain = defineChain({
|
|
136
|
+
id: net.chainId,
|
|
137
|
+
name: net.name,
|
|
138
|
+
nativeCurrency: { name: "HYPE", symbol: "HYPE", decimals: 18 },
|
|
139
|
+
rpcUrls: { default: { http: rpcs } },
|
|
140
|
+
contracts: { multicall3: { address: MULTICALL3 } }
|
|
141
|
+
});
|
|
142
|
+
const transport = fallback(rpcs.map((url) => http(url)));
|
|
143
|
+
return {
|
|
144
|
+
chain,
|
|
145
|
+
transport,
|
|
146
|
+
/** Whether the user's own RPC comes first. */
|
|
147
|
+
own: rpc !== void 0,
|
|
148
|
+
/** Host only: the path of a keyed RPC carries its API key. */
|
|
149
|
+
rpcHost: new URL(rpcs[0]).host,
|
|
150
|
+
read: createPublicClient({ chain, transport, batch: { multicall: true } }),
|
|
151
|
+
logs: createPublicClient({ chain, transport: fallback([...own, ...net.logRpcs].map((url) => http(url))) })
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function connect(net, key, rpc) {
|
|
155
|
+
const { chain, transport, own, rpcHost, read: read2, logs } = clients(net, rpc);
|
|
156
|
+
const account = privateKeyToAccount(key, { nonceManager });
|
|
157
|
+
return { net, account, own, rpcHost, read: read2, logs, write: createWalletClient({ chain, transport, account }) };
|
|
158
|
+
}
|
|
159
|
+
async function setSpenderBySig(c, auth) {
|
|
160
|
+
const hash = await c.write.writeContract({
|
|
161
|
+
address: c.net.minter,
|
|
162
|
+
abi: minterAbi,
|
|
163
|
+
functionName: "setSpenderBySig",
|
|
164
|
+
args: [auth.owner, c.account.address, auth.nonce, auth.deadline, auth.signature]
|
|
165
|
+
});
|
|
166
|
+
const receipt = await c.read.waitForTransactionReceipt({ hash });
|
|
167
|
+
if (receipt.status !== "success") throw new Error(`the authorization failed \xB7 tx ${hash}`);
|
|
168
|
+
return hash;
|
|
169
|
+
}
|
|
170
|
+
var POSITION2 = "0x0000000000000000000000000000000000000813";
|
|
171
|
+
async function l1Szi(c, owner, asset) {
|
|
172
|
+
const { data } = await c.read.call({
|
|
173
|
+
to: POSITION2,
|
|
174
|
+
data: encodeAbiParameters([{ type: "address" }, { type: "uint32" }], [owner, asset])
|
|
175
|
+
});
|
|
176
|
+
if (!data) throw new Error("position2 returned nothing");
|
|
177
|
+
return decodeAbiParameters([{ type: "int64" }], data)[0];
|
|
178
|
+
}
|
|
179
|
+
function padGas(estimate) {
|
|
180
|
+
return estimate * 3n / 2n;
|
|
181
|
+
}
|
|
182
|
+
async function transact(c, functionName, args) {
|
|
183
|
+
const call = { address: c.net.minter, abi: minterAbi, functionName, args };
|
|
184
|
+
const estimate = await c.read.estimateContractGas({ ...call, account: c.account, prepare: false });
|
|
185
|
+
const hash = await c.write.writeContract({ ...call, gas: padGas(estimate) });
|
|
186
|
+
const receipt = await c.read.waitForTransactionReceipt({ hash, checkReplacement: false });
|
|
187
|
+
if (receipt.status !== "success") throw new Error(`${functionName} reverted \xB7 tx ${hash}`);
|
|
188
|
+
return { hash, events: parseEventLogs({ abi: minterAbi, logs: receipt.logs }) };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/core.ts
|
|
192
|
+
import { isAddressEqual, zeroAddress } from "viem";
|
|
193
|
+
|
|
194
|
+
// src/coalesce.ts
|
|
195
|
+
function coalesce(run2) {
|
|
196
|
+
let running = false;
|
|
197
|
+
let queued = false;
|
|
198
|
+
const loop = async () => {
|
|
199
|
+
running = true;
|
|
200
|
+
do {
|
|
201
|
+
queued = false;
|
|
202
|
+
await run2();
|
|
203
|
+
} while (queued);
|
|
204
|
+
running = false;
|
|
205
|
+
};
|
|
206
|
+
return () => {
|
|
207
|
+
if (running) queued = true;
|
|
208
|
+
else void loop();
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/hyperliquid.ts
|
|
213
|
+
import { parseUnits } from "viem";
|
|
214
|
+
async function info(url, body) {
|
|
215
|
+
const res = await fetch(url, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "content-type": "application/json" },
|
|
218
|
+
body: JSON.stringify(body)
|
|
219
|
+
});
|
|
220
|
+
if (!res.ok) throw new Error(`Hyperliquid info ${JSON.stringify(body)}: HTTP ${res.status}`);
|
|
221
|
+
return await res.json();
|
|
222
|
+
}
|
|
223
|
+
function perps(metas) {
|
|
224
|
+
return metas.flatMap((meta, dex) => meta.universe.map(({ name, szDecimals }, i) => ({ name, szDecimals, asset: dex * 1e4 + i })));
|
|
225
|
+
}
|
|
226
|
+
async function perpUniverse(url) {
|
|
227
|
+
return perps(await info(url, { type: "allPerpMetas" }));
|
|
228
|
+
}
|
|
229
|
+
function heldIn(data) {
|
|
230
|
+
return data.clearinghouseStates.flatMap(
|
|
231
|
+
([, state]) => state.assetPositions.filter((p) => Number(p.position.szi) !== 0).map((p) => p.position.coin)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function heldCoins(wsUrl, user) {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const ws = new WebSocket(wsUrl);
|
|
237
|
+
const fail = (why) => {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
ws.close();
|
|
240
|
+
reject(new Error(`Hyperliquid positions: ${why}`));
|
|
241
|
+
};
|
|
242
|
+
const timer = setTimeout(() => fail("no answer in 10s"), 1e4);
|
|
243
|
+
ws.addEventListener(
|
|
244
|
+
"open",
|
|
245
|
+
() => ws.send(JSON.stringify({ method: "subscribe", subscription: { type: "allDexsClearinghouseState", user } }))
|
|
246
|
+
);
|
|
247
|
+
ws.addEventListener("message", (e) => {
|
|
248
|
+
const msg = JSON.parse(String(e.data));
|
|
249
|
+
if (msg.channel !== "allDexsClearinghouseState") return;
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
ws.close();
|
|
252
|
+
resolve(heldIn(msg.data));
|
|
253
|
+
});
|
|
254
|
+
ws.addEventListener("error", () => fail("websocket error"));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async function subAccountNames(url, user) {
|
|
258
|
+
const subs = await info(url, { type: "subAccounts", user });
|
|
259
|
+
return (subs ?? []).map((s) => s.name);
|
|
260
|
+
}
|
|
261
|
+
function fillsAfter(url, user, time) {
|
|
262
|
+
return info(url, { type: "userFillsByTime", user, startTime: time + 1 });
|
|
263
|
+
}
|
|
264
|
+
function sziAfter(f, szDecimals) {
|
|
265
|
+
const sz = parseUnits(f.sz, szDecimals);
|
|
266
|
+
return parseUnits(f.startPosition, szDecimals) + (f.side === "B" ? sz : -sz);
|
|
267
|
+
}
|
|
268
|
+
function watchFills(url, user, onFills, signal) {
|
|
269
|
+
if (signal.aborted) return;
|
|
270
|
+
const ws = new WebSocket(url);
|
|
271
|
+
const stop = () => ws.close();
|
|
272
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
273
|
+
let ping;
|
|
274
|
+
ws.addEventListener("open", () => {
|
|
275
|
+
ws.send(JSON.stringify({ method: "subscribe", subscription: { type: "userFills", user } }));
|
|
276
|
+
ping = setInterval(() => ws.send(JSON.stringify({ method: "ping" })), 5e4);
|
|
277
|
+
});
|
|
278
|
+
ws.addEventListener("message", (e) => {
|
|
279
|
+
const msg = JSON.parse(String(e.data));
|
|
280
|
+
if (msg.channel === "userFills" && !msg.data.isSnapshot) onFills(msg.data.fills);
|
|
281
|
+
});
|
|
282
|
+
ws.addEventListener("close", () => {
|
|
283
|
+
clearInterval(ping);
|
|
284
|
+
signal.removeEventListener("abort", stop);
|
|
285
|
+
setTimeout(() => watchFills(url, user, onFills, signal), 2e3);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/protocol.ts
|
|
290
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
291
|
+
import { bytesToHex, hexToBytes } from "viem";
|
|
292
|
+
var DRAND_GENESIS = 1727521075;
|
|
293
|
+
var DRAND_PERIOD = 3;
|
|
294
|
+
var DRAND_URL = "https://api.drand.sh/v2/beacons/evmnet/rounds/";
|
|
295
|
+
function publishTime(round) {
|
|
296
|
+
return DRAND_GENESIS + (round - 1) * DRAND_PERIOD;
|
|
297
|
+
}
|
|
298
|
+
var ROUND_LEAD = 2;
|
|
299
|
+
function openDrawsSince(lastWon) {
|
|
300
|
+
return publishTime(Number(lastWon) + 1 - ROUND_LEAD);
|
|
301
|
+
}
|
|
302
|
+
function ticketSize(difficulty2, bank) {
|
|
303
|
+
const ceilDiv = (a, b) => (a + b - 1n) / b;
|
|
304
|
+
const cap = ceilDiv(difficulty2, 10n);
|
|
305
|
+
const floor = ceilDiv(bank, 20n);
|
|
306
|
+
const size = cap > floor ? cap : floor;
|
|
307
|
+
return size < bank ? size : bank;
|
|
308
|
+
}
|
|
309
|
+
function parseDrand(body, round) {
|
|
310
|
+
const { round: got, signature } = body;
|
|
311
|
+
if (got !== round) throw new Error(`drand returned round ${String(got)}, wanted ${round}`);
|
|
312
|
+
if (typeof signature !== "string" || !/^[0-9a-f]{128}$/i.test(signature)) {
|
|
313
|
+
throw new Error("drand signature is not 64 bytes of hex");
|
|
314
|
+
}
|
|
315
|
+
return `0x${signature.toLowerCase()}`;
|
|
316
|
+
}
|
|
317
|
+
async function fetchDrand(round) {
|
|
318
|
+
const res = await fetch(DRAND_URL + round);
|
|
319
|
+
if (!res.ok) throw new Error(`drand round ${round}: HTTP ${res.status}`);
|
|
320
|
+
return parseDrand(await res.json(), round);
|
|
321
|
+
}
|
|
322
|
+
function seedOf(signature) {
|
|
323
|
+
return keccak_256(hexToBytes(signature));
|
|
324
|
+
}
|
|
325
|
+
function targetOf(difficulty2) {
|
|
326
|
+
return hexToBytes(`0x${((2n ** 256n - 1n) / difficulty2).toString(16).padStart(64, "0")}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/network.ts
|
|
330
|
+
import { parseEther } from "viem";
|
|
331
|
+
|
|
332
|
+
// src/text.ts
|
|
333
|
+
import { formatEther } from "viem";
|
|
334
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
335
|
+
function short(hex) {
|
|
336
|
+
return `${hex.slice(0, 6)}\u2026${hex.slice(-4)}`;
|
|
337
|
+
}
|
|
338
|
+
function count(n) {
|
|
339
|
+
return n.toLocaleString("en-US");
|
|
340
|
+
}
|
|
341
|
+
function amount(wei) {
|
|
342
|
+
return Number(formatEther(wei)).toLocaleString("en-US", {
|
|
343
|
+
minimumFractionDigits: 2,
|
|
344
|
+
maximumFractionDigits: 2
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function hype(wei) {
|
|
348
|
+
return Number(formatEther(wei)).toLocaleString("en-US", { maximumFractionDigits: 4 });
|
|
349
|
+
}
|
|
350
|
+
function describe(err) {
|
|
351
|
+
if (process.env.DEBUG === "1") console.error(err);
|
|
352
|
+
const e = err;
|
|
353
|
+
const msg = (e.shortMessage ?? e.message ?? String(err)).replace(/\s+/g, " ").trim();
|
|
354
|
+
return e.cause?.code ? `${msg} (${e.cause.code})` : msg;
|
|
355
|
+
}
|
|
356
|
+
async function retrying(line2, fn, delayMs) {
|
|
357
|
+
for (; ; ) {
|
|
358
|
+
try {
|
|
359
|
+
return await fn();
|
|
360
|
+
} catch (err) {
|
|
361
|
+
line2("error", `${describe(err)} \xB7 retrying in ${delayMs / 1e3}s`, err);
|
|
362
|
+
await sleep(delayMs);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/network.ts
|
|
368
|
+
function covers(net, gas) {
|
|
369
|
+
return `it pays the fees for about ${count(gas / net.ticketFee)} tickets`;
|
|
370
|
+
}
|
|
371
|
+
var MAINNET = {
|
|
372
|
+
name: "HyperEVM mainnet",
|
|
373
|
+
chainId: 999,
|
|
374
|
+
minter: "0x14ae637542b6a125217ee477b756504ff009d506",
|
|
375
|
+
deployBlock: 47234006n,
|
|
376
|
+
rpcs: [
|
|
377
|
+
"https://rpc.hyperliquid.xyz/evm",
|
|
378
|
+
"https://hyperliquid-json-rpc.stakely.io",
|
|
379
|
+
"https://rpc.hypurrscan.io",
|
|
380
|
+
"https://hyperliquid.drpc.org"
|
|
381
|
+
],
|
|
382
|
+
logRpcs: ["https://rpc.hyperliquid.xyz/evm", "https://hyperliquid-json-rpc.stakely.io", "https://rpc.hypurrscan.io"],
|
|
383
|
+
walletRpc: "https://rpc.hyperliquid.xyz/evm",
|
|
384
|
+
hyperliquidInfo: "https://api.hyperliquid.xyz/info",
|
|
385
|
+
hyperliquidWs: "wss://api.hyperliquid.xyz/ws",
|
|
386
|
+
gasFund: parseEther("0.01"),
|
|
387
|
+
minGas: parseEther("0.0005"),
|
|
388
|
+
// Measured in the mainnet rehearsal at 0.1 gwei: spend ~77k gas plus cash ~264k is ~3.4e-5 HYPE. Rounded up.
|
|
389
|
+
ticketFee: parseEther("0.00005"),
|
|
390
|
+
starterCoins: ["BTC", "ETH", "SOL", "HYPE"]
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/core.ts
|
|
394
|
+
var RETRY_MS = 5e3;
|
|
395
|
+
var BACKSTOP_MS = 6e4;
|
|
396
|
+
var L1READ_POLL_MS = 1e3;
|
|
397
|
+
var L1READ_CAP_MS = 15e3;
|
|
398
|
+
var MAX_U128 = 2n ** 128n - 1n;
|
|
399
|
+
var LOG_CHUNK = 1000n;
|
|
400
|
+
var GAS_POLL_MS = 5e3;
|
|
401
|
+
async function status(c, owner) {
|
|
402
|
+
const [spender, balance] = await Promise.all([
|
|
403
|
+
c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "spender", args: [owner] }),
|
|
404
|
+
c.read.getBalance({ address: c.account.address })
|
|
405
|
+
]);
|
|
406
|
+
return { spender: spender === c.account.address, gas: balance >= c.net.minGas, balance };
|
|
407
|
+
}
|
|
408
|
+
var ready = (s) => s.spender && s.gas;
|
|
409
|
+
async function untilFunded(balance, min, pollMs, signal, report) {
|
|
410
|
+
let b = await balance();
|
|
411
|
+
if (b >= min) return true;
|
|
412
|
+
report.low(b);
|
|
413
|
+
while (b < min) {
|
|
414
|
+
if (signal.aborted) return false;
|
|
415
|
+
await sleep(pollMs);
|
|
416
|
+
b = await balance();
|
|
417
|
+
}
|
|
418
|
+
report.funded(b);
|
|
419
|
+
return !signal.aborted;
|
|
420
|
+
}
|
|
421
|
+
var perpNamed = (universe, coin) => universe.find((p) => p.name === coin);
|
|
422
|
+
var nameOf = (universe, asset) => universe.find((p) => p.asset === asset)?.name ?? `asset ${asset}`;
|
|
423
|
+
async function run(c, owner, p, signal) {
|
|
424
|
+
const { line: line2 } = p;
|
|
425
|
+
const key = c.account.address;
|
|
426
|
+
const funded = () => untilFunded(() => c.read.getBalance({ address: key }), c.net.minGas, GAS_POLL_MS, signal, {
|
|
427
|
+
low: (b) => {
|
|
428
|
+
line2("gas low", `the miner key holds ${hype(b)} HYPE, too little for fees \xB7 mining paused`);
|
|
429
|
+
line2("", `send ${hype(c.net.gasFund)} HYPE to ${key} on ${c.net.name} \xB7 ${covers(c.net, c.net.gasFund)} \xB7 mining resumes once it lands`);
|
|
430
|
+
p.gas(true);
|
|
431
|
+
},
|
|
432
|
+
funded: (b) => {
|
|
433
|
+
line2("funded", `the miner key holds ${hype(b)} HYPE for fees \xB7 mining again`);
|
|
434
|
+
p.gas(false);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
if (!await retrying(line2, funded, RETRY_MS)) return;
|
|
438
|
+
const pool = await retrying(line2, () => poolOf(c, owner), RETRY_MS);
|
|
439
|
+
if (pool !== zeroAddress) {
|
|
440
|
+
line2("pooled", `${short(owner)} has joined the pool ${short(pool)} \xB7 its trades earn for the pool, and only the pool can capture them`);
|
|
441
|
+
line2("", "this miner mines solo \xB7 to mine here, leave the pool by calling setCreditDelegate(0x0) on the minter, then start again");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const subs = await retrying(line2, () => subAccountNames(c.net.hyperliquidInfo, owner), RETRY_MS);
|
|
445
|
+
if (subs.length > 0) {
|
|
446
|
+
line2("subaccounts", `your subaccounts ${subs.join(", ")} don't mine \xB7 only trades on this main account mine, not on subaccounts or vaults`);
|
|
447
|
+
}
|
|
448
|
+
const m = {
|
|
449
|
+
c,
|
|
450
|
+
owner,
|
|
451
|
+
universe: await retrying(line2, () => perpUniverse(c.net.hyperliquidInfo), RETRY_MS),
|
|
452
|
+
p,
|
|
453
|
+
signal,
|
|
454
|
+
funded
|
|
455
|
+
};
|
|
456
|
+
await retrying(line2, () => register(m), RETRY_MS);
|
|
457
|
+
await retrying(line2, () => resume(m), RETRY_MS);
|
|
458
|
+
if (signal.aborted) return;
|
|
459
|
+
const fills = /* @__PURE__ */ new Map();
|
|
460
|
+
const state = { idle: false, fills };
|
|
461
|
+
let latest = Promise.resolve();
|
|
462
|
+
const pass = coalesce(
|
|
463
|
+
() => latest = (async () => {
|
|
464
|
+
if (signal.aborted) return;
|
|
465
|
+
const targets = new Map(fills);
|
|
466
|
+
fills.clear();
|
|
467
|
+
try {
|
|
468
|
+
const held = heldCoins(c.net.hyperliquidWs, owner);
|
|
469
|
+
held.catch(() => {
|
|
470
|
+
});
|
|
471
|
+
if (!await reflected(m, targets)) setTimeout(pass, BACKSTOP_MS);
|
|
472
|
+
await tick(m, state, await held);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
line2("error", `${describe(err)} \xB7 retrying in ${RETRY_MS / 1e3}s`, err);
|
|
475
|
+
setTimeout(pass, RETRY_MS);
|
|
476
|
+
}
|
|
477
|
+
p.refresh();
|
|
478
|
+
})()
|
|
479
|
+
);
|
|
480
|
+
let seen = Date.now();
|
|
481
|
+
const onFills = (batch) => {
|
|
482
|
+
for (const f of batch) {
|
|
483
|
+
seen = Math.max(seen, f.time);
|
|
484
|
+
const perp = perpNamed(m.universe, f.coin);
|
|
485
|
+
if (!perp) continue;
|
|
486
|
+
fills.set(perp, sziAfter(f, perp.szDecimals));
|
|
487
|
+
line2("trade", `${f.side === "B" ? "bought" : "sold"} ${f.sz} ${f.coin}`);
|
|
488
|
+
}
|
|
489
|
+
pass();
|
|
490
|
+
};
|
|
491
|
+
watchFills(c.net.hyperliquidWs, owner, onFills, signal);
|
|
492
|
+
const backstop = setInterval(async () => {
|
|
493
|
+
try {
|
|
494
|
+
const missed = (await fillsAfter(c.net.hyperliquidInfo, owner, seen)).filter((f) => f.time > seen);
|
|
495
|
+
if (missed.length > 0 && !signal.aborted) onFills(missed);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
line2("error", `${describe(err)} \xB7 asking again in ${BACKSTOP_MS / 1e3}s`, err);
|
|
498
|
+
}
|
|
499
|
+
}, BACKSTOP_MS);
|
|
500
|
+
pass();
|
|
501
|
+
await new Promise((resolve) => signal.addEventListener("abort", resolve, { once: true }));
|
|
502
|
+
clearInterval(backstop);
|
|
503
|
+
for (let last; last !== latest; ) await (last = latest);
|
|
504
|
+
}
|
|
505
|
+
async function reflected({ c, owner, p }, targets) {
|
|
506
|
+
const deadline = Date.now() + L1READ_CAP_MS;
|
|
507
|
+
for (const [perp, szi] of targets) {
|
|
508
|
+
while (await l1Szi(c, owner, perp.asset) !== szi) {
|
|
509
|
+
if (Date.now() > deadline) {
|
|
510
|
+
p.line("lagging", `HyperEVM doesn't show your ${perp.name} fill after ${L1READ_CAP_MS / 1e3}s \xB7 capturing anyway, and again in ${BACKSTOP_MS / 1e3}s`);
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
await sleep(L1READ_POLL_MS);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
async function register({ c, owner, universe, p }) {
|
|
519
|
+
const held = await heldCoins(c.net.hyperliquidWs, owner);
|
|
520
|
+
const coins = [.../* @__PURE__ */ new Set([...held, ...c.net.starterCoins])];
|
|
521
|
+
const known = coins.map((coin) => perpNamed(universe, coin)).filter((perp) => perp !== void 0);
|
|
522
|
+
const slots = await Promise.all(
|
|
523
|
+
known.map(
|
|
524
|
+
(perp) => c.read.readContract({
|
|
525
|
+
address: c.net.minter,
|
|
526
|
+
abi: minterAbi,
|
|
527
|
+
functionName: "memberSlots",
|
|
528
|
+
args: [owner, perp.asset]
|
|
529
|
+
})
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
const fresh = known.filter((_, i) => !slots[i][1]);
|
|
533
|
+
if (fresh.length === 0) return;
|
|
534
|
+
await transact(c, "capture", [owner, fresh.map((perp) => perp.asset)]);
|
|
535
|
+
p.line("registered", `registered ${fresh.map((perp) => perp.name).join(" ")} \xB7 your current positions are the starting point`);
|
|
536
|
+
p.line("", "only trading from now on earns attempts");
|
|
537
|
+
}
|
|
538
|
+
async function resume(m) {
|
|
539
|
+
for (const [round, k] of await openDraws(m.c, m.owner, m.p.line, RETRY_MS)) {
|
|
540
|
+
m.p.line("resuming", `resuming a ticket of ${count(k)} attempts on round ${round}`);
|
|
541
|
+
await draw(m, round, k);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function openDraws(c, owner, line2, retryMs) {
|
|
545
|
+
const [head, lastWon] = await Promise.all([c.logs.getBlockNumber(), lastWonRound(c)]);
|
|
546
|
+
const timestamp = async (n) => (await c.read.getBlock({ blockNumber: n })).timestamp;
|
|
547
|
+
const start = await firstBlockAt(timestamp, c.net.deployBlock, head, BigInt(openDrawsSince(lastWon)));
|
|
548
|
+
const rounds = /* @__PURE__ */ new Set();
|
|
549
|
+
for (let from = start; from <= head; from += LOG_CHUNK) {
|
|
550
|
+
const to = from + LOG_CHUNK - 1n < head ? from + LOG_CHUNK - 1n : head;
|
|
551
|
+
const logs = await retrying(
|
|
552
|
+
line2,
|
|
553
|
+
() => c.logs.getContractEvents({ address: c.net.minter, abi: minterAbi, eventName: "Spent", args: { owner }, fromBlock: from, toBlock: to }),
|
|
554
|
+
retryMs
|
|
555
|
+
);
|
|
556
|
+
for (const log of logs) if (log.args.round > lastWon) rounds.add(log.args.round);
|
|
557
|
+
}
|
|
558
|
+
const open = /* @__PURE__ */ new Map();
|
|
559
|
+
for (const round of rounds) {
|
|
560
|
+
const k = await c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "draws", args: [owner, round] });
|
|
561
|
+
if (k > 0n) open.set(round, k);
|
|
562
|
+
}
|
|
563
|
+
return open;
|
|
564
|
+
}
|
|
565
|
+
async function firstBlockAt(timestamp, from, head, since) {
|
|
566
|
+
let [lo, hi] = [from, head];
|
|
567
|
+
while (lo < hi) {
|
|
568
|
+
const mid = (lo + hi) / 2n;
|
|
569
|
+
if (await timestamp(mid) >= since) hi = mid;
|
|
570
|
+
else lo = mid + 1n;
|
|
571
|
+
}
|
|
572
|
+
return lo;
|
|
573
|
+
}
|
|
574
|
+
async function tick(m, state, held) {
|
|
575
|
+
const { c, owner, universe, p, signal } = m;
|
|
576
|
+
if (!await m.funded()) return;
|
|
577
|
+
const tracked = await trackedAssets(c, owner);
|
|
578
|
+
const opens = held.map((coin) => perpNamed(universe, coin)?.asset).filter((a) => a !== void 0 && !tracked.includes(a));
|
|
579
|
+
if (opens.length > 0) {
|
|
580
|
+
const { events } = await transact(c, "capture", [owner, opens]);
|
|
581
|
+
for (const e of events) {
|
|
582
|
+
if (e.eventName === "AssetRegistered") p.line("registered", `registered ${nameOf(universe, e.args.asset)} \xB7 your current position is the starting point`);
|
|
583
|
+
if (e.eventName === "Captured") p.line("captured", `+${count(e.args.cents)} attempts`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
while (!signal.aborted) {
|
|
587
|
+
const { result } = await c.read.simulateContract({
|
|
588
|
+
address: c.net.minter,
|
|
589
|
+
abi: minterAbi,
|
|
590
|
+
functionName: "spend",
|
|
591
|
+
args: [owner, MAX_U128],
|
|
592
|
+
account: c.account
|
|
593
|
+
});
|
|
594
|
+
const bank = result[1];
|
|
595
|
+
if (bank === 0n) break;
|
|
596
|
+
state.idle = false;
|
|
597
|
+
for (let left = bank; left > 0n; ) {
|
|
598
|
+
if (signal.aborted || state.fills.size > 0 || !await m.funded()) return;
|
|
599
|
+
const { events } = await transact(c, "spend", [owner, ticketSize(await difficulty(c), bank)]);
|
|
600
|
+
for (const e of events) if (e.eventName === "Captured") p.line("captured", `+${count(e.args.cents)} attempts`);
|
|
601
|
+
const spent = events.find((e) => e.eventName === "Spent");
|
|
602
|
+
if (!spent) {
|
|
603
|
+
p.line("missed", "that trade closed before HyperEVM captured it \xB7 nothing to play");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
left -= spent.args.k;
|
|
607
|
+
const drawsIn = Math.max(0, Math.ceil(publishTime(Number(spent.args.round)) - Date.now() / 1e3));
|
|
608
|
+
p.line("ticket", `${count(spent.args.k)} attempts \u2192 round ${spent.args.round} \xB7 draws in ${drawsIn}s`);
|
|
609
|
+
p.refresh();
|
|
610
|
+
await draw(m, spent.args.round, spent.args.drawK);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (signal.aborted) return;
|
|
614
|
+
if (!state.idle) p.line("waiting", "waiting for your next trade on Hyperliquid");
|
|
615
|
+
state.idle = true;
|
|
616
|
+
}
|
|
617
|
+
async function draw(m, round, k) {
|
|
618
|
+
const wait = publishTime(Number(round)) * 1e3 - Date.now();
|
|
619
|
+
if (wait > 0) await sleep(wait + 500);
|
|
620
|
+
const signature = await retrying(m.p.line, () => fetchDrand(Number(round)), 2e3);
|
|
621
|
+
await retrying(m.p.line, () => cash(m, round, k, signature), RETRY_MS);
|
|
622
|
+
}
|
|
623
|
+
async function cash({ c, owner, p }, round, k, signature) {
|
|
624
|
+
const d = await difficulty(c);
|
|
625
|
+
p.line("drawn", `round ${round} drawn \xB7 searching ${count(k)} attempts at difficulty ${count(d)}`);
|
|
626
|
+
const nonce = await p.search(seedOf(signature), owner, Number(k), targetOf(d));
|
|
627
|
+
if (nonce === 0) {
|
|
628
|
+
p.line("searching", `searched ${count(k)}/${count(k)} \xB7 no win`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
p.line("searching", `attempt ${count(nonce)}/${count(k)} wins \xB7 cashing it`);
|
|
632
|
+
if (await lastWonRound(c) >= round) {
|
|
633
|
+
const [winner, , reward] = await c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "wonBy", args: [round] });
|
|
634
|
+
if (!isAddressEqual(winner, owner)) return p.line("lost", "someone else found this block first");
|
|
635
|
+
p.won();
|
|
636
|
+
return p.line("BLOCK", `found on round ${round} \xB7 +${amount(reward)} HYPOW \u2192 ${short(owner)}`);
|
|
637
|
+
}
|
|
638
|
+
const { hash, events } = await transact(c, "settle", [owner, round, signature, BigInt(nonce)]);
|
|
639
|
+
const won = events.find((e) => e.eventName === "Won");
|
|
640
|
+
if (!won) throw new Error("settle emitted no win");
|
|
641
|
+
p.won();
|
|
642
|
+
p.line(`BLOCK ${won.args.winCount + 1n}`, `found on round ${round} \xB7 +${amount(won.args.reward)} HYPOW \u2192 ${short(owner)} \xB7 tx ${short(hash)}`);
|
|
643
|
+
}
|
|
644
|
+
function poolOf(c, owner) {
|
|
645
|
+
return c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "creditDelegate", args: [owner] });
|
|
646
|
+
}
|
|
647
|
+
async function difficulty(c) {
|
|
648
|
+
return c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "difficulty" });
|
|
649
|
+
}
|
|
650
|
+
async function lastWonRound(c) {
|
|
651
|
+
return c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "lastWonRound" });
|
|
652
|
+
}
|
|
653
|
+
async function trackedAssets(c, owner) {
|
|
654
|
+
const n = await c.read.readContract({
|
|
655
|
+
address: c.net.minter,
|
|
656
|
+
abi: minterAbi,
|
|
657
|
+
functionName: "memberAssetsLength",
|
|
658
|
+
args: [owner]
|
|
659
|
+
});
|
|
660
|
+
return Promise.all(
|
|
661
|
+
Array.from(
|
|
662
|
+
{ length: Number(n) },
|
|
663
|
+
(_, i) => c.read.readContract({
|
|
664
|
+
address: c.net.minter,
|
|
665
|
+
abi: minterAbi,
|
|
666
|
+
functionName: "memberAssets",
|
|
667
|
+
args: [owner, BigInt(i)]
|
|
668
|
+
})
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/authorize.ts
|
|
674
|
+
var PORT = 4747;
|
|
675
|
+
async function waitForChain(c, owner) {
|
|
676
|
+
for (; ; ) {
|
|
677
|
+
const s = await retrying(line, () => status(c, owner), 3e3);
|
|
678
|
+
if (ready(s)) return s;
|
|
679
|
+
await sleep2(3e3);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function authorize(c, opts) {
|
|
683
|
+
if (opts.owner) {
|
|
684
|
+
const given = opts.owner;
|
|
685
|
+
const s = await retrying(line, () => status(c, given), 3e3);
|
|
686
|
+
if (ready(s)) return { owner: opts.owner, status: s };
|
|
687
|
+
if (s.spender) line("gas low", `miner key has ${hype(s.balance)} HYPE, needs ${hype(c.net.minGas)} \xB7 top it up to keep mining`);
|
|
688
|
+
if (opts.manual) {
|
|
689
|
+
printManual(c, opts.owner, s, opts.gas);
|
|
690
|
+
return { owner: opts.owner, status: await waitForChain(c, opts.owner) };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return viaPage(c, opts.gas, opts.browser);
|
|
694
|
+
}
|
|
695
|
+
function printManual(c, owner, s, gas) {
|
|
696
|
+
const key = c.account.address;
|
|
697
|
+
line("authorize", `authorize this key from ${owner} on ${c.net.name} (chain ${c.net.chainId}):`);
|
|
698
|
+
if (!s.spender) {
|
|
699
|
+
line("", `\xB7 call setSpender(${key}) on the minter ${c.net.minter}`);
|
|
700
|
+
line("", ` (calldata ${setSpenderData(key)})`);
|
|
701
|
+
}
|
|
702
|
+
if (!s.gas) line("", `\xB7 send ${hype(gas)} HYPE to ${key} \xB7 ${covers(c.net, gas)}`);
|
|
703
|
+
line("waiting", "waiting for both to land on chain\u2026");
|
|
704
|
+
}
|
|
705
|
+
function setSpenderData(key) {
|
|
706
|
+
return encodeFunctionData({ abi: minterAbi, functionName: "setSpender", args: [key] });
|
|
707
|
+
}
|
|
708
|
+
async function readJson(req) {
|
|
709
|
+
let body = "";
|
|
710
|
+
for await (const chunk of req) body += chunk;
|
|
711
|
+
return JSON.parse(body);
|
|
712
|
+
}
|
|
713
|
+
function page(file) {
|
|
714
|
+
return readFileSync2(new URL(`../dist/${file}`, import.meta.url), "utf8");
|
|
715
|
+
}
|
|
716
|
+
async function viaPage(c, gas, browser) {
|
|
717
|
+
const token = randomBytes(16).toString("hex");
|
|
718
|
+
const config = { token, key: c.account.address, gas: gas.toString() };
|
|
719
|
+
const html = page("cli.html").replace("/*CONFIG*/", JSON.stringify(config));
|
|
720
|
+
const script = page("hypow-miner.js");
|
|
721
|
+
let owner;
|
|
722
|
+
let latest;
|
|
723
|
+
const server = createServer(async (req, res) => {
|
|
724
|
+
const json = (code, body) => {
|
|
725
|
+
res.writeHead(code, { "content-type": "application/json" });
|
|
726
|
+
res.end(JSON.stringify(body, (_, v) => typeof v === "bigint" ? v.toString() : v));
|
|
727
|
+
};
|
|
728
|
+
try {
|
|
729
|
+
if (req.method === "GET" && req.url === "/") {
|
|
730
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
731
|
+
res.end(html);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (req.method === "GET" && req.url === "/hypow-miner.js") {
|
|
735
|
+
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
|
|
736
|
+
res.end(script);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (req.headers["x-hypow-token"] !== token) return json(403, { error: "bad token" });
|
|
740
|
+
if (req.method === "POST" && req.url === "/owner") {
|
|
741
|
+
const { owner: posted } = await readJson(req);
|
|
742
|
+
if (!posted || !isAddress2(posted)) return json(400, { error: "not an address" });
|
|
743
|
+
owner = getAddress2(posted);
|
|
744
|
+
latest = await status(c, owner);
|
|
745
|
+
line("owner", `${short(owner)} connected \xB7 waiting for the chain\u2026`);
|
|
746
|
+
return json(200, latest);
|
|
747
|
+
}
|
|
748
|
+
if (req.method === "POST" && req.url === "/spender") {
|
|
749
|
+
const auth = await readJson(req);
|
|
750
|
+
if (!isAddress2(auth.owner) || !isHex2(auth.signature)) return json(400, { error: "not a SetSpender signature" });
|
|
751
|
+
const hash = await setSpenderBySig(c, {
|
|
752
|
+
owner: getAddress2(auth.owner),
|
|
753
|
+
nonce: BigInt(auth.nonce),
|
|
754
|
+
deadline: BigInt(auth.deadline),
|
|
755
|
+
signature: auth.signature
|
|
756
|
+
});
|
|
757
|
+
line("authorized", `${short(auth.owner)} signed \xB7 the key submitted it \xB7 tx ${short(hash)}`);
|
|
758
|
+
return json(200, { hash });
|
|
759
|
+
}
|
|
760
|
+
if (req.method === "GET" && req.url === "/status") return json(200, latest ?? null);
|
|
761
|
+
json(404, { error: "not found" });
|
|
762
|
+
} catch (err) {
|
|
763
|
+
json(500, { error: describe(err) });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
await new Promise((resolve, reject) => {
|
|
767
|
+
server.once(
|
|
768
|
+
"error",
|
|
769
|
+
(err) => reject(
|
|
770
|
+
// The usual holder is another miner waiting at this same step. Two
|
|
771
|
+
// miners for one owner would take the authorization from each other.
|
|
772
|
+
err.code === "EADDRINUSE" ? new Error(`port ${PORT} is taken, most likely by another hypow miner on this machine \xB7 stop it, then run this again`) : err
|
|
773
|
+
)
|
|
774
|
+
);
|
|
775
|
+
server.listen(PORT, "127.0.0.1", resolve);
|
|
776
|
+
});
|
|
777
|
+
const url = `http://localhost:${PORT}`;
|
|
778
|
+
line("authorize", browser ? `${url} \u2014 opening browser\u2026` : `open ${url} in the browser that has your wallet`);
|
|
779
|
+
if (browser) openBrowser(url);
|
|
780
|
+
let done;
|
|
781
|
+
while (!done) {
|
|
782
|
+
await sleep2(2e3);
|
|
783
|
+
const polled = owner;
|
|
784
|
+
if (!polled) continue;
|
|
785
|
+
latest = await retrying(line, () => status(c, polled), 3e3);
|
|
786
|
+
if (ready(latest)) done = { owner: polled, status: latest };
|
|
787
|
+
}
|
|
788
|
+
await sleep2(4e3);
|
|
789
|
+
server.close();
|
|
790
|
+
return done;
|
|
791
|
+
}
|
|
792
|
+
function openBrowser(url) {
|
|
793
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
794
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).on("error", () => {
|
|
795
|
+
}).unref();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/endpoint.ts
|
|
799
|
+
import { createPublicClient as createPublicClient2, http as http2 } from "viem";
|
|
800
|
+
var wantsPublic = (answer) => answer === "" || answer.toLowerCase() === "public";
|
|
801
|
+
async function rpcProblem(net, url) {
|
|
802
|
+
if (!URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol)) return "not an http(s) URL";
|
|
803
|
+
try {
|
|
804
|
+
const id = await createPublicClient2({ transport: http2(url, { timeout: 1e4, retryCount: 0 }) }).getChainId();
|
|
805
|
+
return id === net.chainId ? void 0 : `it serves chain ${id}, not ${net.name} (chain ${net.chainId})`;
|
|
806
|
+
} catch {
|
|
807
|
+
return "can't reach it";
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
var RATE_LIMITED = /rate.?limit|exceeds defined limit|too many requests/i;
|
|
811
|
+
function rateLimited(err) {
|
|
812
|
+
for (let e = err; e; e = e.cause) {
|
|
813
|
+
if ([e.code, e.status, e.data?.code].some((c) => c === -32005 || c === 429)) return true;
|
|
814
|
+
if (RATE_LIMITED.test(`${e.message ?? ""} ${e.details ?? ""} ${e.data?.message ?? ""}`)) return true;
|
|
815
|
+
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
function rpcRow(c, fix) {
|
|
819
|
+
return ["rpc", c.own ? `your own \xB7 ${c.rpcHost}` : `\u26A0 using the rate-limited public RPC \xB7 trading often may miss blocks \xB7 to set your own RPC: ${fix}`];
|
|
820
|
+
}
|
|
821
|
+
var RATE_LIMIT_NOTE_MS = 18e4;
|
|
822
|
+
function explainRateLimits(line2, own, fix, now = Date.now) {
|
|
823
|
+
let last = -Infinity;
|
|
824
|
+
return (label, message, cause) => {
|
|
825
|
+
line2(label, message, cause);
|
|
826
|
+
if (!rateLimited(cause) || now() - last < RATE_LIMIT_NOTE_MS) return;
|
|
827
|
+
last = now();
|
|
828
|
+
if (own) line2("rpc", "your own RPC rate-limited you \xB7 the miner retries until it answers again");
|
|
829
|
+
else line2("\u26A0 rpc", `the public RPC rate-limited you \xB7 mining paused about a minute \xB7 set your own with ${fix}`);
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/rpc.ts
|
|
834
|
+
import { stdin, stdout } from "node:process";
|
|
835
|
+
import { createInterface } from "node:readline/promises";
|
|
836
|
+
async function chooseRpc(net, flag) {
|
|
837
|
+
if (flag !== void 0) {
|
|
838
|
+
const p = await rpcProblem(net, flag);
|
|
839
|
+
if (p) throw new Error(`--rpc: ${p}`);
|
|
840
|
+
saveRpc(flag);
|
|
841
|
+
return flag;
|
|
842
|
+
}
|
|
843
|
+
const saved = loadRpc();
|
|
844
|
+
if (saved !== void 0) return saved ?? void 0;
|
|
845
|
+
if (!stdin.isTTY) return void 0;
|
|
846
|
+
line("rpc", "paste your HyperEVM RPC URL \xB7 a free one from Alchemy, QuickNode, \u2026 \xB7 asked once, change it later with --rpc");
|
|
847
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
848
|
+
try {
|
|
849
|
+
for (; ; ) {
|
|
850
|
+
const answer = (await rl.question("HyperEVM RPC URL (Enter for the rate-limited public one): ")).trim();
|
|
851
|
+
const url = wantsPublic(answer) ? void 0 : answer;
|
|
852
|
+
const p = url && await rpcProblem(net, url);
|
|
853
|
+
if (p) {
|
|
854
|
+
line("rpc", `${p} \xB7 try again`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
saveRpc(url ?? null);
|
|
858
|
+
return url;
|
|
859
|
+
}
|
|
860
|
+
} finally {
|
|
861
|
+
rl.close();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/status.ts
|
|
866
|
+
import { constants } from "node:os";
|
|
867
|
+
|
|
868
|
+
// src/standing.ts
|
|
869
|
+
import { erc20Abi } from "viem";
|
|
870
|
+
async function statusRows(c, token, owner, found) {
|
|
871
|
+
const minter = { address: c.net.minter, abi: minterAbi };
|
|
872
|
+
const [hypow, saved, gas, difficulty2, height] = await Promise.all([
|
|
873
|
+
c.read.readContract({ address: token, abi: erc20Abi, functionName: "balanceOf", args: [owner] }),
|
|
874
|
+
c.read.readContract({ ...minter, functionName: "credits", args: [owner] }),
|
|
875
|
+
c.read.getBalance({ address: c.account.address }),
|
|
876
|
+
c.read.readContract({ ...minter, functionName: "difficulty" }),
|
|
877
|
+
c.read.readContract({ ...minter, functionName: "winCount" })
|
|
878
|
+
]);
|
|
879
|
+
return [
|
|
880
|
+
["wallet", `${amount(hypow)} HYPOW \xB7 ${count(saved)} attempts saved`],
|
|
881
|
+
["session", `${found} block${found === 1 ? "" : "s"} found`],
|
|
882
|
+
["gas", `${hype(gas)} HYPE in the miner key \xB7 pays mining fees`],
|
|
883
|
+
["network", `block ${count(height)} \xB7 difficulty ${count(difficulty2)}`]
|
|
884
|
+
];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/status.ts
|
|
888
|
+
var REFRESH_MS = 6e4;
|
|
889
|
+
var RPC_FIX = "--rpc <url>";
|
|
890
|
+
var LABEL_WIDTH = 9;
|
|
891
|
+
var CSI = "\x1B[";
|
|
892
|
+
var SAVE = "\x1B7";
|
|
893
|
+
var RESTORE = "\x1B8";
|
|
894
|
+
function render(rows, columns) {
|
|
895
|
+
const inner = Math.max(10, Math.min(columns - 4, Math.max(...rows.map(([, v]) => LABEL_WIDTH + v.length))));
|
|
896
|
+
const rule = "\u2500".repeat(inner + 2);
|
|
897
|
+
const body = rows.map(([label, value]) => {
|
|
898
|
+
const text = `${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
899
|
+
const fit = text.length > inner ? `${text.slice(0, inner - 1)}\u2026` : text.padEnd(inner);
|
|
900
|
+
const shown = value.startsWith("\u26A0") ? paint(["bold", "red"], fit.slice(LABEL_WIDTH)) : fit.slice(LABEL_WIDTH);
|
|
901
|
+
return `${paint("dim", "\u2502")} ${paint("dim", fit.slice(0, LABEL_WIDTH))}${shown} ${paint("dim", "\u2502")}`;
|
|
902
|
+
});
|
|
903
|
+
return [paint("dim", `\u256D${rule}\u256E`), ...body, paint("dim", `\u2570${rule}\u256F`)];
|
|
904
|
+
}
|
|
905
|
+
async function statusBox(c, owner) {
|
|
906
|
+
const out = process.stdout;
|
|
907
|
+
if (!out.isTTY) return { refresh: () => {
|
|
908
|
+
}, won: () => {
|
|
909
|
+
} };
|
|
910
|
+
const token = await c.read.readContract({ address: c.net.minter, abi: minterAbi, functionName: "token" });
|
|
911
|
+
let found = 0;
|
|
912
|
+
const read2 = async () => [...await statusRows(c, token, owner, found), rpcRow(c, RPC_FIX)];
|
|
913
|
+
let rows = await read2();
|
|
914
|
+
const height = rows.length + 2;
|
|
915
|
+
const draw2 = () => {
|
|
916
|
+
const top = out.rows - height;
|
|
917
|
+
const box = render(rows, out.columns).map((l, i) => `${CSI}${top + 1 + i};1H${CSI}2K${l}`);
|
|
918
|
+
out.write(`${SAVE}${box.join("")}${RESTORE}`);
|
|
919
|
+
};
|
|
920
|
+
const reserve = () => {
|
|
921
|
+
out.write(
|
|
922
|
+
`${SAVE}${CSI}r${RESTORE}${CSI}J${"\n".repeat(height)}${CSI}${height}A${SAVE}${CSI}1;${out.rows - height}r${RESTORE}`
|
|
923
|
+
);
|
|
924
|
+
draw2();
|
|
925
|
+
};
|
|
926
|
+
const release = () => out.write(`${SAVE}${CSI}r${RESTORE}${CSI}J`);
|
|
927
|
+
reserve();
|
|
928
|
+
out.on("resize", reserve);
|
|
929
|
+
process.on("exit", release);
|
|
930
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => process.exit(128 + constants.signals[signal]));
|
|
931
|
+
const refresh = coalesce(async () => {
|
|
932
|
+
try {
|
|
933
|
+
rows = await read2();
|
|
934
|
+
draw2();
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
setInterval(refresh, REFRESH_MS).unref();
|
|
939
|
+
return {
|
|
940
|
+
refresh,
|
|
941
|
+
won: () => {
|
|
942
|
+
found++;
|
|
943
|
+
refresh();
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/mine.ts
|
|
949
|
+
async function mine(version, opts) {
|
|
950
|
+
line("hypow", `${version} \xB7 ${MAINNET.name} \xB7 minter ${short(MAINNET.minter)}`);
|
|
951
|
+
const { key, created } = loadKey();
|
|
952
|
+
line(
|
|
953
|
+
"miner key",
|
|
954
|
+
`${short(privateKeyToAccount2(key).address)} (${created ? "new, " : ""}saved to ${home().replace(process.env.HOME ?? "", "~")}) \xB7 a small separate wallet that pays mining fees`
|
|
955
|
+
);
|
|
956
|
+
const c = connect(MAINNET, key, await chooseRpc(MAINNET, opts.rpc));
|
|
957
|
+
line(...rpcRow(c, RPC_FIX));
|
|
958
|
+
const { owner, status: status2 } = await authorize(c, opts);
|
|
959
|
+
saveOwner(owner);
|
|
960
|
+
line("owner", `${short(owner)} \xB7 key authorized \xB7 miner key holds ${hype(status2.balance)} HYPE for fees`);
|
|
961
|
+
const say = explainRateLimits(line, c.own, RPC_FIX);
|
|
962
|
+
const box = await retrying(say, () => statusBox(c, owner), 5e3);
|
|
963
|
+
await run(c, owner, { line: say, search: searchInWorker, ...box, gas: box.refresh }, new AbortController().signal);
|
|
964
|
+
}
|
|
965
|
+
var SEARCH_WORKER = new URL(`./search-worker${extname(import.meta.url)}`, import.meta.url);
|
|
966
|
+
function searchInWorker(seed, owner, k, target) {
|
|
967
|
+
const worker = new Worker(SEARCH_WORKER, { workerData: { seed, owner, k, target } });
|
|
968
|
+
return new Promise((resolve, reject) => {
|
|
969
|
+
worker.once("message", resolve);
|
|
970
|
+
worker.once("error", reject);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/cli.ts
|
|
975
|
+
var USAGE = `usage: hypow mine [--owner 0x\u2026] [--gas <HYPE>] [--rpc <url>] [--no-browser]
|
|
976
|
+
|
|
977
|
+
Mines on HyperEVM mainnet. The key, owner and RPC are kept in ~/.hypow/mainnet.
|
|
978
|
+
|
|
979
|
+
--owner 0x\u2026 the wallet you trade with, if you'd rather authorize by hand
|
|
980
|
+
than through the browser page
|
|
981
|
+
--gas <HYPE> HYPE the page sends the miner key for gas (default ${hype(MAINNET.gasFund)})
|
|
982
|
+
--rpc <url> your own HyperEVM RPC, tried before the public ones. The
|
|
983
|
+
first run asks for it; either way it is saved
|
|
984
|
+
--no-browser print the authorize page's address instead of opening it
|
|
985
|
+
-h, --help show this help
|
|
986
|
+
|
|
987
|
+
On a terminal, a status box stays pinned below the log: your HYPOW, the
|
|
988
|
+
attempts you have saved, blocks found this session, the miner key's gas and
|
|
989
|
+
the network. Set NO_COLOR to turn off color. Piped output is plain lines.`;
|
|
990
|
+
async function main() {
|
|
991
|
+
const { positionals, values } = parseArgs({
|
|
992
|
+
allowPositionals: true,
|
|
993
|
+
allowNegative: true,
|
|
994
|
+
options: {
|
|
995
|
+
owner: { type: "string" },
|
|
996
|
+
gas: { type: "string" },
|
|
997
|
+
rpc: { type: "string" },
|
|
998
|
+
browser: { type: "boolean", default: true },
|
|
999
|
+
help: { type: "boolean", short: "h" }
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
if (values.help) {
|
|
1003
|
+
console.log(USAGE);
|
|
1004
|
+
process.exit(0);
|
|
1005
|
+
}
|
|
1006
|
+
if (positionals.length !== 1 || positionals[0] !== "mine") {
|
|
1007
|
+
console.log(USAGE);
|
|
1008
|
+
process.exit(positionals.length === 0 ? 0 : 1);
|
|
1009
|
+
}
|
|
1010
|
+
if (values.owner !== void 0 && !isAddress3(values.owner)) throw new Error(`--owner ${values.owner} is not an address`);
|
|
1011
|
+
const gas = values.gas === void 0 ? MAINNET.gasFund : parseEther2(values.gas);
|
|
1012
|
+
if (gas < MAINNET.minGas) throw new Error(`--gas must be at least ${hype(MAINNET.minGas)} HYPE`);
|
|
1013
|
+
const { version } = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
|
|
1014
|
+
await mine(version, {
|
|
1015
|
+
owner: values.owner ? getAddress3(values.owner) : loadOwner(),
|
|
1016
|
+
manual: values.owner !== void 0,
|
|
1017
|
+
gas,
|
|
1018
|
+
browser: values.browser,
|
|
1019
|
+
rpc: values.rpc
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
main().catch((err) => {
|
|
1023
|
+
console.error(format("error", describe(err)));
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
});
|