Xelis wallet service rework, Xelis TX view formatting adjustments, Xelis event sub cleanup

This commit is contained in:
Tritonn204 2025-06-19 16:28:51 -07:00
parent 9c679204c7
commit 18d94a04c7
12 changed files with 342 additions and 232 deletions

View file

@ -1164,4 +1164,4 @@ packages:
version: "2.2.2"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.0"

View file

@ -810,4 +810,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.0"

View file

@ -959,4 +959,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.0"

View file

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: asn1lib
sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c"
sha256: "0511d6be23b007e95105ae023db599aea731df604608978dada7f9faf2637623"
url: "https://pub.dev"
source: hosted
version: "1.5.9"
version: "1.6.4"
async:
dependency: transitive
description:
@ -157,10 +157,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.4"
file:
dependency: transitive
description:
@ -199,10 +199,10 @@ packages:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: b416ff56002789e636244fb4cc449f587656eff995e5a7169457eb0593fcaddb
sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.9.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -396,7 +396,7 @@ packages:
description:
path: "."
ref: cake-update-v2
resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2"
resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273"
url: "https://github.com/cake-tech/on_chain.git"
source: git
version: "3.7.0"
@ -657,10 +657,10 @@ packages:
dependency: transitive
description:
name: unorm_dart
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
sha256: "8e3870a1caa60bde8352f9597dd3535d8068613269444f8e35ea8925ec84c1f5"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.3.1+1"
vector_math:
dependency: transitive
description:
@ -745,11 +745,11 @@ packages:
dependency: transitive
description:
path: "."
ref: "60f9aa38d5e31256ac13e9d23108ce2776fe9cb1"
resolved-ref: "60f9aa38d5e31256ac13e9d23108ce2776fe9cb1"
ref: "83bda92f1b833fe5d8584aa429d5143a3698b33f"
resolved-ref: "83bda92f1b833fe5d8584aa429d5143a3698b33f"
url: "https://github.com/xelis-project/xelis-flutter-ffi.git"
source: git
version: "0.2.0"
sdks:
dart: ">=3.7.0-0 <4.0.0"
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

View file

@ -8,8 +8,6 @@ extension NetworkName on Network {
return 'mainnet';
case Network.testnet:
return 'testnet';
case Network.dev:
return 'dev';
}
}
@ -19,8 +17,6 @@ extension NetworkName on Network {
return Network.mainnet;
case 'testnet':
return Network.testnet;
case 'dev':
return Network.dev;
default:
throw ArgumentError('Unknown network name: $name');
}

View file

@ -157,7 +157,7 @@ class XelisTransactionInfo extends TransactionInfo {
assetAmounts[asset] = (assetAmounts[asset] ?? BigInt.zero) + BigInt.from(transfer.amount);
if (txType.transfers.length > 1) {
formattedTransfers.add("${transfer.destination} ( $formatted )");
formattedTransfers.add("${transfer.destination} [ $formatted ]");
} else {
formattedTransfers.add("${transfer.destination}");
}
@ -213,7 +213,7 @@ class XelisTransactionInfo extends TransactionInfo {
}
fee = BigInt.from(txType.fee);
to = "SCID:\n${txType.contract}\n\nchunk_id ${txType.chunkId}";
to = "SCID:\n${txType.contract}\n\nChunk ID:\n${txType.chunkId}";
break;
case xelis_sdk.DeployContractEntry():

View file

@ -35,6 +35,7 @@ import 'package:cw_xelis/xelis_store_utils.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:xelis_dart_sdk/xelis_dart_sdk.dart' as xelis_sdk;
import 'package:mutex/mutex.dart';
part 'xelis_wallet.g.dart';
@ -190,7 +191,8 @@ abstract class XelisWalletBase
bool _isTransactionUpdating;
StreamSubscription<void>? _eventSub;
void _subscribeToWalletEvents() {
Future<void> _subscribeToWalletEvents() async {
await _unsubscribeFromWalletEvents();
_eventSub = _convertRawEvents().listen(_handleEvent);
}
@ -201,7 +203,6 @@ abstract class XelisWalletBase
Stream<Event> _convertRawEvents() async* {
final stream = _libWallet.eventsStream();
printV("eventsStream init");
await for (final raw in stream) {
try {
@ -299,12 +300,14 @@ abstract class XelisWalletBase
}
connecting = true;
try {
_subscribeToWalletEvents();
await _subscribeToWalletEvents();
String addr = isTestnet ? "testnet-node.xelis.io" : "us-node.xelis.io";
if (node.uri.host != addr) {
addr = node.uri.host;
if (node.uri.port != "") {
addr += ":" + node.uri.port.toString();
if (node.uri.port != null && node.uri.port > 0) {
addr += ":${node.uri.port}";
} else {
addr += ":443";
}
}
if (addr != persistantPeer) {
@ -347,53 +350,56 @@ abstract class XelisWalletBase
Future<void> 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));
syncStatus = NotConnectedSyncStatus();
await connectToNode(node: currentNode!);
await walletInfo.save();
syncStatus = SyncedSyncStatus();
}
List<XelisTransactionInfo> _txBuffer = [];
Timer? _txBatchTimer;
Timer? _txSaveDebounceTimer;
final _txBufferLock = Mutex();
void _bufferTransaction(XelisTransactionInfo tx) {
_txBufferLock.protect(() async {
_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);
if (_txBuffer.length > 1000) {
_txBatchTimer?.cancel();
_txBatchTimer = Timer(Duration.zero, _processTransactionBuffer);
} else if (_txBatchTimer == null || !_txBatchTimer!.isActive) {
_txBatchTimer = Timer(Duration(seconds: 1), _processTransactionBuffer);
}
});
}
}
Future<void> _flushTransactionBuffer() async {
Future<void> _processTransactionBuffer() async {
await _txBufferLock.protect(() async {
if (_txBuffer.isEmpty) return;
final toAdd = Map.fromEntries(_txBuffer.map((tx) => MapEntry(tx.id, tx)));
runInAction(() {
transactionHistory.addMany(toAdd);
});
final buffered = List<XelisTransactionInfo>.from(_txBuffer);
final txMap = {for (var tx in buffered) tx.id: tx};
runInAction(() => transactionHistory.addMany(txMap));
_txBuffer.clear();
_txBatchTimer = null;
_txSaveDebounceTimer?.cancel();
_txSaveDebounceTimer = Timer(Duration(seconds: 2), () async {
await transactionHistory.save();
});
});
}
@action
Future<void> _handleEvent(Event event) async {
try {
switch (event) {
case NewTransaction():
if (!isSupportedEntryType(event.tx)) break;
@ -404,13 +410,6 @@ abstract class XelisWalletBase
isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset
);
_bufferTransaction(transactionInfo);
_txSaveDebounceTimer?.cancel();
_txSaveDebounceTimer = Timer(Duration(seconds: 1), () async {
await _flushTransactionBuffer();
await transactionHistory.save();
});
break;
case BalanceChanged():
@ -499,6 +498,9 @@ abstract class XelisWalletBase
// _lastSyncError = event.message;
break;
}
} catch (e, s) {
printV("Error handling event $event: $e\n$s");
}
await Future.delayed(Duration.zero);
}
@ -550,6 +552,7 @@ abstract class XelisWalletBase
Future<void> _fetchAssetBalances() async {
for (final asset in xelAssetsBox.values) {
try {
if (asset.id == xelis_sdk.xelisAsset) continue;
final isTracked = await _libWallet.isAssetTracked(asset: asset.id);
@ -559,6 +562,9 @@ abstract class XelisWalletBase
} else if (!asset.enabled && isTracked) {
await _libWallet.untrackAsset(asset: asset.id);
}
} catch (e) {
printV("Failed to sync tracking for asset ${asset.id}: $e");
}
}
final bal = await _libWallet.getTrackedAssetBalancesRaw();
@ -1028,6 +1034,14 @@ abstract class XelisWalletBase
_isTransactionUpdating = false;
_txSaveDebounceTimer?.cancel();
_txBatchTimer?.cancel();
try {
await _processTransactionBuffer();
await transactionHistory.save();
} catch (e) {
printV("Error during wallet close cleanup: $e");
}
await _unsubscribeFromWalletEvents();
await _libWallet.close();
x_wallet.dropWallet(wallet: _libWallet);

View file

@ -42,6 +42,67 @@ class MemoryTierCalculator {
}
}
class XelisLoggerFactory {
static bool _isSetup = false;
static const LOG_LEVEL = 3;
/*
Log level for FFI Rust outputs in xelis_flutter
0: None
1: Error
2: Warn
3: Info
4: Debug
5: Trace
*/
static Future<void> setupIfNeeded() async {
if (_isSetup) return;
await x_api.setUpRustLogger();
_setupLogStream();
_isSetup = true;
}
static void _setupLogStream() {
x_api.createLogStream().listen((entry) {
final logLine = 'XELIS LOG | [${entry.level.name}] ${entry.tag}: ${entry.msg}';
switch (entry.level) {
case x_logger.Level.error:
if (LOG_LEVEL > 0) {
printV('$logLine');
}
break;
case x_logger.Level.warn:
if (LOG_LEVEL > 1) {
printV('⚠️ $logLine');
}
break;
case x_logger.Level.info:
if (LOG_LEVEL > 2) {
printV(' $logLine');
}
break;
case x_logger.Level.debug:
if (LOG_LEVEL > 3) {
printV('🐛 $logLine');
}
break;
case x_logger.Level.trace:
if (LOG_LEVEL > 4) {
printV('🔍 $logLine');
}
break;
}
},
onError: (dynamic e) {
printV("Error receiving Xelis Rust logs: $e");
});
}
}
enum XelisTableSize {
initial,
web,
@ -142,20 +203,8 @@ class XelisWalletService extends WalletService<
XelisNewWalletCredentials
> {
XelisWalletService(this.walletInfoSource, {required this.isDirect}) {
setupRustLogger();
XelisLoggerFactory.setupIfNeeded();
}
static const LOG_LEVEL = 3;
/*
Log level for FFI Rust outputs in xelis_flutter
0: None
1: Error
2: Warn
3: Info
4: Debug
5: Trace
*/
final Box<WalletInfo> walletInfoSource;
final bool isDirect;
@ -165,45 +214,6 @@ class XelisWalletService extends WalletService<
static Completer<void>? _tableUpgradeCompleter;
static XelisWallet? _activeWallet;
void setupRustLogger() async {
await x_api.setUpRustLogger();
x_api.createLogStream().listen((entry) {
final logLine = 'XELIS LOG | [${entry.level.name}] ${entry.tag}: ${entry.msg}';
switch (entry.level) {
case x_logger.Level.error:
if (LOG_LEVEL > 0) {
printV('$logLine');
}
break;
case x_logger.Level.warn:
if (LOG_LEVEL > 1) {
printV('⚠️ $logLine');
}
break;
case x_logger.Level.info:
if (LOG_LEVEL > 2) {
printV(' $logLine');
}
break;
case x_logger.Level.debug:
if (LOG_LEVEL > 3) {
printV('🐛 $logLine');
}
break;
case x_logger.Level.trace:
if (LOG_LEVEL > 4) {
printV('🔍 $logLine');
}
break;
}
},
onError: (dynamic e) {
printV("Error receiving Xelis Rust logs: $e");
});
}
@override
WalletType getType() => WalletType.xelis;
@ -216,7 +226,7 @@ class XelisWalletService extends WalletService<
try {
await _activeWallet!.close();
} catch (e) {
printV("Error closing active Xelis wallet: $e");
}
_activeWallet = null;
}

View file

@ -22,7 +22,7 @@ dependencies:
xelis_flutter:
git:
url: https://github.com/xelis-project/xelis-flutter-ffi.git
ref: 60f9aa38d5e31256ac13e9d23108ce2776fe9cb1
ref: 83bda92f1b833fe5d8584aa429d5143a3698b33f
# path: ./xelis-flutter-ffi
xelis_dart_sdk: ^0.28.0
mutex: ^3.1.0

View file

@ -59,7 +59,7 @@ class WalletUnlockPageState extends AuthPageState<WalletUnlockPage> {
if (state is IsLoadingState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// null duration to make it indefinite until its disposed
_authBar = createBar<void>(S.of(context).loading_wallet, duration: null)..show(context);
_authBar = createBar<void>(S.of(context).loading_wallet, context, duration: null)..show(context);
});
}

View file

@ -11,7 +11,103 @@ class AddressFormatter {
TextAlign? textAlign,
bool shouldTruncate = false,
}) {
// Check for parentheses in the address
final bracketIndex = address.indexOf('[');
if (bracketIndex != -1) {
// Split address and amount parts
final addressPart = address.substring(0, bracketIndex).trim();
final amountPart = address.substring(bracketIndex);
// For truncated addresses, handle differently
if (shouldTruncate) {
final addressWidget = _buildAddressWidget(
address: addressPart,
walletType: walletType,
evenTextStyle: evenTextStyle,
oddTextStyle: oddTextStyle,
textAlign: textAlign,
shouldTruncate: shouldTruncate,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
addressWidget,
Text(amountPart, style: evenTextStyle),
],
);
}
// For full addresses, integrate amount with last line
final cleanAddress = addressPart.replaceAll('bitcoincash:', '');
final isMWEB = addressPart.startsWith('ltcmweb');
final chunkSize = walletType != null ? _getChunkSize(walletType) : 4;
// Build chunks
final chunks = <String>[];
if (isMWEB) {
const mwebDisplayPrefix = 'ltcmweb';
chunks.add(mwebDisplayPrefix);
final startIndex = mwebDisplayPrefix.length;
for (int i = startIndex; i < cleanAddress.length; i += chunkSize) {
final chunk = cleanAddress.substring(
i,
math.min(i + chunkSize, cleanAddress.length),
);
chunks.add(chunk);
}
} else {
for (int i = 0; i < cleanAddress.length; i += chunkSize) {
final chunk = cleanAddress.substring(
i,
math.min(i + chunkSize, cleanAddress.length),
);
chunks.add(chunk);
}
}
// Build text spans with amount appended to last chunk
final spans = <TextSpan>[];
for (int i = 0; i < chunks.length; i++) {
final style = (i % 2 == 0) ? evenTextStyle : oddTextStyle ?? evenTextStyle.copyWith(color: evenTextStyle.color!.withAlpha(128));
if (i == chunks.length - 1) {
// Last chunk - append amount
spans.add(TextSpan(text: '${chunks[i]} ', style: style));
spans.add(TextSpan(text: amountPart, style: evenTextStyle));
} else {
spans.add(TextSpan(text: '${chunks[i]} ', style: style));
}
}
return RichText(
text: TextSpan(children: spans),
textAlign: textAlign ?? TextAlign.start,
overflow: TextOverflow.visible,
);
}
// No parentheses - use original logic
return _buildAddressWidget(
address: address,
walletType: walletType,
evenTextStyle: evenTextStyle,
oddTextStyle: oddTextStyle,
textAlign: textAlign,
shouldTruncate: shouldTruncate,
);
}
static Widget _buildAddressWidget({
required String address,
WalletType? walletType,
required TextStyle evenTextStyle,
TextStyle? oddTextStyle,
TextAlign? textAlign,
bool shouldTruncate = false,
}) {
final cleanAddress = address.replaceAll('bitcoincash:', '');
final isMWEB = address.startsWith('ltcmweb');
final chunkSize = walletType != null ? _getChunkSize(walletType) : 4;

View file

@ -560,6 +560,24 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
throw Exception("Pending transaction doesn't exist. It should not be happened.");
}
if (ocpRequest != null) {
state = TransactionCommitting();
if (selectedCryptoCurrency == CryptoCurrency.xmr) {
await pendingTransaction!.commit();
}
await _ocpService.commitOpenCryptoPayRequest(
pendingTransaction!.hex,
txId: pendingTransaction!.id,
request: ocpRequest!,
asset: selectedCryptoCurrency,
);
state = TransactionCommitted();
return;
}
late String address;
if (walletType == WalletType.xelis) {
@ -580,30 +598,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
});
}
if (ocpRequest != null) {
state = TransactionCommitting();
if (selectedCryptoCurrency == CryptoCurrency.xmr) {
await pendingTransaction!.commit();
}
await _ocpService.commitOpenCryptoPayRequest(
pendingTransaction!.hex,
txId: pendingTransaction!.id,
request: ocpRequest!,
asset: selectedCryptoCurrency,
);
state = TransactionCommitted();
return;
}
String address = outputs.fold('', (acc, value) {
return value.isParsedAddress
? '$acc${value.address}\n${value.extractedAddress}\n\n'
: '$acc${value.address}\n\n';
});
address = address.trim();
String note = outputs.fold('', (acc, value) => '$acc${value.note}\n');