What changed, and why it matters
This commit fixes two practical bugs in a Monero wallet app. First, it stops comparing money amounts using floating-point 'double' numbers, which can round incorrectly and let a user think they are sending their full balance when they are not (or vice versa). It now compares exact integer 'piconero' units. Second, it makes the Tor connection logic respect the user's Tor setting: if Tor is set to 'external' or 'disabled', the app no longer blindly tries to start its built-in Tor and waits two minutes. These are correctness/reliability fixes rather than obvious remote-hack vulnerabilities, but they could affect privacy and funds handling.
Review the decimal conversion helper decimalToBaseUnits for overflow/underflow and locale handling, and verify that wallet.unlockedBalanceBaseUnits is populated atomically with the amount field. For the Tor change, confirm that external/disabled modes correctly propagate to all network paths and that returning true for external mode does not bypass proxy usability checks elsewhere.
Security signals we found
Floating-point monetary comparison replaced with exact base-unit integer comparison
Tor connection startup now gated by user-configured Tor mode, preventing unconditional built-in Tor bootstrap
Removal of internal design-document markers (D10/D24/D25/D30) from comments
Evidence from the diff
The patch removes D-code references (D10/D24/D25/D30) from comments and makes functional changes in two areas. In lib/screens/send.dart, amount validation and sweep-all detection now use decimalToBaseUnits() to produce BigInt piconero values compared against wallet.unlockedBalanceBaseUnits, replacing double.tryParse comparisons. This fixes precision issues with 12-decimal Monero amounts. In lib/wallet_core_glue.dart, _ensureTorConnected() now loads TorSettingsService, returns true for external mode and false for disabled mode without starting the built-in Tor, and only starts/waits for built-in Tor when torMode == TorMode.builtIn. Other changes are comment-only re-export cleanups.
Changed components
lib/screens/send.dartlib/wallet_core_glue.dartlib/models/monero_wallet_adapter.dartlib/util/logging.dartlib/util/secure_clipboard.dartlib/widgets/tx_details.dartlib/widgets/ui/ui.dartInspect captured patch +30 / −11
### lib/models/monero_wallet_adapter.dart
@@ -10,7 +10,7 @@ import 'package:wallet_monero/wallet_monero.dart' show MoneroWallet;
const _moneroDecimals = 12;
-/// Whether a bare `host:port` [address] gets a secure transport (D30).
+/// Whether a bare `host:port` [address] gets a secure transport.
///
/// wallet-core no longer stores a `useSsl` flag; the scheme is derived from the
/// host — https for a routable one, plaintext for an onion or local one. This
### lib/screens/send.dart
@@ -315,7 +315,7 @@ class _SendScreenState extends State<SendScreen> {
return false;
}
- if (amount > (wallet.unlockedBalance ?? 0)) {
+ if (_amountUnits() > (wallet.unlockedBalanceBaseUnits ?? BigInt.zero)) {
if (setErrors) {
setState(() {
_amountError = i18n.sendInsufficientBalanceError;
@@ -602,17 +602,28 @@ class _SendScreenState extends State<SendScreen> {
await _revalidate();
}
+ /// The typed amount in piconero, or zero when the field is not a number.
+ BigInt _amountUnits() {
+ try {
+ return decimalToBaseUnits(_amountController.text, _xmrDecimals);
+ } on FormatException {
+ return BigInt.zero;
+ }
+ }
+
Future<void> _onAmountChanged() async {
final wallet = appWalletOf(context);
- final amount = double.tryParse(_amountController.text) ?? 0;
+ // Base units for the same reason as in `_validateForm`: `==` between two
+ // differently-rounded doubles decided whether this is a sweep.
+ final isFullBalance = _amountUnits() == wallet.unlockedBalanceBaseUnits;
- if (amount == wallet.unlockedBalance! && !_isSweepAll) {
+ if (isFullBalance && !_isSweepAll) {
setState(() {
_isSweepAll = true;
});
}
- if (amount != wallet.unlockedBalance! && _isSweepAll) {
+ if (!isFullBalance && _isSweepAll) {
setState(() {
_isSweepAll = false;
});
### lib/util/logging.dart
@@ -1,4 +1,4 @@
-// The whole logger lives in wallet-core (wallet_infra, D25): the log() API +
+// The whole logger lives in wallet-core (wallet_infra): the log() API +
// formatter, the verbose gate, the file sink (FileLogSink), log rotation
// (cleanOldLogFiles), and file listing/export. The app only installs the sink
// (console + file) in wallet_core_glue.dart.
### lib/util/secure_clipboard.dart
@@ -1,5 +1,5 @@
// SecureClipboard lives in wallet-core (`wallet_infra`) with a fixed, app-neutral
-// MethodChannel name (`org.magicgrants.wallet/secure_clipboard`, per D10). Kept
+// MethodChannel name (`org.magicgrants.wallet/secure_clipboard`). Kept
// under the same import path so call sites are unchanged; the native handler is
// registered under that name in MainActivity.kt / AppDelegate.swift.
export 'package:wallet_infra/wallet_infra.dart' show SecureClipboard;
### lib/wallet_core_glue.dart
@@ -62,7 +62,7 @@ void installWalletCore() {
FiatRates.install(getTorProxy: TorSettingsService.sharedInstance.getProxy);
- // The whole logger lives in wallet-core now (D25): console + file sinks fan out
+ // The whole logger lives in wallet-core now: console + file sinks fan out
// from one installed sink; the file sink is verbose-gated internally.
wcore.WalletLog.sink = wcore.CompositeLogSink([
const wcore.DebugPrintLogSink(),
@@ -99,6 +99,14 @@ void installWalletCore() {
/// wallet connects through. (Background open + the node/Tor gate now live inside
/// `wallet_background`.)
Future<bool> _ensureTorConnected() async {
+ final settings = TorSettingsService.sharedInstance;
+ await settings.ensureLoaded();
+ if (settings.torMode != TorMode.builtIn) {
+ // External: the proxy is the user's, and `getProxy` fails closed if it is
+ // not usable. Disabled: there is no Tor to report.
+ return settings.torMode == TorMode.external;
+ }
+
await TorService.sharedInstance.start();
return TorService.sharedInstance.waitUntilConnected(timeout: const Duration(minutes: 2));
}
@@ -156,7 +164,7 @@ AppWallet appWalletOf(BuildContext context, {bool listen = false}) {
CryptoWallet? xmrWallet(BuildContext context) =>
Provider.of<WalletManager>(context, listen: false).getWallet('XMR');
-/// Shows the shared tx-details sheet (`wallet_ui`, D24) for [tx] from the tx
+/// Shows the shared tx-details sheet (`wallet_ui`) for [tx] from the tx
/// list. The activity list now renders the engine's wallet_domain TxDetails
/// directly, so no neutral-to-engine bridge is needed.
void showTxDetailsDialog(BuildContext context, TxDetails tx) {
### lib/widgets/tx_details.dart
@@ -5,7 +5,7 @@ import 'package:wallet_domain/wallet_domain.dart' show CryptoWallet, TxDetails;
import 'package:wallet_ui/wallet_ui.dart' show TxDetailsSheetLabels, showTxDetailsSheet;
/// The tx-details popup is the shared brand bottom sheet in wallet-core
-/// (`wallet_ui`, D24), localization-agnostic. This adapter keeps the app's call
+/// (`wallet_ui`), localization-agnostic. This adapter keeps the app's call
/// site unchanged and supplies Skylight's translated strings (incl. the status
/// banner).
class TxDetailsDialog {
### lib/widgets/ui/ui.dart
@@ -1,5 +1,5 @@
/// Skylight's brand widget set — the shared design tokens + primitives from the
-/// `wallet_ui` package (D24). This barrel re-exports them so screens import one
+/// `wallet_ui` package. This barrel re-exports them so screens import one
/// path.
library;
Why this scored 45/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.