Skip to content

Commit

Permalink
rpc: add withdrawal bundle data fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
torkelrogstad committed Nov 4, 2023
1 parent 0cab502 commit 2303864
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 34 deletions.
38 changes: 32 additions & 6 deletions lib/pages/tabs/withdrawal_bundle_tab_page.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import 'dart:async';

import 'package:auto_route/auto_route.dart';
import 'package:dart_coin_rpc/dart_coin_rpc.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:sail_ui/sail_ui.dart';
import 'package:sail_ui/widgets/core/sail_text.dart';
import 'package:sidesail/config/this_sidechain.dart';
import 'package:sidesail/rpc/rpc_mainchain.dart';
import 'package:sidesail/rpc/rpc_sidechain.dart';
import 'package:sidesail/rpc/rpc_withdrawal_bundle.dart';
import 'package:stacked/stacked.dart';

@RoutePage()
Expand All @@ -20,7 +24,7 @@ class WithdrawalBundleTabPage extends StatelessWidget {
viewModelBuilder: () => WithdrawalBundleTabPageViewModel(),
builder: ((context, viewModel, child) {
return SailText.primary14(
'Withdrawal bundle status: ${viewModel.withdrawalBundleStatus}',
'Withdrawal bundle status: ${viewModel.message}',
);
}),
),
Expand All @@ -29,20 +33,42 @@ class WithdrawalBundleTabPage extends StatelessWidget {
}

class WithdrawalBundleTabPageViewModel extends BaseViewModel {
SidechainRPC get _rpc => GetIt.I.get<SidechainRPC>();
SidechainRPC get _sidechain => GetIt.I.get<SidechainRPC>();
MainchainRPC get _mainchain => GetIt.I.get<MainchainRPC>();

WithdrawalBundleTabPageViewModel() {
_startWithdrawalBundleFetch();
}

Timer? _withdrawalBundleTimer;
String withdrawalBundleStatus = 'unknown';

String? message;

WithdrawalBundle? currentBundle;
FutureWithdrawalBundle? nextBundle;

int? votes;
final int votesRequired = 131; // higher on mainnet. take into consideration, somehow

void _fetchWithdrawalBundle() async {
try {
currentBundle = await _sidechain.currentWithdrawalBundle();
nextBundle = await _sidechain.nextWithdrawalBundle();
votes = await _mainchain.getWithdrawalBundleWorkScore(ThisSidechain.slot, currentBundle!.hash);
message = 'lookie, a bundle';
} on RPCException catch (err) {
if (err.errorCode != RPCError.errNoWithdrawalBundle) {
rethrow;
}
message = 'No withdrawal bundle created. Need at least 10 withdrawals!';
} finally {
notifyListeners();
}
}

void _startWithdrawalBundleFetch() {
_withdrawalBundleTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
final state = await _rpc.fetchWithdrawalBundleStatus();
withdrawalBundleStatus = state;
notifyListeners();
_fetchWithdrawalBundle();
});
}

Expand Down
6 changes: 6 additions & 0 deletions lib/rpc/rpc_mainchain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:sidesail/rpc/rpc_config.dart';
/// RPC connection to the mainchain node.
abstract class MainchainRPC {
Future<double> estimateFee();
Future<int> getWithdrawalBundleWorkScore(int sidechain, String hash);
}

class MainchainRPCLive implements MainchainRPC {
Expand Down Expand Up @@ -36,4 +37,9 @@ class MainchainRPCLive implements MainchainRPC {
const kbyteInTx = 5;
return btcPerKb * kbyteInTx;
}

@override
Future<int> getWithdrawalBundleWorkScore(int sidechain, String hash) async {
return await _client.call('getworkscore', [sidechain, hash]);
}
}
128 changes: 128 additions & 0 deletions lib/rpc/rpc_rawtx.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
class RawTransaction {
final String txid;
final String hash;
final int size;
final int vsize;
final int version;
final int locktime;
final List<Vin> vin;
final List<Vout> vout;

RawTransaction({
required this.txid,
required this.hash,
required this.size,
required this.vsize,
required this.version,
required this.locktime,
required this.vin,
required this.vout,
});

factory RawTransaction.fromJson(Map<String, dynamic> json) {
return RawTransaction(
txid: json['txid'],
hash: json['hash'],
size: json['size'],
vsize: json['vsize'],
version: json['version'],
locktime: json['locktime'],
vin: (json['vin'] as List).map((e) => Vin.fromJson(e)).toList(),
vout: (json['vout'] as List).map((e) => Vout.fromJson(e)).toList(),
);
}
}

class Vin {
final String txid;
final int vout;
final ScriptSig scriptSig;
final List<String>? txinwitness;
final int sequence;

Vin({
required this.txid,
required this.vout,
required this.scriptSig,
this.txinwitness,
required this.sequence,
});

factory Vin.fromJson(Map<String, dynamic> json) {
return Vin(
txid: getMapKey(json, 'txid', ''),
vout: getMapKey(json, 'vout', 0),
scriptSig: ScriptSig.fromJson(json['scriptSig']),
txinwitness: json['txinwitness']?.cast<String>(),
sequence: json['sequence'],
);
}
}

class ScriptSig {
final String asm;
final String hex;

ScriptSig({required this.asm, required this.hex});

factory ScriptSig.fromJson(Map<String, dynamic>? json) {
if (json == null) {
return ScriptSig(asm: '', hex: '');
}

return ScriptSig(
asm: json['asm'],
hex: json['hex'],
);
}
}

class Vout {
final double value;
final int n;
final ScriptPubKey scriptPubKey;

Vout({required this.value, required this.n, required this.scriptPubKey});

factory Vout.fromJson(Map<String, dynamic> json) {
return Vout(
value: json['value'],
n: json['n'],
scriptPubKey: ScriptPubKey.fromJson(json['scriptPubKey']),
);
}
}

class ScriptPubKey {
final String asm;
final String hex;
final int reqSigs;
final String type;
final List<String> addresses;

ScriptPubKey({
required this.asm,
required this.hex,
required this.reqSigs,
required this.type,
required this.addresses,
});

factory ScriptPubKey.fromJson(Map<String, dynamic> json) {
return ScriptPubKey(
asm: json['asm'],
hex: json['hex'],
reqSigs: getMapKey(json, 'reqSigs', 0),
type: json['type'],
addresses: List<String>.from(json['addresses'] ?? []),
);
}
}

dynamic getMapKey(Map<String, dynamic> json, String key, fallback) {
if (json.containsKey(key)) {
return json[key];
}

return fallback;
}
60 changes: 37 additions & 23 deletions lib/rpc/rpc_sidechain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:dart_coin_rpc/dart_coin_rpc.dart';
import 'package:dio/dio.dart';
import 'package:sidesail/logger.dart';
import 'package:sidesail/rpc/rpc_config.dart';
import 'package:sidesail/rpc/rpc_rawtx.dart';
import 'package:sidesail/rpc/rpc_withdrawal_bundle.dart';

/// RPC connection the sidechain node.
abstract class SidechainRPC {
Expand All @@ -29,7 +31,10 @@ abstract class SidechainRPC {
Future<double> estimateFee();
Future<int> mainchainBlockCount();
Future<int> blockCount();
Future<String> fetchWithdrawalBundleStatus();

Future<WithdrawalBundle> currentWithdrawalBundle();
Future<FutureWithdrawalBundle> nextWithdrawalBundle();

Future<dynamic> callRAW(String method, [dynamic params]);
}

Expand Down Expand Up @@ -125,27 +130,6 @@ class SidechainRPCLive implements SidechainRPC {
return btcPerKb * kbyteInTx;
}

@override
Future<String> fetchWithdrawalBundleStatus() async {
try {
// TODO: do something meaningful with this, we would need it decoded
// with bitcoin core.
// BtcTransaction.fromRaw crashes...
final bundleHex = await _client.call('getwithdrawalbundle', []);

return 'something: ${(bundleHex as String).substring(0, 20)}...';
} on RPCException catch (e) {
if (e.errorCode != RPCError.errNoWithdrawalBundle) {
return 'unexpected withdrawal bundle status: $e';
}

return 'no withdrawal bundle yet';
} catch (e) {
log.e('could not fetch withdrawal bundle: $e', error: e);
rethrow;
}
}

@override
Future<dynamic> callRAW(String method, [dynamic params]) async {
return _client.call(method, params).catchError((err) {
Expand Down Expand Up @@ -188,9 +172,39 @@ class SidechainRPCLive implements SidechainRPC {
Future<int> blockCount() async {
return await _client.call('getblockcount');
}

@override
Future<WithdrawalBundle> currentWithdrawalBundle() async {
final rawWithdrawalBundle = await _client.call('getwithdrawalbundle');

final decoded = await _client.call('decoderawtransaction', [rawWithdrawalBundle]);
final tx = RawTransaction.fromJson(decoded);

return WithdrawalBundle.fromRawTransaction(
tx,
);
}

@override
Future<FutureWithdrawalBundle> nextWithdrawalBundle() async {
final rawNextBundle = await _client.call('listnextbundlewithdrawals') as List<dynamic>;

return FutureWithdrawalBundle(
cumulativeWeight: 0, // TODO: not sure how to obtain this
withdrawals: rawNextBundle
.map(
(withdrawal) => Withdrawal(
mainchainFeesSatoshi: withdrawal['amountmainchainfee'],
amountSatoshi: withdrawal['amount'],
address: withdrawal['destination'],
),
)
.toList(),
);
}
}

abstract class RPCError {
class RPCError {
static const errNoWithdrawalBundle = -100;
static const errWithdrawalNotFound = -101;
}
Expand Down
64 changes: 64 additions & 0 deletions lib/rpc/rpc_withdrawal_bundle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:sidesail/rpc/rpc_rawtx.dart';

class WithdrawalBundle {
WithdrawalBundle({
required this.hash,
required this.bundleSize,
required this.blockHeight,
required this.withdrawals,
});

factory WithdrawalBundle.fromRawTransaction(
RawTransaction tx,
) =>
WithdrawalBundle(
hash: tx.hash,
bundleSize: tx.size * 4,
blockHeight: 0, // TODO: how to get this
withdrawals: tx.vout
// filter out OP_RETURN
.where((out) => out.scriptPubKey.type != 'nulldata')
.map(
(out) => Withdrawal(
mainchainFeesSatoshi: 0, // TODO: how to get this
amountSatoshi: (out.value * 100 * 1000 * 1000).toInt(),
address: out.scriptPubKey.addresses.first,
),
)
.toList(),
);

final String hash;

final int bundleSize;
final int maxBundleSize = 50 * 1000;

/// Block number this withdrawal bundle was initiated.
final int blockHeight;

final List<Withdrawal> withdrawals;
}

/// A collection of withdrawals that have not yet been proposed into
/// a mainchain withdrawal bundle.
class FutureWithdrawalBundle {
FutureWithdrawalBundle({
required this.cumulativeWeight,
required this.withdrawals,
});

final int cumulativeWeight;
final List<Withdrawal> withdrawals;
}

class Withdrawal {
Withdrawal({
required this.mainchainFeesSatoshi,
required this.amountSatoshi,
required this.address,
});

final int mainchainFeesSatoshi; // TODO: how to obtain?
final int amountSatoshi;
final String address;
}
5 changes: 5 additions & 0 deletions test/mocks/rpc_mock_mainchain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ class MockMainchainRPC extends MainchainRPC {
Future<double> estimateFee() async {
return 0.001;
}

@override
Future<int> getWithdrawalBundleWorkScore(int sidechain, String hash) async {
return 1;
}
}
Loading

0 comments on commit 2303864

Please sign in to comment.