electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 35838b088497d2e8a1d52d10d5fe70724188088d
parent 837e154d3928c0d96e2e50ef4425201c863a59db
Author: slush <info@bitcoin.cz>
Date:   Mon, 10 Dec 2012 00:51:07 +0100

Merge branch 'master' of github.com:spesmilo/electrum

Diffstat:
MREADME | 5+++--
Mdata/cleanlook/style.css | 16++++++++++++++++
Aelectrum.icns | 0
Mlib/gui_lite.py | 192++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mlib/history_widget.py | 9++++++---
Alib/receiving_widget.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/util.py | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Msetup-release.py | 2+-
Msetup.py | 1+
9 files changed, 262 insertions(+), 86 deletions(-)

diff --git a/README b/README @@ -33,10 +33,11 @@ On Mac OS X: # On port based installs sudo python setup-release.py py2app + # On brew installs - ARCHFLAGS="-arch i386 -arch x86_64" sudo /usr/bin/python setup-release.py py2app --includes sip + ARCHFLAGS="-arch i386 -arch x86_64" sudo python setup-release.py py2app --includes sip - sudo hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-v0.61-macosx.dmg + sudo hdiutil create -fs HFS+ -volname "Electrum" -srcfolder dist/Electrum.app dist/electrum-VERSION-macosx.dmg == BROWSER CONFIGURATION == diff --git a/data/cleanlook/style.css b/data/cleanlook/style.css @@ -34,6 +34,22 @@ MiniWindow QPushButton { min-height: 23px; padding: 2px; } +#receive_button +{ + color: #777; + border: 1px solid #CCC; + border-radius: 0px; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 white, stop: 1 #E6E6E6); + min-height: 25px; + min-width: 30px; +} +#receive_button[isActive=true] +{ + color: #575757; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 white, stop: 1 #D1D1D1); +} #address_input, #amount_input { diff --git a/electrum.icns b/electrum.icns Binary files differ. diff --git a/lib/gui_lite.py b/lib/gui_lite.py @@ -15,7 +15,6 @@ except ImportError: from decimal import Decimal as D -from interface import DEFAULT_SERVERS from util import get_resource_path as rsrc from i18n import _ import decimal @@ -27,8 +26,12 @@ import time import wallet import webbrowser import history_widget +import receiving_widget import util +import csv +import datetime +from wallet import format_satoshis import gui_qt import shutil @@ -91,7 +94,6 @@ class ElectrumGui(QObject): self.config = config self.check_qt_version() self.app = QApplication(sys.argv) - self.wallet.interface.register_callback('peers', self.server_list_changed) def check_qt_version(self): @@ -105,8 +107,6 @@ class ElectrumGui(QObject): def main(self, url): actuator = MiniActuator(self.wallet) - self.connect(self, SIGNAL("updateservers()"), - actuator.update_servers_list) # Should probably not modify the current path but instead # change the behaviour of rsrc(...) old_path = QDir.currentPath() @@ -130,9 +130,6 @@ class ElectrumGui(QObject): self.expert.update_wallet() self.app.exec_() - def server_list_changed(self): - self.emit(SIGNAL("updateservers()")) - def expand(self): """Hide the lite mode window and show pro-mode.""" self.mini.hide() @@ -175,10 +172,6 @@ class MiniWindow(QDialog): self.balance_label = BalanceLabel(self.change_quote_currency) self.balance_label.setObjectName("balance_label") - self.receive_button = QPushButton(_("&Receive")) - self.receive_button.setObjectName("receive_button") - self.receive_button.setDefault(True) - self.receive_button.clicked.connect(self.copy_address) # Bitcoin address code self.address_input = QLineEdit() @@ -218,12 +211,17 @@ class MiniWindow(QDialog): self.send_button.setDisabled(True); self.send_button.clicked.connect(self.send) + # Creating the receive button + self.receive_button = QPushButton(_("&Receive")) + self.receive_button.setObjectName("receive_button") + self.receive_button.setDefault(True) + main_layout = QGridLayout(self) main_layout.addWidget(self.balance_label, 0, 0) main_layout.addWidget(self.receive_button, 0, 1) - main_layout.addWidget(self.address_input, 1, 0, 1, -1) + main_layout.addWidget(self.address_input, 1, 0) main_layout.addWidget(self.amount_input, 2, 0) main_layout.addWidget(self.send_button, 2, 1) @@ -232,15 +230,44 @@ class MiniWindow(QDialog): self.history_list.setObjectName("history") self.history_list.hide() self.history_list.setAlternatingRowColors(True) - main_layout.addWidget(self.history_list, 3, 0, 1, -1) + main_layout.addWidget(self.history_list, 3, 0, 1, 2) + + + self.receiving = receiving_widget.ReceivingWidget(self) + self.receiving.setObjectName("receiving") + + # Add to the right side + self.receiving_box = QGroupBox(_("Select a receiving address")) + extra_layout = QGridLayout() + + # Checkbox to filter used addresses + hide_used = QCheckBox(_('Hide used addresses')) + hide_used.setChecked(True) + hide_used.stateChanged.connect(self.receiving.toggle_used) + + # Events for receiving addresses + self.receiving.clicked.connect(self.receiving.copy_address) + self.receiving.itemDoubleClicked.connect(self.receiving.edit_label) + self.receiving.itemChanged.connect(self.receiving.update_label) + + # Label + 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) + + extra_layout.addWidget(self.receiving, 1,0) + extra_layout.addWidget(hide_used, 2,0) + extra_layout.setColumnMinimumWidth(0,200) + + self.receiving_box.setLayout(extra_layout) + main_layout.addWidget(self.receiving_box,0,3,-1,3) + self.receiving_box.hide() + + self.receive_button.clicked.connect(self.toggle_receiving_layout) + + # Creating the menu bar menubar = QMenuBar() electrum_menu = menubar.addMenu(_("&Bitcoin")) - servers_menu = electrum_menu.addMenu(_("&Servers")) - servers_group = QActionGroup(self) - self.actuator.set_servers_gui_stuff(servers_menu, servers_group) - self.actuator.populate_servers_menu() electrum_menu.addSeparator() brain_seed = electrum_menu.addAction(_("&BrainWallet Info")) @@ -254,6 +281,9 @@ class MiniWindow(QDialog): backup_wallet = extra_menu.addAction( _("&Create wallet backup")) backup_wallet.triggered.connect(self.backup_wallet) + export_csv = extra_menu.addAction( _("&Export transactions to CSV") ) + export_csv.triggered.connect(self.actuator.csv_transaction) + expert_gui = view_menu.addAction(_("&Classic GUI")) expert_gui.triggered.connect(expand_callback) themes_menu = view_menu.addMenu(_("&Themes")) @@ -288,6 +318,7 @@ class MiniWindow(QDialog): show_about = help_menu.addAction(_("&About")) show_about.triggered.connect(self.show_about) main_layout.setMenuBar(menubar) + self.main_layout = main_layout quit_shortcut = QShortcut(QKeySequence("Ctrl+Q"), self) quit_shortcut.activated.connect(self.close) @@ -308,6 +339,20 @@ class MiniWindow(QDialog): self.setObjectName("main_window") self.show() + def toggle_receiving_layout(self): + if self.receiving_box.isVisible(): + self.receiving_box.hide() + self.receive_button.setProperty("isActive", False) + + qApp.style().unpolish(self.receive_button) + qApp.style().polish(self.receive_button) + else: + self.receiving_box.show() + self.receive_button.setProperty("isActive", 'true') + + qApp.style().unpolish(self.receive_button) + qApp.style().polish(self.receive_button) + def toggle_theme(self, theme_name): old_path = QDir.currentPath() self.actuator.change_theme(theme_name) @@ -439,15 +484,19 @@ class MiniWindow(QDialog): def update_completions(self, completions): self.address_completions.setStringList(completions) + def update_history(self, tx_history): - from util import format_satoshis + from util import format_satoshis, age + + self.history_list.empty() + for item in tx_history[-10:]: tx_hash, conf, is_mine, value, fee, balance, timestamp = item label = self.actuator.wallet.get_label(tx_hash)[0] #amount = D(value) / 10**8 v_str = format_satoshis(value, True) - self.history_list.append(label, v_str) + self.history_list.append(label, v_str, age(timestamp)) def acceptbit(self): self.actuator.acceptbit(self.quote_currencies[0]) @@ -465,8 +514,10 @@ class MiniWindow(QDialog): def show_history(self, toggle_state): if toggle_state: + self.main_layout.setRowMinimumHeight(3,200) self.history_list.show() else: + self.main_layout.setRowMinimumHeight(3,0) self.history_list.hide() def backup_wallet(self): @@ -642,66 +693,6 @@ class MiniActuator: """Change the wallet fiat currency country.""" self.wallet.config.set_key('conversion_currency',conversion_currency,True) - def set_servers_gui_stuff(self, servers_menu, servers_group): - self.servers_menu = servers_menu - self.servers_group = servers_group - - def populate_servers_menu(self): - interface = self.wallet.interface - if not interface.servers: - print "No servers loaded yet." - self.servers_list = [] - for server_string in DEFAULT_SERVERS: - host, port, protocol = server_string.split(':') - transports = [(protocol,port)] - self.servers_list.append((host, transports)) - else: - print "Servers loaded." - self.servers_list = interface.servers - server_names = [details[0] for details in self.servers_list] - current_server = interface.server.split(":")[0] - for server_name in server_names: - server_action = self.servers_menu.addAction(server_name) - server_action.setCheckable(True) - if server_name == current_server: - server_action.setChecked(True) - class SelectServerFunctor: - def __init__(self, server_name, server_selected): - self.server_name = server_name - self.server_selected = server_selected - def __call__(self, checked): - if checked: - # call server_selected - self.server_selected(self.server_name) - delegate = SelectServerFunctor(server_name, self.server_selected) - server_action.toggled.connect(delegate) - self.servers_group.addAction(server_action) - - def update_servers_list(self): - # Clear servers_group - for action in self.servers_group.actions(): - self.servers_group.removeAction(action) - self.populate_servers_menu() - - def server_selected(self, server_name): - match = [transports for (host, transports) in self.servers_list - if host == server_name] - assert len(match) == 1 - match = match[0] - # Default to TCP if available else use anything - # TODO: protocol should be selectable. - tcp_port = [port for (protocol, port) in match if protocol == "t"] - if len(tcp_port) == 0: - protocol = match[0][0] - port = match[0][1] - else: - protocol = "t" - port = tcp_port[0] - server_line = "%s:%s:%s" % (server_name, port, protocol) - - # Should this have exception handling? - self.wallet.interface.set_server(server_line, self.wallet.config.get("proxy")) - def copy_address(self, receive_popup): """Copy the wallet addresses into the client.""" addrs = [addr for addr in self.wallet.all_addresses() @@ -732,6 +723,48 @@ class MiniActuator: w.exec_() w.destroy() + def csv_transaction(self): + try: + fileName = QFileDialog.getSaveFileName(QWidget(), 'Select file to export your wallet transactions to', os.path.expanduser('~/'), "*.csv") + if fileName: + with open(fileName, "w+") as csvfile: + transaction = csv.writer(csvfile) + transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"]) + for item in self.wallet.get_tx_history(): + tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item + if confirmations: + if timestamp is not None: + try: + time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] + except [RuntimeError, TypeError, NameError] as reason: + time_string = "unknown" + pass + else: + time_string = "unknown" + else: + time_string = "pending" + + if value is not None: + value_string = format_satoshis(value, True, self.wallet.num_zeros) + else: + value_string = '--' + + if fee is not None: + fee_string = format_satoshis(fee, True, self.wallet.num_zeros) + else: + fee_string = '0' + + if tx_hash: + label, is_default_label = self.wallet.get_label(tx_hash) + else: + label = "" + + balance_string = format_satoshis(balance, False, self.wallet.num_zeros) + transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string]) + QMessageBox.information(None,"CSV Export created", "Your CSV export has been succesfully created.") + except (IOError, os.error), reason: + QMessageBox.critical(None,"Unable to create csv", "Electrum was unable to produce a transaction export.\n" + str(reason)) + def send(self, address, amount, parent_window): """Send bitcoins to the target address.""" dest_address = self.fetch_destination(address) @@ -902,6 +935,7 @@ class MiniDriver(QObject): tx_history = self.wallet.get_tx_history() self.window.update_history(tx_history) + if __name__ == "__main__": app = QApplication(sys.argv) with open(rsrc("style.css")) as style_file: diff --git a/lib/history_widget.py b/lib/history_widget.py @@ -6,10 +6,13 @@ class HistoryWidget(QTreeWidget): def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.setColumnCount(2) - self.setHeaderLabels([_("Amount"), _("To / From")]) + self.setHeaderLabels([_("Amount"), _("To / From"), _("When")]) self.setIndentation(0) - def append(self, address, amount): - item = QTreeWidgetItem([amount, address]) + def empty(self): + self.clear() + + def append(self, address, amount, date): + item = QTreeWidgetItem([amount, address, date]) self.insertTopLevelItem(0, item) diff --git a/lib/receiving_widget.py b/lib/receiving_widget.py @@ -0,0 +1,72 @@ +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from i18n import _ + +class ReceivingWidget(QTreeWidget): + + def toggle_used(self): + if self.hide_used: + self.hide_used = False + self.setColumnHidden(2, False) + else: + self.hide_used = True + self.setColumnHidden(2, True) + self.update_list() + + def edit_label(self, item, column): + if column == 1 and item.isSelected(): + self.editing = True + item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + self.editItem(item, column) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + self.editing = False + + def update_label(self, item, column): + if self.editing: + return + else: + address = str(item.text(0)) + label = unicode( item.text(1) ) + self.owner.actuator.wallet.labels[address] = label + + def copy_address(self): + address = self.currentItem().text(0) + qApp.clipboard().setText(address) + + + def update_list(self): + + self.clear() + addresses = [addr for addr in self.owner.actuator.wallet.all_addresses() if not self.owner.actuator.wallet.is_change(addr)] + for address in addresses: + history = self.owner.actuator.wallet.history.get(address,[]) + + used = "No" + for tx_hash, tx_height in history: + tx = self.owner.actuator.wallet.transactions.get(tx_hash) + if tx: + used = "Yes" + + if(self.hide_used == True and used == "No") or self.hide_used == False: + label = self.owner.actuator.wallet.labels.get(address,'') + item = QTreeWidgetItem([address, label, used]) + self.insertTopLevelItem(0, item) + + def __init__(self, owner=None): + self.owner = owner + self.editing = False + + QTreeWidget.__init__(self, owner) + self.setColumnCount(3) + self.setHeaderLabels([_("Address"), _("Label"), _("Used")]) + self.setIndentation(0) + + self.hide_used = True + self.setColumnHidden(2, True) + self.update_list() + + + + + + diff --git a/lib/util.py b/lib/util.py @@ -1,8 +1,57 @@ import os, sys import platform - +from datetime import datetime is_verbose = True +# Takes a timestamp and puts out a string with the approxomation of the age +def age(from_date, since_date = None, target_tz=None, include_seconds=False): + if from_date is None: + return "Unknown" + + from_date = datetime.fromtimestamp(from_date) + if since_date is None: + since_date = datetime.now(target_tz) + + distance_in_time = since_date - from_date + distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds))) + distance_in_minutes = int(round(distance_in_seconds/60)) + + + if distance_in_minutes <= 1: + if include_seconds: + for remainder in [5, 10, 20]: + if distance_in_seconds < remainder: + return "less than %s seconds ago" % remainder + if distance_in_seconds < 40: + return "half a minute" + elif distance_in_seconds < 60: + return "less than a minute ago" + else: + return "1 minute ago" + else: + if distance_in_minutes == 0: + return "less than a minute ago" + else: + return "1 minute ago" + elif distance_in_minutes < 45: + return "%s minutes ago" % distance_in_minutes + elif distance_in_minutes < 90: + return "about 1 hour ago" + elif distance_in_minutes < 1440: + return "about %d hours ago" % (round(distance_in_minutes / 60.0)) + elif distance_in_minutes < 2880: + return "1 day ago" + elif distance_in_minutes < 43220: + return "%d days ago" % (round(distance_in_minutes / 1440)) + elif distance_in_minutes < 86400: + return "about 1 month ago" + elif distance_in_minutes < 525600: + return "%d months ago" % (round(distance_in_minutes / 43200)) + elif distance_in_minutes < 1051200: + return "about 1 year ago" + else: + return "over %d years ago" % (round(distance_in_minutes / 525600)) + def set_verbosity(b): global is_verbose is_verbose = b diff --git a/setup-release.py b/setup-release.py @@ -29,7 +29,7 @@ if sys.platform == 'darwin': app=[mainscript], options=dict(py2app=dict(argv_emulation=True, iconfile='electrum.icns', - resources=["data/background.png", "data/style.css", "data/icons"])), + resources=["data", "icons"])), ) elif sys.platform == 'win32': extra_options = dict( diff --git a/setup.py b/setup.py @@ -59,6 +59,7 @@ setup(name = "Electrum", 'electrum.pyqrnative', 'electrum.qrscanner', 'electrum.history_widget', + 'electrum.receiving_widget', 'electrum.simple_config', 'electrum.socks', 'electrum.bmp',