From a9a39b1616591fb08514fba4df1c1ca11b24a500 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 25 Sep 2024 23:07:11 +0100 Subject: [PATCH] Add /txs/package endpoint to submit tx packages --- src/daemon.rs | 47 +++++++++++++++++++++++++++++++++ src/new_index/query.rs | 11 +++++++- src/rest.rs | 59 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 65da53d7..95f78f2e 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -137,6 +137,34 @@ struct NetworkInfo { relayfee: f64, // in BTC/kB } +#[derive(Serialize, Deserialize, Debug)] +struct MempoolFeesSubmitPackage { + base: f64, + #[serde(rename = "effective-feerate")] + effective_feerate: Option, + #[serde(rename = "effective-includes")] + effective_includes: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SubmitPackageResult { + package_msg: String, + #[serde(rename = "tx-results")] + tx_results: HashMap, + #[serde(rename = "replaced-transactions")] + replaced_transactions: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TxResult { + txid: String, + #[serde(rename = "other-wtxid")] + other_wtxid: Option, + vsize: Option, + fees: Option, + error: Option, +} + pub trait CookieGetter: Send + Sync { fn get(&self) -> Result>; } @@ -640,6 +668,25 @@ impl Daemon { ) } + pub fn submit_package( + &self, + txhex: Vec, + maxfeerate: Option, + maxburnamount: Option, + ) -> Result { + let params = match (maxfeerate, maxburnamount) { + (Some(rate), Some(burn)) => { + json!([txhex, format!("{:.8}", rate), format!("{:.8}", burn)]) + } + (Some(rate), None) => json!([txhex, format!("{:.8}", rate)]), + (None, Some(burn)) => json!([txhex, null, format!("{:.8}", burn)]), + (None, None) => json!([txhex]), + }; + let result = self.request("submitpackage", params)?; + serde_json::from_value::(result) + .chain_err(|| "invalid submitpackage reply") + } + // Get estimated feerates for the provided confirmation targets using a batch RPC request // Missing estimates are logged but do not cause a failure, whatever is available is returned #[allow(clippy::float_cmp)] diff --git a/src/new_index/query.rs b/src/new_index/query.rs index 14065857..7bc42b39 100644 --- a/src/new_index/query.rs +++ b/src/new_index/query.rs @@ -6,7 +6,7 @@ use std::time::{Duration, Instant}; use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid}; use crate::config::Config; -use crate::daemon::Daemon; +use crate::daemon::{Daemon, SubmitPackageResult}; use crate::errors::*; use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo}; use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus}; @@ -79,6 +79,15 @@ impl Query { Ok(txid) } + pub fn submit_package( + &self, + txhex: Vec, + maxfeerate: Option, + maxburnamount: Option, + ) -> Result { + self.daemon.submit_package(txhex, maxfeerate, maxburnamount) + } + pub fn utxo(&self, scripthash: &[u8]) -> Result> { let mut utxos = self.chain.utxo(scripthash, self.config.utxos_limit)?; let mempool = self.mempool(); diff --git a/src/rest.rs b/src/rest.rs index be868774..d119e561 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -15,7 +15,7 @@ use crate::util::{ use bitcoin::consensus::encode; use bitcoin::hashes::FromSliceError as HashError; -use bitcoin::hex::{self, DisplayHex, FromHex}; +use bitcoin::hex::{self, DisplayHex, FromHex, HexToBytesIter}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Response, Server, StatusCode}; use hyperlocal::UnixServerExt; @@ -1002,6 +1002,63 @@ fn handle_request( let txid = query.broadcast_raw(&txhex)?; http_message(StatusCode::OK, txid.to_string(), 0) } + (&Method::POST, Some(&"txs"), Some(&"package"), None, None, None) => { + let txhexes: Vec = + serde_json::from_str(String::from_utf8(body.to_vec())?.as_str())?; + + if txhexes.len() > 25 { + Result::Err(HttpError::from( + "Exceeded maximum of 25 transactions".to_string(), + ))? + } + + let maxfeerate = query_params + .get("maxfeerate") + .map(|s| { + s.parse::() + .map_err(|_| HttpError::from("Invalid maxfeerate".to_string())) + }) + .transpose()?; + + let maxburnamount = query_params + .get("maxburnamount") + .map(|s| { + s.parse::() + .map_err(|_| HttpError::from("Invalid maxburnamount".to_string())) + }) + .transpose()?; + + // pre-checks + txhexes.iter().enumerate().try_for_each(|(index, txhex)| { + // each transaction must be of reasonable size + // (more than 60 bytes, within 400kWU standardness limit) + if !(120..800_000).contains(&txhex.len()) { + Result::Err(HttpError::from(format!( + "Invalid transaction size for item {}", + index + ))) + } else { + // must be a valid hex string + HexToBytesIter::new(txhex) + .map_err(|_| { + HttpError::from(format!("Invalid transaction hex for item {}", index)) + })? + .filter(|r| r.is_err()) + .next() + .transpose() + .map_err(|_| { + HttpError::from(format!("Invalid transaction hex for item {}", index)) + }) + .map(|_| ()) + } + })?; + + let result = query + .submit_package(txhexes, maxfeerate, maxburnamount) + .map_err(|err| HttpError::from(err.description().to_string()))?; + + json_response(result, TTL_SHORT) + } (&Method::GET, Some(&"mempool"), None, None, None, None) => { json_response(query.mempool().backlog_stats(), TTL_SHORT)