Skip to content

Commit a3bb57d

Browse files
committed
refactor(transactions): split shared port from broadcast review feature
lib/core/transactions/ was structured as a full feature inside core (domain + application + presentation + ui), violating the rule that core is infrastructure only. The cubit, screen, usecase, and ReviewableTransaction aggregate were consumed solely by lib/features/broadcast_signed_tx/. Split: keep the shared primitives in core (Transaction entities, TransactionPort, TransactionMapper) since electrum implements the port and depends on the mapper; move review-specific concerns into the broadcast_signed_tx feature. Errors split per-layer: TransactionPortError in core (thrown by the port adapter), TransactionReviewError in the feature (thrown by the usecase, wrapping port errors at the boundary). The view renders TransactionReviewError and unwraps the inner port error for granular messaging — never sees a foreign error type. Drop dead code: BuildTransactionUsecase had three execute methods — executeFromTransaction was internal-only and executeFromWalletTransaction was never called. Collapse to a single execute(). Drop unused TransactionMapper.fromLiquidTx and the dead TransactionParseFailed variant. Rename per AGENTS.md naming: TransactionEntity -> ReviewableTransaction (no Entity suffix), TransactionCubit -> TransactionReviewCubit, TransactionScreen -> TransactionReviewView (it's an embedded widget, not a routed screen). Folder layout per rule 14 — no single-file subfolders.
1 parent 6fced65 commit a3bb57d

24 files changed

Lines changed: 345 additions & 360 deletions

integration_test/sqlite_transactions_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'package:bb_mobile/core/storage/sqlite_database.dart';
2-
import 'package:bb_mobile/core/transactions/domain/ports/transaction_port.dart';
2+
import 'package:bb_mobile/core/transactions/domain/transaction_port.dart';
33
import 'package:bb_mobile/locator.dart';
44
import 'package:bb_mobile/main.dart';
55
import 'package:flutter_test/flutter_test.dart';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:bb_mobile/core/electrum/domain/repositories/electrum_transaction_repository.dart';
2+
import 'package:bb_mobile/core/electrum/frameworks/drift/datasources/electrum_remote_datasource.dart';
3+
import 'package:bb_mobile/core/storage/tables/transactions_table.dart';
4+
import 'package:bb_mobile/core/utils/bitcoin_tx.dart';
5+
6+
class DriftElectrumTransactionRepository
7+
implements ElectrumTransactionRepository {
8+
final ElectrumRemoteDatasource _datasource;
9+
10+
DriftElectrumTransactionRepository({
11+
required ElectrumRemoteDatasource datasource,
12+
}) : _datasource = datasource;
13+
14+
@override
15+
Future<BitcoinTx> fetch({
16+
required String serverUrl,
17+
required String txid,
18+
}) async {
19+
final model = await _datasource.fetch(serverUrl: serverUrl, txid: txid);
20+
return TransactionModelExtension.toEntity(model);
21+
}
22+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:bb_mobile/core/electrum/domain/ports/electrum_servers_port.dart';
2+
import 'package:bb_mobile/core/electrum/domain/ports/environment_port.dart';
3+
import 'package:bb_mobile/core/electrum/domain/repositories/electrum_transaction_repository.dart';
4+
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_environment.dart';
5+
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_network.dart';
6+
import 'package:bb_mobile/core/transactions/adapters/transaction_mapper.dart';
7+
import 'package:bb_mobile/core/transactions/domain/domain_errors.dart';
8+
import 'package:bb_mobile/core/transactions/domain/entities/transaction.dart';
9+
import 'package:bb_mobile/core/transactions/domain/transaction_port.dart';
10+
11+
/// Adapter implementing [TransactionPort] for the Electrum module.
12+
///
13+
/// Iterates the configured Electrum servers in priority order, falling back
14+
/// on failure, then maps the parsed [BitcoinTx] into a [Transaction] domain
15+
/// entity. Surfaces failures as [TransactionPortError] so consumers never
16+
/// see electrum's error types.
17+
class ElectrumTransactionPortAdapter implements TransactionPort {
18+
final ElectrumServersPort _serversPort;
19+
final ElectrumTransactionRepository _repository;
20+
final EnvironmentPort _environmentPort;
21+
22+
const ElectrumTransactionPortAdapter({
23+
required ElectrumServersPort serversPort,
24+
required ElectrumTransactionRepository repository,
25+
required EnvironmentPort environmentPort,
26+
}) : _serversPort = serversPort,
27+
_repository = repository,
28+
_environmentPort = environmentPort;
29+
30+
@override
31+
Future<Transaction> fetch({required String txid}) async {
32+
final environment = await _environmentPort.getEnvironment();
33+
final network = ElectrumServerNetwork.fromEnvironment(
34+
isTestnet: environment.isTestnet,
35+
isLiquid: false,
36+
);
37+
38+
final servers = await _serversPort.getServersToUse(network: network);
39+
if (servers.isEmpty) {
40+
throw TransactionPortError.noServersAvailable(
41+
network: network.toString(),
42+
);
43+
}
44+
45+
Object? lastError;
46+
for (final server in servers) {
47+
try {
48+
final bitcoinTx = await _repository.fetch(
49+
serverUrl: server.url,
50+
txid: txid,
51+
);
52+
return TransactionMapper.fromBitcoinTx(
53+
bitcoinTx,
54+
isTestnet: environment.isTestnet,
55+
);
56+
} catch (e) {
57+
lastError = e;
58+
continue;
59+
}
60+
}
61+
62+
throw TransactionPortError.fetchFailed(txid: txid, message: '$lastError');
63+
}
64+
}

lib/core/transactions/data/mappers/transaction_mapper.dart renamed to lib/core/transactions/adapters/transaction_mapper.dart

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import 'dart:typed_data';
22

3-
import 'package:bb_mobile/core/transactions/domain/entity/bitcoin_transaction.dart';
4-
import 'package:bb_mobile/core/transactions/domain/entity/liquid_transaction.dart';
3+
import 'package:bb_mobile/core/transactions/domain/entities/bitcoin_transaction.dart';
54
import 'package:bb_mobile/core/utils/bitcoin_tx.dart' as btc_utils;
6-
import 'package:bb_mobile/core/utils/liquid_tx.dart' as liq_utils;
75
import 'package:bdk_dart/bdk.dart' as bdk;
86
import 'package:convert/convert.dart';
97

10-
/// Maps between the existing utility classes ([BitcoinTx], [LiquidTx])
11-
/// and the new domain entities ([BitcoinTransaction], [LiquidTransaction]).
8+
/// Maps the BDK utility class [btc_utils.BitcoinTx] to the domain entity
9+
/// [BitcoinTransaction].
1210
class TransactionMapper {
1311
/// Convert a [btc_utils.BitcoinTx] utility class to a [BitcoinTransaction]
1412
/// domain entity.
@@ -26,7 +24,7 @@ class TransactionMapper {
2624
size: bitcoinTx.size.toInt(),
2725
vsize: bitcoinTx.vsize.toInt(),
2826
locktime: bitcoinTx.locktime,
29-
inputs: bitcoinTx.vin.map((vin) => _mapBitcoinInput(vin)).toList(),
27+
inputs: bitcoinTx.vin.map(_mapBitcoinInput).toList(),
3028
outputs: bitcoinTx.vout
3129
.asMap()
3230
.entries
@@ -38,25 +36,6 @@ class TransactionMapper {
3836
);
3937
}
4038

41-
/// Convert a [liq_utils.LiquidTx] utility class to a [LiquidTransaction]
42-
/// domain entity.
43-
static LiquidTransaction fromLiquidTx(liq_utils.LiquidTx liquidTx) {
44-
return LiquidTransaction(
45-
txid: liquidTx.txid,
46-
version: liquidTx.version,
47-
vsize: liquidTx.vsize.toInt(),
48-
weight: liquidTx.weight.toInt(),
49-
locktime: liquidTx.locktime,
50-
feeSat: liquidTx.fee.toInt(),
51-
inputs: liquidTx.vin.map((vin) => _mapLiquidInput(vin)).toList(),
52-
outputs: liquidTx.vout
53-
.asMap()
54-
.entries
55-
.map((entry) => _mapLiquidOutput(entry.value, entry.key))
56-
.toList(),
57-
);
58-
}
59-
6039
static BitcoinTxInput _mapBitcoinInput(btc_utils.TxVin vin) {
6140
return BitcoinTxInput(
6241
previousTxId: vin.txid,
@@ -91,28 +70,4 @@ class TransactionMapper {
9170
scriptPubKeyBytes: vout.scriptPubKey.bytes,
9271
);
9372
}
94-
95-
static LiquidTxInput _mapLiquidInput(liq_utils.LiquidTxVin vin) {
96-
return LiquidTxInput(
97-
previousTxId: vin.txid,
98-
previousVout: vin.vout,
99-
sequence: vin.sequence,
100-
scriptSig: vin.scriptSig,
101-
witness: vin.witness,
102-
isPegin: vin.isPegin,
103-
);
104-
}
105-
106-
static LiquidTxOutput _mapLiquidOutput(
107-
liq_utils.LiquidTxVout vout,
108-
int index,
109-
) {
110-
return LiquidTxOutput(
111-
valueSat: vout.value?.toInt() ?? 0,
112-
index: index,
113-
scriptPubKeyHex: vout.scriptPubKey,
114-
asset: vout.asset,
115-
nonce: vout.nonce,
116-
);
117-
}
11873
}

lib/core/transactions/application/build_transaction_usecase.dart

Lines changed: 0 additions & 128 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
part 'domain_errors.freezed.dart';
4+
5+
/// Errors thrown by implementations of [TransactionPort].
6+
///
7+
/// Port-layer concerns only — fetching a parsed transaction from an
8+
/// external source. Higher layers map these into their own domain errors
9+
/// at the boundary so the port's error type never leaks upward.
10+
@freezed
11+
sealed class TransactionPortError with _$TransactionPortError {
12+
/// Failed to fetch the transaction across all configured servers.
13+
const factory TransactionPortError.fetchFailed({
14+
required String txid,
15+
String? message,
16+
}) = TransactionPortFetchFailed;
17+
18+
/// No servers are available for the requested network.
19+
const factory TransactionPortError.noServersAvailable({String? network}) =
20+
TransactionPortNoServersAvailable;
21+
22+
const TransactionPortError._();
23+
}

lib/core/transactions/domain/entity/bitcoin_transaction.dart renamed to lib/core/transactions/domain/entities/bitcoin_transaction.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:bb_mobile/core/transactions/domain/entity/transaction.dart';
1+
import 'package:bb_mobile/core/transactions/domain/entities/transaction.dart';
22

33
/// Concrete Bitcoin transaction implementing the abstract [Transaction].
44
class BitcoinTransaction extends Transaction {

lib/core/transactions/domain/entity/liquid_transaction.dart renamed to lib/core/transactions/domain/entities/liquid_transaction.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:bb_mobile/core/transactions/domain/entity/transaction.dart';
1+
import 'package:bb_mobile/core/transactions/domain/entities/transaction.dart';
22

33
/// Concrete Liquid transaction implementing the abstract [Transaction].
44
///
File renamed without changes.

lib/core/transactions/domain/error/transaction_error.dart

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)