Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native Swap + Animation tools, core improvements & bugfixes, new widgets, and more. #17

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 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,15 +2,14 @@ package com.tonapps.wallet.api

import android.content.Context
import android.util.ArrayMap
import android.util.Log
import com.tonapps.blockchain.Coin
import com.tonapps.blockchain.ton.extensions.base64
import com.tonapps.blockchain.ton.extensions.isValid
import com.tonapps.extensions.ifPunycodeToUnicode
import com.tonapps.extensions.locale
import com.tonapps.extensions.unicodeToPunycode
import com.tonapps.network.SSEvent
import com.tonapps.network.get
import com.tonapps.network.getRequest
import com.tonapps.network.interceptor.AcceptLanguageInterceptor
import com.tonapps.network.interceptor.AuthorizationInterceptor
import com.tonapps.network.post
Expand Down Expand Up @@ -56,6 +55,9 @@ class API(
private val tonAPIHttpClient: OkHttpClient by lazy {
createTonAPIHttpClient(context, config.tonApiV2Key)
}
private val stonfiAPIHttpClient: OkHttpClient by lazy {
createStonfiAPIHttpClient(context)
}

private val internalApi = InternalApi(context, defaultHttpClient)
private val configRepository = ConfigRepository(context, scope, internalApi)
Expand Down Expand Up @@ -202,6 +204,41 @@ class API(
return tonAPIHttpClient.sse(url)
}

fun stonfiApi(url: String): JSONObject {
val response = stonfiAPIHttpClient.getRequest(url, ArrayMap<String, String>().apply {
set("accept", "application/json")
})
val body = response.body?.string()
if (!response.isSuccessful) {
throw Exception("Failed STON.fi API: ${response.code}, body: $body")
}
if (body.isNullOrEmpty()) {
throw Exception("Empty response")
}
return JSONObject(body)
}

fun stonfiRpc(method: String, params: JSONObject? = null, requestId: Long = ++stonfiRequestsCount, rpcVersion: String = "2.0"): JSONObject {
val json = JSONObject().apply {
put("jsonrpc", rpcVersion)
put("id", requestId)
put("method", method)
put("params", params ?: JSONObject())
}
val data = json.toString()
val response = stonfiAPIHttpClient.postJSON(STONFI_RPC_URL, data, ArrayMap<String, String>().apply {
set("accept", "application/json")
})
val body = response.body?.string()
if (!response.isSuccessful) {
throw Exception("Failed STON.fi RPC: ${response.code}, body: $body")
}
if (body.isNullOrEmpty()) {
throw Exception("Empty response")
}
return JSONObject(body)
}

fun tonconnectPayload(): String {
val url = "${config.tonapiMainnetHost}/v2/tonconnect/payload"
val json = JSONObject(tonAPIHttpClient.get(url))
Expand All @@ -226,9 +263,10 @@ class API(
) {
val mimeType = "text/plain".toMediaType()
val url = "${BRIDGE_URL}/message?client_id=$publicKeyHex&to=$clientId&ttl=300"
val response = tonAPIHttpClient.post(url, body.toRequestBody(mimeType))
if (!response.isSuccessful) {
throw Exception("Failed sending event: ${response.code}")
tonAPIHttpClient.post(url, body.toRequestBody(mimeType)).use { response ->
if (!response.isSuccessful) {
throw Exception("Failed sending event: ${response.code}")
}
}
}

Expand Down Expand Up @@ -315,7 +353,9 @@ class API(
json.put("token", firebaseToken)
json.put("accounts", accountsArray)

return tonAPIHttpClient.postJSON(url, json.toString()).isSuccessful
tonAPIHttpClient.postJSON(url, json.toString()).use { response ->
return response.isSuccessful
}
} catch (e: Throwable) {
false
}
Expand Down Expand Up @@ -344,7 +384,9 @@ class API(

tonAPIHttpClient.postJSON(url, data, ArrayMap<String, String>().apply {
set("X-TonConnect-Auth", token)
}).isSuccessful
}).use { response ->
response.isSuccessful
}
} catch (e: Throwable) {
false
}
Expand Down Expand Up @@ -382,6 +424,10 @@ class API(
companion object {

const val BRIDGE_URL = "https://bridge.tonapi.io/bridge"
const val STONFI_RPC_URL = "https://app.ston.fi/rpc"
const val STONFI_API_URL = "https://api.ston.fi/v1"

private var stonfiRequestsCount: Long = 0

val JSON = Json { prettyPrint = true }

Expand All @@ -403,5 +449,13 @@ class API(
.addInterceptor(AuthorizationInterceptor.bearer(tonApiV2Key))
.build()
}

private fun createStonfiAPIHttpClient(
context: Context
): OkHttpClient {
return baseOkHttpClientBuilder()
.addInterceptor(AcceptLanguageInterceptor(context.locale))
.build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class BalanceEntity(
val token: TokenEntity,
val value: Float,
@Deprecated("""
32-bit float isn't suitable for coins, and causes a lot bugs in different parts of the app.

Rework to java.math.BigDecimal for coins and org.ton.bigint.BigInt for nano units.

See deprecations in com.tonapps.blockchain.Coin
""") val value: Float,
val walletAddress: String
): Parcelable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.tonapps.wallet.api.entity

import android.net.Uri
import android.os.Parcelable
import com.tonapps.blockchain.ton.extensions.toUserFriendly
import com.tonapps.wallet.api.R
import io.tonapi.models.JettonPreview
import io.tonapi.models.JettonVerificationType
Expand All @@ -22,6 +23,13 @@ data class TokenEntity(
}

companion object {
@Deprecated("""
Consider dropping special address value for TON,
as it requires adding extra handling everywhere TokenEntity is used.

Rely only on proper address value,
or, better, use special type with cache for raw & user-friendly addresses.
""")
val TON = TokenEntity(
address = "TON",
name = "Toncoin",
Expand All @@ -31,15 +39,36 @@ data class TokenEntity(
verification = Verification.whitelist
)

const val TON_CONTRACT_USER_FRIENDLY_ADDRESS = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"
const val TON_CONTRACT_RAW_ADDRESS = "0:0000000000000000000000000000000000000000000000000000000000000000"

private fun convertVerification(verification: JettonVerificationType): Verification {
return when (verification) {
JettonVerificationType.whitelist -> Verification.whitelist
JettonVerificationType.blacklist -> Verification.blacklist
else -> Verification.none
}
}

// TODO(API): this should not be hardcoded

const val USDT_SYMBOL = "USD₮"
const val USDT_CONTRACT_RAW_ADDRESS = "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe"

fun specialSymbol(symbol: String, address: String): String? {
return if (isUSDT(symbol, address)) {
TON.symbol
} else {
null
}
}

fun isUSDT(symbol: String, address: String): Boolean {
return symbol == USDT_SYMBOL && address == USDT_CONTRACT_RAW_ADDRESS
}
}


val isTon: Boolean
get() = address == TON.address

Expand All @@ -51,4 +80,20 @@ data class TokenEntity(
decimals = jetton.decimals,
verification = convertVerification(jetton.verification)
)

fun getRawAddress(): String {
return if (isTon) {
TON_CONTRACT_RAW_ADDRESS
} else {
address
}
}

fun getUserFriendlyAddress(wallet: Boolean, testnet: Boolean): String {
return if (isTon) {
TON_CONTRACT_USER_FRIENDLY_ADDRESS
} else {
address.toUserFriendly(wallet, testnet)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.tonapps.wallet.data.account

import android.content.Context
import android.util.Log
import android.net.Uri
import com.tonapps.blockchain.ton.contract.WalletVersion
import com.tonapps.blockchain.ton.extensions.base64
import com.tonapps.blockchain.ton.extensions.toAccountId
import com.tonapps.security.securePrefs
import com.tonapps.wallet.api.API
import com.tonapps.wallet.data.account.entities.MessageBodyEntity
import com.tonapps.wallet.data.account.entities.WalletEntity
Expand All @@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
Expand All @@ -34,7 +32,6 @@ import org.ton.api.pk.PrivateKeyEd25519
import org.ton.api.pub.PublicKeyEd25519
import org.ton.cell.Cell
import org.ton.contract.wallet.WalletTransfer
import org.ton.crypto.base64
import org.ton.mnemonic.Mnemonic

class WalletRepository(
Expand Down Expand Up @@ -115,6 +112,11 @@ class WalletRepository(
return WalletEntity(legacyWallet)
}

suspend fun getWalletByAddress(address: String): WalletEntity? {
val legacyWallet = legacyManager.getWallets().find { it.address == address } ?: return null
return WalletEntity(legacyWallet)
}

suspend fun getWallet(id: Long): WalletEntity? {
val legacyWallet = legacyManager.getWallets().find { it.id == id } ?: return null
return WalletEntity(legacyWallet)
Expand Down Expand Up @@ -307,4 +309,21 @@ class WalletRepository(
}
return list
}

fun walletAddress(jettonMaster: String, owner: String): String {
// https://api.ston.fi/v1/jetton/${jettonMaster}/address?owner_address=${owner}
val url = Uri.parse("${API.STONFI_API_URL}/jetton")
.buildUpon()
.appendPath(jettonMaster)
.appendPath("address")
.appendQueryParameter("owner_address", owner)
.build()
.toString()
val response = api.stonfiApi(url)
return response.getString("address")
}

suspend fun walletSeqno(wallet: WalletEntity): Int {
return api.getAccountSeqno(wallet.accountId, wallet.testnet)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ data class WalletCurrency(
"TON", "BTC",
)

const val USD_CODE = "USD"
const val TON_CODE = "TON"

val DEFAULT = WalletCurrency(FIAT.first())
val TON = WalletCurrency("TON")
val USD = WalletCurrency(USD_CODE)
val TON = WalletCurrency(TON_CODE)

val ALL = FIAT + CRYPTO
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import com.tonapps.wallet.localization.Language
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
Expand All @@ -33,6 +30,7 @@ class SettingsRepository(
private const val FIREBASE_TOKEN_KEY = "firebase_token"
private const val INSTALL_ID_KEY = "install_id"
private const val SEARCH_ENGINE_KEY = "search_engine"
private const val SWAP_DETAILS_OPEN_KEY = "swap_details_open"
}

private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
Expand All @@ -58,6 +56,9 @@ class SettingsRepository(
private val _searchEngineFlow = MutableEffectFlow<SearchEngine>()
val searchEngineFlow = _searchEngineFlow.stateIn(scope, SharingStarted.Eagerly, null).filterNotNull()

private val _swapDetailsOpenFlow = MutableEffectFlow<Boolean>()
val swapDetailsOpenFlow = _swapDetailsOpenFlow.stateIn(scope, SharingStarted.Eagerly, null).filterNotNull()

private val prefs = context.getSharedPreferences(NAME, Context.MODE_PRIVATE)

val installId: String
Expand Down Expand Up @@ -146,6 +147,15 @@ class SettingsRepository(
}
}

var swapDetailsOpen: Boolean = prefs.getBoolean(SWAP_DETAILS_OPEN_KEY, false)
set(value) {
if (value != field) {
prefs.edit().putBoolean(SWAP_DETAILS_OPEN_KEY, value).apply()
field = value
_swapDetailsOpenFlow.tryEmit(value)
}
}

init {
scope.launch(Dispatchers.IO) {
_currencyFlow.tryEmit(currency)
Expand All @@ -155,6 +165,7 @@ class SettingsRepository(
_firebaseTokenFlow.tryEmit(firebaseToken)
_countryFlow.tryEmit(country)
_searchEngineFlow.tryEmit(searchEngine)
_swapDetailsOpenFlow.tryEmit(swapDetailsOpen)
}
}
}
Loading