Compare commits

..

No commits in common. "16a4999fdf511c178557b91a71019cb5f4f91e57730e0c4749b3cea067c13d1e" and "61fb9fe2c8a9a24ad95cc3196df668a7a3f202615d0be25e82965eb959fa03b2" have entirely different histories.

9 changed files with 18 additions and 301 deletions

View file

@ -7,17 +7,6 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
## [Unreleased]
## [0.4.0] — 2026-04-24
### Added
- `nu wallet send --to <addr> --amount <shell>`: k256 imzalı TokenTransfer tx gönderir
- `nu story submit`: NodeSubmit tx; UUID temp_id üretir, entry_fee=25_000 Shell
- `nu story register`: VoteRegister tx; stake_lock=10_000 Shell
- `nu story vote --node-id <id> --approve <bool>`: VoteCast tx gönderir
- `nu node stake / unstake`: StakeOp tx gönderir
- `nu node register --stake <amount>`: ValidatorRegister tx gönderir
- Tüm tx-gönderme komutlarında: keystore'dan key yükle → nonce'ı RPC'den al → SHA-256(payload) imzala → nu_sendRawTx
## [0.3.0] — 2026-04-24
### Added

13
Cargo.lock generated
View file

@ -1034,9 +1034,7 @@ dependencies = [
"rpassword",
"serde",
"serde_json",
"sha2",
"tokio",
"uuid",
]
[[package]]
@ -1910,17 +1908,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"

View file

@ -21,5 +21,3 @@ dirs = "5"
argon2 = "0.5"
aes-gcm = "0.10"
rpassword = "7"
sha2 = "0.10"
uuid = { version = "1", features = ["v4"] }

View file

@ -1,5 +1,4 @@
pub mod wallet;
pub mod node;
pub mod nft;
pub mod story;
pub mod genesis;

View file

@ -1,80 +0,0 @@
use anyhow::Result;
use clap::Subcommand;
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
use sha2::{Digest, Sha256};
use serde_json::json;
use crate::{keystore, rpc::Client};
#[derive(Subcommand)]
pub enum NftCmd {
/// Transfer an NFT to another address
Transfer {
#[arg(long)] nft_id: String,
#[arg(long)] to: String,
#[arg(long, default_value = "default")] from: String,
},
/// Claim an exclusive collection from a lineage of NFTs you own
Claim {
/// NFT ids forming the lineage path, from root to deepest (e.g. 0,1,11,115)
#[arg(long, value_delimiter = ',')] nft_ids: Vec<String>,
#[arg(long, default_value = "default")] from: String,
},
}
pub async fn run(cmd: NftCmd, rpc: &Client) -> Result<()> {
match cmd {
NftCmd::Transfer { nft_id, to, from } => {
let (key, sender) = load_key(&from)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = json!({ "NftTransfer": { "nft_id": nft_id, "to": to } });
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Transferred NFT {nft_id}: {}", result["tx_id"].as_str().unwrap_or("?"));
}
NftCmd::Claim { nft_ids, from } => {
let (key, sender) = load_key(&from)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = json!({ "CollectionClaim": { "nft_ids": nft_ids } });
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Collection claimed: {}", result["tx_id"].as_str().unwrap_or("?"));
}
}
Ok(())
}
fn load_key(label: &str) -> Result<(SigningKey, String)> {
let dir = crate::config::keystore_dir();
let path = dir.join(format!("{label}.key"));
let password = keystore::prompt_password(false)?;
let key_bytes = keystore::load_encrypted(&path, &password)?;
let key = SigningKey::from_bytes(key_bytes.as_slice().into())?;
let sender = hex::encode(key.verifying_key().to_sec1_bytes());
Ok((key, sender))
}
async fn send_tx(
key: &SigningKey,
sender: &str,
nonce: u64,
payload: serde_json::Value,
rpc: &Client,
) -> Result<serde_json::Value> {
let payload_bytes = serde_json::to_vec(&payload)?;
let tx_id = hex::encode(Sha256::digest(&payload_bytes));
let sig: Signature = key.sign(tx_id.as_bytes());
let sig_bytes = sig.to_bytes().to_vec();
let tx = json!({
"tx_id": tx_id,
"sender": sender,
"nonce": nonce,
"fee": 0,
"sig": sig_bytes,
"payload": payload,
});
Ok(rpc.call("nu_sendRawTx", vec![tx]).await?)
}

View file

@ -1,9 +1,7 @@
use anyhow::Result;
use clap::Subcommand;
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
use sha2::{Digest, Sha256};
use serde_json::json;
use crate::{keystore, rpc::Client};
use crate::rpc::Client;
#[derive(Subcommand)]
pub enum NodeCmd {
@ -17,20 +15,13 @@ pub enum NodeCmd {
Account {
address: String,
},
/// Stake Shell tokens (StakeOp)
/// Register as validator (stub — Faz 1)
Stake {
#[arg(long)] amount: u64,
#[arg(long, default_value = "default")] from: String,
},
/// Unstake Shell tokens (StakeOp)
/// Unstake from validator set (stub — Faz 1)
Unstake {
#[arg(long)] amount: u64,
#[arg(long, default_value = "default")] from: String,
},
/// Register as a validator (ValidatorRegister)
Register {
#[arg(long)] stake: u64,
#[arg(long, default_value = "default")] from: String,
},
}
@ -58,64 +49,8 @@ pub async fn run(cmd: NodeCmd, rpc: &Client) -> Result<()> {
println!("Nonce : {}", account["nonce"].as_u64().unwrap_or(0));
println!("PoN : {}", account["pon_score"].as_f64().unwrap_or(1.0));
}
NodeCmd::Stake { amount, from } => {
let result = stake_op(rpc, &from, true, amount).await?;
println!("Staked {} Shell: {}", amount, result["tx_id"].as_str().unwrap_or("?"));
}
NodeCmd::Unstake { amount, from } => {
let result = stake_op(rpc, &from, false, amount).await?;
println!("Unstaked {} Shell: {}", amount, result["tx_id"].as_str().unwrap_or("?"));
}
NodeCmd::Register { stake, from } => {
let (key, sender) = load_key(&from)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = json!({ "ValidatorRegister": { "stake": stake } });
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Registered as validator: {}", result["tx_id"].as_str().unwrap_or("?"));
}
NodeCmd::Stake { amount } => println!("TODO: stake {amount} Shell (Faz 1)"),
NodeCmd::Unstake { amount } => println!("TODO: unstake {amount} Shell (Faz 1)"),
}
Ok(())
}
async fn stake_op(rpc: &Client, label: &str, stake: bool, amount: u64) -> Result<serde_json::Value> {
let (key, sender) = load_key(label)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = json!({ "StakeOp": { "stake": stake, "amount": amount } });
send_tx(&key, &sender, nonce, payload, rpc).await
}
fn load_key(label: &str) -> Result<(SigningKey, String)> {
let dir = crate::config::keystore_dir();
let path = dir.join(format!("{label}.key"));
let password = keystore::prompt_password(false)?;
let key_bytes = keystore::load_encrypted(&path, &password)?;
let key = SigningKey::from_bytes(key_bytes.as_slice().into())?;
let sender = hex::encode(key.verifying_key().to_sec1_bytes());
Ok((key, sender))
}
async fn send_tx(
key: &SigningKey,
sender: &str,
nonce: u64,
payload: serde_json::Value,
rpc: &Client,
) -> Result<serde_json::Value> {
let payload_bytes = serde_json::to_vec(&payload)?;
let tx_id = hex::encode(Sha256::digest(&payload_bytes));
let sig: Signature = key.sign(tx_id.as_bytes());
let sig_bytes = sig.to_bytes().to_vec();
let tx = json!({
"tx_id": tx_id,
"sender": sender,
"nonce": nonce,
"fee": 0,
"sig": sig_bytes,
"payload": payload,
});
Ok(rpc.call("nu_sendRawTx", vec![tx]).await?)
}

View file

@ -1,11 +1,7 @@
use anyhow::Result;
use clap::Subcommand;
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
use sha2::{Digest, Sha256};
use serde_json::json;
use uuid::Uuid;
use crate::{keystore, rpc::Client};
use crate::rpc::Client;
#[derive(Subcommand)]
pub enum StoryCmd {
@ -24,23 +20,20 @@ pub enum StoryCmd {
},
/// List nodes currently open for voting
Pending,
/// Submit a new story node
/// Submit a new story node (stub — requires wallet integration)
Submit {
#[arg(long)] story_id: String,
#[arg(long)] parent_id: String,
#[arg(long)] ipfs_hash: String,
#[arg(long, default_value = "default")] from: String,
},
/// Register to vote on a node (locks stake)
/// Register to vote on a node (stub — Faz 1)
Register {
#[arg(long)] node_id: String,
#[arg(long, default_value = "default")] from: String,
},
/// Cast a vote on a node
/// Cast a vote (stub — Faz 1)
Vote {
#[arg(long)] node_id: String,
#[arg(long)] approve: bool,
#[arg(long, default_value = "default")] from: String,
},
}
@ -62,79 +55,15 @@ pub async fn run(cmd: StoryCmd, rpc: &Client) -> Result<()> {
let result = rpc.call("nu_listPendingVotes", vec![]).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
}
StoryCmd::Submit { story_id, parent_id, ipfs_hash, from } => {
let (key, sender) = load_key(&from)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let temp_id = Uuid::new_v4().to_string();
let payload = json!({
"NodeSubmit": {
"story_id": story_id,
"parent_node_id": parent_id,
"content_hash": ipfs_hash,
"temp_id": temp_id,
"entry_fee": 25_000u64,
StoryCmd::Submit { story_id, parent_id, ipfs_hash } => {
println!("TODO: submit node story={story_id} parent={parent_id} hash={ipfs_hash} (requires wallet — Faz 1)");
}
});
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Submitted: tx={} temp_id={temp_id}", result["tx_id"].as_str().unwrap_or("?"));
StoryCmd::Register { node_id } => {
println!("TODO: register vote for node={node_id} (Faz 1)");
}
StoryCmd::Register { node_id, from } => {
let (key, sender) = load_key(&from)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = json!({
"VoteRegister": { "node_id": node_id, "stake_lock": 10_000u64 }
});
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Registered: {}", result["tx_id"].as_str().unwrap_or("?"));
}
StoryCmd::Vote { node_id, approve, from } => {
let (key, sender) = load_key(&from)?;
let account = rpc.call("nu_getAccount", vec![json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = json!({
"VoteCast": { "node_id": node_id, "approve": approve }
});
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Vote cast: {}", result["tx_id"].as_str().unwrap_or("?"));
StoryCmd::Vote { node_id, approve } => {
println!("TODO: vote node={node_id} approve={approve} (Faz 1)");
}
}
Ok(())
}
fn load_key(label: &str) -> Result<(SigningKey, String)> {
let dir = crate::config::keystore_dir();
let path = dir.join(format!("{label}.key"));
let password = keystore::prompt_password(false)?;
let key_bytes = keystore::load_encrypted(&path, &password)?;
let key = SigningKey::from_bytes(key_bytes.as_slice().into())?;
let sender = hex::encode(key.verifying_key().to_sec1_bytes());
Ok((key, sender))
}
async fn send_tx(
key: &SigningKey,
sender: &str,
nonce: u64,
payload: serde_json::Value,
rpc: &Client,
) -> Result<serde_json::Value> {
let payload_bytes = serde_json::to_vec(&payload)?;
let tx_id = hex::encode(Sha256::digest(&payload_bytes));
let sig: Signature = key.sign(tx_id.as_bytes());
let sig_bytes = sig.to_bytes().to_vec();
let tx = json!({
"tx_id": tx_id,
"sender": sender,
"nonce": nonce,
"fee": 0,
"sig": sig_bytes,
"payload": payload,
});
Ok(rpc.call("nu_sendRawTx", vec![tx]).await?)
}

View file

@ -1,8 +1,7 @@
use anyhow::Result;
use clap::Subcommand;
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
use k256::ecdsa::SigningKey;
use rand::rngs::OsRng;
use sha2::{Digest, Sha256};
use crate::keystore;
@ -18,7 +17,7 @@ pub enum WalletCmd {
#[arg(long, default_value = "default")]
label: String,
},
/// Send Shell tokens to another address
/// Send NUT tokens (stub — Faz 1 tx signing)
Send {
#[arg(long)] to: String,
#[arg(long)] amount: u64,
@ -58,40 +57,7 @@ pub async fn run(cmd: WalletCmd) -> Result<()> {
}
WalletCmd::Send { to, amount, from } => {
let rpc_url = std::env::var("NU_RPC_URL")
.unwrap_or_else(|_| "http://localhost:8545".to_string());
let client = crate::rpc::Client::new(&rpc_url);
let dir = crate::config::keystore_dir();
let path = dir.join(format!("{from}.key"));
let password = keystore::prompt_password(false)?;
let key_bytes = keystore::load_encrypted(&path, &password)?;
let key = SigningKey::from_bytes(key_bytes.as_slice().into())?;
let sender = hex::encode(key.verifying_key().to_sec1_bytes());
let account = client.call("nu_getAccount", vec![serde_json::json!(sender)]).await?;
let nonce: u64 = account["nonce"].as_u64().unwrap_or(0);
let payload = serde_json::json!({
"TokenTransfer": { "to": to, "amount": amount }
});
let payload_bytes = serde_json::to_vec(&payload)?;
let tx_id = hex::encode(Sha256::digest(&payload_bytes));
let sig: Signature = key.sign(tx_id.as_bytes());
let sig_bytes = sig.to_bytes().to_vec();
let tx = serde_json::json!({
"tx_id": tx_id,
"sender": sender,
"nonce": nonce,
"fee": 0,
"sig": sig_bytes,
"payload": payload,
});
let result = client.call("nu_sendRawTx", vec![tx]).await?;
println!("Sent: {}", result["tx_id"].as_str().unwrap_or("?"));
println!("TODO: nu wallet send --to {to} --amount {amount} Shell (from: {from}) — Faz 1");
}
}
Ok(())

View file

@ -34,11 +34,6 @@ enum Command {
#[command(subcommand)]
action: commands::story::StoryCmd,
},
/// NFT operations
Nft {
#[command(subcommand)]
action: commands::nft::NftCmd,
},
/// Genesis operations (dev only)
Genesis {
#[command(subcommand)]
@ -55,7 +50,6 @@ async fn main() -> Result<()> {
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::Nft { action } => commands::nft::run(action, &rpc).await?,
Command::Genesis{ action } => commands::genesis::run(action, &rpc).await?,
}