#!/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/>.
# use this because this file is named gtk.py
from __future__ import absolute_import
import datetime
import thread , time , ast , sys , re
import socket , traceback
import pygtk
pygtk . require ( ' 2.0 ' )
import gtk , gobject
from decimal import Decimal
from electrum . util import print_error
from electrum . bitcoin import is_valid
from electrum import mnemonic , pyqrnative , WalletStorage , Wallet
gtk . gdk . threads_init ( )
APP_NAME = " Electrum "
import platform
MONOSPACE_FONT = ' Lucida Console ' if platform . system ( ) == ' Windows ' else ' monospace '
from electrum . util import format_satoshis
from electrum . network import DEFAULT_SERVERS
from electrum . 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 ) * 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 ) :
if not wallet . seed :
show_message ( " No seed " )
return
try :
seed = wallet . decode_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 \n Please 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 ( ) :
# 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. \n Do 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
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 . 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 )
dialog . show ( )
r = dialog . run ( )
seed = seed_entry . get_text ( )
dialog . destroy ( )
if r == gtk . RESPONSE_CANCEL :
return False
try :
seed . decode ( ' hex ' )
except :
print_error ( " Warning: Not hex, trying decode " )
seed = mnemonic . mn_decode ( seed . split ( ' ' ) )
if not seed :
show_message ( " no seed " )
return False
return seed
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 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 ( ' 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. \n For example, if this number is 2, then ' 5. ' is displayed as ' 5.00 ' " )
nz . show ( )
vbox . pack_start ( nz , False , False , 5 )
# gui setting
gui_box = gtk . HBox ( )
gui_label = gtk . Label ( ' Default GUI: ' )
gui_label . set_size_request ( 150 , 10 )
gui_label . show ( )
gui_box . pack_start ( gui_label , False , False , 10 )
gui_combo = gtk . combo_box_new_text ( )
gui_names = [ ' lite ' , ' classic ' , ' gtk ' , ' text ' ]
for name in gui_names : gui_combo . append_text ( name . capitalize ( ) )
gui_combo . show ( )
gui_box . pack_start ( gui_combo , False , False , 10 )
gui_combo . set_active ( gui_names . index ( wallet . config . get ( " gui " , " lite " ) ) )
gui_box . show ( )
add_help_button ( gui_box , " Select which GUI mode to use at start up. " )
vbox . pack_start ( gui_box , False , False , 5 )
dialog . show ( )
r = dialog . run ( )
fee = fee_entry . get_text ( )
nz = nz_entry . get_text ( )
gui = gui_names [ gui_combo . get_active ( ) ]
dialog . destroy ( )
if r == gtk . RESPONSE_CANCEL :
return
try :
fee = int ( 100000000 * Decimal ( fee ) )
except :
show_message ( " error " )
return
wallet . set_fee ( fee )
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 ( )
wallet . config . set_key ( ' gui ' , gui , True )
def run_network_dialog ( network , parent ) :
image = gtk . Image ( )
image . set_from_stock ( gtk . STOCK_NETWORK , gtk . ICON_SIZE_DIALOG )
interface = network . interface
if parent :
if interface . is_connected :
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. \n Select cancel if you are offline. "
server = interface . server
servers = network . get_servers ( )
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 = servers [ 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 servers . keys ( ) :
server_list . append ( [ host ] )
treeview = gtk . TreeView ( model = server_list )
treeview . show ( )
if 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 = 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_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 :
interface . set_server ( server )
except :
show_message ( " error: " + server )
return False
if parent :
wallet . config . set_key ( " server " , server , True )
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 not wallet . seed :
show_message ( " No seed " )
return
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 . decode_seed ( password )
except :
show_message ( " Incorrect password " )
return
if new_password != new_password2 :
show_message ( " passwords do not match " )
return
wallet . update_password ( seed , password , 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 , 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 . WINDOW_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 . 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 . network , 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 )
if self . wallet . seed :
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 ( )
if self . wallet . seed :
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 ( )
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 :
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 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 ) )
scroll = gtk . ScrolledWindow ( )
scroll . add ( tv )
page . pack_start ( scroll )
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 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 ) ] , password , fee )
except BaseException , 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 == 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 . 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 . 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 . 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 . 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 . ICON_SIZE_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 ) :
if self . wallet . is_change ( address ) : continue
label = self . wallet . labels . get ( address )
h = self . wallet . history . get ( address , [ ] )
n = len ( h )
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 = 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 :
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 ' ) , tx . inputs )
outputs = map ( lambda x : x . get ( ' address ' ) , tx . d [ ' 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 . 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 is_valid ( address ) :
self . wallet . add_contact ( address , label )
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 , 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 )
self . wallet . start_threads ( self . network )
if action == ' create ' :
wallet . init_seed ( None )
wallet . save_seed ( )
wallet . create_accounts ( )
wallet . synchronize ( ) # generate first addresses offline
elif action == ' restore ' :
seed = self . seed_dialog ( )
wallet . init_seed ( seed )
wallet . save_seed ( )
self . restore_wallet ( wallet )
else :
exit ( )
else :
self . wallet = Wallet ( storage )
self . wallet . start_threads ( self . network )
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 verify_seed ( self ) :
self . wallet . save_seed ( )
return True
def network_dialog ( self ) :
return run_network_dialog ( self . network , parent = None )
def show_seed ( self ) :
show_seed_dialog ( self . wallet , None , None )
def password_dialog ( self ) :
change_password_dialog ( self . wallet , None , None )
def restore_wallet ( self , 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 . 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 . RESPONSE_CANCEL : return False
if not wallet . is_found ( ) :
show_message ( " No transactions found for this seed " )
return True