feat(nu-cli): initial Faz 0 scaffold
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0e506c7bc0
11 changed files with 315 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
target/
|
||||
*.rs.bk
|
||||
.env
|
||||
~/.nu/keystore/
|
||||
35
CLAUDE.md
Normal file
35
CLAUDE.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# nu-cli — CLAUDE.md
|
||||
|
||||
Geliştirici CLI. Binary adı `nu`.
|
||||
|
||||
## Komutlar
|
||||
|
||||
```bash
|
||||
nu wallet new [--label default] # Yeni keypair üret, ~/.nu/keystore/ altına kaydet
|
||||
nu wallet address [--label default] # Adres göster
|
||||
nu wallet send --to <addr> --amount <n>
|
||||
|
||||
nu node info # nu_chainInfo RPC
|
||||
nu node stake --amount <n>
|
||||
nu node unstake --amount <n>
|
||||
|
||||
nu story submit --story-id <id> --parent-id <id> --ipfs-hash <cid>
|
||||
nu story register --node-id <id> # Oylama için kayıt
|
||||
nu story vote --node-id <id> --approve true/false
|
||||
nu story show --story-id <id>
|
||||
|
||||
nu genesis init --chain-id <id> # genesis.toml üret
|
||||
```
|
||||
|
||||
## Keystore
|
||||
|
||||
Keyler `~/.nu/keystore/<label>.key` altında saklanır.
|
||||
**Faz 0:** hex plaintext (geliştirme kolaylığı)
|
||||
**Faz 1:** password-encrypted (production güvenliği)
|
||||
|
||||
## Geliştirme
|
||||
|
||||
```bash
|
||||
cargo run --bin nu -- wallet new
|
||||
cargo run --bin nu -- node info --rpc http://localhost:8545
|
||||
```
|
||||
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "nu-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "nu"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
anyhow = "1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
k256 = { version = "0.13", features = ["ecdsa"] } # secp256k1
|
||||
rand = "0.8"
|
||||
hex = "0.4"
|
||||
dirs = "5"
|
||||
25
src/commands/genesis.rs
Normal file
25
src/commands/genesis.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use crate::rpc::Client;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum GenesisCmd {
|
||||
/// Initialize genesis block configuration
|
||||
Init {
|
||||
#[arg(long)] chain_id: String,
|
||||
#[arg(long, default_value = "genesis.toml")] output: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cmd: GenesisCmd, _rpc: &Client) -> Result<()> {
|
||||
match cmd {
|
||||
GenesisCmd::Init { chain_id, output } => {
|
||||
let template = format!(
|
||||
"[genesis]\nchain_id = \"{chain_id}\"\n\n[bootstrap_peers]\n# Add validator multiaddrs here\n"
|
||||
);
|
||||
std::fs::write(&output, template)?;
|
||||
println!("Genesis config written to {output}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
4
src/commands/mod.rs
Normal file
4
src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod wallet;
|
||||
pub mod node;
|
||||
pub mod story;
|
||||
pub mod genesis;
|
||||
25
src/commands/node.rs
Normal file
25
src/commands/node.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use crate::rpc::Client;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum NodeCmd {
|
||||
/// Show chain info
|
||||
Info,
|
||||
/// Register as validator
|
||||
Stake { #[arg(long)] amount: u64 },
|
||||
/// Unstake from validator set
|
||||
Unstake { #[arg(long)] amount: u64 },
|
||||
}
|
||||
|
||||
pub async fn run(cmd: NodeCmd, rpc: &Client) -> Result<()> {
|
||||
match cmd {
|
||||
NodeCmd::Info => {
|
||||
let info = rpc.call("nu_chainInfo", serde_json::json!({})).await?;
|
||||
println!("{}", serde_json::to_string_pretty(&info)?);
|
||||
}
|
||||
NodeCmd::Stake { amount } => println!("TODO: stake {amount} NUT"),
|
||||
NodeCmd::Unstake { amount } => println!("TODO: unstake {amount} NUT"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
45
src/commands/story.rs
Normal file
45
src/commands/story.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use crate::rpc::Client;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum StoryCmd {
|
||||
/// Submit a new story node
|
||||
Submit {
|
||||
#[arg(long)] story_id: String,
|
||||
#[arg(long)] parent_id: String,
|
||||
#[arg(long)] ipfs_hash: String,
|
||||
},
|
||||
/// Register to vote on a node
|
||||
Register {
|
||||
#[arg(long)] node_id: String,
|
||||
},
|
||||
/// Cast a vote
|
||||
Vote {
|
||||
#[arg(long)] node_id: String,
|
||||
#[arg(long)] approve: bool,
|
||||
},
|
||||
/// Show story DAG
|
||||
Show {
|
||||
#[arg(long)] story_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cmd: StoryCmd, rpc: &Client) -> Result<()> {
|
||||
match cmd {
|
||||
StoryCmd::Submit { story_id, parent_id, ipfs_hash } => {
|
||||
println!("TODO: submit node story={story_id} parent={parent_id} hash={ipfs_hash}");
|
||||
}
|
||||
StoryCmd::Register { node_id } => {
|
||||
println!("TODO: register vote for node={node_id}");
|
||||
}
|
||||
StoryCmd::Vote { node_id, approve } => {
|
||||
println!("TODO: vote node={node_id} approve={approve}");
|
||||
}
|
||||
StoryCmd::Show { story_id } => {
|
||||
let result = rpc.call("nu_getStory", serde_json::json!({ "story_id": story_id })).await?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
56
src/commands/wallet.rs
Normal file
56
src/commands/wallet.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum WalletCmd {
|
||||
/// Generate a new keypair
|
||||
New {
|
||||
/// Label for the keystore file
|
||||
#[arg(long, default_value = "default")]
|
||||
label: String,
|
||||
},
|
||||
/// Show address for a keystore entry
|
||||
Address {
|
||||
#[arg(long, default_value = "default")]
|
||||
label: String,
|
||||
},
|
||||
/// Send NUT tokens
|
||||
Send {
|
||||
#[arg(long)] to: String,
|
||||
#[arg(long)] amount: u64,
|
||||
#[arg(long, default_value = "default")] from: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cmd: WalletCmd) -> Result<()> {
|
||||
match cmd {
|
||||
WalletCmd::New { label } => {
|
||||
let key = SigningKey::random(&mut OsRng);
|
||||
let bytes = key.to_bytes();
|
||||
let hex_key = hex::encode(bytes);
|
||||
|
||||
let dir = crate::config::keystore_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join(format!("{label}.key"));
|
||||
// Store as hex — Faz 1: add password encryption
|
||||
std::fs::write(&path, &hex_key)?;
|
||||
|
||||
let verifying = key.verifying_key();
|
||||
println!("Created keystore: {}", path.display());
|
||||
println!("Address (pubkey compressed): {}", hex::encode(verifying.to_sec1_bytes()));
|
||||
}
|
||||
WalletCmd::Address { label } => {
|
||||
let dir = crate::config::keystore_dir();
|
||||
let raw = std::fs::read_to_string(dir.join(format!("{label}.key")))?;
|
||||
let bytes = hex::decode(raw.trim())?;
|
||||
let key = SigningKey::from_bytes(bytes.as_slice().into())?;
|
||||
println!("{}", hex::encode(key.verifying_key().to_sec1_bytes()));
|
||||
}
|
||||
WalletCmd::Send { to, amount, from } => {
|
||||
println!("TODO: nu wallet send --to {to} --amount {amount} (from: {from})");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
8
src/config.rs
Normal file
8
src/config.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub fn keystore_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".nu")
|
||||
.join("keystore")
|
||||
}
|
||||
56
src/main.rs
Normal file
56
src/main.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
mod commands;
|
||||
mod config;
|
||||
mod rpc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "nu", version, about = "Narrative Union CLI")]
|
||||
struct Cli {
|
||||
/// RPC endpoint (overrides config)
|
||||
#[arg(long, env = "NU_RPC_URL", default_value = "http://localhost:8545")]
|
||||
rpc: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Wallet management
|
||||
Wallet {
|
||||
#[command(subcommand)]
|
||||
action: commands::wallet::WalletCmd,
|
||||
},
|
||||
/// Node operations
|
||||
Node {
|
||||
#[command(subcommand)]
|
||||
action: commands::node::NodeCmd,
|
||||
},
|
||||
/// Story operations
|
||||
Story {
|
||||
#[command(subcommand)]
|
||||
action: commands::story::StoryCmd,
|
||||
},
|
||||
/// Genesis operations (dev only)
|
||||
Genesis {
|
||||
#[command(subcommand)]
|
||||
action: commands::genesis::GenesisCmd,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let rpc = rpc::Client::new(&cli.rpc);
|
||||
|
||||
match cli.command {
|
||||
Command::Wallet { action } => commands::wallet::run(action).await?,
|
||||
Command::Node { action } => commands::node::run(action, &rpc).await?,
|
||||
Command::Story { action } => commands::story::run(action, &rpc).await?,
|
||||
Command::Genesis{ action } => commands::genesis::run(action, &rpc).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
37
src/rpc.rs
Normal file
37
src/rpc.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub struct Client {
|
||||
endpoint: String,
|
||||
http: reqwest::Client,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(endpoint: &str) -> Self {
|
||||
Self {
|
||||
endpoint: endpoint.to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call(&self, method: &str, params: Value) -> Result<Value> {
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": 1,
|
||||
});
|
||||
let resp: Value = self.http
|
||||
.post(&self.endpoint)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
if let Some(err) = resp.get("error") {
|
||||
anyhow::bail!("RPC error: {}", err);
|
||||
}
|
||||
Ok(resp["result"].clone())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue