Skip to content
Merged
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
16 changes: 11 additions & 5 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ This diagram shows the dependencies between features in the Bull Bitcoin Mobile
```mermaid
graph TB
%% Core infrastructure
CORE[Core<br/>---<br/>Database, Secure Storage,<br/>API Clients, Tor HTTP Client, UI Kit,<br/>DI & Router setup,<br/>PIN encrypted storage,<br/>Domain Primitives/Value Objects]
CORE[Core<br/>---<br/>Database, Secure Storage,<br/>API Clients, Tor Adapters, UI Kit,<br/>DI & Router setup,<br/>PIN encrypted storage,<br/>Domain Primitives/Value Objects]
PRIMITIVES[Primitives Package]
BULL_PAYJOIN[Bull Payjoin Package<br/>Public contract]

%% Feature modules
SETTINGS[Settings]
TOR[Tor]
TOR[Tor<br/>Workspace Package]
PIN_CODE[Pin Code]
LABELS[Labels]
SECRETS[Secrets]
Expand Down Expand Up @@ -66,6 +66,8 @@ graph TB
ANNOUNCEMENTS --> SWAPS
APP_STARTUP --> WALLETS
AUTOSWAP --> SWAPS
APP_STARTUP --> TOR
AUTOSWAPS --> TRANSFER
BIP85 --> SECRETS
BIP85 --> SETTINGS
BACKUPS --> BIP85
Expand Down Expand Up @@ -108,11 +110,12 @@ graph TB
SETTINGS --> CORE
SETTINGS --> BULL_PAYJOIN
STATUS --> BULL_PAYJOIN
STATUS --> TOR
SWAPS --> BULL_PAYJOIN
SWAPS --> EXCHANGE
SWAPS --> LABELS
SWAPS --> UTXO_MGMT
TOR --> CORE
CORE --> TOR
TRANSFER --> CONSOLIDATION
TRANSFER --> SEND
TRANSFER --> RECEIVE
Expand All @@ -136,7 +139,8 @@ graph TB
classDef featureStyle fill:#1a202c,stroke:#2d3748,stroke-width:2px,color:#e2e8f0

class CORE coreStyle
class PRIMITIVES,BULL_PAYJOIN packageStyle
class PRIMITIVES,BULL_PAYJOIN,TOR packageStyle
class SETTINGS,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,DCA,SELL,PAY,BUY,COINS,ANNOUNCEMENTS,CONSOLIDATION,ALL_SEED_VIEW,APP_UNLOCK featureStyle
```

## About Package Dependency Diagrams
Expand Down Expand Up @@ -175,7 +179,8 @@ graph TB
- Database (Drift/SQLite)
- Secure Storage instance (Flutter Secure Storage)
- API Clients (REST/GraphQL clients)
- Factory for a HTTP client to connect to Tor
- Embedded Onion adapter with isolated RecoverBull and Bitcoin Electrum `.onion` sessions
- Explicit Orbot SOCKS override for Bitcoin Electrum `.onion` servers
- UI Kit (shared widgets, theme)
- DI setup and interfaces (Service Locator pattern)
- Router setup and interfaces (Navigation)
Expand All @@ -194,6 +199,7 @@ graph TB
### Central Features (Highly Depended Upon)

- **Core**: Foundation for all features
- **Tor**: `packages/tor` — embedded Onion lifecycle with isolated RecoverBull and Bitcoin Electrum `.onion` sessions, plus external SOCKS verification for the explicit Electrum Orbot override. Depends on Flutter: it owns a platform plugin and app-directory storage, which is the infrastructure-package exception in AGENTS.md
- **Wallets**: Used by Send, UTXO Management, Transaction History, Backups, App Startup
- **Secrets**: Used by Wallets, BIP85
- **Settings**: Used by Wallets, Exchange, BIP85, Bitcoin Price
Expand Down
9 changes: 6 additions & 3 deletions integration_test/recoverbull_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:bb_mobile/core/recoverbull/domain/usecases/restore_vault_usecase
import 'package:bb_mobile/core/seed/data/models/seed_model.dart';
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
import 'package:bb_mobile/core/tor/data/usecases/init_tor_usecase.dart';
import 'package:bb_mobile/core/utils/bip32_derivation.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/core/utils/recoverbull_bip85.dart';
Expand All @@ -18,12 +17,13 @@ import 'package:bb_mobile/main.dart';
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bull_tor/tor.dart';

Future<void> main({bool isInitialized = false}) async {
TestWidgetsFlutterBinding.ensureInitialized();
if (!isInitialized) await Bull.init();

final initializeTorUsecase = locator<InitTorUsecase>();
final ensureTorReadyUsecase = locator<EnsureTorReadyUsecase>();
final restoreVaultUsecase = locator<RestoreVaultUsecase>();
final decryptVaultUsecase = locator<DecryptVaultUsecase>();
final fetchVaultKeyFromServerUsecase =
Expand Down Expand Up @@ -65,7 +65,10 @@ Future<void> main({bool isInitialized = false}) async {
Network.bitcoinMainnet,
);

setUpAll(() async => await initializeTorUsecase.execute());
setUpAll(() async {
final state = await ensureTorReadyUsecase.execute();
expect(state, isA<TorReady>());
});

group('Recoverbull', () {
// Fetches the vault key over Tor from the RecoverBull key server. Tor can
Expand Down
16 changes: 14 additions & 2 deletions lib/core/core_locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import 'package:bb_mobile/core/storage/sqlite_database.dart';
import 'package:bb_mobile/core/storage/storage_locator.dart';
import 'package:bb_mobile/core/swaps/swaps_locator.dart';
import 'package:bb_mobile/core/tor/tor_locator.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/wallet/wallet_locator.dart';
import 'package:bull_tor/tor_adapter.dart' as bull_tor;
import 'package:get_it/get_it.dart';

class CoreLocator {
Expand All @@ -23,13 +25,21 @@ class CoreLocator {
}

static Future<void> registerDatasources(GetIt locator) async {
await bull_tor.TorLocator.registerDatasources(
locator,
logger: bull_tor.TorLogger(
configCallback: log.config,
fineCallback: log.fine,
warningCallback: log.warning,
),
);
await TorLocator.registerDatasources(locator);
BlockchainLocator.registerDatasources(locator);
await ElectrumLocator.registerDatasources(locator);
ExchangeLocator.registerDatasources(locator);
FeesLocator.registerDatasources(locator);
await MempoolLocator.registerDatasources(locator);
await RecoverbullLocator.registerDatasources(locator);
RecoverbullLocator.registerDatasources(locator);
await StorageLocator.registerDatasources(locator);
SeedLocator.registerDatasources(locator);
await SwapsLocator.registerDatasources(locator);
Expand All @@ -47,6 +57,7 @@ class CoreLocator {
}

static Future<void> registerRepositories(GetIt locator) async {
bull_tor.TorLocator.registerRepositories(locator);
await TorLocator.registerRepositories(locator);
BlockchainLocator.registerRepositories(locator);
ElectrumLocator.registerRepositories(locator);
Expand All @@ -55,7 +66,7 @@ class CoreLocator {
MempoolLocator.registerRepositories(locator);
await SettingsLocator.registerRepositories(locator);
SeedLocator.registerRepositories(locator);
await RecoverbullLocator.registerRepositories(locator);
RecoverbullLocator.registerRepositories(locator);
SwapsLocator.registerRepositories(locator);
WalletLocator.registerRepositories(locator);
Bip85DerivationsLocator.registerRepositories(locator);
Expand All @@ -70,6 +81,7 @@ class CoreLocator {
}

static void registerUsecases(GetIt locator) {
bull_tor.TorLocator.registerUsecases(locator);
LabelsLocator.registerUseCases(locator);
ElectrumLocator.registerUsecases(locator);
BlockchainLocator.registerUsecases(locator);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import 'package:bb_mobile/core/recoverbull/data/datasources/recoverbull_settings_datasource.dart';
import 'package:bb_mobile/core/tor/data/datasources/tor_datasource.dart';
import 'package:bb_mobile/core/tor/domain/value_objects/tor_proxy_config.dart';
import 'package:bb_mobile/core/tor/tor_status.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:recoverbull/recoverbull.dart';
import 'package:bull_tor/tor.dart';

class RecoverBullRemoteDatasource {
final RecoverbullSettingsDatasource _recoverbullSettingsDatasource;
final TorDatasource _torDatasource;
final TorHttpClientFactory _torHttpClientFactory;

RecoverBullRemoteDatasource({
required this._recoverbullSettingsDatasource,
required this._torDatasource,
required this._torHttpClientFactory,
});

Future<void> info({TorProxyConfig? externalProxy}) async {
final client = _torDatasource.httpClient(externalProxy: externalProxy);
Future<void> info(TorProxyEndpoint endpoint) async {
final client = _torHttpClientFactory.create(endpoint);
final url = await _recoverbullSettingsDatasource.fetch();
try {
final info = await KeyServer(address: url, client: client).infos();
log.info('KeyServer canary: ${info.canary}');
} catch (e) {
log.severe(error: e, trace: StackTrace.current);
rethrow;
} finally {
client.close(force: true);
}
}

Expand All @@ -31,10 +31,10 @@ class RecoverBullRemoteDatasource {
List<int> password,
List<int> salt,
List<int> backupKey, {
TorProxyConfig? externalProxy,
required TorProxyEndpoint endpoint,
}) async {
final client = _torHttpClientFactory.create(endpoint);
try {
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
await KeyServer(address: url, client: client).storeBackupKey(
backupId: backupId,
Expand All @@ -49,17 +49,19 @@ class RecoverBullRemoteDatasource {
trace: StackTrace.current,
);
rethrow;
} finally {
client.close(force: true);
}
}

Future<List<int>> fetch(
List<int> backupId,
List<int> password,
List<int> salt, {
TorProxyConfig? externalProxy,
required TorProxyEndpoint endpoint,
}) async {
final client = _torHttpClientFactory.create(endpoint);
try {
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
return await KeyServer(
address: url,
Expand All @@ -72,17 +74,19 @@ class RecoverBullRemoteDatasource {
trace: StackTrace.current,
);
rethrow;
} finally {
client.close(force: true);
}
}

Future<void> trash(
List<int> backupId,
List<int> password,
List<int> salt, {
TorProxyConfig? externalProxy,
required TorProxyEndpoint endpoint,
}) async {
final client = _torHttpClientFactory.create(endpoint);
try {
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
await KeyServer(
address: url,
Expand All @@ -95,16 +99,14 @@ class RecoverBullRemoteDatasource {
trace: StackTrace.current,
);
rethrow;
} finally {
client.close(force: true);
}
}

Future<void> checkConnection({TorProxyConfig? externalProxy}) async {
Future<void> checkConnection(TorProxyEndpoint endpoint) async {
final client = _torHttpClientFactory.create(endpoint);
try {
if (externalProxy == null) {
await _waitForInternalTor();
}

final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
await KeyServer(address: url, client: client).infos();
} catch (e) {
Expand All @@ -114,19 +116,8 @@ class RecoverBullRemoteDatasource {
trace: StackTrace.current,
);
rethrow;
}
}

Future<void> _waitForInternalTor() async {
const maxWaitTime = Duration(minutes: 2);
final startTime = DateTime.now();

while (_torDatasource.status == TorStatus.connecting) {
if (DateTime.now().difference(startTime) > maxWaitTime) {
throw Exception('Timeout waiting for Tor to be ready');
}
log.info('Waiting for Tor to be ready...');
await Future.delayed(const Duration(seconds: 3));
} finally {
client.close(force: true);
}
}
}
21 changes: 9 additions & 12 deletions lib/core/recoverbull/data/repository/recoverbull_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@ import 'package:bb_mobile/core/recoverbull/data/datasources/recoverbull_settings
import 'package:bb_mobile/core/recoverbull/domain/entity/decrypted_vault.dart';
import 'package:bb_mobile/core/recoverbull/domain/entity/encrypted_vault.dart';
import 'package:bb_mobile/core/recoverbull/domain/recoverbull_failure.dart';
import 'package:bb_mobile/core/tor/domain/ports/tor_config_port.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:convert/convert.dart' as convert;
import 'package:recoverbull/recoverbull.dart' as recoverbull;
import 'package:bull_tor/tor.dart';

/// Data boundary for the RecoverBull key server and vault crypto. Catches the
/// foreign exceptions the datasources/SDK throw, logs the raw reason, and
/// returns a [RecoverBullCoreFailure] — no exception crosses this boundary.
class RecoverBullRepository {
final RecoverBullRemoteDatasource remoteDatasource;
final RecoverbullSettingsDatasource recoverbullSettingsDatasource;
final TorConfigPort torConfigPort;

RecoverBullRepository({
required this.remoteDatasource,
required this.recoverbullSettingsDatasource,
required this.torConfigPort,
});

/// Builds an encrypted vault file for [plaintext] under [vaultKey] and stamps
Expand Down Expand Up @@ -85,15 +83,15 @@ class RecoverBullRepository {
String password,
String salt,
String vaultKey,
TorProxyEndpoint endpoint,
) async {
try {
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
await remoteDatasource.store(
convert.hex.decode(_normalizeHex(identifier)),
utf8.encode(password),
convert.hex.decode(_normalizeHex(salt)),
convert.hex.decode(_normalizeHex(vaultKey)),
externalProxy: externalProxy,
endpoint: endpoint,
);
return const Ok(null);
} on recoverbull.KeyServerException catch (e, st) {
Expand All @@ -119,14 +117,14 @@ class RecoverBullRepository {
String identifier,
String password,
String salt,
TorProxyEndpoint endpoint,
) async {
try {
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
final vaultKey = await remoteDatasource.fetch(
convert.hex.decode(_normalizeHex(identifier)),
utf8.encode(password),
convert.hex.decode(_normalizeHex(salt)),
externalProxy: externalProxy,
endpoint: endpoint,
);
return Ok(convert.hex.encode(vaultKey));
} on recoverbull.KeyServerException catch (e, st) {
Expand All @@ -152,23 +150,22 @@ class RecoverBullRepository {
String identifier,
String password,
String salt,
TorProxyEndpoint endpoint,
) async {
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
await remoteDatasource.trash(
convert.hex.decode(_normalizeHex(identifier)),
utf8.encode(password),
convert.hex.decode(_normalizeHex(salt)),
externalProxy: externalProxy,
endpoint: endpoint,
);
}

/// Health probe: completes normally when the server is reachable, throws
/// otherwise. Kept throwing (not Result) on purpose so the shared status
/// checker — `CheckServerConnectionUsecase`, which turns the throw/return
/// into the bool — is unaffected by the Result migration.
Future<void> checkConnection() async {
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
await remoteDatasource.checkConnection(externalProxy: externalProxy);
Future<void> checkConnection(TorProxyEndpoint endpoint) async {
await remoteDatasource.checkConnection(endpoint);
}

Future<Uri> fetchUrl() async {
Expand Down
Loading
Loading