Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 15:08:26 +03:00
commit 2a5e3fa7eb
220 changed files with 5932 additions and 1815 deletions

View file

@ -1,4 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
implementation(deps.kotlin.serialization)
}

View file

@ -1,5 +1,8 @@
package com.tangem.domain.appcurrency.model
import kotlinx.serialization.Serializable
@Serializable
data class AppCurrency(
val code: String,
val name: String,

View file

@ -1,30 +1,140 @@
package com.tangem.domain.card
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
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.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
/**
* Derivates an exteneded public key (xpub) based on blockchain hardened derivation
*/
class GetExtendedPublicKeyForCurrencyUseCase(
private val derivationsRepository: DerivationsRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
derivation: Network.DerivationPath,
): Either<Throwable, String> {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, String> {
return Either.catch {
val derivationPath = requireNotNull(derivation.value?.let { DerivationPath(it) }) {
error("Derivation is null")
val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found")
val blockchain = Blockchain.fromId(network.id.value)
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
val hdKey = if (isSecp256k1Blockchain) {
userWallet.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found")
} else {
error("No derivation found")
}
val hardenedNodes = derivationPath.nodes.filter { it.isHardened }
val hardenedDerivation = DerivationPath(hardenedNodes)
derivationsRepository.deriveExtendedPublicKey(userWalletId, hardenedDerivation)
?.serialize(NetworkType.Mainnet).orEmpty()
var childKey = makeChildKey(
isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(),
extendedPublicKey = hdKey.extendedPublicKey,
derivationPath = hdKey.path,
)
var parentKey = Key(
derivationPath = childKey.derivationPath.dropLastNodes(1),
extendedPublicKey = null,
)
val pendingDerivations = getPendingDerivations(childKey, parentKey)
val derivedKeys = deriveKeys(
userWalletId = userWalletId,
seedKey = userWallet.wallet.publicKey.seedKey,
paths = pendingDerivations,
)
if (childKey.extendedPublicKey == null) {
childKey = childKey.copy(
extendedPublicKey = derivedKeys[childKey.derivationPath] ?: error("Failed to derive child key"),
)
}
if (parentKey.extendedPublicKey == null) {
parentKey = parentKey.copy(
extendedPublicKey = derivedKeys[parentKey.derivationPath] ?: error("Failed to derive parent key"),
)
}
makeExtendedKey(childKey, parentKey, network.isTestnet)
}
}
private suspend fun deriveKeys(
userWalletId: UserWalletId,
seedKey: ByteArray,
paths: MutableList<DerivationPath>,
): ExtendedPublicKeysMap {
val result = derivationsRepository.derivePublicKeys(userWalletId, mapOf(ByteArrayKey(seedKey) to paths))
return result.getValue(ByteArrayKey(seedKey))
}
private fun makeExtendedKey(childKey: Key, parentKey: Key, isTestnet: Boolean): String {
val publicKey = childKey.extendedPublicKey?.publicKey ?: error("No public key found")
val chainCode = childKey.extendedPublicKey.chainCode
val lastChildNode = childKey.derivationPath.nodes.last()
val parentPublicKey = parentKey.extendedPublicKey?.publicKey
val depth = childKey.derivationPath.nodes.size
val childNumber = lastChildNode.index
val parentFingerprint = parentPublicKey
?.calculateSha256()?.calculateRipemd160()
?.take(PARENT_FINGERPRINT_SIZE)?.toByteArray()
?: error("No parent fingerprint found")
val net = if (isTestnet) NetworkType.Testnet else NetworkType.Mainnet
return ExtendedPublicKey(
publicKey = publicKey,
chainCode = chainCode,
depth = depth,
parentFingerprint = parentFingerprint,
childNumber = childNumber,
).serialize(net)
}
private fun getPendingDerivations(childKey: Key, parentKey: Key): MutableList<DerivationPath> {
val pendingDerivations = mutableListOf<DerivationPath>()
if (childKey.extendedPublicKey == null) {
pendingDerivations.add(childKey.derivationPath)
}
if (parentKey.extendedPublicKey == null) {
pendingDerivations.add(parentKey.derivationPath)
}
return pendingDerivations
}
private fun makeChildKey(
isBip44DerivationStyleXPUB: Boolean,
extendedPublicKey: ExtendedPublicKey,
derivationPath: DerivationPath,
): Key = if (isBip44DerivationStyleXPUB) {
Key(derivationPath.dropLastNodes(2), null)
} else {
Key(derivationPath, extendedPublicKey)
}
private fun DerivationPath.dropLastNodes(count: Int): DerivationPath {
return DerivationPath(nodes.dropLast(count))
}
private data class Key(
val derivationPath: DerivationPath,
val extendedPublicKey: ExtendedPublicKey?,
)
private companion object {
const val PARENT_FINGERPRINT_SIZE = 4
}
}

View file

@ -1,9 +1,10 @@
package com.tangem.domain.card.repository
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
interface DerivationsRepository {
@ -11,5 +12,8 @@ interface DerivationsRepository {
suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
@Throws
suspend fun deriveExtendedPublicKey(userWalletId: UserWalletId, derivation: DerivationPath): ExtendedPublicKey?
suspend fun derivePublicKeys(
userWalletId: UserWalletId,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Map<ByteArrayKey, ExtendedPublicKeysMap>
}

View file

@ -16,7 +16,8 @@ internal class FeedbackDataBuilder {
fun addCardInfo(cardInfo: CardInfo) {
builder.appendKeyValue("Card ID", cardInfo.cardId)
builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion)
builder.appendKeyValue("Imported wallet", if (cardInfo.isImported) "yes" else "no")
builder.appendKeyValue("Linked cards count:", cardInfo.cardsCount)
builder.appendKeyValue("Has seed phrase:", cardInfo.isImported.toString())
builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain)
builder.appendSignedHashes(cardInfo.signedHashesList)
}
@ -34,12 +35,14 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("Outputs count", outputsCount)
if (tokens.isNotEmpty()) {
builder.append("Tokens:")
builder.breakLine()
tokens.forEach { token ->
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Token ID", token.id ?: "[custom token]")
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("Contract address", token.contractAddress)
builder.appendKeyValue("Decimals", token.decimals)
builder.breakLine()
}
}

View file

@ -24,5 +24,6 @@ data class BlockchainInfo(
val id: String?,
val name: String,
val contractAddress: String,
val decimals: String,
)
}

View file

@ -6,6 +6,7 @@ data class CardInfo(
val userWalletId: UserWalletId?,
val cardId: String,
val firmwareVersion: String,
val cardsCount: String,
val cardBlockchain: String?,
val signedHashesList: List<SignedHashes>,
val isImported: Boolean,

View file

@ -11,9 +11,10 @@ android {
dependencies {
api(projects.domain.markets.models)
api(projects.domain.appCurrency.models)
api(projects.domain.core)
api(projects.core.pagination)
api(projects.domain.markets.models)
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)

View file

@ -5,9 +5,9 @@ import java.math.BigDecimal
data class TokenChart(
val interval: PriceChangeInterval,
val priceY: List<BigDecimal>,
val timeStamp: List<Long>,
val timeStamps: List<Long>,
) {
init {
require(priceY.size == timeStamp.size)
require(priceY.size == timeStamps.size)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.domain.markets
data class TokenMarketListConfig(
val fiatPriceCurrency: String,
val searchText: String?,
val showUnder100kMarketCapTokens: Boolean,
val priceChangeInterval: Interval,
val order: Order,
) {

View file

@ -7,7 +7,7 @@ sealed class TokenMarketUpdateRequest {
) : TokenMarketUpdateRequest()
data class UpdateChart(
val interval: PriceChangeInterval,
val interval: TokenMarketListConfig.Interval,
val currency: String,
) : TokenMarketUpdateRequest()
}

View file

@ -16,6 +16,7 @@ class GetMarketsTokenListFlowUseCase(
firstBatchSize = batchFlowType.firstBatchSize,
nextBatchSize = batchFlowType.nextBatchSize,
)
// TODO listen quotes updates flow and update them in other parts of the application
}
enum class BatchFlowType(

View file

@ -0,0 +1,24 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTokenPriceChartUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(
appCurrency: AppCurrency,
interval: PriceChangeInterval,
tokenId: String,
): Either<Unit, TokenChart> {
return Either.catch {
marketsTokenRepository.getChart(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
}.mapLeft {}
}
}

View file

@ -9,4 +9,6 @@ interface MarketsTokenRepository {
firstBatchSize: Int,
nextBatchSize: Int,
): TokenListBatchFlow
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.SettingsRepository
class ShouldSaveAccessCodesUseCase(private val settingsRepository: SettingsRepository) {
suspend operator fun invoke(): Boolean = settingsRepository.shouldSaveAccessCodes()
}

View file

@ -33,15 +33,21 @@ data class Yield(
@Serializable
data class Enter(
val addresses: Addresses,
val args: Map<String, AddressArgument>,
val args: Map<ArgType, AddressArgument>,
) {
@Serializable
data class Addresses(
val address: AddressArgument,
val additionalAddresses: Map<String, AddressArgument>? = null,
val additionalAddresses: Map<ArgType, AddressArgument>? = null,
)
}
enum class ArgType {
ADDRESS,
AMOUNT,
UNKNOWN,
}
}
@Serializable
@ -113,6 +119,6 @@ data class Token(
data class AddressArgument(
val required: Boolean,
val network: String? = null,
val minimum: Double? = null,
val maximum: Double? = null,
val minimum: SerializedBigDecimal? = null,
val maximum: SerializedBigDecimal? = null,
)

View file

@ -3,5 +3,6 @@ package com.tangem.domain.staking.model.stakekit.action
enum class StakingActionCommonType {
ENTER,
EXIT,
PENDING,
PENDING_REWARDS,
PENDING_OTHER,
}

View file

@ -12,6 +12,7 @@ data class ActionParams(
val address: String,
val validatorAddress: String,
val token: Token,
val publicKey: String? = null,
val passthrough: String? = null,
val type: StakingActionType? = null,
)

View file

@ -6,6 +6,8 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for staking gas estimation.
@ -15,9 +17,13 @@ class EstimateGasUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(params: ActionParams): Either<StakingError, StakingGasEstimate> {
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Either<StakingError, StakingGasEstimate> {
return Either.catch {
stakingRepository.estimateGas(params)
stakingRepository.estimateGas(userWalletId, network, params)
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -6,6 +6,8 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.delay
/**
@ -16,14 +18,20 @@ class GetStakingTransactionUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(params: ActionParams): Either<StakingError, StakingTransaction> {
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Either<StakingError, StakingTransaction> {
return Either.catch {
val createAction = stakingRepository.createAction(params)
val createAction = stakingRepository.createAction(userWalletId, network, params)
// workaround, sometimes transaction is not created immediately after actions/enter
delay(PATCH_TRANSACTION_REQUEST_DELAY)
val createdTransaction = createAction.transactions?.get(0) ?: error("No available transaction to patch")
val createdTransaction = createAction.transactions
?.get(createAction.currentStepIndex)
?: error("No available transaction to patch")
val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id)
patchedTransaction

View file

@ -61,9 +61,9 @@ interface StakingRepository {
addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList
suspend fun createAction(params: ActionParams): StakingAction
suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction
suspend fun estimateGas(params: ActionParams): StakingGasEstimate
suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate
suspend fun constructTransaction(transactionId: String): StakingTransaction

View file

@ -167,7 +167,11 @@ class MockStakingRepository : StakingRepository {
balances = listOf(YieldBalance.Error),
)
override suspend fun createAction(params: ActionParams): StakingAction {
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return StakingAction(
id = "quis",
integrationId = "persequeris",
@ -182,7 +186,11 @@ class MockStakingRepository : StakingRepository {
)
}
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
override suspend fun estimateGas(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingGasEstimate {
return StakingGasEstimate(
amount = BigDecimal(0.0001),
token = Token(