Files
hash-wallet/lib/utils/exception_handler.dart
T
75f3fe735b Introduce Money class and refactor currency handling (#3157)
* feat: add `Money` class for precise currency handling and amount parsing

- Introduced `Money` class for handling money values with fixed-point precision.
- Added parsing and comparison methods to `CryptoCurrency` and `FiatCurrency` interfaces.
- Implemented `smartAmountSanitizer` for format normalization.

Includes extensive unit tests for validation.

* refactor: optimize Money class with new factories and string formatting improvements

* refactor: unify balance handling with `Money` refactor

* refactor: update `ElectrumBalance` and transaction logic to use `Money` for currency precision

* refactor: standardize balance handling with `Money` across `Monero`, `Wownero`, and `Zano` wallets

* refactor: migrate EVM balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Decred balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate nano balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Solana balance and transaction logic to `Money` for precise currency handling

* refactor: migrate Tron balance and transaction handling to `Money` for consistent currency precision

* refactor: migrate Zcash balance and transaction handling to `Money` for consistent currency precision

* refactor: adjust lib to handle money

* refactor: remove legacy Decred amount formatting and migrate to `Money`

* refactor: migrate pending transaction amount and fee to `Money`

* refactor: migrate dEuro savings and transaction logic to `Money` for improved precision and consistency

* refactor: migrate Bitcoin and lightning transaction logic to `Money` for consistent currency precision

* refactor: migrate Wownero, EVM, Zano, and Zcash transaction logic to `Money` for consistent and precise currency handling

* refactor: standardize balance handling across wallets, migrate to `Money.zero` and refine `Balance` class for consistency

* refactor: migrate Solana transaction and wallet logic to `Money` for consistent and precise currency handling

* refactor: improve fiat balance and formatting for consistency on dashboard

* fix: fiat input mode switching and standardize crypto amount display logic

* fix: cw_evm generator

* refactor: migrate exchange amount handling to `Money` for improved precision and consistency

* test: remove redundant and commented-out tests, tidy test cases, and apply minor formatting adjustments

* fix using Money wrapper in BridgeViewModel and USDT0Service

* refactor: streamline `AmountConverter` and `LightningWallet`, add `cw_core` tests to GitHub workflow, and remove unused/deprecated logic

* refactor: enhance `changeReceiveAmount` with `isCanonical` support, improve null handling, and refine amount parsing logic

* minor fix

* try/catch the lightning log file error

* feat: add copy-to-clipboard functionality for transaction amounts, fix typo in trailing widget method name (#3219)

* Cw 1234 disallow sp and mweb addresses as refund for swaps (#3187)

* disallow SP and MWEB for swaps

* Simplify address selection in addressForExchange

Refactor addressForExchange method to simplify address selection logic.

* validate refund and receive address types

* localize address validation messages

* minor ui fix [skip ci]

* minor fix [skip ci]

* Revert "localize address validation messages"

* fix translations

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* refactor: replace manual amount sanitization with `sanitized()` extension across the codebase, add unit tests for `AmountSanitizer`

* fix: correct `sanitized()` test expectations for decimal handling in `amount_sanitizer_test.dart`

* refactor: fee display logic and append currency symbol in `send_confirm_sheet.dart`

* refactor: simplify `ZcashBalance` class by replacing custom fields with inherited properties, update references accordingly

* refactor: replace hardcoded USDT contract address with `DefaultTronTokens` lookup, improve maintainability

* refactor: replace `currency` with `inputAmount.currency` in Solana transaction signing methods to avoid inconsistency

* fix: handle null `fee` in `electrum_transaction_info.dart` to prevent potential runtime errors

* refactor: replace raw fee and amount handling with `Money` object for improved consistency and maintainability

* fix: limit fiat balance display to 2 decimal places in `balance_view_model.dart`

* refactor: replace raw `amount` and `fee` types with `Money` in Solana transaction methods for consistency and precision

* refactor: replace raw `amount` and `fee` handling with `Money` and improve balance calculation consistency across view models

* refactor: replace raw balance strings with `Money` and simplify `UnconfirmedBalanceModal` widget logic

* refactor: replace `print` with `printV` for better logging, add equality and hashCode overrides to `ERC20Currency` for object comparison

* feat: prepare deuro savings migration

* feat: add V1 savings migration support and improve formatting consistency in DEuro view model

* feat: add V1 savings withdrawal support and update associated UI components and logic

* refactor: replace `Image.asset` with `CakeImageWidget`, update balance string handling, and streamline address validation logic

* refactor: use `Money.tryParse` with fallback to `Money.zero` in `NanoBalance.fromRawString` for safer balance parsing

* refactor: replace `ERC20Currency` with `Erc20Token` for consistency and remove unused `evm_erc20_currency.dart` file

* refactor: add computed `hasDepositAmount` for deposit null-checks, optimize balance handling in `zano_wallet`, update Breez SDK to v0.14.0, and implement equality/hashCode for `Currency`

* refactor: rename `amountSats` to `amount` in `PrepareLnurlPayRequest` for consistency

* revert: downgrade Breez SDK to v0.11.0 in `pubspec.yaml` and update relevant references in `pubspec.lock`

* fix: use `cryptoAmount.copyWith` for currency consistency and replace `Image.asset` with `CakeImageWidget` in swap address modal

* fix: ensure currency consistency by using `cryptoAmount.copyWith` in Solana and EVM wallet transactions

* refactor: simplify CSV row generation logic by removing `_splitAmountCurrency` and streamlining amount/fee handling

* fix: LateInitializationError Field 'sdk' has not been initialized

* fix: use `Money.tryParse` for safer parsing of deposit and receive amounts

* chore: remove debug print statement

* refactor: update transaction processing to consistently use `Money` for amounts and fees

* refactor: improve currency and amount handling, simplify logic, and enhance code readability in Send and Swap pages

* refactor: integrate `DecimalInputFormatter` for consistent decimal input across Send and Swap pages, simplify input handling logic

* fix: "fiatMode" per output on sendPage

* refactor: make transaction_details_height applicable more readable

* fix: use correct base unit for deposit

* refactor: use Money in estimateFakeSendAllTxAmount method

* fix decred frozen balance
minor fixes

* fix decred frozen balance
minor fixes

* fix: automatically switch to lightning if lnurl or invoice detected

* fix: getBolt11Amount test cases

* fix: standardise crypto amount formatting and display

* fix: use base unit for Tron transaction fee parsing
* refactor: hardcode fractional digits to 8 in `AmountParsingProxy` display methods
* refactor: ensure consistent 8-decimal precision for fee formatting in `PendingTransaction`
* fix: use `AmountParsingProxy` for available balance display in exchange view model
* fix: improve amount formatting and layout in swap and send confirmation sheets
* chore: remove redundant decimal truncation in balance view model and send confirmation sheet

* fix: allow for editing text field if it is filled with "ALL"

* fix: allow for editing text field if it is filled with "ALL"

* fix: update currency detection logic for lightning mode

* avoid potential infinite loop [skip ci]

* fix: getDisplayCryptoAmount

* fix: update estimated fee calculation

* chore: simplify fee calculation logic [skip-ci]

* fix value display in hidden mode

* fix stale state in send confirm sheet

* fix decimals for mweb

* fix available balance display for sol/trx on swap

* restore proper hint text

* fix swap receive amount calculation for xno

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: Serhii <17529954+serhii-bor@users.noreply.github.com>
Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
2026-06-30 02:45:44 +03:00

449 lines
15 KiB
Dart

import 'dart:io';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/package_info.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mailer/flutter_mailer.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ExceptionHandler {
static bool _hasError = false;
static const _coolDownDurationInDays =
bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode || kProfileMode) ? 0 : 7;
static File? _file;
static Future<void> _saveException(String? error, StackTrace? stackTrace,
{String? library}) async {
final appDocDir = await getAppDir();
if (_file == null) {
_file = File('${appDocDir.path}/error.txt');
}
String? walletType;
CustomTrace? programInfo;
try {
walletType = getIt.get<AppStore>().wallet?.type.name;
programInfo = CustomTrace(stackTrace ?? StackTrace.current);
} catch (_) {}
final exception = {
"${DateTime.now()}": {
"Error": "$error\n\n",
"WalletType": "$walletType\n\n",
"VerboseLog":
"${programInfo?.fileName}#${programInfo?.lineNumber}:${programInfo?.columnNumber} ${programInfo?.callerFunctionName}\n\n",
"Library": "$library\n\n",
"StackTrace": stackTrace.toString(),
}
};
const String separator = '''\n\n==========================================================
==========================================================\n\n''';
/// don't save existing errors
if (_file!.existsSync()) {
final String fileContent = await _file!.readAsString();
if (fileContent.contains("${exception.values.first}")) {
return;
}
}
_file!.writeAsStringSync(
"$exception $separator",
mode: FileMode.append,
);
}
static void _sendExceptionFile() async {
try {
if (_file == null) {
final appDocDir = await getAppDir();
_file = File('${appDocDir.path}/error.txt');
}
await _addDeviceInfo(_file!);
// Check if a mail client is available
final bool canSend = await FlutterMailer.canSendMail();
if (Platform.isIOS && !canSend) {
printV('Mail app is not available');
return;
}
final MailOptions mailOptions = MailOptions(
subject: 'Mobile App Issue',
recipients: ['support@cakewallet.com'],
attachments: [_file!.path],
);
final result = await FlutterMailer.send(mailOptions);
// Clear file content if the error was sent or saved.
// On android we can't know if it was sent or saved
if (result.name == MailerResponse.sent.name ||
result.name == MailerResponse.saved.name ||
result.name == MailerResponse.android.name) {
_file!.writeAsString("", mode: FileMode.write);
}
} catch (e, s) {
_saveException(e.toString(), s);
}
}
static Future<void> resetLastPopupDate() async {
final sharedPrefs = await SharedPreferences.getInstance();
await sharedPrefs.setString(PreferencesKey.lastPopupDate, DateTime(1971).toString());
}
static Future<void> onError(FlutterErrorDetails errorDetails) async {
if (await onLedgerError(errorDetails)) return;
if (kDebugMode || kProfileMode) {
if (_ignoreError(errorDetails.exception.toString()) ||
_ignoreError(errorDetails.stack.toString())) {
printV("(BELOW ERROR IS IGNORED AND WILL NOT TRIGGER POPUP IN PROD)");
}
FlutterError.presentError(errorDetails);
errorDetails.toString().split("\n").forEach(printV);
return;
}
if (_ignoreError(errorDetails.exception.toString()) ||
_ignoreError(errorDetails.stack.toString()) ||
_flutterErrorIgnore(errorDetails)) {
return;
}
_saveException(
errorDetails.exceptionAsString(),
errorDetails.stack,
library: errorDetails.library,
);
if (errorDetails.silent) {
return;
}
final sharedPrefs = await SharedPreferences.getInstance();
final lastPopupDate =
DateTime.tryParse(sharedPrefs.getString(PreferencesKey.lastPopupDate) ?? '') ??
DateTime.now().subtract(Duration(days: _coolDownDurationInDays + 1));
final durationSinceLastReport = DateTime.now().difference(lastPopupDate).inDays;
if (_hasError || durationSinceLastReport < _coolDownDurationInDays) {
return;
}
_hasError = true;
await sharedPrefs.setString(PreferencesKey.lastPopupDate, DateTime.now().toString());
// Instead of using WidgetsBinding.instance.addPostFrameCallback we
// await Future.delayed(Duration.zero), which does essentially the same (
// but doesn't wait for actual frame to be rendered), but it allows us to
// properly await the execution - which is what we want, without awaiting
// other code may call functions like Navigator.pop(), and close the alert
// instead of the intended UI.
// WidgetsBinding.instance.addPostFrameCallback(
// (timeStamp) async {
await Future.delayed(Duration.zero);
if (navigatorKey.currentContext != null) {
await showPopUp<void>(
context: navigatorKey.currentContext!,
builder: (context) {
return AlertWithTwoActions(
isDividerExist: true,
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
rightButtonText: S.of(context).send,
leftButtonText: S.of(context).do_not_send,
actionRightButton: () {
Navigator.of(context).pop();
_sendExceptionFile();
},
actionLeftButton: () {
Navigator.of(context).pop();
},
);
},
);
}
_hasError = false;
}
static const List<String> _ledgerErrors = [
'Wrong Device Status',
'PlatformException(133, Failed to write: (Unknown Error: 133), null, null)',
'PlatformException(IllegalArgument, Unknown deviceId:',
'ServiceNotSupportedException(ConnectionType.ble, Required service not supported. Write characteristic: false, Notify characteristic: false)',
'Make sure no other program is communicating with the Ledger',
'Exception: 6e01', // Wrong App
'Exception: 6d02',
'Exception: 6511',
'Exception: 6e00',
'Exception: 6985',
'Exception: 5515',
];
static bool isLedgerError(Object exception) =>
_ledgerErrors.any((element) => exception.toString().contains(element));
static Future<bool> onLedgerError(FlutterErrorDetails errorDetails) async {
if (!isLedgerError(errorDetails.exception)) return false;
String? interpretErrorCode(String errorCode) {
if (errorCode.contains("6985")) {
return S.current.ledger_error_tx_rejected_by_user;
} else if (errorCode.contains("5515")) {
return S.current.ledger_error_device_locked;
} else if (["6e01", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
return S.current.ledger_error_wrong_app;
}
return null;
}
printV(errorDetails.exception);
if (navigatorKey.currentContext != null) {
await showPopUp<void>(
context: navigatorKey.currentContext!,
builder: (context) => AlertWithOneAction(
alertTitle: "Ledger Error",
alertContent: interpretErrorCode(errorDetails.exception.toString()) ??
S.of(context).ledger_connection_error,
buttonText: S.of(context).close,
buttonAction: () => Navigator.of(context).pop(),
),
);
}
_hasError = false;
return true;
}
/// Ignore User related errors or system errors
static bool _ignoreError(String error) =>
_ignoredErrors.any((element) => error.contains(element));
static const List<String> _ignoredErrors = const [
"Bad file descriptor",
"No space left on device",
"OS Error: Broken pipe",
"Can't assign requested address",
"OS Error: Socket is not connected",
"Operation timed out",
"No route to host",
"Software caused connection abort",
"Connection reset by peer",
"Connection timed out",
"Connection reset by peer",
"Connection closed before full header was received",
"Connection terminated during handshake",
"OS Error: Connection refused, errno = 61",
"PERMISSION_NOT_GRANTED",
"OS Error: Permission denied",
"Failed host lookup:",
"CERTIFICATE_VERIFY_FAILED",
"Handshake error in client",
"Error while launching http",
"OS Error: Network is unreachable",
"ClientException: Write failed, uri=http",
"Corrupted wallets seeds",
"bad_alloc",
"does not correspond",
"basic_string",
"input_stream",
"input stream error",
"invalid signature",
"invalid password",
"NetworkImage._loadAsync",
"SSLV3_ALERT_BAD_RECORD_MAC",
"PlatformException(already_active, File picker is already active",
// SVG-related errors
"SvgParser",
"SVG parsing error",
"Invalid SVG",
"SVG format error",
"SvgPicture",
"Unable to load asset",
// Temporary ignored, More context: Flutter secure storage reads the values as null some times
// probably when the device was locked and then opened on Cake
// this is solved by a restart of the app
// just ignoring until we find a solution to this issue or migrate from flutter secure storage
"core/auth_service.dart:92",
"core/key_service.dart:14",
"Wallet is null",
"Wrong Device Status: 0x5515 (UNKNOWN)",
"Command handling failed. With error: hostUnreachable",
"FocusScopeNode was used after being disposed",
"_getDismissibleFlushbar",
"_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)",
"reown_core/relay_client/websocket/websocket_handler.dart",
"Image upload failed due to loss of GPU access",
"transport error",
"SdkError.sparkError(field0: Operator RPC error: Connection error: status: Unavailable, message: \"dns error\", details: []",
"the timeout of the request was reached",
"support for coin removed, your seedphrase:"
"Exception: Invalid image data"
];
static Future<void> _addDeviceInfo(File file) async {
final packageInfo = await PackageInfo.fromPlatform();
final currentVersion = packageInfo.version;
final appName = packageInfo.appName;
final package = packageInfo.packageName;
final deviceInfoPlugin = DeviceInfoPlugin();
Map<String, dynamic> deviceInfo = {};
if (Platform.isAndroid) {
deviceInfo = _readAndroidBuildData(await deviceInfoPlugin.androidInfo);
deviceInfo["Platform"] = "Android";
} else if (Platform.isIOS) {
deviceInfo = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo);
deviceInfo["Platform"] = "iOS";
} else if (Platform.isLinux) {
deviceInfo = _readLinuxDeviceInfo(await deviceInfoPlugin.linuxInfo);
deviceInfo["Platform"] = "Linux";
} else if (Platform.isMacOS) {
deviceInfo = _readMacOsDeviceInfo(await deviceInfoPlugin.macOsInfo);
deviceInfo["Platform"] = "MacOS";
} else if (Platform.isWindows) {
deviceInfo = _readWindowsDeviceInfo(await deviceInfoPlugin.windowsInfo);
deviceInfo["Platform"] = "Windows";
}
await file.writeAsString(
"App Version: $currentVersion\nApp Name: $appName\nPackage: $package\n\nDevice Info $deviceInfo\n\n",
mode: FileMode.append,
);
}
static Map<String, dynamic> _readAndroidBuildData(AndroidDeviceInfo build) {
return <String, dynamic>{
'brand': build.brand,
'device': build.device,
'manufacturer': build.manufacturer,
'model': build.model,
'product': build.product,
};
}
static Map<String, dynamic> _readIosDeviceInfo(IosDeviceInfo data) {
return <String, dynamic>{
'systemName': data.systemName,
'systemVersion': data.systemVersion,
'model': data.model,
'localizedModel': data.localizedModel,
'isPhysicalDevice': data.isPhysicalDevice,
};
}
static Map<String, dynamic> _readLinuxDeviceInfo(LinuxDeviceInfo data) {
return <String, dynamic>{
'name': data.name,
'version': data.version,
'versionCodename': data.versionCodename,
'versionId': data.versionId,
'prettyName': data.prettyName,
'buildId': data.buildId,
'variant': data.variant,
'variantId': data.variantId,
};
}
static Map<String, dynamic> _readMacOsDeviceInfo(MacOsDeviceInfo data) {
return <String, dynamic>{
'arch': data.arch,
'model': data.model,
'kernelVersion': data.kernelVersion,
'osRelease': data.osRelease,
};
}
static Map<String, dynamic> _readWindowsDeviceInfo(WindowsDeviceInfo data) {
return <String, dynamic>{
'majorVersion': data.majorVersion,
'minorVersion': data.minorVersion,
'buildNumber': data.buildNumber,
'productType': data.productType,
'productName': data.productName,
};
}
static Future<void> showError(String error, {int? delayInSeconds}) async {
if (_hasError) {
return;
}
_hasError = true;
if (delayInSeconds != null) {
Future.delayed(Duration(seconds: delayInSeconds), () => _showCopyPopup(error));
return;
}
await Future.delayed(Duration.zero);
await _showCopyPopup(error);
}
static Future<void> _showCopyPopup(String content) async {
if (navigatorKey.currentContext != null) {
final shouldCopy = await showPopUp<bool?>(
context: navigatorKey.currentContext!,
builder: (context) {
return AlertWithTwoActions(
isDividerExist: true,
alertTitle: S.of(context).error,
alertContent: content,
rightButtonText: S.of(context).copy,
leftButtonText: S.of(context).close,
actionRightButton: () {
Navigator.of(context).pop(true);
},
actionLeftButton: () {
Navigator.of(context).pop();
},
);
},
);
if (shouldCopy == true) {
await Clipboard.setData(ClipboardData(text: content));
await showBar<void>(
navigatorKey.currentContext!,
S.of(navigatorKey.currentContext!).copied_to_clipboard,
);
}
}
_hasError = false;
}
static bool _flutterErrorIgnore(FlutterErrorDetails errorDetails) {
// most probably a flutter context error so just ignore it if there is no stack we can debug with
return errorDetails.exception.toString().contains("Null check operator used on a null value") &&
errorDetails.stack == null;
}
}