diff --git a/crates/ln-dlc-node/src/ldk_node_wallet.rs b/crates/ln-dlc-node/src/ldk_node_wallet.rs index 50d6246b0..4ffa17376 100644 --- a/crates/ln-dlc-node/src/ldk_node_wallet.rs +++ b/crates/ln-dlc-node/src/ldk_node_wallet.rs @@ -9,6 +9,7 @@ use bdk::blockchain::GetBlockHash; use bdk::blockchain::GetHeight; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; +use bdk::FeeRate; use bdk::SignOptions; use bdk::SyncOptions; use bdk::TransactionDetails; @@ -98,6 +99,10 @@ where Ok(()) } + pub fn get_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate { + self.fee_rate_estimator.estimate(confirmation_target) + } + #[autometrics] pub(crate) async fn create_funding_transaction( &self, @@ -108,7 +113,7 @@ where let locked_wallet = self.bdk_lock(); let mut tx_builder = locked_wallet.build_tx(); - let fee_rate = self.fee_rate_estimator.estimate(confirmation_target); + let fee_rate = self.get_fee_rate(confirmation_target); tx_builder .add_recipient(output_script, value_sats) .fee_rate(fee_rate) diff --git a/crates/ln-dlc-node/src/lib.rs b/crates/ln-dlc-node/src/lib.rs index ebd441b75..d92d64ce7 100644 --- a/crates/ln-dlc-node/src/lib.rs +++ b/crates/ln-dlc-node/src/lib.rs @@ -35,6 +35,7 @@ mod util; pub mod node; pub mod seed; +pub use ln::CONFIRMATION_TARGET; pub use ln::CONTRACT_TX_FEE_RATE; pub use ln::JUST_IN_TIME_CHANNEL_OUTBOUND_LIQUIDITY_SAT_MAX; pub use ln::LIQUIDITY_MULTIPLIER; diff --git a/crates/ln-dlc-node/src/ln/event_handler.rs b/crates/ln-dlc-node/src/ln/event_handler.rs index 6db0f87b9..7019315c4 100644 --- a/crates/ln-dlc-node/src/ln/event_handler.rs +++ b/crates/ln-dlc-node/src/ln/event_handler.rs @@ -1,6 +1,7 @@ use crate::dlc_custom_signer::CustomKeysManager; use crate::fee_rate_estimator::FeeRateEstimator; use crate::ln::coordinator_config; +use crate::ln::CONFIRMATION_TARGET; use crate::ln::HTLC_INTERCEPTED_CONNECTION_TIMEOUT; use crate::ln::JUST_IN_TIME_CHANNEL_OUTBOUND_LIQUIDITY_SAT_MAX; use crate::ln::LIQUIDITY_MULTIPLIER; @@ -41,11 +42,6 @@ use std::time::Duration; use time::OffsetDateTime; use tokio::sync::watch; -/// The speed at which we want a transaction to confirm used for feerate estimation. -/// -/// We set it to high priority because the channel funding transaction should be included fast. -const CONFIRMATION_TARGET: ConfirmationTarget = ConfirmationTarget::HighPriority; - pub struct EventHandler { channel_manager: Arc, wallet: Arc, diff --git a/crates/ln-dlc-node/src/ln/mod.rs b/crates/ln-dlc-node/src/ln/mod.rs index f0070021d..075444486 100644 --- a/crates/ln-dlc-node/src/ln/mod.rs +++ b/crates/ln-dlc-node/src/ln/mod.rs @@ -9,6 +9,7 @@ pub(crate) use config::app_config; pub(crate) use config::coordinator_config; pub use dlc_channel_details::DlcChannelDetails; pub(crate) use event_handler::EventHandler; +use lightning::chain::chaininterface::ConfirmationTarget; pub(crate) use logger::TracingLogger; /// When handling the [`Event::HTLCIntercepted`], we may need to @@ -35,6 +36,11 @@ pub const LIQUIDITY_MULTIPLIER: u64 = 2; /// The coordinator and the app have to align on this to agree on the fees. pub const CONTRACT_TX_FEE_RATE: u64 = 4; +/// The speed at which we want a transaction to confirm used for feerate estimation. +/// +/// We set it to high priority because the channel funding transaction should be included fast. +pub const CONFIRMATION_TARGET: ConfirmationTarget = ConfirmationTarget::HighPriority; + /// When handling the [`Event::HTLCIntercepted`], the user might not be online right away. This /// could be because she is funding the wallet through another wallet. In order to give the user /// some time to open 10101 again we wait for a bit to see if we can establish a connection. diff --git a/mobile/lib/common/application/channel_info_service.dart b/mobile/lib/common/application/channel_info_service.dart index 44d8a1817..7bbe714ec 100644 --- a/mobile/lib/common/application/channel_info_service.dart +++ b/mobile/lib/common/application/channel_info_service.dart @@ -10,6 +10,11 @@ class ChannelInfoService { return channelInfo != null ? ChannelInfo.fromApi(channelInfo) : null; } + Future getChannelOpenFeeEstimate() async { + int feeEstimate = await rust.api.getChannelOpenFeeEstimateSat(); + return Amount(feeEstimate); + } + /// The multiplier that is used to determine the coordinator liquidity /// /// This value is an arbitrary number that may be subject to change. diff --git a/mobile/lib/common/modal_bottom_sheet_info.dart b/mobile/lib/common/modal_bottom_sheet_info.dart index e47b5353e..e5219ee61 100644 --- a/mobile/lib/common/modal_bottom_sheet_info.dart +++ b/mobile/lib/common/modal_bottom_sheet_info.dart @@ -2,15 +2,17 @@ import 'package:flutter/material.dart'; import 'package:get_10101/common/color.dart'; class ModalBottomSheetInfo extends StatelessWidget { - final String infoText; - final String buttonText; - final EdgeInsets padding; + final Widget child; + final String closeButtonText; + final EdgeInsets infoButtonPadding; + + static const double buttonRadius = 20.0; const ModalBottomSheetInfo( {super.key, - required this.infoText, - required this.buttonText, - this.padding = const EdgeInsets.all(8.0)}); + required this.child, + required this.closeButtonText, + this.infoButtonPadding = const EdgeInsets.all(8.0)}); @override Widget build(BuildContext context) { @@ -20,7 +22,7 @@ class ModalBottomSheetInfo extends StatelessWidget { showModalBottomSheet( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( - top: Radius.circular(20), + top: Radius.circular(buttonRadius), ), ), clipBehavior: Clip.antiAliasWithSaveLayer, @@ -33,16 +35,16 @@ class ModalBottomSheetInfo extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // TODO: Add link to FAQ - Text(infoText), - ElevatedButton(onPressed: () => Navigator.pop(context), child: Text(buttonText)) + child, + ElevatedButton( + onPressed: () => Navigator.pop(context), child: Text(closeButtonText)) ], ), ); }, ); }, - padding: padding, + padding: infoButtonPadding, constraints: const BoxConstraints(), icon: Icon( Icons.info, diff --git a/mobile/lib/features/trade/trade_bottom_sheet.dart b/mobile/lib/features/trade/trade_bottom_sheet.dart index 021c216ba..7591efdd8 100644 --- a/mobile/lib/features/trade/trade_bottom_sheet.dart +++ b/mobile/lib/features/trade/trade_bottom_sheet.dart @@ -73,11 +73,11 @@ class TradeBottomSheet extends StatelessWidget { style: TextStyle(color: Colors.grey), ), ModalBottomSheetInfo( - infoText: "While in beta only market orders are enabled in the 10101 app.\n\n" + closeButtonText: "Back to order...", + child: Text("While in beta only market orders are enabled in the 10101 app.\n\n" "Market orders are executed at the best market price. \n\nPlease note that the displayed " "price is the best market price at the time but due to fast market " - "movements the market price for order fulfillment can be slightly different.", - buttonText: "Back to order...") + "movements the market price for order fulfillment can be slightly different.")) ], ), ), diff --git a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart index 40e950a51..f3fa3a468 100644 --- a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart +++ b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart @@ -121,13 +121,13 @@ class _TradeBottomSheetTabState extends State { width: 5, ), ModalBottomSheetInfo( - infoText: + closeButtonText: "Back to order...", + infoButtonPadding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( "Your usable balance of $usableBalance sats takes a fixed reserve of $totalReserve sats into account. " "\n${channelReserve.sats} is the minimum amount that has to stay in the Lightning channel. " "\n${tradeFeeReserve.sats} is reserved for fees per trade that is needed for publishing on-chain transactions in a worst case scenario. This is needed for the self-custodial setup" - "\n\nWe are working on optimizing the reserve and it might be subject to change after the beta.", - buttonText: "Back to order...", - padding: const EdgeInsets.symmetric(horizontal: 8.0), + "\n\nWe are working on optimizing the reserve and it might be subject to change after the beta."), ) ], ), @@ -254,12 +254,12 @@ class _TradeBottomSheetTabState extends State { ), if (showCapacityInfo) ModalBottomSheetInfo( - infoText: + closeButtonText: "Back to order...", + child: Text( "Your channel capacity is limited to $channelCapacity sats. During the beta channel resize is not available yet" "In order to trade with higher margin you have to reduce your balance" "\n\nYour current usable balance is $usableBalance." - "Please send ${usableBalance - (channelCapacity.sats / coordinatorLiquidityMultiplier)} sats out of your wallet to free up capacity.", - buttonText: "Back to order...") + "Please send ${usableBalance - (channelCapacity.sats / coordinatorLiquidityMultiplier)} sats out of your wallet to free up capacity.")) ], ), LeverageSlider( diff --git a/mobile/lib/features/wallet/create_invoice_screen.dart b/mobile/lib/features/wallet/create_invoice_screen.dart index 8ad2f5dc6..b5bcae88b 100644 --- a/mobile/lib/features/wallet/create_invoice_screen.dart +++ b/mobile/lib/features/wallet/create_invoice_screen.dart @@ -5,6 +5,7 @@ import 'package:get_10101/common/amount_text.dart'; import 'package:get_10101/common/amount_text_input_form_field.dart'; import 'package:get_10101/common/application/channel_info_service.dart'; import 'package:get_10101/common/domain/model.dart'; +import 'package:get_10101/features/wallet/domain/share_invoice.dart'; import 'package:get_10101/features/wallet/share_invoice_screen.dart'; import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; import 'package:get_10101/features/wallet/wallet_screen.dart'; @@ -34,8 +35,17 @@ class _CreateInvoiceScreenState extends State { final WalletService walletService = const WalletService(); final ChannelInfoService channelInfoService = const ChannelInfoService(); + + /// The channel info if a channel already exists + /// + /// If no channel exists yet this field will be null. ChannelInfo? channelInfo; + /// Estimated fees for receiving + /// + /// These fees have to be added on top of the receive amount because they are collected after receiving the funds. + Amount? feeEstimate; + @override void dispose() { _amountController.dispose(); @@ -50,6 +60,11 @@ class _CreateInvoiceScreenState extends State { Future initChannelInfo() async { channelInfo = await channelInfoService.getChannelInfo(); + + // initial channel opening + if (channelInfo == null) { + feeEstimate = await channelInfoService.getChannelOpenFeeEstimate(); + } } @override @@ -68,16 +83,18 @@ class _CreateInvoiceScreenState extends State { Amount maxAllowedOutboundCapacity = Amount((channelCapacity.sats / coordinatorLiquidityMultiplier).floor()); + // the minimum amount that has to be in the wallet to be able to trade + Amount minAmountToBeAbleToTrade = Amount((channelInfo?.reserve.sats ?? initialReserve.sats) + + tradeFeeReserve.sats + + minTradeMargin.sats + + // make sure that the amount received covers potential fees as well + (feeEstimate?.sats ?? 0)); + // it can go below 0 if the user has an unbalanced channel Amount maxReceiveAmount = Amount(max(maxAllowedOutboundCapacity.sats - balance.sats, 0)); // we have to at least receive enough to be able to trade with the minimum trade amount - Amount minReceiveAmount = Amount(max( - (channelInfo?.reserve.sats ?? initialReserve.sats) + - tradeFeeReserve.sats + - minTradeMargin.sats - - balance.sats, - 1)); + Amount minReceiveAmount = Amount(max(minAmountToBeAbleToTrade.sats - balance.sats, 1)); return Scaffold( appBar: AppBar(title: const Text("Receive funds")), @@ -145,12 +162,12 @@ class _CreateInvoiceScreenState extends State { ), if (showValidationHint) ModalBottomSheetInfo( - infoText: + closeButtonText: "Back to Receive...", + child: Text( "While in beta, maximum channel capacity is limited to ${formatSats(maxChannelCapacity)}; channels above this capacity might get rejected." "\nThe maximum is enforced initially to ensure users only trade with small stakes until the software has proven to be stable." "\n\nYour current balance is ${formatSats(balance)}, so you can receive up to ${formatSats(maxReceiveAmount)}." - "\nIf you hold less than ${formatSats(minReceiveAmount)} or more than ${formatSats(maxAllowedOutboundCapacity)} in your wallet you might not be able to trade.", - buttonText: "Back to Receive..."), + "\nIf you hold less than ${formatSats(minAmountToBeAbleToTrade)} or more than ${formatSats(maxAllowedOutboundCapacity)} in your wallet you might not be able to trade.")), ], ), ), @@ -175,10 +192,14 @@ class _CreateInvoiceScreenState extends State { : () { if (_formKey.currentState!.validate()) { showValidationHint = false; + walletService.createInvoice(amount!).then((invoice) { if (invoice != null) { - GoRouter.of(context) - .go(ShareInvoiceScreen.route, extra: invoice); + GoRouter.of(context).go(ShareInvoiceScreen.route, + extra: ShareInvoice( + rawInvoice: invoice, + invoiceAmount: amount!, + channelOpenFee: feeEstimate)); } }); } else { diff --git a/mobile/lib/features/wallet/domain/share_invoice.dart b/mobile/lib/features/wallet/domain/share_invoice.dart new file mode 100644 index 000000000..96211e308 --- /dev/null +++ b/mobile/lib/features/wallet/domain/share_invoice.dart @@ -0,0 +1,9 @@ +import '../../../common/domain/model.dart'; + +class ShareInvoice { + final String rawInvoice; + final Amount invoiceAmount; + Amount? channelOpenFee; + + ShareInvoice({required this.rawInvoice, required this.invoiceAmount, this.channelOpenFee}); +} diff --git a/mobile/lib/features/wallet/share_invoice_screen.dart b/mobile/lib/features/wallet/share_invoice_screen.dart index dfc816ad7..9a0887068 100644 --- a/mobile/lib/features/wallet/share_invoice_screen.dart +++ b/mobile/lib/features/wallet/share_invoice_screen.dart @@ -4,7 +4,10 @@ import 'dart:developer'; import 'package:f_logs/f_logs.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get_10101/common/amount_text.dart'; +import 'package:get_10101/common/modal_bottom_sheet_info.dart'; import 'package:get_10101/common/snack_bar.dart'; +import 'package:get_10101/common/value_data_row.dart'; import 'package:get_10101/features/wallet/create_invoice_screen.dart'; import 'package:get_10101/features/wallet/domain/wallet_info.dart'; import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; @@ -15,18 +18,14 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:get_10101/ffi.dart' as rust; - -import 'application/wallet_service.dart'; +import 'package:get_10101/features/wallet/domain/share_invoice.dart'; class ShareInvoiceScreen extends StatefulWidget { static const route = "${WalletScreen.route}/${CreateInvoiceScreen.subRouteName}/$subRouteName"; static const subRouteName = "share_invoice"; - final WalletService walletService; - final String invoice; + final ShareInvoice invoice; - const ShareInvoiceScreen( - {super.key, this.walletService = const WalletService(), required this.invoice}); + const ShareInvoiceScreen({super.key, required this.invoice}); @override State createState() => _ShareInvoiceScreenState(); @@ -44,6 +43,11 @@ class _ShareInvoiceScreenState extends State { const EdgeInsets buttonSpacing = EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0); + const qrWidth = 200.0; + const qrPadding = 5.0; + const infoButtonRadius = ModalBottomSheetInfo.buttonRadius; + const infoButtonPadding = 5.0; + return Scaffold( appBar: AppBar(title: const Text("Receive funds")), body: SafeArea( @@ -55,7 +59,7 @@ class _ShareInvoiceScreenState extends State { Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ const Padding( - padding: EdgeInsets.only(top: 25.0, bottom: 50.0), + padding: EdgeInsets.only(top: 25.0, bottom: 30.0), child: Text( "Share payment request", style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), @@ -64,12 +68,56 @@ class _ShareInvoiceScreenState extends State { Expanded( child: Center( child: QrImageView( - data: widget.invoice, + data: widget.invoice.rawInvoice, version: QrVersions.auto, - size: 200.0, + padding: const EdgeInsets.all(qrPadding), ), ), ), + const SizedBox(height: 10), + Center( + child: SizedBox( + // Size of the qr image minus padding + width: qrWidth - 2 * qrPadding, + child: ValueDataRow( + type: ValueType.amount, + value: widget.invoice.invoiceAmount, + label: 'Amount', + ))), + if (widget.invoice.channelOpenFee != null) + Padding( + // Set in by size of info button on the right + padding: const EdgeInsets.only(left: infoButtonRadius * 2), + child: SizedBox( + width: qrWidth - 2 * qrPadding + infoButtonRadius * 2, + child: Row( + children: [ + Expanded( + child: ValueDataRow( + type: ValueType.amount, + value: widget.invoice.channelOpenFee, + label: "Fee Estimate")), + ModalBottomSheetInfo( + closeButtonText: "Back to Share Invoice", + infoButtonPadding: const EdgeInsets.all(infoButtonPadding), + child: Column( + children: [ + Center( + child: Text("Understanding Fees", + style: Theme.of(context).textTheme.headlineSmall), + ), + const SizedBox(height: 10), + Text( + "Upon receiving your first payment the 10101 LSP will open a Lightning channel with you.\n" + "To cover the costs for opening the channel the transaction fee is collected after the channel was opened, meaning that an estimated ${formatSats(widget.invoice.channelOpenFee!)} will be collected from your wallet once the channel was opened.\n" + "The fee estimate is based on a transaction weight with two inputs and the current estimated fee rate."), + ], + )), + ], + ), + ), + ), + const SizedBox(height: 10) ]), ), // Faucet button, only available if we are on regtest @@ -85,7 +133,7 @@ class _ShareInvoiceScreenState extends State { final router = GoRouter.of(context); try { - await payInvoiceWithFaucet(widget.invoice); + await payInvoiceWithFaucet(widget.invoice.rawInvoice); // Pop both create invoice screen and share invoice screen router.pop(); router.pop(); @@ -111,7 +159,7 @@ class _ShareInvoiceScreenState extends State { padding: buttonSpacing, child: OutlinedButton( onPressed: () { - Clipboard.setData(ClipboardData(text: widget.invoice)).then((_) { + Clipboard.setData(ClipboardData(text: widget.invoice.rawInvoice)).then((_) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Invoice copied to clipboard'))); }); @@ -138,7 +186,7 @@ class _ShareInvoiceScreenState extends State { child: Padding( padding: buttonSpacing, child: OutlinedButton( - onPressed: () => Share.share(widget.invoice), + onPressed: () => Share.share(widget.invoice.rawInvoice), style: ElevatedButton.styleFrom( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(5.0))), @@ -211,36 +259,4 @@ class _ShareInvoiceScreenState extends State { FLog.info(text: "Paying invoice succeeded: ${response.body}"); } } - - // Open channel directly between coordinator and app. - // - // Just for regtest. - Future openCoordinatorChannel(String coordinatorHost) async { - int coordinatorPort = const int.fromEnvironment("COORDINATOR_PORT_HTTP", defaultValue: 8000); - var coordinator = 'http://$coordinatorHost:$coordinatorPort'; - - final requestBody = { - 'target': {'pubkey': rust.api.getNodeId()}, - 'local_balance': 200000, - 'remote_balance': 100000, - 'is_public': false - }; - final jsonString = json.encode(requestBody).toString(); - - FLog.info(text: jsonString); - FLog.info(text: coordinator); - - final response = await http.post( - Uri.parse('$coordinator/api/channels'), - headers: {'Content-Type': 'application/json'}, - body: jsonString, - ); - - if (response.statusCode != 200 || response.body.contains("payment_error")) { - throw Exception( - "Failed to open channel with coordinator: Received ${response.statusCode} ${response.body}"); - } else { - FLog.info(text: "Initiating channel open with coordinator succeeded: ${response.body}"); - } - } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 0ff588516..7aaeed7a4 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -21,6 +21,7 @@ import 'package:get_10101/features/trade/settings_screen.dart'; import 'package:get_10101/features/trade/trade_theme.dart'; import 'package:get_10101/features/wallet/application/wallet_service.dart'; import 'package:get_10101/features/wallet/create_invoice_screen.dart'; +import 'package:get_10101/features/wallet/domain/share_invoice.dart'; import 'package:get_10101/features/wallet/seed_screen.dart'; import 'package:get_10101/features/wallet/send_payment_change_notifier.dart'; import 'package:get_10101/features/wallet/share_invoice_screen.dart'; @@ -137,7 +138,7 @@ class _TenTenOneAppState extends State { // Use root navigator so the screen overlays the application shell parentNavigatorKey: _rootNavigatorKey, builder: (BuildContext context, GoRouterState state) { - return ShareInvoiceScreen(invoice: state.extra as String); + return ShareInvoiceScreen(invoice: state.extra as ShareInvoice); }, ), ]), diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index 658b0f7db..521dd9bdc 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -9,6 +9,7 @@ use crate::db; use crate::event; use crate::event::api::FlutterSubscriber; use crate::ln_dlc; +use crate::ln_dlc::FUNDING_TX_WEIGHT_ESTIMATE; use crate::logger; use crate::orderbook; use crate::trade::order; @@ -341,3 +342,10 @@ pub fn decode_invoice(invoice: String) -> Result { pub fn get_node_id() -> SyncReturn { SyncReturn(ln_dlc::get_node_info().pubkey.to_string()) } + +pub fn get_channel_open_fee_estimate_sat() -> Result { + let fee_rate = ln_dlc::get_fee_rate()?; + let estimate = FUNDING_TX_WEIGHT_ESTIMATE as f32 * fee_rate.as_sat_per_vb(); + + Ok(estimate.ceil() as u64) +} diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index bc86bcdff..c2f7cd91c 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -20,6 +20,7 @@ use bdk::bitcoin::secp256k1::SecretKey; use bdk::bitcoin::Txid; use bdk::bitcoin::XOnlyPublicKey; use bdk::BlockTime; +use bdk::FeeRate; use coordinator_commons::TradeParams; use itertools::chain; use itertools::Itertools; @@ -31,6 +32,7 @@ use ln_dlc_node::node::rust_dlc_manager::ChannelId; use ln_dlc_node::node::LnDlcNodeSettings; use ln_dlc_node::node::NodeInfo; use ln_dlc_node::seed::Bip39Seed; +use ln_dlc_node::CONFIRMATION_TARGET; use orderbook_commons::FakeScidResponse; use orderbook_commons::FEE_INVOICE_DESCRIPTION_PREFIX_TAKER; use parking_lot::RwLock; @@ -57,6 +59,15 @@ const PROCESS_INCOMING_MESSAGES_INTERVAL: Duration = Duration::from_secs(5); const UPDATE_WALLET_HISTORY_INTERVAL: Duration = Duration::from_secs(5); const CHECK_OPEN_ORDERS_INTERVAL: Duration = Duration::from_secs(60); +/// The weight estimate of the funding transaction +/// +/// This weight estimate assumes two inputs. +/// This value was chosen based on mainnet channel funding transactions with two inputs. +/// Note that we cannot predict this value precisely, because the app cannot predict what UTXOs the +/// coordinator will use for the channel opening transaction. Only once the transaction is know the +/// exact fee will be know. +pub const FUNDING_TX_WEIGHT_ESTIMATE: u64 = 220; + pub async fn refresh_wallet_info() -> Result<()> { let node = NODE.get(); let wallet = node.inner.wallet(); @@ -426,6 +437,11 @@ pub fn get_usable_channel_details() -> Result> { Ok(channels) } +pub fn get_fee_rate() -> Result { + let node = NODE.try_get().context("failed to get ln dlc node")?; + Ok(node.inner.wallet().get_fee_rate(CONFIRMATION_TARGET)) +} + pub fn create_invoice(amount_sats: Option) -> Result { let runtime = get_or_create_tokio_runtime()?;