1+ import 'dart:io' ;
2+
13import 'package:bb_mobile/core/electrum/domain/electrum_fallback_runner.dart' ;
24import 'package:bb_mobile/core/electrum/domain/errors/electrum_fallback_exception.dart' ;
35import 'package:bb_mobile/core/electrum/domain/ports/electrum_servers_port.dart' ;
6+ import 'package:bb_mobile/core/electrum/domain/ports/electrum_tor_session_port.dart' ;
47import 'package:bb_mobile/core/electrum/domain/repositories/electrum_server_repository.dart' ;
58import 'package:bb_mobile/core/electrum/domain/repositories/electrum_settings_repository.dart' ;
69import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_connection.dart' ;
710import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_network.dart' ;
11+ import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_url.dart' ;
812import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.dart' ;
13+ import 'package:bb_mobile/core/settings/domain/settings_entity.dart' ;
14+ import 'package:bull_tor/tor.dart' ;
915
1016/// Concrete [ElectrumServersPort] . Owns the only place in the app where the
11- /// active server list is resolved, merged with electrum + Tor settings, and
17+ /// active server list is resolved, merged with Electrum + Orbot settings, and
1218/// iterated — there is no other path consumers can take to reach an Electrum
1319/// server, which is what enforces the R1/R2/R2a privacy rule by construction.
1420class ElectrumServersAdapter implements ElectrumServersPort {
1521 final ElectrumServerRepository _serverRepository;
1622 final ElectrumSettingsRepository _settingsRepository;
1723 final SettingsRepository _appSettingsRepository;
24+ final ElectrumTorSessionPort _torSessionPort;
1825
1926 ElectrumServersAdapter ({
2027 required this ._serverRepository,
2128 required this ._settingsRepository,
2229 required this ._appSettingsRepository,
30+ required this ._torSessionPort,
2331 });
2432
2533 @override
@@ -54,11 +62,6 @@ class ElectrumServersAdapter implements ElectrumServersPort {
5462 throw NoElectrumServersConfiguredException (network);
5563 }
5664
57- // Tor proxy applies to Bitcoin only, never Liquid.
58- final socks5 = (appSettings.useTorProxy && ! network.isLiquid)
59- ? (settings.socks5 ?? '127.0.0.1:${appSettings .torProxyPort }' )
60- : settings.socks5;
61-
6265 final connections = servers
6366 .map (
6467 (server) => ElectrumConnection (
@@ -68,7 +71,7 @@ class ElectrumServersAdapter implements ElectrumServersPort {
6871 stopGap: settings.stopGap,
6972 validateDomain: settings.validateDomain,
7073 isCustom: server.isCustom,
71- socks5: socks5,
74+ socks5: settings. socks5,
7275 ),
7376 )
7477 .toList ();
@@ -77,8 +80,77 @@ class ElectrumServersAdapter implements ElectrumServersPort {
7780 servers: connections,
7881 urlOf: (c) => c.url,
7982 isCustomOf: (c) => c.isCustom,
80- operation: operation,
81- isTransient: isTransient,
83+ operation: (connection) async {
84+ final ElectrumTorRoute ? route;
85+ try {
86+ route = await _torSessionPort.open (
87+ network: network,
88+ serverUrl: connection.url,
89+ externalProxyEnabled: appSettings.useTorProxy,
90+ externalProxyPort: appSettings.torProxyPort,
91+ );
92+ } catch (_) {
93+ // Opening the route can fail on its own — an embedded bootstrap that
94+ // never completes is arguably the likeliest way an onion server
95+ // becomes unusable. That makes *this server* unroutable, not the whole
96+ // active set, so it has to surface as the type the fallback loop
97+ // already treats as transient. Otherwise a caller that narrows
98+ // `isTransient` to its own error rethrows immediately and never tries
99+ // the healthy clearnet default sitting next in the set.
100+ throw OnionServerWithoutTorException (connection.url);
101+ }
102+ try {
103+ // Precedence matters and predates this stack: an explicitly persisted
104+ // SOCKS setting wins over the Tor proxy toggle, which is the old
105+ // `settings.socks5 ?? torProxy` semantics. The onion route comes first
106+ // because it is the only one that can carry a hidden service.
107+ final routed = connection.withSocks5 (
108+ route? .endpoint.authority ??
109+ connection.socks5 ??
110+ _clearnetProxy (network, appSettings),
111+ );
112+ // The chokepoint that makes the invariant unavoidable: not every
113+ // consumer goes through our socket connector — BDK and LWK open
114+ // their own — so refusing here is the only check they all share.
115+ if (_isUnroutableOnion (routed)) {
116+ throw OnionServerWithoutTorException (routed.url);
117+ }
118+ return await operation (routed);
119+ } finally {
120+ await route? .close ();
121+ }
122+ },
123+ // An unroutable onion server is skipped, not fatal: the rest of the
124+ // active set may still be reachable. Callers narrowing `isTransient` to
125+ // their own error type must not turn that into a hard stop.
126+ isTransient: isTransient == null
127+ ? null
128+ : (error) =>
129+ error is OnionServerWithoutTorException || isTransient (error),
82130 );
83131 }
132+
133+ /// The external proxy for a *clearnet* Bitcoin server, when one is configured.
134+ ///
135+ /// Onion servers get a dedicated route from [_torSessionPort] ; this covers
136+ /// everything else. Without it, enabling the Tor proxy would silently stop
137+ /// protecting the default Electrum servers — which are clearnet, and are
138+ /// exactly what a user hides their IP from by turning Orbot on. The setting
139+ /// has always been documented as applying to Bitcoin, not to onions only.
140+ ///
141+ /// Liquid stays excluded, as it always has been.
142+ static String ? _clearnetProxy (
143+ ElectrumServerNetwork network,
144+ SettingsEntity appSettings,
145+ ) {
146+ if (network.isLiquid || ! appSettings.useTorProxy) return null ;
147+ return TorProxyEndpoint (
148+ host: InternetAddress .loopbackIPv4.address,
149+ port: appSettings.torProxyPort,
150+ ).authority;
151+ }
152+
153+ static bool _isUnroutableOnion (ElectrumConnection connection) =>
154+ ElectrumServerUrl (connection.url).isOnion &&
155+ (connection.socks5? .isEmpty ?? true );
84156}
0 commit comments