use bitcoin::{util::psbt::PartiallySignedTransaction as Psbt, AddressType, Script, TxOut};
mod error;
use error::InternalRequestError;
pub use error::RequestError;
pub trait Headers {
fn get_header(&self, key: &str) -> Option<&str>;
}
pub struct UncheckedProposal {
psbt: Psbt,
}
pub struct MaybeInputsOwned {
psbt: Psbt,
}
pub struct MaybeMixedInputScripts {
psbt: Psbt,
}
pub struct MaybeInputsSeen {
psbt: Psbt,
}
impl UncheckedProposal {
pub fn from_request(
body: impl std::io::Read,
query: &str,
headers: impl Headers,
) -> Result<Self, RequestError> {
use crate::bitcoin::consensus::Decodable;
let content_type = headers.get_header("content-type").ok_or(InternalRequestError::MissingHeader("Content-Type"))?;
if !content_type.starts_with("text/plain") {
return Err(InternalRequestError::InvalidContentType(content_type.to_owned()).into());
}
let content_length = headers
.get_header("content-length")
.ok_or(InternalRequestError::MissingHeader("Content-Length"))?
.parse::<u64>()
.map_err(InternalRequestError::InvalidContentLength)?;
if content_length > 4_000_000 * 4 / 3 {
return Err(InternalRequestError::ContentLengthTooLarge(content_length).into());
}
let mut limited = body.take(content_length);
let mut reader = base64::read::DecoderReader::new(&mut limited, base64::STANDARD);
let psbt = Psbt::consensus_decode(&mut reader).map_err(InternalRequestError::Decode)?;
Ok(UncheckedProposal {
psbt,
})
}
pub fn get_transaction_to_check_broadcast(&self) -> bitcoin::Transaction {
self.psbt.clone().extract_tx()
}
pub fn assume_tested_and_scheduled_broadcast(self) -> MaybeInputsOwned {
MaybeInputsOwned { psbt: self.psbt }
}
pub fn assume_interactive_receive_endpoint(self) -> MaybeInputsOwned {
MaybeInputsOwned { psbt: self.psbt }
}
}
impl MaybeInputsOwned {
pub fn iter_input_script_pubkeys(&self) -> Vec<Result<&Script, RequestError>> {
todo!() }
pub fn assume_inputs_not_owned(self) -> MaybeMixedInputScripts {
MaybeMixedInputScripts { psbt: self.psbt }
}
}
impl MaybeMixedInputScripts {
pub fn iter_input_script_types(&self) -> Vec<Result<&AddressType, RequestError>> {
todo!() }
pub fn assume_no_mixed_input_scripts(self) -> MaybeInputsSeen {
MaybeInputsSeen { psbt: self.psbt }
}
}
impl MaybeInputsSeen {
pub fn iter_input_outpoints(&self) -> impl '_ + Iterator<Item=&bitcoin::OutPoint> {
self.psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
}
pub fn assume_no_inputs_seen_before(self) -> UnlockedProposal {
UnlockedProposal { psbt: self.psbt }
}
}
pub struct UnlockedProposal {
psbt: Psbt,
}
impl UnlockedProposal {
pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item=&bitcoin::OutPoint> {
self.psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
}
pub fn assume_locked(self) -> Proposal {
Proposal {
psbt: self.psbt,
}
}
}
#[must_use = "The transaction must be broadcasted to prevent abuse"]
pub struct MustBroadcast(pub bitcoin::Transaction);
pub struct Proposal {
psbt: Psbt,
}
pub struct ReceiverOptions {
dust_limit: bitcoin::Amount,
}
pub enum BumpFeePolicy {
FailOnInsufficient,
SubtractOurFeeOutput,
}
pub struct NewOutputOptions {
set_as_fee_output: bool,
subtract_fees_from_this: bool,
}
#[cfg(test)]
mod test {
use super::*;
struct MockHeaders {
length: String,
}
impl MockHeaders {
#[cfg(test)]
fn new(length: u64) -> MockHeaders {
MockHeaders { length: length.to_string() }
}
}
impl Headers for MockHeaders {
fn get_header(&self, key: &str) -> Option<&str> {
match key {
"content-length" => Some(&self.length),
"content-type" => Some("text/plain"),
_ => None,
}
}
}
fn get_proposal_from_test_vector() -> Result<UncheckedProposal, RequestError> {
let original_psbt = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
let body = original_psbt.as_bytes();
let headers = MockHeaders::new(body.len() as u64);
UncheckedProposal::from_request(body, "", headers)
}
#[test]
fn can_get_proposal_from_request() {
let proposal = get_proposal_from_test_vector();
assert!(proposal.is_ok(), "OriginalPSBT should be a valid request");
}
#[test]
fn unchecked_proposal_unlocks_after_checks() {
let proposal = get_proposal_from_test_vector().unwrap();
let unlocked = proposal
.assume_tested_and_scheduled_broadcast()
.assume_inputs_not_owned()
.assume_no_mixed_input_scripts()
.assume_no_inputs_seen_before();
}
}