Skip to content

Commit 29917cd

Browse files
fix(transactions): fall back to the payjoin txid when resolving an order
1 parent 8cb1eb2 commit 29917cd

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

lib/features/transactions/presentation/blocs/transaction_details/transaction_details_cubit.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,10 @@ class TransactionDetailsCubit extends Cubit<TransactionDetailsState> {
804804

805805
// Check if a transaction with the same transaction ID can be found
806806
// and initialize the transaction details with it.
807-
final txId = order.transactionId;
807+
// The payjoin txid is the fallback: the exchange can know the payjoin it
808+
// broadcast before it reports its own payout txid, and without this the
809+
// screen would sit on order-only details until that txid lands.
810+
final txId = order.transactionId ?? order.payjoin?.txid;
808811
if (txId != null) {
809812
try {
810813
final txs = await _getTransactionsByTxIdUsecase.execute(txId);

test/features/transactions/presentation/blocs/transaction_details/transaction_details_cubit_test.dart

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:bb_mobile/core/entities/signer_entity.dart' show SignerEntity;
4+
import 'package:bb_mobile/core/exchange/domain/entity/order.dart';
45
import 'package:bb_mobile/core/exchange/domain/usecases/get_order_usercase.dart';
56
import 'package:bb_mobile/core/swaps/domain/usecases/get_swap_usecase.dart';
67
import 'package:bb_mobile/core/swaps/domain/usecases/watch_swap_usecase.dart';
@@ -145,6 +146,7 @@ void main() {
145146
late _MockBroadcastOriginalTransactionUsecase broadcastOriginalTransaction;
146147
late _MockGetTransactionOrderSwapUsecase getTransactionOrderSwap;
147148
late _MockWatchTransactionOrderSwapUsecase watchTransactionOrderSwap;
149+
late _MockGetOrderUsecase getOrder;
148150

149151
TransactionDetailsCubit buildCubit() => TransactionDetailsCubit(
150152
getWalletUsecase: getWallet,
@@ -155,7 +157,7 @@ void main() {
155157
getSwapUsecase: _MockGetSwapUsecase(),
156158
getPayjoinByIdUsecase: getPayjoinById,
157159
getPayjoinByTxIdUsecase: getPayjoinByTxId,
158-
getOrderUsecase: _MockGetOrderUsecase(),
160+
getOrderUsecase: getOrder,
159161
watchSwapUsecase: _MockWatchSwapUsecase(),
160162
watchPayjoinUsecase: watchPayjoin,
161163
watchTransactionOrderSwapUsecase: watchTransactionOrderSwap,
@@ -178,6 +180,7 @@ void main() {
178180
broadcastOriginalTransaction = _MockBroadcastOriginalTransactionUsecase();
179181
getTransactionOrderSwap = _MockGetTransactionOrderSwapUsecase();
180182
watchTransactionOrderSwap = _MockWatchTransactionOrderSwapUsecase();
183+
getOrder = _MockGetOrderUsecase();
181184

182185
when(() => broadcastOriginalTransaction.canExecute(any())).thenAnswer((
183186
invocation,
@@ -1313,8 +1316,97 @@ void main() {
13131316
},
13141317
);
13151318
});
1319+
1320+
group('TransactionDetailsCubit.initByOrderId transaction resolution', () {
1321+
// _loadDetailsByOrderId looks the txid up, then hands it to
1322+
// initByWalletTxId, which looks it up again — so the usecase is called
1323+
// more than once per init. What matters is which txid was asked for, not
1324+
// how often, hence capture over a call count.
1325+
void expectResolvedTxIds(String txId) {
1326+
final resolved = verify(
1327+
() => getTransactionsByTxId.execute(captureAny()),
1328+
).captured;
1329+
expect(resolved, isNotEmpty);
1330+
expect(resolved, everyElement(txId));
1331+
}
1332+
1333+
test('resolves the payjoin txid when the order has no txid yet', () async {
1334+
// The exchange can know the payjoin it broadcast before it reports its
1335+
// own payout txid. Without the fallback the screen stays on order-only
1336+
// details, with no payjoin shown and no watcher armed.
1337+
final order = _buyOrder(payjoinTxId: 'payjoin-txid');
1338+
when(
1339+
() => getOrder.execute(orderId: 'order-1'),
1340+
).thenAnswer((_) async => order);
1341+
when(() => getTransactionsByTxId.execute(any())).thenAnswer(
1342+
(_) async => [Transaction(payjoin: _receiver(), order: order)],
1343+
);
1344+
1345+
final cubit = buildCubit();
1346+
addTearDown(cubit.close);
1347+
await cubit.initByOrderId('order-1');
1348+
1349+
expectResolvedTxIds('payjoin-txid');
1350+
});
1351+
1352+
test('prefers the order txid when both are known', () async {
1353+
final order = _buyOrder(
1354+
bitcoinTransactionId: 'payout-txid',
1355+
payjoinTxId: 'payjoin-txid',
1356+
);
1357+
when(
1358+
() => getOrder.execute(orderId: 'order-1'),
1359+
).thenAnswer((_) async => order);
1360+
when(() => getTransactionsByTxId.execute(any())).thenAnswer(
1361+
(_) async => [Transaction(payjoin: _receiver(), order: order)],
1362+
);
1363+
1364+
final cubit = buildCubit();
1365+
addTearDown(cubit.close);
1366+
await cubit.initByOrderId('order-1');
1367+
1368+
expectResolvedTxIds('payout-txid');
1369+
});
1370+
1371+
test('falls back to order-only details when no txid is known', () async {
1372+
final order = _buyOrder();
1373+
when(
1374+
() => getOrder.execute(orderId: 'order-1'),
1375+
).thenAnswer((_) async => order);
1376+
1377+
final cubit = buildCubit();
1378+
addTearDown(cubit.close);
1379+
await cubit.initByOrderId('order-1');
1380+
1381+
verifyNever(() => getTransactionsByTxId.execute(any()));
1382+
expect(cubit.state.transaction?.order, order);
1383+
});
1384+
});
13161385
}
13171386

1387+
Order _buyOrder({String? bitcoinTransactionId, String? payjoinTxId}) =>
1388+
Order.buy(
1389+
orderId: 'order-1',
1390+
orderType: OrderType.buy,
1391+
message: OrderMessage(code: '', message: ''),
1392+
orderNumber: 1,
1393+
payinAmount: 100,
1394+
payinCurrency: 'CAD',
1395+
payoutAmount: 0.001,
1396+
payoutCurrency: 'BTC',
1397+
payinMethod: OrderPaymentMethod.eTransfer,
1398+
payoutMethod: OrderPaymentMethod.bitcoin,
1399+
orderStatus: OrderStatus.inProgress,
1400+
payinStatus: OrderPayinStatus.completed,
1401+
payoutStatus: OrderPayoutStatus.completed,
1402+
createdAt: DateTime.utc(2026, 8, 19),
1403+
bitcoinTransactionId: bitcoinTransactionId,
1404+
payjoinDetails: payjoinTxId == null
1405+
? null
1406+
: OrderPayjoinDetails(txid: payjoinTxId),
1407+
isTestnet: false,
1408+
);
1409+
13181410
OrderSwapRecord _receiveOrderSwap() {
13191411
final createdAt = DateTime.utc(2026, 8, 10);
13201412
return OrderSwapRecord(

0 commit comments

Comments
 (0)