From 721b5b58f21a9aa3afe876d8759708c2b19829d4 Mon Sep 17 00:00:00 2001 From: Gleb Levinkov Date: Sun, 28 Jan 2024 15:23:18 +0300 Subject: [PATCH] PWN-838 - fix min SOL amount again --- .../p2p/wallet/send/SendFeeRelayerManager.kt | 2 +- .../p2p/wallet/send/model/CalculationMode.kt | 5 + .../wallet/send/model/NewSendButtonState.kt | 2 +- .../NewSendButtonStateMinSolValidator.kt | 63 ---------- .../model/SendButtonStateMinSolValidator.kt | 81 ++++++++++++ .../p2p/wallet/send/ui/NewSendPresenter.kt | 3 + .../SendButtonStateMinSolValidatorTest.kt | 116 ++++++++++++++++++ 7 files changed, 207 insertions(+), 65 deletions(-) delete mode 100644 app/src/main/java/org/p2p/wallet/send/model/NewSendButtonStateMinSolValidator.kt create mode 100644 app/src/main/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidator.kt create mode 100644 app/src/test/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidatorTest.kt diff --git a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt index 3dc10cf47c..0c78bf3e8d 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -295,7 +295,7 @@ class SendFeeRelayerManager( TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH ) - Timber.i("Requesting minRentExemption for token_program: $minRentExemption") + Timber.i("Requesting minRentExemption for spl_token_program: $minRentExemption") var transactionFee = BigInteger.ZERO diff --git a/app/src/main/java/org/p2p/wallet/send/model/CalculationMode.kt b/app/src/main/java/org/p2p/wallet/send/model/CalculationMode.kt index e96b4fb652..a9980b0a5e 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/CalculationMode.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/CalculationMode.kt @@ -144,6 +144,11 @@ class CalculationMode( fun getCurrencyMode(): CurrencyMode = currencyMode + fun getDebugInfo(): String = buildString { + val remainingBalance = token.totalInLamports - getCurrentAmountLamports() + append("Remaining balance: $remainingBalance") + } + fun isMaxButtonVisible(minRentExemption: BigInteger): Boolean { return if (token.isSOL) { val maxAllowedAmount = token.totalInLamports - minRentExemption diff --git a/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonState.kt b/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonState.kt index 36aeda2bf5..348febdbf4 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonState.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonState.kt @@ -23,7 +23,7 @@ class NewSendButtonState( private val resources: Resources ) { - private val minSolValidator = NewSendButtonStateMinSolValidator( + private val minSolValidator = SendButtonStateMinSolValidator( tokenToSend = tokenToSend, minRentExemption = minRentExemption, recipient = recipient diff --git a/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonStateMinSolValidator.kt b/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonStateMinSolValidator.kt deleted file mode 100644 index ddf2c82fc4..0000000000 --- a/app/src/main/java/org/p2p/wallet/send/model/NewSendButtonStateMinSolValidator.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.p2p.wallet.send.model - -import java.math.BigInteger -import org.p2p.core.token.Token -import org.p2p.core.utils.isZero - -sealed interface SendSolValidation { - object AmountIsValid : SendSolValidation - object EmptyRecipientMinAmountInvalid : SendSolValidation - object SenderNotZeroed : SendSolValidation - object SenderNoRentExemptAmountLeft : SendSolValidation -} - -/** - * One place to keep all the logic regarding min SOL validation - */ -class NewSendButtonStateMinSolValidator( - private val tokenToSend: Token.Active, - private val minRentExemption: BigInteger, - private val recipient: SearchResult -) { - - /** - * This case is only for sending SOL - * - * 1. The recipient should receive at least [minRentExemption] SOL balance if the recipient's current balance is 0 - * 2. The recipient should have at least [minRentExemption] after the transaction - * - * 1. The sender is allowed to sent exactly the whole balance. - * 2. It's allowed for the sender to have a SOL balance 0 or at least [minRentExemption] - * */ - fun validateAmount(inputAmount: BigInteger): SendSolValidation { - if (!tokenToSend.isSOL) return SendSolValidation.AmountIsValid - - val isRecipientEmpty = recipient is SearchResult.AddressFound && recipient.isEmptyBalance - return if (isRecipientEmpty) { - // rentExemption or more - val sendingMoreThanMinRent = inputAmount >= minRentExemption - if (sendingMoreThanMinRent) { - SendSolValidation.AmountIsValid - } else { - SendSolValidation.EmptyRecipientMinAmountInvalid - } - } else { - val balanceRemaining = tokenToSend.totalInLamports - inputAmount - val isSendingFull = balanceRemaining.isZero() - val isRentExemptionRemaining = balanceRemaining >= minRentExemption - - if (isRentExemptionRemaining || isSendingFull) { - return SendSolValidation.AmountIsValid - } - if (!isRentExemptionRemaining) { - return SendSolValidation.SenderNoRentExemptAmountLeft - } - // we are not emptying account to 0, we are left with some crumbs - // that are less than minRentExemption - if (!isSendingFull) { - return SendSolValidation.SenderNotZeroed - } - return SendSolValidation.SenderNotZeroed - } - } -} diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidator.kt b/app/src/main/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidator.kt new file mode 100644 index 0000000000..8645d75223 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidator.kt @@ -0,0 +1,81 @@ +package org.p2p.wallet.send.model + +import timber.log.Timber +import java.math.BigInteger +import org.p2p.core.token.Token +import org.p2p.core.utils.isZero + +sealed interface SendSolValidation { + object AmountIsValid : SendSolValidation + object EmptyRecipientMinAmountInvalid : SendSolValidation + object SenderNotZeroed : SendSolValidation + object SenderNoRentExemptAmountLeft : SendSolValidation +} + +/** + * One place to keep all the logic regarding min SOL validation + */ +class SendButtonStateMinSolValidator( + private val tokenToSend: Token.Active, + private val minRentExemption: BigInteger, + private val recipient: SearchResult +) { + + /** + * This case is only for sending SOL + * + * 1. The recipient should receive at least [minRentExemption] SOL balance if the recipient's current balance is 0 + * 2. The recipient should have at least [minRentExemption] after the transaction + * + * 1. The sender is allowed to sent exactly the whole balance. + * 2. It's allowed for the sender to have a SOL balance 0 or at least [minRentExemption] + * */ + fun validateAmount(inputAmount: BigInteger): SendSolValidation { + if (!tokenToSend.isSOL) return SendSolValidation.AmountIsValid + + val isRecipientEmpty = recipient is SearchResult.AddressFound && recipient.isEmptyBalance + val balanceRemaining = tokenToSend.totalInLamports - inputAmount + val isSendingAllSol = balanceRemaining.isZero() + val isRentExemptionRemaining = balanceRemaining >= minRentExemption + val sendingMoreThanMinRent = inputAmount >= minRentExemption + + Timber.i( + buildString { + appendLine("$minRentExemption") + appendLine("-----") + appendLine("isRecipientEmpty = $isRecipientEmpty") + appendLine("balanceRemaining = $balanceRemaining") + appendLine("isSendingAllSol = $isSendingAllSol") + appendLine("isRentExemptionRemaining = $isRentExemptionRemaining") + appendLine("sendingMoreThanMinRent = $sendingMoreThanMinRent") + } + ) + + val isSolAmountCanBeSent = isSendingAllSol || isRentExemptionRemaining + + if (isRecipientEmpty) { + if (!sendingMoreThanMinRent) { + return SendSolValidation.EmptyRecipientMinAmountInvalid + } + if (isSendingAllSol) { + return SendSolValidation.AmountIsValid + } + + if (!isRentExemptionRemaining && !isSendingAllSol) { + return SendSolValidation.SenderNoRentExemptAmountLeft + } + if (!isSendingAllSol && !isRentExemptionRemaining) { + return SendSolValidation.SenderNoRentExemptAmountLeft + } + } + + if (isSendingAllSol) { + return SendSolValidation.AmountIsValid + } + // if we are not sending all SOL but leave some dust that less than rent + if (!isRentExemptionRemaining) { + return SendSolValidation.SenderNoRentExemptAmountLeft + } + return SendSolValidation.AmountIsValid + } +} diff --git a/app/src/main/java/org/p2p/wallet/send/ui/NewSendPresenter.kt b/app/src/main/java/org/p2p/wallet/send/ui/NewSendPresenter.kt index 9f17fd842e..cac374bde3 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/NewSendPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/NewSendPresenter.kt @@ -637,6 +637,9 @@ class NewSendPresenter( private fun buildDebugInfo(solanaFee: SendSolanaFee?) { launch { val debugInfo = sendFeeRelayerManager.buildDebugInfo(solanaFee) + .plus("\n") + .plus(calculationMode.getDebugInfo()) + view?.showDebugInfo(debugInfo) } } diff --git a/app/src/test/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidatorTest.kt b/app/src/test/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidatorTest.kt new file mode 100644 index 0000000000..089d55231b --- /dev/null +++ b/app/src/test/java/org/p2p/wallet/send/model/SendButtonStateMinSolValidatorTest.kt @@ -0,0 +1,116 @@ +package org.p2p.wallet.send.model + +import assertk.assertions.isInstanceOf +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import java.math.BigInteger +import org.p2p.core.token.Token +import org.p2p.core.utils.fromLamports +import org.p2p.wallet.utils.assertThat + +class SendButtonStateMinSolValidatorTest { + + private lateinit var solValidator: SendButtonStateMinSolValidator + + private val emptyRecipient = mockk { + every { isEmptyBalance }.returns(true) + } + + private val nonEmptyRecipient = mockk { + every { isEmptyBalance }.returns(false) + } + + @Test + fun `GIVEN empty recipient THEN validator cases work`() { + // entering amount that less than minRentExemption + val minRentExemption = BigInteger("890000") + // "0.015000000" + val solAmount = BigInteger.valueOf(15_000_000) + + solValidator = SendButtonStateMinSolValidator( + tokenToSend = createCustomSol(solAmount), + minRentExemption = minRentExemption, + recipient = emptyRecipient + ) + + // enter less than min rent + var input = BigInteger.valueOf(880_000) + var result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.EmptyRecipientMinAmountInvalid::class) + + // enter more == min rent + input = "890000".toBigInteger() + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter more than min rent + input = "890001".toBigInteger() + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter full amount + input = solAmount + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter amount but mint rent is not left + input = BigInteger.valueOf(14_999_000) + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.SenderNoRentExemptAmountLeft::class) + } + + @Test + fun `GIVEN non-empty recipient THEN validator cases work`() { + val minRentExemption = BigInteger.valueOf(890_000) + // "0.015000000" + val solAmount = BigInteger.valueOf(15_000_000) + + solValidator = SendButtonStateMinSolValidator( + tokenToSend = createCustomSol(solAmount), + minRentExemption = minRentExemption, + recipient = nonEmptyRecipient + ) + + // enter less than min rent + var input = BigInteger.valueOf(880_000) + var result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter equals min rent + input = "890000".toBigInteger() + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter more than min rent + input = "890001".toBigInteger() + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter full amount + input = solAmount + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.AmountIsValid::class) + + // enter amount but mint rent is not left + input = BigInteger.valueOf(14_999_000) + result = solValidator.validateAmount(input) + result.assertThat() + .isInstanceOf(SendSolValidation.SenderNoRentExemptAmountLeft::class) + } + + private fun createCustomSol(solAmount: BigInteger): Token.Active = mockk { + every { isSOL }.returns(true) + every { totalInLamports }.returns(solAmount) + every { total }.returns(solAmount.fromLamports(9)) + } +}