Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-27 20:18:41 +03:00
parent c04036723e
commit 4dc5d045a2
12 changed files with 225 additions and 106 deletions

View file

@ -76,6 +76,7 @@ dependencies {
implementation(projects.data.txhistory) implementation(projects.data.txhistory)
implementation(projects.data.wallets) implementation(projects.data.wallets)
implementation(projects.data.analytics) implementation(projects.data.analytics)
implementation(projects.data.transaction)
/** Features */ /** Features */
implementation(projects.features.onboarding) implementation(projects.features.onboarding)

View file

@ -2,6 +2,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.IsDemoCardUseCase 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.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
@ -38,4 +40,10 @@ internal object TransactionDomainModule {
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
) )
} }
@Provides
@ViewModelScoped
fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase {
return CreateTransactionUseCase(transactionRepository)
}
} }

1
data/transaction/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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)
}

View file

@ -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
}
}
}

View file

@ -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,
)
}
}

View file

@ -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?
}

View file

@ -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" }
}
}

View file

@ -14,7 +14,6 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType 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 isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false
val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty() val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty()
val (buttonTextId, buttonClick) = remember { val (buttonTextId, buttonClick) = getButtonData(
getButtonData( currentState = currentState,
currentState = currentState, isSuccess = isSuccess,
isSuccess = isSuccess, uiState = uiState,
uiState = uiState, )
)
}
val isButtonEnabled = isButtonEnabled( val isButtonEnabled = isButtonEnabled(
currentState = currentState, currentState = currentState,

View file

@ -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

View file

@ -6,15 +6,8 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.paging.PagingData import androidx.paging.PagingData
import arrow.core.getOrElse 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.XrpAddressService
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.Blockchain 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.Fee
import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase 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.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.utils.convertToAmount 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.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem
@ -57,6 +51,7 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -75,6 +70,7 @@ internal class SendViewModel @Inject constructor(
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getFeeUseCase: GetFeeUseCase, private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase, private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
@ -135,7 +131,7 @@ internal class SendViewModel @Inject constructor(
getCurrenciesStatusUpdates(owner, wallet) getCurrenciesStatusUpdates(owner, wallet)
}, },
ifLeft = { ifLeft = {
// TODO add error handling // todo add error handling [[REDACTED_JIRA]]
return@launch return@launch
}, },
) )
@ -299,7 +295,7 @@ internal class SendViewModel @Inject constructor(
uiState = stateFactory.onFeeOnLoadedState(it, true) uiState = stateFactory.onFeeOnLoadedState(it, true)
}, },
ifLeft = { 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) val amountToSend = amount.toBigDecimal().convertToAmount(cryptoCurrency)
// todo add notifications [[REDACTED_JIRA]] // todo add error handling [[REDACTED_JIRA]]
// val transactionErrors = walletManagersFacade.validateTransaction( // val transactionErrors = walletManagersFacade.validateTransaction(
// amount = amountToSend, // amount = amountToSend,
// fee = fee.amount, // fee = fee.amount,
@ -429,32 +425,38 @@ internal class SendViewModel @Inject constructor(
// network = cryptoCurrency.network, // network = cryptoCurrency.network,
// ) // )
val txData = walletManagersFacade.createTransaction( createTransactionUseCase(
amount = amountToSend, amount = amountToSend,
fee = fee, fee = fee,
memo = memo, memo = memo,
destination = recipient, destination = recipient,
userWalletId = userWalletId, userWalletId = userWalletId,
network = cryptoCurrency.network, network = cryptoCurrency.network,
)?.copy(extras = getMemoExtras(cryptoCurrency.network.id.value, memo)) ?: return
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = cryptoCurrency.network,
).fold( ).fold(
ifLeft = { ifLeft = {
sendState.isSending.update { false } Timber.e(it)
// todo add notifications [[REDACTED_JIRA]] // todo add error handling [[REDACTED_JIRA]]
}, },
ifRight = { ifRight = { txData ->
sendState.transactionDate.update { sendTransactionUseCase(
txData.date?.timeInMillis ?: System.currentTimeMillis() txData = txData,
} userWallet = userWallet,
sendState.isSuccess.update { true } network = cryptoCurrency.network,
sendState.txUrl.update { ).fold(
getTxUrl(txData.hash.orEmpty()) 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 // 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 { companion object {
private const val XRP_X_ADDRESS = 'X' private const val XRP_X_ADDRESS = 'X'
private const val DEFAULT_VALUE = "0.00" private const val DEFAULT_VALUE = "0.00"

View file

@ -143,4 +143,5 @@ include(":data:settings")
include(":data:txhistory") include(":data:txhistory")
include(":data:wallets") include(":data:wallets")
include(":data:analytics") include(":data:analytics")
include(":data:transaction")
// endregion Data modules // endregion Data modules