From 46af5b200921e7586841f6bc89eec10a263bda42 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 7 Dec 2023 16:43:20 +0100 Subject: [PATCH 1/4] feat: Pay lightning invoice with USDP --- mobile/lib/common/application/switch.dart | 9 +- mobile/lib/common/routes.dart | 15 +- .../lib/features/wallet/scanner_screen.dart | 28 +- .../wallet/send/confirm_payment_modal.dart | 143 +++-- .../wallet/send/execute_payment_modal.dart | 149 ++++++ .../lib/features/wallet/send/send_dialog.dart | 38 -- .../wallet/send/send_lightning_screen.dart | 498 ++++++++++++++++++ .../wallet/send/send_onchain_screen.dart | 309 +++++++++++ .../lib/features/wallet/send/send_screen.dart | 222 -------- 9 files changed, 1103 insertions(+), 308 deletions(-) create mode 100644 mobile/lib/features/wallet/send/execute_payment_modal.dart delete mode 100644 mobile/lib/features/wallet/send/send_dialog.dart create mode 100644 mobile/lib/features/wallet/send/send_lightning_screen.dart create mode 100644 mobile/lib/features/wallet/send/send_onchain_screen.dart delete mode 100644 mobile/lib/features/wallet/send/send_screen.dart diff --git a/mobile/lib/common/application/switch.dart b/mobile/lib/common/application/switch.dart index b1f12518b..0a8ff15fa 100644 --- a/mobile/lib/common/application/switch.dart +++ b/mobile/lib/common/application/switch.dart @@ -4,8 +4,11 @@ import 'package:get_10101/common/color.dart'; class TenTenOneSwitch extends StatefulWidget { final bool value; final ValueChanged onChanged; + final bool showDisabled; - const TenTenOneSwitch({Key? key, required this.value, required this.onChanged}) : super(key: key); + const TenTenOneSwitch( + {Key? key, required this.value, required this.onChanged, this.showDisabled = true}) + : super(key: key); @override State createState() => _TenTenOneSwitchState(); @@ -47,7 +50,9 @@ class _TenTenOneSwitchState extends State with SingleTickerProv borderRadius: BorderRadius.circular(24.0), color: _circleAnimation!.value == Alignment.centerLeft ? tenTenOnePurple.shade300 - : tenTenOnePurple.shade100), + : widget.showDisabled + ? tenTenOnePurple.shade100 + : tenTenOnePurple.shade300), child: Padding( padding: const EdgeInsets.only(top: 6.0, bottom: 6.0, left: 5.0, right: 5.0), child: Container( diff --git a/mobile/lib/common/routes.dart b/mobile/lib/common/routes.dart index 1c36269e8..b4a1b302d 100644 --- a/mobile/lib/common/routes.dart +++ b/mobile/lib/common/routes.dart @@ -3,6 +3,8 @@ import 'package:get_10101/common/global_keys.dart'; import 'package:get_10101/common/settings/channel_screen.dart'; import 'package:get_10101/common/status_screen.dart'; import 'package:get_10101/features/wallet/domain/destination.dart'; +import 'package:get_10101/features/wallet/send/send_lightning_screen.dart'; +import 'package:get_10101/features/wallet/send/send_onchain_screen.dart'; import 'package:get_10101/features/welcome/loading_screen.dart'; import 'package:get_10101/common/scaffold_with_nav_bar.dart'; import 'package:get_10101/common/settings/app_info_screen.dart'; @@ -19,7 +21,6 @@ import 'package:get_10101/features/wallet/receive_screen.dart'; import 'package:get_10101/features/wallet/scanner_screen.dart'; import 'package:get_10101/features/welcome/seed_import_screen.dart'; import 'package:get_10101/common/settings/seed_screen.dart'; -import 'package:get_10101/features/wallet/send/send_screen.dart'; import 'package:get_10101/features/wallet/wallet_screen.dart'; import 'package:get_10101/features/welcome/welcome_screen.dart'; import 'package:go_router/go_router.dart'; @@ -149,11 +150,19 @@ GoRouter createRoutes() { }, routes: [ GoRoute( - path: SendScreen.subRouteName, + path: SendOnChainScreen.subRouteName, // Use root navigator so the screen overlays the application shell parentNavigatorKey: rootNavigatorKey, builder: (BuildContext context, GoRouterState state) { - return SendScreen(destination: state.extra as Destination); + return SendOnChainScreen(destination: state.extra as OnChainAddress); + }, + ), + GoRoute( + path: SendLightningScreen.subRouteName, + // Use root navigator so the screen overlays the application shell + parentNavigatorKey: rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) { + return SendLightningScreen(destination: state.extra as LightningInvoice); }, ), GoRoute( diff --git a/mobile/lib/features/wallet/scanner_screen.dart b/mobile/lib/features/wallet/scanner_screen.dart index a06d6ee36..5ee61f184 100644 --- a/mobile/lib/features/wallet/scanner_screen.dart +++ b/mobile/lib/features/wallet/scanner_screen.dart @@ -6,7 +6,9 @@ import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get_10101/common/color.dart'; import 'package:get_10101/features/wallet/domain/destination.dart'; -import 'package:get_10101/features/wallet/send/send_screen.dart'; +import 'package:get_10101/features/wallet/domain/wallet_type.dart'; +import 'package:get_10101/features/wallet/send/send_lightning_screen.dart'; +import 'package:get_10101/features/wallet/send/send_onchain_screen.dart'; import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; import 'package:get_10101/features/wallet/wallet_screen.dart'; import 'package:get_10101/logger/logger.dart'; @@ -113,7 +115,17 @@ class _ScannerScreenState extends State { .then((destination) { _destination = destination; if (_formKey.currentState!.validate()) { - GoRouter.of(context).go(SendScreen.route, extra: destination); + switch (destination!.getWalletType()) { + case WalletType.lightning: + GoRouter.of(context) + .go(SendLightningScreen.route, extra: destination); + case WalletType.onChain: + GoRouter.of(context) + .go(SendOnChainScreen.route, extra: destination); + case WalletType.stable: + GoRouter.of(context) + .go(SendLightningScreen.route, extra: destination); + } } }); } @@ -181,7 +193,17 @@ class _ScannerScreenState extends State { _destination = destination; if (_formKey.currentState!.validate()) { - GoRouter.of(context).go(SendScreen.route, extra: destination); + switch (destination!.getWalletType()) { + case WalletType.lightning: + GoRouter.of(context) + .go(SendLightningScreen.route, extra: destination); + case WalletType.onChain: + GoRouter.of(context) + .go(SendOnChainScreen.route, extra: destination); + case WalletType.stable: + GoRouter.of(context) + .go(SendLightningScreen.route, extra: destination); + } } }); }, diff --git a/mobile/lib/features/wallet/send/confirm_payment_modal.dart b/mobile/lib/features/wallet/send/confirm_payment_modal.dart index 0cd915d8c..f7b22c11b 100644 --- a/mobile/lib/features/wallet/send/confirm_payment_modal.dart +++ b/mobile/lib/features/wallet/send/confirm_payment_modal.dart @@ -2,18 +2,24 @@ import 'package:flutter/material.dart'; import 'package:get_10101/common/color.dart'; import 'package:get_10101/common/domain/model.dart'; import 'package:get_10101/common/snack_bar.dart'; +import 'package:get_10101/features/trade/domain/direction.dart'; +import 'package:get_10101/features/trade/domain/leverage.dart'; +import 'package:get_10101/features/trade/submit_order_change_notifier.dart'; +import 'package:get_10101/features/trade/trade_value_change_notifier.dart'; import 'package:get_10101/features/wallet/application/util.dart'; import 'package:get_10101/features/wallet/domain/destination.dart'; import 'package:get_10101/features/wallet/domain/wallet_type.dart'; +import 'package:get_10101/features/wallet/send/execute_payment_modal.dart'; import 'package:get_10101/features/wallet/send/payment_sent_change_notifier.dart'; -import 'package:get_10101/features/wallet/send/send_dialog.dart'; import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:slide_to_confirm/slide_to_confirm.dart'; -void showConfirmPaymentModal(BuildContext context, Destination destination, Amount? amount) { +void showConfirmPaymentModal( + BuildContext context, Destination destination, bool payWithUsdp, Amount sats, Amount usdp) { showModalBottomSheet( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( @@ -27,45 +33,110 @@ void showConfirmPaymentModal(BuildContext context, Destination destination, Amou builder: (BuildContext context) { return SingleChildScrollView( child: SizedBox( - height: 320, + height: 420, child: Scaffold( body: ConfirmPayment( + payWithUsdp: payWithUsdp, destination: destination, - amount: amount, + sats: sats, + usdp: usdp, )))); }); } class ConfirmPayment extends StatelessWidget { final Destination destination; - final Amount? amount; + final bool payWithUsdp; + final Amount sats; + final Amount usdp; - const ConfirmPayment({super.key, required this.destination, this.amount}); + const ConfirmPayment( + {super.key, + required this.destination, + required this.payWithUsdp, + required this.sats, + required this.usdp}); @override Widget build(BuildContext context) { final walletService = context.read().service; + final submitOderChangeNotifier = context.read(); + final formatter = NumberFormat("#,###,##0.00", "en"); - final amt = destination.amount.sats > 0 ? destination.amount : amount!; + final tradeValuesChangeNotifier = context.watch(); + + final tradeValues = tradeValuesChangeNotifier.fromDirection(Direction.long); + tradeValues.updateLeverage(Leverage(1)); + + Amount amt = destination.amount; + if (destination.amount.sats == 0) { + if (payWithUsdp) { + // if the destination does not specify an amount and we ar paying with the usdp balance we + // calculate the amount from the quantity point of view. + tradeValues.updateQuantity(usdp); + amt = tradeValues.margin!; + } else { + // Otherwise it is a regular lightning payment and we just pay the given amount. + amt = sats; + } + } else { + // if the amount is set on the invoice we need to pay the amount no matter what. That might + // lead to the usdp amount to jump by one dollar depending on the current bid price + tradeValues.updateMargin(destination.amount); + } return SafeArea( - child: Padding( + child: Container( + color: Colors.white, padding: const EdgeInsets.only(left: 20.0, top: 35.0, right: 20.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("Destination:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 2), - Text(truncateWithEllipsis(32, destination.raw), style: const TextStyle(fontSize: 16)), - const SizedBox(height: 20), - const Text("Payee:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 2), - Text(truncateWithEllipsis(32, destination.payee), style: const TextStyle(fontSize: 16)), - const SizedBox(height: 20), - const Text("Amount:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 2), - Text(amt.toString(), style: const TextStyle(fontSize: 16)), - const SizedBox(height: 25), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Summary", style: TextStyle(fontSize: 20)), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: tenTenOnePurple.shade200.withOpacity(0.1), + borderRadius: BorderRadius.circular(8)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text("Amount", style: TextStyle(color: Colors.grey, fontSize: 16)), + const SizedBox(height: 5), + Visibility( + visible: payWithUsdp, + replacement: Text(amt.sats == 0 ? "Max" : amt.toString(), + style: const TextStyle(fontSize: 16)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("~ \$ ${formatter.format(tradeValues.quantity?.toInt ?? 0)}", + style: const TextStyle(fontSize: 16)), + Text(amt.toString(), + style: const TextStyle(fontSize: 16, color: Colors.grey)) + ], + )), + const Divider(height: 40, indent: 0, endIndent: 0), + const Text("Destination", style: TextStyle(color: Colors.grey, fontSize: 16)), + const SizedBox(height: 5), + Text(truncateWithEllipsis(26, destination.raw), + style: const TextStyle(fontSize: 16)), + const Divider(height: 40, indent: 0, endIndent: 0), + const Text("Payee", style: TextStyle(color: Colors.grey, fontSize: 16)), + const SizedBox(height: 5), + Text(truncateWithEllipsis(26, destination.payee), + style: const TextStyle(fontSize: 16)), + const SizedBox(height: 10), + ], + ), + ) + ], + ), + const SizedBox(height: 15), ConfirmationSlider( text: "Swipe to confirm", textStyle: const TextStyle(color: Colors.black87), @@ -77,30 +148,22 @@ class ConfirmPayment extends StatelessWidget { size: 20, ), onConfirmation: () async { - context.read().waitForPayment(); GoRouter.of(context).pop(); final messenger = ScaffoldMessenger.of(context); if (destination.getWalletType() == WalletType.lightning) { - showDialog( - context: context, - useRootNavigator: true, - barrierDismissible: false, // Prevent user from leaving - builder: (BuildContext context) { - return SendDialog(destination: destination, amount: amt); - }); - } - - walletService.sendPayment(destination, amt).then((value) { - if (destination.getWalletType() == WalletType.onChain) { - GoRouter.of(context).pop(); + context.read().waitForPayment(); + if (payWithUsdp) { + submitOderChangeNotifier.submitPendingOrder(tradeValues, PositionAction.open); } - }).catchError((error) { - logger.e("Failed to send payment: $error"); - if (destination.getWalletType() == WalletType.onChain) { + showExecuteUsdpPaymentModal(context, destination, amt, payWithUsdp); + } else { + walletService.sendPayment(destination, amt).then((value) { + GoRouter.of(context).pop(); + }).catchError((error) { + logger.e("Failed to send payment: $error"); showSnackBar(messenger, error.toString()); - } - context.read().failPayment(); - }); + }); + } }) ], ), diff --git a/mobile/lib/features/wallet/send/execute_payment_modal.dart b/mobile/lib/features/wallet/send/execute_payment_modal.dart new file mode 100644 index 000000000..03252960a --- /dev/null +++ b/mobile/lib/features/wallet/send/execute_payment_modal.dart @@ -0,0 +1,149 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:get_10101/common/color.dart'; +import 'package:get_10101/common/domain/model.dart'; +import 'package:get_10101/features/trade/submit_order_change_notifier.dart'; +import 'package:get_10101/features/wallet/domain/destination.dart'; +import 'package:get_10101/features/wallet/send/payment_sent_change_notifier.dart'; +import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; +import 'package:get_10101/features/wallet/wallet_screen.dart'; +import 'package:get_10101/logger/logger.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +void showExecuteUsdpPaymentModal( + BuildContext context, Destination destination, Amount amount, bool payWithUsdp) { + showModalBottomSheet( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + clipBehavior: Clip.antiAlias, + isScrollControlled: true, + useRootNavigator: false, + isDismissible: false, + context: context, + builder: (BuildContext context) { + return SingleChildScrollView( + child: SizedBox( + height: 300, + child: Scaffold( + body: ExecuteUsdpPayment( + amount: amount, destination: destination, payWithUsdp: payWithUsdp)))); + }); +} + +class ExecuteUsdpPayment extends StatefulWidget { + final Destination destination; + final Amount amount; + final bool payWithUsdp; + + const ExecuteUsdpPayment( + {super.key, required this.amount, required this.payWithUsdp, required this.destination}); + + @override + State createState() => _ExecuteUsdpPaymentState(); +} + +class _ExecuteUsdpPaymentState extends State { + Timer? _timeout; + bool timeout = false; + bool sent = false; + + @override + void dispose() { + _timeout?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final walletService = context.read().service; + final paymentChangeNotifier = context.watch(); + final pendingOrder = context.watch().pendingOrder; + + _timeout ??= Timer(const Duration(seconds: 30), () { + setState(() => timeout = true); + }); + + Widget icon = const SizedBox( + width: 60, + height: 60, + child: CircularProgressIndicator(color: tenTenOnePurple), + ); + String text = ""; + + if ((pendingOrder?.state == PendingOrderState.orderFilled || !widget.payWithUsdp) && !sent) { + logger.d("Order has been filled, attempting to send payment"); + walletService.sendPayment(widget.destination, widget.amount).catchError((error) { + logger.e("Failed to send payment: $error"); + context.read().failPayment(); + }).whenComplete(() => setState(() => sent = true)); + } + + switch (paymentChangeNotifier.getPaymentStatus()) { + case PaymentStatus.pending: + { + if (pendingOrder?.state != PendingOrderState.orderFilled && widget.payWithUsdp) { + text = "Swapping to sats"; + } else { + text = "Sending payment"; + } + } + case PaymentStatus.success: + { + icon = const Icon(FontAwesomeIcons.solidCircleCheck, color: Colors.green, size: 60); + text = "Sent"; + } + case PaymentStatus.failed: + { + icon = Icon(FontAwesomeIcons.circleExclamation, color: Colors.red[600], size: 60); + text = "Something went wrong"; + } + } + + return SafeArea( + child: Container( + color: Colors.white, + padding: const EdgeInsets.all(20), + child: Center( + child: Column( + children: [ + const SizedBox(height: 25), + icon, + const SizedBox(height: 25), + Text(text, style: const TextStyle(fontSize: 22)), + const Spacer(), + Visibility( + visible: timeout || + [PaymentStatus.success, PaymentStatus.failed] + .contains(paymentChangeNotifier.getPaymentStatus()), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: ElevatedButton( + onPressed: () => GoRouter.of(context).go(WalletScreen.route), + style: ButtonStyle( + padding: + MaterialStateProperty.all(const EdgeInsets.all(15)), + backgroundColor: MaterialStateProperty.resolveWith((states) { + return tenTenOnePurple; + }), + shape: MaterialStateProperty.resolveWith((states) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: const BorderSide(color: tenTenOnePurple)); + })), + child: const Text( + "Done", + style: TextStyle(fontSize: 18, color: Colors.white), + )), + )) + ], + ), + )), + ); + } +} diff --git a/mobile/lib/features/wallet/send/send_dialog.dart b/mobile/lib/features/wallet/send/send_dialog.dart deleted file mode 100644 index ea679c7a2..000000000 --- a/mobile/lib/features/wallet/send/send_dialog.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get_10101/common/domain/background_task.dart'; -import 'package:get_10101/common/domain/model.dart'; -import 'package:get_10101/common/task_status_dialog.dart'; -import 'package:get_10101/common/value_data_row.dart'; -import 'package:get_10101/features/wallet/domain/destination.dart'; -import 'package:get_10101/features/wallet/send/payment_sent_change_notifier.dart'; -import 'package:get_10101/features/wallet/wallet_screen.dart'; -import 'package:provider/provider.dart'; - -class SendDialog extends StatelessWidget { - final Destination destination; - final Amount amount; - - const SendDialog({super.key, required this.destination, required this.amount}); - - @override - Widget build(BuildContext context) { - final paymentChangeNotifier = context.watch(); - - final content = ValueDataRow(type: ValueType.amount, value: amount, label: "Amount"); - - switch (paymentChangeNotifier.getPaymentStatus()) { - case PaymentStatus.pending: - return TaskStatusDialog( - title: "Sending payment", status: TaskStatus.pending, content: content); - case PaymentStatus.failed: - return TaskStatusDialog( - title: "Sending payment", status: TaskStatus.failed, content: content); - case PaymentStatus.success: - return TaskStatusDialog( - title: "Sending payment", - status: TaskStatus.success, - content: content, - navigateToRoute: WalletScreen.route); - } - } -} diff --git a/mobile/lib/features/wallet/send/send_lightning_screen.dart b/mobile/lib/features/wallet/send/send_lightning_screen.dart new file mode 100644 index 000000000..58c421138 --- /dev/null +++ b/mobile/lib/features/wallet/send/send_lightning_screen.dart @@ -0,0 +1,498 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:get_10101/common/amount_text.dart'; +import 'package:get_10101/common/application/channel_info_service.dart'; +import 'package:get_10101/common/application/switch.dart'; +import 'package:get_10101/common/color.dart'; +import 'package:get_10101/common/domain/channel.dart'; +import 'package:get_10101/common/domain/model.dart'; +import 'package:get_10101/features/trade/domain/direction.dart'; +import 'package:get_10101/features/trade/domain/leverage.dart'; +import 'package:get_10101/features/trade/position_change_notifier.dart'; +import 'package:get_10101/features/trade/trade_value_change_notifier.dart'; +import 'package:get_10101/features/wallet/application/util.dart'; +import 'package:get_10101/features/wallet/application/wallet_service.dart'; +import 'package:get_10101/features/wallet/domain/destination.dart'; +import 'package:get_10101/features/wallet/domain/wallet_type.dart'; +import 'package:get_10101/features/wallet/send/confirm_payment_modal.dart'; +import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; +import 'package:get_10101/features/wallet/wallet_screen.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class SendLightningScreen extends StatefulWidget { + static const route = "${WalletScreen.route}/$subRouteName"; + static const subRouteName = "send-lightning"; + + final LightningInvoice destination; + + const SendLightningScreen({super.key, required this.destination}); + + @override + State createState() => _SendLightningScreenState(); +} + +class _SendLightningScreenState extends State { + final _satsFormKey = GlobalKey(); + final _usdpFormKey = GlobalKey(); + + bool _payWithUsdp = false; + + ChannelInfo? channelInfo; + + Amount _satsAmount = Amount.zero(); + Amount _usdpAmount = Amount.zero(); + + final TextEditingController _satsController = TextEditingController(); + final TextEditingController _usdpController = TextEditingController(); + + @override + void initState() { + super.initState(); + final ChannelInfoService channelInfoService = context.read(); + final WalletService walletService = context.read().service; + final tradeValueChangeNotifier = context.read(); + init(channelInfoService, walletService, tradeValueChangeNotifier); + } + + @override + void dispose() { + super.dispose(); + _satsController.dispose(); + _usdpController.dispose(); + } + + Future init(ChannelInfoService channelInfoService, WalletService walletService, + TradeValuesChangeNotifier tradeValuesChangeNotifier) async { + channelInfo = await channelInfoService.getChannelInfo(); + setState(() { + _satsAmount = widget.destination.amount; + _satsController.text = _satsAmount.formatted(); + + final tradeValues = tradeValuesChangeNotifier.fromDirection(Direction.long); + tradeValues.updateLeverage(Leverage(1)); + tradeValues.updateMargin(_satsAmount); + + _usdpAmount = tradeValues.quantity ?? Amount.zero(); + _usdpController.text = _usdpAmount.formatted(); + }); + } + + @override + Widget build(BuildContext context) { + final positionChangeNotifier = context.read(); + final tradeValuesChangeNotifier = context.watch(); + + final formatter = NumberFormat("#,###,##0.00", "en"); + + final usdpBalance = positionChangeNotifier.getStableUSDAmountInFiat(); + final lightningBalance = getLightningBalance(); + + return Scaffold( + body: SafeArea( + child: Container( + margin: const EdgeInsets.all(20.0), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Stack( + children: [ + GestureDetector( + child: Container( + alignment: AlignmentDirectional.topStart, + decoration: BoxDecoration( + color: Colors.transparent, borderRadius: BorderRadius.circular(10)), + width: 70, + child: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 22, + )), + onTap: () => GoRouter.of(context).pop(), + ), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Send", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + color: tenTenOnePurple.shade200.withOpacity(0.1)), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Text( + "Pay to:", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + const SizedBox(height: 2), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(truncateWithEllipsis(18, widget.destination.raw), + overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16)), + Container( + padding: const EdgeInsets.only(left: 10, right: 10, top: 5, bottom: 5), + decoration: BoxDecoration( + color: tenTenOnePurple, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.bolt, size: 14, color: Colors.white), + Text("Lightning", style: TextStyle(fontSize: 14, color: Colors.white)) + ], + ), + ) + ]) + ]), + ), + const SizedBox(height: 25), + const Text( + "Enter amount", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + const SizedBox(height: 10), + Container( + margin: const EdgeInsets.only(left: 40, right: 40), + child: Visibility( + maintainState: true, + visible: !_payWithUsdp, + child: buildSatsForm( + tradeValuesChangeNotifier, lightningBalance.$1, lightningBalance.$2))), + Container( + margin: const EdgeInsets.only(left: 40, right: 40), + child: Visibility( + maintainState: true, + visible: _payWithUsdp, + child: buildUsdpForm(tradeValuesChangeNotifier, usdpBalance), + ), + ), + const SizedBox(height: 25), + Visibility( + visible: widget.destination.description != "", + child: Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 20), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + color: tenTenOnePurple.shade200.withOpacity(0.1)), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const Text( + "Memo:", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + const SizedBox(height: 5), + Text(widget.destination.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: const TextStyle(fontSize: 16)) + ]), + ), + const SizedBox(height: 15), + ], + )), + const Text( + "Pay from:", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + const SizedBox(height: 5), + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + color: tenTenOnePurple.shade200.withOpacity(0.1)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () => setState(() => _payWithUsdp = false), + child: Container( + padding: const EdgeInsets.all(20), + child: Opacity( + opacity: _payWithUsdp ? 0.5 : 1.0, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Icon(Icons.bolt, size: 18), + Text("Lightning", style: TextStyle(fontSize: 18)) + ]), + const SizedBox(height: 5), + Text(lightningBalance.$2.toString(), textAlign: TextAlign.start), + ])), + ), + ), + TenTenOneSwitch( + value: _payWithUsdp, + showDisabled: false, + onChanged: (value) => setState(() => _payWithUsdp = value)), + GestureDetector( + onTap: () => setState(() => _payWithUsdp = true), + child: Container( + padding: const EdgeInsets.all(20), + child: Opacity( + opacity: _payWithUsdp ? 1.0 : 0.5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text("USD-P", style: TextStyle(fontSize: 18)), + const SizedBox(height: 5), + Text(formatter.format(usdpBalance), textAlign: TextAlign.end), + ], + ), + ), + ), + ) + ], + ), + ), + const SizedBox(height: 2), + const Spacer(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: ElevatedButton( + onPressed: (_payWithUsdp + ? (_usdpFormKey.currentState?.validate() ?? false) + : (_satsFormKey.currentState?.validate() ?? false)) + ? () => showConfirmPaymentModal( + context, widget.destination, _payWithUsdp, _satsAmount, _usdpAmount) + : null, + style: ButtonStyle( + padding: MaterialStateProperty.all(const EdgeInsets.all(15)), + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return tenTenOnePurple.shade100; + } else { + return tenTenOnePurple; + } + }), + shape: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: BorderSide(color: tenTenOnePurple.shade100), + ); + } else { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: const BorderSide(color: tenTenOnePurple), + ); + } + })), + child: const Text( + "Pay", + style: TextStyle(fontSize: 18, color: Colors.white), + )), + ) + ]), + ), + ), + ); + } + + Form buildUsdpForm(TradeValuesChangeNotifier tradeValuesChangeNotifier, double usdpBalance) { + return Form( + key: _usdpFormKey, + child: FormField( + validator: (val) { + final amount = _usdpAmount; + + if (amount.sats <= 0) { + return "Amount cannot be 0"; + } + + if (amount.sats < 0) { + return "Amount cannot be negative"; + } + + if (amount.sats > usdpBalance) { + return "Not enough funds."; + } + + return null; + }, + builder: (FormFieldState formFieldState) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextField( + textAlign: TextAlign.center, + controller: _usdpController, + decoration: const InputDecoration( + hintText: "0.00", + hintStyle: TextStyle(fontSize: 40), + enabledBorder: InputBorder.none, + border: InputBorder.none, + errorBorder: InputBorder.none, + suffix: Text( + "\$", + style: TextStyle(fontSize: 16), + )), + style: const TextStyle(fontSize: 40), + textAlignVertical: TextAlignVertical.center, + enabled: widget.destination.amount.sats == 0, + onChanged: (value) { + setState(() { + _usdpAmount = Amount.parseAmount(value); + final tradeValues = tradeValuesChangeNotifier.fromDirection(Direction.long); + tradeValues.updateQuantity(_usdpAmount); + _usdpController.text = _usdpAmount.formatted(); + + _satsAmount = tradeValues.margin ?? Amount.zero(); + _satsController.text = _satsAmount.formatted(); + _satsController.selection = + TextSelection.collapsed(offset: _satsController.text.length); + }); + }, + ), + Visibility( + visible: formFieldState.hasError, + replacement: Container(margin: const EdgeInsets.only(top: 30, bottom: 10)), + child: Container( + decoration: BoxDecoration( + color: Colors.redAccent.shade100.withOpacity(0.1), + border: Border.all(color: Colors.red), + borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.all(10), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const Icon(Icons.info_outline, color: Colors.black87, size: 18), + const SizedBox(width: 5), + Text( + formFieldState.errorText ?? "", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ], + ), + ), + ) + ], + ); + }, + ), + ); + } + + Form buildSatsForm( + TradeValuesChangeNotifier tradeValuesChangeNotifier, Amount balance, Amount useableBalance) { + return Form( + key: _satsFormKey, + child: FormField( + validator: (val) { + final amount = _satsAmount; + + if (amount.sats <= 0 && widget.destination.getWalletType() == WalletType.lightning) { + return "Amount cannot be 0"; + } + + if (amount.sats < 0) { + return "Amount cannot be negative"; + } + + if (amount.sats > balance.sats) { + return "Not enough funds."; + } + + if (amount.sats > useableBalance.sats) { + return "Not enough funds. ${formatSats(balance.sub(useableBalance))} have to remain."; + } + + return null; + }, + builder: (FormFieldState formFieldState) { + return Column( + children: [ + TextField( + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: "0.00", + hintStyle: TextStyle(fontSize: 40), + enabledBorder: InputBorder.none, + border: InputBorder.none, + errorBorder: InputBorder.none, + suffix: Text( + "sats", + style: TextStyle(fontSize: 16), + )), + style: const TextStyle(fontSize: 40), + textAlignVertical: TextAlignVertical.center, + enabled: widget.destination.amount.sats == 0, + controller: _satsController, + onChanged: (value) { + setState(() { + _satsAmount = Amount.parseAmount(value); + final tradeValues = tradeValuesChangeNotifier.fromDirection(Direction.long); + tradeValues.updateMargin(_satsAmount); + _satsController.text = _satsAmount.formatted(); + + _usdpAmount = tradeValues.quantity ?? Amount.zero(); + _usdpController.text = _usdpAmount.formatted(); + _usdpController.selection = + TextSelection.collapsed(offset: _usdpController.text.length); + }); + }, + ), + Visibility( + visible: formFieldState.hasError, + replacement: Container(margin: const EdgeInsets.only(top: 30, bottom: 10)), + child: Container( + decoration: BoxDecoration( + color: Colors.redAccent.shade100.withOpacity(0.1), + border: Border.all(color: Colors.red), + borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.all(10), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const Icon(Icons.info_outline, color: Colors.black87, size: 18), + const SizedBox(width: 5), + Text( + formFieldState.errorText ?? "", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ], + ), + ), + ) + ], + ); + }, + ), + ); + } + + (Amount, Amount) getLightningBalance() { + final walletInfo = context.read().walletInfo; + final ChannelInfoService channelInfoService = context.read(); + Amount initialReserve = channelInfoService.getInitialReserve(); + int channelReserve = channelInfo?.reserve.sats ?? initialReserve.sats; + int balance = walletInfo.balances.lightning.sats; + + return (Amount(balance), Amount(max(balance - channelReserve, 0))); + } +} diff --git a/mobile/lib/features/wallet/send/send_onchain_screen.dart b/mobile/lib/features/wallet/send/send_onchain_screen.dart new file mode 100644 index 000000000..b4b44a9a8 --- /dev/null +++ b/mobile/lib/features/wallet/send/send_onchain_screen.dart @@ -0,0 +1,309 @@ +import 'package:flutter/material.dart'; +import 'package:get_10101/common/application/channel_info_service.dart'; +import 'package:get_10101/common/color.dart'; +import 'package:get_10101/common/domain/channel.dart'; +import 'package:get_10101/common/domain/model.dart'; +import 'package:get_10101/features/trade/domain/direction.dart'; +import 'package:get_10101/features/trade/trade_value_change_notifier.dart'; +import 'package:get_10101/features/wallet/application/util.dart'; +import 'package:get_10101/features/wallet/application/wallet_service.dart'; +import 'package:get_10101/features/wallet/domain/destination.dart'; +import 'package:get_10101/features/wallet/send/confirm_payment_modal.dart'; +import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; +import 'package:get_10101/features/wallet/wallet_screen.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +class SendOnChainScreen extends StatefulWidget { + static const route = "${WalletScreen.route}/$subRouteName"; + static const subRouteName = "send-onchain"; + + final OnChainAddress destination; + + const SendOnChainScreen({super.key, required this.destination}); + + @override + State createState() => _SendOnChainScreenState(); +} + +class _SendOnChainScreenState extends State { + final _formKey = GlobalKey(); + + ChannelInfo? channelInfo; + + Amount _amount = Amount.zero(); + + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + final ChannelInfoService channelInfoService = context.read(); + final WalletService walletService = context.read().service; + init(channelInfoService, walletService); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + Future init(ChannelInfoService channelInfoService, WalletService walletService) async { + channelInfo = await channelInfoService.getChannelInfo(); + setState(() { + _amount = widget.destination.amount; + _controller.text = _amount.formatted(); + }); + } + + @override + Widget build(BuildContext context) { + final walletInfo = context.read().walletInfo; + final balance = walletInfo.balances.onChain; + + final tradeValueChangeNotifier = context.read(); + + return Scaffold( + body: Form( + key: _formKey, + child: SafeArea( + child: Container( + margin: const EdgeInsets.all(20.0), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Stack( + children: [ + GestureDetector( + child: Container( + alignment: AlignmentDirectional.topStart, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10)), + width: 70, + child: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 22, + )), + onTap: () => GoRouter.of(context).pop(), + ), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Send", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + color: Colors.orange.shade300.withOpacity(0.1)), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Text( + "Send to:", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + const SizedBox(height: 2), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(truncateWithEllipsis(18, widget.destination.raw), + overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16)), + Container( + padding: const EdgeInsets.only(left: 10, right: 10, top: 5, bottom: 5), + decoration: BoxDecoration( + color: Colors.orange, + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.currency_bitcoin, size: 14, color: Colors.white), + SizedBox(width: 5), + Text("On-Chain", style: TextStyle(fontSize: 14, color: Colors.white)) + ], + ), + ) + ]) + ]), + ), + const SizedBox(height: 25), + const Text( + "Enter amount (0 to send the maximum)", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + const SizedBox(height: 10), + Container( + margin: const EdgeInsets.only(left: 40, right: 40), + child: FormField( + validator: (val) { + final amount = _amount; + + if (amount.sats < 0) { + return "Amount cannot be negative"; + } + + if (amount.sats > balance.sats) { + return "Not enough funds."; + } + + return null; + }, + builder: (FormFieldState formFieldState) { + return Column( + children: [ + TextField( + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: "0.00", + hintStyle: TextStyle(fontSize: 40), + enabledBorder: InputBorder.none, + border: InputBorder.none, + errorBorder: InputBorder.none, + suffix: Text( + "sats", + style: TextStyle(fontSize: 16), + )), + style: const TextStyle(fontSize: 40), + textAlignVertical: TextAlignVertical.center, + enabled: widget.destination.amount.sats == 0, + controller: _controller, + onChanged: (value) { + setState(() { + _amount = Amount.parseAmount(value); + final tradeValues = + tradeValueChangeNotifier.fromDirection(Direction.short); + tradeValues.updateMargin(_amount); + _controller.text = _amount.formatted(); + }); + }, + ), + Visibility( + visible: formFieldState.hasError, + replacement: + Container(margin: const EdgeInsets.only(top: 30, bottom: 10)), + child: Container( + decoration: BoxDecoration( + color: Colors.redAccent.shade100.withOpacity(0.1), + border: Border.all(color: Colors.red), + borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.all(10), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const Icon(Icons.info_outline, color: Colors.black87, size: 18), + const SizedBox(width: 5), + Text( + formFieldState.errorText ?? "", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ], + ), + ), + ) + ], + ); + }, + )), + Visibility( + visible: widget.destination.description != "", + child: Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 20), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + color: Colors.orange.shade200.withOpacity(0.1)), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + const Text( + "Memo:", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + const SizedBox(height: 5), + Text(widget.destination.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: const TextStyle(fontSize: 16)) + ]), + ), + const SizedBox(height: 15), + ], + )), + const SizedBox(height: 35), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + color: Colors.orange.shade300.withOpacity(0.1)), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + const Text("Available Balance", + overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14)), + Text(balance.toString(), + overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14)), + ]) + ]), + ), + const Spacer(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: ElevatedButton( + onPressed: (_formKey.currentState?.validate() ?? false) + ? () => showConfirmPaymentModal( + context, widget.destination, false, _amount, _amount) + : null, + style: ButtonStyle( + padding: MaterialStateProperty.all(const EdgeInsets.all(15)), + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return tenTenOnePurple.shade100; + } else { + return tenTenOnePurple; + } + }), + shape: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: BorderSide(color: tenTenOnePurple.shade100), + ); + } else { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: const BorderSide(color: tenTenOnePurple), + ); + } + })), + child: const Text( + "Send", + style: TextStyle(fontSize: 18, color: Colors.white), + )), + ) + ]), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/features/wallet/send/send_screen.dart b/mobile/lib/features/wallet/send/send_screen.dart deleted file mode 100644 index c4c49ffd2..000000000 --- a/mobile/lib/features/wallet/send/send_screen.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -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/channel.dart'; -import 'package:get_10101/common/domain/model.dart'; -import 'package:get_10101/common/scrollable_safe_area.dart'; -import 'package:get_10101/features/wallet/application/util.dart'; -import 'package:get_10101/features/wallet/application/wallet_service.dart'; -import 'package:get_10101/features/wallet/domain/destination.dart'; -import 'package:get_10101/features/wallet/domain/wallet_type.dart'; -import 'package:get_10101/features/wallet/send/confirm_payment_modal.dart'; -import 'package:get_10101/features/wallet/wallet_change_notifier.dart'; -import 'package:get_10101/features/wallet/wallet_screen.dart'; -import 'package:provider/provider.dart'; - -class SendScreen extends StatefulWidget { - static const route = "${WalletScreen.route}/$subRouteName"; - static const subRouteName = "send"; - - final Destination destination; - - const SendScreen({super.key, required this.destination}); - - @override - State createState() => _SendScreenState(); -} - -class _SendScreenState extends State { - final _formKey = GlobalKey(); - bool _valid = false; - - ChannelInfo? channelInfo; - - Amount _amount = Amount.zero(); - - final TextEditingController _controller = TextEditingController(); - - @override - void initState() { - super.initState(); - final ChannelInfoService channelInfoService = context.read(); - final WalletService walletService = context.read().service; - init(channelInfoService, walletService); - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } - - Future init(ChannelInfoService channelInfoService, WalletService walletService) async { - channelInfo = await channelInfoService.getChannelInfo(); - setState(() { - _amount = widget.destination.amount; - _controller.text = _amount.formatted(); - }); - } - - @override - Widget build(BuildContext context) { - final balance = getBalance(); - - return Scaffold( - appBar: AppBar(title: const Text("Send Funds")), - body: Form( - key: _formKey, - child: ScrollableSafeArea( - child: Container( - padding: const EdgeInsets.all(20.0), - child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text("Destination", style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 2), - InputDecorator( - decoration: InputDecoration( - enabledBorder: - const OutlineInputBorder(borderSide: BorderSide(color: Colors.black12)), - labelStyle: const TextStyle(color: Colors.black87), - filled: true, - fillColor: Colors.grey[50], - ), - child: Text(truncateWithEllipsis(26, widget.destination.raw), - style: const TextStyle(fontSize: 15)), - ), - const SizedBox(height: 15), - const Text("From", style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 2), - OutlinedButton( - onPressed: null, - style: OutlinedButton.styleFrom( - minimumSize: const Size(20, 60), - backgroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(mainAxisAlignment: MainAxisAlignment.start, children: [ - Icon( - widget.destination.getWalletType() == WalletType.lightning - ? Icons.bolt - : Icons.currency_bitcoin, - size: 30), - const SizedBox(width: 5), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.destination.getWalletType() == WalletType.lightning - ? "Lightning" - : "On-chain", - style: const TextStyle(color: Colors.black87, fontSize: 16), - ), - Text(formatSats(balance[widget.destination.getWalletType()]!.$1)) - ], - ), - ]), - const Icon(Icons.arrow_drop_down_sharp, size: 30) - ], - )), - const SizedBox(height: 20), - Visibility( - visible: widget.destination.getWalletType() == WalletType.onChain, - replacement: const Text( - "Amount in sats", - style: TextStyle(fontWeight: FontWeight.bold), - ), - child: const Text( - "Amount in sats (0 to drain the wallet)", - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - const SizedBox(height: 2), - AmountInputField( - controller: _controller, - label: "", - value: _amount, - enabled: widget.destination.amount.sats == 0, - onChanged: (value) { - setState(() { - _amount = Amount.parseAmount(value); - _valid = _formKey.currentState?.validate() ?? false; - }); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return "Amount is mandatory"; - } - - final amount = Amount.parseAmount(value); - - if (amount.sats <= 0 && - widget.destination.getWalletType() == WalletType.lightning) { - return "Amount cannot be 0"; - } - - if (amount.sats < 0) { - return "Amount cannot be negative"; - } - - final bal = balance[widget.destination.getWalletType()]!.$1; - if (amount.sats > bal.sats) { - return "Not enough funds."; - } - - final usebal = balance[widget.destination.getWalletType()]!.$2; - - if (amount.sats > usebal.sats) { - return "Not enough funds. ${formatSats(bal.sub(usebal))} have to remain."; - } - - return null; - }, - ), - const SizedBox(height: 20), - const Text("Note", style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 2), - InputDecorator( - decoration: InputDecoration( - enabledBorder: - const OutlineInputBorder(borderSide: BorderSide(color: Colors.black12)), - labelStyle: const TextStyle(color: Colors.black87), - filled: true, - fillColor: Colors.grey[50], - ), - child: Text(widget.destination.description, style: const TextStyle(fontSize: 15)), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: !_valid - ? null - : () => showConfirmPaymentModal(context, widget.destination, _amount), - child: const Text("Next")), - ], - ), - ) - ]), - ), - ), - ), - ); - } - - Map getBalance() { - final walletInfo = context.read().walletInfo; - final ChannelInfoService channelInfoService = context.read(); - Amount initialReserve = channelInfoService.getInitialReserve(); - int channelReserve = channelInfo?.reserve.sats ?? initialReserve.sats; - int balance = walletInfo.balances.lightning.sats; - return { - WalletType.lightning: (Amount(balance), Amount(max(balance - channelReserve, 0))), - WalletType.onChain: (walletInfo.balances.onChain, walletInfo.balances.onChain) - }; - } -} From 6acd94283f1bfbc678e579569ca86576f4a526b4 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 7 Dec 2023 20:12:14 +0100 Subject: [PATCH 2/4] fix: Do not update stable flag on resize --- coordinator/src/db/positions.rs | 2 -- coordinator/src/node/resize.rs | 5 ----- mobile/native/src/db/models.rs | 3 +-- mobile/native/src/trade/position/mod.rs | 4 +--- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/coordinator/src/db/positions.rs b/coordinator/src/db/positions.rs index fcf6f74a3..314745a75 100644 --- a/coordinator/src/db/positions.rs +++ b/coordinator/src/db/positions.rs @@ -176,7 +176,6 @@ impl Position { liquidation_price: f32, expiry_timestamp: OffsetDateTime, temporary_contract_id: ContractId, - stable: bool, ) -> Result<()> { let affected_rows = diesel::update(positions::table) .filter(positions::trader_pubkey.eq(trader_pubkey.clone())) @@ -194,7 +193,6 @@ impl Position { positions::liquidation_price.eq(liquidation_price), positions::expiry_timestamp.eq(expiry_timestamp), positions::temporary_contract_id.eq(temporary_contract_id.to_hex()), - positions::stable.eq(stable), )) .execute(conn)?; diff --git a/coordinator/src/node/resize.rs b/coordinator/src/node/resize.rs index 3ff024d5f..d93d25e5b 100644 --- a/coordinator/src/node/resize.rs +++ b/coordinator/src/node/resize.rs @@ -174,10 +174,6 @@ impl Node { .dlc_expiry_timestamp .context("No expiry timestamp for resizing trade")?; - let stable = old_position.stable - && leverage_trader == Decimal::ONE - && direction == Direction::Short; - let total_contracts = f32_from_decimal(total_contracts); let leverage_coordinator = f32_from_decimal(leverage_coordinator); let leverage_trader = f32_from_decimal(leverage_trader); @@ -283,7 +279,6 @@ impl Node { f32_from_decimal(liquidation_price_trader), expiry_timestamp, temporary_contract_id, - stable, ) { tracing::error!( channel_id = %channel_details.channel_id.to_hex(), diff --git a/mobile/native/src/db/models.rs b/mobile/native/src/db/models.rs index 14aea60df..20e410636 100644 --- a/mobile/native/src/db/models.rs +++ b/mobile/native/src/db/models.rs @@ -354,7 +354,7 @@ impl Position { creation_timestamp: _, expiry_timestamp, updated_timestamp, - stable, + .. } = position; let affected_rows = diesel::update(positions::table) @@ -369,7 +369,6 @@ impl Position { positions::collateral.eq(collateral), positions::expiry_timestamp.eq(expiry_timestamp), positions::updated_timestamp.eq(updated_timestamp), - positions::stable.eq(stable), )) .execute(conn)?; diff --git a/mobile/native/src/trade/position/mod.rs b/mobile/native/src/trade/position/mod.rs index e4d9d054e..c8c9b044c 100644 --- a/mobile/native/src/trade/position/mod.rs +++ b/mobile/native/src/trade/position/mod.rs @@ -337,8 +337,6 @@ impl Position { .to_sat() }; - let stable = self.stable && order.stable && self.direction == Direction::Short; - let position = Position { leverage: f32_from_decimal(starting_leverage), quantity: contract_diff, @@ -351,7 +349,7 @@ impl Position { expiry, updated: now_timestamp, created: self.created, - stable, + stable: self.stable, }; let fee = order_matching_fee_taker(order.quantity, order_execution_price); From ec8dfcc33c3c8e46a5941733bc32f1d3d3f579f8 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 8 Dec 2023 08:46:18 +0100 Subject: [PATCH 3/4] chore: Replace missing camera permission icon --- mobile/lib/features/wallet/scanner_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/features/wallet/scanner_screen.dart b/mobile/lib/features/wallet/scanner_screen.dart index 5ee61f184..fc8acf4ae 100644 --- a/mobile/lib/features/wallet/scanner_screen.dart +++ b/mobile/lib/features/wallet/scanner_screen.dart @@ -91,7 +91,7 @@ class _ScannerScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(FontAwesomeIcons.ban, size: 55), + Icon(FontAwesomeIcons.eyeSlash, size: 55), SizedBox(height: 20), Text( "To scan a QR code you must give 10101 permission to access the camera.", From 05e96c001f632dda4d81d72cfd961c14c6972970 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 8 Dec 2023 10:00:44 +0100 Subject: [PATCH 4/4] fix: Reduce space between icon and text Otherwise we are running into a rendering overflow on smaller screens (iPhone SE). --- mobile/lib/features/wallet/wallet_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/features/wallet/wallet_screen.dart b/mobile/lib/features/wallet/wallet_screen.dart index 2da4f150f..a8dbfdfc4 100644 --- a/mobile/lib/features/wallet/wallet_screen.dart +++ b/mobile/lib/features/wallet/wallet_screen.dart @@ -59,7 +59,7 @@ class WalletScreen extends StatelessWidget { FontAwesomeIcons.arrowDown, size: 14, ), - SizedBox(width: 10, height: 40), + SizedBox(width: 7, height: 40), Text( 'Receive', style: TextStyle(fontSize: 14, fontWeight: FontWeight.normal), @@ -81,7 +81,7 @@ class WalletScreen extends StatelessWidget { size: 14, ), SizedBox( - width: 10, + width: 7, height: 40, ), Text( @@ -105,7 +105,7 @@ class WalletScreen extends StatelessWidget { size: 14, ), SizedBox( - width: 10, + width: 7, height: 40, ), Text(