Updated on 2026-08-14
This commit is contained in:
commit
fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions
|
|
@ -1,173 +0,0 @@
|
|||
package com.tangem.tap.domain.card
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.BackendId
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
|
||||
private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
|
||||
|
||||
internal class DefaultDerivationsRepository(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : DerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
|
||||
derivePublicKeysByNetworks(
|
||||
userWalletId = userWalletId,
|
||||
networks = networkIds.mapNotNull {
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
|
||||
val userWallet = withContext(dispatchers.io) {
|
||||
userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
}
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return
|
||||
}
|
||||
|
||||
userWallet.requireColdWallet()
|
||||
|
||||
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
|
||||
Timber.d("Nothing to derive")
|
||||
return
|
||||
}
|
||||
|
||||
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
|
||||
.findByNetworks(networks)
|
||||
.ifEmpty {
|
||||
Timber.d("Nothing to derive")
|
||||
return
|
||||
}
|
||||
|
||||
derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return false
|
||||
}
|
||||
|
||||
val derivations =
|
||||
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return derivations.isNotEmpty()
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
|
||||
// todo replace it in task [REDACTED_JIRA]
|
||||
val preflightReadFilter = UserWalletIdPreflightReadFilter(userWalletId)
|
||||
tangemSdkManager.derivePublicKeys(
|
||||
cardId = null,
|
||||
derivations = derivations,
|
||||
preflightReadFilter = preflightReadFilter,
|
||||
).doOnSuccess { response ->
|
||||
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
|
||||
.doOnSuccess {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
validateDerivations(scanResponse = it.requireColdWallet().scanResponse, derivations = derivations)
|
||||
return response.entries
|
||||
}
|
||||
.doOnFailure { throw it }
|
||||
}
|
||||
.doOnFailure { throw it }
|
||||
|
||||
error("This code should never be reached")
|
||||
}
|
||||
|
||||
/**
|
||||
* It throws an exception if any of the provided derivations are invalid
|
||||
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
|
||||
* It needs to be called after success [derivePublicKeys] or in same flows
|
||||
*/
|
||||
private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) {
|
||||
derivations.entries.forEach { derivationForKey ->
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key }
|
||||
if (wallet == null) return@forEach
|
||||
val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } }
|
||||
if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) {
|
||||
throw TangemSdkError.NonHardenedDerivationNotSupported()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult<UserWallet> {
|
||||
return withContext(dispatchers.io) {
|
||||
userWalletsStore.update(
|
||||
userWalletId = userWalletId,
|
||||
update = { userWallet -> userWallet.requireColdWallet().updateDerivedKeys(keys) }, // TODO [REDACTED_TASK_KEY]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet {
|
||||
return copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdatedDerivedKeys(oldKeys: DerivedKeys, newKeys: DerivedKeys): DerivedKeys {
|
||||
return (oldKeys.keys + newKeys.keys).toSet()
|
||||
.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package com.tangem.tap.domain.card
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
|
||||
|
||||
/**
|
||||
* Finder of missed derivations
|
||||
*
|
||||
* @property scanResponse scanning response
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
|
||||
|
||||
/** Find missed derivations for given currencies [currencies] */
|
||||
fun find(currencies: List<CryptoCurrency>): Derivations {
|
||||
return currencies.map { it.network }.let(::findByNetworks)
|
||||
}
|
||||
|
||||
fun findByNetworks(networks: List<Network>): Derivations {
|
||||
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
networks
|
||||
.mapToNewDerivations()
|
||||
.forEach { data ->
|
||||
val current = this[data.first]
|
||||
if (current != null) {
|
||||
current.addAll(data.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[data.first] = data.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
return mapNotNull { network ->
|
||||
val blockchain = network.toBlockchain()
|
||||
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
|
||||
|
||||
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNewDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
network: Network,
|
||||
): DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
val publicKey = wallet.publicKey.toMapKey()
|
||||
|
||||
val derivationCandidates = network
|
||||
.getDerivationCandidates(curve)
|
||||
.ifEmpty { return null }
|
||||
.filterAlreadyDerivedKeys(publicKey)
|
||||
.ifEmpty { return null }
|
||||
|
||||
return publicKey to derivationCandidates
|
||||
}
|
||||
|
||||
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
|
||||
val blockchain = this.toBlockchain()
|
||||
|
||||
return buildList {
|
||||
add(blockchain.getDerivationPath(curve = curve))
|
||||
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
|
||||
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
derivationPath(style = scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
network.derivationPath.value?.let(::DerivationPath)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
|
||||
return if (this == Blockchain.Cardano) {
|
||||
network.derivationPath.value?.let {
|
||||
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey)
|
||||
return filterNot(alreadyDerivedPaths::contains)
|
||||
}
|
||||
|
||||
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val extendedPublicKeysMap = scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
return extendedPublicKeysMap.keys.toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.features.hotwallet.HotWalletPasswordRequester
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.exception.WrongPasswordException
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
) {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> {
|
||||
val auth = when (hotWalletId.authType) {
|
||||
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
|
||||
HotWalletId.AuthType.Password -> requestPassword(false)
|
||||
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
|
||||
}
|
||||
|
||||
return runCatchingSdkErrors(hotWalletId, auth) {
|
||||
tangemHotSdk.signHashes(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = it,
|
||||
),
|
||||
dataToSign = dataToSign,
|
||||
).also {
|
||||
hotWalletPasswordRequester.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingSdkErrors(
|
||||
hotWalletId: HotWalletId,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T {
|
||||
return runCatchingWrongPassInternal(
|
||||
originalAuth = auth,
|
||||
auth = auth,
|
||||
block = { blockAuth ->
|
||||
block(blockAuth).also {
|
||||
// TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method
|
||||
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
|
||||
tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = blockAuth,
|
||||
),
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingWrongPassInternal(
|
||||
originalAuth: HotAuth,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T = runCatching {
|
||||
block(auth)
|
||||
}.getOrElse { exception ->
|
||||
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
|
||||
// fallback to password if biometry fails
|
||||
val passAuth = requestPassword(true)
|
||||
|
||||
return@getOrElse runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passAuth,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
if (exception !is WrongPasswordException) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
// If the exception is a wrong password, we need to request the password again
|
||||
|
||||
hotWalletPasswordRequester.wrongPassword()
|
||||
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
|
||||
|
||||
runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passResult,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
|
||||
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
|
||||
}
|
||||
|
||||
private fun Throwable.isBiometryError(): Boolean {
|
||||
return this is TangemSdkError.AuthenticationFailed ||
|
||||
this is TangemSdkError.AuthenticationCanceled ||
|
||||
this is TangemSdkError.AuthenticationLockout ||
|
||||
this is TangemSdkError.AuthenticationUnavailable ||
|
||||
this is TangemSdkError.AuthenticationAlreadyInProgress ||
|
||||
this is TangemSdkError.AuthenticationNotInitialized ||
|
||||
this is TangemSdkError.AuthenticationPermanentLockout
|
||||
}
|
||||
|
||||
private fun HotWalletPasswordRequester.Result.toAuth() = when (this) {
|
||||
HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry
|
||||
HotWalletPasswordRequester.Result.Dismiss -> null
|
||||
is HotWalletPasswordRequester.Result.EnteredPassword -> this.password
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
|
||||
interface HotWalletPasswordRequester {
|
||||
|
||||
suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
class TangemHotSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
class TangemHotWalletSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet =
|
||||
userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotWalletSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -20,13 +20,11 @@ import com.tangem.crypto.hdWallet.DerivationPath
|
|||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.operations.ScanTask
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
|
|
@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager(
|
|||
): CompletionResult<VisaSignedDataByCustomerWallet> {
|
||||
return runTaskAsyncReturnOnMain(
|
||||
runnable = VisaCustomerWalletApproveTask(
|
||||
visaDataForApprove = visaDataForApprove,
|
||||
VisaCustomerWalletApproveTask.Input(
|
||||
cardId = visaDataForApprove.customerWalletCardId,
|
||||
targetAddress = visaDataForApprove.targetAddress,
|
||||
hashToSign = visaDataForApprove.dataToSign.hashToSign,
|
||||
sign = visaDataForApprove.dataToSign::sign,
|
||||
),
|
||||
),
|
||||
cardId = visaDataForApprove.customerWalletCardId,
|
||||
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
|||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.operations.preflightread.PreflightReadFilter
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.tangem.common.map
|
|||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.CardTypesResolver
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
|
|||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ import com.tangem.common.tlv.Tlv
|
|||
import com.tangem.common.tlv.TlvDecoder
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.domain.tasks.visa
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
|
||||
import com.tangem.blockchain.common.UnmarshalHelper
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
|
|
@ -10,27 +11,24 @@ import com.tangem.common.core.CardSession
|
|||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toDecompressedPublicKey
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.core.error.ext.tangemError
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.model.VisaDataForApprove
|
||||
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
|
||||
import com.tangem.domain.visa.model.sign
|
||||
import com.tangem.operations.ScanTask
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
|
||||
import com.tangem.operations.sign.SignHashCommand
|
||||
|
||||
class VisaCustomerWalletApproveTask(
|
||||
private val visaDataForApprove: VisaDataForApprove,
|
||||
private val visaDataForApprove: Input,
|
||||
) : CardSessionRunnable<VisaSignedDataByCustomerWallet> {
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<VisaSignedDataByCustomerWallet>) {
|
||||
|
|
@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask(
|
|||
return
|
||||
}
|
||||
|
||||
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
|
||||
if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) {
|
||||
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
|
||||
return
|
||||
}
|
||||
|
|
@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
|
||||
private fun hashPersonalMessage(message: ByteArray): ByteArray {
|
||||
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
|
||||
return (prefix + message).toKeccak()
|
||||
}
|
||||
|
||||
private fun signApproveData(
|
||||
targetWalletPublicKey: ByteArray,
|
||||
derivationPath: DerivationPath?,
|
||||
|
|
@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask(
|
|||
session: CardSession,
|
||||
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
|
||||
) {
|
||||
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
|
||||
val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}"
|
||||
val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
|
||||
|
||||
val signTask = SignHashCommand(
|
||||
hash = hashToSign,
|
||||
hash = hash,
|
||||
walletPublicKey = targetWalletPublicKey,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
|
@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask(
|
|||
is CompletionResult.Success -> {
|
||||
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
|
||||
signature = result.data.signature,
|
||||
hash = hashToSign,
|
||||
hash = hash,
|
||||
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
|
||||
?: targetWalletPublicKey.toDecompressedPublicKey(),
|
||||
).asRSVLegacyEVM().toHexString().lowercase()
|
||||
|
|
@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask(
|
|||
scanCard(
|
||||
session = session,
|
||||
callback = callback,
|
||||
signedData = visaDataForApprove.dataToSign.sign(
|
||||
signature = rsvSignature,
|
||||
customerWalletAddress = visaDataForApprove.targetAddress,
|
||||
),
|
||||
signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress),
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Input(
|
||||
val cardId: String? = null,
|
||||
val targetAddress: String,
|
||||
val hashToSign: String,
|
||||
val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,14 +11,19 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.domain.models.scan.serialization.*
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
|
||||
|
|
@ -26,6 +31,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
|
|||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -40,6 +46,7 @@ internal object UserWalletsListManagerModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Deprecated("Use UserWalletsListRepository instead")
|
||||
fun provideGeneralUserWalletsListManager(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
|
|
@ -58,42 +65,14 @@ internal object UserWalletsListManagerModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use UserWalletsListRepository instead")
|
||||
private fun createBiometricUserWalletsListManager(
|
||||
applicationContext: Context,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): UserWalletsListManager {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(WalletDerivedKeysMapAdapter())
|
||||
.add(ScanResponseDerivedKeysMapAdapter())
|
||||
.add(ByteArrayKeyAdapter())
|
||||
.add(ExtendedPublicKeysMapAdapter())
|
||||
.add(CardBackupStatusAdapter())
|
||||
.add(DerivationPathAdapterWithMigration())
|
||||
.add(TangemSdkAdapter.DateAdapter())
|
||||
.add(TangemSdkAdapter.DerivationNodeAdapter())
|
||||
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
|
||||
.add(VisaActivationRemoteState.jsonAdapter)
|
||||
.add(VisaCardActivationStatus.jsonAdapter)
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
|
||||
val secureStorage = AndroidSecureStorage(
|
||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||
context = applicationContext,
|
||||
storageName = "user_wallets_storage",
|
||||
),
|
||||
androidSecureStorageV2 = AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = true,
|
||||
name = "user_wallets_storage2",
|
||||
),
|
||||
androidSecureStorageV3 = AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = false,
|
||||
name = "user_wallets_storage3",
|
||||
),
|
||||
)
|
||||
val moshi = buildMoshi()
|
||||
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
|
||||
|
||||
val authenticatedStorage = AuthenticatedStorage(
|
||||
secureStorage = UserWalletsKeysStoreDecorator(
|
||||
|
|
@ -134,4 +113,97 @@ internal object UserWalletsListManagerModule {
|
|||
selectedUserWalletRepository = selectedUserWalletRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserWalletsListRepository(
|
||||
@ApplicationContext applicationContext: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
passwordRequester: HotWalletPasswordRequester,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
): UserWalletsListRepository {
|
||||
val moshi = buildMoshi()
|
||||
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
|
||||
|
||||
val authenticatedStorage = AuthenticatedStorage(
|
||||
secureStorage = UserWalletsKeysStoreDecorator(
|
||||
featureStorage = secureStorage,
|
||||
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
|
||||
),
|
||||
keystoreManager = DelegatedKeystoreManager(
|
||||
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
|
||||
),
|
||||
)
|
||||
|
||||
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
|
||||
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
|
||||
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
|
||||
secureStorage = secureStorage,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
val userWalletEncryptionKeysRepository = UserWalletEncryptionKeysRepository(
|
||||
moshi = moshi,
|
||||
authenticatedStorage = authenticatedStorage,
|
||||
dispatchers = dispatchers,
|
||||
secureStorage = secureStorage,
|
||||
)
|
||||
|
||||
return DefaultUserWalletsListRepository(
|
||||
publicInformationRepository = publicInformationRepository,
|
||||
sensitiveInformationRepository = sensitiveInformationRepository,
|
||||
selectedUserWalletRepository = selectedUserWalletRepository,
|
||||
passwordRequester = passwordRequester,
|
||||
userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository,
|
||||
tangemSdkManagerProvider = Provider { tangemSdkManager },
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
|
||||
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
|
||||
)
|
||||
}
|
||||
|
||||
fun buildMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(WalletDerivedKeysMapAdapter())
|
||||
.add(ScanResponseDerivedKeysMapAdapter())
|
||||
.add(ByteArrayKeyAdapter())
|
||||
.add(ExtendedPublicKeysMapAdapter())
|
||||
.add(CardBackupStatusAdapter())
|
||||
.add(DerivationPathAdapterWithMigration())
|
||||
.add(TangemSdkAdapter.DateAdapter())
|
||||
.add(TangemSdkAdapter.DerivationNodeAdapter())
|
||||
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
|
||||
.add(VisaActivationRemoteState.jsonAdapter)
|
||||
.add(VisaCardActivationStatus.jsonAdapter)
|
||||
.addLast(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
|
||||
return AndroidSecureStorage(
|
||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||
context = applicationContext,
|
||||
storageName = "user_wallets_storage",
|
||||
),
|
||||
androidSecureStorageV2 = AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = true,
|
||||
name = "user_wallets_storage2",
|
||||
),
|
||||
androidSecureStorageV3 = AndroidSecureStorageV2(
|
||||
appContext = applicationContext,
|
||||
useStrongBox = false,
|
||||
name = "user_wallets_storage3",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.common.map
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.wallets.R
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.domain.core.wallets.error.DeleteWalletError
|
||||
import com.tangem.domain.core.wallets.error.LockWalletsError
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.core.wallets.error.SelectWalletError
|
||||
import com.tangem.domain.core.wallets.error.SetLockError
|
||||
import com.tangem.domain.core.wallets.error.UnlockWalletError
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
||||
import com.tangem.tap.domain.userWalletList.utils.lock
|
||||
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
|
||||
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultUserWalletsListRepository(
|
||||
private val publicInformationRepository: UserWalletsPublicInformationRepository,
|
||||
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
|
||||
private val selectedUserWalletRepository: SelectedUserWalletRepository,
|
||||
private val passwordRequester: HotWalletPasswordRequester,
|
||||
private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository,
|
||||
private val tangemSdkManagerProvider: Provider<TangemSdkManager>,
|
||||
private val savePersistentInformation: ProviderSuspend<Boolean>,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
) : UserWalletsListRepository {
|
||||
|
||||
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
|
||||
override val selectedUserWallet = MutableStateFlow<UserWallet?>(null)
|
||||
|
||||
override suspend fun load() {
|
||||
if (userWallets.value != null) return
|
||||
|
||||
if (savePersistentInformation().not()) {
|
||||
// If we don't save persistent information, we don't need to load user wallets
|
||||
// and we should clear any existing data
|
||||
clearPersistentData()
|
||||
userWallets.value = emptyList()
|
||||
return
|
||||
}
|
||||
|
||||
val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
|
||||
|
||||
publicInformationRepository.getAll()
|
||||
.map { it.toUserWallets() }
|
||||
.flatMap { wallets ->
|
||||
sensitiveInformationRepository.getAll(unsecuredEncryptionKeys)
|
||||
.map { wallets.updateWith(it) }
|
||||
}.doOnSuccess {
|
||||
userWallets.value = it
|
||||
}
|
||||
|
||||
val selectedUserWalletId = selectedUserWalletRepository.get()
|
||||
selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId }
|
||||
?: userWallets.value?.firstOrNull()
|
||||
}
|
||||
|
||||
override suspend fun userWalletsSync(): List<UserWallet> {
|
||||
load()
|
||||
return requireNotNull(userWallets.value) {
|
||||
"This should never happen"
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun selectedUserWalletSync(): UserWallet? {
|
||||
load()
|
||||
return selectedUserWallet.value
|
||||
}
|
||||
|
||||
override suspend fun select(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> = either {
|
||||
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
|
||||
?: raise(SelectWalletError.UnableToSelectUserWallet)
|
||||
selectedUserWalletRepository.set(userWalletId)
|
||||
selectedUserWallet.value = userWallet
|
||||
userWallet
|
||||
}
|
||||
|
||||
override suspend fun saveWithoutLock(
|
||||
userWallet: UserWallet,
|
||||
canOverride: Boolean,
|
||||
): Either<SaveWalletError, UserWallet> = either {
|
||||
if (canOverride.not() && userWallets.value?.any { it.walletId == userWallet.walletId } == true) {
|
||||
raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved))
|
||||
}
|
||||
|
||||
if (savePersistentInformation()) {
|
||||
publicInformationRepository.save(userWallet, canOverride)
|
||||
if (userWallet.isLocked.not()) {
|
||||
sensitiveInformationRepository.save(userWallet, userWallet.encryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// update the userWallets state and add if it doesn't exist
|
||||
userWallets.update { currentWallets ->
|
||||
val wallets = currentWallets ?: emptyList()
|
||||
if (wallets.any { it.walletId == userWallet.walletId }) {
|
||||
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
|
||||
} else {
|
||||
wallets + userWallet
|
||||
}
|
||||
}
|
||||
|
||||
// update the selectedUserWallet state if it is the only wallet
|
||||
if (userWallets.value?.size == 1) {
|
||||
selectedUserWalletRepository.set(userWallet.walletId)
|
||||
selectedUserWallet.value = userWallet
|
||||
}
|
||||
|
||||
userWallet
|
||||
}
|
||||
|
||||
override suspend fun setLock(
|
||||
userWalletId: UserWalletId,
|
||||
lockMethod: LockMethod,
|
||||
changeUnsecured: Boolean,
|
||||
): Either<SetLockError, Unit> = either {
|
||||
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
|
||||
?: raise(SetLockError.UserWalletNotFound)
|
||||
|
||||
val encryptionKey = userWallet.encryptionKey
|
||||
?: raise(SetLockError.UserWalletLocked)
|
||||
|
||||
runCatching {
|
||||
userWalletEncryptionKeysRepository.save(
|
||||
encryptionKey = UserWalletEncryptionKey(
|
||||
walletId = userWalletId,
|
||||
encryptionKey = encryptionKey,
|
||||
),
|
||||
removeUnsecured = changeUnsecured,
|
||||
method = when (lockMethod) {
|
||||
is LockMethod.AccessCode -> {
|
||||
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
|
||||
}
|
||||
LockMethod.Biometric -> {
|
||||
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
|
||||
}
|
||||
LockMethod.NoLock -> {
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
raise(SetLockError.UserWalletNotFound)
|
||||
}
|
||||
|
||||
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
|
||||
}
|
||||
},
|
||||
)
|
||||
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
|
||||
}
|
||||
|
||||
override suspend fun removeBiometricLock(userWalletId: UserWalletId) {
|
||||
userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit> = either {
|
||||
if (userWalletIds.isEmpty()) return Unit.right()
|
||||
|
||||
publicInformationRepository.delete(userWalletIds)
|
||||
.doOnFailure {
|
||||
raise(DeleteWalletError.UnableToDelete)
|
||||
}
|
||||
sensitiveInformationRepository.delete(userWalletIds)
|
||||
.doOnFailure {
|
||||
raise(DeleteWalletError.UnableToDelete)
|
||||
}
|
||||
|
||||
userWalletEncryptionKeysRepository.delete(userWalletIds)
|
||||
|
||||
val userWalletsBeforeDelete = userWallets.value ?: return@either
|
||||
|
||||
userWallets.update { currentWallets ->
|
||||
currentWallets?.filterNot { it.walletId in userWalletIds }
|
||||
}
|
||||
|
||||
selectedUserWallet.update { currentSelected ->
|
||||
if (currentSelected == null) return@update null
|
||||
|
||||
userWallets.value?.findAvailableUserWallet(
|
||||
userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unlock(
|
||||
userWalletId: UserWalletId,
|
||||
unlockMethod: UserWalletsListRepository.UnlockMethod,
|
||||
): Either<UnlockWalletError, Unit> = either {
|
||||
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
|
||||
?: raise(UnlockWalletError.UserWalletNotFound)
|
||||
|
||||
if (userWallet.isLocked.not()) {
|
||||
raise(UnlockWalletError.AlreadyUnlocked)
|
||||
}
|
||||
|
||||
when (unlockMethod) {
|
||||
UserWalletsListRepository.UnlockMethod.Biometric -> {
|
||||
unlockAllWallets().bind()
|
||||
select(userWalletId)
|
||||
}
|
||||
UserWalletsListRepository.UnlockMethod.AccessCode -> {
|
||||
if (userWallet !is UserWallet.Hot) {
|
||||
raise(UnlockWalletError.UnableToUnlock)
|
||||
}
|
||||
|
||||
val encryptionKey = requestPasswordRecursive(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
block = { password ->
|
||||
runCatching {
|
||||
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
|
||||
}.onFailure {
|
||||
raise(UnlockWalletError.UnableToUnlock)
|
||||
}.getOrNull()
|
||||
},
|
||||
biometryFallback = {
|
||||
unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
|
||||
},
|
||||
).bind()
|
||||
|
||||
if (encryptionKey == null) {
|
||||
return@either
|
||||
}
|
||||
|
||||
removePasswordAttempts(userWallet)
|
||||
|
||||
sensitiveInformationRepository.getAll(listOf(encryptionKey))
|
||||
.doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } }
|
||||
.doOnFailure { error ->
|
||||
raise(UnlockWalletError.UnableToUnlock)
|
||||
}
|
||||
}
|
||||
UserWalletsListRepository.UnlockMethod.Scan -> {
|
||||
if (userWallet !is UserWallet.Cold) {
|
||||
raise(UnlockWalletError.UnableToUnlock)
|
||||
}
|
||||
|
||||
tangemSdkManagerProvider().scanProduct()
|
||||
.doOnSuccess { scanResponse ->
|
||||
val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||
|
||||
if (expectedId != userWallet.walletId) {
|
||||
raise(UnlockWalletError.ScannedCardWalletNotMatched)
|
||||
}
|
||||
|
||||
saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true)
|
||||
.mapLeft { UnlockWalletError.UnableToUnlock }
|
||||
.bind()
|
||||
}
|
||||
.doOnFailure {
|
||||
raise(UnlockWalletError.UserCancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
|
||||
val userWalletIds = userWalletsSync().map { it.walletId }.toSet()
|
||||
val biometricKeys = runCatching {
|
||||
userWalletEncryptionKeysRepository.getAllBiometric()
|
||||
}.getOrElse {
|
||||
// TODO handle error properly [REDACTED_TASK_KEY]
|
||||
raise(UnlockWalletError.UserCancelled)
|
||||
}
|
||||
|
||||
val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
|
||||
val allKeys = (biometricKeys + unsecuredKeys).distinct()
|
||||
val unlockedWalletsIds = allKeys.map { it.walletId }
|
||||
|
||||
val unlockedWallets = unlockedWalletsIds.mapNotNull { id ->
|
||||
userWalletsSync().firstOrNull { it.walletId == id }
|
||||
}
|
||||
|
||||
// Remove all password attempts for unlocked hot wallets
|
||||
unlockedWallets.forEach {
|
||||
removePasswordAttempts(it)
|
||||
}
|
||||
|
||||
// if we cant unlock all wallets
|
||||
if (userWalletIds.all { it in unlockedWalletsIds }.not()) {
|
||||
raise(UnlockWalletError.UnableToUnlock)
|
||||
}
|
||||
|
||||
sensitiveInformationRepository.getAll(allKeys)
|
||||
.doOnSuccess { sensitiveInfo ->
|
||||
userWallets.update { it?.updateWith(sensitiveInfo) }
|
||||
}
|
||||
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
|
||||
}
|
||||
|
||||
override suspend fun lockAllWallets(): Either<LockWalletsError, Unit> = either {
|
||||
val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet()
|
||||
|
||||
if (unsecuredWalletIds.size == userWallets.value?.size) {
|
||||
raise(LockWalletsError.NothingToLock)
|
||||
}
|
||||
|
||||
userWallets.update {
|
||||
it?.map {
|
||||
if (it.walletId !in unsecuredWalletIds) {
|
||||
it.lock()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearPersistentData() {
|
||||
publicInformationRepository.clear()
|
||||
sensitiveInformationRepository.clear()
|
||||
userWalletEncryptionKeysRepository.clear()
|
||||
}
|
||||
|
||||
private suspend fun requestPasswordRecursive(
|
||||
hotWalletId: HotWalletId,
|
||||
block: suspend (CharArray) -> UserWalletEncryptionKey?,
|
||||
biometryFallback: suspend () -> Either<UnlockWalletError, Unit>,
|
||||
): Either<UnlockWalletError, UserWalletEncryptionKey?> {
|
||||
val attemptRequest = HotWalletPasswordRequester.AttemptRequest(
|
||||
hotWalletId = hotWalletId,
|
||||
authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts
|
||||
hasBiometry = hasBiometry(),
|
||||
)
|
||||
val result = passwordRequester.requestPassword(attemptRequest)
|
||||
|
||||
return when (result) {
|
||||
HotWalletPasswordRequester.Result.Dismiss -> {
|
||||
passwordRequester.dismiss()
|
||||
UnlockWalletError.UserCancelled.left()
|
||||
}
|
||||
is HotWalletPasswordRequester.Result.EnteredPassword -> {
|
||||
val decrypted = block(result.password.value)
|
||||
if (decrypted == null) {
|
||||
passwordRequester.wrongPassword()
|
||||
requestPasswordRecursive(hotWalletId, block, biometryFallback)
|
||||
} else {
|
||||
passwordRequester.successfulAuthentication()
|
||||
passwordRequester.dismiss()
|
||||
decrypted.right()
|
||||
}
|
||||
}
|
||||
HotWalletPasswordRequester.Result.UseBiometry -> {
|
||||
biometryFallback()
|
||||
.onRight {
|
||||
passwordRequester.successfulAuthentication()
|
||||
passwordRequester.dismiss()
|
||||
}
|
||||
.map { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun removePasswordAttempts(userWallet: UserWallet) {
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hasBiometry(): Boolean {
|
||||
val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
|
||||
default = false,
|
||||
)
|
||||
|
||||
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest available wallet that can be selected
|
||||
*
|
||||
* Example:
|
||||
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
|
||||
*
|
||||
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
|
||||
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
|
||||
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
|
||||
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
|
||||
*
|
||||
* @receiver list of user wallets without deleted wallet
|
||||
*/
|
||||
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
|
||||
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
|
||||
|
||||
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
|
||||
|
||||
for (offset in 1..size) {
|
||||
val rightIndex = prevSelectedIndex + offset
|
||||
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
|
||||
|
||||
val leftIndex = prevSelectedIndex - offset
|
||||
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
|
||||
}
|
||||
|
||||
return lastOrNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.common.authentication.storage.AuthenticatedStorage
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class UserWalletEncryptionKeysRepository(
|
||||
moshi: Moshi,
|
||||
private val authenticatedStorage: AuthenticatedStorage,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val secureStorage: SecureStorage,
|
||||
) {
|
||||
|
||||
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
|
||||
UserWalletEncryptionKey::class.java,
|
||||
)
|
||||
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
|
||||
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
|
||||
)
|
||||
|
||||
suspend fun save(
|
||||
encryptionKey: UserWalletEncryptionKey,
|
||||
removeUnsecured: Boolean = true,
|
||||
method: EncryptionMethod,
|
||||
) = withContext(dispatchers.io) {
|
||||
if (removeUnsecured) {
|
||||
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
|
||||
}
|
||||
|
||||
when (method) {
|
||||
EncryptionMethod.Unsecured -> {
|
||||
secureStorage.store(
|
||||
account = StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name,
|
||||
data = encryptionKey.encode(),
|
||||
)
|
||||
}
|
||||
is EncryptionMethod.Password -> {
|
||||
val encodedWithPass = AESEncryptionProtocol.encryptWithPassword(
|
||||
password = method.password,
|
||||
content = encryptionKey.encode(),
|
||||
)
|
||||
secureStorage.store(
|
||||
account = StorageKey.UserWalletEncryptionKeyEncrypted(encryptionKey.walletId).name,
|
||||
data = encodedWithPass,
|
||||
)
|
||||
}
|
||||
EncryptionMethod.Biometric -> {
|
||||
authenticatedStorage.store(
|
||||
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
|
||||
data = encryptionKey.encode(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
storeUserWalletId(userWalletId = encryptionKey.walletId)
|
||||
}
|
||||
|
||||
fun removeBiometricKey(userWalletId: UserWalletId) {
|
||||
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
|
||||
}
|
||||
|
||||
suspend fun getAllUnsecured(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
|
||||
getUserWalletsIds().mapNotNull { userWalletId ->
|
||||
secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? =
|
||||
withContext(dispatchers.io) {
|
||||
val encrypted = secureStorage.get(
|
||||
account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name,
|
||||
) ?: return@withContext null
|
||||
|
||||
withContext(dispatchers.default) {
|
||||
AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAllBiometric(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
|
||||
val keys = getUserWalletsIds().map { userWalletId ->
|
||||
StorageKey.UserWalletEncryptionKey(userWalletId).name
|
||||
}
|
||||
|
||||
authenticatedStorage.get(keys).mapNotNull {
|
||||
it.value.decodeToKey()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(userWalletIds: List<UserWalletId>) {
|
||||
if (userWalletIds.isEmpty()) return
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
userWalletIds.forEach { userWalletId ->
|
||||
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
|
||||
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
|
||||
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
|
||||
}
|
||||
|
||||
val userWalletsIds = getUserWalletsIds().filterNot { it in userWalletIds }
|
||||
secureStorage.store(userWalletsIds.encode(), StorageKey.UserWalletIds.name)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
withContext(dispatchers.io) {
|
||||
val userWalletsIds = getUserWalletsIds()
|
||||
userWalletsIds.forEach { userWalletId ->
|
||||
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name)
|
||||
secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name)
|
||||
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
|
||||
}
|
||||
secureStorage.delete(StorageKey.UserWalletIds.name)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWalletsIds(): List<UserWalletId> {
|
||||
return withContext(dispatchers.io) {
|
||||
secureStorage.get(StorageKey.UserWalletIds.name)
|
||||
.decodeToUserWalletsIds()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeUserWalletId(userWalletId: UserWalletId) {
|
||||
val userWalletIds = (getUserWalletsIds() + userWalletId).distinct()
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun UserWalletEncryptionKey.encode(): ByteArray {
|
||||
return withContext(dispatchers.default) {
|
||||
this@encode
|
||||
.let(encryptionKeyAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? {
|
||||
return withContext(dispatchers.default) {
|
||||
this@decodeToKey
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(encryptionKeyAdapter::fromJson)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun List<UserWalletId>.encode(): ByteArray {
|
||||
return withContext(dispatchers.default) {
|
||||
this@encode
|
||||
.let(userWalletsIdsListAdapter::toJson)
|
||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
|
||||
return withContext(dispatchers.default) {
|
||||
this@decodeToUserWalletsIds
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
?.let(userWalletsIdsListAdapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class EncryptionMethod {
|
||||
data object Unsecured : EncryptionMethod()
|
||||
data object Biometric : EncryptionMethod()
|
||||
class Password(val password: CharArray) : EncryptionMethod()
|
||||
}
|
||||
|
||||
private sealed interface StorageKey {
|
||||
val name: String
|
||||
|
||||
class UserWalletEncryptionKeyUnsecured(userWalletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_encryption_key_unsecured_${userWalletId.stringValue}"
|
||||
}
|
||||
|
||||
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
|
||||
}
|
||||
|
||||
class UserWalletEncryptionKeyEncrypted(userWalletId: UserWalletId) : StorageKey {
|
||||
override val name: String = "user_wallet_encryption_key_encrypted_${userWalletId.stringValue}"
|
||||
}
|
||||
|
||||
object UserWalletIds : StorageKey {
|
||||
override val name: String = "user_wallets_ids_with_saved_keys"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService
|
|||
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
|
||||
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
|
||||
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
|
||||
|
|
@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule {
|
|||
wcSessionsRepository: WalletConnectSessionsRepository,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
walletConnectFeatureToggles: WalletConnectFeatureToggles,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
): WalletConnectInteractor {
|
||||
return WalletConnectInteractor(
|
||||
handler = WalletConnectEventsHandlerImpl(),
|
||||
|
|
@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule {
|
|||
blockchainHelper = TangemWcBlockchainHelper(),
|
||||
currenciesRepository = currenciesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
walletConnectFeatureToggles = walletConnectFeatureToggles,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account
|
|||
import com.tangem.domain.walletconnect.model.legacy.Session
|
||||
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -38,18 +37,14 @@ class WalletConnectInteractor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
val blockchainHelper: WcBlockchainHelper,
|
||||
) {
|
||||
private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled }
|
||||
|
||||
private var isWalletConnectReadyForDeepLinks = false
|
||||
|
||||
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
GetSelectedWalletUseCase(userWalletsListManager)
|
||||
}
|
||||
|
||||
private val wcScope = CoroutineScope(
|
||||
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
|
||||
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue