Skip to content

Commit

Permalink
holders cards
Browse files Browse the repository at this point in the history
  • Loading branch information
sorokin0andrey committed Dec 3, 2024
1 parent 9dab89a commit 4ab7b8c
Show file tree
Hide file tree
Showing 82 changed files with 2,245 additions and 224 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/wallet/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
id("kotlin-kapt")
}

android {
Expand Down
19 changes: 16 additions & 3 deletions apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.tonapps.wallet.api

import android.content.Context
import android.util.ArrayMap
import android.util.Log
import com.squareup.moshi.JsonAdapter
import com.tonapps.blockchain.ton.contract.BaseWalletContract
import com.tonapps.blockchain.ton.contract.WalletVersion
Expand All @@ -25,6 +24,7 @@ import com.tonapps.wallet.api.entity.BalanceEntity
import com.tonapps.wallet.api.entity.ChartEntity
import com.tonapps.wallet.api.entity.ConfigEntity
import com.tonapps.wallet.api.entity.TokenEntity
import com.tonapps.wallet.api.holders.HoldersApi
import com.tonapps.wallet.api.internal.ConfigRepository
import com.tonapps.wallet.api.internal.InternalApi
import io.batteryapi.apis.BatteryApi
Expand Down Expand Up @@ -73,19 +73,29 @@ class API(
private val internalApi = InternalApi(context, defaultHttpClient)
private val configRepository = ConfigRepository(context, scope, internalApi)


val config: ConfigEntity
get() = configRepository.configEntity

val configTestnet: ConfigEntity
get() = configRepository.configTestnetEntity

val configFlow: Flow<ConfigEntity>
get() = configRepository.stream

private val tonAPIHttpClient: OkHttpClient by lazy {
tonAPIHttpClient { config }
}

val holdersApi = HoldersApi(defaultHttpClient, ::getConfig)

@Volatile
private var cachedCountry: String? = null

fun getConfig(testnet: Boolean): ConfigEntity {
return if (testnet) configTestnet else config
}

suspend fun tonapiFetch(
url: String,
options: String
Expand Down Expand Up @@ -394,8 +404,7 @@ class API(
}
}

fun getRates(currency: String, tokens: List<String>): Map<String, TokenRates>? {
val currencies = listOf(currency, "TON")
fun getRates(currencies: List<String>, tokens: List<String>): Map<String, TokenRates>? {
return withRetry {
rates().getRates(
tokens = tokens,
Expand All @@ -404,6 +413,10 @@ class API(
}
}

fun getRates(currency: String, tokens: List<String>): Map<String, TokenRates>? {
return getRates(listOf(currency, "TON"), tokens)
}

fun getNft(address: String, testnet: Boolean): NftItem? {
return withRetry { nft(testnet).getNftItemByAddress(address) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ data class ConfigEntity(
val burnZeroDomain: String,
val scamAPIURL: String,
val reportAmount: Coins,
val stories: List<String>
val stories: List<String>,
val holdersAppEndpoint: String,
val holdersServiceEndpoint: String,
): Parcelable {

@IgnoredOnParcel
Expand Down Expand Up @@ -118,7 +120,9 @@ data class ConfigEntity(
burnZeroDomain = json.optString("burnZeroDomain", "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ"), // tonkeeper-zero.ton
scamAPIURL = json.optString("scam_api_url", "https://scam.tonkeeper.com"),
reportAmount = Coins.of(json.optString("reportAmount") ?: "0.03"),
stories = json.getJSONArray("stories").toStringList()
stories = json.getJSONArray("stories").toStringList(),
holdersAppEndpoint = json.optString("holdersAppEndpoint", "https://app.holders.io"),
holdersServiceEndpoint = json.optString("holdersServiceEndpoint", "https://card-prod.whales-api.com")
)

constructor() : this(
Expand Down Expand Up @@ -164,7 +168,9 @@ data class ConfigEntity(
burnZeroDomain = "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ",
scamAPIURL = "https://scam.tonkeeper.com",
reportAmount = Coins.of("0.03"),
stories = emptyList()
stories = emptyList(),
holdersAppEndpoint = "https://app.holders.io",
holdersServiceEndpoint = "https://card-prod.whales-api.com"
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.tonapps.wallet.api.entity

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.json.JSONObject

@Parcelize
data class ConfigResponseEntity(
val mainnet: ConfigEntity,
val testnet: ConfigEntity,
): Parcelable {
constructor(json: JSONObject, debug: Boolean) : this(
mainnet = ConfigEntity(json.getJSONObject("mainnet"), debug),
testnet = ConfigEntity(json.getJSONObject("testnet"), debug)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.tonapps.wallet.api.holders

import android.os.Parcelable
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.parcelize.Parcelize

@Parcelize
data class HoldersAccountEntity(
@Json(name = "id") val id: String,
@Json(name = "accountIndex") val accountIndex: Int,
@Json(name = "address") val address: String?,
@Json(name = "name") val name: String?,
@Json(name = "seed") val seed: String?,
@Json(name = "state") val state: String,
@Json(name = "balance") val balance: String,
@Json(name = "tzOffset") val tzOffset: Int,
@Json(name = "contract") val contract: String,
@Json(name = "partner") val partner: String,
@Json(name = "network") val network: String,
@Json(name = "ownerAddress") val ownerAddress: String,
@Json(name = "cryptoCurrency") val cryptoCurrency: CryptoCurrency?,
@Json(name = "limits") val limits: AccountLimits?,
@Json(name = "cards") val cards: List<HoldersCardEntity>
): Parcelable {
@Parcelize
data class CryptoCurrency(
val decimals: Int,
val ticker: String,
val tokenContract: String?
): Parcelable

@Parcelize
data class AccountLimits(
val tzOffset: Int,
val dailyDeadline: Int,
val dailySpent: String,
val monthlyDeadline: Int,
val monthlySpent: String,
val monthly: String,
val daily: String,
val onetime: String
): Parcelable

fun toJSON(): String {
val moshi = Moshi.Builder()
.add(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(HoldersAccountEntity::class.java)
return adapter.toJson(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tonapps.wallet.api.holders

import com.squareup.moshi.Json

data class HoldersAccountTokenResponse(
@Json(name = "ok") val ok: Boolean,
@Json(name = "token") val token: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tonapps.wallet.api.holders

import com.squareup.moshi.Json

data class HoldersAccountsResponse(
@Json(name = "ok") val ok: Boolean,
@Json(name = "list") val list: List<HoldersAccountEntity>,
@Json(name = "prepaidCards") val prepaidCards: List<HoldersCardEntity>,
@Json(name = "error") val error: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.tonapps.wallet.api.holders

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tonapps.blockchain.ton.contract.BaseWalletContract
import com.tonapps.blockchain.ton.extensions.base64
import com.tonapps.blockchain.ton.extensions.hex
import com.tonapps.blockchain.ton.extensions.toAccountId
import com.tonapps.blockchain.ton.proof.TONProof
import com.tonapps.wallet.api.entity.ConfigEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody

class HoldersApi(
private val okHttpClient: OkHttpClient,
private val getConfig: (testnet: Boolean) -> ConfigEntity
) {

private fun endpoint(path: String, testnet: Boolean): String {
val host =
if (testnet) "https://card-staging.whales-api.com" else getConfig(false).holdersServiceEndpoint
return host + path
}

private suspend inline fun <reified T> post(
path: String,
testnet: Boolean,
payload: Map<String, Any>
): T = withContext(Dispatchers.IO) {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(Map::class.java)
val jsonPayload = jsonAdapter.toJson(payload)
val requestBody = jsonPayload.toRequestBody("application/json".toMediaType())

val url = endpoint(path, testnet)

val request = Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Content-Type", "application/json")
.addHeader("Access-Control-Allow-Origin", "*")
.addHeader("Access-Control-Allow-Headers", "*")
.addHeader("Access-Control-Allow-Credentials", "true")
.build()

val response = okHttpClient.newCall(request).execute()

if (response.code == 401) {
throw IllegalArgumentException("Unauthorized")
}

val responseBody = response.body?.string() ?: throw Exception("Empty response body")
val adapter: JsonAdapter<T> = moshi.adapter(T::class.java)

val parseResult = adapter.fromJson(responseBody)
?: throw IllegalArgumentException("Invalid response")

parseResult
}

private fun getNetwork(testnet: Boolean): String {
return if (testnet) "ton-testnet" else "ton-mainnet"
}

suspend fun fetchAccountsPublic(
address: String,
testnet: Boolean
): List<HoldersAccountEntity> = withContext(Dispatchers.IO) {
val response = post<HoldersPublicAccountsResponse>(
"/v2/public/accounts", testnet, mapOf(
"walletKind" to "tonkeeper",
"network" to getNetwork(testnet),
"address" to address
)
)

if (!response.ok) {
throw IllegalArgumentException("Error fetching card list: ${response.error}")
}

response.accounts
}

suspend fun fetchAccountsList(
token: String,
testnet: Boolean
): HoldersAccountsResponse = withContext(Dispatchers.IO) {
val response = post<HoldersAccountsResponse>(
"/v2/account/list", testnet, mapOf(
"token" to token
)
)

if (!response.ok) {
throw IllegalArgumentException("Error fetching card list: ${response.error}")
}

response
}

suspend fun fetchAccountToken(
contract: BaseWalletContract,
proof: TONProof.Result,
testnet: Boolean
): String = withContext(Dispatchers.IO) {
val payload = mapOf(
"stack" to "ton",
"network" to getNetwork(testnet),
"key" to mapOf(
"kind" to "tonconnect-v2",
"wallet" to "tonkeeper",
"config" to mapOf(
"address" to contract.address.toAccountId(),
"proof" to mapOf(
"timestamp" to proof.timestamp,
"domain" to mapOf(
"lengthBytes" to proof.domain.lengthBytes,
"value" to proof.domain.value
),
"signature" to proof.signature,
"payload" to proof.payload,
"walletStateInit" to contract.stateInitCell().base64(),
"publicKey" to contract.publicKey.hex()
)
)
)
)

val response =
post<HoldersAccountTokenResponse>("/v2/user/wallet/connect", testnet, payload)

if (!response.ok) {
throw IllegalArgumentException("Error fetching account token")
}

response.token
}

suspend fun fetchUserState(token: String, testnet: Boolean) = withContext(Dispatchers.IO) {
val response = post<HoldersUserState>(
"/v2/user/state", testnet, mapOf(
"token" to token
)
)

if (!response.ok) {
throw IllegalArgumentException("Error fetching user state")
}

response.toJSON()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.tonapps.wallet.api.holders

import android.os.Parcelable
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.parcelize.Parcelize

@Parcelize
data class HoldersCardEntity(
@Json(name = "id") val id: String,
@Json(name = "type") val type: String,
@Json(name = "status") val status: String,
@Json(name = "walletId") val walletId: String?,
@Json(name = "fiatCurrency") val fiatCurrency: String,
@Json(name = "fiatBalance") val fiatBalance: String?,
@Json(name = "lastFourDigits") val lastFourDigits: String?,
@Json(name = "productId") val productId: String,
@Json(name = "personalizationCode") val personalizationCode: String,
@Json(name = "seed") val seed: String?,
@Json(name = "updatedAt") val updatedAt: String,
@Json(name = "createdAt") val createdAt: String,
@Json(name = "provider") val provider: String?,
@Json(name = "kind") val kind: String?
): Parcelable {
fun toJSON(): String {
val moshi = Moshi.Builder()
.add(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(HoldersCardEntity::class.java)
return adapter.toJson(this)
}
}
Loading

0 comments on commit 4ab7b8c

Please sign in to comment.