From b9fa087423d7a1ba4557727e7c18e517bf714148 Mon Sep 17 00:00:00 2001 From: mblowes Date: Thu, 24 Sep 2026 19:32:38 +1000 Subject: [PATCH 1/2] bitwindow: give FreeBank its settings button and running state FreeBank had no RPC connection in BitWindow, so its Sidechains row had no settings button, and never showed Stop, the status dot, the sync bar or Your balance while the node ran. FreeBankRPC reads all of it through the orchestrator's generic routes, so any FreeBank build works. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Gqbnw5hYV8j1ekU6P4GgYT --- bitwindow/lib/main.dart | 2 + bitwindow/lib/pages/sidechains_page.dart | 4 + bitwindow/test/deposit_modal_test.dart | 17 +++ bitwindow/test/sidechains_table_test.dart | 117 ++++++++++++++ .../lib/providers/backend_state_provider.dart | 2 + .../providers/binaries/binary_provider.dart | 1 + sidechain_core/lib/rpcs/freebank_rpc.dart | 118 +++++++++++++++ sidechain_core/lib/sidechain_core.dart | 1 + sidechain_core/test/freebank_rpc_test.dart | 143 ++++++++++++++++++ 9 files changed, 405 insertions(+) create mode 100644 sidechain_core/lib/rpcs/freebank_rpc.dart create mode 100644 sidechain_core/test/freebank_rpc_test.dart diff --git a/bitwindow/lib/main.dart b/bitwindow/lib/main.dart index 3758db61c6..08da18a34d 100644 --- a/bitwindow/lib/main.dart +++ b/bitwindow/lib/main.dart @@ -227,6 +227,7 @@ Future<(Directory, File, Logger)> init(String arguments) async { GetIt.I.registerSingleton(CoinShiftLive()); GetIt.I.registerSingleton(PhotonLive()); GetIt.I.registerSingleton(TruthcoinLive()); + GetIt.I.registerSingleton(FreeBankRPC()); registerNodeMode(); @@ -257,6 +258,7 @@ Future<(Directory, File, Logger)> init(String arguments) async { GetIt.I.get(), GetIt.I.get(), GetIt.I.get(), + GetIt.I.get(), ], ), ); diff --git a/bitwindow/lib/pages/sidechains_page.dart b/bitwindow/lib/pages/sidechains_page.dart index dd3137a14d..ed2ebbe0e1 100644 --- a/bitwindow/lib/pages/sidechains_page.dart +++ b/bitwindow/lib/pages/sidechains_page.dart @@ -736,6 +736,7 @@ class SidechainsViewModel extends BaseViewModel with ChangeTrackingMixin { Truthcoin() => SidechainType.SIDECHAIN_TYPE_TRUTHCOIN, Photon() => SidechainType.SIDECHAIN_TYPE_PHOTON, CoinShift() => SidechainType.SIDECHAIN_TYPE_COINSHIFT, + FreeBank() => SidechainType.SIDECHAIN_TYPE_FREEBANK, _ => null, }; @@ -898,6 +899,7 @@ class SidechainsViewModel extends BaseViewModel with ChangeTrackingMixin { var b when b is Truthcoin => GetIt.I.isRegistered() ? GetIt.I.get() : null, var b when b is Photon => GetIt.I.isRegistered() ? GetIt.I.get() : null, var b when b is CoinShift => GetIt.I.isRegistered() ? GetIt.I.get() : null, + var b when b is FreeBank => GetIt.I.isRegistered() ? GetIt.I.get() : null, _ => null, }; @@ -949,6 +951,7 @@ class SidechainsViewModel extends BaseViewModel with ChangeTrackingMixin { var b when b is Truthcoin => (GetIt.I.isRegistered() ? GetIt.I.get() : null), var b when b is Photon => (GetIt.I.isRegistered() ? GetIt.I.get() : null), var b when b is CoinShift => (GetIt.I.isRegistered() ? GetIt.I.get() : null), + var b when b is FreeBank => (GetIt.I.isRegistered() ? GetIt.I.get() : null), _ => null, }; } @@ -1645,6 +1648,7 @@ class _DepositModalState extends State { ZSide() => GetIt.I.isRegistered() ? GetIt.I.get() : null, CoinShift() => GetIt.I.isRegistered() ? GetIt.I.get() : null, Thunder() => GetIt.I.isRegistered() ? GetIt.I.get() : null, + FreeBank() => GetIt.I.isRegistered() ? GetIt.I.get() : null, _ => null, }; diff --git a/bitwindow/test/deposit_modal_test.dart b/bitwindow/test/deposit_modal_test.dart index d33166bfb7..ca7fd6f14b 100644 --- a/bitwindow/test/deposit_modal_test.dart +++ b/bitwindow/test/deposit_modal_test.dart @@ -112,6 +112,23 @@ void main() { expect(field.controller.text, 's9_pastedaddress_abc123'); }); + testWidgets('a running FreeBank leaves the address field open for a paste and says why', (tester) async { + GetIt.I.registerSingleton(FreeBankRPC()..connected = true); + await GetIt.I.unregister(); + GetIt.I.registerSingleton( + BinaryProvider.test(appDir: Directory.systemTemp, binaries: [FreeBank()]), + ); + + addTearDown(() => tester.pumpWidget(const SizedBox())); + await tester.pumpSailPage(const DepositModal(slot: 130, sidechainName: 'FreeBank')); + await tester.pump(); + + final field = tester.widget(find.byType(SailTextField).first); + expect(field.readOnly, isFalse); + expect(field.controller.text, isEmpty); + expect(find.textContaining('Paste one from the FreeBank wallet'), findsOneWidget); + }); + testWidgets('a pasted address with an amount arms the deposit button', (tester) async { await pumpModal(tester); diff --git a/bitwindow/test/sidechains_table_test.dart b/bitwindow/test/sidechains_table_test.dart index 63395d15bd..5dab51933b 100644 --- a/bitwindow/test/sidechains_table_test.dart +++ b/bitwindow/test/sidechains_table_test.dart @@ -104,6 +104,24 @@ class _CoinShiftRPC extends MockCoinShiftRPC { Future<(double, double)> balance() async => wallet; } +class _Orchestrator implements OrchestratorRPC { + final balanceReads = []; + GetSidechainBalanceResponse balance = GetSidechainBalanceResponse(); + + @override + Future getSidechainBalance(BinaryType sidechain) async { + balanceReads.add(sidechain); + return balance; + } + + @override + Future getBinaryVersion(String name, {bool forceBackend = false}) async => + GetBinaryVersionResponse(version: '$name 1.0'); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + Thunder _thunder({bool downloaded = true, bool updateAvailable = false}) { final base = Thunder(); return base.copyWith( @@ -116,6 +134,22 @@ Thunder _thunder({bool downloaded = true, bool updateAvailable = false}) { ); } +FreeBank _freeBank({bool downloaded = true}) { + final base = FreeBank(); + return base.copyWith( + metadata: base.metadata.copyWith( + remoteTimestamp: null, + downloadedTimestamp: downloaded ? DateTime(2026, 1) : null, + binaryPath: downloaded ? File('/tmp/freebankd') : null, + updateable: true, + ), + ); +} + +final Finder _settingsButton = find.byWidgetPredicate( + (widget) => widget is SailButton && widget.icon == SailSVGAsset.settings, +); + Finder _button(String label) => find.byWidgetPredicate((widget) => widget is SailButton && widget.label == label); final Finder _updateButton = find.byWidgetPredicate( @@ -268,6 +302,89 @@ void main() { expect(find.text('—'), findsOneWidget); }); + // FreeBank reads all its state through the orchestrator. + group('FreeBank', () { + late _Orchestrator orchestrator; + late FreeBankRPC freeBankRPC; + + Future setUpFreeBank({bool downloaded = true}) async { + GetIt.I.registerSingleton(_Binaries([_freeBank(downloaded: downloaded)])); + orchestrator = _Orchestrator(); + GetIt.I.registerSingleton(orchestrator); + freeBankRPC = FreeBankRPC(); + GetIt.I.registerSingleton(freeBankRPC); + await GetIt.I.unregister(); + balances = BalanceProvider(connections: [thunderRPC, coinShiftRPC, freeBankRPC]); + GetIt.I.registerSingleton(balances); + GetIt.I.get().sidechains[130] = SidechainOverview( + ListSidechainsResponse_Sidechain(title: 'FreeBank', slot: 130, balanceSatoshi: Int64(300000000)), + [], + [], + ); + } + + testWidgets('the row has a settings button that opens the chain settings', (tester) async { + await setUpFreeBank(); + await pumpTable(tester); + + expect(_settingsButton, findsOneWidget); + await tester.tap(_settingsButton); + await tester.pump(kDoubleTapTimeout); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.byType(ChainSettingsModal), findsOneWidget); + expect(find.descendant(of: find.byType(ChainSettingsModal), matching: find.text('FreeBank')), findsOneWidget); + expect(find.textContaining('version freebank 1.0'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('a stopped FreeBank offers Start and reads no balance', (tester) async { + await setUpFreeBank(); + await balances.fetch(); + await pumpTable(tester); + + expect(_buttonWidget(tester, 'Start').variant, ButtonVariant.primary); + expect(_button('Stop'), findsNothing); + expect(_settingsButton, findsOneWidget); + expect(orchestrator.balanceReads, isEmpty); + expect(tester.takeException(), isNull); + }); + + testWidgets('a FreeBank that is not installed offers Download', (tester) async { + await setUpFreeBank(downloaded: false); + await pumpTable(tester); + + expect(_buttonWidget(tester, 'Download').variant, ButtonVariant.primary); + expect(_settingsButton, findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('a running FreeBank offers Stop and shows the balance the orchestrator reads', (tester) async { + await setUpFreeBank(); + orchestrator.balance = GetSidechainBalanceResponse(confirmedSats: Int64(250000000)); + freeBankRPC.connected = true; + await balances.fetch(); + await pumpTable(tester); + + expect(_buttonWidget(tester, 'Stop').variant, ButtonVariant.outline); + expect(_tooltip('Connected'), findsOneWidget); + expect(orchestrator.balanceReads, contains(BinaryType.BINARY_TYPE_FREEBANK)); + expect(find.text(GetIt.I.get().formatBTC(2.5)), findsOneWidget); + }); + + testWidgets('a running FreeBank behind its headers shows the sync bar', (tester) async { + await setUpFreeBank(); + freeBankRPC.connected = true; + sync.sidechains = { + SidechainType.SIDECHAIN_TYPE_FREEBANK: SyncInfo(progressCurrent: 100, progressGoal: 294, lastBlockAt: null), + }; + await pumpTable(tester); + + expect(find.text('34%'), findsOneWidget); + expect(_button('Stop'), findsOneWidget); + }); + }); + testWidgets('a chain that is not downloaded offers a primary Download', (tester) async { setUpChain(_thunder(downloaded: false)); await pumpTable(tester); diff --git a/sidechain_core/lib/providers/backend_state_provider.dart b/sidechain_core/lib/providers/backend_state_provider.dart index 8abb70d9f7..8f792a599c 100644 --- a/sidechain_core/lib/providers/backend_state_provider.dart +++ b/sidechain_core/lib/providers/backend_state_provider.dart @@ -14,6 +14,7 @@ import 'package:sidechain_core/rpcs/coinshift_rpc.dart'; import 'package:sidechain_core/rpcs/enforcer_rpc.dart'; import 'package:sidechain_core/rpcs/bitcoind_connection.dart'; import 'package:sidechain_core/env.dart'; +import 'package:sidechain_core/rpcs/freebank_rpc.dart'; import 'package:sidechain_core/rpcs/orchestrator_rpc.dart'; import 'package:sidechain_core/rpcs/photon_rpc.dart'; import 'package:sidechain_core/rpcs/thunder_rpc.dart'; @@ -219,6 +220,7 @@ class BackendStateProvider extends ChangeNotifier { 'truthcoin' => GetIt.I.isRegistered() ? GetIt.I.get() : null, 'photon' => GetIt.I.isRegistered() ? GetIt.I.get() : null, 'coinshift' => GetIt.I.isRegistered() ? GetIt.I.get() : null, + 'freebank' => GetIt.I.isRegistered() ? GetIt.I.get() : null, _ => null, }; } diff --git a/sidechain_core/lib/providers/binaries/binary_provider.dart b/sidechain_core/lib/providers/binaries/binary_provider.dart index cb6c1f0dc0..f658c46239 100644 --- a/sidechain_core/lib/providers/binaries/binary_provider.dart +++ b/sidechain_core/lib/providers/binaries/binary_provider.dart @@ -654,6 +654,7 @@ class BinaryProvider extends ChangeNotifier { var b when b is BitAssets => _getRegistered(), var b when b is ZSide => _getRegistered(), var b when b is CoinShift => _getRegistered(), + var b when b is FreeBank => _getRegistered(), _ => null, }; } diff --git a/sidechain_core/lib/rpcs/freebank_rpc.dart b/sidechain_core/lib/rpcs/freebank_rpc.dart new file mode 100644 index 0000000000..e1ed298abc --- /dev/null +++ b/sidechain_core/lib/rpcs/freebank_rpc.dart @@ -0,0 +1,118 @@ +import 'package:collection/collection.dart'; +import 'package:get_it/get_it.dart'; +import 'package:sidechain_core/bitcoin.dart' as bitcoin; +import 'package:sidechain_core/classes/rpc_connection.dart'; +import 'package:sidechain_core/gen/orchestrator/v1/orchestrator.pb.dart'; +import 'package:sidechain_core/models/core_transaction.dart'; +import 'package:sidechain_core/rpcs/orchestrator_rpc.dart'; +import 'package:sidechain_core/rpcs/rpc_sidechain.dart'; +import 'package:sidechain_core/rpcs/thunder_utxo.dart'; + +/// API to the FreeBank node, served by the orchestrator. +/// +/// It uses only the orchestrator's generic routes, so it works for any FreeBank +/// build. A call those routes do not cover is unsupported. +class FreeBankRPC extends SidechainRPC { + FreeBankRPC() : super(binaryType: BinaryType.BINARY_TYPE_FREEBANK); + + OrchestratorRPC get _orchestrator => GetIt.I.get(); + + /// The orchestrator builds the node's arguments. + @override + Future> binaryArgs() async => []; + + @override + Future stopRPC() async { + await _orchestrator.stopBinary('freebank'); + } + + @override + Future<(double, double)> balance() async { + final resp = await _orchestrator.getSidechainBalance(binaryType); + return ( + bitcoin.satoshiToBTC(resp.confirmedSats.toInt()), + bitcoin.satoshiToBTC(resp.pendingSats.toInt()), + ); + } + + @override + Future getBlockCount() async => (await _sync()).blocks; + + @override + Future getBlockchainInfo() async { + final sync = await _sync(); + return BlockchainInfo( + chain: '', + blocks: sync.blocks, + headers: sync.headers, + bestBlockHash: '', + difficulty: 0, + time: sync.time.toInt(), + medianTime: 0, + verificationProgress: sync.headers > 0 ? sync.blocks / sync.headers : 0, + initialBlockDownload: sync.blocks < sync.headers, + chainWork: '', + sizeOnDisk: 0, + pruned: false, + warnings: const [], + ); + } + + /// The node's sync state as the orchestrator last read it. + Future _sync() async { + final status = await _orchestrator.getSyncStatus(); + final entry = status.sidechains.firstWhereOrNull((s) => s.type == SidechainType.SIDECHAIN_TYPE_FREEBANK); + if (entry == null) { + throw StateError('the orchestrator reports no FreeBank sync status'); + } + if (entry.sync.error.isNotEmpty) { + throw StateError(entry.sync.error); + } + return entry.sync; + } + + // ── Not served through the orchestrator ──────────────────────────────────── + + @override + Future getDepositAddress() async => + throw UnsupportedError('BitWindow has no FreeBank route for addresses. Paste one from the FreeBank wallet.'); + + @override + Future getSideAddress() async => throw _noRoute('addresses'); + + @override + Future sideSend(String address, double amount, bool subtractFeeFromAmount) async => throw _noRoute('sends'); + + @override + Future sideEstimateFee() async => throw _noRoute('fees'); + + @override + Future> listUTXOs() async => throw _noRoute('coins'); + + @override + Future> listAllUTXOs() async => throw _noRoute('coins'); + + @override + Future> listTransactions() async => throw _noRoute('transactions'); + + @override + Future mine(int feeSats) async => throw _noRoute('mining'); + + @override + Future getPendingWithdrawalBundle() async => throw _noRoute('withdrawal bundles'); + + @override + Future getLatestFailedWithdrawalBundleHeight() async => throw _noRoute('withdrawal bundles'); + + @override + Future withdraw(String address, int amountSats, int sidechainFeeSats, int mainchainFeeSats) async => + throw _noRoute('withdrawals'); + + @override + Future callRAW(String method, [List? params]) async => throw _noRoute('raw calls'); + + @override + List getMethods() => const []; + + UnsupportedError _noRoute(String what) => UnsupportedError('BitWindow has no FreeBank route for $what'); +} diff --git a/sidechain_core/lib/sidechain_core.dart b/sidechain_core/lib/sidechain_core.dart index 38c14c6aa1..683f67f4f6 100644 --- a/sidechain_core/lib/sidechain_core.dart +++ b/sidechain_core/lib/sidechain_core.dart @@ -176,6 +176,7 @@ export 'rpcs/bitnames_rpc.dart'; export 'rpcs/bitwindow_api.dart'; export 'rpcs/coinshift_rpc.dart'; export 'rpcs/enforcer_rpc.dart'; +export 'rpcs/freebank_rpc.dart'; export 'rpcs/keepalive_http_client.dart'; export 'rpcs/orchestrator_multisig_lounge_rpc.dart'; export 'rpcs/orchestrator_rpc.dart'; diff --git a/sidechain_core/test/freebank_rpc_test.dart b/sidechain_core/test/freebank_rpc_test.dart new file mode 100644 index 0000000000..481590ecdc --- /dev/null +++ b/sidechain_core/test/freebank_rpc_test.dart @@ -0,0 +1,143 @@ +import 'dart:io'; + +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logger/logger.dart'; +import 'package:sidechain_core/sidechain_core.dart'; + +class _FakeOrchestrator implements OrchestratorRPC { + final balanceReads = []; + final stops = []; + GetSidechainBalanceResponse balance = GetSidechainBalanceResponse(); + GetSyncStatusResponse syncStatus = GetSyncStatusResponse(); + + @override + Future getSidechainBalance(BinaryType sidechain) async { + balanceReads.add(sidechain); + return balance; + } + + @override + Future getSyncStatus() async => syncStatus; + + @override + Future stopBinary(String name, {bool force = false, bool forceBackend = false}) async { + stops.add(name); + return StopBinaryResponse(); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +SidechainStatus _status(SidechainType type, {int blocks = 0, int headers = 0, String error = ''}) => SidechainStatus( + type: type, + sync: ChainSync(blocks: blocks, headers: headers, error: error), +); + +void main() { + late _FakeOrchestrator orchestrator; + late FreeBankRPC rpc; + + setUp(() async { + await GetIt.I.reset(); + GetIt.I.registerSingleton(Logger(level: Level.off)); + orchestrator = _FakeOrchestrator(); + GetIt.I.registerSingleton(orchestrator); + rpc = FreeBankRPC(); + GetIt.I.registerSingleton(rpc); + }); + + tearDown(() async { + await GetIt.I.reset(); + }); + + test('the balance is the orchestrator FreeBank balance', () async { + orchestrator.balance = GetSidechainBalanceResponse( + confirmedSats: Int64(250000000), + pendingSats: Int64(1000000), + ); + + expect(await rpc.balance(), (2.5, 0.01)); + expect(orchestrator.balanceReads, [BinaryType.BINARY_TYPE_FREEBANK]); + }); + + test('the height is the FreeBank entry of the orchestrator sync status', () async { + orchestrator.syncStatus = GetSyncStatusResponse( + sidechains: [ + _status(SidechainType.SIDECHAIN_TYPE_THUNDER, blocks: 7, headers: 7), + _status(SidechainType.SIDECHAIN_TYPE_FREEBANK, blocks: 80, headers: 90), + ], + ); + + expect(await rpc.getBlockCount(), 80); + final info = await rpc.getBlockchainInfo(); + expect(info.blocks, 80); + expect(info.headers, 90); + expect(info.initialBlockDownload, isTrue); + }); + + test('a node the orchestrator cannot read gives its reason, not a height', () async { + orchestrator.syncStatus = GetSyncStatusResponse( + sidechains: [_status(SidechainType.SIDECHAIN_TYPE_FREEBANK, error: 'not running')], + ); + await expectLater( + rpc.getBlockCount(), + throwsA(isA().having((e) => e.message, 'message', 'not running')), + ); + + orchestrator.syncStatus = GetSyncStatusResponse(); + await expectLater(rpc.getBlockchainInfo(), throwsStateError); + }); + + test('stop goes through the orchestrator', () async { + await rpc.stopRPC(); + expect(orchestrator.stops, ['freebank']); + }); + + test('a call the orchestrator has no route for is unsupported', () async { + await expectLater(rpc.getDepositAddress(), throwsUnsupportedError); + await expectLater(rpc.getSideAddress(), throwsUnsupportedError); + await expectLater(rpc.sideSend('addr', 1, false), throwsUnsupportedError); + await expectLater(rpc.listTransactions(), throwsUnsupportedError); + await expectLater(rpc.mine(1000), throwsUnsupportedError); + await expectLater(rpc.getPendingWithdrawalBundle(), throwsUnsupportedError); + await expectLater(rpc.withdraw('addr', 1000, 10, 10), throwsUnsupportedError); + await expectLater(rpc.callRAW('getbalance'), throwsUnsupportedError); + expect(rpc.getMethods(), isEmpty); + expect(await rpc.binaryArgs(), isEmpty); + }); + + test('the binary state reads the FreeBank connection', () { + final binaries = BinaryProvider.test(appDir: Directory.systemTemp, binaries: [FreeBank()]); + GetIt.I.registerSingleton(binaries); + + expect(binaries.isConnected(FreeBank()), isFalse); + rpc.initializingBinary = true; + expect(binaries.isInitializing(FreeBank()), isTrue); + rpc + ..initializingBinary = false + ..connected = true; + expect(binaries.isConnected(FreeBank()), isTrue); + expect(binaries.isSidechainUp(FreeBank()), isTrue); + rpc.stoppingBinary = true; + expect(binaries.isStopping(FreeBank()), isTrue); + rpc.connectionError = 'refused'; + expect(binaries.connectionError(FreeBank()), 'refused'); + }); + + test('a stopped FreeBank reads no balance, a running one reads it once per fetch', () async { + orchestrator.balance = GetSidechainBalanceResponse(confirmedSats: Int64(100000000)); + final balances = BalanceProvider(connections: [rpc]); + addTearDown(balances.dispose); + + await balances.fetch(); + expect(orchestrator.balanceReads, isEmpty); + + rpc.connected = true; + await balances.fetch(); + expect(orchestrator.balanceReads, [BinaryType.BINARY_TYPE_FREEBANK]); + expect(balances.balanceFor(rpc), (1.0, 0.0)); + }); +} From 1245ff6ad1272183f7d762cbbd9e4541843cd77c Mon Sep 17 00:00:00 2001 From: mblowes Date: Thu, 24 Sep 2026 20:27:41 +1000 Subject: [PATCH 2/2] sidechain_core: give FreeBank the usual RPC shape and plain errors FreeBankRPC is now an abstract SidechainRPC with a FreeBankLive implementation, as every other chain has. BitWindow registers FreeBankLive. The deposit modal shows "Unsupported operation: fetching a FreeBank address. Paste one from your FreeBank wallet." The other unsupported calls no longer name the app. The class doc is one line and no longer says it works for any FreeBank build: the row reports balance and sync only where the orchestrator serves them for FreeBank. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Gqbnw5hYV8j1ekU6P4GgYT --- bitwindow/lib/main.dart | 2 +- bitwindow/test/deposit_modal_test.dart | 7 ++-- bitwindow/test/sidechains_table_test.dart | 2 +- sidechain_core/lib/rpcs/freebank_rpc.dart | 40 ++++++++++++---------- sidechain_core/test/freebank_rpc_test.dart | 15 ++++++-- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/bitwindow/lib/main.dart b/bitwindow/lib/main.dart index 08da18a34d..1fda6efa33 100644 --- a/bitwindow/lib/main.dart +++ b/bitwindow/lib/main.dart @@ -227,7 +227,7 @@ Future<(Directory, File, Logger)> init(String arguments) async { GetIt.I.registerSingleton(CoinShiftLive()); GetIt.I.registerSingleton(PhotonLive()); GetIt.I.registerSingleton(TruthcoinLive()); - GetIt.I.registerSingleton(FreeBankRPC()); + GetIt.I.registerSingleton(FreeBankLive()); registerNodeMode(); diff --git a/bitwindow/test/deposit_modal_test.dart b/bitwindow/test/deposit_modal_test.dart index ca7fd6f14b..75235fd471 100644 --- a/bitwindow/test/deposit_modal_test.dart +++ b/bitwindow/test/deposit_modal_test.dart @@ -113,7 +113,7 @@ void main() { }); testWidgets('a running FreeBank leaves the address field open for a paste and says why', (tester) async { - GetIt.I.registerSingleton(FreeBankRPC()..connected = true); + GetIt.I.registerSingleton(FreeBankLive()..connected = true); await GetIt.I.unregister(); GetIt.I.registerSingleton( BinaryProvider.test(appDir: Directory.systemTemp, binaries: [FreeBank()]), @@ -126,7 +126,10 @@ void main() { final field = tester.widget(find.byType(SailTextField).first); expect(field.readOnly, isFalse); expect(field.controller.text, isEmpty); - expect(find.textContaining('Paste one from the FreeBank wallet'), findsOneWidget); + expect( + find.text('Unsupported operation: fetching a FreeBank address. Paste one from your FreeBank wallet.'), + findsOneWidget, + ); }); testWidgets('a pasted address with an amount arms the deposit button', (tester) async { diff --git a/bitwindow/test/sidechains_table_test.dart b/bitwindow/test/sidechains_table_test.dart index 5dab51933b..671cfa140a 100644 --- a/bitwindow/test/sidechains_table_test.dart +++ b/bitwindow/test/sidechains_table_test.dart @@ -311,7 +311,7 @@ void main() { GetIt.I.registerSingleton(_Binaries([_freeBank(downloaded: downloaded)])); orchestrator = _Orchestrator(); GetIt.I.registerSingleton(orchestrator); - freeBankRPC = FreeBankRPC(); + freeBankRPC = FreeBankLive(); GetIt.I.registerSingleton(freeBankRPC); await GetIt.I.unregister(); balances = BalanceProvider(connections: [thunderRPC, coinShiftRPC, freeBankRPC]); diff --git a/sidechain_core/lib/rpcs/freebank_rpc.dart b/sidechain_core/lib/rpcs/freebank_rpc.dart index e1ed298abc..8c016229ee 100644 --- a/sidechain_core/lib/rpcs/freebank_rpc.dart +++ b/sidechain_core/lib/rpcs/freebank_rpc.dart @@ -8,12 +8,13 @@ import 'package:sidechain_core/rpcs/orchestrator_rpc.dart'; import 'package:sidechain_core/rpcs/rpc_sidechain.dart'; import 'package:sidechain_core/rpcs/thunder_utxo.dart'; -/// API to the FreeBank node, served by the orchestrator. -/// -/// It uses only the orchestrator's generic routes, so it works for any FreeBank -/// build. A call those routes do not cover is unsupported. -class FreeBankRPC extends SidechainRPC { - FreeBankRPC() : super(binaryType: BinaryType.BINARY_TYPE_FREEBANK); +/// API to the FreeBank node, read through the orchestrator's generic routes. +abstract class FreeBankRPC extends SidechainRPC { + FreeBankRPC({required super.binaryType}); +} + +class FreeBankLive extends FreeBankRPC { + FreeBankLive() : super(binaryType: BinaryType.BINARY_TYPE_FREEBANK); OrchestratorRPC get _orchestrator => GetIt.I.get(); @@ -75,44 +76,45 @@ class FreeBankRPC extends SidechainRPC { @override Future getDepositAddress() async => - throw UnsupportedError('BitWindow has no FreeBank route for addresses. Paste one from the FreeBank wallet.'); + throw UnsupportedError('fetching a FreeBank address. Paste one from your FreeBank wallet.'); @override - Future getSideAddress() async => throw _noRoute('addresses'); + Future getSideAddress() async => throw _unsupported('addresses'); @override - Future sideSend(String address, double amount, bool subtractFeeFromAmount) async => throw _noRoute('sends'); + Future sideSend(String address, double amount, bool subtractFeeFromAmount) async => + throw _unsupported('sends'); @override - Future sideEstimateFee() async => throw _noRoute('fees'); + Future sideEstimateFee() async => throw _unsupported('fees'); @override - Future> listUTXOs() async => throw _noRoute('coins'); + Future> listUTXOs() async => throw _unsupported('coins'); @override - Future> listAllUTXOs() async => throw _noRoute('coins'); + Future> listAllUTXOs() async => throw _unsupported('coins'); @override - Future> listTransactions() async => throw _noRoute('transactions'); + Future> listTransactions() async => throw _unsupported('transactions'); @override - Future mine(int feeSats) async => throw _noRoute('mining'); + Future mine(int feeSats) async => throw _unsupported('mining'); @override - Future getPendingWithdrawalBundle() async => throw _noRoute('withdrawal bundles'); + Future getPendingWithdrawalBundle() async => throw _unsupported('withdrawal bundles'); @override - Future getLatestFailedWithdrawalBundleHeight() async => throw _noRoute('withdrawal bundles'); + Future getLatestFailedWithdrawalBundleHeight() async => throw _unsupported('withdrawal bundles'); @override Future withdraw(String address, int amountSats, int sidechainFeeSats, int mainchainFeeSats) async => - throw _noRoute('withdrawals'); + throw _unsupported('withdrawals'); @override - Future callRAW(String method, [List? params]) async => throw _noRoute('raw calls'); + Future callRAW(String method, [List? params]) async => throw _unsupported('raw calls'); @override List getMethods() => const []; - UnsupportedError _noRoute(String what) => UnsupportedError('BitWindow has no FreeBank route for $what'); + UnsupportedError _unsupported(String what) => UnsupportedError('FreeBank $what through the orchestrator'); } diff --git a/sidechain_core/test/freebank_rpc_test.dart b/sidechain_core/test/freebank_rpc_test.dart index 481590ecdc..c6cbe559a4 100644 --- a/sidechain_core/test/freebank_rpc_test.dart +++ b/sidechain_core/test/freebank_rpc_test.dart @@ -45,7 +45,7 @@ void main() { GetIt.I.registerSingleton(Logger(level: Level.off)); orchestrator = _FakeOrchestrator(); GetIt.I.registerSingleton(orchestrator); - rpc = FreeBankRPC(); + rpc = FreeBankLive(); GetIt.I.registerSingleton(rpc); }); @@ -96,8 +96,17 @@ void main() { expect(orchestrator.stops, ['freebank']); }); - test('a call the orchestrator has no route for is unsupported', () async { - await expectLater(rpc.getDepositAddress(), throwsUnsupportedError); + test('a call the orchestrator does not serve is unsupported', () async { + await expectLater( + rpc.getDepositAddress(), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'fetching a FreeBank address. Paste one from your FreeBank wallet.', + ), + ), + ); await expectLater(rpc.getSideAddress(), throwsUnsupportedError); await expectLater(rpc.sideSend('addr', 1, false), throwsUnsupportedError); await expectLater(rpc.listTransactions(), throwsUnsupportedError);