diff --git a/lib/packages/service/dfx/dfx_price_service.dart b/lib/packages/service/dfx/dfx_price_service.dart index dba6fe7b..462dc522 100644 --- a/lib/packages/service/dfx/dfx_price_service.dart +++ b/lib/packages/service/dfx/dfx_price_service.dart @@ -29,19 +29,18 @@ class DFXPriceService extends APriceService { final result = []; for (final entry in body) { - BigInt price; - switch (currency) { - case Currency.eur: - price = BigInt.from(entry['eur'] * 100); - break; - case Currency.chf: - price = BigInt.from(entry['chf'] * 100); - break; - } + final rawPrice = switch (currency) { + Currency.eur => entry['eur'], + Currency.chf => entry['chf'], + }; + // The API omits the price for points it cannot quote (e.g. the latest + // point while the quote is unavailable). Skip them instead of throwing, + // which would otherwise discard the entire chart. + if (rawPrice == null) continue; result.add( PricePoint( asset: asset, - price: price, + price: BigInt.from(rawPrice * 100), time: DateTime.parse(entry['timestamp']), ), ); @@ -59,12 +58,14 @@ class DFXPriceService extends APriceService { final body = jsonDecode(response.body); - switch (currency) { - case Currency.eur: - return BigInt.from(body['eur'] * 100); - case Currency.chf: - return BigInt.from(body['chf'] * 100); - } + final rawPrice = switch (currency) { + Currency.eur => body['eur'], + Currency.chf => body['chf'], + }; + // A missing price means the quote is currently unavailable. Return zero so + // the UI renders "--.--" instead of throwing. + if (rawPrice == null) return BigInt.zero; + return BigInt.from(rawPrice * 100); } /// Returns the equivalent EUR amount for 1 CHF @@ -75,8 +76,8 @@ class DFXPriceService extends APriceService { if (response.statusCode != 200) throw Exception(response.body); final body = jsonDecode(response.body); - final chf = (body['chf'] as num).toDouble(); - final eur = (body['eur'] as num).toDouble(); + final chf = (body['chf'] as num?)?.toDouble() ?? 0; + final eur = (body['eur'] as num?)?.toDouble() ?? 0; return chf > 0 ? eur / chf : 0.0; } diff --git a/test/packages/service/dfx/dfx_price_service_test.dart b/test/packages/service/dfx/dfx_price_service_test.dart index 81bbbb0a..434e15b6 100644 --- a/test/packages/service/dfx/dfx_price_service_test.dart +++ b/test/packages/service/dfx/dfx_price_service_test.dart @@ -60,6 +60,20 @@ void main() { throwsException, ); }); + + test('returns zero when the price is missing (quote unavailable)', () async { + // The live endpoint omits chf/eur while the quote is unavailable, + // e.g. {"timestamp": "..."}. Must return zero (UI shows "--.--"), + // not throw on null * 100. + final client = MockClient((_) async => http.Response( + jsonEncode({'timestamp': '2026-06-04T22:28:16.539Z'}), + 200, + )); + + final price = await build(client).getPriceOfAsset(realUnitAsset, Currency.chf); + + expect(price, BigInt.zero); + }); }); group('getPriceChart', () { @@ -107,6 +121,25 @@ void main() { throwsException, ); }); + + test('skips entries whose price is missing instead of discarding the whole chart', () async { + // A single trailing point without chf/eur must not wipe the entire + // chart — the prior valued points are kept. + final client = MockClient((_) async => http.Response( + jsonEncode([ + {'chf': 1.0, 'eur': 0.95, 'timestamp': '2026-01-01T00:00:00Z'}, + {'chf': 2.0, 'eur': 1.90, 'timestamp': '2026-01-02T00:00:00Z'}, + {'timestamp': '2026-01-03T00:00:00Z'}, + ]), + 200, + )); + + final points = await build(client).getPriceChart(realUnitAsset, Currency.chf); + + expect(points, hasLength(2)); + expect(points.last.price, BigInt.from(200)); + expect(points.last.time, DateTime.utc(2026, 1, 2)); + }); }); group('getChfToEurRate', () { @@ -131,6 +164,17 @@ void main() { expect(rate, 0.0); }); + + test('returns 0.0 when the price is missing (quote unavailable)', () async { + final client = MockClient((_) async => http.Response( + jsonEncode({'timestamp': '2026-06-04T22:28:16.539Z'}), + 200, + )); + + final rate = await build(client).getChfToEurRate(); + + expect(rate, 0.0); + }); }); }); }