Compare commits
No commits in common. "16a4999fdf511c178557b91a71019cb5f4f91e57730e0c4749b3cea067c13d1e" and "61fb9fe2c8a9a24ad95cc3196df668a7a3f202615d0be25e82965eb959fa03b2" have entirely different histories.
16a4999fdf
...
61fb9fe2c8
9 changed files with 18 additions and 301 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -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
13
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -21,5 +21,3 @@ dirs = "5"
|
|||
argon2 = "0.5"
|
||||
aes-gcm = "0.10"
|
||||
rpassword = "7"
|
||||
sha2 = "0.10"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
pub mod wallet;
|
||||
pub mod node;
|
||||
pub mod nft;
|
||||
pub mod story;
|
||||
pub mod genesis;
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
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::Submit { story_id, parent_id, ipfs_hash } => {
|
||||
println!("TODO: submit node story={story_id} parent={parent_id} hash={ipfs_hash} (requires wallet — 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::Register { node_id } => {
|
||||
println!("TODO: register vote for node={node_id} (Faz 1)");
|
||||
}
|
||||
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?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue