From 2335932753b607bf7c7090df72f681c5fce8075f110746940943f4fb1a6d18ae Mon Sep 17 00:00:00 2001 From: Mukan Erkin Date: Fri, 24 Apr 2026 14:58:03 +0300 Subject: [PATCH] feat(wallet): implement wallet send with k256 signing - Loads encrypted keystore, derives sender address from verifying key - Fetches current nonce via nu_getAccount RPC - Builds TokenTransfer payload, signs SHA-256(tx_id) with secp256k1 - Broadcasts via nu_sendRawTx, prints returned tx_id - Add sha2 dependency Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 1 + Cargo.toml | 1 + src/commands/wallet.rs | 40 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0888621..483384f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1034,6 +1034,7 @@ dependencies = [ "rpassword", "serde", "serde_json", + "sha2", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index dcb5889..aa9b56f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ dirs = "5" argon2 = "0.5" aes-gcm = "0.10" rpassword = "7" +sha2 = "0.10" diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index a746c57..88bbcd8 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -1,7 +1,8 @@ use anyhow::Result; use clap::Subcommand; -use k256::ecdsa::SigningKey; +use k256::ecdsa::{signature::Signer, Signature, SigningKey}; use rand::rngs::OsRng; +use sha2::{Digest, Sha256}; use crate::keystore; @@ -17,7 +18,7 @@ pub enum WalletCmd { #[arg(long, default_value = "default")] label: String, }, - /// Send NUT tokens (stub — Faz 1 tx signing) + /// Send Shell tokens to another address Send { #[arg(long)] to: String, #[arg(long)] amount: u64, @@ -57,7 +58,40 @@ pub async fn run(cmd: WalletCmd) -> Result<()> { } WalletCmd::Send { to, amount, from } => { - println!("TODO: nu wallet send --to {to} --amount {amount} Shell (from: {from}) — Faz 1"); + 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("?")); } } Ok(())