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 +21 -0
- package/README.md +259 -0
- package/bin/posci-miner.mjs +42 -0
- package/docs/logo.png +0 -0
- package/docs/logo.svg +51 -0
- package/package.json +56 -0
- package/src/commands/mine.mjs +222 -0
- package/src/commands/status.mjs +60 -0
- package/src/commands/wallet.mjs +82 -0
- package/src/lib/chain.mjs +96 -0
- package/src/lib/config.mjs +24 -0
- package/src/lib/format.mjs +38 -0
- package/src/lib/log.mjs +20 -0
- package/src/lib/wallet.mjs +150 -0
- package/src/mining/cpu-worker.mjs +99 -0
- package/src/mining/gpu-driver.mjs +267 -0
- package/src/mining/keccak256.wgsl.mjs +228 -0
- package/src/mining/manager.mjs +150 -0
- package/src/ui/dashboard.mjs +130 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// `posci-miner status` — one-shot snapshot of the network + your account.
|
|
2
|
+
|
|
3
|
+
import { makePublicClient, readMiningState, readPosciBalance } from '../lib/chain.mjs';
|
|
4
|
+
import { resolveAccount } from '../lib/wallet.mjs';
|
|
5
|
+
import { log, c } from '../lib/log.mjs';
|
|
6
|
+
import { formatHashrate, formatPosci, shortAddr, formatDuration } from '../lib/format.mjs';
|
|
7
|
+
import { POSCI_TOKEN, POSCI_MINING, POSCI_GENESIS } from '../lib/config.mjs';
|
|
8
|
+
|
|
9
|
+
export function registerStatusCommand(program) {
|
|
10
|
+
program.command('status')
|
|
11
|
+
.description('one-shot snapshot of POSCI mining contract + your account')
|
|
12
|
+
.option('--rpc <url>', 'RPC URL (defaults to public endpoints)')
|
|
13
|
+
.option('--wallet <name>', 'local wallet name')
|
|
14
|
+
.option('--password <pw>', 'wallet password (or POSCI_PASSWORD env)')
|
|
15
|
+
.option('--key <0x...>', 'use raw private key (skips local wallet store)')
|
|
16
|
+
.option('--address <0x...>', 'just look up balance of this address (no auth)')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
const pub = makePublicClient(opts.rpc);
|
|
19
|
+
log.banner(c.cyan().bold(' POSCI · network status'));
|
|
20
|
+
try {
|
|
21
|
+
const s = await readMiningState(pub);
|
|
22
|
+
const lines = [
|
|
23
|
+
['Network', 'Ethereum mainnet (chainId 1)'],
|
|
24
|
+
['Mining contract', POSCI_MINING],
|
|
25
|
+
['Token', POSCI_TOKEN],
|
|
26
|
+
['Genesis', POSCI_GENESIS],
|
|
27
|
+
['', ''],
|
|
28
|
+
['Time gate', s.timeGateOpen ? c.green('OPEN ✓') : c.yellow(`opens ${new Date(s.miningStartTime*1000).toLocaleString()}`)],
|
|
29
|
+
['Pool gate', s.poolGateOpen ? c.green('OPEN ✓') : c.yellow('CLOSED — waiting on genesis cap')],
|
|
30
|
+
['Mining live', s.canMine ? c.green().bold('YES') : c.yellow('NO')],
|
|
31
|
+
['', ''],
|
|
32
|
+
['Difficulty', s.difficulty.toLocaleString()],
|
|
33
|
+
['Reward / block', formatPosci(s.miningReward, 0) + ' POSCI'],
|
|
34
|
+
['Epoch', s.epochCount.toString()],
|
|
35
|
+
['Mined so far', formatPosci(s.tokensMinted, 0) + ' POSCI'],
|
|
36
|
+
['Remaining', formatPosci(s.remainingSupply, 0) + ' POSCI'],
|
|
37
|
+
['Network hashrate ≈', formatHashrate(s.networkHashrate)],
|
|
38
|
+
];
|
|
39
|
+
for (const [k, v] of lines) {
|
|
40
|
+
if (k === '') console.log('');
|
|
41
|
+
else console.log(` ${c.dim(k.padEnd(18))} ${v}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let addr;
|
|
45
|
+
if (opts.address) addr = opts.address;
|
|
46
|
+
else {
|
|
47
|
+
try { addr = resolveAccount(opts).address; } catch { /* no auth, skip */ }
|
|
48
|
+
}
|
|
49
|
+
if (addr) {
|
|
50
|
+
const bal = await readPosciBalance(pub, addr);
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(` ${c.dim('Your address ')} ${c.bold(addr)}`);
|
|
53
|
+
console.log(` ${c.dim('Your POSCI ')} ${c.bold(formatPosci(bal, 4))} POSCI`);
|
|
54
|
+
}
|
|
55
|
+
console.log('');
|
|
56
|
+
} catch (e) {
|
|
57
|
+
log.err(`Status failed: ${e.message}`); process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Wallet subcommands.
|
|
2
|
+
|
|
3
|
+
import { walletNew, walletLoad, walletImport, listWallets, walletDir } from '../lib/wallet.mjs';
|
|
4
|
+
import { c, log } from '../lib/log.mjs';
|
|
5
|
+
import { shortAddr } from '../lib/format.mjs';
|
|
6
|
+
|
|
7
|
+
export function registerWalletCommands(program) {
|
|
8
|
+
const w = program.command('wallet').description('manage local encrypted wallets');
|
|
9
|
+
|
|
10
|
+
w.command('new')
|
|
11
|
+
.description('generate a new wallet, encrypted with --password')
|
|
12
|
+
.argument('<name>', 'wallet name (alphanumeric)')
|
|
13
|
+
.option('--password <pw>', 'password for the keystore (or POSCI_PASSWORD env)')
|
|
14
|
+
.action((name, opts) => {
|
|
15
|
+
try {
|
|
16
|
+
const pw = opts.password || process.env.POSCI_PASSWORD;
|
|
17
|
+
if (!pw) { log.err('--password required (or set POSCI_PASSWORD env)'); process.exit(1); }
|
|
18
|
+
const r = walletNew(name, pw);
|
|
19
|
+
log.banner(c.green().bold(' ✓ Wallet created'));
|
|
20
|
+
console.log(` ${c.dim('name ')} ${r.name}`);
|
|
21
|
+
console.log(` ${c.dim('address ')} ${c.bold(r.address)}`);
|
|
22
|
+
console.log(` ${c.dim('keystore ')} ${r.path}`);
|
|
23
|
+
console.log(` ${c.dim('private ')} ${c.yellow(r.privateKey)} ${c.dim('(shown once)')}`);
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(c.yellow(' ⚠ Save the private key OFFLINE — it will not be shown again unless you decrypt.'));
|
|
26
|
+
console.log(` ${c.dim('Send some ETH to')} ${c.bold(r.address)} ${c.dim('to pay for mine() tx fees.')}`);
|
|
27
|
+
} catch (e) { log.err(e.message); process.exit(1); }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
w.command('list')
|
|
31
|
+
.description('list local wallets')
|
|
32
|
+
.action(() => {
|
|
33
|
+
const ws = listWallets();
|
|
34
|
+
if (ws.length === 0) {
|
|
35
|
+
log.info(`No wallets at ${walletDir()}.`);
|
|
36
|
+
log.info('Run: posci-miner wallet new <name> --password <pw>');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
log.banner(c.bold(` Local wallets (${walletDir()})`));
|
|
40
|
+
log.table(ws.map(w => ({
|
|
41
|
+
name: w.name, address: w.address, created: w.createdAt.slice(0, 19),
|
|
42
|
+
})));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
w.command('show')
|
|
46
|
+
.description('reveal address (and optionally private key) of a stored wallet')
|
|
47
|
+
.argument('<name>', 'wallet name')
|
|
48
|
+
.option('--password <pw>', 'password (required to reveal private key)')
|
|
49
|
+
.option('--private', 'also show the decrypted private key')
|
|
50
|
+
.action((name, opts) => {
|
|
51
|
+
try {
|
|
52
|
+
if (opts.private && !opts.password && !process.env.POSCI_PASSWORD) {
|
|
53
|
+
log.err('--password required to reveal private key');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (opts.private) {
|
|
57
|
+
const pw = opts.password || process.env.POSCI_PASSWORD;
|
|
58
|
+
const w = walletLoad(name, pw);
|
|
59
|
+
console.log(`address : ${w.address}`);
|
|
60
|
+
console.log(`privateKey : ${w.privateKey}`);
|
|
61
|
+
} else {
|
|
62
|
+
const ws = listWallets().find(w => w.name === name);
|
|
63
|
+
if (!ws) { log.err(`wallet '${name}' not found`); process.exit(1); }
|
|
64
|
+
console.log(`address: ${ws.address}`);
|
|
65
|
+
}
|
|
66
|
+
} catch (e) { log.err(e.message); process.exit(1); }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
w.command('import')
|
|
70
|
+
.description('import an external private key into the local store')
|
|
71
|
+
.argument('<name>', 'wallet name to save under')
|
|
72
|
+
.requiredOption('--key <0x...>', '0x-prefixed 64-hex private key')
|
|
73
|
+
.option('--password <pw>', 'encryption password (or POSCI_PASSWORD env)')
|
|
74
|
+
.action((name, opts) => {
|
|
75
|
+
try {
|
|
76
|
+
const pw = opts.password || process.env.POSCI_PASSWORD;
|
|
77
|
+
if (!pw) { log.err('--password required'); process.exit(1); }
|
|
78
|
+
const r = walletImport(name, opts.key, pw);
|
|
79
|
+
log.ok(`Imported ${r.address} as '${r.name}' → ${r.path}`);
|
|
80
|
+
} catch (e) { log.err(e.message); process.exit(1); }
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Thin viem wrapper for POSCI mining contract reads + the mine() write.
|
|
2
|
+
|
|
3
|
+
import { createPublicClient, createWalletClient, http } from 'viem';
|
|
4
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
import { mainnet } from 'viem/chains';
|
|
6
|
+
|
|
7
|
+
import { POSCI_TOKEN, POSCI_MINING, PUBLIC_RPCS, MAXIMUM_TARGET } from './config.mjs';
|
|
8
|
+
|
|
9
|
+
const TOKEN_ABI = [
|
|
10
|
+
{ type:'function', name:'balanceOf', stateMutability:'view',
|
|
11
|
+
inputs:[{type:'address',name:'a'}], outputs:[{type:'uint256'}] },
|
|
12
|
+
{ type:'function', name:'symbol', stateMutability:'view', inputs:[], outputs:[{type:'string'}] },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const MINING_ABI = [
|
|
16
|
+
{ type:'function', name:'mine', stateMutability:'nonpayable',
|
|
17
|
+
inputs:[{type:'uint256',name:'nonce'},{type:'bytes32',name:'challengeDigest'}], outputs:[] },
|
|
18
|
+
{ type:'function', name:'challengeNumber', stateMutability:'view', inputs:[], outputs:[{type:'bytes32'}] },
|
|
19
|
+
{ type:'function', name:'miningTarget', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
20
|
+
{ type:'function', name:'getMiningReward', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
21
|
+
{ type:'function', name:'getMiningDifficulty', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
22
|
+
{ type:'function', name:'getRemainingSupply', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
23
|
+
{ type:'function', name:'epochCount', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
24
|
+
{ type:'function', name:'tokensMinted', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
25
|
+
{ type:'function', name:'miningStartTime', stateMutability:'view', inputs:[], outputs:[{type:'uint256'}] },
|
|
26
|
+
{ type:'function', name:'poolGateOpen', stateMutability:'view', inputs:[], outputs:[{type:'bool'}] },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function makePublicClient(rpcUrl) {
|
|
30
|
+
return createPublicClient({
|
|
31
|
+
chain: mainnet,
|
|
32
|
+
transport: http(rpcUrl || process.env.POSCI_RPC || PUBLIC_RPCS[0]),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeWalletClient(rpcUrl, privateKey) {
|
|
37
|
+
return createWalletClient({
|
|
38
|
+
chain: mainnet,
|
|
39
|
+
transport: http(rpcUrl || process.env.POSCI_RPC || PUBLIC_RPCS[0]),
|
|
40
|
+
account: privateKeyToAccount(privateKey),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Pull all mining state in one multicall. */
|
|
45
|
+
export async function readMiningState(publicClient) {
|
|
46
|
+
const calls = [
|
|
47
|
+
'challengeNumber','miningTarget','getMiningReward','getMiningDifficulty',
|
|
48
|
+
'getRemainingSupply','epochCount','tokensMinted','miningStartTime','poolGateOpen',
|
|
49
|
+
].map(fn => ({ address: POSCI_MINING, abi: MINING_ABI, functionName: fn }));
|
|
50
|
+
|
|
51
|
+
const r = await publicClient.multicall({ contracts: calls, allowFailure: false });
|
|
52
|
+
const [challenge, target, reward, difficulty, remaining, epoch, minted, startTime, poolOpen] = r;
|
|
53
|
+
|
|
54
|
+
const now = Math.floor(Date.now() / 1000);
|
|
55
|
+
return {
|
|
56
|
+
challengeNumber: challenge,
|
|
57
|
+
miningTarget: target,
|
|
58
|
+
miningReward: reward,
|
|
59
|
+
difficulty,
|
|
60
|
+
remainingSupply: remaining,
|
|
61
|
+
epochCount: epoch,
|
|
62
|
+
tokensMinted: minted,
|
|
63
|
+
miningStartTime: Number(startTime),
|
|
64
|
+
poolGateOpen: poolOpen,
|
|
65
|
+
timeGateOpen: BigInt(now) >= startTime,
|
|
66
|
+
canMine: (BigInt(now) >= startTime) && poolOpen,
|
|
67
|
+
/** Estimated network hashrate from current target. */
|
|
68
|
+
networkHashrate: estimateNetworkHashrate(difficulty),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readPosciBalance(publicClient, address) {
|
|
73
|
+
return await publicClient.readContract({
|
|
74
|
+
address: POSCI_TOKEN, abi: TOKEN_ABI, functionName: 'balanceOf', args: [address],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function submitMine(walletClient, publicClient, nonce, digest) {
|
|
79
|
+
const hash = await walletClient.writeContract({
|
|
80
|
+
address: POSCI_MINING, abi: MINING_ABI, functionName: 'mine', args: [nonce, digest],
|
|
81
|
+
});
|
|
82
|
+
// Don't await receipt here — caller decides
|
|
83
|
+
return hash;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Heuristic network hashrate from current difficulty.
|
|
88
|
+
* E[hashes/find] = 2^256 / target = 2^22 * difficulty (since MAXIMUM_TARGET = 2^234)
|
|
89
|
+
* Hashrate at steady-state = E[hashes/find] / TARGET_INTERVAL
|
|
90
|
+
*/
|
|
91
|
+
export function estimateNetworkHashrate(difficulty, targetIntervalSec = 60) {
|
|
92
|
+
if (difficulty === 0n) return 0;
|
|
93
|
+
return Number(difficulty) * 2 ** 22 / targetIntervalSec;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const ABIs = { TOKEN_ABI, MINING_ABI };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// On-chain constants for the live POSCI deployment.
|
|
2
|
+
// These addresses match the verified contracts at scientistdapp.online.
|
|
3
|
+
|
|
4
|
+
export const POSCI_TOKEN = '0xD020e5E5c2724B2661C2FEF9AE878f49410a8B77';
|
|
5
|
+
export const POSCI_MINING = '0x9EAdD7dF7701e03d07c3727EC1ba816C2C9De936';
|
|
6
|
+
export const POSCI_GENESIS = '0x7bC1520Da49Cd56D5BE11aA77650cA998951459d';
|
|
7
|
+
|
|
8
|
+
// Public RPC fallbacks. Override with --rpc on the CLI or POSCI_RPC env var.
|
|
9
|
+
export const PUBLIC_RPCS = [
|
|
10
|
+
'https://ethereum.publicnode.com',
|
|
11
|
+
'https://eth.llamarpc.com',
|
|
12
|
+
'https://eth.merkle.io',
|
|
13
|
+
'https://1rpc.io/eth',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Difficulty / supply constants from the contract — copy here so we can
|
|
17
|
+
// compute schedule without an extra RPC call.
|
|
18
|
+
export const TOTAL_MINING_SUPPLY = 20_000_000n * 10n ** 18n;
|
|
19
|
+
export const INITIAL_REWARD = 1_000n * 10n ** 18n;
|
|
20
|
+
export const HALVING_INTERVAL = 10_000n;
|
|
21
|
+
export const MAX_HALVINGS = 64n;
|
|
22
|
+
export const MAXIMUM_TARGET = 1n << 234n;
|
|
23
|
+
export const TARGET_INTERVAL = 60n;
|
|
24
|
+
export const BLOCKS_PER_READJUST = 1024n;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Format hashes-per-second to a human string. */
|
|
2
|
+
export function formatHashrate(hps) {
|
|
3
|
+
if (!Number.isFinite(hps) || hps <= 0) return '0 H/s';
|
|
4
|
+
const units = ['H/s', 'kH/s', 'MH/s', 'GH/s', 'TH/s', 'PH/s'];
|
|
5
|
+
let i = 0;
|
|
6
|
+
while (hps >= 1000 && i < units.length - 1) { hps /= 1000; i++; }
|
|
7
|
+
return `${hps.toFixed(hps < 10 ? 2 : hps < 100 ? 1 : 0)} ${units[i]}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Format wei BigInt as POSCI (18 decimals) human string. */
|
|
11
|
+
export function formatPosci(wei, fractionDigits = 4) {
|
|
12
|
+
if (typeof wei !== 'bigint') wei = BigInt(wei ?? 0);
|
|
13
|
+
const neg = wei < 0n;
|
|
14
|
+
const v = neg ? -wei : wei;
|
|
15
|
+
const base = 10n ** 18n;
|
|
16
|
+
const whole = v / base;
|
|
17
|
+
const frac = v - whole * base;
|
|
18
|
+
const fracStr = frac.toString().padStart(18, '0').slice(0, fractionDigits).replace(/0+$/, '');
|
|
19
|
+
const wholeStr = whole.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
20
|
+
const out = fracStr ? `${wholeStr}.${fracStr}` : wholeStr;
|
|
21
|
+
return neg ? `-${out}` : out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** "0x1234…cdef" — short address. */
|
|
25
|
+
export function shortAddr(a, chars = 6) {
|
|
26
|
+
if (!a) return '';
|
|
27
|
+
if (a.length < 2 + chars * 2) return a;
|
|
28
|
+
return `${a.slice(0, 2 + chars)}…${a.slice(-chars)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** "3m 12s" / "1h 5m" / "2d 4h" relative duration. */
|
|
32
|
+
export function formatDuration(seconds) {
|
|
33
|
+
seconds = Math.max(0, Math.floor(seconds));
|
|
34
|
+
if (seconds < 60) return `${seconds}s`;
|
|
35
|
+
if (seconds < 3600) return `${Math.floor(seconds/60)}m ${seconds%60}s`;
|
|
36
|
+
if (seconds < 86400) return `${Math.floor(seconds/3600)}h ${Math.floor((seconds%3600)/60)}m`;
|
|
37
|
+
return `${Math.floor(seconds/86400)}d ${Math.floor((seconds%86400)/3600)}h`;
|
|
38
|
+
}
|
package/src/lib/log.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
|
|
3
|
+
export const c = kleur;
|
|
4
|
+
|
|
5
|
+
export const log = {
|
|
6
|
+
step: (s) => console.log(`\n${kleur.cyan().bold('▸')} ${kleur.bold(s)}`),
|
|
7
|
+
ok: (s) => console.log(` ${kleur.green('✓')} ${s}`),
|
|
8
|
+
info: (s) => console.log(` ${kleur.dim(s)}`),
|
|
9
|
+
warn: (s) => console.log(` ${kleur.yellow('⚠')} ${s}`),
|
|
10
|
+
err: (s) => console.error(` ${kleur.red('✗')} ${s}`),
|
|
11
|
+
banner: (s) => console.log(`\n${kleur.bold(s)}`),
|
|
12
|
+
table: (rows) => {
|
|
13
|
+
const widths = {};
|
|
14
|
+
for (const r of rows) for (const k in r) widths[k] = Math.max(widths[k] || k.length, String(r[k]).length);
|
|
15
|
+
const fmt = (r) => Object.entries(widths).map(([k, w]) => String(r[k] ?? '').padEnd(w)).join(' ');
|
|
16
|
+
console.log(' ' + kleur.dim(fmt(Object.fromEntries(Object.keys(widths).map(k => [k, k])))));
|
|
17
|
+
console.log(' ' + kleur.dim('─'.repeat(Object.values(widths).reduce((a,b) => a+b+2, -2))));
|
|
18
|
+
for (const r of rows) console.log(' ' + fmt(r));
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Local wallet store. Keys are stored encrypted under ~/.posci/wallets/<name>.json
|
|
2
|
+
// using AES-256-GCM with PBKDF2-derived key (matches Geth keystore v3 spirit).
|
|
3
|
+
//
|
|
4
|
+
// Two paths:
|
|
5
|
+
// - walletNew(name, password) → generate, return address
|
|
6
|
+
// - walletLoad(name, password) → decrypt + return privateKey + address
|
|
7
|
+
//
|
|
8
|
+
// You can also pass a private key directly via --key on the CLI; in that case
|
|
9
|
+
// nothing is read or written from the store.
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
+
import { resolve, join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import {
|
|
15
|
+
randomBytes, pbkdf2Sync, createCipheriv, createDecipheriv,
|
|
16
|
+
} from 'node:crypto';
|
|
17
|
+
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
|
|
18
|
+
|
|
19
|
+
const STORE_DIR = process.env.POSCI_HOME || join(homedir(), '.posci', 'wallets');
|
|
20
|
+
const KDF_ITERS = 250_000;
|
|
21
|
+
const KDF_KEYLEN = 32;
|
|
22
|
+
const KDF_DIGEST = 'sha256';
|
|
23
|
+
|
|
24
|
+
function ensureDir() { mkdirSync(STORE_DIR, { recursive: true }); }
|
|
25
|
+
function walletPath(name) { return resolve(STORE_DIR, `${name}.json`); }
|
|
26
|
+
|
|
27
|
+
export function walletDir() { return STORE_DIR; }
|
|
28
|
+
|
|
29
|
+
export function listWallets() {
|
|
30
|
+
if (!existsSync(STORE_DIR)) return [];
|
|
31
|
+
return readdirSync(STORE_DIR)
|
|
32
|
+
.filter(f => f.endsWith('.json'))
|
|
33
|
+
.map(f => {
|
|
34
|
+
const name = f.replace(/\.json$/, '');
|
|
35
|
+
const path = walletPath(name);
|
|
36
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
37
|
+
return { name, address: data.address, createdAt: data.createdAt, path };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function encrypt(privateKey, password) {
|
|
42
|
+
const salt = randomBytes(16);
|
|
43
|
+
const iv = randomBytes(12);
|
|
44
|
+
const key = pbkdf2Sync(password, salt, KDF_ITERS, KDF_KEYLEN, KDF_DIGEST);
|
|
45
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
46
|
+
const ciphertext = Buffer.concat([cipher.update(privateKey, 'utf8'), cipher.final()]);
|
|
47
|
+
const tag = cipher.getAuthTag();
|
|
48
|
+
return {
|
|
49
|
+
cipher: 'aes-256-gcm',
|
|
50
|
+
kdf: { name: 'pbkdf2', iters: KDF_ITERS, keylen: KDF_KEYLEN, digest: KDF_DIGEST,
|
|
51
|
+
salt: salt.toString('hex') },
|
|
52
|
+
iv: iv.toString('hex'),
|
|
53
|
+
tag: tag.toString('hex'),
|
|
54
|
+
ciphertext: ciphertext.toString('hex'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decrypt(payload, password) {
|
|
59
|
+
if (payload.cipher !== 'aes-256-gcm') throw new Error('unsupported cipher');
|
|
60
|
+
const salt = Buffer.from(payload.kdf.salt, 'hex');
|
|
61
|
+
const iv = Buffer.from(payload.iv, 'hex');
|
|
62
|
+
const tag = Buffer.from(payload.tag, 'hex');
|
|
63
|
+
const key = pbkdf2Sync(password, salt, payload.kdf.iters, payload.kdf.keylen, payload.kdf.digest);
|
|
64
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
65
|
+
decipher.setAuthTag(tag);
|
|
66
|
+
const plain = Buffer.concat([
|
|
67
|
+
decipher.update(Buffer.from(payload.ciphertext, 'hex')),
|
|
68
|
+
decipher.final(),
|
|
69
|
+
]);
|
|
70
|
+
return plain.toString('utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Generate a new wallet, store under `name`, return { name, address, privateKey }. */
|
|
74
|
+
export function walletNew(name, password) {
|
|
75
|
+
if (!name) throw new Error('wallet name required');
|
|
76
|
+
if (!password) throw new Error('password required (pass --password or use POSCI_PASSWORD env)');
|
|
77
|
+
ensureDir();
|
|
78
|
+
if (existsSync(walletPath(name))) throw new Error(`wallet '${name}' already exists at ${walletPath(name)}`);
|
|
79
|
+
|
|
80
|
+
const privateKey = generatePrivateKey();
|
|
81
|
+
const account = privateKeyToAccount(privateKey);
|
|
82
|
+
const data = {
|
|
83
|
+
name, address: account.address, createdAt: new Date().toISOString(),
|
|
84
|
+
keystore: encrypt(privateKey, password),
|
|
85
|
+
};
|
|
86
|
+
writeFileSync(walletPath(name), JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
87
|
+
return { name, address: account.address, privateKey, path: walletPath(name) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Decrypt and return { name, address, privateKey } for an existing wallet. */
|
|
91
|
+
export function walletLoad(name, password) {
|
|
92
|
+
if (!name) throw new Error('wallet name required');
|
|
93
|
+
if (!password) throw new Error('password required (pass --password or use POSCI_PASSWORD env)');
|
|
94
|
+
const path = walletPath(name);
|
|
95
|
+
if (!existsSync(path)) throw new Error(`wallet '${name}' not found at ${path}`);
|
|
96
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
97
|
+
const privateKey = decrypt(data.keystore, password);
|
|
98
|
+
return { name, address: data.address, privateKey, path };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Import an external private key into the local store under `name`. */
|
|
102
|
+
export function walletImport(name, privateKey, password) {
|
|
103
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
|
|
104
|
+
throw new Error('private key must be 0x-prefixed 64-char hex');
|
|
105
|
+
}
|
|
106
|
+
if (!password) throw new Error('password required');
|
|
107
|
+
ensureDir();
|
|
108
|
+
if (existsSync(walletPath(name))) throw new Error(`wallet '${name}' already exists`);
|
|
109
|
+
const account = privateKeyToAccount(privateKey);
|
|
110
|
+
const data = {
|
|
111
|
+
name, address: account.address, createdAt: new Date().toISOString(),
|
|
112
|
+
keystore: encrypt(privateKey, password),
|
|
113
|
+
};
|
|
114
|
+
writeFileSync(walletPath(name), JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
115
|
+
return { name, address: account.address, privateKey, path: walletPath(name) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve an account from CLI flags. Priority:
|
|
120
|
+
* 1. --key <0x...> direct private key (ephemeral, not stored)
|
|
121
|
+
* 2. --wallet <name> + --password <p> (decrypt local store)
|
|
122
|
+
* 3. POSCI_PRIVATE_KEY env var (ephemeral)
|
|
123
|
+
* 4. POSCI_WALLET + POSCI_PASSWORD env vars (load from store)
|
|
124
|
+
*
|
|
125
|
+
* Returns { address, privateKey, source }.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveAccount({ key, wallet, password }) {
|
|
128
|
+
if (key) {
|
|
129
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(key)) throw new Error('--key must be 0x-prefixed 64-char hex');
|
|
130
|
+
const a = privateKeyToAccount(key);
|
|
131
|
+
return { address: a.address, privateKey: key, source: 'flag-key' };
|
|
132
|
+
}
|
|
133
|
+
if (wallet) {
|
|
134
|
+
const pw = password || process.env.POSCI_PASSWORD;
|
|
135
|
+
if (!pw) throw new Error('--password required when using --wallet (or set POSCI_PASSWORD)');
|
|
136
|
+
const w = walletLoad(wallet, pw);
|
|
137
|
+
return { address: w.address, privateKey: w.privateKey, source: `wallet:${wallet}` };
|
|
138
|
+
}
|
|
139
|
+
if (process.env.POSCI_PRIVATE_KEY) {
|
|
140
|
+
const k = process.env.POSCI_PRIVATE_KEY;
|
|
141
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(k)) throw new Error('POSCI_PRIVATE_KEY env: bad format');
|
|
142
|
+
const a = privateKeyToAccount(k);
|
|
143
|
+
return { address: a.address, privateKey: k, source: 'env-key' };
|
|
144
|
+
}
|
|
145
|
+
if (process.env.POSCI_WALLET && process.env.POSCI_PASSWORD) {
|
|
146
|
+
const w = walletLoad(process.env.POSCI_WALLET, process.env.POSCI_PASSWORD);
|
|
147
|
+
return { address: w.address, privateKey: w.privateKey, source: `env-wallet:${process.env.POSCI_WALLET}` };
|
|
148
|
+
}
|
|
149
|
+
throw new Error('No wallet configured. See: posci-miner wallet --help');
|
|
150
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Node worker_threads CPU miner. One worker = one core, hashing in a tight
|
|
2
|
+
// loop. Stride-based partitioning so N workers cover the nonce space without
|
|
3
|
+
// overlap.
|
|
4
|
+
//
|
|
5
|
+
// Started by mining/manager.mjs via:
|
|
6
|
+
// new Worker(new URL('./cpu-worker.mjs', import.meta.url), { workerData: {...} })
|
|
7
|
+
|
|
8
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
9
|
+
import sha3 from 'js-sha3'; // js-sha3 is CommonJS — must use default import
|
|
10
|
+
const { keccak_256 } = sha3;
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
challengeHex, // 0x-prefixed 32 bytes
|
|
14
|
+
minerAddrHex, // 0x-prefixed 20 bytes (msg.sender)
|
|
15
|
+
targetHex, // 0x-prefixed uint256
|
|
16
|
+
startNonceHex, // 0x-prefixed uint256
|
|
17
|
+
strideHex, // total worker count, as bigint hex
|
|
18
|
+
} = workerData;
|
|
19
|
+
|
|
20
|
+
function hexToBytes(h) {
|
|
21
|
+
const s = h.startsWith('0x') ? h.slice(2) : h;
|
|
22
|
+
const out = new Uint8Array(s.length / 2);
|
|
23
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i*2, 2), 16);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function bytesToHex(b) {
|
|
27
|
+
let s = '0x';
|
|
28
|
+
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, '0');
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
function bytesToBigIntBE(b) {
|
|
32
|
+
let v = 0n;
|
|
33
|
+
for (let i = 0; i < b.length; i++) v = (v << 8n) | BigInt(b[i]);
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
function nonceToBytesBE32(n) {
|
|
37
|
+
const out = new Uint8Array(32);
|
|
38
|
+
let v = n;
|
|
39
|
+
for (let i = 31; i >= 0; i--) { out[i] = Number(v & 0xffn); v >>= 8n; }
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const challenge = hexToBytes(challengeHex);
|
|
44
|
+
const miner = hexToBytes(minerAddrHex);
|
|
45
|
+
const target = BigInt(targetHex);
|
|
46
|
+
const stride = BigInt(strideHex);
|
|
47
|
+
|
|
48
|
+
let nonce = BigInt(startNonceHex);
|
|
49
|
+
let hashes = 0;
|
|
50
|
+
let lastReport = Date.now();
|
|
51
|
+
const REPORT_MS = 750;
|
|
52
|
+
const BATCH = 4096;
|
|
53
|
+
|
|
54
|
+
// Reused 84-byte buffer: [challenge(32) | miner(20) | nonce(32)]
|
|
55
|
+
const buf = new Uint8Array(84);
|
|
56
|
+
buf.set(challenge, 0);
|
|
57
|
+
buf.set(miner, 32);
|
|
58
|
+
|
|
59
|
+
function tick() {
|
|
60
|
+
for (let i = 0; i < BATCH; i++) {
|
|
61
|
+
buf.set(nonceToBytesBE32(nonce), 52);
|
|
62
|
+
const digestBuf = keccak_256.arrayBuffer(buf);
|
|
63
|
+
const v = bytesToBigIntBE(new Uint8Array(digestBuf));
|
|
64
|
+
if (v <= target) {
|
|
65
|
+
parentPort.postMessage({
|
|
66
|
+
type: 'hit',
|
|
67
|
+
nonce: '0x' + nonce.toString(16),
|
|
68
|
+
digest: bytesToHex(new Uint8Array(digestBuf)),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
nonce += stride;
|
|
72
|
+
hashes++;
|
|
73
|
+
}
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (now - lastReport >= REPORT_MS) {
|
|
76
|
+
parentPort.postMessage({ type: 'stats', hashes });
|
|
77
|
+
hashes = 0;
|
|
78
|
+
lastReport = now;
|
|
79
|
+
}
|
|
80
|
+
// Yield so we can receive 'stop' / 'reset' messages from the manager.
|
|
81
|
+
setImmediate(tick);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let running = true;
|
|
85
|
+
|
|
86
|
+
parentPort.on('message', (msg) => {
|
|
87
|
+
if (msg.type === 'stop') {
|
|
88
|
+
running = false;
|
|
89
|
+
parentPort.close();
|
|
90
|
+
} else if (msg.type === 'reset') {
|
|
91
|
+
// New mining job: update challenge / target / nonce
|
|
92
|
+
const newChallenge = hexToBytes(msg.challengeHex);
|
|
93
|
+
buf.set(newChallenge, 0);
|
|
94
|
+
nonce = BigInt(msg.startNonceHex);
|
|
95
|
+
if (msg.targetHex) globalThis.__target = BigInt(msg.targetHex);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
tick();
|