Skip to content

Commit

Permalink
feat: add force_min_change_sats to PayoutQueueConfig (#414)
Browse files Browse the repository at this point in the history
* chore: add force_min_change_sats to PayoutQueueConfig

* feat: add min change output when configured on queue
  • Loading branch information
bodymindarts authored Dec 6, 2023
1 parent 9c16dee commit 2c96a76
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 9 deletions.
1 change: 1 addition & 0 deletions proto/api/bria.proto
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ message PayoutQueueConfig {
}
optional uint32 cpfp_payouts_after_mins = 6;
optional uint32 cpfp_payouts_after_blocks = 7;
optional uint64 force_min_change_sats = 8;
}

enum TxPriority {
Expand Down
1 change: 1 addition & 0 deletions src/api/server/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ impl From<PayoutQueue> for proto::PayoutQueue {
consolidate_deprecated_keychains: payout_queue.config.consolidate_deprecated_keychains,
cpfp_payouts_after_mins: payout_queue.config.cpfp_payouts_after_mins,
cpfp_payouts_after_blocks: payout_queue.config.cpfp_payouts_after_blocks,
force_min_change_sats: payout_queue.config.force_min_change_sats.map(u64::from),
});
proto::PayoutQueue {
id,
Expand Down
4 changes: 4 additions & 0 deletions src/cli/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ impl ApiClient {
manual_trigger: Option<bool>,
cpfp_payouts_after_mins: Option<u32>,
cpfp_payouts_after_blocks: Option<u32>,
force_min_change_sats: Option<u64>,
) -> anyhow::Result<()> {
let tx_priority = match tx_priority {
TxPriority::NextBlock => proto::TxPriority::NextBlock as i32,
Expand All @@ -331,6 +332,7 @@ impl ApiClient {
trigger,
cpfp_payouts_after_mins,
cpfp_payouts_after_blocks,
force_min_change_sats,
};

let request = tonic::Request::new(proto::CreatePayoutQueueRequest {
Expand Down Expand Up @@ -525,6 +527,7 @@ impl ApiClient {
interval_trigger: Option<u32>,
cpfp_payouts_after_mins: Option<u32>,
cpfp_payouts_after_blocks: Option<u32>,
force_min_change_sats: Option<u64>,
) -> anyhow::Result<()> {
let tx_priority = tx_priority.map(|priority| match priority {
TxPriority::NextBlock => proto::TxPriority::NextBlock as i32,
Expand All @@ -543,6 +546,7 @@ impl ApiClient {
trigger,
cpfp_payouts_after_mins,
cpfp_payouts_after_blocks,
force_min_change_sats,
})
} else {
None
Expand Down
8 changes: 8 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ enum Command {
cpfp_payouts_after_mins: Option<u32>,
#[clap(long = "cpfp-after-blocks")]
cpfp_payouts_after_blocks: Option<u32>,
#[clap(long)]
min_change: Option<u64>,
},
/// Trigger Payout Queue
TriggerPayoutQueue {
Expand Down Expand Up @@ -500,6 +502,8 @@ enum Command {
cpfp_payouts_after_mins: Option<u32>,
#[clap(long = "cpfp-after-blocks")]
cpfp_payouts_after_blocks: Option<u32>,
#[clap(long)]
min_change: Option<u64>,
},
/// Get Batch details
GetBatch {
Expand Down Expand Up @@ -851,6 +855,7 @@ pub async fn run() -> anyhow::Result<()> {
manual_trigger,
cpfp_payouts_after_mins,
cpfp_payouts_after_blocks,
min_change,
} => {
let client = api_client(cli.bria_home, url, api_key);
client
Expand All @@ -863,6 +868,7 @@ pub async fn run() -> anyhow::Result<()> {
manual_trigger,
cpfp_payouts_after_mins,
cpfp_payouts_after_blocks,
min_change,
)
.await?;
}
Expand Down Expand Up @@ -944,6 +950,7 @@ pub async fn run() -> anyhow::Result<()> {
interval_trigger,
cpfp_payouts_after_mins,
cpfp_payouts_after_blocks,
min_change,
} => {
let client = api_client(cli.bria_home, url, api_key);
client
Expand All @@ -955,6 +962,7 @@ pub async fn run() -> anyhow::Result<()> {
interval_trigger,
cpfp_payouts_after_mins,
cpfp_payouts_after_blocks,
min_change,
)
.await?;
}
Expand Down
3 changes: 2 additions & 1 deletion src/job/process_payout_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ pub async fn construct_psbt(
let mut cfg = PsbtBuilderConfig::builder()
.consolidate_deprecated_keychains(queue_cfg.consolidate_deprecated_keychains)
.fee_rate(fee_rate)
.reserved_utxos(reserved_utxos);
.reserved_utxos(reserved_utxos)
.force_min_change_output(queue_cfg.force_min_change_sats);
if !for_estimation && queue_cfg.should_cpfp() {
let keychain_ids = wallets.values().flat_map(|w| w.keychain_ids());
let utxos = utxos
Expand Down
4 changes: 3 additions & 1 deletion src/payout_queue/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::time::Duration;

use crate::primitives::TxPriority;
use crate::primitives::{Satoshis, TxPriority};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PayoutQueueConfig {
Expand All @@ -10,6 +10,7 @@ pub struct PayoutQueueConfig {
pub cpfp_payouts_after_mins: Option<u32>,
#[serde(default)]
pub cpfp_payouts_after_blocks: Option<u32>,
pub force_min_change_sats: Option<Satoshis>,
pub consolidate_deprecated_keychains: bool,
pub trigger: PayoutQueueTrigger,
}
Expand Down Expand Up @@ -55,6 +56,7 @@ impl Default for PayoutQueueConfig {
},
cpfp_payouts_after_mins: None,
cpfp_payouts_after_blocks: None,
force_min_change_sats: None,
}
}
}
Expand Down
19 changes: 12 additions & 7 deletions src/wallet/psbt_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ pub struct PsbtBuilderConfig {
#[builder(default)]
reserved_utxos: HashMap<KeychainId, Vec<OutPoint>>,
#[builder(default)]
pub cpfp_utxos: HashMap<KeychainId, Vec<CpfpUtxo>>,
cpfp_utxos: HashMap<KeychainId, Vec<CpfpUtxo>>,
#[builder(default)]
for_estimation: bool,
#[builder(default)]
force_min_change_output: Option<Satoshis>,
}

impl PsbtBuilderConfig {
Expand Down Expand Up @@ -550,6 +552,9 @@ impl PsbtBuilder<AcceptingCurrentKeychainState> {
let mut builder = wallet.build_tx();
builder.fee_rate(self.cfg.fee_rate);
builder.drain_to(change_address.script_pubkey());
if let Some(sats) = self.cfg.force_min_change_output {
builder.add_recipient(change_address.script_pubkey(), u64::from(sats));
}

for (_, destination, satoshis) in payouts.iter() {
builder.add_recipient(destination.script_pubkey(), u64::from(*satoshis));
Expand Down Expand Up @@ -602,12 +607,12 @@ impl PsbtBuilder<AcceptingCurrentKeychainState> {
.iter()
.filter(|out| out.script_pubkey == script_pubkey)
.count();
let subtract_fee = if n_change_outputs > 1 {
crate::fees::output_fee(&self.cfg.fee_rate, script_pubkey)
+ self.cfg.fee_rate.fee_vb(HEADER_VBYTES)
} else {
self.cfg.fee_rate.fee_vb(HEADER_VBYTES)
};
// Here we are subtracting HEADER_VBYTES because we only need them 1 time
// and they will be added later if this is the first TX we are building
// also removing redundant change_output weight
let subtract_fee = self.cfg.fee_rate.fee_vb(HEADER_VBYTES)
+ (1.max(n_change_outputs) - 1) as u64
* crate::fees::output_fee(&self.cfg.fee_rate, script_pubkey);
let inputs = psbt
.unsigned_tx
.input
Expand Down
58 changes: 58 additions & 0 deletions tests/psbt_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,64 @@ async fn build_psbt_with_cpfp() -> anyhow::Result<()> {
Ok(())
}

#[tokio::test]
#[serial]
async fn build_psbt_with_min_change_output() -> anyhow::Result<()> {
let pool = helpers::init_pool().await?;

let domain_current_keychain_id = Uuid::new_v4();
let xpub = XPub::try_from(("tpubDD4vFnWuTMEcZiaaZPgvzeGyMzWe6qHW8gALk5Md9kutDvtdDjYFwzauEFFRHgov8pAwup5jX88j5YFyiACsPf3pqn5hBjvuTLRAseaJ6b4", Some("m/84'/0'/0'"))).unwrap();
let keychain_cfg = KeychainConfig::wpkh(xpub);
let domain_current_keychain = KeychainWallet::new(
pool.clone(),
Network::Regtest,
domain_current_keychain_id.into(),
keychain_cfg,
);
let domain_addr = domain_current_keychain.new_external_address().await?;

let bitcoind = helpers::bitcoind_client().await?;
let wallet_funding = 100_000_000;
let wallet_funding_sats = Satoshis::from(wallet_funding);
helpers::fund_addr(&bitcoind, &domain_addr, wallet_funding)?;
let tx_id = helpers::fund_addr(&bitcoind, &domain_addr, wallet_funding)?;
helpers::gen_blocks(&bitcoind, 10)?;

let sats_per_vbyte: f64 = 100.0;
let fee = FeeRate::from_sat_per_vb(sats_per_vbyte as f32);
let min_change = Satoshis::from(100_000_000);
let cfg = PsbtBuilderConfig::builder()
.consolidate_deprecated_keychains(true)
.fee_rate(fee)
.force_min_change_output(Some(min_change))
.build()
.unwrap();
let builder = PsbtBuilder::new(cfg);

let domain_wallet_id = WalletId::new();
let domain_send_amount = wallet_funding_sats - Satoshis::from(50_000_000);
let destination = Address::parse_from_trusted_source("mgWUuj1J1N882jmqFxtDepEC73Rr22E9GU");
let payouts_one = vec![(Uuid::new_v4(), destination.clone(), domain_send_amount)];

let builder = builder
.wallet_payouts(domain_wallet_id, payouts_one)
.accept_current_keychain();
while !find_tx_id(&pool, domain_current_keychain_id, tx_id).await? {
let blockchain = helpers::electrum_blockchain().await?;
domain_current_keychain.sync(blockchain).await?;
}
let builder = domain_current_keychain
.dispatch_bdk_wallet(builder)
.await?
.next_wallet();
let FinishedPsbtBuild { wallet_totals, .. } = builder.finish();
assert_eq!(wallet_totals.len(), 1);
let domain_wallet_total = wallet_totals.get(&domain_wallet_id).unwrap();
assert!(domain_wallet_total.change_satoshis >= min_change);

Ok(())
}

async fn find_tx_id(
pool: &sqlx::PgPool,
keychain_id: Uuid,
Expand Down

0 comments on commit 2c96a76

Please sign in to comment.