diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 4603c684..9081422c 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,9 +12,9 @@ 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(); - await setSidechainRPC(chain); + await _initSidechainRPC(initialChain); final mainRPC = await mainFuture; GetIt.I.registerLazySingleton( @@ -40,40 +39,35 @@ 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 { - SidechainRPC sidechainRPC; +// 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, + ); + + SidechainSubRPC sidechainSubRPC; switch (chain.type) { case SidechainType.testChain: - final testchainRPC = await TestchainRPCLive.create(); - if (GetIt.I.isRegistered()) { - GetIt.I.unregister(); - } - GetIt.I.registerLazySingleton( - () => testchainRPC, - ); - sidechainRPC = testchainRPC; + sidechainSubRPC = testRPC; break; case SidechainType.ethereum: - final testchainRPC = await EthereumRPCLive.create(); - if (GetIt.I.isRegistered()) { - GetIt.I.unregister(); - } - GetIt.I.registerLazySingleton( - () => testchainRPC, - ); - sidechainRPC = testchainRPC; + sidechainSubRPC = ethRPC; break; } - if (GetIt.I.isRegistered()) { - GetIt.I.unregister(); - } GetIt.I.registerLazySingleton( - () => sidechainRPC, + () => SidechainRPC(subRPC: sidechainSubRPC), ); - - SailApp.sailAppKey.currentState?.rebuildUI(); } 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 new file mode 100644 index 00000000..fc1d1c59 --- /dev/null +++ b/lib/pages/tabs/ethereum/ethereum_rpc_tab_page.dart @@ -0,0 +1,30 @@ +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/routing/router.dart'; +import 'package:sidesail/widgets/containers/tabs/console.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: '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( + children: [ + RPCWidget(), + ], + ), + ), + ); + } +} diff --git a/lib/pages/tabs/home_page.dart b/lib/pages/tabs/home_page.dart index a2a3b73f..67afbaa4 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,65 +14,45 @@ 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(); - 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( child: children[tabsRouter.activeIndex], // assume settings tab is final tab! - navigateToSettings: () => tabsRouter.setActiveIndex(routes.length - 1), + navigateToSettings: () => tabsRouter.setActiveIndex(routes.length - 2), ), ); }, ); } - - 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(), - ]; - - case SidechainType.ethereum: - chainRoutes = [ - const DashboardTabRoute(), - ]; - } - - return [ - ...preRoutes, - ...chainRoutes, - ...postRoutes, - ]; - } } class SideNav extends StatefulWidget { @@ -219,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/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/lib/pages/tabs/sidechain_explorer_tab_page.dart b/lib/pages/tabs/sidechain_explorer_tab_page.dart index 2394be12..e67ee2e7 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(test), ), 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(eth), ), ], ), @@ -81,8 +82,9 @@ class SidechainExplorerTabViewModel extends BaseViewModel { _sideRPC.addListener(notifyListeners); } - void setSidechainRPC(Sidechain chain) { - _sideRPC.setChain(chain); + void setSidechainRPC(SidechainSubRPC sideSubRPC) { + _sideRPC.setSubRPC(sideSubRPC); + 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.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..cb674d0d 100644 --- a/lib/routing/router.gr.dart +++ b/lib/routing/router.gr.dart @@ -27,10 +27,16 @@ 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, - child: const HomePage(), + child: HomePage(), ); }, NodeSettingsTabRoute.name: (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 { diff --git a/lib/rpc/rpc_ethereum.dart b/lib/rpc/rpc_ethereum.dart index ff1bedf4..b89270e9 100644 --- a/lib/rpc/rpc_ethereum.dart +++ b/lib/rpc/rpc_ethereum.dart @@ -1,15 +1,29 @@ -import 'dart:async'; - +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/models/core_transaction.dart'; import 'package:sidesail/rpc/rpc_sidechain.dart'; +import 'package:web3dart/web3dart.dart'; -/// RPC connection the sidechain node. -abstract class EthereumRPC extends SidechainRPC {} +abstract class EthereumRPC extends SidechainSubRPC {} class EthereumRPCLive extends EthereumRPC { - // hacky way to create an async class - // https://stackoverflow.com/a/59304510 + final sgweiPerSat = 1000000000; + + late Web3Client _client; + + @override + Future callRAW(String method, [params]) async { + return _client.makeRPCCall(method, params); + } + + // 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() { chain = EthereumSidechain(); } @@ -21,78 +35,107 @@ class EthereumRPCLive extends EthereumRPC { } Future _init() async { - // TODO: Create connection, get inspo from rpc_testchain.dart - } + // TODO: implement authed RPCs + // TODO: make this configurable + connectionSettings = SingleNodeConnectionSettings('', 'localhost', 8545, '', ''); - @override - Future callRAW(String method, [params]) { - // TODO: implement callRAW - throw UnimplementedError(); - } + await createClient(); - @override - Future createClient() { - // TODO: implement createClient - throw UnimplementedError(); + await testConnection(); } @override - Future<(double, double)> getBalance() { - // TODO: implement getBalance - throw UnimplementedError(); + Future ping() async { + await _client.getChainId(); + return; } @override - Future mainBlockCount() { - // TODO: implement mainBlockCount - throw UnimplementedError(); + Future<(double, double)> getBalance() async { + final account = await _account(); + final balance = await _client.getBalance(account); + return (balance.getInEther.toDouble(), 0.0); } - @override - Future mainGenerateAddress() { - // TODO: implement mainGenerateAddress - throw UnimplementedError(); - } + Future _account() async { + final accountFut = await callRAW('eth_accounts'); + final accounts = await accountFut as List; - @override - Future mainSend(String address, double amount, double sidechainFee, double mainchainFee) { - // TODO: implement mainSend - throw UnimplementedError(); - } + if (accounts.isEmpty) { + throw Exception('Create account from cli using personal.newAccount before getting balance'); + } - @override - Future ping() { - // TODO: implement ping - throw UnimplementedError(); + return EthereumAddress.fromHex(accounts[0] as String); } - @override - Future sideBlockCount() { - // TODO: implement sideBlockCount - throw UnimplementedError(); + // ignore: unused_element + 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; } - @override - Future sideGenerateAddress() { - // TODO: implement sideGenerateAddress - throw UnimplementedError(); + String _toHex(int number) { + return '0x${number.toRadixString(16)}'; } @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(); + Future> listTransactions() async { + // TODO: Implement listtransactions + return List.empty(); } } + +/// List of all known RPC methods available / +final ethRpcMethods = [ + 'web3_clientVersion', + 'web3_sha3', + 'net_version', + 'net_listening', + 'net_peerCount', + '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_sidechain.dart b/lib/rpc/rpc_sidechain.dart index c100df68..bbb3fa09 100644 --- a/lib/rpc/rpc_sidechain.dart +++ b/lib/rpc/rpc_sidechain.dart @@ -1,41 +1,68 @@ import 'dart:async'; 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 { - Future<(double, double)> getBalance(); +abstract class SidechainSubRPC extends RPCConnection { + Future callRAW(String method, [List? params]); - 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(); - - // 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; +} + +/// 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 b8a1f300..de84aeae 100644 --- a/lib/rpc/rpc_testchain.dart +++ b/lib/rpc/rpc_testchain.dart @@ -15,7 +15,25 @@ 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, + 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/console.dart b/lib/widgets/containers/tabs/console.dart new file mode 100644 index 00000000..887b1d1d --- /dev/null +++ b/lib/widgets/containers/tabs/console.dart @@ -0,0 +1,208 @@ +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'; +import 'package:sidesail/widgets/containers/tabs/dashboard_tab_widgets.dart'; + +class RPCWidget extends StatefulWidget { + const RPCWidget({super.key}); + + @override + 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(); + List results = []; + + 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); + + results.insert( + 0, + Result( + command: selection, + success: res.toString(), + error: null, + ), + ); + } catch (e) { + results.insert( + 0, + Result( + command: selection, + success: null, + error: e.toString(), + ), + ); + } + + setState(() {}); + } + + @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); + }, + ), + ), + ], + ), + 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 ResultView extends StatelessWidget { + final Result result; + + const ResultView({super.key, required this.result}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: SailStyleValues.padding15, + horizontal: SailStyleValues.padding10, + ), + child: SailColumn( + spacing: SailStyleValues.padding08, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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/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/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!), ], ); 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, 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: 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 f9df25d0..1475ac67 100644 --- a/test/mocks/rpc_mock_sidechain.dart +++ b/test/mocks/rpc_mock_sidechain.dart @@ -3,15 +3,10 @@ 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 mainGenerateAddress() async { - return 'bc1...?'; - } - @override Future<(double, double)> getBalance() async { return (1.12345678, 2.24680); @@ -23,38 +18,39 @@ class MockSidechainRPC extends SidechainRPC { } @override - Future sideEstimateFee() async { - return 0.001; - } - - @override - Future sideGenerateAddress() async { - return 's1deadbeef'; + Future<(bool, String?)> testConnection() async { + return (true, null); } @override - Future mainSend(String address, double amount, double sidechainFee, double mainchainFee) async { - return 'txidmainbeef'; + Future createClient() async { + return; } @override - Future sideSend(String address, double amount, bool subtractFeeFromAmount) async { - return 'txidsidebeef'; + Future ping() async { + return; } @override Future> listTransactions() async { return List.empty(); } +} + +class MockSidechainSubRPC extends SidechainSubRPC { + MockSidechainSubRPC() { + chain = TestSidechain(); + } @override - Future sideBlockCount() async { - return 1; + Future<(double, double)> getBalance() async { + return (1.12345678, 2.24680); } @override - Future mainBlockCount() async { - return 1; + Future callRAW(String method, [dynamic params]) async { + return; } @override @@ -71,4 +67,9 @@ class MockSidechainRPC extends SidechainRPC { Future ping() async { return; } + + @override + Future> listTransactions() async { + return List.empty(); + } }