Compare commits
13
Commits
c6f0ba5f4d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a35b02c7e
|
||
|
|
2c4c6609ba
|
||
|
|
af8357dc63 | ||
|
|
85f61e5c53 | ||
|
|
71cf277796 | ||
|
|
0d76287cf1 | ||
|
|
916701a76f | ||
|
|
83d70b2636 | ||
|
|
8242eec265 | ||
|
|
a19e238b8e | ||
|
|
3f700f8982 | ||
|
|
e341fd7b26 | ||
|
|
daa87e2352 |
@@ -1,82 +1,247 @@
|
|||||||
# Kamado Pool
|
# Kamado Pool
|
||||||
|
|
||||||
A modern, feature-complete solo Bitcoin mining pool built on a patched fork of CKPool, with a real-time web dashboard and a Go-based API middleware that surfaces everything CKPool knows.
|
A solo Bitcoin mining pool built on a patched fork of [CKPool](https://bitbucket.org/ckolivas/ckpool), with a Go middleware API, real-time Svelte dashboard, and full StartOS integration.
|
||||||
|
|
||||||
## Why Kamado?
|
Kamado exists because existing CKPool wrappers like Bassin read only a handful of periodic stats files and miss most of CKPool's rich runtime data. Kamado talks directly to CKPool's Unix socket API, subscribes to bitcoind via both RPC and ZMQ, tails CKPool's log for block-solve events, and merges everything into a single live snapshot that the dashboard consumes over WebSocket.
|
||||||
|
|
||||||
Existing CKPool-based solutions (like Bassin for Umbrel) read only a handful of periodic stats files and miss most of CKPool's rich data. Kamado talks directly to CKPool's Unix socket API to expose:
|
|
||||||
|
|
||||||
- Real-time per-client data: hashrate, difficulty, user agent, hardware detection
|
|
||||||
- Full block-found history with height, hash, reward, and solving worker
|
|
||||||
- Per-worker and per-client best share tracking (current + all-time)
|
|
||||||
- Network difficulty, pool efficiency, expected time to block
|
|
||||||
- Live dashboard updates via WebSocket (no 60-second file polls)
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────────────────────────────┐
|
┌──────────────────────────────┐
|
||||||
│ ckpool-solo (C) ──Unix socket──► kamado-api (Go) │
|
Miners ──stratum:3333──► ckpool-solo (C, patched) │
|
||||||
│ ports: 3333 ports: 80 │
|
│ │ Unix socket │ log │
|
||||||
│ │
|
│ ▼ ▼ │
|
||||||
│ ▲ stratum ▲ HTTP/WS │
|
│ kamado-api (Go) │
|
||||||
│ │ │ │
|
│ │ RPC+ZMQ ▲ HTTP/WS │
|
||||||
│ Miners Browser │
|
│ ▼ │ │
|
||||||
│ │
|
│ bitcoind Browser :8080 │
|
||||||
│ bitcoind ◄──── RPC + ZMQ ────── ckpool + kamado-api│
|
└──────────────────────────────┘
|
||||||
└────────────────────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Three main components:
|
| Component | Language | Role |
|
||||||
|
|-----------|----------|------|
|
||||||
|
| `ckpool/` | C | Stratum server, share validation, vardiff, block assembly and submission |
|
||||||
|
| `api/` | Go | Socket client, bitcoind RPC, ZMQ subscriber, log tailer, state aggregator, REST + WebSocket API, SQLite persistence |
|
||||||
|
| `ui/` | Svelte 5 | Real-time dashboard with pool overview, miner stats, block history, best share tracking, transaction accelerator |
|
||||||
|
|
||||||
| Component | Language | Purpose |
|
The Go binary embeds the built Svelte app via `//go:embed` and serves it at `/` — a single static binary with no external web server.
|
||||||
| -------------- | -------- | -------------------------------------------------- |
|
|
||||||
| `ckpool/` | C | Stratum server, share validation, block submission |
|
|
||||||
| `api/` | Go | Socket client, REST/WebSocket API, persistence |
|
|
||||||
| `ui/` | Svelte | Real-time dashboard |
|
|
||||||
|
|
||||||
## Phases
|
## Improvements Over Upstream CKPool
|
||||||
|
|
||||||
- [x] **Phase 1** — Fork & fix CKPool, build infrastructure
|
CKPool is a high-performance stratum server, but it has no web interface and limited observability. Kamado adds a complete operational layer on top:
|
||||||
- [~] **Phase 2** — Go API middleware
|
|
||||||
- [x] Phase 2a: CKPool socket client, bitcoind RPC, state aggregator, REST API
|
|
||||||
- [x] Phase 2b: CKPool log tailer, block history, stdlib WebSocket push
|
|
||||||
- [ ] Phase 2b.5: ZMQ block notifier, SQLite persistence (deferred until s9pk repo exists — need real Go build env for new deps)
|
|
||||||
- [x] ckpool patch 0001: expose `bestever` in runtime socket JSON so the UI can show "this round" and "all-time" best share side by side
|
|
||||||
- [~] **Phase 3** — Svelte UI dashboard (skeleton: header, pool overview, miners table, blocks, best shares leaderboard; live WS updates)
|
|
||||||
- [x] **Phase 4** — Monorepo Docker build: `kamado-api` embeds `ui/dist` via `//go:embed` and serves it at `/`. The api Dockerfile has a node stage that builds the UI before the Go stage embeds and builds the binary; docker-compose uses the repo root as build context so both `api/` and `ui/` are visible.
|
|
||||||
- [ ] **Phase 5** — Testing (regtest, testnet4), polish
|
|
||||||
|
|
||||||
## Quick start (dev)
|
### Patches Applied to CKPool
|
||||||
|
|
||||||
```sh
|
Seven patches are applied to upstream commit `cfb0f83` (which itself includes the workbase_id fix, extended ESP32/NerdMiner timeouts, configurable `dropidle`, and vardiff improvements):
|
||||||
cp .env.example .env # set POOL_BTCADDRESS and bitcoind creds
|
|
||||||
make up # build + start ckpool + api
|
| Patch | Purpose |
|
||||||
curl localhost:8080/api/health
|
|-------|---------|
|
||||||
curl localhost:8080/api/pool
|
| `0001` | Expose `bestever` (all-time best share) in runtime socket JSON alongside `bestdiff` (current round) |
|
||||||
|
| `0002` | Always reply on the listener socket so kamado-api gets responses in `btcsolo` mode |
|
||||||
|
| `0003` | Return share errors as proper Stratum `[code, msg, null]` arrays per the Slush pool protocol spec |
|
||||||
|
| `0004` | Expose per-worker share counts (accepted/rejected) in runtime socket JSON |
|
||||||
|
| `0005` | Log ZMQ-to-notify latency for block-update performance monitoring |
|
||||||
|
| `0006` | Expose raw reject count in pool stats |
|
||||||
|
| `0007` | Replace hardcoded "ckpool" branding in the coinbase scriptSig with "kamado" (same 6 bytes, consensus-safe) |
|
||||||
|
|
||||||
|
### Middleware API
|
||||||
|
|
||||||
|
The Go API (`kamado-api`) bridges CKPool's Unix socket protocol, bitcoind's JSON-RPC, and ZMQ into a unified HTTP/WebSocket interface:
|
||||||
|
|
||||||
|
- **Socket client** — CKPool uses a 4-byte length-prefixed binary protocol on a Unix domain socket. kamado-api opens a fresh connection per request and queries pool, user, worker, and client state in real time.
|
||||||
|
- **State aggregator** — Merges socket responses, bitcoind chain info, ZMQ events, and log-tailed block solves into a single thread-safe snapshot, refreshed on a configurable interval.
|
||||||
|
- **ZMQ subscriber** — Listens to bitcoind's `hashblock` topic for sub-second block notifications, triggering an immediate state refresh. Falls back to RPC polling if ZMQ is unavailable.
|
||||||
|
- **Log tailer** — Watches CKPool's log file (inotify-based) for "Solved and confirmed block" lines, extracts height and hash, enriches via bitcoind RPC (reward, confirmations), and records to SQLite.
|
||||||
|
- **Block reconciliation** — On startup, cross-references in-memory blocks with the database and bitcoind to detect orphans and fill gaps.
|
||||||
|
- **Chain reorg detection** — Scoped to the active chain; clears stale state on tip changes.
|
||||||
|
- **SQLite persistence** — Blocks, accelerated transactions, and log cursor survive restarts. Non-fatal if unavailable (in-memory ring fallback).
|
||||||
|
- **Transaction accelerator** — Inspects the current block template to find the marginal (lowest fee-rate) transaction, calls `prioritisetransaction` to boost a target tx, and reports the displaced fee as revenue impact. Includes a hard cap (2000 sat/vB) and lifecycle cleanup.
|
||||||
|
|
||||||
|
### Stratum TLS
|
||||||
|
|
||||||
|
Optional encrypted stratum via stunnel. CKPool binds a public plaintext socket plus one loopback-only socket per TLS certificate; stunnel terminates TLS on the external port and forwards decrypted traffic to the matching internal bind. CKPool tags each connection with its `serverurl` index, and the deployment declares what those indices mean via the `STRATUM_SERVERS` env — so the dashboard shows a lock icon next to encrypted miners and names the certificate in use on hover, with no source-IP heuristics.
|
||||||
|
|
||||||
|
Under StartOS this drives two certificates on one port, chosen per connection by SNI: a CA-issued (Let's Encrypt) certificate for miners connecting over a clearnet domain, and the self-signed one for miners on the LAN, which send no SNI and fall through to it.
|
||||||
|
|
||||||
|
The self-signed certificate is auto-generated on first start with broad SAN coverage (`.local`, `.embassy`, `.onion`, `.lan`, `.home.arpa`, `.internal`) so miner firmware that validates the SAN against the connection hostname (e.g. AxeOS with mbedtls) works without manual cert pinning. A version marker triggers automatic regeneration when the cert format changes.
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
The Svelte 5 dashboard connects via WebSocket for real-time push updates (no polling in normal operation) and includes:
|
||||||
|
|
||||||
|
- **Pool overview** — Hashrate (1m/5m/1h/24h), uptime, workers online, shares accepted/rejected, expected time to block, round effort
|
||||||
|
- **Hashrate chart** — Interactive multi-window visualization
|
||||||
|
- **Miners table** — Per-user and per-worker stats: hashrate, difficulty, latency, best shares, idle detection
|
||||||
|
- **Hardware detection** — Parses stratum user-agent strings to identify miner hardware (Bitaxe, Bitaxe Hex, NerdMiner, NerdAxe, NerdQAxe, NerdOCTAXE, NerdEKO, NerdNOS, PiAxe, QAxe, 0xAxe, LeafMiner, and more). Open-source hardware is flagged with a star badge so operators can see their fleet composition at a glance.
|
||||||
|
- **TLS badges** — Miners connected via the encrypted stratum port display a lock icon in the miners table, derived from CKPool's `server` field (no IP heuristics).
|
||||||
|
- **User/worker detail pages** — Deep dive into individual miner stats, cumulative work, and luck
|
||||||
|
- **Block history** — Found blocks with height, hash, reward, solving worker, chain name, orphan status
|
||||||
|
- **Best share leaderboard** — "This round" and "all-time" tracking with per-worker breakdown and glowing difficulty range indicators
|
||||||
|
- **Transaction accelerator** — Boost transactions via `prioritisetransaction` with marginal fee displacement analysis showing the revenue impact of each boost
|
||||||
|
- **Block-found animation** — Celebratory toast on solve events
|
||||||
|
- **Health banners** — Live status indicators for CKPool, bitcoind, ZMQ, and submit-gap diagnostics
|
||||||
|
- **Block-update latency** — Tracks and displays average, last, and wasted-work latency for block notifications so operators can tune ZMQ and polling
|
||||||
|
- **Shares bar** — Accepted/rejected/stale share summary with ratio visualization
|
||||||
|
- **Custom mempool explorer** — Transaction and block links can point at a self-hosted mempool instance instead of the public mempool.space
|
||||||
|
- **Mobile responsive** — Full breakpoint coverage for phone and tablet
|
||||||
|
- **Donation footer** — BTC address with hover QR code
|
||||||
|
|
||||||
|
### Reliability
|
||||||
|
|
||||||
|
- **Dual block notification** — ZMQ hashblock for instant detection + RPC polling as fallback. Both run simultaneously; every second of stale work in solo mode is hashrate burned on a dead block.
|
||||||
|
- **ckpool kill-on-failure** — When bitcoind becomes unreachable, kamado-api kills ckpool so miners can failover to another pool. ckpool restarts automatically when bitcoind recovers.
|
||||||
|
- **Deferred RPC** — Dashboard RPCs wait until the ckpool notifier completes on tip change to prioritize system resources for block reconstruction and validation.
|
||||||
|
- **Log cursor persistence** — The tailer's file offset is stored in SQLite so block-solve detection resumes correctly after restart.
|
||||||
|
- **Warm-up resilience** — The ckpool socket client handles transient EOFs during startup gracefully instead of crashing.
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `GET` | `/api/health` | Pool + bitcoind health, submit-gap tracking, ZMQ staleness |
|
||||||
|
| `GET` | `/api/pool` | Pool stats, hashrate windows, chain info, network hashrate |
|
||||||
|
| `GET` | `/api/users` | All users: shares, best diff, idle status |
|
||||||
|
| `GET` | `/api/workers` | All workers: hashrate, best share, share counts |
|
||||||
|
| `GET` | `/api/clients` | Active stratum sessions: useragent, IP, assigned difficulty |
|
||||||
|
| `GET` | `/api/blocks` | Recent solved blocks: height, hash, reward, orphan status |
|
||||||
|
| `GET` | `/api/snapshot` | Full merged snapshot (everything above combined) |
|
||||||
|
| `GET` | `/api/ws` | WebSocket: push on every state refresh + immediate on block solve |
|
||||||
|
| `GET` | `/api/admin/debug-blocks` | In-memory vs. DB block discrepancy troubleshooting |
|
||||||
|
| `POST` | `/api/admin/reset-latency` | Zero all block-update latency counters |
|
||||||
|
| `POST` | `/api/admin/ack-best` | Acknowledge new best share (UI state marker) |
|
||||||
|
| `POST` | `/api/admin/reset-ack-best` | Reset best share acknowledgment |
|
||||||
|
| `POST` | `/api/accelerate` | Boost a transaction via `prioritisetransaction` |
|
||||||
|
| `POST` | `/api/accelerate/cancel` | Cancel a previously boosted transaction |
|
||||||
|
| `POST` | `/api/accelerate/max` | Boost a tx with a new feerate 2x the mempool highest (capped at 2000 sat/vB) |
|
||||||
|
| `GET` | `/api/accelerate/list` | List all currently boosted transactions |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.22+
|
||||||
|
- Node.js 22+
|
||||||
|
- Docker and Docker Compose
|
||||||
|
- bitcoind (for regtest testing)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # set bitcoind RPC credentials
|
||||||
|
make up # build images + start ckpool + api
|
||||||
```
|
```
|
||||||
|
|
||||||
REST endpoints:
|
The dashboard is at `http://localhost:8080`. Point a miner at `stratum+tcp://localhost:3333` with a valid Bitcoin address as the username.
|
||||||
|
|
||||||
| Route | Returns |
|
### Make Targets
|
||||||
| -------------------- | ----------------------------------------------------------- |
|
|
||||||
| `GET /api/health` | CKPool + bitcoind health |
|
|
||||||
| `GET /api/pool` | Pool stats + derived hashrate windows + chain info |
|
|
||||||
| `GET /api/users` | All users from `users` socket command |
|
|
||||||
| `GET /api/workers` | All workers from `workers` socket command |
|
|
||||||
| `GET /api/clients` | All connected stratum sessions (useragent, IP, diff) |
|
|
||||||
| `GET /api/blocks` | Recent solved blocks (in-memory ring, SQLite in Phase 2b.5) |
|
|
||||||
| `GET /api/snapshot` | Full merged snapshot (everything) |
|
|
||||||
| `GET /api/ws` | WebSocket push: full snapshot on every refresh + on solve |
|
|
||||||
|
|
||||||
StartOS packaging lives in a separate repository.
|
| Target | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `make up` | Build and start all services |
|
||||||
|
| `make down` | Stop all services |
|
||||||
|
| `make logs` | Tail ckpool + api logs |
|
||||||
|
| `make api` | Build the `kamado/api:dev` image |
|
||||||
|
| `make ckpool` | Build the `kamado/ckpool:dev` image |
|
||||||
|
| `make api-test` | Run Go tests with race detector |
|
||||||
|
| `make ui` | Build Svelte dashboard to `ui/dist` |
|
||||||
|
| `make ui-dev` | Start Vite dev server (`:5173`, proxies `/api` to `:8080`) |
|
||||||
|
| `make ui-check` | Run svelte-check type diagnostics |
|
||||||
|
| `make clean` | Remove data, build artifacts, and volumes |
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Required | Description |
|
||||||
|
|----------|---------|----------|-------------|
|
||||||
|
| `BITCOIN_RPC_URL` | — | yes | bitcoind RPC endpoint (e.g. `http://127.0.0.1:8332`) |
|
||||||
|
| `BITCOIN_RPC_USER` | — | yes | RPC username |
|
||||||
|
| `BITCOIN_RPC_PASSWORD` | — | yes | RPC password |
|
||||||
|
| `LISTEN_ADDR` | `:8080` | no | HTTP bind address |
|
||||||
|
| `CKPOOL_SOCKDIR` | `/run/ckpool` | no | CKPool Unix socket directory |
|
||||||
|
| `CKPOOL_LOGFILE` | `/var/log/ckpool/ckpool.log` | no | CKPool log path for block-solve detection |
|
||||||
|
| `DB_PATH` | `/var/lib/kamado/kamado.db` | no | SQLite database path |
|
||||||
|
| `POLL_INTERVAL` | `5s` | no | State refresh interval |
|
||||||
|
| `BITCOIN_ZMQ_BLOCK` | (disabled) | no | ZMQ hashblock endpoint (e.g. `tcp://127.0.0.1:28332`) |
|
||||||
|
| `BITCOIN_RPC_TIMEOUT` | `10s` | no | RPC call timeout |
|
||||||
|
| `MEMPOOL_BASE_URL` | (mempool.space) | no | Custom mempool explorer URL |
|
||||||
|
| `STRATUM_SERVERS` | (undeclared) | no | JSON array describing ckpool's `serverurl[]` binds, e.g. `[{"kind":"plain","label":"Plaintext"},{"kind":"tls-local","label":"TLS — self-signed"}]`. Entry N labels clients with `server == N` in the dashboard. Unset falls back to treating index 1 as TLS |
|
||||||
|
|
||||||
|
### UI Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make ui-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Starts Vite on `:5173` with hot reload. API calls are proxied to `:8080` — run `make up` first so the backend is available.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make api-test # Go unit tests (race detector enabled)
|
||||||
|
cd api && go test -race ./... # same thing without Make
|
||||||
|
./test/regtest_smoke.sh # Full regtest integration test
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Go Unit Tests
|
||||||
|
|
||||||
|
The test suite covers the critical mining path with 38 tests across 5 packages:
|
||||||
|
|
||||||
|
| Package | Tests | Coverage |
|
||||||
|
|---------|-------|----------|
|
||||||
|
| `bitcoind` | RPC retry logic (transient 503s, warmup errors, semantic errors, exhausted retries), coinbase reward extraction | Ensures the RPC client retries on transport failures and bitcoind warmup but fails fast on semantic errors |
|
||||||
|
| `ckpool` | Socket client ping, pool stats parsing, client listing, dial error handling | Validates the 4-byte length-prefixed binary protocol against a mock Unix socket |
|
||||||
|
| `logmon` | Line parsing (solved blocks, submitting variants, diff reset, unrelated lines), tailer run (basic read, cursor resume after restart, log rotation) | Covers the full log tailer lifecycle including inotify-based file watching |
|
||||||
|
| `state` | Block reconciliation (chain-scoped filtering, genuine reorgs, RPC errors, legacy blocks, cross-network), block ingestion (new blocks, dedup, chain stamping) | Ensures reorg detection never false-orphans blocks from other chains |
|
||||||
|
| `store` | SQLite roundtrips (insert, dedup, orphan marking, enrichment updates, enrichment queries, chain column migration, KV store) | Validates schema migrations and all persistence operations |
|
||||||
|
|
||||||
|
#### Regtest Smoke Test
|
||||||
|
|
||||||
|
The smoke test runs two phases on regtest:
|
||||||
|
|
||||||
|
1. **Log-inject path** — Injects a synthetic block-solve line into ckpool's log, verifies the tailer detects it, enriches it via bitcoind RPC, and surfaces it at `/api/blocks`. Quick, no ckpool binary needed.
|
||||||
|
2. **Full stratum path** — Starts ckpool, connects a Python stratum miner, mines a real block, verifies the coinbase contains the "kamado" tag and the configured pool identifier, validates that bitcoind accepted the block with confirmations > 0, and checks that the block validation log shows correct chain acceptance.
|
||||||
|
|
||||||
|
The smoke test auto-builds ckpool from the pinned upstream source with patches applied if not cached (build cached in `~/.cache/kamado-dev/`).
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
api/
|
||||||
|
cmd/kamado-api/ Main entry point
|
||||||
|
internal/
|
||||||
|
accelerator/ Transaction priority boosting via prioritisetransaction
|
||||||
|
bitcoind/ Minimal JSON-RPC client with retry logic
|
||||||
|
ckpool/ Unix socket protocol client (4-byte LE framing)
|
||||||
|
config/ Environment variable loading and validation
|
||||||
|
httpapi/ REST routes, WebSocket hub, SPA handler
|
||||||
|
logmon/ inotify-based CKPool log tailer
|
||||||
|
state/ Snapshot aggregator, block reconciliation, reorg detection
|
||||||
|
store/ SQLite persistence (blocks, KV, accelerated txs, cursor)
|
||||||
|
webui/ Embedded Svelte assets (go:embed)
|
||||||
|
zmqmon/ ZMQ hashblock subscriber
|
||||||
|
ckpool/
|
||||||
|
patches/ Seven-patch series applied to upstream
|
||||||
|
config/ ckpool.conf.template (sed-rendered at startup)
|
||||||
|
Dockerfile Two-stage ckpool build
|
||||||
|
ui/
|
||||||
|
src/
|
||||||
|
lib/ Svelte 5 components (dashboard, miners, blocks, accelerator)
|
||||||
|
format.ts Hardware detection, hashrate formatting, address parsing
|
||||||
|
snapshot.svelte.ts Global reactive store (WebSocket + REST fallback)
|
||||||
|
types.ts TypeScript interfaces for API responses
|
||||||
|
test/
|
||||||
|
regtest_smoke.sh Two-phase integration test (log-inject + full stratum)
|
||||||
|
```
|
||||||
|
|
||||||
|
## StartOS
|
||||||
|
|
||||||
|
The StartOS wrapper packages Kamado Pool as an `.s9pk` for StartOS 0.3.5.1 with health checks, configuration, actions, backup/restore. Porting to StartOS 0.4.0 is in progress.
|
||||||
|
|
||||||
## Upstream
|
## Upstream
|
||||||
|
|
||||||
CKPool by Con Kolivas: https://bitbucket.org/ckolivas/ckpool
|
CKPool by Con Kolivas — https://bitbucket.org/ckolivas/ckpool
|
||||||
|
|
||||||
Pinned commit: see [ckpool/CKPOOL_COMMIT](ckpool/CKPOOL_COMMIT)
|
Pinned commit: `cfb0f83b70d7b382b85d2bd0710cf4cb2dda4007`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-3.0. CKPool itself is distributed under GPL-3.
|
GPL-3.0 — see [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -65,7 +66,20 @@ func main() {
|
|||||||
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
||||||
agg.Store = blockStore
|
agg.Store = blockStore
|
||||||
agg.MempoolBaseURL = cfg.MempoolBaseURL
|
agg.MempoolBaseURL = cfg.MempoolBaseURL
|
||||||
|
// Purely descriptive (it drives the dashboard's connection badge), so a
|
||||||
|
// malformed value degrades to the UI's fallback rather than refusing to
|
||||||
|
// start a pool that is otherwise fine.
|
||||||
|
if cfg.StratumServersJSON != "" {
|
||||||
|
var servers []state.StratumServer
|
||||||
|
if err := json.Unmarshal([]byte(cfg.StratumServersJSON), &servers); err != nil {
|
||||||
|
log.Warn("ignoring malformed STRATUM_SERVERS", "err", err)
|
||||||
|
} else {
|
||||||
|
agg.StratumServers = servers
|
||||||
|
log.Info("stratum binds declared", "count", len(servers))
|
||||||
|
}
|
||||||
|
}
|
||||||
agg.LogFilePath = cfg.CKPoolLogFile
|
agg.LogFilePath = cfg.CKPoolLogFile
|
||||||
|
agg.KillCKPool = killCKPool(log)
|
||||||
|
|
||||||
// Transaction accelerator (prioritisetransaction).
|
// Transaction accelerator (prioritisetransaction).
|
||||||
var accSvc *accelerator.Service
|
var accSvc *accelerator.Service
|
||||||
@@ -164,3 +178,38 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// killCKPool returns a function that finds the ckpool process by name
|
||||||
|
// and sends it SIGTERM. Used by the aggregator to disconnect miners
|
||||||
|
// when bitcoind is unreachable so they can failover to other pools.
|
||||||
|
func killCKPool(log *slog.Logger) func() error {
|
||||||
|
return func() error {
|
||||||
|
entries, err := os.ReadDir("/proc")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read /proc: %w", err)
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pid, err := strconv.Atoi(e.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// ckpool's cmdline is NUL-separated; the first arg is the binary path.
|
||||||
|
if strings.Contains(string(cmdline), "ckpool") {
|
||||||
|
log.Info("sending SIGTERM to ckpool", "pid", pid)
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("find process %d: %w", pid, err)
|
||||||
|
}
|
||||||
|
return proc.Signal(syscall.SIGTERM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("ckpool process not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,54 @@ type AccelerateResult struct {
|
|||||||
FeeLostError string `json:"fee_lost_error,omitempty"`
|
FeeLostError string `json:"fee_lost_error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marginalFeeLost estimates the fee revenue lost by inserting a boosted
|
||||||
|
// transaction into the template. It finds the lowest fee-rate transaction
|
||||||
|
// in the current template (the one that would be displaced) and returns
|
||||||
|
// the difference: displaced_fee - boosted_tx_real_fee. If the boosted tx
|
||||||
|
// is already in the template or the template has room, returns 0.
|
||||||
|
func marginalFeeLost(tpl *bitcoind.BlockTemplate, txid string, txVsize int64) int64 {
|
||||||
|
if len(tpl.Transactions) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the tx is already in the template (nothing displaced).
|
||||||
|
for _, tx := range tpl.Transactions {
|
||||||
|
if tx.Txid == txid {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the marginal transaction: lowest fee-rate in the template.
|
||||||
|
var marginal *bitcoind.TemplateTx
|
||||||
|
var marginalRate float64 = math.MaxFloat64
|
||||||
|
for i := range tpl.Transactions {
|
||||||
|
tx := &tpl.Transactions[i]
|
||||||
|
// Weight to vsize: ceil(weight/4)
|
||||||
|
vsize := (tx.Weight + 3) / 4
|
||||||
|
if vsize <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rate := float64(tx.Fee) / float64(vsize)
|
||||||
|
if rate < marginalRate {
|
||||||
|
marginalRate = rate
|
||||||
|
marginal = tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if marginal == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The displaced tx's fee is the revenue we lose. We don't add the
|
||||||
|
// boosted tx's real fee back because the pool never had it — the tx
|
||||||
|
// wasn't in the template before the boost.
|
||||||
|
lost := marginal.Fee
|
||||||
|
if lost < 0 {
|
||||||
|
lost = 0
|
||||||
|
}
|
||||||
|
return lost
|
||||||
|
}
|
||||||
|
|
||||||
// Accelerate boosts a transaction to the target feerate (sat/vB).
|
// Accelerate boosts a transaction to the target feerate (sat/vB).
|
||||||
func (s *Service) Accelerate(ctx context.Context, txid string, targetFeerateVB float64) (*AccelerateResult, error) {
|
func (s *Service) Accelerate(ctx context.Context, txid string, targetFeerateVB float64) (*AccelerateResult, error) {
|
||||||
if targetFeerateVB > MaxFeerateVB {
|
if targetFeerateVB > MaxFeerateVB {
|
||||||
@@ -68,38 +116,26 @@ func (s *Service) Accelerate(ctx context.Context, txid string, targetFeerateVB f
|
|||||||
targetFeerateVB, modifiedFeeSats/float64(entry.Vsize))
|
targetFeerateVB, modifiedFeeSats/float64(entry.Vsize))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot coinbasevalue BEFORE the boost to measure actual impact.
|
// Estimate revenue impact from the pre-boost template. The boosted tx
|
||||||
var coinbaseBefore int64
|
// will displace the marginal (lowest fee-rate) transaction in the
|
||||||
|
// template. We compute this from the single template snapshot to avoid
|
||||||
|
// a race with new mempool arrivals between two getblocktemplate calls.
|
||||||
|
var feeLost int64
|
||||||
var tplErr string
|
var tplErr string
|
||||||
tplBefore, err := s.RPC.GetBlockTemplate(ctx)
|
tpl, err := s.RPC.GetBlockTemplate(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tplErr = fmt.Sprintf("getblocktemplate (before): %s", bitcoind.RPCErrorMessage(err))
|
tplErr = fmt.Sprintf("getblocktemplate: %s", bitcoind.RPCErrorMessage(err))
|
||||||
s.Log.Warn("accelerate: getblocktemplate failed (before)", "err", err)
|
s.Log.Warn("accelerate: getblocktemplate failed", "err", err)
|
||||||
} else {
|
} else {
|
||||||
coinbaseBefore = tplBefore.CoinbaseValue
|
feeLost = marginalFeeLost(tpl, txid, entry.Vsize)
|
||||||
|
s.Log.Info("accelerate: fee impact estimated",
|
||||||
|
"marginal_fee_lost", feeLost, "boosted_tx_fee", int64(baseFeeSats))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.RPC.PrioritiseTransaction(ctx, txid, delta); err != nil {
|
if err := s.RPC.PrioritiseTransaction(ctx, txid, delta); err != nil {
|
||||||
return nil, fmt.Errorf("prioritisetransaction: %w", err)
|
return nil, fmt.Errorf("prioritisetransaction: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure coinbasevalue AFTER — the difference is the real fee lost.
|
|
||||||
var feeLost int64
|
|
||||||
if coinbaseBefore > 0 {
|
|
||||||
tplAfter, err := s.RPC.GetBlockTemplate(ctx)
|
|
||||||
if err != nil {
|
|
||||||
tplErr = fmt.Sprintf("getblocktemplate (after): %s", bitcoind.RPCErrorMessage(err))
|
|
||||||
s.Log.Warn("accelerate: getblocktemplate failed (after)", "err", err)
|
|
||||||
} else {
|
|
||||||
feeLost = coinbaseBefore - tplAfter.CoinbaseValue
|
|
||||||
if feeLost < 0 {
|
|
||||||
feeLost = 0 // template improved (new tx arrived between calls)
|
|
||||||
}
|
|
||||||
s.Log.Info("accelerate: fee impact measured",
|
|
||||||
"before", coinbaseBefore, "after", tplAfter.CoinbaseValue, "lost", feeLost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rec := store.BoostedTx{
|
rec := store.BoostedTx{
|
||||||
Txid: txid,
|
Txid: txid,
|
||||||
OriginalFeerate: currentFeerate,
|
OriginalFeerate: currentFeerate,
|
||||||
|
|||||||
@@ -273,11 +273,19 @@ func (c *RPC) GetNetworkHashPS(ctx context.Context, blocks, height int) (float64
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemplateTx is one transaction inside a getblocktemplate result.
|
||||||
|
type TemplateTx struct {
|
||||||
|
Txid string `json:"txid"`
|
||||||
|
Fee int64 `json:"fee"` // satoshis
|
||||||
|
Weight int64 `json:"weight"` // weight units
|
||||||
|
}
|
||||||
|
|
||||||
// BlockTemplate is the subset of getblocktemplate we care about: the
|
// BlockTemplate is the subset of getblocktemplate we care about: the
|
||||||
// coinbase value (subsidy + fees) and height of the next block.
|
// coinbase value (subsidy + fees) and height of the next block.
|
||||||
type BlockTemplate struct {
|
type BlockTemplate struct {
|
||||||
CoinbaseValue int64 `json:"coinbasevalue"` // satoshis
|
CoinbaseValue int64 `json:"coinbasevalue"` // satoshis
|
||||||
Height int64 `json:"height"`
|
Height int64 `json:"height"`
|
||||||
|
Transactions []TemplateTx `json:"transactions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockTemplate fetches the next block template with segwit rules.
|
// GetBlockTemplate fetches the next block template with segwit rules.
|
||||||
@@ -343,6 +351,20 @@ func (c *RPC) GetRawMempoolVerbose(ctx context.Context) (map[string]RawMempoolEn
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NetworkInfo struct {
|
||||||
|
Connections int `json:"connections"`
|
||||||
|
ConnectionsIn int `json:"connections_in"`
|
||||||
|
ConnectionsOut int `json:"connections_out"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RPC) GetNetworkInfo(ctx context.Context) (*NetworkInfo, error) {
|
||||||
|
var out NetworkInfo
|
||||||
|
if err := c.Call(ctx, "getnetworkinfo", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsRPCError checks if err is an rpcError with a specific code.
|
// IsRPCError checks if err is an rpcError with a specific code.
|
||||||
func IsRPCError(err error, code int) bool {
|
func IsRPCError(err error, code int) bool {
|
||||||
var rerr *rpcError
|
var rerr *rpcError
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ type Config struct {
|
|||||||
// the UI uses the public mempool.space; non-empty means the user
|
// the UI uses the public mempool.space; non-empty means the user
|
||||||
// has pointed Kamado at their own instance via the StartOS config.
|
// has pointed Kamado at their own instance via the StartOS config.
|
||||||
MempoolBaseURL string
|
MempoolBaseURL string
|
||||||
|
|
||||||
|
// Optional JSON array describing ckpool's serverurl[] binds, so the
|
||||||
|
// dashboard can name the transport a miner connected over instead of
|
||||||
|
// guessing from the bind index. Rendered by whoever writes
|
||||||
|
// ckpool.conf; empty means "undeclared" and the UI falls back. Parsed
|
||||||
|
// in main (the concrete type lives with the snapshot it belongs to).
|
||||||
|
StratumServersJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromEnv() (*Config, error) {
|
func FromEnv() (*Config, error) {
|
||||||
@@ -54,6 +61,7 @@ func FromEnv() (*Config, error) {
|
|||||||
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
||||||
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
||||||
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
||||||
|
StratumServersJSON: os.Getenv("STRATUM_SERVERS"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.BitcoinRPCURL == "" {
|
if cfg.BitcoinRPCURL == "" {
|
||||||
|
|||||||
@@ -25,6 +25,21 @@ type HashratePoint struct {
|
|||||||
V float64 `json:"v"` // H/s
|
V float64 `json:"v"` // H/s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StratumServer describes one entry in ckpool's serverurl[] array. ckpool
|
||||||
|
// tags every client with the index of the bind it arrived on, but the index
|
||||||
|
// alone doesn't say what that bind *is* — that depends on how the deployment
|
||||||
|
// rendered ckpool.conf. Declaring the array lets the dashboard report the
|
||||||
|
// actual transport (plaintext, TLS with which certificate) instead of
|
||||||
|
// hardcoding a bind order.
|
||||||
|
type StratumServer struct {
|
||||||
|
// Kind is the stable machine-readable tag the UI switches on:
|
||||||
|
// "plain", "tls-local" (package-managed self-signed certificate) or
|
||||||
|
// "tls-public" (CA-issued certificate for a public domain).
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
// Label is the human-readable description shown on hover.
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot is the merged view served to the UI. All fields are safe to
|
// Snapshot is the merged view served to the UI. All fields are safe to
|
||||||
// JSON-serialize directly.
|
// JSON-serialize directly.
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
@@ -82,6 +97,13 @@ type Snapshot struct {
|
|||||||
// the StartOS config "Block Explorer" -> "Custom URL".
|
// the StartOS config "Block Explorer" -> "Custom URL".
|
||||||
MempoolBaseURL string `json:"mempool_base_url,omitempty"`
|
MempoolBaseURL string `json:"mempool_base_url,omitempty"`
|
||||||
|
|
||||||
|
// Describes ckpool's serverurl[] array: entry N tells the UI what a
|
||||||
|
// client with `server == N` is actually connected over. Set via the
|
||||||
|
// STRATUM_SERVERS env by whoever renders ckpool.conf (the StartOS
|
||||||
|
// wrapper). Empty when undeclared, in which case the UI falls back to
|
||||||
|
// its historical "index 1 means TLS" assumption.
|
||||||
|
StratumServers []StratumServer `json:"stratum_servers,omitempty"`
|
||||||
|
|
||||||
// Counts of share-submit attempts ("Possible/Submitting block solve"
|
// Counts of share-submit attempts ("Possible/Submitting block solve"
|
||||||
// log lines) and confirmed solves ("Solved and confirmed block").
|
// log lines) and confirmed solves ("Solved and confirmed block").
|
||||||
// A growing gap means bitcoind is rejecting our submissions or
|
// A growing gap means bitcoind is rejecting our submissions or
|
||||||
@@ -98,6 +120,11 @@ type Snapshot struct {
|
|||||||
HasLastZMQEvent bool `json:"has_last_zmq_event"`
|
HasLastZMQEvent bool `json:"has_last_zmq_event"`
|
||||||
TipChangedAge float64 `json:"tip_changed_age"` // seconds since tip height last changed
|
TipChangedAge float64 `json:"tip_changed_age"` // seconds since tip height last changed
|
||||||
|
|
||||||
|
// Bitcoin Core peer connections.
|
||||||
|
PeerCount int `json:"peer_count"`
|
||||||
|
PeersIn int `json:"peers_in"`
|
||||||
|
PeersOut int `json:"peers_out"`
|
||||||
|
|
||||||
// Share counters: raw counts (1 submission = 1 share regardless of diff).
|
// Share counters: raw counts (1 submission = 1 share regardless of diff).
|
||||||
// Session = since ckpool started; AllTime = persisted across restarts.
|
// Session = since ckpool started; AllTime = persisted across restarts.
|
||||||
SessionAccepted int64 `json:"session_accepted"`
|
SessionAccepted int64 `json:"session_accepted"`
|
||||||
@@ -181,6 +208,10 @@ type Aggregator struct {
|
|||||||
// MempoolBaseURL leaves the UI on its mempool.space defaults.
|
// MempoolBaseURL leaves the UI on its mempool.space defaults.
|
||||||
MempoolBaseURL string
|
MempoolBaseURL string
|
||||||
|
|
||||||
|
// Declared meaning of each ckpool serverurl[] index; see StratumServer.
|
||||||
|
// Nil leaves the UI on its index-1-means-TLS fallback.
|
||||||
|
StratumServers []StratumServer
|
||||||
|
|
||||||
// LogFilePath is the ckpool log path, used for one-time backfill
|
// LogFilePath is the ckpool log path, used for one-time backfill
|
||||||
// of the best share hash when upgrading from a version that didn't
|
// of the best share hash when upgrading from a version that didn't
|
||||||
// capture it. Set by main before calling Run.
|
// capture it. Set by main before calling Run.
|
||||||
@@ -190,6 +221,12 @@ type Aggregator struct {
|
|||||||
// refresh. Used by the WebSocket hub to push updates to clients.
|
// refresh. Used by the WebSocket hub to push updates to clients.
|
||||||
OnRefresh func(Snapshot)
|
OnRefresh func(Snapshot)
|
||||||
|
|
||||||
|
// KillCKPool, if set, is called when bitcoind has been unreachable
|
||||||
|
// for several consecutive polls and no block submission is pending.
|
||||||
|
// Killing ckpool disconnects miners so they can failover to other
|
||||||
|
// pools instead of mining stale work.
|
||||||
|
KillCKPool func() error
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
snap Snapshot
|
snap Snapshot
|
||||||
blocks []BlockRecord
|
blocks []BlockRecord
|
||||||
@@ -231,6 +268,12 @@ type Aggregator struct {
|
|||||||
// escalate to WARN.
|
// escalate to WARN.
|
||||||
ckFailStreak int
|
ckFailStreak int
|
||||||
|
|
||||||
|
// btcFailStreak counts consecutive refreshes where bitcoind was
|
||||||
|
// unreachable. After a threshold, if no block submission is pending,
|
||||||
|
// we kill the ckpool process so miners can failover to other pools.
|
||||||
|
btcFailStreak int
|
||||||
|
ckpoolKilled bool // true after we sent SIGTERM, reset on bitcoind recovery
|
||||||
|
|
||||||
// readyOnce + ready closes the Ready() channel exactly once after
|
// readyOnce + ready closes the Ready() channel exactly once after
|
||||||
// the first refresh completes. main blocks briefly on this so the
|
// the first refresh completes. main blocks briefly on this so the
|
||||||
// HTTP server doesn't serve a never-refreshed (all-zeros) snapshot.
|
// HTTP server doesn't serve a never-refreshed (all-zeros) snapshot.
|
||||||
@@ -384,7 +427,11 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
next := Snapshot{GeneratedAt: time.Now(), MempoolBaseURL: a.MempoolBaseURL}
|
next := Snapshot{
|
||||||
|
GeneratedAt: time.Now(),
|
||||||
|
MempoolBaseURL: a.MempoolBaseURL,
|
||||||
|
StratumServers: a.StratumServers,
|
||||||
|
}
|
||||||
|
|
||||||
// --- ckpool: poolstats, users, workers, clients, uptime ---
|
// --- ckpool: poolstats, users, workers, clients, uptime ---
|
||||||
if ps, err := a.CK.PoolStats(ctx); err == nil {
|
if ps, err := a.CK.PoolStats(ctx); err == nil {
|
||||||
@@ -424,6 +471,11 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
if bi, err := a.RPC.GetBlockchainInfo(ctx); err == nil {
|
if bi, err := a.RPC.GetBlockchainInfo(ctx); err == nil {
|
||||||
next.Chain = bi
|
next.Chain = bi
|
||||||
next.BitcoinOK = true
|
next.BitcoinOK = true
|
||||||
|
if a.btcFailStreak > 0 {
|
||||||
|
a.Log.Info("bitcoind recovered", "after_failures", a.btcFailStreak)
|
||||||
|
}
|
||||||
|
a.btcFailStreak = 0
|
||||||
|
a.ckpoolKilled = false
|
||||||
// Track when the tip height last changed.
|
// Track when the tip height last changed.
|
||||||
if bi.Blocks != a.lastTipHeight {
|
if bi.Blocks != a.lastTipHeight {
|
||||||
a.lastTipHeight = bi.Blocks
|
a.lastTipHeight = bi.Blocks
|
||||||
@@ -436,12 +488,20 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
next.NetworkHashrateHs = nh
|
next.NetworkHashrateHs = nh
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
a.Log.Warn("bitcoind getblockchaininfo failed", "err", err)
|
a.btcFailStreak++
|
||||||
|
a.Log.Warn("bitcoind getblockchaininfo failed", "err", err, "streak", a.btcFailStreak)
|
||||||
if next.LastError == "" {
|
if next.LastError == "" {
|
||||||
next.LastError = err.Error()
|
next.LastError = err.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- bitcoind: peer connections ---
|
||||||
|
if ni, err := a.RPC.GetNetworkInfo(ctx); err == nil {
|
||||||
|
next.PeerCount = ni.Connections
|
||||||
|
next.PeersIn = ni.ConnectionsIn
|
||||||
|
next.PeersOut = ni.ConnectionsOut
|
||||||
|
}
|
||||||
|
|
||||||
// --- predicted difficulty adjustment ---
|
// --- predicted difficulty adjustment ---
|
||||||
// Fetch the timestamp of the first block in the current retarget
|
// Fetch the timestamp of the first block in the current retarget
|
||||||
// epoch (height - height%2016) once per epoch and cache it, then
|
// epoch (height - height%2016) once per epoch and cache it, then
|
||||||
@@ -675,6 +735,26 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
cb(pushed)
|
cb(pushed)
|
||||||
}
|
}
|
||||||
a.markReady()
|
a.markReady()
|
||||||
|
|
||||||
|
// Kill ckpool when bitcoind is unreachable so miners can failover.
|
||||||
|
// Conditions: 3+ consecutive failures, no pending block submission,
|
||||||
|
// haven't already killed it, and a kill callback is configured.
|
||||||
|
const btcFailThreshold = 3
|
||||||
|
submitGap := next.BlockSubmitAttempts - next.BlockSubmitsConfirmed
|
||||||
|
if a.btcFailStreak >= btcFailThreshold && !a.ckpoolKilled && a.KillCKPool != nil {
|
||||||
|
if submitGap > 0 {
|
||||||
|
a.Log.Warn("bitcoind down but block submission pending, keeping ckpool alive",
|
||||||
|
"submit_gap", submitGap, "streak", a.btcFailStreak)
|
||||||
|
} else {
|
||||||
|
a.Log.Warn("bitcoind unreachable, killing ckpool so miners can failover",
|
||||||
|
"streak", a.btcFailStreak)
|
||||||
|
if err := a.KillCKPool(); err != nil {
|
||||||
|
a.Log.Error("failed to kill ckpool", "err", err)
|
||||||
|
} else {
|
||||||
|
a.ckpoolKilled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AckBestDiff records the current best_diff as acknowledged so the UI
|
// AckBestDiff records the current best_diff as acknowledged so the UI
|
||||||
@@ -726,7 +806,6 @@ func diffBucket(d float64) int {
|
|||||||
// DiffBucketLabels are the human-readable labels for each difficulty bucket.
|
// DiffBucketLabels are the human-readable labels for each difficulty bucket.
|
||||||
var DiffBucketLabels = [6]string{"< 1M", "1M – 100M", "100M – 1G", "1G – 100G", "100G – 1T", "≥ 1T"}
|
var DiffBucketLabels = [6]string{"< 1M", "1M – 100M", "100M – 1G", "1G – 100G", "100G – 1T", "≥ 1T"}
|
||||||
|
|
||||||
|
|
||||||
// IngestShareEvents reads individual share events from the log tailer
|
// IngestShareEvents reads individual share events from the log tailer
|
||||||
// and maintains rejection-reason counts and difficulty-distribution
|
// and maintains rejection-reason counts and difficulty-distribution
|
||||||
// histograms. Session counters reset when ckpool restarts (detected by
|
// histograms. Session counters reset when ckpool restarts (detected by
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ LOGDIR="${LOGDIR:-/var/log/ckpool}"
|
|||||||
SOCKET_DIR="${SOCKET_DIR:-/run/ckpool}"
|
SOCKET_DIR="${SOCKET_DIR:-/run/ckpool}"
|
||||||
SHARE_LOG="${SHARE_LOG:-1}"
|
SHARE_LOG="${SHARE_LOG:-1}"
|
||||||
CKPOOL_LOGLEVEL="${CKPOOL_LOGLEVEL:-6}"
|
CKPOOL_LOGLEVEL="${CKPOOL_LOGLEVEL:-6}"
|
||||||
|
# ckpool's second, loopback-only stratum bind. Nothing terminates TLS in the
|
||||||
|
# dev compose stack, but the bind must still resolve: ckpool passes the port
|
||||||
|
# straight to getaddrinfo() and a non-numeric service string is fatal
|
||||||
|
# (connector.c logs "Failed to extract resolved url" and exit(1)s).
|
||||||
|
TLS_INTERNAL_PORT="${TLS_INTERNAL_PORT:-3437}"
|
||||||
|
|
||||||
mkdir -p "$LOGDIR" "$SOCKET_DIR"
|
mkdir -p "$LOGDIR" "$SOCKET_DIR"
|
||||||
|
|
||||||
@@ -68,6 +73,7 @@ sed \
|
|||||||
-e "s|\${BLOCKPOLL_MS}|${BLOCKPOLL_MS}|g" \
|
-e "s|\${BLOCKPOLL_MS}|${BLOCKPOLL_MS}|g" \
|
||||||
-e "s|\${UPDATE_INTERVAL_S}|${UPDATE_INTERVAL_S}|g" \
|
-e "s|\${UPDATE_INTERVAL_S}|${UPDATE_INTERVAL_S}|g" \
|
||||||
-e "s|\${STRATUM_PORT}|${STRATUM_PORT}|g" \
|
-e "s|\${STRATUM_PORT}|${STRATUM_PORT}|g" \
|
||||||
|
-e "s|\${TLS_INTERNAL_PORT}|${TLS_INTERNAL_PORT}|g" \
|
||||||
-e "s|\${MINDIFF}|${MINDIFF}|g" \
|
-e "s|\${MINDIFF}|${MINDIFF}|g" \
|
||||||
-e "s|\${STARTDIFF}|${STARTDIFF}|g" \
|
-e "s|\${STARTDIFF}|${STARTDIFF}|g" \
|
||||||
-e "s|\${MAXDIFF}|${MAXDIFF}|g" \
|
-e "s|\${MAXDIFF}|${MAXDIFF}|g" \
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/src/stratifier.c b/src/stratifier.c
|
||||||
|
index 8281fa02..38b2ac02 100644
|
||||||
|
--- a/src/stratifier.c
|
||||||
|
+++ b/src/stratifier.c
|
||||||
|
@@ -574,7 +574,7 @@ static void generate_coinbase(ckpool_t *ckp, workbase_t *wb)
|
||||||
|
len += wb->enonce2varlen;
|
||||||
|
|
||||||
|
wb->coinb2bin = ckzalloc(512);
|
||||||
|
- memcpy(wb->coinb2bin, "\x0a\x63\x6b\x70\x6f\x6f\x6c", 7);
|
||||||
|
+ memcpy(wb->coinb2bin, "\x0a\x6b\x61\x6d\x61\x64\x6f", 7);
|
||||||
|
wb->coinb2len = 7;
|
||||||
|
if (ckp->btcsig) {
|
||||||
|
int siglen = strlen(ckp->btcsig);
|
||||||
@@ -17,6 +17,7 @@ alphabetical order:
|
|||||||
| `0001-expose-bestever-in-runtime-json.patch` | Adds `bestever` field to the `users` / `workers` runtime socket JSON |
|
| `0001-expose-bestever-in-runtime-json.patch` | Adds `bestever` field to the `users` / `workers` runtime socket JSON |
|
||||||
| `0002-enable-socket-api-responses.patch` | Always reply on the listener socket so kamado-api gets responses even with `btcsolo: true` |
|
| `0002-enable-socket-api-responses.patch` | Always reply on the listener socket so kamado-api gets responses even with `btcsolo: true` |
|
||||||
| `0003-share-error-as-stratum-array.patch` | Maps `share_err` to Stratum spec error codes; emits `[code, msg, null]` per Slush |
|
| `0003-share-error-as-stratum-array.patch` | Maps `share_err` to Stratum spec error codes; emits `[code, msg, null]` per Slush |
|
||||||
|
| `0007-rename-coinbase-tag-ckpool-to-kamado.patch` | Replaces hardcoded "ckpool" branding in coinbase scriptSig with "kamado" (same 6 bytes) |
|
||||||
|
|
||||||
### Why 0001 matters
|
### Why 0001 matters
|
||||||
|
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ bitcoind \
|
|||||||
-rpcbind=127.0.0.1 \
|
-rpcbind=127.0.0.1 \
|
||||||
-rpcallowip=127.0.0.1 \
|
-rpcallowip=127.0.0.1 \
|
||||||
-fallbackfee=0.0001 \
|
-fallbackfee=0.0001 \
|
||||||
|
-debug=validation \
|
||||||
-nodaemon \
|
-nodaemon \
|
||||||
>"$WORK_DIR/bitcoind.log" 2>&1 &
|
>"$WORK_DIR/bitcoind.log" 2>&1 &
|
||||||
BITCOIN_PID=$!
|
BITCOIN_PID=$!
|
||||||
@@ -721,8 +722,81 @@ P2_CANONICAL=$(cli getblockhash "$P2_HEIGHT")
|
|||||||
[[ "$P2_HASH" == "$P2_CANONICAL" ]] \
|
[[ "$P2_HASH" == "$P2_CANONICAL" ]] \
|
||||||
|| die "Phase 2: hash mismatch: api=$P2_HASH bitcoind=$P2_CANONICAL"
|
|| die "Phase 2: hash mismatch: api=$P2_HASH bitcoind=$P2_CANONICAL"
|
||||||
|
|
||||||
|
# ---- verbose coinbase validation --------------------------------------------
|
||||||
|
# Decode the mined block's coinbase transaction and verify the scriptSig
|
||||||
|
# contains "kamado" (our patched branding) and the btcsig tag, and that
|
||||||
|
# bitcoind accepted the coinbase as consensus-valid.
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "=== Coinbase validation ==="
|
||||||
|
|
||||||
|
P2_BLOCK_HEX=$(cli getblock "$P2_HASH" 0)
|
||||||
|
# The coinbase is the first transaction in the block. getblock verbosity=2
|
||||||
|
# gives us the decoded transaction directly.
|
||||||
|
P2_BLOCK_JSON=$(cli getblock "$P2_HASH" 2)
|
||||||
|
|
||||||
|
COINBASE_TXID=$(echo "$P2_BLOCK_JSON" | jq -r '.tx[0].txid')
|
||||||
|
COINBASE_SCRIPTSIG_HEX=$(echo "$P2_BLOCK_JSON" | jq -r '.tx[0].vin[0].coinbase')
|
||||||
|
COINBASE_SCRIPTSIG_SIZE=${#COINBASE_SCRIPTSIG_HEX}
|
||||||
|
COINBASE_SCRIPTSIG_BYTES=$((COINBASE_SCRIPTSIG_SIZE / 2))
|
||||||
|
COINBASE_SCRIPTSIG_ASM=$(echo "$P2_BLOCK_JSON" | jq -r '.tx[0].vin[0].coinbase' | xxd -r -p | strings -n 3 | tr '\n' ' ')
|
||||||
|
|
||||||
|
log " coinbase txid: $COINBASE_TXID"
|
||||||
|
log " scriptSig hex: $COINBASE_SCRIPTSIG_HEX"
|
||||||
|
log " scriptSig size: $COINBASE_SCRIPTSIG_BYTES bytes (max 100)"
|
||||||
|
log " readable strings: $COINBASE_SCRIPTSIG_ASM"
|
||||||
|
|
||||||
|
# Verify scriptSig is within consensus limit
|
||||||
|
(( COINBASE_SCRIPTSIG_BYTES <= 100 )) \
|
||||||
|
|| die "Coinbase scriptSig exceeds 100-byte consensus limit ($COINBASE_SCRIPTSIG_BYTES bytes)"
|
||||||
|
(( COINBASE_SCRIPTSIG_BYTES >= 2 )) \
|
||||||
|
|| die "Coinbase scriptSig below 2-byte consensus minimum ($COINBASE_SCRIPTSIG_BYTES bytes)"
|
||||||
|
|
||||||
|
# Verify our branding is present (kamado in hex = 6b616d61646f)
|
||||||
|
if echo "$COINBASE_SCRIPTSIG_HEX" | grep -qi "6b616d61646f"; then
|
||||||
|
log " branding check: 'kamado' found in scriptSig ✓"
|
||||||
|
else
|
||||||
|
# Fall back to checking for 'ckpool' (pre-patch binary)
|
||||||
|
if echo "$COINBASE_SCRIPTSIG_HEX" | grep -qi "636b706f6f6c"; then
|
||||||
|
log " branding check: 'ckpool' found (pre-patch binary, expected if patch not applied)"
|
||||||
|
else
|
||||||
|
log " WARNING: neither 'kamado' nor 'ckpool' found in scriptSig"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify btcsig tag is present
|
||||||
|
BTCSIG_HEX=$(echo -n "/KamadoSmoke/" | xxd -p)
|
||||||
|
if echo "$COINBASE_SCRIPTSIG_HEX" | grep -qi "$BTCSIG_HEX"; then
|
||||||
|
log " btcsig check: '/KamadoSmoke/' found in scriptSig ✓"
|
||||||
|
else
|
||||||
|
log " WARNING: btcsig '/KamadoSmoke/' not found in scriptSig"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The ultimate proof: bitcoind accepted this block into its chain.
|
||||||
|
# getblock succeeds and confirmations > 0 means full consensus validation passed.
|
||||||
|
P2_CONFIRMATIONS=$(echo "$P2_BLOCK_JSON" | jq -r '.confirmations')
|
||||||
|
log " confirmations: $P2_CONFIRMATIONS (block accepted by bitcoind consensus) ✓"
|
||||||
|
|
||||||
log "Phase 2 PASS: block $P2_HEIGHT → hash $P2_HASH (matches bitcoind)"
|
log "Phase 2 PASS: block $P2_HEIGHT → hash $P2_HASH (matches bitcoind)"
|
||||||
|
|
||||||
|
# ---- bitcoind validation debug log ------------------------------------------
|
||||||
|
# Show bitcoind's internal validation messages for the mined block.
|
||||||
|
# Requires -debug=validation flag (added to bitcoind startup above).
|
||||||
|
|
||||||
|
BTC_DEBUG_LOG="$BTC_DIR/regtest/debug.log"
|
||||||
|
if [[ -f "$BTC_DEBUG_LOG" ]]; then
|
||||||
|
log ""
|
||||||
|
log "=== bitcoind validation log (block $P2_HASH) ==="
|
||||||
|
# Show only lines mentioning our specific block hash (filters out generatetoaddress noise)
|
||||||
|
grep -F "$P2_HASH" \
|
||||||
|
"$BTC_DEBUG_LOG" | while IFS= read -r line; do
|
||||||
|
log " $line"
|
||||||
|
done
|
||||||
|
log "=== end validation log ==="
|
||||||
|
else
|
||||||
|
log " WARNING: bitcoind debug.log not found at $BTC_DEBUG_LOG"
|
||||||
|
fi
|
||||||
|
|
||||||
# ---- all done ---------------------------------------------------------------
|
# ---- all done ---------------------------------------------------------------
|
||||||
|
|
||||||
printf '[smoke] PASS: Phase 1 (log-injection) + Phase 2 (stratum → submitblock) both verified\n'
|
printf '[smoke] PASS: Phase 1 (log-injection) + Phase 2 (stratum → submitblock) both verified\n'
|
||||||
|
|||||||
Generated
+308
@@ -7,6 +7,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "kamado-ui",
|
"name": "kamado-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
@@ -981,6 +984,30 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||||
@@ -1001,6 +1028,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
@@ -1017,6 +1053,17 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -1027,6 +1074,24 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1045,6 +1110,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
@@ -1062,6 +1136,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -1147,6 +1233,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1162,6 +1261,24 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -1189,6 +1306,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -1235,6 +1364,51 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1255,6 +1429,15 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||||
@@ -1284,6 +1467,23 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -1298,6 +1498,21 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.60.2",
|
"version": "4.60.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
|
||||||
@@ -1356,6 +1571,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1366,6 +1587,32 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.55.4",
|
"version": "5.55.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz",
|
||||||
@@ -1544,6 +1791,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zimmerframe": {
|
"node_modules/zimmerframe": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||||
|
|||||||
@@ -17,5 +17,8 @@
|
|||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"typescript": "^5.6.0",
|
"typescript": "^5.6.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"qrcode": "^1.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
@@ -16,9 +16,18 @@
|
|||||||
import BestSharePage from "./lib/BestSharePage.svelte";
|
import BestSharePage from "./lib/BestSharePage.svelte";
|
||||||
import SharesBar from "./lib/SharesBar.svelte";
|
import SharesBar from "./lib/SharesBar.svelte";
|
||||||
import BlockFoundAnimation from "./lib/BlockFoundAnimation.svelte";
|
import BlockFoundAnimation from "./lib/BlockFoundAnimation.svelte";
|
||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
const DONATE_ADDR = "bc1qcyuh66rl3xl6a8w6k02ncupg4yhg898023gvar";
|
||||||
|
let qrCanvas: HTMLCanvasElement;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
connect();
|
connect();
|
||||||
|
QRCode.toCanvas(qrCanvas, DONATE_ADDR, {
|
||||||
|
width: 180,
|
||||||
|
margin: 1,
|
||||||
|
color: { dark: "#e6e9ef", light: "#151a24" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -81,6 +90,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<footer class="donate">
|
||||||
|
<div class="donate-tagline">Open source & Made with ❤️</div>
|
||||||
|
<div class="donate-label">Donate BTC</div>
|
||||||
|
<div class="donate-addr-wrap">
|
||||||
|
<span class="addr">bc1qcyuh66rl3xl6a8w6k02ncupg4yhg898023gvar</span>
|
||||||
|
<div class="qr-popup">
|
||||||
|
<canvas bind:this={qrCanvas} width="180" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<BlockFoundAnimation />
|
<BlockFoundAnimation />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -93,6 +113,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
main {
|
||||||
|
padding: 1rem 0.6rem 3rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
main::before {
|
main::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -125,6 +151,66 @@
|
|||||||
.bad {
|
.bad {
|
||||||
color: var(--bad);
|
color: var(--bad);
|
||||||
}
|
}
|
||||||
|
:global(.donate) {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2.5rem 1rem 2rem;
|
||||||
|
color: var(--fg-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
:global(.donate .donate-tagline) {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--fg);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
:global(.donate .donate-label) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--fg-dim);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
:global(.donate .donate-addr-wrap) {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
:global(.donate .addr) {
|
||||||
|
font-family: "Fira Code", "Cascadia Code", monospace;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--accent);
|
||||||
|
word-break: break-all;
|
||||||
|
user-select: all;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: text-shadow 0.2s;
|
||||||
|
}
|
||||||
|
:global(.donate .addr:hover) {
|
||||||
|
text-shadow: 0 0 8px var(--accent);
|
||||||
|
}
|
||||||
|
:global(.donate .qr-popup) {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 12px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) scale(0.9);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
:global(.donate .donate-addr-wrap:hover .qr-popup) {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateX(-50%) scale(1);
|
||||||
|
}
|
||||||
|
:global(.donate .qr-popup canvas) {
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
.page-enter {
|
.page-enter {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -98,6 +98,34 @@ code,
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Mobile breakpoints ── */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
:root {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
padding: 1rem 1rem;
|
||||||
|
}
|
||||||
|
.grid-4 {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.grid-4 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
font-size: 0.82em;
|
font-size: 0.82em;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Formatters for hashrate, share difficulty, time, and miner hardware
|
// Formatters for hashrate, share difficulty, time, and miner hardware
|
||||||
// detection from stratum user-agent strings. Pure functions, no DOM.
|
// detection from stratum user-agent strings. Pure functions, no DOM.
|
||||||
|
|
||||||
|
import type { StratumServer } from "./types";
|
||||||
|
|
||||||
const HASHRATE_UNITS = ["H/s", "kH/s", "MH/s", "GH/s", "TH/s", "PH/s", "EH/s"];
|
const HASHRATE_UNITS = ["H/s", "kH/s", "MH/s", "GH/s", "TH/s", "PH/s", "EH/s"];
|
||||||
|
|
||||||
export function formatHashrate(hs: number): string {
|
export function formatHashrate(hs: number): string {
|
||||||
@@ -100,6 +102,26 @@ export function explorerBaseFor(
|
|||||||
return "https://mempool.space";
|
return "https://mempool.space";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve how a miner is connected from its ckpool serverurl[] index.
|
||||||
|
//
|
||||||
|
// The deployment declares the bind array via STRATUM_SERVERS (the StartOS
|
||||||
|
// wrapper renders one entry per bind it configures). When it hasn't — the
|
||||||
|
// dev compose stack, or an older wrapper — fall back to the historical
|
||||||
|
// layout, where index 0 is the plaintext bind and anything else is the
|
||||||
|
// loopback bind that stunnel forwards TLS traffic to.
|
||||||
|
export function connectionOf(
|
||||||
|
serverIndex: number | undefined,
|
||||||
|
servers: StratumServer[] | undefined,
|
||||||
|
): { tls: boolean; label: string } {
|
||||||
|
const declared = servers?.[serverIndex ?? 0];
|
||||||
|
if (declared) {
|
||||||
|
return { tls: declared.kind !== "plain", label: declared.label };
|
||||||
|
}
|
||||||
|
return serverIndex
|
||||||
|
? { tls: true, label: "Encrypted connection via TLS" }
|
||||||
|
: { tls: false, label: "Unencrypted connection" };
|
||||||
|
}
|
||||||
|
|
||||||
// Detect common Bitcoin mining hardware from the stratum useragent
|
// Detect common Bitcoin mining hardware from the stratum useragent
|
||||||
// string. This is a best-effort heuristic; unknown agents fall back
|
// string. This is a best-effort heuristic; unknown agents fall back
|
||||||
// to a stripped version of the raw string. Order matters — check
|
// to a stripped version of the raw string. Order matters — check
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
} else if (feeLostSats > 0) {
|
} else if (feeLostSats > 0) {
|
||||||
success = `Boosted from ${from} to ${to} sat/vB. Revenue impact: -${feeLostSats.toLocaleString()} sats (${(feeLostSats / 1e8).toFixed(8)} BTC) per block mined.`;
|
success = `Boosted from ${from} to ${to} sat/vB. Revenue impact: -${feeLostSats.toLocaleString()} sats (${(feeLostSats / 1e8).toFixed(8)} BTC) per block mined.`;
|
||||||
} else {
|
} else {
|
||||||
success = `Boosted from ${from} to ${to} sat/vB. No revenue impact — the displaced transaction's fee was equal or lower.`;
|
success = `Boosted from ${from} to ${to} sat/vB. No revenue impact — the transaction was already in the template or the mempool fits in one block.`;
|
||||||
}
|
}
|
||||||
txid = "";
|
txid = "";
|
||||||
feerateInput = "";
|
feerateInput = "";
|
||||||
@@ -169,9 +169,9 @@
|
|||||||
mempool isn't full (all transactions already fit), there is <em>zero cost</em>.
|
mempool isn't full (all transactions already fit), there is <em>zero cost</em>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Revenue impact:</strong> After boosting, the server compares the block template's
|
<strong>Revenue impact:</strong> The server inspects the current block template to find
|
||||||
coinbase value before and after. The difference (if any) is the actual fee revenue
|
the marginal transaction — the lowest fee-rate tx that would be displaced by the
|
||||||
lost per block mined. This is shown after each boost.
|
boosted one. Its fee is the revenue you sacrifice per block mined, shown after each boost.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Your pool: {formatHashrate(poolHashrate)} /
|
Your pool: {formatHashrate(poolHashrate)} /
|
||||||
|
|||||||
@@ -399,15 +399,15 @@
|
|||||||
}
|
}
|
||||||
.disclaimer {
|
.disclaimer {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: var(--fg-dim);
|
color: rgb(160, 195, 240);
|
||||||
background: rgba(245, 196, 71, 0.08);
|
background: rgb(20, 35, 60);
|
||||||
border: 1px solid rgba(245, 196, 71, 0.25);
|
border: 1px solid rgb(60, 100, 170);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.5em 0.85em;
|
padding: 0.5em 0.85em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.disclaimer strong {
|
.disclaimer strong {
|
||||||
color: var(--fg);
|
color: rgb(200, 220, 250);
|
||||||
}
|
}
|
||||||
.head {
|
.head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -572,4 +572,26 @@
|
|||||||
.swatch.have { background: var(--good); }
|
.swatch.have { background: var(--good); }
|
||||||
.swatch.need { background: rgb(245, 196, 71); }
|
.swatch.need { background: rgb(245, 196, 71); }
|
||||||
.swatch.rest { background: var(--fg); }
|
.swatch.rest { background: var(--fg); }
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.toggle-bar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 0.5em 0.5em;
|
||||||
|
}
|
||||||
|
.hash-vis {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.hash-vis.binary {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
.totals {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -69,4 +69,18 @@
|
|||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: hide session best column, truncate worker names */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
table :global(th:nth-child(3)),
|
||||||
|
table :global(td:nth-child(3)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
td.mono {
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -194,4 +194,22 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin-left: 0.25em;
|
margin-left: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: hide Chain, Hash, Reward columns */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
table :global(th:nth-child(3)),
|
||||||
|
table :global(td:nth-child(3)),
|
||||||
|
table :global(th:nth-child(4)),
|
||||||
|
table :global(td:nth-child(4)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
table :global(th:nth-child(5)),
|
||||||
|
table :global(td:nth-child(5)),
|
||||||
|
table :global(th:nth-child(6)),
|
||||||
|
table :global(td:nth-child(6)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -119,4 +119,17 @@
|
|||||||
.ws-label {
|
.ws-label {
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.brand {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,10 +25,22 @@
|
|||||||
// Alarm when ZMQ is 3+ min older than the last tip change.
|
// Alarm when ZMQ is 3+ min older than the last tip change.
|
||||||
return zmqAge > tipAge + 180;
|
return zmqAge > tipAge + 180;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bitcoindDown = $derived(snap.data != null && !snap.data.bitcoin_ok);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if submitGap > 0 || zmqStale}
|
{#if bitcoindDown || submitGap > 0 || zmqStale}
|
||||||
<div class="banners">
|
<div class="banners">
|
||||||
|
{#if bitcoindDown}
|
||||||
|
<div class="banner error">
|
||||||
|
<span class="icon">!</span>
|
||||||
|
<div class="text">
|
||||||
|
<strong>Bitcoin Core is unreachable.</strong>
|
||||||
|
Stratum server has been stopped — miners will failover to backup pools.
|
||||||
|
Service will resume automatically when Bitcoin Core recovers.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if submitGap > 0}
|
{#if submitGap > 0}
|
||||||
<div class="banner warn">
|
<div class="banner warn">
|
||||||
<span class="icon">!</span>
|
<span class="icon">!</span>
|
||||||
@@ -75,6 +87,11 @@
|
|||||||
border-color: rgb(220, 170, 60);
|
border-color: rgb(220, 170, 60);
|
||||||
color: rgb(220, 180, 100);
|
color: rgb(220, 180, 100);
|
||||||
}
|
}
|
||||||
|
.banner.error {
|
||||||
|
background: rgb(50, 20, 20);
|
||||||
|
border-color: rgb(220, 60, 60);
|
||||||
|
color: rgb(230, 120, 120);
|
||||||
|
}
|
||||||
.icon {
|
.icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
btcAddressOf,
|
btcAddressOf,
|
||||||
|
connectionOf,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { StratumClient, Worker } from "../types";
|
import type { StratumClient, Worker } from "../types";
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
hardware: string;
|
hardware: string;
|
||||||
openSource: boolean;
|
openSource: boolean;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
|
tlsLabel: string;
|
||||||
hashrate1m: number;
|
hashrate1m: number;
|
||||||
hashrate1h: number;
|
hashrate1h: number;
|
||||||
bestSession: number;
|
bestSession: number;
|
||||||
@@ -35,9 +37,10 @@
|
|||||||
// from the joined worker_instance, which ckpool keys by full
|
// from the joined worker_instance, which ckpool keys by full
|
||||||
// workername with user = the BTC.
|
// workername with user = the BTC.
|
||||||
//
|
//
|
||||||
// ckpool's serverurl array gives us a second piece of info: TLS
|
// ckpool's serverurl array gives us a second piece of info: which
|
||||||
// traffic comes in via stunnel on the loopback-only bind (server
|
// bind the connection arrived on. connectionOf() maps that index to
|
||||||
// index 1), so client.server === 1 means this miner is using TLS.
|
// the transport the deployment declared, so the lock badge can name
|
||||||
|
// the certificate in play rather than just "encrypted".
|
||||||
const rows = $derived.by<Row[]>(() => {
|
const rows = $derived.by<Row[]>(() => {
|
||||||
const clients = snap.data?.clients ?? [];
|
const clients = snap.data?.clients ?? [];
|
||||||
const workers = snap.data?.workers ?? [];
|
const workers = snap.data?.workers ?? [];
|
||||||
@@ -52,13 +55,15 @@
|
|||||||
seen.add(wname);
|
seen.add(wname);
|
||||||
const w = byWorker.get(wname);
|
const w = byWorker.get(wname);
|
||||||
const btcAddress = w?.user ?? btcAddressOf(wname);
|
const btcAddress = w?.user ?? btcAddressOf(wname);
|
||||||
|
const conn = connectionOf(c.server, snap.data?.stratum_servers);
|
||||||
out.push({
|
out.push({
|
||||||
workerName: wname,
|
workerName: wname,
|
||||||
btcAddress,
|
btcAddress,
|
||||||
sourceIp: c.address,
|
sourceIp: c.address,
|
||||||
hardware: detectHardware(c.useragent),
|
hardware: detectHardware(c.useragent),
|
||||||
openSource: isOpenSource(c.useragent),
|
openSource: isOpenSource(c.useragent),
|
||||||
tls: c.server === 1,
|
tls: conn.tls,
|
||||||
|
tlsLabel: conn.label,
|
||||||
hashrate1m: c.dsps1 * 2 ** 32,
|
hashrate1m: c.dsps1 * 2 ** 32,
|
||||||
hashrate1h: c.dsps60 * 2 ** 32,
|
hashrate1h: c.dsps60 * 2 ** 32,
|
||||||
bestSession: c.bestdiff,
|
bestSession: c.bestdiff,
|
||||||
@@ -79,6 +84,7 @@
|
|||||||
hardware: "offline",
|
hardware: "offline",
|
||||||
openSource: false,
|
openSource: false,
|
||||||
tls: false,
|
tls: false,
|
||||||
|
tlsLabel: "",
|
||||||
hashrate1m: 0,
|
hashrate1m: 0,
|
||||||
hashrate1h: 0,
|
hashrate1h: 0,
|
||||||
bestSession: 0,
|
bestSession: 0,
|
||||||
@@ -133,7 +139,7 @@
|
|||||||
title="View per-worker stats"
|
title="View per-worker stats"
|
||||||
>{r.workerName}</button>
|
>{r.workerName}</button>
|
||||||
{#if r.tls}
|
{#if r.tls}
|
||||||
<span class="badge tls" title="Encrypted connection via TLS">
|
<span class="badge tls" title={r.tlsLabel}>
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -278,4 +284,28 @@
|
|||||||
tr.idle td {
|
tr.idle td {
|
||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: hide less critical columns */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
table :global(th:nth-child(2)),
|
||||||
|
table :global(td:nth-child(2)),
|
||||||
|
table :global(th:nth-child(5)),
|
||||||
|
table :global(td:nth-child(5)),
|
||||||
|
table :global(th:nth-child(6)),
|
||||||
|
table :global(td:nth-child(6)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
table :global(th:nth-child(4)),
|
||||||
|
table :global(td:nth-child(4)),
|
||||||
|
table :global(th:nth-child(7)),
|
||||||
|
table :global(td:nth-child(7)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.section-head {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -208,7 +208,12 @@
|
|||||||
<div class="card height-card" class:new-block={heightFlash}>
|
<div class="card height-card" class:new-block={heightFlash}>
|
||||||
<div class="stat-label">Block height</div>
|
<div class="stat-label">Block height</div>
|
||||||
<div class="stat-value">{height ? height.toLocaleString() : "—"}</div>
|
<div class="stat-value">{height ? height.toLocaleString() : "—"}</div>
|
||||||
<div class="stat-sub">{data.chain?.chain === "main" ? "mainnet" : data.chain?.chain ?? "—"}</div>
|
<div class="stat-sub">
|
||||||
|
{data.chain?.chain === "main" ? "mainnet" : data.chain?.chain ?? "—"}
|
||||||
|
{#if data.peer_count > 0}
|
||||||
|
<span class="peers" title="{data.peers_in} inbound · {data.peers_out} outbound"> · {data.peer_count} peers</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -267,6 +272,12 @@
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.grid-5 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.retarget-bar {
|
.retarget-bar {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
|
|||||||
@@ -285,4 +285,18 @@
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.shares-bar {
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
}
|
||||||
|
.accepted, .rejected {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
.pct {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
.group {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
|
|
||||||
// --- Difficulty distribution ---
|
// --- Difficulty distribution ---
|
||||||
const bucketLabels = ["< 1M", "1M\u2013100M", "100M\u20131G", "1G\u2013100G", "100G\u20131T", "\u2265 1T"];
|
const bucketLabels = ["< 1M", "1M\u2013100M", "100M\u20131G", "1G\u2013100G", "100G\u20131T", "\u2265 1T"];
|
||||||
|
// Fire intensity classes: none for <1M, then progressively more intense.
|
||||||
|
const fireClasses = ["", "fire-1", "fire-2", "fire-3", "fire-4", "fire-5"];
|
||||||
|
|
||||||
type BucketRow = {
|
type BucketRow = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -180,7 +182,7 @@
|
|||||||
|
|
||||||
<!-- Difficulty Distribution Table -->
|
<!-- Difficulty Distribution Table -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h3>Difficulty Distribution</h3>
|
<h3>Difficulty Distribution - Amount of shares in different difficulty ranges</h3>
|
||||||
{#if !hasDistData}
|
{#if !hasDistData}
|
||||||
<div class="empty">No accepted shares yet</div>
|
<div class="empty">No accepted shares yet</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -196,9 +198,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each bucketRows as row}
|
{#each bucketRows as row, i}
|
||||||
<tr class:dim-row={row.session === 0 && row.alltime === 0}>
|
{@const hasShares = row.session > 0 || row.alltime > 0}
|
||||||
<td>{row.label}</td>
|
<tr class:dim-row={!hasShares && !fireClasses[i]}>
|
||||||
|
<td>
|
||||||
|
{#if fireClasses[i]}
|
||||||
|
<span class="fire-label {fireClasses[i]}" class:fire-active={hasShares} class:fire-dimmed={!hasShares}>{row.label}</span>
|
||||||
|
{:else}
|
||||||
|
{row.label}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
<td class="num">{row.session.toLocaleString()}</td>
|
<td class="num">{row.session.toLocaleString()}</td>
|
||||||
<td class="num">{row.sessionPct.toFixed(1)}%</td>
|
<td class="num">{row.sessionPct.toFixed(1)}%</td>
|
||||||
<td class="num">{row.alltime.toLocaleString()}</td>
|
<td class="num">{row.alltime.toLocaleString()}</td>
|
||||||
@@ -260,6 +269,9 @@
|
|||||||
.totals { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.totals { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
}
|
}
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
|
.totals { grid-template-columns: 1fr 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 360px) {
|
||||||
.totals { grid-template-columns: 1fr; }
|
.totals { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,4 +322,24 @@
|
|||||||
tr[title]:hover td { color: var(--accent); }
|
tr[title]:hover td { color: var(--accent); }
|
||||||
.dim-row td { opacity: 0.35; }
|
.dim-row td { opacity: 0.35; }
|
||||||
|
|
||||||
|
/* --- Glowing range labels --- */
|
||||||
|
.fire-label {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.fire-dimmed { opacity: 0.2; }
|
||||||
|
.fire-active { animation: glow-pulse 2s ease-in-out infinite alternate; }
|
||||||
|
|
||||||
|
.fire-1.fire-active { color: #ffcc80; --glow: 255, 180, 100; }
|
||||||
|
.fire-2.fire-active { color: #ff9a5c; --glow: 255, 140, 60; }
|
||||||
|
.fire-3.fire-active { color: #ff6e40; --glow: 255, 90, 30; }
|
||||||
|
.fire-4.fire-active { color: #f44336; --glow: 230, 40, 20; }
|
||||||
|
.fire-5.fire-active { color: #c62828; --glow: 180, 20, 10; }
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0% { text-shadow: 0 0 8px rgba(var(--glow), 0.4); }
|
||||||
|
100% { text-shadow: 0 0 8px rgba(var(--glow), 0.85),
|
||||||
|
0 0 16px rgba(var(--glow), 0.3); }
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
explorerBaseFor,
|
explorerBaseFor,
|
||||||
|
connectionOf,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { Worker, StratumClient } from "../types";
|
import type { Worker, StratumClient } from "../types";
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
hardware: string;
|
hardware: string;
|
||||||
openSource: boolean;
|
openSource: boolean;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
|
tlsLabel: string;
|
||||||
sourceIp: string;
|
sourceIp: string;
|
||||||
hashrate1m: number;
|
hashrate1m: number;
|
||||||
hashrate1h: number;
|
hashrate1h: number;
|
||||||
@@ -64,7 +66,10 @@
|
|||||||
worker: w.worker,
|
worker: w.worker,
|
||||||
hardware: c ? detectHardware(c.useragent) : "offline",
|
hardware: c ? detectHardware(c.useragent) : "offline",
|
||||||
openSource: c ? isOpenSource(c.useragent) : false,
|
openSource: c ? isOpenSource(c.useragent) : false,
|
||||||
tls: c?.server === 1,
|
tls: !!c && connectionOf(c.server, snap.data?.stratum_servers).tls,
|
||||||
|
tlsLabel: c
|
||||||
|
? connectionOf(c.server, snap.data?.stratum_servers).label
|
||||||
|
: "",
|
||||||
sourceIp: c?.address ?? "",
|
sourceIp: c?.address ?? "",
|
||||||
hashrate1m: c ? c.dsps1 * 2 ** 32 : 0,
|
hashrate1m: c ? c.dsps1 * 2 ** 32 : 0,
|
||||||
hashrate1h: c ? c.dsps60 * 2 ** 32 : 0,
|
hashrate1h: c ? c.dsps60 * 2 ** 32 : 0,
|
||||||
@@ -85,7 +90,8 @@
|
|||||||
worker: wname,
|
worker: wname,
|
||||||
hardware: detectHardware(c.useragent),
|
hardware: detectHardware(c.useragent),
|
||||||
openSource: isOpenSource(c.useragent),
|
openSource: isOpenSource(c.useragent),
|
||||||
tls: c.server === 1,
|
tls: connectionOf(c.server, snap.data?.stratum_servers).tls,
|
||||||
|
tlsLabel: connectionOf(c.server, snap.data?.stratum_servers).label,
|
||||||
sourceIp: c.address,
|
sourceIp: c.address,
|
||||||
hashrate1m: c.dsps1 * 2 ** 32,
|
hashrate1m: c.dsps1 * 2 ** 32,
|
||||||
hashrate1h: c.dsps60 * 2 ** 32,
|
hashrate1h: c.dsps60 * 2 ** 32,
|
||||||
@@ -232,7 +238,7 @@
|
|||||||
title="View per-worker stats"
|
title="View per-worker stats"
|
||||||
>{r.worker}</button>
|
>{r.worker}</button>
|
||||||
{#if r.tls}
|
{#if r.tls}
|
||||||
<span class="badge tls" title="Encrypted connection via TLS">
|
<span class="badge tls" title={r.tlsLabel}>
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -435,4 +441,27 @@
|
|||||||
tr.idle td {
|
tr.idle td {
|
||||||
color: var(--fg-dim);
|
color: var(--fg-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: hide Hardware, Status, Diff, Best(session) columns */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
table :global(th:nth-child(2)),
|
||||||
|
table :global(td:nth-child(2)),
|
||||||
|
table :global(th:nth-child(3)),
|
||||||
|
table :global(td:nth-child(3)),
|
||||||
|
table :global(th:nth-child(6)),
|
||||||
|
table :global(td:nth-child(6)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
table :global(th:nth-child(5)),
|
||||||
|
table :global(td:nth-child(5)),
|
||||||
|
table :global(th:nth-child(7)),
|
||||||
|
table :global(td:nth-child(7)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.addr {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
btcAddressOf,
|
btcAddressOf,
|
||||||
|
connectionOf,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { Worker, StratumClient } from "../types";
|
import type { Worker, StratumClient } from "../types";
|
||||||
|
|
||||||
@@ -29,7 +30,12 @@
|
|||||||
const idle = $derived(client?.idle ?? worker?.idle ?? false);
|
const idle = $derived(client?.idle ?? worker?.idle ?? false);
|
||||||
const hardware = $derived(client ? detectHardware(client.useragent) : "offline");
|
const hardware = $derived(client ? detectHardware(client.useragent) : "offline");
|
||||||
const openSource = $derived(client ? isOpenSource(client.useragent) : false);
|
const openSource = $derived(client ? isOpenSource(client.useragent) : false);
|
||||||
const tls = $derived(client?.server === 1);
|
const conn = $derived(
|
||||||
|
client
|
||||||
|
? connectionOf(client.server, snap.data?.stratum_servers)
|
||||||
|
: { tls: false, label: "" },
|
||||||
|
);
|
||||||
|
const tls = $derived(conn.tls);
|
||||||
|
|
||||||
const hs1m = $derived(client ? client.dsps1 * 2 ** 32 : 0);
|
const hs1m = $derived(client ? client.dsps1 * 2 ** 32 : 0);
|
||||||
const hs5m = $derived(client ? client.dsps5 * 2 ** 32 : 0);
|
const hs5m = $derived(client ? client.dsps5 * 2 ** 32 : 0);
|
||||||
@@ -88,7 +94,7 @@
|
|||||||
title="View per-user stats"
|
title="View per-user stats"
|
||||||
>{btcAddress}</button>
|
>{btcAddress}</button>
|
||||||
{#if tls}
|
{#if tls}
|
||||||
<span class="badge tls" title="Encrypted connection via TLS">
|
<span class="badge tls" title={conn.label}>
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H4a1.5 1.5 0 0 0-1.5 1.5v5A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 12 7h-.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6H6V4.5a2 2 0 1 1 4 0V7Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -351,4 +357,13 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.wname {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.detail-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -74,16 +74,5 @@ export function selectBestShare(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearSelection(): void {
|
export function clearSelection(): void {
|
||||||
if (
|
|
||||||
window.history.length > 1 &&
|
|
||||||
(window.location.hash.startsWith(USER_PREFIX) ||
|
|
||||||
window.location.hash.startsWith(WORKER_PREFIX) ||
|
|
||||||
window.location.hash === ACCELERATOR_HASH ||
|
|
||||||
window.location.hash === STATS_HASH ||
|
|
||||||
window.location.hash === BESTSHARE_HASH)
|
|
||||||
) {
|
|
||||||
window.history.back();
|
|
||||||
} else {
|
|
||||||
window.location.hash = "";
|
window.location.hash = "";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+21
-3
@@ -71,9 +71,10 @@ export type StratumClient = {
|
|||||||
workername: string;
|
workername: string;
|
||||||
userid: number;
|
userid: number;
|
||||||
// Index into ckpool's serverurl[] array; identifies which stratum
|
// Index into ckpool's serverurl[] array; identifies which stratum
|
||||||
// bind the client connected on. We use this to tag TLS clients:
|
// bind the client connected on. What each index *means* depends on
|
||||||
// index 0 is the public plaintext bind, index 1 is the loopback-only
|
// how the deployment rendered ckpool.conf, so resolve it through
|
||||||
// bind that stunnel forwards TLS traffic to.
|
// Snapshot.stratum_servers rather than hardcoding — see
|
||||||
|
// connectionOf() in format.ts.
|
||||||
server: number;
|
server: number;
|
||||||
bestdiff: number;
|
bestdiff: number;
|
||||||
};
|
};
|
||||||
@@ -112,6 +113,16 @@ export type HashratePoint = {
|
|||||||
v: number; // H/s
|
v: number; // H/s
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One entry per ckpool serverurl[] bind, declared by the deployment via
|
||||||
|
// the STRATUM_SERVERS env. `kind` is the stable tag the UI switches on;
|
||||||
|
// `label` is free text shown on hover.
|
||||||
|
export type StratumServerKind = "plain" | "tls-local" | "tls-public";
|
||||||
|
|
||||||
|
export type StratumServer = {
|
||||||
|
kind: StratumServerKind;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Snapshot = {
|
export type Snapshot = {
|
||||||
generated_at: string;
|
generated_at: string;
|
||||||
pool: PoolStats | null;
|
pool: PoolStats | null;
|
||||||
@@ -137,6 +148,10 @@ export type Snapshot = {
|
|||||||
// Optional override for explorer links. Empty/undefined means the
|
// Optional override for explorer links. Empty/undefined means the
|
||||||
// UI falls back to its mempool.space defaults.
|
// UI falls back to its mempool.space defaults.
|
||||||
mempool_base_url?: string;
|
mempool_base_url?: string;
|
||||||
|
// Describes ckpool's serverurl[] binds: entry N says what a client
|
||||||
|
// with `server === N` is connected over. Undefined when the
|
||||||
|
// deployment didn't declare them — see connectionOf() in format.ts.
|
||||||
|
stratum_servers?: StratumServer[];
|
||||||
// Submission attempt tracking — count of "Possible block solve"
|
// Submission attempt tracking — count of "Possible block solve"
|
||||||
// log lines vs "Solved and confirmed" lines. A growing gap means
|
// log lines vs "Solved and confirmed" lines. A growing gap means
|
||||||
// submissions are being rejected by bitcoind.
|
// submissions are being rejected by bitcoind.
|
||||||
@@ -147,6 +162,9 @@ export type Snapshot = {
|
|||||||
has_last_zmq_event: boolean;
|
has_last_zmq_event: boolean;
|
||||||
last_zmq_event_age?: number; // seconds
|
last_zmq_event_age?: number; // seconds
|
||||||
tip_changed_age: number; // seconds since tip height last changed
|
tip_changed_age: number; // seconds since tip height last changed
|
||||||
|
peer_count: number;
|
||||||
|
peers_in: number;
|
||||||
|
peers_out: number;
|
||||||
// Share counters (raw, 1 submission = 1 share).
|
// Share counters (raw, 1 submission = 1 share).
|
||||||
session_accepted: number;
|
session_accepted: number;
|
||||||
session_rejected: number;
|
session_rejected: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user