Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Sending/Receiving unsigned/signed transactions via QRCode #227

Merged
merged 1 commit into from

2 participants

@tigereye

Hi.

So I've written a patch to the QRCode plugin that allows an unseeded+online electrum client to communicate with a seeded+offline electrum client via QRCodes.

The motivation of these modifications was to further reduce the risk profile of dealing with offline machines by preventing any "infectable" media from contaminating an offline machine. With QRCodes there's no need to copy files and no opportunity for 'autorun' shenanigans.

The idea is identical to the existing "offline transaction" feature, but with QRCodes:
1. The unseeded+online machine shows a QRCode to the seeded+offline machine. The QRCode represents an unsigned transaction in the standard JSON format that's put into files.
2. The seeded+offline machine reads the QRCode, signs the transaction and displays it as a QRCode to the online machine.
3. The unseeded+online machine can then read this QRCode and broadcast the transaction.

The vast majority of my changes were made to the qrscanner.py plugin, although I had to edit 1 line in gui/qrcodewidget.py to get larger QRCodes to display properly.

In order to pull it off successfully, I ended up copying/modifying a few smaller functions from various places around electrum into the qrscanner.py file itself. I did this because the original functions couldn't be called natively due to subtle changes that were required for the QRCode stuff. I realize that this may not earn style points, but my lack of code-reuse may be able to be solved by someone more skilled with python than myself :)

I'd welcome any feedback to the changes, and if people like them (and testing goes well), I would love to see this functionality end up in some future electrum release... even if all of my code is rewritten by someone who takes all the credit.

Speaking of credit, motivation for this plugin can be credited to Tuxavant who refused to release his own scripts that did similar! In response: take my point-and-click solution and shove it in your hookah!

@ecdsa ecdsa merged commit 7ddc29a into spesmilo:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 24, 2013
  1. @tigereye

    Updated the QR Code plugin to enable offline/online wallets to transm…

    tigereye authored
    …it unsigned/signed transactions via QR code.
This page is out of date. Refresh to see the latest.
Showing with 181 additions and 6 deletions.
  1. +7 −3 gui/qrcodewidget.py
  2. +174 −3 plugins/qrscanner.py
View
10 gui/qrcodewidget.py
@@ -10,7 +10,6 @@ class QRCodeWidget(QWidget):
def __init__(self, data = None):
QWidget.__init__(self)
- self.setMinimumSize(210, 210)
self.addr = None
self.qr = None
if data:
@@ -19,13 +18,18 @@ def __init__(self, data = None):
def set_addr(self, addr):
if self.addr != addr:
- self.addr = addr
+ if len(addr) < 128:
+ MinSize = 210
+ else:
+ MinSize = 500
+ self.setMinimumSize(MinSize, MinSize)
+ self.addr = addr
self.qr = None
self.update()
def update_qr(self):
if self.addr and not self.qr:
- for size in [4,5,6]:
+ for size in range(len(pyqrnative.QRUtil.PATTERN_POSITION_TABLE)): # [4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]:
try:
self.qr = pyqrnative.QRCode(size, pyqrnative.QRErrorCorrectLevel.L)
self.qr.addData(self.addr)
View
177 plugins/qrscanner.py
@@ -1,8 +1,15 @@
from electrum.util import print_error
from urlparse import urlparse, parse_qs
-from PyQt4.QtGui import QPushButton
+from PyQt4.QtGui import QPushButton, QMessageBox, QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel
+from PyQt4.QtCore import Qt
from electrum_gui.i18n import _
+import re
+from electrum.bitcoin import MIN_RELAY_TX_FEE, Transaction, is_valid
+from electrum_gui.qrcodewidget import QRCodeWidget
+import electrum_gui.bmp
+import json
+
try:
import zbar
except ImportError:
@@ -34,6 +41,16 @@ def create_send_tab(self, grid):
b = QPushButton(_("Scan QR code"))
b.clicked.connect(self.fill_from_qr)
grid.addWidget(b, 1, 5)
+ b2 = QPushButton(_("Scan TxQR"))
+ b2.clicked.connect(self.read_raw_qr)
+
+ if not self.gui.wallet.seed:
+ b3 = QPushButton(_("Show unsigned TxQR"))
+ b3.clicked.connect(self.show_raw_qr)
+ grid.addWidget(b3, 7, 1)
+ grid.addWidget(b2, 7, 2)
+ else:
+ grid.addWidget(b2, 7, 1)
def scan_qr(self):
@@ -51,11 +68,165 @@ def scan_qr(self):
for r in proc.results:
if str(r.type) != 'QRCODE':
continue
- return parse_uri(r.data)
+ return r.data
+ def show_raw_qr(self):
+ r = unicode( self.gui.payto_e.text() )
+ r = r.strip()
+
+ # label or alias, with address in brackets
+ m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
+ to_address = m.group(2) if m else r
+
+ if not is_valid(to_address):
+ QMessageBox.warning(self.gui, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
+ return
+
+ try:
+ amount = self.gui.read_amount(unicode( self.gui.amount_e.text()))
+ except:
+ QMessageBox.warning(self.gui, _('Error'), _('Invalid Amount'), _('OK'))
+ return
+ try:
+ fee = self.gui.read_amount(unicode( self.gui.fee_e.text()))
+ except:
+ QMessageBox.warning(self.gui, _('Error'), _('Invalid Fee'), _('OK'))
+ return
+
+ try:
+ tx = self.gui.wallet.mktx( [(to_address, amount)], None, fee, account=self.gui.current_account)
+ except BaseException, e:
+ self.gui.show_message(str(e))
+ return
+
+ if tx.requires_fee(self.gui.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
+ QMessageBox.warning(self.gui, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
+ return
+
+ try:
+ out = {
+ "hex" : tx.hash(),
+ "complete" : "false"
+ }
+
+ input_info = []
+
+ except BaseException, e:
+ self.gui.show_message(str(e))
+
+ try:
+ json_text = json.dumps(tx.as_dict()).replace(' ', '')
+ self.show_tx_qrcode(json_text, 'Unsigned Transaction')
+ except BaseException, e:
+ self.gui.show_message(str(e))
+
+ def show_tx_qrcode(self, data, title):
+ if not data: return
+ d = QDialog(self.gui)
+ d.setModal(1)
+ d.setWindowTitle(title)
+ d.setMinimumSize(250, 525)
+ vbox = QVBoxLayout()
+ qrw = QRCodeWidget(data)
+ vbox.addWidget(qrw, 0)
+ hbox = QHBoxLayout()
+ hbox.addStretch(1)
+
+ def print_qr(self):
+ filename = "qrcode.bmp"
+ electrum_gui.bmp.save_qrcode(qrw.qr, filename)
+ QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
+
+ b = QPushButton(_("Save"))
+ hbox.addWidget(b)
+ b.clicked.connect(print_qr)
+
+ b = QPushButton(_("Close"))
+ hbox.addWidget(b)
+ b.clicked.connect(d.accept)
+ b.setDefault(True)
+
+ vbox.addLayout(hbox, 1)
+ d.setLayout(vbox)
+ d.exec_()
+
+ def read_raw_qr(self):
+ qrcode = self.scan_qr()
+ if qrcode:
+ tx_dict = self.gui.tx_dict_from_text(qrcode)
+ if tx_dict:
+ self.create_transaction_details_window(tx_dict)
+
+
+ def create_transaction_details_window(self, tx_dict):
+ tx = Transaction(tx_dict["hex"])
+
+ dialog = QDialog(self.gui)
+ dialog.setMinimumWidth(500)
+ dialog.setWindowTitle(_('Process Offline transaction'))
+ dialog.setModal(1)
+
+ l = QGridLayout()
+ dialog.setLayout(l)
+
+ l.addWidget(QLabel(_("Transaction status:")), 3,0)
+ l.addWidget(QLabel(_("Actions")), 4,0)
+
+ if tx_dict["complete"] == False:
+ l.addWidget(QLabel(_("Unsigned")), 3,1)
+ if self.gui.wallet.seed :
+ b = QPushButton("Sign transaction")
+ input_info = json.loads(tx_dict["input_info"])
+ b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
+ l.addWidget(b, 4, 1)
+ else:
+ l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
+ else:
+ l.addWidget(QLabel(_("Signed")), 3,1)
+ b = QPushButton("Broadcast transaction")
+ b.clicked.connect(lambda: self.gui.send_raw_transaction(tx, dialog))
+ l.addWidget(b,4,1)
+
+ l.addWidget( self.gui.generate_transaction_information_widget(tx), 0,0,2,3)
+ closeButton = QPushButton(_("Close"))
+ closeButton.clicked.connect(lambda: dialog.done(0))
+ l.addWidget(closeButton, 4,2)
+
+ dialog.exec_()
+
+ def do_protect(self, func, args):
+ if self.gui.wallet.use_encryption:
+ password = self.gui.password_dialog()
+ if not password:
+ return
+ else:
+ password = None
+
+ if args != (False,):
+ args = (self,) + args + (password,)
+ else:
+ args = (self,password)
+ apply( func, args)
+
+ def protected(func):
+ return lambda s, *args: s.do_protect(func, args)
+
+ @protected
+ def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
+ try:
+ self.gui.wallet.signrawtransaction(tx, input_info, [], password)
+ txtext = json.dumps(tx.as_dict()).replace(' ', '')
+ self.show_tx_qrcode(txtext, 'Signed Transaction')
+
+ except BaseException, e:
+ self.gui.show_message(str(e))
+
def fill_from_qr(self):
- qrcode = self.scan_qr()
+ qrcode = parse_uri(self.scan_qr())
+ if not qrcode:
+ return
+
if 'address' in qrcode:
self.gui.payto_e.setText(qrcode['address'])
if 'amount' in qrcode:
Something went wrong with that request. Please try again.