Updated on 2026-08-14
This commit is contained in:
commit
4c12570008
16 changed files with 542 additions and 139 deletions
|
|
@ -4,7 +4,6 @@ import com.tangem.data.wallets.hot.TangemHotWalletSigner
|
|||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
|
|
@ -12,13 +11,11 @@ import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
|||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import com.tangem.domain.transaction.*
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.transaction.usecase.gasless.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -175,8 +172,14 @@ internal object TransactionDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase {
|
||||
return GetAllowanceUseCase(transactionRepository)
|
||||
fun provideGetAllowanceUseCase(allowanceRepository: AllowanceRepository): GetAllowanceUseCase {
|
||||
return GetAllowanceUseCase(allowanceRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAllowanceInfoUseCase(allowanceRepository: AllowanceRepository): GetAllowanceInfoUseCase {
|
||||
return GetAllowanceInfoUseCase(allowanceRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ dependencies {
|
|||
|
||||
/** Domain */
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.AllowanceRepository
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultAllowanceRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AllowanceRepository {
|
||||
|
||||
override suspend fun getAllowanceInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
requiredAmount: BigDecimal,
|
||||
): AllowanceInfo {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) {
|
||||
error("CryptoCurrency must be of type Token")
|
||||
}
|
||||
|
||||
val allowance = getAllowance(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
spenderAddress = spenderAddress,
|
||||
)
|
||||
|
||||
return when {
|
||||
allowance >= requiredAmount -> AllowanceInfo.Enough(allowance)
|
||||
allowance > BigDecimal.ZERO && allowance < requiredAmount &&
|
||||
BlockchainUtils.isTetherInEthereum(
|
||||
blockchainId = cryptoCurrency.network.rawId,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
) -> AllowanceInfo.ResetNeeded(allowance, requiredAmount)
|
||||
else -> AllowanceInfo.NotEnough(allowance, requiredAmount)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
): BigDecimal = withContext(dispatchers.io) {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) {
|
||||
error("CryptoCurrency must be of type Token")
|
||||
}
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network)
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
val allowanceResult = (walletManager as? Approver)?.getAllowance(
|
||||
spenderAddress,
|
||||
Token(
|
||||
symbol = blockchain.currency,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
) ?: error("Cannot cast to Approver")
|
||||
|
||||
allowanceResult.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { error(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.api.tangemTech.models.OperationType
|
||||
import com.tangem.datasource.api.tangemTech.models.TransactionEventBody
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
|
|
@ -332,28 +331,6 @@ internal class DefaultTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
spenderAddress: String,
|
||||
): BigDecimal {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network)
|
||||
val blockchain = cryptoCurrency.network.toBlockchain()
|
||||
val allowanceResult = (walletManager as? Approver)?.getAllowance(
|
||||
spenderAddress,
|
||||
Token(
|
||||
symbol = blockchain.currency,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
) ?: error("Cannot cast to Approver")
|
||||
|
||||
return allowanceResult.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { error(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
package com.tangem.data.transaction.di
|
||||
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.transaction.DefaultFeeRepository
|
||||
import com.tangem.data.transaction.DefaultGaslessTransactionRepository
|
||||
import com.tangem.data.transaction.DefaultMemoValidatorFacade
|
||||
import com.tangem.data.transaction.DefaultTransactionRepository
|
||||
import com.tangem.data.transaction.DefaultWalletAddressServiceRepository
|
||||
import com.tangem.data.transaction.*
|
||||
import com.tangem.data.transaction.error.DefaultFeeErrorResolver
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.transaction.FeeRepository
|
||||
import com.tangem.domain.transaction.GaslessTransactionRepository
|
||||
import com.tangem.domain.transaction.MemoValidatorFacade
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
import com.tangem.domain.transaction.*
|
||||
import com.tangem.domain.transaction.error.FeeErrorResolver
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -100,4 +92,16 @@ internal object TransactionDataModule {
|
|||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAllowanceRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AllowanceRepository {
|
||||
return DefaultAllowanceRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.math.BigDecimal
|
||||
|
||||
class DefaultAllowanceRepositoryTest {
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "1234567890ABCDEF")
|
||||
private val spenderAddress = "0xSpender"
|
||||
|
||||
private val approverWalletManager: WalletManager =
|
||||
mockk<WalletManager>(moreInterfaces = arrayOf(Approver::class))
|
||||
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk {
|
||||
coEvery { getOrCreateWalletManager(userWalletId, any<Network>()) } returns approverWalletManager
|
||||
}
|
||||
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
private lateinit var repository: DefaultAllowanceRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = DefaultAllowanceRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
// region getAllowance
|
||||
|
||||
@Nested
|
||||
inner class GetAllowanceTests {
|
||||
|
||||
@Test
|
||||
fun `throws when cryptoCurrency is Coin`() = runTest {
|
||||
val coin = buildCoin()
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, coin, spenderAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when walletManager is null`() = runTest {
|
||||
val token = buildToken()
|
||||
coEvery {
|
||||
walletManagersFacade.getOrCreateWalletManager(userWalletId, token.network)
|
||||
} returns null
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when walletManager is not Approver`() = runTest {
|
||||
val token = buildToken()
|
||||
val nonApproverWalletManager: WalletManager = mockk()
|
||||
coEvery {
|
||||
walletManagersFacade.getOrCreateWalletManager(userWalletId, token.network)
|
||||
} returns nonApproverWalletManager
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns allowance on success`() = runTest {
|
||||
val token = buildToken()
|
||||
val expected = BigDecimal("100")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any<Token>())
|
||||
} returns Result.success(expected)
|
||||
|
||||
val result = repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
|
||||
assertThat(result).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when approver returns failure`() = runTest {
|
||||
val token = buildToken()
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any<Token>())
|
||||
} returns Result.failure(RuntimeException("rpc error"))
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowance(userWalletId, token, spenderAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getAllowanceInfo
|
||||
|
||||
@Nested
|
||||
inner class GetAllowanceInfoTests {
|
||||
|
||||
@Test
|
||||
fun `throws when cryptoCurrency is Coin`() = runTest {
|
||||
val coin = buildCoin()
|
||||
|
||||
assertThrows<IllegalStateException> {
|
||||
repository.getAllowanceInfo(userWalletId, coin, spenderAddress, BigDecimal.ONE)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Enough when allowance equals required amount`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "usd-coin")
|
||||
val amount = BigDecimal("100")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(amount)
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, amount)
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java)
|
||||
assertThat((result as AllowanceInfo.Enough).allowance).isEqualTo(amount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Enough when allowance exceeds required amount`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "usd-coin")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("200"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java)
|
||||
assertThat((result as AllowanceInfo.Enough).allowance).isEqualTo(BigDecimal("200"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns NotEnough when allowance is zero`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal.ZERO)
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("50"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java)
|
||||
result as AllowanceInfo.NotEnough
|
||||
assertThat(result.allowance).isEqualTo(BigDecimal.ZERO)
|
||||
assertThat(result.requiredAmount).isEqualTo(BigDecimal("50"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns NotEnough when partial allowance for non-tether token`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "usd-coin")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("30"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java)
|
||||
result as AllowanceInfo.NotEnough
|
||||
assertThat(result.allowance).isEqualTo(BigDecimal("30"))
|
||||
assertThat(result.requiredAmount).isEqualTo(BigDecimal("100"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns NotEnough when partial allowance for tether on non-ethereum network`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("30"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns ResetNeeded when partial allowance for tether on ethereum`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ETH", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("30"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.ResetNeeded::class.java)
|
||||
result as AllowanceInfo.ResetNeeded
|
||||
assertThat(result.allowance).isEqualTo(BigDecimal("30"))
|
||||
assertThat(result.requiredAmount).isEqualTo(BigDecimal("100"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns ResetNeeded when partial allowance for tether on ethereum testnet`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ETH/test", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("10"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("50"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.ResetNeeded::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns Enough for tether on ethereum when allowance is sufficient`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
} returns Result.success(BigDecimal("100"))
|
||||
|
||||
val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100"))
|
||||
|
||||
assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun buildNetwork(rawNetworkId: String): Network {
|
||||
val derivationPath = Network.DerivationPath.None
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), derivationPath),
|
||||
backendId = rawNetworkId,
|
||||
name = rawNetworkId.replaceFirstChar { it.uppercase() },
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = derivationPath,
|
||||
isTestnet = rawNetworkId.contains("test"),
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = false,
|
||||
canHandleTokens = true,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildToken(
|
||||
rawNetworkId: String = "ETH",
|
||||
rawCurrencyId: String = "tether",
|
||||
contractAddress: String = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
): CryptoCurrency.Token {
|
||||
val network = buildNetwork(rawNetworkId)
|
||||
return CryptoCurrency.Token(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(rawCurrencyId),
|
||||
),
|
||||
network = network,
|
||||
name = "Token",
|
||||
symbol = "TKN",
|
||||
decimals = 6,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildCoin(rawNetworkId: String = "ethereum"): CryptoCurrency.Coin {
|
||||
val network = buildNetwork(rawNetworkId)
|
||||
return CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("ethereum"),
|
||||
),
|
||||
network = network,
|
||||
name = "Ethereum",
|
||||
symbol = "ETH",
|
||||
decimals = 18,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.domain.transaction
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Repository interface for managing token allowances in the context of blockchain transactions.
|
||||
*/
|
||||
interface AllowanceRepository {
|
||||
|
||||
/**
|
||||
* Retrieves the allowance information for a specific spender and required amount.
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param cryptoCurrency The cryptocurrency for which the allowance is being checked (must be a token).
|
||||
* @param spenderAddress The address of the spender for whom the allowance is being checked.
|
||||
* @param requiredAmount The amount that is required for the transaction.
|
||||
*
|
||||
* @return An [AllowanceInfo] object that indicates whether the current allowance.
|
||||
* @throws IllegalStateException if the provided [cryptoCurrency] is not a token.
|
||||
*/
|
||||
suspend fun getAllowanceInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
requiredAmount: BigDecimal,
|
||||
): AllowanceInfo
|
||||
|
||||
/**
|
||||
* Retrieves the current allowance for a specific spender.
|
||||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param cryptoCurrency The cryptocurrency for which the allowance is being checked (must be a token).
|
||||
* @param spenderAddress The address of the spender for whom the allowance is being checked.
|
||||
*
|
||||
* @return The current allowance as a [BigDecimal].
|
||||
* @throws IllegalStateException if the provided [cryptoCurrency] is not a token.
|
||||
*/
|
||||
suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
): BigDecimal
|
||||
}
|
||||
|
|
@ -6,11 +6,9 @@ import com.tangem.blockchain.common.transaction.Fee
|
|||
import com.tangem.blockchain.common.transaction.TransactionSendResult
|
||||
import com.tangem.blockchain.common.transaction.TransactionsSendResult
|
||||
import com.tangem.blockchain.nft.models.NFTAsset
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.models.EventTransactionTypeDto
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
interface TransactionRepository {
|
||||
|
|
@ -91,12 +89,6 @@ interface TransactionRepository {
|
|||
gasLimit: BigInteger?,
|
||||
): TransactionExtras
|
||||
|
||||
suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
spenderAddress: String,
|
||||
): BigDecimal
|
||||
|
||||
suspend fun prepareForSend(
|
||||
transactionData: TransactionData,
|
||||
signer: TransactionSigner,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.domain.transaction.models
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Model that represents the allowance information for a specific spender and required amount.
|
||||
*/
|
||||
sealed class AllowanceInfo {
|
||||
|
||||
/**
|
||||
* Represents a state where the current allowance is sufficient to cover the required amount.
|
||||
*/
|
||||
data class Enough(val allowance: BigDecimal) : AllowanceInfo()
|
||||
|
||||
/**
|
||||
* Represents a state where the current allowance is insufficient to cover the required amount.
|
||||
*/
|
||||
data class NotEnough(val allowance: BigDecimal, val requiredAmount: BigDecimal) : AllowanceInfo()
|
||||
|
||||
/**
|
||||
* Represents a state where the current allowance is insufficient,
|
||||
* but it must be reset to cover the required amount (specific to certain tokens like Tether in Ethereum).
|
||||
*/
|
||||
data class ResetNeeded(val allowance: BigDecimal, val requiredAmount: BigDecimal) : AllowanceInfo()
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.AllowanceRepository
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case for retrieving the allowance information for a specific spender and required amount.
|
||||
*/
|
||||
class GetAllowanceInfoUseCase(
|
||||
private val allowanceRepository: AllowanceRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
spenderAddress: String,
|
||||
requiredAmount: BigDecimal,
|
||||
): Either<Throwable, AllowanceInfo> {
|
||||
return Either.catch {
|
||||
allowanceRepository.getAllowanceInfo(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
spenderAddress = spenderAddress,
|
||||
requiredAmount = requiredAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,15 @@ package com.tangem.domain.transaction.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.AllowanceRepository
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case for retrieving the current allowance for a specific spender.
|
||||
*/
|
||||
class GetAllowanceUseCase(
|
||||
private val transactionRepository: TransactionRepository,
|
||||
private val allowanceRepository: AllowanceRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -16,9 +19,9 @@ class GetAllowanceUseCase(
|
|||
spenderAddress: String,
|
||||
): Either<Throwable, BigDecimal> {
|
||||
return Either.catch {
|
||||
transactionRepository.getAllowance(
|
||||
allowanceRepository.getAllowance(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency as CryptoCurrency.Token,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
spenderAddress = spenderAddress,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection
|
|||
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
|
||||
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
|
|
@ -83,7 +84,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
private val selectInitialPairUseCase: SelectInitialPairUseCase,
|
||||
private val getSwapQuoteUseCase: GetSwapQuoteUseCase,
|
||||
private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener,
|
||||
private val getAllowanceUseCase: GetAllowanceUseCase,
|
||||
private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val swapBestRateAnimationStore: SwapBestRateAnimationStore,
|
||||
|
|
@ -833,18 +834,15 @@ internal class SwapAmountModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun checkAllowance(state: SwapAmountUM.Content, quote: SwapQuoteModel): Boolean {
|
||||
val allowanceContract = quote.allowanceContract
|
||||
val allowance = if (allowanceContract != null) {
|
||||
getAllowanceUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = state.primaryCryptoCurrencyStatus.currency,
|
||||
spenderAddress = allowanceContract,
|
||||
).getOrNull()
|
||||
} else {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
val allowanceContract = quote.allowanceContract ?: return false
|
||||
val allowance = getAllowanceInfoUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
cryptoCurrency = state.primaryCryptoCurrencyStatus.currency,
|
||||
spenderAddress = allowanceContract,
|
||||
requiredAmount = state.primaryCryptoCurrencyStatus.value.amount.orZero(),
|
||||
).getOrNull()
|
||||
|
||||
return allowance.orZero() < state.primaryCryptoCurrencyStatus.value.amount.orZero()
|
||||
return allowance !is AllowanceInfo.Enough
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@ import arrow.core.raise.catch
|
|||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
|
|
@ -27,7 +23,6 @@ import com.tangem.domain.exchange.RampStateManager
|
|||
import com.tangem.domain.express.models.ExpressOperationType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.feature.swap.converters.*
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
|
|
@ -36,12 +31,11 @@ import com.tangem.feature.swap.domain.models.ExpressException
|
|||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
import java.util.UUID
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo
|
||||
|
||||
|
|
@ -410,36 +404,6 @@ internal class DefaultSwapRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
tokenDecimalCount: Int,
|
||||
tokenAddress: String,
|
||||
spenderAddress: String,
|
||||
): BigDecimal {
|
||||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
val result = (walletManager as? Approver)?.getAllowance(
|
||||
spenderAddress,
|
||||
Token(
|
||||
symbol = blockchain.currency,
|
||||
contractAddress = tokenAddress,
|
||||
decimals = tokenDecimalCount,
|
||||
),
|
||||
) ?: error("Cannot cast to Approver")
|
||||
|
||||
return result.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { error(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDataError(ex: Exception): ExpressDataError {
|
||||
return if (ex is ApiResponseError.HttpException) {
|
||||
errorsDataConverter.convert(ex.errorBody.orEmpty())
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
|||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
|
|
@ -112,6 +113,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase,
|
||||
@Assisted private val userWalletId: UserWalletId,
|
||||
) : SwapInteractor {
|
||||
|
||||
|
|
@ -431,12 +433,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
val isAllowedToSpend = maybeQuotes.fold(
|
||||
ifRight = { quotes ->
|
||||
quotes.allowanceContract?.let { allowanceContract ->
|
||||
isAllowedToSpend(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken.currency,
|
||||
amount = amount,
|
||||
getAllowanceInfoUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = fromToken.currency,
|
||||
spenderAddress = allowanceContract,
|
||||
)
|
||||
requiredAmount = amount.value,
|
||||
).getOrNull() is AllowanceInfo.Enough
|
||||
} != false
|
||||
},
|
||||
ifLeft = { false },
|
||||
|
|
@ -1268,25 +1270,6 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
?: error("Unable to create network coin with ID: ${network.id}")
|
||||
}
|
||||
|
||||
private suspend fun isAllowedToSpend(
|
||||
networkId: String,
|
||||
fromToken: CryptoCurrency,
|
||||
amount: SwapAmount,
|
||||
spenderAddress: String,
|
||||
): Boolean {
|
||||
if (fromToken is CryptoCurrency.Coin) return true
|
||||
|
||||
val allowance = repository.getAllowance(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = networkId,
|
||||
derivationPath = fromToken.network.derivationPath.value,
|
||||
tokenDecimalCount = fromToken.decimals,
|
||||
tokenAddress = getTokenAddress(fromToken),
|
||||
spenderAddress = spenderAddress,
|
||||
)
|
||||
return allowance >= amount.value
|
||||
}
|
||||
|
||||
private suspend fun createEmptyAmountState(): SwapState {
|
||||
val appCurrency = getSelectedAppCurrencyUseCase.unwrap()
|
||||
return SwapState.EmptyAmountState(
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import arrow.core.Either
|
|||
import com.tangem.domain.express.models.ExpressOperationType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SwapRepository {
|
||||
|
||||
|
|
@ -41,17 +39,6 @@ interface SwapRepository {
|
|||
rateType: RateType,
|
||||
): Either<ExpressDataError, QuoteModel>
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Throws(IllegalStateException::class)
|
||||
suspend fun getAllowance(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: String,
|
||||
derivationPath: String?,
|
||||
tokenDecimalCount: Int,
|
||||
tokenAddress: String,
|
||||
spenderAddress: String,
|
||||
): BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun getExchangeData(
|
||||
userWallet: UserWallet,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ object BlockchainUtils {
|
|||
private const val XRP_X_ADDRESS = 'X'
|
||||
private const val TERRA_CLASSIC_USD_COIN_ID = "terrausd"
|
||||
private const val TERRA_LUNA_CLASSIC_COIN_ID = "terra-luna"
|
||||
private const val TETHER_CONTRACT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
|
||||
const val SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES = 930
|
||||
|
||||
/** Decodes XRP Blockchain address */
|
||||
|
|
@ -217,4 +218,13 @@ object BlockchainUtils {
|
|||
coinId == TERRA_CLASSIC_USD_COIN_ID ||
|
||||
coinId == TERRA_LUNA_CLASSIC_COIN_ID
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given coin is Tether on Ethereum network, which may require special handling in some cases.
|
||||
*/
|
||||
fun isTetherInEthereum(blockchainId: String, contractAddress: String): Boolean {
|
||||
val blockchain = Blockchain.fromId(blockchainId)
|
||||
return (blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet) &&
|
||||
contractAddress.equals(TETHER_CONTRACT_ADDRESS, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue