CakeWallet/lib/main.dart
cyan 5082dc20f3
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>
2025-06-20 22:56:18 +03:00

470 lines
17 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/app_scroll_behavior.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/background_sync.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
import 'package:cake_wallet/entities/get_encryption_key.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/locales/locale.dart';
import 'package:cake_wallet/reactions/bootstrap.dart';
import 'package:cake_wallet/router.dart' as Router;
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/root/root.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/themes/utils/theme_provider.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_daemon/flutter_daemon.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart';
import 'package:cw_core/root_dir.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/window_size.dart';
import 'package:logging/logging.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
final navigatorKey = GlobalKey<NavigatorState>();
final rootKey = GlobalKey<RootState>();
final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
Future<void> main({Key? topLevelKey}) async {
await runAppWithZone(topLevelKey: topLevelKey);
}
Future<void> runAppWithZone({Key? topLevelKey}) async {
bool isAppRunning = false;
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = ExceptionHandler.onError;
/// A callback that is invoked when an unhandled error occurs in the root
/// isolate.
PlatformDispatcher.instance.onError = (error, stack) {
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
return true;
};
await FlutterDaemon().unmarkBackgroundSync();
await initializeAppAtRoot();
if (kDebugMode) {
final appDocDir = await getAppDir();
final ledgerFile = File('${appDocDir.path}/ledger_log.txt');
if (!ledgerFile.existsSync()) ledgerFile.createSync();
Logger.root.onRecord.listen((event) async {
final content = ledgerFile.readAsStringSync();
ledgerFile.writeAsStringSync("$content\n${event.message}");
});
}
if (FeatureFlag.hasDevOptions) {
ProxyWrapper.logger = MemoryProxyLogger();
}
runApp(App(key: topLevelKey));
isAppRunning = true;
}, (error, stackTrace) async {
if (!isAppRunning) {
runApp(
TopLevelErrorWidget(error: error, stackTrace: stackTrace),
);
}
await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
});
}
Future<void> initializeAppAtRoot({bool reInitializing = false}) async {
if (!reInitializing) await setDefaultMinimumWindowSize();
await CakeHive.close();
await initializeAppConfigs();
}
Future<void> initializeAppConfigs({bool loadWallet = true}) async {
setRootDirFromEnv();
final appDir = await getAppDir();
CakeHive.init(appDir.path);
if (!CakeHive.isAdapterRegistered(Contact.typeId)) {
CakeHive.registerAdapter(ContactAdapter());
}
if (!CakeHive.isAdapterRegistered(Node.typeId)) {
CakeHive.registerAdapter(NodeAdapter());
}
if (!CakeHive.isAdapterRegistered(TransactionDescription.typeId)) {
CakeHive.registerAdapter(TransactionDescriptionAdapter());
}
if (!CakeHive.isAdapterRegistered(Trade.typeId)) {
CakeHive.registerAdapter(TradeAdapter());
}
if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) {
CakeHive.registerAdapter(AddressInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) {
CakeHive.registerAdapter(WalletInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(DERIVATION_TYPE_TYPE_ID)) {
CakeHive.registerAdapter(DerivationTypeAdapter());
}
if (!CakeHive.isAdapterRegistered(DERIVATION_INFO_TYPE_ID)) {
CakeHive.registerAdapter(DerivationInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(HARDWARE_WALLET_TYPE_TYPE_ID)) {
CakeHive.registerAdapter(HardwareWalletTypeAdapter());
}
if (!CakeHive.isAdapterRegistered(WALLET_TYPE_TYPE_ID)) {
CakeHive.registerAdapter(WalletTypeAdapter());
}
if (!CakeHive.isAdapterRegistered(Template.typeId)) {
CakeHive.registerAdapter(TemplateAdapter());
}
if (!CakeHive.isAdapterRegistered(ExchangeTemplate.typeId)) {
CakeHive.registerAdapter(ExchangeTemplateAdapter());
}
if (!CakeHive.isAdapterRegistered(Order.typeId)) {
CakeHive.registerAdapter(OrderAdapter());
}
if (!CakeHive.isAdapterRegistered(UnspentCoinsInfo.typeId)) {
CakeHive.registerAdapter(UnspentCoinsInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) {
CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter());
}
if (!CakeHive.isAdapterRegistered(HavenSeedStore.typeId)) {
CakeHive.registerAdapter(HavenSeedStoreAdapter());
}
if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) {
CakeHive.registerAdapter(MwebUtxoAdapter());
}
if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
CakeHive.registerAdapter(PayjoinSessionAdapter());
}
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey);
final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey);
final contacts = await CakeHive.openBox<Contact>(Contact.boxName);
final nodes = await CakeHive.openBox<Node>(Node.boxName);
final powNodes =
await CakeHive.openBox<Node>(Node.boxName + "pow"); // must be different from Node.boxName
final transactionDescriptions = await CakeHive.openBox<TransactionDescription>(
TransactionDescription.boxName,
encryptionKey: transactionDescriptionsBoxKey);
final trades = await CakeHive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey);
final orders = await CakeHive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
final templates = await CakeHive.openBox<Template>(Template.boxName);
final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
final havenSeedStoreBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
final havenSeedStore = await CakeHive.openBox<HavenSeedStore>(HavenSeedStore.boxName,
encryptionKey: havenSeedStoreBoxKey);
await initialSetup(
loadWallet: loadWallet,
sharedPreferences: await SharedPreferences.getInstance(),
nodes: nodes,
powNodes: powNodes,
walletInfoSource: walletInfoSource,
contactSource: contacts,
tradesSource: trades,
ordersSource: orders,
unspentCoinsInfoSource: unspentCoinsInfoSource,
// fiatConvertationService: fiatConvertationService,
templates: templates,
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 49,
);
}
Future<void> initialSetup({
required bool loadWallet,
required SharedPreferences sharedPreferences,
required Box<Node> nodes,
required Box<Node> powNodes,
required Box<WalletInfo> walletInfoSource,
required Box<Contact> contactSource,
required Box<Trade> tradesSource,
required Box<Order> ordersSource,
// required FiatConvertationService fiatConvertationService,
required Box<Template> templates,
required Box<ExchangeTemplate> exchangeTemplates,
required Box<TransactionDescription> transactionDescriptions,
required SecureStorage secureStorage,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<HavenSeedStore> havenSeedStore,
int initialMigrationVersion = 15,
}) async {
LanguageService.loadLocaleList();
await defaultSettingsMigration(
secureStorage: secureStorage,
version: initialMigrationVersion,
sharedPreferences: sharedPreferences,
walletInfoSource: walletInfoSource,
contactSource: contactSource,
tradeSource: tradesSource,
nodes: nodes,
powNodes: powNodes,
havenSeedStore: havenSeedStore);
await setup(
walletInfoSource: walletInfoSource,
nodeSource: nodes,
powNodeSource: powNodes,
contactSource: contactSource,
tradesSource: tradesSource,
templates: templates,
exchangeTemplates: exchangeTemplates,
transactionDescriptionBox: transactionDescriptions,
ordersSource: ordersSource,
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
unspentCoinsInfoSource: unspentCoinsInfoSource,
payjoinSessionSource: payjoinSessionSource,
navigatorKey: navigatorKey,
secureStorage: secureStorage,
);
await bootstrapOffline();
final settingsStore = getIt<SettingsStore>();
if (!settingsStore.currentBuiltinTor) {
bootstrapOnline(navigatorKey, loadWallet: loadWallet);
}
}
class App extends StatefulWidget {
App({this.key});
final Key? key;
@override
AppState createState() => AppState();
}
class AppState extends State<App> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Observer(builder: (BuildContext context) {
final appStore = getIt.get<AppStore>();
final authService = getIt.get<AuthService>();
final linkViewModel = getIt.get<LinkViewModel>();
final tradeMonitor = getIt.get<TradeMonitor>();
final settingsStore = appStore.settingsStore;
final statusBarColor = Colors.transparent;
final authenticationStore = getIt.get<AuthenticationStore>();
final initialRoute = authenticationStore.state == AuthenticationState.uninitialized
? Routes.welcome
: settingsStore.currentBuiltinTor ? Routes.startTor : Routes.login;
final currentTheme = appStore.themeStore.currentTheme;
final statusBarBrightness = currentTheme.type == currentTheme.isDark
? Brightness.light
: Brightness.dark;
final statusBarIconBrightness = currentTheme.type == currentTheme.isDark
? Brightness.light
: Brightness.dark;
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: statusBarColor,
statusBarBrightness: statusBarBrightness,
statusBarIconBrightness: statusBarIconBrightness));
return Root(
key: widget.key ?? rootKey,
appStore: appStore,
authenticationStore: authenticationStore,
navigatorKey: navigatorKey,
authService: authService,
linkViewModel: linkViewModel,
tradeMonitor: tradeMonitor,
child: ThemeProvider(
themeStore: appStore.themeStore,
materialAppBuilder: (context, theme, darkTheme, themeMode) =>
MaterialApp(
navigatorObservers: [routeObserver],
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
localizationsDelegates: localizationDelegates,
supportedLocales: S.delegate.supportedLocales,
locale: Locale(appStore.settingsStore.languageCode),
onGenerateRoute: (settings) => Router.createRoute(settings),
initialRoute: initialRoute,
scrollBehavior: AppScrollBehavior(),
home: _Home(),
),
),
);
},
);
}
}
class _Home extends StatefulWidget {
const _Home();
@override
State<_Home> createState() => _HomeState();
}
class _HomeState extends State<_Home> {
@override
void didChangeDependencies() {
_setOrientation(context);
super.didChangeDependencies();
}
void _setOrientation(BuildContext context) {
if (!DeviceInfo.instance.isDesktop) {
if (responsiveLayoutUtil.shouldRenderMobileUI) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
} else {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight
]);
}
}
}
@override
Widget build(BuildContext context) {
return const SizedBox.shrink();
}
}
class TopLevelErrorWidget extends StatelessWidget {
const TopLevelErrorWidget({
required this.error,
required this.stackTrace,
super.key,
});
final Object error;
final StackTrace stackTrace;
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
scrollBehavior: AppScrollBehavior(),
home: Scaffold(
body: SingleChildScrollView(
child: Container(
margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20),
child: Column(
children: [
Text(
'Error:\n${error.toString()}',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 22,
),
),
Text(
'Stack trace:\n${stackTrace.toString()}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
),
),
],
),
),
),
),
);
}
}
@pragma('vm:entry-point')
Future<void> backgroundSync() async {
bool shouldUnmark = false;
try {
printV("Background sync triggered");
printV("- WidgetsFlutterBinding.ensureInitialized()");
WidgetsFlutterBinding.ensureInitialized();
printV("- DartPluginRegistrant.ensureInitialized()");
DartPluginRegistrant.ensureInitialized();
printV("- FlutterDaemon.markBackgroundSync()");
final val = await FlutterDaemon().markBackgroundSync();
if (val) {
printV("Background sync already in progress");
return;
}
shouldUnmark = true;
printV("Starting background sync");
final backgroundSync = BackgroundSync();
await initializeAppConfigs(loadWallet: false);
await backgroundSync.sync();
printV("Background sync completed");
} finally {
if (shouldUnmark) {
printV("Unmarking background sync");
await FlutterDaemon().unmarkBackgroundSync();
} else {
printV("Not unmarking background sync");
}
}
}