Skip to content

Commit

Permalink
Implement WalletConnect ETH transaction signing
Browse files Browse the repository at this point in the history
  • Loading branch information
omurovch committed May 6, 2024
1 parent f147025 commit 1cd0450
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import io.horizontalsystems.bankwallet.ui.compose.components.VSpacer
fun ConfirmTransactionScreen(
title: String = stringResource(R.string.Swap_Confirm_Title),
onClickBack: () -> Unit,
onClickSettings: () -> Unit,
onClickSettings: (() -> Unit)?,
onClickClose: (() -> Unit)?,
buttonsSlot: @Composable() (ColumnScope.() -> Unit),
content: @Composable() (ColumnScope.() -> Unit)
Expand All @@ -37,19 +37,21 @@ fun ConfirmTransactionScreen(
HsBackButton(onClick = onClickBack)
},
menuItems = buildList<MenuItem> {
add(
MenuItem(
title = TranslatableString.ResString(R.string.Settings_Title),
icon = R.drawable.ic_manage_2_24,
onClick = onClickSettings
onClickSettings?.let {
add(
MenuItem(
title = TranslatableString.ResString(R.string.Settings_Title),
icon = R.drawable.ic_manage_2_24,
onClick = onClickSettings
)
)
)
onClickClose?.let<() -> Unit, Unit> {
}
onClickClose?.let {
add(
MenuItem(
title = TranslatableString.ResString(R.string.Button_Close),
icon = R.drawable.ic_close,
onClick = it
onClick = onClickClose
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import io.horizontalsystems.bankwallet.core.IAccountManager
import io.horizontalsystems.bankwallet.core.managers.ActiveAccountState
import io.horizontalsystems.bankwallet.modules.walletconnect.storage.WCSessionStorage
import io.horizontalsystems.bankwallet.modules.walletconnect.storage.WalletConnectV2Session
import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -21,7 +20,6 @@ class WCSessionManager(
) {

private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val disposable = CompositeDisposable()

private val _sessionsFlow = MutableStateFlow<List<Wallet.Model.Session>>(emptyList())
val sessionsFlow: StateFlow<List<Wallet.Model.Session>>
Expand Down Expand Up @@ -130,6 +128,8 @@ class WCSessionManager(
object NoSuitableEvmKit : RequestDataError()
object NoSigner : RequestDataError()
object RequestNotFoundError : RequestDataError()
object InvalidGasPrice: RequestDataError()
object InvalidNonce: RequestDataError()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.horizontalsystems.bankwallet.modules.sendevmtransaction.ValueType
import io.horizontalsystems.bankwallet.modules.sendevmtransaction.ViewItem
import io.horizontalsystems.bankwallet.modules.walletconnect.request.sendtransaction.WCEthereumTransaction
import io.horizontalsystems.bankwallet.modules.walletconnect.request.sendtransaction.WCSendEthRequestScreen
import io.horizontalsystems.bankwallet.modules.walletconnect.request.signtransaction.WCSignEthereumTransactionRequestScreen
import io.horizontalsystems.bankwallet.modules.walletconnect.session.ui.BlockchainCell
import io.horizontalsystems.bankwallet.ui.compose.ComposeAppTheme
import io.horizontalsystems.bankwallet.ui.compose.TranslatableString
Expand All @@ -42,8 +43,7 @@ class WCRequestFragment : BaseComposeFragment() {

@Composable
override fun GetContent(navController: NavController) {
val wcRequestViewModel =
viewModel<WCNewRequestViewModel>(factory = WCNewRequestViewModel.Factory())
val wcRequestViewModel = viewModel<WCNewRequestViewModel>(factory = WCNewRequestViewModel.Factory())
val composableScope = rememberCoroutineScope()
when (val sessionRequestUI = wcRequestViewModel.sessionRequest) {
is SessionRequestUI.Content -> {
Expand All @@ -67,6 +67,26 @@ class WCRequestFragment : BaseComposeFragment() {
transaction,
sessionRequestUI.peerUI.peerName
)
} else if (sessionRequestUI.method == "eth_signTransaction") {
val blockchainType = wcRequestViewModel.blockchain?.type ?: return

val transaction = try {
val ethTransaction = Gson().fromJson(
sessionRequestUI.param,
WCEthereumTransaction::class.java
)
ethTransaction.getWCTransaction()
} catch (e: Throwable) {
return
}

WCSignEthereumTransactionRequestScreen(
navController,
logger,
blockchainType,
transaction,
sessionRequestUI.peerUI.peerName
)
} else {
WCNewSignRequestScreen(
sessionRequestUI,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class WCNewRequestViewModel(
extractMessageParamFromPersonalSign(sessionRequest.request.params)
}

TYPED_DATA_METHOD, SEND_TRANSACTION_METHOD -> {
TYPED_DATA_METHOD, SEND_TRANSACTION_METHOD, SIGN_TRANSACTION_METHOD -> {
val params = JsonParser.parseString(sessionRequest.request.params).asJsonArray
params.firstOrNull { it.isJsonObject }?.asJsonObject?.toString()
?: throw Exception("Invalid Data")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.horizontalsystems.bankwallet.modules.walletconnect.request.signtransaction

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import io.horizontalsystems.bankwallet.R
import io.horizontalsystems.bankwallet.core.AppLogger
import io.horizontalsystems.bankwallet.core.stats.StatPage
import io.horizontalsystems.bankwallet.modules.confirm.ConfirmTransactionScreen
import io.horizontalsystems.bankwallet.modules.sendevmtransaction.SendEvmTransactionView
import io.horizontalsystems.bankwallet.modules.walletconnect.request.sendtransaction.WalletConnectTransaction
import io.horizontalsystems.bankwallet.ui.compose.components.ButtonPrimaryDefault
import io.horizontalsystems.bankwallet.ui.compose.components.ButtonPrimaryYellow
import io.horizontalsystems.bankwallet.ui.compose.components.VSpacer
import io.horizontalsystems.core.helpers.HudHelper
import io.horizontalsystems.marketkit.models.BlockchainType
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
fun WCSignEthereumTransactionRequestScreen(
navController: NavController,
logger: AppLogger,
blockchainType: BlockchainType,
transaction: WalletConnectTransaction,
peerName: String,
) {
val viewModelStoreOwner = remember(navController.currentBackStackEntry) {
navController.getBackStackEntry(R.id.wcRequestFragment)
}
val viewModel = viewModel<WCSignEthereumTransactionRequestViewModel>(
viewModelStoreOwner = viewModelStoreOwner,
factory = WCSignEthereumTransactionRequestViewModel.Factory(
blockchainType = blockchainType,
transaction = transaction,
peerName = peerName
)
)
val uiState = viewModel.uiState

ConfirmTransactionScreen(
title = stringResource(id = R.string.WalletConnect_SignMessageRequest_Title),
onClickBack = navController::popBackStack,
onClickSettings = null,
onClickClose = navController::popBackStack,
buttonsSlot = {
val coroutineScope = rememberCoroutineScope()
val view = LocalView.current


ButtonPrimaryYellow(
modifier = Modifier.fillMaxWidth(),
title = stringResource(R.string.Button_Sign),
onClick = {
coroutineScope.launch {
try {
logger.info("click sign button")
viewModel.sign()
logger.info("success")

HudHelper.showSuccessMessage(view, R.string.Hud_Text_Done)
delay(1200)
} catch (t: Throwable) {
logger.warning("failed", t)
HudHelper.showErrorMessage(view, t.javaClass.simpleName)
}

navController.popBackStack()
}
}
)
VSpacer(16.dp)
ButtonPrimaryDefault(
modifier = Modifier.fillMaxWidth(),
title = stringResource(R.string.Button_Reject),
onClick = {
viewModel.reject()
navController.popBackStack()
}
)
}
) {
SendEvmTransactionView(
navController,
uiState.sectionViewItems,
uiState.cautions,
uiState.transactionFields,
uiState.networkFee,
StatPage.WalletConnect
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package io.horizontalsystems.bankwallet.modules.walletconnect.request.signtransaction

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.horizontalsystems.bankwallet.R
import io.horizontalsystems.bankwallet.core.App
import io.horizontalsystems.bankwallet.core.ViewModelUiState
import io.horizontalsystems.bankwallet.core.ethereum.CautionViewItem
import io.horizontalsystems.bankwallet.core.ethereum.EvmCoinService
import io.horizontalsystems.bankwallet.core.ethereum.EvmCoinServiceFactory
import io.horizontalsystems.bankwallet.core.managers.EvmKitWrapper
import io.horizontalsystems.bankwallet.core.providers.Translator
import io.horizontalsystems.bankwallet.core.toHexString
import io.horizontalsystems.bankwallet.modules.evmfee.GasData
import io.horizontalsystems.bankwallet.modules.multiswap.ui.DataField
import io.horizontalsystems.bankwallet.modules.multiswap.ui.DataFieldNonce
import io.horizontalsystems.bankwallet.modules.send.SendModule
import io.horizontalsystems.bankwallet.modules.sendevmtransaction.SectionViewItem
import io.horizontalsystems.bankwallet.modules.sendevmtransaction.SendEvmTransactionViewItemFactory
import io.horizontalsystems.bankwallet.modules.sendevmtransaction.ValueType
import io.horizontalsystems.bankwallet.modules.sendevmtransaction.ViewItem
import io.horizontalsystems.bankwallet.modules.walletconnect.WCDelegate
import io.horizontalsystems.bankwallet.modules.walletconnect.WCSessionManager
import io.horizontalsystems.bankwallet.modules.walletconnect.request.sendtransaction.WalletConnectTransaction
import io.horizontalsystems.ethereumkit.models.GasPrice
import io.horizontalsystems.ethereumkit.models.TransactionData
import io.horizontalsystems.marketkit.models.BlockchainType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class WCSignEthereumTransactionRequestViewModel(
private val evmKit: EvmKitWrapper,
baseCoinService: EvmCoinService,
private val sendEvmTransactionViewItemFactory: SendEvmTransactionViewItemFactory,
private val dAppName: String,
transaction: WalletConnectTransaction
) : ViewModelUiState<WCSignEthereumTransactionRequestUiState>() {

private val transactionData = TransactionData(
transaction.to,
transaction.value,
transaction.data
)

private var gasData: GasData? = null
private var nonce: Long? = null
private var feeAmountData: SendModule.AmountData?
private var fields: List<DataField>

init {
val gasPrice = if (transaction.maxFeePerGas != null && transaction.maxPriorityFeePerGas != null) {
GasPrice.Eip1559(transaction.maxFeePerGas, transaction.maxPriorityFeePerGas)
} else if (transaction.gasPrice != null) {
GasPrice.Legacy(transaction.gasPrice)
} else {
null
}

feeAmountData = if (gasPrice != null && transaction.gasLimit != null) {
GasData(gasLimit = transaction.gasLimit, gasPrice = gasPrice).let {
gasData = it
baseCoinService.amountData(
it.estimatedFee,
it.isSurcharged
)
}
} else {
null
}

nonce = transaction.nonce

fields = if (transaction.nonce != null) {
listOf(DataFieldNonce(transaction.nonce))
} else {
emptyList()
}
}

override fun createState() = WCSignEthereumTransactionRequestUiState(
networkFee = feeAmountData,
cautions = emptyList(),
transactionFields = fields,
sectionViewItems = getSectionViewItems()
)

private fun getSectionViewItems(): List<SectionViewItem> {
val items = sendEvmTransactionViewItemFactory.getItems(
transactionData,
null,
evmKit.evmKit.decorate(transactionData)
) + SectionViewItem(
buildList {
add(
ViewItem.Value(
Translator.getString(R.string.WalletConnect_SignMessageRequest_dApp),
dAppName,
ValueType.Regular
)
)
}
)

return items
}

suspend fun sign() = withContext(Dispatchers.Default) {
val signer = evmKit.signer ?: throw WCSessionManager.RequestDataError.NoSigner
val gasData = gasData ?: throw WCSessionManager.RequestDataError.InvalidGasPrice
val nonce = nonce ?: throw WCSessionManager.RequestDataError.InvalidNonce

val signature = signer.signedTransaction(
address = transactionData.to,
value = transactionData.value,
transactionInput = transactionData.input,
gasPrice = gasData.gasPrice,
gasLimit = gasData.gasLimit,
nonce = nonce
)

WCDelegate.sessionRequestEvent?.let { sessionRequest ->
WCDelegate.respondPendingRequest(sessionRequest.request.id, sessionRequest.topic, signature.toHexString())
}
}

fun reject() {
WCDelegate.sessionRequestEvent?.let { sessionRequest ->
WCDelegate.rejectRequest(sessionRequest.topic, sessionRequest.request.id)
}
}

class Factory(
private val blockchainType: BlockchainType,
private val transaction: WalletConnectTransaction,
private val peerName: String
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val token = App.evmBlockchainManager.getBaseToken(blockchainType)!!
val evmKitWrapper = App.evmBlockchainManager.getEvmKitManager(blockchainType).evmKitWrapper!!
val coinServiceFactory = EvmCoinServiceFactory(
token,
App.marketKit,
App.currencyManager,
App.coinManager
)

val sendEvmTransactionViewItemFactory = SendEvmTransactionViewItemFactory(
App.evmLabelManager,
coinServiceFactory,
App.contactsRepository,
blockchainType
)


return WCSignEthereumTransactionRequestViewModel(
evmKitWrapper,
coinServiceFactory.baseCoinService,
sendEvmTransactionViewItemFactory,
peerName,
transaction
) as T
}
}
}

data class WCSignEthereumTransactionRequestUiState(
val networkFee: SendModule.AmountData?,
val cautions: List<CautionViewItem>,
val transactionFields: List<DataField>,
val sectionViewItems: List<SectionViewItem>
)
Loading

0 comments on commit 1cd0450

Please sign in to comment.