Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions lib/packages/service/dfx/dfx_price_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,18 @@ class DFXPriceService extends APriceService {
final result = <PricePoint>[];

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']),
),
);
Expand All @@ -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
Expand All @@ -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;
}
Expand Down
44 changes: 44 additions & 0 deletions test/packages/service/dfx/dfx_price_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -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', () {
Expand All @@ -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);
});
});
});
}
Loading