mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
* 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>
303 lines
8.4 KiB
Dart
303 lines
8.4 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:cake_wallet/generated/i18n.dart';
|
||
import 'package:cake_wallet/src/screens/backup/backup_page.dart';
|
||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||
import 'package:cake_wallet/utils/share_util.dart';
|
||
import 'package:cake_wallet/utils/show_bar.dart';
|
||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
|
||
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
|
||
import 'package:cw_core/transaction_direction.dart';
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
|
||
class CsvExportService {
|
||
static const _columns = [
|
||
'record_type',
|
||
'date_time',
|
||
'type',
|
||
'amount',
|
||
'currency',
|
||
'fee',
|
||
'tx_id',
|
||
'address',
|
||
'status',
|
||
'note',
|
||
'from_amount',
|
||
'from_currency',
|
||
'to_amount',
|
||
'to_currency',
|
||
'trade_id',
|
||
'provider',
|
||
'confirmations',
|
||
];
|
||
|
||
static const _utf8Bom = '';
|
||
|
||
String buildCsvContent(List<ActionListItem> items) {
|
||
final buf = StringBuffer();
|
||
buf.write(_utf8Bom);
|
||
buf.writeln(_columns.join(','));
|
||
|
||
for (final item in items) {
|
||
if (item is DateSectionItem) continue;
|
||
|
||
final row = _buildRow(item);
|
||
if (row != null) buf.writeln(row);
|
||
}
|
||
|
||
return buf.toString();
|
||
}
|
||
|
||
String? _buildRow(ActionListItem item) {
|
||
if (item is TransactionListItem) return _transactionRow(item);
|
||
if (item is TradeListItem) return _tradeRow(item);
|
||
if (item is OrderListItem) return _orderRow(item);
|
||
if (item is AnonpayTransactionListItem) return _anonpayRow(item);
|
||
if (item is PayjoinTransactionListItem) return _payjoinRow(item);
|
||
return null;
|
||
}
|
||
|
||
String _transactionRow(TransactionListItem item) {
|
||
final tx = item.transaction;
|
||
final type = tx.direction == TransactionDirection.incoming ? 'incoming' : 'outgoing';
|
||
final status = tx.isPending ? 'pending' : 'confirmed';
|
||
|
||
// Prefer tx.to/tx.from; fall back to address lists for chains that don't populate them.
|
||
final address = tx.direction == TransactionDirection.incoming
|
||
? (tx.from?.isNotEmpty == true
|
||
? tx.from!
|
||
: (tx.inputAddresses?.firstOrNull ?? ''))
|
||
: (tx.to?.isNotEmpty == true
|
||
? tx.to!
|
||
: (tx.outputAddresses?.firstOrNull ?? ''));
|
||
|
||
final fee = tx.fee != null && !tx.fee!.isZero ? tx.fee.toString() : '';
|
||
|
||
return _row([
|
||
'transaction',
|
||
_isoDate(tx.date),
|
||
type,
|
||
tx.amount.toString(),
|
||
tx.amount.currency.symbol,
|
||
fee,
|
||
tx.id,
|
||
address,
|
||
status,
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
tx.confirmations.toString(),
|
||
]);
|
||
}
|
||
|
||
String _tradeRow(TradeListItem item) {
|
||
final trade = item.trade;
|
||
return _row([
|
||
'trade',
|
||
_isoDate(trade.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)),
|
||
'swap',
|
||
'',
|
||
'',
|
||
trade.fee?.toString() ?? '',
|
||
trade.outputTransaction ?? '',
|
||
trade.payoutAddress ?? '',
|
||
trade.state.title,
|
||
trade.memo ?? '',
|
||
trade.amount,
|
||
trade.from?.name ?? '',
|
||
trade.receiveAmount ?? '',
|
||
trade.to?.name ?? '',
|
||
trade.id,
|
||
trade.provider.title,
|
||
'',
|
||
]);
|
||
}
|
||
|
||
String _orderRow(OrderListItem item) {
|
||
final order = item.order;
|
||
return _row([
|
||
'order',
|
||
_isoDate(order.createdAt),
|
||
order.source.title,
|
||
order.amountFormatted(),
|
||
order.from ?? '',
|
||
'',
|
||
order.transferId,
|
||
'',
|
||
order.state.title,
|
||
'',
|
||
order.amountFormatted(),
|
||
order.from ?? '',
|
||
order.receiveAmount ?? '',
|
||
order.to ?? '',
|
||
order.id,
|
||
order.providerTitle,
|
||
'',
|
||
]);
|
||
}
|
||
|
||
String _anonpayRow(AnonpayTransactionListItem item) {
|
||
final tx = item.transaction;
|
||
final amount = tx.fiatAmount?.toString() ?? tx.amountTo?.toString() ?? '';
|
||
final currency = tx.fiatEquiv ?? tx.coinTo;
|
||
|
||
return _row([
|
||
'anonpay',
|
||
_isoDate(tx.createdAt),
|
||
'anonymous_payment',
|
||
amount,
|
||
currency,
|
||
'',
|
||
tx.invoiceId,
|
||
tx.address,
|
||
tx.status,
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
tx.provider,
|
||
'',
|
||
]);
|
||
}
|
||
|
||
String _payjoinRow(PayjoinTransactionListItem item) {
|
||
final session = item.session;
|
||
final type = session.isSenderSession ? 'send' : 'receive';
|
||
final amount = session.rawAmount ?? '0';
|
||
|
||
return _row([
|
||
'payjoin',
|
||
_isoDate(session.inProgressSince ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)),
|
||
type,
|
||
amount,
|
||
'BTC',
|
||
'',
|
||
session.txId ?? '',
|
||
'',
|
||
item.status,
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
'',
|
||
]);
|
||
}
|
||
|
||
String _row(List<String> fields) => fields.map(escapeField).join(',');
|
||
|
||
String escapeField(String field) {
|
||
if (field.contains(',') || field.contains('"') || field.contains('\n')) {
|
||
return '"${field.replaceAll('"', '""')}"';
|
||
}
|
||
return field;
|
||
}
|
||
|
||
String _isoDate(DateTime dt) => dt.toUtc().toIso8601String();
|
||
|
||
Future<void> exportToCsv(List<ActionListItem> items, BuildContext context) async {
|
||
final dataItems = items.whereType<ActionListItem>().where((e) => e is! DateSectionItem).toList();
|
||
|
||
if (dataItems.isEmpty) {
|
||
await showBar<void>(context, S.current.csv_nothing_to_export);
|
||
return;
|
||
}
|
||
|
||
final now = DateTime.now();
|
||
final fileName = 'cake_wallet_export_${DateFormat('yyyyMMdd_HHmmss').format(now)}.csv';
|
||
|
||
late File csvFile;
|
||
|
||
final exportFuture = Future(() async {
|
||
final content = buildCsvContent(items);
|
||
csvFile = await _writeTempFile(fileName, content);
|
||
});
|
||
|
||
showPersistentActionOverlay(context, exportFuture, text: S.current.generating_csv);
|
||
await exportFuture;
|
||
|
||
if (!context.mounted) return;
|
||
|
||
if (Platform.isAndroid) {
|
||
_showAndroidExportDialog(context, csvFile, fileName);
|
||
} else if (Platform.isIOS) {
|
||
await _shareFile(csvFile, fileName, context);
|
||
} else {
|
||
await _saveFileDesktop(csvFile, fileName);
|
||
}
|
||
}
|
||
|
||
void _showAndroidExportDialog(BuildContext context, File csvFile, String fileName) {
|
||
showPopUp<void>(
|
||
context: context,
|
||
builder: (dialogContext) {
|
||
return AlertWithTwoActions(
|
||
alertTitle: S.current.export_csv,
|
||
alertContent: S.current.select_destination,
|
||
rightButtonText: S.current.save_to_downloads,
|
||
leftButtonText: S.current.share,
|
||
actionRightButton: () async {
|
||
await _saveToDownloads(fileName, csvFile);
|
||
Navigator.of(dialogContext).pop();
|
||
await showBar<void>(context, S.current.file_saved);
|
||
await csvFile.delete();
|
||
},
|
||
actionLeftButton: () async {
|
||
Navigator.of(dialogContext).pop();
|
||
await _shareFile(csvFile, fileName, context);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Future<void> _shareFile(File file, String fileName, BuildContext context) async {
|
||
await ShareUtil.shareFile(filePath: file.path, fileName: fileName, context: context);
|
||
if (await file.exists()) await file.delete();
|
||
}
|
||
|
||
Future<void> _saveFileDesktop(File csvFile, String fileName) async {
|
||
final outputPath = await FilePicker.platform.saveFile(
|
||
dialogTitle: 'Save CSV export',
|
||
fileName: fileName,
|
||
lockParentWindow: true,
|
||
);
|
||
if (outputPath == null) return;
|
||
await csvFile.copy(outputPath);
|
||
await csvFile.delete();
|
||
}
|
||
|
||
Future<File> _writeTempFile(String fileName, String content) async {
|
||
final dir = await getApplicationDocumentsDirectory();
|
||
final file = File('${dir.path}/$fileName');
|
||
if (file.existsSync()) file.deleteSync();
|
||
await file.writeAsString(content, flush: true);
|
||
return file;
|
||
}
|
||
|
||
Future<void> _saveToDownloads(String fileName, File file) async {
|
||
if (!Platform.isAndroid) return;
|
||
const downloadsPath = '/storage/emulated/0/Download';
|
||
final dest = File('$downloadsPath/$fileName');
|
||
if (dest.existsSync()) dest.deleteSync();
|
||
await file.copy(dest.path);
|
||
}
|
||
}
|