Updated on 2026-08-14
This commit is contained in:
parent
20e6ff8ef0
commit
03e48b0929
236 changed files with 150 additions and 155 deletions
|
|
@ -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,
|
||||
))
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue