feat(story): implement submit/register/vote commands with tx signing
- NodeSubmit: generates UUID temp_id, signs and broadcasts via nu_sendRawTx - VoteRegister: locks 10,000 Shell stake for node voting - VoteCast: signs approve/reject vote - Extract load_key() and send_tx() helpers shared across story commands - Add uuid and sha2 dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2335932753
commit
2cbbcb00f0
3 changed files with 94 additions and 10 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -1036,6 +1036,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1909,6 +1910,17 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ argon2 = "0.5"
|
|||
aes-gcm = "0.10"
|
||||
rpassword = "7"
|
||||
sha2 = "0.10"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
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 uuid::Uuid;
|
||||
|
||||
use crate::{keystore, rpc::Client};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum StoryCmd {
|
||||
|
|
@ -20,20 +24,23 @@ pub enum StoryCmd {
|
|||
},
|
||||
/// List nodes currently open for voting
|
||||
Pending,
|
||||
/// Submit a new story node (stub — requires wallet integration)
|
||||
/// Submit a new story node
|
||||
Submit {
|
||||
#[arg(long)] story_id: String,
|
||||
#[arg(long)] parent_id: String,
|
||||
#[arg(long)] ipfs_hash: String,
|
||||
#[arg(long, default_value = "default")] from: String,
|
||||
},
|
||||
/// Register to vote on a node (stub — Faz 1)
|
||||
/// Register to vote on a node (locks stake)
|
||||
Register {
|
||||
#[arg(long)] node_id: String,
|
||||
#[arg(long, default_value = "default")] from: String,
|
||||
},
|
||||
/// Cast a vote (stub — Faz 1)
|
||||
/// Cast a vote on a node
|
||||
Vote {
|
||||
#[arg(long)] node_id: String,
|
||||
#[arg(long)] approve: bool,
|
||||
#[arg(long, default_value = "default")] from: String,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -55,15 +62,79 @@ pub async fn run(cmd: StoryCmd, rpc: &Client) -> Result<()> {
|
|||
let result = rpc.call("nu_listPendingVotes", vec![]).await?;
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
}
|
||||
StoryCmd::Submit { story_id, parent_id, ipfs_hash } => {
|
||||
println!("TODO: submit node story={story_id} parent={parent_id} hash={ipfs_hash} (requires wallet — Faz 1)");
|
||||
StoryCmd::Submit { story_id, parent_id, ipfs_hash, 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 temp_id = Uuid::new_v4().to_string();
|
||||
|
||||
let payload = json!({
|
||||
"NodeSubmit": {
|
||||
"story_id": story_id,
|
||||
"parent_node_id": parent_id,
|
||||
"content_hash": ipfs_hash,
|
||||
"temp_id": temp_id,
|
||||
"entry_fee": 25_000u64,
|
||||
}
|
||||
StoryCmd::Register { node_id } => {
|
||||
println!("TODO: register vote for node={node_id} (Faz 1)");
|
||||
});
|
||||
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
|
||||
println!("Submitted: tx={} temp_id={temp_id}", result["tx_id"].as_str().unwrap_or("?"));
|
||||
}
|
||||
StoryCmd::Vote { node_id, approve } => {
|
||||
println!("TODO: vote node={node_id} approve={approve} (Faz 1)");
|
||||
StoryCmd::Register { node_id, 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!({
|
||||
"VoteRegister": { "node_id": node_id, "stake_lock": 10_000u64 }
|
||||
});
|
||||
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
|
||||
println!("Registered: {}", result["tx_id"].as_str().unwrap_or("?"));
|
||||
}
|
||||
StoryCmd::Vote { node_id, approve, 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!({
|
||||
"VoteCast": { "node_id": node_id, "approve": approve }
|
||||
});
|
||||
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
|
||||
println!("Vote cast: {}", result["tx_id"].as_str().unwrap_or("?"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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