feat: account flows, and a distribution table for mining rewards
Reported concern: one pool's balance is disproportionately large, and it is not
clear whether rewards are reaching miners. This makes the question answerable
from the chain rather than from impressions.
Measured on mainnet while building it, blocks 1–42,274:
account mined received sent recipients
QUANPOOL 10,171 5,207 0 0
qzomrwjT… 0 11,382 4,071 90
qzo1Pr3X… 0 3,875 7,528 986
qzo4QjZz… 0 1,257 2,448 742
qznbesUc… 0 1,600 0 0
Direction comes from each transfer event's own `from` and `to` — `Wormhole::
NativeTransferred` and `Balances::Transfer` both carry them — so every figure is
something the chain recorded.
**A nonce cannot answer this, and reaching for one is the obvious mistake.**
Wormhole transfers dispatch unsigned: `verify_private_batch` and
`verify_public_batch` have no signer at all. An address here can move a fortune
with a nonce of zero, and I nearly reported one as proof that nothing had ever
been sent.
The presentation reports flows and does not judge them, because the data cannot
settle what they mean. Two facts in the table above are why: several large
accounts send nothing — ordinary for a solo miner keeping what it earned — and
the accounts distributing to hundreds of recipients have never mined a block, so
payout wallets here are funded from somewhere other than mining addresses. There
is also no such thing as a "pool" on this chain: the word appears only in a name
its operator reports over telemetry, which `MinerFlow::attribution` carries so a
reader can weigh it.
What earns the feature its place is comparison. One miner's recipient count says
little; beside its neighbours' it says a great deal, and the reader draws the
conclusion rather than the site.
Refs #17
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
59
.sqlx/query-717abd3ad62607561855d8fbe6c4f79ef31cf33677396ddd6f2ecc356ff7b045.json
generated
Normal file
59
.sqlx/query-717abd3ad62607561855d8fbe6c4f79ef31cf33677396ddd6f2ecc356ff7b045.json
generated
Normal file
@@ -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"
|
||||
}
|
||||
53
.sqlx/query-fbede02f645d92cce9e02673db479234db0bcb647411773c6416bb072267f35d.json
generated
Normal file
53
.sqlx/query-fbede02f645d92cce9e02673db479234db0bcb647411773c6416bb072267f35d.json
generated
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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<AppState>,
|
||||
Path(chain): Path<String>,
|
||||
Query(query): Query<WindowQuery>,
|
||||
) -> Result<Json<Distribution>, Failure> {
|
||||
let runtime = state.chain(&chain).ok_or_else(|| unknown_chain(&chain))?;
|
||||
let id = runtime.id();
|
||||
let window = query.window()?;
|
||||
|
||||
let rows: Vec<LeaderboardRow> = 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<String> = 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.
|
||||
|
||||
@@ -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<AccountFlows, DataError> {
|
||||
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<std::collections::HashMap<String, AccountFlows>, 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`
|
||||
|
||||
@@ -69,6 +69,12 @@ pub struct AccountDetail {
|
||||
/// The highest block whose events have been read.
|
||||
#[ts(type = "number")]
|
||||
pub indexed_to: Option<u64>,
|
||||
/// 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<crate::AccountFlows>,
|
||||
}
|
||||
|
||||
/// One row of the accounts index: an account and what it has earned.
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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<BigUintDec>,
|
||||
/// What it holds now, read from chain state.
|
||||
pub balance: Option<BigUintDec>,
|
||||
/// Flows in and out.
|
||||
pub flows: Option<AccountFlows>,
|
||||
}
|
||||
|
||||
/// 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<MinerFlow>,
|
||||
/// Lowest block whose events this observer has read.
|
||||
#[ts(type = "number")]
|
||||
pub indexed_from: Option<u64>,
|
||||
/// Highest.
|
||||
#[ts(type = "number")]
|
||||
pub indexed_to: Option<u64>,
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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, };
|
||||
|
||||
45
web/src/api/generated/AccountFlows.ts
Normal file
45
web/src/api/generated/AccountFlows.ts
Normal file
@@ -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, };
|
||||
31
web/src/api/generated/Distribution.ts
Normal file
31
web/src/api/generated/Distribution.ts
Normal file
@@ -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<MinerFlow>,
|
||||
/**
|
||||
* Lowest block whose events this observer has read.
|
||||
*/
|
||||
indexed_from: number,
|
||||
/**
|
||||
* Highest.
|
||||
*/
|
||||
indexed_to: number, };
|
||||
48
web/src/api/generated/MinerFlow.ts
Normal file
48
web/src/api/generated/MinerFlow.ts
Normal file
@@ -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, };
|
||||
Reference in New Issue
Block a user