Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-22 16:25:58 +03:00
parent 9cf90df510
commit 1c5519a702
19 changed files with 327 additions and 83 deletions

View file

@ -79,6 +79,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
appStateHolder.tangemSdkManager = tangemSdkManager
appStateHolder.tangemSdk = tangemSdk
backupService = BackupService.init(tangemSdk, this)
userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
context = applicationContext,

View file

@ -1,5 +1,6 @@
package com.tangem.tap.proxy
import com.tangem.TangemSdk
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.AppState
@ -7,18 +8,20 @@ import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.features.wallet.redux.WalletState
import org.rekotlin.Store
import javax.inject.Inject
/**
* Holds objects from old modules, that missing in DI graph
* Object sets manually to use in new modules and [AppStateHolder] proxies its to DI
*/
class AppStateHolder {
class AppStateHolder @Inject constructor(){
var scanResponse: ScanResponse? = null
var walletState: WalletState? = null
var userTokensRepository: UserTokensRepository? = null
var mainStore: Store<AppState>? = null
var tangemSdkManager: TangemSdkManager? = null
var tangemSdk: TangemSdk? = null
fun getActualCard(): CardDTO? {
return scanResponse?.card

View file

@ -0,0 +1,133 @@
package com.tangem.tap.proxy
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.Message
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NativeToken
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
class TransactionManagerImpl(
private val appStateHolder: AppStateHolder,
) : TransactionManager {
@Throws(IllegalStateException::class)
override suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
currencyToSend: Currency,
feeAmount: BigDecimal,
destinationAddress: String,
dataToSign: String,
): SendTxResult {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain)
val amount = when (currencyToSend) {
is NativeToken -> {
Amount(value = amountToSend, blockchain = blockchain)
}
is NonNativeToken -> {
Amount(
convertNonNativeToken(currencyToSend),
amountToSend,
)
}
}
val txData = walletManager.createTransaction(
amount = amount,
fee = Amount(value = feeAmount, blockchain = blockchain),
destination = destinationAddress,
).copy(hash = dataToSign)
val signer = transactionSigner(walletManager)
val sendResult = try {
(walletManager as TransactionSender).send(txData, signer)
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
return SendTxResult.UnknownError(ex)
}
return handleSendResult(sendResult)
}
private fun handleSendResult(result: SimpleResult): SendTxResult {
when (result) {
is SimpleResult.Success -> {
return SendTxResult.Success
}
is SimpleResult.Failure -> {
val error = (result.error as? BlockchainSdkError) ?: return SendTxResult.UnknownError()
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = (error.tangemError as? TangemSdkError)
?: return SendTxResult.UnknownError()
if (tangemSdkError is TangemSdkError.UserCancelled) return SendTxResult.UserCancelledError
return SendTxResult.TangemSdkError(tangemSdkError.code, tangemSdkError.cause)
}
else -> {
return SendTxResult.TangemSdkError(error.code, error.cause)
}
}
}
}
}
private fun transactionSigner(walletManager: WalletManager): TransactionSigner {
val actualCard = requireNotNull(appStateHolder.getActualCard()) { "no card found" }
return TangemSigner(
card = actualCard,
tangemSdk = tangemSdk,
initialMessage = Message("test transaction title", "test description"),
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures,
),
)
}
}
private fun getActualWalletManager(blockchain: Blockchain): WalletManager {
val card = appStateHolder.getActualCard()
if (card != null) {
val blockchainNetwork = BlockchainNetwork(blockchain, card)
val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork)
if (walletManager != null) {
return walletManager
} else {
error("no wallet manager found")
}
} else {
error("card not found")
}
}
private fun convertNonNativeToken(token: NonNativeToken): Token {
return Token(
name = token.name,
symbol = token.symbol,
contractAddress = token.contractAddress,
decimals = token.decimalCount,
id = token.id,
)
}
}

View file

@ -2,9 +2,11 @@ package com.tangem.tap.proxy.di
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.DerivationManagerImpl
import com.tangem.tap.proxy.TransactionManagerImpl
import com.tangem.tap.proxy.UserWalletManagerImpl
import dagger.Module
import dagger.Provides
@ -31,6 +33,12 @@ class ProxyModule {
)
}
@Provides
@Singleton
fun provideTransactionManager(appStateHolder: AppStateHolder): TransactionManager {
return TransactionManagerImpl(appStateHolder)
}
@Provides
@Singleton
fun provideDerivationManager(appStateHolder: AppStateHolder): DerivationManager {

View file

@ -4,10 +4,8 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.datasource.api.AuthHeaderInterceptor
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.referral.ReferralApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.qualifiers.OneInchEthereum
import com.tangem.lib.auth.AuthProvider
import dagger.Module
import dagger.Provides
@ -39,26 +37,6 @@ class NetworkModule {
.create(TangemTechApi::class.java)
}
@Provides
@Singleton
@OneInchEthereum
fun provideOneInchEthereumApi(moshi: Moshi): OneInchApi {
return Retrofit.Builder()
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(ONE_INCH_ETHER_BASE_URL)
.client(
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY),
)
.build(),
)
.build()
.create(OneInchApi::class.java)
}
@Provides
@Singleton
fun provideReferralApi(okHttpClient: OkHttpClient, moshi: Moshi): ReferralApi {
@ -95,6 +73,5 @@ class NetworkModule {
private companion object {
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
const val ONE_INCH_ETHER_BASE_URL = "https://api.1inch.io/v5.0/1/"
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.di
import com.tangem.datasource.api.oneinch.OneInchApi
class OneInchApiFactory {
private val oneInchApiMap = mutableMapOf<String, OneInchApi>()
fun putApi(networkId: String, api: OneInchApi) {
oneInchApiMap[networkId] = api
}
fun getApi(networkId: String): OneInchApi {
return oneInchApiMap[networkId] ?: error("no api found for networkId $networkId")
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.oneinch.OneInchApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class OneInchApisModule {
@Provides
@Singleton
fun provideOneInchApiFactory(moshi: Moshi): OneInchApiFactory {
val apiFactory = OneInchApiFactory()
apiFactory.putApi(ETH_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_ETH_PATH, moshi))
apiFactory.putApi(BSC_NETWORK, createOneInchApiWithUrl(ONE_INCH_BASE_URL + ONE_INCH_BSC_PATH, moshi))
return apiFactory
}
private fun createOneInchApiWithUrl(url: String, moshi: Moshi): OneInchApi {
return Retrofit.Builder()
.addConverterFactory(
MoshiConverterFactory.create(moshi),
)
.baseUrl(url)
.client(
OkHttpClient.Builder()
.addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY),
)
.build(),
)
.build()
.create(OneInchApi::class.java)
}
companion object {
private const val ONE_INCH_BASE_URL = "https://api.1inch.io/v5.0/"
private const val ONE_INCH_ETH_PATH = "1/"
private const val ONE_INCH_BSC_PATH = "56/"
private const val ETH_NETWORK = "ethereum"
private const val BSC_NETWORK = "binance-smart-chain"
}
}

View file

@ -1,15 +0,0 @@
package com.tangem.datasource.di.qualifiers
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OneInchEthereum
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OneInchBinance
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OneInchOptimism

View file

@ -5,6 +5,7 @@ import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.NativeToken
import com.tangem.lib.crypto.models.NonNativeToken
import com.tangem.utils.converter.Converter
import javax.inject.Inject
class TokensConverter @Inject constructor() : Converter<TokenData, Currency> {

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.oneinch.errors.OneIncResponseException
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.OneInchApiFactory
import com.tangem.feature.swap.converters.ApproveConverter
import com.tangem.feature.swap.converters.QuotesConverter
import com.tangem.feature.swap.converters.SwapConverter
@ -21,7 +22,7 @@ import javax.inject.Inject
internal class SwapRepositoryImpl @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val oneInchApi: OneInchApi,
private val oneInchApiFactory: OneInchApiFactory,
private val tokensConverter: TokensConverter,
private val quotesConverter: QuotesConverter,
private val swapConverter: SwapConverter,
@ -36,12 +37,16 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String):
override suspend fun findBestQuote(
networkId: String, fromTokenAddress: String, toTokenAddress: String,
amount:
String,
):
AggregatedSwapDataModel<QuoteModel> {
return withContext(coroutineDispatcher.io) {
try {
val response = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.quote(
getOneInchApi(networkId).quote(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
@ -54,24 +59,24 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
override suspend fun addressForTrust(): String {
override suspend fun addressForTrust(networkId: String): String {
return withContext(coroutineDispatcher.io) {
oneInchApi.approveSpender().address
getOneInchApi(networkId).approveSpender().address
}
}
override suspend fun dataToApprove(tokenAddress: String, amount: String?): ApproveModel {
override suspend fun dataToApprove(networkId: String, tokenAddress: String, amount: String?): ApproveModel {
return withContext(coroutineDispatcher.io) {
approveConverter.convert(oneInchApi.approveTransaction(tokenAddress, amount))
approveConverter.convert(getOneInchApi(networkId).approveTransaction(tokenAddress, amount))
}
}
override suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String):
override suspend fun checkTokensSpendAllowance(networkId: String, tokenAddress: String, walletAddress: String):
AggregatedSwapDataModel<String> {
return withContext(coroutineDispatcher.io) {
try {
val response = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.approveAllowance(tokenAddress, walletAddress)
getOneInchApi(networkId).approveAllowance(tokenAddress, walletAddress),
)
AggregatedSwapDataModel(response.allowance)
} catch (ex: OneIncResponseException) {
@ -81,6 +86,7 @@ internal class SwapRepositoryImpl @Inject constructor(
}
override suspend fun prepareSwapTransaction(
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
@ -90,7 +96,7 @@ internal class SwapRepositoryImpl @Inject constructor(
return withContext(coroutineDispatcher.io) {
try {
val swapResponse = oneInchErrorsHandler.handleOneInchResponse(
oneInchApi.swap(
getOneInchApi(networkId).swap(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
@ -104,4 +110,8 @@ internal class SwapRepositoryImpl @Inject constructor(
}
}
}
private fun getOneInchApi(networkId: String): OneInchApi {
return oneInchApiFactory.getApi(networkId)
}
}

View file

@ -1,9 +1,8 @@
package com.tangem.feature.swap.di
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.oneinch.OneInchErrorsHandler
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.qualifiers.OneInchEthereum
import com.tangem.datasource.di.OneInchApiFactory
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.converters.ApproveConverter
import com.tangem.feature.swap.converters.QuotesConverter
@ -25,7 +24,7 @@ class SwapDataModule {
@Singleton
fun provideSwapRepository(
tangemTechApi: TangemTechApi,
@OneInchEthereum oneInchApi: OneInchApi,
oneInchApiFactory: OneInchApiFactory,
tokensConverter: TokensConverter,
quotesConverter: QuotesConverter,
swapConverter: SwapConverter,
@ -35,7 +34,7 @@ class SwapDataModule {
): SwapRepository {
return SwapRepositoryImpl(
tangemTechApi = tangemTechApi,
oneInchApi = oneInchApi,
oneInchApiFactory = oneInchApiFactory,
tokensConverter = tokensConverter,
quotesConverter = quotesConverter,
swapConverter = swapConverter,

View file

@ -5,10 +5,13 @@ import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.DataError
import com.tangem.feature.swap.domain.models.data.SwapState
import com.tangem.feature.swap.domain.models.data.TransactionModel
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import java.math.BigDecimal
import javax.inject.Inject
internal class SwapInteractorImpl @Inject constructor(
private val transactionManager: TransactionManager,
private val userWalletManager: UserWalletManager,
private val repository: SwapRepository,
private val cache: SwapDataCache,
@ -29,21 +32,40 @@ internal class SwapInteractorImpl @Inject constructor(
}
override suspend fun givePermissionToSwap(tokenAddress: String) {
val oneInchAddress = repository.addressForTrust()
val transactionData = repository.dataToApprove(tokenAddress)
//todo sign transaction (next tasks)
cache.getNetworkId()?.let { networkId ->
val oneInchAddress = repository.addressForTrust(networkId)
val transactionData = repository.dataToApprove(networkId, tokenAddress)
//todo for test
// transactionManager.sendTransaction(
// networkId = networkId,
// amountToSend = BigDecimal.ZERO,
// currencyToSend = ,
// feeAmount = BigDecimal.ZERO,
// destinationAddress = oneInchAddress,
// dataToSign = transactionData.data
// )
}
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): SwapState {
val networkId = cache.getNetworkId()
val isAllowedToSpend = if (networkId.isNullOrEmpty()) {
false
error("no networkId found, please call getTokensToSwap first")
} else {
val allowance =
repository.checkTokensSpendAllowance(fromTokenAddress, userWalletManager.getWalletAddress(networkId))
repository.checkTokensSpendAllowance(
networkId = networkId,
tokenAddress = fromTokenAddress,
walletAddress = userWalletManager.getWalletAddress(networkId),
)
allowance.error == DataError.NO_ERROR && allowance.dataModel != "0"
}
repository.findBestQuote(fromTokenAddress, toTokenAddress, amount).let { quotes ->
repository.findBestQuote(
networkId = networkId,
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
).let { quotes ->
val quoteDataModel = quotes.dataModel
if (quoteDataModel != null) {
cache.cacheSwapParams(quoteDataModel.copy(isAllowedToSpend = isAllowedToSpend), amount)
@ -60,6 +82,7 @@ internal class SwapInteractorImpl @Inject constructor(
val networkId = cache.getNetworkId()
if (quoteModel != null && amountToSwap != null && networkId != null) {
repository.prepareSwapTransaction(
networkId = networkId,
fromTokenAddress = quoteModel.fromTokenAmount,
toTokenAddress = quoteModel.toTokenAmount,
amount = amountToSwap,

View file

@ -11,6 +11,7 @@ interface SwapRepository {
suspend fun getExchangeableTokens(networkId: String): List<Currency>
suspend fun findBestQuote(
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
@ -21,7 +22,7 @@ interface SwapRepository {
*
* @return address
*/
suspend fun addressForTrust(): String
suspend fun addressForTrust(networkId: String): String
/**
* Generate "data" for calling contract in order to allow 1inch spend funds
@ -30,6 +31,7 @@ interface SwapRepository {
* @param amount number of tokens is allowed. By default infinite
*/
suspend fun dataToApprove(
networkId: String,
tokenAddress: String,
amount: String? = null,
): ApproveModel
@ -42,9 +44,14 @@ interface SwapRepository {
*
* @return amount of tokens allowed to spend
*/
suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String): AggregatedSwapDataModel<String>
suspend fun checkTokensSpendAllowance(
networkId: String,
tokenAddress: String,
walletAddress: String,
): AggregatedSwapDataModel<String>
suspend fun prepareSwapTransaction(
networkId: String,
fromTokenAddress: String,
toTokenAddress: String,
amount: String,

View file

@ -4,6 +4,7 @@ import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.SwapInteractorImpl
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.cache.SwapDataCacheImpl
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import dagger.Module
import dagger.Provides
@ -18,12 +19,14 @@ class SwapDomainModule {
@Provides
@Singleton
fun provideSwapInteractor(
referralRepository: SwapRepository,
swapRepository: SwapRepository,
userWalletManager: UserWalletManager,
transactionManager: TransactionManager,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
userWalletManager = userWalletManager,
repository = referralRepository,
repository = swapRepository,
cache = SwapDataCacheImpl(),
)
}

View file

@ -0,0 +1,17 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
interface TransactionManager {
suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
currencyToSend: Currency,
feeAmount: BigDecimal,
destinationAddress: String,
dataToSign: String,
): SendTxResult
}

View file

@ -4,9 +4,25 @@ package com.tangem.lib.crypto.models
* Currency data class that can be native blockchain Token
* or custom Token (used in this lib to replace and divide logic Currency from app module)
*/
abstract class Currency(
sealed class Currency(
open val id: String,
open val name: String,
open val symbol: String,
open val networkId: String,
)
)
data class NativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
) : Currency(id, name, symbol, networkId)
class NonNativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
val contractAddress: String,
val decimalCount: Int,
) : Currency(id, name, symbol, networkId)

View file

@ -1,8 +0,0 @@
package com.tangem.lib.crypto.models
data class NativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
) : Currency(id, name, symbol, networkId)

View file

@ -1,10 +0,0 @@
package com.tangem.lib.crypto.models
class NonNativeToken(
override val id: String,
override val name: String,
override val symbol: String,
override val networkId: String,
val contractAddress: String,
val decimalCount: Int,
) : Currency(id, name, symbol, networkId)

View file

@ -0,0 +1,10 @@
package com.tangem.lib.crypto.models.transactions
sealed interface SendTxResult {
object Success : SendTxResult
object UserCancelledError : SendTxResult
data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTxResult
data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTxResult
data class UnknownError(val ex: Exception? = null) : SendTxResult
}