diff --git a/src/daemon.rs b/src/daemon.rs index b794a1f9..254c168e 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -117,6 +117,26 @@ struct NetworkInfo { relayfee: f64, // in BTC/kB } +#[derive(Serialize, Deserialize, Debug)] +struct MempoolFees { + base: f64, + #[serde(rename = "effective-feerate")] + effective_feerate: f64, + #[serde(rename = "effective-includes")] + effective_includes: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MempoolAcceptResult { + txid: String, + wtxid: String, + allowed: Option, + vsize: Option, + fees: Option, + #[serde(rename = "reject-reason")] + reject_reason: Option, +} + pub trait CookieGetter: Send + Sync { fn get(&self) -> Result>; } @@ -582,6 +602,20 @@ impl Daemon { .chain_err(|| "failed to parse txid") } + pub fn test_mempool_accept( + &self, + txhex: Vec, + maxfeerate: Option, + ) -> Result> { + let params = match maxfeerate { + Some(rate) => json!([txhex, format!("{:.8}", rate)]), + None => json!([txhex]), + }; + let result = self.request("testmempoolaccept", params)?; + serde_json::from_value::>(result) + .chain_err(|| "invalid testmempoolaccept 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 3003a256..3e314fd1 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, MempoolAcceptResult}; use crate::errors::*; use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo}; use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus}; @@ -87,6 +87,14 @@ impl Query { Ok(txid) } + pub fn test_mempool_accept( + &self, + txhex: Vec, + maxfeerate: Option, + ) -> Result> { + self.daemon.test_mempool_accept(txhex, maxfeerate) + } + pub fn utxo(&self, scripthash: &[u8]) -> Result> { let mut utxos = self.chain.utxo( scripthash, diff --git a/src/rest.rs b/src/rest.rs index df085441..eab06de5 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1202,6 +1202,48 @@ fn handle_request( .map_err(|err| HttpError::from(err.description().to_string()))?; http_message(StatusCode::OK, txid.to_hex(), 0) } + (&Method::POST, Some(&"txs"), Some(&"test"), 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()?; + + // 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 + Vec::::from_hex(txhex) + .map_err(|_| { + HttpError::from(format!("Invalid transaction hex for item {}", index)) + }) + .map(|_| ()) + } + })?; + + let result = query + .test_mempool_accept(txhexes, maxfeerate) + .map_err(|err| HttpError::from(err.description().to_string()))?; + + json_response(result, TTL_SHORT) + } (&Method::GET, Some(&"txs"), Some(&"outspends"), None, None, None) => { let txid_strings: Vec<&str> = query_params .get("txids")