electrum

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

commit b8773178a19a0dbd1b154acb5903d33ab73bae8e
parent ca15ae68910c72bdb4deaf9645a16d4fbc459133
Author: thomasv <thomasv@gitorious>
Date:   Thu, 10 May 2012 14:38:49 +0200

setup package in lib subdirectory

Diffstat:
MMANIFEST.in | 1+
MREADME | 21+++++++++++++--------
Mblocks | 4++--
Melectrum | 10+++++-----
Dgui.py | 1297-------------------------------------------------------------------------------
Dgui_qt.py | 1222-------------------------------------------------------------------------------
Alib/__init__.py | 3+++
Rbmp.py -> lib/bmp.py | 0
Alib/gui.py | 1297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/gui_qt.py | 1222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rinterface.py -> lib/interface.py | 0
Rmnemonic.py -> lib/mnemonic.py | 0
Rmsqr.py -> lib/msqr.py | 0
Rpyqrnative.py -> lib/pyqrnative.py | 0
Rripemd.py -> lib/ripemd.py | 0
Alib/version.py | 2++
Rwallet.py -> lib/wallet.py | 0
Mpeers | 4++--
Msetup.py | 7+++++--
Dversion.py | 2--
Mwatch_address | 6++++--
21 files changed, 2556 insertions(+), 2542 deletions(-)

diff --git a/MANIFEST.in b/MANIFEST.in @@ -2,6 +2,7 @@ include README LICENCE RELEASE-NOTES include *.py include electrum exclude setup.py +recursive-include lib *.py recursive-include ecdsa *.py recursive-include aes *.py include icons.qrc diff --git a/README b/README @@ -30,17 +30,22 @@ In order to use the gtk gui, you need pygtk and tk. * apt-get install python-tk +To install Electrum, type: + + sudo python setup.py install + + RUN -To start the Qt gui, type: - python electrum -To use the Gtk gui, type: - python electrum --gui gtk +To start Electrum in GUI mode, type: + + electrum + + +If arguments are passed to the command line, Electrum will run in text mode: -If arguments are passed to the command line, Electrum will run in text mode. -Examples: - python electrum balance - python electrum help + electrum balance + electrum help diff --git a/blocks b/blocks @@ -1,8 +1,8 @@ #!/usr/bin/env python -import interface +from electrum import TcpStratumInterface -i = interface.TcpStratumInterface('ecdsa.org', 50001) +i = TcpStratumInterface('ecdsa.org', 50001) i.start() i.send([('blockchain.numblocks.subscribe',[])]) diff --git a/electrum b/electrum @@ -18,11 +18,11 @@ import re, sys, getpass +import electrum from optparse import OptionParser -from wallet import Wallet, SecretToASecret -from interface import WalletSynchronizer from decimal import Decimal -from wallet import format_satoshis + +from electrum import Wallet, SecretToASecret, WalletSynchronizer, format_satoshis known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'restore', 'payto', 'sendtx', 'password', 'addresses', 'history', 'label', 'mktx','seed','import','signmessage','verifymessage','eval'] offline_commands = ['password', 'mktx', 'history', 'label', 'contacts', 'help', 'validateaddress', 'signmessage', 'verifymessage', 'eval', 'create', 'addresses', 'import', 'seed'] @@ -60,9 +60,9 @@ if __name__ == '__main__': if cmd == 'gui': if options.gui=='gtk': - import gui + import electrum.gui as gui elif options.gui=='qt': - import gui_qt as gui + import electrum.gui_qt as gui else: print "unknown gui", options.gui exit(1) diff --git a/gui.py b/gui.py @@ -1,1297 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2011 thomasv@gitorious -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import datetime -import thread, time, ast, sys, re -import socket, traceback -import pygtk -pygtk.require('2.0') -import gtk, gobject -import pyqrnative -from decimal import Decimal - -gtk.gdk.threads_init() -APP_NAME = "Electrum" -import platform -MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' - -from wallet import format_satoshis -from interface import DEFAULT_SERVERS - -def numbify(entry, is_int = False): - text = entry.get_text().strip() - chars = '0123456789' - if not is_int: chars +='.' - s = ''.join([i for i in text if i in chars]) - if not is_int: - if '.' in s: - p = s.find('.') - s = s.replace('.','') - s = s[:p] + '.' + s[p:p+8] - try: - amount = int( Decimal(s) * 100000000 ) - except: - amount = None - else: - try: - amount = int( s ) - except: - amount = None - entry.set_text(s) - return amount - - - - -def show_seed_dialog(wallet, password, parent): - import mnemonic - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - show_message("Incorrect password") - return - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_OK, - message_format = "Your wallet generation seed is:\n\n" + seed \ - + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \ - + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" ) - dialog.set_title("Seed") - dialog.show() - dialog.run() - dialog.destroy() - -def restore_create_dialog(wallet): - - # ask if the user wants to create a new wallet, or recover from a seed. - # if he wants to recover, and nothing is found, do not create wallet - dialog = gtk.Dialog("electrum", parent=None, - flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, - buttons= ("create", 0, "restore",1, "cancel",2) ) - - label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" ) - label.show() - dialog.vbox.pack_start(label) - dialog.show() - r = dialog.run() - dialog.destroy() - - if r==2: return False - - is_recovery = (r==1) - - # ask for the server. - if not run_network_dialog( wallet, parent=None ): return False - - if not is_recovery: - - wallet.new_seed(None) - # generate first key - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.update() - - # run a dialog indicating the seed, ask the user to remember it - show_seed_dialog(wallet, None, None) - - #ask for password - change_password_dialog(wallet, None, None) - else: - # ask for seed and gap. - run_recovery_dialog( wallet ) - - dialog = gtk.MessageDialog( - parent = None, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_CANCEL, - message_format = "Please wait..." ) - dialog.show() - - def recover_thread( wallet, dialog ): - wallet.init_mpk( wallet.seed ) # not encrypted at this point - wallet.up_to_date_event.clear() - wallet.update() - - if wallet.is_found(): - # history and addressbook - wallet.update_tx_history() - wallet.fill_addressbook() - print "recovery successful" - - gobject.idle_add( dialog.destroy ) - - thread.start_new_thread( recover_thread, ( wallet, dialog ) ) - r = dialog.run() - dialog.destroy() - if r==gtk.RESPONSE_CANCEL: return False - if not wallet.is_found: - show_message("No transactions found for this seed") - - wallet.save() - return True - - -def run_recovery_dialog(wallet): - message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." - dialog = gtk.MessageDialog( - parent = None, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_OK_CANCEL, - message_format = message) - - vbox = dialog.vbox - dialog.set_default_response(gtk.RESPONSE_OK) - - # ask seed, server and gap in the same dialog - seed_box = gtk.HBox() - seed_label = gtk.Label('Seed or mnemonic:') - seed_label.set_size_request(150,-1) - seed_box.pack_start(seed_label, False, False, 10) - seed_label.show() - seed_entry = gtk.Entry() - seed_entry.show() - seed_entry.set_size_request(450,-1) - seed_box.pack_start(seed_entry, False, False, 10) - add_help_button(seed_box, '.') - seed_box.show() - vbox.pack_start(seed_box, False, False, 5) - - gap = gtk.HBox() - gap_label = gtk.Label('Gap limit:') - gap_label.set_size_request(150,10) - gap_label.show() - gap.pack_start(gap_label,False, False, 10) - gap_entry = gtk.Entry() - gap_entry.set_text("%d"%wallet.gap_limit) - gap_entry.connect('changed', numbify, True) - gap_entry.show() - gap.pack_start(gap_entry,False,False, 10) - add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.') - gap.show() - vbox.pack_start(gap, False,False, 5) - - dialog.show() - r = dialog.run() - gap = gap_entry.get_text() - seed = seed_entry.get_text() - dialog.destroy() - - if r==gtk.RESPONSE_CANCEL: - sys.exit(1) - try: - gap = int(gap) - except: - show_message("error") - sys.exit(1) - - try: - seed.decode('hex') - except: - import mnemonic - print "not hex, trying decode" - seed = mnemonic.mn_decode( seed.split(' ') ) - if not seed: - show_message("no seed") - sys.exit(1) - - wallet.seed = seed - wallet.gap_limit = gap - wallet.save() - - - -def run_settings_dialog(wallet, parent): - - message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field." - - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_OK_CANCEL, - message_format = message) - - image = gtk.Image() - image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG) - image.show() - dialog.set_image(image) - dialog.set_title("Settings") - - vbox = dialog.vbox - dialog.set_default_response(gtk.RESPONSE_OK) - - fee = gtk.HBox() - fee_entry = gtk.Entry() - fee_label = gtk.Label('Transaction fee:') - fee_label.set_size_request(150,10) - fee_label.show() - fee.pack_start(fee_label,False, False, 10) - fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) ) - fee_entry.connect('changed', numbify, False) - fee_entry.show() - fee.pack_start(fee_entry,False,False, 10) - add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005') - fee.show() - vbox.pack_start(fee, False,False, 5) - - nz = gtk.HBox() - nz_entry = gtk.Entry() - nz_label = gtk.Label('Display zeros:') - nz_label.set_size_request(150,10) - nz_label.show() - nz.pack_start(nz_label,False, False, 10) - nz_entry.set_text( str( wallet.num_zeros )) - nz_entry.connect('changed', numbify, True) - nz_entry.show() - nz.pack_start(nz_entry,False,False, 10) - add_help_button(nz, "Number of zeros displayed after the decimal point.\nFor example, if this number is 2, then '5.' is displayed as '5.00'") - nz.show() - vbox.pack_start(nz, False,False, 5) - - dialog.show() - r = dialog.run() - fee = fee_entry.get_text() - nz = nz_entry.get_text() - - dialog.destroy() - if r==gtk.RESPONSE_CANCEL: - return - - try: - fee = int( 100000000 * Decimal(fee) ) - except: - show_message("error") - return - if wallet.fee != fee: - wallet.fee = fee - wallet.save() - - try: - nz = int( nz ) - if nz>8: nz = 8 - except: - show_message("error") - return - if wallet.num_zeros != nz: - wallet.num_zeros = nz - wallet.save() - - - - -def run_network_dialog( wallet, parent ): - image = gtk.Image() - image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG) - interface = wallet.interface - if parent: - if interface.is_connected: - status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks) - else: - status = "Not connected" - server = wallet.server - else: - import random - status = "Please choose a server." - server = random.choice( DEFAULT_SERVERS ) - - if not wallet.interface.servers: - servers_list = [] - for x in DEFAULT_SERVERS: - h,port,protocol = x.split(':') - servers_list.append( (h,[(protocol,port)] ) ) - else: - servers_list = wallet.interface.servers - - plist = {} - for item in servers_list: - host, pp = item - z = {} - for item2 in pp: - protocol, port = item2 - z[protocol] = port - plist[host] = z - - dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status) - dialog.set_title("Server") - dialog.set_image(image) - image.show() - - vbox = dialog.vbox - host_box = gtk.HBox() - host_label = gtk.Label('Connect to:') - host_label.set_size_request(100,-1) - host_label.show() - host_box.pack_start(host_label, False, False, 10) - host_entry = gtk.Entry() - host_entry.set_size_request(200,-1) - host_entry.set_text(server) - host_entry.show() - host_box.pack_start(host_entry, False, False, 10) - add_help_button(host_box, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, port 50000 will be tried. Some servers allow you to connect through http (port 80) or https (port 443)') - host_box.show() - - - p_box = gtk.HBox(False, 10) - p_box.show() - - p_label = gtk.Label('Protocol:') - p_label.set_size_request(100,-1) - p_label.show() - p_box.pack_start(p_label, False, False, 10) - - radio1 = gtk.RadioButton(None, "tcp") - p_box.pack_start(radio1, True, True, 0) - radio1.show() - radio2 = gtk.RadioButton(radio1, "http") - p_box.pack_start(radio2, True, True, 0) - radio2.show() - - def current_line(): - return unicode(host_entry.get_text()).split(':') - - def set_button(protocol): - if protocol == 't': - radio1.set_active(1) - elif protocol == 'h': - radio2.set_active(1) - - def set_protocol(protocol): - host = current_line()[0] - pp = plist[host] - if protocol not in pp.keys(): - protocol = pp.keys()[0] - set_button(protocol) - port = pp[protocol] - host_entry.set_text( host + ':' + port + ':' + protocol) - - radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1") - radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1") - - server_list = gtk.ListStore(str) - for host in plist.keys(): - server_list.append([host]) - - treeview = gtk.TreeView(model=server_list) - treeview.show() - - if wallet.interface.servers: - label = 'Active Servers' - else: - label = 'Default Servers' - - tvcolumn = gtk.TreeViewColumn(label) - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 0) - - scroll = gtk.ScrolledWindow() - scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scroll.add(treeview) - scroll.show() - - vbox.pack_start(host_box, False,False, 5) - vbox.pack_start(p_box, True, True, 0) - vbox.pack_start(scroll) - - def my_treeview_cb(treeview): - path, view_column = treeview.get_cursor() - host = server_list.get_value( server_list.get_iter(path), 0) - - pp = plist[host] - if 't' in pp.keys(): - protocol = 't' - else: - protocol = pp.keys()[0] - port = pp[protocol] - host_entry.set_text( host + ':' + port + ':' + protocol) - set_button(protocol) - - treeview.connect('cursor-changed', my_treeview_cb) - - dialog.show() - r = dialog.run() - server = host_entry.get_text() - dialog.destroy() - - if r==gtk.RESPONSE_CANCEL: - return False - - try: - wallet.set_server(server) - except: - show_message("error:" + server) - return False - - if parent: - wallet.save() - return True - - - -def show_message(message, parent=None): - dialog = gtk.MessageDialog( - parent = parent, - flags = gtk.DIALOG_MODAL, - buttons = gtk.BUTTONS_CLOSE, - message_format = message ) - dialog.show() - dialog.run() - dialog.destroy() - -def password_line(label): - password = gtk.HBox() - password_label = gtk.Label(label) - password_label.set_size_request(120,10) - password_label.show() - password.pack_start(password_label,False, False, 10) - password_entry = gtk.Entry() - password_entry.set_size_request(300,-1) - password_entry.set_visibility(False) - password_entry.show() - password.pack_start(password_entry,False,False, 10) - password.show() - return password, password_entry - -def password_dialog(parent): - dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.") - dialog.get_image().set_visible(False) - current_pw, current_pw_entry = password_line('Password:') - current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK) - dialog.vbox.pack_start(current_pw, False, True, 0) - dialog.show() - result = dialog.run() - pw = current_pw_entry.get_text() - dialog.destroy() - if result != gtk.RESPONSE_CANCEL: return pw - -def change_password_dialog(wallet, parent, icon): - if parent: - msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' - else: - msg = "Please choose a password to encrypt your wallet keys" - - dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) - dialog.set_title("Change password") - image = gtk.Image() - image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG) - image.show() - dialog.set_image(image) - - if wallet.use_encryption: - current_pw, current_pw_entry = password_line('Current password:') - dialog.vbox.pack_start(current_pw, False, True, 0) - - password, password_entry = password_line('New password:') - dialog.vbox.pack_start(password, False, True, 5) - password2, password2_entry = password_line('Confirm password:') - dialog.vbox.pack_start(password2, False, True, 5) - - dialog.show() - result = dialog.run() - password = current_pw_entry.get_text() if wallet.use_encryption else None - new_password = password_entry.get_text() - new_password2 = password2_entry.get_text() - dialog.destroy() - if result == gtk.RESPONSE_CANCEL: - return - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - show_message("Incorrect password") - return - - if new_password != new_password2: - show_message("passwords do not match") - return - - wallet.update_password(seed, new_password) - - if icon: - if wallet.use_encryption: - icon.set_tooltip_text('wallet is encrypted') - else: - icon.set_tooltip_text('wallet is unencrypted') - - -def add_help_button(hbox, message): - button = gtk.Button('?') - button.connect("clicked", lambda x: show_message(message)) - button.show() - hbox.pack_start(button,False, False) - - -class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) ) - -gobject.type_register(MyWindow) -gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W') -gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q') - - -class ElectrumWindow: - - def show_message(self, msg): - show_message(msg, self.window) - - def __init__(self, wallet): - self.wallet = wallet - self.funds_error = False # True if not enough funds - - self.window = MyWindow(gtk.WINDOW_TOPLEVEL) - self.window.set_title(APP_NAME + " " + self.wallet.electrum_version) - self.window.connect("destroy", gtk.main_quit) - self.window.set_border_width(0) - self.window.connect('mykeypress', gtk.main_quit) - self.window.set_default_size(720, 350) - - vbox = gtk.VBox() - - self.notebook = gtk.Notebook() - self.create_history_tab() - self.create_send_tab() - self.create_recv_tab() - self.create_book_tab() - self.create_about_tab() - self.notebook.show() - vbox.pack_start(self.notebook, True, True, 2) - - self.status_bar = gtk.Statusbar() - vbox.pack_start(self.status_bar, False, False, 0) - - self.status_image = gtk.Image() - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - self.status_image.set_alignment(True, 0.5 ) - self.status_image.show() - - self.network_button = gtk.Button() - self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) ) - self.network_button.add(self.status_image) - self.network_button.set_relief(gtk.RELIEF_NONE) - self.network_button.show() - self.status_bar.pack_end(self.network_button, False, False) - - def seedb(w, wallet): - if wallet.use_encryption: - password = password_dialog(self.window) - if not password: return - else: password = None - show_seed_dialog(wallet, password, self.window) - button = gtk.Button('S') - button.connect("clicked", seedb, wallet ) - button.set_relief(gtk.RELIEF_NONE) - button.show() - self.status_bar.pack_end(button,False, False) - - settings_icon = gtk.Image() - settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) - settings_icon.set_alignment(0.5, 0.5) - settings_icon.set_size_request(16,16 ) - settings_icon.show() - - prefs_button = gtk.Button() - prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) ) - prefs_button.add(settings_icon) - prefs_button.set_tooltip_text("Settings") - prefs_button.set_relief(gtk.RELIEF_NONE) - prefs_button.show() - self.status_bar.pack_end(prefs_button,False,False) - - pw_icon = gtk.Image() - pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) - pw_icon.set_alignment(0.5, 0.5) - pw_icon.set_size_request(16,16 ) - pw_icon.show() - - password_button = gtk.Button() - password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon)) - password_button.add(pw_icon) - password_button.set_relief(gtk.RELIEF_NONE) - password_button.show() - self.status_bar.pack_end(password_button,False,False) - - self.window.add(vbox) - self.window.show_all() - #self.fee_box.hide() - - self.context_id = self.status_bar.get_context_id("statusbar") - self.update_status_bar() - - def update_status_bar_thread(): - while True: - gobject.idle_add( self.update_status_bar ) - time.sleep(0.5) - - - def check_recipient_thread(): - old_r = '' - while True: - time.sleep(0.5) - if self.payto_entry.is_focus(): - continue - r = self.payto_entry.get_text() - if r != old_r: - old_r = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = self.wallet.get_alias(r, interactive=False) - except: - continue - if to_address: - s = r + ' <' + to_address + '>' - gobject.idle_add( lambda: self.payto_entry.set_text(s) ) - - - thread.start_new_thread(update_status_bar_thread, ()) - thread.start_new_thread(check_recipient_thread, ()) - self.notebook.set_current_page(0) - - - def add_tab(self, page, name): - tab_label = gtk.Label(name) - tab_label.show() - self.notebook.append_page(page, tab_label) - - - def create_send_tab(self): - - page = vbox = gtk.VBox() - page.show() - - payto = gtk.HBox() - payto_label = gtk.Label('Pay to:') - payto_label.set_size_request(100,-1) - payto.pack_start(payto_label, False) - payto_entry = gtk.Entry() - payto_entry.set_size_request(450, 26) - payto.pack_start(payto_entry, False) - vbox.pack_start(payto, False, False, 5) - - message = gtk.HBox() - message_label = gtk.Label('Description:') - message_label.set_size_request(100,-1) - message.pack_start(message_label, False) - message_entry = gtk.Entry() - message_entry.set_size_request(450, 26) - message.pack_start(message_entry, False) - vbox.pack_start(message, False, False, 5) - - amount_box = gtk.HBox() - amount_label = gtk.Label('Amount:') - amount_label.set_size_request(100,-1) - amount_box.pack_start(amount_label, False) - amount_entry = gtk.Entry() - amount_entry.set_size_request(120, -1) - amount_box.pack_start(amount_entry, False) - vbox.pack_start(amount_box, False, False, 5) - - self.fee_box = fee_box = gtk.HBox() - fee_label = gtk.Label('Fee:') - fee_label.set_size_request(100,-1) - fee_box.pack_start(fee_label, False) - fee_entry = gtk.Entry() - fee_entry.set_size_request(60, 26) - fee_box.pack_start(fee_entry, False) - vbox.pack_start(fee_box, False, False, 5) - - end_box = gtk.HBox() - empty_label = gtk.Label('') - empty_label.set_size_request(100,-1) - end_box.pack_start(empty_label, False) - send_button = gtk.Button("Send") - send_button.show() - end_box.pack_start(send_button, False, False, 0) - clear_button = gtk.Button("Clear") - clear_button.show() - end_box.pack_start(clear_button, False, False, 15) - send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry)) - clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry)) - - vbox.pack_start(end_box, False, False, 5) - - # display this line only if there is a signature - payto_sig = gtk.HBox() - payto_sig_id = gtk.Label('') - payto_sig.pack_start(payto_sig_id, False) - vbox.pack_start(payto_sig, True, True, 5) - - - self.user_fee = False - - def entry_changed( entry, is_fee ): - self.funds_error = False - amount = numbify(amount_entry) - fee = numbify(fee_entry) - if not is_fee: fee = None - if amount is None: - return - inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) - if not is_fee: - fee_entry.set_text( str( Decimal( fee ) / 100000000 ) ) - self.fee_box.show() - if inputs: - amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) - fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) - send_button.set_sensitive(True) - else: - send_button.set_sensitive(False) - amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) - fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) - self.funds_error = True - - amount_entry.connect('changed', entry_changed, False) - fee_entry.connect('changed', entry_changed, True) - - self.payto_entry = payto_entry - self.payto_fee_entry = fee_entry - self.payto_sig_id = payto_sig_id - self.payto_sig = payto_sig - self.amount_entry = amount_entry - self.message_entry = message_entry - self.add_tab(page, 'Send') - - def set_frozen(self,entry,frozen): - if frozen: - entry.set_editable(False) - entry.set_has_frame(False) - entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee")) - else: - entry.set_editable(True) - entry.set_has_frame(True) - entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff")) - - def set_url(self, url): - payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) - self.notebook.set_current_page(1) - self.payto_entry.set_text(payto) - self.message_entry.set_text(message) - self.amount_entry.set_text(amount) - if identity: - self.set_frozen(self.payto_entry,True) - self.set_frozen(self.amount_entry,True) - self.set_frozen(self.message_entry,True) - self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity ) - else: - self.payto_sig.set_visible(False) - - def create_about_tab(self): - import pango - page = gtk.VBox() - page.show() - tv = gtk.TextView() - tv.set_editable(False) - tv.set_cursor_visible(False) - tv.modify_font(pango.FontDescription(MONOSPACE_FONT)) - page.pack_start(tv) - self.info = tv.get_buffer() - self.add_tab(page, 'Wall') - - def do_clear(self, w, data): - self.payto_sig.set_visible(False) - self.payto_fee_entry.set_text('') - for entry in [self.payto_entry,self.amount_entry,self.message_entry]: - self.set_frozen(entry,False) - entry.set_text('') - - def question(self,msg): - dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) - dialog.show() - result = dialog.run() - dialog.destroy() - return result == gtk.RESPONSE_OK - - def do_send(self, w, data): - payto_entry, label_entry, amount_entry, fee_entry = data - label = label_entry.get_text() - r = payto_entry.get_text() - r = r.strip() - - m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) - m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) - - if m1: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - if not to_address: - return - else: - self.update_sending_tab() - - elif m2: - to_address = m2.group(5) - else: - to_address = r - - if not self.wallet.is_valid(to_address): - self.show_message( "invalid bitcoin address:\n"+to_address) - return - - try: - amount = int( Decimal(amount_entry.get_text()) * 100000000 ) - except: - self.show_message( "invalid amount") - return - try: - fee = int( Decimal(fee_entry.get_text()) * 100000000 ) - except: - self.show_message( "invalid fee") - return - - if self.wallet.use_encryption: - password = password_dialog(self.window) - if not password: - return - else: - password = None - - try: - tx = self.wallet.mktx( to_address, amount, label, password, fee ) - except BaseException, e: - self.show_message(e.message) - return - - status, msg = self.wallet.sendtx( tx ) - if status: - self.show_message( "payment sent.\n" + msg ) - payto_entry.set_text("") - label_entry.set_text("") - amount_entry.set_text("") - fee_entry.set_text("") - #self.fee_box.hide() - self.update_sending_tab() - else: - self.show_message( msg ) - - - def treeview_button_press(self, treeview, event): - if event.type == gtk.gdk._2BUTTON_PRESS: - c = treeview.get_cursor()[0] - if treeview == self.history_treeview: - tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) - self.show_message(tx_details) - elif treeview == self.contacts_treeview: - m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) - - - def treeview_key_press(self, treeview, event): - c = treeview.get_cursor()[0] - if event.keyval == gtk.keysyms.Up: - if c and c[0] == 0: - treeview.parent.grab_focus() - treeview.set_cursor((0,)) - elif event.keyval == gtk.keysyms.Return: - if treeview == self.history_treeview: - tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) - self.show_message(tx_details) - elif treeview == self.contacts_treeview: - m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self" - msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] - self.show_message(msg) - - return False - - def create_history_tab(self): - - self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str) - treeview = gtk.TreeView(model=self.history_list) - self.history_treeview = treeview - treeview.set_tooltip_column(7) - treeview.show() - treeview.connect('key-press-event', self.treeview_key_press) - treeview.connect('button-press-event', self.treeview_button_press) - - tvcolumn = gtk.TreeViewColumn('') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererPixbuf() - tvcolumn.pack_start(cell, False) - tvcolumn.set_attributes(cell, stock_id=1) - - tvcolumn = gtk.TreeViewColumn('Date') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 2) - - tvcolumn = gtk.TreeViewColumn('Description') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_property('foreground', 'grey') - cell.set_property('family', MONOSPACE_FONT) - cell.set_property('editable', True) - def edited_cb(cell, path, new_text, h_list): - tx = h_list.get_value( h_list.get_iter(path), 0) - self.wallet.labels[tx] = new_text - self.wallet.save() - self.update_history_tab() - cell.connect('edited', edited_cb, self.history_list) - def editing_started(cell, entry, path, h_list): - tx = h_list.get_value( h_list.get_iter(path), 0) - if not self.wallet.labels.get(tx): entry.set_text('') - cell.connect('editing-started', editing_started, self.history_list) - tvcolumn.set_expand(True) - tvcolumn.pack_start(cell, True) - tvcolumn.set_attributes(cell, text=3, foreground_set = 4) - - tvcolumn = gtk.TreeViewColumn('Amount') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_alignment(1, 0.5) - cell.set_property('family', MONOSPACE_FONT) - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 5) - - tvcolumn = gtk.TreeViewColumn('Balance') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_alignment(1, 0.5) - cell.set_property('family', MONOSPACE_FONT) - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 6) - - tvcolumn = gtk.TreeViewColumn('Tooltip') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, False) - tvcolumn.add_attribute(cell, 'text', 7) - tvcolumn.set_visible(False) - - scroll = gtk.ScrolledWindow() - scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) - scroll.add(treeview) - - self.add_tab(scroll, 'History') - self.update_history_tab() - - - def create_recv_tab(self): - self.recv_list = gtk.ListStore(str, str, str) - self.add_tab( self.make_address_list(True), 'Receive') - self.update_receiving_tab() - - def create_book_tab(self): - self.addressbook_list = gtk.ListStore(str, str, str) - self.add_tab( self.make_address_list(False), 'Contacts') - self.update_sending_tab() - - def make_address_list(self, is_recv): - liststore = self.recv_list if is_recv else self.addressbook_list - treeview = gtk.TreeView(model= liststore) - treeview.connect('key-press-event', self.treeview_key_press) - treeview.connect('button-press-event', self.treeview_button_press) - treeview.show() - if not is_recv: - self.contacts_treeview = treeview - - tvcolumn = gtk.TreeViewColumn('Address') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_property('family', MONOSPACE_FONT) - tvcolumn.pack_start(cell, True) - tvcolumn.add_attribute(cell, 'text', 0) - - tvcolumn = gtk.TreeViewColumn('Label') - tvcolumn.set_expand(True) - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - cell.set_property('editable', True) - def edited_cb2(cell, path, new_text, liststore): - address = liststore.get_value( liststore.get_iter(path), 0) - self.wallet.labels[address] = new_text - self.wallet.save() - self.wallet.update_tx_labels() - self.update_receiving_tab() - self.update_sending_tab() - self.update_history_tab() - cell.connect('edited', edited_cb2, liststore) - tvcolumn.pack_start(cell, True) - tvcolumn.add_attribute(cell, 'text', 1) - - tvcolumn = gtk.TreeViewColumn('Tx') - treeview.append_column(tvcolumn) - cell = gtk.CellRendererText() - tvcolumn.pack_start(cell, True) - tvcolumn.add_attribute(cell, 'text', 2) - - scroll = gtk.ScrolledWindow() - scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - scroll.add(treeview) - - hbox = gtk.HBox() - if not is_recv: - button = gtk.Button("New") - button.connect("clicked", self.newaddress_dialog) - button.show() - hbox.pack_start(button,False) - - def showqrcode(w, treeview, liststore): - path, col = treeview.get_cursor() - if not path: return - address = liststore.get_value(liststore.get_iter(path), 0) - qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H) - qr.addData(address) - qr.make() - boxsize = 7 - size = qr.getModuleCount()*boxsize - def area_expose_cb(area, event): - style = area.get_style() - k = qr.getModuleCount() - for r in range(k): - for c in range(k): - gc = style.black_gc if qr.isDark(r, c) else style.white_gc - area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize) - area = gtk.DrawingArea() - area.set_size_request(size, size) - area.connect("expose-event", area_expose_cb) - area.show() - dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1)) - dialog.vbox.add(area) - dialog.run() - dialog.destroy() - - button = gtk.Button("QR") - button.connect("clicked", showqrcode, treeview, liststore) - button.show() - hbox.pack_start(button,False) - - button = gtk.Button("Copy to clipboard") - def copy2clipboard(w, treeview, liststore): - import platform - path, col = treeview.get_cursor() - if path: - address = liststore.get_value( liststore.get_iter(path), 0) - if platform.system() == 'Windows': - from Tkinter import Tk - r = Tk() - r.withdraw() - r.clipboard_clear() - r.clipboard_append( address ) - r.destroy() - else: - c = gtk.clipboard_get() - c.set_text( address ) - button.connect("clicked", copy2clipboard, treeview, liststore) - button.show() - hbox.pack_start(button,False) - - if not is_recv: - button = gtk.Button("Pay to") - def payto(w, treeview, liststore): - path, col = treeview.get_cursor() - if path: - address = liststore.get_value( liststore.get_iter(path), 0) - self.payto_entry.set_text( address ) - self.notebook.set_current_page(1) - self.amount_entry.grab_focus() - - button.connect("clicked", payto, treeview, liststore) - button.show() - hbox.pack_start(button,False) - - vbox = gtk.VBox() - vbox.pack_start(scroll,True) - vbox.pack_start(hbox, False) - return vbox - - def update_status_bar(self): - interface = self.wallet.interface - if self.funds_error: - text = "Not enough funds" - elif interface.is_connected: - self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks)) - if self.wallet.blocks == -1: - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - text = "Connecting..." - elif self.wallet.blocks == 0: - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - text = "Server not ready" - elif not self.wallet.up_to_date: - self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU) - text = "Synchronizing..." - else: - self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) - self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks)) - c, u = self.wallet.get_balance() - text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) ) - if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() ) - else: - self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) - self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks)) - text = "Not connected" - - self.status_bar.pop(self.context_id) - self.status_bar.push(self.context_id, text) - - if self.wallet.was_updated and self.wallet.up_to_date: - self.update_history_tab() - self.update_receiving_tab() - # addressbook too... - self.info.set_text( self.wallet.banner ) - self.wallet.was_updated = False - - - def update_receiving_tab(self): - self.recv_list.clear() - for address in self.wallet.all_addresses(): - if self.wallet.is_change(address):continue - label = self.wallet.labels.get(address) - n = 0 - h = self.wallet.history.get(address,[]) - for item in h: - if not item['is_input'] : n=n+1 - tx = "None" if n==0 else "%d"%n - self.recv_list.append((address, label, tx )) - - def update_sending_tab(self): - # detect addresses that are not mine in history, add them here... - self.addressbook_list.clear() - for alias, v in self.wallet.aliases.items(): - s, target = v - label = self.wallet.labels.get(alias) - self.addressbook_list.append((alias, label, '-')) - - for address in self.wallet.addressbook: - label = self.wallet.labels.get(address) - n = 0 - for item in self.wallet.tx_history.values(): - if address in item['outputs'] : n=n+1 - tx = "None" if n==0 else "%d"%n - self.addressbook_list.append((address, label, tx)) - - def update_history_tab(self): - cursor = self.history_treeview.get_cursor()[0] - self.history_list.clear() - balance = 0 - for tx in self.wallet.get_tx_history(): - tx_hash = tx['tx_hash'] - if tx['height']: - conf = self.wallet.blocks - tx['height'] + 1 - time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] - conf_icon = gtk.STOCK_APPLY - else: - conf = 0 - time_str = 'pending' - conf_icon = gtk.STOCK_EXECUTE - v = tx['value'] - balance += v - label = self.wallet.labels.get(tx_hash) - is_default_label = (label == '') or (label is None) - if is_default_label: label = tx['default_label'] - tooltip = tx_hash + "\n%d confirmations"%conf - - # tx = self.wallet.tx_history.get(tx_hash) - details = "Transaction Details:\n\n" \ - + "Transaction ID:\n" + tx_hash + "\n\n" \ - + "Status: %d confirmations\n\n"%conf \ - + "Date: %s\n\n"%time_str \ - + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ - + "Outputs:\n-"+ '\n-'.join(tx['outputs']) - r = self.wallet.receipts.get(tx_hash) - if r: - details += "\n_______________________________________" \ - + '\n\nSigned URI: ' + r[2] \ - + "\n\nSigned by: " + r[0] \ - + '\n\nSignature: ' + r[1] - - - self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, - format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] ) - if cursor: self.history_treeview.set_cursor( cursor ) - - - - def newaddress_dialog(self, w): - - title = "New Contact" - dialog = gtk.Dialog(title, parent=self.window, - flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, - buttons= ("cancel", 0, "ok",1) ) - dialog.show() - - label = gtk.HBox() - label_label = gtk.Label('Label:') - label_label.set_size_request(120,10) - label_label.show() - label.pack_start(label_label) - label_entry = gtk.Entry() - label_entry.show() - label.pack_start(label_entry) - label.show() - dialog.vbox.pack_start(label, False, True, 5) - - address = gtk.HBox() - address_label = gtk.Label('Address:') - address_label.set_size_request(120,10) - address_label.show() - address.pack_start(address_label) - address_entry = gtk.Entry() - address_entry.show() - address.pack_start(address_entry) - address.show() - dialog.vbox.pack_start(address, False, True, 5) - - result = dialog.run() - address = address_entry.get_text() - label = label_entry.get_text() - dialog.destroy() - - if result == 1: - if self.wallet.is_valid(address): - self.wallet.addressbook.append(address) - if label: self.wallet.labels[address] = label - self.wallet.save() - self.update_sending_tab() - else: - errorDialog = gtk.MessageDialog( - parent=self.window, - flags=gtk.DIALOG_MODAL, - buttons= gtk.BUTTONS_CLOSE, - message_format = "Invalid address") - errorDialog.show() - errorDialog.run() - errorDialog.destroy() - - - -class ElectrumGui(): - - def __init__(self, wallet): - self.wallet = wallet - - def main(self, url=None): - ew = ElectrumWindow(self.wallet) - if url: ew.set_url(url) - gtk.main() - - def restore_or_create(self): - return restore_create_dialog(self.wallet) diff --git a/gui_qt.py b/gui_qt.py @@ -1,1222 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2012 thomasv@gitorious -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import sys, time, datetime, re - -# todo: see PySide - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import PyQt4.QtCore as QtCore -import PyQt4.QtGui as QtGui -from interface import DEFAULT_SERVERS - -try: - import icons_rc -except: - print "Could not import icons_rp.py" - print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'" - sys.exit(1) - -from wallet import format_satoshis -from decimal import Decimal - -import platform -MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' - - -def numbify(entry, is_int = False): - text = unicode(entry.text()).strip() - chars = '0123456789' - if not is_int: chars +='.' - s = ''.join([i for i in text if i in chars]) - if not is_int: - if '.' in s: - p = s.find('.') - s = s.replace('.','') - s = s[:p] + '.' + s[p:p+8] - try: - amount = int( Decimal(s) * 100000000 ) - except: - amount = None - else: - try: - amount = int( s ) - except: - amount = None - entry.setText(s) - return amount - - -class Timer(QtCore.QThread): - def run(self): - while True: - self.emit(QtCore.SIGNAL('timersignal')) - time.sleep(0.5) - -class EnterButton(QPushButton): - def __init__(self, text, func): - QPushButton.__init__(self, text) - self.func = func - self.clicked.connect(func) - - def keyPressEvent(self, e): - if e.key() == QtCore.Qt.Key_Return: - apply(self.func,()) - -class StatusBarButton(QPushButton): - def __init__(self, icon, tooltip, func): - QPushButton.__init__(self, icon, '') - self.setToolTip(tooltip) - self.setFlat(True) - self.setMaximumWidth(25) - self.clicked.connect(func) - self.func = func - - def keyPressEvent(self, e): - if e.key() == QtCore.Qt.Key_Return: - apply(self.func,()) - - -class QRCodeWidget(QWidget): - - def __init__(self, addr): - super(QRCodeWidget, self).__init__() - self.setGeometry(300, 300, 350, 350) - self.set_addr(addr) - - def set_addr(self, addr): - import pyqrnative - self.addr = addr - self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L) - self.qr.addData(addr) - self.qr.make() - - def paintEvent(self, e): - qp = QtGui.QPainter() - qp.begin(self) - boxsize = 7 - size = self.qr.getModuleCount()*boxsize - k = self.qr.getModuleCount() - black = QColor(0, 0, 0, 255) - white = QColor(255, 255, 255, 255) - for r in range(k): - for c in range(k): - if self.qr.isDark(r, c): - qp.setBrush(black) - qp.setPen(black) - else: - qp.setBrush(white) - qp.setPen(white) - qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize) - qp.end() - - - -def ok_cancel_buttons(dialog): - hbox = QHBoxLayout() - hbox.addStretch(1) - b = QPushButton("OK") - hbox.addWidget(b) - b.clicked.connect(dialog.accept) - b = QPushButton("Cancel") - hbox.addWidget(b) - b.clicked.connect(dialog.reject) - return hbox - - -class ElectrumWindow(QMainWindow): - - def __init__(self, wallet): - QMainWindow.__init__(self) - self.wallet = wallet - self.wallet.gui_callback = self.update_callback - - self.funds_error = False - - self.tabs = tabs = QTabWidget(self) - tabs.addTab(self.create_history_tab(), 'History') - tabs.addTab(self.create_send_tab(), 'Send') - tabs.addTab(self.create_receive_tab(), 'Receive') - tabs.addTab(self.create_contacts_tab(),'Contacts') - tabs.addTab(self.create_wall_tab(), 'Wall') - tabs.setMinimumSize(600, 400) - tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.setCentralWidget(tabs) - self.create_status_bar() - self.setGeometry(100,100,840,400) - self.setWindowTitle( 'Electrum ' + self.wallet.electrum_version ) - self.show() - - QShortcut(QKeySequence("Ctrl+W"), self, self.close) - QShortcut(QKeySequence("Ctrl+Q"), self, self.close) - QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() )) - QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() )) - - self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet) - - - def connect_slots(self, sender): - self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient) - self.previous_payto_e='' - - def check_recipient(self): - if self.payto_e.hasFocus(): - return - r = unicode( self.payto_e.text() ) - if r != self.previous_payto_e: - self.previous_payto_e = r - r = r.strip() - if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): - try: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - except: - return - if to_address: - s = r + ' <' + to_address + '>' - self.payto_e.setText(s) - - - def update_callback(self): - self.emit(QtCore.SIGNAL('updatesignal')) - - def update_wallet(self): - if self.wallet.interface.is_connected: - if self.wallet.blocks == -1: - text = "Connecting..." - icon = QIcon(":icons/status_disconnected.png") - elif self.wallet.blocks == 0: - text = "Server not ready" - icon = QIcon(":icons/status_disconnected.png") - elif not self.wallet.up_to_date: - text = "Synchronizing..." - icon = QIcon(":icons/status_waiting.png") - else: - c, u = self.wallet.get_balance() - text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) ) - if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() ) - icon = QIcon(":icons/status_connected.png") - else: - text = "Not connected" - icon = QIcon(":icons/status_disconnected.png") - - if self.funds_error: - text = "Not enough funds" - - self.statusBar().showMessage(text) - self.status_button.setIcon( icon ) - - if self.wallet.up_to_date: - self.textbox.setText( self.wallet.banner ) - self.update_history_tab() - self.update_receive_tab() - self.update_contacts_tab() - - - def create_history_tab(self): - self.history_list = w = QTreeWidget(self) - #print w.getContentsMargins() - w.setColumnCount(5) - w.setColumnWidth(0, 40) - w.setColumnWidth(1, 140) - w.setColumnWidth(2, 350) - w.setColumnWidth(3, 140) - w.setColumnWidth(4, 140) - w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] ) - self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details) - self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked) - self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed) - return w - - def tx_details(self, item, column): - tx_hash = str(item.toolTip(0)) - tx = self.wallet.tx_history.get(tx_hash) - - if tx['height']: - conf = self.wallet.blocks - tx['height'] + 1 - time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] - else: - conf = 0 - time_str = 'pending' - - tx_details = "Transaction Details:\n\n" \ - + "Transaction ID:\n" + tx_hash + "\n\n" \ - + "Status: %d confirmations\n\n"%conf \ - + "Date: %s\n\n"%time_str \ - + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ - + "Outputs:\n-"+ '\n-'.join(tx['outputs']) - - r = self.wallet.receipts.get(tx_hash) - if r: - tx_details += "\n_______________________________________" \ - + '\n\nSigned URI: ' + r[2] \ - + "\n\nSigned by: " + r[0] \ - + '\n\nSignature: ' + r[1] - - QMessageBox.information(self, 'Details', tx_details, 'OK') - - - def tx_label_clicked(self, item, column): - if column==2 and item.isSelected(): - tx_hash = str(item.toolTip(0)) - self.is_edit=True - #if not self.wallet.labels.get(tx_hash): item.setText(2,'') - item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - self.history_list.editItem( item, column ) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - self.is_edit=False - - def tx_label_changed(self, item, column): - if self.is_edit: - return - self.is_edit=True - tx_hash = str(item.toolTip(0)) - tx = self.wallet.tx_history.get(tx_hash) - s = self.wallet.labels.get(tx_hash) - text = unicode( item.text(2) ) - if text: - self.wallet.labels[tx_hash] = text - item.setForeground(2, QBrush(QColor('black'))) - else: - if s: self.wallet.labels.pop(tx_hash) - text = tx['default_label'] - item.setText(2, text) - item.setForeground(2, QBrush(QColor('gray'))) - self.is_edit=False - - def address_label_clicked(self, item, column, l): - if column==1 and item.isSelected(): - addr = unicode( item.text(0) ) - if addr in map(lambda x:x[1], self.wallet.aliases.values()): - return - item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - l.editItem( item, column ) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) - - def address_label_changed(self, item, column, l): - addr = unicode( item.text(0) ) - text = unicode( item.text(1) ) - if text: - self.wallet.labels[addr] = text - else: - s = self.wallet.labels.get(addr) - if s: self.wallet.labels.pop(addr) - self.update_history_tab() - - def update_history_tab(self): - self.history_list.clear() - balance = 0 - for tx in self.wallet.get_tx_history(): - tx_hash = tx['tx_hash'] - if tx['height']: - conf = self.wallet.blocks - tx['height'] + 1 - time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] - icon = QIcon(":icons/confirmed.png") - else: - conf = 0 - time_str = 'pending' - icon = QIcon(":icons/unconfirmed.png") - v = tx['value'] - balance += v - label = self.wallet.labels.get(tx_hash) - is_default_label = (label == '') or (label is None) - if is_default_label: label = tx['default_label'] - - item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] ) - item.setFont(2, QFont(MONOSPACE_FONT)) - item.setFont(3, QFont(MONOSPACE_FONT)) - item.setFont(4, QFont(MONOSPACE_FONT)) - item.setToolTip(0, tx_hash) - if is_default_label: - item.setForeground(2, QBrush(QColor('grey'))) - - item.setIcon(0, icon) - self.history_list.insertTopLevelItem(0,item) - - - def create_send_tab(self): - w = QWidget() - - grid = QGridLayout() - grid.setSpacing(8) - grid.setColumnMinimumWidth(3,300) - grid.setColumnStretch(4,1) - - self.payto_e = QLineEdit() - grid.addWidget(QLabel('Pay to'), 1, 0) - grid.addWidget(self.payto_e, 1, 1, 1, 3) - - self.message_e = QLineEdit() - grid.addWidget(QLabel('Description'), 2, 0) - grid.addWidget(self.message_e, 2, 1, 1, 3) - - self.amount_e = QLineEdit() - grid.addWidget(QLabel('Amount'), 3, 0) - grid.addWidget(self.amount_e, 3, 1, 1, 2) - - self.fee_e = QLineEdit() - grid.addWidget(QLabel('Fee'), 4, 0) - grid.addWidget(self.fee_e, 4, 1, 1, 2) - - b = EnterButton("Send", self.do_send) - grid.addWidget(b, 5, 1) - - b = EnterButton("Clear",self.do_clear) - grid.addWidget(b, 5, 2) - - self.payto_sig = QLabel('') - grid.addWidget(self.payto_sig, 6, 0, 1, 4) - - w.setLayout(grid) - w.show() - - w2 = QWidget() - vbox = QVBoxLayout() - vbox.addWidget(w) - vbox.addStretch(1) - w2.setLayout(vbox) - - def entry_changed( is_fee ): - self.funds_error = False - amount = numbify(self.amount_e) - fee = numbify(self.fee_e) - if not is_fee: fee = None - if amount is None: - return - inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) - if not is_fee: - self.fee_e.setText( str( Decimal( fee ) / 100000000 ) ) - if inputs: - palette = QPalette() - palette.setColor(self.amount_e.foregroundRole(), QColor('black')) - else: - palette = QPalette() - palette.setColor(self.amount_e.foregroundRole(), QColor('red')) - self.funds_error = True - self.amount_e.setPalette(palette) - self.fee_e.setPalette(palette) - - self.amount_e.textChanged.connect(lambda: entry_changed(False) ) - self.fee_e.textChanged.connect(lambda: entry_changed(True) ) - - return w2 - - def do_send(self): - - label = unicode( self.message_e.text() ) - r = unicode( self.payto_e.text() ) - r = r.strip() - - m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) - m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) - - if m1: - to_address = self.wallet.get_alias(r, True, self.show_message, self.question) - if not to_address: - return - elif m2: - to_address = m2.group(5) - else: - to_address = r - - if not self.wallet.is_valid(to_address): - QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK') - return - - try: - amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 ) - except: - QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK') - return - try: - fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 ) - except: - QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK') - return - - if self.wallet.use_encryption: - password = self.password_dialog() - if not password: - return - else: - password = None - - try: - tx = self.wallet.mktx( to_address, amount, label, password, fee ) - except BaseException, e: - self.show_message(e.message) - return - - status, msg = self.wallet.sendtx( tx ) - if status: - QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK') - self.do_clear() - self.update_contacts_tab() - else: - QMessageBox.warning(self, 'Error', msg, 'OK') - - - def set_url(self, url): - payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) - self.tabs.setCurrentIndex(1) - self.payto_e.setText(payto) - self.message_e.setText(message) - self.amount_e.setText(amount) - if identity: - self.set_frozen(self.payto_e,True) - self.set_frozen(self.amount_e,True) - self.set_frozen(self.message_e,True) - self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity ) - else: - self.payto_sig.setVisible(False) - - def do_clear(self): - self.payto_sig.setVisible(False) - for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]: - e.setText('') - self.set_frozen(e,False) - - def set_frozen(self,entry,frozen): - if frozen: - entry.setReadOnly(True) - entry.setFrame(False) - palette = QPalette() - palette.setColor(entry.backgroundRole(), QColor('lightgray')) - entry.setPalette(palette) - else: - entry.setReadOnly(False) - entry.setFrame(True) - palette = QPalette() - palette.setColor(entry.backgroundRole(), QColor('white')) - entry.setPalette(palette) - - - - - def clear_buttons(self, hbox): - while hbox.count(): hbox.removeItem(hbox.itemAt(0)) - - def add_buttons(self, l, hbox, is_recv): - self.clear_buttons(hbox) - - i = l.currentItem() - if not i: return - addr = unicode( i.text(0) ) - - hbox.addWidget(EnterButton("QR",lambda: self.show_address_qrcode(addr))) - hbox.addWidget(EnterButton("Copy to Clipboard", lambda: self.app.clipboard().setText(addr))) - if is_recv: - def toggle_freeze(addr): - if addr in self.wallet.frozen_addresses: - self.wallet.frozen_addresses.remove(addr) - else: - self.wallet.frozen_addresses.append(addr) - self.wallet.save() - self.update_receive_tab() - - t = "Unfreeze" if addr in self.wallet.frozen_addresses else "Freeze" - hbox.addWidget(EnterButton(t, lambda: toggle_freeze(addr))) - - else: - def payto(addr): - if not addr:return - self.tabs.setCurrentIndex(1) - self.payto_e.setText(addr) - self.amount_e.setFocus() - hbox.addWidget(EnterButton('Pay to', lambda: payto(addr))) - hbox.addWidget(EnterButton("New", self.newaddress_dialog)) - hbox.addStretch(1) - - - def create_receive_tab(self): - l = QTreeWidget(self) - l.setColumnCount(4) - l.setColumnWidth(0, 350) - l.setColumnWidth(1, 330) - l.setColumnWidth(2, 100) - l.setColumnWidth(3, 10) - l.setHeaderLabels( ['Address', 'Label','Balance','Tx']) - - w = QWidget() - vbox = QVBoxLayout() - w.setLayout(vbox) - - vbox.setMargin(0) - vbox.setSpacing(0) - vbox.addWidget(l) - buttons = QWidget() - vbox.addWidget(buttons) - - hbox = QHBoxLayout() - hbox.setMargin(0) - hbox.setSpacing(0) - buttons.setLayout(hbox) - - self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) - self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) - self.connect(l, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda: self.add_buttons(l, hbox, True)) - self.receive_list = l - self.receive_buttons_hbox = hbox - return w - - def create_contacts_tab(self): - l = QTreeWidget(self) - l.setColumnCount(3) - l.setColumnWidth(0, 350) - l.setColumnWidth(1, 330) - l.setColumnWidth(2, 20) - l.setHeaderLabels( ['Address', 'Label','Tx']) - - w = QWidget() - vbox = QVBoxLayout() - w.setLayout(vbox) - - vbox.setMargin(0) - vbox.setSpacing(0) - vbox.addWidget(l) - buttons = QWidget() - vbox.addWidget(buttons) - - hbox = QHBoxLayout() - hbox.setMargin(0) - hbox.setSpacing(0) - buttons.setLayout(hbox) - - self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) - self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) - self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details) - self.connect(l, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda: self.add_buttons(l, hbox, False)) - - self.contacts_list = l - self.contacts_buttons_hbox = hbox - return w - - def update_receive_tab(self): - self.receive_list.clear() - self.clear_buttons(self.receive_buttons_hbox) - - for address in self.wallet.all_addresses(): - if self.wallet.is_change(address):continue - label = self.wallet.labels.get(address,'') - n = 0 - h = self.wallet.history.get(address,[]) - for item in h: - if not item['is_input'] : n=n+1 - tx = "None" if n==0 else "%d"%n - - c, u = self.wallet.get_addr_balance(address) - balance = format_satoshis( c + u, False, self.wallet.num_zeros ) - if address in self.wallet.frozen_addresses: - balance += '[F]' - - item = QTreeWidgetItem( [ address, label, balance, tx] ) - item.setFont(0, QFont(MONOSPACE_FONT)) - self.receive_list.addTopLevelItem(item) - - def show_contact_details(self, item, column): - m = unicode(item.text(0)) - a = self.wallet.aliases.get(m) - if a: - if a[0] in self.wallet.authorities.keys(): - s = self.wallet.authorities.get(a[0]) - else: - s = "self-signed" - msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] - QMessageBox.information(self, 'Alias', msg, 'OK') - - def update_contacts_tab(self): - self.contacts_list.clear() - self.clear_buttons(self.contacts_buttons_hbox) - - for alias, v in self.wallet.aliases.items(): - s, target = v - item = QTreeWidgetItem( [ target, alias, '-'] ) - self.contacts_list.addTopLevelItem(item) - - for address in self.wallet.addressbook: - label = self.wallet.labels.get(address,'') - n = 0 - for item in self.wallet.tx_history.values(): - if address in item['outputs'] : n=n+1 - tx = "None" if n==0 else "%d"%n - item = QTreeWidgetItem( [ address, label, tx] ) - item.setFont(0, QFont(MONOSPACE_FONT)) - self.contacts_list.addTopLevelItem(item) - - - def create_wall_tab(self): - self.textbox = textbox = QTextEdit(self) - textbox.setFont(QFont(MONOSPACE_FONT)) - textbox.setReadOnly(True) - return textbox - - def create_status_bar(self): - sb = QStatusBar() - sb.setFixedHeight(35) - sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) ) - sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) ) - sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) ) - self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) - sb.addPermanentWidget( self.status_button ) - self.setStatusBar(sb) - - def newaddress_dialog(self): - text, ok = QInputDialog.getText(self, 'New Contact', 'Address:') - address = unicode(text) - if ok: - if self.wallet.is_valid(address): - self.wallet.addressbook.append(address) - self.wallet.save() - self.update_contacts_tab() - else: - QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK') - - @staticmethod - def show_seed_dialog(wallet, parent=None): - import mnemonic - if wallet.use_encryption: - password = parent.password_dialog() - if not password: return - else: - password = None - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK') - return - - msg = "Your wallet generation seed is:\n\n" + seed \ - + "\n\nPlease keep it in a safe place; if you lose it,\nyou will not be able to restore your wallet.\n\n" \ - + "Equivalently, your wallet seed can be stored and\nrecovered with the following mnemonic code:\n\n\"" \ - + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n" - - d = QDialog(None) - d.setModal(1) - d.setWindowTitle("Seed") - d.setMinimumSize(400, 270) - - vbox = QVBoxLayout() - hbox = QHBoxLayout() - vbox2 = QVBoxLayout() - l = QLabel() - l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56)) - vbox2.addWidget(l) - vbox2.addStretch(1) - hbox.addLayout(vbox2) - hbox.addWidget(QLabel(msg)) - vbox.addLayout(hbox) - - hbox = QHBoxLayout() - hbox.addStretch(1) - - - if parent: - app = parent.app - else: - app = QApplication - - b = QPushButton("Copy to Clipboard") - b.clicked.connect(lambda: app.clipboard().setText(' '.join(mnemonic.mn_encode(seed)))) - hbox.addWidget(b) - b = QPushButton("View as QR Code") - b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed)) - hbox.addWidget(b) - - b = QPushButton("OK") - b.clicked.connect(d.accept) - hbox.addWidget(b) - vbox.addLayout(hbox) - d.setLayout(vbox) - d.exec_() - - @staticmethod - def show_seed_qrcode(seed): - if not seed: return - d = QDialog(None) - d.setModal(1) - d.setWindowTitle("Seed") - d.setMinimumSize(270, 300) - vbox = QVBoxLayout() - vbox.addWidget(QRCodeWidget(seed)) - hbox = QHBoxLayout() - hbox.addStretch(1) - b = QPushButton("OK") - hbox.addWidget(b) - b.clicked.connect(d.accept) - - vbox.addLayout(hbox) - d.setLayout(vbox) - d.exec_() - - def show_address_qrcode(self,address): - if not address: return - d = QDialog(None) - d.setModal(1) - d.setWindowTitle(address) - d.setMinimumSize(270, 350) - vbox = QVBoxLayout() - qrw = QRCodeWidget(address) - vbox.addWidget(qrw) - - hbox = QHBoxLayout() - amount_e = QLineEdit() - hbox.addWidget(QLabel('Amount')) - hbox.addWidget(amount_e) - vbox.addLayout(hbox) - - #hbox = QHBoxLayout() - #label_e = QLineEdit() - #hbox.addWidget(QLabel('Label')) - #hbox.addWidget(label_e) - #vbox.addLayout(hbox) - - def amount_changed(): - amount = numbify(amount_e) - #label = str( label_e.getText() ) - if amount is not None: - qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000))) - else: - qrw.set_addr( address ) - qrw.repaint() - - def do_save(): - import bmp - bmp.save_qrcode(qrw.qr, "qrcode.bmp") - self.show_message("QR code saved to file 'qrcode.bmp'") - - amount_e.textChanged.connect( amount_changed ) - - hbox = QHBoxLayout() - hbox.addStretch(1) - b = QPushButton("Save") - b.clicked.connect(do_save) - hbox.addWidget(b) - b = QPushButton("Close") - hbox.addWidget(b) - b.clicked.connect(d.accept) - - vbox.addLayout(hbox) - d.setLayout(vbox) - d.exec_() - - def question(self, msg): - return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes - - def show_message(self, msg): - QMessageBox.information(self, 'Message', msg, 'OK') - - def password_dialog(self ): - d = QDialog(self) - d.setModal(1) - - pw = QLineEdit() - pw.setEchoMode(2) - - vbox = QVBoxLayout() - msg = 'Please enter your password' - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - grid.addWidget(QLabel('Password'), 1, 0) - grid.addWidget(pw, 1, 1) - vbox.addLayout(grid) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - return unicode(pw.text()) - - @staticmethod - def change_password_dialog( wallet, parent=None ): - d = QDialog(parent) - d.setModal(1) - - pw = QLineEdit() - pw.setEchoMode(2) - new_pw = QLineEdit() - new_pw.setEchoMode(2) - conf_pw = QLineEdit() - conf_pw.setEchoMode(2) - - vbox = QVBoxLayout() - if parent: - msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' - else: - msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption." - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - - if wallet.use_encryption: - grid.addWidget(QLabel('Password'), 1, 0) - grid.addWidget(pw, 1, 1) - - grid.addWidget(QLabel('New Password'), 2, 0) - grid.addWidget(new_pw, 2, 1) - - grid.addWidget(QLabel('Confirm Password'), 3, 0) - grid.addWidget(conf_pw, 3, 1) - vbox.addLayout(grid) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - - password = unicode(pw.text()) if wallet.use_encryption else None - new_password = unicode(new_pw.text()) - new_password2 = unicode(conf_pw.text()) - - try: - seed = wallet.pw_decode( wallet.seed, password) - except: - QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK') - return - - if new_password != new_password2: - QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK') - return - - wallet.update_password(seed, new_password) - - @staticmethod - def seed_dialog(wallet, parent=None): - d = QDialog(parent) - d.setModal(1) - - vbox = QVBoxLayout() - msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - - seed_e = QLineEdit() - grid.addWidget(QLabel('Seed or mnemonic'), 1, 0) - grid.addWidget(seed_e, 1, 1) - - gap_e = QLineEdit() - gap_e.setText("5") - grid.addWidget(QLabel('Gap limit'), 2, 0) - grid.addWidget(gap_e, 2, 1) - gap_e.textChanged.connect(lambda: numbify(gap_e,True)) - vbox.addLayout(grid) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - - try: - gap = int(unicode(gap_e.text())) - except: - QMessageBox.warning(None, 'Error', 'error', 'OK') - sys.exit(0) - - try: - seed = unicode(seed_e.text()) - seed.decode('hex') - except: - import mnemonic - print "not hex, trying decode" - try: - seed = mnemonic.mn_decode( seed.split(' ') ) - except: - QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK') - sys.exit(0) - if not seed: - QMessageBox.warning(None, 'Error', 'no seed', 'OK') - sys.exit(0) - - wallet.seed = str(seed) - #print repr(wallet.seed) - wallet.gap_limit = gap - return True - - - def settings_dialog(self): - d = QDialog(self) - d.setModal(1) - - vbox = QVBoxLayout() - - msg = 'Here are the settings of your wallet.' - vbox.addWidget(QLabel(msg)) - - grid = QGridLayout() - grid.setSpacing(8) - vbox.addLayout(grid) - - fee_e = QLineEdit() - fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) ) - grid.addWidget(QLabel('Fee per tx. input'), 2, 0) - grid.addWidget(fee_e, 2, 1) - fee_e.textChanged.connect(lambda: numbify(fee_e,False)) - - nz_e = QLineEdit() - nz_e.setText("%d"% self.wallet.num_zeros) - grid.addWidget(QLabel('Zeros displayed after decimal point'), 3, 0) - grid.addWidget(nz_e, 3, 1) - nz_e.textChanged.connect(lambda: numbify(nz_e,True)) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - - fee = unicode(fee_e.text()) - try: - fee = int( 100000000 * Decimal(fee) ) - except: - QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK') - return - - if self.wallet.fee != fee: - self.wallet.fee = fee - self.wallet.save() - - nz = unicode(nz_e.text()) - try: - nz = int( nz ) - if nz>8: nz=8 - except: - QMessageBox.warning(self, 'Error', 'Invalid value:%s'%nz, 'OK') - return - - if self.wallet.num_zeros != nz: - self.wallet.num_zeros = nz - self.update_history_tab() - self.update_receive_tab() - self.wallet.save() - - @staticmethod - def network_dialog(wallet, parent=None): - interface = wallet.interface - if parent: - if interface.is_connected: - status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks) - else: - status = "Not connected" - server = wallet.server - else: - import random - status = "Please choose a server." - server = random.choice( DEFAULT_SERVERS ) - - if not wallet.interface.servers: - servers_list = [] - for x in DEFAULT_SERVERS: - h,port,protocol = x.split(':') - servers_list.append( (h,[(protocol,port)] ) ) - else: - servers_list = wallet.interface.servers - - plist = {} - for item in servers_list: - host, pp = item - z = {} - for item2 in pp: - protocol, port = item2 - z[protocol] = port - plist[host] = z - - d = QDialog(parent) - d.setModal(1) - d.setWindowTitle('Server') - d.setMinimumSize(375, 20) - - vbox = QVBoxLayout() - vbox.setSpacing(20) - - hbox = QHBoxLayout() - l = QLabel() - l.setPixmap(QPixmap(":icons/network.png")) - hbox.addWidget(l) - hbox.addWidget(QLabel(status)) - - vbox.addLayout(hbox) - - hbox = QHBoxLayout() - host_line = QLineEdit() - host_line.setText(server) - hbox.addWidget(QLabel('Connect to:')) - hbox.addWidget(host_line) - vbox.addLayout(hbox) - - hbox = QHBoxLayout() - - buttonGroup = QGroupBox("protocol") - radio1 = QRadioButton("tcp", buttonGroup) - radio2 = QRadioButton("http", buttonGroup) - - def current_line(): - return unicode(host_line.text()).split(':') - - def set_button(protocol): - if protocol == 't': - radio1.setChecked(1) - elif protocol == 'h': - radio2.setChecked(1) - - def set_protocol(protocol): - host = current_line()[0] - pp = plist[host] - if protocol not in pp.keys(): - protocol = pp.keys()[0] - set_button(protocol) - port = pp[protocol] - host_line.setText( host + ':' + port + ':' + protocol) - - radio1.clicked.connect(lambda x: set_protocol('t') ) - radio2.clicked.connect(lambda x: set_protocol('h') ) - - set_button(current_line()[2]) - - hbox.addWidget(QLabel('Protocol:')) - hbox.addWidget(radio1) - hbox.addWidget(radio2) - - vbox.addLayout(hbox) - - if wallet.interface.servers: - label = 'Active Servers' - else: - label = 'Default Servers' - - servers_list_widget = QTreeWidget(parent) - servers_list_widget.setHeaderLabels( [ label ] ) - servers_list_widget.setMaximumHeight(150) - for host in plist.keys(): - servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] )) - - def do_set_line(x): - host = unicode(x.text(0)) - pp = plist[host] - if 't' in pp.keys(): - protocol = 't' - else: - protocol = pp.keys()[0] - port = pp[protocol] - host_line.setText( host + ':' + port + ':' + protocol) - set_button(protocol) - - servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line) - vbox.addWidget(servers_list_widget) - - vbox.addLayout(ok_cancel_buttons(d)) - d.setLayout(vbox) - - if not d.exec_(): return - server = unicode( host_line.text() ) - - try: - wallet.set_server(server) - except: - QMessageBox.information(None, 'Error', 'error', 'OK') - if parent == None: - sys.exit(1) - else: - return - - return True - - - -class ElectrumGui(): - - def __init__(self, wallet): - self.wallet = wallet - self.app = QApplication(sys.argv) - - def waiting_dialog(self): - - s = Timer() - s.start() - w = QDialog() - w.resize(200, 70) - w.setWindowTitle('Electrum') - l = QLabel('') - vbox = QVBoxLayout() - vbox.addWidget(l) - w.setLayout(vbox) - w.show() - def f(): - if self.wallet.up_to_date: w.close() - else: - l.setText("Please wait...\nGenerating addresses: %d"%len(self.wallet.all_addresses())) - pass - w.connect(s, QtCore.SIGNAL('timersignal'), f) - self.wallet.interface.poke() - w.exec_() - w.destroy() - - - def restore_or_create(self): - - msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" - r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2) - if r==2: return False - - is_recovery = (r==1) - wallet = self.wallet - # ask for the server. - if not ElectrumWindow.network_dialog( wallet, parent=None ): return False - - if not is_recovery: - wallet.new_seed(None) - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.up_to_date = False - self.waiting_dialog() - # run a dialog indicating the seed, ask the user to remember it - ElectrumWindow.show_seed_dialog(wallet) - #ask for password - ElectrumWindow.change_password_dialog(wallet) - else: - # ask for seed and gap. - if not ElectrumWindow.seed_dialog( wallet ): return False - wallet.init_mpk( wallet.seed ) - wallet.up_to_date_event.clear() - wallet.up_to_date = False - self.waiting_dialog() - if wallet.is_found(): - # history and addressbook - wallet.update_tx_history() - wallet.fill_addressbook() - print "recovery successful" - wallet.save() - else: - QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK') - - wallet.save() - return True - - def main(self,url): - s = Timer() - s.start() - w = ElectrumWindow(self.wallet) - if url: w.set_url(url) - w.app = self.app - w.connect_slots(s) - w.update_wallet() - - self.app.exec_() diff --git a/lib/__init__.py b/lib/__init__.py @@ -0,0 +1,3 @@ +from wallet import Wallet, SecretToASecret, format_satoshis +from interface import WalletSynchronizer +from interface import TcpStratumInterface diff --git a/bmp.py b/lib/bmp.py diff --git a/lib/gui.py b/lib/gui.py @@ -0,0 +1,1297 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2011 thomasv@gitorious +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import datetime +import thread, time, ast, sys, re +import socket, traceback +import pygtk +pygtk.require('2.0') +import gtk, gobject +from electrum import pyqrnative +from decimal import Decimal + +gtk.gdk.threads_init() +APP_NAME = "Electrum" +import platform +MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' + +from wallet import format_satoshis +from interface import DEFAULT_SERVERS + +def numbify(entry, is_int = False): + text = entry.get_text().strip() + chars = '0123456789' + if not is_int: chars +='.' + s = ''.join([i for i in text if i in chars]) + if not is_int: + if '.' in s: + p = s.find('.') + s = s.replace('.','') + s = s[:p] + '.' + s[p:p+8] + try: + amount = int( Decimal(s) * 100000000 ) + except: + amount = None + else: + try: + amount = int( s ) + except: + amount = None + entry.set_text(s) + return amount + + + + +def show_seed_dialog(wallet, password, parent): + from electrum import mnemonic + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + show_message("Incorrect password") + return + dialog = gtk.MessageDialog( + parent = parent, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_OK, + message_format = "Your wallet generation seed is:\n\n" + seed \ + + "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \ + + "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" ) + dialog.set_title("Seed") + dialog.show() + dialog.run() + dialog.destroy() + +def restore_create_dialog(wallet): + + # ask if the user wants to create a new wallet, or recover from a seed. + # if he wants to recover, and nothing is found, do not create wallet + dialog = gtk.Dialog("electrum", parent=None, + flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, + buttons= ("create", 0, "restore",1, "cancel",2) ) + + label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" ) + label.show() + dialog.vbox.pack_start(label) + dialog.show() + r = dialog.run() + dialog.destroy() + + if r==2: return False + + is_recovery = (r==1) + + # ask for the server. + if not run_network_dialog( wallet, parent=None ): return False + + if not is_recovery: + + wallet.new_seed(None) + # generate first key + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.update() + + # run a dialog indicating the seed, ask the user to remember it + show_seed_dialog(wallet, None, None) + + #ask for password + change_password_dialog(wallet, None, None) + else: + # ask for seed and gap. + run_recovery_dialog( wallet ) + + dialog = gtk.MessageDialog( + parent = None, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_CANCEL, + message_format = "Please wait..." ) + dialog.show() + + def recover_thread( wallet, dialog ): + wallet.init_mpk( wallet.seed ) # not encrypted at this point + wallet.up_to_date_event.clear() + wallet.update() + + if wallet.is_found(): + # history and addressbook + wallet.update_tx_history() + wallet.fill_addressbook() + print "recovery successful" + + gobject.idle_add( dialog.destroy ) + + thread.start_new_thread( recover_thread, ( wallet, dialog ) ) + r = dialog.run() + dialog.destroy() + if r==gtk.RESPONSE_CANCEL: return False + if not wallet.is_found: + show_message("No transactions found for this seed") + + wallet.save() + return True + + +def run_recovery_dialog(wallet): + message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." + dialog = gtk.MessageDialog( + parent = None, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_OK_CANCEL, + message_format = message) + + vbox = dialog.vbox + dialog.set_default_response(gtk.RESPONSE_OK) + + # ask seed, server and gap in the same dialog + seed_box = gtk.HBox() + seed_label = gtk.Label('Seed or mnemonic:') + seed_label.set_size_request(150,-1) + seed_box.pack_start(seed_label, False, False, 10) + seed_label.show() + seed_entry = gtk.Entry() + seed_entry.show() + seed_entry.set_size_request(450,-1) + seed_box.pack_start(seed_entry, False, False, 10) + add_help_button(seed_box, '.') + seed_box.show() + vbox.pack_start(seed_box, False, False, 5) + + gap = gtk.HBox() + gap_label = gtk.Label('Gap limit:') + gap_label.set_size_request(150,10) + gap_label.show() + gap.pack_start(gap_label,False, False, 10) + gap_entry = gtk.Entry() + gap_entry.set_text("%d"%wallet.gap_limit) + gap_entry.connect('changed', numbify, True) + gap_entry.show() + gap.pack_start(gap_entry,False,False, 10) + add_help_button(gap, 'The maximum gap that is allowed between unused addresses in your wallet. During wallet recovery, this parameter is used to decide when to stop the recovery process. If you increase this value, you will need to remember it in order to be able to recover your wallet from seed.') + gap.show() + vbox.pack_start(gap, False,False, 5) + + dialog.show() + r = dialog.run() + gap = gap_entry.get_text() + seed = seed_entry.get_text() + dialog.destroy() + + if r==gtk.RESPONSE_CANCEL: + sys.exit(1) + try: + gap = int(gap) + except: + show_message("error") + sys.exit(1) + + try: + seed.decode('hex') + except: + from electrum import mnemonic + print "not hex, trying decode" + seed = mnemonic.mn_decode( seed.split(' ') ) + if not seed: + show_message("no seed") + sys.exit(1) + + wallet.seed = seed + wallet.gap_limit = gap + wallet.save() + + + +def run_settings_dialog(wallet, parent): + + message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field." + + dialog = gtk.MessageDialog( + parent = parent, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_OK_CANCEL, + message_format = message) + + image = gtk.Image() + image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG) + image.show() + dialog.set_image(image) + dialog.set_title("Settings") + + vbox = dialog.vbox + dialog.set_default_response(gtk.RESPONSE_OK) + + fee = gtk.HBox() + fee_entry = gtk.Entry() + fee_label = gtk.Label('Transaction fee:') + fee_label.set_size_request(150,10) + fee_label.show() + fee.pack_start(fee_label,False, False, 10) + fee_entry.set_text( str( Decimal(wallet.fee) /100000000 ) ) + fee_entry.connect('changed', numbify, False) + fee_entry.show() + fee.pack_start(fee_entry,False,False, 10) + add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005') + fee.show() + vbox.pack_start(fee, False,False, 5) + + nz = gtk.HBox() + nz_entry = gtk.Entry() + nz_label = gtk.Label('Display zeros:') + nz_label.set_size_request(150,10) + nz_label.show() + nz.pack_start(nz_label,False, False, 10) + nz_entry.set_text( str( wallet.num_zeros )) + nz_entry.connect('changed', numbify, True) + nz_entry.show() + nz.pack_start(nz_entry,False,False, 10) + add_help_button(nz, "Number of zeros displayed after the decimal point.\nFor example, if this number is 2, then '5.' is displayed as '5.00'") + nz.show() + vbox.pack_start(nz, False,False, 5) + + dialog.show() + r = dialog.run() + fee = fee_entry.get_text() + nz = nz_entry.get_text() + + dialog.destroy() + if r==gtk.RESPONSE_CANCEL: + return + + try: + fee = int( 100000000 * Decimal(fee) ) + except: + show_message("error") + return + if wallet.fee != fee: + wallet.fee = fee + wallet.save() + + try: + nz = int( nz ) + if nz>8: nz = 8 + except: + show_message("error") + return + if wallet.num_zeros != nz: + wallet.num_zeros = nz + wallet.save() + + + + +def run_network_dialog( wallet, parent ): + image = gtk.Image() + image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG) + interface = wallet.interface + if parent: + if interface.is_connected: + status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks) + else: + status = "Not connected" + server = wallet.server + else: + import random + status = "Please choose a server." + server = random.choice( DEFAULT_SERVERS ) + + if not wallet.interface.servers: + servers_list = [] + for x in DEFAULT_SERVERS: + h,port,protocol = x.split(':') + servers_list.append( (h,[(protocol,port)] ) ) + else: + servers_list = wallet.interface.servers + + plist = {} + for item in servers_list: + host, pp = item + z = {} + for item2 in pp: + protocol, port = item2 + z[protocol] = port + plist[host] = z + + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status) + dialog.set_title("Server") + dialog.set_image(image) + image.show() + + vbox = dialog.vbox + host_box = gtk.HBox() + host_label = gtk.Label('Connect to:') + host_label.set_size_request(100,-1) + host_label.show() + host_box.pack_start(host_label, False, False, 10) + host_entry = gtk.Entry() + host_entry.set_size_request(200,-1) + host_entry.set_text(server) + host_entry.show() + host_box.pack_start(host_entry, False, False, 10) + add_help_button(host_box, 'The name and port number of your Electrum server, separated by a colon. Example: "ecdsa.org:50000". If no port number is provided, port 50000 will be tried. Some servers allow you to connect through http (port 80) or https (port 443)') + host_box.show() + + + p_box = gtk.HBox(False, 10) + p_box.show() + + p_label = gtk.Label('Protocol:') + p_label.set_size_request(100,-1) + p_label.show() + p_box.pack_start(p_label, False, False, 10) + + radio1 = gtk.RadioButton(None, "tcp") + p_box.pack_start(radio1, True, True, 0) + radio1.show() + radio2 = gtk.RadioButton(radio1, "http") + p_box.pack_start(radio2, True, True, 0) + radio2.show() + + def current_line(): + return unicode(host_entry.get_text()).split(':') + + def set_button(protocol): + if protocol == 't': + radio1.set_active(1) + elif protocol == 'h': + radio2.set_active(1) + + def set_protocol(protocol): + host = current_line()[0] + pp = plist[host] + if protocol not in pp.keys(): + protocol = pp.keys()[0] + set_button(protocol) + port = pp[protocol] + host_entry.set_text( host + ':' + port + ':' + protocol) + + radio1.connect("toggled", lambda x,y:set_protocol('t'), "radio button 1") + radio2.connect("toggled", lambda x,y:set_protocol('h'), "radio button 1") + + server_list = gtk.ListStore(str) + for host in plist.keys(): + server_list.append([host]) + + treeview = gtk.TreeView(model=server_list) + treeview.show() + + if wallet.interface.servers: + label = 'Active Servers' + else: + label = 'Default Servers' + + tvcolumn = gtk.TreeViewColumn(label) + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 0) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(treeview) + scroll.show() + + vbox.pack_start(host_box, False,False, 5) + vbox.pack_start(p_box, True, True, 0) + vbox.pack_start(scroll) + + def my_treeview_cb(treeview): + path, view_column = treeview.get_cursor() + host = server_list.get_value( server_list.get_iter(path), 0) + + pp = plist[host] + if 't' in pp.keys(): + protocol = 't' + else: + protocol = pp.keys()[0] + port = pp[protocol] + host_entry.set_text( host + ':' + port + ':' + protocol) + set_button(protocol) + + treeview.connect('cursor-changed', my_treeview_cb) + + dialog.show() + r = dialog.run() + server = host_entry.get_text() + dialog.destroy() + + if r==gtk.RESPONSE_CANCEL: + return False + + try: + wallet.set_server(server) + except: + show_message("error:" + server) + return False + + if parent: + wallet.save() + return True + + + +def show_message(message, parent=None): + dialog = gtk.MessageDialog( + parent = parent, + flags = gtk.DIALOG_MODAL, + buttons = gtk.BUTTONS_CLOSE, + message_format = message ) + dialog.show() + dialog.run() + dialog.destroy() + +def password_line(label): + password = gtk.HBox() + password_label = gtk.Label(label) + password_label.set_size_request(120,10) + password_label.show() + password.pack_start(password_label,False, False, 10) + password_entry = gtk.Entry() + password_entry.set_size_request(300,-1) + password_entry.set_visibility(False) + password_entry.show() + password.pack_start(password_entry,False,False, 10) + password.show() + return password, password_entry + +def password_dialog(parent): + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.") + dialog.get_image().set_visible(False) + current_pw, current_pw_entry = password_line('Password:') + current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK) + dialog.vbox.pack_start(current_pw, False, True, 0) + dialog.show() + result = dialog.run() + pw = current_pw_entry.get_text() + dialog.destroy() + if result != gtk.RESPONSE_CANCEL: return pw + +def change_password_dialog(wallet, parent, icon): + if parent: + msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' + else: + msg = "Please choose a password to encrypt your wallet keys" + + dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) + dialog.set_title("Change password") + image = gtk.Image() + image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG) + image.show() + dialog.set_image(image) + + if wallet.use_encryption: + current_pw, current_pw_entry = password_line('Current password:') + dialog.vbox.pack_start(current_pw, False, True, 0) + + password, password_entry = password_line('New password:') + dialog.vbox.pack_start(password, False, True, 5) + password2, password2_entry = password_line('Confirm password:') + dialog.vbox.pack_start(password2, False, True, 5) + + dialog.show() + result = dialog.run() + password = current_pw_entry.get_text() if wallet.use_encryption else None + new_password = password_entry.get_text() + new_password2 = password2_entry.get_text() + dialog.destroy() + if result == gtk.RESPONSE_CANCEL: + return + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + show_message("Incorrect password") + return + + if new_password != new_password2: + show_message("passwords do not match") + return + + wallet.update_password(seed, new_password) + + if icon: + if wallet.use_encryption: + icon.set_tooltip_text('wallet is encrypted') + else: + icon.set_tooltip_text('wallet is unencrypted') + + +def add_help_button(hbox, message): + button = gtk.Button('?') + button.connect("clicked", lambda x: show_message(message)) + button.show() + hbox.pack_start(button,False, False) + + +class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) ) + +gobject.type_register(MyWindow) +gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W') +gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q') + + +class ElectrumWindow: + + def show_message(self, msg): + show_message(msg, self.window) + + def __init__(self, wallet): + self.wallet = wallet + self.funds_error = False # True if not enough funds + + self.window = MyWindow(gtk.WINDOW_TOPLEVEL) + self.window.set_title(APP_NAME + " " + self.wallet.electrum_version) + self.window.connect("destroy", gtk.main_quit) + self.window.set_border_width(0) + self.window.connect('mykeypress', gtk.main_quit) + self.window.set_default_size(720, 350) + + vbox = gtk.VBox() + + self.notebook = gtk.Notebook() + self.create_history_tab() + self.create_send_tab() + self.create_recv_tab() + self.create_book_tab() + self.create_about_tab() + self.notebook.show() + vbox.pack_start(self.notebook, True, True, 2) + + self.status_bar = gtk.Statusbar() + vbox.pack_start(self.status_bar, False, False, 0) + + self.status_image = gtk.Image() + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + self.status_image.set_alignment(True, 0.5 ) + self.status_image.show() + + self.network_button = gtk.Button() + self.network_button.connect("clicked", lambda x: run_network_dialog(self.wallet, self.window) ) + self.network_button.add(self.status_image) + self.network_button.set_relief(gtk.RELIEF_NONE) + self.network_button.show() + self.status_bar.pack_end(self.network_button, False, False) + + def seedb(w, wallet): + if wallet.use_encryption: + password = password_dialog(self.window) + if not password: return + else: password = None + show_seed_dialog(wallet, password, self.window) + button = gtk.Button('S') + button.connect("clicked", seedb, wallet ) + button.set_relief(gtk.RELIEF_NONE) + button.show() + self.status_bar.pack_end(button,False, False) + + settings_icon = gtk.Image() + settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) + settings_icon.set_alignment(0.5, 0.5) + settings_icon.set_size_request(16,16 ) + settings_icon.show() + + prefs_button = gtk.Button() + prefs_button.connect("clicked", lambda x: run_settings_dialog(self.wallet, self.window) ) + prefs_button.add(settings_icon) + prefs_button.set_tooltip_text("Settings") + prefs_button.set_relief(gtk.RELIEF_NONE) + prefs_button.show() + self.status_bar.pack_end(prefs_button,False,False) + + pw_icon = gtk.Image() + pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU) + pw_icon.set_alignment(0.5, 0.5) + pw_icon.set_size_request(16,16 ) + pw_icon.show() + + password_button = gtk.Button() + password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon)) + password_button.add(pw_icon) + password_button.set_relief(gtk.RELIEF_NONE) + password_button.show() + self.status_bar.pack_end(password_button,False,False) + + self.window.add(vbox) + self.window.show_all() + #self.fee_box.hide() + + self.context_id = self.status_bar.get_context_id("statusbar") + self.update_status_bar() + + def update_status_bar_thread(): + while True: + gobject.idle_add( self.update_status_bar ) + time.sleep(0.5) + + + def check_recipient_thread(): + old_r = '' + while True: + time.sleep(0.5) + if self.payto_entry.is_focus(): + continue + r = self.payto_entry.get_text() + if r != old_r: + old_r = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.wallet.get_alias(r, interactive=False) + except: + continue + if to_address: + s = r + ' <' + to_address + '>' + gobject.idle_add( lambda: self.payto_entry.set_text(s) ) + + + thread.start_new_thread(update_status_bar_thread, ()) + thread.start_new_thread(check_recipient_thread, ()) + self.notebook.set_current_page(0) + + + def add_tab(self, page, name): + tab_label = gtk.Label(name) + tab_label.show() + self.notebook.append_page(page, tab_label) + + + def create_send_tab(self): + + page = vbox = gtk.VBox() + page.show() + + payto = gtk.HBox() + payto_label = gtk.Label('Pay to:') + payto_label.set_size_request(100,-1) + payto.pack_start(payto_label, False) + payto_entry = gtk.Entry() + payto_entry.set_size_request(450, 26) + payto.pack_start(payto_entry, False) + vbox.pack_start(payto, False, False, 5) + + message = gtk.HBox() + message_label = gtk.Label('Description:') + message_label.set_size_request(100,-1) + message.pack_start(message_label, False) + message_entry = gtk.Entry() + message_entry.set_size_request(450, 26) + message.pack_start(message_entry, False) + vbox.pack_start(message, False, False, 5) + + amount_box = gtk.HBox() + amount_label = gtk.Label('Amount:') + amount_label.set_size_request(100,-1) + amount_box.pack_start(amount_label, False) + amount_entry = gtk.Entry() + amount_entry.set_size_request(120, -1) + amount_box.pack_start(amount_entry, False) + vbox.pack_start(amount_box, False, False, 5) + + self.fee_box = fee_box = gtk.HBox() + fee_label = gtk.Label('Fee:') + fee_label.set_size_request(100,-1) + fee_box.pack_start(fee_label, False) + fee_entry = gtk.Entry() + fee_entry.set_size_request(60, 26) + fee_box.pack_start(fee_entry, False) + vbox.pack_start(fee_box, False, False, 5) + + end_box = gtk.HBox() + empty_label = gtk.Label('') + empty_label.set_size_request(100,-1) + end_box.pack_start(empty_label, False) + send_button = gtk.Button("Send") + send_button.show() + end_box.pack_start(send_button, False, False, 0) + clear_button = gtk.Button("Clear") + clear_button.show() + end_box.pack_start(clear_button, False, False, 15) + send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry)) + clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry)) + + vbox.pack_start(end_box, False, False, 5) + + # display this line only if there is a signature + payto_sig = gtk.HBox() + payto_sig_id = gtk.Label('') + payto_sig.pack_start(payto_sig_id, False) + vbox.pack_start(payto_sig, True, True, 5) + + + self.user_fee = False + + def entry_changed( entry, is_fee ): + self.funds_error = False + amount = numbify(amount_entry) + fee = numbify(fee_entry) + if not is_fee: fee = None + if amount is None: + return + inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) + if not is_fee: + fee_entry.set_text( str( Decimal( fee ) / 100000000 ) ) + self.fee_box.show() + if inputs: + amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) + fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000")) + send_button.set_sensitive(True) + else: + send_button.set_sensitive(False) + amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) + fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000")) + self.funds_error = True + + amount_entry.connect('changed', entry_changed, False) + fee_entry.connect('changed', entry_changed, True) + + self.payto_entry = payto_entry + self.payto_fee_entry = fee_entry + self.payto_sig_id = payto_sig_id + self.payto_sig = payto_sig + self.amount_entry = amount_entry + self.message_entry = message_entry + self.add_tab(page, 'Send') + + def set_frozen(self,entry,frozen): + if frozen: + entry.set_editable(False) + entry.set_has_frame(False) + entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee")) + else: + entry.set_editable(True) + entry.set_has_frame(True) + entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff")) + + def set_url(self, url): + payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) + self.notebook.set_current_page(1) + self.payto_entry.set_text(payto) + self.message_entry.set_text(message) + self.amount_entry.set_text(amount) + if identity: + self.set_frozen(self.payto_entry,True) + self.set_frozen(self.amount_entry,True) + self.set_frozen(self.message_entry,True) + self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity ) + else: + self.payto_sig.set_visible(False) + + def create_about_tab(self): + import pango + page = gtk.VBox() + page.show() + tv = gtk.TextView() + tv.set_editable(False) + tv.set_cursor_visible(False) + tv.modify_font(pango.FontDescription(MONOSPACE_FONT)) + page.pack_start(tv) + self.info = tv.get_buffer() + self.add_tab(page, 'Wall') + + def do_clear(self, w, data): + self.payto_sig.set_visible(False) + self.payto_fee_entry.set_text('') + for entry in [self.payto_entry,self.amount_entry,self.message_entry]: + self.set_frozen(entry,False) + entry.set_text('') + + def question(self,msg): + dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg) + dialog.show() + result = dialog.run() + dialog.destroy() + return result == gtk.RESPONSE_OK + + def do_send(self, w, data): + payto_entry, label_entry, amount_entry, fee_entry = data + label = label_entry.get_text() + r = payto_entry.get_text() + r = r.strip() + + m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) + m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + + if m1: + to_address = self.wallet.get_alias(r, True, self.show_message, self.question) + if not to_address: + return + else: + self.update_sending_tab() + + elif m2: + to_address = m2.group(5) + else: + to_address = r + + if not self.wallet.is_valid(to_address): + self.show_message( "invalid bitcoin address:\n"+to_address) + return + + try: + amount = int( Decimal(amount_entry.get_text()) * 100000000 ) + except: + self.show_message( "invalid amount") + return + try: + fee = int( Decimal(fee_entry.get_text()) * 100000000 ) + except: + self.show_message( "invalid fee") + return + + if self.wallet.use_encryption: + password = password_dialog(self.window) + if not password: + return + else: + password = None + + try: + tx = self.wallet.mktx( to_address, amount, label, password, fee ) + except BaseException, e: + self.show_message(e.message) + return + + status, msg = self.wallet.sendtx( tx ) + if status: + self.show_message( "payment sent.\n" + msg ) + payto_entry.set_text("") + label_entry.set_text("") + amount_entry.set_text("") + fee_entry.set_text("") + #self.fee_box.hide() + self.update_sending_tab() + else: + self.show_message( msg ) + + + def treeview_button_press(self, treeview, event): + if event.type == gtk.gdk._2BUTTON_PRESS: + c = treeview.get_cursor()[0] + if treeview == self.history_treeview: + tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) + self.show_message(tx_details) + elif treeview == self.contacts_treeview: + m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self-signed" + msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] + self.show_message(msg) + + + def treeview_key_press(self, treeview, event): + c = treeview.get_cursor()[0] + if event.keyval == gtk.keysyms.Up: + if c and c[0] == 0: + treeview.parent.grab_focus() + treeview.set_cursor((0,)) + elif event.keyval == gtk.keysyms.Return: + if treeview == self.history_treeview: + tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8) + self.show_message(tx_details) + elif treeview == self.contacts_treeview: + m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self" + msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0] + self.show_message(msg) + + return False + + def create_history_tab(self): + + self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str) + treeview = gtk.TreeView(model=self.history_list) + self.history_treeview = treeview + treeview.set_tooltip_column(7) + treeview.show() + treeview.connect('key-press-event', self.treeview_key_press) + treeview.connect('button-press-event', self.treeview_button_press) + + tvcolumn = gtk.TreeViewColumn('') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererPixbuf() + tvcolumn.pack_start(cell, False) + tvcolumn.set_attributes(cell, stock_id=1) + + tvcolumn = gtk.TreeViewColumn('Date') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 2) + + tvcolumn = gtk.TreeViewColumn('Description') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_property('foreground', 'grey') + cell.set_property('family', MONOSPACE_FONT) + cell.set_property('editable', True) + def edited_cb(cell, path, new_text, h_list): + tx = h_list.get_value( h_list.get_iter(path), 0) + self.wallet.labels[tx] = new_text + self.wallet.save() + self.update_history_tab() + cell.connect('edited', edited_cb, self.history_list) + def editing_started(cell, entry, path, h_list): + tx = h_list.get_value( h_list.get_iter(path), 0) + if not self.wallet.labels.get(tx): entry.set_text('') + cell.connect('editing-started', editing_started, self.history_list) + tvcolumn.set_expand(True) + tvcolumn.pack_start(cell, True) + tvcolumn.set_attributes(cell, text=3, foreground_set = 4) + + tvcolumn = gtk.TreeViewColumn('Amount') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_alignment(1, 0.5) + cell.set_property('family', MONOSPACE_FONT) + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 5) + + tvcolumn = gtk.TreeViewColumn('Balance') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_alignment(1, 0.5) + cell.set_property('family', MONOSPACE_FONT) + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 6) + + tvcolumn = gtk.TreeViewColumn('Tooltip') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, False) + tvcolumn.add_attribute(cell, 'text', 7) + tvcolumn.set_visible(False) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scroll.add(treeview) + + self.add_tab(scroll, 'History') + self.update_history_tab() + + + def create_recv_tab(self): + self.recv_list = gtk.ListStore(str, str, str) + self.add_tab( self.make_address_list(True), 'Receive') + self.update_receiving_tab() + + def create_book_tab(self): + self.addressbook_list = gtk.ListStore(str, str, str) + self.add_tab( self.make_address_list(False), 'Contacts') + self.update_sending_tab() + + def make_address_list(self, is_recv): + liststore = self.recv_list if is_recv else self.addressbook_list + treeview = gtk.TreeView(model= liststore) + treeview.connect('key-press-event', self.treeview_key_press) + treeview.connect('button-press-event', self.treeview_button_press) + treeview.show() + if not is_recv: + self.contacts_treeview = treeview + + tvcolumn = gtk.TreeViewColumn('Address') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_property('family', MONOSPACE_FONT) + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 0) + + tvcolumn = gtk.TreeViewColumn('Label') + tvcolumn.set_expand(True) + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + cell.set_property('editable', True) + def edited_cb2(cell, path, new_text, liststore): + address = liststore.get_value( liststore.get_iter(path), 0) + self.wallet.labels[address] = new_text + self.wallet.save() + self.wallet.update_tx_labels() + self.update_receiving_tab() + self.update_sending_tab() + self.update_history_tab() + cell.connect('edited', edited_cb2, liststore) + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 1) + + tvcolumn = gtk.TreeViewColumn('Tx') + treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 2) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(treeview) + + hbox = gtk.HBox() + if not is_recv: + button = gtk.Button("New") + button.connect("clicked", self.newaddress_dialog) + button.show() + hbox.pack_start(button,False) + + def showqrcode(w, treeview, liststore): + path, col = treeview.get_cursor() + if not path: return + address = liststore.get_value(liststore.get_iter(path), 0) + qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H) + qr.addData(address) + qr.make() + boxsize = 7 + size = qr.getModuleCount()*boxsize + def area_expose_cb(area, event): + style = area.get_style() + k = qr.getModuleCount() + for r in range(k): + for c in range(k): + gc = style.black_gc if qr.isDark(r, c) else style.white_gc + area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize) + area = gtk.DrawingArea() + area.set_size_request(size, size) + area.connect("expose-event", area_expose_cb) + area.show() + dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1)) + dialog.vbox.add(area) + dialog.run() + dialog.destroy() + + button = gtk.Button("QR") + button.connect("clicked", showqrcode, treeview, liststore) + button.show() + hbox.pack_start(button,False) + + button = gtk.Button("Copy to clipboard") + def copy2clipboard(w, treeview, liststore): + import platform + path, col = treeview.get_cursor() + if path: + address = liststore.get_value( liststore.get_iter(path), 0) + if platform.system() == 'Windows': + from Tkinter import Tk + r = Tk() + r.withdraw() + r.clipboard_clear() + r.clipboard_append( address ) + r.destroy() + else: + c = gtk.clipboard_get() + c.set_text( address ) + button.connect("clicked", copy2clipboard, treeview, liststore) + button.show() + hbox.pack_start(button,False) + + if not is_recv: + button = gtk.Button("Pay to") + def payto(w, treeview, liststore): + path, col = treeview.get_cursor() + if path: + address = liststore.get_value( liststore.get_iter(path), 0) + self.payto_entry.set_text( address ) + self.notebook.set_current_page(1) + self.amount_entry.grab_focus() + + button.connect("clicked", payto, treeview, liststore) + button.show() + hbox.pack_start(button,False) + + vbox = gtk.VBox() + vbox.pack_start(scroll,True) + vbox.pack_start(hbox, False) + return vbox + + def update_status_bar(self): + interface = self.wallet.interface + if self.funds_error: + text = "Not enough funds" + elif interface.is_connected: + self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks)) + if self.wallet.blocks == -1: + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + text = "Connecting..." + elif self.wallet.blocks == 0: + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + text = "Server not ready" + elif not self.wallet.up_to_date: + self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU) + text = "Synchronizing..." + else: + self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) + self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.wallet.blocks)) + c, u = self.wallet.get_balance() + text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) ) + if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() ) + else: + self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU) + self.network_button.set_tooltip_text("Trying to contact %s.\n%d blocks"%(interface.host, self.wallet.blocks)) + text = "Not connected" + + self.status_bar.pop(self.context_id) + self.status_bar.push(self.context_id, text) + + if self.wallet.was_updated and self.wallet.up_to_date: + self.update_history_tab() + self.update_receiving_tab() + # addressbook too... + self.info.set_text( self.wallet.banner ) + self.wallet.was_updated = False + + + def update_receiving_tab(self): + self.recv_list.clear() + for address in self.wallet.all_addresses(): + if self.wallet.is_change(address):continue + label = self.wallet.labels.get(address) + n = 0 + h = self.wallet.history.get(address,[]) + for item in h: + if not item['is_input'] : n=n+1 + tx = "None" if n==0 else "%d"%n + self.recv_list.append((address, label, tx )) + + def update_sending_tab(self): + # detect addresses that are not mine in history, add them here... + self.addressbook_list.clear() + for alias, v in self.wallet.aliases.items(): + s, target = v + label = self.wallet.labels.get(alias) + self.addressbook_list.append((alias, label, '-')) + + for address in self.wallet.addressbook: + label = self.wallet.labels.get(address) + n = 0 + for item in self.wallet.tx_history.values(): + if address in item['outputs'] : n=n+1 + tx = "None" if n==0 else "%d"%n + self.addressbook_list.append((address, label, tx)) + + def update_history_tab(self): + cursor = self.history_treeview.get_cursor()[0] + self.history_list.clear() + balance = 0 + for tx in self.wallet.get_tx_history(): + tx_hash = tx['tx_hash'] + if tx['height']: + conf = self.wallet.blocks - tx['height'] + 1 + time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] + conf_icon = gtk.STOCK_APPLY + else: + conf = 0 + time_str = 'pending' + conf_icon = gtk.STOCK_EXECUTE + v = tx['value'] + balance += v + label = self.wallet.labels.get(tx_hash) + is_default_label = (label == '') or (label is None) + if is_default_label: label = tx['default_label'] + tooltip = tx_hash + "\n%d confirmations"%conf + + # tx = self.wallet.tx_history.get(tx_hash) + details = "Transaction Details:\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n\n"%conf \ + + "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(tx['outputs']) + r = self.wallet.receipts.get(tx_hash) + if r: + details += "\n_______________________________________" \ + + '\n\nSigned URI: ' + r[2] \ + + "\n\nSigned by: " + r[0] \ + + '\n\nSignature: ' + r[1] + + + self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, + format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros), tooltip, details] ) + if cursor: self.history_treeview.set_cursor( cursor ) + + + + def newaddress_dialog(self, w): + + title = "New Contact" + dialog = gtk.Dialog(title, parent=self.window, + flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, + buttons= ("cancel", 0, "ok",1) ) + dialog.show() + + label = gtk.HBox() + label_label = gtk.Label('Label:') + label_label.set_size_request(120,10) + label_label.show() + label.pack_start(label_label) + label_entry = gtk.Entry() + label_entry.show() + label.pack_start(label_entry) + label.show() + dialog.vbox.pack_start(label, False, True, 5) + + address = gtk.HBox() + address_label = gtk.Label('Address:') + address_label.set_size_request(120,10) + address_label.show() + address.pack_start(address_label) + address_entry = gtk.Entry() + address_entry.show() + address.pack_start(address_entry) + address.show() + dialog.vbox.pack_start(address, False, True, 5) + + result = dialog.run() + address = address_entry.get_text() + label = label_entry.get_text() + dialog.destroy() + + if result == 1: + if self.wallet.is_valid(address): + self.wallet.addressbook.append(address) + if label: self.wallet.labels[address] = label + self.wallet.save() + self.update_sending_tab() + else: + errorDialog = gtk.MessageDialog( + parent=self.window, + flags=gtk.DIALOG_MODAL, + buttons= gtk.BUTTONS_CLOSE, + message_format = "Invalid address") + errorDialog.show() + errorDialog.run() + errorDialog.destroy() + + + +class ElectrumGui(): + + def __init__(self, wallet): + self.wallet = wallet + + def main(self, url=None): + ew = ElectrumWindow(self.wallet) + if url: ew.set_url(url) + gtk.main() + + def restore_or_create(self): + return restore_create_dialog(self.wallet) diff --git a/lib/gui_qt.py b/lib/gui_qt.py @@ -0,0 +1,1222 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2012 thomasv@gitorious +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys, time, datetime, re + +# todo: see PySide + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui +from interface import DEFAULT_SERVERS + +try: + import icons_rc +except: + print "Could not import icons_rp.py" + print "Please generate it with: 'pyrcc4 icons.qrc -o icons_rc.py'" + sys.exit(1) + +from wallet import format_satoshis +from decimal import Decimal + +import platform +MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' + + +def numbify(entry, is_int = False): + text = unicode(entry.text()).strip() + chars = '0123456789' + if not is_int: chars +='.' + s = ''.join([i for i in text if i in chars]) + if not is_int: + if '.' in s: + p = s.find('.') + s = s.replace('.','') + s = s[:p] + '.' + s[p:p+8] + try: + amount = int( Decimal(s) * 100000000 ) + except: + amount = None + else: + try: + amount = int( s ) + except: + amount = None + entry.setText(s) + return amount + + +class Timer(QtCore.QThread): + def run(self): + while True: + self.emit(QtCore.SIGNAL('timersignal')) + time.sleep(0.5) + +class EnterButton(QPushButton): + def __init__(self, text, func): + QPushButton.__init__(self, text) + self.func = func + self.clicked.connect(func) + + def keyPressEvent(self, e): + if e.key() == QtCore.Qt.Key_Return: + apply(self.func,()) + +class StatusBarButton(QPushButton): + def __init__(self, icon, tooltip, func): + QPushButton.__init__(self, icon, '') + self.setToolTip(tooltip) + self.setFlat(True) + self.setMaximumWidth(25) + self.clicked.connect(func) + self.func = func + + def keyPressEvent(self, e): + if e.key() == QtCore.Qt.Key_Return: + apply(self.func,()) + + +class QRCodeWidget(QWidget): + + def __init__(self, addr): + super(QRCodeWidget, self).__init__() + self.setGeometry(300, 300, 350, 350) + self.set_addr(addr) + + def set_addr(self, addr): + from electrum import pyqrnative + self.addr = addr + self.qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.L) + self.qr.addData(addr) + self.qr.make() + + def paintEvent(self, e): + qp = QtGui.QPainter() + qp.begin(self) + boxsize = 7 + size = self.qr.getModuleCount()*boxsize + k = self.qr.getModuleCount() + black = QColor(0, 0, 0, 255) + white = QColor(255, 255, 255, 255) + for r in range(k): + for c in range(k): + if self.qr.isDark(r, c): + qp.setBrush(black) + qp.setPen(black) + else: + qp.setBrush(white) + qp.setPen(white) + qp.drawRect(c*boxsize, r*boxsize, boxsize, boxsize) + qp.end() + + + +def ok_cancel_buttons(dialog): + hbox = QHBoxLayout() + hbox.addStretch(1) + b = QPushButton("OK") + hbox.addWidget(b) + b.clicked.connect(dialog.accept) + b = QPushButton("Cancel") + hbox.addWidget(b) + b.clicked.connect(dialog.reject) + return hbox + + +class ElectrumWindow(QMainWindow): + + def __init__(self, wallet): + QMainWindow.__init__(self) + self.wallet = wallet + self.wallet.gui_callback = self.update_callback + + self.funds_error = False + + self.tabs = tabs = QTabWidget(self) + tabs.addTab(self.create_history_tab(), 'History') + tabs.addTab(self.create_send_tab(), 'Send') + tabs.addTab(self.create_receive_tab(), 'Receive') + tabs.addTab(self.create_contacts_tab(),'Contacts') + tabs.addTab(self.create_wall_tab(), 'Wall') + tabs.setMinimumSize(600, 400) + tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.setCentralWidget(tabs) + self.create_status_bar() + self.setGeometry(100,100,840,400) + self.setWindowTitle( 'Electrum ' + self.wallet.electrum_version ) + self.show() + + QShortcut(QKeySequence("Ctrl+W"), self, self.close) + QShortcut(QKeySequence("Ctrl+Q"), self, self.close) + QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() )) + QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() )) + + self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet) + + + def connect_slots(self, sender): + self.connect(sender, QtCore.SIGNAL('timersignal'), self.check_recipient) + self.previous_payto_e='' + + def check_recipient(self): + if self.payto_e.hasFocus(): + return + r = unicode( self.payto_e.text() ) + if r != self.previous_payto_e: + self.previous_payto_e = r + r = r.strip() + if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r): + try: + to_address = self.wallet.get_alias(r, True, self.show_message, self.question) + except: + return + if to_address: + s = r + ' <' + to_address + '>' + self.payto_e.setText(s) + + + def update_callback(self): + self.emit(QtCore.SIGNAL('updatesignal')) + + def update_wallet(self): + if self.wallet.interface.is_connected: + if self.wallet.blocks == -1: + text = "Connecting..." + icon = QIcon(":icons/status_disconnected.png") + elif self.wallet.blocks == 0: + text = "Server not ready" + icon = QIcon(":icons/status_disconnected.png") + elif not self.wallet.up_to_date: + text = "Synchronizing..." + icon = QIcon(":icons/status_waiting.png") + else: + c, u = self.wallet.get_balance() + text = "Balance: %s "%( format_satoshis(c,False,self.wallet.num_zeros) ) + if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() ) + icon = QIcon(":icons/status_connected.png") + else: + text = "Not connected" + icon = QIcon(":icons/status_disconnected.png") + + if self.funds_error: + text = "Not enough funds" + + self.statusBar().showMessage(text) + self.status_button.setIcon( icon ) + + if self.wallet.up_to_date: + self.textbox.setText( self.wallet.banner ) + self.update_history_tab() + self.update_receive_tab() + self.update_contacts_tab() + + + def create_history_tab(self): + self.history_list = w = QTreeWidget(self) + #print w.getContentsMargins() + w.setColumnCount(5) + w.setColumnWidth(0, 40) + w.setColumnWidth(1, 140) + w.setColumnWidth(2, 350) + w.setColumnWidth(3, 140) + w.setColumnWidth(4, 140) + w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] ) + self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details) + self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked) + self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed) + return w + + def tx_details(self, item, column): + tx_hash = str(item.toolTip(0)) + tx = self.wallet.tx_history.get(tx_hash) + + if tx['height']: + conf = self.wallet.blocks - tx['height'] + 1 + time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] + else: + conf = 0 + time_str = 'pending' + + tx_details = "Transaction Details:\n\n" \ + + "Transaction ID:\n" + tx_hash + "\n\n" \ + + "Status: %d confirmations\n\n"%conf \ + + "Date: %s\n\n"%time_str \ + + "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \ + + "Outputs:\n-"+ '\n-'.join(tx['outputs']) + + r = self.wallet.receipts.get(tx_hash) + if r: + tx_details += "\n_______________________________________" \ + + '\n\nSigned URI: ' + r[2] \ + + "\n\nSigned by: " + r[0] \ + + '\n\nSignature: ' + r[1] + + QMessageBox.information(self, 'Details', tx_details, 'OK') + + + def tx_label_clicked(self, item, column): + if column==2 and item.isSelected(): + tx_hash = str(item.toolTip(0)) + self.is_edit=True + #if not self.wallet.labels.get(tx_hash): item.setText(2,'') + item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + self.history_list.editItem( item, column ) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + self.is_edit=False + + def tx_label_changed(self, item, column): + if self.is_edit: + return + self.is_edit=True + tx_hash = str(item.toolTip(0)) + tx = self.wallet.tx_history.get(tx_hash) + s = self.wallet.labels.get(tx_hash) + text = unicode( item.text(2) ) + if text: + self.wallet.labels[tx_hash] = text + item.setForeground(2, QBrush(QColor('black'))) + else: + if s: self.wallet.labels.pop(tx_hash) + text = tx['default_label'] + item.setText(2, text) + item.setForeground(2, QBrush(QColor('gray'))) + self.is_edit=False + + def address_label_clicked(self, item, column, l): + if column==1 and item.isSelected(): + addr = unicode( item.text(0) ) + if addr in map(lambda x:x[1], self.wallet.aliases.values()): + return + item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + l.editItem( item, column ) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled) + + def address_label_changed(self, item, column, l): + addr = unicode( item.text(0) ) + text = unicode( item.text(1) ) + if text: + self.wallet.labels[addr] = text + else: + s = self.wallet.labels.get(addr) + if s: self.wallet.labels.pop(addr) + self.update_history_tab() + + def update_history_tab(self): + self.history_list.clear() + balance = 0 + for tx in self.wallet.get_tx_history(): + tx_hash = tx['tx_hash'] + if tx['height']: + conf = self.wallet.blocks - tx['height'] + 1 + time_str = datetime.datetime.fromtimestamp( tx['timestamp']).isoformat(' ')[:-3] + icon = QIcon(":icons/confirmed.png") + else: + conf = 0 + time_str = 'pending' + icon = QIcon(":icons/unconfirmed.png") + v = tx['value'] + balance += v + label = self.wallet.labels.get(tx_hash) + is_default_label = (label == '') or (label is None) + if is_default_label: label = tx['default_label'] + + item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True,self.wallet.num_zeros), format_satoshis(balance,False,self.wallet.num_zeros)] ) + item.setFont(2, QFont(MONOSPACE_FONT)) + item.setFont(3, QFont(MONOSPACE_FONT)) + item.setFont(4, QFont(MONOSPACE_FONT)) + item.setToolTip(0, tx_hash) + if is_default_label: + item.setForeground(2, QBrush(QColor('grey'))) + + item.setIcon(0, icon) + self.history_list.insertTopLevelItem(0,item) + + + def create_send_tab(self): + w = QWidget() + + grid = QGridLayout() + grid.setSpacing(8) + grid.setColumnMinimumWidth(3,300) + grid.setColumnStretch(4,1) + + self.payto_e = QLineEdit() + grid.addWidget(QLabel('Pay to'), 1, 0) + grid.addWidget(self.payto_e, 1, 1, 1, 3) + + self.message_e = QLineEdit() + grid.addWidget(QLabel('Description'), 2, 0) + grid.addWidget(self.message_e, 2, 1, 1, 3) + + self.amount_e = QLineEdit() + grid.addWidget(QLabel('Amount'), 3, 0) + grid.addWidget(self.amount_e, 3, 1, 1, 2) + + self.fee_e = QLineEdit() + grid.addWidget(QLabel('Fee'), 4, 0) + grid.addWidget(self.fee_e, 4, 1, 1, 2) + + b = EnterButton("Send", self.do_send) + grid.addWidget(b, 5, 1) + + b = EnterButton("Clear",self.do_clear) + grid.addWidget(b, 5, 2) + + self.payto_sig = QLabel('') + grid.addWidget(self.payto_sig, 6, 0, 1, 4) + + w.setLayout(grid) + w.show() + + w2 = QWidget() + vbox = QVBoxLayout() + vbox.addWidget(w) + vbox.addStretch(1) + w2.setLayout(vbox) + + def entry_changed( is_fee ): + self.funds_error = False + amount = numbify(self.amount_e) + fee = numbify(self.fee_e) + if not is_fee: fee = None + if amount is None: + return + inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee ) + if not is_fee: + self.fee_e.setText( str( Decimal( fee ) / 100000000 ) ) + if inputs: + palette = QPalette() + palette.setColor(self.amount_e.foregroundRole(), QColor('black')) + else: + palette = QPalette() + palette.setColor(self.amount_e.foregroundRole(), QColor('red')) + self.funds_error = True + self.amount_e.setPalette(palette) + self.fee_e.setPalette(palette) + + self.amount_e.textChanged.connect(lambda: entry_changed(False) ) + self.fee_e.textChanged.connect(lambda: entry_changed(True) ) + + return w2 + + def do_send(self): + + label = unicode( self.message_e.text() ) + r = unicode( self.payto_e.text() ) + r = r.strip() + + m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r) + m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + + if m1: + to_address = self.wallet.get_alias(r, True, self.show_message, self.question) + if not to_address: + return + elif m2: + to_address = m2.group(5) + else: + to_address = r + + if not self.wallet.is_valid(to_address): + QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK') + return + + try: + amount = int( Decimal( unicode( self.amount_e.text())) * 100000000 ) + except: + QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK') + return + try: + fee = int( Decimal( unicode( self.fee_e.text())) * 100000000 ) + except: + QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK') + return + + if self.wallet.use_encryption: + password = self.password_dialog() + if not password: + return + else: + password = None + + try: + tx = self.wallet.mktx( to_address, amount, label, password, fee ) + except BaseException, e: + self.show_message(e.message) + return + + status, msg = self.wallet.sendtx( tx ) + if status: + QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK') + self.do_clear() + self.update_contacts_tab() + else: + QMessageBox.warning(self, 'Error', msg, 'OK') + + + def set_url(self, url): + payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question) + self.tabs.setCurrentIndex(1) + self.payto_e.setText(payto) + self.message_e.setText(message) + self.amount_e.setText(amount) + if identity: + self.set_frozen(self.payto_e,True) + self.set_frozen(self.amount_e,True) + self.set_frozen(self.message_e,True) + self.payto_sig.setText( ' The bitcoin URI was signed by ' + identity ) + else: + self.payto_sig.setVisible(False) + + def do_clear(self): + self.payto_sig.setVisible(False) + for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]: + e.setText('') + self.set_frozen(e,False) + + def set_frozen(self,entry,frozen): + if frozen: + entry.setReadOnly(True) + entry.setFrame(False) + palette = QPalette() + palette.setColor(entry.backgroundRole(), QColor('lightgray')) + entry.setPalette(palette) + else: + entry.setReadOnly(False) + entry.setFrame(True) + palette = QPalette() + palette.setColor(entry.backgroundRole(), QColor('white')) + entry.setPalette(palette) + + + + + def clear_buttons(self, hbox): + while hbox.count(): hbox.removeItem(hbox.itemAt(0)) + + def add_buttons(self, l, hbox, is_recv): + self.clear_buttons(hbox) + + i = l.currentItem() + if not i: return + addr = unicode( i.text(0) ) + + hbox.addWidget(EnterButton("QR",lambda: self.show_address_qrcode(addr))) + hbox.addWidget(EnterButton("Copy to Clipboard", lambda: self.app.clipboard().setText(addr))) + if is_recv: + def toggle_freeze(addr): + if addr in self.wallet.frozen_addresses: + self.wallet.frozen_addresses.remove(addr) + else: + self.wallet.frozen_addresses.append(addr) + self.wallet.save() + self.update_receive_tab() + + t = "Unfreeze" if addr in self.wallet.frozen_addresses else "Freeze" + hbox.addWidget(EnterButton(t, lambda: toggle_freeze(addr))) + + else: + def payto(addr): + if not addr:return + self.tabs.setCurrentIndex(1) + self.payto_e.setText(addr) + self.amount_e.setFocus() + hbox.addWidget(EnterButton('Pay to', lambda: payto(addr))) + hbox.addWidget(EnterButton("New", self.newaddress_dialog)) + hbox.addStretch(1) + + + def create_receive_tab(self): + l = QTreeWidget(self) + l.setColumnCount(4) + l.setColumnWidth(0, 350) + l.setColumnWidth(1, 330) + l.setColumnWidth(2, 100) + l.setColumnWidth(3, 10) + l.setHeaderLabels( ['Address', 'Label','Balance','Tx']) + + w = QWidget() + vbox = QVBoxLayout() + w.setLayout(vbox) + + vbox.setMargin(0) + vbox.setSpacing(0) + vbox.addWidget(l) + buttons = QWidget() + vbox.addWidget(buttons) + + hbox = QHBoxLayout() + hbox.setMargin(0) + hbox.setSpacing(0) + buttons.setLayout(hbox) + + self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) + self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) + self.connect(l, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda: self.add_buttons(l, hbox, True)) + self.receive_list = l + self.receive_buttons_hbox = hbox + return w + + def create_contacts_tab(self): + l = QTreeWidget(self) + l.setColumnCount(3) + l.setColumnWidth(0, 350) + l.setColumnWidth(1, 330) + l.setColumnWidth(2, 20) + l.setHeaderLabels( ['Address', 'Label','Tx']) + + w = QWidget() + vbox = QVBoxLayout() + w.setLayout(vbox) + + vbox.setMargin(0) + vbox.setSpacing(0) + vbox.addWidget(l) + buttons = QWidget() + vbox.addWidget(buttons) + + hbox = QHBoxLayout() + hbox.setMargin(0) + hbox.setSpacing(0) + buttons.setLayout(hbox) + + self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l)) + self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l)) + self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details) + self.connect(l, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), lambda: self.add_buttons(l, hbox, False)) + + self.contacts_list = l + self.contacts_buttons_hbox = hbox + return w + + def update_receive_tab(self): + self.receive_list.clear() + self.clear_buttons(self.receive_buttons_hbox) + + for address in self.wallet.all_addresses(): + if self.wallet.is_change(address):continue + label = self.wallet.labels.get(address,'') + n = 0 + h = self.wallet.history.get(address,[]) + for item in h: + if not item['is_input'] : n=n+1 + tx = "None" if n==0 else "%d"%n + + c, u = self.wallet.get_addr_balance(address) + balance = format_satoshis( c + u, False, self.wallet.num_zeros ) + if address in self.wallet.frozen_addresses: + balance += '[F]' + + item = QTreeWidgetItem( [ address, label, balance, tx] ) + item.setFont(0, QFont(MONOSPACE_FONT)) + self.receive_list.addTopLevelItem(item) + + def show_contact_details(self, item, column): + m = unicode(item.text(0)) + a = self.wallet.aliases.get(m) + if a: + if a[0] in self.wallet.authorities.keys(): + s = self.wallet.authorities.get(a[0]) + else: + s = "self-signed" + msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0] + QMessageBox.information(self, 'Alias', msg, 'OK') + + def update_contacts_tab(self): + self.contacts_list.clear() + self.clear_buttons(self.contacts_buttons_hbox) + + for alias, v in self.wallet.aliases.items(): + s, target = v + item = QTreeWidgetItem( [ target, alias, '-'] ) + self.contacts_list.addTopLevelItem(item) + + for address in self.wallet.addressbook: + label = self.wallet.labels.get(address,'') + n = 0 + for item in self.wallet.tx_history.values(): + if address in item['outputs'] : n=n+1 + tx = "None" if n==0 else "%d"%n + item = QTreeWidgetItem( [ address, label, tx] ) + item.setFont(0, QFont(MONOSPACE_FONT)) + self.contacts_list.addTopLevelItem(item) + + + def create_wall_tab(self): + self.textbox = textbox = QTextEdit(self) + textbox.setFont(QFont(MONOSPACE_FONT)) + textbox.setReadOnly(True) + return textbox + + def create_status_bar(self): + sb = QStatusBar() + sb.setFixedHeight(35) + sb.addPermanentWidget( StatusBarButton( QIcon(":icons/lock.png"), "Password", lambda: self.change_password_dialog(self.wallet, self) ) ) + sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), "Preferences", self.settings_dialog ) ) + sb.addPermanentWidget( StatusBarButton( QIcon(":icons/seed.png"), "Seed", lambda: self.show_seed_dialog(self.wallet, self) ) ) + self.status_button = StatusBarButton( QIcon(":icons/status_disconnected.png"), "Network", lambda: self.network_dialog(self.wallet, self) ) + sb.addPermanentWidget( self.status_button ) + self.setStatusBar(sb) + + def newaddress_dialog(self): + text, ok = QInputDialog.getText(self, 'New Contact', 'Address:') + address = unicode(text) + if ok: + if self.wallet.is_valid(address): + self.wallet.addressbook.append(address) + self.wallet.save() + self.update_contacts_tab() + else: + QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK') + + @staticmethod + def show_seed_dialog(wallet, parent=None): + from electrum import mnemonic + if wallet.use_encryption: + password = parent.password_dialog() + if not password: return + else: + password = None + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + QMessageBox.warning(parent, 'Error', 'Invalid Password', 'OK') + return + + msg = "Your wallet generation seed is:\n\n" + seed \ + + "\n\nPlease keep it in a safe place; if you lose it,\nyou will not be able to restore your wallet.\n\n" \ + + "Equivalently, your wallet seed can be stored and\nrecovered with the following mnemonic code:\n\n\"" \ + + ' '.join(mnemonic.mn_encode(seed)) + "\"\n\n\n" + + d = QDialog(None) + d.setModal(1) + d.setWindowTitle("Seed") + d.setMinimumSize(400, 270) + + vbox = QVBoxLayout() + hbox = QHBoxLayout() + vbox2 = QVBoxLayout() + l = QLabel() + l.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56)) + vbox2.addWidget(l) + vbox2.addStretch(1) + hbox.addLayout(vbox2) + hbox.addWidget(QLabel(msg)) + vbox.addLayout(hbox) + + hbox = QHBoxLayout() + hbox.addStretch(1) + + + if parent: + app = parent.app + else: + app = QApplication + + b = QPushButton("Copy to Clipboard") + b.clicked.connect(lambda: app.clipboard().setText(' '.join(mnemonic.mn_encode(seed)))) + hbox.addWidget(b) + b = QPushButton("View as QR Code") + b.clicked.connect(lambda: ElectrumWindow.show_seed_qrcode(seed)) + hbox.addWidget(b) + + b = QPushButton("OK") + b.clicked.connect(d.accept) + hbox.addWidget(b) + vbox.addLayout(hbox) + d.setLayout(vbox) + d.exec_() + + @staticmethod + def show_seed_qrcode(seed): + if not seed: return + d = QDialog(None) + d.setModal(1) + d.setWindowTitle("Seed") + d.setMinimumSize(270, 300) + vbox = QVBoxLayout() + vbox.addWidget(QRCodeWidget(seed)) + hbox = QHBoxLayout() + hbox.addStretch(1) + b = QPushButton("OK") + hbox.addWidget(b) + b.clicked.connect(d.accept) + + vbox.addLayout(hbox) + d.setLayout(vbox) + d.exec_() + + def show_address_qrcode(self,address): + if not address: return + d = QDialog(None) + d.setModal(1) + d.setWindowTitle(address) + d.setMinimumSize(270, 350) + vbox = QVBoxLayout() + qrw = QRCodeWidget(address) + vbox.addWidget(qrw) + + hbox = QHBoxLayout() + amount_e = QLineEdit() + hbox.addWidget(QLabel('Amount')) + hbox.addWidget(amount_e) + vbox.addLayout(hbox) + + #hbox = QHBoxLayout() + #label_e = QLineEdit() + #hbox.addWidget(QLabel('Label')) + #hbox.addWidget(label_e) + #vbox.addLayout(hbox) + + def amount_changed(): + amount = numbify(amount_e) + #label = str( label_e.getText() ) + if amount is not None: + qrw.set_addr('bitcoin:%s?amount=%s'%(address,str( Decimal(amount) /100000000))) + else: + qrw.set_addr( address ) + qrw.repaint() + + def do_save(): + from electrum import bmp + bmp.save_qrcode(qrw.qr, "qrcode.bmp") + self.show_message("QR code saved to file 'qrcode.bmp'") + + amount_e.textChanged.connect( amount_changed ) + + hbox = QHBoxLayout() + hbox.addStretch(1) + b = QPushButton("Save") + b.clicked.connect(do_save) + hbox.addWidget(b) + b = QPushButton("Close") + hbox.addWidget(b) + b.clicked.connect(d.accept) + + vbox.addLayout(hbox) + d.setLayout(vbox) + d.exec_() + + def question(self, msg): + return QMessageBox.question(self, 'Message', msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes + + def show_message(self, msg): + QMessageBox.information(self, 'Message', msg, 'OK') + + def password_dialog(self ): + d = QDialog(self) + d.setModal(1) + + pw = QLineEdit() + pw.setEchoMode(2) + + vbox = QVBoxLayout() + msg = 'Please enter your password' + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + grid.addWidget(QLabel('Password'), 1, 0) + grid.addWidget(pw, 1, 1) + vbox.addLayout(grid) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + return unicode(pw.text()) + + @staticmethod + def change_password_dialog( wallet, parent=None ): + d = QDialog(parent) + d.setModal(1) + + pw = QLineEdit() + pw.setEchoMode(2) + new_pw = QLineEdit() + new_pw.setEchoMode(2) + conf_pw = QLineEdit() + conf_pw.setEchoMode(2) + + vbox = QVBoxLayout() + if parent: + msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if wallet.use_encryption else 'Your wallet keys are not encrypted' + else: + msg = "Please choose a password to encrypt your wallet keys.\nLeave these fields empty if you want to disable encryption." + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + + if wallet.use_encryption: + grid.addWidget(QLabel('Password'), 1, 0) + grid.addWidget(pw, 1, 1) + + grid.addWidget(QLabel('New Password'), 2, 0) + grid.addWidget(new_pw, 2, 1) + + grid.addWidget(QLabel('Confirm Password'), 3, 0) + grid.addWidget(conf_pw, 3, 1) + vbox.addLayout(grid) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + + password = unicode(pw.text()) if wallet.use_encryption else None + new_password = unicode(new_pw.text()) + new_password2 = unicode(conf_pw.text()) + + try: + seed = wallet.pw_decode( wallet.seed, password) + except: + QMessageBox.warning(parent, 'Error', 'Incorrect Password', 'OK') + return + + if new_password != new_password2: + QMessageBox.warning(parent, 'Error', 'Passwords do not match', 'OK') + return + + wallet.update_password(seed, new_password) + + @staticmethod + def seed_dialog(wallet, parent=None): + d = QDialog(parent) + d.setModal(1) + + vbox = QVBoxLayout() + msg = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet." + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + + seed_e = QLineEdit() + grid.addWidget(QLabel('Seed or mnemonic'), 1, 0) + grid.addWidget(seed_e, 1, 1) + + gap_e = QLineEdit() + gap_e.setText("5") + grid.addWidget(QLabel('Gap limit'), 2, 0) + grid.addWidget(gap_e, 2, 1) + gap_e.textChanged.connect(lambda: numbify(gap_e,True)) + vbox.addLayout(grid) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + + try: + gap = int(unicode(gap_e.text())) + except: + QMessageBox.warning(None, 'Error', 'error', 'OK') + sys.exit(0) + + try: + seed = unicode(seed_e.text()) + seed.decode('hex') + except: + from electrum import mnemonic + print "not hex, trying decode" + try: + seed = mnemonic.mn_decode( seed.split(' ') ) + except: + QMessageBox.warning(None, 'Error', 'I cannot decode this', 'OK') + sys.exit(0) + if not seed: + QMessageBox.warning(None, 'Error', 'no seed', 'OK') + sys.exit(0) + + wallet.seed = str(seed) + #print repr(wallet.seed) + wallet.gap_limit = gap + return True + + + def settings_dialog(self): + d = QDialog(self) + d.setModal(1) + + vbox = QVBoxLayout() + + msg = 'Here are the settings of your wallet.' + vbox.addWidget(QLabel(msg)) + + grid = QGridLayout() + grid.setSpacing(8) + vbox.addLayout(grid) + + fee_e = QLineEdit() + fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) ) + grid.addWidget(QLabel('Fee per tx. input'), 2, 0) + grid.addWidget(fee_e, 2, 1) + fee_e.textChanged.connect(lambda: numbify(fee_e,False)) + + nz_e = QLineEdit() + nz_e.setText("%d"% self.wallet.num_zeros) + grid.addWidget(QLabel('Zeros displayed after decimal point'), 3, 0) + grid.addWidget(nz_e, 3, 1) + nz_e.textChanged.connect(lambda: numbify(nz_e,True)) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + + fee = unicode(fee_e.text()) + try: + fee = int( 100000000 * Decimal(fee) ) + except: + QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK') + return + + if self.wallet.fee != fee: + self.wallet.fee = fee + self.wallet.save() + + nz = unicode(nz_e.text()) + try: + nz = int( nz ) + if nz>8: nz=8 + except: + QMessageBox.warning(self, 'Error', 'Invalid value:%s'%nz, 'OK') + return + + if self.wallet.num_zeros != nz: + self.wallet.num_zeros = nz + self.update_history_tab() + self.update_receive_tab() + self.wallet.save() + + @staticmethod + def network_dialog(wallet, parent=None): + interface = wallet.interface + if parent: + if interface.is_connected: + status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, wallet.blocks) + else: + status = "Not connected" + server = wallet.server + else: + import random + status = "Please choose a server." + server = random.choice( DEFAULT_SERVERS ) + + if not wallet.interface.servers: + servers_list = [] + for x in DEFAULT_SERVERS: + h,port,protocol = x.split(':') + servers_list.append( (h,[(protocol,port)] ) ) + else: + servers_list = wallet.interface.servers + + plist = {} + for item in servers_list: + host, pp = item + z = {} + for item2 in pp: + protocol, port = item2 + z[protocol] = port + plist[host] = z + + d = QDialog(parent) + d.setModal(1) + d.setWindowTitle('Server') + d.setMinimumSize(375, 20) + + vbox = QVBoxLayout() + vbox.setSpacing(20) + + hbox = QHBoxLayout() + l = QLabel() + l.setPixmap(QPixmap(":icons/network.png")) + hbox.addWidget(l) + hbox.addWidget(QLabel(status)) + + vbox.addLayout(hbox) + + hbox = QHBoxLayout() + host_line = QLineEdit() + host_line.setText(server) + hbox.addWidget(QLabel('Connect to:')) + hbox.addWidget(host_line) + vbox.addLayout(hbox) + + hbox = QHBoxLayout() + + buttonGroup = QGroupBox("protocol") + radio1 = QRadioButton("tcp", buttonGroup) + radio2 = QRadioButton("http", buttonGroup) + + def current_line(): + return unicode(host_line.text()).split(':') + + def set_button(protocol): + if protocol == 't': + radio1.setChecked(1) + elif protocol == 'h': + radio2.setChecked(1) + + def set_protocol(protocol): + host = current_line()[0] + pp = plist[host] + if protocol not in pp.keys(): + protocol = pp.keys()[0] + set_button(protocol) + port = pp[protocol] + host_line.setText( host + ':' + port + ':' + protocol) + + radio1.clicked.connect(lambda x: set_protocol('t') ) + radio2.clicked.connect(lambda x: set_protocol('h') ) + + set_button(current_line()[2]) + + hbox.addWidget(QLabel('Protocol:')) + hbox.addWidget(radio1) + hbox.addWidget(radio2) + + vbox.addLayout(hbox) + + if wallet.interface.servers: + label = 'Active Servers' + else: + label = 'Default Servers' + + servers_list_widget = QTreeWidget(parent) + servers_list_widget.setHeaderLabels( [ label ] ) + servers_list_widget.setMaximumHeight(150) + for host in plist.keys(): + servers_list_widget.addTopLevelItem(QTreeWidgetItem( [ host ] )) + + def do_set_line(x): + host = unicode(x.text(0)) + pp = plist[host] + if 't' in pp.keys(): + protocol = 't' + else: + protocol = pp.keys()[0] + port = pp[protocol] + host_line.setText( host + ':' + port + ':' + protocol) + set_button(protocol) + + servers_list_widget.connect(servers_list_widget, SIGNAL('itemClicked(QTreeWidgetItem*, int)'), do_set_line) + vbox.addWidget(servers_list_widget) + + vbox.addLayout(ok_cancel_buttons(d)) + d.setLayout(vbox) + + if not d.exec_(): return + server = unicode( host_line.text() ) + + try: + wallet.set_server(server) + except: + QMessageBox.information(None, 'Error', 'error', 'OK') + if parent == None: + sys.exit(1) + else: + return + + return True + + + +class ElectrumGui(): + + def __init__(self, wallet): + self.wallet = wallet + self.app = QApplication(sys.argv) + + def waiting_dialog(self): + + s = Timer() + s.start() + w = QDialog() + w.resize(200, 70) + w.setWindowTitle('Electrum') + l = QLabel('') + vbox = QVBoxLayout() + vbox.addWidget(l) + w.setLayout(vbox) + w.show() + def f(): + if self.wallet.up_to_date: w.close() + else: + l.setText("Please wait...\nGenerating addresses: %d"%len(self.wallet.all_addresses())) + pass + w.connect(s, QtCore.SIGNAL('timersignal'), f) + self.wallet.interface.poke() + w.exec_() + w.destroy() + + + def restore_or_create(self): + + msg = "Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" + r = QMessageBox.question(None, 'Message', msg, 'create', 'restore', 'cancel', 0, 2) + if r==2: return False + + is_recovery = (r==1) + wallet = self.wallet + # ask for the server. + if not ElectrumWindow.network_dialog( wallet, parent=None ): return False + + if not is_recovery: + wallet.new_seed(None) + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.up_to_date = False + self.waiting_dialog() + # run a dialog indicating the seed, ask the user to remember it + ElectrumWindow.show_seed_dialog(wallet) + #ask for password + ElectrumWindow.change_password_dialog(wallet) + else: + # ask for seed and gap. + if not ElectrumWindow.seed_dialog( wallet ): return False + wallet.init_mpk( wallet.seed ) + wallet.up_to_date_event.clear() + wallet.up_to_date = False + self.waiting_dialog() + if wallet.is_found(): + # history and addressbook + wallet.update_tx_history() + wallet.fill_addressbook() + print "recovery successful" + wallet.save() + else: + QMessageBox.information(None, 'Message', "No transactions found for this seed", 'OK') + + wallet.save() + return True + + def main(self,url): + s = Timer() + s.start() + w = ElectrumWindow(self.wallet) + if url: w.set_url(url) + w.app = self.app + w.connect_slots(s) + w.update_wallet() + + self.app.exec_() diff --git a/interface.py b/lib/interface.py diff --git a/mnemonic.py b/lib/mnemonic.py diff --git a/msqr.py b/lib/msqr.py diff --git a/pyqrnative.py b/lib/pyqrnative.py diff --git a/ripemd.py b/lib/ripemd.py diff --git a/lib/version.py b/lib/version.py @@ -0,0 +1,2 @@ +ELECTRUM_VERSION = "0.48" +SEED_VERSION = 4 # bump this everytime the seed generation is modified diff --git a/wallet.py b/lib/wallet.py diff --git a/peers b/peers @@ -1,8 +1,8 @@ #!/usr/bin/env python -import interface +from electrum import TcpStratumInterface -i = interface.TcpStratumInterface('ecdsa.org', 50001) +i = TcpStratumInterface('ecdsa.org', 50001) i.start() i.send([('server.peers.subscribe',[])]) diff --git a/setup.py b/setup.py @@ -3,15 +3,18 @@ # python setup.py sdist --format=zip,gztar from distutils.core import setup -from version import ELECTRUM_VERSION as version +from lib.version import ELECTRUM_VERSION as version setup(name = "Electrum", version = version, + package_dir = {'electrum': 'lib'}, + scripts= ['electrum', 'watch_address', 'blocks'], + py_modules = ['electrum.version','electrum.wallet','electrum.interface','electrum.gui','electrum.gui_qt','electrum.icons_rc','electrum.mnemonic','electrum.pyqrnative','electrum.bmp'], description = "Lightweight Bitcoin Wallet", author = "thomasv", license = "GNU GPLv3", url = "http://ecdsa/electrum", long_description = """Lightweight Bitcoin Wallet""" -) +) diff --git a/version.py b/version.py @@ -1,2 +0,0 @@ -ELECTRUM_VERSION = "0.47b" -SEED_VERSION = 4 # bump this everytime the seed generation is modified diff --git a/watch_address b/watch_address @@ -1,12 +1,14 @@ #!/usr/bin/env python -import interface, sys +import sys +from electrum import TcpStratumInterface + try: addr = sys.argv[1] except: print "usage: watch_address <bitcoin_address>" -i = interface.TcpStratumInterface('ecdsa.org', 50001) +i = TcpStratumInterface('ecdsa.org', 50001) i.start() i.send([('blockchain.address.subscribe',[addr])])