From 556286f455813231ba3294a1d71f13ee4fd4c4c1 Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Wed, 5 Jun 2024 16:14:47 +1000 Subject: [PATCH] feat: use mutinynet faucet for channel opening fundings --- .../channel_funding_screen.dart | 68 ++++++++++++++++++- .../wallet/application/faucet_service.dart | 44 ++++++++++-- .../lib/features/wallet/receive_screen.dart | 3 +- 3 files changed, 107 insertions(+), 8 deletions(-) diff --git a/mobile/lib/features/trade/channel_creation_flow/channel_funding_screen.dart b/mobile/lib/features/trade/channel_creation_flow/channel_funding_screen.dart index 94ebfc3d9..90d7af43f 100644 --- a/mobile/lib/features/trade/channel_creation_flow/channel_funding_screen.dart +++ b/mobile/lib/features/trade/channel_creation_flow/channel_funding_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:get_10101/common/bitcoin_balance_field.dart'; import 'package:get_10101/common/color.dart'; import 'package:get_10101/common/countdown.dart'; +import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge; import 'package:get_10101/common/custom_qr_code.dart'; import 'package:get_10101/common/domain/funding_channel_task.dart'; import 'package:get_10101/common/domain/model.dart'; @@ -13,6 +14,7 @@ import 'package:get_10101/features/trade/application/order_service.dart'; import 'package:get_10101/features/trade/channel_creation_flow/channel_configuration_screen.dart'; import 'package:get_10101/features/trade/submit_order_change_notifier.dart'; import 'package:get_10101/features/trade/trade_screen.dart'; +import 'package:get_10101/features/wallet/application/faucet_service.dart'; import 'package:get_10101/features/wallet/application/util.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -61,17 +63,24 @@ class ChannelFunding extends StatefulWidget { class _ChannelFunding extends State { FundingType selectedBox = FundingType.onchain; + /// Should only be true if on signet + bool _showFaucet = false; + bool _isPayInvoiceButtonDisabled = false; + // TODO(holzeis): It would be nicer if this would come directly from the invoice. final expiry = DateTime.timestamp().second + 300; @override Widget build(BuildContext context) { + final bridge.Config config = context.read(); final status = context.watch().status; final orderCreated = status == FundingChannelTaskStatus.orderCreated; final qrCode = switch (selectedBox) { FundingType.lightning => CustomQrCode( - data: widget.funding.paymentRequest ?? "https://x.com/get10101", + data: widget.funding.paymentRequest != null + ? "lightning:${widget.funding.paymentRequest}" + : "https://x.com/get10101", embeddedImage: widget.funding.paymentRequest != null ? const AssetImage("assets/10101_logo_icon_white_background.png") : const AssetImage("assets/coming_soon.png"), @@ -80,7 +89,7 @@ class _ChannelFunding extends State { dimension: 300, ), FundingType.onchain => CustomQrCode( - // TODO: creating a bip21 qr code should be generic once we support other desposit methods + // TODO: creating a bip21 qr code should be generic once we support other deposit methods data: "bitcoin:${widget.funding.bitcoinAddress}?amount=${widget.amount.btc.toString()}", embeddedImage: const AssetImage("assets/10101_logo_icon_white_background.png"), dimension: 300, @@ -196,9 +205,49 @@ class _ChannelFunding extends State { ScaffoldMessenger.of(context), "Copied: ${qrCode.data}"); }); }, + onDoubleTap: (config.network == "regtest" || config.network == "signet") + ? () => setState(() => _showFaucet = !_showFaucet) + : null, child: Padding( padding: const EdgeInsets.all(8.0), - child: qrCode, + child: _showFaucet + ? Column( + children: [ + const SizedBox(height: 125), + OutlinedButton( + onPressed: _isPayInvoiceButtonDisabled + ? null + : () async { + setState( + () => _isPayInvoiceButtonDisabled = true); + final faucetService = + context.read(); + faucetService + .payInvoiceWithFaucet( + qrCode.data, + widget.amount, + config.network, + parseSelectedBox(selectedBox)) + .catchError((error) { + setState( + () => _isPayInvoiceButtonDisabled = false); + showSnackBar(ScaffoldMessenger.of(context), + error.toString()); + }); + }, + style: ElevatedButton.styleFrom( + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(5.0))), + ), + child: config.network == "regtest" + ? const Text("Pay with 10101 faucet") + : const Text("Pay with Mutinynet faucet"), + ), + const SizedBox(height: 125), + ], + ) + : qrCode, ), ), LayoutBuilder( @@ -284,6 +333,7 @@ class _ChannelFunding extends State { onTap: () { setState(() { selectedBox = FundingType.unified; + _showFaucet = false; }); }, ), @@ -296,6 +346,7 @@ class _ChannelFunding extends State { onTap: () { setState(() { selectedBox = FundingType.lightning; + _showFaucet = false; }); }, ), @@ -308,6 +359,7 @@ class _ChannelFunding extends State { onTap: () { setState(() { selectedBox = FundingType.onchain; + _showFaucet = false; }); }, ), @@ -320,6 +372,7 @@ class _ChannelFunding extends State { onTap: () { setState(() { selectedBox = FundingType.external; + _showFaucet = false; }); }, ), @@ -512,3 +565,12 @@ class _RotatingIconState extends State with SingleTickerProviderSt ); } } + +Layer parseSelectedBox(FundingType fundingType) { + switch (fundingType) { + case FundingType.lightning: + return Layer.lightning; + default: + return Layer.onchain; + } +} diff --git a/mobile/lib/features/wallet/application/faucet_service.dart b/mobile/lib/features/wallet/application/faucet_service.dart index c6c2fee8b..57147f40b 100644 --- a/mobile/lib/features/wallet/application/faucet_service.dart +++ b/mobile/lib/features/wallet/application/faucet_service.dart @@ -5,9 +5,12 @@ import 'package:get_10101/common/domain/model.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:http/http.dart' as http; +enum Layer { onchain, lightning } + class FaucetService { /// Pay the provided invoice with our faucet - Future payInvoiceWithFaucet(String bip21Uri, Amount? invoiceAmount, String network) async { + Future payInvoiceWithFaucet( + String bip21Uri, Amount? invoiceAmount, String network, Layer layer) async { final split = bip21Uri.split(":"); final addressAndMaybeAmount = split[1].split("?"); logger.i("Funding $addressAndMaybeAmount"); @@ -18,10 +21,21 @@ class FaucetService { switch (network) { case "regtest": - await payWith10101Faucet(address, amount); + switch (layer) { + case Layer.onchain: + await payWith10101Faucet(address, amount); + case Layer.lightning: + throw Exception("We don't have a regtest faucet for LN"); + } break; case "signet": - await payWithMutinyFaucet(address, amount); + switch (layer) { + case Layer.onchain: + await payWithMutinyOnChainFaucet(address, amount); + case Layer.lightning: + await payWithMutinyLightningFaucet(address); + } + break; default: throw Exception("Invalid network provided $network. Only regtest or signet supported"); @@ -80,7 +94,7 @@ class FaucetService { } } - Future payWithMutinyFaucet(String address, double amountBtc) async { + Future payWithMutinyOnChainFaucet(String address, double amountBtc) async { final url = Uri.parse('https://faucet.mutinynet.com/api/onchain'); final headers = { 'Content-Type': 'application/json', @@ -108,4 +122,26 @@ class FaucetService { throw Exception("Failed funding address ${e.toString()}"); } } + + Future payWithMutinyLightningFaucet(String bolt11) async { + final url = Uri.parse('https://faucet.mutinynet.com/api/lightning'); + final headers = { + 'Content-Type': 'application/json', + 'Origin': 'https://faucet.mutinynet.com', + }; + final body = jsonEncode({'bolt11': bolt11}); + + try { + final response = await http.post(url, headers: headers, body: body); + + if (response.statusCode == 200) { + logger.i('Funding successful ${response.body}'); + } else { + logger.e('Request failed with status: ${response.statusCode} ${response.body}'); + throw Exception("Failed funding address ${response.statusCode} ${response.body}"); + } + } catch (e) { + throw Exception("Failed funding address ${e.toString()}"); + } + } } diff --git a/mobile/lib/features/wallet/receive_screen.dart b/mobile/lib/features/wallet/receive_screen.dart index 67ab63a9b..47f58634f 100644 --- a/mobile/lib/features/wallet/receive_screen.dart +++ b/mobile/lib/features/wallet/receive_screen.dart @@ -99,7 +99,8 @@ class _ReceiveScreenState extends State { setState(() => _isPayInvoiceButtonDisabled = true); final faucetService = context.read(); faucetService - .payInvoiceWithFaucet(rawInvoice(), amount, config.network) + .payInvoiceWithFaucet( + rawInvoice(), amount, config.network, Layer.onchain) .catchError((error) { setState(() => _isPayInvoiceButtonDisabled = false); showSnackBar(ScaffoldMessenger.of(context), error.toString());