Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-28 13:38:14 +03:00
parent 20e6ff8ef0
commit 03e48b0929
236 changed files with 150 additions and 155 deletions

View file

@ -0,0 +1,30 @@
package com.tangem.datasource.api.common
import okhttp3.Interceptor
import okhttp3.Response
/**
[REDACTED_AUTHOR]
*/
open class AddHeaderInterceptor(
private val headers: Map<String, String>
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder().apply {
headers.forEach {
addHeader(it.key, it.value)
}
}.build()
return chain.proceed(request)
}
}
class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(mapOf(
"Cache-Control" to "max-age=$maxAgeSeconds",
))
class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf(
"card_public_key" to cardPublicKeyHex,
))

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.common.json.MoshiJsonConverter
import retrofit2.Converter
import retrofit2.converter.moshi.MoshiConverterFactory
/**
[REDACTED_AUTHOR]
*/
//todo needs to be refactored
object MoshiConverter {
//todo refactor: provide via DI
var INSTANCE = MoshiJsonConverter()
private set
fun reInitInstance(
adapters: List<Any> = listOf(),
typedAdapters: Map<Class<*>, JsonAdapter<*>> = mapOf(),
) {
INSTANCE = MoshiJsonConverter(adapters, typedAdapters)
}
fun createFactory(moshi: Moshi = INSTANCE.moshi): Converter.Factory = MoshiConverterFactory.create(moshi)
//todo provide via DI using quealifiers
fun defaultMoshi(): Moshi = INSTANCE.moshi
//todo provide via DI using quealifiers
fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi
}

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.common
/**
[REDACTED_AUTHOR]
*/
// sealed interface NetworkModuleMessage : ModuleMessage
//
// sealed class NetworkError(
// subCode: Int,
// override val message: String,
// override val data: Any?,
// ) : NetworkModuleMessage, ModuleError() {
// override val code: Int = ModuleErrorCode.NETWORK + subCode
//
// companion object {
// // base code used for all error in the module
// // const val CODE_ANY_OTHER = 100..199, 200..299
// }
// }

View file

@ -0,0 +1,37 @@
package com.tangem.datasource.api.common
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit
// TODO: refactoring: make it better through factory
fun createRetrofitInstance(
baseUrl: String,
okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder(),
interceptors: List<Interceptor> = emptyList(),
converterFactory: Converter.Factory = MoshiConverter.createFactory(),
logEnabled: Boolean,
): Retrofit {
okHttpBuilder.apply {
callTimeout(10, TimeUnit.SECONDS)
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
}
interceptors.forEach { okHttpBuilder.addInterceptor(it) }
if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor())
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(converterFactory)
.client(okHttpBuilder.build())
.build()
}
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}

View file

@ -0,0 +1,39 @@
package com.tangem.datasource.api.paymentology
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
/**
[REDACTED_AUTHOR]
*/
interface PaymentologyApi {
@Headers("Content-Type: application/json")
@POST("card/verify")
suspend fun checkRegistration(
@Body request: CheckRegistrationRequests,
): RegistrationResponse
@Headers("Content-Type: application/json")
@POST("card/get_challenge")
suspend fun requestAttestationChallenge(
@Body request: CheckRegistrationRequests.Item,
): AttestationResponse
@Headers("Content-Type: application/json")
@POST("card/set_pin")
suspend fun registerWallet(
@Body request: RegisterWalletRequest,
): RegisterWalletResponse
@Headers("Content-Type: application/json")
@POST("card/kyc")
suspend fun registerKYC(
@Body request: RegisterKYCRequest,
): RegisterWalletResponse
companion object {
val baseUrl: String = "https://paymentologygate.oa.r.appspot.com/"
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.datasource.api.paymentology
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createRetrofitInstance
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
*/
class PaymentologyApiService(
private val logEnabled: Boolean,
) {
private val api = createRetrofitInstance(
baseUrl = PaymentologyApi.baseUrl,
converterFactory = MoshiConverter.createFactory(MoshiConverter.sdkMoshi()),
logEnabled = logEnabled,
).create(PaymentologyApi::class.java)
suspend fun checkRegistration(
cardId: String,
publicKey: ByteArray,
): Result<RegistrationResponse> = withContext(Dispatchers.IO) {
val requestItem = CheckRegistrationRequests.Item(cardId, publicKey.toHexString())
val request = CheckRegistrationRequests(listOf(requestItem))
performRequest {
api.checkRegistration(request)
}
}
suspend fun requestAttestationChallenge(
cardId: String,
publicKey: ByteArray,
): Result<AttestationResponse> = withContext(Dispatchers.IO) {
val requestItem = CheckRegistrationRequests.Item(cardId, publicKey.toHexString())
performRequest {
api.requestAttestationChallenge(requestItem)
}
}
suspend fun registerWallet(
request: RegisterWalletRequest,
): Result<RegisterWalletResponse> = withContext(Dispatchers.IO) {
performRequest {
api.registerWallet(request)
}
}
suspend fun registerKYC(
request: RegisterKYCRequest,
): Result<RegisterWalletResponse> = withContext(Dispatchers.IO) {
performRequest {
api.registerKYC(request)
}
}
companion object {
fun stub(): PaymentologyApiService = PaymentologyApiService(false)
}
}

View file

@ -0,0 +1,91 @@
package com.tangem.datasource.api.paymentology
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.extensions.calculateHashCode
/**
[REDACTED_AUTHOR]
*/
data class CheckRegistrationRequests(
val requests: List<Item>,
) {
@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "CID")
val cardId: String = "",
val publicKey: String = "",
)
}
data class RegisterWalletRequest(
@Json(name = "CID")
val cardId: String,
val publicKey: ByteArray,
val walletPublicKey: ByteArray,
val walletSalt: ByteArray,
val walletSignature: ByteArray,
val cardSalt: ByteArray,
val cardSignature: ByteArray,
@Json(name = "PIN")
val pin: String,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RegisterWalletRequest
if (cardId != other.cardId) return false
if (!publicKey.contentEquals(other.publicKey)) return false
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
if (!walletSalt.contentEquals(other.walletSalt)) return false
if (!walletSignature.contentEquals(other.walletSignature)) return false
if (!cardSalt.contentEquals(other.cardSalt)) return false
if (!cardSignature.contentEquals(other.cardSignature)) return false
if (pin != other.pin) return false
return true
}
override fun hashCode(): Int = calculateHashCode(
cardId.hashCode(),
publicKey.contentHashCode(),
walletPublicKey.contentHashCode(),
walletSalt.contentHashCode(),
walletSignature.contentHashCode(),
cardSalt.contentHashCode(),
cardSignature.contentHashCode(),
pin.hashCode(),
)
}
data class RegisterKYCRequest(
@Json(name = "CID")
val cardId: String,
val publicKey: ByteArray,
val kycProvider: String,
val kycRefId: String,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RegisterKYCRequest
if (cardId != other.cardId) return false
if (!publicKey.contentEquals(other.publicKey)) return false
if (kycProvider != other.kycProvider) return false
if (kycRefId != other.kycRefId) return false
return true
}
override fun hashCode(): Int = calculateHashCode(
cardId.hashCode(),
publicKey.contentHashCode(),
kycProvider.hashCode(),
kycRefId.hashCode(),
)
}

View file

@ -0,0 +1,97 @@
package com.tangem.datasource.api.paymentology
import com.squareup.moshi.Json
import com.tangem.common.extensions.calculateHashCode
import com.tangem.common.services.Result
/**
[REDACTED_AUTHOR]
*/
interface ResponseError {
val success: Boolean
val error: String?
val errorCode: Int?
fun makeErrorMessage(): String {
return error ?: "unknown error"
}
}
inline fun <reified T> ResponseError.tryExtractError(): Result<T> = when (success) {
true -> Result.Success(this as T)
else -> Result.Failure(Throwable(makeErrorMessage()))
}
data class RegistrationResponse(
val results: List<Item> = listOf(),
override val success: Boolean,
override val error: String?,
override val errorCode: Int?,
) : ResponseError {
data class Item(
@Json(name = "CID")
val cardId: String,
val passed: Boolean?,
val active: Boolean?,
@Json(name = "pin_set")
val pinSet: Boolean?,
@Json(name = "blockchain_init")
val blockchainInit: Boolean?,
@Json(name = "kyc_passed")
val kycPassed: Boolean?,
@Json(name = "kyc_provider")
val kycProvider: String?,
@Json(name = "kyc_date")
val kycDate: String?,
@Json(name = "kyc_status")
val kycStatus: KYCStatus?,
@Json(name = "disabled_by_admin")
val disabledByAdmin: Boolean?,
val error: String?,
)
}
enum class KYCStatus {
NOT_STARTED,
STARTED,
WAITING_FOR_APPROVAL,
CORRECTION_REQUESTED,
REJECTED,
APPROVED,
}
data class AttestationResponse(
val challenge: ByteArray?,
override val success: Boolean,
override val error: String?,
override val errorCode: Int?,
) : ResponseError {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AttestationResponse
if (!challenge.contentEquals(other.challenge)) return false
if (success != other.success) return false
if (error != other.error) return false
if (errorCode != other.errorCode) return false
return true
}
override fun hashCode(): Int = calculateHashCode(
challenge.contentHashCode(),
success.hashCode(),
error.hashCode(),
errorCode.hashCode(),
)
}
data class RegisterWalletResponse(
override val success: Boolean,
override val error: String?,
override val errorCode: Int?,
) : ResponseError

View file

@ -0,0 +1,72 @@
package com.tangem.datasource.api.tangemTech
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface HttpResponse
sealed interface TangemTechResponse : HttpResponse
data class CoinsResponse(
val imageHost: String?,
val coins: List<Coin>,
val total: Int
) : TangemTechResponse {
data class Coin(
val id: String,
val name: String,
val symbol: String,
val active: Boolean,
val networks: List<Network> = listOf()
) : TangemTechResponse {
data class Network(
val networkId: String,
val contractAddress: String? = null,
val decimalCount: BigDecimal? = null,
) : TangemTechResponse
}
}
//rates.keys = networkId's
data class RatesResponse(val rates: Map<String, Double>) : TangemTechResponse
data class CurrenciesResponse(val currencies: List<Currency>) {
data class Currency(
val id: String,
val code: String, // this is an uppercase id
val name: String,
val rateBTC: String,
val unit: String, // $, €, ₽
val type: String,
) : TangemTechResponse
}
data class GeoResponse(
val code: String,
) : TangemTechResponse
data class UserTokensResponse(
val version: Int = 0,
val group: String? = null,
val sort: String? = null,
val tokens: List<TokenResponse> = emptyList(),
) : TangemTechResponse
data class TokenResponse(
val id: String? = null,
val networkId: String,
val derivationPath: String? = null,
val name: String,
val symbol: String,
val decimals: Int,
val contractAddress: String?,
) : TangemTechResponse
data class TangemTechError(
val code: Int,
val description: String,
)

View file

@ -0,0 +1,41 @@
package com.tangem.datasource.api.tangemTech
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
/**
[REDACTED_AUTHOR]
*/
interface TangemTechApi {
@GET("coins")
suspend fun coins(
@Query("contractAddress") contractAddress: String? = null,
@Query("networkIds") networkIds: String? = null,
@Query("active") active: Boolean? = null,
@Query("searchText") searchText: String? = null,
@Query("offset") offset: Int? = null,
@Query("limit") limit: Int? = null
): CoinsResponse
@GET("rates")
suspend fun rates(
@Query("currencyId") currencyId: String,
@Query("coinIds") coinIds: String,
): RatesResponse
@GET("currencies")
suspend fun currencies(): CurrenciesResponse
@GET("geo")
suspend fun geo(): GeoResponse
@GET("user-tokens/{user-id}")
suspend fun getUserTokens(@Path(value = "user-id") userId: String): UserTokensResponse
@PUT("user-tokens/{user-id}")
suspend fun putUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse)
}

View file

@ -0,0 +1,89 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.AddHeaderInterceptor
import com.tangem.datasource.api.common.CacheControlHttpInterceptor
import com.tangem.datasource.api.common.createRetrofitInstance
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
*/
class TangemTechService(
private val logIsEnabled: Boolean = false,
) {
private val headerInterceptors = mutableListOf<AddHeaderInterceptor>(
CacheControlHttpInterceptor(cacheMaxAge),
)
private var api: TangemTechApi = createApi()
suspend fun coins(
contractAddress: String? = null,
networkIds: String? = null,
active: Boolean? = null,
searchText: String? = null,
offset: Int? = null,
limit: Int? = null,
): Result<CoinsResponse> = withContext(Dispatchers.IO) {
performRequest {
api.coins(
contractAddress = contractAddress,
networkIds = networkIds,
active = active,
searchText = searchText,
offset = offset,
limit = limit,
)
}
}
suspend fun rates(
currency: String,
ids: List<String>,
): Result<RatesResponse> = withContext(Dispatchers.IO) {
performRequest {
api.rates(currency.lowercase(), ids.joinToString(","))
}
}
suspend fun userCountry(): Result<GeoResponse> = withContext(Dispatchers.IO) {
performRequest { api.geo() }
}
suspend fun currencies(): Result<CurrenciesResponse> = withContext(Dispatchers.IO) {
performRequest { api.currencies() }
}
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> = withContext(Dispatchers.IO) {
performRequest { api.getUserTokens(userId) }
}
suspend fun putUserTokens(userId: String, userTokens: UserTokensResponse): Result<Unit> =
withContext(Dispatchers.IO) {
performRequest { api.putUserTokens(userId, userTokens) }
}
fun addHeaderInterceptors(interceptors: List<AddHeaderInterceptor>) {
headerInterceptors.removeAll(interceptors)
headerInterceptors.addAll(interceptors)
api = createApi()
}
private fun createApi(): TangemTechApi {
val retrofit = createRetrofitInstance(
baseUrl = baseUrl,
interceptors = headerInterceptors.toList(),
logEnabled = logIsEnabled,
)
return retrofit.create(TangemTechApi::class.java)
}
companion object {
const val baseUrl = "https://api.tangem-tech.com/v1/"
const val cacheMaxAge = 600
}
}