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"
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
tracing = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
rocksdb = "0.22"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
axum = "0.7"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
[package]
|
||||
name = "nu-node"
|
||||
|
|
@ -39,6 +41,7 @@ serde.workspace = true
|
|||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
clap.workspace = true
|
||||
nu-consensus = { path = "crates/nu-consensus" }
|
||||
nu-mempool = { path = "crates/nu-mempool" }
|
||||
nu-state = { path = "crates/nu-state" }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
sha2.workspace = true
|
||||
|
|
|
|||
|
|
@ -9,3 +9,5 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
anyhow.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.
|
||||
// Handlers are stubs; implementation wired in Faz 1.
|
||||
use serde_json::json;
|
||||
|
||||
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() {
|
||||
"nu_getBlock" => stub(&req, "nu_getBlock"),
|
||||
"nu_getTx" => stub(&req, "nu_getTx"),
|
||||
"nu_getAccount" => stub(&req, "nu_getAccount"),
|
||||
"nu_getStory" => stub(&req, "nu_getStory"),
|
||||
"nu_getNode" => stub(&req, "nu_getNode"),
|
||||
"nu_getNft" => stub(&req, "nu_getNft"),
|
||||
"nu_listStories" => stub(&req, "nu_listStories"),
|
||||
"nu_listPendingVotes" => stub(&req, "nu_listPendingVotes"),
|
||||
"nu_sendRawTx" => stub(&req, "nu_sendRawTx"),
|
||||
"nu_chainInfo" => stub(&req, "nu_chainInfo"),
|
||||
"nu_chainInfo" => handle_chain_info(&req, state),
|
||||
"nu_getAccount" => handle_get_account(&req, state),
|
||||
"nu_sendRawTx" => handle_send_raw_tx(&req, state),
|
||||
"nu_getBlock" => not_implemented(&req, "nu_getBlock"),
|
||||
"nu_getTx" => not_implemented(&req, "nu_getTx"),
|
||||
"nu_getStory" => not_implemented(&req, "nu_getStory"),
|
||||
"nu_getNode" => not_implemented(&req, "nu_getNode"),
|
||||
"nu_getNft" => not_implemented(&req, "nu_getNft"),
|
||||
"nu_listStories" => not_implemented(&req, "nu_listStories"),
|
||||
"nu_listPendingVotes" => not_implemented(&req, "nu_listPendingVotes"),
|
||||
_ => 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(
|
||||
req.id.clone(),
|
||||
-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.
|
||||
// POST /rpc → JSON-RPC dispatch
|
||||
// WS /ws → event subscription stream
|
||||
use std::sync::Arc;
|
||||
|
||||
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 bind_addr: String,
|
||||
pub state: Arc<AppState>,
|
||||
}
|
||||
|
||||
impl RpcServer {
|
||||
pub fn new(bind_addr: impl Into<String>) -> Self {
|
||||
Self { bind_addr: bind_addr.into() }
|
||||
pub fn new(bind_addr: impl Into<String>, db: Arc<StateDb>, chain_id: String) -> Self {
|
||||
Self {
|
||||
bind_addr: bind_addr.into(),
|
||||
state: Arc::new(AppState { db, chain_id }),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self) -> anyhow::Result<()> {
|
||||
tracing::info!("RPC server listening on {}", self.bind_addr);
|
||||
// TODO Faz 1: axum router, /rpc POST handler, /ws upgrade
|
||||
let router = Router::new()
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
|
||||
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 story_node;
|
||||
pub mod nft;
|
||||
pub mod db;
|
||||
|
||||
pub use accessor::StateAccessor;
|
||||
pub use db::StateDb;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
// Transaction executor — validates then mutates state atomically.
|
||||
// Rule: validate fully before any state mutation; on error, state is unchanged.
|
||||
|
||||
use crate::errors::VmError;
|
||||
use nu_state::StateAccessor;
|
||||
|
||||
pub struct ExecutionContext<'a> {
|
||||
pub state: &'a mut dyn StateAccessor,
|
||||
|
|
@ -9,13 +7,6 @@ pub struct ExecutionContext<'a> {
|
|||
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(
|
||||
ctx: &mut ExecutionContext,
|
||||
sender: &str,
|
||||
|
|
@ -24,7 +15,6 @@ pub fn execute_token_transfer(
|
|||
fee: u64,
|
||||
nonce: u64,
|
||||
) -> Result<(), VmError> {
|
||||
// Validate
|
||||
let expected_nonce = ctx.state.get_nonce(sender);
|
||||
if nonce != expected_nonce {
|
||||
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
|
||||
|
|
@ -35,9 +25,9 @@ pub fn execute_token_transfer(
|
|||
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(to, ctx.state.get_balance(to) + amount);
|
||||
ctx.state.set_balance(to, recipient_balance + amount);
|
||||
ctx.state.inc_nonce(sender);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
45
src/main.rs
45
src/main.rs
|
|
@ -1,17 +1,54 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
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]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
tracing::info!("nu-node starting...");
|
||||
let cli = Cli::parse();
|
||||
|
||||
// TODO Faz 1: parse CLI args (--dev, --validator, --rpc-port, --db-path)
|
||||
// TODO Faz 1: init StateDb, Mempool, ValidatorSet, RpcServer
|
||||
// TODO Faz 1: run consensus loop
|
||||
if cli.dev {
|
||||
tracing::info!("Starting in --dev mode (single validator, consensus disabled)");
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue