cleaned up handling of xelis tx details and history syncing

This commit is contained in:
Tritonn204 2025-06-18 12:06:31 -07:00
parent 03665e30f7
commit 194245ba38
9 changed files with 144 additions and 85 deletions

View file

@ -49,7 +49,7 @@ class XelisAssetBalance extends Balance {
} }
@override @override
String get formattedAvailableBalance => formatXelisAmount(balance, decimals: decimals); String get formattedAvailableBalance => XelisFormatter.formatAmount(balance, decimals: decimals);
@override @override
String get formattedAdditionalBalance => '0'; String get formattedAdditionalBalance => '0';

View file

@ -1,13 +1,9 @@
import 'dart:math'; import 'dart:math';
class XelisFormatter { class XelisFormatter {
static int _divider = 0;
static int parseXelisAmount(String amount) { static int parseXelisAmount(String amount) {
try { try {
final decimalLength = _getDividerForInput(amount); return (double.parse(amount) * pow(10, 8)).round();
_divider = decimalLength;
return (double.parse(amount) * pow(10, decimalLength)).round();
} catch (_) { } catch (_) {
return 0; return 0;
} }
@ -15,38 +11,44 @@ class XelisFormatter {
static double parseXelisAmountToDouble(int amount) { static double parseXelisAmountToDouble(int amount) {
try { try {
return amount / pow(10, _divider); return amount / pow(10, 8);
} catch (_) { } catch (_) {
return 0; return 0;
} }
} }
static int _getDividerForInput(String amount) { static int parseAmount(String amount, int decimals) {
final result = amount.split('.'); try {
if (result.length > 1) { return (double.parse(amount) * pow(10, decimals)).round();
final decimalLength = result[1].length; } catch (_) {
return decimalLength;
} else {
return 0; return 0;
} }
} }
}
String formatXelisAmountWithSymbol( static double parseAmountToDouble(int amount, int decimals) {
int rawAmount, { try {
required int decimals, return amount / pow(10, decimals);
String? symbol, } catch (_) {
}) { return 0;
final formatted = rawAmount / pow(10, decimals); }
// final symbol = assetId == null || assetId == xelisAsset ? 'XEL' : assetId; }
final sym = symbol ?? 'XEL';
return '$formatted $sym';
}
String formatXelisAmount( static String formatAmountWithSymbol(
int rawAmount, { int rawAmount, {
required int decimals, required int decimals,
}) { String? symbol,
final formatted = rawAmount / pow(10, decimals); }) {
return '$formatted'; 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';
}
} }

View file

@ -24,7 +24,7 @@ class XelisPendingTransaction with PendingTransaction {
String get amountFormatted => amount.toString(); String get amountFormatted => amount.toString();
@override @override
String get feeFormatted => formatXelisAmount(fee, decimals: 8); String get feeFormatted => XelisFormatter.formatAmount(fee, decimals: 8);
@override @override
String get hex => ""; String get hex => "";

View file

@ -72,7 +72,7 @@ abstract class XelisTransactionHistoryBase
final val = entry.value; final val = entry.value;
if (val is Map<String, dynamic>) { if (val is Map<String, dynamic>) {
final tx = XelisTransactionInfo.fromJson(val); final tx = XelisTransactionInfo.fromJson(val, isAssetEnabled: (id) => true); // asset filtering needs to happen elsewhere before serializing
addOne(tx); addOne(tx);
} }
}); });

View file

@ -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:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; import 'package:cw_xelis/src/api/wallet.dart' as x_wallet;
import 'package:cw_core/format_amount.dart'; import 'package:cw_core/format_amount.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'dart:math'; import 'dart:math';
@ -94,7 +95,7 @@ class XelisTransactionInfo extends TransactionInfo {
@override @override
String feeFormatted() => String feeFormatted() =>
formatXelisAmountWithSymbol(fee, decimals: 8, symbol: 'XEL'); XelisFormatter.formatAmountWithSymbol(fee, decimals: 8, symbol: 'XEL');
@override @override
String fiatAmount() => _fiatAmount ?? ''; String fiatAmount() => _fiatAmount ?? '';
@ -127,9 +128,7 @@ class XelisTransactionInfo extends TransactionInfo {
for (final transfer in txType.transfers) { for (final transfer in txType.transfers) {
final asset = transfer.asset; final asset = transfer.asset;
if (!isAssetEnabled(asset)) { if (!isAssetEnabled(asset)) continue;
continue;
}
assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount); assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount);
@ -144,43 +143,27 @@ class XelisTransactionInfo extends TransactionInfo {
case xelis_sdk.OutgoingEntry(): case xelis_sdk.OutgoingEntry():
direction = TransactionDirection.outgoing; direction = TransactionDirection.outgoing;
List<XelisTransfer> localTransfers = []; final formattedTransfers = <String>[];
for (final transfer in txType.transfers) { for (final transfer in txType.transfers) {
final asset = transfer.asset; final asset = transfer.asset;
if (!isAssetEnabled(asset)) { if (!isAssetEnabled(asset)) continue;
continue;
}
assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount);
final meta = await wallet.getAssetMetadata(asset: asset); final meta = await wallet.getAssetMetadata(asset: asset);
localTransfers.add( final formatted = XelisTransfer(meta: meta, amount: transfer.amount).format();
XelisTransfer(
meta: meta,
amount: transfer.amount
)
);
assetDecimals[asset] = meta.decimals; assetDecimals[asset] = meta.decimals;
assetSymbolsMap[asset] = meta.ticker; 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'); to = formattedTransfers.join('\n\n');
fee = BigInt.from(txType.fee); fee = BigInt.from(txType.fee);
break; break;
@ -248,12 +231,13 @@ class XelisTransactionInfo extends TransactionInfo {
break; 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 assetSymbols = assetIds.map((id) => assetSymbolsMap[id] ?? '???').toList();
final decimals = assetIds.map((id) => assetDecimals[id] ?? 8).toList(); final decimals = assetIds.map((id) => assetDecimals[id] ?? 8).toList();
final amounts = assetIds.map((id) => assetAmounts[id]!).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( return XelisTransactionInfo(
id: entry.hash, id: entry.hash,
@ -271,20 +255,42 @@ class XelisTransactionInfo extends TransactionInfo {
); );
} }
factory XelisTransactionInfo.fromJson(Map<String, dynamic> data) { factory XelisTransactionInfo.fromJson(
Map<String, dynamic> data, {
required bool Function(String assetId) isAssetEnabled,
}) {
final allAssetIds = List<String>.from(data['assetIds']);
final allAssetSymbols = List<String>.from(data['assetSymbols']);
final allAssetAmounts = (data['assetAmounts'] as List)
.map<BigInt>((val) => BigInt.parse(val.toString()))
.toList();
final allDecimals = List<int>.from(data['decimals']);
final filteredIndices = <int>[];
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( return XelisTransactionInfo(
id: data['id'] as String, id: data['id'] as String,
height: data['height'] as int, height: data['height'] as int,
decimals: List<int>.from(data['decimals']), decimals: decimals,
assetAmounts: (data['assetAmounts'] as List) assetAmounts: assetAmounts,
.map<BigInt>((val) => BigInt.parse(val.toString())) xelAmount: xelAmount,
.toList(),
xelAmount: BigInt.parse(data['xelAmount']),
xelFee: BigInt.parse(data['xelFee']), xelFee: BigInt.parse(data['xelFee']),
direction: parseTransactionDirectionFromInt(data['direction'] as int), direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
assetSymbols: List<String>.from(data['assetSymbols']), assetSymbols: assetSymbols,
assetIds: List<String>.from(data['assetIds']), assetIds: assetIds,
to: data['to'], to: data['to'],
from: data['from'], from: data['from'],
); );

View file

@ -18,6 +18,7 @@ import 'package:cw_core/node.dart';
import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.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_exception.dart';
import 'package:cw_xelis/xelis_asset_balance.dart'; import 'package:cw_xelis/xelis_asset_balance.dart';
import 'package:cw_xelis/src/api/wallet.dart' as x_wallet; import 'package:cw_xelis/src/api/wallet.dart' as x_wallet;
@ -342,28 +343,74 @@ abstract class XelisWalletBase
} }
@override @override
@action
Future<void> rescan({required int height}) async { Future<void> rescan({required int height}) async {
walletInfo.restoreHeight = height; walletInfo.restoreHeight = height;
walletInfo.isRecovery = true; walletInfo.isRecovery = true;
syncStatus = AttemptingSyncStatus(); syncStatus = AttemptingSyncStatus();
balance.clear(); 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 _libWallet.rescan(topoheight: BigInt.from(pruneHeight > height ? pruneHeight : height));
await walletInfo.save(); await walletInfo.save();
await connectToNode(node: currentNode!);
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} }
List<XelisTransactionInfo> _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<XelisTransactionInfo>.from(_txBuffer);
_txBuffer.clear();
_txBatchTimer = null;
final txMap = {
for (var tx in buffered) tx.id: tx
};
transactionHistory.addMany(txMap);
});
}
}
Future<void> _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<void> _handleEvent(Event event) async { Future<void> _handleEvent(Event event) async {
switch (event) { switch (event) {
case NewTransaction(): case NewTransaction():
if (!isSupportedEntryType(event.tx)) { break; } if (!isSupportedEntryType(event.tx)) break;
final transactionInfo = await XelisTransactionInfo.fromTransactionEntry( final transactionInfo = await XelisTransactionInfo.fromTransactionEntry(
event.tx, event.tx,
wallet: _libWallet, wallet: _libWallet,
isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset
); );
transactionHistory.addOne(transactionInfo); _bufferTransaction(transactionInfo);
await transactionHistory.save();
_txSaveDebounceTimer?.cancel();
_txSaveDebounceTimer = Timer(Duration(seconds: 1), () async {
await _flushTransactionBuffer();
await transactionHistory.save();
});
break; break;
case BalanceChanged(): case BalanceChanged():
@ -452,6 +499,7 @@ abstract class XelisWalletBase
// _lastSyncError = event.message; // _lastSyncError = event.message;
break; break;
} }
await Future.delayed(Duration.zero);
} }
Future<void> _fetchPruneHeight() async { Future<void> _fetchPruneHeight() async {
@ -880,7 +928,7 @@ abstract class XelisWalletBase
return XelisPendingTransaction( return XelisPendingTransaction(
txid: txHash, txid: txHash,
amount: totalAmountFromCredentials.toString(), amount: XelisFormatter.formatAmount(totalAmountFromCredentials, decimals: balance[transactionCurrency]!.decimals),
fee: txMap['fee'], fee: txMap['fee'],
decimals: balance[transactionCurrency]!.decimals, decimals: balance[transactionCurrency]!.decimals,
send: send send: send
@ -906,10 +954,8 @@ abstract class XelisWalletBase
} }
final txList = (await _libWallet.allHistory()) final txList = (await _libWallet.allHistory())
.map((jsonStr) => xelis_sdk.TransactionEntry.fromJson( .map((jsonStr) => xelis_sdk.TransactionEntry.fromJson(json.decode(jsonStr)) as xelis_sdk.TransactionEntry)
json.decode(jsonStr), .toList();
) as xelis_sdk.TransactionEntry)
.toList();
final Map<String, XelisTransactionInfo> result = {}; final Map<String, XelisTransactionInfo> result = {};
@ -980,6 +1026,8 @@ abstract class XelisWalletBase
} }
requestedClose = true; requestedClose = true;
_isTransactionUpdating = false; _isTransactionUpdating = false;
_txSaveDebounceTimer?.cancel();
_txBatchTimer?.cancel();
await _unsubscribeFromWalletEvents(); await _unsubscribeFromWalletEvents();
await _libWallet.close(); await _libWallet.close();
x_wallet.dropWallet(wallet: _libWallet); x_wallet.dropWallet(wallet: _libWallet);

View file

@ -118,7 +118,7 @@ abstract class OutputBase with Store {
_amount = zano!.formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); _amount = zano!.formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler());
break; break;
case WalletType.xelis: case WalletType.xelis:
_amount = xelis!.formatterStringDoubleToXelisAmount(_cryptoAmount); _amount = xelis!.formatterStringDoubleToAmount(_cryptoAmount, currency: cryptoCurrencyHandler());
break; break;
case WalletType.none: case WalletType.none:
case WalletType.haven: case WalletType.haven:

View file

@ -110,7 +110,10 @@ class CWXelis extends Xelis {
); );
@override @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 @override
double formatterXelisAmountToDouble( double formatterXelisAmountToDouble(

View file

@ -1451,7 +1451,7 @@ abstract class Xelis {
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
double formatterXelisAmountToDouble({TransactionInfo? transaction, BigInt? amount, int decimals = 8}); 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); // CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
double? getEstimateFees(WalletBase wallet); double? getEstimateFees(WalletBase wallet);