feat(nu-node): wire axum RPC server, StateAccessor impl, --dev mode entry point
- nu-rpc: axum HTTP server on /rpc, dispatches JSON-RPC requests - nu-rpc: nu_chainInfo and nu_getAccount return real state from RocksDB - nu-state: StateAccessor trait implemented on StateDb (get/set balance, nonce) - nu-vm: executor uses StateAccessor from nu-state (single source of truth) - main.rs: clap CLI with --dev --validator --rpc-addr --db-path --chain-id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f7cff4513d
commit
a42ca0f8d3
10 changed files with 1723 additions and 45 deletions
1504
Cargo.lock
generated
Normal file
1504
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -16,13 +16,15 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tracing = "1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
rocksdb = "0.22"
|
rocksdb = "0.22"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
axum = "0.7"
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "nu-node"
|
name = "nu-node"
|
||||||
|
|
@ -39,6 +41,7 @@ serde.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
nu-consensus = { path = "crates/nu-consensus" }
|
nu-consensus = { path = "crates/nu-consensus" }
|
||||||
nu-mempool = { path = "crates/nu-mempool" }
|
nu-mempool = { path = "crates/nu-mempool" }
|
||||||
nu-state = { path = "crates/nu-state" }
|
nu-state = { path = "crates/nu-state" }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,5 @@ serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
nu-state = { path = "../nu-state" }
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,70 @@
|
||||||
// Method dispatch — each handler maps to a nu.rpc.api method name.
|
use serde_json::json;
|
||||||
// Handlers are stubs; implementation wired in Faz 1.
|
|
||||||
|
|
||||||
use crate::types::{JsonRpcRequest, JsonRpcResponse};
|
use crate::{
|
||||||
|
server::AppState,
|
||||||
|
types::{JsonRpcRequest, JsonRpcResponse},
|
||||||
|
};
|
||||||
|
use nu_state::account::AccountState;
|
||||||
|
|
||||||
pub fn dispatch(req: JsonRpcRequest) -> JsonRpcResponse {
|
pub fn dispatch(req: JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
"nu_getBlock" => stub(&req, "nu_getBlock"),
|
"nu_chainInfo" => handle_chain_info(&req, state),
|
||||||
"nu_getTx" => stub(&req, "nu_getTx"),
|
"nu_getAccount" => handle_get_account(&req, state),
|
||||||
"nu_getAccount" => stub(&req, "nu_getAccount"),
|
"nu_sendRawTx" => handle_send_raw_tx(&req, state),
|
||||||
"nu_getStory" => stub(&req, "nu_getStory"),
|
"nu_getBlock" => not_implemented(&req, "nu_getBlock"),
|
||||||
"nu_getNode" => stub(&req, "nu_getNode"),
|
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
||||||
"nu_getNft" => stub(&req, "nu_getNft"),
|
"nu_getStory" => not_implemented(&req, "nu_getStory"),
|
||||||
"nu_listStories" => stub(&req, "nu_listStories"),
|
"nu_getNode" => not_implemented(&req, "nu_getNode"),
|
||||||
"nu_listPendingVotes" => stub(&req, "nu_listPendingVotes"),
|
"nu_getNft" => not_implemented(&req, "nu_getNft"),
|
||||||
"nu_sendRawTx" => stub(&req, "nu_sendRawTx"),
|
"nu_listStories" => not_implemented(&req, "nu_listStories"),
|
||||||
"nu_chainInfo" => stub(&req, "nu_chainInfo"),
|
"nu_listPendingVotes" => not_implemented(&req, "nu_listPendingVotes"),
|
||||||
_ => JsonRpcResponse::err(req.id, -32601, "Method not found".into()),
|
_ => JsonRpcResponse::err(req.id, -32601, "Method not found".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stub(req: &JsonRpcRequest, method: &str) -> JsonRpcResponse {
|
fn handle_chain_info(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
|
JsonRpcResponse::ok(
|
||||||
|
req.id.clone(),
|
||||||
|
json!({
|
||||||
|
"chain_id": state.chain_id,
|
||||||
|
"node_version": env!("CARGO_PKG_VERSION"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_get_account(req: &JsonRpcRequest, state: &AppState) -> JsonRpcResponse {
|
||||||
|
let address = match req.params.get(0).and_then(|v| v.as_str()) {
|
||||||
|
Some(a) => a.to_string(),
|
||||||
|
None => return JsonRpcResponse::err(req.id.clone(), -32602, "Missing address param".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = format!("account:{address}");
|
||||||
|
match state.db.get::<AccountState>(&key) {
|
||||||
|
Ok(Some(account)) => JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(account).unwrap()),
|
||||||
|
Ok(None) => {
|
||||||
|
// Return empty account — address exists conceptually with zero balance
|
||||||
|
let empty = AccountState::new(address);
|
||||||
|
JsonRpcResponse::ok(req.id.clone(), serde_json::to_value(empty).unwrap())
|
||||||
|
}
|
||||||
|
Err(e) => JsonRpcResponse::err(req.id.clone(), -32000, e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_send_raw_tx(req: &JsonRpcRequest, _state: &AppState) -> JsonRpcResponse {
|
||||||
|
// Faz 1: accepts tx hex, decodes, validates, adds to mempool
|
||||||
|
// For now: echo back a stub tx_id so platform integration can proceed
|
||||||
|
let _raw = match req.params.get(0).and_then(|v| v.as_str()) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return JsonRpcResponse::err(req.id.clone(), -32602, "Missing raw tx param".into()),
|
||||||
|
};
|
||||||
|
|
||||||
JsonRpcResponse::err(
|
JsonRpcResponse::err(
|
||||||
req.id.clone(),
|
req.id.clone(),
|
||||||
-32000,
|
-32000,
|
||||||
format!("{method} not implemented yet"),
|
"nu_sendRawTx: mempool integration pending (Faz 1)".into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn not_implemented(req: &JsonRpcRequest, method: &str) -> JsonRpcResponse {
|
||||||
|
JsonRpcResponse::err(req.id.clone(), -32000, format!("{method} not implemented yet"))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,66 @@
|
||||||
// HTTP + WebSocket server skeleton — wired in Faz 1 with axum or hyper.
|
use std::sync::Arc;
|
||||||
// POST /rpc → JSON-RPC dispatch
|
|
||||||
// WS /ws → event subscription stream
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::post,
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
handlers::dispatch,
|
||||||
|
types::{JsonRpcRequest, JsonRpcResponse},
|
||||||
|
};
|
||||||
|
use nu_state::db::StateDb;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: Arc<StateDb>,
|
||||||
|
pub chain_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RpcServer {
|
pub struct RpcServer {
|
||||||
pub bind_addr: String,
|
pub bind_addr: String,
|
||||||
|
pub state: Arc<AppState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RpcServer {
|
impl RpcServer {
|
||||||
pub fn new(bind_addr: impl Into<String>) -> Self {
|
pub fn new(bind_addr: impl Into<String>, db: Arc<StateDb>, chain_id: String) -> Self {
|
||||||
Self { bind_addr: bind_addr.into() }
|
Self {
|
||||||
|
bind_addr: bind_addr.into(),
|
||||||
|
state: Arc::new(AppState { db, chain_id }),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self) -> anyhow::Result<()> {
|
pub async fn run(self) -> anyhow::Result<()> {
|
||||||
tracing::info!("RPC server listening on {}", self.bind_addr);
|
let router = Router::new()
|
||||||
// TODO Faz 1: axum router, /rpc POST handler, /ws upgrade
|
.route("/rpc", post(rpc_handler))
|
||||||
|
.with_state(self.state);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(&self.bind_addr).await?;
|
||||||
|
tracing::info!("RPC listening on {}", self.bind_addr);
|
||||||
|
axum::serve(listener, router).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn rpc_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
body: axum::body::Bytes,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let req: JsonRpcRequest = match serde_json::from_slice(&body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => {
|
||||||
|
let resp = JsonRpcResponse::err(
|
||||||
|
serde_json::Value::Null,
|
||||||
|
-32700,
|
||||||
|
"Parse error".into(),
|
||||||
|
);
|
||||||
|
return (StatusCode::OK, Json(resp));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = dispatch(req, &state);
|
||||||
|
(StatusCode::OK, Json(resp))
|
||||||
|
}
|
||||||
|
|
|
||||||
50
crates/nu-state/src/accessor.rs
Normal file
50
crates/nu-state/src/accessor.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
use crate::{account::AccountState, db::StateDb};
|
||||||
|
|
||||||
|
pub trait StateAccessor {
|
||||||
|
fn get_balance(&self, address: &str) -> u64;
|
||||||
|
fn get_nonce(&self, address: &str) -> u64;
|
||||||
|
fn set_balance(&mut self, address: &str, balance: u64);
|
||||||
|
fn inc_nonce(&mut self, address: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateAccessor for StateDb {
|
||||||
|
fn get_balance(&self, address: &str) -> u64 {
|
||||||
|
let key = format!("account:{address}");
|
||||||
|
self.get::<AccountState>(&key)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|a| a.balance)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_nonce(&self, address: &str) -> u64 {
|
||||||
|
let key = format!("account:{address}");
|
||||||
|
self.get::<AccountState>(&key)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|a| a.nonce)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_balance(&mut self, address: &str, balance: u64) {
|
||||||
|
let key = format!("account:{address}");
|
||||||
|
let mut account = self
|
||||||
|
.get::<AccountState>(&key)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| AccountState::new(address.to_string()));
|
||||||
|
account.balance = balance;
|
||||||
|
let _ = self.put(&key, &account);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inc_nonce(&mut self, address: &str) {
|
||||||
|
let key = format!("account:{address}");
|
||||||
|
let mut account = self
|
||||||
|
.get::<AccountState>(&key)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| AccountState::new(address.to_string()));
|
||||||
|
account.nonce += 1;
|
||||||
|
let _ = self.put(&key, &account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
pub mod accessor;
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod story_node;
|
pub mod story_node;
|
||||||
pub mod nft;
|
pub mod nft;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
|
||||||
|
pub use accessor::StateAccessor;
|
||||||
pub use db::StateDb;
|
pub use db::StateDb;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,10 @@
|
||||||
// Transaction executor — validates then mutates state atomically.
|
|
||||||
// Rule: validate fully before any state mutation; on error, state is unchanged.
|
|
||||||
|
|
||||||
use crate::errors::VmError;
|
use crate::errors::VmError;
|
||||||
|
use nu_state::StateAccessor;
|
||||||
|
|
||||||
pub struct ExecutionContext<'a> {
|
pub struct ExecutionContext<'a> {
|
||||||
pub state: &'a mut dyn StateAccessor,
|
pub state: &'a mut dyn StateAccessor,
|
||||||
pub block_height: u64,
|
pub block_height: u64,
|
||||||
pub now_ms: i64,
|
pub now_ms: i64,
|
||||||
}
|
|
||||||
|
|
||||||
pub trait StateAccessor {
|
|
||||||
fn get_balance(&self, address: &str) -> u64;
|
|
||||||
fn get_nonce(&self, address: &str) -> u64;
|
|
||||||
fn set_balance(&mut self, address: &str, balance: u64);
|
|
||||||
fn inc_nonce(&mut self, address: &str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_token_transfer(
|
pub fn execute_token_transfer(
|
||||||
|
|
@ -24,20 +15,19 @@ pub fn execute_token_transfer(
|
||||||
fee: u64,
|
fee: u64,
|
||||||
nonce: u64,
|
nonce: u64,
|
||||||
) -> Result<(), VmError> {
|
) -> Result<(), VmError> {
|
||||||
// Validate
|
|
||||||
let expected_nonce = ctx.state.get_nonce(sender);
|
let expected_nonce = ctx.state.get_nonce(sender);
|
||||||
if nonce != expected_nonce {
|
if nonce != expected_nonce {
|
||||||
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
|
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
|
||||||
}
|
}
|
||||||
let balance = ctx.state.get_balance(sender);
|
let balance = ctx.state.get_balance(sender);
|
||||||
let total = amount.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?;
|
let total = amount.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?;
|
||||||
if balance < total {
|
if balance < total {
|
||||||
return Err(VmError::InsufficientBalance { need: total, have: balance });
|
return Err(VmError::InsufficientBalance { need: total, have: balance });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutate
|
let recipient_balance = ctx.state.get_balance(to);
|
||||||
ctx.state.set_balance(sender, balance - total);
|
ctx.state.set_balance(sender, balance - total);
|
||||||
ctx.state.set_balance(to, ctx.state.get_balance(to) + amount);
|
ctx.state.set_balance(to, recipient_balance + amount);
|
||||||
ctx.state.inc_nonce(sender);
|
ctx.state.inc_nonce(sender);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/main.rs
45
src/main.rs
|
|
@ -1,17 +1,54 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
use nu_rpc::server::RpcServer;
|
||||||
|
use nu_state::StateDb;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "nu-node", version)]
|
||||||
|
struct Cli {
|
||||||
|
/// Run in single-validator dev mode (no consensus required)
|
||||||
|
#[arg(long)]
|
||||||
|
dev: bool,
|
||||||
|
|
||||||
|
/// Act as validator in this session
|
||||||
|
#[arg(long)]
|
||||||
|
validator: bool,
|
||||||
|
|
||||||
|
/// JSON-RPC HTTP bind address
|
||||||
|
#[arg(long, default_value = "0.0.0.0:9545")]
|
||||||
|
rpc_addr: String,
|
||||||
|
|
||||||
|
/// RocksDB data directory
|
||||||
|
#[arg(long, default_value = "./data/state")]
|
||||||
|
db_path: String,
|
||||||
|
|
||||||
|
/// Chain identifier
|
||||||
|
#[arg(long, default_value = "nu-devnet-1")]
|
||||||
|
chain_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(EnvFilter::from_default_env())
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
tracing::info!("nu-node starting...");
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// TODO Faz 1: parse CLI args (--dev, --validator, --rpc-port, --db-path)
|
if cli.dev {
|
||||||
// TODO Faz 1: init StateDb, Mempool, ValidatorSet, RpcServer
|
tracing::info!("Starting in --dev mode (single validator, consensus disabled)");
|
||||||
// TODO Faz 1: run consensus loop
|
}
|
||||||
|
|
||||||
|
let db = Arc::new(StateDb::open(&cli.db_path)?);
|
||||||
|
tracing::info!("State DB opened at {}", cli.db_path);
|
||||||
|
|
||||||
|
let rpc = RpcServer::new(cli.rpc_addr.clone(), Arc::clone(&db), cli.chain_id.clone());
|
||||||
|
tracing::info!("nu-node ready — chain_id={} rpc={}", cli.chain_id, cli.rpc_addr);
|
||||||
|
|
||||||
|
rpc.run().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue