Fixes qrscanner to 1.9 API.
[electrum-nvc.git] / plugins / qrscanner.py
1 from electrum.util import print_error
2 from urlparse import urlparse, parse_qs
3 from PyQt4.QtGui import QPushButton, QMessageBox, QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel
4 from PyQt4.QtCore import Qt
5
6 from electrum.i18n import _
7 import re
8 from electrum import Transaction
9 from electrum.bitcoin import MIN_RELAY_TX_FEE, is_valid
10 from electrum_gui.qt.qrcodewidget import QRCodeWidget
11 from electrum import bmp
12 import json
13
14 try:
15     import zbar
16 except ImportError:
17     zbar = None
18
19 from electrum import BasePlugin
20 class Plugin(BasePlugin):
21
22     def fullname(self): return 'QR scans'
23
24     def description(self): return "QR Scans.\nInstall the zbar package (http://zbar.sourceforge.net/download.html) to enable this plugin"
25
26     def __init__(self, gui, name):
27         BasePlugin.__init__(self, gui, name)
28         self._is_available = self._init()
29
30     def _init(self):
31         if not zbar:
32             return False
33         try:
34             proc = zbar.Processor()
35             proc.init()
36         except zbar.SystemError:
37             # Cannot open video device
38             return False
39
40         return True
41
42     def load_wallet(self, wallet):
43         b = QPushButton(_("Scan QR code"))
44         b.clicked.connect(self.fill_from_qr)
45         self.send_tab_grid.addWidget(b, 1, 5)
46         b2 = QPushButton(_("Scan TxQR"))
47         b2.clicked.connect(self.read_raw_qr)
48         
49         if not wallet.seed:
50             b3 = QPushButton(_("Show unsigned TxQR"))
51             b3.clicked.connect(self.show_raw_qr)
52             self.send_tab_grid.addWidget(b3, 7, 1)
53             self.send_tab_grid.addWidget(b2, 7, 2)
54         else:
55             self.send_tab_grid.addWidget(b2, 7, 1)
56
57     def is_available(self):
58         return self._is_available
59
60     def create_send_tab(self, grid):
61         self.send_tab_grid = grid
62
63     def scan_qr(self):
64         proc = zbar.Processor()
65         proc.init()
66         proc.visible = True
67
68         while True:
69             try:
70                 proc.process_one()
71             except:
72                 # User closed the preview window
73                 return {}
74
75             for r in proc.results:
76                 if str(r.type) != 'QRCODE':
77                     continue
78                 return r.data
79         
80     def show_raw_qr(self):
81         r = unicode( self.gui.main_window.payto_e.text() )
82         r = r.strip()
83
84         # label or alias, with address in brackets
85         m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
86         to_address = m.group(2) if m else r
87
88         if not is_valid(to_address):
89             QMessageBox.warning(self.gui.main_window, _('Error'), _('Invalid Bitcoin Address') + ':\n' + to_address, _('OK'))
90             return
91
92         try:
93             amount = self.gui.main_window.read_amount(unicode( self.gui.main_window.amount_e.text()))
94         except:
95             QMessageBox.warning(self.gui.main_window, _('Error'), _('Invalid Amount'), _('OK'))
96             return
97         try:
98             fee = self.gui.main_window.read_amount(unicode( self.gui.main_window.fee_e.text()))
99         except:
100             QMessageBox.warning(self.gui.main_window, _('Error'), _('Invalid Fee'), _('OK'))
101             return
102
103         try:
104             tx = self.gui.main_window.wallet.mktx( [(to_address, amount)], None, fee)
105         except BaseException, e:
106             self.gui.main_window.show_message(str(e))
107             return
108
109         if tx.requires_fee(self.gui.main_window.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
110             QMessageBox.warning(self.gui.main_window, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
111             return
112
113         try:
114             out = {
115             "hex" : tx.hash(),
116             "complete" : "false"
117             }
118     
119             input_info = []
120
121         except BaseException, e:
122             self.gui.main_window.show_message(str(e))
123
124         try:
125             json_text = json.dumps(tx.as_dict()).replace(' ', '')
126             self.show_tx_qrcode(json_text, 'Unsigned Transaction')
127         except BaseException, e:
128             self.gui.main_window.show_message(str(e))
129
130     def show_tx_qrcode(self, data, title):
131         if not data: return
132         d = QDialog(self.gui.main_window)
133         d.setModal(1)
134         d.setWindowTitle(title)
135         d.setMinimumSize(250, 525)
136         vbox = QVBoxLayout()
137         qrw = QRCodeWidget(data)
138         vbox.addWidget(qrw, 0)
139         hbox = QHBoxLayout()
140         hbox.addStretch(1)
141
142         def print_qr(self):
143             filename = "qrcode.bmp"
144             electrum_gui.bmp.save_qrcode(qrw.qr, filename)
145             QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
146
147         b = QPushButton(_("Save"))
148         hbox.addWidget(b)
149         b.clicked.connect(print_qr)
150
151         b = QPushButton(_("Close"))
152         hbox.addWidget(b)
153         b.clicked.connect(d.accept)
154         b.setDefault(True)
155
156         vbox.addLayout(hbox, 1)
157         d.setLayout(vbox)
158         d.exec_()
159
160     def read_raw_qr(self):
161         qrcode = self.scan_qr()
162         if qrcode:
163             tx_dict = self.gui.main_window.tx_dict_from_text(qrcode)
164             if tx_dict:
165                 self.create_transaction_details_window(tx_dict)
166
167
168     def create_transaction_details_window(self, tx_dict):
169         tx = Transaction(tx_dict["hex"])
170             
171         dialog = QDialog(self.gui.main_window)
172         dialog.setMinimumWidth(500)
173         dialog.setWindowTitle(_('Process Offline transaction'))
174         dialog.setModal(1)
175
176         l = QGridLayout()
177         dialog.setLayout(l)
178
179         l.addWidget(QLabel(_("Transaction status:")), 3,0)
180         l.addWidget(QLabel(_("Actions")), 4,0)
181
182         if tx_dict["complete"] == False:
183             l.addWidget(QLabel(_("Unsigned")), 3,1)
184             if self.gui.main_window.wallet.seed :
185                 b = QPushButton("Sign transaction")
186                 input_info = json.loads(tx_dict["input_info"])
187                 b.clicked.connect(lambda: self.sign_raw_transaction(tx, input_info, dialog))
188                 l.addWidget(b, 4, 1)
189             else:
190                 l.addWidget(QLabel(_("Wallet is de-seeded, can't sign.")), 4,1)
191         else:
192             l.addWidget(QLabel(_("Signed")), 3,1)
193             b = QPushButton("Broadcast transaction")
194             b.clicked.connect(lambda: self.gui.main_window.send_raw_transaction(tx, dialog))
195             l.addWidget(b,4,1)
196     
197         l.addWidget( self.gui.main_window.generate_transaction_information_widget(tx), 0,0,2,3)
198         closeButton = QPushButton(_("Close"))
199         closeButton.clicked.connect(lambda: dialog.done(0))
200         l.addWidget(closeButton, 4,2)
201
202         dialog.exec_()
203
204     def do_protect(self, func, args):
205         if self.gui.main_window.wallet.use_encryption:
206             password = self.gui.main_window.password_dialog()
207             if not password:
208                 return
209         else:
210             password = None
211             
212         if args != (False,):
213             args = (self,) + args + (password,)
214         else:
215             args = (self,password)
216         apply( func, args)
217
218     def protected(func):
219         return lambda s, *args: s.do_protect(func, args)
220
221     @protected
222     def sign_raw_transaction(self, tx, input_info, dialog ="", password = ""):
223         try:
224             self.gui.main_window.wallet.signrawtransaction(tx, input_info, [], password)
225             txtext = json.dumps(tx.as_dict()).replace(' ', '')
226             self.show_tx_qrcode(txtext, 'Signed Transaction')
227         except BaseException, e:
228             self.gui.main_window.show_message(str(e))
229
230
231     def fill_from_qr(self):
232         qrcode = parse_uri(self.scan_qr())
233         if not qrcode:
234             return
235
236         if 'address' in qrcode:
237             self.gui.main_window.payto_e.setText(qrcode['address'])
238         if 'amount' in qrcode:
239             self.gui.main_window.amount_e.setText(str(qrcode['amount']))
240         if 'label' in qrcode:
241             self.gui.main_window.message_e.setText(qrcode['label'])
242         if 'message' in qrcode:
243             self.gui.main_window.message_e.setText("%s (%s)" % (self.gui.main_window.message_e.text(), qrcode['message']))
244                 
245
246
247
248 def parse_uri(uri):
249     if ':' not in uri:
250         # It's just an address (not BIP21)
251         return {'address': uri}
252
253     if '//' not in uri:
254         # Workaround for urlparse, it don't handle bitcoin: URI properly
255         uri = uri.replace(':', '://')
256         
257     uri = urlparse(uri)
258     result = {'address': uri.netloc} 
259     
260     if uri.path.startswith('?'):
261         params = parse_qs(uri.path[1:])
262     else:
263         params = parse_qs(uri.path)    
264
265     for k,v in params.items():
266         if k in ('amount', 'label', 'message'):
267             result[k] = v[0]
268         
269     return result    
270
271
272
273
274
275 if __name__ == '__main__':
276     # Run some tests
277     
278     assert(parse_uri('1Marek48fwU7mugmSe186do2QpUkBnpzSN') ==
279            {'address': '1Marek48fwU7mugmSe186do2QpUkBnpzSN'})
280
281     assert(parse_uri('bitcoin://1Marek48fwU7mugmSe186do2QpUkBnpzSN') ==
282            {'address': '1Marek48fwU7mugmSe186do2QpUkBnpzSN'})
283     
284     assert(parse_uri('bitcoin:1Marek48fwU7mugmSe186do2QpUkBnpzSN') ==
285            {'address': '1Marek48fwU7mugmSe186do2QpUkBnpzSN'})
286     
287     assert(parse_uri('bitcoin:1Marek48fwU7mugmSe186do2QpUkBnpzSN?amount=10') ==
288            {'amount': '10', 'address': '1Marek48fwU7mugmSe186do2QpUkBnpzSN'})
289     
290     assert(parse_uri('bitcoin:1Marek48fwU7mugmSe186do2QpUkBnpzSN?amount=10&label=slush&message=Small%20tip%20to%20slush') ==
291            {'amount': '10', 'label': 'slush', 'message': 'Small tip to slush', 'address': '1Marek48fwU7mugmSe186do2QpUkBnpzSN'})
292     
293