-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathSendSolanaFee.kt
246 lines (219 loc) · 10.2 KB
/
SendSolanaFee.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
package org.p2p.wallet.send.model
import android.os.Parcelable
import timber.log.Timber
import java.math.BigDecimal
import java.math.BigInteger
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.p2p.core.token.Token
import org.p2p.core.utils.formatToken
import org.p2p.core.utils.fromLamports
import org.p2p.core.utils.isLessThan
import org.p2p.core.utils.isMoreThan
import org.p2p.core.utils.isZero
import org.p2p.core.utils.isZeroOrLess
import org.p2p.core.utils.orZero
import org.p2p.core.utils.subtractRatio
import org.p2p.core.utils.toLamports
import org.p2p.core.utils.toUsd
import org.p2p.wallet.feerelayer.model.FeePayerSelectionStrategy
import org.p2p.wallet.feerelayer.model.FeePayerSelectionStrategy.CORRECT_AMOUNT
import org.p2p.wallet.feerelayer.model.FeeRelayerFee
import org.p2p.wallet.send.model.FeePayerState.KeepSame
import org.p2p.wallet.send.model.FeePayerState.ReduceInputAmount
import org.p2p.wallet.send.model.FeePayerState.SwitchToSol
import org.p2p.wallet.send.model.FeePayerState.SwitchToSpl
/**
* This class contains information about fees for transaction or for a new account creation
* */
@Parcelize
data class SendSolanaFee constructor(
val feePayerToken: Token.Active,
val feeRelayerFee: FeeRelayerFee,
val token2022TransferFee: BigInteger,
val token2022TransferFeePercent: BigDecimal,
val sourceToken: Token.Active,
private val solToken: Token.Active?,
private val alternativeFeePayerTokens: List<Token.Active>,
private val supportedFeePayerTokens: List<Token.Active>? = null
) : Parcelable {
@IgnoredOnParcel
val sourceTokenSymbol: String
get() = sourceToken.tokenSymbol
@IgnoredOnParcel
val isTransactionFree: Boolean
get() = feeRelayerFee.transactionFeeInSol.isZeroOrLess()
@IgnoredOnParcel
val isAccountCreationFree: Boolean
get() = feeRelayerFee.accountCreationFeeInSol.isZeroOrLess()
@IgnoredOnParcel
val accountCreationFormattedFee: String
get() = "${accountCreationFeeDecimals.formatToken()} ${feePayerToken.tokenSymbol}"
@IgnoredOnParcel
val totalFee: String
get() = if (isTransactionFree) accountCreationFormattedFee else summedFeeDecimalsFormatted
@IgnoredOnParcel
val feeUsd: BigDecimal?
get() = accountCreationFeeDecimals.toUsd(feePayerToken)
@IgnoredOnParcel
val feePayerSymbol: String
get() = feePayerToken.tokenSymbol
@IgnoredOnParcel
val transactionFullFee: String
get() = "$transactionDecimals $feePayerSymbol ${approxTransactionFeeUsd.orEmpty()}"
@IgnoredOnParcel
val approxTransactionFeeUsd: String?
get() = transactionDecimals.toUsd(feePayerToken)?.let { "(~$$it)" }
@IgnoredOnParcel
val accountCreationFeeUsd: String
get() = "$accountCreationFeeDecimals $feePayerSymbol ${getApproxAccountCreationFeeUsd().orEmpty()}"
@IgnoredOnParcel
val summedFeeDecimalsFormatted: String
get() = "${(totalFeeDecimals).formatToken()} $feePayerSymbol"
@IgnoredOnParcel
val summedFeeDecimalsUsd: String?
get() = totalFeeDecimals.toUsd(feePayerToken)?.let { ("(~$$it)") }
fun getApproxAccountCreationFeeUsd(withBraces: Boolean = true): String? =
accountCreationFeeDecimals.toUsd(feePayerToken)?.let {
if (withBraces) "(~$$it)" else "~$$it"
}
@IgnoredOnParcel
val accountCreationFeeDecimals: BigDecimal
get() {
val amount = if (feePayerToken.isSOL) {
feeRelayerFee.accountCreationFeeInSol
} else {
feeRelayerFee.accountCreationFeeInFeePayerToken
}
return amount.fromLamports(feePayerToken.decimals)
}
@IgnoredOnParcel
val transactionDecimals: BigDecimal
get() {
val amount = if (feePayerToken.isSOL) {
feeRelayerFee.transactionFeeInSol
} else {
feeRelayerFee.transactionFeeInFeePayerToken
}
return amount.fromLamports(feePayerToken.decimals)
}
@IgnoredOnParcel
private val feePayerTotalLamports: BigInteger
get() = feePayerToken.total.toLamports(feePayerToken.decimals)
@IgnoredOnParcel
val totalFeeDecimals: BigDecimal
get() = accountCreationFeeDecimals + transactionDecimals.orZero()
@IgnoredOnParcel
val totalFeeDecimalsUsd: BigDecimal?
get() = totalFeeDecimals.toUsd(feePayerToken)
fun isEnoughToCoverExpenses(
sourceTokenTotal: BigInteger,
inputAmount: BigInteger,
minRentExemption: BigInteger
): Boolean = when {
// if source is SOL, then fee payer is SOL as well
sourceToken.isSOL -> {
isEnoughSol(sourceTokenTotal, inputAmount, minRentExemption)
}
// assuming that source token is not SOL
feePayerToken.isSOL -> {
val totalInSol = feeRelayerFee.totalInSol
sourceTokenTotal >= inputAmount && isEnoughSol(feePayerTotalLamports, totalInSol, minRentExemption)
}
// assuming that source token and fee payer are same
sourceTokenSymbol == feePayerSymbol -> {
sourceTokenTotal >= inputAmount + feeRelayerFee.totalInSourceToken
}
// assuming that source token and fee payer are different
else -> {
feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken
}
}
private fun isEnoughSol(
sourceTokenTotal: BigInteger,
inputAmount: BigInteger,
minRentExemption: BigInteger
): Boolean =
sourceTokenTotal == inputAmount + feeRelayerFee.totalInSol ||
// added min required balance for SOL check
(sourceTokenTotal - minRentExemption) >= inputAmount + feeRelayerFee.totalInSol
fun isEnoughSolBalance() = solToken?.let { !it.totalInLamports.isLessThan(feeRelayerFee.totalInSol) } ?: false
fun calculateFeePayerState(
strategy: FeePayerSelectionStrategy,
sourceTokenTotal: BigInteger,
inputAmount: BigInteger
): FeePayerState {
val feePayerTokenCanCoverExpenses = feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken
val feePayerIsSourceToken = feePayerSymbol == sourceTokenSymbol
val isNotSourceSol = sourceToken.isSpl
val isAllowedToCorrectAmount = strategy == CORRECT_AMOUNT && isNotSourceSol
val totalNeeded = feeRelayerFee.totalInSourceToken + inputAmount
val isInsufficientSolBalance = !isEnoughSolBalance()
val shouldTryReduceAmount = isAllowedToCorrectAmount && (isInsufficientSolBalance || feePayerIsSourceToken)
val hasAlternativeFeePayerTokens = alternativeFeePayerTokens.isNotEmpty()
val isValidToSwitchOnSource = supportedFeePayerTokens?.contains(sourceToken) ?: true
// if there is enough SPL token balance to cover amount and fee
val shouldSwitchToSpl =
isNotSourceSol &&
sourceTokenTotal.isMoreThan(totalNeeded) &&
isValidToSwitchOnSource
Timber.i(
buildString {
appendLine("isNotSourceSol = $isNotSourceSol")
appendLine("isAllowedToCorrectAmount = $isAllowedToCorrectAmount")
appendLine("totalNeeded = $totalNeeded")
appendLine("isInsufficientSolBalance = $isInsufficientSolBalance")
appendLine("shouldTryReduceAmount = $shouldTryReduceAmount")
appendLine("hasAlternativeFeePayerTokens = $hasAlternativeFeePayerTokens")
appendLine("alternativeFeePayerTokens = ${alternativeFeePayerTokens.map(Token.Active::tokenSymbol)}")
appendLine("isValidToSwitchOnSource = $isValidToSwitchOnSource")
appendLine("shouldSwitchToSpl = $shouldSwitchToSpl")
appendLine("feePayerToken = ${feePayerToken.tokenSymbol}")
appendLine("feePayerTokenCanCoverExpenses = $feePayerTokenCanCoverExpenses")
appendLine("feePayerIsSourceToken = $feePayerIsSourceToken")
}
)
return when {
// don't do anything if amount is not entered or it is zero
inputAmount.isZero() -> {
Timber.i("FeePayer: input amount is zero")
KeepSame
}
feePayerTokenCanCoverExpenses && !feePayerIsSourceToken && !shouldTryReduceAmount -> {
Timber.i("FeePayer: keep the same fee payer token")
KeepSame
}
shouldSwitchToSpl && !shouldTryReduceAmount -> {
Timber.i("FeePayer: switch to SPL (${sourceToken.tokenSymbol})")
SwitchToSpl(sourceToken)
}
hasAlternativeFeePayerTokens && !shouldTryReduceAmount -> {
Timber.i("FeePayer: switch to a token with highest USD balance")
SwitchToSpl(alternativeFeePayerTokens.maxBy { it.totalInUsd.orZero() })
}
// if there is not enough SPL token balance to cover amount and fee, then try to reduce input amount
shouldTryReduceAmount && sourceTokenTotal.isLessThan(totalNeeded) -> {
val desiredAmountMinusFees = sourceTokenTotal - feeRelayerFee.totalInSourceToken
if (desiredAmountMinusFees.isZeroOrLess()) {
// if it's not enough money on balance to cover at least fees, selecting another fee payer token
if (hasAlternativeFeePayerTokens) {
SwitchToSpl(alternativeFeePayerTokens.maxBy { it.totalInUsd.orZero() })
} else {
SwitchToSol
}
} else {
// balance is sufficient to cover fees, calculating how much money can be sent
val desiredAmountMinusTransferFee = desiredAmountMinusFees
.toBigDecimal()
.subtractRatio(token2022TransferFeePercent.multiply(BigDecimal("0.01")))
// it rounds down to the nearest integer, so 100% accuracy is not guaranteed
.toBigInteger()
ReduceInputAmount(desiredAmountMinusTransferFee)
}
}
else -> {
SwitchToSol
}
}
}
}