diff --git a/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart b/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart index b1759e6e..9dec1869 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/data/binance_repository.dart @@ -70,24 +70,19 @@ class BinanceRepository implements CexRepository { /// Internal method to fetch coin list data from the API. Future> _fetchCoinListInternal() async { - try { - // Try primary endpoint first, fallback to secondary on failure - Exception? lastException; - for (final baseUrl in binanceApiEndpoint) { - try { - final exchangeInfo = await _binanceProvider.fetchExchangeInfoReduced( - baseUrl: baseUrl, - ); - return _convertSymbolsToCoins(exchangeInfo); - } catch (e) { - lastException = e is Exception ? e : Exception(e.toString()); - } + Exception? lastException; + // Try primary endpoint first, fallback to secondary on failure + for (final baseUrl in binanceApiEndpoint) { + try { + final exchangeInfo = await _binanceProvider.fetchExchangeInfoReduced( + baseUrl: baseUrl, + ); + return _convertSymbolsToCoins(exchangeInfo); + } catch (e) { + lastException = e is Exception ? e : Exception(e.toString()); } - throw lastException ?? Exception('All endpoints failed'); - } catch (e, s) { - _logger.severe('Failed to fetch coin list from Binance API: $e', e, s); - rethrow; } + throw lastException ?? Exception('All endpoints failed'); } CexCoin _binanceCoin(String baseCoinAbbr, String quoteCoinAbbr) { diff --git a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart index db448833..af8403c8 100644 --- a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart +++ b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_cex_provider.dart @@ -607,15 +607,6 @@ class CoinGeckoCexProvider implements ICoinGeckoProvider { response.body, ); - _logger.warning( - ApiErrorParser.createSafeErrorMessage( - operation: operation, - service: 'CoinGecko', - statusCode: response.statusCode, - coinId: coinId, - ), - ); - throw Exception(apiError.message); } } diff --git a/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_cex_provider.dart b/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_cex_provider.dart index 68d3264e..b15f2b72 100644 --- a/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_cex_provider.dart +++ b/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_cex_provider.dart @@ -106,38 +106,23 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { @override Future> fetchCoinList() async { - _logger.info('Fetching coin list from CoinPaprika'); - final uri = Uri.https(baseUrl, '$apiVersion/coins'); - try { - final response = await _httpClient - .get(uri, headers: _createRequestHeaderMap()) - .timeout(CoinPaprikaConfig.timeout); - - if (response.statusCode != 200) { - _throwApiErrorOrException(response, 'ALL', 'coin list fetch'); - } - - final coins = jsonDecode(response.body) as List; - final result = coins - .cast>() - .map(CoinPaprikaCoin.fromJson) - .toList(); - - _logger.info( - 'Successfully fetched ${result.length} coins from CoinPaprika', - ); - return result; - } on TimeoutException catch (e) { - _logger.severe('Timeout while fetching coin list from CoinPaprika', e); - throw TimeoutException( - 'Request to fetch coin list timed out after ${CoinPaprikaConfig.timeout.inSeconds} seconds', - CoinPaprikaConfig.timeout, - ); - } catch (e, st) { - _logger.severe('Failed to fetch coin list from CoinPaprika', e, st); - rethrow; + + final response = await _httpClient + .get(uri, headers: _createRequestHeaderMap()) + .timeout(CoinPaprikaConfig.timeout); + + if (response.statusCode != 200) { + _throwApiErrorOrException(response, 'ALL', 'coin list fetch'); } + + final coins = jsonDecode(response.body) as List; + final result = coins + .cast>() + .map(CoinPaprikaCoin.fromJson) + .toList(); + + return result; } /// Fetches historical OHLC data using the correct CoinPaprika API format. @@ -164,12 +149,6 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { // Map quote currency: stablecoins -> underlying fiat (e.g., USDT -> USD) final mappedQuote = _mapQuoteCurrencyForApi(quote); - _logger.fine( - 'Fetching OHLC data for $coinId from ${_formatDateForApi(startDate)} ' - '${endDate != null ? 'to ${_formatDateForApi(endDate)}' : ''} ' - '(interval: $apiInterval, quote: ${mappedQuote.coinPaprikaId})', - ); - // CoinPaprika API only requires start date and interval for historical data final queryParams = { 'start': _formatDateForApi(startDate), @@ -184,42 +163,22 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { '$apiVersion/tickers/$coinId/historical', queryParams, ); - try { - final response = await _httpClient - .get(uri, headers: _createRequestHeaderMap()) - .timeout(CoinPaprikaConfig.timeout); - - if (response.statusCode != 200) { - _throwApiErrorOrException(response, coinId, 'OHLC data fetch'); - } - - final ticksData = jsonDecode(response.body) as List; - final result = ticksData - .cast>() - .map(_parseTicksToOhlc) - .toList(); - - _logger.info( - 'Successfully fetched ${result.length} OHLC data points for $coinId', - ); - return result; - } on TimeoutException catch (e) { - _logger.severe( - 'Timeout while fetching OHLC data for $coinId from CoinPaprika', - e, - ); - throw TimeoutException( - 'Request to fetch OHLC data for $coinId timed out after ${CoinPaprikaConfig.timeout.inSeconds} seconds', - CoinPaprikaConfig.timeout, - ); - } catch (e, st) { - _logger.severe( - 'Failed to fetch OHLC data for $coinId from CoinPaprika', - e, - st, - ); - rethrow; + + final response = await _httpClient + .get(uri, headers: _createRequestHeaderMap()) + .timeout(CoinPaprikaConfig.timeout); + + if (response.statusCode != 200) { + _throwApiErrorOrException(response, coinId, 'OHLC data fetch'); } + + final ticksData = jsonDecode(response.body) as List; + final result = ticksData + .cast>() + .map(_parseTicksToOhlc) + .toList(); + + return result; } @override @@ -232,7 +191,6 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { final quotesParam = mappedQuotes .map((q) => q.coinPaprikaId.toUpperCase()) .join(','); - _logger.info('Fetching market data for $coinId with quotes: $quotesParam'); final queryParams = {'quotes': quotesParam}; @@ -241,40 +199,22 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { '$apiVersion/coins/$coinId/markets', queryParams, ); - try { - final response = await _httpClient - .get(uri, headers: _createRequestHeaderMap()) - .timeout(CoinPaprikaConfig.timeout); - - if (response.statusCode != 200) { - _throwApiErrorOrException(response, coinId, 'market data fetch'); - } - - final markets = jsonDecode(response.body) as List; - final result = markets - .cast>() - .map(CoinPaprikaMarket.fromJson) - .toList(); - - _logger.info('Successfully fetched ${result.length} markets for $coinId'); - return result; - } on TimeoutException catch (e) { - _logger.severe( - 'Timeout while fetching market data for $coinId from CoinPaprika', - e, - ); - throw TimeoutException( - 'Request to fetch market data for $coinId timed out after ${CoinPaprikaConfig.timeout.inSeconds} seconds', - CoinPaprikaConfig.timeout, - ); - } catch (e, st) { - _logger.severe( - 'Failed to fetch market data for $coinId from CoinPaprika', - e, - st, - ); - rethrow; + + final response = await _httpClient + .get(uri, headers: _createRequestHeaderMap()) + .timeout(CoinPaprikaConfig.timeout); + + if (response.statusCode != 200) { + _throwApiErrorOrException(response, coinId, 'market data fetch'); } + + final markets = jsonDecode(response.body) as List; + final result = markets + .cast>() + .map(CoinPaprikaMarket.fromJson) + .toList(); + + return result; } /// Fetches ticker data for a specific coin. @@ -291,41 +231,21 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { final quotesParam = mappedQuotes .map((q) => q.coinPaprikaId.toUpperCase()) .join(','); - _logger.info('Fetching ticker data for $coinId with quotes: $quotesParam'); final queryParams = {'quotes': quotesParam}; final uri = Uri.https(baseUrl, '$apiVersion/tickers/$coinId', queryParams); - try { - final response = await _httpClient - .get(uri, headers: _createRequestHeaderMap()) - .timeout(CoinPaprikaConfig.timeout); - - if (response.statusCode != 200) { - _throwApiErrorOrException(response, coinId, 'ticker data fetch'); - } - final ticker = jsonDecode(response.body) as Map; - final result = CoinPaprikaTicker.fromJson(ticker); - _logger.info('Successfully fetched ticker data for $coinId'); - return result; - } on TimeoutException catch (e) { - _logger.severe( - 'Timeout while fetching ticker data for $coinId from CoinPaprika', - e, - ); - throw TimeoutException( - 'Request to fetch ticker data for $coinId timed out after ' - '${CoinPaprikaConfig.timeout.inSeconds} seconds', - CoinPaprikaConfig.timeout, - ); - } catch (e, st) { - _logger.severe( - 'Failed to fetch ticker data for $coinId from CoinPaprika', - e, - st, - ); - rethrow; + + final response = await _httpClient + .get(uri, headers: _createRequestHeaderMap()) + .timeout(CoinPaprikaConfig.timeout); + + if (response.statusCode != 200) { + _throwApiErrorOrException(response, coinId, 'ticker data fetch'); } + final ticker = jsonDecode(response.body) as Map; + final result = CoinPaprikaTicker.fromJson(ticker); + return result; } /// Validates if the requested date range is within the current API plan's @@ -343,9 +263,11 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { }) { // Validate interval support if (!apiPlan.isIntervalSupported(interval)) { - throw ArgumentError( + throw ArgumentError.value( + interval, + 'interval', 'Interval "$interval" is not supported in the ${apiPlan.planName} plan. ' - 'Supported intervals: ${apiPlan.availableIntervals.join(", ")}', + 'Supported intervals: ${apiPlan.availableIntervals.join(", ")}', ); } @@ -360,22 +282,26 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { // Check if any requested date is before the cutoff if (startDate != null && startDate.isBefore(cutoffDate)) { - throw ArgumentError( + throw ArgumentError.value( + startDate, + 'startDate', 'Historical data before ${_formatDateForApi(cutoffDate)} is not ' - 'available in the ${apiPlan.planName} plan. ' - 'Requested start date: ${_formatDateForApi(startDate)}. ' - '${apiPlan.ohlcLimitDescription}. Please request more recent data or ' - 'upgrade your plan.', + 'available in the ${apiPlan.planName} plan. ' + 'Requested start date: ${_formatDateForApi(startDate)}. ' + '${apiPlan.ohlcLimitDescription}. Please request more recent data or ' + 'upgrade your plan.', ); } if (endDate != null && endDate.isBefore(cutoffDate)) { - throw ArgumentError( + throw ArgumentError.value( + endDate, + 'endDate', 'Historical data before ${_formatDateForApi(cutoffDate)} is not ' - 'available in the ${apiPlan.planName} plan. ' - 'Requested end date: ${_formatDateForApi(endDate)}. ' - '${apiPlan.ohlcLimitDescription}. Please request more recent data or ' - 'upgrade your plan.', + 'available in the ${apiPlan.planName} plan. ' + 'Requested end date: ${_formatDateForApi(endDate)}. ' + '${apiPlan.ohlcLimitDescription}. Please request more recent data or ' + 'upgrade your plan.', ); } } @@ -385,10 +311,12 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { /// Throws [ArgumentError] if the interval is not supported. void _validateInterval(String interval) { if (!apiPlan.isIntervalSupported(interval)) { - throw ArgumentError( + throw ArgumentError.value( + interval, + 'interval', 'Interval "$interval" is not supported in the ${apiPlan.planName} ' - 'plan. Supported intervals: ${apiPlan.availableIntervals.join(", ")}. ' - 'Please use a supported interval or upgrade to a higher plan.', + 'plan. Supported intervals: ${apiPlan.availableIntervals.join(", ")}. ' + 'Please use a supported interval or upgrade to a higher plan.', ); } } @@ -544,9 +472,11 @@ class CoinPaprikaProvider implements ICoinPaprikaProvider { '${cutoffDate != null ? '(since ${_formatDateForApi(cutoffDate)})' : ''}', ); - throw ArgumentError( + throw ArgumentError.value( + response.body, + 'apiResponse', 'Historical data not available: ${apiPlan.ohlcLimitDescription}. ' - 'Please request more recent data or upgrade your plan.', + 'Please request more recent data or upgrade your plan.', ); } diff --git a/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_repository.dart b/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_repository.dart index 18e9a62c..8135d55c 100644 --- a/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/coinpaprika/data/coinpaprika_repository.dart @@ -51,42 +51,30 @@ class CoinPaprikaRepository implements CexRepository { /// Internal method to fetch coin list data from the API. Future> _fetchCoinListInternal() async { - try { - final coins = await coinPaprikaProvider.fetchCoinList(); - - // Build supported quote currencies from provider (hard-coded in provider) - final supportedCurrencies = coinPaprikaProvider.supportedQuoteCurrencies - .map((q) => q.coinPaprikaId) - .toSet(); - - final result = coins - .where((coin) => coin.isActive) // Only include active coins - .map( - (coin) => CexCoin( - id: coin.id, - symbol: coin.symbol, - name: coin.name, - currencies: supportedCurrencies, - ), - ) - .toList(); - - _cachedQuoteCurrencies = supportedCurrencies - .map((s) => s.toUpperCase()) - .toSet(); - - _logger.info( - 'Successfully processed ${result.length} active coins from CoinPaprika', - ); - return result; - } catch (e, stackTrace) { - _logger.severe( - 'Failed to fetch coin list from CoinPaprika', - e, - stackTrace, - ); - rethrow; - } + final coins = await coinPaprikaProvider.fetchCoinList(); + + // Build supported quote currencies from provider (hard-coded in provider) + final supportedCurrencies = coinPaprikaProvider.supportedQuoteCurrencies + .map((q) => q.coinPaprikaId) + .toSet(); + + final result = coins + .where((coin) => coin.isActive) // Only include active coins + .map( + (coin) => CexCoin( + id: coin.id, + symbol: coin.symbol, + name: coin.name, + currencies: supportedCurrencies, + ), + ) + .toList(); + + _cachedQuoteCurrencies = supportedCurrencies + .map((s) => s.toUpperCase()) + .toSet(); + + return result; } @override @@ -98,94 +86,71 @@ class CoinPaprikaRepository implements CexRepository { DateTime? endAt, int? limit, }) async { - try { - final tradingSymbol = resolveTradingSymbol(assetId); - final apiPlan = coinPaprikaProvider.apiPlan; - - // Determine the actual fetchable date range (using UTC) - var effectiveStartAt = startAt; - final effectiveEndAt = endAt ?? DateTime.now().toUtc(); - - // If no startAt provided, use default based on plan limit or - // reasonable default - if (effectiveStartAt == null) { - if (apiPlan.hasUnlimitedOhlcHistory) { - effectiveStartAt = effectiveEndAt.subtract( - const Duration(days: 365), - ); // Default 1 year for unlimited - } else { - effectiveStartAt = effectiveEndAt.subtract( - apiPlan.ohlcHistoricalDataLimit!, - ); - } + final tradingSymbol = resolveTradingSymbol(assetId); + final apiPlan = coinPaprikaProvider.apiPlan; + + // Determine the actual fetchable date range (using UTC) + var effectiveStartAt = startAt?.toUtc(); + final effectiveEndAt = endAt ?? DateTime.now().toUtc(); + + // If no startAt provided, use default based on plan limit or + // reasonable default + if (effectiveStartAt == null) { + if (apiPlan.hasUnlimitedOhlcHistory) { + effectiveStartAt = effectiveEndAt.subtract( + const Duration(days: 365), + ); // Default 1 year for unlimited + } else { + effectiveStartAt = effectiveEndAt.subtract( + apiPlan.ohlcHistoricalDataLimit!, + ); } + } - // Check if the requested range is entirely before the cutoff date - // (only for limited plans) - if (!apiPlan.hasUnlimitedOhlcHistory) { - final cutoffDate = apiPlan.getHistoricalDataCutoff(); - if (cutoffDate != null) { - // If both start and end dates are before cutoff, return empty data - if (effectiveEndAt.isBefore(cutoffDate)) { - _logger.info( - 'Requested date range is entirely before cutoff ' - '(${_formatDateForApi(cutoffDate)}) - no data available for ' - '${apiPlan.planName} plan', - ); - return const CoinOhlc(ohlc: []); - } - - // If start date is before cutoff, adjust it to cutoff date - if (effectiveStartAt.isBefore(cutoffDate)) { - _logger.info( - 'Adjusting start date from ${_formatDateForApi(effectiveStartAt)} ' - 'to cutoff date ${_formatDateForApi(cutoffDate)} for ' - '${apiPlan.planName} plan', - ); - effectiveStartAt = cutoffDate; - } + // Check if the requested range is entirely before the cutoff date + // (only for limited plans) + if (!apiPlan.hasUnlimitedOhlcHistory) { + final cutoffDate = apiPlan.getHistoricalDataCutoff(); + if (cutoffDate != null) { + // If both start and end dates are before cutoff, return empty data + if (effectiveEndAt.isBefore(cutoffDate)) { + return const CoinOhlc(ohlc: []); } - } - // If effective start is after end, return empty data - if (effectiveStartAt.isAfter(effectiveEndAt)) { - _logger.info( - 'Effective startAt is after endAt - no data available for requested ' - 'period due to ${apiPlan.planName} plan limitations', - ); - return const CoinOhlc(ohlc: []); + // If start date is before cutoff, adjust it to cutoff date + if (effectiveStartAt.isBefore(cutoffDate)) { + effectiveStartAt = cutoffDate; + } } + } - // Determine reasonable batch size based on API plan - final batchDuration = _getBatchDuration(apiPlan); - final totalDuration = effectiveEndAt.difference(effectiveStartAt); + // If effective start is after end, return empty data + if (effectiveStartAt.isAfter(effectiveEndAt)) { + return const CoinOhlc(ohlc: []); + } - // If the request is within the batch size, make a single request - if (totalDuration <= batchDuration) { - return _fetchSingleOhlcRequest( - tradingSymbol, - quoteCurrency, - effectiveStartAt, - effectiveEndAt, - ); - } + // Determine reasonable batch size based on API plan + final batchDuration = _getBatchDuration(apiPlan); + final totalDuration = effectiveEndAt.difference(effectiveStartAt); - // Split the request into multiple sequential requests - return _fetchMultipleOhlcRequests( + // If the request is within the batch size, make a single request + if (totalDuration <= batchDuration) { + return _fetchSingleOhlcRequest( tradingSymbol, quoteCurrency, effectiveStartAt, effectiveEndAt, - batchDuration, - ); - } catch (e, stackTrace) { - _logger.severe( - 'Failed to fetch OHLC data for ${assetId.id}', - e, - stackTrace, ); - rethrow; } + + // Split the request into multiple sequential requests + return _fetchMultipleOhlcRequests( + tradingSymbol, + quoteCurrency, + effectiveStartAt, + effectiveEndAt, + batchDuration, + ); } /// Fetches OHLC data in a single request (within plan limits). @@ -222,17 +187,9 @@ class CoinPaprikaRepository implements CexRepository { DateTime endAt, Duration batchDuration, ) async { - final apiPlan = coinPaprikaProvider.apiPlan; final allOhlcData = []; var currentStart = startAt; - _logger.info( - 'Splitting OHLC request for $tradingSymbol into multiple batches ' - '(${apiPlan.planName} plan: ${apiPlan.ohlcLimitDescription}) ' - 'with ${batchDuration.inDays}-day batches ' - 'from ${startAt.toIso8601String()} to ${endAt.toIso8601String()}', - ); - while (currentStart.isBefore(endAt)) { final batchEnd = currentStart.add(batchDuration); final actualEnd = batchEnd.isAfter(endAt) ? endAt : batchEnd; @@ -244,18 +201,14 @@ class CoinPaprikaRepository implements CexRepository { // Ensure batch duration doesn't exceed our chosen batch size if (actualBatchDuration > batchDuration) { - throw ArgumentError( + throw ArgumentError.value( + actualBatchDuration, + 'actualBatchDuration', 'Batch duration ${actualBatchDuration.inDays} days ' - 'exceeds safe limit of ${batchDuration.inDays} days', + 'exceeds safe limit of ${batchDuration.inDays} days', ); } - _logger.fine( - 'Fetching batch: ${currentStart.toIso8601String()} to ' - '${actualEnd.toIso8601String()} ' - '(duration: ${actualBatchDuration.inDays} days)', - ); - try { final batchOhlc = await _fetchSingleOhlcRequest( tradingSymbol, @@ -265,7 +218,6 @@ class CoinPaprikaRepository implements CexRepository { ); allOhlcData.addAll(batchOhlc.ohlc); - _logger.fine('Batch successful: ${batchOhlc.ohlc.length} data points'); } catch (e) { _logger.warning( 'Failed to fetch batch ${currentStart.toIso8601String()} to ' @@ -282,10 +234,6 @@ class CoinPaprikaRepository implements CexRepository { } } - _logger.info( - 'Successfully fetched ${allOhlcData.length} OHLC data points across ' - 'multiple batches for $tradingSymbol', - ); return CoinOhlc(ohlc: allOhlcData); } @@ -326,48 +274,43 @@ class CoinPaprikaRepository implements CexRepository { DateTime? priceDate, QuoteCurrency fiatCurrency = Stablecoin.usdt, }) async { - try { - final tradingSymbol = resolveTradingSymbol(assetId); - final quoteCurrencyId = fiatCurrency.coinPaprikaId.toUpperCase(); - - if (priceDate != null) { - // For historical prices, use OHLC data - final endDate = priceDate.add(const Duration(hours: 1)); - final ohlcData = await getCoinOhlc( - assetId, - fiatCurrency, - GraphInterval.oneHour, - startAt: priceDate, - endAt: endDate, - ); - - if (ohlcData.ohlc.isEmpty) { - throw Exception( - 'No price data available for ${assetId.id} at $priceDate', - ); - } - - return ohlcData.ohlc.first.closeDecimal; - } + final tradingSymbol = resolveTradingSymbol(assetId); + final quoteCurrencyId = fiatCurrency.coinPaprikaId.toUpperCase(); - // For current prices, use ticker endpoint - final ticker = await coinPaprikaProvider.fetchCoinTicker( - coinId: tradingSymbol, - quotes: [fiatCurrency], + if (priceDate != null) { + // For historical prices, use OHLC data + final endDate = priceDate.add(const Duration(hours: 1)); + final ohlcData = await getCoinOhlc( + assetId, + fiatCurrency, + GraphInterval.oneHour, + startAt: priceDate, + endAt: endDate, ); - final quoteData = ticker.quotes[quoteCurrencyId]; - if (quoteData == null) { + if (ohlcData.ohlc.isEmpty) { throw Exception( - 'No price data found for ${assetId.id} in $quoteCurrencyId', + 'No price data available for ${assetId.id} at $priceDate', ); } - return Decimal.parse(quoteData.price.toString()); - } catch (e, stackTrace) { - _logger.severe('Failed to get price for ${assetId.id}', e, stackTrace); - rethrow; + return ohlcData.ohlc.first.closeDecimal; } + + // For current prices, use ticker endpoint + final ticker = await coinPaprikaProvider.fetchCoinTicker( + coinId: tradingSymbol, + quotes: [fiatCurrency], + ); + + final quoteData = ticker.quotes[quoteCurrencyId]; + if (quoteData == null) { + throw Exception( + 'No price data found for ${assetId.id} in $quoteCurrencyId', + ); + } + + return Decimal.parse(quoteData.price.toString()); } @override @@ -376,56 +319,48 @@ class CoinPaprikaRepository implements CexRepository { List dates, { QuoteCurrency fiatCurrency = Stablecoin.usdt, }) async { - try { - if (dates.isEmpty) { - _logger.warning( - 'No dates provided for price retrieval of ${assetId.id}', - ); - return {}; - } + if (dates.isEmpty) { + return {}; + } - final sortedDates = List.from(dates)..sort(); - final startDate = sortedDates.first.subtract(const Duration(hours: 1)); - final endDate = sortedDates.last.add(const Duration(hours: 1)); + final sortedDates = List.from(dates)..sort(); + final startDate = sortedDates.first.subtract(const Duration(hours: 1)); + final endDate = sortedDates.last.add(const Duration(hours: 1)); - final ohlcData = await getCoinOhlc( - assetId, - fiatCurrency, - GraphInterval.oneDay, - startAt: startDate, - endAt: endDate, - ); + final ohlcData = await getCoinOhlc( + assetId, + fiatCurrency, + GraphInterval.oneDay, + startAt: startDate, + endAt: endDate, + ); - final result = {}; - - // Match OHLC data to requested dates - for (final date in dates) { - final dayStart = DateTime.utc(date.year, date.month, date.day); - final dayEnd = dayStart.add(const Duration(days: 1)).toUtc(); - - // Find the closest OHLC data point - Ohlc? closestOhlc; - for (final ohlc in ohlcData.ohlc) { - final ohlcDate = DateTime.fromMillisecondsSinceEpoch( - ohlc.closeTimeMs, - isUtc: true, - ); - if (!ohlcDate.isBefore(dayStart) && ohlcDate.isBefore(dayEnd)) { - closestOhlc = ohlc; - break; - } - } + final result = {}; - if (closestOhlc != null) { - result[date] = closestOhlc.closeDecimal; + // Match OHLC data to requested dates + for (final date in dates) { + final dayStart = DateTime.utc(date.year, date.month, date.day); + final dayEnd = dayStart.add(const Duration(days: 1)).toUtc(); + + // Find the closest OHLC data point + Ohlc? closestOhlc; + for (final ohlc in ohlcData.ohlc) { + final ohlcDate = DateTime.fromMillisecondsSinceEpoch( + ohlc.closeTimeMs, + isUtc: true, + ); + if (!ohlcDate.isBefore(dayStart) && ohlcDate.isBefore(dayEnd)) { + closestOhlc = ohlc; + break; } } - return result; - } catch (e, stackTrace) { - _logger.severe('Failed to get prices for ${assetId.id}', e, stackTrace); - rethrow; + if (closestOhlc != null) { + result[date] = closestOhlc.closeDecimal; + } } + + return result; } @override @@ -433,32 +368,23 @@ class CoinPaprikaRepository implements CexRepository { AssetId assetId, { QuoteCurrency fiatCurrency = Stablecoin.usdt, }) async { - try { - final tradingSymbol = resolveTradingSymbol(assetId); - final quoteCurrencyId = fiatCurrency.coinPaprikaId.toUpperCase(); - - // Use ticker endpoint for 24hr price change - final ticker = await coinPaprikaProvider.fetchCoinTicker( - coinId: tradingSymbol, - quotes: [fiatCurrency], - ); + final tradingSymbol = resolveTradingSymbol(assetId); + final quoteCurrencyId = fiatCurrency.coinPaprikaId.toUpperCase(); - final quoteData = ticker.quotes[quoteCurrencyId]; - if (quoteData == null) { - throw Exception( - 'No price change data found for ${assetId.id} in $quoteCurrencyId', - ); - } + // Use ticker endpoint for 24hr price change + final ticker = await coinPaprikaProvider.fetchCoinTicker( + coinId: tradingSymbol, + quotes: [fiatCurrency], + ); - return Decimal.parse(quoteData.percentChange24h.toString()); - } catch (e, stackTrace) { - _logger.severe( - 'Failed to get 24hr price change for ${assetId.id}', - e, - stackTrace, + final quoteData = ticker.quotes[quoteCurrencyId]; + if (quoteData == null) { + throw Exception( + 'No price change data found for ${assetId.id} in $quoteCurrencyId', ); - rethrow; } + + return Decimal.parse(quoteData.percentChange24h.toString()); } @override diff --git a/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart b/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart index c6694d70..11b85795 100644 --- a/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart +++ b/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart @@ -36,7 +36,7 @@ class BinanceIdResolutionStrategy implements IdResolutionStrategy { final configSymbol = assetId.symbol.configSymbol; if (binanceId == null || binanceId.isEmpty) { - _logger.warning( + _logger.fine( 'Missing binanceId for asset ${assetId.symbol.configSymbol}, ' 'falling back to configSymbol. This may cause API issues.', ); @@ -90,7 +90,7 @@ class CoinGeckoIdResolutionStrategy implements IdResolutionStrategy { final coinGeckoId = assetId.symbol.coinGeckoId; if (coinGeckoId == null || coinGeckoId.isEmpty) { - _logger.warning( + _logger.fine( 'Missing coinGeckoId for asset ${assetId.symbol.configSymbol}, ' 'falling back to configSymbol. This may cause API issues.', ); @@ -143,7 +143,7 @@ class CoinPaprikaIdResolutionStrategy implements IdResolutionStrategy { final coinPaprikaId = assetId.symbol.coinPaprikaId; if (coinPaprikaId == null || coinPaprikaId.isEmpty) { - _logger.warning( + _logger.fine( 'Missing coinPaprikaId for asset ${assetId.symbol.configSymbol}. ' 'CoinPaprika API cannot be used for this asset.', ); diff --git a/packages/komodo_cex_market_data/lib/src/repository_fallback_mixin.dart b/packages/komodo_cex_market_data/lib/src/repository_fallback_mixin.dart index cae65735..70f6b7dc 100644 --- a/packages/komodo_cex_market_data/lib/src/repository_fallback_mixin.dart +++ b/packages/komodo_cex_market_data/lib/src/repository_fallback_mixin.dart @@ -109,7 +109,7 @@ mixin RepositoryFallbackMixin { _repositoryFailureCounts[repoType] = (_repositoryFailureCounts[repoType] ?? 0) + 1; - _logger.info( + _logger.fine( 'Repository ${repo.runtimeType} failure recorded ' '(count: ${_repositoryFailureCounts[repoType]})', ); @@ -150,9 +150,7 @@ mixin RepositoryFallbackMixin { final healthyRepos = priceRepositories.where(_isRepositoryHealthy).toList(); if (healthyRepos.isEmpty) { - _logger.warning( - 'No healthy repositories available, using all repositories', - ); + _logger.fine('No healthy repositories available, using all repositories'); // Even when no healthy repos, still filter by support final supportingRepos = []; for (final repo in priceRepositories) { @@ -299,7 +297,7 @@ mixin RepositoryFallbackMixin { _recordRepositorySuccess(repo); if (attemptCount > 1) { - _logger.info( + _logger.fine( 'Successfully fetched $operationName for ' '${assetId.symbol.assetConfigId} ' 'using repository ${repo.runtimeType} on attempt $attemptCount', @@ -333,7 +331,6 @@ mixin RepositoryFallbackMixin { } } - // All attempts exhausted _logger.warning( 'All $attemptCount attempts failed for $operationName ' '${assetId.symbol.assetConfigId}', @@ -362,12 +359,7 @@ mixin RepositoryFallbackMixin { maxTotalAttempts: maxTotalAttempts, ); } catch (e, s) { - _logger - ..fine( - 'All attempts failed for $operationName ' - '${assetId.symbol.configSymbol}', - ) - ..finest('Stack trace: $s'); + _logger.finest('Stack trace: $s'); return null; } } diff --git a/packages/komodo_cex_market_data/lib/src/repository_priority_manager.dart b/packages/komodo_cex_market_data/lib/src/repository_priority_manager.dart index bfddca31..58809161 100644 --- a/packages/komodo_cex_market_data/lib/src/repository_priority_manager.dart +++ b/packages/komodo_cex_market_data/lib/src/repository_priority_manager.dart @@ -62,8 +62,8 @@ class RepositoryPriorityManager { /// [repositories] The list of repositories to sort. /// Returns a new sorted list with highest priority repositories first. static List sortByPriority(List repositories) { - final sorted = repositories.toList(); - sorted.sort((a, b) => getPriority(a).compareTo(getPriority(b))); + final sorted = repositories.toList() + ..sort((a, b) => getPriority(a).compareTo(getPriority(b))); return sorted; } diff --git a/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart b/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart index 57be4ab0..0cc3b660 100644 --- a/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart +++ b/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart @@ -76,7 +76,7 @@ class DefaultRepositorySelectionStrategy } } catch (e, st) { // Log errors but continue with other repositories - _logger.warning( + _logger.fine( 'Failed to check support for ${repo.runtimeType} with asset ' '${assetId.id} and fiat ${fiatCurrency.symbol} (requestType: $requestType)', e, diff --git a/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart b/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart index da5d13c8..1d1b8c00 100644 --- a/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/sparkline_repository.dart @@ -250,7 +250,7 @@ class SparklineRepository with RepositoryFallbackMixin { // If all repositories failed, cache null result to avoid repeated attempts final failedCacheData = SparklineData.failed(); await _box!.put(symbol, failedCacheData); - _logger.warning( + _logger.fine( 'All repositories failed fetching sparkline for $symbol; cached null', ); return null; @@ -288,7 +288,7 @@ class SparklineRepository with RepositoryFallbackMixin { try { final raw = _box!.get(symbol); if (raw is! SparklineData) { - _logger.warning( + _logger.fine( 'Cache entry for $symbol has unexpected type: ${raw.runtimeType}; ' 'Clearing entry and skipping', ); @@ -308,7 +308,7 @@ class SparklineRepository with RepositoryFallbackMixin { return List.unmodifiable(data); } } catch (e, s) { - _logger.severe('Error reading cache for $symbol', e, s); + _logger.warning('Error reading cache for $symbol', e, s); } _logger.fine('Cache hit (typed) for $symbol but data null (failed)'); diff --git a/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart b/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart index 66a1a76e..62cd9f3c 100644 --- a/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart +++ b/packages/komodo_cex_market_data/test/binance/binance_repository_test.dart @@ -76,7 +76,6 @@ void main() { Stablecoin.tusd, // TUSD Stablecoin.usdp, // USDP Stablecoin.dai, // DAI - Stablecoin.frax, // FRAX Stablecoin.lusd, // LUSD Stablecoin.gusd, // GUSD Stablecoin.susd, // SUSD @@ -1190,7 +1189,6 @@ void main() { 'TUSD', 'USDP', 'DAI', - 'FRAX', 'LUSD', 'GUSD', 'SUSD', diff --git a/packages/komodo_coin_updates/test/hive/asset_adapter_roundtrip_test.dart b/packages/komodo_coin_updates/test/hive/asset_adapter_roundtrip_test.dart index f2580ec1..a86f97fb 100644 --- a/packages/komodo_coin_updates/test/hive/asset_adapter_roundtrip_test.dart +++ b/packages/komodo_coin_updates/test/hive/asset_adapter_roundtrip_test.dart @@ -78,8 +78,8 @@ void main() { expect(readBack, isNotNull); expect(readBack!.id.id, equals(key)); expect(readBack.id.name, equals('Komodo')); - expect(readBack.id.subClass, equals(CoinSubClass.utxo)); - expect(readBack.protocol.subClass, equals(CoinSubClass.utxo)); + expect(readBack.id.subClass, equals(CoinSubClass.smartChain)); + expect(readBack.protocol.subClass, equals(CoinSubClass.smartChain)); expect(readBack.isWalletOnly, isFalse); expect(readBack.signMessagePrefix, isNull); }); diff --git a/packages/komodo_coins/test/strategic_coin_config_manager_test.dart b/packages/komodo_coins/test/strategic_coin_config_manager_test.dart index f72c6e8e..89c625c4 100644 --- a/packages/komodo_coins/test/strategic_coin_config_manager_test.dart +++ b/packages/komodo_coins/test/strategic_coin_config_manager_test.dart @@ -76,10 +76,12 @@ void main() { }); // Set up source loading - when(() => mockStorageSource.loadAssets()) - .thenAnswer((_) async => testAssets); - when(() => mockLocalSource.loadAssets()) - .thenAnswer((_) async => testAssets); + when( + () => mockStorageSource.loadAssets(), + ).thenAnswer((_) async => testAssets); + when( + () => mockLocalSource.loadAssets(), + ).thenAnswer((_) async => testAssets); }); group('Constructor', () { @@ -138,8 +140,9 @@ void main() { }); test('handles source availability check failures gracefully', () async { - when(() => mockStorageSource.isAvailable()) - .thenThrow(Exception('Availability check failed')); + when( + () => mockStorageSource.isAvailable(), + ).thenThrow(Exception('Availability check failed')); final manager = StrategicCoinConfigManager( configSources: [mockStorageSource, mockLocalSource], @@ -152,8 +155,9 @@ void main() { }); test('handles source loading failures gracefully', () async { - when(() => mockStorageSource.loadAssets()) - .thenThrow(Exception('Load failed')); + when( + () => mockStorageSource.loadAssets(), + ).thenThrow(Exception('Load failed')); final manager = StrategicCoinConfigManager( configSources: [mockStorageSource, mockLocalSource], @@ -200,10 +204,12 @@ void main() { test('deduplicates assets from multiple sources', () async { // Set up sources to return the same assets - when(() => mockStorageSource.loadAssets()) - .thenAnswer((_) async => testAssets); - when(() => mockLocalSource.loadAssets()) - .thenAnswer((_) async => testAssets); + when( + () => mockStorageSource.loadAssets(), + ).thenAnswer((_) async => testAssets); + when( + () => mockLocalSource.loadAssets(), + ).thenAnswer((_) async => testAssets); final dedupManager = StrategicCoinConfigManager( configSources: [mockStorageSource, mockLocalSource], @@ -234,8 +240,11 @@ void main() { expect(filtered, isNotEmpty); // All test assets should be UTXO type expect( - filtered.values - .every((asset) => asset.id.subClass == CoinSubClass.utxo), + filtered.values.every( + (asset) => + asset.id.subClass == CoinSubClass.utxo || + asset.id.subClass == CoinSubClass.smartChain, + ), isTrue, ); }); @@ -254,10 +263,12 @@ void main() { final noTrezorAsset = Asset.fromJson(noTrezorConfig); // Set up source to return only the no-trezor asset - when(() => mockStorageSource.loadAssets()) - .thenAnswer((_) async => [noTrezorAsset]); - when(() => mockLocalSource.loadAssets()) - .thenAnswer((_) async => [noTrezorAsset]); + when( + () => mockStorageSource.loadAssets(), + ).thenAnswer((_) async => [noTrezorAsset]); + when( + () => mockLocalSource.loadAssets(), + ).thenAnswer((_) async => [noTrezorAsset]); final noTrezorManager = StrategicCoinConfigManager( configSources: [mockStorageSource, mockLocalSource], @@ -294,11 +305,11 @@ void main() { }); test('finds asset by ticker and subclass', () { - final found = manager.findByTicker('KMD', CoinSubClass.utxo); + final found = manager.findByTicker('KMD', CoinSubClass.smartChain); expect(found, isNotNull); expect(found!.id.id, equals('KMD')); - expect(found.id.subClass, equals(CoinSubClass.utxo)); + expect(found.id.subClass, equals(CoinSubClass.smartChain)); }); test('returns null when asset not found', () { @@ -356,8 +367,9 @@ void main() { }); test('handles refresh failures gracefully', () async { - when(() => mockStorageSource.loadAssets()) - .thenThrow(Exception('Refresh failed')); + when( + () => mockStorageSource.loadAssets(), + ).thenThrow(Exception('Refresh failed')); final initialAssets = Map.from(manager.all);