diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 1ad295e73..733bcc07d 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -27,6 +27,7 @@
+
diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt
index 625df56af..bfc875094 100644
--- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt
+++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt
@@ -3,6 +3,7 @@ package com.tonapps.wallet.api
import android.content.Context
import android.util.ArrayMap
import android.util.Log
+import com.squareup.moshi.JsonAdapter
import com.tonapps.blockchain.ton.extensions.EmptyPrivateKeyEd25519
import com.tonapps.blockchain.ton.extensions.base64
import com.tonapps.blockchain.ton.extensions.isValidTonAddress
@@ -17,6 +18,7 @@ import com.tonapps.network.interceptor.AuthorizationInterceptor
import com.tonapps.network.post
import com.tonapps.network.postJSON
import com.tonapps.network.sse
+import com.tonapps.wallet.api.core.SourceAPI
import com.tonapps.wallet.api.entity.AccountDetailsEntity
import com.tonapps.wallet.api.entity.BalanceEntity
import com.tonapps.wallet.api.entity.ChartEntity
@@ -24,6 +26,12 @@ import com.tonapps.wallet.api.entity.ConfigEntity
import com.tonapps.wallet.api.entity.TokenEntity
import com.tonapps.wallet.api.internal.ConfigRepository
import com.tonapps.wallet.api.internal.InternalApi
+import io.batteryapi.apis.BatteryApi
+import io.batteryapi.apis.BatteryApi.UnitsGetBalance
+import io.batteryapi.models.Balance
+import io.batteryapi.models.Config
+import io.batteryapi.models.RechargeMethods
+import io.tonapi.infrastructure.Serializer
import io.tonapi.models.Account
import io.tonapi.models.AccountAddress
import io.tonapi.models.AccountEvent
@@ -67,6 +75,10 @@ class API(
)
}
+ private val batteryHttpClient: OkHttpClient by lazy {
+ createBatteryAPIHttpClient(context)
+ }
+
private val internalApi = InternalApi(context, defaultHttpClient)
private val configRepository = ConfigRepository(context, scope, internalApi)
@@ -88,6 +100,14 @@ class API(
Provider(config.tonapiMainnetHost, config.tonapiTestnetHost, tonAPIHttpClient)
}
+ private val batteryApi by lazy {
+ SourceAPI(BatteryApi(config.batteryHost, batteryHttpClient), BatteryApi(config.batteryTestnetHost, batteryHttpClient))
+ }
+
+ private val emulationJSONAdapter: JsonAdapter by lazy {
+ Serializer.moshi.adapter(MessageConsequences::class.java)
+ }
+
fun accounts(testnet: Boolean) = provider.accounts.get(testnet)
fun jettons(testnet: Boolean) = provider.jettons.get(testnet)
@@ -108,11 +128,29 @@ class API(
fun rates() = provider.rates.get(false)
- fun getAlertNotifications() = withRetry {
+ fun battery(testnet: Boolean) = batteryApi.get(testnet)
+
+ suspend fun getBatteryConfig(testnet: Boolean): Config? {
+ return withRetry { battery(testnet).getConfig() }
+ }
+
+ suspend fun getBatteryRechargeMethods(testnet: Boolean): RechargeMethods? {
+ return withRetry { battery(testnet).getRechargeMethods(false) }
+ }
+
+ suspend fun getBatteryBalance(
+ tonProofToken: String,
+ testnet: Boolean,
+ units: UnitsGetBalance = UnitsGetBalance.ton
+ ): Balance? {
+ return withRetry { battery(testnet).getBalance(tonProofToken, units) }
+ }
+
+ suspend fun getAlertNotifications() = withRetry {
internalApi.getNotifications()
} ?: emptyList()
- private fun isOkStatus(testnet: Boolean): Boolean {
+ suspend fun isOkStatus(testnet: Boolean): Boolean {
try {
val status = withRetry {
provider.blockchain.get(testnet).status()
@@ -165,7 +203,6 @@ class API(
return listOf(accountEvent)
}
-
fun getTokenEvents(
tokenAddress: String,
accountId: String,
@@ -355,6 +392,36 @@ class API(
}
}
+ suspend fun emulateWithBattery(
+ tonProofToken: String,
+ cell: Cell,
+ testnet: Boolean
+ ) = emulateWithBattery(tonProofToken, cell.base64(), testnet)
+
+ suspend fun emulateWithBattery(
+ tonProofToken: String,
+ boc: String,
+ testnet: Boolean
+ ): Pair? {
+ val host = if (testnet) config.batteryTestnetHost else config.batteryHost
+ val url = "$host/wallet/emulate"
+ val data = "{\"boc\":\"$boc\"}"
+
+ val response = withRetry {
+ tonAPIHttpClient.postJSON(url, data, ArrayMap().apply {
+ set("X-TonConnect-Auth", tonProofToken)
+ })
+ } ?: return null
+
+ val supportedByBattery = response.headers["supported-by-battery"] == "true"
+ val allowedByBattery = response.headers["allowed-by-battery"] == "true"
+ val withBattery = supportedByBattery && allowedByBattery
+
+ val string = response.body?.string() ?: return null
+ val consequences = emulationJSONAdapter.fromJson(string) ?: return null
+ return Pair(consequences, withBattery)
+ }
+
suspend fun emulate(
boc: String,
testnet: Boolean,
@@ -380,6 +447,29 @@ class API(
return emulate(cell.base64(), testnet, address, balance)
}
+ suspend fun sendToBlockchainWithBattery(
+ boc: Cell,
+ tonProofToken: String,
+ testnet: Boolean,
+ ) = sendToBlockchainWithBattery(boc.base64(), tonProofToken, testnet)
+
+ suspend fun sendToBlockchainWithBattery(
+ boc: String,
+ tonProofToken: String,
+ testnet: Boolean,
+ ): Boolean = withContext(Dispatchers.IO) {
+ if (!isOkStatus(testnet)) {
+ return@withContext false
+ }
+
+ val request = io.batteryapi.models.EmulateMessageToWalletRequest(boc)
+
+ withRetry {
+ battery(testnet).sendMessage(tonProofToken, request)
+ true
+ } ?: false
+ }
+
suspend fun sendToBlockchain(
boc: String,
testnet: Boolean
@@ -387,6 +477,7 @@ class API(
if (!isOkStatus(testnet)) {
return@withContext false
}
+
val request = SendBlockchainMessageRequest(boc)
withRetry {
blockchain(testnet).sendBlockchainMessage(request)
@@ -649,5 +740,13 @@ class API(
)
)).build()
}
+
+ private fun createBatteryAPIHttpClient(
+ context: Context,
+ ): OkHttpClient {
+ return baseOkHttpClientBuilder()
+ .addInterceptor(AcceptLanguageInterceptor(context.locale))
+ .build()
+ }
}
}
\ 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 881c84f56..b66069790 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,5 +1,6 @@
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
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 0c983979a..178040632 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,7 +1,6 @@
package com.tonapps.wallet.api.entity
import android.os.Parcelable
-import android.util.Log
import com.tonapps.icu.Coins
import io.tonapi.models.JettonBalance
import io.tonapi.models.TokenRates
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 05c012f58..48392cd19 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
@@ -26,12 +26,29 @@ data class ConfigEntity(
val faqUrl: String,
val aptabaseEndpoint: String,
val aptabaseAppKey: String,
- val scamEndpoint: String
+ val scamEndpoint: String,
+ 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 disableBatteryCryptoRecharge: Boolean,
+ val batteryReservedAmount: String,
+ val batteryMaxInputAmount: String,
+ val batteryRefundEndpoint: String,
+ val batteryPromoDisabled: Boolean,
): Parcelable {
val swapUri: Uri
get() = Uri.parse(stonfiUrl)
+ val isBatteryDisabled: Boolean
+ get() = batteryDisabled || batterySendDisabled
+
constructor(json: JSONObject, debug: Boolean) : this(
supportLink = json.getString("supportLink"),
nftExplorer = json.getString("NFTOnExplorerUrl"),
@@ -55,10 +72,22 @@ data class ConfigEntity(
faqUrl = json.getString("faq_url"),
aptabaseEndpoint = json.getString("aptabaseEndpoint"),
aptabaseAppKey = json.getString("aptabaseAppKey"),
- scamEndpoint = json.optString("scamEndpoint", "https://scam.tonkeeper.com")
- ) {
- Log.d("ConfigLog", "ConfigEntity: $json")
- }
+ scamEndpoint = json.optString("scamEndpoint", "https://scam.tonkeeper.com"),
+ 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"),
+ disableBatteryCryptoRecharge = json.optBoolean("disable_battery_crypto_recharge_module", false),
+ batteryMeanPriceNft = json.optString("batteryMeanPrice_nft", "0.03"),
+ batteryMeanPriceSwap = json.optString("batteryMeanPrice_swap", "0.22"),
+ batteryMeanPriceJetton = json.optString("batteryMeanPrice_jetton", "0.06"),
+ batteryReservedAmount = json.optString("batteryReservedAmount", "0.3"),
+ batteryMaxInputAmount = json.optString("batteryMaxInputAmount", "3"),
+ batteryRefundEndpoint = json.optString("batteryRefundEndpoint", "https://battery-refund-app.vercel.app"),
+ batteryPromoDisabled = json.optBoolean("disable_battery_promo_module", true),
+ )
constructor() : this(
supportLink = "mailto:support@tonkeeper.com",
@@ -80,6 +109,20 @@ data class ConfigEntity(
aptabaseEndpoint = "https://anonymous-analytics.tonkeeper.com",
aptabaseAppKey = "A-SH-4314447490",
scamEndpoint = "https://scam.tonkeeper.com",
+ batteryHost = "https://battery.tonkeeper.com",
+ batteryTestnetHost = "https://testnet-battery.tonkeeper.com",
+ batteryBeta = true,
+ batteryDisabled = false,
+ batterySendDisabled = false,
+ batteryMeanFees = "0.0055",
+ disableBatteryCryptoRecharge = false,
+ batteryMeanPriceNft = "0.03",
+ batteryMeanPriceSwap = "0.22",
+ batteryMeanPriceJetton = "0.06",
+ batteryReservedAmount = "0.3",
+ batteryMaxInputAmount = "3",
+ batteryRefundEndpoint = "https://battery-refund-app.vercel.app",
+ batteryPromoDisabled = false,
)
companion object {
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 28e5fdb82..f3f8ba2b7 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
@@ -1,7 +1,6 @@
package com.tonapps.wallet.data.account
import android.content.Context
-import android.util.Log
import com.tonapps.blockchain.ton.contract.BaseWalletContract
import com.tonapps.blockchain.ton.contract.WalletVersion
import com.tonapps.blockchain.ton.contract.walletVersion
@@ -9,7 +8,6 @@ import com.tonapps.blockchain.ton.extensions.EmptyPrivateKeyEd25519
import com.tonapps.blockchain.ton.extensions.base64
import com.tonapps.blockchain.ton.extensions.hex
import com.tonapps.blockchain.ton.extensions.toAccountId
-import com.tonapps.extensions.isMainVersion
import com.tonapps.ledger.ton.LedgerAccount
import com.tonapps.wallet.api.API
import com.tonapps.wallet.data.account.entities.MessageBodyEntity
@@ -37,14 +35,10 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import kotlinx.parcelize.RawValue
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
import org.ton.api.pk.PrivateKeyEd25519
import org.ton.api.pub.PublicKeyEd25519
import org.ton.cell.Cell
import org.ton.contract.wallet.WalletTransfer
-import org.ton.mnemonic.Mnemonic
import java.util.UUID
class AccountRepository(
@@ -427,9 +421,10 @@ class AccountRepository(
wallet: WalletEntity,
seqno: Int,
validUntil: Long,
- transfers: List
+ transfers: List,
+ internalMessage: Boolean = false,
): MessageBodyEntity {
- val body = wallet.createBody(seqno, validUntil, transfers)
+ val body = wallet.createBody(seqno, validUntil, transfers, internalMessage)
return MessageBodyEntity(seqno, body, validUntil)
}
@@ -438,9 +433,10 @@ class AccountRepository(
seqno: Int,
privateKeyEd25519: PrivateKeyEd25519,
validUntil: Long,
- transfers: List
+ transfers: List,
+ internalMessage: Boolean = false,
): Cell {
- val data = messageBody(wallet, seqno, validUntil, transfers)
+ val data = messageBody(wallet, seqno, validUntil, transfers, internalMessage)
return wallet.sign(privateKeyEd25519, data.seqno, data.body)
}
}
\ No newline at end of file
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 c3dfab117..35a7072ba 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
@@ -4,6 +4,7 @@ import android.os.Parcelable
import android.util.Log
import com.tonapps.blockchain.ton.TonNetwork
import com.tonapps.blockchain.ton.contract.BaseWalletContract
+import com.tonapps.blockchain.ton.contract.WalletFeature
import com.tonapps.blockchain.ton.contract.WalletVersion
import com.tonapps.blockchain.ton.extensions.toAccountId
import com.tonapps.blockchain.ton.extensions.toRawAddress
@@ -32,7 +33,7 @@ data class WalletEntity(
data class Ledger(
val deviceId: String,
val accountIndex: Int
- ): Parcelable
+ ) : Parcelable
val contract: BaseWalletContract by lazy {
val network = if (testnet) TonNetwork.TESTNET.value else TonNetwork.MAINNET.value
@@ -69,15 +70,21 @@ data class WalletEntity(
return address.toRawAddress().equals(accountId, ignoreCase = true)
}
+ fun isSupportedFeature(feature: WalletFeature): Boolean {
+ return contract.isSupportedFeature(feature)
+ }
+
fun createBody(
seqno: Int,
validUntil: Long,
- gifts: List
+ gifts: List,
+ internalMessage: Boolean = false,
): Cell {
return contract.createTransferUnsignedBody(
validUntil = validUntil,
seqno = seqno,
- gifts = gifts.toTypedArray()
+ gifts = gifts.toTypedArray(),
+ internalMessage = internalMessage,
)
}
diff --git a/apps/wallet/data/battery/.gitignore b/apps/wallet/data/battery/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/apps/wallet/data/battery/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/apps/wallet/data/battery/build.gradle.kts b/apps/wallet/data/battery/build.gradle.kts
new file mode 100644
index 000000000..c79380c1d
--- /dev/null
+++ b/apps/wallet/data/battery/build.gradle.kts
@@ -0,0 +1,40 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = Build.namespacePrefix("wallet.data.battery")
+ compileSdk = Build.compileSdkVersion
+
+ defaultConfig {
+ minSdk = Build.minSdkVersion
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ implementation(Dependence.KotlinX.coroutines)
+ implementation(Dependence.Koin.core)
+ implementation(Dependence.Squareup.moshi)
+ implementation(Dependence.Squareup.moshiAdapters)
+ implementation(Dependence.Squareup.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))
+}
diff --git a/apps/wallet/data/battery/consumer-rules.pro b/apps/wallet/data/battery/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/wallet/data/battery/proguard-rules.pro b/apps/wallet/data/battery/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/apps/wallet/data/battery/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/apps/wallet/data/battery/src/main/AndroidManifest.xml b/apps/wallet/data/battery/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..227314eeb
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
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
new file mode 100644
index 000000000..da64912b3
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryMapper.kt
@@ -0,0 +1,34 @@
+package com.tonapps.wallet.data.battery
+
+import com.tonapps.icu.Coins
+import com.tonapps.wallet.data.battery.entity.RechargeMethodEntity
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+object BatteryMapper {
+
+ fun convertToCharges(
+ balance: Coins,
+ meanFees: String
+ ): Int {
+ val meanFeesBigDecimal = BigDecimal(meanFees)
+ return balance.value.divide(meanFeesBigDecimal, 0, RoundingMode.UP).toInt()
+ }
+
+ fun calculateChargesAmount(
+ transactionCost: String,
+ meanFees: String
+ ): Int {
+ val meanFeesBigDecimal = BigDecimal(meanFees)
+ val transactionCostBigDecimal = BigDecimal(transactionCost)
+
+ return transactionCostBigDecimal.divide(meanFeesBigDecimal, 0, RoundingMode.HALF_UP)
+ .toInt()
+ }
+
+ fun calculateCryptoCharges(method: RechargeMethodEntity, meanFees: String, amount: Coins): BigDecimal {
+ val meanFeesBigDecimal = BigDecimal(meanFees)
+ val rateBigDecimal = BigDecimal(method.rate)
+ return rateBigDecimal.divide(meanFeesBigDecimal, 0, RoundingMode.HALF_UP).multiply(amount.value)
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..5b13b2a9c
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/BatteryRepository.kt
@@ -0,0 +1,125 @@
+package com.tonapps.wallet.data.battery
+
+import android.content.Context
+import androidx.collection.ArrayMap
+import com.tonapps.extensions.MutableEffectFlow
+import com.tonapps.extensions.filterList
+import com.tonapps.wallet.api.API
+import com.tonapps.wallet.data.battery.entity.BatteryConfigEntity
+import com.tonapps.wallet.data.battery.entity.BatteryBalanceEntity
+import com.tonapps.wallet.data.battery.source.LocalDataSource
+import com.tonapps.wallet.data.battery.source.RemoteDataSource
+import io.tonapi.models.MessageConsequences
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.cancellable
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.ton.api.pub.PublicKeyEd25519
+import org.ton.cell.Cell
+
+class BatteryRepository(
+ context: Context,
+ private val api: API,
+ scope: CoroutineScope,
+) {
+ private val localDataSource = LocalDataSource(context)
+ private val remoteDataSource = RemoteDataSource(api)
+
+ private val _balanceUpdatedFlow = MutableEffectFlow()
+ val balanceUpdatedFlow = _balanceUpdatedFlow.asSharedFlow()
+
+ init {
+ _balanceUpdatedFlow.tryEmit(Unit)
+ scope.launch(Dispatchers.IO) {
+ getConfig(false, ignoreCache = true)
+ }
+ }
+
+ suspend fun getConfig(
+ testnet: Boolean,
+ ignoreCache: Boolean = false
+ ): BatteryConfigEntity = withContext(Dispatchers.IO) {
+ if (ignoreCache) {
+ fetchConfig(testnet)
+ } else {
+ localDataSource.getConfig(testnet) ?: fetchConfig(testnet)
+ }
+ }
+
+ private suspend fun fetchConfig(testnet: Boolean): BatteryConfigEntity {
+ val config = remoteDataSource.fetchConfig(testnet) ?: return BatteryConfigEntity.Empty
+ localDataSource.setConfig(testnet, config)
+ return config
+ }
+
+ suspend fun getBalance(
+ tonProofToken: String,
+ publicKey: PublicKeyEd25519,
+ testnet: Boolean,
+ ignoreCache: Boolean = false,
+ ): BatteryBalanceEntity = withContext(Dispatchers.IO) {
+ val balance = if (ignoreCache) {
+ fetchBalance(publicKey, tonProofToken, testnet)
+ } else {
+ localDataSource.getBalance(publicKey, testnet) ?: fetchBalance(publicKey, tonProofToken, testnet)
+ }
+ balance
+ }
+
+ private suspend fun fetchBalance(
+ publicKey: PublicKeyEd25519,
+ tonProofToken: String,
+ testnet: Boolean
+ ): BatteryBalanceEntity {
+ val balance = remoteDataSource.fetchBalance(tonProofToken, testnet) ?: return BatteryBalanceEntity.Empty
+ localDataSource.setBalance(publicKey, testnet, balance)
+ _balanceUpdatedFlow.emit(Unit)
+ return balance
+ }
+
+ suspend fun emulate(
+ tonProofToken: String,
+ publicKey: PublicKeyEd25519,
+ testnet: Boolean,
+ boc: Cell,
+ forceRelayer: Boolean = false
+ ): Pair? = withContext(Dispatchers.IO) {
+
+ val balance = getBalance(
+ tonProofToken = tonProofToken,
+ publicKey = publicKey,
+ testnet = testnet
+ ).balance
+
+ if (!forceRelayer && !balance.isPositive) {
+ throw IllegalStateException("Zero balance")
+ }
+
+ api.emulateWithBattery(tonProofToken, boc, testnet)
+ }
+
+ suspend fun getAppliedPromo(
+ testnet: Boolean,
+ ): String? = withContext(Dispatchers.IO) {
+ localDataSource.getAppliedPromo(testnet)
+ }
+
+ suspend fun setAppliedPromo(
+ testnet: Boolean,
+ promo: String,
+ ) = withContext(Dispatchers.IO) {
+ localDataSource.setAppliedPromo(testnet, promo)
+ }
+
+}
\ No newline at end of file
diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/Module.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/Module.kt
new file mode 100644
index 000000000..21bd0c1af
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/Module.kt
@@ -0,0 +1,7 @@
+package com.tonapps.wallet.data.battery
+
+import org.koin.dsl.module
+
+val batteryModule = module {
+ single { BatteryRepository(get(), get(), get()) }
+}
\ No newline at end of file
diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryBalanceEntity.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryBalanceEntity.kt
new file mode 100644
index 000000000..83ed52c71
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryBalanceEntity.kt
@@ -0,0 +1,19 @@
+package com.tonapps.wallet.data.battery.entity
+
+import android.os.Parcelable
+import com.tonapps.icu.Coins
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class BatteryBalanceEntity(
+ val balance: Coins,
+ val reservedBalance: Coins,
+) : Parcelable {
+
+ companion object {
+ val Empty = BatteryBalanceEntity(
+ balance = Coins.ZERO,
+ reservedBalance = Coins.ZERO
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..d40b23c18
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/BatteryConfigEntity.kt
@@ -0,0 +1,27 @@
+package com.tonapps.wallet.data.battery.entity
+
+import android.os.Parcelable
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import org.ton.block.AddrStd
+
+@Parcelize
+data class BatteryConfigEntity(
+ val excessesAccount: String?,
+ val fundReceiver: String?,
+ val rechargeMethods: List,
+) : Parcelable {
+
+ @IgnoredOnParcel
+ val excessesAddress: AddrStd? by lazy {
+ excessesAccount?.let { AddrStd(it) }
+ }
+
+ companion object {
+ val Empty = BatteryConfigEntity(
+ excessesAccount = null,
+ fundReceiver = null,
+ rechargeMethods = emptyList()
+ )
+ }
+}
\ 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
new file mode 100644
index 000000000..6ceb30dcb
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodEntity.kt
@@ -0,0 +1,51 @@
+package com.tonapps.wallet.data.battery.entity
+
+import android.os.Parcelable
+import com.tonapps.icu.Coins
+import io.batteryapi.models.RechargeMethodsMethodsInner
+import kotlinx.parcelize.Parcelize
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+@Parcelize
+data class RechargeMethodEntity(
+ val type: RechargeMethodType,
+ val rate: String,
+ val symbol: String,
+ val decimals: Int,
+ val supportGasless: Boolean,
+ val supportRecharge: Boolean,
+ val image: String? = null,
+ val jettonMaster: String? = null,
+ val minBootstrapValue: String? = null
+) : Parcelable {
+
+ private companion object {
+
+ private fun RechargeMethodsMethodsInner.Type.toRechargeMethodType(): RechargeMethodType {
+ return when (this) {
+ RechargeMethodsMethodsInner.Type.jetton -> RechargeMethodType.JETTON
+ RechargeMethodsMethodsInner.Type.ton -> RechargeMethodType.TON
+ }
+ }
+ }
+
+ constructor(method: RechargeMethodsMethodsInner) : this(
+ type = method.type.toRechargeMethodType(),
+ rate = method.rate,
+ symbol = method.symbol,
+ decimals = method.decimals,
+ supportGasless = method.supportGasless,
+ supportRecharge = method.supportRecharge,
+ image = method.image,
+ jettonMaster = method.jettonMaster,
+ minBootstrapValue = method.minBootstrapValue
+ )
+
+
+ fun fromTon(amount: BigDecimal): Coins {
+ return Coins.of(amount.divide(rate.toBigDecimal(), decimals, RoundingMode.HALF_UP))
+ }
+
+ fun fromTon(amount: String) = fromTon(amount.toBigDecimal())
+}
\ No newline at end of file
diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodType.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodType.kt
new file mode 100644
index 000000000..b828130b0
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/entity/RechargeMethodType.kt
@@ -0,0 +1,6 @@
+package com.tonapps.wallet.data.battery.entity
+
+enum class RechargeMethodType {
+ TON,
+ JETTON,
+}
\ No newline at end of file
diff --git a/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/LocalDataSource.kt b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/LocalDataSource.kt
new file mode 100644
index 000000000..f0a3c2698
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/LocalDataSource.kt
@@ -0,0 +1,65 @@
+package com.tonapps.wallet.data.battery.source
+
+import android.content.Context
+import androidx.core.content.edit
+import com.tonapps.blockchain.ton.extensions.hex
+import com.tonapps.security.Security
+import com.tonapps.wallet.data.battery.entity.BatteryBalanceEntity
+import com.tonapps.wallet.data.battery.entity.BatteryConfigEntity
+import com.tonapps.wallet.data.core.BlobDataSource
+import org.ton.api.pub.PublicKeyEd25519
+
+internal class LocalDataSource(
+ context: Context
+) {
+
+ companion object {
+ private const val NAME = "battery"
+ private const val KEY_ALIAS = "_com_tonapps_battery_master_key_"
+ }
+
+ private val balance = BlobDataSource.simple(context, "battery_balance")
+ private val configStore = BlobDataSource.simple(context, "battery_config")
+
+ private val encryptedPrefs = Security.pref(context, KEY_ALIAS, NAME)
+
+ fun setConfig(testnet: Boolean, entity: BatteryConfigEntity) {
+ configStore.setCache(configCacheKey(testnet), entity)
+ }
+
+ fun getConfig(testnet: Boolean): BatteryConfigEntity? {
+ return configStore.getCache(configCacheKey(testnet))
+ }
+
+ private fun configCacheKey(testnet: Boolean): String {
+ return if (testnet) "testnet" else "mainnet"
+ }
+
+ fun setBalance(publicKey: PublicKeyEd25519, testnet: Boolean, entity: BatteryBalanceEntity) {
+ balance.setCache(balanceCacheKey(publicKey, testnet), entity)
+ }
+
+ fun getBalance(publicKey: PublicKeyEd25519, testnet: Boolean): BatteryBalanceEntity? {
+ return balance.getCache(balanceCacheKey(publicKey, testnet))
+ }
+
+ private fun balanceCacheKey(publicKey: PublicKeyEd25519, testnet: Boolean): String {
+ val prefix = if (testnet) "testnet" else "mainnet"
+ return "$prefix:${publicKey.hex()}"
+ }
+
+ fun getAppliedPromo(testnet: Boolean): String? {
+ return encryptedPrefs.getString(promoKey(testnet), null)
+ }
+
+ fun setAppliedPromo(testnet: Boolean, promo: String) {
+ encryptedPrefs.edit {
+ putString(promoKey(testnet), promo)
+ }
+ }
+
+ private fun promoKey(testnet: Boolean): String {
+ return "promo_${if (testnet) "testnet" else "mainnet"}"
+ }
+}
+
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
new file mode 100644
index 000000000..1f384a678
--- /dev/null
+++ b/apps/wallet/data/battery/src/main/java/com/tonapps/wallet/data/battery/source/RemoteDataSource.kt
@@ -0,0 +1,44 @@
+package com.tonapps.wallet.data.battery.source
+
+import com.tonapps.icu.Coins
+import com.tonapps.wallet.api.API
+import com.tonapps.wallet.data.battery.entity.BatteryBalanceEntity
+import com.tonapps.wallet.data.battery.entity.BatteryConfigEntity
+import com.tonapps.wallet.data.battery.entity.RechargeMethodEntity
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+
+internal class RemoteDataSource(
+ private val api: API
+) {
+
+ suspend fun fetchBalance(
+ tonProofToken: String,
+ testnet: Boolean
+ ): BatteryBalanceEntity? = withContext(Dispatchers.IO) {
+ val response = api.getBatteryBalance(tonProofToken, testnet) ?: return@withContext null
+
+ BatteryBalanceEntity(
+ balance = Coins.of(response.balance.toBigDecimal(), 20),
+ reservedBalance = Coins.of(response.reserved.toBigDecimal(), 20)
+ )
+ }
+
+ suspend fun fetchConfig(
+ testnet: Boolean
+ ): BatteryConfigEntity? = withContext(Dispatchers.IO) {
+ val configDeferred = async { api.getBatteryConfig(testnet) }
+ val rechargeMethodsDeferred = async { api.getBatteryRechargeMethods(testnet) }
+
+ val config = configDeferred.await() ?: return@withContext null
+ val rechargeMethods = rechargeMethodsDeferred.await() ?: return@withContext null
+
+ BatteryConfigEntity(
+ excessesAccount = config.excessAccount,
+ fundReceiver = config.fundReceiver,
+ rechargeMethods = rechargeMethods.methods.map(::RechargeMethodEntity)
+ )
+ }
+
+}
\ No newline at end of file
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 5a31185e9..0a13e9a5b 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
@@ -1,9 +1,12 @@
package com.tonapps.wallet.data.core
import android.content.Context
+import android.os.Parcelable
import android.util.Log
import com.tonapps.extensions.cacheFolder
import com.tonapps.extensions.file
+import com.tonapps.extensions.toByteArray
+import com.tonapps.extensions.toParcel
import java.io.File
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
@@ -15,6 +18,19 @@ abstract class BlobDataSource(
private val timeout: Long = TimeUnit.DAYS.toMillis(90)
) {
+ companion object {
+
+ inline fun simple(
+ context: Context,
+ path: String
+ ): BlobDataSource {
+ return object : BlobDataSource(context, path) {
+ override fun onMarshall(data: T) = data.toByteArray()
+ override fun onUnmarshall(bytes: ByteArray) = bytes.toParcel()
+ }
+ }
+ }
+
private val diskFolder = context.cacheFolder(path)
fun getCache(key: String): D? {
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 443a5aa16..eaf20336d 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
@@ -8,10 +8,14 @@ import kotlinx.parcelize.Parcelize
import org.json.JSONObject
import org.ton.block.AddrStd
import org.ton.block.Coins
+import org.ton.block.MsgAddressInt
import org.ton.block.StateInit
import org.ton.cell.Cell
+import org.ton.cell.CellBuilder
import org.ton.contract.wallet.WalletTransfer
import org.ton.contract.wallet.WalletTransferBuilder
+import org.ton.tlb.loadTlb
+import org.ton.tlb.storeTlb
@Parcelize
data class RawMessageEntity(
@@ -33,14 +37,46 @@ data class RawMessageEntity(
val payload: Cell
get() = payloadValue.safeParseCell() ?: Cell()
- val walletTransfer: WalletTransfer by lazy {
+ fun getWalletTransfer(excessesAddress: AddrStd? = null): WalletTransfer {
val builder = WalletTransferBuilder()
builder.stateInit = stateInit
builder.destination = address
- builder.body = payload
+ builder.body = if (excessesAddress != null) rebuildBodyWithCustomExcessesAccount(payload, excessesAddress) else payload
// builder.bounceable = address.isBounceable()
builder.coins = coins
- builder.build()
+ return builder.build()
+ }
+
+ private fun rebuildBodyWithCustomExcessesAccount(body: Cell, excessesAddress: AddrStd): Cell {
+ val slice = body.beginParse()
+ val opCode = slice.loadUInt32()
+ var builder = CellBuilder.beginCell()
+ return when (opCode.toInt()) {
+ // stonfi swap
+ 0x25938561 -> {
+ builder
+ .storeUInt(0x25938561, 32)
+ .storeTlb(MsgAddressInt, slice.loadTlb(AddrStd.tlbCodec()))
+ .storeTlb(Coins, slice.loadTlb(Coins.tlbCodec()))
+ .storeTlb(MsgAddressInt, slice.loadTlb(AddrStd.tlbCodec()))
+
+ if (slice.loadBit()) {
+ slice.loadTlb(AddrStd.tlbCodec())
+ }
+ slice.endParse()
+
+ builder
+ .storeBit(true)
+ .storeTlb(MsgAddressInt, excessesAddress)
+
+ builder.endCell()
+ }
+ // nft transfer
+ 0x5fcc3d14 -> body
+ // jetton transfer
+ 0xf8a7ea5 -> body
+ else -> body
+ }
}
constructor(json: JSONObject) : this(
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 f006b8535..4d3da0ec6 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
@@ -21,7 +21,7 @@ data class SignRequestEntity(
val from: AddrStd?
get() = fromValue?.let { AddrStd.parse(it) }
- val transfers = messages.map { it.walletTransfer }
+ val transfers = messages.map { it.getWalletTransfer() }
constructor(json: JSONObject) : this(
fromValue = parseFrom(json),
diff --git a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/BatteryTransaction.kt b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/BatteryTransaction.kt
new file mode 100644
index 000000000..64c42f2e0
--- /dev/null
+++ b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/BatteryTransaction.kt
@@ -0,0 +1,23 @@
+package com.tonapps.wallet.data.settings
+
+@JvmInline
+value class BatteryTransaction(val code: Int) {
+
+ companion object {
+ val SWAP = BatteryTransaction(0)
+ val JETTON = BatteryTransaction(1)
+ val NFT = BatteryTransaction(2)
+
+ val entries = arrayOf(SWAP, JETTON, NFT)
+
+ fun Array.toIntArray(): IntArray {
+ return map { it.code }.toIntArray()
+ }
+
+ fun List.toIntArray(): IntArray {
+ return map { it.code }.toIntArray()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt
index ded651212..2ad0cbc54 100644
--- a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt
+++ b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt
@@ -52,6 +52,7 @@ class SettingsRepository(
private const val INSTALL_ID_KEY = "install_id"
private const val SEARCH_ENGINE_KEY = "search_engine"
private const val ENCRYPTED_COMMENT_MODAL_KEY = "encrypted_comment_modal"
+ private const val BATTERY_VIEWED_KEY = "battery_viewed"
}
private val _currencyFlow = MutableEffectFlow()
@@ -199,6 +200,14 @@ class SettingsRepository(
}
}
+ var batteryViewed: Boolean = prefs.getBoolean(BATTERY_VIEWED_KEY, false)
+ set(value) {
+ if (value != field) {
+ prefs.edit().putBoolean(BATTERY_VIEWED_KEY, value).apply()
+ field = value
+ }
+ }
+
fun getSpamStateTransaction(walletId: String, id: String) = walletPrefsFolder.getSpamStateTransaction(walletId, id)
fun isSpamTransaction(walletId: String, id: String) = getSpamStateTransaction(walletId, id) == SpamTransactionState.SPAM
@@ -233,6 +242,31 @@ class SettingsRepository(
rnLegacy.setSetupLastBackupAt(walletId, date)
}
+ fun getBatteryTxEnabled(accountId: String) = walletPrefsFolder.getBatteryTxEnabled(accountId)
+
+ fun setBatteryTxEnabled(accountId: String, types: Array) {
+ walletPrefsFolder.setBatteryTxEnabled(accountId, types)
+ }
+
+ fun batteryEnableTx(
+ accountId: String,
+ type: BatteryTransaction,
+ enable: Boolean
+ ): Array {
+ val types = getBatteryTxEnabled(accountId).toMutableSet()
+ if (enable && !types.contains(type)) {
+ types.add(type)
+ } else if (!enable && !types.remove(type)) {
+ return types.toTypedArray()
+ }
+ setBatteryTxEnabled(accountId, types.toTypedArray())
+ return types.toTypedArray()
+ }
+
+ fun batteryIsEnabledTx(accountId: String, type: BatteryTransaction): Boolean {
+ return getBatteryTxEnabled(accountId).contains(type)
+ }
+
fun getLocale(): Locale {
if (language.code == Language.DEFAULT) {
return context.locale
diff --git a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/BaseSettingsFolder.kt b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/BaseSettingsFolder.kt
index fc71e2712..a5eec3e68 100644
--- a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/BaseSettingsFolder.kt
+++ b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/BaseSettingsFolder.kt
@@ -5,6 +5,8 @@ import android.content.SharedPreferences
import android.util.Log
import com.tonapps.extensions.MutableEffectFlow
import com.tonapps.extensions.getByteArray
+import com.tonapps.extensions.getIntArray
+import com.tonapps.extensions.putIntArray
import com.tonapps.extensions.state
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -43,6 +45,15 @@ internal abstract class BaseSettingsFolder(
fun getLong(key: String, defValue: Long = 0) = prefs.getLong(key, defValue)
+ fun getIntArray(key: String, def: IntArray? = null) = prefs.getIntArray(key) ?: def
+
+ fun putIntArray(key: String, value: IntArray, notify: Boolean = true) {
+ prefs.putIntArray(key, value)
+ if (notify) {
+ notifyChanged()
+ }
+ }
+
fun putLong(key: String, value: Long, notify: Boolean = true) {
prefs.edit().putLong(key, value).apply()
if (notify) {
diff --git a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/WalletPrefsFolder.kt b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/WalletPrefsFolder.kt
index a25968979..9786bac7f 100644
--- a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/WalletPrefsFolder.kt
+++ b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/folder/WalletPrefsFolder.kt
@@ -3,6 +3,8 @@ package com.tonapps.wallet.data.settings.folder
import android.content.Context
import android.os.SystemClock
import android.util.Log
+import com.tonapps.wallet.data.settings.BatteryTransaction
+import com.tonapps.wallet.data.settings.BatteryTransaction.Companion.toIntArray
import com.tonapps.wallet.data.settings.SettingsRepository
import com.tonapps.wallet.data.settings.SpamTransactionState
import com.tonapps.wallet.data.settings.entities.WalletPrefsEntity
@@ -18,6 +20,18 @@ internal class WalletPrefsFolder(context: Context, scope: CoroutineScope): BaseS
private const val LAST_UPDATED_PREFIX = "last_updated_"
private const val TELEGRAM_CHANNEL_PREFIX = "telegram_channel_"
private const val SPAM_STATE_TRANSACTION_PREFIX = "spam_state_transaction_"
+ private const val BATTERY_TX_ENABLED_PREFIX = "batter_tx_enabled_"
+ }
+
+ fun getBatteryTxEnabled(accountId: String): Array {
+ val key = keyBatteryTxEnabled(accountId)
+ val value = getIntArray(key, BatteryTransaction.entries.toIntArray()) ?: return emptyArray()
+ return value.map { BatteryTransaction(it) }.toTypedArray()
+ }
+
+ fun setBatteryTxEnabled(accountId: String, types: Array) {
+ val key = keyBatteryTxEnabled(accountId)
+ putIntArray(key, types.distinct().toIntArray())
}
fun getSpamStateTransaction(
@@ -94,6 +108,10 @@ internal class WalletPrefsFolder(context: Context, scope: CoroutineScope): BaseS
}
}
+ private fun keyBatteryTxEnabled(accountId: String): String {
+ return key(BATTERY_TX_ENABLED_PREFIX, accountId)
+ }
+
private fun keySpamStateTransaction(walletId: String, id: String): String {
val key = key(SPAM_STATE_TRANSACTION_PREFIX, walletId)
return "$key$id"
diff --git a/apps/wallet/data/staking/src/main/java/com/tonapps/wallet/data/staking/StakingRepository.kt b/apps/wallet/data/staking/src/main/java/com/tonapps/wallet/data/staking/StakingRepository.kt
index 61d384b99..86217deff 100644
--- a/apps/wallet/data/staking/src/main/java/com/tonapps/wallet/data/staking/StakingRepository.kt
+++ b/apps/wallet/data/staking/src/main/java/com/tonapps/wallet/data/staking/StakingRepository.kt
@@ -37,5 +37,4 @@ class StakingRepository(context: Context, api: API) {
}
return "${accountId}_testnet_2"
}
-
}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/build.gradle.kts b/apps/wallet/instance/app/build.gradle.kts
index 9cf087991..198a52037 100644
--- a/apps/wallet/instance/app/build.gradle.kts
+++ b/apps/wallet/instance/app/build.gradle.kts
@@ -50,6 +50,7 @@ dependencies {
implementation(project(Dependence.Wallet.Data.passcode))
implementation(project(Dependence.Wallet.Data.staking))
implementation(project(Dependence.Wallet.Data.purchase))
+ implementation(project(Dependence.Wallet.Data.battery))
implementation(project(Dependence.UIKit.core))
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt
index 82ac96055..b3667282d 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt
@@ -19,6 +19,7 @@ import com.tonapps.wallet.data.token.tokenModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import com.tonapps.wallet.data.backup.backupModule
+import com.tonapps.wallet.data.battery.batteryModule
import com.tonapps.wallet.data.browser.browserModule
import com.tonapps.wallet.data.collectibles.collectiblesModule
import com.tonapps.wallet.data.core.Theme
@@ -49,7 +50,7 @@ class App: Application(), CameraXConfig.Provider, KoinComponent {
startKoin {
androidContext(this@App)
- modules(koinModel, purchaseModule, stakingModule, passcodeModule, rnLegacyModule, backupModule, dataModule, browserModule, pushModule, tonConnectModule, apiModule, accountModule, ratesModule, tokenModule, eventsModule, collectiblesModule)
+ modules(koinModel, purchaseModule, batteryModule, stakingModule, passcodeModule, rnLegacyModule, backupModule, dataModule, browserModule, pushModule, tonConnectModule, apiModule, accountModule, ratesModule, tokenModule, eventsModule, collectiblesModule)
}
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/Extensions.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/Extensions.kt
index 113aff793..18cd5ac80 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/Extensions.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/Extensions.kt
@@ -111,10 +111,7 @@ val JettonSwapAction.ton: Long
fun AccountAddress.getNameOrAddress(testnet: Boolean): String {
if (!name.isNullOrBlank()) {
val accountName = name!!.ifPunycodeToUnicode()
- if (accountName.endsWith(".ton")) {
- return accountName.short6
- }
- return accountName.max12
+ return accountName
}
return address.toUserFriendly(
wallet = isWallet,
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/billing/BillingManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/billing/BillingManager.kt
new file mode 100644
index 000000000..e0a5c9b02
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/billing/BillingManager.kt
@@ -0,0 +1,21 @@
+package com.tonapps.tonkeeper.billing
+
+import com.android.billingclient.api.Purchase
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+class BillingManager {
+
+ // Example
+ private val _purchasesFlow = MutableSharedFlow>()
+ val purchasesFlow = _purchasesFlow.asSharedFlow()
+
+ private val purchasesUpdatedListener = { purchases: List ->
+ // _purchasesFlow.value = purchases
+ }
+
+
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/entities/TransferEntity.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/entities/TransferEntity.kt
index dddfdafb1..fe1c01f93 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/entities/TransferEntity.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/entities/TransferEntity.kt
@@ -75,7 +75,8 @@ data class TransferEntity(
} else if (!commentEncrypted) {
return MessageData.text(comment).body
} else {
- privateKey ?: throw IllegalArgumentException("Private key required for encrypted comment")
+ privateKey
+ ?: throw IllegalArgumentException("Private key required for encrypted comment")
val publicKey = privateKey.publicKey()
return CommentEncryption.encryptComment(
@@ -88,10 +89,13 @@ data class TransferEntity(
}
}
- private fun getWalletTransfer(privateKey: PrivateKeyEd25519?): WalletTransfer {
+ private fun getWalletTransfer(
+ privateKey: PrivateKeyEd25519?,
+ excessesAddress: AddrStd
+ ): WalletTransfer {
val builder = WalletTransferBuilder()
builder.bounceable = bounceable
- builder.body = body(privateKey)
+ builder.body = body(privateKey, excessesAddress)
builder.sendMode = sendMode
if (isNft) {
builder.coins = coins
@@ -107,15 +111,23 @@ data class TransferEntity(
return builder.build()
}
- private fun getGifts(privateKey: PrivateKeyEd25519?): Array {
- return arrayOf(getWalletTransfer(privateKey))
+ private fun getGifts(
+ privateKey: PrivateKeyEd25519?,
+ excessesAddress: AddrStd
+ ): Array {
+ return arrayOf(getWalletTransfer(privateKey, excessesAddress))
}
- fun getUnsignedBody(privateKey: PrivateKeyEd25519? = null): Cell {
+ fun getUnsignedBody(
+ privateKey: PrivateKeyEd25519? = null,
+ internalMessage: Boolean = false,
+ excessesAddress: AddrStd? = null
+ ): Cell {
return contract.createTransferUnsignedBody(
validUntil = validUntil,
seqno = seqno,
- gifts = getGifts(privateKey)
+ gifts = getGifts(privateKey, excessesAddress = excessesAddress ?: contract.address),
+ internalMessage = internalMessage ?: false,
)
}
@@ -185,40 +197,48 @@ data class TransferEntity(
return contract.createTransferMessageCell(contract.address, seqno, signedBody)
}
- private fun body(privateKey: PrivateKeyEd25519?): Cell? {
+ private fun body(privateKey: PrivateKeyEd25519?, excessesAddress: AddrStd): Cell? {
if (isNft) {
- return nftBody(privateKey)
+ return nftBody(privateKey, excessesAddress)
} else if (!isTon) {
- return jettonBody(privateKey)
+ return jettonBody(privateKey, excessesAddress)
}
return getCommentForwardPayload(privateKey)
}
- private fun jettonBody(privateKey: PrivateKeyEd25519?): Cell {
+ private fun jettonBody(privateKey: PrivateKeyEd25519?, excessesAddress: AddrStd): Cell {
return TonTransferHelper.jetton(
coins = coins,
toAddress = destination,
- responseAddress = contract.address,
+ responseAddress = excessesAddress,
queryId = queryId,
body = getCommentForwardPayload(privateKey),
)
}
- private fun nftBody(privateKey: PrivateKeyEd25519?): Cell {
+ private fun nftBody(privateKey: PrivateKeyEd25519?, excessesAddress: AddrStd): Cell {
return TonTransferHelper.nft(
newOwnerAddress = destination,
- excessesAddress = contract.address,
+ excessesAddress = excessesAddress,
queryId = queryId,
body = getCommentForwardPayload(privateKey),
)
}
- fun toSignedMessage(privateKeyEd25519: PrivateKeyEd25519): Cell {
+ fun toSignedMessage(
+ privateKeyEd25519: PrivateKeyEd25519,
+ isBattery: Boolean,
+ excessesAddress: AddrStd? = null
+ ): Cell {
return contract.createTransferMessageCell(
address = contract.address,
privateKey = privateKeyEd25519,
seqno = seqno,
- unsignedBody = getUnsignedBody(privateKeyEd25519),
+ unsignedBody = getUnsignedBody(
+ privateKeyEd25519,
+ internalMessage = isBattery,
+ excessesAddress = excessesAddress,
+ ),
)
}
@@ -264,8 +284,10 @@ data class TransferEntity(
fun build(): TransferEntity {
val token = token ?: throw IllegalArgumentException("Token is not set")
- val destination = destination ?: throw IllegalArgumentException("Destination is not set")
- val destinationPK = destinationPK ?: throw IllegalArgumentException("DestinationPK is not set")
+ val destination =
+ destination ?: throw IllegalArgumentException("Destination is not set")
+ val destinationPK =
+ destinationPK ?: throw IllegalArgumentException("DestinationPK is not set")
val amount = amount ?: throw IllegalArgumentException("Amount is not set")
val seqno = seqno ?: throw IllegalArgumentException("Seqno is not set")
val validUntil = validUntil ?: throw IllegalArgumentException("ValidUntil is not set")
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/ActionType.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/ActionType.kt
index 001650c9e..4f8d2b653 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/ActionType.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/ActionType.kt
@@ -19,4 +19,5 @@ enum class ActionType {
JettonBurn,
UnSubscribe,
Subscribe,
+ Fee,
}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/Extensions.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/Extensions.kt
index 51f708747..9b944750b 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/Extensions.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/Extensions.kt
@@ -22,6 +22,7 @@ val ActionType.iconRes: Int
ActionType.JettonBurn -> R.drawable.ic_fire_28
ActionType.UnSubscribe -> R.drawable.ic_xmark_28
ActionType.Subscribe -> R.drawable.ic_bell_28
+ ActionType.Fee -> R.drawable.ic_ton_28
}
@@ -43,6 +44,7 @@ val ActionType.nameRes: Int
ActionType.JettonBurn -> Localization.burned
ActionType.UnSubscribe -> Localization.unsubscribed
ActionType.Subscribe -> Localization.subscribed
+ ActionType.Fee -> Localization.network_fee
}
val Action.recipient: AccountAddress?
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt
index 6b9b684c4..f0b09e315 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt
@@ -177,15 +177,35 @@ class HistoryHelper(
wallet: WalletEntity,
response: MessageConsequences,
rates: RatesEntity,
+ isBattery: Boolean = false,
): Details {
- val items = mapping(wallet, response.event, true)
+ val items = mapping(wallet, response.event, true, positionExtra = 1).toMutableList()
+
val fee = Coins.of(response.totalFees)
val feeFormat = CurrencyFormatter.format("TON", fee)
val feeFiat = rates.convert("TON", fee)
val feeFiatFormat = CurrencyFormatter.formatFiat(rates.currency.code, feeFiat)
+
+ items.add(
+ HistoryItem.Event(
+ index = items.lastIndex + 1,
+ position = ListCell.Position.LAST,
+ txId = "fee",
+ iconURL = "",
+ action = ActionType.Fee,
+ title = "",
+ subtitle = if (isBattery) context.getString(Localization.will_be_paid_with_battery) else "",
+ value = feeFormat,
+ date = feeFiatFormat.toString(),
+ isOut = true,
+ failed = false,
+ isScam = false,
+ )
+ )
+
return Details(
accountId = wallet.accountId,
- items = items,
+ items = items.toList(),
fee = fee,
feeFormat = feeFormat,
feeFiat = feeFiat,
@@ -218,9 +238,10 @@ class HistoryHelper(
wallet: WalletEntity,
event: AccountEvent,
removeDate: Boolean = false,
- hiddenBalances: Boolean = false
+ hiddenBalances: Boolean = false,
+ positionExtra: Int = 0,
): List {
- return mapping(wallet, listOf(event), removeDate, hiddenBalances)
+ return mapping(wallet, listOf(event), removeDate, hiddenBalances, positionExtra)
}
suspend fun getEvent(
@@ -237,7 +258,8 @@ class HistoryHelper(
wallet: WalletEntity,
events: List,
removeDate: Boolean = false,
- hiddenBalances: Boolean = false
+ hiddenBalances: Boolean = false,
+ positionExtra: Int = 0,
): List = withContext(Dispatchers.IO) {
val items = mutableListOf()
@@ -266,7 +288,7 @@ class HistoryHelper(
)
chunkItems.add(item.copy(
pending = pending,
- position = ListCell.getPosition(actions.size, actionIndex),
+ position = ListCell.getPosition(actions.size + positionExtra, actionIndex),
fee = CurrencyFormatter.format(TokenEntity.TON.symbol, fee, TokenEntity.TON.decimals),
feeInCurrency = CurrencyFormatter.formatFiat(currency.code, feeInCurrency),
lt = event.lt,
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/Extension.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/Extension.kt
index ea31e5b44..1050fbd0c 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/Extension.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/Extension.kt
@@ -1,6 +1,9 @@
package com.tonapps.tonkeeper.koin
import android.content.Context
+import androidx.annotation.MainThread
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModel
import androidx.recyclerview.widget.RecyclerView
import com.tonapps.tonkeeper.core.history.HistoryHelper
import com.tonapps.wallet.api.API
@@ -10,6 +13,7 @@ import com.tonapps.wallet.data.passcode.PasscodeManager
import com.tonapps.wallet.data.rn.RNLegacy
import com.tonapps.wallet.data.settings.SettingsRepository
import com.tonapps.wallet.data.tonconnect.TonConnectRepository
+import org.koin.androidx.viewmodel.ext.android.getViewModel
import org.koin.core.Koin
import org.koin.core.component.KoinComponent
import org.koin.core.definition.Definition
@@ -22,6 +26,11 @@ inline fun > Module.uiAdapter(
return single(definition = definition)
}
+@MainThread
+inline fun Fragment.parentFragmentViewModel(): Lazy {
+ return lazy { requireParentFragment().getViewModel() }
+}
+
val Context.koin: Koin?
get() = (applicationContext as? KoinComponent)?.getKoin()
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt
index 930315a8b..cc4274d00 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt
@@ -1,6 +1,7 @@
package com.tonapps.tonkeeper.koin
import com.tonapps.network.NetworkMonitor
+import com.tonapps.tonkeeper.billing.BillingManager
import com.tonapps.tonkeeper.core.history.HistoryHelper
import com.tonapps.tonkeeper.ui.screen.main.MainViewModel
import com.tonapps.tonkeeper.ui.screen.root.RootViewModel
@@ -10,6 +11,10 @@ import com.tonapps.tonkeeper.ui.screen.action.ActionViewModel
import com.tonapps.tonkeeper.ui.screen.add.imprt.ImportWalletViewModel
import com.tonapps.tonkeeper.ui.screen.backup.main.BackupViewModel
import com.tonapps.tonkeeper.ui.screen.backup.check.BackupCheckViewModel
+import com.tonapps.tonkeeper.ui.screen.battery.BatteryViewModel
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.BatteryRechargeViewModel
+import com.tonapps.tonkeeper.ui.screen.battery.refill.BatteryRefillViewModel
+import com.tonapps.tonkeeper.ui.screen.battery.settings.BatterySettingsViewModel
import com.tonapps.tonkeeper.ui.screen.browser.connected.BrowserConnectedViewModel
import com.tonapps.tonkeeper.ui.screen.browser.dapp.DAppViewModel
import com.tonapps.tonkeeper.ui.screen.browser.explore.BrowserExploreViewModel
@@ -53,6 +58,7 @@ import kotlinx.coroutines.SupervisorJob
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val koinModel = module {
@@ -60,8 +66,9 @@ val koinModel = module {
single(createdAtStart = true) { CoroutineScope(Dispatchers.IO + SupervisorJob()) }
single { SettingsRepository(get(), get(), get()) }
single { NetworkMonitor(get(), get()) }
- single { SignManager(get(), get(), get(), get(), get()) }
+ single { SignManager(get(), get(), get(), get(), get(), get()) }
single { HistoryHelper(get(), get(), get(), get(), get(), get(), get()) }
+ singleOf(::BillingManager)
factory { (viewModel: com.tonapps.tonkeeper.ui.base.BaseWalletVM) ->
// TODO
@@ -75,7 +82,7 @@ val koinModel = module {
viewModel { MainViewModel(androidApplication(), get(), get()) }
viewModel { RootViewModel(androidApplication(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
viewModel { PickerViewModel(androidApplication(), get(), get()) }
- viewModel { WalletViewModel(androidApplication(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
+ viewModel { WalletViewModel(androidApplication(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
viewModel { CurrencyViewModel(androidApplication(), get()) }
viewModel { SettingsViewModel(androidApplication(), get(), get(), get(), get(), get(), get()) }
viewModel { EditNameViewModel(androidApplication(), get()) }
@@ -85,7 +92,7 @@ val koinModel = module {
viewModel { EventsViewModel(androidApplication(), get(), get(), get(), get(), get(), get(), get(), get()) }
viewModel { parameters -> TCAuthViewModel(androidApplication(), request = parameters.get(), get(), get(), get()) }
viewModel { CollectiblesViewModel(androidApplication(), get(), get(), get(), get()) }
- viewModel { parameters -> ActionViewModel(androidApplication(), args = parameters.get(), get(), get(), get()) }
+ viewModel { parameters -> ActionViewModel(androidApplication(), args = parameters.get(), get(), get(), get(), get()) }
viewModel { BrowserExploreViewModel(androidApplication(), get(), get(), get(), get()) }
viewModel { BrowserConnectedViewModel(androidApplication(), get(), get(), get()) }
viewModel { BrowserMainViewModel(androidApplication(), get()) }
@@ -97,7 +104,7 @@ val koinModel = module {
viewModel { BackupViewModel(androidApplication(), get(), get(), get(), get()) }
viewModel { BackupCheckViewModel(androidApplication(), get(), get()) }
viewModel { TokensManageViewModel(androidApplication(), get(), get(), get()) }
- viewModel { parameters -> SendViewModel(androidApplication(), nftAddress = parameters.get(), get(), get(), get(), get(), get(), get(), get()) }
+ viewModel { parameters -> SendViewModel(androidApplication(), nftAddress = parameters.get(), get(), get(), get(), get(), get(), get(), get(), get()) }
viewModel { TokenPickerViewModel(androidApplication(), get(), get(), get()) }
viewModel { CountryPickerViewModel(androidApplication(), get(), get()) }
viewModel { parameters -> StakingViewModel(androidApplication(), address = parameters.get(), get(), get(), get(), get(), get(), get(), get()) }
@@ -110,4 +117,8 @@ val koinModel = module {
viewModel { SendContactsViewModel(androidApplication(), get(), get(), get()) }
viewModel { NotificationsEnableViewModel(get(), get()) }
viewModel { ImportWalletViewModel(androidApplication(), get()) }
+ viewModel { BatteryViewModel(androidApplication()) }
+ viewModel { BatterySettingsViewModel(androidApplication(), get(), get(), get(), get()) }
+ viewModel { BatteryRefillViewModel(androidApplication(), get(), get(), get(), get(), get(), get()) }
+ viewModel { parameters -> BatteryRechargeViewModel(androidApplication(), args = parameters.get(), get(), get(), get(), get(), get(), get()) }
}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/sign/SignManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/sign/SignManager.kt
index 159840112..a2fcf1e8b 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/sign/SignManager.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/sign/SignManager.kt
@@ -11,9 +11,11 @@ import com.tonapps.tonkeeper.ui.screen.action.ActionScreen
import com.tonapps.wallet.api.API
import com.tonapps.wallet.data.account.entities.WalletEntity
import com.tonapps.wallet.data.account.AccountRepository
+import com.tonapps.wallet.data.battery.BatteryRepository
import com.tonapps.wallet.data.core.WalletCurrency
import com.tonapps.wallet.data.core.entity.SignRequestEntity
import com.tonapps.wallet.data.rates.RatesRepository
+import com.tonapps.wallet.data.settings.BatteryTransaction
import com.tonapps.wallet.data.settings.SettingsRepository
import com.tonapps.wallet.localization.Localization
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -26,7 +28,8 @@ class SignManager(
private val settingsRepository: SettingsRepository,
private val accountRepository: AccountRepository,
private val api: API,
- private val historyHelper: HistoryHelper
+ private val historyHelper: HistoryHelper,
+ private val batteryRepository: BatteryRepository,
) {
suspend fun action(
@@ -34,9 +37,19 @@ class SignManager(
wallet: WalletEntity,
request: SignRequestEntity,
canceller: CancellationSignal = CancellationSignal(),
+ batteryTxType: BatteryTransaction? = null,
+ forceRelayer: Boolean = false,
): String {
navigation.toastLoading(true)
- val details = emulate(request, wallet)
+ var isBattery = batteryTxType != null && settingsRepository.batteryIsEnabledTx(wallet.accountId, batteryTxType)
+ val details: HistoryHelper.Details?
+ if (isBattery || forceRelayer) {
+ val result = emulateBattery(request, wallet, forceRelayer = forceRelayer)
+ details = result.first
+ isBattery = result.second
+ } else {
+ details = emulate(request, wallet)
+ }
navigation.toastLoading(false)
if (details == null) {
@@ -44,10 +57,18 @@ class SignManager(
throw IllegalArgumentException("Failed to emulate")
}
- val boc = getBoc(navigation, wallet, request, details, canceller) ?: throw IllegalArgumentException("Failed boc")
+ val boc = getBoc(navigation, wallet, request, details, canceller, isBattery) ?: throw IllegalArgumentException("Failed boc")
AnalyticsHelper.trackEvent("send_transaction")
- if (api.sendToBlockchain(boc, wallet.testnet)) {
+ val success = if (isBattery) {
+ val tonProofToken = accountRepository.requestTonProofToken(wallet) ?: throw IllegalStateException("Can't find TonProof token")
+ api.sendToBlockchainWithBattery(boc, tonProofToken, wallet.testnet)
+ } else {
+ api.sendToBlockchain(boc, wallet.testnet)
+ }
+ if (success) {
AnalyticsHelper.trackEvent("send_success")
+ } else {
+ throw Exception("Failed to send transaction")
}
return boc
}
@@ -57,7 +78,8 @@ class SignManager(
wallet: WalletEntity,
request: SignRequestEntity,
details: HistoryHelper.Details,
- canceller: CancellationSignal
+ canceller: CancellationSignal,
+ isBattery: Boolean,
) = suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { canceller.cancel() }
@@ -67,7 +89,7 @@ class SignManager(
continuation.resume(ActionScreen.parseResult(bundle))
}
}
- navigation.add(ActionScreen.newInstance(details, wallet.id, request, requestKey))
+ navigation.add(ActionScreen.newInstance(details, wallet.id, request, requestKey, isBattery))
}
private suspend fun emulate(
@@ -98,4 +120,36 @@ class SignManager(
null
}
}
+
+ private suspend fun emulateBattery(
+ request: SignRequestEntity,
+ wallet: WalletEntity,
+ currency: WalletCurrency = settingsRepository.currency,
+ forceRelayer: Boolean,
+ ): Pair {
+ return try {
+ if (api.config.isBatteryDisabled) {
+ throw IllegalStateException("Battery is disabled")
+ }
+
+ val tonProofToken = accountRepository.requestTonProofToken(wallet) ?: throw IllegalStateException("Can't find TonProof token")
+
+ val rates = ratesRepository.getRates(currency, "TON")
+ val seqno = accountRepository.getSeqno(wallet)
+ val cell = accountRepository.createSignedMessage(wallet, seqno, EmptyPrivateKeyEd25519, request.validUntil, request.transfers, internalMessage = true)
+
+ val (consequences, withBattery) = batteryRepository.emulate(
+ tonProofToken = tonProofToken,
+ publicKey = wallet.publicKey,
+ testnet = wallet.testnet,
+ boc = cell,
+ forceRelayer = forceRelayer,
+ ) ?: throw IllegalStateException("Failed to emulate battery")
+
+ val details = historyHelper.create(wallet, consequences, rates, isBattery = true)
+ Pair(details, withBattery)
+ } catch (e: Throwable) {
+ Pair(emulate(request, wallet, currency), false)
+ }
+ }
}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseListWalletScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseListWalletScreen.kt
index b626b7dca..24baf3b4c 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseListWalletScreen.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseListWalletScreen.kt
@@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.DrawableRes
+import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.tonapps.uikit.icon.UIKitIcon
@@ -18,7 +19,7 @@ import uikit.widget.SimpleRecyclerView
abstract class BaseListWalletScreen: BaseWalletScreen(R.layout.fragment_list) {
private lateinit var headerContainer: FrameLayout
- private lateinit var headerView: HeaderView
+ protected lateinit var headerView: HeaderView
private lateinit var listView: SimpleRecyclerView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -33,6 +34,8 @@ abstract class BaseListWalletScreen: BaseWalletScreen(R.layout.fragment_list) {
headerView.setIgnoreSystemOffset()
headerView.setAction(UIKitIcon.ic_close_16)
headerView.doOnActionClick = { finish() }
+ } else if (parentFragment != null) {
+ headerView.setIgnoreSystemOffset()
}
listView = view.findViewById(R.id.list)
@@ -40,6 +43,10 @@ abstract class BaseListWalletScreen: BaseWalletScreen(R.layout.fragment_list) {
collectFlow(listView.topScrolled, headerView::setDivider)
}
+ fun hideHeaderContainer() {
+ headerContainer.visibility = View.GONE
+ }
+
fun addViewHeader(view: View, params: FrameLayout.LayoutParams? = null) {
headerContainer.addView(view, params)
}
@@ -74,6 +81,15 @@ abstract class BaseListWalletScreen: BaseWalletScreen(R.layout.fragment_list) {
listView.setPadding(left, top, right, bottom)
}
+ fun updateListPadding(
+ left: Int = listView.paddingLeft,
+ top: Int = listView.paddingTop,
+ right: Int = listView.paddingRight,
+ bottom: Int = listView.paddingBottom
+ ) {
+ listView.updatePadding(left, top, right, bottom)
+ }
+
fun setActionIcon(@DrawableRes resId: Int, onClick: (view: View) -> Unit) {
headerView.setAction(resId)
headerView.doOnActionClick = onClick
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionArgs.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionArgs.kt
index f644f813c..64c85d86b 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionArgs.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionArgs.kt
@@ -6,44 +6,36 @@ import com.tonapps.wallet.data.core.entity.SignRequestEntity
import uikit.base.BaseArgs
data class ActionArgs(
- val accountId: String,
val walletId: String,
val request: SignRequestEntity,
val historyItems: List,
- val feeFormat: CharSequence,
- val feeFiatFormat: CharSequence,
val resultKey: String,
+ val isBattery: Boolean,
): BaseArgs() {
private companion object {
- private const val ARG_ACCOUNT_ID = "account_id"
private const val ARG_WALLET_ID = "wallet_id"
private const val ARG_HISTORY_ITEMS = "history_items"
- private const val ARG_FEE_FORMAT = "fee_format"
- private const val ARG_FEE_FIAT_FORMAT = "fee_fiat_format"
private const val ARG_RESULT_KEY = "result_key"
private const val ARG_REQUEST = "request"
+ private const val ARG_IS_BATTERY = "is_battery"
}
constructor(bundle: Bundle) : this(
- accountId = bundle.getString(ARG_ACCOUNT_ID)!!,
walletId = bundle.getString(ARG_WALLET_ID)!!,
request = bundle.getParcelable(ARG_REQUEST)!!,
historyItems = bundle.getParcelableArrayList(ARG_HISTORY_ITEMS)!!,
- feeFormat = bundle.getCharSequence(ARG_FEE_FORMAT)!!,
- feeFiatFormat = bundle.getCharSequence(ARG_FEE_FIAT_FORMAT)!!,
- resultKey = bundle.getString(ARG_RESULT_KEY)!!
+ resultKey = bundle.getString(ARG_RESULT_KEY)!!,
+ isBattery = bundle.getBoolean(ARG_IS_BATTERY)
)
override fun toBundle(): Bundle {
val bundle = Bundle()
- bundle.putString(ARG_ACCOUNT_ID, accountId)
bundle.putString(ARG_WALLET_ID, walletId)
bundle.putParcelable(ARG_REQUEST, request)
bundle.putParcelableArrayList(ARG_HISTORY_ITEMS, ArrayList(historyItems))
- bundle.putCharSequence(ARG_FEE_FORMAT, feeFormat)
- bundle.putCharSequence(ARG_FEE_FIAT_FORMAT, feeFiatFormat)
bundle.putString(ARG_RESULT_KEY, resultKey)
+ bundle.putBoolean(ARG_IS_BATTERY, isBattery)
return bundle
}
}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionScreen.kt
index 926d429d6..606a7f53d 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionScreen.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionScreen.kt
@@ -7,7 +7,6 @@ import android.widget.Button
import androidx.appcompat.widget.AppCompatTextView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.tonapps.icu.CurrencyFormatter.withCustomSymbol
import com.tonapps.tonkeeper.core.history.HistoryHelper
import com.tonapps.tonkeeper.core.history.list.HistoryAdapter
import com.tonapps.tonkeeper.core.history.list.item.HistoryItem
@@ -43,7 +42,9 @@ class ActionScreen: BaseWalletScreen(R.layout.fragment_action), BaseFragment.Mod
private lateinit var walletView: AppCompatTextView
private lateinit var closeView: View
private lateinit var actionsView: SimpleRecyclerView
- private lateinit var feeView: AppCompatTextView
+ private lateinit var buttonsView: View
+ private lateinit var confirmButton: Button
+ private lateinit var cancelButton: Button
private lateinit var processView: ProcessTaskView
private lateinit var slideView: SlideActionView
@@ -59,15 +60,6 @@ class ActionScreen: BaseWalletScreen(R.layout.fragment_action), BaseFragment.Mod
adapter.submitList(args.historyItems)
- feeView = view.findViewById(R.id.fee)
-
- val builder = SpannableStringBuilder("≈ ")
- builder.append(args.feeFormat.withCustomSymbol(requireContext()))
- builder.append(" · ")
- builder.append(args.feeFiatFormat.withCustomSymbol(requireContext()))
-
- feeView.text = builder
-
processView = view.findViewById(R.id.process)
slideView = view.findViewById(R.id.slide)
@@ -128,27 +120,24 @@ class ActionScreen: BaseWalletScreen(R.layout.fragment_action), BaseFragment.Mod
details: HistoryHelper.Details,
walletId: String,
request: SignRequestEntity,
- requestKey: String
+ requestKey: String,
+ isBattery: Boolean = false,
) = newInstance(
- accountId = details.accountId,
walletId = walletId,
request = request,
historyItems = details.items,
- feeFormat = details.feeFormat,
- feeFiatFormat = details.feeFiatFormat,
- requestKey = requestKey
+ requestKey = requestKey,
+ isBattery = isBattery,
)
fun newInstance(
- accountId: String,
walletId: String,
request: SignRequestEntity,
historyItems: List,
- feeFormat: CharSequence,
- feeFiatFormat: CharSequence,
- requestKey: String
+ requestKey: String,
+ isBattery: Boolean = false,
): ActionScreen {
- val args = ActionArgs(accountId, walletId, request, historyItems, feeFormat, feeFiatFormat, requestKey)
+ val args = ActionArgs(walletId, request, historyItems, requestKey, isBattery)
val screen = ActionScreen()
screen.setArgs(args)
return screen
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionViewModel.kt
index 82b23a7f9..c5ab8c150 100644
--- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionViewModel.kt
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/action/ActionViewModel.kt
@@ -13,6 +13,7 @@ import com.tonapps.tonkeeper.ui.screen.send.main.SendException
import com.tonapps.wallet.api.API
import com.tonapps.wallet.data.account.entities.WalletEntity
import com.tonapps.wallet.data.account.AccountRepository
+import com.tonapps.wallet.data.battery.BatteryRepository
import com.tonapps.wallet.data.passcode.PasscodeManager
import com.tonapps.wallet.localization.Localization
import kotlinx.coroutines.flow.MutableStateFlow
@@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
+import org.ton.contract.wallet.WalletTransfer
class ActionViewModel(
app: Application,
@@ -30,6 +32,7 @@ class ActionViewModel(
private val accountRepository: AccountRepository,
private val passcodeManager: PasscodeManager,
private val api: API,
+ private val batteryRepository: BatteryRepository,
) : BaseWalletVM(app) {
private val _walletFlow = MutableStateFlow(null)
@@ -69,9 +72,11 @@ class ActionViewModel(
transactions.forEachIndexed { index, transaction ->
Log.d("ActionViewModel", "Signing transaction $transaction")
val isLast = index == transactions.size - 1
- val signedBody = context.signLedgerTransaction(transaction, wallet.id) ?: throw SendException.Cancelled()
+ val signedBody = context.signLedgerTransaction(transaction, wallet.id)
+ ?: throw SendException.Cancelled()
val contract = wallet.contract
- val message = contract.createTransferMessageCell(contract.address, seqno, signedBody)
+ val message =
+ contract.createTransferMessageCell(contract.address, seqno, signedBody)
boc = message.base64()
if (!isLast) {
Log.d("ActionViewModel", "Sending transaction to blockchain")
@@ -86,12 +91,19 @@ class ActionViewModel(
boc!!
} else {
val secretKey = accountRepository.getPrivateKey(wallet.id)
+ val excessesAddress = if (args.isBattery) {
+ batteryRepository.getConfig(wallet.testnet).excessesAddress
+ } else null
+
+ val transfers = request.messages.map { it.getWalletTransfer(excessesAddress) }
+
val message = accountRepository.createSignedMessage(
wallet,
seqno,
secretKey,
request.validUntil,
- request.transfers
+ transfers,
+ internalMessage = args.isBattery
)
message.base64()
}
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryRoute.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryRoute.kt
new file mode 100644
index 000000000..bce7d6bf5
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryRoute.kt
@@ -0,0 +1,6 @@
+package com.tonapps.tonkeeper.ui.screen.battery
+
+sealed class BatteryRoute {
+ data object Refill: BatteryRoute()
+ data object Settings: BatteryRoute()
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryScreen.kt
new file mode 100644
index 000000000..ad55f1214
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryScreen.kt
@@ -0,0 +1,88 @@
+package com.tonapps.tonkeeper.ui.screen.battery
+
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.fragment.app.FragmentManager
+import com.tonapps.tonkeeper.ui.base.BaseWalletScreen
+import com.tonapps.tonkeeper.ui.screen.battery.refill.BatteryRefillScreen
+import com.tonapps.tonkeeper.ui.screen.battery.settings.BatterySettingsScreen
+import com.tonapps.tonkeeperx.R
+import com.tonapps.uikit.icon.UIKitIcon
+import kotlinx.coroutines.flow.map
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import uikit.base.BaseFragment
+import uikit.extensions.collectFlow
+import uikit.extensions.commitChildAsSlide
+import uikit.widget.HeaderView
+
+class BatteryScreen : BaseWalletScreen(R.layout.fragment_battery), BaseFragment.BottomSheet {
+
+ private val initialPromo: String? by lazy { requireArguments().getString(ARG_PROMO) }
+
+ override val viewModel: BatteryViewModel by viewModel()
+
+ private val backStackListener = FragmentManager.OnBackStackChangedListener {
+ updateBackVisibility()
+ }
+
+ private lateinit var headerView: HeaderView
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ headerView = view.findViewById(R.id.header)
+ headerView.doOnActionClick = { finish() }
+ headerView.doOnCloseClick = { childFragmentManager.popBackStack() }
+ headerView.setBackgroundResource(uikit.R.drawable.bg_page_gradient)
+
+ childFragmentManager.addOnBackStackChangedListener(backStackListener)
+ collectFlow(viewModel.routeFlow.map { route ->
+ when(route) {
+ BatteryRoute.Refill -> BatteryRefillScreen.newInstance(initialPromo)
+ BatteryRoute.Settings -> BatterySettingsScreen.newInstance()
+ }
+ }, ::setChildFragment)
+
+ collectFlow(viewModel.titleFlow) {
+ headerView.title = it
+ }
+ }
+
+ private fun setChildFragment(fragment: BaseFragment) {
+ childFragmentManager.commitChildAsSlide {
+ replace(R.id.fragment_battery_container, fragment, fragment.toString())
+ addToBackStack(fragment.toString())
+ }
+ }
+
+ private fun updateBackVisibility() {
+ val hasBackStack = childFragmentManager.backStackEntryCount > 1
+ headerView.setIcon(if (hasBackStack) UIKitIcon.ic_chevron_left_16 else 0)
+ }
+
+ override fun onBackPressed(): Boolean {
+ if (childFragmentManager.backStackEntryCount > 1) {
+ childFragmentManager.popBackStack()
+ return false
+ }
+ return super.onBackPressed()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ childFragmentManager.removeOnBackStackChangedListener(backStackListener)
+ }
+
+ companion object {
+ private const val ARG_PROMO = "promo"
+
+ fun newInstance(promo: String? = null): BatteryScreen {
+ val fragment = BatteryScreen()
+ fragment.arguments = Bundle().apply {
+ putString(ARG_PROMO, promo)
+ }
+
+ return fragment
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryViewModel.kt
new file mode 100644
index 000000000..c16da5fd4
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/BatteryViewModel.kt
@@ -0,0 +1,46 @@
+package com.tonapps.tonkeeper.ui.screen.battery
+
+import android.app.Application
+import com.tonapps.extensions.MutableEffectFlow
+import com.tonapps.tonkeeper.ui.base.BaseWalletVM
+import com.tonapps.wallet.api.API
+import com.tonapps.wallet.data.account.AccountRepository
+import com.tonapps.wallet.data.account.entities.WalletEntity
+import com.tonapps.wallet.data.battery.BatteryMapper
+import com.tonapps.wallet.data.battery.BatteryRepository
+import com.tonapps.wallet.data.battery.entity.BatteryBalanceEntity
+import com.tonapps.wallet.data.settings.SettingsRepository
+import com.tonapps.wallet.data.token.TokenRepository
+import com.tonapps.wallet.data.token.entities.AccountTokenEntity
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+class BatteryViewModel(
+ app: Application
+) : BaseWalletVM(app) {
+
+ private val _routeFlow = MutableEffectFlow() // with "_" writable only inside
+ val routeFlow = _routeFlow.asSharedFlow().filterNotNull() // without "_" read-only for outside
+
+ private val _titleFlow = MutableStateFlow(null)
+ val titleFlow = _titleFlow.asStateFlow()
+
+ init {
+ routeToRefill()
+ }
+
+ fun routeToSettings() {
+ _routeFlow.tryEmit(BatteryRoute.Settings)
+ }
+
+ private fun routeToRefill() {
+ _routeFlow.tryEmit(BatteryRoute.Refill)
+ }
+
+ fun setTitle(title: CharSequence?) {
+ _titleFlow.value = title
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/BatteryRechargeScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/BatteryRechargeScreen.kt
new file mode 100644
index 000000000..d5931e6fd
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/BatteryRechargeScreen.kt
@@ -0,0 +1,158 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge
+
+import android.os.Bundle
+import android.util.Log
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.inflate
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.lifecycle.lifecycleScope
+import com.tonapps.extensions.getParcelableCompat
+import com.tonapps.icu.CurrencyFormatter
+import com.tonapps.tonkeeper.extensions.showToast
+import com.tonapps.tonkeeper.extensions.toast
+import com.tonapps.tonkeeper.ui.base.BaseListWalletScreen
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.entity.BatteryRechargeEvent
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.Adapter
+import com.tonapps.tonkeeper.ui.screen.root.RootViewModel
+import com.tonapps.tonkeeper.ui.screen.send.contacts.SendContactsScreen
+import com.tonapps.tonkeeper.ui.screen.send.main.SendContact
+import com.tonapps.tonkeeperx.R
+import com.tonapps.wallet.data.core.entity.SignRequestEntity
+import com.tonapps.wallet.data.token.entities.AccountTokenEntity
+import com.tonapps.wallet.localization.Localization
+import kotlinx.coroutines.launch
+import org.koin.androidx.viewmodel.ext.android.activityViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+import uikit.base.BaseFragment
+import uikit.extensions.collectFlow
+import uikit.extensions.hideKeyboard
+import uikit.widget.FrescoView
+import uikit.widget.InputView
+import java.util.UUID
+
+class BatteryRechargeScreen : BaseListWalletScreen(), BaseFragment.BottomSheet {
+
+ private val args: RechargeArgs by lazy { RechargeArgs(requireArguments()) }
+ private val contractsRequestKey: String by lazy { "contacts_${UUID.randomUUID()}" }
+
+ private val rootViewModel: RootViewModel by activityViewModel()
+
+ override val viewModel: BatteryRechargeViewModel by viewModel { parametersOf(args) }
+
+ private val adapter = Adapter(
+ onAddressChange = { viewModel.updateAddress(it) },
+ openAddressBook = ::openAddressBook,
+ onAmountChange = { viewModel.updateAmount(it) },
+ onPackSelect = { viewModel.setSelectedPack(it) },
+ onCustomAmountSelect = { viewModel.onCustomAmountSelect() },
+ onContinue = ::onContinue,
+ onSubmitPromo = { viewModel.applyPromo(it) }
+ )
+
+ private lateinit var tokenIconView: FrescoView
+ private lateinit var tokenTitleView: AppCompatTextView
+
+ private val addressInput: InputView?
+ get() = findListItemView(0)?.findViewById(R.id.address)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ collectFlow(viewModel.uiItemsFlow, adapter::submitList)
+
+ navigation?.setFragmentResultListener(contractsRequestKey) { bundle ->
+ val contact = bundle.getParcelableCompat("contact")
+ ?: return@setFragmentResultListener
+ addressInput?.text = contact.address
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setAdapter(adapter)
+
+ val rightContentView = inflate(context, R.layout.view_battery_recharge_token, null)
+ tokenIconView = rightContentView.findViewById(R.id.token_icon)
+ tokenTitleView = rightContentView.findViewById(R.id.token_title)
+ rightContentView.findViewById(R.id.token)
+ .setOnClickListener { openTokenSelector() }
+
+ headerView.setRightContent(rightContentView)
+ headerView.hideIcon()
+ headerView.setTitleGravity(Gravity.START)
+ headerView.title = when (args.isGift) {
+ true -> getString(Localization.battery_gift_title)
+ false -> getString(Localization.battery_recharge_title)
+ }
+ collectFlow(viewModel.tokenFlow) { token ->
+ tokenTitleView.text = token.symbol
+ tokenIconView.setImageURI(token.imageUri, this)
+ }
+ collectFlow(viewModel.eventFlow, ::onEvent)
+ }
+
+ private fun onContinue() {
+ requireContext().hideKeyboard()
+ viewModel.onContinue()
+ }
+
+ private fun openAddressBook() {
+ navigation?.add(SendContactsScreen.newInstance(contractsRequestKey))
+ getCurrentFocus()?.hideKeyboard()
+ }
+
+ private fun openTokenSelector() {
+ Log.d("BatteryRechargeScreen", "openTokenSelector")
+ }
+
+ private fun showError(message: String? = null) {
+ navigation?.toast(message ?: getString(Localization.sending_error))
+ }
+
+ private fun onSuccess() {
+ requireContext().showToast(Localization.battery_please_wait)
+ navigation?.openURL("tonkeeper://activity")
+ finish()
+ }
+
+ private fun sing(
+ request: SignRequestEntity
+ ) {
+ lifecycleScope.launch {
+ try {
+ rootViewModel.requestSign(requireContext(), request, forceRelayer = true)
+ onSuccess()
+ } catch (_: Exception) {}
+ }
+ }
+
+ private fun onEvent(event: BatteryRechargeEvent) {
+ when (event) {
+ is BatteryRechargeEvent.Sign -> sing(event.request)
+ is BatteryRechargeEvent.Error -> showError()
+ is BatteryRechargeEvent.MaxAmountError -> {
+ val message = requireContext().getString(
+ Localization.battery_max_input_amount,
+ CurrencyFormatter.format(currency = event.currency, value = event.maxAmount)
+ )
+ showError(message)
+ }
+ }
+ }
+
+ companion object {
+
+ fun newInstance(
+ token: AccountTokenEntity? = null, isGift: Boolean = false
+ ): BatteryRechargeScreen {
+ val args = RechargeArgs(token, isGift)
+ val fragment = BatteryRechargeScreen()
+ fragment.setArgs(args)
+ return fragment
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/BatteryRechargeViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/BatteryRechargeViewModel.kt
new file mode 100644
index 000000000..252b355a3
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/BatteryRechargeViewModel.kt
@@ -0,0 +1,466 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge
+
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.tonapps.blockchain.ton.TonNetwork
+import com.tonapps.blockchain.ton.TonTransferHelper
+import com.tonapps.blockchain.ton.extensions.base64
+import com.tonapps.blockchain.ton.extensions.toAccountId
+import com.tonapps.blockchain.ton.tlb.JettonTransfer
+import com.tonapps.extensions.MutableEffectFlow
+import com.tonapps.extensions.state
+import com.tonapps.icu.Coins
+import com.tonapps.icu.CurrencyFormatter
+import com.tonapps.tonkeeper.core.entities.TransferEntity
+import com.tonapps.tonkeeper.ui.base.BaseWalletVM
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.entity.BatteryRechargeEvent
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.entity.RechargePackEntity
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.entity.RechargePackType
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.Item
+import com.tonapps.tonkeeper.ui.screen.battery.refill.entity.PromoState
+import com.tonapps.tonkeeper.ui.screen.send.main.state.SendDestination
+import com.tonapps.uikit.list.ListCell
+import com.tonapps.wallet.api.API
+import com.tonapps.wallet.api.entity.TokenEntity
+import com.tonapps.wallet.data.account.AccountRepository
+import com.tonapps.wallet.data.account.entities.WalletEntity
+import com.tonapps.wallet.data.battery.BatteryMapper
+import com.tonapps.wallet.data.battery.BatteryRepository
+import com.tonapps.wallet.data.battery.entity.BatteryBalanceEntity
+import com.tonapps.wallet.data.battery.entity.BatteryConfigEntity
+import com.tonapps.wallet.data.battery.entity.RechargeMethodEntity
+import com.tonapps.wallet.data.battery.entity.RechargeMethodType
+import com.tonapps.wallet.data.core.entity.RawMessageEntity
+import com.tonapps.wallet.data.core.entity.SignRequestEntity
+import com.tonapps.wallet.data.rates.RatesRepository
+import com.tonapps.wallet.data.settings.SettingsRepository
+import com.tonapps.wallet.data.token.TokenRepository
+import com.tonapps.wallet.data.token.entities.AccountTokenEntity
+import com.tonapps.wallet.localization.Localization
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.withContext
+import org.ton.block.AddrStd
+import uikit.extensions.collectFlow
+import java.math.BigDecimal
+
+class BatteryRechargeViewModel(
+ app: Application,
+ private val args: RechargeArgs,
+ private val accountRepository: AccountRepository,
+ private val batteryRepository: BatteryRepository,
+ private val tokenRepository: TokenRepository,
+ private val settingsRepository: SettingsRepository,
+ private val ratesRepository: RatesRepository,
+ private val api: API,
+) : BaseWalletVM(app) {
+
+ private val _tokenFlow = MutableStateFlow(null)
+ val tokenFlow = _tokenFlow.asStateFlow().filterNotNull()
+
+ private val promoStateFlow = MutableStateFlow(PromoState.Default)
+
+ private val _amountFlow = MutableStateFlow(0.0)
+ private val amountFlow = _amountFlow.map { Coins.of(it) }
+
+ private val _addressFlow = MutableStateFlow("")
+
+ @OptIn(FlowPreview::class)
+ private val addressDebounceFlow = _addressFlow.debounce { if (it.isEmpty()) 0 else 600 }
+
+ private val _destinationLoadingFlow = MutableStateFlow(false)
+
+ private val destinationFlow = combine(
+ accountRepository.selectedWalletFlow, addressDebounceFlow
+ ) { wallet, address ->
+ if (address.isEmpty()) {
+ return@combine SendDestination.Empty
+ }
+ _destinationLoadingFlow.tryEmit(true)
+ val destination = getDestinationAccount(address, wallet.testnet)
+ _destinationLoadingFlow.tryEmit(false)
+ destination
+ }.flowOn(Dispatchers.IO).state(viewModelScope)
+
+ private val _eventFlow = MutableEffectFlow()
+ val eventFlow = _eventFlow.asSharedFlow().filterNotNull()
+
+ private val _selectedPackTypeFlow = MutableStateFlow(null)
+ private val _customAmountFlow = MutableStateFlow(false)
+
+ private val selectedFlow = combine(
+ _selectedPackTypeFlow,
+ _customAmountFlow,
+ ) { selectedPackType, customAmount ->
+ selectedPackType to customAmount
+ }
+
+ private val stateFlow = combine(
+ accountRepository.selectedWalletFlow, tokenFlow, promoStateFlow
+ ) { wallet, token, promoState ->
+ Triple(wallet, token, promoState)
+ }
+
+ val uiItemsFlow = combine(
+ stateFlow,
+ amountFlow,
+ selectedFlow,
+ _destinationLoadingFlow,
+ destinationFlow,
+ ) { state, amount, selected, destinationLoading, destination ->
+ val uiItems = mutableListOf- ()
+
+ val wallet = state.first
+ val token = state.second
+ val promoState = state.third
+ val selectedPackType = selected.first
+ val customAmount = selected.second
+
+ val batteryBalance = getBatteryBalance(wallet)
+ val ton = tokenRepository.get(settingsRepository.currency, wallet.accountId, wallet.testnet)
+ ?.find { it.isTon } ?: return@combine emptyList()
+ val hasEnoughTonBalance = ton.balance.value >= Coins.of(0.1)
+ val hasBatteryBalance = batteryBalance.balance > Coins.ZERO
+ val rechargeMethod = getRechargeMethod(wallet, token)
+ val shouldMinusReservedAmount = batteryBalance.reservedBalance == Coins.ZERO || args.isGift
+
+ val batteryReservedAmount = rechargeMethod.fromTon(api.config.batteryReservedAmount)
+
+ if (args.isGift) {
+ uiItems.add(
+ Item.Address(
+ loading = destinationLoading,
+ error = destination is SendDestination.NotFound
+ )
+ )
+ uiItems.add(Item.Space)
+ }
+
+ val packs = getPacks(
+ rechargeMethod,
+ token,
+ hasEnoughTonBalance || hasBatteryBalance,
+ shouldMinusReservedAmount
+ )
+
+ uiItems.addAll(uiItemsPacks(packs, selectedPackType, customAmount))
+
+ if (!api.config.batteryPromoDisabled) {
+ uiItems.add(Item.Space)
+ uiItems.add(Item.Promo(promoState))
+ }
+
+ val remainingBalance = token.balance.value - amount
+ val minAmount: Coins = when {
+ !hasBatteryBalance && !hasEnoughTonBalance && rechargeMethod.minBootstrapValue != null -> {
+ Coins.of(rechargeMethod.minBootstrapValue!!)
+ }
+
+ shouldMinusReservedAmount -> batteryReservedAmount
+ else -> Coins.ZERO
+ }
+ val isLessThanMin = amount > Coins.ZERO && amount < minAmount
+
+ if (customAmount) {
+ val charges = BatteryMapper.calculateCryptoCharges(
+ getRechargeMethod(wallet, token), api.config.batteryMeanFees, amount
+ )
+ uiItems.add(Item.Space)
+ uiItems.add(
+ Item.Amount(
+ symbol = token.symbol,
+ formattedRemaining = CurrencyFormatter.format(
+ currency = token.symbol, value = remainingBalance
+ ),
+ isInsufficientBalance = remainingBalance.isNegative,
+ isLessThanMin = isLessThanMin,
+ formattedCharges = CurrencyFormatter.format(value = charges)
+ )
+ )
+ }
+
+ val isValidGiftAddress = if (args.isGift) {
+ destination is SendDestination.Account
+ } else {
+ true
+ }
+
+ val isValidAmount = if (customAmount) {
+ amount.isPositive && !isLessThanMin && !remainingBalance.isNegative
+ } else {
+ selectedPackType != null
+ }
+
+ uiItems.add(Item.Space)
+ uiItems.add(Item.Button(isValidGiftAddress && isValidAmount))
+
+ uiItems.toList()
+ }
+
+ init {
+ accountRepository.selectedWalletFlow.take(1).map { wallet ->
+ val appliedPromo = batteryRepository.getAppliedPromo(wallet.testnet)
+
+ if (appliedPromo.isNullOrBlank()) {
+ promoStateFlow.tryEmit(PromoState.Default)
+ } else {
+ promoStateFlow.tryEmit(PromoState.Applied(appliedPromo))
+ }
+
+ if (args.token == null) {
+ val batteryConfig = getBatteryConfig(wallet)
+ val supportedTokens = getSupportedTokens(wallet, batteryConfig.rechargeMethods)
+ _tokenFlow.tryEmit(supportedTokens.first())
+ } else {
+ _tokenFlow.tryEmit(args.token)
+ }
+ }.flowOn(Dispatchers.IO).launchIn(viewModelScope)
+ }
+
+ fun setToken(token: AccountTokenEntity) {
+ _tokenFlow.tryEmit(token)
+ }
+
+ fun updateAddress(address: String) {
+ _addressFlow.tryEmit(address)
+ }
+
+ fun updateAmount(amount: Double) {
+ _amountFlow.value = amount
+ }
+
+ fun setSelectedPack(packType: RechargePackType) {
+ _selectedPackTypeFlow.tryEmit(packType)
+ _customAmountFlow.tryEmit(false)
+ }
+
+ fun onCustomAmountSelect() {
+ _customAmountFlow.tryEmit(true)
+ _selectedPackTypeFlow.tryEmit(null)
+ }
+
+ fun onContinue() = combine(stateFlow, destinationFlow) { state, destination ->
+ val wallet = state.first
+ val token = state.second
+ val rechargeMethod = getRechargeMethod(wallet, token)
+ val config = getBatteryConfig(wallet)
+ val batteryMaxInputAmount = rechargeMethod.fromTon(api.config.batteryMaxInputAmount)
+
+ val amount = _selectedPackTypeFlow.value?.let { packType ->
+ Coins.of(RechargePackEntity.getTonAmount(api.config.batteryMeanFees, packType))
+ } ?: Coins.of(_amountFlow.value)
+
+ if (amount > batteryMaxInputAmount) {
+ _eventFlow.tryEmit(
+ BatteryRechargeEvent.MaxAmountError(
+ currency = token.symbol,
+ maxAmount = batteryMaxInputAmount
+ )
+ )
+ return@combine
+ }
+
+ val fundReceiver = config.fundReceiver ?: return@combine
+ val recipientAddress = if (destination is SendDestination.Account) {
+ destination.address
+ } else null
+ val payload = wallet.contract.createBatteryBody(
+ recipientAddress,
+ appliedPromo = batteryRepository.getAppliedPromo(wallet.testnet)
+ )
+ val validUntil = accountRepository.getValidUntil(wallet.testnet)
+ val network = when (wallet.testnet) {
+ true -> TonNetwork.TESTNET
+ false -> TonNetwork.MAINNET
+ }
+
+ if (token.isTon) {
+ val request = SignRequestEntity(
+ fromValue = wallet.contract.address.toAccountId(),
+ validUntil = validUntil,
+ messages = listOf(
+ RawMessageEntity(
+ addressValue = fundReceiver,
+ amount = amount.toLong(),
+ stateInitValue = null,
+ payloadValue = payload.base64()
+ )
+ ),
+ network = network,
+ )
+ _eventFlow.tryEmit(BatteryRechargeEvent.Sign(request))
+ } else {
+ val queryId = TransferEntity.newWalletQueryId()
+ val jettonPayload = TonTransferHelper.jetton(
+ coins = org.ton.block.Coins.ofNano(amount.toLong()),
+ toAddress = AddrStd.parse(fundReceiver),
+ responseAddress = wallet.contract.address,
+ queryId = queryId,
+ body = payload,
+ )
+ val request = SignRequestEntity(
+ fromValue = wallet.contract.address.toAccountId(),
+ validUntil = validUntil,
+ messages = listOf(
+ RawMessageEntity(
+ addressValue = token.balance.walletAddress,
+ amount = Coins.of(0.1).toLong(),
+ stateInitValue = null,
+ payloadValue = jettonPayload.base64()
+ )
+ ),
+ network = network,
+ )
+ _eventFlow.tryEmit(BatteryRechargeEvent.Sign(request))
+ }
+ }.catch {
+ _eventFlow.tryEmit(BatteryRechargeEvent.Error)
+ }.flowOn(Dispatchers.IO).launchIn(viewModelScope)
+
+ private fun uiItemsPacks(
+ packs: List,
+ selectedPackType: RechargePackType?,
+ isCustomAmount: Boolean
+ ): List
- {
+ val uiItems = mutableListOf
- ()
+ for ((index, pack) in packs.withIndex()) {
+ val position = ListCell.getPosition(packs.size + 1, index)
+ uiItems.add(
+ Item.RechargePack(
+ position = position,
+ packType = pack.type,
+ charges = pack.charges,
+ formattedAmount = pack.formattedAmount,
+ formattedFiatAmount = pack.formattedFiatAmount,
+ batteryLevel = pack.batteryLevel,
+ isEnabled = pack.isEnabled,
+ selected = pack.type == selectedPackType,
+ transactions = pack.transactions,
+ )
+ )
+ }
+ uiItems.add(Item.CustomAmount(position = ListCell.Position.LAST, selected = isCustomAmount))
+ return uiItems.toList()
+ }
+
+ private suspend fun getBatteryConfig(
+ wallet: WalletEntity
+ ): BatteryConfigEntity {
+ return batteryRepository.getConfig(wallet.testnet)
+ }
+
+ private suspend fun getBatteryBalance(
+ wallet: WalletEntity
+ ): BatteryBalanceEntity {
+ val tonProofToken =
+ accountRepository.requestTonProofToken(wallet) ?: return BatteryBalanceEntity.Empty
+ return batteryRepository.getBalance(
+ tonProofToken = tonProofToken, publicKey = wallet.publicKey, testnet = wallet.testnet
+ )
+ }
+
+ private suspend fun getTokens(wallet: WalletEntity): List {
+ return tokenRepository.get(
+ currency = settingsRepository.currency,
+ accountId = wallet.accountId,
+ testnet = wallet.testnet
+ ) ?: emptyList()
+ }
+
+ private suspend fun getSupportedTokens(
+ wallet: WalletEntity, rechargeMethods: List
+ ): List {
+ val tokens = getTokens(wallet)
+ val supportTokenAddress = rechargeMethods.filter { it.supportRecharge }.mapNotNull {
+ if (it.type == RechargeMethodType.TON) {
+ TokenEntity.TON.address
+ } else {
+ it.jettonMaster
+ }
+ }
+ return tokens.filter { token ->
+ supportTokenAddress.contains(token.address)
+ }.sortedBy { it.fiat }.reversed()
+ }
+
+ private suspend fun getRechargeMethod(
+ wallet: WalletEntity, token: AccountTokenEntity
+ ): RechargeMethodEntity {
+ val rechargeMethods = getBatteryConfig(wallet).rechargeMethods
+ return rechargeMethods.first {
+ if (it.type == RechargeMethodType.TON) {
+ token.isTon
+ } else {
+ it.jettonMaster == token.address
+ }
+ }
+ }
+
+ private fun getPacks(
+ rechargeMethod: RechargeMethodEntity,
+ token: AccountTokenEntity,
+ willBePaidManually: Boolean,
+ shouldMinusReservedAmount: Boolean
+ ): List {
+ val fiatRate = ratesRepository.getRates(settingsRepository.currency, token.address)
+ .getRate(token.address)
+ val config = api.config
+
+ return arrayOf(
+ RechargePackType.LARGE, RechargePackType.MEDIUM, RechargePackType.SMALL
+ ).map { type ->
+ RechargePackEntity(
+ type = type,
+ rechargeMethod = rechargeMethod,
+ fiatRate = fiatRate,
+ token = token,
+ config = config,
+ shouldMinusReservedAmount = shouldMinusReservedAmount,
+ willBePaidManually = willBePaidManually,
+ currency = settingsRepository.currency,
+ )
+ }
+ }
+
+ private suspend fun getDestinationAccount(
+ address: String, testnet: Boolean
+ ) = withContext(Dispatchers.IO) {
+ val accountDeferred = async { api.resolveAccount(address, testnet) }
+ val publicKeyDeferred = async { api.safeGetPublicKey(address, testnet) }
+
+ val account = accountDeferred.await() ?: return@withContext SendDestination.NotFound
+ val publicKey = publicKeyDeferred.await()
+
+ SendDestination.Account(address, publicKey, account)
+ }
+
+ fun applyPromo(promo: String) = accountRepository.selectedWalletFlow.take(1).map { wallet ->
+ if (promo.isEmpty()) {
+ promoStateFlow.tryEmit(PromoState.Default)
+ return@map
+ }
+ promoStateFlow.tryEmit(PromoState.Loading())
+ try {
+ api.battery(wallet.testnet).verifyPurchasePromo(promo)
+ batteryRepository.setAppliedPromo(wallet.testnet, promo)
+ promoStateFlow.tryEmit(PromoState.Applied(promo))
+ } catch (_: Exception) {
+ promoStateFlow.tryEmit(PromoState.Error)
+ }
+ }.flowOn(Dispatchers.IO).launchIn(viewModelScope)
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/RechargeArgs.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/RechargeArgs.kt
new file mode 100644
index 000000000..b97bb2393
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/RechargeArgs.kt
@@ -0,0 +1,29 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge
+
+import android.os.Bundle
+import com.tonapps.extensions.getParcelableCompat
+import com.tonapps.wallet.data.token.entities.AccountTokenEntity
+import uikit.base.BaseArgs
+
+data class RechargeArgs(
+ val token: AccountTokenEntity?,
+ val isGift: Boolean
+) : BaseArgs() {
+
+ private companion object {
+ private const val ARG_TOKEN = "token"
+ private const val ARG_IS_GIFT = "is_gift"
+ }
+
+ constructor(bundle: Bundle) : this(
+ token = bundle.getParcelableCompat(ARG_TOKEN),
+ isGift = bundle.getBoolean(ARG_IS_GIFT)
+ )
+
+ override fun toBundle(): Bundle {
+ val bundle = Bundle()
+ bundle.putParcelable(ARG_TOKEN, token)
+ bundle.putBoolean(ARG_IS_GIFT, isGift)
+ return bundle
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/BatteryRechargeEvent.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/BatteryRechargeEvent.kt
new file mode 100644
index 000000000..9866c9a20
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/BatteryRechargeEvent.kt
@@ -0,0 +1,11 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.entity
+
+import androidx.annotation.StringRes
+import com.tonapps.icu.Coins
+import com.tonapps.wallet.data.core.entity.SignRequestEntity
+
+sealed class BatteryRechargeEvent {
+ data class Sign(val request: SignRequestEntity) : BatteryRechargeEvent()
+ data object Error : BatteryRechargeEvent()
+ data class MaxAmountError(val maxAmount: Coins, val currency: String) : BatteryRechargeEvent()
+}
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/RechargePackEntity.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/RechargePackEntity.kt
new file mode 100644
index 000000000..0557b408a
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/RechargePackEntity.kt
@@ -0,0 +1,109 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.entity
+
+import com.tonapps.icu.Coins
+import com.tonapps.icu.CurrencyFormatter
+import com.tonapps.wallet.api.entity.ConfigEntity
+import com.tonapps.wallet.data.battery.BatteryMapper
+import com.tonapps.wallet.data.battery.entity.RechargeMethodEntity
+import com.tonapps.wallet.data.core.WalletCurrency
+import com.tonapps.wallet.data.settings.BatteryTransaction
+import com.tonapps.wallet.data.token.entities.AccountTokenEntity
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+data class RechargePackEntity(
+ val type: RechargePackType,
+ private val rechargeMethod: RechargeMethodEntity,
+ private val fiatRate: Coins,
+ private val token: AccountTokenEntity,
+ private val config: ConfigEntity,
+ private val willBePaidManually: Boolean,
+ private val shouldMinusReservedAmount: Boolean,
+ private val currency: WalletCurrency,
+) {
+ private val rate: BigDecimal
+ get() = rechargeMethod.rate.toBigDecimal()
+
+ private val meansFee: BigDecimal
+ get() = config.batteryMeanFees.toBigDecimal()
+
+ private val tonAmount: BigDecimal
+ get() = getTonAmount(meansFee, type)
+
+ private val amountInToken: BigDecimal
+ get() = rechargeMethod.fromTon(tonAmount).value
+
+ private val reservedAmount: BigDecimal
+ get() = rechargeMethod.fromTon(config.batteryReservedAmount).value
+
+ val formattedAmount: CharSequence
+ get() = CurrencyFormatter.formatFiat(
+ currency = rechargeMethod.symbol,
+ value = amountInToken,
+ )
+
+ val formattedFiatAmount: CharSequence
+ get() = CurrencyFormatter.formatFiat(
+ currency = currency.code,
+ value = amountInToken.multiply(fiatRate.value)
+ )
+
+ val charges: Int
+ get() = amountInToken.minus(
+ if (shouldMinusReservedAmount) {
+ reservedAmount
+ } else {
+ BigDecimal.valueOf(0)
+ }
+ ).multiply(rate).divide(meansFee, 0, RoundingMode.HALF_UP).toInt()
+
+ val batteryLevel: Float
+ get() = when (type) {
+ RechargePackType.LARGE -> 1f
+ RechargePackType.MEDIUM -> 0.5f
+ RechargePackType.SMALL -> 0.25f
+ }
+
+ val transactions: Map
+ get() = mapOf(
+ BatteryTransaction.SWAP to charges / BatteryMapper.calculateChargesAmount(
+ config.batteryMeanPriceSwap,
+ config.batteryMeanFees
+ ),
+ BatteryTransaction.NFT to charges / BatteryMapper.calculateChargesAmount(
+ config.batteryMeanPriceNft,
+ config.batteryMeanFees
+ ),
+ BatteryTransaction.JETTON to charges / BatteryMapper.calculateChargesAmount(
+ config.batteryMeanPriceJetton,
+ config.batteryMeanFees
+ )
+ )
+
+ private val isAvailableToBuy: Boolean
+ get() = willBePaidManually || rechargeMethod.minBootstrapValue?.let { amountInToken >= it.toBigDecimal() } ?: false
+
+ private val isEnoughBalance: Boolean
+ get() = token.balance.value.value >= amountInToken
+
+ val isEnabled: Boolean
+ get() = isAvailableToBuy && isEnoughBalance
+
+
+ companion object {
+
+ fun getTonAmount(meansFee: BigDecimal, type: RechargePackType): BigDecimal {
+ return when (type) {
+ RechargePackType.LARGE -> meansFee.multiply(BigDecimal.valueOf(400))
+ RechargePackType.MEDIUM -> meansFee.multiply(BigDecimal.valueOf(250))
+ RechargePackType.SMALL -> meansFee.multiply(BigDecimal.valueOf(150))
+ }
+ }
+
+ fun getTonAmount(meansFee: String, type: RechargePackType): BigDecimal {
+ val meansFeeBigDecimal = BigDecimal(meansFee)
+ return getTonAmount(meansFeeBigDecimal, type)
+ }
+ }
+
+}
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/RechargePackType.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/RechargePackType.kt
new file mode 100644
index 000000000..13ff507ba
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/entity/RechargePackType.kt
@@ -0,0 +1,7 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.entity
+
+enum class RechargePackType {
+ LARGE,
+ MEDIUM,
+ SMALL
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/Adapter.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/Adapter.kt
new file mode 100644
index 000000000..072c86dca
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/Adapter.kt
@@ -0,0 +1,43 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.list
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.entity.RechargePackType
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.AddressHolder
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.AmountHolder
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.ButtonHolder
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.CustomAmountHolder
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.PromoHolder
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.RechargePackHolder
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder.SpaceHolder
+import com.tonapps.uikit.list.BaseListAdapter
+import com.tonapps.uikit.list.BaseListHolder
+import com.tonapps.uikit.list.BaseListItem
+
+class Adapter(
+ private val onAddressChange: (String) -> Unit,
+ private val openAddressBook: () -> Unit,
+ private val onAmountChange: (Double) -> Unit,
+ private val onPackSelect: (RechargePackType) -> Unit,
+ private val onCustomAmountSelect: () -> Unit,
+ private val onContinue: () -> Unit,
+ private val onSubmitPromo: (String) -> Unit,
+): BaseListAdapter() {
+ override fun createHolder(parent: ViewGroup, viewType: Int): BaseListHolder {
+ return when(viewType) {
+ Item.TYPE_RECHARGE_PACK -> RechargePackHolder(parent, onPackSelect)
+ Item.TYPE_CUSTOM_AMOUNT -> CustomAmountHolder(parent, onCustomAmountSelect)
+ Item.TYPE_SPACE -> SpaceHolder(parent)
+ Item.TYPE_AMOUNT -> AmountHolder(parent, onAmountChange)
+ Item.TYPE_ADDRESS -> AddressHolder(parent, onAddressChange, openAddressBook)
+ Item.TYPE_BUTTON -> ButtonHolder(parent, onContinue)
+ Item.TYPE_PROMO -> PromoHolder(parent, onSubmitPromo)
+ else -> throw IllegalArgumentException("Unknown view type: $viewType")
+ }
+ }
+
+ override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+ super.onAttachedToRecyclerView(recyclerView)
+ recyclerView.isNestedScrollingEnabled = true
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/Item.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/Item.kt
new file mode 100644
index 000000000..6d4aa616d
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/Item.kt
@@ -0,0 +1,70 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.list
+
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.entity.RechargePackType
+import com.tonapps.tonkeeper.ui.screen.battery.refill.entity.PromoState
+import com.tonapps.uikit.list.BaseListItem
+import com.tonapps.uikit.list.ListCell
+import com.tonapps.wallet.data.settings.BatteryTransaction
+
+sealed class Item(type: Int) : BaseListItem(type) {
+
+ companion object {
+ const val TYPE_RECHARGE_PACK = 0
+ const val TYPE_CUSTOM_AMOUNT = 1
+ const val TYPE_SPACE = 2
+ const val TYPE_AMOUNT = 3
+ const val TYPE_ADDRESS = 4
+ const val TYPE_BUTTON = 5
+ const val TYPE_PROMO = 6
+ }
+
+ data class RechargePack(
+ val position: ListCell.Position,
+ val packType: RechargePackType,
+ val charges: Int,
+ val formattedAmount: CharSequence,
+ val formattedFiatAmount: CharSequence,
+ val batteryLevel: Float,
+ val isEnabled: Boolean,
+ val selected: Boolean,
+ val transactions: Map
+ ) : Item(TYPE_RECHARGE_PACK)
+
+ data class CustomAmount(
+ val position: ListCell.Position,
+ val selected: Boolean,
+ ) : Item(TYPE_CUSTOM_AMOUNT)
+
+ data class Amount(
+ val symbol: String,
+ val formattedRemaining: CharSequence,
+ val isInsufficientBalance: Boolean,
+ val isLessThanMin: Boolean,
+ val formattedCharges: CharSequence,
+ ) : Item(TYPE_AMOUNT)
+
+ data class Address(
+ val loading: Boolean,
+ val error: Boolean
+ ) : Item(TYPE_ADDRESS)
+
+ data class Button(
+ val isEnabled: Boolean,
+ ) : Item(TYPE_BUTTON)
+
+ data class Promo(
+ val promoState: PromoState,
+ ) : Item(TYPE_PROMO) {
+
+ val appliedPromo: String
+ get() = (promoState as? PromoState.Applied)?.appliedPromo ?: ""
+
+ val isLoading: Boolean
+ get() = promoState is PromoState.Loading
+
+ val isError: Boolean
+ get() = promoState is PromoState.Error
+ }
+
+ data object Space : Item(TYPE_SPACE)
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/AddressHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/AddressHolder.kt
new file mode 100644
index 000000000..68fcbd15d
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/AddressHolder.kt
@@ -0,0 +1,41 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import com.tonapps.tonkeeper.extensions.clipboardText
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.Item
+import com.tonapps.tonkeeperx.R
+import uikit.widget.InputView
+import uikit.widget.RowLayout
+
+class AddressHolder(
+ parent: ViewGroup,
+ private val onTextChange: (String) -> Unit,
+ private val openAddressBook: () -> Unit,
+) : Holder(parent, R.layout.fragment_recharge_address) {
+
+ private val inputView = itemView.findViewById(R.id.address)
+ private val addressActionsView = itemView.findViewById(R.id.address_actions)
+ private val pasteView = itemView.findViewById(R.id.paste)
+ private val addressBookView = itemView.findViewById(R.id.address_book)
+
+ override fun onBind(item: Item.Address) {
+ inputView.doOnTextChange = { text ->
+ onTextChange(text)
+ inputView.loading = text.isNotBlank()
+ addressActionsView.visibility = if (text.isBlank()) View.VISIBLE else View.GONE
+ }
+ inputView.loading = item.loading
+ inputView.error = item.error
+
+ addressBookView.setOnClickListener {
+ openAddressBook()
+ }
+
+ pasteView.setOnClickListener {
+ inputView.text = context.clipboardText()
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/AmountHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/AmountHolder.kt
new file mode 100644
index 000000000..67ae66081
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/AmountHolder.kt
@@ -0,0 +1,46 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder
+
+import android.view.ViewGroup
+import androidx.appcompat.widget.AppCompatTextView
+import com.tonapps.tonkeeper.ui.component.coin.CoinEditText
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.Item
+import com.tonapps.tonkeeperx.R
+import com.tonapps.uikit.color.accentRedColor
+import com.tonapps.uikit.color.textSecondaryColor
+import com.tonapps.wallet.localization.Localization
+
+class AmountHolder(
+ parent: ViewGroup,
+ private val onValueChange: (Double) -> Unit
+) : Holder(parent, R.layout.fragment_recharge_amount) {
+
+ private val amountView = itemView.findViewById(R.id.amount)
+ private val currencyView = itemView.findViewById(R.id.currency)
+ private val availableView = itemView.findViewById(R.id.available)
+
+ override fun onBind(item: Item.Amount) {
+ amountView.doOnValueChange = onValueChange
+ amountView.suffix = item.symbol
+ currencyView.text = item.formattedCharges
+ applyAvailable(item.formattedRemaining, item.isInsufficientBalance, item.isLessThanMin)
+ }
+
+
+ private fun applyAvailable(
+ formattedRemaining: CharSequence,
+ isInsufficientBalance: Boolean,
+ isLessThanMin: Boolean
+ ) {
+ if (isInsufficientBalance) {
+ availableView.setText(Localization.insufficient_balance)
+ availableView.setTextColor(context.accentRedColor)
+ } else if (isLessThanMin) {
+ availableView.text = getString(Localization.insufficient_balance)
+ availableView.setTextColor(context.textSecondaryColor)
+ } else {
+ availableView.text =
+ context.getString(Localization.remaining_balance, formattedRemaining)
+ availableView.setTextColor(context.textSecondaryColor)
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/ButtonHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/ButtonHolder.kt
new file mode 100644
index 000000000..5bc793797
--- /dev/null
+++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/recharge/list/holder/ButtonHolder.kt
@@ -0,0 +1,21 @@
+package com.tonapps.tonkeeper.ui.screen.battery.recharge.list.holder
+
+import android.view.ViewGroup
+import android.widget.Button
+import com.tonapps.tonkeeper.ui.screen.battery.recharge.list.Item
+import com.tonapps.tonkeeperx.R
+
+class ButtonHolder(
+ parent: ViewGroup,
+ private val onContinue: () -> Unit,
+) : Holder(parent, R.layout.fragment_recharge_button) {
+
+ private val buttonView = itemView.findViewById