Skip to content

Commit a9a39b1

Browse files
committed
Add /txs/package endpoint to submit tx packages
1 parent f87e908 commit a9a39b1

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

Diff for: src/daemon.rs

+47
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,34 @@ struct NetworkInfo {
137137
relayfee: f64, // in BTC/kB
138138
}
139139

140+
#[derive(Serialize, Deserialize, Debug)]
141+
struct MempoolFeesSubmitPackage {
142+
base: f64,
143+
#[serde(rename = "effective-feerate")]
144+
effective_feerate: Option<f64>,
145+
#[serde(rename = "effective-includes")]
146+
effective_includes: Option<Vec<String>>,
147+
}
148+
149+
#[derive(Serialize, Deserialize, Debug)]
150+
pub struct SubmitPackageResult {
151+
package_msg: String,
152+
#[serde(rename = "tx-results")]
153+
tx_results: HashMap<String, TxResult>,
154+
#[serde(rename = "replaced-transactions")]
155+
replaced_transactions: Option<Vec<String>>,
156+
}
157+
158+
#[derive(Serialize, Deserialize, Debug)]
159+
pub struct TxResult {
160+
txid: String,
161+
#[serde(rename = "other-wtxid")]
162+
other_wtxid: Option<String>,
163+
vsize: Option<u32>,
164+
fees: Option<MempoolFeesSubmitPackage>,
165+
error: Option<String>,
166+
}
167+
140168
pub trait CookieGetter: Send + Sync {
141169
fn get(&self) -> Result<Vec<u8>>;
142170
}
@@ -640,6 +668,25 @@ impl Daemon {
640668
)
641669
}
642670

671+
pub fn submit_package(
672+
&self,
673+
txhex: Vec<String>,
674+
maxfeerate: Option<f64>,
675+
maxburnamount: Option<f64>,
676+
) -> Result<SubmitPackageResult> {
677+
let params = match (maxfeerate, maxburnamount) {
678+
(Some(rate), Some(burn)) => {
679+
json!([txhex, format!("{:.8}", rate), format!("{:.8}", burn)])
680+
}
681+
(Some(rate), None) => json!([txhex, format!("{:.8}", rate)]),
682+
(None, Some(burn)) => json!([txhex, null, format!("{:.8}", burn)]),
683+
(None, None) => json!([txhex]),
684+
};
685+
let result = self.request("submitpackage", params)?;
686+
serde_json::from_value::<SubmitPackageResult>(result)
687+
.chain_err(|| "invalid submitpackage reply")
688+
}
689+
643690
// Get estimated feerates for the provided confirmation targets using a batch RPC request
644691
// Missing estimates are logged but do not cause a failure, whatever is available is returned
645692
#[allow(clippy::float_cmp)]

Diff for: src/new_index/query.rs

+10-1
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};
@@ -79,6 +79,15 @@ impl Query {
7979
Ok(txid)
8080
}
8181

82+
pub fn submit_package(
83+
&self,
84+
txhex: Vec<String>,
85+
maxfeerate: Option<f64>,
86+
maxburnamount: Option<f64>,
87+
) -> Result<SubmitPackageResult> {
88+
self.daemon.submit_package(txhex, maxfeerate, maxburnamount)
89+
}
90+
8291
pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
8392
let mut utxos = self.chain.utxo(scripthash, self.config.utxos_limit)?;
8493
let mempool = self.mempool();

Diff for: src/rest.rs

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

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

0 commit comments

Comments
 (0)