Skip to content

Commit 9a4175d

Browse files
committed
Add /txs/package endpoint to submit tx packages
1 parent 84cd0ed commit 9a4175d

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

src/daemon.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,34 @@ struct NetworkInfo {
141141
relayfee: f64, // in BTC/kB
142142
}
143143

144+
#[derive(Serialize, Deserialize, Debug)]
145+
struct MempoolFeesSubmitPackage {
146+
base: f64,
147+
#[serde(rename = "effective-feerate")]
148+
effective_feerate: Option<f64>,
149+
#[serde(rename = "effective-includes")]
150+
effective_includes: Option<Vec<String>>,
151+
}
152+
153+
#[derive(Serialize, Deserialize, Debug)]
154+
pub struct SubmitPackageResult {
155+
package_msg: String,
156+
#[serde(rename = "tx-results")]
157+
tx_results: HashMap<String, TxResult>,
158+
#[serde(rename = "replaced-transactions")]
159+
replaced_transactions: Option<Vec<String>>,
160+
}
161+
162+
#[derive(Serialize, Deserialize, Debug)]
163+
pub struct TxResult {
164+
txid: String,
165+
#[serde(rename = "other-wtxid")]
166+
other_wtxid: Option<String>,
167+
vsize: Option<u32>,
168+
fees: Option<MempoolFeesSubmitPackage>,
169+
error: Option<String>,
170+
}
171+
144172
pub trait CookieGetter: Send + Sync {
145173
fn get(&self) -> Result<Vec<u8>>;
146174
}
@@ -671,6 +699,25 @@ impl Daemon {
671699
)
672700
}
673701

702+
pub fn submit_package(
703+
&self,
704+
txhex: Vec<String>,
705+
maxfeerate: Option<f64>,
706+
maxburnamount: Option<f64>,
707+
) -> Result<SubmitPackageResult> {
708+
let params = match (maxfeerate, maxburnamount) {
709+
(Some(rate), Some(burn)) => {
710+
json!([txhex, format!("{:.8}", rate), format!("{:.8}", burn)])
711+
}
712+
(Some(rate), None) => json!([txhex, format!("{:.8}", rate)]),
713+
(None, Some(burn)) => json!([txhex, null, format!("{:.8}", burn)]),
714+
(None, None) => json!([txhex]),
715+
};
716+
let result = self.request("submitpackage", params)?;
717+
serde_json::from_value::<SubmitPackageResult>(result)
718+
.chain_err(|| "invalid submitpackage reply")
719+
}
720+
674721
// Get estimated feerates for the provided confirmation targets using a batch RPC request
675722
// Missing estimates are logged but do not cause a failure, whatever is available is returned
676723
#[allow(clippy::float_cmp)]

src/new_index/query.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
66

77
use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid};
88
use crate::config::Config;
9-
use crate::daemon::Daemon;
9+
use crate::daemon::{Daemon, SubmitPackageResult};
1010
use crate::errors::*;
1111
use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo};
1212
use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
@@ -82,6 +82,16 @@ impl Query {
8282
Ok(txid)
8383
}
8484

85+
#[trace]
86+
pub fn submit_package(
87+
&self,
88+
txhex: Vec<String>,
89+
maxfeerate: Option<f64>,
90+
maxburnamount: Option<f64>,
91+
) -> Result<SubmitPackageResult> {
92+
self.daemon.submit_package(txhex, maxfeerate, maxburnamount)
93+
}
94+
8595
#[trace]
8696
pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
8797
let mut utxos = self.chain.utxo(scripthash, self.config.utxos_limit)?;

src/rest.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::util::{
1515
use bitcoin::consensus::encode;
1616

1717
use bitcoin::hashes::FromSliceError as HashError;
18-
use bitcoin::hex::{self, DisplayHex, FromHex};
18+
use bitcoin::hex::{self, DisplayHex, FromHex, HexToBytesIter};
1919
use hyper::service::{make_service_fn, service_fn};
2020
use hyper::{Body, Method, Response, Server, StatusCode};
2121
use hyperlocal::UnixServerExt;
@@ -1005,6 +1005,63 @@ fn handle_request(
10051005
let txid = query.broadcast_raw(&txhex)?;
10061006
http_message(StatusCode::OK, txid.to_string(), 0)
10071007
}
1008+
(&Method::POST, Some(&"txs"), Some(&"package"), None, None, None) => {
1009+
let txhexes: Vec<String> =
1010+
serde_json::from_str(String::from_utf8(body.to_vec())?.as_str())?;
1011+
1012+
if txhexes.len() > 25 {
1013+
Result::Err(HttpError::from(
1014+
"Exceeded maximum of 25 transactions".to_string(),
1015+
))?
1016+
}
1017+
1018+
let maxfeerate = query_params
1019+
.get("maxfeerate")
1020+
.map(|s| {
1021+
s.parse::<f64>()
1022+
.map_err(|_| HttpError::from("Invalid maxfeerate".to_string()))
1023+
})
1024+
.transpose()?;
1025+
1026+
let maxburnamount = query_params
1027+
.get("maxburnamount")
1028+
.map(|s| {
1029+
s.parse::<f64>()
1030+
.map_err(|_| HttpError::from("Invalid maxburnamount".to_string()))
1031+
})
1032+
.transpose()?;
1033+
1034+
// pre-checks
1035+
txhexes.iter().enumerate().try_for_each(|(index, txhex)| {
1036+
// each transaction must be of reasonable size
1037+
// (more than 60 bytes, within 400kWU standardness limit)
1038+
if !(120..800_000).contains(&txhex.len()) {
1039+
Result::Err(HttpError::from(format!(
1040+
"Invalid transaction size for item {}",
1041+
index
1042+
)))
1043+
} else {
1044+
// must be a valid hex string
1045+
HexToBytesIter::new(txhex)
1046+
.map_err(|_| {
1047+
HttpError::from(format!("Invalid transaction hex for item {}", index))
1048+
})?
1049+
.filter(|r| r.is_err())
1050+
.next()
1051+
.transpose()
1052+
.map_err(|_| {
1053+
HttpError::from(format!("Invalid transaction hex for item {}", index))
1054+
})
1055+
.map(|_| ())
1056+
}
1057+
})?;
1058+
1059+
let result = query
1060+
.submit_package(txhexes, maxfeerate, maxburnamount)
1061+
.map_err(|err| HttpError::from(err.description().to_string()))?;
1062+
1063+
json_response(result, TTL_SHORT)
1064+
}
10081065

10091066
(&Method::GET, Some(&"mempool"), None, None, None, None) => {
10101067
json_response(query.mempool().backlog_stats(), TTL_SHORT)

0 commit comments

Comments
 (0)