From bd6863c43d85a5d18933137601156779fa58feb3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 30 Aug 2024 13:17:52 +0200 Subject: [PATCH 1/5] Add `submitpackage` RPC support --- client/src/client_sync/{v28.rs => v28/mod.rs} | 3 + .../src/client_sync/v28/raw_transactions.rs | 52 ++++++++ integration_test/src/lib.rs | 1 + integration_test/src/v28/mod.rs | 5 + integration_test/src/v28/raw_transactions.rs | 75 ++++++++++++ integration_test/tests/v28_api.rs | 1 + json/src/v28/mod.rs | 5 +- json/src/v28/raw_transactions.rs | 112 ++++++++++++++++++ 8 files changed, 253 insertions(+), 1 deletion(-) rename client/src/client_sync/{v28.rs => v28/mod.rs} (95%) create mode 100644 client/src/client_sync/v28/raw_transactions.rs create mode 100644 integration_test/src/v28/mod.rs create mode 100644 integration_test/src/v28/raw_transactions.rs create mode 100644 json/src/v28/raw_transactions.rs diff --git a/client/src/client_sync/v28.rs b/client/src/client_sync/v28/mod.rs similarity index 95% rename from client/src/client_sync/v28.rs rename to client/src/client_sync/v28/mod.rs index dab6ff7..7128bb3 100644 --- a/client/src/client_sync/v28.rs +++ b/client/src/client_sync/v28/mod.rs @@ -4,6 +4,8 @@ //! //! We ignore option arguments unless they effect the shape of the returned JSON data. +mod raw_transactions; + use bitcoin::address::{Address, NetworkChecked}; use bitcoin::{Amount, Block, BlockHash, Txid}; @@ -30,6 +32,7 @@ crate::impl_client_check_expected_server_version!({ [280000] }); // == Rawtransactions == crate::impl_client_v17__sendrawtransaction!(); +crate::impl_client_v28__submitpackage!(); // == Wallet == crate::impl_client_v17__createwallet!(); diff --git a/client/src/client_sync/v28/raw_transactions.rs b/client/src/client_sync/v28/raw_transactions.rs new file mode 100644 index 0000000..db2ca00 --- /dev/null +++ b/client/src/client_sync/v28/raw_transactions.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing JSON-RPC methods on a client. +//! +//! Specifically this is methods found under the `== Rawtransactions ==` section of the +//! API docs of `bitcoind v28.0`. +//! +//! All macros require `Client` to be in scope. +//! +//! See or use the `define_jsonrpc_minreq_client!` macro to define a `Client`. + +/// Implements bitcoind JSON-RPC API method `submitpackage` +#[macro_export] +macro_rules! impl_client_v28__submitpackage { + () => { + impl Client { + /// Submit a package of transactions 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. + /// + /// ## Arguments: + /// 1. `package`: 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. + /// 2. `maxfeerate`: Reject transactions whose fee rate is higher than the specified value. + /// Fee rates larger than 1BTC/kvB are rejected. + /// Set to 0 to accept any fee rate. + /// If unset, will default to 0.10 BTC/kvb. + /// 3. `maxburnamount` If set, reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value. + /// If burning funds through unspendable outputs is desired, increase this value. + /// This check is based on heuristics and does not guarantee spendability of outputs. + pub fn submit_package( + &self, + package: &[bitcoin::Transaction], + max_fee_rate: Option, + max_burn_amount: Option, + ) -> Result { + let package_txs = package + .into_iter() + .map(|tx| bitcoin::consensus::encode::serialize_hex(tx)) + .collect::>(); + let max_fee_rate_btc_kvb = + max_fee_rate.map(|r| r.to_sat_per_vb_floor() as f64 / 100_000.0); + let max_burn_amount_btc = max_burn_amount.map(|a| a.to_btc()); + self.call( + "submitpackage", + &[package_txs.into(), max_fee_rate_btc_kvb.into(), max_burn_amount_btc.into()], + ) + } + } + }; +} diff --git a/integration_test/src/lib.rs b/integration_test/src/lib.rs index 7fca3a9..9a19fe9 100644 --- a/integration_test/src/lib.rs +++ b/integration_test/src/lib.rs @@ -3,6 +3,7 @@ pub mod v17; pub mod v19; pub mod v22; +pub mod v28; /// Requires `RPC_PORT` to be in scope. use bitcoind::BitcoinD; diff --git a/integration_test/src/v28/mod.rs b/integration_test/src/v28/mod.rs new file mode 100644 index 0000000..52143a7 --- /dev/null +++ b/integration_test/src/v28/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing test methods on a JSON-RPC client for `bitcoind v28.0`. + +pub mod raw_transactions; diff --git a/integration_test/src/v28/raw_transactions.rs b/integration_test/src/v28/raw_transactions.rs new file mode 100644 index 0000000..d25634a --- /dev/null +++ b/integration_test/src/v28/raw_transactions.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing test methods on a JSON-RPC client. +//! +//! Specifically this is methods found under the `== Rawtransactions ==` section of the +//! API docs of `bitcoind v28.0`. + +/// Requires `Client` to be in scope +#[macro_export] +macro_rules! impl_test_v28__submitpackage { + () => { + #[test] + fn submitpackage() { + //let bitcoind = $crate::bitcoind_no_wallet(); + + let bitcoind = $crate::bitcoind_with_default_wallet(); + + // Submitting the empty package should simply fail. + assert!(bitcoind.client.submit_package(&[], None, None).is_err()); + + // Premine to get some funds + let address = bitcoind.client.new_address().expect("failed to get new address"); + let json = + bitcoind.client.generate_to_address(101, &address).expect("generatetoaddress"); + json.into_model().unwrap(); + + // Send to ourselves, mine, send again to generate two transactions. + let (tx_0, tx_1) = { + let new_address = bitcoind.client.new_address().expect("failed to get new address"); + let txid = bitcoind + .client + .send_to_address(&new_address, bitcoin::Amount::from_sat(1000000)) + .unwrap() + .into_model() + .unwrap() + .txid; + + let _ = + bitcoind.client.generate_to_address(1, &address).expect("generatetoaddress"); + + let best_block_hash = bitcoind.client.best_block_hash().unwrap(); + let best_block = bitcoind.client.get_block(best_block_hash).unwrap(); + let tx_0 = best_block.txdata[1].clone(); + + let new_address = bitcoind.client.new_address().expect("failed to get new address"); + let txid = bitcoind + .client + .send_to_address(&new_address, bitcoin::Amount::from_sat(1000000)) + .unwrap() + .into_model() + .unwrap() + .txid; + + let _ = + bitcoind.client.generate_to_address(1, &address).expect("generatetoaddress"); + + let best_block_hash = bitcoind.client.best_block_hash().unwrap(); + let best_block = bitcoind.client.get_block(best_block_hash).unwrap(); + let tx_1 = best_block.txdata[1].clone(); + (tx_0, tx_1) + }; + + // The call for submitting this package should succeed, but yield an 'already known' + // error for all transactions. + let res = bitcoind + .client + .submit_package(&[tx_0, tx_1], None, None) + .expect("failed to submit package"); + for (_, tx_result) in &res.tx_results { + assert!(tx_result.error.is_some()); + } + assert!(res.replaced_transactions.is_empty()); + } + }; +} diff --git a/integration_test/tests/v28_api.rs b/integration_test/tests/v28_api.rs index 40a9f75..5bbfc0a 100644 --- a/integration_test/tests/v28_api.rs +++ b/integration_test/tests/v28_api.rs @@ -40,6 +40,7 @@ mod raw_transactions { use super::*; impl_test_v17__sendrawtransaction!(); + impl_test_v28__submitpackage!(); } // == Wallet == diff --git a/json/src/v28/mod.rs b/json/src/v28/mod.rs index 10101a6..9505164 100644 --- a/json/src/v28/mod.rs +++ b/json/src/v28/mod.rs @@ -90,7 +90,7 @@ //! - [ ] `joinpsbts ["psbt",...]` //! - [ ] `sendrawtransaction "hexstring" ( maxfeerate maxburnamount )` //! - [ ] `signrawtransactionwithkey "hexstring" ["privatekey",...] ( [{"txid":"hex","vout":n,"scriptPubKey":"hex","redeemScript":"hex","witnessScript":"hex","amount":amount},...] "sighashtype" )` -//! - [ ] `submitpackage ["rawtx",...] ( maxfeerate maxburnamount )` +//! - [x] `submitpackage ["rawtx",...] ( maxfeerate maxburnamount )` //! - [ ] `testmempoolaccept ["rawtx",...] ( maxfeerate )` //! - [ ] `utxoupdatepsbt "psbt" ( ["",{"desc":"str","range":n or [n,n]},...] )` //! @@ -182,12 +182,15 @@ mod blockchain; mod network; +mod raw_transactions; #[doc(inline)] pub use self::blockchain::GetBlockchainInfo; #[doc(inline)] pub use self::network::GetNetworkInfo; #[doc(inline)] +pub use self::raw_transactions::{SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees}; +#[doc(inline)] pub use crate::{ v17::{ GenerateToAddress, GetBalance, GetBestBlockHash, GetBlockVerbosityOne, diff --git a/json/src/v28/raw_transactions.rs b/json/src/v28/raw_transactions.rs new file mode 100644 index 0000000..7ed6d7f --- /dev/null +++ b/json/src/v28/raw_transactions.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! The JSON-RPC API for Bitcoin Core v28.0 - raw transactions. +//! +//! Types for methods found under the `== Rawtransactions ==` section of the API docs. + +use std::collections::HashMap; + +use bitcoin::{Amount, FeeRate, Txid, Wtxid}; +use serde::{Deserialize, Serialize}; + +/// Models the 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) +// ... +// ] +//2. maxfeerate (numeric or string, optional, default="0.10") Reject transactions whose fee rate is higher than the specified value, expressed in BTC/kvB. +// Fee rates larger than 1BTC/kvB are rejected. +// Set to 0 to accept any fee rate. +//3. maxburnamount (numeric or string, optional, default="0.00") Reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value, expressed in BTC. +// If burning funds through unspendable outputs is desired, increase this value. +// This check is based on heuristics and does not guarantee spendability of outputs. +// +// +//Result: +//{ (json object) +// "package_msg" : "str", (string) The transaction package result message. "success" indicates all transactions were accepted into or are already in the mempool. +// "tx-results" : { (json object) transaction results keyed by wtxid +// "wtxid" : { (json object) transaction wtxid +// "txid" : "hex", (string) The transaction hash in hex +// "other-wtxid" : "hex", (string, optional) The wtxid of a different transaction with the same txid but different witness found in the mempool. This means the submitted transaction was ignored. +// "vsize" : n, (numeric, optional) Sigops-adjusted virtual transaction size. +// "fees" : { (json object, optional) Transaction fees +// "base" : n, (numeric) transaction fee in BTC +// "effective-feerate" : n, (numeric, optional) if the transaction was not already in the mempool, the effective feerate in BTC per KvB. For example, the package feerate and/or feerate with modified fees from prioritisetransaction. +// "effective-includes" : [ (json array, optional) if effective-feerate is provided, the wtxids of the transactions whose fees and vsizes are included in effective-feerate. +// "hex", (string) transaction wtxid in hex +// ... +// ] +// }, +// "error" : "str" (string, optional) The transaction error string, if it was rejected by the mempool +// }, +// ... +// }, +// "replaced-transactions" : [ (json array, optional) List of txids of replaced transactions +// "hex", (string) The transaction id +// ... +// ] +//} +// +//Examples: +//> curl --user myusername --data-binary '{"jsonrpc": "2.0", "id": "curltest", "method": "submitpackage", "params": [["rawtx1", "rawtx2"]]}' -H 'content-type: application/json' http://127.0.0.1:8332/ +//> bitcoin-cli submitpackage '["rawtx1", "rawtx2"]' +#[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: HashMap, + /// 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: Txid, + /// 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: Amount, + /// 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_feerate: Option, + /// If [`Self::effective_feerate`] 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: Vec, +} From 704135779f7fec629db063008571e5905beeb8ef Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 11 Nov 2024 10:16:35 +0100 Subject: [PATCH 2/5] f Include `into_model` implementations --- json/src/model/mod.rs | 4 ++- json/src/model/raw_transactions.rs | 53 +++++++++++++++++++++++++++++- json/src/v28/raw_transactions.rs | 39 +++++++++++++++++++++- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/json/src/model/mod.rs b/json/src/model/mod.rs index adb90cc..f2f4988 100644 --- a/json/src/model/mod.rs +++ b/json/src/model/mod.rs @@ -34,7 +34,9 @@ pub use self::{ }, generating::{Generate, GenerateToAddress}, network::{GetNetworkInfo, GetNetworkInfoAddress, GetNetworkInfoNetwork}, - raw_transactions::SendRawTransaction, + raw_transactions::{ + SendRawTransaction, SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees, + }, wallet::{ CreateWallet, GetBalance, GetBalances, GetBalancesMine, GetBalancesWatchOnly, GetNewAddress, GetTransaction, GetTransactionDetail, GetTransactionDetailCategory, diff --git a/json/src/model/raw_transactions.rs b/json/src/model/raw_transactions.rs index a236ce1..581115e 100644 --- a/json/src/model/raw_transactions.rs +++ b/json/src/model/raw_transactions.rs @@ -5,9 +5,60 @@ //! These structs model the types returned by the JSON-RPC API but have concrete types //! and are not specific to a specific version of Bitcoin Core. -use bitcoin::Txid; +use std::collections::HashMap; + +use bitcoin::{Amount, FeeRate, Txid, Wtxid}; use serde::{Deserialize, Serialize}; /// Models the result of JSON-RPC method `sendrawtransaction`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct SendRawTransaction(pub Txid); + +#[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: HashMap, + /// 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: Txid, + /// 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: Amount, + /// 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_feerate: Option, + /// If [`Self::effective_feerate`] 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: Vec, +} diff --git a/json/src/v28/raw_transactions.rs b/json/src/v28/raw_transactions.rs index 7ed6d7f..64943a7 100644 --- a/json/src/v28/raw_transactions.rs +++ b/json/src/v28/raw_transactions.rs @@ -9,7 +9,9 @@ use std::collections::HashMap; use bitcoin::{Amount, FeeRate, Txid, Wtxid}; use serde::{Deserialize, Serialize}; -/// Models the result of JSON-RPC method `submitpackage`. +use crate::model; + +/// Result of JSON-RPC method `submitpackage`. //submitpackage ["rawtx",...] ( maxfeerate maxburnamount ) // //Submit a package of raw transactions (serialized, hex-encoded) to local node. @@ -74,6 +76,17 @@ pub struct SubmitPackage { pub replaced_transactions: Vec, } +impl SubmitPackage { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::SubmitPackage { + model::SubmitPackage { + package_msg: self.package_msg, + tx_results: self.tx_results.into_iter().map(|(k, v)| (k, v.into_model())).collect(), + replaced_transactions: self.replaced_transactions, + } + } +} + /// Models the per-transaction result included in the JSON-RPC method `submitpackage`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct SubmitPackageTxResult { @@ -92,6 +105,19 @@ pub struct SubmitPackageTxResult { pub error: Option, } +impl SubmitPackageTxResult { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::SubmitPackageTxResult { + model::SubmitPackageTxResult { + txid: self.txid, + other_wtxid: self.other_wtxid, + vsize: self.vsize, + fees: self.fees.map(move |f| f.into_model()), + error: self.error, + } + } +} + /// Models the fees included in the per-transaction result of the JSON-RPC method `submitpackage`. #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct SubmitPackageTxResultFees { @@ -110,3 +136,14 @@ pub struct SubmitPackageTxResultFees { #[serde(rename = "effective-includes")] pub effective_includes: Vec, } + +impl SubmitPackageTxResultFees { + /// Converts version specific type to a version in-specific, more strongly typed type. + pub fn into_model(self) -> model::SubmitPackageTxResultFees { + model::SubmitPackageTxResultFees { + base_fee: self.base_fee, + effective_feerate: self.effective_feerate, + effective_includes: self.effective_includes, + } + } +} From 949125031520a54496ceb5228f0c5a0185fd69b3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 12 Nov 2024 10:36:11 +0100 Subject: [PATCH 3/5] f Drop `serde` stuff from model --- json/src/model/raw_transactions.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/json/src/model/raw_transactions.rs b/json/src/model/raw_transactions.rs index 581115e..179913f 100644 --- a/json/src/model/raw_transactions.rs +++ b/json/src/model/raw_transactions.rs @@ -8,33 +8,29 @@ use std::collections::HashMap; use bitcoin::{Amount, FeeRate, Txid, Wtxid}; -use serde::{Deserialize, Serialize}; /// Models the result of JSON-RPC method `sendrawtransaction`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq)] pub struct SendRawTransaction(pub Txid); -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq)] 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: HashMap, /// 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)] +#[derive(Clone, Debug, PartialEq)] pub struct SubmitPackageTxResult { /// The transaction id. pub txid: Txid, /// 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, @@ -45,20 +41,17 @@ pub struct SubmitPackageTxResult { } /// Models the fees included in the per-transaction result of the JSON-RPC method `submitpackage`. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq)] pub struct SubmitPackageTxResultFees { /// Transaction fee. - #[serde(rename = "base")] pub base_fee: Amount, /// 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_feerate: Option, /// If [`Self::effective_feerate`] 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: Vec, } From 0517507c74358af87e1fa83818472e9598167460 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 12 Nov 2024 11:31:17 +0100 Subject: [PATCH 4/5] f Don't use `rust-bitcoin` types in non-model --- integration_test/src/v28/raw_transactions.rs | 2 + json/src/model/mod.rs | 3 +- json/src/model/raw_transactions.rs | 15 ++++ json/src/v28/raw_transactions.rs | 88 ++++++++++++++------ 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/integration_test/src/v28/raw_transactions.rs b/integration_test/src/v28/raw_transactions.rs index d25634a..b0af281 100644 --- a/integration_test/src/v28/raw_transactions.rs +++ b/integration_test/src/v28/raw_transactions.rs @@ -65,6 +65,8 @@ macro_rules! impl_test_v28__submitpackage { let res = bitcoind .client .submit_package(&[tx_0, tx_1], None, None) + .expect("failed to submit package") + .into_model() .expect("failed to submit package"); for (_, tx_result) in &res.tx_results { assert!(tx_result.error.is_some()); diff --git a/json/src/model/mod.rs b/json/src/model/mod.rs index f2f4988..f3fc086 100644 --- a/json/src/model/mod.rs +++ b/json/src/model/mod.rs @@ -35,7 +35,8 @@ pub use self::{ generating::{Generate, GenerateToAddress}, network::{GetNetworkInfo, GetNetworkInfoAddress, GetNetworkInfoNetwork}, raw_transactions::{ - SendRawTransaction, SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees, + SendRawTransaction, SubmitPackage, SubmitPackageError, SubmitPackageTxResult, + SubmitPackageTxResultFees, }, wallet::{ CreateWallet, GetBalance, GetBalances, GetBalancesMine, GetBalancesWatchOnly, diff --git a/json/src/model/raw_transactions.rs b/json/src/model/raw_transactions.rs index 179913f..8137a29 100644 --- a/json/src/model/raw_transactions.rs +++ b/json/src/model/raw_transactions.rs @@ -7,6 +7,8 @@ use std::collections::HashMap; +use bitcoin::amount::ParseAmountError; +use bitcoin::hex::HexToArrayError; use bitcoin::{Amount, FeeRate, Txid, Wtxid}; /// Models the result of JSON-RPC method `sendrawtransaction`. @@ -55,3 +57,16 @@ pub struct SubmitPackageTxResultFees { /// whose fees and vsizes are included in effective-feerate. pub effective_includes: Vec, } + +/// Error when converting a `SubmitPackageTxResultFees` type into the model type. +#[derive(Debug)] +pub enum SubmitPackageError { + /// Conversion of a `Txid` failed. + Txid(HexToArrayError), + /// Conversion of a `Wtxid` failed. + Wtxid(HexToArrayError), + /// Conversion of the `base_fee` field failed. + BaseFee(ParseAmountError), + /// Conversion of the `vsize` field failed. + Vsize, +} diff --git a/json/src/v28/raw_transactions.rs b/json/src/v28/raw_transactions.rs index 64943a7..9612cb6 100644 --- a/json/src/v28/raw_transactions.rs +++ b/json/src/v28/raw_transactions.rs @@ -5,11 +5,12 @@ //! Types for methods found under the `== Rawtransactions ==` section of the API docs. use std::collections::HashMap; +use std::str::FromStr; use bitcoin::{Amount, FeeRate, Txid, Wtxid}; use serde::{Deserialize, Serialize}; -use crate::model; +use crate::model::{self, SubmitPackageError}; /// Result of JSON-RPC method `submitpackage`. //submitpackage ["rawtx",...] ( maxfeerate maxburnamount ) @@ -70,20 +71,33 @@ pub struct SubmitPackage { pub package_msg: String, /// Transaction results keyed by [`Wtxid`]. #[serde(rename = "tx-results")] - pub tx_results: HashMap, + pub tx_results: HashMap, /// List of txids of replaced transactions. #[serde(rename = "replaced-transactions")] - pub replaced_transactions: Vec, + pub replaced_transactions: Vec, } impl SubmitPackage { /// Converts version specific type to a version in-specific, more strongly typed type. - pub fn into_model(self) -> model::SubmitPackage { - model::SubmitPackage { - package_msg: self.package_msg, - tx_results: self.tx_results.into_iter().map(|(k, v)| (k, v.into_model())).collect(), - replaced_transactions: self.replaced_transactions, + pub fn into_model(self) -> Result { + let mut tx_results = HashMap::with_capacity(self.tx_results.len()); + for (k, v) in self.tx_results { + let wtxid = Wtxid::from_str(&k).map_err(model::SubmitPackageError::Wtxid)?; + let result = v.into_model()?; + tx_results.insert(wtxid, result); + } + + let mut replaced_transactions = Vec::with_capacity(self.replaced_transactions.len()); + for t in self.replaced_transactions { + let txid = Txid::from_str(&t).map_err(model::SubmitPackageError::Txid)?; + replaced_transactions.push(txid); } + + Ok(model::SubmitPackage { + package_msg: self.package_msg, + tx_results, + replaced_transactions, + }) } } @@ -96,9 +110,9 @@ pub struct SubmitPackageTxResult { /// /// If set, this means the submitted transaction was ignored. #[serde(rename = "other-wtxid")] - pub other_wtxid: Option, + pub other_wtxid: Option, /// Sigops-adjusted virtual transaction size. - pub vsize: Option, + pub vsize: Option, /// Transaction fees. pub fees: Option, /// The transaction error string, if it was rejected by the mempool @@ -107,14 +121,26 @@ pub struct SubmitPackageTxResult { impl SubmitPackageTxResult { /// Converts version specific type to a version in-specific, more strongly typed type. - pub fn into_model(self) -> model::SubmitPackageTxResult { - model::SubmitPackageTxResult { + pub fn into_model(self) -> Result { + let other_wtxid = if let Some(other_wtxid) = self.other_wtxid { + Some(Wtxid::from_str(&other_wtxid).map_err(SubmitPackageError::Wtxid)?) + } else { + None + }; + + let vsize = if let Some(vsize) = self.vsize { + Some(vsize.try_into().map_err(|_| SubmitPackageError::Vsize)?) + } else { + None + }; + let fees = if let Some(fees) = self.fees { Some(fees.into_model()?) } else { None }; + Ok(model::SubmitPackageTxResult { txid: self.txid, - other_wtxid: self.other_wtxid, - vsize: self.vsize, - fees: self.fees.map(move |f| f.into_model()), + other_wtxid, + vsize, + fees, error: self.error, - } + }) } } @@ -123,27 +149,41 @@ impl SubmitPackageTxResult { pub struct SubmitPackageTxResultFees { /// Transaction fee. #[serde(rename = "base")] - pub base_fee: Amount, + 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_feerate: Option, + pub effective_feerate: Option, /// If [`Self::effective_feerate`] 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: Vec, + pub effective_includes: Vec, } impl SubmitPackageTxResultFees { /// Converts version specific type to a version in-specific, more strongly typed type. - pub fn into_model(self) -> model::SubmitPackageTxResultFees { - model::SubmitPackageTxResultFees { - base_fee: self.base_fee, - effective_feerate: self.effective_feerate, - effective_includes: self.effective_includes, + pub fn into_model(self) -> Result { + let mut effective_includes = Vec::with_capacity(self.effective_includes.len()); + for include in self.effective_includes { + let wtxid = Wtxid::from_str(&include).map_err(SubmitPackageError::Wtxid)?; + effective_includes.push(wtxid); } + + let effective_feerate = self.effective_feerate.map(|rate_btc_kvb| { + // Bitcoin Core gives us a feerate in BTC/KvB. + // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. + let fee_rate_sat_per_kwu = (rate_btc_kvb * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }); + + Ok(model::SubmitPackageTxResultFees { + base_fee: Amount::from_btc(self.base_fee) + .map_err(model::SubmitPackageError::BaseFee)?, + effective_feerate, + effective_includes, + }) } } From 156be9c4a4d8334cb93eec1319866616e491b7ae Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 12 Nov 2024 11:31:51 +0100 Subject: [PATCH 5/5] f Drop `submitpackage` RPC example output comment --- json/src/v28/raw_transactions.rs | 52 -------------------------------- 1 file changed, 52 deletions(-) diff --git a/json/src/v28/raw_transactions.rs b/json/src/v28/raw_transactions.rs index 9612cb6..7a804a1 100644 --- a/json/src/v28/raw_transactions.rs +++ b/json/src/v28/raw_transactions.rs @@ -13,58 +13,6 @@ use serde::{Deserialize, Serialize}; use crate::model::{self, SubmitPackageError}; /// 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) -// ... -// ] -//2. maxfeerate (numeric or string, optional, default="0.10") Reject transactions whose fee rate is higher than the specified value, expressed in BTC/kvB. -// Fee rates larger than 1BTC/kvB are rejected. -// Set to 0 to accept any fee rate. -//3. maxburnamount (numeric or string, optional, default="0.00") Reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value, expressed in BTC. -// If burning funds through unspendable outputs is desired, increase this value. -// This check is based on heuristics and does not guarantee spendability of outputs. -// -// -//Result: -//{ (json object) -// "package_msg" : "str", (string) The transaction package result message. "success" indicates all transactions were accepted into or are already in the mempool. -// "tx-results" : { (json object) transaction results keyed by wtxid -// "wtxid" : { (json object) transaction wtxid -// "txid" : "hex", (string) The transaction hash in hex -// "other-wtxid" : "hex", (string, optional) The wtxid of a different transaction with the same txid but different witness found in the mempool. This means the submitted transaction was ignored. -// "vsize" : n, (numeric, optional) Sigops-adjusted virtual transaction size. -// "fees" : { (json object, optional) Transaction fees -// "base" : n, (numeric) transaction fee in BTC -// "effective-feerate" : n, (numeric, optional) if the transaction was not already in the mempool, the effective feerate in BTC per KvB. For example, the package feerate and/or feerate with modified fees from prioritisetransaction. -// "effective-includes" : [ (json array, optional) if effective-feerate is provided, the wtxids of the transactions whose fees and vsizes are included in effective-feerate. -// "hex", (string) transaction wtxid in hex -// ... -// ] -// }, -// "error" : "str" (string, optional) The transaction error string, if it was rejected by the mempool -// }, -// ... -// }, -// "replaced-transactions" : [ (json array, optional) List of txids of replaced transactions -// "hex", (string) The transaction id -// ... -// ] -//} -// -//Examples: -//> curl --user myusername --data-binary '{"jsonrpc": "2.0", "id": "curltest", "method": "submitpackage", "params": [["rawtx1", "rawtx2"]]}' -H 'content-type: application/json' http://127.0.0.1:8332/ -//> bitcoin-cli submitpackage '["rawtx1", "rawtx2"]' #[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.