From 0e5d2e12310ecbaef0fcd20b92200207afeed993 Mon Sep 17 00:00:00 2001 From: satyakwok Date: Tue, 28 Apr 2026 23:20:25 +0200 Subject: [PATCH 1/4] feat(scan): Sourcify verification badge on address page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add lib/sourcify.ts hook + components/common/SourcifyBadge.tsx that queries https://verify.sentrixchain.com/files/any// to detect contract source verification status: - "perfect" → green "verified" badge linking to Sourcify - "partial" → amber "partial match" badge - "none" → hidden by default (no noise on EOAs/unverified contracts) Wired into [locale]/address/[addr]/page.tsx PageHeader actions next to the existing label kind chip. Self-hosted Sourcify deployed on vps4 supports both mainnet (7119) and testnet (7120). All 8 canonical contract deployments (WSRX, Multicall3, TokenFactory, SentrixSafe across both chains) verified with perfect bytecode match. Tested: typecheck clean, build OK, scan service restarted. --- .../scan/app/[locale]/address/[addr]/page.tsx | 23 +++--- apps/scan/components/common/SourcifyBadge.tsx | 70 ++++++++++++++++++ apps/scan/lib/sourcify.ts | 71 +++++++++++++++++++ 3 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 apps/scan/components/common/SourcifyBadge.tsx create mode 100644 apps/scan/lib/sourcify.ts diff --git a/apps/scan/app/[locale]/address/[addr]/page.tsx b/apps/scan/app/[locale]/address/[addr]/page.tsx index bb0725c..b6045c2 100644 --- a/apps/scan/app/[locale]/address/[addr]/page.tsx +++ b/apps/scan/app/[locale]/address/[addr]/page.tsx @@ -20,6 +20,7 @@ import { formatSRX, formatNumber } from "@/lib/format"; import { Link } from "@/i18n/navigation"; import { useAddressLabel, toneForKind } from "@/lib/labels"; import { AddressNote } from "@/components/common/AddressNote"; +import { SourcifyBadge } from "@/components/common/SourcifyBadge"; import { downloadCsv } from "@/lib/csv"; import { toMillis } from "@/lib/format"; @@ -48,14 +49,20 @@ export default function AddressDetailPage({ params }: { params: Promise<{ addr: icon={Wallet} eyebrow="Address" title={label?.name ?? "Account"} - actions={label ? (() => { - const tone = toneForKind(label.kind); - return ( - - {label.kind} - - ); - })() : undefined} + actions={ +
+ {label && (() => { + const tone = toneForKind(label.kind); + return ( + + {label.kind} + + ); + })()} + {/* Sourcify verification badge — only meaningful for contract addresses, but harmless on EOAs (returns "unverified" badge) */} + +
+ } /> {/* Address bar */} diff --git a/apps/scan/components/common/SourcifyBadge.tsx b/apps/scan/components/common/SourcifyBadge.tsx new file mode 100644 index 0000000..662d343 --- /dev/null +++ b/apps/scan/components/common/SourcifyBadge.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { ShieldCheck, ShieldAlert } from "lucide-react"; +import { useSourcifyStatus, sourcifyContractUrl } from "@/lib/sourcify"; +import type { NetworkId } from "@/lib/chain"; + +interface SourcifyBadgeProps { + network: NetworkId; + address: string; + /** If false, the component renders nothing. Default true. */ + show?: boolean; + /** If true, also renders a quiet "unverified" badge when not verified. Default false (hides on EOAs / unverified contracts to avoid noise). */ + showUnverified?: boolean; +} + +/** Sourcify verification badge. Reads /files/any from the self-hosted Sourcify server. + * - perfect: bytecode + metadata match + * - partial: bytecode matches but metadata differs (e.g. compiler optimization) + * - none: not verified + */ +export function SourcifyBadge({ network, address, show = true, showUnverified = false }: SourcifyBadgeProps) { + const { match, loading } = useSourcifyStatus(network, address); + + if (!show) return null; + if (loading) return null; // hide silently while loading; render once we know + + // Hide entirely on EOAs / unverified contracts to avoid noise. Caller can opt into showing. + if (match === "none" && !showUnverified) return null; + + if (match === "perfect") { + return ( + + + verified + + ); + } + + if (match === "partial") { + return ( + + + partial match + + ); + } + + // not verified — quiet badge + return ( + + + unverified + + ); +} diff --git a/apps/scan/lib/sourcify.ts b/apps/scan/lib/sourcify.ts new file mode 100644 index 0000000..00fefcc --- /dev/null +++ b/apps/scan/lib/sourcify.ts @@ -0,0 +1,71 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { NetworkId } from "./chain"; + +export type SourcifyMatch = "perfect" | "partial" | "none"; + +const SOURCIFY_URL = "https://verify.sentrixchain.com"; + +const CHAIN_FOR_NETWORK: Record = { + mainnet: "7119", + testnet: "7120", +}; + +/** Hook: query Sourcify for verification status of a contract. + * Returns "perfect" if bytecode matches source 1:1 + metadata, + * "partial" if bytecode matches but metadata differs, + * "none" if not verified or unreachable. + */ +export function useSourcifyStatus(network: NetworkId, address: string | undefined): { + match: SourcifyMatch; + loading: boolean; +} { + const [match, setMatch] = useState("none"); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!address) { + setLoading(false); + return; + } + let cancelled = false; + const chain = CHAIN_FOR_NETWORK[network]; + + (async () => { + try { + // /files/any returns "full" / "partial" / 404 + const res = await fetch(`${SOURCIFY_URL}/files/any/${chain}/${address}`, { + signal: AbortSignal.timeout(5000), + }); + if (cancelled) return; + + if (res.ok) { + const body = await res.json(); + const status = body?.status; + if (status === "full") setMatch("perfect"); + else if (status === "partial") setMatch("partial"); + else setMatch("none"); + } else { + setMatch("none"); + } + } catch { + if (!cancelled) setMatch("none"); + } finally { + if (!cancelled) setLoading(false); + } + })(); + + return () => { + cancelled = true; + }; + }, [network, address]); + + return { match, loading }; +} + +/** External link to Sourcify verification page for a contract */ +export function sourcifyContractUrl(network: NetworkId, address: string): string { + const chain = CHAIN_FOR_NETWORK[network]; + return `${SOURCIFY_URL}/files/any/${chain}/${address}`; +} From 08a15ae9518d1d61d0052f3cdbac74e398513bdc Mon Sep 17 00:00:00 2001 From: satyakwok Date: Thu, 30 Apr 2026 02:05:55 +0200 Subject: [PATCH 2/4] feat(scan): supply / forks / epochs pages + finality + rail badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new top-level pages aimed at "explorer that listing teams + delegators + technical reviewers can read top-to-bottom and understand". Same data sources the rest of the explorer already uses (no new backend endpoints required for Phase 1): - /supply — single canonical SRX breakdown URL. Headline stat row, donut distribution (circulating / premine / bonded / remaining-to-mint / burnt), per-wallet premine breakdown with addresses pulled from the existing canonical-addresses registry, protocol-sentinel addresses called out. Pulls live values via useStats + useValidators so it reflects real chain state without baking magic numbers into the page. - /forks — fork-activation timeline. Source of truth in lib/forks/registry.ts, hand-synced against U64_MAX_FORK_GATES.md. Each fork gets a card with state (active / scheduled / dormant) + activation height + a non-technical "what changed when" description. Cross-network comparison table at the bottom so drift between mainnet + testnet is visible at a glance. Includes the 2026-04-30 JAIL_CONSENSUS activation (mainnet h=950400, testnet h=1030500) and the deliberately-dormant NFT_TOKENOP_HEIGHT (DO NOT activate marker). - /epochs — current-epoch detail with progress bar (blocks-into-epoch %), total bonded, rewards accrued. Past 12 epochs derived locally from the current epoch_number + EPOCH_LENGTH=28800; entries deep-link to the first / last block of each epoch. Footnote explains what an epoch does (validator-set rotation, reward settlement, slashing window, jail evidence boundary). Two new badge components for the deeper tx/block detail pages we'll wire in next: - RailBadge — distinguishes Sentrix's four transaction rails (EVM call / native SRX transfer / SRC-20 native TokenOp / native StakingOp). Sentrix is one of the only chains where SRC-20 lives at the protocol level alongside ERC-20, so users need to be told which rail they're looking at; classifyRail(tx) helper centralises the heuristic. - FinalityBadge — Sentrix BFT finality ladder (Pending → Included → Justified → Finalized). The "Justified" / "Finalized" distinction is what exchanges and bridges should pin, and no other EVM explorer surfaces it because no other EVM chain has 2/3+1 stake-weighted precommit-supermajority finality at 1s blocks. classifyFinality() helper computes the state from txBlockHeight + latestHeight + hasJustification. Nav: /supply added to the top header ("Supply"); /epochs + /forks linked from the footer Explorer column. i18n nav.supply added in en/id. `pnpm typecheck` clean across the workspace; lint reports only existing warnings unrelated to this PR. --- apps/scan/app/[locale]/epochs/page.tsx | 212 ++++++++++++++ apps/scan/app/[locale]/forks/page.tsx | 148 ++++++++++ apps/scan/app/[locale]/supply/donut.tsx | 58 ++++ apps/scan/app/[locale]/supply/page.tsx | 269 ++++++++++++++++++ apps/scan/components/common/FinalityBadge.tsx | 109 +++++++ apps/scan/components/common/RailBadge.tsx | 101 +++++++ apps/scan/components/layout/footer.tsx | 3 + apps/scan/components/layout/header.tsx | 3 +- apps/scan/lib/forks/registry.ts | 155 ++++++++++ apps/scan/messages/en.json | 5 +- apps/scan/messages/id.json | 5 +- 11 files changed, 1063 insertions(+), 5 deletions(-) create mode 100644 apps/scan/app/[locale]/epochs/page.tsx create mode 100644 apps/scan/app/[locale]/forks/page.tsx create mode 100644 apps/scan/app/[locale]/supply/donut.tsx create mode 100644 apps/scan/app/[locale]/supply/page.tsx create mode 100644 apps/scan/components/common/FinalityBadge.tsx create mode 100644 apps/scan/components/common/RailBadge.tsx create mode 100644 apps/scan/lib/forks/registry.ts diff --git a/apps/scan/app/[locale]/epochs/page.tsx b/apps/scan/app/[locale]/epochs/page.tsx new file mode 100644 index 0000000..7e5a39b --- /dev/null +++ b/apps/scan/app/[locale]/epochs/page.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useMemo } from "react"; +import { Link } from "@/i18n/navigation"; +import { CalendarRange, Layers, Coins, Users } from "lucide-react"; +import { PageHeader } from "@/components/common/PageHeader"; +import { DetailCard } from "@/components/common/DetailCard"; +import { InfoRow } from "@/components/common/InfoRow"; +import { StatCard } from "@/components/common/StatCard"; +import { useNetwork } from "@/lib/network-context"; +import { useStats, useCurrentEpoch, useValidators } from "@/lib/hooks"; +import { formatNumber, formatSRX } from "@/lib/format"; + +// Same constant as `crates/sentrix-staking/src/epoch.rs`. Epochs are 28,800 +// blocks long — at 1s blocks that's exactly 8 hours — so we can derive every +// past epoch's height range from a single counter without a dedicated API. +const EPOCH_LENGTH = 28_800; +const PAST_TO_SHOW = 12; + +// DECISION: Epoch view is the primary unit for staking analytics — rewards +// are settled at boundaries, validator set rotates at boundaries, slashing +// looks at the last full epoch's missed-blocks counter. This page surfaces: +// +// - the current epoch (live — what's happening right now) +// - a tail of recent past epochs with their height ranges so a user can +// deep-link to "blocks produced in epoch N" via the list page +// - validator set count + total stake at the current epoch boundary +// +// No new backend endpoint required: `/epoch/current` already exists and +// /validators carries enough to render the active set count. Past epochs +// are derived locally from epoch_number — historical epoch payouts will +// require an indexer-side endpoint we haven't built yet (the indexer +// scaffold has the schema; renderer wires up once that ships). + +export default function EpochsPage() { + const { network } = useNetwork(); + const { data: stats } = useStats(network); + const { data: epoch } = useCurrentEpoch(network); + const { data: validators } = useValidators(network); + + const totalBonded = useMemo(() => { + if (!validators) return 0; + return validators.reduce((s, v) => s + (v.stake ?? 0), 0); + }, [validators]); + + const past = useMemo>(() => { + if (!epoch) return []; + const out: Array<{ n: number; start: number; end: number }> = []; + for (let i = 1; i <= PAST_TO_SHOW; i++) { + const n = epoch.epoch_number - i; + if (n < 0) break; + const start = n * EPOCH_LENGTH; + const end = start + EPOCH_LENGTH - 1; + out.push({ n, start, end }); + } + return out; + }, [epoch]); + + const currentBlocksIn = stats && epoch ? stats.height - epoch.start_height + 1 : 0; + const currentProgressPct = epoch + ? Math.min(100, Math.max(0, (currentBlocksIn / EPOCH_LENGTH) * 100)) + : 0; + + return ( +
+ + + {/* ── Headline stats ─────────────────────────── */} +
+ + + + +
+ + {/* ── Current-epoch detail ──────────────────── */} + + {epoch && stats ? ( + <> + + + {epoch.start_height.toLocaleString()} →{" "} + {epoch.end_height.toLocaleString()} + + } + hint={`${currentBlocksIn.toLocaleString()} of ${EPOCH_LENGTH.toLocaleString()} blocks produced — ${currentProgressPct.toFixed(1)}% complete.`} + /> +
+
+
+
+
+ + + + + ) : ( +
Loading…
+ )} + + + {/* ── Past epochs ──────────────────────────── */} + + Past {PAST_TO_SHOW} epochs + + } + > +
+ + + + + + + + + + + {past.map((e) => ( + + + + + + + ))} + {past.length === 0 && ( + + )} + +
EpochFirst blockLast blockBlocks
#{e.n} + + {e.start.toLocaleString()} + + + + {e.end.toLocaleString()} + + {EPOCH_LENGTH.toLocaleString()}
No past epochs yet.
+
+
+ + {/* ── Footnote ──────────────────────────────── */} + +
+

+ An epoch is the basic unit Sentrix uses for everything time-bucketed: validator-set + rotation, reward settlement, slashing windows, and the consensus-jail boundary + check (post-fork). At the end of every epoch the active validator set is + recomputed, accrued rewards are pushed to delegators' claim balances, and + jail evidence (if any) is dispatched as a system transaction. +

+

+ Length is fixed at 28,800 blocks (≈ 8 hours at + our 1-second block target), defined in{" "} + crates/sentrix-staking/src/epoch.rs. +

+
+
+
+ ); +} diff --git a/apps/scan/app/[locale]/forks/page.tsx b/apps/scan/app/[locale]/forks/page.tsx new file mode 100644 index 0000000..0c44b90 --- /dev/null +++ b/apps/scan/app/[locale]/forks/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { GitFork, CheckCircle2, AlertTriangle, Clock } from "lucide-react"; +import { PageHeader } from "@/components/common/PageHeader"; +import { DetailCard } from "@/components/common/DetailCard"; +import { useNetwork } from "@/lib/network-context"; +import { useStats } from "@/lib/hooks"; +import { FORKS, forkStateAt, type ForkEntry } from "@/lib/forks/registry"; +import { formatNumber } from "@/lib/format"; +import { cn } from "@/lib/utils"; + +// DECISION: a fork-history page that listing teams, security reviewers, and +// curious users can read top-to-bottom and understand "what changed when". +// Two viewing modes share one source of truth (`lib/forks/registry.ts`): +// +// - Top section: a per-fork card on the active network with current status +// (active vs scheduled vs dormant) and the activation height. +// - Bottom section: cross-network comparison table so Satya can spot drift +// between mainnet and testnet at a glance. +// +// Source of truth lives in `founder-private/U64_MAX_FORK_GATES.md`. The +// registry below is hand-synced any time we ship a new fork. + +export default function ForksPage() { + const { network } = useNetwork(); + const { data: stats } = useStats(network); + const height = stats?.height ?? 0; + + const sorted = [...FORKS].sort((a, b) => { + const ah = a.heights[network]; + const bh = b.heights[network]; + if (ah == null && bh == null) return 0; + if (ah == null) return 1; + if (bh == null) return -1; + return ah - bh; + }); + + return ( +
+ + + +

+ Sentrix ships protocol changes through height-gated forks: every consensus-affecting + change has an activation height, and nodes that reach that height switch to the new + rules. This page lists every fork that has been defined for the chain — when it + activates, what it changes, and whether it's currently dormant. +

+
+ + {/* ── Per-fork cards (chronological on this network) ── */} +
+ {sorted.map((f) => { + const state = forkStateAt(f, network, height); + return ; + })} +
+ + {/* ── Cross-network comparison ────────────────── */} + +
+ + + + + + + + + + {FORKS.map((f) => ( + + + + + + ))} + +
ForkMainnetTestnet
{f.title} + {f.heights.mainnet == null + ? dormant + : formatNumber(f.heights.mainnet)} + + {f.heights.testnet == null + ? dormant + : formatNumber(f.heights.testnet)} +
+
+
+
+ ); +} + +function ForkCard({ + fork, + state, + network, +}: { + fork: ForkEntry; + state: "active" | "scheduled" | "dormant"; + network: "mainnet" | "testnet"; +}) { + const fh = fork.heights[network]; + const Icon = + state === "active" ? CheckCircle2 + : state === "scheduled" ? Clock + : AlertTriangle; + const tone = + state === "active" ? "text-green-500" + : state === "scheduled" ? "text-yellow-500" + : fork.state === "danger" ? "text-red-500" : "text-muted-foreground"; + const stateLabel = + state === "active" ? "Active" + : state === "scheduled" ? `Scheduled @ h=${fh!.toLocaleString()}` + : fork.state === "danger" ? "Dormant — DO NOT activate" + : "Dormant"; + + return ( +
+
+
+
{fork.title}
+
{fork.summary}
+
+ + + {stateLabel} + +
+

{fork.description}

+ {fh != null && ( +
+ Activation height ({network}): {fh.toLocaleString()} +
+ )} +
+ ); +} diff --git a/apps/scan/app/[locale]/supply/donut.tsx b/apps/scan/app/[locale]/supply/donut.tsx new file mode 100644 index 0000000..7345275 --- /dev/null +++ b/apps/scan/app/[locale]/supply/donut.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; + +interface Segment { + label: string; + value: number; + color: string; +} + +interface SupplyDonutProps { + segments: Segment[]; +} + +export function SupplyDonut({ segments }: SupplyDonutProps) { + const total = segments.reduce((s, x) => s + x.value, 0) || 1; + const data = segments.map((s) => ({ + name: s.label, + value: s.value, + pct: ((s.value / total) * 100).toFixed(1), + })); + + return ( + + + + {segments.map((s, i) => ( + + ))} + + { + const num = typeof value === "number" ? value : Number(value ?? 0); + const payload = item as { payload?: { pct?: string; name?: string } }; + const pct = payload.payload?.pct ?? "0"; + const name = payload.payload?.name ?? ""; + return [`${num.toLocaleString()} SRX (${pct}%)`, name]; + }} + /> + + + ); +} diff --git a/apps/scan/app/[locale]/supply/page.tsx b/apps/scan/app/[locale]/supply/page.tsx new file mode 100644 index 0000000..6e10584 --- /dev/null +++ b/apps/scan/app/[locale]/supply/page.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { useMemo } from "react"; +import { Coins, Flame, Lock, PieChart, Wallet, ShieldCheck } from "lucide-react"; +import dynamic from "next/dynamic"; +import { PageHeader } from "@/components/common/PageHeader"; +import { DetailCard } from "@/components/common/DetailCard"; +import { InfoRow } from "@/components/common/InfoRow"; +import { StatCard } from "@/components/common/StatCard"; +import { Address } from "@/components/common/Address"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useNetwork } from "@/lib/network-context"; +import { useStats, useValidators } from "@/lib/hooks"; +import { formatSRX, formatNumber } from "@/lib/format"; + +// DECISION: dedicated /supply page so listing platforms (CG / CMC / DefiLlama) +// + due-diligence reviewers can deep-link to a single canonical breakdown URL +// instead of fishing across 3 different docs pages. Mirrors the layout +// Etherscan exposes at /stat/supply but Sentrix-aware: +// - Max supply is the post-tokenomics-v2 cap (315M) once the fork is past; +// - "Locked / Premine" surface the four canonical premine wallets so the +// 20% disclosed premine is publicly auditable; +// - "Bonded" surfaces the live total stake from the validator endpoint; +// - "Burnt" pulls from the chain.info supply tracker. + +const SupplyDonut = dynamic(() => import("./donut").then((m) => m.SupplyDonut), { + ssr: false, + loading: () => , +}); + +interface PremineEntry { + label: string; + address: string; + /** Disclosed premine size in SRX. Pulled from `founder-private/CANONICAL_ADDRESSES.md`. */ + amount: number; +} + +const PREMINE_WALLETS: PremineEntry[] = [ + { + label: "Founder (vesting 1y cliff + 4y linear)", + address: "0x5b5b06688dcdbe532353ac610aaff41af825279d", + amount: 21_000_000, + }, + { + label: "Sentrix Ecosystem Fund", + address: "0xeb70fdefd00fdb768dec06c478f450c351499f14", + amount: 21_000_000, + }, + { + label: "Validator Incentive Pool", + address: "0x328d56b8174697ef6c9e40e19b7663797e16fa47", + amount: 10_500_000, + }, + { + label: "Strategic Reserve", + address: "0x2578cad17e3e56c2970a5b5eab45952439f5ba97", + amount: 10_500_000, + }, +]; + +const PREMINE_TOTAL = PREMINE_WALLETS.reduce((s, e) => s + e.amount, 0); + +export default function SupplyPage() { + const { network } = useNetwork(); + const { data: stats, loading: statsLoading } = useStats(network); + const { data: validators } = useValidators(network); + + // Bonded = sum of all validator stake (self_stake + total_delegated). The + // /validators endpoint returns these per-validator, so the dashboard sums. + const bonded = useMemo(() => { + if (!validators) return 0; + // ValidatorData.stake is total bonded (self + delegations) per the + // /validators endpoint shape — see `lib/api.ts` interface ValidatorData. + return validators.reduce((sum, v) => sum + (v.stake ?? 0), 0); + }, [validators]); + + const max = stats?.max_supply_srx ?? 315_000_000; + const minted = stats?.total_minted_srx ?? 0; + const burnt = stats?.total_burned_srx ?? 0; + // Circulating excludes burnt + premine that is still locked. Until vesting + // contracts go on-chain we approximate "locked" as the static premine total + // (~63M); once the founder vesting contract deploys and we can read the + // vested-vs-unvested split on-chain, this should switch to that. + const circulatingApprox = Math.max(0, minted - burnt - PREMINE_TOTAL); + const remainingToMint = Math.max(0, max - minted); + + const segments = [ + { label: "Circulating (approx)", value: circulatingApprox, color: "var(--green)" }, + { label: "Premine / Locked", value: PREMINE_TOTAL, color: "var(--gold)" }, + { label: "Bonded (Staked)", value: bonded, color: "var(--blue)" }, + { label: "Remaining to Mint", value: remainingToMint, color: "var(--tx-d)" }, + { label: "Burnt", value: burnt, color: "var(--pink)" }, + ]; + + return ( +
+ + + {/* ── Headline stat row ─────────────────────────── */} +
+ + + + +
+ + {/* ── Donut + breakdown ─────────────────────────── */} +
+ +
+ +
    + {segments.map((s) => ( +
  • + + + {s.label} + + {formatSRX(s.value)} +
  • + ))} +
+
+
+ + +
+

+ Max supply is fixed by the on-chain tokenomics-v2 fork at{" "} + 315,000,000 SRX. Anything above + that cannot be minted by any path on the protocol — it's enforced in{" "} + crates/sentrix-core/src/blockchain.rs. +

+

+ Total minted is what the chain has issued so far across genesis + + every block reward. Burnt is the running fee-burn counter — half + of every native transaction fee is destroyed instead of credited to the validator. +

+

+ Bonded sums every active validator's self-stake + delegations + from the staking registry. It's the live number any delegator-facing + calculator (APR, slashing exposure) should pin against, not a snapshot. +

+

+ Premine below is the disclosed 20% allocation set in genesis. The + founder slot is on a 1-year cliff + 4-year linear vest; the others were liquid at + genesis. We surface the wallet addresses so the schedule is auditable on-chain. +

+
+
+
+ + {/* ── Premine breakdown ─────────────────────────── */} + + Premine wallets ({formatSRX(PREMINE_TOTAL)}) + + } + > +
+ {PREMINE_WALLETS.map((w) => ( + +
+ {formatSRX(w.amount)} +
+ } + /> + ))} +
+
+ + {/* ── Sentinels (no private key) ────────────────── */} + + Protocol sentinels + + } + > +
+

+ These addresses hold balance for protocol-level book-keeping and have no private key. + They appear in tx history when a TokenOp / Stake op routes through them. +

+
    +
  • +
    + Sentrix Token Op (sentinel) +
  • +
  • +
    + Protocol Treasury (Reward Escrow) +
  • +
  • +
    + Sentrix Staking (sentinel) +
  • +
+
+
+ + {/* ── Numerical reference ────────────────────────── */} + + + {(BigInt(max) * 100_000_000n).toString()} sentri + + } + mono + hint="1 SRX = 100,000,000 sentri (8-decimal native ledger)." + /> + + + + 0 + ? `${((bonded / minted) * 100).toFixed(2)}%` + : "—" + } + last + hint="Bonded ÷ Total minted. Higher = more SRX securing the chain." + /> + +
+ ); +} diff --git a/apps/scan/components/common/FinalityBadge.tsx b/apps/scan/components/common/FinalityBadge.tsx new file mode 100644 index 0000000..e42c767 --- /dev/null +++ b/apps/scan/components/common/FinalityBadge.tsx @@ -0,0 +1,109 @@ +import { CheckCircle2, Clock, Layers, Shield } from "lucide-react"; +import { cn } from "@/lib/utils"; + +// Sentrix BFT finality has four observable states, modelled after the +// way Tendermint-derived chains expose them but renamed to plain English so +// non-technical users get it on first read: +// +// pending — tx is in the mempool, no block has included it +// included — tx is inside a block, the block is at the chain head +// justified — block has 2/3+1 stake-weighted precommit signatures +// attached (fast finality threshold met for this height) +// finalized — at least one descendant block has been justified, so a +// conflicting block at this height can no longer reach +// supermajority without slashing → safe to consider settled +// +// "Finalized" is what exchanges + bridges should wait for. We surface the +// distinction prominently because it's a Sentrix-specific value-prop other +// EVM chains don't expose. Keeping the colours separate from `StatusBadge` +// (success/failed) so they can be shown together — a tx can be "Success + +// Finalized" or "Success + Justified" or "Failed + Included" etc. + +export type Finality = "pending" | "included" | "justified" | "finalized"; + +const STYLES: Record = { + pending: { + bg: "bg-yellow-500/10", + text: "text-yellow-500", + border: "border-yellow-500/20", + icon: Clock, + label: "Pending", + tooltip: "In the mempool — not yet included in a block.", + }, + included: { + bg: "bg-[color-mix(in_oklab,var(--blue)_12%,transparent)]", + text: "text-[var(--blue)]", + border: "border-[color-mix(in_oklab,var(--blue)_25%,transparent)]", + icon: Layers, + label: "Included", + tooltip: "Sealed inside a block at the chain head. Wait for justification before treating as settled.", + }, + justified: { + bg: "bg-[color-mix(in_oklab,var(--gold)_12%,transparent)]", + text: "text-[var(--gold)]", + border: "border-[color-mix(in_oklab,var(--gold)_25%,transparent)]", + icon: Shield, + label: "Justified", + tooltip: "Block has the 2/3+1 stake-weighted precommit supermajority. Reverting requires slashing.", + }, + finalized: { + bg: "bg-green-500/10", + text: "text-green-500", + border: "border-green-500/20", + icon: CheckCircle2, + label: "Finalized", + tooltip: "A descendant block has been justified — this height is settled. Safe for exchanges + bridges.", + }, +}; + +interface FinalityBadgeProps { + finality: Finality; + size?: "sm" | "md"; + className?: string; +} + +export function FinalityBadge({ finality, size = "sm", className }: FinalityBadgeProps) { + const cfg = STYLES[finality]; + const Icon = cfg.icon; + return ( + + + {cfg.label} + + ); +} + +/** Compute the BFT finality state for a tx from the chain head + the block + * the tx was included in. Caller passes: + * - `txBlockHeight` — null if the tx is still pending in the mempool. + * - `latestHeight` — current chain head as the explorer sees it. + * - `hasJustification` — true if the tx's block carries a precommit- + * supermajority justification (every block produced + * under Voyager ships one, but historical pre-fork + * blocks don't, so the caller should consult the + * block payload, not assume). + * + * We call a height "finalized" if at least one descendant block has its own + * justification — keeping the rule simple instead of walking the precommit + * graph, since under Voyager every block is justified at production time. + */ +export function classifyFinality(opts: { + txBlockHeight: number | null; + latestHeight: number | null; + hasJustification: boolean; +}): Finality { + if (opts.txBlockHeight == null) return "pending"; + if (!opts.hasJustification) return "included"; + if (opts.latestHeight != null && opts.latestHeight > opts.txBlockHeight) return "finalized"; + return "justified"; +} diff --git a/apps/scan/components/common/RailBadge.tsx b/apps/scan/components/common/RailBadge.tsx new file mode 100644 index 0000000..d636177 --- /dev/null +++ b/apps/scan/components/common/RailBadge.tsx @@ -0,0 +1,101 @@ +import { Boxes, Coins, Cpu, Landmark } from "lucide-react"; +import { cn } from "@/lib/utils"; + +// DECISION: Sentrix is one of the only chains where SRC-20 (native TokenOps) +// and ERC-20 (EVM) live side-by-side at the protocol level. A user staring +// at a tx page has to be told which rail this tx travelled on — otherwise the +// "I sent SRX but Etherscan-style decoded view shows nothing" confusion is +// the dominant support ticket. Four rails: +// +// - "evm" — eth_sendRawTransaction call against an EVM contract +// - "native" — basic native SRX transfer (no token op encoded) +// - "token" — native SRC-20 op (Mint / Burn / Transfer / Approve / Deploy) +// - "stake" — native StakingOp (Delegate / Undelegate / ClaimRewards / +// RegisterValidator / AddSelfStake / Unjail) +// +// Colours pull from the chain's existing accent palette so the badge feels +// native to the explorer, not a slap-on add-on. + +export type Rail = "evm" | "native" | "token" | "stake"; + +const STYLES: Record = { + evm: { + bg: "bg-[color-mix(in_oklab,var(--blue)_12%,transparent)]", + text: "text-[var(--blue)]", + border: "border-[color-mix(in_oklab,var(--blue)_25%,transparent)]", + icon: Cpu, + label: "EVM", + tooltip: "EVM transaction — executed by the embedded revm runtime against a Solidity / Vyper contract.", + }, + native: { + bg: "bg-muted", + text: "text-muted-foreground", + border: "border-[var(--brd)]", + icon: Boxes, + label: "Native", + tooltip: "Native Sentrix transfer — moves SRX between two accounts without invoking a contract.", + }, + token: { + bg: "bg-[color-mix(in_oklab,var(--gold)_12%,transparent)]", + text: "text-[var(--gold)]", + border: "border-[color-mix(in_oklab,var(--gold)_25%,transparent)]", + icon: Coins, + label: "SRC-20", + tooltip: "Native SRC-20 token operation (Mint / Burn / Transfer / Approve / Deploy) — runs at the protocol level, not via revm.", + }, + stake: { + bg: "bg-[color-mix(in_oklab,var(--green)_12%,transparent)]", + text: "text-[var(--green)]", + border: "border-[color-mix(in_oklab,var(--green)_25%,transparent)]", + icon: Landmark, + label: "Staking", + tooltip: "Native staking operation (Delegate / Undelegate / Claim / RegisterValidator / AddSelfStake / Unjail) — applied directly against the stake registry.", + }, +}; + +interface RailBadgeProps { + rail: Rail; + size?: "sm" | "md"; + className?: string; + /** Override the default label (e.g. "Delegate" instead of generic "Staking"). */ + label?: string; +} + +export function RailBadge({ rail, size = "sm", className, label }: RailBadgeProps) { + const cfg = STYLES[rail]; + const Icon = cfg.icon; + const displayLabel = label ?? cfg.label; + return ( + + + {displayLabel} + + ); +} + +/** Heuristic: classify a tx into a rail from the data + to_address fields the + * REST API returns. Centralised so every page that renders a rail badge + * agrees on the answer. + * + * - to_address == 0x0000…0000 sentinel → SRC-20 TokenOp + * - to_address == 0x0000…0100 sentinel → Native StakingOp + * - data starts with "EVM:" prefix → EVM call + * - everything else → Native SRX transfer + */ +export function classifyRail(tx: { to_address?: string | null; data?: string | null }): Rail { + const to = (tx.to_address ?? "").toLowerCase(); + if (to === "0x0000000000000000000000000000000000000000") return "token"; + if (to === "0x0000000000000000000000000000000000000100") return "stake"; + if (tx.data && tx.data.startsWith("EVM:")) return "evm"; + return "native"; +} diff --git a/apps/scan/components/layout/footer.tsx b/apps/scan/components/layout/footer.tsx index 58e300f..5b37265 100644 --- a/apps/scan/components/layout/footer.tsx +++ b/apps/scan/components/layout/footer.tsx @@ -32,6 +32,9 @@ export function Footer() { Validators Tokens Leaderboard + Supply + Epochs + Fork History diff --git a/apps/scan/components/layout/header.tsx b/apps/scan/components/layout/header.tsx index 1b00441..022ab31 100644 --- a/apps/scan/components/layout/header.tsx +++ b/apps/scan/components/layout/header.tsx @@ -138,11 +138,12 @@ export function Header() { setLangOpen(false); } - const NAV_LINKS: { href: "/blocks" | "/validators" | "/tokens" | "/" | "/analytics"; key: keyof IntlMessages["nav"] }[] = [ + const NAV_LINKS: { href: "/blocks" | "/validators" | "/tokens" | "/" | "/analytics" | "/supply"; key: keyof IntlMessages["nav"] }[] = [ { href: "/", key: "home" }, { href: "/blocks", key: "blocks" }, { href: "/validators", key: "validators" }, { href: "/tokens", key: "tokens" }, + { href: "/supply", key: "supply" }, { href: "/analytics", key: "analytics" }, ]; diff --git a/apps/scan/lib/forks/registry.ts b/apps/scan/lib/forks/registry.ts new file mode 100644 index 0000000..d7376e5 --- /dev/null +++ b/apps/scan/lib/forks/registry.ts @@ -0,0 +1,155 @@ +// Authoritative fork-activation registry. Same source-of-truth that +// `founder-private/U64_MAX_FORK_GATES.md` documents — kept in sync by hand +// for now (any time we ship a new fork const in `crates/sentrix-core`, we +// add an entry here too). +// +// Each entry pairs an env-var name with the height the fork activated on +// each network. `null` for a network means "still parked at u64::MAX (not +// activated)" — the UI renders that as "Dormant" in the timeline. +// +// Sources for the heights below: BIBLE.md hardfork table + jail-consensus +// activation note in `U64_MAX_FORK_GATES.md`. + +import type { NetworkId } from "../chain"; + +export interface ForkEntry { + /** Stable identifier — used as the React key + URL fragment. */ + id: string; + /** Display name (the env-var name, with dashes for readability). */ + title: string; + /** Short one-line description aimed at non-technical users. */ + summary: string; + /** Fully expanded description for the detail panel. */ + description: string; + /** Per-network activation height; null means dormant on that network. */ + heights: Record; + /** Risk class — "shipped", "dormant", "danger" (do-not-activate). */ + state: "shipped" | "dormant" | "danger"; +} + +export const FORKS: ForkEntry[] = [ + { + id: "state-root", + title: "STATE_ROOT_FORK_HEIGHT", + summary: "Block hash starts committing to the post-block account state root.", + description: + "Before this fork the block hash was computed without including the trie state root, " + + "so two validators could disagree on state and still produce identical block hashes — " + + "any disagreement only surfaced via balance queries. After activation the trie root is " + + "part of the block hash, so any state divergence triggers an immediate `previous_hash` " + + "mismatch on the next block and prevents silent forks.", + heights: { mainnet: 100_000, testnet: 100_000 }, + state: "shipped", + }, + { + id: "legacy-validation", + title: "SENTRIX_LEGACY_VALIDATION_HEIGHT", + summary: "Closes the txid_index backfill startup hang (#268).", + description: + "Below this height the node skips strict re-validation of historical blocks during MDBX " + + "warm-up, because the pre-cutover history was produced under looser invariants. From this " + + "height onward every block goes through the strict validator at boot.", + heights: { mainnet: 557_144, testnet: 0 }, + state: "shipped", + }, + { + id: "voyager", + title: "VOYAGER_FORK_HEIGHT", + summary: "Activates DPoS proposer rotation + 3-phase BFT finality.", + description: + "Replaces the bootstrap Pioneer round-robin proposer with Voyager — Tendermint-style " + + "Propose → Prevote → Precommit → Finalize over a DPoS-elected validator set. From this " + + "height onward every block carries a `BlockJustification` with the precommits that " + + "finalised it, and finality at 2/3+1 stake weight is observable on-chain.", + heights: { mainnet: 579_047, testnet: 10 }, + state: "shipped", + }, + { + id: "voyager-evm", + title: "VOYAGER_EVM_HEIGHT", + summary: "Turns on the embedded revm runtime for `eth_sendRawTransaction`.", + description: + "Before this fork the chain ran native Sentrix transactions only. After activation the " + + "node embeds a revm interpreter and accepts standard EVM transactions, so Hardhat / Foundry " + + "/ ethers.js / viem dApps work without any Sentrix-specific tooling.", + heights: { mainnet: 579_060, testnet: 752 }, + state: "shipped", + }, + { + id: "reward-v2", + title: "VOYAGER_REWARD_V2_HEIGHT", + summary: "Coinbase routes to the protocol treasury; rewards are claimed.", + description: + "Pre-fork the block reward was credited directly to the proposer. Post-fork the coinbase " + + "deposits 1 SRX into the protocol treasury at `0x0000…0002`, and validators + delegators " + + "claim their accrued share via `StakingOp::ClaimRewards`. Keeps stake registry rewards " + + "and account balances in lock-step without per-block payouts.", + heights: { mainnet: 590_100, testnet: 100 }, + state: "shipped", + }, + { + id: "tokenomics-v2", + title: "TOKENOMICS_V2_HEIGHT", + summary: "Max supply moves to 315M; halving aligns with BTC-parity 4y schedule.", + description: + "Pre-fork the cap was 210M with 42M-block halvings. Post-fork: 315M cap, 126M-block " + + "halving (~4 years at 1s blocks). Tightens the issuance curve to BTC-parity for the long " + + "tail and restores the 20% premine ratio.", + heights: { mainnet: 640_800, testnet: 381_651 }, + state: "shipped", + }, + { + id: "bft-gate-relax", + title: "BFT_GATE_RELAX_HEIGHT", + summary: "BFT can run with ⌈2/3 × N⌉ active validators instead of full mesh.", + description: + "Originally BFT activation required every active validator to be connected at startup. " + + "Post-fork the gate relaxes to ⌈2/3 × N⌉, so the chain can recover from a single-validator " + + "outage without a coordinated halt-and-restart.", + heights: { mainnet: 692_700, testnet: 551_500 }, + state: "shipped", + }, + { + id: "add-self-stake", + title: "ADD_SELF_STAKE_HEIGHT", + summary: "Validators can top up `self_stake` from their wallet without a phantom mint.", + description: + "Recovery path for validators that fall under `MIN_SELF_STAKE`: bond real SRX into " + + "`self_stake` directly via `StakingOp::AddSelfStake`. Pre-fork the only path was the " + + "`force-unjail` CLI, which mutated the registry without a corresponding balance debit and " + + "left the supply invariant slightly off.", + heights: { mainnet: 731_245, testnet: 0 }, + state: "shipped", + }, + { + id: "jail-consensus", + title: "JAIL_CONSENSUS_HEIGHT", + summary: "Jail decisions become consensus state via `JailEvidenceBundle` system txs.", + description: + "Replaces the legacy local-only `check_liveness` jail trigger with an epoch-boundary " + + "system transaction (`StakingOp::JailEvidenceBundle`, sender `PROTOCOL_TREASURY`, " + + "dispatch recompute-and-compare for auth). Every node applies identical jail state " + + "post-fork — closes the divergence class observed when LivenessTracker was per-node " + + "in-memory.", + heights: { mainnet: 950_400, testnet: 1_030_500 }, + state: "shipped", + }, + { + id: "nft-tokenop", + title: "NFT_TOKENOP_HEIGHT", + summary: "SRC-721 + SRC-1155 dispatch (DO NOT activate yet).", + description: + "Wire format + Pass-1 gate shipped; Pass-2 dispatch + storage layer is still " + + "`unreachable!()`, so any non-`u64::MAX` value here would crash every validator at apply " + + "time. Stays dormant until the follow-up PR ships the apply path.", + heights: { mainnet: null, testnet: null }, + state: "danger", + }, +]; + +export function forkStateAt(fork: ForkEntry, network: NetworkId, height: number): "active" | "scheduled" | "dormant" { + const fh = fork.heights[network]; + if (fh == null) return "dormant"; + if (height >= fh) return "active"; + return "scheduled"; +} diff --git a/apps/scan/messages/en.json b/apps/scan/messages/en.json index 3a640fa..c354d95 100644 --- a/apps/scan/messages/en.json +++ b/apps/scan/messages/en.json @@ -6,7 +6,8 @@ "tokens": "Tokens", "leaderboard": "Leaderboard", "analytics": "Analytics", - "search_placeholder": "Search by block / tx / address..." + "search_placeholder": "Search by block / tx / address...", + "supply": "Supply" }, "leaderboard": { "eyebrow": "Accounts", @@ -195,4 +196,4 @@ "github": "GitHub", "chain": "Sentrix Chain" } -} +} \ No newline at end of file diff --git a/apps/scan/messages/id.json b/apps/scan/messages/id.json index b5a6540..62d18f0 100644 --- a/apps/scan/messages/id.json +++ b/apps/scan/messages/id.json @@ -6,7 +6,8 @@ "tokens": "Token", "leaderboard": "Leaderboard", "analytics": "Analitik", - "search_placeholder": "Cari berdasarkan blok / tx / alamat..." + "search_placeholder": "Cari berdasarkan blok / tx / alamat...", + "supply": "Suplai" }, "leaderboard": { "eyebrow": "Akun", @@ -195,4 +196,4 @@ "github": "GitHub", "chain": "Sentrix Chain" } -} +} \ No newline at end of file From b22b74e89953f0d56b4ac69d74be953b4a357c2f Mon Sep 17 00:00:00 2001 From: satyakwok Date: Thu, 30 Apr 2026 03:01:33 +0200 Subject: [PATCH 3/4] feat(scan): wire RailBadge + FinalityBadge into the tx detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both badges shipped in PR #7 but weren't wired anywhere yet. This puts them at the top of every tx page so the user instantly sees "what kind of tx is this" + "how settled is it": - Page header actions now render RailBadge + FinalityBadge alongside the existing success/failed status — three compact badges grouped together like Etherscan's "Status: Success | Block: 12345 (Finalized)" cluster, but Sentrix-aware. - The Status row in the Summary card now stacks the success/failed badge with the finality badge (Pending / Included / Justified / Finalized), with a hint explaining what the four states mean. - A new "Rail" row sits below Status: shows the rail badge + a contextual one-liner explaining what that rail means (EVM call vs native SRX transfer vs SRC-20 op vs native staking op). This is the teaching aid we keep getting support tickets about — users don't know which rail their tx travelled on. Finality classification: pending if no block_height, finalized if descendant has landed (chain head > tx block), else justified for any block past the Voyager activation height (`h >= 579_047` — every Voyager block ships a justification by construction). Rail classification piggybacks on the centralised `classifyRail()` heuristic the badge component shipped with — to_address sentinels 0x0000…0000 / 0x0000…0100 → SRC-20 / Staking; data prefix "EVM:" → EVM; everything else → Native. `pnpm --filter @sentriscloud/scan typecheck` clean. --- apps/scan/app/[locale]/tx/[hash]/page.tsx | 53 +++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/apps/scan/app/[locale]/tx/[hash]/page.tsx b/apps/scan/app/[locale]/tx/[hash]/page.tsx index 6fe01e4..ee3eadf 100644 --- a/apps/scan/app/[locale]/tx/[hash]/page.tsx +++ b/apps/scan/app/[locale]/tx/[hash]/page.tsx @@ -12,10 +12,12 @@ import { BlockHeight } from "@/components/common/BlockHeight"; import { Timestamp } from "@/components/common/Timestamp"; import { InfoRow } from "@/components/common/InfoRow"; import { StatusBadge } from "@/components/common/StatusBadge"; +import { RailBadge, classifyRail } from "@/components/common/RailBadge"; +import { FinalityBadge, classifyFinality } from "@/components/common/FinalityBadge"; import { Copyable } from "@/components/common/Copyable"; import { PageHeader } from "@/components/common/PageHeader"; import { useNetwork } from "@/lib/network-context"; -import { useTransaction } from "@/lib/hooks"; +import { useTransaction, useStats } from "@/lib/hooks"; export default function TxDetailPage({ params }: { params: Promise<{ hash: string }> }) { const { hash } = use(params); @@ -42,6 +44,9 @@ export default function TxDetailPage({ params }: { params: Promise<{ hash: strin const { data: tx, loading } = useTransaction(network, hash); const otherNetwork = network === "mainnet" ? "testnet" : "mainnet"; const { data: txOther } = useTransaction(otherNetwork, hash); + // BFT finality is "did a descendant block land?" — we only need the chain + // tip to compute that; we already have everything else from the tx body. + const { data: stats } = useStats(network); if (loading) { return ( @@ -95,6 +100,23 @@ export default function TxDetailPage({ params }: { params: Promise<{ hash: strin const success = tx.status !== "failed"; + // Rail classification doubles as a tx-shape teaching aid — Sentrix has + // four rails (EVM / Native / SRC-20 / Staking) and "user sent SRX, why + // does the receipt look weird" tickets stem from not knowing which rail + // they're staring at. classifyRail() centralises the heuristic so the + // tx detail page agrees with the address page agrees with the home feed. + const rail = classifyRail({ to_address: tx.to, data: tx.input_data ?? null }); + // BFT finality: pending if no block, finalized if a descendant block has + // already landed, justified if we're at the tip with the precommit set. + // We don't have the per-block "hasJustification" field on the tx detail + // body shape, so assume true for any block past the Voyager activation + // height — every Voyager block ships one. + const finality = classifyFinality({ + txBlockHeight: tx.block_height ?? null, + latestHeight: stats?.height ?? null, + hasJustification: tx.block_height != null && tx.block_height >= 579_047, + }); + return (
} + actions={ +
+ + + +
+ } /> @@ -127,7 +155,26 @@ export default function TxDetailPage({ params }: { params: Promise<{ hash: strin } /> - } /> + + + + + } + hint="BFT finality: Pending → Included → Justified (2/3+1 stake-weighted precommits) → Finalized (descendant block also justified)." + /> + } + hint={ + rail === "evm" ? "EVM transaction — runs in the embedded revm interpreter." + : rail === "token" ? "SRC-20 token operation — applied at the protocol level, not via revm." + : rail === "stake" ? "Native staking operation — applied directly against the stake registry." + : "Native SRX transfer between two accounts." + } + /> {tx.block_height !== undefined && ( } /> )} From 1d942680cd4ac09eca5ba1bda5203c83053ffad1 Mon Sep 17 00:00:00 2001 From: satyakwok Date: Thu, 30 Apr 2026 03:47:19 +0200 Subject: [PATCH 4/4] feat(chain-landing): publish canonical chain.json + tokenlist.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new static endpoints served by the chain-landing app at sentrixchain.com: - /chain.json — Sentrix Mainnet (7119) descriptor - /chain-testnet.json — Sentrix Testnet (7120) descriptor - /tokenlist.json — Uniswap-format token list (WSRX 7119 + WtSRX 7120) Mirrors the ethereum-lists/chains#8283 metadata one level deeper — includes consensus + finality model + canonical contract addresses + Sourcify verifier URL inline so a tooling integrator can read a single JSON and have everything they need (RPC, WS, explorer, verifier, WSRX address, decimals, finality semantics). Token list follows the standard Uniswap schema so dApp UIs that already speak it (Uniswap interface, OpenSea-style allowlists, portfolio trackers) can drop the URL in without bespoke parsing. URLs go live the next time chain-landing redeploys via the existing sentrix-landing systemd flow on the build host. --- apps/chain-landing/public/chain-testnet.json | 60 ++++++++++++++++++ apps/chain-landing/public/chain.json | 64 ++++++++++++++++++++ apps/chain-landing/public/tokenlist.json | 41 +++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 apps/chain-landing/public/chain-testnet.json create mode 100644 apps/chain-landing/public/chain.json create mode 100644 apps/chain-landing/public/tokenlist.json diff --git a/apps/chain-landing/public/chain-testnet.json b/apps/chain-landing/public/chain-testnet.json new file mode 100644 index 0000000..4ffbf11 --- /dev/null +++ b/apps/chain-landing/public/chain-testnet.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://chainagnostic.org/chain-meta/v1.json", + "name": "Sentrix Testnet", + "chain": "SRX", + "chainId": 7120, + "networkId": 7120, + "shortName": "srx-testnet", + "infoURL": "https://sentrixchain.com", + "nativeCurrency": { + "name": "Sentrix Testnet", + "symbol": "tSRX", + "decimals": 18 + }, + "rpc": [ + { + "url": "https://testnet-rpc.sentrixchain.com", + "tracking": "limited", + "isOpenSource": false + } + ], + "ws": [ + { + "url": "wss://testnet-rpc.sentrixchain.com/ws" + } + ], + "explorers": [ + { + "name": "Sentrix Scan Testnet", + "url": "https://scan.sentrixchain.com", + "standard": "EIP3091" + } + ], + "faucets": [ + "https://faucet.sentrixchain.com" + ], + "features": [ + { "name": "EIP-1559" }, + { "name": "EIP-3091" } + ], + "consensus": "DPoS+BFT", + "blockTime": 1, + "finality": { + "kind": "single-slot", + "supermajorityThreshold": "2/3+1", + "expectedSeconds": 1 + }, + "canonicalContracts": { + "WSRX": "0x85d5E7694AF31C2Edd0a7e66b7c6c92C59fF949A", + "Multicall3": "0x7900826De548425c6BE56caEbD4760AB0155Cd54", + "TokenFactory": "0x7A2992af0d4979aDD076347666023d66d29276Fc", + "SentrixSafe": "0xc9D7a61D7C2F428F6A055916488041fD00532110" + }, + "verifier": { + "name": "Sourcify", + "url": "https://verify.sentrixchain.com", + "selfHosted": true + }, + "documentation": "https://sentrixchain.com/docs", + "brand": "https://github.com/sentrix-labs/brand-kit" +} diff --git a/apps/chain-landing/public/chain.json b/apps/chain-landing/public/chain.json new file mode 100644 index 0000000..c3410cc --- /dev/null +++ b/apps/chain-landing/public/chain.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://chainagnostic.org/chain-meta/v1.json", + "name": "Sentrix Mainnet", + "chain": "SRX", + "chainId": 7119, + "networkId": 7119, + "shortName": "srx", + "infoURL": "https://sentrixchain.com", + "nativeCurrency": { + "name": "Sentrix", + "symbol": "SRX", + "decimals": 18 + }, + "rpc": [ + { + "url": "https://rpc.sentrixchain.com", + "tracking": "limited", + "isOpenSource": false + } + ], + "ws": [ + { + "url": "wss://rpc.sentrixchain.com/ws" + } + ], + "explorers": [ + { + "name": "Sentrix Scan", + "url": "https://scan.sentrixchain.com", + "standard": "EIP3091" + }, + { + "name": "Blockscout", + "url": "https://blockscout.sentrixchain.com", + "standard": "EIP3091" + } + ], + "faucets": [], + "features": [ + { "name": "EIP-1559" }, + { "name": "EIP-3091" } + ], + "consensus": "DPoS+BFT", + "blockTime": 1, + "finality": { + "kind": "single-slot", + "supermajorityThreshold": "2/3+1", + "expectedSeconds": 1 + }, + "canonicalContracts": { + "WSRX": "0x4693b113e523A196d9579333c4ab8358e2656553", + "Multicall3": "0xFd4b34b5763f54a580a0d9f7997A2A993ef9ceE9", + "TokenFactory": "0xc753199b723649ab92c6db8A45F158921CFDEe49", + "SentrixSafe": "0x6272dC0C842F05542f9fF7B5443E93C0642a3b26" + }, + "verifier": { + "name": "Sourcify", + "url": "https://verify.sentrixchain.com", + "selfHosted": true + }, + "tokenList": "https://sentrixchain.com/tokenlist.json", + "documentation": "https://sentrixchain.com/docs", + "brand": "https://github.com/sentrix-labs/brand-kit" +} diff --git a/apps/chain-landing/public/tokenlist.json b/apps/chain-landing/public/tokenlist.json new file mode 100644 index 0000000..1cc5743 --- /dev/null +++ b/apps/chain-landing/public/tokenlist.json @@ -0,0 +1,41 @@ +{ + "name": "Sentrix Canonical", + "logoURI": "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/png-transparent/sentrix-labs-256.png", + "keywords": ["sentrix", "srx", "canonical"], + "version": { "major": 1, "minor": 0, "patch": 0 }, + "timestamp": "2026-04-30T00:00:00.000Z", + "tokens": [ + { + "chainId": 7119, + "address": "0x4693b113e523A196d9579333c4ab8358e2656553", + "name": "Wrapped SRX", + "symbol": "WSRX", + "decimals": 18, + "logoURI": "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/png-transparent/sentrix-labs-256.png", + "tags": ["wrapped-native", "canonical"] + }, + { + "chainId": 7120, + "address": "0x85d5E7694AF31C2Edd0a7e66b7c6c92C59fF949A", + "name": "Wrapped tSRX", + "symbol": "WtSRX", + "decimals": 18, + "logoURI": "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/png-transparent/sentrix-labs-256.png", + "tags": ["wrapped-native", "canonical", "testnet"] + } + ], + "tags": { + "wrapped-native": { + "name": "Wrapped Native", + "description": "ERC-20 wrapper around the native SRX coin — backed 1:1 by SRX deposits." + }, + "canonical": { + "name": "Canonical", + "description": "Deployed by Sentrix Labs and present in `sentrix-labs/canonical-contracts`." + }, + "testnet": { + "name": "Testnet", + "description": "Token lives on chain 7120 (Sentrix Testnet); not transferable to mainnet." + } + } +}