Skip to content

Commit b8a4015

Browse files
authored
Reduce Rush fee when balance is above minimum but below minimum + fee (#128)
Reduce Rush fee when balance is above minimum but below minimum + fee When a user's balance exceeds the minimum withdrawal amount but is insufficient to cover the Rush fee, reduce the Rush fee at speed selection time so the withdrawal amount is exactly the minimum (5,000 sats). Priority speed remains unselectable in these circumstances.
1 parent 6865034 commit b8a4015

File tree

2 files changed

+94
-41
lines changed

2 files changed

+94
-41
lines changed

outie/src/main/kotlin/xyz/block/bittycity/outie/controllers/InfoCollectionController.kt

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ class SpeedHandler @Inject constructor(
463463
selectable = isSelectable(
464464
speedOption,
465465
amount,
466+
currentBalance,
466467
Bitcoins(amountFreeTierMin),
467468
Bitcoins(minimum),
468469
Bitcoins(amountFreeTierMin),
@@ -472,51 +473,55 @@ class SpeedHandler @Inject constructor(
472473
} else {
473474
speedOption.copy(selectable = false)
474475
}
475-
else ->
476-
if (currentBalance.units < amountFreeTierMin) {
477-
val minAmount = Bitcoins(minimum)
478-
val maxAmount = currentBalance - speedOption.totalFee
479-
speedOption.copy(
480-
minimumAmount = minAmount,
481-
maximumAmount = maxAmount,
482-
selectable = isSelectable(
483-
speedOption,
484-
amount,
485-
Bitcoins(amountFreeTierMin),
486-
Bitcoins(minimum),
487-
minAmount,
488-
maxAmount
489-
)
490-
)
476+
else -> {
477+
val minAmount = Bitcoins(minimum)
478+
val maxAmount = currentBalance - speedOption.totalFee
479+
// For Rush, if the fee pushes maxAmount below minAmount but the balance exceeds
480+
// the minimum, the fee will be reduced at selection time. Reflect that here so the
481+
// global min/max check in getHurdles doesn't reject the withdrawal.
482+
val effectiveMaxAmount = if (
483+
speedOption.speed == RUSH &&
484+
maxAmount < minAmount &&
485+
currentBalance > minAmount
486+
) {
487+
minAmount
491488
} else {
492-
val minAmount = Bitcoins(minimum)
493-
val maxAmount = currentBalance - speedOption.totalFee
494-
speedOption.copy(
495-
minimumAmount = minAmount,
496-
maximumAmount = maxAmount,
497-
selectable = isSelectable(
498-
speedOption,
499-
amount,
500-
Bitcoins(amountFreeTierMin),
501-
Bitcoins(minimum),
502-
minAmount,
503-
maxAmount
504-
)
505-
)
489+
maxAmount
506490
}
491+
speedOption.copy(
492+
minimumAmount = minAmount,
493+
maximumAmount = effectiveMaxAmount,
494+
selectable = isSelectable(
495+
speedOption,
496+
amount,
497+
currentBalance,
498+
Bitcoins(amountFreeTierMin),
499+
Bitcoins(minimum),
500+
minAmount,
501+
effectiveMaxAmount
502+
)
503+
)
504+
}
507505
}
508506
}
509507

510508
@Suppress("LongParameterList")
511509
fun isSelectable(
512510
speedOption: WithdrawalSpeedOption,
513511
amount: Bitcoins,
512+
currentBalance: Bitcoins,
514513
amountFreeTierMin: Bitcoins,
515514
minimumAmount: Bitcoins,
516515
calculatedMinAmount: Bitcoins,
517516
calculatedMaxAmount: Bitcoins,
518517
): Boolean? = when (speedOption.speed) {
519518
STANDARD -> amount >= amountFreeTierMin
519+
RUSH -> {
520+
// Rush is selectable if the customer's balance exceeds the minimum withdrawal amount,
521+
// even if fees would normally push maxAmount below minAmount. In that case, the fee
522+
// will be reduced at selection time so the withdrawal amount is exactly the minimum.
523+
amount >= minimumAmount && currentBalance > minimumAmount
524+
}
520525
else -> {
521526
amount >= minimumAmount && calculatedMaxAmount >= calculatedMinAmount
522527
}
@@ -537,17 +542,32 @@ class SpeedHandler @Inject constructor(
537542
val selectedSpeed = response.selectedSpeed
538543
?: raise(ParameterIsRequired(value.customerId, "selectedSpeed"))
539544

545+
val storedOption =
546+
withdrawalStore.findSpeedOptionByWithdrawalTokenAndSpeed(value.id, selectedSpeed).bind()
547+
?: raise(
548+
IllegalStateException(
549+
"No speed option for speed $selectedSpeed for withdrawal ${value.id}"
550+
)
551+
)
552+
553+
// For Rush speed, if the normal fee would push the withdrawal amount below the minimum,
554+
// reduce the fee so the customer can withdraw exactly the minimum amount.
555+
val effectiveOption = if (
556+
storedOption.speed == RUSH &&
557+
balance - storedOption.totalFee < Bitcoins(minimum) &&
558+
balance > Bitcoins(minimum)
559+
) {
560+
val reducedFee = balance - Bitcoins(minimum)
561+
storedOption.copy(
562+
totalFee = reducedFee,
563+
totalFeeFiatEquivalent = bitcoinsToUsd(reducedFee, exchangeRate),
564+
)
565+
} else {
566+
storedOption
567+
}
568+
540569
val option =
541-
addAmountsAndSelectability(
542-
withdrawalStore.findSpeedOptionByWithdrawalTokenAndSpeed(value.id, selectedSpeed).bind()
543-
?: raise(
544-
IllegalStateException(
545-
"No speed option for speed $selectedSpeed for withdrawal ${value.id}"
546-
)
547-
),
548-
balance,
549-
amount
550-
).map {
570+
addAmountsAndSelectability(effectiveOption, balance, amount).map {
551571
addAdjustedAmount(it, amount, exchangeRate).bind()
552572
}.bind()
553573

outie/src/test/kotlin/xyz/block/bittycity/outie/controllers/SpeedHandlerTest.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ class SpeedHandlerTest : BittyCityTestCase() {
189189
}
190190

191191
@Test
192-
fun `it's impossible to withdraw a balance that is too low`() = runTest {
192+
fun `rush is selectable with reduced fee when balance barely covers minimum`() = runTest {
193193
val amount = Bitcoins(6_999L)
194194
val withdrawal = data.seedWithdrawal(
195195
state = CollectingInfo,
@@ -200,6 +200,39 @@ class SpeedHandlerTest : BittyCityTestCase() {
200200

201201
setupFakes(currentBalance)
202202

203+
val speedHurdle = (
204+
subject.getHurdles(withdrawal, currentBalance).getOrThrow()
205+
.first() as WithdrawalHurdle.SpeedHurdle
206+
)
207+
208+
speedHurdle.withdrawalSpeedOptions.size shouldBe 3
209+
speedHurdle.withdrawalSpeedOptions[0].should { priority ->
210+
priority.selectable shouldBe false
211+
priority.adjustedAmount.shouldBeNull()
212+
}
213+
speedHurdle.withdrawalSpeedOptions[1].should { rush ->
214+
rush.selectable shouldBe true
215+
rush.maximumAmount shouldBe Bitcoins(5_000L)
216+
rush.adjustedAmount shouldBe Bitcoins(5_000L)
217+
}
218+
speedHurdle.withdrawalSpeedOptions[2].should { standard ->
219+
standard.selectable shouldBe false
220+
standard.adjustedAmount.shouldBeNull()
221+
}
222+
}
223+
224+
@Test
225+
fun `it's impossible to withdraw a balance that is too low`() = runTest {
226+
val amount = Bitcoins(5_000L)
227+
val withdrawal = data.seedWithdrawal(
228+
state = CollectingInfo,
229+
walletAddress = data.targetWalletAddress,
230+
amount = amount
231+
)
232+
val currentBalance = Bitcoins(5_000L)
233+
234+
setupFakes(currentBalance)
235+
203236
shouldThrow<InsufficientBalance> {
204237
(
205238
subject.getHurdles(withdrawal, currentBalance).getOrThrow()

0 commit comments

Comments
 (0)