CW-519 Enable built-in Tor (#1950)

* tor wip

* Enable tor on iOS

* Prevent app lag when node is exceptionally slow (usually over tor)

* fix: logic in daemonBlockchainHeight refresh
fix: storing tor state

* Pin ledger_flutter_plus dependency to fix builds

* bump arti version

* wip

* add single httpclient

* route everything I was able to catch trough the built-in tor node

* Enable proxy for http.Client [run tests]

* add tor proxy support to cw_evm, cw_tron and cw_polygon [run tests]

* remove log pollution, cleanup [skip slack]

* fix tests not working in latest main [skip slack] [run tests]

* remove cw_wownero import

* fix build issues

* migrate all remaining calls to use ProxyWrapper
add a CI action to enforce using ProxyWrapper instead of http/http.dart to prevent leaks

* fix tor background sync (will work on test builds after #2142 is merged and this PR is rebased on top)

* wip [skip ci]

* relicense to GPLv3 add socks5 license, build fixes

* use ProxyWrapper instead of http in robinhood

* Revert "relicense to GPLv3"

* feat(cw_bitcoin): support socks proxy and CakeTor

* fix(tor): migrate OCP and EVM over to ProxyWrapper()

* chore: cleanup
fix: show tor loading screen when app is starting

* fix: tor switch properly dismisses fullscreen loading dialog
fix: connectToNode after tor startup on app start

* fix(tor): status check for xmr/wow/zano

* fix(tor): onramper request fix

* fix(api): ServicesResponse is now being cached and doesn't fetch data everytime DashboardViewModel is being rebuilt
fix(tor): do not fallback to clearnet when tor failed.
fix(tor): do not leak connections during app startup
chore: refactor bootstrap() function to be separated into bootstrapOffline and bootstrapOnline
fix(cw_bitcoin): migrate payjoin to use ProxyWrapper

* [skip ci] remove print

* address comments from review

* fix: derusting tor implementation

Instead of rust-based Arti I've moved back to the
OG C++ tor implementation.
This fixed all issues we had with Tor.
- onion services now work
- all requests are going through without random errors
- we don't have to navigate a maze of multiple forks of multiple packages
- fully working `torrc` config file (probably will be needed for Tari).
- logging for Tor client
- and so on.

feat: network logging tab
feat: use built-in proxy on Tails - this should resolve all issues for Tails users (needs testing though)

* fix conflicts with main
bump https to fix build issue
relax store() call

* fix(cw_wownero): tor connection
fix(tor): connection issues

* fix(cw_evm): add missing chainId
fix(cw_core): solana rpc fix

* feat: mark tor as experimental
fix: drop anonpay onion authority
fix: drop fiatapi onion authority
fix: drop trocador onion authority
fix: disable networkimage when tor is enabled
fix: handle cakepay errors gracefully

* fix re-formatting [skip ci]

* changes from review

* Delete android/.kotlin/sessions/kotlin-compiler-2468481326039681181.salive

* fix missing imports

* Update pubspec_base.yaml

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan
2025-06-20 22:56:18 +03:00
committed by GitHub
co-authored by OmarHatem
parent 18c2ba9366
commit 5082dc20f3
139 changed files with 2754 additions and 878 deletions
+155
View File
@@ -0,0 +1,155 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/view_model/dev/network_requests_view_model.dart';
import 'package:cw_core/utils/proxy_logger/abstract.dart';
import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class DevNetworkRequests extends BasePage {
final NetworkRequestsViewModel viewModel = NetworkRequestsViewModel();
DevNetworkRequests() {
viewModel.loadLogs();
}
@override
String? get title => "[dev] network requests";
@override
Widget? trailing(BuildContext context) {
return IconButton(
icon: Icon(Icons.refresh),
onPressed: () => viewModel.loadLogs(),
);
}
@override
Widget body(BuildContext context) {
return Observer(
builder: (_) {
if (viewModel.logs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("No logs loaded"),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => viewModel.loadLogs(),
child: Text("Load Logs"),
),
],
),
);
} else {
return ListView.builder(
itemCount: viewModel.logs.length,
itemBuilder: (BuildContext context, int i) {
final item = viewModel.logs[i];
return ListTile(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return DevRequestDetails(item);
},
),
);
},
leading: switch (item.network) {
RequestNetwork.clearnet => Text("C"),
RequestNetwork.tor => Text("T"),
},
trailing: switch (item.method) {
RequestMethod.get => Text("GET"),
RequestMethod.post => Text("POST"),
RequestMethod.put => Text("PUT"),
RequestMethod.delete => Text("DELETE"),
RequestMethod.newHttpClient ||
RequestMethod.newHttpIOClient ||
RequestMethod.newProxySocket => null,
},
title: Text(item.time.toIso8601String()),
subtitle: switch (item.method) {
RequestMethod.get ||
RequestMethod.post ||
RequestMethod.put ||
RequestMethod.delete => Text("${item.uri}"),
RequestMethod.newHttpClient => Text("newHttpClient"),
RequestMethod.newHttpIOClient => Text("newHttpIOClient"),
RequestMethod.newProxySocket => Text("newProxySocket"),
},
tileColor: item.error != null ? Colors.red : null,
);
},
);
}
},
);
}
}
class DevRequestDetails extends BasePage {
final MemoryProxyLoggerEntry req;
DevRequestDetails(this.req);
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _body(context),
);
}
Widget _body(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Time"),
SelectableText(req.time.toString()),
_sectionTitle("Method"),
SelectableText(req.method.toString()),
_sectionTitle("URI"),
SelectableText(req.uri?.toString() ?? "null"),
_sectionTitle("Network"),
SelectableText(req.network.toString()),
_sectionTitle("Body (as UTF-8)"),
SelectableText(_tryDecodeBody(req.body)),
_sectionTitle("Response"),
SelectableText(req.response?.body ?? "null"),
_sectionTitle("Error"),
SelectableText(req.error ?? "No error"),
_sectionTitle("Stack Trace"),
SelectableText(req.trace.toString()),
],
);
}
Widget _sectionTitle(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
);
}
String _tryDecodeBody(Uint8List body) {
try {
return utf8.decode(body);
} catch (_) {
return 'Binary data (${body.length} bytes)';
}
}
}
+20 -1
View File
@@ -196,8 +196,27 @@ class NodeForm extends StatelessWidget {
Observer(
builder: (_) => Column(
children: [
if (nodeViewModel.usesEmbeddedProxy) ...[
Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.usesEmbeddedProxy,
gradientBackground: false,
borderColor: Theme.of(context).dividerColor,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: null,
caption: 'Embedded Tor SOCKS Proxy',
),
],
),
),
],
Padding(
padding: EdgeInsets.only(top: 20),
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
@@ -5,6 +5,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/wallet_connect_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
@@ -71,10 +72,28 @@ class ConnectionSyncPage extends BasePage {
),
],
if (FeatureFlag.isInAppTorEnabled)
SettingsCellWithArrow(
title: S.current.tor_connection,
handler: (context) => Navigator.of(context).pushNamed(Routes.torPage),
),
Observer(builder: (context) {
return SettingsSwitcherCell(
leading: Container(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: Colors.red.shade100,
borderRadius: BorderRadius.circular(3),
),
child: Text(
'Alpha',
style: TextStyle(
color: Colors.red.shade700,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
),
title: S.current.enable_builtin_tor,
value: dashboardViewModel.builtinTor,
onValueChange: (_, bool value) => dashboardViewModel.setBuiltinTor(value, context),
);
}),
],
),
);
@@ -93,6 +93,12 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devBackgroundSyncLogs),
),
if (FeatureFlag.hasDevOptions)
SettingsCellWithArrow(
title: '[dev] network requests logs',
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devNetworkRequests),
),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
-269
View File
@@ -1,269 +0,0 @@
import 'dart:async';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/themes/utils/custom_theme_colors.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
// import 'package:tor/tor.dart';
class TorPage extends BasePage {
final AppStore appStore;
TorPage(this.appStore);
@override
Widget body(BuildContext context) {
return TorPageBody(appStore);
}
}
class TorPageBody extends StatefulWidget {
final AppStore appStore;
const TorPageBody(this.appStore, {Key? key}) : super(key: key);
@override
State<TorPageBody> createState() => _TorPageBodyState();
}
class _TorPageBodyState extends State<TorPageBody> {
bool torEnabled = false;
bool connecting = false;
// Set the default text for the host input field.
final hostController = TextEditingController(text: 'https://icanhazip.com/');
// https://check.torproject.org is another good option.
Future<void> startTor() async {
setState(() {
connecting = true; // Update flag
});
// await Tor.init();
//
// // Start the proxy
// await Tor.instance.start();
//
// // Toggle started flag.
// setState(() {
// torEnabled = Tor.instance.enabled; // Update flag
// connecting = false;
// });
//
// final node = widget.appStore.settingsStore.getCurrentNode(widget.appStore.wallet!.type);
// if (node.socksProxyAddress?.isEmpty ?? true) {
// node.socksProxyAddress = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}";
// }
// widget.appStore.wallet!.connectToNode(node: node);
printV('Done awaiting; tor should be running');
}
Future<void> endTor() async {
// // Start the proxy
// Tor.instance.disable();
//
// // Toggle started flag.
// setState(() {
// torEnabled = Tor.instance.enabled; // Update flag
// });
//
// printV('Done awaiting; tor should be stopped');
}
//
// @override
// void initState() {
// super.initState();
//
// torEnabled = Tor.instance.enabled;
// }
@override
void dispose() {
// Clean up the controller when the widget is disposed.
hostController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(10),
child: connecting
? ConnectingScreen()
: torEnabled
? DisconnectScreen(disconnect: endTor)
: ConnectScreen(connect: startTor),
),
);
}
}
class ConnectScreen extends StatelessWidget {
final Function() connect;
const ConnectScreen({super.key, required this.connect});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Icon(
Icons.lock,
color: Theme.of(context).colorScheme.onPrimary,
size: 100,
),
),
SizedBox(height: 48),
Text(
'Connect to Tor',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16),
Text(
'Your connection to the Tor network ensures privacy and security.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
SizedBox(height: 48),
ElevatedButton(
onPressed: connect,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Connect',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 18,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
),
);
}
}
class DisconnectScreen extends StatelessWidget {
final Function() disconnect;
const DisconnectScreen({super.key, required this.disconnect});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: CustomThemeColors.syncGreen,
),
child: Icon(
Icons.check,
color: Theme.of(context).colorScheme.onPrimary,
size: 100,
),
),
SizedBox(height: 20),
Text(
'Connected to Tor',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
Text(
'You are currently connected to the Tor network.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
SizedBox(height: 30),
ElevatedButton(
onPressed: disconnect,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Disconnect',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 18,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
);
}
}
class ConnectingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: CustomThemeColors.syncYellow,
),
child: Icon(
Icons.hourglass_bottom,
color: Theme.of(context).colorScheme.onPrimary,
size: 100,
),
),
SizedBox(height: 20),
Text(
'Connecting...',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
],
),
);
}
}
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/view_model/start_tor_view_model.dart';
class StartTorPage extends BasePage {
StartTorPage(this.startTorViewModel);
final StartTorViewModel startTorViewModel;
@override
String get title => S.current.tor_connection;
@override
Widget leading(BuildContext context) {
return Container();
}
@override
Widget body(BuildContext context) {
startTorViewModel.startTor(context);
return Container(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
children: [
SizedBox(width: double.maxFinite),
if (startTorViewModel.isLoading) ...[
CircularProgressIndicator(),
SizedBox(height: 20),
_buildWaitingText(context),
],
if (startTorViewModel.showOptions) ...[
_buildOptionsButtons(context),
],
],
),
],
),
);
}
Widget _buildWaitingText(BuildContext context) {
return Observer(
builder: (_) {
return Column(
children: [
Text(
S.current.establishing_tor_connection,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
if (startTorViewModel.showOptions) _buildOptionsButtons(context),
],
);
},
);
}
Widget _buildOptionsButtons(BuildContext context) {
return Column(
children: [
Text(
S.current.tor_connection_timeout,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
PrimaryButton(
onPressed: () => startTorViewModel.disableTor(context),
text: S.current.disable_tor,
color: Theme.of(context).colorScheme.primary,
textColor: Colors.white,
),
SizedBox(height: 16),
PrimaryButton(
onPressed: () => startTorViewModel.ignoreAndLaunchApp(context),
text: S.current.ignor,
color: Theme.of(context).colorScheme.secondary,
textColor: Colors.white,
),
],
);
}
}
@@ -2,10 +2,10 @@ import 'dart:convert';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:reown_walletkit/reown_walletkit.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
@@ -41,7 +41,7 @@ class EvmChainServiceImpl {
}) : ethClient = web3Client ??
Web3Client(
appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
http.Client(),
ProxyWrapper().getHttpIOClient(),
) {
for (final event in EventsConstants.allEvents) {
walletKit.registerEventEmitter(
@@ -563,14 +563,15 @@ class EvmChainServiceImpl {
},
);
final response = await http.get(
uri,
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: {
"Accept": "application/json",
"X-API-Key": secrets.moralisApiKey,
},
);
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
final symbol = (decodedResponse['symbol'] ?? '') as String;
@@ -4,6 +4,7 @@ import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_servic
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
@@ -153,7 +154,10 @@ class WCCDetailsWidget extends BasePage {
child: CircleAvatar(
backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty
? NetworkImage(pairing.peerMetadata!.icons[0])
: const AssetImage('assets/images/app_logo.png')) as ImageProvider<Object>,
: AssetImage(
CakeTor.instance.enabled
? 'assets/images/tor_logo.svg'
: 'assets/images/app_logo.png')) as ImageProvider<Object>,
),
),
const SizedBox(height: 20.0),
@@ -126,9 +126,9 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
dividerColor: Colors.transparent,
padding: EdgeInsets.zero,
tabs: [
Tab(text: S.of(context).widgets_seed),
if (showKeyTab) Tab(text: S.of(context).keys),
if (showLegacySeedTab) Tab(text: S.of(context).legacy),
Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),),
if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
],
),
),
+6 -1
View File
@@ -1,3 +1,4 @@
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
@@ -265,7 +266,11 @@ class Badge extends StatelessWidget {
}
Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) {
final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
if (CakeTor.instance.enabled && isNetworkImage) {
imagePath = "assets/images/tor_logo.svg";
isNetworkImage = false;
}
final bool isSvg = imagePath.endsWith('.svg');
final double imageHeight = height ?? 35;
final double imageWidth = width ?? 35;
+2 -2
View File
@@ -16,7 +16,7 @@ class StandardCheckbox extends StatelessWidget {
final Color? borderColor;
final Color? iconColor;
final Color? captionColor;
final Function(bool) onChanged;
final Function(bool)? onChanged;
@override
Widget build(BuildContext context) {
@@ -40,7 +40,7 @@ class StandardCheckbox extends StatelessWidget {
BoxDecoration(border: boxBorder, borderRadius: BorderRadius.all(Radius.circular(8.0)));
return GestureDetector(
onTap: () => onChanged(!value),
onTap: onChanged == null ? null : () => onChanged!(!value),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,