Skip to content

Commit

Permalink
multi: add mainchain RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
torkelrogstad committed Nov 2, 2023
1 parent ac16921 commit 175eaa7
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 33 deletions.
15 changes: 13 additions & 2 deletions lib/config/dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart';
import 'package:sidesail/providers/balance_provider.dart';
import 'package:sidesail/providers/transactions_provider.dart';
import 'package:sidesail/routing/router.dart';
import 'package:sidesail/rpc/rpc_mainchain.dart';
import 'package:sidesail/rpc/rpc_sidechain.dart';
import 'package:sidesail/rpc/rpc_config.dart';
import 'package:sidesail/storage/client_settings.dart';
Expand All @@ -11,9 +12,19 @@ import 'package:sidesail/storage/secure_store.dart';
// each dependency can only be registered once
Future<void> initGetitDependencies() async {
// TODO: this can throw an error. How do we display that to the user?
final rpcConfig = await readRpcConfig();
final sidechainConfigFut = readRpcConfig(testchainDatadir(), 'testchain.conf');

final mainchainConfigFut = readRpcConfig(mainchainDatadir(), 'drivechain.conf');

final sidechainConfig = await sidechainConfigFut;
final mainchainConfig = await mainchainConfigFut;

GetIt.I.registerLazySingleton<SidechainRPC>(
() => SidechainRPCLive(rpcConfig),
() => SidechainRPCLive(sidechainConfig),
);

GetIt.I.registerLazySingleton<MainchainRPC>(
() => MainchainRPCLive(mainchainConfig),
);

GetIt.I.registerLazySingleton<AppRouter>(
Expand Down
44 changes: 36 additions & 8 deletions lib/rpc/rpc_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,10 @@ const network = 'regtest';
// 2. Inspect cookie
// 3. Defaults
//
Future<Config> readRpcConfig() async {
final datadir = testchainDatadir();
Future<Config> readRpcConfig(String datadir, String confFile) async {
final networkDir = _filePath([datadir, network]);

log.t('datadir: $datadir');

final conf = File(_filePath([datadir, 'testchain.conf']));
final conf = File(_filePath([datadir, confFile]));

final cookie = File(_filePath([networkDir, '.cookie']));

Expand All @@ -42,7 +39,7 @@ Future<Config> readRpcConfig() async {
int? port;

if (!await conf.exists() && !await cookie.exists()) {
throw 'could not find neither conf nor cookie';
throw 'could not find neither conf ($conf) nor cookie ($cookie)';
}

if (await cookie.exists()) {
Expand Down Expand Up @@ -76,7 +73,7 @@ Future<Config> readRpcConfig() async {
}

host ??= 'localhost';
port ??= _defaultPorts[network]!;
port ??= confFile.startsWith('testchain') ? _defaultSidechainPorts[network]! : _defaultMainchainPorts[network]!;

// Make sure to not include password here
log.i('resolved conf: $username@$host:$port');
Expand All @@ -93,6 +90,32 @@ String? _configValue(List<String> config, String key) {
return line?.split('=').lastOrNull;
}

// TODO: this might need permissions configuration for Windows and Linux?
String mainchainDatadir() {
final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; // windows!
if (home == null) {
throw 'unable to determine HOME location';
}

if (Platform.isLinux) {
return _filePath([
home,
'.drivechain',
]);
} else if (Platform.isMacOS) {
return _filePath([
home,
'Library',
'Application Support',
'Drivechain',
]);
} else if (Platform.isWindows) {
throw 'TODO: windows';
} else {
throw 'unsupported operating system: ${Platform.operatingSystem}';
}
}

// TODO: make this configurable when adding support for more sidechains
// TODO: this might need permissions configuration for Windows and Linux?
String testchainDatadir() {
Expand Down Expand Up @@ -126,6 +149,11 @@ String _filePath(List<String> segments) {

// TODO: this would need to take chain into account
// TODO: add more nets
Map<String, int> _defaultPorts = {
Map<String, int> _defaultSidechainPorts = {
'regtest': 18743,
};

// TODO: add more nets
Map<String, int> _defaultMainchainPorts = {
'regtest': 18443,
};
39 changes: 39 additions & 0 deletions lib/rpc/rpc_mainchain.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:dart_coin_rpc/dart_coin_rpc.dart';
import 'package:dio/dio.dart';
import 'package:sidesail/rpc/rpc_config.dart';

/// RPC connection to the mainchain node.
abstract class MainchainRPC {
Future<double> estimateFee();
}

class MainchainRPCLive implements MainchainRPC {
late RPCClient _client;
MainchainRPCLive(Config config) {
_client = RPCClient(
host: config.host,
port: config.port,
username: config.username,
password: config.password,
useSSL: false,
);

// Completely empty client, with no retry logic.
_client.dioClient = Dio();
}

@override
Future<double> estimateFee() async {
final estimate = await _client.call('estimatesmartfee', [6]) as Map<String, dynamic>;
if (estimate.containsKey('errors')) {
// 10 sats/byte
return 0.001;
}

final btcPerKb = estimate['feerate'] as double;

// who knows!
const kbyteInTx = 5;
return btcPerKb * kbyteInTx;
}
}
14 changes: 7 additions & 7 deletions lib/rpc/rpc_sidechain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ abstract class SidechainRPC {
);
Future<List<Transaction>> listTransactions();

Future<double> estimateSidechainFee();
Future<double> estimateFee();
Future<int> mainchainBlockCount();
Future<int> blockCount();
Future<String> fetchWithdrawalBundleStatus();
Expand Down Expand Up @@ -83,7 +83,7 @@ class SidechainRPCLive implements SidechainRPC {
mainchainFee,
]);

return withdrawalTxid;
return withdrawalTxid['txid'];
}

@override
Expand Down Expand Up @@ -111,18 +111,18 @@ class SidechainRPCLive implements SidechainRPC {
}

@override
Future<double> estimateSidechainFee() async {
Future<double> estimateFee() async {
final estimate = await _client.call('estimatesmartfee', [6]) as Map<String, dynamic>;
if (estimate.containsKey('errors')) {
log.w("could not estimate fee: ${estimate["errors"]}");
// 10 sats/byte
return 0.001;
}

final btcPerKb = estimate.containsKey('feerate') ? estimate['feerate'] as double : 0.0001; // 10 sats/byte
final btcPerKb = estimate['feerate'] as double;

// who knows!
const kbyteInWithdrawal = 5;
return btcPerKb * kbyteInWithdrawal;
const kbyteInTx = 5;
return btcPerKb * kbyteInTx;
}

@override
Expand Down
40 changes: 25 additions & 15 deletions lib/widgets/containers/tabs/dashboard_tab_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:sail_ui/theme/theme.dart';
import 'package:sail_ui/widgets/core/sail_text.dart';
import 'package:sidesail/providers/balance_provider.dart';
import 'package:sidesail/routing/router.dart';
import 'package:sidesail/rpc/rpc_mainchain.dart';
import 'package:sidesail/rpc/rpc_sidechain.dart';
import 'package:sidesail/widgets/containers/dashboard_action_modal.dart';
import 'package:stacked/stacked.dart';
Expand Down Expand Up @@ -93,7 +94,7 @@ class PegOutAction extends StatelessWidget {
),
StaticActionField(
label: 'Mainchain fee',
value: '${(viewModel.mainchainFee).toStringAsFixed(8)} BTC',
value: '${(viewModel.mainchainFee ?? 0).toStringAsFixed(8)} BTC',
),
StaticActionField(
label: 'Sidechain fee',
Expand All @@ -114,16 +115,17 @@ class PegOutViewModel extends BaseViewModel {
final log = Logger(level: Level.debug);
BalanceProvider get _balanceProvider => GetIt.I.get<BalanceProvider>();
AppRouter get _router => GetIt.I.get<AppRouter>();
SidechainRPC get _rpc => GetIt.I.get<SidechainRPC>();
SidechainRPC get _sidechain => GetIt.I.get<SidechainRPC>();
MainchainRPC get _mainchain => GetIt.I.get<MainchainRPC>();

final bitcoinAddressController = TextEditingController();
final bitcoinAmountController = TextEditingController();
String get totalBitcoinAmount =>
((double.tryParse(bitcoinAmountController.text) ?? 0) + mainchainFee + (sidechainFee ?? 0)).toStringAsFixed(8);
((double.tryParse(bitcoinAmountController.text) ?? 0) + (mainchainFee ?? 0) + (sidechainFee ?? 0))
.toStringAsFixed(8);

// executePegOut: estimate this
final double mainchainFee = 0.001;
double? sidechainFee;
double? mainchainFee;
double? get pegOutAmount => double.tryParse(bitcoinAmountController.text);

PegOutViewModel() {
Expand All @@ -133,7 +135,7 @@ class PegOutViewModel extends BaseViewModel {
}

void init() async {
await estimateSidechainFee();
await Future.wait([estimateSidechainFee(), estimateMainchainFee()]);
}

void executePegOut(BuildContext context) async {
Expand All @@ -145,8 +147,12 @@ class PegOutViewModel extends BaseViewModel {
}

Future<void> estimateSidechainFee() async {
final estimate = await _rpc.estimateSidechainFee();
sidechainFee = estimate;
sidechainFee = await _sidechain.estimateFee();
notifyListeners();
}

Future<void> estimateMainchainFee() async {
mainchainFee = await _mainchain.estimateFee();
notifyListeners();
}

Expand Down Expand Up @@ -197,6 +203,7 @@ class PegOutViewModel extends BaseViewModel {
address,
pegOutAmount!,
sidechainFee!,
mainchainFee!,
);
},
child: SailText.primary14(
Expand All @@ -213,13 +220,14 @@ class PegOutViewModel extends BaseViewModel {
String address,
double amount,
double sidechainFee,
double mainchainFee,
) async {
log.i(
'doing peg-out: $amount BTC to $address for $sidechainFee SC fee and $mainchainFee MC fee',
);

try {
final withdrawalTxid = await _rpc.pegOut(
final withdrawalTxid = await _sidechain.pegOut(
address,
amount,
sidechainFee,
Expand All @@ -230,7 +238,7 @@ class PegOutViewModel extends BaseViewModel {
return;
}

// refresh balance, but dont await, so dialog is showed instantly
// refresh balance, but don't await, so dialog is showed instantly
unawaited(_balanceProvider.fetch());

final theme = SailTheme.of(context);
Expand All @@ -253,6 +261,8 @@ class PegOutViewModel extends BaseViewModel {
),
);
} catch (error) {
log.e('could not execute peg-out: $error', error: error);

if (!context.mounted) {
return;
}
Expand All @@ -266,7 +276,7 @@ class PegOutViewModel extends BaseViewModel {
'Failed',
),
content: SailText.primary14(
'Could not execute peg-out ${error.toString()}',
'Could not execute peg-out: ${error.toString()}',
),
actions: [
TextButton(
Expand Down Expand Up @@ -393,7 +403,7 @@ class SendOnSidechainViewModel extends BaseViewModel {
}

void init() async {
await estimateSidechainFee();
await estimateFee();
}

void executeSendOnSidechain(BuildContext context) async {
Expand All @@ -406,8 +416,8 @@ class SendOnSidechainViewModel extends BaseViewModel {
await _router.pop();
}

Future<void> estimateSidechainFee() async {
final estimate = await _rpc.estimateSidechainFee();
Future<void> estimateFee() async {
final estimate = await _rpc.estimateFee();
sidechainExpectedFee = estimate;
notifyListeners();
}
Expand Down Expand Up @@ -485,7 +495,7 @@ class SendOnSidechainViewModel extends BaseViewModel {
);
log.i('sent sidechain withdrawal txid: $sendTXID');

// refresh balance, but dont await, so dialog is showed instantly
// refresh balance, but don't await, so dialog is showed instantly
unawaited(_balanceProvider.fetch());

if (!context.mounted) {
Expand Down
3 changes: 3 additions & 0 deletions test/dashboard_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import 'package:get_it/get_it.dart';
import 'package:sidesail/pages/tabs/dashboard_tab_page.dart';
import 'package:sidesail/providers/balance_provider.dart';
import 'package:sidesail/providers/transactions_provider.dart';
import 'package:sidesail/rpc/rpc_mainchain.dart';
import 'package:sidesail/rpc/rpc_sidechain.dart';

import 'mocks/rpc_mock_mainchain.dart';
import 'mocks/rpc_mock_sidechain.dart';
import 'test_utils.dart';

Expand All @@ -20,6 +22,7 @@ final txProvider = TransactionsProvider();
void main() {
setUpAll(() async {
GetIt.I.registerLazySingleton<SidechainRPC>(() => MockSidechainRPC());
GetIt.I.registerLazySingleton<MainchainRPC>(() => MockMainchainRPC());

GetIt.I.registerLazySingleton<TransactionsProvider>(() => txProvider);
final balanceProvider = BalanceProvider();
Expand Down
8 changes: 8 additions & 0 deletions test/mocks/rpc_mock_mainchain.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:sidesail/rpc/rpc_mainchain.dart';

class MockMainchainRPC extends MainchainRPC {
@override
Future<double> estimateFee() async {
return 0.001;
}
}
2 changes: 1 addition & 1 deletion test/mocks/rpc_mock_sidechain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class MockSidechainRPC extends SidechainRPC {
}

@override
Future<double> estimateSidechainFee() async {
Future<double> estimateFee() async {
return 0.001;
}

Expand Down

0 comments on commit 175eaa7

Please sign in to comment.