From 18d94a04c7d06690d7de9e8dd1f2a7224bb88cab Mon Sep 17 00:00:00 2001 From: Tritonn204 Date: Thu, 19 Jun 2025 16:28:51 -0700 Subject: [PATCH] Xelis wallet service rework, Xelis TX view formatting adjustments, Xelis event sub cleanup --- cw_bitcoin/pubspec.lock | 2 +- cw_core/pubspec.lock | 2 +- cw_nano/pubspec.lock | 2 +- cw_xelis/example/pubspec.lock | 24 +- cw_xelis/lib/src/api/network.dart | 4 - cw_xelis/lib/xelis_transaction_info.dart | 4 +- cw_xelis/lib/xelis_wallet.dart | 278 +++++++++--------- cw_xelis/lib/xelis_wallet_service.dart | 116 ++++---- cw_xelis/pubspec.yaml | 2 +- .../wallet_unlock/wallet_unlock_page.dart | 2 +- lib/utils/address_formatter.dart | 96 ++++++ lib/view_model/send/send_view_model.dart | 42 ++- 12 files changed, 342 insertions(+), 232 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 54b874806..c6488e98c 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -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" diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 67cf4624b..ad9b8393c 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -810,4 +810,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.24.0" \ No newline at end of file + flutter: ">=3.27.0" diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index 30e1a8a82..104b53b98 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -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" diff --git a/cw_xelis/example/pubspec.lock b/cw_xelis/example/pubspec.lock index 757bee583..d71862efd 100644 --- a/cw_xelis/example/pubspec.lock +++ b/cw_xelis/example/pubspec.lock @@ -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" diff --git a/cw_xelis/lib/src/api/network.dart b/cw_xelis/lib/src/api/network.dart index b18926cd5..3dd7583a1 100644 --- a/cw_xelis/lib/src/api/network.dart +++ b/cw_xelis/lib/src/api/network.dart @@ -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'); } diff --git a/cw_xelis/lib/xelis_transaction_info.dart b/cw_xelis/lib/xelis_transaction_info.dart index 72a467cb9..27d3f1fee 100644 --- a/cw_xelis/lib/xelis_transaction_info.dart +++ b/cw_xelis/lib/xelis_transaction_info.dart @@ -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(): diff --git a/cw_xelis/lib/xelis_wallet.dart b/cw_xelis/lib/xelis_wallet.dart index 96bbc61b3..65f41b4cf 100644 --- a/cw_xelis/lib/xelis_wallet.dart +++ b/cw_xelis/lib/xelis_wallet.dart @@ -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? _eventSub; - void _subscribeToWalletEvents() { + Future _subscribeToWalletEvents() async { + await _unsubscribeFromWalletEvents(); _eventSub = _convertRawEvents().listen(_handleEvent); } @@ -201,7 +203,6 @@ abstract class XelisWalletBase Stream _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,157 +350,156 @@ abstract class XelisWalletBase 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)); + syncStatus = NotConnectedSyncStatus(); + await connectToNode(node: currentNode!); await walletInfo.save(); - syncStatus = SyncedSyncStatus(); } List _txBuffer = []; Timer? _txBatchTimer; Timer? _txSaveDebounceTimer; + final _txBufferLock = Mutex(); 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); - }); - } + _txBufferLock.protect(() async { + _txBuffer.add(tx); + + if (_txBuffer.length > 1000) { + _txBatchTimer?.cancel(); + _txBatchTimer = Timer(Duration.zero, _processTransactionBuffer); + } else if (_txBatchTimer == null || !_txBatchTimer!.isActive) { + _txBatchTimer = Timer(Duration(seconds: 1), _processTransactionBuffer); + } + }); } - Future _flushTransactionBuffer() async { - if (_txBuffer.isEmpty) return; + Future _processTransactionBuffer() async { + await _txBufferLock.protect(() async { + if (_txBuffer.isEmpty) return; + + final buffered = List.from(_txBuffer); + + final txMap = {for (var tx in buffered) tx.id: tx}; + runInAction(() => transactionHistory.addMany(txMap)); + _txBuffer.clear(); - final toAdd = Map.fromEntries(_txBuffer.map((tx) => MapEntry(tx.id, tx))); - - runInAction(() { - transactionHistory.addMany(toAdd); + + _txSaveDebounceTimer?.cancel(); + _txSaveDebounceTimer = Timer(Duration(seconds: 2), () async { + await transactionHistory.save(); + }); }); - - _txBuffer.clear(); - _txBatchTimer = null; } @action Future _handleEvent(Event event) async { - switch (event) { - case NewTransaction(): - if (!isSupportedEntryType(event.tx)) break; + try { + switch (event) { + case NewTransaction(): + if (!isSupportedEntryType(event.tx)) break; - final transactionInfo = await XelisTransactionInfo.fromTransactionEntry( - event.tx, - wallet: _libWallet, - 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(): - if (event.asset == xelis_sdk.xelisAsset) { - final curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; - - balance[curr] = XelisAssetBalance( - balance: event.balance, - decimals: 8, + final transactionInfo = await XelisTransactionInfo.fromTransactionEntry( + event.tx, + wallet: _libWallet, + isAssetEnabled: (id) => findTrackedAssetById(id)?.enabled ?? id == xelis_sdk.xelisAsset ); - } else { - final curr = findTrackedAssetById(event.asset); - - if (curr != null) { - if (curr.enabled) { - balance[curr] = XelisAssetBalance( - balance: event.balance, - decimals: curr.decimals, - symbol: curr.symbol, - ); - } else { - balance.remove(curr); - } + _bufferTransaction(transactionInfo); + break; + + case BalanceChanged(): + if (event.asset == xelis_sdk.xelisAsset) { + final curr = isTestnet ? CryptoCurrency.xet : CryptoCurrency.xel; + + balance[curr] = XelisAssetBalance( + balance: event.balance, + decimals: 8, + ); } else { - try { - final metadata = await _libWallet.getAssetMetadata(asset: event.asset); - final asset = XelisAsset( - name: metadata.name, - symbol: metadata.ticker, - id: event.asset, - decimals: metadata.decimals, - enabled: false, - ); - await xelAssetsBox.put(asset.id, asset); - } catch (e) { - printV("Failed to fetch metadata for asset ${event.asset}: $e"); + final curr = findTrackedAssetById(event.asset); + + if (curr != null) { + if (curr.enabled) { + balance[curr] = XelisAssetBalance( + balance: event.balance, + decimals: curr.decimals, + symbol: curr.symbol, + ); + } else { + balance.remove(curr); + } + } else { + try { + final metadata = await _libWallet.getAssetMetadata(asset: event.asset); + final asset = XelisAsset( + name: metadata.name, + symbol: metadata.ticker, + id: event.asset, + decimals: metadata.decimals, + enabled: false, + ); + await xelAssetsBox.put(asset.id, asset); + } catch (e) { + printV("Failed to fetch metadata for asset ${event.asset}: $e"); + } } } - } - break; - - case NewTopoheight(): - topoheight = event.topoheight; - break; - - case Online(): - syncStatus = SyncedSyncStatus(); - break; - - case Offline(): - syncStatus = NotConnectedSyncStatus(); - break; - - case HistorySynced(): - syncStatus = SyncedSyncStatus(); - break; - - case Rescan(): - // optional - break; - - case NewAsset(): - if (event.asset == xelis_sdk.xelisAsset) { break; - } - final existing = xelAssetsBox.values - .cast() - .firstWhere((e) => e?.id == event.asset, orElse: () => null); + case NewTopoheight(): + topoheight = event.topoheight; + break; - final newAsset = XelisAsset( - name: event.name, - symbol: event.ticker, - id: event.asset, - decimals: event.decimals, - enabled: existing?.enabled ?? false, - ); + case Online(): + syncStatus = SyncedSyncStatus(); + break; - await updateAssetState(newAsset); - break; + case Offline(): + syncStatus = NotConnectedSyncStatus(); + break; - case SyncError(): - printV("Sync error occurred: ${event.message}"); - syncStatus = FailedSyncStatus(); - // TODO: display the error message - // _lastSyncError = event.message; - break; + case HistorySynced(): + syncStatus = SyncedSyncStatus(); + break; + + case Rescan(): + // optional + break; + + case NewAsset(): + if (event.asset == xelis_sdk.xelisAsset) { + break; + } + + final existing = xelAssetsBox.values + .cast() + .firstWhere((e) => e?.id == event.asset, orElse: () => null); + + final newAsset = XelisAsset( + name: event.name, + symbol: event.ticker, + id: event.asset, + decimals: event.decimals, + enabled: existing?.enabled ?? false, + ); + + await updateAssetState(newAsset); + break; + + case SyncError(): + printV("Sync error occurred: ${event.message}"); + syncStatus = FailedSyncStatus(); + // TODO: display the error message + // _lastSyncError = event.message; + break; + } + } catch (e, s) { + printV("Error handling event $event: $e\n$s"); } await Future.delayed(Duration.zero); } @@ -550,14 +552,18 @@ abstract class XelisWalletBase Future _fetchAssetBalances() async { for (final asset in xelAssetsBox.values) { - if (asset.id == xelis_sdk.xelisAsset) continue; - - final isTracked = await _libWallet.isAssetTracked(asset: asset.id); - - if (asset.enabled && !isTracked) { - await _libWallet.trackAsset(asset: asset.id); - } else if (!asset.enabled && isTracked) { - await _libWallet.untrackAsset(asset: asset.id); + try { + if (asset.id == xelis_sdk.xelisAsset) continue; + + final isTracked = await _libWallet.isAssetTracked(asset: asset.id); + + if (asset.enabled && !isTracked) { + await _libWallet.trackAsset(asset: asset.id); + } else if (!asset.enabled && isTracked) { + await _libWallet.untrackAsset(asset: asset.id); + } + } catch (e) { + printV("Failed to sync tracking for asset ${asset.id}: $e"); } } @@ -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); diff --git a/cw_xelis/lib/xelis_wallet_service.dart b/cw_xelis/lib/xelis_wallet_service.dart index f4de9e9ec..1a6b570c8 100644 --- a/cw_xelis/lib/xelis_wallet_service.dart +++ b/cw_xelis/lib/xelis_wallet_service.dart @@ -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 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 walletInfoSource; final bool isDirect; @@ -165,45 +214,6 @@ class XelisWalletService extends WalletService< static Completer? _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; } diff --git a/cw_xelis/pubspec.yaml b/cw_xelis/pubspec.yaml index f559d2231..79bc35ce0 100644 --- a/cw_xelis/pubspec.yaml +++ b/cw_xelis/pubspec.yaml @@ -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 diff --git a/lib/src/screens/wallet_unlock/wallet_unlock_page.dart b/lib/src/screens/wallet_unlock/wallet_unlock_page.dart index 3f1dc33e0..1f0ce8a72 100644 --- a/lib/src/screens/wallet_unlock/wallet_unlock_page.dart +++ b/lib/src/screens/wallet_unlock/wallet_unlock_page.dart @@ -59,7 +59,7 @@ class WalletUnlockPageState extends AuthPageState { if (state is IsLoadingState) { WidgetsBinding.instance.addPostFrameCallback((_) { // null duration to make it indefinite until its disposed - _authBar = createBar(S.of(context).loading_wallet, duration: null)..show(context); + _authBar = createBar(S.of(context).loading_wallet, context, duration: null)..show(context); }); } diff --git a/lib/utils/address_formatter.dart b/lib/utils/address_formatter.dart index f2083c772..1b8e5f839 100644 --- a/lib/utils/address_formatter.dart +++ b/lib/utils/address_formatter.dart @@ -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 = []; + 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 = []; + 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; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index fe85b8936..1dcfd9f6d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -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');