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 77d009a6d..1f0e1127e 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 @@ -3,6 +3,8 @@ package com.tonapps.wallet.data.core.entity import android.os.Parcelable import android.util.Log import com.tonapps.blockchain.ton.TonNetwork +import com.tonapps.blockchain.ton.extensions.isValidTonAddress +import com.tonapps.blockchain.ton.extensions.toAccountId import kotlinx.datetime.Clock import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -13,7 +15,8 @@ import kotlin.time.Duration.Companion.seconds @Parcelize data class SignRequestEntity( - val fromValue: String?, + private val fromValue: String?, + private val sourceValue: String?, val validUntil: Long, val messages: List, val network: TonNetwork @@ -21,10 +24,18 @@ data class SignRequestEntity( @IgnoredOnParcel val from: AddrStd? - get() = fromValue?.let { AddrStd.parse(it) } + get() { + val value = fromValue ?: return null + return try { + AddrStd.parse(value) + } catch (e: Throwable) { + null + } + } constructor(json: JSONObject) : this( - fromValue = parseFrom(json), + fromValue = json.optString("from"), + sourceValue = json.optString("source"), validUntil = parseValidUnit(json), messages = parseMessages(json.getJSONArray("messages")), network = parseNetwork(json.opt("network")) @@ -34,6 +45,31 @@ data class SignRequestEntity( constructor(value: Any) : this(value.toString()) + class Builder { + private var from: AddrStd? = null + private var validUntil: Long? = null + private var network: TonNetwork = TonNetwork.MAINNET + private val messages = mutableListOf() + + fun setFrom(from: AddrStd) = apply { this.from = from } + + fun setValidUntil(validUntil: Long) = apply { this.validUntil = validUntil } + + fun setNetwork(network: TonNetwork) = apply { this.network = network } + + fun addMessage(message: RawMessageEntity) = apply { messages.add(message) } + + fun build(): SignRequestEntity { + return SignRequestEntity( + fromValue = from?.toAccountId(), + sourceValue = null, + validUntil = validUntil ?: 0, + messages = messages.toList(), + network = network + ) + } + } + companion object { fun parse(array: JSONArray): List { @@ -59,21 +95,18 @@ data class SignRequestEntity( val messages = mutableListOf() for (i in 0 until array.length()) { val json = array.getJSONObject(i) - messages.add(RawMessageEntity(json)) + val raw = RawMessageEntity(json) + if (0 >= raw.amount) { + throw IllegalArgumentException("Invalid amount: ${raw.amount}") + } + if (!raw.addressValue.isValidTonAddress()) { + throw IllegalArgumentException("Invalid address: ${raw.addressValue}") + } + messages.add(raw) } return messages } - private fun parseFrom(json: JSONObject): String? { - return if (json.has("from")) { - json.getString("from") - } else if (json.has("source")) { - json.getString("source") - } else { - null - } - } - private fun parseNetwork(value: Any?): TonNetwork { if (value == null) { return TonNetwork.MAINNET diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt index eaec62e61..ea673a342 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt @@ -3,6 +3,7 @@ package com.tonapps.tonkeeper.manager.tonconnect import android.content.Context import android.net.Uri import android.util.ArrayMap +import android.util.Log import androidx.core.net.toUri import com.tonapps.blockchain.ton.extensions.equalsAddress import com.tonapps.blockchain.ton.proof.TONProof @@ -61,9 +62,8 @@ class TonConnectManager( if (lastAppRequestId >= event.message.id) { return@mapNotNull null } - dAppsRepository.setLastAppRequestId(event.connection.clientId, event.message.id) event - }.shareIn(scope, SharingStarted.Eagerly) + }.shareIn(scope, SharingStarted.Eagerly, 1) val transactionRequestFlow = eventsFlow.mapNotNull { event -> if (event.method == BridgeMethod.SEND_TRANSACTION) { @@ -76,7 +76,7 @@ class TonConnectManager( } null } - }.flowOn(Dispatchers.IO).shareIn(scope, SharingStarted.Eagerly) + }.flowOn(Dispatchers.IO).shareIn(scope, SharingStarted.Eagerly, 1) fun walletConnectionsFlow(wallet: WalletEntity) = accountConnectionsFlow(wallet.accountId, wallet.testnet) @@ -84,6 +84,10 @@ class TonConnectManager( connection.testnet == testnet && connection.accountId.equalsAddress(accountId) } + fun setLastAppRequestId(clientId: String, messageId: Long) { + dAppsRepository.setLastAppRequestId(clientId, messageId) + } + fun walletAppsFlow(wallet: WalletEntity) = walletConnectionsFlow(wallet).mapList { it.appUrl }.map { it.distinct() }.map { urls -> dAppsRepository.getApps(urls) }.flowOn(Dispatchers.IO) @@ -119,10 +123,12 @@ class TonConnectManager( suspend fun sendBridgeError(connection: AppConnectEntity, error: BridgeError, id: Long) { bridge.sendError(connection, error, id) + setLastAppRequestId(connection.clientId, id) } suspend fun sendTransactionResponseSuccess(connection: AppConnectEntity, boc: String, id: Long) { bridge.sendTransactionResponseSuccess(connection, boc, id) + setLastAppRequestId(connection.clientId, id) } fun isPushEnabled(wallet: WalletEntity, appUrl: Uri): Boolean { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/bridge/Bridge.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/bridge/Bridge.kt index 953a6e4fd..b6abe07e6 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/bridge/Bridge.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/bridge/Bridge.kt @@ -48,27 +48,6 @@ internal class Bridge(private val api: API) { return message } - suspend fun sendConnectError( - connection: AppConnectEntity, - error: BridgeError - ): String { - val message = JsonBuilder.connectEventError(error).toString() - send(connection, message) - return message - } - - suspend fun sendConnectSuccess( - connection: AppConnectEntity, - wallet: WalletEntity, - proof: TONProof.Result?, - proofError: BridgeError?, - appVersion: String, - ): String { - val message = JsonBuilder.connectEventSuccess(wallet, proof, proofError, appVersion).toString() - send(connection, message) - return message - } - suspend fun sendDisconnect(connection: AppConnectEntity): String { val message = JsonBuilder.disconnectEvent().toString() send(connection, message) @@ -78,11 +57,11 @@ internal class Bridge(private val api: API) { suspend fun send( connection: AppConnectEntity, message: String - ) = withContext(Dispatchers.IO) { + ): Boolean = withContext(Dispatchers.IO) { if (connection.type != AppConnectEntity.Type.Internal) { - // val encrypted = connection.encryptMessage(message.toByteArray()) - // api.tonconnectSend(connection.publicKeyHex, connection.clientId, encrypted.base64) send(connection.clientId, connection.keyPair, message) + } else { + true } } @@ -90,12 +69,14 @@ internal class Bridge(private val api: API) { clientId: String, keyPair: CryptoBox.KeyPair, unencryptedMessage: String - ) = withContext(Dispatchers.IO) { + ): Boolean = withContext(Dispatchers.IO) { try { val encryptedMessage = AppConnectEntity.encryptMessage(clientId.hex(), keyPair.privateKey, unencryptedMessage.toByteArray()) api.tonconnectSend(hex(keyPair.publicKey), clientId, encryptedMessage.base64) + true } catch (e: Throwable) { FirebaseCrashlytics.getInstance().recordException(e) + false } } 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 index 90a7095df..38253a100 100644 --- 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 @@ -334,19 +334,18 @@ class BatteryRechargeViewModel( } 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, - ) + val request = SignRequestEntity.Builder() + .setFrom(wallet.contract.address) + .setValidUntil(validUntil) + .addMessage(RawMessageEntity( + addressValue = fundReceiver, + amount = amount.toLong(), + stateInitValue = null, + payloadValue = payload.base64() + )) + .setNetwork(network) + .build() + _eventFlow.tryEmit(BatteryRechargeEvent.Sign(request, forceRelayer)) } else { val queryId = TransferEntity.newWalletQueryId() @@ -364,19 +363,19 @@ class BatteryRechargeViewModel( forwardPayload = payload, customPayload = customPayload?.customPayload ) - 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, - ) + + val request = SignRequestEntity.Builder() + .setFrom(wallet.contract.address) + .setValidUntil(validUntil) + .addMessage(RawMessageEntity( + addressValue = token.balance.walletAddress, + amount = Coins.of(0.1).toLong(), + stateInitValue = null, + payloadValue = jettonPayload.base64() + )) + .setNetwork(network) + .build() + _eventFlow.tryEmit(BatteryRechargeEvent.Sign(request, forceRelayer)) } }.catch { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/step/WordsScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/step/WordsScreen.kt index cc9708e10..da5e011dd 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/step/WordsScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/step/WordsScreen.kt @@ -87,7 +87,11 @@ class WordsScreen: BaseFragment(R.layout.fragment_init_words) { lifecycleScope.launch { val words = getMnemonic() if (words.isEmpty() || !Mnemonic.isValid(words)) { - navigation?.toast(Localization.incorrect_phrase) + if (TonMnemonic.isValidTONKeychain(words)) { + navigation?.toast(Localization.multi_account_secret_wrong) + } else { + navigation?.toast(Localization.incorrect_phrase) + } return@launch } setLoading() diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt index 871b29370..a20e2b3f8 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt @@ -3,15 +3,19 @@ package com.tonapps.tonkeeper.ui.screen.root import android.app.Application import android.net.Uri import android.os.Bundle +import android.util.Log import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.crashlytics.setCustomKeys import com.google.firebase.ktx.Firebase import com.tonapps.blockchain.ton.TonNetwork +import com.tonapps.blockchain.ton.extensions.equalsAddress +import com.tonapps.blockchain.ton.extensions.toAccountId import com.tonapps.extensions.MutableEffectFlow import com.tonapps.extensions.locale import com.tonapps.extensions.setLocales @@ -152,10 +156,14 @@ class RootViewModel( val eventId = message.id try { val signRequests = message.params.map { SignRequestEntity(it) } + if (signRequests.isEmpty()) { + throw IllegalArgumentException("Empty sign requests") + } for (signRequest in signRequests) { signRequest(eventId, connection, signRequest) } - } catch (e: Exception) { + } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) tonConnectManager.sendBridgeError(connection, BridgeError.BAD_REQUEST, eventId) } @@ -174,6 +182,12 @@ class RootViewModel( tonConnectManager.sendBridgeError(connection, BridgeError.METHOD_NOT_SUPPORTED, eventId) return } + + if (signRequest.from != null && !signRequest.from!!.toAccountId().equalsAddress(connection.accountId)) { + tonConnectManager.sendBridgeError(connection, BridgeError.BAD_REQUEST, eventId) + return + } + val wallets = accountRepository.getWalletsByAccountId( accountId = connection.accountId, testnet = connection.testnet diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/usecase/emulation/EmulationUseCase.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/usecase/emulation/EmulationUseCase.kt index 0829ed76f..0d018fde3 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/usecase/emulation/EmulationUseCase.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/usecase/emulation/EmulationUseCase.kt @@ -3,6 +3,7 @@ package com.tonapps.tonkeeper.usecase.emulation import com.tonapps.blockchain.ton.extensions.EmptyPrivateKeyEd25519 import com.tonapps.icu.Coins import com.tonapps.icu.Coins.Companion.sumOf +import com.tonapps.tonkeeper.extensions.toGrams import com.tonapps.tonkeeper.manager.assets.AssetsManager import com.tonapps.wallet.api.API import com.tonapps.wallet.api.entity.BalanceEntity @@ -18,6 +19,7 @@ import io.tonapi.models.JettonQuantity import io.tonapi.models.MessageConsequences import io.tonapi.models.Risk import org.ton.cell.Cell +import org.ton.contract.wallet.WalletTransfer import java.math.BigDecimal import kotlin.math.abs @@ -34,11 +36,16 @@ class EmulationUseCase( message: MessageBodyEntity, useBattery: Boolean = false, forceRelayer: Boolean = false, + params: Boolean = false, ): Emulated { return if (forceRelayer || useBattery) { - emulateWithBattery(message, forceRelayer) + emulateWithBattery( + message = message, + forceRelayer = forceRelayer, + params = params + ) } else { - emulate(message) + emulate(message, params) } } @@ -55,6 +62,7 @@ class EmulationUseCase( private suspend fun emulateWithBattery( message: MessageBodyEntity, forceRelayer: Boolean, + params: Boolean, ): Emulated { try { if (api.config.isBatteryDisabled) { @@ -75,17 +83,38 @@ class EmulationUseCase( return parseEmulated(wallet, consequences, withBattery) } catch (e: Throwable) { - return emulate(message) + return emulate(message, params) } } - private suspend fun emulate(message: MessageBodyEntity): Emulated { + private suspend fun emulate(message: MessageBodyEntity, params: Boolean): Emulated { val wallet = message.wallet val boc = createMessage(message, false) - val consequences = api.emulate(boc, wallet.testnet) ?: throw IllegalArgumentException("Emulation failed") + val consequences = (if (params) { + api.emulate( + cell = boc, + testnet = wallet.testnet, + address = wallet.address, + balance = (Coins.ONE + Coins.ONE).toLong() + calculateTransferAmount(message.transfers) + ) + } else { + api.emulate(boc, wallet.testnet) + }) ?: throw IllegalArgumentException("Emulation failed") return parseEmulated(wallet, consequences, false) } + + private fun calculateTransferAmount(transfers: List): Long { + if (transfers.isEmpty()) { + return 0 + } + var grams = Coins.ZERO.toGrams() + transfers.forEach { + grams = grams.plus(it.coins.coins) + } + return grams.amount.toLong() + } + private suspend fun parseEmulated( wallet: WalletEntity, consequences: MessageConsequences, diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/view/ChartPeriodView.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/view/ChartPeriodView.kt index 4005a41ff..2aab1ed69 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/view/ChartPeriodView.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/view/ChartPeriodView.kt @@ -55,8 +55,8 @@ class ChartPeriodView @JvmOverloads constructor( private fun createButton(period: ChartPeriod): View { val view = AppCompatTextView(context) - view.setTextColor(context.textPrimaryColor) view.setTextAppearance(uikit.R.style.TextAppearance_Label2) + view.setTextColor(context.textPrimaryColor) view.gravity = Gravity.CENTER view.text = period.title return view diff --git a/apps/wallet/instance/app/src/main/res/layout/activity_root.xml b/apps/wallet/instance/app/src/main/res/layout/activity_root.xml index eef952a46..c5da3a755 100644 --- a/apps/wallet/instance/app/src/main/res/layout/activity_root.xml +++ b/apps/wallet/instance/app/src/main/res/layout/activity_root.xml @@ -49,6 +49,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" + android:layout_marginHorizontal="@dimen/offsetLarge" android:visibility="gone"/> \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-ru/strings.xml b/apps/wallet/localization/src/main/res/values-ru/strings.xml index ccdcd3291..814c6f7fc 100644 --- a/apps/wallet/localization/src/main/res/values-ru/strings.xml +++ b/apps/wallet/localization/src/main/res/values-ru/strings.xml @@ -209,7 +209,7 @@ Открыть всё равно Вы уверены, что хотите открыть внешнюю ссылку? Вы открываете внешнее приложение, которым не управляет Tonkeeper. - + Пожалуйста, используйте Tonkeeper Desktop для акаунта мульти-кошелька diff --git a/apps/wallet/localization/src/main/res/values/strings.xml b/apps/wallet/localization/src/main/res/values/strings.xml index 5918de297..56fde7b6a 100644 --- a/apps/wallet/localization/src/main/res/values/strings.xml +++ b/apps/wallet/localization/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ W5 APY Transaction expired + Please use Tonkeeper Desktop for Multi-Wallet Account Total: %1$s %1$s ready. Tap co collect. %1$s will be deposited in %2$s diff --git a/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/TonMnemonic.kt b/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/TonMnemonic.kt index 49ce405d7..7ed5306ac 100644 --- a/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/TonMnemonic.kt +++ b/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/TonMnemonic.kt @@ -1,5 +1,7 @@ package com.tonapps.blockchain.ton +import com.tonapps.blockchain.ton.extensions.hmac_sha512 +import com.tonapps.blockchain.ton.extensions.pbkdf2_sha512 import org.ton.mnemonic.Mnemonic object TonMnemonic { @@ -18,6 +20,12 @@ object TonMnemonic { return list.all { mnemonicWords.contains(it) } } + fun isValidTONKeychain(list: List): Boolean { + val mnemonicHash = hmac_sha512("TON Keychain", list.joinToString(" ")) + val result = pbkdf2_sha512(mnemonicHash, "TON Keychain Version".toByteArray(), 1, 64) + return result.first() == 0.toByte() + } + fun parseMnemonic(value: String): List { if (3 >= value.length) { return emptyList() diff --git a/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/extensions/sha512.kt b/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/extensions/sha512.kt new file mode 100644 index 000000000..31aa7a392 --- /dev/null +++ b/lib/blockchain/src/main/java/com/tonapps/blockchain/ton/extensions/sha512.kt @@ -0,0 +1,28 @@ +package com.tonapps.blockchain.ton.extensions + +import org.ton.crypto.digest.Digest +import org.ton.crypto.kdf.PKCSS2ParametersGenerator +import org.ton.crypto.mac.hmac.HMac + +private val digestSha512: Digest by lazy { Digest.sha512() } + +fun hmac_sha512(key: String, data: String): ByteArray { + return hmac_sha512(key.toByteArray(), data.toByteArray()) +} + +fun hmac_sha512(key: ByteArray, data: ByteArray): ByteArray { + val hMac = HMac(digestSha512) + hMac.init(key) + hMac.update(data, 0, data.size) + return hMac.build() +} + +fun pbkdf2_sha512(key: ByteArray, salt: ByteArray, iterations: Int, keySize: Int): ByteArray { + val pbdkf2Sha512 = PKCSS2ParametersGenerator( + digest = digestSha512, + password = key, + salt = salt, + iterationCount = iterations + ) + return pbdkf2Sha512.generateDerivedParameters(keySize) +} \ No newline at end of file diff --git a/ui/uikit/core/src/main/java/uikit/base/BaseFragment.kt b/ui/uikit/core/src/main/java/uikit/base/BaseFragment.kt index 4103399ba..c88ce1020 100644 --- a/ui/uikit/core/src/main/java/uikit/base/BaseFragment.kt +++ b/ui/uikit/core/src/main/java/uikit/base/BaseFragment.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Parcelable import android.text.SpannableString +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -274,7 +275,7 @@ open class BaseFragment( } override fun onDestroy() { - if (resultKey != null && resultIsSet.get()) { + if (resultKey != null && !resultIsSet.get()) { navigation?.setFragmentResult(resultKey!!, Bundle()) } super.onDestroy()