Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-17 15:30:03 +03:00
parent ca03f18d81
commit cfdc1ea05c
19 changed files with 211 additions and 38 deletions

View file

@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.legacy
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@ -37,8 +37,7 @@ internal object LegacyMiddleware {
)
store.dispatchWithMain(
DetailsAction.PrepareScreen(
// TODO [REDACTED_TASK_KEY]
scanResponse = selectedUserWallet.requireColdWallet().scanResponse,
scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse,
initializedAppSettingsState = initializedAppSettingsStateContent,
),
)

View file

@ -11,6 +11,7 @@ import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.hot.TangemHotSigner
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -43,6 +44,7 @@ internal object TransactionDomainModule {
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tangemHotSignerFactory: TangemHotSigner.Factory,
): SendTransactionUseCase {
return SendTransactionUseCase(
demoConfig = DemoConfig(),
@ -50,6 +52,7 @@ internal object TransactionDomainModule {
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
getHotSigner = tangemHotSignerFactory::create,
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.hot
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester
import com.tangem.tap.features.hot.TangemHotSDKProxy
import dagger.Binds
import dagger.Module
@ -15,4 +17,8 @@ internal interface TangemHotSdkModule {
@Binds
@Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester
}

View file

@ -61,7 +61,11 @@ internal class DefaultDerivationsRepository(
userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
}
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
if (userWallet is UserWallet.Hot) {
return
}
userWallet.requireColdWallet()
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
Timber.d("Nothing to derive")
@ -84,8 +88,12 @@ internal class DefaultDerivationsRepository(
): Boolean {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
if (userWallet is UserWallet.Hot) {
return false
}
val derivations =
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY]
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
networkFactory.create(

View file

@ -0,0 +1,44 @@
package com.tangem.tap.domain.hot
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.*
import javax.inject.Inject
class HotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
) {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> {
val auth = when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
HotWalletId.AuthType.Password -> {
hotWalletPasswordRequester.requestPassword(hotWalletId)
}
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
}
return runCatching {
tangemHotSdk.signHashes(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = auth,
),
dataToSign = dataToSign,
)
}.getOrElse {
if (hotWalletId.authType == HotWalletId.AuthType.Biometry) {
val passwordAuth = hotWalletPasswordRequester.requestPassword(hotWalletId)
tangemHotSdk.signHashes(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = passwordAuth,
),
dataToSign = dataToSign,
)
} else {
throw it
}
}
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.domain.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
interface HotWalletPasswordRequester {
suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password
}

View file

@ -0,0 +1,78 @@
package com.tangem.tap.domain.hot
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.map
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.hot.sdk.model.DataToSign
import com.tangem.operations.sign.SignData
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class TangemHotSigner @AssistedInject constructor(
@Assisted private val userWallet: UserWallet.Hot,
private val hotWalletAccessor: HotWalletAccessor,
) : TransactionSigner {
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
return sign(listOf(hash), publicKey).map { it.first() }
}
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
val result = hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = listOf(
DataToSign(
curve = wallet.curve,
hashes = hashes,
derivationPath = publicKey.derivationPath,
),
),
)
return CompletionResult.Success(result.map { it.signatures }.flatten())
}
override suspend fun multiSign(
dataToSign: List<SignData>,
publicKey: Wallet.PublicKey,
): CompletionResult<Map<ByteArray, ByteArray>> {
val result = hotWalletAccessor.signHashes(
hotWalletId = userWallet.hotWalletId,
dataToSign = dataToSign.map { signData ->
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey }
?: return CompletionResult.Failure(
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
)
DataToSign(
curve = wallet.curve,
hashes = listOf(signData.hash),
derivationPath = signData.derivationPath,
)
},
)
return CompletionResult.Success(
result.mapIndexed { index, data ->
dataToSign[index].publicKey to data.signatures.first()
}.toMap(),
)
}
@AssistedFactory
interface Factory {
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner
}
}

View file

@ -9,7 +9,7 @@ import org.rekotlin.Action
sealed class DetailsAction : Action {
data class PrepareScreen(
val scanResponse: ScanResponse,
val scanResponse: ScanResponse?,
val initializedAppSettingsState: AppSettingsState,
) : DetailsAction()

View file

@ -13,6 +13,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
import com.tangem.core.ui.utils.findActivity
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.home.compose.StoriesScreen
@ -29,6 +30,7 @@ import org.rekotlin.StoreSubscriber
internal class DefaultHomeComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber<HomeState> {
private val model: HomeModel = getOrCreateModel()
@ -58,7 +60,7 @@ internal class DefaultHomeComponent @AssistedInject constructor(
val activity = LocalContext.current.findActivity()
BackHandler(onBack = activity::finish)
SystemBarsIconsDisposable(darkIcons = false)
if (homeState.value.isV2StoriesEnabled) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
StoriesScreenV2(
homeState = homeState,
onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,

View file

@ -7,7 +7,6 @@ import org.rekotlin.StateType
// todo refactor [REDACTED_TASK_KEY]
data class HomeState(
val scanInProgress: Boolean = false,
val isV2StoriesEnabled: Boolean = false,
val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(),
) : StateType {

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.hot
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
import javax.inject.Inject
class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester {
override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password {
return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY]
}
}

View file

@ -15,7 +15,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.converter.Converter
/**
@ -57,13 +56,19 @@ class UserWalletItemUMConverter(
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
userWallet.requireColdWallet()
val cardCount = userWallet.getCardsCount() ?: 1
val text = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val text = when (userWallet) {
is UserWallet.Cold -> {
val cardCount = userWallet.getCardsCount() ?: 1
TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
}
is UserWallet.Hot -> {
TextReference.Res(R.string.hw_mobile_wallet)
}
}
return UserWalletItemUM.Information.Loaded(text)
}

View file

@ -46,5 +46,9 @@
{
"name": "WALLET_BALANCE_FETCHER_ENABLED",
"version": "5.27.0"
},
{
"name": "HOT_WALLET_ENABLED",
"version": "undefined"
}
]

View file

@ -564,7 +564,6 @@ internal class DefaultCurrenciesRepository(
userWallet: UserWallet,
currencyRawId: CryptoCurrency.RawID,
): Flow<List<CryptoCurrency>> {
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
return when {
userWallet.isMultiCurrency -> {
getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
@ -581,7 +580,7 @@ internal class DefaultCurrenciesRepository(
else -> {
val currencies =
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
if (userWallet.requireColdWallet().scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId)
} else {
val currency =
@ -619,9 +618,8 @@ internal class DefaultCurrenciesRepository(
)
}
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver {
return userWalletsStore.getSyncStrict(userWalletId)
.requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY]
override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? {
return (userWalletsStore.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {

View file

@ -265,5 +265,5 @@ interface CurrenciesRepository {
suspend fun syncTokens(userWalletId: UserWalletId)
@Throws
fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver
fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver?
}

View file

@ -76,7 +76,7 @@ class WalletBalanceFetcher internal constructor(
val cardTypesResolver = currenciesRepository.getCardTypesResolver(userWalletId = userWalletId)
val fetcher = when {
cardTypesResolver.isMultiwalletAllowed() -> multiWalletBalanceFetcher
cardTypesResolver == null || cardTypesResolver.isMultiwalletAllowed() -> multiWalletBalanceFetcher
cardTypesResolver.isSingleWalletWithToken() -> singleWalletWithTokenBalanceFetcher
cardTypesResolver.isSingleWallet() -> singleWalletBalanceFetcher
else -> error("Unknown type of wallet: $userWalletId")

View file

@ -25,7 +25,6 @@ import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.error.parseWrappedError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
class SendTransactionUseCase(
private val demoConfig: DemoConfig,
@ -33,6 +32,7 @@ class SendTransactionUseCase(
private val transactionRepository: TransactionRepository,
private val walletManagersFacade: WalletManagersFacade,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val getHotSigner: (UserWallet.Hot) -> TransactionSigner,
) {
suspend operator fun invoke(
txsData: List<TransactionData>,
@ -40,22 +40,30 @@ class SendTransactionUseCase(
network: Network,
sendMode: TransactionSender.MultipleTransactionSendMode,
): Either<SendTransactionError, List<String>> {
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
val signer = when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val coldSigner = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
coldSigner
}
is UserWallet.Hot -> {
getHotSigner(userWallet)
}
}
val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal()
if (userWallet.scanResponse.card.isStart2Coin) {
if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isStart2Coin) {
cardSdkConfigRepository.setLinkedTerminal(false)
}
val sendResult = try {
if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) {
if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(cardId = userWallet.cardId)) {
sendDemo(
userWallet = userWallet,
network = network,

View file

@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.domain.models.MobileWallet
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.DeriveWalletRequest
@ -58,7 +57,7 @@ class HotUserWalletBuilder @AssistedInject constructor(
UserWallet.Hot(
name = generateWalletNameUseCase.invokeForHot(),
walletId = UserWalletId(wallets.first().publicKey),
walletId = UserWalletIdBuilder.walletPublicKey(wallets.first().publicKey),
hotWalletId = hotWalletId,
wallets = wallets,
backedUp = false,

View file

@ -25,7 +25,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.impl.R
@ -194,8 +193,7 @@ internal class MarketsPortfolioModel @Inject constructor(
private fun loadArtworks(wallets: List<UserWallet>) {
modelScope.launch {
loadArtworksMutex.withLock {
wallets.forEach { wallet ->
wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
wallets.filterIsInstance<UserWallet.Cold>().forEach { wallet ->
if (!loadedArtworks.containsKey(wallet.walletId)) {
val artwork = getCardImageUseCase(
cardId = wallet.cardId,