diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt index 75b15b6db..0bc7bc5e1 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/Extensions.kt @@ -30,8 +30,8 @@ inline fun fromJSON(json: String): T { } fun withRetry( - times: Int = 3, - delay: Long = 300, + times: Int = 5, + delay: Long = 500, retryBlock: () -> R ): R? { var index = -1 @@ -47,7 +47,7 @@ fun withRetry( return null } catch (e: Throwable) { val statusCode = e.getHttpStatusCode() - if (statusCode == 429 || statusCode == 401 || statusCode == 502) { + if (statusCode == 429 || statusCode == 401 || statusCode == 502 || statusCode == 520) { SystemClock.sleep(delay) continue } 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 58f52d315..1f35d3e94 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 @@ -111,6 +111,10 @@ class AccountRepository( } } + fun addMnemonic(words: List) { + vaultSource.addMnemonic(words) + } + private suspend fun migrationFromRN() = withContext(Dispatchers.IO) { val (selectedId, wallets) = migrationHelper.loadLegacy() if (wallets.isNotEmpty()) { diff --git a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/DAppsRepository.kt b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/DAppsRepository.kt index 8b635533e..10a550bb0 100644 --- a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/DAppsRepository.kt +++ b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/DAppsRepository.kt @@ -59,16 +59,21 @@ class DAppsRepository( init { scope.launch(Dispatchers.IO) { - if (rnLegacy.isRequestMigration()) { - migrationFromLegacy() - } + try { + if (rnLegacy.isRequestMigration()) { + migrationFromLegacy() + } - val connections = database.getConnections() - if (connections.isEmpty()) { - migrationFromLegacy() - _connectionsFlow.value = database.getConnections() - } else { - _connectionsFlow.value = connections + val connections = database.getConnections() + if (connections.isEmpty()) { + migrationFromLegacy() + _connectionsFlow.value = database.getConnections() + } else { + _connectionsFlow.value = connections + } + } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) + _connectionsFlow.value = emptyList() } } @@ -250,7 +255,7 @@ class DAppsRepository( } } - private suspend fun migrationFromLegacy(connections: RNTCApps, testnet: Boolean) { + suspend fun migrationFromLegacy(connections: RNTCApps, testnet: Boolean) { val accountId = connections.address.toRawAddress() for (legacyApp in connections.apps) { val newApp = AppEntity( diff --git a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/source/DatabaseSource.kt b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/source/DatabaseSource.kt index 6a947240c..597ecb2c3 100644 --- a/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/source/DatabaseSource.kt +++ b/apps/wallet/data/dapps/src/main/java/com/tonapps/wallet/data/dapps/source/DatabaseSource.kt @@ -153,36 +153,41 @@ internal class DatabaseSource( prefs.remove(LAST_EVENT_ID_KEY) true } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) false } } suspend fun insertConnection(connection: AppConnectEntity) = withContext(coroutineContext) { - val prefix = prefixAccount(connection.accountId, connection.testnet) - - writableDatabase.delete(CONNECT_TABLE_NAME, "$CONNECT_TABLE_CLIENT_ID_COLUMN = ?", arrayOf(connection.clientId)) - encryptedPrefs.edit { - remove(prefixKeyPair(prefix, connection.clientId)) - remove(prefixProofSignature(prefix, connection.appUrl)) - remove(prefixProofPayload(prefix, connection.appUrl)) - } - - val values = ContentValues() - values.put(CONNECT_TABLE_APP_URL_COLUMN, connection.appUrl.withoutQuery.toString().removeSuffix("/")) - values.put(CONNECT_TABLE_ACCOUNT_ID_COLUMN, connection.accountId) - values.put(CONNECT_TABLE_TESTNET_COLUMN, if (connection.testnet) 1 else 0) - values.put(CONNECT_TABLE_CLIENT_ID_COLUMN, connection.clientId) - values.put(CONNECT_TABLE_TYPE_COLUMN, connection.type.value) - values.put(CONNECT_TABLE_TIMESTAMP_COLUMN, connection.timestamp) - writableDatabase.insertOrThrow(CONNECT_TABLE_NAME, null, values) + try { + val prefix = prefixAccount(connection.accountId, connection.testnet) + writableDatabase.delete(CONNECT_TABLE_NAME, "$CONNECT_TABLE_CLIENT_ID_COLUMN = ?", arrayOf(connection.clientId)) + encryptedPrefs.edit { + remove(prefixKeyPair(prefix, connection.clientId)) + remove(prefixProofSignature(prefix, connection.appUrl)) + remove(prefixProofPayload(prefix, connection.appUrl)) + } - encryptedPrefs.putParcelable(prefixKeyPair(prefix, connection.clientId), connection.keyPair) - if (connection.proofSignature != null) { - encryptedPrefs.putString(prefixProofSignature(prefix, connection.appUrl), connection.proofSignature) - } - if (connection.proofPayload != null) { - encryptedPrefs.putString(prefixProofPayload(prefix, connection.appUrl), connection.proofPayload) + val values = ContentValues() + values.put(CONNECT_TABLE_APP_URL_COLUMN, connection.appUrl.withoutQuery.toString().removeSuffix("/")) + values.put(CONNECT_TABLE_ACCOUNT_ID_COLUMN, connection.accountId) + values.put(CONNECT_TABLE_TESTNET_COLUMN, if (connection.testnet) 1 else 0) + values.put(CONNECT_TABLE_CLIENT_ID_COLUMN, connection.clientId) + values.put(CONNECT_TABLE_TYPE_COLUMN, connection.type.value) + values.put(CONNECT_TABLE_TIMESTAMP_COLUMN, connection.timestamp) + writableDatabase.insertOrThrow(CONNECT_TABLE_NAME, null, values) + + + encryptedPrefs.putParcelable(prefixKeyPair(prefix, connection.clientId), connection.keyPair) + if (connection.proofSignature != null) { + encryptedPrefs.putString(prefixProofSignature(prefix, connection.appUrl), connection.proofSignature) + } + if (connection.proofPayload != null) { + encryptedPrefs.putString(prefixProofPayload(prefix, connection.appUrl), connection.proofPayload) + } + } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) } } diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNException.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNException.kt new file mode 100644 index 000000000..352c3440b --- /dev/null +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNException.kt @@ -0,0 +1,17 @@ +package com.tonapps.wallet.data.rn + +sealed class RNException(message: String, cause: Throwable? = null) : Exception(message, cause) { + + data object EmptyChunks: RNException("wallets_chunks = 0") { + private fun readResolve(): Any = EmptyChunks + } + + data class NotFoundChunk(val chunk: Int): RNException("chunk $chunk not found") + + data class NotFoundMnemonic(val walletId: String): RNException("mnemonic for wallet $walletId not found") + + data object NotFoundPasscode: RNException("passcode not found") { + private fun readResolve(): Any = NotFoundPasscode + } + +} \ No newline at end of file diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNLegacy.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNLegacy.kt index 34dc62c32..11516b13c 100644 --- a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNLegacy.kt +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNLegacy.kt @@ -134,6 +134,10 @@ class RNLegacy( } } + suspend fun getVaultStateWithThrow(passcode: String): RNVaultState = withContext(Dispatchers.IO) { + seedStorage.getWithThrow(passcode) + } + fun getSpamTransactions(walletId: String): RNSpamTransactions { val key = keySpamTransactions(walletId) val json = sql.getJSONObject(key) ?: return RNSpamTransactions(walletId) diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt index 236c27e4b..2701da1a4 100644 --- a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/RNSeedStorage.kt @@ -121,4 +121,25 @@ internal class RNSeedStorage(context: Context) { val json = JSONObject(encryptedString) return SeedState(json) } + + suspend fun getWithThrow(passcode: String): RNVaultState = withContext(Dispatchers.IO) { + val state = readStateWithThrow() + val decrypted = ScryptBox.decrypt(passcode, state) + val json = JSONObject(decrypted) + RNVaultState.of(json) + } + + private suspend fun readStateWithThrow(): SeedState { + val chunks = kv.getItemImpl("${walletsKey}_chunks")?.toIntOrNull() ?: 0 + if (0 >= chunks) { + throw RNException.EmptyChunks + } + val builder = StringBuilder() + for (i in 0 until chunks) { + val chunk = kv.getItemImpl("${walletsKey}_chunk_$i") ?: throw RNException.NotFoundChunk(i) + builder.append(chunk) + } + val json = JSONObject(builder.toString()) + return SeedState(json) + } } \ No newline at end of file diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNVaultState.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNVaultState.kt index 2e11f0ef4..c2b40e6f3 100644 --- a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNVaultState.kt +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNVaultState.kt @@ -29,6 +29,11 @@ data class RNVaultState( val string: String get() = toJSON().toString() + + fun getDecryptedData(walletId: String): RNDecryptedData? { + return keys[walletId] + } + fun list(): List { val list = mutableListOf() for (m in keys) { diff --git a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNWallets.kt b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNWallets.kt index 9e3cda94c..519b58feb 100644 --- a/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNWallets.kt +++ b/apps/wallet/data/rn/src/main/java/com/tonapps/wallet/data/rn/data/RNWallets.kt @@ -9,6 +9,9 @@ data class RNWallets( val lockScreenEnabled: Boolean ): RNData() { + val count: Int + get() = wallets.size + companion object { val empty = RNWallets( wallets = emptyList(), diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/AnalyticsHelper.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/AnalyticsHelper.kt index 489222645..0d03f5ea2 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/AnalyticsHelper.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/AnalyticsHelper.kt @@ -14,13 +14,18 @@ object AnalyticsHelper { } @UiThread - fun trackEvent(name: String) { - Aptabase.instance.trackEvent(name) + fun trackEvent(name: String, installId: String) { + Aptabase.instance.trackEvent(name, hashMapOf( + "firebase_user_id" to installId + )) } @UiThread - fun trackEventClickDApp(url: String) { - Aptabase.instance.trackEvent("click_dapp", hashMapOf(url to "url")) + fun trackEventClickDApp(url: String, installId: String) { + Aptabase.instance.trackEvent("click_dapp", hashMapOf( + url to "url", + "firebase_user_id" to installId + )) } private fun initAptabase( diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/AccountRepository.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/AccountRepository.kt new file mode 100644 index 000000000..d3da32f03 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/AccountRepository.kt @@ -0,0 +1,36 @@ +package com.tonapps.tonkeeper.extensions + +import com.tonapps.wallet.data.account.AccountRepository +import com.tonapps.wallet.data.rn.RNException +import com.tonapps.wallet.data.rn.RNLegacy +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.ton.api.pk.PrivateKeyEd25519 +import org.ton.mnemonic.Mnemonic +import uikit.navigation.NavigationActivity + +suspend fun AccountRepository.requestPrivateKey( + activity: NavigationActivity, + rnLegacy: RNLegacy, + walletId: String, +): PrivateKeyEd25519 = withContext(Dispatchers.IO) { + val privateKeyEd25519 = getPrivateKey(walletId) + if (privateKeyEd25519 != null) { + privateKeyEd25519 + } else { + val vaultState = rnLegacy.requestVault(activity) + val mnemonic = vaultState.getDecryptedData(walletId)?.mnemonic ?: throw RNException.NotFoundMnemonic(walletId) + val seed = Mnemonic.toSeed(splitMnemonic(mnemonic)) + PrivateKeyEd25519(seed) + } +} + +private fun splitMnemonic(mnemonic: String): List { + val words = if (mnemonic.contains(",")) { + mnemonic.split(",") + } else { + mnemonic.split(" ") + } + return words.map { it.trim() } +} + diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/RNLegacy.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/RNLegacy.kt new file mode 100644 index 000000000..182596f8d --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/RNLegacy.kt @@ -0,0 +1,39 @@ +package com.tonapps.tonkeeper.extensions + +import com.tonapps.wallet.data.passcode.dialog.PasscodeDialog +import com.tonapps.wallet.data.rn.RNException +import com.tonapps.wallet.data.rn.RNLegacy +import com.tonapps.wallet.data.rn.data.RNVaultState +import com.tonapps.wallet.data.rn.data.RNWallets +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import uikit.navigation.NavigationActivity + +suspend fun RNLegacy.requestVault( + activity: NavigationActivity +): RNVaultState = withContext(Dispatchers.IO) { + val wallets = getWallets() + if (wallets.count == 0) { + throw IllegalStateException("No wallets found") + } + val passcode = requestPasscode(activity, wallets) + getVaultStateWithThrow(passcode) +} + +private suspend fun RNLegacy.requestPasscode( + activity: NavigationActivity, + wallets: RNWallets +): String = withContext(Dispatchers.Main) { + val passcodeFromBiometry = if (wallets.biometryEnabled) { + exportPasscodeWithBiometry() + } else { + null + } + val passcode = if (!passcodeFromBiometry.isNullOrBlank()) { + passcodeFromBiometry + } else { + PasscodeDialog.request(activity) + } + + passcode ?: throw RNException.NotFoundPasscode +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt index 7fe9f36b6..277f29c4e 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt @@ -125,7 +125,7 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - AnalyticsHelper.trackEventClickDApp(args.url.toString()) + AnalyticsHelper.trackEventClickDApp(args.url.toString(), rootViewModel.installId) } private fun applyHost(url: String) { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainScreen.kt index 5478caf3a..87fe02a93 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainScreen.kt @@ -62,7 +62,7 @@ class BrowserMainScreen(wallet: WalletEntity): WalletContextScreen(R.layout.frag override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - AnalyticsHelper.trackEvent("browser_open") + AnalyticsHelper.trackEvent("browser_open", viewModel.installId) navigation?.setFragmentResultListener(COUNTRY_REQUEST_KEY) { bundle -> } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainViewModel.kt index 6f4cdec45..094e4a6b6 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/main/BrowserMainViewModel.kt @@ -22,6 +22,9 @@ class BrowserMainViewModel( val countryFlow = settings.getLocaleCountryFlow(api) + val installId: String + get() = settings.installId + fun setBottomScrolled(value: Boolean) { _childBottomScrolled.tryEmit(value) } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt index 666c1fd2e..079c4a41d 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt @@ -2,11 +2,14 @@ package com.tonapps.tonkeeper.ui.screen.dev import android.os.Bundle import android.view.View +import android.widget.Button +import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.tonapps.extensions.locale import com.tonapps.security.Security import com.tonapps.tonkeeper.core.DevSettings +import com.tonapps.tonkeeper.extensions.copyToClipboard import com.tonapps.tonkeeper.extensions.showToast import com.tonapps.tonkeeper.ui.base.BaseWalletScreen import com.tonapps.tonkeeper.ui.base.ScreenContext @@ -17,6 +20,7 @@ import com.tonapps.uikit.list.LinearLayoutManager import com.tonapps.uikit.list.ListCell import org.koin.androidx.viewmodel.ext.android.viewModel import uikit.base.BaseFragment +import uikit.dialog.alert.AlertDialog import uikit.widget.HeaderView import uikit.widget.item.ItemSwitchView @@ -27,6 +31,12 @@ class DevScreen: BaseWalletScreen(R.layout.fragment_dev, Scr private lateinit var iconsView: RecyclerView private lateinit var blurView: ItemSwitchView private lateinit var tonConnectLogsView: ItemSwitchView + private lateinit var importMnemonicAgainView: View + private lateinit var logView: View + private lateinit var logDataView: AppCompatEditText + private lateinit var logCopy: Button + private lateinit var importPasscodeView: View + private lateinit var importDAppsView: View override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -57,6 +67,58 @@ class DevScreen: BaseWalletScreen(R.layout.fragment_dev, Scr requireContext().showToast("Restart app to apply changes") } } + + importMnemonicAgainView = view.findViewById(R.id.import_mnemonic_again) + importMnemonicAgainView.setOnClickListener { importMnemonicAgain(false) } + importMnemonicAgainView.setOnLongClickListener { importMnemonicAgain(true); true } + + importPasscodeView = view.findViewById(R.id.import_passcode) + importPasscodeView.setOnClickListener { importPasscode() } + + importDAppsView = view.findViewById(R.id.import_dapps) + importDAppsView.setOnClickListener { importDApps() } + + logView = view.findViewById(R.id.log) + logDataView = view.findViewById(R.id.log_data) + + view.findViewById