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 interface import DEFAULT_SERVERS
19 from util import get_resource_path as rsrc
34 from wallet import format_satoshis
38 bitcoin = lambda v: v * 100000000
40 def IconButton(filename, parent=None):
41 pixmap = QPixmap(filename)
43 return QPushButton(icon, "", parent)
48 self.emit(SIGNAL('timersignal'))
51 def resize_line_edit_width(line_edit, text_input):
52 metrics = QFontMetrics(qApp.font())
53 # Create an extra character to add some space on the end
55 line_edit.setMinimumWidth(metrics.width(text_input))
57 def load_theme_name(theme_path):
59 with open(os.path.join(theme_path, "name.cfg")) as name_cfg_file:
60 return name_cfg_file.read().rstrip("\n").strip()
65 def theme_dirs_from_prefix(prefix):
66 if not os.path.exists(prefix):
69 for potential_theme in os.listdir(prefix):
70 theme_full_path = os.path.join(prefix, potential_theme)
71 theme_css = os.path.join(theme_full_path, "style.css")
72 if not os.path.exists(theme_css):
74 theme_name = load_theme_name(theme_full_path)
75 if theme_name is None:
77 theme_paths[theme_name] = prefix, potential_theme
80 def load_theme_paths():
82 prefixes = (util.local_data_dir(), util.appdata_dir())
83 for prefix in prefixes:
84 theme_paths.update(theme_dirs_from_prefix(prefix))
88 class ElectrumGui(QObject):
90 def __init__(self, wallet, config):
91 super(QObject, self).__init__()
95 self.check_qt_version()
96 self.app = QApplication(sys.argv)
97 self.wallet.interface.register_callback('peers', self.server_list_changed)
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 self.connect(self, SIGNAL("updateservers()"),
112 actuator.update_servers_list)
113 # Should probably not modify the current path but instead
114 # change the behaviour of rsrc(...)
115 old_path = QDir.currentPath()
116 actuator.load_theme()
118 self.mini = MiniWindow(actuator, self.expand, self.config)
119 driver = MiniDriver(self.wallet, self.mini)
121 # Reset path back to original value now that loading the GUI
123 QDir.setCurrent(old_path)
130 self.expert = gui_qt.ElectrumWindow(self.wallet, self.config)
131 self.expert.app = self.app
132 self.expert.connect_slots(timer)
133 self.expert.update_wallet()
136 def server_list_changed(self):
137 self.emit(SIGNAL("updateservers()"))
140 """Hide the lite mode window and show pro-mode."""
144 def set_url(self, url):
145 payto, amount, label, message, signature, identity, url = \
146 self.wallet.parse_url(url, self.show_message, self.show_question)
147 self.mini.set_payment_fields(payto, amount)
149 def show_message(self, message):
150 QMessageBox.information(self.mini, _("Message"), message, _("OK"))
152 def show_question(self, message):
153 choice = QMessageBox.question(self.mini, _("Message"), message,
154 QMessageBox.Yes|QMessageBox.No,
156 return choice == QMessageBox.Yes
158 def restore_or_create(self):
159 qt_gui_object = gui_qt.ElectrumGui(self.wallet, self.app)
160 return qt_gui_object.restore_or_create()
162 class MiniWindow(QDialog):
164 def __init__(self, actuator, expand_callback, config):
165 super(MiniWindow, self).__init__()
167 self.actuator = actuator
170 self.btc_balance = None
171 self.quote_currencies = ["EUR", "USD", "GBP"]
172 self.actuator.set_configured_currency(self.set_quote_currency)
173 self.exchanger = exchange_rate.Exchanger(self)
174 # Needed because price discovery is done in a different thread
175 # which needs to be sent back to this main one to update the GUI
176 self.connect(self, SIGNAL("refresh_balance()"), self.refresh_balance)
178 self.balance_label = BalanceLabel(self.change_quote_currency)
179 self.balance_label.setObjectName("balance_label")
181 self.receive_button = QPushButton(_("&Receive"))
182 self.receive_button.setObjectName("receive_button")
183 self.receive_button.setDefault(True)
184 self.receive_button.clicked.connect(self.copy_address)
186 # Bitcoin address code
187 self.address_input = QLineEdit()
188 self.address_input.setPlaceholderText(_("Enter a Bitcoin address..."))
189 self.address_input.setObjectName("address_input")
192 self.address_input.textEdited.connect(self.address_field_changed)
193 resize_line_edit_width(self.address_input,
194 "1BtaFUr3qVvAmwrsuDuu5zk6e4s2rxd2Gy")
196 self.address_completions = QStringListModel()
197 address_completer = QCompleter(self.address_input)
198 address_completer.setCaseSensitivity(False)
199 address_completer.setModel(self.address_completions)
200 self.address_input.setCompleter(address_completer)
202 address_layout = QHBoxLayout()
203 address_layout.addWidget(self.address_input)
205 self.amount_input = QLineEdit()
206 self.amount_input.setPlaceholderText(_("... and amount"))
207 self.amount_input.setObjectName("amount_input")
208 # This is changed according to the user's displayed balance
209 self.amount_validator = QDoubleValidator(self.amount_input)
210 self.amount_validator.setNotation(QDoubleValidator.StandardNotation)
211 self.amount_validator.setDecimals(8)
212 self.amount_input.setValidator(self.amount_validator)
214 # This removes the very ugly OSX highlighting, please leave this in :D
215 self.address_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
216 self.amount_input.setAttribute(Qt.WA_MacShowFocusRect, 0)
217 self.amount_input.textChanged.connect(self.amount_input_changed)
219 self.send_button = QPushButton(_("&Send"))
220 self.send_button.setObjectName("send_button")
221 self.send_button.setDisabled(True);
222 self.send_button.clicked.connect(self.send)
224 main_layout = QGridLayout(self)
226 main_layout.addWidget(self.balance_label, 0, 0)
227 main_layout.addWidget(self.receive_button, 0, 1)
229 main_layout.addWidget(self.address_input, 1, 0, 1, -1)
231 main_layout.addWidget(self.amount_input, 2, 0)
232 main_layout.addWidget(self.send_button, 2, 1)
234 self.history_list = history_widget.HistoryWidget()
235 self.history_list.setObjectName("history")
236 self.history_list.hide()
237 self.history_list.setAlternatingRowColors(True)
238 main_layout.addWidget(self.history_list, 3, 0, 1, -1)
241 electrum_menu = menubar.addMenu(_("&Bitcoin"))
243 servers_menu = electrum_menu.addMenu(_("&Servers"))
244 servers_group = QActionGroup(self)
245 self.actuator.set_servers_gui_stuff(servers_menu, servers_group)
246 self.actuator.populate_servers_menu()
247 electrum_menu.addSeparator()
249 brain_seed = electrum_menu.addAction(_("&BrainWallet Info"))
250 brain_seed.triggered.connect(self.actuator.show_seed_dialog)
251 quit_option = electrum_menu.addAction(_("&Quit"))
252 quit_option.triggered.connect(self.close)
254 view_menu = menubar.addMenu(_("&View"))
255 extra_menu = menubar.addMenu(_("&Extra"))
257 backup_wallet = extra_menu.addAction( _("&Create wallet backup"))
258 backup_wallet.triggered.connect(self.backup_wallet)
260 export_csv = extra_menu.addAction( _("&Export transactions to CSV") )
261 export_csv.triggered.connect(self.actuator.csv_transaction)
263 expert_gui = view_menu.addAction(_("&Classic GUI"))
264 expert_gui.triggered.connect(expand_callback)
265 themes_menu = view_menu.addMenu(_("&Themes"))
266 selected_theme = self.actuator.selected_theme()
267 theme_group = QActionGroup(self)
268 for theme_name in self.actuator.theme_names():
269 theme_action = themes_menu.addAction(theme_name)
270 theme_action.setCheckable(True)
271 if selected_theme == theme_name:
272 theme_action.setChecked(True)
273 class SelectThemeFunctor:
274 def __init__(self, theme_name, toggle_theme):
275 self.theme_name = theme_name
276 self.toggle_theme = toggle_theme
277 def __call__(self, checked):
279 self.toggle_theme(self.theme_name)
280 delegate = SelectThemeFunctor(theme_name, self.toggle_theme)
281 theme_action.toggled.connect(delegate)
282 theme_group.addAction(theme_action)
283 view_menu.addSeparator()
284 show_history = view_menu.addAction(_("Show History"))
285 show_history.setCheckable(True)
286 show_history.toggled.connect(self.show_history)
288 help_menu = menubar.addMenu(_("&Help"))
289 the_website = help_menu.addAction(_("&Website"))
290 the_website.triggered.connect(self.the_website)
291 help_menu.addSeparator()
292 report_bug = help_menu.addAction(_("&Report Bug"))
293 report_bug.triggered.connect(self.show_report_bug)
294 show_about = help_menu.addAction(_("&About"))
295 show_about.triggered.connect(self.show_about)
296 main_layout.setMenuBar(menubar)
298 quit_shortcut = QShortcut(QKeySequence("Ctrl+Q"), self)
299 quit_shortcut.activated.connect(self.close)
300 close_shortcut = QShortcut(QKeySequence("Ctrl+W"), self)
301 close_shortcut.activated.connect(self.close)
303 g = self.config.get("winpos-lite",[4, 25, 351, 149])
304 self.setGeometry(g[0], g[1], g[2], g[3])
306 show_hist = self.config.get("gui_show_history",False)
307 show_history.setChecked(show_hist)
308 self.show_history(show_hist)
310 self.setWindowIcon(QIcon(":electrum.png"))
311 self.setWindowTitle("Electrum")
312 self.setWindowFlags(Qt.Window|Qt.MSWindowsFixedSizeDialogHint)
313 self.layout().setSizeConstraint(QLayout.SetFixedSize)
314 self.setObjectName("main_window")
317 def toggle_theme(self, theme_name):
318 old_path = QDir.currentPath()
319 self.actuator.change_theme(theme_name)
320 # Recompute style globally
321 qApp.style().unpolish(self)
322 qApp.style().polish(self)
323 QDir.setCurrent(old_path)
325 def closeEvent(self, event):
327 self.config.set_key("winpos-lite", [g.left(),g.top(),g.width(),g.height()],True)
328 self.config.set_key("gui_show_history", self.history_list.isVisible(),True)
330 super(MiniWindow, self).closeEvent(event)
333 def set_payment_fields(self, dest_address, amount):
334 self.address_input.setText(dest_address)
335 self.address_field_changed(dest_address)
336 self.amount_input.setText(amount)
341 def deactivate(self):
344 def set_quote_currency(self, currency):
345 """Set and display the fiat currency country."""
346 assert currency in self.quote_currencies
347 self.quote_currencies.remove(currency)
348 self.quote_currencies.insert(0, currency)
349 self.refresh_balance()
351 def change_quote_currency(self):
352 self.quote_currencies = \
353 self.quote_currencies[1:] + self.quote_currencies[0:1]
354 self.actuator.set_config_currency(self.quote_currencies[0])
355 self.refresh_balance()
357 def refresh_balance(self):
358 if self.btc_balance is None:
359 # Price has been discovered before wallet has been loaded
360 # and server connect... so bail.
362 self.set_balances(self.btc_balance)
363 self.amount_input_changed(self.amount_input.text())
365 def set_balances(self, btc_balance):
366 """Set the bitcoin balance and update the amount label accordingly."""
367 self.btc_balance = btc_balance
368 quote_text = self.create_quote_text(btc_balance)
370 quote_text = "(%s)" % quote_text
371 btc_balance = "%.2f" % (btc_balance / bitcoin(1))
372 self.balance_label.set_balance_text(btc_balance, quote_text)
373 self.setWindowTitle("Electrum - %s BTC" % btc_balance)
375 def amount_input_changed(self, amount_text):
376 """Update the number of bitcoins displayed."""
377 self.check_button_status()
380 amount = D(str(amount_text))
381 except decimal.InvalidOperation:
382 self.balance_label.show_balance()
384 quote_text = self.create_quote_text(amount * bitcoin(1))
386 self.balance_label.set_amount_text(quote_text)
387 self.balance_label.show_amount()
389 self.balance_label.show_balance()
391 def create_quote_text(self, btc_balance):
392 """Return a string copy of the amount fiat currency the
393 user has in bitcoins."""
394 quote_currency = self.quote_currencies[0]
395 quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
396 if quote_balance is None:
399 quote_text = "%.2f %s" % ((quote_balance / bitcoin(1)),
404 if self.actuator.send(self.address_input.text(),
405 self.amount_input.text(), self):
406 self.address_input.setText("")
407 self.amount_input.setText("")
409 def check_button_status(self):
410 """Check that the bitcoin address is valid and that something
411 is entered in the amount before making the send button clickable."""
413 value = D(str(self.amount_input.text())) * 10**8
414 except decimal.InvalidOperation:
416 # self.address_input.property(...) returns a qVariant, not a bool.
417 # The == is needed to properly invoke a comparison.
418 if (self.address_input.property("isValid") == True and
419 value is not None and 0 < value <= self.btc_balance):
420 self.send_button.setDisabled(False)
422 self.send_button.setDisabled(True)
424 def address_field_changed(self, address):
425 if self.actuator.is_valid(address):
426 self.check_button_status()
427 self.address_input.setProperty("isValid", True)
428 self.recompute_style(self.address_input)
430 self.send_button.setDisabled(True)
431 self.address_input.setProperty("isValid", False)
432 self.recompute_style(self.address_input)
434 if len(address) == 0:
435 self.address_input.setProperty("isValid", None)
436 self.recompute_style(self.address_input)
438 def recompute_style(self, element):
439 self.style().unpolish(element)
440 self.style().polish(element)
442 def copy_address(self):
443 receive_popup = ReceivePopup(self.receive_button)
444 self.actuator.copy_address(receive_popup)
446 def update_completions(self, completions):
447 self.address_completions.setStringList(completions)
449 def update_history(self, tx_history):
450 from util import format_satoshis
451 for item in tx_history[-10:]:
452 tx_hash, conf, is_mine, value, fee, balance, timestamp = item
453 label = self.actuator.wallet.get_label(tx_hash)[0]
454 #amount = D(value) / 10**8
455 v_str = format_satoshis(value, True)
456 self.history_list.append(label, v_str)
459 self.actuator.acceptbit(self.quote_currencies[0])
461 def the_website(self):
462 webbrowser.open("http://electrum-desktop.com")
464 def show_about(self):
465 QMessageBox.about(self, "Electrum",
466 _("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"))
468 def show_report_bug(self):
469 QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"),
470 _("Email bug reports to %s") % "genjix" + "@" + "riseup.net")
472 def show_history(self, toggle_state):
474 self.history_list.show()
476 self.history_list.hide()
478 def backup_wallet(self):
480 folderName = QFileDialog.getExistingDirectory(QWidget(), 'Select folder to save a copy of your wallet to', os.path.expanduser('~/'))
482 sourceFile = util.user_dir() + '/electrum.dat'
483 shutil.copy2(sourceFile, str(folderName))
484 QMessageBox.information(None,"Wallet backup created", "A copy of your wallet file was created in '%s'" % str(folderName))
485 except (IOError, os.error), reason:
486 QMessageBox.critical(None,"Unable to create backup", "Electrum was unable copy your wallet file to the specified location.\n" + str(reason))
491 class BalanceLabel(QLabel):
497 def __init__(self, change_quote_currency, parent=None):
498 super(QLabel, self).__init__(_("Connecting..."), parent)
499 self.change_quote_currency = change_quote_currency
500 self.state = self.SHOW_CONNECTING
501 self.balance_text = ""
502 self.amount_text = ""
504 def mousePressEvent(self, event):
505 """Change the fiat currency selection if window background is clicked."""
506 if self.state != self.SHOW_CONNECTING:
507 self.change_quote_currency()
509 def set_balance_text(self, btc_balance, quote_text):
510 """Set the amount of bitcoins in the gui."""
511 if self.state == self.SHOW_CONNECTING:
512 self.state = self.SHOW_BALANCE
513 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)
514 if self.state == self.SHOW_BALANCE:
515 self.setText(self.balance_text)
517 def set_amount_text(self, quote_text):
518 self.amount_text = "<span style='font-size: 10pt'>%s</span>" % quote_text
519 if self.state == self.SHOW_AMOUNT:
520 self.setText(self.amount_text)
522 def show_balance(self):
523 if self.state == self.SHOW_AMOUNT:
524 self.state = self.SHOW_BALANCE
525 self.setText(self.balance_text)
527 def show_amount(self):
528 if self.state == self.SHOW_BALANCE:
529 self.state = self.SHOW_AMOUNT
530 self.setText(self.amount_text)
532 def ok_cancel_buttons(dialog):
533 row_layout = QHBoxLayout()
534 row_layout.addStretch(1)
535 ok_button = QPushButton(_("OK"))
536 row_layout.addWidget(ok_button)
537 ok_button.clicked.connect(dialog.accept)
538 cancel_button = QPushButton(_("Cancel"))
539 row_layout.addWidget(cancel_button)
540 cancel_button.clicked.connect(dialog.reject)
543 class PasswordDialog(QDialog):
545 def __init__(self, parent):
546 super(QDialog, self).__init__(parent)
550 self.password_input = QLineEdit()
551 self.password_input.setEchoMode(QLineEdit.Password)
553 main_layout = QVBoxLayout(self)
554 message = _('Please enter your password')
555 main_layout.addWidget(QLabel(message))
559 grid.addWidget(QLabel(_('Password')), 1, 0)
560 grid.addWidget(self.password_input, 1, 1)
561 main_layout.addLayout(grid)
563 main_layout.addLayout(ok_cancel_buttons(self))
564 self.setLayout(main_layout)
569 return unicode(self.password_input.text())
571 class ReceivePopup(QDialog):
573 def leaveEvent(self, event):
576 def setup(self, address):
577 label = QLabel(_("Copied your Bitcoin address to the clipboard!"))
578 address_display = QLineEdit(address)
579 address_display.setReadOnly(True)
580 resize_line_edit_width(address_display, address)
582 main_layout = QVBoxLayout(self)
583 main_layout.addWidget(label)
584 main_layout.addWidget(address_display)
586 self.setMouseTracking(True)
587 self.setWindowTitle("Electrum - " + _("Receive Bitcoin payment"))
588 self.setWindowFlags(Qt.Window|Qt.FramelessWindowHint|
589 Qt.MSWindowsFixedSizeDialogHint)
590 self.layout().setSizeConstraint(QLayout.SetFixedSize)
591 #self.setFrameStyle(QFrame.WinPanel|QFrame.Raised)
592 #self.setAlignment(Qt.AlignCenter)
595 parent = self.parent()
596 top_left_pos = parent.mapToGlobal(parent.rect().bottomLeft())
597 self.move(top_left_pos)
598 center_mouse_pos = self.mapToGlobal(self.rect().center())
599 QCursor.setPos(center_mouse_pos)
603 """Initialize the definitions relating to themes and
604 sending/recieving bitcoins."""
607 def __init__(self, wallet):
608 """Retrieve the gui theme used in previous session."""
610 self.theme_name = self.wallet.config.get('litegui_theme','Cleanlook')
611 self.themes = load_theme_paths()
613 def load_theme(self):
614 """Load theme retrieved from wallet file."""
616 theme_prefix, theme_path = self.themes[self.theme_name]
618 util.print_error("Theme not found!", self.theme_name)
620 QDir.setCurrent(os.path.join(theme_prefix, theme_path))
621 with open(rsrc("style.css")) as style_file:
622 qApp.setStyleSheet(style_file.read())
624 def theme_names(self):
626 return sorted(self.themes.keys())
628 def selected_theme(self):
630 return self.theme_name
632 def change_theme(self, theme_name):
634 self.theme_name = theme_name
635 self.wallet.config.set_key('litegui_theme',theme_name)
638 def set_configured_currency(self, set_quote_currency):
639 """Set the inital fiat currency conversion country (USD/EUR/GBP) in
640 the GUI to what it was set to in the wallet."""
641 currency = self.wallet.config.get('conversion_currency')
642 # currency can be none when Electrum is used for the first
643 # time and no setting has been created yet.
644 if currency is not None:
645 set_quote_currency(currency)
647 def set_config_currency(self, conversion_currency):
648 """Change the wallet fiat currency country."""
649 self.wallet.config.set_key('conversion_currency',conversion_currency,True)
651 def set_servers_gui_stuff(self, servers_menu, servers_group):
652 self.servers_menu = servers_menu
653 self.servers_group = servers_group
655 def populate_servers_menu(self):
656 interface = self.wallet.interface
657 if not interface.servers:
658 print "No servers loaded yet."
659 self.servers_list = []
660 for server_string in DEFAULT_SERVERS:
661 host, port, protocol = server_string.split(':')
662 transports = [(protocol,port)]
663 self.servers_list.append((host, transports))
665 print "Servers loaded."
666 self.servers_list = interface.servers
667 server_names = [details[0] for details in self.servers_list]
668 current_server = interface.server.split(":")[0]
669 for server_name in server_names:
670 server_action = self.servers_menu.addAction(server_name)
671 server_action.setCheckable(True)
672 if server_name == current_server:
673 server_action.setChecked(True)
674 class SelectServerFunctor:
675 def __init__(self, server_name, server_selected):
676 self.server_name = server_name
677 self.server_selected = server_selected
678 def __call__(self, checked):
680 # call server_selected
681 self.server_selected(self.server_name)
682 delegate = SelectServerFunctor(server_name, self.server_selected)
683 server_action.toggled.connect(delegate)
684 self.servers_group.addAction(server_action)
686 def update_servers_list(self):
687 # Clear servers_group
688 for action in self.servers_group.actions():
689 self.servers_group.removeAction(action)
690 self.populate_servers_menu()
692 def server_selected(self, server_name):
693 match = [transports for (host, transports) in self.servers_list
694 if host == server_name]
695 assert len(match) == 1
697 # Default to TCP if available else use anything
698 # TODO: protocol should be selectable.
699 tcp_port = [port for (protocol, port) in match if protocol == "t"]
700 if len(tcp_port) == 0:
701 protocol = match[0][0]
706 server_line = "%s:%s:%s" % (server_name, port, protocol)
708 # Should this have exception handling?
709 self.wallet.interface.set_server(server_line, self.wallet.config.get("proxy"))
711 def copy_address(self, receive_popup):
712 """Copy the wallet addresses into the client."""
713 addrs = [addr for addr in self.wallet.all_addresses()
714 if not self.wallet.is_change(addr)]
715 # Select most recent addresses from gap limit
716 addrs = addrs[-self.wallet.gap_limit:]
717 copied_address = random.choice(addrs)
718 qApp.clipboard().setText(copied_address)
719 receive_popup.setup(copied_address)
720 receive_popup.popup()
722 def waiting_dialog(self, f):
727 w.setWindowTitle('Electrum')
728 l = QLabel('Sending transaction, please wait.')
737 w.connect(s, QtCore.SIGNAL('timersignal'), ff)
741 def csv_transaction(self):
743 fileName = QFileDialog.getSaveFileName(QWidget(), 'Select file to export your wallet transactions to', os.path.expanduser('~/'), "*.csv")
745 with open(fileName, "w+") as csvfile:
746 transaction = csv.writer(csvfile)
747 transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
748 for item in self.wallet.get_tx_history():
749 tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
751 if timestamp is not None:
753 time_string = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
754 except [RuntimeError, TypeError, NameError] as reason:
755 time_string = "unknown"
758 time_string = "pending"
760 if value is not None:
761 value_string = format_satoshis(value, True, self.wallet.num_zeros)
766 fee_string = format_satoshis(fee, True, self.wallet.num_zeros)
771 label, is_default_label = self.wallet.get_label(tx_hash)
775 balance_string = format_satoshis(balance, False, self.wallet.num_zeros)
776 transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
777 QMessageBox.information(None,"CSV Export created", "Your CSV export has been succesfully created.")
778 except (IOError, os.error), reason:
779 QMessageBox.critical(None,"Unable to create csv", "Electrum was unable to produce a transaction export.\n" + str(reason))
781 def send(self, address, amount, parent_window):
782 """Send bitcoins to the target address."""
783 dest_address = self.fetch_destination(address)
785 if dest_address is None or not self.wallet.is_valid(dest_address):
786 QMessageBox.warning(parent_window, _('Error'),
787 _('Invalid Bitcoin Address') + ':\n' + address, _('OK'))
790 convert_amount = lambda amount: \
791 int(D(unicode(amount)) * bitcoin(1))
792 amount = convert_amount(amount)
794 if self.wallet.use_encryption:
795 password_dialog = PasswordDialog(parent_window)
796 password = password_dialog.run()
804 if amount < bitcoin(1) / 10:
806 fee = bitcoin(1) / 1000
809 tx = self.wallet.mktx([(dest_address, amount)], "", password, fee)
810 except BaseException as error:
811 QMessageBox.warning(parent_window, _('Error'), str(error), _('OK'))
814 h = self.wallet.send_tx(tx)
816 self.waiting_dialog(lambda: False if self.wallet.tx_event.isSet() else _("Sending transaction, please wait..."))
818 status, message = self.wallet.receive_tx(h)
822 dumpf = tempfile.NamedTemporaryFile(delete=False)
825 print "Dumped error tx to", dumpf.name
826 QMessageBox.warning(parent_window, _('Error'), message, _('OK'))
829 QMessageBox.information(parent_window, '',
830 _('Your transaction has been sent.') + '\n' + message, _('OK'))
833 def fetch_destination(self, address):
834 recipient = unicode(address).strip()
837 match1 = re.match("^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$",
840 # label or alias, with address in brackets
841 match2 = re.match("(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>",
846 self.wallet.get_alias(recipient, True,
847 self.show_message, self.question)
850 return match2.group(2)
854 def is_valid(self, address):
855 """Check if bitcoin address is valid."""
856 return self.wallet.is_valid(address)
858 def acceptbit(self, currency):
859 master_pubkey = self.wallet.master_public_key
860 url = "http://acceptbit.com/mpk/%s/%s" % (master_pubkey, currency)
863 def show_seed_dialog(self):
864 gui_qt.ElectrumWindow.show_seed_dialog(self.wallet)
866 class MiniDriver(QObject):
873 def __init__(self, wallet, window):
874 super(QObject, self).__init__()
879 self.wallet.interface.register_callback('updated',self.update_callback)
880 self.wallet.interface.register_callback('connected', self.update_callback)
881 self.wallet.interface.register_callback('disconnected', self.update_callback)
886 self.connect(self, SIGNAL("updatesignal()"), self.update)
887 self.update_callback()
889 # This is a hack to workaround that Qt does not like changing the
890 # window properties from this other thread before the runloop has
892 def update_callback(self):
893 self.emit(SIGNAL("updatesignal()"))
896 if not self.wallet.interface:
898 elif not self.wallet.interface.is_connected:
900 elif not self.wallet.up_to_date:
905 if self.wallet.up_to_date:
906 self.update_balance()
907 self.update_completions()
908 self.update_history()
910 def initializing(self):
911 if self.state == self.INITIALIZING:
913 self.state = self.INITIALIZING
914 self.window.deactivate()
916 def connecting(self):
917 if self.state == self.CONNECTING:
919 self.state = self.CONNECTING
920 self.window.deactivate()
922 def synchronizing(self):
923 if self.state == self.SYNCHRONIZING:
925 self.state = self.SYNCHRONIZING
926 self.window.deactivate()
929 if self.state == self.READY:
931 self.state = self.READY
932 self.window.activate()
934 def update_balance(self):
935 conf_balance, unconf_balance = self.wallet.get_balance()
936 balance = D(conf_balance + unconf_balance)
937 self.window.set_balances(balance)
939 def update_completions(self):
941 for addr, label in self.wallet.labels.items():
942 if addr in self.wallet.addressbook:
943 completions.append("%s <%s>" % (label, addr))
944 completions = completions + self.wallet.aliases.keys()
945 self.window.update_completions(completions)
947 def update_history(self):
948 tx_history = self.wallet.get_tx_history()
949 self.window.update_history(tx_history)
951 if __name__ == "__main__":
952 app = QApplication(sys.argv)
953 with open(rsrc("style.css")) as style_file:
954 app.setStyleSheet(style_file.read())
956 sys.exit(app.exec_())