From 575d77a0d2c0d6d1a4293bdf04380462c4d24131 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Fri, 17 Jan 2025 14:35:54 -0300 Subject: [PATCH 01/12] feat(btcio): get_current_timestamp --- crates/btcio/src/rpc/client.rs | 12 ++++++++++++ crates/btcio/src/rpc/traits.rs | 7 +++++++ crates/btcio/src/test_utils.rs | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 23962cd39..20cd03869 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -238,6 +238,12 @@ impl ReaderRpc for BitcoinClient { .await } + async fn get_current_timestamp(&self) -> ClientResult { + let best_block_hash = self.call::("getbestblockhash", &[]).await?; + let block = self.get_block(&best_block_hash).await?; + Ok(block.header.time) + } + async fn get_raw_mempool(&self) -> ClientResult> { self.call::>("getrawmempool", &[]).await } @@ -409,6 +415,12 @@ mod test { let get_blockchain_info = client.get_blockchain_info().await.unwrap(); assert_eq!(get_blockchain_info.blocks, 0); + // get_current_timestamp + let _ = client + .get_current_timestamp() + .await + .expect("must be able to get current timestamp"); + let blocks = mine_blocks(&bitcoind, 101, None).unwrap(); // get_block diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index 9dedb00d1..81acb0814 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -58,6 +58,13 @@ pub trait ReaderRpc { /// Gets various state info regarding blockchain processing. async fn get_blockchain_info(&self) -> ClientResult; + /// Gets the timestamp in the block header of the current best block in bitcoin. + /// + /// # Note + /// + /// Time is Unix epoch time in seconds. + async fn get_current_timestamp(&self) -> ClientResult; + /// Gets all transaction ids in mempool. async fn get_raw_mempool(&self) -> ClientResult>; diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index 467d5e487..3b2478ae7 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -95,6 +95,10 @@ impl ReaderRpc for TestBitcoinClient { }) } + async fn get_current_timestamp(&self) -> ClientResult { + Ok(1_000) + } + async fn get_raw_mempool(&self) -> ClientResult> { Ok(vec![]) } From 5868bf1e67c8653956d318b6096827c22b9ada1d Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Fri, 17 Jan 2025 14:49:14 -0300 Subject: [PATCH 02/12] feat(btcio): test_mempool_accept --- crates/btcio/src/rpc/client.rs | 21 ++++++++++++++++++++- crates/btcio/src/rpc/traits.rs | 5 ++++- crates/btcio/src/rpc/types.rs | 10 ++++++++++ crates/btcio/src/test_utils.rs | 9 ++++++++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 20cd03869..b30a96a11 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -29,7 +29,7 @@ use crate::rpc::{ types::{ CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress, GetTransaction, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListTransactions, ListUnspent, - SignRawTransactionWithWallet, + SignRawTransactionWithWallet, TestMempoolAccept, }, }; @@ -279,6 +279,13 @@ impl BroadcasterRpc for BitcoinClient { Err(e) => Err(ClientError::Other(e.to_string())), } } + + async fn test_mempool_accept(&self, tx: &Transaction) -> ClientResult> { + let txstr = serialize_hex(tx); + trace!(%txstr, "Testing mempool accept"); + self.call::>("testmempoolaccept", &[to_value([txstr])?]) + .await + } } #[async_trait] @@ -482,6 +489,18 @@ mod test { assert!(got.complete); assert!(consensus::encode::deserialize_hex::(&got.hex).is_ok()); + // test_mempool_accept + let txids = client + .test_mempool_accept(&tx) + .await + .expect("must be able to test mempool accept"); + let got = txids.first().expect("there must be at least one txid"); + assert_eq!( + got.txid, + tx.compute_txid(), + "txids must match in the mempool" + ); + // send_raw_transaction let got = client.send_raw_transaction(&tx).await.unwrap(); assert!(got.as_byte_array().len() == 32); diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index 81acb0814..b6ae6b450 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -5,7 +5,7 @@ use crate::rpc::{ client::ClientResult, types::{ GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, - ListTransactions, ListUnspent, SignRawTransactionWithWallet, + ListTransactions, ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, }, }; @@ -92,6 +92,9 @@ pub trait BroadcasterRpc { /// - `tx`: The raw transaction to send. This should be a byte array containing the serialized /// raw transaction data. async fn send_raw_transaction(&self, tx: &Transaction) -> ClientResult; + + /// Tests if a raw transaction is valid. + async fn test_mempool_accept(&self, tx: &Transaction) -> ClientResult>; } /// Wallet functionality that any Bitcoin client **without private keys** that diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 64cc76f3a..21de94b26 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -317,6 +317,16 @@ pub struct ListTransactions { pub txid: Txid, } +/// Models the result of JSON-RPC method `testmempoolaccept`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct TestMempoolAccept { + /// The transaction id. + #[serde(deserialize_with = "deserialize_txid")] + pub txid: Txid, + /// Rejection reason, if any. + pub reject_reason: Option, +} + /// Models the result of JSON-RPC method `signrawtransactionwithwallet`. /// /// # Note diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index 3b2478ae7..7859658f3 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -14,7 +14,7 @@ use crate::{ traits::{BroadcasterRpc, ReaderRpc, SignerRpc, WalletRpc}, types::{ GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, - ListTransactions, ListUnspent, SignRawTransactionWithWallet, + ListTransactions, ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, }, ClientResult, }, @@ -114,6 +114,13 @@ impl BroadcasterRpc for TestBitcoinClient { async fn send_raw_transaction(&self, _tx: &Transaction) -> ClientResult { Ok(Txid::from_slice(&[1u8; 32]).unwrap()) } + async fn test_mempool_accept(&self, _tx: &Transaction) -> ClientResult> { + let some_tx: Transaction = consensus::encode::deserialize_hex(SOME_TX).unwrap(); + Ok(vec![TestMempoolAccept { + txid: some_tx.compute_txid(), + reject_reason: None, + }]) + } } #[async_trait] From 71db641ad61755049550dd9cefa3ac7b4416262e Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Mon, 20 Jan 2025 17:02:36 -0300 Subject: [PATCH 03/12] feat(btcio): get_tx_out --- crates/btcio/src/rpc/client.rs | 29 ++++++++++++++++++++++-- crates/btcio/src/rpc/traits.rs | 10 ++++++++- crates/btcio/src/rpc/types.rs | 41 ++++++++++++++++++++++++++++++++++ crates/btcio/src/test_utils.rs | 28 +++++++++++++++++++++-- 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index b30a96a11..d7ee31ba8 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -28,8 +28,8 @@ use crate::rpc::{ traits::{BroadcasterRpc, ReaderRpc, SignerRpc, WalletRpc}, types::{ CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress, GetTransaction, - ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListTransactions, ListUnspent, - SignRawTransactionWithWallet, TestMempoolAccept, + GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListTransactions, + ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, }, }; @@ -248,6 +248,23 @@ impl ReaderRpc for BitcoinClient { self.call::>("getrawmempool", &[]).await } + async fn get_tx_out( + &self, + txid: &Txid, + vout: u32, + include_mempool: bool, + ) -> ClientResult { + self.call::( + "gettxout", + &[ + to_value(txid.to_string())?, + to_value(vout)?, + to_value(include_mempool)?, + ], + ) + .await + } + async fn network(&self) -> ClientResult { Ok(self .call::("getblockchaininfo", &[]) @@ -505,6 +522,14 @@ mod test { let got = client.send_raw_transaction(&tx).await.unwrap(); assert!(got.as_byte_array().len() == 32); + // get_tx_out + let vout = 0; + let got = client + .get_tx_out(&tx.compute_txid(), vout, true) + .await + .unwrap(); + assert_eq!(got.value, 1.0); + // list_transactions let got = client.list_transactions(None).await.unwrap(); assert_eq!(got.len(), 10); diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index b6ae6b450..e8785531f 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -4,7 +4,7 @@ use bitcoin::{bip32::Xpriv, Address, Block, BlockHash, Network, Transaction, Txi use crate::rpc::{ client::ClientResult, types::{ - GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, + GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, ListTransactions, ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, }, }; @@ -68,6 +68,14 @@ pub trait ReaderRpc { /// Gets all transaction ids in mempool. async fn get_raw_mempool(&self) -> ClientResult>; + /// Returns details about an unspent transaction output. + async fn get_tx_out( + &self, + txid: &Txid, + vout: u32, + include_mempool: bool, + ) -> ClientResult; + /// Gets the underlying [`Network`] information. async fn network(&self) -> ClientResult; } diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 21de94b26..3861014f6 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -146,6 +146,47 @@ pub struct GetBlockVerbosityOne { pub next_block_hash: Option, } +/// Result of JSON-RPC method `gettxout`. +/// +/// > gettxout "txid" n ( include_mempool ) +/// > +/// > Returns details about an unspent transaction output. +/// > +/// > Arguments: +/// > 1. txid (string, required) The transaction id +/// > 2. n (numeric, required) vout number +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct GetTxOut { + /// The hash of the block at the tip of the chain. + #[serde(rename = "bestblock")] + pub best_block: String, + /// The number of confirmations. + pub confirmations: u32, // TODO: Change this to an i64. + /// The transaction value in BTC. + pub value: f64, + /// The script pubkey. + #[serde(rename = "scriptPubkey")] + pub script_pubkey: Option, + /// Coinbase or not. + pub coinbase: bool, +} + +/// A script pubkey. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ScriptPubkey { + /// Script assembly. + pub asm: String, + /// Script hex. + pub hex: String, + #[serde(rename = "reqSigs")] + pub req_sigs: i64, + /// The type, eg pubkeyhash. + #[serde(rename = "type")] + pub type_: String, + /// Bitcoin address. + pub address: Option, +} + /// Result of JSON-RPC method `gettxout`. /// /// # Note diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index 7859658f3..96cb031ce 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -13,8 +13,9 @@ use crate::{ rpc::{ traits::{BroadcasterRpc, ReaderRpc, SignerRpc, WalletRpc}, types::{ - GetBlockchainInfo, GetTransaction, ImportDescriptor, ImportDescriptorResult, - ListTransactions, ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, + GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, + ListTransactions, ListUnspent, ScriptPubkey, SignRawTransactionWithWallet, + TestMempoolAccept, }, ClientResult, }, @@ -103,6 +104,29 @@ impl ReaderRpc for TestBitcoinClient { Ok(vec![]) } + async fn get_tx_out( + &self, + _txid: &Txid, + _vout: u32, + _include_mempool: bool, + ) -> ClientResult { + Ok(GetTxOut { + best_block: BlockHash::all_zeros().to_string(), + confirmations: 1, + value: 1.0, + script_pubkey: Some(ScriptPubkey { + // Taken from mainnet txid + // e35e3357cac58a56dab78fa3c544f52f091561ff84428da28bdc5c49fc4c5ffc + asm: "OP_0 OP_PUSHBYTES_20 78a93a5b649de9deabd9494ae9bc41f3c9c13837".to_string(), + hex: "001478a93a5b649de9deabd9494ae9bc41f3c9c13837".to_string(), + req_sigs: 1, + type_: "V0_P2WPKH".to_string(), + address: Some("bc1q0z5n5kmynh5aa27ef99wn0zp70yuzwph68my2c".to_string()), + }), + coinbase: false, + }) + } + async fn network(&self) -> ClientResult { Ok(Network::Regtest) } From ae2738b1f49f1da3cef2c419d2a4b16628c3cb2b Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 21 Jan 2025 10:21:34 -0300 Subject: [PATCH 04/12] feat(btcio): submit_package --- crates/btcio/src/rpc/client.rs | 8 +++- crates/btcio/src/rpc/traits.rs | 15 ++++++- crates/btcio/src/rpc/types.rs | 75 ++++++++++++++++++++++++++++++++++ crates/btcio/src/test_utils.rs | 25 +++++++++++- 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index d7ee31ba8..81b14a969 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -29,7 +29,7 @@ use crate::rpc::{ types::{ CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListTransactions, - ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, + ListUnspent, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }, }; @@ -265,6 +265,12 @@ impl ReaderRpc for BitcoinClient { .await } + async fn submit_package(&self, txs: &[Transaction]) -> ClientResult { + let txstrs: Vec = txs.iter().map(serialize_hex).collect(); + self.call::("submitpackage", &[to_value(txstrs)?]) + .await + } + async fn network(&self) -> ClientResult { Ok(self .call::("getblockchaininfo", &[]) diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index e8785531f..55d7e8d0a 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -5,7 +5,8 @@ use crate::rpc::{ client::ClientResult, types::{ GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListTransactions, ListUnspent, SignRawTransactionWithWallet, TestMempoolAccept, + ListTransactions, ListUnspent, SignRawTransactionWithWallet, SubmitPackage, + TestMempoolAccept, }, }; @@ -76,6 +77,18 @@ pub trait ReaderRpc { include_mempool: bool, ) -> ClientResult; + /// Submit a package of raw transactions (serialized, hex-encoded) to local node. + /// + /// The package will be validated according to consensus and mempool policy rules. If any + /// transaction passes, it will be accepted to mempool. This RPC is experimental and the + /// interface may be unstable. Refer to doc/policy/packages.md for documentation on package + /// policies. + /// + /// # Warning + /// + /// Successful submission does not mean the transactions will propagate throughout the network. + async fn submit_package(&self, txs: &[Transaction]) -> ClientResult; + /// Gets the underlying [`Network`] information. async fn network(&self) -> ClientResult; } diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 3861014f6..973dd9d05 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use bitcoin::{ absolute::Height, address::{self, NetworkUnchecked}, @@ -187,6 +189,79 @@ pub struct ScriptPubkey { pub address: Option, } +/// Result of JSON-RPC method `submitpackage`. +/// +/// > submitpackage ["rawtx",...] ( maxfeerate maxburnamount ) +/// > +/// > Submit a package of raw transactions (serialized, hex-encoded) to local node. +/// > The package will be validated according to consensus and mempool policy rules. If any +/// > transaction passes, it will be accepted to mempool. +/// > This RPC is experimental and the interface may be unstable. Refer to doc/policy/packages.md +/// > for documentation on package policies. +/// > Warning: successful submission does not mean the transactions will propagate throughout the +/// > network. +/// > +/// > Arguments: +/// > 1. package (json array, required) An array of raw transactions. +/// > The package must solely consist of a child and its parents. None of the parents may depend on +/// > each other. +/// > The package must be topologically sorted, with the child being the last element in the array. +/// > [ +/// > "rawtx", (string) +/// > ... +/// > ] +#[allow(clippy::doc_lazy_continuation)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct SubmitPackage { + /// The transaction package result message. + /// + /// "success" indicates all transactions were accepted into or are already in the mempool. + pub package_msg: String, + /// Transaction results keyed by wtxid. + #[serde(rename = "tx-results")] + pub tx_results: BTreeMap, + /// List of txids of replaced transactions. + #[serde(rename = "replaced-transactions")] + pub replaced_transactions: Vec, +} + +/// Models the per-transaction result included in the JSON-RPC method `submitpackage`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct SubmitPackageTxResult { + /// The transaction id. + pub txid: String, + /// The wtxid of a different transaction with the same txid but different witness found in the + /// mempool. + /// + /// If set, this means the submitted transaction was ignored. + #[serde(rename = "other-wtxid")] + pub other_wtxid: Option, + /// Sigops-adjusted virtual transaction size. + pub vsize: Option, + /// Transaction fees. + pub fees: Option, + /// The transaction error string, if it was rejected by the mempool + pub error: Option, +} + +/// Models the fees included in the per-transaction result of the JSON-RPC method `submitpackage`. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct SubmitPackageTxResultFees { + /// Transaction fee. + #[serde(rename = "base")] + pub base_fee: f64, + /// The effective feerate. + /// + /// Will be `None` if the transaction was already in the mempool. For example, the package + /// feerate and/or feerate with modified fees from the `prioritisetransaction` JSON-RPC method. + #[serde(rename = "effective-feerate")] + pub effective_fee_rate: Option, + /// If [`Self::effective_fee_rate`] is provided, this holds the wtxid's of the transactions + /// whose fees and vsizes are included in effective-feerate. + #[serde(rename = "effective-includes")] + pub effective_includes: Option>, +} + /// Result of JSON-RPC method `gettxout`. /// /// # Note diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index 96cb031ce..3994087c7 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use async_trait::async_trait; use bitcoin::{ bip32::Xpriv, @@ -15,7 +17,7 @@ use crate::{ types::{ GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, ListTransactions, ListUnspent, ScriptPubkey, SignRawTransactionWithWallet, - TestMempoolAccept, + SubmitPackage, SubmitPackageTxResult, TestMempoolAccept, }, ClientResult, }, @@ -127,6 +129,27 @@ impl ReaderRpc for TestBitcoinClient { }) } + async fn submit_package(&self, _txs: &[Transaction]) -> ClientResult { + let some_tx: Transaction = consensus::encode::deserialize_hex(SOME_TX).unwrap(); + let wtxid = some_tx.compute_wtxid(); + let vsize = some_tx.vsize(); + let tx_results = BTreeMap::from([( + wtxid.to_string(), + SubmitPackageTxResult { + txid: some_tx.compute_txid().to_string(), + other_wtxid: None, + vsize: Some(vsize as i64), + fees: None, + error: None, + }, + )]); + Ok(SubmitPackage { + package_msg: "success".to_string(), + tx_results, + replaced_transactions: vec![], + }) + } + async fn network(&self) -> ClientResult { Ok(Network::Regtest) } From 609e0e59251fbd829cf08a859e13c99fc19dfb4f Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 21 Jan 2025 11:41:49 -0300 Subject: [PATCH 05/12] feat(btcio): create_raw_transaction --- crates/btcio/src/rpc/client.rs | 28 ++++++++++++--- crates/btcio/src/rpc/traits.rs | 12 +++++-- crates/btcio/src/rpc/types.rs | 62 ++++++++++++++++++++++++++++++++++ crates/btcio/src/test_utils.rs | 14 ++++++-- 4 files changed, 105 insertions(+), 11 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 81b14a969..eee52d615 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -8,8 +8,9 @@ use std::{ use async_trait::async_trait; use base64::{engine::general_purpose, Engine}; use bitcoin::{ - bip32::Xpriv, consensus::encode::serialize_hex, Address, Block, BlockHash, Network, - Transaction, Txid, + bip32::Xpriv, + consensus::{self, encode::serialize_hex}, + Address, Block, BlockHash, Network, Transaction, Txid, }; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, @@ -27,9 +28,10 @@ use crate::rpc::{ error::{BitcoinRpcError, ClientError}, traits::{BroadcasterRpc, ReaderRpc, SignerRpc, WalletRpc}, types::{ - CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress, GetTransaction, - GetTxOut, ImportDescriptor, ImportDescriptorResult, ListDescriptors, ListTransactions, - ListUnspent, SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, + CreateRawTransaction, CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, + GetNewAddress, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, + ListDescriptors, ListTransactions, ListUnspent, SignRawTransactionWithWallet, + SubmitPackage, TestMempoolAccept, }, }; @@ -343,6 +345,22 @@ impl WalletRpc for BitcoinClient { async fn list_wallets(&self) -> ClientResult> { self.call::>("listwallets", &[]).await } + + async fn create_raw_transaction( + &self, + raw_tx: CreateRawTransaction, + ) -> ClientResult { + let raw_tx = self + .call::( + "createrawtransaction", + &[to_value(raw_tx.inputs)?, to_value(raw_tx.outputs)?], + ) + .await?; + trace!(%raw_tx, "Created raw transaction"); + Ok(consensus::encode::deserialize_hex(&raw_tx).map_err(|e| { + ClientError::Other(format!("Failed to deserialize raw transaction: {}", e)) + })?) + } } #[async_trait] diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index 55d7e8d0a..37058bad7 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -4,9 +4,9 @@ use bitcoin::{bip32::Xpriv, Address, Block, BlockHash, Network, Transaction, Txi use crate::rpc::{ client::ClientResult, types::{ - GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListTransactions, ListUnspent, SignRawTransactionWithWallet, SubmitPackage, - TestMempoolAccept, + CreateRawTransaction, GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, + ImportDescriptorResult, ListTransactions, ListUnspent, SignRawTransactionWithWallet, + SubmitPackage, TestMempoolAccept, }, }; @@ -159,6 +159,12 @@ pub trait WalletRpc { /// Lists all wallets in the underlying Bitcoin client. async fn list_wallets(&self) -> ClientResult>; + + /// Creates a raw transaction. + async fn create_raw_transaction( + &self, + raw_tx: CreateRawTransaction, + ) -> ClientResult; } /// Signing functionality that any Bitcoin client **with private keys** that diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 973dd9d05..3c16c497a 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -189,6 +189,68 @@ pub struct ScriptPubkey { pub address: Option, } +/// Models the arguments of JSON-RPC method `createrawtransaction`. +/// +/// # Note +/// +/// Assumes that the transaction is always "replaceable" by default and has a locktime of 0. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct CreateRawTransaction { + pub inputs: Vec, + pub outputs: Vec, +} + +/// Models the input of JSON-RPC method `createrawtransaction`. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct CreateRawTransactionInput { + pub txid: String, + pub vout: u32, +} + +/// Models the output of JSON-RPC method `createrawtransaction`. +/// +/// The outputs specified as key-value pairs, where the keys is an address, +/// and the values are the amounts to be sent to that address. +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(untagged)] +pub enum CreateRawTransactionOutput { + /// A pair of an [`Address`] string and an [`Amount`] in BTC. + AddressAmount { + /// An [`Address`] string. + address: String, + /// An [`Amount`] in BTC. + amount: f64, + }, + /// A payload such as in `OP_RETURN` transactions. + Data { + /// The payload. + data: String, + }, +} + +impl Serialize for CreateRawTransactionOutput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + CreateRawTransactionOutput::AddressAmount { address, amount } => { + let mut map = serde_json::Map::new(); + map.insert( + address.clone(), + serde_json::Value::Number(serde_json::Number::from_f64(*amount).unwrap()), + ); + map.serialize(serializer) + } + CreateRawTransactionOutput::Data { data } => { + let mut map = serde_json::Map::new(); + map.insert("data".to_string(), serde_json::Value::String(data.clone())); + map.serialize(serializer) + } + } + } +} + /// Result of JSON-RPC method `submitpackage`. /// /// > submitpackage ["rawtx",...] ( maxfeerate maxburnamount ) diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index 3994087c7..f26300b40 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -15,9 +15,9 @@ use crate::{ rpc::{ traits::{BroadcasterRpc, ReaderRpc, SignerRpc, WalletRpc}, types::{ - GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListTransactions, ListUnspent, ScriptPubkey, SignRawTransactionWithWallet, - SubmitPackage, SubmitPackageTxResult, TestMempoolAccept, + CreateRawTransaction, GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, + ImportDescriptorResult, ListTransactions, ListUnspent, ScriptPubkey, + SignRawTransactionWithWallet, SubmitPackage, SubmitPackageTxResult, TestMempoolAccept, }, ClientResult, }, @@ -239,6 +239,14 @@ impl WalletRpc for TestBitcoinClient { async fn list_wallets(&self) -> ClientResult> { Ok(vec![]) } + + async fn create_raw_transaction( + &self, + _raw_tx: CreateRawTransaction, + ) -> ClientResult { + let some_tx: Transaction = consensus::encode::deserialize_hex(SOME_TX).unwrap(); + Ok(some_tx) + } } #[async_trait] From 9593b7c7e64ecbaf9f6c72321e1992e6773b6924 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 21 Jan 2025 11:44:11 -0300 Subject: [PATCH 06/12] refactor(btcio): some debugging goodies :) --- crates/btcio/src/rpc/client.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index eee52d615..f7387a406 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -130,10 +130,13 @@ impl BitcoinClient { trace!(?response, "Response received"); match response { Ok(resp) => { - let data = resp - .json::>() + let raw_response = resp + .text() .await .map_err(|e| ClientError::Parse(e.to_string()))?; + trace!(%raw_response, "Raw response received"); + let data: Response = serde_json::from_str(&raw_response) + .map_err(|e| ClientError::Parse(e.to_string()))?; if let Some(err) = data.error { return Err(ClientError::Server(err.code, err.message)); } From a3b44d8825898a60ab1cafd573e1e65cc38d28e4 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 21 Jan 2025 11:44:25 -0300 Subject: [PATCH 07/12] test(btcio): submit_package --- crates/btcio/src/rpc/client.rs | 100 ++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index f7387a406..6c5354a6d 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -445,11 +445,14 @@ impl SignerRpc for BitcoinClient { #[cfg(test)] mod test { - use bitcoin::{consensus, hashes::Hash, NetworkKind}; + use bitcoin::{consensus, hashes::Hash, Amount, NetworkKind}; use strata_common::logging; use super::*; - use crate::test_utils::corepc_node_helpers::{get_bitcoind_and_client, mine_blocks}; + use crate::{ + rpc::types::{CreateRawTransactionInput, CreateRawTransactionOutput}, + test_utils::corepc_node_helpers::{get_bitcoind_and_client, mine_blocks}, + }; #[tokio::test()] async fn client_works() { @@ -588,4 +591,97 @@ mod test { let expected = vec![ImportDescriptorResult { success: true }]; assert_eq!(expected, got); } + + /// Create two transactions. + /// 1. Normal one: sends 1 BTC to an address that we control. + /// 2. CFFP: replaces the first transaction with a different one that we also control. + /// + /// This is needed because we must SIGN all these transactions, and we can't sign a transaction + /// that we don't control. + #[tokio::test()] + async fn submit_package() { + logging::init(logging::LoggerConfig::with_base_name("btcio-tests")); + + let (bitcoind, client) = get_bitcoind_and_client(); + + // network + let got = client.network().await.unwrap(); + let expected = Network::Regtest; + assert_eq!(expected, got); + + let blocks = mine_blocks(&bitcoind, 101, None).unwrap(); + let last_block = client.get_block(blocks.first().unwrap()).await.unwrap(); + let coinbase_tx = last_block.coinbase().unwrap(); + + let destination = client.get_new_address().await.unwrap(); + let change_address = client.get_new_address().await.unwrap(); + let amount = Amount::from_btc(1.0).unwrap(); + let change_amount = Amount::from_btc(48.999).unwrap(); // 0.0001 fee + let amount_minus_fees = Amount::from_sat(amount.to_sat() - 2_000); + + let send_back_address = client.get_new_address().await.unwrap(); + let parent_raw_tx = CreateRawTransaction { + inputs: vec![CreateRawTransactionInput { + txid: coinbase_tx.compute_txid().to_string(), + vout: 0, + }], + outputs: vec![ + // Destination + CreateRawTransactionOutput::AddressAmount { + address: destination.to_string(), + amount: amount.to_btc(), + }, + // Change + CreateRawTransactionOutput::AddressAmount { + address: change_address.to_string(), + amount: change_amount.to_btc(), + }, + ], + }; + let parent = client.create_raw_transaction(parent_raw_tx).await.unwrap(); + let signed_parent: Transaction = consensus::encode::deserialize_hex( + client + .sign_raw_transaction_with_wallet(&parent) + .await + .unwrap() + .hex + .as_str(), + ) + .unwrap(); + + // sanity check + let parent_submitted = client.send_raw_transaction(&signed_parent).await.unwrap(); + + let child_raw_tx = CreateRawTransaction { + inputs: vec![CreateRawTransactionInput { + txid: parent_submitted.to_string(), + vout: 0, + }], + outputs: vec![ + // Send back + CreateRawTransactionOutput::AddressAmount { + address: send_back_address.to_string(), + amount: amount_minus_fees.to_btc(), + }, + ], + }; + let child = client.create_raw_transaction(child_raw_tx).await.unwrap(); + let signed_child: Transaction = consensus::encode::deserialize_hex( + client + .sign_raw_transaction_with_wallet(&child) + .await + .unwrap() + .hex + .as_str(), + ) + .unwrap(); + + // Ok now we have a parent and a child transaction. + let result = client + .submit_package(&[signed_parent, signed_child]) + .await + .unwrap(); + assert_eq!(result.tx_results.len(), 2); + assert_eq!(result.package_msg, "success"); + } } From 835223d7edd0fe2d49ee0357de2cd9e060f16571 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 21 Jan 2025 15:00:09 -0300 Subject: [PATCH 08/12] fix(btcio): submit_package vsize not an Option --- crates/btcio/src/rpc/types.rs | 2 +- crates/btcio/src/test_utils.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index 3c16c497a..be9f05eef 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -299,7 +299,7 @@ pub struct SubmitPackageTxResult { #[serde(rename = "other-wtxid")] pub other_wtxid: Option, /// Sigops-adjusted virtual transaction size. - pub vsize: Option, + pub vsize: i64, /// Transaction fees. pub fees: Option, /// The transaction error string, if it was rejected by the mempool diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index f26300b40..23fcd2dad 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -138,7 +138,7 @@ impl ReaderRpc for TestBitcoinClient { SubmitPackageTxResult { txid: some_tx.compute_txid().to_string(), other_wtxid: None, - vsize: Some(vsize as i64), + vsize: vsize as i64, fees: None, error: None, }, From 94cac87e998082d8e97434f0924bbc52be6363ed Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 21 Jan 2025 15:34:43 -0300 Subject: [PATCH 09/12] test(btcio): get_tx_out unspent and spent --- crates/btcio/src/rpc/client.rs | 61 +++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 6c5354a6d..39b22d75f 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -454,6 +454,9 @@ mod test { test_utils::corepc_node_helpers::{get_bitcoind_and_client, mine_blocks}, }; + /// 50 BTC in [`Network::Regtest`]. + const COINBASE_AMOUNT: Amount = Amount::from_sat(50 * 100_000_000); + #[tokio::test()] async fn client_works() { logging::init(logging::LoggerConfig::with_base_name("btcio-tests")); @@ -552,14 +555,6 @@ mod test { let got = client.send_raw_transaction(&tx).await.unwrap(); assert!(got.as_byte_array().len() == 32); - // get_tx_out - let vout = 0; - let got = client - .get_tx_out(&tx.compute_txid(), vout, true) - .await - .unwrap(); - assert_eq!(got.value, 1.0); - // list_transactions let got = client.list_transactions(None).await.unwrap(); assert_eq!(got.len(), 10); @@ -592,6 +587,56 @@ mod test { assert_eq!(expected, got); } + async fn get_tx_out() { + logging::init(logging::LoggerConfig::with_base_name("btcio-gettxout")); + + let (bitcoind, client) = get_bitcoind_and_client(); + + // network sanity check + let got = client.network().await.unwrap(); + let expected = Network::Regtest; + assert_eq!(expected, got); + + let address = bitcoind + .client + .get_new_address() + .unwrap() + .address() + .unwrap() + .assume_checked(); + let blocks = mine_blocks(&bitcoind, 101, Some(address)).unwrap(); + let last_block = client.get_block(blocks.first().unwrap()).await.unwrap(); + let coinbase_tx = last_block.coinbase().unwrap(); + + // gettxout should work with a non-spent UTXO. + let got = client + .get_tx_out(&coinbase_tx.compute_txid(), 0, true) + .await + .unwrap(); + assert_eq!(got.value, COINBASE_AMOUNT.to_btc()); + + // gettxout should fail with a spent UTXO. + let new_address = bitcoind + .client + .get_new_address() + .unwrap() + .address() + .unwrap() + .assume_checked(); + let send_amount = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 2_000); // 2k sats as fees. + let _send_tx = bitcoind + .client + .send_to_address(&new_address, send_amount) + .unwrap() + .txid() + .unwrap(); + let result = client + .get_tx_out(&coinbase_tx.compute_txid(), 0, true) + .await; + trace!(?result, "gettxout result"); + assert!(result.is_err()); + } + /// Create two transactions. /// 1. Normal one: sends 1 BTC to an address that we control. /// 2. CFFP: replaces the first transaction with a different one that we also control. From 8cf5bbece171e3dbe9df7fb45d997a033f80cfe6 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 22 Jan 2025 09:18:20 -0300 Subject: [PATCH 10/12] test(btcio): refactor submit_package --- crates/btcio/src/rpc/client.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index 39b22d75f..c1544030d 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -645,11 +645,11 @@ mod test { /// that we don't control. #[tokio::test()] async fn submit_package() { - logging::init(logging::LoggerConfig::with_base_name("btcio-tests")); + logging::init(logging::LoggerConfig::with_base_name("btcio-submitpackage")); let (bitcoind, client) = get_bitcoind_and_client(); - // network + // network sanity check let got = client.network().await.unwrap(); let expected = Network::Regtest; assert_eq!(expected, got); @@ -661,7 +661,8 @@ mod test { let destination = client.get_new_address().await.unwrap(); let change_address = client.get_new_address().await.unwrap(); let amount = Amount::from_btc(1.0).unwrap(); - let change_amount = Amount::from_btc(48.999).unwrap(); // 0.0001 fee + let fees = Amount::from_btc(0.0001).unwrap(); + let change_amount = COINBASE_AMOUNT - amount - fees; let amount_minus_fees = Amount::from_sat(amount.to_sat() - 2_000); let send_back_address = client.get_new_address().await.unwrap(); From 5994041609c2ec35baf49b3866ace7ff5b7ffa52 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 22 Jan 2025 11:49:34 -0300 Subject: [PATCH 11/12] feat(btcio): PreviousTransactionOutput for signrawtx --- crates/btcio/src/rpc/types.rs | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/btcio/src/rpc/types.rs b/crates/btcio/src/rpc/types.rs index be9f05eef..60c73d21d 100644 --- a/crates/btcio/src/rpc/types.rs +++ b/crates/btcio/src/rpc/types.rs @@ -521,6 +521,46 @@ pub struct SignRawTransactionWithWallet { pub errors: Option>, } +/// Models the optional previous transaction outputs argument for the method +/// `signrawtransactionwithwallet`. +/// +/// These are the outputs that this transaction depends on but may not yet be in the block chain. +/// Widely used for One Parent One Child (1P1C) Relay in Bitcoin >28.0. +/// +/// > transaction outputs +/// > [ +/// > { (json object) +/// > "txid": "hex", (string, required) The transaction id +/// > "vout": n, (numeric, required) The output number +/// > "scriptPubKey": "hex", (string, required) The output script +/// > "redeemScript": "hex", (string, optional) (required for P2SH) redeem script +/// > "witnessScript": "hex", (string, optional) (required for P2WSH or P2SH-P2WSH) witness +/// > script +/// > "amount": amount, (numeric or string, optional) (required for Segwit inputs) the +/// > amount spent +/// > }, +/// > ... +/// > ] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PreviousTransactionOutput { + /// The transaction id. + #[serde(deserialize_with = "deserialize_txid")] + pub txid: Txid, + /// The output number. + pub vout: u32, + /// The output script. + #[serde(rename = "scriptPubKey")] + pub script_pubkey: String, + /// The redeem script. + #[serde(rename = "redeemScript")] + pub redeem_script: Option, + /// The witness script. + #[serde(rename = "witnessScript")] + pub witness_script: Option, + /// The amount spent. + pub amount: Option, +} + /// Models the result of the JSON-RPC method `listdescriptors`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct ListDescriptors { From 198ba41fdf3c4740c7998335b72832d58b88f24a Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Wed, 22 Jan 2025 11:49:55 -0300 Subject: [PATCH 12/12] test(btcio): submit_package 1P1C --- crates/btcio/src/rpc/client.rs | 120 ++++++++++++++++++++++++++++-- crates/btcio/src/rpc/traits.rs | 5 +- crates/btcio/src/test_utils.rs | 6 +- crates/btcio/src/writer/signer.rs | 2 +- 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/crates/btcio/src/rpc/client.rs b/crates/btcio/src/rpc/client.rs index c1544030d..38e26fbb4 100644 --- a/crates/btcio/src/rpc/client.rs +++ b/crates/btcio/src/rpc/client.rs @@ -30,8 +30,8 @@ use crate::rpc::{ types::{ CreateRawTransaction, CreateWallet, GetBlockVerbosityZero, GetBlockchainInfo, GetNewAddress, GetTransaction, GetTxOut, ImportDescriptor, ImportDescriptorResult, - ListDescriptors, ListTransactions, ListUnspent, SignRawTransactionWithWallet, - SubmitPackage, TestMempoolAccept, + ListDescriptors, ListTransactions, ListUnspent, PreviousTransactionOutput, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }, }; @@ -371,12 +371,14 @@ impl SignerRpc for BitcoinClient { async fn sign_raw_transaction_with_wallet( &self, tx: &Transaction, + prev_outputs: Option>, ) -> ClientResult { let tx_hex = serialize_hex(tx); trace!(tx_hex = %tx_hex, "Signing transaction"); + trace!(?prev_outputs, "Signing transaction with previous outputs"); self.call::( "signrawtransactionwithwallet", - &[to_value(tx_hex)?], + &[to_value(tx_hex)?, to_value(prev_outputs)?], ) .await } @@ -445,7 +447,7 @@ impl SignerRpc for BitcoinClient { #[cfg(test)] mod test { - use bitcoin::{consensus, hashes::Hash, Amount, NetworkKind}; + use bitcoin::{consensus, hashes::Hash, transaction, Amount, NetworkKind}; use strata_common::logging; use super::*; @@ -535,7 +537,10 @@ mod test { assert_eq!(expected, got); // sign_raw_transaction_with_wallet - let got = client.sign_raw_transaction_with_wallet(&tx).await.unwrap(); + let got = client + .sign_raw_transaction_with_wallet(&tx, None) + .await + .unwrap(); assert!(got.complete); assert!(consensus::encode::deserialize_hex::(&got.hex).is_ok()); @@ -687,7 +692,7 @@ mod test { let parent = client.create_raw_transaction(parent_raw_tx).await.unwrap(); let signed_parent: Transaction = consensus::encode::deserialize_hex( client - .sign_raw_transaction_with_wallet(&parent) + .sign_raw_transaction_with_wallet(&parent, None) .await .unwrap() .hex @@ -714,7 +719,7 @@ mod test { let child = client.create_raw_transaction(child_raw_tx).await.unwrap(); let signed_child: Transaction = consensus::encode::deserialize_hex( client - .sign_raw_transaction_with_wallet(&child) + .sign_raw_transaction_with_wallet(&child, None) .await .unwrap() .hex @@ -730,4 +735,105 @@ mod test { assert_eq!(result.tx_results.len(), 2); assert_eq!(result.package_msg, "success"); } + + /// Similar to [`submit_package`], but with where the parent does not pay fees, + /// and the child has to pay fees. + /// + /// This is called 1P1C because it has one parent and one child. + /// See + /// for more information. + #[tokio::test] + async fn submit_package_1p1c() { + logging::init(logging::LoggerConfig::with_base_name( + "btcio-submitpackage-1p1c", + )); + + let (bitcoind, client) = get_bitcoind_and_client(); + + // 1p1c sanity check + let server_version = bitcoind.client.server_version().unwrap(); + assert!(server_version > 28); + + let destination = client.get_new_address().await.unwrap(); + + let blocks = mine_blocks(&bitcoind, 101, None).unwrap(); + let last_block = client.get_block(blocks.first().unwrap()).await.unwrap(); + let coinbase_tx = last_block.coinbase().unwrap(); + + let parent_raw_tx = CreateRawTransaction { + inputs: vec![CreateRawTransactionInput { + txid: coinbase_tx.compute_txid().to_string(), + vout: 0, + }], + outputs: vec![CreateRawTransactionOutput::AddressAmount { + address: destination.to_string(), + amount: COINBASE_AMOUNT.to_btc(), + }], + }; + let mut parent = client.create_raw_transaction(parent_raw_tx).await.unwrap(); + parent.version = transaction::Version(3); + assert_eq!(parent.version, transaction::Version(3)); + trace!(?parent, "parent:"); + let signed_parent: Transaction = consensus::encode::deserialize_hex( + client + .sign_raw_transaction_with_wallet(&parent, None) + .await + .unwrap() + .hex + .as_str(), + ) + .unwrap(); + assert_eq!(signed_parent.version, transaction::Version(3)); + + // Assert that the parent tx cannot be broadcasted. + let parent_broadcasted = client.send_raw_transaction(&signed_parent).await; + assert!(parent_broadcasted.is_err()); + + // 5k sats as fees. + let amount_minus_fees = Amount::from_sat(COINBASE_AMOUNT.to_sat() - 43_000); + let child_raw_tx = CreateRawTransaction { + inputs: vec![CreateRawTransactionInput { + txid: signed_parent.compute_txid().to_string(), + vout: 0, + }], + outputs: vec![CreateRawTransactionOutput::AddressAmount { + address: destination.to_string(), + amount: amount_minus_fees.to_btc(), + }], + }; + let mut child = client.create_raw_transaction(child_raw_tx).await.unwrap(); + child.version = transaction::Version(3); + assert_eq!(child.version, transaction::Version(3)); + trace!(?child, "child:"); + let prev_outputs = vec![PreviousTransactionOutput { + txid: parent.compute_txid(), + vout: 0, + script_pubkey: parent.output[0].script_pubkey.to_hex_string(), + redeem_script: None, + witness_script: None, + amount: Some(COINBASE_AMOUNT.to_btc()), + }]; + let signed_child: Transaction = consensus::encode::deserialize_hex( + client + .sign_raw_transaction_with_wallet(&child, Some(prev_outputs)) + .await + .unwrap() + .hex + .as_str(), + ) + .unwrap(); + assert_eq!(signed_child.version, transaction::Version(3)); + + // Assert that the child tx cannot be broadcasted. + let child_broadcasted = client.send_raw_transaction(&signed_child).await; + assert!(child_broadcasted.is_err()); + + // Let's send as a package 1C1P. + let result = client + .submit_package(&[signed_parent, signed_child]) + .await + .unwrap(); + assert_eq!(result.tx_results.len(), 2); + assert_eq!(result.package_msg, "success"); + } } diff --git a/crates/btcio/src/rpc/traits.rs b/crates/btcio/src/rpc/traits.rs index 37058bad7..6938e4895 100644 --- a/crates/btcio/src/rpc/traits.rs +++ b/crates/btcio/src/rpc/traits.rs @@ -5,8 +5,8 @@ use crate::rpc::{ client::ClientResult, types::{ CreateRawTransaction, GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListTransactions, ListUnspent, SignRawTransactionWithWallet, - SubmitPackage, TestMempoolAccept, + ImportDescriptorResult, ListTransactions, ListUnspent, PreviousTransactionOutput, + SignRawTransactionWithWallet, SubmitPackage, TestMempoolAccept, }, }; @@ -190,6 +190,7 @@ pub trait SignerRpc { async fn sign_raw_transaction_with_wallet( &self, tx: &Transaction, + prev_outputs: Option>, ) -> ClientResult; /// Gets the underlying [`Xpriv`] from the wallet. diff --git a/crates/btcio/src/test_utils.rs b/crates/btcio/src/test_utils.rs index 23fcd2dad..b14cd33a3 100644 --- a/crates/btcio/src/test_utils.rs +++ b/crates/btcio/src/test_utils.rs @@ -16,8 +16,9 @@ use crate::{ traits::{BroadcasterRpc, ReaderRpc, SignerRpc, WalletRpc}, types::{ CreateRawTransaction, GetBlockchainInfo, GetTransaction, GetTxOut, ImportDescriptor, - ImportDescriptorResult, ListTransactions, ListUnspent, ScriptPubkey, - SignRawTransactionWithWallet, SubmitPackage, SubmitPackageTxResult, TestMempoolAccept, + ImportDescriptorResult, ListTransactions, ListUnspent, PreviousTransactionOutput, + ScriptPubkey, SignRawTransactionWithWallet, SubmitPackage, SubmitPackageTxResult, + TestMempoolAccept, }, ClientResult, }, @@ -254,6 +255,7 @@ impl SignerRpc for TestBitcoinClient { async fn sign_raw_transaction_with_wallet( &self, tx: &Transaction, + _prev_outputs: Option>, ) -> ClientResult { let tx_hex = consensus::encode::serialize_hex(tx); Ok(SignRawTransactionWithWallet { diff --git a/crates/btcio/src/writer/signer.rs b/crates/btcio/src/writer/signer.rs index e79d157cd..f8e9bec7b 100644 --- a/crates/btcio/src/writer/signer.rs +++ b/crates/btcio/src/writer/signer.rs @@ -31,7 +31,7 @@ pub async fn create_and_sign_payload_envelopes( debug!(commit_txid = ?ctxid, "Signing commit transaction"); let signed_commit = ctx .client - .sign_raw_transaction_with_wallet(&commit) + .sign_raw_transaction_with_wallet(&commit, None) .await .map_err(|e| EnvelopeError::SignRawTransaction(e.to_string()))? .hex;