Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-21 16:36:12 +03:00
commit e8295055f0
1240 changed files with 39272 additions and 7385 deletions

View file

@ -24,9 +24,12 @@ sealed class ApiConfig {
Express,
TangemTech,
StakeKit,
P2PEthPool,
TangemPay,
BlockAid,
YieldSupply,
MoonPay,
News,
}
private fun initializeId(): ID {
@ -34,9 +37,12 @@ sealed class ApiConfig {
is Express -> ID.Express
is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit
is P2PEthPool -> ID.P2PEthPool
is TangemPay -> ID.TangemPay
is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
is News -> ID.News
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
/**
* MoonPay [ApiConfig]
*/
internal class MoonPay : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
)
}
}

View file

@ -0,0 +1,62 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
/**
* News [ApiConfig]
[REDACTED_AUTHOR]
*/
internal class News(
private val authProvider: AuthProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createDevEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = PROD_BASE_URL,
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = DEV_BASE_URL,
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createHeaders(environment: ApiEnvironment) = buildMap {
putAll(
RequestHeader.TangemApiKeyHeader(
authProvider = authProvider,
apiEnvironment = Provider { environment },
).values,
)
}
private companion object {
private const val PROD_BASE_URL = "https://api.tangem.org/"
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.utils.ProviderSuspend
/**
* P2P.org Ethereum Pooled Staking API configuration
*/
internal class P2PEthPool(
private val p2pAuthProvider: P2PEthPoolAuthProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createTestEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.p2p.org/",
headers = createHeaders(),
)
}
private fun createTestEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://api-test.p2p.org/",
headers = createHeaders(),
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
}
private fun createHeaders() = buildMap {
put(key = "Authorization", value = ProviderSuspend { "Bearer ${p2pAuthProvider.getApiKey()}" })
put(key = "accept", value = ProviderSuspend { "application/json" })
put(key = "Content-Type", value = ProviderSuspend { "application/json" })
}
}

View file

@ -31,7 +31,7 @@ internal class TangemPay(
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
baseUrl = "https://api.dev.us.paera.com/bff/",
headers = createHeaders(),
)

View file

@ -14,10 +14,12 @@ sealed class ApiResponse<T : Any> {
* Represents a successful response from the API
*
* @property data the data returned by the API
* @property code the HTTP status code of the response
* @property headers the headers returned by the API
*/
data class Success<T : Any>(
val data: T,
val code: ApiResponseError.HttpException.Code = ApiResponseError.HttpException.Code.OK,
override val headers: Map<String, List<String>> = emptyMap(),
) : ApiResponse<T>()
@ -37,11 +39,20 @@ sealed class ApiResponse<T : Any> {
* Wraps data in a [ApiResponse.Success] instance
*
* @param data the data to wrap
* @param code the HTTP status code of the response
* @param headers the headers returned by the API
* @return a [ApiResponse.Success] instance containing the provided data
*/
internal fun <T : Any> apiSuccess(data: T, headers: Map<String, List<String>>): ApiResponse<T> {
return ApiResponse.Success(data, headers)
internal fun <T : Any> apiSuccess(
data: T,
code: ApiResponseError.HttpException.Code?,
headers: Map<String, List<String>>,
): ApiResponse<T> {
return ApiResponse.Success(
data = data,
code = code ?: ApiResponseError.HttpException.Code.OK,
headers = headers,
)
}
/**

View file

@ -18,8 +18,13 @@ sealed class ApiResponseError : Exception() {
val errorBody: String?,
) : ApiResponseError() {
// TODO: extract Code from HttpException
// region Error Codes
enum class Code(val numericCode: Int) {
// 2xx Success
OK(numericCode = 200),
CREATED(numericCode = 201),
ACCEPTED(numericCode = 202),
// 3xx Server Errors
NOT_MODIFIED(numericCode = 304),
// 4xx Server Errors

View file

@ -15,11 +15,12 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
val headers = headers().toMultimap()
val body = body()
val code = ApiResponseError.HttpException.Code.entries
.firstOrNull { it.numericCode == code() }
return if (isSuccessful && body != null) {
apiSuccess(data = body, headers = headers)
apiSuccess(data = body, code = code, headers = headers)
} else {
val code = ApiResponseError.HttpException.Code.entries
.firstOrNull { it.numericCode == code() }
val e = try {
if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))

View file

@ -0,0 +1,119 @@
package com.tangem.datasource.api.ethpool
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
import com.tangem.datasource.api.ethpool.models.response.*
import retrofit2.http.*
/**
* P2P.org Ethereum Pooled Staking API client
*
* Documentation: https://docs.p2p.org/
*
* Base URL: https://api.p2p.org (prod) / https://api-test.p2p.org (testnet)
*/
interface P2PEthPoolApi {
/**
* Get list of available vaults
*
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
*/
@GET("api/v1/staking/pool/{network}/vaults")
suspend fun getVaults(
@Path("network") network: String = "mainnet",
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
/**
* Prepare deposit transaction
*
* Create unsigned transaction for depositing ETH into a vault.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Deposit parameters (delegator address, vault address, amount)
*/
@POST("api/v1/staking/pool/{network}/staking/deposit")
suspend fun createDepositTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolDepositRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
/**
* Prepare unstake transaction
*
* Create unsigned transaction to initiate unstaking process.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Unstake parameters (staker public key, stake transaction hash)
*/
@POST("api/v1/staking/pool/{network}/staking/unstake")
suspend fun createUnstakeTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolUnstakeRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
/**
* Prepare withdrawal transaction
*
* Create unsigned transaction to withdraw available funds from exit queue.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Withdrawal parameters (staker address)
*/
@POST("api/v1/staking/pool/{network}/staking/withdraw")
suspend fun createWithdrawTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolWithdrawRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
/**
* Broadcast signed transaction
*
* Submit a signed transaction to the blockchain network.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Signed transaction in hexadecimal format
*/
@POST("api/v1/staking/pool/{network}/transaction/send")
suspend fun broadcastTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolBroadcastRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolBroadcastResponse>>
/**
* Get account summary
*
* Retrieve staking balance, rewards, and exit queue information for a specific account and vault.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param delegatorAddress Account address that initiated staking
* @param vaultAddress Ethereum address of the vault
*/
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}")
suspend fun getAccountInfo(
@Path("network") network: String,
@Path("delegatorAddress") delegatorAddress: String,
@Path("vaultAddress") vaultAddress: String,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
/**
* Get rewards history
*
* Retrieve historical rewards data for a specific account and vault.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param delegatorAddress Account address that initiated staking
* @param vaultAddress Ethereum address of the vault
* @param period Optional period filter (30, 60, or 90 days)
*/
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards")
suspend fun getRewards(
@Path("network") network: String,
@Path("delegatorAddress") delegatorAddress: String,
@Path("vaultAddress") vaultAddress: String,
@Query("period") period: Int? = null,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolRewardsResponse>>
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for broadcasting signed transaction
*
* Used in: POST /api/v1/staking/pool/{network}/transaction/send
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolBroadcastRequest(
@Json(name = "signedTransaction")
val signedTransaction: String,
)

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for creating deposit transaction
*
* Used in: POST /api/v1/staking/pool/{network}/staking/deposit
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolDepositRequest(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "amount")
val amount: Double,
)

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for creating unstake transaction
*
* Used in: POST /api/v1/staking/pool/{network}/staking/unstake
*
* Note: Documentation seems to contain Bitcoin-related fields (possibly copy-paste error).
* Using as-is per specification.
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolUnstakeRequest(
@Json(name = "stakerPublicKey")
val stakerPublicKey: String,
@Json(name = "stakeTransactionHash")
val stakeTransactionHash: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for creating withdrawal transaction
*
* Used in: POST /api/v1/staking/pool/{network}/staking/withdraw
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolWithdrawRequest(
@Json(name = "stakerAddress")
val stakerAddress: String,
)

View file

@ -0,0 +1,54 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolAccountResponse(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "stake")
val stake: P2PEthPoolStakeDTO,
@Json(name = "availableToUnstake")
val availableToUnstake: BigDecimal,
@Json(name = "availableToWithdraw")
val availableToWithdraw: BigDecimal,
@Json(name = "exitQueue")
val exitQueue: P2PEthPoolExitQueueDTO,
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolStakeDTO(
@Json(name = "assets")
val assets: BigDecimal,
@Json(name = "totalEarnedAssets")
val totalEarnedAssets: BigDecimal,
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolExitQueueDTO(
@Json(name = "total")
val total: Double,
@Json(name = "requests")
val requests: List<P2PEthPoolExitRequestDTO>,
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolExitRequestDTO(
@Json(name = "ticket")
val ticket: String,
@Json(name = "totalAssets")
val totalAssets: Double,
@Json(name = "timestamp")
val timestamp: Long,
@Json(name = "withdrawalTimestamp")
val withdrawalTimestamp: Long,
@Json(name = "isClaimable")
val isClaimable: Boolean,
)

View file

@ -0,0 +1,41 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response for POST /api/v1/staking/pool/{network}/transaction/send
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolBroadcastResponse(
@Json(name = "hash")
val hash: String,
@Json(name = "status")
val status: P2PEthPoolTxStatusDTO,
@Json(name = "blockNumber")
val blockNumber: Int,
@Json(name = "transactionIndex")
val transactionIndex: Int,
@Json(name = "gasUsed")
val gasUsed: String,
@Json(name = "cumulativeGasUsed")
val cumulativeGasUsed: String,
@Json(name = "effectiveGasPrice")
val effectiveGasPrice: String?,
@Json(name = "from")
val from: String,
@Json(name = "to")
val to: String,
)
/**
* Transaction status from P2P API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolTxStatusDTO {
@Json(name = "success")
SUCCESS,
@Json(name = "failed")
FAILED,
}

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
/**
* Response for POST /api/v1/staking/pool/{network}/staking/deposit
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolDepositResponse(
@Json(name = "amount")
val amount: Double,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "unsignedTransaction")
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
@Json(name = "createdAt")
val createdAt: DateTime,
)

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Error response structure for P2P.org API
*
* All P2P API endpoints return errors in this format
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolErrorResponse(
@Json(name = "error")
val error: P2PEthPoolErrorDetailsDTO,
@Json(name = "result")
val result: Any? = null, // null on error
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolErrorDetailsDTO(
@Json(name = "code")
val code: Int, // Error code (e.g., 127106, 101111)
@Json(name = "message")
val message: String, // Human-readable error message
@Json(name = "name")
val name: String, // Error name/type
@Json(name = "errors")
val errors: List<String>? = null, // Optional validation errors array
)

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Unified response wrapper for all P2P.org API responses
*
* All P2P API endpoints return responses in this format:
* ```json
* {
* "error": null | { code, message, name, errors },
* "result": { ... } | null
* }
* ```
*
* @param T The type of the result data
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolResponse<T>(
@Json(name = "error")
val error: P2PEthPoolErrorDetailsDTO?,
@Json(name = "result")
val result: T?,
)

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
/**
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolRewardsResponse(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "rewards")
val rewards: List<P2PEthPoolRewardDTO>,
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolRewardDTO(
@Json(name = "date")
val date: DateTime,
@Json(name = "apy")
val apy: Double,
@Json(name = "balance")
val balance: BigDecimal,
@Json(name = "rewards")
val rewards: BigDecimal,
)

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Unsigned transaction structure
*
* Used in deposit, withdraw responses
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolUnsignedTxDTO(
@Json(name = "serializeTx")
val serializeTx: String,
@Json(name = "to")
val to: String,
@Json(name = "data")
val data: String,
@Json(name = "value")
val value: String,
@Json(name = "nonce")
val nonce: Int,
@Json(name = "chainId")
val chainId: Int,
@Json(name = "gasLimit")
val gasLimit: BigDecimal,
@Json(name = "maxFeePerGas")
val maxFeePerGas: BigDecimal,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: BigDecimal,
)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response for POST /api/v1/staking/pool/{network}/staking/unstake
*
* Note: Contains Bitcoin-related fields (likely documentation error).
* Using as-is per specification.
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolUnstakeResponse(
@Json(name = "stakerPublicKey")
val stakerPublicKey: String,
@Json(name = "stakeTransactionHash")
val stakeTransactionHash: String,
@Json(name = "unstakeTransactionHex")
val unstakeTransactionHex: String, // unsigned
@Json(name = "unstakeFee")
val unstakeFee: Double,
)

View file

@ -0,0 +1,59 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response for GET /api/v1/staking/pool/{network}/vaults
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolVaultsResponse(
@Json(name = "network")
val network: P2PEthPoolNetworkDTO,
@Json(name = "vaults")
val vaults: List<P2PEthPoolVaultDTO>,
)
/**
* Network identifier in P2P API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolNetworkDTO {
@Json(name = "mainnet")
MAINNET,
@Json(name = "hoodi")
HOODI,
}
@JsonClass(generateAdapter = true)
data class P2PEthPoolVaultDTO(
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "displayName")
val displayName: String,
@Json(name = "apy")
val apy: Double,
@Json(name = "baseApy")
val baseApy: Double,
@Json(name = "capacity")
val capacity: Double,
@Json(name = "totalAssets")
val totalAssets: Double,
@Json(name = "feePercent")
val feePercent: Double,
@Json(name = "isPrivate")
val isPrivate: Boolean,
@Json(name = "isGenesis")
val isGenesis: Boolean,
@Json(name = "isSmoothingPool")
val isSmoothingPool: Boolean,
@Json(name = "isErc20")
val isErc20: Boolean,
@Json(name = "tokenName")
val tokenName: String?,
@Json(name = "tokenSymbol")
val tokenSymbol: String?,
@Json(name = "createdAt")
val createdAt: Long,
)

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
/**
* Response for POST /api/v1/staking/pool/{network}/staking/withdraw
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolWithdrawResponse(
@Json(name = "amount")
val amount: Double,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "unsignedTransaction")
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
@Json(name = "createdAt")
val createdAt: DateTime,
@Json(name = "tickets")
val tickets: List<String>,
)

View file

@ -1,5 +0,0 @@
package com.tangem.datasource.api.express.models
object TangemExpressValues {
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
}

View file

@ -0,0 +1,48 @@
package com.tangem.datasource.api.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Query
interface MoonPayApi {
@GET("v4/ip_address/")
suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus
@GET("v3/currencies/")
suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies>
}
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
@Json(name = "isBuyAllowed")
val isBuyAllowed: Boolean,
@Json(name = "isSellAllowed")
val isSellAllowed: Boolean,
@Json(name = "isAllowed")
val isMoonpayAllowed: Boolean,
@Json(name = "alpha3")
val countryCode: String,
@Json(name = "state")
val stateCode: String,
)
@Suppress("BooleanPropertyNaming")
@JsonClass(generateAdapter = true)
data class MoonPayCurrencies(
@Json(name = "type") val type: String,
@Json(name = "code") val code: String,
@Json(name = "supportsLiveMode") val supportsLiveMode: Boolean = false,
@Json(name = "isSuspended") val isSuspended: Boolean = true,
@Json(name = "isSupportedInUS") val isSupportedInUS: Boolean = false,
@Json(name = "isSellSupported") val isSellSupported: Boolean = false,
@Json(name = "notAllowedUSStates") val notAllowedUSStates: List<String> = emptyList(),
@Json(name = "metadata") val metadata: MoonPayCurrenciesMetadata? = null,
)
@JsonClass(generateAdapter = true)
data class MoonPayCurrenciesMetadata(
@Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "networkCode") val networkCode: String?,
)

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.news
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.news.models.response.NewsCategoriesResponse
import com.tangem.datasource.api.news.models.response.NewsDetailsResponse
import com.tangem.datasource.api.news.models.response.NewsListResponse
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface NewsApi {
@GET(NEWS_PATH)
suspend fun getNews(
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@Query("lang") language: String? = null,
@Query("asOf") snapshot: String? = null,
@Query("tokenIds") tokenIds: List<String>? = null,
@Query("categoryIds") categoryIds: List<Int>? = null,
): ApiResponse<NewsListResponse>
@GET("$NEWS_PATH/{newsId}")
suspend fun getNewsDetails(
@Path("newsId") newsId: Int,
@Query("lang") language: String? = null,
): ApiResponse<NewsDetailsResponse>
@GET("$NEWS_PATH/trending")
suspend fun getTrendingNews(
@Query("limit") limit: Int? = null,
@Query("lang") language: String? = null,
): ApiResponse<NewsTrendingResponse>
@GET("$NEWS_PATH/categories")
suspend fun getCategories(): ApiResponse<NewsCategoriesResponse>
private companion object {
private const val NEWS_PATH = "api/v1/news"
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "score") val score: Double,
@Json(name = "language") val language: String,
@Json(name = "isTrending") val isTrending: Boolean,
@Json(name = "categories") val categories: List<NewsCategoryDto>,
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
@Json(name = "title") val title: String,
@Json(name = "newsUrl") val newsUrl: String,
)
@JsonClass(generateAdapter = true)
data class NewsCategoryDto(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class NewsRelatedTokenDto(
@Json(name = "id") val id: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class NewsOriginalArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "sourceName") val sourceName: String,
@Json(name = "language") val language: String,
@Json(name = "publishedAt") val publishedAt: String,
@Json(name = "url") val url: String,
@Json(name = "imageUrl") val imageUrl: String? = null,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsCategoriesResponse(
@Json(name = "items") val items: List<NewsCategoryDto>,
)

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsDetailsResponse(
@Json(name = "id") val id: Int,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "score") val score: Double,
@Json(name = "language") val language: String,
@Json(name = "isTrending") val isTrending: Boolean,
@Json(name = "categories") val categories: List<NewsCategoryDto>,
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
@Json(name = "title") val title: String,
@Json(name = "newsUrl") val newsUrl: String,
@Json(name = "shortContent") val shortContent: String,
@Json(name = "content") val content: String,
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
)

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsListResponse(
@Json(name = "meta") val meta: NewsListMetaDto,
@Json(name = "items") val items: List<NewsArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsListMetaDto(
@Json(name = "page") val page: Int,
@Json(name = "limit") val limit: Int,
@Json(name = "total") val total: Long,
@Json(name = "hasNext") val hasNext: Boolean,
@Json(name = "asOf") val asOf: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.news.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class NewsTrendingResponse(
@Json(name = "meta") val meta: NewsTrendingMetaDto,
@Json(name = "items") val items: List<NewsArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsTrendingMetaDto(
@Json(name = "limit") val limit: Int,
)

View file

@ -7,6 +7,7 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
@ -146,4 +147,22 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String,
@Body body: CardDetailsRequest,
): ApiResponse<CardDetailsResponse>
@PUT("v1/customer/card/pin")
suspend fun setPin(
@Header("Authorization") authHeader: String,
@Body body: SetPinRequest,
): ApiResponse<SetPinResponse>
@POST("v1/customer/card/freeze")
suspend fun freezeCard(
@Header("Authorization") authHeader: String,
@Body body: FreezeUnfreezeCardRequest,
): ApiResponse<FreezeUnfreezeCardResponse>
@POST("v1/customer/card/unfreeze")
suspend fun unfreezeCard(
@Header("Authorization") authHeader: String,
@Body body: FreezeUnfreezeCardRequest,
): ApiResponse<FreezeUnfreezeCardResponse>
}

View file

@ -0,0 +1,7 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FreezeUnfreezeCardRequest(@Json(name = "card_id") val cardId: String)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinRequest(
@Json(name = "pin") val pin: String,
@Json(name = "session_id") val sessionId: String,
@Json(name = "iv") val iv: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "result") val result: String,
)
}

View file

@ -28,10 +28,49 @@ data class CustomerMeResponse(
@Json(name = "cid") val cid: String,
@Json(name = "card_id") val cardId: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
@Json(name = "status") val status: String,
@Json(name = "status") val status: Status,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
)
) {
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "new")
NEW,
@Json(name = "ready_for_manufacturing")
READY_FOR_MANUFACTURING,
@Json(name = "manufacturing")
MANUFACTURING,
@Json(name = "sent_to_delivery")
SENT_TO_DELIVERY,
@Json(name = "delivered")
DELIVERED,
@Json(name = "activating")
ACTIVATING,
@Json(name = "active")
ACTIVE,
@Json(name = "blocked")
BLOCKED,
@Json(name = "deactivating")
DEACTIVATING,
@Json(name = "deactivated")
DEACTIVATED,
@Json(name = "canceled")
CANCELED,
@Json(name = "unknown")
UNKNOWN,
}
}
@JsonClass(generateAdapter = true)
data class PaymentAccount(

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class FreezeUnfreezeCardResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "order_id") val orderId: String,
@Json(name = "status") val status: Status,
)
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "NEW")
NEW,
@Json(name = "PROCESSING")
PROCESSING,
@Json(name = "COMPLETED")
COMPLETED,
@Json(name = "CANCELED")
CANCELED,
}
}

View file

@ -37,7 +37,7 @@ data class TangemPayTxHistoryResponse(
@Json(name = "memo") val memo: String? = null,
@Json(name = "receipt") val receipt: Boolean,
@Json(name = "merchant_name") val merchantName: String,
@Json(name = "merchant_category") val merchantCategory: String,
@Json(name = "merchant_category") val merchantCategory: String?,
@Json(name = "merchant_category_code") val merchantCategoryCode: String,
@Json(name = "merchant_id") val merchantId: String? = null,
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,

View file

@ -137,6 +137,9 @@ interface TangemTechApi {
@GET("v1/user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
@POST("v1/user-wallets/wallets")
suspend fun createWallet(@Body body: WalletIdBody): ApiResponse<Unit>
// endregion
// promo

View file

@ -3,6 +3,9 @@ package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.extensions.calculateHashCode
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType.NONE
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
@JsonClass(generateAdapter = true)
@Suppress("BooleanPropertyNaming")
@ -68,4 +71,8 @@ data class UserTokensResponse(
@Json(name = "marketcap")
MARKETCAP,
}
}
}
fun GroupType?.orDefault(): GroupType = this ?: NONE
fun SortType?.orDefault(): SortType = this ?: SortType.MANUAL

View file

@ -7,5 +7,16 @@ import com.squareup.moshi.JsonClass
data class WalletIdBody(
@Json(name = "id") val walletId: String,
@Json(name = "name") val name: String,
@Json(name = "cards") val cards: List<CardInfoBody>,
)
@Json(name = "type") val walletType: WalletType? = null,
@Json(name = "cards") val cards: List<CardInfoBody>? = null,
) {
@JsonClass(generateAdapter = false)
enum class WalletType {
@Json(name = "card")
COLD,
@Json(name = "mobile")
HOT,
}
}

View file

@ -15,9 +15,23 @@ data class GetWalletAccountsResponse(
@JsonClass(generateAdapter = true)
data class Wallet(
@Json(name = "version") val version: Int = 0,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "version") val version: Int? = 0,
@Json(name = "group") val group: GroupType?,
@Json(name = "sort") val sort: SortType?,
@Json(name = "totalAccounts") val totalAccounts: Int,
)
}
/** Flattens the tokens from all wallet accounts into a single list */
fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
return accounts.flatMap { it.tokens.orEmpty() }
}
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
group = wallet.group ?: GroupType.NONE,
sort = wallet.sort ?: SortType.MANUAL,
tokens = flattenTokens(),
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
import com.tangem.datasource.local.accounts.DefaultAccountTokenMigrationStore
import com.tangem.datasource.local.datastore.RuntimeStateStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AccountsMigrationStoreModule {
@Provides
@Singleton
fun provideAccountTokenMigrationStore(): AccountTokenMigrationStore {
return DefaultAccountTokenMigrationStore(
runtimeStateStore = RuntimeStateStore(emptyMap()),
)
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.*
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
@ -39,6 +40,12 @@ internal object ApiConfigsModule {
return StakeKit(stakeKitAuthProvider)
}
@Provides
@IntoSet
fun provideP2PEthPoolConfig(p2pAuthProvider: P2PEthPoolAuthProvider): ApiConfig {
return P2PEthPool(p2pAuthProvider)
}
@Provides
@IntoSet
fun provideTangemTechConfig(
@ -51,6 +58,10 @@ internal object ApiConfigsModule {
appInfoProvider = appInfoProvider,
)
@Provides
@IntoSet
fun provideNewsConfig(authProvider: AuthProvider): ApiConfig = News(authProvider = authProvider)
@Provides
@IntoSet
fun provideYieldSupplyConfig(
@ -74,4 +85,10 @@ internal object ApiConfigsModule {
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
}
@Provides
@IntoSet
fun provideMoonPayConfig(): ApiConfig {
return MoonPay()
}
}

View file

@ -5,13 +5,17 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.MoonPay
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.news.NewsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -71,6 +75,15 @@ internal object NetworkModule {
)
}
@Provides
@Singleton
fun provideP2PEthPoolApi(retrofitApiBuilder: RetrofitApiBuilder): P2PEthPoolApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.P2PEthPool,
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi {
@ -130,4 +143,22 @@ internal object NetworkModule {
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideMoonPayApi(retrofitApiBuilder: RetrofitApiBuilder): MoonPayApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.MoonPay,
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideNewsApi(retrofitApiBuilder: RetrofitApiBuilder): NewsApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.News,
applyTimeoutAnnotations = false,
)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
import com.tangem.datasource.local.news.details.NewsDetailsStore
import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore
import com.tangem.datasource.local.news.trending.TrendingNewsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NewsStoreModule {
@Provides
@Singleton
fun provideNewsDetailsStore(): NewsDetailsStore {
return DefaultNewsDetailsStore(store = RuntimeSharedStore())
}
@Provides
@Singleton
fun provideTrendingNewsStore(): TrendingNewsStore {
return DefaultTrendingNewsStore(store = RuntimeSharedStore())
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object TangemPayStoresModule {
@Provides
@Singleton
fun provideTangemPayCardFrozenStateStore(): TangemPayCardFrozenStateStore {
return DefaultTangemPayCardFrozenStateStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
}

View file

@ -197,6 +197,7 @@ internal class RetrofitApiBuilder @Inject constructor(
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
ApiConfig.ID.MoonPay,
)
}
}

View file

@ -1,91 +0,0 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
/**
* Default implementation of [ExpressServiceLoader]
*
* @property tangemExpressApi express api
* @property expressAssetsStore local storage
*
[REDACTED_AUTHOR]
*/
internal class DefaultExpressServiceLoader @Inject constructor(
private val tangemExpressApi: TangemExpressApi,
private val expressAssetsStore: ExpressAssetsStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : ExpressServiceLoader {
private val initializationStatuses =
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>) {
withContext(dispatchers.io) {
val initializationStatus = getInitializationStatusInternal(userWallet.walletId)
try {
if (userTokens.isNotEmpty()) {
val response = tangemExpressApi.getAssets(
userWalletId = userWallet.walletId.stringValue,
refCode = getRefCode(userWallet, appPreferencesStore),
body = AssetsRequestBody(tokensList = userTokens),
).getOrThrow()
expressAssetsStore.store(userWallet.walletId, response)
initializationStatus.update { response.lceContent() }
}
} catch (e: Throwable) {
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
initializationStatus.update { e.lceError() }
}
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
}
}
}
override fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>> {
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
}
@Suppress("SuspendFunWithFlowReturnType")
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
val initializationStatus = initializationStatuses.value[userWalletId]
if (initializationStatus != null) return initializationStatus
val cached = expressAssetsStore.getSyncOrNull(userWalletId)
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
initializationStatuses.update { statuses ->
statuses.toMutableMap().apply {
put(key = userWalletId, value = default)
}
}
return default
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Express service loader
*
[REDACTED_AUTHOR]
*/
interface ExpressServiceLoader {
/** Update service using [userWallet] and [userTokens] */
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.accounts
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
interface AccountTokenMigrationStore {
fun get(userWalletId: UserWalletId): Flow<Pair<String, String>?>
suspend fun store(userWalletId: UserWalletId, value: Pair<String, String>)
suspend fun remove(userWalletId: UserWalletId)
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.local.accounts
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class DefaultAccountTokenMigrationStore(
private val runtimeStateStore: RuntimeStateStore<Map<UserWalletId, Pair<String, String>>>,
) : AccountTokenMigrationStore {
override fun get(userWalletId: UserWalletId): Flow<Pair<String, String>?> {
return runtimeStateStore.get().map { it[userWalletId] }
}
override suspend fun store(userWalletId: UserWalletId, value: Pair<String, String>) {
runtimeStateStore.update { it + (userWalletId to value) }
}
override suspend fun remove(userWalletId: UserWalletId) {
runtimeStateStore.update { it - userWalletId }
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.local.config.environment
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.datasource.local.config.environment.models.ExpressModel
import com.tangem.datasource.local.config.environment.models.P2PKeys
data class EnvironmentConfig(
val moonPayApiKey: String = "",
@ -16,6 +17,7 @@ data class EnvironmentConfig(
val express: ExpressModel? = null,
val devExpress: ExpressModel? = null,
val stakeKitApiKey: String? = null,
val p2pApiKey: P2PKeys? = null,
val blockAidApiKey: String? = null,
val tangemApiKey: String? = null,
val tangemApiKeyDev: String? = null,

View file

@ -26,6 +26,10 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
apiKey = value.bscQuiknodeApiKey,
subdomain = value.bscQuiknodeSubdomain,
),
quickNodePlasmaCredentials = QuickNodeCredentials(
apiKey = value.quiknodeApiKey,
subdomain = value.quiknodeSubdomain,
),
infuraProjectId = value.infuraProjectId,
tronGridApiKey = value.tronGridApiKey,
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),

View file

@ -25,6 +25,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
express = value.express,
devExpress = value.devExpress,
stakeKitApiKey = value.stakeKitApiKey,
p2pApiKey = value.p2pApiKey,
blockAidApiKey = value.blockaidApiKey,
tangemApiKey = value.tangemApiKey,
tangemApiKeyDev = value.tangemApiKeyDev,

View file

@ -33,6 +33,7 @@ class EnvironmentConfigModel(
@Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?,
@Json(name = "polygonScanApiKey") val polygonScanApiKey: String?,
@Json(name = "stakeKitApiKey") val stakeKitApiKey: String?,
@Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?,
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
@ -97,6 +98,12 @@ data class TonCenterKeys(
@Json(name = "testnet") val testnet: String,
)
@JsonClass(generateAdapter = true)
data class P2PKeys(
@Json(name = "mainnet") val mainnet: String,
@Json(name = "hoodi") val hoodi: String,
)
@JsonClass(generateAdapter = true)
data class GetBlockToken(
@Json(name = "jsonRpc") val jsonRPC: String?,

View file

@ -1,3 +1,7 @@
package com.tangem.datasource.local.datastore.core
@Deprecated(
message = "Use RuntimeSharedStore instead",
replaceWith = ReplaceWith("RuntimeSharedStore"),
)
internal interface StringKeyDataStore<Value : Any> : DataStore<String, Value>

View file

@ -0,0 +1,37 @@
package com.tangem.datasource.local.news.details
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.news.DetailedArticle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class DefaultNewsDetailsStore(
private val store: RuntimeSharedStore<Map<Int, DetailedArticle>>,
) : NewsDetailsStore {
override fun getAll(): Flow<List<DetailedArticle>> {
return store.get().map { it.values.toList() }
}
override suspend fun getSyncOrNull(id: Int): DetailedArticle? {
return store.getSyncOrNull()?.get(id)
}
override suspend fun store(id: Int, article: DetailedArticle) {
store.update(emptyMap()) { current ->
current + (id to article)
}
}
override suspend fun store(articles: Map<Int, DetailedArticle>) {
if (articles.isEmpty()) return
store.update(emptyMap()) { current ->
current + articles
}
}
override suspend fun clear() {
store.store(emptyMap())
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.local.news.details
import com.tangem.domain.models.news.DetailedArticle
import kotlinx.coroutines.flow.Flow
interface NewsDetailsStore {
fun getAll(): Flow<List<DetailedArticle>>
suspend fun getSyncOrNull(id: Int): DetailedArticle?
suspend fun store(id: Int, article: DetailedArticle)
suspend fun store(articles: Map<Int, DetailedArticle>)
suspend fun clear()
}

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.local.news.trending
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.news.ShortArticle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private typealias TrendingCache = Map<String, List<ShortArticle>>
internal class DefaultTrendingNewsStore(
private val store: RuntimeSharedStore<TrendingCache>,
) : TrendingNewsStore {
override fun get(key: String): Flow<List<ShortArticle>> {
return store.get().map { it[key].orEmpty() }
}
override suspend fun getSyncOrNull(key: String): List<ShortArticle>? {
return store.getSyncOrNull()?.get(key)
}
override suspend fun store(key: String, value: List<ShortArticle>) {
store.update(emptyMap()) { current ->
current + (key to value)
}
}
override suspend fun clear() {
store.store(emptyMap())
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.local.news.trending
import com.tangem.domain.models.news.ShortArticle
import kotlinx.coroutines.flow.Flow
interface TrendingNewsStore {
fun get(key: String): Flow<List<ShortArticle>>
suspend fun getSyncOrNull(key: String): List<ShortArticle>?
suspend fun store(key: String, value: List<ShortArticle>)
suspend fun clear()
}

View file

@ -121,6 +121,8 @@ object PreferencesKeys {
val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") }
val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
@ -171,6 +173,9 @@ object PreferencesKeys {
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
// endregion
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.visa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
internal class DefaultTangemPayCardFrozenStateStore(
private val dataStore: StringKeyDataStore<TangemPayCardFrozenState>,
) : TangemPayCardFrozenStateStore {
override suspend fun getSyncOrNull(key: String): TangemPayCardFrozenState? {
return dataStore.getSyncOrNull(key)
}
override fun get(key: String): Flow<TangemPayCardFrozenState> {
return dataStore.get(key)
}
override suspend fun store(key: String, value: TangemPayCardFrozenState) {
dataStore.store(key, value)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
interface TangemPayCardFrozenStateStore {
suspend fun getSyncOrNull(key: String): TangemPayCardFrozenState?
fun get(key: String): Flow<TangemPayCardFrozenState>
suspend fun store(key: String, value: TangemPayCardFrozenState)
}

View file

@ -1,9 +1,13 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens
interface TangemPayStorage {
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
@ -14,5 +18,9 @@ interface TangemPayStorage {
suspend fun clearOrderId(customerWalletAddress: String)
suspend fun clearAll(customerWalletAddress: String)
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
}

View file

@ -71,6 +71,9 @@ class ApiConfigTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.api.common.config.managers
import android.os.Build
import com.google.common.truth.Truth
import com.tangem.common.test.utils.ProvideTestModels
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.*
@ -14,7 +13,9 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.test.core.ProvideTestModels
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
@ -39,6 +40,7 @@ internal class ProdApiConfigsManagerTest {
private val appVersionProvider = mockk<AppVersionProvider>()
private val expressAuthProvider = mockk<ExpressAuthProvider>()
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
private val p2pEthPoolAuthProvider = mockk<P2PEthPoolAuthProvider>()
private val appAuthProvider = mockk<AuthProvider>()
private val appInfoProvider = mockk<AppInfoProvider>()
private val tangemApiKeyProvider = mockk<ProviderSuspend<String>>()
@ -58,6 +60,7 @@ internal class ProdApiConfigsManagerTest {
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { p2pEthPoolAuthProvider.getApiKey() } returns P2P_API_KEY
every { appAuthProvider.getApiKey(any()) } returns tangemApiKeyProvider
coEvery { tangemApiKeyProvider.invoke() } returns TANGEM_API_KEY
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
@ -109,6 +112,9 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
ApiConfig.ID.MoonPay -> MoonPay()
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
}
}
}
@ -121,6 +127,9 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> createStakeKitModel()
ApiConfig.ID.TangemPay -> createTangemPayModel()
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
ApiConfig.ID.MoonPay -> createMoonPayModel()
ApiConfig.ID.P2PEthPool -> createP2PModel()
ApiConfig.ID.News -> createNewsModel()
}
}
@ -239,7 +248,7 @@ internal class ProdApiConfigsManagerTest {
id = ApiConfig.ID.TangemPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
baseUrl = "https://api.dev.us.paera.com/bff/",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
@ -263,6 +272,54 @@ internal class ProdApiConfigsManagerTest {
)
}
private fun createMoonPayModel(): TestModel {
return TestModel(
id = ApiConfig.ID.MoonPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
),
)
}
private fun createP2PModel(): TestModel {
return TestModel(
id = ApiConfig.ID.P2PEthPool,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.p2p.org/",
headers = mapOf(
"Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" },
"accept" to ProviderSuspend { "application/json" },
"Content-Type" to ProviderSuspend { "application/json" },
),
),
)
}
private fun createNewsModel(): TestModel {
val (environment, baseUrl) = when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV to "[REDACTED_ENV_URL]"
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD to "https://tangem.com/"
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
return TestModel(
id = ApiConfig.ID.News,
expected = ApiEnvironmentConfig(
environment = environment,
baseUrl = baseUrl,
headers = mapOf(
"api-key" to ProviderSuspend { TANGEM_API_KEY },
),
),
)
}
private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) {
val c = this[i]
@ -281,6 +338,7 @@ internal class ProdApiConfigsManagerTest {
const val VERSION_NAME = "debug"
const val EXPRESS_SESSION_ID = "express_session_id"
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
const val P2P_API_KEY = "p2p_api_key"
const val APP_CARD_ID = "app_card_id"
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
}

View file

@ -1,9 +1,9 @@
package com.tangem.datasource.local.logs
import com.google.common.truth.Truth
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
/**
[REDACTED_AUTHOR]
@ -12,7 +12,7 @@ import org.junit.jupiter.params.provider.MethodSource
internal class LogsSanitizerTest {
@ParameterizedTest
@MethodSource("provideTestModels")
@ProvideTestModels
fun sanitize(model: TestModel) {
// Act
val actual = LogsSanitizer.sanitize(model.input)