Skip to content

Commit

Permalink
Merge pull request #86 from DanGould/rec-fee
Browse files Browse the repository at this point in the history
Support spec recommended additional fee API
  • Loading branch information
DanGould authored Aug 8, 2023
2 parents 6435b11 + 743e208 commit 9588dc2
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 16 deletions.
28 changes: 19 additions & 9 deletions payjoin-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ impl App {
}

pub fn send_payjoin(&self, bip21: &str) -> Result<()> {
use payjoin::send::Configuration;

let link = payjoin::Uri::try_from(bip21)
.map_err(|e| anyhow!("Failed to create URI from BIP21: {}", e))?;

Expand All @@ -50,16 +52,22 @@ impl App {
.check_pj_supported()
.map_err(|e| anyhow!("The provided URI doesn't support payjoin (BIP78): {}", e))?;

let amount = link
.amount
.ok_or_else(|| anyhow!("please specify the amount in the Uri"))
.map(|amt| Amount::from_sat(amt.to_sat()))?;
let amount = link.amount.ok_or_else(|| anyhow!("please specify the amount in the Uri"))?;

// wallet_create_funded_psbt requires a HashMap<address: String, Amount>
let mut outputs = HashMap::with_capacity(1);
outputs.insert(link.address.to_string(), amount);

// TODO: make payjoin-cli send feerate configurable
// 2.1 sat/vB == 525 sat/kwu for testing purposes.
let fee_rate = bitcoin::FeeRate::from_sat_per_kwu(525);
let fee_sat_per_kvb =
fee_rate.to_sat_per_kwu().checked_mul(4).ok_or(anyhow!("Invalid fee rate"))?;
let fee_per_kvb = Amount::from_sat(fee_sat_per_kvb);
log::debug!("Fee rate sat/kvb: {}", fee_per_kvb.display_in(bitcoin::Denomination::Satoshi));
let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions {
lock_unspent: Some(true),
fee_rate: Some(Amount::from_sat(2000)),
fee_rate: Some(fee_per_kvb),
..Default::default()
};
let psbt = self
Expand All @@ -80,10 +88,12 @@ impl App {
.psbt;
let psbt = Psbt::from_str(&psbt).with_context(|| "Failed to load PSBT from base64")?;
log::debug!("Original psbt: {:#?}", psbt);
let pj_params = payjoin::send::Configuration::with_fee_contribution(
payjoin::bitcoin::Amount::from_sat(10000),
None,
);

let payout_scripts = std::iter::once(link.address.script_pubkey());
// recommendation or bust for this simple reference implementation
let pj_params = Configuration::recommended(&psbt, payout_scripts, fee_rate)
.unwrap_or_else(|_| Configuration::non_incentivizing());

let (req, ctx) = link
.create_pj_request(psbt, pj_params)
.with_context(|| "Failed to create payjoin request")?;
Expand Down
10 changes: 7 additions & 3 deletions payjoin/src/receive/optional_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ impl Params {
}
},
("minfeerate", feerate) =>
params.min_feerate = match feerate.parse::<u64>() {
Ok(rate) => FeeRate::from_sat_per_vb(rate)
.ok_or_else(|| Error::FeeRate(rate.to_string()))?,
params.min_feerate = match feerate.parse::<f32>() {
Ok(fee_rate_sat_per_vb) => {
// TODO Parse with serde when rust-bitcoin supports it
let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
// since it's a minnimum, we want to round up
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64)
}
Err(e) => return Err(Error::FeeRate(e.to_string())),
},
("disableoutputsubstitution", v) =>
Expand Down
14 changes: 14 additions & 0 deletions payjoin/src/send/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ impl std::error::Error for ValidationError {
}
}

#[derive(Debug)]
pub struct ConfigurationError(InternalConfigurationError);

#[derive(Debug)]
pub(crate) enum InternalConfigurationError {
PrevTxOut(crate::psbt::PrevTxOutError),
InputType(crate::input_type::InputTypeError),
NoInputs,
}

impl From<InternalConfigurationError> for ConfigurationError {
fn from(value: InternalConfigurationError) -> Self { ConfigurationError(value) }
}

/// Error returned when request could not be created.
///
/// This error can currently only happen due to programmer mistake.
Expand Down
69 changes: 65 additions & 4 deletions payjoin/src/send/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ pub use error::{CreateRequestError, ValidationError};
pub(crate) use error::{InternalCreateRequestError, InternalValidationError};
use url::Url;

use self::error::ConfigurationError;
use crate::input_type::InputType;
use crate::psbt::PsbtExt;
use crate::send::error::InternalConfigurationError;
use crate::weight::{varint_size, ComputeWeight};

// See usize casts
Expand All @@ -172,6 +174,64 @@ pub struct Configuration {
}

impl Configuration {
// Calculate the recommended fee contribution for an Original PSBT.
//
// BIP 78 recommends contributing `originalPSBTFeeRate * vsize(sender_input_type)`.
// The minfeerate parameter is set if the contribution is available in change.
//
// This method fails if no recommendation can be made or if the PSBT is malformed.
pub fn recommended(
psbt: &Psbt,
payout_scripts: impl IntoIterator<Item = ScriptBuf>,
min_fee_rate: FeeRate,
) -> Result<Self, ConfigurationError> {
let mut payout_scripts = payout_scripts.into_iter();
if let Some((additional_fee_index, fee_available)) = psbt
.unsigned_tx
.output
.clone()
.into_iter()
.enumerate()
.find(|(_, txo)| payout_scripts.all(|script| script != txo.script_pubkey))
.map(|(i, txo)| (i, bitcoin::Amount::from_sat(txo.value)))
{
let input_types = psbt
.input_pairs()
.map(|input| {
let txo =
input.previous_txout().map_err(InternalConfigurationError::PrevTxOut)?;
Ok(InputType::from_spent_input(txo, input.psbtin)
.map_err(InternalConfigurationError::InputType)?)
})
.collect::<Result<Vec<InputType>, ConfigurationError>>()?;

let first_type = input_types.first().ok_or(InternalConfigurationError::NoInputs)?;
// use cheapest default if mixed input types
let mut input_vsize = InputType::Taproot.expected_input_weight();
// Check if all inputs are the same type
if input_types.iter().all(|input_type| input_type == first_type) {
input_vsize = first_type.expected_input_weight();
}

let recommended_additional_fee = min_fee_rate * input_vsize;
if fee_available < recommended_additional_fee {
log::warn!("Insufficient funds to maintain specified minimum feerate.");
return Ok(Configuration::with_fee_contribution(
fee_available,
Some(additional_fee_index),
)
.clamp_fee_contribution(true));
}
return Ok(Configuration::with_fee_contribution(
recommended_additional_fee,
Some(additional_fee_index),
)
.clamp_fee_contribution(false)
.min_fee_rate(min_fee_rate));
}
Ok(Configuration::non_incentivizing())
}

/// Offer the receiver contribution to pay for his input.
///
/// These parameters will allow the receiver to take `max_fee_contribution` from given change
Expand Down Expand Up @@ -226,8 +286,8 @@ impl Configuration {
}

/// Sets minimum fee rate required by the sender.
pub fn min_fee_rate_sat_per_vb(mut self, fee_rate: u64) -> Self {
self.min_fee_rate = FeeRate::from_sat_per_vb_unchecked(fee_rate);
pub fn min_fee_rate(mut self, fee_rate: FeeRate) -> Self {
self.min_fee_rate = fee_rate;
self
}
}
Expand Down Expand Up @@ -667,8 +727,9 @@ fn serialize_url(
.append_pair("maxadditionalfeecontribution", &amount.to_sat().to_string());
}
if min_fee_rate > FeeRate::ZERO {
url.query_pairs_mut()
.append_pair("minfeerate", &min_fee_rate.to_sat_per_vb_floor().to_string());
// TODO serialize in rust-bitcoin <https://github.com/rust-bitcoin/rust-bitcoin/pull/1787/files#diff-c2ea40075e93ccd068673873166cfa3312ec7439d6bc5a4cbc03e972c7e045c4>
let float_fee_rate = min_fee_rate.to_sat_per_kwu() as f32 / 250.0_f32;
url.query_pairs_mut().append_pair("minfeerate", &float_fee_rate.to_string());
}
Ok(url)
}
Expand Down

0 comments on commit 9588dc2

Please sign in to comment.