From 175eaa7f6cf9e8e97ff62a9c74f164a6791dc8df Mon Sep 17 00:00:00 2001 From: Torkel Rogstad Date: Thu, 2 Nov 2023 10:45:16 +0100 Subject: [PATCH] multi: add mainchain RPC --- lib/config/dependencies.dart | 15 ++++++- lib/rpc/rpc_config.dart | 44 +++++++++++++++---- lib/rpc/rpc_mainchain.dart | 39 ++++++++++++++++ lib/rpc/rpc_sidechain.dart | 14 +++--- .../tabs/dashboard_tab_widgets.dart | 40 ++++++++++------- test/dashboard_test.dart | 3 ++ test/mocks/rpc_mock_mainchain.dart | 8 ++++ test/mocks/rpc_mock_sidechain.dart | 2 +- 8 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 lib/rpc/rpc_mainchain.dart create mode 100644 test/mocks/rpc_mock_mainchain.dart diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 60764920..a9bc70c0 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart'; import 'package:sidesail/providers/balance_provider.dart'; import 'package:sidesail/providers/transactions_provider.dart'; import 'package:sidesail/routing/router.dart'; +import 'package:sidesail/rpc/rpc_mainchain.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:sidesail/rpc/rpc_config.dart'; import 'package:sidesail/storage/client_settings.dart'; @@ -11,9 +12,19 @@ import 'package:sidesail/storage/secure_store.dart'; // each dependency can only be registered once Future initGetitDependencies() async { // TODO: this can throw an error. How do we display that to the user? - final rpcConfig = await readRpcConfig(); + final sidechainConfigFut = readRpcConfig(testchainDatadir(), 'testchain.conf'); + + final mainchainConfigFut = readRpcConfig(mainchainDatadir(), 'drivechain.conf'); + + final sidechainConfig = await sidechainConfigFut; + final mainchainConfig = await mainchainConfigFut; + GetIt.I.registerLazySingleton( - () => SidechainRPCLive(rpcConfig), + () => SidechainRPCLive(sidechainConfig), + ); + + GetIt.I.registerLazySingleton( + () => MainchainRPCLive(mainchainConfig), ); GetIt.I.registerLazySingleton( diff --git a/lib/rpc/rpc_config.dart b/lib/rpc/rpc_config.dart index c8e223dc..51f4f345 100644 --- a/lib/rpc/rpc_config.dart +++ b/lib/rpc/rpc_config.dart @@ -26,13 +26,10 @@ const network = 'regtest'; // 2. Inspect cookie // 3. Defaults // -Future readRpcConfig() async { - final datadir = testchainDatadir(); +Future readRpcConfig(String datadir, String confFile) async { final networkDir = _filePath([datadir, network]); - log.t('datadir: $datadir'); - - final conf = File(_filePath([datadir, 'testchain.conf'])); + final conf = File(_filePath([datadir, confFile])); final cookie = File(_filePath([networkDir, '.cookie'])); @@ -42,7 +39,7 @@ Future readRpcConfig() async { int? port; if (!await conf.exists() && !await cookie.exists()) { - throw 'could not find neither conf nor cookie'; + throw 'could not find neither conf ($conf) nor cookie ($cookie)'; } if (await cookie.exists()) { @@ -76,7 +73,7 @@ Future readRpcConfig() async { } host ??= 'localhost'; - port ??= _defaultPorts[network]!; + port ??= confFile.startsWith('testchain') ? _defaultSidechainPorts[network]! : _defaultMainchainPorts[network]!; // Make sure to not include password here log.i('resolved conf: $username@$host:$port'); @@ -93,6 +90,32 @@ String? _configValue(List config, String key) { return line?.split('=').lastOrNull; } +// TODO: this might need permissions configuration for Windows and Linux? +String mainchainDatadir() { + final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; // windows! + if (home == null) { + throw 'unable to determine HOME location'; + } + + if (Platform.isLinux) { + return _filePath([ + home, + '.drivechain', + ]); + } else if (Platform.isMacOS) { + return _filePath([ + home, + 'Library', + 'Application Support', + 'Drivechain', + ]); + } else if (Platform.isWindows) { + throw 'TODO: windows'; + } else { + throw 'unsupported operating system: ${Platform.operatingSystem}'; + } +} + // TODO: make this configurable when adding support for more sidechains // TODO: this might need permissions configuration for Windows and Linux? String testchainDatadir() { @@ -126,6 +149,11 @@ String _filePath(List segments) { // TODO: this would need to take chain into account // TODO: add more nets -Map _defaultPorts = { +Map _defaultSidechainPorts = { 'regtest': 18743, }; + +// TODO: add more nets +Map _defaultMainchainPorts = { + 'regtest': 18443, +}; diff --git a/lib/rpc/rpc_mainchain.dart b/lib/rpc/rpc_mainchain.dart new file mode 100644 index 00000000..c69e8aba --- /dev/null +++ b/lib/rpc/rpc_mainchain.dart @@ -0,0 +1,39 @@ +import 'package:dart_coin_rpc/dart_coin_rpc.dart'; +import 'package:dio/dio.dart'; +import 'package:sidesail/rpc/rpc_config.dart'; + +/// RPC connection to the mainchain node. +abstract class MainchainRPC { + Future estimateFee(); +} + +class MainchainRPCLive implements MainchainRPC { + late RPCClient _client; + MainchainRPCLive(Config config) { + _client = RPCClient( + host: config.host, + port: config.port, + username: config.username, + password: config.password, + useSSL: false, + ); + + // Completely empty client, with no retry logic. + _client.dioClient = Dio(); + } + + @override + Future estimateFee() async { + final estimate = await _client.call('estimatesmartfee', [6]) as Map; + if (estimate.containsKey('errors')) { + // 10 sats/byte + return 0.001; + } + + final btcPerKb = estimate['feerate'] as double; + + // who knows! + const kbyteInTx = 5; + return btcPerKb * kbyteInTx; + } +} diff --git a/lib/rpc/rpc_sidechain.dart b/lib/rpc/rpc_sidechain.dart index 577d7d31..0aa9e1e3 100644 --- a/lib/rpc/rpc_sidechain.dart +++ b/lib/rpc/rpc_sidechain.dart @@ -26,7 +26,7 @@ abstract class SidechainRPC { ); Future> listTransactions(); - Future estimateSidechainFee(); + Future estimateFee(); Future mainchainBlockCount(); Future blockCount(); Future fetchWithdrawalBundleStatus(); @@ -83,7 +83,7 @@ class SidechainRPCLive implements SidechainRPC { mainchainFee, ]); - return withdrawalTxid; + return withdrawalTxid['txid']; } @override @@ -111,18 +111,18 @@ class SidechainRPCLive implements SidechainRPC { } @override - Future estimateSidechainFee() async { + Future estimateFee() async { final estimate = await _client.call('estimatesmartfee', [6]) as Map; if (estimate.containsKey('errors')) { - log.w("could not estimate fee: ${estimate["errors"]}"); + // 10 sats/byte return 0.001; } - final btcPerKb = estimate.containsKey('feerate') ? estimate['feerate'] as double : 0.0001; // 10 sats/byte + final btcPerKb = estimate['feerate'] as double; // who knows! - const kbyteInWithdrawal = 5; - return btcPerKb * kbyteInWithdrawal; + const kbyteInTx = 5; + return btcPerKb * kbyteInTx; } @override diff --git a/lib/widgets/containers/tabs/dashboard_tab_widgets.dart b/lib/widgets/containers/tabs/dashboard_tab_widgets.dart index 67fed271..77a49ca6 100644 --- a/lib/widgets/containers/tabs/dashboard_tab_widgets.dart +++ b/lib/widgets/containers/tabs/dashboard_tab_widgets.dart @@ -8,6 +8,7 @@ import 'package:sail_ui/theme/theme.dart'; import 'package:sail_ui/widgets/core/sail_text.dart'; import 'package:sidesail/providers/balance_provider.dart'; import 'package:sidesail/routing/router.dart'; +import 'package:sidesail/rpc/rpc_mainchain.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:sidesail/widgets/containers/dashboard_action_modal.dart'; import 'package:stacked/stacked.dart'; @@ -93,7 +94,7 @@ class PegOutAction extends StatelessWidget { ), StaticActionField( label: 'Mainchain fee', - value: '${(viewModel.mainchainFee).toStringAsFixed(8)} BTC', + value: '${(viewModel.mainchainFee ?? 0).toStringAsFixed(8)} BTC', ), StaticActionField( label: 'Sidechain fee', @@ -114,16 +115,17 @@ class PegOutViewModel extends BaseViewModel { final log = Logger(level: Level.debug); BalanceProvider get _balanceProvider => GetIt.I.get(); AppRouter get _router => GetIt.I.get(); - SidechainRPC get _rpc => GetIt.I.get(); + SidechainRPC get _sidechain => GetIt.I.get(); + MainchainRPC get _mainchain => GetIt.I.get(); final bitcoinAddressController = TextEditingController(); final bitcoinAmountController = TextEditingController(); String get totalBitcoinAmount => - ((double.tryParse(bitcoinAmountController.text) ?? 0) + mainchainFee + (sidechainFee ?? 0)).toStringAsFixed(8); + ((double.tryParse(bitcoinAmountController.text) ?? 0) + (mainchainFee ?? 0) + (sidechainFee ?? 0)) + .toStringAsFixed(8); - // executePegOut: estimate this - final double mainchainFee = 0.001; double? sidechainFee; + double? mainchainFee; double? get pegOutAmount => double.tryParse(bitcoinAmountController.text); PegOutViewModel() { @@ -133,7 +135,7 @@ class PegOutViewModel extends BaseViewModel { } void init() async { - await estimateSidechainFee(); + await Future.wait([estimateSidechainFee(), estimateMainchainFee()]); } void executePegOut(BuildContext context) async { @@ -145,8 +147,12 @@ class PegOutViewModel extends BaseViewModel { } Future estimateSidechainFee() async { - final estimate = await _rpc.estimateSidechainFee(); - sidechainFee = estimate; + sidechainFee = await _sidechain.estimateFee(); + notifyListeners(); + } + + Future estimateMainchainFee() async { + mainchainFee = await _mainchain.estimateFee(); notifyListeners(); } @@ -197,6 +203,7 @@ class PegOutViewModel extends BaseViewModel { address, pegOutAmount!, sidechainFee!, + mainchainFee!, ); }, child: SailText.primary14( @@ -213,13 +220,14 @@ class PegOutViewModel extends BaseViewModel { String address, double amount, double sidechainFee, + double mainchainFee, ) async { log.i( 'doing peg-out: $amount BTC to $address for $sidechainFee SC fee and $mainchainFee MC fee', ); try { - final withdrawalTxid = await _rpc.pegOut( + final withdrawalTxid = await _sidechain.pegOut( address, amount, sidechainFee, @@ -230,7 +238,7 @@ class PegOutViewModel extends BaseViewModel { return; } - // refresh balance, but dont await, so dialog is showed instantly + // refresh balance, but don't await, so dialog is showed instantly unawaited(_balanceProvider.fetch()); final theme = SailTheme.of(context); @@ -253,6 +261,8 @@ class PegOutViewModel extends BaseViewModel { ), ); } catch (error) { + log.e('could not execute peg-out: $error', error: error); + if (!context.mounted) { return; } @@ -266,7 +276,7 @@ class PegOutViewModel extends BaseViewModel { 'Failed', ), content: SailText.primary14( - 'Could not execute peg-out ${error.toString()}', + 'Could not execute peg-out: ${error.toString()}', ), actions: [ TextButton( @@ -393,7 +403,7 @@ class SendOnSidechainViewModel extends BaseViewModel { } void init() async { - await estimateSidechainFee(); + await estimateFee(); } void executeSendOnSidechain(BuildContext context) async { @@ -406,8 +416,8 @@ class SendOnSidechainViewModel extends BaseViewModel { await _router.pop(); } - Future estimateSidechainFee() async { - final estimate = await _rpc.estimateSidechainFee(); + Future estimateFee() async { + final estimate = await _rpc.estimateFee(); sidechainExpectedFee = estimate; notifyListeners(); } @@ -485,7 +495,7 @@ class SendOnSidechainViewModel extends BaseViewModel { ); log.i('sent sidechain withdrawal txid: $sendTXID'); - // refresh balance, but dont await, so dialog is showed instantly + // refresh balance, but don't await, so dialog is showed instantly unawaited(_balanceProvider.fetch()); if (!context.mounted) { diff --git a/test/dashboard_test.dart b/test/dashboard_test.dart index 71a57331..33cfbe01 100644 --- a/test/dashboard_test.dart +++ b/test/dashboard_test.dart @@ -10,8 +10,10 @@ import 'package:get_it/get_it.dart'; import 'package:sidesail/pages/tabs/dashboard_tab_page.dart'; import 'package:sidesail/providers/balance_provider.dart'; import 'package:sidesail/providers/transactions_provider.dart'; +import 'package:sidesail/rpc/rpc_mainchain.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; +import 'mocks/rpc_mock_mainchain.dart'; import 'mocks/rpc_mock_sidechain.dart'; import 'test_utils.dart'; @@ -20,6 +22,7 @@ final txProvider = TransactionsProvider(); void main() { setUpAll(() async { GetIt.I.registerLazySingleton(() => MockSidechainRPC()); + GetIt.I.registerLazySingleton(() => MockMainchainRPC()); GetIt.I.registerLazySingleton(() => txProvider); final balanceProvider = BalanceProvider(); diff --git a/test/mocks/rpc_mock_mainchain.dart b/test/mocks/rpc_mock_mainchain.dart new file mode 100644 index 00000000..9e49e8a5 --- /dev/null +++ b/test/mocks/rpc_mock_mainchain.dart @@ -0,0 +1,8 @@ +import 'package:sidesail/rpc/rpc_mainchain.dart'; + +class MockMainchainRPC extends MainchainRPC { + @override + Future estimateFee() async { + return 0.001; + } +} diff --git a/test/mocks/rpc_mock_sidechain.dart b/test/mocks/rpc_mock_sidechain.dart index 15010c03..132d42dc 100644 --- a/test/mocks/rpc_mock_sidechain.dart +++ b/test/mocks/rpc_mock_sidechain.dart @@ -36,7 +36,7 @@ class MockSidechainRPC extends SidechainRPC { } @override - Future estimateSidechainFee() async { + Future estimateFee() async { return 0.001; }