This commit is contained in:
Anwesh 2015-12-24 06:59:16 +05:30
commit 93bcd98763
70 changed files with 2153 additions and 3432 deletions

View File

@ -2,6 +2,9 @@
* separation between plugins and GUIs * separation between plugins and GUIs
* the daemon supports jsonrpc commands * the daemon supports jsonrpc commands
* new command: 'notify <address> <url>' * new command: 'notify <address> <url>'
* improved coin selection to help preserve user privacy. This is an
experimental feature. Enable it by setting the Coin Selection
preference to Privacy.
# Release 2.5.4 # Release 2.5.4
* increase MIN_RELAY_TX_FEE to avoid dust transactions * increase MIN_RELAY_TX_FEE to avoid dust transactions

View File

@ -1,5 +1,4 @@
#!/usr/bin/python #!/usr/bin/python2
if __name__ == '__main__': if __name__ == '__main__':
import sys, re, shutil, os, hashlib import sys, re, shutil, os, hashlib

View File

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python2
import sys import sys
import re import re
import hashlib import hashlib

View File

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python2
import sys, re, shutil, os, hashlib import sys, re, shutil, os, hashlib
import imp import imp

View File

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python2
import sys, re, shutil, os, hashlib import sys, re, shutil, os, hashlib
import imp import imp

239
electrum
View File

@ -107,9 +107,74 @@ def init_gui(config, network, plugins):
return gui return gui
def run_non_RPC(config):
cmdname = config.get('cmd')
def init_cmdline(config): storage = WalletStorage(config.get_wallet_path())
if storage.file_exists:
sys.exit("Error: Remove the existing wallet first!")
def password_dialog():
return prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
if cmdname == 'restore':
text = config.get('text')
password = password_dialog() if Wallet.is_seed(text) or Wallet.is_xprv(text) or Wallet.is_private_key(text) else None
try:
wallet = Wallet.from_text(text, password, storage)
except BaseException as e:
sys.exit(str(e))
if not config.get('offline'):
network = Network(config)
network.start()
wallet.start_threads(network)
print_msg("Recovering wallet...")
wallet.synchronize()
wallet.wait_until_synchronized()
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
else:
msg = "This wallet was restored offline. It may contain more addresses than displayed."
print_msg(msg)
elif cmdname == 'create':
password = password_dialog()
wallet = Wallet(storage)
seed = wallet.make_seed()
wallet.add_seed(seed, password)
wallet.create_master_keys(password)
wallet.create_main_account(password)
wallet.synchronize()
print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
elif cmdname == 'deseed':
if not wallet.seed:
print_msg("Error: This wallet has no seed")
else:
ns = wallet.storage.path + '.seedless'
print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
wallet.storage.path = ns
wallet.seed = ''
wallet.storage.put('seed', '')
wallet.use_encryption = False
wallet.storage.put('use_encryption', wallet.use_encryption)
for k in wallet.imported_keys.keys():
wallet.imported_keys[k] = ''
wallet.storage.put('imported_keys', wallet.imported_keys)
print_msg("Done.")
else:
print_msg("Action canceled.")
wallet.storage.write()
sys.exit(0)
wallet.storage.write()
print_msg("Wallet saved in '%s'" % wallet.storage.path)
sys.exit(0)
def init_cmdline(config_options):
config = SimpleConfig(config_options)
cmdname = config.get('cmd') cmdname = config.get('cmd')
cmd = known_commands[cmdname] cmd = known_commands[cmdname]
@ -130,57 +195,11 @@ def init_cmdline(config):
# instanciate wallet for command-line # instanciate wallet for command-line
storage = WalletStorage(config.get_wallet_path()) storage = WalletStorage(config.get_wallet_path())
if cmd.name in ['create', 'restore']:
if storage.file_exists:
sys.exit("Error: Remove the existing wallet first!")
def password_dialog():
return prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
if cmd.name == 'restore':
text = config.get('text')
password = password_dialog() if Wallet.is_seed(text) or Wallet.is_xprv(text) or Wallet.is_private_key(text) else None
try:
wallet = Wallet.from_text(text, password, storage)
except BaseException as e:
sys.exit(str(e))
if not config.get('offline'):
network = Network(config)
network.start()
wallet.start_threads(network)
print_msg("Recovering wallet...")
wallet.synchronize()
wallet.wait_until_synchronized()
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
else:
msg = "This wallet was restored offline. It may contain more addresses than displayed."
print_msg(msg)
else:
password = password_dialog()
wallet = Wallet(storage)
seed = wallet.make_seed()
wallet.add_seed(seed, password)
wallet.create_master_keys(password)
wallet.create_main_account(password)
wallet.synchronize()
print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
print_msg("Wallet saved in '%s'" % wallet.storage.path)
sys.exit(0)
if cmd.requires_wallet and not storage.file_exists: if cmd.requires_wallet and not storage.file_exists:
print_msg("Error: Wallet file not found.") print_msg("Error: Wallet file not found.")
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option") print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
sys.exit(0) sys.exit(0)
# create wallet instance
wallet = Wallet(storage) if cmd.requires_wallet else None
# notify plugins
always_hook('cmdline_load_wallet', wallet)
# important warning # important warning
if cmd.name in ['getprivatekeys']: if cmd.name in ['getprivatekeys']:
print_stderr("WARNING: ALL your private keys are secret.") print_stderr("WARNING: ALL your private keys are secret.")
@ -188,7 +207,7 @@ def init_cmdline(config):
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.") print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
# commands needing password # commands needing password
if cmd.requires_password and wallet.use_encryption: if cmd.requires_password and storage.get('use_encryption'):
if config.get('password'): if config.get('password'):
password = config.get('password') password = config.get('password')
else: else:
@ -196,55 +215,49 @@ def init_cmdline(config):
if not password: if not password:
print_msg("Error: Password required") print_msg("Error: Password required")
sys.exit(1) sys.exit(1)
else:
password = None
config_options['password'] = password
if cmd.name == 'password':
new_password = prompt_password('New password:')
config_options['new_password'] = new_password
return cmd, password
def run_offline_command(config, config_options):
cmdname = config.get('cmd')
cmd = known_commands[cmdname]
storage = WalletStorage(config.get_wallet_path())
wallet = Wallet(storage) if cmd.requires_wallet else None
# check password # check password
if cmd.requires_password and storage.get('use_encryption'):
password = config_options.get('password')
try: try:
seed = wallet.check_password(password) seed = wallet.check_password(password)
except InvalidPassword: except InvalidPassword:
print_msg("Error: This password does not decode this wallet.") print_msg("Error: This password does not decode this wallet.")
sys.exit(1) sys.exit(1)
else: if cmd.requires_network:
password = None print_stderr("Warning: running command offline")
# notify plugins
# run the command always_hook('cmdline_load_wallet', wallet)
if cmd.name == 'deseed':
if not wallet.seed:
print_msg("Error: This wallet has no seed")
else:
ns = wallet.storage.path + '.seedless'
print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
wallet.storage.path = ns
wallet.seed = ''
wallet.storage.put('seed', '', True)
wallet.use_encryption = False
wallet.storage.put('use_encryption', wallet.use_encryption, True)
for k in wallet.imported_keys.keys():
wallet.imported_keys[k] = ''
wallet.storage.put('imported_keys', wallet.imported_keys, True)
print_msg("Done.")
else:
print_msg("Action canceled.")
sys.exit(0)
elif cmd.name == 'password':
new_password = prompt_password('New password:')
wallet.update_password(password, new_password)
sys.exit(0)
return cmd, password, wallet
def run_offline_command(config, cmd, wallet, password):
# arguments passed to function # arguments passed to function
args = map(lambda x: config.get(x), cmd.params) args = map(lambda x: config.get(x), cmd.params)
# decode json arguments # decode json arguments
args = map(json_decode, args) args = map(json_decode, args)
# options # options
args += map(lambda x: config.get(x), cmd.options) args += map(lambda x: config.get(x), cmd.options)
cmd_runner = Commands(config, wallet, None) cmd_runner = Commands(config, wallet, None,
cmd_runner.password = password password=config_options.get('password'),
new_password=config_options.get('new_password'))
func = getattr(cmd_runner, cmd.name) func = getattr(cmd_runner, cmd.name)
result = func(*args) result = func(*args)
# save wallet
if wallet:
wallet.storage.write()
return result return result
@ -308,43 +321,24 @@ if __name__ == '__main__':
config_options['url'] = uri config_options['url'] = uri
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
cmd_name = config.get('cmd') cmdname = config.get('cmd')
# initialize plugins. # initialize plugins.
gui_name = config.get('gui', 'qt') if cmd_name == 'gui' else 'cmdline' gui_name = config.get('gui', 'qt') if cmdname == 'gui' else 'cmdline'
plugins = Plugins(config, is_bundle or is_local or is_android, gui_name) plugins = Plugins(config, is_bundle or is_local or is_android, gui_name)
# run command offline # run non-RPC commands separately
if cmd_name not in ['gui', 'daemon']: if cmdname in ['create', 'restore', 'deseed']:
cmd, password, wallet = init_cmdline(config) run_non_RPC(config)
if not cmd.requires_network or config.get('offline'):
result = run_offline_command(config, cmd, wallet, password)
print_msg(json_encode(result))
sys.exit(0) sys.exit(0)
else:
config_options['password'] = password
# check if a daemon is running
server = get_daemon(config) server = get_daemon(config)
# daemon is running if cmdname == 'gui':
if server is not None: if server is not None:
cmdname = config_options.get('cmd')
if cmdname == 'daemon':
result = server.daemon(config_options)
elif cmdname == 'gui':
result = server.gui(config_options) result = server.gui(config_options)
else: else:
result = server.run_cmdline(config_options)
if type(result) in [str, unicode]:
print_msg(result)
elif type(result) is dict and result.get('error'):
print_stderr(result.get('error'))
elif result is not None:
print_msg(json_encode(result))
sys.exit(0)
# daemon is not running
if cmd_name == 'gui':
if not config.get('offline'): if not config.get('offline'):
network = Network(config) network = Network(config)
network.start() network.start()
@ -357,7 +351,10 @@ if __name__ == '__main__':
gui.main() gui.main()
sys.exit(0) sys.exit(0)
elif cmd_name == 'daemon': elif cmdname == 'daemon':
if server is not None:
result = server.daemon(config_options)
else:
subcommand = config.get('subcommand') subcommand = config.get('subcommand')
if subcommand in ['status', 'stop']: if subcommand in ['status', 'stop']:
print_msg("Daemon not running") print_msg("Daemon not running")
@ -376,12 +373,32 @@ if __name__ == '__main__':
util.check_www_dir(config.get('requests_dir')) util.check_www_dir(config.get('requests_dir'))
daemon.start() daemon.start()
daemon.join() daemon.join()
sys.exit(0)
else: else:
print_stderr("starting daemon (PID %d)"%p) print_stderr("starting daemon (PID %d)"%p)
sys.exit(0) sys.exit(0)
else: else:
print_msg("syntax: electrum daemon <start|status|stop>") print_msg("syntax: electrum daemon <start|status|stop>")
sys.exit(1) sys.exit(1)
else: else:
print_msg("Network daemon is not running. Try 'electrum daemon start'\nIf you want to run this command offline, use the -o flag.") # command line
init_cmdline(config_options)
if server is not None:
result = server.run_cmdline(config_options)
else:
cmd = known_commands[cmdname]
if cmd.requires_network:
print_msg("Network daemon is not running. Try 'electrum daemon start'")
sys.exit(1) sys.exit(1)
else:
result = run_offline_command(config, config_options)
# print result
if type(result) in [str, unicode]:
print_msg(result)
elif type(result) is dict and result.get('error'):
print_stderr(result.get('error'))
elif result is not None:
print_msg(json_encode(result))
sys.exit(0)

View File

@ -332,7 +332,7 @@ def get_history_values(n):
except Exception: except Exception:
time_str = 'pending' time_str = 'pending'
conf_str = 'v' if conf else 'o' conf_str = 'v' if conf else 'o'
label, is_default_label = wallet.get_label(tx_hash) label = wallet.get_label(tx_hash)
label = label.replace('<','').replace('>','') label = label.replace('<','').replace('>','')
values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value, True), ' ' + label)) values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value, True), ' ' + label))

View File

@ -1185,7 +1185,7 @@ class ElectrumWindow:
time_str = 'pending' time_str = 'pending'
conf_icon = Gtk.STOCK_EXECUTE conf_icon = Gtk.STOCK_EXECUTE
label, is_default_label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else '' tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
details = self.get_tx_details(tx_hash) details = self.get_tx_details(tx_hash)
@ -1300,7 +1300,7 @@ class ElectrumGui():
gap = self.config.get('gap_limit', 5) gap = self.config.get('gap_limit', 5)
if gap != 5: if gap != 5:
wallet.gap_limit = gap wallet.gap_limit = gap
wallet.storage.put('gap_limit', gap, True) wallet.storage.put('gap_limit', gap)
if action == 'create': if action == 'create':
seed = wallet.make_seed() seed = wallet.make_seed()

View File

@ -29,6 +29,7 @@ except ImportError:
# minimum required version for kivy # minimum required version for kivy
kivy.require('1.8.0') kivy.require('1.8.0')
from electrum.i18n import set_language
from kivy.logger import Logger from kivy.logger import Logger
from main_window import ElectrumWindow from main_window import ElectrumWindow
@ -39,6 +40,7 @@ class ElectrumGui:
self.network = network self.network = network
self.config = config self.config = config
self.plugins = plugins self.plugins = plugins
set_language(config.get('language'))
def main(self): def main(self):
w = ElectrumWindow(config=self.config, w = ElectrumWindow(config=self.config,

View File

@ -1,8 +1,11 @@
#:import Clock kivy.clock.Clock
#:import Window kivy.core.window.Window #:import Window kivy.core.window.Window
#:import Factory kivy.factory.Factory #:import Factory kivy.factory.Factory
#:import _ electrum.i18n._ #:import _ electrum.i18n._
# Custom Global Widgets # Custom Global Widgets
<Button>
on_parent: self.MIN_STATE_TIME = 0.1
<VGridLayout@GridLayout>: <VGridLayout@GridLayout>:
rows: 1 rows: 1
@ -187,22 +190,44 @@
size: self.size size: self.size
pos: self.pos pos: self.pos
<CardItem@ToggleButtonBehavior+BoxLayout>
size_hint: 1, None
height: '65dp'
group: 'requests'
padding: dp(12)
spacing: dp(5)
screen: None
on_release:
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
canvas.before:
Color:
rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1)
Rectangle:
size: self.size
pos: self.pos
<AddressSelector@BlueSpinner> <AddressSelector@BlueSpinner>
icon: 'atlas://gui/kivy/theming/light/globe' icon: 'atlas://gui/kivy/theming/light/globe'
values: [] #app.wallet.addresses() if app.wallet else [] values: [] #app.wallet.addresses() if app.wallet else []
text: _("Select Your address") text: _("Select Your address")
<AmountButton@Button>: <BlueButton@Button>:
background_color: .238, .585, .878, 0 background_color: .238, .585, .878, 0
halign: 'left' halign: 'left'
text_size: (self.width-10, None) text_size: (self.width-10, None)
size_hint: 0.5, None size_hint: 0.5, None
default_text: 'Amount' default_text: ''
text: self.default_text text: self.default_text
padding: '5dp', '5db' padding: '5dp', '5db'
height: '40dp' height: '40dp'
text_color: self.foreground_color text_color: self.foreground_color
foreground_color: 1, 0, 0, 1 foreground_color: 1, 0, 0, 1
canvas.before:
Color:
rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color
Rectangle:
size: self.size
pos: self.pos
<TextInputBlue@TextInput> <TextInputBlue@TextInput>
@ -222,24 +247,26 @@
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
on_release: on_release:
self.parent.update_text(self.parent, self.text) self.parent.update_amount(self.text)
<TabbedPanelStrip>: <TabbedPanelStrip>:
on_parent: on_parent:
if self.parent: self.parent.bar_width = 0 if self.parent: self.parent.bar_width = 0
if self.parent: self.parent.scroll_x = 0.5
<TabbedCarousel> <TabbedCarousel>
carousel: carousel carousel: carousel
do_default_tab: False do_default_tab: False
Carousel: Carousel:
scroll_timeout: 190 scroll_timeout: 250
scroll_distance: '20dp'
anim_type: 'out_quart' anim_type: 'out_quart'
min_move: .05 min_move: .05
anim_move_duration: .1 anim_move_duration: .1
anim_cancel_duration: .54 anim_cancel_duration: .54
scroll_distance: '10dp'
on_index: root.on_index(*args) on_index: root.on_index(*args)
id: carousel id: carousel
@ -281,65 +308,59 @@
TabbedCarousel: TabbedCarousel:
id: panel id: panel
tab_height: '48dp' tab_height: '48dp'
#default_tab: send_tab tab_width: panel.width/3
default_tab: history_tab
strip_border: 0, 0, 0, 0 strip_border: 0, 0, 0, 0
HistoryScreen: InvoicesScreen:
id: history_screen id: invoices_screen
tab: history_tab tab: invoices_tab
SendScreen: SendScreen:
id: send_screen id: send_screen
tab: send_tab tab: send_tab
HistoryScreen:
id: history_screen
tab: history_tab
ReceiveScreen: ReceiveScreen:
id: receive_screen id: receive_screen
tab: receive_tab tab: receive_tab
ContactsScreen: RequestsScreen:
id: contacts_screen id: requests_screen
tab: contacts_tab tab: requests_tab
#ContactsScreen:
# id: contacts_screen
# tab: contacts_tab
CleanHeader: CleanHeader:
id: history_tab id: invoices_tab
text: _('History') text: _('Invoices')
slide: 0 slide: 0
CleanHeader: CleanHeader:
id: send_tab id: send_tab
text: _('Send') text: _('Send')
slide: 1 slide: 1
CleanHeader: CleanHeader:
id: receive_tab id: history_tab
text: _('Receive') text: _('History')
slide: 2 slide: 2
CleanHeader: CleanHeader:
id: contacts_tab id: receive_tab
text: _('Contacts') text: _('Receive')
slide: 3 slide: 3
CleanHeader:
id: requests_tab
text: _('Requests')
slide: 4
#CleanHeader:
# id: contacts_tab
# text: _('Contacts')
# slide: 4
<ActionOvrButton@ActionButton> <ActionOvrButton@ActionButton>
on_release: on_release:
if self.parent: self.parent.parent.dismiss() Clock.schedule_once(lambda dt: self.parent.parent.dismiss() if self.parent else None, 0.05)
app.popup_dialog(self.name) Clock.schedule_once(lambda dt: app.popup_dialog(self.name), 0.05)
<SettingsItem@ButtonBehavior+BoxLayout>
orientation: 'vertical'
title: ''
description: ''
size_hint: 1, 1
Label:
id: title
text: self.parent.title
size_hint: 1, 1
bold: True
text_size: self.size
halign: 'left'
Label:
text: self.parent.description
size_hint: 1, 1
text_size: self.width, None
color: 0.8, 0.8, 0.8, 1
halign: 'left'
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
canvas.before: canvas.before:
@ -373,34 +394,26 @@ BoxLayout:
.format(app.status) .format(app.status)
font_size: '22dp' font_size: '22dp'
minimum_width: '1dp' minimum_width: '1dp'
on_release: app.popup_dialog('status')
ActionButton:
id: context_button
text: app.context
width: 0
on_text:
self.width = 20 if self.text else 0
on_release: app.context_action()
ActionOverflow: ActionOverflow:
id: ao id: ao
ActionOvrButton: ActionOvrButton:
text: _('Network') name: 'about'
text: _('About')
ActionOvrButton:
name: 'network' name: 'network'
text: _('Network')
on_parent: on_parent:
# when widget overflow drop down is shown, adjust the width # when widget overflow drop down is shown, adjust the width
parent = args[1] parent = args[1]
if parent: ao._dropdown.width = sp(200) if parent: ao._dropdown.width = sp(200)
ActionOvrButton:
name: 'settings'
text: _('Settings')
ActionOvrButton: ActionOvrButton:
name: 'wallets' name: 'wallets'
text: _('Wallets') text: _('Wallets')
ActionOvrButton: ActionOvrButton:
name: 'plugins' name: 'settings'
text: _('Plugins') text: _('Settings')
ScreenManager: ScreenManager:
id: manager id: manager
ScreenTabs: ScreenTabs:

View File

@ -7,11 +7,13 @@ from decimal import Decimal
import electrum import electrum
from electrum import WalletStorage, Wallet from electrum import WalletStorage, Wallet
from electrum.i18n import _, set_language from electrum.i18n import _
from electrum.contacts import Contacts from electrum.contacts import Contacts
from electrum.paymentrequest import InvoiceStore
from electrum.util import profiler, InvalidPassword from electrum.util import profiler, InvalidPassword
from electrum.plugins import run_hook from electrum.plugins import run_hook
from electrum.util import format_satoshis, format_satoshis_plain from electrum.util import format_satoshis, format_satoshis_plain
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from kivy.app import App from kivy.app import App
from kivy.core.window import Window from kivy.core.window import Window
@ -31,6 +33,7 @@ Factory.register('InstallWizard',
Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs') Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs')
Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens') Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens')
#from kivy.core.window import Window #from kivy.core.window import Window
#Window.softinput_mode = 'below_target' #Window.softinput_mode = 'below_target'
@ -54,8 +57,8 @@ from kivy.core.clipboard import Clipboard
Factory.register('TabbedCarousel', module='electrum_gui.kivy.uix.screens') Factory.register('TabbedCarousel', module='electrum_gui.kivy.uix.screens')
from electrum.util import base_units
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
class ElectrumWindow(App): class ElectrumWindow(App):
@ -72,13 +75,6 @@ class ElectrumWindow(App):
self.history_screen.update() self.history_screen.update()
base_unit = AliasProperty(_get_bu, _set_bu) base_unit = AliasProperty(_get_bu, _set_bu)
def _rotate_bu(self):
keys = sorted(base_units.keys())
self.base_unit = keys[ (keys.index(self.base_unit) + 1) % len(keys)]
context = StringProperty('')
context_action = lambda x: None
status = StringProperty('') status = StringProperty('')
fiat_unit = StringProperty('') fiat_unit = StringProperty('')
@ -165,6 +161,9 @@ class ElectrumWindow(App):
self.nfcscanner = None self.nfcscanner = None
self.tabs = None self.tabs = None
self.receive_address = None
self.current_invoice = None
super(ElectrumWindow, self).__init__(**kwargs) super(ElectrumWindow, self).__init__(**kwargs)
title = _('Electrum App') title = _('Electrum App')
@ -176,6 +175,7 @@ class ElectrumWindow(App):
#self.config = self.gui_object.config #self.config = self.gui_object.config
self.contacts = Contacts(self.electrum_config) self.contacts = Contacts(self.electrum_config)
self.invoices = InvoiceStore(self.electrum_config)
self.bind(url=self.set_URI) self.bind(url=self.set_URI)
# were we sent a url? # were we sent a url?
@ -191,15 +191,60 @@ class ElectrumWindow(App):
self._trigger_notify_transactions = \ self._trigger_notify_transactions = \
Clock.create_trigger(self.notify_transactions, 5) Clock.create_trigger(self.notify_transactions, 5)
def get_receive_address(self):
return self.receive_address if self.receive_address else self.wallet.get_unused_address(None)
def do_pay(self, obj):
pr = self.invoices.get(obj.key)
self.on_pr(pr)
def on_pr(self, pr):
if pr.verify(self.contacts):
key = self.invoices.add(pr)
self.invoices_screen.update()
status = self.invoices.get_status(key)
if status == PR_PAID:
self.show_error("invoice already paid")
self.send_screen.do_clear()
else:
if pr.has_expired():
self.show_error(_('Payment request has expired'))
else:
self.current_invoice = pr
self.update_tab('send')
self.switch_to('send')
else:
self.show_error("invoice error:" + pr.error)
self.send_screen.do_clear()
def set_URI(self, url): def set_URI(self, url):
try: try:
url = electrum.util.parse_URI(url) url = electrum.util.parse_URI(url, self.on_pr)
except: except:
self.show_info("Invalid URI", url) self.show_info("Invalid URI", url)
return return
self.send_screen.set_URI(url) self.send_screen.set_URI(url)
def update_tab(self, name):
s = getattr(self, name + '_screen', None)
if s:
s.update()
@profiler
def update_tabs(self):
for tab in ['invoices', 'send', 'history', 'receive', 'requests']:
self.update_tab(tab)
def switch_to(self, name):
tab = self.tabs.ids[name + '_tab']
self.tabs.ids.panel.switch_to(tab)
def show_request(self, addr):
self.receive_address = addr
self.update_tab('receive')
self.switch_to('receive')
def scan_qr(self, on_complete): def scan_qr(self, on_complete):
from jnius import autoclass from jnius import autoclass
from android import activity from android import activity
@ -212,31 +257,10 @@ class ElectrumWindow(App):
if resultCode == -1: # RESULT_OK: if resultCode == -1: # RESULT_OK:
contents = intent.getStringExtra("SCAN_RESULT") contents = intent.getStringExtra("SCAN_RESULT")
if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE': if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE':
try: on_complete(contents)
uri = electrum.util.parse_URI(contents)
except:
self.show_info("Invalid URI", url)
return
on_complete(uri)
activity.bind(on_activity_result=on_qr_result) activity.bind(on_activity_result=on_qr_result)
PythonActivity.mActivity.startActivityForResult(intent, 0) PythonActivity.mActivity.startActivityForResult(intent, 0)
def show_plugins(self, plugins_list):
def on_active(sw, value):
self.plugins.toggle_enabled(self.electrum_config, sw.name)
run_hook('init_kivy', self)
for item in self.plugins.descriptions:
if 'kivy' not in item.get('available_for', []):
continue
name = item.get('__name__')
label = Label(text=item.get('fullname'), height='48db', size_hint=(1, None))
plugins_list.add_widget(label)
sw = Switch()
sw.name = name
p = self.plugins.get(name)
sw.active = (p is not None) and p.is_enabled()
sw.bind(active=on_active)
plugins_list.add_widget(sw)
def build(self): def build(self):
return Builder.load_file('gui/kivy/main.kv') return Builder.load_file('gui/kivy/main.kv')
@ -273,30 +297,46 @@ class ElectrumWindow(App):
win.bind(keyboard_height=self.on_keyboard_height) win.bind(keyboard_height=self.on_keyboard_height)
self.on_size(win, win.size) self.on_size(win, win.size)
self.init_ui()
self.load_wallet_by_name(self.electrum_config.get_wallet_path())
def load_wallet_by_name(self, wallet_path):
if not wallet_path:
return
self.stop_wallet()
config = self.electrum_config config = self.electrum_config
storage = WalletStorage(config.get_wallet_path()) storage = WalletStorage(wallet_path)
Logger.info('Electrum: Check for existing wallet') Logger.info('Electrum: Check for existing wallet')
if storage.file_exists: if storage.file_exists:
wallet = Wallet(storage) wallet = Wallet(storage)
action = wallet.get_action() action = wallet.get_action()
else: else:
action = 'new' action = 'new'
if action is not None: if action is not None:
# start installation wizard # start installation wizard
Logger.debug('Electrum: Wallet not found. Launching install wizard') Logger.debug('Electrum: Wallet not found. Launching install wizard')
wizard = Factory.InstallWizard(config, self.network, storage) wizard = Factory.InstallWizard(config, self.network, storage)
wizard.bind(on_wizard_complete=self.on_wizard_complete) wizard.bind(on_wizard_complete=lambda instance, wallet: self.load_wallet(wallet))
wizard.run(action) wizard.run(action)
else: else:
wallet.start_threads(self.network) wallet.start_threads(self.network)
self.on_wizard_complete(None, wallet) self.load_wallet(wallet)
self.on_resume() self.on_resume()
def create_wallet_dialog(self, l):
from uix.dialogs.label_dialog import LabelDialog
def f(text):
if text:
l.text = text
d = LabelDialog(_('Enter wallet name'), '', f)
d.open()
def on_stop(self): def on_stop(self):
self.stop_wallet()
def stop_wallet(self):
if self.wallet: if self.wallet:
self.wallet.stop_threads() self.wallet.stop_threads()
@ -316,7 +356,6 @@ class ElectrumWindow(App):
active_widg = self.root.children[0] active_widg = self.root.children[0]
except IndexError: except IndexError:
return return
try: try:
fw = self._focused_widget fw = self._focused_widget
except AttributeError: except AttributeError:
@ -350,29 +389,25 @@ class ElectrumWindow(App):
self.gui.main_gui.toggle_settings(self) self.gui.main_gui.toggle_settings(self)
return True return True
def on_wizard_complete(self, instance, wallet):
if not wallet:
Logger.debug('Electrum: No Wallet set/found. Exiting...')
app = App.get_running_app()
app.show_error('Electrum: No Wallet set/found. Exiting...',
exit=True)
self.init_ui()
self.load_wallet(wallet)
def popup_dialog(self, name): def popup_dialog(self, name):
if name == 'settings':
from uix.dialogs.settings import SettingsDialog
d = SettingsDialog(self)
d.open()
elif name == 'wallets':
from uix.dialogs.wallets import WalletDialog
d = WalletDialog()
d.open()
else:
popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv') popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv')
popup.open() popup.open()
@profiler @profiler
def init_ui(self): def init_ui(self):
''' Initialize The Ux part of electrum. This function performs the basic ''' Initialize The Ux part of electrum. This function performs the basic
tasks of setting up the ui. tasks of setting up the ui.
''' '''
from weakref import ref from weakref import ref
set_language(self.electrum_config.get('language'))
self.funds_error = False self.funds_error = False
# setup UX # setup UX
@ -401,7 +436,8 @@ class ElectrumWindow(App):
interests = ['updated', 'status', 'new_transaction'] interests = ['updated', 'status', 'new_transaction']
self.network.register_callback(self.on_network, interests) self.network.register_callback(self.on_network, interests)
self.wallet = None #self.wallet = None
self.tabs = self.root.ids['tabs']
def on_network(self, event, *args): def on_network(self, event, *args):
if event == 'updated': if event == 'updated':
@ -418,7 +454,7 @@ class ElectrumWindow(App):
self.update_wallet() self.update_wallet()
# Once GUI has been initialized check if we want to announce something # Once GUI has been initialized check if we want to announce something
# since the callback has been called before the GUI was initialized # since the callback has been called before the GUI was initialized
self.update_history_tab() self.update_tabs()
self.notify_transactions() self.notify_transactions()
run_hook('load_wallet', wallet, self) run_hook('load_wallet', wallet, self)
@ -447,58 +483,17 @@ class ElectrumWindow(App):
amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs, None) amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs, None)
return format_satoshis_plain(amount, self.decimal_point()) return format_satoshis_plain(amount, self.decimal_point())
def update_password(self, label, c):
text = label.password
if c == '<':
text = text[:-1]
elif c == 'Clear':
text = ''
else:
text += c
label.password = text
def toggle_fiat(self, a):
a.is_fiat = not a.is_fiat
def update_amount(self, label, c):
amount = label.fiat_amount if label.is_fiat else label.amount
if c == '<':
amount = amount[:-1]
elif c == '.' and amount == '':
amount = '0.'
elif c == '0' and amount == '0':
amount = '0'
else:
try:
Decimal(amount+c)
amount += c
except:
pass
if label.is_fiat:
label.fiat_amount = amount
else:
label.amount = amount
def format_amount(self, x, is_diff=False, whitespaces=False): def format_amount(self, x, is_diff=False, whitespaces=False):
return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces) return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces)
def format_amount_and_units(self, x):
return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit
@profiler @profiler
def update_wallet(self, *dt): def update_wallet(self, *dt):
self._trigger_update_status() self._trigger_update_status()
if self.wallet.up_to_date or not self.network or not self.network.is_connected(): #if self.wallet.up_to_date or not self.network or not self.network.is_connected():
self.update_history_tab() self.update_tabs()
self.update_contacts_tab()
@profiler
def update_history_tab(self, see_all=False):
if self.history_screen:
self.history_screen.update(see_all)
def update_contacts_tab(self):
if self.contacts_screen:
self.contacts_screen.update()
@profiler @profiler
@ -593,64 +588,10 @@ class ElectrumWindow(App):
else: else:
self.show_error(_('Invalid Address')) self.show_error(_('Invalid Address'))
def send_payment(self, address, amount=0, label='', message=''):
tabs = self.tabs
screen_send = tabs.ids.screen_send
if label and self.wallet.labels.get(address) != label:
#if self.question('Give label "%s" to address %s ?'%(label,address)):
if address not in self.wallet.addressbook and not self.wallet. is_mine(address):
self.wallet.addressbook.append(address)
self.wallet.set_label(address, label)
# switch_to the send screen
tabs.ids.panel.switch_to(tabs.ids.tab_send)
label = self.wallet.labels.get(address)
m_addr = label + ' <'+ address +'>' if label else address
# populate
def set_address(*l):
content = screen_send.ids
content.payto_e.text = m_addr
content.message_e.text = message
if amount:
content.amount_e.text = amount
# wait for screen to load
Clock.schedule_once(set_address, .5)
def set_send(self, address, amount, label, message): def set_send(self, address, amount, label, message):
self.send_payment(address, amount=amount, label=label, message=message) self.send_payment(address, amount=amount, label=label, message=message)
def prepare_for_payment_request(self):
tabs = self.tabs
screen_send = tabs.ids.screen_send
# switch_to the send screen
tabs.ids.panel.switch_to(tabs.ids.tab_send)
content = screen_send.ids
if content:
self.set_frozen(content, False)
screen_send.screen_label.text = _("please wait...")
return True
def payment_request_ok(self):
tabs = self.tabs
screen_send = tabs.ids.screen_send
# switch_to the send screen
tabs.ids.panel.switch_to(tabs.ids.tab_send)
self.set_frozen(content, True)
screen_send.ids.payto_e.text = self.gui_object.payment_request.domain
screen_send.ids.amount_e.text = self.format_amount(self.gui_object.payment_request.get_amount())
screen_send.ids.message_e.text = self.gui_object.payment_request.memo
# wait for screen to load
Clock.schedule_once(set_address, .5)
def set_frozen(self, entry, frozen): def set_frozen(self, entry, frozen):
if frozen: if frozen:
@ -660,15 +601,6 @@ class ElectrumWindow(App):
entry.disabled = False entry.disabled = False
Factory.Animation(opacity=1).start(content) Factory.Animation(opacity=1).start(content)
def payment_request_error(self):
tabs = self.tabs
screen_send = tabs.ids.screen_send
# switch_to the send screen
tabs.ids.panel.switch_to(tabs.ids.tab_send)
self.do_clear()
self.show_info(self.gui_object.payment_request.error)
def show_error(self, error, width='200dp', pos=None, arrow_pos=None, def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0, exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0,
@ -737,43 +669,32 @@ class ElectrumWindow(App):
pos = (win.center[0], win.center[1] - (info_bubble.height/2)) pos = (win.center[0], win.center[1] - (info_bubble.height/2))
info_bubble.show(pos, duration, width, modal=modal, exit=exit) info_bubble.show(pos, duration, width, modal=modal, exit=exit)
def tx_dialog(self, tx_hash): def tx_details_dialog(self, obj):
popup = Builder.load_file('gui/kivy/uix/ui_screens/transaction.kv') popup = Builder.load_file('gui/kivy/uix/ui_screens/transaction.kv')
popup.tx_hash = tx_hash popup.tx_hash = obj.tx_hash
popup.open() popup.open()
def tx_selected(self, txid, state): def address_dialog(self, screen):
if state == 'down': pass
self.context = 'tx'
self.context_action = lambda: self.tx_dialog(txid)
else:
self.reset_context()
def reset_context(self): def description_dialog(self, screen):
self.context = '' from uix.dialogs.label_dialog import LabelDialog
self.context_action = lambda: None text = screen.message
def callback(text):
screen.message = text
d = LabelDialog(_('Enter description'), text, callback)
d.open()
@profiler
def amount_dialog(self, screen, show_max): def amount_dialog(self, screen, show_max):
popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv') from uix.dialogs.amount_dialog import AmountDialog
but_max = popup.ids.but_max
if not show_max:
but_max.disabled = True
but_max.opacity = 0
else:
but_max.disabled = False
but_max.opacity = 1
amount = screen.amount amount = screen.amount
if amount: if amount:
a, u = str(amount).split() amount, u = str(amount).split()
assert u == self.base_unit assert u == self.base_unit
popup.ids.kb.amount = a def cb(amount):
screen.amount = amount
def cb(): popup = AmountDialog(show_max, amount, cb)
o = popup.ids.a.btc_text
screen.amount = o
popup.on_dismiss = cb
popup.open() popup.open()
def protected(self, f, args): def protected(self, f, args):
@ -804,12 +725,9 @@ class ElectrumWindow(App):
self.show_error("PIN numbers do not match") self.show_error("PIN numbers do not match")
def password_dialog(self, title, f, args): def password_dialog(self, title, f, args):
popup = Builder.load_file('gui/kivy/uix/ui_screens/password.kv') from uix.dialogs.password_dialog import PasswordDialog
popup.title = title def callback(pw):
def callback():
pw = popup.ids.kb.password
Clock.schedule_once(lambda x: apply(f, args + (pw,)), 0.1) Clock.schedule_once(lambda x: apply(f, args + (pw,)), 0.1)
popup.on_dismiss = callback popup = PasswordDialog(title, callback)
popup.open() popup.open()

View File

@ -0,0 +1,52 @@
#!python
#!/usr/bin/env python
from kivy.app import App
from kivy.uix.bubble import Bubble
from kivy.animation import Animation
from kivy.uix.floatlayout import FloatLayout
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.clock import Clock
Builder.load_string('''
<MenuItem@Button>
background_color: .2, .9, 1, 1
height: '48dp'
size_hint: 1, None
<ContextMenu>
size_hint: 1, None
height: '48dp'
pos: (0, 0)
show_arrow: False
arrow_pos: 'top_mid'
padding: 0
orientation: 'horizontal'
BoxLayout:
size_hint: 1, 1
height: '48dp'
orientation: 'horizontal'
id: buttons
''')
class MenuItem(Factory.Button):
pass
class ContextMenu(Bubble):
def __init__(self, obj, action_list):
Bubble.__init__(self)
self.obj = obj
for k, v in action_list:
l = MenuItem()
l.text = k
def func(f=v):
Clock.schedule_once(lambda dt: self.hide(), 0.1)
Clock.schedule_once(lambda dt: f(obj), 0.15)
l.on_release = func
self.ids.buttons.add_widget(l)
def hide(self):
if self.parent:
self.parent.hide_menu()

View File

@ -144,6 +144,7 @@ class InfoBubble(Factory.Bubble):
m.add_widget(self) m.add_widget(self)
else: else:
Window.add_widget(self) Window.add_widget(self)
# wait for the bubble to adjust it's size according to text then animate # wait for the bubble to adjust it's size according to text then animate
Clock.schedule_once(lambda dt: self._show(pos, duration)) Clock.schedule_once(lambda dt: self._show(pos, duration))

View File

@ -1,13 +1,16 @@
#:import Decimal decimal.Decimal from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from decimal import Decimal
Builder.load_string('''
Popup: <AmountDialog@Popup>
id: popup id: popup
title: _('Amount') title: _('Amount')
AnchorLayout: AnchorLayout:
anchor_x: 'center' anchor_x: 'center'
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
size_hint: 0.8, 1 size_hint: 0.8, 1
@ -18,7 +21,7 @@ Popup:
id: a id: a
btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else '' btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else ''
fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else '' fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else ''
text: (self.fiat_text + ' / ' + self.btc_text if kb.is_fiat else self.btc_text + ' / ' + self.fiat_text) if self.btc_text else '' text: ((self.fiat_text + ' / ' + self.btc_text if kb.is_fiat else self.btc_text + ' / ' + self.fiat_text) if app.fiat_unit else self.btc_text) if self.btc_text else ''
size_hint: 1, 1 size_hint: 1, 1
font_size: '22dp' font_size: '22dp'
Widget: Widget:
@ -30,8 +33,8 @@ Popup:
is_fiat: False is_fiat: False
on_fiat_amount: if self.is_fiat: self.amount = app.fiat_to_btc(self.fiat_amount) on_fiat_amount: if self.is_fiat: self.amount = app.fiat_to_btc(self.fiat_amount)
on_amount: if not self.is_fiat: self.fiat_amount = app.btc_to_fiat(self.amount) on_amount: if not self.is_fiat: self.fiat_amount = app.btc_to_fiat(self.amount)
update_text: app.update_amount
size_hint: 1, None size_hint: 1, None
update_amount: popup.update_amount
height: '300dp' height: '300dp'
cols: 3 cols: 3
KButton: KButton:
@ -60,6 +63,8 @@ Popup:
text: '<' text: '<'
Button: Button:
id: but_max id: but_max
opacity: 1 if root.show_max else 0
disabled: not root.show_max
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
text: 'Max' text: 'Max'
@ -70,9 +75,9 @@ Popup:
id: button_fiat id: button_fiat
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
text: app.fiat_unit if kb.is_fiat else app.base_unit text: (app.fiat_unit if kb.is_fiat else app.base_unit) if app.fiat_unit else ''
on_release: on_release:
app.toggle_fiat(kb) popup.toggle_fiat(kb)
Button: Button:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
@ -80,18 +85,53 @@ Popup:
on_release: on_release:
kb.amount = '' kb.amount = ''
kb.fiat_amount = '' kb.fiat_amount = ''
Widget: Widget:
size_hint: 1, None size_hint: 1, None
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
Widget: Widget:
size_hint: 2, None size_hint: 1, None
height: '48dp' height: '48dp'
Button: Button:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
text: _('OK') text: _('OK')
on_release: popup.dismiss() on_release:
root.callback(a.btc_text)
popup.dismiss()
''')
from kivy.properties import BooleanProperty
class AmountDialog(Factory.Popup):
show_max = BooleanProperty(False)
def __init__(self, show_max, amount, cb):
Factory.Popup.__init__(self)
self.show_max = show_max
self.callback = cb
if amount:
self.ids.kb.amount = amount
def toggle_fiat(self, a):
a.is_fiat = not a.is_fiat
def update_amount(self, c):
kb = self.ids.kb
amount = kb.fiat_amount if kb.is_fiat else kb.amount
if c == '<':
amount = amount[:-1]
elif c == '.' and amount in ['0', '']:
amount = '0.'
elif amount == '0':
amount = c
else:
try:
Decimal(amount+c)
amount += c
except:
pass
if kb.is_fiat:
kb.fiat_amount = amount
else:
kb.amount = amount

View File

@ -0,0 +1,65 @@
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.uix.checkbox import CheckBox
from kivy.uix.label import Label
from kivy.uix.widget import Widget
Builder.load_string('''
<ChoiceDialog@Popup>
id: popup
title: ''
size_hint: 0.8, 0.8
pos_hint: {'top':0.9}
BoxLayout:
orientation: 'vertical'
Widget:
size_hint: 1, 0.1
ScrollView:
orientation: 'vertical'
size_hint: 1, 0.8
GridLayout:
row_default_height: '48dp'
orientation: 'vertical'
id: choices
cols: 2
size_hint: 1, 1
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.2
Button:
text: 'Cancel'
size_hint: 0.5, None
height: '48dp'
on_release: popup.dismiss()
Button:
text: 'OK'
size_hint: 0.5, None
height: '48dp'
on_release:
root.callback(popup.value)
popup.dismiss()
''')
class ChoiceDialog(Factory.Popup):
def __init__(self, title, choices, key, callback):
Factory.Popup.__init__(self)
for k, v in choices.items():
l = Label(text=v)
l.height = '48dp'
cb = CheckBox(group='choices')
cb.value = k
cb.height = '48dp'
def f(cb, x):
if x: self.value = cb.value
cb.bind(active=f)
if k == key:
cb.active = True
self.ids.choices.add_widget(l)
self.ids.choices.add_widget(cb)
self.ids.choices.add_widget(Widget(size_hint_y=1))
self.callback = callback
self.title = title
self.value = key

View File

@ -43,7 +43,6 @@ Builder.load_string('''
on_release: if self.root: self.root.dispatch('on_release', self) on_release: if self.root: self.root.dispatch('on_release', self)
<-CreateAccountDialog> <-CreateAccountDialog>
text_color: .854, .925, .984, 1 text_color: .854, .925, .984, 1
auto_dismiss: False auto_dismiss: False
@ -140,11 +139,11 @@ Builder.load_string('''
height: self.minimum_height height: self.minimum_height
CreateAccountButton: CreateAccountButton:
id: create id: create
text: _('Create a Wallet') text: _('Create a new seed')
root: root root: root
CreateAccountButton: CreateAccountButton:
id: restore id: restore
text: _('I already have a wallet') text: _('I already have a seed')
root: root root: root
@ -457,8 +456,7 @@ class RestoreSeedDialog(CreateAccountDialog):
self._trigger_check_seed = Clock.create_trigger(self.check_seed) self._trigger_check_seed = Clock.create_trigger(self.check_seed)
def check_seed(self, dt): def check_seed(self, dt):
self.ids.next.disabled = not bool(self._wizard.is_any( self.ids.next.disabled = not bool(self._wizard.is_any(self.ids.text_input_seed))
self.ids.text_input_seed))
def on_parent(self, instance, value): def on_parent(self, instance, value):
if value: if value:

View File

@ -73,7 +73,9 @@ class InstallWizard(Widget):
def is_any(self, seed_e): def is_any(self, seed_e):
text = self.get_seed_text(seed_e) text = self.get_seed_text(seed_e)
return (Wallet.is_seed(text) or return (Wallet.is_seed(text) or
Wallet.is_mpk(text) or Wallet.is_old_mpk(text) or
Wallet.is_xpub(text) or
Wallet.is_xprv(text) or
Wallet.is_address(text) or Wallet.is_address(text) or
Wallet.is_private_key(text)) Wallet.is_private_key(text))
@ -129,8 +131,8 @@ class InstallWizard(Widget):
if Wallet.is_seed(seed): if Wallet.is_seed(seed):
return self.password_dialog(wallet=wallet, mode='restore', return self.password_dialog(wallet=wallet, mode='restore',
seed=seed) seed=seed)
elif Wallet.is_mpk(seed): elif Wallet.is_xpub(seed):
wallet = Wallet.from_mpk(seed, self.storage) wallet = Wallet.from_xpub(seed, self.storage)
elif Wallet.is_address(seed): elif Wallet.is_address(seed):
wallet = Wallet.from_address(seed, self.storage) wallet = Wallet.from_address(seed, self.storage)
elif Wallet.is_private_key(seed): elif Wallet.is_private_key(seed):
@ -257,18 +259,19 @@ class InstallWizard(Widget):
new_password = None new_password = None
if mode == 'restore': if mode == 'restore':
wallet = Wallet.from_seed(seed, self.storage) password = unicode(ti_password.text)
password = (unicode(ti_password.text) # if wallet and wallet.use_encryption else
if wallet and wallet.use_encryption else # None)
None) if not password:
password = None
wallet = Wallet.from_text(seed, password, self.storage)
def on_complete(*l): def on_complete(*l):
wallet.create_accounts(new_password)
self.load_network(wallet, mode='restore') self.load_network(wallet, mode='restore')
_dlg.close() _dlg.close()
self.waiting_dialog(lambda: wallet.add_seed(seed, new_password), self.waiting_dialog(wallet.synchronize,
msg=_("saving seed"), msg=_("generating addresses"),
on_complete=on_complete) on_complete=on_complete)
return return

View File

@ -0,0 +1,55 @@
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
Builder.load_string('''
<LabelDialog@Popup>
id: popup
title: ''
size_hint: 0.8, 0.3
pos_hint: {'top':0.9}
BoxLayout:
orientation: 'vertical'
Widget:
size_hint: 1, 0.2
TextInput:
id:input
padding: '5dp'
size_hint: 1, None
height: '27dp'
pos_hint: {'center_y':.5}
text:''
multiline: False
background_normal: 'atlas://gui/kivy/theming/light/tab_btn'
background_active: 'atlas://gui/kivy/theming/light/textinput_active'
hint_text_color: self.foreground_color
foreground_color: 1, 1, 1, 1
font_size: '16dp'
focus: True
Widget:
size_hint: 1, 0.2
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Button:
text: 'Cancel'
size_hint: 0.5, None
height: '48dp'
on_release: popup.dismiss()
Button:
text: 'OK'
size_hint: 0.5, None
height: '48dp'
on_release:
root.callback(input.text)
popup.dismiss()
''')
class LabelDialog(Factory.Popup):
def __init__(self, title, text, callback):
Factory.Popup.__init__(self)
self.ids.input.text = text
self.callback = callback
self.title = title

View File

@ -1,4 +1,12 @@
Popup: from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from decimal import Decimal
Builder.load_string('''
<PasswordDialog@Popup>
id: popup id: popup
title: _('Enter PIN Code') title: _('Enter PIN Code')
size_hint: 0.9, 0.9 size_hint: 0.9, 0.9
@ -16,9 +24,9 @@ Popup:
GridLayout: GridLayout:
id: kb id: kb
update_text: app.update_password update_amount: popup.update_password
password: '' password: ''
on_password: if len(self.password) == 6: popup.dismiss() on_password: popup.on_password(self.password)
size_hint: 1, None size_hint: 1, None
height: '300dp' height: '300dp'
cols: 3 cols: 3
@ -49,3 +57,28 @@ Popup:
Widget: Widget:
size_hint: 1, 1 size_hint: 1, 1
''')
class PasswordDialog(Factory.Popup):
def __init__(self, title, cb):
Factory.Popup.__init__(self)
self.title = title
self.callback = cb
def update_password(self, c):
kb = self.ids.kb
text = kb.password
if c == '<':
text = text[:-1]
elif c == 'Clear':
text = ''
else:
text += c
kb.password = text
def on_password(self, pw):
if len(pw) == 6:
self.dismiss()
self.callback(pw)

View File

@ -0,0 +1,171 @@
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from electrum.i18n import _
from electrum.util import base_units
from electrum.i18n import languages, set_language
Builder.load_string('''
<SettingsItem@ButtonBehavior+BoxLayout>
orientation: 'vertical'
title: ''
description: ''
size_hint: 1, 1
Label:
id: title
text: self.parent.title
size_hint: 1, 1
bold: True
text_size: self.size
halign: 'left'
Label:
text: self.parent.description
size_hint: 1, 1
text_size: self.width, None
color: 0.8, 0.8, 0.8, 1
halign: 'left'
<PluginItem@ButtonBehavior+BoxLayout>
orientation: 'vertical'
title: ''
description: ''
size_hint: 1, 1
BoxLayout:
orientation: 'horizontal'
Label:
id: title
text: self.parent.title
size_hint: 1, 1
bold: True
text_size: self.size
halign: 'left'
Switch:
id: sw
name: ''
Label:
text: self.parent.description
size_hint: 1, 1
text_size: self.width, None
color: 0.8, 0.8, 0.8, 1
halign: 'left'
<SettingsDialog@Popup>
id: settings
title: _('Settings')
BoxLayout:
orientation: 'vertical'
SettingsItem:
lang: settings.get_language_name()
title: _('Language') + ': %s'%self.lang
description: _("Language")
on_release:
settings.language_dialog(self)
CardSeparator
SettingsItem:
title: _('PIN Code') + ': %s'%('ON' if app.wallet.use_encryption else 'OFF')
description: _("Your PIN code will be required in order to spend bitcoins.")
on_release:
app.change_password()
self.title = _('PIN Code') + ' (%s)'%('ON' if app.wallet.use_encryption else 'OFF')
CardSeparator
SettingsItem:
bu: app.base_unit
title: _('Denomination') + ': ' + self.bu
description: _("Base unit for Bitcoin amounts.")
on_release:
settings.unit_dialog(self)
CardSeparator
SettingsItem:
title: _('Fiat Currency') + ': ' + app.fiat_unit
description: "Select the local fiat currency."
on_release:
settings.fiat_dialog(self)
CardSeparator
SettingsItem:
title: _('OpenAlias')
description: "Email-like address."
on_release:
settings.openalias_dialog()
Widget:
size_hint: 1, 1
BoxLayout:
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('OK')
on_release:
settings.dismiss()
''')
class SettingsDialog(Factory.Popup):
def __init__(self, app):
self.app = app
Factory.Popup.__init__(self)
def get_language_name(self):
return languages.get(self.app.electrum_config.get('language', 'en_UK'), '')
def language_dialog(self, item):
from choice_dialog import ChoiceDialog
l = self.app.electrum_config.get('language', 'en_UK')
def cb(key):
self.app.electrum_config.set_key("language", key, True)
item.lang = self.get_language_name()
set_language(key)
d = ChoiceDialog(_('Language'), languages, l, cb)
d.open()
def unit_dialog(self, item):
from choice_dialog import ChoiceDialog
def cb(text):
self.app._set_bu(text)
item.bu = self.app.base_unit
d = ChoiceDialog(_('Denomination'), dict(map(lambda x: (x,x), base_units)), self.app.base_unit, cb)
d.open()
def fiat_dialog(self, item):
from choice_dialog import ChoiceDialog
def cb(text):
pass
d = ChoiceDialog(_('Fiat Currency'), {}, '', cb)
d.open()
def openalias_dialog(self):
from label_dialog import LabelDialog
def callback(text):
pass
d = LabelDialog(_('OpenAlias'), '', callback)
d.open()
def show_plugins(self, plugins_list):
def on_active(sw, value):
self.plugins.toggle_enabled(self.electrum_config, sw.name)
run_hook('init_kivy', self)
for item in self.plugins.descriptions:
if 'kivy' not in item.get('available_for', []):
continue
name = item.get('__name__')
label = Label(text=item.get('fullname'), height='48db', size_hint=(1, None))
plugins_list.add_widget(label)
sw = Switch()
sw.name = name
p = self.plugins.get(name)
sw.active = (p is not None) and p.is_enabled()
sw.bind(active=on_active)
plugins_list.add_widget(sw)
class PluginItem():
def __init__(self, name):
p = self.plugins.get(name)
sw.active = (p is not None) and p.is_enabled()
sw.bind(active=on_active)
plugins_list.add_widget(sw)

View File

@ -0,0 +1,79 @@
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from electrum.i18n import _
from electrum.util import base_units
import os
from label_dialog import LabelDialog
Builder.load_string('''
#:import os os
<WalletDialog@Popup>:
title: _('Wallets')
id: popup
path: app.wallet.storage.path
on_path:
button.text = _('Open') if os.path.exists(popup.path) else _('Create')
BoxLayout:
orientation: 'vertical'
BoxLayout:
height: '48dp'
size_hint_y: None
orientation: 'horizontal'
Label:
text: _('Wallet') + ': '
height: '48dp'
size_hint_y: None
Button:
id: wallet_name
height: '48dp'
size_hint_y: None
text: os.path.basename(app.wallet.storage.path)
on_release:
root.name_dialog()
on_text:
popup.path = os.path.join(wallet_selector.path, self.text)
Widget
size_hint_y: None
FileChooserListView:
id: wallet_selector
dirselect: False
filter_dirs: True
filter: '*.*'
path: os.path.dirname(app.wallet.storage.path)
on_selection:
wallet_name.text = os.path.basename(self.selection[0]) if self.selection else ''
size_hint_y: 0.4
Widget
size_hint_y: 0.1
GridLayout:
cols: 2
size_hint_y: None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Cancel')
on_release:
popup.dismiss()
Button:
id: button
size_hint: 0.5, None
height: '48dp'
text: _('Open') if os.path.exists(popup.path) else _('Create')
on_release:
popup.dismiss()
app.load_wallet_by_name(popup.path)
''')
class WalletDialog(Factory.Popup):
def name_dialog(self):
def cb(text):
if text:
self.ids.wallet_name.text = text
d = LabelDialog(_('Enter wallet name'), '', cb)
d.open()

View File

@ -42,14 +42,9 @@ Builder.load_string('''
class QRCodeWidget(FloatLayout): class QRCodeWidget(FloatLayout):
data = StringProperty(None, allow_none=True) data = StringProperty(None, allow_none=True)
background_color = ListProperty((1, 1, 1, 1)) background_color = ListProperty((1, 1, 1, 1))
foreground_color = ListProperty((0, 0, 0, 0)) foreground_color = ListProperty((0, 0, 0, 0))
#loading_image = StringProperty('gui/kivy/theming/loading.gif')
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(QRCodeWidget, self).__init__(**kwargs) super(QRCodeWidget, self).__init__(**kwargs)
self.data = None self.data = None
@ -57,21 +52,11 @@ class QRCodeWidget(FloatLayout):
self._qrtexture = None self._qrtexture = None
def on_data(self, instance, value): def on_data(self, instance, value):
print "on data", value
if not (self.canvas or value): if not (self.canvas or value):
return return
img = self.ids.get('qrimage', None)
if not img:
# if texture hasn't yet been created delay the texture updation
Clock.schedule_once(lambda dt: self.on_data(instance, value))
return
#Thread(target=partial(self.update_qr, )).start()
self.update_qr() self.update_qr()
def set_data(self, data): def set_data(self, data):
print "set data", data
if self.data == data: if self.data == data:
return return
MinSize = 210 if len(data) < 128 else 500 MinSize = 210 if len(data) < 128 else 500
@ -98,7 +83,7 @@ class QRCodeWidget(FloatLayout):
# currently unused, do we need this? # currently unused, do we need this?
self._texture_size = size self._texture_size = size
def _create_texture(self, k, dt): def _create_texture(self, k):
self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb') self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb')
# don't interpolate texture # don't interpolate texture
texture.min_filter = 'nearest' texture.min_filter = 'nearest'
@ -107,32 +92,24 @@ class QRCodeWidget(FloatLayout):
def update_texture(self): def update_texture(self):
if not self.qr: if not self.qr:
return return
matrix = self.qr.get_matrix() matrix = self.qr.get_matrix()
k = len(matrix) k = len(matrix)
# create the texture in main UI thread otherwise # create the texture
# this will lead to memory corruption self._create_texture(k)
Clock.schedule_once(partial(self._create_texture, k), -1)
buff = [] buff = []
bext = buff.extend bext = buff.extend
cr, cg, cb, ca = self.background_color[:] cr, cg, cb, ca = self.background_color[:]
cr, cg, cb = cr*255, cg*255, cb*255 cr, cg, cb = cr*255, cg*255, cb*255
for r in range(k): for r in range(k):
for c in range(k): for c in range(k):
bext([0, 0, 0] if matrix[r][c] else [cr, cg, cb]) bext([0, 0, 0] if matrix[k-1-r][c] else [cr, cg, cb])
# then blit the buffer # then blit the buffer
buff = ''.join(map(chr, buff)) buff = ''.join(map(chr, buff))
# update texture in UI thread. # update texture
Clock.schedule_once(lambda dt: self._upd_texture(buff), .1) self._upd_texture(buff)
def _upd_texture(self, buff): def _upd_texture(self, buff):
texture = self._qrtexture texture = self._qrtexture
if not texture:
# if texture hasn't yet been created delay the texture updation
Clock.schedule_once(lambda dt: self._upd_texture(buff), .1)
return
texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte') texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte')
img =self.ids.qrimage img =self.ids.qrimage
img.anim_delay = -1 img.anim_delay = -1

View File

@ -17,17 +17,24 @@ from kivy.lang import Builder
from kivy.factory import Factory from kivy.factory import Factory
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import profiler, parse_URI from electrum.util import profiler, parse_URI, format_time
from electrum import bitcoin from electrum import bitcoin
from electrum.util import timestamp_to_datetime from electrum.util import timestamp_to_datetime
from electrum.plugins import run_hook from electrum.plugins import run_hook
from context_menu import ContextMenu
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
class CScreen(Factory.Screen): class CScreen(Factory.Screen):
__events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave') __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
action_view = ObjectProperty(None) action_view = ObjectProperty(None)
loaded = False loaded = False
kvname = None kvname = None
context_menu = None
menu_actions = []
app = App.get_running_app() app = App.get_running_app()
def _change_action_view(self): def _change_action_view(self):
@ -65,8 +72,17 @@ class CScreen(Factory.Screen):
self.dispatch('on_deactivate') self.dispatch('on_deactivate')
def on_deactivate(self): def on_deactivate(self):
pass self.hide_menu()
#Clock.schedule_once(lambda dt: self._change_action_view())
def hide_menu(self):
if self.context_menu is not None:
self.remove_widget(self.context_menu)
self.context_menu = None
def show_menu(self, obj):
self.hide_menu()
self.context_menu = ContextMenu(obj, self.menu_actions)
self.add_widget(self.context_menu)
class HistoryScreen(CScreen): class HistoryScreen(CScreen):
@ -77,10 +93,18 @@ class HistoryScreen(CScreen):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.ra_dialog = None self.ra_dialog = None
super(HistoryScreen, self).__init__(**kwargs) super(HistoryScreen, self).__init__(**kwargs)
self.menu_actions = [ (_('Label'), self.label_dialog), (_('Details'), self.app.tx_details_dialog)]
def label_dialog(self, obj):
from dialogs.label_dialog import LabelDialog
key = obj.tx_hash
text = self.app.wallet.get_label(key)
def callback(text):
self.app.wallet.set_label(key, text)
self.update()
d = LabelDialog(_('Enter Transaction Label'), text, callback)
d.open()
def get_history_rate(self, btc_balance, timestamp):
date = timestamp_to_datetime(timestamp)
return run_hook('historical_value_str', btc_balance, date)
def parse_history(self, items): def parse_history(self, items):
for item in items: for item in items:
@ -98,49 +122,43 @@ class HistoryScreen(CScreen):
time_str = _('pending') time_str = _('pending')
icon = "atlas://gui/kivy/theming/light/unconfirmed" icon = "atlas://gui/kivy/theming/light/unconfirmed"
elif conf < 6: elif conf < 6:
time_str = '' # add new to fix error when conf < 0
conf = max(1, conf) conf = max(1, conf)
icon = "atlas://gui/kivy/theming/light/clock{}".format(conf) icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)
else: else:
icon = "atlas://gui/kivy/theming/light/confirmed" icon = "atlas://gui/kivy/theming/light/confirmed"
if tx_hash: label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
label, is_default_label = self.app.wallet.get_label(tx_hash) date = timestamp_to_datetime(timestamp)
rate = run_hook('history_rate', date)
if self.app.fiat_unit:
quote_text = "..." if rate is None else "{0:.3} {1}".format(Decimal(value) / 100000000 * Decimal(rate), self.app.fiat_unit)
else: else:
label = _('Pruned transaction outputs') quote_text = ''
is_default_label = False
quote_currency = 'USD'
rate = self.get_history_rate(value, timestamp)
quote_text = "..." if rate is None else "{0:.3} {1}".format(rate, quote_currency)
yield (conf, icon, time_str, label, value, tx_hash, quote_text) yield (conf, icon, time_str, label, value, tx_hash, quote_text)
def update(self, see_all=False): def update(self, see_all=False):
if self.app.wallet is None: if self.app.wallet is None:
return return
history_card = self.screen.ids.recent_activity_card history_card = self.screen.ids.history_container
history = self.parse_history(reversed( history = self.parse_history(reversed(
self.app.wallet.get_history(self.app.current_account))) self.app.wallet.get_history(self.app.current_account)))
# repopulate History Card # repopulate History Card
last_widget = history_card.ids.content.children[-1] history_card.clear_widgets()
history_card.ids.content.clear_widgets() history_add = history_card.add_widget
history_add = history_card.ids.content.add_widget
history_add(last_widget)
RecentActivityItem = Factory.RecentActivityItem
count = 0 count = 0
for item in history: for item in history:
count += 1 count += 1
conf, icon, date_time, address, value, tx, quote_text = item conf, icon, date_time, message, value, tx, quote_text = item
ri = RecentActivityItem() ri = Factory.HistoryItem()
ri.icon = icon ri.icon = icon
ri.date = date_time ri.date = date_time
ri.address = address ri.message = message
ri.value = value ri.value = value
ri.quote_text = quote_text ri.quote_text = quote_text
ri.confirmations = conf ri.confirmations = conf
ri.tx_hash = tx ri.tx_hash = tx
ri.screen = self
history_add(ri) history_add(ri)
if count == 8 and not see_all: if count == 8 and not see_all:
break break
@ -181,20 +199,35 @@ class ScreenPassword(Factory.Screen):
class SendScreen(CScreen): class SendScreen(CScreen):
kvname = 'send' kvname = 'send'
payment_request = None
def set_URI(self, uri): def set_URI(self, uri):
print "set uri", uri
self.screen.address = uri.get('address', '') self.screen.address = uri.get('address', '')
self.screen.message = uri.get('message', '') self.screen.message = uri.get('message', '')
amount = uri.get('amount') amount = uri.get('amount')
if amount: if amount:
amount_str = str( Decimal(amount) / pow(10, self.app.decimal_point())) self.screen.amount = self.app.format_amount_and_units(amount)
self.screen.amount = amount_str + ' ' + self.app.base_unit
def update(self):
if self.app.current_invoice:
self.set_request(self.app.current_invoice)
def do_clear(self): def do_clear(self):
self.screen.amount = '' self.screen.amount = ''
self.screen.message = '' self.screen.message = ''
self.screen.address = '' self.screen.address = ''
self.payment_request = None
def amount_dialog(self):
Clock.schedule_once(lambda dt: self.app.amount_dialog(self, True), .25)
def set_request(self, pr):
self.payment_request = pr
self.screen.address = pr.get_requestor()
amount = pr.get_amount()
if amount:
self.screen.amount = self.app.format_amount_and_units(amount)
self.screen.message = pr.get_memo()
def do_paste(self): def do_paste(self):
contents = unicode(self.app._clipboard.get()) contents = unicode(self.app._clipboard.get())
@ -206,6 +239,12 @@ class SendScreen(CScreen):
self.set_URI(uri) self.set_URI(uri)
def do_send(self): def do_send(self):
if self.payment_request:
if self.payment_request.has_expired():
self.app.show_error(_('Payment request has expired'))
return
outputs = self.payment_request.get_outputs()
else:
address = str(self.screen.address) address = str(self.screen.address)
if not bitcoin.is_address(address): if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
@ -215,9 +254,9 @@ class SendScreen(CScreen):
except: except:
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount) self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
return return
outputs = [('address', address, amount)]
message = unicode(self.screen.message) message = unicode(self.screen.message)
fee = None fee = None
outputs = [('address', address, amount)]
self.app.protected(self.send_tx, (outputs, fee, message)) self.app.protected(self.send_tx, (outputs, fee, message))
def send_tx(self, *args): def send_tx(self, *args):
@ -250,7 +289,14 @@ class ReceiveScreen(CScreen):
kvname = 'receive' kvname = 'receive'
def update(self): def update(self):
self.screen.address = self.app.wallet.get_unused_address(None) addr = self.app.get_receive_address()
self.screen.address = addr
req = self.app.wallet.receive_requests.get(addr)
if req:
self.screen.message = unicode(req.get('memo', ''))
amount = req.get('amount')
if amount:
self.screen.amount = self.app.format_amount_and_units(amount)
def amount_callback(self, popup): def amount_callback(self, popup):
amount_label = self.screen.ids.get('amount') amount_label = self.screen.ids.get('amount')
@ -269,17 +315,35 @@ class ReceiveScreen(CScreen):
@profiler @profiler
def update_qr(self): def update_qr(self):
uri = self.get_URI() uri = self.get_URI()
qr = self.screen.ids.get('qr') qr = self.screen.ids.qr
qr.set_data(uri) qr.set_data(uri)
def do_copy(self): def do_copy(self):
uri = self.get_URI() uri = self.get_URI()
self.app._clipboard.put(uri, 'text/plain') self.app._clipboard.put(uri, 'text/plain')
def do_clear(self): def do_save(self):
addr = str(self.screen.address)
amount = str(self.screen.amount)
message = str(self.screen.message) #.ids.message_input.text)
if not message and not amount:
self.app.show_error(_('No message or amount'))
return
if amount:
amount = self.app.get_amount(amount)
else:
amount = 0
print "saving", amount, message
req = self.app.wallet.make_payment_request(addr, amount, message, None)
self.app.wallet.add_payment_request(req, self.app.electrum_config)
self.app.show_error(_('Request saved'))
self.app.update_tab('requests')
def do_new(self):
self.app.receive_address = None
self.screen.amount = '' self.screen.amount = ''
self.screen.message = '' self.screen.message = ''
self.update()
class ContactsScreen(CScreen): class ContactsScreen(CScreen):
@ -311,6 +375,76 @@ class ContactsScreen(CScreen):
contact_list.add_widget(ci) contact_list.add_widget(ci)
class InvoicesScreen(CScreen):
kvname = 'invoices'
def update(self):
self.menu_actions = [(_('Pay'), self.do_pay), (_('Delete'), self.do_delete)]
invoices_list = self.screen.ids.invoices_container
invoices_list.clear_widgets()
for pr in self.app.invoices.sorted_list():
ci = Factory.InvoiceItem()
ci.key = pr.get_id()
ci.requestor = pr.get_requestor()
ci.memo = pr.memo
ci.amount = self.app.format_amount_and_units(pr.get_amount())
status = self.app.invoices.get_status(ci.key)
if status == PR_PAID:
ci.icon = "atlas://gui/kivy/theming/light/confirmed"
elif status == PR_EXPIRED:
ci.icon = "atlas://gui/kivy/theming/light/important"
else:
ci.icon = "atlas://gui/kivy/theming/light/important"
exp = pr.get_expiration_date()
ci.date = format_time(exp) if exp else _('Never')
ci.screen = self
invoices_list.add_widget(ci)
def do_pay(self, obj):
self.app.do_pay(obj)
def do_delete(self, obj):
self.app.invoices.remove(obj.key)
self.app.update_tab('invoices')
class RequestsScreen(CScreen):
kvname = 'requests'
def update(self):
self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)]
requests_list = self.screen.ids.requests_container
requests_list.clear_widgets()
for req in self.app.wallet.get_sorted_requests(self.app.electrum_config):
address = req['address']
timestamp = req.get('time', 0)
amount = req.get('amount')
expiration = req.get('exp', None)
status = req.get('status')
signature = req.get('sig')
ci = Factory.RequestItem()
ci.address = req['address']
ci.memo = self.app.wallet.get_label(address)
status = req.get('status')
if status == PR_PAID:
ci.icon = "atlas://gui/kivy/theming/light/confirmed"
elif status == PR_EXPIRED:
ci.icon = "atlas://gui/kivy/theming/light/important"
else:
ci.icon = "atlas://gui/kivy/theming/light/important"
ci.amount = self.app.format_amount_and_units(amount) if amount else ''
ci.date = format_time(timestamp)
ci.screen = self
requests_list.add_widget(ci)
def do_show(self, obj):
self.app.show_request(obj.address)
def do_delete(self, obj):
self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config)
self.update()
class CSpinner(Factory.Spinner): class CSpinner(Factory.Spinner):
'''CustomDropDown that allows fading out the dropdown '''CustomDropDown that allows fading out the dropdown
@ -340,27 +474,20 @@ class TabbedCarousel(Factory.TabbedPanel):
scrlv = self._tab_strip.parent scrlv = self._tab_strip.parent
if not scrlv: if not scrlv:
return return
idx = self.tab_list.index(value) idx = self.tab_list.index(value)
if idx == 0: n = len(self.tab_list)
if idx in [0, 1]:
scroll_x = 1 scroll_x = 1
elif idx == len(self.tab_list) - 1: elif idx in [n-1, n-2]:
scroll_x = 0 scroll_x = 0
else: else:
self_center_x = scrlv.center_x scroll_x = 1. * (n - idx - 1) / (n - 1)
vcenter_x = value.center_x
diff_x = (self_center_x - vcenter_x)
try:
scroll_x = scrlv.scroll_x - (diff_x / scrlv.width)
except ZeroDivisionError:
pass
mation = Factory.Animation(scroll_x=scroll_x, d=.25) mation = Factory.Animation(scroll_x=scroll_x, d=.25)
mation.cancel_all(scrlv) mation.cancel_all(scrlv)
mation.start(scrlv) mation.start(scrlv)
def on_current_tab(self, instance, value): def on_current_tab(self, instance, value):
if value.text == 'default_tab':
return
self.animate_tab_to_center(value) self.animate_tab_to_center(value)
def on_index(self, instance, value): def on_index(self, instance, value):

View File

@ -0,0 +1,12 @@
Popup:
title: "About Electrum"
BoxLayout:
orientation: 'vertical'
spacing: '1dp'
Label:
text: "Lightweight Bitcoin Wallet"
Label:
text: "Author: Thomas Voegtlin"
Label:
text: "https://electrum.org"
Widget

View File

@ -5,23 +5,9 @@
#:set mbtc_symbol unichr(187) #:set mbtc_symbol unichr(187)
<Card@GridLayout>
cols: 1
padding: '12dp' , '22dp', '12dp' , '12dp'
spacing: '12dp'
size_hint: 1, None
height: max(100, self.minimum_height)
canvas.before:
Color:
rgba: 1, 1, 1, 1
BorderImage:
border: 18, 18, 18, 18
source: 'atlas://gui/kivy/theming/light/card'
size: self.size
pos: self.pos
<CardLabel@Label> <CardLabel@Label>
color: 0.45, 0.45, 0.45, 1 color: 0.95, 0.95, 0.95, 1
size_hint: 1, None size_hint: 1, None
text: '' text: ''
text_size: self.width, None text_size: self.width, None
@ -29,45 +15,17 @@
halign: 'left' halign: 'left'
valign: 'top' valign: 'top'
<CardButton@Button>
background_normal: 'atlas://gui/kivy/theming/light/card_btn'
bold: True
font_size: '10sp'
color: 0.699, 0.699, 0.699, 1
size_hint: None, None
size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7)
<HistoryItem@CardItem>
<CardItem@ToggleButtonBehavior+GridLayout>
canvas.before:
Color:
rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0
Rectangle
size: self.size
pos: self.x, self.y + dp(5)
cols: 1
padding: '2dp', '2dp'
spacing: '2dp'
size_hint: 1, None
height: self.minimum_height
group: 'history'
<RecentActivityItem@CardItem>
icon: 'atlas://gui/kivy/theming/light/important' icon: 'atlas://gui/kivy/theming/light/important'
address: 'no address set' message: ''
value: 0 value: 0
amount: app.format_amount(self.value, True) if self.value is not None else '--' amount: app.format_amount(self.value, True) if self.value is not None else '--'
amount_color: '#DB3627' if self.value < 0 else '#2EA442' amount_color: '#FF6657' if self.value < 0 else '#2EA442'
confirmations: 0 confirmations: 0
date: '0/0/0' date: ''
quote_text: '.' quote_text: ''
spacing: '9dp' spacing: '9dp'
on_release:
app.tx_selected(root.tx_hash, self.state)
BoxLayout:
size_hint: 1, None
spacing: '8dp'
height: '32dp'
Image: Image:
id: icon id: icon
source: root.icon source: root.icon
@ -78,38 +36,27 @@
orientation: 'vertical' orientation: 'vertical'
Widget Widget
CardLabel: CardLabel:
shorten: True text: root.date
text: root.address font_size: '14sp'
markup: False
text_size: self.size
CardLabel: CardLabel:
color: .699, .699, .699, 1 color: .699, .699, .699, 1
text: root.date font_size: '13sp'
font_size: '12sp' shorten: True
text: root.message
Widget Widget
CardLabel: CardLabel:
halign: 'right' halign: 'right'
font_size: '13sp' font_size: '15sp'
size_hint: None, 1 size_hint: None, 1
width: '110sp' width: '110sp'
markup: True markup: True
font_name: font_light font_name: font_light
text: text:
u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\ u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\
u'[color=#B2B3B3][size=12sp]{qt}[/size]'\ u'[color=#B2B3B3][size=13sp]{qt}[/size]'\
u'[/color]'.format(amount_color=root.amount_color,\ u'[/color]'.format(amount_color=root.amount_color,\
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
unit=app.base_unit) unit=app.base_unit)
CardSeparator
<CardRecentActivity@Card>
GridLayout:
id: content
spacing: '7dp'
cols: 1
size_hint: 1, None
height: self.minimum_height
CardSeparator
HistoryScreen: HistoryScreen:
@ -119,12 +66,9 @@ HistoryScreen:
id: content id: content
do_scroll_x: False do_scroll_x: False
GridLayout GridLayout
id: grid id: history_container
cols: 1 #if root.width < root.height else 2 cols: 1
size_hint: 1, None size_hint: 1, None
height: self.minimum_height height: self.minimum_height
padding: '12dp' padding: '12dp'
spacing: '12dp' spacing: '2dp'
CardRecentActivity:
id: recent_activity_card

View File

@ -0,0 +1,59 @@
<InvoicesLabel@Label>
#color: .305, .309, .309, 1
text_size: self.width, None
halign: 'left'
valign: 'top'
<InvoiceItem@CardItem>
requestor: ''
memo: ''
amount: ''
status: ''
date: ''
icon: 'atlas://gui/kivy/theming/light/important'
Image:
id: icon
source: root.icon
size_hint: None, 1
width: self.height *.54
mipmap: True
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
InvoicesLabel:
text: root.requestor
shorten: True
InvoicesLabel:
text: root.memo
color: .699, .699, .699, 1
font_size: '13sp'
shorten: True
Widget
InvoicesLabel:
halign: 'right'
font_size: '15sp'
size_hint: None, 1
width: '110sp'
text: root.amount
InvoicesScreen:
name: 'invoices'
BoxLayout:
orientation: 'vertical'
spacing: '1dp'
ScrollView:
canvas.before:
Color:
rgba: .8901, .8901, .8901, 0
Rectangle:
size: self.size
pos: self.pos
GridLayout:
cols: 1
id: invoices_container
size_hint: 1, None
height: self.minimum_height
spacing: '2dp'
padding: '12dp'

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
Popup:
title: _('Plugins')
id: popup
BoxLayout:
orientation: 'vertical'
GridLayout:
cols: 2
size_hint: 1, None
height: '100dp'
id: plugins_list
on_parent:
app.show_plugins(plugins_list)
Widget:
size_hint: 1, 1
BoxLayout:
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('OK')
on_release:
popup.dismiss()

View File

@ -42,7 +42,8 @@ ReceiveScreen:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
opacity: 0.5 if qr.shaded else 1 opacity: 0.5 if qr.shaded else 1
text: s.address text: _('Bitcoin Address') + ': ' + s.address
text_size: self.width, None
SendReceiveBlueBottom: SendReceiveBlueBottom:
id: blue_bottom id: blue_bottom
@ -51,13 +52,15 @@ ReceiveScreen:
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp'
Image: Image:
source: 'atlas://gui/kivy/theming/light/globe' source: 'atlas://gui/kivy/theming/light/globe'
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
AmountButton: BlueButton:
id: amount_label id: amount_label
default_text: 'Amount'
text: s.amount if s.amount else 'Amount' text: s.amount if s.amount else 'Amount'
on_release: app.amount_dialog(s, False) on_release: app.amount_dialog(s, False)
CardSeparator: CardSeparator:
@ -74,12 +77,10 @@ ReceiveScreen:
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
TextInputBlue: BlueButton:
id: message_input id: description
hint_text: 'Description' text: s.message if s.message else _('Description')
text: s.message on_release: app.description_dialog(s)
on_text_validate: s.message = self.text
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
@ -89,9 +90,14 @@ ReceiveScreen:
height: '48dp' height: '48dp'
on_release: s.parent.do_copy() on_release: s.parent.do_copy()
Button: Button:
text: _('Clear') text: _('Save')
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
on_release: s.parent.do_clear() on_release: s.parent.do_save()
Widget: Button:
size_hint: 1, 0.3 text: _('New')
size_hint: 1, None
height: '48dp'
on_release: s.parent.do_new()
#Widget:
# size_hint: 1, 0.3

View File

@ -0,0 +1,60 @@
<RequestLabel@Label>
#color: .305, .309, .309, 1
text_size: self.width, None
halign: 'left'
valign: 'top'
<RequestItem@CardItem>
address: ''
memo: ''
amount: ''
status: ''
date: ''
icon: 'atlas://gui/kivy/theming/light/important'
Image:
id: icon
source: root.icon
size_hint: None, 1
width: self.height *.54
mipmap: True
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
RequestLabel:
text: root.address
shorten: True
RequestLabel:
text: root.memo
color: .699, .699, .699, 1
font_size: '13sp'
shorten: True
Widget
RequestLabel:
halign: 'right'
font_size: '15sp'
size_hint: None, 1
width: '110sp'
text: root.amount
RequestsScreen:
name: 'requests'
BoxLayout:
orientation: 'vertical'
spacing: '1dp'
ScrollView:
canvas.before:
Color:
rgba: .8901, .8901, .8901, 0
Rectangle:
size: self.size
pos: self.pos
GridLayout:
cols: 1
id: requests_container
size_hint_y: None
height: self.minimum_height
spacing: '2dp'
padding: '12dp'

View File

@ -23,6 +23,7 @@ SendScreen:
id: blue_bottom id: blue_bottom
size_hint: 1, None size_hint: 1, None
height: self.minimum_height height: self.minimum_height
spacing: '5dp'
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: blue_bottom.item_height height: blue_bottom.item_height
@ -32,32 +33,32 @@ SendScreen:
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
TextInputBlue: BlueButton:
id: payto_e id: payto_e
text: s.address text: s.address if s.address else _('Recipient')
hint_text: "Recipient" on_release: app.address_dialog(s)
CardSeparator: CardSeparator:
opacity: message_selection.opacity opacity: message_selection.opacity
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp'
Image: Image:
source: 'atlas://gui/kivy/theming/light/globe' source: 'atlas://gui/kivy/theming/light/globe'
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
AmountButton: BlueButton:
id: amount_e id: amount_e
text: s.amount if s.amount else 'Amount' default_text: _('Amount')
on_release: app.amount_dialog(s, True) text: s.amount if s.amount else _('Amount')
on_release: s.amount_dialog()
CardSeparator: CardSeparator:
opacity: message_selection.opacity opacity: message_selection.opacity
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
BoxLayout: BoxLayout:
id: message_selection id: message_selection
opacity: 1
size_hint: 1, None size_hint: 1, None
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp' spacing: '5dp'
@ -66,11 +67,10 @@ SendScreen:
size_hint: None, None size_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
TextInputBlue: BlueButton:
id: message_e id: description
hint_text: 'Description' text: s.message if s.message else _('Description')
text: s.message on_release: app.description_dialog(s)
on_text_validate: s.message = self.text
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
@ -78,7 +78,7 @@ SendScreen:
id: qr id: qr
text: _('QR Code') text: _('QR Code')
on_release: on_release:
app.scan_qr(on_complete=s.parent.set_URI) app.scan_qr(on_complete=app.set_URI)
Button: Button:
id: paste_button id: paste_button
text: _('Paste') text: _('Paste')

View File

@ -1,37 +0,0 @@
Popup:
id: settings
title: _('Settings')
BoxLayout:
orientation: 'vertical'
SettingsItem:
title: _('PIN Code') + ' (%s)'%('ON' if app.wallet.use_encryption else 'OFF')
description: _("Your PIN code will be required in order to spend bitcoins.")
on_release:
app.change_password()
self.title = _('PIN Code') + ' (%s)'%('ON' if app.wallet.use_encryption else 'OFF')
CardSeparator
SettingsItem:
title: _('Denomination') + ' (' + app.base_unit + ')'
description: _("Base unit for Bitcoin amounts.")
on_release:
app._rotate_bu()
self.title = _('Denomination') + ' (' + app.base_unit + ')'
CardSeparator
SettingsItem:
title: _('OpenAlias')
description: "Email-like address."
Widget:
size_hint: 1, 1
BoxLayout:
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('OK')
on_release:
settings.dismiss()

View File

@ -0,0 +1,31 @@
#:import os os
Popup:
title: "Balance"
confirmed: 0
unconfirmed: 0
unmatured: 0
on_parent:
self.confirmed, self.unconfirmed, self.x = app.wallet.get_balance()
BoxLayout:
orientation: 'vertical'
spacing: '1dp'
GridLayout:
cols:2
Label:
text: _("Wallet:")
Label:
text: os.path.basename(app.wallet.storage.path)
Label:
text: _("Confirmed:")
Label:
text: app.format_amount_and_units(root.confirmed)
Label:
text: _("Unconfirmed:")
Label:
text: app.format_amount_and_units(root.unconfirmed)
Label:
text: "Unmatured:"
Label:
text: app.format_amount_and_units(root.unmatured)
Widget

View File

@ -1,35 +0,0 @@
#:import os os
Popup:
title: _('Wallets')
id: popup
BoxLayout:
orientation: 'vertical'
GridLayout:
size_hint_y: None
cols: 2
Label:
height: '32dp'
size_hint_y: None
text: _('Wallet file') + ':'
TextInput:
id: text_input
height: '32dp'
size_hint_y: None
text: os.path.basename(app.wallet.storage.path)
FileChooserIconView:
id: wallet_selector
path: os.path.dirname(app.wallet.storage.path)
on_selection: text_input.text = os.path.basename(self.selection[0]) if self.selection else ''
size_hint: 1, 1
BoxLayout:
Widget:
size_hint: 0.5, None
Button:
size_hint: 0.5, None
height: '48dp'
text: _('OK')
on_release:
popup.dismiss()

View File

@ -64,7 +64,7 @@ class OpenFileEventFilter(QObject):
class ElectrumGui: class ElectrumGui(MessageBoxMixin):
def __init__(self, config, network, plugins): def __init__(self, config, network, plugins):
set_language(config.get('language')) set_language(config.get('language'))
@ -134,7 +134,7 @@ class ElectrumGui:
try: try:
storage = WalletStorage(filename) storage = WalletStorage(filename)
except Exception as e: except Exception as e:
QMessageBox.information(None, _('Error'), str(e), _('OK')) self.show_error(str(e))
return return
if not storage.file_exists: if not storage.file_exists:
recent = self.config.get('recently_open', []) recent = self.config.get('recently_open', [])
@ -147,7 +147,7 @@ class ElectrumGui:
wallet = Wallet(storage) wallet = Wallet(storage)
except BaseException as e: except BaseException as e:
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
QMessageBox.warning(None, _('Warning'), str(e), _('OK')) self.show_warning(str(e))
return return
action = wallet.get_action() action = wallet.get_action()
# run wizard # run wizard
@ -162,32 +162,6 @@ class ElectrumGui:
return wallet return wallet
def get_wallet_folder(self):
#return os.path.dirname(os.path.abspath(self.wallet.storage.path if self.wallet else self.wallet.storage.path))
return os.path.dirname(os.path.abspath(self.config.get_wallet_path()))
def new_wallet(self):
wallet_folder = self.get_wallet_folder()
i = 1
while True:
filename = "wallet_%d"%i
if filename in os.listdir(wallet_folder):
i += 1
else:
break
filename = line_dialog(None, _('New Wallet'), _('Enter file name') + ':', _('OK'), filename)
if not filename:
return
full_path = os.path.join(wallet_folder, filename)
storage = WalletStorage(full_path)
if storage.file_exists:
QMessageBox.critical(None, "Error", _("File exists"))
return
wizard = InstallWizard(self.app, self.config, self.network, storage)
wallet = wizard.run('new')
if wallet:
self.new_window(full_path)
def new_window(self, path, uri=None): def new_window(self, path, uri=None):
# Use a signal as can be called from daemon thread # Use a signal as can be called from daemon thread
self.app.emit(SIGNAL('new_window'), path, uri) self.app.emit(SIGNAL('new_window'), path, uri)
@ -245,6 +219,9 @@ class ElectrumGui:
# main loop # main loop
self.app.exec_() self.app.exec_()
# Shut down the timer cleanly
self.timer.stop()
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html # clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
event = QtCore.QEvent(QtCore.QEvent.Clipboard) event = QtCore.QEvent(QtCore.QEvent.Clipboard)
self.app.sendEvent(self.app.clipboard(), event) self.app.sendEvent(self.app.clipboard(), event)

View File

@ -25,9 +25,10 @@ from PyQt4.QtCore import *
from util import * from util import *
from history_widget import HistoryWidget from history_widget import HistoryWidget
class AddressDialog(QDialog): class AddressDialog(WindowModalDialog):
def __init__(self, address, parent): def __init__(self, parent, address):
WindowModalDialog.__init__(self, parent, _("Address"))
self.address = address self.address = address
self.parent = parent self.parent = parent
self.config = parent.config self.config = parent.config
@ -35,10 +36,7 @@ class AddressDialog(QDialog):
self.app = parent.app self.app = parent.app
self.saved = True self.saved = True
QDialog.__init__(self)
self.setMinimumWidth(700) self.setMinimumWidth(700)
self.setWindowTitle(_("Address"))
self.setModal(1)
vbox = QVBoxLayout() vbox = QVBoxLayout()
self.setLayout(vbox) self.setLayout(vbox)

View File

@ -74,7 +74,7 @@ class HistoryWidget(MyTreeWidget):
icon, time_str = self.get_icon(conf, timestamp) icon, time_str = self.get_icon(conf, timestamp)
v_str = self.parent.format_amount(value, True, whitespaces=True) v_str = self.parent.format_amount(value, True, whitespaces=True)
balance_str = self.parent.format_amount(balance, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True)
label, is_default_label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
entry = ['', tx_hash, time_str, label, v_str, balance_str] entry = ['', tx_hash, time_str, label, v_str, balance_str]
run_hook('history_tab_update', tx, entry) run_hook('history_tab_update', tx, entry)
item = QTreeWidgetItem(entry) item = QTreeWidgetItem(entry)
@ -88,8 +88,6 @@ class HistoryWidget(MyTreeWidget):
item.setForeground(4, QBrush(QColor("#BC1E1E"))) item.setForeground(4, QBrush(QColor("#BC1E1E")))
if tx_hash: if tx_hash:
item.setData(0, Qt.UserRole, tx_hash) item.setData(0, Qt.UserRole, tx_hash)
if is_default_label:
item.setForeground(3, QBrush(QColor('grey')))
self.insertTopLevelItem(0, item) self.insertTopLevelItem(0, item)
if current_tx == tx_hash: if current_tx == tx_hash:
self.setCurrentItem(item) self.setCurrentItem(item)

View File

@ -62,17 +62,17 @@ class CosignWidget(QWidget):
class InstallWizard(QDialog): class InstallWizard(WindowModalDialog, MessageBoxMixin):
def __init__(self, app, config, network, storage): def __init__(self, app, config, network, storage):
QDialog.__init__(self) title = 'Electrum' + ' - ' + _('Install Wizard')
WindowModalDialog.__init__(self, None, title=title)
self.app = app self.app = app
self.config = config self.config = config
self.network = network self.network = network
self.storage = storage self.storage = storage
self.setMinimumSize(575, 400) self.setMinimumSize(575, 400)
self.setMaximumSize(575, 400) self.setMaximumSize(575, 400)
self.setWindowTitle('Electrum' + ' - ' + _('Install Wizard'))
self.connect(self, QtCore.SIGNAL('accept'), self.accept) self.connect(self, QtCore.SIGNAL('accept'), self.accept)
self.stack = QStackedLayout() self.stack = QStackedLayout()
self.setLayout(self.stack) self.setLayout(self.stack)
@ -139,8 +139,8 @@ class InstallWizard(QDialog):
button.setChecked(True) button.setChecked(True)
vbox.addStretch(1) vbox.addStretch(1)
self.set_layout(vbox)
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next')))) vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next'))))
self.set_layout(vbox)
self.show() self.show()
self.raise_() self.raise_()
@ -157,7 +157,7 @@ class InstallWizard(QDialog):
if not r: if not r:
return return
if prepare_seed(r) != prepare_seed(seed): if prepare_seed(r) != prepare_seed(seed):
QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK')) self.show_error(_('Incorrect seed'))
return False return False
else: else:
return True return True
@ -384,22 +384,6 @@ class InstallWizard(QDialog):
wallet_type = '%dof%d'%(m,n) wallet_type = '%dof%d'%(m,n)
return wallet_type return wallet_type
def question(self, msg, yes_label=_('OK'), no_label=_('Cancel'), icon=None):
vbox = QVBoxLayout()
self.set_layout(vbox)
if icon:
logo = QLabel()
logo.setPixmap(icon)
vbox.addWidget(logo)
label = QLabel(msg)
label.setWordWrap(True)
vbox.addWidget(label)
vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self, no_label), OkButton(self, yes_label)))
if not self.exec_():
return None
return True
def show_seed(self, seed, sid): def show_seed(self, seed, sid):
vbox = seed_dialog.show_seed_box_msg(seed, sid) vbox = seed_dialog.show_seed_box_msg(seed, sid)
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _("Next")))) vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _("Next"))))
@ -407,22 +391,21 @@ class InstallWizard(QDialog):
return self.exec_() return self.exec_()
def password_dialog(self): def password_dialog(self):
msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\ from password_dialog import PasswordDialog
+_("Leave these fields empty if you want to disable encryption.") msg = _("Please choose a password to encrypt your wallet keys.\n"
from password_dialog import make_password_dialog, run_password_dialog "Leave these fields empty if you want to disable encryption.")
self.set_layout( make_password_dialog(self, None, msg) ) dialog = PasswordDialog(self, None, _("Choose a password"), msg, True)
return run_password_dialog(self, None, self)[2] return dialog.run()[2]
def run(self, action): def run(self, action):
if self.storage.file_exists and action != 'new': if self.storage.file_exists and action != 'new':
path = self.storage.path path = self.storage.path
msg = _("The file '%s' contains an incompletely created wallet.\n" msg = _("The file '%s' contains an incompletely created wallet.\n"
"Do you want to complete its creation now?") % path "Do you want to complete its creation now?") % path
if not question(msg): if not self.question(msg):
if question(_("Do you want to delete '%s'?") % path): if self.question(_("Do you want to delete '%s'?") % path):
os.remove(path) os.remove(path)
QMessageBox.information(self, _('Warning'), self.show_warning(_('The file was removed'))
_('The file was removed'), _('OK'))
return return
return return
self.show() self.show()
@ -434,7 +417,7 @@ class InstallWizard(QDialog):
wallet = self.run_wallet_type(action, wallet_type) wallet = self.run_wallet_type(action, wallet_type)
except BaseException as e: except BaseException as e:
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e), _('OK')) self.show_error(str(e))
return return
return wallet return wallet
@ -463,7 +446,7 @@ class InstallWizard(QDialog):
elif wallet_type == 'twofactor': elif wallet_type == 'twofactor':
wallet_type = '2fa' wallet_type = '2fa'
if action == 'create': if action == 'create':
self.storage.put('wallet_type', wallet_type, False) self.storage.put('wallet_type', wallet_type)
if action is None: if action is None:
return return
@ -527,7 +510,7 @@ class InstallWizard(QDialog):
if self.config.get('server') is None: if self.config.get('server') is None:
self.network_dialog() self.network_dialog()
else: else:
QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK')) self.show_warning(_('You are offline'))
# start wallet threads # start wallet threads
@ -539,7 +522,7 @@ class InstallWizard(QDialog):
msg = _("Recovery successful") if wallet.is_found() else _("No transactions found for this seed") msg = _("Recovery successful") if wallet.is_found() else _("No transactions found for this seed")
else: else:
msg = _("This wallet was restored offline. It may contain more addresses than displayed.") msg = _("This wallet was restored offline. It may contain more addresses than displayed.")
QMessageBox.information(None, _('Information'), msg, _('OK')) self.show_message(msg)
return wallet return wallet
@ -560,7 +543,7 @@ class InstallWizard(QDialog):
password = self.password_dialog() if any(map(lambda x: Wallet.is_seed(x) or Wallet.is_xprv(x), key_list)) else None password = self.password_dialog() if any(map(lambda x: Wallet.is_seed(x) or Wallet.is_xprv(x), key_list)) else None
wallet = Wallet.from_multisig(key_list, password, self.storage, t) wallet = Wallet.from_multisig(key_list, password, self.storage, t)
else: else:
self.storage.put('wallet_type', t, False) self.storage.put('wallet_type', t)
# call the constructor to load the plugin (side effect) # call the constructor to load the plugin (side effect)
Wallet(self.storage) Wallet(self.storage)
wallet = always_hook('installwizard_restore', self, self.storage) wallet = always_hook('installwizard_restore', self, self.storage)

View File

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys, time, threading import sys, time, threading
import os.path, json, traceback import os, json, traceback
import shutil import shutil
import socket import socket
import weakref import weakref
@ -40,20 +40,17 @@ from electrum.i18n import _
from electrum.util import block_explorer, block_explorer_info, block_explorer_URL from electrum.util import block_explorer, block_explorer_info, block_explorer_URL
from electrum.util import format_satoshis, format_satoshis_plain, format_time from electrum.util import format_satoshis, format_satoshis_plain, format_time
from electrum.util import PrintError, NotEnoughFunds, StoreDict from electrum.util import PrintError, NotEnoughFunds, StoreDict
from electrum import Transaction from electrum import Transaction, mnemonic
from electrum import mnemonic
from electrum import util, bitcoin, commands, Wallet from electrum import util, bitcoin, commands, Wallet
from electrum import SimpleConfig, COIN_CHOOSERS, WalletStorage from electrum import SimpleConfig, COIN_CHOOSERS, WalletStorage
from electrum import Imported_Wallet from electrum import Imported_Wallet, paymentrequest
from electrum import paymentrequest
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
from network_dialog import NetworkDialog from network_dialog import NetworkDialog
from qrcodewidget import QRCodeWidget, QRDialog from qrcodewidget import QRCodeWidget, QRDialog
from qrtextedit import ScanQRTextEdit, ShowQRTextEdit from qrtextedit import ShowQRTextEdit
from transaction_dialog import show_transaction from transaction_dialog import show_transaction
from installwizard import InstallWizard
@ -83,7 +80,6 @@ class StatusBarButton(QPushButton):
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from electrum.paymentrequest import PaymentRequest, get_payment_request
pr_icons = { pr_icons = {
PR_UNPAID:":icons/unpaid.png", PR_UNPAID:":icons/unpaid.png",
@ -106,7 +102,7 @@ expiration_values = [
class ElectrumWindow(QMainWindow, PrintError): class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def __init__(self, gui_object, wallet): def __init__(self, gui_object, wallet):
QMainWindow.__init__(self) QMainWindow.__init__(self)
@ -270,7 +266,7 @@ class ElectrumWindow(QMainWindow, PrintError):
self.update_account_selector() self.update_account_selector()
# update menus # update menus
self.new_account_menu.setVisible(self.wallet.can_create_accounts()) self.new_account_menu.setVisible(self.wallet.can_create_accounts())
self.private_keys_menu.setEnabled(not self.wallet.is_watching_only()) self.export_menu.setEnabled(not self.wallet.is_watching_only())
self.password_menu.setEnabled(self.wallet.can_change_password()) self.password_menu.setEnabled(self.wallet.can_change_password())
self.seed_menu.setEnabled(self.wallet.has_seed()) self.seed_menu.setEnabled(self.wallet.has_seed())
self.mpk_menu.setEnabled(self.wallet.is_deterministic()) self.mpk_menu.setEnabled(self.wallet.is_deterministic())
@ -288,14 +284,17 @@ class ElectrumWindow(QMainWindow, PrintError):
except: except:
self.setGeometry(100, 100, 840, 400) self.setGeometry(100, 100, 840, 400)
self.show() self.show()
self.warn_if_watching_only()
run_hook('load_wallet', wallet, self)
def warn_if_watching_only(self):
if self.wallet.is_watching_only(): if self.wallet.is_watching_only():
msg = ' '.join([ msg = ' '.join([
_("This wallet is watching-only."), _("This wallet is watching-only."),
_("This means you will not be able to spend Bitcoins with it."), _("This means you will not be able to spend Bitcoins with it."),
_("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.") _("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.")
]) ])
QMessageBox.warning(self, _('Information'), msg, _('OK')) self.show_warning(msg, title=_('Information'))
run_hook('load_wallet', wallet, self)
def import_old_contacts(self): def import_old_contacts(self):
# backward compatibility: import contacts # backward compatibility: import contacts
@ -321,7 +320,7 @@ class ElectrumWindow(QMainWindow, PrintError):
self.wallet.synchronize() self.wallet.synchronize()
def open_wallet(self): def open_wallet(self):
wallet_folder = self.gui_object.get_wallet_folder() wallet_folder = self.get_wallet_folder()
filename = unicode(QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder)) filename = unicode(QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder))
if not filename: if not filename:
return return
@ -339,11 +338,9 @@ class ElectrumWindow(QMainWindow, PrintError):
if new_path != path: if new_path != path:
try: try:
shutil.copy2(path, new_path) shutil.copy2(path, new_path)
QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path)) self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
except (IOError, os.error), reason: except (IOError, os.error), reason:
QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason)) self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
def update_recently_visited(self, filename=None): def update_recently_visited(self, filename=None):
recent = self.config.get('recently_open', []) recent = self.config.get('recently_open', [])
@ -361,13 +358,39 @@ class ElectrumWindow(QMainWindow, PrintError):
self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1))) self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1)))
self.recently_visited_menu.setEnabled(len(recent)) self.recently_visited_menu.setEnabled(len(recent))
def get_wallet_folder(self):
return os.path.dirname(os.path.abspath(self.config.get_wallet_path()))
def new_wallet(self):
wallet_folder = self.get_wallet_folder()
i = 1
while True:
filename = "wallet_%d" % i
if filename in os.listdir(wallet_folder):
i += 1
else:
break
filename = line_dialog(self, _('New Wallet'), _('Enter file name')
+ ':', _('OK'), filename)
if not filename:
return
full_path = os.path.join(wallet_folder, filename)
storage = WalletStorage(full_path)
if storage.file_exists:
self.show_critical(_("File exists"))
return
wizard = InstallWizard(self.app, self.config, self.network, storage)
wallet = wizard.run('new')
if wallet:
self.new_window(full_path)
def init_menubar(self): def init_menubar(self):
menubar = QMenuBar() menubar = QMenuBar()
file_menu = menubar.addMenu(_("&File")) file_menu = menubar.addMenu(_("&File"))
self.recently_visited_menu = file_menu.addMenu(_("&Recently open")) self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open) file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
file_menu.addAction(_("&New/Restore"), self.gui_object.new_wallet).setShortcut(QKeySequence.New) file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs) file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction(_("&Quit"), self.close) file_menu.addAction(_("&Quit"), self.close)
@ -435,7 +458,7 @@ class ElectrumWindow(QMainWindow, PrintError):
_("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."), _("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
_("Try to explain not only what the bug is, but how it occurs.") _("Try to explain not only what the bug is, but how it occurs.")
]) ])
QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"), msg) self.show_message(msg, title="Electrum - " + _("Reporting Bugs"))
def notify_transactions(self): def notify_transactions(self):
if not self.network or not self.network.is_connected(): if not self.network or not self.network.is_connected():
@ -582,7 +605,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def show_address(self, addr): def show_address(self, addr):
import address_dialog import address_dialog
d = address_dialog.AddressDialog(addr, self) d = address_dialog.AddressDialog(self, addr)
d.exec_() d.exec_()
def show_transaction(self, tx, tx_desc = None): def show_transaction(self, tx, tx_desc = None):
@ -744,7 +767,7 @@ class ElectrumWindow(QMainWindow, PrintError):
try: try:
self.wallet.sign_payment_request(addr, alias, alias_addr, password) self.wallet.sign_payment_request(addr, alias, alias_addr, password)
except Exception as e: except Exception as e:
QMessageBox.warning(self, _('Error'), str(e), _('OK')) self.show_error(str(e))
return return
else: else:
return return
@ -757,7 +780,7 @@ class ElectrumWindow(QMainWindow, PrintError):
amount = self.receive_amount_e.get_amount() amount = self.receive_amount_e.get_amount()
message = unicode(self.receive_message_e.text()) message = unicode(self.receive_message_e.text())
if not message and not amount: if not message and not amount:
QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK')) self.show_error(_('No message or amount'))
return False return False
i = self.expires_combo.currentIndex() i = self.expires_combo.currentIndex()
expiration = map(lambda x: x[1], expiration_values)[i] expiration = map(lambda x: x[1], expiration_values)[i]
@ -769,8 +792,7 @@ class ElectrumWindow(QMainWindow, PrintError):
self.save_request_button.setEnabled(False) self.save_request_button.setEnabled(False)
def view_and_paste(self, title, msg, data): def view_and_paste(self, title, msg, data):
dialog = QDialog(self) dialog = WindowModalDialog(self, title)
dialog.setWindowTitle(title)
vbox = QVBoxLayout() vbox = QVBoxLayout()
label = QLabel(msg) label = QLabel(msg)
label.setWordWrap(True) label.setWordWrap(True)
@ -1129,7 +1151,7 @@ class ElectrumWindow(QMainWindow, PrintError):
self.wallet.check_password(password) self.wallet.check_password(password)
break break
except Exception as e: except Exception as e:
QMessageBox.warning(parent, _('Error'), str(e), _('OK')) self.show_error(str(e), parent=parent)
continue continue
else: else:
password = None password = None
@ -1140,7 +1162,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def read_send_tab(self): def read_send_tab(self):
if self.payment_request and self.payment_request.has_expired(): if self.payment_request and self.payment_request.has_expired():
QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK')) self.show_error(_('Payment request has expired'))
return return
label = unicode( self.message_e.text() ) label = unicode( self.message_e.text() )
@ -1161,23 +1183,23 @@ class ElectrumWindow(QMainWindow, PrintError):
return return
if not outputs: if not outputs:
QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK')) self.show_error(_('No outputs'))
return return
for _type, addr, amount in outputs: for _type, addr, amount in outputs:
if addr is None: if addr is None:
QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK')) self.show_error(_('Bitcoin Address is None'))
return return
if _type == 'address' and not bitcoin.is_address(addr): if _type == 'address' and not bitcoin.is_address(addr):
QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK')) self.show_error(_('Invalid Bitcoin Address'))
return return
if amount is None: if amount is None:
QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK')) self.show_error(_('Invalid Amount'))
return return
fee = self.fee_e.get_amount() fee = self.fee_e.get_amount()
if fee is None: if fee is None:
QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK')) self.show_error(_('Invalid Fee'))
return return
coins = self.get_coins() coins = self.get_coins()
@ -1203,7 +1225,7 @@ class ElectrumWindow(QMainWindow, PrintError):
return return
if tx.get_fee() < MIN_RELAY_TX_FEE and tx.requires_fee(self.wallet): if tx.get_fee() < MIN_RELAY_TX_FEE and tx.requires_fee(self.wallet):
QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK')) self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network"))
return return
if self.show_before_broadcast(): if self.show_before_broadcast():
@ -1258,16 +1280,14 @@ class ElectrumWindow(QMainWindow, PrintError):
def sign_thread(): def sign_thread():
if not self.wallet.is_watching_only(): if not self.wallet.is_watching_only():
self.wallet.sign_transaction(tx, password) self.wallet.sign_transaction(tx, password)
def on_sign_successful(ret): def on_signed(ret):
success[0] = True success[0] = True
def on_dialog_close(): def on_finished():
self.send_button.setDisabled(False) self.send_button.setDisabled(False)
callback(success[0]) callback(success[0])
# keep a reference to WaitingDialog or the gui might crash WaitingDialog(parent, _('Signing transaction...'), sign_thread,
self.waiting_dialog = WaitingDialog(parent, 'Signing transaction...', sign_thread, on_sign_successful, on_dialog_close) on_success=on_signed, on_finished=on_finished)
self.waiting_dialog.start()
def broadcast_transaction(self, tx, tx_desc, parent=None): def broadcast_transaction(self, tx, tx_desc, parent=None):
@ -1296,19 +1316,16 @@ class ElectrumWindow(QMainWindow, PrintError):
if status: if status:
if tx_desc is not None and tx.is_complete(): if tx_desc is not None and tx.is_complete():
self.wallet.set_label(tx.hash(), tx_desc) self.wallet.set_label(tx.hash(), tx_desc)
QMessageBox.information(parent, '', _('Payment sent.') + '\n' + msg, _('OK')) self.show_message(_('Payment sent.') + '\n' + msg, parent=parent)
self.invoices_list.update() self.invoices_list.update()
self.do_clear() self.do_clear()
else: else:
QMessageBox.warning(parent, _('Error'), msg, _('OK')) self.show_error(msg, parent=parent)
self.send_button.setDisabled(False) self.send_button.setDisabled(False)
if parent == None: parent = parent or self
parent = self WaitingDialog(parent, _('Broadcasting transaction...'),
self.waiting_dialog = WaitingDialog(parent, 'Broadcasting transaction...', broadcast_thread, broadcast_done) broadcast_thread, broadcast_done)
self.waiting_dialog.start()
def prepare_for_payment_request(self): def prepare_for_payment_request(self):
self.tabs.setCurrentIndex(1) self.tabs.setCurrentIndex(1)
@ -1346,37 +1363,28 @@ class ElectrumWindow(QMainWindow, PrintError):
self.payment_request = None self.payment_request = None
self.do_clear() self.do_clear()
def pay_to_URI(self, URI): def on_pr(self, request):
if not URI: self.payment_request = request
return
try:
out = util.parse_URI(unicode(URI))
except Exception as e:
QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
return
self.tabs.setCurrentIndex(1)
r = out.get('r')
sig = out.get('sig')
name = out.get('name')
if r or (name and sig):
def get_payment_request_thread():
if name and sig:
from electrum import paymentrequest
pr = paymentrequest.serialize_request(out).SerializeToString()
self.payment_request = paymentrequest.PaymentRequest(pr)
else:
self.payment_request = get_payment_request(r)
if self.payment_request.verify(self.contacts): if self.payment_request.verify(self.contacts):
self.emit(SIGNAL('payment_request_ok')) self.emit(SIGNAL('payment_request_ok'))
else: else:
self.emit(SIGNAL('payment_request_error')) self.emit(SIGNAL('payment_request_error'))
t = threading.Thread(target=get_payment_request_thread)
t.setDaemon(True) def pay_to_URI(self, URI):
t.start() if not URI:
return
try:
out = util.parse_URI(unicode(URI), self.on_pr)
except BaseException as e:
self.show_error(_('Invalid bitcoin URI:') + '\n' + str(e))
return
self.tabs.setCurrentIndex(1)
r = out.get('r')
sig = out.get('sig')
name = out.get('name')
if r or (name and sig):
self.prepare_for_payment_request() self.prepare_for_payment_request()
return return
address = out.get('address') address = out.get('address')
amount = out.get('amount') amount = out.get('amount')
label = out.get('label') label = out.get('label')
@ -1555,6 +1563,13 @@ class ElectrumWindow(QMainWindow, PrintError):
def paytomany(self): def paytomany(self):
self.tabs.setCurrentIndex(1) self.tabs.setCurrentIndex(1)
self.payto_e.paytomany() self.payto_e.paytomany()
msg = '\n'.join([
_('Enter a list of outputs in the \'Pay to\' field.'),
_('One output per line.'),
_('Format: address, amount'),
_('You may load a CSV file using the file icon.')
])
self.show_warning(msg, title=_('Pay to many'))
def payto_contacts(self, labels): def payto_contacts(self, labels):
paytos = [self.get_contact_payto(label) for label in labels] paytos = [self.get_contact_payto(label) for label in labels]
@ -1578,7 +1593,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def set_contact(self, label, address): def set_contact(self, label, address):
if not is_valid(address): if not is_valid(address):
QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK')) self.show_error(_('Invalid Address'))
self.contacts_list.update() # Displays original unchanged value self.contacts_list.update() # Displays original unchanged value
return False return False
self.contacts[label] = ('address', address) self.contacts[label] = ('address', address)
@ -1628,8 +1643,7 @@ class ElectrumWindow(QMainWindow, PrintError):
self.show_pr_details(pr) self.show_pr_details(pr)
def show_pr_details(self, pr): def show_pr_details(self, pr):
d = QDialog(self) d = WindowModalDialog(self, _("Invoice"))
d.setWindowTitle(_("Invoice"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
grid = QGridLayout() grid = QGridLayout()
grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0) grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
@ -1840,8 +1854,39 @@ class ElectrumWindow(QMainWindow, PrintError):
def change_password_dialog(self): def change_password_dialog(self):
from password_dialog import PasswordDialog from password_dialog import PasswordDialog
d = PasswordDialog(self.wallet, self)
d.run() if self.wallet and self.wallet.is_watching_only():
self.show_error(_('This is a watching-only wallet'))
return
msg = (_('Your wallet is encrypted. Use this dialog to change your '
'password. To disable wallet encryption, enter an empty new '
'password.') if self.wallet.use_encryption
else _('Your wallet keys are not encrypted'))
d = PasswordDialog(self, self.wallet, _("Set Password"), msg, True)
ok, password, new_password = d.run()
if not ok:
return
try:
self.wallet.check_password(password)
except BaseException as e:
self.show_error(str(e))
return
try:
self.wallet.update_password(password, new_password)
except:
traceback.print_exc(file=sys.stdout)
self.show_error(_('Failed to update password'))
return
if new_password:
msg = _('Password was updated successfully')
else:
msg = _('This wallet is not encrypted')
self.show_message(msg, title=_("Success"))
self.update_lock_icon() self.update_lock_icon()
def toggle_search(self): def toggle_search(self):
@ -1866,8 +1911,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def new_contact_dialog(self): def new_contact_dialog(self):
d = QDialog(self) d = WindowModalDialog(self, _("New Contact"))
d.setWindowTitle(_("New Contact"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
vbox.addWidget(QLabel(_('New Contact') + ':')) vbox.addWidget(QLabel(_('New Contact') + ':'))
grid = QGridLayout() grid = QGridLayout()
@ -1892,9 +1936,7 @@ class ElectrumWindow(QMainWindow, PrintError):
@protected @protected
def new_account_dialog(self, password): def new_account_dialog(self, password):
dialog = QDialog(self) dialog = WindowModalDialog(self, _("New Account"))
dialog.setModal(1)
dialog.setWindowTitle(_("New Account"))
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget(QLabel(_('Account name')+':')) vbox.addWidget(QLabel(_('Account name')+':'))
e = QLineEdit() e = QLineEdit()
@ -1917,11 +1959,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def show_master_public_keys(self): def show_master_public_keys(self):
dialog = WindowModalDialog(self, "Master Public Keys")
dialog = QDialog(self)
dialog.setModal(1)
dialog.setWindowTitle(_("Master Public Keys"))
mpk_dict = self.wallet.get_master_public_keys() mpk_dict = self.wallet.get_master_public_keys()
vbox = QVBoxLayout() vbox = QVBoxLayout()
# only show the combobox in case multiple accounts are available # only show the combobox in case multiple accounts are available
@ -1966,13 +2004,13 @@ class ElectrumWindow(QMainWindow, PrintError):
@protected @protected
def show_seed_dialog(self, password): def show_seed_dialog(self, password):
if not self.wallet.has_seed(): if not self.wallet.has_seed():
QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK')) self.show_message(_('This wallet has no seed'))
return return
try: try:
mnemonic = self.wallet.get_mnemonic(password) mnemonic = self.wallet.get_mnemonic(password)
except BaseException as e: except BaseException as e:
QMessageBox.warning(self, _('Error'), str(e), _('OK')) self.show_error(str(e))
return return
from seed_dialog import SeedDialog from seed_dialog import SeedDialog
d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys()) d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
@ -1980,10 +2018,10 @@ class ElectrumWindow(QMainWindow, PrintError):
def show_qrcode(self, data, title = _("QR code")): def show_qrcode(self, data, title = _("QR code"), parent=None):
if not data: if not data:
return return
d = QRDialog(data, self, title) d = QRDialog(data, parent or self, title)
d.exec_() d.exec_()
def show_public_keys(self, address): def show_public_keys(self, address):
@ -1995,10 +2033,8 @@ class ElectrumWindow(QMainWindow, PrintError):
self.show_message(str(e)) self.show_message(str(e))
return return
d = QDialog(self) d = WindowModalDialog(self, _("Public key"))
d.setMinimumSize(600, 200) d.setMinimumSize(600, 200)
d.setModal(1)
d.setWindowTitle(_("Public key"))
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget( QLabel(_("Address") + ': ' + address)) vbox.addWidget( QLabel(_("Address") + ': ' + address))
vbox.addWidget( QLabel(_("Public key") + ':')) vbox.addWidget( QLabel(_("Public key") + ':'))
@ -2019,10 +2055,8 @@ class ElectrumWindow(QMainWindow, PrintError):
self.show_message(str(e)) self.show_message(str(e))
return return
d = QDialog(self) d = WindowModalDialog(self, _("Private key"))
d.setMinimumSize(600, 200) d.setMinimumSize(600, 200)
d.setModal(1)
d.setWindowTitle(_("Private key"))
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget( QLabel(_("Address") + ': ' + address)) vbox.addWidget( QLabel(_("Address") + ': ' + address))
vbox.addWidget( QLabel(_("Private key") + ':')) vbox.addWidget( QLabel(_("Private key") + ':'))
@ -2056,9 +2090,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def sign_verify_message(self, address=''): def sign_verify_message(self, address=''):
d = QDialog(self) d = WindowModalDialog(self, _('Sign/verify Message'))
d.setModal(1)
d.setWindowTitle(_('Sign/verify Message'))
d.setMinimumSize(410, 290) d.setMinimumSize(410, 290)
layout = QGridLayout(d) layout = QGridLayout(d)
@ -2117,9 +2149,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def encrypt_message(self, address = ''): def encrypt_message(self, address = ''):
d = QDialog(self) d = WindowModalDialog(self, _('Encrypt/decrypt Message'))
d.setModal(1)
d.setWindowTitle(_('Encrypt/decrypt Message'))
d.setMinimumSize(610, 490) d.setMinimumSize(610, 490)
layout = QGridLayout(d) layout = QGridLayout(d)
@ -2157,22 +2187,9 @@ class ElectrumWindow(QMainWindow, PrintError):
layout.addLayout(hbox, 4, 1) layout.addLayout(hbox, 4, 1)
d.exec_() d.exec_()
def question(self, msg):
return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
def show_message(self, msg):
QMessageBox.information(self, _('Message'), msg, _('OK'))
def show_warning(self, msg):
QMessageBox.warning(self, _('Warning'), msg, _('OK'))
def password_dialog(self, msg=None, parent=None): def password_dialog(self, msg=None, parent=None):
if parent == None: parent = parent or self
parent = self d = WindowModalDialog(parent, _("Enter Password"))
d = QDialog(parent)
d.setModal(1)
d.setWindowTitle(_("Enter Password"))
pw = QLineEdit() pw = QLineEdit()
pw.setEchoMode(2) pw.setEchoMode(2)
vbox = QVBoxLayout() vbox = QVBoxLayout()
@ -2200,33 +2217,24 @@ class ElectrumWindow(QMainWindow, PrintError):
except: except:
is_hex = False is_hex = False
try:
if is_hex: if is_hex:
try:
return Transaction(txt) return Transaction(txt)
except:
traceback.print_exc(file=sys.stdout)
QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
return
try:
tx_dict = json.loads(str(txt)) tx_dict = json.loads(str(txt))
assert "hex" in tx_dict.keys() assert "hex" in tx_dict.keys()
tx = Transaction(tx_dict["hex"]) tx = Transaction(tx_dict["hex"])
#if tx_dict.has_key("input_info"):
# input_info = json.loads(tx_dict['input_info'])
# tx.add_input_info(input_info)
return tx return tx
except Exception: except:
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction")) self.show_critical(_("Electrum was unable to parse your transaction"))
return
def read_tx_from_qrcode(self): def read_tx_from_qrcode(self):
from electrum import qrscanner from electrum import qrscanner
try: try:
data = qrscanner.scan_qr(self.config) data = qrscanner.scan_qr(self.config)
except BaseException, e: except BaseException as e:
QMessageBox.warning(self, _('Error'), _(e), _('OK')) self.show_error(str(e))
return return
if not data: if not data:
return return
@ -2252,8 +2260,8 @@ class ElectrumWindow(QMainWindow, PrintError):
try: try:
with open(fileName, "r") as f: with open(fileName, "r") as f:
file_content = f.read() file_content = f.read()
except (ValueError, IOError, os.error), reason: except (ValueError, IOError, os.error) as reason:
QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason)) self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), title=_("Unable to read file or no transaction found"))
return self.tx_from_text(file_content) return self.tx_from_text(file_content)
def do_process_from_text(self): def do_process_from_text(self):
@ -2292,11 +2300,10 @@ class ElectrumWindow(QMainWindow, PrintError):
try: try:
self.wallet.check_password(password) self.wallet.check_password(password)
except Exception as e: except Exception as e:
QMessageBox.warning(self, _('Error'), str(e), _('OK')) self.show_error(str(e))
return return
d = QDialog(self) d = WindowModalDialog(self, _('Private keys'))
d.setWindowTitle(_('Private keys'))
d.setMinimumSize(850, 300) d.setMinimumSize(850, 300)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
@ -2349,9 +2356,12 @@ class ElectrumWindow(QMainWindow, PrintError):
try: try:
self.do_export_privkeys(filename, private_keys, csv_button.isChecked()) self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
except (IOError, os.error), reason: except (IOError, os.error) as reason:
export_error_label = _("Electrum was unable to produce a private key-export.") txt = "\n".join([
QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason)) _("Electrum was unable to produce a private key-export."),
str(reason)
])
self.show_critical(txt, title=_("Unable to create csv"))
except Exception as e: except Exception as e:
self.show_message(str(e)) self.show_message(str(e))
@ -2381,9 +2391,9 @@ class ElectrumWindow(QMainWindow, PrintError):
f.close() f.close()
for key, value in json.loads(data).items(): for key, value in json.loads(data).items():
self.wallet.set_label(key, value) self.wallet.set_label(key, value)
QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile)) self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
except (IOError, os.error), reason: except (IOError, os.error) as reason:
QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason)) self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
def do_export_labels(self): def do_export_labels(self):
@ -2393,14 +2403,13 @@ class ElectrumWindow(QMainWindow, PrintError):
if fileName: if fileName:
with open(fileName, 'w+') as f: with open(fileName, 'w+') as f:
json.dump(labels, f) json.dump(labels, f)
QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName)) self.show_message(_("Your labels where exported to") + " '%s'" % str(fileName))
except (IOError, os.error), reason: except (IOError, os.error), reason:
QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason)) self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
def export_history_dialog(self): def export_history_dialog(self):
d = QDialog(self) d = WindowModalDialog(self, _('Export History'))
d.setWindowTitle(_('Export History'))
d.setMinimumSize(400, 200) d.setMinimumSize(400, 200)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
defaultname = os.path.expanduser('~/electrum-history.csv') defaultname = os.path.expanduser('~/electrum-history.csv')
@ -2421,9 +2430,9 @@ class ElectrumWindow(QMainWindow, PrintError):
self.do_export_history(self.wallet, filename, csv_button.isChecked()) self.do_export_history(self.wallet, filename, csv_button.isChecked())
except (IOError, os.error), reason: except (IOError, os.error), reason:
export_error_label = _("Electrum was unable to produce a transaction export.") export_error_label = _("Electrum was unable to produce a transaction export.")
QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason)) self.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
return return
QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported.")) self.show_message(_("Your wallet history has been successfully exported."))
def do_export_history(self, wallet, fileName, is_csv): def do_export_history(self, wallet, fileName, is_csv):
@ -2445,7 +2454,7 @@ class ElectrumWindow(QMainWindow, PrintError):
value_string = '--' value_string = '--'
if tx_hash: if tx_hash:
label, is_default_label = wallet.get_label(tx_hash) label = wallet.get_label(tx_hash)
label = label.encode('utf-8') label = label.encode('utf-8')
else: else:
label = "" label = ""
@ -2467,12 +2476,11 @@ class ElectrumWindow(QMainWindow, PrintError):
def sweep_key_dialog(self): def sweep_key_dialog(self):
d = QDialog(self) d = WindowModalDialog(self, title=_('Sweep private keys'))
d.setWindowTitle(_('Sweep private keys'))
d.setMinimumSize(600, 300) d.setMinimumSize(600, 300)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
vbox.addWidget(QLabel(_("Enter private keys"))) vbox.addWidget(QLabel(_("Enter private keys:")))
keys_e = QTextEdit() keys_e = QTextEdit()
keys_e.setTabChangesFocus(True) keys_e.setTabChangesFocus(True)
@ -2508,16 +2516,17 @@ class ElectrumWindow(QMainWindow, PrintError):
if not tx: if not tx:
self.show_message(_('No inputs found. (Note that inputs need to be confirmed)')) self.show_message(_('No inputs found. (Note that inputs need to be confirmed)'))
return return
self.warn_if_watching_only()
self.show_transaction(tx) self.show_transaction(tx)
@protected @protected
def do_import_privkey(self, password): def do_import_privkey(self, password):
if not self.wallet.has_imported_keys(): if not self.wallet.has_imported_keys():
r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \ if not self.question('<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
+ _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \ + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
+ _('Are you sure you understand what you are doing?'), 3, 4) + _('Are you sure you understand what you are doing?'), title=_('Warning')):
if r == 4: return return
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import")) text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
if not text: return if not text: return
@ -2536,18 +2545,16 @@ class ElectrumWindow(QMainWindow, PrintError):
else: else:
addrlist.append(addr) addrlist.append(addr)
if addrlist: if addrlist:
QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist)) self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(addrlist))
if badkeys: if badkeys:
QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys)) self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
self.address_list.update() self.address_list.update()
self.history_list.update() self.history_list.update()
def settings_dialog(self): def settings_dialog(self):
self.need_restart = False self.need_restart = False
d = QDialog(self) d = WindowModalDialog(self, _('Preferences'))
d.setWindowTitle(_('Preferences'))
d.setModal(1)
vbox = QVBoxLayout() vbox = QVBoxLayout()
tabs = QTabWidget() tabs = QTabWidget()
gui_widgets = [] gui_widgets = []
@ -2593,21 +2600,6 @@ class ElectrumWindow(QMainWindow, PrintError):
nz.valueChanged.connect(on_nz) nz.valueChanged.connect(on_nz)
gui_widgets.append((nz_label, nz)) gui_widgets.append((nz_label, nz))
choosers = sorted(COIN_CHOOSERS.keys())
chooser_name = self.wallet.coin_chooser_name(self.config)
msg = _('Choose coin (UTXO) selection method. The following are available:\n\n')
msg += '\n\n'.join(key + ": " + klass.__doc__
for key, klass in COIN_CHOOSERS.items())
chooser_label = HelpLabel(_('Coin selection') + ':', msg)
chooser_combo = QComboBox()
chooser_combo.addItems(choosers)
chooser_combo.setCurrentIndex(choosers.index(chooser_name))
def on_chooser(x):
chooser_name = choosers[chooser_combo.currentIndex()]
self.config.set_key('coin_chooser', chooser_name)
chooser_combo.currentIndexChanged.connect(on_chooser)
tx_widgets.append((chooser_label, chooser_combo))
msg = _('Fee per kilobyte of transaction.') + '\n' \ msg = _('Fee per kilobyte of transaction.') + '\n' \
+ _('If you enable dynamic fees, this parameter will be used as upper bound.') + _('If you enable dynamic fees, this parameter will be used as upper bound.')
fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg) fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg)
@ -2784,6 +2776,24 @@ class ElectrumWindow(QMainWindow, PrintError):
can_edit_fees_cb.setToolTip(_('This option lets you edit fees in the send tab.')) can_edit_fees_cb.setToolTip(_('This option lets you edit fees in the send tab.'))
tx_widgets.append((can_edit_fees_cb, None)) tx_widgets.append((can_edit_fees_cb, None))
def fmt_docs(key, klass):
lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")]
return '\n'.join([key, "", " ".join(lines)])
choosers = sorted(COIN_CHOOSERS.keys())
chooser_name = self.wallet.coin_chooser_name(self.config)
msg = _('Choose coin (UTXO) selection method. The following are available:\n\n')
msg += '\n\n'.join(fmt_docs(*item) for item in COIN_CHOOSERS.items())
chooser_label = HelpLabel(_('Coin selection') + ':', msg)
chooser_combo = QComboBox()
chooser_combo.addItems(choosers)
chooser_combo.setCurrentIndex(choosers.index(chooser_name))
def on_chooser(x):
chooser_name = choosers[chooser_combo.currentIndex()]
self.config.set_key('coin_chooser', chooser_name)
chooser_combo.currentIndexChanged.connect(on_chooser)
tx_widgets.append((chooser_label, chooser_combo))
tabs_info = [ tabs_info = [
(tx_widgets, _('Transactions')), (tx_widgets, _('Transactions')),
(gui_widgets, _('Appearance')), (gui_widgets, _('Appearance')),
@ -2813,13 +2823,11 @@ class ElectrumWindow(QMainWindow, PrintError):
run_hook('close_settings_dialog') run_hook('close_settings_dialog')
if self.need_restart: if self.need_restart:
QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK')) self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
def run_network_dialog(self): def run_network_dialog(self):
if not self.network: if not self.network:
QMessageBox.warning(self, _('Offline'), _('You are using Electrum in offline mode.\nRestart Electrum if you want to get connected.'), _('OK')) self.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline'))
return return
NetworkDialog(self.wallet.network, self.config, self).do_exec() NetworkDialog(self.wallet.network, self.config, self).do_exec()
@ -2839,9 +2847,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def plugins_dialog(self): def plugins_dialog(self):
self.pluginsdialog = d = QDialog(self) self.pluginsdialog = d = WindowModalDialog(self, _('Electrum Plugins'))
d.setWindowTitle(_('Electrum Plugins'))
d.setModal(1)
plugins = self.gui_object.plugins plugins = self.gui_object.plugins
@ -2867,7 +2873,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def enable_settings_widget(p, name, i): def enable_settings_widget(p, name, i):
widget = settings_widgets.get(name) widget = settings_widgets.get(name)
if not widget and p and p.requires_settings(): if not widget and p and p.requires_settings():
widget = settings_widgets[name] = p.settings_widget(self) widget = settings_widgets[name] = p.settings_widget(d)
grid.addWidget(widget, i, 1) grid.addWidget(widget, i, 1)
if widget: if widget:
widget.setEnabled(bool(p and p.is_enabled())) widget.setEnabled(bool(p and p.is_enabled()))
@ -2904,9 +2910,7 @@ class ElectrumWindow(QMainWindow, PrintError):
def show_account_details(self, k): def show_account_details(self, k):
account = self.wallet.accounts[k] account = self.wallet.accounts[k]
d = QDialog(self) d = WindowModalDialog(self, _('Account Details'))
d.setWindowTitle(_('Account Details'))
d.setModal(1)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
name = self.wallet.get_account_name(k) name = self.wallet.get_account_name(k)

View File

@ -16,30 +16,21 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys, time, datetime, re, threading
import os.path, json, ast, traceback
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import print_error, print_msg
from electrum import DEFAULT_PORTS from electrum import DEFAULT_PORTS
from electrum.network import serialize_server, deserialize_server from electrum.network import serialize_server, deserialize_server
from util import * from util import *
#protocol_names = ['TCP', 'HTTP', 'SSL', 'HTTPS']
#protocol_letters = 'thsg'
protocol_names = ['TCP', 'SSL'] protocol_names = ['TCP', 'SSL']
protocol_letters = 'ts' protocol_letters = 'ts'
class NetworkDialog(QDialog): class NetworkDialog(WindowModalDialog):
def __init__(self, network, config, parent): def __init__(self, network, config, parent):
WindowModalDialog.__init__(self, parent, _('Network'))
QDialog.__init__(self,parent)
self.setModal(1)
self.setWindowTitle(_('Network'))
self.setMinimumSize(375, 20) self.setMinimumSize(375, 20)
self.network = network self.network = network

View File

@ -23,9 +23,27 @@ from util import *
import re import re
import math import math
def check_password_strength(password):
'''
Check the strength of the password entered by the user and return back the same
:param password: password entered by user in New Password
:return: password strength Weak or Medium or Strong
'''
password = unicode(password)
n = math.log(len(set(password)))
num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None
caps = password != password.upper() and password != password.lower()
extra = re.match("^[a-zA-Z0-9]*$", password) is None
score = len(password)*( n + caps + num + extra)/20
password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"}
return password_strength[min(3, int(score))]
def make_password_dialog(self, wallet, msg, new_pass=True): class PasswordDialog(WindowModalDialog):
def __init__(self, parent, wallet, title, msg, new_pass):
WindowModalDialog.__init__(self, parent, title)
self.wallet = wallet
self.pw = QLineEdit() self.pw = QLineEdit()
self.pw.setEchoMode(2) self.pw.setEchoMode(2)
@ -44,8 +62,6 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
grid.setColumnStretch(1,1) grid.setColumnStretch(1,1)
logo = QLabel() logo = QLabel()
lockfile = ":icons/lock.png" if wallet and wallet.use_encryption else ":icons/unlock.png"
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
logo.setAlignment(Qt.AlignCenter) logo.setAlignment(Qt.AlignCenter)
grid.addWidget(logo, 0, 0) grid.addWidget(logo, 0, 0)
@ -60,6 +76,11 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
if wallet and wallet.use_encryption: if wallet and wallet.use_encryption:
grid.addWidget(QLabel(_('Password')), 0, 0) grid.addWidget(QLabel(_('Password')), 0, 0)
grid.addWidget(self.pw, 0, 1) grid.addWidget(self.pw, 0, 1)
lockfile = ":icons/lock.png"
else:
self.pw = None
lockfile = ":icons/unlock.png"
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
grid.addWidget(QLabel(_('New Password') if new_pass else _('Password')), 1, 0) grid.addWidget(QLabel(_('New Password') if new_pass else _('Password')), 1, 0)
grid.addWidget(self.new_pw, 1, 1) grid.addWidget(self.new_pw, 1, 1)
@ -71,105 +92,36 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
# Password Strength Label # Password Strength Label
self.pw_strength = QLabel() self.pw_strength = QLabel()
grid.addWidget(self.pw_strength, 3, 0, 1, 2) grid.addWidget(self.pw_strength, 3, 0, 1, 2)
self.new_pw.textChanged.connect(lambda: update_password_strength(self.pw_strength, self.new_pw.text())) self.new_pw.textChanged.connect(self.pw_changed)
self.conf_pw.textChanged.connect(self.check_OKButton)
self.OKButton = OkButton(self)
vbox.addStretch(1) vbox.addStretch(1)
vbox.addLayout(Buttons(CancelButton(self), OkButton(self))) vbox.addLayout(Buttons(CancelButton(self), self.OKButton))
return vbox self.setLayout(vbox)
def pw_changed(self):
password = self.new_pw.text()
if password:
colors = {"Weak":"Red", "Medium":"Blue", "Strong":"Green",
"Very Strong":"Green"}
strength = check_password_strength(password)
label = (_("Password Strength") + ": " + "<font color="
+ colors[strength] + ">" + strength + "</font>")
else:
label = ""
self.pw_strength.setText(label)
self.check_OKButton()
def run_password_dialog(self, wallet, parent): def check_OKButton(self):
self.OKButton.setEnabled(self.new_pw.text() == self.conf_pw.text())
if wallet and wallet.is_watching_only():
QMessageBox.information(parent, _('Error'), _('This is a watching-only wallet'), _('OK'))
return False, None, None
def run(self):
if not self.exec_(): if not self.exec_():
return False, None, None return False, None, None
password = unicode(self.pw.text()) if wallet and wallet.use_encryption else None password = unicode(self.pw.text()) if self.pw else None
new_password = unicode(self.new_pw.text()) new_password = unicode(self.new_pw.text())
new_password2 = unicode(self.conf_pw.text()) new_password2 = unicode(self.conf_pw.text())
if new_password != new_password2: return True, password or None, new_password or None
QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
# Retry
return run_password_dialog(self, wallet, parent)
if not new_password:
new_password = None
return True, password, new_password
def check_password_strength(password):
'''
Check the strength of the password entered by the user and return back the same
:param password: password entered by user in New Password
:return: password strength Weak or Medium or Strong
'''
password = unicode(password)
n = math.log(len(set(password)))
num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None
caps = password != password.upper() and password != password.lower()
extra = re.match("^[a-zA-Z0-9]*$", password) is None
score = len(password)*( n + caps + num + extra)/20
password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"}
return password_strength[min(3, int(score))]
def update_password_strength(pw_strength_label,password):
'''
call the function check_password_strength and update the label pw_strength interactively as the user is typing the password
:param pw_strength_label: the label pw_strength
:param password: password entered in New Password text box
:return: None
'''
if password:
colors = {"Weak":"Red","Medium":"Blue","Strong":"Green", "Very Strong":"Green"}
strength = check_password_strength(password)
label = _("Password Strength")+ ": "+"<font color=" + colors[strength] + ">" + strength + "</font>"
else:
label = ""
pw_strength_label.setText(label)
class PasswordDialog(QDialog):
def __init__(self, wallet, parent):
QDialog.__init__(self, parent)
self.setModal(1)
self.wallet = wallet
self.parent = parent
self.setWindowTitle(_("Set Password"))
msg = (_('Your wallet is encrypted. Use this dialog to change your password.') + ' '\
+_('To disable wallet encryption, enter an empty new password.')) \
if wallet.use_encryption else _('Your wallet keys are not encrypted')
self.setLayout(make_password_dialog(self, wallet, msg))
def run(self):
ok, password, new_password = run_password_dialog(self, self.wallet, self.parent)
if not ok:
return
try:
self.wallet.check_password(password)
except BaseException as e:
QMessageBox.warning(self.parent, _('Error'), str(e), _('OK'))
return False, None, None
try:
self.wallet.update_password(password, new_password)
except:
import traceback, sys
traceback.print_exc(file=sys.stdout)
QMessageBox.warning(self.parent, _('Error'), _('Failed to update password'), _('OK'))
return
if new_password:
QMessageBox.information(self.parent, _('Success'), _('Password was updated successfully'), _('OK'))
else:
QMessageBox.information(self.parent, _('Success'), _('This wallet is not encrypted'), _('OK'))

View File

@ -160,16 +160,8 @@ class PayToEdit(ScanQRTextEdit):
return len(self.lines()) > 1 return len(self.lines()) > 1
def paytomany(self): def paytomany(self):
from electrum.i18n import _
self.setText("\n\n\n") self.setText("\n\n\n")
self.update_size() self.update_size()
msg = '\n'.join([
_('Enter a list of outputs in the \'Pay to\' field.'),
_('One output per line.'),
_('Format: address, amount.'),
_('You may load a CSV file using the file icon.')
])
QMessageBox.warning(self, _('Pay to many'), msg, _('OK'))
def update_size(self): def update_size(self):
docHeight = self.document().size().height() docHeight = self.document().size().height()

View File

@ -1,6 +1,5 @@
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore
import PyQt4.QtGui as QtGui import PyQt4.QtGui as QtGui
import os import os
@ -9,6 +8,7 @@ import qrcode
import electrum import electrum
from electrum import bmp from electrum import bmp
from electrum.i18n import _ from electrum.i18n import _
from util import WindowModalDialog, MessageBoxMixin
class QRCodeWidget(QWidget): class QRCodeWidget(QWidget):
@ -83,13 +83,11 @@ class QRCodeWidget(QWidget):
class QRDialog(QDialog): class QRDialog(WindowModalDialog, MessageBoxMixin):
def __init__(self, data, parent=None, title = "", show_text=False): def __init__(self, data, parent=None, title = "", show_text=False):
QDialog.__init__(self, parent) WindowModalDialog.__init__(self, parent, title)
d = self
d.setWindowTitle(title)
vbox = QVBoxLayout() vbox = QVBoxLayout()
qrw = QRCodeWidget(data) qrw = QRCodeWidget(data)
vbox.addWidget(qrw, 1) vbox.addWidget(qrw, 1)
@ -107,12 +105,12 @@ class QRDialog(QDialog):
def print_qr(): def print_qr():
bmp.save_qrcode(qrw.qr, filename) bmp.save_qrcode(qrw.qr, filename)
QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK')) self.show_message(_("QR code saved to file") + " " + filename)
def copy_to_clipboard(): def copy_to_clipboard():
bmp.save_qrcode(qrw.qr, filename) bmp.save_qrcode(qrw.qr, filename)
QApplication.clipboard().setImage(QImage(filename)) QApplication.clipboard().setImage(QImage(filename))
QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK')) self.show_message(_("QR code copied to clipboard"))
b = QPushButton(_("Copy")) b = QPushButton(_("Copy"))
hbox.addWidget(b) hbox.addWidget(b)
@ -124,8 +122,8 @@ class QRDialog(QDialog):
b = QPushButton(_("Close")) b = QPushButton(_("Close"))
hbox.addWidget(b) hbox.addWidget(b)
b.clicked.connect(d.accept) b.clicked.connect(self.accept)
b.setDefault(True) b.setDefault(True)
vbox.addLayout(hbox) vbox.addLayout(hbox)
d.setLayout(vbox) self.setLayout(vbox)

View File

@ -3,7 +3,7 @@ from electrum.plugins import run_hook
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
from util import ButtonsTextEdit from util import ButtonsTextEdit, MessageBoxMixin
class ShowQRTextEdit(ButtonsTextEdit): class ShowQRTextEdit(ButtonsTextEdit):
@ -29,7 +29,7 @@ class ShowQRTextEdit(ButtonsTextEdit):
m.exec_(e.globalPos()) m.exec_(e.globalPos())
class ScanQRTextEdit(ButtonsTextEdit): class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
def __init__(self, text=""): def __init__(self, text=""):
ButtonsTextEdit.__init__(self, text) ButtonsTextEdit.__init__(self, text)
@ -50,8 +50,8 @@ class ScanQRTextEdit(ButtonsTextEdit):
from electrum import qrscanner, get_config from electrum import qrscanner, get_config
try: try:
data = qrscanner.scan_qr(get_config()) data = qrscanner.scan_qr(get_config())
except BaseException, e: except BaseException as e:
QMessageBox.warning(self, _('Error'), _(e), _('OK')) self.show_error(str(e))
return "" return ""
if type(data) != str: if type(data) != str:
return return

View File

@ -20,17 +20,14 @@ from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore import PyQt4.QtCore as QtCore
from electrum.i18n import _ from electrum.i18n import _
from electrum import mnemonic
from util import * from util import *
from qrtextedit import ShowQRTextEdit, ScanQRTextEdit from qrtextedit import ShowQRTextEdit, ScanQRTextEdit
class SeedDialog(QDialog): class SeedDialog(WindowModalDialog):
def __init__(self, parent, seed, imported_keys): def __init__(self, parent, seed, imported_keys):
QDialog.__init__(self, parent) WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
self.setModal(1)
self.setMinimumWidth(400) self.setMinimumWidth(400)
self.setWindowTitle('Electrum' + ' - ' + _('Seed'))
vbox = show_seed_box_msg(seed) vbox = show_seed_box_msg(seed)
if imported_keys: if imported_keys:
vbox.addWidget(QLabel("<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>")) vbox.addWidget(QLabel("<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"))

View File

@ -38,7 +38,7 @@ def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
dialogs.append(d) dialogs.append(d)
d.show() d.show()
class TxDialog(QDialog): class TxDialog(QDialog, MessageBoxMixin):
def __init__(self, tx, parent, desc, prompt_if_unsaved): def __init__(self, tx, parent, desc, prompt_if_unsaved):
'''Transactions in the wallet will show their description. '''Transactions in the wallet will show their description.
@ -54,7 +54,7 @@ class TxDialog(QDialog):
self.desc = desc self.desc = desc
QDialog.__init__(self) QDialog.__init__(self)
self.setMinimumWidth(600) self.setMinimumWidth(660)
self.setWindowTitle(_("Transaction")) self.setWindowTitle(_("Transaction"))
vbox = QVBoxLayout() vbox = QVBoxLayout()
@ -62,7 +62,7 @@ class TxDialog(QDialog):
vbox.addWidget(QLabel(_("Transaction ID:"))) vbox.addWidget(QLabel(_("Transaction ID:")))
self.tx_hash_e = ButtonsLineEdit() self.tx_hash_e = ButtonsLineEdit()
qr_show = lambda: self.parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID') qr_show = lambda: self.parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID', parent=self)
self.tx_hash_e.addButton(":icons/qrcode.png", qr_show, _("Show as QR code")) self.tx_hash_e.addButton(":icons/qrcode.png", qr_show, _("Show as QR code"))
self.tx_hash_e.setReadOnly(True) self.tx_hash_e.setReadOnly(True)
vbox.addWidget(self.tx_hash_e) vbox.addWidget(self.tx_hash_e)
@ -122,10 +122,7 @@ class TxDialog(QDialog):
def closeEvent(self, event): def closeEvent(self, event):
if (self.prompt_if_unsaved and not self.saved and not self.broadcast if (self.prompt_if_unsaved and not self.saved and not self.broadcast
and QMessageBox.question( and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
self, _('Warning'),
_('This transaction is not saved. Close anyway?'),
QMessageBox.Yes | QMessageBox.No) == QMessageBox.No):
event.ignore() event.ignore()
else: else:
event.accept() event.accept()
@ -135,7 +132,7 @@ class TxDialog(QDialog):
text = self.tx.raw.decode('hex') text = self.tx.raw.decode('hex')
text = base_encode(text, base=43) text = base_encode(text, base=43)
try: try:
self.parent.show_qrcode(text, 'Transaction') self.parent.show_qrcode(text, 'Transaction', parent=self)
except Exception as e: except Exception as e:
self.show_message(str(e)) self.show_message(str(e))
@ -173,7 +170,7 @@ class TxDialog(QDialog):
status = _("Signed") status = _("Signed")
if tx_hash in self.wallet.transactions.keys(): if tx_hash in self.wallet.transactions.keys():
desc, is_default = self.wallet.get_label(tx_hash) desc = self.wallet.get_label(tx_hash)
conf, timestamp = self.wallet.get_confirmations(tx_hash) conf, timestamp = self.wallet.get_confirmations(tx_hash)
if timestamp: if timestamp:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
@ -249,6 +246,9 @@ class TxDialog(QDialog):
return chg if self.wallet.is_change(addr) else rec return chg if self.wallet.is_change(addr) else rec
return ext return ext
def format_amount(amt):
return self.parent.format_amount(amt, whitespaces = True)
i_text = QTextEdit() i_text = QTextEdit()
i_text.setFont(QFont(MONOSPACE_FONT)) i_text.setFont(QFont(MONOSPACE_FONT))
i_text.setReadOnly(True) i_text.setReadOnly(True)
@ -270,6 +270,8 @@ class TxDialog(QDialog):
if addr is None: if addr is None:
addr = _('unknown') addr = _('unknown')
cursor.insertText(addr, text_format(addr)) cursor.insertText(addr, text_format(addr))
if x.get('value'):
cursor.insertText(format_amount(x['value']), ext)
cursor.insertBlock() cursor.insertBlock()
vbox.addWidget(i_text) vbox.addWidget(i_text)
@ -283,11 +285,6 @@ class TxDialog(QDialog):
cursor.insertText(addr, text_format(addr)) cursor.insertText(addr, text_format(addr))
if v is not None: if v is not None:
cursor.insertText('\t', ext) cursor.insertText('\t', ext)
cursor.insertText(self.parent.format_amount(v, whitespaces = True), ext) cursor.insertText(format_amount(v), ext)
cursor.insertBlock() cursor.insertBlock()
vbox.addWidget(o_text) vbox.addWidget(o_text)
def show_message(self, msg):
QMessageBox.information(self, _('Message'), msg, _('OK'))

View File

@ -21,51 +21,19 @@ RED_FG = "QWidget {color:red;}"
BLUE_FG = "QWidget {color:blue;}" BLUE_FG = "QWidget {color:blue;}"
BLACK_FG = "QWidget {color:black;}" BLACK_FG = "QWidget {color:black;}"
dialogs = []
class WaitingDialog(QThread):
def __init__(self, parent, message, run_task, on_success=None, on_complete=None):
QThread.__init__(self)
self.parent = parent
self.d = QDialog(parent)
self.d.setWindowTitle('Please wait')
l = QLabel(message)
vbox = QVBoxLayout(self.d)
vbox.addWidget(l)
self.run_task = run_task
self.on_success = on_success
self.on_complete = on_complete
self.d.connect(self.d, SIGNAL('done'), self.close)
self.d.show()
def run(self):
self.error = None
try:
self.result = self.run_task()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
self.error = str(e)
self.d.emit(SIGNAL('done'))
def close(self):
self.d.accept()
if self.error:
QMessageBox.warning(self.parent, _('Error'), self.error, _('OK'))
else:
if self.on_success:
if type(self.result) is not tuple:
self.result = (self.result,)
self.on_success(*self.result)
if self.on_complete:
self.on_complete()
class Timer(QThread): class Timer(QThread):
stopped = False
def run(self): def run(self):
while True: while not self.stopped:
self.emit(SIGNAL('timersignal')) self.emit(SIGNAL('timersignal'))
time.sleep(0.5) time.sleep(0.5)
def stop(self):
self.stopped = True
self.wait()
class EnterButton(QPushButton): class EnterButton(QPushButton):
def __init__(self, text, func): def __init__(self, text, func):
@ -187,12 +155,91 @@ class CancelButton(QPushButton):
QPushButton.__init__(self, label or _("Cancel")) QPushButton.__init__(self, label or _("Cancel"))
self.clicked.connect(dialog.reject) self.clicked.connect(dialog.reject)
class MessageBoxMixin:
def question(self, msg, parent=None, title=None):
Yes, No = QMessageBox.Yes, QMessageBox.No
return self.msg_box(QMessageBox.Question, parent or self, title or '',
msg, buttons=Yes|No, defaultButton=No) == Yes
def show_warning(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Warning, parent or self,
title or _('Warning'), msg)
def show_error(self, msg, parent=None):
return self.msg_box(QMessageBox.Warning, parent or self,
_('Error'), msg)
def show_critical(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Critical, parent or self,
title or _('Critical Error'), msg)
def show_message(self, msg, parent=None, title=None):
return self.msg_box(QMessageBox.Information, parent or self,
title or _('Information'), msg)
@staticmethod
def msg_box(icon, parent, title, text, buttons=QMessageBox.Ok,
defaultButton=QMessageBox.NoButton):
# handle e.g. ElectrumGui
if not isinstance(parent, QWidget):
parent = None
d = QMessageBox(icon, title, text, buttons, parent)
d.setWindowModality(Qt.WindowModal)
d.setDefaultButton(defaultButton)
return d.exec_()
class WindowModalDialog(QDialog):
'''Handy wrapper; window modal dialogs are better for our multi-window
daemon model as other wallet windows can still be accessed.'''
def __init__(self, parent, title=None):
QDialog.__init__(self, parent)
self.setWindowModality(Qt.WindowModal)
if title:
self.setWindowTitle(title)
class WaitingDialog(QThread, MessageBoxMixin):
'''Shows a please wait dialog whilst runnning a task. It is not
necessary to maintain a reference to this dialog.'''
def __init__(self, parent, message, task, on_success=None,
on_finished=None):
global dialogs
dialogs.append(self) # Prevent GC
QThread.__init__(self)
self.task = task
self.on_success = on_success
self.on_finished = on_finished
self.dialog = WindowModalDialog(parent, _("Please wait"))
vbox = QVBoxLayout(self.dialog)
vbox.addWidget(QLabel(message))
self.dialog.show()
self.dialog.connect(self, SIGNAL("finished()"), self.finished)
self.start()
def run(self):
self.error = None
try:
self.result = self.task()
except BaseException as e:
traceback.print_exc(file=sys.stdout)
self.error = str(e)
def finished(self):
global dialogs
dialogs.remove(self)
if self.error:
self.show_error(self.error, parent=self.dialog.parent())
elif self.on_success:
result = self.result
if type(result) is not tuple:
result = (result,)
self.on_success(*result)
if self.on_finished:
self.on_finished()
self.dialog.accept()
def line_dialog(parent, title, label, ok_label, default=None): def line_dialog(parent, title, label, ok_label, default=None):
dialog = QDialog(parent) dialog = WindowModalDialog(parent, title)
dialog.setMinimumWidth(500) dialog.setMinimumWidth(500)
dialog.setWindowTitle(title)
dialog.setModal(1)
l = QVBoxLayout() l = QVBoxLayout()
dialog.setLayout(l) dialog.setLayout(l)
l.addWidget(QLabel(label)) l.addWidget(QLabel(label))
@ -206,10 +253,8 @@ def line_dialog(parent, title, label, ok_label, default=None):
def text_dialog(parent, title, label, ok_label, default=None): def text_dialog(parent, title, label, ok_label, default=None):
from qrtextedit import ScanQRTextEdit from qrtextedit import ScanQRTextEdit
dialog = QDialog(parent) dialog = WindowModalDialog(parent, title)
dialog.setMinimumWidth(500) dialog.setMinimumWidth(500)
dialog.setWindowTitle(title)
dialog.setModal(1)
l = QVBoxLayout() l = QVBoxLayout()
dialog.setLayout(l) dialog.setLayout(l)
l.addWidget(QLabel(label)) l.addWidget(QLabel(label))
@ -221,9 +266,6 @@ def text_dialog(parent, title, label, ok_label, default=None):
if dialog.exec_(): if dialog.exec_():
return unicode(txt.toPlainText()) return unicode(txt.toPlainText())
def question(msg):
return QMessageBox.question(None, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
def address_field(addresses): def address_field(addresses):
hbox = QHBoxLayout() hbox = QHBoxLayout()
address_e = QLineEdit() address_e = QLineEdit()
@ -383,12 +425,6 @@ class MyTreeWidget(QTreeWidget):
key = str(item.data(0, Qt.UserRole).toString()) key = str(item.data(0, Qt.UserRole).toString())
text = unicode(item.text(column)) text = unicode(item.text(column))
self.parent.wallet.set_label(key, text) self.parent.wallet.set_label(key, text)
if text:
item.setForeground(column, QBrush(QColor('black')))
else:
text = self.parent.wallet.get_default_label(key)
item.setText(column, text)
item.setForeground(column, QBrush(QColor('gray')))
self.parent.history_list.update() self.parent.history_list.update()
self.parent.update_completions() self.parent.update_completions()

View File

@ -96,7 +96,7 @@ class ElectrumGui:
else: else:
time_str = 'pending' time_str = 'pending'
label, is_default_label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
messages.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) ) messages.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance"))) self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))

View File

@ -116,7 +116,7 @@ class ElectrumGui:
else: else:
time_str = 'pending' time_str = 'pending'
label, is_default_label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
if len(label) > 40: if len(label) > 40:
label = label[0:37] + '...' label = label[0:37] + '...'
self.history.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) ) self.history.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )

View File

@ -21,13 +21,14 @@ import os
import util import util
from bitcoin import * from bitcoin import *
MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
class Blockchain(util.PrintError): class Blockchain(util.PrintError):
'''Manages blockchain headers and their verification''' '''Manages blockchain headers and their verification'''
def __init__(self, config, network): def __init__(self, config, network):
self.config = config self.config = config
self.network = network self.network = network
self.headers_url = 'https://headers.electrum.org/blockchain_headers' self.headers_url = "https://headers.electrum.org/blockchain_headers"
self.local_height = 0 self.local_height = 0
self.set_local_height() self.set_local_height()
@ -39,66 +40,35 @@ class Blockchain(util.PrintError):
self.set_local_height() self.set_local_height()
self.print_error("%d blocks" % self.local_height) self.print_error("%d blocks" % self.local_height)
def verify_header(self, header, prev_header, bits, target):
prev_hash = self.hash_header(prev_header)
assert prev_hash == header.get('prev_block_hash'), "prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))
assert bits == header.get('bits'), "bits mismatch: %s vs %s" % (bits, header.get('bits'))
_hash = self.hash_header(header)
assert int('0x' + _hash, 16) <= target, "insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target)
def verify_chain(self, chain): def verify_chain(self, chain):
first_header = chain[0] first_header = chain[0]
prev_header = self.read_header(first_header.get('block_height') - 1) prev_header = self.read_header(first_header.get('block_height') - 1)
for header in chain: for header in chain:
height = header.get('block_height') height = header.get('block_height')
prev_hash = self.hash_header(prev_header)
if prev_hash != header.get('prev_block_hash'):
self.print_error("prev hash mismatch: %s vs %s"
% (prev_hash, header.get('prev_block_hash')))
return False
bits, target = self.get_target(height / 2016, chain) bits, target = self.get_target(height / 2016, chain)
if bits != header.get('bits'): self.verify_header(header, prev_header, bits, target)
self.print_error("bits mismatch: %s vs %s"
% (bits, header.get('bits')))
return False
_hash = self.hash_header(header)
if int('0x'+_hash, 16) > target:
self.print_error("insufficient proof of work: %s vs target %s"
% (int('0x'+_hash, 16), target))
return False
prev_header = header prev_header = header
return True def verify_chunk(self, index, data):
def verify_chunk(self, index, hexdata):
data = hexdata.decode('hex')
height = index*2016
num = len(data) / 80 num = len(data) / 80
prev_header = None
if index == 0: if index != 0:
previous_hash = ("0"*64)
else:
prev_header = self.read_header(index*2016 - 1) prev_header = self.read_header(index*2016 - 1)
if prev_header is None: raise
previous_hash = self.hash_header(prev_header)
bits, target = self.get_target(index) bits, target = self.get_target(index)
for i in range(num): for i in range(num):
height = index*2016 + i
raw_header = data[i*80:(i+1) * 80] raw_header = data[i*80:(i+1) * 80]
header = self.header_from_string(raw_header) header = self.deserialize_header(raw_header)
_hash = self.hash_header(header) self.verify_header(header, prev_header, bits, target)
assert previous_hash == header.get('prev_block_hash') prev_header = header
assert bits == header.get('bits')
assert int('0x'+_hash,16) < target
previous_header = header def serialize_header(self, res):
previous_hash = _hash
self.save_chunk(index, data)
self.print_error("validated chunk %d to height %d" % (index, height))
def header_to_string(self, res):
s = int_to_hex(res.get('version'), 4) \ s = int_to_hex(res.get('version'), 4) \
+ rev_hex(res.get('prev_block_hash')) \ + rev_hex(res.get('prev_block_hash')) \
+ rev_hex(res.get('merkle_root')) \ + rev_hex(res.get('merkle_root')) \
@ -107,8 +77,7 @@ class Blockchain(util.PrintError):
+ int_to_hex(int(res.get('nonce')), 4) + int_to_hex(int(res.get('nonce')), 4)
return s return s
def deserialize_header(self, s):
def header_from_string(self, s):
hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16) hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16)
h = {} h = {}
h['version'] = hex_to_int(s[0:4]) h['version'] = hex_to_int(s[0:4])
@ -120,7 +89,9 @@ class Blockchain(util.PrintError):
return h return h
def hash_header(self, header): def hash_header(self, header):
return rev_hex(Hash(self.header_to_string(header).decode('hex')).encode('hex')) if header is None:
return '0' * 64
return hash_encode(Hash(self.serialize_header(header).decode('hex')))
def path(self): def path(self):
return os.path.join(self.config.path, 'blockchain_headers') return os.path.join(self.config.path, 'blockchain_headers')
@ -148,7 +119,7 @@ class Blockchain(util.PrintError):
self.set_local_height() self.set_local_height()
def save_header(self, header): def save_header(self, header):
data = self.header_to_string(header).decode('hex') data = self.serialize_header(header).decode('hex')
assert len(data) == 80 assert len(data) == 80
height = header.get('block_height') height = header.get('block_height')
filename = self.path() filename = self.path()
@ -173,53 +144,42 @@ class Blockchain(util.PrintError):
h = f.read(80) h = f.read(80)
f.close() f.close()
if len(h) == 80: if len(h) == 80:
h = self.header_from_string(h) h = self.deserialize_header(h)
return h return h
def get_target(self, index, chain=None): def get_target(self, index, chain=None):
if chain is None: if index == 0:
chain = [] # Do not use mutables as default values! return 0x1d00ffff, MAX_TARGET
max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
if index == 0: return 0x1d00ffff, max_target
first = self.read_header((index-1) * 2016) first = self.read_header((index-1) * 2016)
last = self.read_header(index*2016 - 1) last = self.read_header(index*2016 - 1)
if last is None: if last is None:
for h in chain: for h in chain:
if h.get('block_height') == index*2016 - 1: if h.get('block_height') == index*2016 - 1:
last = h last = h
assert last is not None
# bits to target
bits = last.get('bits')
bitsN = (bits >> 24) & 0xff
assert bitsN >= 0x03 and bitsN <= 0x1d, "First part of bits should be in [0x03, 0x1d]"
bitsBase = bits & 0xffffff
assert bitsBase >= 0x8000 and bitsBase <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]"
target = bitsBase << (8 * (bitsN-3))
# new target
nActualTimespan = last.get('timestamp') - first.get('timestamp') nActualTimespan = last.get('timestamp') - first.get('timestamp')
nTargetTimespan = 14 * 24 * 60 * 60 nTargetTimespan = 14 * 24 * 60 * 60
nActualTimespan = max(nActualTimespan, nTargetTimespan / 4) nActualTimespan = max(nActualTimespan, nTargetTimespan / 4)
nActualTimespan = min(nActualTimespan, nTargetTimespan * 4) nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
new_target = min(MAX_TARGET, (target*nActualTimespan) / nTargetTimespan)
bits = last.get('bits') # convert new target to bits
# convert to bignum c = ("%064x" % new_target)[2:]
MM = 256*256*256 while c[:2] == '00' and len(c) > 6:
a = bits%MM
if a < 0x8000:
a *= 256
target = (a) * pow(2, 8 * (bits/MM - 3))
# new target
new_target = min( max_target, (target * nActualTimespan)/nTargetTimespan )
# convert it to bits
c = ("%064X"%new_target)[2:]
i = 31
while c[0:2]=="00":
c = c[2:] c = c[2:]
i -= 1 bitsN, bitsBase = len(c) / 2, int('0x' + c[:6], 16)
if bitsBase >= 0x800000:
c = int('0x'+c[0:6],16) bitsN += 1
if c >= 0x800000: bitsBase >>= 8
c /= 256 new_bits = bitsN << 24 | bitsBase
i += 1 return new_bits, bitsBase << (8 * (bitsN-3))
new_bits = c + MM * i
return new_bits, new_target
def connect_header(self, chain, header): def connect_header(self, chain, header):
'''Builds a header chain until it connects. Returns True if it has '''Builds a header chain until it connects. Returns True if it has
@ -241,18 +201,23 @@ class Blockchain(util.PrintError):
# The chain is complete. Reverse to order by increasing height # The chain is complete. Reverse to order by increasing height
chain.reverse() chain.reverse()
if self.verify_chain(chain): try:
self.verify_chain(chain)
self.print_error("connected at height:", previous_height) self.print_error("connected at height:", previous_height)
for header in chain: for header in chain:
self.save_header(header) self.save_header(header)
return True return True
except BaseException as e:
self.print_error(str(e))
return False return False
def connect_chunk(self, idx, chunk): def connect_chunk(self, idx, hexdata):
try: try:
self.verify_chunk(idx, chunk) data = hexdata.decode('hex')
self.verify_chunk(idx, data)
self.print_error("validated chunk %d" % idx)
self.save_chunk(idx, data)
return idx + 1 return idx + 1
except Exception: except BaseException as e:
self.print_error('verify_chunk failed') self.print_error('verify_chunk failed', str(e))
return idx - 1 return idx - 1

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# #
# Electrum - lightweight Bitcoin client # Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious # Copyright (C) 2015 kyuupichan@gmail
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -17,7 +17,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from random import shuffle from random import choice, randint, shuffle
from math import floor, log10
from bitcoin import COIN from bitcoin import COIN
from transaction import Transaction from transaction import Transaction
@ -25,6 +26,15 @@ from util import NotEnoughFunds, PrintError, profiler
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins']) Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount'''
bkts = sorted(bkts, key = lambda bkt: bkt.value)
for i in range(len(bkts)):
if not sufficient_funds(bkts[i + 1:]):
return bkts[i:]
# Shouldn't get here
return bkts
class CoinChooserBase(PrintError): class CoinChooserBase(PrintError):
def keys(self, coins): def keys(self, coins):
@ -49,17 +59,25 @@ class CoinChooserBase(PrintError):
return 0 return 0
return penalty return penalty
def add_change(self, tx, change_addrs, fee_estimator, dust_threshold): def change_amounts(self, tx, count, fee_estimator, dust_threshold):
# How much is left if we add 1 change output? # The amount left after adding 1 change output
change_amount = tx.get_fee() - fee_estimator(1) return [max(0, tx.get_fee() - fee_estimator(1))]
def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
amounts = self.change_amounts(tx, len(change_addrs), fee_estimator,
dust_threshold)
assert min(amounts) >= 0
assert len(change_addrs) >= len(amounts)
# If change is above dust threshold after accounting for the # If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction. # size of the change output, add it to the transaction.
if change_amount > dust_threshold: dust = sum(amount for amount in amounts if amount < dust_threshold)
tx.outputs.append(('address', change_addrs[0], change_amount)) amounts = [amount for amount in amounts if amount >= dust_threshold]
self.print_error('change', change_amount) change = [('address', addr, amount)
elif change_amount: for addr, amount in zip(change_addrs, amounts)]
self.print_error('not keeping dust', change_amount) self.print_error('change:', change)
if dust:
self.print_error('not keeping dust', dust)
return change
def make_tx(self, coins, outputs, change_addrs, fee_estimator, def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold): dust_threshold):
@ -72,12 +90,18 @@ class CoinChooserBase(PrintError):
tx = Transaction.from_io([], outputs[:]) tx = Transaction.from_io([], outputs[:])
# Size of the transaction with no inputs and no change # Size of the transaction with no inputs and no change
base_size = tx.estimated_size() base_size = tx.estimated_size()
# Returns fee given input size spent_amount = tx.output_value()
fee = lambda input_size: fee_estimator(base_size + input_size)
def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough
value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets)
total_size = sum(bucket.size for bucket in buckets) + base_size
return total_input >= spent_amount + fee_estimator(total_size)
# Collect the coins into buckets, choose a subset of the buckets # Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins) buckets = self.bucketize_coins(coins)
buckets = self.choose_buckets(buckets, tx.output_value(), fee, buckets = self.choose_buckets(buckets, sufficient_funds,
self.penalty_func(tx)) self.penalty_func(tx))
tx.inputs = [coin for b in buckets for coin in b.coins] tx.inputs = [coin for b in buckets for coin in b.coins]
@ -86,50 +110,37 @@ class CoinChooserBase(PrintError):
# This takes a count of change outputs and returns a tx fee; # This takes a count of change outputs and returns a tx fee;
# each pay-to-bitcoin-address output serializes as 34 bytes # each pay-to-bitcoin-address output serializes as 34 bytes
fee = lambda count: fee_estimator(tx_size + count * 34) fee = lambda count: fee_estimator(tx_size + count * 34)
self.add_change(tx, change_addrs, fee, dust_threshold) change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
tx.outputs.extend(change)
self.print_error("using %d inputs" % len(tx.inputs)) self.print_error("using %d inputs" % len(tx.inputs))
self.print_error("using buckets:", [bucket.desc for bucket in buckets]) self.print_error("using buckets:", [bucket.desc for bucket in buckets])
return tx return tx
class CoinChooserClassic(CoinChooserBase): class CoinChooserOldestFirst(CoinChooserBase):
''' '''The classic electrum algorithm. Chooses coins starting with the
The classic electrum algorithm. Chooses coins starting with oldest that are sufficient to cover the spent amount, and then
the oldest that are sufficient to cover the spent amount, and removes any unneeded starting with the smallest in value.'''
then removes any unneeded starting with the smallest in value.'''
def keys(self, coins): def keys(self, coins):
return [coin['prevout_hash'] + ':' + str(coin['prevout_n']) return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
for coin in coins] for coin in coins]
def choose_buckets(self, buckets, spent_amount, fee, penalty_func): def choose_buckets(self, buckets, sufficient_funds, penalty_func):
'''Spend the oldest buckets first.''' '''Spend the oldest buckets first.'''
# Unconfirmed coins are young, not old # Unconfirmed coins are young, not old
adj_height = lambda height: 99999999 if height == 0 else height adj_height = lambda height: 99999999 if height == 0 else height
buckets.sort(key = lambda b: max(adj_height(coin['height']) buckets.sort(key = lambda b: max(adj_height(coin['height'])
for coin in b.coins)) for coin in b.coins))
selected, value, size = [], 0, 0 selected = []
for bucket in buckets: for bucket in buckets:
selected.append(bucket) selected.append(bucket)
value += bucket.value if sufficient_funds(selected):
size += bucket.size return strip_unneeded(selected, sufficient_funds)
if value >= spent_amount + fee(size):
break
else: else:
raise NotEnoughFunds() raise NotEnoughFunds()
# Remove unneeded inputs starting with the smallest.
selected.sort(key = lambda b: b.value)
dropped = []
for bucket in selected:
if value - bucket.value >= spent_amount + fee(size - bucket.size):
value -= bucket.value
size -= bucket.size
dropped.append(bucket)
return [bucket for bucket in selected if bucket not in dropped]
class CoinChooserRandom(CoinChooserBase): class CoinChooserRandom(CoinChooserBase):
def bucket_candidates(self, buckets, sufficient_funds): def bucket_candidates(self, buckets, sufficient_funds):
@ -157,18 +168,11 @@ class CoinChooserRandom(CoinChooserBase):
else: else:
raise NotEnoughFunds() raise NotEnoughFunds()
return [[buckets[n] for n in candidate] for candidate in candidates] candidates = [[buckets[n] for n in c] for c in candidates]
return [strip_unneeded(c, sufficient_funds) for c in candidates]
def choose_buckets(self, buckets, spent_amount, fee, penalty_func): def choose_buckets(self, buckets, sufficient_funds, penalty_func):
candidates = self.bucket_candidates(buckets, sufficient_funds)
def sufficient(buckets):
'''Given a set of buckets, return True if it has enough
value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets)
total_size = sum(bucket.size for bucket in buckets)
return total_input >= spent_amount + fee(total_size)
candidates = self.bucket_candidates(buckets, sufficient)
penalties = [penalty_func(cand) for cand in candidates] penalties = [penalty_func(cand) for cand in candidates]
winner = candidates[penalties.index(min(penalties))] winner = candidates[penalties.index(min(penalties))]
self.print_error("Bucket sets:", len(buckets)) self.print_error("Bucket sets:", len(buckets))
@ -176,15 +180,19 @@ class CoinChooserRandom(CoinChooserBase):
return winner return winner
class CoinChooserPrivacy(CoinChooserRandom): class CoinChooserPrivacy(CoinChooserRandom):
''' '''Attempts to better preserve user privacy. First, if any coin is
Attempts to better preserve user privacy. First, if any coin is
spent from a user address, all coins are. Compared to spending spent from a user address, all coins are. Compared to spending
from other addresses to make up an amount, this reduces from other addresses to make up an amount, this reduces
information leakage about sender holdings. It also helps to information leakage about sender holdings. It also helps to
reduce blockchain UTXO bloat, and reduce future privacy loss reduce blockchain UTXO bloat, and reduce future privacy loss that
that would come from reusing that address' remaining UTXOs. would come from reusing that address' remaining UTXOs. Second, it
Second, it penalizes change that is quite different to the sent penalizes change that is quite different to the sent amount.
amount. Third, it penalizes change that is too big.''' Third, it penalizes change that is too big. Fourth, it breaks
large change up into amounts comparable to the spent amount.
Finally, change is rounded to similar precision to sent amounts.
Extra change outputs and rounding might raise the transaction fee
slightly. Transaction priority might be less than if older coins
were chosen.'''
def keys(self, coins): def keys(self, coins):
return [coin['address'] for coin in coins] return [coin['address'] for coin in coins]
@ -213,5 +221,52 @@ class CoinChooserPrivacy(CoinChooserRandom):
return penalty return penalty
COIN_CHOOSERS = {'Classic': CoinChooserClassic, def change_amounts(self, tx, count, fee_estimator, dust_threshold):
# Break change up if bigger than max_change
output_amounts = [o[2] for o in tx.outputs]
max_change = max(max(output_amounts) * 1.25, dust_threshold * 10)
# Use N change outputs
for n in range(1, count + 1):
# How much is left if we add this many change outputs?
change_amount = max(0, tx.get_fee() - fee_estimator(n))
if change_amount // n <= max_change:
break
# Get a handle on the precision of the output amounts; round our
# change to look similar
def trailing_zeroes(val):
s = str(val)
return len(s) - len(s.rstrip('0'))
zeroes = map(trailing_zeroes, output_amounts)
min_zeroes = min(zeroes)
max_zeroes = max(zeroes)
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
# Calculate change; randomize it a bit if using more than 1 output
remaining = change_amount
amounts = []
while n > 1:
average = remaining // n
amount = randint(int(average * 0.7), int(average * 1.3))
precision = min(choice(zeroes), int(floor(log10(amount))))
amount = int(round(amount, -precision))
amounts.append(amount)
remaining -= amount
n -= 1
# Last change output. Round down to maximum precision but lose
# no more than 100 satoshis to fees (2dp)
N = pow(10, min(2, zeroes[0]))
amount = (remaining // N) * N
amounts.append(amount)
assert sum(amounts) <= change_amount
return amounts
COIN_CHOOSERS = {'Oldest First': CoinChooserOldestFirst,
'Privacy': CoinChooserPrivacy} 'Privacy': CoinChooserPrivacy}

View File

@ -74,21 +74,22 @@ def command(s):
class Commands: class Commands:
def __init__(self, config, wallet, network, callback = None): def __init__(self, config, wallet, network, callback = None, password=None, new_password=None):
self.config = config self.config = config
self.wallet = wallet self.wallet = wallet
self.network = network self.network = network
self._callback = callback self._callback = callback
self.password = None self._password = password
self.new_password = new_password
self.contacts = contacts.Contacts(self.config) self.contacts = contacts.Contacts(self.config)
def _run(self, method, args, password_getter): def _run(self, method, args, password_getter):
cmd = known_commands[method] cmd = known_commands[method]
if cmd.requires_password and self.wallet.use_encryption: if cmd.requires_password and self.wallet.use_encryption:
self.password = apply(password_getter,()) self._password = apply(password_getter,())
f = getattr(self, method) f = getattr(self, method)
result = f(*args) result = f(*args)
self.password = None self._password = None
if self._callback: if self._callback:
apply(self._callback, ()) apply(self._callback, ())
return result return result
@ -120,7 +121,9 @@ class Commands:
@command('wp') @command('wp')
def password(self): def password(self):
"""Change wallet password. """ """Change wallet password. """
raise BaseException('Not a JSON-RPC command') self.wallet.update_password(self._password, self.new_password)
self.wallet.storage.write()
return {'password':self.wallet.use_encryption}
@command('') @command('')
def getconfig(self, key): def getconfig(self, key):
@ -157,7 +160,7 @@ class Commands:
""" """
return self.network.synchronous_get(('blockchain.address.get_history', [address])) return self.network.synchronous_get(('blockchain.address.get_history', [address]))
@command('nw') @command('w')
def listunspent(self): def listunspent(self):
"""List unspent outputs. Returns the list of unspent transaction """List unspent outputs. Returns the list of unspent transaction
outputs in your wallet.""" outputs in your wallet."""
@ -200,7 +203,7 @@ class Commands:
outputs = map(lambda x: ('address', x[0], int(COIN*x[1])), outputs.items()) outputs = map(lambda x: ('address', x[0], int(COIN*x[1])), outputs.items())
tx = Transaction.from_io(tx_inputs, outputs) tx = Transaction.from_io(tx_inputs, outputs)
if not unsigned: if not unsigned:
self.wallet.sign_transaction(tx, self.password) self.wallet.sign_transaction(tx, self._password)
return tx.as_dict() return tx.as_dict()
@command('wp') @command('wp')
@ -212,7 +215,7 @@ class Commands:
pubkey = bitcoin.public_key_from_private_key(privkey) pubkey = bitcoin.public_key_from_private_key(privkey)
t.sign({pubkey:privkey}) t.sign({pubkey:privkey})
else: else:
self.wallet.sign_transaction(t, self.password) self.wallet.sign_transaction(t, self._password)
return t.as_dict() return t.as_dict()
@command('') @command('')
@ -250,7 +253,7 @@ class Commands:
"""Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.""" """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses."""
is_list = type(address) is list is_list = type(address) is list
domain = address if is_list else [address] domain = address if is_list else [address]
out = [self.wallet.get_private_key(address, self.password) for address in domain] out = [self.wallet.get_private_key(address, self._password) for address in domain]
return out if is_list else out[0] return out if is_list else out[0]
@command('w') @command('w')
@ -273,10 +276,9 @@ class Commands:
"""Return the public keys for a wallet address. """ """Return the public keys for a wallet address. """
return self.wallet.get_public_keys(address) return self.wallet.get_public_keys(address)
@command('nw') @command('w')
def getbalance(self, account=None): def getbalance(self, account=None):
"""Return the balance of your wallet. If run with the --offline flag, """Return the balance of your wallet. """
returns the last known balance."""
if account is None: if account is None:
c, u, x = self.wallet.get_balance() c, u, x = self.wallet.get_balance()
else: else:
@ -334,19 +336,19 @@ class Commands:
@command('wp') @command('wp')
def getmasterprivate(self): def getmasterprivate(self):
"""Get master private key. Return your wallet\'s master private key""" """Get master private key. Return your wallet\'s master private key"""
return str(self.wallet.get_master_private_key(self.wallet.root_name, self.password)) return str(self.wallet.get_master_private_key(self.wallet.root_name, self._password))
@command('wp') @command('wp')
def getseed(self): def getseed(self):
"""Get seed phrase. Print the generation seed of your wallet.""" """Get seed phrase. Print the generation seed of your wallet."""
s = self.wallet.get_mnemonic(self.password) s = self.wallet.get_mnemonic(self._password)
return s.encode('utf8') return s.encode('utf8')
@command('wp') @command('wp')
def importprivkey(self, privkey): def importprivkey(self, privkey):
"""Import a private key. """ """Import a private key. """
try: try:
addr = self.wallet.import_key(privkey, self.password) addr = self.wallet.import_key(privkey, self._password)
out = "Keypair imported: " + addr out = "Keypair imported: " + addr
except Exception as e: except Exception as e:
out = "Error: " + str(e) out = "Error: " + str(e)
@ -377,7 +379,7 @@ class Commands:
def signmessage(self, address, message): def signmessage(self, address, message):
"""Sign a message with a key. Use quotes if your message contains """Sign a message with a key. Use quotes if your message contains
whitespaces""" whitespaces"""
sig = self.wallet.sign_message(address, message, self.password) sig = self.wallet.sign_message(address, message, self._password)
return base64.b64encode(sig) return base64.b64encode(sig)
@command('') @command('')
@ -415,24 +417,24 @@ class Commands:
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
str(tx) #this serializes str(tx) #this serializes
if not unsigned: if not unsigned:
self.wallet.sign_transaction(tx, self.password) self.wallet.sign_transaction(tx, self._password)
return tx return tx
@command('wpn') @command('wp')
def payto(self, destination, amount, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False): def payto(self, destination, amount, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
"""Create a transaction. """ """Create a transaction. """
domain = [from_addr] if from_addr else None domain = [from_addr] if from_addr else None
tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned) tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned)
return tx.as_dict() return tx.as_dict()
@command('wpn') @command('wp')
def paytomany(self, outputs, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False): def paytomany(self, outputs, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
"""Create a multi-output transaction. """ """Create a multi-output transaction. """
domain = [from_addr] if from_addr else None domain = [from_addr] if from_addr else None
tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned) tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned)
return tx.as_dict() return tx.as_dict()
@command('wn') @command('w')
def history(self): def history(self):
"""Wallet history. Returns the transaction history of your wallet.""" """Wallet history. Returns the transaction history of your wallet."""
balance = 0 balance = 0
@ -443,7 +445,7 @@ class Commands:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception: except Exception:
time_str = "----" time_str = "----"
label, is_default_label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
out.append({ out.append({
'txid':tx_hash, 'txid':tx_hash,
'timestamp':timestamp, 'timestamp':timestamp,
@ -502,7 +504,7 @@ class Commands:
out.append(item) out.append(item)
return out return out
@command('nw') @command('w')
def gettransaction(self, txid): def gettransaction(self, txid):
"""Retrieve a transaction. """ """Retrieve a transaction. """
tx = self.wallet.transactions.get(txid) if self.wallet else None tx = self.wallet.transactions.get(txid) if self.wallet else None
@ -522,7 +524,7 @@ class Commands:
@command('wp') @command('wp')
def decrypt(self, pubkey, encrypted): def decrypt(self, pubkey, encrypted):
"""Decrypt a message encrypted with a public key.""" """Decrypt a message encrypted with a public key."""
return self.wallet.decrypt_message(pubkey, encrypted, self.password) return self.wallet.decrypt_message(pubkey, encrypted, self._password)
def _format_request(self, out): def _format_request(self, out):
pr_str = { pr_str = {
@ -535,7 +537,7 @@ class Commands:
out['status'] = pr_str[out.get('status', PR_UNKNOWN)] out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
return out return out
@command('wn') @command('w')
def getrequest(self, key): def getrequest(self, key):
"""Return a payment request""" """Return a payment request"""
r = self.wallet.get_payment_request(key, self.config) r = self.wallet.get_payment_request(key, self.config)
@ -548,7 +550,7 @@ class Commands:
# """<Not implemented>""" # """<Not implemented>"""
# pass # pass
@command('wn') @command('w')
def listrequests(self, pending=False, expired=False, paid=False): def listrequests(self, pending=False, expired=False, paid=False):
"""List the payment requests you made.""" """List the payment requests you made."""
out = self.wallet.get_sorted_requests(self.config) out = self.wallet.get_sorted_requests(self.config)
@ -565,7 +567,7 @@ class Commands:
return map(self._format_request, out) return map(self._format_request, out)
@command('w') @command('w')
def addrequest(self, requested_amount, memo='', expiration=60*60, force=False): def addrequest(self, amount, memo='', expiration=60*60, force=False):
"""Create a payment request.""" """Create a payment request."""
addr = self.wallet.get_unused_address(None) addr = self.wallet.get_unused_address(None)
if addr is None: if addr is None:
@ -573,7 +575,7 @@ class Commands:
addr = self.wallet.create_new_address(None, False) addr = self.wallet.create_new_address(None, False)
else: else:
return False return False
amount = int(Decimal(requested_amount)*COIN) amount = int(COIN*Decimal(amount))
expiration = int(expiration) expiration = int(expiration)
req = self.wallet.make_payment_request(addr, amount, memo, expiration) req = self.wallet.make_payment_request(addr, amount, memo, expiration)
self.wallet.add_payment_request(req, self.config) self.wallet.add_payment_request(req, self.config)
@ -587,7 +589,7 @@ class Commands:
if not alias: if not alias:
raise BaseException('No alias in your configuration') raise BaseException('No alias in your configuration')
alias_addr = self.contacts.resolve(alias)['address'] alias_addr = self.contacts.resolve(alias)['address']
self.wallet.sign_payment_request(address, alias, alias_addr, self.password) self.wallet.sign_payment_request(address, alias, alias_addr, self._password)
@command('w') @command('w')
def rmrequest(self, address): def rmrequest(self, address):
@ -664,15 +666,18 @@ command_options = {
} }
# don't use floats because of rounding errors
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
arg_types = { arg_types = {
'num': int, 'num': int,
'nbits': int, 'nbits': int,
'entropy': long, 'entropy': long,
'pubkeys': json.loads, 'tx': json_loads,
'inputs': json.loads, 'pubkeys': json_loads,
'outputs': json.loads, 'inputs': json_loads,
'tx_fee': lambda x: float(x) if x is not None else None, 'outputs': json_loads,
'amount': lambda x: float(x) if x!='!' else '!', 'tx_fee': lambda x: str(Decimal(x)) if x is not None else None,
'amount': lambda x: str(Decimal(x)) if x!='!' else '!',
} }
config_variables = { config_variables = {
@ -728,7 +733,6 @@ def get_parser():
group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Show debugging information") group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Show debugging information")
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory") group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path") group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
# create main parser # create main parser
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
parents=[parent_parser], parents=[parent_parser],
@ -739,6 +743,7 @@ def get_parser():
parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)") parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
#parser_gui.set_defaults(func=run_gui) #parser_gui.set_defaults(func=run_gui)
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'lite', 'gtk', 'kivy', 'text', 'stdio', 'jsonrpc']) parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'lite', 'gtk', 'kivy', 'text', 'stdio', 'jsonrpc'])
parser_gui.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup") parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI") parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
add_network_options(parser_gui) add_network_options(parser_gui)

View File

@ -103,7 +103,7 @@ class Daemon(DaemonThread):
'nodes': self.network.get_interfaces(), 'nodes': self.network.get_interfaces(),
'connected': self.network.is_connected(), 'connected': self.network.is_connected(),
'auto_connect': p[4], 'auto_connect': p[4],
'wallets': self.wallets.keys(), 'wallets': dict([ (k, w.is_up_to_date()) for k, w in self.wallets.items()]),
} }
elif sub == 'stop': elif sub == 'stop':
self.stop() self.stop()
@ -135,21 +135,19 @@ class Daemon(DaemonThread):
return wallet return wallet
def run_cmdline(self, config_options): def run_cmdline(self, config_options):
password = config_options.get('password')
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
cmdname = config.get('cmd') cmdname = config.get('cmd')
cmd = known_commands[cmdname] cmd = known_commands[cmdname]
wallet = self.load_wallet(config) if cmd.requires_wallet else None wallet = self.load_wallet(config) if cmd.requires_wallet else None
if wallet:
wallet.wait_until_synchronized()
# arguments passed to function # arguments passed to function
args = map(lambda x: config.get(x), cmd.params) args = map(lambda x: config.get(x), cmd.params)
# decode json arguments # decode json arguments
args = map(json_decode, args) args = map(json_decode, args)
# options # options
args += map(lambda x: config.get(x), cmd.options) args += map(lambda x: config.get(x), cmd.options)
cmd_runner = Commands(config, wallet, self.network) cmd_runner = Commands(config, wallet, self.network,
cmd_runner.password = password password=config_options.get('password'),
new_password=config_options.get('new_password'))
func = getattr(cmd_runner, cmd.name) func = getattr(cmd_runner, cmd.name)
result = func(*args) result = func(*args)
return result return result

View File

@ -12,7 +12,7 @@ proc = None
def scan_qr(config): def scan_qr(config):
global proc global proc
if not zbar: if not zbar:
raise BaseException("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo pip install zbar'")])) raise RuntimeError("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo pip install zbar'")]))
if proc is None: if proc is None:
device = config.get("video_device", "default") device = config.get("video_device", "default")
if device == 'default': if device == 'default':

View File

@ -179,5 +179,5 @@ class Synchronizer(ThreadJob):
if up_to_date != self.wallet.is_up_to_date(): if up_to_date != self.wallet.is_up_to_date():
self.wallet.set_up_to_date(up_to_date) self.wallet.set_up_to_date(up_to_date)
if up_to_date: if up_to_date:
self.wallet.save_transactions() self.wallet.save_transactions(write=True)
self.network.trigger_callback('updated') self.network.trigger_callback('updated')

View File

@ -57,7 +57,7 @@ class TestWalletStorage(WalletTestCase):
some_dict = {"a":"b", "c":"d"} some_dict = {"a":"b", "c":"d"}
for key, value in some_dict.items(): for key, value in some_dict.items():
storage.put(key, value, False) storage.put(key, value)
storage.write() storage.write()
contents = "" contents = ""

View File

@ -10,6 +10,8 @@ import urllib
import threading import threading
from i18n import _ from i18n import _
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
def normalize_version(v): def normalize_version(v):
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
@ -147,20 +149,20 @@ def json_encode(obj):
def json_decode(x): def json_decode(x):
try: try:
return json.loads(x) return json.loads(x, parse_float=decimal.Decimal)
except: except:
return x return x
# decorator that prints execution time # decorator that prints execution time
def profiler(func): def profiler(func):
def do_profile(func, args): def do_profile(func, args, kw_args):
n = func.func_name n = func.func_name
t0 = time.time() t0 = time.time()
o = apply(func, args) o = func(*args, **kw_args)
t = time.time() - t0 t = time.time() - t0
print_error("[profiler]", n, "%.4f"%t) print_error("[profiler]", n, "%.4f"%t)
return o return o
return lambda *args: do_profile(func, args) return lambda *args, **kw_args: do_profile(func, args, kw_args)
@ -317,7 +319,7 @@ def block_explorer_URL(config, kind, item):
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) #urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
def parse_URI(uri): def parse_URI(uri, on_pr=None):
import bitcoin import bitcoin
from bitcoin import COIN from bitcoin import COIN
@ -364,6 +366,22 @@ def parse_URI(uri):
if 'sig' in out: if 'sig' in out:
out['sig'] = bitcoin.base_decode(out['sig'], None, base=58).encode('hex') out['sig'] = bitcoin.base_decode(out['sig'], None, base=58).encode('hex')
r = out.get('r')
sig = out.get('sig')
name = out.get('name')
if r or (name and sig):
def get_payment_request_thread():
import paymentrequest as pr
if name and sig:
s = pr.serialize_request(out).SerializeToString()
request = pr.PaymentRequest(s)
else:
request = pr.get_payment_request(r)
on_pr(request)
t = threading.Thread(target=get_payment_request_thread)
t.setDaemon(True)
t.start()
return out return out
@ -561,11 +579,12 @@ class StoreDict(dict):
def check_www_dir(rdir): def check_www_dir(rdir):
# rewrite index.html every time
import urllib, urlparse, shutil, os import urllib, urlparse, shutil, os
if not os.path.exists(rdir): if not os.path.exists(rdir):
os.mkdir(rdir) os.mkdir(rdir)
index = os.path.join(rdir, 'index.html') index = os.path.join(rdir, 'index.html')
if not os.path.exists(index):
print_error("copying index.html")
src = os.path.join(os.path.dirname(__file__), 'www', 'index.html') src = os.path.join(os.path.dirname(__file__), 'www', 'index.html')
shutil.copy(src, index) shutil.copy(src, index)
files = [ files = [

View File

@ -26,7 +26,7 @@ import json
import copy import copy
from functools import partial from functools import partial
from util import PrintError, profiler from util import NotEnoughFunds, PrintError, profiler
from bitcoin import * from bitcoin import *
from account import * from account import *
@ -69,11 +69,11 @@ class WalletStorage(PrintError):
except: except:
try: try:
d = ast.literal_eval(data) #parse raw data from reading wallet file d = ast.literal_eval(data) #parse raw data from reading wallet file
labels = d.get('labels', {})
except Exception as e: except Exception as e:
raise IOError("Cannot read wallet file '%s'" % self.path) raise IOError("Cannot read wallet file '%s'" % self.path)
self.data = {} self.data = {}
# In old versions of Electrum labels were latin1 encoded, this fixes breakage. # In old versions of Electrum labels were latin1 encoded, this fixes breakage.
labels = d.get('labels', {})
for i, label in labels.items(): for i, label in labels.items():
try: try:
unicode(label) unicode(label)
@ -98,7 +98,7 @@ class WalletStorage(PrintError):
v = copy.deepcopy(v) v = copy.deepcopy(v)
return v return v
def put(self, key, value, save = True): def put(self, key, value):
try: try:
json.dumps(key) json.dumps(key)
json.dumps(value) json.dumps(value)
@ -113,8 +113,6 @@ class WalletStorage(PrintError):
elif key in self.data: elif key in self.data:
self.modified = True self.modified = True
self.data.pop(key) self.data.pop(key)
if save:
self.write()
def write(self): def write(self):
if threading.currentThread().isDaemon(): if threading.currentThread().isDaemon():
@ -142,7 +140,7 @@ class WalletStorage(PrintError):
import stat import stat
os.chmod(self.path, mode) os.chmod(self.path, mode)
self.print_error("saved", self.path) self.print_error("saved", self.path)
self.modified = False
class Abstract_Wallet(PrintError): class Abstract_Wallet(PrintError):
@ -199,7 +197,7 @@ class Abstract_Wallet(PrintError):
# save wallet type the first time # save wallet type the first time
if self.storage.get('wallet_type') is None: if self.storage.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type, True) self.storage.put('wallet_type', self.wallet_type)
def diagnostic_name(self): def diagnostic_name(self):
return self.basename() return self.basename()
@ -220,17 +218,18 @@ class Abstract_Wallet(PrintError):
self.transactions.pop(tx_hash) self.transactions.pop(tx_hash)
@profiler @profiler
def save_transactions(self): def save_transactions(self, write=False):
with self.transaction_lock: with self.transaction_lock:
tx = {} tx = {}
for k,v in self.transactions.items(): for k,v in self.transactions.items():
tx[k] = str(v) tx[k] = str(v)
# Flush storage only with the last put self.storage.put('transactions', tx)
self.storage.put('transactions', tx, False) self.storage.put('txi', self.txi)
self.storage.put('txi', self.txi, False) self.storage.put('txo', self.txo)
self.storage.put('txo', self.txo, False) self.storage.put('pruned_txo', self.pruned_txo)
self.storage.put('pruned_txo', self.pruned_txo, False) self.storage.put('addr_history', self.history)
self.storage.put('addr_history', self.history, True) if write:
self.storage.write()
def clear_history(self): def clear_history(self):
with self.transaction_lock: with self.transaction_lock:
@ -376,7 +375,7 @@ class Abstract_Wallet(PrintError):
if changed: if changed:
run_hook('set_label', self, name, text) run_hook('set_label', self, name, text)
self.storage.put('labels', self.labels, True) self.storage.put('labels', self.labels)
return changed return changed
@ -436,7 +435,7 @@ class Abstract_Wallet(PrintError):
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
with self.lock: with self.lock:
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos) self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos)
self.storage.put('verified_tx3', self.verified_tx, True) self.storage.put('verified_tx3', self.verified_tx)
conf, timestamp = self.get_confirmations(tx_hash) conf, timestamp = self.get_confirmations(tx_hash)
self.network.trigger_callback('verified', tx_hash, conf, timestamp) self.network.trigger_callback('verified', tx_hash, conf, timestamp)
@ -867,24 +866,9 @@ class Abstract_Wallet(PrintError):
return h2 return h2
def get_label(self, tx_hash): def get_label(self, tx_hash):
label = self.labels.get(tx_hash) label = self.labels.get(tx_hash, '')
is_default = (label == '') or (label is None) return label
if is_default:
label = self.get_default_label(tx_hash)
return label, is_default
def get_default_label(self, tx_hash):
if self.txi.get(tx_hash) == {}:
d = self.txo.get(tx_hash, {})
labels = []
for addr in d.keys():
label = self.labels.get(addr)
if label:
labels.append(label)
return ', '.join(labels)
return ''
def fee_per_kb(self, config): def fee_per_kb(self, config):
b = config.get('dynamic_fees') b = config.get('dynamic_fees')
@ -899,7 +883,7 @@ class Abstract_Wallet(PrintError):
def coin_chooser_name(self, config): def coin_chooser_name(self, config):
kind = config.get('coin_chooser') kind = config.get('coin_chooser')
if not kind in COIN_CHOOSERS: if not kind in COIN_CHOOSERS:
kind = 'Classic' kind = 'Oldest First'
return kind return kind
def coin_chooser(self, config): def coin_chooser(self, config):
@ -912,6 +896,10 @@ class Abstract_Wallet(PrintError):
if type == 'address': if type == 'address':
assert is_address(data), "Address " + data + " is invalid!" assert is_address(data), "Address " + data + " is invalid!"
# Avoid index-out-of-range with coins[0] below
if not coins:
raise NotEnoughFunds()
for item in coins: for item in coins:
self.add_input_info(item) self.add_input_info(item)
@ -1044,7 +1032,7 @@ class Abstract_Wallet(PrintError):
if self.has_seed(): if self.has_seed():
decoded = self.get_seed(old_password) decoded = self.get_seed(old_password)
self.seed = pw_encode( decoded, new_password) self.seed = pw_encode( decoded, new_password)
self.storage.put('seed', self.seed, True) self.storage.put('seed', self.seed)
imported_account = self.accounts.get(IMPORTED_ACCOUNT) imported_account = self.accounts.get(IMPORTED_ACCOUNT)
if imported_account: if imported_account:
@ -1056,10 +1044,10 @@ class Abstract_Wallet(PrintError):
b = pw_decode(v, old_password) b = pw_decode(v, old_password)
c = pw_encode(b, new_password) c = pw_encode(b, new_password)
self.master_private_keys[k] = c self.master_private_keys[k] = c
self.storage.put('master_private_keys', self.master_private_keys, True) self.storage.put('master_private_keys', self.master_private_keys)
self.use_encryption = (new_password != None) self.use_encryption = (new_password != None)
self.storage.put('use_encryption', self.use_encryption,True) self.storage.put('use_encryption', self.use_encryption)
def is_frozen(self, addr): def is_frozen(self, addr):
return addr in self.frozen_addresses return addr in self.frozen_addresses
@ -1071,7 +1059,7 @@ class Abstract_Wallet(PrintError):
self.frozen_addresses |= set(addrs) self.frozen_addresses |= set(addrs)
else: else:
self.frozen_addresses -= set(addrs) self.frozen_addresses -= set(addrs)
self.storage.put('frozen_addresses', list(self.frozen_addresses), True) self.storage.put('frozen_addresses', list(self.frozen_addresses))
return True return True
return False return False
@ -1109,7 +1097,8 @@ class Abstract_Wallet(PrintError):
self.verifier = None self.verifier = None
# Now no references to the syncronizer or verifier # Now no references to the syncronizer or verifier
# remain so they will be GC-ed # remain so they will be GC-ed
self.storage.put('stored_height', self.get_local_height(), True) self.storage.put('stored_height', self.get_local_height())
self.storage.write()
def wait_until_synchronized(self, callback=None): def wait_until_synchronized(self, callback=None):
from i18n import _ from i18n import _
@ -1147,7 +1136,7 @@ class Abstract_Wallet(PrintError):
d = {} d = {}
for k, v in self.accounts.items(): for k, v in self.accounts.items():
d[k] = v.dump() d[k] = v.dump()
self.storage.put('accounts', d, True) self.storage.put('accounts', d)
def can_import(self): def can_import(self):
return not self.is_watching_only() return not self.is_watching_only()
@ -1431,9 +1420,9 @@ class Deterministic_Wallet(Abstract_Wallet):
else: else:
self.use_encryption = False self.use_encryption = False
self.storage.put('seed', self.seed, False) self.storage.put('seed', self.seed)
self.storage.put('seed_version', self.seed_version, False) self.storage.put('seed_version', self.seed_version)
self.storage.put('use_encryption', self.use_encryption,True) self.storage.put('use_encryption', self.use_encryption)
def get_seed(self, password): def get_seed(self, password):
return pw_decode(self.seed, password) return pw_decode(self.seed, password)
@ -1445,7 +1434,7 @@ class Deterministic_Wallet(Abstract_Wallet):
assert isinstance(value, int), 'gap limit must be of type int, not of %s'%type(value) assert isinstance(value, int), 'gap limit must be of type int, not of %s'%type(value)
if value >= self.gap_limit: if value >= self.gap_limit:
self.gap_limit = value self.gap_limit = value
self.storage.put('gap_limit', self.gap_limit, True) self.storage.put('gap_limit', self.gap_limit)
return True return True
elif value >= self.min_acceptable_gap(): elif value >= self.min_acceptable_gap():
@ -1456,7 +1445,7 @@ class Deterministic_Wallet(Abstract_Wallet):
account.receiving_pubkeys = account.receiving_pubkeys[0:n] account.receiving_pubkeys = account.receiving_pubkeys[0:n]
account.receiving_addresses = account.receiving_addresses[0:n] account.receiving_addresses = account.receiving_addresses[0:n]
self.gap_limit = value self.gap_limit = value
self.storage.put('gap_limit', self.gap_limit, True) self.storage.put('gap_limit', self.gap_limit)
self.save_accounts() self.save_accounts()
return True return True
else: else:
@ -1579,11 +1568,11 @@ class BIP32_Wallet(Deterministic_Wallet):
if xpub in self.master_public_keys.values(): if xpub in self.master_public_keys.values():
raise BaseException('Duplicate master public key') raise BaseException('Duplicate master public key')
self.master_public_keys[name] = xpub self.master_public_keys[name] = xpub
self.storage.put('master_public_keys', self.master_public_keys, True) self.storage.put('master_public_keys', self.master_public_keys)
def add_master_private_key(self, name, xpriv, password): def add_master_private_key(self, name, xpriv, password):
self.master_private_keys[name] = pw_encode(xpriv, password) self.master_private_keys[name] = pw_encode(xpriv, password)
self.storage.put('master_private_keys', self.master_private_keys, True) self.storage.put('master_private_keys', self.master_private_keys)
def derive_xkeys(self, root, derivation, password): def derive_xkeys(self, root, derivation, password):
x = self.master_private_keys[root] x = self.master_private_keys[root]
@ -1626,16 +1615,16 @@ class BIP32_Simple_Wallet(BIP32_Wallet):
def create_xprv_wallet(self, xprv, password): def create_xprv_wallet(self, xprv, password):
xpub = bitcoin.xpub_from_xprv(xprv) xpub = bitcoin.xpub_from_xprv(xprv)
account = BIP32_Account({'xpub':xpub}) account = BIP32_Account({'xpub':xpub})
self.storage.put('seed_version', self.seed_version, True) self.storage.put('seed_version', self.seed_version)
self.add_master_private_key(self.root_name, xprv, password) self.add_master_private_key(self.root_name, xprv, password)
self.add_master_public_key(self.root_name, xpub) self.add_master_public_key(self.root_name, xpub)
self.add_account('0', account) self.add_account('0', account)
self.use_encryption = (password != None) self.use_encryption = (password != None)
self.storage.put('use_encryption', self.use_encryption,True) self.storage.put('use_encryption', self.use_encryption)
def create_xpub_wallet(self, xpub): def create_xpub_wallet(self, xpub):
account = BIP32_Account({'xpub':xpub}) account = BIP32_Account({'xpub':xpub})
self.storage.put('seed_version', self.seed_version, True) self.storage.put('seed_version', self.seed_version)
self.add_master_public_key(self.root_name, xpub) self.add_master_public_key(self.root_name, xpub)
self.add_account('0', account) self.add_account('0', account)
@ -1834,7 +1823,7 @@ class OldWallet(Deterministic_Wallet):
def create_master_keys(self, password): def create_master_keys(self, password):
seed = self.get_seed(password) seed = self.get_seed(password)
mpk = OldAccount.mpk_from_seed(seed) mpk = OldAccount.mpk_from_seed(seed)
self.storage.put('master_public_key', mpk, True) self.storage.put('master_public_key', mpk)
def get_master_public_key(self): def get_master_public_key(self):
return self.storage.get("master_public_key") return self.storage.get("master_public_key")
@ -1852,8 +1841,8 @@ class OldWallet(Deterministic_Wallet):
def create_watching_only_wallet(self, mpk): def create_watching_only_wallet(self, mpk):
self.seed_version = OLD_SEED_VERSION self.seed_version = OLD_SEED_VERSION
self.storage.put('seed_version', self.seed_version, False) self.storage.put('seed_version', self.seed_version)
self.storage.put('master_public_key', mpk, True) self.storage.put('master_public_key', mpk)
self.create_account(mpk) self.create_account(mpk)
def get_seed(self, password): def get_seed(self, password):
@ -2037,7 +2026,7 @@ class Wallet(object):
@classmethod @classmethod
def from_multisig(klass, key_list, password, storage, wallet_type): def from_multisig(klass, key_list, password, storage, wallet_type):
storage.put('wallet_type', wallet_type, True) storage.put('wallet_type', wallet_type)
self = Multisig_Wallet(storage) self = Multisig_Wallet(storage)
key_list = sorted(key_list, key = lambda x: klass.is_xpub(x)) key_list = sorted(key_list, key = lambda x: klass.is_xpub(x))
for i, text in enumerate(key_list): for i, text in enumerate(key_list):
@ -2056,7 +2045,7 @@ class Wallet(object):
else: else:
self.add_cosigner_seed(text, name, password) self.add_cosigner_seed(text, name, password)
self.use_encryption = (password != None) self.use_encryption = (password != None)
self.storage.put('use_encryption', self.use_encryption, True) self.storage.put('use_encryption', self.use_encryption)
self.create_main_account(password) self.create_main_account(password)
return self return self

View File

@ -1,18 +1,18 @@
from electrum.plugins import BasePlugin, hook from functools import partial
from electrum_gui.qt.util import WaitingDialog, EnterButton
from electrum.util import print_msg, print_error
from electrum.i18n import _
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import traceback
import zlib import zlib
import json import json
from io import BytesIO from io import BytesIO
import sys import sys
import platform import platform
from electrum.plugins import BasePlugin, hook
from electrum_gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog
from electrum.util import print_msg, print_error
from electrum.i18n import _
from PyQt4.QtGui import *
from PyQt4.QtCore import *
try: try:
import amodem.audio import amodem.audio
import amodem.main import amodem.main
@ -42,11 +42,10 @@ class Plugin(BasePlugin):
return True return True
def settings_widget(self, window): def settings_widget(self, window):
return EnterButton(_('Settings'), self.settings_dialog) return EnterButton(_('Settings'), partial(self.settings_dialog, window))
def settings_dialog(self): def settings_dialog(self, window):
d = QDialog() d = WindowModalDialog(window, _("Audio Modem Settings"))
d.setWindowTitle("Settings")
layout = QGridLayout(d) layout = QGridLayout(d)
layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0) layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0)
@ -75,24 +74,20 @@ class Plugin(BasePlugin):
def handler(): def handler():
blob = json.dumps(dialog.tx.as_dict()) blob = json.dumps(dialog.tx.as_dict())
self.sender = self._send(parent=dialog, blob=blob) self._send(parent=dialog, blob=blob)
self.sender.start()
b.clicked.connect(handler) b.clicked.connect(handler)
dialog.sharing_buttons.insert(-1, b) dialog.sharing_buttons.insert(-1, b)
@hook @hook
def scan_text_edit(self, parent): def scan_text_edit(self, parent):
def handler(): parent.addButton(':icons/microphone.png', partial(self._recv, parent),
self.receiver = self._recv(parent=parent) _("Read from microphone"))
self.receiver.start()
parent.addButton(':icons/microphone.png', handler, _("Read from microphone"))
@hook @hook
def show_text_edit(self, parent): def show_text_edit(self, parent):
def handler(): def handler():
blob = str(parent.toPlainText()) blob = str(parent.toPlainText())
self.sender = self._send(parent=parent, blob=blob) self._send(parent=parent, blob=blob)
self.sender.start()
parent.addButton(':icons/speaker.png', handler, _("Send to speaker")) parent.addButton(':icons/speaker.png', handler, _("Send to speaker"))
def _audio_interface(self): def _audio_interface(self):
@ -101,31 +96,25 @@ class Plugin(BasePlugin):
def _send(self, parent, blob): def _send(self, parent, blob):
def sender_thread(): def sender_thread():
try:
with self._audio_interface() as interface: with self._audio_interface() as interface:
src = BytesIO(blob) src = BytesIO(blob)
dst = interface.player() dst = interface.player()
amodem.main.send(config=self.modem_config, src=src, dst=dst) amodem.main.send(config=self.modem_config, src=src, dst=dst)
except Exception:
traceback.print_exc()
print_msg('Sending:', repr(blob)) print_msg('Sending:', repr(blob))
blob = zlib.compress(blob) blob = zlib.compress(blob)
kbps = self.modem_config.modem_bps / 1e3 kbps = self.modem_config.modem_bps / 1e3
msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps) msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps)
return WaitingDialog(parent=parent, message=msg, run_task=sender_thread) WaitingDialog(parent, msg, sender_thread)
def _recv(self, parent): def _recv(self, parent):
def receiver_thread(): def receiver_thread():
try:
with self._audio_interface() as interface: with self._audio_interface() as interface:
src = interface.recorder() src = interface.recorder()
dst = BytesIO() dst = BytesIO()
amodem.main.recv(config=self.modem_config, src=src, dst=dst) amodem.main.recv(config=self.modem_config, src=src, dst=dst)
return dst.getvalue() return dst.getvalue()
except Exception:
traceback.print_exc()
def on_success(blob): def on_success(blob):
if blob: if blob:
@ -135,5 +124,4 @@ class Plugin(BasePlugin):
kbps = self.modem_config.modem_bps / 1e3 kbps = self.modem_config.modem_bps / 1e3
msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps) msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps)
return WaitingDialog(parent=parent, message=msg, WaitingDialog(parent, msg, receiver_thread, on_success=on_success)
run_task=receiver_thread, on_success=on_success)

View File

@ -18,12 +18,10 @@
from __future__ import absolute_import from __future__ import absolute_import
import socket
import time import time
import threading import threading
import base64 import base64
from decimal import Decimal from functools import partial
from Queue import Queue
import smtplib import smtplib
import imaplib import imaplib
@ -37,12 +35,11 @@ from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore import PyQt4.QtCore as QtCore
import PyQt4.QtGui as QtGui import PyQt4.QtGui as QtGui
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
from electrum.plugins import BasePlugin, hook from electrum.plugins import BasePlugin, hook
from electrum import util
from electrum.paymentrequest import PaymentRequest from electrum.paymentrequest import PaymentRequest
from electrum.i18n import _ from electrum.i18n import _
from electrum_gui.qt.util import text_dialog, EnterButton from electrum_gui.qt.util import EnterButton, Buttons, CloseButton
from electrum_gui.qt.util import OkButton, WindowModalDialog
@ -166,14 +163,10 @@ class Plugin(BasePlugin):
return True return True
def settings_widget(self, window): def settings_widget(self, window):
self.settings_window = window return EnterButton(_('Settings'), partial(self.settings_dialog, window))
return EnterButton(_('Settings'), self.settings_dialog)
def settings_dialog(self, x): def settings_dialog(self, window):
from electrum_gui.qt.util import Buttons, CloseButton, OkButton d = WindowModalDialog(window, _("Email settings"))
d = QDialog(self.settings_window)
d.setWindowTitle("Email settings")
d.setMinimumSize(500, 200) d.setMinimumSize(500, 200)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)

View File

@ -346,13 +346,18 @@ class FxPlugin(BasePlugin, ThreadJob):
return _("No data") return _("No data")
@hook @hook
def historical_value_str(self, satoshis, d_t): def history_rate(self, d_t):
rate = self.exchange.historical_rate(self.ccy, d_t) rate = self.exchange.historical_rate(self.ccy, d_t)
# Frequently there is no rate for today, until tomorrow :) # Frequently there is no rate for today, until tomorrow :)
# Use spot quotes in that case # Use spot quotes in that case
if rate is None and (datetime.today().date() - d_t.date()).days <= 2: if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
rate = self.exchange.quotes.get(self.ccy) rate = self.exchange.quotes.get(self.ccy)
self.history_used_spot = True self.history_used_spot = True
return rate
@hook
def historical_value_str(self, satoshis, d_t):
rate = self.history_rate(d_t)
return self.value_str(satoshis, rate) return self.value_str(satoshis, rate)
@hook @hook

View File

@ -128,11 +128,10 @@ class Plugin(FxPlugin):
window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers) window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
def settings_widget(self, window): def settings_widget(self, window):
return EnterButton(_('Settings'), self.settings_dialog) return EnterButton(_('Settings'), partial(self.settings_dialog, window))
def settings_dialog(self): def settings_dialog(self, window):
d = QDialog() d = WindowModalDialog(window, _("Exchange Rate Settings"))
d.setWindowTitle("Settings")
layout = QGridLayout(d) layout = QGridLayout(d)
layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0) layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
layout.addWidget(QLabel(_('Currency: ')), 1, 0) layout.addWidget(QLabel(_('Currency: ')), 1, 0)

View File

@ -21,7 +21,7 @@ import urllib
import sys import sys
import requests import requests
from PyQt4.QtGui import QMessageBox, QApplication, QPushButton from PyQt4.QtGui import QApplication, QPushButton
from electrum.plugins import BasePlugin, hook from electrum.plugins import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
@ -65,7 +65,7 @@ class Plugin(BasePlugin):
'to verify that transaction is instant.\n' 'to verify that transaction is instant.\n'
'Please enter your password to sign a\n' 'Please enter your password to sign a\n'
'verification request.') 'verification request.')
password = window.password_dialog(msg) password = window.password_dialog(msg, parent=d)
if not password: if not password:
return return
try: try:
@ -84,14 +84,12 @@ class Plugin(BasePlugin):
# 3. display the result # 3. display the result
if response.get('verified'): if response.get('verified'):
QMessageBox.information(None, _('Verification successful!'), d.show_message(_('%s is covered by GreenAddress instant confirmation') % (tx.hash()), title=_('Verification successful!'))
_('%s is covered by GreenAddress instant confirmation') % (tx.hash()), _('OK'))
else: else:
QMessageBox.critical(None, _('Verification failed!'), d.show_critical(_('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), title=_('Verification failed!'))
_('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), _('OK'))
except BaseException as e: except BaseException as e:
import traceback import traceback
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
QMessageBox.information(None, _('Error'), str(e), _('OK')) d.show_error(str(e))
finally: finally:
d.verify_button.setText(self.button_label) d.verify_button.setText(self.button_label)

View File

@ -1,4 +1,4 @@
from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL, QGridLayout, QInputDialog, QPushButton
import PyQt4.QtCore as QtCore import PyQt4.QtCore as QtCore
from electrum_gui.qt.util import * from electrum_gui.qt.util import *
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
@ -26,11 +26,11 @@ class Plugin(KeepKeyPlugin):
try: try:
self.get_client().ping('t') self.get_client().ping('t')
except BaseException as e: except BaseException as e:
QMessageBox.information(window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) window.show_error(_('KeepKey device not detected.\nContinuing in watching-only mode.\nReason:\n' + str(e)))
self.wallet.force_watching_only = True self.wallet.force_watching_only = True
return return
if self.wallet.addresses() and not self.wallet.check_proper_device(): if self.wallet.addresses() and not self.wallet.check_proper_device():
QMessageBox.information(window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK')) window.show_error(_("This wallet does not match your KeepKey device"))
self.wallet.force_watching_only = True self.wallet.force_watching_only = True
@hook @hook
@ -73,7 +73,7 @@ class Plugin(KeepKeyPlugin):
return return
get_label = lambda: self.get_client().features.label get_label = lambda: self.get_client().features.label
update_label = lambda: current_label_label.setText("Label: %s" % get_label()) update_label = lambda: current_label_label.setText("Label: %s" % get_label())
d = QDialog() d = WindowModalDialog(window, _("KeepKey Settings"))
layout = QGridLayout(d) layout = QGridLayout(d)
layout.addWidget(QLabel("KeepKey Options"),0,0) layout.addWidget(QLabel("KeepKey Options"),0,0)
layout.addWidget(QLabel("ID:"),1,0) layout.addWidget(QLabel("ID:"),1,0)
@ -132,10 +132,7 @@ class KeepKeyQtHandler:
return self.passphrase return self.passphrase
def pin_dialog(self): def pin_dialog(self):
d = QDialog(None) d = WindowModalDialog(self.win, _("Enter PIN"))
d.setModal(1)
d.setWindowTitle(_("Enter PIN"))
d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
matrix = PinMatrixWidget() matrix = PinMatrixWidget()
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget(QLabel(self.message)) vbox.addWidget(QLabel(self.message))
@ -153,23 +150,18 @@ class KeepKeyQtHandler:
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
else: else:
assert type(self.win) is InstallWizard assert type(self.win) is InstallWizard
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog from electrum_gui.qt.password_dialog import PasswordDialog
d = QDialog() d = PasswordDialog(self.win, None, None, self.message, False)
d.setModal(1) confirmed, p, passphrase = d.run()
d.setLayout(make_password_dialog(d, None, self.message, False))
confirmed, p, passphrase = run_password_dialog(d, None, None)
if not confirmed: if not confirmed:
QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) self.win.show_critical(_("Password request canceled"))
self.passphrase = None self.passphrase = None
else: else:
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
self.done.set() self.done.set()
def message_dialog(self): def message_dialog(self):
self.d = QDialog() self.d = WindowModalDialog(self.win, _('Please Check KeepKey Device'))
self.d.setModal(1)
self.d.setWindowTitle('Please Check KeepKey Device')
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
l = QLabel(self.message) l = QLabel(self.message)
vbox = QVBoxLayout(self.d) vbox = QVBoxLayout(self.d)
vbox.addWidget(l) vbox.addWidget(l)
@ -182,5 +174,3 @@ class KeepKeyQtHandler:
def dialog_stop(self): def dialog_stop(self):
self.d.hide() self.d.hide()

View File

@ -42,9 +42,9 @@ class LabelsPlugin(BasePlugin):
self.set_nonce(wallet, nonce) self.set_nonce(wallet, nonce)
return nonce return nonce
def set_nonce(self, wallet, nonce, force_write=True): def set_nonce(self, wallet, nonce):
self.print_error("set", wallet.basename(), "nonce to", nonce) self.print_error("set", wallet.basename(), "nonce to", nonce)
wallet.storage.put("wallet_nonce", nonce, force_write) wallet.storage.put("wallet_nonce", nonce)
@hook @hook
def set_label(self, wallet, item, label): def set_label(self, wallet, item, label):
@ -61,7 +61,7 @@ class LabelsPlugin(BasePlugin):
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
# Caller will write the wallet # Caller will write the wallet
self.set_nonce(wallet, nonce + 1, force_write=False) self.set_nonce(wallet, nonce + 1)
def do_request(self, method, url = "/labels", is_batch=False, data=None): def do_request(self, method, url = "/labels", is_batch=False, data=None):
url = 'https://' + self.target_host + url url = 'https://' + self.target_host + url
@ -125,8 +125,8 @@ class LabelsPlugin(BasePlugin):
self.print_error("received %d labels" % len(response)) self.print_error("received %d labels" % len(response))
# do not write to disk because we're in a daemon thread # do not write to disk because we're in a daemon thread
wallet.storage.put('labels', wallet.labels, False) wallet.storage.put('labels', wallet.labels)
self.set_nonce(wallet, response["nonce"] + 1, False) self.set_nonce(wallet, response["nonce"] + 1)
self.on_pulled(wallet) self.on_pulled(wallet)
except Exception as e: except Exception as e:

View File

@ -6,7 +6,8 @@ from PyQt4.QtCore import *
from electrum.plugins import hook from electrum.plugins import hook
from electrum.i18n import _ from electrum.i18n import _
from electrum_gui.qt import EnterButton from electrum_gui.qt import EnterButton
from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton
from electrum_gui.qt.util import WindowModalDialog, OkButton
from labels import LabelsPlugin from labels import LabelsPlugin
@ -25,25 +26,23 @@ class Plugin(LabelsPlugin):
partial(self.settings_dialog, window)) partial(self.settings_dialog, window))
def settings_dialog(self, window): def settings_dialog(self, window):
d = QDialog(window) wallet = window.parent().wallet
d = WindowModalDialog(window, _("Label Settings"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
layout = QGridLayout() layout = QGridLayout()
vbox.addLayout(layout) vbox.addLayout(layout)
layout.addWidget(QLabel("Label sync options: "), 2, 0) layout.addWidget(QLabel("Label sync options: "), 2, 0)
self.upload = ThreadedButton("Force upload", self.upload = ThreadedButton("Force upload",
partial(self.push_thread, window.wallet), partial(self.push_thread, wallet),
self.done_processing) self.done_processing)
layout.addWidget(self.upload, 2, 1) layout.addWidget(self.upload, 2, 1)
self.download = ThreadedButton("Force download", self.download = ThreadedButton("Force download",
partial(self.pull_thread, window.wallet, True), partial(self.pull_thread, wallet, True),
self.done_processing) self.done_processing)
layout.addWidget(self.download, 2, 2) layout.addWidget(self.download, 2, 2)
self.accept = OkButton(d, _("Done")) self.accept = OkButton(d, _("Done"))
vbox.addLayout(Buttons(CancelButton(d), self.accept)) vbox.addLayout(Buttons(CancelButton(d), self.accept))
if d.exec_(): return bool(d.exec_())
return True
else:
return False
def on_pulled(self, wallet): def on_pulled(self, wallet):
self.obj.emit(SIGNAL('labels_changed'), wallet) self.obj.emit(SIGNAL('labels_changed'), wallet)

View File

@ -1,7 +1,7 @@
from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL from PyQt4.Qt import QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, SIGNAL
import PyQt4.QtCore as QtCore import PyQt4.QtCore as QtCore
import threading
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
from electrum.plugins import BasePlugin, hook from electrum.plugins import BasePlugin, hook
from ledger import LedgerPlugin from ledger import LedgerPlugin
@ -16,10 +16,10 @@ class Plugin(LedgerPlugin):
self.handler = BTChipQTHandler(window) self.handler = BTChipQTHandler(window)
if self.btchip_is_connected(): if self.btchip_is_connected():
if not self.wallet.check_proper_device(): if not self.wallet.check_proper_device():
QMessageBox.information(window, _('Error'), _("This wallet does not match your Ledger device"), _('OK')) window.show_error(_("This wallet does not match your Ledger device"))
self.wallet.force_watching_only = True self.wallet.force_watching_only = True
else: else:
QMessageBox.information(window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK')) window.show_error(_("Ledger device not detected.\nContinuing in watching-only mode."))
self.wallet.force_watching_only = True self.wallet.force_watching_only = True

View File

@ -1,4 +1,4 @@
from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL, QGridLayout, QInputDialog, QPushButton
import PyQt4.QtCore as QtCore import PyQt4.QtCore as QtCore
from electrum_gui.qt.util import * from electrum_gui.qt.util import *
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
@ -46,10 +46,7 @@ class TrezorQtHandler:
return self.passphrase return self.passphrase
def pin_dialog(self): def pin_dialog(self):
d = QDialog(None) d = WindowModalDialog(self.win, _("Enter PIN"))
d.setModal(1)
d.setWindowTitle(_("Enter PIN"))
d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
matrix = PinMatrixWidget() matrix = PinMatrixWidget()
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addWidget(QLabel(self.message)) vbox.addWidget(QLabel(self.message))
@ -67,23 +64,18 @@ class TrezorQtHandler:
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
else: else:
assert type(self.win) is InstallWizard assert type(self.win) is InstallWizard
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog from electrum_gui.qt.password_dialog import PasswordDialog
d = QDialog() d = PasswordDialog(self.win, None, None, self.message, False)
d.setModal(1) confirmed, p, passphrase = d.run()
d.setLayout(make_password_dialog(d, None, self.message, False))
confirmed, p, passphrase = run_password_dialog(d, None, None)
if not confirmed: if not confirmed:
QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) self.win.show_critical(_("Password request canceled"))
self.passphrase = None self.passphrase = None
else: else:
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
self.done.set() self.done.set()
def message_dialog(self): def message_dialog(self):
self.d = QDialog() self.d = WindowModalDialog(self.win, _('Please Check Trezor Device'))
self.d.setModal(1)
self.d.setWindowTitle('Please Check Trezor Device')
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
l = QLabel(self.message) l = QLabel(self.message)
vbox = QVBoxLayout(self.d) vbox = QVBoxLayout(self.d)
vbox.addWidget(l) vbox.addWidget(l)
@ -108,11 +100,11 @@ class Plugin(TrezorPlugin):
try: try:
self.get_client().ping('t') self.get_client().ping('t')
except BaseException as e: except BaseException as e:
QMessageBox.information(window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) window.show_error(_('Trezor device not detected.\nContinuing in watching-only mode.\nReason:\n' + str(e)))
self.wallet.force_watching_only = True self.wallet.force_watching_only = True
return return
if self.wallet.addresses() and not self.wallet.check_proper_device(): if self.wallet.addresses() and not self.wallet.check_proper_device():
QMessageBox.information(window, _('Error'), _("This wallet does not match your Trezor device"), _('OK')) window.show_error(_("This wallet does not match your Trezor device"))
self.wallet.force_watching_only = True self.wallet.force_watching_only = True
@hook @hook
@ -171,7 +163,7 @@ class Plugin(TrezorPlugin):
return return
get_label = lambda: self.get_client().features.label get_label = lambda: self.get_client().features.label
update_label = lambda: current_label_label.setText("Label: %s" % get_label()) update_label = lambda: current_label_label.setText("Label: %s" % get_label())
d = QDialog() d = WindowModalDialog(window, _("Trezor Settings"))
layout = QGridLayout(d) layout = QGridLayout(d)
layout.addWidget(QLabel("Trezor Options"),0,0) layout.addWidget(QLabel("Trezor Options"),0,0)
layout.addWidget(QLabel("ID:"),1,0) layout.addWidget(QLabel("ID:"),1,0)
@ -194,7 +186,3 @@ class Plugin(TrezorPlugin):
layout.addWidget(current_label_label,3,0) layout.addWidget(current_label_label,3,0)
layout.addWidget(change_label_button,3,1) layout.addWidget(change_label_button,3,1)
d.exec_() d.exec_()

View File

@ -1,4 +1,23 @@
#!/usr/bin/env python
#
# Electrum - Lightweight Bitcoin Client
# Copyright (C) 2015 Thomas Voegtlin
#
# 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/>.
from functools import partial from functools import partial
from threading import Thread
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtCore import * from PyQt4.QtCore import *
@ -10,7 +29,19 @@ from electrum_gui.qt.main_window import StatusBarButton
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugins import hook from electrum.plugins import hook
from trustedcoin import TrustedCoinPlugin from trustedcoin import TrustedCoinPlugin, Wallet_2fa
def need_server(wallet, tx):
from electrum.account import BIP32_Account
# Detect if the server is needed
long_id, short_id = wallet.get_user_id()
xpub3 = wallet.master_public_keys['x3/']
for x in tx.inputs_to_sign():
if x[0:2] == 'ff':
xpub, sequence = BIP32_Account.parse_xpubkey(x)
if xpub == xpub3:
return True
return False
class Plugin(TrustedCoinPlugin): class Plugin(TrustedCoinPlugin):
@ -27,8 +58,7 @@ class Plugin(TrustedCoinPlugin):
t.start() t.start()
def auth_dialog(self, window): def auth_dialog(self, window):
d = QDialog(window) d = WindowModalDialog(window, _("Authorization"))
d.setModal(1)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
pw = AmountEdit(None, is_int = True) pw = AmountEdit(None, is_int = True)
msg = _('Please enter your Google Authenticator code') msg = _('Please enter your Google Authenticator code')
@ -55,16 +85,18 @@ class Plugin(TrustedCoinPlugin):
self.print_error("twofactor: xpub3 not needed") self.print_error("twofactor: xpub3 not needed")
window.wallet.auth_code = auth_code window.wallet.auth_code = auth_code
def waiting_dialog(self, window, on_success=None):
task = partial(self.request_billing_info, window.wallet)
return WaitingDialog(window, 'Getting billing information...', task,
on_success=on_success)
@hook @hook
def abort_send(self, window): def abort_send(self, window):
wallet = window.wallet wallet = window.wallet
if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server(): if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server():
if wallet.billing_info is None: if wallet.billing_info is None:
# request billing info before forming the transaction # request billing info before forming the transaction
task = partial(self.request_billing_info, wallet) waiting_dialog(self, window).wait()
waiting_dialog = WaitingDialog(window, 'please wait...', task)
waiting_dialog.start()
waiting_dialog.wait()
if wallet.billing_info is None: if wallet.billing_info is None:
window.show_message('Could not contact server') window.show_message('Could not contact server')
return True return True
@ -72,9 +104,8 @@ class Plugin(TrustedCoinPlugin):
def settings_dialog(self, window): def settings_dialog(self, window):
task = partial(self.request_billing_info, window.wallet) on_success = partial(self.show_settings_dialog, window)
self.waiting_dialog = WaitingDialog(window, 'please wait...', task, partial(self.show_settings_dialog, window)) self.waiting_dialog(window, on_success)
self.waiting_dialog.start()
def show_settings_dialog(self, window, success): def show_settings_dialog(self, window, success):
if not success: if not success:
@ -82,8 +113,7 @@ class Plugin(TrustedCoinPlugin):
return return
wallet = window.wallet wallet = window.wallet
d = QDialog(window) d = WindowModalDialog(window, _("TrustedCoin Information"))
d.setWindowTitle("TrustedCoin Information")
d.setMinimumSize(500, 200) d.setMinimumSize(500, 200)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
hbox = QHBoxLayout() hbox = QHBoxLayout()
@ -238,7 +268,5 @@ class Plugin(TrustedCoinPlugin):
server.auth(_id, otp) server.auth(_id, otp)
return True return True
except: except:
QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK')) window.show_message(_('Incorrect password'))
pw.setText('') pw.setText('')

View File

@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from threading import Thread
import socket import socket
import os import os
import re import re
@ -270,18 +269,6 @@ def make_billing_address(wallet, num):
address = public_key_to_bc_address( cK ) address = public_key_to_bc_address( cK )
return address return address
def need_server(wallet, tx):
from electrum.account import BIP32_Account
# Detect if the server is needed
long_id, short_id = wallet.get_user_id()
xpub3 = wallet.master_public_keys['x3/']
for x in tx.inputs_to_sign():
if x[0:2] == 'ff':
xpub, sequence = BIP32_Account.parse_xpubkey(x)
if xpub == xpub3:
return True
return False
class TrustedCoinPlugin(BasePlugin): class TrustedCoinPlugin(BasePlugin):
@ -318,8 +305,8 @@ class TrustedCoinPlugin(BasePlugin):
return return
password = window.password_dialog() password = window.password_dialog()
wallet.storage.put('seed_version', wallet.seed_version, True) wallet.storage.put('seed_version', wallet.seed_version)
wallet.storage.put('use_encryption', password is not None, True) wallet.storage.put('use_encryption', password is not None)
words = seed.split() words = seed.split()
n = len(words)/2 n = len(words)/2