Skip to content

Commit dada062

Browse files
committed
Merge #14: v28: Add support for submitpackage
7c3024c v28: Add support for submitpackage (Tobin C. Harding) Pull request description: Bitcoin Core introduced a new RPC method `submitpackage`. Add support for it including integration testing. This work was originally done by tnull a while ago over on the old `rust-bitcoind-json-rpc` repository (PR: rust-bitcoin/rust-bitcoind-json-rpc#23). I took his changes and updated the code with the ideas I've learned recently fleshing out v17. Patch includes `<Co-developed-by>` tag. Close: #7 ACKs for top commit: apoelstra: ACK 7c3024c; successfully ran local tests Tree-SHA512: 35a6a43b472633436069867619ca82440c777b675b2c6b9e4baff85f5461241dd7f3fca3e26d5d7c98e1df3b38d5aa41d448b4e657c3ad99a1ed66bace656926
2 parents b32b8ee + 7c3024c commit dada062

File tree

10 files changed

+434
-6
lines changed

10 files changed

+434
-6
lines changed

client/src/client_sync/v28.rs client/src/client_sync/v28/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//!
55
//! We ignore option arguments unless they effect the shape of the returned JSON data.
66
7+
pub mod raw_transactions;
8+
79
use bitcoin::address::{Address, NetworkChecked};
810
use bitcoin::{Amount, Block, BlockHash, Txid};
911

@@ -33,6 +35,7 @@ crate::impl_client_check_expected_server_version!({ [280000] });
3335

3436
// == Rawtransactions ==
3537
crate::impl_client_v17__sendrawtransaction!();
38+
crate::impl_client_v28__submitpackage!();
3639

3740
// == Wallet ==
3841
crate::impl_client_v17__createwallet!();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! Macros for implementing JSON-RPC methods on a client.
4+
//!
5+
//! Specifically this is methods found under the `== Rawtransactions ==` section of the
6+
//! API docs of `bitcoind v28.0`.
7+
//!
8+
//! All macros require `Client` to be in scope.
9+
//!
10+
//! See or use the `define_jsonrpc_minreq_client!` macro to define a `Client`.
11+
12+
/// Implements Bitcoin Core JSON-RPC API method `submitpackage`
13+
#[macro_export]
14+
macro_rules! impl_client_v28__submitpackage {
15+
() => {
16+
impl Client {
17+
pub fn submit_package(
18+
&self,
19+
package: &[bitcoin::Transaction],
20+
max_fee_rate: Option<bitcoin::FeeRate>,
21+
max_burn_amount: Option<bitcoin::Amount>,
22+
) -> Result<SubmitPackage> {
23+
let package_txs = package
24+
.into_iter()
25+
.map(|tx| bitcoin::consensus::encode::serialize_hex(tx))
26+
.collect::<Vec<_>>();
27+
let max_fee_rate_btc_kvb =
28+
max_fee_rate.map(|r| r.to_sat_per_vb_floor() as f64 / 100_000.0);
29+
let max_burn_amount_btc = max_burn_amount.map(|a| a.to_btc());
30+
self.call(
31+
"submitpackage",
32+
&[package_txs.into(), max_fee_rate_btc_kvb.into(), max_burn_amount_btc.into()],
33+
)
34+
}
35+
}
36+
};
37+
}

integration_test/src/lib.rs

+34-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ pub trait NodeExt {
4747

4848
/// Generates [`NBLOCKS`] to an address controlled by the loaded wallet.
4949
fn fund_wallet(&self);
50+
51+
/// Mine a block.
52+
///
53+
/// Should send mining reward to a new address for the loaded wallet.
54+
fn mine_a_block(&self);
55+
56+
/// Create a transaction and mine it.
57+
///
58+
/// # Returns
59+
///
60+
/// The receive address and the transaction.
61+
fn create_mined_transaction(&self) -> (bitcoin::Address, bitcoin::Transaction);
5062
}
5163

5264
impl NodeExt for Node {
@@ -56,15 +68,35 @@ impl NodeExt for Node {
5668
if let Some(wallet) = wallet {
5769
conf.wallet = Some(wallet);
5870
}
59-
71+
6072
Node::with_conf(exe, &conf).expect("failed to create node")
6173
}
6274

6375
fn fund_wallet(&self) {
64-
// TODO: Consider returning the error.
6576
let address = self.client.new_address().expect("failed to get new address");
6677
self.client.generate_to_address(NBLOCKS, &address).expect("failed to generate to address");
6778
}
79+
80+
fn create_mined_transaction(&self) -> (bitcoin::Address, bitcoin::Transaction) {
81+
const MILLION_SATS: bitcoin::Amount = bitcoin::Amount::from_sat(1000000);
82+
83+
let address = self.client.new_address().expect("failed to get new address");
84+
85+
let _ = self.client.send_to_address(&address, MILLION_SATS);
86+
self.mine_a_block();
87+
88+
let best_block_hash = self.client.best_block_hash().expect("best_block_hash");
89+
let best_block = self.client.get_block(best_block_hash).expect("best_block");
90+
let tx = best_block.txdata[1].clone();
91+
92+
(address, tx)
93+
}
94+
95+
fn mine_a_block(&self) {
96+
// TODO: Consider returning the error.
97+
let address = self.client.new_address().expect("failed to get new address");
98+
self.client.generate_to_address(1, &address).expect("failed to generate to address");
99+
}
68100
}
69101

70102
/// Return a temporary file path.

integration_test/tests/raw_transactions.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
//! Tests for methods found under the `== Rawtransactions ==` section of the API docs.
44
5-
#[cfg(feature = "TODO")]
65
use integration_test::{Node, NodeExt as _};
76

87
#[test]
@@ -11,3 +10,30 @@ fn send_raw_transaction() {
1110
let _node = Node::new_no_wallet();
1211
todo!()
1312
}
13+
14+
#[test]
15+
#[cfg(feature = "v28")]
16+
fn submitpackage() {
17+
let node = Node::new_with_default_wallet();
18+
19+
// Submitting the empty package should simply fail.
20+
assert!(node.client.submit_package(&[], None, None).is_err());
21+
22+
node.fund_wallet();
23+
24+
let (_, tx_0) = node.create_mined_transaction();
25+
let (_, tx_1) = node.create_mined_transaction();
26+
27+
// The call for submitting this package should succeed, but yield an 'already known'
28+
// error for all transactions.
29+
let res = node
30+
.client
31+
.submit_package(&[tx_0, tx_1], None, None)
32+
.expect("failed to submit package")
33+
.into_model()
34+
.expect("failed to submit package");
35+
for (_, tx_result) in &res.tx_results {
36+
assert!(tx_result.error.is_some());
37+
}
38+
assert!(res.replaced_transactions.is_empty());
39+
}

types/src/model/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ pub use self::{
3232
},
3333
generating::{Generate, GenerateToAddress},
3434
network::{GetNetworkInfo, GetNetworkInfoAddress, GetNetworkInfoNetwork},
35-
raw_transactions::SendRawTransaction,
35+
raw_transactions::{
36+
SendRawTransaction, SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees,
37+
},
3638
wallet::{
3739
AddMultisigAddress, AddressInformation, AddressLabel, AddressPurpose, Bip125Replaceable,
3840
BumpFee, CreateWallet, DumpPrivKey, DumpWallet, GetAddressInfo, GetAddressInfoEmbedded,

types/src/model/raw_transactions.rs

+46-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,54 @@
55
//! These structs model the types returned by the JSON-RPC API but have concrete types
66
//! and are not specific to a specific version of Bitcoin Core.
77
8-
use bitcoin::Txid;
8+
use std::collections::BTreeMap;
9+
10+
use bitcoin::{Amount, FeeRate, Txid, Wtxid};
911
use serde::{Deserialize, Serialize};
1012

1113
/// Models the result of JSON-RPC method `sendrawtransaction`.
1214
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
1315
pub struct SendRawTransaction(pub Txid);
16+
17+
/// Models the result of JSON-RPC method `submitpackage`.
18+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
19+
pub struct SubmitPackage {
20+
/// The transaction package result message. "success" indicates all transactions were accepted into or are already in the mempool.
21+
pub package_msg: String,
22+
/// Transaction results keyed by [`Wtxid`].
23+
pub tx_results: BTreeMap<Wtxid, SubmitPackageTxResult>,
24+
/// List of txids of replaced transactions.
25+
pub replaced_transactions: Vec<Txid>,
26+
}
27+
28+
/// Models the per-transaction result included in the JSON-RPC method `submitpackage`.
29+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
30+
pub struct SubmitPackageTxResult {
31+
/// The transaction id.
32+
pub txid: Txid,
33+
/// The [`Wtxid`] of a different transaction with the same [`Txid`] but different witness found in the mempool.
34+
///
35+
/// If set, this means the submitted transaction was ignored.
36+
pub other_wtxid: Option<Wtxid>,
37+
/// Sigops-adjusted virtual transaction size.
38+
pub vsize: Option<u32>,
39+
/// Transaction fees.
40+
pub fees: Option<SubmitPackageTxResultFees>,
41+
/// The transaction error string, if it was rejected by the mempool
42+
pub error: Option<String>,
43+
}
44+
45+
/// Models the fees included in the per-transaction result of the JSON-RPC method `submitpackage`.
46+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
47+
pub struct SubmitPackageTxResultFees {
48+
/// Transaction fee.
49+
pub base_fee: Amount,
50+
/// The effective feerate.
51+
///
52+
/// Will be `None` if the transaction was already in the mempool. For example, the package
53+
/// feerate and/or feerate with modified fees from the `prioritisetransaction` JSON-RPC method.
54+
pub effective_fee_rate: Option<FeeRate>,
55+
/// If [`Self::effective_fee_rate`] is provided, this holds the [`Wtxid`]s of the transactions
56+
/// whose fees and vsizes are included in effective-feerate.
57+
pub effective_includes: Vec<Wtxid>,
58+
}

types/src/v28/mod.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
//! - [ ] `joinpsbts ["psbt",...]`
9191
//! - [ ] `sendrawtransaction "hexstring" ( maxfeerate maxburnamount )`
9292
//! - [ ] `signrawtransactionwithkey "hexstring" ["privatekey",...] ( [{"txid":"hex","vout":n,"scriptPubKey":"hex","redeemScript":"hex","witnessScript":"hex","amount":amount},...] "sighashtype" )`
93-
//! - [ ] `submitpackage ["rawtx",...] ( maxfeerate maxburnamount )`
93+
//! - [x] `submitpackage ["rawtx",...] ( maxfeerate maxburnamount )`
9494
//! - [ ] `testmempoolaccept ["rawtx",...] ( maxfeerate )`
9595
//! - [ ] `utxoupdatepsbt "psbt" ( ["",{"desc":"str","range":n or [n,n]},...] )`
9696
//!
@@ -182,12 +182,15 @@
182182
183183
mod blockchain;
184184
mod network;
185+
mod raw_transactions;
185186

186187
#[doc(inline)]
187188
pub use self::blockchain::GetBlockchainInfo;
188189
#[doc(inline)]
189190
pub use self::network::GetNetworkInfo;
190191
#[doc(inline)]
192+
pub use self::raw_transactions::{SubmitPackage, SubmitPackageTxResult, SubmitPackageTxResultFees};
193+
#[doc(inline)]
191194
pub use crate::{
192195
v17::{
193196
GenerateToAddress, GetBalance, GetBestBlockHash, GetBlockCount, GetBlockVerbosityOne,
+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
use core::fmt;
4+
5+
use bitcoin::amount::ParseAmountError;
6+
use bitcoin::hex::HexToArrayError;
7+
use internals::write_err;
8+
9+
use crate::NumericError;
10+
11+
/// Error when converting a `SubmitPackage` type into the model type.
12+
#[derive(Debug)]
13+
pub enum SubmitPackageError {
14+
/// Conversion of key from `tx_results` map failed.
15+
TxResultKey(HexToArrayError),
16+
/// Conversion of value from `tx_results` map failed.
17+
TxResultValue(SubmitPackageTxResultError),
18+
/// Conversion of a list item from `replaced_transactions` field failed.
19+
ReplaceTransactions(HexToArrayError),
20+
}
21+
22+
impl fmt::Display for SubmitPackageError {
23+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
24+
use SubmitPackageError as E;
25+
26+
match *self {
27+
E::TxResultKey(ref e) =>
28+
write_err!(f, "conversion of key from `tx_results` map failed"; e),
29+
E::TxResultValue(ref e) =>
30+
write_err!(f, "conversion of value from `tx_results` map failed"; e),
31+
E::ReplaceTransactions(ref e) =>
32+
write_err!(f, "conversion of a list item from `replaced_transactions` field failed"; e),
33+
}
34+
}
35+
}
36+
37+
impl std::error::Error for SubmitPackageError {
38+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
39+
use SubmitPackageError as E;
40+
41+
match *self {
42+
E::TxResultKey(ref e) => Some(e),
43+
E::TxResultValue(ref e) => Some(e),
44+
E::ReplaceTransactions(ref e) => Some(e),
45+
}
46+
}
47+
}
48+
49+
/// Error when converting a `SubmitPackageTxResult` type into the model type.
50+
#[derive(Debug)]
51+
pub enum SubmitPackageTxResultError {
52+
/// Conversion of numeric type to expected type failed.
53+
Numeric(NumericError),
54+
/// Conversion of the `txid` field failed.
55+
Txid(HexToArrayError),
56+
/// Conversion of the `other_wtxid` field failed.
57+
OtherWtxid(HexToArrayError),
58+
/// Conversion of the `fees` field failed.
59+
Fees(SubmitPackageTxResultFeesError),
60+
}
61+
62+
impl fmt::Display for SubmitPackageTxResultError {
63+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64+
use SubmitPackageTxResultError as E;
65+
66+
match *self {
67+
E::Numeric(ref e) => write_err!(f, "numeric"; e),
68+
E::Txid(ref e) => write_err!(f, "conversion of the `txid` field failed"; e),
69+
E::OtherWtxid(ref e) =>
70+
write_err!(f, "conversion of the `other_wtxid` field failed"; e),
71+
E::Fees(ref e) => write_err!(f, "conversion of the `fees` field failed"; e),
72+
}
73+
}
74+
}
75+
76+
impl std::error::Error for SubmitPackageTxResultError {
77+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78+
use SubmitPackageTxResultError as E;
79+
80+
match *self {
81+
E::Numeric(ref e) => Some(e),
82+
E::Txid(ref e) => Some(e),
83+
E::OtherWtxid(ref e) => Some(e),
84+
E::Fees(ref e) => Some(e),
85+
}
86+
}
87+
}
88+
89+
impl From<NumericError> for SubmitPackageTxResultError {
90+
fn from(e: NumericError) -> Self { Self::Numeric(e) }
91+
}
92+
93+
/// Error when converting a `SubmitPackageTxResultFees` type into the model type.
94+
#[derive(Debug)]
95+
pub enum SubmitPackageTxResultFeesError {
96+
/// Conversion of the `base_fee` field failed.
97+
BaseFee(ParseAmountError),
98+
/// Conversion of the `effective_fee_rate` field failed.
99+
EffectiveFeeRate(ParseAmountError),
100+
/// Conversion of a list item from `effective_includes` field failed.
101+
EffectiveIncludes(HexToArrayError),
102+
}
103+
104+
impl fmt::Display for SubmitPackageTxResultFeesError {
105+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106+
use SubmitPackageTxResultFeesError as E;
107+
108+
match *self {
109+
E::BaseFee(ref e) => write_err!(f, "conversion of the `base_fee` field failed"; e),
110+
E::EffectiveFeeRate(ref e) =>
111+
write_err!(f, "conversion of the `effective_fee_rate` field failed"; e),
112+
E::EffectiveIncludes(ref e) => write_err!(f, "effective_includes"; e),
113+
}
114+
}
115+
}
116+
117+
impl std::error::Error for SubmitPackageTxResultFeesError {
118+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
119+
use SubmitPackageTxResultFeesError as E;
120+
121+
match *self {
122+
E::BaseFee(ref e) => Some(e),
123+
E::EffectiveFeeRate(ref e) => Some(e),
124+
E::EffectiveIncludes(ref e) => Some(e),
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)