diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6944d456dc..586ded1a5c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { implementation(projects.data.txhistory) implementation(projects.data.wallets) implementation(projects.data.analytics) + implementation(projects.data.transaction) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index ead5fca293..5429ab3030 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -38,4 +40,10 @@ internal object TransactionDomainModule { walletManagersFacade = walletManagersFacade, ) } + + @Provides + @ViewModelScoped + fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { + return CreateTransactionUseCase(transactionRepository) + } } \ No newline at end of file diff --git a/data/transaction/.gitignore b/data/transaction/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/transaction/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts new file mode 100644 index 0000000000..e1b448fb10 --- /dev/null +++ b/data/transaction/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.transaction" +} + +dependencies { + + /** Tangem SDKs */ + implementation(deps.tangem.blockchain) + + /** Core */ + implementation(projects.core.utils) + + /** Domain */ + implementation(projects.domain.transaction) + implementation(projects.domain.legacy) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt new file mode 100644 index 0000000000..e5cf1cec6c --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -0,0 +1,65 @@ +package com.tangem.data.transaction + +import androidx.core.text.isDigitsOnly +import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras +import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras +import com.tangem.blockchain.blockchains.stellar.StellarMemo +import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras +import com.tangem.blockchain.blockchains.ton.TonTransactionExtras +import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionExtras +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultTransactionRepository( + private val walletManagersFacade: WalletManagersFacade, + private val coroutineDispatcherProvider: CoroutineDispatcherProvider, +) : TransactionRepository { + + override suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + return@withContext walletManager?.createTransaction(amount, fee, destination)?.copy( + extras = getMemoExtras(network.id.value, memo), + ) + } + + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { + val blockchain = Blockchain.fromId(networkId) + if (memo == null) return null + return when (blockchain) { + Blockchain.Stellar -> { + val xmlMemo = when { + memo.isNotEmpty() && memo.isDigitsOnly() -> StellarMemo.Id(memo.toBigInteger()) + else -> StellarMemo.Text(memo) + } + StellarTransactionExtras(xmlMemo) + } + Blockchain.Binance -> BinanceTransactionExtras(memo) + Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } + Blockchain.Cosmos -> CosmosTransactionExtras(memo) + Blockchain.TON -> TonTransactionExtras(memo) + else -> null + } + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt new file mode 100644 index 0000000000..de935654bb --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.transaction.di + +import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TransactionDataModule { + + @Provides + @Singleton + fun providesTransactionRepository( + walletManagersFacade: WalletManagersFacade, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): TransactionRepository { + return DefaultTransactionRepository( + walletManagersFacade = walletManagersFacade, + coroutineDispatcherProvider = coroutineDispatcherProvider, + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt new file mode 100644 index 0000000000..6c38bc0e7d --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.transaction + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +interface TransactionRepository { + + @Suppress("LongParameterList") + suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt new file mode 100644 index 0000000000..55c5e0cd88 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId + +class CreateTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + /** + * [REDACTED_TODO_COMMENT] + */ + @Suppress("LongParameterList") + suspend operator fun invoke( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ) = Either.catch { + requireNotNull( + transactionRepository.createTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + ), + ) { "Failed to create transaction" } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index d15db2208c..f64e8a85a8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -77,13 +76,11 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty() - val (buttonTextId, buttonClick) = remember { - getButtonData( - currentState = currentState, - isSuccess = isSuccess, - uiState = uiState, - ) - } + val (buttonTextId, buttonClick) = getButtonData( + currentState = currentState, + isSuccess = isSuccess, + uiState = uiState, + ) val isButtonEnabled = isButtonEnabled( currentState = currentState, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt deleted file mode 100644 index 2e63cb625f..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.features.send.impl.presentation.viewmodel - -import androidx.core.text.isDigitsOnly -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.tokens.model.CryptoCurrency -import java.math.BigInteger - -internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean { - if (cryptoCurrency == null) return false - if (memo.isEmpty()) return true - return when (cryptoCurrency.network.id.value) { - Blockchain.XRP.id -> { - val tag = memo.toLongOrNull() - tag != null && tag <= XRP_TAG_MAX_NUMBER - } - Blockchain.Stellar.id -> { - isAssignableValue(memo) - } - else -> true - } -} - -private fun isAssignableValue(value: String): Boolean { - val memoType = when { - value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID - else -> XlmMemoType.TEXT - } - return when (memoType) { - XlmMemoType.TEXT -> { - // from org.stellar.sdk.MemoText - value.toByteArray().size <= XLM_MEMO_MAX_LENGTH - } - XlmMemoType.ID -> { - try { - // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo - value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() - } catch (ex: NumberFormatException) { - false - } - } - } -} - -fun determineXlmMemoType(value: String): XlmMemoType = when { - value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID - else -> XlmMemoType.TEXT -} - -enum class XlmMemoType { TEXT, ID } - -private const val XRP_TAG_MAX_NUMBER = 4294967295 -private const val XLM_MEMO_MAX_LENGTH = 28 \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 0b5cc51a05..9be54b3b66 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -6,15 +6,8 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.PagingData import arrow.core.getOrElse -import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras -import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.stellar.StellarMemo -import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras -import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpAddressService -import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -25,6 +18,7 @@ import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem @@ -57,6 +51,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -75,6 +70,7 @@ internal class SendViewModel @Inject constructor( private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, + private val createTransactionUseCase: CreateTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val walletManagersFacade: WalletManagersFacade, @@ -135,7 +131,7 @@ internal class SendViewModel @Inject constructor( getCurrenciesStatusUpdates(owner, wallet) }, ifLeft = { - // TODO add error handling + // todo add error handling [[REDACTED_JIRA]] return@launch }, ) @@ -299,7 +295,7 @@ internal class SendViewModel @Inject constructor( uiState = stateFactory.onFeeOnLoadedState(it, true) }, ifLeft = { - // TODO add error handling + // todo add error handling [[REDACTED_JIRA]] }, ) } @@ -421,7 +417,7 @@ internal class SendViewModel @Inject constructor( val amountToSend = amount.toBigDecimal().convertToAmount(cryptoCurrency) - // todo add notifications [[REDACTED_JIRA]] + // todo add error handling [[REDACTED_JIRA]] // val transactionErrors = walletManagersFacade.validateTransaction( // amount = amountToSend, // fee = fee.amount, @@ -429,32 +425,38 @@ internal class SendViewModel @Inject constructor( // network = cryptoCurrency.network, // ) - val txData = walletManagersFacade.createTransaction( + createTransactionUseCase( amount = amountToSend, fee = fee, memo = memo, destination = recipient, userWalletId = userWalletId, network = cryptoCurrency.network, - )?.copy(extras = getMemoExtras(cryptoCurrency.network.id.value, memo)) ?: return - - sendTransactionUseCase( - txData = txData, - userWallet = userWallet, - network = cryptoCurrency.network, ).fold( ifLeft = { - sendState.isSending.update { false } - // todo add notifications [[REDACTED_JIRA]] + Timber.e(it) + // todo add error handling [[REDACTED_JIRA]] }, - ifRight = { - sendState.transactionDate.update { - txData.date?.timeInMillis ?: System.currentTimeMillis() - } - sendState.isSuccess.update { true } - sendState.txUrl.update { - getTxUrl(txData.hash.orEmpty()) - } + ifRight = { txData -> + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + sendState.isSending.update { false } + // todo add error handling [[REDACTED_JIRA]] + }, + ifRight = { + sendState.transactionDate.update { + txData.date?.timeInMillis ?: System.currentTimeMillis() + } + sendState.isSuccess.update { true } + sendState.txUrl.update { + getTxUrl(txData.hash.orEmpty()) + } + }, + ) }, ) } @@ -499,25 +501,6 @@ internal class SendViewModel @Inject constructor( } // endregion - private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { - val blockchain = Blockchain.fromId(networkId) - if (memo == null) return null - return when (blockchain) { - Blockchain.Stellar -> { - val xmlMemo = when (determineXlmMemoType(memo)) { - XlmMemoType.TEXT -> StellarMemo.Text(memo) - XlmMemoType.ID -> StellarMemo.Id(memo.toBigInteger()) - } - StellarTransactionExtras(xmlMemo) - } - Blockchain.Binance -> BinanceTransactionExtras(memo) - Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } - Blockchain.Cosmos -> CosmosTransactionExtras(memo) - Blockchain.TON -> TonTransactionExtras(memo) - else -> null - } - } - companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" diff --git a/settings.gradle.kts b/settings.gradle.kts index 0f7c6dfa32..08729b531c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -143,4 +143,5 @@ include(":data:settings") include(":data:txhistory") include(":data:wallets") include(":data:analytics") +include(":data:transaction") // endregion Data modules