Updated on 2026-08-14

This commit is contained in:
Tangem 2022-02-10 07:39:58 +00:00
commit c98e6195ec
21 changed files with 127 additions and 101 deletions

View file

@ -78,8 +78,8 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-61'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-129'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-129'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-131'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-131'
// WebView
implementation "androidx.browser:browser:1.3.0"

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
@ -37,7 +38,7 @@ sealed class GlobalAction : Action {
}
data class ScanCard(
val shouldDeriveWC: Boolean,
val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
val onSuccess: ((ScanResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,

View file

@ -90,6 +90,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
val result = tangemSdkManager.scanProduct(
store.state.globalState.analyticsHandlers,
currenciesRepository,
action.additionalBlockchainsToDerive,
action.messageResId
)
withMainContext {

View file

@ -5,6 +5,7 @@ import CreateProductWalletTaskResponse
import android.content.Context
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
@ -25,7 +26,6 @@ import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
@ -33,6 +33,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -42,13 +43,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler?,
currenciesRepository: CurrenciesRepository,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler?.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(ScanProductTask(null, currenciesRepository), null, message)
.also { sendScanResultsToAnalytics(analyticsHandler, it) }
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(null, currenciesRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message
).also { sendScanResultsToAnalytics(analyticsHandler, it) }
}
suspend fun createProductWallet(
@ -135,7 +139,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
if (continuation.context.isActive) continuation.resume(result)
}
}
}

View file

@ -14,10 +14,11 @@ fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
return when (currency) {
is Currency.Blockchain -> {
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
false
} else {
availableToBuy.contains(currency.currencySymbol)
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
blockchain == Blockchain.Unknown || blockchain == Blockchain.BSC -> false
else -> availableToBuy.contains(currency.currencySymbol)
}
}
is Currency.Token -> false

View file

@ -80,7 +80,8 @@ typealias KeyWalletPublicKey = ByteArrayKey
class ScanProductTask(
val card: Card? = null,
private val currenciesRepository: CurrenciesRepository?
private val currenciesRepository: CurrenciesRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
) : CardSessionRunnable<ScanResponse> {
override fun run(
@ -101,7 +102,7 @@ class ScanProductTask(
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
else -> ScanWalletProcessor(currenciesRepository)
else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive)
}
commandProcessor.proceed(card, session) { processorResult ->
when (processorResult) {
@ -153,7 +154,8 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
}
private class ScanWalletProcessor(
private val currenciesRepository: CurrenciesRepository?
private val currenciesRepository: CurrenciesRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null
@ -291,6 +293,9 @@ private class ScanWalletProcessor(
)
)
}
if (additionalBlockchainsToDerive != null) {
blockchainsToDerive.addAll(additionalBlockchainsToDerive)
}
return blockchainsToDerive.distinct()
}

View file

@ -4,6 +4,7 @@ import com.github.salomonbrys.kotson.registerTypeAdapter
import com.google.gson.GsonBuilder
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.calculateSha256
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
import com.tangem.tap.features.details.redux.walletconnect.TradeData
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
@ -24,8 +25,8 @@ class BnbHelper {
.mapNotNull { if (it.denom == currency) it.amount else null }
.sum()
.toBigDecimal()
.movePointRight(Blockchain.Binance.decimals())
.toString()
.movePointLeft(Blockchain.Binance.decimals())
.stripZeroPlainString()
val gson = GsonBuilder()
.registerTypeAdapter(tradeOrderSerializer)

View file

@ -42,7 +42,7 @@ class WalletConnectManager {
private var sessions: MutableMap<WCSession, WalletConnectActiveData> = mutableMapOf()
fun connect(wcUri: String, wallet: WalletForSession) {
fun connect(wcUri: String) {
val session = WCSession.from(wcUri) ?: return
if (sessions[session] != null) {
store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession)
@ -57,7 +57,7 @@ class WalletConnectManager {
remotePeerId = null,
session = session,
client = client,
wallet = wallet
wallet = WalletForSession(cardId = "")
)
setupConnectionTimeoutCheck(session)
}
@ -154,7 +154,12 @@ class WalletConnectManager {
fun disconnect(session: WCSession) {
val activeData = sessions[session] ?: return
val disconnected = activeData.client.killSession()
val disconnected = if (activeData.client.isConnected) {
activeData.client.killSession()
} else {
true
}
if (disconnected) {
onSessionClosed(session)
}
@ -296,7 +301,7 @@ class WalletConnectManager {
val sessionData = data.toWalletConnectSession()
sessionData?.let {
store.dispatchOnMain(
WalletConnectAction.AcceptOpeningSession(
WalletConnectAction.ScanCard(
session = sessionData,
chainId = client.chainId?.toIntOrNull()
)

View file

@ -10,7 +10,6 @@ class WalletConnectNetworkUtils {
fun parseBlockchain(
chainId: Int?,
peer: WCPeerMeta,
isTestNet: Boolean? = null
): Blockchain? {
return when {
chainId != null -> {
@ -30,7 +29,7 @@ class WalletConnectNetworkUtils {
Blockchain.BSC
}
else -> {
if (isTestNet == true) Blockchain.EthereumTestnet else Blockchain.Ethereum
Blockchain.Ethereum
}
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.blockchain.extensions.*
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
@ -187,8 +188,8 @@ class WalletConnectSdkHelper {
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message())
return when (result) {
is CompletionResult.Success -> {
val key = session.wallet.derivedPublicKey
?: session.wallet.walletPublicKey
val key = session.wallet.derivedPublicKey?.toDecompressedPublicKey()
?: session.wallet.walletPublicKey.toDecompressedPublicKey()
getBnbResultString(
key.toHexString(),
result.data.signature.toHexString()

View file

@ -23,21 +23,21 @@ sealed class WalletConnectAction : Action {
data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction()
data class ScanCard(val wcUri: String) : WalletConnectAction()
object UnsupportedCard : WalletConnectAction()
data class OpenSession(
val wcUri: String,
val wallet: WalletForSession,
val scanResponse: ScanResponse
) : WalletConnectAction()
data class AddScanResponse(
val scanResponse: ScanResponse
): WalletConnectAction()
object RefuseOpeningSession : WalletConnectAction()
data class OpeningSessionTimeout(val session: WCSession) : WalletConnectAction()
data class AcceptOpeningSession(
data class ScanCard(
val session: WalletConnectSession, val chainId: Int?,
) : WalletConnectAction()

View file

@ -1,13 +1,15 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.*
import com.tangem.tap.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
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.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.isMultiwalletAllowed
@ -16,10 +18,9 @@ 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.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.trustwallet.walletconnect.models.WCPeerMeta
import org.rekotlin.Middleware
import timber.log.Timber
class WalletConnectMiddleware {
private val walletConnectManager = WalletConnectManager()
@ -36,7 +37,7 @@ class WalletConnectMiddleware {
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
if (WalletConnectManager.isCorrectWcUri(action.wcUri)) {
store.dispatchOnMain(WalletConnectAction.ScanCard(action.wcUri))
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
}
@ -60,10 +61,6 @@ class WalletConnectMiddleware {
)
}
is WalletConnectAction.ScanCard -> {
handleScanCard(action.wcUri)
}
is WalletConnectAction.OpeningSessionTimeout -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
}
@ -81,7 +78,6 @@ class WalletConnectMiddleware {
is WalletConnectAction.OpenSession -> {
walletConnectManager.connect(
wcUri = action.wcUri,
wallet = action.wallet
)
}
@ -93,16 +89,8 @@ class WalletConnectMiddleware {
)
}
is WalletConnectAction.AcceptOpeningSession -> {
generateWalletKeys(action.session, action.chainId)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(
action.session
)
)
)
is WalletConnectAction.ScanCard -> {
scanCard(action.session, action.chainId)
}
is WalletConnectAction.ApproveSession -> {
@ -176,35 +164,35 @@ class WalletConnectMiddleware {
}
}
private fun handleScanCard(wcUri: String) {
store.dispatch(GlobalAction.ScanCard(true, { scanResponse ->
val card = scanResponse.card
if (!card.isMultiwalletAllowed) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@ScanCard
}
private fun scanCard(session: WalletConnectSession, chainId: Int?) {
val blockchain = WalletConnectNetworkUtils.parseBlockchain(
chainId = chainId, peer = session.peerMeta
) ?: Blockchain.Ethereum
store.dispatchOnMain(
WalletConnectAction.OpenSession(
wcUri = wcUri,
wallet = WalletForSession(
scanResponse.card.cardId,
null,
null,
null,
scanResponse.card.isTestCard
),
scanResponse = scanResponse
)
)
}, {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
}, R.string.wallet_connect_scan_card_message))
store.dispatch(GlobalAction.ScanCard(additionalBlockchainsToDerive = listOf(blockchain),
onSuccess = { scanResponse ->
handleScanResponse(scanResponse, session, blockchain)
},
onFailure = {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
}, R.string.wallet_connect_scan_card_message
)
)
}
private fun generateWalletKeys(session: WalletConnectSession, chainId: Int?) {
val scanResponse = store.state.walletConnectState.scanResponse ?: return
val walletManager = getWalletManager(scanResponse, session.peerMeta, chainId) ?: return
private fun handleScanResponse(
scanResponse: ScanResponse, session: WalletConnectSession, blockchain: Blockchain
) {
val card = scanResponse.card
if (!card.isMultiwalletAllowed) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return
}
val walletManager = getWalletManager(scanResponse, blockchain).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
return
}
val wallet = walletManager.wallet
val derivedKey =
@ -213,46 +201,55 @@ class WalletConnectMiddleware {
} else {
walletManager.wallet.publicKey.blockchainKey
}
val updatedWallet = session.wallet.copy(
val walletForSession = WalletForSession(
cardId = scanResponse.card.cardId,
walletPublicKey = wallet.publicKey.seedKey,
derivedPublicKey = derivedKey,
derivationPath = wallet.publicKey.derivationPath,
blockchain = wallet.blockchain
)
val updatedSession = session.copy(wallet = updatedWallet)
val updatedSession = session.copy(wallet = walletForSession)
walletConnectManager.updateSession(updatedSession)
store.dispatchOnMain(WalletConnectAction.AddScanResponse(scanResponse))
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(
updatedSession
)
)
)
}
private fun getWalletManager(
scanResponse: ScanResponse, peer: WCPeerMeta, chainId: Int?
scanResponse: ScanResponse, blockchain: Blockchain
): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val blockchain = WalletConnectNetworkUtils.parseBlockchain(
chainId = chainId, peer = peer, isTestNet = scanResponse.card.isTestCard
) ?: if (scanResponse.card.isTestCard) Blockchain.EthereumTestnet else Blockchain.Ethereum
Timber.d(blockchain.fullName)
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(blockchain)
?: factory.makeWalletManagerForApp(scanResponse, blockchain)
store.state.walletState.getWalletManager(blockchainToMake)
?: factory.makeWalletManagerForApp(scanResponse, blockchainToMake)
?.also { walletManager ->
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
}
} else {
val walletManager = factory.makeWalletManagerForApp(scanResponse, blockchain)
val walletManager = factory.makeWalletManagerForApp(scanResponse, blockchainToMake)
if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains(
blockchain
blockchainToMake
) != true
) {
walletManager?.let {
currenciesRepository.saveAddedBlockchain(
card.cardId,
blockchain
blockchainToMake
)
}
}

View file

@ -17,6 +17,9 @@ class WalletConnectReducer {
)
}
is WalletConnectAction.OpenSession -> {
state.copy(loading = true)
}
is WalletConnectAction.AddScanResponse -> {
state.copy(scanResponse = action.scanResponse)
}
is WalletConnectAction.SetSessionsRestored ->
@ -27,8 +30,6 @@ class WalletConnectReducer {
state.sessions.filterNot { it.session.toUri() == action.session.toUri() }
state.copy(sessions = sessions)
}
is WalletConnectAction.ScanCard -> state.copy(loading = true)
is WalletConnectAction.UnsupportedCard -> state.copy(loading = false)
is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false)
is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false)

View file

@ -34,9 +34,9 @@ data class WalletConnectSession(
@JsonClass(generateAdapter = true)
data class WalletForSession(
val cardId: String,
val walletPublicKey: ByteArray?,
val derivedPublicKey: ByteArray?,
val derivationPath: DerivationPath?,
val walletPublicKey: ByteArray? = null,
val derivedPublicKey: ByteArray? = null,
val derivationPath: DerivationPath? = null,
val isTestNet: Boolean = false,
val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
) {

View file

@ -56,7 +56,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
store.dispatch(NavigationAction.PopBackTo())
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.ScanCard(result.text))
store.dispatch(WalletConnectAction.OpenSession(result.text))
}
}

View file

@ -16,7 +16,7 @@ class ClipboardOrScanQrDialog {
setTitle(context.getString(R.string.wallet_connect))
setMessage(context.getText(R.string.wallet_connect_clipboard_alert))
setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ ->
store.dispatch(WalletConnectAction.ScanCard(wcUri))
store.dispatch(WalletConnectAction.OpenSession(wcUri))
}
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.QrScan))

View file

@ -67,7 +67,7 @@ private fun handleReadCard() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard(false, { scanResponse ->
store.dispatch(GlobalAction.ScanCard( onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
@ -88,7 +88,7 @@ private fun handleReadCard() {
}
}
}
}, {
}, onFailure = {
changeButtonState(ButtonState.ENABLED)
}))
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding
import android.content.Context
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
@ -23,6 +24,7 @@ class AddressInfoBottomSheetDialog(
init {
this.setContentView(R.layout.dialog_onboarding_address_info)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
setOnCancelListener { store.dispatchDialogHide() }
}

View file

@ -182,7 +182,10 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, token, token.blockchain)
view.tv_currency_name.text = token.name
view.tv_currency_symbol.text = token.symbol
view.tv_currency_symbol.text = view.context.getString(
R.string.token_symbol_address_format,
token.symbol, token.contractAddress
)
view.btn_add_token.setOnClickListener {
onItemAddListener.invoke(currency)
currency.isAdded = true

View file

@ -61,13 +61,16 @@
<TextView
android:id="@+id/tv_currency_symbol"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:singleLine="true"
android:textColor="@color/darkGray6"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_add_token"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline"

View file

@ -315,4 +315,6 @@ Amount to pay: %s</string>
<string name="solana_rent_warning" translatable="false">Solana network charges a rent of %s every 2 days. Accounts that cant afford the rent are purged from the network. Deposit your account with more than %s to use it for free.</string>
<string name="token_symbol_address_format" translatable="false">%1s (%2s)</string>
</resources>