From fb9df6b8832257149dce46905a311d912e1fc94e Mon Sep 17 00:00:00 2001 From: Francois Date: Sat, 29 Mar 2025 17:17:58 +0200 Subject: [PATCH 1/3] fix(market-data-manager): use trading symbol for price requests credit to charl --- .../src/market_data/market_data_manager.dart | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart b/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart index 8e11b2a8..2bb6fdb6 100644 --- a/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart @@ -81,7 +81,7 @@ class CexMarketDataManager implements MarketDataManager { Future init() async { // Initialize any resources if needed _knownTickers = UnmodifiableSetView( - (await _priceRepository.getCoinList()).map((e) => e.id).toSet(), + (await _priceRepository.getCoinList()).map((e) => e.symbol).toSet(), ); // Start cache clearing timer @@ -113,15 +113,19 @@ class CexMarketDataManager implements MarketDataManager { DateTime? priceDate, String fiatCurrency = 'usdt', }) { - return '${assetId.id}_${fiatCurrency}_${priceDate?.millisecondsSinceEpoch ?? 'current'}'; + return '${assetId.symbol.configSymbol}_${fiatCurrency}_${priceDate?.millisecondsSinceEpoch ?? 'current'}'; } // Helper method to generate change cache keys String _getChangeCacheKey(AssetId assetId, {String fiatCurrency = 'usdt'}) { - return '${assetId.id}_${fiatCurrency}_change24h'; + return '${assetId.symbol.configSymbol}_${fiatCurrency}_change24h'; } - + /// Gets the trading symbol to use for price lookups. + /// Prefers the binanceId if available, falls back to configSymbol + String _getTradingSymbol(AssetId assetId) { + return assetId.symbol.configSymbol; + } @override Decimal? priceIfKnown( @@ -167,7 +171,7 @@ class CexMarketDataManager implements MarketDataManager { try { final priceDouble = await _priceRepository.getCoinFiatPrice( - assetId.id, + _getTradingSymbol(assetId), priceDate: priceDate, fiatCoinId: fiatCurrency, ); @@ -204,10 +208,8 @@ class CexMarketDataManager implements MarketDataManager { return cachedPrice; } - final maybeKnownTicker = - assetId.symbol.binanceId ?? assetId.symbol.configSymbol; - - final isKnownTicker = _knownTickers?.contains(maybeKnownTicker) ?? false; + final tradingSymbol = _getTradingSymbol(assetId); + final isKnownTicker = _knownTickers?.contains(tradingSymbol) ?? false; if (!isKnownTicker) { return null; @@ -248,7 +250,7 @@ class CexMarketDataManager implements MarketDataManager { final prices = await _komodoPriceRepository.getKomodoPrices(); // Find the price for the requested asset - final priceData = prices[assetId.id]; + final priceData = prices[assetId.symbol.configSymbol]; if (priceData == null || priceData.change24h == null) { return null; @@ -281,7 +283,7 @@ class CexMarketDataManager implements MarketDataManager { try { final priceDoubleMap = await _priceRepository.getCoinFiatPrices( - assetId.id, + assetId.symbol.configSymbol, dates, fiatCoinId: fiatCurrency, ); From b4a52f83af3405ddd058d4daccdd59697c8c5859 Mon Sep 17 00:00:00 2001 From: Francois Date: Sat, 29 Mar 2025 17:32:52 +0200 Subject: [PATCH 2/3] fix(withdraw-amount-field): show max amount value when isMaxAmount convert to stateful widget and use a text editing controller to set the update the amount value regardless of the enabled/disabled status of the widget --- .../defi/withdraw/withdraw_amount_field.dart | 88 +++++++++++++++---- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/packages/komodo_ui/lib/src/defi/withdraw/withdraw_amount_field.dart b/packages/komodo_ui/lib/src/defi/withdraw/withdraw_amount_field.dart index 803c9dbc..f35f5ee3 100644 --- a/packages/komodo_ui/lib/src/defi/withdraw/withdraw_amount_field.dart +++ b/packages/komodo_ui/lib/src/defi/withdraw/withdraw_amount_field.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; -class WithdrawAmountField extends StatelessWidget { +/// A text input field for entering the withdrawal amount +class WithdrawAmountField extends StatefulWidget { + /// Creates a [WithdrawAmountField]. const WithdrawAmountField({ required this.asset, required this.amount, @@ -14,15 +16,69 @@ class WithdrawAmountField extends StatelessWidget { super.key, }); + /// The asset for which the withdrawal amount is being entered. final Asset asset; + + /// The current amount entered in the field. final String amount; + + /// Whether the maximum amount is selected. final bool isMaxAmount; + + /// Callback for when the amount changes. final ValueChanged onChanged; + + /// Callback for when the maximum amount is toggled. final ValueChanged onMaxToggled; + + /// Error message for the amount field. final String? amountError; + + /// Whether the user has insufficient balance. final bool hasInsufficientBalance; + + /// The available balance for the asset. final String? availableBalance; + @override + State createState() => _WithdrawAmountFieldState(); +} + +class _WithdrawAmountFieldState extends State { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.amount); + } + + @override + void didUpdateWidget(WithdrawAmountField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.amount != oldWidget.amount && _controller.text != widget.amount) { + // Save current cursor position + final selection = _controller.selection; + + // Update text + _controller.text = widget.amount; + + // Restore cursor position, but handle potential out-of-bounds + if (widget.amount.length >= selection.baseOffset) { + _controller.selection = selection; + } else { + // If new text is shorter, move cursor to end + _controller.selection = TextSelection.collapsed(offset: widget.amount.length); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -38,29 +94,29 @@ class WithdrawAmountField extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - if (availableBalance != null) + if (widget.availableBalance != null) Text( - 'Available: $availableBalance ${asset.id.id}', + 'Available: ${widget.availableBalance} ${widget.asset.id.id}', style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), ], ), const SizedBox(height: 8), TextFormField( - initialValue: amount, - enabled: !isMaxAmount, + controller: _controller, + enabled: !widget.isMaxAmount, decoration: InputDecoration( border: const OutlineInputBorder(), - errorText: amountError, + errorText: widget.amountError, suffixIcon: Padding( padding: const EdgeInsets.only(right: 12), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - asset.id.id, + widget.asset.id.id, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, ), @@ -69,7 +125,7 @@ class WithdrawAmountField extends StatelessWidget { ), ), prefixIcon: - hasInsufficientBalance + widget.hasInsufficientBalance ? Tooltip( message: 'Insufficient balance', child: Icon( @@ -79,16 +135,16 @@ class WithdrawAmountField extends StatelessWidget { ) : null, helperText: - hasInsufficientBalance + widget.hasInsufficientBalance ? 'Insufficient balance' : 'Enter the amount to send', helperStyle: - hasInsufficientBalance + widget.hasInsufficientBalance ? TextStyle(color: theme.colorScheme.error) : null, ), keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: onChanged, + onChanged: widget.onChanged, ), const SizedBox(height: 12), Row( @@ -97,16 +153,16 @@ class WithdrawAmountField extends StatelessWidget { Row( children: [ Checkbox( - value: isMaxAmount, - onChanged: (value) => onMaxToggled(value ?? false), + value: widget.isMaxAmount, + onChanged: (value) => widget.onMaxToggled(value ?? false), ), const Text('Send maximum available'), ], ), - if (!isMaxAmount) + if (!widget.isMaxAmount) TextButton( onPressed: () { - onMaxToggled(true); + widget.onMaxToggled(true); }, child: const Text('MAX'), ), From 7a3219f06814f0450c67fb0958f36f83f7fcefba Mon Sep 17 00:00:00 2001 From: Francois Date: Sun, 30 Mar 2025 21:14:35 +0200 Subject: [PATCH 3/3] fix(address-select-input): dropdown icon "hidden" by color configuration --- .../defi/withdraw/source_address_field.dart | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart b/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart index c3a3e9ae..b13d4848 100644 --- a/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart +++ b/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart @@ -58,7 +58,9 @@ class SourceAddressField extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer.withOpacity(0.7), + color: theme.colorScheme.secondaryContainer.withValues( + alpha: 0.7, + ), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -71,7 +73,30 @@ class SourceAddressField extends StatelessWidget { ], ), const SizedBox(height: 8), - _buildAddressSelector(context), + AddressSelectInput( + addresses: pubkeys!.keys, + selectedAddress: selectedAddress, + onAddressSelected: onChanged, + assetName: asset.id.name, + hint: 'Choose source address', + onCopied: (address) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Row( + children: [ + Icon(Icons.check_circle, color: Colors.white, size: 16), + SizedBox(width: 8), + Text('Address copied to clipboard'), + ], + ), + behavior: SnackBarBehavior.floating, + width: 280, + backgroundColor: theme.colorScheme.primary, + ), + ); + }, + verified: _isAddressVerified, + ), if (selectedAddress != null && showBalanceIndicator) ...[ const SizedBox(height: 12), _BalanceIndicator( @@ -84,38 +109,8 @@ class SourceAddressField extends StatelessWidget { ); } - Widget _buildAddressSelector(BuildContext context) { - final theme = Theme.of(context); - - // Remove the Container wrapper that was causing the double border - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: AddressSelectInput( - addresses: pubkeys!.keys, - selectedAddress: selectedAddress, - onAddressSelected: onChanged, - assetName: asset.id.name, - hint: 'Choose source address', - onCopied: (address) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Row( - children: [ - Icon(Icons.check_circle, color: Colors.white, size: 16), - SizedBox(width: 8), - Text('Address copied to clipboard'), - ], - ), - behavior: SnackBarBehavior.floating, - width: 280, - backgroundColor: theme.colorScheme.primary, - ), - ); - }, - verified: - (address) => _getAddressStatus(address) == AddressStatus.available, - ), - ); + bool _isAddressVerified(PubkeyInfo address) { + return _getAddressStatus(address) == AddressStatus.available; } AddressStatus _getAddressStatus(PubkeyInfo address) { @@ -146,7 +141,9 @@ class _LoadingState extends StatelessWidget { color: theme.colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + side: BorderSide( + color: theme.colorScheme.outline.withValues(alpha: 0.2), + ), ), child: Padding( padding: const EdgeInsets.all(24), @@ -167,7 +164,7 @@ class _LoadingState extends StatelessWidget { Text( 'Fetching your ${asset.id.name} addresses', style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), ], @@ -189,7 +186,7 @@ class _ErrorState extends StatelessWidget { return Card( elevation: 0, - color: theme.colorScheme.errorContainer.withOpacity(0.8), + color: theme.colorScheme.errorContainer.withValues(alpha: 0.8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), @@ -218,9 +215,12 @@ class _ErrorState extends StatelessWidget { ), const SizedBox(height: 8), Text( - "We couldn't load your wallet addresses. Please check your connection and try again.", + "We couldn't load your wallet addresses. " + 'Please check your connection and try again.', style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onErrorContainer.withOpacity(0.8), + color: theme.colorScheme.onErrorContainer.withValues( + alpha: 0.8, + ), ), ), if (onRetry != null) ...[ @@ -282,10 +282,10 @@ class _BalanceIndicator extends StatelessWidget { return Card( elevation: 0, - color: statusColor.withOpacity(0.08), + color: statusColor.withValues(alpha: 0.08), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: BorderSide(color: statusColor.withOpacity(0.3)), + side: BorderSide(color: statusColor.withValues(alpha: 0.3)), ), child: Padding( padding: const EdgeInsets.all(16), @@ -311,7 +311,7 @@ class _BalanceIndicator extends StatelessWidget { vertical: 4, ), decoration: BoxDecoration( - color: statusColor.withOpacity(0.15), + color: statusColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16), ), child: Text( @@ -347,14 +347,14 @@ class _BalanceIndicator extends StatelessWidget { Text( 'Locked:', style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), Text( '${balance.unspendable} $assetName', style: theme.textTheme.bodyMedium?.copyWith( fontStyle: FontStyle.italic, - color: theme.colorScheme.onSurface.withOpacity(0.7), + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), ), ], @@ -367,7 +367,7 @@ class _BalanceIndicator extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 6), decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), + color: statusColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), alignment: Alignment.center,