diff --git a/cw_xelis/lib/xelis_asset_balance.dart b/cw_xelis/lib/xelis_asset_balance.dart index e47167333..53aea00d3 100644 --- a/cw_xelis/lib/xelis_asset_balance.dart +++ b/cw_xelis/lib/xelis_asset_balance.dart @@ -49,7 +49,7 @@ class XelisAssetBalance extends Balance { } @override - String get formattedAvailableBalance => formatXelisAmount(balance, decimals: decimals); + String get formattedAvailableBalance => XelisFormatter.formatAmount(balance, decimals: decimals); @override String get formattedAdditionalBalance => '0'; diff --git a/cw_xelis/lib/xelis_formatting.dart b/cw_xelis/lib/xelis_formatting.dart index 38aaccdcd..6b1e4afa4 100644 --- a/cw_xelis/lib/xelis_formatting.dart +++ b/cw_xelis/lib/xelis_formatting.dart @@ -1,13 +1,9 @@ import 'dart:math'; class XelisFormatter { - static int _divider = 0; - static int parseXelisAmount(String amount) { try { - final decimalLength = _getDividerForInput(amount); - _divider = decimalLength; - return (double.parse(amount) * pow(10, decimalLength)).round(); + return (double.parse(amount) * pow(10, 8)).round(); } catch (_) { return 0; } @@ -15,38 +11,44 @@ class XelisFormatter { static double parseXelisAmountToDouble(int amount) { try { - return amount / pow(10, _divider); + return amount / pow(10, 8); } catch (_) { return 0; } } - static int _getDividerForInput(String amount) { - final result = amount.split('.'); - if (result.length > 1) { - final decimalLength = result[1].length; - return decimalLength; - } else { + static int parseAmount(String amount, int decimals) { + try { + return (double.parse(amount) * pow(10, decimals)).round(); + } catch (_) { return 0; } } -} -String formatXelisAmountWithSymbol( - int rawAmount, { - required int decimals, - String? symbol, -}) { - final formatted = rawAmount / pow(10, decimals); - // final symbol = assetId == null || assetId == xelisAsset ? 'XEL' : assetId; - final sym = symbol ?? 'XEL'; - return '$formatted $sym'; -} + static double parseAmountToDouble(int amount, int decimals) { + try { + return amount / pow(10, decimals); + } catch (_) { + return 0; + } + } -String formatXelisAmount( - int rawAmount, { - required int decimals, -}) { - final formatted = rawAmount / pow(10, decimals); - return '$formatted'; + static String formatAmountWithSymbol( + int rawAmount, { + required int decimals, + String? symbol, + }) { + final formatted = rawAmount / pow(10, decimals); + // final symbol = assetId == null || assetId == xelisAsset ? 'XEL' : assetId; + final sym = symbol ?? 'XEL'; + return '$formatted $sym'; + } + + static String formatAmount( + int rawAmount, { + required int decimals, + }) { + final formatted = rawAmount / pow(10, decimals); + return '$formatted'; + } } diff --git a/cw_xelis/lib/xelis_pending_transaction.dart b/cw_xelis/lib/xelis_pending_transaction.dart index 8f805ae6a..e32791e3b 100644 --- a/cw_xelis/lib/xelis_pending_transaction.dart +++ b/cw_xelis/lib/xelis_pending_transaction.dart @@ -24,7 +24,7 @@ class XelisPendingTransaction with PendingTransaction { String get amountFormatted => amount.toString(); @override - String get feeFormatted => formatXelisAmount(fee, decimals: 8); + String get feeFormatted => XelisFormatter.formatAmount(fee, decimals: 8); @override String get hex => ""; diff --git a/cw_xelis/lib/xelis_transaction_history.dart b/cw_xelis/lib/xelis_transaction_history.dart index 3bb15bee8..40a7ad82b 100644 --- a/cw_xelis/lib/xelis_transaction_history.dart +++ b/cw_xelis/lib/xelis_transaction_history.dart @@ -72,7 +72,7 @@ abstract class XelisTransactionHistoryBase final val = entry.value; if (val is Map) { - final tx = XelisTransactionInfo.fromJson(val); + final tx = XelisTransactionInfo.fromJson(val, isAssetEnabled: (id) => true); // asset filtering needs to happen elsewhere before serializing addOne(tx); } }); diff --git a/cw_xelis/lib/xelis_transaction_info.dart b/cw_xelis/lib/xelis_transaction_info.dart index 0d382ea94..72a467cb9 100644 --- a/cw_xelis/lib/xelis_transaction_info.dart +++ b/cw_xelis/lib/xelis_transaction_info.dart @@ -4,6 +4,7 @@ import 'package:cw_xelis/xelis_formatting.dart'; import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk; import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'dart:math'; @@ -94,7 +95,7 @@ class XelisTransactionInfo extends TransactionInfo { @override String feeFormatted() => - formatXelisAmountWithSymbol(fee, decimals: 8, symbol: 'XEL'); + XelisFormatter.formatAmountWithSymbol(fee, decimals: 8, symbol: 'XEL'); @override String fiatAmount() => _fiatAmount ?? ''; @@ -127,9 +128,7 @@ class XelisTransactionInfo extends TransactionInfo { for (final transfer in txType.transfers) { final asset = transfer.asset; - if (!isAssetEnabled(asset)) { - continue; - } + if (!isAssetEnabled(asset)) continue; assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount); @@ -144,43 +143,27 @@ class XelisTransactionInfo extends TransactionInfo { case xelis_sdk.OutgoingEntry(): direction = TransactionDirection.outgoing; - List localTransfers = []; + final formattedTransfers = []; for (final transfer in txType.transfers) { final asset = transfer.asset; - if (!isAssetEnabled(asset)) { - continue; - } - - assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount); + if (!isAssetEnabled(asset)) continue; final meta = await wallet.getAssetMetadata(asset: asset); - localTransfers.add( - XelisTransfer( - meta: meta, - amount: transfer.amount - ) - ); + final formatted = XelisTransfer(meta: meta, amount: transfer.amount).format(); + assetDecimals[asset] = meta.decimals; assetSymbolsMap[asset] = meta.ticker; + assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount); + + if (txType.transfers.length > 1) { + formattedTransfers.add("${transfer.destination} ( $formatted )"); + } else { + formattedTransfers.add("${transfer.destination}"); + } } - final formattedTransfers = txType.transfers - .asMap() - .entries - .map((entry) { - final index = entry.key; - final t = entry.value; - if (txType.transfers.length > 1) { - return "${t.destination} ( ${localTransfers[index].format()} )"; - } - return "${t.destination}"; - }) - .where((transfer) => transfer.isNotEmpty) - .toList(); - to = formattedTransfers.join('\n\n'); - fee = BigInt.from(txType.fee); break; @@ -248,12 +231,13 @@ class XelisTransactionInfo extends TransactionInfo { break; } - final assetIds = assetAmounts.keys.toList(); + final filteredAssetIds = assetAmounts.keys.where(isAssetEnabled).toList(); + final assetIds = filteredAssetIds; final assetSymbols = assetIds.map((id) => assetSymbolsMap[id] ?? '???').toList(); final decimals = assetIds.map((id) => assetDecimals[id] ?? 8).toList(); final amounts = assetIds.map((id) => assetAmounts[id]!).toList(); - final xelAmount = amounts.isNotEmpty ? amounts[0] : BigInt.zero; + final xelAmount = assetAmounts[xelis_sdk.xelisAsset] ?? BigInt.zero; return XelisTransactionInfo( id: entry.hash, @@ -271,20 +255,42 @@ class XelisTransactionInfo extends TransactionInfo { ); } - factory XelisTransactionInfo.fromJson(Map data) { + factory XelisTransactionInfo.fromJson( + Map data, { + required bool Function(String assetId) isAssetEnabled, + }) { + final allAssetIds = List.from(data['assetIds']); + final allAssetSymbols = List.from(data['assetSymbols']); + final allAssetAmounts = (data['assetAmounts'] as List) + .map((val) => BigInt.parse(val.toString())) + .toList(); + final allDecimals = List.from(data['decimals']); + + final filteredIndices = []; + for (int i = 0; i < allAssetIds.length; i++) { + if (isAssetEnabled(allAssetIds[i])) { + filteredIndices.add(i); + } + } + + final assetIds = filteredIndices.map((i) => allAssetIds[i]).toList(); + final assetSymbols = filteredIndices.map((i) => allAssetSymbols[i]).toList(); + final assetAmounts = filteredIndices.map((i) => allAssetAmounts[i]).toList(); + final decimals = filteredIndices.map((i) => allDecimals[i]).toList(); + + final xelAmount = assetAmounts.isNotEmpty ? assetAmounts[0] : BigInt.zero; + return XelisTransactionInfo( id: data['id'] as String, height: data['height'] as int, - decimals: List.from(data['decimals']), - assetAmounts: (data['assetAmounts'] as List) - .map((val) => BigInt.parse(val.toString())) - .toList(), - xelAmount: BigInt.parse(data['xelAmount']), + decimals: decimals, + assetAmounts: assetAmounts, + xelAmount: xelAmount, xelFee: BigInt.parse(data['xelFee']), direction: parseTransactionDirectionFromInt(data['direction'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), - assetSymbols: List.from(data['assetSymbols']), - assetIds: List.from(data['assetIds']), + assetSymbols: assetSymbols, + assetIds: assetIds, to: data['to'], from: data['from'], ); diff --git a/cw_xelis/lib/xelis_wallet.dart b/cw_xelis/lib/xelis_wallet.dart index d5817f180..96bbc61b3 100644 --- a/cw_xelis/lib/xelis_wallet.dart +++ b/cw_xelis/lib/xelis_wallet.dart @@ -18,6 +18,7 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_xelis/xelis_formatting.dart'; import 'package:cw_xelis/xelis_exception.dart'; import 'package:cw_xelis/xelis_asset_balance.dart'; import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; @@ -342,28 +343,74 @@ abstract class XelisWalletBase } @override + @action Future rescan({required int height}) async { walletInfo.restoreHeight = height; walletInfo.isRecovery = true; syncStatus = AttemptingSyncStatus(); balance.clear(); + + final curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + balance[curr] = XelisAssetBalance.zero(symbol: isTestnet ? "XET" : "XEL"); + await _libWallet.rescan(topoheight: BigInt.from(pruneHeight > height ? pruneHeight : height)); await walletInfo.save(); - await connectToNode(node: currentNode!); syncStatus = SyncedSyncStatus(); } + List _txBuffer = []; + Timer? _txBatchTimer; + Timer? _txSaveDebounceTimer; + + void _bufferTransaction(XelisTransactionInfo tx) { + _txBuffer.add(tx); + + if (_txBatchTimer == null || !_txBatchTimer!.isActive) { + _txBatchTimer = Timer(Duration(seconds: 1), () async { + final buffered = List.from(_txBuffer); + _txBuffer.clear(); + _txBatchTimer = null; + + final txMap = { + for (var tx in buffered) tx.id: tx + }; + transactionHistory.addMany(txMap); + }); + } + } + + Future _flushTransactionBuffer() async { + if (_txBuffer.isEmpty) return; + + final toAdd = Map.fromEntries(_txBuffer.map((tx) => MapEntry(tx.id, tx))); + + runInAction(() { + transactionHistory.addMany(toAdd); + }); + + _txBuffer.clear(); + _txBatchTimer = null; + } + + @action Future _handleEvent(Event event) async { switch (event) { case NewTransaction(): - if (!isSupportedEntryType(event.tx)) { break; } + if (!isSupportedEntryType(event.tx)) break; + final transactionInfo = await XelisTransactionInfo.fromTransactionEntry( event.tx, wallet: _libWallet, isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset ); - transactionHistory.addOne(transactionInfo); - await transactionHistory.save(); + _bufferTransaction(transactionInfo); + + _txSaveDebounceTimer?.cancel(); + _txSaveDebounceTimer = Timer(Duration(seconds: 1), () async { + await _flushTransactionBuffer(); + await transactionHistory.save(); + }); + break; case BalanceChanged(): @@ -452,6 +499,7 @@ abstract class XelisWalletBase // _lastSyncError = event.message; break; } + await Future.delayed(Duration.zero); } Future _fetchPruneHeight() async { @@ -880,7 +928,7 @@ abstract class XelisWalletBase return XelisPendingTransaction( txid: txHash, - amount: totalAmountFromCredentials.toString(), + amount: XelisFormatter.formatAmount(totalAmountFromCredentials, decimals: balance[transactionCurrency]!.decimals), fee: txMap['fee'], decimals: balance[transactionCurrency]!.decimals, send: send @@ -906,10 +954,8 @@ abstract class XelisWalletBase } final txList = (await _libWallet.allHistory()) - .map((jsonStr) => xelis_sdk.TransactionEntry.fromJson( - json.decode(jsonStr), - ) as xelis_sdk.TransactionEntry) - .toList(); + .map((jsonStr) => xelis_sdk.TransactionEntry.fromJson(json.decode(jsonStr)) as xelis_sdk.TransactionEntry) + .toList(); final Map result = {}; @@ -980,6 +1026,8 @@ abstract class XelisWalletBase } requestedClose = true; _isTransactionUpdating = false; + _txSaveDebounceTimer?.cancel(); + _txBatchTimer?.cancel(); await _unsubscribeFromWalletEvents(); await _libWallet.close(); x_wallet.dropWallet(wallet: _libWallet); diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index fd306e9c3..ec58c474f 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -118,7 +118,7 @@ abstract class OutputBase with Store { _amount = zano!.formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); break; case WalletType.xelis: - _amount = xelis!.formatterStringDoubleToXelisAmount(_cryptoAmount); + _amount = xelis!.formatterStringDoubleToAmount(_cryptoAmount, currency: cryptoCurrencyHandler()); break; case WalletType.none: case WalletType.haven: diff --git a/lib/xelis/cw_xelis.dart b/lib/xelis/cw_xelis.dart index 822fa4bb8..799cc8e69 100644 --- a/lib/xelis/cw_xelis.dart +++ b/lib/xelis/cw_xelis.dart @@ -110,7 +110,10 @@ class CWXelis extends Xelis { ); @override - int formatterStringDoubleToXelisAmount(String amount) => XelisFormatter.parseXelisAmount(amount); + int formatterStringDoubleToAmount(String amount, {required CryptoCurrency currency}) { + if (currency is XelisAsset) return XelisFormatter.parseAmount(amount, currency.decimals); + return XelisFormatter.parseXelisAmount(amount); + } @override double formatterXelisAmountToDouble( diff --git a/tool/configure.dart b/tool/configure.dart index 620be99d3..0590bf4c6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1451,7 +1451,7 @@ abstract class Xelis { CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); double formatterXelisAmountToDouble({TransactionInfo? transaction, BigInt? amount, int decimals = 8}); - int formatterStringDoubleToXelisAmount(String amount); + int formatterStringDoubleToAmount(String amount, {required CryptoCurrency currency}); // CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); double? getEstimateFees(WalletBase wallet);