Updated on 2026-08-14
This commit is contained in:
commit
39114200c5
14 changed files with 623 additions and 120 deletions
|
|
@ -77,7 +77,7 @@ dependencies {
|
|||
implementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'com.tangem:blockchain:develop-52'
|
||||
implementation 'com.tangem:blockchain:develop-53'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-115'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-115'
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
TransactionDialog.create(state.dialog.dialogData, context)
|
||||
is WalletConnectDialog.PersonalSign ->
|
||||
PersonalSignDialog.create(state.dialog.data, context)
|
||||
is WalletConnectDialog.BnbTransactionDialog ->
|
||||
BnbTransactionDialog.create(
|
||||
data = state.dialog.data,
|
||||
session = state.dialog.session,
|
||||
sessionId = state.dialog.sessionId,
|
||||
cardId = state.dialog.cardId,
|
||||
dAppName = state.dialog.dAppName,
|
||||
context = context
|
||||
)
|
||||
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
|
||||
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
|
||||
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.tap.domain.walletconnect
|
||||
|
||||
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.features.details.redux.walletconnect.BinanceMessageData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.TradeData
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
|
||||
import com.trustwallet.walletconnect.models.binance.tradeOrderSerializer
|
||||
import timber.log.Timber
|
||||
|
||||
class BnbHelper {
|
||||
companion object {
|
||||
|
||||
fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer {
|
||||
val input = order.msgs.first().inputs.first()
|
||||
val output = order.msgs.first().inputs.first()
|
||||
|
||||
val currency =
|
||||
input.coins.firstOrNull()?.denom ?: Blockchain.Binance
|
||||
val amount = input.coins
|
||||
.mapNotNull { if (it.denom == currency) it.amount else null }
|
||||
.sum()
|
||||
.toBigDecimal()
|
||||
.movePointRight(Blockchain.Binance.decimals())
|
||||
.toString()
|
||||
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(tradeOrderSerializer)
|
||||
.serializeNulls()
|
||||
.create()
|
||||
|
||||
return BinanceMessageData.Transfer(
|
||||
outputAddress = output.address,
|
||||
amount = amount,
|
||||
address = input.address,
|
||||
data = gson.toJson(order).toByteArray().calculateSha256()
|
||||
)
|
||||
}
|
||||
|
||||
fun createMessageData(order: WCBinanceTradeOrder): BinanceMessageData.Trade {
|
||||
val address = order.msgs.first().sender
|
||||
|
||||
val tradeData = order.msgs.map {
|
||||
val price = it.price.toBigDecimal()
|
||||
.movePointLeft(Blockchain.Binance.decimals())
|
||||
.stripTrailingZeros()
|
||||
val quantity = it.quantity.toBigDecimal()
|
||||
.movePointLeft(Blockchain.Binance.decimals())
|
||||
.stripTrailingZeros()
|
||||
|
||||
val amount = price * quantity
|
||||
val symbol = it.symbol.substringBefore("-")
|
||||
|
||||
TradeData(
|
||||
price = "$price ${Blockchain.Binance.currency}",
|
||||
quantity = "$quantity $symbol",
|
||||
amount = "$amount ${Blockchain.Binance.currency}",
|
||||
symbol = symbol
|
||||
)
|
||||
}
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(tradeOrderSerializer)
|
||||
.serializeNulls()
|
||||
.create()
|
||||
val serialized = gson.toJson(order)
|
||||
Timber.d(serialized)
|
||||
|
||||
return BinanceMessageData.Trade(
|
||||
tradeData = tradeData,
|
||||
address = address,
|
||||
data = serialized.toByteArray().calculateSha256()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ 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.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
|
||||
|
|
@ -60,6 +64,15 @@ class WalletConnectManager {
|
|||
setupConnectionTimeoutCheck(session)
|
||||
}
|
||||
|
||||
fun updateSession(session: WalletConnectSession) {
|
||||
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)
|
||||
|
|
@ -81,7 +94,7 @@ class WalletConnectManager {
|
|||
client = WCClient(httpClient = okHttpClient),
|
||||
session = session.session,
|
||||
peerMeta = session.peerMeta,
|
||||
wallet = session.wallet
|
||||
wallet = session.wallet,
|
||||
)
|
||||
.also {
|
||||
setListeners(it.client)
|
||||
|
|
@ -96,11 +109,12 @@ class WalletConnectManager {
|
|||
val activeData = sessions[session] ?: return
|
||||
removeSimilarSessions(activeData)
|
||||
|
||||
val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey
|
||||
val accounts = listOf(Blockchain.Ethereum.makeAddresses(key).first().value)
|
||||
val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey ?: return
|
||||
val blockchain = activeData.wallet.getBlockchainForSession()
|
||||
val accounts = listOf(blockchain.makeAddresses(key).first().value)
|
||||
val approved = activeData.client.approveSession(
|
||||
accounts = accounts,
|
||||
chainId = activeData.wallet.chainId
|
||||
chainId = blockchain.getChainId() ?: Blockchain.Ethereum.getChainId()!!
|
||||
)
|
||||
if (approved) {
|
||||
val walletConnectSession = WalletConnectSession(
|
||||
|
|
@ -111,15 +125,18 @@ class WalletConnectManager {
|
|||
peerMeta = activeData.peerMeta!!
|
||||
)
|
||||
walletConnectRepository.saveSession(walletConnectSession)
|
||||
store.dispatchOnMain(WalletConnectAction.ApproveSession.Success(
|
||||
walletConnectSession))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.ApproveSession.Success(
|
||||
walletConnectSession
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSimilarSessions(activeData: WalletConnectActiveData) {
|
||||
val sessionsToRemove = sessions.filter {
|
||||
it.value.wallet.walletPublicKey == activeData.wallet.walletPublicKey
|
||||
it.value.wallet.walletPublicKey?.equals(activeData.wallet.walletPublicKey) == true
|
||||
&& it.value.peerMeta?.url == activeData.peerMeta?.url
|
||||
&& it.value.session != activeData.session
|
||||
}
|
||||
|
|
@ -148,7 +165,8 @@ class WalletConnectManager {
|
|||
private fun onSessionClosed(session: WCSession) {
|
||||
FirebaseAnalyticsHandler.logWcEvent(
|
||||
FirebaseAnalyticsHandler.WcAnalyticsEvent.Session(
|
||||
FirebaseAnalyticsHandler.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url)
|
||||
FirebaseAnalyticsHandler.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url
|
||||
)
|
||||
)
|
||||
sessions.remove(session)
|
||||
walletConnectRepository.removeSession(session)
|
||||
|
|
@ -170,17 +188,23 @@ class WalletConnectManager {
|
|||
type = type
|
||||
).guard {
|
||||
sessions[session.session] = activeData.copy(transactionData = null)
|
||||
store.dispatchOnMain(WalletConnectAction.RejectRequest(
|
||||
session.session,
|
||||
id
|
||||
))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
session.session,
|
||||
id
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
sessions[session.session] = activeData.copy(transactionData = data)
|
||||
|
||||
store.dispatchOnMain(WalletConnectAction.SetDataToSend(data))
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.RequestTransaction(
|
||||
data.dialogData)))
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.RequestTransaction(
|
||||
data.dialogData
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,13 +214,34 @@ class WalletConnectManager {
|
|||
scope.launch {
|
||||
val hash = WalletConnectSdkHelper().completeTransaction(data).guard {
|
||||
sessions[data.session.session] = activeData.copy(transactionData = null)
|
||||
store.dispatchOnMain(WalletConnectAction.RejectRequest(data.session.session,
|
||||
data.id
|
||||
))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
data.session.session,
|
||||
data.id
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
acceptRequest(data.session.session, data.id, hash)
|
||||
sessions[data.session.session] = activeData.copy(transactionData = null)
|
||||
acceptRequest(session, data.id, hash)
|
||||
sessions[session] = activeData.copy(transactionData = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun signBnb(
|
||||
id: Long, data: ByteArray, sessionData: WCSession,
|
||||
) {
|
||||
val activeData = sessions[sessionData] ?: return
|
||||
scope.launch {
|
||||
val hash = WalletConnectSdkHelper().signBnbTransaction(data, activeData).guard {
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
sessionData,
|
||||
id
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
acceptRequest(sessionData, id, hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,9 +256,13 @@ class WalletConnectManager {
|
|||
message = message, session = session, id = id
|
||||
)
|
||||
sessions[session.session] = activeData.copy(personalSignData = data)
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.PersonalSign(
|
||||
data.dialogData
|
||||
)))
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.PersonalSign(
|
||||
data.dialogData
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,9 +273,12 @@ class WalletConnectManager {
|
|||
val hash = WalletConnectSdkHelper().signPersonalMessage(data.hash, activeData.wallet)
|
||||
.guard {
|
||||
sessions[data.session.session] = activeData.copy(transactionData = null)
|
||||
store.dispatchOnMain(WalletConnectAction.RejectRequest(data.session.session,
|
||||
data.id
|
||||
))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.RejectRequest(
|
||||
data.session.session,
|
||||
data.id
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
sessions[session] = activeData.copy(personalSignData = data)
|
||||
|
|
@ -245,12 +297,17 @@ class WalletConnectManager {
|
|||
sessions[session] = data
|
||||
val sessionData = data.toWalletConnectSession()
|
||||
sessionData?.let {
|
||||
store.dispatchOnMain(WalletConnectAction.AcceptOpeningSession(
|
||||
sessionData))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.AcceptOpeningSession(
|
||||
session = sessionData,
|
||||
chainId = client.chainId?.toIntOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
FirebaseAnalyticsHandler.logWcEvent(
|
||||
FirebaseAnalyticsHandler.WcAnalyticsEvent.Session(
|
||||
FirebaseAnalyticsHandler.WcSessionEvent.Connect, peer.url)
|
||||
FirebaseAnalyticsHandler.WcSessionEvent.Connect, peer.url
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -267,12 +324,14 @@ class WalletConnectManager {
|
|||
)
|
||||
)
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest(
|
||||
transaction = transaction,
|
||||
session = sessionData,
|
||||
id = id,
|
||||
type = WcTransactionType.EthSendTransaction
|
||||
))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.HandleTransactionRequest(
|
||||
transaction = transaction,
|
||||
session = sessionData,
|
||||
id = id,
|
||||
type = WcTransactionType.EthSendTransaction
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction ->
|
||||
|
|
@ -283,12 +342,14 @@ class WalletConnectManager {
|
|||
)
|
||||
)
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest(
|
||||
transaction = transaction,
|
||||
session = sessionData,
|
||||
id = id,
|
||||
type = WcTransactionType.EthSignTransaction
|
||||
))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.HandleTransactionRequest(
|
||||
transaction = transaction,
|
||||
session = sessionData,
|
||||
id = id,
|
||||
type = WcTransactionType.EthSignTransaction
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
client.onEthSign = { id: Long, message: WCEthereumSignMessage ->
|
||||
|
|
@ -299,12 +360,42 @@ class WalletConnectManager {
|
|||
)
|
||||
)
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(WalletConnectAction.HandlePersonalSignRequest(
|
||||
message,
|
||||
sessionData,
|
||||
id))
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.HandlePersonalSignRequest(
|
||||
message,
|
||||
sessionData,
|
||||
id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
client.onBnbCancel = { id: Long, order: WCBinanceCancelOrder ->
|
||||
|
||||
}
|
||||
client.onBnbTrade = { id: Long, order: WCBinanceTradeOrder ->
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.BinanceTransaction.Trade(
|
||||
id = id, order = order, sessionData = sessionData
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
client.onBnbTransfer = { id: Long, order: WCBinanceTransferOrder ->
|
||||
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
|
||||
store.dispatchOnMain(
|
||||
WalletConnectAction.BinanceTransaction.Transfer(
|
||||
id = id, order = order, sessionData = sessionData
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
client.onBnbTxConfirm = { id: Long, order: WCBinanceTxConfirmParam ->
|
||||
// send empty approve request if status is OK
|
||||
if (order.ok) client.approveRequest(id, "")
|
||||
}
|
||||
client.onDisconnect = { code: Int, reason: String ->
|
||||
val session = client.session
|
||||
if (session != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.domain.walletconnect
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.trustwallet.walletconnect.models.WCPeerMeta
|
||||
|
||||
class WalletConnectNetworkUtils {
|
||||
|
||||
companion object {
|
||||
|
||||
fun parseBlockchain(
|
||||
chainId: Int?,
|
||||
peer: WCPeerMeta,
|
||||
isTestNet: Boolean? = null
|
||||
): Blockchain? {
|
||||
return when {
|
||||
chainId != null -> {
|
||||
Blockchain.fromChainId(chainId)
|
||||
}
|
||||
peer.url.contains("matic.network") || peer.name == "Polygon" -> {
|
||||
Blockchain.Polygon
|
||||
}
|
||||
peer.url.contains("binance.org") || peer.name.contains("Binance") -> {
|
||||
if (peer.icons.firstOrNull()?.contains("testnet") == true) {
|
||||
Blockchain.BinanceTestnet
|
||||
} else {
|
||||
Blockchain.Binance
|
||||
}
|
||||
}
|
||||
peer.name.contains("BSC") -> {
|
||||
Blockchain.BSC
|
||||
}
|
||||
else -> {
|
||||
if (isTestNet == true) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +88,6 @@ data class SessionDao(
|
|||
session = session.session,
|
||||
peerMeta = session.peerMeta
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.operations.sign.SignHashCommand
|
||||
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
|
||||
|
|
@ -33,20 +34,8 @@ class WalletConnectSdkHelper {
|
|||
id: Long,
|
||||
type: WcTransactionType,
|
||||
): WcTransactionData? {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val publicKey = Wallet.PublicKey(
|
||||
session.wallet.walletPublicKey,
|
||||
session.wallet.derivedPublicKey,
|
||||
session.wallet.derivationPath,
|
||||
)
|
||||
val walletManager = factory.makeEthereumWalletManager(
|
||||
session.wallet.cardId,
|
||||
publicKey,
|
||||
emptyList(),
|
||||
isTestNet = session.wallet.isTestNet
|
||||
) ?: return null
|
||||
|
||||
|
||||
val walletManager = getWalletManager(session) ?: return null
|
||||
try {
|
||||
walletManager.update()
|
||||
} catch (exception: Exception) {
|
||||
|
|
@ -54,15 +43,14 @@ class WalletConnectSdkHelper {
|
|||
return null
|
||||
}
|
||||
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
|
||||
val wallet = walletManager.wallet
|
||||
val balance =
|
||||
walletManager.wallet.amounts[AmountType.Coin]?.value ?: return null
|
||||
wallet.amounts[AmountType.Coin]?.value ?: return null
|
||||
|
||||
val gas = transaction.gas?.hexToBigDecimal()
|
||||
?: transaction.gasLimit?.hexToBigDecimal() ?: return null
|
||||
|
||||
val decimals = blockchain.decimals()
|
||||
val decimals = wallet.blockchain.decimals()
|
||||
|
||||
val value = (transaction.value ?: "0").hexToBigDecimal()
|
||||
?.movePointLeft(decimals) ?: return null
|
||||
|
|
@ -82,8 +70,8 @@ class WalletConnectSdkHelper {
|
|||
val total = value + fee
|
||||
|
||||
val transactionData = TransactionData(
|
||||
amount = Amount(value, blockchain),
|
||||
fee = Amount(fee, blockchain),
|
||||
amount = Amount(value, wallet.blockchain),
|
||||
fee = Amount(fee, wallet.blockchain),
|
||||
sourceAddress = transaction.from,
|
||||
destinationAddress = transaction.to!!,
|
||||
extras = EthereumTransactionExtras(
|
||||
|
|
@ -115,6 +103,21 @@ class WalletConnectSdkHelper {
|
|||
)
|
||||
}
|
||||
|
||||
private fun getWalletManager(session: WalletConnectSession): WalletManager? {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val publicKey = Wallet.PublicKey(
|
||||
session.wallet.walletPublicKey ?: return null,
|
||||
session.wallet.derivedPublicKey,
|
||||
session.wallet.derivationPath,
|
||||
)
|
||||
val blockchain = session.wallet.getBlockchainForSession()
|
||||
return factory.makeWalletManager(
|
||||
session.wallet.cardId,
|
||||
blockchain,
|
||||
publicKey
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun completeTransaction(data: WcTransactionData): String? {
|
||||
return when (data.type) {
|
||||
WcTransactionType.EthSendTransaction -> sendTransaction(data)
|
||||
|
|
@ -175,6 +178,35 @@ class WalletConnectSdkHelper {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun signBnbTransaction(data: ByteArray, session: WalletConnectActiveData): String? {
|
||||
val command = SignHashCommand(
|
||||
hash = data,
|
||||
walletPublicKey = session.wallet.walletPublicKey ?: return null,
|
||||
derivationPath = session.wallet.derivationPath
|
||||
)
|
||||
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message())
|
||||
return when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val key = session.wallet.derivedPublicKey
|
||||
?: session.wallet.walletPublicKey
|
||||
getBnbResultString(
|
||||
key.toHexString(),
|
||||
result.data.signature.toHexString()
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
(result.error as? TangemSdkError)?.let { error ->
|
||||
FirebaseAnalyticsHandler.logCardSdkError(
|
||||
error,
|
||||
FirebaseAnalyticsHandler.ActionToLog.WalletConnectTransaction,
|
||||
)
|
||||
}
|
||||
Timber.e(result.error.customMessage)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareDataForPersonalSign(
|
||||
message: WCEthereumSignMessage,
|
||||
session: WalletConnectSession,
|
||||
|
|
@ -213,12 +245,12 @@ class WalletConnectSdkHelper {
|
|||
|
||||
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
|
||||
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey
|
||||
val command = SignHashCommand(hashToSign, wallet.walletPublicKey, wallet.derivationPath)
|
||||
val command = SignHashCommand(hashToSign, wallet.walletPublicKey!!, wallet.derivationPath)
|
||||
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
|
||||
is CompletionResult.Success -> {
|
||||
val hash = result.data.signature
|
||||
return EthereumUtils.prepareSignedMessageData(
|
||||
hash, hashToSign, CryptoUtils.decompressPublicKey(key)
|
||||
hash, hashToSign, CryptoUtils.decompressPublicKey(key!!)
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
@ -237,5 +269,8 @@ class WalletConnectSdkHelper {
|
|||
companion object {
|
||||
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
|
||||
private const val HEX_PREFIX = "0x"
|
||||
fun getBnbResultString(publicKey: String, signature: String): String {
|
||||
return "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@ package com.tangem.tap.features.details.redux.walletconnect
|
|||
|
||||
import android.app.Activity
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.domain.tasks.product.ScanResponse
|
||||
import com.tangem.wallet.R
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
|
||||
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
|
||||
import com.trustwallet.walletconnect.models.session.WCSession
|
||||
|
|
@ -27,13 +30,16 @@ sealed class WalletConnectAction : Action {
|
|||
data class OpenSession(
|
||||
val wcUri: String,
|
||||
val wallet: WalletForSession,
|
||||
val scanResponse: ScanResponse
|
||||
) : WalletConnectAction()
|
||||
|
||||
object RefuseOpeningSession : WalletConnectAction()
|
||||
|
||||
data class OpeningSessionTimeout(val session: WCSession) : WalletConnectAction()
|
||||
|
||||
data class AcceptOpeningSession(val session: WalletConnectSession) : WalletConnectAction()
|
||||
data class AcceptOpeningSession(
|
||||
val session: WalletConnectSession, val chainId: Int?,
|
||||
) : WalletConnectAction()
|
||||
|
||||
data class ApproveSession(
|
||||
val session: WCSession,
|
||||
|
|
@ -79,4 +85,20 @@ sealed class WalletConnectAction : Action {
|
|||
object NotifyCameraPermissionIsRequired : WalletConnectAction(), NotificationAction {
|
||||
override val messageResource = R.string.common_camera_denied_alert_message
|
||||
}
|
||||
|
||||
object BinanceTransaction : WalletConnectAction() {
|
||||
data class Trade(
|
||||
val id: Long, val order: WCBinanceTradeOrder,
|
||||
val sessionData: WalletConnectSession,
|
||||
) : WalletConnectAction()
|
||||
|
||||
data class Transfer(
|
||||
val id: Long, val order: WCBinanceTransferOrder,
|
||||
val sessionData: WalletConnectSession,
|
||||
) : WalletConnectAction()
|
||||
|
||||
data class Sign(
|
||||
val id: Long, val data: ByteArray, val sessionData: WCSession,
|
||||
) : WalletConnectAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.features.details.redux.walletconnect
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
|
|
@ -13,10 +12,14 @@ import com.tangem.tap.domain.TapWorkarounds.isTestCard
|
|||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.tasks.product.ScanResponse
|
||||
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.wallet.R
|
||||
import com.trustwallet.walletconnect.models.WCPeerMeta
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class WalletConnectMiddleware {
|
||||
private val walletConnectManager = WalletConnectManager()
|
||||
|
|
@ -48,8 +51,13 @@ class WalletConnectMiddleware {
|
|||
}
|
||||
|
||||
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.ClipboardOrScanQr(
|
||||
action.wcUri)))
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.ClipboardOrScanQr(
|
||||
action.wcUri
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is WalletConnectAction.ScanCard -> {
|
||||
|
|
@ -78,14 +86,23 @@ class WalletConnectMiddleware {
|
|||
}
|
||||
|
||||
is WalletConnectAction.RefuseOpeningSession -> {
|
||||
store.dispatch(GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.OpeningSessionRejected
|
||||
))
|
||||
store.dispatch(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.OpeningSessionRejected
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is WalletConnectAction.AcceptOpeningSession -> {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.ApproveWcSession(
|
||||
action.session)))
|
||||
generateWalletKeys(action.session, action.chainId)
|
||||
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.ApproveWcSession(
|
||||
action.session
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is WalletConnectAction.ApproveSession -> {
|
||||
|
|
@ -120,6 +137,39 @@ class WalletConnectMiddleware {
|
|||
is WalletConnectAction.SignMessage -> {
|
||||
walletConnectManager.sendSignedMessage(action.session)
|
||||
}
|
||||
is WalletConnectAction.BinanceTransaction.Trade -> {
|
||||
val messageData = BnbHelper.createMessageData(action.order)
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.BnbTransactionDialog(
|
||||
data = messageData,
|
||||
session = action.sessionData.session,
|
||||
sessionId = action.id,
|
||||
cardId = action.sessionData.wallet.cardId,
|
||||
dAppName = action.sessionData.peerMeta.name
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
is WalletConnectAction.BinanceTransaction.Transfer -> {
|
||||
val messageData = BnbHelper.createMessageData(action.order)
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.BnbTransactionDialog(
|
||||
data = messageData,
|
||||
session = action.sessionData.session,
|
||||
sessionId = action.id,
|
||||
cardId = action.sessionData.wallet.cardId,
|
||||
dAppName = action.sessionData.peerMeta.name
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
is WalletConnectAction.BinanceTransaction.Sign -> {
|
||||
walletConnectManager.signBnb(
|
||||
action.id, action.data, action.sessionData
|
||||
)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
|
|
@ -134,60 +184,79 @@ class WalletConnectMiddleware {
|
|||
return@ScanCard
|
||||
}
|
||||
|
||||
val walletManager = getWalletManager(scanResponse).guard {
|
||||
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
|
||||
return@ScanCard
|
||||
}
|
||||
|
||||
val wallet = walletManager.wallet
|
||||
val derivedKey = if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) {
|
||||
null
|
||||
} else {
|
||||
walletManager.wallet.publicKey.blockchainKey
|
||||
}
|
||||
store.dispatchOnMain(WalletConnectAction.OpenSession(
|
||||
wcUri = wcUri,
|
||||
wallet = WalletForSession(
|
||||
card.cardId,
|
||||
wallet.publicKey.seedKey,
|
||||
derivedKey,
|
||||
wallet.publicKey.derivationPath,
|
||||
card.isTestCard
|
||||
),
|
||||
))
|
||||
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))
|
||||
}
|
||||
|
||||
private fun getWalletManager(scanResponse: ScanResponse): WalletManager? {
|
||||
private fun generateWalletKeys(session: WalletConnectSession, chainId: Int?) {
|
||||
val scanResponse = store.state.walletConnectState.scanResponse ?: return
|
||||
val walletManager = getWalletManager(scanResponse, session.peerMeta, chainId) ?: return
|
||||
|
||||
val wallet = walletManager.wallet
|
||||
val derivedKey =
|
||||
if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) {
|
||||
null
|
||||
} else {
|
||||
walletManager.wallet.publicKey.blockchainKey
|
||||
}
|
||||
val updatedWallet = session.wallet.copy(
|
||||
walletPublicKey = wallet.publicKey.seedKey,
|
||||
derivedPublicKey = derivedKey,
|
||||
derivationPath = wallet.publicKey.derivationPath,
|
||||
blockchain = wallet.blockchain
|
||||
)
|
||||
val updatedSession = session.copy(wallet = updatedWallet)
|
||||
walletConnectManager.updateSession(updatedSession)
|
||||
}
|
||||
|
||||
private fun getWalletManager(
|
||||
scanResponse: ScanResponse, peer: WCPeerMeta, chainId: Int?
|
||||
): WalletManager? {
|
||||
val card = scanResponse.card
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
|
||||
val wcBlockchain = if (scanResponse.card.isTestCard) {
|
||||
Blockchain.EthereumTestnet
|
||||
} else {
|
||||
Blockchain.Ethereum
|
||||
}
|
||||
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)
|
||||
|
||||
|
||||
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
|
||||
store.state.walletState.getWalletManager(wcBlockchain)
|
||||
?: factory.makeWalletManagerForApp(scanResponse, wcBlockchain)
|
||||
store.state.walletState.getWalletManager(blockchain)
|
||||
?: factory.makeWalletManagerForApp(scanResponse, blockchain)
|
||||
?.also { walletManager ->
|
||||
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager))
|
||||
store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain))
|
||||
}
|
||||
} else {
|
||||
val walletManager = factory.makeWalletManagerForApp(scanResponse, blockchain)
|
||||
if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains(
|
||||
wcBlockchain) == true
|
||||
blockchain
|
||||
) != true
|
||||
) {
|
||||
factory.makeWalletManagerForApp(scanResponse, wcBlockchain)
|
||||
} else {
|
||||
factory.makeWalletManagerForApp(scanResponse, wcBlockchain)
|
||||
?.also {
|
||||
currenciesRepository.saveAddedBlockchain(card.cardId, wcBlockchain)
|
||||
}
|
||||
walletManager?.let {
|
||||
currenciesRepository.saveAddedBlockchain(
|
||||
card.cardId,
|
||||
blockchain
|
||||
)
|
||||
}
|
||||
}
|
||||
return walletManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,9 @@ class WalletConnectReducer {
|
|||
sessions = state.sessions + action.session
|
||||
)
|
||||
}
|
||||
is WalletConnectAction.OpenSession -> {
|
||||
state.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is WalletConnectAction.SetSessionsRestored ->
|
||||
WalletConnectState(sessions = action.sessions)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.tap.features.details.redux.walletconnect
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.domain.tasks.product.ScanResponse
|
||||
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
|
||||
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
|
||||
import com.trustwallet.walletconnect.models.WCPeerMeta
|
||||
|
|
@ -13,6 +15,7 @@ import com.trustwallet.walletconnect.models.session.WCSession
|
|||
data class WalletConnectState(
|
||||
val loading: Boolean = false,
|
||||
val sessions: List<WalletConnectSession> = listOf(),
|
||||
val scanResponse: ScanResponse? = null
|
||||
)
|
||||
|
||||
data class WalletConnectSession(
|
||||
|
|
@ -21,18 +24,58 @@ data class WalletConnectSession(
|
|||
val wallet: WalletForSession,
|
||||
val session: WCSession,
|
||||
val peerMeta: WCPeerMeta,
|
||||
)
|
||||
) {
|
||||
fun getAddress(): String? {
|
||||
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey ?: return null
|
||||
return wallet.blockchain?.makeAddresses(key)?.first()?.value
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletForSession(
|
||||
val cardId: String,
|
||||
val walletPublicKey: ByteArray,
|
||||
val walletPublicKey: ByteArray?,
|
||||
val derivedPublicKey: ByteArray?,
|
||||
val derivationPath: DerivationPath?,
|
||||
val isTestNet: Boolean = false,
|
||||
val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
) {
|
||||
val chainId
|
||||
get() = if (isTestNet) 4 else 1
|
||||
|
||||
fun getBlockchainForSession(): Blockchain {
|
||||
return blockchain ?: if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WalletForSession
|
||||
|
||||
if (cardId != other.cardId) return false
|
||||
if (walletPublicKey != null) {
|
||||
if (other.walletPublicKey == null) return false
|
||||
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
|
||||
} else if (other.walletPublicKey != null) return false
|
||||
if (derivedPublicKey != null) {
|
||||
if (other.derivedPublicKey == null) return false
|
||||
if (!derivedPublicKey.contentEquals(other.derivedPublicKey)) return false
|
||||
} else if (other.derivedPublicKey != null) return false
|
||||
if (derivationPath != other.derivationPath) return false
|
||||
if (isTestNet != other.isTestNet) return false
|
||||
if (blockchain != other.blockchain) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = cardId.hashCode()
|
||||
result = 31 * result + (walletPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (derivedPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (derivationPath?.hashCode() ?: 0)
|
||||
result = 31 * result + isTestNet.hashCode()
|
||||
result = 31 * result + (blockchain?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -46,6 +89,12 @@ sealed class WalletConnectDialog : StateDialog {
|
|||
WalletConnectDialog()
|
||||
|
||||
data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog()
|
||||
data class BnbTransactionDialog(val data: BinanceMessageData,
|
||||
val session: WCSession,
|
||||
val sessionId: Long,
|
||||
val cardId: String,
|
||||
val dAppName: String
|
||||
) : WalletConnectDialog()
|
||||
}
|
||||
|
||||
data class WcTransactionData(
|
||||
|
|
@ -59,7 +108,7 @@ data class WcTransactionData(
|
|||
|
||||
enum class WcTransactionType {
|
||||
EthSignTransaction,
|
||||
EthSendTransaction,
|
||||
EthSendTransaction
|
||||
}
|
||||
|
||||
data class WcPersonalSignData(
|
||||
|
|
@ -68,4 +117,29 @@ data class WcPersonalSignData(
|
|||
val id: Long,
|
||||
val dialogData: PersonalSignDialogData,
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
sealed class BinanceMessageData(
|
||||
val address: String,
|
||||
val data: ByteArray,
|
||||
) {
|
||||
class Trade(
|
||||
val tradeData: List<TradeData>,
|
||||
address: String,
|
||||
data: ByteArray,
|
||||
) : BinanceMessageData(address, data)
|
||||
|
||||
class Transfer(
|
||||
val outputAddress: String,
|
||||
val amount: String,
|
||||
address: String,
|
||||
data: ByteArray,
|
||||
) : BinanceMessageData(address, data)
|
||||
}
|
||||
|
||||
data class TradeData(
|
||||
val price: String,
|
||||
val quantity: String,
|
||||
val amount: String,
|
||||
val symbol: String
|
||||
)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.tap.features.details.ui.walletconnect.dialogs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.trustwallet.walletconnect.models.session.WCSession
|
||||
|
||||
class BnbTransactionDialog {
|
||||
companion object {
|
||||
fun create(
|
||||
data: BinanceMessageData,
|
||||
session: WCSession,
|
||||
sessionId: Long,
|
||||
cardId: String,
|
||||
dAppName: String,
|
||||
context: Context,
|
||||
): AlertDialog {
|
||||
|
||||
val message = when (data) {
|
||||
is BinanceMessageData.Trade -> data.tradeData.map {
|
||||
context.getString(
|
||||
R.string.wallet_connect_bnb_trade_order_message,
|
||||
it.symbol,
|
||||
it.price,
|
||||
it.quantity,
|
||||
it.amount
|
||||
)
|
||||
}.joinToString ( "\n\n" )
|
||||
is BinanceMessageData.Transfer -> context.getString(
|
||||
R.string.wallet_connect_bnb_transaction_message,
|
||||
data.address,
|
||||
data.outputAddress,
|
||||
data.amount
|
||||
)
|
||||
}
|
||||
|
||||
val fullMessage = context.getString(
|
||||
R.string.wallet_connect_bnb_sign_message,
|
||||
dAppName, cardId, message
|
||||
)
|
||||
val positiveButtonTitle = context.getText(R.string.common_sign)
|
||||
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(context.getString(R.string.wallet_connect))
|
||||
setMessage(fullMessage)
|
||||
setPositiveButton(positiveButtonTitle) { _, _ ->
|
||||
store.dispatch(WalletConnectAction.BinanceTransaction.Sign(
|
||||
id = sessionId,
|
||||
data = data.data,
|
||||
sessionData = session
|
||||
))
|
||||
}
|
||||
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
|
||||
store.dispatch(WalletConnectAction.RejectRequest(session, sessionId))
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
}
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ class MultiWalletMiddleware {
|
|||
val walletManager = walletState?.getWalletManager(Blockchain.Ethereum)
|
||||
?: walletFactory.makeWalletManagerForApp(scanResponse, Blockchain.Ethereum)
|
||||
|
||||
val tokenFinder = walletManager as TokenFinder
|
||||
val tokenFinder = walletManager as? TokenFinder ?: return
|
||||
scope.launch {
|
||||
val result = tokenFinder.findTokens()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
|
|||
|
|
@ -294,4 +294,21 @@
|
|||
|
||||
<string name="details_manage_security_access_code_disclaimer" translatable="false">Access code is available only for cards with a backup.</string>
|
||||
|
||||
|
||||
|
||||
<string name="wallet_connect_bnb_transaction_signed" translatable="false">The BNB transaction has been successfully signed and sent to the Dapp</string>
|
||||
<string name="wallet_connect_bnb_sign_message" translatable="false">Dapp %s, requesting to\n
|
||||
sign BNB transaction with\n
|
||||
Card: %s\n
|
||||
\n
|
||||
%s</string>
|
||||
<string name="wallet_connect_bnb_transaction_message" translatable="false">Transaction details:\n
|
||||
From: %s\n
|
||||
To: %s\n
|
||||
Amount: %s</string>
|
||||
<string name="wallet_connect_bnb_trade_order_message" translatable="false">Trade order for %s\n
|
||||
Price: %s\n
|
||||
Amount to receive: %s\n
|
||||
Amount to pay: %s</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue