From 7f7b84a485a0418cd39ef2119cf51c049c43f720 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:50:09 -0300 Subject: [PATCH 1/4] fix(rpc)!: correct getrawtransaction method name and standardize verbosity to numeric type The RPC client was incorrectly calling 'gettransaction' (which doesn't exist on the server) instead of 'getrawtransaction'. Additionally, the verbosity parameter handling was inconsistent. While the method signature correctly accepted Option, it needed better standardization to match Bitcoin Core's RPC specification. --- bin/floresta-cli/src/main.rs | 8 +++--- crates/floresta-node/src/json_rpc/server.rs | 28 ++++++++++----------- crates/floresta-rpc/src/rpc.rs | 20 ++++++++------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 498bcf0e8..ab409701d 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -68,8 +68,8 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { Methods::GetTxOutProof { txids, blockhash } => { serde_json::to_string_pretty(&client.get_txout_proof(txids, blockhash))? } - Methods::GetTransaction { txid, .. } => { - serde_json::to_string_pretty(&client.get_transaction(txid, Some(true))?)? + Methods::GetRawTransaction { txid, verbose } => { + serde_json::to_string_pretty(&client.get_raw_transaction(txid, verbose)?)? } Methods::RescanBlockchain { start_block, @@ -213,8 +213,8 @@ pub enum Methods { }, /// Returns the transaction, assuming it is cached by our watch only wallet - #[command(name = "gettransaction")] - GetTransaction { txid: Txid, verbose: Option }, + #[command(name = "getrawtransaction")] + GetRawTransaction { txid: Txid, verbose: Option }, #[doc = include_str!("../../../doc/rpc/rescanblockchain.md")] #[command( diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 2fd8ed4c6..853853dfe 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -86,19 +86,19 @@ pub struct RpcImpl { type Result = std::result::Result; impl RpcImpl { - fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { - if verbosity == Some(true) { - let tx = self - .wallet - .get_transaction(&tx_id) - .ok_or(JsonRpcError::TxNotFound); - return tx.map(|tx| serde_json::to_value(self.make_raw_transaction(tx)).unwrap()); - } - - self.wallet + fn get_raw_transaction(&self, tx_id: Txid, verbosity: u8) -> Result { + let tx = self + .wallet .get_transaction(&tx_id) - .and_then(|tx| serde_json::to_value(self.make_raw_transaction(tx)).ok()) - .ok_or(JsonRpcError::TxNotFound) + .ok_or(JsonRpcError::TxNotFound)?; + + match verbosity { + 0 => serde_json::to_value(serialize_hex(&tx.tx)) + .map_err(|e| JsonRpcError::Decode(e.to_string())), + 1 => serde_json::to_value(self.make_raw_transaction(tx)) + .map_err(|e| JsonRpcError::Decode(e.to_string())), + _ => Err(JsonRpcError::InvalidVerbosityLevel), + } } fn load_descriptor(&self, descriptor: String) -> Result { @@ -290,10 +290,10 @@ async fn handle_json_rpc_request( "getrawtransaction" => { let txid = get_hash(¶ms, 0, "txid")?; - let verbosity = get_optional_field(¶ms, 1, "verbosity", get_bool)?; + let verbosity = get_optional_field(¶ms, 1, "verbosity", get_numeric)?.unwrap_or(0); state - .get_transaction(txid, verbosity) + .get_raw_transaction(txid, verbosity) .map(|v| serde_json::to_value(v).unwrap()) } diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 64d18b149..19ff9ebc7 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -51,9 +51,9 @@ pub trait FlorestaRPC { /// Gets a transaction from the blockchain /// /// This method returns a transaction that's cached in our wallet. If the verbosity flag is - /// set to false, the transaction is returned as a hexadecimal string. If the verbosity - /// flag is set to true, the transaction is returned as a json object. - fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result; + /// set to 0, the transaction is returned as a hexadecimal string. If the verbosity + /// flag is set to 1, the transaction is returned as a json object. + fn get_raw_transaction(&self, tx_id: Txid, verbosity: Option) -> Result; /// Returns the proof that one or more transactions were included in a block /// /// This method returns the Merkle proof, showing that a transaction was included in a block. @@ -304,12 +304,14 @@ impl FlorestaRPC for T { self.call("getblockhash", &[Value::Number(Number::from(height))]) } - fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { - let verbosity = verbosity.unwrap_or(false); - self.call( - "gettransaction", - &[Value::String(tx_id.to_string()), Value::Bool(verbosity)], - ) + fn get_raw_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { + let mut params = vec![Value::String(tx_id.to_string())]; + + if let Some(verbosity) = verbosity { + params.push(Value::Number(Number::from(verbosity))); + } + + self.call("getrawtransaction", ¶ms) } fn load_descriptor(&self, descriptor: String) -> Result { From 688879560f50f999b4e916830c3624b91b8dcb0c Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:03:19 -0300 Subject: [PATCH 2/4] chore(rpc)!: Adjust getrawtransaction to match Bitcoin Core response format This commit aligns the getrawtransaction RPC response with Bitcoin Core's format and behavior. The following changes were made: RawTx struct: - Made blockhash, confirmations, blocktime, and time optional fields They are now only serialized when present, avoiding null values - Fixed txid encoding that was previously inverted TxOut struct: - Changed value from u64 (sats) to f64 (BTC) for proper denomination - Made address field optional, only serialized when script can be converted to a valid address TxIn struct: - Added coinbase field that only appears in coinbase transactions, containing the hex-encoded script from scriptSig - Coinbase inputs have: coinbase, sequence, witness (optional) - Regular inputs have: txid, vout, script_sig, sequence, witness (optional) Script handling: - Improved ASM (assembly) format conversion to strictly follow Bitcoin Core rules and formatting standards TxOut types: - Updated to include all script types that Bitcoin Core supports Note: The `desc` field was not implemented due to complex matching rules. This will be added in a future PR. --- .../floresta-node/src/json_rpc/blockchain.rs | 101 +----- crates/floresta-node/src/json_rpc/res.rs | 130 +++++++- crates/floresta-node/src/json_rpc/server.rs | 305 ++++++++++++++---- crates/floresta-rpc/src/rpc.rs | 12 +- crates/floresta-rpc/src/rpc_types.rs | 92 ++++-- 5 files changed, 443 insertions(+), 197 deletions(-) diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index 8254e2f90..1cf676729 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -33,6 +33,7 @@ use super::server::RpcChain; use super::server::RpcImpl; use crate::json_rpc::res::GetBlockRes; use crate::json_rpc::res::RescanConfidence; +use crate::json_rpc::server::to_core_asm_string; impl RpcImpl { async fn get_block_inner(&self, hash: BlockHash) -> Result { @@ -375,7 +376,7 @@ impl RpcImpl { /// Returns a label about the scriptPubKey type /// (pubkey, pubkeyhash, multisig, nulldata, scripthash, witness_v0_keyhash, witness_v0_scripthash, witness_v1_taproot, anchor, nonstandard) - fn get_script_type_label(script: &Script) -> &'static str { + pub(super) fn get_script_type_label(script: &Script) -> &'static str { if script.is_p2pk() { return "pubkey"; } @@ -415,20 +416,15 @@ impl RpcImpl { "nonstandard" } - fn get_script_type_descriptor(script: &Script, address: &Option
) -> String { - let get_addr_str = || { - address - .as_ref() - .expect("address should be Some") - .to_string() - }; - - if script.is_p2pk() { - let addr = get_addr_str(); - return format!("pk({addr}"); - } - + /// TODO: This function is not compliant with Bitcoin Core. + /// See: + pub(super) fn get_script_type_descriptor(script: &Script, address: &Option
) -> String { + // Try script from the address if let Some(addr) = address { + if script.is_p2pk() { + return format!("pk({addr}"); + } + return format!("addr({addr})"); } @@ -437,85 +433,10 @@ impl RpcImpl { return format!("raw({hex})"); } - if Self::is_anchor_type(script) { - let addr = get_addr_str(); - return format!("addr({addr})"); - } - let hex = script.to_hex_string(); format!("raw({hex})") } - /// Parses the serialized opcodes in a [ScriptBuf] as numbers and it's hashes. - /// This differs from `ScriptBuf::to_asm_string` in that, `rust-bitcoin` will - /// show the the human representation of the opcode. It does not omit the number representations of - /// `OP_PUSHDATA_` and `OP_PUSHBYTE`. This method do the opposite: it not show the human - /// representation and omit the last opcodes, so it can be compliant with bitcoin-core. - /// For reference see - fn to_core_asm_string(script: &ScriptBuf) -> Result { - let mut asm = vec![]; - let bytes = script.as_bytes(); - let mut i = 0usize; - - // little reused helper to hex string - let to_hex_string = |r: &[u8]| r.iter().map(|b| format!("{b:02x}")).collect::(); - - while i < bytes.len() { - let byte = bytes[i]; - i += 1; - - match byte { - // OP_0 - 0x00 => asm.push(format!("{}", 0)), - // OP_PUSHDATA_: The next N bytes is data to be pushed onto the stack - 0x01..=0x4b => { - let pushed_bytes = byte as usize; - let hex = to_hex_string(&bytes[i..i + pushed_bytes]); - asm.push(hex); - i += pushed_bytes; - } - // OP_PUSHBYTE1: the next byte contains the number of bytes to be pushed onto the stack. - 0x4c => { - let pushed_bytes = bytes[i] as usize; - i += 1; - let hex = to_hex_string(&bytes[i..i + pushed_bytes]); - asm.push(hex); - i += pushed_bytes; - } - // OP_PUSHBYTE2: the next two bytes contain the number of bytes to be pushed onto the stack in little endian order. - 0x4d => { - let pushed_bytes = u16::from_le_bytes([bytes[i], bytes[i + 1]]) as usize; - i += 2; - let hex = to_hex_string(&bytes[i..i + pushed_bytes]); - asm.push(hex); - i += pushed_bytes; - } - // OP_PUSHBYTE4: the next four bytes contain the number of bytes to be pushed onto the stack in little endian order. - 0x4e => { - let pushed_bytes = - u32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]) - as usize; - i += 4; - let hex = to_hex_string(&bytes[i..i + pushed_bytes]); - asm.push(hex); - i += pushed_bytes; - } - // OP_1 to OP_16 - 0x51..=0x60 => { - // 0x50 is OP_RESERVED - let reserved = 0x50; - asm.push(format!("{}", byte - reserved)); - } - // Any other opcode that should be pushed - another_one => { - asm.push(format!("{another_one:02x}")); - } - } - } - - Ok(asm.join(" ")) - } - /// gettxout: returns details about an unspent transaction output. pub(super) fn get_tx_out( &self, @@ -547,7 +468,7 @@ impl RpcImpl { Err(_) => None, }; - let asm = Self::to_core_asm_string(&txout.script_pubkey)?; + let asm = to_core_asm_string(&txout.script_pubkey, false); let script_pubkey = ScriptPubkey { asm, hex: txout.script_pubkey.to_hex_string(), diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index 5710cc1d2..80b14f8ae 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -76,54 +76,152 @@ impl RescanConfidence { } } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum GetRawTransactionRes { + Zero(String), + + One(Box), +} + +#[derive(Debug, Deserialize, Serialize)] +/// The information returned by a get_raw_tx pub struct RawTxJson { + /// Whether this tx is in our best known chain pub in_active_chain: bool, + + /// The hex-encoded tx pub hex: String, + + /// The sha256d of the serialized transaction without witness pub txid: String, + + /// The sha256d of the serialized transaction including witness pub hash: String, + + /// The size this transaction occupies on disk pub size: u32, + + /// The virtual size of this transaction, as define by the segwit soft-fork pub vsize: u32, + + /// The weight of this transaction, as defined by the segwit soft-fork pub weight: u32, + + /// This transaction's version. The current bigger version is 2 pub version: u32, + + /// This transaction's locktime pub locktime: u32, + + /// A list of inputs being spent by this transaction + /// + /// See [TxInJson] for more information about the contents of this pub vin: Vec, + + /// A list of outputs being created by this tx + /// + /// See [TxOutJson] for more information pub vout: Vec, - pub blockhash: String, - pub confirmations: u32, - pub blocktime: u32, - pub time: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The hash of the block that included this tx, if any + pub blockhash: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// How many blocks have been mined after this transaction's confirmation + /// including the block that confirms it. A zero value means this tx is unconfirmed + pub confirmations: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The timestamp for the block confirming this tx, if confirmed + pub blocktime: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Same as blocktime + pub time: Option, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +/// A transaction output returned by some RPCs like gettransaction and getblock pub struct TxOutJson { - pub value: u64, + /// The amount in btc locked in this UTXO + pub value: f64, + + /// This utxo's index inside the transaction pub n: u32, + + #[serde(rename = "scriptPubKey")] + /// The locking script of this utxo pub script_pub_key: ScriptPubKeyJson, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +/// The locking script inside a txout pub struct ScriptPubKeyJson { + /// A ASM representation for this script + /// + /// Assembly is a high-level representation of a lower level code. Instructions + /// are turned into OP_XXXXX and data is hex-encoded. + /// E.g: OP_DUP OP_HASH160 <0000000000000000000000000000000000000000> OP_EQUALVERIFY OP_CHECKSIG pub asm: String, + + /// The hex-encoded raw script pub hex: String, - pub req_sigs: u32, + #[serde(rename = "type")] + /// The type of this spk. E.g: PKH, SH, WSH, WPKH, TR, non-standard... pub type_: String, - pub address: String, + + #[serde(skip_serializing_if = "Option::is_none")] + /// Encode this script using one of the standard address types, if possible + pub address: Option, + + #[serde(rename = "desc")] + /// Inferred descriptor for the output + pub descriptor: String, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Default)] +/// A transaction input returned by some rpcs, like gettransaction and getblock pub struct TxInJson { - pub txid: String, - pub vout: u32, - pub script_sig: ScriptSigJson, + #[serde(skip_serializing_if = "Option::is_none")] + /// The coinbase field is only set for coinbase transactions, and contains the hex-encoded coinbase script + pub coinbase: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The txid that created this UTXO. Not set for coinbase transactions + pub txid: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The index of this UTXO inside the tx that created it. Not set for coinbase transactions + pub vout: Option, + + #[serde(rename = "scriptSig", skip_serializing_if = "Option::is_none")] + /// Unlocking script that should solve the challenge and prove ownership over + /// that UTXO. Not set for coinbase transactions + pub script_sig: Option, + + /// The nSequence field, used in relative and absolute lock-times pub sequence: u32, - pub witness: Vec, + + #[serde(rename = "txinwitness", skip_serializing_if = "Option::is_none")] + /// A vector of witness elements for this input + pub witness: Option>, } -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +/// A representation for the transaction ScriptSig, returned by some rpcs +/// like gettransaction and getblock pub struct ScriptSigJson { + /// A ASM representation for this scriptSig + /// + /// Assembly is a high-level representation of a lower level code. Instructions + /// are turned into OP_XXXXX and data is hex-encoded. + /// E.g: OP_PUSHBYTES32 <000000000000000000000000000000000000000000000000000000000000000000> pub asm: String, + + /// The hex-encoded script sig pub hex: String, } diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 853853dfe..f26aeadc7 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -16,11 +16,11 @@ use axum::Json; use axum::Router; use bitcoin::consensus::deserialize; use bitcoin::consensus::encode::serialize_hex; +use bitcoin::ecdsa::Signature as EcdsaSignature; use bitcoin::hashes::hex::FromHex; -use bitcoin::hashes::Hash; use bitcoin::hex::DisplayHex; +use bitcoin::taproot::Signature as TaprootSignature; use bitcoin::Address; -use bitcoin::BlockHash; use bitcoin::Network; use bitcoin::ScriptBuf; use bitcoin::Transaction; @@ -42,6 +42,7 @@ use tracing::debug; use tracing::error; use tracing::info; +use super::res::GetRawTransactionRes; use super::res::JsonRpcError; use super::res::RawTxJson; use super::res::RpcError; @@ -86,17 +87,21 @@ pub struct RpcImpl { type Result = std::result::Result; impl RpcImpl { - fn get_raw_transaction(&self, tx_id: Txid, verbosity: u8) -> Result { + fn get_raw_transaction(&self, tx_id: Txid, verbosity: u8) -> Result { + if verbosity > 1 { + return Err(JsonRpcError::InvalidVerbosityLevel); + } + let tx = self .wallet .get_transaction(&tx_id) .ok_or(JsonRpcError::TxNotFound)?; match verbosity { - 0 => serde_json::to_value(serialize_hex(&tx.tx)) - .map_err(|e| JsonRpcError::Decode(e.to_string())), - 1 => serde_json::to_value(self.make_raw_transaction(tx)) - .map_err(|e| JsonRpcError::Decode(e.to_string())), + 0 => Ok(GetRawTransactionRes::Zero(serialize_hex(&tx.tx))), + 1 => Ok(GetRawTransactionRes::One(Box::new( + self.make_raw_transaction(tx), + ))), _ => Err(JsonRpcError::InvalidVerbosityLevel), } } @@ -606,57 +611,58 @@ impl RpcImpl { Ok(()) } - fn make_vin(&self, input: TxIn) -> TxInJson { - let txid = serialize_hex(&input.previous_output.txid); - let vout = input.previous_output.vout; + fn make_vin(&self, input: TxIn, is_coinbase: bool) -> TxInJson { let sequence = input.sequence.0; - TxInJson { - txid, - vout, - script_sig: ScriptSigJson { - asm: input.script_sig.to_asm_string(), - hex: input.script_sig.to_hex_string(), - }, - witness: input + let witness = (!input.witness.is_empty()).then_some( + input .witness .iter() - .map(|w| w.to_hex_string(bitcoin::hex::Case::Upper)) + .map(|w| w.to_hex_string(bitcoin::hex::Case::Lower)) .collect(), - sequence, + ); + + if is_coinbase { + return TxInJson { + coinbase: Some(input.script_sig.to_hex_string()), + sequence, + witness, + ..Default::default() + }; } - } - fn get_script_type(script: ScriptBuf) -> Option<&'static str> { - if script.is_p2pkh() { - return Some("p2pkh"); - } - if script.is_p2sh() { - return Some("p2sh"); - } - if script.is_p2wpkh() { - return Some("v0_p2wpkh"); - } - if script.is_p2wsh() { - return Some("v0_p2wsh"); + let txid = Some(input.previous_output.txid.to_string()); + let vout = Some(input.previous_output.vout); + let script_sig = ScriptSigJson { + asm: to_core_asm_string(&input.script_sig, true), + hex: input.script_sig.to_hex_string(), + }; + + TxInJson { + coinbase: None, + txid, + vout, + script_sig: Some(script_sig), + witness, + sequence, } - None } fn make_vout(&self, output: TxOut, n: u32) -> TxOutJson { let value = output.value; TxOutJson { - value: value.to_sat(), + value: value.to_btc(), n, script_pub_key: ScriptPubKeyJson { - asm: output.script_pubkey.to_asm_string(), + asm: to_core_asm_string(&output.script_pubkey, false), hex: output.script_pubkey.to_hex_string(), - req_sigs: 0, // This field is deprecated address: Address::from_script(&output.script_pubkey, self.network) .map(|a| a.to_string()) - .unwrap(), - type_: Self::get_script_type(output.script_pubkey) - .unwrap_or("nonstandard") - .to_string(), + .ok(), + type_: Self::get_script_type_label(&output.script_pubkey).to_string(), + descriptor: Self::get_script_type_descriptor( + &output.script_pubkey, + &Address::from_script(&output.script_pubkey, self.network).ok(), + ), }, } } @@ -665,23 +671,35 @@ impl RpcImpl { let raw_tx = tx.tx; let in_active_chain = tx.height != 0; let hex = serialize_hex(&raw_tx); - let txid = serialize_hex(&raw_tx.compute_txid()); - let block_hash = self - .chain - .get_block_hash(tx.height) - .unwrap_or(BlockHash::all_zeros()); - let tip = self.chain.get_height().unwrap(); - let confirmations = if in_active_chain { - tip - tx.height + 1 - } else { - 0 - }; + let txid = raw_tx.compute_txid().to_string(); + + let mut blockhash = None; + let mut blocktime = None; + let mut time = None; + let mut confirmations = Some(0); + if in_active_chain { + confirmations = self.chain.get_height().ok().and_then(|tip| { + if tip >= tx.height { + Some(tip - tx.height + 1) + } else { + None + } + }); + + if let Ok(hash) = self.chain.get_block_hash(tx.height) { + if let Ok(header) = self.chain.get_block_header(&hash) { + blockhash = Some(header.block_hash().to_string()); + blocktime = Some(header.time); + time = Some(header.time); + } + } + } RawTxJson { in_active_chain, hex, txid, - hash: serialize_hex(&raw_tx.compute_wtxid()), + hash: raw_tx.compute_wtxid().to_string(), size: raw_tx.total_size() as u32, vsize: raw_tx.vsize() as u32, weight: raw_tx.weight().to_wu() as u32, @@ -690,26 +708,18 @@ impl RpcImpl { vin: raw_tx .input .iter() - .map(|input| self.make_vin(input.clone())) + .map(|input| self.make_vin(input.clone(), raw_tx.is_coinbase())) .collect(), vout: raw_tx .output .into_iter() .enumerate() .map(|(i, output)| self.make_vout(output, i as u32)) - .collect(), - blockhash: serialize_hex(&block_hash), + .collect::>(), + blockhash, confirmations, - blocktime: self - .chain - .get_block_header(&block_hash) - .map(|h| h.time) - .unwrap_or(0), - time: self - .chain - .get_block_header(&block_hash) - .map(|h| h.time) - .unwrap_or(0), + blocktime, + time, } } @@ -782,3 +792,166 @@ impl RpcImpl { .expect("failed to start rpc server"); } } + +/// Converts a script to ASM (assembly) format, displaying the script's operations +/// in a format similar to Bitcoin Core. +/// +/// This function performs the following transformations: +/// 1. Removes OP_PUSHBYTES and OP_PUSHDATA opcodes (these are unnecessary in ASM output) +/// 2. Converts leading OP_0 to "0" and OP_PUSHNUM_1 to "1" (these represent witness versions) +/// 3. If `attempt_sighash_decode` is true, attempts to decode hexadecimal data as signatures +/// and appends their sighash type (useful for analyzing scripts in scriptSig) +/// +/// # Arguments +/// * `script` - The script buffer to convert +/// * `attempt_sighash_decode` - If true, tries to parse data elements as signatures and format them +pub(super) fn to_core_asm_string(script: &ScriptBuf, attempt_sighash_decode: bool) -> String { + let mut script_asm = script.to_asm_string(); + if !script_asm.contains(' ') { + return script_asm; + } + + // Remove OP_PUSHBYTES_X opcodes (these are only metadata for script serialization) + for i in 0..=75 { + script_asm = script_asm.replace(&format!("OP_PUSHBYTES_{} ", i), ""); + } + + // Remove OP_PUSHDATA1/2/4 opcodes (these are only metadata for script serialization) + for i in 1..=4 { + script_asm = script_asm.replace(&format!("OP_PUSHDATA{} ", i), ""); + } + + let mut array_script_asm: Vec = script_asm.split(' ').map(String::from).collect(); + + // Convert leading OP_0 to "0" - represents witness version 0 + if array_script_asm[0] == "OP_0" { + array_script_asm[0] = "0".to_string(); + } + + // Convert leading OP_PUSHNUM_1 to "1" - represents witness version 1 (Taproot) + if array_script_asm[0] == "OP_PUSHNUM_1" { + array_script_asm[0] = "1".to_string(); + } + + // If enabled, attempt to decode data elements as signatures and format them + // This is particularly useful for scriptSig analysis, where signatures are wrapped with their sighash type + if attempt_sighash_decode { + for word in array_script_asm.iter_mut() { + // Skip OP codes and small words that are unlikely to be signatures + if word.contains("OP") || word.len() <= 8 { + continue; + } + + if let Some(decoded) = + try_parse_and_format_signature(&Vec::from_hex(word).unwrap_or_default()) + { + *word = decoded; + } + } + } + + array_script_asm.join(" ") +} + +/// Attempts to decode a byte slice as a valid signature (ECDSA or Taproot). +/// If the bytes represent a valid signature, returns the signature with the sighash type appended. +fn try_parse_and_format_signature(signature_bytes: &[u8]) -> Option { + macro_rules! try_decode_signature { + ($sig_type:ty) => { + if let Ok(signature) = <$sig_type>::from_slice(signature_bytes) { + // Extract the sighash type and remove the "SIGHASH_" prefix + // The rust-bitcoin library prefixes "SIGHASH_" to the type name, but Bitcoin Core + // does not include this prefix in the output + let label = signature.sighash_type.to_string().replace("SIGHASH_", ""); + return Some(format!("{}[{}]", signature.signature, label)); + } + }; + } + + // Attempt to parse as ECDSA signature + try_decode_signature!(EcdsaSignature); + + // Attempt to parse as Taproot signature + try_decode_signature!(TaprootSignature); + + // If the bytes don't match any known signature format, return None + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_converter_script_into_asm_not_attempt_sighash_decode() { + let test_cases = [ + // P2WPKH + ( + "0014aabc2cd363103811113b040c541afe3759489c96", + "0 aabc2cd363103811113b040c541afe3759489c96", + ), + // BECH32 + ( + "0014251619c32f6500664e71a6d0393ec4b5f6da549c", + "0 251619c32f6500664e71a6d0393ec4b5f6da549c", + ), + ( + "0014aa138477d24cb7b7a84160ef55af14b7bfb98143", + "0 aa138477d24cb7b7a84160ef55af14b7bfb98143", + ), + // P2PKH + ( + "76a914e7d68c17e6275b2e5c1da053ef648c676c38962488ac", + "OP_DUP OP_HASH160 e7d68c17e6275b2e5c1da053ef648c676c389624 OP_EQUALVERIFY OP_CHECKSIG", + ), + ( + "76a9144eb2df72d9befff81b6dd985044d2d1b3ed4de4188ac", + "OP_DUP OP_HASH160 4eb2df72d9befff81b6dd985044d2d1b3ed4de41 OP_EQUALVERIFY OP_CHECKSIG", + ), + // P2SH + ( + "a914fae946075d1f629d35ed4067eca928c1632f4fef87", + "OP_HASH160 fae946075d1f629d35ed4067eca928c1632f4fef OP_EQUAL", + ), + // P2TR (Taproot) + ( + "51209ec7be23a1ec17cd9c4b621d899eec02bacde1d754ab080f9e1ac8445820014e", + "1 9ec7be23a1ec17cd9c4b621d899eec02bacde1d754ab080f9e1ac8445820014e", + ), + ]; + + for (script_hex, expected_asm) in test_cases.iter() { + let script = ScriptBuf::from_hex(script_hex).unwrap(); + let asm = to_core_asm_string(&script, false); + + assert_eq!(asm, *expected_asm); + } + } + + #[test] + fn test_converter_script_into_asm_attempt_sighash_decode() { + let test_cases = [ + // scriptSig with ECDSA signature and pubkey + ( + "47304402205a9b7c4432f9d895cbf4ac78519ae4e9776d47776078521b93e06beda560dd9a02202b1afbda3c917c2698b38f78203e03d2743069939e3ce2b6a3a153e148502f19012103fde976887234670c672e33a4707356997df737f3e7ac6de809164b5a606b8bad", + "304402205a9b7c4432f9d895cbf4ac78519ae4e9776d47776078521b93e06beda560dd9a02202b1afbda3c917c2698b38f78203e03d2743069939e3ce2b6a3a153e148502f19[ALL] 03fde976887234670c672e33a4707356997df737f3e7ac6de809164b5a606b8bad", + ), + ( + "47304402204ab6753b249205b01d938826189cefaa4176e32ca5aa64fc6fd51891fb78fed2022065b7ba08d8739884ba232f5f7bf6efbb36b2cf98917630c64343cad2fe9db3a2012102ecf8dfb67cae8fe66d700cb13c458e5cc59be2a1c5f3ca3c5a54745259cbe45c", + "304402204ab6753b249205b01d938826189cefaa4176e32ca5aa64fc6fd51891fb78fed2022065b7ba08d8739884ba232f5f7bf6efbb36b2cf98917630c64343cad2fe9db3a2[ALL] 02ecf8dfb67cae8fe66d700cb13c458e5cc59be2a1c5f3ca3c5a54745259cbe45c", + ), + // P2WPKH + ( + "160014bb180b7bf33f066f7b557c09a0bd3b6accc84fcf", + "0014bb180b7bf33f066f7b557c09a0bd3b6accc84fcf", + ), + ]; + + for (script_hex, expected_asm) in test_cases.iter() { + let script = ScriptBuf::from_hex(script_hex).unwrap(); + let asm = to_core_asm_string(&script, true); + + assert_eq!(asm, *expected_asm); + } + } +} diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 19ff9ebc7..bd5034d23 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -53,7 +53,11 @@ pub trait FlorestaRPC { /// This method returns a transaction that's cached in our wallet. If the verbosity flag is /// set to 0, the transaction is returned as a hexadecimal string. If the verbosity /// flag is set to 1, the transaction is returned as a json object. - fn get_raw_transaction(&self, tx_id: Txid, verbosity: Option) -> Result; + fn get_raw_transaction( + &self, + tx_id: Txid, + verbosity: Option, + ) -> Result; /// Returns the proof that one or more transactions were included in a block /// /// This method returns the Merkle proof, showing that a transaction was included in a block. @@ -304,7 +308,11 @@ impl FlorestaRPC for T { self.call("getblockhash", &[Value::Number(Number::from(height))]) } - fn get_raw_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { + fn get_raw_transaction( + &self, + tx_id: Txid, + verbosity: Option, + ) -> Result { let mut params = vec![Value::String(tx_id.to_string())]; if let Some(verbosity) = verbosity { diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 02377eb1b..2a4389be7 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -58,59 +58,88 @@ pub struct GetBlockchainInfoRes { pub difficulty: u64, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum GetRawTransactionRes { + Zero(String), + + One(Box), +} + +#[derive(Debug, Deserialize, Serialize)] /// The information returned by a get_raw_tx -#[derive(Deserialize, Serialize)] pub struct RawTx { /// Whether this tx is in our best known chain pub in_active_chain: bool, + /// The hex-encoded tx pub hex: String, + /// The sha256d of the serialized transaction without witness pub txid: String, + /// The sha256d of the serialized transaction including witness pub hash: String, + /// The size this transaction occupies on disk pub size: u32, + /// The virtual size of this transaction, as define by the segwit soft-fork pub vsize: u32, + /// The weight of this transaction, as defined by the segwit soft-fork pub weight: u32, + /// This transaction's version. The current bigger version is 2 pub version: u32, + /// This transaction's locktime pub locktime: u32, + /// A list of inputs being spent by this transaction /// /// See [TxIn] for more information about the contents of this pub vin: Vec, + /// A list of outputs being created by this tx /// /// Se [TxOut] for more information pub vout: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] /// The hash of the block that included this tx, if any - pub blockhash: String, + pub blockhash: Option, + + #[serde(skip_serializing_if = "Option::is_none")] /// How many blocks have been mined after this transaction's confirmation /// including the block that confirms it. A zero value means this tx is unconfirmed - pub confirmations: u32, + pub confirmations: Option, + + #[serde(skip_serializing_if = "Option::is_none")] /// The timestamp for the block confirming this tx, if confirmed - pub blocktime: u32, + pub blocktime: Option, + + #[serde(skip_serializing_if = "Option::is_none")] /// Same as blocktime - pub time: u32, + pub time: Option, } +#[derive(Debug, Deserialize, Serialize)] /// A transaction output returned by some RPCs like gettransaction and getblock -#[derive(Deserialize, Serialize)] pub struct TxOut { - /// The amount in sats locked in this UTXO - pub value: u64, + /// The amount in btc locked in this UTXO + pub value: f64, + /// This utxo's index inside the transaction pub n: u32, + + #[serde(rename = "scriptPubKey")] /// The locking script of this utxo pub script_pub_key: ScriptPubKey, } +#[derive(Debug, Deserialize, Serialize)] /// The locking script inside a txout -#[derive(Deserialize, Serialize)] pub struct ScriptPubKey { /// A ASM representation for this script /// @@ -118,38 +147,54 @@ pub struct ScriptPubKey { /// are turned into OP_XXXXX and data is hex-encoded. /// E.g: OP_DUP OP_HASH160 <0000000000000000000000000000000000000000> OP_EQUALVERIFY OP_CHECKSIG pub asm: String, + /// The hex-encoded raw script pub hex: String, - /// How many signatures are required to spend this UTXO. - /// - /// This field is deprecated and is here for compatibility with Core - pub req_sigs: u32, + #[serde(rename = "type")] /// The type of this spk. E.g: PKH, SH, WSH, WPKH, TR, non-standard... pub type_: String, + + #[serde(skip_serializing_if = "Option::is_none")] /// Encode this script using one of the standard address types, if possible - pub address: String, + pub address: Option, + + #[serde(rename = "desc")] + /// Inferred descriptor for the output + pub descriptor: String, } +#[derive(Debug, Deserialize, Serialize, Default)] /// A transaction input returned by some rpcs, like gettransaction and getblock -#[derive(Deserialize, Serialize)] pub struct TxIn { - /// The txid that created this UTXO - pub txid: String, - /// The index of this UTXO inside the tx that created it - pub vout: u32, + #[serde(skip_serializing_if = "Option::is_none")] + /// The coinbase field is only set for coinbase transactions, and contains the hex-encoded coinbase script + pub coinbase: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The txid that created this UTXO. Not set for coinbase transactions + pub txid: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The index of this UTXO inside the tx that created it. Not set for coinbase transactions + pub vout: Option, + + #[serde(rename = "scriptSig", skip_serializing_if = "Option::is_none")] /// Unlocking script that should solve the challenge and prove ownership over - /// that UTXO - pub script_sig: ScriptSigJson, + /// that UTXO. Not set for coinbase transactions + pub script_sig: Option, + /// The nSequence field, used in relative and absolute lock-times pub sequence: u32, + + #[serde(rename = "txinwitness", skip_serializing_if = "Option::is_none")] /// A vector of witness elements for this input - pub witness: Vec, + pub witness: Option>, } +#[derive(Debug, Deserialize, Serialize)] /// A representation for the transaction ScriptSig, returned by some rpcs /// like gettransaction and getblock -#[derive(Deserialize, Serialize)] pub struct ScriptSigJson { /// A ASM representation for this scriptSig /// @@ -157,6 +202,7 @@ pub struct ScriptSigJson { /// are turned into OP_XXXXX and data is hex-encoded. /// E.g: OP_PUSHBYTES32 <000000000000000000000000000000000000000000000000000000000000000000> pub asm: String, + /// The hex-encoded script sig pub hex: String, } From 1af596bbe4a4af3e04323f7d525724ba2cab20a6 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:17:24 -0300 Subject: [PATCH 3/4] test(integration): add `getrawtransaction` integration test Add comprehensive integration test comparing Floresta's `getrawtransaction` RPC output with Bitcoin Core for verbosity levels 0 and 1, covering: - Regular transactions (`P2PKH`, `P2WPKH`, `P2SH`, `P2TR types`) - Coinbase transactions - Input/output field validation - Optional field handling New BaseRPC methods: - `get_raw_transaction()` New BitcoinRPC methods: - `create_wallet()` - `get_new_address()` - `generate_block_to_wallet()` - `send_to_address()` --- tests/floresta-cli/getrawtransaction.py | 245 ++++++++++++++++++++++++ tests/test_framework/rpc/base.py | 13 +- tests/test_framework/rpc/bitcoin.py | 25 +++ 3 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 tests/floresta-cli/getrawtransaction.py diff --git a/tests/floresta-cli/getrawtransaction.py b/tests/floresta-cli/getrawtransaction.py new file mode 100644 index 000000000..db758ed0a --- /dev/null +++ b/tests/floresta-cli/getrawtransaction.py @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: MIT OR Apache-2.0 + +""" +floresta_cli_getrawtransaction.py + +Integrate test for the CLI utility that interacts with a Floresta node +using the `getrawtransaction` RPC method. +""" + +import time +import os +import pytest +from test_framework.node import NodeType +from requests.exceptions import HTTPError + +ADDRESS_COINBASE = "bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y" +ADDRESS_LEGACY = "n2eoQNSGg7ZWjnbXzdnGDMHZShn3MjaEfR" +ADDRESS_P2PKH = "mnh5HKWqsYdRo8ChUc2rN9bDcMRj4HopFw" +ADDRESS_P2PWH = "2NG7vNNXVRMihLD14WBbxhMxEQ1qKBkepq1" +ADDRESS_BECH32 = "bcrt1q427ze5mrzqupzyfmqsx9gxh7xav538yk2j4cft" +ADDRESS_BECH32M = "bcrt1p929uxzkp0lnh3smkkcvdqerj7ejhhac6vsc2lr0gqc4l092w8yjq64dhfy" +ADDRESS_TAPROOT = "bcrt1pnmrmugapastum8ztvgwcn8hvq2avmcwh2j4ssru7rtyygkpqq98q4wyd6s" + +WALLET_CONFIG = "\n".join( + [ + "[wallet]", + ( + f'addresses = [ "{ADDRESS_COINBASE}", ' + f'"{ADDRESS_LEGACY}", "{ADDRESS_P2PKH}", ' + f'"{ADDRESS_P2PWH}", "{ADDRESS_BECH32}", ' + f'"{ADDRESS_BECH32M}", "{ADDRESS_TAPROOT}" ]' + ), + ] +) + +MINED_BLOCKS = 101 +COINBASE_BLOCKS = 6 + + +class TestGetRawTransaction: + """ + Test `getrawtransaction` RPC method of Floresta node by comparing + its response with Bitcoin Core's response for the same transaction. + """ + + log = None + node_manager = None + florestad = None + bitcoind = None + utreexod = None + + # pylint: disable=too-many-statements,too-many-locals + @pytest.mark.rpc + def test_get_raw_transaction( + self, setup_logging, node_manager, add_node_with_extra_args, utreexod_node + ): + """ + Run the test by sending transactions, mining blocks, and + comparing `getrawtransaction` responses between Floresta + and Bitcoin Core. + """ + self.log = setup_logging + self.node_manager = node_manager + + self.florestad = node_manager.add_node_default_args(variant=NodeType.FLORESTAD) + config_dir = os.path.join(self.florestad.daemon.data_dir, "config.toml") + with open(config_dir, "w", encoding="utf-8") as f: + f.write(WALLET_CONFIG) + self.florestad.set_extra_args([f"--config-file={config_dir}"]) + + node_manager.run_node(self.florestad) + + self.log.info("Test getrawtransaction with a non existing txid") + + with pytest.raises(HTTPError): + self.florestad.rpc.get_raw_transaction("nonexistingtxid") + + with pytest.raises(HTTPError): + self.florestad.rpc.get_raw_transaction("nonexistingtxid", verbose=0) + + with pytest.raises(HTTPError): + self.florestad.rpc.get_raw_transaction("nonexistingtxid", verbose=1) + + self.log.info( + "Test getrawtransaction with a non existing txid and " + "invalid verbose level" + ) + with pytest.raises(HTTPError): + self.florestad.rpc.get_raw_transaction("nonexistingtxid", verbose=2) + + self.log.info("Creating and funding transactions in Bitcoin Core") + self.bitcoind = add_node_with_extra_args( + variant=NodeType.BITCOIND, + extra_args=["-txindex", "-fallbackfee=0.00000001"], + ) + self.bitcoind.rpc.create_wallet("testwallet") + + self.bitcoind.rpc.generate_block_to_wallet(MINED_BLOCKS) + + txids = [] + value = 6.4242521 + for address in [ + ADDRESS_BECH32, + ADDRESS_LEGACY, + ADDRESS_BECH32M, + ADDRESS_P2PWH, + ADDRESS_P2PKH, + ADDRESS_TAPROOT, + ]: + txid = self.bitcoind.rpc.send_to_address(address, value) + self.log.info( + f"Sent transaction to {address} and value {value} " f"with txid: {txid}" + ) + txids.append(txid) + + txids.append(self.bitcoind.rpc.send_to_address(ADDRESS_BECH32, 5.5)) + self.log.info(f"Sent transaction with txid: {txid}") + + self.bitcoind.rpc.generate_block_to_address(COINBASE_BLOCKS, ADDRESS_COINBASE) + bestblockhash = self.bitcoind.rpc.get_bestblockhash() + block = self.bitcoind.rpc.get_block(bestblockhash) + txids.append(block["tx"][0]) # Get the coinbase transaction txid + + self.utreexod = utreexod_node + + self.node_manager.connect_nodes(self.bitcoind, self.utreexod) + time.sleep(5) + + self.node_manager.connect_nodes(self.florestad, self.utreexod) + time.sleep(5) + + self.node_manager.connect_nodes(self.florestad, self.bitcoind) + + self.log.info("Waiting for Florestad to sync with Bitcoin Core") + start = time.time() + blocks = self.bitcoind.rpc.get_block_count() + while time.time() - start < 30: + block_chain_info = self.florestad.rpc.get_blockchain_info() + if block_chain_info["height"] == blocks and not block_chain_info["ibd"]: + break + + time.sleep(1) + + elapsed_time = time.time() - start + self.log.info(f"=== time for nodes to sync: {elapsed_time:.2f} seconds ===") + assert self.florestad.rpc.get_block_count() == blocks + for txid in txids: + verbose = 2 + self.log.info( + f"Testing getrawtransaction for txid: {txid} " + f"with invalid verbose level {verbose}" + ) + with pytest.raises(HTTPError): + self.florestad.rpc.get_raw_transaction(txid, verbose=verbose) + + self.compare_getrawtransaction(txid) + + block_range = ( + f"Testing getrawtransaction for coinbase transactions " + f"in the block range {blocks - COINBASE_BLOCKS} to {blocks}" + ) + self.log.info(block_range) + for height in range(blocks - (COINBASE_BLOCKS - 1), blocks): + block_hash = self.bitcoind.rpc.get_blockhash(height) + + block = self.bitcoind.rpc.get_block(block_hash, verbosity=1) + coinbase_tx = block["tx"][0] # Get the coinbase transaction txid + + self.compare_getrawtransaction(coinbase_tx) + + self.compare_getrawtransaction(coinbase_tx) + + def compare_getrawtransaction(self, txid): + """Compare getrawtransaction output between Floresta and Bitcoin Core.""" + self.log.info( + f"Comparing getrawtransaction for txid: {txid} " f"with verbose level 0" + ) + get_raw_tx = self.florestad.rpc.get_raw_transaction(txid, verbose=0) + get_raw_tx_bitcoind = self.bitcoind.rpc.get_raw_transaction(txid, verbose=0) + assert get_raw_tx == get_raw_tx_bitcoind + + self.log.info( + f"Comparing getrawtransaction for txid: {txid} " f"with verbose level 1" + ) + get_raw_tx = self.florestad.rpc.get_raw_transaction(txid, verbose=1) + get_raw_tx_bitcoind = self.bitcoind.rpc.get_raw_transaction(txid, verbose=1) + + self.compare_transaction_data(get_raw_tx, get_raw_tx_bitcoind) + + def compare_transaction_data(self, tx_floresta, tx_bitcoind): + """Compare transaction data between Floresta and Bitcoin Core.""" + for key in tx_bitcoind.keys(): + self.log.info(f"Comparing key: {key}") + + if key == "vin": + self.compare_inputs(tx_floresta[key], tx_bitcoind[key]) + elif key == "vout": + self.compare_outputs(tx_floresta[key], tx_bitcoind[key]) + else: + assert tx_floresta[key] == tx_bitcoind[key] + + def compare_inputs(self, vin_floresta, vin_bitcoind): + """Compare transaction inputs (vin) field by field.""" + for i, vin in enumerate(vin_bitcoind): + self.log.info(f"Comparing vin index: {i}") + + for vin_key in vin.keys(): + self.log.info(f"Comparing vin key: {vin_key}") + if vin_key == "scriptSig": + self.compare_script_sig(vin_floresta[i][vin_key], vin[vin_key]) + else: + assert vin_floresta[i][vin_key] == vin[vin_key] + + def compare_script_sig(self, script_sig_floresta, script_sig_bitcoind): + """Compare scriptSig fields.""" + for script_key in script_sig_bitcoind.keys(): + self.log.info(f"Comparing scriptSig key: {script_key}") + assert script_sig_floresta[script_key] == script_sig_bitcoind[script_key] + + def compare_outputs(self, vout_floresta, vout_bitcoind): + """Compare transaction outputs (vout) field by field.""" + for i, vout in enumerate(vout_bitcoind): + self.log.info(f"Comparing vout index: {i}") + + for vout_key in vout.keys(): + self.log.info(f"Comparing vout key: {vout_key}") + + if vout_key == "scriptPubKey": + self.compare_script_pubkey( + vout_floresta[i][vout_key], vout[vout_key] + ) + else: + assert vout_floresta[i][vout_key] == vout[vout_key] + + def compare_script_pubkey(self, spk_floresta, spk_bitcoind): + """Skip Bitcoin Core specific fields like 'desc'.""" + for spk_key in spk_bitcoind.keys(): + self.log.info(f"Comparing scriptPubKey key: {spk_key}") + + # Skip fields that only Bitcoin Core returns + # (It's not implemented in Floresta yet) + if spk_key == "desc": + continue + + assert spk_floresta[spk_key] == spk_bitcoind[spk_key] diff --git a/tests/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index 4be48cdab..c39281596 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -39,7 +39,7 @@ class BaseRPC(ABC): Subclasses should use `perform_request` to implement RPC calls. """ - TIMEOUT: int = 15 # seconds + TIMEOUT: int = 30 # seconds def __init__(self, config: ConfigRPC, log): self._config = config @@ -150,6 +150,7 @@ def perform_request( # If response isnt 200, raise an HTTPError if response.status_code != 200: + self.log.error(f"HTTP error {response.status_code}: {response.text}") raise HTTPError result = response.json() @@ -344,3 +345,13 @@ def list_descriptors(self): List all loaded descriptors """ return self.perform_request("listdescriptors") + + def get_raw_transaction(self, txid: str, verbose: int | None = None): + """ + Returns the raw transaction data for a given transaction ID. + """ + params = [txid] + if verbose is not None: + params.append(int(verbose)) + + return self.perform_request("getrawtransaction", params=params) diff --git a/tests/test_framework/rpc/bitcoin.py b/tests/test_framework/rpc/bitcoin.py index 085b3061c..fd7de7569 100644 --- a/tests/test_framework/rpc/bitcoin.py +++ b/tests/test_framework/rpc/bitcoin.py @@ -46,3 +46,28 @@ def generate_block(self, nblocks: int) -> list: """ address = "bcrt1q3ml87jemlfvk7lq8gfs7pthvj5678ndnxnw9ch" return self.generate_block_to_address(nblocks, address) + + def get_new_address(self) -> str: + """ + Get a new address from the wallet. + """ + return self.perform_request("getnewaddress", params=[]) + + def generate_block_to_wallet(self, nblocks: int) -> list: + """ + Mine blocks immediately, with coinbase reward sent to a new wallet address + """ + address = self.get_new_address() + return self.generate_block_to_address(nblocks, address) + + def create_wallet(self, wallet_name: str): + """ + Create a new wallet with the given name. + """ + return self.perform_request("createwallet", params=[wallet_name]) + + def send_to_address(self, address: str, amount: float) -> str: + """ + Send a specified amount to a given address. + """ + return self.perform_request("sendtoaddress", params=[address, amount]) From 7331f65dffdeaafc1a184c46b2c6f2288d1da719 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:06:06 -0300 Subject: [PATCH 4/4] docs(rpc): add documentation for `getrawtransaction` RPC --- bin/floresta-cli/src/main.rs | 8 +++- doc/rpc/getrawtransaction.md | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 doc/rpc/getrawtransaction.md diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index ab409701d..71aa50fe9 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -213,7 +213,13 @@ pub enum Methods { }, /// Returns the transaction, assuming it is cached by our watch only wallet - #[command(name = "getrawtransaction")] + #[doc = include_str!("../../../doc/rpc/getrawtransaction.md")] + #[command( + name = "getrawtransaction", + about = "Returns raw transaction data for a given txid from wallet cache (controlled by verbosity level)", + long_about = Some(include_str!("../../../doc/rpc/getrawtransaction.md")), + disable_help_subcommand = true + )] GetRawTransaction { txid: Txid, verbose: Option }, #[doc = include_str!("../../../doc/rpc/rescanblockchain.md")] diff --git a/doc/rpc/getrawtransaction.md b/doc/rpc/getrawtransaction.md new file mode 100644 index 000000000..c498a7d55 --- /dev/null +++ b/doc/rpc/getrawtransaction.md @@ -0,0 +1,80 @@ +# `getrawtransaction` + +Returns a transaction that is cached in the wallet. This method retrieves transactions +that have been tracked and indexed by the wallet during blockchain synchronization or +through explicit tracking. The response format can be controlled via the verbosity parameter. + +## Usage + +### Synopsis + +``` +floresta-cli getrawtransaction [] +``` + +### Examples + +```bash +# Get transaction as serialized hex string +floresta-cli getrawtransaction "d7d7558342e3aff8fc4ae0a84bc7d19c0add4dfa1a3c0fa56cf66dc4f2400145" + +# Get transaction as detailed JSON object +floresta-cli getrawtransaction "d7d7558342e3aff8fc4ae0a84bc7d19c0add4dfa1a3c0fa56cf66dc4f2400145" 1 + +# Default behavior (hex format) +floresta-cli getrawtransaction "d7d7558342e3aff8fc4ae0a84bc7d19c0add4dfa1a3c0fa56cf66dc4f2400145" 0 +``` + +## Arguments + +-`txid` - (string, required) The transaction ID to retrieve. +- `verbosity` - (numeric, optional, default=1) + * `0`: Returns transaction as a raw serialized hexadecimal string. + * `1`: Returns transaction as a detailed JSON object with decoded fields. + +## Returns + +### Ok Response (verbosity = 0) + +- `"hex"` - (string) A serialized, hex-encoded string of the transaction data. + +### Ok Response (verbosity = 1) + +- `in_active_chain` - (boolean) Whether the transaction is in the active blockchain +- `blockhash` - (string, optional) The block hash containing this transaction +- `confirmations` - (numeric, optional) Number of confirmations (0 if in mempool) +- `blocktime` - (numeric, optional) Block creation time as Unix timestamp +- `time` - (numeric, optional) Transaction time as Unix timestamp +- `hex` - (string) The serialized, hex-encoded data for 'txid' +- `txid` - (string) The transaction id (same as provided) +- `hash` - (string) The transaction hash (differs from txid for witness transactions) +- `size` - (numeric) The transaction size in bytes +- `vsize` - (numeric) The virtual transaction size (differs from size for witness transactions) +- `weight` - (numeric) The transaction's weight (between vsize*4-3 and vsize*4) +- `version` - (numeric) The transaction version +- `locktime` - (numeric) The block height or timestamp at which transaction is final +- `vin` - (array) Array of transaction inputs + * `coinbase` - (string) The hex-encoded coinbase script data. **Only if coinbase transaction** + * `txid` - (string) The previous transaction ID. **Only if non-coinbase transaction** + * `vout` - (numeric) The output index in the previous transaction. **Only if non-coinbase transaction** + * `script_sig` - (object) Contains the scriptSig. **Only if non-coinbase transaction** + - `hex` - (string) The script hex + * `sequence` - (numeric) The sequence number + * `txinwitness` - (array, optional) Witness stack + - `hex` - (string) hex-encoded witness data (if any) +- `vout` - (array) Array of transaction outputs + * `value` - (numeric) Amount in btc + * `n` - (numeric) The output index + * `script_pub_key` - (object) Contains the scriptPubKey + - `asm` - (string) Disassembly of the output script + - `hex` - (string) The raw output script bytes, hex-encoded + - `type` - (string) The type of script (pubkey, pubkeyhash, multisig, etc.) + - `address` - (string, optional) The Bitcoin address (if applicable) + +### Error Enum `JsonRpcError` + +- `TxNotFound` - The requested transaction is not found in the wallet cache +- `InvalidVerbosityLevel` - Verbosity parameter is not 0 or 1 +- `MissingParameter` - The txid parameter was not provided +- `InvalidParameterType` - The txid is not a valid hex string +- `Decode` - Error during transaction decoding or serialization