Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-17 21:44:49 +03:00
parent 6d56af68f5
commit 3d9e937ee9
30 changed files with 258 additions and 344 deletions

View file

@ -20,27 +20,38 @@ internal class DefaultVisaContractInfoProvider(
private val dispatchers: CoroutineDispatcherProvider,
) : VisaContractInfoProvider {
override suspend fun getContractInfo(walletAddress: String): VisaContractInfo {
override suspend fun getContractInfo(walletAddress: String, paymentAccountAddress: String?): VisaContractInfo {
return parZip(
dispatchers.io,
{ loadPaymentAccount(walletAddress) },
{ loadPaymentAccount(walletAddress = walletAddress, paymentAccountAddress = paymentAccountAddress) },
{ loadPaymentTokenInfo() },
{ paymentAccount, paymentToken ->
fetchBalancesAndLimits(paymentAccount, paymentToken)
fetchBalancesAndLimits(
paymentAccount = paymentAccount,
paymentToken = paymentToken,
walletAddress = walletAddress,
)
},
)
}
private fun loadPaymentAccount(walletAddress: String): TangemPaymentAccount {
private fun loadPaymentAccount(walletAddress: String, paymentAccountAddress: String?): TangemPaymentAccount {
return TangemPaymentAccount.load(
/* contractAddress = */ paymentAccountAddress ?: getPaymentAccountAddressFromRegistry(walletAddress),
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
}
private fun getPaymentAccountAddressFromRegistry(walletAddress: String): String {
val paymentAccountRegistry = TangemPaymentAccountRegistry.load(
/* contractAddress = */ paymentAccountRegistryAddress,
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
val paymentAccountAddress = paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider)
return paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
}
private fun loadPaymentTokenInfo(): PaymentTokenInfo {
@ -63,11 +74,12 @@ internal class DefaultVisaContractInfoProvider(
private suspend fun fetchBalancesAndLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
walletAddress: String,
): VisaContractInfo = parZip(
dispatchers.io,
{ fetchToken(paymentAccount) },
{ fetchBalances(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken, walletAddress) },
{ token, balances, (oldLimit, newLimit, changeDate) ->
VisaContractInfo(token, balances, oldLimit, newLimit, changeDate)
},
@ -123,19 +135,15 @@ internal class DefaultVisaContractInfoProvider(
private fun fetchLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
walletAddress: String,
): Triple<Limits, Limits, Instant> {
TODO() // TODO: [REDACTED_TASK_KEY]
// val (
// oldLimit,
// newLimit,
// changeDateSeconds,
// ) = paymentAccount.cards("").send().component5()
//
// return Triple(
// first = getLimits(oldLimit, paymentToken),
// second = getLimits(newLimit, paymentToken),
// third = changeDateSeconds.toInstant(),
// )
val limits = paymentAccount.cards(walletAddress).send().component5()
return Triple(
first = getLimits(limits.oldValue, paymentToken),
second = getLimits(limits.newValue, paymentToken),
third = limits.changeTimestamp.toInstant(),
)
}
@Suppress("UnusedPrivateMember")

View file

@ -21,7 +21,14 @@ import java.util.concurrent.TimeUnit
interface VisaContractInfoProvider {
suspend fun getContractInfo(walletAddress: String): VisaContractInfo
/**
* Fetches Visa contract info for the given wallet address.
*
* @param walletAddress Wallet address to fetch contract info for.
* @param paymentAccountAddress Payment account address to fetch data from. If null,
* it will be fetched from the registry.
*/
suspend fun getContractInfo(walletAddress: String, paymentAccountAddress: String?): VisaContractInfo
class Builder(
private val useTestnetRpc: Boolean,

View file

@ -1,18 +0,0 @@
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.Header
import retrofit2.http.Query
interface VisaApi {
@GET("transaction")
suspend fun getTxHistory(
@Header("Authorization") authorizationHeader: String,
@Query("card_public_key") cardPublicKey: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -1,72 +0,0 @@
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.Constants
import com.tangem.lib.visa.utils.Constants.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 headers: Map<String, String>,
private val networkTimeoutSeconds: Long = Constants.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())
}
if (headers.isNotEmpty()) {
addInterceptor { chain ->
val request = chain.request().newBuilder().apply {
headers.forEach { (key, value) -> addHeader(key, value) }
}.build()
chain.proceed(request)
}
}
}
return builder.build()
}
private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.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

@ -1,88 +0,0 @@
package com.tangem.lib.visa.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class VisaTxHistoryResponse(
@Json(name = "card_wallet_address")
val cardWalletAddress: String,
@Json(name = "transactions")
val transactions: List<Transaction>,
) {
@JsonClass(generateAdapter = true)
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,
) {
@JsonClass(generateAdapter = true)
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?,
)
}
}