@@ -3,10 +3,12 @@ import 'package:bb_mobile/core/exchange/domain/repositories/exchange_order_repos
33import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.dart' ;
44import 'package:bb_mobile/core/swaps/domain/entity/swap.dart' ;
55import 'package:bb_mobile/core/swaps/domain/repositories/swap_history_repository.dart' ;
6+ import 'package:bb_mobile/core/wallet/domain/entities/wallet_transaction.dart' ;
67import 'package:bb_mobile/core/wallet/domain/repositories/wallet_transaction_repository.dart' ;
8+ import 'package:bb_mobile/core/utils/amount_conversions.dart' ;
79import 'package:bb_mobile/core/utils/logger.dart' ;
8- import 'package:bb_mobile/features/transactions/application/usecases/label_exchange_orders_usecase.dart' ;
910import 'package:bb_mobile/features/transactions/domain/entities/transaction.dart' ;
11+ import 'package:bb_mobile/features/transactions/domain/transaction_failure.dart' ;
1012import 'package:bull_payjoin/bull_payjoin.dart' ;
1113import 'package:primitives/primitives.dart' ;
1214
@@ -17,7 +19,6 @@ class GetTransactionsUsecase {
1719 final PayjoinSessions _payjoinSessions;
1820 final ExchangeOrderRepository _mainnetExchangeOrderRepository;
1921 final ExchangeOrderRepository _testnetExchangeOrderRepository;
20- final LabelExchangeOrdersUsecase _labelExchangeOrdersUsecase;
2122
2223 GetTransactionsUsecase ({
2324 required this ._settingsRepository,
@@ -26,7 +27,6 @@ class GetTransactionsUsecase {
2627 required this ._payjoinSessions,
2728 required this ._mainnetExchangeOrderRepository,
2829 required this ._testnetExchangeOrderRepository,
29- required this ._labelExchangeOrdersUsecase,
3030 });
3131
3232 Future <List <Transaction >> execute ({
@@ -41,7 +41,6 @@ class GetTransactionsUsecase {
4141 : _mainnetExchangeOrderRepository;
4242
4343 final orders = await orderRepository.getOrders ();
44- await _labelExchangeOrdersUsecase.execute (orders: orders);
4544
4645 // Labels must exist before wallet transactions are hydrated.
4746 final (walletTransactions, payjoinResult, swaps) = await (
@@ -76,12 +75,16 @@ class GetTransactionsUsecase {
7675 final broadcastedTransactions = walletTransactions.map ((wt) {
7776 Swap ? swap;
7877 try {
79- swap = swaps.firstWhere (
80- (s) =>
78+ swap = swaps.firstWhere ((s) {
79+ final claimedLeg =
8180 (wt.isOutgoing && s.sendTxId == wt.txId) ||
8281 (wt.isIncoming &&
83- (s.receiveTxId == wt.txId || s.refundTxId == wt.txId)),
84- );
82+ (s.receiveTxId == wt.txId || s.refundTxId == wt.txId));
83+ return claimedLeg &&
84+ s.walletId == wt.walletId &&
85+ (s.amountSat == 0 || wt.amountSat <= s.amountSat) &&
86+ _transactionPaysSwapAddress (wt, s);
87+ });
8588 } catch (_) {
8689 // If no swap is found, it means the transaction is not a swap
8790 swap = null ;
@@ -107,7 +110,18 @@ class GetTransactionsUsecase {
107110
108111 Order ? order;
109112 try {
110- order = orders.firstWhere ((o) => o.transactionId == wt.txId);
113+ order = orders.firstWhere ((o) {
114+ if (o.transactionId != wt.txId) return false ;
115+ if (o.toAddress != null && o.toAddress != wt.toAddress) {
116+ return false ;
117+ }
118+ final expectedDirection = o is BuyOrder
119+ ? wt.isIncoming
120+ : o is SellOrder
121+ ? wt.isOutgoing
122+ : true ;
123+ return expectedDirection && _transactionCoversOrderAmount (wt, o);
124+ });
111125 // Remove the order from the list of orders to avoid duplication
112126 // since it's already included in the broadcasted transaction
113127 orders.remove (order);
@@ -196,7 +210,66 @@ class GetTransactionsUsecase {
196210 error: e,
197211 trace: stackTrace,
198212 );
199- throw Exception ('Failed to fetch transactions: $e ' );
213+ throw TransactionAggregationFailure (e.toString ());
214+ }
215+ }
216+
217+ /// The exchange tells us which address a completed order paid. Before the
218+ /// order is attached to one of the user's transactions, that transaction
219+ /// must actually have moved at least what the order claims — otherwise a
220+ /// wrong or hostile server figure gets attributed to unrelated funds.
221+ ///
222+ /// `>=` rather than `==` on purpose: payouts are batched, so one transaction
223+ /// can legitimately carry several orders and more sats than a single one.
224+ bool _transactionCoversOrderAmount (WalletTransaction wt, Order order) {
225+ final double declared;
226+ switch (order) {
227+ case BuyOrder (: final payoutAmount, : final payoutCurrency):
228+ if (payoutCurrency != 'BTC' ) return true ;
229+ declared = payoutAmount;
230+ case SellOrder (: final payinAmount, : final payinCurrency):
231+ if (payinCurrency != 'BTC' ) return true ;
232+ declared = payinAmount;
233+ default :
234+ return true ;
200235 }
236+ final declaredSat = ConvertAmount .btcToSats (declared);
237+ if (declaredSat <= 0 ) return true ;
238+ return wt.amountSat >= declaredSat;
239+ }
240+
241+ /// A swap leg is only this transaction's if the transaction actually pays
242+ /// the address the swap is built around. Matching on the server-reported
243+ /// txid alone lets a wrong id graft a swap onto an unrelated payment.
244+ bool _transactionPaysSwapAddress (WalletTransaction wt, Swap swap) {
245+ // Liquid outputs expose the unconfidential address while a swap address is
246+ // confidential, so the two are not comparable here; the txid + wallet +
247+ // amount checks remain. TODO(swaps): unblind before comparing.
248+ if (! wt.isBitcoin) return true ;
249+
250+ final expected = < String > {
251+ if (wt.isOutgoing)
252+ ...switch (swap) {
253+ LnSendSwap (: final paymentAddress) => [paymentAddress],
254+ ChainSwap (: final paymentAddress) => [paymentAddress],
255+ _ => < String > [],
256+ },
257+ if (wt.isIncoming)
258+ ...switch (swap) {
259+ LnReceiveSwap (: final receiveAddress) => [? receiveAddress],
260+ ChainSwap (: final receiveAddress, : final refundAddress) => [
261+ ? receiveAddress,
262+ ? refundAddress,
263+ ],
264+ LnSendSwap (: final refundAddress) => [? refundAddress],
265+ },
266+ };
267+ // Nothing to compare against (a recovered swap carries no address): fall
268+ // back to the other checks rather than dropping a legitimate leg.
269+ if (expected.isEmpty) return true ;
270+
271+ return wt.outputs.any (
272+ (output) => output.address != null && expected.contains (output.address),
273+ );
201274 }
202275}
0 commit comments