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, expert=None):
92 super(QObject, self).__init__()
96 self.check_qt_version()
98 if self.expert != None:
99 self.app = self.expert.app
101 self.app = QApplication(sys.argv)
103 def check_qt_version(self):
104 qtVersion = qVersion()
105 if not(int(qtVersion[0]) >= 4 and int(qtVersion[2]) >= 7):
106 app = QApplication(sys.argv)
107 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")
108 self.config.set_key('gui','classic',True)
113 actuator = MiniActuator(self.wallet)
114 # Should probably not modify the current path but instead
115 # change the behaviour of rsrc(...)
116 old_path = QDir.currentPath()
117 actuator.load_theme()
119 self.mini = MiniWindow(actuator, self.expand, self.config)
120 driver = MiniDriver(self.wallet, self.mini)
122 # Reset path back to original value now that loading the GUI
124 QDir.setCurrent(old_path)
129 if self.expert == None:
132 self.expert = gui_qt.ElectrumWindow(self.wallet, self.config)
133 self.expert.app = self.app
134 self.expert.connect_slots(timer)
135 self.expert.update_wallet()
139 """Hide the lite mode window and show pro-mode."""
143 def set_url(self, url):
144 payto, amount, label, message, signature, identity, url = \
145 self.wallet.parse_url(url, self.show_message, self.show_question)
146 self.mini.set_payment_fields(payto, amount)
148 def show_message(self, message):
149 QMessageBox.information(self.mini, _("Message"), message, _("OK"))
151 def show_question(self, message):
152 choice = QMessageBox.question(self.mini, _("Message"), message,
153 QMessageBox.Yes|QMessageBox.No,
155 return choice == QMessageBox.Yes
157 def restore_or_create(self):
158 qt_gui_object = gui_qt.ElectrumGui(self.wallet, self.app)
159 return qt_gui_object.restore_or_create()
161 class TransactionWindow(QDialog):
164 label = unicode(self.label_edit.text())
165 self.parent.wallet.labels[self.tx_id] = label
167 super(TransactionWindow, self).accept()
169 def __init__(self, transaction_id, parent):
170 super(TransactionWindow, self).__init__()
172 self.tx_id = str(transaction_id)
177 self.setWindowTitle("Transaction successfully sent")
179 self.layout = QGridLayout(self)
180 self.layout.addWidget(QLabel("Your transaction has been sent.\nPlease enter a label for this transaction for future reference."))
182 self.label_edit = QLineEdit()
183 self.label_edit.setPlaceholderText(_("Transaction label"))
184 self.label_edit.setObjectName("label_input")
185 self.label_edit.setAttribute(Qt.WA_MacShowFocusRect, 0)
186 self.label_edit.setFocusPolicy(Qt.ClickFocus)
187 self.layout.addWidget(self.label_edit)
189 self.save_button = QPushButton(_("Save"))
190 self.layout.addWidget(self.save_button)
191 self.save_button.clicked.connect(self.set_label)
195 class MiniWindow(QDialog):
197 def __init__(self, actuator, expand_callback, config):
198 super(MiniWindow, self).__init__()
199 tx = "e08115d0f7819aee65b9d24f81ef9d46eb62bb67ddef5318156cbc3ceb7b703e"
201 self.actuator = actuator
203 self.btc_balance = None
204 self.quote_currencies = ["BRL", "CNY", "EUR", "GBP", "RUB", "USD"]
205 self.actuator.set_configured_currency(self.set_quote_currency)
206 self.exchanger = exchange_rate.Exchanger(self)
207 # Needed because price discovery is done in a different thread
208 # which needs to be sent back to this main one to update the GUI
209 self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
211 self.balance_label = BalanceLabel(self.change_quote_currency)
212 self.balance_label.setObjectName("balance_label")
215 # Bitcoin address code
216 self.address_input = QLineEdit()
217 self.address_input.setPlaceholderText(_("Enter a Bitcoin address or contact"))
218 self.address_input.setObjectName("address_input")
220 self.address_input.setFocusPolicy(Qt.ClickFocus)
222 self.address_input.textChanged.connect(self.address_field_changed)
223 resize_line_edit_width(self.address_input,
224 "1BtaFUr3qVvAmwrsuDuu5zk6e4s2rxd2Gy")
226 self.address_completions = QStringListModel()
227 address_completer = QCompleter(self.address_input)
228 address_completer.setCaseSensitivity(False)
229 address_completer.setModel(self.address_completions)
230 self.address_input.setCompleter(address_completer)
232 address_layout = QHBoxLayout()
233 address_layout.addWidget(self.address_input)
235 self.amount_input = QLineEdit()
236 self.amount_input.setPlaceholderText(_("... and amount"))
237 self.amount_input.setObjectName("amount_input")
239 self.amount_input.setFocusPolicy(Qt.ClickFocus)
240 # This is changed according to the user's displayed balance
241 self.amount_validator = QDoubleValidator(self.amount_input)
242 self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
243 self.amount_validator.setDecimals(8)
244 self.amount_input.setValidator(self.amount_validator)
246 # This removes the very ugly OSX highlighting, please leave this in :D
247 self.address_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
248 self.amount_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
249 self.amount_input.textChanged.connect(self.amount_input_changed)
251 self.send_button = QPushButton(_("&Send"))
252 self.send_button.setObjectName("send_button")
253 self.send_button.setDisabled(True);
254 self.send_button.clicked.connect(self.send)
256 # Creating the receive button
257 self.receive_button = QPushButton(_("&Receive"))
258 self.receive_button.setObjectName("receive_button")
259 self.receive_button.setDefault(True)
261 main_layout = QGridLayout(self)
263 main_layout.addWidget(self.balance_label, 0, 0)
264 main_layout.addWidget(self.receive_button, 0, 1)
266 main_layout.addWidget(self.address_input, 1, 0)
268 main_layout.addWidget(self.amount_input, 2, 0)
269 main_layout.addWidget(self.send_button, 2, 1)
271 self.history_list = history_widget.HistoryWidget()
272 self.history_list.setObjectName("history")
273 self.history_list.hide()
274 self.history_list.setAlternatingRowColors(True)
276 main_layout.addWidget(self.history_list, 3, 0, 1, 2)
279 self.receiving = receiving_widget.ReceivingWidget(self)
280 self.receiving.setObjectName("receiving")
282 # Add to the right side
283 self.receiving_box = QGroupBox(_("Select a receiving address"))
284 extra_layout = QGridLayout()
286 # Checkbox to filter used addresses
287 hide_used = QCheckBox(_('Hide used addresses'))
288 hide_used.setChecked(True)
289 hide_used.stateChanged.connect(self.receiving.toggle_used)
291 # Events for receiving addresses
292 self.receiving.clicked.connect(self.receiving.copy_address)
293 self.receiving.itemDoubleClicked.connect(self.receiving.edit_label)
294 self.receiving.itemChanged.connect(self.receiving.update_label)
297 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)
299 extra_layout.addWidget(self.receiving, 1,0)
300 extra_layout.addWidget(hide_used, 2,0)
301 extra_layout.setColumnMinimumWidth(0,200)
303 self.receiving_box.setLayout(extra_layout)
304 main_layout.addWidget(self.receiving_box,0,3,-1,3)
305 self.receiving_box.hide()
307 self.receive_button.clicked.connect(self.toggle_receiving_layout)
309 # Creating the menu bar
311 electrum_menu = menubar.addMenu(_("&Bitcoin"))
313 electrum_menu.addSeparator()
315 quit_option = electrum_menu.addAction(_("&Quit"))
316 quit_option.triggered.connect(self.close)
318 view_menu = menubar.addMenu(_("&View"))
319 extra_menu = menubar.addMenu(_("&Extra"))
321 backup_wallet = extra_menu.addAction( _("&Create wallet backup"))
322 backup_wallet.triggered.connect(self.backup_wallet)
324 export_csv = extra_menu.addAction( _("&Export transactions to CSV") )
325 export_csv.triggered.connect(self.actuator.csv_transaction)
327 master_key = extra_menu.addAction( _("Copy master public key to clipboard") )
328 master_key.triggered.connect(self.actuator.copy_master_public_key)
330 expert_gui = view_menu.addAction(_("&Classic GUI"))
331 expert_gui.triggered.connect(expand_callback)
332 themes_menu = view_menu.addMenu(_("&Themes"))
333 selected_theme = self.actuator.selected_theme()
334 theme_group = QActionGroup(self)
335 for theme_name in self.actuator.theme_names():
336 theme_action = themes_menu.addAction(theme_name)
337 theme_action.setCheckable(True)
338 if selected_theme == theme_name:
339 theme_action.setChecked(True)
340 class SelectThemeFunctor:
341 def __init__(self, theme_name, toggle_theme):
342 self.theme_name = theme_name
343 self.toggle_theme = toggle_theme
344 def __call__(self, checked):
346 self.toggle_theme(self.theme_name)
347 delegate = SelectThemeFunctor(theme_name, self.toggle_theme)
348 theme_action.toggled.connect(delegate)
349 theme_group.addAction(theme_action)
350 view_menu.addSeparator()
351 show_history = view_menu.addAction(_("Show History"))
352 show_history.setCheckable(True)
353 show_history.toggled.connect(self.show_history)
355 help_menu = menubar.addMenu(_("&Help"))
356 the_website = help_menu.addAction(_("&Website"))
357 the_website.triggered.connect(self.the_website)
358 help_menu.addSeparator()
359 report_bug = help_menu.addAction(_("&Report Bug"))
360 report_bug.triggered.connect(self.show_report_bug)
361 show_about = help_menu.addAction(_("&About"))
362 show_about.triggered.connect(self.show_about)
363 main_layout.setMenuBar(menubar)
364 self.main_layout = main_layout
366 quit_shortcut = QShortcut(QKeySequence("Ctrl+Q"), self)
367 quit_shortcut.activated.connect(self.close)
368 close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
369 close_shortcut.activated.connect(self.close)
371 g = self.config.get("winpos-lite",[4, 25, 351, 149])
372 self.setGeometry(g[0], g[1], g[2], g[3])
374 show_hist = self.config.get("gui_show_history",False)
375 show_history.setChecked(show_hist)
376 self.show_history(show_hist)
378 self.setWindowIcon(QIcon(":electrum.png"))
379 self.setWindowTitle("Electrum")
380 self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
381 self.layout().setSizeConstraint(QLayout.SetFixedSize)
382 self.setObjectName("main_window")
385 def toggle_receiving_layout(self):
386 if self.receiving_box.isVisible():
387 self.receiving_box.hide()
388 self.receive_button.setProperty("isActive", False)
390 qApp.style().unpolish(self.receive_button)
391 qApp.style().polish(self.receive_button)
393 self.receiving_box.show()
394 self.receive_button.setProperty("isActive", 'true')
396 qApp.style().unpolish(self.receive_button)
397 qApp.style().polish(self.receive_button)
399 def toggle_theme(self, theme_name):
400 old_path = QDir.currentPath()
401 self.actuator.change_theme(theme_name)
402 # Recompute style globally
403 qApp.style().unpolish(self)
404 qApp.style().polish(self)
405 QDir.setCurrent(old_path)
407 def closeEvent(self, event):
409 self.config.set_key("winpos-lite", [g.left(),g.top(),g.width(),g.height()],True)
410 self.config.set_key("gui_show_history", self.history_list.isVisible(),True)
412 super(MiniWindow, self).closeEvent(event)
415 def set_payment_fields(self, dest_address, amount):
416 self.address_input.setText(dest_address)
417 self.address_field_changed(dest_address)
418 self.amount_input.setText(amount)
423 def deactivate(self):
426 def set_quote_currency(self, currency):
427 """Set and display the fiat currency country."""
428 assert currency in self.quote_currencies
429 self.quote_currencies.remove(currency)
430 self.quote_currencies.insert(0, currency)
431 self.refresh_balance()
433 def change_quote_currency(self):
434 self.quote_currencies = \
435 self.quote_currencies[1:] + self.quote_currencies[0:1]
436 self.actuator.set_config_currency(self.quote_currencies[0])
437 self.refresh_balance()
439 def refresh_balance(self):
440 if self.btc_balance is None:
441 # Price has been discovered before wallet has been loaded
442 # and server connect... so bail.
444 self.set_balances(self.btc_balance)
445 self.amount_input_changed(self.amount_input.text())
447 def set_balances(self, btc_balance):
448 """Set the bitcoin balance and update the amount label accordingly."""
449 self.btc_balance = btc_balance
450 quote_text = self.create_quote_text(btc_balance)
452 quote_text = "(%s)" % quote_text
453 btc_balance = "%.2f" % (btc_balance / bitcoin(1))
454 self.balance_label.set_balance_text(btc_balance, quote_text)
455 self.setWindowTitle("Electrum %s - %s BTC" % (electrum_version, btc_balance))
457 def amount_input_changed(self, amount_text):
458 """Update the number of bitcoins displayed."""
459 self.check_button_status()
462 amount = D(str(amount_text))
463 except decimal.InvalidOperation:
464 self.balance_label.show_balance()
466 quote_text = self.create_quote_text(amount * bitcoin(1))
468 self.balance_label.set_amount_text(quote_text)
469 self.balance_label.show_amount()
471 self.balance_label.show_balance()
473 def create_quote_text(self, btc_balance):
474 """Return a string copy of the amount fiat currency the
475 user has in bitcoins."""
476 quote_currency = self.quote_currencies[0]
477 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
478 if quote_balance is None:
481 quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
486 if self.actuator.send(self.address_input.text(),
487 self.amount_input.text(), self):
488 self.address_input.setText("")
489 self.amount_input.setText("")
491 def check_button_status(self):
492 """Check that the bitcoin address is valid and that something
493 is entered in the amount before making the send button clickable."""
495 value = D(str(self.amount_input.text())) * 10**8
496 except decimal.InvalidOperation:
498 # self.address_input.property(...) returns a qVariant, not a bool.
499 # The == is needed to properly invoke a comparison.
500 if (self.address_input.property("isValid") == True and
501 value is not None and 0 < value <= self.btc_balance):
502 self.send_button.setDisabled(False)
504 self.send_button.setDisabled(True)
506 def address_field_changed(self, address):
507 # label or alias, with address in brackets
508 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
511 address = match2.group(2)
512 self.address_input.setText(address)
514 if self.actuator.is_valid(address):
515 self.check_button_status()
516 self.address_input.setProperty("isValid", True)
517 self.recompute_style(self.address_input)
519 self.send_button.setDisabled(True)
520 self.address_input.setProperty("isValid", False)
521 self.recompute_style(self.address_input)
523 if len(address) == 0:
524 self.address_input.setProperty("isValid", None)
525 self.recompute_style(self.address_input)
527 def recompute_style(self, element):
528 self.style().unpolish(element)
529 self.style().polish(element)
531 def copy_address(self):
532 receive_popup = ReceivePopup(self.receive_button)
533 self.actuator.copy_address(receive_popup)
535 def update_completions(self, completions):
536 self.address_completions.setStringList(completions)
539 def update_history(self, tx_history):
540 from util import format_satoshis, age
542 self.history_list.empty()
544 for item in tx_history[-10:]:
545 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
546 label = self.actuator.wallet.get_label(tx_hash)[0]
547 #amount = D(value) / 10**8
548 v_str = format_satoshis(value, True)
549 self.history_list.append(label, v_str, age(timestamp))
552 self.actuator.acceptbit(self.quote_currencies[0])
554 def the_website(self):
555 webbrowser.open("http://electrum-desktop.com")
557 def show_about(self):
558 QMessageBox.about(self, "Electrum",
559 _("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."))
561 def show_report_bug(self):
562 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
563 _("Please report any bugs as issues on github: <a href=\"https://github.com/spesmilo/electrum/issues\">https://github.com/spesmilo/electrum/issues</a>"))
565 def show_history(self, toggle_state):
567 self.main_layout.setRowMinimumHeight(3,200)
568 self.history_list.show()
570 self.main_layout.setRowMinimumHeight(3,0)
571 self.history_list.hide()
573 def backup_wallet(self):
575 folderName = QFileDialog.getExistingDirectory(QWidget(), 'Select folder to save a copy of your wallet to', os.path.expanduser('~/'))
577 sourceFile = util.user_dir() + '/electrum.dat'
578 shutil.copy2(sourceFile, str(folderName))
579 QMessageBox.information(None,"Wallet backup created", "A copy of your wallet file was created in '%s'" % str(folderName))
580 except (IOError, os.error), reason:
581 QMessageBox.critical(None,"Unable to create backup", "Electrum was unable copy your wallet file to the specified location.\n" + str(reason))
586 class BalanceLabel(QLabel):
592 def __init__(self, change_quote_currency, parent=None):
593 super(QLabel, self).__init__(_("Connecting..."), parent)
594 self.change_quote_currency = change_quote_currency
595 self.state = self.SHOW_CONNECTING
596 self.balance_text = ""
597 self.amount_text = ""
599 def mousePressEvent(self, event):
600 """Change the fiat currency selection if window background is clicked."""
601 if self.state != self.SHOW_CONNECTING:
602 self.change_quote_currency()
604 def set_balance_text(self, btc_balance, quote_text):
605 """Set the amount of bitcoins in the gui."""
606 if self.state == self.SHOW_CONNECTING:
607 self.state = self.SHOW_BALANCE
608 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)
609 if self.state == self.SHOW_BALANCE:
610 self.setText(self.balance_text)
612 def set_amount_text(self, quote_text):
613 self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
614 if self.state == self.SHOW_AMOUNT:
615 self.setText(self.amount_text)
617 def show_balance(self):
618 if self.state == self.SHOW_AMOUNT:
619 self.state = self.SHOW_BALANCE
620 self.setText(self.balance_text)
622 def show_amount(self):
623 if self.state == self.SHOW_BALANCE:
624 self.state = self.SHOW_AMOUNT
625 self.setText(self.amount_text)
627 def ok_cancel_buttons(dialog):
628 row_layout = QHBoxLayout()
629 row_layout.addStretch(1)
630 ok_button = QPushButton(_("OK"))
631 row_layout.addWidget(ok_button)
632 ok_button.clicked.connect(dialog.accept)
633 cancel_button = QPushButton(_("Cancel"))
634 row_layout.addWidget(cancel_button)
635 cancel_button.clicked.connect(dialog.reject)
638 class PasswordDialog(QDialog):
640 def __init__(self, parent):
641 super(QDialog, self).__init__(parent)
645 self.password_input = QLineEdit()
646 self.password_input.setEchoMode(QLineEdit.Password)
648 main_layout = QVBoxLayout(self)
649 message = _('Please enter your password')
650 main_layout.addWidget(QLabel(message))
654 grid.addWidget(QLabel(_('Password')), 1, 0)
655 grid.addWidget(self.password_input, 1, 1)
656 main_layout.addLayout(grid)
658 main_layout.addLayout(ok_cancel_buttons(self))
659 self.setLayout(main_layout)
664 return unicode(self.password_input.text())
666 class ReceivePopup(QDialog):
668 def leaveEvent(self, event):
671 def setup(self, address):
672 label = QLabel(_("Copied your Bitcoin address to the clipboard!"))
673 address_display = QLineEdit(address)
674 address_display.setReadOnly(True)
675 resize_line_edit_width(address_display, address)
677 main_layout = QVBoxLayout(self)
678 main_layout.addWidget(label)
679 main_layout.addWidget(address_display)
681 self.setMouseTracking(True)
682 self.setWindowTitle("Electrum - " + _("Receive Bitcoin payment"))
683 self.setWindowFlags(Qt.Window|Qt.FramelessWindowHint|
684 Qt.MSWindowsFixedSizeDialogHint)
685 self.layout().setSizeConstraint(QLayout.SetFixedSize)
686 #self.setFrameStyle(QFrame.WinPanel|QFrame.Raised)
687 #self.setAlignment(Qt.AlignCenter)
690 parent = self.parent()
691 top_left_pos = parent.mapToGlobal(parent.rect().bottomLeft())
692 self.move(top_left_pos)
693 center_mouse_pos = self.mapToGlobal(self.rect().center())
694 QCursor.setPos(center_mouse_pos)
698 """Initialize the definitions relating to themes and
699 sending/recieving bitcoins."""
702 def __init__(self, wallet):
703 """Retrieve the gui theme used in previous session."""
705 self.theme_name = self.wallet.config.get('litegui_theme','Cleanlook')
706 self.themes = load_theme_paths()
708 def load_theme(self):
709 """Load theme retrieved from wallet file."""
711 theme_prefix, theme_path = self.themes[self.theme_name]
713 util.print_error("Theme not found!", self.theme_name)
715 QDir.setCurrent(os.path.join(theme_prefix, theme_path))
716 with open(rsrc("style.css")) as style_file:
717 qApp.setStyleSheet(style_file.read())
719 def theme_names(self):
721 return sorted(self.themes.keys())
723 def selected_theme(self):
725 return self.theme_name
727 def change_theme(self, theme_name):
729 self.theme_name = theme_name
730 self.wallet.config.set_key('litegui_theme',theme_name)
733 def set_configured_currency(self, set_quote_currency):
734 """Set the inital fiat currency conversion country (USD/EUR/GBP) in
735 the GUI to what it was set to in the wallet."""
736 currency = self.wallet.config.get('conversion_currency')
737 # currency can be none when Electrum is used for the first
738 # time and no setting has been created yet.
739 if currency is not None:
740 set_quote_currency(currency)
742 def set_config_currency(self, conversion_currency):
743 """Change the wallet fiat currency country."""
744 self.wallet.config.set_key('conversion_currency',conversion_currency,True)
746 def copy_address(self, receive_popup):
747 """Copy the wallet addresses into the client."""
748 addrs = [addr for addr in self.wallet.all_addresses()
749 if not self.wallet.is_change(addr)]
750 # Select most recent addresses from gap limit
751 addrs = addrs[-self.wallet.gap_limit:]
752 copied_address = random.choice(addrs)
753 qApp.clipboard().setText(copied_address)
754 receive_popup.setup(copied_address)
755 receive_popup.popup()
757 def waiting_dialog(self, f):
762 w.setWindowTitle('Electrum')
763 l = QLabel('Sending transaction, please wait.')
772 w.connect(s, QtCore.SIGNAL('timersignal'), ff)
776 def csv_transaction(self):
778 fileName = QFileDialog.getSaveFileName(QWidget(), 'Select file to export your wallet transactions to', os.path.expanduser('~/'), "*.csv")
780 with open(fileName, "w+") as csvfile:
781 transaction = csv.writer(csvfile)
782 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
783 for item in self.wallet.get_tx_history():
784 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
786 if timestamp is not None:
788 time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
789 except [RuntimeError, TypeError, NameError] as reason:
790 time_string = "unknown"
793 time_string = "unknown"
795 time_string = "pending"
797 if value is not None:
798 value_string = format_satoshis(value, True, self.wallet.num_zeros)
803 fee_string = format_satoshis(fee, True, self.wallet.num_zeros)
808 label, is_default_label = self.wallet.get_label(tx_hash)
812 balance_string = format_satoshis(balance, False, self.wallet.num_zeros)
813 transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
814 QMessageBox.information(None,"CSV Export created", "Your CSV export has been succesfully created.")
815 except (IOError, os.error), reason:
816 QMessageBox.critical(None,"Unable to create csv", "Electrum was unable to produce a transaction export.\n" + str(reason))
818 def send(self, address, amount, parent_window):
819 """Send bitcoins to the target address."""
820 dest_address = self.fetch_destination(address)
822 if dest_address is None or not self.wallet.is_valid(dest_address):
823 QMessageBox.warning(parent_window, _('Error'),
824 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
827 convert_amount = lambda amount: \
828 int(D(unicode(amount)) * bitcoin(1))
829 amount = convert_amount(amount)
831 if self.wallet.use_encryption:
832 password_dialog = PasswordDialog(parent_window)
833 password = password_dialog.run()
841 if amount < bitcoin(1) / 10:
843 fee = bitcoin(1) / 1000
846 tx = self.wallet.mktx([(dest_address, amount)], "", password, fee)
847 except BaseException as error:
848 QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
851 h = self.wallet.send_tx(tx)
853 self.waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Sending transaction, please wait..."))
855 status, message = self.wallet.receive_tx(h)
859 dumpf = tempfile.NamedTemporaryFile(delete=False)
862 print "Dumped error tx to", dumpf.name
863 QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
866 TransactionWindow(message, self)
867 # QMessageBox.information(parent_window, '',
868 # _('Your transaction has been sent.') + '\n' + message, _('OK'))
871 def fetch_destination(self, address):
872 recipient = unicode(address).strip()
875 match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
878 # label or alias, with address in brackets
879 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
884 self.wallet.get_alias(recipient, True,
885 self.show_message, self.question)
888 return match2.group(2)
892 def is_valid(self, address):
893 """Check if bitcoin address is valid."""
895 return self.wallet.is_valid(address)
897 def copy_master_public_key(self):
898 master_pubkey = self.wallet.master_public_key
899 qApp.clipboard().setText(master_pubkey)
900 QMessageBox.information(None,"Copy succesful", "Your public master key has been copied to your clipboard.")
903 def acceptbit(self, currency):
904 master_pubkey = self.wallet.master_public_key
905 url = "http://acceptbit.com/mpk/%s/%s" % (master_pubkey, currency)
908 def show_seed_dialog(self):
909 gui_qt.ElectrumWindow.show_seed_dialog(self.wallet)
911 class MiniDriver(QObject):
918 def __init__(self, wallet, window):
919 super(QObject, self).__init__()
924 self.wallet.interface.register_callback('updated',self.update_callback)
925 self.wallet.interface.register_callback('connected', self.update_callback)
926 self.wallet.interface.register_callback('disconnected', self.update_callback)
931 self.connect(self, SIGNAL("updatesignal()"), self.update)
932 self.update_callback()
934 # This is a hack to workaround that Qt does not like changing the
935 # window properties from this other thread before the runloop has
937 def update_callback(self):
938 self.emit(SIGNAL("updatesignal()"))
941 if not self.wallet.interface:
943 elif not self.wallet.interface.is_connected:
945 elif not self.wallet.up_to_date:
950 if self.wallet.up_to_date:
951 self.update_balance()
952 self.update_completions()
953 self.update_history()
955 def initializing(self):
956 if self.state == self.INITIALIZING:
958 self.state = self.INITIALIZING
959 self.window.deactivate()
961 def connecting(self):
962 if self.state == self.CONNECTING:
964 self.state = self.CONNECTING
965 self.window.deactivate()
967 def synchronizing(self):
968 if self.state == self.SYNCHRONIZING:
970 self.state = self.SYNCHRONIZING
971 self.window.deactivate()
974 if self.state == self.READY:
976 self.state = self.READY
977 self.window.activate()
979 def update_balance(self):
980 conf_balance, unconf_balance = self.wallet.get_balance()
981 balance = D(conf_balance + unconf_balance)
982 self.window.set_balances(balance)
984 def update_completions(self):
986 for addr, label in self.wallet.labels.items():
987 if addr in self.wallet.addressbook:
988 completions.append("%s <%s>" % (label, addr))
989 completions = completions + self.wallet.aliases.keys()
990 self.window.update_completions(completions)
992 def update_history(self):
993 tx_history = self.wallet.get_tx_history()
994 self.window.update_history(tx_history)
997 if __name__ == "__main__":
998 app = QApplication(sys.argv)
999 with open(rsrc("style.css")) as style_file:
1000 app.setStyleSheet(style_file.read())
1002 sys.exit(app.exec_())