diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index 085f322..5ede1dc 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -30,7 +30,7 @@ use spaces_client::{ }, rpc::{ BidParams, ExecuteParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest, - RpcWalletTxBuilder, SendCoinsParams, TransferSpacesParams, + RpcWalletTxBuilder, SendCoinsParams, SetPtrParams, TransferSpacesParams, }, serialize_base64, wallets::{AddressKind, WalletResponse}, @@ -419,6 +419,20 @@ enum Commands { #[arg(long, short)] fee_rate: Option, }, + /// Set data for a PTR record + #[command(name = "setptr")] + SetPtr { + /// The sha256 hash of the spk or the spk itself prefixed with hex: + spk: String, + /// Hex encoded data + data: String, + /// Hex encoded private key (for testing purposes) + #[arg(long)] + prv: Option, + /// Fee rate to use in sat/vB + #[arg(long, short)] + fee_rate: Option, + }, /// List last transactions #[command(name = "listtransactions")] ListTransactions { @@ -482,6 +496,24 @@ struct Base64Bytes( Vec, ); +#[derive(Serialize, Deserialize)] +struct HexCommitment { + pub state_root: String, + pub prev_root: Option, + pub history_hash: String, + pub block_height: u32, +} + +#[derive(Serialize, Deserialize)] +struct HexFullPtrOut { + pub txid: String, + pub n: usize, + pub value: u64, // Amount in satoshis + pub script_pubkey: String, + pub genesis_spk: Option, + pub data: Option, +} + impl SpaceCli { async fn configure() -> anyhow::Result<(Self, Args)> { let mut args = Args::parse(); @@ -868,6 +900,39 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client ) .await?; } + Commands::SetPtr { spk, data, prv, fee_rate } => { + let sptr = Sptr::from_str(&spk) + .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; + + let data = match hex::decode(&data) { + Ok(data) => data, + Err(e) => { + return Err(ClientError::Custom(format!( + "Could not hex decode data: {}", + e + ))) + } + }; + + // Validate private key if provided + if let Some(ref prv_hex) = prv { + if hex::decode(prv_hex).is_err() { + return Err(ClientError::Custom("Invalid hex encoded private key".to_string())); + } + } + + cli.send_request( + Some(RpcWalletRequest::SetPtr(SetPtrParams { + spk: sptr.to_string(), + data: hex::encode(&data), + prv: prv.clone(), + })), + None, + fee_rate, + false, + ) + .await?; + } Commands::ListUnspent => { let utxos = cli.client.wallet_list_unspent(&cli.wallet).await?; print_list_unspent(utxos, cli.format); @@ -1095,12 +1160,25 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client let sptr = Sptr::from_str(&spk) .map_err(|e| ClientError::Custom(format!("input error: {}", e.to_string())))?; - let ptr = cli + let full_ptr_out = cli .client .get_ptr(sptr) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - println!("{}", serde_json::to_string(&ptr).expect("result")); + + if let Some(full_ptr_out) = full_ptr_out { + let hex_full_ptr_out = HexFullPtrOut { + txid: full_ptr_out.txid.to_string(), + n: full_ptr_out.ptrout.n, + value: full_ptr_out.ptrout.value.to_sat(), + script_pubkey: hex::encode(full_ptr_out.ptrout.script_pubkey.as_bytes()), + genesis_spk: full_ptr_out.ptrout.sptr.as_ref().map(|ptr| hex::encode(&ptr.genesis_spk)), + data: full_ptr_out.ptrout.sptr.as_ref().and_then(|ptr| ptr.data.as_ref().map(|data| hex::encode(data))), + }; + println!("{}", serde_json::to_string_pretty(&hex_full_ptr_out).expect("result")); + } else { + println!("null"); + } } Commands::GetPtrOut { outpoint } => { @@ -1221,7 +1299,18 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client .get_commitment(space, root) .await .map_err(|e| ClientError::Custom(e.to_string()))?; - println!("{}", serde_json::to_string(& c).expect("result")); + + if let Some(commitment) = c { + let hex_commitment = HexCommitment { + state_root: hex::encode(commitment.state_root), + prev_root: commitment.prev_root.map(|root| hex::encode(root)), + history_hash: hex::encode(commitment.history_hash), + block_height: commitment.block_height, + }; + println!("{}", serde_json::to_string_pretty(&hex_commitment).expect("result")); + } else { + println!("null"); + } } } diff --git a/client/src/rpc.rs b/client/src/rpc.rs index 3fac1bf..2dbe0a3 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -428,6 +428,8 @@ pub enum RpcWalletRequest { CreatePtr(CreatePtrParams), #[serde(rename = "commit")] Commit(CommitParams), + #[serde(rename = "setptr")] + SetPtr(SetPtrParams), #[serde(rename = "send")] SendCoins(SendCoinsParams), } @@ -457,6 +459,13 @@ pub struct CommitParams { pub root: sha256::Hash, } +#[derive(Clone, Serialize, Deserialize)] +pub struct SetPtrParams { + pub spk: String, + pub data: String, + pub prv: Option, +} + #[derive(Clone, Serialize, Deserialize)] pub struct SendCoinsParams { pub amount: Amount, diff --git a/client/src/wallets.rs b/client/src/wallets.rs index 9bc77c4..811abad 100644 --- a/client/src/wallets.rs +++ b/client/src/wallets.rs @@ -1306,6 +1306,49 @@ impl RpcWallet { root: *params.root.as_ref() }) } + RpcWalletRequest::SetPtr(params) => { + let sptr = Sptr::from_str(¶ms.spk) + .map_err(|e| anyhow!("setptr: invalid sptr: {}", e))?; + + let _data = hex::decode(¶ms.data) + .map_err(|e| anyhow!("setptr: invalid hex data: {}", e))?; + + // Verify the PTR exists + let ptr_info = chain.get_ptr_info(&sptr)?; + if ptr_info.is_none() { + return Err(anyhow!("setptr: PTR not found")); + } + + let ptr_info = ptr_info.unwrap(); + + // If private key is provided, validate it matches the script_pubkey + if let Some(ref prv_hex) = params.prv { + let _prv_bytes = hex::decode(prv_hex) + .map_err(|e| anyhow!("setptr: invalid private key hex: {}", e))?; + + // For testing purposes, we'll assume the private key is valid + // In a real implementation, you'd verify the private key corresponds to the script_pubkey + info!("setptr: Using provided private key for signing (testing mode)"); + + // TODO: Implement proper private key validation and transaction signing + // This would involve: + // 1. Converting the private key to a keypair + // 2. Verifying it corresponds to the script_pubkey + // 3. Using it to sign the transaction + + return Err(anyhow!("setptr: Private key signing not yet implemented - requires keypair validation and transaction signing")); + } else { + // No private key provided, check wallet ownership + if !wallet.is_mine(ptr_info.ptrout.script_pubkey.clone()) { + return Err(anyhow!("setptr: you don't control this PTR and no private key provided")); + } + } + + // For now, we'll use the Execute approach similar to setrawfallback + // This creates a transaction that updates the PTR data + // TODO: Implement proper PTR data update mechanism + return Err(anyhow!("setptr: PTR data update not yet implemented - this requires PTR script creation")); + } } }