Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-20 13:34:54 +03:00
parent f509c71955
commit 7a7a2bed12
7 changed files with 527 additions and 2 deletions

View file

@ -183,7 +183,9 @@ dependencies {
}
/** Testing libraries */
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
androidTestImplementation(deps.test.junit.android)
androidTestImplementation(deps.test.espresso)
@ -205,7 +207,7 @@ dependencies {
/** Excluded dependencies */
implementation("com.google.guava:guava:30.0-android") {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
exclude(group="com.google.guava", module = "listenablefuture")
exclude(group = "com.google.guava", module = "listenablefuture")
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
/**
[REDACTED_AUTHOR]
*/
internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
private val factory = CryptoCurrencyFactory()
val cardano by lazy { listOf(createCoin(blockchain = Blockchain.Cardano)) }
val chia by lazy { listOf(element = createCoin(Blockchain.Chia)) }
val ethereum by lazy { listOf(element = createCoin(Blockchain.Ethereum)) }
val chiaAndEthereum by lazy {
listOf(
createCoin(blockchain = Blockchain.Chia),
createCoin(blockchain = Blockchain.Ethereum),
)
}
val ethereumAndStellar by lazy {
listOf(
createCoin(blockchain = Blockchain.Ethereum),
createCoin(blockchain = Blockchain.Stellar),
)
}
val ethereumTokenWithBinanceDerivation by lazy {
listOf(
createCustomToken(blockchain = Blockchain.Ethereum, derivationBlockchain = Blockchain.Binance),
)
}
private fun createCoin(blockchain: Blockchain): CryptoCurrency {
return factory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)!!
}
// Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood
private fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency {
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(blockchain.id),
suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id),
),
network = Network(
id = Network.ID(value = blockchain.id),
backendId = "NEVER-MIND",
name = blockchain.fullName,
currencySymbol = "NEVER-MIND",
derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(
scanResponse.derivationStyleProvider.getDerivationStyle(),
)!!.rawPath,
),
isTestnet = false,
standardType = Network.StandardType.ERC20,
),
name = "NEVER-MIND",
symbol = "NEVER-MIND",
decimals = 8,
iconUrl = null,
isCustom = false,
contractAddress = "NEVER-MIND",
)
}
private fun createToken(blockchain: Blockchain): CryptoCurrency {
return factory.createToken(
sdkToken = Token(symbol = "NEVER-MIND", contractAddress = "NEVER-MIND", decimals = 8),
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)!!
}
}

View file

@ -0,0 +1,161 @@
package com.tangem.tap.domain.card
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.tangem.common.CompletionResult
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.configs.MultiWalletCardConfig
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultDerivationsRepositoryTest {
private val tangemSdkManager = mockk<TangemSdkManager>()
private val userWalletsStore = mockk<UserWalletsStore>()
private val repository = DefaultDerivationsRepository(
tangemSdkManager = tangemSdkManager,
userWalletsStore = userWalletsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val defaultUserWalletId = UserWalletId("011")
private val defaultUserWallet = UserWallet(
name = "",
walletId = defaultUserWalletId,
artworkUrl = "",
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig, derivedKeys = emptyMap()),
)
@Test
fun `error if userWalletId not found`() = runTest {
coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns null
runCatching {
repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList())
}
.onSuccess { error("Should throws exception") }
.onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) }
coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) }
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any()) }
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
}
@SuppressLint("CheckResult")
@Test
fun `success if card is not supported derivations`() = runTest {
coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns defaultUserWallet
runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) }
.onSuccess { Truth.assertThat(it) }
.onFailure { error("Should returns success") }
coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) }
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any()) }
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
}
@SuppressLint("CheckResult")
@Test
fun `success if currencies is empty`() = runTest {
val userWallet = defaultUserWallet.copy(
scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
)
coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet
runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) }
.onSuccess { Truth.assertThat(it) }
.onFailure { error("Should returns success") }
coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) }
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any()) }
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
}
@SuppressLint("CheckResult")
@Test
fun `success if card already has derivations`() = runTest {
val userWallet = defaultUserWallet.copy(
scanResponse = ScanResponseMockFactory.create(
cardConfig = MultiWalletCardConfig,
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
),
)
coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet
runCatching {
repository.derivePublicKeys(
userWalletId = defaultUserWalletId,
currencies = CryptoCurrenciesMocks(userWallet.scanResponse).ethereum,
)
}
.onSuccess { Truth.assertThat(it) }
.onFailure { error("Should returns success") }
coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) }
coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any()) }
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
}
@Test
fun `error if tangemSdkManager throws exception`() = runTest {
val userWallet = defaultUserWallet.copy(
scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
)
coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet
coEvery { tangemSdkManager.derivePublicKeys(null, any()) } throws ScanCardException.UserCancelled
runCatching {
repository.derivePublicKeys(
userWalletId = defaultUserWalletId,
currencies = CryptoCurrenciesMocks(userWallet.scanResponse).ethereum,
)
}
.onSuccess { error("Should throws exception") }
.onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) }
coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) }
coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any()) }
coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) }
}
@SuppressLint("CheckResult")
@Test
fun `success case`() = runTest {
val userWallet = defaultUserWallet.copy(
scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()),
)
coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet
coEvery { tangemSdkManager.derivePublicKeys(null, any()) } returns CompletionResult.Success(
DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys),
)
coEvery { userWalletsStore.update(defaultUserWalletId, any()) } just Runs
runCatching {
repository.derivePublicKeys(
userWalletId = defaultUserWalletId,
currencies = CryptoCurrenciesMocks(userWallet.scanResponse).ethereum,
)
}
.onSuccess { Truth.assertThat(it) }
.onFailure { error("Should returns success") }
coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) }
coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any()) }
coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) }
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationConfigV2
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.derivation.ExtendedPublicKeysMap
/**
[REDACTED_AUTHOR]
*/
internal object DerivedKeysMocks {
val ethereumDerivedKeys = mapOf(
EllipticCurve.Secp256k1.name.toByteArray().toMapKey() to ExtendedPublicKeysMap(
mapOf(
DerivationConfigV2.derivations(Blockchain.Ethereum).values.first() to ExtendedPublicKey(
publicKey = ByteArray(0),
chainCode = ByteArray(0),
depth = 2646,
parentFingerprint = ByteArray(0),
childNumber = 1142,
),
),
),
)
}

View file

@ -0,0 +1,136 @@
package com.tangem.tap.domain.card
import com.google.common.truth.Truth
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationConfigV2
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.configs.MultiWalletCardConfig
import com.tangem.domain.common.configs.Wallet2CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class MissedDerivationsFinderTest {
@Test
fun `empty derivations for empty currencies`() {
val scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig, derivedKeys = emptyMap())
val finder = MissedDerivationsFinder(scanResponse)
val actual = finder.find(emptyList())
Truth.assertThat(actual).isEmpty()
}
@Test
fun `empty derivations for non supported blockchains`() {
// Bls is not supported
val scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig, derivedKeys = emptyMap())
val finder = MissedDerivationsFinder(scanResponse)
val currencies = CryptoCurrenciesMocks(scanResponse).chia
val actual = finder.find(currencies)
Truth.assertThat(actual).isEmpty()
}
@Test
fun `derivations ONLY for supported blockchains`() {
// Bls is not supported
val scanResponse = ScanResponseMockFactory.create(
cardConfig = GenericCardConfig,
derivedKeys = emptyMap(),
).let {
it.copy(
card = it.card.copy(
settings = it.card.settings.copy(isHDWalletAllowed = true, isBackupAllowed = true),
),
)
}
val finder = MissedDerivationsFinder(scanResponse)
val currencies = CryptoCurrenciesMocks(scanResponse).chiaAndEthereum
val actual = finder.find(currencies)
Truth.assertThat(actual).containsExactly(
ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()),
listOf(DerivationConfigV2.derivations(Blockchain.Ethereum).values.first()),
)
}
@Test
fun `derivations for custom token`() {
val scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
val finder = MissedDerivationsFinder(scanResponse)
val currencies = CryptoCurrenciesMocks(scanResponse).ethereumTokenWithBinanceDerivation
val actual = finder.find(currencies)
Truth.assertThat(actual).containsExactly(
ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()),
listOf(
DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(),
DerivationConfigV2.derivations(Blockchain.Binance).values.first(),
),
)
}
@Test
fun `derivations for cardano`() {
val scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap())
val finder = MissedDerivationsFinder(scanResponse)
val currencies = CryptoCurrenciesMocks(scanResponse).cardano
val actual = finder.find(currencies)
Truth.assertThat(actual).containsExactly(
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()),
listOf(
DerivationConfigV2.derivations(Blockchain.Cardano).values.first(),
CardanoUtils.extendedDerivationPath(
derivationPath = DerivationPath(
Blockchain.Cardano.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())!!
.rawPath,
),
),
),
)
}
@Test
fun `empty derivations for already derived currencies`() {
val scanResponse = ScanResponseMockFactory.create(
cardConfig = Wallet2CardConfig,
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
)
val finder = MissedDerivationsFinder(scanResponse)
val currencies = CryptoCurrenciesMocks(scanResponse).ethereum
val actual = finder.find(currencies)
Truth.assertThat(actual).isEmpty()
}
@Test
fun `derivations ONLY for never derived currencies`() {
val scanResponse = ScanResponseMockFactory.create(
cardConfig = MultiWalletCardConfig,
derivedKeys = DerivedKeysMocks.ethereumDerivedKeys,
)
val finder = MissedDerivationsFinder(scanResponse)
val currencies = CryptoCurrenciesMocks(scanResponse).ethereumAndStellar
val actual = finder.find(currencies)
Truth.assertThat(actual).containsExactly(
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()),
listOf(DerivationConfigV2.derivations(Blockchain.Stellar).values.first()),
)
}
}

View file

@ -0,0 +1,109 @@
package com.tangem.tap.domain.card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.configs.MultiWalletCardConfig
import com.tangem.domain.common.configs.Wallet2CardConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import java.util.Date
/**
[REDACTED_AUTHOR]
*/
object ScanResponseMockFactory {
private val genericFirmwareVersion = CardDTO.FirmwareVersion(
major = 3,
minor = 29,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
)
private val walletFirmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 0,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
)
private val wallet2FirmwareVersion = CardDTO.FirmwareVersion(
major = 6,
minor = 33,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
)
fun create(cardConfig: CardConfig, derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>): ScanResponse {
return ScanResponse(
card = CardDTO(
cardId = "NEVER-MIND",
batchId = "NEVER-MIND",
cardPublicKey = ByteArray(0),
firmwareVersion = when (cardConfig) {
GenericCardConfig -> genericFirmwareVersion
MultiWalletCardConfig -> walletFirmwareVersion
Wallet2CardConfig -> wallet2FirmwareVersion
},
manufacturer = CardDTO.Manufacturer(name = "NEVER-MIND", manufactureDate = Date(), signature = null),
issuer = CardDTO.Issuer(name = "NEVER-MIND", publicKey = ByteArray(0)),
settings = CardDTO.Settings(
securityDelay = 0,
maxWalletsCount = 0,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = false,
isLinkedTerminalEnabled = false,
isBackupAllowed = cardConfig is MultiWalletCardConfig,
supportedEncryptionModes = listOf(),
isFilesAllowed = true,
isHDWalletAllowed = cardConfig is MultiWalletCardConfig,
isKeysImportAllowed = true,
),
userSettings = null,
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.Current,
isAccessCodeSet = false,
isPasscodeSet = null,
supportedCurves = listOf(),
wallets = cardConfig.mandatoryCurves.map {
CardDTO.Wallet(
CardWallet(
publicKey = it.name.toByteArray(), // IMPORTANT: public key must equal to curve name
chainCode = null,
curve = it,
settings = createSettings(),
totalSignedHashes = null,
remainingSignatures = null,
index = 0,
isImported = true,
hasBackup = true,
derivedKeys = mapOf(),
),
)
},
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Skipped,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = null,
),
productType = ProductType.Wallet2,
walletData = null,
derivedKeys = derivedKeys,
)
}
private fun createSettings(): CardWallet.Settings {
val constructor = CardWallet.Settings::class.java.declaredConstructors[0]
constructor.isAccessible = true
return constructor.newInstance(false) as CardWallet.Settings
}
}

View file

@ -7,9 +7,10 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
// FIXME: Cover with tests [REDACTED_JIRA]
// TODO: Convert to class [REDACTED_JIRA]
interface DerivePublicKeysUseCase {
// TODO: delete [REDACTED_JIRA]
@Deprecated(message = "Use invoke(cardId: String?, derivations: Map<ByteArrayKey, List<DerivationPath>>) instead")
suspend operator fun invoke(
cardId: String? = null,