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:
parent
2cbbcb00f0
commit
5d4a4c97c1
1 changed files with 70 additions and 5 deletions
|
|
@ -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?)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue