Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-12 17:02:16 +04:00
parent 1f97352e5a
commit c6dac76deb
16 changed files with 269 additions and 303 deletions

View file

@ -9,20 +9,25 @@ import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.paymentology.models.request.CheckRegistrationRequests
import com.tangem.datasource.api.paymentology.models.request.RegisterKYCRequest
import com.tangem.datasource.api.paymentology.models.request.RegisterWalletRequest
import com.tangem.datasource.api.paymentology.models.response.AttestationResponse
import com.tangem.datasource.api.paymentology.models.response.RegisterWalletResponse
import com.tangem.datasource.api.paymentology.models.response.RegistrationResponse
import com.tangem.datasource.api.paymentology.models.response.tryExtractError
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.util.UserWalletId
import com.tangem.datasource.api.paymentology.AttestationResponse
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.paymentology.RegisterKYCRequest
import com.tangem.datasource.api.paymentology.RegisterWalletRequest
import com.tangem.datasource.api.paymentology.RegisterWalletResponse
import com.tangem.datasource.api.paymentology.RegistrationResponse
import com.tangem.datasource.api.paymentology.tryExtractError
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
/**
@ -55,14 +60,30 @@ class SaltPayActivationManager(
}
suspend fun registerKYC(): Result<Unit> {
return when (val result = paymentologyService.registerKYC(makeRegisterKYCRequest())) {
val result = withContext(Dispatchers.IO) {
performRequest { paymentologyService.api.registerKYC(makeRegisterKYCRequest()) }
}
return when (result) {
is Result.Success -> Result.Success(Unit)
is Result.Failure -> result
}
}
suspend fun checkRegistration(): Result<RegistrationResponse.Item> {
val response: RegistrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey)
val response: RegistrationResponse = withContext(Dispatchers.IO) {
performRequest {
paymentologyService.api.checkRegistration(
request = CheckRegistrationRequests(
requests = listOf(
CheckRegistrationRequests.Item(
cardId = cardId,
publicKey = cardPublicKey.toHexString(),
),
),
),
)
}
}
.successOr { return it }
.tryExtractError<RegistrationResponse>()
.successOr { return it }
@ -80,7 +101,16 @@ class SaltPayActivationManager(
}
suspend fun requestAttestationChallenge(): Result<AttestationResponse> {
return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey)
return withContext(Dispatchers.IO) {
performRequest {
paymentologyService.api.requestAttestationChallenge(
request = CheckRegistrationRequests.Item(
cardId = cardId,
publicKey = cardPublicKey.toHexString(),
),
)
}
}
.successOr { return it }
.tryExtractError()
}
@ -105,7 +135,9 @@ class SaltPayActivationManager(
cardSignature = attestResponse.cardSignature ?: byteArrayOf(),
pin = pinCode,
)
return paymentologyService.registerWallet(request)
return withContext(Dispatchers.IO) {
performRequest { paymentologyService.api.registerWallet(request) }
}
.successOr { return it }
.tryExtractError()
}
@ -163,7 +195,7 @@ class SaltPayActivationManager(
companion object {
fun stub(): SaltPayActivationManager = SaltPayActivationManager(
kycProvider = SaltPayConfig.stub().kycProvider,
paymentologyService = PaymentologyApiService.stub(),
paymentologyService = PaymentologyApiService,
gnosisRegistrator = GnosisRegistrator.stub(),
cardId = "",
cardPublicKey = byteArrayOf(),

View file

@ -6,11 +6,11 @@ import com.tangem.blockchain.extensions.successOr
import com.tangem.common.Filter
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.api.paymentology.models.response.KYCStatus
import com.tangem.datasource.api.paymentology.models.response.RegistrationResponse
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.datasource.api.paymentology.KYCStatus
import com.tangem.datasource.api.paymentology.RegistrationResponse
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -412,34 +412,6 @@ private suspend fun checkGasIfNeeded(
return result
}
@Throws(SaltPayActivationError::class)
fun RegistrationResponse.Item.toSaltPayStep(currentStep: SaltPayActivationStep): SaltPayActivationStep {
return when {
passed != true -> throw SaltPayActivationError.CardNotPassed(this.error)
disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(this.error)
// go to claim screen
active == true -> SaltPayActivationStep.Claim
// pinSet is false, go toPin screen
pinSet == false -> SaltPayActivationStep.NeedPin
kycStatus != null -> {
when (kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject
KYCStatus.APPROVED -> SaltPayActivationStep.Claim
null -> throw UnsupportedOperationException()
}
}
// kycDate is set, go to kyc waiting screen
kycDate != null -> SaltPayActivationStep.KycWaiting
else -> SaltPayActivationStep.KycIntro
}
}
@Throws(SaltPayActivationError::class)
private fun assertPinValid(pin: String, pinLength: Int) {
val array = pin.toCharArray()

View file

@ -1,39 +1,38 @@
package com.tangem.datasource.api.paymentology
import com.tangem.datasource.api.paymentology.models.request.CheckRegistrationRequests
import com.tangem.datasource.api.paymentology.models.request.RegisterKYCRequest
import com.tangem.datasource.api.paymentology.models.request.RegisterWalletRequest
import com.tangem.datasource.api.paymentology.models.response.AttestationResponse
import com.tangem.datasource.api.paymentology.models.response.RegisterWalletResponse
import com.tangem.datasource.api.paymentology.models.response.RegistrationResponse
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
/**
* Interface of Paymentology Api.
*
* IMPORTANT: Cannot replace [Headers] annotations with OkHttpClient.addHeader(), because OkHttp adds encoding to
* content type value. But Paymentology API expects a value without the charset.
*
[REDACTED_AUTHOR]
*/
interface PaymentologyApi {
@Headers("Content-Type: application/json")
@POST("card/verify")
suspend fun checkRegistration(
@Body request: CheckRegistrationRequests,
): RegistrationResponse
suspend fun checkRegistration(@Body request: CheckRegistrationRequests): RegistrationResponse
@Headers("Content-Type: application/json")
@POST("card/get_challenge")
suspend fun requestAttestationChallenge(
@Body request: CheckRegistrationRequests.Item,
): AttestationResponse
suspend fun requestAttestationChallenge(@Body request: CheckRegistrationRequests.Item): AttestationResponse
@Headers("Content-Type: application/json")
@POST("card/set_pin")
suspend fun registerWallet(
@Body request: RegisterWalletRequest,
): RegisterWalletResponse
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/"
}
suspend fun registerKYC(@Body request: RegisterKYCRequest): RegisterWalletResponse
}

View file

@ -1,63 +1,30 @@
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
import com.tangem.datasource.utils.allowLogging
import okhttp3.OkHttpClient
import retrofit2.Retrofit
/**
[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)
//TODO("Remove after removing Redux")
@Deprecated("Use PaymentologyApi")
object PaymentologyApiService {
val api = createApi()
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)
}
}
private const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
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)
private fun createApi(): PaymentologyApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverter.createFactory(MoshiConverter.sdkMoshi()))
.baseUrl(PAYMENTOLOGY_BASE_URL)
.client(
OkHttpClient.Builder()
.allowLogging()
.build(),
)
.build()
.create(PaymentologyApi::class.java)
}
}

View file

@ -1,91 +0,0 @@
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

@ -1,97 +0,0 @@
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,15 @@
package com.tangem.datasource.api.paymentology.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
data class CheckRegistrationRequests(
@Json(name = "requests") val requests: List<Item>,
) {
@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "CID") val cardId: String = "",
@Json(name = "publicKey") val publicKey: String = "",
)
}

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.paymentology.models.request
import com.squareup.moshi.Json
import com.tangem.common.extensions.calculateHashCode
data class RegisterKYCRequest(
@Json(name = "CID") val cardId: String,
@Json(name = "publicKey") val publicKey: ByteArray,
@Json(name = "kycProvider") val kycProvider: String,
@Json(name = "kycRefId") 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,44 @@
package com.tangem.datasource.api.paymentology.models.request
import com.squareup.moshi.Json
import com.tangem.common.extensions.calculateHashCode
data class RegisterWalletRequest(
@Json(name = "CID") val cardId: String,
@Json(name = "publicKey") val publicKey: ByteArray,
@Json(name = "walletPublicKey") val walletPublicKey: ByteArray,
@Json(name = "walletSalt") val walletSalt: ByteArray,
@Json(name = "walletSignature") val walletSignature: ByteArray,
@Json(name = "cardSalt") val cardSalt: ByteArray,
@Json(name = "cardSignature") 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(),
)
}

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.paymentology.models.response
import com.squareup.moshi.Json
import com.tangem.common.extensions.calculateHashCode
data class AttestationResponse(
@Json(name = "challenge") val challenge: ByteArray?,
@Json(name = "success") override val success: Boolean,
@Json(name = "error") override val error: String?,
@Json(name = "errorCode") 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(),
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.paymentology.models.response
enum class KYCStatus {
NOT_STARTED,
STARTED,
WAITING_FOR_APPROVAL,
CORRECTION_REQUESTED,
REJECTED,
APPROVED,
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.paymentology.models.response
import com.squareup.moshi.Json
data class RegisterWalletResponse(
@Json(name = "success") override val success: Boolean,
@Json(name = "error") override val error: String?,
@Json(name = "errorCode") override val errorCode: Int?,
) : ResponseError

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.api.paymentology.models.response
import com.squareup.moshi.Json
data class RegistrationResponse(
@Json(name = "results") val results: List<Item> = listOf(),
@Json(name = "success") override val success: Boolean,
@Json(name = "error") override val error: String?,
@Json(name = "errorCode") override val errorCode: Int?,
) : ResponseError {
data class Item(
@Json(name = "CID") val cardId: String,
@Json(name = "passed") val passed: Boolean?,
@Json(name = "active") 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?,
@Json(name = "error") val error: String?,
)
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.paymentology.models.response
import com.tangem.common.services.Result
interface ResponseError {
val success: Boolean
val error: String?
val errorCode: Int?
fun makeErrorMessage(): String = error ?: "unknown error"
}
inline fun <reified T> ResponseError.tryExtractError(): Result<T> = when (success) {
true -> Result.Success(this as T)
else -> Result.Failure(Throwable(makeErrorMessage()))
}

View file

@ -53,5 +53,7 @@ class NetworkModule {
private companion object {
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
private const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/"
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.domain.redux.global
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.domain.DomainDialog
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.ScanResponse
/**
@ -20,7 +19,5 @@ data class DomainGlobalState(
data class NetworkServices(
val tangemTechService: TangemTechService = TangemTechService,
val paymentologyService: PaymentologyApiService = PaymentologyApiService(
LogConfig.network.paymentologyApiService,
),
val paymentologyService: PaymentologyApiService = PaymentologyApiService,
)