Updated on 2026-08-14
This commit is contained in:
parent
c04036723e
commit
4dc5d045a2
12 changed files with 225 additions and 106 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
1
data/transaction/.gitignore
vendored
Normal file
1
data/transaction/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
30
data/transaction/build.gradle.kts
Normal file
30
data/transaction/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
val (buttonTextId, buttonClick) = getButtonData(
|
||||
currentState = currentState,
|
||||
isSuccess = isSuccess,
|
||||
uiState = uiState,
|
||||
)
|
||||
}
|
||||
|
||||
val isButtonEnabled = isButtonEnabled(
|
||||
currentState = currentState,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,15 +425,19 @@ 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
|
||||
|
||||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = { txData ->
|
||||
sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
|
|
@ -445,7 +445,7 @@ internal class SendViewModel @Inject constructor(
|
|||
).fold(
|
||||
ifLeft = {
|
||||
sendState.isSending.update { false }
|
||||
// todo add notifications [[REDACTED_JIRA]]
|
||||
// todo add error handling [[REDACTED_JIRA]]
|
||||
},
|
||||
ifRight = {
|
||||
sendState.transactionDate.update {
|
||||
|
|
@ -457,6 +457,8 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFee(feeState: FeeSelectorState.Content): Fee? {
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -143,4 +143,5 @@ include(":data:settings")
|
|||
include(":data:txhistory")
|
||||
include(":data:wallets")
|
||||
include(":data:analytics")
|
||||
include(":data:transaction")
|
||||
// endregion Data modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue