diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt index 568b042179..e81aa15e06 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -class BigDecimalAdapter { +internal class BigDecimalAdapter { @FromJson fun fromJson(value: String) = BigDecimal(value) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt new file mode 100644 index 0000000000..38f4b81532 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.common + +import com.squareup.moshi.* +import org.joda.time.DateTime + +internal class DateTimeAdapter : JsonAdapter() { + + @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() + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt index b11c76fccd..95978206f2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt @@ -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() { +internal class LocalDateAdapter : JsonAdapter() { private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt index 11eca2e0d6..8f8c65e92d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt @@ -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, retrofit: Retrofit): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index bf3d94095f..c22d4433bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -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() } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index d2555bdf84..92bb35d307 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -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 { diff --git a/libs/visa/build.gradle.kts b/libs/visa/build.gradle.kts index bc0aa12ec9..8ef254a4e4 100644 --- a/libs/visa/build.gradle.kts +++ b/libs/visa/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt index 1b038aa5f9..c2ac96c707 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt @@ -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,22 +21,24 @@ internal class DefaultVisaContractInfoProvider( private val dispatchers: CoroutineDispatcherProvider, ) : VisaContractInfoProvider { - override suspend fun getBalancesAndLimits(walletAddress: String): BalancesAndLimits = withContext(dispatchers.io) { - val tangemBridgeProcessor = TangemBridgeProcessor.load( - /* contractAddress = */ bridgeProcessorAddress, - /* web3j = */ web3j, - /* transactionManager = */ transactionManager, - /* contractGasProvider = */ gasProvider, - ) + override suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits { + return withContext(dispatchers.io) { + val tangemBridgeProcessor = TangemBridgeProcessor.load( + /* contractAddress = */ bridgeProcessorAddress, + /* web3j = */ web3j, + /* transactionManager = */ transactionManager, + /* contractGasProvider = */ gasProvider, + ) - parZip( - dispatchers.io, - { loadPaymentAccount(walletAddress, tangemBridgeProcessor) }, - { loadPaymentTokenInfo(tangemBridgeProcessor) }, - { paymentAccount, paymentToken -> - fetchBalancesAndLimits(paymentAccount, paymentToken) - }, - ) + parZip( + dispatchers.io, + { loadPaymentAccount(walletAddress, tangemBridgeProcessor) }, + { loadPaymentTokenInfo(tangemBridgeProcessor) }, + { paymentAccount, paymentToken -> + fetchBalancesAndLimits(paymentAccount, paymentToken) + }, + ) + } } private fun loadPaymentAccount( @@ -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) }, ) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index efb5ab487a..35a8a3c4ad 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -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, diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt new file mode 100644 index 0000000000..b05b3096a2 --- /dev/null +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt @@ -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 +} \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt new file mode 100644 index 0000000000..50f200249b --- /dev/null +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt @@ -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() +} \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/BalancesAndLimits.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaBalancesAndLimits.kt similarity index 96% rename from libs/visa/src/main/kotlin/com/tangem/lib/visa/model/BalancesAndLimits.kt rename to libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaBalancesAndLimits.kt index 5098ee3c43..50145e05da 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/BalancesAndLimits.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaBalancesAndLimits.kt @@ -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, diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt new file mode 100644 index 0000000000..dcec060570 --- /dev/null +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt @@ -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, +) { + + 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, + @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?, + ) + } +} \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt index 6176a369e5..7819bd734e 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt @@ -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" } \ No newline at end of file