Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions proto/sentrix.proto
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ service Sentrix {
// the pending-aware nonce behaviour from chain v2.1.57).
rpc GetBalance(GetBalanceRequest) returns (Account);

// v0.4 read-only state queries — drop the REST `/sentrix_status_extended`
// bridge from the explorer's stats hot path. All three are pure reads
// off `state.read()` snapshots; same lock contention profile as
// GetBlock/GetBalance.
//
// Active validator set + per-validator stake/active/jailed flags.
rpc GetValidatorSet(GetValidatorSetRequest) returns (ValidatorSet);
// Native-token supply snapshot (minted, burned, circulating).
rpc GetSupply(GetSupplyRequest) returns (Supply);
// Pending-tx count + a capped header window for UI display.
rpc GetMempool(GetMempoolRequest) returns (Mempool);

// Server-streaming chain events. Subscribe once, receive every event type
// until cancel or server restart. Replaces N separate eth_subscribe calls.
// Backpressure: server bounded channel per stream (capacity 4096 — same as
Expand Down Expand Up @@ -182,3 +194,64 @@ enum EventFilter {
EVENT_FILTER_VALIDATOR_SET = 3;
EVENT_FILTER_LOG = 4;
}

// ────────────────────────────────────────────────────────────
// v0.4 read-only state-query messages
// ────────────────────────────────────────────────────────────

message GetValidatorSetRequest {
// Reserved for at_height historical reads — gated on MDBX snapshot
// isolation (Refactor 5). v0.4 returns the latest finalized set.
optional BlockHeight at_height = 1;
}

message ValidatorSet {
uint32 epoch = 1;
uint32 active_count = 2;
uint32 total_count = 3;
// Total stake across the active set, in sentri (10^-8 SRX).
uint64 total_active_stake_sentri = 4;
repeated ValidatorEntry validators = 5;
}

message ValidatorEntry {
Address address = 1;
uint64 stake_sentri = 2;
bool active = 3;
bool jailed = 4;
}

message GetSupplyRequest {
optional BlockHeight at_height = 1;
}

message Supply {
// sentri = 10^-8 SRX. Cap is fork-gated (210M pre-fork, 315M post-
// tokenomics-v2 at h=640800); leave conversion to display layer.
uint64 minted_sentri = 1;
uint64 burned_sentri = 2;
uint64 circulating_sentri = 3; // = minted − burned
}

message GetMempoolRequest {
// Cap returned tx headers; default 100, max 500. Larger windows need
// pagination (deferred). 0 = use server default (100).
uint32 limit = 1;
}

message Mempool {
// Authoritative pending-tx count. `entries` is capped by `limit`;
// size always reflects the full mempool depth.
uint32 size = 1;
repeated MempoolEntry entries = 2;
}

message MempoolEntry {
Hash txid = 1;
Address from_address = 2;
Address to_address = 3;
Amount amount = 4;
Amount fee = 5;
uint64 nonce = 6;
uint32 tx_type = 7;
}
6 changes: 5 additions & 1 deletion src/components/navbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ pub fn Navbar() -> impl IntoView {
}

#[component]
fn MobileNavLink(href: &'static str, label_key: &'static str, close: RwSignal<bool>) -> impl IntoView {
fn MobileNavLink(
href: &'static str,
label_key: &'static str,
close: RwSignal<bool>,
) -> impl IntoView {
let lang = use_lang();
view! {
<a
Expand Down
24 changes: 4 additions & 20 deletions src/components/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,28 +134,12 @@ fn SupplyBar() -> impl IntoView {

#[cfg(target_arch = "wasm32")]
{
use crate::grpc::client::SentrixGrpcClient;
leptos::task::spawn_local(async move {
let endpoint = format!(
"{}/sentrix_status_extended",
crate::state::network::Network::from_host(
&web_sys::window()
.and_then(|w| w.location().host().ok())
.unwrap_or_default()
)
.rpc_url()
);
let mut client = SentrixGrpcClient::new(crate::GRPC_ENDPOINT);
loop {
if let Ok(resp) = gloo_net::http::Request::get(&endpoint).send().await {
if resp.ok() {
if let Ok(body) = resp.json::<serde_json::Value>().await {
if let Some(v) = body
.pointer("/supply/minted_sentri")
.and_then(serde_json::Value::as_u64)
{
minted_srx.set(v / 100_000_000);
}
}
}
if let Ok(supply) = client.get_supply().await {
minted_srx.set(supply.minted_sentri / 100_000_000);
}
crate::util::sleep_ms(5_000).await;
}
Expand Down
72 changes: 22 additions & 50 deletions src/components/stats_dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ async fn fetch_chain_stats(network: Network) -> Result<ChainStats, FetchError> {
})
}

/// Subset of `/sentrix_status_extended` the dashboard cares about.
/// `Default` returns zero-valued fields so a partial-failure path can
/// keep rendering the JSON-RPC-backed cards without short-circuiting.
/// Subset of fields the dashboard cares about. Backed by gRPC v0.4
/// `GetValidatorSet` + `GetMempool` — no JSON / REST involvement.
/// `Default` keeps the UI rendering zeros rather than blank cards if
/// any single call fails.
#[cfg(target_arch = "wasm32")]
#[derive(Default)]
struct SentrixStatusSubset {
Expand All @@ -100,52 +101,28 @@ struct SentrixStatusSubset {
}

#[cfg(target_arch = "wasm32")]
async fn fetch_sentrix_status(network: Network) -> Result<SentrixStatusSubset, FetchError> {
use gloo_net::http::Request;
use serde_json::Value;
async fn fetch_sentrix_status(_network: Network) -> Result<SentrixStatusSubset, FetchError> {
use crate::grpc::client::SentrixGrpcClient;

let url = format!("{}/sentrix_status_extended", network.rpc_url());
let resp = Request::get(&url)
.send()
let mut client = SentrixGrpcClient::new(crate::GRPC_ENDPOINT);

// Two parallel single-method round-trips beat one bigger query —
// each handler is a single `state.read()` snapshot on the chain
// side, contending for the same lock anyway. Sequential keeps
// futures::join macros out of the bundle.
let validators = client
.get_validator_set()
.await
.map_err(|e| FetchError::Rpc(format!("status: {e}")))?;
if !resp.ok() {
return Err(FetchError::Rpc(format!("status http {}", resp.status())));
}
let body: Value = resp
.json()
.map_err(|s| FetchError::Rpc(format!("validator_set: {}", s.message())))?;
let mempool = client
.get_mempool(0)
.await
.map_err(|e| FetchError::Rpc(format!("status decode: {e}")))?;

// Pull fields defensively — every one of these has a chain-side
// type guarantee, but a future field rename shouldn't crash the
// dashboard. Missing field == 0; UI shows "0 / 0" which is
// recognisable as "endpoint changed shape" without a panic.
let active_validators = body
.pointer("/validators/active_count")
.and_then(Value::as_u64)
.map(|n| u32::try_from(n).unwrap_or(u32::MAX))
.unwrap_or(0);

// `top` is capped at 7 by stake-rank server-side; for mainnet's
// current 4-validator set this equals the registered count. Once
// external validators push registered > 7, swap to a dedicated
// count field on the endpoint or a /staking/validators call.
let total_validators = body
.pointer("/validators/top")
.and_then(Value::as_array)
.map(|a| u32::try_from(a.len()).unwrap_or(u32::MAX))
.unwrap_or(0);

let mempool_pending = body
.pointer("/mempool/size")
.and_then(Value::as_u64)
.unwrap_or(0);
.map_err(|s| FetchError::Rpc(format!("mempool: {}", s.message())))?;

Ok(SentrixStatusSubset {
active_validators,
total_validators,
mempool_pending,
active_validators: validators.active_count,
total_validators: validators.total_count,
mempool_pending: u64::from(mempool.size),
})
}

Expand Down Expand Up @@ -343,12 +320,7 @@ enum Icon {
}

#[component]
fn StatCard(
label: &'static str,
value: String,
accent: bool,
icon: Icon,
) -> impl IntoView {
fn StatCard(label: &'static str, value: String, accent: bool, icon: Icon) -> impl IntoView {
let value_class = if accent {
"mt-2 font-mono text-2xl font-bold tabular-nums text-sentrix-gold"
} else {
Expand Down
27 changes: 26 additions & 1 deletion src/grpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use tonic_web_wasm_client::Client as WebClient;

use super::pb::{
get_block_request::Selector, sentrix_client::SentrixClient, BlockHeight, EventFilter,
GetBalanceRequest, GetBlockRequest, StreamEventsRequest,
GetBalanceRequest, GetBlockRequest, GetMempoolRequest, GetSupplyRequest,
GetValidatorSetRequest, Mempool, StreamEventsRequest, Supply, ValidatorSet,
};

/// The concrete client type after we've wired the wasm transport.
Expand Down Expand Up @@ -67,6 +68,30 @@ impl SentrixGrpcClient {
Ok(resp.into_inner())
}

/// v0.4 — full active set + jail/active flags + per-validator stake.
pub async fn get_validator_set(&mut self) -> Result<ValidatorSet, tonic::Status> {
let req = GetValidatorSetRequest { at_height: None };
let resp = self.inner.get_validator_set(req).await?;
Ok(resp.into_inner())
}

/// v0.4 — minted/burned/circulating supply snapshot.
pub async fn get_supply(&mut self) -> Result<Supply, tonic::Status> {
let req = GetSupplyRequest { at_height: None };
let resp = self.inner.get_supply(req).await?;
Ok(resp.into_inner())
}

/// v0.4 — pending-tx size + capped header window. `limit = 0` ⇒
/// server default (100). Pass a smaller limit for the dashboard
/// header card (just need `size`); pass max 500 for the mempool
/// panel that lists actual entries.
pub async fn get_mempool(&mut self, limit: u32) -> Result<Mempool, tonic::Status> {
let req = GetMempoolRequest { limit };
let resp = self.inner.get_mempool(req).await?;
Ok(resp.into_inner())
}

/// Server-streaming events. Returns a `Streaming<ChainEvent>` that the
/// caller drains with `.message().await`. Filter list is sent verbatim;
/// empty = subscribe-all (server-side filter).
Expand Down
Loading
Loading