Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,21 @@ class BullbitcoinApiDatasource implements BitcoinPriceDatasource {
final elements = resp.data['result']['elements'] as List<dynamic>?;
if (elements == null) return [];
return elements
.map((e) => OrderModel.fromJson(e as Map<String, dynamic>))
.map((e) {
// Parse each order on its own so one malformed element doesn't cost
// the user their whole order history. Nulls are filtered out below.
try {
return OrderModel.fromJson(e as Map<String, dynamic>);
} catch (err, stackTrace) {
log.severe(
message: 'Error parsing order element',
error: err,
trace: stackTrace,
);
return null;
}
})
.whereType<OrderModel>()
.toList();
}

Expand Down
72 changes: 53 additions & 19 deletions lib/core/exchange/data/models/order_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ class OrderModel {
final String orderType;
final String? orderSubtype;
final int orderNumber;
final double exchangeRateAmount;
final String exchangeRateCurrency;
final double? exchangeRateAmount;
final String? exchangeRateCurrency;
final double? indexRateAmount;
final String? indexRateCurrency;
final double payinAmount;
Expand All @@ -24,7 +24,7 @@ class OrderModel {
final String payinMethod;
final String payoutMethod;
final String triggerType;
final String confirmationDeadline;
final String? confirmationDeadline;
final String? bitcoinTransactionId;
final String? lnUrl;
final String? lightningVoucherExpiresAt;
Expand Down Expand Up @@ -52,8 +52,8 @@ class OrderModel {
required this.orderType,
this.orderSubtype,
required this.orderNumber,
required this.exchangeRateAmount,
required this.exchangeRateCurrency,
this.exchangeRateAmount,
this.exchangeRateCurrency,
this.indexRateAmount,
this.indexRateCurrency,
required this.payinAmount,
Expand All @@ -71,7 +71,7 @@ class OrderModel {
required this.payinMethod,
required this.payoutMethod,
required this.triggerType,
required this.confirmationDeadline,
this.confirmationDeadline,
this.bitcoinTransactionId,
this.lnUrl,
this.lightningVoucherExpiresAt,
Expand Down Expand Up @@ -101,26 +101,30 @@ class OrderModel {
orderType: json['orderType'] as String,
orderSubtype: json['orderSubtype'] as String?,
orderNumber: json['orderNumber'] as int,
exchangeRateAmount: (json['exchangeRateAmount'] as num).toDouble(),
exchangeRateCurrency: json['exchangeRateCurrency'] as String,
// Nullable per the server contract: reward, funding, refund and
// balance-adjustment orders carry no exchange rate and no deadline. The
// amounts and the status strings are read defensively for the same
// reason — an admin-initiated order has no real pay-in side.
exchangeRateAmount: (json['exchangeRateAmount'] as num?)?.toDouble(),
exchangeRateCurrency: json['exchangeRateCurrency'] as String?,
indexRateAmount: (json['indexRateAmount'] as num?)?.toDouble(),
indexRateCurrency: json['indexRateCurrency'] as String?,
payinAmount: (json['payinAmount'] as num).toDouble(),
payinCurrency: json['payinCurrency'] as String,
payoutAmount: (json['payoutAmount'] as num).toDouble(),
payoutCurrency: json['payoutCurrency'] as String,
orderStatus: json['orderStatus'] as String,
payinStatus: json['payinStatus'] as String,
payoutStatus: json['payoutStatus'] as String,
payinAmount: (json['payinAmount'] as num?)?.toDouble() ?? 0,
payinCurrency: json['payinCurrency'] as String? ?? '',
payoutAmount: (json['payoutAmount'] as num?)?.toDouble() ?? 0,
payoutCurrency: json['payoutCurrency'] as String? ?? '',
orderStatus: json['orderStatus'] as String? ?? '',
payinStatus: json['payinStatus'] as String? ?? '',
payoutStatus: json['payoutStatus'] as String? ?? '',
scheduledPayoutTime: json['scheduledPayoutTime'] as String?,
createdAt: json['createdAt'] as String,
completedAt: json['completedAt'] as String?,
message: json['message'] as Map<String, dynamic>?,
sentAt: json['sentAt'] as String?,
payinMethod: json['payinMethod'] as String,
payoutMethod: json['payoutMethod'] as String,
payinMethod: json['payinMethod'] as String? ?? '',
payoutMethod: json['payoutMethod'] as String? ?? '',
triggerType: json['triggerType'] as String,
confirmationDeadline: json['confirmationDeadline'] as String,
confirmationDeadline: json['confirmationDeadline'] as String?,
bitcoinTransactionId: json['bitcoinTransactionId'] as String?,
lnUrl: json['lnUrl'] as String?,
lightningVoucherExpiresAt: json['lightningVoucherExpiresAt'] as String?,
Expand Down Expand Up @@ -209,7 +213,9 @@ class OrderModel {
final orderStatusEnum = OrderStatus.fromValue(orderStatus);
final payinStatusEnum = OrderPayinStatus.fromValue(payinStatus);
final payoutStatusEnum = OrderPayoutStatus.fromValue(payoutStatus);
final confirmationDeadlineDt = DateTime.parse(confirmationDeadline);
final confirmationDeadlineDt = confirmationDeadline != null
? DateTime.tryParse(confirmationDeadline!)
: null;
final createdAtDt = DateTime.parse(createdAt);
final completedAtDt =
completedAt != null ? DateTime.tryParse(completedAt!) : null;
Expand Down Expand Up @@ -534,6 +540,34 @@ class OrderModel {
sentAt: sentAtDt,
isTestnet: isTestnet,
);
case OrderType.sellUsdt:
case OrderType.unknown:
return Order.generic(
orderId: orderId,
orderType: orderTypeEnum,
orderTypeName: orderType,
orderSubtype: orderSubtype,
message: orderMsg,
orderNumber: orderNumber,
payinAmount: payinAmount,
payinCurrency: payinCurrency,
payoutAmount: payoutAmount,
payoutCurrency: payoutCurrency,
exchangeRateAmount: exchangeRateAmount,
exchangeRateCurrency: exchangeRateCurrency,
payinMethod: payinMethodEnum,
payoutMethod: payoutMethodEnum,
orderStatus: orderStatusEnum,
payinStatus: payinStatusEnum,
payoutStatus: payoutStatusEnum,
confirmationDeadline: confirmationDeadlineDt,
createdAt: createdAtDt,
scheduledPayoutTime: scheduledPayoutTimeDt,
paymentDescription: paymentDescription,
completedAt: completedAtDt,
sentAt: sentAtDt,
isTestnet: isTestnet,
);
}
}
}
7 changes: 5 additions & 2 deletions lib/core/exchange/data/models/user_summary_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,11 @@ sealed class UserDcaModel with _$UserDcaModel {
'monthly' => DcaBuyFrequency.monthly,
_ => null,
},
currency:
currencyCode != null ? FiatCurrency.fromCode(currencyCode!) : null,
// A DCA currency this build doesn't support must not fail the whole user
// summary parse (balances, stats and preferences come with it).
currency: currencyCode != null
? FiatCurrency.tryFromCode(currencyCode!)
: null,
amount: amount,
network: switch (recipientType) {
'OUT_BITCOIN_ADDRESS' => DcaNetwork.bitcoin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:bb_mobile/core/exchange/domain/errors/sell_error.dart';
import 'package:bb_mobile/core/exchange/domain/errors/withdraw_error.dart';
import 'package:bb_mobile/core/exchange/domain/repositories/exchange_order_repository.dart';
import 'package:bb_mobile/core/utils/amount_conversions.dart';
import 'package:bb_mobile/core/utils/generic_extensions.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/features/dca/domain/dca.dart';

Expand Down Expand Up @@ -67,12 +68,14 @@ class ExchangeOrderRepositoryImpl implements ExchangeOrderRepository {
apiKey: apiKeyModel.key,
);

final orderModel = orderModels.firstWhere(
// A wallet transaction with no matching order is the normal case, not an
// error, so it must not be logged as one.
final orderModel = orderModels.firstWhereOrNull(
(model) =>
model.bitcoinTransactionId == txId ||
model.liquidTransactionId == txId,
orElse: () => throw Exception('Order not found for txId: $txId'),
);
if (orderModel == null) return null;

return orderModel.toEntity(isTestnet: _isTestnet);
} catch (e) {
Expand Down Expand Up @@ -104,8 +107,23 @@ class ExchangeOrderRepositoryImpl implements ExchangeOrderRepository {
apiKey: apiKeyModel.key,
);

// Map each order on its own. The catch-all below returns an empty list, so
// a single order the app can't map would otherwise cost the user all of
// them.
List<Order> orders = orderModels
.map((model) => model.toEntity(isTestnet: _isTestnet))
.map((model) {
try {
return model.toEntity(isTestnet: _isTestnet);
} catch (e, stackTrace) {
log.severe(
message: 'Error mapping order ${model.orderId}',
error: e,
trace: stackTrace,
);
return null;
}
})
.whereType<Order>()
.toList();

// this filtering should also be done separately, read from disk not over network
Expand All @@ -127,6 +145,10 @@ class ExchangeOrderRepositoryImpl implements ExchangeOrderRepository {
orders = orders.whereType<RefundOrder>().toList();
case OrderType.balanceAdjustment:
orders = orders.whereType<BalanceAdjustmentOrder>().toList();
case OrderType.sellUsdt:
case OrderType.unknown:
// Both map onto GenericOrder, so match on the type itself.
orders = orders.where((o) => o.orderType == type).toList();
}
}

Expand Down
Loading