diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4deafcaa4..9372310aa 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -27,11 +27,14 @@ jobs: distribution: 'temurin' java-version: 20.0.2+9 + - name: Accept Android SDK licenses + run: yes | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --licenses + - name: Decode signing certificate into a file env: - CERTIFICATE_BASE64: ${{ secrets.ANDROID_DIST_SIGNING_KEY }} + CERTIFICATE_BASE64: ${{ secrets.ANDROID_DIST_SIGNING_KEY }} run: | - echo $CERTIFICATE_BASE64 | base64 --decode > google-release.keystore + echo $CERTIFICATE_BASE64 | base64 --decode > google-release.keystore - name: Build android beta run: bundle exec fastlane android beta diff --git a/.gitignore b/.gitignore index 33eef6ddc..0e69164b8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ captures .cxx *.apk .kotlin -build +/build diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b86273d94..312bf2eab 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index fb6ee35d1..8cfc672e9 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -47,6 +47,7 @@ + diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index d4b7accba..c224ad564 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a35..99b6bd53c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,7 @@ + - + diff --git a/Gemfile.lock b/Gemfile.lock index 5c88840c5..ab3f6a0a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -222,4 +222,4 @@ RUBY VERSION ruby 3.3.0p0 BUNDLED WITH - 2.5.4 + 2.5.4 \ No newline at end of file diff --git a/apps/signer/build.gradle.kts b/apps/signer/build.gradle.kts index 10c0c5cb5..97604812e 100644 --- a/apps/signer/build.gradle.kts +++ b/apps/signer/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -9,10 +11,10 @@ android { defaultConfig { applicationId = Build.namespacePrefix("signer") - minSdk = Build.minSdkVersion - targetSdk = 34 - versionCode = 22 - versionName = "0.2.2" + minSdk = 26 + targetSdk = Build.compileSdkVersion + versionCode = 23 + versionName = "0.2.3" } lint { @@ -43,51 +45,42 @@ android { } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } - packaging { resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}") } } dependencies { - implementation(Dependence.AndroidX.core) - implementation(Dependence.AndroidX.appCompat) - implementation(Dependence.AndroidX.activity) - implementation(Dependence.AndroidX.fragment) - implementation(Dependence.AndroidX.recyclerView) - implementation(Dependence.AndroidX.viewPager2) - implementation(Dependence.AndroidX.splashscreen) + implementation(libs.androidX.core) + implementation(libs.androidX.appCompat) + implementation(libs.androidX.activity) + implementation(libs.androidX.fragment) + implementation(libs.androidX.recyclerView) + implementation(libs.androidX.viewPager2) + implementation(libs.androidX.splashscreen) - implementation(Dependence.UI.material) - implementation(Dependence.UI.flexbox) - implementation(Dependence.AndroidX.Camera.base) - implementation(Dependence.AndroidX.Camera.core) - implementation(Dependence.AndroidX.Camera.lifecycle) - implementation(Dependence.AndroidX.Camera.view) - implementation(Dependence.AndroidX.security) - implementation(Dependence.AndroidX.constraintlayout) - implementation(Dependence.AndroidX.lifecycleSavedState) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.extensions)) + implementation(libs.material) + implementation(libs.flexbox) + implementation(libs.cameraX.base) + implementation(libs.cameraX.core) + implementation(libs.cameraX.lifecycle) + implementation(libs.cameraX.view) + implementation(libs.androidX.security) + implementation(libs.androidX.constraintlayout) + implementation(libs.androidX.lifecycleSavedState) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.extensions)) - implementation(Dependence.KotlinX.guava) + implementation(libs.kotlinX.coroutines.guava) - implementation(project(Dependence.UIKit.core)) { + implementation(project(ProjectModules.UIKit.core)) { exclude("com.airbnb.android", "lottie") exclude("com.facebook.fresco", "fresco") } - implementation(project(Dependence.Lib.qr)) - implementation(project(Dependence.Lib.security)) - implementation(project(Dependence.Lib.icu)) - implementation(Dependence.Koin.core) + implementation(project(ProjectModules.Lib.qr)) + implementation(project(ProjectModules.Lib.security)) + implementation(project(ProjectModules.Lib.icu)) + implementation(libs.koin.core) } diff --git a/apps/signer/lint-baseline.xml b/apps/signer/lint-baseline.xml new file mode 100644 index 000000000..65ada1635 --- /dev/null +++ b/apps/signer/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/apps/signer/proguard-rules.pro b/apps/signer/proguard-rules.pro index de400b7f8..71a6747bd 100644 --- a/apps/signer/proguard-rules.pro +++ b/apps/signer/proguard-rules.pro @@ -7,3 +7,9 @@ -repackageclasses '' -renamesourcefileattribute SourceFile -dontskipnonpubliclibraryclasses + +-dontwarn com.fasterxml.jackson.databind.ext.Java7SupportImpl +-keep class com.fasterxml.jackson.databind.ext.** { *; } +-dontwarn org.slf4j.** +-dontwarn org.w3c.dom.** +-dontwarn com.fasterxml.jackson.databind.ext.DOMSerializer \ No newline at end of file diff --git a/apps/signer/src/main/java/com/tonapps/signer/screen/camera/CameraFragment.kt b/apps/signer/src/main/java/com/tonapps/signer/screen/camera/CameraFragment.kt index 8221904de..5ee72bcd8 100644 --- a/apps/signer/src/main/java/com/tonapps/signer/screen/camera/CameraFragment.kt +++ b/apps/signer/src/main/java/com/tonapps/signer/screen/camera/CameraFragment.kt @@ -71,6 +71,8 @@ class CameraFragment: BaseFragment(R.layout.fragment_camera), BaseFragment.Botto headerView.background = null headerView.doOnCloseClick = { finish() } + view.findViewById(R.id.permission_header).doOnCloseClick = { finish() } + cameraView = view.findViewById(R.id.camera) flashView = view.findViewById(R.id.flash) diff --git a/apps/signer/src/main/java/com/tonapps/signer/screen/settings/SettingsFragment.kt b/apps/signer/src/main/java/com/tonapps/signer/screen/settings/SettingsFragment.kt index f2c5ce05a..777211bc1 100644 --- a/apps/signer/src/main/java/com/tonapps/signer/screen/settings/SettingsFragment.kt +++ b/apps/signer/src/main/java/com/tonapps/signer/screen/settings/SettingsFragment.kt @@ -54,7 +54,7 @@ class SettingsFragment: BaseFragment(R.layout.fragment_settings), BaseFragment.S } private fun openSupport() { - val uri = Uri.parse("https://t.me/help_tonkeeper_bot") + val uri = Uri.parse("https://t.me/tonkeeper") val intent = Intent(Intent.ACTION_VIEW, uri) startActivity(intent) } diff --git a/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignFragment.kt b/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignFragment.kt index 67d354e1b..4c7339234 100644 --- a/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignFragment.kt +++ b/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignFragment.kt @@ -6,8 +6,11 @@ import android.os.Bundle import android.text.SpannableString import android.text.method.ScrollingMovementMethod import android.view.View +import android.view.ViewGroup import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.ViewCompat +import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import com.tonapps.blockchain.ton.TonNetwork import com.tonapps.security.hex @@ -36,7 +39,9 @@ import org.koin.core.parameter.parametersOf import org.ton.cell.Cell import uikit.HapticHelper import uikit.base.BaseFragment +import uikit.extensions.bottomBarsOffset import uikit.extensions.collectFlow +import uikit.extensions.pinToBottomInsets import uikit.extensions.setColor import uikit.navigation.Navigation.Companion.navigation import uikit.widget.LoaderView @@ -129,6 +134,13 @@ class SignFragment: BaseFragment(R.layout.fragment_sign), BaseFragment.Modal { collectFlow(signViewModel.actionsFlow, adapter::submitList) collectFlow(signViewModel.keyEntity, ::setKeyEntity) + + ViewCompat.setOnApplyWindowInsetsListener(view) { view, insets -> + actionView.updateLayoutParams { + bottomMargin = insets.bottomBarsOffset + } + insets + } } private fun showError() { @@ -137,7 +149,11 @@ class SignFragment: BaseFragment(R.layout.fragment_sign), BaseFragment.Modal { } private fun copyBody() { - requireContext().copyToClipboard(args.bodyHex) + signViewModel.openEmulate().catch { + navigation?.toast(R.string.unknown_error) + }.onEach{ + requireContext().copyToClipboard(it) + }.launchIn(lifecycleScope) } private fun reject() { diff --git a/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignViewModel.kt b/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignViewModel.kt index 7a7724169..37402c17b 100644 --- a/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignViewModel.kt +++ b/apps/signer/src/main/java/com/tonapps/signer/screen/sign/SignViewModel.kt @@ -7,9 +7,9 @@ import com.tonapps.blockchain.ton.TonNetwork import com.tonapps.blockchain.ton.contract.BaseWalletContract import com.tonapps.blockchain.ton.extensions.EmptyPrivateKeyEd25519 import com.tonapps.blockchain.ton.extensions.hex +import com.tonapps.blockchain.ton.extensions.loadString import com.tonapps.blockchain.ton.tlb.JettonTransfer import com.tonapps.blockchain.ton.tlb.NftTransfer -import com.tonapps.blockchain.ton.tlb.StringTlbConstructor import com.tonapps.icu.CurrencyFormatter import com.tonapps.signer.core.repository.KeyRepository import com.tonapps.signer.password.Password @@ -38,6 +38,7 @@ import org.ton.tlb.constructor.AnyTlbConstructor import org.ton.tlb.loadTlb import com.tonapps.security.vault.safeArea import com.tonapps.uikit.list.ListCell +import org.ton.cell.loadRef import java.math.BigDecimal class SignViewModel( @@ -52,6 +53,8 @@ class SignViewModel( val keyEntity = repository.getKey(id).filterNotNull() + private var normalizedV = v + private val _actionsFlow = MutableStateFlow?>(null) val actionsFlow = _actionsFlow.asStateFlow().filterNotNull() @@ -68,8 +71,13 @@ class SignViewModel( }.flowOn(Dispatchers.IO).take(1) fun openEmulate() = keyEntity.map { - val contract = BaseWalletContract.create(it.publicKey, v, network.value) - val cell = contract.createTransferMessageCell(contract.address, EmptyPrivateKeyEd25519.invoke(), seqno, unsignedBody) + val contract = BaseWalletContract.create(it.publicKey, normalizedV, network.value) + val cell = contract.createTransferMessageCell( + address = contract.address, + privateKey = EmptyPrivateKeyEd25519.invoke(), + seqNo = seqno, + unsignedBody = unsignedBody + ) cell.hex() }.flowOn(Dispatchers.IO).take(1) @@ -77,7 +85,7 @@ class SignViewModel( return privateKey.sign(unsignedBody.hash().toByteArray()) } - private fun parseBoc(): List { + private fun parseV4Boc(): List { val items = mutableListOf() try { val slice = unsignedBody.beginParse() @@ -92,7 +100,48 @@ class SignViewModel( val item = parseMessage(msg, position) items.add(item) } - } catch (ignored: Throwable) {} + } catch (_: Throwable) {} + return items + } + + private fun parseV5Boc(): List { + val items = mutableListOf() + try { + val slice = unsignedBody.beginParse() + val opCode = slice.loadUInt(32) + val serialized = slice.loadInt(32) + val seqno = slice.loadUInt(32) + val validUntil = slice.loadUInt(32) + + var list = slice.loadRef() + while (!list.bits.isEmpty() || list.refs.isNotEmpty()) { + val cellSlice = list.beginParse() + + val prev = cellSlice.loadRef() + val tag = cellSlice.loadUInt(32) + val sendMode = cellSlice.loadUInt(8) + val msg = cellSlice.loadRef { + loadTlb(MessageRelaxed.tlbCodec(AnyTlbConstructor)) + } + val position = ListCell.getPosition(1, 0) + items.add(parseMessage(msg, position)) + + list = prev + } + } catch (_: Throwable) {} + return items + } + + private fun parseBoc(): List { + val items = mutableListOf() + items.addAll(parseV4Boc()) + + val v5Boc = parseV5Boc() + if (!v5Boc.isEmpty()) { + items.addAll(parseV5Boc()) + normalizedV = "v5r1" + } + if (items.isEmpty()) { items.add(SignItem.Unknown(ListCell.Position.SINGLE)) } @@ -101,20 +150,13 @@ class SignViewModel( private fun parseMessage(msg: MessageRelaxed, position: ListCell.Position): SignItem { try { - val info = msg.info as CommonMsgInfoRelaxed.IntMsgInfoRelaxed val body = getBody(msg.body) val opCode = parseOpCode(body) val jettonTransfer = parseJettonTransfer(opCode, body) val nftTransfer = parseNftTransfer(opCode, body) - val target = if (nftTransfer != null) { - nftTransfer.newOwnerAddress - } else if (jettonTransfer != null) { - jettonTransfer.toAddress - } else { - info.dest - } + val target = nftTransfer?.newOwnerAddress?: (jettonTransfer?.toAddress ?: info.dest) val value = if (nftTransfer != null) { "NFT" @@ -137,7 +179,7 @@ class SignViewModel( value2 = value2, extra = nftTransfer != null || jettonTransfer != null ) - } catch (e: Throwable) { + } catch (_: Throwable) { return SignItem.Unknown(position) } } @@ -186,7 +228,7 @@ class SignViewModel( } else if (nftTransfer != null) { nftTransfer.comment } else { - cell?.parse { loadTlb(StringTlbConstructor) } + cell?.parse { loadString() } } if (string != null) { return string @@ -200,7 +242,7 @@ class SignViewModel( private fun formatCoins(coins: Coins): String { val value = BigDecimal(coins.amount.toLong() / 1000000000L.toDouble()) - return CurrencyFormatter.format("TON", value, 9).toString() + return CurrencyFormatter.format("TON", value).toString() } private fun parseAddress(address: MsgAddressInt, bounceable: Boolean = true): String { diff --git a/apps/signer/src/main/res/layout/fragment_emulate.xml b/apps/signer/src/main/res/layout/fragment_emulate.xml index e70961b5e..46de28bf2 100644 --- a/apps/signer/src/main/res/layout/fragment_emulate.xml +++ b/apps/signer/src/main/res/layout/fragment_emulate.xml @@ -2,7 +2,6 @@ by lazy { - Serializer.moshi.adapter(MessageConsequences::class.java) + val tron: TronApi by lazy { + TronApi(config, defaultHttpClient, batteryProvider.default.get(false)) } - val tron = TronApi(config, defaultHttpClient, batteryApi.get(false)) - fun accounts(testnet: Boolean) = provider.accounts.get(testnet) fun jettons(testnet: Boolean) = provider.jettons.get(testnet) @@ -173,7 +173,11 @@ class API( fun rates() = provider.rates.get(false) - fun battery(testnet: Boolean) = batteryApi.get(testnet) + fun battery(testnet: Boolean) = batteryProvider.default.get(testnet) + + fun batteryWallet(testnet: Boolean) = batteryProvider.wallet.get(testnet) + + fun batteryEmulation(testnet: Boolean) = batteryProvider.emulation.get(testnet) fun getBatteryConfig(testnet: Boolean): Config? { return withRetry { battery(testnet).getConfig() } @@ -183,24 +187,38 @@ class API( return withRetry { battery(testnet).getRechargeMethods(false) } } - fun getOnRampData(country: String) = internalApi.getOnRampData(country) + fun getOnRampData() = internalApi.getOnRampData(config.webSwapsUrl) - suspend fun calculateOnRamp(args: OnRampArgsEntity): List = withContext(Dispatchers.IO) { - val data = internalApi.calculateOnRamp(args) ?: return@withContext emptyList() - val items = JSONObject(data).getJSONArray("items") - items.map { OnRampMerchantEntity(it) } + fun getOnRampPaymentMethods(currency: String) = internalApi.getOnRampPaymentMethods(config.webSwapsUrl, currency) + + fun getOnRampMerchants() = internalApi.getOnRampMerchants(config.webSwapsUrl) + + fun getSwapAssets(): JSONArray = runCatching { + swapApi.getSwapAssets(config.webSwapsUrl)?.let(::JSONArray) + }.getOrNull() ?: JSONArray() + + @Throws + suspend fun calculateOnRamp(args: OnRampArgsEntity): OnRampMerchantEntity.Data = withContext(Dispatchers.IO) { + val data = internalApi.calculateOnRamp(config.webSwapsUrl, args) ?: throw Exception("Empty response") + val json = JSONObject(data) + val items = json.getJSONArray("items").map { OnRampMerchantEntity(it) } + val suggested = json.optJSONArray("suggested")?.map { OnRampMerchantEntity(it) } ?: emptyList() + OnRampMerchantEntity.Data( + items = items, + suggested = suggested + ) } - suspend fun getEthenaStakingAPY(address: String): BigDecimal = withContext(Dispatchers.IO) { - internalApi.getEthenaStakingAPY(address) + suspend fun getEthena(accountId: String): EthenaEntity? = withContext(Dispatchers.IO) { + withRetry { internalApi.getEthena(accountId) } } fun getBatteryBalance( tonProofToken: String, testnet: Boolean, - units: UnitsGetBalance = UnitsGetBalance.ton + units: DefaultApi.UnitsGetBalance = DefaultApi.UnitsGetBalance.ton ): Balance? { - return withRetry { battery(testnet).getBalance(tonProofToken, units) } + return withRetry { battery(testnet).getBalance(tonProofToken, units, region = config.region) } } fun getAlertNotifications() = withRetry { @@ -210,7 +228,7 @@ class API( private fun isOkStatus(testnet: Boolean): Boolean { try { val status = withRetry { - provider.blockchain.get(testnet).status() + provider.utilities.get(testnet).status() } ?: return false if (!status.restOnline) { return false @@ -235,6 +253,16 @@ class API( return seeHttpClient.sse(url, onFailure = onFailure) } + suspend fun refreshConfig(testnet: Boolean) { + configRepository.refresh(testnet) + } + + fun swapStream( + from: SwapAssetParam, + to: SwapAssetParam, + userAddress: String + ) = swapApi.stream(config.webSwapsUrl, from, to, userAddress) + suspend fun getPageTitle(url: String): String = withContext(Dispatchers.IO) { try { val headers = ArrayMap().apply { @@ -276,6 +304,14 @@ class API( "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ" } + suspend fun getDnsExpiring( + accountId: String, + testnet: Boolean, + period: Int + ) = withContext(Dispatchers.IO) { + withRetry { accounts(testnet).getAccountDnsExpiring(accountId, period).items } ?: emptyList() + } + fun getEvents( accountId: String, testnet: Boolean, @@ -290,6 +326,35 @@ class API( ) } + fun fetchTonEvents( + accountId: String, + testnet: Boolean, + beforeLt: Long? = null, + beforeTimestamp: Timestamp? = null, + afterTimestamp: Timestamp? = null, + limit: Int, + ): List { + val response = withRetry { + accounts(testnet).getAccountEvents( + accountId = accountId, + beforeLt = beforeLt, + endDate = beforeTimestamp?.seconds(), + startDate = afterTimestamp?.seconds(), + limit = limit, + subjectOnly = true, + ) + } ?: throw Exception("Failed to get events") + return response.events + } + + fun fetchTronTransactions( + tronAddress: String, + tonProofToken: String, + beforeTimestamp: Timestamp? = null, + afterTimestamp: Timestamp? = null, + limit: Int + ) = tron.getTronHistory(tronAddress, tonProofToken, limit, beforeTimestamp, afterTimestamp) + suspend fun getTransactionByHash( accountId: String, testnet: Boolean, @@ -329,6 +394,7 @@ class API( lt = event.lt, inProgress = event.inProgress, extra = 0L, + progress = 0f, ) listOf(accountEvent) } @@ -400,7 +466,7 @@ class API( accounts(testnet).getAccountJettonsBalances( accountId = accountId, currencies = currency?.let { listOf(it) }, - extensions = extensions, + supportedExtensions = extensions, ).balances } ?: return null return jettonsBalances.map { BalanceEntity(it) }.filter { it.value.isPositive } @@ -439,7 +505,11 @@ class API( val wallets = withRetry { wallet(testnet).getWalletsByPublicKey(query).accounts } ?: return emptyList() - wallets.map { AccountDetailsEntity(query, it, testnet) }.map { + wallets.map { AccountDetailsEntity( + query = query, + wallet = it, + testnet = testnet + ) }.map { if (it.walletVersion == WalletVersion.UNKNOWN) { it.copy( walletVersion = BaseWalletContract.resolveVersion( @@ -467,6 +537,15 @@ class API( } } + fun getRates(from: String, to: String): Map? { + return withRetry { + rates().getRates( + tokens = listOf(from), + currencies = listOf(to) + ).rates + } + } + fun getNft(address: String, testnet: Boolean): NftItem? { return withRetry { nft(testnet).getNftItemByAddress(address) } } @@ -566,7 +645,7 @@ class API( cell: Cell, testnet: Boolean, ): String? { - val request = io.batteryapi.models.EstimateGaslessCostRequest(cell.base64(), false) + val request = EstimateGaslessCostRequest(cell.base64(), false) return withRetry { battery(testnet).estimateGaslessCost(jettonMaster, request, tonProofToken).commission @@ -601,7 +680,11 @@ class API( val withBattery = supportedByBattery && allowedByBattery val string = response.body?.string() ?: return null - val consequences = emulationJSONAdapter.fromJson(string) ?: return null + val consequences = try { + Serializer.JSON.decodeFromString(string) + } catch (e: Throwable) { + return null + } return Pair(consequences, withBattery) } @@ -619,7 +702,7 @@ class API( val request = EmulateMessageToWalletRequest( boc = boc, params = params, - safeMode = safeModeEnabled + // safeMode = safeModeEnabled ) withRetry { emulation(testnet).emulateMessageToWallet(request) @@ -667,12 +750,15 @@ class API( return@withContext SendBlockchainState.STATUS_ERROR } + val meta = hashMapOf( + "platform" to "android", + "version" to appVersionName, + "source" to source, + "confirmation_time" to confirmationTime.toString() + ) val request = SendBlockchainMessageRequest( boc = boc, - platform = "android", - version = appVersionName, - source = source, - confirmationTime = confirmationTime + meta = meta ) withRetry { blockchain(testnet).sendBlockchainMessage(request) @@ -889,17 +975,26 @@ class API( } } - fun getServerTime(testnet: Boolean) = withRetry { - liteServer(testnet).getRawTime().time - } ?: (System.currentTimeMillis() / 1000).toInt() - - suspend fun resolveCountry(): String? = withContext(Dispatchers.IO) { - if (cachedCountry == null) { - cachedCountry = internalApi.resolveCountry() + fun getServerTime(testnet: Boolean): Int { + /*val time = serverTimeProvider.getServerTime(testnet) + if (time == null) { + val serverTimeSeconds = withRetry { liteServer(testnet).getRawTime().time } + if (serverTimeSeconds == null) { + return (System.currentTimeMillis() / 1000).toInt() + } + serverTimeProvider.setServerTime(testnet, serverTimeSeconds) + return serverTimeSeconds } - cachedCountry + return time*/ + val serverTimeSeconds = withRetry { liteServer(testnet).getRawTime().time } + if (serverTimeSeconds == null) { + return (System.currentTimeMillis() / 1000).toInt() + } + return serverTimeSeconds } + suspend fun resolveCountry(): String? = internalApi.resolveCountry() + suspend fun reportNtfSpam( nftAddress: String, scam: Boolean diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/BatteryProvider.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/BatteryProvider.kt new file mode 100644 index 000000000..0ef717b48 --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/BatteryProvider.kt @@ -0,0 +1,23 @@ +package com.tonapps.wallet.api + +import com.tonapps.wallet.api.core.BatteryAPI +import com.tonapps.wallet.api.core.SourceAPI +import okhttp3.OkHttpClient + +internal class BatteryProvider( + mainnetHost: String, + testnetHost: String, + okHttpClient: OkHttpClient, +) { + + private val main = BatteryAPI(mainnetHost, okHttpClient) + private val test = BatteryAPI(testnetHost, okHttpClient) + + val default = SourceAPI(main.default, test.default) + + val emulation = SourceAPI(main.emulation, test.emulation) + + val wallet = SourceAPI(main.wallet, test.wallet) + + +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Constants.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Constants.kt new file mode 100644 index 000000000..eed64766c --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Constants.kt @@ -0,0 +1,6 @@ +package com.tonapps.wallet.api + +internal object Constants { + + const val SWAP_PREFIX = "https://swap.tonkeeper.com" +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/CoreAPI.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/CoreAPI.kt index 906dcec0b..e11e3a200 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/CoreAPI.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/CoreAPI.kt @@ -26,7 +26,7 @@ abstract class CoreAPI(private val context: Context) { val defaultHttpClient = baseOkHttpClientBuilder( cronetEngine = { cronetEngine }, - timeoutSeconds = 15, + timeoutSeconds = 30, interceptors = listOf( UserAgentInterceptor(userAgent), ) @@ -34,7 +34,7 @@ abstract class CoreAPI(private val context: Context) { val seeHttpClient = baseOkHttpClientBuilder( cronetEngine = { null }, - timeoutSeconds = 30, + timeoutSeconds = 60, interceptors = listOf( UserAgentInterceptor(userAgent), ) @@ -71,7 +71,7 @@ abstract class CoreAPI(private val context: Context) { private fun baseOkHttpClientBuilder( cronetEngine: () -> CronetEngine?, - timeoutSeconds: Long = 5, + timeoutSeconds: Long = 30, interceptors: List = emptyList() ): OkHttpClient.Builder { val builder = OkHttpClient().newBuilder() diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt index a299e0091..3719a108f 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt @@ -1,35 +1,15 @@ package com.tonapps.wallet.api import android.os.SystemClock -import io.tonapi.infrastructure.Serializer -import com.squareup.moshi.adapter -import io.tonapi.infrastructure.ClientException import android.util.Log -import android.widget.Toast -import com.google.firebase.crashlytics.BuildConfig import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.network.OkHttpError -import io.tonapi.infrastructure.ClientError -import io.tonapi.infrastructure.Response -import io.tonapi.infrastructure.ServerError -import kotlinx.coroutines.delay +import io.infrastructure.ClientError +import io.infrastructure.ClientException +import io.infrastructure.ServerError import kotlinx.coroutines.CancellationException import kotlinx.io.IOException import java.net.SocketTimeoutException -import java.net.UnknownHostException - -@OptIn(ExperimentalStdlibApi::class) -inline fun toJSON(obj: T?): String { - if (obj == null) { - return "" - } - return Serializer.moshi.adapter().toJson(obj) -} - -@OptIn(ExperimentalStdlibApi::class) -inline fun fromJSON(json: String): T { - return Serializer.moshi.adapter().fromJson(json)!! -} fun withRetry( times: Int = 5, @@ -44,14 +24,15 @@ fun withRetry( } catch (e: CancellationException) { throw e } catch (e: SocketTimeoutException) { + Log.e("RetryLogNew", "SocketTimeoutException occurred: ${e.message}", e) SystemClock.sleep(delay + 100) return null } catch (e: IOException) { - Log.e("WithRetryLog", "IOException: ${e.message}", e) + Log.e("RetryLogNew", "IOException occurred: ${e.message}", e) SystemClock.sleep(delay + 100) return null } catch (e: Throwable) { - Log.e("WithRetryLog", "Error: ${e.message}", e) + Log.e("RetryLogNew", "Error occurred: ${e.message}", e) val statusCode = e.getHttpStatusCode() if (statusCode == 429 || statusCode == 401 || statusCode == 502 || statusCode == 520) { SystemClock.sleep(delay + 100) diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Provider.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Provider.kt index 05c65ed23..dd14fd69f 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Provider.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Provider.kt @@ -42,4 +42,6 @@ internal class Provider( val wallet = SourceAPI(main.wallet, test.wallet) val gasless = SourceAPI(main.gasless, test.gasless) + + val utilities = SourceAPI(main.utilities, test.utilities) } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/ServerTimeProvider.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/ServerTimeProvider.kt new file mode 100644 index 000000000..b409a3e61 --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/ServerTimeProvider.kt @@ -0,0 +1,48 @@ +package com.tonapps.wallet.api + +import android.content.Context +import android.os.SystemClock +import com.tonapps.extensions.prefs +import androidx.core.content.edit + +internal class ServerTimeProvider(context: Context) { + + private companion object { + const val SERVER_TIME_KEY = "server_time" + const val LOCAL_TIME_KEY = "local_time" + + // 24 hours in milliseconds + private const val CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000L + + private fun getServerTimePrefKey(testnet: Boolean) = "${SERVER_TIME_KEY}_${if (testnet) "test" else "main"}" + + private fun getLocalTimePrefKey(testnet: Boolean) = "${LOCAL_TIME_KEY}_${if (testnet) "test" else "main"}" + + } + + private val prefs = context.prefs("server_time") + + fun setServerTime(testnet: Boolean, serverTimeSeconds: Int) { + val localTimeMillis = SystemClock.elapsedRealtime() + + prefs.edit { + putInt(getServerTimePrefKey(testnet), serverTimeSeconds) + putLong(getLocalTimePrefKey(testnet), localTimeMillis) + } + } + + fun getServerTime(testnet: Boolean): Int? { + val savedServerSeconds = prefs.getInt(getServerTimePrefKey(testnet), 0) + val savedLocalMillis = prefs.getLong(getLocalTimePrefKey(testnet), 0L) + if (0 >= savedServerSeconds || 0 >= savedLocalMillis) { + return null + } + val elapsedTimeMillis = SystemClock.elapsedRealtime() - savedLocalMillis + if (elapsedTimeMillis > CACHE_EXPIRATION_MS) { + return null + } + val elapsedSeconds = elapsedTimeMillis / 1000 + val currentServerTime = savedServerSeconds + elapsedSeconds + return currentServerTime.toInt() + } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/SwapAssetParam.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/SwapAssetParam.kt new file mode 100644 index 000000000..e1bb70ec2 --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/SwapAssetParam.kt @@ -0,0 +1,24 @@ +package com.tonapps.wallet.api + +import android.net.Uri + +data class SwapAssetParam( + val address: String, + val amount: String?, +) { + + val isEmpty: Boolean + get() = amount.isNullOrBlank() || amount == "0" + + fun apply(prefix: String, builder: Uri.Builder): Uri.Builder { + if (address.equals("ton", true)) { + builder.appendQueryParameter("${prefix}Asset", "0:0000000000000000000000000000000000000000000000000000000000000000") + } else { + builder.appendQueryParameter("${prefix}Asset", address) + } + amount?.let { + builder.appendQueryParameter("${prefix}Amount", it) + } + return builder + } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BaseAPI.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BaseAPI.kt index f9f003714..f4627c99d 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BaseAPI.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BaseAPI.kt @@ -1,6 +1,5 @@ package com.tonapps.wallet.api.core -import io.batteryapi.apis.BatteryApi import io.tonapi.apis.AccountsApi import io.tonapi.apis.BlockchainApi import io.tonapi.apis.ConnectApi @@ -15,6 +14,7 @@ import io.tonapi.apis.RatesApi import io.tonapi.apis.StakingApi import io.tonapi.apis.StorageApi import io.tonapi.apis.TracesApi +import io.tonapi.apis.UtilitiesApi import io.tonapi.apis.WalletApi import okhttp3.OkHttpClient @@ -53,4 +53,6 @@ class BaseAPI( val gasless: GaslessApi by lazy { GaslessApi(basePath, okHttpClient) } + val utilities: UtilitiesApi by lazy { UtilitiesApi(basePath, okHttpClient) } + } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BatteryAPI.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BatteryAPI.kt new file mode 100644 index 000000000..52703cddc --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/core/BatteryAPI.kt @@ -0,0 +1,18 @@ +package com.tonapps.wallet.api.core + +import io.batteryapi.apis.DefaultApi +import io.batteryapi.apis.EmulationApi +import io.batteryapi.apis.WalletApi +import okhttp3.OkHttpClient + +class BatteryAPI( + basePath: String, + okHttpClient: OkHttpClient +) { + + val emulation: EmulationApi by lazy { EmulationApi(basePath, okHttpClient) } + + val default: DefaultApi by lazy { DefaultApi(basePath, okHttpClient) } + + val wallet: WalletApi by lazy { WalletApi(basePath, okHttpClient) } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/cronet/CronetInterceptor.java b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/cronet/CronetInterceptor.java index 9cf9dd622..a7cc7ecea 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/cronet/CronetInterceptor.java +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/cronet/CronetInterceptor.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -77,7 +78,7 @@ private CronetInterceptor(RequestResponseConverter converter) { @Override public Response intercept(Chain chain) throws IOException { if (chain.call().isCanceled()) { - throw new IOException("Canceled"); + throw new CancellationException(); } Request request = chain.request(); diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountDetailsEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountDetailsEntity.kt index cb14f235a..2e1a7c20f 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountDetailsEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountDetailsEntity.kt @@ -9,6 +9,7 @@ import com.tonapps.blockchain.ton.extensions.toRawAddress import com.tonapps.blockchain.ton.extensions.toUserFriendly import io.tonapi.models.Account import io.tonapi.models.AccountStatus +import io.tonapi.models.Wallet import kotlinx.parcelize.Parcelize @Parcelize @@ -61,6 +62,22 @@ data class AccountDetailsEntity( testnet = testnet, ) + constructor( + query: String, + wallet: Wallet, + testnet: Boolean, + new: Boolean = false + ) : this( + query = query, + preview = AccountEntity(wallet, testnet), + active = wallet.status == AccountStatus.active, + walletVersion = resolveVersion(testnet, wallet.interfaces, wallet.address), + balance = wallet.balance, + new = new, + initialized = wallet.status == AccountStatus.active || wallet.status == AccountStatus.frozen, + testnet = testnet, + ) + private companion object { private fun resolveVersion(testnet: Boolean, interfaces: List?, address: String): WalletVersion { diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountEntity.kt index b72a2aa44..9d563eab0 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/AccountEntity.kt @@ -9,8 +9,10 @@ import com.tonapps.blockchain.ton.extensions.toWalletAddress import com.tonapps.extensions.short4 import io.tonapi.models.Account import io.tonapi.models.AccountAddress +import io.tonapi.models.Wallet import kotlinx.parcelize.Parcelize import org.ton.block.AddrStd +import androidx.core.net.toUri @Parcelize data class AccountEntity( @@ -43,7 +45,7 @@ data class AccountEntity( address = model.address.toUserFriendly(model.isWallet, testnet), accountId = model.address.toRawAddress(), name = model.name, - iconUri = model.icon?.let { Uri.parse(it) }, + iconUri = model.icon?.toUri(), isWallet = model.isWallet, isScam = model.isScam ) @@ -52,8 +54,17 @@ data class AccountEntity( address = account.address.toUserFriendly(account.isWallet, testnet), accountId = account.address.toRawAddress(), name = account.name, - iconUri = account.icon?.let { Uri.parse(it) }, + iconUri = account.icon?.toUri(), isWallet = account.isWallet, isScam = account.isScam ?: false ) + + constructor(wallet: Wallet, testnet: Boolean) : this( + address = wallet.address.toUserFriendly(wallet.isWallet, testnet), + accountId = wallet.address.toRawAddress(), + name = wallet.name, + iconUri = wallet.icon?.toUri(), + isWallet = wallet.isWallet, + isScam = false + ) } diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/BalanceEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/BalanceEntity.kt index d021ea68c..e2f61d979 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/BalanceEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/BalanceEntity.kt @@ -1,8 +1,8 @@ package com.tonapps.wallet.api.entity import android.os.Parcelable -import android.util.Log import com.tonapps.icu.Coins +import com.tonapps.wallet.api.entity.value.Blockchain import io.tonapi.models.JettonBalance import io.tonapi.models.TokenRates import kotlinx.parcelize.IgnoredOnParcel @@ -55,6 +55,9 @@ data class BalanceEntity( val customPayloadApiUri: String? get() = token.customPayloadApiUri + val blockchain: Blockchain + get() = token.blockchain + constructor(jettonBalance: JettonBalance) : this( token = TokenEntity(jettonBalance.jetton, jettonBalance.extensions, jettonBalance.lock), value = Coins.of(BigDecimal(jettonBalance.balance).movePointLeft(jettonBalance.jetton.decimals), jettonBalance.jetton.decimals), diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/ConfigEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/ConfigEntity.kt index 44266a929..cbaacb6d1 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/ConfigEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/ConfigEntity.kt @@ -8,6 +8,8 @@ import com.tonapps.icu.Coins import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.json.JSONObject +import androidx.core.net.toUri +import com.tonapps.wallet.api.Constants @Parcelize data class ConfigEntity( @@ -15,6 +17,7 @@ data class ConfigEntity( val supportLink: String, val nftExplorer: String, val transactionExplorer: String, + val accountExplorer: String, val mercuryoSecret: String, val tonapiMainnetHost: String, val tonapiTestnetHost: String, @@ -35,16 +38,9 @@ data class ConfigEntity( val batteryHost: String, val batteryTestnetHost: String, val batteryBeta: Boolean, - val batteryDisabled: Boolean, val batterySendDisabled: Boolean, - val batteryMeanFees: String, - val batteryMeanPriceNft: String, - val batteryMeanPriceSwap: String, - val batteryMeanPriceJetton: String, - val batteryMeanPriceTrcMin: String, - val batteryMeanPriceTrcMax: String, val disableBatteryIapModule: Boolean, - val batteryReservedAmount: String, + val disableBatteryCryptoRechargeModule: Boolean, val batteryMaxInputAmount: String, val batteryRefundEndpoint: String, val batteryPromoDisable: Boolean, @@ -59,15 +55,21 @@ data class ConfigEntity( val apkDownloadUrl: String?, val apkName: AppVersion?, val tronApiUrl: String, + val enabledStaking: List, + val qrScannerExtends: List, + val region: String, + val tonkeeperApiUrl: String, + val tronSwapUrl: String, + val tronSwapTitle: String, + val tronApiKey: String? = null, + val privacyPolicyUrl: String, + val termsOfUseUrl: String, + val webSwapsUrl: String, ): Parcelable { @IgnoredOnParcel val swapUri: Uri - get() = Uri.parse(stonfiUrl) - - @IgnoredOnParcel - val isBatteryDisabled: Boolean - get() = batteryDisabled || batterySendDisabled + get() = stonfiUrl.toUri() @IgnoredOnParcel val domains: List by lazy { @@ -86,6 +88,7 @@ data class ConfigEntity( supportLink = json.getString("supportLink"), nftExplorer = json.getString("NFTOnExplorerUrl"), transactionExplorer = json.getString("transactionExplorer"), + accountExplorer = json.getString("accountExplorer"), mercuryoSecret = json.getString("mercuryoSecret"), tonapiMainnetHost = json.getString("tonapiMainnetHost"), tonapiTestnetHost = json.getString("tonapiTestnetHost"), @@ -110,16 +113,9 @@ data class ConfigEntity( batteryHost = json.optString("batteryHost", "https://battery.tonkeeper.com"), batteryTestnetHost = json.optString("batteryTestnetHost", "https://testnet-battery.tonkeeper.com"), batteryBeta = json.optBoolean("battery_beta", true), - batteryDisabled = json.optBoolean("disable_battery", false), batterySendDisabled = json.optBoolean("disable_battery_send", false), - batteryMeanFees = json.optString("batteryMeanFees", "0.0055"), disableBatteryIapModule = json.optBoolean("disable_battery_iap_module", false), - batteryMeanPriceNft = json.optString("batteryMeanPrice_nft", "0.03"), - batteryMeanPriceSwap = json.optString("batteryMeanPrice_swap", "0.22"), - batteryMeanPriceJetton = json.optString("batteryMeanPrice_jetton", "0.06"), - batteryMeanPriceTrcMin = json.optString("batteryMeanPrice_trc20_min", "0.312"), - batteryMeanPriceTrcMax = json.optString("batteryMeanPrice_trc20_max", "0.78"), - batteryReservedAmount = json.optString("batteryReservedAmount", "0.3"), + disableBatteryCryptoRechargeModule = json.optBoolean("disable_battery_crypto_recharge_module", false), batteryMaxInputAmount = json.optString("batteryMaxInputAmount", "3"), batteryRefundEndpoint = json.optString("batteryRefundEndpoint", "https://battery-refund-app.vercel.app"), batteryPromoDisable = json.optBoolean("disable_battery_promo_module", true), @@ -136,6 +132,20 @@ data class ConfigEntity( apkDownloadUrl = json.optString("apk_download_url"), apkName = json.optString("apk_name")?.let { AppVersion(it.removePrefix("v")) }, tronApiUrl = json.optString("tron_api_url", "https://api.trongrid.io"), + enabledStaking = json.optJSONArray("enabled_staking")?.let { array -> + (0 until array.length()).map { array.getString(it) } + } ?: emptyList(), + qrScannerExtends = json.optJSONArray("qr_scanner_extends")?.let { array -> + QRScannerExtendsEntity.of(array) + } ?: emptyList(), + region = json.getString("region"), + tonkeeperApiUrl = json.optString("tonkeeper_api_url", "https://api.tonkeeper.com"), + tronSwapUrl = json.optString("tron_swap_url", "https://widget.letsexchange.io/en?affiliate_id=ffzymmunvvyxyypo&coin_from=ton&coin_to=USDT-TRC20&is_iframe=true"), + tronSwapTitle = json.optString("tron_swap_title", "LetsExchange"), + // tronApiKey = json.optString("tron_api_key"), + privacyPolicyUrl = json.getString("privacy_policy"), + termsOfUseUrl = json.getString("terms_of_use"), + webSwapsUrl = json.optString("web_swaps_url", Constants.SWAP_PREFIX) ) constructor() : this( @@ -143,6 +153,7 @@ data class ConfigEntity( supportLink = "mailto:support@tonkeeper.com", nftExplorer = "https://tonviewer.com/nft/%s", transactionExplorer = "https://tonviewer.com/transaction/%s", + accountExplorer = "https://tonviewer.com/%s", mercuryoSecret = "", tonapiMainnetHost = "https://keeper.tonapi.io", tonapiTestnetHost = "https://testnet.tonapi.io", @@ -163,16 +174,9 @@ data class ConfigEntity( batteryHost = "https://battery.tonkeeper.com", batteryTestnetHost = "https://testnet-battery.tonkeeper.com", batteryBeta = true, - batteryDisabled = false, batterySendDisabled = false, - batteryMeanFees = "0.0055", disableBatteryIapModule = false, - batteryMeanPriceNft = "0.03", - batteryMeanPriceSwap = "0.22", - batteryMeanPriceJetton = "0.06", - batteryMeanPriceTrcMin = "0.312", - batteryMeanPriceTrcMax = "0.78", - batteryReservedAmount = "0.3", + disableBatteryCryptoRechargeModule = false, batteryMaxInputAmount = "3", batteryRefundEndpoint = "https://battery-refund-app.vercel.app", batteryPromoDisable = true, @@ -187,8 +191,27 @@ data class ConfigEntity( apkDownloadUrl = null, apkName = null, tronApiUrl = "https://api.trongrid.io", + enabledStaking = emptyList(), + qrScannerExtends = emptyList(), + region = "US", + tonkeeperApiUrl = "https://api.tonkeeper.com", + tronSwapUrl = "https://widget.letsexchange.io/en?affiliate_id=ffzymmunvvyxyypo&coin_from=ton&coin_to=USDT-TRC20&is_iframe=true", + tronSwapTitle = "LetsExchange", + privacyPolicyUrl = "https://tonkeeper.com/privacy", + termsOfUseUrl = "https://tonkeeper.com/terms", + webSwapsUrl = Constants.SWAP_PREFIX ) + fun formatTransactionExplorer(testnet: Boolean, tron: Boolean, hash: String): String { + return if (tron) { + "https://tronscan.org/#/transaction/$hash" + } else if (testnet) { + "https://testnet.tonviewer.com/transaction/$hash" + } else { + transactionExplorer.format(hash) + } + } + companion object { val default = ConfigEntity() } diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/EthenaEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/EthenaEntity.kt new file mode 100644 index 000000000..e07c77c0b --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/EthenaEntity.kt @@ -0,0 +1,87 @@ +package com.tonapps.wallet.api.entity + +import android.os.Parcelable +import com.tonapps.extensions.map +import kotlinx.parcelize.Parcelize +import org.json.JSONObject +import java.math.BigDecimal + +@Parcelize +data class EthenaEntity( + val methods: List, + val about: About, +) : Parcelable { + + @Parcelize + data class About( + val description: String, + val tsusdeDescription: String, + val faqUrl: String, + val aboutUrl: String, + val stakeTitle: String, + val stakeDescription: String, + ) : Parcelable { + constructor(json: JSONObject) : this( + description = json.getString("description"), + tsusdeDescription = json.getString("tsusde_description"), + faqUrl = json.getString("faq_url"), + aboutUrl = json.getString("about_url"), + stakeTitle = json.getString("tsusde_stake_title"), + stakeDescription = json.getString("tsusde_stake_description"), + ) + } + + @Parcelize + data class Method( + val type: Type, + val name: String, + val apy: BigDecimal, + val apyTitle: String, + val apyDescription: String, + val bonusApy: BigDecimal?, + val bonusTitle: String?, + val bonusDescription: String?, + val eligibleBonusUrl: String?, + val depositUrl: String, + val withdrawalUrl: String, + val jettonMaster: String, + val links: List, + ) : Parcelable { + enum class Type(val id: String) { + STONFI("stonfi"), + AFFLUENT("affluent"); + + companion object { + fun fromId(id: String): Type { + return entries.find { it.id.equals(id, ignoreCase = true) } + ?: throw IllegalArgumentException("Invalid type: $id") + } + } + } + + constructor(json: JSONObject) : this( + type = Type.fromId(json.getString("type")), + name = json.getString("name"), + apy = BigDecimal.valueOf(json.getDouble("apy")), + apyTitle = json.getString("apy_title"), + apyDescription = json.getString("apy_description"), + bonusApy = json.optString("bonus_apy").takeIf { it.isNotEmpty() }?.let { + BigDecimal(it) + }, + bonusTitle = json.optString("apy_bonus_title"), + bonusDescription = json.optString("apy_bonus_description"), + eligibleBonusUrl = json.optString("eligible_bonus_url"), + depositUrl = json.getString("deposit_url"), + withdrawalUrl = json.getString("withdrawal_url"), + jettonMaster = json.getString("jetton_master"), + links = json.optJSONArray("links")?.let { array -> + (0 until array.length()).map { array.getString(it) } + } ?: emptyList() + ) + } + + constructor(json: JSONObject) : this( + methods = json.getJSONArray("methods").map { Method(it) }, + about = About(json.getJSONObject("about")) + ) +} diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/FlagsEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/FlagsEntity.kt index 25d7eb95f..46dfe74f0 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/FlagsEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/FlagsEntity.kt @@ -1,7 +1,6 @@ package com.tonapps.wallet.api.entity import android.os.Parcelable -import android.util.Log import kotlinx.parcelize.Parcelize import org.json.JSONObject @@ -10,29 +9,47 @@ data class FlagsEntity( val disableSwap: Boolean, val disableExchangeMethods: Boolean, val disableDApps: Boolean, - val disableBlur: Boolean, - val disableLegacyBlur: Boolean, val disableSigner: Boolean, val safeModeEnabled: Boolean, -): Parcelable { + val disableStaking: Boolean, + val disableTron: Boolean, + val disableBattery: Boolean, + val disableGasless: Boolean, + val disableUsde: Boolean, + val disableNativeSwap: Boolean, + val disableOnboardingStory: Boolean, + val disableNfts: Boolean +) : Parcelable { constructor(json: JSONObject) : this( disableSwap = json.optBoolean("disable_swap", false), disableExchangeMethods = json.optBoolean("disable_exchange_methods", false), disableDApps = json.optBoolean("disable_dapps", false), - disableBlur = json.optBoolean("disable_blur", false), - disableLegacyBlur = json.optBoolean("disable_legacy_blur", false), disableSigner = json.optBoolean("disable_signer", false), - safeModeEnabled = json.optBoolean("safe_mode_enabled", false) + safeModeEnabled = json.optBoolean("safe_mode_enabled", false), + disableStaking = json.optBoolean("disable_staking", false), + disableTron = json.optBoolean("disable_tron", false), + disableBattery = json.optBoolean("disable_battery", false), + disableGasless = json.optBoolean("disable_gaseless", false), + disableUsde = json.optBoolean("disable_usde", false), + disableNativeSwap = json.optBoolean("disable_native_swap", false), + disableOnboardingStory = json.optBoolean("disable_onboarding_story", false), + disableNfts = json.optBoolean("disable_nfts", false) ) constructor() : this( disableSwap = false, disableExchangeMethods = false, disableDApps = false, - disableBlur = false, - disableLegacyBlur = false, disableSigner = false, - safeModeEnabled = false + safeModeEnabled = false, + disableStaking = false, + disableTron = false, + disableBattery = false, + disableGasless = false, + disableUsde = false, + disableNativeSwap = false, + disableOnboardingStory = false, + disableNfts = false ) } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampArgsEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampArgsEntity.kt index 7b29ca647..ae94bdefc 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampArgsEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampArgsEntity.kt @@ -6,22 +6,35 @@ import org.json.JSONObject data class OnRampArgsEntity( val from: String, val to: String, - val network: String, + val fromNetwork: String?, + val toNetwork: String?, val wallet: String, val purchaseType: String, val amount: Coins, - val country: String, val paymentMethod: String? ) { + val isSwap: Boolean + get() = purchaseType == "swap" + + val isSell: Boolean + get() = purchaseType == "sell" + + val withoutPaymentMethod: Boolean + get() = isSwap || isSell + fun toJSON() = JSONObject().apply { put("from", from) put("to", to) - put("network", network) + fromNetwork?.let { + put("from_network", it) + } + toNetwork?.let { + put("to_network", it) + } put("wallet", wallet) put("purchase_type", purchaseType) put("amount", amount.value.toPlainString()) - put("country", country) paymentMethod?.let { put("payment_method", it) } diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampMerchantEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampMerchantEntity.kt index f854b6239..498fbe376 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampMerchantEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/OnRampMerchantEntity.kt @@ -5,12 +5,26 @@ import org.json.JSONObject data class OnRampMerchantEntity( val merchant: String, val amount: Double, - val widgetUrl: String + val widgetUrl: String, + val minAmount: Double, ) { + data class Data( + val items: List = emptyList(), + val suggested: List = emptyList() + ) { + + val isEmpty: Boolean + get() = items.isEmpty() && suggested.isEmpty() + + val size: Int + get() = items.size + suggested.size + } + constructor(json: JSONObject) : this( merchant = json.getString("merchant"), amount = json.getDouble("amount"), - widgetUrl = json.getString("widget_url") + widgetUrl = json.getString("widget_url"), + minAmount = json.optDouble("min_amount", 0.0) ) } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/QRScannerExtendsEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/QRScannerExtendsEntity.kt new file mode 100644 index 000000000..3ff6fadcd --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/QRScannerExtendsEntity.kt @@ -0,0 +1,55 @@ +package com.tonapps.wallet.api.entity + +import android.os.Parcelable +import android.util.Log +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.json.JSONArray +import org.json.JSONObject +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@Parcelize +data class QRScannerExtendsEntity( + val version: Int, + val regexp: String, + val url: String +): Parcelable { + + @IgnoredOnParcel + val regex: Regex by lazy { + Regex(regexp) + } + + constructor(json: JSONObject) : this( + version = json.getInt("version"), + regexp = json.getString("regexp"), + url = json.getString("url") + ) + + fun isMatch(input: String): Boolean { + return regex.containsMatchIn(input) + } + + fun buildUrl(input: String): String? { + if (!isMatch(input)) { + return null + } + val encoded = URLEncoder.encode(input, StandardCharsets.UTF_8.name()) + return url.replace("{{QR_CODE}}", encoded) + } + + companion object { + + fun of(array: JSONArray): List { + return (0 until array.length()).mapNotNull { + val json = array.getJSONObject(it) + if (json.getInt("version") == 1) { + QRScannerExtendsEntity(array.getJSONObject(it)) + } else { + null + } + } + } + } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/SwapEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/SwapEntity.kt new file mode 100644 index 000000000..dc04e6d3e --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/SwapEntity.kt @@ -0,0 +1,51 @@ +package com.tonapps.wallet.api.entity + +import io.Serializer +import kotlinx.serialization.Serializable + +object SwapEntity { + + @Serializable + data class Message( + val targetAddress: String, + val sendAmount: String, + val payload: String?, + ) + + @Serializable + data class Messages( + val messages: List, + val quoteId: String, + val resolverName: String, + val askUnits: String, + val bidUnits: String, + val protocolFeeUnits: String, + val tradeStartDeadline: String, + val gasBudget: String, + val estimatedGasConsumption: String, + val slippage: Int + ) { + + val isEmpty: Boolean + get() = messages.isEmpty() + } + + val empty = Messages( + messages = emptyList(), + quoteId = "", + resolverName = "", + askUnits = "", + bidUnits = "", + protocolFeeUnits = "", + tradeStartDeadline = "", + gasBudget = "", + estimatedGasConsumption = "", + slippage = 100 + ) + + fun parse(data: String) = try { + Serializer.fromJSON(data) + } catch (ignored: Throwable) { + null + } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/TokenEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/TokenEntity.kt index 4e01f4633..d5ed94e17 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/TokenEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/TokenEntity.kt @@ -6,6 +6,7 @@ import com.tonapps.blockchain.ton.extensions.cellFromHex import com.tonapps.blockchain.ton.extensions.equalsAddress import com.tonapps.blockchain.ton.extensions.toRawAddress import com.tonapps.wallet.api.R +import com.tonapps.wallet.api.entity.value.Blockchain import io.tonapi.models.JettonBalanceLock import io.tonapi.models.JettonInfo import io.tonapi.models.JettonPreview @@ -90,9 +91,14 @@ data class TokenEntity( val TON_ICON_URI = Uri.Builder().scheme("res").path(R.drawable.ic_ton_with_bg.toString()).build() val USDT_ICON_URI = Uri.Builder().scheme("res").path(R.drawable.ic_usdt_with_bg.toString()).build() + val USDE_ICON_URI = Uri.Builder().scheme("res").path(R.drawable.ic_udse_ethena_with_bg.toString()).build() + val TS_USDE_ICON_URI = Uri.Builder().scheme("res").path(R.drawable.ic_tsusde_with_bg.toString()).build() const val TRC20_USDT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" const val TON_USDT = "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe" + const val TON_USDE = "0:086fa2a675f74347b08dd4606a549b8fdb98829cb282bc1949d3b12fbaed9dcc" + + const val TON_TS_USDE = "0:d0e545323c7acb7102653c073377f7e3c67f122eb94d430a250739f109d4a57d" val TON = TokenEntity( blockchain = Blockchain.TON, @@ -133,6 +139,32 @@ data class TokenEntity( customPayloadApiUri = null ) + val USDE = TokenEntity( + blockchain = Blockchain.TON, + address = TON_USDE, + name = "Ethena USDe", + symbol = "USDe", + imageUri = USDE_ICON_URI, + decimals = 6, + verification = Verification.whitelist, + isRequestMinting = false, + isTransferable = true, + customPayloadApiUri = null + ) + + val TS_USDE = TokenEntity( + blockchain = Blockchain.TON, + address = TON_TS_USDE, + name = "Ethena tsUSDe", + symbol = "tsUSDe", + imageUri = TS_USDE_ICON_URI, + decimals = 6, + verification = Verification.whitelist, + isRequestMinting = false, + isTransferable = true, + customPayloadApiUri = null + ) + private fun convertVerification(verification: JettonVerificationType): Verification { return when (verification) { JettonVerificationType.whitelist -> Verification.whitelist @@ -194,6 +226,6 @@ data class TokenEntity( isRequestMinting = extensions?.contains(Extension.CustomPayload.value) == true, isTransferable = extensions?.contains(Extension.NonTransferable.value) != true, lock = lock?.let { Lock(it) }, - customPayloadApiUri = jetton.customPayloadApiUri + customPayloadApiUri = jetton.metadata.customPayloadApiUri ) } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/Blockchain.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/Blockchain.kt similarity index 50% rename from apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/Blockchain.kt rename to apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/Blockchain.kt index c9b9d181e..38d1499d7 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/Blockchain.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/Blockchain.kt @@ -1,6 +1,6 @@ -package com.tonapps.wallet.api.entity +package com.tonapps.wallet.api.entity.value enum class Blockchain(val id: String) { - TON("ton"), + TON("TON"), TRON("TRON"); } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/BlockchainAddress.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/BlockchainAddress.kt new file mode 100644 index 000000000..3ff6a6180 --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/BlockchainAddress.kt @@ -0,0 +1,42 @@ +package com.tonapps.wallet.api.entity.value + +import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BlockchainAddress( + val value: String, + val testnet: Boolean, + val blockchain: Blockchain, +): Parcelable { + + @IgnoredOnParcel + val key: String by lazy { + if (testnet) { + "${blockchain.id}:$value:testnet" + } else { + "${blockchain.id}:$value" + } + } + + companion object { + + fun valueOf(value: String): BlockchainAddress { + val split = value.split(":") + return if (split.size == 2) { + BlockchainAddress( + value = split[1], + testnet = false, + blockchain = Blockchain.valueOf(split[0]) + ) + } else { + BlockchainAddress( + value = split[2], + testnet = split[1] == "testnet", + blockchain = Blockchain.valueOf(split[0]) + ) + } + } + } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/Timestamp.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/Timestamp.kt new file mode 100644 index 000000000..89811bf79 --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/Timestamp.kt @@ -0,0 +1,32 @@ +package com.tonapps.wallet.api.entity.value + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@JvmInline +@Parcelize +value class Timestamp(val value: Long) : Parcelable, Comparable { + + data class Range( + val from: Timestamp, + val to: Timestamp + ) + + fun toLong() = value + + fun seconds() = value / 1000 + + override fun compareTo(other: Timestamp): Int { + return value.compareTo(other.value) + } + + companion object { + + val zero = Timestamp(0) + val now = Timestamp(System.currentTimeMillis()) + + fun from(value: Long) = if (value < 1_000_000_000_000L) { + Timestamp(value * 1000) + } else Timestamp(value) + } +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/ValueConverters.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/ValueConverters.kt new file mode 100644 index 000000000..b07afe671 --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/entity/value/ValueConverters.kt @@ -0,0 +1,31 @@ +package com.tonapps.wallet.api.entity.value + +import androidx.room.TypeConverter + +object ValueConverters { + + @TypeConverter + @JvmStatic + fun fromTimestamp(value: Timestamp) = value.toLong() + + @TypeConverter + @JvmStatic + fun toTimestamp(value: Long) = Timestamp(value) + + @TypeConverter + @JvmStatic + fun fromBlockchain(value: Blockchain) = value.id + + @TypeConverter + @JvmStatic + fun toBlockchain(value: String) = Blockchain.valueOf(value) + + @TypeConverter + @JvmStatic + fun fromAddress(value: BlockchainAddress) = value.key + + @TypeConverter + @JvmStatic + fun toAddress(value: String) = BlockchainAddress.valueOf(value) + +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/ConfigRepository.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/ConfigRepository.kt index 7e0bed499..21ba64b93 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/ConfigRepository.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/ConfigRepository.kt @@ -1,7 +1,6 @@ package com.tonapps.wallet.api.internal import android.content.Context -import android.util.Log import com.tonapps.extensions.file import com.tonapps.extensions.toByteArray import com.tonapps.extensions.toParcel @@ -10,7 +9,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -29,15 +27,16 @@ internal class ConfigRepository( private set (value) { field = value _stream.value = value.copy() + internalApi.setApiUrl(value.tonkeeperApiUrl) } init { scope.launch(Dispatchers.IO) { - readCache()?.let { - setConfig(it) - } - remote(false)?.let { - setConfig(it) + val cached = readCache() + if (cached != null) { + setConfig(cached) + } else { + initConfig() } } } @@ -59,4 +58,15 @@ internal class ConfigRepository( config } + suspend fun refresh(testnet: Boolean) { + val config = remote(testnet) ?: return + setConfig(config) + } + + suspend fun initConfig() { + remote(false)?.let { + setConfig(it) + } + } + } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/InternalApi.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/InternalApi.kt index ee70b799f..380086a4b 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/InternalApi.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/InternalApi.kt @@ -1,18 +1,17 @@ package com.tonapps.wallet.api.internal import android.content.Context -import android.net.Uri -import android.util.ArrayMap import android.util.Log +import androidx.collection.ArrayMap +import androidx.core.net.toUri import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tonapps.extensions.deviceCountry -import com.tonapps.extensions.getStoreCountry import com.tonapps.extensions.isDebug import com.tonapps.extensions.locale import com.tonapps.extensions.map import com.tonapps.network.get import com.tonapps.network.postJSON import com.tonapps.wallet.api.entity.ConfigEntity +import com.tonapps.wallet.api.entity.EthenaEntity import com.tonapps.wallet.api.entity.NotificationEntity import com.tonapps.wallet.api.entity.OnRampArgsEntity import com.tonapps.wallet.api.entity.StoryEntity @@ -22,7 +21,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.json.JSONObject -import java.math.BigDecimal import java.util.Locale internal class InternalApi( @@ -31,16 +29,39 @@ internal class InternalApi( private val appVersionName: String ) { + private var _deviceCountry: String? = null + private var _storeCountry: String? = null + private var _apiEndpoint = "https://api.tonkeeper.com".toUri() + + val country: String + get() = _storeCountry ?: _deviceCountry ?: Locale.getDefault().country.uppercase() + + fun setCountry(deviceCountry: String, storeCountry: String?) { + _deviceCountry = deviceCountry.uppercase() + _storeCountry = storeCountry?.uppercase() + } + + fun setApiUrl(url: String) { + _apiEndpoint = url.toUri() + } + private fun endpoint( path: String, testnet: Boolean, platform: String, build: String, boot: Boolean = false, + queryParams: Map = emptyMap(), + bootFallback: Boolean = false, ): String = runBlocking { - val builder = Uri.Builder() - builder.scheme("https") - .authority(if (boot) "boot.tonkeeper.com" else "api.tonkeeper.com") + val builder = if (bootFallback) { + "https://block.tonkeeper.com".toUri().buildUpon() + } else if (boot) { + "https://boot.tonkeeper.com".toUri().buildUpon() + } else { + _apiEndpoint.buildUpon() + } + builder .appendEncodedPath(path) .appendQueryParameter("lang", context.locale.language) .appendQueryParameter("build", build) @@ -48,11 +69,16 @@ internal class InternalApi( .appendQueryParameter("chainName", if (testnet) "testnet" else "mainnet") .appendQueryParameter("bundle_id", context.packageName) - val storeCountry = context.getStoreCountry() - storeCountry?.let { - builder.appendQueryParameter("store_country_code", storeCountry) + _storeCountry?.let { + builder.appendQueryParameter("store_country_code", it) + } + _deviceCountry?.let { + builder.appendQueryParameter("device_country_code", it) + } + + queryParams.forEach { + builder.appendQueryParameter(it.key, it.value) } - builder.appendQueryParameter("device_country_code", context.deviceCountry) builder.build().toString() } @@ -64,8 +90,10 @@ internal class InternalApi( build: String = appVersionName, locale: Locale, boot: Boolean = false, + queryParams: Map = emptyMap(), + bootFallback: Boolean = false, ): JSONObject { - val url = endpoint(path, testnet, platform, build, boot) + val url = endpoint(path, testnet, platform, build, boot, queryParams, bootFallback) val headers = ArrayMap() headers["Accept-Language"] = locale.toString() val body = withRetry { @@ -74,18 +102,40 @@ internal class InternalApi( return JSONObject(body) } - fun getOnRampData(country: String) = withRetry { - okHttpClient.get("https://swap.tonkeeper.com/v2/onramp/currencies?country=${country.uppercase()}") + private fun swapEndpoint(prefix: String, path: String): String { + val builder = prefix.toUri().buildUpon() + .appendEncodedPath(path) + _deviceCountry?.let { + builder.appendQueryParameter("device_country_code", _deviceCountry) + builder.appendQueryParameter("country", _storeCountry ?: _deviceCountry) + } + _storeCountry?.let { + builder.appendQueryParameter("store_country_code", _storeCountry) + } + return builder.build().toString() } - fun getEthenaStakingAPY(address: String): BigDecimal = withRetry { - val json = request("ethena/staking?address=$address", false, locale = context.locale) - BigDecimal.valueOf(json.getDouble("value")) - } ?: BigDecimal.ZERO + fun getOnRampData(prefix: String) = withRetry { + okHttpClient.get(swapEndpoint(prefix, "v2/onramp/currencies")) + } - fun calculateOnRamp(args: OnRampArgsEntity) = withRetry { - val url = "https://swap.tonkeeper.com/v2/onramp/calculate" - okHttpClient.postJSON(url, args.toJSON().toString()).body?.string() + fun getOnRampPaymentMethods(prefix: String, currency: String) = withRetry { + okHttpClient.get(swapEndpoint(prefix, "v2/onramp/payment_methods")) + } + + fun getOnRampMerchants(prefix: String) = withRetry { + okHttpClient.get(swapEndpoint(prefix, "v2/onramp/merchants")) + } + + fun calculateOnRamp(prefix: String, args: OnRampArgsEntity): String? { + val json = args.toJSON() + _deviceCountry?.let { json.put("country", _deviceCountry) } + return withRetry { + okHttpClient.postJSON( + swapEndpoint(prefix, "v2/onramp/calculate"), + json.toString() + ).body.string() + } } fun getNotifications(): List { @@ -113,7 +163,11 @@ internal class InternalApi( val telegramBots = domains.filter { it.startsWith("@") }.map { "t.me/${it.substring(1)}" } val maskDomains = domains.filter { it.startsWith("*.") } val cleanDomains = domains.filter { domain -> - !domain.startsWith("@") && !domain.startsWith("*.") && maskDomains.none { mask -> domain.endsWith(".$mask") } + !domain.startsWith("@") && !domain.startsWith("*.") && maskDomains.none { mask -> + domain.endsWith( + ".$mask" + ) + } } return (maskDomains + cleanDomains + telegramBots).toTypedArray() @@ -129,13 +183,23 @@ internal class InternalApi( return data.getJSONObject("data") } - fun downloadConfig(testnet: Boolean): ConfigEntity? { + fun downloadConfig(testnet: Boolean, fallback: Boolean = false): ConfigEntity? { return try { - val json = request("keys", testnet, locale = context.locale, boot = true) + val json = request( + "keys", + testnet, + locale = context.locale, + boot = true, + bootFallback = fallback + ) ConfigEntity(json, context.isDebug) } catch (e: Throwable) { - FirebaseCrashlytics.getInstance().recordException(e) - null + if (!fallback) { + downloadConfig(testnet, true) + } else { + FirebaseCrashlytics.getInstance().recordException(e) + null + } } } @@ -173,4 +237,14 @@ internal class InternalApi( } } + fun getEthena(accountId: String): EthenaEntity? = withRetry { + val json = request( + "staking/ethena", + false, + locale = context.locale, + queryParams = mapOf("address" to accountId) + ) + EthenaEntity(json) + } + } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/SwapApi.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/SwapApi.kt new file mode 100644 index 000000000..aeb4bb6da --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/internal/SwapApi.kt @@ -0,0 +1,41 @@ +package com.tonapps.wallet.api.internal + +import androidx.core.net.toUri +import com.tonapps.network.get +import com.tonapps.network.sse +import com.tonapps.wallet.api.SwapAssetParam +import com.tonapps.wallet.api.entity.SwapEntity +import com.tonapps.wallet.api.withRetry +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import okhttp3.OkHttpClient + +internal class SwapApi( + private val okHttpClient: OkHttpClient +) { + + fun getSwapAssets(prefix: String) = withRetry { + okHttpClient.get("$prefix/v2/swap/assets") + } + + fun stream( + prefix: String, + from: SwapAssetParam, + to: SwapAssetParam, + userAddress: String + ): Flow { + if (from.isEmpty && to.isEmpty) { + return emptyFlow() + } + val builder = "$prefix/v2/swap/omniston/stream".toUri().buildUpon() + from.apply("from", builder) + to.apply("to", builder) + builder.appendQueryParameter("userAddress", userAddress) + val url = builder.build().toString() + return okHttpClient.sse(url) { }.map { + SwapEntity.parse(it.data) + } + } + +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/omniston/Omniston.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/omniston/Omniston.kt new file mode 100644 index 000000000..28010a31f --- /dev/null +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/omniston/Omniston.kt @@ -0,0 +1,155 @@ +package com.tonapps.wallet.api.omniston + +import kotlinx.serialization.Serializable + +object Omniston { + + fun fixAddress(address: String): String { + if (address.equals("ton", true)) { + return "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c" + } + return address + } + + @Serializable + data class AssetAddress( + val blockchain: Int = 607, + val address: String + ) + + @Serializable + data class Amount( + val offer_units: String? = null, + val ask_units: String? = null + ) + + @Serializable + data class SettlementParams( + val max_price_slippage_bps: Int = 500, + val max_outgoing_messages: Int = 4 + ) + + @Serializable + data class QuoteParams( + val offer_asset_address: AssetAddress, + val ask_asset_address: AssetAddress, + val amount: Amount, + val referrer_fee_bps: Int = 0, + val settlement_methods: List = listOf(0), + val settlement_params: SettlementParams + ) + + @Serializable + data class QuoteResult( + val quote_id: String, + val offer_asset_address: AssetAddress, + val ask_asset_address: AssetAddress, + val offer_amount: Amount, + val ask_amount: Amount, + val rate: String, + val expires_at: Long, + val settlement_methods: List + ) + + @Serializable + data class EventWrapper( + val jsonrpc: String, + val method: String, + val params: EventParams + ) + + @Serializable + data class EventParams( + val subscription: Long, + val result: EventResult + ) + + @Serializable + data class EventResult( + val event: Event + ) + + @Serializable + data class Event( + val quote_updated: QuoteUpdated? = null, + ) + + @Serializable + data class QuoteUpdated( + val quote_id: String, + val resolver_id: String, + val resolver_name: String, + val offer_asset_address: AssetAddress, + val ask_asset_address: AssetAddress, + val offer_units: String, + val ask_units: String, + val referrer_address: String?, + val referrer_fee_units: String, + val protocol_fee_units: String, + val quote_timestamp: Long, + val trade_start_deadline: Long, + val gas_budget: String, + val estimated_gas_consumption: String, + val referrer_fee_asset: AssetAddress, + val protocol_fee_asset: AssetAddress, + val params: SwapParams + ) + + @Serializable + data class SwapParams( + val swap: SwapDetails + ) + + @Serializable + data class SwapDetails( + val routes: List + ) + + @Serializable + data class Route( + val steps: List, + val gas_budget: String + ) + + @Serializable + data class Step( + val offer_asset_address: AssetAddress, + val ask_asset_address: AssetAddress, + val chunks: List + ) + + @Serializable + data class Chunk( + val protocol: String, + val offer_amount: String, + val ask_amount: String, + val extra_version: Int, + val extra: List + ) + + @Serializable + data class TonEventResult( + val ton: TonPayload + ) + + @Serializable + data class TonPayload( + val messages: List + ) + + @Serializable + data class TonMessage( + val target_address: String, + val send_amount: String, + val payload: String + ) + + @Serializable + data class TransactionBuildTransfer( + val destination_address: AssetAddress, + val gas_excess_address: AssetAddress, + val source_address: AssetAddress, + val quote: QuoteUpdated, + ) + +} \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/TronApi.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/TronApi.kt index 407d8d9fe..322f1258e 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/TronApi.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/TronApi.kt @@ -1,5 +1,7 @@ package com.tonapps.wallet.api.tron +import android.net.Uri +import androidx.collection.arrayMapOf import androidx.core.net.toUri import com.tonapps.blockchain.tron.TronTransaction import com.tonapps.blockchain.tron.TronTransfer @@ -12,13 +14,15 @@ import com.tonapps.network.postJSON import com.tonapps.wallet.api.entity.BalanceEntity import com.tonapps.wallet.api.entity.ConfigEntity import com.tonapps.wallet.api.entity.TokenEntity +import com.tonapps.wallet.api.entity.value.Timestamp import com.tonapps.wallet.api.tron.entity.TronEstimationEntity import com.tonapps.wallet.api.tron.entity.TronEventEntity import com.tonapps.wallet.api.tron.entity.TronResourcesEntity import com.tonapps.wallet.api.withRetry -import io.batteryapi.apis.BatteryApi +import io.batteryapi.apis.DefaultApi import io.batteryapi.models.TronSendRequest import io.ktor.util.encodeBase64 +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import okhttp3.OkHttpClient @@ -28,7 +32,7 @@ import java.math.BigInteger class TronApi( private val config: ConfigEntity, private val okHttpClient: OkHttpClient, - private val batteryApi: BatteryApi + private val batteryApi: DefaultApi ) { companion object { @@ -39,13 +43,33 @@ class TronApi( private var safetyMargin: Double? = null + private val tronApiKey: String? + get() = config.tronApiKey?.ifBlank { null } + + private fun headers() = arrayMapOf().apply { + tronApiKey?.let { + put("TRON-PRO-API-KEY", it) + } + } + + private fun post( + uri: Uri, + body: JsonObject + ) = tronRetry { + okHttpClient.postJSON(uri.toString(), body.toString(), headers()) + } ?: throw Exception("tron api failed") + + private fun get(uri: Uri) = tronRetry { + okHttpClient.get(uri.toString(), headers()) + } ?: throw Exception("tron api failed") + fun getTronUsdtBalance( tronAddress: String, ): BalanceEntity { try { val builder = config.tronApiUrl.toUri().buildUpon() .appendEncodedPath("wallet/triggersmartcontract") - val url = builder.build().toString() + val url = builder.build() val requestBody = buildJsonObject { put("owner_address", tronAddress.tronHex()) @@ -54,10 +78,8 @@ class TronApi( put("parameter", tronAddress.encodeTronAddress()) } - val response = withRetry { - okHttpClient.postJSON(url, requestBody.toString()) - } ?: throw Exception("tron api failed") - val body = response.body?.string() ?: throw Exception("empty response") + val response = post(url, requestBody) + val body = response.body.string() val json = JSONObject(body) val constantResultArray = json.optJSONArray("constant_result") @@ -78,36 +100,36 @@ class TronApi( } } - private fun getTronBlockchainHistory( tronAddress: String, limit: Int, - beforeLt: Long? = null + beforeTimestamp: Timestamp?, + afterTimestamp: Timestamp?, ): List { val builder = config.tronApiUrl.toUri().buildUpon() .appendEncodedPath("v1/accounts/$tronAddress/transactions/trc20") .appendQueryParameter("limit", limit.toString()) - beforeLt?.let { - builder.appendQueryParameter("max_timestamp", (it * 1000 - 1).toString()) + beforeTimestamp?.toLong()?.let { + builder.appendQueryParameter("max_timestamp", (it - 1).toString()) + } + afterTimestamp?.toLong()?.let { + builder.appendQueryParameter("min_timestamp", it.toString()) } - val url = builder.build().toString() - val body = withRetry { - okHttpClient.get(url) - } ?: throw Exception("tron api failed") + val body = get(builder.build()) val json = JSONObject(body).getJSONArray("data") - val events = json.map { TronEventEntity(it) } - - return events + return json.map { TronEventEntity(it) } } private fun getBatteryTransfersHistory( batteryAuthToken: String, limit: Int, - beforeLt: Long? = null + beforeTimestamp: Timestamp?, ): List { - val maxTimestamp = beforeLt?.let { it * 1000 - 1 } - val response = batteryApi.getTronTransactions(batteryAuthToken, limit, maxTimestamp); + val maxTimestamp = beforeTimestamp?.toLong()?.let { it - 1 } + val response = withRetry { + batteryApi.getTronTransactions(batteryAuthToken, limit, maxTimestamp) + } ?: return emptyList() return response.transactions.filter { it.txid.isNotEmpty() }.map { TronEventEntity(it) } } @@ -116,12 +138,14 @@ class TronApi( tronAddress: String, tonProofToken: String, limit: Int, - beforeLt: Long?, + beforeTimestamp: Timestamp?, + afterTimestamp: Timestamp? = null, ): List { - val blockchainEvents = getTronBlockchainHistory(tronAddress, limit, beforeLt) - val batteryEvents = getBatteryTransfersHistory(tonProofToken, limit, beforeLt) + val blockchainEvents = getTronBlockchainHistory(tronAddress, limit, beforeTimestamp, afterTimestamp) + val batteryEvents = getBatteryTransfersHistory(tonProofToken, limit, beforeTimestamp) - return (batteryEvents + blockchainEvents).distinctBy { it.transactionHash } + return (batteryEvents + blockchainEvents) + .distinctBy { it.transactionHash } .sortedByDescending { it.timestamp } } @@ -129,7 +153,7 @@ class TronApi( val builder = config.tronApiUrl.toUri().buildUpon() .appendEncodedPath("wallet/triggerconstantcontract") - val url = builder.build().toString() + val url = builder.build() val requestBody = buildJsonObject { put("owner_address", transfer.from) @@ -139,10 +163,8 @@ class TronApi( put("visible", true) } - val response = withRetry { - okHttpClient.postJSON(url, requestBody.toString()) - } ?: throw Exception("tron api failed") - val body = response.body?.string() ?: throw Exception("empty response") + val response = post(url, requestBody) + val body = response.body.string() val json = JSONObject(body) val resultObj = json.optJSONObject("result") @@ -183,11 +205,8 @@ class TronApi( private fun getAccountBandwidth(tronAddress: String): Int { val builder = config.tronApiUrl.toUri().buildUpon().appendEncodedPath("v1/accounts/$tronAddress") - val url = builder.build().toString() - val body = withRetry { - okHttpClient.get(url) - } ?: throw Exception("tron api failed") + val body = get(builder.build()) val json = JSONObject(body) val dataArray = json.optJSONArray("data") @@ -231,13 +250,13 @@ class TronApi( bandwidth = resources.bandwidth, ) - batteryApi.tronSend(tonProofToken, request) + batteryApi.tronSend(request, tonProofToken) } fun buildSmartContractTransaction(transfer: TronTransfer): TronTransaction { val builder = config.tronApiUrl.toUri().buildUpon() .appendEncodedPath("wallet/triggersmartcontract") - val url = builder.build().toString() + val url = builder.build() val requestBody = buildJsonObject { put("contract_address", transfer.contractAddress.tronHex()) @@ -248,20 +267,24 @@ class TronApi( put("fee_limit", 150000000) } - val response = withRetry { - okHttpClient.postJSON(url, requestBody.toString()) - } ?: throw Exception("tron api failed") - val body = response.body?.string() ?: throw Exception("empty response") + val response = post(url, requestBody) + val body = response.body.string() val json = JSONObject(body) return TronTransaction(json = json.getJSONObject("transaction")) } + private fun tronRetry(retryBlock: () -> R) = withRetry( + delay = (1000L..3000L).random() + ) { + retryBlock() + } + fun activateWallet( tronAddress: String, tonProofToken: String, ) { - batteryApi.tronSend(tonProofToken, TronSendRequest(wallet = tronAddress, tx = "")) + batteryApi.tronSend(TronSendRequest(wallet = tronAddress, tx = ""), tonProofToken) } } \ No newline at end of file diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/entity/TronEventEntity.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/entity/TronEventEntity.kt index b8d481170..d4087d9a4 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/entity/TronEventEntity.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/tron/entity/TronEventEntity.kt @@ -2,12 +2,13 @@ package com.tonapps.wallet.api.tron.entity import com.tonapps.icu.Coins import com.tonapps.wallet.api.entity.TokenEntity +import com.tonapps.wallet.api.entity.value.Timestamp import io.batteryapi.models.TronTransactionsListTransactionsInner import org.json.JSONObject data class TronEventEntity( val amount: Coins, - val timestamp: Long, + val timestamp: Timestamp, val transactionHash: String, val from: String, val to: String, @@ -17,7 +18,7 @@ data class TronEventEntity( ) { constructor(json: JSONObject) : this( amount = Coins.ofNano(json.getString("value"), decimals = TokenEntity.TRON_USDT.decimals), - timestamp = json.getLong("block_timestamp") / 1000, + timestamp = Timestamp.from(json.getLong("block_timestamp")), transactionHash = json.getString("transaction_id"), from = json.getString("from"), to = json.getString("to"), @@ -25,7 +26,7 @@ data class TronEventEntity( constructor(transaction: TronTransactionsListTransactionsInner) : this( amount = Coins.ofNano(transaction.amount, decimals = TokenEntity.TRON_USDT.decimals), - timestamp = transaction.timestamp, + timestamp = Timestamp.from(transaction.timestamp), transactionHash = transaction.txid, from = transaction.fromAccount, to = transaction.toAccount, diff --git a/apps/wallet/api/src/main/res/drawable/ic_tsusde_with_bg.png b/apps/wallet/api/src/main/res/drawable/ic_tsusde_with_bg.png new file mode 100644 index 000000000..da88b1238 Binary files /dev/null and b/apps/wallet/api/src/main/res/drawable/ic_tsusde_with_bg.png differ diff --git a/apps/wallet/data/account/build.gradle.kts b/apps/wallet/data/account/build.gradle.kts index dd77e2ecf..38b476884 100644 --- a/apps/wallet/data/account/build.gradle.kts +++ b/apps/wallet/data/account/build.gradle.kts @@ -9,24 +9,24 @@ android { } dependencies { - implementation(Dependence.KotlinX.serializationJSON) - implementation(Dependence.KotlinX.coroutines) - implementation(Dependence.Koin.core) - implementation(Dependence.TON.tvm) - implementation(Dependence.TON.crypto) - implementation(Dependence.TON.tlb) - implementation(Dependence.TON.blockTlb) - implementation(Dependence.TON.tonapiTl) - implementation(Dependence.TON.contract) - implementation(project(Dependence.Module.tonApi)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.Data.rn)) - implementation(project(Dependence.Wallet.Data.rates)) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Lib.security)) - implementation(project(Dependence.Lib.network)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.sqlite)) - implementation(project(Dependence.Lib.ledger)) + implementation(libs.kotlinX.serialization.json) + implementation(libs.kotlinX.coroutines.android) + implementation(libs.koin.core) + implementation(libs.ton.tvm) + implementation(libs.ton.crypto) + implementation(libs.ton.tlb) + implementation(libs.ton.blockTlb) + implementation(libs.ton.tonapiTl) + implementation(libs.ton.contract) + implementation(project(ProjectModules.Module.tonApi)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.Data.rn)) + implementation(project(ProjectModules.Wallet.Data.rates)) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Lib.security)) + implementation(project(ProjectModules.Lib.network)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.sqlite)) + implementation(project(ProjectModules.Lib.ledger)) } diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt index 65038ae93..b31d52ee2 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt @@ -13,6 +13,8 @@ import com.tonapps.blockchain.ton.extensions.toAccountId import com.tonapps.blockchain.tron.KeychainTrxAccountsProvider import com.tonapps.ledger.ton.LedgerAccount import com.tonapps.wallet.api.API +import com.tonapps.wallet.api.entity.value.Blockchain +import com.tonapps.wallet.api.entity.value.BlockchainAddress import com.tonapps.wallet.data.account.entities.MessageBodyEntity import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.account.source.DatabaseSource @@ -273,6 +275,15 @@ class AccountRepository( return trxAccountsProvider.getAddress() } + suspend fun getTronBlockchainAddress(id: String): BlockchainAddress? { + val address = getTronAddress(id) ?: return null + return BlockchainAddress( + value = address, + testnet = false, + blockchain = Blockchain.TRON + ) + } + suspend fun getTronPrivateKey(id: String): BigInteger? { val trxAccountsProvider = getTrxAccountsProvider(id) ?: return null return trxAccountsProvider.getPrivateKey() diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/Wallet.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/Wallet.kt index 5b15cb4b3..7ce2decbf 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/Wallet.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/Wallet.kt @@ -1,6 +1,8 @@ package com.tonapps.wallet.data.account +import android.graphics.Color import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize sealed class Wallet { @@ -18,25 +20,49 @@ sealed class Wallet { @Parcelize data class Label( - val accountName: String, - val emoji: CharSequence, - val color: Int + val accountName: String = "", + val emoji: CharSequence = "", + val color: Int = WalletColor.all.first() ): Parcelable { - val isEmpty: Boolean - get() = accountName.isBlank() && emoji.isBlank() + @IgnoredOnParcel + val isEmpty: Boolean by lazy { + accountName.isBlank() && emoji.isBlank() + } val name: String get() = accountName - val title: CharSequence? - get() = if (isEmpty) { + @IgnoredOnParcel + val title: CharSequence? by lazy { + if (isEmpty) { null } else if (emoji.startsWith("custom_")) { name } else { String.format("%s %s", emoji, name) } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Label + if (accountName != other.accountName) return false + if (emoji != other.emoji) return false + if (color != other.color) return false + return true + } + + override fun hashCode(): Int { + var result = color + result = 31 * result + accountName.hashCode() + result = 31 * result + emoji.hashCode() + result = 31 * result + isEmpty.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + (title?.hashCode() ?: 0) + return result + } } diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/WalletColor.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/WalletColor.kt index 19d5c0405..4fe759847 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/WalletColor.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/WalletColor.kt @@ -1,32 +1,33 @@ package com.tonapps.wallet.data.account import android.graphics.Color +import androidx.core.graphics.toColorInt object WalletColor { - val SteelGray = Color.parseColor("#293342") - val LightSteelGray = Color.parseColor("#424C5C") - val Gray = Color.parseColor("#9DA2A4") - val LightRed = Color.parseColor("#FF8585") - val LightOrange = Color.parseColor("#FFA970") - val LightYellow = Color.parseColor("#FFC95C") - val LightGreen = Color.parseColor("#85CC7A") - val LightBlue = Color.parseColor("#70A0FF") - val LightAquamarine = Color.parseColor("#6CCCF5") - val LightPurple = Color.parseColor("#AD89F5") - val LightViolet = Color.parseColor("#F57FF5") - val LightMagenta = Color.parseColor("#F576B1") - val LightFireOrange = Color.parseColor("#F57F87") - val Red = Color.parseColor("#FF5252") - val Orange = Color.parseColor("#FF8B3D") - val Yellow = Color.parseColor("#FFB92E") - val Green = Color.parseColor("#69CC5A") - val Blue = Color.parseColor("#528BFF") - val Aquamarine = Color.parseColor("#47C8FF") - val Purple = Color.parseColor("#925CFF") - val Violet = Color.parseColor("#FF5CFF") - val Magenta = Color.parseColor("#FF479D") - val FireOrange = Color.parseColor("#FF525D") + val SteelGray = "#293342".toColorInt() + val LightSteelGray = "#424C5C".toColorInt() + val Gray = "#9DA2A4".toColorInt() + val LightRed = "#FF8585".toColorInt() + val LightOrange = "#FFA970".toColorInt() + val LightYellow = "#FFC95C".toColorInt() + val LightGreen = "#85CC7A".toColorInt() + val LightBlue = "#70A0FF".toColorInt() + val LightAquamarine = "#6CCCF5".toColorInt() + val LightPurple = "#AD89F5".toColorInt() + val LightViolet = "#F57FF5".toColorInt() + val LightMagenta = "#F576B1".toColorInt() + val LightFireOrange = "#F57F87".toColorInt() + val Red = "#FF5252".toColorInt() + val Orange = "#FF8B3D".toColorInt() + val Yellow = "#FFB92E".toColorInt() + val Green = "#69CC5A".toColorInt() + val Blue = "#528BFF".toColorInt() + val Aquamarine = "#47C8FF".toColorInt() + val Purple = "#925CFF".toColorInt() + val Violet = "#FF5CFF".toColorInt() + val Magenta = "#FF479D".toColorInt() + val FireOrange = "#FF525D".toColorInt() val all = listOf( SteelGray, diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/entities/WalletEntity.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/entities/WalletEntity.kt index 2e3dcb7ce..797e36415 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/entities/WalletEntity.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/entities/WalletEntity.kt @@ -17,6 +17,8 @@ import com.tonapps.extensions.readEnum import com.tonapps.extensions.readParcelableCompat import com.tonapps.extensions.writeBooleanCompat import com.tonapps.extensions.writeEnum +import com.tonapps.wallet.api.entity.value.Blockchain +import com.tonapps.wallet.api.entity.value.BlockchainAddress import com.tonapps.wallet.data.account.Wallet import kotlinx.parcelize.Parcelize import org.ton.api.pk.PrivateKeyEd25519 @@ -91,6 +93,13 @@ data class WalletEntity( val address: String = contract.address.toWalletAddress(testnet) + val blockchainAddress: BlockchainAddress + get() = BlockchainAddress( + value = address, + testnet = testnet, + blockchain = Blockchain.TON + ) + val isWatchOnly: Boolean get() = type == Wallet.Type.Watch diff --git a/apps/wallet/data/backup/build.gradle.kts b/apps/wallet/data/backup/build.gradle.kts index fa81eb7ab..f9429764e 100644 --- a/apps/wallet/data/backup/build.gradle.kts +++ b/apps/wallet/data/backup/build.gradle.kts @@ -8,8 +8,8 @@ android { } dependencies { - implementation(project(Dependence.Lib.sqlite)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Wallet.Data.rn)) + implementation(project(ProjectModules.Lib.sqlite)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Wallet.Data.rn)) } diff --git a/apps/wallet/data/battery/build.gradle.kts b/apps/wallet/data/battery/build.gradle.kts index 56d40c87c..631851974 100644 --- a/apps/wallet/data/battery/build.gradle.kts +++ b/apps/wallet/data/battery/build.gradle.kts @@ -8,16 +8,14 @@ android { } dependencies { - implementation(Dependence.Squareup.moshi) - implementation(Dependence.Squareup.moshiAdapters) - implementation(Dependence.Squareup.okhttp) + implementation(libs.okhttp) - implementation(project(Dependence.Module.tonApi)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.network)) - implementation(project(Dependence.Lib.icu)) - implementation(project(Dependence.Lib.security)) + implementation(project(ProjectModules.Module.tonApi)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.network)) + implementation(project(ProjectModules.Lib.icu)) + implementation(project(ProjectModules.Lib.security)) } diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryMapper.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryMapper.kt index 53a1d8f7b..52ef57364 100644 --- a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryMapper.kt +++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryMapper.kt @@ -11,7 +11,13 @@ object BatteryMapper { balance: Coins, meanFees: String ): Int { + if (!balance.isPositive) { + return 0 + } val meanFeesBigDecimal = BigDecimal(meanFees) + if (BigDecimal.ZERO >= meanFeesBigDecimal) { + return 0 + } return balance.value.divide(meanFeesBigDecimal, 0, RoundingMode.UP).toInt() } diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryRepository.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryRepository.kt index 19d1b2f57..8f6f6bb1c 100644 --- a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryRepository.kt +++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryRepository.kt @@ -109,7 +109,8 @@ class BatteryRepository( ignoreCache: Boolean = false, ): Int = withContext(Dispatchers.IO) { val balance = getBalance(tonProofToken, publicKey, testnet, ignoreCache) - val charges = BatteryMapper.convertToCharges(balance.balance, api.config.batteryMeanFees) + val config = getConfig(testnet, ignoreCache) + val charges = BatteryMapper.convertToCharges(balance.balance, config.chargeCost) charges } diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryConfigEntity.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryConfigEntity.kt index 2e13f35b7..0174189e4 100644 --- a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryConfigEntity.kt +++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryConfigEntity.kt @@ -1,8 +1,10 @@ package com.tonapps.wallet.data.battery.entity import android.os.Parcelable +import io.batteryapi.models.ConfigMeanPrices import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import kotlinx.serialization.SerialName import org.ton.block.AddrStd @Parcelize @@ -10,9 +12,20 @@ data class BatteryConfigEntity( val excessesAccount: String?, val fundReceiver: String?, val rechargeMethods: List, - val gasProxy: List + val gasProxy: List, + val meanPrices: MeanPrices, + val chargeCost: String, + val reservedAmount: String, ) : Parcelable { + @Parcelize + data class MeanPrices( + val batteryMeanPriceSwap: Int, + val batteryMeanPriceJetton: Int, + val batteryMeanPriceNft: Int, + val batteryMeanPriceTronUsdt: Int? = null, + ) : Parcelable + @IgnoredOnParcel val excessesAddress: AddrStd? by lazy { excessesAccount?.let { AddrStd(it) } @@ -23,7 +36,10 @@ data class BatteryConfigEntity( excessesAccount = null, fundReceiver = null, rechargeMethods = emptyList(), - gasProxy = emptyList() + gasProxy = emptyList(), + meanPrices = MeanPrices(0, 0, 0, null), + chargeCost = "0", + reservedAmount = "0", ) } } \ No newline at end of file diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodEntity.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodEntity.kt index 514de4089..4a05cfb8b 100644 --- a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodEntity.kt +++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodEntity.kt @@ -26,6 +26,7 @@ data class RechargeMethodEntity( return when (this) { RechargeMethodsMethodsInner.Type.jetton -> RechargeMethodType.JETTON RechargeMethodsMethodsInner.Type.ton -> RechargeMethodType.TON + RechargeMethodsMethodsInner.Type.unknown -> RechargeMethodType.TON } } } diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/RemoteDataSource.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/RemoteDataSource.kt index 3ef996e94..91ce2aeed 100644 --- a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/RemoteDataSource.kt +++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/RemoteDataSource.kt @@ -39,7 +39,15 @@ internal class RemoteDataSource( excessesAccount = config.excessAccount, fundReceiver = config.fundReceiver, rechargeMethods = rechargeMethods.methods.map(::RechargeMethodEntity), - gasProxy = config.gasProxy.map { it.address } + gasProxy = config.gasProxy.map { it.address }, + meanPrices = BatteryConfigEntity.MeanPrices( + batteryMeanPriceSwap = config.meanPrices.batteryMeanPriceSwap, + batteryMeanPriceJetton = config.meanPrices.batteryMeanPriceJetton, + batteryMeanPriceNft = config.meanPrices.batteryMeanPriceNft, + batteryMeanPriceTronUsdt = config.meanPrices.batteryMeanPriceTronUsdt + ), + chargeCost = config.chargeCost, + reservedAmount = config.batteryReservedAmount ) } diff --git a/apps/wallet/data/browser/build.gradle.kts b/apps/wallet/data/browser/build.gradle.kts index 403a84b4d..d4430de92 100644 --- a/apps/wallet/data/browser/build.gradle.kts +++ b/apps/wallet/data/browser/build.gradle.kts @@ -8,13 +8,13 @@ android { } dependencies { - implementation(Dependence.Squareup.okhttp) + implementation(libs.okhttp) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.Data.account)) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.Data.account)) - implementation(project(Dependence.Lib.network)) - implementation(project(Dependence.Lib.extensions)) + implementation(project(ProjectModules.Lib.network)) + implementation(project(ProjectModules.Lib.extensions)) } diff --git a/apps/wallet/data/browser/src/main/java/com/tonapps/wallet/data/browser/BrowserRepository.kt b/apps/wallet/data/browser/src/main/java/com/tonapps/wallet/data/browser/BrowserRepository.kt index e191773bd..861c54f8a 100644 --- a/apps/wallet/data/browser/src/main/java/com/tonapps/wallet/data/browser/BrowserRepository.kt +++ b/apps/wallet/data/browser/src/main/java/com/tonapps/wallet/data/browser/BrowserRepository.kt @@ -41,6 +41,9 @@ class BrowserRepository(context: Context, api: API) { } suspend fun isTrustedApp(country: String, testnet: Boolean, locale: Locale, deeplink: Uri): Boolean { + if (deeplink.host == "dapp.aeon.xyz" || deeplink.host == "tonkeeper.com" || deeplink.host?.endsWith(".tonkeeper.com") == true) { + return true + } val host = deeplink.host ?: return false val apps = getApps(country, testnet, locale) for (app in apps) { diff --git a/apps/wallet/data/collectibles/build.gradle.kts b/apps/wallet/data/collectibles/build.gradle.kts index 158b9210b..aa2a5a682 100644 --- a/apps/wallet/data/collectibles/build.gradle.kts +++ b/apps/wallet/data/collectibles/build.gradle.kts @@ -8,10 +8,10 @@ android { } dependencies { - implementation(project(Dependence.Module.tonApi)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.sqlite)) + implementation(project(ProjectModules.Module.tonApi)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.sqlite)) } diff --git a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/CollectiblesRepository.kt b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/CollectiblesRepository.kt index ef5be2757..bb9c01368 100644 --- a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/CollectiblesRepository.kt +++ b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/CollectiblesRepository.kt @@ -3,12 +3,19 @@ package com.tonapps.wallet.data.collectibles import android.content.Context import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tonapps.blockchain.ton.extensions.equalsAddress import com.tonapps.wallet.api.API +import com.tonapps.wallet.api.withRetry +import com.tonapps.wallet.data.collectibles.entities.DnsExpiringEntity import com.tonapps.wallet.data.collectibles.entities.NftEntity import com.tonapps.wallet.data.collectibles.entities.NftListResult import com.tonapps.wallet.data.collectibles.source.LocalDataSource +import io.extensions.renderType +import io.tonapi.models.TrustType +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext class CollectiblesRepository( private val context: Context, @@ -19,6 +26,24 @@ class CollectiblesRepository( LocalDataSource(context) } + suspend fun getDnsExpiring(accountId: String, testnet: Boolean, period: Int) = api.getDnsExpiring(accountId, testnet, period).map { model -> + DnsExpiringEntity( + expiringAt = model.expiringAt, + name = model.name, + dnsItem = model.dnsItem?.let { NftEntity(it, testnet) } + ) + }.sortedBy { it.daysUntilExpiration } + + suspend fun getDnsSoonExpiring(accountId: String, testnet: Boolean, period: Int = 30) = getDnsExpiring(accountId, testnet, period) + + suspend fun getDnsNftExpiring( + accountId: String, + testnet: Boolean, + nftAddress: String + ) = getDnsExpiring(accountId, testnet, 366).firstOrNull { + it.dnsItem?.address?.equalsAddress(nftAddress) == true + } + fun getNft(accountId: String, testnet: Boolean, address: String): NftEntity? { val nft = localDataSource.getSingle(accountId, testnet, address) if (nft != null) { @@ -64,7 +89,7 @@ class CollectiblesRepository( ): List? { val nftItems = api.getNftItems(address, testnet) ?: return null val items = nftItems.filter { - it.trust != "blacklist" && it.metadata["render_type"] != "hidden" + it.trust != TrustType.blacklist && it.renderType != "hidden" }.map { NftEntity(it, testnet) } localDataSource.save(address, testnet, items.toList()) diff --git a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/DnsExpiringEntity.kt b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/DnsExpiringEntity.kt new file mode 100644 index 000000000..afdc90908 --- /dev/null +++ b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/DnsExpiringEntity.kt @@ -0,0 +1,37 @@ +package com.tonapps.wallet.data.collectibles.entities + +import android.os.Parcelable +import com.tonapps.blockchain.ton.extensions.toRawAddress +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class DnsExpiringEntity( + val expiringAt: Long, + val name: String, + val dnsItem: NftEntity? = null +): Parcelable { + + @IgnoredOnParcel + val addressRaw: String by lazy { + dnsItem?.address?.toRawAddress() ?: "" + } + + @IgnoredOnParcel + val inSale: Boolean by lazy { + dnsItem?.inSale ?: false + } + + @IgnoredOnParcel + val daysUntilExpiration: Int by lazy { + val currentTime = System.currentTimeMillis() / 1000 + val remainingSeconds = expiringAt - currentTime + + if (remainingSeconds <= 0) { + 0 + } else { + (remainingSeconds / (24 * 60 * 60)).toInt() + } + } + +} \ No newline at end of file diff --git a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftEntity.kt b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftEntity.kt index eaeb39552..6e3362af0 100644 --- a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftEntity.kt +++ b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftEntity.kt @@ -119,6 +119,6 @@ data class NftEntity( verified = item.approvedBy.isNotEmpty(), inSale = item.sale != null, dns = item.dns, - trust = Trust(item.trust), + trust = Trust(item.trust.value), ) } \ No newline at end of file diff --git a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftMetadataEntity.kt b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftMetadataEntity.kt index c7e52f642..6e3eaff71 100644 --- a/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftMetadataEntity.kt +++ b/apps/wallet/data/collectibles/src/main/java/com/tonapps/wallet/data/collectibles/entities/NftMetadataEntity.kt @@ -3,6 +3,7 @@ package com.tonapps.wallet.data.collectibles.entities import android.os.Parcelable import android.util.Base64 import android.util.Log +import io.JsonAny import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -43,12 +44,18 @@ data class NftMetadataEntity( "https://c.tonapi.io/json?url=$encoded" } - constructor(map: Map) : this( - strings = map.filter { it.value is String }.mapValues { it.value as String } as HashMap, - buttons = map["buttons"]?.let { buttons -> - (buttons as List>).map { - Button(it) + constructor(map: Map) : this( + strings = HashMap(map.mapNotNull { (key, value) -> + value.asString()?.let { key to it } + }.toMap()), + + buttons = map["buttons"]?.asArray()?.mapNotNull { buttonElement -> + buttonElement.asObject()?.let { buttonMap -> + val stringMap = buttonMap.mapNotNull { (key, value) -> + value.asString()?.let { key to it } + }.toMap() + Button(stringMap) } - } ?: arrayListOf() + } ?: emptyList() ) } \ No newline at end of file diff --git a/apps/wallet/data/contacts/build.gradle.kts b/apps/wallet/data/contacts/build.gradle.kts index 876fd596b..976af010e 100644 --- a/apps/wallet/data/contacts/build.gradle.kts +++ b/apps/wallet/data/contacts/build.gradle.kts @@ -8,8 +8,8 @@ android { } dependencies { - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.sqlite)) - implementation(project(Dependence.Wallet.Data.rn)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.sqlite)) + implementation(project(ProjectModules.Wallet.Data.rn)) } diff --git a/apps/wallet/data/core/build.gradle.kts b/apps/wallet/data/core/build.gradle.kts index a83654d66..778852732 100644 --- a/apps/wallet/data/core/build.gradle.kts +++ b/apps/wallet/data/core/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { id("com.android.library") id("org.jetbrains.kotlin.android") @@ -12,34 +14,26 @@ android { minSdk = Build.minSdkVersion consumerProguardFiles("consumer-rules.pro") } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } } dependencies { - api(platform(Dependence.Firebase.bom)) - api(Dependence.Firebase.crashlytics) - - implementation(Dependence.TON.tvm) - implementation(Dependence.TON.crypto) - implementation(Dependence.TON.tlb) - implementation(Dependence.TON.blockTlb) - implementation(Dependence.TON.tonapiTl) - implementation(Dependence.TON.contract) - implementation(Dependence.Koin.core) - implementation(Dependence.AndroidX.biometric) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.sqlite)) - implementation(project(Dependence.Module.tonApi)) - implementation(project(Dependence.UIKit.flag)) + api(platform(libs.firebase.bom)) + api(libs.firebase.crashlytics) + + implementation(libs.ton.tvm) + implementation(libs.ton.crypto) + implementation(libs.ton.tlb) + implementation(libs.ton.blockTlb) + implementation(libs.ton.tonapiTl) + implementation(libs.ton.contract) + implementation(libs.koin.core) + implementation(libs.androidX.biometric) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.sqlite)) + implementation(project(ProjectModules.Module.tonApi)) + implementation(project(ProjectModules.UIKit.flag)) } diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt index 944ebc8a3..88a9c5fa2 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/BlobDataSource.kt @@ -2,17 +2,14 @@ package com.tonapps.wallet.data.core import android.content.Context import android.os.Parcelable -import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.extensions.cacheFolder import com.tonapps.extensions.file import com.tonapps.extensions.toByteArray import com.tonapps.extensions.toParcel -import com.tonapps.wallet.api.fromJSON -import com.tonapps.wallet.api.toJSON +import io.Serializer import java.io.File import java.io.IOException -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit abstract class BlobDataSource( @@ -40,7 +37,7 @@ abstract class BlobDataSource( timeout: Long = TimeUnit.DAYS.toMillis(90) ): BlobDataSource { return object : BlobDataSource(context, path, timeout) { - override fun onMarshall(data: T) = toJSON(data).toByteArray() + override fun onMarshall(data: T) = Serializer.toJSON(data).toByteArray() override fun onUnmarshall(bytes: ByteArray): T? { if (bytes.isEmpty()) { @@ -48,7 +45,7 @@ abstract class BlobDataSource( } return try { val string = String(bytes) - fromJSON(string) + Serializer.fromJSON(string) } catch (e: Throwable) { FirebaseCrashlytics.getInstance().recordException(e) null diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/Extensions.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/Extensions.kt index 4befdaead..f9ac18470 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/Extensions.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/Extensions.kt @@ -3,6 +3,12 @@ package com.tonapps.wallet.data.core import android.content.Context import androidx.biometric.BiometricManager import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tonapps.blockchain.ton.extensions.equalsAddress +import com.tonapps.wallet.data.core.currency.WalletCurrency + +fun List.query(value: String) = firstOrNull { + it.address.equalsAddress(value) +} fun accountId(accountId: String, testnet: Boolean): String { if (testnet) { diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/CurrencyCountries.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/CurrencyCountries.kt index 9c89dda23..1efd99ef5 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/CurrencyCountries.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/CurrencyCountries.kt @@ -229,4 +229,8 @@ object CurrencyCountries { return currencyToCountryMap[code.uppercase()] ?: "" } + fun getCurrencyCode(countryCode: String): String? { + return currencyToCountryMap.entries.firstOrNull { it.value.equals(countryCode, ignoreCase = true) }?.key + } + } \ No newline at end of file diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/WalletCurrency.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/WalletCurrency.kt index fbf614fb5..9c0f194fe 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/WalletCurrency.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/currency/WalletCurrency.kt @@ -2,6 +2,7 @@ package com.tonapps.wallet.data.core.currency import android.net.Uri import android.os.Parcelable +import android.util.Log import androidx.annotation.DrawableRes import com.tonapps.extensions.toUriOrNull import com.tonapps.uikit.flag.getFlagDrawable @@ -106,6 +107,7 @@ data class WalletCurrency( "KRW", // South Korean Won "IDR", // Indonesian Rupiah "INR", // Indian Rupee + "PKR", // Pakistani Rupee "JPY", // Japanese Yen "CAD", // Canadian Dollar "ARS", // Argentine Peso @@ -193,19 +195,22 @@ data class WalletCurrency( "KWD", // Kuwaiti Dinar "RON", // Romanian Leu "EGP", // Egyptian Pound + "NOK" // Norwegian Krone ) const val USDT_KEY = "USDT" const val USDE_KEY = "USDE" + const val TS_USDE_KEY = "TS_USDE" const val TON_KEY = "TON" const val BTC_KEY = "BTC" const val ETH_KEY = "ETH" private val USDT_TRON_ADDRESS = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" - private val USDT_TON_ADDRESS = "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe" + val USDT_TON_ADDRESS = "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe" private val USDT_ETH_ADDRESS = "0xdac17f958d2ee523a2206206994597c13d831ec7" val USDE_TON_ETHENA_ADDRESS = "0:086fa2a675f74347b08dd4606a549b8fdb98829cb282bc1949d3b12fbaed9dcc" + val TS_USDE_TON_ETHENA_ADDRESS = "0:d0e545323c7acb7102653c073377f7e3c67f122eb94d430a250739f109d4a57d" val USD = WalletCurrency( code = "USD", @@ -239,6 +244,35 @@ data class WalletCurrency( chain = Chain.ETC() ) + fun simple(code: String, decimals: Int, name: String, imageUrl: String): WalletCurrency { + val chain = Chain.Unknown(name, code, decimals) + return WalletCurrency( + code = code, + title = name, + chain = chain, + iconUrl = imageUrl + ) + } + + fun unknownChain( + type: String = "unknown", + address: String = "unknown" + ) = Chain.Unknown(type, address) + + fun unknown( + code: String = "unknown", + name: String = "unknown", + imageUrl: String? = null, + chain: Chain.Unknown = unknownChain() + ): WalletCurrency { + return WalletCurrency( + code = code, + title = name, + chain = chain, + iconUrl = imageUrl + ) + } + fun createChain(type: String, address: String): Chain { return when (type) { "jetton" -> Chain.TON(address) @@ -263,6 +297,13 @@ data class WalletCurrency( chain = Chain.TON(USDE_TON_ETHENA_ADDRESS, 6) ) + val TS_USDE_TON_ETHENA = WalletCurrency( + code = TS_USDE_KEY, + alias = createAlias(TON_KEY, TS_USDE_KEY), + title = "Staked USDe", + chain = Chain.TON(TS_USDE_TON_ETHENA_ADDRESS, 6) + ) + val USDT_TRON = WalletCurrency( code = USDT_KEY, alias = createAlias("TRON", USDT_KEY), @@ -349,6 +390,9 @@ data class WalletCurrency( } fun of(code: String?): WalletCurrency? { + if (code == "US") { + return of("USD") + } if (code.isNullOrBlank()) { return null } else if (code in FIAT) { @@ -386,6 +430,20 @@ data class WalletCurrency( val isTONChain: Boolean get() = chain is Chain.TON + @IgnoredOnParcel + val isTronChain: Boolean + get() = chain is Chain.TRON + + @IgnoredOnParcel + val isJetton: Boolean + get() = if (isTONChain && code == TON_KEY) { + false + } else if (isTONChain) { + true + } else { + false + } + @IgnoredOnParcel val decimals: Int get() = chain.decimals @@ -414,6 +472,49 @@ data class WalletCurrency( } } + @IgnoredOnParcel + val address: String by lazy { + when (chain) { + is Chain.TON -> chain.address + is Chain.TRON -> chain.address + is Chain.ETC -> chain.address + else -> code + } + } + + @IgnoredOnParcel + val symbol: String by lazy { + /*if (chain is Chain.FIAT) { + code + } else if (chain is Chain.TON) { + chain.address + } else { + chain.name // code + }*/ + code + } + + @IgnoredOnParcel + val key: String by lazy { + if (fiat) { + "fiat:$code" + } else if (isUSDT) { + "stablecoin:$code" + } else if (isTONChain && code == TON_KEY) { + "crypto:TON" + } else if (isTONChain) { + "crypto:TON:$address" + } else { + "crypto:$code" + } + } + + + @IgnoredOnParcel + val tokenQuery: String by lazy { + if (isTONChain) address else code + } + override fun equals(other: Any?): Boolean { val currency = other as? WalletCurrency ?: return false if (!code.equals(currency.code, true)) { @@ -443,4 +544,24 @@ data class WalletCurrency( } return false } + + override fun hashCode(): Int { + var result = code.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + alias.hashCode() + result = 31 * result + chain.hashCode() + result = 31 * result + (iconUrl?.hashCode() ?: 0) + result = 31 * result + fiat.hashCode() + result = 31 * result + isUSDT.hashCode() + result = 31 * result + isTONChain.hashCode() + result = 31 * result + isTronChain.hashCode() + result = 31 * result + decimals + result = 31 * result + (drawableRes ?: 0) + result = 31 * result + isCustom.hashCode() + result = 31 * result + (iconUri?.hashCode() ?: 0) + result = 31 * result + (chainName?.hashCode() ?: 0) + result = 31 * result + address.hashCode() + result = 31 * result + symbol.hashCode() + return result + } } diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/RawMessageEntity.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/RawMessageEntity.kt index a97f64032..6b37bdb6b 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/RawMessageEntity.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/RawMessageEntity.kt @@ -1,10 +1,10 @@ package com.tonapps.wallet.data.core.entity import android.os.Parcelable -import android.util.Log +import com.tonapps.blockchain.ton.TonAddressTags +import com.tonapps.blockchain.ton.extensions.base64 import com.tonapps.blockchain.ton.extensions.cellFromBase64 import com.tonapps.blockchain.ton.extensions.isValidTonAddress -import com.tonapps.extensions.optStringCompat import com.tonapps.extensions.optStringCompatJS import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -16,11 +16,12 @@ import org.ton.block.StateInit import org.ton.cell.Cell import org.ton.tlb.CellRef import org.ton.tlb.asRef +import java.math.BigInteger @Parcelize data class RawMessageEntity( val addressValue: String, - val amount: Long, + val amount: BigInteger, val stateInitValue: String?, val payloadValue: String?, val withBattery: Boolean = false @@ -31,6 +32,11 @@ data class RawMessageEntity( AddrStd.parse(addressValue) } + @IgnoredOnParcel + val addressTags: TonAddressTags by lazy { + TonAddressTags.of(addressValue) + } + @IgnoredOnParcel val coins: Coins by lazy { Coins.ofNano(amount) @@ -70,11 +76,29 @@ data class RawMessageEntity( companion object { - private fun parseAmount(value: Any): Long { + fun of(address: String, amount: BigInteger, payload: String?) = RawMessageEntity( + addressValue = address, + amount = amount, + stateInitValue = null, + payloadValue = payload, + ) + + fun of( + amount: BigInteger, + address: String, + payload: Cell? + ) = RawMessageEntity( + addressValue = address, + amount = amount, + stateInitValue = null, + payloadValue = payload?.base64() + ) + + private fun parseAmount(value: Any): BigInteger { if (value is Long) { - return value + return value.toBigInteger() } - return value.toString().toLong() + return value.toString().toBigInteger() } fun parseArray(array: JSONArray?, withBattery: Boolean): List { @@ -85,7 +109,7 @@ data class RawMessageEntity( for (i in 0 until array.length()) { val json = array.getJSONObject(i) val raw = RawMessageEntity(json, withBattery) - if (0 >= raw.amount) { + if (BigInteger.ZERO >= raw.amount) { throw IllegalArgumentException("Invalid amount: ${raw.amount}") } if (!raw.addressValue.isValidTonAddress()) { diff --git a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/SignRequestEntity.kt b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/SignRequestEntity.kt index ebd0dd4f1..ac2f81349 100644 --- a/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/SignRequestEntity.kt +++ b/apps/wallet/data/core/src/main/java/com/tonapps/wallet/data/core/entity/SignRequestEntity.kt @@ -26,7 +26,9 @@ data class SignRequestEntity( val validUntil: Long, val messages: List, val network: TonNetwork, - val messagesVariants: MessagesVariantsEntity? = null + val messagesVariants: MessagesVariantsEntity? = null, + val seqNo: Int? = null, + val ignoreInsufficientBalance: Boolean = false ): Parcelable { @IgnoredOnParcel @@ -73,6 +75,12 @@ data class SignRequestEntity( private var validUntil: Long? = null private var network: TonNetwork = TonNetwork.MAINNET private val messages = mutableListOf() + private var seqNo: Int? = null + private var ignoreInsufficientBalance = false + + fun setIgnoreInsufficientBalance(ignoreInsufficientBalance: Boolean) = apply { this.ignoreInsufficientBalance = ignoreInsufficientBalance } + + fun setSeqNo(seqNo: Int) = apply { this.seqNo = seqNo } fun setFrom(from: AddrStd) = apply { this.from = from } @@ -84,6 +92,8 @@ data class SignRequestEntity( fun addMessage(message: RawMessageEntity) = apply { messages.add(message) } + fun addMessages(messages: List) = apply { this.messages.addAll(messages) } + fun build(appUri: Uri): SignRequestEntity { return SignRequestEntity( fromValue = from?.toAccountId(), @@ -91,7 +101,9 @@ data class SignRequestEntity( validUntil = validUntil ?: 0, messages = messages.toList(), network = network, - appUri = appUri + appUri = appUri, + seqNo = seqNo, + ignoreInsufficientBalance = ignoreInsufficientBalance ) } } diff --git a/apps/wallet/data/dapps/build.gradle.kts b/apps/wallet/data/dapps/build.gradle.kts index 50661ca7b..cd0ae5059 100644 --- a/apps/wallet/data/dapps/build.gradle.kts +++ b/apps/wallet/data/dapps/build.gradle.kts @@ -9,19 +9,19 @@ android { dependencies { - implementation(Dependence.TON.tvm) - implementation(Dependence.TON.crypto) - implementation(Dependence.TON.tlb) - implementation(Dependence.TON.blockTlb) - implementation(Dependence.TON.tonapiTl) - implementation(Dependence.TON.contract) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.Data.rn)) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.sqlite)) - implementation(project(Dependence.Lib.security)) - implementation(project(Dependence.Lib.network)) - implementation(project(Dependence.Lib.base64)) + implementation(libs.ton.tvm) + implementation(libs.ton.crypto) + implementation(libs.ton.tlb) + implementation(libs.ton.blockTlb) + implementation(libs.ton.tonapiTl) + implementation(libs.ton.contract) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.Data.rn)) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.sqlite)) + implementation(project(ProjectModules.Lib.security)) + implementation(project(ProjectModules.Lib.network)) + implementation(project(ProjectModules.Lib.base64)) } \ No newline at end of file diff --git a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppConnectEntity.kt b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppConnectEntity.kt index a36f5eac9..b5a2090c8 100644 --- a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppConnectEntity.kt +++ b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppConnectEntity.kt @@ -7,6 +7,7 @@ import com.tonapps.extensions.asJSON import com.tonapps.security.CryptoBox import com.tonapps.security.Sodium import com.tonapps.security.hex +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.json.JSONObject @@ -54,6 +55,7 @@ data class AppConnectEntity( } } + @IgnoredOnParcel val publicKeyHex: String by lazy { hex(keyPair.publicKey) } diff --git a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppEntity.kt b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppEntity.kt index fc5cd1ad3..424958bb4 100644 --- a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppEntity.kt +++ b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/entities/AppEntity.kt @@ -48,8 +48,10 @@ data class AppEntity( companion object { + val ETHENA_BASE_URL = "https://ethena.ston.fi".toUri() + val ethena = AppEntity( - url = "https://ethena.ston.fi/".toUri(), + url = ETHENA_BASE_URL, name = "ethena.ston.fi", iconUrl = "https://static.ston.fi/logo/external-logo.jpg", empty = false diff --git a/apps/wallet/data/events/build.gradle.kts b/apps/wallet/data/events/build.gradle.kts index 7d07bf655..26fd17466 100644 --- a/apps/wallet/data/events/build.gradle.kts +++ b/apps/wallet/data/events/build.gradle.kts @@ -1,6 +1,8 @@ plugins { id("com.tonapps.wallet.data") id("kotlin-parcelize") + id("com.google.devtools.ksp") + kotlin("plugin.serialization") } android { @@ -8,14 +10,21 @@ android { } dependencies { - implementation(project(Dependence.Module.tonApi)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.Data.rates)) - implementation(project(Dependence.Wallet.Data.collectibles)) - implementation(project(Dependence.Wallet.api)) - implementation(project(Dependence.Lib.blockchain)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.icu)) - implementation(project(Dependence.Lib.security)) - implementation(project(Dependence.Lib.sqlite)) + implementation(project(ProjectModules.Module.tonApi)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.Data.rates)) + implementation(project(ProjectModules.Wallet.Data.collectibles)) + implementation(project(ProjectModules.Wallet.Data.staking)) + implementation(project(ProjectModules.Wallet.api)) + implementation(project(ProjectModules.Lib.blockchain)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.icu)) + implementation(project(ProjectModules.Lib.security)) + implementation(project(ProjectModules.Lib.sqlite)) + + implementation(libs.compose.paging) + + implementation(libs.androidX.room.runtime) + implementation(libs.androidX.room.ktx) + ksp(libs.androidX.room.compiler) } diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/ActionType.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/ActionType.kt index f737e4b43..ee1d20b57 100644 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/ActionType.kt +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/ActionType.kt @@ -1,10 +1,49 @@ package com.tonapps.wallet.data.events enum class ActionType { - TonTransfer, - JettonTransfer, - JettonSwap, - NftTransfer, + Received, + Send, + CallContract, + NftReceived, + NftSend, + Swap, DeployContract, + DepositStake, + JettonMint, + AuctionBid, + WithdrawStakeRequest, + WithdrawStake, + DomainRenewal, Unknown, -} \ No newline at end of file + NftPurchase, + JettonBurn, + UnSubscribe, + Subscribe, + Fee, + Refund, + Purchase, + GasRelay, + RemoveExtension, + AddExtension, + SetSignatureAllowed, + SetSignatureNotAllowed, +} + +val ActionTypeOut = arrayOf( + ActionType.Send, + ActionType.CallContract, + ActionType.NftSend, + ActionType.Swap, + ActionType.DeployContract, + ActionType.DepositStake, + ActionType.JettonMint, + ActionType.AuctionBid, + ActionType.WithdrawStakeRequest, + ActionType.WithdrawStake, + ActionType.DomainRenewal, + ActionType.NftPurchase, + ActionType.JettonBurn, + ActionType.UnSubscribe, + ActionType.Subscribe, + ActionType.Purchase +) \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/EventsRepository.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/EventsRepository.kt index af4fcf775..221bb5fe9 100644 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/EventsRepository.kt +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/EventsRepository.kt @@ -4,34 +4,54 @@ import android.content.Context import android.util.Log import com.tonapps.wallet.api.API import com.tonapps.wallet.api.entity.TokenEntity +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.api.entity.value.Timestamp import com.tonapps.wallet.api.tron.entity.TronEventEntity -import com.tonapps.wallet.data.events.entities.AccountEventsResult +import com.tonapps.wallet.data.collectibles.CollectiblesRepository import com.tonapps.wallet.data.events.entities.LatestRecipientEntity import com.tonapps.wallet.data.events.source.LocalDataSource import com.tonapps.wallet.data.events.source.RemoteDataSource -import io.tonapi.models.AccountAddress +import com.tonapps.wallet.data.events.tx.TxActionMapper +import com.tonapps.wallet.data.events.tx.TxFetchQuery +import com.tonapps.wallet.data.events.tx.TxPage +import com.tonapps.wallet.data.events.tx.db.TxDatabase +import com.tonapps.wallet.data.rates.RatesRepository import io.tonapi.models.AccountEvent import io.tonapi.models.AccountEvents import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext +import kotlin.collections.emptyList class EventsRepository( scope: CoroutineScope, context: Context, - private val api: API + private val api: API, + private val collectiblesRepository: CollectiblesRepository, + private val ratesRepository: RatesRepository ) { + private val txDatabase = TxDatabase.instance(context) + private val localDataSource: LocalDataSource by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { LocalDataSource(scope, context) } - private val remoteDataSource = RemoteDataSource(api) + private val txActionMapper = TxActionMapper(collectiblesRepository, ratesRepository, api) + private val remoteDataSource = RemoteDataSource(api, txActionMapper) + + val decryptedCommentFlow: Flow> + get() = localDataSource.decryptedCommentFlow - val decryptedCommentFlow = localDataSource.decryptedCommentFlow + private val _hiddenTxIdsFlow = MutableStateFlow>(emptySet()) + val hiddenTxIdsFlow = _hiddenTxIdsFlow.stateIn(scope, SharingStarted.WhileSubscribed(), emptySet()) fun getDecryptedComment(txId: String) = localDataSource.getDecryptedComment(txId) @@ -39,12 +59,25 @@ class EventsRepository( localDataSource.saveDecryptedComment(txId, comment) } + fun clearTxEvents(account: BlockchainAddress) { + localDataSource.clearTxEvents(account) + } + + suspend fun fetch(query: TxFetchQuery): TxPage { + val events = remoteDataSource.events(query) + return TxPage( + source = TxPage.Source.REMOTE, + events = events, + beforeTimestamp = query.beforeTimestamp, + afterTimestamp = query.afterTimestamp, + limit = query.limit + ) + } + suspend fun tronLatestSentTransactions( tronWalletAddress: String, tonProofToken: String ): List { - val events = - getTronLocal(tronWalletAddress) ?: loadTronEvents(tronWalletAddress, tonProofToken) - ?: emptyList() + val events = loadTronEvents(tronWalletAddress, tonProofToken) ?: return emptyList() val sentTransactions = events.filter { it.from == tronWalletAddress && it.to != tronWalletAddress } @@ -70,17 +103,6 @@ class EventsRepository( suspend fun getSingle(eventId: String, testnet: Boolean) = remoteDataSource.getSingle(eventId, testnet) - suspend fun getLast( - accountId: String, - testnet: Boolean - ): AccountEvents? = withContext(Dispatchers.IO) { - try { - remoteDataSource.get(accountId, testnet, limit = 2) - } catch (e: Throwable) { - null - } - } - suspend fun loadForToken( tokenAddress: String, accountId: String, @@ -101,38 +123,22 @@ class EventsRepository( suspend fun loadTronEvents( tronWalletAddress: String, tonProofToken: String, - beforeLt: Long? = null, + maxTimestamp: Long? = null, limit: Int = 30 ) = withContext(Dispatchers.IO) { try { - val events = api.tron.getTronHistory(tronWalletAddress, tonProofToken, limit, beforeLt) + val events = api.tron.getTronHistory(tronWalletAddress, tonProofToken, limit, maxTimestamp?.let { Timestamp.from(it) }) - if (beforeLt == null) { + if (maxTimestamp == null) { localDataSource.setTronEvents(tronWalletAddress, events) } events } catch (e: Throwable) { - Log.d("API", "loadTronEvents: error", e) null } } - fun getFlow( - accountId: String, - testnet: Boolean - ) = flow { - try { - val local = getLocal(accountId, testnet) - if (local != null && local.events.isNotEmpty()) { - emit(AccountEventsResult(cache = true, events = local)) - } - - val remote = getRemote(accountId, testnet) ?: return@flow - emit(AccountEventsResult(cache = false, events = remote)) - } catch (ignored: Throwable) { } - }.cancellable() - suspend fun get( accountId: String, testnet: Boolean @@ -175,6 +181,9 @@ class EventsRepository( ) = withContext(Dispatchers.IO) { val events = getSingle(eventId, testnet) ?: return@withContext localDataSource.addSpam(accountId, testnet, events) + _hiddenTxIdsFlow.update { + it.plus(eventId) + } } suspend fun removeSpam( @@ -183,6 +192,9 @@ class EventsRepository( eventId: String, ) = withContext(Dispatchers.IO) { localDataSource.removeSpam(accountId, testnet, eventId) + _hiddenTxIdsFlow.update { + it.minus(eventId) + } } suspend fun getRemoteSpam( @@ -212,12 +224,6 @@ class EventsRepository( spamList } - suspend fun getTronLocal( - tronWalletAddress: String, - ): List? = withContext(Dispatchers.IO) { - localDataSource.getTronEvents(tronWalletAddress) - } - suspend fun getLocal( accountId: String, testnet: Boolean diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/Extensions.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/Extensions.kt index 5d84c8ab4..643412384 100644 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/Extensions.kt +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/Extensions.kt @@ -1,13 +1,31 @@ package com.tonapps.wallet.data.events import com.tonapps.blockchain.ton.extensions.equalsAddress +import com.tonapps.icu.Coins +import com.tonapps.wallet.data.core.currency.WalletCurrency +import com.tonapps.wallet.data.rates.RatesRepository import io.tonapi.models.AccountAddress import io.tonapi.models.AccountEvent import io.tonapi.models.Action +import io.tonapi.models.JettonTransferAction + +val JettonTransferAction.amountCoins: Coins + get() = Coins.ofNano(amount, jetton.decimals) + +suspend fun Action.getTonAmountRaw(ratesRepository: RatesRepository): Coins { + val tonAmount = tonTransfer?.let { Coins.of(it.amount) } + val jettonAmountInTON = jettonTransfer?.let { + val amountCoins = it.amountCoins + val jettonAddress = it.jetton.address + val rates = ratesRepository.getRates(WalletCurrency.TON, jettonAddress) + rates.convert(jettonAddress, amountCoins) + } + return tonAmount ?: jettonAmountInTON ?: Coins.ZERO +} val Action.isTransfer: Boolean get() { - return type == TxActionType.TonTransfer || type == TxActionType.JettonTransfer || type == TxActionType.NftItemTransfer + return type == Action.Type.TonTransfer || type == Action.Type.JettonTransfer || type == Action.Type.NftItemTransfer || type == Action.Type.Purchase } fun AccountEvent.isOutTransfer(accountId: String): Boolean { @@ -23,7 +41,7 @@ fun Action.isOutTransfer(accountId: String): Boolean { } val Action.recipient: AccountAddress? - get() = nftItemTransfer?.recipient ?: tonTransfer?.recipient ?: jettonTransfer?.recipient ?: jettonSwap?.userWallet ?: jettonMint?.recipient ?: depositStake?.staker ?: withdrawStake?.staker ?: withdrawStakeRequest?.staker + get() = nftItemTransfer?.recipient ?: tonTransfer?.recipient ?: jettonTransfer?.recipient ?: jettonSwap?.userWallet ?: jettonMint?.recipient ?: depositStake?.staker ?: withdrawStake?.staker ?: withdrawStakeRequest?.staker ?: depositTokenStake?.staker ?: withdrawTokenStakeRequest?.staker val Action.sender: AccountAddress? - get() = nftItemTransfer?.sender ?:tonTransfer?.sender ?: jettonTransfer?.sender ?: jettonSwap?.userWallet ?: jettonBurn?.sender ?: depositStake?.staker ?: withdrawStake?.staker ?: withdrawStakeRequest?.staker + get() = nftItemTransfer?.sender ?:tonTransfer?.sender ?: jettonTransfer?.sender ?: jettonSwap?.userWallet ?: jettonBurn?.sender ?: depositStake?.staker ?: withdrawStake?.staker ?: withdrawStakeRequest?.staker ?: depositTokenStake?.staker ?: withdrawTokenStakeRequest?.staker diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/TxActionType.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/TxActionType.kt deleted file mode 100644 index 580bdfb8a..000000000 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/TxActionType.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tonapps.wallet.data.events - -object TxActionType { - const val TonTransfer = "TonTransfer" - const val JettonTransfer = "JettonTransfer" - const val JettonBurn = "JettonBurn" - const val JettonMint = "JettonMint" - const val NftItemTransfer = "NftItemTransfer" - const val ContractDeploy = "ContractDeploy" - const val Subscribe = "Subscribe" - const val UnSubscribe = "UnSubscribe" - const val AuctionBid = "AuctionBid" - const val NftPurchase = "NftPurchase" - const val DepositStake = "DepositStake" - const val WithdrawStake = "WithdrawStake" - const val WithdrawStakeRequest = "WithdrawStakeRequest" - const val JettonSwap = "JettonSwap" - const val SmartContractExec = "SmartContractExec" - const val ElectionsRecoverStake = "ElectionsRecoverStake" - const val ElectionsDepositStake = "ElectionsDepositStake" - const val DomainRenew = "DomainRenew" - const val InscriptionTransfer = "InscriptionTransfer" - const val InscriptionMint = "InscriptionMint" - const val Unknown = "Unknown" -} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/ActionEntity.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/ActionEntity.kt deleted file mode 100644 index bbe6c4621..000000000 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/ActionEntity.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.tonapps.wallet.data.events.entities - -import android.os.Parcelable -import com.tonapps.icu.Coins -import com.tonapps.wallet.api.entity.AccountEntity -import com.tonapps.wallet.api.entity.TokenEntity -import com.tonapps.wallet.data.collectibles.entities.NftEntity -import com.tonapps.wallet.data.events.ActionType -import io.tonapi.models.Action -import io.tonapi.models.ContractDeployAction -import io.tonapi.models.JettonSwapAction -import io.tonapi.models.JettonTransferAction -import io.tonapi.models.NftItemTransferAction -import io.tonapi.models.TonTransferAction -import kotlinx.parcelize.Parcelize - -@Parcelize -data class ActionEntity( - val type: ActionType, - val sender: AccountEntity? = null, - val recipient: AccountEntity? = null, - val comment: String? = null, - val token: TokenEntity? = null, - val amount: Coins? = null, - val nftAddress: String? = null, - var nftEntity: NftEntity? = null -): Parcelable { - - companion object { - fun map( - actions: List, - testnet: Boolean - ): List { - val list = mutableListOf() - for (action in actions) { - action.tonTransfer?.let { - list.add(mapTonTransferAction(it, testnet)) - } - action.jettonTransfer?.let { - list.add(mapJettonTransferAction(it, testnet)) - } - action.nftItemTransfer?.let { - list.add(mapNftTransferAction(it, testnet)) - } - action.jettonSwap?.let { - list.add(mapJettonSwapAction(it)) - } - action.contractDeploy?.let { - list.add(mapDeployContract(it)) - } - } - return list.toList() - } - - private fun mapTonTransferAction( - tonTransfer: TonTransferAction, - testnet: Boolean - ): ActionEntity { - return ActionEntity( - type = ActionType.TonTransfer, - sender = AccountEntity(tonTransfer.sender, testnet), - recipient = AccountEntity(tonTransfer.recipient, testnet), - comment = tonTransfer.comment, - token = TokenEntity.TON, - amount = Coins.of(tonTransfer.amount) - ) - } - - private fun mapJettonTransferAction( - jettonTransfer: JettonTransferAction, - testnet: Boolean - ): ActionEntity { - return ActionEntity( - type = ActionType.JettonTransfer, - sender = jettonTransfer.sender?.let { AccountEntity(it, testnet) }, - recipient = jettonTransfer.recipient?.let { AccountEntity(it, testnet) }, - comment = jettonTransfer.comment, - token = TokenEntity(jettonTransfer.jetton), - amount = Coins.ofNano(jettonTransfer.amount, jettonTransfer.jetton.decimals) - ) - } - - private fun mapJettonSwapAction( - jettonSwap: JettonSwapAction - ): ActionEntity { - return ActionEntity( - type = ActionType.JettonSwap, - ) - } - - private fun mapDeployContract( - deployContract: ContractDeployAction - ): ActionEntity { - return ActionEntity( - type = ActionType.DeployContract, - ) - } - - private fun mapNftTransferAction( - nftTransfer: NftItemTransferAction, - testnet: Boolean - ): ActionEntity { - return ActionEntity( - type = ActionType.NftTransfer, - sender = nftTransfer.sender?.let { AccountEntity(it, testnet) }, - recipient = nftTransfer.recipient?.let { AccountEntity(it, testnet) }, - comment = nftTransfer.comment, - nftAddress = nftTransfer.nft, - ) - } - } -} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/ActionOwnerEntity.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/ActionOwnerEntity.kt deleted file mode 100644 index 959c0cb31..000000000 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/ActionOwnerEntity.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tonapps.wallet.data.events.entities - -class ActionOwnerEntity { -} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/EventEntity.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/EventEntity.kt deleted file mode 100644 index 8f249b4de..000000000 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/EventEntity.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tonapps.wallet.data.events.entities - -import android.os.Parcelable -import io.tonapi.models.AccountEvent -import kotlinx.parcelize.Parcelize - -@Parcelize -data class EventEntity( - val id: String, - val timestamp: Long, - val lt: Long, - val isScam: Boolean, - val inProgress: Boolean, - val fee: Long, - val actions: List -): Parcelable { - - constructor(model: AccountEvent, testnet: Boolean): this( - id = model.eventId, - timestamp = model.timestamp, - lt = model.lt, - isScam = model.isScam, - inProgress = model.inProgress, - fee = model.extra, - actions = ActionEntity.map(model.actions, testnet) - ) -} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/PendingTransactionEntity.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/PendingTransactionEntity.kt deleted file mode 100644 index 31c2fc75e..000000000 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/entities/PendingTransactionEntity.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tonapps.wallet.data.events.entities - -class PendingTransactionEntity { -} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/DatabaseSource.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/DatabaseSource.kt index 71b863881..f517beb70 100644 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/DatabaseSource.kt +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/DatabaseSource.kt @@ -3,12 +3,13 @@ package com.tonapps.wallet.data.events.source import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase -import android.util.Log import com.tonapps.blockchain.ton.extensions.toRawAddress +import com.tonapps.extensions.toByteArray import com.tonapps.sqlite.SQLiteHelper import com.tonapps.sqlite.withTransaction -import com.tonapps.wallet.api.fromJSON -import com.tonapps.wallet.api.toJSON +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.data.events.tx.model.TxEvent +import io.Serializer import io.tonapi.models.AccountEvent import kotlinx.coroutines.CoroutineScope @@ -19,7 +20,7 @@ internal class DatabaseSource( private companion object { private const val DATABASE_NAME = "events.db" - private const val DATABASE_VERSION = 1 + private const val DATABASE_VERSION = 2 private const val SPAM_TABLE_NAME = "spam" private const val SPAM_TABLE_EVENT_ID_COLUMN = "event_id" @@ -28,23 +29,50 @@ internal class DatabaseSource( private const val SPAM_TABLE_BODY_COLUMN = "body" private const val SPAM_TABLE_DATE_COLUMN = "date" + private const val EVENTS_TABLE_NAME = "events" + private const val EVENTS_TABLE_EVENT_ID_COLUMN = "event_id" + private const val EVENTS_TABLE_ACCOUNT_KEY_COLUMN = "account_key" + private const val EVENTS_TABLE_BODY_COLUMN = "body" + private const val EVENTS_TABLE_DATE_COLUMN = "date" + private val spamFields = arrayOf( SPAM_TABLE_BODY_COLUMN ).joinToString(",") + private val eventsFields = arrayOf( + EVENTS_TABLE_BODY_COLUMN + ).joinToString(",") + private fun AccountEvent.toValues(accountId: String, testnet: Boolean): ContentValues { val values = ContentValues() values.put(SPAM_TABLE_EVENT_ID_COLUMN, eventId) values.put(SPAM_TABLE_ACCOUNT_ID_COLUMN, accountId.toRawAddress()) values.put(SPAM_TABLE_TESTNET_COLUMN, if (testnet) 1 else 0) - values.put(SPAM_TABLE_BODY_COLUMN, toJSON(this)) + values.put(SPAM_TABLE_BODY_COLUMN, Serializer.toJSON(this)) values.put(SPAM_TABLE_DATE_COLUMN, timestamp) return values } + + private fun TxEvent.toValues(address: BlockchainAddress): ContentValues { + val values = ContentValues() + values.put(EVENTS_TABLE_EVENT_ID_COLUMN, id) + values.put(EVENTS_TABLE_ACCOUNT_KEY_COLUMN, address.key) + values.put(EVENTS_TABLE_BODY_COLUMN, toByteArray()) + values.put(EVENTS_TABLE_DATE_COLUMN, timestamp.toLong()) + return values + } } override fun create(db: SQLiteDatabase) { createSpamTable(db) + createEventsTable(db) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + super.onUpgrade(db, oldVersion, newVersion) + if (oldVersion < 2) { + createEventsTable(db) + } } private fun createSpamTable(db: SQLiteDatabase) { @@ -60,10 +88,31 @@ internal class DatabaseSource( db.execSQL("CREATE INDEX ${spamIndexPrefix}_account_id_testnet ON $SPAM_TABLE_NAME ($SPAM_TABLE_ACCOUNT_ID_COLUMN, $SPAM_TABLE_TESTNET_COLUMN)") } + private fun createEventsTable(db: SQLiteDatabase) { + db.execSQL("CREATE TABLE $EVENTS_TABLE_NAME (" + + "$EVENTS_TABLE_EVENT_ID_COLUMN TEXT NOT NULL UNIQUE, " + + "$EVENTS_TABLE_ACCOUNT_KEY_COLUMN TEXT NOT NULL, " + + "$EVENTS_TABLE_BODY_COLUMN BLOB," + + "$EVENTS_TABLE_DATE_COLUMN INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" + + ")") + + val eventsIndexPrefix = "idx_${EVENTS_TABLE_NAME}" + db.execSQL("CREATE INDEX ${eventsIndexPrefix}_account ON $EVENTS_TABLE_NAME($EVENTS_TABLE_ACCOUNT_KEY_COLUMN);") + db.execSQL("CREATE INDEX ${eventsIndexPrefix}_account_date ON $EVENTS_TABLE_NAME($EVENTS_TABLE_ACCOUNT_KEY_COLUMN, $EVENTS_TABLE_DATE_COLUMN DESC);") + } + + fun insertEvent(address: BlockchainAddress, events: List) { + writableDatabase.withTransaction { + for (event in events) { + writableDatabase.insertWithOnConflict(EVENTS_TABLE_NAME, null, event.toValues(address), SQLiteDatabase.CONFLICT_REPLACE) + } + } + } + fun addSpam(accountId: String, testnet: Boolean, events: List) { writableDatabase.withTransaction { for (event in events) { - writableDatabase.insert(SPAM_TABLE_NAME, null, event.toValues(accountId, testnet)) + writableDatabase.insertWithOnConflict(SPAM_TABLE_NAME, null, event.toValues(accountId, testnet), SQLiteDatabase.CONFLICT_REPLACE) } } } @@ -80,8 +129,10 @@ internal class DatabaseSource( val bodyIndex = cursor.getColumnIndex(SPAM_TABLE_BODY_COLUMN) val events = mutableListOf() while (cursor.moveToNext()) { - val body = cursor.getString(bodyIndex) - events.add(fromJSON(body)) + try { + val body = cursor.getString(bodyIndex) + events.add(Serializer.fromJSON(body)) + } catch (ignored: Throwable) { } } cursor.close() return events.toList() diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/LocalDataSource.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/LocalDataSource.kt index d675f8bef..6a3c43244 100644 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/LocalDataSource.kt +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/LocalDataSource.kt @@ -5,14 +5,19 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.tonapps.extensions.MutableEffectFlow import com.tonapps.security.Security +import com.tonapps.wallet.api.entity.value.BlockchainAddress import com.tonapps.wallet.api.tron.entity.TronEventEntity import com.tonapps.wallet.data.core.BlobDataSource import com.tonapps.wallet.data.events.entities.LatestRecipientEntity +import com.tonapps.wallet.data.events.tx.TxEvents +import com.tonapps.wallet.data.events.tx.model.TxEvent import io.tonapi.models.AccountEvent import io.tonapi.models.AccountEvents import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn internal class LocalDataSource( scope: CoroutineScope, @@ -25,16 +30,34 @@ internal class LocalDataSource( private const val KEY_ALIAS = "_com_tonapps_events_master_key_" } + private val txEvents = BlobDataSource.simple(context, "tx_events") private val databaseSource: DatabaseSource = DatabaseSource(scope, context) private val eventsCache = BlobDataSource.simpleJSON(context, "events") private val tronEventsCache = BlobDataSource.simpleJSON>(context, "tron_events") private val latestRecipientsCache = BlobDataSource.simpleJSON>(context, LATEST_RECIPIENTS) - private val _decryptedCommentFlow = MutableEffectFlow() - val decryptedCommentFlow = _decryptedCommentFlow.shareIn(scope, SharingStarted.WhileSubscribed(), 1) + private val _decryptedCommentFlow = MutableStateFlow(emptyMap()) + val decryptedCommentFlow = _decryptedCommentFlow.stateIn(scope, SharingStarted.WhileSubscribed(), emptyMap()) private val encryptedPrefs: SharedPreferences by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { Security.pref(context, KEY_ALIAS, NAME) } + fun getTxEvents(account: BlockchainAddress): List { + val events = txEvents.getCache(account.key)?.events + return events ?: listOf() + } + + fun clearTxEvents(account: BlockchainAddress) { + txEvents.clearCache(account.key) + } + + fun setTxEvents(account: BlockchainAddress, events: List) { + if (events.isEmpty()) { + clearTxEvents(account) + } else { + txEvents.setCache(account.key, TxEvents(events)) + } + } + private fun keyDecryptedComment(txId: String): String { return "tx_$txId" } @@ -58,7 +81,7 @@ internal class LocalDataSource( fun saveDecryptedComment(txId: String, comment: String) { encryptedPrefs.edit { putString(keyDecryptedComment(txId), comment) - _decryptedCommentFlow.tryEmit(Unit) + _decryptedCommentFlow.value += (txId to comment) } } diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/RemoteDataSource.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/RemoteDataSource.kt index 43be22661..050ba4db9 100644 --- a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/RemoteDataSource.kt +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/source/RemoteDataSource.kt @@ -2,15 +2,73 @@ package com.tonapps.wallet.data.events.source import com.tonapps.blockchain.ton.extensions.equalsAddress import com.tonapps.wallet.api.API +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.api.entity.value.Timestamp +import com.tonapps.wallet.data.events.tx.TxActionMapper import com.tonapps.wallet.data.events.entities.LatestRecipientEntity +import com.tonapps.wallet.data.events.tx.model.TxEvent import com.tonapps.wallet.data.events.isOutTransfer import com.tonapps.wallet.data.events.recipient +import com.tonapps.wallet.data.events.tx.TxFetchQuery import io.tonapi.models.AccountEvents +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope internal class RemoteDataSource( - private val api: API + private val api: API, + private val mapper: TxActionMapper, ) { + suspend fun events(query: TxFetchQuery): List = coroutineScope { + val fetchLimit = query.limit + val tonDeferred = async { + tonEvents(query.tonAddress, query.beforeTimestamp, query.afterTimestamp, fetchLimit) + } + + val tronDeferred = async { + val address = query.tronAddress ?: return@async emptyList() + val tonProof = query.tonProofToken ?: return@async emptyList() + tronEvents(address, tonProof, query.beforeTimestamp, query.afterTimestamp, fetchLimit) + } + + val tonEvents = tonDeferred.await() + val tronEvents = tronDeferred.await() + (tonEvents + tronEvents).sortedByDescending { it.timestamp }.take(query.limit) + } + + suspend fun tonEvents( + address: BlockchainAddress, + beforeTimestamp: Timestamp?, + afterTimestamp: Timestamp?, + limit: Int, + ): List { + val events = api.fetchTonEvents( + accountId = address.value, + testnet = address.testnet, + beforeTimestamp = beforeTimestamp, + afterTimestamp = afterTimestamp, + limit = limit + ) + return mapper.events(address, events) + } + + fun tronEvents( + address: BlockchainAddress, + tonProofToken: String, + beforeTimestamp: Timestamp?, + afterTimestamp: Timestamp?, + limit: Int + ): List { + val events = api.fetchTronTransactions( + tronAddress = address.value, + tonProofToken = tonProofToken, + beforeTimestamp = beforeTimestamp, + afterTimestamp = afterTimestamp, + limit = limit + ) + return mapper.tronEvents(address, events) + } + fun get( accountId: String, testnet: Boolean, diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxActionMapper.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxActionMapper.kt new file mode 100644 index 000000000..e1f5b69c4 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxActionMapper.kt @@ -0,0 +1,634 @@ +package com.tonapps.wallet.data.events.tx + +import android.util.Log +import com.tonapps.blockchain.ton.extensions.equalsAddress +import com.tonapps.icu.Coins +import com.tonapps.wallet.api.API +import com.tonapps.wallet.api.entity.value.Blockchain +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.api.entity.value.Timestamp +import com.tonapps.wallet.api.tron.entity.TronEventEntity +import com.tonapps.wallet.data.collectibles.CollectiblesRepository +import com.tonapps.wallet.data.collectibles.entities.NftEntity +import com.tonapps.wallet.data.core.currency.WalletCurrency +import com.tonapps.wallet.data.events.ActionType +import com.tonapps.wallet.data.events.tx.model.TxAction +import com.tonapps.wallet.data.events.tx.model.TxActionBody +import com.tonapps.wallet.data.events.tx.model.TxEvent +import com.tonapps.wallet.data.events.tx.model.TxFlag +import com.tonapps.wallet.data.events.getTonAmountRaw +import com.tonapps.wallet.data.events.isOutTransfer +import com.tonapps.wallet.data.rates.RatesRepository +import com.tonapps.wallet.data.staking.StakingPool +import io.tonapi.models.AccountAddress +import io.tonapi.models.AccountEvent +import io.tonapi.models.Action +import io.tonapi.models.ActionSimplePreview +import io.tonapi.models.AddExtensionAction +import io.tonapi.models.AuctionBidAction +import io.tonapi.models.ContractDeployAction +import io.tonapi.models.CurrencyType +import io.tonapi.models.DepositStakeAction +import io.tonapi.models.DepositTokenStakeAction +import io.tonapi.models.DomainRenewAction +import io.tonapi.models.EncryptedComment +import io.tonapi.models.GasRelayAction +import io.tonapi.models.JettonBurnAction +import io.tonapi.models.JettonMintAction +import io.tonapi.models.JettonPreview +import io.tonapi.models.JettonSwapAction +import io.tonapi.models.JettonTransferAction +import io.tonapi.models.JettonVerificationType +import io.tonapi.models.NftItem +import io.tonapi.models.NftItemTransferAction +import io.tonapi.models.NftPurchaseAction +import io.tonapi.models.Price +import io.tonapi.models.PurchaseAction +import io.tonapi.models.RemoveExtensionAction +import io.tonapi.models.SetSignatureAllowedAction +import io.tonapi.models.SmartContractAction +import io.tonapi.models.SubscriptionAction +import io.tonapi.models.TonTransferAction +import io.tonapi.models.TrustType +import io.tonapi.models.UnSubscriptionAction +import io.tonapi.models.WithdrawStakeAction +import io.tonapi.models.WithdrawStakeRequestAction +import io.tonapi.models.WithdrawTokenStakeRequestAction +import kotlin.math.abs + +internal class TxActionMapper( + private val collectiblesRepository: CollectiblesRepository, + private val ratesRepository: RatesRepository, + private val api: API, +) { + + fun tronEvents(address: BlockchainAddress, events: List): List { + return events.mapNotNull { event -> + tronEvent(address, event) + } + } + + fun tronEvent(address: BlockchainAddress, event: TronEventEntity): TxEvent? { + val currency = WalletCurrency.USDT_TRON + val isOutgoing = event.from == address.value + val builder = TxActionBody.Builder(if (isOutgoing) ActionType.Send else ActionType.Received) + builder.setRecipient(TxActionBody.Account( + address = event.to, + testnet = address.testnet + )) + builder.setSender(TxActionBody.Account( + address = event.from, + testnet = address.testnet + )) + val isScam = event.from != address.value && event.amount < Coins.of(0.1, currency.decimals) + if (isOutgoing) { + builder.setOutgoingAmount(event.amount, currency) + } else { + builder.setIncomingAmount(event.amount, currency) + } + val action = TxAction( + body = builder.build(), + status = if (event.isFailed) TxAction.Status.Failed else TxAction.Status.Ok, + isMaybeSpam = false + ) + + val extra = event.batteryCharges?.let { + TxEvent.Extra.Battery(it) + } ?: TxEvent.Extra.Battery() + + return TxEvent( + hash = event.transactionHash, + lt = event.timestamp.value, + timestamp = event.timestamp, + actions = listOf(action), + isScam = isScam, + inProgress = event.inProgress, + progress = .5f, + blockchain = Blockchain.TRON, + extra = extra + ) + } + + suspend fun events(address: BlockchainAddress, events: List) = events.map { event -> + event(address, event) + } + + suspend fun event(address: BlockchainAddress, event: AccountEvent): TxEvent { + val actions = event.actions.map { action -> + action(address, action) + } + val extra = if (event.extra > 0) { + TxEvent.Extra.Refund(Coins.of(event.extra)) + } else { + TxEvent.Extra.Fee(Coins.of(abs(event.extra))) + } + return TxEvent( + hash = event.eventId, + lt = event.lt, + timestamp = Timestamp.from(event.timestamp), + actions = actions, + isScam = event.isScam, + inProgress = event.inProgress, + progress = event.progress, + blockchain = Blockchain.TON, + extra = extra + ) + } + + private fun account(account: AccountAddress, testnet: Boolean) = TxActionBody.Account( + address = account.address, + isScam = account.isScam, + isWallet = account.isWallet, + name = account.name, + icon = account.icon, + testnet = testnet + ) + + private fun currencySimple(price: Price) = WalletCurrency.simple( + code = price.tokenName, + decimals = price.decimals, + name = price.tokenName, + imageUrl = price.image + ) + + private fun fetchNft(address: BlockchainAddress, nftAddress: String) = collectiblesRepository.getNft( + accountId = address.value, + testnet = address.testnet, + address = nftAddress + ) + + private fun product(product: NftEntity) = TxActionBody.Product( + id = product.address, + type = TxActionBody.Product.Type.Nft, + title = product.name, + subtitle = product.collectionName, + imageUrl = product.thumbUri.toString() + ) + + private fun product(nft: NftItem, testnet: Boolean) = product(NftEntity(nft, testnet)) + + private fun text(comment: String?, encryptedComment: EncryptedComment?): TxActionBody.Text? { + if (comment != null) { + val text = comment.trim() + if (text.isBlank()) { + return null + } + return TxActionBody.Text.Plain(text) + } + if (encryptedComment != null) { + return TxActionBody.Text.Encrypted(encryptedComment.encryptionType, encryptedComment.cipherText) + } + return null + } + + private fun currency(jettonPreview: JettonPreview): WalletCurrency { + val chain = WalletCurrency.Chain.TON( + address = jettonPreview.address, + decimals = jettonPreview.decimals, + ) + return WalletCurrency( + code = jettonPreview.symbol, + title = jettonPreview.name, + chain = chain, + iconUrl = jettonPreview.image + ) + } + + private fun currency(price: Price): WalletCurrency { + return when (price.currencyType) { + CurrencyType.fiat -> { + return WalletCurrency.of(price.tokenName) ?: currencySimple(price) + } + CurrencyType.native -> WalletCurrency.TON + CurrencyType.jetton -> { + val jetton = price.jetton ?: return currencySimple(price) + val chain = WalletCurrency.createChain("jetton", jetton) + return WalletCurrency( + code = price.tokenName, + title = price.tokenName, + chain = chain, + iconUrl = price.image + ) + } + else -> currencySimple(price) + } + } + + private suspend fun isMaybeSpam(address: BlockchainAddress, action: Action): Boolean { + val isTransfer = action.type == Action.Type.TonTransfer || action.type == Action.Type.JettonTransfer + if (isTransfer && !action.isOutTransfer(address.value)) { + val total = action.getTonAmountRaw(ratesRepository) + return total < api.config.reportAmount + } else { + return false + } + } + + suspend fun action(address: BlockchainAddress, action: Action): TxAction { + val status = when(action.status) { + Action.Status.ok -> TxAction.Status.Ok + Action.Status.failed -> TxAction.Status.Failed + else -> TxAction.Status.Unknown + } + val body = actionBody(address, action) + return TxAction( + body = body, + status = status, + isMaybeSpam = isMaybeSpam(address, action) + ) + } + + fun actionBody(address: BlockchainAddress, action: Action): TxActionBody { + if (action.gasRelay != null) { + return gasRelay(address,action.gasRelay!!) + } + if (action.purchase != null) { + return purchase(address,action.purchase!!) + } + if (action.jettonSwap != null) { + return jettonSwap(address,action.jettonSwap!!) + } + if (action.jettonTransfer != null) { + return jettonTransfer(address,action.jettonTransfer!!) + } + if (action.tonTransfer != null) { + return tonTransfer(address,action.tonTransfer!!) + } + if (action.smartContractExec != null) { + return smartContract(address,action.smartContractExec!!) + } + if (action.nftItemTransfer != null) { + return nftItemTransfer(address,action.nftItemTransfer!!) + } + if (action.contractDeploy != null) { + return contractDeploy(action.contractDeploy!!) + } + if (action.depositStake != null) { + return depositStake(address,action.depositStake!!) + } + if (action.jettonMint != null) { + return jettonMint(address,action.jettonMint!!) + } + if (action.withdrawStakeRequest != null) { + return withdrawStakeRequest(address, action.withdrawStakeRequest!!) + } + if (action.domainRenew != null) { + return domainRenew(action.domainRenew!!) + } + if (action.auctionBid != null) { + return auctionBid(address,action.auctionBid!!) + } + if (action.withdrawStake != null) { + return withdrawStake(address,action.withdrawStake!!) + } + if (action.nftPurchase != null) { + return nftPurchase(address,action.nftPurchase!!) + } + if (action.jettonBurn != null) { + return jettonBurn(action.jettonBurn!!) + } + if (action.unSubscribe != null) { + return unSubscribe(address,action.unSubscribe!!) + } + if (action.subscribe != null) { + return subscribe(address,action.subscribe!!) + } + if (action.depositTokenStake != null) { + return depositTokenStake(action.depositTokenStake!!) + } + if (action.withdrawTokenStakeRequest != null) { + return withdrawTokenStakeRequest(action.withdrawTokenStakeRequest!!) + } + if (action.removeExtension != null) { + return removeExtension(address, action.removeExtension!!) + } + if (action.addExtension != null) { + return addExtension(address, action.addExtension!!) + } + if (action.setSignatureAllowedAction != null) { + return setSignatureAllowed(address, action.setSignatureAllowedAction!!) + } + return simplePreview(address, action.simplePreview) + } + + private fun removeExtension(address: BlockchainAddress, action: RemoveExtensionAction): TxActionBody { + val builder = TxActionBody.Builder(ActionType.RemoveExtension) + builder.setSubtitle(action.extension) + builder.setSender(account(action.wallet, address.testnet)) + return builder.build() + } + + private fun addExtension(address: BlockchainAddress, action: AddExtensionAction): TxActionBody { + val builder = TxActionBody.Builder(ActionType.AddExtension) + builder.setSubtitle(action.extension) + builder.setSender(account(action.wallet, address.testnet)) + return builder.build() + } + + private fun setSignatureAllowed(address: BlockchainAddress, action: SetSignatureAllowedAction): TxActionBody { + val type = if (action.allowed) ActionType.SetSignatureAllowed else ActionType.SetSignatureNotAllowed + val builder = TxActionBody.Builder(type) + builder.setSender(account(action.wallet, address.testnet)) + return builder.build() + } + + private fun subscribe(address: BlockchainAddress, action: SubscriptionAction): TxActionBody { + val amount = Coins.ofNano(action.price.value, action.price.decimals) + val builder = TxActionBody.Builder(ActionType.Subscribe) + builder.setRecipient(account(action.beneficiary, address.testnet)) + builder.setSubtitle(action.subscription) + builder.setOutgoingAmount(amount) + builder.setImageUrl(action.beneficiary.icon) + return builder.build() + } + + private fun unSubscribe(address: BlockchainAddress, action: UnSubscriptionAction): TxActionBody { + val builder = TxActionBody.Builder(ActionType.UnSubscribe) + builder.setRecipient(account(action.beneficiary, address.testnet)) + builder.setSubtitle(action.subscription) + builder.setImageUrl(action.beneficiary.icon) + return builder.build() + } + + private fun jettonBurn(action: JettonBurnAction): TxActionBody { + val currency = currency(action.jetton) + val amount = Coins.ofNano(action.amount, currency.decimals) + val builder = TxActionBody.Builder(ActionType.JettonBurn) + builder.setOutgoingAmount(amount, currency) + if (action.jetton.verification != JettonVerificationType.whitelist) { + builder.addFlag(TxFlag.UnverifiedToken) + } + return builder.build() + } + + private fun nftPurchase(address: BlockchainAddress, action: NftPurchaseAction): TxActionBody { + val currency = currency(action.amount) + val amount = Coins.of(action.amount.value, currency.decimals) + val recipient = account(action.seller, address.testnet) + val product = product(action.nft, address.testnet) + + val builder = TxActionBody.Builder(ActionType.NftPurchase) + builder.setRecipient(recipient) + builder.setOutgoingAmount(amount, currency) + builder.setProduct(product) + if (!action.nft.verified) { + builder.addFlag(TxFlag.UnverifiedNft) + } else { + builder.addFlag(TxFlag.VerifiedNft) + } + return builder.build() + } + + private fun withdrawStake(address: BlockchainAddress, action: WithdrawStakeAction): TxActionBody { + val amount = Coins.of(action.amount) + val recipient = account(action.pool, address.testnet) + val builder = TxActionBody.Builder(ActionType.WithdrawStake) + builder.setRecipient(recipient) + builder.setOutgoingAmount(amount) + return builder.build() + } + + private fun auctionBid(address: BlockchainAddress, action: AuctionBidAction): TxActionBody { + val currency = currency(action.amount) + val amount = Coins.ofNano(action.amount.value, currency.decimals) + val product = action.nft?.let { + product(it, address.testnet) + } + val recipient = account(action.auction, address.testnet) + val builder = TxActionBody.Builder(ActionType.AuctionBid) + builder.setRecipient(recipient) + builder.setOutgoingAmount(amount, currency) + builder.setProduct(product) + if (action.nft?.verified == false) { + builder.addFlag(TxFlag.UnverifiedNft) + } else if (action.nft?.verified == true) { + builder.addFlag(TxFlag.VerifiedNft) + } + return builder.build() + } + + private fun domainRenew(action: DomainRenewAction): TxActionBody { + val builder = TxActionBody.Builder(ActionType.DomainRenewal) + builder.setSubtitle(action.domain) + return builder.build() + } + + private fun withdrawStakeRequest(address: BlockchainAddress, action: WithdrawStakeRequestAction): TxActionBody { + val amount = Coins.of(action.amount ?: 0L) + val recipient = account(action.pool, address.testnet) + val builder = TxActionBody.Builder(ActionType.WithdrawStakeRequest) + builder.setRecipient(recipient) + builder.setOutgoingAmount(amount) + return builder.build() + } + + private fun jettonMint(address: BlockchainAddress, action: JettonMintAction): TxActionBody { + val amount = Coins.ofNano(action.amount, action.jetton.decimals) + val currency = currency(action.jetton) + val recipient = account(action.recipient, address.testnet) + val builder = TxActionBody.Builder(ActionType.JettonMint) + builder.setRecipient(recipient) + builder.setOutgoingAmount(amount, currency) + if (action.jetton.verification != JettonVerificationType.whitelist) { + builder.addFlag(TxFlag.UnverifiedToken) + } + return builder.build() + } + + private fun withdrawTokenStakeRequest(action: WithdrawTokenStakeRequestAction): TxActionBody { + val stakeMeta = action.stakeMeta + val ingoingAmount = stakeMeta?.let { + val currency = currency(it) + val amount = Coins.ofNano(it.value, it.decimals) + TxActionBody.Value(amount, currency) + } + + val builder = TxActionBody.Builder(ActionType.WithdrawStake) + builder.setSubtitle(action.protocol.name) + builder.setImageUrl(action.protocol.image) + ingoingAmount?.let(builder::setIncomingAmount) + return builder.build() + } + + private fun depositTokenStake(action: DepositTokenStakeAction): TxActionBody { + val stakeMeta = action.stakeMeta + val outgoingAmount = stakeMeta?.let { + val currency = currency(it) + val amount = Coins.ofNano(it.value, it.decimals) + TxActionBody.Value(amount, currency) + } + + val builder = TxActionBody.Builder(ActionType.DepositStake) + builder.setSubtitle(action.protocol.name) + builder.setImageUrl(action.protocol.image) + outgoingAmount?.let(builder::setOutgoingAmount) + return builder.build() + } + + private fun depositStake(address: BlockchainAddress, action: DepositStakeAction): TxActionBody { + val implementation = StakingPool.implementation(action.implementation) + // TODO fix after full move KMM + val iconUrl = "android.resource://com.ton_keeper/${StakingPool.getIcon(implementation)}" + val amount = Coins.of(action.amount) + val builder = TxActionBody.Builder(ActionType.DepositStake) + builder.setRecipient(account(action.pool, address.testnet)) + builder.setOutgoingAmount(amount) + builder.setImageUrl(iconUrl) + return builder.build() + } + + private fun contractDeploy(action: ContractDeployAction) = TxActionBody.Builder(ActionType.DeployContract).build() + + private fun nftItemTransfer(address: BlockchainAddress, action: NftItemTransferAction): TxActionBody { + val nft = fetchNft(address, action.nft) + val product = nft?.let(::product) + val sender = action.sender?.let { account(it, address.testnet) } + val recipient = action.recipient?.let { account(it, address.testnet) } + val isOutgoing = sender?.address?.equalsAddress(address.value) == true + val builder = TxActionBody.Builder(if (isOutgoing) ActionType.NftSend else ActionType.NftReceived) + sender?.let(builder::setSender) + recipient?.let(builder::setRecipient) + builder.setProduct(product) + if (!isOutgoing) { + builder.setImageUrl(sender?.icon) + } + builder.setText(text(action.comment, action.encryptedComment)) + if (nft?.verified == false) { + builder.addFlag(TxFlag.UnverifiedNft) + } else if (nft?.verified == true) { + builder.addFlag(TxFlag.VerifiedNft) + } + return builder.build() + } + + private fun smartContract(address: BlockchainAddress, action: SmartContractAction): TxActionBody { + val amount = Coins.of(action.tonAttached) + val builder = TxActionBody.Builder(ActionType.CallContract) + builder.setSender(account(action.executor, address.testnet)) + builder.setSubtitle(action.payload ?: action.operation) + builder.setOutgoingAmount(amount) + return builder.build() + } + + private fun tonTransfer(address: BlockchainAddress, action: TonTransferAction): TxActionBody { + val amount = Coins.of(action.amount) + val currency = WalletCurrency.TON + val sender = account(action.sender, address.testnet) + val recipient = account(action.recipient, address.testnet) + val isOutgoing = sender.address.equalsAddress(address.value) + val builder = TxActionBody.Builder(if (isOutgoing) ActionType.Send else ActionType.Received) + builder.setSender(sender) + builder.setRecipient(recipient) + if (isOutgoing) { + builder.setOutgoingAmount(amount, currency) + } else { + builder.setIncomingAmount(amount, currency) + builder.setImageUrl(action.sender.icon) + } + builder.setText(text(action.comment, action.encryptedComment)) + return builder.build() + } + + private fun jettonTransfer(address: BlockchainAddress, action: JettonTransferAction): TxActionBody { + val amount = Coins.ofNano(action.amount, action.jetton.decimals) + val currency = currency(action.jetton) + val sender = action.sender?.let { account(it, address.testnet) } + val recipient = action.recipient?.let { account(it, address.testnet) } + val isOutgoing = recipient?.let { + !it.address.equalsAddress(address.value) + } ?: false + val type = if (isOutgoing) ActionType.Send else ActionType.Received + val builder = TxActionBody.Builder(type) + sender?.let(builder::setSender) + recipient?.let(builder::setRecipient) + if (action.jetton.verification != JettonVerificationType.whitelist) { + builder.addFlag(TxFlag.UnverifiedToken) + } + if (type == ActionType.Received) { + builder.setIncomingAmount(amount, currency) + builder.setImageUrl(sender?.icon) + } else { + builder.setOutgoingAmount(amount, currency) + } + builder.setText(text(action.comment, action.encryptedComment)) + return builder.build() + } + + private fun jettonSwap(address: BlockchainAddress, action: JettonSwapAction): TxActionBody { + val incomingAmount = if (action.tonIn != null) { + TxActionBody.Value(Coins.of(action.tonIn!!), WalletCurrency.TON) + } else { + val currency = currency(action.jettonMasterIn!!) + TxActionBody.Value(Coins.ofNano(action.amountIn, currency.decimals), currency) + } + + val outgoingAmount = if (action.tonOut != null) { + TxActionBody.Value(Coins.of(action.tonOut!!), WalletCurrency.TON) + } else { + val currency = currency(action.jettonMasterOut!!) + TxActionBody.Value(Coins.ofNano(action.amountOut, currency.decimals), currency) + } + + val builder = TxActionBody.Builder(ActionType.Swap) + builder.setSubtitle(action.dex) + builder.setIncomingAmount(outgoingAmount) + builder.setOutgoingAmount(incomingAmount) + builder.setRecipient(account(action.userWallet, address.testnet)) + builder.setSender(account(action.router, address.testnet)) + action.jettonMasterOut?.verification?.let { verification -> + if (verification != JettonVerificationType.whitelist) { + builder.addFlag(TxFlag.UnverifiedToken) + } + } + action.jettonMasterIn?.verification?.let { verification -> + if (verification != JettonVerificationType.whitelist) { + builder.addFlag(TxFlag.UnverifiedToken) + } + } + return builder.build() + } + + private fun purchase(address: BlockchainAddress, action: PurchaseAction): TxActionBody { + val amount = action.amount + val coins = Coins.ofNano(amount.value, amount.decimals) + val builder = TxActionBody.Builder(ActionType.Purchase) + builder.setRecipient(account(action.destination, address.testnet)) + builder.setOutgoingAmount(coins, currency(amount)) + if (amount.verification != TrustType.whitelist) { + builder.addFlag(TxFlag.UnverifiedToken) + } + return builder.build() + } + + private fun gasRelay(address: BlockchainAddress, action: GasRelayAction): TxActionBody { + val coins = Coins.of(action.amount) + val builder = TxActionBody.Builder(ActionType.GasRelay) + builder.setRecipient(account(action.target, address.testnet)) + builder.setIncomingAmount(coins, WalletCurrency.TON) + return builder.build() + } + + private fun simplePreview(address: BlockchainAddress, simplePreview: ActionSimplePreview): TxActionBody { + val unknown = WalletCurrency.unknown( + imageUrl = simplePreview.valueImage + ) + + val sender = simplePreview.accounts.firstOrNull { + it.address.equalsAddress(address.value) + }?.let { account(it, address.testnet) } + + val recipient = simplePreview.accounts.firstOrNull { + !it.address.equalsAddress(address.value) + }?.let { account(it, address.testnet) } + + val builder = TxActionBody.Builder(ActionType.Unknown) + builder.setTitle(simplePreview.name) + builder.setSubtitle(simplePreview.description) + builder.setImageUrl(simplePreview.actionImage) + sender?.let(builder::setSender) + recipient?.let(builder::setRecipient) + builder.setValue(simplePreview.value) + builder.setOutgoingAmount(Coins.ZERO, unknown) + return builder.build() + } +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxEvents.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxEvents.kt new file mode 100644 index 000000000..cb7a5df53 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxEvents.kt @@ -0,0 +1,10 @@ +package com.tonapps.wallet.data.events.tx + +import android.os.Parcelable +import com.tonapps.wallet.data.events.tx.model.TxEvent +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TxEvents( + val events: List +): Parcelable \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxFetchQuery.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxFetchQuery.kt new file mode 100644 index 000000000..592656cc0 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxFetchQuery.kt @@ -0,0 +1,13 @@ +package com.tonapps.wallet.data.events.tx + +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.api.entity.value.Timestamp + +data class TxFetchQuery( + val tonAddress: BlockchainAddress, + val tronAddress: BlockchainAddress?, + val tonProofToken: String?, + val beforeTimestamp: Timestamp?, + val afterTimestamp: Timestamp?, + val limit: Int +) \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxPage.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxPage.kt new file mode 100644 index 000000000..6a8500edc --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/TxPage.kt @@ -0,0 +1,26 @@ +package com.tonapps.wallet.data.events.tx + +import com.tonapps.wallet.api.entity.value.Timestamp +import com.tonapps.wallet.data.events.tx.model.TxEvent + +data class TxPage( + val source: Source, + val events: List, + val beforeTimestamp: Timestamp?, + val afterTimestamp: Timestamp?, + val limit: Int +) { + + enum class Source { + LOCAL, REMOTE + } + + val isEmpty: Boolean + get() = events.isEmpty() + + val isCached: Boolean + get() = source == Source.LOCAL + + val nextKey: Timestamp? + get() = events.minByOrNull { it.timestamp.value }?.timestamp +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxConverters.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxConverters.kt new file mode 100644 index 000000000..97c802fb3 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxConverters.kt @@ -0,0 +1,17 @@ +package com.tonapps.wallet.data.events.tx.db + +import androidx.room.TypeConverter +import com.tonapps.extensions.toByteArray +import com.tonapps.extensions.toParcel +import com.tonapps.wallet.data.events.tx.model.TxEvent + +object TxConverters { + + @TypeConverter + @JvmStatic + fun fromEvent(event: TxEvent) = event.toByteArray() + + @TypeConverter + @JvmStatic + fun toEvent(bytes: ByteArray) = bytes.toParcel() +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxDatabase.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxDatabase.kt new file mode 100644 index 000000000..d04fc0774 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxDatabase.kt @@ -0,0 +1,26 @@ +package com.tonapps.wallet.data.events.tx.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.tonapps.wallet.api.entity.value.ValueConverters + +@Database(entities = [TxRecordEntity::class], version = 1) +@TypeConverters(TxConverters::class, ValueConverters::class) +internal abstract class TxDatabase: RoomDatabase() { + + companion object { + + fun instance(context: Context): TxDatabase { + return Room.databaseBuilder( + context, + TxDatabase::class.java, + "tx_database" + ).build() + } + } + + abstract fun txRecordDao(): TxRecordDao +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxRecordDao.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxRecordDao.kt new file mode 100644 index 000000000..bf4fbad6d --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxRecordDao.kt @@ -0,0 +1,34 @@ +package com.tonapps.wallet.data.events.tx.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.tonapps.wallet.api.entity.value.Blockchain +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.api.entity.value.Timestamp +import com.tonapps.wallet.data.events.tx.model.TxEvent + +@Dao +interface TxRecordDao { + + @Query(""" + SELECT event FROM tx_records + WHERE account = :account + AND blockchain IN (:blockchains) + AND (:beforeTimestamp IS NULL OR timestamp < :beforeTimestamp) + ORDER BY timestamp DESC + LIMIT :limit + """) + fun getEvents( + account: BlockchainAddress, + blockchains: List, + beforeTimestamp: Timestamp?, + limit: Int + ): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + @Transaction + fun insertRecords(records: List) +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxRecordEntity.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxRecordEntity.kt new file mode 100644 index 000000000..656df0cd7 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/db/TxRecordEntity.kt @@ -0,0 +1,41 @@ +package com.tonapps.wallet.data.events.tx.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import com.tonapps.wallet.api.entity.value.Blockchain +import com.tonapps.wallet.api.entity.value.BlockchainAddress +import com.tonapps.wallet.api.entity.value.Timestamp +import com.tonapps.wallet.data.events.tx.model.TxEvent + +@Entity( + tableName = "tx_records", + indices = [ + Index( + value = ["account", "blockchain", "timestamp"], + name = "idx_account_blockchain_timestamp" + ), + Index( + value = ["timestamp"], + name = "idx_timestamp", + orders = [Index.Order.DESC] + ) + ] +) +data class TxRecordEntity( + @PrimaryKey val id: String, + @ColumnInfo(name = "event") val event: TxEvent, + @ColumnInfo(name = "account") val account: BlockchainAddress, + @ColumnInfo(name = "timestamp") val timestamp: Timestamp, + @ColumnInfo(name = "blockchain") val blockchain: Blockchain +) { + + constructor(account: BlockchainAddress, event: TxEvent) : this( + id = event.id, + event = event, + account = account, + timestamp = event.timestamp, + blockchain = event.blockchain + ) +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxAction.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxAction.kt new file mode 100644 index 000000000..b4cbb4d66 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxAction.kt @@ -0,0 +1,94 @@ +package com.tonapps.wallet.data.events.tx.model + +import android.os.Parcelable +import com.tonapps.wallet.data.core.currency.WalletCurrency +import com.tonapps.wallet.data.events.ActionType +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TxAction( + val body: TxActionBody, + val status: Status = Status.Ok, + val isMaybeSpam: Boolean, +): Parcelable { + + enum class Status { + Ok, Failed, Unknown + } + + val amount: TxActionBody.Amount + get() = body.amount + + val type: ActionType + get() = body.type + + val isFailed: Boolean + get() = status == Status.Failed + + val title: String? + get() = body.title.ifBlank { null } + + val recipient: TxActionBody.Account? + get() = body.recipient + + val sender: TxActionBody.Account? + get() = body.sender + + val account: TxActionBody.Account? + get() = if (isOut) recipient else sender + + val subtitle: String? + get() = body.subtitle.ifBlank { + account?.title + } + + val incomingFormatted: CharSequence? + get() = body.amount.incomingFormatted + + val outgoingFormatted: CharSequence? + get() = body.amount.outgoingFormatted + + val text: TxActionBody.Text? + get() = body.text + + val product: TxActionBody.Product? + get() = body.product + + val hasUnverifiedNft: Boolean + get() = body.hasUnverifiedNft + + val hasVerifiedNft: Boolean + get() = body.hasVerifiedNft + + val hasEncryptedText: Boolean + get() = body.hasEncryptedText + + val hasUnverifiedToken: Boolean + get() = body.hasUnverifiedToken + + val imageUrl: String? + get() = body.imageUrl + + val isOut: Boolean + get() = body.isOut + + val currencies: List + get() = body.currencies + + val tokens: List + get() = currencies.filter { !it.fiat } + + val value: TxActionBody.Value? + get() = if (isOut) body.amount.outgoing else body.amount.incoming + + val primaryValue: TxActionBody.Value? + get() = if (type == ActionType.Swap) { + body.amount.outgoing ?: body.amount.incoming + } else { + body.amount.incoming ?: body.amount.outgoing + } + + val encryptedText: TxActionBody.Text.Encrypted? + get() = body.text as? TxActionBody.Text.Encrypted + +} diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxActionBody.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxActionBody.kt new file mode 100644 index 000000000..90bb04ff0 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxActionBody.kt @@ -0,0 +1,207 @@ +package com.tonapps.wallet.data.events.tx.model + +import android.os.Parcelable +import com.tonapps.blockchain.ton.extensions.toUserFriendly +import com.tonapps.extensions.short4 +import com.tonapps.icu.Coins +import com.tonapps.icu.CurrencyFormatter +import com.tonapps.wallet.data.core.currency.WalletCurrency +import com.tonapps.wallet.data.events.ActionType +import com.tonapps.wallet.data.events.ActionTypeOut +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TxActionBody( + val type: ActionType, + val title: String, + val subtitle: String, + val imageUrl: String?, + val value: String?, + val amount: Amount, + val product: Product?, + val sender: Account?, + val recipient: Account?, + val flags: List, + val text: Text? +): Parcelable { + + val hasUnverifiedNft: Boolean + get() = flags.contains(TxFlag.UnverifiedNft) + + val hasVerifiedNft: Boolean + get() = flags.contains(TxFlag.VerifiedNft) + + val hasUnverifiedToken: Boolean + get() = flags.contains(TxFlag.UnverifiedToken) + + val hasEncryptedText: Boolean + get() = text is Text.Encrypted + + val isOut: Boolean + get() = ActionTypeOut.contains(type) + + val currencies: List + get() = amount.currencies + + @Parcelize + data class Account( + val address: String, + val isScam: Boolean = false, + val isWallet: Boolean = true, + val name: String? = null, + val icon: String? = null, + val testnet: Boolean + ): Parcelable { + + @IgnoredOnParcel + val userFriendlyAddress: String by lazy { + address.toUserFriendly( + wallet = isWallet, + testnet = testnet + ) + } + + @IgnoredOnParcel + val title: String by lazy { + name ?: userFriendlyAddress.short4 + } + } + + @Parcelize + sealed class Text: Parcelable { + data class Plain(val text: String) : Text() + data class Encrypted(val type: String, val cipher: String) : Text() + } + + @Parcelize + data class Product( + val id: String, + val type: Type, + val title: String, + val subtitle: String, + val imageUrl: String, + ): Parcelable { + + enum class Type { + Nft + } + } + + @Parcelize + data class Value( + val value: Coins, + val currency: WalletCurrency, + ): Parcelable { + + @IgnoredOnParcel + val formatted: CharSequence? by lazy { + CurrencyFormatter.format( + currency = currency.symbol, + value = value, + cutLongSymbol = true, + ) + } + + @IgnoredOnParcel + val formattedFull: CharSequence? by lazy { + CurrencyFormatter.formatFull(currency.symbol, value, currency.decimals) + } + } + + @Parcelize + data class Amount( + val incoming: Value? = null, + val outgoing: Value? = null + ): Parcelable { + + val isEmpty: Boolean + get() = incoming == null && outgoing == null + + val currencies: List + get() = listOfNotNull(incoming?.currency, outgoing?.currency) + + val incomingFormatted: CharSequence? + get() = incoming?.formatted + + val outgoingFormatted: CharSequence? + get() = outgoing?.formatted + + } + + class Builder(private val type: ActionType) { + + private var title: String = "" + private var subtitle: String = "" + private var imageUrl: String? = null + private var amount: Amount = Amount() + private var product: Product? = null + private var sender: Account? = null + private var recipient: Account? = null + private val flags: MutableList = mutableListOf() + private var text: Text? = null + private var value: String? = null + + fun setTitle(title: String) = apply { this.title = title } + + fun setSubtitle(subtitle: String) = apply { this.subtitle = subtitle } + + fun setSender(sender: Account) = apply { this.sender = sender } + + fun setImageUrl(imageUrl: String?) = apply { this.imageUrl = imageUrl } + + fun setRecipient(recipient: Account) = apply { this.recipient = recipient } + + fun setIncomingAmount(value: Coins, currency: WalletCurrency) = apply { + this.amount = this.amount.copy(incoming = Value(value, currency)) + } + + fun setIncomingAmount(value: Value) = apply { + this.amount = this.amount.copy(incoming = value) + } + + fun setOutgoingAmount(value: Coins, currency: WalletCurrency = WalletCurrency.TON) = apply { + this.amount = this.amount.copy(outgoing = Value(value, currency)) + } + + fun setOutgoingAmount(value: Value) = apply { + this.amount = this.amount.copy(outgoing = value) + } + + fun setTextComment(text: String) = apply { + this.text = Text.Plain(text) + } + + fun setTextEncrypted(type: String, cipher: String) = apply { + this.text = Text.Encrypted(type, cipher) + } + + fun setText(text: Text?) = apply { + this.text = text + } + + fun setProduct(product: Product?) = apply { + this.product = product + } + + fun addFlag(flag: TxFlag) = apply { flags.add(flag) } + + fun setValue(value: String?) = apply { this.value = value } + + fun build() = TxActionBody( + type = type, + title = title, + subtitle = subtitle, + imageUrl = imageUrl, + amount = amount, + product = product, + sender = sender, + recipient = recipient, + flags = flags.toList(), + text = text, + value = value + ) + + } +} + diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxEvent.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxEvent.kt new file mode 100644 index 000000000..845a06288 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxEvent.kt @@ -0,0 +1,57 @@ +package com.tonapps.wallet.data.events.tx.model + +import android.os.Parcelable +import com.tonapps.icu.Coins +import com.tonapps.wallet.api.entity.value.Blockchain +import com.tonapps.wallet.api.entity.value.Timestamp +import com.tonapps.wallet.data.events.ActionType +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TxEvent( + val hash: String, + val lt: Long, + val timestamp: Timestamp, + val actions: List, + val isScam: Boolean, + val inProgress: Boolean, + val progress: Float, + val blockchain: Blockchain, + val extra: Extra, +): Parcelable { + + @Parcelize + sealed class Extra: Parcelable { + data class Refund(val value: Coins) : Extra() + data class Fee(val value: Coins) : Extra() + data class Battery(val charges: Int = 0) : Extra() + } + + val isTron: Boolean + get() = blockchain == Blockchain.TRON + + val hasEncryptedText: Boolean + get() = actions.any { it.hasEncryptedText } + + @IgnoredOnParcel + val id: String by lazy { + if (blockchain == Blockchain.TON) { + hash + } else { + "$blockchain:$hash" + } + } + + val isOut: Boolean + get() = actions.size == 1 && actions.first().isOut + + val spam: Boolean + get() = if (isOut) false else isScam + + fun containsActionType(vararg types: ActionType): Boolean { + return actions.any { action -> + action.type in types + } + } +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxFlag.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxFlag.kt new file mode 100644 index 000000000..467340a49 --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxFlag.kt @@ -0,0 +1,5 @@ +package com.tonapps.wallet.data.events.tx.model + +enum class TxFlag { + ScamAccount, UnverifiedToken, UnverifiedNft, VerifiedNft +} \ No newline at end of file diff --git a/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxTag.kt b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxTag.kt new file mode 100644 index 000000000..60a7d291d --- /dev/null +++ b/apps/wallet/data/events/src/main/java/com/tonapps/wallet/data/events/tx/model/TxTag.kt @@ -0,0 +1,5 @@ +package com.tonapps.wallet.data.events.tx.model + +enum class TxTag { + Send, Received, Spam, Purchase, Subscribe +} \ No newline at end of file diff --git a/apps/wallet/data/passcode/build.gradle.kts b/apps/wallet/data/passcode/build.gradle.kts index 4f4c35a07..5a397012c 100644 --- a/apps/wallet/data/passcode/build.gradle.kts +++ b/apps/wallet/data/passcode/build.gradle.kts @@ -8,14 +8,14 @@ android { dependencies { - implementation(Dependence.AndroidX.biometric) + implementation(libs.androidX.biometric) - implementation(project(Dependence.UIKit.core)) + implementation(project(ProjectModules.UIKit.core)) - implementation(project(Dependence.Wallet.Data.core)) - implementation(project(Dependence.Wallet.Data.account)) - implementation(project(Dependence.Wallet.Data.settings)) - implementation(project(Dependence.Wallet.Data.rn)) - implementation(project(Dependence.Lib.extensions)) - implementation(project(Dependence.Lib.security)) + implementation(project(ProjectModules.Wallet.Data.core)) + implementation(project(ProjectModules.Wallet.Data.account)) + implementation(project(ProjectModules.Wallet.Data.settings)) + implementation(project(ProjectModules.Wallet.Data.rn)) + implementation(project(ProjectModules.Lib.extensions)) + implementation(project(ProjectModules.Lib.security)) } diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/dialog/PasscodeDialog.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/dialog/PasscodeDialog.kt index b9e0d33cb..3a2f7181a 100644 --- a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/dialog/PasscodeDialog.kt +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/dialog/PasscodeDialog.kt @@ -2,6 +2,7 @@ package com.tonapps.wallet.data.passcode.dialog import android.content.Context import android.os.Bundle +import android.view.View import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import com.tonapps.wallet.data.passcode.PasscodeHelper @@ -51,6 +52,7 @@ class PasscodeDialog( init { setContentView(R.layout.dialog_password) + findViewById(R.id.container).setOnClickListener { } headerView = findViewById(R.id.header) headerView.doOnCloseClick = { dismiss() } diff --git a/apps/wallet/data/passcode/src/main/res/layout/dialog_password.xml b/apps/wallet/data/passcode/src/main/res/layout/dialog_password.xml index 45669b74c..33c52cfc8 100644 --- a/apps/wallet/data/passcode/src/main/res/layout/dialog_password.xml +++ b/apps/wallet/data/passcode/src/main/res/layout/dialog_password.xml @@ -1,5 +1,6 @@ = currency.address.length) { + return "native" + } + return when (currency.chain.name.lowercase()) { + "etc", "erc-20" -> "erc-20" + "ton", "jetton" -> "jetton" + "tron", "trc-20" -> "trc-20" + "sol", "spl" -> "spl" + "bnb", "bep-20" -> "bep-20" + "avalanche" -> "avalanche" + "arbitrum" -> "arbitrum" + else -> null + } + } + + fun smartRoundUp(value: Double): Double { + if (value <= 0.0) return value + val mag = 10.0.pow(floor(log10(value))) + val ticks = doubleArrayOf(1.0, 1.5, 2.0, 5.0) + for (t in ticks) { + val target = t * mag + if (value <= target + 1e-12) { + return if (abs(value - target) < 1e-9) value else target + } + } + return 10.0 * mag + } +} \ No newline at end of file diff --git a/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/PurchaseRepository.kt b/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/PurchaseRepository.kt index bfa97715f..3ed17d589 100644 --- a/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/PurchaseRepository.kt +++ b/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/PurchaseRepository.kt @@ -2,32 +2,27 @@ package com.tonapps.wallet.data.purchase import android.content.Context import android.util.Log -import com.tonapps.extensions.JSON +import com.google.firebase.BuildConfig import com.tonapps.extensions.getParcelable import com.tonapps.extensions.prefs import com.tonapps.extensions.putParcelable -import com.tonapps.extensions.putString -import com.tonapps.extensions.state import com.tonapps.extensions.toByteArray import com.tonapps.extensions.toParcel import com.tonapps.wallet.api.API import com.tonapps.wallet.data.core.BlobDataSource import com.tonapps.wallet.data.core.currency.WalletCurrency +import com.tonapps.wallet.data.purchase.entity.MerchantEntity import com.tonapps.wallet.data.purchase.entity.OnRamp import com.tonapps.wallet.data.purchase.entity.PurchaseCategoryEntity import com.tonapps.wallet.data.purchase.entity.PurchaseDataEntity import com.tonapps.wallet.data.purchase.entity.PurchaseMethodEntity +import io.Serializer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.ton.crypto.digest.sha512 import org.ton.crypto.hex @@ -45,62 +40,70 @@ class PurchaseRepository( timeout = TimeUnit.DAYS.toMillis(1) ) { - private companion object { - private const val SEND_CURRENCY_KEY = "send_currency" - private const val RECEIVE_CURRENCY_KEY = "receive_currency" - } - - private val prefs = context.prefs("onramp") - private val onRampCache = simple(context, "onRamp", TimeUnit.DAYS.toMillis(1)) + private val onRampCache = simple(context, "onRamp", TimeUnit.DAYS.toMillis(14)) + private val merchantsCache = simpleJSON>(context,"merchants", TimeUnit.DAYS.toMillis(1)) - private val _sendCurrencyFlow = MutableStateFlow(null) - val sendCurrencyFlow = _sendCurrencyFlow.asStateFlow() - - private val _receiveCurrencyFlow = MutableStateFlow(WalletCurrency.TON) - val receiveCurrencyFlow = _receiveCurrencyFlow.asStateFlow() + fun onRampDataFlow() = flow { + getOnRampDataCache()?.let { + emit(it) + } - var sendCurrency: WalletCurrency? - get() = prefs.getParcelable(SEND_CURRENCY_KEY) - set(value) { - _sendCurrencyFlow.value = value - prefs.putParcelable(SEND_CURRENCY_KEY, value) + fetchOnRampDataCache()?.let { + emit(it) } + }.flowOn(Dispatchers.IO) - var receiveCurrency: WalletCurrency - get() = prefs.getParcelable(RECEIVE_CURRENCY_KEY) ?: WalletCurrency.TON - set(value) { - _receiveCurrencyFlow.value = value - prefs.putParcelable(RECEIVE_CURRENCY_KEY, value) + private fun loadOnRampMerchants(): List { + return try { + val data = api.getOnRampMerchants() ?: throw Exception("No merchants found") + Serializer.JSON.decodeFromString>(data) + } catch (e: Throwable) { + emptyList() } + } - init { - _sendCurrencyFlow.value = sendCurrency - _receiveCurrencyFlow.value = receiveCurrency + fun getMerchants(): List { + var list = merchantsCache.getCache("main") ?: emptyList() + if (list.isEmpty()) { + list = loadOnRampMerchants() + merchantsCache.setCache("main", list) + } + return list } - suspend fun getOnRamp(country: String): OnRamp.Data? = withContext(Dispatchers.IO) { - getOnRampData(country) + suspend fun getPaymentMethods(currency: String): List = withContext(Dispatchers.IO) { + try { + val data = api.getOnRampPaymentMethods(currency) ?: throw Exception("No payment methods found for country: ${api.country}") + Log.d("PurchaseRepositoryLog", "getPaymentMethods: $data") + Serializer.JSON.decodeFromString>(data) + } catch (e: Throwable) { + Log.e("PurchaseRepositoryLog", "error", e) + emptyList() + } } - private suspend fun loadOnRampData(country: String): OnRamp.Data? = withContext(Dispatchers.IO) { - val data = api.getOnRampData(country) ?: return@withContext null + private suspend fun loadOnRampData(): OnRamp.Data? = withContext(Dispatchers.IO) { + val data = api.getOnRampData() ?: return@withContext null try { - JSON.decodeFromString(data) + Serializer.fromJSON(data) } catch (e: Throwable) { null } } - private suspend fun getOnRampData(country: String): OnRamp.Data? { - val cacheKey = "data_$country" - var data = onRampCache.getCache(cacheKey) - if (data == null) { - data = loadOnRampData(country) ?: return null - onRampCache.setCache(cacheKey, data) - } + private fun getOnRampDataCache(): OnRamp.Data? { + val cacheKey = "data_${api.country}" + return onRampCache.getCache(cacheKey) + } + + private suspend fun fetchOnRampDataCache(): OnRamp.Data? { + val data = loadOnRampData() ?: return null + val cacheKey = "data_${api.country}" + onRampCache.setCache(cacheKey, data) return data } + fun get( testnet: Boolean, country: String, diff --git a/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/entity/MerchantEntity.kt b/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/entity/MerchantEntity.kt new file mode 100644 index 000000000..d1c35595e --- /dev/null +++ b/apps/wallet/data/purchase/src/main/java/com/tonapps/wallet/data/purchase/entity/MerchantEntity.kt @@ -0,0 +1,24 @@ +package com.tonapps.wallet.data.purchase.entity + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@Serializable +data class MerchantEntity( + val id: String, + val title: String, + val description: String, + val image: String, + val fee: Double, + val buttons: List