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 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-24 15:03:14 +03:00
parent 2cbbcb00f0
commit 5d4a4c97c1

View file

@ -1,7 +1,9 @@
use anyhow::Result; use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
use sha2::{Digest, Sha256};
use serde_json::json; use serde_json::json;
use crate::rpc::Client; use crate::{keystore, rpc::Client};
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum NodeCmd { pub enum NodeCmd {
@ -15,13 +17,20 @@ pub enum NodeCmd {
Account { Account {
address: String, address: String,
}, },
/// Register as validator (stub — Faz 1) /// Stake Shell tokens (StakeOp)
Stake { Stake {
#[arg(long)] amount: u64, #[arg(long)] amount: u64,
#[arg(long, default_value = "default")] from: String,
}, },
/// Unstake from validator set (stub — Faz 1) /// Unstake Shell tokens (StakeOp)
Unstake { Unstake {
#[arg(long)] amount: u64, #[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!("Nonce : {}", account["nonce"].as_u64().unwrap_or(0));
println!("PoN : {}", account["pon_score"].as_f64().unwrap_or(1.0)); println!("PoN : {}", account["pon_score"].as_f64().unwrap_or(1.0));
} }
NodeCmd::Stake { amount } => println!("TODO: stake {amount} Shell (Faz 1)"), NodeCmd::Stake { amount, from } => {
NodeCmd::Unstake { amount } => println!("TODO: unstake {amount} Shell (Faz 1)"), 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(()) 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?)
}