Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions lwk_wollet/src/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1172,16 +1172,23 @@ impl TxBuilder {
});
}
let satoshi_change = satoshi_in - satoshi_out - temp_fee;
let addressee = if let Some(address) = self.drain_to {
Recipient::from_address(satoshi_change, &address, wollet.policy_asset())
} else {
wollet.addressee_change(
// Track whether we add a change/drain output (needed later for fee adjustment).
// Skip change only when: drain_lbtc is set AND there are explicit recipients AND no drain_to.
Copy link
Contributor

@LeoComandini LeoComandini Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are we checking checking that there are explicit recipients ?

// If there are no recipients (satoshi_out == 0), we always need a change/drain output.
let has_change_output = self.drain_to.is_some() || !self.drain_lbtc || satoshi_out == 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since has_change_output is only used once below, it makes sense to compute it below, and move all of the comments there

if let Some(address) = self.drain_to {
let addressee =
Recipient::from_address(satoshi_change, &address, wollet.policy_asset());
wollet.add_output(&mut pset, &addressee)?;
} else if !self.drain_lbtc || satoshi_out == 0 {
let addressee = wollet.addressee_change(
satoshi_change,
wollet.policy_asset(),
&mut last_unused_internal,
)?
};
wollet.add_output(&mut pset, &addressee)?;
)?;
wollet.add_output(&mut pset, &addressee)?;
}

let fee_output =
Output::new_explicit(Script::default(), temp_fee, wollet.policy_asset(), None);
pset.add_output(fee_output);
Expand All @@ -1203,7 +1210,7 @@ impl TxBuilder {

let vsize = weight.div_ceil(4);
let fee = (vsize as f32 * self.fee_rate / 1000.0).ceil() as u64;
if satoshi_in <= (satoshi_out + fee) {
if satoshi_in < (satoshi_out + fee) {
return Err(Error::InsufficientFunds {
missing_sats: (satoshi_out + fee + 1) - satoshi_in, // +1 to ensure we have more than just equal
asset_id: wollet.policy_asset(),
Expand All @@ -1214,8 +1221,11 @@ impl TxBuilder {
// Replace change and fee outputs
let n_outputs = pset.n_outputs();
let outputs = pset.outputs_mut();
let change_output = &mut outputs[n_outputs - 2]; // index check: we always have the lbtc change and the fee output at least
change_output.amount = Some(satoshi_change);
// Update change output only if it exists (not when drain_lbtc without drain_to)
if has_change_output {
let change_output = &mut outputs[n_outputs - 2]; // index check: we always have the lbtc change and the fee output at least
change_output.amount = Some(satoshi_change);
}
let fee_output = &mut outputs[n_outputs - 1];
fee_output.amount = Some(fee);

Expand Down
47 changes: 47 additions & 0 deletions lwk_wollet/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,53 @@ fn drain() {
}
}

#[test]
fn send_all_vs_send_balance_minus_fee() {
// Create a send_all transaction to extract the fee, then verify that sending
// (balance - fee) with a normal transaction results in the same outcome.
let env = TestEnvBuilder::from_env().with_electrum().build();
let signer = generate_signer();
let view_key = generate_view_key();
let desc = format!("ct({},elwpkh({}/*))", view_key, signer.xpub());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
let desc = format!("ct({},elwpkh({}/*))", view_key, signer.xpub());
let desc = format!("ct({},elwpkh({}/<0;1>/*))", view_key, signer.xpub());

since this PR is affecting the change we have the test generating change addresses
(although for the sake of this PR it should not matter)

let signer = AnySigner::Software(signer);

let client = test_client_electrum(&env.electrum_url());
let mut wallet = TestWollet::new(client, &desc);

// Fund the wallet
wallet.fund_btc(&env);

let node_address = env.elementsd_getnewaddress();
let balance = wallet.balance_btc();

// Create a send_all transaction without broadcasting to extract the fee
let pset_send_all = wallet
.tx_builder()
.drain_lbtc_wallet()
.drain_lbtc_to(node_address.clone())
.finish()
.unwrap();

let details = wallet.wollet.get_details(&pset_send_all).unwrap();
let fee = details.balance.fee;

// Create a normal send transaction with amount = balance - fee
let amount = balance - fee;
let mut pset = wallet
.tx_builder()
.drain_lbtc_wallet()
.add_lbtc_recipient(&node_address, amount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test where we do the same thing, but sending to an explicit address?

.unwrap()
.finish()
.unwrap();

wallet.sign(&signer, &mut pset);
wallet.send(&mut pset);

// Verify the wallet is now empty
assert_eq!(wallet.balance_btc(), 0);
}

fn wait_tx_update<C: BlockchainBackend>(wallet: &mut TestWollet<C>) {
for _ in 0..50 {
if let Some(update) = wallet.client.full_scan(&wallet.wollet).unwrap() {
Expand Down
Loading