feat(nft): add nft transfer and collection claim CLI commands

- nu nft transfer --nft-id <id> --to <addr>: signed NftTransfer tx
- nu nft claim --nft-ids 0,1,11,115: signed CollectionClaim tx with comma-separated path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mukan Erkin TÖRÜK 2026-04-24 15:10:39 +03:00
parent e6616d1cf9
commit 16a4999fdf
3 changed files with 87 additions and 0 deletions

View file

@ -1,4 +1,5 @@
pub mod wallet; pub mod wallet;
pub mod node; pub mod node;
pub mod nft;
pub mod story; pub mod story;
pub mod genesis; pub mod genesis;

80
src/commands/nft.rs Normal file
View file

@ -0,0 +1,80 @@
use anyhow::Result;
use clap::Subcommand;
use k256::ecdsa::{signature::Signer, Signature, SigningKey};
use sha2::{Digest, Sha256};
use serde_json::json;
use crate::{keystore, rpc::Client};
#[derive(Subcommand)]
pub enum NftCmd {
/// Transfer an NFT to another address
Transfer {
#[arg(long)] nft_id: String,
#[arg(long)] to: String,
#[arg(long, default_value = "default")] from: String,
},
/// Claim an exclusive collection from a lineage of NFTs you own
Claim {
/// NFT ids forming the lineage path, from root to deepest (e.g. 0,1,11,115)
#[arg(long, value_delimiter = ',')] nft_ids: Vec<String>,
#[arg(long, default_value = "default")] from: String,
},
}
pub async fn run(cmd: NftCmd, rpc: &Client) -> Result<()> {
match cmd {
NftCmd::Transfer { nft_id, to, 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!({ "NftTransfer": { "nft_id": nft_id, "to": to } });
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Transferred NFT {nft_id}: {}", result["tx_id"].as_str().unwrap_or("?"));
}
NftCmd::Claim { nft_ids, 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!({ "CollectionClaim": { "nft_ids": nft_ids } });
let result = send_tx(&key, &sender, nonce, payload, rpc).await?;
println!("Collection claimed: {}", 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?)
}

View file

@ -34,6 +34,11 @@ enum Command {
#[command(subcommand)] #[command(subcommand)]
action: commands::story::StoryCmd, action: commands::story::StoryCmd,
}, },
/// NFT operations
Nft {
#[command(subcommand)]
action: commands::nft::NftCmd,
},
/// Genesis operations (dev only) /// Genesis operations (dev only)
Genesis { Genesis {
#[command(subcommand)] #[command(subcommand)]
@ -50,6 +55,7 @@ async fn main() -> Result<()> {
Command::Wallet { action } => commands::wallet::run(action).await?, Command::Wallet { action } => commands::wallet::run(action).await?,
Command::Node { action } => commands::node::run(action, &rpc).await?, Command::Node { action } => commands::node::run(action, &rpc).await?,
Command::Story { action } => commands::story::run(action, &rpc).await?, Command::Story { action } => commands::story::run(action, &rpc).await?,
Command::Nft { action } => commands::nft::run(action, &rpc).await?,
Command::Genesis{ action } => commands::genesis::run(action, &rpc).await?, Command::Genesis{ action } => commands::genesis::run(action, &rpc).await?,
} }