Expose config options for cleartext stratum port
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
import { storeJson } from '../fileModels/store.json'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { curlJson, defaultStratumPort, uiPort } from '../utils'
|
||||
|
||||
const API_BASE = `http://127.0.0.1:${uiPort}`
|
||||
|
||||
// ── formatting helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function ts(): string {
|
||||
return new Date().toISOString().slice(11, 22)
|
||||
}
|
||||
|
||||
function fmtHR(hs: number): string {
|
||||
if (hs <= 0) return '0 H/s'
|
||||
if (hs >= 1e18) return `${(hs / 1e18).toFixed(2)} EH/s`
|
||||
if (hs >= 1e15) return `${(hs / 1e15).toFixed(2)} PH/s`
|
||||
if (hs >= 1e12) return `${(hs / 1e12).toFixed(2)} TH/s`
|
||||
if (hs >= 1e9) return `${(hs / 1e9).toFixed(2)} GH/s`
|
||||
if (hs >= 1e6) return `${(hs / 1e6).toFixed(2)} MH/s`
|
||||
if (hs >= 1e3) return `${(hs / 1e3).toFixed(2)} kH/s`
|
||||
return `${hs.toFixed(0)} H/s`
|
||||
}
|
||||
|
||||
function fmtDiff(d: number): string {
|
||||
if (d <= 0) return '0'
|
||||
if (d >= 1e12) return `${(d / 1e12).toFixed(2)}T`
|
||||
if (d >= 1e9) return `${(d / 1e9).toFixed(2)}G`
|
||||
if (d >= 1e6) return `${(d / 1e6).toFixed(2)}M`
|
||||
if (d >= 1e3) return `${(d / 1e3).toFixed(2)}K`
|
||||
return d.toFixed(2)
|
||||
}
|
||||
|
||||
function fmtUptime(s: number): string {
|
||||
if (s <= 0) return '0m'
|
||||
const d = Math.floor(s / 86400)
|
||||
const h = Math.floor((s % 86400) / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
if (d > 0) return `${d}d ${h}h ${m}m`
|
||||
if (h > 0) return `${h}h ${m}m`
|
||||
return `${m}m`
|
||||
}
|
||||
|
||||
function fmtBTC(btc: number): string {
|
||||
return `${btc.toFixed(8)} BTC`
|
||||
}
|
||||
function pad(s: string, w: number): string {
|
||||
return s.padEnd(w)
|
||||
}
|
||||
|
||||
const chainNames: Record<string, string> = { main: 'mainnet' }
|
||||
function displayChain(c: string): string {
|
||||
return chainNames[c] ?? c
|
||||
}
|
||||
|
||||
// ── snapshot shape ────────────────────────────────────────────────────────────
|
||||
|
||||
interface PoolStats {
|
||||
workers: number
|
||||
users: number
|
||||
accepted: number
|
||||
rejected: number
|
||||
shares: number
|
||||
dsps1: number
|
||||
dsps5: number
|
||||
dsps60: number
|
||||
dsps1440: number
|
||||
}
|
||||
interface Chain {
|
||||
chain: string
|
||||
blocks: number
|
||||
headers: number
|
||||
difficulty: number
|
||||
initialblockdownload: boolean
|
||||
verificationprogress: number
|
||||
bestblockhash: string
|
||||
}
|
||||
interface Worker {
|
||||
worker: string
|
||||
dsps1: number
|
||||
bestdiff: number
|
||||
bestever: number
|
||||
idle: boolean
|
||||
}
|
||||
interface Client {
|
||||
id: number
|
||||
workername: string
|
||||
diff: number
|
||||
dsps1: number
|
||||
useragent: string
|
||||
}
|
||||
interface BlockRecord {
|
||||
height: number
|
||||
hash?: string
|
||||
reward_btc?: number
|
||||
found_at: string
|
||||
orphaned_at?: string
|
||||
chain?: string
|
||||
}
|
||||
interface Snapshot {
|
||||
pool: PoolStats | null
|
||||
uptime_seconds: number
|
||||
hashrate_hs_1m: number
|
||||
hashrate_hs_5m: number
|
||||
hashrate_hs_1h: number
|
||||
hashrate_hs_24h: number
|
||||
best_diff: number
|
||||
cumulative_shares: number
|
||||
next_block_reward_btc: number
|
||||
next_difficulty_percent: number
|
||||
chain: Chain | null
|
||||
network_hashrate_hs: number
|
||||
recent_blocks: BlockRecord[]
|
||||
ckpool_ok: boolean
|
||||
bitcoin_ok: boolean
|
||||
last_error?: string
|
||||
block_submit_attempts: number
|
||||
block_submits_confirmed: number
|
||||
zmq_enabled: boolean
|
||||
zmq_stale: boolean
|
||||
has_last_zmq_event: boolean
|
||||
last_zmq_event_age: number
|
||||
workers: Worker[]
|
||||
clients: Client[]
|
||||
}
|
||||
interface DebugBlocks {
|
||||
memory: {
|
||||
height: number
|
||||
hash: string
|
||||
chain: string
|
||||
orphaned_at: string
|
||||
found_at: string
|
||||
}[]
|
||||
db: {
|
||||
height: number
|
||||
hash: string
|
||||
chain: string
|
||||
orphaned_at: string
|
||||
found_at: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const poolStatus = sdk.Action.withoutInput(
|
||||
// id
|
||||
'pool-status',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Pool Status'),
|
||||
description: i18n(
|
||||
'Displays a full status snapshot: Bitcoin Core sync state, ckpool health, connected miners, hashrate, found blocks, and submit-gap diagnostics.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'only-running',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// the execution function
|
||||
async ({ effects }) => {
|
||||
const stratumPort =
|
||||
(await storeJson.read((s) => s.stratumPort).once()) ?? defaultStratumPort
|
||||
|
||||
// Fetch from inside the service's network namespace: temp subcontainers
|
||||
// share it, so curl reaches kamado-api on 127.0.0.1.
|
||||
const { snap, dbg } = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
null,
|
||||
'pool-status',
|
||||
async (sub) => ({
|
||||
snap: await curlJson<Snapshot>(sub, `${API_BASE}/api/snapshot`),
|
||||
dbg: await curlJson<DebugBlocks>(
|
||||
sub,
|
||||
`${API_BASE}/api/admin/debug-blocks`,
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
const lines: string[] = []
|
||||
const log = (s: string) => lines.push(`[${ts()}] ${s}`)
|
||||
const sep = () => log('──────────────────────────────────────────')
|
||||
let allPass = true
|
||||
|
||||
log('Kamado Pool — Status')
|
||||
sep()
|
||||
|
||||
// ── 1. Full snapshot ────────────────────────────────────────────────────
|
||||
log('● Pool & Bitcoin Core status')
|
||||
if (!snap) {
|
||||
log(' API unreachable — service may still be starting.')
|
||||
allPass = false
|
||||
}
|
||||
|
||||
if (snap) {
|
||||
const chain = snap.chain
|
||||
const pool = snap.pool
|
||||
|
||||
log(` Bitcoin Core: ${snap.bitcoin_ok ? 'OK' : 'FAIL'}`)
|
||||
if (chain) {
|
||||
const ibd = chain.initialblockdownload
|
||||
const syncPct = (chain.verificationprogress * 100).toFixed(2)
|
||||
log(
|
||||
` Network: ${chain.chain} height=${chain.blocks} headers=${chain.headers}`,
|
||||
)
|
||||
log(
|
||||
` Sync: ${
|
||||
ibd
|
||||
? `IBD — ${syncPct}% (pool will not mine until synced)`
|
||||
: `${syncPct}% — fully synced`
|
||||
}`,
|
||||
)
|
||||
log(` Block hash: ${chain.bestblockhash}`)
|
||||
log(` Difficulty: ${fmtDiff(chain.difficulty)}`)
|
||||
log(` Network hashrate: ${fmtHR(snap.network_hashrate_hs)}`)
|
||||
if (ibd) allPass = false
|
||||
}
|
||||
if (!snap.bitcoin_ok) allPass = false
|
||||
|
||||
if (snap.next_block_reward_btc > 0)
|
||||
log(` Next block reward: ${fmtBTC(snap.next_block_reward_btc)}`)
|
||||
if (snap.next_difficulty_percent !== 0) {
|
||||
const sign = snap.next_difficulty_percent > 0 ? '+' : ''
|
||||
log(
|
||||
` Next diff adjust: ${sign}${snap.next_difficulty_percent.toFixed(1)}%`,
|
||||
)
|
||||
}
|
||||
|
||||
if (snap.zmq_enabled) {
|
||||
const age = snap.has_last_zmq_event
|
||||
? `last event ${snap.last_zmq_event_age.toFixed(0)}s ago`
|
||||
: 'no event yet since startup'
|
||||
log(
|
||||
` ZMQ: ${snap.zmq_stale ? 'STALE — ' : 'OK — '}${age}`,
|
||||
)
|
||||
if (snap.zmq_stale) allPass = false
|
||||
} else {
|
||||
log(` ZMQ: disabled`)
|
||||
}
|
||||
|
||||
log('')
|
||||
log(` ckpool process: ${snap.ckpool_ok ? 'OK' : 'FAIL'}`)
|
||||
if (!snap.ckpool_ok) allPass = false
|
||||
if (snap.last_error) log(` last_error: ${snap.last_error}`)
|
||||
if (pool) {
|
||||
log(` Uptime: ${fmtUptime(snap.uptime_seconds)}`)
|
||||
log(
|
||||
` Workers online: ${pool.workers} (${pool.users} user${
|
||||
pool.users !== 1 ? 's' : ''
|
||||
})`,
|
||||
)
|
||||
log(` Shares accepted: ${pool.accepted} rejected: ${pool.rejected}`)
|
||||
log(` Hashrate 1m: ${fmtHR(snap.hashrate_hs_1m)}`)
|
||||
log(` Hashrate 5m: ${fmtHR(snap.hashrate_hs_5m)}`)
|
||||
log(` Hashrate 1h: ${fmtHR(snap.hashrate_hs_1h)}`)
|
||||
log(` Hashrate 24h: ${fmtHR(snap.hashrate_hs_24h)}`)
|
||||
log(` Best share ever: diff ${fmtDiff(snap.best_diff)}`)
|
||||
log(
|
||||
` Cumul. work: ${fmtDiff(snap.cumulative_shares)} diff-1 shares`,
|
||||
)
|
||||
}
|
||||
|
||||
const gap = snap.block_submit_attempts - snap.block_submits_confirmed
|
||||
log(
|
||||
` Submit attempts: ${snap.block_submit_attempts} confirmed: ${snap.block_submits_confirmed}`,
|
||||
)
|
||||
log(
|
||||
` Submit gap: ${gap > 0 ? `WARN — ${gap} unconfirmed` : 'OK (all confirmed)'}`,
|
||||
)
|
||||
if (gap > 0) allPass = false
|
||||
|
||||
if (snap.clients && snap.clients.length > 0) {
|
||||
log('')
|
||||
log(`● Connected miners (${snap.clients.length})`)
|
||||
for (const c of snap.clients) {
|
||||
const tag = c.useragent ? ` [${c.useragent}]` : ''
|
||||
log(
|
||||
` ${pad(c.workername || `client#${c.id}`, 30)} diff=${fmtDiff(
|
||||
c.diff,
|
||||
)} ${fmtHR(c.dsps1 * 4294967296)}${tag}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
log('')
|
||||
log('● Connected miners: none')
|
||||
}
|
||||
|
||||
if (snap.workers && snap.workers.length > 0) {
|
||||
log('')
|
||||
log('● Workers — best shares')
|
||||
for (const w of snap.workers) {
|
||||
const ever = w.bestever > 0 ? w.bestever : w.bestdiff
|
||||
log(
|
||||
` ${pad(w.worker, 30)} best=${fmtDiff(ever)}${w.idle ? ' (idle)' : ''}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
log('')
|
||||
if (snap.recent_blocks && snap.recent_blocks.length > 0) {
|
||||
const currentNetwork = chain?.chain ?? ''
|
||||
log(`● Blocks found (${snap.recent_blocks.length})`)
|
||||
for (const b of snap.recent_blocks) {
|
||||
let status = ''
|
||||
if (b.orphaned_at) {
|
||||
status = ' ORPHANED'
|
||||
} else if (b.chain && currentNetwork && b.chain !== currentNetwork) {
|
||||
status = ` [${displayChain(b.chain)}]`
|
||||
}
|
||||
const reward = b.reward_btc ? ` ${fmtBTC(b.reward_btc)}` : ''
|
||||
const hash = b.hash ? ` ${b.hash.slice(0, 16)}…` : ''
|
||||
log(
|
||||
` height=${b.height} ${b.found_at.slice(0, 19)}${reward}${hash}${status}`,
|
||||
)
|
||||
log(
|
||||
` chain="${b.chain ?? ''}" orphaned_at="${b.orphaned_at ?? ''}" hash="${b.hash ?? ''}"`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
log('● Blocks found: none yet')
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1b. Debug: memory vs DB comparison ─────────────────────────────────
|
||||
if (dbg) {
|
||||
log('')
|
||||
log('● Debug: memory vs DB')
|
||||
log(
|
||||
` Memory blocks: ${dbg.memory?.length ?? 0} DB blocks: ${dbg.db?.length ?? 0}`,
|
||||
)
|
||||
if (dbg.memory && dbg.memory.length > 0) {
|
||||
for (const m of dbg.memory) {
|
||||
const dbRow = dbg.db?.find((d) => d.height === m.height)
|
||||
const memOrph = m.orphaned_at || 'none'
|
||||
const dbOrph = dbRow?.orphaned_at || 'none'
|
||||
const match = memOrph === dbOrph ? '✓' : 'MISMATCH'
|
||||
log(
|
||||
` h=${m.height} mem_orphan="${memOrph}" db_orphan="${dbOrph}" ${match}`,
|
||||
)
|
||||
if (memOrph !== dbOrph) {
|
||||
log(
|
||||
` mem: chain="${m.chain}" hash="${(m.hash || '').slice(0, 20)}…"`,
|
||||
)
|
||||
log(
|
||||
` db: chain="${dbRow?.chain ?? '?'}" hash="${(dbRow?.hash || '').slice(0, 20)}…"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log(' (debug endpoint unavailable)')
|
||||
}
|
||||
|
||||
// ── 2. Stratum ───────────────────────────────────────────────────────────
|
||||
log('')
|
||||
sep()
|
||||
log(`● Stratum (port ${stratumPort})`)
|
||||
if (snap?.ckpool_ok) {
|
||||
log(
|
||||
' ckpool process: healthy — stratum port is served by the same process',
|
||||
)
|
||||
} else {
|
||||
log(
|
||||
' FAIL — ckpool is not running, stratum port will not accept connections',
|
||||
)
|
||||
}
|
||||
|
||||
// ── 3. Overall ──────────────────────────────────────────────────────────
|
||||
log('')
|
||||
sep()
|
||||
const pass = allPass && (snap?.ckpool_ok ?? false)
|
||||
const summary = pass
|
||||
? i18n('PASS — pool is healthy')
|
||||
: i18n('FAIL — see details above')
|
||||
log(`Overall: ${summary}`)
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Pool Status'),
|
||||
message: summary,
|
||||
result: {
|
||||
type: 'single',
|
||||
value: lines.join('\n'),
|
||||
copyable: true,
|
||||
qr: false,
|
||||
masked: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user