Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-07 14:14:11 +03:00
parent a10c1a8505
commit 710e944eae
14 changed files with 238 additions and 38 deletions

View file

@ -7,7 +7,7 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class BigDecimalAdapter {
internal class BigDecimalAdapter {
@FromJson
fun fromJson(value: String) = BigDecimal(value)

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.*
import org.joda.time.DateTime
internal class DateTimeAdapter : JsonAdapter<DateTime>() {
@FromJson
override fun fromJson(reader: JsonReader): DateTime? {
val dateString = reader.nextString() ?: return null
return DateTime.parse(dateString)
}
@ToJson
override fun toJson(writer: JsonWriter, value: DateTime?) {
if (value != null) {
writer.value(value.toString())
} else {
writer.nullValue()
}
}
}

View file

@ -1,14 +1,10 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import com.squareup.moshi.*
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat
class LocalDateAdapter : JsonAdapter<LocalDate>() {
internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")

View file

@ -6,7 +6,7 @@ import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
internal class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.DateTimeAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import dagger.Module
import dagger.Provides
@ -22,6 +23,7 @@ class MoshiModule {
return Moshi.Builder()
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}

View file

@ -1,7 +1,7 @@
package com.tangem.data.visa.utils
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.lib.visa.model.BalancesAndLimits
import com.tangem.lib.visa.model.VisaBalancesAndLimits
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Instant
@ -10,7 +10,7 @@ import java.math.BigInteger
internal class VisaCurrencyFactory {
fun create(balancesAndLimits: BalancesAndLimits, fiatRate: BigDecimal?): VisaCurrency {
fun create(balancesAndLimits: VisaBalancesAndLimits, fiatRate: BigDecimal?): VisaCurrency {
val now = Instant.now()
val currentLimit = if (balancesAndLimits.limitsChangeDate > now) {
balancesAndLimits.oldLimits
@ -43,7 +43,7 @@ internal class VisaCurrencyFactory {
)
}
private fun getRemainingOtp(currentLimit: BalancesAndLimits.Limits, now: Instant): BigDecimal {
private fun getRemainingOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent
}
@ -51,7 +51,7 @@ internal class VisaCurrencyFactory {
return currentLimit.spendLimit.limit
}
private fun getRemainingNoOtp(currentLimit: BalancesAndLimits.Limits, now: Instant): BigDecimal {
private fun getRemainingNoOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent
}
@ -59,7 +59,7 @@ internal class VisaCurrencyFactory {
return currentLimit.noOtpLimit.limit
}
private fun getLimitsExpirationDate(currentLimits: BalancesAndLimits.Limits, now: Instant): DateTime {
private fun getLimitsExpirationDate(currentLimits: VisaBalancesAndLimits.Limits, now: Instant): DateTime {
val expirationDate = if (currentLimits.expirationDate >= now) {
currentLimits.expirationDate.toDateTime()
} else {

View file

@ -1,17 +1,30 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.libs.visa"
}
dependencies {
/** Project */
implementation(projects.core.utils)
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Libs - Network */
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)
implementation(deps.okHttp.prettyLogging)
implementation(deps.retrofit)
implementation(deps.retrofit.moshi)
/** Libs - Other */
implementation(deps.web3j.core)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.fx)
implementation(deps.jodatime)
implementation(deps.okHttp.prettyLogging)
}

View file

@ -1,9 +1,9 @@
package com.tangem.lib.visa
import arrow.fx.coroutines.parZip
import com.tangem.lib.visa.model.BalancesAndLimits
import com.tangem.lib.visa.model.BalancesAndLimits.Balances
import com.tangem.lib.visa.model.BalancesAndLimits.Limits
import com.tangem.lib.visa.model.VisaBalancesAndLimits
import com.tangem.lib.visa.model.VisaBalancesAndLimits.Balances
import com.tangem.lib.visa.model.VisaBalancesAndLimits.Limits
import com.tangem.lib.visa.utils.toBigDecimal
import com.tangem.lib.visa.utils.toInstant
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -21,7 +21,8 @@ internal class DefaultVisaContractInfoProvider(
private val dispatchers: CoroutineDispatcherProvider,
) : VisaContractInfoProvider {
override suspend fun getBalancesAndLimits(walletAddress: String): BalancesAndLimits = withContext(dispatchers.io) {
override suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits {
return withContext(dispatchers.io) {
val tangemBridgeProcessor = TangemBridgeProcessor.load(
/* contractAddress = */ bridgeProcessorAddress,
/* web3j = */ web3j,
@ -38,6 +39,7 @@ internal class DefaultVisaContractInfoProvider(
},
)
}
}
private fun loadPaymentAccount(
walletAddress: String,
@ -62,12 +64,12 @@ internal class DefaultVisaContractInfoProvider(
private suspend fun fetchBalancesAndLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
): BalancesAndLimits = parZip(
): VisaBalancesAndLimits = parZip(
dispatchers.io,
{ fetchBalances(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken) },
{ balances, (oldLimit, newLimit, changeDate) ->
BalancesAndLimits(balances, oldLimit, newLimit, changeDate)
VisaBalancesAndLimits(balances, oldLimit, newLimit, changeDate)
},
)

View file

@ -2,7 +2,7 @@ package com.tangem.lib.visa
import com.ihsanbal.logging.Level
import com.ihsanbal.logging.LoggingInterceptor
import com.tangem.lib.visa.model.BalancesAndLimits
import com.tangem.lib.visa.model.VisaBalancesAndLimits
import com.tangem.lib.visa.utils.VisaConfig
import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG
import com.tangem.lib.visa.utils.toHexString
@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
interface VisaContractInfoProvider {
suspend fun getBalancesAndLimits(walletAddress: String): BalancesAndLimits
suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits
class Builder(
private val isNetworkLoggingEnabled: Boolean,

View file

@ -0,0 +1,16 @@
package com.tangem.lib.visa.api
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import retrofit2.http.GET
import retrofit2.http.Query
interface VisaApi {
@GET("transaction")
suspend fun getTxHistory(
@Query("card_public_key") cardPublicKey: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -0,0 +1,61 @@
package com.tangem.lib.visa.api
import android.util.Log
import com.ihsanbal.logging.Level
import com.ihsanbal.logging.LoggingInterceptor
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.lib.visa.utils.VisaConfig
import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
class VisaApiBuilder(
private val useDevApi: Boolean,
private val isNetworkLoggingEnabled: Boolean,
private val moshi: Moshi,
private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS,
) {
fun build(): VisaApi {
val okHttpClient = createOkHttpClient()
val retrofit = createRetrofit(okHttpClient)
return retrofit.create(VisaApi::class.java)
}
private fun createOkHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder().apply {
connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
writeTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
if (isNetworkLoggingEnabled) {
addInterceptor(createNetworkLoggingInterceptor())
}
}
return builder.build()
}
private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
val baseUrl = if (useDevApi) VisaConfig.VISA_API_DEV_URL else VisaConfig.VISA_API_PROD_URL
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(baseUrl)
.client(okHttpClient)
.build()
}
}
private fun createNetworkLoggingInterceptor(): Interceptor {
return LoggingInterceptor.Builder()
.setLevel(Level.BODY)
.log(Log.VERBOSE)
.tag(NETWORK_LOGS_TAG)
.build()
}

View file

@ -4,7 +4,7 @@ import org.joda.time.Instant
import java.math.BigDecimal
import java.math.BigInteger
data class BalancesAndLimits(
data class VisaBalancesAndLimits(
val balances: Balances,
val oldLimits: Limits,
val newLimits: Limits,

View file

@ -0,0 +1,84 @@
package com.tangem.lib.visa.model
import com.squareup.moshi.Json
import org.joda.time.DateTime
import java.math.BigDecimal
data class VisaTxHistoryResponse(
@Json(name = "card_wallet_address")
val cardWalletAddress: String,
@Json(name = "transactions")
val transactions: List<Transaction>,
) {
data class Transaction(
@Json(name = "auth_code")
val authCode: String?,
@Json(name = "billing_amount")
val billingAmount: BigDecimal,
@Json(name = "billing_currency_code")
val billingCurrencyCode: Int,
@Json(name = "blockchain_amount")
val blockchainAmount: BigDecimal,
@Json(name = "blockchain_coin_name")
val blockchainCoinName: String,
@Json(name = "blockchain_fee")
val blockchainFee: BigDecimal,
// @Json(name = "local_dt")
// val localDate: DateTime?,
@Json(name = "merchant_category_code")
val merchantCategoryCode: String?,
@Json(name = "merchant_city")
val merchantCity: String?,
@Json(name = "merchant_country_code")
val merchantCountryCode: String?,
@Json(name = "merchant_name")
val merchantName: String?,
@Json(name = "requests")
val requests: List<Request>,
@Json(name = "rrn")
val rrn: String?,
@Json(name = "transaction_amount")
val transactionAmount: BigDecimal,
@Json(name = "transaction_currency_code")
val transactionCurrencyCode: Int,
@Json(name = "transaction_dt")
val transactionDt: DateTime,
@Json(name = "transaction_id")
val transactionId: Long,
@Json(name = "transaction_status")
val transactionStatus: String,
@Json(name = "transaction_type")
val transactionType: String,
) {
data class Request(
@Json(name = "billing_amount")
val billingAmount: BigDecimal,
@Json(name = "billing_currency_code")
val billingCurrencyCode: Int,
@Json(name = "blockchain_amount")
val blockchainAmount: BigDecimal,
@Json(name = "blockchain_fee")
val blockchainFee: BigDecimal,
@Json(name = "error_code")
val errorCode: Int,
@Json(name = "request_dt")
val requestDt: DateTime,
@Json(name = "request_status")
val requestStatus: String,
@Json(name = "request_type")
val requestType: String,
@Json(name = "transaction_amount")
val transactionAmount: BigDecimal,
@Json(name = "transaction_currency_code")
val transactionCurrencyCode: Int,
@Json(name = "transaction_request_id")
val transactionRequestId: Long,
@Json(name = "tx_hash")
val txHash: String?,
@Json(name = "tx_status")
val txStatus: String?,
)
}
}

View file

@ -9,6 +9,9 @@ internal object VisaConfig {
const val GAS_LIMIT = 500_000_000L
const val PRIVATE_KEY_LENGTH = 32
const val VISA_API_PROD_URL = "https://payapi.tangem-tech.com/api/v1/"
const val VISA_API_DEV_URL = "[REDACTED_ENV_URL]"
const val NETWORK_TIMEOUT_SECONDS = 65L
const val NETWORK_LOGS_TAG = "VisaNetworkLogs"
}