Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-13 13:39:55 +03:00
parent f6ac775120
commit fc1591532c
24 changed files with 219 additions and 34 deletions

View file

@ -598,6 +598,16 @@
"networkId": "aptos/test"
}
]
},
{
"id": "hedera-hashgraph",
"name": "Hedera",
"symbol": "HBAR",
"networks": [
{
"networkId": "hedera-hashgraph/test"
}
]
}
]
}

View file

@ -10,6 +10,7 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
@ -151,6 +152,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var blockchainDataStorage: BlockchainDataStorage
@Inject
lateinit var accountCreator: AccountCreator
// endregion Injected
override fun onCreate() {
@ -227,6 +231,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
accountCreator = accountCreator,
),
),
)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
@ -27,6 +28,7 @@ internal object WalletManagersFacadeModule {
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
accountCreator: AccountCreator,
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
@ -39,6 +41,7 @@ internal object WalletManagersFacadeModule {
assetReader = assetReader,
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
accountCreator = accountCreator,
)
}
}

View file

@ -44,6 +44,7 @@ class TapWalletManager(
val walletManagerFactory: WalletManagerFactory by lazy {
WalletManagerFactory(
config = blockchainSdkConfig,
accountCreator = store.state.daggerGraphState.get(DaggerGraphState::accountCreator),
blockchainDataStorage = store.state.daggerGraphState.get(DaggerGraphState::blockchainDataStorage),
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.proxy.redux
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
@ -53,6 +54,7 @@ data class DaggerGraphState(
val qrScanningRouter: QrScanningRouter? = null,
val currenciesRepository: CurrenciesRepository? = null,
val blockchainDataStorage: BlockchainDataStorage? = null,
val accountCreator: AccountCreator? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -75,4 +75,11 @@ interface TangemTechApi {
@GET("promotion")
suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse<PromotionInfoResponse>
@POST("user-network-account")
suspend fun createUserNetworkAccount(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CreateUserNetworkAccountBody(
@Json(name = "networkId") val networkId: String,
@Json(name = "publicWalletKey") val publicWalletKey: String,
)

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CreateUserNetworkAccountResponse(
@Json(name = "status") val status: String,
@Json(name = "data") val data: AccountCreated,
) {
@JsonClass(generateAdapter = true)
data class AccountCreated(
@Json(name = "accountId") val accountId: String,
@Json(name = "publicWalletKey") val publicWalletKey: String,
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.AccountCreator
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.blockchain.DefaultAccountCreator
import com.tangem.lib.auth.AuthProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AccountCreatorModule {
@Provides
@Singleton
fun provideAccountCreator(authProvider: AuthProvider, @DevTangemApi tangemTechApi: TangemTechApi): AccountCreator {
return DefaultAccountCreator(authProvider, tangemTechApi)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.local.blockchain
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
import com.tangem.lib.auth.AuthProvider
internal class DefaultAccountCreator(
private val authProvider: AuthProvider,
private val tangemTechApi: TangemTechApi,
) : AccountCreator {
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString())
return try {
val response = tangemTechApi.createUserNetworkAccount(
cardPublicKey = authProvider.getCardPublicKey(),
cardId = authProvider.getCardId(),
body = request,
).getOrThrow()
Result.Success(response.data.accountId)
} catch (e: Exception) {
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
}
}
}

View file

@ -51,6 +51,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"aptos", "aptos/test" -> R.drawable.img_aptos_22
"shibarium", "shibarium/test" -> R.drawable.img_shibarium_22
"algorand", "algorand/test" -> R.drawable.img_algorand_22
"hedera", "hedera/test" -> R.drawable.img_hedera_22
else -> R.drawable.ic_alert_24
}
}
@ -103,6 +104,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"aptos", "aptos/test" -> R.drawable.img_aptos_22
"shibarium", "shibarium/test" -> R.drawable.img_shibarium_22
"algorand", "algorand/test" -> R.drawable.img_algorand_22
"hedera", "hedera/test" -> R.drawable.img_hedera_22
else -> R.drawable.ic_alert_24
}
}
@ -152,6 +154,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"aptos" -> R.drawable.img_aptos_22
"shibarium" -> R.drawable.img_shibarium_22
"algorand" -> R.drawable.img_algorand_22
"hedera" -> R.drawable.img_hedera_22
else -> R.drawable.ic_alert_24
}
}
@ -204,6 +207,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
"shibarium", "shibarium/test" -> R.drawable.ic_shibarium_22
"algorand", "algorand/test" -> R.drawable.ic_algorand_22
"hedera", "hedera/test" -> R.drawable.ic_hedera_22
else -> R.drawable.ic_alert_24
}
}
@ -256,6 +260,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"aptos", "aptos/test" -> R.drawable.ic_aptos_22
"shibarium", "shibarium/test" -> R.drawable.ic_shibarium_22
"algorand", "algorand/test" -> R.drawable.ic_algorand_22
"hedera", "hedera/test" -> R.drawable.ic_hedera_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M7.876,6H6.5V16H7.876V6Z"
android:fillColor="#000000"/>
<path
android:pathData="M15.5,6H14.123V9.118H7.877V10.194H14.123V11.806H7.877V12.882H14.123V16H15.5V6Z"
android:fillColor="#000000"/>
</group>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#000000"/>
<path
android:pathData="M7.876,6H6.5V16H7.876V6Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M15.5,6H14.123V9.118H7.877V10.194H14.123V11.806H7.877V12.882H14.123V16H15.5V6Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -85,6 +85,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"shibarium/test" -> Blockchain.ShibariumTestnet
"algorand" -> Blockchain.Algorand
"algorand/test" -> Blockchain.AlgorandTestnet
"hedera-hashgraph" -> Blockchain.Hedera
"hedera-hashgraph/test" -> Blockchain.HederaTestnet
else -> null
}
}
@ -171,6 +173,8 @@ fun Blockchain.toNetworkId(): String {
Blockchain.ShibariumTestnet -> "shibarium/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Hedera -> "hedera-hashgraph"
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
}
}
@ -228,6 +232,8 @@ fun Blockchain.toCoinId(): String {
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Unknown -> "unknown"
Blockchain.Hedera -> "hedera-hashgraph"
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
}
}

View file

@ -52,11 +52,18 @@ class DefaultWalletManagersFacade(
moshi: Moshi,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
accountCreator: AccountCreator,
) : WalletManagersFacade {
private val demoConfig by lazy { DemoConfig() }
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
private val walletManagerFactory by lazy { WalletManagerFactory(configManager, blockchainDataStorage) }
private val walletManagerFactory by lazy {
WalletManagerFactory(
configManager,
accountCreator,
blockchainDataStorage,
)
}
private val sdkTokenConverter by lazy { SdkTokenConverter() }
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
@ -13,10 +14,14 @@ import com.tangem.domain.models.scan.ScanResponse
import timber.log.Timber
import com.tangem.blockchain.common.WalletManagerFactory as BlockchainWalletManagerFactory
internal class WalletManagerFactory(configManager: ConfigManager, blockchainDataStorage: BlockchainDataStorage) {
internal class WalletManagerFactory(
configManager: ConfigManager,
accountCreator: AccountCreator,
blockchainDataStorage: BlockchainDataStorage,
) {
private val sdkWalletManagerFactory by lazy {
BlockchainWalletManagerFactory(configManager.config.blockchainSdkConfig, blockchainDataStorage)
BlockchainWalletManagerFactory(configManager.config.blockchainSdkConfig, accountCreator, blockchainDataStorage)
}
fun createWalletManager(

View file

@ -16,4 +16,5 @@ dependencies {
exclude(module = "joda-time")
}
implementation(deps.jodatime)
implementation(deps.timber)
}

View file

@ -1,5 +1,7 @@
package com.tangem.domain.tokens.model
import timber.log.Timber
/**
* Represents a network address configuration.
*/
@ -47,7 +49,9 @@ sealed class NetworkAddress {
}
init {
require(value.isNotBlank()) { "Address value must not be blank" }
if (value.isEmpty()) {
Timber.w("Address value is blank")
}
}
}
}

View file

@ -2,10 +2,7 @@ package com.tangem.domain.tokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
@ -94,17 +91,21 @@ class GetCryptoCurrencyActionsUseCase(
return listOf(TokenActionsState.ActionState.HideToken(true))
}
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) {
return getActionsForUnreachableCurrency(cryptoCurrency)
return getActionsForUnreachableCurrency(cryptoCurrencyStatus)
}
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
// copy address
activeList.add(TokenActionsState.ActionState.CopyAddress(true))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(true))
}
// receive
activeList.add(TokenActionsState.ActionState.Receive(true))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.Receive(true))
}
// send
if (
@ -148,12 +149,16 @@ class GetCryptoCurrencyActionsUseCase(
return activeList + disabledList
}
private fun getActionsForUnreachableCurrency(cryptoCurrency: CryptoCurrency): List<TokenActionsState.ActionState> {
private fun getActionsForUnreachableCurrency(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): List<TokenActionsState.ActionState> {
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
activeList.add(TokenActionsState.ActionState.CopyAddress(true))
if (rampManager.availableForBuy(cryptoCurrency)) {
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(true))
}
if (rampManager.availableForBuy(cryptoCurrencyStatus.currency)) {
activeList.add(TokenActionsState.ActionState.Buy(true))
} else {
disabledList.add(TokenActionsState.ActionState.Buy(false))
@ -161,7 +166,10 @@ class GetCryptoCurrencyActionsUseCase(
disabledList.add(TokenActionsState.ActionState.Send(false))
disabledList.add(TokenActionsState.ActionState.Swap(false))
disabledList.add(TokenActionsState.ActionState.Sell(false))
activeList.add(TokenActionsState.ActionState.Receive(true))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.Receive(true))
}
activeList.add(TokenActionsState.ActionState.HideToken(true))
return activeList + disabledList
}
@ -200,6 +208,10 @@ class GetCryptoCurrencyActionsUseCase(
}
}
private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean {
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
private fun BigDecimal?.isZero(): Boolean {
return this?.signum() == 0
}

View file

@ -19,7 +19,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.onboarding.navigation.OnboardingRouter
import com.tangem.feature.wallet.presentation.WalletFragment
@ -136,16 +136,19 @@ internal class DefaultWalletRouter(
reduxNavController.navigate(action = NavigationAction.OpenUrl(url))
}
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.WalletDetails,
bundle = bundleOf(
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency,
override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
val networkAddress = currencyStatus.value.networkAddress
if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.WalletDetails,
bundle = bundleOf(
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currencyStatus.currency,
),
),
),
)
)
}
}
override fun openStoriesScreen() {

View file

@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -44,7 +44,7 @@ internal interface InnerWalletRouter : WalletRouter {
fun openUrl(url: String)
/** Open token details screen */
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
/** Open stories screen */
fun openStoriesScreen()

View file

@ -54,7 +54,7 @@ internal class TokenItemStateConverter(
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()),
cryptoPriceState = getCryptoPriceState(),
onItemClick = { clickIntents.onTokenItemClick(currency) },
onItemClick = { clickIntents.onTokenItemClick(this) },
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
)
}
@ -76,7 +76,7 @@ internal class TokenItemStateConverter(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = TokenItemState.TitleState.Content(text = currency.name),
onItemClick = { clickIntents.onTokenItemClick(currency) },
onItemClick = { clickIntents.onTokenItemClick(this) },
onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) },
)

View file

@ -5,7 +5,6 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
@ -33,7 +32,7 @@ internal interface WalletContentClickIntents {
fun onOrganizeTokensClick()
fun onTokenItemClick(currency: CryptoCurrency)
fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus)
fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
@ -69,9 +68,9 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onTokenItemClick(currency: CryptoCurrency) {
override fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(PortfolioEvent.TokenTapped)
router.openTokenDetails(stateHolder.getSelectedWalletId(), currency)
router.openTokenDetails(stateHolder.getSelectedWalletId(), currencyStatus)
}
override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {

View file

@ -85,7 +85,7 @@ web3j = "4.10.1"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-488"
tangemBlockchainSdk = "develop-489"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-324"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^