Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-14 14:23:17 +03:00
parent ebc4350919
commit 7e23a3a63b
18 changed files with 336 additions and 42 deletions

View file

@ -41,6 +41,7 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase
@ -200,7 +201,12 @@ internal class MainViewModel @Inject constructor(
userWalletsListManager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.setContext(userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY]
when (userWallet) {
is UserWallet.Cold -> Analytics.setContext(userWallet.scanResponse)
is UserWallet.Hot -> {
// TODO [REDACTED_TASK_KEY]
}
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)

View file

@ -9,13 +9,23 @@ import com.tangem.domain.wallets.models.requireColdWallet
internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider {
override fun getCardPublicKey(): String {
return userWalletsListManager.selectedUserWalletSync
?.requireColdWallet()?.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""
val userWallet = userWalletsListManager.selectedUserWalletSync
return when (userWallet) {
is UserWallet.Cold -> userWallet.scanResponse.card.cardPublicKey.toHexString()
is UserWallet.Hot -> userWallet.wallets?.firstOrNull()?.publicKey?.toHexString() ?: ""
null -> ""
}
}
override fun getCardId(): String {
return userWalletsListManager.selectedUserWalletSync
?.requireColdWallet()?.scanResponse?.card?.cardId ?: ""
val userWallet = userWalletsListManager.selectedUserWalletSync
if (userWallet !is UserWallet.Cold) {
return ""
}
return userWallet.requireColdWallet().scanResponse.card.cardId
}
override fun getCardsPublicKeys(): Map<String, String> {

View file

@ -74,7 +74,7 @@ fun WalletManagerFactory.makeWalletManagerForApp(
}
}
private fun makePublicKey(
fun makePublicKey(
seedKey: ByteArray,
blockchain: Blockchain,
derivationPath: DerivationPath,

View file

@ -41,7 +41,6 @@ import com.tangem.domain.walletmanager.utils.*
import com.tangem.domain.walletmanager.utils.WalletManagerFactory
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
@ -371,21 +370,31 @@ class DefaultWalletManagersFacade(
initMutex.withLock {
val userWallet = getUserWallet(userWalletId)
userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY]
var walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
val path = derivationPath?.let { DerivationPath(rawPath = it) }
if (walletManager == null) {
when (userWallet) {
is UserWallet.Hot -> {
walletManager = walletManagerFactory.createWalletManagerForHot(
hotWallet = userWallet,
blockchain = blockchain,
derivationPath = path,
)
}
is UserWallet.Cold -> {
walletManager = walletManagerFactory.createWalletManager(
scanResponse = userWallet.scanResponse,
blockchain = blockchain,
derivationPath = derivationPath?.let { DerivationPath(rawPath = it) },
derivationPath = path,
)
}
}
walletManager ?: return null
walletManagersStore.store(userWalletId, walletManager)
}
return walletManager

View file

@ -6,9 +6,11 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.makePublicKey
import com.tangem.domain.common.extensions.makeWalletManagerForApp
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import timber.log.Timber
internal class WalletManagerFactory(
@ -34,6 +36,42 @@ internal class WalletManagerFactory(
}
}
suspend fun createWalletManagerForHot(
hotWallet: UserWallet.Hot,
blockchain: Blockchain,
derivationPath: DerivationPath?,
): WalletManager? {
val curve = blockchain.getSupportedCurves().first()
val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve }
?: return null
return try {
val factory = blockchainSDKFactory.getWalletManagerFactorySync() ?: return null
if (derivationPath == null) {
factory.createLegacyWalletManager(
blockchain = blockchain,
walletPublicKey = selectedWallet.publicKey,
curve = selectedWallet.curve,
)
} else {
factory.createWalletManager(
blockchain = blockchain,
publicKey = makePublicKey(
seedKey = selectedWallet.publicKey,
blockchain = blockchain,
derivationPath = derivationPath,
derivedWalletKeys = selectedWallet.derivedKeys,
isWallet2 = true,
) ?: return null,
curve = selectedWallet.curve,
)
}
} catch (e: Throwable) {
Timber.w(e, "Failed to create wallet manager for $blockchain")
null
}
}
private fun getDerivationParams(
derivationPath: DerivationPath?,
derivationStyleProvider: DerivationStyleProvider,

View file

@ -3,4 +3,22 @@ package com.tangem.domain.models
data class ArtworkModel(
val verifiedArtwork: ByteArray? = null,
val defaultUrl: String,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ArtworkModel
if (!verifiedArtwork.contentEquals(other.verifiedArtwork)) return false
if (defaultUrl != other.defaultUrl) return false
return true
}
override fun hashCode(): Int {
var result = verifiedArtwork?.contentHashCode() ?: 0
result = 31 * result + defaultUrl.hashCode()
return result
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.domain.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.card.EllipticCurve
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.serialization.MobileWalletAsStringSerializer
import kotlinx.serialization.Serializable
@Serializable(with = MobileWalletAsStringSerializer::class)
@JsonClass(generateAdapter = true)
class MobileWallet(
@Json(name = "publicKey")
val publicKey: ByteArray,
@Json(name = "chainCode")
val chainCode: ByteArray?,
@Json(name = "curve")
val curve: EllipticCurve,
@Json(name = "derivedKeys")
val derivedKeys: Map<DerivationPath, ExtendedPublicKey>,
) {
val extendedPublicKey: ExtendedPublicKey?
get() = chainCode?.let {
ExtendedPublicKey(
publicKey = publicKey,
chainCode = it,
)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.domain.models.serialization
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.domain.models.MobileWallet
import com.tangem.domain.models.scan.serialization.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
internal object MobileWalletAsStringSerializer : KSerializer<MobileWallet> {
private val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.addLast(KotlinJsonAdapterFactory())
.build()
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MobileWallet", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): MobileWallet {
return moshi.adapter(MobileWallet::class.java).fromJson(decoder.decodeString())!!
}
override fun serialize(encoder: Encoder, value: MobileWallet) {
encoder.encodeString(moshi.adapter(MobileWallet::class.java).toJson(value)!!)
}
}

View file

@ -32,6 +32,7 @@ dependencies {
// region Tangem libraries
implementation(tangemDeps.blockchain) // android-library
implementation(tangemDeps.card.core)
implementation(tangemDeps.hot.core)
// endregion
// region DI

View file

@ -8,6 +8,7 @@ plugins {
dependencies {
// region Tangem libraries
implementation(tangemDeps.card.core)
implementation(tangemDeps.hot.core)
// endregion
// region Domain modules

View file

@ -1,8 +1,9 @@
package com.tangem.domain.wallets.models
import com.tangem.domain.models.MobileWallet
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet.Cold
import com.tangem.hot.sdk.model.HotWalletId
import kotlinx.serialization.Serializable
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@ -56,17 +57,21 @@ sealed interface UserWallet {
data class Hot(
override val name: String,
override val walletId: UserWalletId,
val isLocked: Boolean,
) : UserWallet
val hotWalletId: HotWalletId,
val wallets: List<MobileWallet>?,
) : UserWallet {
val isLocked: Boolean get() = wallets == null
}
}
@OptIn(ExperimentalContracts::class)
fun UserWallet.requireColdWallet(): Cold {
fun UserWallet.requireColdWallet(): UserWallet.Cold {
contract {
returns() implies (this@requireColdWallet is Cold)
returns() implies (this@requireColdWallet is UserWallet.Cold)
}
return this as? Cold
return this as? UserWallet.Cold
?: error("This user wallet is not a cold wallet")
}

View file

@ -0,0 +1,69 @@
package com.tangem.domain.wallets.builder
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
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class HotUserWalletBuilder @AssistedInject constructor(
@Assisted private val hotWalletId: HotWalletId,
private val hotSdk: TangemHotSdk,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
) {
suspend fun build(): UserWallet.Hot {
val allNetworks = Blockchain.entries // TODO use HotDerivationsRepository to get supported networks
val requests = allNetworks.map {
val curves = it.getSupportedCurves()
val derivationPath = it.derivationPath(DerivationStyle.V3)
curves.map {
DeriveWalletRequest.Request(
curve = it,
paths = listOfNotNull(derivationPath),
)
}
}.flatten()
val derivationResult = hotSdk.derivePublicKey(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,
auth = HotAuth.NoAuth,
),
request = DeriveWalletRequest(
requests = requests,
),
)
val wallets = derivationResult.responses.map {
MobileWallet(
publicKey = it.seedKey.publicKey,
chainCode = it.seedKey.chainCode,
curve = it.curve,
derivedKeys = it.publicKeys,
)
}
return UserWallet.Hot(
name = generateWalletNameUseCase.invokeForHot(),
walletId = UserWalletId(wallets.first().publicKey),
hotWalletId = hotWalletId,
wallets = wallets,
)
}
@AssistedFactory
interface Factory {
fun create(hotWalletId: HotWalletId): HotUserWalletBuilder
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.wallets.repository
import com.tangem.domain.models.network.Network
interface HotDerivationsRepository {
fun getAllSupportedNetworks(): Set<Network>
}

View file

@ -21,6 +21,12 @@ class GenerateWalletNameUseCase(
return suggestedWalletName(defaultName, existingNames)
}
fun invokeForHot(): String {
val defaultName = "Mobile Wallet"
val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet()
return suggestedWalletName(defaultName, existingNames)
}
private fun suggestedWalletName(defaultName: String, existingNames: Set<String>): String {
val startIndex = 2
if (!existingNames.contains(defaultName)) {

View file

@ -1,8 +1,11 @@
package com.tangem.features.hotwallet.createmobilewallet
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
@ -10,12 +13,15 @@ import com.tangem.hot.sdk.model.MnemonicType
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class CreateMobileWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
private val saveUserWalletUseCase: SaveWalletUseCase,
private val router: Router,
private val tangemHotSdk: TangemHotSdk,
) : Model() {
@ -25,19 +31,28 @@ internal class CreateMobileWalletModel @Inject constructor(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
private fun onCreateClick() {
modelScope.launch {
tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
// TODO
// val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId, tangemHotSdk)
// saveUserWalletUseCase(
// hotUserWalletBuilder.build(),
// )
uiState.update {
it.copy(createButtonLoading = true)
}
// router.push(AppRoute.Wallet)
runCatching {
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
saveUserWalletUseCase(
hotUserWalletBuilder.build(),
)
router.push(AppRoute.Wallet)
}.onFailure {
uiState.update {
it.copy(createButtonLoading = false)
}
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.createmobilewallet.entity
internal data class CreateMobileWalletUM(
val createButtonLoading: Boolean,
val onBackClick: () -> Unit,
val onCreateClick: () -> Unit,
)

View file

@ -85,7 +85,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
.fillMaxWidth()
.padding(16.dp),
text = stringResourceSafe(R.string.common_create),
showProgress = false,
showProgress = state.createButtonLoading,
enabled = true,
onClick = state.onCreateClick,
)
@ -133,6 +133,7 @@ private fun PreviewCreateWalletContent() {
CreateMobileWalletContent(
state = CreateMobileWalletUM(
onBackClick = {},
createButtonLoading = false,
onCreateClick = {},
),
)

View file

@ -6,7 +6,6 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
@ -26,14 +25,37 @@ internal class WalletLoadingStateFactory(
) {
fun create(userWallet: UserWallet): WalletState {
userWallet.requireColdWallet()
return userWallet.createStateByWalletType(
return when (userWallet) {
is UserWallet.Cold -> {
userWallet.createStateByWalletType(
multiCurrencyCreator = { createLoadingMultiCurrencyContent(userWallet) },
singleCurrencyCreator = { createLoadingSingleCurrencyContent(userWallet) },
visaWalletCreator = { createLoadingVisaWalletContent(userWallet) },
)
}
is UserWallet.Hot -> {
createLoadingHotWalletContent(userWallet)
}
}
}
private fun createLoadingHotWalletContent(userWallet: UserWallet.Hot): WalletState.MultiCurrency.Content {
return WalletState.MultiCurrency.Content(
pullToRefreshConfig = createPullToRefreshConfig(),
walletCardState = WalletCardState.Loading(
id = userWallet.walletId,
title = userWallet.name,
additionalInfo = null, // TODO [REDACTED_TASK_KEY]
imageResId = null, // TODO [REDACTED_TASK_KEY]
dropDownItems = persistentListOf(),
),
buttons = createMultiWalletActions(userWallet),
warnings = persistentListOf(),
bottomSheetConfig = null,
tokensListState = WalletTokensListState.ContentState.Loading,
nftState = WalletNFTItemUM.Hidden,
)
}
private fun createLoadingMultiCurrencyContent(userWallet: UserWallet.Cold): WalletState.MultiCurrency.Content {
return WalletState.MultiCurrency.Content(
@ -99,15 +121,21 @@ internal class WalletLoadingStateFactory(
)
}
private fun createMultiWalletActions(userWallet: UserWallet.Cold): PersistentList<WalletManageButton> {
val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
private fun createMultiWalletActions(userWallet: UserWallet): PersistentList<WalletManageButton> {
val isSingleWalletWithToken =
userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
if (isSingleWalletWithToken) return persistentListOf()
return persistentListOf(
WalletManageButton.Buy(
enabled = true,
dimContent = false,
onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId, WALLET_TYPE) },
onClick = {
clickIntents.onMultiWalletBuyClick(
userWalletId = userWallet.walletId,
WALLET_TYPE,
)
},
),
WalletManageButton.Swap(
enabled = true,
@ -124,14 +152,24 @@ internal class WalletLoadingStateFactory(
private fun createVisaDimmedButtons(): PersistentList<WalletManageButton> {
return persistentListOf(
WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null),
WalletManageButton.Receive(
enabled = true,
dimContent = true,
onClick = {},
onLongClick = null,
),
WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}),
)
}
private fun createDimmedButtons(): PersistentList<WalletManageButton> {
return persistentListOf(
WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null),
WalletManageButton.Receive(
enabled = true,
dimContent = true,
onClick = {},
onLongClick = null,
),
WalletManageButton.Send(enabled = true, dimContent = true, onClick = {}),
WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}),
WalletManageButton.Sell(enabled = true, dimContent = true, onClick = {}),