Skip to content

Commit

Permalink
Merge pull request #1527 from get10101/configure-fee
Browse files Browse the repository at this point in the history
feat: configurable fee for sending on chain
  • Loading branch information
bonomat authored Jan 3, 2024
2 parents dce84a0 + 5ffea20 commit 4eb41dc
Show file tree
Hide file tree
Showing 18 changed files with 858 additions and 181 deletions.
127 changes: 86 additions & 41 deletions crates/ln-dlc-node/src/ldk_node_wallet.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::fee_rate_estimator::EstimateFeeRate;
use crate::node::Fee;
use crate::node::Storage;
use anyhow::anyhow;
use anyhow::bail;
Expand All @@ -9,12 +10,15 @@ use bdk::blockchain::Blockchain;
use bdk::blockchain::GetBlockHash;
use bdk::blockchain::GetHeight;
use bdk::database::BatchDatabase;
use bdk::psbt::PsbtUtils;
use bdk::wallet::AddressIndex;
use bdk::FeeRate;
use bdk::SignOptions;
use bdk::SyncOptions;
use bdk::TransactionDetails;
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::psbt::PartiallySignedTransaction;
use bitcoin::Amount;
use bitcoin::BlockHash;
use bitcoin::OutPoint;
use bitcoin::Script;
Expand All @@ -28,6 +32,9 @@ use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;

/// Taken from mempool.space
const AVG_SEGWIT_TX_WEIGHT_VB: usize = 140;

pub struct Wallet<D, B, F, N>
where
D: BatchDatabase,
Expand Down Expand Up @@ -182,57 +189,95 @@ where
Ok(self.bdk_lock().get_balance()?)
}

/// Send funds to the given address.
///
/// If `amount_sat_or_drain` is `0` the wallet will be drained, i.e., all available funds
/// will be spent.
pub(crate) fn send_to_address(
/// Build the PSBT for sending funds to a given address.
fn build_psbt(
&self,
address: &bitcoin::Address,
amount_sat_or_drain: u64,
) -> Result<Txid> {
let fee_rate = self.fee_rate_estimator.estimate(ConfirmationTarget::Normal);

let tx = {
let locked_wallet = self.bdk_lock();
let mut tx_builder = locked_wallet.build_tx();

if amount_sat_or_drain > 0 {
tx_builder
.add_recipient(address.script_pubkey(), amount_sat_or_drain)
.fee_rate(fee_rate)
.enable_rbf();
} else {
tx_builder
.drain_wallet()
.drain_to(address.script_pubkey())
.fee_rate(fee_rate)
.enable_rbf();
fee: Fee,
) -> Result<PartiallySignedTransaction> {
let locked_wallet = self.bdk_lock();
let mut tx_builder = locked_wallet.build_tx();

if amount_sat_or_drain > 0 {
tx_builder
.add_recipient(address.script_pubkey(), amount_sat_or_drain)
.enable_rbf();
} else {
tx_builder
.drain_wallet()
.drain_to(address.script_pubkey())
.enable_rbf();
}

match fee {
Fee::Priority(target) => tx_builder.fee_rate(self.fee_rate_estimator.estimate(target)),
Fee::Custom(amt) => tx_builder.fee_absolute(amt.to_sat()),
};

let mut psbt = match tx_builder.finish() {
Ok((psbt, _)) => {
tracing::trace!("Created PSBT: {:?}", psbt);
psbt
}
Err(err) => {
bail!(err)
}
};

let mut psbt = match tx_builder.finish() {
Ok((psbt, _)) => {
tracing::trace!("Created PSBT: {:?}", psbt);
psbt
}
Err(err) => {
bail!(err)
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
Ok(finalized) => {
if !finalized {
bail!("On chain creation failed");
}
};
}
Err(err) => {
bail!(err)
}
}

match locked_wallet.sign(&mut psbt, SignOptions::default()) {
Ok(finalized) => {
if !finalized {
bail!("On chain creation failed");
}
}
Err(err) => {
bail!(err)
}
Ok(psbt)
}

/// Estimate the fee for sending funds to a given address
pub(crate) fn calculate_fee(
&self,
address: &bitcoin::Address,
amount_sat_or_drain: u64,
confirmation_target: ConfirmationTarget,
) -> Result<Amount> {
let psbt = self.build_psbt(
address,
amount_sat_or_drain,
Fee::Priority(confirmation_target),
);

let fee_sat = match psbt {
Ok(psbt) => psbt
.fee_amount()
.context("Fee info could not be calculated")?,
Err(_) => {
let rate = self.fee_rate_estimator.estimate(confirmation_target);
rate.fee_vb(AVG_SEGWIT_TX_WEIGHT_VB)
}
psbt.extract_tx()
};

Ok(Amount::from_sat(fee_sat))
}

/// Send funds to the given address.
///
/// If `amount_sat_or_drain` is `0` the wallet will be drained, i.e., all available funds
/// will be spent.
pub(crate) fn send_to_address(
&self,
address: &bitcoin::Address,
amount_sat_or_drain: u64,
fee: Fee,
) -> Result<Txid> {
let tx = self
.build_psbt(address, amount_sat_or_drain, fee)?
.extract_tx();
let txid = self.broadcast_transaction(&tx)?;

if amount_sat_or_drain > 0 {
Expand Down
32 changes: 30 additions & 2 deletions crates/ln-dlc-node/src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ use anyhow::Result;
use bdk::FeeRate;
use bitcoin::hashes::hex::ToHex;
use bitcoin::secp256k1::PublicKey;
use bitcoin::Amount;
use bitcoin::Network;
use bitcoin::Txid;
use bitcoin::XOnlyPublicKey;
use dlc_messages::message_handler::MessageHandler as DlcMessageHandler;
use futures::future::RemoteHandle;
use futures::FutureExt;
use lightning::chain::chaininterface::ConfirmationTarget;
use lightning::chain::chainmonitor;
use lightning::chain::Confirm;
use lightning::ln::msgs::RoutingMessageHandler;
Expand Down Expand Up @@ -180,6 +182,14 @@ pub struct Node<S: TenTenOneStorage, N: Storage> {
pub probes: Probes,
}

/// An on-chain network fee for a transaction
pub enum Fee {
/// A fee given by the transaction's priority
Priority(ConfirmationTarget),
/// A fee given by an absolute amount
Custom(Amount),
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
pub struct NodeInfo {
pub pubkey: PublicKey,
Expand Down Expand Up @@ -635,11 +645,29 @@ impl<S: TenTenOneStorage + 'static, N: Storage + Sync + Send + 'static> Node<S,
)
}

/// Calculate the fee for sending the given `amount_sats` to the given `address` on-chain with
/// the given `fee`.
pub fn calculate_fee(
&self,
address: &bitcoin::Address,
amount_sats: u64,
fee: ConfirmationTarget,
) -> Result<Amount> {
self.wallet
.ldk_wallet()
.calculate_fee(address, amount_sats, fee)
}

/// Send the given `amount_sats` sats to the given `address` on-chain.
pub fn send_to_address(&self, address: &bitcoin::Address, amount_sats: u64) -> Result<Txid> {
pub fn send_to_address(
&self,
address: &bitcoin::Address,
amount_sats: u64,
fee: Fee,
) -> Result<Txid> {
self.wallet
.ldk_wallet()
.send_to_address(address, amount_sats)
.send_to_address(address, amount_sats, fee)
}
}

Expand Down
29 changes: 29 additions & 0 deletions mobile/lib/common/amount_and_fiat_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/amount_text.dart';
import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/common/fiat_text.dart';
import 'package:get_10101/features/swap/swap_value_change_notifier.dart';
import 'package:provider/provider.dart';

class AmountAndFiatText extends StatelessWidget {
final Amount amount;

const AmountAndFiatText({super.key, required this.amount});

@override
Widget build(BuildContext context) {
return Selector<SwapValuesChangeNotifier, double>(
selector: (_, provider) => provider.stableValues().price ?? 1.0,
builder: (BuildContext context, double price, Widget? child) =>
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
AmountText(amount: amount, textStyle: const TextStyle(fontSize: 17)),
Wrap(children: [
Text("~", style: TextStyle(color: Colors.grey.shade700, fontSize: 15)),
FiatText(
amount: amount.btc / price,
textStyle: TextStyle(color: Colors.grey.shade700, fontSize: 15))
])
]),
);
}
}
43 changes: 6 additions & 37 deletions mobile/lib/common/amount_input_modalised.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/edit_modal.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:get_10101/common/amount_text.dart';
Expand Down Expand Up @@ -66,43 +67,11 @@ class AmountInputModalisedField extends StatelessWidget {

void _showModal(BuildContext context, BtcOrFiat type, int? amount, void Function(int?) onSetAmount,
String? Function(String?)? validator) {
showModalBottomSheet<void>(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
clipBehavior: Clip.antiAlias,
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
// the GestureDetector ensures that we can close the keyboard by tapping into the modal
child: GestureDetector(
onTap: () {
FocusScopeNode currentFocus = FocusScope.of(context);

if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
},
child: SingleChildScrollView(
child: SizedBox(
// TODO: Find a way to make height dynamic depending on the children size
// This is needed because otherwise the keyboard does not push the sheet up correctly
height: 200,
child: EnterAmountModal(
amount: amount,
onSetAmount: onSetAmount,
validator: validator,
type: type),
),
),
)));
});
showEditModal<void>(
context: context,
builder: (BuildContext context, _) => EnterAmountModal(
amount: amount, onSetAmount: onSetAmount, validator: validator, type: type),
);
}

class EnterAmountModal extends StatefulWidget {
Expand Down
Loading

0 comments on commit 4eb41dc

Please sign in to comment.