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