From d39ed2e2208c1cc35a008919adf24037647320b7 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:29:29 +0400 Subject: [PATCH 01/51] ETH-781 - referral in history (#2165) --- .../p2p/wallet/common/InAppFeatureFlags.kt | 3 + .../toggles/inapp/ReferralRewardTxMockFlag.kt | 14 ++ .../RpcHistoryTransactionInfoResponse.kt | 9 + .../api/model/RpcHistoryTypeResponse.kt | 3 + .../mapper/RpcHistoryTransactionConverter.kt | 44 ++++- .../model/rpc/RpcHistoryTransaction.kt | 43 ++++ .../model/rpc/RpcHistoryTransactionType.kt | 1 + ...ryTransactionDetailsBottomSheetFragment.kt | 49 ++++- .../HistoryTransactionDetailsContract.kt | 14 +- .../HistoryTransactionDetailsPresenter.kt | 85 +++++++- .../sell/interactor/HistoryItemMapper.kt | 17 +- .../dialog_history_transaction_details.xml | 186 ++++++++++++------ app/src/main/res/values/history_strings.xml | 4 + 13 files changed, 398 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/inapp/ReferralRewardTxMockFlag.kt diff --git a/app/src/main/java/org/p2p/wallet/common/InAppFeatureFlags.kt b/app/src/main/java/org/p2p/wallet/common/InAppFeatureFlags.kt index beee3aec92..54429372bc 100644 --- a/app/src/main/java/org/p2p/wallet/common/InAppFeatureFlags.kt +++ b/app/src/main/java/org/p2p/wallet/common/InAppFeatureFlags.kt @@ -5,6 +5,7 @@ import org.p2p.wallet.common.feature_toggles.toggles.inapp.DebugTogglesFeatureFl import org.p2p.wallet.common.feature_toggles.toggles.inapp.DevNetFeatureFlag import org.p2p.wallet.common.feature_toggles.toggles.inapp.InAppFeatureFlag import org.p2p.wallet.common.feature_toggles.toggles.inapp.PollingFeatureFlag +import org.p2p.wallet.common.feature_toggles.toggles.inapp.ReferralRewardTxMockFlag import org.p2p.wallet.common.feature_toggles.toggles.inapp.StrigaEnableEmailFieldFlag import org.p2p.wallet.common.feature_toggles.toggles.inapp.StrigaKycBannerMockFlag import org.p2p.wallet.common.feature_toggles.toggles.inapp.StrigaSimulateIbanNotFilledFlag @@ -16,6 +17,7 @@ import org.p2p.wallet.common.feature_toggles.toggles.inapp.StrigaSmsVerification class InAppFeatureFlags(prefs: SharedPreferences) { val isPollingEnabled = PollingFeatureFlag(prefs) var isDevNetEnabled = DevNetFeatureFlag(prefs) + val referralRewardTxMockEnabled = ReferralRewardTxMockFlag(prefs) val strigaSimulateWeb3Flag = StrigaSimulateWeb3Flag(prefs) val strigaSimulateUserCreateFlag = StrigaSimulateUserCreateFlag(prefs) val strigaSmsVerificationMockFlag = StrigaSmsVerificationMockFlag(prefs) @@ -33,6 +35,7 @@ class InAppFeatureFlags(prefs: SharedPreferences) { isPollingEnabled, isDevNetEnabled, isDebugRemoteConfigEnabled, + referralRewardTxMockEnabled, /* strigaSimulateWeb3Flag, strigaSimulateUserCreateFlag, diff --git a/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/inapp/ReferralRewardTxMockFlag.kt b/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/inapp/ReferralRewardTxMockFlag.kt new file mode 100644 index 0000000000..e05e3d13ac --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/inapp/ReferralRewardTxMockFlag.kt @@ -0,0 +1,14 @@ +package org.p2p.wallet.common.feature_toggles.toggles.inapp + +import android.content.SharedPreferences +import org.p2p.wallet.common.BooleanPreference + +class ReferralRewardTxMockFlag(prefs: SharedPreferences) : InAppFeatureFlag() { + private companion object { + private const val KEY_FLAG = "KEY_REFERRAL_REWARD_TX_MOCK" + } + override val featureName: String = KEY_FLAG + override var featureValue: Boolean by BooleanPreference( + preferences = prefs, key = KEY_FLAG, defaultValue = false + ) +} diff --git a/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTransactionInfoResponse.kt b/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTransactionInfoResponse.kt index d5a1e21886..7e08388759 100644 --- a/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTransactionInfoResponse.kt +++ b/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTransactionInfoResponse.kt @@ -22,6 +22,15 @@ sealed class RpcHistoryTransactionInfoResponse { val counterParty: RpcHistoryAccountResponse ) : RpcHistoryTransactionInfoResponse() + data class ReferralReward( + @SerializedName("amount") + val amount: RpcHistoryAmountResponse, + @SerializedName("token") + val token: RpcHistoryTokenResponse, + @SerializedName("account") + val counterParty: RpcHistoryAccountResponse + ) : RpcHistoryTransactionInfoResponse() + data class Swap( @SerializedName("from") val from: RpcHistorySwapTokenResponse, diff --git a/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTypeResponse.kt b/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTypeResponse.kt index db31b04ad9..c7f9cf95f4 100644 --- a/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTypeResponse.kt +++ b/app/src/main/java/org/p2p/wallet/history/api/model/RpcHistoryTypeResponse.kt @@ -9,6 +9,9 @@ enum class RpcHistoryTypeResponse { @SerializedName("receive") RECEIVE, + @SerializedName("referral_reward") + REFERRAL_REWARD, + @SerializedName("swap") SWAP, diff --git a/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt b/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt index 050f6a6421..af695f5024 100644 --- a/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt +++ b/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt @@ -6,6 +6,7 @@ import org.p2p.core.utils.fromJsonReified import org.p2p.core.utils.toBigDecimalOrZero import org.p2p.wallet.bridge.claim.repository.EthereumBridgeInMemoryRepository import org.p2p.wallet.bridge.model.BridgeFee +import org.p2p.wallet.common.InAppFeatureFlags import org.p2p.wallet.common.date.toZonedDateTime import org.p2p.wallet.history.api.model.RpcHistoryFeeResponse import org.p2p.wallet.history.api.model.RpcHistoryStatusResponse @@ -24,15 +25,24 @@ class RpcHistoryTransactionConverter( private val tokenKeyProvider: TokenKeyProvider, private val gson: Gson, private val usernameFormatter: UsernameFormatter, - private val bridgeInMemoryRepository: EthereumBridgeInMemoryRepository + private val bridgeInMemoryRepository: EthereumBridgeInMemoryRepository, + private val inAppFeatureFlags: InAppFeatureFlags, ) { fun toDomain( transaction: RpcHistoryTransactionResponse, - ): RpcHistoryTransaction? = - when (transaction.type) { + ): RpcHistoryTransaction? { + val enableReferralRewardMock = inAppFeatureFlags.referralRewardTxMockEnabled.featureValue + return when (transaction.type) { RpcHistoryTypeResponse.SEND -> parseSend(transaction) - RpcHistoryTypeResponse.RECEIVE -> parseReceive(transaction) + RpcHistoryTypeResponse.RECEIVE -> { + if (enableReferralRewardMock) { + parseReferralReward(transaction) + } else { + parseReceive(transaction) + } + } + RpcHistoryTypeResponse.REFERRAL_REWARD -> parseReferralReward(transaction) RpcHistoryTypeResponse.SWAP -> parseSwap(transaction) RpcHistoryTypeResponse.STAKE -> parseStake(transaction) RpcHistoryTypeResponse.UNSTAKE -> parseUnstake(transaction) @@ -45,6 +55,7 @@ class RpcHistoryTransactionConverter( RpcHistoryTypeResponse.UNKNOWN -> parseUnknown(transaction) else -> null } + } private fun parseReceive(transaction: RpcHistoryTransactionResponse): RpcHistoryTransaction { val info = gson.fromJsonReified( @@ -70,6 +81,30 @@ class RpcHistoryTransactionConverter( ) } + private fun parseReferralReward(transaction: RpcHistoryTransactionResponse): RpcHistoryTransaction { + val info = gson.fromJsonReified( + transaction.info.toString() + ) + ?: error("Parsing error: cannot parse json object ${transaction.info}") + + val total = info.amount.amount.toBigDecimalOrZero() + val totalInUsd = info.amount.usdAmount.toBigDecimalOrZero() + return RpcHistoryTransaction.ReferralReward( + signature = transaction.signature, + date = transaction.date.toZonedDateTime(), + blockNumber = transaction.blockNumber.toInt(), + status = transaction.status.toDomain(), + type = transaction.type.toDomain(), + senderAddress = info.counterParty.address, + counterPartyUsername = usernameFormatter.formatOrNull(info.counterParty.username), + iconUrl = info.token.logoUrl, + amount = RpcHistoryAmount(total, totalInUsd), + symbol = info.token.symbol.orEmpty(), + destination = tokenKeyProvider.publicKey, + fees = transaction.fees.parseFees() + ) + } + private fun parseSend(transaction: RpcHistoryTransactionResponse): RpcHistoryTransaction { val info = gson.fromJsonReified(transaction.info.toString()) ?: error("Parsing error: cannot parse json object ${transaction.info}") @@ -327,6 +362,7 @@ private fun RpcHistoryTypeResponse?.toDomain(): RpcHistoryTransactionType { return when (this) { RpcHistoryTypeResponse.SEND -> RpcHistoryTransactionType.SEND RpcHistoryTypeResponse.RECEIVE -> RpcHistoryTransactionType.RECEIVE + RpcHistoryTypeResponse.REFERRAL_REWARD -> RpcHistoryTransactionType.REFERRAL_REWARD RpcHistoryTypeResponse.SWAP -> RpcHistoryTransactionType.SWAP RpcHistoryTypeResponse.STAKE -> RpcHistoryTransactionType.STAKE RpcHistoryTypeResponse.UNSTAKE -> RpcHistoryTransactionType.UNSTAKE diff --git a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt index 472dbe3f18..d42d055fec 100644 --- a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt +++ b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt @@ -271,6 +271,49 @@ sealed class RpcHistoryTransaction( fun getFormattedAmount(): String? = amount.totalInUsd?.asUsdTransaction(getSymbol(isSend)) } + @Parcelize + data class ReferralReward( + override val signature: String, + override val date: ZonedDateTime, + override val blockNumber: Int, + override val status: HistoryTransactionStatus, + override val type: RpcHistoryTransactionType, + val senderAddress: String, + val iconUrl: String?, + val amount: RpcHistoryAmount, + val symbol: String, + val destination: String, + val counterPartyUsername: String?, + val fees: List?, + ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { + @DrawableRes + fun getIcon(): Int = R.drawable.ic_transaction_receive + + fun getFormattedFiatValue(): String? { + return amount.totalInUsd + ?.scaleShortOrFirstNotZero() + ?.asUsdTransaction(getSymbol(false)) + } + + fun getTotalWithSymbol(): String = "${getSymbol(false)}${getFormattedTotal()}" + + fun getFormattedTotal(scaleMedium: Boolean = false): String = if (scaleMedium) { + "${amount.total.scaleMedium().formatToken()} $symbol" + } else { + "${amount.total.formatToken()} $symbol" + } + + @ColorRes + fun getTextColor() = when { + !status.isCompleted() -> { + R.color.text_rose + } + else -> { + R.color.text_mint + } + } + } + @Parcelize data class StakeUnstake( override val signature: String, diff --git a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransactionType.kt b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransactionType.kt index 5f89672c46..4f50b812b8 100644 --- a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransactionType.kt +++ b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransactionType.kt @@ -5,6 +5,7 @@ enum class RpcHistoryTransactionType { SWAP, SEND, RECEIVE, + REFERRAL_REWARD, STAKE, UNSTAKE, CREATE_ACCOUNT, diff --git a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt index 46a50000dc..a065945d88 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt @@ -2,16 +2,19 @@ package org.p2p.wallet.history.ui.detailsbottomsheet import androidx.annotation.ColorRes import androidx.annotation.StringRes +import androidx.core.net.toUri import androidx.core.text.buildSpannedString import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager +import android.content.Intent import android.os.Bundle import android.view.View import android.widget.ImageView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.inject +import org.p2p.core.crypto.Base58String import org.p2p.core.glide.GlideManager import org.p2p.core.utils.DEFAULT_DECIMAL import org.p2p.core.utils.formatFiat @@ -24,13 +27,13 @@ import org.p2p.wallet.R import org.p2p.wallet.common.mvp.BaseMvpBottomSheet import org.p2p.wallet.databinding.DialogHistoryTransactionDetailsBinding import org.p2p.wallet.history.model.rpc.RpcFee -import org.p2p.core.crypto.Base58String import org.p2p.wallet.utils.CUT_7_SYMBOLS import org.p2p.wallet.utils.UsernameFormatter import org.p2p.wallet.utils.appendWhitespace import org.p2p.wallet.utils.args import org.p2p.wallet.utils.copyToClipBoard import org.p2p.wallet.utils.cutMiddle +import org.p2p.wallet.utils.doOnAnimationEnd import org.p2p.wallet.utils.shareText import org.p2p.wallet.utils.showInfoDialog import org.p2p.wallet.utils.showUrlInCustomTabs @@ -127,6 +130,31 @@ class HistoryTransactionDetailsBottomSheetFragment : imageViewSecondToken.isVisible = true } + override fun playApplauseAnimation() { + with(binding.animationView) { + setAnimation(R.raw.raw_animation_applause) + isVisible = true + doOnAnimationEnd { isVisible = false } + playAnimation() + } + } + + override fun showNewButtons(firstButtonTitleRes: Int, secondButtonTitleRes: Int) { + with(binding) { + containerOldButtons.isVisible = false + containerNewButtons.isVisible = true + + buttonPrimary.setText(firstButtonTitleRes) + buttonSecondary.setText(secondButtonTitleRes) + buttonPrimary.setOnClickListener { + presenter.onPrimaryButtonClick() + } + buttonSecondary.setOnClickListener { + presenter.onSecondaryButtonClick() + } + } + } + override fun showSubtitle(subtitle: String) { binding.textViewSubtitle.text = subtitle } @@ -136,6 +164,14 @@ class HistoryTransactionDetailsBottomSheetFragment : textViewAmountUsd.setTextColor(getColor(colorId)) } + override fun setTitle(@StringRes titleResId: Int) { + binding.textViewTitle.setText(titleResId) + } + + override fun setSmokeBackground() { + binding.root.setBackgroundResource(R.drawable.bg_alert_elevation_rounded_top_smoke) + } + override fun showProgressTransactionErrorState(errorMessage: String) = with(binding.progressStateTransaction) { isVisible = true setFailedState() @@ -244,6 +280,17 @@ class HistoryTransactionDetailsBottomSheetFragment : textViewAmountUsd.withTextOrGone(amountUsd) } + override fun navigateToSolscan(txSignature: String) { + dismissAllowingStateLoss() + Intent(Intent.ACTION_VIEW, getString(R.string.solscan_tx_url, txSignature).toUri()) + .also { startActivity(it) } + } + + override fun showAmountReferralReward(amountToken: String) = with(binding) { + textViewAmountTokens.isVisible = false + textViewAmountUsd.withTextOrGone(amountToken) + } + override fun showFee(fees: List?) = with(binding) { textViewFeeTitle.isVisible = true textViewFeeValue.isVisible = true diff --git a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsContract.kt b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsContract.kt index a640e67fc8..9e6164f525 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsContract.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsContract.kt @@ -2,21 +2,29 @@ package org.p2p.wallet.history.ui.detailsbottomsheet import androidx.annotation.ColorRes import androidx.annotation.StringRes +import org.p2p.core.crypto.Base58String import org.p2p.wallet.common.mvp.MvpPresenter import org.p2p.wallet.common.mvp.MvpView import org.p2p.wallet.history.model.rpc.RpcFee -import org.p2p.core.crypto.Base58String interface HistoryTransactionDetailsContract { interface View : MvpView { fun showTransferView(tokenIconUrl: String?, placeholderIcon: Int) + fun setSmokeBackground() + fun showNewButtons( + @StringRes firstButtonTitleRes: Int, + @StringRes secondButtonTitleRes: Int, + ) + fun playApplauseAnimation() fun showSwapView(sourceIconUrl: String?, destinationIconUrl: String?) fun showSubtitle(subtitle: String) fun showStatus(@StringRes titleResId: Int, @ColorRes colorId: Int) + fun setTitle(@StringRes titleResId: Int) fun showProgressTransactionErrorState(errorMessage: String) fun showTransactionId(signature: String) fun showAmount(amountToken: String?, amountUsd: String?) + fun showAmountReferralReward(amountToken: String) fun showFee(fees: List?) fun showLoading(isLoading: Boolean) fun showError(@StringRes messageId: Int) @@ -25,9 +33,13 @@ interface HistoryTransactionDetailsContract { fun hideSendReceiveTitleAndValue() fun showSenderAddress(senderAddress: Base58String, senderUsername: String?, isReceivePending: Boolean) fun showProgressTransactionInProgress() + fun dismiss() + fun navigateToSolscan(txSignature: String) } interface Presenter : MvpPresenter { fun load(transactionId: String) + fun onPrimaryButtonClick() + fun onSecondaryButtonClick() } } diff --git a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt index fc34a07173..934f8b08a3 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt @@ -4,7 +4,11 @@ import android.content.res.Resources import org.threeten.bp.format.DateTimeFormatter import timber.log.Timber import java.util.Locale +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.async import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.toBase58Instance import org.p2p.core.utils.emptyString @@ -28,13 +32,16 @@ class HistoryTransactionDetailsPresenter( HistoryTransactionDetailsContract.Presenter { private val timeFormat = DateTimeFormatter.ofPattern(DateTimeUtils.PATTERN_FULL_DAY, Locale.US) + private var tx: HistoryTransaction? = null override fun load(transactionId: String) { launch { try { view?.showLoading(isLoading = true) - val transaction = historyInteractor.findTransactionById(transactionId) - loadDetailsByTransaction(transaction) + Timber.d("Start finding tx by id: $transactionId") + tx = historyInteractor.findTransactionById(transactionId) + Timber.d("Tx by id is found") + loadDetailsByTransaction(tx) } catch (e: Throwable) { Timber.e(e, "Error on finding transaction by id: $e") view?.showErrorMessage(e) @@ -61,6 +68,7 @@ class HistoryTransactionDetailsPresenter( when (transaction) { is RpcHistoryTransaction.Swap -> parseSwap(transaction) is RpcHistoryTransaction.Transfer -> parseTransfer(transaction) + is RpcHistoryTransaction.ReferralReward -> parseReferralReward(transaction) is RpcHistoryTransaction.BurnOrMint -> parseBurnOrMint(transaction) is RpcHistoryTransaction.WormholeReceive -> parseWormholeReceive(transaction) is RpcHistoryTransaction.WormholeSend -> parseWormholeSend(transaction) @@ -119,6 +127,55 @@ class HistoryTransactionDetailsPresenter( } } + private suspend fun parseReferralReward(transaction: RpcHistoryTransaction.ReferralReward) { + val transferActorAddress = transaction.senderAddress.toBase58Instance() + + view?.apply { + setSmokeBackground() + setTitle(R.string.transaction_details_referral_title) + showNewButtons( + firstButtonTitleRes = R.string.transaction_details_referral_button_first, + secondButtonTitleRes = R.string.transaction_details_referral_button_second + ) + showTransferView( + tokenIconUrl = transaction.iconUrl, + placeholderIcon = transaction.getIcon() + ) + showAmountReferralReward(amountToken = transaction.getFormattedTotal()) + + showSenderAddress( + senderAddress = transferActorAddress, + senderUsername = null, + isReceivePending = true + ) + playApplauseAnimation() + } + + // fetching username is failing all the time and it takes way too long + // so: + // 1. defer it, + // 2. limit by timeout + // todo: the same ought to be done for [parseTransfer] + async { + Timber.d("Start loading username") + val transferActorUsername = withTimeoutOrNull(5.seconds) { + getTransferActorUsername(transferActorAddress) + } + + if (transferActorUsername != null && view != null) { + transferActorUsername.username.fullUsername.let { + view?.showSenderAddress( + senderAddress = transferActorAddress, + senderUsername = it, + isReceivePending = true + ) + } + } else { + Timber.w("Couldn't load username for address: $transferActorAddress") + } + } + } + private suspend fun showTransferAddress( isSend: Boolean, isPending: Boolean, @@ -145,7 +202,11 @@ class HistoryTransactionDetailsPresenter( private suspend fun getTransferActorUsername(actorAddress: Base58String): UsernameDetails? { return runCatching { usernameInteractor.findUsernameByAddress(actorAddress) } - .onFailure { Timber.e(it, "Failed to find username by address: $actorAddress") } + .onFailure { + if (it !is CancellationException) { + Timber.e(it, "Failed to find username by address: $actorAddress") + } + } .getOrNull() } @@ -325,4 +386,22 @@ class HistoryTransactionDetailsPresenter( ) } } + + override fun onPrimaryButtonClick() { + when (tx) { + is RpcHistoryTransaction.ReferralReward -> { + view?.dismiss() + } + else -> Unit + } + } + + override fun onSecondaryButtonClick() { + when (tx) { + is RpcHistoryTransaction.ReferralReward -> { + view?.navigateToSolscan(tx!!.getHistoryTransactionId()) + } + else -> Unit + } + } } diff --git a/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt b/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt index ec3b5203db..2487a1411e 100644 --- a/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt +++ b/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt @@ -3,23 +3,24 @@ package org.p2p.wallet.sell.interactor import android.content.res.Resources import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext +import org.p2p.core.crypto.Base58String +import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.utils.Constants import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken import org.p2p.ethereumkit.external.model.ERC20Tokens import org.p2p.wallet.R import org.p2p.wallet.common.date.isSameDayAs +import org.p2p.wallet.common.date.toDateString import org.p2p.wallet.common.date.toZonedDateTime import org.p2p.wallet.history.model.HistoryTransaction import org.p2p.wallet.history.model.bridge.BridgeHistoryTransaction import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction import org.p2p.wallet.history.ui.model.HistoryItem -import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.wallet.moonpay.model.SellTransaction import org.p2p.wallet.moonpay.serversideapi.response.SellTransactionStatus import org.p2p.wallet.sell.ui.lock.SellTransactionViewDetails import org.p2p.wallet.user.repository.UserLocalRepository -import org.p2p.core.crypto.Base58String import org.p2p.wallet.utils.cutStart import org.p2p.wallet.utils.getStatusIcon @@ -153,6 +154,16 @@ class HistoryItemMapper( endTopValueTextColor = getTextColor() endBottomValue = getTotalWithSymbol() } + is RpcHistoryTransaction.ReferralReward -> with(transaction) { + tokenIconUrl = iconUrl + iconRes = getIcon() + + startTitle = resources.getString(R.string.transaction_details_referral_title) + startSubtitle = date.toDateString(resources) + endTopValue = getTotalWithSymbol() + endTopValueTextColor = getTextColor() + endBottomValue = getFormattedFiatValue() + } is RpcHistoryTransaction.StakeUnstake -> with(transaction) { tokenIconUrl = getTokenIconUrl() iconRes = getIcon() @@ -209,7 +220,7 @@ class HistoryItemMapper( endTopValueTextColor = getTextColor() endBottomValue = getTotal() } - is RpcHistoryTransaction.Unknown -> { + else -> { iconRes = R.drawable.ic_transaction_unknown startTitle = resources.getString(R.string.transaction_history_unknown) diff --git a/app/src/main/res/layout/dialog_history_transaction_details.xml b/app/src/main/res/layout/dialog_history_transaction_details.xml index 56a0453e47..e7e3e546c1 100644 --- a/app/src/main/res/layout/dialog_history_transaction_details.xml +++ b/app/src/main/res/layout/dialog_history_transaction_details.xml @@ -12,7 +12,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_rounded_transparent_8" - android:backgroundTint="@color/backgroundPrimary"> + android:backgroundTint="@color/smoke" + tools:background="@drawable/bg_alert_elevation_rounded_top_smoke"> - + tools:visibility="visible"> - + - + - + + + + app:layout_goneMarginBottom="32dp" /> + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/textViewSendReceiveTitle" + tools:visibility="visible" /> + + + + + + + + + Vote account: %s Transaction %s + Referral reward + Done + Open SOLScan submitted succeeded failed @@ -48,4 +51,5 @@ Sent via one-time link Unique one-time link works once only + https://solscan.io/tx/%s \ No newline at end of file From a5927c161d3d7ebb9145128379cdb21993b598a8 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:36:25 +0300 Subject: [PATCH 02/51] PWN-782 - Bugfixes for referral deeplink (#2166) --- .../auth/interactor/UsernameInteractor.kt | 8 ++++-- .../p2p/wallet/jupiter/JupiterSwapModule.kt | 3 ++ .../jupiter/ui/main/JupiterSwapFragment.kt | 4 +-- .../ui/main/SwapShareDeeplinkBuilder.kt | 28 +++++++++++++++++++ .../org/p2p/wallet/referral/ReferralBridge.kt | 2 +- .../org/p2p/wallet/referral/ReferralModule.kt | 3 +- .../banner/ReferralFragmentFactory.kt | 8 ++++-- .../repository/ReferralRemoteRepository.kt | 2 ++ .../p2p/wallet/android/application.gradle.kts | 2 ++ 9 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapShareDeeplinkBuilder.kt diff --git a/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt b/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt index 3cd79176b8..c5ee47b407 100644 --- a/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt @@ -5,18 +5,18 @@ import android.content.SharedPreferences import android.graphics.Bitmap import timber.log.Timber import java.io.File +import org.p2p.core.crashlytics.CrashLogger +import org.p2p.core.crypto.Base58String +import org.p2p.core.crypto.toBase58Instance import org.p2p.wallet.auth.model.Username import org.p2p.wallet.auth.repository.UserSignUpDetailsStorage import org.p2p.wallet.auth.username.repository.UsernameRepository import org.p2p.wallet.auth.username.repository.model.UsernameDetails -import org.p2p.core.crashlytics.CrashLogger import org.p2p.wallet.common.feature_toggles.toggles.remote.RegisterUsernameEnabledFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.UsernameDomainFeatureToggle import org.p2p.wallet.common.storage.FileRepository import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.restore.interactor.KEY_IS_AUTH_BY_SEED_PHRASE -import org.p2p.core.crypto.Base58String -import org.p2p.core.crypto.toBase58Instance const val KEY_USERNAME = "KEY_USERNAME" const val KEY_USERNAME_DOMAIN = "KEY_USERNAME_DOMAIN" @@ -95,6 +95,8 @@ class UsernameInteractor( } fun isUsernameItemVisibleInSettings(): Boolean { + // https://r.key.app/nickname.sol + // https://s.key.app/swap?from=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&to=So11111111111111111111111111111111111111112&r=nickname.sol val isUserUsedWeb3Auth = userSignUpDetailsStorage.getLastSignUpUserDetails() != null val isRegisterUsernameEnabled = registerUsernameEnabledFeatureToggle.isFeatureEnabled // sometimes user can use seed phrase to login, we cant show item to him too diff --git a/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt b/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt index 24f80f4829..f8901d764b 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt @@ -54,6 +54,7 @@ import org.p2p.wallet.jupiter.statemanager.validator.SwapValidator import org.p2p.wallet.jupiter.ui.info.SwapInfoMapper import org.p2p.wallet.jupiter.ui.main.JupiterSwapContract import org.p2p.wallet.jupiter.ui.main.JupiterSwapPresenter +import org.p2p.wallet.jupiter.ui.main.SwapShareDeeplinkBuilder import org.p2p.wallet.jupiter.ui.main.SwapTokenRateLoader import org.p2p.wallet.jupiter.ui.main.mapper.SwapButtonMapper import org.p2p.wallet.jupiter.ui.main.mapper.SwapRateTickerMapper @@ -113,6 +114,8 @@ object JupiterSwapModule : InjectionModule { factoryOf(::SwapButtonMapper) factoryOf(::SwapRateTickerMapper) + factoryOf(::SwapShareDeeplinkBuilder) + factory { (initialData: JupiterPresenterInitialData) -> val stateManager: SwapStateManager = getSwapStateManager( initialTokensData = SwapInitialTokensData( diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapFragment.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapFragment.kt index d6f09c1991..62d22f7e28 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapFragment.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapFragment.kt @@ -126,6 +126,7 @@ class JupiterSwapFragment : private val binding: FragmentJupiterSwapBinding by viewBinding() private val analytics: JupiterSwapMainScreenAnalytics by inject() + private val shareLinkBuilder: SwapShareDeeplinkBuilder by inject() override val presenter: JupiterSwapContract.Presenter by inject { parametersOf( @@ -254,8 +255,7 @@ class JupiterSwapFragment : } override fun showSwapLinkShareDialog(tokenAMint: Base58String, tokenBMint: Base58String) { - val shareUrl = "https://s.key.app/swap?from=%s&to=%s".format(tokenAMint.base58Value, tokenBMint.base58Value) - requireContext().shareText(shareUrl) + requireContext().shareText(shareLinkBuilder.buildDeeplink(tokenAMint, tokenBMint)) } private fun setupWidgetsActionCallbacks() = with(binding) { diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapShareDeeplinkBuilder.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapShareDeeplinkBuilder.kt new file mode 100644 index 0000000000..c59750ecd5 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapShareDeeplinkBuilder.kt @@ -0,0 +1,28 @@ +package org.p2p.wallet.jupiter.ui.main + +import android.net.Uri +import org.p2p.core.crypto.Base58String +import org.p2p.wallet.auth.interactor.UsernameInteractor +import org.p2p.wallet.common.feature_toggles.toggles.remote.ReferralProgramEnabledFeatureToggle +import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider + +class SwapShareDeeplinkBuilder( + private val tokenKeyProvider: TokenKeyProvider, + private val usernameInteractor: UsernameInteractor, + private val referralProgramEnabledFt: ReferralProgramEnabledFeatureToggle +) { + fun buildDeeplink(tokenAMint: Base58String, tokenBMint: Base58String): String { + val userReferralId = usernameInteractor.getUsername()?.fullUsername ?: tokenKeyProvider.publicKey + + return Uri.parse("https://s.key.app/swap").buildUpon() + .appendQueryParameter("from", tokenAMint.base58Value) + .appendQueryParameter("to", tokenBMint.base58Value) + .apply { + if (referralProgramEnabledFt.isFeatureEnabled) { + appendQueryParameter("r", userReferralId) + } + } + .build() + .toString() + } +} diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 0fd70a8880..3564dd5bf2 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -22,7 +22,7 @@ class ReferralWebViewBridge( ) { companion object { const val JS_BRIDGE_OBJECT_NAME = "ReferralBridge" - private const val REFERRAL_URL = "https://referral-2ii.pages.dev/" + private const val REFERRAL_URL = "https://referral-dapp.key.app/" } private var referralWebView: WebView? = null diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralModule.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralModule.kt index 68dfff7bf8..591de70dd2 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralModule.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralModule.kt @@ -1,6 +1,5 @@ package org.p2p.wallet.referral -import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf import org.koin.dsl.bind import org.koin.dsl.module @@ -10,7 +9,7 @@ import org.p2p.wallet.referral.repository.ReferralRemoteRepository import org.p2p.wallet.referral.repository.ReferralRepository object ReferralModule : InjectionModule { - override fun create(): Module = module { + override fun create() = module { factoryOf(::ReferralFragmentFactory) factoryOf(::ReferralRemoteRepository) bind ReferralRepository::class } diff --git a/app/src/main/java/org/p2p/wallet/referral/banner/ReferralFragmentFactory.kt b/app/src/main/java/org/p2p/wallet/referral/banner/ReferralFragmentFactory.kt index ac319a066b..1f81be7dc9 100644 --- a/app/src/main/java/org/p2p/wallet/referral/banner/ReferralFragmentFactory.kt +++ b/app/src/main/java/org/p2p/wallet/referral/banner/ReferralFragmentFactory.kt @@ -3,17 +3,21 @@ package org.p2p.wallet.referral.banner import androidx.fragment.app.Fragment import android.content.Context import org.p2p.wallet.R +import org.p2p.wallet.auth.interactor.UsernameInteractor import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.referral.ReferralFragment import org.p2p.wallet.utils.shareText class ReferralFragmentFactory( - private val tokenKeyProvider: TokenKeyProvider + private val tokenKeyProvider: TokenKeyProvider, + private val usernameInteractor: UsernameInteractor ) { fun shareLink(context: Context) { - val sharingLink = context.getString(R.string.share_referral_link, tokenKeyProvider.publicKey) + val userReferralId = usernameInteractor.getUsername()?.fullUsername ?: tokenKeyProvider.publicKey + val sharingLink = context.getString(R.string.share_referral_link, userReferralId) context.shareText(sharingLink) } + fun openDetails(): Fragment { return ReferralFragment.create() } diff --git a/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt index defc2d3b7e..0b919937a8 100644 --- a/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt @@ -39,6 +39,7 @@ class ReferralRemoteRepository( try { val requestTimestamp = System.currentTimeMillis().milliseconds.inWholeSeconds val signature = signRequestHex( + // order matters tokenKeyProvider.publicKey, Optional.empty(), requestTimestamp ) val request = ReferralRequest( @@ -76,6 +77,7 @@ class ReferralRemoteRepository( try { val requestTimestamp = System.currentTimeMillis().milliseconds.inWholeSeconds val signature = signRequestHex( + // order matters tokenKeyProvider.publicKey, referent, requestTimestamp ) val request = ReferralRequest( diff --git a/buildSrc/src/main/kotlin/org/p2p/wallet/android/application.gradle.kts b/buildSrc/src/main/kotlin/org/p2p/wallet/android/application.gradle.kts index e66e8f959d..d1a58c39d5 100644 --- a/buildSrc/src/main/kotlin/org/p2p/wallet/android/application.gradle.kts +++ b/buildSrc/src/main/kotlin/org/p2p/wallet/android/application.gradle.kts @@ -29,7 +29,9 @@ android { applicationId = "org.p2p.wallet" minSdk = Versions.sdkMinVersion targetSdk = Versions.sdkTargetVersion + versionCode = Versions.VERSION_CODE + logger.quiet("VERSION_CODE: $versionCode") versionName = Versions.VERSION_NAME multiDexEnabled = true From 86ef985af28902ef08aea8973cac89c56b12d1a0 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:07:04 +0300 Subject: [PATCH 03/51] PWN-777 - Bridge fixes (#2168) --- .github/CODEOWNERS | 2 +- .../main/assets/referral_bridge_provider.js | 19 +++++ .../auth/interactor/UsernameInteractor.kt | 2 - .../org/p2p/wallet/referral/ReferralBridge.kt | 85 +++++++++++++------ 4 files changed, 81 insertions(+), 27 deletions(-) create mode 100644 app/src/main/assets/referral_bridge_provider.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fdf1e04c0f..84ea323743 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,4 +3,4 @@ # In this example, those users owns any file in an checking directory # anywhere in your repository. -* @1sakov @Davran331122 @kabramovp2p @gslevinkov @eduardmaximovich \ No newline at end of file +* @gslevinkov @eduardmaximovich diff --git a/app/src/main/assets/referral_bridge_provider.js b/app/src/main/assets/referral_bridge_provider.js new file mode 100644 index 0000000000..02bccc2f5d --- /dev/null +++ b/app/src/main/assets/referral_bridge_provider.js @@ -0,0 +1,19 @@ + +// we need this JS code to inject the ReferralBridge object into the global namespace +// and parse the result data into a JSON object instead of raw string +window.ReferralBridge = { + getUserPublicKeyAsync: async function() { + const result = await AndroidReferralBridge.getUserPublicKeyAsync(); + return JSON.parse(result) + }, + nativeLog: function(info) { + AndroidReferralBridge.nativeLog(info); + }, + showShareDialog: function(link) { + AndroidReferralBridge.showShareDialog(link); + }, + signMessageAsync: async function(message) { + const result = AndroidReferralBridge.signMessageAsync(message); + return JSON.parse(result) + } +} diff --git a/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt b/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt index c5ee47b407..ebe8ece9a1 100644 --- a/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/auth/interactor/UsernameInteractor.kt @@ -95,8 +95,6 @@ class UsernameInteractor( } fun isUsernameItemVisibleInSettings(): Boolean { - // https://r.key.app/nickname.sol - // https://s.key.app/swap?from=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&to=So11111111111111111111111111111111111111112&r=nickname.sol val isUserUsedWeb3Auth = userSignUpDetailsStorage.getLastSignUpUserDetails() != null val isRegisterUsernameEnabled = registerUsernameEnabledFeatureToggle.isFeatureEnabled // sometimes user can use seed phrase to login, we cant show item to him too diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 3564dd5bf2..d54cdaba96 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -1,6 +1,9 @@ package org.p2p.wallet.referral +import android.graphics.Bitmap +import android.webkit.ConsoleMessage import android.webkit.JavascriptInterface +import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import com.google.gson.JsonObject @@ -21,13 +24,28 @@ class ReferralWebViewBridge( private val onWebViewLoaded: () -> Unit ) { companion object { - const val JS_BRIDGE_OBJECT_NAME = "ReferralBridge" + const val JS_BRIDGE_OBJECT_NAME = "AndroidReferralBridge" private const val REFERRAL_URL = "https://referral-dapp.key.app/" } private var referralWebView: WebView? = null + private var isJsProviderInjected = false + private val onPageLoadingFinishedClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + // inject JS provider so WebView can work with our JS interface + if (!isJsProviderInjected) { + val jsProvider = view.resources.assets.open("referral_bridge_provider.js") + .bufferedReader() + .readText() + + view.evaluateJavascript(jsProvider, null) + isJsProviderInjected = true + } + } + override fun onPageFinished(view: WebView?, url: String?) { if (view?.progress == 100) { onWebViewLoaded.invoke() @@ -36,12 +54,32 @@ class ReferralWebViewBridge( } } + private val consoleLogger = object : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + val logMessage = buildString { + append(consoleMessage.message()) + append(" at ") + append(consoleMessage.sourceId()) + append(":") + append(consoleMessage.lineNumber()) + } + + if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { + Timber.tag(JS_BRIDGE_OBJECT_NAME).e(Exception(logMessage)) + } else { + Timber.tag(JS_BRIDGE_OBJECT_NAME).w(logMessage) + } + return true + } + } + init { referralWebView = webView referralWebView?.apply { settings.javaScriptEnabled = true addJavascriptInterface(BridgeInterface(), JS_BRIDGE_OBJECT_NAME) webViewClient = onPageLoadingFinishedClient + webChromeClient = consoleLogger loadUrl(REFERRAL_URL) } } @@ -60,7 +98,6 @@ class ReferralWebViewBridge( } private inner class BridgeInterface { - @JavascriptInterface fun nativeLog(message: String) { Timber.tag(JS_BRIDGE_OBJECT_NAME).w(message) @@ -80,32 +117,32 @@ class ReferralWebViewBridge( wrapInJsResult(e) } } - } - private fun wrapInJsResult(value: String): JsResultWrapper { - return JsonObject() - .apply { addProperty("value", value) } - .toString() - } + @JavascriptInterface + fun showShareDialog(link: String) { + onShareLinkCalled.invoke(link) + } - private fun wrapInJsResult(error: Throwable): JsResultWrapper { - return JsonObject() - .apply { addProperty("error", error.message) } - .toString() - } + @JavascriptInterface + fun getUserPublicKeyAsync(): JsResultWrapper = makeAsyncCall { + wrapInJsResult(tokenKeyProvider.publicKey) + } - @JavascriptInterface - fun showShareDialog(link: String) { - onShareLinkCalled.invoke(link) - } + private fun wrapInJsResult(value: String): JsResultWrapper { + return JsonObject() + .apply { addProperty("value", value) } + .toString() + } - @JavascriptInterface - fun getUserPublicKeyAsync(): JsResultWrapper = makeAsyncCall { - wrapInJsResult(tokenKeyProvider.publicKey) - } + private fun wrapInJsResult(error: Throwable): JsResultWrapper { + return JsonObject() + .apply { addProperty("error", error.message) } + .toString() + } - // invoke block in IO, return result on Main thread - private fun makeAsyncCall(block: suspend () -> String): String = runBlocking(Dispatchers.Main.immediate) { - withContext(Dispatchers.IO) { block.invoke() } + // invoke block in IO, return result on Main thread + private fun makeAsyncCall(block: suspend () -> String): String = runBlocking(Dispatchers.Main.immediate) { + withContext(Dispatchers.IO) { block.invoke() } + } } } From 7ba9c9c33bd83439b8456826a055bd6814ae2dea Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:01:42 +0400 Subject: [PATCH 04/51] ETH-855, ETH-770 - fixing empty tokens states for ref banner and pnl (#2169) --- .../wallet/home/ui/crypto/MyCryptoFragment.kt | 1 - .../home/ui/crypto/MyCryptoPresenter.kt | 8 +- .../home/ui/crypto/mapper/MyCryptoMapper.kt | 10 +++ .../wallet/pnl/interactor/PnlDataObserver.kt | 3 +- .../p2p/wallet/pnl/interactor/PnlDataState.kt | 1 + .../java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt | 2 + .../main/res/layout/fragment_my_crypto.xml | 73 ++++++++++--------- 7 files changed, 62 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt index 1c1081f7d6..52f885385d 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt @@ -253,7 +253,6 @@ class MyCryptoFragment : override fun showEmptyState(isEmpty: Boolean) { with(binding) { textViewEmpty.isVisible = isEmpty - recyclerViewCrypto.isVisible = !isEmpty } setAppBarScrollingState(!isEmpty) } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt index 5b39560e64..1346f66be7 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt @@ -85,7 +85,11 @@ class MyCryptoPresenter( try { tokenServiceCoordinator.refresh() // pnl must restart it's 5 minutes timer after force refresh - pnlInteractor.restartAndRefresh() + // and we should restart it only if it was initially started + // other cases might indicate that we didn't start observer due to empty state + if (pnlInteractor.isStarted()) { + pnlInteractor.restartAndRefresh() + } } catch (cancelled: CancellationException) { Timber.i("Loading tokens job cancelled") } catch (error: Throwable) { @@ -205,7 +209,9 @@ class MyCryptoPresenter( private fun handleEmptyAccount() { cryptoScreenAnalytics.logUserAggregateBalanceBase(BigDecimal.ZERO) + view?.showBalancePnl(null) view?.showBalance(cryptoMapper.mapBalance(BigDecimal.ZERO)) + view?.showItems(cryptoMapper.mapEmptyStateCellItems()) } override fun toggleTokenVisibility(token: Token.Active) { diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt index dfb5dcb110..0fa45b9093 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt @@ -93,6 +93,16 @@ class MyCryptoMapper( return result.toList() } + fun mapEmptyStateCellItems(): List { + val result = mutableListOf() + + if (referralProgramEnabledFeatureToggle.isFeatureEnabled) { + result += ReferralBannerCellModel + } + + return result + } + private fun Token.Active.mapToCellModel( isZerosHidden: Boolean, pnlTokenData: PnlTokenData?, diff --git a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt index 8934bd9d5e..a7da3f03ea 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt @@ -52,13 +52,14 @@ class PnlDataObserver( private var updaterJob: Job? = null private val tokenMints = mutableListOf() private val tokenMintsMutex = Mutex() - private val _state = MutableStateFlow(PnlDataState.Loading) + private val _state = MutableStateFlow(PnlDataState.Idle) val state = _state.asStateFlow() fun isStarted(): Boolean = updaterJob != null fun start() { if (updaterJob != null) return + _state.value = PnlDataState.Loading launchJob() } diff --git a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt index d2e29013b3..71d9110e23 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt @@ -5,6 +5,7 @@ import org.p2p.wallet.pnl.models.PnlData import org.p2p.wallet.pnl.models.PnlTokenData sealed class PnlDataState { + object Idle : PnlDataState() object Loading : PnlDataState() data class Result(val data: PnlData) : PnlDataState() data class Error(val error: Throwable) : PnlDataState() diff --git a/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt b/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt index bdade535e2..5df6f56697 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt @@ -31,6 +31,7 @@ class PnlUiMapper { is PnlDataState.Loading -> { mapBalancePnlLoading() } + is PnlDataState.Idle, is PnlDataState.Error -> { null } @@ -49,6 +50,7 @@ class PnlUiMapper { is PnlDataState.Loading -> { mapBalancePnlLoading() } + is PnlDataState.Idle, is PnlDataState.Error -> { null } diff --git a/app/src/main/res/layout/fragment_my_crypto.xml b/app/src/main/res/layout/fragment_my_crypto.xml index e7f5c289ea..7dd29db4d4 100644 --- a/app/src/main/res/layout/fragment_my_crypto.xml +++ b/app/src/main/res/layout/fragment_my_crypto.xml @@ -65,41 +65,48 @@ android:layout_height="wrap_content" app:layout_behavior="@string/appbar_scrolling_view_behavior"> - - - - - + - - + android:layout_height="wrap_content"> + + + + + + + From e3b37dd8c038e5c865f27605e13d03b108592046 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:01:58 +0400 Subject: [PATCH 05/51] ETH-854 - adjust action buttons for lowres (#2170) --- .../actionbuttons/ActionButtonViewHolder.kt | 37 ++++++++++++++++++- .../actionbuttons/ActionButtonsAdapter.kt | 8 +++- app/src/main/res/layout/item_home_button.xml | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonViewHolder.kt b/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonViewHolder.kt index 6f4e8ec377..b48ab19b91 100644 --- a/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonViewHolder.kt +++ b/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonViewHolder.kt @@ -2,20 +2,55 @@ package org.p2p.wallet.common.ui.widget.actionbuttons import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import org.p2p.wallet.databinding.ItemHomeButtonBinding +import org.p2p.wallet.utils.toPx +import org.p2p.wallet.utils.viewbinding.context import org.p2p.wallet.utils.viewbinding.inflateViewBinding class ActionButtonViewHolder( parent: ViewGroup, private val onButtonClicked: (ActionButton) -> Unit, + private val itemsCount: Int, private val binding: ItemHomeButtonBinding = parent.inflateViewBinding(attachToRoot = false), ) : RecyclerView.ViewHolder(binding.root) { - fun onBind(actionButton: ActionButton) { + private val displayMetrics = binding.context.resources.displayMetrics + private val screenWidthMinusMargins = displayMetrics.widthPixels - (16.toPx() * 2) + + fun onBind(actionButton: ActionButton, position: Int) { binding.root.id = actionButton.viewId // for UI testing + adjustMarginsForLowRes(position) + binding.textViewTitle.setText(actionButton.textRes) binding.imageButtonAction.setImageResource(actionButton.iconRes) binding.imageButtonAction.setOnClickListener { onButtonClicked.invoke(actionButton) } } + + /** + * Handling case, when display resolution is scaled down (dynamic resolution feature) + * and we don't have enough space to fit all buttons on the screen. + * This logic takes into account that by design, our buttons are centered on the screen, + * so when buttons can fit on the screen, we don't change original layout. + */ + private fun adjustMarginsForLowRes(position: Int) { + val lp = binding.root.layoutParams as MarginLayoutParams + val requiredSpace = ((lp.width + lp.leftMargin + lp.rightMargin) * itemsCount) + + // use "widthPixels" because there can be a case when difference + // between requiredSpace and widthPixels is around 2 pixels (float conversions loss?) + if (requiredSpace > displayMetrics.widthPixels) { + if (position == itemsCount - 1) { + lp.leftMargin = 0 + lp.rightMargin = 0 + } else { + val totalMargin = screenWidthMinusMargins - (lp.width * itemsCount) + val marginPerItem = totalMargin / (itemsCount - 1) + lp.leftMargin = 0 + lp.rightMargin = marginPerItem + } + binding.root.layoutParams = lp + } + } } diff --git a/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonsAdapter.kt b/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonsAdapter.kt index 1696ea8a91..a4ed673e37 100644 --- a/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonsAdapter.kt +++ b/app/src/main/java/org/p2p/wallet/common/ui/widget/actionbuttons/ActionButtonsAdapter.kt @@ -18,9 +18,13 @@ class ActionButtonsAdapter( override fun getItemCount(): Int = buttons.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActionButtonViewHolder = - ActionButtonViewHolder(parent, onButtonClicked) + ActionButtonViewHolder( + parent = parent, + onButtonClicked = onButtonClicked, + itemsCount = buttons.size + ) override fun onBindViewHolder(holder: ActionButtonViewHolder, position: Int) { - holder.onBind(buttons[position]) + holder.onBind(buttons[position], position) } } diff --git a/app/src/main/res/layout/item_home_button.xml b/app/src/main/res/layout/item_home_button.xml index 838bb91d0b..525875f442 100644 --- a/app/src/main/res/layout/item_home_button.xml +++ b/app/src/main/res/layout/item_home_button.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/viewContainer" - android:layout_width="wrap_content" + android:layout_width="52dp" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:gravity="center" From 8cf901be618e406c48789294b9231af080e01beb Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:26:49 +0300 Subject: [PATCH 06/51] PWN-794 - Add in-app updates (#2167) --- .../di/FeatureTogglesModule.kt | 3 + .../remote_config/AppFirebaseRemoteConfig.kt | 3 +- .../ForceUpdateVersionCodeFeatureToggle.kt | 15 ++++ .../org/p2p/wallet/root/AppUpdateService.kt | 80 +++++++++++++++++++ .../java/org/p2p/wallet/root/RootActivity.kt | 43 ++++++++-- buildSrc/src/main/java/Dependencies.kt | 4 +- 6 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/ForceUpdateVersionCodeFeatureToggle.kt create mode 100644 app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt diff --git a/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt b/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt index f7f4a6d0d1..b2c1862a26 100644 --- a/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt +++ b/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt @@ -12,6 +12,7 @@ import org.p2p.wallet.common.feature_toggles.remote_config.LocalFirebaseRemoteCo import org.p2p.wallet.common.feature_toggles.remote_config.RemoteConfigValuesProvider import org.p2p.wallet.common.feature_toggles.toggles.FeatureToggleProvider import org.p2p.wallet.common.feature_toggles.toggles.remote.EthAddressEnabledFeatureToggle +import org.p2p.wallet.common.feature_toggles.toggles.remote.ForceUpdateVersionCodeFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationDebounceFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationFrequencyFeatureToggle @@ -52,6 +53,7 @@ object FeatureTogglesModule : InjectionModule { get(), get(), get(), + get(), ).toList() } @@ -62,6 +64,7 @@ object FeatureTogglesModule : InjectionModule { factoryOf(::NetworkObservationPercentFeatureToggle) factoryOf(::NetworkObservationFrequencyFeatureToggle) factoryOf(::NetworkObservationDebounceFeatureToggle) + factoryOf(::ForceUpdateVersionCodeFeatureToggle) factoryOf(::UsernameDomainFeatureToggle) factoryOf(::RegisterUsernameEnabledFeatureToggle) factoryOf(::RegisterUsernameSkipEnabledFeatureToggle) diff --git a/app/src/main/java/org/p2p/wallet/common/feature_toggles/remote_config/AppFirebaseRemoteConfig.kt b/app/src/main/java/org/p2p/wallet/common/feature_toggles/remote_config/AppFirebaseRemoteConfig.kt index f9da5c76d2..3891930e12 100644 --- a/app/src/main/java/org/p2p/wallet/common/feature_toggles/remote_config/AppFirebaseRemoteConfig.kt +++ b/app/src/main/java/org/p2p/wallet/common/feature_toggles/remote_config/AppFirebaseRemoteConfig.kt @@ -8,8 +8,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import org.p2p.wallet.BuildConfig import org.p2p.core.common.di.AppScope +import org.p2p.wallet.BuildConfig import org.p2p.wallet.utils.retryOnException private const val NO_VALUE = "" @@ -53,7 +53,6 @@ class AppFirebaseRemoteConfig( remoteConfig.fetchAndActivate() .addOnSuccessListener { isFetchFailed = false - Timber.d("Remote config fetched and activated") Timber.i("Remote config fetched toggles: ${allFeatureTogglesRaw()}") continuation.resume(allFeatureTogglesRaw()) diff --git a/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/ForceUpdateVersionCodeFeatureToggle.kt b/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/ForceUpdateVersionCodeFeatureToggle.kt new file mode 100644 index 0000000000..2652096ca8 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/ForceUpdateVersionCodeFeatureToggle.kt @@ -0,0 +1,15 @@ +package org.p2p.wallet.common.feature_toggles.toggles.remote + +import org.p2p.wallet.common.feature_toggles.remote_config.RemoteConfigValuesProvider + +class ForceUpdateVersionCodeFeatureToggle( + valuesProvider: RemoteConfigValuesProvider +) : LongFeatureToggle(valuesProvider) { + companion object { + const val NOT_FETCHED = -1L + } + + override val featureKey: String = "android_force_update_version_code" + override val featureDescription: String = "Version code to force update to" + override val defaultValue: Long = NOT_FETCHED +} diff --git a/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt b/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt new file mode 100644 index 0000000000..023d4a1317 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt @@ -0,0 +1,80 @@ +package org.p2p.wallet.root + +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import android.content.Context +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability +import com.google.android.play.core.ktx.isImmediateUpdateAllowed +import com.google.android.play.core.ktx.requestAppUpdateInfo +import timber.log.Timber +import kotlin.time.Duration.Companion.seconds +import org.p2p.wallet.BuildConfig +import org.p2p.wallet.common.feature_toggles.toggles.remote.ForceUpdateVersionCodeFeatureToggle +import org.p2p.wallet.common.feature_toggles.toggles.remote.ForceUpdateVersionCodeFeatureToggle.Companion.NOT_FETCHED +import org.p2p.wallet.infrastructure.coroutines.waitForCondition + +class AppUpdateService( + private val context: Context, + private val forceUpdateVersionCodeFt: ForceUpdateVersionCodeFeatureToggle +) { + private suspend fun isUpdateNeeded(): Boolean { + val isForceUpdateCodeFetched = waitForCondition(2.seconds.inWholeMilliseconds) { + forceUpdateVersionCodeFt.value != NOT_FETCHED + } + if (!isForceUpdateCodeFetched) { + Timber.e(IllegalArgumentException("Force update version is not fetched")) + return false + } + + val forceUpdateVersionCode = forceUpdateVersionCodeFt.value + + val appUpdateInfo = AppUpdateManagerFactory.create(context).requestAppUpdateInfo() + val currentVersionCode = BuildConfig.VERSION_CODE + val storeVersionCode = appUpdateInfo.availableVersionCode() + + Timber.i( + buildString { + appendLine("storeVersionCode = $storeVersionCode, ") + appendLine("currentVersionCode = $currentVersionCode, ") + appendLine("forceUpdateVersionCode = $forceUpdateVersionCode") + } + ) + + val isForceUpdateNeeded = currentVersionCode < forceUpdateVersionCode + val isForceUpdateAvailableInStore = storeVersionCode >= forceUpdateVersionCode + + val isUpdatePossible = appUpdateInfo.isImmediateUpdateAllowed + + return isUpdatePossible && isForceUpdateAvailableInStore && isForceUpdateNeeded + } + + suspend fun startInAppUpdateIfNeeded( + activityResultLauncher: ActivityResultLauncher, + ) { + if (!isUpdateNeeded()) return + + val appUpdateManager = AppUpdateManagerFactory.create(context) + val appUpdateInfo = appUpdateManager.requestAppUpdateInfo() + val immediateUpdate = AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE) + + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + activityResultLauncher, + immediateUpdate + ) + } + + suspend fun checkUpdateIsNotStalled( + activityResultLauncher: ActivityResultLauncher + ) { + val appUpdateManager = AppUpdateManagerFactory.create(context) + val appUpdateInfo = appUpdateManager.requestAppUpdateInfo() + val isStalled = appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS + if (isStalled) { + startInAppUpdateIfNeeded(activityResultLauncher) + } + } +} diff --git a/app/src/main/java/org/p2p/wallet/root/RootActivity.kt b/app/src/main/java/org/p2p/wallet/root/RootActivity.kt index f8964db7ea..bce2f7b22e 100644 --- a/app/src/main/java/org/p2p/wallet/root/RootActivity.kt +++ b/app/src/main/java/org/p2p/wallet/root/RootActivity.kt @@ -1,6 +1,7 @@ package org.p2p.wallet.root import androidx.activity.addCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.splashscreen.SplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat @@ -15,11 +16,13 @@ import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.Snackbar +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import timber.log.Timber import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.p2p.core.crashlytics.CrashLogger import org.p2p.core.crashlytics.helpers.FragmentLoggingLifecycleListener import org.p2p.core.utils.KeyboardListener @@ -67,11 +70,17 @@ class RootActivity : private val analyticsInteractor: ScreensAnalyticsInteractor by inject() private val crashLogger: CrashLogger by inject() + private val appUpdateService: AppUpdateService by lazy { + AppUpdateService( + context = applicationContext, + forceUpdateVersionCodeFt = get() + ) + } private val networkObserver: SolanaNetworkObserver by inject() private val decorSystemBarsDelegate by lazy { DecorSystemBarsDelegate(this) } private val visibilityDelegate by lazy { ActivityVisibilityDelegate(this) } - override val keyboardState: MutableStateFlow = MutableStateFlow(false) + override val keyboardState = MutableStateFlow(false) private lateinit var binding: ActivityRootBinding @@ -79,6 +88,14 @@ class RootActivity : private var splashScreenBox: SplashScreenBox? = null private var onSplashDataLoaded: Boolean = false + private val appUpdateIntentLauncher = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> + if (result.resultCode != RESULT_OK) { + Timber.e(Exception("Update flow failed! Result code: ${result.resultCode}}")) + finishAffinity() + } + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) handleDeeplink(intent) @@ -105,11 +122,18 @@ class RootActivity : checkForGoogleServices() - supportFragmentManager.registerFragmentLifecycleCallbacks(FragmentLoggingLifecycleListener(), true) + supportFragmentManager.registerFragmentLifecycleCallbacks( + FragmentLoggingLifecycleListener(), + true + ) deeplinksManager.setRootListener(this) handleDeeplink() registerNetworkObserver() + + lifecycleScope.launch { + appUpdateService.startInAppUpdateIfNeeded(appUpdateIntentLauncher) + } } fun hideSplashScreen() { @@ -169,10 +193,10 @@ class RootActivity : handlerQualifierName: String ) { NewTransactionProgressBottomSheet.show( - supportFragmentManager, - internalTransactionId, - data, - handlerQualifierName + fragmentManager = supportFragmentManager, + transactionId = internalTransactionId, + data = data, + handlerQualifierName = handlerQualifierName ) } @@ -222,6 +246,13 @@ class RootActivity : crashLogger.setCustomKey("has_google_services", userHasGoogleServices) } + override fun onResume() { + super.onResume() + lifecycleScope.launch { + appUpdateService.checkUpdateIsNotStalled(appUpdateIntentLauncher) + } + } + override fun onStop() { super.onStop() if (intent.action == ACTION_RESTART) return diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index cee8fbaada..2a9c60c1de 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -29,6 +29,7 @@ object Dependencies { private const val fragmentKtx = "androidx.fragment:fragment-ktx:$fragmentKtxVersion" private const val biometricKtx = "androidx.biometric:biometric:1.1.0" private const val browser = "androidx.browser:browser:1.5.0" + private const val appUpdate = "com.google.android.play:app-update-ktx:2.1.0" const val swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" const val flexbox = "com.google.android.flexbox:flexbox:3.0.0" @@ -200,7 +201,8 @@ object Dependencies { activityKtx, fragmentKtx, biometricKtx, - browser + browser, + appUpdate ) val coroutineLibraries = listOf( From 4a3bc45984b3870a6be1e811306f6598572399a2 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:45:24 +0400 Subject: [PATCH 07/51] =?UTF-8?q?NO-TICKET=20-=20hotfix:=20make=20rentEpoc?= =?UTF-8?q?h=20field=20BigInteger=20instead=20of=20int=20an=E2=80=A6=20(#2?= =?UTF-8?q?173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * NO-TICKET - hotfix: make rentEpoch field BigInteger instead of int and long * NO-TICKET - rollback app version --- .../p2p/wallet/updates/handler/SolanaAccountUpdateHandler.kt | 2 +- .../p2p/wallet/updates/handler/SplTokenProgramUpdateHandler.kt | 2 +- solana/src/main/java/org/p2p/solanaj/kits/AccountInfoParsed.kt | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/updates/handler/SolanaAccountUpdateHandler.kt b/app/src/main/java/org/p2p/wallet/updates/handler/SolanaAccountUpdateHandler.kt index ab13303991..f31c00f1ae 100644 --- a/app/src/main/java/org/p2p/wallet/updates/handler/SolanaAccountUpdateHandler.kt +++ b/app/src/main/java/org/p2p/wallet/updates/handler/SolanaAccountUpdateHandler.kt @@ -47,5 +47,5 @@ private data class BalanceValueResponse( @SerializedName("executable") val executable: Boolean, @SerializedName("rentEpoch") - val rentEpoch: Int + val rentEpoch: BigInteger ) diff --git a/app/src/main/java/org/p2p/wallet/updates/handler/SplTokenProgramUpdateHandler.kt b/app/src/main/java/org/p2p/wallet/updates/handler/SplTokenProgramUpdateHandler.kt index c40e8b992a..a47af6d39a 100644 --- a/app/src/main/java/org/p2p/wallet/updates/handler/SplTokenProgramUpdateHandler.kt +++ b/app/src/main/java/org/p2p/wallet/updates/handler/SplTokenProgramUpdateHandler.kt @@ -75,5 +75,5 @@ private data class AccountNotificationResponse( @SerializedName("executable") val executable: Boolean, @SerializedName("rentEpoch") - val rentEpoch: Int + val rentEpoch: BigInteger ) diff --git a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoParsed.kt b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoParsed.kt index 089921e6dc..0e351282a6 100644 --- a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoParsed.kt +++ b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoParsed.kt @@ -1,6 +1,7 @@ package org.p2p.solanaj.kits import com.google.gson.annotations.SerializedName +import java.math.BigInteger data class AccountInfoParsed( @SerializedName("data") @@ -16,7 +17,7 @@ data class AccountInfoParsed( val owner: String, @SerializedName("rentEpoch") - val rentEpoch: Long + val rentEpoch: BigInteger ) { var address: String? = null From 042db53af2e8f507c6227701dc28f4bbf21a2b29 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:33:15 +0300 Subject: [PATCH 08/51] NO-TICKET - fix rent from release hotfix (#2175) * NO-TICKET - hotfix: make rentEpoch field BigInteger instead of int and long * NO-TICKET - migrate rent to BigInteger --------- Co-authored-by: Eduard Maximovich --- .../java/org/p2p/wallet/send/SendFeeRelayerManager.kt | 2 +- buildSrc/src/main/java/AppVersions.kt | 2 +- .../org/p2p/solanaj/kits/AccountInfoTokenExtension.kt | 8 +++++--- .../java/org/p2p/solanaj/model/types/AccountInfo.java | 5 +++-- .../java/org/p2p/solanaj/model/types/EpochInfo.kt | 11 ++++++----- 5 files changed, 16 insertions(+), 12 deletions(-) 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 0c78bf3e8d..b44308837b 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -77,7 +77,7 @@ class SendFeeRelayerManager( private lateinit var recipientAddress: SearchResult private lateinit var solToken: Token.Active private var initializeCompleted = false - private var currentSolanaEpoch: Int = 0 + private var currentSolanaEpoch: BigInteger = BigInteger.ZERO private var minRentExemption: BigInteger = BigInteger.ZERO diff --git a/buildSrc/src/main/java/AppVersions.kt b/buildSrc/src/main/java/AppVersions.kt index e6ce906ec5..fca9ed6990 100644 --- a/buildSrc/src/main/java/AppVersions.kt +++ b/buildSrc/src/main/java/AppVersions.kt @@ -1,7 +1,7 @@ object AppVersions { const val VERSION_MAJOR = 2 const val VERSION_MINOR = 11 - const val VERSION_HOTFIX = 1 + const val VERSION_HOTFIX = 2 const val VERSION_PATCH = 0 const val VERSION_BUILD = 3582 } diff --git a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt index 66cae35c9c..b95bafd763 100644 --- a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt +++ b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt @@ -5,6 +5,8 @@ import com.google.gson.annotations.SerializedName import java.math.BigDecimal import java.math.BigInteger import org.p2p.core.utils.divideSafe +import org.p2p.core.utils.isZero +import org.p2p.core.utils.orZero typealias TokenExtensionsMap = Map @@ -32,7 +34,7 @@ sealed interface AccountInfoTokenExtensionConfig { } data class TransferFeeConfigData( - val epoch: Int, + val epoch: BigInteger, val maximumFee: BigInteger, val transferFeeBasisPoints: Int, ) { @@ -42,8 +44,8 @@ sealed interface AccountInfoTokenExtensionConfig { .multiply("100".toBigDecimal().setScale(4)) } - fun getActualTransferFee(currentEpoch: Int): TransferFeeConfigData? { - if (currentEpoch > (newerTransferFee?.epoch ?: 0) || currentEpoch == 0) { + fun getActualTransferFee(currentEpoch: BigInteger): TransferFeeConfigData? { + if (currentEpoch > (newerTransferFee?.epoch.orZero()) || currentEpoch.isZero()) { return newerTransferFee ?: olderTransferFee } return olderTransferFee diff --git a/solana/src/main/java/org/p2p/solanaj/model/types/AccountInfo.java b/solana/src/main/java/org/p2p/solanaj/model/types/AccountInfo.java index f233c278c5..9f8dc1899b 100644 --- a/solana/src/main/java/org/p2p/solanaj/model/types/AccountInfo.java +++ b/solana/src/main/java/org/p2p/solanaj/model/types/AccountInfo.java @@ -2,6 +2,7 @@ import com.google.gson.annotations.SerializedName; +import java.math.BigInteger; import java.util.List; import androidx.annotation.Nullable; @@ -20,7 +21,7 @@ public static class Value { @SerializedName("owner") private String owner; @SerializedName("rentEpoch") - private long rentEpoch; + private BigInteger rentEpoch; @Nullable public List getData() { @@ -39,7 +40,7 @@ public String getOwner() { return owner; } - public long getRentEpoch() { + public BigInteger getRentEpoch() { return rentEpoch; } diff --git a/solana/src/main/java/org/p2p/solanaj/model/types/EpochInfo.kt b/solana/src/main/java/org/p2p/solanaj/model/types/EpochInfo.kt index 9c4dfea559..278887a8ab 100644 --- a/solana/src/main/java/org/p2p/solanaj/model/types/EpochInfo.kt +++ b/solana/src/main/java/org/p2p/solanaj/model/types/EpochInfo.kt @@ -1,18 +1,19 @@ package org.p2p.solanaj.model.types import com.google.gson.annotations.SerializedName +import java.math.BigInteger data class EpochInfo( @SerializedName("absoluteSlot") - val absoluteSlot: Long, + val absoluteSlot: BigInteger, @SerializedName("blockHeight") - val blockHeight: Long, + val blockHeight: BigInteger, @SerializedName("epoch") - val epoch: Int, + val epoch: BigInteger, @SerializedName("slotIndex") - val slotIndex: Long, + val slotIndex: BigInteger, @SerializedName("slotsInEpoch") - val slotsInEpoch: Long, + val slotsInEpoch: BigInteger, @SerializedName("transactionCount") val transactionCount: Long ) From 38741f9b5d3be51233c15d9fdb665fc6d1f5edb6 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Tue, 6 Feb 2024 13:10:03 +0400 Subject: [PATCH 09/51] ETH-843 - fix token2022 websocket subscription filter (#2176) --- .../p2p/wallet/updates/subscribe/Token2022ProgramSubscriber.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/updates/subscribe/Token2022ProgramSubscriber.kt b/app/src/main/java/org/p2p/wallet/updates/subscribe/Token2022ProgramSubscriber.kt index 918a4ffb31..01b3955b56 100644 --- a/app/src/main/java/org/p2p/wallet/updates/subscribe/Token2022ProgramSubscriber.kt +++ b/app/src/main/java/org/p2p/wallet/updates/subscribe/Token2022ProgramSubscriber.kt @@ -5,7 +5,6 @@ import org.p2p.solanaj.model.types.Encoding import org.p2p.solanaj.model.types.RpcMapRequest import org.p2p.solanaj.model.types.RpcRequest import org.p2p.solanaj.programs.SystemProgram -import org.p2p.solanaj.programs.TokenProgram import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.updates.SocketSubscriptionUpdateType import org.p2p.wallet.updates.SubscriptionUpdatesManager @@ -30,7 +29,6 @@ class Token2022ProgramSubscriber( "commitment" to ConfirmationStatus.CONFIRMED.value, "encoding" to Encoding.BASE64.encoding, "filters" to listOf( - mapOf("dataSize" to TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH), mapOf( "memcmp" to mapOf( "offset" to 32, From ece91d9dc6ed5244951f8e04285d2296de16271e Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:49:39 +0300 Subject: [PATCH 10/51] PWN-841 - Fix switching card when usd is selected (#2177) --- .../history/ui/history/HistoryFragment.kt | 4 +- .../history/ui/token/TokenHistoryFragment.kt | 4 +- .../home/addmoney/ui/AddMoneyBottomSheet.kt | 4 +- .../home/deeplinks/DeeplinkScreenNavigator.kt | 2 +- .../deeplinks/MainFragmentDeeplinkHandler.kt | 2 +- .../ui/container/MainContainerFragment.kt | 8 +- .../org/p2p/wallet/moonpay/ui/BuyModule.kt | 8 +- .../new/{NewBuyContract.kt => BuyContract.kt} | 2 +- .../new/{NewBuyFragment.kt => BuyFragment.kt} | 40 ++-- .../{NewBuyPresenter.kt => BuyPresenter.kt} | 47 +++-- .../send/ui/stub/SendUnavailableFragment.kt | 4 +- app/src/main/res/layout/fragment_buy.xml | 186 +++++++++++++++--- app/src/main/res/layout/fragment_new_buy.xml | 175 ---------------- core/build.gradle.kts | 1 - 14 files changed, 226 insertions(+), 261 deletions(-) rename app/src/main/java/org/p2p/wallet/moonpay/ui/new/{NewBuyContract.kt => BuyContract.kt} (98%) rename app/src/main/java/org/p2p/wallet/moonpay/ui/new/{NewBuyFragment.kt => BuyFragment.kt} (91%) rename app/src/main/java/org/p2p/wallet/moonpay/ui/new/{NewBuyPresenter.kt => BuyPresenter.kt} (93%) delete mode 100644 app/src/main/res/layout/fragment_new_buy.xml diff --git a/app/src/main/java/org/p2p/wallet/history/ui/history/HistoryFragment.kt b/app/src/main/java/org/p2p/wallet/history/ui/history/HistoryFragment.kt index 8f3b294584..6684d17218 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/history/HistoryFragment.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/history/HistoryFragment.kt @@ -17,7 +17,7 @@ import org.p2p.wallet.history.ui.historylist.HistoryListViewType import org.p2p.wallet.history.ui.sendvialink.HistorySendLinksFragment import org.p2p.wallet.jupiter.model.SwapOpenedFrom import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment -import org.p2p.wallet.moonpay.ui.new.NewBuyFragment +import org.p2p.wallet.moonpay.ui.new.BuyFragment import org.p2p.wallet.moonpay.ui.transaction.SellTransactionDetailsBottomSheet import org.p2p.wallet.root.RootListener import org.p2p.wallet.transaction.model.NewShowProgress @@ -106,7 +106,7 @@ class HistoryFragment : } override fun showBuyScreen(token: Token) { - replaceFragment(NewBuyFragment.create(token)) + replaceFragment(BuyFragment.create(token)) } override fun openTransactionDetailsScreen(transactionId: String) { diff --git a/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryFragment.kt b/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryFragment.kt index 0506f3d3a3..0960ff0cf4 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryFragment.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryFragment.kt @@ -25,7 +25,7 @@ import org.p2p.wallet.history.ui.historylist.HistoryListViewType import org.p2p.wallet.jupiter.model.SwapOpenedFrom import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment import org.p2p.wallet.moonpay.analytics.BuyAnalytics -import org.p2p.wallet.moonpay.ui.new.NewBuyFragment +import org.p2p.wallet.moonpay.ui.new.BuyFragment import org.p2p.wallet.moonpay.ui.transaction.SellTransactionDetailsBottomSheet import org.p2p.wallet.pnl.ui.PnlDetailsBottomSheet import org.p2p.wallet.receive.analytics.ReceiveAnalytics @@ -175,7 +175,7 @@ class TokenHistoryFragment : when (clickedButton) { ActionButton.BUY_BUTTON -> { buyAnalytics.logTokenScreenActionClicked() - replaceFragment(NewBuyFragment.create(tokenForHistory)) + replaceFragment(BuyFragment.create(tokenForHistory)) } ActionButton.RECEIVE_BUTTON -> { receiveAnalytics.logTokenScreenActionClicked() diff --git a/app/src/main/java/org/p2p/wallet/home/addmoney/ui/AddMoneyBottomSheet.kt b/app/src/main/java/org/p2p/wallet/home/addmoney/ui/AddMoneyBottomSheet.kt index c40ff50237..d8b3bc2e40 100644 --- a/app/src/main/java/org/p2p/wallet/home/addmoney/ui/AddMoneyBottomSheet.kt +++ b/app/src/main/java/org/p2p/wallet/home/addmoney/ui/AddMoneyBottomSheet.kt @@ -19,7 +19,7 @@ import org.p2p.wallet.common.mvp.BaseMvpBottomSheet import org.p2p.wallet.databinding.DialogAddMoneyBinding import org.p2p.wallet.home.addmoney.AddMoneyContract import org.p2p.wallet.moonpay.model.PaymentMethod -import org.p2p.wallet.moonpay.ui.new.NewBuyFragment +import org.p2p.wallet.moonpay.ui.new.BuyFragment import org.p2p.wallet.receive.ReceiveFragmentFactory import org.p2p.wallet.striga.StrigaFragmentFactory import org.p2p.wallet.striga.user.model.StrigaUserStatusDestination @@ -70,7 +70,7 @@ class AddMoneyBottomSheet : override fun navigateToBankCard(tokenToBuy: Token, paymentMethod: PaymentMethod.MethodType) { dismissAndNavigate( - NewBuyFragment.create( + BuyFragment.create( token = tokenToBuy, preselectedMethodType = paymentMethod ) diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt index 7b811b751f..b37717d292 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt @@ -6,7 +6,7 @@ import org.p2p.wallet.jupiter.model.SwapOpenedFrom import org.p2p.wallet.jupiter.ui.main.SwapDeeplinkStrictTokenWarning interface DeeplinkScreenNavigator { - fun navigateToNewBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) + fun navigateToBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) fun navigateToBuyScreen(token: Token) fun showCashOut() fun showSwapWithArgs( diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt index 0b854f09bd..14ea615c2b 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt @@ -97,7 +97,7 @@ class MainFragmentDeeplinkHandler( coroutineScope.launch { val token = userInteractor.getSingleTokenForBuy(listOf(cryptoToken)) if (token != null) { - screenNavigator?.navigateToNewBuyScreen(token, fiatToken, fiatAmount) + screenNavigator?.navigateToBuyScreen(token, fiatToken, fiatAmount) } else { onBuyHandled() } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt index 04bd267c77..812b298a3e 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt @@ -28,7 +28,7 @@ import org.p2p.wallet.home.ui.main.MainFragmentOnCreateAction import org.p2p.wallet.jupiter.model.SwapOpenedFrom import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment import org.p2p.wallet.jupiter.ui.main.SwapDeeplinkStrictTokenWarning -import org.p2p.wallet.moonpay.ui.new.NewBuyFragment +import org.p2p.wallet.moonpay.ui.new.BuyFragment import org.p2p.wallet.notification.AppNotificationManager import org.p2p.wallet.push_notifications.analytics.AnalyticsPushChannel import org.p2p.wallet.sell.ui.payload.SellPayloadFragment @@ -175,12 +175,12 @@ class MainContainerFragment : } //region DeeplinkScreenNavigator - override fun navigateToNewBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) { - replaceFragment(NewBuyFragment.create(token, fiatToken, fiatAmount)) + override fun navigateToBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) { + replaceFragment(BuyFragment.create(token, fiatToken, fiatAmount)) } override fun navigateToBuyScreen(token: Token) { - replaceFragment(NewBuyFragment.create(token)) + replaceFragment(BuyFragment.create(token)) } override fun showCashOut() { diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/BuyModule.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/BuyModule.kt index 9d1c5aa5f6..0c531c1a06 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/BuyModule.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/BuyModule.kt @@ -8,8 +8,8 @@ import org.p2p.core.token.Token import org.p2p.wallet.moonpay.interactor.BuyInteractor import org.p2p.wallet.moonpay.interactor.PaymentMethodsInteractor import org.p2p.wallet.moonpay.model.PaymentMethod -import org.p2p.wallet.moonpay.ui.new.NewBuyContract -import org.p2p.wallet.moonpay.ui.new.NewBuyPresenter +import org.p2p.wallet.moonpay.ui.new.BuyContract +import org.p2p.wallet.moonpay.ui.new.BuyPresenter import org.p2p.wallet.receive.solana.ReceiveSolanaContract import org.p2p.wallet.receive.solana.ReceiveSolanaPresenter @@ -37,13 +37,13 @@ object BuyModule : InjectionModule { context = get() ) } - factory { ( + factory { ( token: Token, fiatToken: String?, fiatAmount: String?, preselectedMethodType: PaymentMethod.MethodType? ) -> - NewBuyPresenter( + BuyPresenter( tokenToBuy = token, fiatToken = fiatToken, fiatAmount = fiatAmount, diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyContract.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyContract.kt similarity index 98% rename from app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyContract.kt rename to app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyContract.kt index 680fb7c34f..cf60a2b3c7 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyContract.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyContract.kt @@ -9,7 +9,7 @@ import org.p2p.wallet.moonpay.model.BuyViewData import org.p2p.wallet.moonpay.model.PaymentMethod import org.p2p.wallet.moonpay.repository.sell.FiatCurrency -interface NewBuyContract { +interface BuyContract { interface View : MvpView { fun showPreselectedAmount(amount: String) diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyFragment.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt similarity index 91% rename from app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyFragment.kt rename to app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt index aea0b95e79..a4fc3ba1c1 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyFragment.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt @@ -13,10 +13,11 @@ import org.p2p.core.token.Token import org.p2p.core.utils.Constants import org.p2p.uikit.components.FocusField import org.p2p.uikit.utils.getColor +import org.p2p.uikit.utils.setTextColorRes import org.p2p.wallet.R import org.p2p.wallet.common.analytics.interactor.ScreensAnalyticsInteractor import org.p2p.wallet.common.mvp.BaseMvpFragment -import org.p2p.wallet.databinding.FragmentNewBuyBinding +import org.p2p.wallet.databinding.FragmentBuyBinding import org.p2p.wallet.home.ui.select.bottomsheet.NewSelectTokenBottomSheet import org.p2p.wallet.home.ui.select.bottomsheet.SelectCurrencyBottomSheet import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider @@ -46,9 +47,9 @@ private const val KEY_REQUEST = "KEY_REQUEST_NEW_BUY" private const val KEY_RESULT_TOKEN = "KEY_RESULT_TOKEN" private const val KEY_RESULT_CURRENCY = "KEY_RESULT_CURRENCY" -class NewBuyFragment : - BaseMvpFragment(R.layout.fragment_new_buy), - NewBuyContract.View { +class BuyFragment : + BaseMvpFragment(R.layout.fragment_buy), + BuyContract.View { companion object { fun create( @@ -56,8 +57,8 @@ class NewBuyFragment : fiatToken: String? = null, fiatAmount: String? = null, preselectedMethodType: PaymentMethod.MethodType? = null - ): NewBuyFragment = - NewBuyFragment() + ): BuyFragment = + BuyFragment() .withArgs( EXTRA_TOKEN to token, EXTRA_FIAT_TOKEN to fiatToken, @@ -71,7 +72,7 @@ class NewBuyFragment : private val fiatAmount: String? by args(EXTRA_FIAT_AMOUNT) private val preselectedMethodType: PaymentMethod.MethodType? by args(EXTRA_PRESELECTED_METHOD_TYPE) - override val presenter: NewBuyContract.Presenter by inject { + override val presenter: BuyContract.Presenter by inject { parametersOf( token, fiatToken, @@ -80,7 +81,7 @@ class NewBuyFragment : ) } - private val binding: FragmentNewBuyBinding by viewBinding() + private val binding: FragmentBuyBinding by viewBinding() private val adapter: PaymentMethodsAdapter by unsafeLazy { PaymentMethodsAdapter(presenter::onPaymentMethodSelected) } @@ -119,7 +120,6 @@ class NewBuyFragment : presenter.setTokenToBuy(it) } } - result.containsKey(KEY_RESULT_CURRENCY) -> { result.getSerializableCompat(KEY_RESULT_CURRENCY)?.let { setCurrencyCode(it.abbreviation) @@ -129,7 +129,7 @@ class NewBuyFragment : } } - private fun FragmentNewBuyBinding.initViews() { + private fun FragmentBuyBinding.initViews() { recyclerViewMethods.adapter = adapter toolbarBuy.title = getString(R.string.buy_toolbar_title, token.tokenSymbol) @@ -143,18 +143,16 @@ class NewBuyFragment : buttonBuy.text = getString(R.string.buy_toolbar_title, token.tokenSymbol) amountsView.apply { - tokenSymbol = this@NewBuyFragment.token.tokenSymbol + tokenSymbol = this@BuyFragment.token.tokenSymbol currencyCode = FiatCurrency.USD.abbreviation.uppercase() - setOnSelectTokenClickListener { presenter.onSelectTokenClicked() } + setOnSelectTokenClickListener(presenter::onSelectTokenClicked) setOnTokenAmountChangeListener { amount -> presenter.setBuyAmount(amount, isDelayEnabled = false) } - setOnSelectCurrencyClickListener { presenter.onSelectCurrencyClicked() } + setOnSelectCurrencyClickListener(presenter::onSelectCurrencyClicked) setOnCurrencyAmountChangeListener { amount -> presenter.setBuyAmount(amount, isDelayEnabled = false) } - setOnFocusChangeListener { focusField -> - presenter.onFocusFieldChanged(focusField) - } + setOnFocusChangeListener(presenter::onFocusFieldChanged) } buttonBuy.setOnClickListener { presenter.onContinueClicked() } @@ -234,10 +232,10 @@ class NewBuyFragment : binding.buttonBuy.isEnabled = isEnabled with(binding.buttonBuy) { if (isEnabled) { - setTextColor(getColor(R.color.text_snow)) + setTextColorRes(R.color.text_snow) setBackgroundColor(getColor(R.color.bg_night)) } else { - setTextColor(getColor(R.color.text_mountain)) + setTextColorRes(R.color.text_mountain) setBackgroundColor(getColor(R.color.bg_rain)) strokeWidth = 1 } @@ -246,8 +244,8 @@ class NewBuyFragment : override fun clearOppositeFieldAndTotal(totalText: String) { when (binding.amountsView.focusField) { - FocusField.TOKEN -> binding.amountsView.setCurrencyAmount(null) - FocusField.CURRENCY -> binding.amountsView.setTokenAmount(null) + FocusField.TOKEN -> binding.amountsView.setCurrencyAmount(currencyAmount = null) + FocusField.CURRENCY -> binding.amountsView.setTokenAmount(tokenAmount = null) } binding.textViewTotalValue.text = totalText @@ -282,7 +280,7 @@ class NewBuyFragment : } override fun onDetach() { - super.onDetach() backPressedCallback?.remove() + super.onDetach() } } diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyPresenter.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt similarity index 93% rename from app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyPresenter.kt rename to app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt index a6354bfa5e..7f0d7f2678 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/NewBuyPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt @@ -9,8 +9,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.p2p.core.analytics.constants.ScreenNames import org.p2p.core.token.Token -import org.p2p.core.utils.Constants -import org.p2p.core.utils.Constants.EUR_READABLE_SYMBOL import org.p2p.core.utils.asCurrency import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken @@ -39,7 +37,7 @@ import org.p2p.wallet.utils.emptyString private const val DELAY_IN_MS = 500L -class NewBuyPresenter( +class BuyPresenter( tokenToBuy: Token, private val fiatToken: String? = null, private val fiatAmount: String? = null, @@ -50,7 +48,7 @@ class NewBuyPresenter( private val resources: Resources, private val buyInteractor: BuyInteractor, private val analyticsInteractor: ScreensAnalyticsInteractor, -) : BasePresenter(), NewBuyContract.Presenter { +) : BasePresenter(), BuyContract.Presenter { private lateinit var tokensToBuy: List @@ -74,7 +72,7 @@ class NewBuyPresenter( FiatCurrency.USD ) - override fun attach(view: NewBuyContract.View) { + override fun attach(view: BuyContract.View) { super.attach(view) loadTokensToBuy() loadAvailablePaymentMethods() @@ -211,7 +209,10 @@ class NewBuyPresenter( override fun setCurrency(currency: FiatCurrency, byUser: Boolean) { if (byUser) { - buyAnalytics.logBuyCurrencyChanged(selectedCurrency.abbreviation, currency.abbreviation) + buyAnalytics.logBuyCurrencyChanged( + fromCurrency = selectedCurrency.abbreviation, + toCurrency = currency.abbreviation + ) } selectedCurrency = currency @@ -230,27 +231,31 @@ class NewBuyPresenter( } private fun isValidCurrencyForPay(): Boolean { - val selectedCurrencyCode = selectedCurrency.abbreviation val currentPaymentMethod = selectedPaymentMethod ?: return false - if (currentPaymentMethod.method == PaymentMethod.MethodType.BANK_TRANSFER) { - if (selectedCurrencyCode == Constants.USD_READABLE_SYMBOL || - (currentAlpha3Code == BANK_TRANSFER_UK_CODE && selectedCurrencyCode == EUR_READABLE_SYMBOL) - ) { - paymentMethods.find { it.method == PaymentMethod.MethodType.CARD }?.let { - onPaymentMethodSelected(it, byUser = false) - } - return isValidCurrencyForPay() - } else if (selectedCurrency == FiatCurrency.GBP && currentAlpha3Code != BANK_TRANSFER_UK_CODE) { - paymentMethods.find { it.method == PaymentMethod.MethodType.CARD }?.let { - onPaymentMethodSelected(it, byUser = false) - } - return isValidCurrencyForPay() - } + if (currentPaymentMethod.method != PaymentMethod.MethodType.BANK_TRANSFER) { + return true + } + + val isUsdSelected = + selectedCurrency == FiatCurrency.USD + val isUkWithEurSelected = + selectedCurrency == FiatCurrency.EUR && currentAlpha3Code == BANK_TRANSFER_UK_CODE + val isNotUkWithGbpSelected = + selectedCurrency == FiatCurrency.GBP && currentAlpha3Code != BANK_TRANSFER_UK_CODE + + if (isUsdSelected || isUkWithEurSelected || isNotUkWithGbpSelected) { + switchToCardPaymentMethod() + return isValidCurrencyForPay() } return true } + private fun switchToCardPaymentMethod() { + paymentMethods.find { it.method == PaymentMethod.MethodType.CARD } + ?.also { onPaymentMethodSelected(selectedMethod = it, byUser = false) } + } + override fun onFocusFieldChanged(focusField: FocusField) { isSwappedToToken = focusField == FocusField.TOKEN } diff --git a/app/src/main/java/org/p2p/wallet/send/ui/stub/SendUnavailableFragment.kt b/app/src/main/java/org/p2p/wallet/send/ui/stub/SendUnavailableFragment.kt index b1548c8954..7e037beef1 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/stub/SendUnavailableFragment.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/stub/SendUnavailableFragment.kt @@ -12,7 +12,7 @@ import org.p2p.core.utils.insets.systemAndIme import org.p2p.wallet.R import org.p2p.wallet.common.mvp.BaseFragment import org.p2p.wallet.databinding.FragmentSendUnavailableBinding -import org.p2p.wallet.moonpay.ui.new.NewBuyFragment +import org.p2p.wallet.moonpay.ui.new.BuyFragment import org.p2p.wallet.receive.ReceiveFragmentFactory import org.p2p.wallet.utils.args import org.p2p.wallet.utils.popBackStack @@ -41,7 +41,7 @@ class SendUnavailableFragment : BaseFragment(R.layout.fragment_send_unavailable) popBackStack() } buttonBuy.setOnClickListener { - replaceFragment(NewBuyFragment.create(token = defaultTokenToBuy)) + replaceFragment(BuyFragment.create(token = defaultTokenToBuy)) } buttonReceive.setOnClickListener { replaceFragment(receiveFragmentFactory.receiveFragment(token = null)) diff --git a/app/src/main/res/layout/fragment_buy.xml b/app/src/main/res/layout/fragment_buy.xml index 655a42e104..2dfdb5caae 100644 --- a/app/src/main/res/layout/fragment_buy.xml +++ b/app/src/main/res/layout/fragment_buy.xml @@ -1,37 +1,175 @@ - + android:layout_height="match_parent"> - + + + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/buttonBuy" + app:layout_constraintTop_toBottomOf="@id/toolbarBuy"> - + android:orientation="vertical"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + diff --git a/app/src/main/res/layout/fragment_new_buy.xml b/app/src/main/res/layout/fragment_new_buy.xml deleted file mode 100644 index 2dfdb5caae..0000000000 --- a/app/src/main/res/layout/fragment_new_buy.xml +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 29e5e8987c..53dac4e75e 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -40,7 +40,6 @@ apply { android { buildFeatures.buildConfig = true - } dependencies { From 150d644b69cb5c21df51386078a329a62c7fea74 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:49:52 +0300 Subject: [PATCH 11/51] PWN-877 - add PNL feature toggle (#2178) --- .../di/FeatureTogglesModule.kt | 7 +++++-- .../toggles/remote/PnlEnabledFeatureToggle.kt | 11 ++++++++++ .../history/ui/token/TokenHistoryPresenter.kt | 8 +++---- .../home/ui/crypto/MyCryptoPresenter.kt | 18 ++++++++-------- .../home/ui/crypto/mapper/MyCryptoMapper.kt | 4 ++-- .../main/java/org/p2p/wallet/pnl/PnlModule.kt | 3 ++- .../wallet/pnl/interactor/PnlDataObserver.kt | 21 ++++++++++--------- .../p2p/wallet/pnl/interactor/PnlDataState.kt | 8 +++---- .../pnl/repository/PnlRemoteRepository.kt | 20 ++++++++++-------- .../java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt | 4 ++-- 10 files changed, 61 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/PnlEnabledFeatureToggle.kt diff --git a/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt b/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt index b2c1862a26..5f30dd6628 100644 --- a/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt +++ b/app/src/main/java/org/p2p/wallet/common/feature_toggles/di/FeatureTogglesModule.kt @@ -17,6 +17,7 @@ import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationDe import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationFrequencyFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.NetworkObservationPercentFeatureToggle +import org.p2p.wallet.common.feature_toggles.toggles.remote.PnlEnabledFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.ReferralProgramEnabledFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.RegisterUsernameEnabledFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.RegisterUsernameSkipEnabledFeatureToggle @@ -38,7 +39,7 @@ object FeatureTogglesModule : InjectionModule { singleOf(::FeatureTogglesValuesSource) bind RemoteConfigValuesProvider::class factory { - setOf( + listOf( get(), get(), get(), @@ -54,7 +55,8 @@ object FeatureTogglesModule : InjectionModule { get(), get(), get(), - ).toList() + get(), + ) } factoryOf(::SslPinningFeatureToggle) @@ -75,6 +77,7 @@ object FeatureTogglesModule : InjectionModule { factoryOf(::SwapRoutesValidationEnabledFeatureToggle) factoryOf(::StrigaSignupEnabledFeatureToggle) factoryOf(::ReferralProgramEnabledFeatureToggle) + factoryOf(::PnlEnabledFeatureToggle) singleOf(::FeatureToggleProvider) } diff --git a/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/PnlEnabledFeatureToggle.kt b/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/PnlEnabledFeatureToggle.kt new file mode 100644 index 0000000000..5f1b5b312d --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/common/feature_toggles/toggles/remote/PnlEnabledFeatureToggle.kt @@ -0,0 +1,11 @@ +package org.p2p.wallet.common.feature_toggles.toggles.remote + +import org.p2p.wallet.common.feature_toggles.remote_config.RemoteConfigValuesProvider + +class PnlEnabledFeatureToggle( + valuesProvider: RemoteConfigValuesProvider +) : BooleanFeatureToggle(valuesProvider) { + override val featureKey: String = "pnl_enabled" + override val featureDescription: String = "Enables PNL calculation" + override val defaultValue: Boolean = false +} diff --git a/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryPresenter.kt b/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryPresenter.kt index d0086aa839..0bb2928c6a 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/token/TokenHistoryPresenter.kt @@ -55,7 +55,7 @@ class TokenHistoryPresenter( } private fun observePnlData() { - pnlInteractor.state + pnlInteractor.pnlState .onEach { updateTokenAmounts(this.token) } .launchIn(this) } @@ -87,14 +87,14 @@ class TokenHistoryPresenter( view?.renderTokenPnl( pnlUiMapper.mapTokenBalancePnl( tokenMint = token.mintAddressB58, - pnlDataState = pnlInteractor.state.value + pnlDataState = pnlInteractor.pnlState.value ) ) } override fun onTokenPnlClicked() { - if (pnlInteractor.state.value.isResult()) { - pnlInteractor.state.value.findForToken(token.mintAddressB58)?.let { + if (pnlInteractor.pnlState.value.isLoaded()) { + pnlInteractor.pnlState.value.findForToken(token.mintAddressB58)?.let { view?.showPnlDetails(it.percent) } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt index 1346f66be7..0ca88ec0de 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt @@ -49,7 +49,7 @@ class MyCryptoPresenter( private val usernameInteractor: UsernameInteractor, private val tokenKeyProvider: TokenKeyProvider, private val mainScreenAnalytics: MainScreenAnalytics, - private val pnlInteractor: PnlDataObserver, + private val pnlDataObserver: PnlDataObserver, private val pnlUiMapper: PnlUiMapper, ) : BasePresenter(), MyCryptoContract.Presenter { @@ -87,8 +87,8 @@ class MyCryptoPresenter( // pnl must restart it's 5 minutes timer after force refresh // and we should restart it only if it was initially started // other cases might indicate that we didn't start observer due to empty state - if (pnlInteractor.isStarted()) { - pnlInteractor.restartAndRefresh() + if (pnlDataObserver.isStarted()) { + pnlDataObserver.restartAndRefresh() } } catch (cancelled: CancellationException) { Timber.i("Loading tokens job cancelled") @@ -119,7 +119,7 @@ class MyCryptoPresenter( private fun observePnlData() { pnlDataSubscription?.cancel() pnlDataSubscription = launch { - pnlInteractor.state + pnlDataObserver.pnlState .onEach { handleTokenState(tokenServiceCoordinator.observeLastState().value) } @@ -142,12 +142,12 @@ class MyCryptoPresenter( is UserTokensState.Loaded -> { view?.showEmptyState(isEmpty = false) - if (!pnlInteractor.isStarted()) { - pnlInteractor.start() + if (!pnlDataObserver.isStarted()) { + pnlDataObserver.start() } showTokensAndBalance( - pnlDataState = pnlInteractor.state.value, + pnlDataState = pnlDataObserver.pnlState.value, // separated screens logic: solTokens = filterCryptoTokens(newState.solTokens), solTokens = newState.solTokens, ethTokens = newState.ethTokens @@ -186,8 +186,8 @@ class MyCryptoPresenter( } override fun onBalancePnlClicked() { - if (pnlInteractor.state.value.isResult()) { - view?.showPnlDetails(pnlInteractor.state.value.toResultOrNull()!!.total.percent) + if (pnlDataObserver.pnlState.value.isLoaded()) { + view?.showPnlDetails(pnlDataObserver.pnlState.value.toLoadedOrNull()!!.total.percent) } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt index 0fa45b9093..2448c81350 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/mapper/MyCryptoMapper.kt @@ -71,7 +71,7 @@ class MyCryptoMapper( result += visibleTokens.map { it.mapToCellModel( isZerosHidden = isZerosHidden, - pnlTokenData = pnlDataState.toResultOrNull()?.findForToken(it.mintAddressB58) + pnlTokenData = pnlDataState.toLoadedOrNull()?.findForToken(it.mintAddressB58) ) } @@ -85,7 +85,7 @@ class MyCryptoMapper( result += hiddenTokens.map { it.mapToCellModel( isZerosHidden = isZerosHidden, - pnlTokenData = pnlDataState.toResultOrNull()?.findForToken(it.mintAddressB58) + pnlTokenData = pnlDataState.toLoadedOrNull()?.findForToken(it.mintAddressB58) ) } } diff --git a/app/src/main/java/org/p2p/wallet/pnl/PnlModule.kt b/app/src/main/java/org/p2p/wallet/pnl/PnlModule.kt index c0fec65bfb..90f7496ebc 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/PnlModule.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/PnlModule.kt @@ -33,7 +33,8 @@ object PnlModule : InjectionModule { factory { val api = get(named(RETROFIT_QUALIFIER)).create(PnlServiceApi::class.java) PnlRemoteRepository( - api = api + api = api, + dispatchers = get() ) } bind PnlRepository::class diff --git a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt index a7da3f03ea..72b935534e 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataObserver.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.p2p.core.common.di.AppScope import org.p2p.core.crypto.Base58String +import org.p2p.wallet.common.feature_toggles.toggles.remote.PnlEnabledFeatureToggle import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.pnl.repository.PnlRepository import org.p2p.wallet.tokenservice.TokenServiceCoordinator @@ -43,6 +44,7 @@ class PnlDataObserver( private val pnlRepository: PnlRepository, private val tokenKeyProvider: TokenKeyProvider, private val tokenServiceCoordinator: TokenServiceCoordinator, + private val pnlEnabledFt: PnlEnabledFeatureToggle ) { private companion object { @@ -52,14 +54,14 @@ class PnlDataObserver( private var updaterJob: Job? = null private val tokenMints = mutableListOf() private val tokenMintsMutex = Mutex() - private val _state = MutableStateFlow(PnlDataState.Idle) - val state = _state.asStateFlow() + private val _pnlState = MutableStateFlow(PnlDataState.Idle) + val pnlState = _pnlState.asStateFlow() fun isStarted(): Boolean = updaterJob != null fun start() { - if (updaterJob != null) return - _state.value = PnlDataState.Loading + if (!pnlEnabledFt.isFeatureEnabled || isStarted()) return + _pnlState.value = PnlDataState.Loading launchJob() } @@ -74,6 +76,8 @@ class PnlDataObserver( } private fun launchJob() { + if (!pnlEnabledFt.isFeatureEnabled) return + updaterJob = appScope.launch { // firstly, get current state, because observer may return actual tokens later tokenServiceCoordinator.observeLastState().value.extractTokenMints() @@ -85,14 +89,11 @@ class PnlDataObserver( while (isActive) { try { - _state.emit( - PnlDataState.Result( - pnlRepository.getPnlData(tokenKeyProvider.publicKeyBase58, tokenMints) - ) - ) + val pnlData = pnlRepository.getPnlData(tokenKeyProvider.publicKeyBase58, tokenMints) + _pnlState.emit(PnlDataState.Loaded(pnlData)) } catch (e: Throwable) { Timber.e(e, "Unable to get pnl data") - _state.emit( + _pnlState.emit( PnlDataState.Error(e) ) } diff --git a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt index 71d9110e23..8c064ff83e 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/interactor/PnlDataState.kt @@ -7,11 +7,11 @@ import org.p2p.wallet.pnl.models.PnlTokenData sealed class PnlDataState { object Idle : PnlDataState() object Loading : PnlDataState() - data class Result(val data: PnlData) : PnlDataState() + data class Loaded(val data: PnlData) : PnlDataState() data class Error(val error: Throwable) : PnlDataState() - fun toResultOrNull(): PnlData? = (this as? Result)?.data - fun isResult(): Boolean = this is Result + fun toLoadedOrNull(): PnlData? = (this as? Loaded)?.data + fun isLoaded(): Boolean = this is Loaded - fun findForToken(tokenMint: Base58String): PnlTokenData? = toResultOrNull()?.findForToken(tokenMint) + fun findForToken(tokenMint: Base58String): PnlTokenData? = toLoadedOrNull()?.findForToken(tokenMint) } diff --git a/app/src/main/java/org/p2p/wallet/pnl/repository/PnlRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/pnl/repository/PnlRemoteRepository.kt index d5b56386e7..340b57bac1 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/repository/PnlRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/repository/PnlRemoteRepository.kt @@ -1,8 +1,8 @@ package org.p2p.wallet.pnl.repository -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.p2p.core.crypto.Base58String +import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.network.gson.GsonProvider import org.p2p.solanaj.model.types.RpcMapRequest import org.p2p.wallet.pnl.api.PnlDataTimeSpan @@ -12,25 +12,27 @@ import org.p2p.wallet.pnl.models.PnlData class PnlRemoteRepository( private val api: PnlServiceApi, + private val dispatchers: CoroutineDispatchers ) : PnlRepository { override suspend fun getPnlData( userWallet: Base58String, tokenMints: List, timeSpan: PnlDataTimeSpan, - ): PnlData = withContext(Dispatchers.IO) { + ): PnlData = withContext(dispatchers.io) { + val requestedTokenMints: List? = tokenMints + .map { it.base58Value } + .ifEmpty { null } + val params = buildMap { - put("user_wallet", userWallet.base58Value) + this["user_wallet"] = userWallet.base58Value // todo: backend decided to use null for a default duration // currently it's 24 hours, something may change in the future - put("since", timeSpan.sinceEpochSeconds) - val requestedTokenMints: List? = tokenMints - .map { it.base58Value } - .ifEmpty { null } - put("token_mints", requestedTokenMints) + this["since"] = timeSpan.sinceEpochSeconds + this["token_mints"] = requestedTokenMints } - val rpcRequest = RpcMapRequest("get_pnl", params) + val rpcRequest = RpcMapRequest(method = "get_pnl", params = params) val response = api.getAccountPnl(rpcRequest) val gson = GsonProvider() .withBuilder { diff --git a/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt b/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt index 5df6f56697..2b1004d175 100644 --- a/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt +++ b/app/src/main/java/org/p2p/wallet/pnl/ui/PnlUiMapper.kt @@ -23,7 +23,7 @@ class PnlUiMapper { fun mapBalancePnl(pnlDataState: PnlDataState): TextViewCellModel? { return when (pnlDataState) { - is PnlDataState.Result -> { + is PnlDataState.Loaded -> { TextViewCellModel.Raw( TextContainer(R.string.home_pnl_format, pnlDataState.data.total.percent) ) @@ -40,7 +40,7 @@ class PnlUiMapper { fun mapTokenBalancePnl(tokenMint: Base58String, pnlDataState: PnlDataState): TextViewCellModel? { return when (pnlDataState) { - is PnlDataState.Result -> { + is PnlDataState.Loaded -> { pnlDataState.findForToken(tokenMint)?.let { pnlTokenData -> TextViewCellModel.Raw( TextContainer(R.string.home_pnl_format, pnlTokenData.percent) From aac11f038666eaa67999f939eb00dfcc1cebe7f2 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:58:12 +0300 Subject: [PATCH 12/51] PWN-878 - migrate to SolScan (#2180) --- .../main/res/layout/dialog_history_transaction_details.xml | 2 +- app/src/main/res/values/history_strings.xml | 6 +++--- app/src/main/res/values/strings.xml | 4 ++-- core/src/main/res/values/environment_config.xml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/dialog_history_transaction_details.xml b/app/src/main/res/layout/dialog_history_transaction_details.xml index e7e3e546c1..6f99d71331 100644 --- a/app/src/main/res/layout/dialog_history_transaction_details.xml +++ b/app/src/main/res/layout/dialog_history_transaction_details.xml @@ -299,7 +299,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/buttonPrimary" - tools:text="Open SOLScan" /> + tools:text="Open Solscan" /> diff --git a/app/src/main/res/values/history_strings.xml b/app/src/main/res/values/history_strings.xml index 211eb50c49..10088e9347 100644 --- a/app/src/main/res/values/history_strings.xml +++ b/app/src/main/res/values/history_strings.xml @@ -27,7 +27,7 @@ Transaction %s Referral reward Done - Open SOLScan + Open Solscan submitted succeeded failed @@ -43,7 +43,7 @@ The transaction will be completed in a few seconds Share - Explorer + Solscan No balance change @@ -52,4 +52,4 @@ Sent via one-time link Unique one-time link works once only https://solscan.io/tx/%s - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 603aac7067..616933c6b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,7 +204,7 @@ Share You can not send tokens to yourself Transaction ID - View in Solana explorer + View in Solscan Success Transaction has been confirmed Wrong wallet address @@ -291,7 +291,7 @@ Tap to read Each token in this list is available for receiving with this address; you can search for a token by typing its name or ticker.\n\nIf a token is not on this list, we do not recommend sending it to this address. we do not recommend sending it to this address. - View in Solana explorer + View in Solscan Statuses received Choose the network Solana network diff --git a/core/src/main/res/values/environment_config.xml b/core/src/main/res/values/environment_config.xml index 5da1a3334a..91f0e64941 100644 --- a/core/src/main/res/values/environment_config.xml +++ b/core/src/main/res/values/environment_config.xml @@ -3,8 +3,8 @@ https://api.coingecko.com/api/v3/ https://btc.com/btc/address/%s - https://explorer.solana.com/tx/%s - https://explorer.solana.com/address/%s + https://solscan.io/tx/%s + https://solscan.io/account/%s https://raw.githubusercontent.com/p2p-org/token-list/main/src/tokens/ From 7d659089c759caf723898d13c57e6e95111b5fa4 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:32:01 +0300 Subject: [PATCH 13/51] PWN-839 - fix opening send on empty account (#2179) --- .../wallet/bridge/send/SendFragmentFactory.kt | 19 +++++++++++++++- .../ui/container/MainContainerContract.kt | 2 +- .../ui/container/MainContainerPresenter.kt | 22 +++++++++---------- .../wallet/home/ui/crypto/MyCryptoContract.kt | 2 -- .../wallet/home/ui/crypto/MyCryptoFragment.kt | 13 +++++------ .../home/ui/crypto/MyCryptoPresenter.kt | 6 +---- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/bridge/send/SendFragmentFactory.kt b/app/src/main/java/org/p2p/wallet/bridge/send/SendFragmentFactory.kt index 9cbefbbd91..447578e72f 100644 --- a/app/src/main/java/org/p2p/wallet/bridge/send/SendFragmentFactory.kt +++ b/app/src/main/java/org/p2p/wallet/bridge/send/SendFragmentFactory.kt @@ -7,8 +7,25 @@ import org.p2p.wallet.common.feature_toggles.toggles.remote.EthAddressEnabledFea import org.p2p.wallet.send.model.NetworkType import org.p2p.wallet.send.model.SearchResult import org.p2p.wallet.send.ui.NewSendFragment +import org.p2p.wallet.send.ui.SearchOpenedFromScreen +import org.p2p.wallet.send.ui.search.NewSearchFragment +import org.p2p.wallet.send.ui.stub.SendUnavailableFragment +import org.p2p.wallet.user.interactor.UserInteractor -class SendFragmentFactory(private val ethAddressEnabledFeatureToggle: EthAddressEnabledFeatureToggle) { +class SendFragmentFactory( + private val ethAddressEnabledFeatureToggle: EthAddressEnabledFeatureToggle, + private val userInteractor: UserInteractor +) { + + suspend fun searchFragment(): Fragment? { + val isAccountEmpty = userInteractor.getNonZeroUserTokens().isEmpty() + return if (isAccountEmpty) { + val token = userInteractor.getSingleTokenForBuy() ?: return null + SendUnavailableFragment.create(token) + } else { + NewSearchFragment.create(SearchOpenedFromScreen.MAIN) + } + } fun sendFragment(searchResult: SearchResult, initialToken: Token.Active?): Fragment { return if (ethAddressEnabledFeatureToggle.isFeatureEnabled && diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt index 6a183e5fb5..a2ee925604 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt @@ -31,6 +31,6 @@ interface MainContainerContract { fun logCryptoOpened() fun logHistoryOpened() fun logSettingsOpened() - fun onSendClicked() + fun handleSendDeeplink() } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt index c3821154dd..6fbbb75195 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.p2p.core.network.ConnectionManager +import org.p2p.core.token.Token import org.p2p.core.token.filterTokensForWalletScreen import org.p2p.uikit.components.ScreenTab import org.p2p.wallet.R @@ -89,7 +90,7 @@ class MainContainerPresenter( launchSupervisor { deeplinksManager.subscribeOnDeeplinks(supportedTargets) .onEach { view?.navigateToTabFromDeeplink(it) } - .onEach { deeplinkHandler.handle(it) } + .onEach(deeplinkHandler::handle) .launchIn(this) deeplinksManager.executeHomePendingDeeplink() @@ -98,10 +99,9 @@ class MainContainerPresenter( } override fun observeUserTokens() { - launch { - tokenServiceCoordinator.observeUserTokens() - .collect { handleTokenState(it) } - } + tokenServiceCoordinator.observeUserTokens() + .onEach(::handleTokenState) + .launchIn(this) } override fun logWalletOpened() { @@ -120,11 +120,11 @@ class MainContainerPresenter( mainScreenAnalytics.logMainScreenSettingsClick() } - override fun onSendClicked() { + override fun handleSendDeeplink() { mainScreenAnalytics.logMainScreenSendClick() launch { val userTokens = tokenServiceCoordinator.getUserTokens() - val isAccountEmpty = userTokens.isEmpty() || userTokens.all { it.isZero } + val isAccountEmpty = userTokens.isEmpty() || userTokens.all(Token.Active::isZero) if (isAccountEmpty) { val validTokenToBuy = userInteractor.getSingleTokenForBuy() ?: return@launch view?.navigateToSendNoTokens(validTokenToBuy) @@ -144,7 +144,7 @@ class MainContainerPresenter( private fun handleDeeplinkTarget(target: DeeplinkTarget) { when (target) { - DeeplinkTarget.SEND -> onSendClicked() + DeeplinkTarget.SEND -> handleSendDeeplink() else -> Timber.d("Unsupported deeplink target! $target") } } @@ -179,9 +179,9 @@ class MainContainerPresenter( is UserTokensState.Loaded -> { // todo: this new filter supposed to be used for new design val filteredTokens = newState.solTokens.filterTokensForWalletScreen() - val balance = filteredTokens.sumOf { it.total } - view?.showWalletBalance(balanceMapper.formatBalance(balance)) - view?.showWalletBalance("Wallet") + val balance = filteredTokens.sumOf(Token.Active::total) + view?.showWalletBalance(balance = balanceMapper.formatBalance(balance)) + view?.showWalletBalance(balance = "Wallet") view?.showCryptoBadgeVisible(isVisible = newState.ethTokens.isNotEmpty()) } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoContract.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoContract.kt index f45f228c60..75b5b5b8e1 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoContract.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoContract.kt @@ -25,7 +25,6 @@ interface MyCryptoContract { fun showEmptyState(isEmpty: Boolean) fun navigateToReceive() fun showAddMoneyDialog() - fun navigateToSend() fun navigateToSwap() fun navigateToSell() fun navigateToTokenClaim(token: Token.Eth) @@ -38,7 +37,6 @@ interface MyCryptoContract { fun onAmountClicked() fun onBuyClicked() fun onReceiveClicked() - fun onSendClicked() fun onAddressClicked() fun onSwapClicked() fun onSellClicked() diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt index 52f885385d..46c800f55f 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt @@ -3,6 +3,7 @@ package org.p2p.wallet.home.ui.crypto import androidx.annotation.StringRes import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.SimpleItemAnimator import android.content.Context import android.os.Bundle @@ -13,6 +14,7 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.inject import timber.log.Timber +import kotlinx.coroutines.launch import org.p2p.core.glide.GlideManager import org.p2p.core.token.Token import org.p2p.uikit.model.AnyCellItem @@ -47,8 +49,6 @@ import org.p2p.wallet.referral.banner.ReferralFragmentFactory import org.p2p.wallet.referral.banner.referralBannerDelegate import org.p2p.wallet.root.RootListener import org.p2p.wallet.sell.ui.payload.SellPayloadFragment -import org.p2p.wallet.send.ui.SearchOpenedFromScreen -import org.p2p.wallet.send.ui.search.NewSearchFragment import org.p2p.wallet.transaction.model.NewShowProgress import org.p2p.wallet.transaction.progresshandler.ClaimProgressHandler import org.p2p.wallet.utils.HomeScreenLayoutManager @@ -178,7 +178,9 @@ class MyCryptoFragment : presenter.onReceiveClicked() } ActionButton.SEND_BUTTON -> { - presenter.onSendClicked() + lifecycleScope.launch { + sendFragmentFactory.searchFragment()?.also { replaceFragment(it) } + } } ActionButton.SWAP_BUTTON -> { presenter.onSwapClicked() @@ -194,11 +196,6 @@ class MyCryptoFragment : onOffRampNavigator.navigateToAddMoney(this) } - override fun navigateToSend() { - val target = NewSearchFragment.create(SearchOpenedFromScreen.MAIN) - replaceFragment(target) - } - override fun showBalance(cellModel: TextViewCellModel?) { binding.viewBalance.textViewAmount.bindOrGone(cellModel) } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt index 0ca88ec0de..fcdfe4ab00 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt @@ -263,10 +263,6 @@ class MyCryptoPresenter( view?.navigateToReceive() } - override fun onSendClicked() { - view?.navigateToSend() - } - override fun onSwapClicked() { cryptoScreenAnalytics.logCryptoSwapClick() view?.navigateToSwap() @@ -279,7 +275,7 @@ class MyCryptoPresenter( override fun onClaimClicked(canBeClaimed: Boolean, token: Token.Eth) { cryptoScreenAnalytics.logCryptoClaimTransferedClicked() launch { - claimHandler.handle(view, canBeClaimed, token) + claimHandler.handle(view = view, canBeClaimed = canBeClaimed, token = token) } } From c0e0488a20b0579278ddc548e4930d0349920860 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:21:33 +0400 Subject: [PATCH 14/51] ETH-842 - fix displaying free fee; fix fee layout (#2181) --- app/src/main/res/layout/dialog_history_transaction_details.xml | 3 ++- core/src/main/java/org/p2p/core/utils/Constants.kt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/dialog_history_transaction_details.xml b/app/src/main/res/layout/dialog_history_transaction_details.xml index 6f99d71331..3809e21c58 100644 --- a/app/src/main/res/layout/dialog_history_transaction_details.xml +++ b/app/src/main/res/layout/dialog_history_transaction_details.xml @@ -225,6 +225,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginHorizontal="16dp" + app:layout_goneMarginTop="24dp" android:gravity="center_vertical" android:paddingBottom="28dp" android:text="@string/transaction_transaction_fee_title" @@ -247,7 +248,7 @@ android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/textViewFeeTitle" - app:layout_constraintTop_toBottomOf="@id/textViewSendReceiveTitle" + app:layout_constraintTop_toTopOf="@id/textViewFeeTitle" tools:visibility="visible" /> Date: Wed, 7 Feb 2024 13:43:13 +0300 Subject: [PATCH 15/51] PWN-897 - Add JS bridge for paste (#2182) --- app/src/main/AndroidManifest.xml | 1 + app/src/main/assets/referral_bridge_provider.js | 3 +++ .../main/java/org/p2p/wallet/referral/ReferralBridge.kt | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e0db33135a..249b0f18cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + Date: Wed, 7 Feb 2024 14:46:56 +0400 Subject: [PATCH 16/51] NO-TICKET - disable local debug app update (#2183) --- app/src/main/java/org/p2p/wallet/App.kt | 4 ++++ app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/app/src/main/java/org/p2p/wallet/App.kt b/app/src/main/java/org/p2p/wallet/App.kt index 006cd4ebe4..040fd1bfcd 100644 --- a/app/src/main/java/org/p2p/wallet/App.kt +++ b/app/src/main/java/org/p2p/wallet/App.kt @@ -31,6 +31,10 @@ import org.p2p.wallet.utils.SolanajTimberLogger import org.p2p.core.BuildConfig as CoreBuildConfig import org.p2p.wallet.BuildConfig as AppBuildConfig +@Suppress("KotlinConstantConditions") +val isLocalDebugBuild: Boolean + get() = AppBuildConfig.DEBUG && AppBuildConfig.BUILD_TYPE == "debug" + class App : Application(), Configuration.Provider { private val crashLoggerInitializer: CrashLoggerInitializer by inject() private val crashLogger: CrashLogger by inject() diff --git a/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt b/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt index 023d4a1317..1392337331 100644 --- a/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt +++ b/app/src/main/java/org/p2p/wallet/root/AppUpdateService.kt @@ -15,12 +15,15 @@ import org.p2p.wallet.BuildConfig import org.p2p.wallet.common.feature_toggles.toggles.remote.ForceUpdateVersionCodeFeatureToggle import org.p2p.wallet.common.feature_toggles.toggles.remote.ForceUpdateVersionCodeFeatureToggle.Companion.NOT_FETCHED import org.p2p.wallet.infrastructure.coroutines.waitForCondition +import org.p2p.wallet.isLocalDebugBuild class AppUpdateService( private val context: Context, private val forceUpdateVersionCodeFt: ForceUpdateVersionCodeFeatureToggle ) { private suspend fun isUpdateNeeded(): Boolean { + if (isLocalDebugBuild) return false + val isForceUpdateCodeFetched = waitForCondition(2.seconds.inWholeMilliseconds) { forceUpdateVersionCodeFt.value != NOT_FETCHED } @@ -70,6 +73,8 @@ class AppUpdateService( suspend fun checkUpdateIsNotStalled( activityResultLauncher: ActivityResultLauncher ) { + if (isLocalDebugBuild) return + val appUpdateManager = AppUpdateManagerFactory.create(context) val appUpdateInfo = appUpdateManager.requestAppUpdateInfo() val isStalled = appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS From bc0980227970b908099e4335230e7d6e91e14930 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:50:46 +0300 Subject: [PATCH 17/51] PWN-896 - Fix logout crash (#2184) --- .../wallet/home/ui/crypto/MyCryptoFragment.kt | 10 +++++++-- .../home/ui/crypto/MyCryptoPresenter.kt | 22 +++++++++---------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt index 46c800f55f..eba5d2e550 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoFragment.kt @@ -90,7 +90,7 @@ class MyCryptoFragment : } } }, - tokenButtonDelegate() { binding, _ -> + tokenButtonDelegate { binding, _ -> binding.root.setOnClickListener { onVisibilityToggleClicked() } }, ethClaimTokenDelegate(glideManager) { binding, item -> @@ -243,7 +243,13 @@ class MyCryptoFragment : override fun showItems(items: List) { cellAdapter.setItems(items) { - binding.recyclerViewCrypto.invalidateItemDecorations() + // very stupid check but + // when user logouts, data layer emits new cell items and update the list + // async stuff is going inside when items are set and when this callback is called + // view is already null because we navigated to auth screen + if (view != null) { + binding.recyclerViewCrypto.invalidateItemDecorations() + } } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt index fcdfe4ab00..f561f66621 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt @@ -110,21 +110,16 @@ class MyCryptoPresenter( private fun observeCryptoTokens() { cryptoTokensSubscription?.cancel() - cryptoTokensSubscription = launch { - tokenServiceCoordinator.observeUserTokens() - .collect { handleTokenState(it) } - } + cryptoTokensSubscription = tokenServiceCoordinator.observeUserTokens() + .onEach { handleTokenState(it) } + .launchIn(this) } private fun observePnlData() { pnlDataSubscription?.cancel() - pnlDataSubscription = launch { - pnlDataObserver.pnlState - .onEach { - handleTokenState(tokenServiceCoordinator.observeLastState().value) - } - .launchIn(this) - } + pnlDataSubscription = pnlDataObserver.pnlState + .onEach { handleTokenState(tokenServiceCoordinator.observeLastState().value) } + .launchIn(this) } private fun handleTokenState(newState: UserTokensState) { @@ -281,6 +276,11 @@ class MyCryptoPresenter( override fun detach() { sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener) + cryptoTokensSubscription?.cancel() + cryptoTokensSubscription = null + + pnlDataSubscription?.cancel() + pnlDataSubscription = null super.detach() } From 01c56032db88f1dd1d681483af38e0c281111636 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:01:18 +0300 Subject: [PATCH 18/51] PWN-880 PWN-925 - add swap fee and fix selecting tokens (#2185) --- .../response/SwapJupiterV6QuoteResponse.kt | 3 +- .../repository/model/JupiterSwapRouteV6.kt | 2 + .../v6/JupiterSwapRoutesV6Mapper.kt | 32 ++++++++++++++-- .../token_selector/CommonSwapTokenSelector.kt | 20 +++++++--- .../presenter/SwapContentSettingsMapper.kt | 8 +++- .../settings/presenter/SwapFeeCellsBuilder.kt | 38 ++++++++++++++++++- .../main/res/xml/network_security_config.xml | 2 +- .../jupiter/ui/main/JupiterSwapTestHelpers.kt | 4 +- 8 files changed, 94 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/api/response/SwapJupiterV6QuoteResponse.kt b/app/src/main/java/org/p2p/wallet/jupiter/api/response/SwapJupiterV6QuoteResponse.kt index 918fa4ed54..f41d2ef0b2 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/api/response/SwapJupiterV6QuoteResponse.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/api/response/SwapJupiterV6QuoteResponse.kt @@ -2,6 +2,7 @@ package org.p2p.wallet.jupiter.api.response import com.google.gson.JsonObject import com.google.gson.annotations.SerializedName +import java.math.BigDecimal data class SwapJupiterV6QuoteResponse( @SerializedName("inputMint") @@ -35,7 +36,7 @@ data class SwapJupiterV6QuoteResponse( @SerializedName("amount") val amount: String, @SerializedName("feeBps") - val feeBps: Int + val feeBps: BigDecimal ) data class RoutePlanResponse( diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt index f9e0b42bce..a8b840c40a 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt @@ -39,4 +39,6 @@ data class SwapKeyAppFees( val ataDeposits: BigInteger, val totalFeeAndDeposits: BigInteger, val minimumSolForTransaction: BigInteger, + val platformFeeTokenB: BigInteger, + val platformFeePercent: BigDecimal, ) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt index dcf0d5ed86..0d26e1c80e 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt @@ -2,6 +2,8 @@ package org.p2p.wallet.jupiter.repository.v6 import com.google.gson.Gson import org.json.JSONObject +import java.math.BigDecimal +import java.math.BigInteger import org.p2p.core.crypto.toBase58Instance import org.p2p.core.utils.orZero import org.p2p.core.utils.toJsonObject @@ -30,14 +32,23 @@ class JupiterSwapRoutesV6Mapper( percent = it.percent.toString() ) } + val feesJson = JSONObject(response.keyAppFees.feeDetails.toString()) + + // feeBps=20 == 2% + // feeBps=200 == 20% + // feeBps=100 == 10% + // feeBps=2 == 0.2% + val platformFeePercent = getPlatformFeePercent(response.platformFee) + val platformFeeAmount = response.platformFee?.amount?.toBigInteger().orZero() + + val ataDeposits = getAtaDepositsSum(feesJson) val fees = SwapKeyAppFees( totalFees = response.keyAppFees.fee.toBigInteger(), signatureFee = feesJson.optLong("signatureFee").toBigInteger(), - ataDeposits = feesJson.optJSONArray("ataDeposits") - ?.mapAsStrings { it.toBigInteger() } - ?.sumOf { it } - .orZero(), + ataDeposits = ataDeposits, + platformFeeTokenB = platformFeeAmount, + platformFeePercent = platformFeePercent, totalFeeAndDeposits = feesJson.optLong("totalFeeAndDeposits").toBigInteger(), minimumSolForTransaction = feesJson.optLong("minimumSOLForTransaction").toBigInteger(), ) @@ -54,4 +65,17 @@ class JupiterSwapRoutesV6Mapper( fees = fees ) } + + private fun getAtaDepositsSum(feesJson: JSONObject): BigInteger { + return feesJson.optJSONArray("ataDeposits") + ?.mapAsStrings { it.toBigInteger() } + ?.sumOf { it } + .orZero() + } + + private fun getPlatformFeePercent(fee: SwapJupiterV6QuoteResponse.PlatformFeeResponse?): BigDecimal { + return fee?.feeBps + ?.divide(100.toBigDecimal()) + .orZero() + } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/token_selector/CommonSwapTokenSelector.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/token_selector/CommonSwapTokenSelector.kt index 7ec5b5eea8..5c2d2cb3a2 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/token_selector/CommonSwapTokenSelector.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/token_selector/CommonSwapTokenSelector.kt @@ -8,6 +8,7 @@ import org.p2p.core.token.Token import org.p2p.core.token.findSolOrThrow import org.p2p.wallet.infrastructure.swap.JupiterSwapStorageContract import org.p2p.wallet.jupiter.interactor.model.SwapTokenModel +import org.p2p.wallet.jupiter.repository.model.JupiterSwapToken import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository import org.p2p.wallet.tokenservice.TokenServiceCoordinator @@ -22,6 +23,7 @@ class CommonSwapTokenSelector( val userTokensJob = async { tokenServiceCoordinator.getUserTokens() } val userTokens = userTokensJob.await() + val userIsEmpty = userTokens.isEmpty() || userTokens.all { it.isZero } val userHasUsdc = userTokens.any { it.isUSDC } val userHasSol = userTokens.any { it.isSOL && !it.isZero } val userTokensContainsJupiter = jupiterTokensRepository.filterIntersectedTokens(userTokens) @@ -38,6 +40,11 @@ class CommonSwapTokenSelector( tokenA = findTokenByMint(userTokens, savedSelectedTokenMintA) tokenB = findTokenByMint(userTokens, savedSelectedTokenMintB) } + userIsEmpty -> { + val jupiterUsdcToSol = jupiterUsdcToJupiterSol() + tokenA = SwapTokenModel.JupiterToken(jupiterUsdcToSol.first) + tokenB = SwapTokenModel.JupiterToken(jupiterUsdcToSol.second) + } userHasUsdc -> { tokenA = SwapTokenModel.UserToken(userTokens.first { it.isUSDC }) tokenB = getTokenB( @@ -53,7 +60,7 @@ class CommonSwapTokenSelector( jupiterTokensRepository = jupiterTokensRepository, userTokens = userTokens, preferSol = false, - savedSwapTokenB = savedSelectedTokenMintB + savedSwapTokenB = savedSelectedTokenMintB.takeIf { it != tokenA.mintAddress } ) } userTokenWithMaxAmount != null -> { @@ -67,10 +74,9 @@ class CommonSwapTokenSelector( } // no any tokens else -> { - val jupiterUsdc = jupiterTokensRepository.requireUsdc() - val jupiterSol = jupiterTokensRepository.requireWrappedSol() - tokenA = SwapTokenModel.JupiterToken(jupiterUsdc) - tokenB = SwapTokenModel.JupiterToken(jupiterSol) + val jupiterUsdcToSol = jupiterUsdcToJupiterSol() + tokenA = SwapTokenModel.JupiterToken(jupiterUsdcToSol.first) + tokenB = SwapTokenModel.JupiterToken(jupiterUsdcToSol.second) } } @@ -79,6 +85,10 @@ class CommonSwapTokenSelector( tokenA to tokenB } + private suspend fun jupiterUsdcToJupiterSol(): Pair { + return jupiterTokensRepository.requireUsdc() to jupiterTokensRepository.requireWrappedSol() + } + private suspend fun findTokenByMint( userTokens: List, tokenMint: Base58String diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt index 43862a88ec..b51bd6a78f 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt @@ -123,11 +123,17 @@ class SwapContentSettingsMapper( this += liquidityFee.cellModel } + val keyAppFee = swapFeeBuilder.buildKeyAppFee(route, tokenB) + if (keyAppFee != null) { + this += keyAppFee.cellModel + } + val estimatedFee = swapFeeBuilder.buildEstimatedFeeString( networkFees = networkFeeCell, accountFee = accountFee, liquidityFees = liquidityFee, - token2022Fee = token2022Fee + token2022Fee = token2022Fee, + keyAppFee = keyAppFee ) if (estimatedFee != null) { this += estimatedFee diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt index d6a0c1321b..df0a566771 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.supervisorScope import org.p2p.core.common.DrawableContainer import org.p2p.core.common.TextContainer import org.p2p.core.utils.asUsdSwap +import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.fromLamports @@ -90,9 +91,7 @@ class SwapFeeCellsBuilder( } val tokenBRate: BigDecimal? = loadRateForToken(tokenB)?.rate - val feeUsd = tokenBRate?.let { ataFee.multiply(it) } - val formattedFeeAmount = ataFee.formatTokenWithSymbol(tokenB.tokenSymbol, tokenB.decimals) val cellModel = MainCellModel( @@ -193,11 +192,44 @@ class SwapFeeCellsBuilder( } } + suspend fun buildKeyAppFee(activeRoute: JupiterSwapRouteV6, tokenB: SwapTokenModel): SwapSettingsFeeBox? { + val platformFee = activeRoute.fees.platformFeeTokenB + if (platformFee.isZero()) { + return null + } + + val tokenBRate = loadRateForToken(tokenB)?.rate + + val platformFeeUsd = activeRoute.fees.platformFeeTokenB + .fromLamports(tokenB.decimals) + .multiply(tokenBRate.orZero()) + + val formattedPlatformFee = "${activeRoute.fees.platformFeePercent.formatFiat()} %" + + val cellModel = MainCellModel( + leftSideCellModel = LeftSideCellModel.IconWithText( + firstLineText = TextViewCellModel.Raw( + text = TextContainer("KeyApp Fee"), + ), + secondLineText = TextViewCellModel.Raw( + text = TextContainer(formattedPlatformFee) + ), + ), + rightSideCellModel = null, + styleType = MainCellStyle.BASE_CELL, + ) + return SwapSettingsFeeBox( + cellModel = cellModel, + feeInUsd = platformFeeUsd + ) + } + fun buildEstimatedFeeString( networkFees: SwapSettingsFeeBox, accountFee: SwapSettingsFeeBox?, liquidityFees: SwapSettingsFeeBox?, token2022Fee: SwapSettingsFeeBox?, + keyAppFee: SwapSettingsFeeBox?, ): MainCellModel? { if (liquidityFees?.feeInUsd == null) { return null @@ -206,7 +238,9 @@ class SwapFeeCellsBuilder( val totalFee: String = liquidityFees.feeInUsd .plus(accountFee?.feeInUsd.orZero()) .plus(token2022Fee?.feeInUsd.orZero()) + .plus(keyAppFee?.feeInUsd.orZero()) .asUsdSwap() + return MainCellModel( leftSideCellModel = LeftSideCellModel.IconWithText( firstLineText = TextViewCellModel.Raw( diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index 464b038548..bad695c4a4 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -13,4 +13,4 @@ beardict.net:5051 beardict.net - \ No newline at end of file + diff --git a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt index 5fda7cc9b0..daf559a8a8 100644 --- a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt +++ b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt @@ -198,7 +198,9 @@ object JupiterSwapTestHelpers { ataDeposits = BigInteger.ZERO, totalFeeAndDeposits = BigInteger.ZERO, minimumSolForTransaction = BigInteger.ZERO, - totalFees = BigInteger.ZERO + totalFees = BigInteger.ZERO, + platformFeePercent = BigDecimal.ZERO, + platformFeeTokenB = BigInteger.ZERO ) ) } From 6c30c145ef64968a4fdb7f55c7e6ced971129971 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Thu, 8 Feb 2024 13:01:03 +0400 Subject: [PATCH 19/51] ETH-840 - fix fee payer selection for non-compensating tokens (#2186) --- .../main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt | 4 +--- .../main/java/org/p2p/wallet/send/model/SendSolanaFee.kt | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) 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 b44308837b..cb4765be41 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -325,15 +325,13 @@ class SendFeeRelayerManager( return FeeCalculationState.NoFees } - val feePayerTokensMints = sendServiceRepository.getCompensationTokens() - val poolsStateFees = getFeesInPayingTokenUseCase.execute( feePayerToken = feePayerToken, transactionFeeInSol = fees.transactionFee, accountCreationFeeInSol = fees.accountCreationFee ) - if (poolsStateFees != null && feePayerToken.mintAddressB58 in feePayerTokensMints) { + if (poolsStateFees != null) { FeeCalculationState.Success( fee = FeeRelayerFee( feeInSol = fees, diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt index 62e07a434f..c9e9744e06 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt @@ -148,10 +148,6 @@ data class SendSolanaFee constructor( fun isEnoughSolBalance() = solToken?.let { !it.totalInLamports.isLessThan(feeRelayerFee.totalInSol) } ?: false - fun firstAlternativeTokenOrNull(): Token.Active? { - return alternativeFeePayerTokens.firstOrNull() - } - fun calculateFeePayerState( strategy: FeePayerSelectionStrategy, sourceTokenTotal: BigInteger, @@ -188,7 +184,7 @@ data class SendSolanaFee constructor( SwitchToSpl(sourceToken) } hasAlternativeFeePayerTokens -> { - SwitchToSpl(alternativeFeePayerTokens.first()) + 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) -> { From 7b673c5284cc78ec694296d8758c8aeb0cde807e Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Thu, 8 Feb 2024 12:04:26 +0300 Subject: [PATCH 20/51] PWN-897 - Fix webview paste button (#2187) --- app/src/main/assets/referral_bridge_provider.js | 2 +- app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/assets/referral_bridge_provider.js b/app/src/main/assets/referral_bridge_provider.js index e66166a24c..ffebf5fc27 100644 --- a/app/src/main/assets/referral_bridge_provider.js +++ b/app/src/main/assets/referral_bridge_provider.js @@ -13,7 +13,7 @@ window.ReferralBridge = { AndroidReferralBridge.showShareDialog(link); }, getClipboardContent: function() { - return AndroidReferralBridge.getClipboardValue(); + return JSON.parse(AndroidReferralBridge.getClipboardValue()); }, signMessageAsync: async function(message) { const result = AndroidReferralBridge.signMessageAsync(message); diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 6ea3f6304c..177b5e063b 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -18,6 +18,9 @@ import org.p2p.wallet.utils.getClipboardText private typealias JsResultWrapper = String +/** + * @see [assets/referral_bridge_provider.js] + */ class ReferralWebViewBridge( webView: WebView, private val tokenKeyProvider: TokenKeyProvider, @@ -131,7 +134,7 @@ class ReferralWebViewBridge( @JavascriptInterface fun getClipboardValue(): JsResultWrapper { - val context = referralWebView?.context ?: return wrapInJsResult("No Android context found") + val context = referralWebView?.context ?: return wrapInJsResult(Exception("No Android context found")) return wrapInJsResult(context.getClipboardText(trimmed = true).orEmpty()) } From 1f9552615342520623e8414f62edb273dc38202f Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:48:20 +0300 Subject: [PATCH 21/51] PWN-933 - Focus Referral webview on resume (#2189) --- app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt | 2 ++ app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 177b5e063b..1d61bff3e7 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -40,6 +40,7 @@ class ReferralWebViewBridge( override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) // inject JS provider so WebView can work with our JS interface + // should be provided again on reload if (!isJsProviderInjected) { val jsProvider = view.resources.assets.open("referral_bridge_provider.js") .bufferedReader() @@ -90,6 +91,7 @@ class ReferralWebViewBridge( fun onFragmentResumed() { referralWebView?.onResume() + referralWebView?.requestFocus() } fun onFragmentPaused() { diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt index 9727230059..480843aadf 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt @@ -41,8 +41,8 @@ class ReferralFragment : } override fun onResume() { - jsBridge.onFragmentResumed() super.onResume() + jsBridge.onFragmentResumed() } override fun onPause() { From bdce0fdac847a83678d2111788d55f0e2de7a165 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:45:03 +0300 Subject: [PATCH 22/51] PWN-880 - Fix swap fee calculations (#2188) * PWN-880 - fix ATA fees and transfer fees calculations * PWN-880 - fix percent for getPlatformFeePercent * PWN-880 - fix BPS calculation * PWN-880 - comment fixes --- .../p2p/wallet/jupiter/JupiterSwapModule.kt | 8 +++--- .../repository/model/JupiterSwapRouteV6.kt | 6 ++--- .../repository/tokens/db/SwapTokensDao.kt | 2 +- .../v6/JupiterSwapRoutesRemoteV6Repository.kt | 2 +- ...=> JupiterSwapRoutesRepositoryV6Mapper.kt} | 9 +++---- .../statemanager/SwapStateRoutesRefresher.kt | 4 +-- ...or.kt => SwapMinimumSolAmountValidator.kt} | 2 +- .../presenter/JupiterSwapSettingsPresenter.kt | 9 ++++--- .../presenter/SwapContentSettingsMapper.kt | 2 +- .../presenter/SwapEmptySettingsMapper.kt | 4 ++- .../settings/presenter/SwapFeeCellsBuilder.kt | 26 +++++++++++++----- .../presenter/SwapLoadingSettingsMapper.kt | 5 ++-- .../CalculateToken2022TransferFeeUseCase.kt | 2 +- .../ui/main/JupiterSwapPresenterBaseTest.kt | 4 +-- .../jupiter/ui/main/JupiterSwapTestHelpers.kt | 2 +- .../org/p2p/core/network/gson/GsonProvider.kt | 3 --- .../p2p/core/network/gson/GsonTypeAdapters.kt | 23 ---------------- .../org/p2p/core/utils/AmountExtensions.kt | 1 + .../main/java/org/p2p/core/utils/MathUtils.kt | 7 +++-- .../solanaj/kits/AccountInfoTokenExtension.kt | 27 +++++++++++++------ 20 files changed, 76 insertions(+), 72 deletions(-) rename app/src/main/java/org/p2p/wallet/jupiter/repository/v6/{JupiterSwapRoutesV6Mapper.kt => JupiterSwapRoutesRepositoryV6Mapper.kt} (94%) rename app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/{MinimumSolAmountValidator.kt => SwapMinimumSolAmountValidator.kt} (98%) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt b/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt index f8901d764b..cbd0948db0 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt @@ -30,7 +30,7 @@ import org.p2p.wallet.jupiter.repository.transaction.JupiterSwapTransactionMappe import org.p2p.wallet.jupiter.repository.transaction.JupiterSwapTransactionRemoteRepository import org.p2p.wallet.jupiter.repository.transaction.JupiterSwapTransactionRepository import org.p2p.wallet.jupiter.repository.v6.JupiterSwapRoutesRemoteV6Repository -import org.p2p.wallet.jupiter.repository.v6.JupiterSwapRoutesV6Mapper +import org.p2p.wallet.jupiter.repository.v6.JupiterSwapRoutesRepositoryV6Mapper import org.p2p.wallet.jupiter.repository.v6.JupiterSwapRoutesV6Repository import org.p2p.wallet.jupiter.statemanager.SwapCoroutineScope import org.p2p.wallet.jupiter.statemanager.SwapProfiler @@ -49,7 +49,7 @@ import org.p2p.wallet.jupiter.statemanager.handler.SwapStateTokenAZeroHandler import org.p2p.wallet.jupiter.statemanager.rate.SwapRateTickerManager import org.p2p.wallet.jupiter.statemanager.token_selector.SwapInitialTokenSelector import org.p2p.wallet.jupiter.statemanager.token_selector.SwapInitialTokensData -import org.p2p.wallet.jupiter.statemanager.validator.MinimumSolAmountValidator +import org.p2p.wallet.jupiter.statemanager.validator.SwapMinimumSolAmountValidator import org.p2p.wallet.jupiter.statemanager.validator.SwapValidator import org.p2p.wallet.jupiter.ui.info.SwapInfoMapper import org.p2p.wallet.jupiter.ui.main.JupiterSwapContract @@ -106,7 +106,7 @@ object JupiterSwapModule : InjectionModule { factoryOf(::JupiterSwapTransactionRpcErrorMapper) factoryOf(::JupiterSwapInteractor) factoryOf(::SwapUserTokensChangeHandler) - factoryOf(::MinimumSolAmountValidator) + factoryOf(::SwapMinimumSolAmountValidator) factoryOf(::SwapValidator) factoryOf(::SwapStateRoutesRefresher) factoryOf(::SwapWidgetMapper) @@ -155,7 +155,7 @@ object JupiterSwapModule : InjectionModule { private fun Module.initV6Api() { factory { get(named(JUPITER_RETROFIT_V6_QUALIFIER)).create() } factoryOf(::JupiterSwapRoutesRemoteV6Repository) bind JupiterSwapRoutesV6Repository::class - factoryOf(::JupiterSwapRoutesV6Mapper) + factoryOf(::JupiterSwapRoutesRepositoryV6Mapper) } private fun Module.initJupiterSwapStateManager() { diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt index a8b840c40a..0d97d7835b 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt @@ -32,11 +32,11 @@ data class JupiterSwapRoutePlanV6( ) data class SwapKeyAppFees( - // maybe totalFees, maybe else, - // I don't know what fees, no doc at the moment + // totalFeeAndDeposits + transfer fee if exists + some other fee val totalFees: BigInteger, val signatureFee: BigInteger, - val ataDeposits: BigInteger, + val ataDepositsInSol: BigInteger, + // signatureFee + ataDeposits + minimumSolForTransaction val totalFeeAndDeposits: BigInteger, val minimumSolForTransaction: BigInteger, val platformFeeTokenB: BigInteger, diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt index f54905988f..a2c86c579b 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt @@ -35,7 +35,7 @@ interface SwapTokensDao { suspend fun findTokensByMints(mints: Set): List // todo: should be done with pagination - @Query("SELECT * from swap_tokens") + @Query("SELECT * from swap_tokens LIMIT 150") suspend fun getAllSwapTokens(): List @Query("SELECT COUNT(*) from swap_tokens") diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRemoteV6Repository.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRemoteV6Repository.kt index 5413c36f39..e84ffacf04 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRemoteV6Repository.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRemoteV6Repository.kt @@ -16,7 +16,7 @@ import org.p2p.wallet.utils.retryOnException class JupiterSwapRoutesRemoteV6Repository( private val apiV6: SwapJupiterV6Api, private val dispatchers: CoroutineDispatchers, - private val mapper: JupiterSwapRoutesV6Mapper, + private val mapper: JupiterSwapRoutesRepositoryV6Mapper, private val validator: JupiterSwapRouteValidator, ) : JupiterSwapRoutesV6Repository { override suspend fun getSwapRoutesForSwapPair( diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt similarity index 94% rename from app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt rename to app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt index 0d26e1c80e..aeca028187 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesV6Mapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt @@ -5,6 +5,7 @@ import org.json.JSONObject import java.math.BigDecimal import java.math.BigInteger import org.p2p.core.crypto.toBase58Instance +import org.p2p.core.utils.divideSafe import org.p2p.core.utils.orZero import org.p2p.core.utils.toJsonObject import org.p2p.wallet.jupiter.api.response.SwapJupiterV6QuoteResponse @@ -13,7 +14,7 @@ import org.p2p.wallet.jupiter.repository.model.JupiterSwapRouteV6 import org.p2p.wallet.jupiter.repository.model.SwapKeyAppFees import org.p2p.wallet.utils.mapAsStrings -class JupiterSwapRoutesV6Mapper( +class JupiterSwapRoutesRepositoryV6Mapper( private val gson: Gson ) { fun fromNetwork( @@ -46,7 +47,7 @@ class JupiterSwapRoutesV6Mapper( val fees = SwapKeyAppFees( totalFees = response.keyAppFees.fee.toBigInteger(), signatureFee = feesJson.optLong("signatureFee").toBigInteger(), - ataDeposits = ataDeposits, + ataDepositsInSol = ataDeposits, platformFeeTokenB = platformFeeAmount, platformFeePercent = platformFeePercent, totalFeeAndDeposits = feesJson.optLong("totalFeeAndDeposits").toBigInteger(), @@ -74,8 +75,6 @@ class JupiterSwapRoutesV6Mapper( } private fun getPlatformFeePercent(fee: SwapJupiterV6QuoteResponse.PlatformFeeResponse?): BigDecimal { - return fee?.feeBps - ?.divide(100.toBigDecimal()) - .orZero() + return fee?.feeBps?.divideSafe(100.toBigDecimal()).orZero() } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt index 41a7ee5ab0..68b8a0e294 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt @@ -13,7 +13,7 @@ import org.p2p.wallet.jupiter.repository.model.JupiterSwapRouteV6 import org.p2p.wallet.jupiter.repository.model.SwapFailure import org.p2p.wallet.jupiter.repository.transaction.JupiterSwapTransactionRepository import org.p2p.wallet.jupiter.repository.v6.JupiterSwapRoutesV6Repository -import org.p2p.wallet.jupiter.statemanager.validator.MinimumSolAmountValidator +import org.p2p.wallet.jupiter.statemanager.validator.SwapMinimumSolAmountValidator import org.p2p.wallet.jupiter.statemanager.validator.SwapValidator import org.p2p.wallet.swap.model.Slippage @@ -21,7 +21,7 @@ class SwapStateRoutesRefresher( private val tokenKeyProvider: TokenKeyProvider, private val swapRoutesRepository: JupiterSwapRoutesV6Repository, private val swapTransactionRepository: JupiterSwapTransactionRepository, - private val minSolBalanceValidator: MinimumSolAmountValidator, + private val minSolBalanceValidator: SwapMinimumSolAmountValidator, private val swapValidator: SwapValidator, private val swapProfiler: SwapProfiler, ) { diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/MinimumSolAmountValidator.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt similarity index 98% rename from app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/MinimumSolAmountValidator.kt rename to app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt index e776e741c8..fb67e12907 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/MinimumSolAmountValidator.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt @@ -11,7 +11,7 @@ import org.p2p.wallet.jupiter.statemanager.SwapFeatureException import org.p2p.wallet.rpc.repository.amount.RpcAmountRepository import org.p2p.wallet.swap.model.Slippage -class MinimumSolAmountValidator( +class SwapMinimumSolAmountValidator( private val rpcAmountRepository: RpcAmountRepository ) { diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt index 542b9ef0f8..67b3a39199 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt @@ -53,7 +53,7 @@ class JupiterSwapSettingsPresenter( super.attach(view) stateManager.observe() - .mapLatest { handleFeatureState(it) } + .mapLatest(::handleFeatureState) .launchIn(this) rateTickerManager.observe() @@ -217,21 +217,22 @@ class JupiterSwapSettingsPresenter( private suspend fun getContentListByFeatureState( state: SwapState, ): List { + val jupiterSolToken = swapTokensRepository.requireWrappedSol() return when (state) { SwapState.InitialLoading -> { emptyList() } is SwapState.TokenAZero -> { - emptyMapper.mapEmptyList(tokenB = state.tokenB) + emptyMapper.mapEmptyList(tokenB = state.tokenB, jupiterSolToken) } is SwapState.TokenANotZero -> { - emptyMapper.mapEmptyList(tokenB = state.tokenB) + emptyMapper.mapEmptyList(tokenB = state.tokenB, jupiterSolToken) } is SwapState.LoadingRoutes, is SwapState.LoadingTransaction, is SwapState.RoutesLoaded, is SwapState.SwapLoaded -> { - loadingMapper.mapLoadingList() + loadingMapper.mapLoadingList(jupiterSolToken) } is SwapState.SwapException -> { getContentListByFeatureState(state.previousFeatureState) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt index b51bd6a78f..cecdfe31f0 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt @@ -103,7 +103,7 @@ class SwapContentSettingsMapper( val networkFeeCell = swapFeeBuilder.buildNetworkFeeCell(route, solTokenForFee) this += networkFeeCell.cellModel - val accountFee = swapFeeBuilder.buildAccountFeeCell(route, tokenB) + val accountFee = swapFeeBuilder.buildAccountFeeCell(route, tokenB, solTokenForFee) if (accountFee != null) { this += accountFee.cellModel } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapEmptySettingsMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapEmptySettingsMapper.kt index 8a1caf2584..eff913f33b 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapEmptySettingsMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapEmptySettingsMapper.kt @@ -13,6 +13,7 @@ import org.p2p.uikit.utils.image.ImageViewCellModel import org.p2p.uikit.utils.text.TextViewCellModel import org.p2p.wallet.R import org.p2p.wallet.jupiter.interactor.model.SwapTokenModel +import org.p2p.wallet.jupiter.repository.model.JupiterSwapToken class SwapEmptySettingsMapper( private val swapFeeCellsBuilder: SwapFeeCellsBuilder @@ -20,8 +21,9 @@ class SwapEmptySettingsMapper( suspend fun mapEmptyList( tokenB: SwapTokenModel, + jupiterSolToken: JupiterSwapToken ): List = buildList { - this += swapFeeCellsBuilder.buildNetworkFeeCell(activeRoute = null, solToken = null).cellModel + this += swapFeeCellsBuilder.buildNetworkFeeCell(activeRoute = null, solToken = jupiterSolToken).cellModel addMinimumReceivedCell(tokenB) } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt index df0a566771..0c46f4f18d 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.supervisorScope import org.p2p.core.common.DrawableContainer import org.p2p.core.common.TextContainer import org.p2p.core.utils.asUsdSwap +import org.p2p.core.utils.divideSafe import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken import org.p2p.core.utils.formatTokenWithSymbol @@ -43,9 +44,9 @@ class SwapFeeCellsBuilder( suspend fun buildNetworkFeeCell( activeRoute: JupiterSwapRouteV6?, - solToken: JupiterSwapToken?, + solToken: JupiterSwapToken, ): SwapSettingsFeeBox { - if (activeRoute != null && solToken != null) { + if (activeRoute != null) { val networkFee = activeRoute.fees.signatureFee.fromLamports(solToken.decimals) val solTokenRate: BigDecimal? = loadRateForToken(SwapTokenModel.JupiterToken(solToken))?.rate val feeUsd: BigDecimal? = solTokenRate?.let { networkFee.multiply(it) } @@ -84,15 +85,26 @@ class SwapFeeCellsBuilder( suspend fun buildAccountFeeCell( activeRoute: JupiterSwapRouteV6, tokenB: SwapTokenModel, + solToken: JupiterSwapToken ): SwapSettingsFeeBox? { - val ataFee = activeRoute.fees.totalFees.fromLamports(tokenB.decimals) - if (ataFee.isZero()) { + val ataFeeInSol = activeRoute.fees.ataDepositsInSol.fromLamports(solToken.decimals) + if (ataFeeInSol.isZero()) { return null } - val tokenBRate: BigDecimal? = loadRateForToken(tokenB)?.rate - val feeUsd = tokenBRate?.let { ataFee.multiply(it) } - val formattedFeeAmount = ataFee.formatTokenWithSymbol(tokenB.tokenSymbol, tokenB.decimals) + val solRate = loadRateForToken(SwapTokenModel.JupiterToken(solToken))?.rate ?: kotlin.run { + Timber.e(IllegalStateException("Sol rate is null")) + return null + } + val tokenBRate = loadRateForToken(tokenB)?.rate ?: kotlin.run { + Timber.e(IllegalStateException("Token B (${tokenB.mintAddress} rate is null")) + return null + } + val ataFeeInTokenB: BigDecimal = solRate.divideSafe(tokenBRate) * ataFeeInSol + + val feeUsd = ataFeeInTokenB.multiply(tokenBRate) + + val formattedFeeAmount = ataFeeInTokenB.formatTokenWithSymbol(tokenB.tokenSymbol, tokenB.decimals) val cellModel = MainCellModel( leftSideCellModel = LeftSideCellModel.IconWithText( diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapLoadingSettingsMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapLoadingSettingsMapper.kt index 0ab85c3a2e..1041a360e1 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapLoadingSettingsMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapLoadingSettingsMapper.kt @@ -12,14 +12,15 @@ import org.p2p.uikit.utils.skeleton.SkeletonCellModel import org.p2p.uikit.utils.text.TextViewCellModel import org.p2p.uikit.utils.toPx import org.p2p.wallet.R +import org.p2p.wallet.jupiter.repository.model.JupiterSwapToken class SwapLoadingSettingsMapper( private val feeCellsBuilder: SwapFeeCellsBuilder ) { - suspend fun mapLoadingList(): List = buildList { + suspend fun mapLoadingList(solToken: JupiterSwapToken): List = buildList { addRouteCell() - add(feeCellsBuilder.buildNetworkFeeCell(activeRoute = null, solToken = null).cellModel) + add(feeCellsBuilder.buildNetworkFeeCell(activeRoute = null, solToken = solToken).cellModel) addAccountFeeCell() addLiquidityFeeCell() addEstimatedFeeCell() diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt index ea6e260073..07bb7e81be 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt @@ -31,7 +31,7 @@ class CalculateToken2022TransferFeeUseCase( ): BigInteger { if (this == null) return BigInteger.ZERO - if (transferFeeBasisPoints == 0 || preFeeAmount.isZero()) { + if (transferFeeBasisPoints == 0L || preFeeAmount.isZero()) { return BigInteger.ZERO } val transferFeeBasisPoints = transferFeeBasisPoints.toBigInteger() diff --git a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt index 5f05081211..21cfaea3aa 100644 --- a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt +++ b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt @@ -69,7 +69,7 @@ import org.p2p.wallet.jupiter.statemanager.token_selector.CommonSwapTokenSelecto import org.p2p.wallet.jupiter.statemanager.token_selector.PreinstallTokenASelector import org.p2p.wallet.jupiter.statemanager.token_selector.PreinstallTokensByMintSelector import org.p2p.wallet.jupiter.statemanager.token_selector.SwapInitialTokenSelector -import org.p2p.wallet.jupiter.statemanager.validator.MinimumSolAmountValidator +import org.p2p.wallet.jupiter.statemanager.validator.SwapMinimumSolAmountValidator import org.p2p.wallet.jupiter.statemanager.validator.SwapValidator import org.p2p.wallet.jupiter.ui.main.JupiterSwapTestHelpers.toTokenData import org.p2p.wallet.jupiter.ui.main.mapper.SwapButtonMapper @@ -244,7 +244,7 @@ open class JupiterSwapPresenterBaseTest { tokenKeyProvider = tokenKeyProvider, swapRoutesRepository = jupiterSwapRoutesV6Repository, swapTransactionRepository = jupiterSwapTransactionRepository, - minSolBalanceValidator = MinimumSolAmountValidator( + minSolBalanceValidator = SwapMinimumSolAmountValidator( rpcAmountRepository = rpcAmountRepository ), swapValidator = SwapValidator(), diff --git a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt index daf559a8a8..f4aa1a7eae 100644 --- a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt +++ b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt @@ -195,7 +195,7 @@ object JupiterSwapTestHelpers { originalRoute = JsonObject(), fees = SwapKeyAppFees( signatureFee = BigInteger.ZERO, - ataDeposits = BigInteger.ZERO, + ataDepositsInSol = BigInteger.ZERO, totalFeeAndDeposits = BigInteger.ZERO, minimumSolForTransaction = BigInteger.ZERO, totalFees = BigInteger.ZERO, diff --git a/core/src/main/java/org/p2p/core/network/gson/GsonProvider.kt b/core/src/main/java/org/p2p/core/network/gson/GsonProvider.kt index 774b78d907..77bc71586e 100644 --- a/core/src/main/java/org/p2p/core/network/gson/GsonProvider.kt +++ b/core/src/main/java/org/p2p/core/network/gson/GsonProvider.kt @@ -4,7 +4,6 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import java.lang.reflect.Type -import java.math.BigInteger import kotlin.reflect.KClass import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.Base64String @@ -31,8 +30,6 @@ class GsonProvider { private fun buildGson(): Gson { return builder.apply { setLenient() - registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter()) - registerTypeAdapter(object : TypeToken() {}.type, BigIntegerTypeAdapter()) registerTypeAdapter(Long::class.java, LongTypeAdapter()) registerTypeAdapter(Int::class.java, IntTypeAdapter()) registerTypeAdapter(ByteArray::class.java, ByteArrayTypeAdapter()) diff --git a/core/src/main/java/org/p2p/core/network/gson/GsonTypeAdapters.kt b/core/src/main/java/org/p2p/core/network/gson/GsonTypeAdapters.kt index b164b667f0..fa5cddba13 100644 --- a/core/src/main/java/org/p2p/core/network/gson/GsonTypeAdapters.kt +++ b/core/src/main/java/org/p2p/core/network/gson/GsonTypeAdapters.kt @@ -8,38 +8,16 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import java.lang.reflect.Type -import java.math.BigInteger import java.util.Optional import org.p2p.core.model.DefaultBlockParameter import org.p2p.core.token.SolAddress import org.p2p.core.wrapper.HexString import org.p2p.core.wrapper.eth.EthAddress -import org.p2p.core.wrapper.eth.hexStringToBigIntegerOrNull import org.p2p.core.wrapper.eth.hexStringToByteArrayOrNull import org.p2p.core.wrapper.eth.hexStringToIntOrNull import org.p2p.core.wrapper.eth.hexStringToLongOrNull import org.p2p.core.wrapper.eth.toHexString -internal class BigIntegerTypeAdapter(private val isHex: Boolean = true) : TypeAdapter() { - override fun write(writer: JsonWriter, value: BigInteger?) { - if (value == null) { - writer.nullValue() - } else { - val stringValue = if (isHex) value.toHexString() else value.toString() - writer.value(stringValue) - } - } - - override fun read(reader: JsonReader): BigInteger? { - if (reader.peek() == JsonToken.NULL) { - reader.nextNull() - return null - } - val stringValue = reader.nextString() - return if (isHex) stringValue.hexStringToBigIntegerOrNull() else BigInteger(stringValue) - } -} - internal class LongTypeAdapter(private val isHex: Boolean = false) : TypeAdapter() { override fun write(writer: JsonWriter, value: Long?) { if (value == null) { @@ -190,7 +168,6 @@ internal class OptionalTypeAdapter( .setLenient() .registerTypeAdapter(EthAddress::class.java, AddressTypeAdapter()) .registerTypeAdapter(ByteArray::class.java, ByteArrayTypeAdapter()) - .registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter()) .registerTypeAdapter(Long::class.java, LongTypeAdapter()) .registerTypeAdapter(object : TypeToken() {}.type, LongTypeAdapter()) .registerTypeAdapter(Int::class.java, IntTypeAdapter()) diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index 343cf899b6..e6a13457bf 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -187,3 +187,4 @@ fun Int?.orZero(): Int = this ?: 0 // value is in (0..0.01) fun BigDecimal.lessThenMinValue() = !isZero() && isLessThan(AMOUNT_MIN_VALUE.toBigDecimal()) fun BigDecimal.moreThenMinValue() = isMoreThan(AMOUNT_MIN_VALUE.toBigDecimal()) +fun BigDecimal.divideByInt(byInt: Int): BigDecimal = divide(byInt.toBigDecimal()) diff --git a/core/src/main/java/org/p2p/core/utils/MathUtils.kt b/core/src/main/java/org/p2p/core/utils/MathUtils.kt index 97cf4a4a3b..514ac774f2 100644 --- a/core/src/main/java/org/p2p/core/utils/MathUtils.kt +++ b/core/src/main/java/org/p2p/core/utils/MathUtils.kt @@ -15,7 +15,10 @@ fun BigInteger.divideSafe(value: BigInteger): BigInteger = // scales by default to the decimals fun BigDecimal.divideSafe(value: BigDecimal, decimals: Int = DEFAULT_DECIMALS_VALUE): BigDecimal = - if (this.isZero() || value.isZero()) BigDecimal.ZERO - else this.divide(value, decimals, RoundingMode.HALF_EVEN).stripTrailingZeros() + if (this.isZero() || value.isZero()) { + BigDecimal.ZERO + } else { + this.divide(value, decimals, RoundingMode.HALF_EVEN).stripTrailingZeros() + } fun clamp(value: Int, min: Int, max: Int): Int = min(max(min, value), max) diff --git a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt index b95bafd763..49805ffa6b 100644 --- a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt +++ b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt @@ -22,7 +22,7 @@ sealed interface AccountInfoTokenExtensionConfig { data class TransferFeeConfig( @SerializedName("withheldAmount") - val withheldAmount: Long, + val withheldAmount: String, @SerializedName("newerTransferFee") val newerTransferFee: TransferFeeConfigData? = null, @SerializedName("olderTransferFee") @@ -33,10 +33,13 @@ sealed interface AccountInfoTokenExtensionConfig { const val NAME = "transferFeeConfig" } - data class TransferFeeConfigData( + class TransferFeeConfigData( + @SerializedName("epoch") val epoch: BigInteger, + @SerializedName("maximumFee") val maximumFee: BigInteger, - val transferFeeBasisPoints: Int, + @SerializedName("transferFeeBasisPoints") + val transferFeeBasisPoints: Long, ) { val transferFeePercent: BigDecimal get() = transferFeeBasisPoints.toBigDecimal().setScale(4) @@ -44,11 +47,19 @@ sealed interface AccountInfoTokenExtensionConfig { .multiply("100".toBigDecimal().setScale(4)) } + /** + * Each transfer fee has it's "turn on" epoch field + * when epoch comes - we should use the turned on transfer fee + */ fun getActualTransferFee(currentEpoch: BigInteger): TransferFeeConfigData? { - if (currentEpoch > (newerTransferFee?.epoch.orZero()) || currentEpoch.isZero()) { - return newerTransferFee ?: olderTransferFee + if (currentEpoch.isZero()) return newerTransferFee ?: olderTransferFee + + val isNewerTransferFeeEnabled = currentEpoch >= newerTransferFee?.epoch.orZero() + return if (isNewerTransferFeeEnabled) { + newerTransferFee + } else { + olderTransferFee } - return olderTransferFee } } @@ -58,9 +69,9 @@ sealed interface AccountInfoTokenExtensionConfig { @SerializedName("preUpdateAverageRate") val preUpdateAverageRate: Double, @SerializedName("initializationTimestamp") - val initializationTimestamp: Long, + val initializationTimestamp: BigInteger, @SerializedName("lastUpdateTimestamp") - val lastUpdateTimestamp: Long, + val lastUpdateTimestamp: BigInteger, @SerializedName("rateAuthority") val rateAuthority: String, ) : AccountInfoTokenExtensionConfig { From 98b93110acd3374741812adc2f6125c5e7aef065 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:56:13 +0300 Subject: [PATCH 23/51] PWN-933 - Add terms of service to JS bridge (#2190) --- .../main/assets/referral_bridge_provider.js | 3 + .../historylist/HistoryListViewPresenter.kt | 1 - .../org/p2p/wallet/referral/ReferralBridge.kt | 12 ++- .../p2p/wallet/referral/ReferralFragment.kt | 14 +++ .../org/p2p/wallet/url/OpenUrlFragment.kt | 93 +++++++++++++++++++ app/src/main/res/layout/fragment_open_url.xml | 39 ++++++++ app/src/main/res/layout/fragment_referral.xml | 4 + 7 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/url/OpenUrlFragment.kt create mode 100644 app/src/main/res/layout/fragment_open_url.xml diff --git a/app/src/main/assets/referral_bridge_provider.js b/app/src/main/assets/referral_bridge_provider.js index ffebf5fc27..e50c165c85 100644 --- a/app/src/main/assets/referral_bridge_provider.js +++ b/app/src/main/assets/referral_bridge_provider.js @@ -12,6 +12,9 @@ window.ReferralBridge = { showShareDialog: function(link) { AndroidReferralBridge.showShareDialog(link); }, + openTermsUrl: function(link) { + AndroidReferralBridge.openTermsUrl(link); + }, getClipboardContent: function() { return JSON.parse(AndroidReferralBridge.getClipboardValue()); }, diff --git a/app/src/main/java/org/p2p/wallet/history/ui/historylist/HistoryListViewPresenter.kt b/app/src/main/java/org/p2p/wallet/history/ui/historylist/HistoryListViewPresenter.kt index 40cf194020..3d4fb9bc99 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/historylist/HistoryListViewPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/historylist/HistoryListViewPresenter.kt @@ -130,7 +130,6 @@ class HistoryListViewPresenter( } override fun onItemClicked(historyItem: HistoryItem) { - Timber.i("on clicked") launch { when (historyItem) { is HistoryItem.TransactionItem -> { diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 1d61bff3e7..6402b4589e 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -25,6 +25,7 @@ class ReferralWebViewBridge( webView: WebView, private val tokenKeyProvider: TokenKeyProvider, private val onShareLinkCalled: (String) -> Unit, + private val openTerms: (link: String) -> Unit, private val onWebViewLoaded: () -> Unit ) { companion object { @@ -69,11 +70,7 @@ class ReferralWebViewBridge( append(consoleMessage.lineNumber()) } - if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { - Timber.tag(JS_BRIDGE_OBJECT_NAME).e(Exception(logMessage)) - } else { - Timber.tag(JS_BRIDGE_OBJECT_NAME).w(logMessage) - } + Timber.tag(JS_BRIDGE_OBJECT_NAME).i(logMessage) return true } } @@ -129,6 +126,11 @@ class ReferralWebViewBridge( onShareLinkCalled.invoke(link) } + @JavascriptInterface + fun openTermsUrl(link: String) { + openTerms.invoke(link) + } + @JavascriptInterface fun getUserPublicKeyAsync(): JsResultWrapper = makeAsyncCall { wrapInJsResult(tokenKeyProvider.publicKey) diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt index 480843aadf..df777cfe90 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt @@ -1,5 +1,6 @@ package org.p2p.wallet.referral +import androidx.core.view.isInvisible import androidx.core.view.isVisible import android.os.Bundle import android.view.View @@ -7,7 +8,9 @@ import org.koin.android.ext.android.get import org.p2p.wallet.R import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.databinding.FragmentReferralBinding +import org.p2p.wallet.url.OpenUrlFragment import org.p2p.wallet.utils.popBackStack +import org.p2p.wallet.utils.replaceFragment import org.p2p.wallet.utils.shareText import org.p2p.wallet.utils.viewbinding.viewBinding @@ -28,11 +31,13 @@ class ReferralFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.progressBar.isVisible = true + binding.webViewReferral.isInvisible = true jsBridge = ReferralWebViewBridge( webView = binding.webViewReferral, tokenKeyProvider = get(), onShareLinkCalled = ::showShareLinkDialog, + openTerms = ::navigateToTerms, onWebViewLoaded = ::onWebViewLoaded ) binding.toolbar.setNavigationOnClickListener { @@ -59,7 +64,16 @@ class ReferralFragment : requireContext().shareText(link) } + private fun navigateToTerms(link: String) { + val target = OpenUrlFragment.create( + url = link, + title = getString(R.string.onboarding_terms_of_use) + ) + replaceFragment(target) + } + private fun onWebViewLoaded() { + binding.webViewReferral.isVisible = true binding.progressBar.isVisible = false } } diff --git a/app/src/main/java/org/p2p/wallet/url/OpenUrlFragment.kt b/app/src/main/java/org/p2p/wallet/url/OpenUrlFragment.kt new file mode 100644 index 0000000000..f01a51bcf5 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/url/OpenUrlFragment.kt @@ -0,0 +1,93 @@ +package org.p2p.wallet.url + +import androidx.core.view.isVisible +import android.graphics.Bitmap +import android.os.Bundle +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import org.p2p.wallet.R +import org.p2p.wallet.common.mvp.BaseMvpFragment +import org.p2p.wallet.common.mvp.MvpView +import org.p2p.wallet.common.mvp.NoOpPresenter +import org.p2p.wallet.databinding.FragmentOpenUrlBinding +import org.p2p.wallet.utils.args +import org.p2p.wallet.utils.popBackStack +import org.p2p.wallet.utils.viewbinding.viewBinding +import org.p2p.wallet.utils.withArgs + +class OpenUrlFragment : BaseMvpFragment>( + R.layout.fragment_open_url +) { + + companion object { + private const val ARG_URL = "ARG_URL" + private const val ARG_BACK_ENABLED = "ARG_BACK_ENABLED" + private const val ARG_TITLE = "ARG_TITLE" + fun create( + url: String, + title: String? = null, + isBackEnabled: Boolean = true, + ): OpenUrlFragment { + return OpenUrlFragment() + .withArgs( + ARG_URL to url, + ARG_BACK_ENABLED to isBackEnabled, + ARG_TITLE to title + ) + } + } + + override val presenter = NoOpPresenter() + + private val binding: FragmentOpenUrlBinding by viewBinding() + + private val webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + binding.progressBar.isVisible = true + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView?, url: String?) { + if (view?.progress == 100) { + binding.progressBar.isVisible = false + } + super.onPageFinished(view, url) + } + } + + private val toolbarTitle: String? by args(ARG_TITLE) + private val isBackEnabled: Boolean by args(ARG_BACK_ENABLED, true) + private val url: String by args(ARG_URL) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (isBackEnabled) { + binding.toolbar.setNavigationOnClickListener { popBackStack() } + } else { + binding.toolbar.navigationIcon = null + } + if (toolbarTitle != null) { + binding.toolbar.title = toolbarTitle + } + + binding.webViewUrl.webViewClient = webViewClient + binding.webViewUrl.loadUrl(url) + } + + override fun onResume() { + super.onResume() + binding.webViewUrl.onResume() + binding.webViewUrl.requestFocus() + } + + override fun onPause() { + binding.webViewUrl.onPause() + super.onPause() + } + + override fun onDestroyView() { + binding.webViewUrl.destroy() + super.onDestroyView() + } +} diff --git a/app/src/main/res/layout/fragment_open_url.xml b/app/src/main/res/layout/fragment_open_url.xml new file mode 100644 index 0000000000..ff976a3cbf --- /dev/null +++ b/app/src/main/res/layout/fragment_open_url.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_referral.xml b/app/src/main/res/layout/fragment_referral.xml index 029785527c..ec08dec6ed 100644 --- a/app/src/main/res/layout/fragment_referral.xml +++ b/app/src/main/res/layout/fragment_referral.xml @@ -3,6 +3,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/bg_smoke" + android:backgroundTint="@color/bg_smoke" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> @@ -18,6 +20,8 @@ Date: Tue, 13 Feb 2024 18:09:42 +0300 Subject: [PATCH 24/51] PWN-947 - Swap fixes (#2191) --- .../bridge/send/ui/BridgeSendPresenter.kt | 3 +- ...ryTransactionDetailsBottomSheetFragment.kt | 4 +- .../p2p/wallet/jupiter/JupiterSwapModule.kt | 2 - .../repository/model/JupiterSwapRouteV6.kt | 2 + .../v6/JupiterSwapRoutesRepositoryV6Mapper.kt | 4 +- .../rate/SwapRateTickerManager.kt | 2 +- .../jupiter/ui/info/SwapFeeInfoBottomSheet.kt | 92 ++------- .../ui/info/SwapInfoLiquidityFeeMapper.kt | 186 ++++++++++++++++++ .../wallet/jupiter/ui/info/SwapInfoMapper.kt | 116 +---------- .../jupiter/ui/main/JupiterSwapPresenter.kt | 21 +- .../jupiter/ui/main/SwapTokenRateLoader.kt | 2 +- .../jupiter/ui/main/widget/SwapWidget.kt | 4 +- .../presenter/SwapContentSettingsMapper.kt | 12 +- .../settings/presenter/SwapFeeCellsBuilder.kt | 172 ++++++++-------- .../presenter/SwapTokensCommonMapper.kt | 13 +- .../interactor/SeedPhraseInteractor.kt | 16 +- .../derivable/DerivableAccountsPresenter.kt | 7 +- .../org/p2p/wallet/send/model/SendFeeTotal.kt | 5 +- .../interactor/ReceiveViaLinkInteractor.kt | 11 +- .../org/p2p/wallet/swap/model/orca/SwapFee.kt | 7 +- .../wallet/user/interactor/UserInteractor.kt | 6 +- .../user/interactor/UserTokensInteractor.kt | 5 +- .../user/repository/UserInMemoryRepository.kt | 2 +- .../UserTokensDatabaseRepository.kt | 2 +- .../repository/UserTokensRemoteRepository.kt | 4 +- app/src/main/res/values/swap_strings.xml | 7 +- .../ui/main/JupiterSwapPresenterBaseTest.kt | 2 +- .../jupiter/ui/main/JupiterSwapTestHelpers.kt | 4 +- .../ui/main/JupiterTestPresenterBuilder.kt | 2 +- .../org/p2p/core/utils/AmountExtensions.kt | 38 ++-- .../repository/EthereumKitRepository.kt | 4 +- .../token/EthereumTokensInMemoryRepository.kt | 6 +- .../manager/TokenServiceEventPublisher.kt | 4 +- .../service/api/mapper/TokenServiceMapper.kt | 2 +- .../mapper/TokenServiceDatabaseMapper.kt | 4 +- .../token/service/model/TokenServicePrice.kt | 4 +- .../repository/TokenServiceRepository.kt | 21 +- .../repository/TokenServiceRepositoryImpl.kt | 24 ++- .../mapper/TokenServiceDatabaseMapperTest.kt | 6 +- .../price/TokenPriceDatabaseRepositoryTest.kt | 2 +- 40 files changed, 434 insertions(+), 396 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt diff --git a/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt b/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt index 74d966c3a8..8ff87bb73b 100644 --- a/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt @@ -11,6 +11,7 @@ import org.p2p.core.common.TextContainer import org.p2p.core.common.di.AppScope import org.p2p.core.model.CurrencyMode import org.p2p.core.token.Token +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.asNegativeUsdTransaction import org.p2p.core.utils.asUsd import org.p2p.core.utils.formatTokenWithSymbol @@ -300,7 +301,7 @@ class BridgeSendPresenter( } is CurrencyMode.Token -> { val tokenSymbol = mode.symbol - amountPlusTotalFee.formatTokenWithSymbol(tokenSymbol) + amountPlusTotalFee.formatTokenWithSymbol(tokenSymbol, SOL_DECIMALS) } else -> { emptyString() diff --git a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt index a065945d88..caf8359f76 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsBottomSheetFragment.kt @@ -16,7 +16,7 @@ import com.google.android.material.snackbar.Snackbar import org.koin.android.ext.android.inject import org.p2p.core.crypto.Base58String import org.p2p.core.glide.GlideManager -import org.p2p.core.utils.DEFAULT_DECIMAL +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken import org.p2p.core.utils.lessThenMinValue @@ -324,7 +324,7 @@ class HistoryTransactionDetailsBottomSheetFragment : val highlightedText = "($formattedUsdAmount)" val commonText = buildString { - append(feeAmount.totalInTokens.formatToken(feeAmount.tokensDecimals ?: DEFAULT_DECIMAL)) + append(feeAmount.totalInTokens.formatToken(feeAmount.tokensDecimals ?: SOL_DECIMALS)) appendWhitespace() append(feeAmount.tokenSymbol.orEmpty()) appendWhitespace() diff --git a/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt b/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt index cbd0948db0..a315e80597 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/JupiterSwapModule.kt @@ -51,7 +51,6 @@ import org.p2p.wallet.jupiter.statemanager.token_selector.SwapInitialTokenSelect import org.p2p.wallet.jupiter.statemanager.token_selector.SwapInitialTokensData import org.p2p.wallet.jupiter.statemanager.validator.SwapMinimumSolAmountValidator import org.p2p.wallet.jupiter.statemanager.validator.SwapValidator -import org.p2p.wallet.jupiter.ui.info.SwapInfoMapper import org.p2p.wallet.jupiter.ui.main.JupiterSwapContract import org.p2p.wallet.jupiter.ui.main.JupiterSwapPresenter import org.p2p.wallet.jupiter.ui.main.SwapShareDeeplinkBuilder @@ -230,7 +229,6 @@ object JupiterSwapModule : InjectionModule { } factoryOf(::SwapCommonSettingsMapper) - factoryOf(::SwapInfoMapper) factoryOf(::SwapSelectRoutesMapper) factoryOf(::SwapEmptySettingsMapper) factoryOf(::SwapLoadingSettingsMapper) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt index 0d97d7835b..2543f033f9 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapRouteV6.kt @@ -9,6 +9,8 @@ import org.p2p.core.crypto.Base58String data class JupiterSwapRouteV6( val inAmountInLamports: BigInteger, val outAmountInLamports: BigInteger, + val inTokenMint: Base58String, + val outTokenMint: Base58String, val priceImpactPercent: BigDecimal, @IntRange(from = 0, to = 10000) val slippageBps: Int, diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt index aeca028187..debcce27de 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/v6/JupiterSwapRoutesRepositoryV6Mapper.kt @@ -57,13 +57,15 @@ class JupiterSwapRoutesRepositoryV6Mapper( return JupiterSwapRouteV6( inAmountInLamports = response.inAmount.toBigInteger(), outAmountInLamports = response.outAmount.toBigInteger(), + inTokenMint = response.inputMint.toBase58Instance(), + outTokenMint = response.outputMint.toBase58Instance(), priceImpactPercent = response.priceImpactPct.toBigDecimal(), slippageBps = response.slippageBps, otherAmountThreshold = response.otherAmountThreshold, swapMode = response.swapMode, originalRoute = gson.toJsonObject(response), routePlans = plans, - fees = fees + fees = fees, ) } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/rate/SwapRateTickerManager.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/rate/SwapRateTickerManager.kt index 9df1ffc223..21455683cf 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/rate/SwapRateTickerManager.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/rate/SwapRateTickerManager.kt @@ -131,7 +131,7 @@ class SwapRateTickerManager( private suspend fun findJupiterTokenRate(to: SwapTokenModel.JupiterToken): BigDecimal? { val tokenData = userLocalRepository.findTokenData(to.mintAddress.base58Value) ?: return null - val cachedPrice = tokenServiceRepository.findTokenPriceByAddress( + val cachedPrice = tokenServiceRepository.getTokenPriceByAddress( tokenAddress = tokenData.mintAddress, networkChain = TokenServiceNetwork.SOLANA ) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapFeeInfoBottomSheet.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapFeeInfoBottomSheet.kt index fa0b96b8ca..53a9dd1b91 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapFeeInfoBottomSheet.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapFeeInfoBottomSheet.kt @@ -8,20 +8,17 @@ import android.os.Bundle import android.view.View import com.hannesdorfmann.adapterdelegates4.AdapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject -import org.koin.core.parameter.parametersOf +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.p2p.core.dispatchers.CoroutineDispatchers -import org.p2p.uikit.components.finance_block.MainCellModel import org.p2p.uikit.components.finance_block.baseCellDelegate import org.p2p.uikit.components.info_block.InfoBlockCellModel import org.p2p.uikit.model.AnyCellItem @@ -31,14 +28,9 @@ import org.p2p.wallet.common.adapter.CommonAnyCellAdapter import org.p2p.wallet.common.ui.bottomsheet.BaseBottomSheet import org.p2p.wallet.databinding.DialogSwapInfoBinding import org.p2p.wallet.databinding.ItemSwapInfoBannerBinding -import org.p2p.wallet.jupiter.interactor.SwapTokensInteractor -import org.p2p.wallet.jupiter.interactor.model.SwapTokenModel -import org.p2p.wallet.jupiter.repository.model.JupiterSwapRoutePlanV6 -import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository import org.p2p.wallet.jupiter.statemanager.SwapState import org.p2p.wallet.jupiter.statemanager.SwapStateManager import org.p2p.wallet.jupiter.statemanager.SwapStateManagerHolder -import org.p2p.wallet.jupiter.ui.main.SwapRateLoaderState import org.p2p.wallet.utils.args import org.p2p.wallet.utils.viewbinding.viewBinding import org.p2p.wallet.utils.withArgs @@ -55,8 +47,6 @@ enum class SwapInfoType { TOKEN_2022_TRANSFER } -private typealias LoadRateBox = Triple - private fun swapInfoBannerDelegate(): AdapterDelegate> = adapterDelegateViewBinding( viewBinding = { inflater, parent -> ItemSwapInfoBannerBinding.inflate(inflater, parent, false) } @@ -96,13 +86,12 @@ class SwapFeeInfoBottomSheet : BaseBottomSheet(R.layout.dialog_swap_info) { private val swapInfoType: SwapInfoType by args(ARG_INFO_TYPE_KEY) private val managerHolder: SwapStateManagerHolder by inject() - private val mapper: SwapInfoMapper by inject() - private val interactor: SwapTokensInteractor by inject { - parametersOf(stateManagerKey) + private val mapper = SwapInfoMapper() + private val liquidityFeeMapper: SwapInfoLiquidityFeeMapper by lazy { + SwapInfoLiquidityFeeMapper(swapTokensRepository = get(), tokenServiceRepository = get()) } - private val swapTokensRepository: JupiterSwapTokensRepository by inject() - private val coroutineDispatchers: CoroutineDispatchers by inject() + private val dispatchers: CoroutineDispatchers by inject() private val stateManager: SwapStateManager get() = managerHolder.get(stateManagerKey) @@ -144,82 +133,39 @@ class SwapFeeInfoBottomSheet : BaseBottomSheet(R.layout.dialog_swap_info) { setExpanded(true) } + @OptIn(ExperimentalCoroutinesApi::class) private fun observeFeatureState() { stateManager.observe() - .flatMapLatest { handleFeatureState(it) } - .flowOn(coroutineDispatchers.io) - .onEach { adapter.items = it } + .flatMapLatest(transform = ::mapLiquidityCellsFlow) + .flowOn(dispatchers.io) + .onEach(adapter::setItems) .flowWithLifecycle(lifecycle) .launchIn(lifecycleScope) } - private suspend fun handleFeatureState(state: SwapState): Flow> { + private fun mapLiquidityCellsFlow(state: SwapState): Flow> { return when (state) { SwapState.InitialLoading, is SwapState.LoadingRoutes, is SwapState.TokenANotZero, is SwapState.TokenAZero -> { - flowOf(mapper.mapEmptyLiquidityFee()) + flowOf(liquidityFeeMapper.mapNoRouteLoaded()) } is SwapState.SwapException -> { - handleFeatureState(state.previousFeatureState) + mapLiquidityCellsFlow(state.previousFeatureState) } is SwapState.RoutesLoaded, + is SwapState.LoadingTransaction, is SwapState.SwapLoaded -> { val route = when (state) { is SwapState.RoutesLoaded -> state.route is SwapState.SwapLoaded -> state.route - else -> return flowOf(mapper.mapEmptyLiquidityFee()) - } - flow { - val rateLoaderList = mutableListOf>() - val feeCells = route.routePlans.map { plan -> - val loadingCell = mapper.getLiquidityFeeCell(plan) - rateLoaderList += getRateLoaderFlow(plan, loadingCell) - loadingCell - } - var fullUiList = mapper.mapEmptyLiquidityFee().plus(feeCells) - emit(fullUiList) - - rateLoaderList.merge() - .collect { - val routePlan = it.first - val loadingCell = it.second - val rateLoaderState = it.third - val indexOf = fullUiList.indexOf(loadingCell) - if (indexOf >= 0) { - val newCell = mapper.updateLiquidityFee( - marketInfo = routePlan, - oldCell = loadingCell, - state = rateLoaderState - ) - val newList = fullUiList.toMutableList().apply { set(indexOf, newCell) } - fullUiList = newList - emit(newList) - } - } - } - } - is SwapState.LoadingTransaction -> { - flow { - val fullUiList = mapper.mapLoadingLiquidityFee(state.route) - emit(fullUiList) - } - } - } - } + is SwapState.LoadingTransaction -> state.route + else -> null + } ?: return flowOf(liquidityFeeMapper.mapNoRouteLoaded()) - private suspend fun getRateLoaderFlow( - routePlan: JupiterSwapRoutePlanV6, - loadingCell: MainCellModel, - ): Flow { - val lpToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) - val loadingCellFlow = flowOf(routePlan to loadingCell) - val rateLoaderFlow = lpToken - ?.let { stateManager.getTokenRate(SwapTokenModel.JupiterToken(it)) } - ?: flowOf(SwapRateLoaderState.Error) - return loadingCellFlow.combine(rateLoaderFlow) { a, b -> - Triple(a.first, a.second, b) + liquidityFeeMapper.mapLiquidityFees(route) + } } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt new file mode 100644 index 0000000000..0e6c538dec --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt @@ -0,0 +1,186 @@ +package org.p2p.wallet.jupiter.ui.info + +import androidx.annotation.DrawableRes +import java.math.BigDecimal +import java.math.BigInteger +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.p2p.core.common.DrawableContainer +import org.p2p.core.common.TextContainer +import org.p2p.core.crypto.Base58String +import org.p2p.core.utils.asUsdSwap +import org.p2p.core.utils.formatTokenWithSymbol +import org.p2p.core.utils.fromLamports +import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.repository.TokenServiceRepository +import org.p2p.uikit.components.finance_block.MainCellModel +import org.p2p.uikit.components.finance_block.MainCellStyle +import org.p2p.uikit.components.icon_wrapper.IconWrapperCellModel +import org.p2p.uikit.components.info_block.InfoBlockCellModel +import org.p2p.uikit.components.left_side.LeftSideCellModel +import org.p2p.uikit.components.right_side.RightSideCellModel +import org.p2p.uikit.model.AnyCellItem +import org.p2p.uikit.utils.drawable.DrawableCellModel +import org.p2p.uikit.utils.drawable.shape.shapeCircle +import org.p2p.uikit.utils.image.ImageViewCellModel +import org.p2p.uikit.utils.skeleton.SkeletonCellModel +import org.p2p.uikit.utils.text.TextViewCellModel +import org.p2p.uikit.utils.toPx +import org.p2p.wallet.R +import org.p2p.wallet.jupiter.repository.model.JupiterSwapRoutePlanV6 +import org.p2p.wallet.jupiter.repository.model.JupiterSwapRouteV6 +import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository + +class SwapInfoLiquidityFeeMapper( + private val swapTokensRepository: JupiterSwapTokensRepository, + private val tokenServiceRepository: TokenServiceRepository +) { + + fun mapNoRouteLoaded(): List { + return listOf(createLiquidityFeeBanner()) + } + + fun mapLiquidityFees( + route: JupiterSwapRouteV6, + ): Flow> = flow { + val keyAppFee = route.fees.platformFeeTokenB + val tokenBMint = route.outTokenMint + + val cellModels = mutableListOf() + + cellModels += createLiquidityFeeBanner() + route.routePlans.forEach { routePlan -> + cellModels += mapLiquidityFeeCellLoadingRate(routePlan) + } + + val keyAppFeeCellModel = createKeyAppFeeCellNoRate(keyAppFee, tokenBMint) + if (keyAppFeeCellModel != null) { + cellModels += keyAppFeeCellModel + } + // emit info without rates + emit(cellModels) + + // another collection, we can't modify cellModels + val cellModelsWithRates = mutableListOf() + // start loading rates for liquidity fees + cellModelsWithRates += createLiquidityFeeBanner() + route.routePlans.forEach { routePlan -> + val feeUsd = getUsdAmountForTokenAmount(routePlan.feeAmount, routePlan.feeMint) + + cellModelsWithRates += mapLiquidityFeeCellLoadingRate(routePlan) + .copy(rightSideCellModel = feeUsd?.let(::createUsdRateCellModel)) + } + + val keyAppFeeUsd = getUsdAmountForTokenAmount(keyAppFee, tokenBMint) + val updatedKeyAppFeeCell = createKeyAppFeeCellNoRate(keyAppFee, tokenBMint)?.copy( + rightSideCellModel = keyAppFeeUsd?.let(::createUsdRateCellModel) + ) + if (updatedKeyAppFeeCell != null) { + cellModelsWithRates += updatedKeyAppFeeCell + } + // emit info with rates + emit(cellModelsWithRates) + } + + private suspend fun mapLiquidityFeeCellLoadingRate( + routePlan: JupiterSwapRoutePlanV6, + ): MainCellModel { + val label = routePlan.label + val liquidityToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) + val liquidityFee = routePlan.feeAmount + + val feePercent = "${routePlan.percent}%" + val firstLineText = TextViewCellModel.Raw( + text = TextContainer(R.string.swap_info_details_liquidity_cell_title, label, feePercent), + maxLines = 2 + ) + + val secondLineText = liquidityToken?.let { + val feeInTokenLamports = liquidityFee + .fromLamports(it.decimals) + .formatTokenWithSymbol(it.tokenSymbol, it.decimals) + TextViewCellModel.Raw(text = TextContainer(feeInTokenLamports)) + } + + return MainCellModel( + styleType = MainCellStyle.BASE_CELL, + leftSideCellModel = LeftSideCellModel.IconWithText( + firstLineText = firstLineText, + secondLineText = secondLineText + ), + rightSideCellModel = rightSideSkeleton(), + ) + } + + private suspend fun createKeyAppFeeCellNoRate(keyAppFee: BigInteger, tokenBMint: Base58String): MainCellModel? { + val tokenB = swapTokensRepository.findTokenByMint(tokenBMint) ?: return null + val feeAmount = keyAppFee.fromLamports(tokenB.decimals) + .formatTokenWithSymbol(tokenB.tokenSymbol, tokenB.decimals) + + return MainCellModel( + leftSideCellModel = LeftSideCellModel.IconWithText( + firstLineText = TextViewCellModel.Raw( + TextContainer(R.string.swap_info_details_key_app_fee_title) + ), + secondLineText = TextViewCellModel.Raw( + TextContainer(feeAmount) + ) + ), + rightSideCellModel = rightSideSkeleton(), + styleType = MainCellStyle.BASE_CELL, + ) + } + + private fun createUsdRateCellModel( + usdAmount: BigDecimal + ): RightSideCellModel { + return RightSideCellModel.SingleTextTwoIcon( + text = TextViewCellModel.Raw(TextContainer(usdAmount.asUsdSwap())) + ) + } + + private fun rightSideSkeleton(): RightSideCellModel.SingleTextTwoIcon = RightSideCellModel.SingleTextTwoIcon( + text = TextViewCellModel.Skeleton( + skeleton = SkeletonCellModel( + height = 20.toPx(), + width = 60.toPx(), + radius = 5f.toPx(), + ), + ), + ) + + private fun createLiquidityFeeBanner(): AnyCellItem = SwapInfoBannerCellModel( + banner = R.drawable.ic_fee_banner, + infoCell = InfoBlockCellModel( + icon = getIcon(R.drawable.ic_lightning), + firstLineText = TextViewCellModel.Raw( + text = TextContainer(R.string.swap_info_details_liquidity_fee_title) + ), + secondLineText = TextViewCellModel.Raw( + text = TextContainer(R.string.swap_info_details_liquidity_fee_subtitle) + ) + ) + ) + + private fun getIcon(@DrawableRes icon: Int): IconWrapperCellModel.SingleIcon = IconWrapperCellModel.SingleIcon( + icon = ImageViewCellModel( + icon = DrawableContainer(icon), + iconTint = R.color.icons_mountain, + background = DrawableCellModel(tint = R.color.bg_smoke), + clippingShape = shapeCircle(), + ) + ) + + private suspend fun getUsdAmountForTokenAmount( + amount: BigInteger, + tokenMint: Base58String, + ): BigDecimal? { + val token = swapTokensRepository.findTokenByMint(tokenMint) ?: return null + val tokenRate = tokenServiceRepository.getTokenPriceByAddress( + tokenAddress = tokenMint.base58Value, + networkChain = TokenServiceNetwork.SOLANA + )?.usdRate ?: return null + + return amount.fromLamports(token.decimals) * tokenRate + } +} diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoMapper.kt index 881860ebc6..12da7d5864 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoMapper.kt @@ -3,33 +3,19 @@ package org.p2p.wallet.jupiter.ui.info import androidx.annotation.DrawableRes import org.p2p.core.common.DrawableContainer import org.p2p.core.common.TextContainer -import org.p2p.core.utils.asUsdSwap -import org.p2p.core.utils.formatTokenWithSymbol -import org.p2p.core.utils.fromLamports -import org.p2p.uikit.components.finance_block.MainCellModel -import org.p2p.uikit.components.finance_block.MainCellStyle import org.p2p.uikit.components.icon_wrapper.IconWrapperCellModel import org.p2p.uikit.components.info_block.InfoBlockCellModel -import org.p2p.uikit.components.left_side.LeftSideCellModel -import org.p2p.uikit.components.right_side.RightSideCellModel import org.p2p.uikit.model.AnyCellItem import org.p2p.uikit.utils.drawable.DrawableCellModel import org.p2p.uikit.utils.drawable.shape.shapeCircle import org.p2p.uikit.utils.drawable.shape.shapeRoundedAll import org.p2p.uikit.utils.drawable.shapeDrawable import org.p2p.uikit.utils.image.ImageViewCellModel -import org.p2p.uikit.utils.skeleton.SkeletonCellModel import org.p2p.uikit.utils.text.TextViewCellModel import org.p2p.uikit.utils.toPx import org.p2p.wallet.R -import org.p2p.wallet.jupiter.repository.model.JupiterSwapRoutePlanV6 -import org.p2p.wallet.jupiter.repository.model.JupiterSwapRouteV6 -import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository -import org.p2p.wallet.jupiter.ui.main.SwapRateLoaderState -class SwapInfoMapper( - private val swapTokensRepository: JupiterSwapTokensRepository -) { +class SwapInfoMapper { fun mapNetworkFee(): List = buildList { this += SwapInfoBannerCellModel( @@ -87,73 +73,6 @@ class SwapInfoMapper( ) } - suspend fun mapLoadingLiquidityFee( - route: JupiterSwapRouteV6? = null, - ): List = buildList { - addAll(mapEmptyLiquidityFee()) - if (route == null) return@buildList - - route.routePlans.forEach { routePlan -> - this += getLiquidityFeeCell(routePlan) - } - } - - suspend fun getLiquidityFeeCell( - routePlan: JupiterSwapRoutePlanV6, - ): MainCellModel { - val label = routePlan.label - val liquidityToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) - val liquidityFee = routePlan.feeAmount - - val feePercent = routePlan.percent + "%" - val firstLineText = TextViewCellModel.Raw( - text = TextContainer(R.string.swap_info_details_liquidity_cell_title, label, feePercent), - maxLines = 2 - ) - - val secondLineText = liquidityToken?.let { - val feeInTokenLamports = liquidityFee.fromLamports(it.decimals) - .formatTokenWithSymbol(it.tokenSymbol, it.decimals) - TextViewCellModel.Raw( - text = TextContainer(feeInTokenLamports) - ) - } - - return MainCellModel( - styleType = MainCellStyle.BASE_CELL, - leftSideCellModel = LeftSideCellModel.IconWithText( - firstLineText = firstLineText, - secondLineText = secondLineText - ), - rightSideCellModel = rightSideSkeleton(), - ) - } - - private fun rightSideSkeleton(): RightSideCellModel.SingleTextTwoIcon = RightSideCellModel.SingleTextTwoIcon( - text = TextViewCellModel.Skeleton( - skeleton = SkeletonCellModel( - height = 20.toPx(), - width = 60.toPx(), - radius = 5f.toPx(), - ), - ), - ) - - fun mapEmptyLiquidityFee(): List = buildList { - this += SwapInfoBannerCellModel( - banner = R.drawable.ic_fee_banner, - infoCell = InfoBlockCellModel( - icon = getIcon(R.drawable.ic_lightning), - firstLineText = TextViewCellModel.Raw( - text = TextContainer(R.string.swap_info_details_liquidity_fee_title) - ), - secondLineText = TextViewCellModel.Raw( - text = TextContainer(R.string.swap_info_details_liquidity_fee_subtitle) - ) - ) - ) - } - fun mapMinimumReceived(): List = buildList { this += SwapInfoBannerCellModel( banner = R.drawable.ic_about_earn_3, @@ -188,37 +107,4 @@ class SwapInfoMapper( clippingShape = shapeCircle(), ) ) - - fun updateLiquidityFee( - marketInfo: JupiterSwapRoutePlanV6, - oldCell: MainCellModel, - state: SwapRateLoaderState - ): MainCellModel { - return when (state) { - SwapRateLoaderState.Empty -> { - oldCell - } - is SwapRateLoaderState.NoRateAvailable, is SwapRateLoaderState.Error -> { - oldCell.copy(rightSideCellModel = null) - } - SwapRateLoaderState.Loading -> { - oldCell.copy(rightSideCellModel = rightSideSkeleton()) - } - is SwapRateLoaderState.Loaded -> { - val rate = state.rate - val token = state.token - val feeInUsd = marketInfo.feeAmount - .fromLamports(token.decimals) - .multiply(rate) - .asUsdSwap() - oldCell.copy( - rightSideCellModel = RightSideCellModel.SingleTextTwoIcon( - text = TextViewCellModel.Raw( - text = TextContainer(feeInUsd) - ), - ) - ) - } - } - } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt index 33579699d7..d48807656a 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt @@ -21,6 +21,7 @@ import org.p2p.core.common.TextContainer import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.network.data.ServerException import org.p2p.core.utils.asUsd +import org.p2p.core.utils.emptyString import org.p2p.core.utils.formatToken import org.p2p.core.utils.isZero import org.p2p.core.utils.toBigDecimalOrZero @@ -517,6 +518,7 @@ class JupiterSwapPresenter( private suspend fun handleLoadingRoutes(state: SwapState.LoadingRoutes) { rateTickerManager.handleRoutesLoading(state) + showRoutesForDebug(bestRoute = null, slippage = state.slippage) mapWidgetStates(state) updateWidgets() @@ -684,7 +686,15 @@ class JupiterSwapPresenter( view?.setSecondTokenWidgetState(state = widgetBState) } - private fun showRoutesForDebug(bestRoute: JupiterSwapRouteV6, slippage: Slippage) { + private fun showRoutesForDebug( + bestRoute: JupiterSwapRouteV6?, + slippage: Slippage + ) { + if (bestRoute == null) { + view?.showDebugInfo(TextViewCellModel.Raw(TextContainer(emptyString()))) + return + } + val info = buildString { append("Route: ") @@ -708,10 +718,13 @@ class JupiterSwapPresenter( } appendLine() + appendLine("Slippage: ${slippage.percentValue}") appendLine() - append("Slippage: ${slippage.percentValue}") - appendLine() - append("KeyApp fee: NONE for v6") + val keyApp = bestRoute.originalRoute.getAsJsonObject("keyapp") + appendLine("fee: ${keyApp.get("fee")}") + appendLine("fees: ${keyApp.getAsJsonObject("fees")}") + appendLine("KeyApp fee lamports: ${bestRoute.fees.platformFeeTokenB}") + append("KeyApp fee %: ${bestRoute.fees.platformFeePercent}") } view?.showDebugInfo( diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapTokenRateLoader.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapTokenRateLoader.kt index ab1949b73e..6963c11da1 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapTokenRateLoader.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/SwapTokenRateLoader.kt @@ -66,7 +66,7 @@ class SwapTokenRateLoader( ) { emitAndSaveState(SwapRateLoaderState.Loading) try { - val tokenPrice = tokenServiceRepository.findTokenPriceByAddress( + val tokenPrice = tokenServiceRepository.getTokenPriceByAddress( tokenAddress = token.details.tokenMint.base58Value, networkChain = TokenServiceNetwork.SOLANA )?.usdRate diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/widget/SwapWidget.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/widget/SwapWidget.kt index 63c9918dde..193f0e6abc 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/widget/SwapWidget.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/widget/SwapWidget.kt @@ -17,7 +17,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.p2p.core.glide.GlideManager import org.p2p.core.textwatcher.AmountFractionTextWatcher -import org.p2p.core.utils.DEFAULT_DECIMAL +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.emptyString import org.p2p.core.utils.hideKeyboard import org.p2p.uikit.utils.drawable.shape.shapeRounded16dp @@ -156,7 +156,7 @@ class SwapWidget @JvmOverloads constructor( val readOnly = model.isStatic || newAmount is TextViewCellModel.Skeleton val inputType = if (readOnly) InputType.TYPE_NULL else initInputType editTextAmount.setReadOnly(readOnly, inputType) - val amountMaxDecimals = model.amountMaxDecimals ?: DEFAULT_DECIMAL + val amountMaxDecimals = model.amountMaxDecimals ?: SOL_DECIMALS updateFormatter(amountMaxDecimals) val oldAmountRaw = editTextAmount.text?.toString() ?: emptyString() val newAmountRaw = (newAmount as? TextViewCellModel.Raw)?.text?.getString(context) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt index cecdfe31f0..c325136486 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapContentSettingsMapper.kt @@ -118,22 +118,14 @@ class SwapContentSettingsMapper( this += token2022Fee.cellModel } - val liquidityFee = swapFeeBuilder.buildLiquidityFeeCell(route) - if (liquidityFee != null) { - this += liquidityFee.cellModel - } - - val keyAppFee = swapFeeBuilder.buildKeyAppFee(route, tokenB) - if (keyAppFee != null) { - this += keyAppFee.cellModel - } + val liquidityFee = swapFeeBuilder.buildLiquidityFeeCell(route, tokenB) + this += liquidityFee.cellModel val estimatedFee = swapFeeBuilder.buildEstimatedFeeString( networkFees = networkFeeCell, accountFee = accountFee, liquidityFees = liquidityFee, token2022Fee = token2022Fee, - keyAppFee = keyAppFee ) if (estimatedFee != null) { this += estimatedFee diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt index 0c46f4f18d..a43a0a2351 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/SwapFeeCellsBuilder.kt @@ -2,22 +2,21 @@ package org.p2p.wallet.jupiter.ui.settings.presenter import timber.log.Timber import java.math.BigDecimal -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.onEach +import java.math.BigInteger import kotlinx.coroutines.supervisorScope import org.p2p.core.common.DrawableContainer import org.p2p.core.common.TextContainer +import org.p2p.core.crypto.Base58String import org.p2p.core.utils.asUsdSwap import org.p2p.core.utils.divideSafe -import org.p2p.core.utils.formatFiat -import org.p2p.core.utils.formatToken import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.fromLamports +import org.p2p.core.utils.isNotZero import org.p2p.core.utils.isZero import org.p2p.core.utils.orZero +import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.model.TokenServicePrice +import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.uikit.components.finance_block.MainCellModel import org.p2p.uikit.components.finance_block.MainCellStyle import org.p2p.uikit.components.left_side.LeftSideCellModel @@ -26,11 +25,10 @@ import org.p2p.uikit.utils.image.ImageViewCellModel import org.p2p.uikit.utils.text.TextViewCellModel import org.p2p.wallet.R import org.p2p.wallet.jupiter.interactor.model.SwapTokenModel +import org.p2p.wallet.jupiter.repository.model.JupiterSwapRoutePlanV6 import org.p2p.wallet.jupiter.repository.model.JupiterSwapRouteV6 import org.p2p.wallet.jupiter.repository.model.JupiterSwapToken import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository -import org.p2p.wallet.jupiter.ui.main.SwapRateLoaderState -import org.p2p.wallet.jupiter.ui.main.SwapTokenRateLoader class SwapSettingsFeeBox( val cellModel: MainCellModel, @@ -38,20 +36,24 @@ class SwapSettingsFeeBox( ) class SwapFeeCellsBuilder( - private val rateLoader: SwapTokenRateLoader, + private val tokenServiceRepository: TokenServiceRepository, private val swapTokensRepository: JupiterSwapTokensRepository ) { + private class SwapFeeBuildFailed( + message: String, + cause: Throwable? = null + ) : Exception(message, cause) + suspend fun buildNetworkFeeCell( activeRoute: JupiterSwapRouteV6?, solToken: JupiterSwapToken, ): SwapSettingsFeeBox { - if (activeRoute != null) { - val networkFee = activeRoute.fees.signatureFee.fromLamports(solToken.decimals) - val solTokenRate: BigDecimal? = loadRateForToken(SwapTokenModel.JupiterToken(solToken))?.rate - val feeUsd: BigDecimal? = solTokenRate?.let { networkFee.multiply(it) } - // amount is not used, network fee is free right now - } + // amount is not used, network fee is free right now +// val networkFee = activeRoute.fees.signatureFee.fromLamports(solToken.decimals) +// val solTokenRate: BigDecimal? = loadRateForToken(SwapTokenModel.JupiterToken(solToken))?.rate +// val feeUsd: BigDecimal? = solTokenRate?.let { networkFee.multiply(it) } + val cellModel = MainCellModel( leftSideCellModel = LeftSideCellModel.IconWithText( firstLineText = TextViewCellModel.Raw( @@ -92,12 +94,12 @@ class SwapFeeCellsBuilder( return null } - val solRate = loadRateForToken(SwapTokenModel.JupiterToken(solToken))?.rate ?: kotlin.run { - Timber.e(IllegalStateException("Sol rate is null")) + val solRate = loadRateForToken(solToken.tokenMint)?.usdRate ?: kotlin.run { + Timber.e(SwapFeeBuildFailed("Sol rate is null")) return null } - val tokenBRate = loadRateForToken(tokenB)?.rate ?: kotlin.run { - Timber.e(IllegalStateException("Token B (${tokenB.mintAddress} rate is null")) + val tokenBRate = loadRateForToken(tokenB.mintAddress)?.usdRate ?: kotlin.run { + Timber.e(SwapFeeBuildFailed("Token B (${tokenB.mintAddress} rate is null")) return null } val ataFeeInTokenB: BigDecimal = solRate.divideSafe(tokenBRate) * ataFeeInSol @@ -136,24 +138,20 @@ class SwapFeeCellsBuilder( suspend fun buildLiquidityFeeCell( activeRoute: JupiterSwapRouteV6, - ): SwapSettingsFeeBox? = supervisorScope { - val lpTokensRates = activeRoute.routePlans.map { routePlan -> - val lpToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) ?: return@supervisorScope null - async { loadRateForToken(SwapTokenModel.JupiterToken(lpToken)) } - } - .awaitAll() - .filterNotNull() - - val feeAmountFormatted = formatLiquidityFeeString(activeRoute) - - val usdAmounts = activeRoute.routePlans.map { routePlan -> - val (lpToken, tokenRate) = lpTokensRates - .find { it.token.mintAddress == routePlan.feeMint } - ?: return@map null + tokenB: SwapTokenModel, + ): SwapSettingsFeeBox = supervisorScope { + val keyAppFee = activeRoute.fees.platformFeeTokenB - val amountLamports = routePlan.feeAmount.fromLamports(lpToken.decimals) - amountLamports * tokenRate - } + val feeAmountFormatted = formatLiquidityFeeString( + routePlans = activeRoute.routePlans, + keyAppFee = keyAppFee, + tokenB = tokenB + ) + val usdAmounts = getLiquidityFeeUsdRates( + routePlans = activeRoute.routePlans, + keyAppFee = keyAppFee, + tokenB = tokenB + ) val someAmountsUsdNotLoaded = usdAmounts.any { it == null } val liquidityFeeInUsd = if (someAmountsUsdNotLoaded) { @@ -186,72 +184,66 @@ class SwapFeeCellsBuilder( SwapSettingsFeeBox(cellModel, liquidityFeeInUsd) } - private suspend fun formatLiquidityFeeString(route: JupiterSwapRouteV6): String { + /** + * Append KeyApp fee to liquidity fee string + */ + private suspend fun formatLiquidityFeeString( + routePlans: List, + keyAppFee: BigInteger, + tokenB: SwapTokenModel + ): String { return buildString { - route.routePlans.forEachIndexed { index, routePlan -> - val lpFee = routePlan.feeAmount + routePlans.forEachIndexed { index, routePlan -> val lpToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) ?: return@forEachIndexed + val lpFee = routePlan.feeAmount val feeAmount = lpFee .fromLamports(lpToken.decimals) - .formatToken(lpToken.decimals) - val fee = "$feeAmount ${lpToken.tokenSymbol}" - append(fee) - if (index != route.routePlans.lastIndex) { + .formatTokenWithSymbol(lpToken.tokenSymbol, lpToken.decimals) + append(feeAmount) + if (index != routePlans.lastIndex) { append(", ") } } + if (keyAppFee.isNotZero()) { + append(", ") + val feeAmount = keyAppFee + .fromLamports(tokenB.decimals) + .formatTokenWithSymbol(tokenB.tokenSymbol, tokenB.decimals) + append(feeAmount) + } } } - suspend fun buildKeyAppFee(activeRoute: JupiterSwapRouteV6, tokenB: SwapTokenModel): SwapSettingsFeeBox? { - val platformFee = activeRoute.fees.platformFeeTokenB - if (platformFee.isZero()) { - return null - } - - val tokenBRate = loadRateForToken(tokenB)?.rate - - val platformFeeUsd = activeRoute.fees.platformFeeTokenB - .fromLamports(tokenB.decimals) - .multiply(tokenBRate.orZero()) + private suspend fun getLiquidityFeeUsdRates( + routePlans: List, + keyAppFee: BigInteger, + tokenB: SwapTokenModel, + ): List { + val tokenBRate = loadRateForToken(tokenB.mintAddress)?.usdRate + val keyAppFeeUsd = tokenBRate?.let { it * keyAppFee.fromLamports(tokenB.decimals) } - val formattedPlatformFee = "${activeRoute.fees.platformFeePercent.formatFiat()} %" + return routePlans.map { routePlan -> + val lpToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) ?: return@map null + val usdRate = loadRateForToken(lpToken.tokenMint)?.usdRate ?: return@map null + val amountLamports = routePlan.feeAmount.fromLamports(lpToken.decimals) - val cellModel = MainCellModel( - leftSideCellModel = LeftSideCellModel.IconWithText( - firstLineText = TextViewCellModel.Raw( - text = TextContainer("KeyApp Fee"), - ), - secondLineText = TextViewCellModel.Raw( - text = TextContainer(formattedPlatformFee) - ), - ), - rightSideCellModel = null, - styleType = MainCellStyle.BASE_CELL, - ) - return SwapSettingsFeeBox( - cellModel = cellModel, - feeInUsd = platformFeeUsd - ) + amountLamports * usdRate + } + .plus(keyAppFeeUsd) } fun buildEstimatedFeeString( networkFees: SwapSettingsFeeBox, accountFee: SwapSettingsFeeBox?, - liquidityFees: SwapSettingsFeeBox?, + liquidityFees: SwapSettingsFeeBox, token2022Fee: SwapSettingsFeeBox?, - keyAppFee: SwapSettingsFeeBox?, ): MainCellModel? { - if (liquidityFees?.feeInUsd == null) { - return null - } - - val totalFee: String = liquidityFees.feeInUsd + val totalFee = liquidityFees.feeInUsd.orZero() .plus(accountFee?.feeInUsd.orZero()) .plus(token2022Fee?.feeInUsd.orZero()) - .plus(keyAppFee?.feeInUsd.orZero()) - .asUsdSwap() + + if (totalFee.isZero()) return null return MainCellModel( leftSideCellModel = LeftSideCellModel.IconWithText( @@ -261,19 +253,21 @@ class SwapFeeCellsBuilder( ), ), rightSideCellModel = RightSideCellModel.SingleTextTwoIcon( - text = totalFee.let { - TextViewCellModel.Raw(text = TextContainer(it)) - }, + text = TextViewCellModel.Raw(text = TextContainer(totalFee.asUsdSwap())) ), payload = SwapSettingsPayload.ESTIMATED_FEE, styleType = MainCellStyle.BASE_CELL, ) } - private suspend fun loadRateForToken(token: SwapTokenModel): SwapRateLoaderState.Loaded? { - return rateLoader.getRate(token) - .onEach { Timber.i("JupiterSwapFeeBuilder getting rate for ${token.tokenSymbol}") } - .filterIsInstance() - .firstOrNull() + private suspend fun loadRateForToken(tokenMint: Base58String): TokenServicePrice? { + return kotlin.runCatching { + tokenServiceRepository.getTokenPriceByAddress( + tokenAddress = tokenMint.base58Value, + networkChain = TokenServiceNetwork.SOLANA + ) + } + .onFailure { Timber.e(SwapFeeBuildFailed("Failed to get rate for $tokenMint", it)) } + .getOrNull() } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/tokens/presenter/SwapTokensCommonMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/tokens/presenter/SwapTokensCommonMapper.kt index 24d96f00e1..24417726d3 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/tokens/presenter/SwapTokensCommonMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/tokens/presenter/SwapTokensCommonMapper.kt @@ -82,6 +82,7 @@ class SwapTokensCommonMapper( tokenName = tokenName, tokenSymbol = tokenSymbol, tokenMint = mintAddress, + tokenDecimals = decimals, totalTokenAmount = total, totalTokenPriceInUsd = totalInUsd?.formatFiat(), payload = SwapTokensCellModelPayload( @@ -104,6 +105,7 @@ class SwapTokensCommonMapper( tokenName = tokenName, tokenSymbol = tokenSymbol, tokenMint = tokenMint.base58Value, + tokenDecimals = decimals, totalTokenAmount = null, totalTokenPriceInUsd = null, addPopularLabel = isPopularToken, @@ -121,6 +123,7 @@ class SwapTokensCommonMapper( tokenName: String, tokenSymbol: String, tokenMint: String, + tokenDecimals: Int, totalTokenAmount: BigDecimal?, totalTokenPriceInUsd: String?, payload: SwapTokensCellModelPayload, @@ -138,7 +141,12 @@ class SwapTokensCommonMapper( rightSideCellModel = if (addPopularLabel) { createPopularRightSideModel() } else { - createPriceRightSideModel(totalTokenAmount, tokenSymbol, totalTokenPriceInUsd) + createPriceRightSideModel( + totalTokenAmount = totalTokenAmount, + tokenSymbol = tokenSymbol, + tokenDecimals = tokenDecimals, + totalTokenPriceInUsd = totalTokenPriceInUsd, + ) }, payload = payload ) @@ -195,6 +203,7 @@ class SwapTokensCommonMapper( private fun createPriceRightSideModel( totalTokenAmount: BigDecimal?, tokenSymbol: String, + tokenDecimals: Int, totalTokenPriceInUsd: String? ): RightSideCellModel.TwoLineText? { val usdAmount = totalTokenPriceInUsd?.let { @@ -204,7 +213,7 @@ class SwapTokensCommonMapper( } val tokenAmount = totalTokenAmount?.let { TextViewCellModel.Raw( - TextContainer.Raw(it.formatTokenWithSymbol(tokenSymbol)), + TextContainer.Raw(it.formatTokenWithSymbol(tokenSymbol, tokenDecimals)), ) } diff --git a/app/src/main/java/org/p2p/wallet/restore/interactor/SeedPhraseInteractor.kt b/app/src/main/java/org/p2p/wallet/restore/interactor/SeedPhraseInteractor.kt index 03ddf7e525..c9b63ad491 100644 --- a/app/src/main/java/org/p2p/wallet/restore/interactor/SeedPhraseInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/restore/interactor/SeedPhraseInteractor.kt @@ -7,23 +7,23 @@ import org.bitcoinj.crypto.MnemonicException import timber.log.Timber import java.math.BigInteger import kotlinx.coroutines.withContext +import org.p2p.core.crypto.Base58String +import org.p2p.core.crypto.toBase58Instance +import org.p2p.core.dispatchers.CoroutineDispatchers +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.fromLamports -import org.p2p.core.utils.scaleLong import org.p2p.solanaj.core.Account +import org.p2p.solanaj.core.toBase58Instance import org.p2p.solanaj.crypto.DerivationPath import org.p2p.uikit.organisms.seedphrase.SeedPhraseWord import org.p2p.wallet.auth.analytics.AdminAnalytics import org.p2p.wallet.auth.interactor.UsernameInteractor import org.p2p.wallet.auth.repository.AuthRepository -import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.restore.model.DerivableAccount import org.p2p.wallet.restore.model.SeedPhraseVerifyResult import org.p2p.wallet.rpc.repository.balance.RpcBalanceRepository -import org.p2p.core.crypto.Base58String import org.p2p.wallet.utils.mnemoticgenerator.English -import org.p2p.core.crypto.toBase58Instance -import org.p2p.solanaj.core.toBase58Instance // duck-taped, extract to storage some day const val KEY_IS_AUTH_BY_SEED_PHRASE = "KEY_IS_AUTH_BY_SEED_PHRASE" @@ -71,15 +71,15 @@ class SeedPhraseInteractor( balances: List>, path: DerivationPath ): List = accounts.mapNotNull { account -> - val balance = balances.find { it.first == account.publicKey.toBase58() } + val solBalance = balances.find { it.first == account.publicKey.toBase58() } ?.second ?: return@mapNotNull null - val total = balance.fromLamports().scaleLong() + val totalSol = solBalance.fromLamports(SOL_DECIMALS) DerivableAccount( path = path, account = account, - totalInSol = total + totalInSol = totalSol ) } diff --git a/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsPresenter.kt b/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsPresenter.kt index 004ed5f31b..30449f1204 100644 --- a/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsPresenter.kt @@ -3,6 +3,7 @@ package org.p2p.wallet.restore.ui.derivable import timber.log.Timber import kotlin.properties.Delegates.observable import kotlinx.coroutines.launch +import org.p2p.core.analytics.constants.ScreenNames import org.p2p.core.utils.Constants import org.p2p.solanaj.crypto.DerivationPath import org.p2p.token.service.model.TokenServiceNetwork @@ -11,7 +12,6 @@ import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.wallet.auth.analytics.OnboardingAnalytics import org.p2p.wallet.auth.analytics.RestoreWalletAnalytics import org.p2p.wallet.auth.analytics.RestoreWalletAnalytics.UsernameRestoreMethod -import org.p2p.core.analytics.constants.ScreenNames import org.p2p.wallet.common.mvp.BasePresenter import org.p2p.wallet.restore.interactor.SeedPhraseInteractor import org.p2p.wallet.restore.model.DerivableAccount @@ -42,9 +42,10 @@ class DerivableAccountsPresenter( launch { try { val tokenAddress = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN - val solRate = tokenServiceRepository.fetchTokenPriceByAddress( + val solRate = tokenServiceRepository.getTokenPriceByAddress( networkChain = TokenServiceNetwork.SOLANA, - tokenAddress = tokenAddress + tokenAddress = tokenAddress, + forceRemote = true ).also { solRate = it } ?: return@launch allAccounts = allAccounts.updateWithTotalInUsd(solRate).toMutableList() diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt b/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt index cb72e42f50..0b89b947df 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt @@ -6,6 +6,7 @@ import java.math.BigDecimal import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.p2p.core.model.TextHighlighting +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.asApproximateUsd import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.orZero @@ -112,7 +113,7 @@ class SendFeeTotal constructor( get() = receiveUsd?.asApproximateUsd().orEmpty() private val totalWithSymbolFormatted: String - get() = currentAmount.formatTokenWithSymbol(sourceSymbol) + get() = currentAmount.formatTokenWithSymbol(sourceSymbol, SOL_DECIMALS) private val totalSumWithSymbol: String get() { @@ -130,6 +131,6 @@ class SendFeeTotal constructor( } } .plus(transferFee.orZero()) - return totalSum.formatTokenWithSymbol(sourceSymbol) + return totalSum.formatTokenWithSymbol(sourceSymbol, SOL_DECIMALS) } } diff --git a/app/src/main/java/org/p2p/wallet/svl/interactor/ReceiveViaLinkInteractor.kt b/app/src/main/java/org/p2p/wallet/svl/interactor/ReceiveViaLinkInteractor.kt index 1110c76ab9..ba77fb0e7d 100644 --- a/app/src/main/java/org/p2p/wallet/svl/interactor/ReceiveViaLinkInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/svl/interactor/ReceiveViaLinkInteractor.kt @@ -124,16 +124,11 @@ class ReceiveViaLinkInteractor( } private suspend fun fetchPriceForToken(mintAddress: String): TokenServicePrice? { - val price = tokenServiceRepository.findTokenPriceByAddress( - tokenAddress = mintAddress, - networkChain = TokenServiceNetwork.SOLANA - ) - if (price != null) return price - return kotlin.runCatching { - tokenServiceRepository.fetchTokenPriceByAddress( + tokenServiceRepository.getTokenPriceByAddress( + tokenAddress = mintAddress, networkChain = TokenServiceNetwork.SOLANA, - tokenAddress = mintAddress + forceRemote = true ) } .onFailure { Timber.i(it) } diff --git a/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt b/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt index 6a57d63d84..a184e857a0 100644 --- a/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt +++ b/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt @@ -1,8 +1,11 @@ package org.p2p.wallet.swap.model.orca +import java.math.BigDecimal +import java.math.BigInteger import kotlinx.parcelize.IgnoredOnParcel import org.p2p.core.token.Token import org.p2p.core.utils.Constants.SOL_SYMBOL +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.asApproximateUsd import org.p2p.core.utils.formatToken import org.p2p.core.utils.fromLamports @@ -13,8 +16,6 @@ import org.p2p.core.utils.toUsd import org.p2p.wallet.feerelayer.model.FeePayerSelectionStrategy import org.p2p.wallet.send.model.FeePayerState import org.p2p.wallet.swap.model.FeeRelayerSwapFee -import java.math.BigDecimal -import java.math.BigInteger class SwapFee constructor( private val fee: FeeRelayerSwapFee, @@ -70,7 +71,7 @@ class SwapFee constructor( get() = fee.feeInPayingToken.fromLamports(feePayerToken.decimals).scaleMedium() val feeAmountInSol: BigDecimal - get() = fee.feeInSol.fromLamports().scaleMedium() + get() = fee.feeInSol.fromLamports(SOL_DECIMALS).scaleMedium() val feePayerSymbol: String = feePayerToken.tokenSymbol diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt index 1d50c9e886..524b1f619a 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt @@ -10,9 +10,9 @@ import org.p2p.solanaj.core.PublicKey import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.wallet.home.model.TokenConverter +import org.p2p.wallet.rpc.repository.balance.RpcBalanceRepository import org.p2p.wallet.send.model.SearchResult import org.p2p.wallet.send.repository.RecipientsLocalRepository -import org.p2p.wallet.rpc.repository.balance.RpcBalanceRepository import org.p2p.wallet.user.repository.UserLocalRepository import org.p2p.wallet.user.repository.UserTokensLocalRepository import org.p2p.wallet.utils.emptyString @@ -33,7 +33,7 @@ class UserInteractor( suspend fun findTokenData(mintAddress: String): Token? { val tokenData = userLocalRepository.findTokenData(mintAddress) val price = tokenData?.let { - tokenServiceRepository.findTokenPriceByAddress( + tokenServiceRepository.getTokenPriceByAddress( tokenAddress = it.mintAddress, networkChain = TokenServiceNetwork.SOLANA ) @@ -94,7 +94,7 @@ class UserInteractor( private suspend fun findTokenDataBySymbol(symbol: String): Token.Other? { val tokenData = userLocalRepository.findTokenDataBySymbol(symbol) val price = tokenData?.let { - tokenServiceRepository.findTokenPriceByAddress( + tokenServiceRepository.getTokenPriceByAddress( tokenAddress = it.mintAddress, networkChain = TokenServiceNetwork.SOLANA ) diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt index 8f22d7aa4d..f682288cf8 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt @@ -135,9 +135,10 @@ class UserTokensInteractor( val tokenMetadataData = userLocalRepository.findTokenData(mintAddress.base58Value) ?: return null - val price = tokenServiceRepository.fetchTokenPriceByAddress( + val price = tokenServiceRepository.getTokenPriceByAddress( networkChain = TokenServiceNetwork.SOLANA, - tokenAddress = mintAddress.base58Value + tokenAddress = mintAddress.base58Value, + forceRemote = true ) return TokenConverter.createToken( diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt index 445468cbc0..7f207e0526 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt @@ -136,7 +136,7 @@ class UserInMemoryRepository( override suspend fun findTokenByMint(mintAddress: String): Token? { val tokenMetadata: TokenMetadata? = findTokenData(mintAddress) return if (tokenMetadata != null) { - val price = tokenServiceRepository.findTokenPriceByAddress( + val price = tokenServiceRepository.getTokenPriceByAddress( tokenAddress = tokenMetadata.mintAddress, networkChain = TokenServiceNetwork.SOLANA ) diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt index 52b62290fb..98f77cfafc 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt @@ -69,7 +69,7 @@ class UserTokensDatabaseRepository( val oldTokens = getUserTokens() val newTokens = oldTokens.map { token -> - val newTokenRate = prices.firstOrNull { token.tokenServiceAddress == it.address } + val newTokenRate = prices.firstOrNull { token.tokenServiceAddress == it.tokenAddress } val oldTokenRate = token.rate val tokenRate = newTokenRate?.usdRate ?: oldTokenRate token.copy(rate = tokenRate, totalInUsd = tokenRate?.let { token.total.times(it) }) diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt index 1ab368810c..9b840cbd47 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt @@ -41,7 +41,7 @@ class UserTokensRemoteRepository( val tokenData = userLocalRepository.findTokenData(mintAddress) ?: return@mapNotNull null if (tokenData.decimals == NFT_DECIMALS) return@mapNotNull null - val tokenPrice = tokenServiceRepository.findTokenPriceByAddress( + val tokenPrice = tokenServiceRepository.getTokenPriceByAddress( tokenAddress = tokenData.mintAddress, networkChain = TokenServiceNetwork.SOLANA ) @@ -58,7 +58,7 @@ class UserTokensRemoteRepository( * */ val solBalance = rpcBalanceRepository.getBalance(publicKey) val tokenData = userLocalRepository.findTokenData(Constants.WRAPPED_SOL_MINT) ?: return tokens - val solPrice = tokenServiceRepository.findTokenPriceByAddress( + val solPrice = tokenServiceRepository.getTokenPriceByAddress( tokenAddress = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN, networkChain = TokenServiceNetwork.SOLANA ) diff --git a/app/src/main/res/values/swap_strings.xml b/app/src/main/res/values/swap_strings.xml index 9f7a1ba92a..db1a7d8901 100644 --- a/app/src/main/res/values/swap_strings.xml +++ b/app/src/main/res/values/swap_strings.xml @@ -52,8 +52,8 @@ Estimated fees - Interest bearing - Transfer fee + Token-2022 interest bearing + Token-2022 transfer fee Minimum received %s %s @@ -101,7 +101,8 @@ Interest bearing %s Liquidity fee %s - + Swap fee + The token %s is out of the strict list Make sure the mint address for %s %s is correct before confirming Confirm selection diff --git a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt index 21cfaea3aa..2b263376c3 100644 --- a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt +++ b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenterBaseTest.kt @@ -277,7 +277,7 @@ open class JupiterSwapPresenterBaseTest { tokenRates: (List) -> Map, ) { coEvery { jupiterSwapTokensRepository.getTokens() } returns allTokens - coEvery { tokenServiceRepository.loadPriceForTokens(any(), any()) } answers { + coEvery { tokenServiceRepository.fetchTokenPricesForTokens(any(), any()) } answers { val tokens: List = arg(0) tokenRates(tokens) } diff --git a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt index f4aa1a7eae..3344c62aee 100644 --- a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt +++ b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapTestHelpers.kt @@ -201,7 +201,9 @@ object JupiterSwapTestHelpers { totalFees = BigInteger.ZERO, platformFeePercent = BigDecimal.ZERO, platformFeeTokenB = BigInteger.ZERO - ) + ), + inTokenMint = data.inputMint, + outTokenMint = data.outputMint ) } diff --git a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterTestPresenterBuilder.kt b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterTestPresenterBuilder.kt index 7887265a60..561fdb047b 100644 --- a/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterTestPresenterBuilder.kt +++ b/app/src/test/java/org/p2p/wallet/jupiter/ui/main/JupiterTestPresenterBuilder.kt @@ -102,7 +102,7 @@ class JupiterTestPresenterBuilder { else -> null } - }.associateBy { it.address.toBase58Instance() } + }.associateBy { it.tokenAddress.toBase58Instance() } } /** diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index e6a13457bf..691ef0c3c5 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -3,12 +3,11 @@ package org.p2p.core.utils import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode -import kotlin.math.pow import org.p2p.core.token.Token import org.p2p.core.utils.Constants.FIAT_FRACTION_LENGTH -private const val POWER_VALUE = 10.0 -const val DEFAULT_DECIMAL = 9 +private val POWER_VALUE = BigDecimal("10") +const val SOL_DECIMALS = 9 const val MOONPAY_DECIMAL = 2 const val STRIGA_FIAT_DECIMALS = 2 @@ -27,8 +26,7 @@ fun String?.toBigIntegerOrZero(): BigInteger { return this?.toBigIntegerOrNull() ?: BigInteger.ZERO } -fun Int.toPowerValue(): BigDecimal = - BigDecimal(POWER_VALUE.pow(this)) +fun Int.toPowerValue(): BigDecimal = POWER_VALUE.pow(this) fun BigDecimal.scaleShortOrFirstNotZero(): BigDecimal { return if (isZero()) { @@ -56,8 +54,10 @@ fun BigDecimal.scaleLong(decimals: Int = SCALE_VALUE_LONG): BigDecimal = if (this.isZero()) this else this.setScale(decimals, RoundingMode.DOWN) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 -fun BigInteger.fromLamports(decimals: Int = DEFAULT_DECIMAL): BigDecimal = - BigDecimal(this.toDouble() / (POWER_VALUE.pow(decimals))) +// do not use BigDecimal(double) sometimes it makes the amount less +// example: pass 0.030, get 0.029 +fun BigInteger.fromLamports(decimals: Int): BigDecimal = + (this.toBigDecimal() / (POWER_VALUE.pow(decimals))) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 .scaleLong(decimals) @@ -77,7 +77,7 @@ fun BigDecimal.formatFiat(): String = formatWithDecimals(FIAT_FRACTION_LENGTH) // case 2: 1.0 -> 1 - default behavior // case 3: 1.0 -> 1.0 -> with keepInitialDecimals=true fun BigDecimal.formatToken( - decimals: Int = DEFAULT_DECIMAL, + decimals: Int = SOL_DECIMALS, exactDecimals: Boolean = false, keepInitialDecimals: Boolean = false, ): String = formatWithDecimals( @@ -88,7 +88,7 @@ fun BigDecimal.formatToken( fun BigDecimal.formatTokenWithSymbol( tokenSymbol: String, - decimals: Int = DEFAULT_DECIMAL, + decimals: Int, exactDecimals: Boolean = false, keepInitialDecimals: Boolean = false, ): String { @@ -133,20 +133,20 @@ fun BigDecimal.formatWithDecimals( } fun BigDecimal?.isNullOrZero(): Boolean = this == null || this.compareTo(BigDecimal.ZERO) == 0 -fun BigDecimal.isZero() = this.compareTo(BigDecimal.ZERO) == 0 -fun BigDecimal.isNotZero() = this.compareTo(BigDecimal.ZERO) != 0 -fun BigDecimal.isMoreThan(value: BigDecimal) = this.compareTo(value) == 1 -fun BigDecimal.isLessThan(value: BigDecimal) = this.compareTo(value) == -1 -fun BigDecimal.isZeroOrLess() = isZero() || isLessThan(BigDecimal.ZERO) +fun BigDecimal.isZero(): Boolean = this.compareTo(BigDecimal.ZERO) == 0 +fun BigDecimal.isNotZero(): Boolean = this.compareTo(BigDecimal.ZERO) != 0 +fun BigDecimal.isMoreThan(value: BigDecimal): Boolean = this.compareTo(value) == 1 +fun BigDecimal.isLessThan(value: BigDecimal): Boolean = this.compareTo(value) == -1 +fun BigDecimal.isZeroOrLess(): Boolean = isZero() || isLessThan(BigDecimal.ZERO) fun BigDecimal?.orZero(): BigDecimal = this ?: BigDecimal.ZERO fun BigInteger?.orZero(): BigInteger = this ?: BigInteger.ZERO -fun BigInteger.isZero() = this.compareTo(BigInteger.ZERO) == 0 -fun BigInteger.isNotZero() = this.compareTo(BigInteger.ZERO) != 0 +fun BigInteger.isZero(): Boolean = this.compareTo(BigInteger.ZERO) == 0 +fun BigInteger.isNotZero(): Boolean = this.compareTo(BigInteger.ZERO) != 0 fun BigInteger.isLessThan(value: BigInteger) = this.compareTo(value) == -1 fun BigInteger.isMoreThan(value: BigInteger) = this.compareTo(value) == 1 -fun BigInteger.isZeroOrLess() = isZero() || isLessThan(BigInteger.ZERO) +fun BigInteger.isZeroOrLess(): Boolean = isZero() || isLessThan(BigInteger.ZERO) fun BigDecimal.asCurrency(currencyUiSymbol: String): String = when { isZero() -> "${currencyUiSymbol}0" @@ -185,6 +185,6 @@ fun BigDecimal.asUsdSwap(): String = when { fun Int?.orZero(): Int = this ?: 0 // value is in (0..0.01) -fun BigDecimal.lessThenMinValue() = !isZero() && isLessThan(AMOUNT_MIN_VALUE.toBigDecimal()) -fun BigDecimal.moreThenMinValue() = isMoreThan(AMOUNT_MIN_VALUE.toBigDecimal()) +fun BigDecimal.lessThenMinValue(): Boolean = !isZero() && isLessThan(AMOUNT_MIN_VALUE.toBigDecimal()) +fun BigDecimal.moreThenMinValue(): Boolean = isMoreThan(AMOUNT_MIN_VALUE.toBigDecimal()) fun BigDecimal.divideByInt(byInt: Int): BigDecimal = divide(byInt.toBigDecimal()) diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt index 9bc23c7e81..75ced37c79 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt @@ -15,8 +15,8 @@ import org.p2p.ethereumkit.external.model.EthTokenConverter import org.p2p.ethereumkit.external.model.EthTokenKeyProvider import org.p2p.ethereumkit.external.model.EthTokenMetadata import org.p2p.ethereumkit.external.model.EthereumClaimToken -import org.p2p.ethereumkit.external.token.EthereumTokensLocalRepository import org.p2p.ethereumkit.external.token.EthereumTokenRepository +import org.p2p.ethereumkit.external.token.EthereumTokensLocalRepository import org.p2p.ethereumkit.internal.core.TransactionSignerEip1559 import org.p2p.ethereumkit.internal.core.TransactionSignerLegacy import org.p2p.ethereumkit.internal.core.signer.Signer @@ -131,7 +131,7 @@ internal class EthereumKitRepository( this += erc20TokensAddresses } // Fetch tokens metadata - val tokensMetadata = tokenServiceRepository.loadMetadataForTokens( + val tokensMetadata = tokenServiceRepository.fetchMetadataForTokens( chain = TokenServiceNetwork.ETHEREUM, tokenAddresses = allTokensAddresses ) diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt index a5a79a78f1..fa3c7a02d3 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt @@ -25,14 +25,14 @@ class EthereumTokensInMemoryRepository : EthereumTokensLocalRepository { override suspend fun updateTokensRate(tokensRate: List) { val newTokenRates = buildList { - val nativePrice = tokensRate.firstOrNull { it.address == ERC20Tokens.ETH.contractAddress } + val nativePrice = tokensRate.firstOrNull { it.tokenAddress == ERC20Tokens.ETH.contractAddress } if (nativePrice != null) { - this += nativePrice.copy(address = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) + this += nativePrice.copy(tokenAddress = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) } this += tokensRate } cachedTokens.value = cachedTokens.value.map { token -> - val foundTokenRate = newTokenRates.firstOrNull { it.address == token.tokenServiceAddress } + val foundTokenRate = newTokenRates.firstOrNull { it.tokenAddress == token.tokenServiceAddress } val totalInUsd = foundTokenRate?.usdRate?.let { token.total.times(it) } token.copy(totalInUsd = totalInUsd, rate = foundTokenRate?.usdRate) } diff --git a/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt b/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt index 70ad046c0d..31e47730f7 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt @@ -6,8 +6,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.launch import org.p2p.core.dispatchers.CoroutineDispatchers -import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.repository.TokenServiceRepository class TokenServiceEventPublisher( private val tokenServiceInteractor: TokenServiceRepository, @@ -27,7 +27,7 @@ class TokenServiceEventPublisher( eventType = TokenServiceEventType.from(networkChain), data = TokenServiceUpdate.Loading ) - tokenServiceInteractor.loadPriceForTokens( + tokenServiceInteractor.fetchTokenPricesForTokens( chain = networkChain, tokenAddresses = addresses ) diff --git a/token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt b/token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt index 4becaf47e6..4854d20cbe 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt @@ -34,7 +34,7 @@ class TokenServiceMapper { ): TokenServicePrice? { val tokenRate = response.price ?: return null return TokenServicePrice( - address = response.tokenAddress, + tokenAddress = response.tokenAddress, rate = fromNetwork(tokenRate), network = tokenServiceNetwork ) diff --git a/token-service/src/main/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapper.kt b/token-service/src/main/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapper.kt index ffbea25533..b1a0fd7cf9 100644 --- a/token-service/src/main/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapper.kt +++ b/token-service/src/main/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapper.kt @@ -11,7 +11,7 @@ internal class TokenServiceDatabaseMapper { val tokenRate = item.rate val usdRate = tokenRate.usd ?: return null return TokenServicePriceEntity( - tokenAddress = item.address, + tokenAddress = item.tokenAddress, networkChainName = item.network.networkName, usdRate = usdRate ) @@ -19,7 +19,7 @@ internal class TokenServiceDatabaseMapper { fun fromEntity(entity: TokenServicePriceEntity): TokenServicePrice { return TokenServicePrice( - address = entity.tokenAddress, + tokenAddress = entity.tokenAddress, rate = TokenRate( usd = entity.usdRate ), diff --git a/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt b/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt index b2c4c17fa8..20450135fb 100644 --- a/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt +++ b/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt @@ -4,7 +4,7 @@ import java.math.BigDecimal import org.p2p.core.utils.scaleShort data class TokenServicePrice( - val address: String, + val tokenAddress: String, val rate: TokenRate, val network: TokenServiceNetwork ) { @@ -15,6 +15,6 @@ data class TokenServicePrice( get() = rate.usd override fun toString(): String { - return "TokenServicePrice($address - ${rate.usd} - ${network.networkName})" + return "TokenServicePrice($tokenAddress - ${rate.usd} - ${network.networkName})" } } diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt index 815c0996ed..636eda2728 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt @@ -6,14 +6,23 @@ import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice interface TokenServiceRepository { - suspend fun loadPriceForTokens(chain: TokenServiceNetwork, tokenAddresses: List) - suspend fun loadMetadataForTokens( + suspend fun fetchTokenPricesForTokens( + chain: TokenServiceNetwork, + tokenAddresses: List + ) + suspend fun fetchMetadataForTokens( chain: TokenServiceNetwork, tokenAddresses: List ): List - suspend fun observeTokenPricesFlow(networkChain: TokenServiceNetwork): Flow> - suspend fun findTokenPriceByAddress(tokenAddress: String, networkChain: TokenServiceNetwork): TokenServicePrice? - suspend fun fetchTokenPriceByAddress(networkChain: TokenServiceNetwork, tokenAddress: String): TokenServicePrice? - fun findTokenMetadataByAddress(networkChain: TokenServiceNetwork, tokenAddress: String): TokenServiceMetadata? + fun observeTokenPricesFlow(networkChain: TokenServiceNetwork): Flow> + suspend fun getTokenPriceByAddress( + tokenAddress: String, + networkChain: TokenServiceNetwork, + forceRemote: Boolean = false + ): TokenServicePrice? + fun findTokenMetadataByAddress( + networkChain: TokenServiceNetwork, + tokenAddress: String + ): TokenServiceMetadata? } diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt index 2055a9afbc..6da967b328 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt @@ -16,7 +16,7 @@ internal class TokenServiceRepositoryImpl( private val metadataRemoteRepository: TokenMetadataRepository ) : TokenServiceRepository { - override suspend fun loadPriceForTokens(chain: TokenServiceNetwork, tokenAddresses: List) { + override suspend fun fetchTokenPricesForTokens(chain: TokenServiceNetwork, tokenAddresses: List) { val result = priceRemoteRepository.loadTokensPrice( chain = chain, addresses = tokenAddresses @@ -25,7 +25,7 @@ internal class TokenServiceRepositoryImpl( priceLocalRepository.saveTokensPrice(tokensPrices) } - override suspend fun loadMetadataForTokens( + override suspend fun fetchMetadataForTokens( chain: TokenServiceNetwork, tokenAddresses: List ): List { @@ -38,22 +38,20 @@ internal class TokenServiceRepositoryImpl( return tokensMetadata } - override suspend fun observeTokenPricesFlow(networkChain: TokenServiceNetwork): Flow> = + override fun observeTokenPricesFlow(networkChain: TokenServiceNetwork): Flow> = priceLocalRepository.observeTokenPrices(networkChain) - override suspend fun findTokenPriceByAddress( + override suspend fun getTokenPriceByAddress( tokenAddress: String, - networkChain: TokenServiceNetwork - ): TokenServicePrice? { - return priceLocalRepository.findTokenPriceByAddress(address = tokenAddress, networkChain = networkChain) - } - - override suspend fun fetchTokenPriceByAddress( networkChain: TokenServiceNetwork, - tokenAddress: String + forceRemote: Boolean ): TokenServicePrice? { - loadPriceForTokens(chain = networkChain, tokenAddresses = listOf(tokenAddress)) - return findTokenPriceByAddress(tokenAddress = tokenAddress, networkChain = networkChain) + val localPrice = priceLocalRepository.findTokenPriceByAddress(tokenAddress, networkChain) + if (forceRemote || localPrice == null) { + fetchTokenPricesForTokens(networkChain, listOf(tokenAddress)) + } + + return priceLocalRepository.findTokenPriceByAddress(tokenAddress, networkChain) } override fun findTokenMetadataByAddress( diff --git a/token-service/src/test/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapperTest.kt b/token-service/src/test/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapperTest.kt index e8a9494843..9ff61b0dca 100644 --- a/token-service/src/test/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapperTest.kt +++ b/token-service/src/test/java/org/p2p/token/service/database/mapper/TokenServiceDatabaseMapperTest.kt @@ -38,7 +38,7 @@ class TokenServiceDatabaseMapperTest { // THEN val expectedDomainModel = TokenServicePrice( - address = Constants.SOL_MINT, + tokenAddress = Constants.SOL_MINT, rate = TokenRate(usd = usdRate), network = network ) @@ -69,7 +69,7 @@ class TokenServiceDatabaseMapperTest { val usdRate = BigDecimal.TEN val network = TokenServiceNetwork.SOLANA val domain = TokenServicePrice( - address = Constants.SOL_MINT, + tokenAddress = Constants.SOL_MINT, network = network, rate = TokenRate(usd = usdRate) ) @@ -92,7 +92,7 @@ class TokenServiceDatabaseMapperTest { // GIVEN val network = TokenServiceNetwork.SOLANA val domain = TokenServicePrice( - address = Constants.SOL_MINT, + tokenAddress = Constants.SOL_MINT, network = network, rate = TokenRate(usd = null) ) diff --git a/token-service/src/test/java/org/p2p/token/service/repository/price/TokenPriceDatabaseRepositoryTest.kt b/token-service/src/test/java/org/p2p/token/service/repository/price/TokenPriceDatabaseRepositoryTest.kt index 2f0d60a07b..a938c6ed95 100644 --- a/token-service/src/test/java/org/p2p/token/service/repository/price/TokenPriceDatabaseRepositoryTest.kt +++ b/token-service/src/test/java/org/p2p/token/service/repository/price/TokenPriceDatabaseRepositoryTest.kt @@ -112,7 +112,7 @@ class TokenPriceDatabaseRepositoryTest { TokenServiceNetwork.ETHEREUM } this += TokenServicePrice( - address = Constants.SOL_MINT + it, + tokenAddress = Constants.SOL_MINT + it, rate = TokenRate(usd = it.toLong().toBigDecimal()), network = randomNetwork, ) From b2d545dc2260e40b37dc83c03ba5bf18c30ed56c Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:47:00 +0400 Subject: [PATCH 25/51] ETH-930 - disable svl deeplink handling (#2192) --- .../java/org/p2p/wallet/deeplinks/AppDeeplinksManager.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/p2p/wallet/deeplinks/AppDeeplinksManager.kt b/app/src/main/java/org/p2p/wallet/deeplinks/AppDeeplinksManager.kt index a514abdd50..f4b6b2250a 100644 --- a/app/src/main/java/org/p2p/wallet/deeplinks/AppDeeplinksManager.kt +++ b/app/src/main/java/org/p2p/wallet/deeplinks/AppDeeplinksManager.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onCompletion import org.p2p.wallet.R +import org.p2p.wallet.common.feature_toggles.toggles.remote.SendViaLinkFeatureToggle import org.p2p.wallet.intercom.IntercomDeeplinkManager import org.p2p.wallet.notification.NotificationType import org.p2p.wallet.root.RootActivity @@ -25,7 +26,8 @@ class AppDeeplinksManager( private val context: Context, private val swapDeeplinkHandler: SwapDeeplinkHandler, private val referralDeeplinkHandler: ReferralDeeplinkHandler, - private val intercomDeeplinkManager: IntercomDeeplinkManager + private val intercomDeeplinkManager: IntercomDeeplinkManager, + private val sendViaLinkFeatureToggle: SendViaLinkFeatureToggle ) { companion object { const val NOTIFICATION_TYPE = "eventType" @@ -114,6 +116,8 @@ class AppDeeplinksManager( * or keyapp://t/... if came from website */ private fun isTransferDeeplink(data: Uri): Boolean { + if (!sendViaLinkFeatureToggle.isFeatureEnabled) return false + val transferHostMain = context.getString(R.string.transfer_app_host) val transferSchemeMain = "https" val transferHostAlternative = "t" From 5dd3763b7649e11d99751da4c057162435f70ba9 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:09:02 +0400 Subject: [PATCH 26/51] ETH-951 - display full amount in send button (#2193) * ETH-951 - display full amount in send button * ETH-951 - import class --- .../wallet/send/model/NewSendButtonState.kt | 5 +-- .../org/p2p/wallet/send/model/SendFeeTotal.kt | 34 +++++++++++++++++-- .../p2p/wallet/send/ui/NewSendPresenter.kt | 7 +++- .../svl/ui/send/SendViaLinkPresenter.kt | 4 ++- 4 files changed, 44 insertions(+), 6 deletions(-) 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 348febdbf4..de3eebe3ca 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 @@ -20,7 +20,8 @@ class NewSendButtonState( private val feeRelayerState: FeeRelayerState, private val calculationMode: CalculationMode, private val minRentExemption: BigInteger, - private val resources: Resources + private val resources: Resources, + private val totalFee: SendFeeTotal, ) { private val minSolValidator = SendButtonStateMinSolValidator( @@ -114,7 +115,7 @@ class NewSendButtonState( else -> { State.Enabled( textResId = R.string.send_format, - value = "${calculationMode.getCurrentAmount().toPlainString()} ${tokenToSend.tokenSymbol}", + value = totalFee.totalSumWithSymbol, totalAmountTextColor = R.color.text_night ) } diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt b/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt index 0b89b947df..26b0cdf7d7 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt @@ -3,6 +3,7 @@ package org.p2p.wallet.send.model import androidx.annotation.ColorInt import android.os.Parcelable import java.math.BigDecimal +import java.math.BigInteger import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.p2p.core.model.TextHighlighting @@ -12,6 +13,8 @@ import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.orZero import org.p2p.uikit.utils.SpanUtils import org.p2p.wallet.R +import org.p2p.wallet.feerelayer.model.Limits +import org.p2p.wallet.feerelayer.model.ProcessedFee import org.p2p.wallet.feerelayer.model.TransactionFeeLimits /** @@ -36,6 +39,33 @@ class SendFeeTotal constructor( val interestBearingPercent: BigDecimal? = null ) : Parcelable { + companion object { + fun createEmpty(): SendFeeTotal = SendFeeTotal( + currentAmount = BigDecimal.ZERO, + currentAmountUsd = null, + receiveFormatted = "", + receiveUsd = null, + sendFee = null, + feeLimit = TransactionFeeLimits( + limits = Limits( + maxFeeCountAllowed = 0, + maxFeeAmountAllowed = BigInteger.ZERO, + maxAccountCreationCountAllowed = 0, + maxAccountCreationAmountAllowed = BigInteger.ZERO + ), + processedFee = ProcessedFee( + totalFeeAmountUsed = BigInteger.ZERO, + totalRentAmountUsed = BigInteger.ZERO, + totalFeeCountUsed = 0, + totalRentCountUsed = 0, + totalAmountUsed = BigInteger.ZERO + ) + ), + sourceSymbol = "", + recipientAddress = "" + ) + } + @IgnoredOnParcel val isSendingToken2022: Boolean get() = transferFeePercent != null || interestBearingPercent != null @@ -115,10 +145,10 @@ class SendFeeTotal constructor( private val totalWithSymbolFormatted: String get() = currentAmount.formatTokenWithSymbol(sourceSymbol, SOL_DECIMALS) - private val totalSumWithSymbol: String + val totalSumWithSymbol: String get() { val transferFee = transferFeePercent - ?.let { it / 100.toBigDecimal() } + ?.multiply("0.01".toBigDecimal()) ?.multiply(currentAmount) val totalSum = currentAmount 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 cac374bde3..3495323ff2 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 @@ -616,7 +616,12 @@ class NewSendPresenter( calculationMode = calculationMode, feeRelayerState = feeRelayerState, minRentExemption = sendFeeRelayerManager.getMinRentExemption(), - resources = resources + resources = resources, + totalFee = sendFeeRelayerManager.buildTotalFee( + sourceToken = sourceToken, + calculationMode = calculationMode, + tokenExtensions = (feeRelayerState as? FeeRelayerState.UpdateFee)?.tokenExtensions.orEmpty() + ) ) when (val state = sendButton.getCurrentState()) { diff --git a/app/src/main/java/org/p2p/wallet/svl/ui/send/SendViaLinkPresenter.kt b/app/src/main/java/org/p2p/wallet/svl/ui/send/SendViaLinkPresenter.kt index b7d633bbee..d344901d21 100644 --- a/app/src/main/java/org/p2p/wallet/svl/ui/send/SendViaLinkPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/svl/ui/send/SendViaLinkPresenter.kt @@ -25,6 +25,7 @@ import org.p2p.wallet.send.interactor.SendInteractor import org.p2p.wallet.send.model.CalculationMode import org.p2p.wallet.send.model.FeeRelayerState import org.p2p.wallet.send.model.NewSendButtonState +import org.p2p.wallet.send.model.SendFeeTotal import org.p2p.wallet.send.model.TemporaryAccount import org.p2p.wallet.send.model.toSearchResult import org.p2p.wallet.svl.analytics.SendViaLinkAnalytics @@ -278,7 +279,8 @@ class SendViaLinkPresenter( calculationMode = calculationMode, feeRelayerState = FeeRelayerState.Idle, minRentExemption = minRentExemption, - resources = resources + resources = resources, + totalFee = SendFeeTotal.createEmpty() ) when (val state = sendButton.getCurrentState()) { From dcd3b9afbdb0a6c2bf49b5bb8a4e809c9266b83a Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:36:28 +0300 Subject: [PATCH 27/51] PWN-961 - add Jupiter API as price source (#2195) --- .../wallet/home/events/SolanaTokensLoader.kt | 17 ++---- .../repository/tokens/db/SwapTokensDao.kt | 2 +- .../ui/info/SwapInfoLiquidityFeeMapper.kt | 3 + .../user/interactor/UserTokensInteractor.kt | 13 ++--- .../UserTokensDatabaseRepository.kt | 12 ++-- .../org/p2p/core/network/NetworkCoreModule.kt | 5 +- .../org/p2p/core/utils/AmountExtensions.kt | 4 +- .../p2p/token/service/TokenServiceModule.kt | 19 ++++-- .../service/api/JupiterPricesDataSource.kt | 9 +++ .../service/api/JupiterPricesRootResponse.kt | 18 ++++++ .../manager/TokenServiceEventPublisher.kt | 1 + .../repository/TokenServiceRepository.kt | 7 +++ .../repository/TokenServiceRepositoryImpl.kt | 39 +++++++++---- .../price/JupiterTokenPriceRepository.kt | 58 +++++++++++++++++++ 14 files changed, 160 insertions(+), 47 deletions(-) create mode 100644 token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt create mode 100644 token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt create mode 100644 token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt diff --git a/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt b/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt index f4b1cd2a8a..7c28e5a172 100644 --- a/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt +++ b/app/src/main/java/org/p2p/wallet/home/events/SolanaTokensLoader.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import org.p2p.core.common.di.AppScope import org.p2p.core.token.Token import org.p2p.token.service.api.events.manager.TokenServiceEventManager @@ -23,10 +22,9 @@ class SolanaTokensLoader( private val userTokensInteractor: UserTokensInteractor, private val tokenKeyProvider: TokenKeyProvider, private val tokenServiceEventManager: TokenServiceEventManager, - private val appScope: AppScope + appScope: AppScope ) { - - private val state: MutableStateFlow = MutableStateFlow(SolanaTokenLoadState.Idle) + private val state = MutableStateFlow(SolanaTokenLoadState.Idle) init { userTokensInteractor.observeUserTokens() @@ -40,10 +38,9 @@ class SolanaTokensLoader( try { updateState(SolanaTokenLoadState.Loading) - tokenServiceEventManager.subscribe(SolanaTokensRatesEventSubscriber(::saveTokensRates)) val tokens = userTokensInteractor.loadUserTokens(tokenKeyProvider.publicKey.toPublicKey()) userTokensInteractor.saveUserTokens(tokens) - userTokensInteractor.loadUserRates(tokens) + userTokensInteractor.loadAndSaveUserRates(tokens) } catch (e: CancellationException) { Timber.d("Loading sol tokens job cancelled") } catch (e: Throwable) { @@ -58,7 +55,7 @@ class SolanaTokensLoader( val tokens = userTokensInteractor.loadUserTokens(tokenKeyProvider.publicKey.toPublicKey()) userTokensInteractor.saveUserTokens(tokens) - userTokensInteractor.loadUserRates(tokens) + userTokensInteractor.loadAndSaveUserRates(tokens) } catch (e: CancellationException) { Timber.d("Refreshing sol tokens job cancelled") } catch (e: Throwable) { @@ -71,12 +68,6 @@ class SolanaTokensLoader( return userTokensInteractor.getUserTokens() } - private fun saveTokensRates(list: List) { - appScope.launch { - userTokensInteractor.saveUserTokensRates(list) - } - } - private fun updateState(newState: SolanaTokenLoadState) { state.value = newState } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt index a2c86c579b..fd88560e3c 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDao.kt @@ -35,7 +35,7 @@ interface SwapTokensDao { suspend fun findTokensByMints(mints: Set): List // todo: should be done with pagination - @Query("SELECT * from swap_tokens LIMIT 150") + @Query("SELECT * from swap_tokens LIMIT 100") suspend fun getAllSwapTokens(): List @Query("SELECT COUNT(*) from swap_tokens") diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt index 0e6c538dec..221ad3ac38 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt @@ -43,6 +43,9 @@ class SwapInfoLiquidityFeeMapper( fun mapLiquidityFees( route: JupiterSwapRouteV6, ): Flow> = flow { + // emit banner while we load + emit(listOf(createLiquidityFeeBanner())) + val keyAppFee = route.fees.platformFeeTokenB val tokenBMint = route.outTokenMint diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt index f682288cf8..cc175737dc 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt @@ -17,7 +17,6 @@ import org.p2p.token.service.api.events.manager.TokenServiceEventPublisher import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice import org.p2p.token.service.repository.TokenServiceRepository -import org.p2p.wallet.home.model.TokenComparator import org.p2p.wallet.home.model.TokenConverter import org.p2p.wallet.user.repository.UserLocalRepository import org.p2p.wallet.user.repository.UserTokensLocalRepository @@ -32,12 +31,10 @@ class UserTokensInteractor( private val dispatchers: CoroutineDispatchers ) { - suspend fun loadUserRates(userTokens: List) { + suspend fun loadAndSaveUserRates(userTokens: List) { val tokenAddresses = userTokens.map { it.tokenServiceAddress } - tokenServiceInteractor.loadTokensPrice( - networkChain = TokenServiceNetwork.SOLANA, - addresses = tokenAddresses - ) + val prices = tokenServiceRepository.getTokenPricesByAddress(tokenAddresses, TokenServiceNetwork.SOLANA) + saveUserTokensRates(prices) } suspend fun loadUserTokens(publicKey: PublicKey): List { @@ -93,7 +90,6 @@ class UserTokensInteractor( val cachedTokens = userTokensLocalRepository.getUserTokens() tokens .updateVisibilityState(cachedTokens) - .sortedWith(TokenComparator()) .let { userTokensLocalRepository.updateTokens(it) } } @@ -110,7 +106,8 @@ class UserTokensInteractor( } fun observeUserTokens(): Flow> { - return userTokensLocalRepository.observeUserTokens().map { it.filterTokensByAvailability() } + return userTokensLocalRepository.observeUserTokens() + .map { it.filterTokensByAvailability() } } fun observeUserToken(mintAddress: Base58String): Flow { diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt index 98f77cfafc..f89dd55ce9 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt @@ -8,6 +8,7 @@ import org.p2p.core.token.Token import org.p2p.core.utils.scaleShort import org.p2p.token.service.model.TokenServicePrice import org.p2p.wallet.home.db.TokenDao +import org.p2p.wallet.home.model.TokenComparator import org.p2p.wallet.home.model.TokenConverter class UserTokensDatabaseRepository( @@ -33,13 +34,15 @@ class UserTokensDatabaseRepository( override fun observeUserTokens(): Flow> { return tokensDao.getTokensFlow() - .map { tokenEntities -> - tokenEntities.map { TokenConverter.fromDatabase(it) } + .map { + it.map(TokenConverter::fromDatabase) + .sortedWith(TokenComparator()) } } override fun observeUserToken(mintAddress: Base58String): Flow = - tokensDao.getSingleTokenFlow(mintAddress.base58Value).map { TokenConverter.fromDatabase(it) } + tokensDao.getSingleTokenFlow(mintAddress.base58Value) + .map(TokenConverter::fromDatabase) override suspend fun updateTokenBalance(publicKey: Base58String, newTotal: BigDecimal, newTotalInUsd: BigDecimal?) { tokensDao.updateTokenTotal( @@ -58,7 +61,8 @@ class UserTokensDatabaseRepository( override suspend fun getUserTokens(): List { return tokensDao.getTokens() - .map { TokenConverter.fromDatabase(it) } + .map(TokenConverter::fromDatabase) + .sortedWith(TokenComparator()) } override suspend fun clear() { diff --git a/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt b/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt index 31d609bd31..1127c2ef63 100644 --- a/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt +++ b/core/src/main/java/org/p2p/core/network/NetworkCoreModule.kt @@ -95,9 +95,8 @@ object NetworkCoreModule : InjectionModule { } fun Scope.httpLoggingInterceptor(logTag: String): HttpLoggingInterceptor { - return HttpLoggingInterceptor(DebugHttpLoggingLogger(gson = get(), logTag = logTag)).apply { - level = HttpLoggingInterceptor.Level.BODY - } + return HttpLoggingInterceptor(DebugHttpLoggingLogger(gson = get(), logTag = logTag)) + .apply { level = HttpLoggingInterceptor.Level.BODY } } fun Scope.getClient( diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index 691ef0c3c5..db673a2c2e 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -6,7 +6,7 @@ import java.math.RoundingMode import org.p2p.core.token.Token import org.p2p.core.utils.Constants.FIAT_FRACTION_LENGTH -private val POWER_VALUE = BigDecimal("10") +private val POWER_VALUE = BigDecimal.TEN const val SOL_DECIMALS = 9 const val MOONPAY_DECIMAL = 2 const val STRIGA_FIAT_DECIMALS = 2 @@ -57,7 +57,7 @@ fun BigDecimal.scaleLong(decimals: Int = SCALE_VALUE_LONG): BigDecimal = // do not use BigDecimal(double) sometimes it makes the amount less // example: pass 0.030, get 0.029 fun BigInteger.fromLamports(decimals: Int): BigDecimal = - (this.toBigDecimal() / (POWER_VALUE.pow(decimals))) + (this.toBigDecimal().divide(POWER_VALUE.pow(decimals), 18, RoundingMode.HALF_DOWN)) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 .scaleLong(decimals) diff --git a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt index 932acd4522..aca43e81dd 100644 --- a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt +++ b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt @@ -11,6 +11,8 @@ import org.p2p.core.common.di.InjectionModule import org.p2p.core.network.NetworkCoreModule.getRetrofit import org.p2p.core.network.environment.NetworkServicesUrlProvider import org.p2p.core.rpc.RPC_JSON_QUALIFIER +import org.p2p.token.service.api.JupiterPricesDataSource +import org.p2p.token.service.api.TokenServiceDataSource import org.p2p.token.service.api.TokenServiceRemoteDataSource import org.p2p.token.service.api.events.manager.TokenServiceEventManager import org.p2p.token.service.api.events.manager.TokenServiceEventPublisher @@ -25,6 +27,7 @@ import org.p2p.token.service.repository.metadata.TokenMetadataInMemoryRepository import org.p2p.token.service.repository.metadata.TokenMetadataLocalRepository import org.p2p.token.service.repository.metadata.TokenMetadataRemoteRepository import org.p2p.token.service.repository.metadata.TokenMetadataRepository +import org.p2p.token.service.repository.price.JupiterTokenPriceRepository import org.p2p.token.service.repository.price.TokenPriceDatabaseRepository import org.p2p.token.service.repository.price.TokenPriceLocalRepository import org.p2p.token.service.repository.price.TokenPriceRemoteRepository @@ -35,7 +38,7 @@ object TokenServiceModule : InjectionModule { override fun create() = module { includes(TokenServiceDatabaseModule.create()) - single { + single { TokenServiceRemoteDataSource( api = get(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)).create(), gson = get(named(RPC_JSON_QUALIFIER)), @@ -43,6 +46,10 @@ object TokenServiceModule : InjectionModule { ) } + single { + getRetrofit(baseUrl = "https://price.jup.ag/", tag = null, interceptor = null).create() + } + single(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)) { val url = get() getRetrofit( @@ -52,9 +59,10 @@ object TokenServiceModule : InjectionModule { ) } - single { TokenMetadataInMemoryRepository() } - single { TokenPriceDatabaseRepository(get(), get()) } - factory { TokenPriceRemoteRepository(get(), get()) } + singleOf(::TokenMetadataInMemoryRepository) bind TokenMetadataLocalRepository::class + singleOf(::TokenPriceDatabaseRepository) bind TokenPriceLocalRepository::class + factoryOf(::TokenPriceRemoteRepository) bind TokenPriceRepository::class + factoryOf(::JupiterTokenPriceRepository) factoryOf(::TokenServiceAmountsRemoteConverter) bind TokenServiceAmountsConverter::class factoryOf(::TokenServiceMapper) @@ -64,7 +72,8 @@ object TokenServiceModule : InjectionModule { priceRemoteRepository = get(), priceLocalRepository = get(), metadataLocalRepository = get(), - metadataRemoteRepository = get() + metadataRemoteRepository = get(), + jupiterPriceRepository = get() ) } singleOf(::TokenServiceEventPublisher) diff --git a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt new file mode 100644 index 0000000000..a042f0e2cd --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt @@ -0,0 +1,9 @@ +package org.p2p.token.service.api + +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface JupiterPricesDataSource { + @GET("v4/price") + suspend fun getPrices(@Query("ids", encoded = true) tokenMints: String): JupiterPricesRootResponse +} diff --git a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt new file mode 100644 index 0000000000..8cb118656a --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt @@ -0,0 +1,18 @@ +package org.p2p.token.service.api + +import com.google.gson.annotations.SerializedName + +internal class JupiterPricesRootResponse( + @SerializedName("data") + val tokenMintsToPrices: Map +) + +internal class JupiterPricesResponse( + @SerializedName("id") + val mintAddress: String, + @SerializedName("mintSymbol") + val tokenSymbol: String, + // 1 unit of the token worth in USDC + @SerializedName("price") + val usdPrice: String +) diff --git a/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt b/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt index 31e47730f7..d4bc1e9b44 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/events/manager/TokenServiceEventPublisher.kt @@ -9,6 +9,7 @@ import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.repository.TokenServiceRepository +@Deprecated("Maybe deprecated due to fetching prices from remote only without cache") class TokenServiceEventPublisher( private val tokenServiceInteractor: TokenServiceRepository, private val eventManager: TokenServiceEventManager, diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt index 636eda2728..e3cfe2ccd9 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt @@ -21,6 +21,13 @@ interface TokenServiceRepository { networkChain: TokenServiceNetwork, forceRemote: Boolean = false ): TokenServicePrice? + + suspend fun getTokenPricesByAddress( + tokenAddress: List, + networkChain: TokenServiceNetwork, + forceRemote: Boolean = false + ): List + fun findTokenMetadataByAddress( networkChain: TokenServiceNetwork, tokenAddress: String diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt index 6da967b328..44b2bda58a 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt @@ -1,28 +1,32 @@ package org.p2p.token.service.repository +import timber.log.Timber import kotlinx.coroutines.flow.Flow import org.p2p.token.service.model.TokenServiceMetadata import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice import org.p2p.token.service.repository.metadata.TokenMetadataLocalRepository import org.p2p.token.service.repository.metadata.TokenMetadataRepository +import org.p2p.token.service.repository.price.JupiterTokenPriceRepository import org.p2p.token.service.repository.price.TokenPriceLocalRepository import org.p2p.token.service.repository.price.TokenPriceRepository internal class TokenServiceRepositoryImpl( private val priceRemoteRepository: TokenPriceRepository, + private val jupiterPriceRepository: JupiterTokenPriceRepository, private val priceLocalRepository: TokenPriceLocalRepository, private val metadataLocalRepository: TokenMetadataLocalRepository, private val metadataRemoteRepository: TokenMetadataRepository ) : TokenServiceRepository { + @Deprecated("Use getTokenPriceByAddress") override suspend fun fetchTokenPricesForTokens(chain: TokenServiceNetwork, tokenAddresses: List) { - val result = priceRemoteRepository.loadTokensPrice( - chain = chain, - addresses = tokenAddresses - ) - val tokensPrices = result.flatMap { it.items } - priceLocalRepository.saveTokensPrice(tokensPrices) +// val result = priceRemoteRepository.loadTokensPrice( +// chain = chain, +// addresses = tokenAddresses +// ) +// val tokensPrices = result.flatMap { it.items } +// priceLocalRepository.saveTokensPrice(tokensPrices) } override suspend fun fetchMetadataForTokens( @@ -46,12 +50,25 @@ internal class TokenServiceRepositoryImpl( networkChain: TokenServiceNetwork, forceRemote: Boolean ): TokenServicePrice? { - val localPrice = priceLocalRepository.findTokenPriceByAddress(tokenAddress, networkChain) - if (forceRemote || localPrice == null) { - fetchTokenPricesForTokens(networkChain, listOf(tokenAddress)) - } + return getTokenPricesByAddress(listOf(tokenAddress), networkChain).firstOrNull() + } - return priceLocalRepository.findTokenPriceByAddress(tokenAddress, networkChain) + override suspend fun getTokenPricesByAddress( + tokenAddress: List, + networkChain: TokenServiceNetwork, + forceRemote: Boolean + ): List = try { + when (networkChain) { + TokenServiceNetwork.SOLANA -> { + jupiterPriceRepository.loadTokensPrice(networkChain, tokenAddress).items + } + TokenServiceNetwork.ETHEREUM -> { + priceRemoteRepository.loadTokensPrice(networkChain, tokenAddress).flatMap { it.items } + } + } + } catch (error: Exception) { + Timber.i(error) + emptyList() } override fun findTokenMetadataByAddress( diff --git a/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt new file mode 100644 index 0000000000..dcd819933d --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt @@ -0,0 +1,58 @@ +package org.p2p.token.service.repository.price + +import timber.log.Timber +import kotlinx.coroutines.withContext +import org.p2p.core.dispatchers.CoroutineDispatchers +import org.p2p.core.utils.Constants +import org.p2p.token.service.api.JupiterPricesDataSource +import org.p2p.token.service.model.TokenRate +import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.model.TokenServicePrice +import org.p2p.token.service.model.TokenServiceQueryResult + +internal class JupiterTokenPriceRepository( + private val api: JupiterPricesDataSource, + private val dispatchers: CoroutineDispatchers +) { + suspend fun loadTokensPrice( + chain: TokenServiceNetwork, + addresses: List + ): TokenServiceQueryResult = withContext(dispatchers.io) { + try { + require(chain == TokenServiceNetwork.SOLANA) { "Only Solana tokens prices can be loaded" } + + val isNativeRequested = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN in addresses + + val mintsAsQuery = addresses.joinToString(separator = ",") { + if (it == Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) Constants.WRAPPED_SOL_MINT else it + } + val tokenMintsToRates = api.getPrices(mintsAsQuery).tokenMintsToPrices + .mapValues { it.value.usdPrice } + .run { + if (isNativeRequested) { + val nativeSolRate = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN to this[Constants.WRAPPED_SOL_MINT] + this + nativeSolRate + } else { + this + } + } + + val prices = addresses.map { mintAddress -> + val rate = tokenMintsToRates[mintAddress]?.toBigDecimal() + if (rate == null) { + Timber.e("USD rate for $mintAddress not found!") + } + + TokenServicePrice( + tokenAddress = mintAddress, + rate = TokenRate(rate), + network = chain + ) + } + TokenServiceQueryResult(networkChain = chain, items = prices) + } catch (error: Exception) { + Timber.i(error, "Failed fetching prices") + throw error + } + } +} From 27a2c3a85001bae616ff043ee84d980fb1c54e4f Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:00:48 +0300 Subject: [PATCH 28/51] PWN-945 - History and Send fixes (#2196) * PWN-948 - do not open details for token-2022 if input is empty --- .../history/analytics/HistoryAnalytics.kt | 8 +- .../mapper/RpcHistoryTransactionConverter.kt | 67 +++--- .../model/rpc/RpcHistoryTransaction.kt | 215 +++++++----------- .../HistoryTransactionDetailsPresenter.kt | 43 ++-- .../ui/wallet/handlers/WalletStrigaHandler.kt | 9 +- .../jupiter/ui/main/JupiterSwapPresenter.kt | 25 +- .../sell/interactor/HistoryItemMapper.kt | 49 ++-- .../p2p/wallet/send/model/FeeRelayerState.kt | 5 +- .../p2p/wallet/send/ui/NewSendPresenter.kt | 37 +-- .../interactor/StrigaWithdrawInteractor.kt | 8 +- .../svl/ui/receive/ReceiveViaLinkPresenter.kt | 12 +- app/src/main/res/values/history_strings.xml | 1 + .../org/p2p/core/utils/AmountExtensions.kt | 6 +- 13 files changed, 242 insertions(+), 243 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/history/analytics/HistoryAnalytics.kt b/app/src/main/java/org/p2p/wallet/history/analytics/HistoryAnalytics.kt index f9f0d384ca..5076eb70d1 100644 --- a/app/src/main/java/org/p2p/wallet/history/analytics/HistoryAnalytics.kt +++ b/app/src/main/java/org/p2p/wallet/history/analytics/HistoryAnalytics.kt @@ -57,10 +57,10 @@ class HistoryAnalytics( swapAnalytics.logSwapShowingDetails( swapStatus = SwapAnalytics.SwapStatus.SUCCESS, lastScreenName = analyticsInteractor.getPreviousScreenName(), - tokenAName = transaction.sourceSymbol, - tokenBName = transaction.destinationSymbol, - swapSum = transaction.receiveAmount.total, - swapUSD = transaction.sentAmount.totalInUsd.orZero(), + tokenAName = transaction.tokenA.symbol, + tokenBName = transaction.tokenB.symbol, + swapSum = transaction.tokenAAmount.total, + swapUSD = transaction.tokenBAmount.totalInUsd.orZero(), feesSource = SwapAnalytics.FeeSource.UNKNOWN ) } diff --git a/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt b/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt index af695f5024..b2873ddf36 100644 --- a/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt +++ b/app/src/main/java/org/p2p/wallet/history/interactor/mapper/RpcHistoryTransactionConverter.kt @@ -2,6 +2,7 @@ package org.p2p.wallet.history.interactor.mapper import com.google.gson.Gson import org.p2p.core.utils.Constants.FEE_RELAYER_ACCOUNTS +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.fromJsonReified import org.p2p.core.utils.toBigDecimalOrZero import org.p2p.wallet.bridge.claim.repository.EthereumBridgeInMemoryRepository @@ -10,16 +11,19 @@ import org.p2p.wallet.common.InAppFeatureFlags import org.p2p.wallet.common.date.toZonedDateTime import org.p2p.wallet.history.api.model.RpcHistoryFeeResponse import org.p2p.wallet.history.api.model.RpcHistoryStatusResponse +import org.p2p.wallet.history.api.model.RpcHistoryTokenResponse import org.p2p.wallet.history.api.model.RpcHistoryTransactionInfoResponse import org.p2p.wallet.history.api.model.RpcHistoryTransactionResponse import org.p2p.wallet.history.api.model.RpcHistoryTypeResponse import org.p2p.wallet.history.model.rpc.RpcFee import org.p2p.wallet.history.model.rpc.RpcHistoryAmount import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction +import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionToken import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionType import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.transaction.model.HistoryTransactionStatus import org.p2p.wallet.utils.UsernameFormatter +import org.p2p.wallet.utils.emptyString class RpcHistoryTransactionConverter( private val tokenKeyProvider: TokenKeyProvider, @@ -73,9 +77,8 @@ class RpcHistoryTransactionConverter( type = transaction.type.toDomain(), senderAddress = info.counterParty.address, counterPartyUsername = usernameFormatter.formatOrNull(info.counterParty.username), - iconUrl = info.token.logoUrl, + token = info.token.toDomain(), amount = RpcHistoryAmount(total, totalInUsd), - symbol = info.token.symbol.orEmpty(), destination = tokenKeyProvider.publicKey, fees = transaction.fees.parseFees() ) @@ -100,6 +103,7 @@ class RpcHistoryTransactionConverter( iconUrl = info.token.logoUrl, amount = RpcHistoryAmount(total, totalInUsd), symbol = info.token.symbol.orEmpty(), + decimals = info.token.decimals, destination = tokenKeyProvider.publicKey, fees = transaction.fees.parseFees() ) @@ -119,9 +123,8 @@ class RpcHistoryTransactionConverter( type = transaction.type.toDomain(), senderAddress = tokenKeyProvider.publicKey, counterPartyUsername = usernameFormatter.formatOrNull(info.counterParty.username), - iconUrl = info.token.logoUrl, + token = info.token.toDomain(), amount = RpcHistoryAmount(total, totalInUsd), - symbol = info.token.symbol.orEmpty(), destination = info.counterParty.address, fees = transaction.fees.parseFees() ) @@ -135,21 +138,20 @@ class RpcHistoryTransactionConverter( val destinationTotal = info.to.amounts.amount.toBigDecimalOrZero() val destinationTotalInUsd = info.to.amounts.usdAmount.toBigDecimalOrZero() + val tokenA = info.from.token + val tokenB = info.to.token + return RpcHistoryTransaction.Swap( signature = transaction.signature, date = transaction.date.toZonedDateTime(), blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), - sourceAddress = info.from.token.mint, - destinationAddress = info.to.token.mint, - receiveAmount = RpcHistoryAmount(sourceTotal, sourceTotalInUsd), - sentAmount = RpcHistoryAmount(destinationTotal, destinationTotalInUsd), + tokenA = tokenA.toDomain(), + tokenB = tokenB.toDomain(), + tokenAAmount = RpcHistoryAmount(sourceTotal, sourceTotalInUsd), + tokenBAmount = RpcHistoryAmount(destinationTotal, destinationTotalInUsd), fees = transaction.fees.parseFees(), - sourceSymbol = info.from.token.symbol.orEmpty(), - sourceIconUrl = info.from.token.logoUrl, - destinationSymbol = info.to.token.symbol.orEmpty(), - destinationIconUrl = info.to.token.logoUrl, - type = transaction.type.toDomain() + type = transaction.type.toDomain(), ) } @@ -166,9 +168,8 @@ class RpcHistoryTransactionConverter( status = transaction.status.toDomain(), type = transaction.type.toDomain(), senderAddress = tokenKeyProvider.publicKey, - iconUrl = info.token.logoUrl, amount = RpcHistoryAmount(total, totalInUsd), - symbol = info.token.symbol.orEmpty(), + token = info.token.toDomain(), destination = info.token.mint, fees = transaction.fees.parseFees() ) @@ -188,8 +189,7 @@ class RpcHistoryTransactionConverter( type = transaction.type.toDomain(), amount = RpcHistoryAmount(total, totalInUsd), senderAddress = info.token.mint, - iconUrl = info.token.logoUrl, - symbol = info.token.symbol.orEmpty(), + token = info.token.toDomain(), destination = tokenKeyProvider.publicKey, fees = transaction.fees.parseFees() ) @@ -206,9 +206,8 @@ class RpcHistoryTransactionConverter( signature = transaction.signature, blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), - iconUrl = info.token.logoUrl, fees = transaction.fees.parseFees(), - tokenSymbol = info.token.symbol.orEmpty(), + token = info.token.toDomain(), type = transaction.type.toDomain(), amount = RpcHistoryAmount(total, totalInUsd) ) @@ -242,8 +241,7 @@ class RpcHistoryTransactionConverter( date = transaction.date.toZonedDateTime(), blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), - tokenSymbol = info.token.symbol.orEmpty(), - iconUrl = info.token.logoUrl, + token = info.token.toDomain(), type = transaction.type.toDomain(), amount = RpcHistoryAmount(total, totalInUsd), fees = transaction.fees.parseFees() @@ -261,8 +259,7 @@ class RpcHistoryTransactionConverter( date = transaction.date.toZonedDateTime(), blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), - tokenSymbol = info.token.symbol.orEmpty(), - iconUrl = info.token.logoUrl, + token = info.token.toDomain(), type = transaction.type.toDomain(), amount = RpcHistoryAmount(total, totalInUsd), fees = transaction.fees.parseFees() @@ -294,9 +291,10 @@ class RpcHistoryTransactionConverter( blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), type = transaction.type.toDomain(), - tokenSymbol = info.tokenAmount?.token?.symbol.orEmpty(), + token = info.tokenAmount?.token?.toDomain() ?: RpcHistoryTransactionToken( + symbol = emptyString(), decimals = SOL_DECIMALS, logoUrl = null + ), amount = RpcHistoryAmount(total, totalInUsd), - iconUrl = info.tokenAmount?.token?.logoUrl, fees = fees, claimKey = claimKey ) @@ -324,9 +322,10 @@ class RpcHistoryTransactionConverter( blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), type = transaction.type.toDomain(), - tokenSymbol = info.tokenAmount?.token?.symbol.orEmpty(), + token = info.tokenAmount?.token?.toDomain() ?: RpcHistoryTransactionToken( + symbol = emptyString(), decimals = SOL_DECIMALS, logoUrl = null + ), amount = RpcHistoryAmount(total, totalInUsd), - iconUrl = info.tokenAmount?.token?.logoUrl, fees = bundleFees.parseBridgeFees() ?: transaction.fees.parseFees(), sourceAddress = info.to?.address.orEmpty(), message = message @@ -345,8 +344,10 @@ class RpcHistoryTransactionConverter( blockNumber = transaction.blockNumber.toInt(), status = transaction.status.toDomain(), type = transaction.type.toDomain(), - tokenSymbol = info.token?.symbol.orEmpty(), - amount = RpcHistoryAmount(total, totalInUsd) + token = info.token?.toDomain() ?: RpcHistoryTransactionToken( + symbol = emptyString(), decimals = SOL_DECIMALS, logoUrl = null + ), + amount = RpcHistoryAmount(total = total, totalInUsd = totalInUsd) ) } } @@ -376,8 +377,14 @@ private fun RpcHistoryTypeResponse?.toDomain(): RpcHistoryTransactionType { } } +private fun RpcHistoryTokenResponse.toDomain(): RpcHistoryTransactionToken = RpcHistoryTransactionToken( + symbol = symbol.orEmpty(), + decimals = decimals, + logoUrl = logoUrl +) + private fun List.parseFees(): List? { - return if (this.all { fee -> FEE_RELAYER_ACCOUNTS.contains(fee.payer) }) { + return if (this.all { fee -> fee.payer in FEE_RELAYER_ACCOUNTS }) { null } else { map { fee -> diff --git a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt index d42d055fec..06205dc6b3 100644 --- a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt +++ b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt @@ -11,9 +11,7 @@ import kotlinx.parcelize.Parcelize import org.p2p.core.utils.asNegativeUsdTransaction import org.p2p.core.utils.asPositiveUsdTransaction import org.p2p.core.utils.asUsdTransaction -import org.p2p.core.utils.formatToken -import org.p2p.core.utils.scaleLong -import org.p2p.core.utils.scaleMedium +import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.scaleShortOrFirstNotZero import org.p2p.wallet.R import org.p2p.wallet.history.model.HistoryTransaction @@ -21,6 +19,13 @@ import org.p2p.wallet.transaction.model.HistoryTransactionStatus import org.p2p.wallet.utils.cutMiddle import org.p2p.wallet.utils.cutStart +@Parcelize +data class RpcHistoryTransactionToken( + val symbol: String, + val decimals: Int, + val logoUrl: String?, +) : Parcelable + sealed class RpcHistoryTransaction( override val date: ZonedDateTime, open val signature: String, @@ -46,8 +51,7 @@ sealed class RpcHistoryTransaction( override val blockNumber: Int, override val status: HistoryTransactionStatus, val amount: RpcHistoryAmount, - val tokenSymbol: String, - val iconUrl: String?, + val token: RpcHistoryTransactionToken, override val type: RpcHistoryTransactionType, val fees: List?, ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { @@ -59,19 +63,14 @@ sealed class RpcHistoryTransaction( @StringRes fun getTitle(): Int = if (isBurn) R.string.common_burn else R.string.common_mint - fun getUsdAmount(): String = "${getFormattedAmount()}" - - fun getTotal(): String = "${getSymbol(isBurn)}${amount.total.abs().scaleMedium().formatToken()} $tokenSymbol" - - fun getFormattedTotal(scaleMedium: Boolean = false): String = - if (scaleMedium) { - "${amount.total.abs().scaleMedium().toPlainString()} $tokenSymbol" - } else { - "${amount.total.abs().scaleLong().toPlainString()} $tokenSymbol" - } + fun getTotal(): String { + val symbol = getSymbol(isBurn) + val amount = amount.total.abs().formatTokenWithSymbol(token.symbol, token.decimals) + return "$symbol$amount" + } fun getFormattedAbsTotal(): String { - return "${amount.total.abs().scaleLong().toPlainString()} $tokenSymbol" + return amount.total.abs().formatTokenWithSymbol(token.symbol, token.decimals) } override fun getSymbol(isNegativeOperation: Boolean): String { @@ -85,7 +84,9 @@ sealed class RpcHistoryTransaction( else -> R.color.text_mint } - fun getFormattedAmount(): String? = amount.totalInUsd?.abs()?.asUsdTransaction(getSymbol(isBurn)) + fun getFormattedAmountUsd(): String? { + return amount.totalInUsd?.abs()?.asUsdTransaction(getSymbol(isBurn)) + } } @Parcelize @@ -109,17 +110,14 @@ sealed class RpcHistoryTransaction( override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, val amount: RpcHistoryAmount, - val iconUrl: String?, + val token: RpcHistoryTransactionToken, val fees: List?, - val tokenSymbol: String, ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { - fun getFormattedTotal(scaleMedium: Boolean = false): String = if (scaleMedium) { - "${amount.total.scaleMedium().formatToken()} $tokenSymbol" - } else { - "${amount.total.formatToken()} $tokenSymbol" - } + fun getFormattedTotal(): String = + amount.total.formatTokenWithSymbol(token.symbol, token.decimals) - fun getFormattedAmount(): String? = amount.totalInUsd?.asNegativeUsdTransaction() + fun getFormattedAmountUsd(): String? = + amount.totalInUsd?.asNegativeUsdTransaction() } @Parcelize @@ -129,31 +127,20 @@ sealed class RpcHistoryTransaction( override val blockNumber: Int, override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, - val sourceAddress: String, - val destinationAddress: String, + val tokenA: RpcHistoryTransactionToken, + val tokenB: RpcHistoryTransactionToken, val fees: List?, - val receiveAmount: RpcHistoryAmount, - val sentAmount: RpcHistoryAmount, - val sourceSymbol: String, - val sourceIconUrl: String?, - val destinationSymbol: String, - val destinationIconUrl: String?, + val tokenAAmount: RpcHistoryAmount, + val tokenBAmount: RpcHistoryAmount, ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { - fun getTitle(): String = "$sourceSymbol → $destinationSymbol" - - fun getReceivedUsdAmount(): String? = receiveAmount.totalInUsd?.asPositiveUsdTransaction() - - fun getSentUsdAmount(): String? = sentAmount.totalInUsd?.asNegativeUsdTransaction() - - fun getFormattedAmount() = - "${receiveAmount.total.formatToken()} $sourceSymbol to ${sentAmount.total.formatToken()} $destinationSymbol" + fun getTokenBUsdAmount(): String? = tokenAAmount.totalInUsd?.asPositiveUsdTransaction() fun getFormattedAmountWithArrow(): String { return buildString { - append("${receiveAmount.total.abs().formatToken()} $sourceSymbol") + append(tokenAAmount.total.abs().formatTokenWithSymbol(tokenA.symbol, tokenA.decimals)) append(" → ") - append("${sentAmount.total.formatToken()} $destinationSymbol") + append(tokenBAmount.total.formatTokenWithSymbol(tokenB.symbol, tokenB.decimals)) } } @@ -164,18 +151,14 @@ sealed class RpcHistoryTransaction( } @ColorRes - fun getTextColor() = when { - status.isCompleted() -> { - R.color.text_mint - } - else -> { - R.color.text_rose - } + fun getTextColor(): Int = when { + status.isCompleted() -> R.color.text_mint + else -> R.color.text_rose } - fun getSourceTotal(): String = "${receiveAmount.total.formatToken()} $sourceSymbol" + fun getTokenATotal(): String = tokenAAmount.total.formatTokenWithSymbol(tokenA.symbol, tokenA.decimals) - fun getDestinationTotal(): String = "${sentAmount.total.formatToken()} $destinationSymbol" + fun getTokenBTotal(): String = tokenBAmount.total.formatTokenWithSymbol(tokenB.symbol, tokenB.decimals) } @Parcelize @@ -186,9 +169,8 @@ sealed class RpcHistoryTransaction( override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, val senderAddress: String, - val iconUrl: String?, val amount: RpcHistoryAmount, - val symbol: String, + val token: RpcHistoryTransactionToken, val destination: String, val counterPartyUsername: String?, val fees: List?, @@ -202,16 +184,16 @@ sealed class RpcHistoryTransaction( val isSend: Boolean get() = type == RpcHistoryTransactionType.SEND - fun getTokenIconUrl(): String? = iconUrl + fun getTokenIconUrl(): String? = token.logoUrl @DrawableRes fun getIcon(): Int = if (isSend) R.drawable.ic_transaction_send else R.drawable.ic_transaction_receive fun getTitle(resources: Resources): String { val (from: String, to: String) = if (isSend) { - symbol to destination.cutMiddle() + token.symbol to destination.cutMiddle() } else { - destination.cutMiddle() to symbol + destination.cutMiddle() to token.symbol } return resources.getString(R.string.details_transfer_format, from, to) } @@ -250,25 +232,14 @@ sealed class RpcHistoryTransaction( } @ColorRes - fun getTextColor() = when { - !status.isCompleted() -> { - R.color.text_rose - } - isSend -> { - R.color.text_night - } - else -> { - R.color.text_mint - } - } - - fun getFormattedTotal(scaleMedium: Boolean = false): String = if (scaleMedium) { - "${amount.total.scaleMedium().formatToken()} $symbol" - } else { - "${amount.total.formatToken()} $symbol" + fun getTextColor(): Int = when { + !status.isCompleted() -> R.color.text_rose + isSend -> R.color.text_night + else -> R.color.text_mint } - fun getFormattedAmount(): String? = amount.totalInUsd?.asUsdTransaction(getSymbol(isSend)) + fun getFormattedTotal(): String = amount.total.formatTokenWithSymbol(token.symbol, token.decimals) + fun getFormattedAmountUsd(): String? = amount.totalInUsd?.asUsdTransaction(getSymbol(isSend)) } @Parcelize @@ -282,6 +253,7 @@ sealed class RpcHistoryTransaction( val iconUrl: String?, val amount: RpcHistoryAmount, val symbol: String, + val decimals: Int, val destination: String, val counterPartyUsername: String?, val fees: List?, @@ -292,25 +264,17 @@ sealed class RpcHistoryTransaction( fun getFormattedFiatValue(): String? { return amount.totalInUsd ?.scaleShortOrFirstNotZero() - ?.asUsdTransaction(getSymbol(false)) + ?.asPositiveUsdTransaction() } - fun getTotalWithSymbol(): String = "${getSymbol(false)}${getFormattedTotal()}" + fun getTotalWithSymbol(): String = "+${getFormattedTotal()}" - fun getFormattedTotal(scaleMedium: Boolean = false): String = if (scaleMedium) { - "${amount.total.scaleMedium().formatToken()} $symbol" - } else { - "${amount.total.formatToken()} $symbol" - } + fun getFormattedTotal(): String = amount.total.formatTokenWithSymbol(symbol, decimals) @ColorRes - fun getTextColor() = when { - !status.isCompleted() -> { - R.color.text_rose - } - else -> { - R.color.text_mint - } + fun getTextColor(): Int = when { + !status.isCompleted() -> R.color.text_rose + else -> R.color.text_mint } } @@ -322,9 +286,8 @@ sealed class RpcHistoryTransaction( override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, val senderAddress: String, - val iconUrl: String?, + val token: RpcHistoryTransactionToken, val amount: RpcHistoryAmount, - val symbol: String, val destination: String, val fees: List?, ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { @@ -333,14 +296,18 @@ sealed class RpcHistoryTransaction( val isStake: Boolean get() = type == RpcHistoryTransactionType.STAKE - fun getTokenIconUrl(): String? = iconUrl + fun getTokenIconUrl(): String? = token.logoUrl @DrawableRes fun getIcon(): Int = if (isStake) R.drawable.ic_transaction_send else R.drawable.ic_transaction_receive fun getAddress(): String = if (isStake) destination.cutStart() else senderAddress.cutStart() - fun getValue(): String? = amount.totalInUsd?.scaleShortOrFirstNotZero()?.asUsdTransaction(getSymbol(isStake)) + fun getValue(): String? { + return amount.totalInUsd + ?.scaleShortOrFirstNotZero() + ?.asUsdTransaction(getSymbol(isStake)) + } fun getTotal(): String = "${getSymbol(isStake)}${getFormattedTotal()}" @@ -352,24 +319,14 @@ sealed class RpcHistoryTransaction( @ColorRes fun getTextColor() = when { - !status.isCompleted() -> { - R.color.text_rose - } - isStake -> { - R.color.text_night - } - else -> { - R.color.text_mint - } + !status.isCompleted() -> R.color.text_rose + isStake -> R.color.text_night + else -> R.color.text_mint } - fun getFormattedTotal(scaleMedium: Boolean = false): String = if (scaleMedium) { - "${amount.total.scaleMedium().formatToken()} $symbol" - } else { - "${amount.total.formatToken()} $symbol" - } + fun getFormattedTotal(): String = amount.total.formatTokenWithSymbol(token.symbol, token.decimals) - fun getFormattedAmount(): String? = amount.totalInUsd?.asUsdTransaction(getSymbol(isStake)) + fun getFormattedAmountUsd(): String? = amount.totalInUsd?.asUsdTransaction(getSymbol(isStake)) } @Parcelize @@ -380,8 +337,7 @@ sealed class RpcHistoryTransaction( override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, val amount: RpcHistoryAmount, - val tokenSymbol: String, - val iconUrl: String?, + val token: RpcHistoryTransactionToken, val fees: List?, val claimKey: String ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { @@ -396,16 +352,11 @@ sealed class RpcHistoryTransaction( R.string.transaction_history_receive } - fun getUsdAmount(): String = "${getFormattedUsdAmount()}" + fun getUsdAmount(): String = getFormattedUsdAmount().orEmpty() - fun getTotal(): String = "+${amount.total.scaleMedium().formatToken()} $tokenSymbol" + fun getTotalWithSymbol(): String = "+${getFormattedTotal()}" - fun getFormattedTotal(scaleMedium: Boolean = false): String = - if (scaleMedium) { - "${amount.total.scaleMedium().toPlainString()} $tokenSymbol" - } else { - "${amount.total.scaleLong().toPlainString()} $tokenSymbol" - } + fun getFormattedTotal(): String = amount.total.formatTokenWithSymbol(token.symbol, token.decimals) @ColorRes fun getTextColor(): Int = when { @@ -413,7 +364,11 @@ sealed class RpcHistoryTransaction( else -> R.color.text_night } - fun getFormattedUsdAmount(): String? = amount.totalInUsd?.abs()?.asPositiveUsdTransaction() + fun getFormattedUsdAmount(): String? { + return amount.totalInUsd + ?.abs() + ?.asPositiveUsdTransaction() + } } @Parcelize @@ -424,14 +379,13 @@ sealed class RpcHistoryTransaction( override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, val amount: RpcHistoryAmount, + val token: RpcHistoryTransactionToken, val sourceAddress: String, - val tokenSymbol: String, - val iconUrl: String?, val fees: List?, val message: String ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { - fun getUsdAmount(): String = "${getFormattedUsdAmount()}" + fun getUsdAmount(): String = getFormattedUsdAmount().orEmpty() @StringRes fun getTitle(): Int = R.string.transaction_history_wh_send @@ -443,14 +397,9 @@ sealed class RpcHistoryTransaction( R.string.transaction_history_send } - fun getTotal(): String = "${amount.total.scaleMedium().formatToken()} $tokenSymbol" + fun getTotal(): String = getFormattedTotal() - fun getFormattedTotal(scaleMedium: Boolean = false): String = - if (scaleMedium) { - "${amount.total.scaleMedium().toPlainString()} $tokenSymbol" - } else { - "${amount.total.scaleLong().toPlainString()} $tokenSymbol" - } + fun getFormattedTotal(): String = amount.total.formatTokenWithSymbol(token.symbol, token.decimals) fun getFormattedUsdAmount(): String? = amount.totalInUsd?.abs()?.asNegativeUsdTransaction() @@ -468,15 +417,11 @@ sealed class RpcHistoryTransaction( override val blockNumber: Int, override val status: HistoryTransactionStatus, override val type: RpcHistoryTransactionType, - val tokenSymbol: String, + val token: RpcHistoryTransactionToken, val amount: RpcHistoryAmount ) : RpcHistoryTransaction(date, signature, blockNumber, status, type) { - fun getFormattedTotal(scaleMedium: Boolean = false): String = if (scaleMedium) { - "${amount.total.scaleMedium().formatToken()} $tokenSymbol" - } else { - "${amount.total.formatToken()} $tokenSymbol" - } + fun getFormattedTotal(): String = amount.total.formatTokenWithSymbol(token.symbol, token.decimals) - fun getFormattedAmount(): String? = amount.totalInUsd?.asNegativeUsdTransaction() + fun getFormattedAmountUsd(): String? = amount.totalInUsd?.asNegativeUsdTransaction() } } diff --git a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt index 934f8b08a3..2d24cac498 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt @@ -86,15 +86,15 @@ class HistoryTransactionDetailsPresenter( private fun parseSwap(transaction: RpcHistoryTransaction.Swap) { view?.apply { - val usdTotal = transaction.getReceivedUsdAmount() + val usdTotal = transaction.getTokenBUsdAmount() val total = transaction.getFormattedAmountWithArrow() - showAmount(total, usdTotal) + showAmount(amountToken = total, amountUsd = usdTotal) if (!transaction.status.isPending()) { showFee(transaction.fees) } - val sourceIcon = transaction.sourceIconUrl - val destinationIcon = transaction.destinationIconUrl + val sourceIcon = transaction.tokenA.logoUrl + val destinationIcon = transaction.tokenB.logoUrl if (sourceIcon.isNullOrEmpty() && destinationIcon.isNullOrEmpty()) { showTransferView(null, R.drawable.ic_swap_arrows) } else { @@ -110,13 +110,13 @@ class HistoryTransactionDetailsPresenter( showSubtitle(resources.getString(R.string.details_pending)) showProgressTransactionInProgress() } - showTransferView(transaction.iconUrl, transaction.getIcon()) + showTransferView(transaction.token.logoUrl, transaction.getIcon()) if (!transaction.status.isPending()) { showFee(transaction.fees) } showAmount( amountToken = transaction.getFormattedTotal(), - amountUsd = transaction.getFormattedAmount() + amountUsd = transaction.getFormattedAmountUsd() ) showTransferAddress( isSend = transaction.isSend, @@ -212,13 +212,13 @@ class HistoryTransactionDetailsPresenter( private fun parseBurnOrMint(transaction: RpcHistoryTransaction.BurnOrMint) { view?.apply { - val usdTotal = transaction.getFormattedAmount() + val usdTotal = transaction.getFormattedAmountUsd() val total = transaction.getFormattedAbsTotal() showAmount(total, usdTotal) if (!transaction.status.isPending()) { showFee(transaction.fees) } - showTransferView(transaction.iconUrl, R.drawable.ic_placeholder_image) + showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_image) showStateTitleValue( resources.getString( if (transaction.isBurn) R.string.transaction_details_burn @@ -235,7 +235,7 @@ class HistoryTransactionDetailsPresenter( view?.showSubtitle(resources.getString(R.string.details_pending)) showProgressTransactionInProgress() } - showTransferView(transaction.iconUrl, R.drawable.ic_placeholder_image) + showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_image) if (transaction.fees != null && !transaction.status.isPending()) { showFee(transaction.fees) } @@ -259,7 +259,7 @@ class HistoryTransactionDetailsPresenter( view?.showSubtitle(resources.getString(R.string.details_pending)) showProgressTransactionInProgress() } - showTransferView(transaction.iconUrl, R.drawable.ic_placeholder_image) + showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_image) if (transaction.fees != null && !transaction.status.isPending()) { showFee(transaction.fees) } @@ -279,11 +279,11 @@ class HistoryTransactionDetailsPresenter( private fun parseCreateAccount(transaction: RpcHistoryTransaction.CreateAccount) { view?.apply { - val usdTotal = transaction.getFormattedAmount() + val usdTotal = transaction.getFormattedAmountUsd() val total = transaction.getFormattedTotal() showAmount(total, usdTotal) showFee(transaction.fees) - showTransferView(transaction.iconUrl, R.drawable.ic_transaction_create) + showTransferView(transaction.token.logoUrl, R.drawable.ic_transaction_create) showStateTitleValue( resources.getString(R.string.transaction_details_signature), transaction.signature.cutMiddle() @@ -297,23 +297,26 @@ class HistoryTransactionDetailsPresenter( showFee(transaction.fees) showTransferView(transaction.iconUrl, R.drawable.ic_transaction_closed) showStateTitleValue( - resources.getString(R.string.transaction_details_signature), - transaction.signature.cutMiddle() + title = resources.getString(R.string.transaction_details_signature), + value = transaction.signature.cutMiddle() ) } } private fun parseStakeUnstake(transaction: RpcHistoryTransaction.StakeUnstake) { view?.apply { - val usdTotal = transaction.getFormattedAmount() + val usdTotal = transaction.getFormattedAmountUsd() val total = transaction.getFormattedTotal() showAmount(total, usdTotal) showFee(transaction.fees) - showTransferView(transaction.iconUrl, transaction.getIcon()) + showTransferView(transaction.token.logoUrl, transaction.getIcon()) showStateTitleValue( title = resources.getString( - if (transaction.isStake) R.string.transaction_details_stake - else R.string.transaction_details_unstake + if (transaction.isStake) { + R.string.transaction_details_stake + } else { + R.string.transaction_details_unstake + } ), value = transaction.signature.cutMiddle() ) @@ -322,13 +325,13 @@ class HistoryTransactionDetailsPresenter( private fun parseUnknown(transaction: RpcHistoryTransaction.Unknown) { view?.apply { - val usdTotal = transaction.getFormattedAmount() + val usdTotal = transaction.getFormattedAmountUsd() val total = transaction.getFormattedTotal() showAmount(total, usdTotal) showTransferView(tokenIconUrl = null, placeholderIcon = R.drawable.ic_transaction_unknown) showStateTitleValue( title = resources.getString(R.string.transaction_details_signature), - value = transaction.signature.cutMiddle() + value = transaction.signature.cutMiddle(cutCount = 8) ) } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/wallet/handlers/WalletStrigaHandler.kt b/app/src/main/java/org/p2p/wallet/home/ui/wallet/handlers/WalletStrigaHandler.kt index 8bb071e00b..d81f57900e 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/wallet/handlers/WalletStrigaHandler.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/wallet/handlers/WalletStrigaHandler.kt @@ -3,6 +3,7 @@ package org.p2p.wallet.home.ui.wallet.handlers import org.threeten.bp.ZonedDateTime import timber.log.Timber import java.util.UUID +import org.p2p.core.utils.SOL_DECIMALS import org.p2p.core.utils.emptyString import org.p2p.wallet.BuildConfig import org.p2p.wallet.R @@ -10,6 +11,7 @@ import org.p2p.wallet.common.InAppFeatureFlags import org.p2p.wallet.history.interactor.HistoryInteractor import org.p2p.wallet.history.model.rpc.RpcHistoryAmount import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction +import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionToken import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionType import org.p2p.wallet.home.events.AppLoaderFacade import org.p2p.wallet.home.events.StrigaFeatureLoader @@ -164,9 +166,12 @@ class WalletStrigaHandler( status = HistoryTransactionStatus.PENDING, type = RpcHistoryTransactionType.RECEIVE, senderAddress = strigaUserCryptoAddress, - iconUrl = claimedToken.tokenIcon, + token = RpcHistoryTransactionToken( + symbol = claimedToken.tokenSymbol, + decimals = SOL_DECIMALS, + logoUrl = claimedToken.tokenIcon + ), amount = claimedToken.amountAvailable.let { RpcHistoryAmount(it, it) }, - symbol = claimedToken.tokenSymbol, destination = tokenKeyProvider.publicKey, counterPartyUsername = null, fees = null diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt index d48807656a..b546e95f47 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt @@ -33,6 +33,7 @@ import org.p2p.wallet.common.mvp.BasePresenter import org.p2p.wallet.history.interactor.HistoryInteractor import org.p2p.wallet.history.model.rpc.RpcHistoryAmount import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction +import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionToken import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionType import org.p2p.wallet.infrastructure.transactionmanager.TransactionManager import org.p2p.wallet.jupiter.analytics.JupiterSwapMainScreenAnalytics @@ -749,15 +750,23 @@ class JupiterSwapPresenter( blockNumber = -1, status = HistoryTransactionStatus.PENDING, type = RpcHistoryTransactionType.SWAP, - sourceSymbol = currentState.tokenA.tokenSymbol, - sourceAddress = currentState.tokenA.mintAddress.toString(), + tokenA = currentState.tokenA.run { + RpcHistoryTransactionToken( + symbol = tokenSymbol, + decimals = decimals, + logoUrl = iconUrl, + ) + }, + tokenB = currentState.tokenB.run { + RpcHistoryTransactionToken( + symbol = tokenSymbol, + decimals = decimals, + logoUrl = iconUrl, + ) + }, fees = emptyList(), - receiveAmount = RpcHistoryAmount(total = currentState.amountTokenA, totalInUsd = null), - sentAmount = RpcHistoryAmount(total = currentState.amountTokenB, totalInUsd = null), - sourceIconUrl = currentState.tokenA.iconUrl, - destinationSymbol = currentState.tokenB.tokenSymbol, - destinationIconUrl = currentState.tokenB.iconUrl, - destinationAddress = currentState.tokenB.mintAddress.toString() + tokenAAmount = RpcHistoryAmount(total = currentState.amountTokenA, totalInUsd = null), + tokenBAmount = RpcHistoryAmount(total = currentState.amountTokenB, totalInUsd = null), ) } } diff --git a/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt b/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt index 2487a1411e..57ff9fe151 100644 --- a/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt +++ b/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt @@ -21,6 +21,7 @@ import org.p2p.wallet.moonpay.model.SellTransaction import org.p2p.wallet.moonpay.serversideapi.response.SellTransactionStatus import org.p2p.wallet.sell.ui.lock.SellTransactionViewDetails import org.p2p.wallet.user.repository.UserLocalRepository +import org.p2p.wallet.utils.cutMiddle import org.p2p.wallet.utils.cutStart import org.p2p.wallet.utils.getStatusIcon @@ -134,15 +135,15 @@ class HistoryItemMapper( val iconRes: Int when (transaction) { is RpcHistoryTransaction.Swap -> with(transaction) { - sourceTokenIconUrl = sourceIconUrl - destinationTokenIconUrl = destinationIconUrl + sourceTokenIconUrl = tokenA.logoUrl + destinationTokenIconUrl = tokenB.logoUrl iconRes = R.drawable.ic_swap_arrows - startTitle = "$sourceSymbol to $destinationSymbol" + startTitle = "${tokenA.symbol} to ${tokenB.symbol}" startSubtitle = resources.getString(getTypeName()) - endTopValue = "+${getDestinationTotal()}" + endTopValue = "+${getTokenBTotal()}" endTopValueTextColor = getTextColor() - endBottomValue = getSourceTotal() + endBottomValue = getTokenATotal() } is RpcHistoryTransaction.Transfer -> with(transaction) { tokenIconUrl = getTokenIconUrl() @@ -175,34 +176,40 @@ class HistoryItemMapper( endBottomValue = getTotal() } is RpcHistoryTransaction.BurnOrMint -> with(transaction) { - tokenIconUrl = iconUrl + tokenIconUrl = token.logoUrl iconRes = R.drawable.ic_placeholder_image startTitle = resources.getString(getTitle()) - startSubtitle = - resources.getString(R.string.transaction_history_signature_format, signature.cutStart()) - endTopValue = getUsdAmount() + startSubtitle = resources.getString( + R.string.transaction_history_signature_format, + signature.cutMiddle() + ) + endTopValue = getFormattedAmountUsd() endTopValueTextColor = getTextColor() endBottomValue = getTotal() } is RpcHistoryTransaction.CreateAccount -> with(transaction) { - tokenIconUrl = iconUrl + tokenIconUrl = token.logoUrl iconRes = R.drawable.ic_transaction_create startTitle = resources.getString(R.string.transaction_history_create) - startSubtitle = - resources.getString(R.string.transaction_history_signature_format, signature.cutStart()) + startSubtitle = resources.getString( + R.string.transaction_history_signature_format, + signature.cutMiddle() + ) } is RpcHistoryTransaction.CloseAccount -> with(transaction) { tokenIconUrl = iconUrl iconRes = R.drawable.ic_transaction_closed startTitle = resources.getString(R.string.transaction_history_closed) - startSubtitle = - resources.getString(R.string.transaction_history_signature_format, signature.cutStart()) + startSubtitle = resources.getString( + R.string.transaction_history_signature_format, + signature.cutMiddle() + ) } is RpcHistoryTransaction.WormholeSend -> with(transaction) { - tokenIconUrl = iconUrl + tokenIconUrl = token.logoUrl iconRes = R.drawable.ic_transaction_send startTitle = resources.getString(getTitle()) startSubtitle = resources.getString(getSubtitle()) @@ -210,24 +217,20 @@ class HistoryItemMapper( endTopValueTextColor = getTextColor() endBottomValue = getTotal() } - is RpcHistoryTransaction.WormholeReceive -> with(transaction) { - tokenIconUrl = iconUrl + tokenIconUrl = token.logoUrl iconRes = R.drawable.ic_transaction_send startTitle = resources.getString(getTitle()) startSubtitle = resources.getString(getSubtitle()) endTopValue = getUsdAmount() endTopValueTextColor = getTextColor() - endBottomValue = getTotal() + endBottomValue = getTotalWithSymbol() } else -> { iconRes = R.drawable.ic_transaction_unknown - startTitle = resources.getString(R.string.transaction_history_unknown) - startSubtitle = resources.getString( - R.string.transaction_history_signature_format, - transaction.signature.cutStart() - ) + startTitle = resources.getString(R.string.transaction_history_unknown_title) + startSubtitle = transaction.signature.cutMiddle(cutCount = 8) } } val historyItem = HistoryItem.TransactionItem( diff --git a/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt b/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt index a12513c236..5af9a8bdbe 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt @@ -15,7 +15,10 @@ sealed interface FeeRelayerState { val solanaFee: SendSolanaFee?, val feeLimitInfo: TransactionFeeLimits, val tokenExtensions: TokenExtensionsMap - ) : FeeRelayerState + ) : FeeRelayerState { + val hasToken2022Fee: Boolean + get() = tokenExtensions.isNotEmpty() + } data class Failure( val previousState: FeeRelayerState, 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 3495323ff2..f3b9aa8dd3 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 @@ -38,6 +38,7 @@ import org.p2p.wallet.history.interactor.HistoryInteractor import org.p2p.wallet.history.model.HistoryTransaction import org.p2p.wallet.history.model.rpc.RpcHistoryAmount import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction +import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionToken import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionType import org.p2p.wallet.infrastructure.network.provider.SendModeProvider import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider @@ -264,7 +265,6 @@ class NewSendPresenter( view.setFeeLabel(feesLabel) updateButton(sourceToken, feeRelayerState) - // FIXME: only for debug needs, remove after release if (BuildConfig.DEBUG) buildDebugInfo(feeRelayerState.solanaFee) } @@ -466,16 +466,24 @@ class NewSendPresenter( if (currentState !is FeeRelayerState.UpdateFee) return val solanaFee = currentState.solanaFee - if (calculationMode.isCurrentInputEmpty() && solanaFee == null) { - newSendAnalytics.logFreeTransactionsClicked(flow) - view?.showFreeTransactionsInfo() - } else { - val total = sendFeeRelayerManager.buildTotalFee( - sourceToken = requireToken(), - calculationMode = calculationMode, - tokenExtensions = currentState.tokenExtensions - ) - view?.showTransactionDetails(total) + + when { + calculationMode.isCurrentInputEmpty() && currentState.hasToken2022Fee -> { + return + } + calculationMode.isCurrentInputEmpty() && solanaFee == null -> { + newSendAnalytics.logFreeTransactionsClicked(flow) + view?.showFreeTransactionsInfo() + } + else -> { + val total = sendFeeRelayerManager.buildTotalFee( + sourceToken = requireToken(), + calculationMode = calculationMode, + tokenExtensions = currentState.tokenExtensions + ) + + view?.showTransactionDetails(total) + } } } @@ -605,8 +613,11 @@ class NewSendPresenter( counterPartyUsername = recipientAddress.nicknameOrAddress(), fees = null, status = HistoryTransactionStatus.PENDING, - iconUrl = token.iconUrl, - symbol = token.tokenSymbol + token = RpcHistoryTransactionToken( + symbol = token.tokenSymbol, + decimals = token.decimals, + logoUrl = token.iconUrl + ) ) private fun updateButton(sourceToken: Token.Active, feeRelayerState: FeeRelayerState) { diff --git a/app/src/main/java/org/p2p/wallet/striga/offramp/withdraw/interactor/StrigaWithdrawInteractor.kt b/app/src/main/java/org/p2p/wallet/striga/offramp/withdraw/interactor/StrigaWithdrawInteractor.kt index b32e56c6e6..12641158cd 100644 --- a/app/src/main/java/org/p2p/wallet/striga/offramp/withdraw/interactor/StrigaWithdrawInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/striga/offramp/withdraw/interactor/StrigaWithdrawInteractor.kt @@ -17,6 +17,7 @@ import org.p2p.wallet.feerelayer.model.FeeRelayerStatistics import org.p2p.wallet.history.interactor.HistoryInteractor import org.p2p.wallet.history.model.rpc.RpcHistoryAmount import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction +import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionToken import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionType import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.infrastructure.transactionmanager.TransactionManager @@ -135,8 +136,11 @@ class StrigaWithdrawInteractor( destination = recipientAddress, fees = null, status = HistoryTransactionStatus.PENDING, - iconUrl = token.iconUrl, - symbol = token.tokenSymbol, + token = RpcHistoryTransactionToken( + symbol = token.tokenSymbol, + decimals = token.decimals, + logoUrl = token.iconUrl + ), counterPartyUsername = null ) } diff --git a/app/src/main/java/org/p2p/wallet/svl/ui/receive/ReceiveViaLinkPresenter.kt b/app/src/main/java/org/p2p/wallet/svl/ui/receive/ReceiveViaLinkPresenter.kt index 2b689557f2..316aee7118 100644 --- a/app/src/main/java/org/p2p/wallet/svl/ui/receive/ReceiveViaLinkPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/svl/ui/receive/ReceiveViaLinkPresenter.kt @@ -4,17 +4,19 @@ import org.threeten.bp.ZonedDateTime import timber.log.Timber import java.net.UnknownHostException import kotlinx.coroutines.launch +import org.p2p.core.common.di.AppScope +import org.p2p.core.crypto.toBase58Instance import org.p2p.core.network.ConnectionManager import org.p2p.core.token.Token import org.p2p.solanaj.core.PublicKey import org.p2p.wallet.R import org.p2p.wallet.alarmlogger.logger.AlarmErrorsLogger import org.p2p.wallet.auth.interactor.UsernameInteractor -import org.p2p.core.common.di.AppScope import org.p2p.wallet.common.mvp.BasePresenter import org.p2p.wallet.history.interactor.HistoryInteractor import org.p2p.wallet.history.model.rpc.RpcHistoryAmount import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction +import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionToken import org.p2p.wallet.history.model.rpc.RpcHistoryTransactionType import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.send.model.TemporaryAccount @@ -27,7 +29,6 @@ import org.p2p.wallet.svl.model.TemporaryAccountState import org.p2p.wallet.svl.ui.error.SendViaLinkError import org.p2p.wallet.transaction.model.HistoryTransactionStatus import org.p2p.wallet.utils.emptyString -import org.p2p.core.crypto.toBase58Instance import org.p2p.wallet.utils.toPublicKey class ReceiveViaLinkPresenter( @@ -158,8 +159,11 @@ class ReceiveViaLinkPresenter( counterPartyUsername = usernameInteractor.getUsername()?.fullUsername, fees = null, status = HistoryTransactionStatus.PENDING, - iconUrl = token.iconUrl, - symbol = token.tokenSymbol + token = RpcHistoryTransactionToken( + symbol = token.tokenSymbol, + decimals = token.decimals, + logoUrl = token.iconUrl + ) ) private fun logCvlError( diff --git a/app/src/main/res/values/history_strings.xml b/app/src/main/res/values/history_strings.xml index 10088e9347..76696ee766 100644 --- a/app/src/main/res/values/history_strings.xml +++ b/app/src/main/res/values/history_strings.xml @@ -8,6 +8,7 @@ Oops! Something happened. Unknown + Transaction Swap Swap… Send diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index db673a2c2e..edcb5154d0 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -174,7 +174,11 @@ fun BigDecimal.asPositiveUsdTransaction(): String = asUsdTransaction("+") fun BigDecimal.asNegativeUsdTransaction(): String = asUsdTransaction("-") fun BigDecimal.asUsdTransaction( transactionSymbol: String -): String = if (lessThenMinValue()) "<$0.01" else "$transactionSymbol$ ${formatFiat()}" +): String { + if (lessThenMinValue()) return "<$0.01" + val fiatAmountWithSymbol = "$${formatFiat()}" + return "$transactionSymbol $fiatAmountWithSymbol" +} fun BigDecimal.asUsdSwap(): String = when { isZero() -> "0 USD" From 9a492afe90a9c41805db397d7373a06ba08e87eb Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:15:25 +0400 Subject: [PATCH 29/51] ETH-946 - save send fee payer (#2194) * 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 --- .../wallet/feerelayer/model/FeeRelayerFee.kt | 33 +++- .../home/ui/container/MainContainerModule.kt | 4 +- .../wallet/infrastructure/StorageModule.kt | 8 +- .../p2p/wallet/send/SendFeeRelayerManager.kt | 134 +++++++------ .../java/org/p2p/wallet/send/SendModule.kt | 10 + .../send/interactor/FeePayersRepository.kt | 6 +- .../wallet/send/interactor/SendInteractor.kt | 25 ++- .../usecase/GetFeesInPayingTokenUseCase.kt | 28 +-- .../p2p/wallet/send/model/SendSolanaFee.kt | 46 +++-- .../FeePayerTokenValidityRepository.kt | 28 +++ .../p2p/wallet/send/repository/SendStorage.kt | 30 +++ .../send/repository/SendStorageContract.kt | 9 + .../p2p/wallet/send/ui/NewSendPresenter.kt | 185 ++++++++++-------- .../org/p2p/core/utils/AmountExtensions.kt | 17 +- .../TokenServiceAmountsRemoteConverter.kt | 7 + 15 files changed, 385 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/send/repository/FeePayerTokenValidityRepository.kt create mode 100644 app/src/main/java/org/p2p/wallet/send/repository/SendStorage.kt create mode 100644 app/src/main/java/org/p2p/wallet/send/repository/SendStorageContract.kt diff --git a/app/src/main/java/org/p2p/wallet/feerelayer/model/FeeRelayerFee.kt b/app/src/main/java/org/p2p/wallet/feerelayer/model/FeeRelayerFee.kt index b7034975ea..93287745dc 100644 --- a/app/src/main/java/org/p2p/wallet/feerelayer/model/FeeRelayerFee.kt +++ b/app/src/main/java/org/p2p/wallet/feerelayer/model/FeeRelayerFee.kt @@ -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 } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerModule.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerModule.kt index 3fb92abebe..394d4bfebf 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerModule.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerModule.kt @@ -42,7 +42,9 @@ object MainContainerModule : InjectionModule { tokenKeyProvider = get(), dispatchers = get(), sendServiceRepository = get(), - feePayersRepository = get() + feePayersRepository = get(), + feePayerTokenValidityRepository = get(), + sendStorage = get(), ) } factoryOf(::SearchInteractor) diff --git a/app/src/main/java/org/p2p/wallet/infrastructure/StorageModule.kt b/app/src/main/java/org/p2p/wallet/infrastructure/StorageModule.kt index 82c49f6697..0eebf8eb21 100644 --- a/app/src/main/java/org/p2p/wallet/infrastructure/StorageModule.kt +++ b/app/src/main/java/org/p2p/wallet/infrastructure/StorageModule.kt @@ -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()) { @@ -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 @@ -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) } } 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 cb4765be41..bc9995e9cf 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -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 @@ -84,7 +82,7 @@ class SendFeeRelayerManager( private val alternativeTokensMap: HashMap> = HashMap() suspend fun initialize( - initialToken: Token.Active, + feePayerToken: Token.Active, solToken: Token.Active, recipientAddress: SearchResult ) { @@ -94,7 +92,7 @@ class SendFeeRelayerManager( onFeeLoading?.invoke(FeeLoadingState.Instant(isLoading = true)) try { - initializeWithToken(initialToken) + initializeWithToken(feePayerToken) initializeCompleted = true } catch (e: Throwable) { @@ -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 @@ -171,7 +169,7 @@ class SendFeeRelayerManager( try { onFeeLoading?.invoke(FeeLoadingState(isLoading = true, isDelayed = useCache)) if (!initializeCompleted) { - initializeWithToken(sourceToken) + initializeWithToken(feePayer) initializeCompleted = true } @@ -189,7 +187,7 @@ class SendFeeRelayerManager( feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions ) - sendInteractor.setFeePayerToken(sourceToken) + sendInteractor.setFeePayerToken(feePayer) } is FeeCalculationState.PoolsNotFound -> { val solanaFee = buildSolanaFee( @@ -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() @@ -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}") } } @@ -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 diff --git a/app/src/main/java/org/p2p/wallet/send/SendModule.kt b/app/src/main/java/org/p2p/wallet/send/SendModule.kt index 8179dbaa0c..12079c4d01 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendModule.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendModule.kt @@ -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 @@ -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 @@ -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() { diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt b/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt index 3c9d757f88..45b998c1bb 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt @@ -38,7 +38,7 @@ class FeePayersRepository( val feePayerTokensMints = sendServiceRepository.getCompensationTokens() val tokenToExcludeSymbol = feePayerToExclude.tokenSymbol - val fees: Map = getFeesInPayingTokenUseCase.execute( + val feesInGivenTokens: Map = getFeesInPayingTokenUseCase.execute( findFeesIn = feePayerTokensMints, transactionFeeInSol = transactionFeeInSol, accountCreationFeeInSol = accountCreationFeeInSol @@ -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 diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt b/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt index 28632922da..bb1438d38f 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt @@ -34,7 +34,9 @@ import org.p2p.wallet.send.model.SendTransactionFailed import org.p2p.wallet.send.model.send_service.SendFeePayerMode import org.p2p.wallet.send.model.send_service.SendRentPayerMode import org.p2p.wallet.send.model.send_service.SendTransferMode +import org.p2p.wallet.send.repository.FeePayerTokenValidityRepository import org.p2p.wallet.send.repository.SendServiceRepository +import org.p2p.wallet.send.repository.SendStorageContract import org.p2p.wallet.swap.interactor.orca.OrcaInfoInteractor import org.p2p.wallet.utils.toPublicKey @@ -51,6 +53,8 @@ class SendInteractor( private val dispatchers: CoroutineDispatchers, private val sendServiceRepository: SendServiceRepository, private val feePayersRepository: FeePayersRepository, + private val feePayerTokenValidityRepository: FeePayerTokenValidityRepository, + private val sendStorage: SendStorageContract, ) { /* @@ -62,8 +66,8 @@ class SendInteractor( /* * Initialize fee payer token * */ - suspend fun initialize(token: Token.Active) { - feePayerToken = token + suspend fun initialize(feePayer: Token.Active) { + feePayerToken = feePayer feeRelayerInteractor.load() orcaInfoInteractor.load() } @@ -462,4 +466,21 @@ class SendInteractor( accountCreationFeeInSol = accountCreationFeeInSOL ) } + + fun restoreSavedFeePayerToken(): Base58String? { + return sendStorage.restore() + } + + fun removeSavedFeePayerToken() { + sendStorage.remove() + } + + suspend fun saveFeePayerToken(token: Token.Active) { + if (checkTokenIsValidFeePayer(token)) { + sendStorage.save(token.mintAddressB58) + } + } + + suspend fun checkTokenIsValidFeePayer(token: Token.Active): Boolean = + feePayerTokenValidityRepository.checkIsValid(token) } diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/GetFeesInPayingTokenUseCase.kt b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/GetFeesInPayingTokenUseCase.kt index 8a8aa3b05f..1e7721656e 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/GetFeesInPayingTokenUseCase.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/GetFeesInPayingTokenUseCase.kt @@ -8,11 +8,9 @@ import org.p2p.core.token.Token import org.p2p.core.utils.Constants import org.p2p.solanaj.core.FeeAmount import org.p2p.token.service.converter.TokenServiceAmountsConverter -import org.p2p.wallet.send.repository.SendServiceRepository class GetFeesInPayingTokenUseCase( private val amountsConverter: TokenServiceAmountsConverter, - private val sendServiceRepository: SendServiceRepository ) { suspend fun execute( @@ -39,22 +37,30 @@ class GetFeesInPayingTokenUseCase( emptyMap() } + /** + * This function calculates how many target tokens we need in SOL equivalent to pay for the transaction + */ suspend fun execute( - feePayerToken: Token.Active, + targetToken: Token.Active, transactionFeeInSol: BigInteger, accountCreationFeeInSol: BigInteger ): FeeAmount? { - val transactionFeeSpl = amountsConverter.convertAmount( + if (targetToken.isSOL) { + return FeeAmount(transactionFeeInSol, accountCreationFeeInSol) + } + + val transactionFeeInTargetToken = amountsConverter.convertAmount( amountFrom = Constants.WRAPPED_SOL_MINT.toBase58Instance() to transactionFeeInSol, - mintsToConvertTo = listOf(feePayerToken.mintAddress.toBase58Instance()) - )[feePayerToken.mintAddress.toBase58Instance()] - val accountCreationFee = amountsConverter.convertAmount( + mintsToConvertTo = listOf(targetToken.mintAddress.toBase58Instance()) + )[targetToken.mintAddress.toBase58Instance()] + + val accountCreationFeeInTargetToken = amountsConverter.convertAmount( amountFrom = Constants.WRAPPED_SOL_MINT.toBase58Instance() to accountCreationFeeInSol, - mintsToConvertTo = listOf(feePayerToken.mintAddress.toBase58Instance()) - )[feePayerToken.mintAddress.toBase58Instance()] + mintsToConvertTo = listOf(targetToken.mintAddress.toBase58Instance()) + )[targetToken.mintAddress.toBase58Instance()] - return if (transactionFeeSpl != null && accountCreationFee != null) { - FeeAmount(transactionFeeSpl, accountCreationFee) + return if (transactionFeeInTargetToken != null && accountCreationFeeInTargetToken != null) { + FeeAmount(transactionFeeInTargetToken, accountCreationFeeInTargetToken) } else { null } diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt index c9e9744e06..877cb51635 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt @@ -14,7 +14,6 @@ import org.p2p.core.utils.isLessThan import org.p2p.core.utils.isMoreThan import org.p2p.core.utils.isZeroOrLess 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.wallet.feerelayer.model.FeePayerSelectionStrategy @@ -32,7 +31,7 @@ data class SendSolanaFee constructor( val feePayerToken: Token.Active, val feeRelayerFee: FeeRelayerFee, val token2022TransferFee: BigInteger, - private val sourceToken: Token.Active, + val sourceToken: Token.Active, private val solToken: Token.Active?, private val alternativeFeePayerTokens: List, private val supportedFeePayerTokens: List? = null @@ -92,16 +91,27 @@ data class SendSolanaFee constructor( } @IgnoredOnParcel - val accountCreationFeeDecimals: BigDecimal = - (if (feePayerToken.isSOL) feeRelayerFee.accountCreationFeeInSol else feeRelayerFee.accountCreationFeeInSpl) - .fromLamports(feePayerToken.decimals) - .scaleLong() + val accountCreationFeeDecimals: BigDecimal + get() { + val amount = if (feePayerToken.isSOL) { + feeRelayerFee.accountCreationFeeInSol + } else { + feeRelayerFee.accountCreationFeeInFeePayerToken + } + return amount.fromLamports(feePayerToken.decimals) + } @IgnoredOnParcel - val transactionDecimals: BigDecimal = - (if (feePayerToken.isSOL) feeRelayerFee.transactionFeeInSol else feeRelayerFee.transactionFeeInSpl) - .fromLamports(feePayerToken.decimals) - .scaleLong() + val transactionDecimals: BigDecimal + get() { + val amount = if (feePayerToken.isSOL) { + feeRelayerFee.transactionFeeInSol + } else { + feeRelayerFee.transactionFeeInFeePayerToken + } + + return amount.fromLamports(feePayerToken.decimals) + } @IgnoredOnParcel private val feePayerTotalLamports: BigInteger @@ -131,10 +141,10 @@ data class SendSolanaFee constructor( } // assuming that source token and fee payer are same sourceTokenSymbol == feePayerSymbol -> - sourceTokenTotal >= inputAmount + feeRelayerFee.totalInSpl + sourceTokenTotal >= inputAmount + feeRelayerFee.totalInSourceToken // assuming that source token and fee payer are different else -> - feePayerToken.totalInLamports >= feeRelayerFee.totalInSpl + feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken } private fun isEnoughSol( @@ -153,9 +163,10 @@ data class SendSolanaFee constructor( sourceTokenTotal: BigInteger, inputAmount: BigInteger ): FeePayerState { + val feePayerTokenCanCoverExpenses = feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken val isSourceSol = sourceTokenSymbol == SOL_SYMBOL val isAllowedToCorrectAmount = strategy == CORRECT_AMOUNT - val totalNeeded = feeRelayerFee.totalInSpl + inputAmount + val totalNeeded = feeRelayerFee.totalInSourceToken + inputAmount val isEnoughSolBalance = isEnoughSolBalance() val shouldTryReduceAmount = isAllowedToCorrectAmount && !isSourceSol && !isEnoughSolBalance val hasAlternativeFeePayerTokens = alternativeFeePayerTokens.isNotEmpty() @@ -177,9 +188,18 @@ data class SendSolanaFee constructor( appendLine("alternativeFeePayerTokens = ${alternativeFeePayerTokens.map(Token.Active::tokenSymbol)}") appendLine("isValidToSwitchOnSource = $isValidToSwitchOnSource") appendLine("shouldSwitchToSpl = $shouldSwitchToSpl") + appendLine("feePayerToken = ${feePayerToken.tokenSymbol}") + appendLine("feePayerTokenCanCoverExpenses = $feePayerTokenCanCoverExpenses") } ) return when { + feePayerTokenCanCoverExpenses -> { + if (feePayerToken.isSOL) { + SwitchToSol + } else { + SwitchToSpl(feePayerToken) + } + } shouldSwitchToSpl -> { SwitchToSpl(sourceToken) } diff --git a/app/src/main/java/org/p2p/wallet/send/repository/FeePayerTokenValidityRepository.kt b/app/src/main/java/org/p2p/wallet/send/repository/FeePayerTokenValidityRepository.kt new file mode 100644 index 0000000000..d343603f80 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/send/repository/FeePayerTokenValidityRepository.kt @@ -0,0 +1,28 @@ +package org.p2p.wallet.send.repository + +import kotlinx.coroutines.withContext +import org.p2p.core.crypto.Base58String +import org.p2p.core.dispatchers.CoroutineDispatchers +import org.p2p.core.token.Token + +/** + * Check if a token can be used as fee payer using send-service compensation tokens + */ +class FeePayerTokenValidityRepository( + private val dispatchers: CoroutineDispatchers, + private val sendServiceRepository: SendServiceRepository, +) { + + private val allowedTokenMintsCache = mutableSetOf() + + suspend fun checkIsValid(token: Token.Active): Boolean = + checkIsValid(token.mintAddressB58) + + private suspend fun checkIsValid(tokenMint: Base58String): Boolean = withContext(dispatchers.io) { + if (allowedTokenMintsCache.isEmpty()) { + allowedTokenMintsCache += sendServiceRepository.getCompensationTokens() + } + + tokenMint in allowedTokenMintsCache + } +} diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendStorage.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendStorage.kt new file mode 100644 index 0000000000..be3a521b7e --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendStorage.kt @@ -0,0 +1,30 @@ +package org.p2p.wallet.send.repository + +import androidx.core.content.edit +import android.content.SharedPreferences +import org.p2p.core.crypto.Base58String +import org.p2p.core.crypto.toBase58Instance + +class SendStorage( + private val prefs: SharedPreferences +) : SendStorageContract { + private companion object { + const val KEY_TOKEN_MINT = "TOKEN_MINT" + } + + override fun restore(): Base58String? { + return prefs.getString(KEY_TOKEN_MINT, null)?.toBase58Instance() + } + + override fun save(tokenMint: Base58String) { + prefs.edit { + putString(KEY_TOKEN_MINT, tokenMint.base58Value) + } + } + + override fun remove() { + prefs.edit { + remove(KEY_TOKEN_MINT) + } + } +} diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendStorageContract.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendStorageContract.kt new file mode 100644 index 0000000000..952d7cda30 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendStorageContract.kt @@ -0,0 +1,9 @@ +package org.p2p.wallet.send.repository + +import org.p2p.core.crypto.Base58String + +interface SendStorageContract { + fun restore(): Base58String? + fun save(tokenMint: Base58String) + fun remove() +} 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 f3b9aa8dd3..4d7757b2d3 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 @@ -72,7 +72,7 @@ private const val TAG = "NewSendPresenter" class NewSendPresenter( private val recipientAddress: SearchResult, - private val openedFrom: SendOpenedFrom, + openedFrom: SendOpenedFrom, private val userInteractor: UserInteractor, private val userTokensInteractor: UserTokensInteractor, private val sendInteractor: SendInteractor, @@ -87,7 +87,7 @@ class NewSendPresenter( private val historyInteractor: HistoryInteractor, private val tokenServiceCoordinator: TokenServiceCoordinator, private val sendFeeRelayerManager: SendFeeRelayerManager, - private val maximumAmountCalculator: SendMaximumAmountCalculator + private val maximumAmountCalculator: SendMaximumAmountCalculator, ) : BasePresenter(), NewSendContract.Presenter { private val flow: NewSendAnalytics.AnalyticsSendFlow = if (openedFrom == SendOpenedFrom.SELL_FLOW) { @@ -108,6 +108,10 @@ class NewSendPresenter( lessThenMinString = resources.getString(R.string.common_less_than_minimum), ) + /** + * Token which is selected by user before the screen is opened + * Used for detecting whether we need to show token selection or not + */ private var selectedToken: Token.Active? = null private var initialAmount: BigDecimal? = null @@ -153,71 +157,101 @@ class NewSendPresenter( } } - if (token != null) { - restoreSelectedToken(view, requireToken()) - } else { - setupInitialToken(view) + launch { + if (token != null) { + restoreSelectedToken(view, requireToken()) + } else { + setupInitialToken(view) + } } } - private fun restoreSelectedToken(view: NewSendContract.View, token: Token.Active) { - launch { - view.showToken(token) - calculationMode.updateToken(token) - checkTokenRatesAndSetSwitchAmountState(token) + private suspend fun restoreSelectedToken(view: NewSendContract.View, token: Token.Active) { + view.showToken(token) + calculationMode.updateToken(token) + checkTokenRatesAndSetSwitchAmountState(token) - val userTokens = getNonZeroUserTokensOrSol() - val isTokenChangeEnabled = userTokens.size > 1 && selectedToken == null - view.setTokenContainerEnabled(isEnabled = isTokenChangeEnabled) + val userTokens = getNonZeroUserTokensOrSol() + val isTokenChangeEnabled = userTokens.size > 1 && selectedToken == null + view.setTokenContainerEnabled(isEnabled = isTokenChangeEnabled) - val currentState = sendFeeRelayerManager.getState() - handleFeeRelayerStateUpdate(currentState, view) + val currentState = sendFeeRelayerManager.getState() + handleFeeRelayerStateUpdate(currentState, view) - maximumAmountCalculator.getMaxAvailableAmountToSend( - token = token, - recipient = recipientAddress.address.toBase58Instance() - ) - // set max available amount - ?.also { calculationMode.setMaxAmounts(it) } - } + maximumAmountCalculator.getMaxAvailableAmountToSend( + token = token, + recipient = recipientAddress.address.toBase58Instance() + ) + // set max available amount + ?.also { calculationMode.setMaxAmounts(it) } } - private fun setupInitialToken(view: NewSendContract.View) { - launch { - // We should find SOL anyway because SOL is needed for Selection Mechanism - // todo: check this logic as user definitely may have empty account, we should not error by this reason - val userNonZeroTokens = getNonZeroUserTokensOrSol() - if (userNonZeroTokens.isEmpty()) { - Timber.tag(TAG).e(SendFatalError("User non-zero tokens can't be empty!")) - // we cannot proceed if user tokens are not loaded - view.showUiKitSnackBar(resources.getString(R.string.error_general_message)) - return@launch - } + private suspend fun setupInitialToken(view: NewSendContract.View) { + // We should find SOL anyway because SOL is needed for Selection Mechanism + // todo: check this logic as user definitely may have empty account, we should not error by this reason + val userNonZeroTokens = getNonZeroUserTokensOrSol() + if (userNonZeroTokens.isEmpty()) { + Timber.tag(TAG).e(SendFatalError("User non-zero tokens can't be empty!")) + // we cannot proceed if user tokens are not loaded + view.showUiKitSnackBar(resources.getString(R.string.error_general_message)) + return + } - val isTokenChangeEnabled = userNonZeroTokens.size > 1 && selectedToken == null - view.setTokenContainerEnabled(isEnabled = isTokenChangeEnabled) + val isTokenChangeEnabled = userNonZeroTokens.size > 1 && selectedToken == null + view.setTokenContainerEnabled(isEnabled = isTokenChangeEnabled) + + val initialToken = if (selectedToken != null) selectedToken!! else userNonZeroTokens.first() + token = initialToken + + // paths where this fee payer token is settling down: + // : sendFeeRelayerManager.initialize + // -> sendFeeRelayerManager.initializeWithToken + // -> sendInteractor.initialize + // : sendFeeRelayerManager.executeSmartSelection + // -> if(!initializeCompleted) + // -> sendFeeRelayerManager.initializeWithToken + // -> sendInteractor.initialize + // so send interactor is the single source of truth for the fee payer token + val feePayerToken = requireDefaultFeePayerToken(userNonZeroTokens) + + checkTokenRatesAndSetSwitchAmountState(initialToken) + + val solToken = if (initialToken.isSOL) initialToken else tokenServiceCoordinator.getUserSolToken() + if (solToken == null) { + // we cannot proceed without SOL. + view.showUiKitSnackBar(resources.getString(R.string.error_general_message)) + Timber.tag(TAG).e(SendFatalError("Couldn't find user's SOL account!")) + return + } + maximumAmountCalculator.getMaxAvailableAmountToSend( + token = initialToken, + recipient = recipientAddress.address.toBase58Instance() + ) + ?.also { calculationMode.setMaxAmounts(it) } - val initialToken = if (selectedToken != null) selectedToken!! else userNonZeroTokens.first() - token = initialToken + initializeFeeRelayer( + view = view, + sourceToken = requireToken(), + feePayerToken = feePayerToken, + solToken = solToken + ) + initialAmount?.let(::setupDefaultFields) + } - checkTokenRatesAndSetSwitchAmountState(initialToken) + private suspend fun requireDefaultFeePayerToken(userNonZeroTokens: List): Token.Active { + val restoredFeePayerTokenMint = sendInteractor.restoreSavedFeePayerToken() ?: return requireToken() - val solToken = if (initialToken.isSOL) initialToken else tokenServiceCoordinator.getUserSolToken() - if (solToken == null) { - // we cannot proceed without SOL. - view.showUiKitSnackBar(resources.getString(R.string.error_general_message)) - Timber.tag(TAG).e(SendFatalError("Couldn't find user's SOL account!")) - return@launch - } - maximumAmountCalculator.getMaxAvailableAmountToSend( - token = initialToken, - recipient = recipientAddress.address.toBase58Instance() - ) - ?.also { calculationMode.setMaxAmounts(it) } + val restoredToken = userNonZeroTokens.firstOrNull { + it.mintAddressB58 == restoredFeePayerTokenMint + } - initializeFeeRelayer(view, initialToken, solToken) - initialAmount?.let(::setupDefaultFields) + if (restoredToken == null || !sendInteractor.checkTokenIsValidFeePayer(restoredToken)) { + sendInteractor.removeSavedFeePayerToken() + return requireToken() } + + Timber.d("Using user defined fee payer token ${restoredToken.tokenSymbol}") + return restoredToken } /** @@ -307,26 +341,29 @@ class NewSendPresenter( private suspend fun initializeFeeRelayer( view: NewSendContract.View, - initialToken: Token.Active, + sourceToken: Token.Active, + feePayerToken: Token.Active, solToken: Token.Active ) { - val initialFeeLabel = if (initialToken.isToken2022) { - resources.getString(R.string.send_fees) - } else { + val initialFeeLabel = if (sourceToken.isToken2022) { resources.getString(R.string.send_fees_token2022_format) + } else { + resources.getString(R.string.send_fees) } view.setFeeLabel(initialFeeLabel) view.setBottomButtonText(TextContainer.Res(R.string.send_calculating_fees)) - sendFeeRelayerManager.initialize(initialToken, solToken, recipientAddress) + sendFeeRelayerManager.initialize(feePayerToken, solToken, recipientAddress) executeSmartSelection( token = requireToken(), - feePayerToken = requireToken(), strategy = SELECT_FEE_PAYER, useCache = false ) - updateButton(sourceToken = initialToken, feeRelayerState = sendFeeRelayerManager.getState()) + updateButton( + sourceToken = sourceToken, + feeRelayerState = sendFeeRelayerManager.getState() + ) } override fun onTokenClicked() { @@ -352,11 +389,10 @@ class NewSendPresenter( updateButton(requireToken(), sendFeeRelayerManager.getState()) /* - * Calculating if we can pay with current token instead of already selected fee payer token - * */ + * Calculating if we can pay with current token instead of already selected fee payer token + */ executeSmartSelection( token = requireToken(), - feePayerToken = requireToken(), strategy = CORRECT_AMOUNT, useCache = false ) @@ -389,7 +425,6 @@ class NewSendPresenter( * */ executeSmartSelection( token = requireToken(), - feePayerToken = requireToken(), strategy = SELECT_FEE_PAYER ) } @@ -405,20 +440,21 @@ class NewSendPresenter( /* * Calculating if we can pay with current token instead of already selected fee payer token - * */ + */ executeSmartSelection( token = requireToken(), - feePayerToken = requireToken(), strategy = SELECT_FEE_PAYER ) } override fun updateFeePayerToken(feePayerToken: Token.Active) { try { + launch { + sendInteractor.saveFeePayerToken(feePayerToken) + } sendInteractor.setFeePayerToken(feePayerToken) executeSmartSelection( token = requireToken(), - feePayerToken = feePayerToken, strategy = NO_ACTION ) } catch (e: Throwable) { @@ -455,7 +491,6 @@ class NewSendPresenter( // already selected fee payer token executeSmartSelection( token = requireToken(), - feePayerToken = requireToken(), strategy = CORRECT_AMOUNT ) } @@ -537,7 +572,6 @@ class NewSendPresenter( ) view?.showProgressDialog(internalTransactionId, progressDetails) -// val result = sendInteractor.sendTransaction(address.toPublicKey(), token, lamports) val result = sendInteractor.sendTransactionV2(address.toPublicKey(), token, lamports) userInteractor.addRecipient(recipientAddress, Date(transactionDate.dateMilli())) @@ -578,7 +612,6 @@ class NewSendPresenter( * */ private fun executeSmartSelection( token: Token.Active, - feePayerToken: Token.Active?, strategy: FeePayerSelectionStrategy, useCache: Boolean = true ) { @@ -586,7 +619,7 @@ class NewSendPresenter( feePayerJob = launch { sendFeeRelayerManager.executeSmartSelection( sourceToken = token, - feePayerToken = feePayerToken, + feePayerToken = sendInteractor.getFeePayerToken(), strategy = strategy, tokenAmount = calculationMode.getCurrentAmount(), useCache = useCache @@ -651,13 +684,11 @@ class NewSendPresenter( } private fun buildDebugInfo(solanaFee: SendSolanaFee?) { - launch { - val debugInfo = sendFeeRelayerManager.buildDebugInfo(solanaFee) - .plus("\n") - .plus(calculationMode.getDebugInfo()) + val debugInfo = sendFeeRelayerManager.buildDebugInfo(solanaFee) + .plus("\n") + .plus(calculationMode.getDebugInfo()) - view?.showDebugInfo(debugInfo) - } + view?.showDebugInfo(debugInfo) } private fun isInternetConnectionEnabled(): Boolean = diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index edcb5154d0..209f0a4340 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -6,7 +6,7 @@ import java.math.RoundingMode import org.p2p.core.token.Token import org.p2p.core.utils.Constants.FIAT_FRACTION_LENGTH -private val POWER_VALUE = BigDecimal.TEN +private val BASE_TEN = BigDecimal.TEN const val SOL_DECIMALS = 9 const val MOONPAY_DECIMAL = 2 const val STRIGA_FIAT_DECIMALS = 2 @@ -26,7 +26,7 @@ fun String?.toBigIntegerOrZero(): BigInteger { return this?.toBigIntegerOrNull() ?: BigInteger.ZERO } -fun Int.toPowerValue(): BigDecimal = POWER_VALUE.pow(this) +fun Int.toPowerValue(): BigDecimal = BASE_TEN.pow(this) fun BigDecimal.scaleShortOrFirstNotZero(): BigDecimal { return if (isZero()) { @@ -57,7 +57,7 @@ fun BigDecimal.scaleLong(decimals: Int = SCALE_VALUE_LONG): BigDecimal = // do not use BigDecimal(double) sometimes it makes the amount less // example: pass 0.030, get 0.029 fun BigInteger.fromLamports(decimals: Int): BigDecimal = - (this.toBigDecimal().divide(POWER_VALUE.pow(decimals), 18, RoundingMode.HALF_DOWN)) + (this.toBigDecimal().divide(BASE_TEN.pow(decimals), 18, RoundingMode.HALF_DOWN)) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 .scaleLong(decimals) @@ -100,6 +100,17 @@ fun BigDecimal.formatTokenWithSymbol( return "$formattedAmount $tokenSymbol" } +fun BigDecimal.formatTokenWithSymbol( + token: Token.Active, + exactDecimals: Boolean = false, + keepInitialDecimals: Boolean = false, +): String = formatTokenWithSymbol( + token.tokenSymbol, + token.decimals, + exactDecimals, + keepInitialDecimals +) + // case: 10000.000000007900 -> 100 000.00 fun BigDecimal.formatTokenForMoonpay(): String = formatWithDecimals(MOONPAY_DECIMAL) diff --git a/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt b/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt index 9d2670241e..5e731dd8d5 100644 --- a/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt +++ b/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt @@ -2,6 +2,7 @@ package org.p2p.token.service.converter import java.math.BigInteger import org.p2p.core.crypto.Base58String +import org.p2p.core.utils.isZero import org.p2p.token.service.api.TokenServiceDataSource import org.p2p.token.service.api.request.TokenAmountsBodyRequest import org.p2p.token.service.api.request.TokenAmountsRequest @@ -15,6 +16,12 @@ internal class TokenServiceAmountsRemoteConverter( amountFrom: Pair, mintsToConvertTo: List ): Map { + + // do not request if amount is zero, output will obviously be zero + if (amountFrom.second.isZero()) { + return mintsToConvertTo.associateWith { BigInteger.ZERO } + } + val request = TokenAmountsBodyRequest( vsTokenMint = amountFrom.first.base58Value, amountLamports = amountFrom.second.toString(), From a77087b34f6bc447e5b535584d478991fa177242 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:00:00 +0300 Subject: [PATCH 30/51] PWN-934 - Fix the rule of min swap in Swap (#2197) --- .../statemanager/SwapFeatureException.kt | 2 +- .../statemanager/SwapStateRoutesRefresher.kt | 11 ++-- .../SwapMinimumSolAmountValidator.kt | 55 ++++++------------- .../jupiter/ui/main/JupiterSwapPresenter.kt | 3 +- .../ui/main/mapper/SwapButtonMapper.kt | 11 ++-- .../presenter/JupiterSwapSettingsPresenter.kt | 18 +++--- 6 files changed, 37 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapFeatureException.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapFeatureException.kt index a5bf1522ea..f66fe139ec 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapFeatureException.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapFeatureException.kt @@ -12,7 +12,7 @@ open class SwapFeatureException : Exception() { data class InsufficientSolBalance( val inputAmount: BigDecimal, val userSolToken: SwapTokenModel.UserToken, - val remainingAmount: BigDecimal, + val allowedAmount: BigDecimal, ) : SwapFeatureException() object SameTokens : SwapFeatureException() diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt index 68b8a0e294..16469ef904 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateRoutesRefresher.kt @@ -32,11 +32,12 @@ class SwapStateRoutesRefresher( amountTokenA: BigDecimal, slippage: Slippage, ) { - minSolBalanceValidator.validateMinimumSolAmount( - tokenA = tokenA, - newAmount = amountTokenA, - slippage = slippage - ) + if (tokenA !is SwapTokenModel.JupiterToken && tokenA.isWrappedSol) { + minSolBalanceValidator.validateMinimumSolAmount( + solToken = tokenA as SwapTokenModel.UserToken, + newAmount = amountTokenA, + ) + } swapValidator.validateIsSameTokens(tokenA = tokenA, tokenB = tokenB) state.value = SwapState.LoadingRoutes( tokenA = tokenA, diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt index fb67e12907..2b9357deee 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/validator/SwapMinimumSolAmountValidator.kt @@ -1,57 +1,36 @@ package org.p2p.wallet.jupiter.statemanager.validator import java.math.BigDecimal -import java.math.BigInteger 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.toLamports import org.p2p.wallet.jupiter.interactor.model.SwapTokenModel import org.p2p.wallet.jupiter.statemanager.SwapFeatureException import org.p2p.wallet.rpc.repository.amount.RpcAmountRepository -import org.p2p.wallet.swap.model.Slippage class SwapMinimumSolAmountValidator( private val rpcAmountRepository: RpcAmountRepository ) { - suspend fun validateMinimumSolAmount( - tokenA: SwapTokenModel, - newAmount: BigDecimal, - slippage: Slippage - ) { + /** + * We need to leave the min required balance for SOL account. + * It's about 0.0089088 SOL (890880 LAMPORTS) + * */ + suspend fun validateMinimumSolAmount(solToken: SwapTokenModel.UserToken, newAmount: BigDecimal) { + val minRentExemption = rpcAmountRepository.getMinBalanceForRentExemption(dataLength = 0) + val totalInLamports = solToken.tokenAmountInLamports - if (!tokenA.isWrappedSol) return + val newAmountInLamports = newAmount.toLamports(solToken.decimals) + val remainingBalance = totalInLamports - newAmountInLamports - val minRentExemption = rpcAmountRepository.getMinBalanceForRentExemption(0) - - val totalInLamports = when (tokenA) { - is SwapTokenModel.UserToken -> tokenA.tokenAmountInLamports - is SwapTokenModel.JupiterToken -> return + if (remainingBalance.isZero() || remainingBalance >= minRentExemption) { + return } - val newAmountInLamports = newAmount.toLamports(tokenA.decimals) - - val allowedAmountChange = (slippage.doubleValue * newAmountInLamports.toLong()) / 100 - - /** - * We need to leave the min required balance for SOL account. - * It's about 0.0089088 SOL (890880 LAMPORTS) - * - * But in swap operations, we have a risk of slippage value. - * We are adding it to the amount needed to be left on the account. - * */ - val amountNeeded = newAmountInLamports + BigInteger.valueOf(allowedAmountChange.toLong()) - - val remainingBalance = totalInLamports - amountNeeded - val remainingAmount = remainingBalance.fromLamports(tokenA.decimals) - - if (remainingBalance.isMoreThan(BigInteger.ZERO) && remainingBalance.isLessThan(minRentExemption)) { - throw SwapFeatureException.InsufficientSolBalance( - inputAmount = newAmount, - userSolToken = tokenA, - remainingAmount = remainingAmount, - ) - } + throw SwapFeatureException.InsufficientSolBalance( + inputAmount = newAmount, + userSolToken = solToken, + allowedAmount = (totalInLamports - minRentExemption).fromLamports(solToken.decimals) + ) } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt index b546e95f47..06db823abc 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/JupiterSwapPresenter.kt @@ -441,9 +441,8 @@ class JupiterSwapPresenter( notValidAmount = featureException.inputAmount ) val solToken = featureException.userSolToken - val remainingAmount = featureException.remainingAmount + val remainingAmount = featureException.allowedAmount view?.setButtonState(buttonState = buttonMapper.mapInsufficientSolBalance(solToken, remainingAmount)) - view?.showSolErrorToast() } } updateWidgets() diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/mapper/SwapButtonMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/mapper/SwapButtonMapper.kt index 551e1b9821..f29a27f0b2 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/main/mapper/SwapButtonMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/main/mapper/SwapButtonMapper.kt @@ -2,7 +2,7 @@ package org.p2p.wallet.jupiter.ui.main.mapper import java.math.BigDecimal import org.p2p.core.common.TextContainer -import org.p2p.core.utils.formatToken +import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.wallet.R import org.p2p.wallet.jupiter.interactor.model.SwapTokenModel import org.p2p.wallet.jupiter.ui.main.SwapButtonState @@ -26,14 +26,11 @@ class SwapButtonMapper { fun mapInsufficientSolBalance( solToken: SwapTokenModel.UserToken, - remainingAmount: BigDecimal + allowedAmount: BigDecimal ): SwapButtonState.Disabled { - val formatAmount = remainingAmount.formatToken(solToken.decimals) + val formatAmount = allowedAmount.formatTokenWithSymbol(solToken.tokenSymbol, solToken.decimals) return SwapButtonState.Disabled( - TextContainer( - R.string.swap_main_button_sol_error, - "$formatAmount ${solToken.tokenSymbol}" - ) + TextContainer(R.string.swap_main_button_sol_error, formatAmount) ) } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt index 67b3a39199..523b26b447 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/settings/presenter/JupiterSwapSettingsPresenter.kt @@ -14,7 +14,6 @@ import org.p2p.uikit.model.AnyCellItem import org.p2p.wallet.common.mvp.BasePresenter import org.p2p.wallet.jupiter.analytics.JupiterSwapSettingsAnalytics import org.p2p.wallet.jupiter.model.SwapRateTickerState -import org.p2p.wallet.jupiter.repository.model.JupiterSwapToken import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository import org.p2p.wallet.jupiter.statemanager.SwapState import org.p2p.wallet.jupiter.statemanager.SwapStateAction @@ -43,9 +42,8 @@ class JupiterSwapSettingsPresenter( private var featureState: SwapState = SwapState.InitialLoading - private var jupiterTokens: List = listOf() private var debounceInputJob: Job? = null - private var isSelectedCustom: Boolean? = null + private var isCustomSlippageSelected: Boolean? = null private var currentContentList = listOf() get() = field.addSlippageSettings(featureState) @@ -65,8 +63,8 @@ class JupiterSwapSettingsPresenter( private suspend fun handleFeatureState(state: SwapState) { featureState = state - if (isSelectedCustom == null) { - isSelectedCustom = state.getCurrentSlippage() is Slippage.Custom + if (isCustomSlippageSelected == null) { + isCustomSlippageSelected = state.getCurrentSlippage() is Slippage.Custom } val contentList = getContentListByFeatureState(state) currentContentList = contentList @@ -119,22 +117,22 @@ class JupiterSwapSettingsPresenter( val payload = item.payload ?: return when (payload) { SwapSlippagePayload.ZERO_POINT_ONE -> { - isSelectedCustom = false + isCustomSlippageSelected = false analytics.logSlippageChangedClicked(Slippage.Min) stateManager.onNewAction(SwapStateAction.SlippageChanged(Slippage.Min)) } SwapSlippagePayload.ZERO_POINT_FIVE -> { - isSelectedCustom = false + isCustomSlippageSelected = false analytics.logSlippageChangedClicked(Slippage.Medium) stateManager.onNewAction(SwapStateAction.SlippageChanged(Slippage.Medium)) } SwapSlippagePayload.ONE -> { - isSelectedCustom = false + isCustomSlippageSelected = false analytics.logSlippageChangedClicked(Slippage.One) stateManager.onNewAction(SwapStateAction.SlippageChanged(Slippage.One)) } SwapSlippagePayload.CUSTOM -> { - isSelectedCustom = true + isCustomSlippageSelected = true view?.bindSettingsList(currentContentList) } is SwapSettingsPayload -> { @@ -252,7 +250,7 @@ class JupiterSwapSettingsPresenter( val currentSlippage = state.getCurrentSlippage() return this + commonMapper.mapSlippageList( slippage = currentSlippage, - isSelectedCustom = isSelectedCustom ?: false + isSelectedCustom = isCustomSlippageSelected ?: false ) } } From 3304d061ca34dba378af6d560e2af82851dce352 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Thu, 15 Feb 2024 14:19:40 +0400 Subject: [PATCH 31/51] ETH-864 - fixed deeplinks handling (#2198) --- .../auth/ui/pin/signin/SignInPinFragment.kt | 6 +- .../home/deeplinks/DeeplinkScreenNavigator.kt | 8 ++- .../deeplinks/MainFragmentDeeplinkHandler.kt | 22 +++++-- .../ui/container/MainContainerContract.kt | 8 +-- .../ui/container/MainContainerFragment.kt | 55 ---------------- .../ui/container/MainContainerPresenter.kt | 46 +------------ .../MainFragmentDeeplinkHandlerFactory.kt | 9 ++- .../java/org/p2p/wallet/root/RootActivity.kt | 64 ++++++++++++++++++- .../java/org/p2p/wallet/root/RootContract.kt | 7 +- .../java/org/p2p/wallet/root/RootPresenter.kt | 37 ++++++++++- .../p2p/wallet/utils/NavigationExtensions.kt | 26 +++++++- 11 files changed, 161 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt b/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt index a2fded1ed1..1a11b29f2d 100644 --- a/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt +++ b/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt @@ -6,17 +6,17 @@ import androidx.appcompat.app.AlertDialog import android.os.Bundle import android.view.View import org.koin.android.ext.android.inject +import javax.crypto.Cipher import org.p2p.wallet.R import org.p2p.wallet.auth.ui.onboarding.root.OnboardingRootFragment import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.databinding.FragmentSignInPinBinding -import org.p2p.wallet.home.ui.container.MainContainerFragment import org.p2p.wallet.intercom.IntercomService +import org.p2p.wallet.root.RootActivity import org.p2p.wallet.utils.BiometricPromptWrapper import org.p2p.wallet.utils.popAndReplaceFragment import org.p2p.wallet.utils.vibrate import org.p2p.wallet.utils.viewbinding.viewBinding -import javax.crypto.Cipher class SignInPinFragment : BaseMvpFragment(R.layout.fragment_sign_in_pin), @@ -74,7 +74,7 @@ class SignInPinFragment : override fun onSignInSuccess() { binding.pinView.onSuccessPin() - popAndReplaceFragment(MainContainerFragment.create(), inclusive = true) + (activity as? RootActivity)?.handleSigninSuccess() } override fun onLogout() { diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt index b37717d292..56c5a66a75 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt @@ -8,13 +8,15 @@ import org.p2p.wallet.jupiter.ui.main.SwapDeeplinkStrictTokenWarning interface DeeplinkScreenNavigator { fun navigateToBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) fun navigateToBuyScreen(token: Token) - fun showCashOut() - fun showSwapWithArgs( + fun navigateToSendNoTokens(fallbackToken: Token) + fun navigateToSendScreen() + fun navigateToCashOut() + fun navigateToSwapWithArgs( tokenAMint: Base58String, tokenBMint: Base58String, amountA: String, strictWarning: SwapDeeplinkStrictTokenWarning?, source: SwapOpenedFrom ) - fun showSwap() + fun navigateToSwap() } diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt index 14ea615c2b..9ab3ac26eb 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt @@ -4,11 +4,13 @@ import timber.log.Timber import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.p2p.core.crypto.toBase58Instance +import org.p2p.core.token.Token import org.p2p.wallet.deeplinks.DeeplinkData import org.p2p.wallet.deeplinks.DeeplinkTarget import org.p2p.wallet.deeplinks.ReferralDeeplinkHandler import org.p2p.wallet.deeplinks.SwapDeeplinkData import org.p2p.wallet.deeplinks.SwapDeeplinkHandler +import org.p2p.wallet.home.ui.wallet.analytics.MainScreenAnalytics import org.p2p.wallet.infrastructure.coroutines.waitForCondition import org.p2p.wallet.jupiter.model.SwapOpenedFrom import org.p2p.wallet.jupiter.ui.main.SwapDeeplinkStrictTokenWarning @@ -28,6 +30,7 @@ class MainFragmentDeeplinkHandler( private val swapDeeplinkHandler: SwapDeeplinkHandler, private val deeplinkTopLevelHandler: (target: DeeplinkTarget) -> Unit, private val referralDeeplinkHandler: ReferralDeeplinkHandler, + private val mainScreenAnalytics: MainScreenAnalytics, ) { suspend fun handle(data: DeeplinkData) { @@ -43,13 +46,13 @@ class MainFragmentDeeplinkHandler( } private fun handleCashOutDeeplink() { - screenNavigator?.showCashOut() + screenNavigator?.navigateToCashOut() } private suspend fun handleSwapDeeplink(data: DeeplinkData) { when (val parsedData = swapDeeplinkHandler.parseDeeplink(data)) { is SwapDeeplinkData.TokensFound -> { - screenNavigator?.showSwapWithArgs( + screenNavigator?.navigateToSwapWithArgs( tokenAMint = parsedData.tokenAMint, tokenBMint = parsedData.tokenBMint, amountA = "0", @@ -59,7 +62,7 @@ class MainFragmentDeeplinkHandler( } is SwapDeeplinkData.NonStrictTokensFound -> { // pass empty strings to rollback tokens to default values - screenNavigator?.showSwapWithArgs( + screenNavigator?.navigateToSwapWithArgs( tokenAMint = emptyString().toBase58Instance(), tokenBMint = emptyString().toBase58Instance(), amountA = "0", @@ -71,7 +74,7 @@ class MainFragmentDeeplinkHandler( ) } SwapDeeplinkData.TokensNotFound -> { - screenNavigator?.showSwap() + screenNavigator?.navigateToSwap() } } } @@ -82,7 +85,16 @@ class MainFragmentDeeplinkHandler( private suspend fun handleSendDeeplink() { waitForCondition(1000) { tokenServiceCoordinator.getUserTokens().isNotEmpty() } - deeplinkTopLevelHandler(DeeplinkTarget.SEND) + mainScreenAnalytics.logMainScreenSendClick() + + val userTokens = tokenServiceCoordinator.getUserTokens() + val isAccountEmpty = userTokens.isEmpty() || userTokens.all(Token.Active::isZero) + if (isAccountEmpty) { + val validTokenToBuy = userInteractor.getSingleTokenForBuy() ?: return + screenNavigator?.navigateToSendNoTokens(validTokenToBuy) + } else { + screenNavigator?.navigateToSendScreen() + } } /** diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt index a2ee925604..11fa9a7163 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerContract.kt @@ -1,15 +1,13 @@ package org.p2p.wallet.home.ui.container import androidx.annotation.MenuRes -import org.p2p.core.token.Token import org.p2p.wallet.common.mvp.MvpPresenter import org.p2p.wallet.common.mvp.MvpView import org.p2p.wallet.deeplinks.DeeplinkData -import org.p2p.wallet.home.deeplinks.DeeplinkScreenNavigator interface MainContainerContract { - interface View : MvpView, DeeplinkScreenNavigator { + interface View : MvpView { fun setMainNavigationConfiguration(screensConfiguration: List) fun inflateBottomNavigationMenu(@MenuRes menuRes: Int) fun showSettingsBadgeVisible(isVisible: Boolean) @@ -17,9 +15,6 @@ interface MainContainerContract { fun showWalletBadgeVisible(isVisible: Boolean) fun showWalletBalance(balance: String) fun navigateToTabFromDeeplink(data: DeeplinkData) - - fun navigateToSendNoTokens(fallbackToken: Token) - fun navigateToSendScreen() } interface Presenter : MvpPresenter { @@ -31,6 +26,5 @@ interface MainContainerContract { fun logCryptoOpened() fun logHistoryOpened() fun logSettingsOpened() - fun handleSendDeeplink() } } diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt index 812b298a3e..8a678e52eb 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerFragment.kt @@ -9,8 +9,6 @@ import android.view.View import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.koin.android.ext.android.inject import kotlin.reflect.KClass -import org.p2p.core.crypto.Base58String -import org.p2p.core.token.Token import org.p2p.core.utils.insets.doOnApplyWindowInsets import org.p2p.core.utils.insets.ime import org.p2p.core.utils.insets.systemBars @@ -25,19 +23,10 @@ import org.p2p.wallet.deeplinks.DeeplinkData import org.p2p.wallet.deeplinks.DeeplinkTarget import org.p2p.wallet.deeplinks.MainTabsSwitcher import org.p2p.wallet.home.ui.main.MainFragmentOnCreateAction -import org.p2p.wallet.jupiter.model.SwapOpenedFrom -import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment -import org.p2p.wallet.jupiter.ui.main.SwapDeeplinkStrictTokenWarning -import org.p2p.wallet.moonpay.ui.new.BuyFragment import org.p2p.wallet.notification.AppNotificationManager import org.p2p.wallet.push_notifications.analytics.AnalyticsPushChannel -import org.p2p.wallet.sell.ui.payload.SellPayloadFragment -import org.p2p.wallet.send.ui.SearchOpenedFromScreen -import org.p2p.wallet.send.ui.search.NewSearchFragment -import org.p2p.wallet.send.ui.stub.SendUnavailableFragment import org.p2p.wallet.utils.args import org.p2p.wallet.utils.doOnAnimationEnd -import org.p2p.wallet.utils.replaceFragment import org.p2p.wallet.utils.viewbinding.viewBinding import org.p2p.wallet.utils.withArgs @@ -166,50 +155,6 @@ class MainContainerFragment : ) } - override fun navigateToSendNoTokens(fallbackToken: Token) { - replaceFragment(SendUnavailableFragment.create(fallbackToken)) - } - - override fun navigateToSendScreen() { - replaceFragment(NewSearchFragment.create(SearchOpenedFromScreen.MAIN)) - } - - //region DeeplinkScreenNavigator - override fun navigateToBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) { - replaceFragment(BuyFragment.create(token, fiatToken, fiatAmount)) - } - - override fun navigateToBuyScreen(token: Token) { - replaceFragment(BuyFragment.create(token)) - } - - override fun showCashOut() { - replaceFragment(SellPayloadFragment.create()) - } - - override fun showSwapWithArgs( - tokenAMint: Base58String, - tokenBMint: Base58String, - amountA: String, - strictWarning: SwapDeeplinkStrictTokenWarning?, - source: SwapOpenedFrom - ) { - replaceFragment( - JupiterSwapFragment.create( - tokenAMint = tokenAMint, - tokenBMint = tokenBMint, - amountA = amountA, - strictWarning = strictWarning, - source = source - ) - ) - } - - override fun showSwap() { - replaceFragment(JupiterSwapFragment.create(source = SwapOpenedFrom.MAIN_SCREEN)) - } - //endregion - override fun navigateToTabFromDeeplink(data: DeeplinkData) { when (data.target) { DeeplinkTarget.HISTORY -> navigate(ScreenTab.HISTORY_SCREEN) diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt index 6fbbb75195..a36bcefd8f 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt @@ -1,6 +1,5 @@ package org.p2p.wallet.home.ui.container -import timber.log.Timber import java.math.BigDecimal import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -25,29 +24,17 @@ import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment import org.p2p.wallet.settings.ui.settings.SettingsFragment import org.p2p.wallet.tokenservice.TokenServiceCoordinator import org.p2p.wallet.tokenservice.UserTokensState -import org.p2p.wallet.user.interactor.UserInteractor -import org.p2p.wallet.utils.unsafeLazy class MainContainerPresenter( private val deeplinksManager: AppDeeplinksManager, private val connectionManager: ConnectionManager, private val tokenServiceCoordinator: TokenServiceCoordinator, private val metadataInteractor: MetadataInteractor, - private val userInteractor: UserInteractor, private val walletStrigaInteractor: WalletStrigaInteractor, private val balanceMapper: WalletBalanceMapper, - private val deeplinkHandlerFactory: MainFragmentDeeplinkHandlerFactory, private val mainScreenAnalytics: MainScreenAnalytics, ) : BasePresenter(), MainContainerContract.Presenter { - private val deeplinkHandler by unsafeLazy { - deeplinkHandlerFactory.create( - navigator = view, - scope = this, - deeplinkTopLevelHandler = ::handleDeeplinkTarget - ) - } - override fun attach(view: MainContainerContract.View) { super.attach(view) observeInternetState() @@ -78,23 +65,15 @@ class MainContainerPresenter( } override fun initializeDeeplinks() { + // deeplinks that should be handled only at MainContainer level, because it's a tab switching val supportedTargets = setOf( DeeplinkTarget.HISTORY, - DeeplinkTarget.SETTINGS, - DeeplinkTarget.BUY, - DeeplinkTarget.SEND, - DeeplinkTarget.SWAP, - DeeplinkTarget.CASH_OUT, - DeeplinkTarget.REFERRAL + DeeplinkTarget.SETTINGS ) launchSupervisor { deeplinksManager.subscribeOnDeeplinks(supportedTargets) .onEach { view?.navigateToTabFromDeeplink(it) } - .onEach(deeplinkHandler::handle) .launchIn(this) - - deeplinksManager.executeHomePendingDeeplink() - deeplinksManager.executeTransferPendingAppLink() } } @@ -120,20 +99,6 @@ class MainContainerPresenter( mainScreenAnalytics.logMainScreenSettingsClick() } - override fun handleSendDeeplink() { - mainScreenAnalytics.logMainScreenSendClick() - launch { - val userTokens = tokenServiceCoordinator.getUserTokens() - val isAccountEmpty = userTokens.isEmpty() || userTokens.all(Token.Active::isZero) - if (isAccountEmpty) { - val validTokenToBuy = userInteractor.getSingleTokenForBuy() ?: return@launch - view?.navigateToSendNoTokens(validTokenToBuy) - } else { - view?.navigateToSendScreen() - } - } - } - private fun observeInternetState() { connectionManager.connectionStatus .onEach { isConnected -> @@ -142,13 +107,6 @@ class MainContainerPresenter( .launchIn(this) } - private fun handleDeeplinkTarget(target: DeeplinkTarget) { - when (target) { - DeeplinkTarget.SEND -> handleSendDeeplink() - else -> Timber.d("Unsupported deeplink target! $target") - } - } - private fun checkDeviceShare() { val hasDifferentDeviceShare = metadataInteractor.hasDifferentDeviceShare() view?.showSettingsBadgeVisible(isVisible = hasDifferentDeviceShare) diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainFragmentDeeplinkHandlerFactory.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainFragmentDeeplinkHandlerFactory.kt index e5a3e0d032..0b87e0e38b 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainFragmentDeeplinkHandlerFactory.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainFragmentDeeplinkHandlerFactory.kt @@ -6,6 +6,7 @@ import org.p2p.wallet.deeplinks.ReferralDeeplinkHandler import org.p2p.wallet.deeplinks.SwapDeeplinkHandler import org.p2p.wallet.home.deeplinks.DeeplinkScreenNavigator import org.p2p.wallet.home.deeplinks.MainFragmentDeeplinkHandler +import org.p2p.wallet.home.ui.wallet.analytics.MainScreenAnalytics import org.p2p.wallet.tokenservice.TokenServiceCoordinator import org.p2p.wallet.user.interactor.UserInteractor @@ -13,12 +14,13 @@ class MainFragmentDeeplinkHandlerFactory( private val tokenServiceCoordinator: TokenServiceCoordinator, private val userInteractor: UserInteractor, private val swapDeeplinkHandler: SwapDeeplinkHandler, - private val referralDeeplinkHandler: ReferralDeeplinkHandler + private val referralDeeplinkHandler: ReferralDeeplinkHandler, + private val mainScreenAnalytics: MainScreenAnalytics, ) { fun create( navigator: DeeplinkScreenNavigator?, scope: CoroutineScope, - deeplinkTopLevelHandler: (target: DeeplinkTarget) -> Unit, + deeplinkTopLevelHandler: (target: DeeplinkTarget) -> Unit = {}, ): MainFragmentDeeplinkHandler { return MainFragmentDeeplinkHandler( coroutineScope = scope, @@ -27,7 +29,8 @@ class MainFragmentDeeplinkHandlerFactory( userInteractor = userInteractor, swapDeeplinkHandler = swapDeeplinkHandler, referralDeeplinkHandler = referralDeeplinkHandler, - deeplinkTopLevelHandler = deeplinkTopLevelHandler + deeplinkTopLevelHandler = deeplinkTopLevelHandler, + mainScreenAnalytics = mainScreenAnalytics, ) } } diff --git a/app/src/main/java/org/p2p/wallet/root/RootActivity.kt b/app/src/main/java/org/p2p/wallet/root/RootActivity.kt index bce2f7b22e..bdf7f64609 100644 --- a/app/src/main/java/org/p2p/wallet/root/RootActivity.kt +++ b/app/src/main/java/org/p2p/wallet/root/RootActivity.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.p2p.core.crashlytics.CrashLogger import org.p2p.core.crashlytics.helpers.FragmentLoggingLifecycleListener +import org.p2p.core.crypto.Base58String +import org.p2p.core.token.Token import org.p2p.core.utils.KeyboardListener import org.p2p.uikit.natives.showSnackbarIndefinite import org.p2p.uikit.utils.toast @@ -38,7 +40,15 @@ import org.p2p.wallet.common.mvp.BaseMvpActivity import org.p2p.wallet.databinding.ActivityRootBinding import org.p2p.wallet.deeplinks.AppDeeplinksManager import org.p2p.wallet.home.ui.container.MainContainerFragment +import org.p2p.wallet.jupiter.model.SwapOpenedFrom +import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment +import org.p2p.wallet.jupiter.ui.main.SwapDeeplinkStrictTokenWarning import org.p2p.wallet.lokalise.LokaliseService +import org.p2p.wallet.moonpay.ui.new.BuyFragment +import org.p2p.wallet.sell.ui.payload.SellPayloadFragment +import org.p2p.wallet.send.ui.SearchOpenedFromScreen +import org.p2p.wallet.send.ui.search.NewSearchFragment +import org.p2p.wallet.send.ui.stub.SendUnavailableFragment import org.p2p.wallet.solana.SolanaNetworkObserver import org.p2p.wallet.solana.model.SolanaNetworkState import org.p2p.wallet.splash.SplashFragment @@ -46,7 +56,9 @@ import org.p2p.wallet.svl.interactor.SendViaLinkWrapper import org.p2p.wallet.svl.ui.receive.ReceiveViaLinkBottomSheet import org.p2p.wallet.transaction.model.NewShowProgress import org.p2p.wallet.transaction.ui.NewTransactionProgressBottomSheet +import org.p2p.wallet.utils.popAndReplaceFragment import org.p2p.wallet.utils.popBackStack +import org.p2p.wallet.utils.popBackStackTo import org.p2p.wallet.utils.replaceFragment class RootActivity : @@ -80,6 +92,7 @@ class RootActivity : private val networkObserver: SolanaNetworkObserver by inject() private val decorSystemBarsDelegate by lazy { DecorSystemBarsDelegate(this) } private val visibilityDelegate by lazy { ActivityVisibilityDelegate(this) } + override val visibilityState: StateFlow = visibilityDelegate.getState() override val keyboardState = MutableStateFlow(false) private lateinit var binding: ActivityRootBinding @@ -306,5 +319,54 @@ class RootActivity : ) } - override val visibilityState: StateFlow = visibilityDelegate.getState() + override fun navigateToBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) { + replaceFragment(BuyFragment.create(token, fiatToken, fiatAmount)) + } + + override fun navigateToBuyScreen(token: Token) { + replaceFragment(BuyFragment.create(token)) + } + + override fun navigateToCashOut() { + replaceFragment(SellPayloadFragment.create()) + } + + override fun navigateToSwapWithArgs( + tokenAMint: Base58String, + tokenBMint: Base58String, + amountA: String, + strictWarning: SwapDeeplinkStrictTokenWarning?, + source: SwapOpenedFrom + ) { + // popAndReplaceFragment overrides transition animations, popBackStackTo doesn't + popBackStackTo(MainContainerFragment::class) + replaceFragment( + target = JupiterSwapFragment.create( + tokenAMint = tokenAMint, + tokenBMint = tokenBMint, + amountA = amountA, + strictWarning = strictWarning, + source = source, + ) + ) + } + + override fun navigateToSwap() { + replaceFragment(JupiterSwapFragment.create(source = SwapOpenedFrom.MAIN_SCREEN)) + } + + override fun navigateToSendNoTokens(fallbackToken: Token) { + replaceFragment(SendUnavailableFragment.create(fallbackToken)) + } + + override fun navigateToSendScreen() { + replaceFragment(NewSearchFragment.create(SearchOpenedFromScreen.MAIN)) + } + + fun handleSigninSuccess() { + // do deeplinks handling init only after user signed in, otherwise it will be handled on splash + // and pin/biometric screens will be skipped + presenter.observeDeeplinks() + popAndReplaceFragment(MainContainerFragment.create(), inclusive = true) + } } diff --git a/app/src/main/java/org/p2p/wallet/root/RootContract.kt b/app/src/main/java/org/p2p/wallet/root/RootContract.kt index 5283637d75..16ba750232 100644 --- a/app/src/main/java/org/p2p/wallet/root/RootContract.kt +++ b/app/src/main/java/org/p2p/wallet/root/RootContract.kt @@ -3,13 +3,16 @@ package org.p2p.wallet.root import androidx.annotation.StringRes import org.p2p.wallet.common.mvp.MvpPresenter import org.p2p.wallet.common.mvp.MvpView +import org.p2p.wallet.home.deeplinks.DeeplinkScreenNavigator interface RootContract { - interface View : MvpView { + interface View : MvpView, DeeplinkScreenNavigator { fun showToast(@StringRes message: Int) fun showToast(message: String) } - interface Presenter : MvpPresenter + interface Presenter : MvpPresenter { + fun observeDeeplinks() + } } diff --git a/app/src/main/java/org/p2p/wallet/root/RootPresenter.kt b/app/src/main/java/org/p2p/wallet/root/RootPresenter.kt index 72efea80ce..27e8c7ecf4 100644 --- a/app/src/main/java/org/p2p/wallet/root/RootPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/root/RootPresenter.kt @@ -4,15 +4,20 @@ import timber.log.Timber import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.p2p.core.utils.addIf import org.p2p.wallet.common.feature_toggles.toggles.remote.SellEnabledFeatureToggle import org.p2p.wallet.common.mvp.BasePresenter +import org.p2p.wallet.deeplinks.AppDeeplinksManager +import org.p2p.wallet.deeplinks.DeeplinkTarget import org.p2p.wallet.feerelayer.interactor.FeeRelayerAccountInteractor -import org.p2p.wallet.jupiter.repository.routes.JupiterSwapRoutesRepository +import org.p2p.wallet.home.ui.container.MainFragmentDeeplinkHandlerFactory import org.p2p.wallet.jupiter.repository.tokens.JupiterSwapTokensRepository import org.p2p.wallet.sell.interactor.SellInteractor import org.p2p.wallet.swap.interactor.orca.OrcaInfoInteractor +import org.p2p.wallet.utils.unsafeLazy private const val TAG = "RootPresenter" @@ -21,10 +26,18 @@ class RootPresenter( private val feeRelayerAccountInteractor: FeeRelayerAccountInteractor, private val sellInteractor: SellInteractor, private val swapTokensRepository: JupiterSwapTokensRepository, - private val swapRoutesRepository: JupiterSwapRoutesRepository, - private val sellEnabledFeatureToggle: SellEnabledFeatureToggle + private val sellEnabledFeatureToggle: SellEnabledFeatureToggle, + private val deeplinksManager: AppDeeplinksManager, + private val deeplinkHandlerFactory: MainFragmentDeeplinkHandlerFactory, ) : BasePresenter(), RootContract.Presenter { + private val deeplinkHandler by unsafeLazy { + deeplinkHandlerFactory.create( + navigator = view, + scope = this, + ) + } + override fun attach(view: RootContract.View) { super.attach(view) loadInitialData() @@ -57,4 +70,22 @@ class RootPresenter( .also { Timber.tag(TAG).i("Total requests added: ${it.size}") } + + override fun observeDeeplinks() { + launchSupervisor { + val supportedTargets = setOf( + DeeplinkTarget.BUY, + DeeplinkTarget.SEND, + DeeplinkTarget.SWAP, + DeeplinkTarget.CASH_OUT, + DeeplinkTarget.REFERRAL + ) + deeplinksManager.subscribeOnDeeplinks(supportedTargets) + .onEach(deeplinkHandler::handle) + .launchIn(this) + + deeplinksManager.executeHomePendingDeeplink() + deeplinksManager.executeTransferPendingAppLink() + } + } } diff --git a/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt b/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt index ee43289bd9..0911c74999 100644 --- a/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt +++ b/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt @@ -82,8 +82,32 @@ fun Fragment.popAndReplaceFragment( @AnimRes popEnter: Int = R.anim.nav_pop_enter, @AnimRes popExit: Int = R.anim.nav_pop_exit, fragmentManager: FragmentManager = requireActivity().supportFragmentManager +) = requireActivity().popAndReplaceFragment( + target = target, + popTo = popTo, + containerId = containerId, + addToBackStack = addToBackStack, + inclusive = inclusive, + enter = enter, + exit = exit, + popEnter = popEnter, + popExit = popExit, + fragmentManager = fragmentManager +) + +fun FragmentActivity.popAndReplaceFragment( + target: Fragment, + popTo: KClass? = null, + @IdRes containerId: Int = R.id.rootContainer, + addToBackStack: Boolean = true, + inclusive: Boolean = false, + @AnimRes enter: Int = R.anim.nav_enter, + @AnimRes exit: Int = R.anim.nav_exit, + @AnimRes popEnter: Int = R.anim.nav_pop_enter, + @AnimRes popExit: Int = R.anim.nav_pop_exit, + fragmentManager: FragmentManager = supportFragmentManager ) = whenStateAtLeast(Lifecycle.State.STARTED) { - requireActivity().hideKeyboard() + hideKeyboard() with(fragmentManager) { // Override exit animation for popping fragment if (this@popAndReplaceFragment is BaseFragmentContract) { From 944ed56cfd9172216d9f88f0ca33997fd49a4d96 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:22:35 +0300 Subject: [PATCH 32/51] PWN-961 - Group prices requests and navigate to swap (#2199) * PWN-961 - group price requests * PWN-966 - navigate to swap for bridges --- .../main/assets/referral_bridge_provider.js | 3 ++ .../tokens/db/SwapTokensDaoDelegate.kt | 20 +++++++------ .../org/p2p/wallet/referral/ReferralBridge.kt | 6 ++++ .../p2p/wallet/referral/ReferralFragment.kt | 10 ++++++- .../wallet/user/interactor/UserInteractor.kt | 28 +++++++++---------- .../user/interactor/UserTokensInteractor.kt | 2 +- .../repository/UserTokensRemoteRepository.kt | 14 ++-------- .../p2p/token/service/TokenServiceModule.kt | 2 +- .../repository/TokenServiceRepository.kt | 8 +++++- .../repository/TokenServiceRepositoryImpl.kt | 13 +++++++-- .../price/JupiterTokenPriceRepository.kt | 19 +++++++++++-- 11 files changed, 83 insertions(+), 42 deletions(-) diff --git a/app/src/main/assets/referral_bridge_provider.js b/app/src/main/assets/referral_bridge_provider.js index e50c165c85..fa51d46165 100644 --- a/app/src/main/assets/referral_bridge_provider.js +++ b/app/src/main/assets/referral_bridge_provider.js @@ -15,6 +15,9 @@ window.ReferralBridge = { openTermsUrl: function(link) { AndroidReferralBridge.openTermsUrl(link); }, + navigateToSwap: function() { + AndroidReferralBridge.navigateToSwap(); + }, getClipboardContent: function() { return JSON.parse(AndroidReferralBridge.getClipboardValue()); }, diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDaoDelegate.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDaoDelegate.kt index aaa374d5aa..be2872421e 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDaoDelegate.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/tokens/db/SwapTokensDaoDelegate.kt @@ -4,14 +4,15 @@ import org.json.JSONArray import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.toBase58Instance import org.p2p.core.token.TokenExtensions +import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.wallet.jupiter.api.response.tokens.JupiterTokenResponse import org.p2p.wallet.jupiter.repository.model.JupiterSwapToken -import org.p2p.wallet.user.repository.UserLocalRepository import org.p2p.wallet.utils.toStringSet class SwapTokensDaoDelegate( private val dao: SwapTokensDao, - private val userLocalRepository: UserLocalRepository + private val tokenServiceRepository: TokenServiceRepository, ) { private val tokenEntityInserter = SwapTokenEntityInserter(dao) @@ -44,17 +45,20 @@ class SwapTokensDaoDelegate( } private suspend fun SwapTokenEntity.toDomain(): JupiterSwapToken { - val appTokens = userLocalRepository.findTokenByMint(address) + val appTokens = tokenServiceRepository.findTokenMetadataByAddress( + networkChain = TokenServiceNetwork.SOLANA, + tokenAddress = address + ) return JupiterSwapToken( - tokenMint = appTokens?.mintAddress?.toBase58Instance() ?: address.toBase58Instance(), + tokenMint = appTokens?.address?.toBase58Instance() ?: address.toBase58Instance(), chainId = chainId, decimals = appTokens?.decimals ?: decimals, coingeckoId = coingeckoId, - logoUri = appTokens?.iconUrl ?: logoUri.orEmpty(), - tokenName = appTokens?.tokenName ?: name, - tokenSymbol = appTokens?.tokenSymbol ?: symbol, + logoUri = appTokens?.logoUrl ?: logoUri.orEmpty(), + tokenName = appTokens?.name ?: name, + tokenSymbol = appTokens?.symbol ?: symbol, tags = JSONArray(tagsAsJsonList).toStringSet(), - tokenExtensions = appTokens?.tokenExtensions ?: TokenExtensions.NONE, + tokenExtensions = TokenExtensions.NONE, ) } diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 6402b4589e..66d4bf6d32 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -26,6 +26,7 @@ class ReferralWebViewBridge( private val tokenKeyProvider: TokenKeyProvider, private val onShareLinkCalled: (String) -> Unit, private val openTerms: (link: String) -> Unit, + private val navigateToSwap: () -> Unit, private val onWebViewLoaded: () -> Unit ) { companion object { @@ -131,6 +132,11 @@ class ReferralWebViewBridge( openTerms.invoke(link) } + @JavascriptInterface + fun navigateToSwap() { + navigateToSwap.invoke() + } + @JavascriptInterface fun getUserPublicKeyAsync(): JsResultWrapper = makeAsyncCall { wrapInJsResult(tokenKeyProvider.publicKey) diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt index df777cfe90..20eacff6ab 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralFragment.kt @@ -8,6 +8,8 @@ import org.koin.android.ext.android.get import org.p2p.wallet.R import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.databinding.FragmentReferralBinding +import org.p2p.wallet.jupiter.model.SwapOpenedFrom +import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment import org.p2p.wallet.url.OpenUrlFragment import org.p2p.wallet.utils.popBackStack import org.p2p.wallet.utils.replaceFragment @@ -38,7 +40,8 @@ class ReferralFragment : tokenKeyProvider = get(), onShareLinkCalled = ::showShareLinkDialog, openTerms = ::navigateToTerms, - onWebViewLoaded = ::onWebViewLoaded + onWebViewLoaded = ::onWebViewLoaded, + navigateToSwap = ::navigateToSwap ) binding.toolbar.setNavigationOnClickListener { popBackStack() @@ -72,6 +75,11 @@ class ReferralFragment : replaceFragment(target) } + private fun navigateToSwap() { + val target = JupiterSwapFragment.create(source = SwapOpenedFrom.MAIN_SCREEN) + replaceFragment(target) + } + private fun onWebViewLoaded() { binding.webViewReferral.isVisible = true binding.progressBar.isVisible = false diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt index 524b1f619a..2c21474a74 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import timber.log.Timber import java.util.Date import org.p2p.core.token.Token +import org.p2p.core.token.TokenMetadata import org.p2p.core.utils.Constants import org.p2p.solanaj.core.PublicKey import org.p2p.token.service.model.TokenServiceNetwork @@ -47,18 +48,14 @@ class UserInteractor( suspend fun getTokensForBuy( availableTokensSymbols: List = TOKEN_SYMBOLS_VALID_FOR_BUY ): List { - val userTokens = userTokensRepository.getUserTokens() - val allTokens = availableTokensSymbols.mapNotNull { tokenSymbol -> - val userToken = userTokens.find { it.tokenSymbol == tokenSymbol } - userToken ?: findTokenDataBySymbol(tokenSymbol) - } + val tokensToBuy = findTokensMetadataBySymbols(availableTokensSymbols) - if (allTokens.isEmpty()) { + if (tokensToBuy.isEmpty()) { Timber.e( - IllegalStateException("No tokens to buy! All tokens: ${userTokens.map(Token.Active::tokenSymbol)}") + IllegalStateException("No tokens to buy! All tokens: $availableTokensSymbols") ) } - return allTokens + return tokensToBuy } suspend fun getBalance(address: PublicKey): Long = rpcRepository.getBalance(address) @@ -91,15 +88,18 @@ class UserInteractor( return userTokens.any { it.publicKey == address } } - private suspend fun findTokenDataBySymbol(symbol: String): Token.Other? { - val tokenData = userLocalRepository.findTokenDataBySymbol(symbol) - val price = tokenData?.let { - tokenServiceRepository.getTokenPriceByAddress( - tokenAddress = it.mintAddress, + private suspend fun findTokensMetadataBySymbols(symbols: List): List { + val tokensData = symbols.mapNotNull { userLocalRepository.findTokenDataBySymbol(it) } + val prices = tokensData.let { + tokenServiceRepository.getTokenPricesByAddressAsMap( + tokenAddress = it.map(TokenMetadata::mintAddress), networkChain = TokenServiceNetwork.SOLANA ) } - return tokenData?.let { TokenConverter.fromNetwork(it, price) } + return tokensData.map { data -> + val price = prices[data.mintAddress] + TokenConverter.fromNetwork(data, price) + } } suspend fun findTokenDataByAddress(mintAddress: String): Token? = userLocalRepository.findTokenByMint(mintAddress) diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt index cc175737dc..32b49ad5eb 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserTokensInteractor.kt @@ -33,7 +33,7 @@ class UserTokensInteractor( suspend fun loadAndSaveUserRates(userTokens: List) { val tokenAddresses = userTokens.map { it.tokenServiceAddress } - val prices = tokenServiceRepository.getTokenPricesByAddress(tokenAddresses, TokenServiceNetwork.SOLANA) + val prices = tokenServiceRepository.getTokenPricesByAddresses(tokenAddresses, TokenServiceNetwork.SOLANA) saveUserTokensRates(prices) } diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt index 9b840cbd47..ef7175ed50 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensRemoteRepository.kt @@ -6,7 +6,6 @@ import org.p2p.core.token.Token import org.p2p.core.utils.Constants import org.p2p.solanaj.core.PublicKey import org.p2p.solanaj.model.types.Account -import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.wallet.home.model.TokenConverter import org.p2p.wallet.rpc.repository.account.RpcAccountRepository @@ -41,15 +40,12 @@ class UserTokensRemoteRepository( val tokenData = userLocalRepository.findTokenData(mintAddress) ?: return@mapNotNull null if (tokenData.decimals == NFT_DECIMALS) return@mapNotNull null - val tokenPrice = tokenServiceRepository.getTokenPriceByAddress( - tokenAddress = tokenData.mintAddress, - networkChain = TokenServiceNetwork.SOLANA - ) + TokenConverter.fromNetwork( programId = programId, account = token, tokenMetadata = tokenData, - price = tokenPrice + price = null // fetched after loading tokens ) } @@ -58,15 +54,11 @@ class UserTokensRemoteRepository( * */ val solBalance = rpcBalanceRepository.getBalance(publicKey) val tokenData = userLocalRepository.findTokenData(Constants.WRAPPED_SOL_MINT) ?: return tokens - val solPrice = tokenServiceRepository.getTokenPriceByAddress( - tokenAddress = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN, - networkChain = TokenServiceNetwork.SOLANA - ) val solToken = Token.createSOL( publicKey = publicKey.toBase58(), tokenMetadata = tokenData, amount = solBalance, - solPrice = solPrice?.usdRate + solPrice = null // fetched after loading tokens ) return listOf(solToken) + tokens diff --git a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt index aca43e81dd..a67f4220fb 100644 --- a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt +++ b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt @@ -47,7 +47,7 @@ object TokenServiceModule : InjectionModule { } single { - getRetrofit(baseUrl = "https://price.jup.ag/", tag = null, interceptor = null).create() + getRetrofit(baseUrl = "https://price.jup.ag/", interceptor = null).create() } single(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)) { diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt index e3cfe2ccd9..8d55342dfd 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepository.kt @@ -22,12 +22,18 @@ interface TokenServiceRepository { forceRemote: Boolean = false ): TokenServicePrice? - suspend fun getTokenPricesByAddress( + suspend fun getTokenPricesByAddresses( tokenAddress: List, networkChain: TokenServiceNetwork, forceRemote: Boolean = false ): List + suspend fun getTokenPricesByAddressAsMap( + tokenAddress: List, + networkChain: TokenServiceNetwork, + forceRemote: Boolean = false + ): Map + fun findTokenMetadataByAddress( networkChain: TokenServiceNetwork, tokenAddress: String diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt index 44b2bda58a..cfe3b3fa4d 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt @@ -50,10 +50,10 @@ internal class TokenServiceRepositoryImpl( networkChain: TokenServiceNetwork, forceRemote: Boolean ): TokenServicePrice? { - return getTokenPricesByAddress(listOf(tokenAddress), networkChain).firstOrNull() + return getTokenPricesByAddresses(listOf(tokenAddress), networkChain).firstOrNull() } - override suspend fun getTokenPricesByAddress( + override suspend fun getTokenPricesByAddresses( tokenAddress: List, networkChain: TokenServiceNetwork, forceRemote: Boolean @@ -71,6 +71,15 @@ internal class TokenServiceRepositoryImpl( emptyList() } + override suspend fun getTokenPricesByAddressAsMap( + tokenAddress: List, + networkChain: TokenServiceNetwork, + forceRemote: Boolean + ): Map { + return getTokenPricesByAddresses(tokenAddress, networkChain, forceRemote) + .associateBy { it.tokenAddress } + } + override fun findTokenMetadataByAddress( networkChain: TokenServiceNetwork, tokenAddress: String diff --git a/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt index dcd819933d..46187eeea5 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt @@ -1,10 +1,13 @@ package org.p2p.token.service.repository.price import timber.log.Timber +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.utils.Constants import org.p2p.token.service.api.JupiterPricesDataSource +import org.p2p.token.service.api.JupiterPricesResponse import org.p2p.token.service.model.TokenRate import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice @@ -23,10 +26,20 @@ internal class JupiterTokenPriceRepository( val isNativeRequested = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN in addresses - val mintsAsQuery = addresses.joinToString(separator = ",") { - if (it == Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) Constants.WRAPPED_SOL_MINT else it + val chunkedMints = addresses.chunked(100) + + val mintsAsQueries = chunkedMints.map { + it.joinToString(separator = ",") { + if (it == Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) Constants.WRAPPED_SOL_MINT else it + } + } + + val tokenMintsToRates = mintsAsQueries.map { + // jupiter supports only 100 tokens in one query + async { api.getPrices(it).tokenMintsToPrices } } - val tokenMintsToRates = api.getPrices(mintsAsQuery).tokenMintsToPrices + .awaitAll() + .reduce(Map::plus) .mapValues { it.value.usdPrice } .run { if (isNativeRequested) { From 906e50f45b7426ece560b049a7fd7b3128169afc Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:23:09 +0300 Subject: [PATCH 33/51] PWN-978 PWN-864 - navigate to Home when ref, fix set_referent request (#2200) --- .../auth/ui/pin/newcreate/NewCreatePinFragment.kt | 8 +++++--- .../wallet/auth/ui/pin/signin/SignInPinFragment.kt | 4 +++- .../wallet/home/deeplinks/DeeplinkScreenNavigator.kt | 1 + .../home/deeplinks/MainFragmentDeeplinkHandler.kt | 1 + app/src/main/java/org/p2p/wallet/root/RootActivity.kt | 11 ++++++++--- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/auth/ui/pin/newcreate/NewCreatePinFragment.kt b/app/src/main/java/org/p2p/wallet/auth/ui/pin/newcreate/NewCreatePinFragment.kt index 857b9d6d1b..1411c76d97 100644 --- a/app/src/main/java/org/p2p/wallet/auth/ui/pin/newcreate/NewCreatePinFragment.kt +++ b/app/src/main/java/org/p2p/wallet/auth/ui/pin/newcreate/NewCreatePinFragment.kt @@ -1,14 +1,15 @@ package org.p2p.wallet.auth.ui.pin.newcreate +import androidx.activity.addCallback import android.os.Bundle import android.view.View -import androidx.activity.addCallback import org.koin.android.ext.android.inject +import javax.crypto.Cipher +import org.p2p.core.analytics.constants.ScreenNames import org.p2p.uikit.organisms.UiKitToolbar import org.p2p.wallet.R import org.p2p.wallet.auth.ui.reserveusername.ReserveUsernameFragment import org.p2p.wallet.auth.ui.reserveusername.ReserveUsernameOpenedFrom -import org.p2p.core.analytics.constants.ScreenNames import org.p2p.wallet.common.analytics.interactor.ScreensAnalyticsInteractor import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.databinding.FragmentNewCreatePinBinding @@ -16,12 +17,12 @@ import org.p2p.wallet.home.ui.container.MainContainerFragment import org.p2p.wallet.home.ui.main.MainFragmentOnCreateAction import org.p2p.wallet.home.ui.main.MainFragmentOnCreateAction.PlayAnimation import org.p2p.wallet.intercom.IntercomService +import org.p2p.wallet.root.RootActivity import org.p2p.wallet.utils.BiometricPromptWrapper import org.p2p.wallet.utils.popAndReplaceFragment import org.p2p.wallet.utils.popBackStack import org.p2p.wallet.utils.vibrate import org.p2p.wallet.utils.viewbinding.viewBinding -import javax.crypto.Cipher private const val VIBRATE_DURATION = 10L @@ -125,6 +126,7 @@ class NewCreatePinFragment : } else { arrayListOf() } + (activity as? RootActivity)?.onSignInSuccess() popAndReplaceFragment( MainContainerFragment.create(actions), inclusive = true diff --git a/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt b/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt index 1a11b29f2d..88bf26a262 100644 --- a/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt +++ b/app/src/main/java/org/p2p/wallet/auth/ui/pin/signin/SignInPinFragment.kt @@ -11,6 +11,7 @@ import org.p2p.wallet.R import org.p2p.wallet.auth.ui.onboarding.root.OnboardingRootFragment import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.databinding.FragmentSignInPinBinding +import org.p2p.wallet.home.ui.container.MainContainerFragment import org.p2p.wallet.intercom.IntercomService import org.p2p.wallet.root.RootActivity import org.p2p.wallet.utils.BiometricPromptWrapper @@ -74,7 +75,8 @@ class SignInPinFragment : override fun onSignInSuccess() { binding.pinView.onSuccessPin() - (activity as? RootActivity)?.handleSigninSuccess() + (activity as? RootActivity)?.onSignInSuccess() + popAndReplaceFragment(MainContainerFragment.create(), inclusive = true) } override fun onLogout() { diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt index 56c5a66a75..db877287ed 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/DeeplinkScreenNavigator.kt @@ -19,4 +19,5 @@ interface DeeplinkScreenNavigator { source: SwapOpenedFrom ) fun navigateToSwap() + fun navigateToHomeScreen() } diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt index 9ab3ac26eb..4d45fa6008 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt @@ -81,6 +81,7 @@ class MainFragmentDeeplinkHandler( private fun handleReferralDeeplink(data: DeeplinkData) { referralDeeplinkHandler.handleDeeplink(data) + screenNavigator?.navigateToHomeScreen() } private suspend fun handleSendDeeplink() { diff --git a/app/src/main/java/org/p2p/wallet/root/RootActivity.kt b/app/src/main/java/org/p2p/wallet/root/RootActivity.kt index bdf7f64609..a7e232f636 100644 --- a/app/src/main/java/org/p2p/wallet/root/RootActivity.kt +++ b/app/src/main/java/org/p2p/wallet/root/RootActivity.kt @@ -56,7 +56,6 @@ import org.p2p.wallet.svl.interactor.SendViaLinkWrapper import org.p2p.wallet.svl.ui.receive.ReceiveViaLinkBottomSheet import org.p2p.wallet.transaction.model.NewShowProgress import org.p2p.wallet.transaction.ui.NewTransactionProgressBottomSheet -import org.p2p.wallet.utils.popAndReplaceFragment import org.p2p.wallet.utils.popBackStack import org.p2p.wallet.utils.popBackStackTo import org.p2p.wallet.utils.replaceFragment @@ -319,6 +318,13 @@ class RootActivity : ) } + override fun navigateToHomeScreen() { + val isNavigated = popBackStackTo(MainContainerFragment::class) + if (!isNavigated) { + replaceFragment(MainContainerFragment.create()) + } + } + override fun navigateToBuyScreen(token: Token, fiatToken: String, fiatAmount: String?) { replaceFragment(BuyFragment.create(token, fiatToken, fiatAmount)) } @@ -363,10 +369,9 @@ class RootActivity : replaceFragment(NewSearchFragment.create(SearchOpenedFromScreen.MAIN)) } - fun handleSigninSuccess() { + fun onSignInSuccess() { // do deeplinks handling init only after user signed in, otherwise it will be handled on splash // and pin/biometric screens will be skipped presenter.observeDeeplinks() - popAndReplaceFragment(MainContainerFragment.create(), inclusive = true) } } From 674152bc6161ec633212cb7b76a73be7d77151ea Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:43:08 +0300 Subject: [PATCH 34/51] PWN-983 - Add placeholder v2 (#2201) --- .../HistoryTransactionDetailsPresenter.kt | 6 +++--- .../HistorySendLinkDetailsBottomSheet.kt | 2 +- .../ui/main/delegates/token/TokenDelegate.kt | 12 +++++------- .../ui/new/adapter/NewSelectTokenViewHolder.kt | 2 +- .../home/ui/select/NewSelectTokenViewHolder.kt | 8 ++++---- .../wallet/sell/interactor/HistoryItemMapper.kt | 2 +- .../ui/search/adapter/SearchErrorViewHolder.kt | 8 ++++---- .../send/ui/search/adapter/SearchViewHolder.kt | 4 ++-- .../org/p2p/core/common/DrawableContainer.kt | 2 +- .../main/java/org/p2p/core/glide/GlideManager.kt | 4 +++- core/src/main/res/drawable/ic_placeholder_v2.xml | 16 ++++++++++++++++ .../p2p/uikit/atoms/UiKitTransactionImageView.kt | 2 +- .../org/p2p/uikit/utils/ImageViewExtensions.kt | 2 +- 13 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 core/src/main/res/drawable/ic_placeholder_v2.xml diff --git a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt index 2d24cac498..80493583b8 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/detailsbottomsheet/HistoryTransactionDetailsPresenter.kt @@ -218,7 +218,7 @@ class HistoryTransactionDetailsPresenter( if (!transaction.status.isPending()) { showFee(transaction.fees) } - showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_image) + showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_v2) showStateTitleValue( resources.getString( if (transaction.isBurn) R.string.transaction_details_burn @@ -235,7 +235,7 @@ class HistoryTransactionDetailsPresenter( view?.showSubtitle(resources.getString(R.string.details_pending)) showProgressTransactionInProgress() } - showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_image) + showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_v2) if (transaction.fees != null && !transaction.status.isPending()) { showFee(transaction.fees) } @@ -259,7 +259,7 @@ class HistoryTransactionDetailsPresenter( view?.showSubtitle(resources.getString(R.string.details_pending)) showProgressTransactionInProgress() } - showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_image) + showTransferView(transaction.token.logoUrl, R.drawable.ic_placeholder_v2) if (transaction.fees != null && !transaction.status.isPending()) { showFee(transaction.fees) } diff --git a/app/src/main/java/org/p2p/wallet/history/ui/sendvialink/HistorySendLinkDetailsBottomSheet.kt b/app/src/main/java/org/p2p/wallet/history/ui/sendvialink/HistorySendLinkDetailsBottomSheet.kt index a83750d876..100fa9a883 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/sendvialink/HistorySendLinkDetailsBottomSheet.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/sendvialink/HistorySendLinkDetailsBottomSheet.kt @@ -108,7 +108,7 @@ class HistorySendLinkDetailsBottomSheet : private fun renderContent(state: ViewState.Content) = with(binding) { val tokenIconContainer = state.iconUrl?.let(DrawableContainer::invoke) - ?: DrawableContainer.invoke(R.drawable.ic_placeholder_image) + ?: DrawableContainer.invoke(R.drawable.ic_placeholder_v2) imageViewSecondIcon.bind(ImageViewCellModel(icon = tokenIconContainer, clippingShape = shapeCircle())) textViewSubtitle.bind(TextViewCellModel.Raw(TextContainer(state.formattedDate))) diff --git a/app/src/main/java/org/p2p/wallet/home/ui/main/delegates/token/TokenDelegate.kt b/app/src/main/java/org/p2p/wallet/home/ui/main/delegates/token/TokenDelegate.kt index 92bfbc0dca..30e1291c2f 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/main/delegates/token/TokenDelegate.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/main/delegates/token/TokenDelegate.kt @@ -54,13 +54,11 @@ private fun AdapterDelegateViewBindingViewHolder with(transaction) { tokenIconUrl = token.logoUrl - iconRes = R.drawable.ic_placeholder_image + iconRes = R.drawable.ic_placeholder_v2 startTitle = resources.getString(getTitle()) startSubtitle = resources.getString( diff --git a/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchErrorViewHolder.kt b/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchErrorViewHolder.kt index de9a42b519..ebef9f2d1a 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchErrorViewHolder.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchErrorViewHolder.kt @@ -1,8 +1,9 @@ package org.p2p.wallet.send.ui.search.adapter -import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import android.view.ViewGroup import com.bumptech.glide.Glide +import timber.log.Timber import org.p2p.wallet.R import org.p2p.wallet.databinding.ItemSearchInvalidResultBinding import org.p2p.wallet.send.model.SearchResult @@ -12,7 +13,6 @@ import org.p2p.wallet.utils.toPx import org.p2p.wallet.utils.viewbinding.getString import org.p2p.wallet.utils.viewbinding.inflateViewBinding import org.p2p.wallet.utils.withTextOrGone -import timber.log.Timber class SearchErrorViewHolder( parent: ViewGroup, @@ -70,8 +70,8 @@ class SearchErrorViewHolder( Glide.with(this) .load(iconUrl) .circleCrop() - .error(R.drawable.ic_placeholder_image) - .placeholder(R.drawable.ic_placeholder_image) + .error(R.drawable.ic_placeholder_v2) + .placeholder(R.drawable.ic_placeholder_v2) .into(this) } } diff --git a/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchViewHolder.kt b/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchViewHolder.kt index f9a2a7cde4..07c886b769 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchViewHolder.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/search/adapter/SearchViewHolder.kt @@ -87,8 +87,8 @@ class SearchViewHolder( Glide.with(root) .load(imageObject) .circleCrop() - .error(R.drawable.ic_placeholder_image) - .placeholder(R.drawable.ic_placeholder_image) + .error(R.drawable.ic_placeholder_v2) + .placeholder(R.drawable.ic_placeholder_v2) .into(walletImageView) } } diff --git a/core/src/main/java/org/p2p/core/common/DrawableContainer.kt b/core/src/main/java/org/p2p/core/common/DrawableContainer.kt index 1d1612ab76..c397183f0d 100644 --- a/core/src/main/java/org/p2p/core/common/DrawableContainer.kt +++ b/core/src/main/java/org/p2p/core/common/DrawableContainer.kt @@ -39,7 +39,7 @@ sealed class DrawableContainer : Parcelable { val requestBuilder: RequestBuilder = Glide.with(imageView.context) .`as`(PictureDrawable::class.java) .listener(SvgSoftwareLayerSetter()) - val placeholder: Int = R.drawable.ic_placeholder_image + val placeholder: Int = R.drawable.ic_placeholder_v2 if (iconUrl.contains(".svg")) { requestBuilder diff --git a/core/src/main/java/org/p2p/core/glide/GlideManager.kt b/core/src/main/java/org/p2p/core/glide/GlideManager.kt index 427bb85f8a..d92599f24c 100644 --- a/core/src/main/java/org/p2p/core/glide/GlideManager.kt +++ b/core/src/main/java/org/p2p/core/glide/GlideManager.kt @@ -27,13 +27,15 @@ class GlideManager(context: Context) { url: String?, size: Int = DEFAULT_IMAGE_SIZE, circleCrop: Boolean = false, - placeholder: Int = R.drawable.ic_placeholder_image + placeholder: Int = R.drawable.ic_placeholder_v2 ) { if (url?.contains(".svg") == true) { requestBuilder .load(Uri.parse(url)) .apply(RequestOptions().override(size, size)) .scaleCrop(url, circleCrop) + .placeholder(placeholder) + .error(placeholder) .into(imageView) } else { Glide diff --git a/core/src/main/res/drawable/ic_placeholder_v2.xml b/core/src/main/res/drawable/ic_placeholder_v2.xml new file mode 100644 index 0000000000..d67fabde43 --- /dev/null +++ b/core/src/main/res/drawable/ic_placeholder_v2.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/ui-kit/src/main/kotlin/org/p2p/uikit/atoms/UiKitTransactionImageView.kt b/ui-kit/src/main/kotlin/org/p2p/uikit/atoms/UiKitTransactionImageView.kt index d8e19c6851..39021f99d8 100644 --- a/ui-kit/src/main/kotlin/org/p2p/uikit/atoms/UiKitTransactionImageView.kt +++ b/ui-kit/src/main/kotlin/org/p2p/uikit/atoms/UiKitTransactionImageView.kt @@ -49,7 +49,7 @@ class UiKitTransactionImageView @JvmOverloads constructor( tokenIconUrl: String ) = with(binding) { imageViewToken.scaleType = ImageView.ScaleType.FIT_CENTER - glideManager.load(imageViewToken, tokenIconUrl, IMAGE_SIZE, circleCrop = true) + glideManager.load(imageView = imageViewToken, url = tokenIconUrl, size = IMAGE_SIZE, circleCrop = true) } fun setStatusIcon(@DrawableRes statusIcon: Int?) { diff --git a/ui-kit/src/main/kotlin/org/p2p/uikit/utils/ImageViewExtensions.kt b/ui-kit/src/main/kotlin/org/p2p/uikit/utils/ImageViewExtensions.kt index 30c46e7dd3..77a1b2db1b 100644 --- a/ui-kit/src/main/kotlin/org/p2p/uikit/utils/ImageViewExtensions.kt +++ b/ui-kit/src/main/kotlin/org/p2p/uikit/utils/ImageViewExtensions.kt @@ -21,7 +21,7 @@ fun ImageView.loadUrl( url: String?, size: Int = DEFAULT_IMAGE_SIZE, circleCrop: Boolean = false, - placeholder: Int = R.drawable.ic_placeholder_image + placeholder: Int = R.drawable.ic_placeholder_v2 ) { glide.load( imageView = this, From f81f677bfff6c9a52e4a663677771883d64347a9 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:49:32 +0300 Subject: [PATCH 35/51] PWN-990 - Fix loading ETH tokens (#2203) --- .../alarmlogger/logger/AlarmErrorsLogger.kt | 4 ++-- .../bridge/interactor/EthereumInteractor.kt | 2 +- .../wallet/home/events/EthereumTokensLoader.kt | 18 +++++++++++------- .../tokenservice/model/EthTokenLoadState.kt | 1 + .../org/p2p/core/wrapper/eth/EthereumExt.kt | 4 ++++ .../api/alchemy/request/GetBalanceJsonRpc.kt | 9 ++++----- .../alchemy/response/TokenBalancesResponse.kt | 3 +-- .../repository/EthereumKitRepository.kt | 4 +++- .../token/EthereumTokenRemoteRepository.kt | 15 ++++++++------- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/alarmlogger/logger/AlarmErrorsLogger.kt b/app/src/main/java/org/p2p/wallet/alarmlogger/logger/AlarmErrorsLogger.kt index c3ed96b917..6f5c9ecdb0 100644 --- a/app/src/main/java/org/p2p/wallet/alarmlogger/logger/AlarmErrorsLogger.kt +++ b/app/src/main/java/org/p2p/wallet/alarmlogger/logger/AlarmErrorsLogger.kt @@ -176,8 +176,8 @@ class AlarmErrorsLogger( error: Throwable, noinline requestProvider: suspend T.() -> AlarmErrorsRequest, ) { - val isErrorShouldBeLogged = error is CancellationException || error.cause is CancellationException - if (!isLoggerEnabled || !isErrorShouldBeLogged) return + val isFilteredError = error is CancellationException || error.cause is CancellationException + if (!isLoggerEnabled || isFilteredError) return appScope.launch { try { diff --git a/app/src/main/java/org/p2p/wallet/bridge/interactor/EthereumInteractor.kt b/app/src/main/java/org/p2p/wallet/bridge/interactor/EthereumInteractor.kt index 3a771f90d1..c56c05081e 100644 --- a/app/src/main/java/org/p2p/wallet/bridge/interactor/EthereumInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/bridge/interactor/EthereumInteractor.kt @@ -41,7 +41,7 @@ class EthereumInteractor( ethereumRepository.cacheWalletTokens(tokens) } - suspend fun updateTokensRates(tokensRates: List) { + suspend fun saveTokensRates(tokensRates: List) { ethereumRepository.updateTokensRates(tokensRates) } diff --git a/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt b/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt index 83cd1fa7c3..28cd40ae3d 100644 --- a/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt +++ b/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt @@ -25,9 +25,6 @@ import org.p2p.wallet.common.feature_toggles.toggles.remote.EthAddressEnabledFea import org.p2p.wallet.infrastructure.network.provider.SeedPhraseProvider import org.p2p.wallet.tokenservice.model.EthTokenLoadState -private const val TAG = "EthereumTokensLoader" -private val MINIMAL_DUST = BigDecimal("5") - class EthereumTokensLoader( private val seedPhraseProvider: SeedPhraseProvider, private val bridgeFeatureToggle: EthAddressEnabledFeatureToggle, @@ -37,11 +34,18 @@ class EthereumTokensLoader( appScope: AppScope ) : CoroutineScope { - private val state: MutableStateFlow = MutableStateFlow(EthTokenLoadState.Idle) + private companion object { + private const val TAG = "EthereumTokensLoader" + private val MINIMAL_DUST = BigDecimal("5") + } + + private val state = MutableStateFlow(EthTokenLoadState.Idle) // Caching last ethereum tokens, to prevent hiding tokens while refreshing private var lastLoadedEthTokens = listOf() + override val coroutineContext: CoroutineContext = appScope.coroutineContext + init { ethereumInteractor.observeTokensFlow() .onEach { ethTokens -> @@ -57,14 +61,13 @@ class EthereumTokensLoader( .launchIn(appScope) } - override val coroutineContext: CoroutineContext = appScope.coroutineContext - fun observeState(): Flow = state.asStateFlow() fun getLastLoadedTokens(): List = lastLoadedEthTokens suspend fun loadIfEnabled() { if (!isEnabled()) { + Timber.i("ETH tokens are not enabled, or seed is empty") updateState(EthTokenLoadState.Loaded(emptyList())) return } @@ -129,7 +132,7 @@ class EthereumTokensLoader( private fun saveTokensRates(list: List) { launch { - ethereumInteractor.updateTokensRates(list) + ethereumInteractor.saveTokensRates(list) } } @@ -149,6 +152,7 @@ class EthereumTokensLoader( } private fun updateState(newState: EthTokenLoadState) { + Timber.i("Updating ETH tokens state: ${newState::class.simpleName}") state.value = newState } } diff --git a/app/src/main/java/org/p2p/wallet/tokenservice/model/EthTokenLoadState.kt b/app/src/main/java/org/p2p/wallet/tokenservice/model/EthTokenLoadState.kt index be41dcfcc3..936effafe9 100644 --- a/app/src/main/java/org/p2p/wallet/tokenservice/model/EthTokenLoadState.kt +++ b/app/src/main/java/org/p2p/wallet/tokenservice/model/EthTokenLoadState.kt @@ -2,6 +2,7 @@ package org.p2p.wallet.tokenservice.model import org.p2p.core.token.Token +@androidx.annotation.Keep sealed interface EthTokenLoadState { object Loading : EthTokenLoadState object Refreshing : EthTokenLoadState diff --git a/core/src/main/java/org/p2p/core/wrapper/eth/EthereumExt.kt b/core/src/main/java/org/p2p/core/wrapper/eth/EthereumExt.kt index 18e20bf1ad..ac9560cdb2 100644 --- a/core/src/main/java/org/p2p/core/wrapper/eth/EthereumExt.kt +++ b/core/src/main/java/org/p2p/core/wrapper/eth/EthereumExt.kt @@ -77,6 +77,10 @@ fun String.hexStringToBigIntegerOrNull(): BigInteger? { return this.stripHexPrefix().toBigIntegerOrNull(16) } +fun String.hexStringToBigInteger(): BigInteger { + return this.stripHexPrefix().toBigInteger(16) +} + // Converts positive long values to a byte array without leading zero byte (for sign bit) fun Long.toByteArray(): ByteArray { var array = this.toBigInteger().toByteArray() diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/request/GetBalanceJsonRpc.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/request/GetBalanceJsonRpc.kt index 91704e36c2..6a78e4bf74 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/request/GetBalanceJsonRpc.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/request/GetBalanceJsonRpc.kt @@ -1,18 +1,17 @@ package org.p2p.ethereumkit.external.api.alchemy.request -import org.p2p.core.rpc.JsonRpc -import org.p2p.core.wrapper.eth.EthAddress import java.lang.reflect.Type -import java.math.BigInteger import org.p2p.core.model.DefaultBlockParameter +import org.p2p.core.rpc.JsonRpc +import org.p2p.core.wrapper.eth.EthAddress internal class GetBalanceJsonRpc( @Transient val address: EthAddress, @Transient val defaultBlockParameter: DefaultBlockParameter -) : JsonRpc, BigInteger>( +) : JsonRpc, String>( method = "eth_getBalance", params = listOf(address, defaultBlockParameter) ) { @Transient - override val typeOfResult: Type = BigInteger::class.java + override val typeOfResult: Type = String::class.java } diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/response/TokenBalancesResponse.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/response/TokenBalancesResponse.kt index f03cc3ce9a..69949603d9 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/response/TokenBalancesResponse.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/alchemy/response/TokenBalancesResponse.kt @@ -1,7 +1,6 @@ package org.p2p.ethereumkit.external.api.alchemy.response import com.google.gson.annotations.SerializedName -import java.math.BigInteger import org.p2p.core.wrapper.eth.EthAddress internal data class TokenBalancesResponse( @@ -15,7 +14,7 @@ internal data class TokenBalanceResponse( @SerializedName("contractAddress") val contractAddress: EthAddress, @SerializedName("tokenBalance") - val tokenBalance: BigInteger + val tokenBalanceHex: String ) internal data class TokenMetadataResponse( diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt index 75ced37c79..aa46992992 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt @@ -9,6 +9,7 @@ import org.p2p.core.utils.Constants.TOKEN_SERVICE_NATIVE_ETH_TOKEN import org.p2p.core.utils.orZero import org.p2p.core.wrapper.HexString import org.p2p.core.wrapper.eth.EthAddress +import org.p2p.core.wrapper.eth.hexStringToBigInteger import org.p2p.ethereumkit.external.api.alchemy.response.TokenBalanceResponse import org.p2p.ethereumkit.external.model.ERC20Tokens import org.p2p.ethereumkit.external.model.EthTokenConverter @@ -142,7 +143,8 @@ internal class EthereumKitRepository( val erc20TokensMetadata = tokensMetadata.map { metadata -> val tokenBalance = tokensBalances .firstOrNull { it.contractAddress.hex == metadata.address } - ?.tokenBalance + ?.tokenBalanceHex + ?.hexStringToBigInteger() .orZero() converter.toEthTokenMetadata( diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokenRemoteRepository.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokenRemoteRepository.kt index 23512c0552..501deeb473 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokenRemoteRepository.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokenRemoteRepository.kt @@ -1,17 +1,18 @@ package org.p2p.ethereumkit.external.token import com.google.gson.Gson -import org.p2p.ethereumkit.external.core.EthereumNetworkEnvironment +import java.math.BigInteger +import java.net.URI +import org.p2p.core.model.DefaultBlockParameter +import org.p2p.core.rpc.RpcApi +import org.p2p.core.wrapper.eth.EthAddress +import org.p2p.core.wrapper.eth.hexStringToBigInteger import org.p2p.ethereumkit.external.api.alchemy.request.GetBalanceJsonRpc import org.p2p.ethereumkit.external.api.alchemy.request.GetTokenBalancesJsonRpc import org.p2p.ethereumkit.external.api.alchemy.request.GetTokenMetadataJsonRpc import org.p2p.ethereumkit.external.api.alchemy.response.TokenBalancesResponse import org.p2p.ethereumkit.external.api.alchemy.response.TokenMetadataResponse -import org.p2p.core.wrapper.eth.EthAddress -import java.math.BigInteger -import java.net.URI -import org.p2p.core.rpc.RpcApi -import org.p2p.core.model.DefaultBlockParameter +import org.p2p.ethereumkit.external.core.EthereumNetworkEnvironment internal class EthereumTokenRemoteRepository( private val alchemyService: RpcApi, @@ -29,7 +30,7 @@ internal class EthereumTokenRemoteRepository( uri = URI.create(networkEnvironment.baseUrl), jsonRpc = requestGson ) - return request.parseResponse(response, gson) + return request.parseResponse(response, gson).hexStringToBigInteger() } override suspend fun getTokenBalances( From 9e52f5ac266bb239712217f132d314aca7af9413 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:49:46 +0300 Subject: [PATCH 36/51] PWN-989 - Swap fixes with slippage and LP (#2202) * PWN-989 - remove percent from Swap LP fees * PWN-986 - update default slippage percent to 1 --- .../wallet/jupiter/statemanager/SwapStateManager.kt | 2 +- .../jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt | 11 +++++------ app/src/main/res/values/swap_strings.xml | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateManager.kt b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateManager.kt index 3cd4e2a729..e7d81f5b1c 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateManager.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/statemanager/SwapStateManager.kt @@ -54,7 +54,7 @@ class SwapStateManager( companion object { const val DEFAULT_ACTIVE_ROUTE_ORDINAL = 0 - val DEFAULT_SLIPPAGE = Slippage.Medium + val DEFAULT_SLIPPAGE = Slippage.One } override val coroutineContext: CoroutineContext = SupervisorJob() + dispatchers.io diff --git a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt index 221ad3ac38..caad07d6bd 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/ui/info/SwapInfoLiquidityFeeMapper.kt @@ -92,13 +92,12 @@ class SwapInfoLiquidityFeeMapper( val liquidityToken = swapTokensRepository.findTokenByMint(routePlan.feeMint) val liquidityFee = routePlan.feeAmount - val feePercent = "${routePlan.percent}%" - val firstLineText = TextViewCellModel.Raw( - text = TextContainer(R.string.swap_info_details_liquidity_cell_title, label, feePercent), + val liquidityOwner = TextViewCellModel.Raw( + text = TextContainer(R.string.swap_info_details_liquidity_cell_title, label), maxLines = 2 ) - val secondLineText = liquidityToken?.let { + val feeAmountFormatted = liquidityToken?.let { val feeInTokenLamports = liquidityFee .fromLamports(it.decimals) .formatTokenWithSymbol(it.tokenSymbol, it.decimals) @@ -108,8 +107,8 @@ class SwapInfoLiquidityFeeMapper( return MainCellModel( styleType = MainCellStyle.BASE_CELL, leftSideCellModel = LeftSideCellModel.IconWithText( - firstLineText = firstLineText, - secondLineText = secondLineText + firstLineText = liquidityOwner, + secondLineText = feeAmountFormatted ), rightSideCellModel = rightSideSkeleton(), ) diff --git a/app/src/main/res/values/swap_strings.xml b/app/src/main/res/values/swap_strings.xml index db1a7d8901..e4fb4ce0e0 100644 --- a/app/src/main/res/values/swap_strings.xml +++ b/app/src/main/res/values/swap_strings.xml @@ -100,7 +100,7 @@ Interest bearing - %s Liquidity fee %s + %s Liquidity fee Swap fee The token %s is out of the strict list From f1b5a9ded9e917feecac54d4c4970bf6ad68da11 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:00:52 +0300 Subject: [PATCH 37/51] PWN-991 - inject JS provider on reload (#2205) --- app/src/main/assets/referral_bridge_provider.js | 3 +++ .../java/org/p2p/wallet/referral/ReferralBridge.kt | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/referral_bridge_provider.js b/app/src/main/assets/referral_bridge_provider.js index fa51d46165..f89e1fd885 100644 --- a/app/src/main/assets/referral_bridge_provider.js +++ b/app/src/main/assets/referral_bridge_provider.js @@ -18,6 +18,9 @@ window.ReferralBridge = { navigateToSwap: function() { AndroidReferralBridge.navigateToSwap(); }, + reloadPage: function() { + AndroidReferralBridge.reloadPage(); + } getClipboardContent: function() { return JSON.parse(AndroidReferralBridge.getClipboardValue()); }, diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 66d4bf6d32..587f56c378 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -36,7 +36,10 @@ class ReferralWebViewBridge( private var referralWebView: WebView? = null - private var isJsProviderInjected = false + /** + * Should be set to false when reloading the page + */ + var isJsProviderInjected = false private val onPageLoadingFinishedClient = object : WebViewClient() { override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { @@ -148,6 +151,12 @@ class ReferralWebViewBridge( return wrapInJsResult(context.getClipboardText(trimmed = true).orEmpty()) } + @JavascriptInterface + fun reloadPage() { + isJsProviderInjected = false + referralWebView?.reload() + } + private fun wrapInJsResult(value: String): JsResultWrapper { return JsonObject() .apply { addProperty("value", value) } From 048930cce1aa586b24df0a5469973cfaf3f63d7a Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:19:21 +0400 Subject: [PATCH 38/51] ETH-975 - fixed use max when fee payer is the source token (#2204) * ETH-975 - fixed use max when fee payer is the source token - fixed calculating rent exemption for token2022 - fixed send slider state evaluation when "max" is used * ETH-975 - fixed comments --- .../p2p/wallet/send/SendFeeRelayerManager.kt | 139 +++---------- .../java/org/p2p/wallet/send/SendModule.kt | 2 + .../usecase/CalculateSendFeesUseCase.kt | 194 ++++++++++++++++++ .../CalculateToken2022TransferFeeUseCase.kt | 8 +- .../p2p/wallet/send/model/FeePayerState.kt | 3 +- .../wallet/send/model/NewSendButtonState.kt | 2 +- .../p2p/wallet/send/model/SendSolanaFee.kt | 61 ++++-- .../SendServiceInMemoryRepository.kt | 2 +- .../repository/SendServiceRemoteRepository.kt | 33 ++- .../send/repository/SendServiceRepository.kt | 6 + .../main/java/org/p2p/core/utils/MathUtils.kt | 18 +- .../solanaj/kits/AccountInfoTokenExtension.kt | 4 + 12 files changed, 340 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.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 bc9995e9cf..aaebf98f4a 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -6,28 +6,22 @@ import java.math.BigInteger import kotlin.properties.Delegates.observable import kotlinx.coroutines.CancellationException import org.p2p.core.token.Token -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.toLamports import org.p2p.core.utils.toUsd -import org.p2p.solanaj.core.FeeAmount import org.p2p.solanaj.kits.AccountInfoTokenExtensionConfig.Companion.interestBearingConfig import org.p2p.solanaj.kits.AccountInfoTokenExtensionConfig.Companion.transferFeeConfig import org.p2p.solanaj.kits.TokenExtensionsMap -import org.p2p.solanaj.programs.TokenProgram import org.p2p.solanaj.rpc.RpcSolanaRepository -import org.p2p.wallet.feerelayer.interactor.FeeRelayerTopUpInteractor import org.p2p.wallet.feerelayer.model.FeeCalculationState import org.p2p.wallet.feerelayer.model.FeePayerSelectionStrategy import org.p2p.wallet.feerelayer.model.FeeRelayerFee import org.p2p.wallet.feerelayer.model.TransactionFeeLimits -import org.p2p.wallet.rpc.interactor.TransactionAddressInteractor -import org.p2p.wallet.rpc.repository.amount.RpcAmountRepository import org.p2p.wallet.send.interactor.SendInteractor +import org.p2p.wallet.send.interactor.usecase.CalculateSendFeesUseCase import org.p2p.wallet.send.interactor.usecase.CalculateToken2022TransferFeeUseCase -import org.p2p.wallet.send.interactor.usecase.GetFeesInPayingTokenUseCase import org.p2p.wallet.send.interactor.usecase.GetTokenExtensionsUseCase import org.p2p.wallet.send.model.CalculationMode import org.p2p.wallet.send.model.FeeLoadingState @@ -42,9 +36,7 @@ import org.p2p.wallet.send.model.FeeRelayerStateError.InsufficientFundsToCoverFe import org.p2p.wallet.send.model.SearchResult import org.p2p.wallet.send.model.SendFeeTotal import org.p2p.wallet.send.model.SendSolanaFee -import org.p2p.wallet.send.repository.SendServiceRepository import org.p2p.wallet.user.interactor.UserInteractor -import org.p2p.wallet.utils.toPublicKey private const val TAG = "SendFeeRelayerManager" @@ -52,13 +44,9 @@ class SendFeeRelayerManager( private val sendInteractor: SendInteractor, private val userInteractor: UserInteractor, private val solanaRepository: RpcSolanaRepository, - private val feeRelayerTopUpInteractor: FeeRelayerTopUpInteractor, - private val amountRepository: RpcAmountRepository, - private val sendServiceRepository: SendServiceRepository, - private val addressInteractor: TransactionAddressInteractor, - private val getFeesInPayingTokenUseCase: GetFeesInPayingTokenUseCase, private val getTokenExtensionsUseCase: GetTokenExtensionsUseCase, private val calculateToken2022TransferFeeUseCase: CalculateToken2022TransferFeeUseCase, + private val calculateSendFeesUseCase: CalculateSendFeesUseCase, ) { var onStateUpdated: ((FeeRelayerState) -> Unit)? = null @@ -81,6 +69,14 @@ class SendFeeRelayerManager( private val alternativeTokensMap: HashMap> = HashMap() + private val TokenExtensionsMap.actualTransferFeePercent: BigDecimal + get() { + return this.transferFeeConfig + ?.getActualTransferFee(currentSolanaEpoch) + ?.transferFeePercent + .orZero() + } + suspend fun initialize( feePayerToken: Token.Active, solToken: Token.Active, @@ -195,6 +191,7 @@ class SendFeeRelayerManager( source = sourceToken, feeRelayerFee = feeState.feeInSol, token2022TransferFee = token2022TransferFee, + token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent ) currentState = UpdateFee( solanaFee = solanaFee, @@ -277,7 +274,8 @@ class SendFeeRelayerManager( append("Expected total fee in Source Token: $sourceTokenValue (T)") appendLine() - append("[Token2022] Transfer Fee: ${solanaFee.token2022TransferFee}") + // todo: it is inaccurate when "max" is used + append("[Token2022] Transfer Fee: ${solanaFee.token2022TransferFee.fromLamports(sourceToken.decimals)}") } } } @@ -290,84 +288,11 @@ class SendFeeRelayerManager( feePayerToken: Token.Active, result: SearchResult, @Suppress("UNUSED_PARAMETER") useCache: Boolean = true - ): FeeCalculationState { - return try { - val lamportsPerSignature: BigInteger = amountRepository.getLamportsPerSignature(null) - val minRentExemption: BigInteger = amountRepository.getMinBalanceForRentExemption( - TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH - ) - - Timber.i("Requesting minRentExemption for spl_token_program: $minRentExemption") - - var transactionFeeInSol = BigInteger.ZERO - - // owner's signature - transactionFeeInSol += lamportsPerSignature - - // feePayer's signature - if (!feePayerToken.isSOL) { - Timber.i("Fee payer is not sol, adding $lamportsPerSignature for fee") - transactionFeeInSol += lamportsPerSignature - } - - val shouldCreateAccount = checkAccountCreationIsRequired(sourceToken, result.address) - Timber.i("Should create account = $shouldCreateAccount") - - val accountCreationFeeInSol = if (shouldCreateAccount) minRentExemption else BigInteger.ZERO - - val expectedFeesInSol = FeeAmount( - transactionFee = transactionFeeInSol, - accountCreationFee = accountCreationFeeInSol, - ) - - // 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 (feesInSol.totalFeeLamports.isZero()) { - Timber.i("Total fees are zero!") - return FeeCalculationState.NoFees - } - - val feesInFeePayerToken = getFeesInPayingTokenUseCase.execute( - targetToken = feePayerToken, - transactionFeeInSol = feesInSol.transactionFee, - accountCreationFeeInSol = feesInSol.accountCreationFee - ) - - 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 - } catch (e: Throwable) { - Timber.i(e, "Failed to calculateFeesForFeeRelayer") - return FeeCalculationState.Error(e) - } - } + ): FeeCalculationState = calculateSendFeesUseCase.execute( + sourceToken = sourceToken, + feePayerToken = feePayerToken, + searchResult = result + ) private suspend fun setFeeDetailsState( sourceToken: Token.Active, @@ -383,6 +308,7 @@ class SendFeeRelayerManager( source = sourceToken, feeRelayerFee = feeRelayerFee, token2022TransferFee = token2022TransferFee, + token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent ) if (strategy == FeePayerSelectionStrategy.NO_ACTION) { @@ -419,6 +345,7 @@ class SendFeeRelayerManager( source: Token.Active, feeRelayerFee: FeeRelayerFee, token2022TransferFee: BigInteger, + token2022TransferFeePercent: BigDecimal, ): SendSolanaFee { val keyForAlternativeRequest = "${source.tokenSymbol}_${feeRelayerFee.totalInSol}" var alternativeTokens = alternativeTokensMap[keyForAlternativeRequest] @@ -437,7 +364,8 @@ class SendFeeRelayerManager( feeRelayerFee = feeRelayerFee, alternativeFeePayerTokens = alternativeTokens, sourceToken = source, - token2022TransferFee = token2022TransferFee + token2022TransferFee = token2022TransferFee, + token2022TransferFeePercent = token2022TransferFeePercent ) } @@ -451,6 +379,8 @@ class SendFeeRelayerManager( // Assuming token is not SOL val tokenTotal = sourceToken.total.toLamports(sourceToken.decimals) + var amountToCheck = inputAmount + /* * Checking if fee payer is SOL, otherwise fee payer is already correctly set up * - if there is enough SPL balance to cover fee, setting the default fee payer as SPL token @@ -475,11 +405,15 @@ class SendFeeRelayerManager( "Reducing amount $inputAmount for ${state.maxAllowedAmount}" ) sendInteractor.setFeePayerToken(sourceToken) + amountToCheck = state.maxAllowedAmount currentState = ReduceAmount(fee, state.maxAllowedAmount) } + is FeePayerState.KeepSame -> { + Timber.tag(TAG).i("Keeping the same fee payer token") + } } - recalculate(sourceToken, inputAmount) + recalculate(sourceToken, amountToCheck) } private suspend fun recalculate(sourceToken: Token.Active, inputAmount: BigInteger) { @@ -519,6 +453,7 @@ class SendFeeRelayerManager( source = sourceToken, feeRelayerFee = feeState.feeInSol, token2022TransferFee = token2022TransferFee, + token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent ) currentState = UpdateFee( solanaFee = solanaFee, @@ -533,6 +468,7 @@ class SendFeeRelayerManager( source = sourceToken, feeRelayerFee = feeState.fee, token2022TransferFee = token2022TransferFee, + token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent ) validateFunds(sourceToken, fee, inputAmount) currentState = UpdateFee( @@ -552,15 +488,4 @@ class SendFeeRelayerManager( val previousState = currentState currentState = Failure(previousState, error) } - - private suspend fun checkAccountCreationIsRequired( - token: Token.Active, - recipient: String - ): Boolean { - return token.mintAddress != Constants.WRAPPED_SOL_MINT && addressInteractor.findSplTokenAddressData( - mintAddress = token.mintAddress, - destinationAddress = recipient.toPublicKey(), - programId = token.programId?.toPublicKey() ?: TokenProgram.PROGRAM_ID - ).shouldCreateAccount - } } diff --git a/app/src/main/java/org/p2p/wallet/send/SendModule.kt b/app/src/main/java/org/p2p/wallet/send/SendModule.kt index 12079c4d01..bee81c971a 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendModule.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendModule.kt @@ -19,6 +19,7 @@ import org.p2p.wallet.infrastructure.sendvialink.UserSendLinksDatabaseRepository import org.p2p.wallet.infrastructure.sendvialink.UserSendLinksLocalRepository import org.p2p.wallet.send.api.SendServiceApi import org.p2p.wallet.send.interactor.SendMaximumAmountCalculator +import org.p2p.wallet.send.interactor.usecase.CalculateSendFeesUseCase import org.p2p.wallet.send.interactor.usecase.CalculateToken2022TransferFeeUseCase import org.p2p.wallet.send.interactor.usecase.GetFeesInPayingTokenUseCase import org.p2p.wallet.send.interactor.usecase.GetTokenExtensionsUseCase @@ -113,6 +114,7 @@ object SendModule : InjectionModule { factoryOf(::GetFeesInPayingTokenUseCase) factoryOf(::CalculateToken2022TransferFeeUseCase) factoryOf(::GetTokenExtensionsUseCase) + factoryOf(::CalculateSendFeesUseCase) } private fun Module.initDataLayer() { diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt new file mode 100644 index 0000000000..0bd1a616b9 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt @@ -0,0 +1,194 @@ +package org.p2p.wallet.send.interactor.usecase + +import timber.log.Timber +import java.math.BigInteger +import kotlin.coroutines.cancellation.CancellationException +import org.p2p.core.crypto.toBase58Instance +import org.p2p.core.token.Token +import org.p2p.core.utils.Constants +import org.p2p.core.utils.isNotZero +import org.p2p.core.utils.isZero +import org.p2p.solanaj.core.FeeAmount +import org.p2p.solanaj.programs.TokenProgram +import org.p2p.wallet.feerelayer.model.FeeCalculationState +import org.p2p.wallet.feerelayer.model.FeeRelayerFee +import org.p2p.wallet.rpc.interactor.TransactionAddressInteractor +import org.p2p.wallet.rpc.repository.amount.RpcAmountRepository +import org.p2p.wallet.send.model.SearchResult +import org.p2p.wallet.send.repository.SendServiceRepository +import org.p2p.wallet.utils.toPublicKey + +class CalculateSendFeesUseCase( + private val amountRepository: RpcAmountRepository, + private val sendServiceRepository: SendServiceRepository, + private val getFeesInPayingTokenUseCase: GetFeesInPayingTokenUseCase, + private val addressInteractor: TransactionAddressInteractor, +) { + + suspend fun execute( + sourceToken: Token.Active, + feePayerToken: Token.Active, + searchResult: SearchResult, + @Suppress("UNUSED_PARAMETER") useCache: Boolean = true + ): FeeCalculationState { + return try { + val minRentExemption = getMinRentExemption(sourceToken) + val alternativeMinRentExemption = getSendServiceMinRentExemptionForSameTokens(sourceToken, feePayerToken) + + val feesInSol = getTransactionFeesInSol( + minRentExemption = minRentExemption, + sourceToken = sourceToken, + feePayerToken = feePayerToken, + searchResult = searchResult + ) + + if (feesInSol.totalFeeLamports.isZero()) { + Timber.i("Total fees are zero!") + return FeeCalculationState.NoFees + } + + val feesInFeePayerToken = getTransactionFeesInToken( + targetToken = feePayerToken, + feesInSol = feesInSol, + alternativeAccountCreationFee = alternativeMinRentExemption + ) + + val feesInSourceToken = getTransactionFeesInToken( + targetToken = sourceToken, + feesInSol = feesInSol, + alternativeAccountCreationFee = alternativeMinRentExemption + ) + + // 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 = feesInSol + ) + ) + } catch (e: CancellationException) { + Timber.i("Fee calculation cancelled") + return FeeCalculationState.Cancelled + } catch (e: Throwable) { + Timber.i(e, "Failed to calculateFeesForFeeRelayer") + return FeeCalculationState.Error(e) + } + } + + /** + * this is a special case, because if we have fee payer token the same as source token + * we have to be very precise with amounts we use + * since the math is calculated manually when "max" button is pressed + * transaction will fail if account creation fee in token has incorrect value (rates in usd I guess) + * given this, we just make request to send-service to see what "token_account_rent" it returns + * + * todo: it looks like we don't have correct prices for tokens at a moment + * + * @return zero if tokens are different, or min rent exemption in source token + */ + private suspend fun getSendServiceMinRentExemptionForSameTokens( + sourceToken: Token.Active, + feePayerToken: Token.Active + ): BigInteger { + if (sourceToken.isSOL || sourceToken.mintAddress != feePayerToken.mintAddress) { + return BigInteger.ZERO + } + + return sendServiceRepository + .getTokenRentExemption( + userWallet = sourceToken.publicKey.toBase58Instance(), + token = sourceToken, + // return in TOKEN + returnInToken = true + ) + } + + /** + * token2022 may has higher account creation fee as it takes more data for account + */ + private suspend fun getMinRentExemption(sourceToken: Token.Active) = if (sourceToken.isToken2022) { + sendServiceRepository + .getTokenRentExemption( + userWallet = sourceToken.publicKey.toBase58Instance(), + token = sourceToken, + // return in SOL + returnInToken = false + ) + .takeIf(BigInteger::isNotZero) + ?: amountRepository.getMinBalanceForRentExemption( + TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH + ) + } else { + amountRepository.getMinBalanceForRentExemption( + TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH + ) + } + + private suspend fun getTransactionFeesInSol( + minRentExemption: BigInteger, + sourceToken: Token.Active, + feePayerToken: Token.Active, + searchResult: SearchResult, + ): FeeAmount { + val lamportsPerSignature: BigInteger = amountRepository.getLamportsPerSignature(null) + var transactionFeeInSol = BigInteger.ZERO + + // owner's signature + transactionFeeInSol += lamportsPerSignature + + // feePayer's signature + if (!feePayerToken.isSOL) { + Timber.i("Fee payer is not sol, adding $lamportsPerSignature for fee") + transactionFeeInSol += lamportsPerSignature + } + + val shouldCreateAccount = checkAccountCreationIsRequired(sourceToken, searchResult.address) + Timber.i("Should create account = $shouldCreateAccount") + + val accountCreationFeeInSol = if (shouldCreateAccount) minRentExemption else BigInteger.ZERO + + return FeeAmount( + transactionFee = transactionFeeInSol, + accountCreationFee = accountCreationFeeInSol, + ).copy( + // todo: while we use send service, we don't pay network fee + transactionFee = BigInteger.ZERO + ) + } + + private suspend fun getTransactionFeesInToken( + targetToken: Token.Active, + feesInSol: FeeAmount, + alternativeAccountCreationFee: BigInteger + ): FeeAmount? { + return getFeesInPayingTokenUseCase.execute( + targetToken = targetToken, + transactionFeeInSol = feesInSol.transactionFee, + accountCreationFeeInSol = feesInSol.accountCreationFee + )?.let { + // if local calculation in token represents lower fee, than use send-service fee + it.copy(accountCreationFee = maxOf(it.accountCreationFee, alternativeAccountCreationFee)) + } + } + + private suspend fun checkAccountCreationIsRequired( + token: Token.Active, + recipient: String + ): Boolean { + return token.mintAddress != Constants.WRAPPED_SOL_MINT && addressInteractor.findSplTokenAddressData( + mintAddress = token.mintAddress, + destinationAddress = recipient.toPublicKey(), + programId = token.programId?.toPublicKey() ?: TokenProgram.PROGRAM_ID + ).shouldCreateAccount + } +} diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt index 07bb7e81be..f8af23158a 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateToken2022TransferFeeUseCase.kt @@ -2,6 +2,7 @@ package org.p2p.wallet.send.interactor.usecase import java.math.BigDecimal import java.math.BigInteger +import java.math.RoundingMode import org.p2p.core.token.Token import org.p2p.core.utils.divideSafe import org.p2p.core.utils.isZero @@ -36,7 +37,12 @@ class CalculateToken2022TransferFeeUseCase( } val transferFeeBasisPoints = transferFeeBasisPoints.toBigInteger() val numerator = preFeeAmount * transferFeeBasisPoints - val rawFee = numerator.divideSafe(BigInteger("10000")) + + // calculate with ceiling rounding mode so that we don't lose any lamports + val rawFee = numerator.toBigDecimal() + .divideSafe(BigDecimal("10000"), 6, RoundingMode.CEILING) + .setScale(0, RoundingMode.CEILING) + .toBigInteger() return minOf(rawFee, maximumFee) } diff --git a/app/src/main/java/org/p2p/wallet/send/model/FeePayerState.kt b/app/src/main/java/org/p2p/wallet/send/model/FeePayerState.kt index e4f299cc01..b7d5d9b5cd 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/FeePayerState.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/FeePayerState.kt @@ -1,10 +1,11 @@ package org.p2p.wallet.send.model -import org.p2p.core.token.Token import java.math.BigInteger +import org.p2p.core.token.Token sealed interface FeePayerState { object SwitchToSol : FeePayerState + object KeepSame : FeePayerState class SwitchToSpl(val tokenToSwitch: Token.Active) : FeePayerState class ReduceInputAmount(val maxAllowedAmount: BigInteger) : FeePayerState } 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 de3eebe3ca..02bf98ee35 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 @@ -55,7 +55,7 @@ class NewSendButtonState( val sendFee = (feeRelayerState as? FeeRelayerState.UpdateFee)?.solanaFee val isEnoughToCoverExpenses = sendFee == null || sendFee.isEnoughToCoverExpenses( - sourceTokenTotal = maxAmountToSendLamports, + sourceTokenTotal = tokenToSend.totalInLamports, inputAmount = inputAmount, minRentExemption = minRentExemption ) diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt index 877cb51635..cfa04f6914 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt @@ -12,13 +12,16 @@ 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 @@ -31,6 +34,7 @@ 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, @@ -164,25 +168,26 @@ data class SendSolanaFee constructor( inputAmount: BigInteger ): FeePayerState { val feePayerTokenCanCoverExpenses = feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken - val isSourceSol = sourceTokenSymbol == SOL_SYMBOL - val isAllowedToCorrectAmount = strategy == CORRECT_AMOUNT + val feePayerIsSourceToken = feePayerSymbol == sourceTokenSymbol + val isNotSourceSol = sourceTokenSymbol != SOL_SYMBOL + val isAllowedToCorrectAmount = strategy == CORRECT_AMOUNT && isNotSourceSol val totalNeeded = feeRelayerFee.totalInSourceToken + inputAmount - val isEnoughSolBalance = isEnoughSolBalance() - val shouldTryReduceAmount = isAllowedToCorrectAmount && !isSourceSol && !isEnoughSolBalance + 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 = - !isSourceSol && + isNotSourceSol && sourceTokenTotal.isMoreThan(totalNeeded) && isValidToSwitchOnSource Timber.i( buildString { - appendLine("isSourceSol = $isSourceSol") + appendLine("isNotSourceSol = $isNotSourceSol") appendLine("isAllowedToCorrectAmount = $isAllowedToCorrectAmount") appendLine("totalNeeded = $totalNeeded") - appendLine("isEnoughSolBalance = $isEnoughSolBalance") + appendLine("isInsufficientSolBalance = $isInsufficientSolBalance") appendLine("shouldTryReduceAmount = $shouldTryReduceAmount") appendLine("hasAlternativeFeePayerTokens = $hasAlternativeFeePayerTokens") appendLine("alternativeFeePayerTokens = ${alternativeFeePayerTokens.map(Token.Active::tokenSymbol)}") @@ -190,27 +195,47 @@ data class SendSolanaFee constructor( appendLine("shouldSwitchToSpl = $shouldSwitchToSpl") appendLine("feePayerToken = ${feePayerToken.tokenSymbol}") appendLine("feePayerTokenCanCoverExpenses = $feePayerTokenCanCoverExpenses") + appendLine("feePayerIsSourceToken = $feePayerIsSourceToken") } ) return when { - feePayerTokenCanCoverExpenses -> { - if (feePayerToken.isSOL) { - SwitchToSol - } else { - SwitchToSpl(feePayerToken) - } + // 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 -> { + shouldSwitchToSpl && !shouldTryReduceAmount -> { + Timber.i("FeePayer: switch to SPL (${sourceToken.tokenSymbol})") SwitchToSpl(sourceToken) } - hasAlternativeFeePayerTokens -> { + 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 diff = totalNeeded - sourceTokenTotal - val desiredAmount = if (diff.isLessThan(inputAmount)) inputAmount - diff else null - if (desiredAmount != null) ReduceInputAmount(desiredAmount) else SwitchToSol + 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 diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceInMemoryRepository.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceInMemoryRepository.kt index 0bc94330f5..a6b698f4bb 100644 --- a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceInMemoryRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceInMemoryRepository.kt @@ -10,7 +10,7 @@ class SendServiceInMemoryRepository { cache[mintAddress] = maxAmount } - fun getMaxAmountToSned(mintAddress: Base58String): BigInteger? { + fun getMaxAmountToSend(mintAddress: Base58String): BigInteger? { return cache[mintAddress] } } diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt index ca40909953..ced775cf89 100644 --- a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt @@ -7,6 +7,7 @@ import org.p2p.core.crypto.toBase58Instance import org.p2p.core.network.data.EmptyDataException import org.p2p.core.token.Token import org.p2p.core.utils.Constants +import org.p2p.core.wrapper.eth.helpers.RandomHelper.randomBytes import org.p2p.solanaj.model.types.RpcMapRequest import org.p2p.solanaj.model.types.RpcRequest import org.p2p.wallet.send.api.SendServiceApi @@ -37,7 +38,7 @@ class SendServiceRemoteRepository( recipient: Base58String, token: Token.Active ): BigInteger { - val cachedFeeInfo = inMemoryRepository.getMaxAmountToSned(token.mintAddress.toBase58Instance()) + val cachedFeeInfo = inMemoryRepository.getMaxAmountToSend(token.mintAddress.toBase58Instance()) if (cachedFeeInfo != null) { return cachedFeeInfo } @@ -58,6 +59,36 @@ class SendServiceRemoteRepository( return generatedTransaction.recipientGetsAmount.amount } + /** + * todo: this and getMaxAmountToSend are very similar, maybe we can merge them + * but this method should not use cache, values may change very often due to volatility + */ + override suspend fun getTokenRentExemption( + userWallet: Base58String, + token: Token.Active, + returnInToken: Boolean + ): BigInteger { + val generatedTransaction = generateTransaction( + userWallet = userWallet, + amountLamports = token.totalInLamports, + recipient = randomBytes(32).toBase58Instance(), + tokenMint = if (token.isSOL) null else token.mintAddress.toBase58Instance(), + // exactIn because we need to get totalAmount less than amount with send + transferMode = SendTransferMode.ExactIn, + + feePayerMode = SendFeePayerMode.UserSol, + // get rent exemption in SOL + rentPayerMode = if (returnInToken) SendRentPayerMode.Custom else SendRentPayerMode.UserSol, + customRentPayerTokenMint = if (returnInToken) token.mintAddress.toBase58Instance() else null + ) + + if (generatedTransaction.tokenAccountRent == null) { + return BigInteger.ZERO + } + + return generatedTransaction.tokenAccountRent.amount.amount + } + override suspend fun generateTransaction( userWallet: Base58String, amountLamports: BigInteger, diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt index 694dae80f4..5117c6d75e 100644 --- a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt @@ -28,4 +28,10 @@ interface SendServiceRepository { recipient: Base58String, token: Token.Active ): BigInteger + + suspend fun getTokenRentExemption( + userWallet: Base58String, + token: Token.Active, + returnInToken: Boolean = false + ): BigInteger } diff --git a/core/src/main/java/org/p2p/core/utils/MathUtils.kt b/core/src/main/java/org/p2p/core/utils/MathUtils.kt index 514ac774f2..900d75369f 100644 --- a/core/src/main/java/org/p2p/core/utils/MathUtils.kt +++ b/core/src/main/java/org/p2p/core/utils/MathUtils.kt @@ -14,11 +14,25 @@ fun BigInteger.divideSafe(value: BigInteger): BigInteger = else this / value // scales by default to the decimals -fun BigDecimal.divideSafe(value: BigDecimal, decimals: Int = DEFAULT_DECIMALS_VALUE): BigDecimal = +fun BigDecimal.divideSafe( + value: BigDecimal, + decimals: Int = DEFAULT_DECIMALS_VALUE, + roundingMode: RoundingMode = RoundingMode.HALF_EVEN +): BigDecimal = if (this.isZero() || value.isZero()) { BigDecimal.ZERO } else { - this.divide(value, decimals, RoundingMode.HALF_EVEN).stripTrailingZeros() + this.divide(value, decimals, roundingMode).stripTrailingZeros() } fun clamp(value: Int, min: Int, max: Int): Int = min(max(min, value), max) + +/** + * @param ratio - ratio in range (0.0 .. 1.0) + */ +fun BigDecimal.subtractRatio(ratio: BigDecimal): BigDecimal { + require(ratio.toDouble() in 0.0..1.0) { + "Expected ratio between 0.0 and 1.0, but given is $ratio" + } + return this.subtract(this.multiply(ratio)) +} diff --git a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt index 49805ffa6b..33ef5b8b9d 100644 --- a/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt +++ b/solana/src/main/java/org/p2p/solanaj/kits/AccountInfoTokenExtension.kt @@ -41,6 +41,10 @@ sealed interface AccountInfoTokenExtensionConfig { @SerializedName("transferFeeBasisPoints") val transferFeeBasisPoints: Long, ) { + /** + * Value in percents (e.g. 6.65) in range [0, 100] + * todo: I think we should use ratio instead of percent, it's useless except for UI + */ val transferFeePercent: BigDecimal get() = transferFeeBasisPoints.toBigDecimal().setScale(4) .divideSafe("10000".toBigDecimal().setScale(4)) From 21d468294a8eb712ace5c453224c6d8510b6d2f4 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:46:54 +0300 Subject: [PATCH 39/51] PWN-991 - add coma for JS bridge (#2206) --- app/src/main/assets/referral_bridge_provider.js | 2 +- .../main/java/org/p2p/wallet/referral/ReferralBridge.kt | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/assets/referral_bridge_provider.js b/app/src/main/assets/referral_bridge_provider.js index f89e1fd885..004ad14454 100644 --- a/app/src/main/assets/referral_bridge_provider.js +++ b/app/src/main/assets/referral_bridge_provider.js @@ -20,7 +20,7 @@ window.ReferralBridge = { }, reloadPage: function() { AndroidReferralBridge.reloadPage(); - } + }, getClipboardContent: function() { return JSON.parse(AndroidReferralBridge.getClipboardValue()); }, diff --git a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt index 587f56c378..877ca3af2d 100644 --- a/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt +++ b/app/src/main/java/org/p2p/wallet/referral/ReferralBridge.kt @@ -39,7 +39,7 @@ class ReferralWebViewBridge( /** * Should be set to false when reloading the page */ - var isJsProviderInjected = false + private var isJsProviderInjected = false private val onPageLoadingFinishedClient = object : WebViewClient() { override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { @@ -153,8 +153,10 @@ class ReferralWebViewBridge( @JavascriptInterface fun reloadPage() { - isJsProviderInjected = false - referralWebView?.reload() + referralWebView?.post { + isJsProviderInjected = false + referralWebView?.reload() + } } private fun wrapInJsResult(value: String): JsResultWrapper { From 472345bd85da4c1bfe5830efc8d9ed93e7524890 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:07:59 +0300 Subject: [PATCH 40/51] PWN-999 - Retry on socket referral exception (#2207) --- .../repository/ReferralRemoteRepository.kt | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt index 0b919937a8..f767bda3b8 100644 --- a/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt @@ -3,9 +3,11 @@ package org.p2p.wallet.referral.repository import com.google.gson.Gson import org.near.borshj.BorshBuffer import timber.log.Timber +import java.net.SocketTimeoutException import java.net.URI import java.util.Optional import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withContext import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.rpc.JsonRpc @@ -17,6 +19,7 @@ import org.p2p.solanaj.utils.SolanaMessageSigner import org.p2p.wallet.auth.gateway.repository.mapper.write import org.p2p.wallet.common.feature_toggles.toggles.remote.ReferralProgramEnabledFeatureToggle import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider +import org.p2p.wallet.utils.retryOnException class ReferralRemoteRepository( private val api: RpcApi, @@ -29,9 +32,23 @@ class ReferralRemoteRepository( private val url = URI("https://referral.key.app/") + /** + * Sometimes the service responds with the timeout error `operation timed out` + * + */ + private class ReferralServiceTimedOut : Exception() + override suspend fun registerReferent() { if (referralEnabledFt.isFeatureEnabled) { - registerReferentInternal() + retryOnException( + exceptionTypes = setOf( + ReferralServiceTimedOut::class, + SocketTimeoutException::class + ), + maxAttempts = 10, + delayMillis = 5.seconds.inWholeMilliseconds, + block = ::registerReferentInternal + ) } } @@ -57,11 +74,19 @@ class ReferralRemoteRepository( request.parseResponse(response, gson) } catch (rpcError: JsonRpc.ResponseError) { val message = rpcError.message ?: return - if (message.contains("duplicate key value violates unique constraint")) { - Timber.i("User already registered") - } else { - Timber.e(rpcError, "Failed to register referent") + when { + message.contains("duplicate key value violates unique constraint") -> { + Timber.i("User already registered") + } + message.contains("timed out") -> { + throw ReferralServiceTimedOut() + } + else -> { + Timber.e(rpcError, "Failed to register referent") + } } + } catch (error: SocketTimeoutException) { + throw error } catch (error: Throwable) { Timber.e(error, "Failed to register referent") } From 224dc34801b1baba7177b27cbdbfba77e2d6380c Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:55:40 +0300 Subject: [PATCH 41/51] PWN-990 PWN-1021 - Bridge fixes (#2208) * PWN-990 - add Coingecko as price source for ETH * PWN-1021 - fix receive sol design * PWN-1021 - fix receive sol design and tokens list sorting * NO-TICKET - add UUID as id to json rpc * NO-TICKET - add UUID as id to json rpc 2 --- .../auth/ui/username/UsernameFragment.kt | 8 +- .../wallet/common/ui/widget/ProgressView.kt | 6 +- .../home/events/EthereumTokensLoader.kt | 17 +++-- .../wallet/home/events/HomeEventsModule.kt | 17 +---- .../p2p/wallet/home/ui/crypto/CryptoModule.kt | 4 +- .../network/coingecko/CoinGeckoApi.kt | 32 -------- .../response/CoinGeckoEthereumPrice.kt | 9 --- ...ontract.kt => ReceiveTokenListContract.kt} | 4 +- ...ragment.kt => ReceiveTokenListFragment.kt} | 27 ++++--- .../wallet/receive/list/TokenListAdapter.kt | 33 +++++---- .../wallet/receive/list/TokenListPresenter.kt | 8 +- .../receive/solana/ReceiveSolanaFragment.kt | 46 ++++++------ .../tokenselect/ReceiveTokensPresenter.kt | 2 +- .../wallet/receive/widget/ReceiveCardView.kt | 4 +- .../repository/ReferralRemoteRepository.kt | 22 +++--- .../wallet/user/interactor/UserInteractor.kt | 6 +- .../user/repository/UserInMemoryRepository.kt | 25 ++++--- .../res/layout/widget_receive_card_view.xml | 16 ++-- .../main/java/org/p2p/core/rpc/Interfaces.kt | 2 +- .../src/main/java/org/p2p/core/rpc/JsonRpc.kt | 5 +- .../external/api/EthereumNetworkModule.kt | 7 +- .../api/coingecko/CoinGeckoService.kt | 16 ---- .../response/CoinGeckoPriceResponse.kt | 9 --- .../repository/EthereumKitRepository.kt | 2 +- .../token/EthereumTokensInMemoryRepository.kt | 4 +- .../internal/core/TransactionSignerLegacy.kt | 4 +- .../p2p/token/service/TokenServiceModule.kt | 23 ++++-- .../api/coingecko/CoinGeckoDataSource.kt | 53 ++++++++++++++ .../CoinGeckoEthereumPriceResponse.kt | 9 +++ .../response/CoinGeckoSolPriceResponse.kt | 4 +- .../{ => jupiter}/JupiterPricesDataSource.kt | 2 +- .../JupiterPricesRootResponse.kt | 2 +- .../TokenServiceDataSource.kt | 2 +- .../TokenServiceRemoteDataSource.kt | 2 +- .../request/TokenAmountsRequest.kt | 4 +- .../request/TokenServiceItemRequest.kt | 4 +- .../request/TokenServiceMetadataRequest.kt | 4 +- .../request/TokenServicePriceRequest.kt | 4 +- .../request/TokenServiceQueryRequest.kt | 2 +- .../response/TokenAmountsResponse.kt | 2 +- .../response/TokenItemMetadataResponse.kt | 2 +- .../response/TokenItemPriceResponse.kt | 2 +- .../response/TokenListResponse.kt | 2 +- .../response/TokenRateResponse.kt | 2 +- .../response/TokenServiceMetadataResponse.kt | 2 +- .../response/TokenServiceNetworkResponse.kt | 2 +- .../response/TokenServicePriceResponse.kt | 2 +- .../TokenServiceAmountsRemoteConverter.kt | 8 +- .../repository/TokenServiceRepositoryImpl.kt | 10 ++- .../mapper/TokenServiceMapper.kt | 20 ++--- .../metadata/TokenMetadataRemoteRepository.kt | 8 +- .../price/JupiterTokenPriceRepository.kt | 73 +++++++++---------- .../price/TokenPriceRemoteRepository.kt | 6 +- 53 files changed, 308 insertions(+), 283 deletions(-) delete mode 100644 app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/CoinGeckoApi.kt delete mode 100644 app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoEthereumPrice.kt rename app/src/main/java/org/p2p/wallet/receive/list/{TokenListContract.kt => ReceiveTokenListContract.kt} (93%) rename app/src/main/java/org/p2p/wallet/receive/list/{TokenListFragment.kt => ReceiveTokenListFragment.kt} (85%) delete mode 100644 ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/CoinGeckoService.kt delete mode 100644 ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/response/CoinGeckoPriceResponse.kt create mode 100644 token-service/src/main/java/org/p2p/token/service/api/coingecko/CoinGeckoDataSource.kt create mode 100644 token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoEthereumPriceResponse.kt rename app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoPriceResponse.kt => token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoSolPriceResponse.kt (76%) rename token-service/src/main/java/org/p2p/token/service/api/{ => jupiter}/JupiterPricesDataSource.kt (84%) rename token-service/src/main/java/org/p2p/token/service/api/{ => jupiter}/JupiterPricesRootResponse.kt (91%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/TokenServiceDataSource.kt (81%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/TokenServiceRemoteDataSource.kt (96%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/request/TokenAmountsRequest.kt (84%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/request/TokenServiceItemRequest.kt (63%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/request/TokenServiceMetadataRequest.kt (78%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/request/TokenServicePriceRequest.kt (78%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/request/TokenServiceQueryRequest.kt (75%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenAmountsResponse.kt (87%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenItemMetadataResponse.kt (88%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenItemPriceResponse.kt (79%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenListResponse.kt (96%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenRateResponse.kt (75%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenServiceMetadataResponse.kt (84%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenServiceNetworkResponse.kt (77%) rename token-service/src/main/java/org/p2p/token/service/api/{ => tokenservice}/response/TokenServicePriceResponse.kt (83%) rename token-service/src/main/java/org/p2p/token/service/{api => repository}/mapper/TokenServiceMapper.kt (82%) diff --git a/app/src/main/java/org/p2p/wallet/auth/ui/username/UsernameFragment.kt b/app/src/main/java/org/p2p/wallet/auth/ui/username/UsernameFragment.kt index b20a640984..2950c7f14c 100644 --- a/app/src/main/java/org/p2p/wallet/auth/ui/username/UsernameFragment.kt +++ b/app/src/main/java/org/p2p/wallet/auth/ui/username/UsernameFragment.kt @@ -4,6 +4,8 @@ import android.graphics.Bitmap import android.os.Bundle import android.view.View import org.koin.android.ext.android.inject +import java.io.File +import org.p2p.core.utils.Constants import org.p2p.uikit.utils.SpanUtils.highlightPublicKey import org.p2p.uikit.utils.toast import org.p2p.wallet.R @@ -11,15 +13,13 @@ import org.p2p.wallet.auth.model.Username import org.p2p.wallet.common.analytics.interactor.ScreensAnalyticsInteractor import org.p2p.wallet.databinding.FragmentUsernameBinding import org.p2p.wallet.receive.analytics.ReceiveAnalytics -import org.p2p.wallet.receive.list.TokenListFragment +import org.p2p.wallet.receive.list.ReceiveTokenListFragment import org.p2p.wallet.receive.widget.BaseQrCodeFragment import org.p2p.wallet.receive.widget.ReceiveCardView -import org.p2p.core.utils.Constants import org.p2p.wallet.utils.popBackStack import org.p2p.wallet.utils.replaceFragment import org.p2p.wallet.utils.shareScreenShot import org.p2p.wallet.utils.viewbinding.viewBinding -import java.io.File class UsernameFragment : BaseQrCodeFragment(R.layout.fragment_username), @@ -42,7 +42,7 @@ class UsernameFragment : toolbar.setNavigationOnClickListener { popBackStack() } receiveCardView.initReceiveCardView() progressButton.setOnClickListener { - replaceFragment(TokenListFragment.create()) + replaceFragment(ReceiveTokenListFragment.create()) } } presenter.loadData() diff --git a/app/src/main/java/org/p2p/wallet/common/ui/widget/ProgressView.kt b/app/src/main/java/org/p2p/wallet/common/ui/widget/ProgressView.kt index 263071503c..1fd97dfd76 100644 --- a/app/src/main/java/org/p2p/wallet/common/ui/widget/ProgressView.kt +++ b/app/src/main/java/org/p2p/wallet/common/ui/widget/ProgressView.kt @@ -2,10 +2,10 @@ package org.p2p.wallet.common.ui.widget import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater import android.widget.FrameLayout import org.p2p.wallet.R import org.p2p.wallet.databinding.WidgetProgressViewBinding +import org.p2p.wallet.utils.viewbinding.inflateViewBinding class ProgressView @JvmOverloads constructor( context: Context, @@ -13,9 +13,7 @@ class ProgressView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { - private val binding = WidgetProgressViewBinding.inflate( - LayoutInflater.from(context), this - ) + private val binding = inflateViewBinding() init { binding.root.setBackgroundColor(context.getColor(R.color.colorProgressBackground)) diff --git a/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt b/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt index 28cd40ae3d..fd76051c75 100644 --- a/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt +++ b/app/src/main/java/org/p2p/wallet/home/events/EthereumTokensLoader.kt @@ -20,6 +20,7 @@ import org.p2p.token.service.api.events.manager.TokenServiceEventType import org.p2p.token.service.api.events.manager.TokenServiceUpdate import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice +import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.wallet.bridge.interactor.EthereumInteractor import org.p2p.wallet.common.feature_toggles.toggles.remote.EthAddressEnabledFeatureToggle import org.p2p.wallet.infrastructure.network.provider.SeedPhraseProvider @@ -30,6 +31,7 @@ class EthereumTokensLoader( private val bridgeFeatureToggle: EthAddressEnabledFeatureToggle, private val ethereumInteractor: EthereumInteractor, private val tokenServiceEventPublisher: TokenServiceEventPublisher, + private val tokenServiceRepository: TokenServiceRepository, private val tokenServiceEventManager: TokenServiceEventManager, appScope: AppScope ) : CoroutineScope { @@ -83,10 +85,11 @@ class EthereumTokensLoader( val ethTokens = ethereumInteractor.loadWalletTokens(claimTokens) ethereumInteractor.cacheWalletTokens(ethTokens) - tokenServiceEventPublisher.loadTokensPrice( - networkChain = TokenServiceNetwork.ETHEREUM, - addresses = ethTokens.map { it.tokenServiceAddress } + val prices = tokenServiceRepository.getTokenPricesByAddresses( + ethTokens.map { it.tokenServiceAddress }, + networkChain = TokenServiceNetwork.ETHEREUM ) + ethereumInteractor.saveTokensRates(prices) } catch (e: Throwable) { Timber.tag(TAG).e(e, "Error while loading ethereum tokens") updateState(EthTokenLoadState.Error(e)) @@ -108,10 +111,12 @@ class EthereumTokensLoader( val ethTokens = ethereumInteractor.loadWalletTokens(claimTokens) ethereumInteractor.cacheWalletTokens(ethTokens) - tokenServiceEventPublisher.loadTokensPrice( - networkChain = TokenServiceNetwork.ETHEREUM, - addresses = ethTokens.map { it.tokenServiceAddress } + + val prices = tokenServiceRepository.getTokenPricesByAddresses( + ethTokens.map { it.tokenServiceAddress }, + networkChain = TokenServiceNetwork.ETHEREUM ) + ethereumInteractor.saveTokensRates(prices) } catch (e: Throwable) { Timber.tag(TAG).e(e, "Error while refreshing ethereum tokens") updateState(EthTokenLoadState.Error(e)) diff --git a/app/src/main/java/org/p2p/wallet/home/events/HomeEventsModule.kt b/app/src/main/java/org/p2p/wallet/home/events/HomeEventsModule.kt index d7f2aa4b52..8f9cf785b4 100644 --- a/app/src/main/java/org/p2p/wallet/home/events/HomeEventsModule.kt +++ b/app/src/main/java/org/p2p/wallet/home/events/HomeEventsModule.kt @@ -1,6 +1,7 @@ package org.p2p.wallet.home.events import org.koin.core.module.Module +import org.koin.core.module.dsl.new import org.koin.core.module.dsl.singleOf import org.koin.dsl.bind import org.koin.dsl.module @@ -48,20 +49,8 @@ object HomeEventsModule : InjectionModule { } single { - val solTokensLoader = SolanaTokensLoader( - userTokensInteractor = get(), - tokenKeyProvider = get(), - tokenServiceEventManager = get(), - appScope = get() - ) - val ethTokensLoader = EthereumTokensLoader( - seedPhraseProvider = get(), - bridgeFeatureToggle = get(), - ethereumInteractor = get(), - tokenServiceEventManager = get(), - tokenServiceEventPublisher = get(), - appScope = get() - ) + val solTokensLoader = new(::SolanaTokensLoader) + val ethTokensLoader = new(::EthereumTokensLoader) TokenServiceCoordinator( solanaTokensLoader = solTokensLoader, diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/CryptoModule.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/CryptoModule.kt index 5d23e0748e..809a40f51e 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/CryptoModule.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/CryptoModule.kt @@ -8,7 +8,7 @@ import org.p2p.wallet.common.di.factoryOf import org.p2p.wallet.home.interactor.MyCryptoInteractor import org.p2p.wallet.home.ui.crypto.handlers.BridgeClaimBundleClickHandler import org.p2p.wallet.home.ui.crypto.mapper.MyCryptoMapper -import org.p2p.wallet.receive.list.TokenListContract +import org.p2p.wallet.receive.list.ReceiveTokenListContract import org.p2p.wallet.receive.list.TokenListPresenter object CryptoModule : InjectionModule { @@ -18,6 +18,6 @@ object CryptoModule : InjectionModule { factoryOf(::BridgeClaimBundleClickHandler) factoryOf(::MyCryptoMapper) factoryOf(::MyCryptoPresenter) bind MyCryptoContract.Presenter::class - factoryOf(::TokenListPresenter) bind TokenListContract.Presenter::class + factoryOf(::TokenListPresenter) bind ReceiveTokenListContract.Presenter::class } } diff --git a/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/CoinGeckoApi.kt b/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/CoinGeckoApi.kt deleted file mode 100644 index 2140dc1ebb..0000000000 --- a/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/CoinGeckoApi.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.p2p.wallet.infrastructure.network.coingecko - -import okhttp3.ResponseBody -import retrofit2.http.GET -import retrofit2.http.Query -import org.p2p.wallet.infrastructure.network.coingecko.response.CoinGeckoEthereumPrice -import org.p2p.wallet.infrastructure.network.coingecko.response.CoinGeckoPriceResponse - -interface CoinGeckoApi { - /** - * { - * bitcoin: { - * usd: 1111 - * }, - * ... - * } - */ - @GET("coins/markets") - suspend fun getTokenPrices( - @Query("ids") tokenIds: String, - @Query("vs_currency") targetCurrency: String - ): List - - @GET("simple/token_price/ethereum") - suspend fun getEthereumTokenPrices( - @Query("contract_addresses") tokenAddresses: String, - @Query("vs_currencies") targetCurrency: String - ): Map - - @GET("coins/list") - suspend fun getAllTokens(): ResponseBody -} diff --git a/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoEthereumPrice.kt b/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoEthereumPrice.kt deleted file mode 100644 index c7fb7e8ddc..0000000000 --- a/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoEthereumPrice.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.p2p.wallet.infrastructure.network.coingecko.response - -import com.google.gson.annotations.SerializedName -import java.math.BigDecimal - -class CoinGeckoEthereumPrice( - @SerializedName("usd") - val currentPrice: BigDecimal -) diff --git a/app/src/main/java/org/p2p/wallet/receive/list/TokenListContract.kt b/app/src/main/java/org/p2p/wallet/receive/list/ReceiveTokenListContract.kt similarity index 93% rename from app/src/main/java/org/p2p/wallet/receive/list/TokenListContract.kt rename to app/src/main/java/org/p2p/wallet/receive/list/ReceiveTokenListContract.kt index a80de0e793..037f4f4ed4 100644 --- a/app/src/main/java/org/p2p/wallet/receive/list/TokenListContract.kt +++ b/app/src/main/java/org/p2p/wallet/receive/list/ReceiveTokenListContract.kt @@ -1,10 +1,10 @@ package org.p2p.wallet.receive.list +import org.p2p.core.token.TokenMetadata import org.p2p.wallet.common.mvp.MvpPresenter import org.p2p.wallet.common.mvp.MvpView -import org.p2p.core.token.TokenMetadata -interface TokenListContract { +interface ReceiveTokenListContract { interface View : MvpView { fun showLoading(isLoading: Boolean) diff --git a/app/src/main/java/org/p2p/wallet/receive/list/TokenListFragment.kt b/app/src/main/java/org/p2p/wallet/receive/list/ReceiveTokenListFragment.kt similarity index 85% rename from app/src/main/java/org/p2p/wallet/receive/list/TokenListFragment.kt rename to app/src/main/java/org/p2p/wallet/receive/list/ReceiveTokenListFragment.kt index 61d1b2edd2..0695f46605 100644 --- a/app/src/main/java/org/p2p/wallet/receive/list/TokenListFragment.kt +++ b/app/src/main/java/org/p2p/wallet/receive/list/ReceiveTokenListFragment.kt @@ -1,12 +1,14 @@ package org.p2p.wallet.receive.list -import android.os.Bundle -import android.view.View import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.recyclerview.widget.LinearLayoutManager +import android.os.Bundle +import android.view.View +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject +import org.p2p.core.token.TokenMetadata import org.p2p.uikit.utils.SpanUtils import org.p2p.uikit.utils.attachAdapter import org.p2p.uikit.utils.focusAndShowKeyboard @@ -15,23 +17,26 @@ import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.common.ui.recycler.EndlessScrollListener import org.p2p.wallet.databinding.FragmentReceiveListBinding import org.p2p.wallet.home.analytics.BrowseAnalytics -import org.p2p.core.token.TokenMetadata import org.p2p.wallet.utils.popBackStack import org.p2p.wallet.utils.viewbinding.viewBinding -class TokenListFragment : - BaseMvpFragment(R.layout.fragment_receive_list), - TokenListContract.View { +class ReceiveTokenListFragment : + BaseMvpFragment(R.layout.fragment_receive_list), + ReceiveTokenListContract.View { companion object { - fun create() = TokenListFragment() + fun create() = ReceiveTokenListFragment() } - override val presenter: TokenListContract.Presenter by inject() + override val presenter: ReceiveTokenListContract.Presenter by inject() private val binding: FragmentReceiveListBinding by viewBinding() private val browseAnalytics: BrowseAnalytics by inject() - private val adapter = TokenListAdapter() - private val linearLayoutManager by lazy { LinearLayoutManager(requireContext()) } + + private val adapter = TokenListAdapter(glideManager = get()) + private val linearLayoutManager by lazy { + LinearLayoutManager(requireContext()) + } + private val scrollListener by lazy { EndlessScrollListener(linearLayoutManager, ::loadNextPage, ::onScrollYChanged) } @@ -41,7 +46,7 @@ class TokenListFragment : with(binding) { with(binding.recyclerView) { layoutManager = linearLayoutManager - attachAdapter(this@TokenListFragment.adapter) + attachAdapter(this@ReceiveTokenListFragment.adapter) clearOnScrollListeners() addOnScrollListener(scrollListener) diff --git a/app/src/main/java/org/p2p/wallet/receive/list/TokenListAdapter.kt b/app/src/main/java/org/p2p/wallet/receive/list/TokenListAdapter.kt index bf2052b499..a09d857e12 100644 --- a/app/src/main/java/org/p2p/wallet/receive/list/TokenListAdapter.kt +++ b/app/src/main/java/org/p2p/wallet/receive/list/TokenListAdapter.kt @@ -1,15 +1,18 @@ package org.p2p.wallet.receive.list -import android.view.LayoutInflater -import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import org.p2p.wallet.databinding.ItemTokenListBinding +import android.view.ViewGroup +import org.p2p.core.glide.GlideManager import org.p2p.core.token.TokenMetadata +import org.p2p.core.utils.Constants import org.p2p.core.utils.Constants.SOL_NAME +import org.p2p.wallet.databinding.ItemTokenListBinding +import org.p2p.wallet.utils.viewbinding.inflateViewBinding -class TokenListAdapter : RecyclerView.Adapter() { +class TokenListAdapter( + private val glideManager: GlideManager +) : RecyclerView.Adapter() { private val data = mutableListOf() @@ -17,12 +20,13 @@ class TokenListAdapter : RecyclerView.Adapter() { val old = ArrayList(data) data.clear() data.addAll(new) - DiffUtil.calculateDiff(getDiffCallback(old, data)).dispatchUpdatesTo(this) + DiffUtil.calculateDiff(getDiffCallback(old, data)) + .dispatchUpdatesTo(this) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder( - ItemTokenListBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(parent, glideManager) + } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(data[position]) @@ -31,7 +35,9 @@ class TokenListAdapter : RecyclerView.Adapter() { override fun getItemCount(): Int = data.size inner class ViewHolder( - binding: ItemTokenListBinding + parent: ViewGroup, + private val glideManager: GlideManager, + binding: ItemTokenListBinding = parent.inflateViewBinding(attachToRoot = false) ) : RecyclerView.ViewHolder(binding.root) { private val imageView = binding.imageView @@ -39,14 +45,13 @@ class TokenListAdapter : RecyclerView.Adapter() { private val symbolTextView = binding.symbolTextView fun bind(value: TokenMetadata) { - // TODO temporary solution - if (value.symbol == "SOL") { + if (value.mintAddress == Constants.WRAPPED_SOL_MINT) { textView.text = SOL_NAME } else { textView.text = value.name } symbolTextView.text = value.symbol - Glide.with(imageView).load(value.iconUrl).into(imageView) + glideManager.load(imageView, value.iconUrl) } } @@ -64,7 +69,7 @@ class TokenListAdapter : RecyclerView.Adapter() { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val old = oldList[oldItemPosition] val new = newList[newItemPosition] - return old.name == new.name && old.symbol == new.symbol + return old.mintAddress == new.mintAddress } override fun getOldListSize(): Int = oldList.size diff --git a/app/src/main/java/org/p2p/wallet/receive/list/TokenListPresenter.kt b/app/src/main/java/org/p2p/wallet/receive/list/TokenListPresenter.kt index 947e353994..de24e4fc77 100644 --- a/app/src/main/java/org/p2p/wallet/receive/list/TokenListPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/receive/list/TokenListPresenter.kt @@ -13,12 +13,12 @@ class TokenListPresenter( private val interactor: UserInteractor, private val browseAnalytics: BrowseAnalytics, private val analyticsInteractor: ScreensAnalyticsInteractor -) : BasePresenter(), TokenListContract.Presenter { +) : BasePresenter(), ReceiveTokenListContract.Presenter { private var searchText = emptyString() private var scrollToUp = false - override fun attach(view: TokenListContract.View) { + override fun attach(view: ReceiveTokenListContract.View) { super.attach(view) browseAnalytics.logTokenListViewed( analyticsInteractor.getPreviousScreenName(), @@ -29,9 +29,9 @@ class TokenListPresenter( override fun load(isRefresh: Boolean, scrollToUp: Boolean) { launch { - view?.showLoading(true) + view?.showLoading(isLoading = true) this@TokenListPresenter.scrollToUp = scrollToUp - interactor.fetchTokens(searchText, PAGE_SIZE, isRefresh) + interactor.getReceiveTokens(searchText, PAGE_SIZE, isRefresh) view?.showLoading(false) } } diff --git a/app/src/main/java/org/p2p/wallet/receive/solana/ReceiveSolanaFragment.kt b/app/src/main/java/org/p2p/wallet/receive/solana/ReceiveSolanaFragment.kt index fcb5524e42..636c7b26d9 100644 --- a/app/src/main/java/org/p2p/wallet/receive/solana/ReceiveSolanaFragment.kt +++ b/app/src/main/java/org/p2p/wallet/receive/solana/ReceiveSolanaFragment.kt @@ -7,17 +7,17 @@ import android.view.View import org.koin.android.ext.android.inject import org.koin.core.parameter.parametersOf import java.io.File +import org.p2p.core.analytics.constants.ScreenNames import org.p2p.core.token.Token import org.p2p.core.utils.Constants import org.p2p.uikit.utils.SpanUtils.highlightPublicKey import org.p2p.uikit.utils.toast import org.p2p.wallet.R import org.p2p.wallet.auth.model.Username -import org.p2p.core.analytics.constants.ScreenNames import org.p2p.wallet.common.analytics.interactor.ScreensAnalyticsInteractor import org.p2p.wallet.databinding.FragmentReceiveSolanaBinding import org.p2p.wallet.receive.analytics.ReceiveAnalytics -import org.p2p.wallet.receive.list.TokenListFragment +import org.p2p.wallet.receive.list.ReceiveTokenListFragment import org.p2p.wallet.receive.widget.BaseQrCodeFragment import org.p2p.wallet.receive.widget.ReceiveCardView import org.p2p.wallet.utils.args @@ -33,42 +33,42 @@ class ReceiveSolanaFragment : ReceiveSolanaContract.View { companion object { - private const val EXTRA_TOKEN = "EXTRA_TOKEN" - fun create(token: Token?) = ReceiveSolanaFragment().withArgs( - EXTRA_TOKEN to token - ) - } + private const val ARG_TOKEN = "EXTRA_TOKEN" - private val token: Token? by args(EXTRA_TOKEN) - override val presenter: ReceiveSolanaContract.Presenter by inject { - parametersOf(token) + fun create(token: Token?): ReceiveSolanaFragment = + ReceiveSolanaFragment() + .withArgs(ARG_TOKEN to token) } - override val receiveCardView: ReceiveCardView by lazy { binding.receiveCardView } + + private val token: Token? by args(ARG_TOKEN) + + override val presenter: ReceiveSolanaContract.Presenter by inject { parametersOf(token) } + + override val receiveCardView: ReceiveCardView + get() = binding.receiveCardView + private val binding: FragmentReceiveSolanaBinding by viewBinding() private val analyticsInteractor: ScreensAnalyticsInteractor by inject() private val receiveAnalytics: ReceiveAnalytics by inject() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - with(binding) { - toolbar.setNavigationOnClickListener { popBackStack() } - receiveCardView.setOnFaqClickListener { + binding.toolbar.setNavigationOnClickListener { popBackStack() } + + with(receiveCardView) { + setOnFaqClickListener { analyticsInteractor.logScreenOpenEvent(ScreenNames.Receive.LIST) - replaceFragment(TokenListFragment.create()) + replaceFragment(ReceiveTokenListFragment.create()) } - receiveCardView.setOnRequestPermissions { - checkStatusAndRequestPermissionsIfNotGranted() - } - receiveCardView.setOnShareQrClickListener { qrValue, qrImage, shareText -> + setOnRequestPermissions(::checkStatusAndRequestPermissionsIfNotGranted) + setOnShareQrClickListener { qrValue, qrImage, shareText -> presenter.saveQr(qrValue, qrImage, shareText) receiveAnalytics.logUserCardShared(analyticsInteractor.getPreviousScreenName()) } - receiveCardView.setOnCopyQrClickListener { + setOnCopyQrClickListener { receiveAnalytics.logReceiveAddressCopied(analyticsInteractor.getPreviousScreenName()) } - receiveCardView.setOnSaveQrClickListener { qrValue, qrImage -> - presenter.saveQr(qrValue, qrImage) - } + setOnSaveQrClickListener(presenter::saveQr) receiveCardView.setTokenSymbol(token?.tokenSymbol ?: Constants.SOL_SYMBOL) receiveCardView.setSelectNetworkVisibility(isVisible = true) receiveCardView.setChevronInvisible(isInvisible = true) diff --git a/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensPresenter.kt b/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensPresenter.kt index b9140e3799..c6815c4199 100644 --- a/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensPresenter.kt @@ -78,7 +78,7 @@ class ReceiveTokensPresenter( tokensFlow.value = emptyList() } this@ReceiveTokensPresenter.scrollToUp = scrollToUp - interactor.fetchTokens(searchText, PAGE_SIZE, isRefresh) + interactor.getReceiveTokens(searchText, PAGE_SIZE, isRefresh) } } diff --git a/app/src/main/java/org/p2p/wallet/receive/widget/ReceiveCardView.kt b/app/src/main/java/org/p2p/wallet/receive/widget/ReceiveCardView.kt index 0ba4521a32..e6477c2bfb 100644 --- a/app/src/main/java/org/p2p/wallet/receive/widget/ReceiveCardView.kt +++ b/app/src/main/java/org/p2p/wallet/receive/widget/ReceiveCardView.kt @@ -8,8 +8,8 @@ import android.content.Context import android.graphics.Bitmap import android.text.Spannable import android.util.AttributeSet -import android.view.LayoutInflater import org.p2p.wallet.databinding.WidgetReceiveCardViewBinding +import org.p2p.wallet.utils.viewbinding.inflateViewBinding class ReceiveCardView @JvmOverloads constructor( context: Context, @@ -17,7 +17,7 @@ class ReceiveCardView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { - val binding = WidgetReceiveCardViewBinding.inflate(LayoutInflater.from(context), this) + val binding = inflateViewBinding() fun setOnNetworkClickListener(block: () -> Unit) { binding.networkView.setOnClickListener { block() } diff --git a/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt index f767bda3b8..4bf70597ff 100644 --- a/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/referral/repository/ReferralRemoteRepository.kt @@ -40,15 +40,19 @@ class ReferralRemoteRepository( override suspend fun registerReferent() { if (referralEnabledFt.isFeatureEnabled) { - retryOnException( - exceptionTypes = setOf( - ReferralServiceTimedOut::class, - SocketTimeoutException::class - ), - maxAttempts = 10, - delayMillis = 5.seconds.inWholeMilliseconds, - block = ::registerReferentInternal - ) + try { + retryOnException( + exceptionTypes = setOf( + ReferralServiceTimedOut::class, + SocketTimeoutException::class + ), + maxAttempts = 10, + delayMillis = 5.seconds.inWholeMilliseconds, + block = ::registerReferentInternal + ) + } catch (error: Exception) { + Timber.e(error, "Failed to register referent after 10 attempts") + } } } diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt index 2c21474a74..6cc6e45bb0 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt @@ -60,7 +60,11 @@ class UserInteractor( suspend fun getBalance(address: PublicKey): Long = rpcRepository.getBalance(address) - fun fetchTokens(searchText: String = emptyString(), count: Int, refresh: Boolean) { + fun getReceiveTokens( + searchText: String = emptyString(), + count: Int, + refresh: Boolean + ) { userLocalRepository.fetchTokens(searchText, count, refresh) } diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt index 7f207e0526..1f1ec95658 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserInMemoryRepository.kt @@ -20,7 +20,13 @@ class UserInMemoryRepository( private val tokenConverter: TokenConverter, private val tokenServiceRepository: TokenServiceRepository ) : UserLocalRepository { - private val popularItems = arrayOf("SOL", "USDC", "BTC", "USDT", "ETH") + private val popularTokensMints = arrayOf( + Constants.WRAPPED_SOL_MINT, + Constants.USDC_MINT, + Constants.WRAPPED_BTC_MINT, + Constants.USDT_MINT, + Constants.WRAPPED_ETH_MINT, + ) private val allTokensFlow = MutableStateFlow>(emptyList()) private val tokensSearchResultFlow = MutableStateFlow(TokenListData()) @@ -47,13 +53,15 @@ class UserInMemoryRepository( return } + val popularTokens = getPopularTokensMetadata() + when { searchText.isEmpty() -> { val defaultTokens = searchTextByTokens.getOrDefault(DEFAULT_TOKEN_KEY, emptyList()) - val newSize = (defaultTokens.size - getPopularDecimals().size) + count + val newSize = (defaultTokens.size - popularTokens.size) + count val needToLoadMore = newSize > defaultTokens.size if (needToLoadMore) { - searchTextByTokens[DEFAULT_TOKEN_KEY] = getPopularDecimals() + allInMemoryTokens.take(newSize) + searchTextByTokens[DEFAULT_TOKEN_KEY] = popularTokens + allInMemoryTokens.take(newSize) } setSearchResult(DEFAULT_TOKEN_KEY) } @@ -122,14 +130,11 @@ class UserInMemoryRepository( return resultToken } - private fun getPopularDecimals(): List { - val popularTokens = mutableListOf() - val tokens = allTokensFlow.value - for (symbol in popularItems) { - val token = tokens.firstOrNull { it.symbol == symbol } - if (token != null) popularTokens.add(token) + private fun getPopularTokensMetadata(): List { + val allTokens = allTokensFlow.value + return popularTokensMints.mapNotNull { popularMint -> + allTokens.find { it.mintAddress == popularMint } } - return popularTokens } @Deprecated("This repository should not return [Token] objects but only [TokenMetadata]") diff --git a/app/src/main/res/layout/widget_receive_card_view.xml b/app/src/main/res/layout/widget_receive_card_view.xml index 562a3472de..70b1fa811e 100644 --- a/app/src/main/res/layout/widget_receive_card_view.xml +++ b/app/src/main/res/layout/widget_receive_card_view.xml @@ -34,19 +34,23 @@ app:layout_constraintTop_toBottomOf="@id/toolbar"> + app:layout_constraintTop_toTopOf="@+id/networkTextView" /> @@ -59,9 +63,9 @@ android:text="@string/send_solana_network" android:textAppearance="@style/WalletTheme.TextAppearance.Bold16" android:textColor="@color/bg_night" - app:layout_constraintBottom_toBottomOf="@id/arrowImageView" + app:layout_constraintBottom_toBottomOf="@+id/arrowImageView" app:layout_constraintEnd_toStartOf="@id/arrowImageView" - app:layout_constraintTop_toTopOf="@id/arrowImageView" /> + app:layout_constraintTop_toTopOf="@+id/arrowImageView" /> diff --git a/core/src/main/java/org/p2p/core/rpc/Interfaces.kt b/core/src/main/java/org/p2p/core/rpc/Interfaces.kt index 71975a08f8..4ac11001f6 100644 --- a/core/src/main/java/org/p2p/core/rpc/Interfaces.kt +++ b/core/src/main/java/org/p2p/core/rpc/Interfaces.kt @@ -6,7 +6,7 @@ import io.reactivex.Single data class RpcResponse( @SerializedName("id") - val id: Int, + val id: String, @SerializedName("result") val result: JsonElement?, @SerializedName("error") diff --git a/core/src/main/java/org/p2p/core/rpc/JsonRpc.kt b/core/src/main/java/org/p2p/core/rpc/JsonRpc.kt index b7e7bd087c..44a3bb23a7 100644 --- a/core/src/main/java/org/p2p/core/rpc/JsonRpc.kt +++ b/core/src/main/java/org/p2p/core/rpc/JsonRpc.kt @@ -5,6 +5,7 @@ import com.google.gson.JsonElement import com.google.gson.annotations.SerializedName import timber.log.Timber import java.lang.reflect.Type +import java.util.UUID private const val TAG = "JsonRpcParser" @@ -12,13 +13,13 @@ abstract class JsonRpc( @SerializedName("method") val method: String, @SerializedName("params") - val params: Body + val params: Body, ) { @SerializedName("jsonrpc") val version: String = "2.0" @SerializedName("id") - var id: Int = 1 + val id: String = UUID.randomUUID().toString() protected abstract val typeOfResult: Type diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/EthereumNetworkModule.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/EthereumNetworkModule.kt index 76c2db63cf..d387f33feb 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/EthereumNetworkModule.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/EthereumNetworkModule.kt @@ -9,7 +9,6 @@ import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.scalars.ScalarsConverterFactory import org.p2p.core.rpc.RPC_JSON_QUALIFIER -import org.p2p.ethereumkit.external.api.coingecko.CoinGeckoService import org.p2p.ethereumkit.external.api.interceptor.EthereumApiLoggingInterceptor import org.p2p.ethereumkit.external.core.EthereumNetworkEnvironment @@ -22,11 +21,11 @@ internal object EthereumNetworkModule { single(named(QUALIFIER_ETH_HTTP_CLIENT)) { getOkHttpClient() } single(named(QUALIFIER_ETH_RETROFIT)) { getRetrofit( + baseUrl = EthereumNetworkEnvironment.ALCHEMY.baseUrl, httpClient = get(named(QUALIFIER_ETH_HTTP_CLIENT)), gson = get(named(RPC_JSON_QUALIFIER)) ) } - single { get(named(QUALIFIER_ETH_RETROFIT)).create(CoinGeckoService::class.java) } } private fun getOkHttpClient(): OkHttpClient { @@ -35,9 +34,9 @@ internal object EthereumNetworkModule { return httpClient.build() } - private fun getRetrofit(httpClient: OkHttpClient, gson: Gson): Retrofit { + private fun getRetrofit(httpClient: OkHttpClient, gson: Gson, baseUrl: String): Retrofit { return Retrofit.Builder() - .baseUrl(EthereumNetworkEnvironment.ALCHEMY.baseUrl) + .baseUrl(baseUrl) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .client(httpClient) diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/CoinGeckoService.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/CoinGeckoService.kt deleted file mode 100644 index 3cd9f4af7a..0000000000 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/CoinGeckoService.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.p2p.ethereumkit.external.api.coingecko - -import org.p2p.ethereumkit.external.api.coingecko.response.CoinGeckoPriceResponse -import retrofit2.http.GET -import retrofit2.http.Query -import retrofit2.http.Url - -internal interface CoinGeckoService { - - @GET - suspend fun getEthereumTokenPrices( - @Url url: String = "https://api.coingecko.com/api/v3/simple/token_price/ethereum", - @Query("contract_addresses") tokenAddresses: String, - @Query("vs_currencies") targetCurrency: String, - ): Map -} diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/response/CoinGeckoPriceResponse.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/response/CoinGeckoPriceResponse.kt deleted file mode 100644 index 0ea1ff680c..0000000000 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/api/coingecko/response/CoinGeckoPriceResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.p2p.ethereumkit.external.api.coingecko.response - -import com.google.gson.annotations.SerializedName -import java.math.BigDecimal - -internal data class CoinGeckoPriceResponse( - @SerializedName("usd") - val priceInUsd: BigDecimal -) diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt index aa46992992..501da14c62 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/repository/EthereumKitRepository.kt @@ -43,7 +43,7 @@ internal class EthereumKitRepository( privateKey = Signer.privateKey(words = seedPhrase, chain = Chain.Ethereum) ) - val ethAddress = tokenKeyProvider?.publicKey ?: return + tokenKeyProvider?.publicKey ?: return } override fun isInitialized(): Boolean { diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt index fa3c7a02d3..24f25c866e 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/external/token/EthereumTokensInMemoryRepository.kt @@ -11,9 +11,9 @@ class EthereumTokensInMemoryRepository : EthereumTokensLocalRepository { private val cachedTokens = MutableStateFlow>(emptyList()) - override suspend fun cacheTokens(newTokens: List) { + override suspend fun cacheTokens(tokens: List) { val oldTokens = cachedTokens.value - val validatedTokens = newTokens.map { newToken -> + val validatedTokens = tokens.map { newToken -> val oldToken = oldTokens.firstOrNull { newToken.tokenServiceAddress == it.tokenServiceAddress } val newTokenRate = newToken.rate ?: oldToken?.rate diff --git a/ethereumkit/src/main/java/org/p2p/ethereumkit/internal/core/TransactionSignerLegacy.kt b/ethereumkit/src/main/java/org/p2p/ethereumkit/internal/core/TransactionSignerLegacy.kt index e4534832c4..f9e1cbb6e3 100644 --- a/ethereumkit/src/main/java/org/p2p/ethereumkit/internal/core/TransactionSignerLegacy.kt +++ b/ethereumkit/src/main/java/org/p2p/ethereumkit/internal/core/TransactionSignerLegacy.kt @@ -12,8 +12,8 @@ class TransactionSignerLegacy( private val chainId: Int ) { - fun signatureLegacy(encodedTransaction: HexString): Signature { - val decodedTransaction = TransactionDecoder.decode(encodedTransaction.rawValue) + fun signatureLegacy(encodedTransactionHex: HexString): Signature { + val decodedTransaction = TransactionDecoder.decode(encodedTransactionHex.rawValue) val encodedTransaction = TransactionEncoder.encode(decodedTransaction, chainId.toLong()) val rawTransactionHash = CryptoUtils.sha3(encodedTransaction) val sign = CryptoUtils.ellipticSign(rawTransactionHash, privateKey) diff --git a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt index a67f4220fb..2110972f9b 100644 --- a/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt +++ b/token-service/src/main/java/org/p2p/token/service/TokenServiceModule.kt @@ -11,18 +11,20 @@ import org.p2p.core.common.di.InjectionModule import org.p2p.core.network.NetworkCoreModule.getRetrofit import org.p2p.core.network.environment.NetworkServicesUrlProvider import org.p2p.core.rpc.RPC_JSON_QUALIFIER -import org.p2p.token.service.api.JupiterPricesDataSource -import org.p2p.token.service.api.TokenServiceDataSource -import org.p2p.token.service.api.TokenServiceRemoteDataSource +import org.p2p.token.service.api.coingecko.CoinGeckoDataSource +import org.p2p.token.service.api.coingecko.CoinGeckoTokenPriceRepository import org.p2p.token.service.api.events.manager.TokenServiceEventManager import org.p2p.token.service.api.events.manager.TokenServiceEventPublisher -import org.p2p.token.service.api.mapper.TokenServiceMapper +import org.p2p.token.service.api.jupiter.JupiterPricesDataSource +import org.p2p.token.service.api.tokenservice.TokenServiceDataSource +import org.p2p.token.service.api.tokenservice.TokenServiceRemoteDataSource import org.p2p.token.service.converter.TokenServiceAmountsConverter import org.p2p.token.service.converter.TokenServiceAmountsRemoteConverter import org.p2p.token.service.database.TokenServiceDatabaseModule import org.p2p.token.service.database.mapper.TokenServiceDatabaseMapper import org.p2p.token.service.repository.TokenServiceRepository import org.p2p.token.service.repository.TokenServiceRepositoryImpl +import org.p2p.token.service.repository.mapper.TokenServiceMapper import org.p2p.token.service.repository.metadata.TokenMetadataInMemoryRepository import org.p2p.token.service.repository.metadata.TokenMetadataLocalRepository import org.p2p.token.service.repository.metadata.TokenMetadataRemoteRepository @@ -50,6 +52,14 @@ object TokenServiceModule : InjectionModule { getRetrofit(baseUrl = "https://price.jup.ag/", interceptor = null).create() } + single { + getRetrofit( + baseUrl = "https://api.coingecko.com/api/v3/", + tag = "CoinGeckoApi", + interceptor = null, + ).create() + } + single(named(TOKEN_SERVICE_RETROFIT_QUALIFIER)) { val url = get() getRetrofit( @@ -63,6 +73,8 @@ object TokenServiceModule : InjectionModule { singleOf(::TokenPriceDatabaseRepository) bind TokenPriceLocalRepository::class factoryOf(::TokenPriceRemoteRepository) bind TokenPriceRepository::class factoryOf(::JupiterTokenPriceRepository) + // caches data across app + singleOf(::CoinGeckoTokenPriceRepository) factoryOf(::TokenServiceAmountsRemoteConverter) bind TokenServiceAmountsConverter::class factoryOf(::TokenServiceMapper) @@ -73,7 +85,8 @@ object TokenServiceModule : InjectionModule { priceLocalRepository = get(), metadataLocalRepository = get(), metadataRemoteRepository = get(), - jupiterPriceRepository = get() + jupiterPriceRepository = get(), + coinGeckoTokenPriceRepository = get() ) } singleOf(::TokenServiceEventPublisher) diff --git a/token-service/src/main/java/org/p2p/token/service/api/coingecko/CoinGeckoDataSource.kt b/token-service/src/main/java/org/p2p/token/service/api/coingecko/CoinGeckoDataSource.kt new file mode 100644 index 0000000000..a391f6371a --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/api/coingecko/CoinGeckoDataSource.kt @@ -0,0 +1,53 @@ +package org.p2p.token.service.api.coingecko + +import retrofit2.http.GET +import retrofit2.http.Query +import kotlinx.coroutines.withContext +import org.p2p.core.dispatchers.CoroutineDispatchers +import org.p2p.token.service.api.coingecko.response.CoinGeckoEthereumPriceResponse +import org.p2p.token.service.api.coingecko.response.CoinGeckoSolPriceResponse +import org.p2p.token.service.model.TokenRate +import org.p2p.token.service.model.TokenServiceNetwork +import org.p2p.token.service.model.TokenServicePrice + +internal class CoinGeckoTokenPriceRepository( + private val api: CoinGeckoDataSource, + private val dispatchers: CoroutineDispatchers +) { + private var cachedAddressesToPrices = mutableMapOf() + + suspend fun loadEthereumTokenPrices( + tokenAddresses: List + ): List = withContext(dispatchers.io) { + val cachedPrices = tokenAddresses.mapNotNull { cachedAddressesToPrices[it] } + if (cachedPrices.size == tokenAddresses.size) { + return@withContext cachedPrices + } + + val request = tokenAddresses.joinToString(",") + val currency = "usd" + val response = api.getEthereumTokenPrices(request, currency).mapValues { + TokenServicePrice( + tokenAddress = it.key, + rate = TokenRate(it.value.priceInUsd), + network = TokenServiceNetwork.ETHEREUM + ) + } + cachedAddressesToPrices.putAll(response) + response.values.toList() + } +} + +internal interface CoinGeckoDataSource { + @GET("coins/markets") + suspend fun getTokenPrices( + @Query("ids") tokenIds: String, + @Query("vs_currency") targetCurrency: String + ): List + + @GET("simple/token_price/ethereum") + suspend fun getEthereumTokenPrices( + @Query("contract_addresses", encoded = true) tokenAddresses: String, + @Query("vs_currencies") targetCurrency: String + ): Map +} diff --git a/token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoEthereumPriceResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoEthereumPriceResponse.kt new file mode 100644 index 0000000000..d82569304d --- /dev/null +++ b/token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoEthereumPriceResponse.kt @@ -0,0 +1,9 @@ +package org.p2p.token.service.api.coingecko.response + +import com.google.gson.annotations.SerializedName +import java.math.BigDecimal + +internal class CoinGeckoEthereumPriceResponse( + @SerializedName("usd") + val priceInUsd: BigDecimal? +) diff --git a/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoPriceResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoSolPriceResponse.kt similarity index 76% rename from app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoPriceResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoSolPriceResponse.kt index 5916b86932..c029b57233 100644 --- a/app/src/main/java/org/p2p/wallet/infrastructure/network/coingecko/response/CoinGeckoPriceResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/coingecko/response/CoinGeckoSolPriceResponse.kt @@ -1,9 +1,9 @@ -package org.p2p.wallet.infrastructure.network.coingecko.response +package org.p2p.token.service.api.coingecko.response import com.google.gson.annotations.SerializedName import java.math.BigDecimal -class CoinGeckoPriceResponse( +internal class CoinGeckoSolPriceResponse( @SerializedName("id") val id: String, @SerializedName("symbol") diff --git a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt b/token-service/src/main/java/org/p2p/token/service/api/jupiter/JupiterPricesDataSource.kt similarity index 84% rename from token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt rename to token-service/src/main/java/org/p2p/token/service/api/jupiter/JupiterPricesDataSource.kt index a042f0e2cd..fd7c826e08 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesDataSource.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/jupiter/JupiterPricesDataSource.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api +package org.p2p.token.service.api.jupiter import retrofit2.http.GET import retrofit2.http.Query diff --git a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/jupiter/JupiterPricesRootResponse.kt similarity index 91% rename from token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/jupiter/JupiterPricesRootResponse.kt index 8cb118656a..c4498d5ad7 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/JupiterPricesRootResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/jupiter/JupiterPricesRootResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api +package org.p2p.token.service.api.jupiter import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/TokenServiceDataSource.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/TokenServiceDataSource.kt similarity index 81% rename from token-service/src/main/java/org/p2p/token/service/api/TokenServiceDataSource.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/TokenServiceDataSource.kt index 9d6be753af..1d6b033bc5 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/TokenServiceDataSource.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/TokenServiceDataSource.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api +package org.p2p.token.service.api.tokenservice import org.p2p.core.rpc.JsonRpc import org.p2p.token.service.model.TokenServiceResult diff --git a/token-service/src/main/java/org/p2p/token/service/api/TokenServiceRemoteDataSource.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/TokenServiceRemoteDataSource.kt similarity index 96% rename from token-service/src/main/java/org/p2p/token/service/api/TokenServiceRemoteDataSource.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/TokenServiceRemoteDataSource.kt index e801c3fa7a..f3f4248837 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/TokenServiceRemoteDataSource.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/TokenServiceRemoteDataSource.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api +package org.p2p.token.service.api.tokenservice import com.google.gson.Gson import timber.log.Timber diff --git a/token-service/src/main/java/org/p2p/token/service/api/request/TokenAmountsRequest.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenAmountsRequest.kt similarity index 84% rename from token-service/src/main/java/org/p2p/token/service/api/request/TokenAmountsRequest.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenAmountsRequest.kt index 2d7956edf4..8ea3bd89f4 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/request/TokenAmountsRequest.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenAmountsRequest.kt @@ -1,10 +1,10 @@ -package org.p2p.token.service.api.request +package org.p2p.token.service.api.tokenservice.request import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import java.lang.reflect.Type import org.p2p.core.rpc.JsonRpc -import org.p2p.token.service.api.response.TokenAmountsResponse +import org.p2p.token.service.api.tokenservice.response.TokenAmountsResponse internal data class TokenAmountsRequest( @Transient val request: TokenAmountsBodyRequest diff --git a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceItemRequest.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceItemRequest.kt similarity index 63% rename from token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceItemRequest.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceItemRequest.kt index e4f6584dc4..5d19d49d15 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceItemRequest.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceItemRequest.kt @@ -1,7 +1,7 @@ -package org.p2p.token.service.api.request +package org.p2p.token.service.api.tokenservice.request import com.google.gson.annotations.SerializedName -import org.p2p.token.service.api.response.TokenServiceNetworkResponse +import org.p2p.token.service.api.tokenservice.response.TokenServiceNetworkResponse internal data class TokenServiceItemRequest( @SerializedName("chain_id") diff --git a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceMetadataRequest.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceMetadataRequest.kt similarity index 78% rename from token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceMetadataRequest.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceMetadataRequest.kt index a02cb58572..c81fac6657 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceMetadataRequest.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceMetadataRequest.kt @@ -1,9 +1,9 @@ -package org.p2p.token.service.api.request +package org.p2p.token.service.api.tokenservice.request import com.google.gson.reflect.TypeToken import java.lang.reflect.Type import org.p2p.core.rpc.JsonRpc -import org.p2p.token.service.api.response.TokenServiceMetadataResponse +import org.p2p.token.service.api.tokenservice.response.TokenServiceMetadataResponse private typealias MetadataResponse = List diff --git a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServicePriceRequest.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServicePriceRequest.kt similarity index 78% rename from token-service/src/main/java/org/p2p/token/service/api/request/TokenServicePriceRequest.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServicePriceRequest.kt index b6d8c3b36d..f42a0fb2a1 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServicePriceRequest.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServicePriceRequest.kt @@ -1,9 +1,9 @@ -package org.p2p.token.service.api.request +package org.p2p.token.service.api.tokenservice.request import com.google.gson.reflect.TypeToken import java.lang.reflect.Type import org.p2p.core.rpc.JsonRpc -import org.p2p.token.service.api.response.TokenServicePriceResponse +import org.p2p.token.service.api.tokenservice.response.TokenServicePriceResponse private typealias PriceResponse = List diff --git a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceQueryRequest.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceQueryRequest.kt similarity index 75% rename from token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceQueryRequest.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceQueryRequest.kt index 25d1bce602..5db4d5c12a 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/request/TokenServiceQueryRequest.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/request/TokenServiceQueryRequest.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.request +package org.p2p.token.service.api.tokenservice.request import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenAmountsResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenAmountsResponse.kt similarity index 87% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenAmountsResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenAmountsResponse.kt index a42e23d456..5e6f82dc4f 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenAmountsResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenAmountsResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName import java.math.BigInteger diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenItemMetadataResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenItemMetadataResponse.kt similarity index 88% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenItemMetadataResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenItemMetadataResponse.kt index c2c8bf167f..e1f6e0b9d9 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenItemMetadataResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenItemMetadataResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenItemPriceResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenItemPriceResponse.kt similarity index 79% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenItemPriceResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenItemPriceResponse.kt index 8ec95d4f32..521b675f7d 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenItemPriceResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenItemPriceResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenListResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenListResponse.kt similarity index 96% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenListResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenListResponse.kt index 2bde356ebf..2c2d7e7983 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenListResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenListResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenRateResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenRateResponse.kt similarity index 75% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenRateResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenRateResponse.kt index 676a71db4c..99f491d179 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenRateResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenRateResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName import java.math.BigDecimal diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenServiceMetadataResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServiceMetadataResponse.kt similarity index 84% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenServiceMetadataResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServiceMetadataResponse.kt index 79b632d038..6662586d1c 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenServiceMetadataResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServiceMetadataResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenServiceNetworkResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServiceNetworkResponse.kt similarity index 77% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenServiceNetworkResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServiceNetworkResponse.kt index 9fd8c3ce1a..de97b013d0 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenServiceNetworkResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServiceNetworkResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/api/response/TokenServicePriceResponse.kt b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServicePriceResponse.kt similarity index 83% rename from token-service/src/main/java/org/p2p/token/service/api/response/TokenServicePriceResponse.kt rename to token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServicePriceResponse.kt index 6f1e37ba04..adf47d1651 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/response/TokenServicePriceResponse.kt +++ b/token-service/src/main/java/org/p2p/token/service/api/tokenservice/response/TokenServicePriceResponse.kt @@ -1,4 +1,4 @@ -package org.p2p.token.service.api.response +package org.p2p.token.service.api.tokenservice.response import com.google.gson.annotations.SerializedName diff --git a/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt b/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt index 5e731dd8d5..1c1080d536 100644 --- a/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt +++ b/token-service/src/main/java/org/p2p/token/service/converter/TokenServiceAmountsRemoteConverter.kt @@ -3,10 +3,10 @@ package org.p2p.token.service.converter import java.math.BigInteger import org.p2p.core.crypto.Base58String import org.p2p.core.utils.isZero -import org.p2p.token.service.api.TokenServiceDataSource -import org.p2p.token.service.api.request.TokenAmountsBodyRequest -import org.p2p.token.service.api.request.TokenAmountsRequest -import org.p2p.token.service.api.response.TokenAmountsResponse +import org.p2p.token.service.api.tokenservice.TokenServiceDataSource +import org.p2p.token.service.api.tokenservice.request.TokenAmountsBodyRequest +import org.p2p.token.service.api.tokenservice.request.TokenAmountsRequest +import org.p2p.token.service.api.tokenservice.response.TokenAmountsResponse import org.p2p.token.service.model.successOrNull internal class TokenServiceAmountsRemoteConverter( diff --git a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt index cfe3b3fa4d..72f19a5158 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/TokenServiceRepositoryImpl.kt @@ -2,6 +2,7 @@ package org.p2p.token.service.repository import timber.log.Timber import kotlinx.coroutines.flow.Flow +import org.p2p.token.service.api.coingecko.CoinGeckoTokenPriceRepository import org.p2p.token.service.model.TokenServiceMetadata import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice @@ -14,6 +15,7 @@ import org.p2p.token.service.repository.price.TokenPriceRepository internal class TokenServiceRepositoryImpl( private val priceRemoteRepository: TokenPriceRepository, private val jupiterPriceRepository: JupiterTokenPriceRepository, + private val coinGeckoTokenPriceRepository: CoinGeckoTokenPriceRepository, private val priceLocalRepository: TokenPriceLocalRepository, private val metadataLocalRepository: TokenMetadataLocalRepository, private val metadataRemoteRepository: TokenMetadataRepository @@ -54,20 +56,20 @@ internal class TokenServiceRepositoryImpl( } override suspend fun getTokenPricesByAddresses( - tokenAddress: List, + tokensAddresses: List, networkChain: TokenServiceNetwork, forceRemote: Boolean ): List = try { when (networkChain) { TokenServiceNetwork.SOLANA -> { - jupiterPriceRepository.loadTokensPrice(networkChain, tokenAddress).items + jupiterPriceRepository.loadTokensPrice(networkChain, tokensAddresses).items } TokenServiceNetwork.ETHEREUM -> { - priceRemoteRepository.loadTokensPrice(networkChain, tokenAddress).flatMap { it.items } + coinGeckoTokenPriceRepository.loadEthereumTokenPrices(tokensAddresses) } } } catch (error: Exception) { - Timber.i(error) + Timber.e(error, "failed to get prices for $tokensAddresses") emptyList() } diff --git a/token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt b/token-service/src/main/java/org/p2p/token/service/repository/mapper/TokenServiceMapper.kt similarity index 82% rename from token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt rename to token-service/src/main/java/org/p2p/token/service/repository/mapper/TokenServiceMapper.kt index 4854d20cbe..1066e61279 100644 --- a/token-service/src/main/java/org/p2p/token/service/api/mapper/TokenServiceMapper.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/mapper/TokenServiceMapper.kt @@ -1,21 +1,21 @@ -package org.p2p.token.service.api.mapper +package org.p2p.token.service.repository.mapper import org.p2p.core.token.TokenMetadata import org.p2p.core.token.TokenMetadataExtension -import org.p2p.token.service.api.request.TokenServiceItemRequest -import org.p2p.token.service.api.request.TokenServiceQueryRequest -import org.p2p.token.service.api.response.MetadataExtensionResponse -import org.p2p.token.service.api.response.TokenItemMetadataResponse -import org.p2p.token.service.api.response.TokenItemPriceResponse -import org.p2p.token.service.api.response.TokenRateResponse -import org.p2p.token.service.api.response.TokenResponse -import org.p2p.token.service.api.response.TokenServiceNetworkResponse +import org.p2p.token.service.api.tokenservice.request.TokenServiceItemRequest +import org.p2p.token.service.api.tokenservice.request.TokenServiceQueryRequest +import org.p2p.token.service.api.tokenservice.response.MetadataExtensionResponse +import org.p2p.token.service.api.tokenservice.response.TokenItemMetadataResponse +import org.p2p.token.service.api.tokenservice.response.TokenItemPriceResponse +import org.p2p.token.service.api.tokenservice.response.TokenRateResponse +import org.p2p.token.service.api.tokenservice.response.TokenResponse +import org.p2p.token.service.api.tokenservice.response.TokenServiceNetworkResponse import org.p2p.token.service.model.TokenRate import org.p2p.token.service.model.TokenServiceMetadata import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice -class TokenServiceMapper { +internal class TokenServiceMapper { internal fun fromNetwork(response: TokenResponse): TokenMetadata = TokenMetadata( diff --git a/token-service/src/main/java/org/p2p/token/service/repository/metadata/TokenMetadataRemoteRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/metadata/TokenMetadataRemoteRepository.kt index ae1c1e1c5d..bd451c9eb3 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/metadata/TokenMetadataRemoteRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/metadata/TokenMetadataRemoteRepository.kt @@ -8,15 +8,15 @@ import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.network.environment.NetworkServicesUrlProvider import org.p2p.core.rpc.RpcApi import org.p2p.core.token.TokensMetadataInfo -import org.p2p.token.service.api.TokenServiceDataSource -import org.p2p.token.service.api.mapper.TokenServiceMapper -import org.p2p.token.service.api.request.TokenServiceMetadataRequest -import org.p2p.token.service.api.response.TokenListResponse +import org.p2p.token.service.api.tokenservice.TokenServiceDataSource +import org.p2p.token.service.api.tokenservice.request.TokenServiceMetadataRequest +import org.p2p.token.service.api.tokenservice.response.TokenListResponse import org.p2p.token.service.model.TokenServiceMetadata import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServiceQueryResult import org.p2p.token.service.model.UpdateTokenMetadataResult import org.p2p.token.service.model.successOrNull +import org.p2p.token.service.repository.mapper.TokenServiceMapper private const val ALL_TOKENS_MAP_CHUNKED_COUNT = 50 private const val HEADER_LAST_MODIFIED = "last-modified" diff --git a/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt index 46187eeea5..06cb5219eb 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/price/JupiterTokenPriceRepository.kt @@ -6,8 +6,8 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.utils.Constants -import org.p2p.token.service.api.JupiterPricesDataSource -import org.p2p.token.service.api.JupiterPricesResponse +import org.p2p.token.service.api.jupiter.JupiterPricesDataSource +import org.p2p.token.service.api.jupiter.JupiterPricesResponse import org.p2p.token.service.model.TokenRate import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice @@ -21,51 +21,46 @@ internal class JupiterTokenPriceRepository( chain: TokenServiceNetwork, addresses: List ): TokenServiceQueryResult = withContext(dispatchers.io) { - try { - require(chain == TokenServiceNetwork.SOLANA) { "Only Solana tokens prices can be loaded" } + require(chain == TokenServiceNetwork.SOLANA) { "Only Solana tokens prices can be loaded" } - val isNativeRequested = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN in addresses + val isNativeRequested = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN in addresses - val chunkedMints = addresses.chunked(100) + val chunkedMints = addresses.chunked(100) - val mintsAsQueries = chunkedMints.map { - it.joinToString(separator = ",") { - if (it == Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) Constants.WRAPPED_SOL_MINT else it - } - } - - val tokenMintsToRates = mintsAsQueries.map { - // jupiter supports only 100 tokens in one query - async { api.getPrices(it).tokenMintsToPrices } + val mintsAsQueries = chunkedMints.map { + it.joinToString(separator = ",") { + if (it == Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN) Constants.WRAPPED_SOL_MINT else it } - .awaitAll() - .reduce(Map::plus) - .mapValues { it.value.usdPrice } - .run { - if (isNativeRequested) { - val nativeSolRate = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN to this[Constants.WRAPPED_SOL_MINT] - this + nativeSolRate - } else { - this - } - } + } - val prices = addresses.map { mintAddress -> - val rate = tokenMintsToRates[mintAddress]?.toBigDecimal() - if (rate == null) { - Timber.e("USD rate for $mintAddress not found!") + val tokenMintsToRates = mintsAsQueries.map { + // jupiter supports only 100 tokens in one query + async { api.getPrices(it).tokenMintsToPrices } + } + .awaitAll() + .reduce(Map::plus) + .mapValues { it.value.usdPrice } + .run { + if (isNativeRequested) { + val nativeSolRate = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN to this[Constants.WRAPPED_SOL_MINT] + this + nativeSolRate + } else { + this } + } - TokenServicePrice( - tokenAddress = mintAddress, - rate = TokenRate(rate), - network = chain - ) + val prices = addresses.map { mintAddress -> + val rate = tokenMintsToRates[mintAddress]?.toBigDecimal() + if (rate == null) { + Timber.e("USD rate for $mintAddress not found!") } - TokenServiceQueryResult(networkChain = chain, items = prices) - } catch (error: Exception) { - Timber.i(error, "Failed fetching prices") - throw error + + TokenServicePrice( + tokenAddress = mintAddress, + rate = TokenRate(rate), + network = chain + ) } + TokenServiceQueryResult(networkChain = chain, items = prices) } } diff --git a/token-service/src/main/java/org/p2p/token/service/repository/price/TokenPriceRemoteRepository.kt b/token-service/src/main/java/org/p2p/token/service/repository/price/TokenPriceRemoteRepository.kt index 9ffdaa6ccd..60cc2cb74a 100644 --- a/token-service/src/main/java/org/p2p/token/service/repository/price/TokenPriceRemoteRepository.kt +++ b/token-service/src/main/java/org/p2p/token/service/repository/price/TokenPriceRemoteRepository.kt @@ -1,12 +1,12 @@ package org.p2p.token.service.repository.price -import org.p2p.token.service.api.TokenServiceDataSource -import org.p2p.token.service.api.mapper.TokenServiceMapper -import org.p2p.token.service.api.request.TokenServicePriceRequest +import org.p2p.token.service.api.tokenservice.TokenServiceDataSource +import org.p2p.token.service.api.tokenservice.request.TokenServicePriceRequest import org.p2p.token.service.model.TokenServiceNetwork import org.p2p.token.service.model.TokenServicePrice import org.p2p.token.service.model.TokenServiceQueryResult import org.p2p.token.service.model.successOrNull +import org.p2p.token.service.repository.mapper.TokenServiceMapper internal class TokenPriceRemoteRepository( private val api: TokenServiceDataSource, From 7c6abd5c57e619c5f3896b40d697dd7918918153 Mon Sep 17 00:00:00 2001 From: Gleb Levinkov Date: Tue, 20 Feb 2024 21:56:30 +0300 Subject: [PATCH 42/51] bump app version to 2.12.0 --- buildSrc/src/main/java/AppVersions.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/java/AppVersions.kt b/buildSrc/src/main/java/AppVersions.kt index dd354f5386..f71eee89c3 100644 --- a/buildSrc/src/main/java/AppVersions.kt +++ b/buildSrc/src/main/java/AppVersions.kt @@ -1,7 +1,7 @@ object AppVersions { const val VERSION_MAJOR = 2 - const val VERSION_MINOR = 11 - const val VERSION_HOTFIX = 2 - const val VERSION_PATCH = 1 + const val VERSION_MINOR = 12 + const val VERSION_HOTFIX = 0 + const val VERSION_PATCH = 0 const val VERSION_BUILD = 3582 } From f5ca887e9bc2a0b5aab98141cc541e703e76e994 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Thu, 22 Feb 2024 00:28:48 +0400 Subject: [PATCH 43/51] NO-TICKET - handle send service failure and do not block send flow (#2212) (#2213) --- .../usecase/CalculateSendFeesUseCase.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt index 0bd1a616b9..77c1232d3f 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt @@ -104,13 +104,21 @@ class CalculateSendFeesUseCase( return BigInteger.ZERO } - return sendServiceRepository - .getTokenRentExemption( - userWallet = sourceToken.publicKey.toBase58Instance(), - token = sourceToken, - // return in TOKEN - returnInToken = true - ) + return try { + sendServiceRepository + .getTokenRentExemption( + userWallet = sourceToken.publicKey.toBase58Instance(), + token = sourceToken, + // return in TOKEN + returnInToken = true + ) + } catch (e: Throwable) { + // send service can return "insufficient funds" when fee payer token is the same as source token + // and there's not enough balance to cover account creation fee + // todo: errors should be checked specifically for this type of error, because send-service may fail + // by another reason + BigInteger.ZERO + } } /** From 697f1bf7a71d33c4762d51309ba5b2215c67f321 Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:50:06 +0300 Subject: [PATCH 44/51] PWN-1048 - fix Bridge banner (#2211) * PWN-1048 - fix Bridge banner * PWN-1049 - fix searching Buy by symbols * PWN-1052 - create a delay to show snackbar * PWN-1050 PWN-1051 - fix scaling issues in CashOut --- .../bridge/send/ui/BridgeSendPresenter.kt | 4 +- .../model/rpc/RpcHistoryTransaction.kt | 8 ++-- .../wallet/history/ui/model/HistoryItem.kt | 6 +-- .../deeplinks/MainFragmentDeeplinkHandler.kt | 6 +-- .../home/ui/crypto/MyCryptoPresenter.kt | 4 +- ...mSheet.kt => BuySelectTokenBottomSheet.kt} | 33 +++++++------- .../interactor/JupiterSwapTokensResult.kt | 1 + .../model/JupiterSwapMarketInformation.kt | 6 +-- .../clientsideapi/response/MoonpayCurrency.kt | 4 +- .../moonpay/interactor/BuyInteractor.kt | 24 +++++----- .../wallet/moonpay/model/MoonpayBuyQuote.kt | 2 +- .../p2p/wallet/moonpay/ui/new/BuyFragment.kt | 6 +-- .../p2p/wallet/moonpay/ui/new/BuyPresenter.kt | 14 +++--- .../SellTransactionDetailsPresenter.kt | 26 ++++++----- .../tokenselect/ReceiveTokensMapper.kt | 2 +- .../ui/derivable/DerivableAccountsAdapter.kt | 4 +- .../sell/interactor/HistoryItemMapper.kt | 8 +++- .../sell/ui/payload/SellPayloadPresenter.kt | 26 ++++++----- .../p2p/wallet/send/model/SendSolanaFee.kt | 11 ++--- .../p2p/wallet/send/ui/NewSendPresenter.kt | 4 +- .../org/p2p/wallet/swap/model/orca/SwapFee.kt | 11 ++--- .../wallet/user/interactor/UserInteractor.kt | 16 +++---- .../user/repository/UserLocalRepository.kt | 1 + .../UserTokensDatabaseRepository.kt | 4 +- .../StrigaOffRampPresenterCalculationTest.kt | 4 +- .../src/main/java/org/p2p/core/token/Token.kt | 35 +++++++-------- .../org/p2p/core/utils/AmountExtensions.kt | 45 +++++++++---------- .../main/java/org/p2p/core/utils/Constants.kt | 3 ++ .../org/p2p/core/utils/DecimalFormatter.kt | 2 + .../token/service/model/TokenServicePrice.kt | 4 +- 30 files changed, 174 insertions(+), 150 deletions(-) rename app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/{NewSelectTokenBottomSheet.kt => BuySelectTokenBottomSheet.kt} (76%) diff --git a/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt b/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt index 8ff87bb73b..4245a145e4 100644 --- a/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/bridge/send/ui/BridgeSendPresenter.kt @@ -18,7 +18,7 @@ import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.isConnectionError import org.p2p.core.utils.isZero import org.p2p.core.utils.orZero -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo import org.p2p.core.utils.toBigDecimalOrZero import org.p2p.uikit.utils.text.TextViewCellModel import org.p2p.wallet.R @@ -242,7 +242,7 @@ class BridgeSendPresenter( if (calculationMode.getCurrencyMode() is CurrencyMode.Fiat.Usd) { switchCurrencyMode() } - val newTextValue = inputAmount.scaleShort().toPlainString() + val newTextValue = inputAmount.scaleTwo().toPlainString() updateInputValue(newTextValue, forced = true) calculationMode.updateInputAmount(newTextValue) disableInputs() diff --git a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt index 06205dc6b3..093df376e6 100644 --- a/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt +++ b/app/src/main/java/org/p2p/wallet/history/model/rpc/RpcHistoryTransaction.kt @@ -12,7 +12,7 @@ import org.p2p.core.utils.asNegativeUsdTransaction import org.p2p.core.utils.asPositiveUsdTransaction import org.p2p.core.utils.asUsdTransaction import org.p2p.core.utils.formatTokenWithSymbol -import org.p2p.core.utils.scaleShortOrFirstNotZero +import org.p2p.core.utils.scaleTwoOrFirstNotZero import org.p2p.wallet.R import org.p2p.wallet.history.model.HistoryTransaction import org.p2p.wallet.transaction.model.HistoryTransactionStatus @@ -210,7 +210,7 @@ sealed class RpcHistoryTransaction( fun getFormattedFiatValue(): String? { return amount.totalInUsd - ?.scaleShortOrFirstNotZero() + ?.scaleTwoOrFirstNotZero() ?.asUsdTransaction(getSymbol(isSend)) } @@ -263,7 +263,7 @@ sealed class RpcHistoryTransaction( fun getFormattedFiatValue(): String? { return amount.totalInUsd - ?.scaleShortOrFirstNotZero() + ?.scaleTwoOrFirstNotZero() ?.asPositiveUsdTransaction() } @@ -305,7 +305,7 @@ sealed class RpcHistoryTransaction( fun getValue(): String? { return amount.totalInUsd - ?.scaleShortOrFirstNotZero() + ?.scaleTwoOrFirstNotZero() ?.asUsdTransaction(getSymbol(isStake)) } diff --git a/app/src/main/java/org/p2p/wallet/history/ui/model/HistoryItem.kt b/app/src/main/java/org/p2p/wallet/history/ui/model/HistoryItem.kt index 1b17b296f3..823a268d1d 100644 --- a/app/src/main/java/org/p2p/wallet/history/ui/model/HistoryItem.kt +++ b/app/src/main/java/org/p2p/wallet/history/ui/model/HistoryItem.kt @@ -6,7 +6,7 @@ import org.threeten.bp.ZonedDateTime import org.p2p.core.utils.asNegativeUsdTransaction import org.p2p.core.utils.asPositiveUsdTransaction import org.p2p.core.utils.formatToken -import org.p2p.core.utils.scaleMedium +import org.p2p.core.utils.scaleSix import org.p2p.core.utils.toBigDecimalOrZero import org.p2p.uikit.utils.recycler.RoundedItem import org.p2p.wallet.bridge.model.BridgeBundle @@ -92,7 +92,7 @@ sealed interface HistoryItem { val tokenSymbol = sendDetails.amount.symbol return if (scaleMedium) { - "${totalAmount.scaleMedium().formatToken()} $tokenSymbol" + "${totalAmount.scaleSix().formatToken()} $tokenSymbol" } else { "${totalAmount.formatToken()} $tokenSymbol" } @@ -117,7 +117,7 @@ sealed interface HistoryItem { val tokenSymbol = bundle.resultAmount.symbol return if (scaleMedium) { - "${totalAmount.scaleMedium().formatToken()} $tokenSymbol" + "${totalAmount.scaleSix().formatToken()} $tokenSymbol" } else { "${totalAmount.formatToken()} $tokenSymbol" } diff --git a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt index 4d45fa6008..81bf185869 100644 --- a/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt +++ b/app/src/main/java/org/p2p/wallet/home/deeplinks/MainFragmentDeeplinkHandler.kt @@ -102,13 +102,13 @@ class MainFragmentDeeplinkHandler( * Handles buy token for fiat deeplink */ private fun handleBuyDeeplink(data: DeeplinkData) { - val cryptoToken = data.args["to"] + val cryptoTokenMint = data.args["to"] val fiatToken = data.args["from"] val fiatAmount = data.args["amount"] - if (!cryptoToken.isNullOrBlank() && !fiatToken.isNullOrBlank()) { + if (!cryptoTokenMint.isNullOrBlank() && !fiatToken.isNullOrBlank()) { coroutineScope.launch { - val token = userInteractor.getSingleTokenForBuy(listOf(cryptoToken)) + val token = userInteractor.getSingleTokenForBuy(listOf(cryptoTokenMint)) if (token != null) { screenNavigator?.navigateToBuyScreen(token, fiatToken, fiatAmount) } else { diff --git a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt index f561f66621..78f33288f6 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/crypto/MyCryptoPresenter.kt @@ -13,7 +13,7 @@ import org.p2p.core.token.Token import org.p2p.core.token.TokenVisibility import org.p2p.core.token.filterTokensForWalletScreen import org.p2p.core.utils.isMoreThan -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo import org.p2p.uikit.model.AnyCellItem import org.p2p.wallet.R import org.p2p.wallet.auth.interactor.UsernameInteractor @@ -199,7 +199,7 @@ class MyCryptoPresenter( .mapNotNull(Token.Active::totalInUsd) .filter { it.isMoreThan(MINIMAL_DUST_FOR_BALANCE) } .fold(BigDecimal.ZERO, BigDecimal::add) - .scaleShort() + .scaleTwo() } private fun handleEmptyAccount() { diff --git a/app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/NewSelectTokenBottomSheet.kt b/app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/BuySelectTokenBottomSheet.kt similarity index 76% rename from app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/NewSelectTokenBottomSheet.kt rename to app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/BuySelectTokenBottomSheet.kt index 90e576dbe4..38284050f6 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/NewSelectTokenBottomSheet.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/select/bottomsheet/BuySelectTokenBottomSheet.kt @@ -1,56 +1,59 @@ package org.p2p.wallet.home.ui.select.bottomsheet -import android.os.Bundle -import android.view.View import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager import androidx.fragment.app.setFragmentResult import androidx.recyclerview.widget.LinearLayoutManager +import android.os.Bundle +import android.view.View import org.koin.android.ext.android.inject +import org.p2p.core.token.Token import org.p2p.uikit.utils.attachAdapter import org.p2p.wallet.common.analytics.interactor.ScreensAnalyticsInteractor import org.p2p.wallet.common.ui.bottomsheet.BaseRecyclerDoneBottomSheet import org.p2p.wallet.common.ui.recycler.adapter.DividerItemDecorator -import org.p2p.core.token.Token import org.p2p.wallet.home.ui.select.NewSelectTokenAdapter import org.p2p.wallet.moonpay.analytics.BuyAnalytics import org.p2p.wallet.utils.args import org.p2p.wallet.utils.unsafeLazy import org.p2p.wallet.utils.withArgs -private const val ARG_ALL_TOKENS = "ARG_ALL_TOKENS" +private const val ARG_TOKENS_TO_BUY = "ARG_ALL_TOKENS" private const val ARG_SELECTED_TOKEN = "ARG_SELECTED_TOKEN" -class NewSelectTokenBottomSheet : BaseRecyclerDoneBottomSheet() { +class BuySelectTokenBottomSheet : BaseRecyclerDoneBottomSheet() { companion object { fun show( fm: FragmentManager, title: String, - tokens: List, + tokensToBuy: List, preselectedToken: Token? = null, requestKey: String, resultKey: String - ) = NewSelectTokenBottomSheet().withArgs( + ) = BuySelectTokenBottomSheet().withArgs( ARG_TITLE to title, - ARG_ALL_TOKENS to tokens, + ARG_TOKENS_TO_BUY to tokensToBuy, ARG_SELECTED_TOKEN to preselectedToken, ARG_REQUEST_KEY to requestKey, ARG_RESULT_KEY to resultKey - ).show(fm, NewSelectTokenBottomSheet::javaClass.name) + ).show(fm, BuySelectTokenBottomSheet::javaClass.name) } - private val tokens: List by args(ARG_ALL_TOKENS) + private val tokensToBuy: List by args(ARG_TOKENS_TO_BUY) private val preselectedToken: Token? by args(ARG_SELECTED_TOKEN) private val buyAnalytics: BuyAnalytics by inject() private val analyticsInteractor: ScreensAnalyticsInteractor by inject() private val tokenAdapter: NewSelectTokenAdapter by unsafeLazy { - NewSelectTokenAdapter(preselectedItem = preselectedToken, onItemClicked = { - setFragmentResult(requestKey, bundleOf(resultKey to it)) - dismissAllowingStateLoss() - }) + NewSelectTokenAdapter( + preselectedItem = preselectedToken, + onItemClicked = { + setFragmentResult(requestKey, bundleOf(resultKey to it)) + dismissAllowingStateLoss() + } + ) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -61,7 +64,7 @@ class NewSelectTokenBottomSheet : BaseRecyclerDoneBottomSheet() { DividerItemDecorator(requireContext()) ) recyclerView.attachAdapter(tokenAdapter) - tokenAdapter.setItems(tokens) + tokenAdapter.setItems(tokensToBuy) } } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/interactor/JupiterSwapTokensResult.kt b/app/src/main/java/org/p2p/wallet/jupiter/interactor/JupiterSwapTokensResult.kt index 962863e733..65f1c4630e 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/interactor/JupiterSwapTokensResult.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/interactor/JupiterSwapTokensResult.kt @@ -12,6 +12,7 @@ sealed interface JupiterSwapTokensResult { * https://github.com/orca-so/whirlpools/blob/e06505fb1e41508295eca116ca676c8f498398d2/programs/whirlpool/src/manager/whirlpool_manager.rs#L13 * happens randomly error occurs when sending transaction, * but it's not our fault, it depends on the node state + * https://discord.com/channels/767761912167006292/1106227799448101004 */ class InvalidTimestampRpcError(override val cause: ServerException) : Throwable(cause.message) } diff --git a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapMarketInformation.kt b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapMarketInformation.kt index 012b35c997..9366a30828 100644 --- a/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapMarketInformation.kt +++ b/app/src/main/java/org/p2p/wallet/jupiter/repository/model/JupiterSwapMarketInformation.kt @@ -3,7 +3,7 @@ package org.p2p.wallet.jupiter.repository.model import java.math.BigDecimal import java.math.BigInteger import org.p2p.core.crypto.Base58String -import org.p2p.core.utils.scaleShortOrFirstNotZero +import org.p2p.core.utils.scaleTwoOrFirstNotZero @Deprecated("Old v4 swap logic") data class JupiterSwapMarketInformation( @@ -26,7 +26,7 @@ data class JupiterSwapMarketInformation( val mint: Base58String, val percent: BigDecimal ) { - val formattedPercent: BigDecimal = percent.times(BigDecimal(100)).scaleShortOrFirstNotZero() + val formattedPercent: BigDecimal = percent.times(BigDecimal(100)).scaleTwoOrFirstNotZero() } @Deprecated("Old v4 swap logic") @@ -35,6 +35,6 @@ data class JupiterSwapMarketInformation( val mint: Base58String, val percent: BigDecimal ) { - val formattedPercent: BigDecimal = percent.times(BigDecimal(100)).scaleShortOrFirstNotZero() + val formattedPercent: BigDecimal = percent.times(BigDecimal(100)).scaleTwoOrFirstNotZero() } } diff --git a/app/src/main/java/org/p2p/wallet/moonpay/clientsideapi/response/MoonpayCurrency.kt b/app/src/main/java/org/p2p/wallet/moonpay/clientsideapi/response/MoonpayCurrency.kt index 9c23e7f023..c88e3a8e65 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/clientsideapi/response/MoonpayCurrency.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/clientsideapi/response/MoonpayCurrency.kt @@ -6,7 +6,9 @@ sealed class MoonpayCurrency { abstract val currencyId: String abstract val amounts: MoonpayCurrencyAmounts - fun isSol() = this is CryptoToken && tokenSymbol.uppercase() == Constants.SOL_SYMBOL + // it's okay to keep equals by symbol here + // because we only have non-scam wrapped SOL in/out Moonpay + fun isSol() = this is CryptoToken && tokenSymbol.equals(Constants.SOL_SYMBOL, ignoreCase = true) data class CryptoToken( val tokenSymbol: String, diff --git a/app/src/main/java/org/p2p/wallet/moonpay/interactor/BuyInteractor.kt b/app/src/main/java/org/p2p/wallet/moonpay/interactor/BuyInteractor.kt index f5298002a3..d1220a1bee 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/interactor/BuyInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/interactor/BuyInteractor.kt @@ -39,10 +39,13 @@ class BuyInteractor( fun getQuotes(): List = quotes.toList() - suspend fun loadQuotes(currencies: List, tokens: List): Unit = withContext(dispatchers.io) { - Timber.i("Loading quotes for buy: currencies=$currencies; tokens=${tokens.map(Token::mintAddress)}") + suspend fun loadQuotes( + currencies: List, + tokensToBuy: List + ): Unit = withContext(dispatchers.io) { + Timber.i("Loading quotes for buy: currencies=$currencies; tokens=${tokensToBuy.map(Token::mintAddress)}") currencies.flatMap { currency -> - tokens.map { token -> + tokensToBuy.map { token -> async { loadQuote(currency, token) } } } @@ -102,31 +105,30 @@ class BuyInteractor( tokenSymbol: String ): BigDecimal { val quote = quotes.find { - it.currency == FiatCurrency.getFromAbbreviation(currencyCode) && it.token.tokenSymbol == tokenSymbol + it.currency == FiatCurrency.getFromAbbreviation(currencyCode) && it.tokenToBuy.tokenSymbol == tokenSymbol } return HARDCODED_MIN_BUY_CURRENCY_AMOUNT.toBigDecimal() } - private suspend fun loadQuote(currency: FiatCurrency, token: Token) { - Timber.d("Load quote for currency=$currency; token=${token.tokenSymbol}") + private suspend fun loadQuote(currency: FiatCurrency, tokenToBuy: Token) { + Timber.d("Load quote for currency=$currency; token=${tokenToBuy.tokenSymbol}") try { val response = moonpayRepository.getBuyCurrencyData( baseCurrencyAmount = CURRENCY_AMOUNT_FOR_PRICE_REQUEST, quoteCurrencyAmount = null, - tokenToBuy = token, + tokenToBuy = tokenToBuy, baseCurrencyCode = currency.abbreviation.lowercase(), paymentMethod = DEFAULT_PAYMENT_TYPE ) - val result = MoonpayBuyQuote( + quotes += MoonpayBuyQuote( currency = currency, - token = token, + tokenToBuy = tokenToBuy, price = response.quoteCurrencyPrice, minAmount = HARDCODED_MIN_BUY_CURRENCY_AMOUNT.toBigDecimal() ) - quotes += result } catch (e: Throwable) { - Timber.e(e, "Error while loading quote for currency=$currency; token=${token.tokenSymbol}") + Timber.e(e, "Error while loading quote for currency=$currency; token=${tokenToBuy.tokenSymbol}") } } diff --git a/app/src/main/java/org/p2p/wallet/moonpay/model/MoonpayBuyQuote.kt b/app/src/main/java/org/p2p/wallet/moonpay/model/MoonpayBuyQuote.kt index 8cf350a3a7..901fdb5f59 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/model/MoonpayBuyQuote.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/model/MoonpayBuyQuote.kt @@ -6,7 +6,7 @@ import org.p2p.wallet.moonpay.repository.sell.FiatCurrency data class MoonpayBuyQuote( val currency: FiatCurrency, - val token: Token, + val tokenToBuy: Token, val price: BigDecimal, val minAmount: BigDecimal ) diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt index a4fc3ba1c1..9d3c7de481 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyFragment.kt @@ -18,7 +18,7 @@ import org.p2p.wallet.R import org.p2p.wallet.common.analytics.interactor.ScreensAnalyticsInteractor import org.p2p.wallet.common.mvp.BaseMvpFragment import org.p2p.wallet.databinding.FragmentBuyBinding -import org.p2p.wallet.home.ui.select.bottomsheet.NewSelectTokenBottomSheet +import org.p2p.wallet.home.ui.select.bottomsheet.BuySelectTokenBottomSheet import org.p2p.wallet.home.ui.select.bottomsheet.SelectCurrencyBottomSheet import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.moonpay.model.BuyDetailsState @@ -171,10 +171,10 @@ class BuyFragment : } override fun showTokensToBuy(selectedToken: Token, tokensToBuy: List) { - NewSelectTokenBottomSheet.show( + BuySelectTokenBottomSheet.show( fm = childFragmentManager, title = getString(R.string.buy_select_token_title), - tokens = tokensToBuy, + tokensToBuy = tokensToBuy, preselectedToken = selectedToken, requestKey = KEY_REQUEST, resultKey = KEY_RESULT_TOKEN diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt index 7f0d7f2678..db1cf3d280 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/new/BuyPresenter.kt @@ -13,7 +13,7 @@ import org.p2p.core.utils.asCurrency import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken import org.p2p.core.utils.isZero -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo import org.p2p.core.utils.toBigDecimalOrZero import org.p2p.uikit.components.FocusField import org.p2p.wallet.R @@ -177,7 +177,7 @@ class BuyPresenter( override fun onSelectTokenClicked() { buyInteractor.getQuotesByCurrency(selectedCurrency).forEach { quote -> - tokensToBuy.find { it.tokenSymbol == quote.token.tokenSymbol }?.let { + tokensToBuy.find { it.tokenSymbol == quote.tokenToBuy.tokenSymbol }?.let { it.rate = quote.price it.currency = quote.currency.abbreviation.uppercase() } @@ -405,13 +405,13 @@ class BuyPresenter( val data = BuyViewData( tokenSymbol = selectedToken.tokenSymbol, currencySymbol = selectedCurrency.uiSymbol, - price = buyCurrencyInfo.price.scaleShort(), + price = buyCurrencyInfo.price.scaleTwo(), receiveAmount = buyCurrencyInfo.receiveAmount, - processingFee = buyCurrencyInfo.feeAmount.scaleShort(), - networkFee = buyCurrencyInfo.networkFeeAmount.scaleShort(), - extraFee = buyCurrencyInfo.extraFeeAmount.scaleShort(), + processingFee = buyCurrencyInfo.feeAmount.scaleTwo(), + networkFee = buyCurrencyInfo.networkFeeAmount.scaleTwo(), + extraFee = buyCurrencyInfo.extraFeeAmount.scaleTwo(), accountCreationCost = null, - total = buyCurrencyInfo.totalFiatAmount.scaleShort(), + total = buyCurrencyInfo.totalFiatAmount.scaleTwo(), receiveAmountText = amount, purchaseCostText = currencyForTokensAmount.asCurrency(selectedCurrency.uiSymbol) ) diff --git a/app/src/main/java/org/p2p/wallet/moonpay/ui/transaction/SellTransactionDetailsPresenter.kt b/app/src/main/java/org/p2p/wallet/moonpay/ui/transaction/SellTransactionDetailsPresenter.kt index 7c67466c79..a70eebb9ea 100644 --- a/app/src/main/java/org/p2p/wallet/moonpay/ui/transaction/SellTransactionDetailsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/moonpay/ui/transaction/SellTransactionDetailsPresenter.kt @@ -1,6 +1,12 @@ package org.p2p.wallet.moonpay.ui.transaction import android.content.res.Resources +import org.threeten.bp.ZonedDateTime +import org.threeten.bp.format.DateTimeFormatter +import timber.log.Timber +import java.util.Locale +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.p2p.core.utils.Constants import org.p2p.core.utils.getHtmlString import org.p2p.core.utils.removeLinksUnderline @@ -8,24 +14,19 @@ import org.p2p.wallet.R import org.p2p.wallet.common.date.isSameDayAs import org.p2p.wallet.common.date.toZonedDateTime import org.p2p.wallet.common.mvp.BasePresenter +import org.p2p.wallet.history.analytics.HistoryAnalytics +import org.p2p.wallet.history.interactor.HistoryInteractor +import org.p2p.wallet.moonpay.model.SellTransaction import org.p2p.wallet.moonpay.repository.sell.MoonpaySellCancelResult import org.p2p.wallet.moonpay.serversideapi.response.SellTransactionStatus +import org.p2p.wallet.sell.interactor.HistoryItemMapper import org.p2p.wallet.sell.interactor.SellInteractor import org.p2p.wallet.sell.ui.lock.SellTransactionViewDetails +import org.p2p.wallet.user.interactor.UserTokensInteractor +import org.p2p.wallet.utils.CUT_ADDRESS_SYMBOLS_COUNT import org.p2p.wallet.utils.cutMiddle import org.p2p.wallet.utils.emptyString import org.p2p.wallet.utils.unsafeLazy -import org.threeten.bp.ZonedDateTime -import org.threeten.bp.format.DateTimeFormatter -import timber.log.Timber -import java.util.Locale -import kotlinx.coroutines.launch -import org.p2p.wallet.history.analytics.HistoryAnalytics -import org.p2p.wallet.history.interactor.HistoryInteractor -import org.p2p.wallet.moonpay.model.SellTransaction -import org.p2p.wallet.sell.interactor.HistoryItemMapper -import org.p2p.wallet.user.interactor.UserTokensInteractor -import org.p2p.wallet.utils.CUT_ADDRESS_SYMBOLS_COUNT private const val DATE_FORMAT = "MMMM dd, yyyy" private const val TIME_FORMAT = "HH:mm" @@ -213,6 +214,9 @@ class SellTransactionDetailsPresenter( when (val result = sellInteractor.cancelTransaction(transaction.transactionId)) { is MoonpaySellCancelResult.CancelSuccess -> { view?.showUiKitSnackBar(messageResId = R.string.sell_details_cancel_success) + // I put a 1 second delay here because Snackbar doesn't have any change to appear + // the view closes too fast + delay(1000) view?.close() } is MoonpaySellCancelResult.CancelFailed -> { diff --git a/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensMapper.kt b/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensMapper.kt index 75dbc95af4..6663a9a696 100644 --- a/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensMapper.kt +++ b/app/src/main/java/org/p2p/wallet/receive/tokenselect/ReceiveTokensMapper.kt @@ -27,7 +27,7 @@ object ReceiveTokensMapper { ethTokenUrl: String ): AnyCellItem { val isErc20Token = ERC20Tokens.findTokenByMint(mintAddress, ERC20Tokens.MATIC) != null - val isSol = symbol == Constants.SOL_SYMBOL + val isSol = mintAddress == Constants.WRAPPED_SOL_MINT val replacedName = if (isSol) Constants.SOL_NAME else name return MainCellModel( leftSideCellModel = createLeftSideModel( diff --git a/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsAdapter.kt b/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsAdapter.kt index 7945e6743b..975ae68cad 100644 --- a/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsAdapter.kt +++ b/app/src/main/java/org/p2p/wallet/restore/ui/derivable/DerivableAccountsAdapter.kt @@ -5,7 +5,7 @@ import android.annotation.SuppressLint import android.view.ViewGroup import org.p2p.core.utils.Constants.SOL_SYMBOL import org.p2p.core.utils.isZero -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo import org.p2p.wallet.R import org.p2p.wallet.databinding.ItemTokenSimpleBinding import org.p2p.wallet.restore.model.DerivableAccount @@ -50,7 +50,7 @@ class DerivableAccountsAdapter( @SuppressLint("SetTextI18n") fun onBind(account: DerivableAccount) { - val total = account.totalInUsd?.scaleShort() + val total = account.totalInUsd?.scaleTwo() val tokenTotal = account.totalInSol startAmountView.title = SOL_SYMBOL diff --git a/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt b/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt index 003ef9c7cf..8d87d0aee7 100644 --- a/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt +++ b/app/src/main/java/org/p2p/wallet/sell/interactor/HistoryItemMapper.kt @@ -13,6 +13,7 @@ import org.p2p.wallet.R import org.p2p.wallet.common.date.isSameDayAs import org.p2p.wallet.common.date.toDateString import org.p2p.wallet.common.date.toZonedDateTime +import org.p2p.wallet.common.feature_toggles.toggles.remote.EthAddressEnabledFeatureToggle import org.p2p.wallet.history.model.HistoryTransaction import org.p2p.wallet.history.model.bridge.BridgeHistoryTransaction import org.p2p.wallet.history.model.rpc.RpcHistoryTransaction @@ -31,7 +32,8 @@ private const val USDC_ETH_TOKEN_SYMBOL = "USDCet" class HistoryItemMapper( private val resources: Resources, private val dispatchers: CoroutineDispatchers, - private val userLocalRepository: UserLocalRepository + private val userLocalRepository: UserLocalRepository, + private val ethAddressEnabledFeatureToggle: EthAddressEnabledFeatureToggle ) { private val historyItemFlow = MutableStateFlow?>(null) @@ -91,6 +93,10 @@ class HistoryItemMapper( } private fun createSwapBanner(tokenMintAddress: Base58String): HistoryItem.SwapBannerItem? { + if (!ethAddressEnabledFeatureToggle.isFeatureEnabled) { + return null + } + return when (tokenMintAddress.base58Value) { Constants.USDC_MINT -> { HistoryItem.SwapBannerItem( diff --git a/app/src/main/java/org/p2p/wallet/sell/ui/payload/SellPayloadPresenter.kt b/app/src/main/java/org/p2p/wallet/sell/ui/payload/SellPayloadPresenter.kt index d9ba595b2a..e89b0e0e2c 100644 --- a/app/src/main/java/org/p2p/wallet/sell/ui/payload/SellPayloadPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/sell/ui/payload/SellPayloadPresenter.kt @@ -248,6 +248,7 @@ class SellPayloadPresenter( override fun switchCurrencyMode() { val oldAmount = rawUserSelectedAmount.toBigDecimalOrZero() selectedCurrencyMode = currencyModeToSwitch + val switchedAmount = when (selectedCurrencyMode) { is CurrencyMode.Fiat -> oldAmount.toFiat() is CurrencyMode.Token -> oldAmount.toToken() @@ -290,10 +291,12 @@ class SellPayloadPresenter( val buttonState = determineButtonState() when (buttonState) { - is CashOutButtonState.CashOutAvailable, is CashOutButtonState.NotEnoughUserTokenError -> { + is CashOutButtonState.CashOutAvailable, + is CashOutButtonState.NotEnoughUserTokenError -> { startLoadSellQuoteJob() } - is CashOutButtonState.MinAmountEntered, is CashOutButtonState.MaxAmountExceeded -> { + is CashOutButtonState.MinAmountEntered, + is CashOutButtonState.MaxAmountExceeded -> { sellQuoteJob?.cancel() } } @@ -311,11 +314,9 @@ class SellPayloadPresenter( is CurrencyMode.Fiat -> userSolBalance.toFiat() is CurrencyMode.Token -> userSolBalance }.formatTokenForMoonpay() - viewState = viewState.copy( - widgetViewState = viewState.widgetViewState.copy( - inputAmount = maxAmount - ) - ) + + val newWidgetViewState = viewState.widgetViewState.copy(inputAmount = maxAmount) + viewState = viewState.copy(widgetViewState = newWidgetViewState) view?.updateViewState(viewState) onTokenAmountChanged(maxAmount) } @@ -325,15 +326,12 @@ class SellPayloadPresenter( selectedTokenAmount.isLessThan(minTokenSellAmount) -> { CashOutButtonState.MinAmountEntered(resources, minTokenSellAmount) } - selectedTokenAmount.isMoreThan(maxTokenSellAmount.orZero()) -> { CashOutButtonState.MaxAmountExceeded(resources, maxTokenSellAmount.orZero()) } - selectedTokenAmount.isMoreThan(userSolBalance) -> { CashOutButtonState.NotEnoughUserTokenError(resources) } - else -> { CashOutButtonState.CashOutAvailable(resources) } @@ -356,6 +354,10 @@ class SellPayloadPresenter( } } - private fun BigDecimal.toFiat() = this.multiply(tokenPrice).setScale(2, RoundingMode.DOWN) - private fun BigDecimal.toToken() = this.divide(tokenPrice, 2, RoundingMode.DOWN) + private fun BigDecimal.toFiat(): BigDecimal { + return this.multiply(tokenPrice).setScale(MOONPAY_DECIMAL, RoundingMode.HALF_UP) + } + private fun BigDecimal.toToken(): BigDecimal { + return this.divide(tokenPrice, MOONPAY_DECIMAL, RoundingMode.HALF_UP) + } } diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt index cfa04f6914..82d3bfa269 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendSolanaFee.kt @@ -7,7 +7,6 @@ import java.math.BigInteger import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.p2p.core.token.Token -import org.p2p.core.utils.Constants.SOL_SYMBOL import org.p2p.core.utils.formatToken import org.p2p.core.utils.fromLamports import org.p2p.core.utils.isLessThan @@ -135,7 +134,7 @@ data class SendSolanaFee constructor( minRentExemption: BigInteger ): Boolean = when { // if source is SOL, then fee payer is SOL as well - sourceTokenSymbol == SOL_SYMBOL -> { + sourceToken.isSOL -> { isEnoughSol(sourceTokenTotal, inputAmount, minRentExemption) } // assuming that source token is not SOL @@ -144,11 +143,13 @@ data class SendSolanaFee constructor( sourceTokenTotal >= inputAmount && isEnoughSol(feePayerTotalLamports, totalInSol, minRentExemption) } // assuming that source token and fee payer are same - sourceTokenSymbol == feePayerSymbol -> + sourceTokenSymbol == feePayerSymbol -> { sourceTokenTotal >= inputAmount + feeRelayerFee.totalInSourceToken + } // assuming that source token and fee payer are different - else -> + else -> { feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken + } } private fun isEnoughSol( @@ -169,7 +170,7 @@ data class SendSolanaFee constructor( ): FeePayerState { val feePayerTokenCanCoverExpenses = feePayerToken.totalInLamports >= feeRelayerFee.totalInFeePayerToken val feePayerIsSourceToken = feePayerSymbol == sourceTokenSymbol - val isNotSourceSol = sourceTokenSymbol != SOL_SYMBOL + val isNotSourceSol = sourceToken.isSpl val isAllowedToCorrectAmount = strategy == CORRECT_AMOUNT && isNotSourceSol val totalNeeded = feeRelayerFee.totalInSourceToken + inputAmount val isInsufficientSolBalance = !isEnoughSolBalance() 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 4d7757b2d3..1efbf65bfb 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 @@ -24,7 +24,7 @@ import org.p2p.core.token.findByMintAddress import org.p2p.core.token.sortedWithPreferredStableCoins import org.p2p.core.utils.asNegativeUsdTransaction import org.p2p.core.utils.orZero -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo import org.p2p.wallet.BuildConfig import org.p2p.wallet.R import org.p2p.wallet.alarmlogger.logger.AlarmErrorsLogger @@ -274,7 +274,7 @@ class NewSendPresenter( if (calculationMode.getCurrencyMode() is CurrencyMode.Fiat.Usd) { switchCurrencyMode() } - val newTextValue = inputAmount.scaleShort().toPlainString() + val newTextValue = inputAmount.scaleTwo().toPlainString() updateInputValue(newTextValue, forced = true) calculationMode.updateInputAmount(newTextValue) disableInputs() diff --git a/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt b/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt index a184e857a0..ab9b7bcfdf 100644 --- a/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt +++ b/app/src/main/java/org/p2p/wallet/swap/model/orca/SwapFee.kt @@ -11,7 +11,7 @@ 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.scaleMedium +import org.p2p.core.utils.scaleSix import org.p2p.core.utils.toUsd import org.p2p.wallet.feerelayer.model.FeePayerSelectionStrategy import org.p2p.wallet.send.model.FeePayerState @@ -54,6 +54,7 @@ class SwapFee constructor( } } + @Deprecated("Old") fun isEnoughToCoverExpenses(sourceTokenTotal: BigInteger, inputAmount: BigInteger): Boolean = when { // if source is SOL, then fee payer is SOL as well @@ -68,10 +69,10 @@ class SwapFee constructor( } val feeAmountInPayingToken: BigDecimal - get() = fee.feeInPayingToken.fromLamports(feePayerToken.decimals).scaleMedium() + get() = fee.feeInPayingToken.fromLamports(feePayerToken.decimals).scaleSix() val feeAmountInSol: BigDecimal - get() = fee.feeInSol.fromLamports(SOL_DECIMALS).scaleMedium() + get() = fee.feeInSol.fromLamports(SOL_DECIMALS).scaleSix() val feePayerSymbol: String = feePayerToken.tokenSymbol @@ -96,7 +97,7 @@ class SwapFee constructor( } else { fee.feeInPayingToken.fromLamports(feePayerToken.decimals) } - .scaleMedium() + .scaleSix() } private val accountCreationFeeUsdDecimals: BigDecimal? @@ -105,7 +106,7 @@ class SwapFee constructor( private val currentDecimals: BigDecimal = (if (feePayerToken.isSOL) fee.feeInSol else fee.feeInPayingToken) .fromLamports(feePayerToken.decimals) - .scaleMedium() + .scaleSix() private val feePayerTotalLamports: BigInteger get() = feePayerToken.totalInLamports diff --git a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt index 6cc6e45bb0..e7b1de59b1 100644 --- a/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/user/interactor/UserInteractor.kt @@ -19,7 +19,7 @@ import org.p2p.wallet.user.repository.UserTokensLocalRepository import org.p2p.wallet.utils.emptyString private const val KEY_HIDDEN_TOKENS_VISIBILITY = "KEY_HIDDEN_TOKENS_VISIBILITY" -val TOKEN_SYMBOLS_VALID_FOR_BUY: List = listOf(Constants.USDC_SYMBOL, Constants.SOL_SYMBOL) +val TOKEN_MINTS_VALID_FOR_BUY: List = listOf(Constants.USDC_MINT, Constants.WRAPPED_SOL_MINT) class UserInteractor( private val userLocalRepository: UserLocalRepository, @@ -42,17 +42,17 @@ class UserInteractor( return tokenData?.let { TokenConverter.fromNetwork(it, price) } } - suspend fun getSingleTokenForBuy(availableTokensSymbols: List = TOKEN_SYMBOLS_VALID_FOR_BUY): Token? = - getTokensForBuy(availableTokensSymbols).firstOrNull() + suspend fun getSingleTokenForBuy(availableTokensMints: List = TOKEN_MINTS_VALID_FOR_BUY): Token? = + getTokensForBuy(availableTokensMints).firstOrNull() suspend fun getTokensForBuy( - availableTokensSymbols: List = TOKEN_SYMBOLS_VALID_FOR_BUY + availableTokensMints: List = TOKEN_MINTS_VALID_FOR_BUY ): List { - val tokensToBuy = findTokensMetadataBySymbols(availableTokensSymbols) + val tokensToBuy = findTokensMetadataByMints(availableTokensMints) if (tokensToBuy.isEmpty()) { Timber.e( - IllegalStateException("No tokens to buy! All tokens: $availableTokensSymbols") + IllegalStateException("No tokens to buy! All tokens: $availableTokensMints") ) } return tokensToBuy @@ -92,8 +92,8 @@ class UserInteractor( return userTokens.any { it.publicKey == address } } - private suspend fun findTokensMetadataBySymbols(symbols: List): List { - val tokensData = symbols.mapNotNull { userLocalRepository.findTokenDataBySymbol(it) } + private suspend fun findTokensMetadataByMints(mints: List): List { + val tokensData = mints.mapNotNull(userLocalRepository::findTokenData) val prices = tokensData.let { tokenServiceRepository.getTokenPricesByAddressAsMap( tokenAddress = it.map(TokenMetadata::mintAddress), diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserLocalRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserLocalRepository.kt index 442900dee3..a6c7b9d928 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserLocalRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserLocalRepository.kt @@ -28,6 +28,7 @@ interface UserLocalRepository { /** * Find [TokenMetadata] by its symbol */ + @Deprecated("use findTokenData with mint address") fun findTokenDataBySymbol(symbol: String): TokenMetadata? /** diff --git a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt index f89dd55ce9..83de763088 100644 --- a/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt +++ b/app/src/main/java/org/p2p/wallet/user/repository/UserTokensDatabaseRepository.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.p2p.core.crypto.Base58String import org.p2p.core.token.Token -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo import org.p2p.token.service.model.TokenServicePrice import org.p2p.wallet.home.db.TokenDao import org.p2p.wallet.home.model.TokenComparator @@ -57,7 +57,7 @@ class UserTokensDatabaseRepository( private fun calculateUserBalance(tokens: List): BigDecimal = tokens.mapNotNull(Token.Active::totalInUsd) .fold(BigDecimal.ZERO, BigDecimal::add) - .scaleShort() + .scaleTwo() override suspend fun getUserTokens(): List { return tokensDao.getTokens() diff --git a/app/src/test/java/org/p2p/wallet/striga/offramp/ui/StrigaOffRampPresenterCalculationTest.kt b/app/src/test/java/org/p2p/wallet/striga/offramp/ui/StrigaOffRampPresenterCalculationTest.kt index c16eeccb04..2e3a208cb2 100644 --- a/app/src/test/java/org/p2p/wallet/striga/offramp/ui/StrigaOffRampPresenterCalculationTest.kt +++ b/app/src/test/java/org/p2p/wallet/striga/offramp/ui/StrigaOffRampPresenterCalculationTest.kt @@ -1,14 +1,15 @@ package org.p2p.wallet.striga.offramp.ui import io.mockk.verify +import org.junit.Ignore import org.junit.Test import java.math.BigDecimal import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.p2p.core.utils.divideSafe import org.p2p.wallet.striga.offramp.models.StrigaOffRampButtonState import org.p2p.wallet.striga.offramp.models.StrigaOffRampTokenType -import org.p2p.core.utils.divideSafe @OptIn(ExperimentalCoroutinesApi::class) class StrigaOffRampPresenterCalculationTest : StrigaOffRampPresenterBaseTest() { @@ -56,6 +57,7 @@ class StrigaOffRampPresenterCalculationTest : StrigaOffRampPresenterBaseTest() { } @Test + @Ignore("Striga is not used") fun `GIVEN striga off ramp WHEN clicked all amount THEN check states for A and B tokens`() = runTest { val amountA = BigDecimal("4000.21") val amountB = amountA * exchangeRate.sellRate diff --git a/core/src/main/java/org/p2p/core/token/Token.kt b/core/src/main/java/org/p2p/core/token/Token.kt index 67ec130c2e..a1e44f5c2c 100644 --- a/core/src/main/java/org/p2p/core/token/Token.kt +++ b/core/src/main/java/org/p2p/core/token/Token.kt @@ -8,10 +8,9 @@ import kotlinx.parcelize.Parcelize import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.toBase58Instance import org.p2p.core.utils.Constants -import org.p2p.core.utils.Constants.REN_BTC_SYMBOL import org.p2p.core.utils.Constants.SOL_NAME -import org.p2p.core.utils.Constants.USDC_SYMBOL -import org.p2p.core.utils.Constants.USDT_SYMBOL +import org.p2p.core.utils.Constants.USDC_MINT +import org.p2p.core.utils.Constants.USDT_MINT import org.p2p.core.utils.Constants.WRAPPED_ETH_MINT import org.p2p.core.utils.Constants.WRAPPED_SOL_MINT import org.p2p.core.utils.asCurrency @@ -20,8 +19,8 @@ import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken import org.p2p.core.utils.isZero import org.p2p.core.utils.orZero -import org.p2p.core.utils.scaleLong -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleNine +import org.p2p.core.utils.scaleTwo import org.p2p.core.utils.toLamports import org.p2p.core.utils.toPowerValue import org.p2p.core.wrapper.eth.EthAddress @@ -78,7 +77,7 @@ sealed class Token constructor( @IgnoredOnParcel val totalInUsdScaled: BigDecimal? - get() = totalInUsd?.scaleShort() + get() = totalInUsd?.scaleTwo() @IgnoredOnParcel val isZero: Boolean @@ -202,35 +201,31 @@ sealed class Token constructor( @IgnoredOnParcel val isSOL: Boolean - get() = mintAddress == WRAPPED_SOL_MINT + get() = mintAddress.equals(WRAPPED_SOL_MINT, ignoreCase = true) @IgnoredOnParcel val isSpl: Boolean - get() = mintAddress != WRAPPED_SOL_MINT + get() = !isSOL @IgnoredOnParcel val isToken2022: Boolean get() = programId == Constants.SOLANA_TOKEN_2022_PROGRAM_ID - @IgnoredOnParcel - val isRenBTC: Boolean - get() = tokenSymbol == REN_BTC_SYMBOL - @IgnoredOnParcel val isUSDC: Boolean - get() = tokenSymbol.equals(USDC_SYMBOL, ignoreCase = true) + get() = mintAddress.equals(USDC_MINT, ignoreCase = true) @IgnoredOnParcel val isUSDT: Boolean - get() = tokenSymbol == USDT_SYMBOL + get() = mintAddress.equals(USDT_MINT, ignoreCase = true) @IgnoredOnParcel val usdRateOrZero: BigDecimal - get() = rate ?: BigDecimal.ZERO + get() = rate.orZero() @IgnoredOnParcel val currencyFormattedRate: String - get() = (rate ?: BigDecimal.ZERO).asCurrency(currencySymbol) + get() = usdRateOrZero.asCurrency(currencySymbol) @IgnoredOnParcel val currencySymbol: String @@ -264,7 +259,7 @@ sealed class Token constructor( tokenName = SOL_NAME, iconUrl = tokenMetadata.iconUrl, totalInUsd = if (amount == 0L) null else solPrice?.let { total.multiply(it) }, - total = total.scaleLong(tokenMetadata.decimals), + total = total.scaleNine(tokenMetadata.decimals), rate = solPrice, tokenServiceAddress = Constants.TOKEN_SERVICE_NATIVE_SOL_TOKEN, visibility = TokenVisibility.DEFAULT, @@ -289,9 +284,9 @@ fun List.findByMintAddress(mintAddress: String?): Token.Active? = fun List.sortedWithPreferredStableCoins(): List { return sortedWith( compareBy { - when (it.tokenSymbol) { - USDC_SYMBOL -> 1 - USDT_SYMBOL -> 2 + when (it.mintAddress) { + USDC_MINT -> 1 + USDT_MINT -> 2 else -> 3 } } diff --git a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt index 209f0a4340..4c3fe4d80a 100644 --- a/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt +++ b/core/src/main/java/org/p2p/core/utils/AmountExtensions.kt @@ -11,46 +11,45 @@ const val SOL_DECIMALS = 9 const val MOONPAY_DECIMAL = 2 const val STRIGA_FIAT_DECIMALS = 2 -private const val SCALE_VALUE_SHORT = 2 -private const val SCALE_VALUE_MEDIUM = 6 -private const val SCALE_VALUE_LONG = 9 +private const val SCALE_VALUE_TWO = 2 +private const val SCALE_VALUE_SIX = 6 +private const val SCALE_VALUE_NINE = 9 private const val AMOUNT_MIN_VALUE = 0.01 fun String?.toBigDecimalOrZero(): BigDecimal { val removedZeros = this?.replace("(?<=\\d)\\.?0+(?![\\d\\.])", emptyString()) - return removedZeros?.toBigDecimalOrNull() ?: BigDecimal.ZERO + return removedZeros?.toBigDecimalOrNull().orZero() } fun String?.toBigIntegerOrZero(): BigInteger { - return this?.toBigIntegerOrNull() ?: BigInteger.ZERO + return this?.toBigIntegerOrNull().orZero() } fun Int.toPowerValue(): BigDecimal = BASE_TEN.pow(this) -fun BigDecimal.scaleShortOrFirstNotZero(): BigDecimal { - return if (isZero()) { - this +fun BigDecimal.scaleTwoOrFirstNotZero(): BigDecimal { + if (isZero()) { + return this + } + val scale = if (scale() > SCALE_VALUE_TWO) { + scale() - (unscaledValue().toString().length - SCALE_VALUE_TWO) } else { - val scale = if (scale() > SCALE_VALUE_SHORT) { - scale() - (unscaledValue().toString().length - SCALE_VALUE_SHORT) - } else { - SCALE_VALUE_SHORT - } - // removing zeros, case: 0.02000 -> 0.2 - setScale(scale, RoundingMode.DOWN).stripTrailingZeros() + SCALE_VALUE_TWO } + // removing zeros, case: 0.02000 -> 0.2 + return setScale(scale, RoundingMode.DOWN).stripTrailingZeros() } -fun BigDecimal.scaleShort(): BigDecimal = - this.setScale(SCALE_VALUE_SHORT, RoundingMode.DOWN) +fun BigDecimal.scaleTwo(): BigDecimal = + this.setScale(SCALE_VALUE_TWO, RoundingMode.DOWN) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 -fun BigDecimal.scaleMedium(): BigDecimal = - if (this.isZero()) this else this.setScale(SCALE_VALUE_MEDIUM, RoundingMode.DOWN) +fun BigDecimal.scaleSix(): BigDecimal = + if (this.isZero()) this else this.setScale(SCALE_VALUE_SIX, RoundingMode.DOWN) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 -fun BigDecimal.scaleLong(decimals: Int = SCALE_VALUE_LONG): BigDecimal = +fun BigDecimal.scaleNine(decimals: Int = SCALE_VALUE_NINE): BigDecimal = if (this.isZero()) this else this.setScale(decimals, RoundingMode.DOWN) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 @@ -59,16 +58,16 @@ fun BigDecimal.scaleLong(decimals: Int = SCALE_VALUE_LONG): BigDecimal = fun BigInteger.fromLamports(decimals: Int): BigDecimal = (this.toBigDecimal().divide(BASE_TEN.pow(decimals), 18, RoundingMode.HALF_DOWN)) .stripTrailingZeros() // removing zeros, case: 0.02000 -> 0.02 - .scaleLong(decimals) + .scaleNine(decimals) fun BigDecimal.toLamports(decimals: Int): BigInteger = this.multiply(decimals.toPowerValue()).toBigInteger() fun BigDecimal.toUsd(usdRate: BigDecimal?): BigDecimal? = - usdRate?.let { this.multiply(it).scaleShort() } + usdRate?.let { this.multiply(it).scaleTwo() } fun BigDecimal.toUsd(token: Token): BigDecimal? = - token.rate?.let { this.multiply(it).scaleShort() } + token.rate?.let { this.multiply(it).scaleTwo() } // case: 1000.023000 -> 1 000.02 fun BigDecimal.formatFiat(): String = formatWithDecimals(FIAT_FRACTION_LENGTH) diff --git a/core/src/main/java/org/p2p/core/utils/Constants.kt b/core/src/main/java/org/p2p/core/utils/Constants.kt index f18d321f79..5010d1c355 100644 --- a/core/src/main/java/org/p2p/core/utils/Constants.kt +++ b/core/src/main/java/org/p2p/core/utils/Constants.kt @@ -1,5 +1,7 @@ package org.p2p.core.utils +import org.p2p.core.crypto.toBase58Instance + object Constants { const val MIN_REQUIRED_ACCOUNT_INFO_DATA_LENGTH = 0 @@ -26,6 +28,7 @@ object Constants { const val TOKEN_SERVICE_NATIVE_ETH_TOKEN = "native" const val WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112" + val WRAPPED_SOL_MINT_B58 = WRAPPED_SOL_MINT.toBase58Instance() const val WRAPPED_ETH_MINT = "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs" const val WRAPPED_BTC_MINT = "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh" diff --git a/core/src/main/java/org/p2p/core/utils/DecimalFormatter.kt b/core/src/main/java/org/p2p/core/utils/DecimalFormatter.kt index 3885e48461..64509284ae 100644 --- a/core/src/main/java/org/p2p/core/utils/DecimalFormatter.kt +++ b/core/src/main/java/org/p2p/core/utils/DecimalFormatter.kt @@ -48,6 +48,8 @@ object DecimalFormatter { fmt.minimumFractionDigits = if (keepInitialDecimals) clamp(value.scale(), 0, decimals) else 0 } fmt.decimalFormatSymbols = symbols + // HALF_DOWN to we won't round, for example, 1.505 to 1.51 which is not valid + fmt.roundingMode = RoundingMode.DOWN return fmt.format(value) } } diff --git a/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt b/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt index 20450135fb..b4830a4b4a 100644 --- a/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt +++ b/token-service/src/main/java/org/p2p/token/service/model/TokenServicePrice.kt @@ -1,7 +1,7 @@ package org.p2p.token.service.model import java.math.BigDecimal -import org.p2p.core.utils.scaleShort +import org.p2p.core.utils.scaleTwo data class TokenServicePrice( val tokenAddress: String, @@ -9,7 +9,7 @@ data class TokenServicePrice( val network: TokenServiceNetwork ) { val scaledUsdRate: BigDecimal? - get() = rate.usd?.scaleShort() + get() = rate.usd?.scaleTwo() val usdRate: BigDecimal? get() = rate.usd From 68b57ae87356b5312f524ae1a490b475e52ccf5e Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:20:48 +0300 Subject: [PATCH 45/51] PWN-843 - reconnect socket on connection true (#2215) --- .../home/ui/container/MainContainerPresenter.kt | 11 ++++++++++- .../org/p2p/wallet/updates/SocketUpdatesManager.kt | 10 ++++++---- .../java/org/p2p/solanaj/ws/SocketStateListener.kt | 4 +--- .../solanaj/ws/impl/SubscriptionWebSocketClient.kt | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt index a36bcefd8f..5ca20e7f85 100644 --- a/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/home/ui/container/MainContainerPresenter.kt @@ -24,6 +24,7 @@ import org.p2p.wallet.jupiter.ui.main.JupiterSwapFragment import org.p2p.wallet.settings.ui.settings.SettingsFragment import org.p2p.wallet.tokenservice.TokenServiceCoordinator import org.p2p.wallet.tokenservice.UserTokensState +import org.p2p.wallet.updates.SocketUpdatesManager class MainContainerPresenter( private val deeplinksManager: AppDeeplinksManager, @@ -31,6 +32,7 @@ class MainContainerPresenter( private val tokenServiceCoordinator: TokenServiceCoordinator, private val metadataInteractor: MetadataInteractor, private val walletStrigaInteractor: WalletStrigaInteractor, + private val socketUpdatesManager: SocketUpdatesManager, private val balanceMapper: WalletBalanceMapper, private val mainScreenAnalytics: MainScreenAnalytics, ) : BasePresenter(), MainContainerContract.Presenter { @@ -102,7 +104,14 @@ class MainContainerPresenter( private fun observeInternetState() { connectionManager.connectionStatus .onEach { isConnected -> - if (!isConnected) view?.showUiKitSnackBar(messageResId = R.string.error_no_internet_message) + if (!isConnected) { + view?.showUiKitSnackBar(messageResId = R.string.error_no_internet_message) + socketUpdatesManager.stop() + } else { + if (!socketUpdatesManager.isStarted()) { + socketUpdatesManager.restart() + } + } } .launchIn(this) } diff --git a/app/src/main/java/org/p2p/wallet/updates/SocketUpdatesManager.kt b/app/src/main/java/org/p2p/wallet/updates/SocketUpdatesManager.kt index 860a5c1db9..fbbec05310 100644 --- a/app/src/main/java/org/p2p/wallet/updates/SocketUpdatesManager.kt +++ b/app/src/main/java/org/p2p/wallet/updates/SocketUpdatesManager.kt @@ -17,7 +17,10 @@ import kotlinx.coroutines.plus import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.p2p.core.BuildConfig.rpcPoolApiKey +import org.p2p.core.common.di.AppScope import org.p2p.core.network.ConnectionManager +import org.p2p.core.network.environment.NetworkEnvironment +import org.p2p.core.network.environment.NetworkEnvironmentManager import org.p2p.solanaj.model.types.RpcMapRequest import org.p2p.solanaj.model.types.RpcRequest import org.p2p.solanaj.ws.SocketClientCreateResult @@ -26,10 +29,7 @@ import org.p2p.solanaj.ws.SubscriptionEventListener import org.p2p.solanaj.ws.SubscriptionSocketClient import org.p2p.solanaj.ws.SubscriptionSocketClientFactory import org.p2p.solanaj.ws.impl.SocketClientException -import org.p2p.core.common.di.AppScope import org.p2p.wallet.common.feature_toggles.toggles.remote.SocketSubscriptionsFeatureToggle -import org.p2p.core.network.environment.NetworkEnvironment -import org.p2p.core.network.environment.NetworkEnvironmentManager private const val DELAY_MS = 5000L @@ -149,8 +149,10 @@ class SocketUpdatesManager( } } - override fun onClosed(code: Int, message: String) { + override fun onClientClosed(code: Int, message: String) { Timber.tag(TAG).i("Event updates connection is closed: $message") + isStarted = false + state = SocketState.DISCONNECTED } private suspend fun startActual() { diff --git a/solana/src/main/java/org/p2p/solanaj/ws/SocketStateListener.kt b/solana/src/main/java/org/p2p/solanaj/ws/SocketStateListener.kt index 4a679f90fe..fb31ff5357 100644 --- a/solana/src/main/java/org/p2p/solanaj/ws/SocketStateListener.kt +++ b/solana/src/main/java/org/p2p/solanaj/ws/SocketStateListener.kt @@ -1,10 +1,8 @@ package org.p2p.solanaj.ws -import java.lang.Exception - interface SocketStateListener { fun onConnected() fun onWebSocketPong() fun onFailed(exception: Exception) - fun onClosed(code: Int, message: String) + fun onClientClosed(code: Int, message: String) } diff --git a/solana/src/main/java/org/p2p/solanaj/ws/impl/SubscriptionWebSocketClient.kt b/solana/src/main/java/org/p2p/solanaj/ws/impl/SubscriptionWebSocketClient.kt index 2d4c51d37f..502e39c5a6 100644 --- a/solana/src/main/java/org/p2p/solanaj/ws/impl/SubscriptionWebSocketClient.kt +++ b/solana/src/main/java/org/p2p/solanaj/ws/impl/SubscriptionWebSocketClient.kt @@ -130,7 +130,7 @@ internal class SubscriptionWebSocketClient internal constructor( override fun onClose(code: Int, reason: String, remote: Boolean) { val closedFrom = if (remote) "remote peer" else "us" - stateListener.onClosed( + stateListener.onClientClosed( code = code, message = "Connection closed by $closedFrom Code: $code Reason: $reason" ) From 089f6e3727fc18a32fc3098800e34d63d7c28aed Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:38:01 +0400 Subject: [PATCH 46/51] ETH-1058 - fixed navigation infinite recursion (#2217) --- .../p2p/wallet/utils/NavigationExtensions.kt | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt b/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt index 0911c74999..6a31097b33 100644 --- a/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt +++ b/app/src/main/java/org/p2p/wallet/utils/NavigationExtensions.kt @@ -82,18 +82,32 @@ fun Fragment.popAndReplaceFragment( @AnimRes popEnter: Int = R.anim.nav_pop_enter, @AnimRes popExit: Int = R.anim.nav_pop_exit, fragmentManager: FragmentManager = requireActivity().supportFragmentManager -) = requireActivity().popAndReplaceFragment( - target = target, - popTo = popTo, - containerId = containerId, - addToBackStack = addToBackStack, - inclusive = inclusive, - enter = enter, - exit = exit, - popEnter = popEnter, - popExit = popExit, - fragmentManager = fragmentManager -) +) = whenStateAtLeast(Lifecycle.State.STARTED) { + requireActivity().hideKeyboard() + with(fragmentManager) { + // Override exit animation for popping fragment + if (this@popAndReplaceFragment is BaseFragmentContract) { + this@popAndReplaceFragment.overrideExitAnimation(exit) + } + + // Make pop entering fragment invisible during transition + popTo?.java?.name + ?.let { findFragmentByTag(it) as? BaseFragmentContract } + ?.apply { overrideEnterAnimation(R.anim.nav_stay_transparent) } + + popBackStack( + popTo?.java?.name, + if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0 + ) + + commit(allowStateLoss = true) { + // Preform immediate replace + setCustomAnimations(enter, 0, popEnter, popExit) + replace(containerId, target, target.javaClass.name) + if (addToBackStack) addToBackStack(target.javaClass.name) + } + } +} fun FragmentActivity.popAndReplaceFragment( target: Fragment, From d1db109bc674e977dc25e153369790e5ad7f0d8d Mon Sep 17 00:00:00 2001 From: Gleb Levinkov Date: Mon, 26 Feb 2024 17:37:16 +0300 Subject: [PATCH 47/51] bump app build version --- buildSrc/src/main/java/AppVersions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AppVersions.kt b/buildSrc/src/main/java/AppVersions.kt index f71eee89c3..4f710688eb 100644 --- a/buildSrc/src/main/java/AppVersions.kt +++ b/buildSrc/src/main/java/AppVersions.kt @@ -3,5 +3,5 @@ object AppVersions { const val VERSION_MINOR = 12 const val VERSION_HOTFIX = 0 const val VERSION_PATCH = 0 - const val VERSION_BUILD = 3582 + const val VERSION_BUILD = 3585 } From 689ce97a10aa86909878925f18a42ba7ab8d47ce Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:06:11 +0400 Subject: [PATCH 48/51] ETH-1059 - send total fix (#2214) --- .../p2p/wallet/send/SendFeeRelayerManager.kt | 12 ++------ .../p2p/wallet/send/model/CalculationMode.kt | 8 ++++++ .../p2p/wallet/send/model/FeeRelayerState.kt | 2 -- .../org/p2p/wallet/send/model/SendFeeTotal.kt | 28 ++++++------------- .../p2p/wallet/send/ui/NewSendPresenter.kt | 25 +++++++++-------- 5 files changed, 31 insertions(+), 44 deletions(-) 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 aaebf98f4a..fcfd59e899 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -18,7 +18,6 @@ import org.p2p.solanaj.rpc.RpcSolanaRepository import org.p2p.wallet.feerelayer.model.FeeCalculationState import org.p2p.wallet.feerelayer.model.FeePayerSelectionStrategy import org.p2p.wallet.feerelayer.model.FeeRelayerFee -import org.p2p.wallet.feerelayer.model.TransactionFeeLimits import org.p2p.wallet.send.interactor.SendInteractor import org.p2p.wallet.send.interactor.usecase.CalculateSendFeesUseCase import org.p2p.wallet.send.interactor.usecase.CalculateToken2022TransferFeeUseCase @@ -59,7 +58,6 @@ class SendFeeRelayerManager( onStateUpdated?.invoke(newState) } - private lateinit var feeLimitInfo: TransactionFeeLimits private lateinit var recipientAddress: SearchResult private lateinit var solToken: Token.Active private var initializeCompleted = false @@ -103,7 +101,6 @@ class SendFeeRelayerManager( 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(feePayerToken) } @@ -131,6 +128,8 @@ class SendFeeRelayerManager( return SendFeeTotal( currentAmount = currentAmount, + tokenTotalAmount = sourceToken.total, + isMaxButtonUsed = calculationMode.isMaxUsed(), currentAmountUsd = calculationMode.getCurrentAmountUsd(), receiveFormatted = currentAmount.formatTokenWithSymbol( tokenSymbol = sourceToken.tokenSymbol, @@ -140,7 +139,6 @@ class SendFeeRelayerManager( sourceSymbol = sourceToken.tokenSymbol, sendFee = (currentState as? UpdateFee)?.solanaFee, recipientAddress = recipientAddress.address, - feeLimit = feeLimitInfo, transferFeePercent = transferFeePercent, interestBearingPercent = interestBearingPercent ) @@ -180,7 +178,6 @@ class SendFeeRelayerManager( is FeeCalculationState.NoFees -> { currentState = UpdateFee( solanaFee = null, - feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions ) sendInteractor.setFeePayerToken(feePayer) @@ -195,7 +192,6 @@ class SendFeeRelayerManager( ) currentState = UpdateFee( solanaFee = solanaFee, - feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions, ) sendInteractor.setFeePayerToken(solToken) @@ -315,7 +311,6 @@ class SendFeeRelayerManager( validateFunds(sourceToken, fee, inputAmount) currentState = UpdateFee( solanaFee = fee, - feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions, ) } else { @@ -443,7 +438,6 @@ class SendFeeRelayerManager( is FeeCalculationState.NoFees -> { currentState = UpdateFee( solanaFee = null, - feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions ) } @@ -457,7 +451,6 @@ class SendFeeRelayerManager( ) currentState = UpdateFee( solanaFee = solanaFee, - feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions ) sendInteractor.setFeePayerToken(solToken) @@ -473,7 +466,6 @@ class SendFeeRelayerManager( validateFunds(sourceToken, fee, inputAmount) currentState = UpdateFee( solanaFee = fee, - feeLimitInfo = feeLimitInfo, tokenExtensions = tokenExtensions ) } 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 a9980b0a5e..666fbd0b03 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 @@ -42,6 +42,8 @@ class CalculationMode( var inputAmountDecimal: BigDecimal = BigDecimal.ZERO private set + private var useMax = false + val formatInputAmount: String get() = when (currencyMode) { is CurrencyMode.Fiat -> inputAmountDecimal.formatFiat() @@ -270,4 +272,10 @@ class CalculationMode( currencyMode = newMode return inputAmount } + + fun setUseMax(value: Boolean) { + useMax = value + } + + fun isMaxUsed(): Boolean = useMax } diff --git a/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt b/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt index 5af9a8bdbe..82409b5fcd 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/FeeRelayerState.kt @@ -2,7 +2,6 @@ package org.p2p.wallet.send.model import java.math.BigInteger import org.p2p.solanaj.kits.TokenExtensionsMap -import org.p2p.wallet.feerelayer.model.TransactionFeeLimits sealed interface FeeRelayerState { object Idle : FeeRelayerState @@ -13,7 +12,6 @@ sealed interface FeeRelayerState { data class UpdateFee( val solanaFee: SendSolanaFee?, - val feeLimitInfo: TransactionFeeLimits, val tokenExtensions: TokenExtensionsMap ) : FeeRelayerState { val hasToken2022Fee: Boolean diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt b/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt index 26b0cdf7d7..c1805cd294 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/SendFeeTotal.kt @@ -3,7 +3,6 @@ package org.p2p.wallet.send.model import androidx.annotation.ColorInt import android.os.Parcelable import java.math.BigDecimal -import java.math.BigInteger import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.p2p.core.model.TextHighlighting @@ -13,9 +12,6 @@ import org.p2p.core.utils.formatTokenWithSymbol import org.p2p.core.utils.orZero import org.p2p.uikit.utils.SpanUtils import org.p2p.wallet.R -import org.p2p.wallet.feerelayer.model.Limits -import org.p2p.wallet.feerelayer.model.ProcessedFee -import org.p2p.wallet.feerelayer.model.TransactionFeeLimits /** * [SendSolanaFee] can be null only if total fees is Zero. (transaction fee and account creation fee) @@ -28,11 +24,12 @@ import org.p2p.wallet.feerelayer.model.TransactionFeeLimits @Parcelize class SendFeeTotal constructor( val currentAmount: BigDecimal, + val tokenTotalAmount: BigDecimal, + val isMaxButtonUsed: Boolean, val currentAmountUsd: BigDecimal?, val receiveFormatted: String, val receiveUsd: BigDecimal?, val sendFee: SendSolanaFee?, - val feeLimit: TransactionFeeLimits, val sourceSymbol: String, val recipientAddress: String, val transferFeePercent: BigDecimal? = null, @@ -42,25 +39,12 @@ class SendFeeTotal constructor( companion object { fun createEmpty(): SendFeeTotal = SendFeeTotal( currentAmount = BigDecimal.ZERO, + tokenTotalAmount = BigDecimal.ZERO, + isMaxButtonUsed = false, currentAmountUsd = null, receiveFormatted = "", receiveUsd = null, sendFee = null, - feeLimit = TransactionFeeLimits( - limits = Limits( - maxFeeCountAllowed = 0, - maxFeeAmountAllowed = BigInteger.ZERO, - maxAccountCreationCountAllowed = 0, - maxAccountCreationAmountAllowed = BigInteger.ZERO - ), - processedFee = ProcessedFee( - totalFeeAmountUsed = BigInteger.ZERO, - totalRentAmountUsed = BigInteger.ZERO, - totalFeeCountUsed = 0, - totalRentCountUsed = 0, - totalAmountUsed = BigInteger.ZERO - ) - ), sourceSymbol = "", recipientAddress = "" ) @@ -147,6 +131,10 @@ class SendFeeTotal constructor( val totalSumWithSymbol: String get() { + if (isMaxButtonUsed) { + return tokenTotalAmount.formatTokenWithSymbol(sourceSymbol, SOL_DECIMALS) + } + val transferFee = transferFeePercent ?.multiply("0.01".toBigDecimal()) ?.multiply(currentAmount) 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 1efbf65bfb..5ef2922f3b 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 @@ -178,12 +178,18 @@ class NewSendPresenter( val currentState = sendFeeRelayerManager.getState() handleFeeRelayerStateUpdate(currentState, view) + calculateMaxAvailableAmount(token) + } + + private suspend fun calculateMaxAvailableAmount(token: Token.Active) { maximumAmountCalculator.getMaxAvailableAmountToSend( token = token, recipient = recipientAddress.address.toBase58Instance() ) // set max available amount - ?.also { calculationMode.setMaxAmounts(it) } + ?.also { + calculationMode.setMaxAmounts(it) + } } private suspend fun setupInitialToken(view: NewSendContract.View) { @@ -223,11 +229,6 @@ class NewSendPresenter( Timber.tag(TAG).e(SendFatalError("Couldn't find user's SOL account!")) return } - maximumAmountCalculator.getMaxAvailableAmountToSend( - token = initialToken, - recipient = recipientAddress.address.toBase58Instance() - ) - ?.also { calculationMode.setMaxAmounts(it) } initializeFeeRelayer( view = view, @@ -235,6 +236,7 @@ class NewSendPresenter( feePayerToken = feePayerToken, solToken = solToken ) + calculateMaxAvailableAmount(initialToken) initialAmount?.let(::setupDefaultFields) } @@ -375,12 +377,8 @@ class NewSendPresenter( launch { newSendAnalytics.logTokenChanged(newToken.tokenSymbol, flow) token = newToken - maximumAmountCalculator.getMaxAvailableAmountToSend( - token = newToken, - recipient = recipientAddress.address.toBase58Instance() - ) - // set max available amount - ?.also { calculationMode.setMaxAmounts(it) } + calculationMode.setUseMax(false) + calculateMaxAvailableAmount(newToken) checkTokenRatesAndSetSwitchAmountState(newToken) @@ -431,6 +429,7 @@ class NewSendPresenter( override fun updateInputAmount(amount: String) { newSendAnalytics.logTokenAmountChanged(token?.tokenSymbol.orEmpty(), amount, flow) + calculationMode.setUseMax(false) calculationMode.updateInputAmount(amount) view?.showFeeViewVisible(isVisible = true) showMaxButtonIfNeeded() @@ -449,6 +448,7 @@ class NewSendPresenter( override fun updateFeePayerToken(feePayerToken: Token.Active) { try { + calculationMode.setUseMax(false) launch { sendInteractor.saveFeePayerToken(feePayerToken) } @@ -468,6 +468,7 @@ class NewSendPresenter( Timber.tag(TAG).e(SendFatalError("Token can't be null")) return@launch } + calculationMode.setUseMax(true) val totalAvailable = maximumAmountCalculator.getMaxAvailableAmountToSend( token = token, recipient = recipientAddress.address.toBase58Instance() From d30236317ce3f02521d6999eddf62b63c29593aa Mon Sep 17 00:00:00 2001 From: Eduard Maximovich <130352737+eduardmaximovich@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:13:48 +0400 Subject: [PATCH 49/51] ETH-1068 - send fees fix (#2216) --- .../p2p/wallet/send/SendFeeRelayerManager.kt | 30 ++- .../org/p2p/wallet/send/api/SendServiceApi.kt | 13 + .../send/interactor/FeePayersRepository.kt | 90 ++++++- .../wallet/send/interactor/SendInteractor.kt | 6 + .../usecase/CalculateSendFeesUseCase.kt | 233 +++++++----------- .../p2p/wallet/send/model/SendServiceFees.kt | 14 ++ .../send_service/GeneratedTransaction.kt | 24 +- .../repository/SendServiceRemoteRepository.kt | 84 +++++-- .../send/repository/SendServiceRepository.kt | 24 +- .../org/p2p/wallet/send/ui/NewSendContract.kt | 7 +- .../org/p2p/wallet/send/ui/NewSendFragment.kt | 9 +- .../p2p/wallet/send/ui/NewSendPresenter.kt | 7 +- .../ui/details/NewSendDetailsBottomSheet.kt | 14 +- .../send/ui/details/NewSendDetailsContract.kt | 7 +- .../ui/details/NewSendDetailsPresenter.kt | 10 +- 15 files changed, 384 insertions(+), 188 deletions(-) create mode 100644 app/src/main/java/org/p2p/wallet/send/model/SendServiceFees.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 fcfd59e899..c1fc7173ca 100644 --- a/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt +++ b/app/src/main/java/org/p2p/wallet/send/SendFeeRelayerManager.kt @@ -153,6 +153,7 @@ class SendFeeRelayerManager( feePayerToken: Token.Active?, strategy: FeePayerSelectionStrategy, tokenAmount: BigDecimal, + useMax: Boolean, useCache: Boolean ) { val feePayer = feePayerToken ?: sendInteractor.getFeePayerToken() @@ -170,6 +171,7 @@ class SendFeeRelayerManager( val feeState = calculateFees( sourceToken = sourceToken, feePayerToken = feePayer, + sourceTokenAmount = tokenAmount.toLamports(sourceToken.decimals), result = recipientAddress, useCache = useCache ) @@ -186,6 +188,8 @@ class SendFeeRelayerManager( val solanaFee = buildSolanaFee( newFeePayer = solToken, source = sourceToken, + sourceTokenAmount = tokenAmount.toLamports(sourceToken.decimals), + useMax = useMax, feeRelayerFee = feeState.feeInSol, token2022TransferFee = token2022TransferFee, token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent @@ -204,6 +208,7 @@ class SendFeeRelayerManager( feeRelayerFee = feeState.fee, feePayerToken = feePayer, inputAmount = inputAmount, + useMax = useMax, strategy = strategy, token2022TransferFee = token2022TransferFee, tokenExtensions = tokenExtensions @@ -282,11 +287,13 @@ class SendFeeRelayerManager( private suspend fun calculateFees( sourceToken: Token.Active, feePayerToken: Token.Active, + sourceTokenAmount: BigInteger, result: SearchResult, @Suppress("UNUSED_PARAMETER") useCache: Boolean = true ): FeeCalculationState = calculateSendFeesUseCase.execute( sourceToken = sourceToken, feePayerToken = feePayerToken, + sourceTokenAmount = sourceTokenAmount, searchResult = result ) @@ -296,12 +303,15 @@ class SendFeeRelayerManager( feePayerToken: Token.Active, token2022TransferFee: BigInteger, inputAmount: BigInteger, + useMax: Boolean, strategy: FeePayerSelectionStrategy, tokenExtensions: TokenExtensionsMap, ) { val fee = buildSolanaFee( newFeePayer = feePayerToken, source = sourceToken, + sourceTokenAmount = inputAmount, + useMax = useMax, feeRelayerFee = feeRelayerFee, token2022TransferFee = token2022TransferFee, token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent @@ -318,6 +328,7 @@ class SendFeeRelayerManager( sourceToken = sourceToken, fee = fee, inputAmount = inputAmount, + useMax = useMax, strategy = strategy ) } @@ -338,6 +349,8 @@ class SendFeeRelayerManager( private suspend fun buildSolanaFee( newFeePayer: Token.Active, source: Token.Active, + sourceTokenAmount: BigInteger, + useMax: Boolean, feeRelayerFee: FeeRelayerFee, token2022TransferFee: BigInteger, token2022TransferFeePercent: BigDecimal, @@ -347,6 +360,9 @@ class SendFeeRelayerManager( if (alternativeTokens == null) { alternativeTokens = sendInteractor.findAlternativeFeePayerTokens( userTokens = userInteractor.getNonZeroUserTokens(), + sourceToken = source, + sourceTokenAmount = sourceTokenAmount, + useMax = useMax, feePayerToExclude = newFeePayer, transactionFeeInSOL = feeRelayerFee.transactionFeeInSol, accountCreationFeeInSOL = feeRelayerFee.accountCreationFeeInSol @@ -368,6 +384,7 @@ class SendFeeRelayerManager( sourceToken: Token.Active, fee: SendSolanaFee, inputAmount: BigInteger, + useMax: Boolean, strategy: FeePayerSelectionStrategy ) { @@ -408,10 +425,14 @@ class SendFeeRelayerManager( } } - recalculate(sourceToken, amountToCheck) + recalculate(sourceToken, amountToCheck, useMax) } - private suspend fun recalculate(sourceToken: Token.Active, inputAmount: BigInteger) { + private suspend fun recalculate( + sourceToken: Token.Active, + inputAmount: BigInteger, + useMax: Boolean, + ) { /* * Optimized recalculation and UI update * */ @@ -420,6 +441,7 @@ class SendFeeRelayerManager( calculateFees( sourceToken = sourceToken, feePayerToken = newFeePayer, + sourceTokenAmount = inputAmount, result = recipientAddress ) } catch (e: CancellationException) { @@ -445,6 +467,8 @@ class SendFeeRelayerManager( val solanaFee = buildSolanaFee( newFeePayer = solToken, source = sourceToken, + sourceTokenAmount = inputAmount, + useMax = useMax, feeRelayerFee = feeState.feeInSol, token2022TransferFee = token2022TransferFee, token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent @@ -459,6 +483,8 @@ class SendFeeRelayerManager( val fee = buildSolanaFee( newFeePayer = newFeePayer, source = sourceToken, + sourceTokenAmount = inputAmount, + useMax = useMax, feeRelayerFee = feeState.fee, token2022TransferFee = token2022TransferFee, token2022TransferFeePercent = tokenExtensions.actualTransferFeePercent diff --git a/app/src/main/java/org/p2p/wallet/send/api/SendServiceApi.kt b/app/src/main/java/org/p2p/wallet/send/api/SendServiceApi.kt index 60cff37755..5800d4e03d 100644 --- a/app/src/main/java/org/p2p/wallet/send/api/SendServiceApi.kt +++ b/app/src/main/java/org/p2p/wallet/send/api/SendServiceApi.kt @@ -1,5 +1,6 @@ package org.p2p.wallet.send.api +import com.google.gson.JsonElement import retrofit2.http.Body import retrofit2.http.POST import retrofit2.http.Url @@ -24,9 +25,21 @@ interface SendServiceApi { @Url url: String = emptyString() ): CommonResponse + @POST + suspend fun generateTransactionRaw( + @Body rpcRequest: RpcMapRequest, + @Url url: String = emptyString() + ): JsonElement + @POST suspend fun getTokenAmounts( @Body rpcRequest: RpcMapRequest, @Url url: String = emptyString() ): CommonResponse> + + @POST + suspend fun getTokenAccountRentExemption( + @Body rpcRequest: RpcMapRequest, + @Url url: String = emptyString() + ): CommonResponse> } diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt b/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt index 45b998c1bb..dfb23a29d9 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/FeePayersRepository.kt @@ -2,6 +2,9 @@ package org.p2p.wallet.send.interactor import timber.log.Timber import java.math.BigInteger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.toBase58Instance @@ -9,13 +12,16 @@ import org.p2p.core.dispatchers.CoroutineDispatchers import org.p2p.core.token.Token import org.p2p.core.token.findByMintAddress import org.p2p.core.utils.plusAssign +import org.p2p.core.wrapper.eth.helpers.RandomHelper.randomBytes +import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.send.interactor.usecase.GetFeesInPayingTokenUseCase import org.p2p.wallet.send.repository.SendServiceRepository class FeePayersRepository( private val getFeesInPayingTokenUseCase: GetFeesInPayingTokenUseCase, private val sendServiceRepository: SendServiceRepository, - private val dispatchers: CoroutineDispatchers + private val dispatchers: CoroutineDispatchers, + private val tokenKeyProvider: TokenKeyProvider, ) { /** * The request is too complex @@ -24,6 +30,9 @@ class FeePayersRepository( */ suspend fun findAlternativeFeePayerTokens( userTokens: List, + sourceToken: Token.Active, + sourceTokenAmount: BigInteger, + useMax: Boolean, feePayerToExclude: Token.Active, transactionFeeInSol: BigInteger, accountCreationFeeInSol: BigInteger @@ -35,19 +44,35 @@ class FeePayersRepository( this += "transactionFeeInSol = $transactionFeeInSol" } ) - val feePayerTokensMints = sendServiceRepository.getCompensationTokens() + val feePayerTokensMints = sendServiceRepository.getCompensationTokens().toMutableList() val tokenToExcludeSymbol = feePayerToExclude.tokenSymbol - val feesInGivenTokens: Map = getFeesInPayingTokenUseCase.execute( - findFeesIn = feePayerTokensMints, - transactionFeeInSol = transactionFeeInSol, - accountCreationFeeInSol = accountCreationFeeInSol + val feesInGivenTokens: MutableMap = + getFeesInPayingTokenUseCase.execute( + findFeesIn = feePayerTokensMints, + transactionFeeInSol = transactionFeeInSol, + accountCreationFeeInSol = accountCreationFeeInSol + ) + .toMutableMap() + + // token2022 token rent fees are also include doubled transfer fee + // so we need to take rent fee for each token separately + getTokens2022Fees( + scope = this, + userTokens = userTokens, + feePayerTokensMints = feePayerTokensMints.toSet(), + sourceToken = sourceToken, + sourceTokenAmount = sourceTokenAmount, + useMax = useMax, + accountCreationFeeInSol = accountCreationFeeInSol, + resultMapRef = feesInGivenTokens ) Timber.i( "Filtering user tokens for alternative fee payers: $feePayerTokensMints" ) - feePayerTokensMints.mapNotNull { userTokens.findByMintAddress(it.base58Value) } + val userTokensForFee = feePayerTokensMints.mapNotNull { userTokens.findByMintAddress(it.base58Value) } + userTokensForFee .filter { token -> if (token.tokenSymbol == tokenToExcludeSymbol) { Timber.i("Excluding ${token.mintAddress} ${token.tokenSymbol}") @@ -77,4 +102,55 @@ class FeePayersRepository( } .also { Timber.i("Found alternative feepayer tokens: ${it.map(Token.Active::mintAddress)}") } } + + private suspend fun getTokens2022Fees( + scope: CoroutineScope, + userTokens: List, + feePayerTokensMints: Set, + sourceToken: Token.Active, + sourceTokenAmount: BigInteger, + useMax: Boolean, + accountCreationFeeInSol: BigInteger, + resultMapRef: MutableMap + ) { + val amount = when { + useMax -> { + SendServiceRepository.UINT64_MAX + } + sourceToken.isSOL -> { + maxOf(BigInteger("2"), accountCreationFeeInSol) + } + else -> { + maxOf(BigInteger("2"), sourceTokenAmount) + } + } + userTokens + .filter { it.isToken2022 } + .filter { it.mintAddressB58 in feePayerTokensMints } + .map { + scope.async { + try { + it.mintAddressB58 to sendServiceRepository.estimateFees( + userWallet = tokenKeyProvider.publicKeyBase58, + recipient = randomBytes(32).toBase58Instance(), + sourceToken = sourceToken, + feePayerToken = it, + amount = amount + ) + } catch (e: Throwable) { + Timber.i(e, "Send service return error for token ${it.mintAddress}") + it.mintAddressB58 to null + } + } + } + .awaitAll() + .forEach { + if (it.second == null) { + // exclude token from selection if send-service error + resultMapRef.remove(it.first) + } else { + resultMapRef[it.first] = it.second!!.tokenAccountRent.amount.amount + } + } + } } diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt b/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt index bb1438d38f..10b53480cd 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/SendInteractor.kt @@ -455,12 +455,18 @@ class SendInteractor( suspend fun findAlternativeFeePayerTokens( userTokens: List, + sourceToken: Token.Active, + sourceTokenAmount: BigInteger, + useMax: Boolean, feePayerToExclude: Token.Active, transactionFeeInSOL: BigInteger, accountCreationFeeInSOL: BigInteger ): List { return feePayersRepository.findAlternativeFeePayerTokens( userTokens = userTokens, + sourceToken = sourceToken, + sourceTokenAmount = sourceTokenAmount, + useMax = useMax, feePayerToExclude = feePayerToExclude, transactionFeeInSol = transactionFeeInSOL, accountCreationFeeInSol = accountCreationFeeInSOL diff --git a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt index 77c1232d3f..bef614dbf0 100644 --- a/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt +++ b/app/src/main/java/org/p2p/wallet/send/interactor/usecase/CalculateSendFeesUseCase.kt @@ -6,74 +6,99 @@ import kotlin.coroutines.cancellation.CancellationException import org.p2p.core.crypto.toBase58Instance import org.p2p.core.token.Token import org.p2p.core.utils.Constants -import org.p2p.core.utils.isNotZero -import org.p2p.core.utils.isZero +import org.p2p.core.utils.isZeroOrLess import org.p2p.solanaj.core.FeeAmount -import org.p2p.solanaj.programs.TokenProgram +import org.p2p.token.service.converter.TokenServiceAmountsConverter import org.p2p.wallet.feerelayer.model.FeeCalculationState import org.p2p.wallet.feerelayer.model.FeeRelayerFee -import org.p2p.wallet.rpc.interactor.TransactionAddressInteractor -import org.p2p.wallet.rpc.repository.amount.RpcAmountRepository +import org.p2p.wallet.infrastructure.network.provider.TokenKeyProvider import org.p2p.wallet.send.model.SearchResult import org.p2p.wallet.send.repository.SendServiceRepository -import org.p2p.wallet.utils.toPublicKey +import org.p2p.wallet.send.repository.SendServiceTransactionError class CalculateSendFeesUseCase( - private val amountRepository: RpcAmountRepository, private val sendServiceRepository: SendServiceRepository, - private val getFeesInPayingTokenUseCase: GetFeesInPayingTokenUseCase, - private val addressInteractor: TransactionAddressInteractor, + private val tokenKeyProvider: TokenKeyProvider, + private val amountsConverter: TokenServiceAmountsConverter, ) { suspend fun execute( sourceToken: Token.Active, feePayerToken: Token.Active, + sourceTokenAmount: BigInteger, searchResult: SearchResult, @Suppress("UNUSED_PARAMETER") useCache: Boolean = true ): FeeCalculationState { - return try { - val minRentExemption = getMinRentExemption(sourceToken) - val alternativeMinRentExemption = getSendServiceMinRentExemptionForSameTokens(sourceToken, feePayerToken) - - val feesInSol = getTransactionFeesInSol( - minRentExemption = minRentExemption, - sourceToken = sourceToken, - feePayerToken = feePayerToken, - searchResult = searchResult + try { + val rentExemptionInSol = sendServiceRepository.getTokenRentExemption( + tokenMint = Constants.WRAPPED_SOL_MINT.toBase58Instance() ) + val fees = try { + sendServiceRepository.estimateFees( + userWallet = tokenKeyProvider.publicKeyBase58, + recipient = searchResult.address.toBase58Instance(), + sourceToken = sourceToken, + feePayerToken = feePayerToken, + amount = if (sourceToken.isSOL) { + maxOf(rentExemptionInSol, sourceTokenAmount) + } else { + sourceTokenAmount + }, + ) + } catch (e: SendServiceTransactionError) { + val feesInSol = FeeAmount( + transactionFee = BigInteger.ZERO, + accountCreationFee = rentExemptionInSol + ) + return FeeCalculationState.PoolsNotFound( + feeInSol = FeeRelayerFee( + feesInSol = feesInSol, + feesInFeePayerToken = FeeAmount( + transactionFee = BigInteger.ZERO, + accountCreationFee = convertFromSol(feePayerToken, rentExemptionInSol) + ), + feesInSourceToken = FeeAmount( + transactionFee = BigInteger.ZERO, + accountCreationFee = convertFromSol(sourceToken, rentExemptionInSol) + ), + expectedFee = feesInSol + ), + ) + } - if (feesInSol.totalFeeLamports.isZero()) { - Timber.i("Total fees are zero!") + if (fees.tokenAccountRent.amount.amount.isZeroOrLess()) { return FeeCalculationState.NoFees } - val feesInFeePayerToken = getTransactionFeesInToken( - targetToken = feePayerToken, - feesInSol = feesInSol, - alternativeAccountCreationFee = alternativeMinRentExemption + val networkFee = BigInteger.ZERO + val accountCreationFeeInFeePayerToken = fees.tokenAccountRent.amount.amount + + val feesInSol = FeeAmount( + transactionFee = networkFee, + accountCreationFee = convertToSol(feePayerToken, accountCreationFeeInFeePayerToken) ) - val feesInSourceToken = getTransactionFeesInToken( - targetToken = sourceToken, - feesInSol = feesInSol, - alternativeAccountCreationFee = alternativeMinRentExemption + val feesInFeePayerToken = FeeAmount( + transactionFee = networkFee, + accountCreationFee = accountCreationFeeInFeePayerToken ) - // 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}") - } - } + val feesInSourceToken = FeeAmount( + transactionFee = networkFee, + accountCreationFee = convertAmount( + from = feePayerToken, + to = sourceToken, + amount = accountCreationFeeInFeePayerToken + ) + ) - FeeCalculationState.Success( + return FeeCalculationState.Success( fee = FeeRelayerFee( feesInSol = feesInSol, feesInFeePayerToken = feesInFeePayerToken, feesInSourceToken = feesInSourceToken, expectedFee = feesInSol + ) ) } catch (e: CancellationException) { @@ -85,118 +110,36 @@ class CalculateSendFeesUseCase( } } - /** - * this is a special case, because if we have fee payer token the same as source token - * we have to be very precise with amounts we use - * since the math is calculated manually when "max" button is pressed - * transaction will fail if account creation fee in token has incorrect value (rates in usd I guess) - * given this, we just make request to send-service to see what "token_account_rent" it returns - * - * todo: it looks like we don't have correct prices for tokens at a moment - * - * @return zero if tokens are different, or min rent exemption in source token - */ - private suspend fun getSendServiceMinRentExemptionForSameTokens( - sourceToken: Token.Active, - feePayerToken: Token.Active + private suspend fun convertToSol( + token: Token.Active, + amount: BigInteger, ): BigInteger { - if (sourceToken.isSOL || sourceToken.mintAddress != feePayerToken.mintAddress) { - return BigInteger.ZERO - } - - return try { - sendServiceRepository - .getTokenRentExemption( - userWallet = sourceToken.publicKey.toBase58Instance(), - token = sourceToken, - // return in TOKEN - returnInToken = true - ) - } catch (e: Throwable) { - // send service can return "insufficient funds" when fee payer token is the same as source token - // and there's not enough balance to cover account creation fee - // todo: errors should be checked specifically for this type of error, because send-service may fail - // by another reason - BigInteger.ZERO - } + if (token.isSOL) return amount + return amountsConverter.convertAmount( + amountFrom = token.mintAddressB58 to amount, + mintsToConvertTo = listOf(Constants.WRAPPED_SOL_MINT.toBase58Instance()) + )[Constants.WRAPPED_SOL_MINT.toBase58Instance()] ?: BigInteger.ZERO } - /** - * token2022 may has higher account creation fee as it takes more data for account - */ - private suspend fun getMinRentExemption(sourceToken: Token.Active) = if (sourceToken.isToken2022) { - sendServiceRepository - .getTokenRentExemption( - userWallet = sourceToken.publicKey.toBase58Instance(), - token = sourceToken, - // return in SOL - returnInToken = false - ) - .takeIf(BigInteger::isNotZero) - ?: amountRepository.getMinBalanceForRentExemption( - TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH - ) - } else { - amountRepository.getMinBalanceForRentExemption( - TokenProgram.AccountInfoData.ACCOUNT_INFO_DATA_LENGTH - ) - } - - private suspend fun getTransactionFeesInSol( - minRentExemption: BigInteger, - sourceToken: Token.Active, - feePayerToken: Token.Active, - searchResult: SearchResult, - ): FeeAmount { - val lamportsPerSignature: BigInteger = amountRepository.getLamportsPerSignature(null) - var transactionFeeInSol = BigInteger.ZERO - - // owner's signature - transactionFeeInSol += lamportsPerSignature - - // feePayer's signature - if (!feePayerToken.isSOL) { - Timber.i("Fee payer is not sol, adding $lamportsPerSignature for fee") - transactionFeeInSol += lamportsPerSignature - } - - val shouldCreateAccount = checkAccountCreationIsRequired(sourceToken, searchResult.address) - Timber.i("Should create account = $shouldCreateAccount") - - val accountCreationFeeInSol = if (shouldCreateAccount) minRentExemption else BigInteger.ZERO - - return FeeAmount( - transactionFee = transactionFeeInSol, - accountCreationFee = accountCreationFeeInSol, - ).copy( - // todo: while we use send service, we don't pay network fee - transactionFee = BigInteger.ZERO - ) - } - - private suspend fun getTransactionFeesInToken( - targetToken: Token.Active, - feesInSol: FeeAmount, - alternativeAccountCreationFee: BigInteger - ): FeeAmount? { - return getFeesInPayingTokenUseCase.execute( - targetToken = targetToken, - transactionFeeInSol = feesInSol.transactionFee, - accountCreationFeeInSol = feesInSol.accountCreationFee - )?.let { - // if local calculation in token represents lower fee, than use send-service fee - it.copy(accountCreationFee = maxOf(it.accountCreationFee, alternativeAccountCreationFee)) - } + private suspend fun convertFromSol( + token: Token.Active, + amount: BigInteger, + ): BigInteger { + if (token.isSOL) return amount + return amountsConverter.convertAmount( + amountFrom = Constants.WRAPPED_SOL_MINT.toBase58Instance() to amount, + mintsToConvertTo = listOf(token.mintAddressB58) + )[token.mintAddressB58] ?: BigInteger.ZERO } - private suspend fun checkAccountCreationIsRequired( - token: Token.Active, - recipient: String - ): Boolean { - return token.mintAddress != Constants.WRAPPED_SOL_MINT && addressInteractor.findSplTokenAddressData( - mintAddress = token.mintAddress, - destinationAddress = recipient.toPublicKey(), - programId = token.programId?.toPublicKey() ?: TokenProgram.PROGRAM_ID - ).shouldCreateAccount + private suspend fun convertAmount( + from: Token.Active, + to: Token.Active, + amount: BigInteger, + ): BigInteger { + return amountsConverter.convertAmount( + amountFrom = from.mintAddressB58 to amount, + mintsToConvertTo = listOf(to.mintAddressB58) + )[to.mintAddressB58] ?: BigInteger.ZERO } } diff --git a/app/src/main/java/org/p2p/wallet/send/model/SendServiceFees.kt b/app/src/main/java/org/p2p/wallet/send/model/SendServiceFees.kt new file mode 100644 index 0000000000..3bcc25605e --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/send/model/SendServiceFees.kt @@ -0,0 +1,14 @@ +package org.p2p.wallet.send.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.p2p.wallet.send.model.send_service.GeneratedTransaction + +@Parcelize +data class SendServiceFees( + val recipientGetsAmount: GeneratedTransaction.AmountData, + val totalAmount: GeneratedTransaction.AmountData, + val networkFee: GeneratedTransaction.NetworkFeeData, + val tokenAccountRent: GeneratedTransaction.NetworkFeeData, + val token2022TransferFee: GeneratedTransaction.NetworkFeeData, +) : Parcelable diff --git a/app/src/main/java/org/p2p/wallet/send/model/send_service/GeneratedTransaction.kt b/app/src/main/java/org/p2p/wallet/send/model/send_service/GeneratedTransaction.kt index f80465502a..13329d7642 100644 --- a/app/src/main/java/org/p2p/wallet/send/model/send_service/GeneratedTransaction.kt +++ b/app/src/main/java/org/p2p/wallet/send/model/send_service/GeneratedTransaction.kt @@ -1,9 +1,13 @@ package org.p2p.wallet.send.model.send_service +import android.os.Parcelable import java.math.BigDecimal import java.math.BigInteger +import kotlinx.parcelize.Parcelize import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.Base64String +import org.p2p.core.crypto.toBase58Instance +import org.p2p.core.wrapper.eth.helpers.RandomHelper.randomBytes data class GeneratedTransaction( val transaction: Base64String, @@ -16,6 +20,7 @@ data class GeneratedTransaction( val tokenAccountRent: NetworkFeeData? = null, val token2022TransferFee: NetworkFeeData? = null, ) { + @Parcelize data class AmountData( val amount: BigInteger, val usdAmount: BigDecimal, @@ -26,10 +31,25 @@ data class GeneratedTransaction( val logoUrl: String?, val coingeckoId: String?, val price: Map, - ) + ) : Parcelable { + companion object { + val EMPTY = AmountData( + amount = BigInteger.ZERO, + usdAmount = BigDecimal.ZERO, + address = randomBytes(32).toBase58Instance(), + tokenSymbol = "", + tokenName = "", + decimals = 0, + logoUrl = null, + coingeckoId = null, + price = emptyMap(), + ) + } + } + @Parcelize data class NetworkFeeData( val source: NetworkFeeSource, val amount: AmountData, - ) + ) : Parcelable } diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt index ced775cf89..147d8ae9b5 100644 --- a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRemoteRepository.kt @@ -5,13 +5,17 @@ import java.math.BigInteger import org.p2p.core.crypto.Base58String import org.p2p.core.crypto.toBase58Instance import org.p2p.core.network.data.EmptyDataException +import org.p2p.core.network.gson.GsonProvider import org.p2p.core.token.Token import org.p2p.core.utils.Constants -import org.p2p.core.wrapper.eth.helpers.RandomHelper.randomBytes +import org.p2p.core.utils.isZeroOrLess import org.p2p.solanaj.model.types.RpcMapRequest import org.p2p.solanaj.model.types.RpcRequest import org.p2p.wallet.send.api.SendServiceApi +import org.p2p.wallet.send.api.responses.SendGeneratedTransactionResponse +import org.p2p.wallet.send.model.SendServiceFees import org.p2p.wallet.send.model.send_service.GeneratedTransaction +import org.p2p.wallet.send.model.send_service.NetworkFeeSource import org.p2p.wallet.send.model.send_service.SendFeePayerMode import org.p2p.wallet.send.model.send_service.SendRentPayerMode import org.p2p.wallet.send.model.send_service.SendTransferMode @@ -59,34 +63,60 @@ class SendServiceRemoteRepository( return generatedTransaction.recipientGetsAmount.amount } - /** - * todo: this and getMaxAmountToSend are very similar, maybe we can merge them - * but this method should not use cache, values may change very often due to volatility - */ - override suspend fun getTokenRentExemption( + override suspend fun getTokenRentExemption(tokenMint: Base58String): BigInteger { + val params = buildMap { + this += "mints" to listOf( + tokenMint.base58Value + ) + } + val rpcRequest = RpcMapRequest("get_token_account_rent_exempt", params) + val response = api.getTokenAccountRentExemption(rpcRequest).result.mapValues { it.value.toBigInteger() } + return response[tokenMint] ?: BigInteger.ZERO + } + + override suspend fun getTokenRentExemption(token: Token.Active): BigInteger = + getTokenRentExemption(token.mintAddressB58) + + override suspend fun estimateFees( userWallet: Base58String, - token: Token.Active, - returnInToken: Boolean - ): BigInteger { + recipient: Base58String, + sourceToken: Token.Active, + feePayerToken: Token.Active, + amount: BigInteger + ): SendServiceFees { val generatedTransaction = generateTransaction( userWallet = userWallet, - amountLamports = token.totalInLamports, - recipient = randomBytes(32).toBase58Instance(), - tokenMint = if (token.isSOL) null else token.mintAddress.toBase58Instance(), + // 2 is the minimum amount to make send-service work correct while estimating fees + amountLamports = if (amount.isZeroOrLess()) { + // uint64::MAX in rust + SendServiceRepository.UINT64_MAX + } else { + maxOf(amount, BigInteger("2")) + }, + recipient = recipient, + tokenMint = if (sourceToken.isSOL) null else sourceToken.mintAddressB58, // exactIn because we need to get totalAmount less than amount with send transferMode = SendTransferMode.ExactIn, - feePayerMode = SendFeePayerMode.UserSol, // get rent exemption in SOL - rentPayerMode = if (returnInToken) SendRentPayerMode.Custom else SendRentPayerMode.UserSol, - customRentPayerTokenMint = if (returnInToken) token.mintAddress.toBase58Instance() else null + rentPayerMode = if (feePayerToken.isSOL) SendRentPayerMode.UserSol else SendRentPayerMode.Custom, + customRentPayerTokenMint = if (feePayerToken.isSOL) null else feePayerToken.mintAddressB58 ) - if (generatedTransaction.tokenAccountRent == null) { - return BigInteger.ZERO - } - - return generatedTransaction.tokenAccountRent.amount.amount + return SendServiceFees( + recipientGetsAmount = generatedTransaction.recipientGetsAmount, + totalAmount = generatedTransaction.totalAmount, + networkFee = generatedTransaction.networkFee, + tokenAccountRent = generatedTransaction.tokenAccountRent + ?: GeneratedTransaction.NetworkFeeData( + NetworkFeeSource.UserCompensated, + GeneratedTransaction.AmountData.EMPTY + ), + token2022TransferFee = generatedTransaction.token2022TransferFee ?: GeneratedTransaction.NetworkFeeData( + NetworkFeeSource.UserCompensated, + GeneratedTransaction.AmountData.EMPTY + ) + ) } override suspend fun generateTransaction( @@ -131,6 +161,18 @@ class SendServiceRemoteRepository( } val rpcRequest = RpcMapRequest("transfer", params) - return api.generateTransaction(rpcRequest).result.toDomain() + val responseJson = api.generateTransactionRaw(rpcRequest).asJsonObject + if (responseJson.has("result")) { + val parsed: SendGeneratedTransactionResponse = GsonProvider().provide() + .fromJson(responseJson.get("result"), SendGeneratedTransactionResponse::class.java) + return parsed.toDomain() + } else if (responseJson.has("error")) { + val error = responseJson.get("error").asJsonObject + val code = error.get("code")?.asInt ?: -1 + val message = error.get("message")?.asString ?: "Unknown error" + throw SendServiceTransactionError(message, code) + } else { + throw Exception("Unknown RPC error: $responseJson") + } } } diff --git a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt index 5117c6d75e..f2e6f2bfd5 100644 --- a/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt +++ b/app/src/main/java/org/p2p/wallet/send/repository/SendServiceRepository.kt @@ -3,12 +3,22 @@ package org.p2p.wallet.send.repository import java.math.BigInteger import org.p2p.core.crypto.Base58String import org.p2p.core.token.Token +import org.p2p.wallet.send.model.SendServiceFees import org.p2p.wallet.send.model.send_service.GeneratedTransaction import org.p2p.wallet.send.model.send_service.SendFeePayerMode import org.p2p.wallet.send.model.send_service.SendRentPayerMode import org.p2p.wallet.send.model.send_service.SendTransferMode +class SendServiceTransactionError( + message: String, + val code: Int, +) : Exception(message) + interface SendServiceRepository { + companion object { + val UINT64_MAX = BigInteger("18446744073709551615") + } + suspend fun getCompensationTokens(): List suspend fun generateTransaction( @@ -29,9 +39,15 @@ interface SendServiceRepository { token: Token.Active ): BigInteger - suspend fun getTokenRentExemption( + suspend fun getTokenRentExemption(token: Token.Active): BigInteger + + suspend fun getTokenRentExemption(tokenMint: Base58String): BigInteger + + suspend fun estimateFees( userWallet: Base58String, - token: Token.Active, - returnInToken: Boolean = false - ): BigInteger + recipient: Base58String, + sourceToken: Token.Active, + feePayerToken: Token.Active, + amount: BigInteger + ): SendServiceFees } diff --git a/app/src/main/java/org/p2p/wallet/send/ui/NewSendContract.kt b/app/src/main/java/org/p2p/wallet/send/ui/NewSendContract.kt index fea944eedb..554a0a9070 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/NewSendContract.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/NewSendContract.kt @@ -1,6 +1,7 @@ package org.p2p.wallet.send.ui import java.math.BigDecimal +import java.math.BigInteger import org.p2p.core.common.TextContainer import org.p2p.core.token.Token import org.p2p.uikit.components.UiKitSendDetailsWidgetContract @@ -15,7 +16,11 @@ interface NewSendContract { fun updateInputFraction(newInputFractionLength: Int) fun showFreeTransactionsInfo() - fun showTransactionDetails(sendFeeTotal: SendFeeTotal) + fun showTransactionDetails( + sendFeeTotal: SendFeeTotal, + inputAmount: BigInteger, + useMax: Boolean, + ) fun showProgressDialog(internalTransactionId: String, data: NewShowProgress) fun showTokenSelection(selectedToken: Token.Active?) fun showDebugInfo(text: CharSequence) diff --git a/app/src/main/java/org/p2p/wallet/send/ui/NewSendFragment.kt b/app/src/main/java/org/p2p/wallet/send/ui/NewSendFragment.kt index 839543c1f1..fc0b71e83e 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/NewSendFragment.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/NewSendFragment.kt @@ -8,6 +8,7 @@ import android.view.View import org.koin.android.ext.android.inject import org.koin.core.parameter.parametersOf import java.math.BigDecimal +import java.math.BigInteger import org.p2p.core.common.TextContainer import org.p2p.core.token.Token import org.p2p.uikit.organisms.UiKitToolbar @@ -143,10 +144,16 @@ class NewSendFragment : } } - override fun showTransactionDetails(sendFeeTotal: SendFeeTotal) { + override fun showTransactionDetails( + sendFeeTotal: SendFeeTotal, + inputAmount: BigInteger, + useMax: Boolean, + ) { NewSendDetailsBottomSheet.show( fm = childFragmentManager, totalFee = sendFeeTotal, + inputAmount = inputAmount, + useMax = useMax, requestKey = KEY_REQUEST_SEND, feeResultKey = KEY_RESULT_FEE, feePayerTokensResultKey = KEY_RESULT_FEE_PAYER_TOKENS 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 5ef2922f3b..a04d44a06d 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 @@ -518,7 +518,11 @@ class NewSendPresenter( tokenExtensions = currentState.tokenExtensions ) - view?.showTransactionDetails(total) + view?.showTransactionDetails( + total, + calculationMode.getCurrentAmountLamports(), + calculationMode.isMaxUsed() + ) } } } @@ -623,6 +627,7 @@ class NewSendPresenter( feePayerToken = sendInteractor.getFeePayerToken(), strategy = strategy, tokenAmount = calculationMode.getCurrentAmount(), + useMax = calculationMode.isMaxUsed(), useCache = useCache ) } diff --git a/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsBottomSheet.kt b/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsBottomSheet.kt index c865bf5754..141c2080c9 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsBottomSheet.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsBottomSheet.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import org.koin.android.ext.android.inject +import java.math.BigInteger import org.p2p.core.token.Token import org.p2p.core.utils.formatFiat import org.p2p.core.utils.formatToken @@ -27,6 +28,8 @@ import org.p2p.wallet.utils.withArgs import org.p2p.wallet.utils.withTextOrGone private const val ARG_SEND_FEE_TOTAL = "ARG_SEND_FEE_TOTAL" +private const val ARG_AMOUNT = "ARG_AMOUNT" +private const val ARG_USE_MAX = "ARG_USE_MAX" private const val ARG_REQUEST_KEY = "ARG_REQUEST_KEY" private const val ARG_RESULT_KEY_FEE = "ARG_RESULT_KEY_FEE" private const val ARG_RESULT_KEY_FEE_PAYER_TOKENS = "ARG_RESULT_KEY_FEE_PAYER_TOKENS" @@ -41,12 +44,16 @@ class NewSendDetailsBottomSheet : fun show( fm: FragmentManager, totalFee: SendFeeTotal, + inputAmount: BigInteger, + useMax: Boolean, requestKey: String, feeResultKey: String, feePayerTokensResultKey: String, ) = NewSendDetailsBottomSheet() .withArgs( ARG_SEND_FEE_TOTAL to totalFee, + ARG_AMOUNT to inputAmount, + ARG_USE_MAX to useMax, ARG_REQUEST_KEY to requestKey, ARG_RESULT_KEY_FEE to feeResultKey, ARG_RESULT_KEY_FEE_PAYER_TOKENS to feePayerTokensResultKey @@ -59,6 +66,9 @@ class NewSendDetailsBottomSheet : private lateinit var binding: DialogNewSendDetailsBinding private val sendFee: SendFeeTotal by args(ARG_SEND_FEE_TOTAL) + private val inputAmount: BigInteger by args(ARG_AMOUNT) + private val useMax: Boolean by args(ARG_USE_MAX) + private val feeResultKey: String by args(ARG_RESULT_KEY_FEE) private val feePayerTokensResultKey: String by args(ARG_RESULT_KEY_FEE_PAYER_TOKENS) private val requestKey: String by args(ARG_REQUEST_KEY) @@ -202,8 +212,8 @@ class NewSendDetailsBottomSheet : highlightedText = fee.getApproxAccountCreationFeeUsd().orEmpty(), color = colorMountain ) - imageViewAccountFeeInfo.setOnClickListener { presenter.loadFeePayerTokens(fee) } - textViewTitleAccountFee.setOnClickListener { presenter.loadFeePayerTokens(fee) } + imageViewAccountFeeInfo.setOnClickListener { presenter.loadFeePayerTokens(fee, inputAmount, useMax) } + textViewTitleAccountFee.setOnClickListener { presenter.loadFeePayerTokens(fee, inputAmount, useMax) } } else { textViewSubtitleAccountFee.setTextColor(colorMint) textViewSubtitleAccountFee.text = getString(R.string.send_free_fee_format) diff --git a/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsContract.kt b/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsContract.kt index 560a8d4cd9..c8faadfa78 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsContract.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsContract.kt @@ -1,5 +1,6 @@ package org.p2p.wallet.send.ui.details +import java.math.BigInteger import org.p2p.core.token.Token import org.p2p.wallet.common.mvp.MvpPresenter import org.p2p.wallet.common.mvp.MvpView @@ -13,6 +14,10 @@ interface NewSendDetailsContract { } interface Presenter : MvpPresenter { - fun loadFeePayerTokens(fee: SendSolanaFee) + fun loadFeePayerTokens( + fee: SendSolanaFee, + inputAmount: BigInteger, + useMax: Boolean + ) } } diff --git a/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsPresenter.kt b/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsPresenter.kt index fb353f4f53..da41e76d99 100644 --- a/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsPresenter.kt +++ b/app/src/main/java/org/p2p/wallet/send/ui/details/NewSendDetailsPresenter.kt @@ -2,6 +2,7 @@ package org.p2p.wallet.send.ui.details import android.content.res.Resources import timber.log.Timber +import java.math.BigInteger import kotlinx.coroutines.launch import org.p2p.wallet.common.mvp.BasePresenter import org.p2p.wallet.send.interactor.SendInteractor @@ -15,13 +16,20 @@ class NewSendDetailsPresenter( private val resources: Resources ) : BasePresenter(), NewSendDetailsContract.Presenter { - override fun loadFeePayerTokens(fee: SendSolanaFee) { + override fun loadFeePayerTokens( + fee: SendSolanaFee, + inputAmount: BigInteger, + useMax: Boolean, + ) { view?.showAccountCreationFeeLoading(isLoading = true) launch { try { val userTokens = userInteractor.getUserTokens() val feePayerTokens = sendInteractor.findAlternativeFeePayerTokens( userTokens = userTokens, + sourceToken = fee.sourceToken, + sourceTokenAmount = inputAmount, + useMax = useMax, feePayerToExclude = fee.feePayerToken, transactionFeeInSOL = fee.feeRelayerFee.transactionFeeInSol, accountCreationFeeInSOL = fee.feeRelayerFee.accountCreationFeeInSol From aa60e77b1179073796cbea6dda11187c87133de5 Mon Sep 17 00:00:00 2001 From: Eduard Maximovich Date: Mon, 26 Feb 2024 19:22:41 +0400 Subject: [PATCH 50/51] bump app build version --- buildSrc/src/main/java/AppVersions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AppVersions.kt b/buildSrc/src/main/java/AppVersions.kt index 4f710688eb..3f728bcf05 100644 --- a/buildSrc/src/main/java/AppVersions.kt +++ b/buildSrc/src/main/java/AppVersions.kt @@ -3,5 +3,5 @@ object AppVersions { const val VERSION_MINOR = 12 const val VERSION_HOTFIX = 0 const val VERSION_PATCH = 0 - const val VERSION_BUILD = 3585 + const val VERSION_BUILD = 3586 } From 18f930beb1adc34b4cfa8ee9caac192659a791bb Mon Sep 17 00:00:00 2001 From: "Gleb Levinkov (p2p)" <100791035+gslevinkov@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:33:09 +0300 Subject: [PATCH 51/51] Update buildSrc/src/main/java/AppVersions.kt Signed-off-by: Gleb Levinkov (p2p) <100791035+gslevinkov@users.noreply.github.com> --- buildSrc/src/main/java/AppVersions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/AppVersions.kt b/buildSrc/src/main/java/AppVersions.kt index 3f728bcf05..0baa3954e4 100644 --- a/buildSrc/src/main/java/AppVersions.kt +++ b/buildSrc/src/main/java/AppVersions.kt @@ -2,6 +2,6 @@ object AppVersions { const val VERSION_MAJOR = 2 const val VERSION_MINOR = 12 const val VERSION_HOTFIX = 0 - const val VERSION_PATCH = 0 + const val VERSION_PATCH = 1 const val VERSION_BUILD = 3586 }