3 # Let's do some dep checking and handle missing ones gracefully
5 from PyQt4.QtCore import *
6 from PyQt4.QtGui import *
7 import PyQt4.QtCore as QtCore
10 print "You need to have PyQT installed to run Electrum in graphical mode."
11 print "If you have pip installed try 'sudo pip install pyqt' if you are on Debian/Ubuntu try 'sudo apt-get install python-qt4'."
17 from decimal import Decimal as D
18 from util import get_resource_path as rsrc
29 import receiving_widget
34 from version import ELECTRUM_VERSION as electrum_version
35 from wallet import format_satoshis
39 bitcoin = lambda v: v * 100000000
41 def IconButton(filename, parent=None):
42 pixmap = QPixmap(filename)
44 return QPushButton(icon, "", parent)
49 self.emit(SIGNAL('timersignal'))
52 def resize_line_edit_width(line_edit, text_input):
53 metrics = QFontMetrics(qApp.font())
54 # Create an extra character to add some space on the end
56 line_edit.setMinimumWidth(metrics.width(text_input))
58 def load_theme_name(theme_path):
60 with open(os.path.join(theme_path, "name.cfg")) as name_cfg_file:
61 return name_cfg_file.read().rstrip("\n").strip()
66 def theme_dirs_from_prefix(prefix):
67 if not os.path.exists(prefix):
70 for potential_theme in os.listdir(prefix):
71 theme_full_path = os.path.join(prefix, potential_theme)
72 theme_css = os.path.join(theme_full_path, "style.css")
73 if not os.path.exists(theme_css):
75 theme_name = load_theme_name(theme_full_path)
76 if theme_name is None:
78 theme_paths[theme_name] = prefix, potential_theme
81 def load_theme_paths():
83 prefixes = (util.local_data_dir(), util.appdata_dir())
84 for prefix in prefixes:
85 theme_paths.update(theme_dirs_from_prefix(prefix))
89 class ElectrumGui(QObject):
91 def __init__(self, wallet, config):
92 super(QObject, self).__init__()
96 self.check_qt_version()
97 self.app = QApplication(sys.argv)
100 def check_qt_version(self):
101 qtVersion = qVersion()
102 if not(int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
103 app = QApplication(sys.argv)
104 QMessageBox.warning(None,"Could not start Lite GUI.", "Electrum was unable to load the 'Lite GUI' because it needs Qt version >= 4.7.\nChanging your config to use the 'Classic' GUI")
105 self.config.set_key('gui','classic',True)
110 actuator = MiniActuator(self.wallet)
111 # Should probably not modify the current path but instead
112 # change the behaviour of rsrc(...)
113 old_path = QDir.currentPath()
114 actuator.load_theme()
116 self.mini = MiniWindow(actuator, self.expand, self.config)
117 driver = MiniDriver(self.wallet, self.mini)
119 # Reset path back to original value now that loading the GUI
121 QDir.setCurrent(old_path)
128 self.expert = gui_qt.ElectrumWindow(self.wallet, self.config)
129 self.expert.app = self.app
130 self.expert.connect_slots(timer)
131 self.expert.update_wallet()
135 """Hide the lite mode window and show pro-mode."""
139 def set_url(self, url):
140 payto, amount, label, message, signature, identity, url = \
141 self.wallet.parse_url(url, self.show_message, self.show_question)
142 self.mini.set_payment_fields(payto, amount)
144 def show_message(self, message):
145 QMessageBox.information(self.mini, _("Message"), message, _("OK"))
147 def show_question(self, message):
148 choice = QMessageBox.question(self.mini, _("Message"), message,
149 QMessageBox.Yes|QMessageBox.No,
151 return choice == QMessageBox.Yes
153 def restore_or_create(self):
154 qt_gui_object = gui_qt.ElectrumGui(self.wallet, self.app)
155 return qt_gui_object.restore_or_create()
157 class TransactionWindow(QDialog):
160 label = unicode(self.label_edit.text())
161 self.parent.wallet.labels[self.tx_id] = label
163 super(TransactionWindow, self).accept()
165 def __init__(self, transaction_id, parent):
166 super(TransactionWindow, self).__init__()
168 self.tx_id = str(transaction_id)
173 self.setWindowTitle("Transaction successfully sent")
175 self.layout = QGridLayout(self)
176 self.layout.addWidget(QLabel("Your transaction has been sent.\nPlease enter a label for this transaction for future reference."))
178 self.label_edit = QLineEdit()
179 self.label_edit.setPlaceholderText(_("Transaction label"))
180 self.label_edit.setObjectName("label_input")
181 self.label_edit.setAttribute(Qt.WA_MacShowFocusRect, 0)
182 self.label_edit.setFocusPolicy(Qt.ClickFocus)
183 self.layout.addWidget(self.label_edit)
185 self.save_button = QPushButton(_("Save"))
186 self.layout.addWidget(self.save_button)
187 self.save_button.clicked.connect(self.set_label)
191 class MiniWindow(QDialog):
193 def __init__(self, actuator, expand_callback, config):
194 super(MiniWindow, self).__init__()
195 tx = "e08115d0f7819aee65b9d24f81ef9d46eb62bb67ddef5318156cbc3ceb7b703e"
197 self.actuator = actuator
199 self.btc_balance = None
200 self.quote_currencies = ["EUR", "USD", "GBP"]
201 self.actuator.set_configured_currency(self.set_quote_currency)
202 self.exchanger = exchange_rate.Exchanger(self)
203 # Needed because price discovery is done in a different thread
204 # which needs to be sent back to this main one to update the GUI
205 self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
207 self.balance_label = BalanceLabel(self.change_quote_currency)
208 self.balance_label.setObjectName("balance_label")
211 # Bitcoin address code
212 self.address_input = QLineEdit()
213 self.address_input.setPlaceholderText(_("Enter a Bitcoin address or contact"))
214 self.address_input.setObjectName("address_input")
216 self.address_input.setFocusPolicy(Qt.ClickFocus)
218 self.address_input.textChanged.connect(self.address_field_changed)
219 resize_line_edit_width(self.address_input,
220 "1BtaFUr3qVvAmwrsuDuu5zk6e4s2rxd2Gy")
222 self.address_completions = QStringListModel()
223 address_completer = QCompleter(self.address_input)
224 address_completer.setCaseSensitivity(False)
225 address_completer.setModel(self.address_completions)
226 self.address_input.setCompleter(address_completer)
228 address_layout = QHBoxLayout()
229 address_layout.addWidget(self.address_input)
231 self.amount_input = QLineEdit()
232 self.amount_input.setPlaceholderText(_("... and amount"))
233 self.amount_input.setObjectName("amount_input")
235 self.amount_input.setFocusPolicy(Qt.ClickFocus)
236 # This is changed according to the user's displayed balance
237 self.amount_validator = QDoubleValidator(self.amount_input)
238 self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
239 self.amount_validator.setDecimals(8)
240 self.amount_input.setValidator(self.amount_validator)
242 # This removes the very ugly OSX highlighting, please leave this in :D
243 self.address_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
244 self.amount_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
245 self.amount_input.textChanged.connect(self.amount_input_changed)
247 self.send_button = QPushButton(_("&Send"))
248 self.send_button.setObjectName("send_button")
249 self.send_button.setDisabled(True);
250 self.send_button.clicked.connect(self.send)
252 # Creating the receive button
253 self.receive_button = QPushButton(_("&Receive"))
254 self.receive_button.setObjectName("receive_button")
255 self.receive_button.setDefault(True)
257 main_layout = QGridLayout(self)
259 main_layout.addWidget(self.balance_label, 0, 0)
260 main_layout.addWidget(self.receive_button, 0, 1)
262 main_layout.addWidget(self.address_input, 1, 0)
264 main_layout.addWidget(self.amount_input, 2, 0)
265 main_layout.addWidget(self.send_button, 2, 1)
267 self.history_list = history_widget.HistoryWidget()
268 self.history_list.setObjectName("history")
269 self.history_list.hide()
270 self.history_list.setAlternatingRowColors(True)
272 main_layout.addWidget(self.history_list, 3, 0, 1, 2)
275 self.receiving = receiving_widget.ReceivingWidget(self)
276 self.receiving.setObjectName("receiving")
278 # Add to the right side
279 self.receiving_box = QGroupBox(_("Select a receiving address"))
280 extra_layout = QGridLayout()
282 # Checkbox to filter used addresses
283 hide_used = QCheckBox(_('Hide used addresses'))
284 hide_used.setChecked(True)
285 hide_used.stateChanged.connect(self.receiving.toggle_used)
287 # Events for receiving addresses
288 self.receiving.clicked.connect(self.receiving.copy_address)
289 self.receiving.itemDoubleClicked.connect(self.receiving.edit_label)
290 self.receiving.itemChanged.connect(self.receiving.update_label)
293 extra_layout.addWidget( QLabel(_('Selecting an address will copy it to the clipboard.\nDouble clicking the label will allow you to edit it.') ),0,0)
295 extra_layout.addWidget(self.receiving, 1,0)
296 extra_layout.addWidget(hide_used, 2,0)
297 extra_layout.setColumnMinimumWidth(0,200)
299 self.receiving_box.setLayout(extra_layout)
300 main_layout.addWidget(self.receiving_box,0,3,-1,3)
301 self.receiving_box.hide()
303 self.receive_button.clicked.connect(self.toggle_receiving_layout)
305 # Creating the menu bar
307 electrum_menu = menubar.addMenu(_("&Bitcoin"))
309 electrum_menu.addSeparator()
311 quit_option = electrum_menu.addAction(_("&Quit"))
312 quit_option.triggered.connect(self.close)
314 view_menu = menubar.addMenu(_("&View"))
315 extra_menu = menubar.addMenu(_("&Extra"))
317 backup_wallet = extra_menu.addAction( _("&Create wallet backup"))
318 backup_wallet.triggered.connect(self.backup_wallet)
320 export_csv = extra_menu.addAction( _("&Export transactions to CSV") )
321 export_csv.triggered.connect(self.actuator.csv_transaction)
323 master_key = extra_menu.addAction( _("Copy master public key to clipboard") )
324 master_key.triggered.connect(self.actuator.copy_master_public_key)
326 expert_gui = view_menu.addAction(_("&Classic GUI"))
327 expert_gui.triggered.connect(expand_callback)
328 themes_menu = view_menu.addMenu(_("&Themes"))
329 selected_theme = self.actuator.selected_theme()
330 theme_group = QActionGroup(self)
331 for theme_name in self.actuator.theme_names():
332 theme_action = themes_menu.addAction(theme_name)
333 theme_action.setCheckable(True)
334 if selected_theme == theme_name:
335 theme_action.setChecked(True)
336 class SelectThemeFunctor:
337 def __init__(self, theme_name, toggle_theme):
338 self.theme_name = theme_name
339 self.toggle_theme = toggle_theme
340 def __call__(self, checked):
342 self.toggle_theme(self.theme_name)
343 delegate = SelectThemeFunctor(theme_name, self.toggle_theme)
344 theme_action.toggled.connect(delegate)
345 theme_group.addAction(theme_action)
346 view_menu.addSeparator()
347 show_history = view_menu.addAction(_("Show History"))
348 show_history.setCheckable(True)
349 show_history.toggled.connect(self.show_history)
351 help_menu = menubar.addMenu(_("&Help"))
352 the_website = help_menu.addAction(_("&Website"))
353 the_website.triggered.connect(self.the_website)
354 help_menu.addSeparator()
355 report_bug = help_menu.addAction(_("&Report Bug"))
356 report_bug.triggered.connect(self.show_report_bug)
357 show_about = help_menu.addAction(_("&About"))
358 show_about.triggered.connect(self.show_about)
359 main_layout.setMenuBar(menubar)
360 self.main_layout = main_layout
362 quit_shortcut = QShortcut(QKeySequence("Ctrl+Q"), self)
363 quit_shortcut.activated.connect(self.close)
364 close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
365 close_shortcut.activated.connect(self.close)
367 g = self.config.get("winpos-lite",[4, 25, 351, 149])
368 self.setGeometry(g[0], g[1], g[2], g[3])
370 show_hist = self.config.get("gui_show_history",False)
371 show_history.setChecked(show_hist)
372 self.show_history(show_hist)
374 self.setWindowIcon(QIcon(":electrum.png"))
375 self.setWindowTitle("Electrum")
376 self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
377 self.layout().setSizeConstraint(QLayout.SetFixedSize)
378 self.setObjectName("main_window")
381 def toggle_receiving_layout(self):
382 if self.receiving_box.isVisible():
383 self.receiving_box.hide()
384 self.receive_button.setProperty("isActive", False)
386 qApp.style().unpolish(self.receive_button)
387 qApp.style().polish(self.receive_button)
389 self.receiving_box.show()
390 self.receive_button.setProperty("isActive", 'true')
392 qApp.style().unpolish(self.receive_button)
393 qApp.style().polish(self.receive_button)
395 def toggle_theme(self, theme_name):
396 old_path = QDir.currentPath()
397 self.actuator.change_theme(theme_name)
398 # Recompute style globally
399 qApp.style().unpolish(self)
400 qApp.style().polish(self)
401 QDir.setCurrent(old_path)
403 def closeEvent(self, event):
405 self.config.set_key("winpos-lite", [g.left(),g.top(),g.width(),g.height()],True)
406 self.config.set_key("gui_show_history", self.history_list.isVisible(),True)
408 super(MiniWindow, self).closeEvent(event)
411 def set_payment_fields(self, dest_address, amount):
412 self.address_input.setText(dest_address)
413 self.address_field_changed(dest_address)
414 self.amount_input.setText(amount)
419 def deactivate(self):
422 def set_quote_currency(self, currency):
423 """Set and display the fiat currency country."""
424 assert currency in self.quote_currencies
425 self.quote_currencies.remove(currency)
426 self.quote_currencies.insert(0, currency)
427 self.refresh_balance()
429 def change_quote_currency(self):
430 self.quote_currencies = \
431 self.quote_currencies[1:] + self.quote_currencies[0:1]
432 self.actuator.set_config_currency(self.quote_currencies[0])
433 self.refresh_balance()
435 def refresh_balance(self):
436 if self.btc_balance is None:
437 # Price has been discovered before wallet has been loaded
438 # and server connect... so bail.
440 self.set_balances(self.btc_balance)
441 self.amount_input_changed(self.amount_input.text())
443 def set_balances(self, btc_balance):
444 """Set the bitcoin balance and update the amount label accordingly."""
445 self.btc_balance = btc_balance
446 quote_text = self.create_quote_text(btc_balance)
448 quote_text = "(%s)" % quote_text
449 btc_balance = "%.2f" % (btc_balance / bitcoin(1))
450 self.balance_label.set_balance_text(btc_balance, quote_text)
451 self.setWindowTitle("Electrum %s - %s BTC" % (electrum_version, btc_balance))
453 def amount_input_changed(self, amount_text):
454 """Update the number of bitcoins displayed."""
455 self.check_button_status()
458 amount = D(str(amount_text))
459 except decimal.InvalidOperation:
460 self.balance_label.show_balance()
462 quote_text = self.create_quote_text(amount * bitcoin(1))
464 self.balance_label.set_amount_text(quote_text)
465 self.balance_label.show_amount()
467 self.balance_label.show_balance()
469 def create_quote_text(self, btc_balance):
470 """Return a string copy of the amount fiat currency the
471 user has in bitcoins."""
472 quote_currency = self.quote_currencies[0]
473 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
474 if quote_balance is None:
477 quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
482 if self.actuator.send(self.address_input.text(),
483 self.amount_input.text(), self):
484 self.address_input.setText("")
485 self.amount_input.setText("")
487 def check_button_status(self):
488 """Check that the bitcoin address is valid and that something
489 is entered in the amount before making the send button clickable."""
491 value = D(str(self.amount_input.text())) * 10**8
492 except decimal.InvalidOperation:
494 # self.address_input.property(...) returns a qVariant, not a bool.
495 # The == is needed to properly invoke a comparison.
496 if (self.address_input.property("isValid") == True and
497 value is not None and 0 < value <= self.btc_balance):
498 self.send_button.setDisabled(False)
500 self.send_button.setDisabled(True)
502 def address_field_changed(self, address):
503 # label or alias, with address in brackets
504 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
507 address = match2.group(2)
508 self.address_input.setText(address)
510 if self.actuator.is_valid(address):
511 self.check_button_status()
512 self.address_input.setProperty("isValid", True)
513 self.recompute_style(self.address_input)
515 self.send_button.setDisabled(True)
516 self.address_input.setProperty("isValid", False)
517 self.recompute_style(self.address_input)
519 if len(address) == 0:
520 self.address_input.setProperty("isValid", None)
521 self.recompute_style(self.address_input)
523 def recompute_style(self, element):
524 self.style().unpolish(element)
525 self.style().polish(element)
527 def copy_address(self):
528 receive_popup = ReceivePopup(self.receive_button)
529 self.actuator.copy_address(receive_popup)
531 def update_completions(self, completions):
532 self.address_completions.setStringList(completions)
535 def update_history(self, tx_history):
536 from util import format_satoshis, age
538 self.history_list.empty()
540 for item in tx_history[-10:]:
541 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
542 label = self.actuator.wallet.get_label(tx_hash)[0]
543 #amount = D(value) / 10**8
544 v_str = format_satoshis(value, True)
545 self.history_list.append(label, v_str, age(timestamp))
548 self.actuator.acceptbit(self.quote_currencies[0])
550 def the_website(self):
551 webbrowser.open("http://electrum-desktop.com")
553 def show_about(self):
554 QMessageBox.about(self, "Electrum",
555 _("Electrum's focus is speed, with low resource usage and simplifying Bitcoin. You do not need to perform regular backups, because your wallet can be recovered from a secret phrase that you can memorize or write on paper. Startup times are instant because it operates in conjuction with high-performance servers that handle the most complicated parts of the Bitcoin system.\n\nSend donations to 1JwTMv4GWaPdf931N6LNPJeZBfZgZJ3zX1"))
557 def show_report_bug(self):
558 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
559 _("Please report any bugs as issues on github: https://github.com/spesmilo/electrum/issues"))
561 def show_history(self, toggle_state):
563 self.main_layout.setRowMinimumHeight(3,200)
564 self.history_list.show()
566 self.main_layout.setRowMinimumHeight(3,0)
567 self.history_list.hide()
569 def backup_wallet(self):
571 folderName = QFileDialog.getExistingDirectory(QWidget(), 'Select folder to save a copy of your wallet to', os.path.expanduser('~/'))
573 sourceFile = util.user_dir() + '/electrum.dat'
574 shutil.copy2(sourceFile, str(folderName))
575 QMessageBox.information(None,"Wallet backup created", "A copy of your wallet file was created in '%s'" % str(folderName))
576 except (IOError, os.error), reason:
577 QMessageBox.critical(None,"Unable to create backup", "Electrum was unable copy your wallet file to the specified location.\n" + str(reason))
582 class BalanceLabel(QLabel):
588 def __init__(self, change_quote_currency, parent=None):
589 super(QLabel, self).__init__(_("Connecting..."), parent)
590 self.change_quote_currency = change_quote_currency
591 self.state = self.SHOW_CONNECTING
592 self.balance_text = ""
593 self.amount_text = ""
595 def mousePressEvent(self, event):
596 """Change the fiat currency selection if window background is clicked."""
597 if self.state != self.SHOW_CONNECTING:
598 self.change_quote_currency()
600 def set_balance_text(self, btc_balance, quote_text):
601 """Set the amount of bitcoins in the gui."""
602 if self.state == self.SHOW_CONNECTING:
603 self.state = self.SHOW_BALANCE
604 self.balance_text = "<span style='font-size: 18pt'>%s</span> <span style='font-size: 10pt'>BTC</span> <span style='font-size: 10pt'>%s</span>" % (btc_balance, quote_text)
605 if self.state == self.SHOW_BALANCE:
606 self.setText(self.balance_text)
608 def set_amount_text(self, quote_text):
609 self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
610 if self.state == self.SHOW_AMOUNT:
611 self.setText(self.amount_text)
613 def show_balance(self):
614 if self.state == self.SHOW_AMOUNT:
615 self.state = self.SHOW_BALANCE
616 self.setText(self.balance_text)
618 def show_amount(self):
619 if self.state == self.SHOW_BALANCE:
620 self.state = self.SHOW_AMOUNT
621 self.setText(self.amount_text)
623 def ok_cancel_buttons(dialog):
624 row_layout = QHBoxLayout()
625 row_layout.addStretch(1)
626 ok_button = QPushButton(_("OK"))
627 row_layout.addWidget(ok_button)
628 ok_button.clicked.connect(dialog.accept)
629 cancel_button = QPushButton(_("Cancel"))
630 row_layout.addWidget(cancel_button)
631 cancel_button.clicked.connect(dialog.reject)
634 class PasswordDialog(QDialog):
636 def __init__(self, parent):
637 super(QDialog, self).__init__(parent)
641 self.password_input = QLineEdit()
642 self.password_input.setEchoMode(QLineEdit.Password)
644 main_layout = QVBoxLayout(self)
645 message = _('Please enter your password')
646 main_layout.addWidget(QLabel(message))
650 grid.addWidget(QLabel(_('Password')), 1, 0)
651 grid.addWidget(self.password_input, 1, 1)
652 main_layout.addLayout(grid)
654 main_layout.addLayout(ok_cancel_buttons(self))
655 self.setLayout(main_layout)
660 return unicode(self.password_input.text())
662 class ReceivePopup(QDialog):
664 def leaveEvent(self, event):
667 def setup(self, address):
668 label = QLabel(_("Copied your Bitcoin address to the clipboard!"))
669 address_display = QLineEdit(address)
670 address_display.setReadOnly(True)
671 resize_line_edit_width(address_display, address)
673 main_layout = QVBoxLayout(self)
674 main_layout.addWidget(label)
675 main_layout.addWidget(address_display)
677 self.setMouseTracking(True)
678 self.setWindowTitle("Electrum - " + _("Receive Bitcoin payment"))
679 self.setWindowFlags(Qt.Window|Qt.FramelessWindowHint|
680 Qt.MSWindowsFixedSizeDialogHint)
681 self.layout().setSizeConstraint(QLayout.SetFixedSize)
682 #self.setFrameStyle(QFrame.WinPanel|QFrame.Raised)
683 #self.setAlignment(Qt.AlignCenter)
686 parent = self.parent()
687 top_left_pos = parent.mapToGlobal(parent.rect().bottomLeft())
688 self.move(top_left_pos)
689 center_mouse_pos = self.mapToGlobal(self.rect().center())
690 QCursor.setPos(center_mouse_pos)
694 """Initialize the definitions relating to themes and
695 sending/recieving bitcoins."""
698 def __init__(self, wallet):
699 """Retrieve the gui theme used in previous session."""
701 self.theme_name = self.wallet.config.get('litegui_theme','Cleanlook')
702 self.themes = load_theme_paths()
704 def load_theme(self):
705 """Load theme retrieved from wallet file."""
707 theme_prefix, theme_path = self.themes[self.theme_name]
709 util.print_error("Theme not found!", self.theme_name)
711 QDir.setCurrent(os.path.join(theme_prefix, theme_path))
712 with open(rsrc("style.css")) as style_file:
713 qApp.setStyleSheet(style_file.read())
715 def theme_names(self):
717 return sorted(self.themes.keys())
719 def selected_theme(self):
721 return self.theme_name
723 def change_theme(self, theme_name):
725 self.theme_name = theme_name
726 self.wallet.config.set_key('litegui_theme',theme_name)
729 def set_configured_currency(self, set_quote_currency):
730 """Set the inital fiat currency conversion country (USD/EUR/GBP) in
731 the GUI to what it was set to in the wallet."""
732 currency = self.wallet.config.get('conversion_currency')
733 # currency can be none when Electrum is used for the first
734 # time and no setting has been created yet.
735 if currency is not None:
736 set_quote_currency(currency)
738 def set_config_currency(self, conversion_currency):
739 """Change the wallet fiat currency country."""
740 self.wallet.config.set_key('conversion_currency',conversion_currency,True)
742 def copy_address(self, receive_popup):
743 """Copy the wallet addresses into the client."""
744 addrs = [addr for addr in self.wallet.all_addresses()
745 if not self.wallet.is_change(addr)]
746 # Select most recent addresses from gap limit
747 addrs = addrs[-self.wallet.gap_limit:]
748 copied_address = random.choice(addrs)
749 qApp.clipboard().setText(copied_address)
750 receive_popup.setup(copied_address)
751 receive_popup.popup()
753 def waiting_dialog(self, f):
758 w.setWindowTitle('Electrum')
759 l = QLabel('Sending transaction, please wait.')
768 w.connect(s, QtCore.SIGNAL('timersignal'), ff)
772 def csv_transaction(self):
774 fileName = QFileDialog.getSaveFileName(QWidget(), 'Select file to export your wallet transactions to', os.path.expanduser('~/'), "*.csv")
776 with open(fileName, "w+") as csvfile:
777 transaction = csv.writer(csvfile)
778 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
779 for item in self.wallet.get_tx_history():
780 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
782 if timestamp is not None:
784 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
785 except [RuntimeError, TypeError, NameError] as reason:
786 time_string = "unknown"
789 time_string = "unknown"
791 time_string = "pending"
793 if value is not None:
794 value_string = format_satoshis(value, True, self.wallet.num_zeros)
799 fee_string = format_satoshis(fee, True, self.wallet.num_zeros)
804 label, is_default_label = self.wallet.get_label(tx_hash)
808 balance_string = format_satoshis(balance, False, self.wallet.num_zeros)
809 transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
810 QMessageBox.information(None,"CSV Export created", "Your CSV export has been succesfully created.")
811 except (IOError, os.error), reason:
812 QMessageBox.critical(None,"Unable to create csv", "Electrum was unable to produce a transaction export.\n" + str(reason))
814 def send(self, address, amount, parent_window):
815 """Send bitcoins to the target address."""
816 dest_address = self.fetch_destination(address)
818 if dest_address is None or not self.wallet.is_valid(dest_address):
819 QMessageBox.warning(parent_window, _('Error'),
820 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
823 convert_amount = lambda amount: \
824 int(D(unicode(amount)) * bitcoin(1))
825 amount = convert_amount(amount)
827 if self.wallet.use_encryption:
828 password_dialog = PasswordDialog(parent_window)
829 password = password_dialog.run()
837 if amount < bitcoin(1) / 10:
839 fee = bitcoin(1) / 1000
842 tx = self.wallet.mktx([(dest_address, amount)], "", password, fee)
843 except BaseException as error:
844 QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
847 h = self.wallet.send_tx(tx)
849 self.waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Sending transaction, please wait..."))
851 status, message = self.wallet.receive_tx(h)
855 dumpf = tempfile.NamedTemporaryFile(delete=False)
858 print "Dumped error tx to", dumpf.name
859 QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
862 TransactionWindow(message, self)
863 # QMessageBox.information(parent_window, '',
864 # _('Your transaction has been sent.') + '\n' + message, _('OK'))
867 def fetch_destination(self, address):
868 recipient = unicode(address).strip()
871 match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
874 # label or alias, with address in brackets
875 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
880 self.wallet.get_alias(recipient, True,
881 self.show_message, self.question)
884 return match2.group(2)
888 def is_valid(self, address):
889 """Check if bitcoin address is valid."""
891 return self.wallet.is_valid(address)
893 def copy_master_public_key(self):
894 master_pubkey = self.wallet.master_public_key
895 qApp.clipboard().setText(master_pubkey)
896 QMessageBox.information(None,"Copy succesful", "Your public master key has been copied to your clipboard.")
899 def acceptbit(self, currency):
900 master_pubkey = self.wallet.master_public_key
901 url = "http://acceptbit.com/mpk/%s/%s" % (master_pubkey, currency)
904 def show_seed_dialog(self):
905 gui_qt.ElectrumWindow.show_seed_dialog(self.wallet)
907 class MiniDriver(QObject):
914 def __init__(self, wallet, window):
915 super(QObject, self).__init__()
920 self.wallet.interface.register_callback('updated',self.update_callback)
921 self.wallet.interface.register_callback('connected', self.update_callback)
922 self.wallet.interface.register_callback('disconnected', self.update_callback)
927 self.connect(self, SIGNAL("updatesignal()"), self.update)
928 self.update_callback()
930 # This is a hack to workaround that Qt does not like changing the
931 # window properties from this other thread before the runloop has
933 def update_callback(self):
934 self.emit(SIGNAL("updatesignal()"))
937 if not self.wallet.interface:
939 elif not self.wallet.interface.is_connected:
941 elif not self.wallet.up_to_date:
946 if self.wallet.up_to_date:
947 self.update_balance()
948 self.update_completions()
949 self.update_history()
951 def initializing(self):
952 if self.state == self.INITIALIZING:
954 self.state = self.INITIALIZING
955 self.window.deactivate()
957 def connecting(self):
958 if self.state == self.CONNECTING:
960 self.state = self.CONNECTING
961 self.window.deactivate()
963 def synchronizing(self):
964 if self.state == self.SYNCHRONIZING:
966 self.state = self.SYNCHRONIZING
967 self.window.deactivate()
970 if self.state == self.READY:
972 self.state = self.READY
973 self.window.activate()
975 def update_balance(self):
976 conf_balance, unconf_balance = self.wallet.get_balance()
977 balance = D(conf_balance + unconf_balance)
978 self.window.set_balances(balance)
980 def update_completions(self):
982 for addr, label in self.wallet.labels.items():
983 if addr in self.wallet.addressbook:
984 completions.append("%s <%s>" % (label, addr))
985 completions = completions + self.wallet.aliases.keys()
986 self.window.update_completions(completions)
988 def update_history(self):
989 tx_history = self.wallet.get_tx_history()
990 self.window.update_history(tx_history)
993 if __name__ == "__main__":
994 app = QApplication(sys.argv)
995 with open(rsrc("style.css")) as style_file:
996 app.setStyleSheet(style_file.read())
998 sys.exit(app.exec_())