Browse Source

qml: add message sign/verify

master
Sander van Grieken 2 years ago
parent
commit
e5e1e46b7b
  1. 25
      electrum/gui/qml/components/AddressDetails.qml
  2. 3
      electrum/gui/qml/components/Constants.qml
  3. 211
      electrum/gui/qml/components/SignVerifyMessageDialog.qml
  4. 19
      electrum/gui/qml/components/WalletMainView.qml
  5. 67
      electrum/gui/qml/components/controls/ElTextArea.qml
  6. 8
      electrum/gui/qml/components/main.qml
  7. 5
      electrum/gui/qml/qebitcoin.py
  8. 17
      electrum/gui/qml/qedaemon.py
  9. 14
      electrum/gui/qml/qewallet.py

25
electrum/gui/qml/components/AddressDetails.qml

@ -279,11 +279,28 @@ Pane {
}
}
FlatButton {
ButtonContainer {
Layout.fillWidth: true
text: addressdetails.isFrozen ? qsTr('Unfreeze address') : qsTr('Freeze address')
onClicked: addressdetails.freeze(!addressdetails.isFrozen)
icon.source: '../../icons/seal.png'
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: addressdetails.isFrozen ? qsTr('Unfreeze address') : qsTr('Freeze address')
onClicked: addressdetails.freeze(!addressdetails.isFrozen)
icon.source: '../../icons/seal.png'
}
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
visible: Daemon.currentWallet.canSignMessage
text: qsTr('Sign/Verify')
icon.source: '../../icons/pen.png'
onClicked: {
var dialog = app.signVerifyMessageDialog.createObject(app, {
address: root.address
})
dialog.open()
}
}
}
}

3
electrum/gui/qml/components/Constants.qml

@ -30,6 +30,7 @@ Item {
property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2)
property color darkerBackground: Qt.darker(Material.background, 1.20)
property color lighterBackground: Qt.lighter(Material.background, 1.10)
property color darkerDialogBackground: Qt.darker(Material.dialogColor, 1.20)
property color notificationBackground: Qt.lighter(Material.background, 1.5)
property color colorCredit: "#ff80ff80"
@ -40,6 +41,8 @@ Item {
property color colorError: '#ffff8080'
property color colorProgress: '#ffffff80'
property color colorDone: '#ff80ff80'
property color colorValidBackground: '#ff008000'
property color colorInvalidBackground: '#ff800000'
property color colorLightningLocal: "#6060ff"
property color colorLightningLocalReserve: "#0000a0"

211
electrum/gui/qml/components/SignVerifyMessageDialog.qml

@ -0,0 +1,211 @@
import QtQuick 2.15
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.14
import QtQuick.Controls.Material 2.0
import org.electrum 1.0
import "controls"
ElDialog {
id: dialog
enum Check {
Unknown,
Valid,
Invalid
}
property string address
property bool _addressValid: false
property bool _addressMine: false
property int _verified: SignVerifyMessageDialog.Check.Unknown
implicitHeight: parent.height
implicitWidth: parent.width
title: qsTr('Sign/Verify Message')
iconSource: Qt.resolvedUrl('../../icons/pen.png')
padding: 0
function validateAddress() {
// TODO: not all types of addresses are valid (e.g. p2wsh)
_addressValid = bitcoin.isAddress(addressField.text)
_addressMine = Daemon.currentWallet.isAddressMine(addressField.text)
}
ColumnLayout {
width: parent.width
height: parent.height
spacing: constants.paddingLarge
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: constants.paddingLarge
Layout.rightMargin: constants.paddingLarge
Label {
text: qsTr('Address')
color: Material.accentColor
}
RowLayout {
Layout.fillWidth: true
TextField {
id: addressField
Layout.fillWidth: true
placeholderText: qsTr('Address')
font.family: FixedFont
onTextChanged: {
validateAddress()
_verified = SignVerifyMessageDialog.Check.Unknown
}
}
ToolButton {
icon.source: '../../icons/paste.png'
icon.color: 'transparent'
onClicked: {
addressField.text = AppController.clipboardToText()
}
}
}
Label {
text: qsTr('Message')
color: Material.accentColor
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
ElTextArea {
id: plaintext
Layout.fillWidth: true
Layout.fillHeight: true
font.family: FixedFont
wrapMode: TextInput.Wrap
background: PaneInsetBackground {
baseColor: constants.darkerDialogBackground
}
onTextChanged: _verified = SignVerifyMessageDialog.Check.Unknown
}
ColumnLayout {
Layout.alignment: Qt.AlignTop
ToolButton {
icon.source: '../../icons/paste.png'
icon.color: 'transparent'
onClicked: {
plaintext.text = AppController.clipboardToText()
}
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: qsTr('Signature')
color: Material.accentColor
}
Label {
Layout.alignment: Qt.AlignRight
visible: _verified != SignVerifyMessageDialog.Check.Unknown
text: _verified == SignVerifyMessageDialog.Check.Valid
? qsTr('Valid!')
: qsTr('Invalid!')
color: _verified == SignVerifyMessageDialog.Check.Valid
? constants.colorDone
: constants.colorError
}
}
RowLayout {
Layout.fillWidth: true
ElTextArea {
id: signature
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 4 + topPadding + bottomPadding
Layout.minimumHeight: fontMetrics.lineSpacing * 4 + topPadding + bottomPadding
font.family: FixedFont
wrapMode: TextInput.Wrap
background: PaneInsetBackground {
baseColor: _verified == SignVerifyMessageDialog.Check.Unknown
? constants.darkerDialogBackground
: _verified == SignVerifyMessageDialog.Check.Valid
? constants.colorValidBackground
: constants.colorInvalidBackground
}
onTextChanged: _verified = SignVerifyMessageDialog.Check.Unknown
}
ColumnLayout {
Layout.alignment: Qt.AlignTop
ToolButton {
icon.source: '../../icons/paste.png'
icon.color: 'transparent'
onClicked: {
signature.text = AppController.clipboardToText()
}
}
ToolButton {
icon.source: '../../icons/share.png'
icon.color: enabled ? 'transparent' : Material.iconDisabledColor
enabled: signature.text
onClicked: {
var dialog = app.genericShareDialog.createObject(app, {
title: qsTr('Message signature'),
text_qr: signature.text
})
dialog.open()
}
}
}
}
}
ButtonContainer {
Layout.fillWidth: true
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: qsTr('Sign')
visible: Daemon.currentWallet.canSignMessage
enabled: _addressMine
icon.source: '../../icons/seal.png'
onClicked: {
var sig = Daemon.currentWallet.signMessage(addressField.text, plaintext.text)
signature.text = sig
}
}
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
enabled: _addressValid && signature.text
text: qsTr('Verify')
icon.source: '../../icons/confirmed.png'
onClicked: {
var result = Daemon.verifyMessage(addressField.text, plaintext.text, signature.text)
_verified = result
? SignVerifyMessageDialog.Check.Valid
: SignVerifyMessageDialog.Check.Invalid
}
}
}
}
Component.onCompleted: {
addressField.text = address
}
Bitcoin {
id: bitcoin
}
FontMetrics {
id: fontMetrics
font: signature.font
}
}

19
electrum/gui/qml/components/WalletMainView.qml

@ -126,6 +126,21 @@ Item {
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/pen.png'
action: Action {
text: Daemon.currentWallet.canSignMessage
? qsTr('Sign/Verify Message')
: qsTr('Verify Message')
onTriggered: {
var dialog = app.signVerifyMessageDialog.createObject(app)
dialog.open()
menu.deselect()
}
}
}
MenuSeparator { }
MenuItem {
@ -140,6 +155,10 @@ Item {
function openPage(url) {
stack.pushOnRoot(url)
deselect()
}
function deselect() {
currentIndex = -1
}
}

67
electrum/gui/qml/components/controls/ElTextArea.qml

@ -0,0 +1,67 @@
import QtQuick 2.15
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.14
import QtQuick.Controls.Material 2.0
import org.electrum 1.0
// this component adds (auto)scrolling to the bare TextArea, to make it
// workable if text overflows the available space.
// This unfortunately hides many signals and properties from the TextArea,
// so add signals propagation and property aliases when needed.
Flickable {
id: root
property alias text: edit.text
property alias wrapMode: edit.wrapMode
property alias background: rootpane.background
property alias font: edit.font
contentWidth: rootpane.width
contentHeight: rootpane.height
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.VerticalFlick
function ensureVisible(r) {
r.x = r.x + rootpane.leftPadding
r.y = r.y + rootpane.topPadding
var w = width - rootpane.leftPadding - rootpane.rightPadding
var h = height - rootpane.topPadding - rootpane.bottomPadding
if (contentX >= r.x)
contentX = r.x
else if (contentX+w <= r.x+r.width)
contentX = r.x+r.width-w
if (contentY >= r.y)
contentY = r.y
else if (contentY+h <= r.y+r.height)
contentY = r.y+r.height-h
}
Pane {
id: rootpane
width: root.width
height: Math.max(root.height, edit.height + topPadding + bottomPadding)
padding: constants.paddingXSmall
TextArea {
id: edit
width: parent.width
focus: true
wrapMode: TextEdit.Wrap
onCursorRectangleChanged: root.ensureVisible(cursorRectangle)
onTextChanged: root.textChanged()
background: Rectangle {
color: 'transparent'
}
}
MouseArea {
// remaining area clicks focus textarea
width: parent.width
anchors.top: edit.bottom
anchors.bottom: parent.bottom
onClicked: edit.forceActiveFocus()
}
}
}

8
electrum/gui/qml/components/main.qml

@ -387,6 +387,14 @@ ApplicationWindow
id: _channelOpenProgressDialog
}
property alias signVerifyMessageDialog: _signVerifyMessageDialog
Component {
id: _signVerifyMessageDialog
SignVerifyMessageDialog {
onClosed: destroy()
}
}
Component {
id: swapDialog
SwapDialog {

5
electrum/gui/qml/qebitcoin.py

@ -12,6 +12,7 @@ from electrum.util import get_asyncio_loop
from electrum.transaction import tx_from_any
from electrum.mnemonic import Mnemonic, is_any_2fa_seed_type
from electrum.old_mnemonic import wordlist as old_wordlist
from electrum.bitcoin import is_address
class QEBitcoin(QObject):
@ -150,6 +151,10 @@ class QEBitcoin(QObject):
except Exception:
return False
@pyqtSlot(str, result=bool)
def isAddress(self, addr: str):
return is_address(addr)
@pyqtSlot(str, result=bool)
def isAddressList(self, csv: str):
return keystore.is_address_list(csv)

17
electrum/gui/qml/qedaemon.py

@ -1,3 +1,4 @@
import base64
import os
import threading
from typing import TYPE_CHECKING
@ -10,6 +11,8 @@ from electrum.logging import get_logger
from electrum.util import WalletFileException, standardize_path
from electrum.plugin import run_hook
from electrum.lnchannel import ChannelState
from electrum.bitcoin import is_address
from electrum.ecc import verify_message_with_address
from .auth import AuthMixin, auth_protect
from .qefx import QEFX
@ -354,3 +357,17 @@ class QEDaemon(AuthMixin, QObject):
@pyqtSlot()
def startNetwork(self):
self.daemon.start_network()
@pyqtSlot(str, str, str, result=bool)
def verifyMessage(self, address, message, signature):
address = address.strip()
message = message.strip().encode('utf-8')
if not is_address(address):
return False
try:
# This can throw on invalid base64
sig = base64.b64decode(str(signature.strip()))
verified = verify_message_with_address(address, sig, message)
except Exception as e:
verified = False
return verified

14
electrum/gui/qml/qewallet.py

@ -1,4 +1,5 @@
import asyncio
import base64
import queue
import threading
import time
@ -433,6 +434,10 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
return self.wallet.m == 1
return True
@pyqtProperty(bool, notify=dataChanged)
def canSignMessage(self):
return not isinstance(self.wallet, Multisig_Wallet) and not self.wallet.is_watching_only()
@pyqtProperty(QEAmount, notify=balanceChanged)
def frozenBalance(self):
c, u, x = self.wallet.get_frozen_balance()
@ -762,3 +767,12 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
'f_lightning': int(f_lightning),
'total': sum([int(x) for x in list(balances)])
}
@pyqtSlot(str, result=bool)
def isAddressMine(self, addr):
return self.wallet.is_mine(addr)
@pyqtSlot(str, str, result=str)
def signMessage(self, address, message):
sig = self.wallet.sign_message(address, message, self.password)
return base64.b64encode(sig).decode('ascii')

Loading…
Cancel
Save