posci-miner 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Proof of Scientist (POSCI) contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,259 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/logo.png" alt="POSCI logo" width="160" height="160" />
4
+
5
+ # posci-miner
6
+
7
+ **CLI miner for [POSCI](https://scientistdapp.online) — Proof of Scientist · Ethereum mainnet**
8
+
9
+ `CPU + WebGPU + hybrid · Local encrypted wallet · Anti-MEV by design`
10
+
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
12
+ [![Node](https://img.shields.io/badge/node-%E2%89%A518.17-339933)](https://nodejs.org)
13
+ [![Chain](https://img.shields.io/badge/chain-Ethereum%20mainnet-627EEA)](https://etherscan.io/token/0xD020e5E5c2724B2661C2FEF9AE878f49410a8B77)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## What is POSCI?
20
+
21
+ [**POSCI**](https://scientistdapp.online) is a **21,000,000-cap, owner-less, PoW-mined ERC-20** on Ethereum mainnet. 95% of the supply is distributed via on-chain Proof-of-Work mining (Bitcoin-style halving + difficulty retarget). The mining contract is **fully renounced** — there is no admin, no mint function, no upgrade proxy. The launch LP is permanently burned to `0x000…dEaD`.
22
+
23
+ The PoW digest is `keccak256(challenge ‖ msg.sender ‖ nonce)` — your wallet address is **inside the hash**. A copied nonce produces a different digest for any other miner, so **MEV bots cannot snipe your nonce from the mempool**.
24
+
25
+ | | |
26
+ |---|---|
27
+ | Token contract | [`0xD020e5E5c2724B2661C2FEF9AE878f49410a8B77`](https://etherscan.io/token/0xD020e5E5c2724B2661C2FEF9AE878f49410a8B77) |
28
+ | Mining contract | [`0x9EAdD7dF7701e03d07c3727EC1ba816C2C9De936`](https://etherscan.io/address/0x9EAdD7dF7701e03d07c3727EC1ba816C2C9De936) |
29
+ | Genesis contract| [`0x7bC1520Da49Cd56D5BE11aA77650cA998951459d`](https://etherscan.io/address/0x7bC1520Da49Cd56D5BE11aA77650cA998951459d) |
30
+ | Site / web miner| https://scientistdapp.online |
31
+
32
+ ---
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ # Requires Node.js ≥ 18.17
38
+ npm install -g posci-miner
39
+
40
+ # Or run without installing:
41
+ npx posci-miner --help
42
+ ```
43
+
44
+ > **GPU mining** requires Google Chrome / Chromium / Edge installed locally
45
+ > (the CLI launches a headless instance for the WebGPU compute kernel).
46
+ > Set `POSCI_CHROME_PATH=/path/to/chrome` if it's not in a default location.
47
+
48
+ ---
49
+
50
+ ## Quick start
51
+
52
+ ```bash
53
+ # 1. Create a fresh wallet (encrypted under ~/.posci/wallets/)
54
+ posci-miner wallet new alice --password mypw
55
+
56
+ # → prints address + private key (private key shown once, save it)
57
+ # → send some ETH to that address to pay for mine() tx fees (~0.001 ETH/win)
58
+
59
+ # 2. Check network status
60
+ posci-miner status --wallet alice --password mypw
61
+
62
+ # 3. Start mining
63
+ posci-miner mine --wallet alice --password mypw --hybrid
64
+ ```
65
+
66
+ That's it. The CLI auto-submits successful PoW solutions and shows a live dashboard with hashrate, network difficulty, and recent hits.
67
+
68
+ ---
69
+
70
+ ## Commands
71
+
72
+ ### `wallet new <name>`
73
+ Generate a fresh wallet, encrypt the private key with `--password` (AES-256-GCM, PBKDF2 250k rounds), store under `~/.posci/wallets/<name>.json`.
74
+
75
+ ```bash
76
+ posci-miner wallet new alice --password mypw
77
+ ```
78
+
79
+ ### `wallet list` / `wallet show <name>` / `wallet import <name>`
80
+ Manage local encrypted keystores. `wallet show --private` reveals the decrypted private key (requires password).
81
+
82
+ ### `mine`
83
+ Start mining with one of three engines:
84
+
85
+ | Flag | Meaning |
86
+ |---|---|
87
+ | `--cpu [n]` | n CPU worker threads (default: all cores) |
88
+ | `--gpu [n]` | n WebGPU workgroups per dispatch (default: 64). Requires Chrome. |
89
+ | `--hybrid` | both CPU and GPU concurrently |
90
+
91
+ Examples:
92
+
93
+ ```bash
94
+ # Pure CPU, all cores
95
+ posci-miner mine --wallet alice --password mypw
96
+
97
+ # Pure CPU, 4 workers
98
+ posci-miner mine --wallet alice --password mypw --cpu 4
99
+
100
+ # Pure GPU (one Chrome process)
101
+ posci-miner mine --wallet alice --password mypw --gpu 256
102
+
103
+ # Hybrid: CPU on N-1 cores + GPU
104
+ posci-miner mine --wallet alice --password mypw --hybrid
105
+
106
+ # Use raw private key instead of local wallet store
107
+ posci-miner mine --key 0xabcdef... --hybrid
108
+
109
+ # Use a specific RPC (defaults to a public Ethereum node)
110
+ posci-miner mine --wallet alice --password mypw --rpc https://eth-mainnet.g.alchemy.com/v2/<key> --hybrid
111
+
112
+ # Plain log mode (no full TUI — useful for CI / nohup)
113
+ posci-miner mine --wallet alice --password mypw --no-dashboard
114
+ ```
115
+
116
+ ### `status`
117
+ Snapshot the on-chain mining state and (optionally) your account balance.
118
+
119
+ ```bash
120
+ posci-miner status # network only
121
+ posci-miner status --address 0xYourAddress # + balance
122
+ posci-miner status --wallet alice --password mypw # + balance from local wallet
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Live dashboard
128
+
129
+ When stdout is a TTY, the `mine` command renders a live dashboard:
130
+
131
+ ```
132
+ POSCI miner · HYBRID · 3m 12s
133
+ ─────────────────────────────────────────────────────────────
134
+ Miner 0x9Fa33C79C8bc2Ad785fdE6D10608dB97A95093fa
135
+ Status time ✓ · pool ✓
136
+
137
+ Hashrate
138
+ CPU 2.4 MH/s
139
+ GPU 18.7 MH/s
140
+ Total 21.1 MH/s
141
+ Network ~134 MH/s (implied by difficulty)
142
+
143
+ Network
144
+ Difficulty 1,920
145
+ Reward 1,000 POSCI/block
146
+ Epoch 47
147
+ Remaining 19,953,000 POSCI
148
+
149
+ My account
150
+ Balance 2,000.0000 POSCI
151
+ Mined this run 1,000.0000 POSCI
152
+
153
+ Recent hits
154
+ ✓ 14:23:01 GPU +1000 POSCI 0xa1b2c3d4…
155
+
156
+ Ctrl+C to stop
157
+ ```
158
+
159
+ ---
160
+
161
+ ## How it works
162
+
163
+ 1. **CPU engine** — N Node.js `worker_threads`, each running `js-sha3.keccak_256` in a tight loop with a unique stride to cover disjoint nonce ranges.
164
+ 2. **GPU engine** — spawns headless Chrome with our WGSL compute shader (same one the web frontend uses). Chrome posts hits + hashrate over a localhost WebSocket back to the CLI. Reuses the battle-tested shader in `src/mining/keccak256.wgsl.mjs`.
165
+ 3. **Hybrid** — both run concurrently with disjoint nonce strides so they never duplicate work. Dedup at the manager catches the rare cross-engine collision.
166
+ 4. **Auto-submit** — when any engine finds a valid digest, the CLI immediately calls `mine(nonce, digest)` on the contract from your wallet. Reward lands in your wallet.
167
+ 5. **Auto-refresh** — every 12s (configurable), the CLI polls the chain for new challenge / target / difficulty and hot-swaps the job into running workers.
168
+
169
+ The hash that the contract verifies is exactly:
170
+ ```
171
+ keccak256(abi.encodePacked(challengeNumber, msg.sender, nonce))
172
+ ```
173
+ Both engines produce digests with **your wallet's address** as `msg.sender`. That's why **only YOU** can submit a valid mine() tx — anyone copying your nonce from the mempool would get a different digest (since their msg.sender is different) and the contract would revert.
174
+
175
+ ---
176
+
177
+ ## Wallet security
178
+
179
+ Wallets created with `wallet new`:
180
+ - Stored under `~/.posci/wallets/<name>.json` (POSIX 0600 perms)
181
+ - Private key encrypted with AES-256-GCM
182
+ - Key derivation: PBKDF2-HMAC-SHA256, 250,000 iterations, 16-byte random salt
183
+ - Plaintext private key only held in memory while mining
184
+
185
+ If you lose the password, the wallet is unrecoverable. Back up the keystore JSON + remember the password.
186
+
187
+ You can also pass a private key directly with `--key 0x...` for ephemeral use (e.g., a dedicated mining wallet), or set `POSCI_PRIVATE_KEY` / `POSCI_WALLET` + `POSCI_PASSWORD` env vars.
188
+
189
+ ---
190
+
191
+ ## Realistic expectations
192
+
193
+ POSCI mining is competitive — at any given moment, the network hashrate × difficulty determines your expected wait. Some napkin math:
194
+
195
+ | Your hashrate | Network hashrate | Expected wait per block (1000 POSCI) |
196
+ |---|---|---|
197
+ | 1 MH/s | 100 MH/s | ~10 minutes |
198
+ | 10 MH/s | 100 MH/s | ~1 minute |
199
+ | 100 MH/s | 1 GH/s | ~10 minutes |
200
+
201
+ Use the live `posci-miner status` to see current network hashrate.
202
+
203
+ Each successful `mine()` tx costs ETH gas (~0.0005-0.005 ETH at typical mainnet rates). At low POSCI prices, ensure your reward × POSCI_USD_price > tx_gas_cost.
204
+
205
+ ---
206
+
207
+ ## Troubleshooting
208
+
209
+ **"Mining is not yet open"** — both gates (time + pool) must be open. Time gate opens 12h after deploy. Pool gate opens after the genesis sale fills 0.5 ETH. Check live status: https://scientistdapp.online/stats
210
+
211
+ **"No Chrome/Edge/Chromium binary found"** — install Chrome or set `POSCI_CHROME_PATH`. GPU mode needs a recent Chrome (≥113) for WebGPU.
212
+
213
+ **"WebGPU NOT available in this Chrome"** — your Chrome was started without GPU access. On Linux, ensure you have GPU drivers + try `--enable-features=Vulkan` flag (the CLI passes this automatically).
214
+
215
+ **Hits show ✗ failed** — the most common cause is the challenge having rotated between when your worker hashed and when your tx landed. The CLI auto-detects this via periodic chain reads and hot-swaps. If you see persistent failures, check that your wallet has enough ETH for gas.
216
+
217
+ **"DifficultyNotMet"** — your engine's local target was stale (chain difficulty went up). The CLI will auto-correct on the next 12s refresh.
218
+
219
+ ---
220
+
221
+ ## Architecture
222
+
223
+ ```
224
+ posci-miner/
225
+ ├── bin/posci-miner.mjs CLI entry (commander)
226
+ ├── src/
227
+ │ ├── commands/ wallet · mine · status
228
+ │ ├── lib/
229
+ │ │ ├── chain.mjs viem clients, mining contract calls
230
+ │ │ ├── wallet.mjs local encrypted keystore (AES-256-GCM)
231
+ │ │ ├── config.mjs contract addresses + RPCs
232
+ │ │ ├── format.mjs hashrate / POSCI / duration formatters
233
+ │ │ └── log.mjs colored logging
234
+ │ ├── mining/
235
+ │ │ ├── manager.mjs orchestrator (CPU + GPU + dedup)
236
+ │ │ ├── cpu-worker.mjs Node worker_threads CPU keccak
237
+ │ │ ├── gpu-driver.mjs spawns headless Chrome for WebGPU
238
+ │ │ └── keccak256.wgsl.mjs the WGSL compute shader
239
+ │ └── ui/dashboard.mjs live TUI dashboard
240
+ └── docs/ logo assets
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Contributing
246
+
247
+ PRs welcome. Open an issue for design discussion before significant changes.
248
+
249
+ Please don't submit token-shilling spam — this repo is for the miner only. For protocol questions, [open an issue](https://github.com/Scientists-posci/posci-miner/issues) or come to [@scientistsdapp](https://x.com/scientistsdapp) on X.
250
+
251
+ ---
252
+
253
+ ## License
254
+
255
+ [MIT](LICENSE) © 2026 Proof of Scientist contributors
256
+
257
+ This software is provided as-is. Mining cryptocurrency may be regulated in your jurisdiction. You are responsible for understanding and complying with applicable laws.
258
+
259
+ The POSCI smart contracts are immutable, non-custodial, and were deployed by an EOA with no admin powers. The contributors of this miner have no control over the protocol or any user funds.
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // posci-miner — CLI miner for POSCI (Proof of Scientist) on Ethereum mainnet
4
+ //
5
+
6
+ import { Command } from 'commander';
7
+ import { readFileSync } from 'node:fs';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ import { registerWalletCommands } from '../src/commands/wallet.mjs';
12
+ import { registerMineCommand } from '../src/commands/mine.mjs';
13
+ import { registerStatusCommand } from '../src/commands/status.mjs';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
17
+
18
+ const program = new Command();
19
+ program
20
+ .name('posci-miner')
21
+ .description('CLI miner for POSCI (Proof of Scientist) on Ethereum mainnet.\n CPU + WebGPU + hybrid modes. Local encrypted wallet store.')
22
+ .version(pkg.version, '-v, --version', 'show version')
23
+ .addHelpText('after', `
24
+ Examples:
25
+ $ posci-miner wallet new alice --password mypw
26
+ $ posci-miner status --wallet alice --password mypw
27
+ $ posci-miner mine --wallet alice --password mypw --hybrid
28
+ $ posci-miner mine --key 0xabc... --cpu 8
29
+ $ posci-miner mine --wallet alice --password mypw --gpu 256
30
+
31
+ Live data: https://scientistdapp.online
32
+ Source: https://github.com/Scientists-posci/posci-miner
33
+ `);
34
+
35
+ registerWalletCommands(program);
36
+ registerMineCommand(program);
37
+ registerStatusCommand(program);
38
+
39
+ program.parseAsync(process.argv).catch((e) => {
40
+ console.error('FATAL:', e?.stack ?? e?.message ?? e);
41
+ process.exit(1);
42
+ });
package/docs/logo.png ADDED
Binary file
package/docs/logo.svg ADDED
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ POSCI primary token logo. Monochrome edition matching the Tesla/Starlink-
4
+ inflected site theme. 512×512 viewBox, recognizable at any size from 16px to
5
+ 1024px, no gradients (rasterizes cleanly via Chrome headless).
6
+ -->
7
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
8
+ <defs>
9
+ <radialGradient id="orbBg" cx="35%" cy="28%" r="80%">
10
+ <stop offset="0%" stop-color="#3a3f4a"/>
11
+ <stop offset="35%" stop-color="#1a1d23"/>
12
+ <stop offset="100%" stop-color="#000000"/>
13
+ </radialGradient>
14
+ <radialGradient id="specular" cx="38%" cy="22%" r="35%">
15
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.55"/>
16
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
17
+ </radialGradient>
18
+ </defs>
19
+
20
+ <!-- Outer body — slightly off-pure-black sphere, gives subtle depth -->
21
+ <circle cx="256" cy="256" r="248" fill="url(#orbBg)"/>
22
+
23
+ <!-- Top-light specular highlight — gives the orb a "lit" feel -->
24
+ <ellipse cx="200" cy="160" rx="160" ry="90" fill="url(#specular)"/>
25
+
26
+ <!-- Crisp 1px white outline ring — readable at any size -->
27
+ <circle cx="256" cy="256" r="248" fill="none" stroke="#ffffff" stroke-width="2" stroke-opacity="0.92"/>
28
+
29
+ <!-- Subtle inner mesh: 2 latitude lines, suggests "scientific instrument" -->
30
+ <g fill="none" stroke="#ffffff" stroke-opacity="0.10" stroke-width="1">
31
+ <ellipse cx="256" cy="256" rx="246" ry="80"/>
32
+ <ellipse cx="256" cy="256" rx="246" ry="160"/>
33
+ </g>
34
+
35
+ <!-- One thin tilted orbital ring — the project's "scientist" motif -->
36
+ <g transform="rotate(-22 256 256)">
37
+ <ellipse cx="256" cy="256" rx="234" ry="58" fill="none" stroke="#ffffff" stroke-opacity="0.42" stroke-width="2"/>
38
+ <circle cx="22" cy="256" r="6" fill="#ffffff"/>
39
+ </g>
40
+
41
+ <!-- Φ glyph — the brand. Light weight, large, perfectly centered. -->
42
+ <text
43
+ x="256" y="256"
44
+ text-anchor="middle"
45
+ dominant-baseline="central"
46
+ font-family="Georgia, 'Times New Roman', serif"
47
+ font-weight="500"
48
+ font-size="296"
49
+ fill="#ffffff"
50
+ >Φ</text>
51
+ </svg>
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "posci-miner",
3
+ "version": "0.1.0",
4
+ "description": "CLI miner for POSCI (Proof of Scientist) — Bitcoin-style PoW token on Ethereum mainnet. CPU + WebGPU (via headless Chrome) + hybrid modes. Anti-MEV: hash includes msg.sender.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://scientistdapp.online",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Scientists-posci/posci-miner.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Scientists-posci/posci-miner/issues"
14
+ },
15
+ "keywords": [
16
+ "posci",
17
+ "ethereum",
18
+ "mining",
19
+ "pow",
20
+ "proof-of-work",
21
+ "erc20",
22
+ "webgpu",
23
+ "0xbitcoin",
24
+ "fair-launch",
25
+ "cli",
26
+ "miner"
27
+ ],
28
+ "author": "scientistsdapp",
29
+ "engines": {
30
+ "node": ">=18.17"
31
+ },
32
+ "bin": {
33
+ "posci-miner": "bin/posci-miner.mjs",
34
+ "posci": "bin/posci-miner.mjs"
35
+ },
36
+ "scripts": {
37
+ "start": "node bin/posci-miner.mjs",
38
+ "dev": "node bin/posci-miner.mjs --help",
39
+ "test": "node --test test/"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^12.1.0",
43
+ "js-sha3": "^0.9.3",
44
+ "kleur": "^4.1.5",
45
+ "viem": "^2.21.55",
46
+ "ws": "^8.18.0"
47
+ },
48
+ "files": [
49
+ "bin/",
50
+ "src/",
51
+ "docs/logo.png",
52
+ "docs/logo.svg",
53
+ "README.md",
54
+ "LICENSE"
55
+ ]
56
+ }
@@ -0,0 +1,222 @@
1
+ // `posci-miner mine` — start mining with CPU / GPU / hybrid.
2
+
3
+ import { availableParallelism } from 'node:os';
4
+ import { randomBytes } from 'node:crypto';
5
+
6
+ import { resolveAccount } from '../lib/wallet.mjs';
7
+ import { makePublicClient, makeWalletClient, readMiningState, readPosciBalance, submitMine } from '../lib/chain.mjs';
8
+ import { MiningManager } from '../mining/manager.mjs';
9
+ import { Dashboard } from '../ui/dashboard.mjs';
10
+ import { log, c } from '../lib/log.mjs';
11
+ import { formatPosci, formatHashrate } from '../lib/format.mjs';
12
+
13
+ export function registerMineCommand(program) {
14
+ program.command('mine')
15
+ .description('start mining POSCI (CPU / GPU / hybrid)')
16
+ .option('--cpu [n]', 'use n CPU workers (default: number of cores)')
17
+ .option('--gpu [n]', 'use GPU with n workgroups per dispatch (1-1024, default: 64). Requires Chrome.')
18
+ .option('--hybrid', 'use both CPU and GPU')
19
+ .option('--rpc <url>', 'Ethereum RPC URL (or POSCI_RPC env)')
20
+ .option('--wallet <name>', 'local wallet name (use `wallet new` to create one)')
21
+ .option('--password <pw>', 'wallet password (or POSCI_PASSWORD env)')
22
+ .option('--key <0x...>', 'raw private key (skip local wallet store; use with care)')
23
+ .option('--chrome <path>', 'path to chrome binary (or POSCI_CHROME_PATH env)')
24
+ .option('--no-dashboard', 'plain log mode instead of full TUI')
25
+ .option('--refresh <sec>', 'how often to re-poll chain state', '12')
26
+ .action(async (opts) => {
27
+ // ── Resolve auth + mode ────────────────────────────────────────────
28
+ let acct;
29
+ try { acct = resolveAccount(opts); }
30
+ catch (e) { log.err(e.message); process.exit(1); }
31
+
32
+ let cpuN = 0, gpuN = 0;
33
+ const allCores = availableParallelism();
34
+
35
+ if (opts.hybrid) {
36
+ cpuN = Math.max(1, allCores - 1);
37
+ gpuN = 64;
38
+ } else if (opts.gpu !== undefined) {
39
+ gpuN = Number(opts.gpu === true ? 64 : opts.gpu);
40
+ if (opts.cpu !== undefined) cpuN = Number(opts.cpu === true ? allCores : opts.cpu);
41
+ } else if (opts.cpu !== undefined) {
42
+ cpuN = Number(opts.cpu === true ? allCores : opts.cpu);
43
+ } else {
44
+ // Default: CPU with all cores
45
+ cpuN = allCores;
46
+ }
47
+
48
+ cpuN = Math.max(0, Math.min(64, cpuN));
49
+ gpuN = Math.max(0, Math.min(1024, gpuN));
50
+
51
+ const mode = (cpuN > 0 && gpuN > 0) ? 'hybrid' : (gpuN > 0 ? 'gpu' : 'cpu');
52
+
53
+ log.banner(c.bold().cyan(' POSCI miner'));
54
+ log.info(` miner: ${acct.address} (${acct.source})`);
55
+ log.info(` mode : ${mode.toUpperCase()} (cpu=${cpuN}, gpu=${gpuN})`);
56
+
57
+ const pub = makePublicClient(opts.rpc);
58
+ const wallet = makeWalletClient(opts.rpc, acct.privateKey);
59
+
60
+ // ── Initial chain pull ─────────────────────────────────────────────
61
+ let chain = await readMiningState(pub);
62
+ if (!chain.canMine) {
63
+ log.warn(`Mining is not yet open:`);
64
+ log.warn(` time gate ${chain.timeGateOpen ? '✓' : '⏳'} pool gate ${chain.poolGateOpen ? '✓' : '⏳'}`);
65
+ log.info(` CLI will wait and check every ${opts.refresh}s.`);
66
+ }
67
+
68
+ const myMinedAtStart = await readPosciBalance(pub, acct.address);
69
+ let myMinedRun = 0n;
70
+
71
+ const dash = new Dashboard({ minerAddress: acct.address, mode });
72
+ dash.update({
73
+ difficulty: chain.difficulty,
74
+ reward: chain.miningReward,
75
+ remaining: chain.remainingSupply,
76
+ epoch: chain.epochCount,
77
+ poolGate: chain.poolGateOpen,
78
+ timeGate: chain.timeGateOpen,
79
+ miningStartTime: chain.miningStartTime,
80
+ networkHashrate: chain.networkHashrate,
81
+ myBalance: myMinedAtStart,
82
+ });
83
+
84
+ if (opts.dashboard !== false) dash.start();
85
+
86
+ // ── Mining manager ─────────────────────────────────────────────────
87
+ const startNonce = BigInt('0x' + randomBytes(8).toString('hex'));
88
+ const mgr = new MiningManager({
89
+ minerAddress: acct.address,
90
+ cpuWorkers: cpuN,
91
+ gpuPower: gpuN,
92
+ gpuChromePath: opts.chrome,
93
+ });
94
+ mgr.on('error', ({ source, error }) => log.warn(`[${source}] ${error.message ?? error}`));
95
+ mgr.on('stats', ({ cpu, gpu }) => dash.update({ cpuRate: cpu.hashrate, gpuRate: gpu.hashrate }));
96
+
97
+ // ── Hit handling — gated on chain.canMine ─────────────────────────
98
+ // Workers don't know about gates; they just hash. The check that
99
+ // matters is "is the contract actually willing to accept mine() right
100
+ // now?" — driven by chain.canMine (time gate AND pool gate both open).
101
+ //
102
+ // If gates are closed, we BUFFER the most recent valid solution
103
+ // (against the current challenge) and re-attempt as soon as gates
104
+ // open. This gets you the FIRST submission window without burning gas
105
+ // on guaranteed-revert txs.
106
+ let pendingHit = null;
107
+ let busy = false; // ensure only one in-flight tx at a time
108
+
109
+ async function trySubmit(hit) {
110
+ if (busy) return;
111
+ if (!chain.canMine) {
112
+ pendingHit = hit;
113
+ return;
114
+ }
115
+ busy = true;
116
+ const nonceHex = '0x' + hit.nonce.toString(16);
117
+ dash.pushHit({ source: hit.source, nonce: nonceHex, digest: hit.digest,
118
+ reward: chain.miningReward, status: 'pending' });
119
+ try {
120
+ const txHash = await submitMine(wallet, pub, hit.nonce, hit.digest);
121
+ dash.pushHit({ source: hit.source, nonce: nonceHex, digest: hit.digest,
122
+ reward: chain.miningReward, txHash, status: 'pending' });
123
+ const receipt = await pub.waitForTransactionReceipt({ hash: txHash, timeout: 180_000 });
124
+ if (receipt.status === 'success') {
125
+ myMinedRun += chain.miningReward;
126
+ const bal = await readPosciBalance(pub, acct.address);
127
+ dash.pushHit({ source: hit.source, nonce: nonceHex, digest: hit.digest,
128
+ reward: chain.miningReward, txHash, status: 'success' });
129
+ dash.update({ myMined: myMinedRun, myBalance: bal });
130
+ } else {
131
+ dash.pushHit({ source: hit.source, nonce: nonceHex, digest: hit.digest,
132
+ reward: 0n, txHash, status: 'failed' });
133
+ }
134
+ } catch (e) {
135
+ dash.pushHit({ source: hit.source, nonce: nonceHex, digest: hit.digest,
136
+ reward: 0n, status: 'failed' });
137
+ log.warn(`submit error: ${e.shortMessage ?? e.message ?? e}`);
138
+ } finally {
139
+ busy = false;
140
+ pendingHit = null;
141
+ }
142
+ }
143
+
144
+ mgr.on('hit', (hit) => {
145
+ if (chain.canMine) {
146
+ // Gates open — submit immediately
147
+ trySubmit(hit).catch(() => {});
148
+ } else {
149
+ // Gates closed — keep the most recent solution for current challenge.
150
+ // Don't try to submit yet (would burn gas on guaranteed revert).
151
+ pendingHit = hit;
152
+ }
153
+ });
154
+
155
+ // Surface "we have a buffered solution waiting for gates" in the dashboard
156
+ const pendingTimer = setInterval(() => {
157
+ dash.update({
158
+ pendingNote: (!chain.canMine && pendingHit)
159
+ ? `★ buffered solution ready — will submit the moment gates open`
160
+ : '',
161
+ });
162
+ }, 1000);
163
+
164
+ await mgr.start({
165
+ challenge: chain.challengeNumber,
166
+ target: chain.miningTarget,
167
+ startNonce,
168
+ });
169
+
170
+ // ── Periodic chain refresh: detect new challenge / target / gates ──
171
+ const refreshSec = Math.max(3, Number(opts.refresh));
172
+ const refreshTimer = setInterval(async () => {
173
+ try {
174
+ const fresh = await readMiningState(pub);
175
+ const gatesJustOpened = fresh.canMine && !chain.canMine;
176
+ const challengeRotated = fresh.challengeNumber !== chain.challengeNumber
177
+ || fresh.miningTarget !== chain.miningTarget;
178
+
179
+ if (challengeRotated) {
180
+ mgr.setJob({
181
+ challenge: fresh.challengeNumber,
182
+ target: fresh.miningTarget,
183
+ startNonce: BigInt('0x' + randomBytes(8).toString('hex')),
184
+ });
185
+ // Old buffered solution is now invalid (different challenge)
186
+ pendingHit = null;
187
+ }
188
+ chain = fresh;
189
+ dash.update({
190
+ difficulty: chain.difficulty,
191
+ reward: chain.miningReward,
192
+ remaining: chain.remainingSupply,
193
+ epoch: chain.epochCount,
194
+ poolGate: chain.poolGateOpen,
195
+ timeGate: chain.timeGateOpen,
196
+ miningStartTime: chain.miningStartTime,
197
+ networkHashrate: chain.networkHashrate,
198
+ });
199
+
200
+ // Race-window: gates just flipped open AND we have a buffered solution
201
+ if (gatesJustOpened && pendingHit) {
202
+ log.ok(`Gates opened — submitting buffered solution from pre-warm period`);
203
+ trySubmit(pendingHit).catch(() => {});
204
+ }
205
+ } catch { /* ignore transient RPC errors */ }
206
+ }, refreshSec * 1000);
207
+
208
+ // ── Graceful exit ──────────────────────────────────────────────────
209
+ const cleanup = () => {
210
+ clearInterval(refreshTimer);
211
+ clearInterval(pendingTimer);
212
+ mgr.stop();
213
+ dash.stop();
214
+ log.banner(c.bold(' Stopped.'));
215
+ log.info(` Hashes computed this run: ${(mgr.cpuStats.totalHashes + mgr.gpuStats.totalHashes).toString()}`);
216
+ log.info(` POSCI mined this run : ${formatPosci(myMinedRun, 4)}`);
217
+ process.exit(0);
218
+ };
219
+ process.on('SIGINT', cleanup);
220
+ process.on('SIGTERM', cleanup);
221
+ });
222
+ }