Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-13 19:48:06 +03:00
parent 27e9400b2a
commit b38ebb3995
16 changed files with 196 additions and 71 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
/**