Updated on 2026-08-14
This commit is contained in:
parent
182aa24095
commit
94a9b132a2
23 changed files with 458 additions and 60 deletions
|
|
@ -114,6 +114,7 @@ dependencies {
|
||||||
implementation(projects.domain.card)
|
implementation(projects.domain.card)
|
||||||
implementation(projects.domain.demo)
|
implementation(projects.domain.demo)
|
||||||
implementation(projects.domain.demo.models)
|
implementation(projects.domain.demo.models)
|
||||||
|
implementation(projects.domain.dynamicAddresses)
|
||||||
implementation(projects.domain.express)
|
implementation(projects.domain.express)
|
||||||
implementation(projects.domain.wallets)
|
implementation(projects.domain.wallets)
|
||||||
implementation(projects.domain.wallets.models)
|
implementation(projects.domain.wallets.models)
|
||||||
|
|
@ -199,6 +200,7 @@ dependencies {
|
||||||
implementation(projects.data.visa)
|
implementation(projects.data.visa)
|
||||||
implementation(projects.data.promo)
|
implementation(projects.data.promo)
|
||||||
implementation(projects.data.onboarding)
|
implementation(projects.data.onboarding)
|
||||||
|
implementation(projects.data.dynamicAddresses)
|
||||||
implementation(projects.data.feedback)
|
implementation(projects.data.feedback)
|
||||||
implementation(projects.data.qrScanning)
|
implementation(projects.data.qrScanning)
|
||||||
implementation(projects.data.staking)
|
implementation(projects.data.staking)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||||
import com.tangem.domain.card.BuildConfig
|
import com.tangem.domain.card.BuildConfig
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.sdk.api.TangemSdkManager
|
import com.tangem.sdk.api.TangemSdkManager
|
||||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||||
|
|
@ -32,6 +33,7 @@ internal class TangemSdkManagerModule {
|
||||||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||||
|
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||||
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||||
analyticsErrorHandler: AnalyticsErrorHandler,
|
analyticsErrorHandler: AnalyticsErrorHandler,
|
||||||
): TangemSdkManager {
|
): TangemSdkManager {
|
||||||
|
|
@ -45,6 +47,7 @@ internal class TangemSdkManagerModule {
|
||||||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||||
|
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
|
||||||
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
blockchainToDeriveFinder = blockchainToDeriveFinder,
|
||||||
analyticsErrorHandler = analyticsErrorHandler,
|
analyticsErrorHandler = analyticsErrorHandler,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.pay.WithdrawalSignatureResult
|
import com.tangem.domain.pay.WithdrawalSignatureResult
|
||||||
import com.tangem.domain.visa.model.*
|
import com.tangem.domain.visa.model.*
|
||||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||||
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.operations.ScanTask
|
import com.tangem.operations.ScanTask
|
||||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||||
|
|
@ -68,6 +69,7 @@ internal class DefaultTangemSdkManager(
|
||||||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||||
|
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
|
||||||
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
private val analyticsErrorHandler: AnalyticsErrorHandler,
|
||||||
) : TangemSdkManager {
|
) : TangemSdkManager {
|
||||||
|
|
@ -142,6 +144,7 @@ internal class DefaultTangemSdkManager(
|
||||||
visaCardScanHandler = visaCardScanHandler,
|
visaCardScanHandler = visaCardScanHandler,
|
||||||
visaCoroutineScope = this,
|
visaCoroutineScope = this,
|
||||||
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated,
|
||||||
|
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
|
||||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||||
),
|
),
|
||||||
cardId = cardId,
|
cardId = cardId,
|
||||||
|
|
@ -446,7 +449,11 @@ internal class DefaultTangemSdkManager(
|
||||||
initialMessage: Message,
|
initialMessage: Message,
|
||||||
): CompletionResult<ScanResponse> {
|
): CompletionResult<ScanResponse> {
|
||||||
return runTaskAsync(
|
return runTaskAsync(
|
||||||
runnable = FinalizeTwinTask(secondCardPublicKey, issuerKeyPair),
|
runnable = FinalizeTwinTask(
|
||||||
|
twinPublicKey = secondCardPublicKey,
|
||||||
|
issuerKeys = issuerKeyPair,
|
||||||
|
isDynamicAddressesEnabled = dynamicAddressesFeatureToggles.isDynamicAddressesEnabled,
|
||||||
|
),
|
||||||
cardId = cardId,
|
cardId = cardId,
|
||||||
initialMessage = initialMessage,
|
initialMessage = initialMessage,
|
||||||
preflightReadFilter = null,
|
preflightReadFilter = null,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import com.tangem.tap.store
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class ScanProductTask(
|
internal class ScanProductTask(
|
||||||
private val card: Card?,
|
private val card: Card?,
|
||||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
||||||
|
|
@ -51,6 +52,7 @@ internal class ScanProductTask(
|
||||||
private val visaCoroutineScope: CoroutineScope?,
|
private val visaCoroutineScope: CoroutineScope?,
|
||||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
|
||||||
private val shouldCheckIsAlreadyActivated: Boolean,
|
private val shouldCheckIsAlreadyActivated: Boolean,
|
||||||
|
private val isDynamicAddressesEnabled: Boolean,
|
||||||
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
override val allowsRequestAccessCodeFromRepository: Boolean = false,
|
||||||
) : CardSessionRunnable<ScanResponse> {
|
) : CardSessionRunnable<ScanResponse> {
|
||||||
|
|
||||||
|
|
@ -78,7 +80,7 @@ internal class ScanProductTask(
|
||||||
readVisaCard(
|
readVisaCard(
|
||||||
session = session,
|
session = session,
|
||||||
cardDto = cardDto,
|
cardDto = cardDto,
|
||||||
scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder),
|
scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder, isDynamicAddressesEnabled),
|
||||||
callback = callback,
|
callback = callback,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -86,7 +88,7 @@ internal class ScanProductTask(
|
||||||
|
|
||||||
val commandProcessor = when {
|
val commandProcessor = when {
|
||||||
cardDto.isTangemTwins -> ScanTwinProcessor()
|
cardDto.isTangemTwins -> ScanTwinProcessor()
|
||||||
else -> ScanWalletProcessor(blockchainToDeriveFinder)
|
else -> ScanWalletProcessor(blockchainToDeriveFinder, isDynamicAddressesEnabled)
|
||||||
}
|
}
|
||||||
commandProcessor.proceed(cardDto, session) { processorResult ->
|
commandProcessor.proceed(cardDto, session) { processorResult ->
|
||||||
when (processorResult) {
|
when (processorResult) {
|
||||||
|
|
@ -168,6 +170,7 @@ internal class ScanProductTask(
|
||||||
|
|
||||||
private class ScanWalletProcessor(
|
private class ScanWalletProcessor(
|
||||||
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
|
||||||
|
private val isDynamicAddressesEnabled: Boolean,
|
||||||
) : ProductCommandProcessor<ScanResponse> {
|
) : ProductCommandProcessor<ScanResponse> {
|
||||||
|
|
||||||
var primaryCard: PrimaryCard? = null
|
var primaryCard: PrimaryCard? = null
|
||||||
|
|
@ -333,7 +336,7 @@ private class ScanWalletProcessor(
|
||||||
?.find(card)
|
?.find(card)
|
||||||
?: return emptyMap()
|
?: return emptyMap()
|
||||||
|
|
||||||
return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains)
|
return MissedDerivationsFinder(scanResponse, isDynamicAddressesEnabled).findByBlockchainsToDerive(blockchains)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||||
class FinalizeTwinTask(
|
class FinalizeTwinTask(
|
||||||
private val twinPublicKey: ByteArray,
|
private val twinPublicKey: ByteArray,
|
||||||
private val issuerKeys: KeyPair,
|
private val issuerKeys: KeyPair,
|
||||||
|
private val isDynamicAddressesEnabled: Boolean,
|
||||||
) : CardSessionRunnable<ScanResponse> {
|
) : CardSessionRunnable<ScanResponse> {
|
||||||
|
|
||||||
override val allowsRequestAccessCodeFromRepository: Boolean = false
|
override val allowsRequestAccessCodeFromRepository: Boolean = false
|
||||||
|
|
@ -32,6 +33,7 @@ class FinalizeTwinTask(
|
||||||
visaCardScanHandler = null,
|
visaCardScanHandler = null,
|
||||||
visaCoroutineScope = null,
|
visaCoroutineScope = null,
|
||||||
shouldCheckIsAlreadyActivated = false,
|
shouldCheckIsAlreadyActivated = false,
|
||||||
|
isDynamicAddressesEnabled = false,
|
||||||
onboardingV2FeatureToggles = null,
|
onboardingV2FeatureToggles = null,
|
||||||
).run(session, callback)
|
).run(session, callback)
|
||||||
is CompletionResult.Failure ->
|
is CompletionResult.Failure ->
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ data class UserTokensResponse(
|
||||||
@Json(name = "decimals") val decimals: Int,
|
@Json(name = "decimals") val decimals: Int,
|
||||||
@Json(name = "contractAddress") val contractAddress: String?,
|
@Json(name = "contractAddress") val contractAddress: String?,
|
||||||
@Json(name = "addresses") val addresses: List<String>? = null,
|
@Json(name = "addresses") val addresses: List<String>? = null,
|
||||||
|
@Json(name = "dynamicAddressesEnabled") val dynamicAddressesEnabled: Boolean? = null,
|
||||||
) {
|
) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
val otherToken = other as? Token ?: return false
|
val otherToken = other as? Token ?: return false
|
||||||
|
|
|
||||||
39
data/dynamic-addresses/build.gradle.kts
Normal file
39
data/dynamic-addresses/build.gradle.kts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.android.library)
|
||||||
|
alias(deps.plugins.kotlin.android)
|
||||||
|
alias(deps.plugins.kotlin.kapt)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.tangem.data.dynamicaddresses"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// region Project - Core
|
||||||
|
implementation(projects.core.configToggles)
|
||||||
|
implementation(projects.core.datasource)
|
||||||
|
implementation(projects.core.utils)
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Project - Data
|
||||||
|
implementation(projects.data.common)
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Project - Domain
|
||||||
|
implementation(projects.domain.account)
|
||||||
|
implementation(projects.domain.dynamicAddresses)
|
||||||
|
implementation(projects.domain.dynamicAddresses.models)
|
||||||
|
implementation(projects.domain.models)
|
||||||
|
implementation(projects.domain.walletManager)
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Project - Libs
|
||||||
|
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region DI
|
||||||
|
implementation(deps.hilt.android)
|
||||||
|
kapt(deps.hilt.kapt)
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.tangem.data.dynamicaddresses
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import com.tangem.blockchain.common.DynamicAddressesManager
|
||||||
|
import com.tangem.blockchain.common.TransactionData
|
||||||
|
import com.tangem.blockchain.common.WalletManager
|
||||||
|
import com.tangem.blockchain.common.transaction.Fee
|
||||||
|
import com.tangem.blockchain.extensions.Result
|
||||||
|
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class DefaultConsolidationRepository(
|
||||||
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : ConsolidationRepository {
|
||||||
|
|
||||||
|
override suspend fun createConsolidationTransaction(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
): Either<Throwable, TransactionData> = Either.catch {
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
|
||||||
|
?: error("WalletManager not found")
|
||||||
|
|
||||||
|
val dynamicAddressesManager = walletManager as? DynamicAddressesManager
|
||||||
|
?: error("WalletManager does not support dynamic addresses")
|
||||||
|
|
||||||
|
val fee = getNormalFee(walletManager)
|
||||||
|
|
||||||
|
when (val result = dynamicAddressesManager.createConsolidationTransaction(fee)) {
|
||||||
|
is Result.Success -> result.data
|
||||||
|
is Result.Failure -> error("Failed to create consolidation tx: ${result.error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedParameter")
|
||||||
|
private fun getNormalFee(walletManager: WalletManager): Fee {
|
||||||
|
TODO("Fee calculation for consolidation from multiple addresses is not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.data.dynamicaddresses
|
||||||
|
|
||||||
|
import com.tangem.core.configtoggle.FeatureToggles
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||||
|
|
||||||
|
internal class DefaultDynamicAddressesFeatureToggles(
|
||||||
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
|
) : DynamicAddressesFeatureToggles {
|
||||||
|
|
||||||
|
override val isDynamicAddressesEnabled: Boolean
|
||||||
|
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.DYNAMIC_ADDRESSES_ENABLED)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.tangem.data.dynamicaddresses
|
||||||
|
|
||||||
|
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||||
|
import com.tangem.data.common.account.WalletAccountsSaver
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||||
|
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||||
|
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||||
|
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||||
|
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||||
|
import com.tangem.domain.models.network.Network
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class DefaultDynamicAddressesRepository(
|
||||||
|
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||||
|
private val walletAccountsSaver: WalletAccountsSaver,
|
||||||
|
private val accountsCRUDRepository: AccountsCRUDRepository,
|
||||||
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : DynamicAddressesRepository {
|
||||||
|
|
||||||
|
override fun getStatus(userWalletId: UserWalletId, network: Network): Flow<DynamicAddressesStatus> {
|
||||||
|
return walletAccountsFetcher.get(userWalletId)
|
||||||
|
.map { response ->
|
||||||
|
val token = response.findToken(network)
|
||||||
|
when {
|
||||||
|
token?.dynamicAddressesEnabled == true -> DynamicAddressesStatus.ENABLED
|
||||||
|
else -> DynamicAddressesStatus.DISABLED
|
||||||
|
}
|
||||||
|
// TODO handle ENABLED_REQUIRES_SETUP when XPUB is not derived locally
|
||||||
|
}
|
||||||
|
.flowOn(dispatchers.io)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String) {
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
|
||||||
|
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true)
|
||||||
|
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||||
|
.onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun disable(userWalletId: UserWalletId, network: Network) {
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
walletManagersFacade.disableXpubMode(userWalletId, network)
|
||||||
|
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false)
|
||||||
|
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||||
|
.onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getReceiveAddress(userWalletId: UserWalletId, network: Network): String {
|
||||||
|
return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network)
|
||||||
|
?: error("Dynamic receive address not available for $userWalletId / ${network.id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String? {
|
||||||
|
return walletManagersFacade.getDynamicAddressesLastUsedReceiveAddress(userWalletId, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean {
|
||||||
|
return walletManagersFacade.hasDynamicAddressesNonBaseBalances(userWalletId, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateTokenDynamicAddressesFlag(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
enabled: Boolean,
|
||||||
|
) {
|
||||||
|
walletAccountsSaver.update(userWalletId) { response ->
|
||||||
|
response?.copy(
|
||||||
|
accounts = response.accounts.map { account ->
|
||||||
|
account.copy(
|
||||||
|
tokens = account.tokens?.map { token ->
|
||||||
|
if (token.matchesNetwork(network)) {
|
||||||
|
token.copy(dynamicAddressesEnabled = enabled)
|
||||||
|
} else {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? {
|
||||||
|
return accounts
|
||||||
|
.flatMap { it.tokens.orEmpty() }
|
||||||
|
.find { it.matchesNetwork(network) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UserTokensResponse.Token.matchesNetwork(network: Network): Boolean {
|
||||||
|
return networkId == network.backendId &&
|
||||||
|
derivationPath == network.derivationPath.value &&
|
||||||
|
contractAddress == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.tangem.data.dynamicaddresses.di
|
||||||
|
|
||||||
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
|
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||||
|
import com.tangem.data.common.account.WalletAccountsSaver
|
||||||
|
import com.tangem.data.dynamicaddresses.DefaultConsolidationRepository
|
||||||
|
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||||
|
import com.tangem.data.dynamicaddresses.DefaultDynamicAddressesFeatureToggles
|
||||||
|
import com.tangem.data.dynamicaddresses.DefaultDynamicAddressesRepository
|
||||||
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||||
|
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||||
|
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal object DynamicAddressesDataModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDynamicAddressesRepository(
|
||||||
|
walletAccountsFetcher: WalletAccountsFetcher,
|
||||||
|
walletAccountsSaver: WalletAccountsSaver,
|
||||||
|
accountsCRUDRepository: AccountsCRUDRepository,
|
||||||
|
walletManagersFacade: WalletManagersFacade,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): DynamicAddressesRepository {
|
||||||
|
return DefaultDynamicAddressesRepository(
|
||||||
|
walletAccountsFetcher = walletAccountsFetcher,
|
||||||
|
walletAccountsSaver = walletAccountsSaver,
|
||||||
|
accountsCRUDRepository = accountsCRUDRepository,
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDynamicAddressesFeatureToggles(
|
||||||
|
featureTogglesManager: FeatureTogglesManager,
|
||||||
|
): DynamicAddressesFeatureToggles {
|
||||||
|
return DefaultDynamicAddressesFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideConsolidationRepository(
|
||||||
|
walletManagersFacade: WalletManagersFacade,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): ConsolidationRepository {
|
||||||
|
return DefaultConsolidationRepository(
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import arrow.core.raise.ensureNotNull
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||||
import com.tangem.blockchain.common.*
|
import com.tangem.blockchain.common.*
|
||||||
|
import com.tangem.blockchain.common.DynamicAddressesManager
|
||||||
import com.tangem.blockchain.common.address.Address
|
import com.tangem.blockchain.common.address.Address
|
||||||
import com.tangem.blockchain.common.address.AddressType
|
import com.tangem.blockchain.common.address.AddressType
|
||||||
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
|
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
|
||||||
|
|
@ -422,6 +423,85 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region Dynamic Addresses
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
override suspend fun enableXpubMode(userWalletId: UserWalletId, network: Network, xpub: String): SimpleResult =
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||||
|
|
||||||
|
if (walletManager !is DynamicAddressesManager) {
|
||||||
|
return@withContext SimpleResult.Failure(
|
||||||
|
BlockchainSdkError.CustomError("WalletManager does not support dynamic addresses"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
walletManager.enableDynamicAddresses(xpub)
|
||||||
|
SimpleResult.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SimpleResult.Failure(BlockchainSdkError.CustomError(e.message ?: "Failed to enable XPUB mode"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
override suspend fun disableXpubMode(userWalletId: UserWalletId, network: Network): SimpleResult =
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||||
|
|
||||||
|
if (walletManager !is DynamicAddressesManager) {
|
||||||
|
return@withContext SimpleResult.Failure(
|
||||||
|
BlockchainSdkError.CustomError("WalletManager does not support dynamic addresses"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
walletManager.disableDynamicAddresses()
|
||||||
|
SimpleResult.Success
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SimpleResult.Failure(BlockchainSdkError.CustomError(e.message ?: "Failed to disable XPUB mode"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDynamicAddressesReceiveAddress(userWalletId: UserWalletId, network: Network): String? {
|
||||||
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||||
|
val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return null
|
||||||
|
return dynamicAddressesManager.findFirstUnusedReceiveAddress()?.address
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDynamicAddressesLastUsedReceiveAddress(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
network: Network,
|
||||||
|
): String? {
|
||||||
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||||
|
val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return null
|
||||||
|
return dynamicAddressesManager.usedAddresses
|
||||||
|
.filter { usedAddress ->
|
||||||
|
val nodes = runCatching { DerivationPath(usedAddress.path).nodes }.getOrNull()
|
||||||
|
?: return@filter false
|
||||||
|
nodes.size >= XPUB_PATH_MIN_NODES && nodes[nodes.size - 2].index == RECEIVE_CHAIN_INDEX
|
||||||
|
}
|
||||||
|
.maxByOrNull { usedAddress ->
|
||||||
|
runCatching { DerivationPath(usedAddress.path).nodes.last().index }.getOrDefault(0L)
|
||||||
|
}
|
||||||
|
?.address
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun hasDynamicAddressesNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean {
|
||||||
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||||
|
val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return false
|
||||||
|
return dynamicAddressesManager.usedAddresses.any { usedAddress ->
|
||||||
|
val nodes = runCatching { DerivationPath(usedAddress.path).nodes }.getOrNull()
|
||||||
|
?: return@any false
|
||||||
|
val isBaseAddress = nodes.size >= XPUB_PATH_MIN_NODES &&
|
||||||
|
nodes[nodes.size - 2].index == RECEIVE_CHAIN_INDEX &&
|
||||||
|
nodes.last().index == 0L
|
||||||
|
!isBaseAddress && usedAddress.balance > BigDecimal.ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion Dynamic Addresses
|
||||||
|
|
||||||
@Deprecated("Will be removed in future")
|
@Deprecated("Will be removed in future")
|
||||||
override suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager? {
|
override suspend fun getOrCreateWalletManager(userWalletId: UserWalletId, network: Network): WalletManager? {
|
||||||
val blockchain = network.toBlockchain()
|
val blockchain = network.toBlockchain()
|
||||||
|
|
@ -825,4 +905,9 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
||||||
private suspend fun readSmartContractMethods(): Map<String, SmartContractMethod> {
|
private suspend fun readSmartContractMethods(): Map<String, SmartContractMethod> {
|
||||||
return assetLoader.loadMap<SmartContractMethod>(fileName = "contract_methods")
|
return assetLoader.loadMap<SmartContractMethod>(fileName = "contract_methods")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val XPUB_PATH_MIN_NODES = 2
|
||||||
|
const val RECEIVE_CHAIN_INDEX = 0L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
/** Domain */
|
/** Domain */
|
||||||
implementation(projects.domain.account)
|
implementation(projects.domain.account)
|
||||||
implementation(projects.domain.card)
|
implementation(projects.domain.card)
|
||||||
|
implementation(projects.domain.dynamicAddresses)
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
implementation(projects.domain.tokens.models)
|
implementation(projects.domain.tokens.models)
|
||||||
implementation(projects.domain.wallets)
|
implementation(projects.domain.wallets)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
import com.tangem.data.common.network.NetworkFactory
|
import com.tangem.data.common.network.NetworkFactory
|
||||||
import com.tangem.data.wallets.derivations.Derivations
|
import com.tangem.data.wallets.derivations.Derivations
|
||||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||||
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
|
|
@ -30,6 +31,7 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor(
|
||||||
private val tangemSdkManager: TangemSdkManager,
|
private val tangemSdkManager: TangemSdkManager,
|
||||||
private val networkFactory: NetworkFactory,
|
private val networkFactory: NetworkFactory,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||||
) : ColdMapDerivationsRepository {
|
) : ColdMapDerivationsRepository {
|
||||||
|
|
||||||
override suspend fun derivePublicKeys(
|
override suspend fun derivePublicKeys(
|
||||||
|
|
@ -64,7 +66,7 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor(
|
||||||
return@withContext userWallet
|
return@withContext userWallet
|
||||||
}
|
}
|
||||||
|
|
||||||
val derivations = MissedDerivationsFinder(userWallet)
|
val derivations = MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
|
||||||
.findByNetworks(networks)
|
.findByNetworks(networks)
|
||||||
.ifEmpty {
|
.ifEmpty {
|
||||||
TangemLogger.d("Nothing to derive")
|
TangemLogger.d("Nothing to derive")
|
||||||
|
|
@ -103,7 +105,7 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor(
|
||||||
networksWithDerivationPath: Map<BackendId, String?>,
|
networksWithDerivationPath: Map<BackendId, String?>,
|
||||||
): Boolean = withContext(dispatchers.io) {
|
): Boolean = withContext(dispatchers.io) {
|
||||||
val derivations =
|
val derivations =
|
||||||
MissedDerivationsFinder(userWallet)
|
MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
|
||||||
.findByNetworks(
|
.findByNetworks(
|
||||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||||
networkFactory.create(
|
networkFactory.create(
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,20 @@ data class BlockchainToDerive(
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
class MissedDerivationsFinder private constructor(private val source: DerivationsSource) {
|
class MissedDerivationsFinder private constructor(
|
||||||
|
private val source: DerivationsSource,
|
||||||
|
private val isDynamicAddressesEnabled: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
constructor(userWallet: UserWallet, isDynamicAddressesEnabled: Boolean) : this(
|
||||||
* Secondary constructor for backward compatibility with UserWallet
|
source = DerivationsSource.FromUserWallet(userWallet),
|
||||||
*/
|
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
||||||
constructor(userWallet: UserWallet) : this(DerivationsSource.FromUserWallet(userWallet))
|
)
|
||||||
|
|
||||||
/**
|
constructor(scanResponse: ScanResponse, isDynamicAddressesEnabled: Boolean) : this(
|
||||||
* Secondary constructor for ScanResponse
|
source = DerivationsSource.FromScanResponse(scanResponse),
|
||||||
*/
|
isDynamicAddressesEnabled = isDynamicAddressesEnabled,
|
||||||
constructor(scanResponse: ScanResponse) : this(DerivationsSource.FromScanResponse(scanResponse))
|
)
|
||||||
|
|
||||||
/** Find missed derivations for given currencies [currencies] */
|
/** Find missed derivations for given currencies [currencies] */
|
||||||
fun find(currencies: List<CryptoCurrency>): Derivations {
|
fun find(currencies: List<CryptoCurrency>): Derivations {
|
||||||
|
|
@ -115,6 +118,9 @@ class MissedDerivationsFinder private constructor(private val source: Derivation
|
||||||
|
|
||||||
// Extended Cardano derivation path if needed
|
// Extended Cardano derivation path if needed
|
||||||
add(getCardanoExtendedDerivationPath(derivationPath))
|
add(getCardanoExtendedDerivationPath(derivationPath))
|
||||||
|
|
||||||
|
// Account-level and parent paths for XPUB generation (Dynamic Addresses)
|
||||||
|
addAll(getXpubDerivationPaths(derivationPath))
|
||||||
}
|
}
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.distinct()
|
.distinct()
|
||||||
|
|
@ -129,6 +135,26 @@ class MissedDerivationsFinder private constructor(private val source: Derivation
|
||||||
return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath)
|
return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns account-level and parent derivation paths needed for XPUB generation.
|
||||||
|
* - Account-level (e.g. m/84'/0'/0') — the XPUB itself
|
||||||
|
* - Parent (e.g. m/84'/0') — needed for parent fingerprint in XPUB serialization
|
||||||
|
*
|
||||||
|
* Only applicable for BIP44-style XPUB blockchains (BTC, BCH, LTC, DOGE, DASH, RVN).
|
||||||
|
*/
|
||||||
|
private fun Blockchain.getXpubDerivationPaths(derivationPath: DerivationPath): List<DerivationPath> {
|
||||||
|
if (!isDynamicAddressesEnabled) return emptyList()
|
||||||
|
if (!isBip44DerivationStyleXPUB()) return emptyList()
|
||||||
|
|
||||||
|
val nodes = derivationPath.nodes
|
||||||
|
if (nodes.size < XPUB_MIN_NODES) return emptyList()
|
||||||
|
|
||||||
|
val accountPath = DerivationPath(nodes.take(XPUB_ACCOUNT_NODE_COUNT))
|
||||||
|
val parentPath = DerivationPath(nodes.take(XPUB_PARENT_NODE_COUNT))
|
||||||
|
|
||||||
|
return listOf(accountPath, parentPath)
|
||||||
|
}
|
||||||
|
|
||||||
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||||
val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList()
|
val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList()
|
||||||
return filterNot(alreadyDerivedPaths::contains)
|
return filterNot(alreadyDerivedPaths::contains)
|
||||||
|
|
@ -161,4 +187,10 @@ class MissedDerivationsFinder private constructor(private val source: Derivation
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val XPUB_MIN_NODES = 3
|
||||||
|
const val XPUB_ACCOUNT_NODE_COUNT = 3
|
||||||
|
const val XPUB_PARENT_NODE_COUNT = 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
import com.tangem.data.common.network.NetworkFactory
|
import com.tangem.data.common.network.NetworkFactory
|
||||||
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||||
import com.tangem.domain.common.wallets.getSyncStrict
|
import com.tangem.domain.common.wallets.getSyncStrict
|
||||||
import com.tangem.domain.models.account.DerivationIndex
|
import com.tangem.domain.models.account.DerivationIndex
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
|
@ -28,6 +29,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor(
|
||||||
private val networkFactory: NetworkFactory,
|
private val networkFactory: NetworkFactory,
|
||||||
private val hotWalletAccessor: HotWalletAccessor,
|
private val hotWalletAccessor: HotWalletAccessor,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||||
) : HotMapDerivationsRepository {
|
) : HotMapDerivationsRepository {
|
||||||
|
|
||||||
override suspend fun derivePublicKeys(
|
override suspend fun derivePublicKeys(
|
||||||
|
|
@ -59,7 +61,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor(
|
||||||
userWallet: UserWallet.Hot,
|
userWallet: UserWallet.Hot,
|
||||||
networks: List<Network>,
|
networks: List<Network>,
|
||||||
): UserWallet.Hot = withContext(dispatchers.default) {
|
): UserWallet.Hot = withContext(dispatchers.default) {
|
||||||
val derivations = MissedDerivationsFinder(userWallet)
|
val derivations = MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
|
||||||
.findByNetworks(networks)
|
.findByNetworks(networks)
|
||||||
.ifEmpty {
|
.ifEmpty {
|
||||||
TangemLogger.d("Nothing to derive")
|
TangemLogger.d("Nothing to derive")
|
||||||
|
|
@ -103,7 +105,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor(
|
||||||
userWallet: UserWallet.Hot,
|
userWallet: UserWallet.Hot,
|
||||||
networksWithDerivationPath: Map<BackendId, String?>,
|
networksWithDerivationPath: Map<BackendId, String?>,
|
||||||
): Boolean = withContext(dispatchers.default) {
|
): Boolean = withContext(dispatchers.default) {
|
||||||
val derivations = MissedDerivationsFinder(userWallet)
|
val derivations = MissedDerivationsFinder(userWallet, dynamicAddressesFeatureToggles.isDynamicAddressesEnabled)
|
||||||
.findByNetworks(
|
.findByNetworks(
|
||||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||||
networkFactory.create(
|
networkFactory.create(
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
fun `empty derivations for empty currencies`() {
|
fun `empty derivations for empty currencies`() {
|
||||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val actual = finder.find(emptyList())
|
val actual = finder.find(emptyList())
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
// Bls is not supported
|
// Bls is not supported
|
||||||
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap())
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf)
|
val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf)
|
||||||
val actual = finder.find(currencies)
|
val actual = finder.find(currencies)
|
||||||
|
|
@ -61,7 +61,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum
|
val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum
|
||||||
val actual = finder.find(currencies)
|
val actual = finder.find(currencies)
|
||||||
|
|
@ -76,7 +76,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
fun `derivations for custom token`() {
|
fun `derivations for custom token`() {
|
||||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation
|
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation
|
||||||
val actual = finder.find(currencies)
|
val actual = finder.find(currencies)
|
||||||
|
|
@ -94,7 +94,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
fun `derivations for cardano`() {
|
fun `derivations for cardano`() {
|
||||||
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
|
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
|
||||||
val actual = finder.find(currencies)
|
val actual = finder.find(currencies)
|
||||||
|
|
@ -124,7 +124,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||||
)
|
)
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf)
|
val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf)
|
||||||
val actual = finder.find(currencies)
|
val actual = finder.find(currencies)
|
||||||
|
|
@ -139,7 +139,7 @@ internal class MissedDerivationsFinderTest {
|
||||||
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
|
||||||
)
|
)
|
||||||
val userWallet = MockUserWalletFactory.create(scanResponse)
|
val userWallet = MockUserWalletFactory.create(scanResponse)
|
||||||
val finder = MissedDerivationsFinder(userWallet)
|
val finder = MissedDerivationsFinder(userWallet = userWallet, isDynamicAddressesEnabled = false)
|
||||||
|
|
||||||
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar
|
val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar
|
||||||
val actual = finder.find(currencies)
|
val actual = finder.find(currencies)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.tangem.domain.dynamicaddresses.model
|
|
||||||
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
data class ConsolidationInfo(
|
|
||||||
val fee: BigDecimal,
|
|
||||||
val inputCount: Int,
|
|
||||||
val canCoverFee: Boolean,
|
|
||||||
)
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package com.tangem.domain.dynamicaddresses
|
package com.tangem.domain.dynamicaddresses
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo
|
import com.tangem.blockchain.common.TransactionData
|
||||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
class GetConsolidationInfoUseCase(
|
class CreateConsolidationTransactionUseCase(
|
||||||
private val consolidationRepository: ConsolidationRepository,
|
private val consolidationRepository: ConsolidationRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, ConsolidationInfo> {
|
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, TransactionData> {
|
||||||
return consolidationRepository.getConsolidationInfo(userWalletId, network)
|
return consolidationRepository.createConsolidationTransaction(userWalletId, network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,33 +1,27 @@
|
||||||
package com.tangem.domain.dynamicaddresses
|
package com.tangem.domain.dynamicaddresses
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.getOrElse
|
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||||
import arrow.core.raise.either
|
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo
|
|
||||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
|
||||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
|
||||||
|
|
||||||
class DisableDynamicAddressesUseCase(
|
class DisableDynamicAddressesUseCase(
|
||||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||||
private val consolidationRepository: ConsolidationRepository,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [ConsolidationInfo] when consolidation is required before disabling,
|
* Returns true when consolidation is required before disabling (non-base balances exist),
|
||||||
* or null when DA can be disabled immediately (no non-base balances).
|
* or false when DA was disabled immediately.
|
||||||
*/
|
*/
|
||||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, ConsolidationInfo?> =
|
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> =
|
||||||
either {
|
Either.catch {
|
||||||
val hasNonBaseBalances = dynamicAddressesRepository.hasNonBaseBalances(userWalletId, network)
|
val hasNonBaseBalances = dynamicAddressesRepository.hasNonBaseBalances(userWalletId, network)
|
||||||
|
|
||||||
if (!hasNonBaseBalances) {
|
if (!hasNonBaseBalances) {
|
||||||
dynamicAddressesRepository.disable(userWalletId, network)
|
dynamicAddressesRepository.disable(userWalletId, network)
|
||||||
return@either null
|
return@catch false
|
||||||
}
|
}
|
||||||
|
|
||||||
consolidationRepository.getConsolidationInfo(userWalletId, network)
|
true
|
||||||
.getOrElse { raise(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,14 @@
|
||||||
package com.tangem.domain.dynamicaddresses.repository
|
package com.tangem.domain.dynamicaddresses.repository
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.tangem.blockchain.common.TransactionSigner
|
import com.tangem.blockchain.common.TransactionData
|
||||||
import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo
|
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
interface ConsolidationRepository {
|
interface ConsolidationRepository {
|
||||||
|
|
||||||
suspend fun getConsolidationInfo(
|
suspend fun createConsolidationTransaction(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
network: Network,
|
network: Network,
|
||||||
): Either<Throwable, ConsolidationInfo>
|
): Either<Throwable, TransactionData>
|
||||||
|
|
||||||
suspend fun sendConsolidationTransaction(
|
|
||||||
userWalletId: UserWalletId,
|
|
||||||
network: Network,
|
|
||||||
signer: TransactionSigner,
|
|
||||||
): Either<Throwable, String>
|
|
||||||
}
|
}
|
||||||
|
|
@ -289,4 +289,18 @@ interface WalletManagersFacade {
|
||||||
* value. Otherwise always return true
|
* value. Otherwise always return true
|
||||||
*/
|
*/
|
||||||
suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean
|
suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean
|
||||||
|
|
||||||
|
// region Dynamic Addresses
|
||||||
|
|
||||||
|
suspend fun enableXpubMode(userWalletId: UserWalletId, network: Network, xpub: String): SimpleResult
|
||||||
|
|
||||||
|
suspend fun disableXpubMode(userWalletId: UserWalletId, network: Network): SimpleResult
|
||||||
|
|
||||||
|
suspend fun getDynamicAddressesReceiveAddress(userWalletId: UserWalletId, network: Network): String?
|
||||||
|
|
||||||
|
suspend fun getDynamicAddressesLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String?
|
||||||
|
|
||||||
|
suspend fun hasDynamicAddressesNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean
|
||||||
|
|
||||||
|
// endregion Dynamic Addresses
|
||||||
}
|
}
|
||||||
|
|
@ -420,6 +420,7 @@ include(":data:payment")
|
||||||
include(":data:virtual-account")
|
include(":data:virtual-account")
|
||||||
include(":data:promo")
|
include(":data:promo")
|
||||||
include(":data:onboarding")
|
include(":data:onboarding")
|
||||||
|
include(":data:dynamic-addresses")
|
||||||
include(":data:feedback")
|
include(":data:feedback")
|
||||||
include(":data:qr-scanning")
|
include(":data:qr-scanning")
|
||||||
include(":data:staking")
|
include(":data:staking")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue