From d8cc11cea5fd1354d752b104fa808f4aca8dd2ad Mon Sep 17 00:00:00 2001 From: Torkel Rogstad Date: Tue, 7 Nov 2023 13:21:00 +0100 Subject: [PATCH 1/8] rpc: add basic ETH connection --- lib/config/dependencies.dart | 8 ++- lib/rpc/rpc_eth.dart | 101 +++++++++++++++++++++++++++++++++++ lib/rpc/rpc_ethereum.dart | 98 --------------------------------- pubspec.lock | 87 ++++++++++++++++++++++++++++-- pubspec.yaml | 9 ++++ 5 files changed, 201 insertions(+), 102 deletions(-) create mode 100644 lib/rpc/rpc_eth.dart delete mode 100644 lib/rpc/rpc_ethereum.dart diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 4603c684..8b766e75 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -4,7 +4,7 @@ import 'package:sidesail/config/sidechains.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_ethereum.dart'; +import 'package:sidesail/rpc/rpc_eth.dart'; import 'package:sidesail/rpc/rpc_mainchain.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:sidesail/rpc/rpc_testchain.dart'; @@ -15,13 +15,19 @@ import 'package:sidesail/storage/secure_store.dart'; // each dependency can only be registered once Future initGetitDependencies(Sidechain chain) async { final mainFuture = MainchainRPCLive.create(); + final ethFuture = EthereumRPCLive.create(); await setSidechainRPC(chain); final mainRPC = await mainFuture; + final ethRPC = await ethFuture; GetIt.I.registerLazySingleton( () => mainRPC, ); + GetIt.I.registerLazySingleton( + () => ethRPC, + ); + GetIt.I.registerLazySingleton( () => AppRouter(), ); diff --git a/lib/rpc/rpc_eth.dart b/lib/rpc/rpc_eth.dart new file mode 100644 index 00000000..94995579 --- /dev/null +++ b/lib/rpc/rpc_eth.dart @@ -0,0 +1,101 @@ +import 'package:http/http.dart'; +import 'package:sidesail/pages/tabs/settings/node_settings_tab.dart'; +import 'package:sidesail/rpc/rpc.dart'; +import 'package:web3dart/web3dart.dart'; + +abstract class EthereumRPC extends RPCConnection { + Future call(String rpc, [args]); +} + +class EthereumRPCLive extends EthereumRPC { + late Web3Client _client; + + @override + Future call(String rpc, [args]) async { + return _client.makeRPCCall(rpc, args); + } + + // Apparently Ethereum doesn't have a conf file? + @override + Future createClient() async { + final url = 'http://${connectionSettings.host}:${connectionSettings.port}'; + _client = Web3Client(url, Client()); + } + + EthereumRPCLive._create(); + + static Future create() async { + final rpc = EthereumRPCLive._create(); + await rpc._init(); + return rpc; + } + + Future _init() async { + // TODO: implement authed RPCs + // TODO: make this configurable + connectionSettings = SingleNodeConnectionSettings('', 'localhost', 8545, '', ''); + + await createClient(); + + await testConnection(); + } + + @override + Future ping() async { + await _client.getChainId(); + return; + } +} + +/// List of all known RPC methods available / +final ethRpcMethods = [ + 'web3_clientVersion', + 'web3_sha3', + 'net_version', + 'net_listening', + 'net_peerCount', + 'eth_protocolVersion', + 'eth_syncing', + 'eth_coinbase', + 'eth_chainId', + 'eth_mining', + 'eth_hashrate', + 'eth_gasPrice', + 'eth_accounts', + 'eth_blockNumber', + 'eth_getBalance', + 'eth_getStorageAt', + 'eth_getTransactionCount', + 'eth_getBlockTransactionCountByHash', + 'eth_getBlockTransactionCountByNumber', + 'eth_getUncleCountByBlockHash', + 'eth_getUncleCountByBlockNumber', + 'eth_getCode', + 'eth_sign', + 'eth_signTransaction', + 'eth_sendTransaction', + 'eth_sendRawTransaction', + 'eth_call', + 'eth_estimateGas', + 'eth_getBlockByHash', + 'eth_getBlockByNumber', + 'eth_getTransactionByHash', + 'eth_getTransactionByBlockHashAndIndex', + 'eth_getTransactionByBlockNumberAndIndex', + 'eth_getTransactionReceipt', + 'eth_getUncleByBlockHashAndIndex', + 'eth_getUncleByBlockNumberAndIndex', + 'eth_newFilter', + 'eth_newBlockFilter', + 'eth_newPendingTransactionFilter', + 'eth_uninstallFilter', + 'eth_getFilterChanges', + 'eth_getFilterLogs', + 'eth_getLogs', + + // Sidechain specific RPC calls + 'eth_deposit', + 'eth_withdraw', + 'eth_getUnspentWithdrawals', + 'eth_refund', +]; diff --git a/lib/rpc/rpc_ethereum.dart b/lib/rpc/rpc_ethereum.dart deleted file mode 100644 index ff1bedf4..00000000 --- a/lib/rpc/rpc_ethereum.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'dart:async'; - -import 'package:sidesail/config/sidechains.dart'; -import 'package:sidesail/rpc/models/core_transaction.dart'; -import 'package:sidesail/rpc/rpc_sidechain.dart'; - -/// RPC connection the sidechain node. -abstract class EthereumRPC extends SidechainRPC {} - -class EthereumRPCLive extends EthereumRPC { - // hacky way to create an async class - // https://stackoverflow.com/a/59304510 - EthereumRPCLive._create() { - chain = EthereumSidechain(); - } - - static Future create() async { - final rpc = EthereumRPCLive._create(); - await rpc._init(); - return rpc; - } - - Future _init() async { - // TODO: Create connection, get inspo from rpc_testchain.dart - } - - @override - Future callRAW(String method, [params]) { - // TODO: implement callRAW - throw UnimplementedError(); - } - - @override - Future createClient() { - // TODO: implement createClient - throw UnimplementedError(); - } - - @override - Future<(double, double)> getBalance() { - // TODO: implement getBalance - throw UnimplementedError(); - } - - @override - Future mainBlockCount() { - // TODO: implement mainBlockCount - throw UnimplementedError(); - } - - @override - Future mainGenerateAddress() { - // TODO: implement mainGenerateAddress - throw UnimplementedError(); - } - - @override - Future mainSend(String address, double amount, double sidechainFee, double mainchainFee) { - // TODO: implement mainSend - throw UnimplementedError(); - } - - @override - Future ping() { - // TODO: implement ping - throw UnimplementedError(); - } - - @override - Future sideBlockCount() { - // TODO: implement sideBlockCount - throw UnimplementedError(); - } - - @override - Future sideGenerateAddress() { - // TODO: implement sideGenerateAddress - throw UnimplementedError(); - } - - @override - Future sideSend(String address, double amount, bool subtractFeeFromAmount) { - // TODO: implement sideSend - throw UnimplementedError(); - } - - @override - Future sideEstimateFee() { - // TODO: implement sideEstimateFee - throw UnimplementedError(); - } - - @override - Future> listTransactions() { - // TODO: implement listTransactions - throw UnimplementedError(); - } -} diff --git a/pubspec.lock b/pubspec.lock index 49536a40..dc463824 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -218,6 +218,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + eip1559: + dependency: transitive + description: + name: eip1559 + sha256: c2b81ac85f3e0e71aaf558201dd9a4600f051ece7ebacd0c5d70065c9b458004 + url: "https://pub.dev" + source: hosted + version: "0.6.2" + eip55: + dependency: transitive + description: + name: eip55 + sha256: "213a9b86add87a5216328e8494b0ab836e401210c4d55eb5e521bd39e39169e1" + url: "https://pub.dev" + source: hosted + version: "1.0.2" fake_async: dependency: transitive description: @@ -409,6 +425,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -457,6 +481,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "5e469bffa23899edacb7b22787780068d650b106a21c76db3c49218ab7ca447e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" lints: dependency: transitive description: @@ -625,6 +657,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.6" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" pool: dependency: transitive description: @@ -664,6 +704,14 @@ packages: relative: true source: path version: "0.0.1" + sec: + dependency: transitive + description: + name: sec + sha256: "8bbd56df884502192a441b5f5d667265498f2f8728a282beccd9db79e215f379" + url: "https://pub.dev" + source: hosted + version: "1.1.0" shelf: dependency: transitive description: @@ -701,6 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -726,13 +782,13 @@ packages: source: hosted version: "1.4.0" stream_channel: - dependency: transitive + dependency: "direct overridden" description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -805,6 +861,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 + url: "https://pub.dev" + source: hosted + version: "4.1.0" vector_math: dependency: transitive description: @@ -813,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + wallet: + dependency: transitive + description: + name: wallet + sha256: "569c91c2af13a9e1119c001f9c09218eccf3f383eb8d15ba13a5b558010c1bc0" + url: "https://pub.dev" + source: hosted + version: "0.0.12+1" watcher: dependency: transitive description: @@ -829,6 +901,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.4-beta" + web3dart: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: d9fe4457865ebaee04336d51d2c061773517c9f2 + url: "https://github.com/xclud/web3dart.git" + source: git + version: "2.7.2" web_socket_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 415d6f2c..42834c3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +51,16 @@ dependencies: collection: ^1.17.2 stacked: ^3.4.1+1 intl: ^0.18.0 + web3dart: + git: + url: https://github.com/xclud/web3dart.git + ref: main + http: ^1.1.0 + +# Needed for silencing complaints from adding the web3dart lib +dependency_overrides: + stream_channel: 2.1.2 dev_dependencies: flutter_test: From d805813115c96eab0b8f67f21d841cf18e9e36a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 9 Nov 2023 10:34:41 +0100 Subject: [PATCH 2/8] rpc: move functions from Sidechain to Testchain --- lib/config/dependencies.dart | 9 ++-- lib/rpc/rpc_sidechain.dart | 26 ++--------- lib/rpc/rpc_testchain.dart | 18 ++++++++ .../tabs/dashboard_tab_widgets.dart | 6 +-- .../tabs/transfer_mainchain_tab_widgets.dart | 6 +-- test/mocks/rpc_mock_sidechain.dart | 45 +++---------------- 6 files changed, 38 insertions(+), 72 deletions(-) diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 8b766e75..6beaefe7 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -4,7 +4,7 @@ import 'package:sidesail/config/sidechains.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_eth.dart'; +import 'package:sidesail/rpc/rpc_ethereum.dart'; import 'package:sidesail/rpc/rpc_mainchain.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:sidesail/rpc/rpc_testchain.dart'; @@ -63,14 +63,15 @@ Future setSidechainRPC(Sidechain chain) async { break; case SidechainType.ethereum: - final testchainRPC = await EthereumRPCLive.create(); + final ethRPC = await EthereumRPCLive.create(); if (GetIt.I.isRegistered()) { GetIt.I.unregister(); } GetIt.I.registerLazySingleton( - () => testchainRPC, + () => ethRPC, ); - sidechainRPC = testchainRPC; + + sidechainRPC = ethRPC; break; } diff --git a/lib/rpc/rpc_sidechain.dart b/lib/rpc/rpc_sidechain.dart index c100df68..c36cde3f 100644 --- a/lib/rpc/rpc_sidechain.dart +++ b/lib/rpc/rpc_sidechain.dart @@ -1,39 +1,21 @@ import 'dart:async'; +import 'package:sidesail/config/dependencies.dart'; import 'package:sidesail/config/sidechains.dart'; import 'package:sidesail/rpc/models/core_transaction.dart'; import 'package:sidesail/rpc/rpc.dart'; /// RPC connection for all sidechain nodes abstract class SidechainRPC extends RPCConnection { - Future<(double, double)> getBalance(); - - Future mainSend( - String address, - double amount, - double sidechainFee, - double mainchainFee, - ); - Future mainGenerateAddress(); - Future mainBlockCount(); - - Future sideSend( - String address, - double amount, - bool subtractFeeFromAmount, - ); - Future sideGenerateAddress(); - Future sideBlockCount(); - Future sideEstimateFee(); + Future callRAW(String method, [List? params]); - // TODO: Don't do a CoreTransaction here + Future<(double, double)> getBalance(); Future> listTransactions(); - Future callRAW(String method, [dynamic params]); - late Sidechain chain; void setChain(Sidechain newChain) { chain = newChain; + setSidechainRPC(newChain); notifyListeners(); } } diff --git a/lib/rpc/rpc_testchain.dart b/lib/rpc/rpc_testchain.dart index b8a1f300..0afc11af 100644 --- a/lib/rpc/rpc_testchain.dart +++ b/lib/rpc/rpc_testchain.dart @@ -16,6 +16,24 @@ import 'package:sidesail/rpc/rpc_withdrawal_bundle.dart'; /// RPC connection the sidechain node. abstract class TestchainRPC extends SidechainRPC { + Future mainSend( + String address, + double amount, + double sidechainFee, + double mainchainFee, + ); + Future mainGenerateAddress(); + Future mainBlockCount(); + + Future sideSend( + String address, + double amount, + bool subtractFeeFromAmount, + ); + Future sideGenerateAddress(); + Future sideBlockCount(); + Future sideEstimateFee(); + Future refreshBMM(int bidSatoshis); /// Returns null if there's no current bundle diff --git a/lib/widgets/containers/tabs/dashboard_tab_widgets.dart b/lib/widgets/containers/tabs/dashboard_tab_widgets.dart index 4582d380..4ac8a892 100644 --- a/lib/widgets/containers/tabs/dashboard_tab_widgets.dart +++ b/lib/widgets/containers/tabs/dashboard_tab_widgets.dart @@ -9,7 +9,7 @@ import 'package:sail_ui/widgets/core/sail_text.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_sidechain.dart'; +import 'package:sidesail/rpc/rpc_testchain.dart'; import 'package:sidesail/widgets/containers/dashboard_action_modal.dart'; import 'package:stacked/stacked.dart'; @@ -115,7 +115,7 @@ class SendOnSidechainViewModel extends BaseViewModel { BalanceProvider get _balanceProvider => GetIt.I.get(); TransactionsProvider get _transactionsProvider => GetIt.I.get(); AppRouter get _router => GetIt.I.get(); - SidechainRPC get _rpc => GetIt.I.get(); + TestchainRPC get _rpc => GetIt.I.get(); final bitcoinAddressController = TextEditingController(); final bitcoinAmountController = TextEditingController(); @@ -312,7 +312,7 @@ class ReceiveOnSidechainAction extends StatelessWidget { } class ReceiveOnSidechainViewModel extends BaseViewModel { - SidechainRPC get _rpc => GetIt.I.get(); + TestchainRPC get _rpc => GetIt.I.get(); final log = Logger(level: Level.debug); String? sidechainAddress; diff --git a/lib/widgets/containers/tabs/transfer_mainchain_tab_widgets.dart b/lib/widgets/containers/tabs/transfer_mainchain_tab_widgets.dart index 64773ed1..5bc63543 100644 --- a/lib/widgets/containers/tabs/transfer_mainchain_tab_widgets.dart +++ b/lib/widgets/containers/tabs/transfer_mainchain_tab_widgets.dart @@ -10,7 +10,7 @@ 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_testchain.dart'; import 'package:sidesail/widgets/containers/dashboard_action_modal.dart'; import 'package:stacked/stacked.dart'; @@ -68,7 +68,7 @@ class PegOutViewModel extends BaseViewModel { BalanceProvider get _balanceProvider => GetIt.I.get(); TransactionsProvider get _transactionsProvider => GetIt.I.get(); AppRouter get _router => GetIt.I.get(); - SidechainRPC get _testchain => GetIt.I.get(); + TestchainRPC get _testchain => GetIt.I.get(); MainchainRPC get _mainchain => GetIt.I.get(); final bitcoinAddressController = TextEditingController(); @@ -282,7 +282,7 @@ class PegInAction extends StatelessWidget { } class PegInViewModel extends BaseViewModel { - SidechainRPC get _rpc => GetIt.I.get(); + TestchainRPC get _rpc => GetIt.I.get(); final log = Logger(level: Level.debug); String? pegInAddress; diff --git a/test/mocks/rpc_mock_sidechain.dart b/test/mocks/rpc_mock_sidechain.dart index f9df25d0..98828556 100644 --- a/test/mocks/rpc_mock_sidechain.dart +++ b/test/mocks/rpc_mock_sidechain.dart @@ -7,11 +7,6 @@ class MockSidechainRPC extends SidechainRPC { chain = TestSidechain(); } - @override - Future mainGenerateAddress() async { - return 'bc1...?'; - } - @override Future<(double, double)> getBalance() async { return (1.12345678, 2.24680); @@ -22,41 +17,6 @@ class MockSidechainRPC extends SidechainRPC { return; } - @override - Future sideEstimateFee() async { - return 0.001; - } - - @override - Future sideGenerateAddress() async { - return 's1deadbeef'; - } - - @override - Future mainSend(String address, double amount, double sidechainFee, double mainchainFee) async { - return 'txidmainbeef'; - } - - @override - Future sideSend(String address, double amount, bool subtractFeeFromAmount) async { - return 'txidsidebeef'; - } - - @override - Future> listTransactions() async { - return List.empty(); - } - - @override - Future sideBlockCount() async { - return 1; - } - - @override - Future mainBlockCount() async { - return 1; - } - @override Future<(bool, String?)> testConnection() async { return (true, null); @@ -71,4 +31,9 @@ class MockSidechainRPC extends SidechainRPC { Future ping() async { return; } + + @override + Future> listTransactions() async { + return List.empty(); + } } From a11c0eced92b67c573671d3ee359273fd870703f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 9 Nov 2023 10:35:21 +0100 Subject: [PATCH 3/8] sail_ui: improve text field slightly --- lib/pages/tabs/settings/node_settings_tab.dart | 9 +++------ packages/sail_ui/lib/widgets/core/sail_shadow.dart | 8 +++++--- packages/sail_ui/lib/widgets/inputs/text_field.dart | 11 ++++++++++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/pages/tabs/settings/node_settings_tab.dart b/lib/pages/tabs/settings/node_settings_tab.dart index 3fcb4a12..85a95cd7 100644 --- a/lib/pages/tabs/settings/node_settings_tab.dart +++ b/lib/pages/tabs/settings/node_settings_tab.dart @@ -106,12 +106,9 @@ class NodeConnectionSettings extends ViewModelWidget { label: 'Config path', controller: settings.configPathController, hintText: '/the/path/to/your/somethingchain.conf', - suffixWidget: Padding( - padding: const EdgeInsets.only(left: SailStyleValues.padding08), - child: SailTextButton( - label: 'Read file', - onPressed: settings.readAndSetValuesFromFile, - ), + suffixWidget: SailTextButton( + label: 'Read file', + onPressed: settings.readAndSetValuesFromFile, ), ), SailTextField( diff --git a/packages/sail_ui/lib/widgets/core/sail_shadow.dart b/packages/sail_ui/lib/widgets/core/sail_shadow.dart index 4c93cd7a..1a0f53d0 100644 --- a/packages/sail_ui/lib/widgets/core/sail_shadow.dart +++ b/packages/sail_ui/lib/widgets/core/sail_shadow.dart @@ -71,11 +71,13 @@ class SailShadow extends StatelessWidget { class SailErrorShadow extends StatelessWidget { final bool enabled; final Widget child; + final bool small; const SailErrorShadow({ super.key, required this.enabled, required this.child, + this.small = false, }); @override @@ -92,21 +94,21 @@ class SailErrorShadow extends StatelessWidget { BoxShadow( color: theme.colors.error, spreadRadius: 0, - blurRadius: 24, + blurRadius: small ? 6 : 24, offset: const Offset(0, 5), ), // Left shadow BoxShadow( color: theme.colors.error, spreadRadius: 0, - blurRadius: 24, + blurRadius: small ? 6 : 24, offset: const Offset(-5, 0), ), // Right shadow BoxShadow( color: theme.colors.error, spreadRadius: 0, - blurRadius: 24, + blurRadius: small ? 6 : 24, offset: const Offset(5, 0), ), ], diff --git a/packages/sail_ui/lib/widgets/inputs/text_field.dart b/packages/sail_ui/lib/widgets/inputs/text_field.dart index 16d91071..47ce0378 100644 --- a/packages/sail_ui/lib/widgets/inputs/text_field.dart +++ b/packages/sail_ui/lib/widgets/inputs/text_field.dart @@ -10,6 +10,7 @@ enum TextFieldType { number, bitcoin, text } class SailTextField extends StatelessWidget { final TextEditingController controller; + final FocusNode? focusNode; final String? label; final String hintText; final String? suffix; @@ -20,6 +21,7 @@ class SailTextField extends StatelessWidget { final Widget? prefixIcon; final BoxConstraints? prefixIconConstraints; final TextFieldSize size; + final void Function(String)? onSubmitted; const SailTextField({ super.key, @@ -34,6 +36,8 @@ class SailTextField extends StatelessWidget { this.prefixIcon, this.prefixIconConstraints, this.size = TextFieldSize.regular, + this.focusNode, + this.onSubmitted, }); @override @@ -62,6 +66,8 @@ class SailTextField extends StatelessWidget { TextField( cursorColor: theme.colors.primary, controller: controller, + focusNode: focusNode, + onSubmitted: onSubmitted, style: TextStyle( color: SailTheme.of(context).colors.text, fontSize: textSize, @@ -86,7 +92,10 @@ class SailTextField extends StatelessWidget { fontSize: textSize, ), suffixText: suffix, - suffix: suffixWidget, + suffix: Padding( + padding: const EdgeInsets.only(left: SailStyleValues.padding08), + child: suffixWidget, + ), prefixStyle: TextStyle( color: SailTheme.of(context).colors.textTertiary, fontSize: textSize, From f3ebeb7608fae738fc874cf71276844c8282b3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 9 Nov 2023 10:35:53 +0100 Subject: [PATCH 4/8] tabs/home: add ethereum rpc tab --- .../tabs/ethereum/ethereum_rpc_tab_page.dart | 31 +++++++++++++++++++ lib/pages/tabs/home_page.dart | 9 ++++-- lib/routing/router.dart | 4 +++ lib/routing/router.gr.dart | 20 ++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart diff --git a/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart new file mode 100644 index 00000000..c4804e75 --- /dev/null +++ b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart @@ -0,0 +1,31 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:sail_ui/sail_ui.dart'; +import 'package:sidesail/console.dart'; +import 'package:sidesail/routing/router.dart'; + +@RoutePage() +class EthereumRPCTabPage extends StatelessWidget { + AppRouter get router => GetIt.I.get(); + + const EthereumRPCTabPage({super.key}); + + @override + Widget build(BuildContext context) { + return const SailPage( + scrollable: true, + title: 'Ethereum RPC', + subtitle: + 'Here you can call eth rpcs directly to the eth-sidechain. Try typing in "eth_blockNumber" in the input below.', + body: Padding( + padding: EdgeInsets.only(bottom: SailStyleValues.padding30), + child: Column( + children: [ + RPCWidget(), + ], + ), + ), + ); + } +} diff --git a/lib/pages/tabs/home_page.dart b/lib/pages/tabs/home_page.dart index a2a3b73f..8eafa805 100644 --- a/lib/pages/tabs/home_page.dart +++ b/lib/pages/tabs/home_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart' as auto_router; +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:logger/logger.dart'; @@ -13,7 +14,7 @@ import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:sidesail/widgets/containers/chain_overview_card.dart'; import 'package:stacked/stacked.dart'; -@auto_router.RoutePage() +@RoutePage() class HomePage extends StatelessWidget { SidechainRPC get _sideRPC => GetIt.I.get(); @@ -34,7 +35,7 @@ class HomePage extends StatelessWidget { body: SideNav( child: children[tabsRouter.activeIndex], // assume settings tab is final tab! - navigateToSettings: () => tabsRouter.setActiveIndex(routes.length - 1), + navigateToSettings: () => tabsRouter.setActiveIndex(routes.length - 2), ), ); }, @@ -59,11 +60,13 @@ class HomePage extends StatelessWidget { const WithdrawalBundleTabRoute(), const BlindMergedMiningTabRoute(), ]; + break; case SidechainType.ethereum: chainRoutes = [ - const DashboardTabRoute(), + const EthereumRPCTabRoute(), ]; + break; } return [ diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 13784908..f2439680 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:sidesail/pages/tabs/dashboard_tab_page.dart'; +import 'package:sidesail/pages/tabs/ethereum/ethereum_rpc_tab_page.dart'; import 'package:sidesail/pages/tabs/home_page.dart'; import 'package:sidesail/pages/tabs/settings/node_settings_tab.dart'; import 'package:sidesail/pages/tabs/settings/theme_settings_tab.dart'; @@ -50,6 +51,9 @@ class AppRouter extends _$AppRouter { AutoRoute( page: BlindMergedMiningTabRoute.page, ), + AutoRoute( + page: EthereumRPCTabRoute.page, + ), AutoRoute( page: NodeSettingsTabRoute.page, ), diff --git a/lib/routing/router.gr.dart b/lib/routing/router.gr.dart index 27101602..ceae0fdb 100644 --- a/lib/routing/router.gr.dart +++ b/lib/routing/router.gr.dart @@ -27,6 +27,12 @@ abstract class _$AppRouter extends RootStackRouter { child: const DashboardTabPage(), ); }, + EthereumRPCTabRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const EthereumRPCTabPage(), + ); + }, HomeRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -104,6 +110,20 @@ class DashboardTabRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [EthereumRPCTabPage] +class EthereumRPCTabRoute extends PageRouteInfo { + const EthereumRPCTabRoute({List? children}) + : super( + EthereumRPCTabRoute.name, + initialChildren: children, + ); + + static const String name = 'EthereumRPCTabRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [HomePage] class HomeRoute extends PageRouteInfo { From db425f6b57331d7c8f2587611909e8b58ac651eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 9 Nov 2023 10:37:14 +0100 Subject: [PATCH 5/8] lib: add rpc console --- lib/console.dart | 118 --------- .../tabs/ethereum/ethereum_rpc_tab_page.dart | 2 +- lib/widgets/containers/tabs/console.dart | 223 ++++++++++++++++++ 3 files changed, 224 insertions(+), 119 deletions(-) delete mode 100644 lib/console.dart create mode 100644 lib/widgets/containers/tabs/console.dart diff --git a/lib/console.dart b/lib/console.dart deleted file mode 100644 index 32ae30ce..00000000 --- a/lib/console.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_highlighter/flutter_highlighter.dart'; -import 'package:flutter_highlighter/themes/github.dart'; -import 'package:get_it/get_it.dart'; -import 'package:sail_ui/widgets/core/sail_text.dart'; -import 'package:sidesail/logger.dart'; -import 'package:sidesail/rpc/rpc_sidechain.dart'; - -class RpcWidget extends StatefulWidget { - const RpcWidget({super.key}); - - @override - RpcWidgetState createState() => RpcWidgetState(); -} - -class RpcWidgetState extends State { - SidechainRPC get rpc => GetIt.I.get(); - final TextEditingController _textController = TextEditingController(); - dynamic _result; - String _command = ''; - String? _error; - - Future _callRpc(String args) async { - if (args.trim().isEmpty) { - throw 'Must provide method name'; - } - - final fields = args.trim().split(' ').where((field) => field.isNotEmpty); - - final start = DateTime.now(); - - List params = []; - if (fields.length > 1) { - params = fields.skip(1).toList(); - } - - final method = fields.first; - var res = await rpc.callRAW(method, params); - - log.t( - 'bitcoin core: $method completed in ${DateTime.now().difference(start)}', - error: jsonEncode(res), - ); - - return res; - } - - void _handleSubmit() async { - try { - var res = await _callRpc(_textController.text); - - setState(() { - _command = _textController.text; - _result = res; - _error = null; - }); - } catch (e) { - setState(() { - _result = null; - _error = e.toString(); - }); - } - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Row( - children: [ - SailText.primary13( - 'RPC:', - ), - Expanded( - child: TextField( - controller: _textController, - ), - ), - ElevatedButton( - onPressed: _handleSubmit, - child: SailText.primary13('Submit'), - ), - ], - ), - if (_result != null) _JsonViewer(_result), - if (_error != null) SailText.primary13('Error: $_command: $_error'), - ], - ), - ); - } -} - -class _JsonViewer extends StatelessWidget { - final dynamic json; - - const _JsonViewer(this.json); - - String prettyPrintJson(dynamic json) { - JsonEncoder encoder = const JsonEncoder.withIndent(' '); - var printed = encoder.convert(json); - return printed; - } - - @override - Widget build(BuildContext context) { - return HighlightView( - prettyPrintJson(json), - language: 'json', - theme: githubTheme, - textStyle: const TextStyle(fontFamily: 'monospace'), - padding: const EdgeInsets.all(12), - ); - } -} diff --git a/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart index c4804e75..bd9de730 100644 --- a/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart +++ b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart @@ -2,8 +2,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:sail_ui/sail_ui.dart'; -import 'package:sidesail/console.dart'; import 'package:sidesail/routing/router.dart'; +import 'package:sidesail/widgets/containers/tabs/console.dart'; @RoutePage() class EthereumRPCTabPage extends StatelessWidget { diff --git a/lib/widgets/containers/tabs/console.dart b/lib/widgets/containers/tabs/console.dart new file mode 100644 index 00000000..37001fb5 --- /dev/null +++ b/lib/widgets/containers/tabs/console.dart @@ -0,0 +1,223 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:sail_ui/sail_ui.dart'; +import 'package:sail_ui/theme/theme.dart'; +import 'package:sail_ui/widgets/core/sail_text.dart'; +import 'package:sidesail/logger.dart'; +import 'package:sidesail/rpc/rpc_ethereum.dart'; + +class RPCWidget extends StatefulWidget { + const RPCWidget({super.key}); + + @override + RPCWidgetState createState() => RPCWidgetState(); +} + +class RPCWidgetState extends State { + EthereumRPC get rpc => GetIt.I.get(); + dynamic _result; + String _command = ''; + String? _error; + + Future _callRpc(String args) async { + if (args.trim().isEmpty) { + throw 'Must provide method name'; + } + + final fields = args.trim().split(' ').where((field) => field.isNotEmpty); + + final start = DateTime.now(); + + List params = []; + if (fields.length > 1) { + params = fields.skip(1).toList(); + } + + final method = fields.first; + var res = await rpc.callRAW(method, params); + + log.t( + 'eth: $method completed in ${DateTime.now().difference(start)}', + error: jsonEncode(res), + ); + + return res; + } + + void _handleSubmit(String selection) async { + try { + var res = await _callRpc(selection); + + setState(() { + _command = selection; + _result = res; + _error = null; + }); + } catch (e) { + setState(() { + _command = selection; + _result = null; + _error = e.toString(); + }); + } + } + + @override + Widget build(BuildContext context) { + final theme = SailTheme.of(context); + + return SailColumn( + spacing: SailStyleValues.padding08, + children: [ + Row( + children: [ + Expanded( + child: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + return const Iterable.empty(); + } + return ethRpcMethods.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + optionsMaxHeight: 500, + fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { + return SailTextField( + controller: textEditingController, + label: 'RPC command', + prefix: '> ', + hintText: 'Enter rpc command here', + focusNode: focusNode, + onSubmitted: _handleSubmit, + suffixWidget: SailTextButton( + label: 'Submit', + onPressed: () { + _handleSubmit(textEditingController.text); + }, + ), + ); + }, + optionsViewBuilder: (context, onSelected, options) { + return Scaffold( + backgroundColor: theme.colors.background, + body: Padding( + padding: const EdgeInsets.only(left: SailStyleValues.padding30), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: SailColumn( + spacing: SailStyleValues.padding08, + withDivider: true, + children: [ + for (final opt in options) + SailScaleButton( + onPressed: () => onSelected(opt), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: SailStyleValues.padding10, + horizontal: SailStyleValues.padding10, + ), + child: SailText.primary12(opt), + ), + ), + ], + ), + ), + ), + ); + }, + onSelected: (String selection) { + _handleSubmit(selection); + }, + ), + ), + ], + ), + if (_result != null) _JSONView(_result), + if (_error != null) _ErrorView('$_command: $_error'), + ], + ); + } +} + +class _JSONView extends StatelessWidget { + final dynamic json; + + const _JSONView(this.json); + + String prettyPrintJson(dynamic json) { + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + var printed = encoder.convert(json); + return printed; + } + + @override + Widget build(BuildContext context) { + return SailRawCard( + padding: true, + child: SailColumn( + spacing: SailStyleValues.padding50, + mainAxisSize: MainAxisSize.min, + children: [ + SailColumn( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: SailStyleValues.padding08, + withDivider: true, + trailingSpacing: true, + children: [ + const ActionHeaderChip(title: 'Response'), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: SailStyleValues.padding10, + ), + child: SailText.primary12(json.toString()), + ), + ], + ), + ], + ), + ); + } +} + +class _ErrorView extends StatelessWidget { + final String error; + + const _ErrorView(this.error); + + @override + Widget build(BuildContext context) { + return SailRawCard( + padding: true, + child: SailColumn( + spacing: SailStyleValues.padding50, + mainAxisSize: MainAxisSize.min, + children: [ + SailColumn( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: SailStyleValues.padding08, + withDivider: true, + trailingSpacing: true, + children: [ + const SailErrorShadow( + enabled: true, + small: true, + child: ActionHeaderChip(title: 'Error'), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: SailStyleValues.padding10, + ), + child: SailText.primary12(error), + ), + ], + ), + ], + ), + ); + } +} From 9dfce3a78d984c5db57caca3de3056656b670752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 9 Nov 2023 10:37:30 +0100 Subject: [PATCH 6/8] rpc: complete initial ethereumrpc implementation --- lib/rpc/{rpc_eth.dart => rpc_ethereum.dart} | 55 ++++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) rename lib/rpc/{rpc_eth.dart => rpc_ethereum.dart} (59%) diff --git a/lib/rpc/rpc_eth.dart b/lib/rpc/rpc_ethereum.dart similarity index 59% rename from lib/rpc/rpc_eth.dart rename to lib/rpc/rpc_ethereum.dart index 94995579..a889a5b9 100644 --- a/lib/rpc/rpc_eth.dart +++ b/lib/rpc/rpc_ethereum.dart @@ -1,18 +1,20 @@ import 'package:http/http.dart'; +import 'package:sidesail/config/sidechains.dart'; import 'package:sidesail/pages/tabs/settings/node_settings_tab.dart'; -import 'package:sidesail/rpc/rpc.dart'; +import 'package:sidesail/rpc/models/core_transaction.dart'; +import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:web3dart/web3dart.dart'; -abstract class EthereumRPC extends RPCConnection { - Future call(String rpc, [args]); -} +abstract class EthereumRPC extends SidechainRPC {} class EthereumRPCLive extends EthereumRPC { + final sgweiPerSat = 1000000000; + late Web3Client _client; @override - Future call(String rpc, [args]) async { - return _client.makeRPCCall(rpc, args); + Future callRAW(String method, [params]) async { + return _client.makeRPCCall(method, params); } // Apparently Ethereum doesn't have a conf file? @@ -22,7 +24,9 @@ class EthereumRPCLive extends EthereumRPC { _client = Web3Client(url, Client()); } - EthereumRPCLive._create(); + EthereumRPCLive._create() { + chain = EthereumSidechain(); + } static Future create() async { final rpc = EthereumRPCLive._create(); @@ -45,6 +49,42 @@ class EthereumRPCLive extends EthereumRPC { await _client.getChainId(); return; } + + @override + Future<(double, double)> getBalance() async { + final account = await _account(); + final balance = await _client.getBalance(account); + return (balance.getInEther.toDouble(), 0.0); + } + + Future _account() async { + final accountFut = await callRAW('eth_accounts'); + final accounts = await accountFut as List; + + if (accounts.isEmpty) { + throw Exception('Create account from cli using personal.newAccount before getting balance'); + } + + return EthereumAddress.fromHex(accounts[0] as String); + } + + Future _deposit(int amountSat, int feeSat) async { + final amount = sgweiPerSat * amountSat; + final fee = sgweiPerSat * feeSat; + final account = await _account(); + final deposit = await callRAW('eth_deposit', [account.hex, _toHex(amount), _toHex(fee)]); + return deposit as bool; + } + + String _toHex(int number) { + return '0x${number.toRadixString(16)}'; + } + + @override + Future> listTransactions() async { + // TODO: Implement listtransactions + return List.empty(); + } } /// List of all known RPC methods available / @@ -54,7 +94,6 @@ final ethRpcMethods = [ 'net_version', 'net_listening', 'net_peerCount', - 'eth_protocolVersion', 'eth_syncing', 'eth_coinbase', 'eth_chainId', From 90b6f1bedbc7cf1fbb97f4ccdce2d3f0f1285e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 9 Nov 2023 19:13:56 +0100 Subject: [PATCH 7/8] multi: misc improvements for swapping chains --- lib/config/dependencies.dart | 69 ++++---- .../tabs/ethereum/ethereum_rpc_tab_page.dart | 3 +- lib/pages/tabs/home_page.dart | 60 +++---- .../tabs/sidechain_explorer_tab_page.dart | 14 +- .../mainchain/withdrawal_bundle_tab_page.dart | 1 + lib/providers/balance_provider.dart | 2 + lib/routing/router.gr.dart | 2 +- lib/rpc/rpc_ethereum.dart | 1 + lib/rpc/rpc_sidechain.dart | 2 +- lib/widgets/containers/tabs/console.dart | 147 ++++++++---------- .../containers/single_value_container.dart | 30 ++-- 11 files changed, 155 insertions(+), 176 deletions(-) diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 6beaefe7..7282cf42 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -1,5 +1,4 @@ import 'package:get_it/get_it.dart'; -import 'package:sidesail/app.dart'; import 'package:sidesail/config/sidechains.dart'; import 'package:sidesail/providers/balance_provider.dart'; import 'package:sidesail/providers/transactions_provider.dart'; @@ -13,21 +12,15 @@ import 'package:sidesail/storage/secure_store.dart'; // register all global dependencies, for use in views, or in view models // each dependency can only be registered once -Future initGetitDependencies(Sidechain chain) async { +Future initGetitDependencies(Sidechain initialChain) async { final mainFuture = MainchainRPCLive.create(); - final ethFuture = EthereumRPCLive.create(); - await setSidechainRPC(chain); + await _initSidechainRPC(initialChain); final mainRPC = await mainFuture; - final ethRPC = await ethFuture; GetIt.I.registerLazySingleton( () => mainRPC, ); - GetIt.I.registerLazySingleton( - () => ethRPC, - ); - GetIt.I.registerLazySingleton( () => AppRouter(), ); @@ -46,31 +39,51 @@ Future initGetitDependencies(Sidechain chain) async { ); } -// register all global dependencies, for use in views, or in view models -// each dependency can only be registered once -Future setSidechainRPC(Sidechain chain) async { +// register all rpc connections. We attempt to create all +// rpcs in parallell, so they're ready instantly when swapping +// we can also query the balance +Future _initSidechainRPC(Sidechain chain) async { + final ethFuture = EthereumRPCLive.create(); + final testFuture = TestchainRPCLive.create(); + + final ethRPC = await ethFuture; + final testRPC = await testFuture; + + GetIt.I.registerLazySingleton( + () => testRPC, + ); + GetIt.I.registerLazySingleton( + () => ethRPC, + ); + SidechainRPC sidechainRPC; switch (chain.type) { case SidechainType.testChain: - final testchainRPC = await TestchainRPCLive.create(); - if (GetIt.I.isRegistered()) { - GetIt.I.unregister(); - } - GetIt.I.registerLazySingleton( - () => testchainRPC, - ); - sidechainRPC = testchainRPC; + sidechainRPC = testRPC; break; case SidechainType.ethereum: - final ethRPC = await EthereumRPCLive.create(); - if (GetIt.I.isRegistered()) { - GetIt.I.unregister(); - } - GetIt.I.registerLazySingleton( - () => ethRPC, - ); + sidechainRPC = ethRPC; + break; + } + + GetIt.I.registerLazySingleton( + () => sidechainRPC, + ); +} +// swap out the SidechainRPC type in GetIt with the appropriate +// rpc. All of them are already registered, so this is pretty quick +void swapSidechainRPC(Sidechain chain) async { + SidechainRPC sidechainRPC; + switch (chain.type) { + case SidechainType.testChain: + final testchainRPC = GetIt.I.get(); + sidechainRPC = testchainRPC; + break; + + case SidechainType.ethereum: + final ethRPC = GetIt.I.get(); sidechainRPC = ethRPC; break; } @@ -81,6 +94,4 @@ Future setSidechainRPC(Sidechain chain) async { GetIt.I.registerLazySingleton( () => sidechainRPC, ); - - SailApp.sailAppKey.currentState?.rebuildUI(); } diff --git a/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart index bd9de730..fc1d1c59 100644 --- a/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart +++ b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart @@ -16,8 +16,7 @@ class EthereumRPCTabPage extends StatelessWidget { return const SailPage( scrollable: true, title: 'Ethereum RPC', - subtitle: - 'Here you can call eth rpcs directly to the eth-sidechain. Try typing in "eth_blockNumber" in the input below.', + subtitle: 'Call RPCs directly to the Ethereum sidechain. Try typing in "eth_blockNumber" in the input below.', body: Padding( padding: EdgeInsets.only(bottom: SailStyleValues.padding30), child: Column( diff --git a/lib/pages/tabs/home_page.dart b/lib/pages/tabs/home_page.dart index 8eafa805..67afbaa4 100644 --- a/lib/pages/tabs/home_page.dart +++ b/lib/pages/tabs/home_page.dart @@ -16,20 +16,32 @@ import 'package:stacked/stacked.dart'; @RoutePage() class HomePage extends StatelessWidget { - SidechainRPC get _sideRPC => GetIt.I.get(); - const HomePage({super.key}); @override Widget build(BuildContext context) { - final routes = routesForChain(_sideRPC.chain); final theme = SailTheme.of(context); + const routes = [ + // common routes + SidechainExplorerTabRoute(), + // testchain routes + DashboardTabRoute(), + TransferMainchainTabRoute(), + WithdrawalBundleTabRoute(), + BlindMergedMiningTabRoute(), + + // ethereum routes + EthereumRPCTabRoute(), + + // trailing common routes + NodeSettingsTabRoute(), + ThemeSettingsTabRoute(), + ]; return auto_router.AutoTabsRouter.builder( homeIndex: 1, routes: routes, - builder: (context, children, _) { - final tabsRouter = auto_router.AutoTabsRouter.of(context); + builder: (context, children, tabsRouter) { return Scaffold( backgroundColor: theme.colors.background, body: SideNav( @@ -41,40 +53,6 @@ class HomePage extends StatelessWidget { }, ); } - - List> routesForChain(Sidechain chain) { - final preRoutes = [ - const SidechainExplorerTabRoute(), - ]; - final postRoutes = [ - const NodeSettingsTabRoute(), - const ThemeSettingsTabRoute(), - ]; - - List> chainRoutes = []; - switch (chain.type) { - case SidechainType.testChain: - chainRoutes = [ - const DashboardTabRoute(), - const TransferMainchainTabRoute(), - const WithdrawalBundleTabRoute(), - const BlindMergedMiningTabRoute(), - ]; - break; - - case SidechainType.ethereum: - chainRoutes = [ - const EthereumRPCTabRoute(), - ]; - break; - } - - return [ - ...preRoutes, - ...chainRoutes, - ...postRoutes, - ]; - } } class SideNav extends StatefulWidget { @@ -222,9 +200,9 @@ class _SideNavState extends State { NavEntry( title: '${viewModel.chain.name} Dashboard', icon: SailSVGAsset.iconDashboardTab, - selected: tabsRouter.activeIndex == 1, + selected: tabsRouter.activeIndex == 5, onPressed: () { - tabsRouter.setActiveIndex(1); + tabsRouter.setActiveIndex(5); }, ), ]; diff --git a/lib/pages/tabs/sidechain_explorer_tab_page.dart b/lib/pages/tabs/sidechain_explorer_tab_page.dart index 2394be12..9272eb51 100644 --- a/lib/pages/tabs/sidechain_explorer_tab_page.dart +++ b/lib/pages/tabs/sidechain_explorer_tab_page.dart @@ -4,13 +4,18 @@ import 'package:get_it/get_it.dart'; import 'package:sail_ui/sail_ui.dart'; import 'package:sidesail/config/sidechains.dart'; import 'package:sidesail/providers/balance_provider.dart'; +import 'package:sidesail/rpc/rpc_ethereum.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; +import 'package:sidesail/rpc/rpc_testchain.dart'; import 'package:sidesail/widgets/containers/chain_overview_card.dart'; import 'package:sidesail/widgets/containers/tabs/dashboard_tab_widgets.dart'; import 'package:stacked/stacked.dart'; @RoutePage() class SidechainExplorerTabPage extends StatelessWidget { + TestchainRPC get test => GetIt.I.get(); + EthereumRPC get eth => GetIt.I.get(); + const SidechainExplorerTabPage({super.key}); @override @@ -36,9 +41,7 @@ class SidechainExplorerTabPage extends StatelessWidget { unconfirmedBalance: viewModel.pendingBalance, highlighted: false, currentChain: viewModel.chain.type == SidechainType.testChain, - onPressed: () { - viewModel.setSidechainRPC(TestSidechain()); - }, + onPressed: () => viewModel.setSidechainRPC(TestSidechain()), ), ChainOverviewCard( chain: EthereumSidechain(), @@ -46,9 +49,7 @@ class SidechainExplorerTabPage extends StatelessWidget { unconfirmedBalance: viewModel.pendingBalance, highlighted: false, currentChain: viewModel.chain.type == SidechainType.ethereum, - onPressed: () { - viewModel.setSidechainRPC(EthereumSidechain()); - }, + onPressed: () => viewModel.setSidechainRPC(EthereumSidechain()), ), ], ), @@ -83,6 +84,7 @@ class SidechainExplorerTabViewModel extends BaseViewModel { void setSidechainRPC(Sidechain chain) { _sideRPC.setChain(chain); + notifyListeners(); } @override diff --git a/lib/pages/tabs/testchain/mainchain/withdrawal_bundle_tab_page.dart b/lib/pages/tabs/testchain/mainchain/withdrawal_bundle_tab_page.dart index ae7179d6..2bc49627 100644 --- a/lib/pages/tabs/testchain/mainchain/withdrawal_bundle_tab_page.dart +++ b/lib/pages/tabs/testchain/mainchain/withdrawal_bundle_tab_page.dart @@ -283,6 +283,7 @@ class _UnbundledWithdrawalViewState extends State { ), ), copyable: false, + label: '', value: '${satoshiToBTC(widget.withdrawal.amountSatoshi).toStringAsFixed(8)} BTC to ${widget.withdrawal.address}', ), diff --git a/lib/providers/balance_provider.dart b/lib/providers/balance_provider.dart index bd3b02a1..47d2d5fe 100644 --- a/lib/providers/balance_provider.dart +++ b/lib/providers/balance_provider.dart @@ -20,6 +20,7 @@ class BalanceProvider extends ChangeNotifier { BalanceProvider() { fetch(); _startPolling(); + _rpc.addListener(fetch); } // call this function from anywhere to refresh the balance @@ -45,5 +46,6 @@ class BalanceProvider extends ChangeNotifier { super.dispose(); // Cancel timer when provider is disposed (never?) _timer.cancel(); + _rpc.removeListener(notifyListeners); } } diff --git a/lib/routing/router.gr.dart b/lib/routing/router.gr.dart index ceae0fdb..cb674d0d 100644 --- a/lib/routing/router.gr.dart +++ b/lib/routing/router.gr.dart @@ -36,7 +36,7 @@ abstract class _$AppRouter extends RootStackRouter { HomeRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: const HomePage(), + child: HomePage(), ); }, NodeSettingsTabRoute.name: (routeData) { diff --git a/lib/rpc/rpc_ethereum.dart b/lib/rpc/rpc_ethereum.dart index a889a5b9..75670bca 100644 --- a/lib/rpc/rpc_ethereum.dart +++ b/lib/rpc/rpc_ethereum.dart @@ -68,6 +68,7 @@ class EthereumRPCLive extends EthereumRPC { return EthereumAddress.fromHex(accounts[0] as String); } + // ignore: unused_element Future _deposit(int amountSat, int feeSat) async { final amount = sgweiPerSat * amountSat; final fee = sgweiPerSat * feeSat; diff --git a/lib/rpc/rpc_sidechain.dart b/lib/rpc/rpc_sidechain.dart index c36cde3f..6ed15db2 100644 --- a/lib/rpc/rpc_sidechain.dart +++ b/lib/rpc/rpc_sidechain.dart @@ -15,7 +15,7 @@ abstract class SidechainRPC extends RPCConnection { late Sidechain chain; void setChain(Sidechain newChain) { chain = newChain; - setSidechainRPC(newChain); + swapSidechainRPC(newChain); notifyListeners(); } } diff --git a/lib/widgets/containers/tabs/console.dart b/lib/widgets/containers/tabs/console.dart index 37001fb5..887b1d1d 100644 --- a/lib/widgets/containers/tabs/console.dart +++ b/lib/widgets/containers/tabs/console.dart @@ -7,6 +7,7 @@ import 'package:sail_ui/theme/theme.dart'; import 'package:sail_ui/widgets/core/sail_text.dart'; import 'package:sidesail/logger.dart'; import 'package:sidesail/rpc/rpc_ethereum.dart'; +import 'package:sidesail/widgets/containers/tabs/dashboard_tab_widgets.dart'; class RPCWidget extends StatefulWidget { const RPCWidget({super.key}); @@ -15,11 +16,24 @@ class RPCWidget extends StatefulWidget { RPCWidgetState createState() => RPCWidgetState(); } +class Result { + late int id; + final String command; + final String? success; + final String? error; + + Result({ + required this.command, + required this.success, + required this.error, + }) { + id = UniqueKey().hashCode; + } +} + class RPCWidgetState extends State { EthereumRPC get rpc => GetIt.I.get(); - dynamic _result; - String _command = ''; - String? _error; + List results = []; Future _callRpc(String args) async { if (args.trim().isEmpty) { @@ -50,18 +64,26 @@ class RPCWidgetState extends State { try { var res = await _callRpc(selection); - setState(() { - _command = selection; - _result = res; - _error = null; - }); + results.insert( + 0, + Result( + command: selection, + success: res.toString(), + error: null, + ), + ); } catch (e) { - setState(() { - _command = selection; - _result = null; - _error = e.toString(); - }); + results.insert( + 0, + Result( + command: selection, + success: null, + error: e.toString(), + ), + ); } + + setState(() {}); } @override @@ -135,86 +157,49 @@ class RPCWidgetState extends State { ), ], ), - if (_result != null) _JSONView(_result), - if (_error != null) _ErrorView('$_command: $_error'), + DashboardGroup( + title: 'Responses', + children: [ + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: results.length, + itemBuilder: (context, index) => ResultView( + key: ValueKey(results[index].id), + result: results[index], + ), + ), + ], + ), ], ); } } -class _JSONView extends StatelessWidget { - final dynamic json; +class ResultView extends StatelessWidget { + final Result result; - const _JSONView(this.json); - - String prettyPrintJson(dynamic json) { - JsonEncoder encoder = const JsonEncoder.withIndent(' '); - var printed = encoder.convert(json); - return printed; - } + const ResultView({super.key, required this.result}); @override Widget build(BuildContext context) { - return SailRawCard( - padding: true, - child: SailColumn( - spacing: SailStyleValues.padding50, - mainAxisSize: MainAxisSize.min, - children: [ - SailColumn( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: SailStyleValues.padding08, - withDivider: true, - trailingSpacing: true, - children: [ - const ActionHeaderChip(title: 'Response'), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: SailStyleValues.padding10, - ), - child: SailText.primary12(json.toString()), - ), - ], - ), - ], + return Padding( + padding: const EdgeInsets.symmetric( + vertical: SailStyleValues.padding15, + horizontal: SailStyleValues.padding10, ), - ); - } -} - -class _ErrorView extends StatelessWidget { - final String error; - - const _ErrorView(this.error); - - @override - Widget build(BuildContext context) { - return SailRawCard( - padding: true, child: SailColumn( - spacing: SailStyleValues.padding50, - mainAxisSize: MainAxisSize.min, + spacing: SailStyleValues.padding08, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SailColumn( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: SailStyleValues.padding08, - withDivider: true, - trailingSpacing: true, - children: [ - const SailErrorShadow( - enabled: true, - small: true, - child: ActionHeaderChip(title: 'Error'), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: SailStyleValues.padding10, - ), - child: SailText.primary12(error), - ), - ], + SingleValueContainer( + width: 95, + icon: result.error == null + ? SailSVG.icon(SailSVGAsset.iconConfirmed, width: 13) + : SailSVG.icon(SailSVGAsset.iconFailed, width: 13), + copyable: true, + label: result.command.split(' ').first, + value: result.error ?? result.success, ), ], ), diff --git a/packages/sail_ui/lib/widgets/containers/single_value_container.dart b/packages/sail_ui/lib/widgets/containers/single_value_container.dart index 8a8b873d..356b9467 100644 --- a/packages/sail_ui/lib/widgets/containers/single_value_container.dart +++ b/packages/sail_ui/lib/widgets/containers/single_value_container.dart @@ -5,7 +5,7 @@ import 'package:sail_ui/widgets/core/sail_snackbar.dart'; import 'package:sail_ui/widgets/core/sail_text.dart'; class SingleValueContainer extends StatelessWidget { - final String? label; + final String label; final dynamic value; final double width; final String? trailingText; @@ -14,7 +14,7 @@ class SingleValueContainer extends StatelessWidget { const SingleValueContainer({ super.key, - this.label, + required this.label, required this.value, required this.width, this.trailingText, @@ -33,21 +33,21 @@ class SingleValueContainer extends StatelessWidget { const SizedBox( width: 13, ), - if (label != null) - SizedBox( - width: width, - child: SailText.secondary12(label!), + SizedBox( + width: width, + child: SailText.secondary12(label), + ), + Expanded( + child: SailScaleButton( + onPressed: copyable + ? () { + Clipboard.setData(ClipboardData(text: value.toString())); + showSnackBar(context, 'Copied $label'); + } + : null, + child: SailText.primary12(value.toString()), ), - SailScaleButton( - onPressed: copyable - ? () { - Clipboard.setData(ClipboardData(text: value.toString())); - showSnackBar(context, 'Copied $label'); - } - : null, - child: SailText.primary12(value.toString()), ), - Expanded(child: Container()), if (trailingText != null) SailText.secondary12(trailingText!), ], ); From 6d6ec2fa9325de10df7f7153147939228b39041e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Fri, 10 Nov 2023 00:23:25 +0100 Subject: [PATCH 8/8] rpc: introduce SidechainSubRPC for quick changing of sidechains --- lib/config/dependencies.dart | 32 ++--------- .../tabs/sidechain_explorer_tab_page.dart | 8 +-- lib/rpc/rpc_ethereum.dart | 2 +- lib/rpc/rpc_sidechain.dart | 55 +++++++++++++++++-- lib/rpc/rpc_testchain.dart | 2 +- test/dashboard_test.dart | 2 +- test/mocks/rpc_mock_sidechain.dart | 38 ++++++++++++- 7 files changed, 98 insertions(+), 41 deletions(-) diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 7282cf42..9081422c 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -56,42 +56,18 @@ Future _initSidechainRPC(Sidechain chain) async { () => ethRPC, ); - SidechainRPC sidechainRPC; + SidechainSubRPC sidechainSubRPC; switch (chain.type) { case SidechainType.testChain: - sidechainRPC = testRPC; + sidechainSubRPC = testRPC; break; case SidechainType.ethereum: - sidechainRPC = ethRPC; + sidechainSubRPC = ethRPC; break; } GetIt.I.registerLazySingleton( - () => sidechainRPC, - ); -} - -// swap out the SidechainRPC type in GetIt with the appropriate -// rpc. All of them are already registered, so this is pretty quick -void swapSidechainRPC(Sidechain chain) async { - SidechainRPC sidechainRPC; - switch (chain.type) { - case SidechainType.testChain: - final testchainRPC = GetIt.I.get(); - sidechainRPC = testchainRPC; - break; - - case SidechainType.ethereum: - final ethRPC = GetIt.I.get(); - sidechainRPC = ethRPC; - break; - } - - if (GetIt.I.isRegistered()) { - GetIt.I.unregister(); - } - GetIt.I.registerLazySingleton( - () => sidechainRPC, + () => SidechainRPC(subRPC: sidechainSubRPC), ); } diff --git a/lib/pages/tabs/sidechain_explorer_tab_page.dart b/lib/pages/tabs/sidechain_explorer_tab_page.dart index 9272eb51..e67ee2e7 100644 --- a/lib/pages/tabs/sidechain_explorer_tab_page.dart +++ b/lib/pages/tabs/sidechain_explorer_tab_page.dart @@ -41,7 +41,7 @@ class SidechainExplorerTabPage extends StatelessWidget { unconfirmedBalance: viewModel.pendingBalance, highlighted: false, currentChain: viewModel.chain.type == SidechainType.testChain, - onPressed: () => viewModel.setSidechainRPC(TestSidechain()), + onPressed: () => viewModel.setSidechainRPC(test), ), ChainOverviewCard( chain: EthereumSidechain(), @@ -49,7 +49,7 @@ class SidechainExplorerTabPage extends StatelessWidget { unconfirmedBalance: viewModel.pendingBalance, highlighted: false, currentChain: viewModel.chain.type == SidechainType.ethereum, - onPressed: () => viewModel.setSidechainRPC(EthereumSidechain()), + onPressed: () => viewModel.setSidechainRPC(eth), ), ], ), @@ -82,8 +82,8 @@ class SidechainExplorerTabViewModel extends BaseViewModel { _sideRPC.addListener(notifyListeners); } - void setSidechainRPC(Sidechain chain) { - _sideRPC.setChain(chain); + void setSidechainRPC(SidechainSubRPC sideSubRPC) { + _sideRPC.setSubRPC(sideSubRPC); notifyListeners(); } diff --git a/lib/rpc/rpc_ethereum.dart b/lib/rpc/rpc_ethereum.dart index 75670bca..b89270e9 100644 --- a/lib/rpc/rpc_ethereum.dart +++ b/lib/rpc/rpc_ethereum.dart @@ -5,7 +5,7 @@ import 'package:sidesail/rpc/models/core_transaction.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:web3dart/web3dart.dart'; -abstract class EthereumRPC extends SidechainRPC {} +abstract class EthereumRPC extends SidechainSubRPC {} class EthereumRPCLive extends EthereumRPC { final sgweiPerSat = 1000000000; diff --git a/lib/rpc/rpc_sidechain.dart b/lib/rpc/rpc_sidechain.dart index 6ed15db2..bbb3fa09 100644 --- a/lib/rpc/rpc_sidechain.dart +++ b/lib/rpc/rpc_sidechain.dart @@ -1,23 +1,68 @@ import 'dart:async'; -import 'package:sidesail/config/dependencies.dart'; import 'package:sidesail/config/sidechains.dart'; +import 'package:sidesail/pages/tabs/settings/node_settings_tab.dart'; import 'package:sidesail/rpc/models/core_transaction.dart'; import 'package:sidesail/rpc/rpc.dart'; /// RPC connection for all sidechain nodes -abstract class SidechainRPC extends RPCConnection { +abstract class SidechainSubRPC extends RPCConnection { Future callRAW(String method, [List? params]); Future<(double, double)> getBalance(); Future> listTransactions(); late Sidechain chain; - void setChain(Sidechain newChain) { - chain = newChain; - swapSidechainRPC(newChain); +} + +/// RPC connection for all sidechain nodes +class SidechainRPC extends SidechainSubRPC { + SidechainSubRPC subRPC; + + SidechainRPC({ + required this.subRPC, + }) { + chain = subRPC.chain; + } + + // values for tracking connection state, and error (if any) + @override + SingleNodeConnectionSettings get connectionSettings => subRPC.connectionSettings; + @override + bool get connected => subRPC.connected; + @override + String? get connectionError => subRPC.connectionError; + + void setSubRPC(SidechainSubRPC newSubRPC) { + subRPC = newSubRPC; + chain = subRPC.chain; notifyListeners(); } + + @override + Future callRAW(String method, [List? params]) async { + return await subRPC.callRAW(method, params); + } + + @override + Future<(double, double)> getBalance() async { + return subRPC.getBalance(); + } + + @override + Future> listTransactions() { + return subRPC.listTransactions(); + } + + @override + Future createClient() async { + return subRPC.createClient(); + } + + @override + Future ping() async { + return subRPC.ping(); + } } class RPCError { diff --git a/lib/rpc/rpc_testchain.dart b/lib/rpc/rpc_testchain.dart index 0afc11af..de84aeae 100644 --- a/lib/rpc/rpc_testchain.dart +++ b/lib/rpc/rpc_testchain.dart @@ -15,7 +15,7 @@ import 'package:sidesail/rpc/rpc_sidechain.dart'; import 'package:sidesail/rpc/rpc_withdrawal_bundle.dart'; /// RPC connection the sidechain node. -abstract class TestchainRPC extends SidechainRPC { +abstract class TestchainRPC extends SidechainSubRPC { Future mainSend( String address, double amount, diff --git a/test/dashboard_test.dart b/test/dashboard_test.dart index 741da6ee..46582c29 100644 --- a/test/dashboard_test.dart +++ b/test/dashboard_test.dart @@ -21,7 +21,7 @@ final txProvider = TransactionsProvider(); void main() { setUpAll(() async { - GetIt.I.registerLazySingleton(() => MockSidechainRPC()); + GetIt.I.registerLazySingleton(() => MockSidechainRPC(subRPC: MockSidechainSubRPC())); GetIt.I.registerLazySingleton(() => MockMainchainRPC()); GetIt.I.registerLazySingleton(() => txProvider); diff --git a/test/mocks/rpc_mock_sidechain.dart b/test/mocks/rpc_mock_sidechain.dart index 98828556..1475ac67 100644 --- a/test/mocks/rpc_mock_sidechain.dart +++ b/test/mocks/rpc_mock_sidechain.dart @@ -3,7 +3,43 @@ import 'package:sidesail/rpc/models/core_transaction.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; class MockSidechainRPC extends SidechainRPC { - MockSidechainRPC() { + MockSidechainRPC({required super.subRPC}) { + chain = TestSidechain(); + } + + @override + Future<(double, double)> getBalance() async { + return (1.12345678, 2.24680); + } + + @override + Future callRAW(String method, [dynamic params]) async { + return; + } + + @override + Future<(bool, String?)> testConnection() async { + return (true, null); + } + + @override + Future createClient() async { + return; + } + + @override + Future ping() async { + return; + } + + @override + Future> listTransactions() async { + return List.empty(); + } +} + +class MockSidechainSubRPC extends SidechainSubRPC { + MockSidechainSubRPC() { chain = TestSidechain(); }