diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..8ce2226 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,79 @@ +name: Rust + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install protobuf compiler + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: cargo fmt + run: cargo fmt --check + + - name: cargo clippy + run: cargo clippy --all-features -- -D warnings + + - name: cargo check default + run: cargo check + + - name: cargo check no default features + run: cargo check --no-default-features + + - name: cargo check native + run: cargo check --no-default-features --features native + + - name: cargo check wallet + run: cargo check --no-default-features --features wallet + + - name: cargo check evm + run: cargo check --no-default-features --features evm + + - name: cargo check grpc + run: cargo check --no-default-features --features grpc + + - name: cargo check bft + run: cargo check --no-default-features --features bft + + - name: cargo check native,wallet + run: cargo check --no-default-features --features native,wallet + + - name: cargo check evm,grpc + run: cargo check --no-default-features --features evm,grpc + + - name: cargo check all features + run: cargo check --all-features + + - name: cargo check examples all features + run: cargo check --examples --all-features + + - name: cargo test wallet + run: cargo test --no-default-features --features wallet + + - name: cargo test evm + run: cargo test --no-default-features --features evm + + - name: cargo test all features + run: cargo test --all-features + + - name: cargo doc all features + run: cargo doc --all-features --no-deps diff --git a/Cargo.lock b/Cargo.lock index 84080e0..66f0a98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3555,7 +3555,7 @@ dependencies = [ [[package]] name = "sentrix-chain" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" dependencies = [ "alloy", "futures", diff --git a/Cargo.toml b/Cargo.toml index 1a994bf..d0dfcb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sentrix-chain" -version = "0.1.0-alpha.0" +version = "0.1.0-alpha.1" edition = "2021" license = "MIT" description = "Official Rust SDK for Sentrix Chain — typed clients for native REST, EVM, gRPC, and secp256k1 wallet/signing." @@ -19,9 +19,9 @@ default = ["native"] network = [] native = ["dep:reqwest", "dep:tokio", "dep:serde", "dep:serde_json", "dep:thiserror"] wallet = ["dep:secp256k1", "dep:sha2", "dep:tiny-keccak", "dep:hex", "dep:serde", "dep:serde_json", "dep:thiserror", "native"] -# evm and grpc are scaffolded behind features but not implemented yet — -# alpha.0 ships the network spec + native client foundation. -evm = ["dep:alloy", "dep:url", "network"] +# EVM/gRPC/BFT are alpha surfaces. Keep them opt-in because they pull +# heavier networking stacks than the default native REST client. +evm = ["dep:alloy", "dep:url", "dep:thiserror", "network"] grpc = ["dep:tonic", "dep:sentrix-proto", "dep:tokio", "dep:futures", "dep:hex", "dep:thiserror", "network"] bft = ["dep:tokio", "dep:tokio-tungstenite", "dep:futures", "dep:serde", "dep:serde_json", "dep:thiserror", "network"] @@ -29,7 +29,7 @@ bft = ["dep:tokio", "dep:tokio-tungstenite", "dep:futures", "dep:serde", "dep:se # Network spec — always available, no feature gate. # Native REST stack reqwest = { version = "0.12", optional = true, default-features = false, features = ["json", "rustls-tls"] } -tokio = { version = "1", optional = true, features = ["macros", "rt-multi-thread"] } +tokio = { version = "1", optional = true, features = ["macros", "rt-multi-thread", "sync", "time"] } serde = { version = "1", optional = true, features = ["derive"] } serde_json = { version = "1", optional = true } thiserror = { version = "2", optional = true } @@ -56,5 +56,25 @@ tokio-tungstenite = { version = "0.29", optional = true, features = ["rustls-tls [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +[[example]] +name = "chain_info" +required-features = ["native"] + +[[example]] +name = "evm_block_number" +required-features = ["evm"] + +[[example]] +name = "grpc_latest_block" +required-features = ["grpc"] + +[[example]] +name = "websocket_subscribe" +required-features = ["bft"] + +[[example]] +name = "sign_native_transfer" +required-features = ["wallet"] + [lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index f248d1c..ca80c08 100644 --- a/README.md +++ b/README.md @@ -6,35 +6,66 @@ Official Rust SDK for **Sentrix Chain** (chain ID `7119` mainnet, `7120` testnet). -Mirror of [`@sentrix/chain`](https://github.com/Sentriscloud/sdk-ts) on the TypeScript side — same network spec, same canonical addresses, same tx signing semantics. Use this crate for Rust services (validators, indexers, bridges, monitoring agents) that need to talk to Sentrix without spinning up a Node process. +Mirror of [`@sentrix/chain`](https://github.com/Sentriscloud/sdk-ts) on the TypeScript side: same network spec, same canonical addresses, same tx signing semantics. Use this crate for Rust services, indexers, bridges, and monitoring agents that need to talk to Sentrix without spinning up a Node process. + +The `0.1.x` line is alpha. APIs are intended for integration testing and early developer use, but may still change before 1.0. ## Surface | Module | Feature flag | Status | What it does | |---|---|---|---| -| `network` | _always on_ | ✅ stable | Chain spec types + `MAINNET_SPEC` / `TESTNET_SPEC` constants. Single source of truth for chain ID, RPC / REST / WS / gRPC URLs, explorer, faucet. | -| `native` | `native` (default) | ✅ alpha | Typed REST client over `reqwest` for `/chain/info`, `/staking/validators`, `/accounts//nonce`, `POST /transactions`. | -| `wallet` | `wallet` | ✅ alpha | secp256k1 keypair + Ethereum-style address derivation + native tx signing. | -| `evm` | `evm` | ✅ alpha | alloy-based EVM JSON-RPC client (Provider factory; reach for alloy directly for signing / contract bindings / event filters). | -| `grpc` | `grpc` | ✅ alpha | tonic client over `sentrix.v1.Sentrix` — getBlock / getBalance / getValidatorSet / getSupply / getMempool / streamEvents. Generated proto types come from the published [`sentrix-proto`](https://crates.io/crates/sentrix-proto) crate (single source of truth, shared with the chain server). Consumers building from source need `protoc` installed (`apt install protobuf-compiler` or equivalent). | -| `bft` | `bft` | ✅ alpha | WebSocket subscription manager for the 9 channels (newHeads, logs, sentrix_finalized, sentrix_jail, …) over tokio-tungstenite. Multiplexes everything on one socket; pings every 30 s + force-reconnects on 90 s stale; auto re-subscribes after reconnect. Mirror of `@sentrix/chain/bft`. | +| `network` | _always on_ | alpha, low churn | Chain spec types + `MAINNET_SPEC` / `TESTNET_SPEC` constants. Single source of truth for chain ID, RPC / REST / WS / gRPC URLs, explorer, faucet. | +| `native` | `native` (default) | alpha | Typed REST client over `reqwest` for `/chain/info`, `/staking/validators`, `/accounts//nonce`, `POST /transactions`. | +| `wallet` | `wallet` | alpha | secp256k1 keypair + Ethereum-style address derivation + native tx signing. Applications remain responsible for secret storage. | +| `evm` | `evm` | alpha | alloy-based EVM HTTP provider factory using Sentrix mainnet/testnet RPC config. Reach for alloy directly for signing / contract bindings / event filters. | +| `grpc` | `grpc` | alpha | tonic client over `sentrix.v1.Sentrix` — getBlock / getBalance / getValidatorSet / getSupply / getMempool / streamEvents. Proto types come from the published [`sentrix-proto`](https://crates.io/crates/sentrix-proto) crate. Consumers building from source may need `protoc` installed (`apt install protobuf-compiler` or equivalent). | +| `bft` | `bft` | alpha | WebSocket subscription manager for EVM and Sentrix-specific subscription channels over tokio-tungstenite. Runtime behavior depends on the configured WS endpoint. | Trim what you actually use: ```toml [dependencies] -sentrix-chain = { version = "0.1.0-alpha.0", default-features = false, features = ["native", "wallet"] } +sentrix-chain = { version = "0.1.0-alpha.1", default-features = false, features = ["native", "wallet"] } ``` ## Quick start +## Unit warning + +Native REST/native ledger amounts use **sentri**, the 8-decimal SRX +unit: `1 SRX = 100_000_000 sentri`. + +EVM JSON-RPC uses **wei-style 18-decimal units** for Ethereum tooling +compatibility. Do not mix native and EVM amounts directly; convert +explicitly at API boundaries. + +## Examples + +Run examples against mainnet by default, or set `SENTRIX_NETWORK=testnet`. + +```bash +cargo run --example chain_info +cargo run --no-default-features --features evm --example evm_block_number +cargo run --no-default-features --features grpc --example grpc_latest_block +cargo run --no-default-features --features bft --example websocket_subscribe +cargo run --no-default-features --features wallet --example sign_native_transfer +``` + +`sign_native_transfer` reads secrets from the environment and only +prints the signed transaction envelope; it does not broadcast. + +A native balance example is not included in this alpha because +`NativeClient` does not yet expose a documented native balance endpoint. +Use the `grpc` surface for balance reads when that endpoint is available +for your deployment. + ### Read chain stats ```rust use sentrix_chain::{Network, NativeClient}; #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<(), Box> { let client = NativeClient::new(Network::Mainnet); let info = client.chain_info().await?; println!( @@ -55,7 +86,7 @@ async fn main() -> anyhow::Result<()> { use sentrix_chain::{Network, NativeClient, SentrixWallet}; #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> Result<(), Box> { let w = SentrixWallet::from_private_key_hex(&std::env::var("PRIVATE_KEY")?)?; let client = NativeClient::new(Network::Mainnet); @@ -146,11 +177,11 @@ println!("{}: {}", MAINNET.name, MAINNET.rpc_url); ## Status -`v0.1.0-alpha.0` on crates.io. All six doors (`network`, `native`, `wallet`, `evm`, `grpc`, `bft`) compile and have working client paths against the public RPC + gRPC endpoints. Surface is alpha — expect breaking changes before 1.0 stabilises. +`v0.1.0-alpha.1` is the release candidate prepared for feature-flag hardening, examples, docs, and publish readiness. All six surfaces (`network`, `native`, `wallet`, `evm`, `grpc`, `bft`) are intended to compile behind their feature flags. Live endpoint compatibility is still alpha, so expect breaking changes before 1.0 stabilises. ## Roadmap -All six doors landed for v0.1.0-alpha.0: +All six surfaces are present in v0.1.0-alpha.1: - [x] `network` — chain spec, mainnet + testnet constants - [x] `native` — REST read + tx broadcast @@ -159,11 +190,7 @@ All six doors landed for v0.1.0-alpha.0: - [x] `grpc` — tonic client over `sentrix.v1.Sentrix` (consumes [`sentrix-proto`](https://crates.io/crates/sentrix-proto) for the schema) - [x] `bft` — WebSocket subscription manager (multiplex + keepalive ping + auto-reconnect, port of `@sentrix/chain/bft`) -Next: surface stabilisation toward 1.0 — naming review, error-type cleanup, optional `EvmClient` wrapper around alloy. - -## Decimals - -Sentrix's underlying ledger is **8-decimal** native (1 SRX = 100,000,000 sentri). The EVM tooling sees an **18-decimal** view because `eth_getBalance` returns wei-scaled values for compatibility with MetaMask / ethers / viem. When you use `NativeClient::balance(...)` you get sentri (8-decimal); when the planned `EvmClient` ships you'll get wei (18-decimal). Don't mix the units across surfaces. +Next: surface stabilisation toward 1.0 — naming review, error-type cleanup, native balance support if the REST endpoint is documented, and optional `EvmClient` wrappers around alloy. ## License diff --git a/examples/chain_info.rs b/examples/chain_info.rs new file mode 100644 index 0000000..5e85d25 --- /dev/null +++ b/examples/chain_info.rs @@ -0,0 +1,25 @@ +use sentrix_chain::{NativeClient, Network}; + +fn network_from_env() -> Network { + match std::env::var("SENTRIX_NETWORK").as_deref() { + Ok("testnet") => Network::Testnet, + _ => Network::Mainnet, + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let network = network_from_env(); + let client = NativeClient::new(network); + let info = client.chain_info().await?; + + println!("network={}", client.spec().name); + println!("chain_id={}", client.spec().chain_id); + println!("height={}", info.height); + println!("active_validators={}", info.active_validators); + println!("mempool_size={}", info.mempool_size); + println!("total_minted_srx={}", info.total_minted_srx); + println!("total_burned_srx={}", info.total_burned_srx); + + Ok(()) +} diff --git a/examples/evm_block_number.rs b/examples/evm_block_number.rs new file mode 100644 index 0000000..cd307b7 --- /dev/null +++ b/examples/evm_block_number.rs @@ -0,0 +1,20 @@ +use alloy::providers::Provider; +use sentrix_chain::{evm, Network}; + +fn network_from_env() -> Network { + match std::env::var("SENTRIX_NETWORK").as_deref() { + Ok("testnet") => Network::Testnet, + _ => Network::Mainnet, + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let network = network_from_env(); + let provider = evm::http_provider(network)?; + let block_number = provider.get_block_number().await?; + + println!("latest_evm_block={block_number}"); + + Ok(()) +} diff --git a/examples/grpc_latest_block.rs b/examples/grpc_latest_block.rs new file mode 100644 index 0000000..470cc99 --- /dev/null +++ b/examples/grpc_latest_block.rs @@ -0,0 +1,20 @@ +use sentrix_chain::{grpc::SentrixGrpcClient, Network}; + +fn network_from_env() -> Network { + match std::env::var("SENTRIX_NETWORK").as_deref() { + Ok("testnet") => Network::Testnet, + _ => Network::Mainnet, + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let network = network_from_env(); + let mut client = SentrixGrpcClient::connect(network).await?; + let block = client.get_latest_block().await?; + + println!("latest_block_height={}", block.index); + println!("transactions={}", block.transactions.len()); + + Ok(()) +} diff --git a/examples/sign_native_transfer.rs b/examples/sign_native_transfer.rs new file mode 100644 index 0000000..a023228 --- /dev/null +++ b/examples/sign_native_transfer.rs @@ -0,0 +1,54 @@ +use sentrix_chain::{NativeClient, Network, SentrixWallet}; + +fn network_from_env() -> Network { + match std::env::var("SENTRIX_NETWORK").as_deref() { + Ok("testnet") => Network::Testnet, + _ => Network::Mainnet, + } +} + +fn parse_u64_env(name: &str, default: u64) -> Result> { + match std::env::var(name) { + Ok(value) => Ok(value.parse()?), + Err(_) => Ok(default), + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let private_key = match std::env::var("SENTRIX_PRIVATE_KEY") { + Ok(value) => value, + Err(_) => { + println!("set SENTRIX_PRIVATE_KEY and SENTRIX_TO to build a signed transfer"); + return Ok(()); + } + }; + let to = match std::env::var("SENTRIX_TO") { + Ok(value) => value, + Err(_) => { + println!("set SENTRIX_TO to the 0x recipient address"); + return Ok(()); + } + }; + + let network = network_from_env(); + let client = NativeClient::new(network); + let wallet = SentrixWallet::from_private_key_hex(&private_key)?; + let nonce = match std::env::var("SENTRIX_NONCE") { + Ok(value) => value.parse()?, + Err(_) => client.next_nonce(&wallet.address).await?, + }; + + let tx = wallet.build_and_sign_transfer( + &to, + parse_u64_env("SENTRIX_AMOUNT_SENTRI", 100_000_000)?, + parse_u64_env("SENTRIX_FEE_SENTRI", 10_000)?, + nonce, + client.spec().chain_id, + )?; + + println!("{}", serde_json::to_string_pretty(&tx)?); + println!("not broadcast; submit with NativeClient::broadcast after review"); + + Ok(()) +} diff --git a/examples/websocket_subscribe.rs b/examples/websocket_subscribe.rs new file mode 100644 index 0000000..4240e46 --- /dev/null +++ b/examples/websocket_subscribe.rs @@ -0,0 +1,27 @@ +use std::time::Duration; + +use sentrix_chain::{ + bft::{Channel, SubscriptionManager}, + Network, +}; + +fn network_from_env() -> Network { + match std::env::var("SENTRIX_NETWORK").as_deref() { + Ok("testnet") => Network::Testnet, + _ => Network::Mainnet, + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let manager = SubscriptionManager::new(network_from_env()); + let mut heads = manager.subscribe(Channel::NewHeads).await?; + + match tokio::time::timeout(Duration::from_secs(30), heads.recv()).await { + Ok(Some(head)) => println!("new_head={head}"), + Ok(None) => println!("subscription closed before a head arrived"), + Err(_) => println!("no head received within 30 seconds"), + } + + Ok(()) +} diff --git a/src/bft.rs b/src/bft.rs index 7648d52..26fe300 100644 --- a/src/bft.rs +++ b/src/bft.rs @@ -15,8 +15,9 @@ //! transparently re-subscribes after reconnect with exponential //! backoff (1 s → 2 s → … → 30 s capped). //! -//! Each [`subscribe`] call returns a `tokio::sync::mpsc::UnboundedReceiver` -//! that yields `serde_json::Value` payloads. Drain it with the standard +//! Each [`SubscriptionManager::subscribe`] call returns a +//! `tokio::sync::mpsc::UnboundedReceiver` that yields +//! `serde_json::Value` payloads. Drain it with the standard //! `.recv().await` loop. use std::collections::HashMap; @@ -165,7 +166,7 @@ impl SubscriptionManager { /// Subscribe to `logs` with a filter object. The filter is passed /// verbatim as the second param in the `eth_subscribe` request — - /// see https://eth.wiki/json-rpc/API for the address/topics + /// see for the address/topics /// shape. pub async fn subscribe_with_filter( &self, diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 9cc9fe7..899bc37 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -9,10 +9,9 @@ //! - [`pb`] — the raw prost types + tonic stub if you need richer //! request shapes (custom `at_height`, multi-filter streams, …). //! -//! The proto types are pre-generated + committed (`pb.rs` is checked -//! in) so consumers don't need `protoc` installed. Regenerate via -//! `cargo run --bin gen-grpc` if the upstream chain bumps the proto -//! (planned tooling — not shipped in alpha.0). +//! Proto types come from the published `sentrix-proto` crate. Building +//! with the `grpc` feature may require `protoc` through that dependency +//! (`apt install protobuf-compiler` or equivalent). //! //! Available calls (chain v0.4+): //! - `get_latest_block()` / `get_block_by_height(h)` @@ -177,8 +176,8 @@ impl SentrixGrpcClient { /// Convenience — short-form hex of a 32-byte hash for UI rendering. pub fn hash_short(h: &pb::Hash) -> String { if h.value.len() != 32 { - return "—".into(); + return "-".into(); } let hex_str = ::hex::encode(&h.value); - format!("{}…{}", &hex_str[..6], &hex_str[hex_str.len() - 4..]) + format!("{}...{}", &hex_str[..6], &hex_str[hex_str.len() - 4..]) } diff --git a/src/lib.rs b/src/lib.rs index bb6cbf3..a1998d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,29 @@ //! Official Rust SDK for Sentrix Chain. //! -//! Surface (mirrors `@sentrix/chain` on the TypeScript side): +//! Alpha surface (mirrors `@sentrix/chain` on the TypeScript side): //! //! - [`network`] — chain spec types + mainnet/testnet constants. Always //! compiled in; zero runtime deps. -//! - [`native`] — typed REST client for the Sentrix-shaped endpoints -//! (`/chain/info`, `/staking/validators`, `/epoch/current`, …). Behind -//! the `native` feature (default). Uses `reqwest` + `tokio`. -//! - [`wallet`] — secp256k1 keypair + Ethereum-style address derivation -//! + Sentrix-native tx signing. Behind the `wallet` feature. -//! - `evm` (planned) — alloy-based EVM client. -//! - `grpc` (planned) — tonic client over the chain's `sentrix.v1.Sentrix` -//! service. +//! - [`native`] — alpha typed REST client for `/chain/info`, +//! `/staking/validators`, `/accounts//nonce`, and +//! `POST /transactions`. Behind the `native` feature (default). +//! - [`wallet`] — alpha secp256k1 keypair, Ethereum-style address +//! derivation, and Sentrix-native transfer signing. Behind `wallet`. +//! - [`evm`] — alpha Alloy HTTP provider factory for Sentrix EVM RPC. +//! Behind `evm`. +//! - [`grpc`] — alpha tonic client over the chain's `sentrix.v1.Sentrix` +//! service via the `sentrix-proto` crate. Behind `grpc`. +//! - [`bft`] — alpha WebSocket subscription manager for Sentrix/EVM +//! subscription channels. Behind `bft`. //! -//! Status: `0.1.0-alpha.0`. Network spec + native REST are usable; -//! wallet signing scaffolded; EVM and gRPC are doors-only stubs. +//! Status: `0.1.0-alpha.1`. APIs compile and are intended for external +//! integration testing, but the crate is not a 1.0-stable production +//! interface yet. +//! +//! Unit warning: native REST/native ledger amounts use `sentri` +//! (8-decimal SRX). EVM JSON-RPC uses wei-style 18-decimal units for +//! Ethereum tooling compatibility. Do not mix native and EVM amounts +//! without explicit conversion. #![deny(unsafe_code)] #![warn(missing_docs)] diff --git a/src/native.rs b/src/native.rs index eac5a37..0817cf3 100644 --- a/src/native.rs +++ b/src/native.rs @@ -7,6 +7,10 @@ //! directly: typed response structs, single endpoint resolution, //! consistent error handling, and a place to attach retry / backoff //! when the chain LB returns 5xx during binary swaps. +//! +//! Native transfer amounts and fees are sentri (8-decimal SRX). The +//! `/chain/info` supply fields exposed by [`ChainInfo`] are display SRX +//! values because the REST endpoint returns them that way. use serde::{Deserialize, Serialize}; @@ -41,9 +45,9 @@ pub struct ChainInfo { pub height: u64, /// Total blocks ever produced (= height + 1 for a healthy chain). pub total_blocks: u64, - /// Total minted SRX denominated in sentri (10^-8 SRX). + /// Total minted SRX, as returned by `/chain/info`. pub total_minted_srx: f64, - /// Total burned SRX (50% of every fee). + /// Total burned SRX (50% of every fee), as returned by `/chain/info`. pub total_burned_srx: f64, /// Configured max supply (315 M post-tokenomics-v2 fork). pub max_supply_srx: f64, @@ -51,7 +55,7 @@ pub struct ChainInfo { pub active_validators: u32, /// Pending tx count in the mempool. pub mempool_size: u64, - /// Reward paid for the next block. + /// Reward paid for the next block in SRX display units. pub next_block_reward_srx: f64, }