qml: add message sign/verify
This commit is contained in:
@@ -279,11 +279,28 @@ Pane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FlatButton {
|
ButtonContainer {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: addressdetails.isFrozen ? qsTr('Unfreeze address') : qsTr('Freeze address')
|
FlatButton {
|
||||||
onClicked: addressdetails.freeze(!addressdetails.isFrozen)
|
Layout.fillWidth: true
|
||||||
icon.source: '../../icons/seal.png'
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Item {
|
|||||||
property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2)
|
property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2)
|
||||||
property color darkerBackground: Qt.darker(Material.background, 1.20)
|
property color darkerBackground: Qt.darker(Material.background, 1.20)
|
||||||
property color lighterBackground: Qt.lighter(Material.background, 1.10)
|
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 notificationBackground: Qt.lighter(Material.background, 1.5)
|
||||||
|
|
||||||
property color colorCredit: "#ff80ff80"
|
property color colorCredit: "#ff80ff80"
|
||||||
@@ -40,6 +41,8 @@ Item {
|
|||||||
property color colorError: '#ffff8080'
|
property color colorError: '#ffff8080'
|
||||||
property color colorProgress: '#ffffff80'
|
property color colorProgress: '#ffffff80'
|
||||||
property color colorDone: '#ff80ff80'
|
property color colorDone: '#ff80ff80'
|
||||||
|
property color colorValidBackground: '#ff008000'
|
||||||
|
property color colorInvalidBackground: '#ff800000'
|
||||||
|
|
||||||
property color colorLightningLocal: "#6060ff"
|
property color colorLightningLocal: "#6060ff"
|
||||||
property color colorLightningLocalReserve: "#0000a0"
|
property color colorLightningLocalReserve: "#0000a0"
|
||||||
|
|||||||
211
electrum/gui/qml/components/SignVerifyMessageDialog.qml
Normal file
211
electrum/gui/qml/components/SignVerifyMessageDialog.qml
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 { }
|
MenuSeparator { }
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
@@ -140,6 +155,10 @@ Item {
|
|||||||
|
|
||||||
function openPage(url) {
|
function openPage(url) {
|
||||||
stack.pushOnRoot(url)
|
stack.pushOnRoot(url)
|
||||||
|
deselect()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselect() {
|
||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
electrum/gui/qml/components/controls/ElTextArea.qml
Normal file
67
electrum/gui/qml/components/controls/ElTextArea.qml
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -387,6 +387,14 @@ ApplicationWindow
|
|||||||
id: _channelOpenProgressDialog
|
id: _channelOpenProgressDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property alias signVerifyMessageDialog: _signVerifyMessageDialog
|
||||||
|
Component {
|
||||||
|
id: _signVerifyMessageDialog
|
||||||
|
SignVerifyMessageDialog {
|
||||||
|
onClosed: destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: swapDialog
|
id: swapDialog
|
||||||
SwapDialog {
|
SwapDialog {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from electrum.util import get_asyncio_loop
|
|||||||
from electrum.transaction import tx_from_any
|
from electrum.transaction import tx_from_any
|
||||||
from electrum.mnemonic import Mnemonic, is_any_2fa_seed_type
|
from electrum.mnemonic import Mnemonic, is_any_2fa_seed_type
|
||||||
from electrum.old_mnemonic import wordlist as old_wordlist
|
from electrum.old_mnemonic import wordlist as old_wordlist
|
||||||
|
from electrum.bitcoin import is_address
|
||||||
|
|
||||||
|
|
||||||
class QEBitcoin(QObject):
|
class QEBitcoin(QObject):
|
||||||
@@ -150,6 +151,10 @@ class QEBitcoin(QObject):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@pyqtSlot(str, result=bool)
|
||||||
|
def isAddress(self, addr: str):
|
||||||
|
return is_address(addr)
|
||||||
|
|
||||||
@pyqtSlot(str, result=bool)
|
@pyqtSlot(str, result=bool)
|
||||||
def isAddressList(self, csv: str):
|
def isAddressList(self, csv: str):
|
||||||
return keystore.is_address_list(csv)
|
return keystore.is_address_list(csv)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import base64
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -10,6 +11,8 @@ from electrum.logging import get_logger
|
|||||||
from electrum.util import WalletFileException, standardize_path
|
from electrum.util import WalletFileException, standardize_path
|
||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
from electrum.lnchannel import ChannelState
|
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 .auth import AuthMixin, auth_protect
|
||||||
from .qefx import QEFX
|
from .qefx import QEFX
|
||||||
@@ -354,3 +357,17 @@ class QEDaemon(AuthMixin, QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def startNetwork(self):
|
def startNetwork(self):
|
||||||
self.daemon.start_network()
|
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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -433,6 +434,10 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
|
|||||||
return self.wallet.m == 1
|
return self.wallet.m == 1
|
||||||
return True
|
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)
|
@pyqtProperty(QEAmount, notify=balanceChanged)
|
||||||
def frozenBalance(self):
|
def frozenBalance(self):
|
||||||
c, u, x = self.wallet.get_frozen_balance()
|
c, u, x = self.wallet.get_frozen_balance()
|
||||||
@@ -762,3 +767,12 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
|
|||||||
'f_lightning': int(f_lightning),
|
'f_lightning': int(f_lightning),
|
||||||
'total': sum([int(x) for x in list(balances)])
|
'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')
|
||||||
|
|||||||
Reference in New Issue
Block a user