From 5d4a4c97c15183335ef7c3e2e083361e947ac5e812404f1af1352608a97bccd6 Mon Sep 17 00:00:00 2001 From: Mukan Erkin Date: Fri, 24 Apr 2026 15:03:14 +0300 Subject: [PATCH] feat(node): implement stake/unstake/register commands with tx signing - NodeCmd::Stake/Unstake: sends StakeOp tx signed with keystore key - NodeCmd::Register: sends ValidatorRegister tx - Extract load_key() and send_tx() local helpers Co-Authored-By: Claude Sonnet 4.6 --- src/commands/node.rs | 75 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/src/commands/node.rs b/src/commands/node.rs index 8b2d9ab..271c9cc 100644 --- a/src/commands/node.rs +++ b/src/commands/node.rs @@ -1,7 +1,9 @@ use anyhow::Result; use clap::Subcommand; +use k256::ecdsa::{signature::Signer, Signature, SigningKey}; +use sha2::{Digest, Sha256}; use serde_json::json; -use crate::rpc::Client; +use crate::{keystore, rpc::Client}; #[derive(Subcommand)] pub enum NodeCmd { @@ -15,13 +17,20 @@ pub enum NodeCmd { Account { address: String, }, - /// Register as validator (stub — Faz 1) + /// Stake Shell tokens (StakeOp) Stake { #[arg(long)] amount: u64, + #[arg(long, default_value = "default")] from: String, }, - /// Unstake from validator set (stub — Faz 1) + /// Unstake Shell tokens (StakeOp) 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, }, } @@ -49,8 +58,64 @@ 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 } => println!("TODO: stake {amount} Shell (Faz 1)"), - NodeCmd::Unstake { amount } => println!("TODO: unstake {amount} Shell (Faz 1)"), + 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("?")); + } } Ok(()) } + +async fn stake_op(rpc: &Client, label: &str, stake: bool, amount: u64) -> Result { + 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 { + 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?) +}