Skip to content

Commit 1bf43d4

Browse files
refactor(swaps): move auto swap settings rules onto the entity
1 parent ad6c1b5 commit 1bf43d4

2 files changed

Lines changed: 161 additions & 0 deletions

File tree

lib/core/swaps/domain/entity/auto_swap.dart

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import 'package:freezed_annotation/freezed_annotation.dart';
33
part 'auto_swap.freezed.dart';
44
part 'auto_swap.g.dart';
55

6+
/// Which rule a set of auto swap settings breaks, in the order they are
7+
/// checked. Callers map this to whatever they show the user.
8+
enum AutoSwapSettingsViolation {
9+
recipientWalletMissing,
10+
balanceThresholdTooLow,
11+
triggerBalanceTooLow,
12+
feeThresholdTooHigh,
13+
}
14+
615
@freezed
716
sealed class AutoSwap with _$AutoSwap {
817
const factory AutoSwap({
@@ -21,6 +30,54 @@ sealed class AutoSwap with _$AutoSwap {
2130
factory AutoSwap.fromJson(Map<String, dynamic> json) =>
2231
_$AutoSwapFromJson(json);
2332

33+
/// Below this a swap would move less than it costs in fees.
34+
static const int minimumBalanceThresholdSats = 50000;
35+
36+
/// Accepting a fee ceiling above this is almost certainly a mistake.
37+
static const int maximumFeeThresholdPercent = 10;
38+
39+
/// Applied when a fee ceiling cannot be determined.
40+
static const double defaultFeeThresholdPercent = 3.0;
41+
42+
static bool isBalanceThresholdTooLow(int balanceThresholdSats) =>
43+
balanceThresholdSats < minimumBalanceThresholdSats;
44+
45+
/// A swap has to leave the wallet at its target, so the trigger must be at
46+
/// least twice the target — otherwise a swap would fire and immediately
47+
/// leave the balance below where it started.
48+
static bool isTriggerBalanceTooLow({
49+
required int balanceThresholdSats,
50+
required int triggerBalanceSats,
51+
}) => triggerBalanceSats < 2 * balanceThresholdSats;
52+
53+
static bool isFeeThresholdTooHigh(double feeThresholdPercent) =>
54+
feeThresholdPercent > maximumFeeThresholdPercent;
55+
56+
/// The first rule these settings break, or null when they are acceptable.
57+
///
58+
/// Deliberately not enforced in the constructor: these settings are
59+
/// persisted and deserialized, so a row written by an older version must
60+
/// still load even if it would no longer be accepted.
61+
AutoSwapSettingsViolation? get violation {
62+
// Only required while enabled: switching auto swap off needs no recipient.
63+
if (enabled && recipientWalletId == null) {
64+
return AutoSwapSettingsViolation.recipientWalletMissing;
65+
}
66+
if (isBalanceThresholdTooLow(balanceThresholdSats)) {
67+
return AutoSwapSettingsViolation.balanceThresholdTooLow;
68+
}
69+
if (isTriggerBalanceTooLow(
70+
balanceThresholdSats: balanceThresholdSats,
71+
triggerBalanceSats: triggerBalanceSats,
72+
)) {
73+
return AutoSwapSettingsViolation.triggerBalanceTooLow;
74+
}
75+
if (isFeeThresholdTooHigh(feeThresholdPercent)) {
76+
return AutoSwapSettingsViolation.feeThresholdTooHigh;
77+
}
78+
return null;
79+
}
80+
2481
bool passedRequiredBalance(int balanceSat) {
2582
return balanceSat >= triggerBalanceSats && enabled;
2683
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'package:bb_mobile/core/swaps/domain/entity/auto_swap.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
/// Settings that break no rule, for tests to bend one field at a time.
5+
const _valid = AutoSwap(
6+
enabled: true,
7+
balanceThresholdSats: 100000,
8+
triggerBalanceSats: 200000,
9+
feeThresholdPercent: 3,
10+
recipientWalletId: 'wallet-id',
11+
);
12+
13+
void main() {
14+
group('AutoSwap.violation', () {
15+
test('accepts settings that break no rule', () {
16+
expect(_valid.violation, isNull);
17+
});
18+
19+
test('requires a recipient wallet only while enabled', () {
20+
expect(
21+
_valid.copyWith(recipientWalletId: null).violation,
22+
AutoSwapSettingsViolation.recipientWalletMissing,
23+
);
24+
25+
// Switching auto swap off must not demand a recipient, or turning the
26+
// feature off would be impossible.
27+
expect(
28+
_valid.copyWith(enabled: false, recipientWalletId: null).violation,
29+
isNull,
30+
);
31+
});
32+
33+
test('rejects a target balance below the minimum', () {
34+
expect(
35+
_valid.copyWith(balanceThresholdSats: 49999).violation,
36+
AutoSwapSettingsViolation.balanceThresholdTooLow,
37+
);
38+
expect(
39+
_valid
40+
.copyWith(balanceThresholdSats: 50000, triggerBalanceSats: 100000)
41+
.violation,
42+
isNull,
43+
);
44+
});
45+
46+
test('rejects a trigger balance below twice the target', () {
47+
expect(
48+
_valid.copyWith(triggerBalanceSats: 199999).violation,
49+
AutoSwapSettingsViolation.triggerBalanceTooLow,
50+
);
51+
expect(_valid.copyWith(triggerBalanceSats: 200000).violation, isNull);
52+
});
53+
54+
test('rejects a fee ceiling above the maximum', () {
55+
expect(
56+
_valid.copyWith(feeThresholdPercent: 10.1).violation,
57+
AutoSwapSettingsViolation.feeThresholdTooHigh,
58+
);
59+
expect(_valid.copyWith(feeThresholdPercent: 10).violation, isNull);
60+
});
61+
62+
test('reports the recipient rule before the amount rules', () {
63+
final broken = _valid.copyWith(
64+
recipientWalletId: null,
65+
balanceThresholdSats: 1,
66+
triggerBalanceSats: 1,
67+
feeThresholdPercent: 99,
68+
);
69+
70+
expect(
71+
broken.violation,
72+
AutoSwapSettingsViolation.recipientWalletMissing,
73+
);
74+
});
75+
76+
test('reports the balance rule before the trigger rule', () {
77+
// Both broken; the target is the field the user must fix first.
78+
expect(
79+
_valid
80+
.copyWith(balanceThresholdSats: 1000, triggerBalanceSats: 1)
81+
.violation,
82+
AutoSwapSettingsViolation.balanceThresholdTooLow,
83+
);
84+
});
85+
86+
test('a persisted row that breaks a rule still deserializes', () {
87+
// The rules are not constructor-enforced on purpose: settings written by
88+
// an older version must still load, or the feature becomes unopenable.
89+
final legacy = AutoSwap.fromJson(const {
90+
'enabled': true,
91+
'balanceThresholdSats': 10000,
92+
'triggerBalanceSats': 15000,
93+
'feeThresholdPercent': 50.0,
94+
'blockTillNextExecution': false,
95+
'alwaysBlock': false,
96+
'recipientWalletId': null,
97+
'showWarning': true,
98+
});
99+
100+
expect(legacy.balanceThresholdSats, 10000);
101+
expect(legacy.violation, isNotNull);
102+
});
103+
});
104+
}

0 commit comments

Comments
 (0)