diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e4e4c2b4e5..e312758a52 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -35,6 +35,7 @@ import com.tangem.tap.features.signin.redux.SignInMiddleware import com.tangem.tap.features.signin.redux.SignInState import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware import com.tangem.tap.features.tokens.legacy.redux.TokensState +import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.proxy.redux.DaggerGraphMiddleware @@ -101,6 +102,7 @@ data class AppState( SignInMiddleware.middleware, DaggerGraphMiddleware.daggerGraphMiddleware, LegacyMiddleware.legacyMiddleware, + TradeCryptoMiddleware.middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt index 69dee481db..64763a6216 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl @@ -40,9 +39,8 @@ class RussianCardholdersWarningBottomSheetDialog( if (dialogData != null) { store.dispatchOpenUrl(dialogData.topUpUrl) Analytics.send(Token.Topup.ScreenOpened()) - } else { - store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false)) } + dismiss() } binding?.btnNo?.setOnClickListener { diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 7fa2522fb1..70b2540317 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -22,6 +22,9 @@ sealed class TapError( object UnknownError : TapError(R.string.send_error_unknown) open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage)) + data class UnsupportedState( + val stateError: String, + ) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError")) object NoInternetConnection : TapError(R.string.wallet_notification_no_internet) object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index d784133e08..7dc282cacd 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -2,7 +2,6 @@ package com.tangem.tap.features.demo import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -21,8 +20,6 @@ object DemoHelper { private val disabledActionFeatures = listOf( WalletConnectAction.StartWalletConnect::class.java, - TradeCryptoAction.Buy::class.java, - TradeCryptoAction.Sell::class.java, BackupAction.StartBackup::class.java, DetailsAction.ResetToFactory.Start::class.java, ) diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt index f58d3a9e72..31a7657c79 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt @@ -9,7 +9,7 @@ import com.tangem.tap.features.intentHandler.IntentHandler class BuyCurrencyIntentHandler : IntentHandler { override fun handleIntent(intent: Intent?): Boolean { - // FIXME: It hasn't worked since the redesign + // FIXME: [REDACTED_JIRA] // val data = intent?.data ?: return false // val currency = store.state.walletState.selectedCurrency ?: return false // diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index da1c6cc68a..52e8e12a33 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -1,11 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.intentHandler.IntentHandler -import com.tangem.tap.store -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -13,33 +9,36 @@ import timber.log.Timber class SellCurrencyIntentHandler : IntentHandler { override fun handleIntent(intent: Intent?): Boolean { - return try { - val intentData = intent?.data ?: return false - val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false - val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false - val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false - val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false + // FIXME: [REDACTED_JIRA] + // return try { + // val intentData = intent?.data ?: return false + // val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false + // val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false + // val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false + // val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false + // + // Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") + // store.dispatchOnMain( + // TradeCryptoAction.SendCrypto( + // currencyId = currency, + // amount = amount, + // destinationAddress = destinationAddress, + // transactionId = transactionID, + // ), + // ) + // true + // } catch (exception: Exception) { + // Timber.d("Not MoonPay URL") + // false + // } - Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - store.dispatchOnMain( - TradeCryptoAction.SendCrypto( - currencyId = currency, - amount = amount, - destinationAddress = destinationAddress, - transactionId = transactionID, - ), - ) - true - } catch (exception: Exception) { - Timber.d("Not MoonPay URL") - false - } + return false } - private companion object { - private const val TRANSACTION_ID_PARAM = "transactionId" - private const val CURRENCY_CODE_PARAM = "baseCurrencyCode" - private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" - private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" - } + // private companion object { + // private const val TRANSACTION_ID_PARAM = "transactionId" + // private const val CURRENCY_CODE_PARAM = "baseCurrencyCode" + // private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" + // private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" + // } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt new file mode 100644 index 0000000000..dac4ffd59d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -0,0 +1,280 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import androidx.core.os.bundleOf +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.analytics.Analytics +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.tap.common.analytics.events.Token +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.TapError +import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE +import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.launch +import org.rekotlin.Middleware + +@Suppress("LargeClass") +@Deprecated("Will be removed soon") +object TradeCryptoMiddleware { + + val middleware: Middleware = { _, appState -> + { nextDispatch -> + { action -> + if (action is TradeCryptoAction) { + handle(appState, action) + } + nextDispatch(action) + } + } + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") + private fun handle(state: () -> AppState?, action: TradeCryptoAction) { + if (DemoHelper.tryHandle(state, action)) return + + when (action) { + is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen() + is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) + is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) + is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) + is TradeCryptoAction.New.Swap -> openSwap( + currency = action.cryptoCurrency, + ) + is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) + is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) + } + } + + private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress + ?.defaultAddress + ?.let(NetworkAddress.Address::value) + ?: return + + val status = action.cryptoCurrencyStatus + val currency = status.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + val exchangeManager = store.state.globalState.exchangeManager + val topUrl = exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Buy, + blockchain = blockchain, + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, + ) + + if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { + val dialogData = topUrl?.let { + AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl = it) + } + store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(data = dialogData)) + return + } + + if (currency is CryptoCurrency.Token && currency.network.isTestnet) { + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWalletId = action.userWallet.walletId, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager !is EthereumWalletManager) { + store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum") + return@launch + } + + buyErc20TestnetTokens( + card = action.userWallet.scanResponse.card, + walletManager = walletManager, + destinationAddress = currency.contractAddress, + ) + } + return + } + + topUrl?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Topup.ScreenOpened()) + } + } + + private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress + ?.defaultAddress + ?.let(NetworkAddress.Address::value) + ?: return + val currency = action.cryptoCurrencyStatus.currency + + store.state.globalState.exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Sell, + blockchain = Blockchain.fromId(currency.network.id.value), + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Withdraw.ScreenOpened()) + } + } + + private fun preconfigureAndOpenSendScreen() = scope.launch { + // FIXME: [REDACTED_JIRA] + // val selectedWalletData = store.state.walletState.selectedWalletData ?: return + // + // Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency))) + // val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard { + // FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null")) + // return + // } + // + // store.dispatchOnMain( + // PrepareSendScreen( + // walletManager = walletManager, + // coinAmount = walletManager.wallet.amounts[AmountType.Coin], + // coinRate = selectedWalletData.fiatRate, + // ), + // ) + // store.dispatchOnMain( + // SendAction.SendSpecificTransaction( + // sendAmount = action.amount, + // destinationAddress = action.destinationAddress, + // transactionId = action.transactionId, + // ), + // ) + // store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + } + + private fun openReceiptUrl(transactionId: String) { + store.dispatchOnMain(NavigationAction.PopBackTo()) + store.state.globalState.exchangeManager.getSellCryptoReceiptUrl( + action = CurrencyExchangeManager.Action.Sell, + transactionId = transactionId, + )?.let { store.dispatchOpenUrl(it) } + } + + private fun openSwap(currency: CryptoCurrency) { + val bundle = bundleOf( + SwapFragment.CURRENCY_BUNDLE_KEY to currency, + ) + + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) + } + + private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { + val currency = action.tokenCurrency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWalletId = action.userWallet.walletId, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmount = walletManager.wallet.amounts.values.firstOrNull { + val amountType = it.type + amountType is AmountType.Token && amountType.token.contractAddress == currency.contractAddress + } + + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = walletManager.wallet.amounts[AmountType.Coin], + coinRate = action.coinFiatRate, + tokenAmount = sendableAmount, + tokenRate = action.tokenFiatRate, + ), + ) + + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + } + } + + private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) { + val cryptoStatus = action.coinStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWalletId = action.userWallet.walletId, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin } + when (currency) { + is CryptoCurrency.Coin -> { + val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol } + + if (amountToSend == null) { + val error = TapError.UnsupportedState(stateError = "Amount to send is null") + FirebaseCrashlytics.getInstance() + .recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = amountToSend, + coinRate = cryptoStatus.value.fiatRate, + ), + ) + } + is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") + } + + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 97b7ddcdec..7b0d4d5a93 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -1,8 +1,22 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.Message +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.models.scan.CardDTO +import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.model.Currency +import com.tangem.tap.features.demo.isDemoCard +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store +import java.math.BigDecimal /** [REDACTED_AUTHOR] @@ -73,4 +87,42 @@ class CurrencyExchangeManager( primaryRules = ExchangeRules.dummy(), ) } +} + +suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) { + walletManager.safeUpdate(card.isDemoCard()) + + val amountToSend = Amount(walletManager.wallet.blockchain) + + val feeResult = walletManager.getFee(amountToSend, destinationAddress) as? Result.Success ?: return + val fee = when (val feeForTx = feeResult.data) { + is TransactionFee.Choosable -> feeForTx.minimum + is TransactionFee.Single -> feeForTx.normal + } + + val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO + if (coinValue < fee.amount.value) return + + val signer = TangemSigner( + card = card, + tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, + initialMessage = Message(), + ) { signResponse -> + store.dispatch( + GlobalAction.UpdateWalletSignedHashes( + walletSignedHashes = signResponse.totalSignedHashes, + walletPublicKey = walletManager.wallet.publicKey.seedKey, + remainingSignatures = signResponse.remainingSignatures, + ), + ) + } + + walletManager.send( + transactionData = walletManager.createTransaction( + amount = amountToSend, + fee = fee, + destination = destinationAddress, + ), + signer = signer, + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index b2ce607a1f..09eaa23946 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -8,10 +8,6 @@ import java.math.BigDecimal sealed class TradeCryptoAction : Action { - data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction() - - object Sell : TradeCryptoAction() - data class SendCrypto( val currencyId: String, val amount: String, @@ -21,8 +17,6 @@ sealed class TradeCryptoAction : Action { data class FinishSelling(val transactionId: String) : TradeCryptoAction() - object Swap : TradeCryptoAction() - sealed class New : TradeCryptoAction() { data class Buy(