Updated on 2026-08-14

This commit is contained in:
Tangem 2022-07-26 18:34:38 +03:00
commit e2d1147b5c
45 changed files with 600 additions and 232 deletions

View file

@ -5,6 +5,8 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
@ -27,11 +29,11 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
@ -83,8 +85,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
}
private fun systemActions() {
// makes the status bar text dark
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, binding.root)
.isAppearanceLightStatusBars = true
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}

View file

@ -105,6 +105,12 @@ class DialogManager : StoreSubscriber<GlobalState> {
dAppName = state.dialog.dAppName,
context = context
)
is WalletConnectDialog.UnsupportedNetwork ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
context = context
)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)

View file

@ -61,7 +61,11 @@ fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
}
fun FragmentActivity.getPreviousScreen(): AppScreen? {
val indexOfLastFragment = this.supportFragmentManager.backStackEntryCount - 1
val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) {
this.supportFragmentManager.backStackEntryCount - 1
} else {
0
}
val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount)
this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name
else null

View file

@ -8,7 +8,7 @@ import com.tangem.tap.common.TestActions
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
@ -21,7 +21,10 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
if (isDemoWallet() || TestActions.testAmountInjectionForWalletManagerEnabled) {
val scanResponse = store.state.globalState.scanResponse
?: error("Scan response must not be null")
if (scanResponse.isDemoCard() || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)

View file

@ -5,44 +5,25 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.tap.domain.tasks.SignHashTask
import com.tangem.common.card.Card
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
private val card: Card,
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val signerCallback: (TangemSignerResponse) -> Unit,
) : TransactionSigner {
override suspend fun sign(hash: ByteArray, cardId: String, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
override suspend fun sign(
hashes: List<ByteArray>,
publicKey: Wallet.PublicKey
): CompletionResult<List<ByteArray>> {
return suspendCoroutine { continuation ->
val command = SignHashTask(hash, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = command,
cardId = cardId,
initialMessage = initialMessage,
) { result ->
when (result) {
is CompletionResult.Success -> {
signerCallback(
TangemSignerResponse(
result.data.totalSignedHashes,
result.data.remainingSignatures
)
)
continuation.resume(CompletionResult.Success(result.data.signature))
}
is CompletionResult.Failure ->
continuation.resume(CompletionResult.Failure(result.error))
}
}
}
}
val cardId = if (card.backupStatus?.isActive == true) null else card.cardId
override suspend fun sign(hashes: List<ByteArray>, cardId: String, publicKey: Wallet.PublicKey): CompletionResult<List<ByteArray>> {
return suspendCoroutine { continuation ->
val task = SignHashesTask(hashes, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = task,
@ -65,6 +46,21 @@ class TangemSigner(
}
}
}
override suspend fun sign(
hash: ByteArray,
publicKey: Wallet.PublicKey
): CompletionResult<ByteArray> {
val result = sign(
hashes = listOf(hash),
publicKey = publicKey
)
return when (result) {
is CompletionResult.Success -> CompletionResult.Success(result.data.first())
is CompletionResult.Failure -> CompletionResult.Failure(result.error)
}
}
}
data class TangemSignerResponse(

View file

@ -32,9 +32,10 @@ fun WalletManagerFactory.makeWalletManagerForApp(
return when {
scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> {
makeTwinWalletManager(
card.cardId,
wallet.publicKey, scanResponse.secondTwinPublicKey!!.hexToBytes(),
environmentBlockchain, wallet.curve
walletPublicKey = wallet.publicKey,
pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(),
blockchain = environmentBlockchain,
curve = wallet.curve
)
}
seedKey != null && derivationParams != null -> {
@ -47,7 +48,6 @@ fun WalletManagerFactory.makeWalletManagerForApp(
?: return null
makeWalletManager(
cardId = card.cardId,
blockchain = environmentBlockchain,
seedKey = wallet.publicKey,
derivedKey = derivedKey,
@ -56,7 +56,6 @@ fun WalletManagerFactory.makeWalletManagerForApp(
}
else -> {
makeWalletManager(
cardId = card.cardId,
blockchain = environmentBlockchain,
walletPublicKey = wallet.publicKey,
curve = wallet.curve

View file

@ -57,7 +57,6 @@ class WalletStateConverter : StringStateConverter<AppState> {
walletMap["publicKey"] = publicKeyMap
walletMap["amounts"] = amounts
walletMap["addresses"] = wallet.addresses.toString()
walletMap["cardId"] = wallet.cardId
return walletMap
}

View file

@ -4,16 +4,17 @@ import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
import com.tangem.common.card.Card
import java.util.*
import java.util.Locale
/**
[REDACTED_AUTHOR]
*/
class CardTou {
private val locale: Locale = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)
private val locale: Locale =
ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!!
fun getUrl(card: Card): Uri? {
val issuerName = card.issuer.name ?: return null
val issuerName = card.issuer.name
if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null
val baseUrl = "https://app.tangem.com/tou/"

View file

@ -1,11 +1,13 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.toMap
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import com.google.gson.annotations.SerializedName
import com.tangem.blockchain.common.Blockchain
import com.trustwallet.walletconnect.JSONRPC_VERSION
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
@ -18,9 +20,11 @@ class EthSignHelper {
.create()
}
fun tryToParseEthTypedMessage(data: String): WCEthereumSignMessage? {
val request =
gson.fromJson<CustomJsonRpcRequests>(data)
fun parseCustomRequest(data: String): CustomJsonRpcRequest {
return gson.fromJson<CustomJsonRpcRequest>(data)
}
fun tryToParseEthTypedMessage(request: CustomJsonRpcRequest): WCEthereumSignMessage? {
return if (request.method == WCMethodExtended.ETH_SIGN_TYPE_DATA_V4) {
WCEthereumSignMessage(
listOf(
@ -53,12 +57,26 @@ class EthSignHelper {
}
}
data class CustomJsonRpcRequests(
data class CustomJsonRpcRequest(
val id: Long,
val jsonrpc: String = JSONRPC_VERSION,
val method: WCMethodExtended?,
val params: JsonArray
)
) {
fun blockchainFromChainId(): Blockchain? {
return try {
val hex = params[0].asJsonObject.toMap()[CHAIN_ID_KEY]?.asString ?: ""
Blockchain.fromChainId(Integer.decode(hex))
} catch (exception: Exception) {
null
}
}
companion object {
const val CHAIN_ID_KEY = "chainId"
}
}
enum class WCMethodExtended {
@SerializedName("wc_sessionRequest")
@ -95,5 +113,10 @@ enum class WCMethodExtended {
GET_ACCOUNTS,
@SerializedName("trust_signTransaction")
SIGN_TRANSACTION;
SIGN_TRANSACTION,
@SerializedName("wallet_switchEthereumChain")
SWITCH_CHAIN,
;
}

View file

@ -11,14 +11,20 @@ import com.tangem.tap.store
import com.tangem.tap.walletConnectRepository
import com.trustwallet.walletconnect.WCClient
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.binance.*
import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCSession
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.*
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.util.*
@ -71,6 +77,22 @@ class WalletConnectManager {
}
}
fun updateBlockchain(session: WalletConnectSession) {
sessions[session.session]?.client?.updateSession(
accounts = listOfNotNull(session.getAddress()),
chainId = session.wallet.blockchain?.getChainId(),
approved = true
)
val updatedSession = sessions[session.session]?.copy(
wallet = session.wallet
)
if (updatedSession != null) {
sessions[session.session] = updatedSession
}
}
private fun setupConnectionTimeoutCheck(session: WCSession) {
scope.launch {
delay(20_000)
@ -409,23 +431,43 @@ class WalletConnectManager {
Timber.d("Custom Request")
Timber.d(data)
val message = EthSignHelper.tryToParseEthTypedMessage(data)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandlers?.logWcEvent(
Analytics.WcAnalyticsEvent.Action(
Analytics.WcAction.PersonalSign
val request = EthSignHelper.parseCustomRequest(data)
when (request.method) {
WCMethodExtended.ETH_SIGN_TYPE_DATA_V4 -> handleTypedDataV4(request, client, id)
WCMethodExtended.SWITCH_CHAIN -> {
val blockchain = request.blockchainFromChainId()
Timber.d("WC switch chainID\nNew Blockchain: $blockchain")
val session = sessions[client.session]?.toWalletConnectSession()
if (session != null) {
store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session))
}
}
else -> {
Timber.d("WC: unrecognized custom request")
}
}
}
}
private fun handleTypedDataV4(request: CustomJsonRpcRequest, client: WCClient, id: Long) {
val message = EthSignHelper.tryToParseEthTypedMessage(request)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandlers?.logWcEvent(
Analytics.WcAnalyticsEvent.Action(
Analytics.WcAction.PersonalSign
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id
)
)
}
}
}
}

View file

@ -5,8 +5,17 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.CommonSigner
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.blockchain.extensions.isAscii
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
@ -17,7 +26,11 @@ import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.store
@ -25,8 +38,8 @@ import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import timber.log.Timber
import java.math.BigDecimal
import timber.log.Timber
class WalletConnectSdkHelper {
@ -108,9 +121,8 @@ class WalletConnectSdkHelper {
)
val blockchain = session.wallet.getBlockchainForSession()
return factory.makeWalletManager(
session.wallet.cardId,
blockchain,
publicKey
blockchain = blockchain,
publicKey = publicKey
)
}
@ -124,7 +136,7 @@ class WalletConnectSdkHelper {
private suspend fun sendTransaction(data: WcTransactionData): String? {
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
signer = Signer(tangemSdk)
signer = CommonSigner(tangemSdk)
)
return when (result) {
SimpleResult.Success -> {

View file

@ -0,0 +1,95 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.wallet.redux.WalletState
class WcWalletManagerFactory(
private val factory: WalletManagerFactory,
private val currenciesRepository: CurrenciesRepository,
) {
suspend fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = wallet.derivationPath?.rawPath,
tokens = emptyList()
)
return if (walletState.cardId == wallet.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
val blockchainNetworkWithTokens = currenciesRepository
.loadSavedCurrencies(
cardId = wallet.cardId,
isHdWalletSupported = wallet.derivationPath != null
).firstOrNull { it == blockchainNetwork }
if (blockchainNetworkWithTokens != null) {
factory.makeWalletManager(
cardId = wallet.cardId,
blockchain = blockchainToMake,
publicKey = Wallet.PublicKey(
wallet.walletPublicKey!!,
wallet.derivedPublicKey,
wallet.derivationPath
),
tokens = blockchainNetworkWithTokens.tokens,
curve = blockchainToMake.getPrimaryCurve() ?: EllipticCurve.Secp256k1
)
} else {
null
}
}
}
suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val card = scanResponse.card
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (walletState.cardId == card.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -109,7 +109,7 @@ class DemoConfig {
testDemoCardIds).distinct()
}
private val releaseDemoCardIds = mutableListOf<String>(
private val releaseDemoCardIds = mutableListOf(
// Tangem Wallet:
"AC01000000041100",
"AC01000000042462",
@ -284,7 +284,10 @@ class DemoTransactionSender(
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val dataToSign = randomString(32).toByteArray()
val signerResponse = signer.sign(dataToSign, walletManager.wallet.cardId, walletManager.wallet.publicKey)
val signerResponse = signer.sign(
hash = dataToSign,
publicKey = walletManager.wallet.publicKey
)
return when (signerResponse) {
is CompletionResult.Success -> SimpleResult.Failure(Exception(ID))
is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error)

View file

@ -1,10 +1,10 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
/**
[REDACTED_AUTHOR]
*/
fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId)
fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId)
fun Card.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.redux.walletconnect
import android.app.Activity
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.wallet.R
@ -47,6 +48,16 @@ sealed class WalletConnectAction : Action {
data class Success(val session: WalletConnectSession) : WalletConnectAction()
}
data class SwitchBlockchain(
val blockchain: Blockchain?,
val session: WalletConnectSession
) : WalletConnectAction()
data class UpdateBlockchain(
val updatedSession: WalletConnectSession
) : WalletConnectAction()
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :

View file

@ -1,10 +1,8 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
@ -14,11 +12,10 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.domain.walletconnect.WcWalletManagerFactory
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.scope
import com.tangem.tap.store
@ -159,6 +156,45 @@ class WalletConnectMiddleware {
action.id, action.data, action.sessionData
)
}
is WalletConnectAction.SwitchBlockchain -> {
val blockchain = action.blockchain.guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository
)
val walletState = store.state.walletState
scope.launch {
val walletManager = factory.getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = walletState
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchain.fullName)
)
)
return@launch
}
val updatedWallet = action.session.wallet.copy(
walletPublicKey = walletManager.wallet.publicKey.seedKey,
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
derivationPath = walletManager.wallet.publicKey.derivationPath,
blockchain = action.blockchain
)
val updatedSession = action.session.copy(wallet = updatedWallet)
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
}
}
is WalletConnectAction.UpdateBlockchain -> {
walletConnectManager.updateBlockchain(action.updatedSession)
}
}
}
@ -189,8 +225,13 @@ class WalletConnectMiddleware {
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository
)
val walletState = store.state.walletState
scope.launch {
val walletManager = getWalletManager(scanResponse, blockchain).guard {
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -232,37 +273,4 @@ class WalletConnectMiddleware {
}
}
private suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain
): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -34,6 +34,11 @@ class WalletConnectReducer {
is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false)
is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false)
is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false)
is WalletConnectAction.UpdateBlockchain -> state.copy(
sessions = state.sessions
.filterNot { it.peerId == action.updatedSession.peerId }
+ action.updatedSession
)
else -> state
}

View file

@ -82,6 +82,7 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
object UnsupportedCard : WalletConnectDialog()
object UnsupportedNetwork : WalletConnectDialog()
data class AddNetwork(val network: String) : WalletConnectDialog()
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()

View file

@ -1,10 +1,14 @@
package com.tangem.tap.features.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.AppScreen
@ -15,12 +19,12 @@ import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private var composeView: ComposeView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -29,6 +33,13 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val context = container?.context ?: return null
store.dispatch(BackupAction.CheckForUnfinishedBackup)
getView()?.findViewById<ComposeView>(R.id.cv_stories)?.setContent {
AppCompatTheme {
@ -46,16 +57,16 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
store.dispatch(TokensAction.LoadCurrencies())
}
)
composeView = ComposeView(context).apply {
setContent {
AppCompatTheme {
ScreenContent()
}
}
}
}
private fun getRegionProvider(): RegionProvider = RegionService(
listOf(
// TelephonyManagerRegionProvider(requireContext()),
LocaleRegionProvider()
)
)
return composeView
}
override fun onStart() {
super.onStart()
@ -71,6 +82,11 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
store.unsubscribe(this)
}
override fun onDestroyView() {
super.onDestroyView()
rollbackStatusBarIconsColor()
composeView = null
}
override fun newState(state: HomeState) {
if (activity == null || view == null) return
@ -78,4 +94,32 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
homeState.value = state
}
@Composable
private fun ScreenContent() {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = {
store.dispatch(
HomeAction.GoToShop(store.state.globalState.userCountryCode)
)
},
onSearchTokensClick = {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())
}
)
}
/*
* !!! Workaround !!!
* Used to roll back the color of icons in the status bar after the stories screen
* */
private fun rollbackStatusBarIconsColor() {
WindowInsetsControllerCompat(
activity?.window ?: return,
view ?: return,
).isAppearanceLightStatusBars = true
}
}

View file

@ -3,12 +3,25 @@ package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -25,8 +38,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.features.home.compose.content.*
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.tap.features.home.compose.content.FirstStoriesContent
import com.tangem.tap.features.home.compose.content.StoriesCurrencies
import com.tangem.tap.features.home.compose.content.StoriesRevolutionaryWallet
import com.tangem.tap.features.home.compose.content.StoriesUltraSecureBackup
import com.tangem.tap.features.home.compose.content.StoriesWalletForEveryone
import com.tangem.tap.features.home.compose.content.StoriesWeb3
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
@ -43,6 +61,7 @@ fun StoriesScreen(
) {
val steps = 6
val currentStep = remember { mutableStateOf(1) }
val systemUiController = rememberSystemUiController()
val isDarkBackground = currentStep.value !in 3..5
@ -58,13 +77,20 @@ fun StoriesScreen(
val hideContent = remember { mutableStateOf(true) }
SideEffect {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = false,
)
}
Box(
Modifier
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF090E13))
) {
Row(
Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize()
) {
Box(
Modifier
@ -105,19 +131,23 @@ fun StoriesScreen(
}
if (!isDarkBackground) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.ic_overlay),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
val insets = WindowInsets.systemBars
.union(WindowInsets(top = 32.dp))
Column(
modifier = Modifier
.windowInsetsPadding(insets)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
SpacerS24()
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,

View file

@ -7,7 +7,7 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.ReceiptAction
@ -28,6 +28,7 @@ class RequestFeeMiddleware {
fun handle(appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val scanResponse = appState.globalState.scanResponse ?: return
if (!SendState.isReadyToRequestFee()) {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
@ -40,7 +41,7 @@ class RequestFeeMiddleware {
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = if (walletManager.isDemoWallet()) {
val txSender = if (scanResponse.isDemoCard()) {
DemoTransactionSender(walletManager)
} else {
walletManager as TransactionSender

View file

@ -4,7 +4,11 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionError
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -15,7 +19,11 @@ import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -26,9 +34,15 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.MainCurrencyType
@ -38,6 +52,7 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.wallet.R
import java.util.EnumSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -45,7 +60,6 @@ import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.*
/**
[REDACTED_AUTHOR]
@ -161,7 +175,11 @@ private fun sendTransaction(
tangemSdk.config.linkedTerminal = false
}
val signer = TangemSigner(tangemSdk, action.messageForSigner) { signResponse ->
val signer = TangemSigner(
card = card,
tangemSdk = tangemSdk,
initialMessage = action.messageForSigner
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
@ -171,7 +189,7 @@ private fun sendTransaction(
)
}
val sendResult = try {
if (walletManager.isDemoWallet()) {
if (card.isDemoCard()) {
DemoTransactionSender(walletManager).send(txData, signer)
} else {
(walletManager as TransactionSender).send(txData, signer)
@ -340,5 +358,4 @@ private fun updateWarnings(dispatch: (Action) -> Unit) {
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
dispatch(SendAction.Warnings.Set(warnings))
}
}

View file

@ -73,6 +73,9 @@ data class WalletState(
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
val cardId: String?
get() = wallets.firstOrNull()?.walletManager?.wallet?.cardId
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager

View file

@ -45,6 +45,7 @@ class TradeCryptoMiddleware {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
val card = store.state.globalState.scanResponse?.card ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
@ -59,7 +60,13 @@ class TradeCryptoMiddleware {
return
}
scope.launch { exchangeManager.buyErc20TestnetTokens(walletManager, currency.token) }
scope.launch {
exchangeManager.buyErc20TestnetTokens(
card = card,
walletManager = walletManager,
token = currency.token
)
}
return
}

View file

@ -1,8 +1,5 @@
package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
@ -15,12 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -233,9 +225,7 @@ class WalletMiddleware {
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(action.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount, store.state.walletState)

View file

@ -5,7 +5,6 @@ import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import coil.imageLoader
import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
@ -21,13 +20,13 @@ private const val QCX = "QCX"
private const val VOYR = "VOYRME"
fun loadCurrencyIcon(
currencyImageView: ImageFilterView,
currencyImageView: CurrencyIconView,
currencyTextView: TextView,
token: Token?,
blockchain: Blockchain,
) {
CurrencyIconLoader(
currencyImageView = currencyImageView,
currencyImageView = currencyImageView.imageView,
currencyTextView = currencyTextView,
token = token,
blockchain = blockchain
@ -135,14 +134,6 @@ private inline fun ImageView.loadIcon(
.placeholder(placeholderRes)
.error(placeholderRes)
.fallback(placeholderRes)
.transformations(
RoundedCornersTransformation(
topLeft = 32f,
topRight = 32f,
bottomLeft = 32f,
bottomRight = 32f
)
)
.listener(
onStart = { onStart() },
onSuccess = { _, _ -> onSuccess() },

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.wallet.ui.images
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.utils.widget.ImageFilterView
import com.google.android.material.card.MaterialCardView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.wallet.databinding.ViewCurrencyIconBinding
class CurrencyIconView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : MaterialCardView(context, attrs, defStyleAttr) {
private val binding = ViewCurrencyIconBinding.inflate(
LayoutInflater.from(context),
this
)
val imageView: ImageFilterView
get() = binding.iv
init {
elevation = 0f
cardElevation = 0f
radius = dpToPx(8f)
background = null
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
@ -68,7 +69,11 @@ class CurrencyExchangeManager(
enum class Action { Buy, Sell }
}
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: EthereumWalletManager, token: Token) {
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(
card: Card,
walletManager: EthereumWalletManager,
token: Token,
) {
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)
@ -85,7 +90,9 @@ suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: Ethereu
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
tangemSdk = tangemSdk, Message()
card = card,
tangemSdk = tangemSdk,
initialMessage = Message(),
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(