diff --git a/lib/bloc/network/network_settings_bloc.dart b/lib/bloc/network/network_settings_bloc.dart index e53801935..c17855f9f 100644 --- a/lib/bloc/network/network_settings_bloc.dart +++ b/lib/bloc/network/network_settings_bloc.dart @@ -1,5 +1,9 @@ +import 'dart:convert'; import 'package:c_breez/bloc/network/network_settings_state.dart'; import 'package:c_breez/config.dart' as lib; +import 'package:c_breez/config.dart'; +import 'package:c_breez/services/injector.dart'; +import 'package:c_breez/utils/blockchain_explorer_utils.dart'; import 'package:c_breez/utils/preferences.dart'; import 'package:logging/logging.dart'; import 'package:http/http.dart' as http; @@ -47,7 +51,7 @@ class NetworkSettingsBloc extends Cubit with HydratedMixin _log.warning("Invalid mempool url: $mempoolUrl"); return false; } - if (!await _testUri(uri)) { + if (!await _testUriSupportsMempoolApi(uri)) { _log.warning("Mempool url is not reachable: $mempoolUrl"); return false; } @@ -83,10 +87,28 @@ class NetworkSettingsBloc extends Cubit with HydratedMixin )); } - Future _testUri(Uri uri) async { + Future get mempoolInstance async { + String? mempoolInstance = await ServiceInjector().preferences.getMempoolSpaceUrl(); + if (mempoolInstance == null) { + final config = await Config.instance(); + mempoolInstance = config.defaultMempoolUrl; + } + return mempoolInstance; + } + + Future _testUriSupportsMempoolApi(Uri uri) async { + // We need to make sure that the mempool rest api is supported + // as the sdk depends on it. + final mempoolUri = + Uri.tryParse(BlockChainExplorerUtils().formatRecommendedFeesUrl(mempoolInstance: uri.toString())); + if (mempoolUri == null) return false; try { - final response = await _httpClient.get(uri); - return response.statusCode < 400; + final response = await _httpClient.get(mempoolUri); + if (response.statusCode != 200) { + return false; + } + final Map body = jsonDecode(response.body); + return body.containsKey("fastestFee"); } catch (e) { _log.warning("Failed to test mempool url: $uri", e); return false; diff --git a/lib/routes/home/widgets/payments_list/dialog/closed_channel_payment_details.dart b/lib/routes/home/widgets/payments_list/dialog/closed_channel_payment_details.dart index 7c9d3bd60..58ba584fc 100644 --- a/lib/routes/home/widgets/payments_list/dialog/closed_channel_payment_details.dart +++ b/lib/routes/home/widgets/payments_list/dialog/closed_channel_payment_details.dart @@ -1,10 +1,12 @@ import 'package:breez_sdk/bridge_generated.dart' as sdk; import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:c_breez/config.dart'; +import 'package:c_breez/bloc/network/network_settings_bloc.dart'; import 'package:c_breez/models/payment_minutiae.dart'; import 'package:c_breez/routes/home/widgets/payments_list/dialog/tx_widget.dart'; +import 'package:c_breez/utils/blockchain_explorer_utils.dart'; import 'package:c_breez/widgets/loader.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class ClosedChannelPaymentDetailsWidget extends StatelessWidget { final PaymentMinutiae paymentMinutiae; @@ -18,61 +20,66 @@ class ClosedChannelPaymentDetailsWidget extends StatelessWidget { Widget build(BuildContext context) { final themeData = Theme.of(context); final texts = context.texts(); - return FutureBuilder( - future: Config.instance(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - final blockExplorer = snapshot.data!.defaultMempoolUrl; - if (paymentMinutiae.status == sdk.PaymentStatus.Complete) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - RichText( - text: TextSpan( - style: themeData.dialogTheme.contentTextStyle, - text: texts.payment_details_dialog_closed_channel_local_wallet, - ), - ), - if (paymentMinutiae.paymentType == sdk.PaymentType.ClosedChannel && - paymentMinutiae.closingTxid != null) ...[ - TxWidget( - txURL: "$blockExplorer/tx/${paymentMinutiae.closingTxid!}", - txID: paymentMinutiae.closingTxid!, - ), - ], - ], - ); - } - // TODO pendingExpirationHeight - // TODO hoursToExpire - String estimation = texts.payment_details_dialog_closed_channel_transfer_no_estimation; + final networkSettingsBloc = context.read(); + + return FutureBuilder( + future: networkSettingsBloc.mempoolInstance, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Loader(); + } + final mempoolInstance = snapshot.data!; + if (paymentMinutiae.status == sdk.PaymentStatus.Complete) { return Column( mainAxisSize: MainAxisSize.min, children: [ RichText( text: TextSpan( style: themeData.dialogTheme.contentTextStyle, - text: estimation, + text: texts.payment_details_dialog_closed_channel_local_wallet, ), ), - if (paymentMinutiae.fundingTxid != null) ...[ - TxWidget( - txURL: "$blockExplorer/tx/${paymentMinutiae.fundingTxid!}", - txID: paymentMinutiae.fundingTxid!, - ), - ], - if (paymentMinutiae.closingTxid != null) ...[ + if (paymentMinutiae.paymentType == sdk.PaymentType.ClosedChannel && + paymentMinutiae.closingTxid != null) ...[ TxWidget( - txURL: "$blockExplorer/tx/${paymentMinutiae.closingTxid!}", + txURL: BlockChainExplorerUtils().formatTransactionUrl( + txid: paymentMinutiae.closingTxid!, mempoolInstance: mempoolInstance), txID: paymentMinutiae.closingTxid!, ), - ] + ], ], ); - } else { - return const Loader(); } + // TODO pendingExpirationHeight + // TODO hoursToExpire + String estimation = texts.payment_details_dialog_closed_channel_transfer_no_estimation; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + style: themeData.dialogTheme.contentTextStyle, + text: estimation, + ), + ), + if (paymentMinutiae.fundingTxid != null) ...[ + TxWidget( + txURL: BlockChainExplorerUtils().formatTransactionUrl( + txid: paymentMinutiae.fundingTxid!, mempoolInstance: mempoolInstance), + txID: paymentMinutiae.fundingTxid!, + ), + ], + if (paymentMinutiae.closingTxid != null) ...[ + TxWidget( + txURL: BlockChainExplorerUtils().formatTransactionUrl( + txid: paymentMinutiae.closingTxid!, mempoolInstance: mempoolInstance), + txID: paymentMinutiae.closingTxid!, + ), + ] + ], + ); }, ); } diff --git a/lib/routes/subswap/swap/widgets/inprogress_swap.dart b/lib/routes/subswap/swap/widgets/inprogress_swap.dart index 8d7debd01..30f0bca8a 100644 --- a/lib/routes/subswap/swap/widgets/inprogress_swap.dart +++ b/lib/routes/subswap/swap/widgets/inprogress_swap.dart @@ -1,9 +1,13 @@ import 'package:breez_sdk/bridge_generated.dart'; import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:c_breez/bloc/network/network_settings_bloc.dart'; import 'package:c_breez/services/injector.dart'; +import 'package:c_breez/utils/blockchain_explorer_utils.dart'; import 'package:c_breez/widgets/flushbar.dart'; import 'package:c_breez/widgets/link_launcher.dart'; +import 'package:c_breez/widgets/loader.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SwapInprogress extends StatelessWidget { final SwapInfo swap; @@ -48,16 +52,28 @@ class _TxLink extends StatelessWidget { @override Widget build(BuildContext context) { final text = context.texts(); + final networkSettingsBloc = context.read(); - return LinkLauncher( - linkName: txid, - linkAddress: "https://blockstream.info/tx/$txid", - onCopy: () { - ServiceInjector().device.setClipboardText(txid); - showFlushbar( - context, - message: text.add_funds_transaction_id_copied, - duration: const Duration(seconds: 3), + return FutureBuilder( + future: networkSettingsBloc.mempoolInstance, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Loader(); + } + final mempoolInstance = snapshot.data!; + + return LinkLauncher( + linkName: txid, + linkAddress: + BlockChainExplorerUtils().formatTransactionUrl(txid: txid, mempoolInstance: mempoolInstance), + onCopy: () { + ServiceInjector().device.setClipboardText(txid); + showFlushbar( + context, + message: text.add_funds_transaction_id_copied, + duration: const Duration(seconds: 3), + ); + }, ); }, ); diff --git a/lib/routes/withdraw/reverse_swap/in_progress/reverse_swap_in_progress.dart b/lib/routes/withdraw/reverse_swap/in_progress/reverse_swap_in_progress.dart index 3f5f9e3a6..12f4dfb11 100644 --- a/lib/routes/withdraw/reverse_swap/in_progress/reverse_swap_in_progress.dart +++ b/lib/routes/withdraw/reverse_swap/in_progress/reverse_swap_in_progress.dart @@ -1,7 +1,8 @@ import 'package:breez_sdk/bridge_generated.dart' as sdk; import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:c_breez/config.dart'; +import 'package:c_breez/bloc/network/network_settings_bloc.dart'; import 'package:c_breez/services/injector.dart'; +import 'package:c_breez/utils/blockchain_explorer_utils.dart'; import 'package:c_breez/widgets/flushbar.dart'; import 'package:c_breez/widgets/link_launcher.dart'; import 'package:c_breez/widgets/loader.dart'; @@ -46,19 +47,21 @@ class _TxLink extends StatelessWidget { @override Widget build(BuildContext context) { final text = context.texts(); + final networkSettingsBloc = context.read(); return FutureBuilder( - future: Config.instance(), + future: networkSettingsBloc.mempoolInstance, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Loader(); } - final blockExplorer = snapshot.data!.defaultMempoolUrl; + final transactionUrl = + BlockChainExplorerUtils().formatTransactionUrl(mempoolInstance: snapshot.data!, txid: txid); return LinkLauncher( linkName: txid, - linkAddress: "$blockExplorer/tx/$txid", + linkAddress: transactionUrl, onCopy: () { ServiceInjector().device.setClipboardText(txid); showFlushbar( diff --git a/lib/utils/blockchain_explorer_utils.dart b/lib/utils/blockchain_explorer_utils.dart new file mode 100644 index 000000000..963444e99 --- /dev/null +++ b/lib/utils/blockchain_explorer_utils.dart @@ -0,0 +1,9 @@ +class BlockChainExplorerUtils { + String formatTransactionUrl({required String txid, required String mempoolInstance}) { + return "$mempoolInstance/tx/$txid"; + } + + String formatRecommendedFeesUrl({required String mempoolInstance}) { + return "$mempoolInstance/api/v1/fees/recommended"; + } +} diff --git a/test/bloc/network/network_settings_bloc_test.dart b/test/bloc/network/network_settings_bloc_test.dart index fc156dae5..5330b8af8 100644 --- a/test/bloc/network/network_settings_bloc_test.dart +++ b/test/bloc/network/network_settings_bloc_test.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; + import 'package:c_breez/bloc/network/network_settings_bloc.dart'; import 'package:c_breez/bloc/network/network_settings_state.dart'; import 'package:c_breez/services/injector.dart'; +import 'package:c_breez/utils/blockchain_explorer_utils.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; @@ -12,6 +15,17 @@ import '../../unit_logger.dart'; import '../../utils/fake_path_provider_platform.dart'; import '../../utils/hydrated_bloc_storage.dart'; +String get _recomendedMockFeesResponse { + final Map recomendedFees = { + "fastestFee": 1, + "halfHourFee": 1, + "hourFee": 1, + "economyFee": 1, + "minimumFee": 1 + }; + return jsonEncode(recomendedFees); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final platform = FakePathProviderPlatform(); @@ -75,8 +89,12 @@ void main() { test('set mempool space url with a valid url should set on the preferences', () async { const url = "https://mempool.space"; + httpClient.getAnswer[url] = http.Response("{}", 200); final bloc = make(); + + httpClient.getAnswer[BlockChainExplorerUtils().formatRecommendedFeesUrl(mempoolInstance: url)] = + http.Response(_recomendedMockFeesResponse, 200); final result = await bloc.setMempoolUrl(url); expect(result, true); expect(injector.preferencesMock.setMempoolSpaceUrlUrl, url); @@ -84,8 +102,11 @@ void main() { test('set mempool space url with a valid url missing scheme should set on the preferences', () async { const url = "mempool.space"; - httpClient.getAnswer["https://$url"] = http.Response("{}", 200); + final bloc = make(); + httpClient.getAnswer[ + "https://${BlockChainExplorerUtils().formatRecommendedFeesUrl(mempoolInstance: url)}"] = + http.Response(_recomendedMockFeesResponse, 200); final result = await bloc.setMempoolUrl(url); expect(result, true); expect(injector.preferencesMock.setMempoolSpaceUrlUrl, "https://$url"); @@ -101,8 +122,9 @@ void main() { test('set mempool space url with an ip should should set on the preferences', () async { const url = "https://192.168.15.2"; - httpClient.getAnswer[url] = http.Response("{}", 200); final bloc = make(); + httpClient.getAnswer[BlockChainExplorerUtils().formatRecommendedFeesUrl(mempoolInstance: url)] = + http.Response(_recomendedMockFeesResponse, 200); final result = await bloc.setMempoolUrl(url); expect(result, true); expect(injector.preferencesMock.setMempoolSpaceUrlUrl, url); @@ -110,8 +132,9 @@ void main() { test('set mempool space url with an ip and port should should set on the preferences', () async { const url = "https://192.168.15.2:3006"; - httpClient.getAnswer[url] = http.Response("{}", 200); final bloc = make(); + httpClient.getAnswer[BlockChainExplorerUtils().formatRecommendedFeesUrl(mempoolInstance: url)] = + http.Response(_recomendedMockFeesResponse, 200); final result = await bloc.setMempoolUrl(url); expect(result, true); expect(injector.preferencesMock.setMempoolSpaceUrlUrl, url); @@ -119,8 +142,10 @@ void main() { test('set mempool space url with an ip missing scheme should should set on the preferences', () async { const url = "192.168.15.2"; - httpClient.getAnswer["https://$url"] = http.Response("{}", 200); final bloc = make(); + httpClient.getAnswer[ + "https://${BlockChainExplorerUtils().formatRecommendedFeesUrl(mempoolInstance: url)}"] = + http.Response(_recomendedMockFeesResponse, 200); final result = await bloc.setMempoolUrl(url); expect(result, true); expect(injector.preferencesMock.setMempoolSpaceUrlUrl, "https://$url");