Skip to content

Commit

Permalink
Add submitpackage endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
natsoni committed Oct 15, 2024
1 parent 055aba1 commit 9f87c8f
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 1 deletion.
47 changes: 47 additions & 0 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,34 @@ pub struct MempoolAcceptResult {
reject_reason: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct MempoolFeesSubmitPackage {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: Option<f64>,
#[serde(rename = "effective-includes")]
effective_includes: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitPackageResult {
package_msg: String,
#[serde(rename = "tx-results")]
tx_results: HashMap<String, TxResult>,
#[serde(rename = "replaced-transactions")]
replaced_transactions: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct TxResult {
txid: String,
#[serde(rename = "other-wtxid")]
other_wtxid: Option<String>,
vsize: Option<u32>,
fees: Option<MempoolFeesSubmitPackage>,
error: Option<String>,
}

pub trait CookieGetter: Send + Sync {
fn get(&self) -> Result<Vec<u8>>;
}
Expand Down Expand Up @@ -621,6 +649,25 @@ impl Daemon {
.chain_err(|| "invalid testmempoolaccept reply")
}

pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
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::<SubmitPackageResult>(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)]
Expand Down
11 changes: 10 additions & 1 deletion src/new_index/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, MempoolAcceptResult};
use crate::daemon::{Daemon, MempoolAcceptResult, SubmitPackageResult};
use crate::errors::*;
use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo};
use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
Expand Down Expand Up @@ -95,6 +95,15 @@ impl Query {
self.daemon.test_mempool_accept(txhex, maxfeerate)
}

pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
self.daemon.submit_package(txhex, maxfeerate, maxburnamount)
}

pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
let mut utxos = self.chain.utxo(
scripthash,
Expand Down
50 changes: 50 additions & 0 deletions src/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,56 @@ fn handle_request(

json_response(result, TTL_SHORT)
}
(&Method::POST, Some(&"txs"), Some(&"package"), None, None, None) => {
let txhexes: Vec<String> =
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::<f64>()
.map_err(|_| HttpError::from("Invalid maxfeerate".to_string()))
})
.transpose()?;

let maxburnamount = query_params
.get("maxburnamount")
.map(|s| {
s.parse::<f64>()
.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
Vec::<u8>::from_hex(txhex)
.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(&"txs"), Some(&"outspends"), None, None, None) => {
let txid_strings: Vec<&str> = query_params
.get("txids")
Expand Down

0 comments on commit 9f87c8f

Please sign in to comment.