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 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-24 14:58:03 +03:00
parent 61fb9fe2c8
commit 2335932753
3 changed files with 39 additions and 3 deletions

1
Cargo.lock generated
View file

@ -1034,6 +1034,7 @@ dependencies = [
"rpassword",
"serde",
"serde_json",
"sha2",
"tokio",
]

View file

@ -21,3 +21,4 @@ dirs = "5"
argon2 = "0.5"
aes-gcm = "0.10"
rpassword = "7"
sha2 = "0.10"

View file

@ -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(())