What changed, and why it matters
This commit adds user-configurable Tor settings to the Skylight Wallet app. Users can now choose between built-in Tor, an external Tor proxy, or disabling Tor entirely. It also updates how the app fetches exchange rates and blockchain heights so they respect the new Tor setting. There is no clear security bug in the patch itself, but it introduces a 'Tor disabled' mode and changes how network traffic is routed, which could affect user privacy if the settings are mishandled or bypassed.
Review the full diff and any follow-up commits to confirm that all network paths in the wallet (daemon sync, LWS, fiat API, fee fetching) consistently honor TorSettingsService. Pay special attention to the FIXME in lib/util/height.dart where getCurrentBlockchainHeight() returns 0 when Tor is disabled. Consider validating the external SOCKS proxy more robustly and warning users clearly when Tor is disabled.
Security signals we found
New 'disabled' Tor mode allows wallet traffic to bypass Tor entirely
External Tor mode lets users specify an arbitrary SOCKS port, which may be misconfigured or point to a non-Tor proxy
Connection test only validates external Tor against check.torproject.org/api/ip, which can give false confidence
getCurrentBlockchainHeight() returns 0 with a FIXME comment when Tor is disabled, potentially causing restore-height issues
Fiat rate service now stops when Tor is disabled, but other wallet traffic paths need review to confirm they also respect the setting
SharedPreferences-backed Tor settings are not integrity-protected or encrypted
Evidence from the diff
The commit introduces a TorSettingsService singleton with three modes: builtIn, external, and disabled. It persists the mode and SOCKS port in SharedPreferences, exposes a getProxy() helper, and updates FiatRateModel, WalletModel, and connection setup to route HTTP requests through the configured proxy. It replaces the previous always-on built-in Tor behavior with user choice, adds UI screens for Tor info/settings, and updates fiat-rate fetching to stop when Tor is disabled. It also switches wallet creation from fetching the current blockchain height over the network to estimating it from a local date-to-height table, and extends that table through 2025.
Changed components
lib/services/tor_settings_service.dartlib/models/fiat_rate_model.dartlib/models/wallet_model.dartlib/screens/connection_setup.dartlib/screens/settings.dartlib/screens/tor_info.dartlib/screens/tor_settings.dartlib/widgets/tor_settings_form.dartlib/util/height.dartlib/util/get_height_by_date.dartInspect captured patch +959 / −170
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index aa865c4..29e6cc1 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -158,5 +158,28 @@
"sendTransactionPriority": "Transaction Priority",
"sendFeeLabel": "Fee",
"sendBalanceLabel": "Balance",
- "sendFailedToGetFeesError": "Failed to get fees."
+ "sendFailedToGetFeesError": "Failed to get fees.",
+ "torInfoTitle": "Tor Built-in",
+ "torInfoDescription": "Skylight Wallet automatically uses built-in Tor to protect your internet connections.",
+ "torInfoContinueButton": "Continue",
+ "torInfoConfigureButton": "Configure",
+ "torSettingsTitle": "Tor Settings",
+ "torSettingsModeLabel": "Tor Mode",
+ "torSettingsModeBuiltIn": "Built-in Tor",
+ "torSettingsModeExternal": "External Tor",
+ "torSettingsModeDisabled": "No Tor",
+ "torSettingsSocksPortLabel": "SOCKS Port",
+ "torSettingsSocksPortHint": "e.g. 9050",
+ "torSettingsSaveButton": "Save",
+ "torSettingsTestConnectionButton": "Test Connection",
+ "settingsTorSettingsLabel": "Tor Settings",
+ "connectionSetupUsingInternalTor": "Using internal Tor",
+ "connectionSetupUsingExternalTor": "Using external Tor proxy at {address}",
+ "@connectionSetupUsingExternalTor": {
+ "placeholders": {
+ "address": {
+ "type": "String"
+ }
+ }
+ }
}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 9178fa3..50ac05d 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -1004,6 +1004,102 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Failed to get fees.'**
String get sendFailedToGetFeesError;
+
+ /// No description provided for @torInfoTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Tor Built-in'**
+ String get torInfoTitle;
+
+ /// No description provided for @torInfoDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Skylight Wallet automatically uses built-in Tor to protect your internet connections.'**
+ String get torInfoDescription;
+
+ /// No description provided for @torInfoContinueButton.
+ ///
+ /// In en, this message translates to:
+ /// **'Continue'**
+ String get torInfoContinueButton;
+
+ /// No description provided for @torInfoConfigureButton.
+ ///
+ /// In en, this message translates to:
+ /// **'Configure'**
+ String get torInfoConfigureButton;
+
+ /// No description provided for @torSettingsTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Tor Settings'**
+ String get torSettingsTitle;
+
+ /// No description provided for @torSettingsModeLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Tor Mode'**
+ String get torSettingsModeLabel;
+
+ /// No description provided for @torSettingsModeBuiltIn.
+ ///
+ /// In en, this message translates to:
+ /// **'Built-in Tor'**
+ String get torSettingsModeBuiltIn;
+
+ /// No description provided for @torSettingsModeExternal.
+ ///
+ /// In en, this message translates to:
+ /// **'External Tor'**
+ String get torSettingsModeExternal;
+
+ /// No description provided for @torSettingsModeDisabled.
+ ///
+ /// In en, this message translates to:
+ /// **'No Tor'**
+ String get torSettingsModeDisabled;
+
+ /// No description provided for @torSettingsSocksPortLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'SOCKS Port'**
+ String get torSettingsSocksPortLabel;
+
+ /// No description provided for @torSettingsSocksPortHint.
+ ///
+ /// In en, this message translates to:
+ /// **'e.g. 9050'**
+ String get torSettingsSocksPortHint;
+
+ /// No description provided for @torSettingsSaveButton.
+ ///
+ /// In en, this message translates to:
+ /// **'Save'**
+ String get torSettingsSaveButton;
+
+ /// No description provided for @torSettingsTestConnectionButton.
+ ///
+ /// In en, this message translates to:
+ /// **'Test Connection'**
+ String get torSettingsTestConnectionButton;
+
+ /// No description provided for @settingsTorSettingsLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Tor Settings'**
+ String get settingsTorSettingsLabel;
+
+ /// No description provided for @connectionSetupUsingInternalTor.
+ ///
+ /// In en, this message translates to:
+ /// **'Using internal Tor'**
+ String get connectionSetupUsingInternalTor;
+
+ /// No description provided for @connectionSetupUsingExternalTor.
+ ///
+ /// In en, this message translates to:
+ /// **'Using external Tor proxy at {address}'**
+ String connectionSetupUsingExternalTor(String address);
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 3a03b10..9704bc0 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -482,4 +482,55 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sendFailedToGetFeesError => 'Failed to get fees.';
+
+ @override
+ String get torInfoTitle => 'Tor Built-in';
+
+ @override
+ String get torInfoDescription =>
+ 'Skylight Wallet automatically uses built-in Tor to protect your internet connections.';
+
+ @override
+ String get torInfoContinueButton => 'Continue';
+
+ @override
+ String get torInfoConfigureButton => 'Configure';
+
+ @override
+ String get torSettingsTitle => 'Tor Settings';
+
+ @override
+ String get torSettingsModeLabel => 'Tor Mode';
+
+ @override
+ String get torSettingsModeBuiltIn => 'Built-in Tor';
+
+ @override
+ String get torSettingsModeExternal => 'External Tor';
+
+ @override
+ String get torSettingsModeDisabled => 'No Tor';
+
+ @override
+ String get torSettingsSocksPortLabel => 'SOCKS Port';
+
+ @override
+ String get torSettingsSocksPortHint => 'e.g. 9050';
+
+ @override
+ String get torSettingsSaveButton => 'Save';
+
+ @override
+ String get torSettingsTestConnectionButton => 'Test Connection';
+
+ @override
+ String get settingsTorSettingsLabel => 'Tor Settings';
+
+ @override
+ String get connectionSetupUsingInternalTor => 'Using internal Tor';
+
+ @override
+ String connectionSetupUsingExternalTor(String address) {
+ return 'Using external Tor proxy at $address';
+ }
}
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 3db66b5..e7f315d 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -482,4 +482,55 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get sendFailedToGetFeesError => 'Não foi possível carregar taxas.';
+
+ @override
+ String get torInfoTitle => 'Tor Integrado';
+
+ @override
+ String get torInfoDescription =>
+ 'A Carteira Skylight usa automaticamente Tor integrado para proteger suas conexões de internet.';
+
+ @override
+ String get torInfoContinueButton => 'Continuar';
+
+ @override
+ String get torInfoConfigureButton => 'Configurar';
+
+ @override
+ String get torSettingsTitle => 'Configurações do Tor';
+
+ @override
+ String get torSettingsModeLabel => 'Modo Tor';
+
+ @override
+ String get torSettingsModeBuiltIn => 'Tor Integrado';
+
+ @override
+ String get torSettingsModeExternal => 'Tor Externo';
+
+ @override
+ String get torSettingsModeDisabled => 'Sem Tor';
+
+ @override
+ String get torSettingsSocksPortLabel => 'Porta SOCKS';
+
+ @override
+ String get torSettingsSocksPortHint => 'ex: 9050';
+
+ @override
+ String get torSettingsSaveButton => 'Salvar';
+
+ @override
+ String get torSettingsTestConnectionButton => 'Testar Conexão';
+
+ @override
+ String get settingsTorSettingsLabel => 'Configurações do Tor';
+
+ @override
+ String get connectionSetupUsingInternalTor => 'Usando Tor interno';
+
+ @override
+ String connectionSetupUsingExternalTor(String address) {
+ return 'Usando proxy Tor externo em $address';
+ }
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index bf053fb..47a2df0 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -158,5 +158,28 @@
"sendTransactionPriority": "Prioridade da Transação",
"sendFeeLabel": "Taxa",
"sendBalanceLabel": "Saldo",
- "sendFailedToGetFeesError": "Não foi possível carregar taxas."
+ "sendFailedToGetFeesError": "Não foi possível carregar taxas.",
+ "torInfoTitle": "Tor Integrado",
+ "torInfoDescription": "A Carteira Skylight usa automaticamente Tor integrado para proteger suas conexões de internet.",
+ "torInfoContinueButton": "Continuar",
+ "torInfoConfigureButton": "Configurar",
+ "torSettingsTitle": "Configurações do Tor",
+ "torSettingsModeLabel": "Modo Tor",
+ "torSettingsModeBuiltIn": "Tor Integrado",
+ "torSettingsModeExternal": "Tor Externo",
+ "torSettingsModeDisabled": "Sem Tor",
+ "torSettingsSocksPortLabel": "Porta SOCKS",
+ "torSettingsSocksPortHint": "ex: 9050",
+ "torSettingsSaveButton": "Salvar",
+ "torSettingsTestConnectionButton": "Testar Conexão",
+ "settingsTorSettingsLabel": "Configurações do Tor",
+ "connectionSetupUsingInternalTor": "Usando Tor interno",
+ "connectionSetupUsingExternalTor": "Usando proxy Tor externo em {address}",
+ "@connectionSetupUsingExternalTor": {
+ "placeholders": {
+ "address": {
+ "type": "String"
+ }
+ }
+ }
}
diff --git a/lib/main.dart b/lib/main.dart
index b95438e..7641509 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -7,6 +7,7 @@ import 'package:timeago/timeago.dart' as timeago;
import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/models/contact_model.dart';
+import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/screens/confirm_send.dart';
import 'package:skylight_wallet/screens/lws_details.dart';
import 'package:skylight_wallet/screens/lws_keys.dart';
@@ -28,6 +29,8 @@ import 'package:skylight_wallet/screens/restore_wallet.dart';
import 'package:skylight_wallet/screens/restore_warning.dart';
import 'package:skylight_wallet/screens/wallet_home.dart';
import 'package:skylight_wallet/screens/welcome.dart';
+import 'package:skylight_wallet/screens/tor_info.dart';
+import 'package:skylight_wallet/screens/tor_settings.dart';
import 'package:skylight_wallet/screens/address_book.dart';
import 'package:skylight_wallet/screens/privacy_policy.dart';
import 'package:skylight_wallet/screens/terms_of_service.dart';
@@ -48,6 +51,7 @@ void main() async {
await createAppDir();
copyCacertToAppDocumentsDir();
TorService.sharedInstance.start();
+ TorSettingsService.sharedInstance.loadSettings();
await registerPeriodicTasks();
await NotificationService().init();
cleanOldLogFiles();
@@ -65,11 +69,6 @@ Future<bool> loadExistingWalletIfExists(WalletModel wallet) async {
return true;
}
- // No wallet exists, fetch and cache the blockchain height in the background for future wallet creation
- if (wallet.blockchainHeightCompleter == null) {
- wallet.fetchAndCacheBlockchainHeight();
- }
-
return false;
}
@@ -89,6 +88,7 @@ class MyApp extends StatelessWidget {
child: Consumer2<LanguageModel, ThemeModel>(
builder: (context, languageProvider, themeProvider, child) {
final wallet = Provider.of<WalletModel>(context, listen: false);
+ final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
return FutureBuilder(
// We need to check for wallet existence to determine the correct initial route,
@@ -113,6 +113,10 @@ class MyApp extends StatelessWidget {
: '/wallet_home'
: '/welcome';
+ if (walletExists) {
+ fiatRate.startService();
+ }
+
return MaterialApp(
title: 'Skylight Monero Wallet',
localizationsDelegates: AppLocalizations.localizationsDelegates,
@@ -133,6 +137,8 @@ class MyApp extends StatelessWidget {
locale: Locale.fromSubtags(languageCode: languageProvider.language),
routes: {
'/welcome': (context) => WelcomeScreen(),
+ '/tor_info': (context) => TorInfoScreen(),
+ '/tor_settings': (context) => TorSettingsScreen(),
'/connection_setup': (context) => ConnectionSetupScreen(),
'/create_wallet_password': (context) => CreateWalletPasswordScreen(),
'/create_wallet': (context) => CreateWalletScreen(),
diff --git a/lib/models/fiat_rate_model.dart b/lib/models/fiat_rate_model.dart
index 0723695..5a1a4a8 100644
--- a/lib/models/fiat_rate_model.dart
+++ b/lib/models/fiat_rate_model.dart
@@ -1,42 +1,46 @@
import 'dart:async';
import 'package:flutter/material.dart';
-import 'package:skylight_wallet/consts.dart';
+import 'package:skylight_wallet/consts.dart';
+import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
-import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/socks_http.dart';
class FiatRateModel with ChangeNotifier {
double? _rate;
+ bool _isLoading = false;
bool _hasFailed = false;
+ bool _isDisabled = false;
String _fiatCode = 'USD';
Timer? _rateFetchTimer;
double? get rate => _rate;
+ bool get isLoading => _isLoading;
bool get hasFailed => _hasFailed;
+ bool get isDisabled => _isDisabled;
String get fiatCode => _fiatCode;
- FiatRateModel() {
- _loadPersisted();
- _startTorStatusCheckTimer();
- }
+ void _startRateFetchTimer() {
+ if (TorSettingsService.sharedInstance.torMode == TorMode.disabled) {
+ _isDisabled = true;
+ log(LogLevel.info, 'Tor is disabled. Not starting rate fetch timer.');
+ return;
+ } else {
+ _isDisabled = false;
+ }
- void _startTorStatusCheckTimer() {
- Timer.periodic(Duration(seconds: 1), (timer) {
- if (TorService.sharedInstance.status == TorConnectionStatus.connected &&
- _rateFetchTimer == null) {
- _startRateFetchTimer();
- _loadRate();
- }
- });
- }
+ if (_rateFetchTimer != null) {
+ _rateFetchTimer?.cancel();
+ }
- void _startRateFetchTimer() {
- _rateFetchTimer = Timer.periodic(Duration(minutes: 10), (timer) async {
- if (TorService.sharedInstance.status == TorConnectionStatus.connected) {
- await _loadRate();
+ _loadRate();
+
+ _rateFetchTimer = Timer.periodic(Duration(minutes: 10), (timer) {
+ if (TorSettingsService.sharedInstance.torMode != TorMode.disabled) {
+ _loadRate();
} else {
+ log(LogLevel.info, 'Tor is disabled. Stopping rate fetch timer.');
timer.cancel();
_rateFetchTimer = null;
}
@@ -45,35 +49,26 @@ class FiatRateModel with ChangeNotifier {
Future<void> _loadPersisted() async {
_fiatCode =
- await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.fiatCurrency,
- ) ??
- 'USD';
-
- _fiatCode = fiatCode;
-
- final rate = await SharedPreferencesService.get<double>(
- SharedPreferencesKeys.fiatRate,
- );
-
- _rate = rate;
+ await SharedPreferencesService.get<String>(SharedPreferencesKeys.fiatCurrency) ?? 'USD';
+ _rate = await SharedPreferencesService.get<double>(SharedPreferencesKeys.fiatRate);
}
Future<void> _persist(double rate) async {
- await SharedPreferencesService.set<double>(
- SharedPreferencesKeys.fiatRate,
- rate,
- );
+ await SharedPreferencesService.set<double>(SharedPreferencesKeys.fiatRate, rate);
}
Future<double> _requestPairRate(String pair) async {
final url = 'https://api.kraken.com/0/public/Ticker?pair=$pair';
- final proxyInfo = TorService.sharedInstance.getProxyInfo();
+ final proxyInfo = await TorSettingsService.sharedInstance.getProxy();
log(LogLevel.info, 'Fetching rate from fiat api:');
log(LogLevel.info, ' url: $url');
log(LogLevel.info, ' proxyInfo: $proxyInfo');
+ if (proxyInfo == null) {
+ throw Exception('Not fetching rate from fiat API because Tor is disabled');
+ }
+
final response = await makeSocksHttpRequest('GET', url, proxyInfo);
if (response.statusCode == 200) {
@@ -88,23 +83,16 @@ class FiatRateModel with ChangeNotifier {
}
Future<void> _loadRate() async {
- final fiatCode =
- await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.fiatCurrency,
- ) ??
- 'USD';
-
- _fiatCode = fiatCode;
-
- final pair1 = indirectPairCurrencies.contains(fiatCode)
- ? 'XXMRZUSD'
- : 'XXMRZ$fiatCode';
+ _fiatCode =
+ await SharedPreferencesService.get<String>(SharedPreferencesKeys.fiatCurrency) ?? 'USD';
- final pair2 = indirectPairCurrencies.contains(fiatCode)
- ? 'USDT$fiatCode'
- : null;
+ final pair1 = indirectPairCurrencies.contains(_fiatCode) ? 'XXMRZUSD' : 'XXMRZ$_fiatCode';
+ final pair2 = indirectPairCurrencies.contains(fiatCode) ? 'USDT$fiatCode' : null;
try {
+ _isLoading = true;
+ notifyListeners();
+
final rates = await Future.wait([
_requestPairRate(pair1),
pair2 != null ? _requestPairRate(pair2) : Future.value(null),
@@ -114,20 +102,18 @@ class FiatRateModel with ChangeNotifier {
_rate = finalRate;
_persist(finalRate);
_hasFailed = false;
- log(LogLevel.info, '$fiatCode rate: $finalRate');
+ log(LogLevel.info, '$_fiatCode rate: $finalRate');
} catch (error) {
log(LogLevel.error, 'Failed to get fiat rate. ${error.toString()}');
_hasFailed = true;
+ } finally {
+ _isLoading = false;
+ notifyListeners();
}
-
- notifyListeners();
}
- Future<void> reset() async {
+ Future<void> startService() async {
await _loadPersisted();
-
- if (TorService.sharedInstance.status == TorConnectionStatus.connected) {
- await _loadRate();
- }
+ _startRateFetchTimer();
}
}
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 168f4f6..d67ff5f 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -13,13 +13,16 @@ import 'package:monero/src/wallet2.dart';
import 'package:http/http.dart' as http;
import 'package:polyseed/polyseed.dart';
import 'package:bip39/bip39.dart' as bip39;
+import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_service.dart';
+import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/util/bip39.dart';
import 'package:skylight_wallet/util/cacert.dart';
import 'package:skylight_wallet/util/formatting.dart';
+import 'package:skylight_wallet/util/get_height_by_date.dart';
import 'package:skylight_wallet/util/height.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/socks_http.dart';
@@ -149,7 +152,6 @@ class WalletModel with ChangeNotifier {
int? _unusedSubaddressIndex;
bool? _unusedSubaddressIndexIsSupported;
String? _desktopWalletPassword;
- Completer<int>? _blockchainHeightCompleter;
Wallet2Wallet? get w2Wallet => _w2Wallet;
bool get hasAttemptedConnection => _hasAttemptedConnection;
@@ -163,7 +165,6 @@ class WalletModel with ChangeNotifier {
bool? get serverSupportsSubaddresses => _serverSupportsSubaddresses;
int? get unusedSubaddressIndex => _unusedSubaddressIndex;
bool? get unusedSubaddressIndexIsSupported => _unusedSubaddressIndexIsSupported;
- Completer<int>? get blockchainHeightCompleter => _blockchainHeightCompleter;
WalletModel() {
_startTimers();
@@ -356,8 +357,10 @@ class WalletModel with ChangeNotifier {
String? torProxyPort;
if (_connectionUseTor) {
- await TorService.sharedInstance.waitUntilConnected();
- torProxyPort = TorService.sharedInstance.getProxyInfo().port.toString();
+ final proxyInfo = await TorSettingsService.sharedInstance.getProxy();
+ if (proxyInfo != null) {
+ torProxyPort = proxyInfo.port.toString();
+ }
}
final proxyPort = torProxyPort ?? _connectionProxyPort;
@@ -587,24 +590,21 @@ class WalletModel with ChangeNotifier {
log(LogLevel.info, 'Wallet refresh methods completed successfully');
}
- Future<String> create() async {
+ Future<(String, int)> create() async {
// ignore: deprecated_member_use
final polyseed = await Isolate.run(() => monero.Wallet_createPolyseed());
log(LogLevel.info, 'Wallet_createPolyseed completed');
- final isCached = _blockchainHeightCompleter != null;
- final currentHeight = isCached
- ? await _blockchainHeightCompleter!.future
- : await getCurrentBlockchainHeight();
- log(LogLevel.info, 'Using blockchain height: $currentHeight (cached: $isCached)');
+ final restoreHeight = getHeightByDate(date: DateTime.now());
+ log(LogLevel.info, 'Using blockchain height: $restoreHeight');
- await restoreFromMnemonic(polyseed, currentHeight);
+ await restoreFromMnemonic(polyseed, restoreHeight);
await SharedPreferencesService.set<int>(
SharedPreferencesKeys.walletRestoreHeight,
- currentHeight,
+ restoreHeight,
);
refresh().then((_) => connectToDaemon().then((_) => store()));
- return polyseed;
+ return (polyseed, restoreHeight);
}
Future<int> getRestoreHeight() async {
@@ -822,26 +822,6 @@ class WalletModel with ChangeNotifier {
await SharedPreferencesService.remove(SharedPreferencesKeys.contacts);
await SharedPreferencesService.remove(SharedPreferencesKeys.unusedSubaddressIndex);
await SharedPreferencesService.remove(SharedPreferencesKeys.unusedSubaddressIndexIsSupported);
-
- // Fetch and cache blockchain height in the background for future wallet creation
- fetchAndCacheBlockchainHeight();
- }
-
- void fetchAndCacheBlockchainHeight() async {
- _blockchainHeightCompleter = Completer<int>();
-
- try {
- final height = await getCurrentBlockchainHeight();
- log(LogLevel.info, 'Cached blockchain height: $height');
-
- // another call might have already completed the completer
- if (_blockchainHeightCompleter?.isCompleted == false) {
- _blockchainHeightCompleter!.complete(height);
- }
- } catch (e) {
- log(LogLevel.error, 'Failed to fetch and cache blockchain height: $e');
- _blockchainHeightCompleter!.completeError(e);
- }
}
Future<bool> hasExistingWallet() async {
diff --git a/lib/screens/connection_setup.dart b/lib/screens/connection_setup.dart
index 69aac3f..d150625 100644
--- a/lib/screens/connection_setup.dart
+++ b/lib/screens/connection_setup.dart
@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:skylight_wallet/services/tor_settings_service.dart';
+import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/socks_http.dart';
import 'package:provider/provider.dart';
@@ -56,7 +58,7 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
_useSsl = conn.useSsl;
});
- if (conn.useTor) {
+ if (conn.useTor && TorSettingsService.sharedInstance.torMode == TorMode.builtIn) {
_pollTorStatus();
}
}
@@ -111,6 +113,11 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
}
void setUseTor(bool? value) {
+ if (TorSettingsService.sharedInstance.torMode == TorMode.disabled) {
+ log(LogLevel.info, 'Tor is disabled. Not setting useTor to true.');
+ value = false;
+ }
+
setState(() {
_useTor = value ?? false;
_useSsl = value == true ? false : _useSsl;
@@ -120,7 +127,8 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
if (value == true) {
_customProxyPortController.text = '';
- if (TorService.sharedInstance.status != TorConnectionStatus.connected) {
+ if (TorSettingsService.sharedInstance.torMode == TorMode.builtIn &&
+ TorService.sharedInstance.status != TorConnectionStatus.connected) {
_pollTorStatus();
}
}
@@ -163,12 +171,6 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
}
}
- if (_useTor) {
- await TorService.sharedInstance.start();
- await TorService.sharedInstance.waitUntilConnected();
- torProxyPort = TorService.sharedInstance.getProxyInfo().port.toString();
- }
-
String proxyPort = torProxyPort != '' ? torProxyPort : customProxyPort;
setState(() {
@@ -180,11 +182,26 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
try {
if (_useTor) {
- final proxyInfo = TorService.sharedInstance.getProxyInfo();
+ if (!mounted) {
+ return;
+ }
+
+ final torSettings = TorSettingsService.sharedInstance;
+
+ if (torSettings.torMode == TorMode.disabled) {
+ // show error toast
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text("Tor is disabled. Please go back and enable it.")));
+ return;
+ }
+
+ final proxyInfo = await torSettings.getProxy();
+
final response = await makeSocksHttpRequest(
'POST',
url,
- proxyInfo,
+ proxyInfo!,
).timeout(Duration(seconds: 10));
setState(() {
@@ -255,8 +272,10 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+ final torMode = TorSettingsService.sharedInstance.torMode;
return Scaffold(
+ appBar: AppBar(title: Text('Skylight Monero Wallet')),
body: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 500),
@@ -312,7 +331,7 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
CheckboxListTile(
title: Text(i18n.connectionSetupUseTorLabel),
value: _useTor,
- onChanged: !_useSsl ? setUseTor : null,
+ onChanged: _useSsl || torMode == TorMode.disabled ? null : setUseTor,
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
@@ -323,11 +342,22 @@ class _ConnectionSetupScreenState extends State<ConnectionSetupScreen> {
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
+ if (_useTor)
+ Text(
+ torMode == TorMode.builtIn
+ ? i18n.connectionSetupUsingInternalTor
+ : i18n.connectionSetupUsingExternalTor(
+ '127.0.0.1:${TorSettingsService.sharedInstance.socksPort}',
+ ),
+ style: TextStyle(color: Colors.purple, fontStyle: FontStyle.italic),
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [
- if (_useTor && _torStatus != TorConnectionStatus.connected)
+ if (_useTor &&
+ torMode == TorMode.builtIn &&
+ _torStatus != TorConnectionStatus.connected)
Row(
mainAxisSize: MainAxisSize.min,
children: [
diff --git a/lib/screens/generate_seed.dart b/lib/screens/generate_seed.dart
index 292b5a8..958bfb2 100644
--- a/lib/screens/generate_seed.dart
+++ b/lib/screens/generate_seed.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/screens/create_wallet.dart';
import 'package:skylight_wallet/util/logging.dart';
@@ -22,18 +23,18 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> {
void initState() {
super.initState();
_createWallet();
- _loadCurrentHeight();
}
Future<void> _createWallet() async {
final wallet = Provider.of<WalletModel>(context, listen: false);
try {
- final seed = await wallet.create();
+ final (seed, restoreHeight) = await wallet.create();
wallet.load();
setState(() {
_seed = seed.split(' ');
+ _restoreHeight = restoreHeight;
});
} catch (error) {
var errorMsg = 'Sorry, something went wrong.';
@@ -54,34 +55,14 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> {
}
}
- Future<void> _loadCurrentHeight() async {
- try {
- // get current height from cache
- final height = await Provider.of<WalletModel>(
- context,
- listen: false,
- ).blockchainHeightCompleter!.future;
-
- if (mounted) {
- setState(() {
- _restoreHeight = height;
- });
- }
- } catch (error) {
- var errorMsg = 'Sorry, something went wrong.';
-
- if (error.toString().contains('failedToLoadHeight')) {
- errorMsg = 'Check your internet connection.';
- }
-
- if (mounted) {
- Navigator.pushNamed(
- context,
- '/create_wallet',
- arguments: CreateWalletScreenArgs(toastMessage: errorMsg),
- );
- }
- }
+ void _continue() {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/lws_details',
+ (Route<dynamic> route) => false,
+ arguments: _restoreHeight,
+ );
+ Provider.of<FiatRateModel>(context, listen: false).startService();
}
@override
@@ -120,12 +101,7 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> {
),
if (_seed.isNotEmpty && _restoreHeight > 0)
FilledButton(
- onPressed: () => Navigator.pushNamedAndRemoveUntil(
- context,
- '/lws_details',
- (Route<dynamic> route) => false,
- arguments: _restoreHeight,
- ),
+ onPressed: _continue,
child: Text(i18n.generateSeedContinueButton),
),
],
diff --git a/lib/screens/restore_wallet.dart b/lib/screens/restore_wallet.dart
index bf6a44a..ba202bd 100644
--- a/lib/screens/restore_wallet.dart
+++ b/lib/screens/restore_wallet.dart
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:polyseed/polyseed.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/util/get_height_by_date.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
@@ -102,6 +103,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
wallet.load();
if (mounted) {
+ Provider.of<FiatRateModel>(context, listen: false).startService();
Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
}
}
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index 99390ae..f9e1717 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -9,6 +9,7 @@ import 'package:local_auth/local_auth.dart';
import 'package:skylight_wallet/consts.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
+import 'package:skylight_wallet/widgets/tor_settings_form.dart';
import 'package:skylight_wallet/models/language_model.dart';
import 'package:skylight_wallet/models/theme_model.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
@@ -143,13 +144,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
});
await SharedPreferencesService.set<String>(SharedPreferencesKeys.fiatCurrency, value);
-
- // Clear rate
await SharedPreferencesService.remove(SharedPreferencesKeys.fiatRate);
if (mounted) {
final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
- await fiatRate.reset();
+ await fiatRate.startService();
}
}
@@ -287,6 +286,32 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
+ void _showTorSettingsDialog() {
+ final i18n = AppLocalizations.of(context)!;
+ final screenWidth = MediaQuery.of(context).size.width;
+ final dialogWidth = screenWidth.clamp(0.0, 500.0);
+
+ void onSaved() {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+ wallet.load();
+
+ final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
+ fiatRate.startService();
+
+ Navigator.pop(context);
+ }
+
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ constraints: BoxConstraints.tightFor(width: dialogWidth),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
+ title: Text(i18n.torSettingsTitle),
+ content: TorSettingsForm(saveButtonLabel: i18n.torSettingsSaveButton, onSaved: onSaved),
+ ),
+ );
+ }
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
@@ -348,6 +373,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
],
),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(i18n.settingsTorSettingsLabel, style: TextStyle(fontSize: 18)),
+ TextButton.icon(
+ onPressed: _showTorSettingsDialog,
+ icon: Icon(Icons.security),
+ label: Text(i18n.settingsLwsViewKeysButton),
+ ),
+ ],
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
diff --git a/lib/screens/tor_info.dart b/lib/screens/tor_info.dart
new file mode 100644
index 0000000..590adb1
--- /dev/null
+++ b/lib/screens/tor_info.dart
@@ -0,0 +1,46 @@
+import 'package:flutter/material.dart';
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+
+class TorInfoScreen extends StatelessWidget {
+ const TorInfoScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+
+ return Scaffold(
+ appBar: AppBar(title: Text('Skylight Monero Wallet')),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ Text(i18n.torInfoTitle, style: Theme.of(context).textTheme.headlineMedium),
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 40),
+ child: Text(
+ i18n.torInfoDescription,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ ),
+ Row(
+ spacing: 20,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ OutlinedButton(
+ onPressed: () => Navigator.pushNamed(context, '/tor_settings'),
+ child: Text(i18n.torInfoConfigureButton),
+ ),
+ FilledButton(
+ onPressed: () => Navigator.pushNamed(context, '/connection_setup'),
+ child: Text(i18n.torInfoContinueButton),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/tor_settings.dart b/lib/screens/tor_settings.dart
new file mode 100644
index 0000000..5dc0831
--- /dev/null
+++ b/lib/screens/tor_settings.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/widgets/tor_settings_form.dart';
+
+class TorSettingsScreen extends StatelessWidget {
+ const TorSettingsScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+
+ return Scaffold(
+ appBar: AppBar(title: Text('Skylight Monero Wallet')),
+ body: Center(
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 500),
+ child: Padding(
+ padding: EdgeInsets.all(20),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ Text(i18n.torSettingsTitle, style: Theme.of(context).textTheme.headlineMedium),
+ TorSettingsForm(
+ saveButtonLabel: i18n.continueText,
+ onSaved: () => Navigator.pushNamed(context, '/connection_setup'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/screens/unlock.dart b/lib/screens/unlock.dart
index c1e81ba..6887132 100644
--- a/lib/screens/unlock.dart
+++ b/lib/screens/unlock.dart
@@ -77,7 +77,7 @@ class _UnlockScreenState extends State<UnlockScreen> {
await wallet.openExisting(desktopWalletPassword: enteredPassword);
await wallet.loadPersistedConnection();
- await wallet.load();
+ wallet.load();
if (mounted) {
Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index 13a49d3..c3ae97a 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -85,16 +85,18 @@ class _TransactionListItemState extends State<_TransactionListItem> {
),
),
Column(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisAlignment: widget.fiatRate.isDisabled
+ ? MainAxisAlignment.center
+ : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MoneroAmount(amount: widget.tx.amount, maxFontSize: 16),
- if (amountFiat == null)
+ if (amountFiat == null && !widget.fiatRate.isDisabled)
Skeletonizer(
enabled: true,
child: Text('Potato', style: TextStyle(fontSize: 14)),
),
- if (amountFiat is double)
+ if (amountFiat is double && !widget.fiatRate.isDisabled)
FiatAmount(prefix: widget.fiatSymbol, amount: amountFiat, maxFontSize: 14),
],
),
@@ -194,6 +196,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
WalletModel wallet,
StatusIconStatus lwsConnectionIconStatus,
StatusIconStatus fiatApiIconStatus,
+ FiatRateModel fiatRate,
) {
return Column(
spacing: 10,
@@ -220,10 +223,11 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
status: lwsConnectionIconStatus,
child: SvgPicture.asset('assets/icons/tor.svg', width: 22, height: 22),
),
- StatusIcon(
- status: fiatApiIconStatus,
- child: SvgPicture.asset('assets/icons/tor.svg', width: 22, height: 22),
- ),
+ if (!fiatRate.isDisabled)
+ StatusIcon(
+ status: fiatApiIconStatus,
+ child: SvgPicture.asset('assets/icons/tor.svg', width: 22, height: 22),
+ ),
],
);
}
@@ -235,6 +239,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
double? unlockedBalanceFiat,
double lockedBalance,
String fiatSymbol,
+ FiatRateModel fiatRate,
) {
return Column(
children: [
@@ -253,9 +258,9 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
- if (unlockedBalanceFiat == null)
+ if (unlockedBalanceFiat == null && !fiatRate.isDisabled)
Skeletonizer(enabled: true, child: Text('Potato', style: TextStyle(fontSize: 18))),
- if (unlockedBalanceFiat is double)
+ if (unlockedBalanceFiat is double && !fiatRate.isDisabled)
FiatAmount(prefix: fiatSymbol, amount: unlockedBalanceFiat, maxFontSize: 18),
],
);
@@ -356,6 +361,8 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
!fiatRate.hasFailed &&
TorService.sharedInstance.status == TorConnectionStatus.connected) {
fiatApiIconStatus = StatusIconStatus.complete;
+ } else if (fiatRate.isLoading) {
+ fiatApiIconStatus = StatusIconStatus.loading;
} else if (fiatRate.hasFailed) {
fiatApiIconStatus = StatusIconStatus.fail;
}
@@ -465,7 +472,12 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
Positioned(
top: 0,
right: 0,
- child: _buildStatusIcons(wallet, lwsConnectionIconStatus, fiatApiIconStatus),
+ child: _buildStatusIcons(
+ wallet,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ fiatRate,
+ ),
),
Center(
child: _buildBalanceDisplay(
@@ -475,6 +487,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
unlockedBalanceFiat,
lockedBalance,
fiatSymbol,
+ fiatRate,
),
),
],
@@ -524,7 +537,12 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
Positioned(
top: 0,
left: 0,
- child: _buildStatusIcons(wallet, lwsConnectionIconStatus, fiatApiIconStatus),
+ child: _buildStatusIcons(
+ wallet,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ fiatRate,
+ ),
),
Column(
children: [
@@ -535,6 +553,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
unlockedBalanceFiat,
lockedBalance,
fiatSymbol,
+ fiatRate,
),
SizedBox(height: 10),
_buildActionButtons(i18n),
diff --git a/lib/screens/welcome.dart b/lib/screens/welcome.dart
index 6db21be..952a709 100644
--- a/lib/screens/welcome.dart
+++ b/lib/screens/welcome.dart
@@ -53,7 +53,7 @@ class _WalletHomeScreenState extends State<WelcomeScreen> {
),
),
FilledButton(
- onPressed: () => Navigator.pushNamed(context, '/connection_setup'),
+ onPressed: () => Navigator.pushNamed(context, '/tor_info'),
child: Text(i18n.welcomeGetStarted),
),
],
diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart
index 1eb2d76..ec2f65f 100644
--- a/lib/services/shared_preferences_service.dart
+++ b/lib/services/shared_preferences_service.dart
@@ -18,8 +18,9 @@ class SharedPreferencesKeys {
static const String pendingOutgoingTxs = 'pendingOutgoingTxs';
static const String contacts = 'contacts';
static const String unusedSubaddressIndex = 'unusedSubaddressIndex';
- static const String unusedSubaddressIndexIsSupported =
- 'unusedSubaddressIndexIsSupported';
+ static const String unusedSubaddressIndexIsSupported = 'unusedSubaddressIndexIsSupported';
+ static const String torMode = 'torMode';
+ static const String torSocksPort = 'torSocksPort';
}
class SharedPreferencesService {
diff --git a/lib/services/tor_settings_service.dart b/lib/services/tor_settings_service.dart
new file mode 100644
index 0000000..135d1bd
--- /dev/null
+++ b/lib/services/tor_settings_service.dart
@@ -0,0 +1,83 @@
+import 'dart:io';
+
+import 'package:skylight_wallet/services/shared_preferences_service.dart';
+import 'package:skylight_wallet/services/tor_service.dart';
+
+enum TorMode { builtIn, external, disabled }
+
+class TorSettingsService {
+ static final TorSettingsService sharedInstance = TorSettingsService._();
+
+ TorMode _torMode = TorMode.builtIn;
+ String _socksPort = '9150';
+
+ TorMode get torMode => _torMode;
+ String get socksPort => _socksPort;
+
+ TorSettingsService._();
+
+ Future<void> loadSettings() async {
+ final String? torModeString = await SharedPreferencesService.get<String>(
+ SharedPreferencesKeys.torMode,
+ );
+ final String? socksPortString = await SharedPreferencesService.get<String>(
+ SharedPreferencesKeys.torSocksPort,
+ );
+
+ if (torModeString != null) {
+ _torMode = _torModeFromString(torModeString);
+ }
+
+ if (socksPortString != null) {
+ _socksPort = socksPortString;
+ }
+ }
+
+ Future<void> save({required TorMode torMode, String? socksPort}) async {
+ _torMode = torMode;
+ await SharedPreferencesService.set<String>(
+ SharedPreferencesKeys.torMode,
+ _torModeToString(torMode),
+ );
+
+ if (socksPort != null) {
+ _socksPort = socksPort;
+ await SharedPreferencesService.set<String>(SharedPreferencesKeys.torSocksPort, socksPort);
+ }
+ }
+
+ Future<({InternetAddress host, int port})?> getProxy() async {
+ if (_torMode == TorMode.builtIn) {
+ await TorService.sharedInstance.waitUntilConnected();
+ return TorService.sharedInstance.getProxyInfo();
+ } else if (_torMode == TorMode.external) {
+ return (host: InternetAddress.loopbackIPv4, port: int.parse(_socksPort));
+ } else {
+ return null;
+ }
+ }
+
+ String _torModeToString(TorMode mode) {
+ switch (mode) {
+ case TorMode.builtIn:
+ return 'builtIn';
+ case TorMode.external:
+ return 'external';
+ case TorMode.disabled:
+ return 'disabled';
+ }
+ }
+
+ TorMode _torModeFromString(String modeString) {
+ switch (modeString) {
+ case 'builtIn':
+ return TorMode.builtIn;
+ case 'external':
+ return TorMode.external;
+ case 'disabled':
+ return TorMode.disabled;
+ default:
+ return TorMode.builtIn;
+ }
+ }
+}
diff --git a/lib/util/get_height_by_date.dart b/lib/util/get_height_by_date.dart
index 6e3a2d5..281ea81 100644
--- a/lib/util/get_height_by_date.dart
+++ b/lib/util/get_height_by_date.dart
@@ -122,6 +122,30 @@ final dates = {
"2023-10": 2985937,
"2023-11": 3008178,
"2023-12": 3029759,
+ "2024-1": 3051991,
+ "2024-2": 3074316,
+ "2024-3": 3095123,
+ "2024-4": 3117427,
+ "2024-5": 3139022,
+ "2024-6": 3161279,
+ "2024-7": 3182945,
+ "2024-8": 3205207,
+ "2024-9": 3227566,
+ "2024-10": 3249124,
+ "2024-11": 3271454,
+ "2024-12": 3293087,
+ "2025-1": 3315383,
+ "2025-2": 3337734,
+ "2025-3": 3357843,
+ "2025-4": 3380178,
+ "2025-5": 3401714,
+ "2025-6": 3424052,
+ "2025-7": 3445678,
+ "2025-8": 3467960,
+ "2025-9": 3490175,
+ "2025-10": 3511714,
+ "2025-11": 3533988,
+ "2025-12": 3555615,
};
int getHeightByDate({required DateTime date}) {
diff --git a/lib/util/height.dart b/lib/util/height.dart
index 410128a..b116a5e 100644
--- a/lib/util/height.dart
+++ b/lib/util/height.dart
@@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math';
+import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/socks_http.dart';
-import 'package:skylight_wallet/services/tor_service.dart';
Future<int> getCurrentBlockchainHeight() async {
final urls = [
@@ -15,8 +15,12 @@ Future<int> getCurrentBlockchainHeight() async {
urls.shuffle(Random.secure());
- await TorService.sharedInstance.waitUntilConnected();
- final proxyInfo = TorService.sharedInstance.getProxyInfo();
+ final proxyInfo = await TorSettingsService.sharedInstance.getProxy();
+
+ if (proxyInfo == null) {
+ // FIXME
+ return 0;
+ }
for (String url in urls) {
try {
diff --git a/lib/util/socks_http.dart b/lib/util/socks_http.dart
index 52b5cf4..47cfb27 100644
--- a/lib/util/socks_http.dart
+++ b/lib/util/socks_http.dart
@@ -71,9 +71,7 @@ ParsedHttpResponse parseHttpResponse(String rawResponse) {
final separatorIndex = rawResponse.indexOf(separator);
if (separatorIndex == -1) {
- throw FormatException(
- 'Invalid HTTP response: No header/body separator found.',
- );
+ throw FormatException('Invalid HTTP response: No header/body separator found.');
}
final headersPart = rawResponse.substring(0, separatorIndex);
diff --git a/lib/widgets/tor_settings_form.dart b/lib/widgets/tor_settings_form.dart
new file mode 100644
index 0000000..691605f
--- /dev/null
+++ b/lib/widgets/tor_settings_form.dart
@@ -0,0 +1,170 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/services/tor_settings_service.dart';
+import 'package:skylight_wallet/util/socks_http.dart';
+
+/// Shared form widget used by both TorSettingsScreen and the Tor settings dialog
+class TorSettingsForm extends StatefulWidget {
+ final String saveButtonLabel;
+ final VoidCallback onSaved;
+
+ const TorSettingsForm({super.key, required this.saveButtonLabel, required this.onSaved});
+
+ @override
+ State<TorSettingsForm> createState() => _TorSettingsFormState();
+}
+
+class _TorSettingsFormState extends State<TorSettingsForm> {
+ TorMode _selectedMode = TorMode.builtIn;
+ final TextEditingController _socksPortController = TextEditingController();
+
+ bool _isTestingConnection = false;
+ bool _hasTested = false;
+ bool _connectionSuccess = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadSettings();
+ }
+
+ void _loadSettings() {
+ final torSettings = TorSettingsService.sharedInstance;
+ setState(() {
+ _selectedMode = torSettings.torMode;
+ _socksPortController.text = torSettings.socksPort;
+ });
+ }
+
+ Future<void> _saveSettings() async {
+ final torSettings = TorSettingsService.sharedInstance;
+ await torSettings.save(torMode: _selectedMode, socksPort: _socksPortController.text);
+ }
+
+ void _onSavePressed() async {
+ await _saveSettings();
+ widget.onSaved();
+ }
+
+ Future<void> _testConnection() async {
+ if (_selectedMode != TorMode.external) {
+ return;
+ }
+
+ setState(() {
+ _isTestingConnection = true;
+ _hasTested = true;
+ _connectionSuccess = false;
+ });
+
+ try {
+ final port = int.tryParse(_socksPortController.text) ?? 9050;
+ final proxyInfo = (host: InternetAddress.loopbackIPv4, port: port);
+
+ final response = await makeSocksHttpRequest(
+ 'GET',
+ 'https://check.torproject.org/api/ip',
+ proxyInfo,
+ ).timeout(Duration(seconds: 15));
+
+ // Check if the response indicates we're connected through Tor
+ final isTor = response.jsonBody != null && response.jsonBody['IsTor'] == true;
+
+ setState(() {
+ _connectionSuccess = response.statusCode == HttpStatus.ok && isTor;
+ });
+ } catch (e) {
+ setState(() {
+ _connectionSuccess = false;
+ });
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isTestingConnection = false;
+ });
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _socksPortController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 16,
+ children: [
+ DropdownButtonFormField<TorMode>(
+ initialValue: _selectedMode,
+ decoration: InputDecoration(
+ labelText: i18n.torSettingsModeLabel,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ ),
+ items: [
+ DropdownMenuItem(value: TorMode.builtIn, child: Text(i18n.torSettingsModeBuiltIn)),
+ DropdownMenuItem(value: TorMode.external, child: Text(i18n.torSettingsModeExternal)),
+ DropdownMenuItem(value: TorMode.disabled, child: Text(i18n.torSettingsModeDisabled)),
+ ],
+ onChanged: (TorMode? newValue) {
+ if (newValue != null) {
+ setState(() {
+ _selectedMode = newValue;
+ _hasTested = false;
+ });
+ }
+ },
+ ),
+ if (_selectedMode == TorMode.external)
+ TextFormField(
+ controller: _socksPortController,
+ decoration: InputDecoration(
+ labelText: i18n.torSettingsSocksPortLabel,
+ hintText: i18n.torSettingsSocksPortHint,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ suffixIcon: _hasTested && !_isTestingConnection
+ ? Icon(_connectionSuccess ? Icons.check : Icons.cancel_outlined)
+ : null,
+ suffixIconColor: _connectionSuccess ? Colors.teal : Colors.red,
+ ),
+ keyboardType: TextInputType.number,
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+ onChanged: (_) {
+ setState(() {
+ _hasTested = false;
+ });
+ },
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 10,
+ children: [
+ if (_selectedMode == TorMode.external)
+ TextButton.icon(
+ label: Text(i18n.torSettingsTestConnectionButton),
+ onPressed: _isTestingConnection ? null : _testConnection,
+ icon: _isTestingConnection
+ ? SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : Icon(Icons.network_check),
+ ),
+ if (_selectedMode != TorMode.external ||
+ (_connectionSuccess && _hasTested && !_isTestingConnection))
+ FilledButton(onPressed: _onSavePressed, child: Text(widget.saveButtonLabel)),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/scripts/find_monthly_heights.sh b/scripts/find_monthly_heights.sh
new file mode 100755
index 0000000..c285465
--- /dev/null
+++ b/scripts/find_monthly_heights.sh
@@ -0,0 +1,149 @@
+#!/bin/bash
+
+# Script to find the first block height of each month and update get_height_by_date.dart
+# Usage: ./find_monthly_heights.sh
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DART_FILE="$SCRIPT_DIR/../lib/util/get_height_by_date.dart"
+RPC_URL="http://localhost:18081/json_rpc"
+
+# Check for jq dependency
+if ! command -v jq &> /dev/null; then
+ echo "Error: jq is required but not installed."
+ echo "Install it with: sudo pacman -S jq (Arch) or sudo apt install jq (Debian/Ubuntu)"
+ exit 1
+fi
+
+# Check if Dart file exists
+if [ ! -f "$DART_FILE" ]; then
+ echo "Error: Dart file not found at $DART_FILE"
+ exit 1
+fi
+
+# Extract the last entry from the dates map
+# Format in file: "2023-12": 3029759,
+LAST_LINE=$(grep -E '^\s*"[0-9]{4}-[0-9]{1,2}":\s*[0-9]+,' "$DART_FILE" | tail -1)
+LAST_DATE=$(echo "$LAST_LINE" | sed -E 's/.*"([0-9]{4}-[0-9]{1,2})".*/\1/')
+LAST_HEIGHT=$(echo "$LAST_LINE" | sed -E 's/.*:\s*([0-9]+).*/\1/')
+
+echo "Last entry in Dart file: $LAST_DATE -> height $LAST_HEIGHT"
+
+# Get block header by height
+get_block_timestamp() {
+ local height=$1
+ local response
+ response=$(curl -s "$RPC_URL" -d "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"get_block_header_by_height\",\"params\":{\"height\":$height}}")
+
+ # Check for error
+ if echo "$response" | grep -q '"error"'; then
+ echo "ERROR"
+ return
+ fi
+
+ # Extract timestamp using jq
+ echo "$response" | jq -r '.result.block_header.timestamp'
+}
+
+# Convert timestamp to year and month (no leading zero on month)
+get_year_month() {
+ local timestamp=$1
+ local year=$(date -u -d "@$timestamp" "+%Y")
+ local month=$(date -u -d "@$timestamp" "+%-m") # %-m removes leading zero
+ echo "$year-$month"
+}
+
+# Get day of month from timestamp
+get_day() {
+ local timestamp=$1
+ date -u -d "@$timestamp" "+%-d"
+}
+
+# Convert timestamp to human-readable date
+get_readable_date() {
+ local timestamp=$1
+ date -u -d "@$timestamp" "+%Y-%m-%d %H:%M:%S UTC"
+}
+
+# Check if a month already exists in the file
+month_exists() {
+ local date=$1
+ grep -q "\"$date\":" "$DART_FILE"
+}
+
+# Insert a new entry into the Dart file after the current last entry
+insert_entry() {
+ local new_date=$1
+ local new_height=$2
+
+ # Skip if month already exists
+ if month_exists "$new_date"; then
+ echo " -> Skipped (already exists in file)"
+ return 0
+ fi
+
+ local new_entry=" \"$new_date\": $new_height,"
+
+ # Find the line number of the last date entry in the file
+ local line_num=$(grep -nE '^\s*"[0-9]{4}-[0-9]{1,2}":\s*[0-9]+,' "$DART_FILE" | tail -1 | cut -d: -f1)
+
+ if [ -z "$line_num" ]; then
+ echo "Error: Could not find any date entries in the Dart file"
+ return 1
+ fi
+
+ # Insert new entry after that line
+ head -n "$line_num" "$DART_FILE" > "$DART_FILE.tmp"
+ echo "$new_entry" >> "$DART_FILE.tmp"
+ tail -n +"$((line_num + 1))" "$DART_FILE" >> "$DART_FILE.tmp"
+ mv "$DART_FILE.tmp" "$DART_FILE"
+
+ echo " -> Written to file"
+}
+
+START_HEIGHT=$((LAST_HEIGHT + 1))
+echo "Starting from height: $START_HEIGHT"
+echo "Looking for months after: $LAST_DATE"
+echo "---"
+
+current_height=$START_HEIGHT
+current_month="$LAST_DATE"
+
+while true; do
+ # Show progress on same line
+ printf "\rProcessing height: %d" "$current_height"
+
+ timestamp=$(get_block_timestamp "$current_height")
+
+ if [ "$timestamp" == "ERROR" ] || [ "$timestamp" == "null" ] || [ -z "$timestamp" ]; then
+ echo "" # New line after progress
+ echo "Reached end of blockchain or error at height $current_height"
+ break
+ fi
+
+ year_month=$(get_year_month "$timestamp")
+ day=$(get_day "$timestamp")
+
+ # If month changed, record this block as first of new month
+ if [ "$year_month" != "$current_month" ]; then
+ readable_date=$(get_readable_date "$timestamp")
+ echo "" # New line after progress
+ echo "First block of $year_month: height $current_height ($readable_date)"
+
+ # Write to file immediately
+ insert_entry "$year_month" "$current_height"
+
+ current_month="$year_month"
+
+ ((current_height++))
+ elif [ "$day" -lt 28 ]; then
+ # Safe to skip ahead by 1000 blocks when before 28th of month
+ ((current_height += 1000))
+ else
+ ((current_height++))
+ fi
+done
+
+echo "---"
+echo "Done!"
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.