Skip to content

Commit

Permalink
ETH-946 - save send fee payer (#2194)
Browse files Browse the repository at this point in the history
* ETH-946 - save send fee payer

- fixed fee payer token auto selection
- clarified some points

* ETH-946 - a bit cleaned up presenter, added check for compensating token, fixed "fromLamports"

* ETH-946 - removed unnecessary bigdecimal scaling

* ETH-946 - renamed power value to base because it's a base value, not a power
  • Loading branch information
eduardmaximovich authored Feb 14, 2024
1 parent 27a2c3a commit 9a492af
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 185 deletions.
33 changes: 23 additions & 10 deletions app/src/main/java/org/p2p/wallet/feerelayer/model/FeeRelayerFee.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,40 @@ import kotlinx.parcelize.Parcelize
import org.p2p.solanaj.core.FeeAmount

@Parcelize
data class FeeRelayerFee constructor(
data class FeeRelayerFee(
val transactionFeeInSol: BigInteger,
val accountCreationFeeInSol: BigInteger,

val transactionFeeInSpl: BigInteger,
val accountCreationFeeInSpl: BigInteger,
val transactionFeeInFeePayerToken: BigInteger,
val accountCreationFeeInFeePayerToken: BigInteger,

val transactionFeeInSourceToken: BigInteger,
val accountCreateFeeInSourceToken: BigInteger,

val expectedFee: FeeAmount
) : Parcelable {

constructor(feeInSol: FeeAmount, feeInSpl: FeeAmount, expectedFee: FeeAmount) : this(
transactionFeeInSol = feeInSol.transactionFee,
accountCreationFeeInSol = feeInSol.accountCreationFee,
transactionFeeInSpl = feeInSpl.transactionFee,
accountCreationFeeInSpl = feeInSpl.accountCreationFee,
constructor(
feesInSol: FeeAmount,
feesInFeePayerToken: FeeAmount,
feesInSourceToken: FeeAmount,
expectedFee: FeeAmount
) : this(
transactionFeeInSol = feesInSol.transactionFee,
accountCreationFeeInSol = feesInSol.accountCreationFee,
transactionFeeInFeePayerToken = feesInFeePayerToken.transactionFee,
accountCreationFeeInFeePayerToken = feesInFeePayerToken.accountCreationFee,
transactionFeeInSourceToken = feesInSourceToken.transactionFee,
accountCreateFeeInSourceToken = feesInSourceToken.accountCreationFee,
expectedFee = expectedFee
)

val totalInSol: BigInteger
get() = transactionFeeInSol + accountCreationFeeInSol

val totalInSpl: BigInteger
get() = transactionFeeInSpl + accountCreationFeeInSpl
val totalInFeePayerToken: BigInteger
get() = transactionFeeInFeePayerToken + accountCreationFeeInFeePayerToken

val totalInSourceToken: BigInteger
get() = transactionFeeInSourceToken + accountCreateFeeInSourceToken
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ object MainContainerModule : InjectionModule {
tokenKeyProvider = get(),
dispatchers = get(),
sendServiceRepository = get(),
feePayersRepository = get()
feePayersRepository = get(),
feePayerTokenValidityRepository = get(),
sendStorage = get(),
)
}
factoryOf(::SearchInteractor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ private const val PREFS_SWAP = "swap_prefs"
private const val PREFS_STRIGA = "striga_prefs"

object StorageModule {
const val PREFS_PNL = "pnl_prefs"
const val PREFS_SEND = "send_prefs"

private fun Scope.androidPreferences(prefsName: String): SharedPreferences {
return with(androidContext()) {
Expand Down Expand Up @@ -70,7 +70,7 @@ object StorageModule {
SecureStorageContract::class
factory { AccountStorage(get(named(PREFS_ACCOUNT))) } bind
AccountStorageContract::class
single { JupiterSwapStorage(androidPreferences(PREFS_SWAP),) } bind
single { JupiterSwapStorage(androidPreferences(PREFS_SWAP)) } bind
JupiterSwapStorageContract::class
factory { StrigaStorage(get(named(PREFS_STRIGA))) } bind
StrigaStorageContract::class
Expand All @@ -94,8 +94,8 @@ object StorageModule {
single(named(PREFS_STRIGA)) {
androidEncryptedPreferences(PREFS_STRIGA)
}
single(named(PREFS_PNL)) {
androidPreferences(PREFS_PNL)
single(named(PREFS_SEND)) {
androidPreferences(PREFS_SEND)
}
}

Expand Down
134 changes: 73 additions & 61 deletions app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import org.p2p.core.utils.Constants
import org.p2p.core.utils.formatTokenWithSymbol
import org.p2p.core.utils.fromLamports
import org.p2p.core.utils.isZero
import org.p2p.core.utils.orZero
import org.p2p.core.utils.scaleLong
import org.p2p.core.utils.toLamports
import org.p2p.core.utils.toUsd
import org.p2p.solanaj.core.FeeAmount
Expand Down Expand Up @@ -84,7 +82,7 @@ class SendFeeRelayerManager(
private val alternativeTokensMap: HashMap<String, List<Token.Active>> = HashMap()

suspend fun initialize(
initialToken: Token.Active,
feePayerToken: Token.Active,
solToken: Token.Active,
recipientAddress: SearchResult
) {
Expand All @@ -94,7 +92,7 @@ class SendFeeRelayerManager(

onFeeLoading?.invoke(FeeLoadingState.Instant(isLoading = true))
try {
initializeWithToken(initialToken)
initializeWithToken(feePayerToken)

initializeCompleted = true
} catch (e: Throwable) {
Expand All @@ -106,12 +104,12 @@ class SendFeeRelayerManager(
}
}

private suspend fun initializeWithToken(initialToken: Token.Active) {
Timber.tag(TAG).i("initialize for SendFeeRelayerManager with token ${initialToken.mintAddress}")
private suspend fun initializeWithToken(feePayerToken: Token.Active) {
Timber.tag(TAG).i("initialize for SendFeeRelayerManager with token ${feePayerToken.mintAddress}")
minRentExemption = sendInteractor.getMinRelayRentExemption()
feeLimitInfo = sendInteractor.getFreeTransactionsInfo()
currentSolanaEpoch = solanaRepository.getEpochInfo(useCache = true).epoch
sendInteractor.initialize(initialToken)
sendInteractor.initialize(feePayerToken)
}

fun getMinRentExemption(): BigInteger = minRentExemption
Expand Down Expand Up @@ -171,7 +169,7 @@ class SendFeeRelayerManager(
try {
onFeeLoading?.invoke(FeeLoadingState(isLoading = true, isDelayed = useCache))
if (!initializeCompleted) {
initializeWithToken(sourceToken)
initializeWithToken(feePayer)
initializeCompleted = true
}

Expand All @@ -189,7 +187,7 @@ class SendFeeRelayerManager(
feeLimitInfo = feeLimitInfo,
tokenExtensions = tokenExtensions
)
sendInteractor.setFeePayerToken(sourceToken)
sendInteractor.setFeePayerToken(feePayer)
}
is FeeCalculationState.PoolsNotFound -> {
val solanaFee = buildSolanaFee(
Expand Down Expand Up @@ -233,20 +231,8 @@ class SendFeeRelayerManager(
}
}

suspend fun buildDebugInfo(solanaFee: SendSolanaFee?): String {
val relayAccount = sendInteractor.getUserRelayAccount()
val relayInfo = sendInteractor.getRelayInfo()
fun buildDebugInfo(solanaFee: SendSolanaFee?): String {
return buildString {
append("Relay account is created: ${relayAccount.isCreated}, balance: ${relayAccount.balance} (A)")
appendLine()
append("Min relay account balance required: ${relayInfo.minimumRelayAccountRent} (B)")
appendLine()
if (relayAccount.balance != null) {
val diff = relayAccount.balance - relayInfo.minimumRelayAccountRent
append("Remainder (A - B): $diff (R)")
appendLine()
}

if (solanaFee == null) {
append("Expected total fee in SOL: 0 (E)")
appendLine()
Expand All @@ -255,26 +241,42 @@ class SendFeeRelayerManager(
append("Expected total fee in Token: 0 (T)")
} else {
val accountBalances = solanaFee.feeRelayerFee.expectedFee.accountCreationFee
val expectedFee = if (!relayAccount.isCreated) {
accountBalances + relayInfo.minimumRelayAccountRent
} else {
accountBalances
}
append("Expected total fee in SOL: $expectedFee (E)")
append("Expected total fee in SOL: $accountBalances (E)")
appendLine()

val neededTopUpAmount = solanaFee.feeRelayerFee.totalInSol
append("Needed top up amount (E - R): $neededTopUpAmount (S)")

appendLine()

val feePayerToken = solanaFee.feePayerToken
val expectedFeeInSpl = solanaFee.feeRelayerFee.totalInSpl.orZero()

val feePayerTokenValue = solanaFee
.feeRelayerFee
.totalInFeePayerToken
.fromLamports(feePayerToken.decimals)
.scaleLong()
append("Expected total fee in Token: $expectedFeeInSpl ${feePayerToken.tokenSymbol} (T)")
.formatTokenWithSymbol(
token = feePayerToken,
exactDecimals = true,
keepInitialDecimals = true
)

val sourceToken = solanaFee.sourceToken
val sourceTokenValue = solanaFee
.feeRelayerFee
.totalInSourceToken
.fromLamports(sourceToken.decimals)
.formatTokenWithSymbol(
token = sourceToken,
exactDecimals = true,
keepInitialDecimals = true
)

append("Expected total fee in Fee Payer Token: $feePayerTokenValue (T)")
appendLine()

append("Expected total fee in Source Token: $sourceTokenValue (T)")
appendLine()

append("[Token2022] Transfer Fee: ${solanaFee.token2022TransferFee}")
}
}
Expand All @@ -297,57 +299,67 @@ class SendFeeRelayerManager(

Timber.i("Requesting minRentExemption for spl_token_program: $minRentExemption")

var transactionFee = BigInteger.ZERO
var transactionFeeInSol = BigInteger.ZERO

// owner's signature
transactionFee += lamportsPerSignature
transactionFeeInSol += lamportsPerSignature

// feePayer's signature
if (!feePayerToken.isSOL) {
Timber.i("Fee payer is not sol, adding $lamportsPerSignature for fee")
transactionFee += lamportsPerSignature
transactionFeeInSol += lamportsPerSignature
}

val shouldCreateAccount = checkAccountCreationIsRequired(sourceToken, result.address)
Timber.i("Should create account = $shouldCreateAccount")

val accountCreationFee = if (shouldCreateAccount) minRentExemption else BigInteger.ZERO
val accountCreationFeeInSol = if (shouldCreateAccount) minRentExemption else BigInteger.ZERO

val expectedFee = FeeAmount(
transactionFee = transactionFee,
accountCreationFee = accountCreationFee,
val expectedFeesInSol = FeeAmount(
transactionFee = transactionFeeInSol,
accountCreationFee = accountCreationFeeInSol,
)

val fees = feeRelayerTopUpInteractor.calculateNeededTopUpAmount(expectedFee)
// todo: since we use send service it covers all network fees, no necessity to use fee relayer
// keep it here for a while, just in case
// val feesInSol = feeRelayerTopUpInteractor.calculateNeededTopUpAmount(expectedFeesInSol)

val feesInSol = expectedFeesInSol.copy(transactionFee = BigInteger.ZERO)

if (fees.totalFeeLamports.isZero()) {
if (feesInSol.totalFeeLamports.isZero()) {
Timber.i("Total fees are zero!")
return FeeCalculationState.NoFees
}

val poolsStateFees = getFeesInPayingTokenUseCase.execute(
feePayerToken = feePayerToken,
transactionFeeInSol = fees.transactionFee,
accountCreationFeeInSol = fees.accountCreationFee
val feesInFeePayerToken = getFeesInPayingTokenUseCase.execute(
targetToken = feePayerToken,
transactionFeeInSol = feesInSol.transactionFee,
accountCreationFeeInSol = feesInSol.accountCreationFee
)

if (poolsStateFees != null) {
FeeCalculationState.Success(
fee = FeeRelayerFee(
feeInSol = fees,
feeInSpl = poolsStateFees,
expectedFee = expectedFee
)
)
} else {
FeeCalculationState.PoolsNotFound(
feeInSol = FeeRelayerFee(
feeInSol = fees,
feeInSpl = FeeAmount(fees.transactionFee, fees.accountCreationFee),
expectedFee = expectedFee
)
)
val feesInSourceToken = getFeesInPayingTokenUseCase.execute(
targetToken = sourceToken,
transactionFeeInSol = feesInSol.transactionFee,
accountCreationFeeInSol = feesInSol.accountCreationFee
)

// it is incorrect to return fees in sol if there is some error happened
// because we would add apples to oranges when choosing fee payer token
require(feesInFeePayerToken != null && feesInSourceToken != null) {
buildString {
append("Cannot calculate transaction fees in source (${sourceToken.tokenSymbol})")
append("and fee payer token ${feePayerToken.tokenSymbol}")
}
}

FeeCalculationState.Success(
fee = FeeRelayerFee(
feesInSol = feesInSol,
feesInFeePayerToken = feesInFeePayerToken,
feesInSourceToken = feesInSourceToken,
expectedFee = expectedFeesInSol
)
)
} catch (e: CancellationException) {
Timber.i("Fee calculation cancelled")
return FeeCalculationState.Cancelled
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/org/p2p/wallet/send/SendModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.p2p.core.token.Token
import org.p2p.wallet.feerelayer.interactor.FeeRelayerViaLinkInteractor
import org.p2p.wallet.home.ui.new.NewSelectTokenContract
import org.p2p.wallet.home.ui.new.NewSelectTokenPresenter
import org.p2p.wallet.infrastructure.StorageModule
import org.p2p.wallet.infrastructure.network.provider.SendModeProvider
import org.p2p.wallet.infrastructure.sendvialink.UserSendLinksDatabaseRepository
import org.p2p.wallet.infrastructure.sendvialink.UserSendLinksLocalRepository
Expand All @@ -22,11 +23,14 @@ import org.p2p.wallet.send.interactor.usecase.CalculateToken2022TransferFeeUseCa
import org.p2p.wallet.send.interactor.usecase.GetFeesInPayingTokenUseCase
import org.p2p.wallet.send.interactor.usecase.GetTokenExtensionsUseCase
import org.p2p.wallet.send.model.SearchResult
import org.p2p.wallet.send.repository.FeePayerTokenValidityRepository
import org.p2p.wallet.send.repository.RecipientsDatabaseRepository
import org.p2p.wallet.send.repository.RecipientsLocalRepository
import org.p2p.wallet.send.repository.SendServiceInMemoryRepository
import org.p2p.wallet.send.repository.SendServiceRemoteRepository
import org.p2p.wallet.send.repository.SendServiceRepository
import org.p2p.wallet.send.repository.SendStorage
import org.p2p.wallet.send.repository.SendStorageContract
import org.p2p.wallet.send.ui.NewSendContract
import org.p2p.wallet.send.ui.NewSendPresenter
import org.p2p.wallet.send.ui.SendOpenedFrom
Expand Down Expand Up @@ -116,6 +120,12 @@ object SendModule : InjectionModule {
factoryOf(::FeeRelayerViaLinkInteractor)
factoryOf(::SendViaLinkInteractor)
factoryOf(::UserSendLinksDatabaseRepository) bind UserSendLinksLocalRepository::class
factory {
SendStorage(
prefs = get(named(StorageModule.PREFS_SEND))
)
} bind SendStorageContract::class
singleOf(::FeePayerTokenValidityRepository)
}

private fun Module.initSendService() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class FeePayersRepository(
val feePayerTokensMints = sendServiceRepository.getCompensationTokens()
val tokenToExcludeSymbol = feePayerToExclude.tokenSymbol

val fees: Map<Base58String, BigInteger> = getFeesInPayingTokenUseCase.execute(
val feesInGivenTokens: Map<Base58String, BigInteger> = getFeesInPayingTokenUseCase.execute(
findFeesIn = feePayerTokensMints,
transactionFeeInSol = transactionFeeInSol,
accountCreationFeeInSol = accountCreationFeeInSol
Expand All @@ -61,9 +61,9 @@ class FeePayersRepository(
}

// assuming that all other tokens are SPL
val feesInSpl = fees[token.mintAddress.toBase58Instance()]
val feesInSpl = feesInGivenTokens[token.mintAddress.toBase58Instance()]
if (feesInSpl == null) {
Timber.i("Fee in SPL not found for ${token.tokenSymbol} in ${fees.keys}")
Timber.i("Fee in SPL not found for ${token.tokenSymbol} in ${feesInGivenTokens.keys}")
return@filter false
}
val isTokenCoversTheFee = token.totalInLamports >= feesInSpl
Expand Down
Loading

0 comments on commit 9a492af

Please sign in to comment.