Initial setup: Rust CLI tool for blockchain node
- clap 4 CLI with wallet, tx, mine, block, chain commands - reqwest HTTP client (REST API only, no lib dependency) - Colored output with owo-colors + comfy-table - Mining progress bar with indicatif
This commit is contained in:
commit
96b4dfa77b
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.env
|
||||||
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "blockchain-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Alex"]
|
||||||
|
license = "MIT"
|
||||||
|
description = "CLI tool for interacting with the blockchain node via REST API"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "blockchain-cli"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
owo-colors = "4"
|
||||||
|
comfy-table = "7"
|
||||||
|
indicatif = "0.17"
|
||||||
|
anyhow = "1"
|
||||||
|
dirs = "6"
|
||||||
|
chrono = "0.4"
|
||||||
125
README.md
Normal file
125
README.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# blockchain-cli
|
||||||
|
|
||||||
|
Rust command-line tool for interacting with the blockchain node. Communicates entirely via REST API - no direct library dependency on `blockchain-core`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
blockchain-cli/
|
||||||
|
└── src/
|
||||||
|
├── main.rs # Clap parsing + command dispatch
|
||||||
|
├── commands/
|
||||||
|
│ ├── mod.rs # Command enum (subcommands)
|
||||||
|
│ ├── wallet.rs # create, balance, list
|
||||||
|
│ ├── transaction.rs # send, list-pending
|
||||||
|
│ ├── mining.rs # mine (with progress bar)
|
||||||
|
│ ├── blocks.rs # list, get
|
||||||
|
│ └── node.rs # info, validate
|
||||||
|
├── client.rs # NodeClient (reqwest HTTP wrapper)
|
||||||
|
├── display.rs # Colored/formatted terminal output
|
||||||
|
├── config.rs # Node URL, wallet directory
|
||||||
|
└── error.rs # CLI error types (anyhow)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
USAGE:
|
||||||
|
blockchain-cli [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
wallet Wallet management
|
||||||
|
tx Transaction operations
|
||||||
|
mine Mine a new block
|
||||||
|
block Block explorer
|
||||||
|
chain Chain information
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--node <URL> Node URL [default: http://localhost:3000]
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wallet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new wallet (keypair saved locally)
|
||||||
|
blockchain-cli wallet create
|
||||||
|
|
||||||
|
# Check balance
|
||||||
|
blockchain-cli wallet balance <ADDRESS>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send tokens
|
||||||
|
blockchain-cli tx send <FROM_ADDRESS> <TO_ADDRESS> <AMOUNT>
|
||||||
|
|
||||||
|
# View pending transactions
|
||||||
|
blockchain-cli tx pending
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mining
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mine pending transactions (with progress indicator)
|
||||||
|
blockchain-cli mine <MINER_ADDRESS>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blocks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent blocks
|
||||||
|
blockchain-cli block list [--limit 10]
|
||||||
|
|
||||||
|
# Get block details
|
||||||
|
blockchain-cli block get <HASH>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chain info (height, difficulty, latest hash)
|
||||||
|
blockchain-cli chain info
|
||||||
|
|
||||||
|
# Validate entire chain
|
||||||
|
blockchain-cli chain validate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice | Rationale |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| CLI framework | clap 4 (derive) | Industry standard, excellent UX |
|
||||||
|
| HTTP client | reqwest | Async, well-maintained |
|
||||||
|
| Output | owo-colors + comfy-table | Colored text + formatted tables |
|
||||||
|
| Progress | indicatif | Mining progress bar |
|
||||||
|
| Errors | anyhow | Convenient error chaining for CLI |
|
||||||
|
| Node coupling | REST API only | Fully decoupled from core lib |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Start blockchain-node first (see blockchain-core repo)
|
||||||
|
# Then use the CLI:
|
||||||
|
blockchain-cli chain info
|
||||||
|
blockchain-cli wallet create
|
||||||
|
blockchain-cli mine <YOUR_ADDRESS>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The CLI looks for wallets in `~/.blockchain-cli/wallets/`. Node URL defaults to `http://localhost:3000` and can be overridden with `--node`.
|
||||||
|
|
||||||
|
## Related Repos
|
||||||
|
|
||||||
|
- [blockchain-core](../blockchain-core) - Core library + REST API node
|
||||||
|
- [blockchain-flutter](../blockchain-flutter) - Flutter mobile/web app
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
76
src/client.rs
Normal file
76
src/client.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
/// HTTP client wrapper for the blockchain node API.
|
||||||
|
pub struct NodeClient {
|
||||||
|
base_url: String,
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeClient {
|
||||||
|
pub fn new(base_url: &str) -> Self {
|
||||||
|
NodeClient {
|
||||||
|
base_url: base_url.trim_end_matches('/').to_string(),
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||||
|
let url = format!("{}{}", self.base_url, path);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to node")?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("API error {}: {}", status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.json().await.context("failed to parse response")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post<B: serde::Serialize, T: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: &B,
|
||||||
|
) -> Result<T> {
|
||||||
|
let url = format!("{}{}", self.base_url, path);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.json(body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to node")?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("API error {}: {}", status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.json().await.context("failed to parse response")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||||
|
let url = format!("{}{}", self.base_url, path);
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.post(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to connect to node")?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let body = resp.text().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("API error {}: {}", status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.json().await.context("failed to parse response")
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/commands/blocks.rs
Normal file
85
src/commands/blocks.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::client::NodeClient;
|
||||||
|
use crate::display;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum BlockCommand {
|
||||||
|
/// List recent blocks
|
||||||
|
List {
|
||||||
|
/// Number of blocks to show
|
||||||
|
#[arg(short, long, default_value = "10")]
|
||||||
|
limit: usize,
|
||||||
|
},
|
||||||
|
/// Get block by hash
|
||||||
|
Get {
|
||||||
|
/// Block hash
|
||||||
|
hash: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(command: BlockCommand, client: &NodeClient) -> Result<()> {
|
||||||
|
match command {
|
||||||
|
BlockCommand::List { limit } => list(client, limit).await,
|
||||||
|
BlockCommand::Get { hash } => get(client, &hash).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list(client: &NodeClient, limit: usize) -> Result<()> {
|
||||||
|
let path = format!("/api/blocks?limit={}", limit);
|
||||||
|
let resp: serde_json::Value = client.get(&path).await?;
|
||||||
|
|
||||||
|
let blocks = resp["blocks"].as_array().unwrap();
|
||||||
|
display::print_header(&format!("Blocks ({} total)", resp["total"].as_u64().unwrap_or(0)));
|
||||||
|
|
||||||
|
let rows: Vec<(&str, String)> = blocks
|
||||||
|
.iter()
|
||||||
|
.map(|b| {
|
||||||
|
let hash = b["header"]["hash"].as_str().unwrap_or("?");
|
||||||
|
let index = b["header"]["index"].as_u64().unwrap_or(0);
|
||||||
|
let txs = b["transactions"].as_array().map(|a| a.len()).unwrap_or(0);
|
||||||
|
// Return static str by leaking - acceptable for CLI display
|
||||||
|
let label = format!("#{}", index);
|
||||||
|
let value = format!("{} ({} txs)", &hash[..16], txs);
|
||||||
|
(Box::leak(label.into_boxed_str()) as &str, value)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
display::print_key_value_table(rows);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(client: &NodeClient, hash: &str) -> Result<()> {
|
||||||
|
let path = format!("/api/blocks/{}", hash);
|
||||||
|
let block: serde_json::Value = client.get(&path).await?;
|
||||||
|
|
||||||
|
display::print_header("Block Details");
|
||||||
|
display::print_info(
|
||||||
|
"Index",
|
||||||
|
&block["header"]["index"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info("Hash", block["header"]["hash"].as_str().unwrap_or("?"));
|
||||||
|
display::print_info(
|
||||||
|
"Previous",
|
||||||
|
block["header"]["previous_hash"].as_str().unwrap_or("?"),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Nonce",
|
||||||
|
&block["header"]["nonce"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Difficulty",
|
||||||
|
&block["header"]["difficulty"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Transactions",
|
||||||
|
&block["transactions"]
|
||||||
|
.as_array()
|
||||||
|
.map(|a| a.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
47
src/commands/mining.rs
Normal file
47
src/commands/mining.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
|
use crate::client::NodeClient;
|
||||||
|
use crate::display;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
pub async fn execute(client: &NodeClient, miner_address: &str) -> Result<()> {
|
||||||
|
display::print_header("Mining new block...");
|
||||||
|
|
||||||
|
let pb = ProgressBar::new_spinner();
|
||||||
|
pb.set_style(
|
||||||
|
ProgressStyle::default_spinner()
|
||||||
|
.template("{spinner:.cyan} {msg}")
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
pb.set_message("Mining in progress...");
|
||||||
|
pb.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||||
|
|
||||||
|
let body = serde_json::json!({ "miner_address": miner_address });
|
||||||
|
let block: serde_json::Value = client.post("/api/mine", &body).await?;
|
||||||
|
|
||||||
|
pb.finish_and_clear();
|
||||||
|
|
||||||
|
display::print_success("Block mined successfully!");
|
||||||
|
display::print_info(
|
||||||
|
"Index",
|
||||||
|
&block["header"]["index"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Hash",
|
||||||
|
block["header"]["hash"].as_str().unwrap_or("?"),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Nonce",
|
||||||
|
&block["header"]["nonce"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Transactions",
|
||||||
|
&block["transactions"]
|
||||||
|
.as_array()
|
||||||
|
.map(|a| a.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
36
src/commands/mod.rs
Normal file
36
src/commands/mod.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
pub mod blocks;
|
||||||
|
pub mod mining;
|
||||||
|
pub mod node;
|
||||||
|
pub mod transaction;
|
||||||
|
pub mod wallet;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Wallet management
|
||||||
|
Wallet {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: wallet::WalletCommand,
|
||||||
|
},
|
||||||
|
/// Transaction operations
|
||||||
|
Tx {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: transaction::TxCommand,
|
||||||
|
},
|
||||||
|
/// Mine a new block
|
||||||
|
Mine {
|
||||||
|
/// Miner address to receive block reward
|
||||||
|
miner_address: String,
|
||||||
|
},
|
||||||
|
/// Block explorer
|
||||||
|
Block {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: blocks::BlockCommand,
|
||||||
|
},
|
||||||
|
/// Chain information
|
||||||
|
Chain {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: node::ChainCommand,
|
||||||
|
},
|
||||||
|
}
|
||||||
56
src/commands/node.rs
Normal file
56
src/commands/node.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::client::NodeClient;
|
||||||
|
use crate::display;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum ChainCommand {
|
||||||
|
/// Display chain information
|
||||||
|
Info,
|
||||||
|
/// Validate the entire chain
|
||||||
|
Validate,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(command: ChainCommand, client: &NodeClient) -> Result<()> {
|
||||||
|
match command {
|
||||||
|
ChainCommand::Info => info(client).await,
|
||||||
|
ChainCommand::Validate => validate(client).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn info(client: &NodeClient) -> Result<()> {
|
||||||
|
let info: serde_json::Value = client.get("/api/chain/info").await?;
|
||||||
|
|
||||||
|
display::print_header("Chain Info");
|
||||||
|
display::print_info("Length", &info["length"].as_u64().unwrap_or(0).to_string());
|
||||||
|
display::print_info(
|
||||||
|
"Difficulty",
|
||||||
|
&info["difficulty"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Latest Hash",
|
||||||
|
info["latest_hash"].as_str().unwrap_or("?"),
|
||||||
|
);
|
||||||
|
display::print_info(
|
||||||
|
"Block Reward",
|
||||||
|
&info["block_reward"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate(client: &NodeClient) -> Result<()> {
|
||||||
|
let result: serde_json::Value = client.post_empty("/api/chain/validate").await?;
|
||||||
|
|
||||||
|
if result["valid"].as_bool().unwrap_or(false) {
|
||||||
|
display::print_success("Chain is valid!");
|
||||||
|
} else {
|
||||||
|
display::print_error(&format!(
|
||||||
|
"Chain is invalid: {}",
|
||||||
|
result["error"].as_str().unwrap_or("unknown error")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
69
src/commands/transaction.rs
Normal file
69
src/commands/transaction.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::client::NodeClient;
|
||||||
|
use crate::display;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum TxCommand {
|
||||||
|
/// Send tokens
|
||||||
|
Send {
|
||||||
|
/// Sender address
|
||||||
|
from: String,
|
||||||
|
/// Recipient address
|
||||||
|
to: String,
|
||||||
|
/// Amount to send
|
||||||
|
amount: u64,
|
||||||
|
},
|
||||||
|
/// List pending transactions
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(command: TxCommand, client: &NodeClient) -> Result<()> {
|
||||||
|
match command {
|
||||||
|
TxCommand::Send { from, to, amount } => send(client, &from, &to, amount).await,
|
||||||
|
TxCommand::Pending => pending(client).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send(client: &NodeClient, from: &str, to: &str, amount: u64) -> Result<()> {
|
||||||
|
// Note: In a real implementation, the CLI would load the private key,
|
||||||
|
// sign the transaction locally, and send the signed tx.
|
||||||
|
// For now, this sends unsigned (the signing logic would need the wallet file).
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"from": from,
|
||||||
|
"to": to,
|
||||||
|
"amount": amount,
|
||||||
|
"nonce": 0,
|
||||||
|
"timestamp": chrono::Utc::now().timestamp_millis(),
|
||||||
|
"signature": ""
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx: serde_json::Value = client.post("/api/transactions", &body).await?;
|
||||||
|
display::print_header("Transaction Submitted");
|
||||||
|
display::print_info("ID", tx["id"].as_str().unwrap_or("?"));
|
||||||
|
display::print_info("From", from);
|
||||||
|
display::print_info("To", to);
|
||||||
|
display::print_info("Amount", &amount.to_string());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pending(client: &NodeClient) -> Result<()> {
|
||||||
|
let txs: Vec<serde_json::Value> = client.get("/api/transactions/pending").await?;
|
||||||
|
|
||||||
|
display::print_header(&format!("Pending Transactions ({})", txs.len()));
|
||||||
|
for tx in &txs {
|
||||||
|
display::print_info(
|
||||||
|
tx["id"].as_str().unwrap_or("?"),
|
||||||
|
&format!(
|
||||||
|
"{} -> {} : {}",
|
||||||
|
tx["from"].as_str().unwrap_or("?"),
|
||||||
|
tx["to"].as_str().unwrap_or("?"),
|
||||||
|
tx["amount"].as_u64().unwrap_or(0)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
53
src/commands/wallet.rs
Normal file
53
src/commands/wallet.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::client::NodeClient;
|
||||||
|
use crate::display;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum WalletCommand {
|
||||||
|
/// Create a new wallet
|
||||||
|
Create,
|
||||||
|
/// Check wallet balance
|
||||||
|
Balance {
|
||||||
|
/// Wallet address
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(command: WalletCommand, client: &NodeClient) -> Result<()> {
|
||||||
|
match command {
|
||||||
|
WalletCommand::Create => create(client).await,
|
||||||
|
WalletCommand::Balance { address } => balance(client, &address).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(client: &NodeClient) -> Result<()> {
|
||||||
|
let wallet: serde_json::Value = client.post_empty("/api/wallets").await?;
|
||||||
|
|
||||||
|
display::print_header("New Wallet Created");
|
||||||
|
display::print_info("Address", wallet["address"].as_str().unwrap_or("?"));
|
||||||
|
if let Some(key) = wallet["signing_key_hex"].as_str() {
|
||||||
|
display::print_info("Private Key", key);
|
||||||
|
println!("\n {} Save this private key securely!", "⚠".yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn balance(client: &NodeClient, address: &str) -> Result<()> {
|
||||||
|
let path = format!("/api/wallets/{}/balance", address);
|
||||||
|
let info: serde_json::Value = client.get(&path).await?;
|
||||||
|
|
||||||
|
display::print_header("Wallet Balance");
|
||||||
|
display::print_info("Address", address);
|
||||||
|
display::print_info(
|
||||||
|
"Balance",
|
||||||
|
&info["balance"].as_u64().unwrap_or(0).to_string(),
|
||||||
|
);
|
||||||
|
display::print_info("Nonce", &info["nonce"].as_u64().unwrap_or(0).to_string());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
9
src/config.rs
Normal file
9
src/config.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const DEFAULT_NODE_URL: &str = "http://localhost:3000";
|
||||||
|
|
||||||
|
/// Get the wallet storage directory (~/.blockchain-cli/wallets/).
|
||||||
|
pub fn wallet_dir() -> PathBuf {
|
||||||
|
let base = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
base.join(".blockchain-cli").join("wallets")
|
||||||
|
}
|
||||||
28
src/display.rs
Normal file
28
src/display.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use comfy_table::{Cell, Table};
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
|
||||||
|
pub fn print_header(text: &str) {
|
||||||
|
println!("\n{}", text.bold().cyan());
|
||||||
|
println!("{}", "─".repeat(50).dimmed());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_success(text: &str) {
|
||||||
|
println!("{} {}", "✓".green(), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_error(text: &str) {
|
||||||
|
println!("{} {}", "✗".red(), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_info(label: &str, value: &str) {
|
||||||
|
println!(" {}: {}", label.dimmed(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_key_value_table(rows: Vec<(&str, String)>) {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.set_header(vec!["Field", "Value"]);
|
||||||
|
for (key, value) in rows {
|
||||||
|
table.add_row(vec![Cell::new(key), Cell::new(value)]);
|
||||||
|
}
|
||||||
|
println!("{table}");
|
||||||
|
}
|
||||||
1
src/error.rs
Normal file
1
src/error.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub type Result<T> = anyhow::Result<T>;
|
||||||
37
src/main.rs
Normal file
37
src/main.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
mod client;
|
||||||
|
mod commands;
|
||||||
|
mod config;
|
||||||
|
mod display;
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use commands::Commands;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "blockchain-cli")]
|
||||||
|
#[command(about = "CLI tool for interacting with the blockchain node")]
|
||||||
|
#[command(version)]
|
||||||
|
struct Cli {
|
||||||
|
/// Node URL
|
||||||
|
#[arg(long, default_value = config::DEFAULT_NODE_URL)]
|
||||||
|
node: String,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let client = client::NodeClient::new(&cli.node);
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Wallet { command } => commands::wallet::execute(command, &client).await?,
|
||||||
|
Commands::Tx { command } => commands::transaction::execute(command, &client).await?,
|
||||||
|
Commands::Mine { miner_address } => commands::mining::execute(&client, &miner_address).await?,
|
||||||
|
Commands::Block { command } => commands::blocks::execute(command, &client).await?,
|
||||||
|
Commands::Chain { command } => commands::node::execute(command, &client).await?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user