diff --git a/.sqlx/query-717abd3ad62607561855d8fbe6c4f79ef31cf33677396ddd6f2ecc356ff7b045.json b/.sqlx/query-717abd3ad62607561855d8fbe6c4f79ef31cf33677396ddd6f2ecc356ff7b045.json new file mode 100644 index 0000000..176db76 --- /dev/null +++ b/.sqlx/query-717abd3ad62607561855d8fbe6c4f79ef31cf33677396ddd6f2ecc356ff7b045.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "\n with moves as (\n select fields->>'from' as src,\n fields->>'to' as dst,\n (fields->>'amount')::numeric as amount\n from chain_event\n where chain = $1\n and accounts && $2::text[]\n and ( (pallet = 'Wormhole' and variant = 'NativeTransferred')\n or (pallet = 'Balances' and variant = 'Transfer'))\n ),\n wanted as (select unnest($2::text[]) as account)\n select w.account as \"account!\",\n coalesce((select sum(amount) from moves where src = w.account), 0) as \"sent!\",\n coalesce((select sum(amount) from moves where dst = w.account), 0) as \"received!\",\n (select count(*) from moves where src = w.account) as \"sends!\",\n (select count(*) from moves where dst = w.account) as \"receipts!\",\n (select count(distinct dst) from moves where src = w.account) as \"recipients!\",\n (select count(distinct src) from moves where dst = w.account) as \"senders!\"\n from wanted w\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "account!", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "sent!", + "type_info": "Numeric" + }, + { + "ordinal": 2, + "name": "received!", + "type_info": "Numeric" + }, + { + "ordinal": 3, + "name": "sends!", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "receipts!", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "recipients!", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "senders!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "TextArray" + ] + }, + "nullable": [ + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "717abd3ad62607561855d8fbe6c4f79ef31cf33677396ddd6f2ecc356ff7b045" +} diff --git a/.sqlx/query-fbede02f645d92cce9e02673db479234db0bcb647411773c6416bb072267f35d.json b/.sqlx/query-fbede02f645d92cce9e02673db479234db0bcb647411773c6416bb072267f35d.json new file mode 100644 index 0000000..d72b69e --- /dev/null +++ b/.sqlx/query-fbede02f645d92cce9e02673db479234db0bcb647411773c6416bb072267f35d.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "\n select\n coalesce(sum((fields->>'amount')::numeric)\n filter (where fields->>'from' = $2), 0) as \"sent!\",\n coalesce(sum((fields->>'amount')::numeric)\n filter (where fields->>'to' = $2), 0) as \"received!\",\n count(*) filter (where fields->>'from' = $2) as \"sends!\",\n count(*) filter (where fields->>'to' = $2) as \"receipts!\",\n count(distinct fields->>'to')\n filter (where fields->>'from' = $2) as \"recipients!\",\n count(distinct fields->>'from')\n filter (where fields->>'to' = $2) as \"senders!\"\n from chain_event\n where chain = $1\n and accounts @> array[$2]\n and ( (pallet = 'Wormhole' and variant = 'NativeTransferred')\n or (pallet = 'Balances' and variant = 'Transfer'))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sent!", + "type_info": "Numeric" + }, + { + "ordinal": 1, + "name": "received!", + "type_info": "Numeric" + }, + { + "ordinal": 2, + "name": "sends!", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "receipts!", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "recipients!", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "senders!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null, + null, + null, + null, + null, + null + ] + }, + "hash": "fbede02f645d92cce9e02673db479234db0bcb647411773c6416bb072267f35d" +} diff --git a/crates/blackbeard-api/src/routes.rs b/crates/blackbeard-api/src/routes.rs index 8a45b1c..e68f594 100644 --- a/crates/blackbeard-api/src/routes.rs +++ b/crates/blackbeard-api/src/routes.rs @@ -16,13 +16,14 @@ use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::{Json, Router}; use blackbeard_entities::{ - AccountDetail, AccountEvent, AccountRoleEntry, AccountRow, ActivitySource, ApiError, - BigUintDec, BlockDetail, CallIndex, CallSummary, ChainInfo, ChainRoles, ChainSeries, - ChainState, ChainSummary, DailyActivity, EventSummary, GenesisDetail, LeaderboardRow, - LockCounts, MinerDetail, MinerId, MinerSeriesPoint, NamedAccount, NetworkSummary, NodeActivity, - NodeIndex, NodeInfo, NodeRow, PendingTransfer, RecentBlock, ReversibleState, RewardSummary, - RoleSource, RuntimeConstant, RuntimeDetail, RuntimeField, RuntimePallet, - RuntimeSignedExtension, RuntimeStorage, RuntimeSummary, RuntimeVariant, StateEntry, Window, + AccountDetail, AccountEvent, AccountFlows, AccountRoleEntry, AccountRow, ActivitySource, + ApiError, BigUintDec, BlockDetail, CallIndex, CallSummary, ChainInfo, ChainRoles, ChainSeries, + ChainState, ChainSummary, DailyActivity, Distribution, EventSummary, GenesisDetail, + LeaderboardRow, LockCounts, MinerDetail, MinerFlow, MinerId, MinerSeriesPoint, NamedAccount, + NetworkSummary, NodeActivity, NodeIndex, NodeInfo, NodeRow, PendingTransfer, RecentBlock, + ReversibleState, RewardSummary, RoleSource, RuntimeConstant, RuntimeDetail, RuntimeField, + RuntimePallet, RuntimeSignedExtension, RuntimeStorage, RuntimeSummary, RuntimeVariant, + StateEntry, Window, }; use serde::{Deserialize, Serialize}; use tower_http::compression::CompressionLayer; @@ -56,6 +57,7 @@ pub fn router(state: AppState, allowed_origins: &[String]) -> Router { .route("/v1/chains/{chain}/blocks/{block}", get(block)) .route("/v1/chains/{chain}/miners/{miner}", get(miner)) .route("/v1/chains/{chain}/network", get(network)) + .route("/v1/chains/{chain}/distribution", get(distribution)) .route("/v1/chains/{chain}/nodes", get(nodes)) .route("/v1/chains/{chain}/nodes/{peer}", get(node)) .route("/v1/chains/{chain}/accounts", get(accounts)) @@ -784,6 +786,114 @@ async fn miner( })) } +/// Miners the distribution table covers. +/// +/// Enough that the long tail is visible beside the head: a single row reading +/// "78% of blocks, no recipients" means nothing without the rows under it +/// showing what everyone else did with theirs. +const DISTRIBUTION_ROWS: usize = 25; + +/// `GET /v1/chains/{chain}/distribution` +/// +/// Who won the blocks, and what became of the rewards. +/// +/// **Reports flows; does not judge them.** Direction comes from each transfer +/// event's own `from` and `to`, so every figure is something the chain +/// recorded. What the figures *mean* is a question the data cannot settle: an +/// operator may hold several addresses, may settle off-chain, or may simply be +/// a solo miner keeping what it earned — the ordinary case, and unremarkable. +/// The reason to assemble this at all is comparison: one miner's recipient +/// count says little alone and a great deal beside its neighbours'. +async fn distribution( + State(state): State, + Path(chain): Path, + Query(query): Query, +) -> Result, Failure> { + let runtime = state.chain(&chain).ok_or_else(|| unknown_chain(&chain))?; + let id = runtime.id(); + let window = query.window()?; + + let rows: Vec = runtime + .leaderboard(window, state.leaderboard_max_age) + .into_iter() + .take(DISTRIBUTION_ROWS) + .collect(); + + // The account each miner is paid at, derived from its preimage by the same + // function the chain uses to decide who to pay — so the join is the chain's + // own rather than a guess of ours. + let accounts: Vec = rows + .iter() + .filter_map(|r| { + r.address + .as_deref() + .and_then(blackbeard_core::wormhole::account_id) + }) + .collect(); + + let flows = state + .store + .flows_for(&id, &accounts) + .await + .map_err(database_unavailable)?; + let scan = state + .store + .event_scan(&id) + .await + .map_err(database_unavailable)?; + let mut miners = Vec::with_capacity(rows.len()); + for row in rows { + let account = row + .address + .as_deref() + .and_then(blackbeard_core::wormhole::account_id); + // Keyed by the account, not the preimage: rewards are recorded against + // whoever the chain paid. + let mined = match &account { + Some(account) => state + .store + .account_rewards(&id, account) + .await + .ok() + .map(|r| BigUintDec(r.total)), + None => None, + }; + let balance = match &account { + Some(account) => read_balance(&runtime, account).await.0, + None => None, + }; + miners.push(MinerFlow { + display: row.display.clone(), + attribution: row.attribution, + blocks: row.blocks, + share: row.share, + mined, + balance, + flows: account + .as_ref() + .and_then(|a| flows.get(a)) + .map(|f| AccountFlows { + sent: BigUintDec(f.sent.clone()), + received: BigUintDec(f.received.clone()), + sends: f.sends, + receipts: f.receipts, + recipients: f.recipients, + senders: f.senders, + }), + address: row.address, + miner: row.miner, + }); + } + + Ok(Json(Distribution { + chain: id, + window_blocks: window.blocks(), + miners, + indexed_from: scan.map(|s| s.0), + indexed_to: scan.map(|s| s.1), + })) +} + /// Days of activity the network page reports. const NETWORK_DAYS: i32 = 30; @@ -2385,8 +2495,26 @@ async fn account( } }); + // What it has received and sent on. Facts, not a verdict — an account that + // has sent nothing is the ordinary case for a miner keeping what it earned, + // and the figure earns its place by being comparable rather than damning. + let flows = state + .store + .account_flows(&id, &account) + .await + .ok() + .map(|f| AccountFlows { + sent: BigUintDec(f.sent), + received: BigUintDec(f.received), + sends: f.sends, + receipts: f.receipts, + recipients: f.recipients, + senders: f.senders, + }); + Ok(Json(AccountDetail { chain: id, + flows, // Re-encoded rather than echoed: the answer is about the account, and // returning the caller's spelling of it would let two spellings of one // address look like two accounts. diff --git a/crates/blackbeard-data/src/store.rs b/crates/blackbeard-data/src/store.rs index 68740e4..6e7b092 100644 --- a/crates/blackbeard-data/src/store.rs +++ b/crates/blackbeard-data/src/store.rs @@ -60,6 +60,24 @@ pub struct StoreConfig { pub max_connections: u32, } +/// What an account has received and sent, over the indexed range. +#[derive(Debug, Clone, PartialEq)] +pub struct AccountFlows { + /// Total sent, smallest unit, as a decimal string. + pub sent: String, + /// Total received. + pub received: String, + /// Transfers out. + pub sends: u64, + /// Transfers in. + pub receipts: u64, + /// Distinct addresses it has sent to. The number that separates an account + /// distributing to many from one moving funds to itself. + pub recipients: u64, + /// Distinct addresses it has received from. + pub senders: u64, +} + /// One day of a chain's activity. /// /// Every count is over the chain's own clock and over whatever this observer @@ -1954,6 +1972,121 @@ impl Store { })) } + /// What one account has received and what it has sent on. + /// + /// Summed from the transfer events themselves — `Wormhole::NativeTransferred` + /// and `Balances::Transfer` both carry `from`, `to` and `amount`, so + /// direction is a fact in the payload rather than something inferred from + /// an account appearing nearby. + /// + /// **A nonce cannot answer this question on this chain.** Wormhole + /// transfers are dispatched unsigned — `verify_private_batch` and + /// `verify_public_batch` have no signer — so an address can move a great + /// deal while its nonce stays at zero. Reading a zero nonce as "has never + /// sent anything" would be badly wrong, and it is the obvious mistake to + /// make here. + /// + /// Scoped to the indexed range like everything else derived from our own + /// tables, and the caller has to say so. + pub async fn account_flows( + &self, + chain: &ChainId, + account: &str, + ) -> Result { + let row = sqlx::query!( + r#" + select + coalesce(sum((fields->>'amount')::numeric) + filter (where fields->>'from' = $2), 0) as "sent!", + coalesce(sum((fields->>'amount')::numeric) + filter (where fields->>'to' = $2), 0) as "received!", + count(*) filter (where fields->>'from' = $2) as "sends!", + count(*) filter (where fields->>'to' = $2) as "receipts!", + count(distinct fields->>'to') + filter (where fields->>'from' = $2) as "recipients!", + count(distinct fields->>'from') + filter (where fields->>'to' = $2) as "senders!" + from chain_event + where chain = $1 + and accounts @> array[$2] + and ( (pallet = 'Wormhole' and variant = 'NativeTransferred') + or (pallet = 'Balances' and variant = 'Transfer')) + "#, + chain.as_str(), + account, + ) + .fetch_one(&self.pool) + .await?; + + Ok(AccountFlows { + sent: row.sent.to_string(), + received: row.received.to_string(), + sends: row.sends as u64, + receipts: row.receipts as u64, + recipients: row.recipients as u64, + senders: row.senders as u64, + }) + } + + /// Flows for many accounts at once. + /// + /// One query rather than one per row: a distribution table asks the same + /// question of twenty accounts, and twenty round trips to answer it would + /// make the page the slowest on the site. + pub async fn flows_for( + &self, + chain: &ChainId, + accounts: &[String], + ) -> Result, DataError> { + if accounts.is_empty() { + return Ok(std::collections::HashMap::new()); + } + let rows = sqlx::query!( + r#" + with moves as ( + select fields->>'from' as src, + fields->>'to' as dst, + (fields->>'amount')::numeric as amount + from chain_event + where chain = $1 + and accounts && $2::text[] + and ( (pallet = 'Wormhole' and variant = 'NativeTransferred') + or (pallet = 'Balances' and variant = 'Transfer')) + ), + wanted as (select unnest($2::text[]) as account) + select w.account as "account!", + coalesce((select sum(amount) from moves where src = w.account), 0) as "sent!", + coalesce((select sum(amount) from moves where dst = w.account), 0) as "received!", + (select count(*) from moves where src = w.account) as "sends!", + (select count(*) from moves where dst = w.account) as "receipts!", + (select count(distinct dst) from moves where src = w.account) as "recipients!", + (select count(distinct src) from moves where dst = w.account) as "senders!" + from wanted w + "#, + chain.as_str(), + accounts, + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows + .into_iter() + .map(|r| { + ( + r.account, + AccountFlows { + sent: r.sent.to_string(), + received: r.received.to_string(), + sends: r.sends as u64, + receipts: r.receipts as u64, + recipients: r.recipients as u64, + senders: r.senders as u64, + }, + ) + }) + .collect()) + } + /// One day of the chain's activity, from this observer's index. /// /// Bucketed by the *chain's* clock — `chain_extrinsic.at` and `block.authored_at` diff --git a/crates/blackbeard-entities/src/account.rs b/crates/blackbeard-entities/src/account.rs index 920d877..f380d60 100644 --- a/crates/blackbeard-entities/src/account.rs +++ b/crates/blackbeard-entities/src/account.rs @@ -69,6 +69,12 @@ pub struct AccountDetail { /// The highest block whose events have been read. #[ts(type = "number")] pub indexed_to: Option, + /// What this account has received and sent on, over the indexed range. + /// + /// Facts rather than a verdict — see [`crate::AccountFlows`]. An account + /// that has sent nothing is the ordinary case for a miner keeping what it + /// earned; the figure is worth showing because it is worth *comparing*. + pub flows: Option, } /// One row of the accounts index: an account and what it has earned. diff --git a/crates/blackbeard-entities/src/chain.rs b/crates/blackbeard-entities/src/chain.rs index ed1cfe7..6563475 100644 --- a/crates/blackbeard-entities/src/chain.rs +++ b/crates/blackbeard-entities/src/chain.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::BigUintDec; +use crate::{AttributionSource, BigUintDec, MinerId}; /// Short, stable, URL-safe name for a chain (`planck`, `quantus`). /// @@ -327,3 +327,88 @@ pub struct DailyActivity { #[ts(type = "number")] pub new_accounts: u64, } + +/// What one account has received and sent on, over the indexed range. +/// +/// **Facts, not a verdict.** Direction comes from the transfer events' +/// own `from` and `to` fields, so these are what the chain recorded. What they +/// cannot tell you is what the numbers *mean*: an operator may hold several +/// addresses, may settle off-chain, or may simply be a solo miner keeping what +/// it earned — which is the ordinary case and not remarkable in the slightest. +/// +/// A nonce cannot substitute for any of this. Wormhole transfers dispatch +/// unsigned, so an address on this chain can move a fortune with a nonce of +/// zero. +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "AccountFlows.ts")] +pub struct AccountFlows { + /// Total sent, smallest unit. + pub sent: BigUintDec, + /// Total received. + pub received: BigUintDec, + /// Transfers out. + #[ts(type = "number")] + pub sends: u64, + /// Transfers in. + #[ts(type = "number")] + pub receipts: u64, + /// Distinct addresses it has sent to. + /// + /// The number that separates an account distributing to many from one that + /// has moved nothing — and the one figure here a reader can compare across + /// accounts without knowing anything about any of them. + #[ts(type = "number")] + pub recipients: u64, + /// Distinct addresses it has received from. + #[ts(type = "number")] + pub senders: u64, +} + +/// One miner's share of the chain's mining, and what it has done with it. +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "MinerFlow.ts")] +pub struct MinerFlow { + /// The reward preimage. + pub miner: MinerId, + /// The address the chain pays it at. + pub address: Option, + /// What to call it. **Self-reported over telemetry, or the address** — the + /// chain asserts no names. A name containing "pool" is a claim its operator + /// made about itself and nothing more. + pub display: String, + /// How that name was arrived at, so a reader can weigh it. + pub attribution: AttributionSource, + /// Blocks won in the window. + #[ts(type = "number")] + pub blocks: u32, + /// Its share of the window's blocks, 0.0–1.0. + pub share: f64, + /// What the chain has paid it in rewards, over the indexed range. + pub mined: Option, + /// What it holds now, read from chain state. + pub balance: Option, + /// Flows in and out. + pub flows: Option, +} + +/// How the chain's mining is shared, and where the rewards went. +/// +/// Assembled so the comparison is possible at all: one miner's recipient count +/// means little alone, and means a great deal beside its neighbours'. +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "Distribution.ts")] +pub struct Distribution { + /// Which chain. + pub chain: ChainId, + /// The window the shares were computed over, in blocks. + #[ts(type = "number")] + pub window_blocks: u32, + /// Miners, most blocks first. + pub miners: Vec, + /// Lowest block whose events this observer has read. + #[ts(type = "number")] + pub indexed_from: Option, + /// Highest. + #[ts(type = "number")] + pub indexed_to: Option, +} diff --git a/crates/blackbeard-entities/src/lib.rs b/crates/blackbeard-entities/src/lib.rs index 35ed5bc..9548f56 100644 --- a/crates/blackbeard-entities/src/lib.rs +++ b/crates/blackbeard-entities/src/lib.rs @@ -32,8 +32,8 @@ pub use account::{AccountDetail, AccountEvent, AccountRow, ActivitySource, Rewar pub use block::{BlockDetail, BlockEvent, BlockExtrinsic, BlockObservation, RecentBlock}; pub use call::{CallIndex, CallSummary, EventSummary}; pub use chain::{ - ChainId, ChainInfo, ChainStatus, ChainSummary, ClientVersion, DailyActivity, LockCounts, - NetworkSummary, Tracking, + AccountFlows, ChainId, ChainInfo, ChainStatus, ChainSummary, ClientVersion, DailyActivity, + Distribution, LockCounts, MinerFlow, NetworkSummary, Tracking, }; pub use error::{ApiError, EntityError}; pub use miner::{ diff --git a/web/src/api/generated/AccountDetail.ts b/web/src/api/generated/AccountDetail.ts index 150247b..d210fe1 100644 --- a/web/src/api/generated/AccountDetail.ts +++ b/web/src/api/generated/AccountDetail.ts @@ -1,5 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AccountEvent } from "./AccountEvent"; +import type { AccountFlows } from "./AccountFlows"; import type { AccountRoleEntry } from "./AccountRoleEntry"; import type { BigUintDec } from "./BigUintDec"; import type { ChainId } from "./ChainId"; @@ -80,4 +81,12 @@ indexed_from: number, /** * The highest block whose events have been read. */ -indexed_to: number, }; +indexed_to: number, +/** + * What this account has received and sent on, over the indexed range. + * + * Facts rather than a verdict — see [`crate::AccountFlows`]. An account + * that has sent nothing is the ordinary case for a miner keeping what it + * earned; the figure is worth showing because it is worth *comparing*. + */ +flows: AccountFlows | null, }; diff --git a/web/src/api/generated/AccountFlows.ts b/web/src/api/generated/AccountFlows.ts new file mode 100644 index 0000000..c242e9f --- /dev/null +++ b/web/src/api/generated/AccountFlows.ts @@ -0,0 +1,45 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BigUintDec } from "./BigUintDec"; + +/** + * What one account has received and sent on, over the indexed range. + * + * **Facts, not a verdict.** Direction comes from the transfer events' + * own `from` and `to` fields, so these are what the chain recorded. What they + * cannot tell you is what the numbers *mean*: an operator may hold several + * addresses, may settle off-chain, or may simply be a solo miner keeping what + * it earned — which is the ordinary case and not remarkable in the slightest. + * + * A nonce cannot substitute for any of this. Wormhole transfers dispatch + * unsigned, so an address on this chain can move a fortune with a nonce of + * zero. + */ +export type AccountFlows = { +/** + * Total sent, smallest unit. + */ +sent: BigUintDec, +/** + * Total received. + */ +received: BigUintDec, +/** + * Transfers out. + */ +sends: number, +/** + * Transfers in. + */ +receipts: number, +/** + * Distinct addresses it has sent to. + * + * The number that separates an account distributing to many from one that + * has moved nothing — and the one figure here a reader can compare across + * accounts without knowing anything about any of them. + */ +recipients: number, +/** + * Distinct addresses it has received from. + */ +senders: number, }; diff --git a/web/src/api/generated/Distribution.ts b/web/src/api/generated/Distribution.ts new file mode 100644 index 0000000..41307b0 --- /dev/null +++ b/web/src/api/generated/Distribution.ts @@ -0,0 +1,31 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ChainId } from "./ChainId"; +import type { MinerFlow } from "./MinerFlow"; + +/** + * How the chain's mining is shared, and where the rewards went. + * + * Assembled so the comparison is possible at all: one miner's recipient count + * means little alone, and means a great deal beside its neighbours'. + */ +export type Distribution = { +/** + * Which chain. + */ +chain: ChainId, +/** + * The window the shares were computed over, in blocks. + */ +window_blocks: number, +/** + * Miners, most blocks first. + */ +miners: Array, +/** + * Lowest block whose events this observer has read. + */ +indexed_from: number, +/** + * Highest. + */ +indexed_to: number, }; diff --git a/web/src/api/generated/MinerFlow.ts b/web/src/api/generated/MinerFlow.ts new file mode 100644 index 0000000..3e78884 --- /dev/null +++ b/web/src/api/generated/MinerFlow.ts @@ -0,0 +1,48 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AccountFlows } from "./AccountFlows"; +import type { AttributionSource } from "./AttributionSource"; +import type { BigUintDec } from "./BigUintDec"; +import type { MinerId } from "./MinerId"; + +/** + * One miner's share of the chain's mining, and what it has done with it. + */ +export type MinerFlow = { +/** + * The reward preimage. + */ +miner: MinerId, +/** + * The address the chain pays it at. + */ +address: string | null, +/** + * What to call it. **Self-reported over telemetry, or the address** — the + * chain asserts no names. A name containing "pool" is a claim its operator + * made about itself and nothing more. + */ +display: string, +/** + * How that name was arrived at, so a reader can weigh it. + */ +attribution: AttributionSource, +/** + * Blocks won in the window. + */ +blocks: number, +/** + * Its share of the window's blocks, 0.0–1.0. + */ +share: number, +/** + * What the chain has paid it in rewards, over the indexed range. + */ +mined: BigUintDec | null, +/** + * What it holds now, read from chain state. + */ +balance: BigUintDec | null, +/** + * Flows in and out. + */ +flows: AccountFlows | null, };