Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-08 16:58:37 +03:00
parent 87c43ab20f
commit 8261e72fec
25 changed files with 269 additions and 48 deletions

View file

@ -11,6 +11,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
@ -29,7 +30,6 @@ import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.moshi.BigDecimalAdapter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction

View file

@ -7,6 +7,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
@ -105,7 +106,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.Details -> SwapFragment()//DetailsFragment()
AppScreen.DetailsSecurity -> SecurityModeFragment()
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> AppSettingsFragment()

View file

@ -63,6 +63,8 @@ dependencies {
implementation(Library.okHttp)
implementation(Library.okHttpLogging)
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.14.0")
/** Time */
implementation(Library.jodatime)
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common.moshi
package com.tangem.datasource.api.common
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson

View file

@ -15,7 +15,8 @@ class TangemTechService(
private val logIsEnabled: Boolean = false,
) {
private val headerInterceptors = mutableListOf<AddHeaderInterceptor>(
private val
headerInterceptors = mutableListOf<AddHeaderInterceptor>(
CacheControlHttpInterceptor(cacheMaxAge),
)

View file

@ -3,6 +3,7 @@ package com.tangem.datasource.di
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
@ -27,7 +28,12 @@ class NetworkModule {
fun provideTangemTechApi(okHttpClient: OkHttpClient): TangemTechApi {
return Retrofit.Builder()
.addConverterFactory(
MoshiConverterFactory.create(),
MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(BigDecimalAdapter())
.build(),
),
)
.baseUrl(DEV_TANGEM_TECH_BASE_URL)
.client(okHttpClient)
@ -80,6 +86,6 @@ 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 = ""
const val ONE_INCH_ETHER_BASE_URL = "https://api.1inch.io/swagger/ethereum-json/"
}
}

View file

@ -31,6 +31,8 @@ dependencies {
implementation(project(":core:utils"))
implementation(project(":features:swap:domain"))
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.14.0")
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)

View file

@ -7,9 +7,10 @@ import com.tangem.feature.swap.converters.QuotesConverter
import com.tangem.feature.swap.converters.SwapConverter
import com.tangem.feature.swap.converters.TokensConverter
import com.tangem.feature.swap.domain.SwapRepository
import com.tangem.feature.swap.domain.models.ApproveModel
import com.tangem.feature.swap.domain.models.Currency
import com.tangem.feature.swap.domain.models.QuoteModel
import com.tangem.feature.swap.domain.models.data.ApproveModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.SwapDataModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -64,16 +65,16 @@ class SwapRepositoryImpl @Inject constructor(
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
fromAddress: String,
fromWalletAddress: String,
slippage: Int,
) {
): SwapDataModel {
return withContext(coroutineDispatcher.io) {
swapConverter.convert(
oneInchApi.swap(
fromTokenAddress = fromTokenAddress,
toTokenAddress = toTokenAddress,
amount = amount,
fromAddress = fromAddress,
fromAddress = fromWalletAddress,
slippage = slippage,
),
)

View file

@ -1,7 +1,7 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse
import com.tangem.feature.swap.domain.models.ApproveModel
import com.tangem.feature.swap.domain.models.data.ApproveModel
import com.tangem.utils.converter.Converter
import javax.inject.Inject

View file

@ -1,10 +1,11 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.feature.swap.domain.models.QuoteModel
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.utils.converter.Converter
import javax.inject.Inject
class QuotesConverter : Converter<QuoteResponse, QuoteModel> {
class QuotesConverter @Inject constructor(): Converter<QuoteResponse, QuoteModel> {
override fun convert(value: QuoteResponse): QuoteModel {
return QuoteModel(

View file

@ -1,24 +1,32 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.oneinch.models.SwapResponse
import com.tangem.feature.swap.domain.models.SwapTransactionModel
import com.tangem.datasource.api.oneinch.models.TransactionDto
import com.tangem.feature.swap.domain.models.data.SwapDataModel
import com.tangem.feature.swap.domain.models.data.TransactionModel
import com.tangem.utils.converter.Converter
import javax.inject.Inject
class SwapConverter @Inject constructor() : Converter<SwapResponse, SwapTransactionModel> {
class SwapConverter @Inject constructor() : Converter<SwapResponse, SwapDataModel> {
override fun convert(value: SwapResponse): SwapTransactionModel {
return SwapTransactionModel(
override fun convert(value: SwapResponse): SwapDataModel {
return SwapDataModel(
fromTokenAddress = value.fromToken.address,
toTokenAddress = value.toToken.address,
toTokenAmount = value.fromTokenAmount,
fromTokenAmount = value.toTokenAmount,
fromWalletAddress = value.transaction.fromAddress,
toWalletAddress = value.transaction.toAddress,
data = value.transaction.data,
value = value.transaction.value,
gasPrice = value.transaction.gasPrice,
gas = value.transaction.gas,
transaction = convertTransaction(value.transaction),
)
}
private fun convertTransaction(transactionDto: TransactionDto): TransactionModel {
return TransactionModel(
fromWalletAddress = transactionDto.fromAddress,
toWalletAddress = transactionDto.toAddress,
data = transactionDto.data,
value = transactionDto.value,
gasPrice = transactionDto.gasPrice,
gas = transactionDto.gas,
)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.swap.converters
import com.tangem.datasource.api.tangemTech.CoinsResponse
import com.tangem.feature.swap.domain.models.Currency
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.utils.converter.Converter
import javax.inject.Inject
@ -17,7 +17,7 @@ class TokensConverter @Inject constructor() : Converter<CoinsResponse.Coin, Curr
networkId = network.networkId,
contractAddress = network.contractAddress!!,
decimalCount = network.decimalCount!!.intValueExact(),
logoUrl = "",//todo add logo
logoUrl = getSmallIconUrl(value.id),
)
} else {
Currency.NativeToken(
@ -25,8 +25,17 @@ class TokensConverter @Inject constructor() : Converter<CoinsResponse.Coin, Curr
name = value.name,
symbol = value.symbol,
networkId = network.networkId,
logoUrl = "", //todo add logo
logoUrl = getSmallIconUrl(value.id),
)
}
}
private fun getSmallIconUrl(coin: String): String {
return "$DEFAULT_IMAGE_HOST$SMALL_ICON_PATH/$coin.png"
}
companion object {
private const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
private const val SMALL_ICON_PATH = "small"
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.feature.swap.di
import com.tangem.datasource.api.oneinch.OneInchApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.qualifiers.OneInchEthereum
import com.tangem.feature.swap.SwapRepositoryImpl
import com.tangem.feature.swap.converters.ApproveConverter
import com.tangem.feature.swap.converters.QuotesConverter
import com.tangem.feature.swap.converters.SwapConverter
import com.tangem.feature.swap.converters.TokensConverter
import com.tangem.feature.swap.domain.SwapRepository
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)
class SwapDataModule {
@Provides
@Singleton
fun provideSwapRepository(
tangemTechApi: TangemTechApi,
@OneInchEthereum oneInchApi: OneInchApi,
tokensConverter: TokensConverter,
quotesConverter: QuotesConverter,
swapConverter: SwapConverter,
approveConverter: ApproveConverter,
coroutineDispatcher: CoroutineDispatcherProvider,
): SwapRepository {
return SwapRepositoryImpl(
tangemTechApi = tangemTechApi,
oneInchApi = oneInchApi,
tokensConverter = tokensConverter,
quotesConverter = quotesConverter,
swapConverter = swapConverter,
approveConverter = approveConverter,
coroutineDispatcher = coroutineDispatcher,
)
}
}

View file

@ -1,4 +1,20 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.SwapResultModel
interface SwapInteractor {
suspend fun getTokensToSwap(networkId: String): List<Currency>
suspend fun getTokenBalance(): String
suspend fun findBestQuote(
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
): QuoteModel
suspend fun onSwap(): SwapResultModel
}

View file

@ -0,0 +1,65 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.SwapDataHolder
import com.tangem.feature.swap.domain.models.SwapResultModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.TransactionModel
//todo add @Inject after merge referral and add HiltCore
class SwapInteractorImpl(
private val repository: SwapRepository,
) : SwapInteractor {
private var lastDataForSwap: SwapDataHolder? = null
override suspend fun getTokensToSwap(networkId: String): List<Currency> {
return repository.getExchangeableTokens(networkId)
}
override suspend fun getTokenBalance(): String {
TODO("Not yet implemented")
}
override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): QuoteModel {
repository.findBestQuote(fromTokenAddress, toTokenAddress, amount).let {
lastDataForSwap = SwapDataHolder(it, amount)
return it
}
}
override suspend fun onSwap(): SwapResultModel {
val dataForSwap = lastDataForSwap
if (dataForSwap != null) {
val swapData = repository.prepareSwapTransaction(
fromTokenAddress = dataForSwap.quoteModel.fromTokenAmount,
toTokenAddress = dataForSwap.quoteModel.toTokenAmount,
amount = dataForSwap.amount,
slippage = DEFAULT_SLIPPAGE,
fromWalletAddress = getWalletAddress(),
)
signTransactionData(swapData.transaction) //todo implement
return SwapResultModel.SwapSuccess(
dataForSwap.quoteModel.fromTokenAmount,
dataForSwap.quoteModel.toTokenAmount,
)
}
return SwapResultModel.SwapError(
0,
"",
)
}
//todo implement after merge referral
private fun getWalletAddress(): String {
return ""
}
//todo implement after merge referral
private fun signTransactionData(transaction: TransactionModel) {
}
companion object {
private const val DEFAULT_SLIPPAGE = 1
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.ApproveModel
import com.tangem.feature.swap.domain.models.Currency
import com.tangem.feature.swap.domain.models.QuoteModel
import com.tangem.feature.swap.domain.models.data.ApproveModel
import com.tangem.feature.swap.domain.models.data.Currency
import com.tangem.feature.swap.domain.models.data.QuoteModel
import com.tangem.feature.swap.domain.models.data.SwapDataModel
interface SwapRepository {
@ -46,7 +47,7 @@ interface SwapRepository {
fromTokenAddress: String,
toTokenAddress: String,
amount: String,
fromAddress: String,
fromWalletAddress: String,
slippage: Int,
)
): SwapDataModel
}

View file

@ -0,0 +1,8 @@
package com.tangem.feature.swap.domain.models
import com.tangem.feature.swap.domain.models.data.QuoteModel
data class SwapDataHolder(
val quoteModel: QuoteModel,
val amount: String,
)

View file

@ -0,0 +1,14 @@
package com.tangem.feature.swap.domain.models
sealed interface SwapResultModel {
data class SwapSuccess(
val fromTokenAmount: String,
val toTokenAmount: String,
) : SwapResultModel
data class SwapError(
val errorCode: Int,
val errorMessage: String,
) : SwapResultModel
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.data
/**
* Approve model

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.data
sealed class Currency(
open val id: String,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.data
data class QuoteModel(
val fromTokenAmount: String,

View file

@ -0,0 +1,17 @@
package com.tangem.feature.swap.domain.models.data
/**
* Swap transaction model
*
* @property fromTokenAddress token from which want to convert
* @property toTokenAddress token to want to convert
* @property toTokenAmount amount "target" token
* @property fromTokenAmount amount "initial" token
*/
data class SwapDataModel(
val fromTokenAddress: String,
val toTokenAddress: String,
val toTokenAmount: String,
val fromTokenAmount: String,
val transaction: TransactionModel
)

View file

@ -1,25 +1,16 @@
package com.tangem.feature.swap.domain.models
package com.tangem.feature.swap.domain.models.data
/**
* Swap transaction model
* Transaction model
*
* @property fromTokenAddress token from which want to convert
* @property toTokenAddress token to want to convert
* @property toTokenAmount amount "target" token
* @property fromTokenAmount amount "initial" token
* @property fromWalletAddress wallet from address (transactions will be sent from this address)
* @property toWalletAddress transactions will be sent to our(1inch) contract address
* @property data The encoded data to call the approve method on the swapped token contract
* @property value Native token value in WEI (for approve is always 0)
* @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000
* @property gas estimated amount of the gas limit, increase this value by 25%
* @constructor Create empty Swap transaction model
*/
data class SwapTransactionModel(
val fromTokenAddress: String,
val toTokenAddress: String,
val toTokenAmount: String,
val fromTokenAmount: String,
data class TransactionModel(
val fromWalletAddress: String,
val toWalletAddress: String,
val data: String,

View file

@ -49,6 +49,7 @@ dependencies {
/** Domain */
implementation(project(":features:swap:domain"))
implementation(project(":features:swap:data"))//todo remove, only for test
/** Other libraries */
implementation(Library.composeShimmer)

View file

@ -0,0 +1,33 @@
package com.tangem.feature.swap.presentation
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.tangem.feature.swap.domain.SwapInteractorImpl
import com.tangem.feature.swap.domain.SwapRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class SwapFragment : Fragment() {
@Inject
lateinit var swapRepository: SwapRepository
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//test
val swapInteractor = SwapInteractorImpl(swapRepository)
CoroutineScope(Dispatchers.IO).launch {
Log.d("test", swapInteractor.getTokensToSwap("ethereum").toString())
}
return super.onCreateView(inflater, container, savedInstanceState)
}
}