Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 23:22:34 +05:00
parent 941863f79b
commit 411a591509
18 changed files with 453 additions and 43 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
@ -9,8 +10,8 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
@ -372,4 +373,32 @@ internal object WalletsDomainModule {
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesUnlockHotWalletContextualUseCase(
hotWalletAccessor: HotWalletAccessor,
): UnlockHotWalletContextualUseCase {
return UnlockHotWalletContextualUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesClearHotWalletContextualUnlockUseCase(
hotWalletAccessor: HotWalletAccessor,
): ClearHotWalletContextualUnlockUseCase {
return ClearHotWalletContextualUnlockUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
@Provides
@Singleton
fun providesExportSeedPhraseUseCase(hotWalletAccessor: HotWalletAccessor): ExportSeedPhraseUseCase {
return ExportSeedPhraseUseCase(
hotWalletAccessor = hotWalletAccessor,
)
}
}

View file

@ -32,6 +32,13 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
override suspend fun exportBackup(unlockHotWallet: UnlockHotWallet): ByteArray =
callSdk { exportBackup(unlockHotWallet) }
override suspend fun clearUnlockContext(hotWalletId: HotWalletId) {
callSdk { clearUnlockContext(hotWalletId) }
}
override suspend fun getContextUnlock(unlockHotWallet: UnlockHotWallet): UnlockHotWallet =
callSdk { getContextUnlock(unlockHotWallet) }
override suspend fun delete(id: HotWalletId) = callSdk { delete(id) }
override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId =

View file

@ -23,6 +23,7 @@ import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
@ -109,6 +110,7 @@ internal class ChildFactory @Inject constructor(
private val walletActivationComponentFactory: WalletActivationComponent.Factory,
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val viewPhraseComponentFactory: ViewPhraseComponent.Factory,
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
@ -532,6 +534,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = updateAccessCodeComponentFactory,
)
}
is AppRoute.ViewPhrase -> {
createComponentChild(
context = context,
params = ViewPhraseComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = viewPhraseComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,

View file

@ -334,6 +334,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}")
@Serializable
data class ViewPhrase(
val userWalletId: UserWalletId,
) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,

View file

@ -9,15 +9,26 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.exception.WrongPasswordException
import com.tangem.hot.sdk.model.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlin.collections.set
class DefaultHotWalletAccessor @Inject constructor(
private val tangemHotSdk: TangemHotSdk,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
private val walletsRepository: WalletsRepository,
dispatchers: CoroutineDispatcherProvider,
) : HotWalletAccessor {
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
private var contextualUnlockHotWallet: ConcurrentHashMap<HotWalletId, UnlockHotWallet?> = ConcurrentHashMap()
override suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> =
hotSdkRequest(hotWalletId) { unlock ->
tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign)
@ -30,6 +41,24 @@ class DefaultHotWalletAccessor @Inject constructor(
tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request)
}
override suspend fun exportSeedPhrase(hotWalletId: HotWalletId): SeedPhrasePrivateInfo {
val unlockHotWallet = contextualUnlockHotWallet[hotWalletId] ?: hotSdkRequest(hotWalletId) { it }
return tangemHotSdk.exportMnemonic(unlockHotWallet = unlockHotWallet)
}
override suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet = hotSdkRequest(hotWalletId) {
tangemHotSdk.getContextUnlock(it).also { unlockHotWallet ->
contextualUnlockHotWallet[hotWalletId] = unlockHotWallet
}
}
override fun clearContextualUnlock(hotWalletId: HotWalletId) {
contextualUnlockHotWallet[hotWalletId] = null
scope.launch {
tangemHotSdk.clearUnlockContext(hotWalletId)
}
}
private suspend fun <T> hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T {
val isAccessCodeRequired = walletsRepository.requireAccessCode()

View file

@ -7,4 +7,10 @@ interface HotWalletAccessor {
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData>
suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse
suspend fun exportSeedPhrase(hotWalletId: HotWalletId): SeedPhrasePrivateInfo
suspend fun unlockContextual(hotWalletId: HotWalletId): UnlockHotWallet
fun clearContextualUnlock(hotWalletId: HotWalletId)
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
class ClearHotWalletContextualUnlockUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, Unit> {
return Either.catch {
hotWalletAccessor.clearContextualUnlock(hotWalletId)
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.SeedPhrasePrivateInfo
class ExportSeedPhraseUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
suspend operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, SeedPhrasePrivateInfo> {
return Either.catch {
hotWalletAccessor.exportSeedPhrase(hotWalletId)
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.hot.sdk.model.UnlockHotWallet
class UnlockHotWalletContextualUseCase(
private val hotWalletAccessor: HotWalletAccessor,
) {
suspend operator fun invoke(hotWalletId: HotWalletId): Either<Throwable, UnlockHotWallet> {
return Either.catch {
hotWalletAccessor.unlockContextual(hotWalletId)
}
}
}

View file

@ -149,7 +149,7 @@ private fun WalletBlock(
.padding(top = 8.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
color = TangemTheme.colors.background.secondary,
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.clickable(onClick = onClick)

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface ViewPhraseComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, ViewPhraseComponent>
}

View file

@ -0,0 +1,38 @@
package com.tangem.features.hotwallet.viewphrase
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
import com.tangem.features.hotwallet.viewphrase.ui.ViewPhraseContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultViewPhraseComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: ViewPhraseComponent.Params,
) : ViewPhraseComponent, AppComponentContext by context {
private val model: ViewPhraseModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ViewPhraseContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : ViewPhraseComponent.Factory {
override fun create(
context: AppComponentContext,
params: ViewPhraseComponent.Params,
): DefaultViewPhraseComponent
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.hotwallet.viewphrase.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.DefaultViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface ViewPhraseModule {
@Binds
@IntoMap
@ClassKey(ViewPhraseModel::class)
fun bindViewPhraseModel(model: ViewPhraseModel): Model
@Binds
fun bindViewPhraseComponentFactory(factory: DefaultViewPhraseComponent.Factory): ViewPhraseComponent.Factory
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.hotwallet.viewphrase.entity
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class ViewPhraseUM(
val onBackClick: () -> Unit,
val words: ImmutableList<EnumeratedTwoColumnGridItem> = persistentListOf(),
)

View file

@ -0,0 +1,76 @@
package com.tangem.features.hotwallet.viewphrase.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
import com.tangem.domain.wallets.usecase.ExportSeedPhraseUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class ViewPhraseModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val exportSeedPhraseUseCase: ExportSeedPhraseUseCase,
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
) : Model() {
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
private var hotWalletId: HotWalletId? = null
internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
init {
loadSeedPhrase()
}
private fun loadSeedPhrase() {
val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrElse { error("User wallet with id ${params.userWalletId} not found") }
if (userWallet is UserWallet.Hot) {
hotWalletId = userWallet.hotWalletId
modelScope.launch {
val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId)
.getOrElse { error("Unable to export seed phrase for wallet with id ${params.userWalletId}") }
.mnemonic
.mnemonicComponents
uiState.update {
it.copy(
words = words.mapIndexed { index, s ->
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList(),
)
}
}
}
}
override fun onDestroy() {
hotWalletId?.let { clearHotWalletContextualUnlockUseCase.invoke(it) }
super.onDestroy()
}
}

View file

@ -0,0 +1,103 @@
package com.tangem.features.hotwallet.viewphrase.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.viewphrase.entity.ViewPhraseUM
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun ViewPhraseContent(state: ViewPhraseUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
AppBarWithBackButton(
text = stringResourceSafe(R.string.common_backup),
onBackClick = state.onBackClick,
)
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.imePadding()
.weight(1f),
) {
TitleBlock(
state = state,
modifier = Modifier.padding(top = 20.dp),
)
EnumeratedTwoColumnGrid(
items = state.words,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 32.dp),
)
}
}
}
@Composable
private fun TitleBlock(state: ViewPhraseUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size36)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResourceSafe(R.string.backup_seed_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResourceSafe(
R.string.backup_seed_caution,
state.words.size,
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
ViewPhraseContent(
state = ViewPhraseUM(
onBackClick = {},
words = List(12) {
EnumeratedTwoColumnGridItem(
index = it + 1,
mnemonic = "word${it + 1}",
)
}.toImmutableList(),
),
)
}
}

View file

@ -1,38 +1,33 @@
package com.tangem.features.hotwallet.walletbackup.model
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.common.routing.AppRoute
import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class WalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
getWalletUseCase: GetUserWalletUseCase,
private val router: Router,
private val getWalletUseCase: GetUserWalletUseCase,
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
override val dispatchers: CoroutineDispatcherProvider,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val router: Router,
) : Model() {
private val params: WalletBackupComponent.Params = paramsContainer.require()
@ -57,34 +52,16 @@ internal class WalletBackupModel @Inject constructor(
),
)
private val makeBackupAtFirstAlertBS
get() = bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_passcode_lock_32) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_backup_need_title)
body = resourceReference(R.string.hw_backup_need_description)
}
secondaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
closeBs()
}
}
}
init {
getWalletUseCase.invokeFlow(params.userWalletId)
.map { it.getOrNull() }
.distinctUntilChanged()
.filterNotNull()
.onEach {
updateBackupStatuses(it)
}
.launchIn(modelScope)
getWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = {
updateBackupStatuses(it)
},
)
}
private fun updateBackupStatuses(userWallet: UserWallet) {
@ -118,9 +95,37 @@ internal class WalletBackupModel @Inject constructor(
private fun onRecoveryPhraseClick() {
if (uiState.value.backedUp) {
// TODO [REDACTED_TASK_KEY]
getWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = { userWallet ->
when (userWallet) {
is UserWallet.Cold -> {
val userWalletId = userWallet.walletId
Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId")
}
is UserWallet.Hot -> showSeedPhrase(userWallet)
}
},
)
} else {
uiMessageSender.send(makeBackupAtFirstAlertBS)
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
}
}
private fun showSeedPhrase(hotWallet: UserWallet.Hot) {
modelScope.launch {
unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId)
.fold(
ifLeft = {
Timber.e("Error while export seed phrase: $it")
},
ifRight = { seedPhrasePrivateInfo ->
router.push(AppRoute.ViewPhrase(params.userWalletId))
},
)
}
}

View file

@ -11,7 +11,7 @@ tangemCardSdk = "develop-561"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem12"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
tangemHotSdk = "develop-525"
tangemHotSdk = "develop-526"
#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^