Skip to content

Commit c0f126c

Browse files
refactor(autoswap): sanitize user-facing settings errors
1 parent 1bf43d4 commit c0f126c

13 files changed

Lines changed: 927 additions & 503 deletions

lib/core/errors/autoswap_errors.dart

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,4 @@
11
import 'package:bb_mobile/core/errors/bull_exception.dart';
2-
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
3-
import 'package:bb_mobile/core/utils/amount_conversions.dart';
4-
import 'package:bb_mobile/generated/l10n/localization.dart';
5-
6-
class MinimumAmountThresholdException extends BullException {
7-
final int minimumThresholdSats;
8-
final BitcoinUnit bitcoinUnit;
9-
10-
MinimumAmountThresholdException(this.minimumThresholdSats, this.bitcoinUnit)
11-
: super(
12-
'Minimum balance threshold is $minimumThresholdSats ${bitcoinUnit.code}',
13-
);
14-
15-
String displayMessage([AppLocalizations? loc]) {
16-
if (loc == null) {
17-
if (bitcoinUnit == BitcoinUnit.btc) {
18-
final btcAmount = ConvertAmount.satsToBtc(minimumThresholdSats);
19-
return 'Minimum balance threshold is $btcAmount BTC';
20-
}
21-
return 'Minimum balance threshold is $minimumThresholdSats sats';
22-
}
23-
24-
if (bitcoinUnit == BitcoinUnit.btc) {
25-
final btcAmount = ConvertAmount.satsToBtc(minimumThresholdSats);
26-
return loc.autoswapMinimumThresholdErrorBtc(btcAmount.toString());
27-
}
28-
return loc.autoswapMinimumThresholdErrorSats(
29-
minimumThresholdSats.toString(),
30-
);
31-
}
32-
}
33-
34-
class MaximumFeeThresholdException extends BullException {
35-
final int maximumThreshold;
36-
37-
MaximumFeeThresholdException(this.maximumThreshold)
38-
: super('Maximum fee threshold is $maximumThreshold%');
39-
40-
String displayMessage([AppLocalizations? loc]) {
41-
if (loc == null) {
42-
return 'Maximum fee threshold is $maximumThreshold%';
43-
}
44-
return loc.autoswapMaximumFeeError(maximumThreshold.toString());
45-
}
46-
}
47-
48-
class AutoSwapProcessException extends BullException {
49-
final Object? error;
50-
51-
AutoSwapProcessException(super.message, {this.error});
52-
53-
@override
54-
String toString() => error != null ? '$message: $error' : message;
55-
}
562

573
class FeeBlockException extends BullException {
584
final double currentFeePercent;

lib/features/autoswap/autoswap_locator.dart

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,38 @@ import 'package:bb_mobile/core/settings/domain/get_settings_usecase.dart';
22
import 'package:bb_mobile/core/swaps/domain/usecases/get_auto_swap_settings_usecase.dart';
33
import 'package:bb_mobile/core/swaps/domain/usecases/save_auto_swap_settings_usecase.dart';
44
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
5+
import 'package:bb_mobile/features/autoswap/domain/usecases/load_autoswap_settings_usecase.dart';
6+
import 'package:bb_mobile/features/autoswap/domain/usecases/save_autoswap_settings_usecase.dart';
57
import 'package:bb_mobile/features/autoswap/presentation/autoswap_settings_cubit.dart';
68
import 'package:get_it/get_it.dart';
79

810
class AutoSwapLocator {
911
static void setup(GetIt locator) {
10-
// Register the cubit
11-
locator.registerFactory<AutoSwapSettingsCubit>(
12-
() => AutoSwapSettingsCubit(
12+
registerUsecases(locator);
13+
registerBlocs(locator);
14+
}
15+
16+
static void registerUsecases(GetIt locator) {
17+
locator.registerFactory<LoadAutoswapSettingsUsecase>(
18+
() => LoadAutoswapSettingsUsecase(
1319
getAutoSwapSettingsUsecase: locator<GetAutoSwapSettingsUsecase>(),
14-
saveAutoSwapSettingsUsecase: locator<SaveAutoSwapSettingsUsecase>(),
1520
getSettingsUsecase: locator<GetSettingsUsecase>(),
1621
walletRepository: locator<WalletRepository>(),
1722
),
1823
);
24+
locator.registerFactory<SaveAutoswapSettingsUsecase>(
25+
() => SaveAutoswapSettingsUsecase(
26+
saveAutoSwapSettingsUsecase: locator<SaveAutoSwapSettingsUsecase>(),
27+
),
28+
);
29+
}
30+
31+
static void registerBlocs(GetIt locator) {
32+
locator.registerFactory<AutoSwapSettingsCubit>(
33+
() => AutoSwapSettingsCubit(
34+
loadAutoswapSettingsUsecase: locator<LoadAutoswapSettingsUsecase>(),
35+
saveAutoswapSettingsUsecase: locator<SaveAutoswapSettingsUsecase>(),
36+
),
37+
);
1938
}
2039
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:bb_mobile/core/failures/failure.dart';
2+
3+
sealed class AutoswapFailure extends Failure {
4+
const AutoswapFailure([super.logMessage]);
5+
}
6+
7+
/// The stored settings, the wallet list or the app settings could not be read,
8+
/// so the form cannot be populated.
9+
final class AutoswapSettingsUnavailableFailure extends AutoswapFailure {
10+
const AutoswapSettingsUnavailableFailure([super.logMessage]);
11+
}
12+
13+
/// Writing the settings failed.
14+
final class AutoswapSettingsSaveFailure extends AutoswapFailure {
15+
const AutoswapSettingsSaveFailure([super.logMessage]);
16+
}
17+
18+
/// Auto swap is being enabled without a wallet to send the swapped funds to.
19+
/// Only enforced while enabling — disabling needs no recipient.
20+
final class AutoswapRecipientWalletRequiredFailure extends AutoswapFailure {
21+
const AutoswapRecipientWalletRequiredFailure();
22+
}
23+
24+
/// The target balance is below the minimum a swap can be made for.
25+
///
26+
/// Carries the limit in sats because the message states it. The *unit* it is
27+
/// rendered in is the user's current display preference, so that choice stays
28+
/// in presentation — `BitcoinUnit` lives in a Flutter-importing file and must
29+
/// not reach this layer.
30+
final class AutoswapBalanceThresholdTooLowFailure extends AutoswapFailure {
31+
final int minimumSats;
32+
33+
const AutoswapBalanceThresholdTooLowFailure(this.minimumSats);
34+
}
35+
36+
/// The trigger balance is not at least twice the target balance, so a swap
37+
/// would leave the wallet below its target immediately.
38+
final class AutoswapTriggerBalanceTooLowFailure extends AutoswapFailure {
39+
const AutoswapTriggerBalanceTooLowFailure();
40+
}
41+
42+
/// The accepted fee ceiling is above what we allow to be set.
43+
final class AutoswapFeeThresholdTooHighFailure extends AutoswapFailure {
44+
final int maximumPercent;
45+
46+
const AutoswapFeeThresholdTooHighFailure(this.maximumPercent);
47+
}
48+
49+
/// Catch-all. [logMessage] is for logs ONLY and MUST never reach the UI — the
50+
/// presentation extension returns the shared generic string.
51+
final class AutoswapUnexpectedFailure extends AutoswapFailure {
52+
const AutoswapUnexpectedFailure([super.logMessage]);
53+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'package:bb_mobile/core/settings/domain/get_settings_usecase.dart';
2+
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
3+
import 'package:bb_mobile/core/swaps/domain/entity/auto_swap.dart';
4+
import 'package:bb_mobile/core/swaps/domain/usecases/get_auto_swap_settings_usecase.dart';
5+
import 'package:bb_mobile/core/utils/logger.dart';
6+
import 'package:bb_mobile/core/utils/result.dart';
7+
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
8+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
9+
import 'package:bb_mobile/features/autoswap/domain/autoswap_failure.dart';
10+
import 'package:meta/meta.dart';
11+
12+
/// Everything the settings form needs to populate itself.
13+
typedef AutoswapSettingsData = ({
14+
AutoSwap settings,
15+
BitcoinUnit bitcoinUnit,
16+
List<Wallet> bitcoinWallets,
17+
String? recipientWalletId,
18+
});
19+
20+
class LoadAutoswapSettingsUsecase {
21+
final GetAutoSwapSettingsUsecase _getAutoSwapSettingsUsecase;
22+
final GetSettingsUsecase _getSettingsUsecase;
23+
final WalletRepository _walletRepository;
24+
25+
const LoadAutoswapSettingsUsecase({
26+
required this._getAutoSwapSettingsUsecase,
27+
required this._getSettingsUsecase,
28+
required this._walletRepository,
29+
});
30+
31+
@useResult
32+
Future<Result<AutoswapSettingsData, AutoswapFailure>> execute() async {
33+
try {
34+
final appSettings = await _getSettingsUsecase.execute();
35+
final autoSwapSettings = await _getAutoSwapSettingsUsecase.execute();
36+
37+
final wallets = await _walletRepository.getWallets(
38+
environment: appSettings.environment,
39+
);
40+
final bitcoinWallets = wallets.where((w) => !w.isLiquid).toList();
41+
final defaultBitcoinWallet = bitcoinWallets
42+
.where((w) => w.isDefault)
43+
.firstOrNull;
44+
45+
return Ok((
46+
settings: autoSwapSettings,
47+
bitcoinUnit: appSettings.bitcoinUnit,
48+
bitcoinWallets: bitcoinWallets,
49+
recipientWalletId:
50+
autoSwapSettings.recipientWalletId ?? defaultBitcoinWallet?.id,
51+
));
52+
} catch (e, st) {
53+
log.severe(
54+
message: 'Failed to load auto swap settings',
55+
error: e,
56+
trace: st,
57+
);
58+
return Err(AutoswapSettingsUnavailableFailure(e.runtimeType.toString()));
59+
}
60+
}
61+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'package:bb_mobile/core/swaps/domain/entity/auto_swap.dart';
2+
import 'package:bb_mobile/core/swaps/domain/usecases/save_auto_swap_settings_usecase.dart';
3+
import 'package:bb_mobile/core/utils/logger.dart';
4+
import 'package:bb_mobile/core/utils/result.dart';
5+
import 'package:bb_mobile/features/autoswap/domain/autoswap_failure.dart';
6+
import 'package:meta/meta.dart';
7+
8+
class SaveAutoswapSettingsUsecase {
9+
final SaveAutoSwapSettingsUsecase _saveAutoSwapSettingsUsecase;
10+
11+
const SaveAutoswapSettingsUsecase({
12+
required this._saveAutoSwapSettingsUsecase,
13+
});
14+
15+
/// Refuses settings the entity rejects, then writes them.
16+
///
17+
/// Validating here rather than in the caller means no future caller can
18+
/// persist settings that break the rules.
19+
@useResult
20+
Future<Result<void, AutoswapFailure>> execute(AutoSwap settings) async {
21+
final violation = settings.violation;
22+
if (violation != null) return Err(_failureFor(violation));
23+
24+
try {
25+
await _saveAutoSwapSettingsUsecase.execute(settings);
26+
return const Ok(null);
27+
} catch (e, st) {
28+
log.severe(
29+
message: 'Failed to save auto swap settings',
30+
error: e,
31+
trace: st,
32+
);
33+
return Err(AutoswapSettingsSaveFailure(e.runtimeType.toString()));
34+
}
35+
}
36+
37+
AutoswapFailure _failureFor(AutoSwapSettingsViolation violation) =>
38+
switch (violation) {
39+
AutoSwapSettingsViolation.recipientWalletMissing =>
40+
const AutoswapRecipientWalletRequiredFailure(),
41+
AutoSwapSettingsViolation.balanceThresholdTooLow =>
42+
const AutoswapBalanceThresholdTooLowFailure(
43+
AutoSwap.minimumBalanceThresholdSats,
44+
),
45+
AutoSwapSettingsViolation.triggerBalanceTooLow =>
46+
const AutoswapTriggerBalanceTooLowFailure(),
47+
AutoSwapSettingsViolation.feeThresholdTooHigh =>
48+
const AutoswapFeeThresholdTooHighFailure(
49+
AutoSwap.maximumFeeThresholdPercent,
50+
),
51+
};
52+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
2+
import 'package:bb_mobile/core/utils/amount_conversions.dart';
3+
import 'package:bb_mobile/core/utils/build_context_x.dart';
4+
import 'package:bb_mobile/features/autoswap/domain/autoswap_failure.dart';
5+
import 'package:flutter/widgets.dart';
6+
7+
extension AutoswapFailureL10n on AutoswapFailure {
8+
String toTranslated(BuildContext context, {BitcoinUnit? unit}) =>
9+
switch (this) {
10+
AutoswapSettingsUnavailableFailure() =>
11+
context.loc.autoswapLoadSettingsError,
12+
AutoswapSettingsSaveFailure() =>
13+
context.loc.autoswapUpdateSettingsError,
14+
AutoswapRecipientWalletRequiredFailure() =>
15+
context.loc.autoswapSelectWalletError,
16+
AutoswapBalanceThresholdTooLowFailure(:final minimumSats) =>
17+
unit == BitcoinUnit.btc
18+
? context.loc.autoswapMinimumThresholdErrorBtc(
19+
ConvertAmount.satsToBtc(minimumSats).toString(),
20+
)
21+
: context.loc.autoswapMinimumThresholdErrorSats('$minimumSats'),
22+
AutoswapTriggerBalanceTooLowFailure() =>
23+
context.loc.autoswapTriggerBalanceError,
24+
AutoswapFeeThresholdTooHighFailure(:final maximumPercent) =>
25+
context.loc.autoswapMaximumFeeError('$maximumPercent'),
26+
AutoswapUnexpectedFailure() => context.loc.oopsSomethingWentWrong,
27+
};
28+
}

0 commit comments

Comments
 (0)