Updated on 2026-08-14
This commit is contained in:
parent
27e9400b2a
commit
b38ebb3995
16 changed files with 196 additions and 71 deletions
|
|
@ -5,7 +5,7 @@ import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase
|
|||
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDynamicAddressesStatusUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
|
||||
import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase
|
||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
|
|
@ -69,10 +69,10 @@ internal object DynamicAddressesDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsXpubDerivedUseCase(
|
||||
fun provideGetDerivedXpubUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
derivationsRepository: DerivationsRepository,
|
||||
): IsXpubDerivedUseCase {
|
||||
return IsXpubDerivedUseCase(walletManagersFacade, derivationsRepository)
|
||||
): GetDerivedXpubUseCase {
|
||||
return GetDerivedXpubUseCase(walletManagersFacade, derivationsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,8 @@ data class UserTokensResponse(
|
|||
return otherToken.contractAddress == this.contractAddress &&
|
||||
otherToken.networkId == this.networkId &&
|
||||
otherToken.derivationPath == this.derivationPath &&
|
||||
otherToken.decimals == this.decimals
|
||||
otherToken.decimals == this.decimals &&
|
||||
otherToken.dynamicAddressesEnabled == this.dynamicAddressesEnabled
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = calculateHashCode(
|
||||
|
|
@ -50,6 +51,7 @@ data class UserTokensResponse(
|
|||
networkId.hashCode(),
|
||||
derivationPath.hashCode(),
|
||||
decimals.hashCode(),
|
||||
dynamicAddressesEnabled.hashCode(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@ internal class DefaultDynamicAddressesRepository(
|
|||
}
|
||||
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true)
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) }
|
||||
.onFailure { throwable ->
|
||||
TangemLogger.e(
|
||||
messageString = "Failed to sync tokens after dynamic addresses enable for $userWalletId",
|
||||
throwable = throwable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +66,12 @@ internal class DefaultDynamicAddressesRepository(
|
|||
}
|
||||
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false)
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) }
|
||||
.onFailure { throwable ->
|
||||
TangemLogger.e(
|
||||
messageString = "Failed to sync tokens after dynamic addresses disable for $userWalletId",
|
||||
throwable = throwable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +150,7 @@ internal class DefaultDynamicAddressesRepository(
|
|||
}
|
||||
|
||||
private suspend fun isXpubAvailable(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
// Check if WalletManager is already in XPUB mode (DA was previously enabled on this device)
|
||||
// Check if WalletManager is already in XPUB mode (dynamic addresses was previously enabled on this device)
|
||||
return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network) != null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.data.dynamicaddresses
|
||||
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
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.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides XPUB strings for networks that need dynamic addresses restore (ENABLED_REQUIRES_SETUP).
|
||||
* Uses only already-derived keys — no card scan triggered.
|
||||
*/
|
||||
@Singleton
|
||||
class DynamicAddressesInitializer @Inject constructor(
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
) {
|
||||
|
||||
suspend fun getXpubs(userWalletId: UserWalletId, networks: Set<Network>): Map<Network, String> {
|
||||
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap()
|
||||
|
||||
val result = mutableMapOf<Network, String>()
|
||||
for (network in networks) {
|
||||
val status = dynamicAddressesRepository.getStatus(userWalletId, network).firstOrNull()
|
||||
if (status != DynamicAddressesStatus.ENABLED_REQUIRES_SETUP) continue
|
||||
|
||||
val xpub = getDerivedXpubUseCase(userWalletId, network)
|
||||
if (xpub != null) {
|
||||
result[network] = xpub
|
||||
} else {
|
||||
TangemLogger.w("Dynamic addresses enabled but XPUB not available for ${network.id}")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ dependencies {
|
|||
|
||||
// region Project - Data
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.dynamicAddresses)
|
||||
// endregion
|
||||
|
||||
// region Project - Domain
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
networkCurrencies: Set<CryptoCurrency>,
|
||||
xpub: String? = null,
|
||||
): Either<Throwable, Unit> {
|
||||
return Either.catchOn(dispatchers.default) {
|
||||
val result = withContext(dispatchers.io) {
|
||||
|
|
@ -52,6 +53,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
|
|||
extraTokens = networkCurrencies
|
||||
.filterIsInstance<CryptoCurrency.Token>()
|
||||
.toSet(),
|
||||
xpub = xpub,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ package com.tangem.data.networks.multi
|
|||
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
|
||||
import com.tangem.data.networks.store.NetworksStatusesStore
|
||||
import com.tangem.data.networks.store.setSourceAsCache
|
||||
import com.tangem.data.networks.store.setSourceAsOnlyCache
|
||||
import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -27,11 +29,11 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
||||
private val networksStatusesStore: NetworksStatusesStore,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
|
||||
private val dynamicAddressesInitializer: DynamicAddressesInitializer,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiNetworkStatusFetcher {
|
||||
|
||||
|
|
@ -50,6 +52,14 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
val xpubByNetwork = catch(
|
||||
block = { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) },
|
||||
catch = { error ->
|
||||
TangemLogger.e("Failed to build XPUBs for restore", error)
|
||||
emptyMap()
|
||||
},
|
||||
)
|
||||
|
||||
val result = coroutineScope {
|
||||
params.networks
|
||||
.map { network ->
|
||||
|
|
@ -58,6 +68,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
network = network,
|
||||
networkCurrencies = networksCurrencies[network].orEmpty().toSet(),
|
||||
xpub = xpubByNetwork[network],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.networks.multi
|
|||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
|
||||
import com.tangem.data.networks.store.NetworksStatusesStore
|
||||
|
|
@ -27,17 +28,21 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true)
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
|
||||
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
|
||||
private val dynamicAddressesInitializer: DynamicAddressesInitializer = mockk()
|
||||
|
||||
private val fetcher = DefaultMultiNetworkStatusFetcher(
|
||||
networksStatusesStore = networksStatusesStore,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
commonNetworkStatusFetcher = commonNetworkStatusFetcher,
|
||||
dynamicAddressesInitializer = dynamicAddressesInitializer,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher)
|
||||
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher, dynamicAddressesInitializer)
|
||||
// No dynamic addresses restore by default
|
||||
coEvery { dynamicAddressesInitializer.getXpubs(any(), any()) } returns emptyMap()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -62,6 +67,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns ethereumFetcherResult
|
||||
|
||||
|
|
@ -70,6 +76,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns cardanoFetcherResult
|
||||
|
||||
|
|
@ -87,11 +94,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +131,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns ethereumFetcherResult
|
||||
|
||||
|
|
@ -130,6 +140,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns cardanoFetcherResult
|
||||
|
||||
|
|
@ -147,11 +158,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +195,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns ethereumFetcherResult
|
||||
|
||||
|
|
@ -190,6 +204,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns cardanoFetcherResult
|
||||
|
||||
|
|
@ -207,11 +222,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +262,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) }
|
||||
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import java.util.EnumSet
|
|||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
|
||||
internal class DefaultWalletManagersFacade @Inject constructor(
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
|
|
@ -85,6 +85,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
extraTokens: Set<CryptoCurrency.Token>,
|
||||
xpub: String?,
|
||||
): UpdateWalletManagerResult {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val blockchain = network.toBlockchain()
|
||||
|
|
@ -95,6 +96,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
blockchain = blockchain,
|
||||
derivationPath = derivationPath,
|
||||
extraTokens = extraTokens,
|
||||
xpub = xpub,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -309,6 +311,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
blockchain: Blockchain,
|
||||
derivationPath: String?,
|
||||
extraTokens: Set<CryptoCurrency.Token>,
|
||||
xpub: String? = null,
|
||||
): UpdateWalletManagerResult {
|
||||
if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath)) {
|
||||
TangemLogger.w("Derivation missed for: $blockchain")
|
||||
|
|
@ -326,6 +329,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
}
|
||||
|
||||
val isUpdated = updateWalletManagerTokensIfNeeded(walletManager, extraTokens)
|
||||
if (xpub != null) restoreXpubModeIfNeeded(walletManager, xpub)
|
||||
|
||||
return try {
|
||||
if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) {
|
||||
|
|
@ -499,6 +503,17 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun restoreXpubModeIfNeeded(walletManager: WalletManager, xpub: String) {
|
||||
val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return
|
||||
|
||||
try {
|
||||
dynamicAddressesManager.enableDynamicAddresses(xpub)
|
||||
TangemLogger.i("Restored XPUB mode for ${walletManager.wallet.blockchain}")
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to restore XPUB mode: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Dynamic Addresses
|
||||
|
||||
@Deprecated("Will be removed in future")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DisableDynamicAddressesUseCase(
|
|||
|
||||
/**
|
||||
* Returns true when consolidation is required before disabling (non-base balances exist),
|
||||
* or false when DA was disabled immediately.
|
||||
* or false when dynamic addresses was disabled immediately.
|
||||
*/
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> =
|
||||
Either.catch {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
|
||||
/**
|
||||
* List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode).
|
||||
* Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred).
|
||||
*
|
||||
* Per ASMPT-005: DA is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses.
|
||||
* Dynamic addresses is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses.
|
||||
* Only the default derivation style per blockchain is supported.
|
||||
*/
|
||||
object DynamicAddressesSupportedBlockchains {
|
||||
|
|
@ -26,22 +27,22 @@ object DynamicAddressesSupportedBlockchains {
|
|||
Blockchain.RavencoinTestnet,
|
||||
)
|
||||
|
||||
private val supportedNetworkIds = supported.map { it.id }.toSet()
|
||||
private val supportedNetworkIds = supported.map { it.toNetworkId() }.toSet()
|
||||
|
||||
/**
|
||||
* Allowed BIP purpose nodes per network ID.
|
||||
* BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH).
|
||||
*/
|
||||
private val allowedPurposeByNetworkId: Map<String, Long> = buildMap {
|
||||
put(Blockchain.Bitcoin.id, BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinTestnet.id, BIP84_PURPOSE)
|
||||
put(Blockchain.Litecoin.id, BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinCash.id, BIP44_PURPOSE)
|
||||
put(Blockchain.BitcoinCashTestnet.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Dogecoin.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Dash.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Ravencoin.id, BIP44_PURPOSE)
|
||||
put(Blockchain.RavencoinTestnet.id, BIP44_PURPOSE)
|
||||
put(Blockchain.Bitcoin.toNetworkId(), BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinTestnet.toNetworkId(), BIP84_PURPOSE)
|
||||
put(Blockchain.Litecoin.toNetworkId(), BIP84_PURPOSE)
|
||||
put(Blockchain.BitcoinCash.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.BitcoinCashTestnet.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.Dogecoin.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.Dash.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.Ravencoin.toNetworkId(), BIP44_PURPOSE)
|
||||
put(Blockchain.RavencoinTestnet.toNetworkId(), BIP44_PURPOSE)
|
||||
}
|
||||
|
||||
fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.crypto.NetworkType
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
|
||||
/**
|
||||
* Returns the XPUB string if account-level keys are already derived (no card scan needed),
|
||||
* or null if keys are not available.
|
||||
*/
|
||||
class GetDerivedXpubUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String? {
|
||||
val blockchain = network.toBlockchain()
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return null
|
||||
if (!blockchain.isBip44DerivationStyleXPUB()) return null
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return null
|
||||
val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return null
|
||||
if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return null
|
||||
|
||||
val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey)
|
||||
val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey)
|
||||
|
||||
val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT))
|
||||
val parentPath = DerivationPath(accountPath.nodes.dropLast(1))
|
||||
|
||||
val childExtKey = existingKeys[accountPath] ?: return null
|
||||
val parentExtKey = existingKeys[parentPath] ?: return null
|
||||
|
||||
val parentFingerprint = parentExtKey.publicKey
|
||||
.calculateSha256().calculateRipemd160()
|
||||
.take(PARENT_FINGERPRINT_SIZE).toByteArray()
|
||||
|
||||
val net = if (blockchain.isTestnet()) NetworkType.Testnet else NetworkType.Mainnet
|
||||
return ExtendedPublicKey(
|
||||
publicKey = childExtKey.publicKey,
|
||||
chainCode = childExtKey.chainCode,
|
||||
depth = accountPath.nodes.size,
|
||||
parentFingerprint = parentFingerprint,
|
||||
childNumber = accountPath.nodes.last().index,
|
||||
).serialize(net)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCOUNT_PATH_DROP_COUNT = 2
|
||||
const val PARENT_FINGERPRINT_SIZE = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.domain.dynamicaddresses
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
|
||||
/**
|
||||
* Checks if the account-level XPUB key is already derived (no card scan needed).
|
||||
*/
|
||||
class IsXpubDerivedUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val derivationsRepository: DerivationsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val blockchain = network.toBlockchain()
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false
|
||||
if (!blockchain.isBip44DerivationStyleXPUB()) return false
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false
|
||||
val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return false
|
||||
if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return false
|
||||
val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT))
|
||||
|
||||
val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey)
|
||||
val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey)
|
||||
return existingKeys[accountPath] != null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCOUNT_PATH_DROP_COUNT = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,6 @@ interface DynamicAddressesRepository {
|
|||
|
||||
suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean
|
||||
|
||||
/** Returns true if there are custom tokens with change/index ≠ 0 that conflict with DA */
|
||||
/** Returns true if there are custom tokens with change/index ≠ 0 that conflict with dynamic addresses */
|
||||
suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean
|
||||
}
|
||||
|
|
@ -38,12 +38,14 @@ interface WalletManagersFacade {
|
|||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param network The network.
|
||||
* @param extraTokens Additional tokens.
|
||||
* @param xpub XPUB string to restore dynamic addresses mode if not yet active.
|
||||
* @return The result of updating the wallet manager.
|
||||
*/
|
||||
suspend fun update(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
extraTokens: Set<CryptoCurrency.Token>,
|
||||
xpub: String? = null,
|
||||
): UpdateWalletManagerResult
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase
|
|||
import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError
|
||||
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -44,7 +44,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
private val createConsolidationTransactionUseCase: CreateConsolidationTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val isXpubDerivedUseCase: IsXpubDerivedUseCase,
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
|
|
@ -138,7 +138,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
is EnableDynamicAddressesError.ServiceError -> {
|
||||
TangemLogger.e("Failed to enable DA: ${error.cause.message}")
|
||||
TangemLogger.e("Failed to enable dynamic addresses: ${error.cause.message}")
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
|
||||
onDismissClick = dismissBottomSheet,
|
||||
)
|
||||
|
|
@ -201,7 +201,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
.onFailure { e ->
|
||||
TangemLogger.e("Failed to disable DA: ${e.message}")
|
||||
TangemLogger.e("Failed to disable dynamic addresses: ${e.message}")
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
|
||||
onDismissClick = dismissBottomSheet,
|
||||
)
|
||||
|
|
@ -277,9 +277,8 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: Replace with actual DA documentation URL
|
||||
private fun onReadMoreClick() {
|
||||
// Stub: open documentation about Dynamic Addresses consolidation
|
||||
// TODO: Replace with actual URL
|
||||
}
|
||||
|
||||
private fun onDisableClick() {
|
||||
|
|
@ -319,7 +318,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
try {
|
||||
dynamicAddressesRepository.disable(userWalletId, network)
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to disable DA after consolidation: ${e.message}")
|
||||
TangemLogger.e("Failed to disable dynamic addresses after consolidation: ${e.message}")
|
||||
}
|
||||
dismissBottomSheet()
|
||||
onDynamicAddressesStateChanged()
|
||||
|
|
@ -338,7 +337,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
// region Common
|
||||
|
||||
private suspend fun isXpubAlreadyDerived(network: Network): Boolean {
|
||||
return isXpubDerivedUseCase(userWalletId, network)
|
||||
return getDerivedXpubUseCase(userWalletId, network) != null
|
||||
}
|
||||
|
||||
private fun isUserCancellation(error: Throwable): Boolean {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue