feat: support message signing
This commit is contained in:
parent
37e213e89b
commit
b18a1d6b4d
|
@ -1,6 +1,7 @@
|
||||||
import NimQml
|
import NimQml
|
||||||
import ../../status/[status, ens, chat/stickers, wallet]
|
import ../../status/[status, ens, chat/stickers, wallet]
|
||||||
import ../../status/libstatus/types
|
import ../../status/libstatus/types
|
||||||
|
import ../../status/libstatus/accounts
|
||||||
import ../../status/libstatus/core
|
import ../../status/libstatus/core
|
||||||
import ../../status/libstatus/settings as status_settings
|
import ../../status/libstatus/settings as status_settings
|
||||||
import json, json_serialization, sets, strutils
|
import json, json_serialization, sets, strutils
|
||||||
|
@ -9,8 +10,8 @@ import nbaser
|
||||||
import stew/byteutils
|
import stew/byteutils
|
||||||
from base32 import nil
|
from base32 import nil
|
||||||
|
|
||||||
const AUTH_METHODS = toHashSet(["eth_accounts", "eth_coinbase", "eth_sendTransaction", "eth_sign", "keycard_signTypedData", "eth_signTypedData", "personal_sign", "personal_ecRecover"])
|
const AUTH_METHODS = toHashSet(["eth_accounts", "eth_coinbase", "eth_sendTransaction", "eth_sign", "keycard_signTypedData", "eth_signTypedData", "eth_signTypedData_v3", "personal_sign", "personal_ecRecover"])
|
||||||
const SIGN_METHODS = toHashSet(["eth_sendTransaction", "personal_sign", "eth_signTypedData", "eth_signTypedData_v3"])
|
const SIGN_METHODS = toHashSet(["eth_sign", "personal_sign", "eth_signTypedData", "eth_signTypedData_v3"])
|
||||||
const ACC_METHODS = toHashSet(["eth_accounts", "eth_coinbase"])
|
const ACC_METHODS = toHashSet(["eth_accounts", "eth_coinbase"])
|
||||||
|
|
||||||
const IPFS_SCHEME = "https"
|
const IPFS_SCHEME = "https"
|
||||||
|
@ -65,7 +66,7 @@ proc toWeb3SendAsyncReadOnly(message: string): Web3SendAsyncReadOnly =
|
||||||
request: $data["payload"],
|
request: $data["payload"],
|
||||||
hostname: data{"hostname"}.getStr(),
|
hostname: data{"hostname"}.getStr(),
|
||||||
payload: Payload(
|
payload: Payload(
|
||||||
id: data["payload"]["id"],
|
id: data["payload"]{"id"},
|
||||||
rpcMethod: data["payload"]["method"].getStr()
|
rpcMethod: data["payload"]["method"].getStr()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -108,7 +109,7 @@ QtObject:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if SIGN_METHODS.contains(data.payload.rpcMethod):
|
if data.payload.rpcMethod == "eth_sendTransaction":
|
||||||
try:
|
try:
|
||||||
let request = data.request.parseJson
|
let request = data.request.parseJson
|
||||||
let fromAddress = request["params"][0]["from"].getStr()
|
let fromAddress = request["params"][0]["from"].getStr()
|
||||||
|
@ -120,7 +121,7 @@ QtObject:
|
||||||
let password = request["password"].getStr()
|
let password = request["password"].getStr()
|
||||||
let selectedGasLimit = request["selectedGasLimit"].getStr()
|
let selectedGasLimit = request["selectedGasLimit"].getStr()
|
||||||
let selectedGasPrice = request["selectedGasPrice"].getStr()
|
let selectedGasPrice = request["selectedGasPrice"].getStr()
|
||||||
let txData = if (request["params"][0]["data"] != nil):
|
let txData = if (request["params"][0].hasKey("data") and request["params"][0]["data"].kind != JNull):
|
||||||
request["params"][0]["data"].getStr()
|
request["params"][0]["data"].getStr()
|
||||||
else:
|
else:
|
||||||
""
|
""
|
||||||
|
@ -156,6 +157,52 @@ QtObject:
|
||||||
"message": e.msg
|
"message": e.msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if SIGN_METHODS.contains(data.payload.rpcMethod):
|
||||||
|
try:
|
||||||
|
let request = data.request.parseJson
|
||||||
|
var params = request["params"]
|
||||||
|
let password = hashPassword(request["password"].getStr())
|
||||||
|
let dappAddress = status_settings.getSetting[string](Setting.DappsAddress)
|
||||||
|
var rpcResult = "{}"
|
||||||
|
|
||||||
|
case data.payload.rpcMethod:
|
||||||
|
of "eth_signTypedData", "eth_signTypedData_v3":
|
||||||
|
rpcResult = signTypedData(params[1].getStr(), dappAddress, password)
|
||||||
|
else:
|
||||||
|
rpcResult = signMessage($ %* {
|
||||||
|
"data": params[0].getStr(),
|
||||||
|
"password": password,
|
||||||
|
"account": dappAddress
|
||||||
|
})
|
||||||
|
|
||||||
|
let jsonRpcResult = rpcResult.parseJson
|
||||||
|
let success: bool = not jsonRpcResult.hasKey("error")
|
||||||
|
let errorMessage = if success: "" else: jsonRpcResult["error"]{"message"}.getStr()
|
||||||
|
let response = if success: jsonRpcResult["result"].getStr() else: ""
|
||||||
|
|
||||||
|
return $ %* {
|
||||||
|
"type": ResponseTypes.Web3SendAsyncCallback,
|
||||||
|
"messageId": data.messageId,
|
||||||
|
"error": errorMessage,
|
||||||
|
"result": {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": if (success): response else: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error "Error signing message", msg = e.msg
|
||||||
|
return $ %* {
|
||||||
|
"type": ResponseTypes.Web3SendAsyncCallback,
|
||||||
|
"messageId": data.messageId,
|
||||||
|
"error": {
|
||||||
|
"code": 4100,
|
||||||
|
"message": e.msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if ACC_METHODS.contains(data.payload.rpcMethod):
|
if ACC_METHODS.contains(data.payload.rpcMethod):
|
||||||
let dappAddress = status_settings.getSetting[string](Setting.DappsAddress)
|
let dappAddress = status_settings.getSetting[string](Setting.DappsAddress)
|
||||||
|
@ -248,5 +295,9 @@ QtObject:
|
||||||
proc getHost*(self: Web3ProviderView, url: string): string {.slot.} =
|
proc getHost*(self: Web3ProviderView, url: string): string {.slot.} =
|
||||||
result = url_host(url)
|
result = url_host(url)
|
||||||
|
|
||||||
|
proc signMessage*(self: Web3ProviderView, payload: string, password: string) {.slot.} =
|
||||||
|
let jsonPayload = payload.parseJson
|
||||||
|
|
||||||
|
|
||||||
proc init*(self: Web3ProviderView) =
|
proc init*(self: Web3ProviderView) =
|
||||||
self.setDappsAddress(status_settings.getSetting[string](Setting.DappsAddress))
|
self.setDappsAddress(status_settings.getSetting[string](Setting.DappsAddress))
|
||||||
|
|
|
@ -8,6 +8,7 @@ import ../../status/libstatus/wallet as status_wallet
|
||||||
import ../../status/libstatus/utils as status_utils
|
import ../../status/libstatus/utils as status_utils
|
||||||
import ../../status/ens as status_ens
|
import ../../status/ens as status_ens
|
||||||
import web3/[ethtypes, conversions]
|
import web3/[ethtypes, conversions]
|
||||||
|
import stew/byteutils
|
||||||
|
|
||||||
QtObject:
|
QtObject:
|
||||||
type UtilsView* = ref object of QObject
|
type UtilsView* = ref object of QObject
|
||||||
|
@ -69,6 +70,9 @@ QtObject:
|
||||||
add(str, "0")
|
add(str, "0")
|
||||||
return str
|
return str
|
||||||
|
|
||||||
|
proc hex2Ascii*(self: UtilsView, value: string): string {.slot.} =
|
||||||
|
result = string.fromBytes(hexToSeqByte(value))
|
||||||
|
|
||||||
proc hex2Eth*(self: UtilsView, value: string): string {.slot.} =
|
proc hex2Eth*(self: UtilsView, value: string): string {.slot.} =
|
||||||
return stripTrailingZeroes(status_utils.wei2Eth(stint.fromHex(StUint[256], value)))
|
return stripTrailingZeroes(status_utils.wei2Eth(stint.fromHex(StUint[256], value)))
|
||||||
|
|
||||||
|
@ -77,6 +81,6 @@ QtObject:
|
||||||
if value == "0x0":
|
if value == "0x0":
|
||||||
return "0"
|
return "0"
|
||||||
return stripTrailingZeroes(stint.toString(stint.fromHex(StUint[256], value)))
|
return stripTrailingZeroes(stint.toString(stint.fromHex(StUint[256], value)))
|
||||||
|
|
||||||
proc urlFromUserInput*(self: UtilsView, input: string): string {.slot.} =
|
proc urlFromUserInput*(self: UtilsView, input: string): string {.slot.} =
|
||||||
result = url_fromUserInput(input)
|
result = url_fromUserInput(input)
|
||||||
|
|
|
@ -25,7 +25,7 @@ proc getNodeConfig*(installationId: string, currentNetwork: string = constants.D
|
||||||
result["ListenAddr"] = if existsEnv("STATUS_PORT"): newJString("0.0.0.0:" & $getEnv("STATUS_PORT")) else: newJString("0.0.0.0:30305")
|
result["ListenAddr"] = if existsEnv("STATUS_PORT"): newJString("0.0.0.0:" & $getEnv("STATUS_PORT")) else: newJString("0.0.0.0:30305")
|
||||||
result = constants.NODE_CONFIG
|
result = constants.NODE_CONFIG
|
||||||
|
|
||||||
proc hashPassword(password: string): string =
|
proc hashPassword*(password: string): string =
|
||||||
result = "0x" & $keccak_256.digest(password)
|
result = "0x" & $keccak_256.digest(password)
|
||||||
|
|
||||||
proc getDefaultAccount*(): string =
|
proc getDefaultAccount*(): string =
|
||||||
|
|
|
@ -49,3 +49,9 @@ proc getBlockByNumber*(blockNumber: string): string =
|
||||||
|
|
||||||
proc getTransfersByAddress*(address: string, toBlock: string, limit: string): string =
|
proc getTransfersByAddress*(address: string, toBlock: string, limit: string): string =
|
||||||
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, toBlock, limit])
|
result = callPrivateRPC("wallet_getTransfersByAddress", %* [address, toBlock, limit])
|
||||||
|
|
||||||
|
proc signMessage*(rpcParams: string): string =
|
||||||
|
return $nim_status.signMessage(rpcParams)
|
||||||
|
|
||||||
|
proc signTypedData*(data: string, address: string, password: string): string =
|
||||||
|
return $nim_status.signTypedData(data, address, password)
|
||||||
|
|
|
@ -109,7 +109,6 @@ proc getPendingOutboundTransactionsByAddress*(address: string): string =
|
||||||
let payload = %* [address]
|
let payload = %* [address]
|
||||||
result = callPrivateRPC("wallet_getPendingOutboundTransactionsByAddress", payload)
|
result = callPrivateRPC("wallet_getPendingOutboundTransactionsByAddress", payload)
|
||||||
|
|
||||||
|
|
||||||
proc deletePendingTransaction*(transactionHash: string) =
|
proc deletePendingTransaction*(transactionHash: string) =
|
||||||
let payload = %* [transactionHash]
|
let payload = %* [transactionHash]
|
||||||
discard callPrivateRPC("wallet_deletePendingTransaction", payload)
|
discard callPrivateRPC("wallet_deletePendingTransaction", payload)
|
||||||
|
|
|
@ -328,4 +328,3 @@ proc getGasPricePredictions*(self: WalletModel): GasPricePrediction =
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
echo "error getting gas price predictions"
|
echo "error getting gas price predictions"
|
||||||
echo e.msg
|
echo e.msg
|
||||||
|
|
||||||
|
|
|
@ -47,13 +47,18 @@ Rectangle {
|
||||||
id: accessDialog
|
id: accessDialog
|
||||||
|
|
||||||
property var request: ({"hostname": "", "title": "", "permission": ""})
|
property var request: ({"hostname": "", "title": "", "permission": ""})
|
||||||
|
property bool interactedWith: false
|
||||||
|
|
||||||
function postMessage(isAllowed){
|
function postMessage(isAllowed){
|
||||||
|
interactedWith = true
|
||||||
request.isAllowed = isAllowed;
|
request.isAllowed = isAllowed;
|
||||||
provider.web3Response(_web3Provider.postMessage(JSON.stringify(request)));
|
provider.web3Response(_web3Provider.postMessage(JSON.stringify(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
onClosed: {
|
onClosed: {
|
||||||
|
if(!interactedWith){
|
||||||
|
postMessage(false);
|
||||||
|
}
|
||||||
accessDialog.destroy();
|
accessDialog.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +134,8 @@ Rectangle {
|
||||||
// TODO we'll need a new dialog at one point because this one is not using the same call, but it's good for now
|
// TODO we'll need a new dialog at one point because this one is not using the same call, but it's good for now
|
||||||
property Component sendTransactionModalComponent: SignTransactionModal {}
|
property Component sendTransactionModalComponent: SignTransactionModal {}
|
||||||
|
|
||||||
|
property Component signMessageModalComponent: SignMessageModal {}
|
||||||
|
|
||||||
property MessageDialog sendingError: MessageDialog {
|
property MessageDialog sendingError: MessageDialog {
|
||||||
id: sendingError
|
id: sendingError
|
||||||
//% "Error sending the transaction"
|
//% "Error sending the transaction"
|
||||||
|
@ -137,6 +144,13 @@ Rectangle {
|
||||||
standardButtons: StandardButton.Ok
|
standardButtons: StandardButton.Ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property MessageDialog signingError: MessageDialog {
|
||||||
|
id: signingError
|
||||||
|
title: qsTr("Error signing message")
|
||||||
|
icon: StandardIcon.Critical
|
||||||
|
standardButtons: StandardButton.Ok
|
||||||
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: provider
|
id: provider
|
||||||
WebChannel.id: "backend"
|
WebChannel.id: "backend"
|
||||||
|
@ -235,7 +249,35 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDialog.open();
|
sendDialog.open();
|
||||||
_walletModel.getGasPricePredictions()
|
walletModel.getGasPricePredictions()
|
||||||
|
} else if (request.type === Constants.web3SendAsyncReadOnly && ["eth_sign", "personal_sign", "eth_signTypedData", "eth_signTypedData_v3"].indexOf(request.payload.method) > -1) {
|
||||||
|
const signDialog = signMessageModalComponent.createObject(browserWindow, {request});
|
||||||
|
signDialog.web3Response = web3Response
|
||||||
|
signDialog.signMessage = function (enteredPassword) {
|
||||||
|
signDialog.interactedWith = true;
|
||||||
|
request.payload.password = enteredPassword;
|
||||||
|
const response = web3Provider.postMessage(JSON.stringify(request));
|
||||||
|
provider.web3Response(response);
|
||||||
|
try {
|
||||||
|
let responseObj = JSON.parse(response)
|
||||||
|
if (responseObj.error) {
|
||||||
|
throw new Error(responseObj.error)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes("could not decrypt key with given password")){
|
||||||
|
//% "Wrong password"
|
||||||
|
signDialog.transactionSigner.validationError = qsTrId("wrong-password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
signingError.text = e.message
|
||||||
|
return signingError.open()
|
||||||
|
}
|
||||||
|
signDialog.close()
|
||||||
|
signDialog.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
signDialog.open();
|
||||||
} else {
|
} else {
|
||||||
web3Response(_web3Provider.postMessage(data));
|
web3Response(_web3Provider.postMessage(data));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import QtQuick 2.13
|
||||||
|
import QtQuick.Controls 2.13
|
||||||
|
import QtQuick.Layouts 1.13
|
||||||
|
import QtQuick.Dialogs 1.3
|
||||||
|
import "../../../imports"
|
||||||
|
import "../../../shared"
|
||||||
|
import "../../../shared/status"
|
||||||
|
|
||||||
|
ModalPopup {
|
||||||
|
property var request
|
||||||
|
|
||||||
|
readonly property int bytes32Length: 66
|
||||||
|
|
||||||
|
property bool interactedWith: false
|
||||||
|
|
||||||
|
property alias transactionSigner: transactionSigner
|
||||||
|
|
||||||
|
property var signMessage: function(enteredPassword) {}
|
||||||
|
|
||||||
|
property var web3Response
|
||||||
|
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Signing a message")
|
||||||
|
height: 504
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
if(!interactedWith){
|
||||||
|
web3Response(JSON.stringify({
|
||||||
|
"type": "web3-send-async-callback",
|
||||||
|
"messageId": request.messageId,
|
||||||
|
"error": {
|
||||||
|
"code": 4100
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
stack.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Style.current.padding
|
||||||
|
anchors.rightMargin: Style.current.padding
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: messageToSign
|
||||||
|
width: parent.width
|
||||||
|
height: 100
|
||||||
|
TextArea {
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
readOnly: true
|
||||||
|
text: {
|
||||||
|
switch(request.payload.method){
|
||||||
|
case Constants.personal_sign:
|
||||||
|
return request.payload.params[0].length === bytes32Length ? request.payload.params[0] : utilsModel.hex2Ascii(request.payload.params[0]);
|
||||||
|
case Constants.eth_sign:
|
||||||
|
return request.payload.params[1];
|
||||||
|
case Constants.eth_signTypedData:
|
||||||
|
case Constants.eth_signTypedData_v3:
|
||||||
|
return JSON.stringify(request.payload.params[1]); // TODO: requires design
|
||||||
|
default:
|
||||||
|
return JSON.stringify(request.payload.params); // support for any unhandled sign method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionSigner {
|
||||||
|
id: transactionSigner
|
||||||
|
width: parent.width
|
||||||
|
anchors.top: messageToSign.bottom
|
||||||
|
anchors.topMargin: Style.current.padding * 3
|
||||||
|
signingPhrase: walletModel.signingPhrase
|
||||||
|
reset: function() {
|
||||||
|
signingPhrase = Qt.binding(function() { return walletModel.signingPhrase })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
footer: Item {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
StatusButton {
|
||||||
|
id: btnNext
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: qsTr("Sign")
|
||||||
|
onClicked: root.signMessage(transactionSigner.enteredPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*##^##
|
||||||
|
Designer {
|
||||||
|
D{i:0;autoSize:true;height:480;width:640}
|
||||||
|
}
|
||||||
|
##^##*/
|
||||||
|
|
|
@ -67,4 +67,8 @@ QtObject {
|
||||||
readonly property string permission_web3: "web3"
|
readonly property string permission_web3: "web3"
|
||||||
readonly property string permission_contactCode: "contact-code"
|
readonly property string permission_contactCode: "contact-code"
|
||||||
|
|
||||||
|
readonly property string personal_sign: "personal_sign"
|
||||||
|
readonly property string eth_sign: "eth_sign"
|
||||||
|
readonly property string eth_signTypedData: "eth_signTypedData"
|
||||||
|
readonly property string eth_signTypedData_v3: "eth_signTypedData_v3"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue