Updated on 2026-08-14

This commit is contained in:
Tangem 2022-09-22 13:41:10 +04:00
commit b484d1e08f
41 changed files with 978 additions and 758 deletions

View file

@ -13,6 +13,7 @@ import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
@ -26,7 +27,7 @@ import com.tangem.tap.domain.configurable.config.Config
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage
@ -41,11 +42,11 @@ val store = Store(
)
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
class TapApplication : Application(), ImageLoaderFactory {
@ -62,14 +63,19 @@ class TapApplication : Application(), ImageLoaderFactory {
DomainLayer.init()
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
currenciesRepository = CurrenciesRepository(this, store.state.domainNetworks.tangemTechService)
walletConnectRepository = WalletConnectRepository(this)
val configLoader = FeaturesLocalLoader(AndroidAssetReader(this), MoshiConverter.defaultMoshi())
assetReader = AndroidAssetReader(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.defaultMoshi())
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
BlockchainSdkRetrofitBuilder.enableNetworkLogging = LogConfig.network.blockchainSdkNetwork
userTokensRepository = UserTokensRepository.init(
context = this,
tangemTechService = store.state.domainNetworks.tangemTechService,
)
}
override fun newImageLoader(): ImageLoader {

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common
import android.content.Context
import com.tangem.tap.common.extensions.readFile
import com.tangem.tap.common.extensions.rewriteFile
interface FileReader {
fun readFile(fileName: String): String
fun rewriteFile(content: String, fileName: String)
}
class AndroidFileReader(private val context: Context) : FileReader {
override fun readFile(fileName: String): String {
return context.readFile(fileName)
}
override fun rewriteFile(content: String, fileName: String) {
context.rewriteFile(content, fileName)
}
}

View file

@ -12,7 +12,6 @@ import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
@ -25,6 +24,7 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -141,7 +141,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
val result = tangemSdkManager.scanProduct(
store.state.globalState.analyticsHandlers,
currenciesRepository,
userTokensRepository,
action.additionalBlockchainsToDerive,
action.messageResId,
)

View file

@ -32,7 +32,7 @@ import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
@ -45,7 +45,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler?,
currenciesRepository: CurrenciesRepository,
userTokensRepository: UserTokensRepository,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
@ -53,8 +53,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(null, currenciesRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message,
).also { sendScanResultsToAnalytics(analyticsHandler, it) }
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
import com.tangem.network.api.tangemTech.TangemTechError
import com.tangem.wallet.R
interface TapErrors
@ -66,7 +67,6 @@ sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemE
object CardNotSupportedByRelease : TapSdkError(R.string.error_update_app)
}
fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
val idList = mutableListOf<Pair<Int, List<Any>?>>()
when (this) {
@ -74,4 +74,13 @@ fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
is TapError -> idList.add(Pair(this.messageResource, this.args))
}
return idList
}
}
fun TangemTechError.toTapError(): TapError {
return when (this.code) {
404 -> NoDataError(this.description)
else -> TapError.CustomError(customMessage = this.description)
}
}
class NoDataError(message: String) : TapError.CustomError(customMessage = message)

View file

@ -16,7 +16,6 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
@ -24,23 +23,21 @@ import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
by lazy { WalletManagerFactory(blockchainSdkConfig) }
val rates: RatesRepository = RatesRepository()
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler =
ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
@ -65,16 +62,16 @@ class TapWalletManager {
WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManager.NoAccountError).customMessage
)
(result.error as TapError.WalletManager.NoAccountError).customMessage,
),
)
}
else -> {
dispatchOnMain(
WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
)
result.error.localizedMessage,
),
)
}
}
@ -139,22 +136,52 @@ class TapWalletManager {
}
private suspend fun loadMultiWalletData(
scanResponse: ScanResponse
scanResponse: ScanResponse,
) {
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
)
if (savedCurrencies.isEmpty()) return
when (val tokensResult = userTokensRepository.getUserTokens(scanResponse.card)) {
is Result.Success -> {
withMainContext {
val blockchainNetworks = tokensResult.data.toBlockchainNetworks()
val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, tokensResult.data)
store.dispatch(
WalletAction.MultiWallet.AddBlockchains(
blockchains = blockchainNetworks,
walletManagers = walletManagers,
save = false,
),
)
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies)
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
if (it.tokens.isNotEmpty()) {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
blockchainNetworks.filter { it.tokens.isNotEmpty() }
.map {
store.dispatch(
WalletAction.MultiWallet.AddTokens(
tokens = it.tokens,
blockchain = it,
save = false,
),
)
}
checkIfDerivationsAreMissing(blockchainNetworks, scanResponse)
}
}
is Result.Failure -> {
return
}
}
}
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<BlockchainNetwork>, scanResponse: ScanResponse) {
blockchainNetworks.map {
if (it.tokens.isNotEmpty()) {
WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
}
}
val missingDerivations = blockchainNetworks
.filter {
it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath)
}
if (missingDerivations.isNotEmpty()) {
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
}
}
@ -172,9 +199,10 @@ class TapWalletManager {
}
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
listOf(primaryWalletManager)
)
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
walletManagers = listOf(primaryWalletManager),
save = false,
),
)
}
}

View file

@ -1,6 +1,10 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
@ -99,9 +103,11 @@ fun WalletManagerFactory.makeWalletManagerForApp(
}
fun WalletManagerFactory.makeWalletManagersForApp(
scanResponse: ScanResponse, blockchains: List<BlockchainNetwork>,
scanResponse: ScanResponse, blockchains: List<Currency>,
): List<WalletManager> {
return blockchains.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
return blockchains
.filter { it.isBlockchain() }
.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
}
fun WalletManagerFactory.makePrimaryWalletManager(

View file

@ -32,16 +32,14 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import kotlinx.coroutines.launch
class ScanProductTask(
val card: Card? = null,
private val currenciesRepository: CurrenciesRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
private val userTokensRepository: UserTokensRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
) : CardSessionRunnable<ScanResponse> {
override fun run(
@ -62,7 +60,7 @@ class ScanProductTask(
val commandProcessor = when {
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive)
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
}
commandProcessor.proceed(card, session) { processorResult ->
when (processorResult) {
@ -111,8 +109,8 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
}
private class ScanWalletProcessor(
private val currenciesRepository: CurrenciesRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
private val userTokensRepository: UserTokensRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null
@ -184,47 +182,41 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
scope.launch {
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
primaryCard = primaryCard
)
)
)
return@launch
}
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
primaryCard = primaryCard,
),
),
)
return
}
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard
)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard,
)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
private fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val userTokensRepository = userTokensRepository ?: return emptyList()
val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card),
@ -236,7 +228,7 @@ private class ScanWalletProcessor(
listOf(
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card),
)
),
)
}
if (additionalBlockchainsToDerive != null) {
@ -256,16 +248,14 @@ private class ScanWalletProcessor(
return blockchainsToDerive.distinct()
}
private suspend fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
private fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
val blockchains = getBlockchainsToDerive(card)
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
blockchains.forEach { blockchain ->
val curve = blockchain.blockchain.getPrimaryCurve()
val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
if (wallet.chainCode == null) return@forEach
val key = wallet.publicKey.toMapKey()
val path = blockchain.derivationPath?.let { DerivationPath(it) }
if (path != null) {

View file

@ -1,247 +1,12 @@
package com.tangem.tap.domain.tokens
import android.app.Application
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.extensions.appendIf
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.ObsoleteTokenDao
import com.tangem.tap.domain.tokens.models.TokenDao
import com.tangem.tap.features.demo.DemoHelper
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.util.*
class CurrenciesRepository(
private val context: Application,
private val tangemNetworkService: TangemTechService
) {
private val moshi = MoshiConverter.defaultMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java)
)
private val obsoleteTokensAdapter: JsonAdapter<List<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, ObsoleteTokenDao::class.java)
)
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
moshi.adapter(CurrenciesFromJson::class.java)
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
fun saveUpdatedCurrency(cardId: String, blockchainNetwork: BlockchainNetwork) {
var changed = false
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
if (it == blockchainNetwork) {
changed = true
blockchainNetwork
} else {
it
}
}
val updatedCurrencies = if (changed) currencies else currencies + blockchainNetwork
saveCurrencies(cardId, updatedCurrencies.distinct())
}
fun removeToken(cardId: String, token: Token, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
if (it == blockchainNetwork) {
it.copy(tokens = it.tokens.filterNot { it == token })
} else {
it
}
}
saveCurrencies(cardId, currencies)
}
fun removeBlockchain(cardId: String, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrenciesWithoutMigration(cardId)
.filterNot { it == blockchainNetwork }
saveCurrencies(cardId, currencies)
}
fun removeCurrencies(cardId: String) {
saveCurrencies(cardId, emptyList())
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedTokens(cardId: String): List<TokenDao> {
val json = try {
context.readFileText(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptyList()
}
return try {
tokensAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
suspend fun loadSavedCurrencies(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies()
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
}
}
fun loadSavedCurrenciesWithoutMigration(
cardId: String,
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies()
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
private fun loadDemoCurrencies(): List<BlockchainNetwork> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList()
)
}
}
private suspend fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
return try {
loadSavedCurrenciesOldWay(
cardId,
isHdWalletSupported
)
} catch (exception: Exception) {
emptyList()
}
}
private suspend fun loadSavedCurrenciesOldWay(
cardId: String, isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val ids = getTokensIds(tokens)
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = tokens
.filter { it.blockchainDao.toBlockchain() == blockchain }
.map {
val token = it.toToken()
token.copy(id = ids[token.contractAddress])
}
)
}
saveCurrencies(cardId, blockchainNetworks) // migrate saved currencies
return blockchainNetworks
}
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
tokens.map {
async {
tangemNetworkService.getTokens(
contractAddress = it.contractAddress,
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
active = true,
)
}
}.map { it.await() }
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
.mapIndexedNotNull { index, id ->
if (id == null) null else tokens[index].contractAddress to id
}.toMap()
}
fun saveCurrencies(cardId: String, currencies: List<BlockchainNetwork>) {
val json = blockchainNetworkAdapter.toJson(currencies)
context.rewriteFile(json, getFileNameForBlockchains(cardId))
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
private fun Context.rewriteFile(content: String, fileName: String) {
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
}
}
fun getTestnetCoins(): List<Currency> {
val json = context.assets.readJsonFileToString(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun loadTokensJson(blockchain: Blockchain): String? {
val fileName = getFileName(blockchain)
return try {
context.assets.readJsonFileToString(fileName)
} catch (ex: Exception) {
Timber.e(ex, "Tokens with the file name %s not found", fileName)
null
}
}
private fun getFileName(blockchain: Blockchain): String {
return StringBuilder().apply {
append(blockchain.id.lowercase(Locale.getDefault()).replace("/test", ""))
append("_tokens")
appendIf("_testnet") { blockchain.isTestnet() }
}.toString()
}
private fun fromJsonToTokensDao(tokenJson: String, blockchain: Blockchain): List<TokenDao> {
return obsoleteTokensAdapter.fromJson(tokenJson)!!.map { it.toTokenDao(blockchain) }
}
object CurrenciesRepository {
fun getBlockchains(
cardFirmware: FirmwareVersion,
isTestNet: Boolean = false
isTestNet: Boolean = false,
): List<Blockchain> {
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
Blockchain.secp256k1Blockchains(isTestNet)
@ -261,21 +26,6 @@ class CurrenciesRepository(
)
}
}
companion object {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
}
}
fun Blockchain.getTokensName(): String {
return when (this) {
Blockchain.Fantom -> "Fantom Opera"
else -> this.fullName
}
}

View file

@ -1,29 +1,36 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getListOfCoins
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AssetReader
class LoadAvailableCoinsService(
private val networkService: TangemTechService,
private val currenciesRepository: CurrenciesRepository
private val assetReader: AssetReader,
) {
private val moshi: Moshi by lazy { MoshiConverter.defaultMoshi() }
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
moshi.adapter(CurrenciesFromJson::class.java)
suspend fun getSupportedTokens(
isTestNet: Boolean = false,
supportedBlockchains: List<Blockchain>,
page: Int,
searchInput: String? = null
searchInput: String? = null,
): Result<LoadedCoins> {
if (isTestNet) {
return Result.Success(
LoadedCoins(
currencies = currenciesRepository.getTestnetCoins().filter(searchInput),
currencies = getTestnetCoins().filter(searchInput),
moreAvailable = false,
)
),
)
}
val offset = page * LOAD_PER_PAGE
@ -56,21 +63,28 @@ class LoadAvailableCoinsService(
active = true,
offset = offset,
limit = LOAD_PER_PAGE,
searchText = searchInput
searchText = searchInput,
)
}
fun getTestnetCoins(): List<Currency> {
val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun List<Currency>.filter(searchInput: String?): List<Currency> {
if (searchInput.isNullOrBlank()) return this
return filter{
return filter {
it.symbol.contains(searchInput, ignoreCase = true) ||
it.name.contains(searchInput, ignoreCase = true)
it.name.contains(searchInput, ignoreCase = true)
}
}
companion object {
const val LOAD_PER_PAGE = 100
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
}
}

View file

@ -0,0 +1,130 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.TokenDao
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@Deprecated("Use this only for migration")
class OldUserTokensRepository(
private val fileReader: FileReader,
private val tangemNetworkService: TangemTechService,
) {
private val moshi = MoshiConverter.defaultMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java),
)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java),
)
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedTokens(cardId: String): List<TokenDao> {
val json = try {
fileReader.readFile(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptyList()
}
return try {
tokensAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use TokensRepository instead")
suspend fun loadSavedCurrencies(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
return try {
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
}
}
private suspend fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
return try {
loadSavedCurrenciesOldWay(
cardId,
isHdWalletSupported,
)
} catch (exception: Exception) {
emptyList()
}
}
private suspend fun loadSavedCurrenciesOldWay(
cardId: String, isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val ids = getTokensIds(tokens)
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = tokens
.filter { it.blockchainDao.toBlockchain() == blockchain }
.map {
val token = it.toToken()
token.copy(id = ids[token.contractAddress])
},
)
}
return blockchainNetworks
}
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
tokens.map {
async {
tangemNetworkService.getTokens(
contractAddress = it.contractAddress,
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
active = true,
)
}
}.map { it.await() }
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
.mapIndexedNotNull { index, id ->
if (id == null) null else tokens[index].contractAddress to id
}.toMap()
}
companion object {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
private fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.tokens
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.features.wallet.models.Currency
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> {
return when (val result = tangemTechService.getUserTokens(userId)) {
is Result.Success -> result
is Result.Failure -> {
val error = result.error
if (error is TangemSdkError.NetworkError && error.customMessage.contains("404")) {
return Result.Failure(NoDataError(error.customMessage))
} else {
return result
}
}
}
}
suspend fun saveUserTokens(userId: String, tokens: List<Currency>): Result<Unit> {
val tokensResponse = tokens.map { it.toTokenResponse() }
val data = UserTokensResponse(tokens = tokensResponse)
return tangemTechService.putUserTokens(userId, data)
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val networkService: UserTokensNetworkService,
) {
suspend fun getUserTokens(card: Card): Result<List<Currency>> {
if (DemoHelper.isDemoCardId(card.cardId)) {
return Result.Success(loadDemoCurrencies())
}
val userId = card.getUserId()
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
return Result.Success(loadTokensOffline(card, userId))
}
return when (val networkResult = networkService.getUserTokens(userId)) {
is Result.Success -> {
val tokens = networkResult.data.tokens.map { Currency.fromTokenResponse(it) }
storageService.saveUserTokens(card.getUserId(), tokens)
Result.Success(tokens)
}
is Result.Failure -> {
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
}
}
}
suspend fun saveUserTokens(card: Card, tokens: List<Currency>) {
networkService.saveUserTokens(card.getUserId(), tokens)
storageService.saveUserTokens(card.getUserId(), tokens)
}
suspend fun removeUserTokens(card: Card) {
networkService.saveUserTokens(card.getUserId(), emptyList())
storageService.saveUserTokens(card.getUserId(), emptyList())
}
fun loadBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
return storageService.getUserTokens(card.getUserId())?.toBlockchainNetworks() ?: emptyList()
}
private fun loadDemoCurrencies(): List<Currency> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList(),
)
}.flatMap { it.toCurrencies() }
}
private suspend fun handleGetUserTokensFailure(
card: Card,
userId: String,
error: Throwable,
): Result<List<Currency>> {
return when (error) {
is NoDataError -> {
val tokens = storageService.getUserTokens(card)
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = tokens) } }
Result.Success(tokens)
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
Result.Success(tokens)
}
}
}
private suspend fun loadTokensOffline(card: Card, userId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
private fun Card.getUserId(): String {
val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
return calculateUserId(walletPublicKey)
}
private fun calculateUserId(walletPublicKey: ByteArray): String {
val message = MESSAGE.toByteArray()
val keyHash = walletPublicKey.calculateSha256()
return message.calculateHmacSha256(keyHash).toHexString()
}
companion object {
const val MESSAGE = "AccountID"
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
val fileReader = AndroidFileReader(context)
val oldUserTokensRepository = OldUserTokensRepository(
fileReader, store.state.domainNetworks.tangemTechService,
)
val storageService = UserTokensStorageService(oldUserTokensRepository, fileReader)
val networkService = UserTokensNetworkService(tangemTechService)
return UserTokensRepository(storageService, networkService)
}
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
import com.tangem.common.card.Card
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
class UserTokensStorageService(
private val oldUserTokensRepository: OldUserTokensRepository,
private val fileReader: FileReader,
) {
private val moshi = MoshiConverter.defaultMoshi()
private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
moshi.adapter(UserTokensResponse::class.java)
fun getUserTokens(userId: String): List<Currency>? {
return try {
val json = fileReader.readFile(getFileNameForUserTokens(userId))
userTokensAdapter.fromJson(json)?.tokens?.map { Currency.fromTokenResponse(it) }
} catch (exception: Exception) {
Log.error { exception.stackTraceToString() }
null
}
}
@Deprecated("")
suspend fun getUserTokens(card: Card): List<Currency> {
val blockchainNetworks =
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
return blockchainNetworks.flatMap { it.toCurrencies() }
}
fun saveUserTokens(userId: String, tokens: List<Currency>) {
val tokensResponse = tokens.map { it.toTokenResponse() }
val data = UserTokensResponse(tokens = tokensResponse)
val json = userTokensAdapter.toJson(data)
fileReader.rewriteFile(json, getFileNameForUserTokens(userId))
}
companion object {
private const val FILE_NAME_PREFIX_USER_TOKENS = "user_tokens"
private fun getFileNameForUserTokens(userId: String): String = "${FILE_NAME_PREFIX_USER_TOKENS}_$userId"
}
}

View file

@ -1,64 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.wallet.redux.WalletState
class WcWalletManagerFactory(
private val factory: WalletManagerFactory,
private val currenciesRepository: CurrenciesRepository,
) {
fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState,
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = wallet.derivationPath?.rawPath,
tokens = emptyList(),
)
return walletState.getWalletManager(blockchainNetwork)
}
suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState,
): WalletManager? {
val card = scanResponse.card
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card,
)
return if (walletState.cardId == card.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork,
)
} else {
null
}
}
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
@ -110,11 +109,9 @@ class DetailsMiddleware {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val result = tangemSdkManager.resetToFactorySettings(card)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
currenciesRepository.removeCurrencies(card.cardId)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
@ -125,7 +122,6 @@ class DetailsMiddleware {
)
}
}
}
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
@ -12,12 +13,11 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.domain.walletconnect.WcWalletManagerFactory
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
@ -189,15 +189,10 @@ class WalletConnectMiddleware {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository,
)
val walletState = store.state.walletState
val walletManager = factory.getWalletManager(
val walletManager = getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = walletState,
walletState = store.state.walletState,
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -233,20 +228,10 @@ class WalletConnectMiddleware {
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
}
private suspend fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
return if (walletState.cardId == card.cardId) {
walletState.currencies.filter {
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
}.map { it.blockchain }
} else {
currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.filter {
it.blockchain.isEvm() &&
it.derivationPath == it.blockchain.derivationPath(card.derivationStyle)?.rawPath
}
.map { it.blockchain }
}
private fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
return walletState.currencies.filter {
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
}.map { it.blockchain }
}
private suspend fun prepareWalletManager(
@ -256,11 +241,7 @@ class WalletConnectMiddleware {
session: WalletConnectSession,
walletConnectManager: WalletConnectManager,
) {
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository,
)
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
val walletManager = getWalletManager(session.wallet, blockchain, walletState).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -309,17 +290,27 @@ class WalletConnectMiddleware {
NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain),
),
)
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
),
)
}
scope.launch {
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
withMainContext {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
),
)
}
private fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState,
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = wallet.derivationPath?.rawPath,
tokens = emptyList(),
)
return walletState.getWalletManager(blockchainNetwork)
}
}

View file

@ -78,30 +78,35 @@ private fun handleReadCard() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard(onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(
GlobalAction.ScanCard(
onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo)
} else {
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo)
} else {
scope.launch {
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
}
store.onCardScanned(scanResponse)
}
}
}
}
}, onFailure = {
changeButtonState(ButtonState.ENABLED)
}))
},
onFailure = {
changeButtonState(ButtonState.ENABLED)
},
),
)
}
}

View file

@ -18,7 +18,6 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.twins.TwinCardsManager
@ -271,7 +270,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
CreateTwinWalletMode.RecreateWallet -> {
currenciesRepository.removeCurrencies(scanResponse.card.cardId)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}

View file

@ -94,8 +94,8 @@ private fun handleWalletAction(action: Action) {
store.dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks,
cardId = result.data.card.cardId
)
card = result.data.card,
),
)
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingWalletAction.ProceedBackup)
@ -273,15 +273,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
val blockchainNetworks = listOf(
BlockchainNetwork(Blockchain.Bitcoin, result.data),
BlockchainNetwork(Blockchain.Ethereum, result.data)
)
store.dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks, cardId = result.data.cardId
)
)
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup)
} else {

View file

@ -19,7 +19,8 @@ import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.assetReader
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -32,6 +33,9 @@ import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -77,7 +81,7 @@ class TokensMiddleware {
val loadCoinsService = LoadAvailableCoinsService(
store.state.domainNetworks.tangemTechService,
currenciesRepository
assetReader,
)
scope.launch {
@ -285,22 +289,28 @@ class TokensMiddleware {
else -> DerivationParams.Custom(derivationPath)
}
}
val walletManager = factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = currency.blockchain,
derivationParams = derivationParams
derivationParams = derivationParams,
) ?: return@mapNotNull null
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, walletManager)
WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork,
walletManager = walletManager,
save = true,
)
}
is Currency.Token -> {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork =
BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork)
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
WalletAction.MultiWallet.AddToken(
token = currency.token,
blockchain = blockchainNetwork,
save = true,
)
}
}
}

View file

@ -1,14 +1,18 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.network.api.tangemTech.TokenResponse
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
sealed interface Currency {
val coinId: String?
get() = when (this) {
is Blockchain -> blockchain.toCoinId()
@ -17,24 +21,28 @@ sealed interface Currency {
val blockchain: com.tangem.blockchain.common.Blockchain
val currencySymbol: CryptoCurrencyName
val derivationPath: String?
val currencyName: String
get() = when (this) {
is Blockchain -> blockchain.fullName
is Token -> token.name
}
val decimals
get() = when (this) {
is Blockchain -> blockchain.decimals()
is Token -> token.decimals
}
data class Token(
val token: com.tangem.blockchain.common.Token,
override val blockchain: com.tangem.blockchain.common.Blockchain,
override val derivationPath: String?
override val derivationPath: String?,
) : Currency {
override val currencySymbol = token.symbol
}
data class Blockchain(
override val blockchain: com.tangem.blockchain.common.Blockchain,
override val derivationPath: String?
override val derivationPath: String?,
) : Currency {
override val currencySymbol: CryptoCurrencyName = blockchain.currency
}
@ -48,24 +56,35 @@ sealed interface Currency {
}
fun isBlockchain(): Boolean = this is Blockchain
fun isToken(): Boolean = this is Token
fun toTokenResponse(): TokenResponse {
return TokenResponse(
id = coinId ?: "",
networkId = blockchain.toNetworkId(),
derivationPath = derivationPath ?: DERIVATION_PATH_RAW_VALUE,
name = currencyName,
symbol = currencySymbol,
decimals = decimals,
contractAddress = if (this is Token) token.contractAddress else null,
)
}
companion object {
private const val DERIVATION_PATH_RAW_VALUE = "m/44/0'/0/0"
fun fromBlockchainNetwork(
blockchainNetwork: BlockchainNetwork,
token: com.tangem.blockchain.common.Token? = null
token: com.tangem.blockchain.common.Token? = null,
): Currency {
return if (token != null) {
Token(
token = token,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
derivationPath = blockchainNetwork.derivationPath,
)
} else {
Blockchain(
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
derivationPath = blockchainNetwork.derivationPath,
)
}
}
@ -74,7 +93,7 @@ sealed interface Currency {
return when (customCurrency) {
is CustomCurrency.CustomBlockchain -> Blockchain(
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath
derivationPath = customCurrency.derivationPath?.rawPath,
)
is CustomCurrency.CustomToken -> Token(
token = customCurrency.token,
@ -88,8 +107,53 @@ sealed interface Currency {
return Token(
token = tokenWithBlockchain.token,
blockchain = tokenWithBlockchain.blockchain,
derivationPath = null
derivationPath = null,
)
}
fun fromTokenResponse(tokenResponse: TokenResponse): Currency {
val derivationPath = if (tokenResponse.derivationPath == DERIVATION_PATH_RAW_VALUE) {
null
} else {
tokenResponse.derivationPath
}
return when {
tokenResponse.contractAddress != null -> Token(
com.tangem.blockchain.common.Token(
name = tokenResponse.name,
symbol = tokenResponse.symbol,
contractAddress = tokenResponse.contractAddress!!,
decimals = tokenResponse.decimals,
id = tokenResponse.id,
),
blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!,
derivationPath = derivationPath,
)
else -> Blockchain(
blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!,
derivationPath = derivationPath,
)
}
}
}
}
}
fun BlockchainNetwork.toCurrencies(): List<Currency> {
val blockchain = Currency.fromBlockchainNetwork(this)
val tokens = this.tokens.map { Currency.fromBlockchainNetwork(this, it) }
return listOf(blockchain) + tokens
}
fun List<BlockchainNetwork>.toCurrencies(): List<Currency> {
return flatMap { it.toCurrencies() }
}
fun List<Currency>.toBlockchainNetworks(): List<BlockchainNetwork> {
return this.filter { it.isBlockchain() }.map { BlockchainNetwork(it.blockchain, it.derivationPath, getTokens(it)) }
}
private fun List<Currency>.getTokens(currency: Currency): List<Token> {
return this
.filter { it.isToken() && it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
.mapNotNull { if (it is Currency.Token) it.token else null }
}

View file

@ -31,7 +31,6 @@ sealed class WalletAction : Action {
data class Failure(val error: TapError) : WalletAction()
}
data class LoadWallet(
val blockchain: BlockchainNetwork? = null,
val walletManager: WalletManager? = null
@ -54,22 +53,21 @@ sealed class WalletAction : Action {
data class AddBlockchain(
val blockchain: BlockchainNetwork,
val walletManager: WalletManager?
val walletManager: WalletManager?,
val save: Boolean,
) : MultiWallet()
data class AddBlockchains(
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>, val save: Boolean,
) : MultiWallet()
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork) :
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork, val save: Boolean) :
MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork, val save: Boolean) : MultiWallet()
data class SaveCurrencies(
val blockchainNetworks: List<BlockchainNetwork>, val cardId: String? = null
val blockchainNetworks: List<BlockchainNetwork>, val card: Card? = null,
) : MultiWallet()
// object FindTokensInUse : MultiWallet()
// object FindBlockchainsInUse : MultiWallet()
data class TokenLoaded(
val amount: Amount,
@ -82,14 +80,15 @@ sealed class WalletAction : Action {
data class TryToRemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallet(
val currency: Currency,
val fromScreen: AppScreen
val fromScreen: AppScreen,
) : MultiWallet()
data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet()
data class SetPrimaryToken(val token: Token) : MultiWallet()
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
object BackupWallet : MultiWallet()
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : MultiWallet()
object ScanToGetDerivations : MultiWallet()
}
sealed class Warnings : WalletAction() {

View file

@ -19,9 +19,6 @@ import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.models.hasSendableAmounts
import com.tangem.tap.features.wallet.models.isSendableAmount
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -47,6 +44,7 @@ data class WalletState(
val isTestnet: Boolean = false,
val totalBalance: TotalBalance? = null,
val showBackupWarning: Boolean = false,
val missingDerivations: List<BlockchainNetwork> = emptyList(),
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -131,32 +129,6 @@ data class WalletState(
return walletsData.find { it.currency == selectedCurrency }
}
fun canBeRemoved(walletData: WalletData?): Boolean {
if (walletData == null) return false
if (!isPrimaryCurrency(walletData)) {
val walletManager = getWalletManager(walletData.currency)
?: return true
if (walletData.currency is Currency.Blockchain &&
walletManager.cardTokens.isNotEmpty()
) {
return false
}
val wallet = walletManager.wallet
return when (walletData.currency) {
is Currency.Blockchain -> !wallet.hasPendingTransactions() && !wallet.hasSendableAmounts()
is Currency.Token -> {
val token = walletData.currency.token
!wallet.hasPendingTransactions(token) && !wallet.isSendableAmount(token)
}
}
}
return false
}
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
return (walletData.currency is Currency.Blockchain &&
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.safeUpdate
@ -11,18 +12,20 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.redux.reducers.toWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -44,41 +47,47 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.AddToken -> {
addTokens(listOf(action.token), action.blockchain, walletState, globalState)
addTokens(listOf(action.token), action.blockchain, walletState, globalState, action.save)
}
is WalletAction.MultiWallet.AddTokens -> {
addTokens(action.tokens, action.blockchain, walletState, globalState)
addTokens(action.tokens, action.blockchain, walletState, globalState, action.save)
}
is WalletAction.MultiWallet.AddBlockchain -> {
action.walletManager?.let {
handleAddingWalletManagers(globalState, listOf(action.walletManager))
}
val currencies: List<Currency> =
(walletState?.currencies ?: emptyList()) + action.blockchain.toCurrencies()
globalState.scanResponse?.let {
currenciesRepository.saveUpdatedCurrency(
cardId = it.card.cardId,
blockchainNetwork = action.blockchain
)
if (action.save && globalState.scanResponse != null) {
scope.launch {
userTokensRepository.saveUserTokens(
card = globalState.scanResponse.card,
tokens = currencies,
)
}
}
store.dispatch(
WalletAction.LoadFiatRate(
coinsList = listOf(
Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
)
)
)
action.blockchain.derivationPath,
),
),
),
)
store.dispatch(
WalletAction.LoadWallet(
action.blockchain, action.walletManager
)
action.blockchain, action.walletManager,
),
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val cardId = action.cardId ?: globalState.scanResponse?.card?.cardId ?: return
currenciesRepository.saveCurrencies(cardId, action.blockchainNetworks)
val card = action.card ?: globalState.scanResponse?.card ?: return
scope.launch { userTokensRepository.saveUserTokens(card, action.blockchainNetworks.toCurrencies()) }
}
is WalletAction.MultiWallet.TryToRemoveWallet -> {
val currency = action.currency
@ -89,54 +98,44 @@ class MultiWalletMiddleware {
}
if (currency.isBlockchain() && walletManager.cardTokens.isNotEmpty()) {
store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog(
currencyTitle = currency.currencyName,
currencySymbol = currency.currencySymbol
))
store.dispatchDialogShow(
WalletDialog.TokensAreLinkedDialog(
currencyTitle = currency.currencyName,
currencySymbol = currency.currencySymbol,
),
)
} else {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(
currency = currency,
fromScreen = AppScreen.WalletDetails
))
store.dispatch(NavigationAction.PopBackTo())
}
))
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
store.dispatch(
WalletAction.MultiWallet.RemoveWallet(
currency = currency,
fromScreen = AppScreen.WalletDetails,
),
)
store.dispatch(NavigationAction.PopBackTo())
},
),
)
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
val currency = action.currency
val cardId = globalState.scanResponse?.card?.cardId.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("cardId is NULL"))
val card = globalState.scanResponse?.card.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
when (currency) {
is Currency.Blockchain -> {
currenciesRepository.removeBlockchain(
cardId = cardId,
blockchainNetwork = BlockchainNetwork(
blockchain = currency.blockchain,
derivationPath = currency.derivationPath,
tokens = emptyList()
)
)
}
is Currency.Token -> {
val walletManager = walletState?.getWalletManager(currency)
if (walletManager != null) {
walletManager.removeToken(currency.token)
currenciesRepository.removeToken(
cardId = cardId,
token = currency.token,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
var currencies = walletState?.currencies ?: emptyList()
currencies = currencies.filterNot { it == currency }
if (currency.isBlockchain()) {
currencies
.filter { it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
}
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
if (action.fromScreen == AppScreen.AddTokens) {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
}
@ -148,94 +147,9 @@ class MultiWalletMiddleware {
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
}
}
// is WalletAction.MultiWallet.FindBlockchainsInUse -> {
// val scanResponse = globalState.scanResponse ?: return
// if (scanResponse.supportsHdWallet()) return
//
// val cardFirmware = scanResponse.card.firmwareVersion
// val blockchains = currenciesRepository.getBlockchains(cardFirmware)
// .filterNot { walletState?.blockchains?.contains(it) == true }
// .map { BlockchainNetwork(it, null, emptyList()) }
// val walletManagers =
// tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
// scanResponse,
// blockchains
// )
//
// scope.launch {
// walletManagers.map { walletManager ->
// async(Dispatchers.IO) {
// walletManager.safeUpdate()
// val wallet = walletManager.wallet
// val coinAmount = wallet.amounts[AmountType.Coin]?.value
// if (coinAmount != null && !coinAmount.isZero()) {
// scope.launch(Dispatchers.Main) {
// val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
// if (walletState?.getWalletData(blockchainNetwork) == null) {
// store.dispatch(WalletAction.MultiWallet.AddBlockchain(
// blockchainNetwork, walletManager
// ))
// store.dispatch(WalletAction.LoadWallet.Success(
// wallet = wallet,
// blockchain = blockchainNetwork
// ))
// }
// }
// }
// }
// }
// }
// }
// is WalletAction.MultiWallet.FindTokensInUse -> {
// val scanResponse = globalState.scanResponse ?: return
// if (scanResponse.supportsHdWallet()) return
//
// val walletFactory = tapWalletManager.walletManagerFactory
// val card = scanResponse.card
//
// val walletManager = walletState?.getWalletManager(
// Currency.Blockchain(Blockchain.Ethereum, null)
// )
// ?: walletFactory.makeWalletManagerForApp(
// scanResponse,
// Currency.Blockchain(Blockchain.Ethereum, null)
// )
//
// val tokenFinder = walletManager as? TokenFinder ?: return
// scope.launch {
// val result = tokenFinder.findTokens()
//
// withContext(Dispatchers.Main) {
// when (result) {
// is Result.Success -> {
// if (result.data.isNotEmpty()) {
// val blockchainNetwork = BlockchainNetwork(
// walletManager.wallet.blockchain,
// walletManager.wallet.publicKey.derivationPath?.rawPath,
// walletManager.cardTokens.toList()
// )
// currenciesRepository.saveUpdatedCurrency(
// card.cardId,
// blockchainNetwork
// )
// store.dispatch(
// WalletAction.MultiWallet.AddBlockchain(
// blockchainNetwork,
// walletManager
// )
// )
// store.dispatch(
// WalletAction.MultiWallet.AddTokens(
// walletManager.cardTokens.toList(),
// blockchainNetwork
// )
// )
// }
// }
// }
// }
// }
// }
is WalletAction.MultiWallet.ScanToGetDerivations -> {
store.dispatch(WalletAction.Scan)
}
}
}
@ -249,7 +163,7 @@ class MultiWalletMiddleware {
private fun handleAddingWalletManagers(
globalState: GlobalState,
walletManagers: List<WalletManager>
walletManagers: List<WalletManager>,
) {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(walletManagers)
if (globalState.scanResponse?.isDemoCard() == true) {
@ -259,50 +173,59 @@ class MultiWalletMiddleware {
private fun addTokens(
tokens: List<Token>, blockchainNetwork: BlockchainNetwork,
walletState: WalletState?, globalState: GlobalState?
walletState: WalletState?, globalState: GlobalState?,
save: Boolean,
) {
if (tokens.isEmpty()) return
val scanResponse = globalState?.scanResponse ?: return
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletState = walletState ?: return
val walletManager = walletState.getWalletManager(blockchainNetwork)?.also {
if (save) {
val wallets = tokens.mapNotNull { token -> token.toWallet(walletState, blockchainNetwork) }
val currencies = walletState.updateWalletsData(wallets).currencies
scope.launch { userTokensRepository.saveUserTokens(scanResponse.card, currencies) }
}
} ?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork.updateTokens(tokens), it, save))
}
val walletManager = walletState?.getWalletManager(blockchainNetwork)
?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, it))
} ?: return
store.dispatch(WalletAction.LoadFiatRate(coinsList = tokens.map { token ->
Currency.Token(
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
)
}))
if (tokens.isNotEmpty()) walletManager.addTokens(tokens)
currenciesRepository.saveUpdatedCurrency(
cardId = scanResponse.card.cardId,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
store.dispatch(
WalletAction.LoadFiatRate(
coinsList = tokens.map { token ->
Currency.Token(
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath,
)
},
),
)
if (tokens.isNotEmpty()) walletManager?.addTokens(tokens)
scope.launch {
when (val result = walletManager.safeUpdate()) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token ->
wallet.getTokenAmount(token)?.let { Pair(token, it) }
}
.forEach {
withContext(Dispatchers.Main) {
store.dispatch(
WalletAction.MultiWallet.TokenLoaded(
it.second,
it.first,
blockchainNetwork
)
)
val result = walletManager?.safeUpdate()
withMainContext {
when (result) {
is com.tangem.common.services.Result.Success -> {
val wallet = result.data
wallet.getTokens()
.filter { tokens.contains(it) }
.mapNotNull { token ->
wallet.getTokenAmount(token)?.let { Pair(token, it) }
}
}
.forEach {
withContext(Dispatchers.Main) {
store.dispatch(
WalletAction.MultiWallet.TokenLoaded(
it.second,
it.first,
blockchainNetwork,
),
)
}
}
}
else -> {}
}
is com.tangem.common.services.Result.Failure -> {}
}
}
}

View file

@ -108,10 +108,13 @@ class WalletMiddleware {
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState.getWalletData(action.blockchain) == null) {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
))
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null,
true,
),
)
store.dispatch(WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain

View file

@ -34,11 +34,10 @@ class MultiWalletReducer {
val walletManager = action.walletManagers.firstOrNull {
it.wallet.blockchain == blockchain.blockchain &&
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
} ?: return@mapNotNull null
val wallet = walletManager.wallet
}
val wallet = walletManager?.wallet
val cardToken = if (!state.isMultiwalletAllowed) {
wallet.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
} else {
null
}
@ -168,9 +167,11 @@ class MultiWalletReducer {
state.copy(primaryToken = action.token)
is WalletAction.MultiWallet.SaveCurrencies -> state
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(
showBackupWarning = action.show
showBackupWarning = action.show,
)
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(missingDerivations = action.blockchains)
is WalletAction.MultiWallet.BackupWallet -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state
}
}

View file

@ -145,18 +145,19 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
BalanceStatus.Loading
}
if (action.blockchain == null) {
val wallets = newState.wallets.map {
it.copy(
walletsData = it.walletsData.map { walletData ->
val wallets = newState.wallets.map { walletStore ->
walletStore.copy(
walletsData = walletStore.walletsData.map { walletData ->
walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus,
status =
if (walletStore.walletManager != null) balanceStatus else BalanceStatus.Unreachable,
currency = walletData.currencyData.currency,
currencySymbol = walletData.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
)
}
},
)
}
newState = newState.copy(

View file

@ -13,7 +13,7 @@ import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
@ -78,13 +78,14 @@ class MultiWalletView : WalletView() {
handleTotalBalance(binding, state.totalBalance)
handleBackupWarning(binding, state.showBackupWarning)
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
binding.btnAddToken.setOnClickListener {
val card = store.state.globalState.scanResponse!!.card
store.dispatch(
TokensAction.LoadCurrencies(
supportedBlockchains = currenciesRepository.getBlockchains(
supportedBlockchains = CurrenciesRepository.getBlockchains(
card.firmwareVersion,
card.isTestCard,
),
@ -113,6 +114,16 @@ class MultiWalletView : WalletView() {
}
}
private fun handleRescanWarning(
binding: FragmentWalletBinding,
showRescanWarning: Boolean,
) = with(binding.lWalletRescanWarning) {
root.isVisible = showRescanWarning
root.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.ScanToGetDerivations)
}
}
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="42dp"
android:autoMirrored="true"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:fillColor="#F4F5F5"
android:pathData="M21,21m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0" />
<group>
<clip-path android:pathData="M21,42C32.598,42 42,32.598 42,21C42,9.402 32.598,0 21,0C9.402,0 0,9.402 0,21C0,32.598 9.402,42 21,42Z" />
<path
android:fillColor="#0F1820"
android:pathData="M0,8L27,8A4,4 0,0 1,31 12L31,30A4,4 0,0 1,27 34L0,34A4,4 0,0 1,-4 30L-4,12A4,4 0,0 1,0 8z" />
<path
android:fillAlpha="0.3"
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M9.047,27H24.954C25.231,27 25.497,27.105 25.694,27.293C25.89,27.48 26,27.735 26,28C26,28.265 25.89,28.52 25.694,28.707C25.497,28.895 25.231,29 24.954,29H9.047C8.769,29 8.503,28.895 8.307,28.707C8.11,28.52 8,28.265 8,28C8,27.735 8.11,27.48 8.307,27.293C8.503,27.105 8.769,27 9.047,27Z" />
</group>
</vector>

View file

@ -93,17 +93,6 @@
app:barrierDirection="bottom"
app:constraint_referenced_ids="iv_card,tv_twin_card_number" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_warning_messages"
android:layout_width="match_parent"
@ -116,16 +105,47 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier" />
<LinearLayout
android:id="@+id/ll_warnings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages">
<include
android:id="@+id/l_wallet_rescan_warning"
layout="@layout/layout_wallet_rescan_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
</LinearLayout>
<include
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/l_wallet_backup_warning"
app:layout_constraintTop_toBottomOf="@id/ll_warnings"
app:layout_goneMarginTop="16dp" />
<androidx.recyclerview.widget.RecyclerView

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="14dp"
android:paddingEnd="16dp"
android:paddingBottom="14dp">
<ImageView
android:id="@+id/iv_rescan_warning_icon"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/shape_ellipse"
android:backgroundTint="@color/buttonGray"
android:contentDescription="@null"
android:paddingBottom="4dp"
android:scaleType="center"
android:src="@drawable/ic_scan_card"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_rescan_warning_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:text="@string/main_scan_card_warning_view_title"
android:textColor="@color/textBlack"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_rescan_warning_subtitle"
app:layout_constraintEnd_toStartOf="@id/iv_rescan_warning_end_icon"
app:layout_constraintStart_toEndOf="@id/iv_rescan_warning_icon"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_rescan_warning_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/main_scan_card_warning_view_subtitle"
android:textColor="@color/iconGray"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/tv_rescan_warning_title"
app:layout_constraintStart_toStartOf="@id/tv_rescan_warning_title"
app:layout_constraintTop_toBottomOf="@id/tv_rescan_warning_title" />
<ImageView
android:id="@+id/iv_rescan_warning_end_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_arrow_angle_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/darkGray1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -86,4 +86,6 @@
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
<string name="card_settings_reset_card_to_factory_footer">Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.</string>
<string name="wallet_connect_select_network">Select network</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
</resources>

View file

@ -86,4 +86,6 @@
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
<string name="card_settings_reset_card_to_factory_footer">Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.</string>
<string name="wallet_connect_select_network">Select network</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
</resources>

View file

@ -86,4 +86,6 @@
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
<string name="card_settings_reset_card_to_factory_footer">Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.</string>
<string name="wallet_connect_select_network">Select network</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
</resources>

View file

@ -102,4 +102,6 @@
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="warning_existential_deposit_message">Сеть %s использует концепцию экзистенциального депозита. Если баланс вашего счета опустится ниже %s %s, он будет деактивирован, а все оставшиеся средства будут уничтожены.</string>
<string name="main_scan_card_warning_view_title">Отсканируйте карту</string>
<string name="main_scan_card_warning_view_subtitle">Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту</string>
</resources>

View file

@ -99,4 +99,6 @@
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
<string name="card_settings_reset_card_to_factory_footer">Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.</string>
<string name="wallet_connect_select_network">Select network</string>
<string name="main_scan_card_warning_view_title">Scan your card</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
</resources>

View file

@ -0,0 +1,10 @@
package com.tangem.domain.common.extensions
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
fun ByteArray.calculateHmacSha256(key: ByteArray): ByteArray {
val mac: Mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(key, "HmacSHA256"))
return mac.doFinal(this)
}

View file

@ -46,5 +46,27 @@ data class CurrenciesResponse(val currencies: List<Currency>) {
}
data class GeoResponse(
val code: String
) : TangemTechResponse
val code: String,
) : TangemTechResponse
data class UserTokensResponse(
val version: Int = 0,
val group: String = "",
val sort: String = "",
val tokens: List<TokenResponse> = emptyList(),
) : TangemTechResponse
data class TokenResponse(
val id: String,
val networkId: String,
val derivationPath: String,
val name: String,
val symbol: String,
val decimals: Int,
val contractAddress: String?,
) : TangemTechResponse
data class TangemTechError(
val code: Int,
val description: String,
)

View file

@ -1,6 +1,9 @@
package com.tangem.network.api.tangemTech
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
/**
@ -29,4 +32,10 @@ interface TangemTechApi {
@GET("geo")
suspend fun geo(): GeoResponse
@GET("user-tokens/{user-id}")
suspend fun getUserTokens(@Path(value = "user-id") userId: String): UserTokensResponse
@PUT("user-tokens/{user-id}")
suspend fun putUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse)
}

View file

@ -58,6 +58,15 @@ class TangemTechService(
performRequest { api.currencies() }
}
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> = withContext(Dispatchers.IO) {
performRequest { api.getUserTokens(userId) }
}
suspend fun putUserTokens(userId: String, userTokens: UserTokensResponse): Result<Unit> =
withContext(Dispatchers.IO) {
performRequest { api.putUserTokens(userId, userTokens) }
}
fun addHeaderInterceptors(interceptors: List<AddHeaderInterceptor>) {
headerInterceptors.removeAll(interceptors)
headerInterceptors.addAll(interceptors)