#!/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 . import datetime import thread, time, ast, sys, re import socket, traceback import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GObject, cairo from decimal import Decimal from electrum_nvc.util import print_error from electrum_nvc.bitcoin import is_valid from electrum_nvc import mnemonic, WalletStorage, Wallet Gdk.threads_init() APP_NAME = "Electrum-NVC" import platform MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace' from electrum_nvc.util import format_satoshis, parse_URI from electrum_nvc.network import DEFAULT_SERVERS from electrum_nvc.bitcoin import MIN_RELAY_TX_FEE 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) * 1000000 ) except Exception: amount = None else: try: amount = int( s ) except Exception: amount = None entry.set_text(s) return amount def show_seed_dialog(seed, parent): if not seed: show_message("No seed") return dialog = Gtk.MessageDialog( parent = parent, flags = Gtk.DialogFlags.MODAL, buttons = Gtk.ButtonsType.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" ) dialog.set_title("Seed") dialog.show() dialog.run() dialog.destroy() def restore_create_dialog(): # 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.DialogFlags.MODAL, 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, True, True, 0) dialog.show() r = dialog.run() dialog.destroy() if r==2: return False return 'restore' if r==1 else 'create' def run_recovery_dialog(): 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.DialogFlags.MODAL, buttons = Gtk.ButtonsType.OK_CANCEL, message_format = message) vbox = dialog.vbox dialog.set_default_response(Gtk.ResponseType.OK) # ask seed, server and gap in the same dialog seed_box = Gtk.HBox() seed_label = Gtk.Label(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) dialog.show() r = dialog.run() seed = seed_entry.get_text() dialog.destroy() if r==Gtk.ResponseType.CANCEL: return False if Wallet.is_seed(seed): return seed show_message("no seed") return False def run_settings_dialog(self): 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 = self.window, flags = Gtk.DialogFlags.MODAL, buttons = Gtk.ButtonsType.OK_CANCEL, message_format = message) image = Gtk.Image() image.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.DIALOG) image.show() dialog.set_image(image) dialog.set_title("Settings") vbox = dialog.vbox dialog.set_default_response(Gtk.ResponseType.OK) fee = Gtk.HBox() fee_entry = Gtk.Entry() fee_label = Gtk.Label(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(self.wallet.fee) /1000000 ) ) fee_entry.connect('changed', numbify, False) fee_entry.show() fee.pack_start(fee_entry,False,False, 10) add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001') fee.show() vbox.pack_start(fee, False,False, 5) nz = Gtk.HBox() nz_entry = Gtk.Entry() nz_label = Gtk.Label(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( self.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.ResponseType.CANCEL: return try: fee = int( 1000000 * Decimal(fee) ) except Exception: show_message("error") return self.wallet.set_fee(fee) try: nz = int( nz ) if nz>8: nz = 8 except Exception: show_message("error") return if self.num_zeros != nz: self.num_zeros = nz self.config.set_key('num_zeros',nz,True) self.update_history_tab() def run_network_dialog( network, parent ): image = Gtk.Image() image.set_from_stock(Gtk.STOCK_NETWORK, Gtk.IconSize.DIALOG) if parent: if network.is_connected(): interface = network.interface status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, network.blockchain.height()) else: status = "Not connected" else: import random status = "Please choose a server.\nSelect cancel if you are offline." if network.is_connected(): server = interface.server host, port, protocol = server.split(':') servers = network.get_servers() dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, status) dialog.set_title("Server") dialog.set_image(image) image.show() vbox = dialog.vbox host_box = Gtk.HBox() host_label = Gtk.Label(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) if network.is_connected(): host_entry.set_text(server) else: host_entry.set_text("Not Connected") host_entry.show() host_box.pack_start(host_entry, False, False, 10) add_help_button(host_box, 'The name, port number and protocol of your Electrum server, separated by a colon. Example: "ecdsa.org:50002:s". 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(label='Protocol:') p_label.set_size_request(100,-1) p_label.show() p_box.pack_start(p_label, False, False, 10) combobox = Gtk.ComboBoxText() combobox.show() combobox.append_text("TCP") combobox.append_text("SSL") combobox.append_text("HTTP") combobox.append_text("HTTPS") p_box.pack_start(combobox, True, True, 0) def current_line(): return unicode(host_entry.get_text()).split(':') def set_combobox(protocol): combobox.set_active('tshg'.index(protocol)) def set_protocol(protocol): host = current_line()[0] pp = servers[host] if protocol not in pp.keys(): protocol = pp.keys()[0] set_combobox(protocol) port = pp[protocol] host_entry.set_text( host + ':' + port + ':' + protocol) combobox.connect("changed", lambda x:set_protocol('tshg'[combobox.get_active()])) if network.is_connected(): set_combobox(protocol) server_list = Gtk.ListStore(str) for host in servers.keys(): server_list.append([host]) treeview = Gtk.TreeView(model=server_list) treeview.show() label = 'Active Servers' if network.irc_servers else 'Default Servers' tvcolumn = Gtk.TreeViewColumn(label) treeview.append_column(tvcolumn) cell = Gtk.CellRendererText() tvcolumn.pack_start(cell, False) tvcolumn.add_attribute(cell, 'text', 0) vbox.pack_start(host_box, False,False, 5) vbox.pack_start(p_box, True, True, 0) #scroll = Gtk.ScrolledWindow() #scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) #scroll.add_with_viewport(treeview) #scroll.show() #vbox.pack_start(scroll, True) vbox.pack_start(treeview, True, True, 0) def my_treeview_cb(treeview): path, view_column = treeview.get_cursor() host = server_list.get_value( server_list.get_iter(path), 0) pp = servers[host] if 't' in pp.keys(): protocol = 't' else: protocol = pp.keys()[0] port = pp[protocol] host_entry.set_text( host + ':' + port + ':' + protocol) set_combobox(protocol) treeview.connect('cursor-changed', my_treeview_cb) dialog.show_all() r = dialog.run() server = host_entry.get_text() dialog.destroy() if r==Gtk.ResponseType.CANCEL: return False try: host, port, protocol = server.split(':') proxy = network.config.get('proxy') auto_connect = network.config.get('auto_cycle') network.set_parameters(host, port, protocol, proxy, auto_connect) except Exception: show_message("error:" + server) return False def show_message(message, parent=None): dialog = Gtk.MessageDialog( parent = parent, flags = Gtk.DialogFlags.MODAL, buttons = Gtk.ButtonsType.CLOSE, message_format = message ) dialog.show() dialog.run() dialog.destroy() def password_line(label): password = Gtk.HBox() password_label = Gtk.Label(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.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.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.ResponseType.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.ResponseType.CANCEL: return pw def change_password_dialog(is_encrypted, parent): if parent: msg = 'Your wallet is encrypted. Use this dialog to change the password. To disable wallet encryption, enter an empty new password.' if is_encrypted else 'Your wallet keys are not encrypted' else: msg = "Please choose a password to encrypt your wallet keys" dialog = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, msg) dialog.set_title("Change password") image = Gtk.Image() image.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.DIALOG) image.show() dialog.set_image(image) if is_encrypted: 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 is_encrypted else None new_password = password_entry.get_text() new_password2 = password2_entry.get_text() dialog.destroy() if result == Gtk.ResponseType.CANCEL: return if new_password != new_password2: show_message("passwords do not match") return change_password_dialog(is_encrypted, parent) if not new_password: new_password = None return True, password, new_password 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, 0) class MyWindow(Gtk.Window): __gsignals__ = dict( mykeypress = (GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, None, (str,)) ) GObject.type_register(MyWindow) #FIXME: can't find docs how to create keybindings in PyGI #Gtk.binding_entry_add_signall(MyWindow, Gdk.KEY_W, Gdk.ModifierType.CONTROL_MASK, 'mykeypress', ['ctrl+W']) #Gtk.binding_entry_add_signall(MyWindow, Gdk.KEY_Q, Gdk.ModifierType.CONTROL_MASK, 'mykeypress', ['ctrl+Q']) class ElectrumWindow: def show_message(self, msg): show_message(msg, self.window) def __init__(self, wallet, config, network): self.config = config self.wallet = wallet self.network = network self.funds_error = False # True if not enough funds self.num_zeros = int(self.config.get('num_zeros',0)) self.window = MyWindow(Gtk.WindowType.TOPLEVEL) title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path if not self.wallet.seed: title += ' [seedless]' self.window.set_title(title) 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) self.wallet_updated = False vbox = Gtk.VBox() self.notebook = Gtk.Notebook() self.create_history_tab() if self.wallet.seed: 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.IconSize.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.network, self.window) ) self.network_button.add(self.status_image) self.network_button.set_relief(Gtk.ReliefStyle.NONE) self.network_button.show() self.status_bar.pack_end(self.network_button, False, False, 0) if self.wallet.seed: def seedb(w, wallet): if wallet.use_encryption: password = password_dialog(self.window) if not password: return else: password = None seed = wallet.get_mnemonic(password) show_seed_dialog(seed, self.window) button = Gtk.Button('S') button.connect("clicked", seedb, self.wallet ) button.set_relief(Gtk.ReliefStyle.NONE) button.show() self.status_bar.pack_end(button,False, False, 0) settings_icon = Gtk.Image() settings_icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.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) ) prefs_button.add(settings_icon) prefs_button.set_tooltip_text("Settings") prefs_button.set_relief(Gtk.ReliefStyle.NONE) prefs_button.show() self.status_bar.pack_end(prefs_button,False,False, 0) self.pw_icon = Gtk.Image() self.pw_icon.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION, Gtk.IconSize.MENU) self.pw_icon.set_alignment(0.5, 0.5) self.pw_icon.set_size_request(16,16 ) self.pw_icon.show() if self.wallet.seed: if self.wallet.use_encryption: self.pw_icon.set_tooltip_text('Wallet is encrypted') else: self.pw_icon.set_tooltip_text('Wallet is unencrypted') password_button = Gtk.Button() password_button.connect("clicked", self.do_update_password, self.wallet) password_button.add(self.pw_icon) password_button.set_relief(Gtk.ReliefStyle.NONE) password_button.show() self.status_bar.pack_end(password_button,False,False, 0) 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() self.network.register_callback('updated', self.update_callback) 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 Exception: 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, ()) if self.wallet.seed: thread.start_new_thread(check_recipient_thread, ()) self.notebook.set_current_page(0) def update_callback(self): self.wallet_updated = True def do_update_password(self, button, wallet): if not wallet.seed: show_message("No seed") return res = change_password_dialog(wallet.use_encryption, self.window) if res: _, password, new_password = res try: wallet.get_seed(password) except Exception: show_message("Incorrect password") return wallet.update_password(password, new_password) if wallet.use_encryption: self.pw_icon.set_tooltip_text('Wallet is encrypted') else: self.pw_icon.set_tooltip_text('Wallet is unencrypted') def add_tab(self, page, name): tab_label = Gtk.Label(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(label='Pay to:') payto_label.set_size_request(100,-1) payto.pack_start(payto_label, False, False, 0) payto_entry = Gtk.Entry() payto_entry.set_size_request(450, 26) payto.pack_start(payto_entry, False, False, 0) vbox.pack_start(payto, False, False, 5) message = Gtk.HBox() message_label = Gtk.Label(label='Description:') message_label.set_size_request(100,-1) message.pack_start(message_label, False, False, 0) message_entry = Gtk.Entry() message_entry.set_size_request(450, 26) message.pack_start(message_entry, False, False, 0) vbox.pack_start(message, False, False, 5) amount_box = Gtk.HBox() amount_label = Gtk.Label(label='Amount:') amount_label.set_size_request(100,-1) amount_box.pack_start(amount_label, False, False, 0) amount_entry = Gtk.Entry() amount_entry.set_size_request(120, -1) amount_box.pack_start(amount_entry, False, False, 0) vbox.pack_start(amount_box, False, False, 5) self.fee_box = fee_box = Gtk.HBox() fee_label = Gtk.Label(label='Fee:') fee_label.set_size_request(100,-1) fee_box.pack_start(fee_label, False, False, 0) fee_entry = Gtk.Entry() fee_entry.set_size_request(60, 26) fee_box.pack_start(fee_entry, False, False, 0) vbox.pack_start(fee_box, False, False, 5) end_box = Gtk.HBox() empty_label = Gtk.Label(label='') empty_label.set_size_request(100,-1) end_box.pack_start(empty_label, False, False, 0) 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(label='') payto_sig.pack_start(payto_sig_id, False, False, 0) 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 #assume two outputs - one for change inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee, 2 ) if not is_fee: fee_entry.set_text( str( Decimal( fee ) / 1000000 ) ) self.fee_box.show() if inputs: amount_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#000000")) fee_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#000000")) send_button.set_sensitive(True) else: send_button.set_sensitive(False) amount_entry.modify_text(Gtk.StateType.NORMAL, Gdk.color_parse("#cc0000")) fee_entry.modify_text(Gtk.StateType.NORMAL, 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.StateType.NORMAL, Gdk.color_parse("#eeeeee")) else: entry.set_editable(True) entry.set_has_frame(True) entry.modify_base(Gtk.StateType.NORMAL, Gdk.color_parse("#ffffff")) def set_url(self, url): payto, amount, label, message, payment_request = parse_URI(url) self.notebook.set_current_page(1) self.payto_entry.set_text(payto) self.message_entry.set_text(message) self.amount_entry.set_text(amount) self.payto_sig.set_visible(False) def create_about_tab(self): from gi.repository 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)) scroll = Gtk.ScrolledWindow() scroll.add(tv) page.pack_start(scroll, True, True, 0) 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.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.OK_CANCEL, msg) dialog.show() result = dialog.run() dialog.destroy() return result == Gtk.ResponseType.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 is_valid(to_address): self.show_message( "invalid novacoin address:\n"+to_address) return try: amount = int( Decimal(amount_entry.get_text()) * 1000000 ) except Exception: self.show_message( "invalid amount") return try: fee = int( Decimal(fee_entry.get_text()) * 1000000 ) except Exception: 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)], password, fee ) except Exception as e: self.show_message(str(e)) return if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE: self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." ) return if label: self.wallet.labels[tx.hash()] = label 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 == Gdk.EventType.DOUBLE_BUTTON_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 == Gdk.KEY_Up: if c and c[0] == 0: treeview.parent.grab_focus() treeview.set_cursor((0,)) elif event.keyval == Gdk.KEY_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) # here is an exception 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.set_label(tx,new_text) 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) # here is an exception 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.PolicyType.NEVER, Gtk.PolicyType.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, 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.set_label(address, new_text) 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) if is_recv: tvcolumn = Gtk.TreeViewColumn('Balance') treeview.append_column(tvcolumn) cell = Gtk.CellRendererText() tvcolumn.pack_start(cell, True) tvcolumn.add_attribute(cell, 'text', 3) tvcolumn = Gtk.TreeViewColumn('Type') treeview.append_column(tvcolumn) cell = Gtk.CellRendererText() tvcolumn.pack_start(cell, True) tvcolumn.add_attribute(cell, 'text', 4) scroll = Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.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, False, 0) def showqrcode(w, treeview, liststore): import qrcode path, col = treeview.get_cursor() if not path: return address = liststore.get_value(liststore.get_iter(path), 0) qr = qrcode.QRCode() qr.add_data(address) boxsize = 7 matrix = qr.get_matrix() boxcount_row = len(matrix) size = (boxcount_row + 4) * boxsize def area_expose_cb(area, cr): style = area.get_style() Gdk.cairo_set_source_color(cr, style.white) cr.rectangle(0, 0, size, size) cr.fill() Gdk.cairo_set_source_color(cr, style.black) for r in range(boxcount_row): for c in range(boxcount_row): if matrix[r][c]: cr.rectangle((c + 2) * boxsize, (r + 2) * boxsize, boxsize, boxsize) cr.fill() area = Gtk.DrawingArea() area.set_size_request(size, size) area.connect("draw", area_expose_cb) area.show() dialog = Gtk.Dialog(address, parent=self.window, flags=Gtk.DialogFlags.MODAL, 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, False, 0) 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: atom = Gdk.atom_intern('CLIPBOARD', True) c = Gtk.Clipboard.get(atom) c.set_text( address, len(address) ) button.connect("clicked", copy2clipboard, treeview, liststore) button.show() hbox.pack_start(button,False, False, 0) if is_recv: button = Gtk.Button("Freeze") def freeze_address(w, treeview, liststore, wallet): path, col = treeview.get_cursor() if path: address = liststore.get_value( liststore.get_iter(path), 0) if address in wallet.frozen_addresses: wallet.unfreeze(address) else: wallet.freeze(address) self.update_receiving_tab() button.connect("clicked", freeze_address, treeview, liststore, self.wallet) button.show() hbox.pack_start(button,False, False, 0) 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, False, 0) vbox = Gtk.VBox() vbox.pack_start(scroll,True, True, 0) vbox.pack_start(hbox, False, False, 0) return vbox def update_status_bar(self): interface = self.network.interface if self.funds_error: text = "Not enough funds" elif interface and interface.is_connected: self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height())) if not self.wallet.up_to_date: self.status_image.set_from_stock(Gtk.STOCK_REFRESH, Gtk.IconSize.MENU) text = "Synchronizing..." else: self.status_image.set_from_stock(Gtk.STOCK_YES, Gtk.IconSize.MENU) self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height())) c, u = self.wallet.get_balance() text = "Balance: %s "%( format_satoshis(c,False,self.num_zeros) ) if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() ) else: self.status_image.set_from_stock(Gtk.STOCK_NO, Gtk.IconSize.MENU) self.network_button.set_tooltip_text("Not connected.") text = "Not connected" self.status_bar.pop(self.context_id) self.status_bar.push(self.context_id, text) if self.wallet.up_to_date and self.wallet_updated: self.update_history_tab() self.update_receiving_tab() # addressbook too... self.info.set_text( self.network.banner ) self.wallet_updated = False def update_receiving_tab(self): self.recv_list.clear() for address in self.wallet.addresses(True): Type = "R" c = u = 0 if self.wallet.is_change(address): Type = "C" if address in self.wallet.imported_keys.keys(): Type = "I" c, u = self.wallet.get_addr_balance(address) if address in self.wallet.frozen_addresses: Type = Type + "F" label = self.wallet.labels.get(address) h = self.wallet.history.get(address,[]) n = len(h) tx = "0" if n==0 else "%d"%n self.recv_list.append((address, label, tx, format_satoshis(c,False,self.num_zeros), Type )) 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 = self.wallet.get_num_tx(address) self.addressbook_list.append((address, label, "%d"%n)) def update_history_tab(self): cursor = self.history_treeview.get_cursor()[0] self.history_list.clear() for item in self.wallet.get_tx_history(): tx_hash, conf, is_mine, value, fee, balance, timestamp = item if conf > 0: try: time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3] except Exception: time_str = "------" conf_icon = Gtk.STOCK_APPLY elif conf == -1: time_str = 'unverified' conf_icon = None else: time_str = 'pending' conf_icon = Gtk.STOCK_EXECUTE label, is_default_label = self.wallet.get_label(tx_hash) tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else '' details = self.get_tx_details(tx_hash) self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label, format_satoshis(value,True,self.num_zeros, whitespaces=True), format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] ) if cursor: self.history_treeview.set_cursor( cursor ) def get_tx_details(self, tx_hash): import datetime if not tx_hash: return '' tx = self.wallet.transactions.get(tx_hash) is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash) if timestamp: time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] else: time_str = 'pending' inputs = map(lambda x: x.get('address') if not x.get('is_coinbase') else 'Coinbase', tx.inputs) outputs = map(lambda x: x[1] if x[2] != 0 else 'Zero', tx.get_outputs()) tx_details = "Transaction Details" +"\n\n" \ + "Transaction ID:\n" + tx_hash + "\n\n" \ + "Status: %d confirmations\n"%conf if is_mine: if fee: tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \ + "Transaction fee: %s\n"% format_satoshis(fee, False) else: tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \ + "Transaction fee: unknown\n" else: tx_details += "Amount received: %s\n"% format_satoshis(v, False) \ tx_details += "Date: %s\n\n"%time_str \ + "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \ + "Outputs:\n-"+ '\n-'.join(outputs) return tx_details def newaddress_dialog(self, w): title = "New Contact" dialog = Gtk.Dialog(title, parent=self.window, flags=Gtk.DialogFlags.MODAL, buttons= ("cancel", 0, "ok",1) ) dialog.show() label = Gtk.HBox() label_label = Gtk.Label(label='Label:') label_label.set_size_request(120,10) label_label.show() label.pack_start(label_label, True, True, 0) label_entry = Gtk.Entry() label_entry.show() label.pack_start(label_entry, True, True, 0) label.show() dialog.vbox.pack_start(label, False, True, 5) address = Gtk.HBox() address_label = Gtk.Label(label='Address:') address_label.set_size_request(120,10) address_label.show() address.pack_start(address_label, True, True, 0) address_entry = Gtk.Entry() address_entry.show() address.pack_start(address_entry, True, True, 0) 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 is_valid(address): self.wallet.add_contact(address,label) self.update_sending_tab() else: errorDialog = Gtk.MessageDialog( parent=self.window, flags=Gtk.DialogFlags.MODAL, buttons= Gtk.ButtonsType.CLOSE, message_format = "Invalid address") errorDialog.show() errorDialog.run() errorDialog.destroy() class ElectrumGui(): def __init__(self, config, network): self.network = network self.config = config def main(self, url=None): storage = WalletStorage(self.config) if not storage.file_exists: action = self.restore_or_create() if not action: exit() self.wallet = wallet = Wallet(storage) gap = self.config.get('gap_limit', 5) if gap != 5: wallet.gap_limit = gap wallet.storage.put('gap_limit', gap, True) if action == 'create': seed = wallet.make_seed() show_seed_dialog(seed, None) r = change_password_dialog(False, None) password = r[2] if r else None wallet.add_seed(seed, password) wallet.create_accounts(password) wallet.synchronize() # generate first addresses offline elif action == 'restore': seed = self.seed_dialog() if not seed: exit() r = change_password_dialog(False, None) password = r[2] if r else None wallet.add_seed(seed, password) wallet.create_accounts(password) else: exit() else: self.wallet = Wallet(storage) action = None self.wallet.start_threads(self.network) if action == 'restore': self.restore_wallet(wallet) w = ElectrumWindow(self.wallet, self.config, self.network) if url: w.set_url(url) Gtk.main() def restore_or_create(self): return restore_create_dialog() def seed_dialog(self): return run_recovery_dialog() def network_dialog(self): return run_network_dialog( self.network, parent=None ) def restore_wallet(self, wallet): dialog = Gtk.MessageDialog( parent = None, flags = Gtk.DialogFlags.MODAL, buttons = Gtk.ButtonsType.CANCEL, message_format = "Please wait..." ) dialog.show() def recover_thread( wallet, dialog ): wallet.restore(lambda x:x) GObject.idle_add( dialog.destroy ) thread.start_new_thread( recover_thread, ( wallet, dialog ) ) r = dialog.run() dialog.destroy() if r==Gtk.ResponseType.CANCEL: return False if not wallet.is_found(): show_message("No transactions found for this seed") return True