Skip to content

Commit

Permalink
Merge #921
Browse files Browse the repository at this point in the history
921: Calculate and display open-channel fee estimate r=da-kami a=da-kami

fixes https://github.com/get10101/meta/issues/172

Please note: The screen font may look a bit odd for you because my default test setup is iPhone SE (3rd gen) with **Accessibility Large, Bold Text** activated to ensure that the screen does not break (unless users somehow increase the font even more, not sure if that is possible):

![2023-07-07 14 56 04](https://github.com/get10101/10101/assets/5557790/f7b9c5d0-eb4b-4d68-8006-2eadeda6f895)


Co-authored-by: Daniel Karzel <[email protected]>
  • Loading branch information
bors[bot] and da-kami authored Jul 11, 2023
2 parents df21c1e + 24236da commit 49e0dc4
Show file tree
Hide file tree
Showing 14 changed files with 170 additions and 84 deletions.
7 changes: 6 additions & 1 deletion crates/ln-dlc-node/src/ldk_node_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions crates/ln-dlc-node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 1 addition & 5 deletions crates/ln-dlc-node/src/ln/event_handler.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<S> {
channel_manager: Arc<ChannelManager>,
wallet: Arc<LnDlcWallet>,
Expand Down
6 changes: 6 additions & 0 deletions crates/ln-dlc-node/src/ln/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions mobile/lib/common/application/channel_info_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ class ChannelInfoService {
return channelInfo != null ? ChannelInfo.fromApi(channelInfo) : null;
}

Future<Amount> 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.
Expand Down
24 changes: 13 additions & 11 deletions mobile/lib/common/modal_bottom_sheet_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -20,7 +22,7 @@ class ModalBottomSheetInfo extends StatelessWidget {
showModalBottomSheet<void>(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
top: Radius.circular(buttonRadius),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions mobile/lib/features/trade/trade_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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."))
],
),
),
Expand Down
14 changes: 7 additions & 7 deletions mobile/lib/features/trade/trade_bottom_sheet_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
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."),
)
],
),
Expand Down Expand Up @@ -254,12 +254,12 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
),
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(
Expand Down
43 changes: 32 additions & 11 deletions mobile/lib/features/wallet/create_invoice_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,8 +35,17 @@ class _CreateInvoiceScreenState extends State<CreateInvoiceScreen> {
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();
Expand All @@ -50,6 +60,11 @@ class _CreateInvoiceScreenState extends State<CreateInvoiceScreen> {

Future<void> initChannelInfo() async {
channelInfo = await channelInfoService.getChannelInfo();

// initial channel opening
if (channelInfo == null) {
feeEstimate = await channelInfoService.getChannelOpenFeeEstimate();
}
}

@override
Expand All @@ -68,16 +83,18 @@ class _CreateInvoiceScreenState extends State<CreateInvoiceScreen> {
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")),
Expand Down Expand Up @@ -145,12 +162,12 @@ class _CreateInvoiceScreenState extends State<CreateInvoiceScreen> {
),
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.")),
],
),
),
Expand All @@ -175,10 +192,14 @@ class _CreateInvoiceScreenState extends State<CreateInvoiceScreen> {
: () {
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 {
Expand Down
9 changes: 9 additions & 0 deletions mobile/lib/features/wallet/domain/share_invoice.dart
Original file line number Diff line number Diff line change
@@ -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});
}
Loading

0 comments on commit 49e0dc4

Please sign in to comment.