Skip to content

Commit abcdf11

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

2 files changed

Lines changed: 86 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: 82 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,86 @@ void main() {
13131316
},
13141317
);
13151318
});
1319+
1320+
group('TransactionDetailsCubit.initByOrderId transaction resolution', () {
1321+
test('resolves the payjoin txid when the order has no txid yet', () async {
1322+
// The exchange can know the payjoin it broadcast before it reports its
1323+
// own payout txid. Without the fallback the screen stays on order-only
1324+
// details, with no payjoin shown and no watcher armed.
1325+
final order = _buyOrder(payjoinTxId: 'payjoin-txid');
1326+
when(
1327+
() => getOrder.execute(orderId: 'order-1'),
1328+
).thenAnswer((_) async => order);
1329+
when(() => getTransactionsByTxId.execute(any())).thenAnswer(
1330+
(_) async => [Transaction(payjoin: _receiver(), order: order)],
1331+
);
1332+
1333+
final cubit = buildCubit();
1334+
addTearDown(cubit.close);
1335+
await cubit.initByOrderId('order-1');
1336+
1337+
verify(() => getTransactionsByTxId.execute('payjoin-txid')).called(1);
1338+
});
1339+
1340+
test('prefers the order txid when both are known', () async {
1341+
final order = _buyOrder(
1342+
bitcoinTransactionId: 'payout-txid',
1343+
payjoinTxId: 'payjoin-txid',
1344+
);
1345+
when(
1346+
() => getOrder.execute(orderId: 'order-1'),
1347+
).thenAnswer((_) async => order);
1348+
when(() => getTransactionsByTxId.execute(any())).thenAnswer(
1349+
(_) async => [Transaction(payjoin: _receiver(), order: order)],
1350+
);
1351+
1352+
final cubit = buildCubit();
1353+
addTearDown(cubit.close);
1354+
await cubit.initByOrderId('order-1');
1355+
1356+
verify(() => getTransactionsByTxId.execute('payout-txid')).called(1);
1357+
verifyNever(() => getTransactionsByTxId.execute('payjoin-txid'));
1358+
});
1359+
1360+
test('falls back to order-only details when no txid is known', () async {
1361+
final order = _buyOrder();
1362+
when(
1363+
() => getOrder.execute(orderId: 'order-1'),
1364+
).thenAnswer((_) async => order);
1365+
1366+
final cubit = buildCubit();
1367+
addTearDown(cubit.close);
1368+
await cubit.initByOrderId('order-1');
1369+
1370+
verifyNever(() => getTransactionsByTxId.execute(any()));
1371+
expect(cubit.state.transaction?.order, order);
1372+
});
1373+
});
13161374
}
13171375

1376+
Order _buyOrder({String? bitcoinTransactionId, String? payjoinTxId}) =>
1377+
Order.buy(
1378+
orderId: 'order-1',
1379+
orderType: OrderType.buy,
1380+
message: OrderMessage(code: '', message: ''),
1381+
orderNumber: 1,
1382+
payinAmount: 100,
1383+
payinCurrency: 'CAD',
1384+
payoutAmount: 0.001,
1385+
payoutCurrency: 'BTC',
1386+
payinMethod: OrderPaymentMethod.eTransfer,
1387+
payoutMethod: OrderPaymentMethod.bitcoin,
1388+
orderStatus: OrderStatus.inProgress,
1389+
payinStatus: OrderPayinStatus.completed,
1390+
payoutStatus: OrderPayoutStatus.completed,
1391+
createdAt: DateTime.utc(2026, 8, 19),
1392+
bitcoinTransactionId: bitcoinTransactionId,
1393+
payjoinDetails: payjoinTxId == null
1394+
? null
1395+
: OrderPayjoinDetails(txid: payjoinTxId),
1396+
isTestnet: false,
1397+
);
1398+
13181399
OrderSwapRecord _receiveOrderSwap() {
13191400
final createdAt = DateTime.utc(2026, 8, 10);
13201401
return OrderSwapRecord(

0 commit comments

Comments
 (0)