Cargo workspace plus a Vite frontend, following ~/git/architecture/generic.md. Every block header carries its author's wormhole reward preimage in a `pow_` PreRuntime digest, so authorship for the whole network is derivable from headers alone — no indexer, no registration, no way for a miner to be left out. That decoding, the hashrate maths and the telemetry name attribution live in blackbeard-core with no I/O at all, so the parts that are easy to get subtly wrong are exercised by unit tests rather than only against a live chain. The browser holds one WebSocket: snapshot on subscribe, deltas thereafter. The head stream is itself a push (chain_subscribeNewHeads), so a block reaches the page the moment the node imports it. Messages are serialised once per broadcast, and leaderboards are recomputed only for windows a socket is actually watching. No RxJS — useSyncExternalStore is React's own contract for this. Verified against the live Planck testnet: 12/12 headers decoded, telemetry names attributed (quanpool-planck, baba-gorchitsa, …), warm start restoring 84 blocks and 5 held names across a restart. Three findings worth recording, all in CLAUDE.md: - substrate-telemetry sends its JSON in *binary* frames. A text-only client connects, subscribes, reports healthy and receives nothing at all — and a Python probe hides it, because json.loads accepts bytes. - Difficulty is a little-endian U512; decoding it big-endian gives a number wrong by ~10^150 that still renders fine. - Planck's real block interval is ~13-15s against a 6s target with enormous variance, so a measured interval needs 20 tip samples before it is publishable. Deploy assets, the Gitea Actions workflow and script/infra-setup.sh are included; port 25864 is registered in architecture/port-allocations.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
48 lines
1.5 KiB
Rust
48 lines
1.5 KiB
Rust
//! Error types shared across the workspace.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use ts_rs::TS;
|
|
|
|
/// Errors raised by the types themselves — parsing a window name, a malformed
|
|
/// preimage. Deliberately small: this crate does no I/O, so it has few ways to
|
|
/// fail.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum EntityError {
|
|
/// A window name that is not one of the four.
|
|
#[error("unknown window `{0}` (expected hour, six_hours, day or week)")]
|
|
UnknownWindow(String),
|
|
|
|
/// A miner id that is not 0x-prefixed 32-byte hex.
|
|
#[error("`{0}` is not a 32-byte 0x-prefixed reward preimage")]
|
|
MalformedMinerId(String),
|
|
|
|
/// A chain name that is not configured.
|
|
#[error("no chain named `{0}` is configured")]
|
|
UnknownChain(String),
|
|
}
|
|
|
|
/// The JSON body every failing REST response carries.
|
|
///
|
|
/// Shared with the frontend so error rendering is not a guess about what the
|
|
/// backend emits.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
|
#[ts(export, export_to = "ApiError.ts")]
|
|
pub struct ApiError {
|
|
/// A stable machine-readable code (`unknown_chain`, `not_found`,
|
|
/// `chain_unavailable`). The frontend switches on this; `message` is for
|
|
/// humans and may be reworded at any time.
|
|
pub code: String,
|
|
/// Human-readable explanation.
|
|
pub message: String,
|
|
}
|
|
|
|
impl ApiError {
|
|
/// Build one.
|
|
pub fn new(code: &str, message: impl Into<String>) -> Self {
|
|
Self {
|
|
code: code.to_owned(),
|
|
message: message.into(),
|
|
}
|
|
}
|
|
}
|