3
# Electrum - lightweight Bitcoin client
4
# Copyright (C) 2011 thomasv@gitorious
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
# use this because this file is named gtk.py
20
from __future__ import absolute_import
23
import thread, time, ast, sys, re
24
import socket, traceback
28
from decimal import Decimal
29
from electrum.util import print_error
30
from electrum.bitcoin import is_valid
31
from electrum import mnemonic, pyqrnative, WalletStorage, Wallet
33
gtk.gdk.threads_init()
36
MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monospace'
38
from electrum.util import format_satoshis
39
from electrum.network import DEFAULT_SERVERS
40
from electrum.bitcoin import MIN_RELAY_TX_FEE
42
def numbify(entry, is_int = False):
43
text = entry.get_text().strip()
45
if not is_int: chars +='.'
46
s = ''.join([i for i in text if i in chars])
51
s = s[:p] + '.' + s[p:p+8]
53
amount = int( Decimal(s) * 100000000 )
67
def show_seed_dialog(wallet, password, parent):
69
show_message("No seed")
72
seed = wallet.get_seed(password)
74
show_message("Incorrect password")
76
dialog = gtk.MessageDialog(
78
flags = gtk.DIALOG_MODAL,
79
buttons = gtk.BUTTONS_OK,
80
message_format = "Your wallet generation seed is:\n\n" + seed \
81
+ "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
82
+ "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" + ' '.join(mnemonic.mn_encode(seed)) + "\"" )
83
dialog.set_title("Seed")
88
def restore_create_dialog():
90
# ask if the user wants to create a new wallet, or recover from a seed.
91
# if he wants to recover, and nothing is found, do not create wallet
92
dialog = gtk.Dialog("electrum", parent=None,
93
flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
94
buttons= ("create", 0, "restore",1, "cancel",2) )
96
label = gtk.Label("Wallet file not found.\nDo you want to create a new wallet,\n or to restore an existing one?" )
98
dialog.vbox.pack_start(label)
103
if r==2: return False
104
return 'restore' if r==1 else 'create'
108
def run_recovery_dialog():
109
message = "Please enter your wallet seed or the corresponding mnemonic list of words, and the gap limit of your wallet."
110
dialog = gtk.MessageDialog(
112
flags = gtk.DIALOG_MODAL,
113
buttons = gtk.BUTTONS_OK_CANCEL,
114
message_format = message)
117
dialog.set_default_response(gtk.RESPONSE_OK)
119
# ask seed, server and gap in the same dialog
120
seed_box = gtk.HBox()
121
seed_label = gtk.Label('Seed or mnemonic:')
122
seed_label.set_size_request(150,-1)
123
seed_box.pack_start(seed_label, False, False, 10)
125
seed_entry = gtk.Entry()
127
seed_entry.set_size_request(450,-1)
128
seed_box.pack_start(seed_entry, False, False, 10)
129
add_help_button(seed_box, '.')
131
vbox.pack_start(seed_box, False, False, 5)
135
seed = seed_entry.get_text()
138
if r==gtk.RESPONSE_CANCEL:
144
print_error("Warning: Not hex, trying decode")
145
seed = mnemonic.mn_decode( seed.split(' ') )
147
show_message("no seed")
154
def run_settings_dialog(self):
156
message = "Here are the settings of your wallet. For more explanations, click on the question mark buttons next to each input field."
158
dialog = gtk.MessageDialog(
159
parent = self.window,
160
flags = gtk.DIALOG_MODAL,
161
buttons = gtk.BUTTONS_OK_CANCEL,
162
message_format = message)
165
image.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG)
167
dialog.set_image(image)
168
dialog.set_title("Settings")
171
dialog.set_default_response(gtk.RESPONSE_OK)
174
fee_entry = gtk.Entry()
175
fee_label = gtk.Label('Transaction fee:')
176
fee_label.set_size_request(150,10)
178
fee.pack_start(fee_label,False, False, 10)
179
fee_entry.set_text( str( Decimal(self.wallet.fee) /100000000 ) )
180
fee_entry.connect('changed', numbify, False)
182
fee.pack_start(fee_entry,False,False, 10)
183
add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001')
185
vbox.pack_start(fee, False,False, 5)
188
nz_entry = gtk.Entry()
189
nz_label = gtk.Label('Display zeros:')
190
nz_label.set_size_request(150,10)
192
nz.pack_start(nz_label,False, False, 10)
193
nz_entry.set_text( str( self.num_zeros ))
194
nz_entry.connect('changed', numbify, True)
196
nz.pack_start(nz_entry,False,False, 10)
197
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'")
199
vbox.pack_start(nz, False,False, 5)
203
fee = fee_entry.get_text()
204
nz = nz_entry.get_text()
207
if r==gtk.RESPONSE_CANCEL:
211
fee = int( 100000000 * Decimal(fee) )
213
show_message("error")
215
self.wallet.set_fee(fee)
221
show_message("error")
224
if self.num_zeros != nz:
226
self.config.set_key('num_zeros',nz,True)
227
self.update_history_tab()
232
def run_network_dialog( network, parent ):
234
image.set_from_stock(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
236
if network.is_connected():
237
interface = network.interface
238
status = "Connected to %s:%d\n%d blocks"%(interface.host, interface.port, network.blockchain.height())
240
status = "Not connected"
243
status = "Please choose a server.\nSelect cancel if you are offline."
245
if network.is_connected():
246
server = interface.server
247
host, port, protocol = server.split(':')
249
servers = network.get_servers()
251
dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
252
gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, status)
253
dialog.set_title("Server")
254
dialog.set_image(image)
258
host_box = gtk.HBox()
259
host_label = gtk.Label('Connect to:')
260
host_label.set_size_request(100,-1)
262
host_box.pack_start(host_label, False, False, 10)
263
host_entry = gtk.Entry()
264
host_entry.set_size_request(200,-1)
265
if network.is_connected():
266
host_entry.set_text(server)
268
host_entry.set_text("Not Connected")
270
host_box.pack_start(host_entry, False, False, 10)
271
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)')
274
p_box = gtk.HBox(False, 10)
277
p_label = gtk.Label('Protocol:')
278
p_label.set_size_request(100,-1)
280
p_box.pack_start(p_label, False, False, 10)
282
combobox = gtk.combo_box_new_text()
284
combobox.append_text("TCP")
285
combobox.append_text("SSL")
286
combobox.append_text("HTTP")
287
combobox.append_text("HTTPS")
289
p_box.pack_start(combobox, True, True, 0)
292
return unicode(host_entry.get_text()).split(':')
294
def set_combobox(protocol):
295
combobox.set_active('tshg'.index(protocol))
297
def set_protocol(protocol):
298
host = current_line()[0]
300
if protocol not in pp.keys():
301
protocol = pp.keys()[0]
302
set_combobox(protocol)
304
host_entry.set_text( host + ':' + port + ':' + protocol)
306
combobox.connect("changed", lambda x:set_protocol('tshg'[combobox.get_active()]))
307
if network.is_connected():
308
set_combobox(protocol)
310
server_list = gtk.ListStore(str)
311
for host in servers.keys():
312
server_list.append([host])
314
treeview = gtk.TreeView(model=server_list)
317
label = 'Active Servers' if network.irc_servers else 'Default Servers'
318
tvcolumn = gtk.TreeViewColumn(label)
319
treeview.append_column(tvcolumn)
320
cell = gtk.CellRendererText()
321
tvcolumn.pack_start(cell, False)
322
tvcolumn.add_attribute(cell, 'text', 0)
324
vbox.pack_start(host_box, False,False, 5)
325
vbox.pack_start(p_box, True, True, 0)
327
#scroll = gtk.ScrolledWindow()
328
#scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
329
#scroll.add_with_viewport(treeview)
331
#vbox.pack_start(scroll, True)
332
vbox.pack_start(treeview, True)
334
def my_treeview_cb(treeview):
335
path, view_column = treeview.get_cursor()
336
host = server_list.get_value( server_list.get_iter(path), 0)
342
protocol = pp.keys()[0]
344
host_entry.set_text( host + ':' + port + ':' + protocol)
345
set_combobox(protocol)
347
treeview.connect('cursor-changed', my_treeview_cb)
351
server = host_entry.get_text()
354
if r==gtk.RESPONSE_CANCEL:
358
host, port, protocol = server.split(':')
359
proxy = network.config.get('proxy')
360
auto_connect = network.config.get('auto_cycle')
361
network.set_parameters(host, port, protocol, proxy, auto_connect)
363
show_message("error:" + server)
370
def show_message(message, parent=None):
371
dialog = gtk.MessageDialog(
373
flags = gtk.DIALOG_MODAL,
374
buttons = gtk.BUTTONS_CLOSE,
375
message_format = message )
380
def password_line(label):
381
password = gtk.HBox()
382
password_label = gtk.Label(label)
383
password_label.set_size_request(120,10)
384
password_label.show()
385
password.pack_start(password_label,False, False, 10)
386
password_entry = gtk.Entry()
387
password_entry.set_size_request(300,-1)
388
password_entry.set_visibility(False)
389
password_entry.show()
390
password.pack_start(password_entry,False,False, 10)
392
return password, password_entry
394
def password_dialog(parent):
395
dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
396
gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, "Please enter your password.")
397
dialog.get_image().set_visible(False)
398
current_pw, current_pw_entry = password_line('Password:')
399
current_pw_entry.connect("activate", lambda entry, dialog, response: dialog.response(response), dialog, gtk.RESPONSE_OK)
400
dialog.vbox.pack_start(current_pw, False, True, 0)
402
result = dialog.run()
403
pw = current_pw_entry.get_text()
405
if result != gtk.RESPONSE_CANCEL: return pw
407
def change_password_dialog(wallet, parent, icon):
409
show_message("No seed")
413
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'
415
msg = "Please choose a password to encrypt your wallet keys"
417
dialog = gtk.MessageDialog( parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
418
dialog.set_title("Change password")
420
image.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)
422
dialog.set_image(image)
424
if wallet.use_encryption:
425
current_pw, current_pw_entry = password_line('Current password:')
426
dialog.vbox.pack_start(current_pw, False, True, 0)
428
password, password_entry = password_line('New password:')
429
dialog.vbox.pack_start(password, False, True, 5)
430
password2, password2_entry = password_line('Confirm password:')
431
dialog.vbox.pack_start(password2, False, True, 5)
434
result = dialog.run()
435
password = current_pw_entry.get_text() if wallet.use_encryption else None
436
new_password = password_entry.get_text()
437
new_password2 = password2_entry.get_text()
439
if result == gtk.RESPONSE_CANCEL:
443
wallet.get_seed(password)
445
show_message("Incorrect password")
448
if new_password != new_password2:
449
show_message("passwords do not match")
452
wallet.update_password(password, new_password)
455
if wallet.use_encryption:
456
icon.set_tooltip_text('wallet is encrypted')
458
icon.set_tooltip_text('wallet is unencrypted')
461
def add_help_button(hbox, message):
462
button = gtk.Button('?')
463
button.connect("clicked", lambda x: show_message(message))
465
hbox.pack_start(button,False, False)
468
class MyWindow(gtk.Window): __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, (str,)) )
470
gobject.type_register(MyWindow)
471
gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.W, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+W')
472
gtk.binding_entry_add_signal(MyWindow, gtk.keysyms.Q, gtk.gdk.CONTROL_MASK, 'mykeypress', str, 'ctrl+Q')
475
class ElectrumWindow:
477
def show_message(self, msg):
478
show_message(msg, self.window)
480
def __init__(self, wallet, config, network):
483
self.network = network
484
self.funds_error = False # True if not enough funds
485
self.num_zeros = int(self.config.get('num_zeros',0))
487
self.window = MyWindow(gtk.WINDOW_TOPLEVEL)
488
title = 'Electrum ' + self.wallet.electrum_version + ' - ' + self.config.path
489
if not self.wallet.seed: title += ' [seedless]'
490
self.window.set_title(title)
491
self.window.connect("destroy", gtk.main_quit)
492
self.window.set_border_width(0)
493
self.window.connect('mykeypress', gtk.main_quit)
494
self.window.set_default_size(720, 350)
495
self.wallet_updated = False
499
self.notebook = gtk.Notebook()
500
self.create_history_tab()
502
self.create_send_tab()
503
self.create_recv_tab()
504
self.create_book_tab()
505
self.create_about_tab()
507
vbox.pack_start(self.notebook, True, True, 2)
509
self.status_bar = gtk.Statusbar()
510
vbox.pack_start(self.status_bar, False, False, 0)
512
self.status_image = gtk.Image()
513
self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
514
self.status_image.set_alignment(True, 0.5 )
515
self.status_image.show()
517
self.network_button = gtk.Button()
518
self.network_button.connect("clicked", lambda x: run_network_dialog(self.network, self.window) )
519
self.network_button.add(self.status_image)
520
self.network_button.set_relief(gtk.RELIEF_NONE)
521
self.network_button.show()
522
self.status_bar.pack_end(self.network_button, False, False)
525
def seedb(w, wallet):
526
if wallet.use_encryption:
527
password = password_dialog(self.window)
528
if not password: return
529
else: password = None
530
show_seed_dialog(wallet, password, self.window)
531
button = gtk.Button('S')
532
button.connect("clicked", seedb, wallet )
533
button.set_relief(gtk.RELIEF_NONE)
535
self.status_bar.pack_end(button,False, False)
537
settings_icon = gtk.Image()
538
settings_icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU)
539
settings_icon.set_alignment(0.5, 0.5)
540
settings_icon.set_size_request(16,16 )
543
prefs_button = gtk.Button()
544
prefs_button.connect("clicked", lambda x: run_settings_dialog(self) )
545
prefs_button.add(settings_icon)
546
prefs_button.set_tooltip_text("Settings")
547
prefs_button.set_relief(gtk.RELIEF_NONE)
549
self.status_bar.pack_end(prefs_button,False,False)
551
pw_icon = gtk.Image()
552
pw_icon.set_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU)
553
pw_icon.set_alignment(0.5, 0.5)
554
pw_icon.set_size_request(16,16 )
558
password_button = gtk.Button()
559
password_button.connect("clicked", lambda x: change_password_dialog(self.wallet, self.window, pw_icon))
560
password_button.add(pw_icon)
561
password_button.set_relief(gtk.RELIEF_NONE)
562
password_button.show()
563
self.status_bar.pack_end(password_button,False,False)
565
self.window.add(vbox)
566
self.window.show_all()
569
self.context_id = self.status_bar.get_context_id("statusbar")
570
self.update_status_bar()
572
self.network.register_callback('updated', self.update_callback)
575
def update_status_bar_thread():
577
gobject.idle_add( self.update_status_bar )
581
def check_recipient_thread():
585
if self.payto_entry.is_focus():
587
r = self.payto_entry.get_text()
591
if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r):
593
to_address = self.wallet.get_alias(r, interactive=False)
597
s = r + ' <' + to_address + '>'
598
gobject.idle_add( lambda: self.payto_entry.set_text(s) )
601
thread.start_new_thread(update_status_bar_thread, ())
603
thread.start_new_thread(check_recipient_thread, ())
604
self.notebook.set_current_page(0)
606
def update_callback(self):
607
self.wallet_updated = True
610
def add_tab(self, page, name):
611
tab_label = gtk.Label(name)
613
self.notebook.append_page(page, tab_label)
616
def create_send_tab(self):
618
page = vbox = gtk.VBox()
622
payto_label = gtk.Label('Pay to:')
623
payto_label.set_size_request(100,-1)
624
payto.pack_start(payto_label, False)
625
payto_entry = gtk.Entry()
626
payto_entry.set_size_request(450, 26)
627
payto.pack_start(payto_entry, False)
628
vbox.pack_start(payto, False, False, 5)
631
message_label = gtk.Label('Description:')
632
message_label.set_size_request(100,-1)
633
message.pack_start(message_label, False)
634
message_entry = gtk.Entry()
635
message_entry.set_size_request(450, 26)
636
message.pack_start(message_entry, False)
637
vbox.pack_start(message, False, False, 5)
639
amount_box = gtk.HBox()
640
amount_label = gtk.Label('Amount:')
641
amount_label.set_size_request(100,-1)
642
amount_box.pack_start(amount_label, False)
643
amount_entry = gtk.Entry()
644
amount_entry.set_size_request(120, -1)
645
amount_box.pack_start(amount_entry, False)
646
vbox.pack_start(amount_box, False, False, 5)
648
self.fee_box = fee_box = gtk.HBox()
649
fee_label = gtk.Label('Fee:')
650
fee_label.set_size_request(100,-1)
651
fee_box.pack_start(fee_label, False)
652
fee_entry = gtk.Entry()
653
fee_entry.set_size_request(60, 26)
654
fee_box.pack_start(fee_entry, False)
655
vbox.pack_start(fee_box, False, False, 5)
658
empty_label = gtk.Label('')
659
empty_label.set_size_request(100,-1)
660
end_box.pack_start(empty_label, False)
661
send_button = gtk.Button("Send")
663
end_box.pack_start(send_button, False, False, 0)
664
clear_button = gtk.Button("Clear")
666
end_box.pack_start(clear_button, False, False, 15)
667
send_button.connect("clicked", self.do_send, (payto_entry, message_entry, amount_entry, fee_entry))
668
clear_button.connect("clicked", self.do_clear, (payto_entry, message_entry, amount_entry, fee_entry))
670
vbox.pack_start(end_box, False, False, 5)
672
# display this line only if there is a signature
673
payto_sig = gtk.HBox()
674
payto_sig_id = gtk.Label('')
675
payto_sig.pack_start(payto_sig_id, False)
676
vbox.pack_start(payto_sig, True, True, 5)
679
self.user_fee = False
681
def entry_changed( entry, is_fee ):
682
self.funds_error = False
683
amount = numbify(amount_entry)
684
fee = numbify(fee_entry)
685
if not is_fee: fee = None
688
inputs, total, fee = self.wallet.choose_tx_inputs( amount, fee )
690
fee_entry.set_text( str( Decimal( fee ) / 100000000 ) )
693
amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
694
fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#000000"))
695
send_button.set_sensitive(True)
697
send_button.set_sensitive(False)
698
amount_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
699
fee_entry.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#cc0000"))
700
self.funds_error = True
702
amount_entry.connect('changed', entry_changed, False)
703
fee_entry.connect('changed', entry_changed, True)
705
self.payto_entry = payto_entry
706
self.payto_fee_entry = fee_entry
707
self.payto_sig_id = payto_sig_id
708
self.payto_sig = payto_sig
709
self.amount_entry = amount_entry
710
self.message_entry = message_entry
711
self.add_tab(page, 'Send')
713
def set_frozen(self,entry,frozen):
715
entry.set_editable(False)
716
entry.set_has_frame(False)
717
entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#eeeeee"))
719
entry.set_editable(True)
720
entry.set_has_frame(True)
721
entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffffff"))
723
def set_url(self, url):
724
payto, amount, label, message, signature, identity, url = self.wallet.parse_url(url, self.show_message, self.question)
725
self.notebook.set_current_page(1)
726
self.payto_entry.set_text(payto)
727
self.message_entry.set_text(message)
728
self.amount_entry.set_text(amount)
730
self.set_frozen(self.payto_entry,True)
731
self.set_frozen(self.amount_entry,True)
732
self.set_frozen(self.message_entry,True)
733
self.payto_sig_id.set_text( ' The bitcoin URI was signed by ' + identity )
735
self.payto_sig.set_visible(False)
737
def create_about_tab(self):
742
tv.set_editable(False)
743
tv.set_cursor_visible(False)
744
tv.modify_font(pango.FontDescription(MONOSPACE_FONT))
745
scroll = gtk.ScrolledWindow()
747
page.pack_start(scroll)
748
self.info = tv.get_buffer()
749
self.add_tab(page, 'Wall')
751
def do_clear(self, w, data):
752
self.payto_sig.set_visible(False)
753
self.payto_fee_entry.set_text('')
754
for entry in [self.payto_entry,self.amount_entry,self.message_entry]:
755
self.set_frozen(entry,False)
758
def question(self,msg):
759
dialog = gtk.MessageDialog( self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, msg)
761
result = dialog.run()
763
return result == gtk.RESPONSE_OK
765
def do_send(self, w, data):
766
payto_entry, label_entry, amount_entry, fee_entry = data
767
label = label_entry.get_text()
768
r = payto_entry.get_text()
771
m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
772
m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
775
to_address = self.wallet.get_alias(r, True, self.show_message, self.question)
779
self.update_sending_tab()
782
to_address = m2.group(5)
786
if not is_valid(to_address):
787
self.show_message( "invalid bitcoin address:\n"+to_address)
791
amount = int( Decimal(amount_entry.get_text()) * 100000000 )
793
self.show_message( "invalid amount")
796
fee = int( Decimal(fee_entry.get_text()) * 100000000 )
798
self.show_message( "invalid fee")
801
if self.wallet.use_encryption:
802
password = password_dialog(self.window)
809
tx = self.wallet.mktx( [(to_address, amount)], password, fee )
810
except Exception as e:
811
self.show_message(str(e))
814
if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE:
815
self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." )
820
self.wallet.labels[tx.hash()] = label
822
status, msg = self.wallet.sendtx( tx )
824
self.show_message( "payment sent.\n" + msg )
825
payto_entry.set_text("")
826
label_entry.set_text("")
827
amount_entry.set_text("")
828
fee_entry.set_text("")
830
self.update_sending_tab()
832
self.show_message( msg )
835
def treeview_button_press(self, treeview, event):
836
if event.type == gtk.gdk._2BUTTON_PRESS:
837
c = treeview.get_cursor()[0]
838
if treeview == self.history_treeview:
839
tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
840
self.show_message(tx_details)
841
elif treeview == self.contacts_treeview:
842
m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
843
#a = self.wallet.aliases.get(m)
845
# if a[0] in self.wallet.authorities.keys():
846
# s = self.wallet.authorities.get(a[0])
849
# msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
850
# self.show_message(msg)
853
def treeview_key_press(self, treeview, event):
854
c = treeview.get_cursor()[0]
855
if event.keyval == gtk.keysyms.Up:
857
treeview.parent.grab_focus()
858
treeview.set_cursor((0,))
859
elif event.keyval == gtk.keysyms.Return:
860
if treeview == self.history_treeview:
861
tx_details = self.history_list.get_value( self.history_list.get_iter(c), 8)
862
self.show_message(tx_details)
863
elif treeview == self.contacts_treeview:
864
m = self.addressbook_list.get_value( self.addressbook_list.get_iter(c), 0)
865
#a = self.wallet.aliases.get(m)
867
# if a[0] in self.wallet.authorities.keys():
868
# s = self.wallet.authorities.get(a[0])
871
# msg = 'Alias:'+ m + '\n\nTarget: '+ a[1] + '\nSigned by: ' + s + '\nSigning address:' + a[0]
872
# self.show_message(msg)
876
def create_history_tab(self):
878
self.history_list = gtk.ListStore(str, str, str, str, 'gboolean', str, str, str, str)
879
treeview = gtk.TreeView(model=self.history_list)
880
self.history_treeview = treeview
881
treeview.set_tooltip_column(7)
883
treeview.connect('key-press-event', self.treeview_key_press)
884
treeview.connect('button-press-event', self.treeview_button_press)
886
tvcolumn = gtk.TreeViewColumn('')
887
treeview.append_column(tvcolumn)
888
cell = gtk.CellRendererPixbuf()
889
tvcolumn.pack_start(cell, False)
890
tvcolumn.set_attributes(cell, stock_id=1)
892
tvcolumn = gtk.TreeViewColumn('Date')
893
treeview.append_column(tvcolumn)
894
cell = gtk.CellRendererText()
895
tvcolumn.pack_start(cell, False)
896
tvcolumn.add_attribute(cell, 'text', 2)
898
tvcolumn = gtk.TreeViewColumn('Description')
899
treeview.append_column(tvcolumn)
900
cell = gtk.CellRendererText()
901
cell.set_property('foreground', 'grey')
902
cell.set_property('family', MONOSPACE_FONT)
903
cell.set_property('editable', True)
904
def edited_cb(cell, path, new_text, h_list):
905
tx = h_list.get_value( h_list.get_iter(path), 0)
906
self.wallet.set_label(tx,new_text)
907
self.update_history_tab()
908
cell.connect('edited', edited_cb, self.history_list)
909
def editing_started(cell, entry, path, h_list):
910
tx = h_list.get_value( h_list.get_iter(path), 0)
911
if not self.wallet.labels.get(tx): entry.set_text('')
912
cell.connect('editing-started', editing_started, self.history_list)
913
tvcolumn.set_expand(True)
914
tvcolumn.pack_start(cell, True)
915
tvcolumn.set_attributes(cell, text=3, foreground_set = 4)
917
tvcolumn = gtk.TreeViewColumn('Amount')
918
treeview.append_column(tvcolumn)
919
cell = gtk.CellRendererText()
920
cell.set_alignment(1, 0.5)
921
cell.set_property('family', MONOSPACE_FONT)
922
tvcolumn.pack_start(cell, False)
923
tvcolumn.add_attribute(cell, 'text', 5)
925
tvcolumn = gtk.TreeViewColumn('Balance')
926
treeview.append_column(tvcolumn)
927
cell = gtk.CellRendererText()
928
cell.set_alignment(1, 0.5)
929
cell.set_property('family', MONOSPACE_FONT)
930
tvcolumn.pack_start(cell, False)
931
tvcolumn.add_attribute(cell, 'text', 6)
933
tvcolumn = gtk.TreeViewColumn('Tooltip')
934
treeview.append_column(tvcolumn)
935
cell = gtk.CellRendererText()
936
tvcolumn.pack_start(cell, False)
937
tvcolumn.add_attribute(cell, 'text', 7)
938
tvcolumn.set_visible(False)
940
scroll = gtk.ScrolledWindow()
941
scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
944
self.add_tab(scroll, 'History')
945
self.update_history_tab()
948
def create_recv_tab(self):
949
self.recv_list = gtk.ListStore(str, str, str, str, str)
950
self.add_tab( self.make_address_list(True), 'Receive')
951
self.update_receiving_tab()
953
def create_book_tab(self):
954
self.addressbook_list = gtk.ListStore(str, str, str)
955
self.add_tab( self.make_address_list(False), 'Contacts')
956
self.update_sending_tab()
958
def make_address_list(self, is_recv):
959
liststore = self.recv_list if is_recv else self.addressbook_list
960
treeview = gtk.TreeView(model= liststore)
961
treeview.connect('key-press-event', self.treeview_key_press)
962
treeview.connect('button-press-event', self.treeview_button_press)
965
self.contacts_treeview = treeview
967
tvcolumn = gtk.TreeViewColumn('Address')
968
treeview.append_column(tvcolumn)
969
cell = gtk.CellRendererText()
970
cell.set_property('family', MONOSPACE_FONT)
971
tvcolumn.pack_start(cell, True)
972
tvcolumn.add_attribute(cell, 'text', 0)
974
tvcolumn = gtk.TreeViewColumn('Label')
975
tvcolumn.set_expand(True)
976
treeview.append_column(tvcolumn)
977
cell = gtk.CellRendererText()
978
cell.set_property('editable', True)
979
def edited_cb2(cell, path, new_text, liststore):
980
address = liststore.get_value( liststore.get_iter(path), 0)
981
self.wallet.set_label(address, new_text)
982
self.update_receiving_tab()
983
self.update_sending_tab()
984
self.update_history_tab()
985
cell.connect('edited', edited_cb2, liststore)
986
tvcolumn.pack_start(cell, True)
987
tvcolumn.add_attribute(cell, 'text', 1)
989
tvcolumn = gtk.TreeViewColumn('Tx')
990
treeview.append_column(tvcolumn)
991
cell = gtk.CellRendererText()
992
tvcolumn.pack_start(cell, True)
993
tvcolumn.add_attribute(cell, 'text', 2)
996
tvcolumn = gtk.TreeViewColumn('Balance')
997
treeview.append_column(tvcolumn)
998
cell = gtk.CellRendererText()
999
tvcolumn.pack_start(cell, True)
1000
tvcolumn.add_attribute(cell, 'text', 3)
1001
tvcolumn = gtk.TreeViewColumn('Type')
1002
treeview.append_column(tvcolumn)
1003
cell = gtk.CellRendererText()
1004
tvcolumn.pack_start(cell, True)
1005
tvcolumn.add_attribute(cell, 'text', 4)
1007
scroll = gtk.ScrolledWindow()
1008
scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1009
scroll.add(treeview)
1013
button = gtk.Button("New")
1014
button.connect("clicked", self.newaddress_dialog)
1016
hbox.pack_start(button,False)
1018
def showqrcode(w, treeview, liststore):
1019
path, col = treeview.get_cursor()
1021
address = liststore.get_value(liststore.get_iter(path), 0)
1022
qr = pyqrnative.QRCode(4, pyqrnative.QRErrorCorrectLevel.H)
1026
size = qr.getModuleCount()*boxsize
1027
def area_expose_cb(area, event):
1028
style = area.get_style()
1029
k = qr.getModuleCount()
1032
gc = style.black_gc if qr.isDark(r, c) else style.white_gc
1033
area.window.draw_rectangle(gc, True, c*boxsize, r*boxsize, boxsize, boxsize)
1034
area = gtk.DrawingArea()
1035
area.set_size_request(size, size)
1036
area.connect("expose-event", area_expose_cb)
1038
dialog = gtk.Dialog(address, parent=self.window, flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR, buttons = ("ok",1))
1039
dialog.vbox.add(area)
1043
button = gtk.Button("QR")
1044
button.connect("clicked", showqrcode, treeview, liststore)
1046
hbox.pack_start(button,False)
1048
button = gtk.Button("Copy to clipboard")
1049
def copy2clipboard(w, treeview, liststore):
1051
path, col = treeview.get_cursor()
1053
address = liststore.get_value( liststore.get_iter(path), 0)
1054
if platform.system() == 'Windows':
1055
from Tkinter import Tk
1059
r.clipboard_append( address )
1062
c = gtk.clipboard_get()
1063
c.set_text( address )
1064
button.connect("clicked", copy2clipboard, treeview, liststore)
1066
hbox.pack_start(button,False)
1069
button = gtk.Button("Freeze")
1070
def freeze_address(w, treeview, liststore, wallet):
1071
path, col = treeview.get_cursor()
1073
address = liststore.get_value( liststore.get_iter(path), 0)
1074
if address in wallet.frozen_addresses:
1075
wallet.unfreeze(address)
1077
wallet.freeze(address)
1078
self.update_receiving_tab()
1079
button.connect("clicked", freeze_address, treeview, liststore, self.wallet)
1081
hbox.pack_start(button,False)
1084
button = gtk.Button("Pay to")
1085
def payto(w, treeview, liststore):
1086
path, col = treeview.get_cursor()
1088
address = liststore.get_value( liststore.get_iter(path), 0)
1089
self.payto_entry.set_text( address )
1090
self.notebook.set_current_page(1)
1091
self.amount_entry.grab_focus()
1093
button.connect("clicked", payto, treeview, liststore)
1095
hbox.pack_start(button,False)
1098
vbox.pack_start(scroll,True)
1099
vbox.pack_start(hbox, False)
1102
def update_status_bar(self):
1103
interface = self.network.interface
1104
if self.funds_error:
1105
text = "Not enough funds"
1106
elif interface and interface.is_connected:
1107
self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1108
if not self.wallet.up_to_date:
1109
self.status_image.set_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU)
1110
text = "Synchronizing..."
1112
self.status_image.set_from_stock(gtk.STOCK_YES, gtk.ICON_SIZE_MENU)
1113
self.network_button.set_tooltip_text("Connected to %s:%d.\n%d blocks"%(interface.host, interface.port, self.network.blockchain.height()))
1114
c, u = self.wallet.get_balance()
1115
text = "Balance: %s "%( format_satoshis(c,False,self.num_zeros) )
1116
if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.num_zeros).strip() )
1118
self.status_image.set_from_stock(gtk.STOCK_NO, gtk.ICON_SIZE_MENU)
1119
self.network_button.set_tooltip_text("Not connected.")
1120
text = "Not connected"
1122
self.status_bar.pop(self.context_id)
1123
self.status_bar.push(self.context_id, text)
1125
if self.wallet.up_to_date and self.wallet_updated:
1126
self.update_history_tab()
1127
self.update_receiving_tab()
1128
# addressbook too...
1129
self.info.set_text( self.network.banner )
1130
self.wallet_updated = False
1132
def update_receiving_tab(self):
1133
self.recv_list.clear()
1134
for address in self.wallet.addresses(True):
1137
if self.wallet.is_change(address): Type = "C"
1138
if address in self.wallet.imported_keys.keys():
1140
c, u = self.wallet.get_addr_balance(address)
1141
if address in self.wallet.frozen_addresses: Type = Type + "F"
1142
label = self.wallet.labels.get(address)
1143
h = self.wallet.history.get(address,[])
1145
tx = "0" if n==0 else "%d"%n
1146
self.recv_list.append((address, label, tx, format_satoshis(c,False,self.num_zeros), Type ))
1148
def update_sending_tab(self):
1149
# detect addresses that are not mine in history, add them here...
1150
self.addressbook_list.clear()
1151
#for alias, v in self.wallet.aliases.items():
1153
# label = self.wallet.labels.get(alias)
1154
# self.addressbook_list.append((alias, label, '-'))
1156
for address in self.wallet.addressbook:
1157
label = self.wallet.labels.get(address)
1158
n = self.wallet.get_num_tx(address)
1159
self.addressbook_list.append((address, label, "%d"%n))
1161
def update_history_tab(self):
1162
cursor = self.history_treeview.get_cursor()[0]
1163
self.history_list.clear()
1165
for item in self.wallet.get_tx_history():
1166
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
1169
time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
1172
conf_icon = gtk.STOCK_APPLY
1174
time_str = 'unverified'
1177
time_str = 'pending'
1178
conf_icon = gtk.STOCK_EXECUTE
1180
label, is_default_label = self.wallet.get_label(tx_hash)
1181
tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
1182
details = self.get_tx_details(tx_hash)
1184
self.history_list.prepend( [tx_hash, conf_icon, time_str, label, is_default_label,
1185
format_satoshis(value,True,self.num_zeros, whitespaces=True),
1186
format_satoshis(balance,False,self.num_zeros, whitespaces=True), tooltip, details] )
1187
if cursor: self.history_treeview.set_cursor( cursor )
1190
def get_tx_details(self, tx_hash):
1192
if not tx_hash: return ''
1193
tx = self.wallet.transactions.get(tx_hash)
1194
is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
1195
conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
1198
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
1200
time_str = 'pending'
1202
inputs = map(lambda x: x.get('address'), tx.inputs)
1203
outputs = map(lambda x: x.get('address'), tx.d['outputs'])
1204
tx_details = "Transaction Details" +"\n\n" \
1205
+ "Transaction ID:\n" + tx_hash + "\n\n" \
1206
+ "Status: %d confirmations\n"%conf
1209
tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
1210
+ "Transaction fee: %s\n"% format_satoshis(fee, False)
1212
tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
1213
+ "Transaction fee: unknown\n"
1215
tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
1217
tx_details += "Date: %s\n\n"%time_str \
1218
+ "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
1219
+ "Outputs:\n-"+ '\n-'.join(outputs)
1225
def newaddress_dialog(self, w):
1227
title = "New Contact"
1228
dialog = gtk.Dialog(title, parent=self.window,
1229
flags=gtk.DIALOG_MODAL|gtk.DIALOG_NO_SEPARATOR,
1230
buttons= ("cancel", 0, "ok",1) )
1234
label_label = gtk.Label('Label:')
1235
label_label.set_size_request(120,10)
1237
label.pack_start(label_label)
1238
label_entry = gtk.Entry()
1240
label.pack_start(label_entry)
1242
dialog.vbox.pack_start(label, False, True, 5)
1244
address = gtk.HBox()
1245
address_label = gtk.Label('Address:')
1246
address_label.set_size_request(120,10)
1247
address_label.show()
1248
address.pack_start(address_label)
1249
address_entry = gtk.Entry()
1250
address_entry.show()
1251
address.pack_start(address_entry)
1253
dialog.vbox.pack_start(address, False, True, 5)
1255
result = dialog.run()
1256
address = address_entry.get_text()
1257
label = label_entry.get_text()
1261
if is_valid(address):
1262
self.wallet.add_contact(address,label)
1263
self.update_sending_tab()
1265
errorDialog = gtk.MessageDialog(
1267
flags=gtk.DIALOG_MODAL,
1268
buttons= gtk.BUTTONS_CLOSE,
1269
message_format = "Invalid address")
1272
errorDialog.destroy()
1276
class ElectrumGui():
1278
def __init__(self, config, network):
1279
self.network = network
1280
self.config = config
1283
def main(self, url=None):
1285
storage = WalletStorage(self.config)
1286
if not storage.file_exists:
1287
action = self.restore_or_create()
1290
self.wallet = wallet = Wallet(storage)
1291
gap = self.config.get('gap_limit', 5)
1293
wallet.gap_limit = gap
1294
wallet.storage.put('gap_limit', gap, True)
1296
self.wallet.start_threads(self.network)
1298
if action == 'create':
1299
wallet.init_seed(None)
1301
wallet.synchronize() # generate first addresses offline
1302
elif action == 'restore':
1303
seed = self.seed_dialog()
1304
wallet.init_seed(seed)
1306
self.restore_wallet(wallet)
1311
self.wallet = Wallet(storage)
1312
self.wallet.start_threads(self.network)
1314
w = ElectrumWindow(self.wallet, self.config, self.network)
1315
if url: w.set_url(url)
1318
def restore_or_create(self):
1319
return restore_create_dialog()
1321
def seed_dialog(self):
1322
return run_recovery_dialog()
1324
def verify_seed(self):
1325
self.wallet.save_seed()
1328
def network_dialog(self):
1329
return run_network_dialog( self.network, parent=None )
1331
def show_seed(self):
1332
show_seed_dialog(self.wallet, None, None)
1334
def password_dialog(self):
1335
change_password_dialog(self.wallet, None, None)
1337
def restore_wallet(self, wallet):
1339
dialog = gtk.MessageDialog(
1341
flags = gtk.DIALOG_MODAL,
1342
buttons = gtk.BUTTONS_CANCEL,
1343
message_format = "Please wait..." )
1346
def recover_thread( wallet, dialog ):
1347
wallet.restore(lambda x:x)
1348
gobject.idle_add( dialog.destroy )
1350
thread.start_new_thread( recover_thread, ( wallet, dialog ) )
1353
if r==gtk.RESPONSE_CANCEL: return False
1354
if not wallet.is_found():
1355
show_message("No transactions found for this seed")