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
36 changes: 32 additions & 4 deletions lib/core/mempool/application/dtos/mempool_server_dto.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import 'package:bb_mobile/core/mempool/domain/entities/mempool_server.dart';
import 'package:bb_mobile/core/mempool/domain/value_objects/mempool_server_status.dart';

class MempoolServerDto {
final String url;
final bool isCustom;
final bool isTestnet;
final bool isLiquid;
final bool enableSsl;
final MempoolServerStatus status;

MempoolServerDto({
required this.url,
required this.isCustom,
required this.isTestnet,
required this.isLiquid,
this.enableSsl = true,
this.status = MempoolServerStatus.unknown,
});

factory MempoolServerDto.fromEntity(MempoolServer entity) {
Expand All @@ -19,10 +24,29 @@ class MempoolServerDto {
isCustom: entity.isCustom,
isTestnet: entity.isTestnet,
isLiquid: entity.isLiquid,
enableSsl: entity.enableSsl,
);
}

String get fullUrl => 'https://$url';
String get fullUrl => enableSsl ? 'https://$url' : 'http://$url';

MempoolServerDto copyWith({
String? url,
bool? isCustom,
bool? isTestnet,
bool? isLiquid,
bool? enableSsl,
MempoolServerStatus? status,
}) {
return MempoolServerDto(
url: url ?? this.url,
isCustom: isCustom ?? this.isCustom,
isTestnet: isTestnet ?? this.isTestnet,
isLiquid: isLiquid ?? this.isLiquid,
enableSsl: enableSsl ?? this.enableSsl,
status: status ?? this.status,
);
}

@override
bool operator ==(Object other) =>
Expand All @@ -32,16 +56,20 @@ class MempoolServerDto {
url == other.url &&
isCustom == other.isCustom &&
isTestnet == other.isTestnet &&
isLiquid == other.isLiquid;
isLiquid == other.isLiquid &&
enableSsl == other.enableSsl &&
status == other.status;

@override
int get hashCode =>
url.hashCode ^
isCustom.hashCode ^
isTestnet.hashCode ^
isLiquid.hashCode;
isLiquid.hashCode ^
enableSsl.hashCode ^
status.hashCode;

@override
String toString() =>
'MempoolServerDto(url: $url, isCustom: $isCustom, isTestnet: $isTestnet, isLiquid: $isLiquid)';
'MempoolServerDto(url: $url, isCustom: $isCustom, isTestnet: $isTestnet, isLiquid: $isLiquid, enableSsl: $enableSsl, status: $status)';
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
class SetCustomMempoolServerRequest {
final String url;
final bool isLiquid;
final bool enableSsl;

SetCustomMempoolServerRequest({
required this.url,
required this.isLiquid,
this.enableSsl = true,
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:bb_mobile/core/mempool/application/dtos/requests/set_custom_mempool_server_request.dart';
import 'package:bb_mobile/core/mempool/domain/entities/mempool_server.dart';
import 'package:bb_mobile/core/mempool/domain/errors/mempool_server_exception.dart';
import 'package:bb_mobile/core/mempool/domain/ports/mempool_server_validator_port.dart';
import 'package:bb_mobile/core/mempool/domain/repositories/mempool_server_repository.dart';
import 'package:bb_mobile/core/mempool/domain/value_objects/mempool_server_network.dart';
Expand All @@ -8,20 +9,36 @@ import 'package:bb_mobile/core/mempool/domain/ports/environment_port.dart';
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
import 'package:bb_mobile/core/utils/logger.dart';

enum SetCustomMempoolServerError {
sameAsDefault,
validationFailed,
saveFailed,
unexpected,
}

class SetCustomMempoolServerResult {
final bool isValid;
final String? errorMessage;
final SetCustomMempoolServerError? errorType;
final MempoolValidationErrorType? validationErrorType;

SetCustomMempoolServerResult({required this.isValid, this.errorMessage});
SetCustomMempoolServerResult({
required this.isValid,
this.errorType,
this.validationErrorType,
});

factory SetCustomMempoolServerResult.success() {
return SetCustomMempoolServerResult(isValid: true);
}

factory SetCustomMempoolServerResult.failure(String errorMessage) {
factory SetCustomMempoolServerResult.failure(
SetCustomMempoolServerError errorType, {
MempoolValidationErrorType? validationErrorType,
}) {
return SetCustomMempoolServerResult(
isValid: false,
errorMessage: errorMessage,
errorType: errorType,
validationErrorType: validationErrorType,
);
}
}
Expand Down Expand Up @@ -51,14 +68,20 @@ class SetCustomMempoolServerUsecase {
isLiquid: request.isLiquid,
);

final customUrl = NormalizedMempoolUrl(request.url);
final customUrl = NormalizedMempoolUrl(
request.url,
enableSsl: request.enableSsl,
);

final defaultServerResult = await _fetchDefaultServerSafely(network);
if (defaultServerResult != null) {
final defaultUrl = NormalizedMempoolUrl(defaultServerResult.url);
final defaultUrl = NormalizedMempoolUrl(
defaultServerResult.url,
enableSsl: defaultServerResult.enableSsl,
);
if (customUrl == defaultUrl) {
return SetCustomMempoolServerResult.failure(
'This URL is the same as the default server. Please use a different URL.',
SetCustomMempoolServerError.sameAsDefault,
);
}
}
Expand All @@ -68,16 +91,22 @@ class SetCustomMempoolServerUsecase {
final isValid = await _validator.validateServer(
url: request.url,
network: network,
enableSsl: request.enableSsl,
);

if (!isValid) {
return SetCustomMempoolServerResult.failure(
'Server validation failed: Unable to connect or invalid response',
SetCustomMempoolServerError.validationFailed,
);
}
} on MempoolServerValidationException catch (e) {
return SetCustomMempoolServerResult.failure(
SetCustomMempoolServerError.validationFailed,
validationErrorType: e.errorType,
);
} catch (e) {
return SetCustomMempoolServerResult.failure(
'Validation error: ${e.toString()}',
SetCustomMempoolServerError.unexpected,
);
}
}
Expand All @@ -86,14 +115,16 @@ class SetCustomMempoolServerUsecase {
final server = MempoolServer.createCustom(
url: request.url,
network: network,
enableSsl: request.enableSsl,
);

await _serverRepository.save(server);

return SetCustomMempoolServerResult.success();
} catch (e) {
log.warning('Failed to save mempool server: $e');
return SetCustomMempoolServerResult.failure(
'Failed to save server: ${e.toString()}',
SetCustomMempoolServerError.saveFailed,
);
}
}
Expand All @@ -105,9 +136,7 @@ class SetCustomMempoolServerUsecase {
return await _serverRepository.fetchDefaultServer(network);
} catch (e) {
// log the error for debugging but don't throw.
log.warning(
'Could not fetch default mempool server for comparison: $e',
);
log.warning('Could not fetch default mempool server for comparison: $e');
return null;
}
}
Expand Down
75 changes: 47 additions & 28 deletions lib/core/mempool/domain/entities/mempool_server.dart
Original file line number Diff line number Diff line change
@@ -1,64 +1,78 @@
import 'package:bb_mobile/core/mempool/domain/errors/mempool_server_exception.dart';
import 'package:bb_mobile/core/mempool/domain/value_objects/mempool_server_network.dart';
import 'package:bb_mobile/core/utils/mempool_url_parser.dart';

class MempoolServer {
final String _url;
final MempoolServerNetwork _network;
final bool _isCustom;
final bool _enableSsl;

MempoolServer._({
required String url,
required MempoolServerNetwork network,
required bool isCustom,
required bool enableSsl,
}) : _url = url,
_network = network,
_isCustom = isCustom;
_isCustom = isCustom,
_enableSsl = enableSsl;

factory MempoolServer.createCustom({
required String url,
required MempoolServerNetwork network,
bool enableSsl = true,
}) {
final cleanedUrl = _validateAndCleanUrl(url);
return MempoolServer._(url: cleanedUrl, network: network, isCustom: true);
return MempoolServer._(
url: cleanedUrl,
network: network,
isCustom: true,
enableSsl: enableSsl,
);
}

factory MempoolServer.existing({
required String url,
required MempoolServerNetwork network,
required bool isCustom,
bool enableSsl = true,
}) {
return MempoolServer._(url: url, network: network, isCustom: isCustom);
return MempoolServer._(
url: url,
network: network,
isCustom: isCustom,
enableSsl: enableSsl,
);
}

static String _validateAndCleanUrl(String url) {
if (url.isEmpty) {
throw InvalidMempoolUrlException('URL cannot be empty');
try {
final result = MempoolUrlParser.parse(url);
return result.cleanUrl;
} on MempoolUrlValidationError catch (e) {
switch (e) {
case MempoolUrlValidationError.empty:
throw InvalidMempoolUrlException('URL cannot be empty');
case MempoolUrlValidationError.hasPath:
throw InvalidMempoolUrlException(
'URL should not contain paths. Enter only the domain or IP address',
);
case MempoolUrlValidationError.invalidDomain:
throw InvalidMempoolUrlException(
'Invalid URL format: must be a valid domain',
);
case MempoolUrlValidationError.invalidFormat:
throw InvalidMempoolUrlException('Invalid URL format');
}
}

// Remove protocol if present
String cleanedUrl = url.replaceFirst(RegExp(r'^https?://'), '');

// Remove trailing slash
cleanedUrl = cleanedUrl.replaceFirst(RegExp(r'/$'), '');

if (cleanedUrl.isEmpty) {
throw InvalidMempoolUrlException('Invalid URL format');
}

// Basic domain validation - should contain at least one dot or be localhost
if (!cleanedUrl.contains('.') && !cleanedUrl.startsWith('localhost')) {
throw InvalidMempoolUrlException(
'Invalid URL format: must be a valid domain',
);
}

return cleanedUrl;
}

String get url => _url;
MempoolServerNetwork get network => _network;
bool get isCustom => _isCustom;
String get fullUrl => 'https://$_url';
bool get enableSsl => _enableSsl;
String get fullUrl => _enableSsl ? 'https://$_url' : 'http://$_url';

bool get isTestnet => _network.isTestnet;
bool get isLiquid => _network.isLiquid;
Expand All @@ -70,12 +84,17 @@ class MempoolServer {
runtimeType == other.runtimeType &&
_url == other._url &&
_network == other._network &&
_isCustom == other._isCustom;
_isCustom == other._isCustom &&
_enableSsl == other._enableSsl;

@override
int get hashCode => _url.hashCode ^ _network.hashCode ^ _isCustom.hashCode;
int get hashCode =>
_url.hashCode ^
_network.hashCode ^
_isCustom.hashCode ^
_enableSsl.hashCode;

@override
String toString() =>
'MempoolServer(url: $_url, network: $_network, isCustom: $_isCustom)';
'MempoolServer(url: $_url, network: $_network, isCustom: $_isCustom, enableSsl: $_enableSsl)';
}
18 changes: 15 additions & 3 deletions lib/core/mempool/domain/errors/mempool_server_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@ class InvalidMempoolUrlException implements Exception {
String toString() => 'InvalidMempoolUrlException: $message';
}

enum MempoolValidationErrorType {
connectionTimeout,
hostNotFound,
torNotRunning,
connectionError,
notMempoolServer,
serverUnavailable,
serverError,
invalidResponse,
unexpected,
}

class MempoolServerValidationException implements Exception {
final String message;
final MempoolValidationErrorType errorType;
final Object? cause;

MempoolServerValidationException(this.message, [this.cause]);
MempoolServerValidationException(this.errorType, [this.cause]);

@override
String toString() =>
'MempoolServerValidationException: $message${cause != null ? ' (cause: $cause)' : ''}';
'MempoolServerValidationException: $errorType${cause != null ? ' (cause: $cause)' : ''}';
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ abstract class MempoolServerValidatorPort {
Future<bool> validateServer({
required String url,
required MempoolServerNetwork network,
bool enableSsl = true,
});
}
11 changes: 11 additions & 0 deletions lib/core/mempool/domain/value_objects/mempool_server_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
enum MempoolServerStatus {
online,
offline,
checking,
unknown;

bool get isOnline => this == MempoolServerStatus.online;
bool get isOffline => this == MempoolServerStatus.offline;
bool get isChecking => this == MempoolServerStatus.checking;
bool get isUnknown => this == MempoolServerStatus.unknown;
}
Loading