Merge branch 'master' of https://github.com/spesmilo/electrum
This commit is contained in:
commit
93bcd98763
|
@ -2,6 +2,9 @@
|
|||
* separation between plugins and GUIs
|
||||
* the daemon supports jsonrpc commands
|
||||
* 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
|
||||
* increase MIN_RELAY_TX_FEE to avoid dust transactions
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
#!/usr/bin/python2
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys, re, shutil, os, hashlib
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
import sys
|
||||
import re
|
||||
import hashlib
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
|
||||
import sys, re, shutil, os, hashlib
|
||||
import imp
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/python2
|
||||
|
||||
import sys, re, shutil, os, hashlib
|
||||
import imp
|
||||
|
|
239
electrum
239
electrum
|
@ -107,9 +107,74 @@ def init_gui(config, network, plugins):
|
|||
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')
|
||||
cmd = known_commands[cmdname]
|
||||
|
||||
|
@ -130,57 +195,11 @@ def init_cmdline(config):
|
|||
# instanciate wallet for command-line
|
||||
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:
|
||||
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")
|
||||
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
|
||||
if cmd.name in ['getprivatekeys']:
|
||||
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.")
|
||||
|
||||
# commands needing password
|
||||
if cmd.requires_password and wallet.use_encryption:
|
||||
if cmd.requires_password and storage.get('use_encryption'):
|
||||
if config.get('password'):
|
||||
password = config.get('password')
|
||||
else:
|
||||
|
@ -196,55 +215,49 @@ def init_cmdline(config):
|
|||
if not password:
|
||||
print_msg("Error: Password required")
|
||||
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
|
||||
if cmd.requires_password and storage.get('use_encryption'):
|
||||
password = config_options.get('password')
|
||||
try:
|
||||
seed = wallet.check_password(password)
|
||||
except InvalidPassword:
|
||||
print_msg("Error: This password does not decode this wallet.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
password = None
|
||||
|
||||
# run the command
|
||||
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):
|
||||
if cmd.requires_network:
|
||||
print_stderr("Warning: running command offline")
|
||||
# notify plugins
|
||||
always_hook('cmdline_load_wallet', wallet)
|
||||
# arguments passed to function
|
||||
args = map(lambda x: config.get(x), cmd.params)
|
||||
# decode json arguments
|
||||
args = map(json_decode, args)
|
||||
# options
|
||||
args += map(lambda x: config.get(x), cmd.options)
|
||||
cmd_runner = Commands(config, wallet, None)
|
||||
cmd_runner.password = password
|
||||
cmd_runner = Commands(config, wallet, None,
|
||||
password=config_options.get('password'),
|
||||
new_password=config_options.get('new_password'))
|
||||
func = getattr(cmd_runner, cmd.name)
|
||||
result = func(*args)
|
||||
# save wallet
|
||||
if wallet:
|
||||
wallet.storage.write()
|
||||
return result
|
||||
|
||||
|
||||
|
@ -308,43 +321,24 @@ if __name__ == '__main__':
|
|||
config_options['url'] = uri
|
||||
|
||||
config = SimpleConfig(config_options)
|
||||
cmd_name = config.get('cmd')
|
||||
cmdname = config.get('cmd')
|
||||
|
||||
# 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)
|
||||
|
||||
# run command offline
|
||||
if cmd_name not in ['gui', 'daemon']:
|
||||
cmd, password, wallet = init_cmdline(config)
|
||||
if not cmd.requires_network or config.get('offline'):
|
||||
result = run_offline_command(config, cmd, wallet, password)
|
||||
print_msg(json_encode(result))
|
||||
# run non-RPC commands separately
|
||||
if cmdname in ['create', 'restore', 'deseed']:
|
||||
run_non_RPC(config)
|
||||
sys.exit(0)
|
||||
else:
|
||||
config_options['password'] = password
|
||||
|
||||
# check if a daemon is running
|
||||
server = get_daemon(config)
|
||||
|
||||
# daemon is running
|
||||
if cmdname == 'gui':
|
||||
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)
|
||||
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'):
|
||||
network = Network(config)
|
||||
network.start()
|
||||
|
@ -357,7 +351,10 @@ if __name__ == '__main__':
|
|||
gui.main()
|
||||
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')
|
||||
if subcommand in ['status', 'stop']:
|
||||
print_msg("Daemon not running")
|
||||
|
@ -376,12 +373,32 @@ if __name__ == '__main__':
|
|||
util.check_www_dir(config.get('requests_dir'))
|
||||
daemon.start()
|
||||
daemon.join()
|
||||
sys.exit(0)
|
||||
else:
|
||||
print_stderr("starting daemon (PID %d)"%p)
|
||||
sys.exit(0)
|
||||
else:
|
||||
print_msg("syntax: electrum daemon <start|status|stop>")
|
||||
sys.exit(1)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -332,7 +332,7 @@ def get_history_values(n):
|
|||
except Exception:
|
||||
time_str = 'pending'
|
||||
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('>','')
|
||||
values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value, True), ' ' + label))
|
||||
|
||||
|
|
|
@ -1185,7 +1185,7 @@ class ElectrumWindow:
|
|||
time_str = 'pending'
|
||||
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 ''
|
||||
details = self.get_tx_details(tx_hash)
|
||||
|
||||
|
@ -1300,7 +1300,7 @@ class ElectrumGui():
|
|||
gap = self.config.get('gap_limit', 5)
|
||||
if gap != 5:
|
||||
wallet.gap_limit = gap
|
||||
wallet.storage.put('gap_limit', gap, True)
|
||||
wallet.storage.put('gap_limit', gap)
|
||||
|
||||
if action == 'create':
|
||||
seed = wallet.make_seed()
|
||||
|
|
|
@ -29,6 +29,7 @@ except ImportError:
|
|||
|
||||
# minimum required version for kivy
|
||||
kivy.require('1.8.0')
|
||||
from electrum.i18n import set_language
|
||||
from kivy.logger import Logger
|
||||
from main_window import ElectrumWindow
|
||||
|
||||
|
@ -39,6 +40,7 @@ class ElectrumGui:
|
|||
self.network = network
|
||||
self.config = config
|
||||
self.plugins = plugins
|
||||
set_language(config.get('language'))
|
||||
|
||||
def main(self):
|
||||
w = ElectrumWindow(config=self.config,
|
||||
|
|
125
gui/kivy/main.kv
125
gui/kivy/main.kv
|
@ -1,8 +1,11 @@
|
|||
#:import Clock kivy.clock.Clock
|
||||
#:import Window kivy.core.window.Window
|
||||
#:import Factory kivy.factory.Factory
|
||||
#:import _ electrum.i18n._
|
||||
|
||||
# Custom Global Widgets
|
||||
<Button>
|
||||
on_parent: self.MIN_STATE_TIME = 0.1
|
||||
|
||||
<VGridLayout@GridLayout>:
|
||||
rows: 1
|
||||
|
@ -187,22 +190,44 @@
|
|||
size: self.size
|
||||
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>
|
||||
icon: 'atlas://gui/kivy/theming/light/globe'
|
||||
values: [] #app.wallet.addresses() if app.wallet else []
|
||||
text: _("Select Your address")
|
||||
|
||||
<AmountButton@Button>:
|
||||
<BlueButton@Button>:
|
||||
background_color: .238, .585, .878, 0
|
||||
halign: 'left'
|
||||
text_size: (self.width-10, None)
|
||||
size_hint: 0.5, None
|
||||
default_text: 'Amount'
|
||||
default_text: ''
|
||||
text: self.default_text
|
||||
padding: '5dp', '5db'
|
||||
height: '40dp'
|
||||
text_color: self.foreground_color
|
||||
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>
|
||||
|
@ -222,24 +247,26 @@
|
|||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
on_release:
|
||||
self.parent.update_text(self.parent, self.text)
|
||||
self.parent.update_amount(self.text)
|
||||
|
||||
|
||||
|
||||
<TabbedPanelStrip>:
|
||||
on_parent:
|
||||
if self.parent: self.parent.bar_width = 0
|
||||
if self.parent: self.parent.scroll_x = 0.5
|
||||
|
||||
|
||||
<TabbedCarousel>
|
||||
carousel: carousel
|
||||
do_default_tab: False
|
||||
Carousel:
|
||||
scroll_timeout: 190
|
||||
scroll_timeout: 250
|
||||
scroll_distance: '20dp'
|
||||
anim_type: 'out_quart'
|
||||
min_move: .05
|
||||
anim_move_duration: .1
|
||||
anim_cancel_duration: .54
|
||||
scroll_distance: '10dp'
|
||||
on_index: root.on_index(*args)
|
||||
id: carousel
|
||||
|
||||
|
@ -281,65 +308,59 @@
|
|||
TabbedCarousel:
|
||||
id: panel
|
||||
tab_height: '48dp'
|
||||
#default_tab: send_tab
|
||||
tab_width: panel.width/3
|
||||
default_tab: history_tab
|
||||
strip_border: 0, 0, 0, 0
|
||||
HistoryScreen:
|
||||
id: history_screen
|
||||
tab: history_tab
|
||||
InvoicesScreen:
|
||||
id: invoices_screen
|
||||
tab: invoices_tab
|
||||
SendScreen:
|
||||
id: send_screen
|
||||
tab: send_tab
|
||||
HistoryScreen:
|
||||
id: history_screen
|
||||
tab: history_tab
|
||||
ReceiveScreen:
|
||||
id: receive_screen
|
||||
tab: receive_tab
|
||||
ContactsScreen:
|
||||
id: contacts_screen
|
||||
tab: contacts_tab
|
||||
RequestsScreen:
|
||||
id: requests_screen
|
||||
tab: requests_tab
|
||||
#ContactsScreen:
|
||||
# id: contacts_screen
|
||||
# tab: contacts_tab
|
||||
CleanHeader:
|
||||
id: history_tab
|
||||
text: _('History')
|
||||
id: invoices_tab
|
||||
text: _('Invoices')
|
||||
slide: 0
|
||||
CleanHeader:
|
||||
id: send_tab
|
||||
text: _('Send')
|
||||
slide: 1
|
||||
CleanHeader:
|
||||
id: receive_tab
|
||||
text: _('Receive')
|
||||
id: history_tab
|
||||
text: _('History')
|
||||
slide: 2
|
||||
CleanHeader:
|
||||
id: contacts_tab
|
||||
text: _('Contacts')
|
||||
id: receive_tab
|
||||
text: _('Receive')
|
||||
slide: 3
|
||||
CleanHeader:
|
||||
id: requests_tab
|
||||
text: _('Requests')
|
||||
slide: 4
|
||||
#CleanHeader:
|
||||
# id: contacts_tab
|
||||
# text: _('Contacts')
|
||||
# slide: 4
|
||||
|
||||
<ActionOvrButton@ActionButton>
|
||||
on_release:
|
||||
if self.parent: self.parent.parent.dismiss()
|
||||
app.popup_dialog(self.name)
|
||||
|
||||
|
||||
<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'
|
||||
Clock.schedule_once(lambda dt: self.parent.parent.dismiss() if self.parent else None, 0.05)
|
||||
Clock.schedule_once(lambda dt: app.popup_dialog(self.name), 0.05)
|
||||
|
||||
|
||||
BoxLayout:
|
||||
|
||||
orientation: 'vertical'
|
||||
|
||||
canvas.before:
|
||||
|
@ -373,34 +394,26 @@ BoxLayout:
|
|||
.format(app.status)
|
||||
font_size: '22dp'
|
||||
minimum_width: '1dp'
|
||||
|
||||
ActionButton:
|
||||
id: context_button
|
||||
text: app.context
|
||||
width: 0
|
||||
on_text:
|
||||
self.width = 20 if self.text else 0
|
||||
on_release: app.context_action()
|
||||
on_release: app.popup_dialog('status')
|
||||
|
||||
ActionOverflow:
|
||||
id: ao
|
||||
ActionOvrButton:
|
||||
text: _('Network')
|
||||
name: 'about'
|
||||
text: _('About')
|
||||
ActionOvrButton:
|
||||
name: 'network'
|
||||
text: _('Network')
|
||||
on_parent:
|
||||
# when widget overflow drop down is shown, adjust the width
|
||||
parent = args[1]
|
||||
if parent: ao._dropdown.width = sp(200)
|
||||
ActionOvrButton:
|
||||
name: 'settings'
|
||||
text: _('Settings')
|
||||
ActionOvrButton:
|
||||
name: 'wallets'
|
||||
text: _('Wallets')
|
||||
ActionOvrButton:
|
||||
name: 'plugins'
|
||||
text: _('Plugins')
|
||||
|
||||
name: 'settings'
|
||||
text: _('Settings')
|
||||
ScreenManager:
|
||||
id: manager
|
||||
ScreenTabs:
|
||||
|
|
|
@ -7,11 +7,13 @@ from decimal import Decimal
|
|||
|
||||
import electrum
|
||||
from electrum import WalletStorage, Wallet
|
||||
from electrum.i18n import _, set_language
|
||||
from electrum.i18n import _
|
||||
from electrum.contacts import Contacts
|
||||
from electrum.paymentrequest import InvoiceStore
|
||||
from electrum.util import profiler, InvalidPassword
|
||||
from electrum.plugins import run_hook
|
||||
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.core.window import Window
|
||||
|
@ -31,6 +33,7 @@ Factory.register('InstallWizard',
|
|||
Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs')
|
||||
Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens')
|
||||
|
||||
|
||||
#from kivy.core.window import Window
|
||||
#Window.softinput_mode = 'below_target'
|
||||
|
||||
|
@ -54,8 +57,8 @@ from kivy.core.clipboard import Clipboard
|
|||
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):
|
||||
|
||||
|
@ -72,13 +75,6 @@ class ElectrumWindow(App):
|
|||
self.history_screen.update()
|
||||
|
||||
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('')
|
||||
fiat_unit = StringProperty('')
|
||||
|
||||
|
@ -165,6 +161,9 @@ class ElectrumWindow(App):
|
|||
self.nfcscanner = None
|
||||
self.tabs = None
|
||||
|
||||
self.receive_address = None
|
||||
self.current_invoice = None
|
||||
|
||||
super(ElectrumWindow, self).__init__(**kwargs)
|
||||
|
||||
title = _('Electrum App')
|
||||
|
@ -176,6 +175,7 @@ class ElectrumWindow(App):
|
|||
|
||||
#self.config = self.gui_object.config
|
||||
self.contacts = Contacts(self.electrum_config)
|
||||
self.invoices = InvoiceStore(self.electrum_config)
|
||||
|
||||
self.bind(url=self.set_URI)
|
||||
# were we sent a url?
|
||||
|
@ -191,15 +191,60 @@ class ElectrumWindow(App):
|
|||
self._trigger_notify_transactions = \
|
||||
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):
|
||||
try:
|
||||
url = electrum.util.parse_URI(url)
|
||||
url = electrum.util.parse_URI(url, self.on_pr)
|
||||
except:
|
||||
self.show_info("Invalid URI", url)
|
||||
return
|
||||
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):
|
||||
from jnius import autoclass
|
||||
from android import activity
|
||||
|
@ -212,31 +257,10 @@ class ElectrumWindow(App):
|
|||
if resultCode == -1: # RESULT_OK:
|
||||
contents = intent.getStringExtra("SCAN_RESULT")
|
||||
if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE':
|
||||
try:
|
||||
uri = electrum.util.parse_URI(contents)
|
||||
except:
|
||||
self.show_info("Invalid URI", url)
|
||||
return
|
||||
on_complete(uri)
|
||||
on_complete(contents)
|
||||
activity.bind(on_activity_result=on_qr_result)
|
||||
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):
|
||||
return Builder.load_file('gui/kivy/main.kv')
|
||||
|
@ -273,30 +297,46 @@ class ElectrumWindow(App):
|
|||
win.bind(keyboard_height=self.on_keyboard_height)
|
||||
|
||||
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
|
||||
storage = WalletStorage(config.get_wallet_path())
|
||||
|
||||
storage = WalletStorage(wallet_path)
|
||||
Logger.info('Electrum: Check for existing wallet')
|
||||
|
||||
if storage.file_exists:
|
||||
wallet = Wallet(storage)
|
||||
action = wallet.get_action()
|
||||
else:
|
||||
action = 'new'
|
||||
|
||||
if action is not None:
|
||||
# start installation wizard
|
||||
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
||||
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)
|
||||
else:
|
||||
wallet.start_threads(self.network)
|
||||
self.on_wizard_complete(None, wallet)
|
||||
|
||||
self.load_wallet(wallet)
|
||||
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):
|
||||
self.stop_wallet()
|
||||
|
||||
def stop_wallet(self):
|
||||
if self.wallet:
|
||||
self.wallet.stop_threads()
|
||||
|
||||
|
@ -316,7 +356,6 @@ class ElectrumWindow(App):
|
|||
active_widg = self.root.children[0]
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
try:
|
||||
fw = self._focused_widget
|
||||
except AttributeError:
|
||||
|
@ -350,29 +389,25 @@ class ElectrumWindow(App):
|
|||
self.gui.main_gui.toggle_settings(self)
|
||||
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):
|
||||
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.open()
|
||||
|
||||
|
||||
|
||||
@profiler
|
||||
def init_ui(self):
|
||||
''' Initialize The Ux part of electrum. This function performs the basic
|
||||
tasks of setting up the ui.
|
||||
'''
|
||||
from weakref import ref
|
||||
set_language(self.electrum_config.get('language'))
|
||||
|
||||
self.funds_error = False
|
||||
# setup UX
|
||||
|
@ -401,7 +436,8 @@ class ElectrumWindow(App):
|
|||
interests = ['updated', 'status', 'new_transaction']
|
||||
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):
|
||||
if event == 'updated':
|
||||
|
@ -418,7 +454,7 @@ class ElectrumWindow(App):
|
|||
self.update_wallet()
|
||||
# Once GUI has been initialized check if we want to announce something
|
||||
# since the callback has been called before the GUI was initialized
|
||||
self.update_history_tab()
|
||||
self.update_tabs()
|
||||
self.notify_transactions()
|
||||
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)
|
||||
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):
|
||||
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
|
||||
def update_wallet(self, *dt):
|
||||
self._trigger_update_status()
|
||||
if self.wallet.up_to_date or not self.network or not self.network.is_connected():
|
||||
self.update_history_tab()
|
||||
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()
|
||||
#if self.wallet.up_to_date or not self.network or not self.network.is_connected():
|
||||
self.update_tabs()
|
||||
|
||||
|
||||
@profiler
|
||||
|
@ -593,64 +588,10 @@ class ElectrumWindow(App):
|
|||
else:
|
||||
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):
|
||||
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):
|
||||
if frozen:
|
||||
|
@ -660,15 +601,6 @@ class ElectrumWindow(App):
|
|||
entry.disabled = False
|
||||
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,
|
||||
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))
|
||||
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.tx_hash = tx_hash
|
||||
popup.tx_hash = obj.tx_hash
|
||||
popup.open()
|
||||
|
||||
def tx_selected(self, txid, state):
|
||||
if state == 'down':
|
||||
self.context = 'tx'
|
||||
self.context_action = lambda: self.tx_dialog(txid)
|
||||
else:
|
||||
self.reset_context()
|
||||
def address_dialog(self, screen):
|
||||
pass
|
||||
|
||||
def reset_context(self):
|
||||
self.context = ''
|
||||
self.context_action = lambda: None
|
||||
def description_dialog(self, screen):
|
||||
from uix.dialogs.label_dialog import LabelDialog
|
||||
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):
|
||||
popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv')
|
||||
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
|
||||
|
||||
from uix.dialogs.amount_dialog import AmountDialog
|
||||
amount = screen.amount
|
||||
if amount:
|
||||
a, u = str(amount).split()
|
||||
amount, u = str(amount).split()
|
||||
assert u == self.base_unit
|
||||
popup.ids.kb.amount = a
|
||||
|
||||
def cb():
|
||||
o = popup.ids.a.btc_text
|
||||
screen.amount = o
|
||||
|
||||
popup.on_dismiss = cb
|
||||
def cb(amount):
|
||||
screen.amount = amount
|
||||
popup = AmountDialog(show_max, amount, cb)
|
||||
popup.open()
|
||||
|
||||
def protected(self, f, args):
|
||||
|
@ -804,12 +725,9 @@ class ElectrumWindow(App):
|
|||
self.show_error("PIN numbers do not match")
|
||||
|
||||
def password_dialog(self, title, f, args):
|
||||
popup = Builder.load_file('gui/kivy/uix/ui_screens/password.kv')
|
||||
popup.title = title
|
||||
def callback():
|
||||
pw = popup.ids.kb.password
|
||||
from uix.dialogs.password_dialog import PasswordDialog
|
||||
def callback(pw):
|
||||
Clock.schedule_once(lambda x: apply(f, args + (pw,)), 0.1)
|
||||
popup.on_dismiss = callback
|
||||
popup = PasswordDialog(title, callback)
|
||||
popup.open()
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -144,6 +144,7 @@ class InfoBubble(Factory.Bubble):
|
|||
m.add_widget(self)
|
||||
else:
|
||||
Window.add_widget(self)
|
||||
|
||||
# wait for the bubble to adjust it's size according to text then animate
|
||||
Clock.schedule_once(lambda dt: self._show(pos, duration))
|
||||
|
||||
|
|
|
@ -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
|
||||
title: _('Amount')
|
||||
|
||||
AnchorLayout:
|
||||
anchor_x: 'center'
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
size_hint: 0.8, 1
|
||||
|
@ -18,7 +21,7 @@ Popup:
|
|||
id: a
|
||||
btc_text: (kb.amount + ' ' + app.base_unit) if kb.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
|
||||
font_size: '22dp'
|
||||
Widget:
|
||||
|
@ -30,8 +33,8 @@ Popup:
|
|||
is_fiat: False
|
||||
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)
|
||||
update_text: app.update_amount
|
||||
size_hint: 1, None
|
||||
update_amount: popup.update_amount
|
||||
height: '300dp'
|
||||
cols: 3
|
||||
KButton:
|
||||
|
@ -60,6 +63,8 @@ Popup:
|
|||
text: '<'
|
||||
Button:
|
||||
id: but_max
|
||||
opacity: 1 if root.show_max else 0
|
||||
disabled: not root.show_max
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
text: 'Max'
|
||||
|
@ -70,9 +75,9 @@ Popup:
|
|||
id: button_fiat
|
||||
size_hint: 1, None
|
||||
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:
|
||||
app.toggle_fiat(kb)
|
||||
popup.toggle_fiat(kb)
|
||||
Button:
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
|
@ -80,18 +85,53 @@ Popup:
|
|||
on_release:
|
||||
kb.amount = ''
|
||||
kb.fiat_amount = ''
|
||||
|
||||
Widget:
|
||||
size_hint: 1, None
|
||||
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
Widget:
|
||||
size_hint: 2, None
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
Button:
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
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
|
|
@ -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
|
|
@ -43,7 +43,6 @@ Builder.load_string('''
|
|||
on_release: if self.root: self.root.dispatch('on_release', self)
|
||||
|
||||
|
||||
|
||||
<-CreateAccountDialog>
|
||||
text_color: .854, .925, .984, 1
|
||||
auto_dismiss: False
|
||||
|
@ -140,11 +139,11 @@ Builder.load_string('''
|
|||
height: self.minimum_height
|
||||
CreateAccountButton:
|
||||
id: create
|
||||
text: _('Create a Wallet')
|
||||
text: _('Create a new seed')
|
||||
root: root
|
||||
CreateAccountButton:
|
||||
id: restore
|
||||
text: _('I already have a wallet')
|
||||
text: _('I already have a seed')
|
||||
root: root
|
||||
|
||||
|
||||
|
@ -457,8 +456,7 @@ class RestoreSeedDialog(CreateAccountDialog):
|
|||
self._trigger_check_seed = Clock.create_trigger(self.check_seed)
|
||||
|
||||
def check_seed(self, dt):
|
||||
self.ids.next.disabled = not bool(self._wizard.is_any(
|
||||
self.ids.text_input_seed))
|
||||
self.ids.next.disabled = not bool(self._wizard.is_any(self.ids.text_input_seed))
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
|
|
|
@ -73,7 +73,9 @@ class InstallWizard(Widget):
|
|||
def is_any(self, seed_e):
|
||||
text = self.get_seed_text(seed_e)
|
||||
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_private_key(text))
|
||||
|
||||
|
@ -129,8 +131,8 @@ class InstallWizard(Widget):
|
|||
if Wallet.is_seed(seed):
|
||||
return self.password_dialog(wallet=wallet, mode='restore',
|
||||
seed=seed)
|
||||
elif Wallet.is_mpk(seed):
|
||||
wallet = Wallet.from_mpk(seed, self.storage)
|
||||
elif Wallet.is_xpub(seed):
|
||||
wallet = Wallet.from_xpub(seed, self.storage)
|
||||
elif Wallet.is_address(seed):
|
||||
wallet = Wallet.from_address(seed, self.storage)
|
||||
elif Wallet.is_private_key(seed):
|
||||
|
@ -257,18 +259,19 @@ class InstallWizard(Widget):
|
|||
new_password = None
|
||||
|
||||
if mode == 'restore':
|
||||
wallet = Wallet.from_seed(seed, self.storage)
|
||||
password = (unicode(ti_password.text)
|
||||
if wallet and wallet.use_encryption else
|
||||
None)
|
||||
password = unicode(ti_password.text)
|
||||
# if wallet and wallet.use_encryption else
|
||||
# None)
|
||||
if not password:
|
||||
password = None
|
||||
wallet = Wallet.from_text(seed, password, self.storage)
|
||||
|
||||
def on_complete(*l):
|
||||
wallet.create_accounts(new_password)
|
||||
self.load_network(wallet, mode='restore')
|
||||
_dlg.close()
|
||||
|
||||
self.waiting_dialog(lambda: wallet.add_seed(seed, new_password),
|
||||
msg=_("saving seed"),
|
||||
self.waiting_dialog(wallet.synchronize,
|
||||
msg=_("generating addresses"),
|
||||
on_complete=on_complete)
|
||||
return
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
title: _('Enter PIN Code')
|
||||
size_hint: 0.9, 0.9
|
||||
|
@ -16,9 +24,9 @@ Popup:
|
|||
|
||||
GridLayout:
|
||||
id: kb
|
||||
update_text: app.update_password
|
||||
update_amount: popup.update_password
|
||||
password: ''
|
||||
on_password: if len(self.password) == 6: popup.dismiss()
|
||||
on_password: popup.on_password(self.password)
|
||||
size_hint: 1, None
|
||||
height: '300dp'
|
||||
cols: 3
|
||||
|
@ -49,3 +57,28 @@ Popup:
|
|||
|
||||
Widget:
|
||||
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)
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -42,14 +42,9 @@ Builder.load_string('''
|
|||
class QRCodeWidget(FloatLayout):
|
||||
|
||||
data = StringProperty(None, allow_none=True)
|
||||
|
||||
background_color = ListProperty((1, 1, 1, 1))
|
||||
|
||||
foreground_color = ListProperty((0, 0, 0, 0))
|
||||
|
||||
|
||||
#loading_image = StringProperty('gui/kivy/theming/loading.gif')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(QRCodeWidget, self).__init__(**kwargs)
|
||||
self.data = None
|
||||
|
@ -57,21 +52,11 @@ class QRCodeWidget(FloatLayout):
|
|||
self._qrtexture = None
|
||||
|
||||
def on_data(self, instance, value):
|
||||
print "on data", value
|
||||
if not (self.canvas or value):
|
||||
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()
|
||||
|
||||
def set_data(self, data):
|
||||
print "set data", data
|
||||
if self.data == data:
|
||||
return
|
||||
MinSize = 210 if len(data) < 128 else 500
|
||||
|
@ -98,7 +83,7 @@ class QRCodeWidget(FloatLayout):
|
|||
# currently unused, do we need this?
|
||||
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')
|
||||
# don't interpolate texture
|
||||
texture.min_filter = 'nearest'
|
||||
|
@ -107,32 +92,24 @@ class QRCodeWidget(FloatLayout):
|
|||
def update_texture(self):
|
||||
if not self.qr:
|
||||
return
|
||||
|
||||
matrix = self.qr.get_matrix()
|
||||
k = len(matrix)
|
||||
# create the texture in main UI thread otherwise
|
||||
# this will lead to memory corruption
|
||||
Clock.schedule_once(partial(self._create_texture, k), -1)
|
||||
# create the texture
|
||||
self._create_texture(k)
|
||||
buff = []
|
||||
bext = buff.extend
|
||||
cr, cg, cb, ca = self.background_color[:]
|
||||
cr, cg, cb = cr*255, cg*255, cb*255
|
||||
|
||||
for r 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
|
||||
buff = ''.join(map(chr, buff))
|
||||
# update texture in UI thread.
|
||||
Clock.schedule_once(lambda dt: self._upd_texture(buff), .1)
|
||||
# update texture
|
||||
self._upd_texture(buff)
|
||||
|
||||
def _upd_texture(self, buff):
|
||||
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')
|
||||
img =self.ids.qrimage
|
||||
img.anim_delay = -1
|
||||
|
|
|
@ -17,17 +17,24 @@ from kivy.lang import Builder
|
|||
from kivy.factory import Factory
|
||||
|
||||
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.util import timestamp_to_datetime
|
||||
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):
|
||||
|
||||
__events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
|
||||
action_view = ObjectProperty(None)
|
||||
loaded = False
|
||||
kvname = None
|
||||
context_menu = None
|
||||
menu_actions = []
|
||||
app = App.get_running_app()
|
||||
|
||||
def _change_action_view(self):
|
||||
|
@ -65,8 +72,17 @@ class CScreen(Factory.Screen):
|
|||
self.dispatch('on_deactivate')
|
||||
|
||||
def on_deactivate(self):
|
||||
pass
|
||||
#Clock.schedule_once(lambda dt: self._change_action_view())
|
||||
self.hide_menu()
|
||||
|
||||
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):
|
||||
|
@ -77,10 +93,18 @@ class HistoryScreen(CScreen):
|
|||
def __init__(self, **kwargs):
|
||||
self.ra_dialog = None
|
||||
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):
|
||||
for item in items:
|
||||
|
@ -98,49 +122,43 @@ class HistoryScreen(CScreen):
|
|||
time_str = _('pending')
|
||||
icon = "atlas://gui/kivy/theming/light/unconfirmed"
|
||||
elif conf < 6:
|
||||
time_str = '' # add new to fix error when conf < 0
|
||||
conf = max(1, conf)
|
||||
icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)
|
||||
else:
|
||||
icon = "atlas://gui/kivy/theming/light/confirmed"
|
||||
|
||||
if tx_hash:
|
||||
label, is_default_label = self.app.wallet.get_label(tx_hash)
|
||||
label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
|
||||
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:
|
||||
label = _('Pruned transaction outputs')
|
||||
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)
|
||||
|
||||
quote_text = ''
|
||||
yield (conf, icon, time_str, label, value, tx_hash, quote_text)
|
||||
|
||||
def update(self, see_all=False):
|
||||
if self.app.wallet is None:
|
||||
return
|
||||
|
||||
history_card = self.screen.ids.recent_activity_card
|
||||
history_card = self.screen.ids.history_container
|
||||
history = self.parse_history(reversed(
|
||||
self.app.wallet.get_history(self.app.current_account)))
|
||||
# repopulate History Card
|
||||
last_widget = history_card.ids.content.children[-1]
|
||||
history_card.ids.content.clear_widgets()
|
||||
history_add = history_card.ids.content.add_widget
|
||||
history_add(last_widget)
|
||||
RecentActivityItem = Factory.RecentActivityItem
|
||||
history_card.clear_widgets()
|
||||
history_add = history_card.add_widget
|
||||
count = 0
|
||||
for item in history:
|
||||
count += 1
|
||||
conf, icon, date_time, address, value, tx, quote_text = item
|
||||
ri = RecentActivityItem()
|
||||
conf, icon, date_time, message, value, tx, quote_text = item
|
||||
ri = Factory.HistoryItem()
|
||||
ri.icon = icon
|
||||
ri.date = date_time
|
||||
ri.address = address
|
||||
ri.message = message
|
||||
ri.value = value
|
||||
ri.quote_text = quote_text
|
||||
ri.confirmations = conf
|
||||
ri.tx_hash = tx
|
||||
ri.screen = self
|
||||
history_add(ri)
|
||||
if count == 8 and not see_all:
|
||||
break
|
||||
|
@ -181,20 +199,35 @@ class ScreenPassword(Factory.Screen):
|
|||
class SendScreen(CScreen):
|
||||
|
||||
kvname = 'send'
|
||||
payment_request = None
|
||||
|
||||
def set_URI(self, uri):
|
||||
print "set uri", uri
|
||||
self.screen.address = uri.get('address', '')
|
||||
self.screen.message = uri.get('message', '')
|
||||
amount = uri.get('amount')
|
||||
if amount:
|
||||
amount_str = str( Decimal(amount) / pow(10, self.app.decimal_point()))
|
||||
self.screen.amount = amount_str + ' ' + self.app.base_unit
|
||||
self.screen.amount = self.app.format_amount_and_units(amount)
|
||||
|
||||
def update(self):
|
||||
if self.app.current_invoice:
|
||||
self.set_request(self.app.current_invoice)
|
||||
|
||||
def do_clear(self):
|
||||
self.screen.amount = ''
|
||||
self.screen.message = ''
|
||||
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):
|
||||
contents = unicode(self.app._clipboard.get())
|
||||
|
@ -206,6 +239,12 @@ class SendScreen(CScreen):
|
|||
self.set_URI(uri)
|
||||
|
||||
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)
|
||||
if not bitcoin.is_address(address):
|
||||
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
|
||||
|
@ -215,9 +254,9 @@ class SendScreen(CScreen):
|
|||
except:
|
||||
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
|
||||
return
|
||||
outputs = [('address', address, amount)]
|
||||
message = unicode(self.screen.message)
|
||||
fee = None
|
||||
outputs = [('address', address, amount)]
|
||||
self.app.protected(self.send_tx, (outputs, fee, message))
|
||||
|
||||
def send_tx(self, *args):
|
||||
|
@ -250,7 +289,14 @@ class ReceiveScreen(CScreen):
|
|||
kvname = 'receive'
|
||||
|
||||
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):
|
||||
amount_label = self.screen.ids.get('amount')
|
||||
|
@ -269,17 +315,35 @@ class ReceiveScreen(CScreen):
|
|||
@profiler
|
||||
def update_qr(self):
|
||||
uri = self.get_URI()
|
||||
qr = self.screen.ids.get('qr')
|
||||
qr = self.screen.ids.qr
|
||||
qr.set_data(uri)
|
||||
|
||||
def do_copy(self):
|
||||
uri = self.get_URI()
|
||||
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.message = ''
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
class ContactsScreen(CScreen):
|
||||
|
@ -311,6 +375,76 @@ class ContactsScreen(CScreen):
|
|||
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):
|
||||
'''CustomDropDown that allows fading out the dropdown
|
||||
|
@ -340,27 +474,20 @@ class TabbedCarousel(Factory.TabbedPanel):
|
|||
scrlv = self._tab_strip.parent
|
||||
if not scrlv:
|
||||
return
|
||||
|
||||
idx = self.tab_list.index(value)
|
||||
if idx == 0:
|
||||
n = len(self.tab_list)
|
||||
if idx in [0, 1]:
|
||||
scroll_x = 1
|
||||
elif idx == len(self.tab_list) - 1:
|
||||
elif idx in [n-1, n-2]:
|
||||
scroll_x = 0
|
||||
else:
|
||||
self_center_x = scrlv.center_x
|
||||
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
|
||||
scroll_x = 1. * (n - idx - 1) / (n - 1)
|
||||
|
||||
mation = Factory.Animation(scroll_x=scroll_x, d=.25)
|
||||
mation.cancel_all(scrlv)
|
||||
mation.start(scrlv)
|
||||
|
||||
def on_current_tab(self, instance, value):
|
||||
if value.text == 'default_tab':
|
||||
return
|
||||
self.animate_tab_to_center(value)
|
||||
|
||||
def on_index(self, instance, value):
|
||||
|
|
|
@ -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
|
|
@ -5,23 +5,9 @@
|
|||
#: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>
|
||||
color: 0.45, 0.45, 0.45, 1
|
||||
color: 0.95, 0.95, 0.95, 1
|
||||
size_hint: 1, None
|
||||
text: ''
|
||||
text_size: self.width, None
|
||||
|
@ -29,45 +15,17 @@
|
|||
halign: 'left'
|
||||
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)
|
||||
|
||||
|
||||
<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>
|
||||
<HistoryItem@CardItem>
|
||||
icon: 'atlas://gui/kivy/theming/light/important'
|
||||
address: 'no address set'
|
||||
message: ''
|
||||
value: 0
|
||||
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
|
||||
date: '0/0/0'
|
||||
quote_text: '.'
|
||||
date: ''
|
||||
quote_text: ''
|
||||
spacing: '9dp'
|
||||
on_release:
|
||||
app.tx_selected(root.tx_hash, self.state)
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
spacing: '8dp'
|
||||
height: '32dp'
|
||||
Image:
|
||||
id: icon
|
||||
source: root.icon
|
||||
|
@ -78,38 +36,27 @@
|
|||
orientation: 'vertical'
|
||||
Widget
|
||||
CardLabel:
|
||||
shorten: True
|
||||
text: root.address
|
||||
markup: False
|
||||
text_size: self.size
|
||||
text: root.date
|
||||
font_size: '14sp'
|
||||
CardLabel:
|
||||
color: .699, .699, .699, 1
|
||||
text: root.date
|
||||
font_size: '12sp'
|
||||
font_size: '13sp'
|
||||
shorten: True
|
||||
text: root.message
|
||||
Widget
|
||||
CardLabel:
|
||||
halign: 'right'
|
||||
font_size: '13sp'
|
||||
font_size: '15sp'
|
||||
size_hint: None, 1
|
||||
width: '110sp'
|
||||
markup: True
|
||||
font_name: font_light
|
||||
text:
|
||||
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,\
|
||||
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
|
||||
unit=app.base_unit)
|
||||
CardSeparator
|
||||
|
||||
<CardRecentActivity@Card>
|
||||
GridLayout:
|
||||
id: content
|
||||
spacing: '7dp'
|
||||
cols: 1
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
CardSeparator
|
||||
|
||||
|
||||
HistoryScreen:
|
||||
|
@ -119,12 +66,9 @@ HistoryScreen:
|
|||
id: content
|
||||
do_scroll_x: False
|
||||
GridLayout
|
||||
id: grid
|
||||
cols: 1 #if root.width < root.height else 2
|
||||
id: history_container
|
||||
cols: 1
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
padding: '12dp'
|
||||
spacing: '12dp'
|
||||
CardRecentActivity:
|
||||
id: recent_activity_card
|
||||
|
||||
spacing: '2dp'
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
|
|
@ -42,7 +42,8 @@ ReceiveScreen:
|
|||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
opacity: 0.5 if qr.shaded else 1
|
||||
text: s.address
|
||||
text: _('Bitcoin Address') + ': ' + s.address
|
||||
text_size: self.width, None
|
||||
|
||||
SendReceiveBlueBottom:
|
||||
id: blue_bottom
|
||||
|
@ -51,13 +52,15 @@ ReceiveScreen:
|
|||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: blue_bottom.item_height
|
||||
spacing: '5dp'
|
||||
Image:
|
||||
source: 'atlas://gui/kivy/theming/light/globe'
|
||||
size_hint: None, None
|
||||
size: '22dp', '22dp'
|
||||
pos_hint: {'center_y': .5}
|
||||
AmountButton:
|
||||
BlueButton:
|
||||
id: amount_label
|
||||
default_text: 'Amount'
|
||||
text: s.amount if s.amount else 'Amount'
|
||||
on_release: app.amount_dialog(s, False)
|
||||
CardSeparator:
|
||||
|
@ -74,12 +77,10 @@ ReceiveScreen:
|
|||
size_hint: None, None
|
||||
size: '22dp', '22dp'
|
||||
pos_hint: {'center_y': .5}
|
||||
TextInputBlue:
|
||||
id: message_input
|
||||
hint_text: 'Description'
|
||||
text: s.message
|
||||
on_text_validate: s.message = self.text
|
||||
|
||||
BlueButton:
|
||||
id: description
|
||||
text: s.message if s.message else _('Description')
|
||||
on_release: app.description_dialog(s)
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
|
@ -89,9 +90,14 @@ ReceiveScreen:
|
|||
height: '48dp'
|
||||
on_release: s.parent.do_copy()
|
||||
Button:
|
||||
text: _('Clear')
|
||||
text: _('Save')
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
on_release: s.parent.do_clear()
|
||||
Widget:
|
||||
size_hint: 1, 0.3
|
||||
on_release: s.parent.do_save()
|
||||
Button:
|
||||
text: _('New')
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
on_release: s.parent.do_new()
|
||||
#Widget:
|
||||
# size_hint: 1, 0.3
|
||||
|
|
|
@ -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'
|
|
@ -23,6 +23,7 @@ SendScreen:
|
|||
id: blue_bottom
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
spacing: '5dp'
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: blue_bottom.item_height
|
||||
|
@ -32,32 +33,32 @@ SendScreen:
|
|||
size_hint: None, None
|
||||
size: '22dp', '22dp'
|
||||
pos_hint: {'center_y': .5}
|
||||
TextInputBlue:
|
||||
BlueButton:
|
||||
id: payto_e
|
||||
text: s.address
|
||||
hint_text: "Recipient"
|
||||
text: s.address if s.address else _('Recipient')
|
||||
on_release: app.address_dialog(s)
|
||||
CardSeparator:
|
||||
opacity: message_selection.opacity
|
||||
color: blue_bottom.foreground_color
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: blue_bottom.item_height
|
||||
spacing: '5dp'
|
||||
Image:
|
||||
source: 'atlas://gui/kivy/theming/light/globe'
|
||||
size_hint: None, None
|
||||
size: '22dp', '22dp'
|
||||
pos_hint: {'center_y': .5}
|
||||
AmountButton:
|
||||
BlueButton:
|
||||
id: amount_e
|
||||
text: s.amount if s.amount else 'Amount'
|
||||
on_release: app.amount_dialog(s, True)
|
||||
|
||||
default_text: _('Amount')
|
||||
text: s.amount if s.amount else _('Amount')
|
||||
on_release: s.amount_dialog()
|
||||
CardSeparator:
|
||||
opacity: message_selection.opacity
|
||||
color: blue_bottom.foreground_color
|
||||
BoxLayout:
|
||||
id: message_selection
|
||||
opacity: 1
|
||||
size_hint: 1, None
|
||||
height: blue_bottom.item_height
|
||||
spacing: '5dp'
|
||||
|
@ -66,11 +67,10 @@ SendScreen:
|
|||
size_hint: None, None
|
||||
size: '22dp', '22dp'
|
||||
pos_hint: {'center_y': .5}
|
||||
TextInputBlue:
|
||||
id: message_e
|
||||
hint_text: 'Description'
|
||||
text: s.message
|
||||
on_text_validate: s.message = self.text
|
||||
BlueButton:
|
||||
id: description
|
||||
text: s.message if s.message else _('Description')
|
||||
on_release: app.description_dialog(s)
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
|
@ -78,7 +78,7 @@ SendScreen:
|
|||
id: qr
|
||||
text: _('QR Code')
|
||||
on_release:
|
||||
app.scan_qr(on_complete=s.parent.set_URI)
|
||||
app.scan_qr(on_complete=app.set_URI)
|
||||
Button:
|
||||
id: paste_button
|
||||
text: _('Paste')
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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
|
|
@ -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()
|
|
@ -64,7 +64,7 @@ class OpenFileEventFilter(QObject):
|
|||
|
||||
|
||||
|
||||
class ElectrumGui:
|
||||
class ElectrumGui(MessageBoxMixin):
|
||||
|
||||
def __init__(self, config, network, plugins):
|
||||
set_language(config.get('language'))
|
||||
|
@ -134,7 +134,7 @@ class ElectrumGui:
|
|||
try:
|
||||
storage = WalletStorage(filename)
|
||||
except Exception as e:
|
||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
||||
self.show_error(str(e))
|
||||
return
|
||||
if not storage.file_exists:
|
||||
recent = self.config.get('recently_open', [])
|
||||
|
@ -147,7 +147,7 @@ class ElectrumGui:
|
|||
wallet = Wallet(storage)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
QMessageBox.warning(None, _('Warning'), str(e), _('OK'))
|
||||
self.show_warning(str(e))
|
||||
return
|
||||
action = wallet.get_action()
|
||||
# run wizard
|
||||
|
@ -162,32 +162,6 @@ class ElectrumGui:
|
|||
|
||||
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):
|
||||
# Use a signal as can be called from daemon thread
|
||||
self.app.emit(SIGNAL('new_window'), path, uri)
|
||||
|
@ -245,6 +219,9 @@ class ElectrumGui:
|
|||
# main loop
|
||||
self.app.exec_()
|
||||
|
||||
# Shut down the timer cleanly
|
||||
self.timer.stop()
|
||||
|
||||
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
|
||||
event = QtCore.QEvent(QtCore.QEvent.Clipboard)
|
||||
self.app.sendEvent(self.app.clipboard(), event)
|
||||
|
|
|
@ -25,9 +25,10 @@ from PyQt4.QtCore import *
|
|||
from util import *
|
||||
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.parent = parent
|
||||
self.config = parent.config
|
||||
|
@ -35,10 +36,7 @@ class AddressDialog(QDialog):
|
|||
self.app = parent.app
|
||||
self.saved = True
|
||||
|
||||
QDialog.__init__(self)
|
||||
self.setMinimumWidth(700)
|
||||
self.setWindowTitle(_("Address"))
|
||||
self.setModal(1)
|
||||
vbox = QVBoxLayout()
|
||||
self.setLayout(vbox)
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ class HistoryWidget(MyTreeWidget):
|
|||
icon, time_str = self.get_icon(conf, timestamp)
|
||||
v_str = self.parent.format_amount(value, True, 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]
|
||||
run_hook('history_tab_update', tx, entry)
|
||||
item = QTreeWidgetItem(entry)
|
||||
|
@ -88,8 +88,6 @@ class HistoryWidget(MyTreeWidget):
|
|||
item.setForeground(4, QBrush(QColor("#BC1E1E")))
|
||||
if tx_hash:
|
||||
item.setData(0, Qt.UserRole, tx_hash)
|
||||
if is_default_label:
|
||||
item.setForeground(3, QBrush(QColor('grey')))
|
||||
self.insertTopLevelItem(0, item)
|
||||
if current_tx == tx_hash:
|
||||
self.setCurrentItem(item)
|
||||
|
|
|
@ -62,17 +62,17 @@ class CosignWidget(QWidget):
|
|||
|
||||
|
||||
|
||||
class InstallWizard(QDialog):
|
||||
class InstallWizard(WindowModalDialog, MessageBoxMixin):
|
||||
|
||||
def __init__(self, app, config, network, storage):
|
||||
QDialog.__init__(self)
|
||||
title = 'Electrum' + ' - ' + _('Install Wizard')
|
||||
WindowModalDialog.__init__(self, None, title=title)
|
||||
self.app = app
|
||||
self.config = config
|
||||
self.network = network
|
||||
self.storage = storage
|
||||
self.setMinimumSize(575, 400)
|
||||
self.setMaximumSize(575, 400)
|
||||
self.setWindowTitle('Electrum' + ' - ' + _('Install Wizard'))
|
||||
self.connect(self, QtCore.SIGNAL('accept'), self.accept)
|
||||
self.stack = QStackedLayout()
|
||||
self.setLayout(self.stack)
|
||||
|
@ -139,8 +139,8 @@ class InstallWizard(QDialog):
|
|||
button.setChecked(True)
|
||||
|
||||
vbox.addStretch(1)
|
||||
self.set_layout(vbox)
|
||||
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next'))))
|
||||
self.set_layout(vbox)
|
||||
self.show()
|
||||
self.raise_()
|
||||
|
||||
|
@ -157,7 +157,7 @@ class InstallWizard(QDialog):
|
|||
if not r:
|
||||
return
|
||||
if prepare_seed(r) != prepare_seed(seed):
|
||||
QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK'))
|
||||
self.show_error(_('Incorrect seed'))
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
@ -384,22 +384,6 @@ class InstallWizard(QDialog):
|
|||
wallet_type = '%dof%d'%(m,n)
|
||||
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):
|
||||
vbox = seed_dialog.show_seed_box_msg(seed, sid)
|
||||
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _("Next"))))
|
||||
|
@ -407,22 +391,21 @@ class InstallWizard(QDialog):
|
|||
return self.exec_()
|
||||
|
||||
def password_dialog(self):
|
||||
msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
|
||||
+_("Leave these fields empty if you want to disable encryption.")
|
||||
from password_dialog import make_password_dialog, run_password_dialog
|
||||
self.set_layout( make_password_dialog(self, None, msg) )
|
||||
return run_password_dialog(self, None, self)[2]
|
||||
from password_dialog import PasswordDialog
|
||||
msg = _("Please choose a password to encrypt your wallet keys.\n"
|
||||
"Leave these fields empty if you want to disable encryption.")
|
||||
dialog = PasswordDialog(self, None, _("Choose a password"), msg, True)
|
||||
return dialog.run()[2]
|
||||
|
||||
def run(self, action):
|
||||
if self.storage.file_exists and action != 'new':
|
||||
path = self.storage.path
|
||||
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
||||
"Do you want to complete its creation now?") % path
|
||||
if not question(msg):
|
||||
if question(_("Do you want to delete '%s'?") % path):
|
||||
if not self.question(msg):
|
||||
if self.question(_("Do you want to delete '%s'?") % path):
|
||||
os.remove(path)
|
||||
QMessageBox.information(self, _('Warning'),
|
||||
_('The file was removed'), _('OK'))
|
||||
self.show_warning(_('The file was removed'))
|
||||
return
|
||||
return
|
||||
self.show()
|
||||
|
@ -434,7 +417,7 @@ class InstallWizard(QDialog):
|
|||
wallet = self.run_wallet_type(action, wallet_type)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
||||
self.show_error(str(e))
|
||||
return
|
||||
return wallet
|
||||
|
||||
|
@ -463,7 +446,7 @@ class InstallWizard(QDialog):
|
|||
elif wallet_type == 'twofactor':
|
||||
wallet_type = '2fa'
|
||||
if action == 'create':
|
||||
self.storage.put('wallet_type', wallet_type, False)
|
||||
self.storage.put('wallet_type', wallet_type)
|
||||
|
||||
if action is None:
|
||||
return
|
||||
|
@ -527,7 +510,7 @@ class InstallWizard(QDialog):
|
|||
if self.config.get('server') is None:
|
||||
self.network_dialog()
|
||||
else:
|
||||
QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
|
||||
self.show_warning(_('You are offline'))
|
||||
|
||||
|
||||
# start wallet threads
|
||||
|
@ -539,7 +522,7 @@ class InstallWizard(QDialog):
|
|||
msg = _("Recovery successful") if wallet.is_found() else _("No transactions found for this seed")
|
||||
else:
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
wallet = Wallet.from_multisig(key_list, password, self.storage, t)
|
||||
else:
|
||||
self.storage.put('wallet_type', t, False)
|
||||
self.storage.put('wallet_type', t)
|
||||
# call the constructor to load the plugin (side effect)
|
||||
Wallet(self.storage)
|
||||
wallet = always_hook('installwizard_restore', self, self.storage)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys, time, threading
|
||||
import os.path, json, traceback
|
||||
import os, json, traceback
|
||||
import shutil
|
||||
import socket
|
||||
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 format_satoshis, format_satoshis_plain, format_time
|
||||
from electrum.util import PrintError, NotEnoughFunds, StoreDict
|
||||
from electrum import Transaction
|
||||
from electrum import mnemonic
|
||||
from electrum import Transaction, mnemonic
|
||||
from electrum import util, bitcoin, commands, Wallet
|
||||
from electrum import SimpleConfig, COIN_CHOOSERS, WalletStorage
|
||||
from electrum import Imported_Wallet
|
||||
from electrum import paymentrequest
|
||||
from electrum import Imported_Wallet, paymentrequest
|
||||
|
||||
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
|
||||
from network_dialog import NetworkDialog
|
||||
from qrcodewidget import QRCodeWidget, QRDialog
|
||||
from qrtextedit import ScanQRTextEdit, ShowQRTextEdit
|
||||
from qrtextedit import ShowQRTextEdit
|
||||
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 PaymentRequest, get_payment_request
|
||||
|
||||
pr_icons = {
|
||||
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):
|
||||
QMainWindow.__init__(self)
|
||||
|
@ -270,7 +266,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.update_account_selector()
|
||||
# update menus
|
||||
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.seed_menu.setEnabled(self.wallet.has_seed())
|
||||
self.mpk_menu.setEnabled(self.wallet.is_deterministic())
|
||||
|
@ -288,14 +284,17 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
except:
|
||||
self.setGeometry(100, 100, 840, 400)
|
||||
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():
|
||||
msg = ' '.join([
|
||||
_("This wallet is watching-only."),
|
||||
_("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.")
|
||||
])
|
||||
QMessageBox.warning(self, _('Information'), msg, _('OK'))
|
||||
run_hook('load_wallet', wallet, self)
|
||||
self.show_warning(msg, title=_('Information'))
|
||||
|
||||
def import_old_contacts(self):
|
||||
# backward compatibility: import contacts
|
||||
|
@ -321,7 +320,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.wallet.synchronize()
|
||||
|
||||
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))
|
||||
if not filename:
|
||||
return
|
||||
|
@ -339,11 +338,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
if new_path != path:
|
||||
try:
|
||||
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:
|
||||
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):
|
||||
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.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):
|
||||
menubar = QMenuBar()
|
||||
|
||||
file_menu = menubar.addMenu(_("&File"))
|
||||
self.recently_visited_menu = file_menu.addMenu(_("&Recently 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.addSeparator()
|
||||
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."),
|
||||
_("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):
|
||||
if not self.network or not self.network.is_connected():
|
||||
|
@ -582,7 +605,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
def show_address(self, addr):
|
||||
import address_dialog
|
||||
d = address_dialog.AddressDialog(addr, self)
|
||||
d = address_dialog.AddressDialog(self, addr)
|
||||
d.exec_()
|
||||
|
||||
def show_transaction(self, tx, tx_desc = None):
|
||||
|
@ -744,7 +767,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
try:
|
||||
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _('Error'), str(e), _('OK'))
|
||||
self.show_error(str(e))
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
@ -757,7 +780,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
amount = self.receive_amount_e.get_amount()
|
||||
message = unicode(self.receive_message_e.text())
|
||||
if not message and not amount:
|
||||
QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
|
||||
self.show_error(_('No message or amount'))
|
||||
return False
|
||||
i = self.expires_combo.currentIndex()
|
||||
expiration = map(lambda x: x[1], expiration_values)[i]
|
||||
|
@ -769,8 +792,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.save_request_button.setEnabled(False)
|
||||
|
||||
def view_and_paste(self, title, msg, data):
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle(title)
|
||||
dialog = WindowModalDialog(self, title)
|
||||
vbox = QVBoxLayout()
|
||||
label = QLabel(msg)
|
||||
label.setWordWrap(True)
|
||||
|
@ -1129,7 +1151,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.wallet.check_password(password)
|
||||
break
|
||||
except Exception as e:
|
||||
QMessageBox.warning(parent, _('Error'), str(e), _('OK'))
|
||||
self.show_error(str(e), parent=parent)
|
||||
continue
|
||||
else:
|
||||
password = None
|
||||
|
@ -1140,7 +1162,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
def read_send_tab(self):
|
||||
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
|
||||
label = unicode( self.message_e.text() )
|
||||
|
||||
|
@ -1161,23 +1183,23 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
return
|
||||
|
||||
if not outputs:
|
||||
QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
|
||||
self.show_error(_('No outputs'))
|
||||
return
|
||||
|
||||
for _type, addr, amount in outputs:
|
||||
if addr is None:
|
||||
QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK'))
|
||||
self.show_error(_('Bitcoin Address is None'))
|
||||
return
|
||||
if _type == 'address' and not bitcoin.is_address(addr):
|
||||
QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
|
||||
self.show_error(_('Invalid Bitcoin Address'))
|
||||
return
|
||||
if amount is None:
|
||||
QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
|
||||
self.show_error(_('Invalid Amount'))
|
||||
return
|
||||
|
||||
fee = self.fee_e.get_amount()
|
||||
if fee is None:
|
||||
QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
|
||||
self.show_error(_('Invalid Fee'))
|
||||
return
|
||||
|
||||
coins = self.get_coins()
|
||||
|
@ -1203,7 +1225,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
return
|
||||
|
||||
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
|
||||
|
||||
if self.show_before_broadcast():
|
||||
|
@ -1258,16 +1280,14 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
def sign_thread():
|
||||
if not self.wallet.is_watching_only():
|
||||
self.wallet.sign_transaction(tx, password)
|
||||
def on_sign_successful(ret):
|
||||
def on_signed(ret):
|
||||
success[0] = True
|
||||
def on_dialog_close():
|
||||
def on_finished():
|
||||
self.send_button.setDisabled(False)
|
||||
callback(success[0])
|
||||
|
||||
# keep a reference to WaitingDialog or the gui might crash
|
||||
self.waiting_dialog = WaitingDialog(parent, 'Signing transaction...', sign_thread, on_sign_successful, on_dialog_close)
|
||||
self.waiting_dialog.start()
|
||||
|
||||
WaitingDialog(parent, _('Signing transaction...'), sign_thread,
|
||||
on_success=on_signed, on_finished=on_finished)
|
||||
|
||||
def broadcast_transaction(self, tx, tx_desc, parent=None):
|
||||
|
||||
|
@ -1296,19 +1316,16 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
if status:
|
||||
if tx_desc is not None and tx.is_complete():
|
||||
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.do_clear()
|
||||
else:
|
||||
QMessageBox.warning(parent, _('Error'), msg, _('OK'))
|
||||
self.show_error(msg, parent=parent)
|
||||
self.send_button.setDisabled(False)
|
||||
|
||||
if parent == None:
|
||||
parent = self
|
||||
self.waiting_dialog = WaitingDialog(parent, 'Broadcasting transaction...', broadcast_thread, broadcast_done)
|
||||
self.waiting_dialog.start()
|
||||
|
||||
|
||||
parent = parent or self
|
||||
WaitingDialog(parent, _('Broadcasting transaction...'),
|
||||
broadcast_thread, broadcast_done)
|
||||
|
||||
def prepare_for_payment_request(self):
|
||||
self.tabs.setCurrentIndex(1)
|
||||
|
@ -1346,37 +1363,28 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.payment_request = None
|
||||
self.do_clear()
|
||||
|
||||
def pay_to_URI(self, URI):
|
||||
if not URI:
|
||||
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)
|
||||
def on_pr(self, request):
|
||||
self.payment_request = request
|
||||
if self.payment_request.verify(self.contacts):
|
||||
self.emit(SIGNAL('payment_request_ok'))
|
||||
else:
|
||||
self.emit(SIGNAL('payment_request_error'))
|
||||
t = threading.Thread(target=get_payment_request_thread)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
def pay_to_URI(self, URI):
|
||||
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()
|
||||
return
|
||||
|
||||
address = out.get('address')
|
||||
amount = out.get('amount')
|
||||
label = out.get('label')
|
||||
|
@ -1555,6 +1563,13 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
def paytomany(self):
|
||||
self.tabs.setCurrentIndex(1)
|
||||
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):
|
||||
paytos = [self.get_contact_payto(label) for label in labels]
|
||||
|
@ -1578,7 +1593,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
def set_contact(self, label, 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
|
||||
return False
|
||||
self.contacts[label] = ('address', address)
|
||||
|
@ -1628,8 +1643,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.show_pr_details(pr)
|
||||
|
||||
def show_pr_details(self, pr):
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_("Invoice"))
|
||||
d = WindowModalDialog(self, _("Invoice"))
|
||||
vbox = QVBoxLayout(d)
|
||||
grid = QGridLayout()
|
||||
grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
|
||||
|
@ -1840,8 +1854,39 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
def change_password_dialog(self):
|
||||
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()
|
||||
|
||||
def toggle_search(self):
|
||||
|
@ -1866,8 +1911,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
|
||||
def new_contact_dialog(self):
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_("New Contact"))
|
||||
d = WindowModalDialog(self, _("New Contact"))
|
||||
vbox = QVBoxLayout(d)
|
||||
vbox.addWidget(QLabel(_('New Contact') + ':'))
|
||||
grid = QGridLayout()
|
||||
|
@ -1892,9 +1936,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
@protected
|
||||
def new_account_dialog(self, password):
|
||||
dialog = QDialog(self)
|
||||
dialog.setModal(1)
|
||||
dialog.setWindowTitle(_("New Account"))
|
||||
dialog = WindowModalDialog(self, _("New Account"))
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(QLabel(_('Account name')+':'))
|
||||
e = QLineEdit()
|
||||
|
@ -1917,11 +1959,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
|
||||
def show_master_public_keys(self):
|
||||
|
||||
dialog = QDialog(self)
|
||||
dialog.setModal(1)
|
||||
dialog.setWindowTitle(_("Master Public Keys"))
|
||||
|
||||
dialog = WindowModalDialog(self, "Master Public Keys")
|
||||
mpk_dict = self.wallet.get_master_public_keys()
|
||||
vbox = QVBoxLayout()
|
||||
# only show the combobox in case multiple accounts are available
|
||||
|
@ -1966,13 +2004,13 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
@protected
|
||||
def show_seed_dialog(self, password):
|
||||
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
|
||||
|
||||
try:
|
||||
mnemonic = self.wallet.get_mnemonic(password)
|
||||
except BaseException as e:
|
||||
QMessageBox.warning(self, _('Error'), str(e), _('OK'))
|
||||
self.show_error(str(e))
|
||||
return
|
||||
from seed_dialog import SeedDialog
|
||||
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:
|
||||
return
|
||||
d = QRDialog(data, self, title)
|
||||
d = QRDialog(data, parent or self, title)
|
||||
d.exec_()
|
||||
|
||||
def show_public_keys(self, address):
|
||||
|
@ -1995,10 +2033,8 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.show_message(str(e))
|
||||
return
|
||||
|
||||
d = QDialog(self)
|
||||
d = WindowModalDialog(self, _("Public key"))
|
||||
d.setMinimumSize(600, 200)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_("Public key"))
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
||||
vbox.addWidget( QLabel(_("Public key") + ':'))
|
||||
|
@ -2019,10 +2055,8 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
self.show_message(str(e))
|
||||
return
|
||||
|
||||
d = QDialog(self)
|
||||
d = WindowModalDialog(self, _("Private key"))
|
||||
d.setMinimumSize(600, 200)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_("Private key"))
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
||||
vbox.addWidget( QLabel(_("Private key") + ':'))
|
||||
|
@ -2056,9 +2090,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
|
||||
def sign_verify_message(self, address=''):
|
||||
d = QDialog(self)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_('Sign/verify Message'))
|
||||
d = WindowModalDialog(self, _('Sign/verify Message'))
|
||||
d.setMinimumSize(410, 290)
|
||||
|
||||
layout = QGridLayout(d)
|
||||
|
@ -2117,9 +2149,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
|
||||
def encrypt_message(self, address = ''):
|
||||
d = QDialog(self)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_('Encrypt/decrypt Message'))
|
||||
d = WindowModalDialog(self, _('Encrypt/decrypt Message'))
|
||||
d.setMinimumSize(610, 490)
|
||||
|
||||
layout = QGridLayout(d)
|
||||
|
@ -2157,22 +2187,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
layout.addLayout(hbox, 4, 1)
|
||||
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):
|
||||
if parent == None:
|
||||
parent = self
|
||||
d = QDialog(parent)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_("Enter Password"))
|
||||
parent = parent or self
|
||||
d = WindowModalDialog(parent, _("Enter Password"))
|
||||
pw = QLineEdit()
|
||||
pw.setEchoMode(2)
|
||||
vbox = QVBoxLayout()
|
||||
|
@ -2200,33 +2217,24 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
except:
|
||||
is_hex = False
|
||||
|
||||
try:
|
||||
if is_hex:
|
||||
try:
|
||||
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))
|
||||
assert "hex" in tx_dict.keys()
|
||||
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
|
||||
except Exception:
|
||||
except:
|
||||
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):
|
||||
from electrum import qrscanner
|
||||
try:
|
||||
data = qrscanner.scan_qr(self.config)
|
||||
except BaseException, e:
|
||||
QMessageBox.warning(self, _('Error'), _(e), _('OK'))
|
||||
except BaseException as e:
|
||||
self.show_error(str(e))
|
||||
return
|
||||
if not data:
|
||||
return
|
||||
|
@ -2252,8 +2260,8 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
try:
|
||||
with open(fileName, "r") as f:
|
||||
file_content = f.read()
|
||||
except (ValueError, IOError, os.error), reason:
|
||||
QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
|
||||
except (ValueError, IOError, os.error) as 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)
|
||||
|
||||
def do_process_from_text(self):
|
||||
|
@ -2292,11 +2300,10 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
try:
|
||||
self.wallet.check_password(password)
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, _('Error'), str(e), _('OK'))
|
||||
self.show_error(str(e))
|
||||
return
|
||||
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_('Private keys'))
|
||||
d = WindowModalDialog(self, _('Private keys'))
|
||||
d.setMinimumSize(850, 300)
|
||||
vbox = QVBoxLayout(d)
|
||||
|
||||
|
@ -2349,9 +2356,12 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
try:
|
||||
self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
|
||||
except (IOError, os.error), reason:
|
||||
export_error_label = _("Electrum was unable to produce a private key-export.")
|
||||
QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
|
||||
except (IOError, os.error) as reason:
|
||||
txt = "\n".join([
|
||||
_("Electrum was unable to produce a private key-export."),
|
||||
str(reason)
|
||||
])
|
||||
self.show_critical(txt, title=_("Unable to create csv"))
|
||||
|
||||
except Exception as e:
|
||||
self.show_message(str(e))
|
||||
|
@ -2381,9 +2391,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
f.close()
|
||||
for key, value in json.loads(data).items():
|
||||
self.wallet.set_label(key, value)
|
||||
QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
|
||||
except (IOError, os.error), reason:
|
||||
QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
|
||||
self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
|
||||
except (IOError, os.error) as reason:
|
||||
self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
|
||||
|
||||
|
||||
def do_export_labels(self):
|
||||
|
@ -2393,14 +2403,13 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
if fileName:
|
||||
with open(fileName, 'w+') as 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:
|
||||
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):
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_('Export History'))
|
||||
d = WindowModalDialog(self, _('Export History'))
|
||||
d.setMinimumSize(400, 200)
|
||||
vbox = QVBoxLayout(d)
|
||||
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())
|
||||
except (IOError, os.error), reason:
|
||||
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
|
||||
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):
|
||||
|
@ -2445,7 +2454,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
value_string = '--'
|
||||
|
||||
if tx_hash:
|
||||
label, is_default_label = wallet.get_label(tx_hash)
|
||||
label = wallet.get_label(tx_hash)
|
||||
label = label.encode('utf-8')
|
||||
else:
|
||||
label = ""
|
||||
|
@ -2467,12 +2476,11 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
|
||||
def sweep_key_dialog(self):
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_('Sweep private keys'))
|
||||
d = WindowModalDialog(self, title=_('Sweep private keys'))
|
||||
d.setMinimumSize(600, 300)
|
||||
|
||||
vbox = QVBoxLayout(d)
|
||||
vbox.addWidget(QLabel(_("Enter private keys")))
|
||||
vbox.addWidget(QLabel(_("Enter private keys:")))
|
||||
|
||||
keys_e = QTextEdit()
|
||||
keys_e.setTabChangesFocus(True)
|
||||
|
@ -2508,16 +2516,17 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
if not tx:
|
||||
self.show_message(_('No inputs found. (Note that inputs need to be confirmed)'))
|
||||
return
|
||||
self.warn_if_watching_only()
|
||||
self.show_transaction(tx)
|
||||
|
||||
|
||||
@protected
|
||||
def do_import_privkey(self, password):
|
||||
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>' \
|
||||
+ _('Are you sure you understand what you are doing?'), 3, 4)
|
||||
if r == 4: return
|
||||
+ _('Are you sure you understand what you are doing?'), title=_('Warning')):
|
||||
return
|
||||
|
||||
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
|
||||
if not text: return
|
||||
|
@ -2536,18 +2545,16 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
else:
|
||||
addrlist.append(addr)
|
||||
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:
|
||||
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.history_list.update()
|
||||
|
||||
|
||||
def settings_dialog(self):
|
||||
self.need_restart = False
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_('Preferences'))
|
||||
d.setModal(1)
|
||||
d = WindowModalDialog(self, _('Preferences'))
|
||||
vbox = QVBoxLayout()
|
||||
tabs = QTabWidget()
|
||||
gui_widgets = []
|
||||
|
@ -2593,21 +2600,6 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
nz.valueChanged.connect(on_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' \
|
||||
+ _('If you enable dynamic fees, this parameter will be used as upper bound.')
|
||||
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.'))
|
||||
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 = [
|
||||
(tx_widgets, _('Transactions')),
|
||||
(gui_widgets, _('Appearance')),
|
||||
|
@ -2813,13 +2823,11 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
run_hook('close_settings_dialog')
|
||||
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):
|
||||
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
|
||||
NetworkDialog(self.wallet.network, self.config, self).do_exec()
|
||||
|
||||
|
@ -2839,9 +2847,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
|
||||
|
||||
def plugins_dialog(self):
|
||||
self.pluginsdialog = d = QDialog(self)
|
||||
d.setWindowTitle(_('Electrum Plugins'))
|
||||
d.setModal(1)
|
||||
self.pluginsdialog = d = WindowModalDialog(self, _('Electrum Plugins'))
|
||||
|
||||
plugins = self.gui_object.plugins
|
||||
|
||||
|
@ -2867,7 +2873,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
def enable_settings_widget(p, name, i):
|
||||
widget = settings_widgets.get(name)
|
||||
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)
|
||||
if widget:
|
||||
widget.setEnabled(bool(p and p.is_enabled()))
|
||||
|
@ -2904,9 +2910,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
|||
def show_account_details(self, k):
|
||||
account = self.wallet.accounts[k]
|
||||
|
||||
d = QDialog(self)
|
||||
d.setWindowTitle(_('Account Details'))
|
||||
d.setModal(1)
|
||||
d = WindowModalDialog(self, _('Account Details'))
|
||||
|
||||
vbox = QVBoxLayout(d)
|
||||
name = self.wallet.get_account_name(k)
|
||||
|
|
|
@ -16,30 +16,21 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys, time, datetime, re, threading
|
||||
import os.path, json, ast, traceback
|
||||
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.util import print_error, print_msg
|
||||
from electrum import DEFAULT_PORTS
|
||||
from electrum.network import serialize_server, deserialize_server
|
||||
|
||||
from util import *
|
||||
|
||||
#protocol_names = ['TCP', 'HTTP', 'SSL', 'HTTPS']
|
||||
#protocol_letters = 'thsg'
|
||||
protocol_names = ['TCP', 'SSL']
|
||||
protocol_letters = 'ts'
|
||||
|
||||
class NetworkDialog(QDialog):
|
||||
class NetworkDialog(WindowModalDialog):
|
||||
def __init__(self, network, config, parent):
|
||||
|
||||
QDialog.__init__(self,parent)
|
||||
self.setModal(1)
|
||||
self.setWindowTitle(_('Network'))
|
||||
WindowModalDialog.__init__(self, parent, _('Network'))
|
||||
self.setMinimumSize(375, 20)
|
||||
|
||||
self.network = network
|
||||
|
|
|
@ -23,9 +23,27 @@ from util import *
|
|||
import re
|
||||
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.setEchoMode(2)
|
||||
|
@ -44,8 +62,6 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
|
|||
grid.setColumnStretch(1,1)
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
grid.addWidget(QLabel(_('Password')), 0, 0)
|
||||
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(self.new_pw, 1, 1)
|
||||
|
@ -71,105 +92,36 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
|
|||
# Password Strength Label
|
||||
self.pw_strength = QLabel()
|
||||
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.addLayout(Buttons(CancelButton(self), OkButton(self)))
|
||||
return vbox
|
||||
vbox.addLayout(Buttons(CancelButton(self), self.OKButton))
|
||||
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):
|
||||
|
||||
if wallet and wallet.is_watching_only():
|
||||
QMessageBox.information(parent, _('Error'), _('This is a watching-only wallet'), _('OK'))
|
||||
return False, None, None
|
||||
def check_OKButton(self):
|
||||
self.OKButton.setEnabled(self.new_pw.text() == self.conf_pw.text())
|
||||
|
||||
def run(self):
|
||||
if not self.exec_():
|
||||
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_password2 = unicode(self.conf_pw.text())
|
||||
|
||||
if new_password != new_password2:
|
||||
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'))
|
||||
return True, password or None, new_password or None
|
||||
|
|
|
@ -160,16 +160,8 @@ class PayToEdit(ScanQRTextEdit):
|
|||
return len(self.lines()) > 1
|
||||
|
||||
def paytomany(self):
|
||||
from electrum.i18n import _
|
||||
self.setText("\n\n\n")
|
||||
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):
|
||||
docHeight = self.document().size().height()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
import PyQt4.QtCore as QtCore
|
||||
import PyQt4.QtGui as QtGui
|
||||
|
||||
import os
|
||||
|
@ -9,6 +8,7 @@ import qrcode
|
|||
import electrum
|
||||
from electrum import bmp
|
||||
from electrum.i18n import _
|
||||
from util import WindowModalDialog, MessageBoxMixin
|
||||
|
||||
|
||||
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):
|
||||
QDialog.__init__(self, parent)
|
||||
WindowModalDialog.__init__(self, parent, title)
|
||||
|
||||
d = self
|
||||
d.setWindowTitle(title)
|
||||
vbox = QVBoxLayout()
|
||||
qrw = QRCodeWidget(data)
|
||||
vbox.addWidget(qrw, 1)
|
||||
|
@ -107,12 +105,12 @@ class QRDialog(QDialog):
|
|||
|
||||
def print_qr():
|
||||
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():
|
||||
bmp.save_qrcode(qrw.qr, 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"))
|
||||
hbox.addWidget(b)
|
||||
|
@ -124,8 +122,8 @@ class QRDialog(QDialog):
|
|||
|
||||
b = QPushButton(_("Close"))
|
||||
hbox.addWidget(b)
|
||||
b.clicked.connect(d.accept)
|
||||
b.clicked.connect(self.accept)
|
||||
b.setDefault(True)
|
||||
|
||||
vbox.addLayout(hbox)
|
||||
d.setLayout(vbox)
|
||||
self.setLayout(vbox)
|
||||
|
|
|
@ -3,7 +3,7 @@ from electrum.plugins import run_hook
|
|||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
||||
from util import ButtonsTextEdit
|
||||
from util import ButtonsTextEdit, MessageBoxMixin
|
||||
|
||||
|
||||
class ShowQRTextEdit(ButtonsTextEdit):
|
||||
|
@ -29,7 +29,7 @@ class ShowQRTextEdit(ButtonsTextEdit):
|
|||
m.exec_(e.globalPos())
|
||||
|
||||
|
||||
class ScanQRTextEdit(ButtonsTextEdit):
|
||||
class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
|
||||
|
||||
def __init__(self, text=""):
|
||||
ButtonsTextEdit.__init__(self, text)
|
||||
|
@ -50,8 +50,8 @@ class ScanQRTextEdit(ButtonsTextEdit):
|
|||
from electrum import qrscanner, get_config
|
||||
try:
|
||||
data = qrscanner.scan_qr(get_config())
|
||||
except BaseException, e:
|
||||
QMessageBox.warning(self, _('Error'), _(e), _('OK'))
|
||||
except BaseException as e:
|
||||
self.show_error(str(e))
|
||||
return ""
|
||||
if type(data) != str:
|
||||
return
|
||||
|
|
|
@ -20,17 +20,14 @@ from PyQt4.QtGui import *
|
|||
from PyQt4.QtCore import *
|
||||
import PyQt4.QtCore as QtCore
|
||||
from electrum.i18n import _
|
||||
from electrum import mnemonic
|
||||
|
||||
from util import *
|
||||
from qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
||||
|
||||
class SeedDialog(QDialog):
|
||||
class SeedDialog(WindowModalDialog):
|
||||
def __init__(self, parent, seed, imported_keys):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setModal(1)
|
||||
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
|
||||
self.setMinimumWidth(400)
|
||||
self.setWindowTitle('Electrum' + ' - ' + _('Seed'))
|
||||
vbox = show_seed_box_msg(seed)
|
||||
if imported_keys:
|
||||
vbox.addWidget(QLabel("<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"))
|
||||
|
|
|
@ -38,7 +38,7 @@ def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
|
|||
dialogs.append(d)
|
||||
d.show()
|
||||
|
||||
class TxDialog(QDialog):
|
||||
class TxDialog(QDialog, MessageBoxMixin):
|
||||
|
||||
def __init__(self, tx, parent, desc, prompt_if_unsaved):
|
||||
'''Transactions in the wallet will show their description.
|
||||
|
@ -54,7 +54,7 @@ class TxDialog(QDialog):
|
|||
self.desc = desc
|
||||
|
||||
QDialog.__init__(self)
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumWidth(660)
|
||||
self.setWindowTitle(_("Transaction"))
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
|
@ -62,7 +62,7 @@ class TxDialog(QDialog):
|
|||
|
||||
vbox.addWidget(QLabel(_("Transaction ID:")))
|
||||
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.setReadOnly(True)
|
||||
vbox.addWidget(self.tx_hash_e)
|
||||
|
@ -122,10 +122,7 @@ class TxDialog(QDialog):
|
|||
|
||||
def closeEvent(self, event):
|
||||
if (self.prompt_if_unsaved and not self.saved and not self.broadcast
|
||||
and QMessageBox.question(
|
||||
self, _('Warning'),
|
||||
_('This transaction is not saved. Close anyway?'),
|
||||
QMessageBox.Yes | QMessageBox.No) == QMessageBox.No):
|
||||
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
|
@ -135,7 +132,7 @@ class TxDialog(QDialog):
|
|||
text = self.tx.raw.decode('hex')
|
||||
text = base_encode(text, base=43)
|
||||
try:
|
||||
self.parent.show_qrcode(text, 'Transaction')
|
||||
self.parent.show_qrcode(text, 'Transaction', parent=self)
|
||||
except Exception as e:
|
||||
self.show_message(str(e))
|
||||
|
||||
|
@ -173,7 +170,7 @@ class TxDialog(QDialog):
|
|||
status = _("Signed")
|
||||
|
||||
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)
|
||||
if timestamp:
|
||||
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 ext
|
||||
|
||||
def format_amount(amt):
|
||||
return self.parent.format_amount(amt, whitespaces = True)
|
||||
|
||||
i_text = QTextEdit()
|
||||
i_text.setFont(QFont(MONOSPACE_FONT))
|
||||
i_text.setReadOnly(True)
|
||||
|
@ -270,6 +270,8 @@ class TxDialog(QDialog):
|
|||
if addr is None:
|
||||
addr = _('unknown')
|
||||
cursor.insertText(addr, text_format(addr))
|
||||
if x.get('value'):
|
||||
cursor.insertText(format_amount(x['value']), ext)
|
||||
cursor.insertBlock()
|
||||
|
||||
vbox.addWidget(i_text)
|
||||
|
@ -283,11 +285,6 @@ class TxDialog(QDialog):
|
|||
cursor.insertText(addr, text_format(addr))
|
||||
if v is not None:
|
||||
cursor.insertText('\t', ext)
|
||||
cursor.insertText(self.parent.format_amount(v, whitespaces = True), ext)
|
||||
cursor.insertText(format_amount(v), ext)
|
||||
cursor.insertBlock()
|
||||
vbox.addWidget(o_text)
|
||||
|
||||
|
||||
|
||||
def show_message(self, msg):
|
||||
QMessageBox.information(self, _('Message'), msg, _('OK'))
|
||||
|
|
144
gui/qt/util.py
144
gui/qt/util.py
|
@ -21,51 +21,19 @@ RED_FG = "QWidget {color:red;}"
|
|||
BLUE_FG = "QWidget {color:blue;}"
|
||||
BLACK_FG = "QWidget {color:black;}"
|
||||
|
||||
|
||||
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()
|
||||
|
||||
dialogs = []
|
||||
|
||||
class Timer(QThread):
|
||||
stopped = False
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
while not self.stopped:
|
||||
self.emit(SIGNAL('timersignal'))
|
||||
time.sleep(0.5)
|
||||
|
||||
def stop(self):
|
||||
self.stopped = True
|
||||
self.wait()
|
||||
|
||||
class EnterButton(QPushButton):
|
||||
def __init__(self, text, func):
|
||||
|
@ -187,12 +155,91 @@ class CancelButton(QPushButton):
|
|||
QPushButton.__init__(self, label or _("Cancel"))
|
||||
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):
|
||||
dialog = QDialog(parent)
|
||||
dialog = WindowModalDialog(parent, title)
|
||||
dialog.setMinimumWidth(500)
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setModal(1)
|
||||
l = QVBoxLayout()
|
||||
dialog.setLayout(l)
|
||||
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):
|
||||
from qrtextedit import ScanQRTextEdit
|
||||
dialog = QDialog(parent)
|
||||
dialog = WindowModalDialog(parent, title)
|
||||
dialog.setMinimumWidth(500)
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setModal(1)
|
||||
l = QVBoxLayout()
|
||||
dialog.setLayout(l)
|
||||
l.addWidget(QLabel(label))
|
||||
|
@ -221,9 +266,6 @@ def text_dialog(parent, title, label, ok_label, default=None):
|
|||
if dialog.exec_():
|
||||
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):
|
||||
hbox = QHBoxLayout()
|
||||
address_e = QLineEdit()
|
||||
|
@ -383,12 +425,6 @@ class MyTreeWidget(QTreeWidget):
|
|||
key = str(item.data(0, Qt.UserRole).toString())
|
||||
text = unicode(item.text(column))
|
||||
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.update_completions()
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ class ElectrumGui:
|
|||
else:
|
||||
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) ) )
|
||||
|
||||
self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
|
||||
|
|
|
@ -116,7 +116,7 @@ class ElectrumGui:
|
|||
else:
|
||||
time_str = 'pending'
|
||||
|
||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
||||
label = self.wallet.get_label(tx_hash)
|
||||
if len(label) > 40:
|
||||
label = label[0:37] + '...'
|
||||
self.history.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
|
||||
|
|
|
@ -21,13 +21,14 @@ import os
|
|||
import util
|
||||
from bitcoin import *
|
||||
|
||||
MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
|
||||
|
||||
class Blockchain(util.PrintError):
|
||||
'''Manages blockchain headers and their verification'''
|
||||
def __init__(self, config, network):
|
||||
self.config = config
|
||||
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.set_local_height()
|
||||
|
||||
|
@ -39,66 +40,35 @@ class Blockchain(util.PrintError):
|
|||
self.set_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):
|
||||
first_header = chain[0]
|
||||
prev_header = self.read_header(first_header.get('block_height') - 1)
|
||||
|
||||
for header in chain:
|
||||
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)
|
||||
if bits != header.get('bits'):
|
||||
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
|
||||
|
||||
self.verify_header(header, prev_header, bits, target)
|
||||
prev_header = header
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def verify_chunk(self, index, hexdata):
|
||||
data = hexdata.decode('hex')
|
||||
height = index*2016
|
||||
def verify_chunk(self, index, data):
|
||||
num = len(data) / 80
|
||||
|
||||
if index == 0:
|
||||
previous_hash = ("0"*64)
|
||||
else:
|
||||
prev_header = None
|
||||
if index != 0:
|
||||
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)
|
||||
|
||||
for i in range(num):
|
||||
height = index*2016 + i
|
||||
raw_header = data[i*80:(i+1) * 80]
|
||||
header = self.header_from_string(raw_header)
|
||||
_hash = self.hash_header(header)
|
||||
assert previous_hash == header.get('prev_block_hash')
|
||||
assert bits == header.get('bits')
|
||||
assert int('0x'+_hash,16) < target
|
||||
header = self.deserialize_header(raw_header)
|
||||
self.verify_header(header, prev_header, bits, target)
|
||||
prev_header = header
|
||||
|
||||
previous_header = header
|
||||
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):
|
||||
def serialize_header(self, res):
|
||||
s = int_to_hex(res.get('version'), 4) \
|
||||
+ rev_hex(res.get('prev_block_hash')) \
|
||||
+ rev_hex(res.get('merkle_root')) \
|
||||
|
@ -107,8 +77,7 @@ class Blockchain(util.PrintError):
|
|||
+ int_to_hex(int(res.get('nonce')), 4)
|
||||
return s
|
||||
|
||||
|
||||
def header_from_string(self, s):
|
||||
def deserialize_header(self, s):
|
||||
hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16)
|
||||
h = {}
|
||||
h['version'] = hex_to_int(s[0:4])
|
||||
|
@ -120,7 +89,9 @@ class Blockchain(util.PrintError):
|
|||
return h
|
||||
|
||||
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):
|
||||
return os.path.join(self.config.path, 'blockchain_headers')
|
||||
|
@ -148,7 +119,7 @@ class Blockchain(util.PrintError):
|
|||
self.set_local_height()
|
||||
|
||||
def save_header(self, header):
|
||||
data = self.header_to_string(header).decode('hex')
|
||||
data = self.serialize_header(header).decode('hex')
|
||||
assert len(data) == 80
|
||||
height = header.get('block_height')
|
||||
filename = self.path()
|
||||
|
@ -173,53 +144,42 @@ class Blockchain(util.PrintError):
|
|||
h = f.read(80)
|
||||
f.close()
|
||||
if len(h) == 80:
|
||||
h = self.header_from_string(h)
|
||||
h = self.deserialize_header(h)
|
||||
return h
|
||||
|
||||
def get_target(self, index, chain=None):
|
||||
if chain is None:
|
||||
chain = [] # Do not use mutables as default values!
|
||||
|
||||
max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
|
||||
if index == 0: return 0x1d00ffff, max_target
|
||||
|
||||
if index == 0:
|
||||
return 0x1d00ffff, MAX_TARGET
|
||||
first = self.read_header((index-1) * 2016)
|
||||
last = self.read_header(index*2016 - 1)
|
||||
if last is None:
|
||||
for h in chain:
|
||||
if h.get('block_height') == index*2016 - 1:
|
||||
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')
|
||||
nTargetTimespan = 14 * 24 * 60 * 60
|
||||
nActualTimespan = max(nActualTimespan, nTargetTimespan / 4)
|
||||
nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
|
||||
|
||||
bits = last.get('bits')
|
||||
# convert to bignum
|
||||
MM = 256*256*256
|
||||
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":
|
||||
new_target = min(MAX_TARGET, (target*nActualTimespan) / nTargetTimespan)
|
||||
# convert new target to bits
|
||||
c = ("%064x" % new_target)[2:]
|
||||
while c[:2] == '00' and len(c) > 6:
|
||||
c = c[2:]
|
||||
i -= 1
|
||||
|
||||
c = int('0x'+c[0:6],16)
|
||||
if c >= 0x800000:
|
||||
c /= 256
|
||||
i += 1
|
||||
|
||||
new_bits = c + MM * i
|
||||
return new_bits, new_target
|
||||
bitsN, bitsBase = len(c) / 2, int('0x' + c[:6], 16)
|
||||
if bitsBase >= 0x800000:
|
||||
bitsN += 1
|
||||
bitsBase >>= 8
|
||||
new_bits = bitsN << 24 | bitsBase
|
||||
return new_bits, bitsBase << (8 * (bitsN-3))
|
||||
|
||||
def connect_header(self, chain, header):
|
||||
'''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
|
||||
chain.reverse()
|
||||
if self.verify_chain(chain):
|
||||
try:
|
||||
self.verify_chain(chain)
|
||||
self.print_error("connected at height:", previous_height)
|
||||
for header in chain:
|
||||
self.save_header(header)
|
||||
return True
|
||||
|
||||
except BaseException as e:
|
||||
self.print_error(str(e))
|
||||
return False
|
||||
|
||||
def connect_chunk(self, idx, chunk):
|
||||
def connect_chunk(self, idx, hexdata):
|
||||
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
|
||||
except Exception:
|
||||
self.print_error('verify_chunk failed')
|
||||
except BaseException as e:
|
||||
self.print_error('verify_chunk failed', str(e))
|
||||
return idx - 1
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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
|
||||
# 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/>.
|
||||
|
||||
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 transaction import Transaction
|
||||
|
@ -25,6 +26,15 @@ from util import NotEnoughFunds, PrintError, profiler
|
|||
|
||||
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):
|
||||
|
||||
def keys(self, coins):
|
||||
|
@ -49,17 +59,25 @@ class CoinChooserBase(PrintError):
|
|||
return 0
|
||||
return penalty
|
||||
|
||||
def add_change(self, tx, change_addrs, fee_estimator, dust_threshold):
|
||||
# How much is left if we add 1 change output?
|
||||
change_amount = tx.get_fee() - fee_estimator(1)
|
||||
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
|
||||
# The amount left after adding 1 change output
|
||||
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
|
||||
# size of the change output, add it to the transaction.
|
||||
if change_amount > dust_threshold:
|
||||
tx.outputs.append(('address', change_addrs[0], change_amount))
|
||||
self.print_error('change', change_amount)
|
||||
elif change_amount:
|
||||
self.print_error('not keeping dust', change_amount)
|
||||
dust = sum(amount for amount in amounts if amount < dust_threshold)
|
||||
amounts = [amount for amount in amounts if amount >= dust_threshold]
|
||||
change = [('address', addr, amount)
|
||||
for addr, amount in zip(change_addrs, amounts)]
|
||||
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,
|
||||
dust_threshold):
|
||||
|
@ -72,12 +90,18 @@ class CoinChooserBase(PrintError):
|
|||
tx = Transaction.from_io([], outputs[:])
|
||||
# Size of the transaction with no inputs and no change
|
||||
base_size = tx.estimated_size()
|
||||
# Returns fee given input size
|
||||
fee = lambda input_size: fee_estimator(base_size + input_size)
|
||||
spent_amount = tx.output_value()
|
||||
|
||||
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
|
||||
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))
|
||||
|
||||
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;
|
||||
# each pay-to-bitcoin-address output serializes as 34 bytes
|
||||
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 buckets:", [bucket.desc for bucket in buckets])
|
||||
|
||||
return tx
|
||||
|
||||
class CoinChooserClassic(CoinChooserBase):
|
||||
'''
|
||||
The classic electrum algorithm. Chooses coins starting with
|
||||
the oldest that are sufficient to cover the spent amount, and
|
||||
then removes any unneeded starting with the smallest in value.'''
|
||||
class CoinChooserOldestFirst(CoinChooserBase):
|
||||
'''The classic electrum algorithm. Chooses coins starting with the
|
||||
oldest that are sufficient to cover the spent amount, and then
|
||||
removes any unneeded starting with the smallest in value.'''
|
||||
|
||||
def keys(self, coins):
|
||||
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
|
||||
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.'''
|
||||
# Unconfirmed coins are young, not old
|
||||
adj_height = lambda height: 99999999 if height == 0 else height
|
||||
buckets.sort(key = lambda b: max(adj_height(coin['height'])
|
||||
for coin in b.coins))
|
||||
selected, value, size = [], 0, 0
|
||||
selected = []
|
||||
for bucket in buckets:
|
||||
selected.append(bucket)
|
||||
value += bucket.value
|
||||
size += bucket.size
|
||||
if value >= spent_amount + fee(size):
|
||||
break
|
||||
if sufficient_funds(selected):
|
||||
return strip_unneeded(selected, sufficient_funds)
|
||||
else:
|
||||
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):
|
||||
|
||||
def bucket_candidates(self, buckets, sufficient_funds):
|
||||
|
@ -157,18 +168,11 @@ class CoinChooserRandom(CoinChooserBase):
|
|||
else:
|
||||
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 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)
|
||||
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
||||
candidates = self.bucket_candidates(buckets, sufficient_funds)
|
||||
penalties = [penalty_func(cand) for cand in candidates]
|
||||
winner = candidates[penalties.index(min(penalties))]
|
||||
self.print_error("Bucket sets:", len(buckets))
|
||||
|
@ -176,15 +180,19 @@ class CoinChooserRandom(CoinChooserBase):
|
|||
return winner
|
||||
|
||||
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
|
||||
from other addresses to make up an amount, this reduces
|
||||
information leakage about sender holdings. It also helps to
|
||||
reduce blockchain UTXO bloat, and reduce future privacy loss
|
||||
that would come from reusing that address' remaining UTXOs.
|
||||
Second, it penalizes change that is quite different to the sent
|
||||
amount. Third, it penalizes change that is too big.'''
|
||||
reduce blockchain UTXO bloat, and reduce future privacy loss that
|
||||
would come from reusing that address' remaining UTXOs. Second, it
|
||||
penalizes change that is quite different to the sent amount.
|
||||
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):
|
||||
return [coin['address'] for coin in coins]
|
||||
|
@ -213,5 +221,52 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
|||
|
||||
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}
|
||||
|
|
|
@ -74,21 +74,22 @@ def command(s):
|
|||
|
||||
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.wallet = wallet
|
||||
self.network = network
|
||||
self._callback = callback
|
||||
self.password = None
|
||||
self._password = password
|
||||
self.new_password = new_password
|
||||
self.contacts = contacts.Contacts(self.config)
|
||||
|
||||
def _run(self, method, args, password_getter):
|
||||
cmd = known_commands[method]
|
||||
if cmd.requires_password and self.wallet.use_encryption:
|
||||
self.password = apply(password_getter,())
|
||||
self._password = apply(password_getter,())
|
||||
f = getattr(self, method)
|
||||
result = f(*args)
|
||||
self.password = None
|
||||
self._password = None
|
||||
if self._callback:
|
||||
apply(self._callback, ())
|
||||
return result
|
||||
|
@ -120,7 +121,9 @@ class Commands:
|
|||
@command('wp')
|
||||
def password(self):
|
||||
"""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('')
|
||||
def getconfig(self, key):
|
||||
|
@ -157,7 +160,7 @@ class Commands:
|
|||
"""
|
||||
return self.network.synchronous_get(('blockchain.address.get_history', [address]))
|
||||
|
||||
@command('nw')
|
||||
@command('w')
|
||||
def listunspent(self):
|
||||
"""List unspent outputs. Returns the list of unspent transaction
|
||||
outputs in your wallet."""
|
||||
|
@ -200,7 +203,7 @@ class Commands:
|
|||
outputs = map(lambda x: ('address', x[0], int(COIN*x[1])), outputs.items())
|
||||
tx = Transaction.from_io(tx_inputs, outputs)
|
||||
if not unsigned:
|
||||
self.wallet.sign_transaction(tx, self.password)
|
||||
self.wallet.sign_transaction(tx, self._password)
|
||||
return tx.as_dict()
|
||||
|
||||
@command('wp')
|
||||
|
@ -212,7 +215,7 @@ class Commands:
|
|||
pubkey = bitcoin.public_key_from_private_key(privkey)
|
||||
t.sign({pubkey:privkey})
|
||||
else:
|
||||
self.wallet.sign_transaction(t, self.password)
|
||||
self.wallet.sign_transaction(t, self._password)
|
||||
return t.as_dict()
|
||||
|
||||
@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."""
|
||||
is_list = type(address) is list
|
||||
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]
|
||||
|
||||
@command('w')
|
||||
|
@ -273,10 +276,9 @@ class Commands:
|
|||
"""Return the public keys for a wallet address. """
|
||||
return self.wallet.get_public_keys(address)
|
||||
|
||||
@command('nw')
|
||||
@command('w')
|
||||
def getbalance(self, account=None):
|
||||
"""Return the balance of your wallet. If run with the --offline flag,
|
||||
returns the last known balance."""
|
||||
"""Return the balance of your wallet. """
|
||||
if account is None:
|
||||
c, u, x = self.wallet.get_balance()
|
||||
else:
|
||||
|
@ -334,19 +336,19 @@ class Commands:
|
|||
@command('wp')
|
||||
def getmasterprivate(self):
|
||||
"""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')
|
||||
def getseed(self):
|
||||
"""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')
|
||||
|
||||
@command('wp')
|
||||
def importprivkey(self, privkey):
|
||||
"""Import a private key. """
|
||||
try:
|
||||
addr = self.wallet.import_key(privkey, self.password)
|
||||
addr = self.wallet.import_key(privkey, self._password)
|
||||
out = "Keypair imported: " + addr
|
||||
except Exception as e:
|
||||
out = "Error: " + str(e)
|
||||
|
@ -377,7 +379,7 @@ class Commands:
|
|||
def signmessage(self, address, message):
|
||||
"""Sign a message with a key. Use quotes if your message contains
|
||||
whitespaces"""
|
||||
sig = self.wallet.sign_message(address, message, self.password)
|
||||
sig = self.wallet.sign_message(address, message, self._password)
|
||||
return base64.b64encode(sig)
|
||||
|
||||
@command('')
|
||||
|
@ -415,24 +417,24 @@ class Commands:
|
|||
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
|
||||
str(tx) #this serializes
|
||||
if not unsigned:
|
||||
self.wallet.sign_transaction(tx, self.password)
|
||||
self.wallet.sign_transaction(tx, self._password)
|
||||
return tx
|
||||
|
||||
@command('wpn')
|
||||
@command('wp')
|
||||
def payto(self, destination, amount, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
|
||||
"""Create a transaction. """
|
||||
domain = [from_addr] if from_addr else None
|
||||
tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned)
|
||||
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):
|
||||
"""Create a multi-output transaction. """
|
||||
domain = [from_addr] if from_addr else None
|
||||
tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned)
|
||||
return tx.as_dict()
|
||||
|
||||
@command('wn')
|
||||
@command('w')
|
||||
def history(self):
|
||||
"""Wallet history. Returns the transaction history of your wallet."""
|
||||
balance = 0
|
||||
|
@ -443,7 +445,7 @@ class Commands:
|
|||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||
except Exception:
|
||||
time_str = "----"
|
||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
||||
label = self.wallet.get_label(tx_hash)
|
||||
out.append({
|
||||
'txid':tx_hash,
|
||||
'timestamp':timestamp,
|
||||
|
@ -502,7 +504,7 @@ class Commands:
|
|||
out.append(item)
|
||||
return out
|
||||
|
||||
@command('nw')
|
||||
@command('w')
|
||||
def gettransaction(self, txid):
|
||||
"""Retrieve a transaction. """
|
||||
tx = self.wallet.transactions.get(txid) if self.wallet else None
|
||||
|
@ -522,7 +524,7 @@ class Commands:
|
|||
@command('wp')
|
||||
def decrypt(self, pubkey, encrypted):
|
||||
"""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):
|
||||
pr_str = {
|
||||
|
@ -535,7 +537,7 @@ class Commands:
|
|||
out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
|
||||
return out
|
||||
|
||||
@command('wn')
|
||||
@command('w')
|
||||
def getrequest(self, key):
|
||||
"""Return a payment request"""
|
||||
r = self.wallet.get_payment_request(key, self.config)
|
||||
|
@ -548,7 +550,7 @@ class Commands:
|
|||
# """<Not implemented>"""
|
||||
# pass
|
||||
|
||||
@command('wn')
|
||||
@command('w')
|
||||
def listrequests(self, pending=False, expired=False, paid=False):
|
||||
"""List the payment requests you made."""
|
||||
out = self.wallet.get_sorted_requests(self.config)
|
||||
|
@ -565,7 +567,7 @@ class Commands:
|
|||
return map(self._format_request, out)
|
||||
|
||||
@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."""
|
||||
addr = self.wallet.get_unused_address(None)
|
||||
if addr is None:
|
||||
|
@ -573,7 +575,7 @@ class Commands:
|
|||
addr = self.wallet.create_new_address(None, False)
|
||||
else:
|
||||
return False
|
||||
amount = int(Decimal(requested_amount)*COIN)
|
||||
amount = int(COIN*Decimal(amount))
|
||||
expiration = int(expiration)
|
||||
req = self.wallet.make_payment_request(addr, amount, memo, expiration)
|
||||
self.wallet.add_payment_request(req, self.config)
|
||||
|
@ -587,7 +589,7 @@ class Commands:
|
|||
if not alias:
|
||||
raise BaseException('No alias in your configuration')
|
||||
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')
|
||||
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 = {
|
||||
'num': int,
|
||||
'nbits': int,
|
||||
'entropy': long,
|
||||
'pubkeys': json.loads,
|
||||
'inputs': json.loads,
|
||||
'outputs': json.loads,
|
||||
'tx_fee': lambda x: float(x) if x is not None else None,
|
||||
'amount': lambda x: float(x) if x!='!' else '!',
|
||||
'tx': json_loads,
|
||||
'pubkeys': json_loads,
|
||||
'inputs': json_loads,
|
||||
'outputs': json_loads,
|
||||
'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 = {
|
||||
|
@ -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("-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("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
|
||||
# create main parser
|
||||
parser = argparse.ArgumentParser(
|
||||
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.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("-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("-L", "--lang", dest="language", default=None, help="default language used in GUI")
|
||||
add_network_options(parser_gui)
|
||||
|
|
|
@ -103,7 +103,7 @@ class Daemon(DaemonThread):
|
|||
'nodes': self.network.get_interfaces(),
|
||||
'connected': self.network.is_connected(),
|
||||
'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':
|
||||
self.stop()
|
||||
|
@ -135,21 +135,19 @@ class Daemon(DaemonThread):
|
|||
return wallet
|
||||
|
||||
def run_cmdline(self, config_options):
|
||||
password = config_options.get('password')
|
||||
config = SimpleConfig(config_options)
|
||||
cmdname = config.get('cmd')
|
||||
cmd = known_commands[cmdname]
|
||||
wallet = self.load_wallet(config) if cmd.requires_wallet else None
|
||||
if wallet:
|
||||
wallet.wait_until_synchronized()
|
||||
# arguments passed to function
|
||||
args = map(lambda x: config.get(x), cmd.params)
|
||||
# decode json arguments
|
||||
args = map(json_decode, args)
|
||||
# options
|
||||
args += map(lambda x: config.get(x), cmd.options)
|
||||
cmd_runner = Commands(config, wallet, self.network)
|
||||
cmd_runner.password = password
|
||||
cmd_runner = Commands(config, wallet, self.network,
|
||||
password=config_options.get('password'),
|
||||
new_password=config_options.get('new_password'))
|
||||
func = getattr(cmd_runner, cmd.name)
|
||||
result = func(*args)
|
||||
return result
|
||||
|
|
|
@ -12,7 +12,7 @@ proc = None
|
|||
def scan_qr(config):
|
||||
global proc
|
||||
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:
|
||||
device = config.get("video_device", "default")
|
||||
if device == 'default':
|
||||
|
|
|
@ -179,5 +179,5 @@ class Synchronizer(ThreadJob):
|
|||
if up_to_date != self.wallet.is_up_to_date():
|
||||
self.wallet.set_up_to_date(up_to_date)
|
||||
if up_to_date:
|
||||
self.wallet.save_transactions()
|
||||
self.wallet.save_transactions(write=True)
|
||||
self.network.trigger_callback('updated')
|
||||
|
|
|
@ -57,7 +57,7 @@ class TestWalletStorage(WalletTestCase):
|
|||
some_dict = {"a":"b", "c":"d"}
|
||||
|
||||
for key, value in some_dict.items():
|
||||
storage.put(key, value, False)
|
||||
storage.put(key, value)
|
||||
storage.write()
|
||||
|
||||
contents = ""
|
||||
|
|
31
lib/util.py
31
lib/util.py
|
@ -10,6 +10,8 @@ import urllib
|
|||
import threading
|
||||
from i18n import _
|
||||
|
||||
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
|
||||
|
||||
def normalize_version(v):
|
||||
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
|
||||
|
||||
|
@ -147,20 +149,20 @@ def json_encode(obj):
|
|||
|
||||
def json_decode(x):
|
||||
try:
|
||||
return json.loads(x)
|
||||
return json.loads(x, parse_float=decimal.Decimal)
|
||||
except:
|
||||
return x
|
||||
|
||||
# decorator that prints execution time
|
||||
def profiler(func):
|
||||
def do_profile(func, args):
|
||||
def do_profile(func, args, kw_args):
|
||||
n = func.func_name
|
||||
t0 = time.time()
|
||||
o = apply(func, args)
|
||||
o = func(*args, **kw_args)
|
||||
t = time.time() - t0
|
||||
print_error("[profiler]", n, "%.4f"%t)
|
||||
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)
|
||||
#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
|
||||
from bitcoin import COIN
|
||||
|
||||
|
@ -364,6 +366,22 @@ def parse_URI(uri):
|
|||
if 'sig' in out:
|
||||
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
|
||||
|
||||
|
||||
|
@ -561,11 +579,12 @@ class StoreDict(dict):
|
|||
|
||||
|
||||
def check_www_dir(rdir):
|
||||
# rewrite index.html every time
|
||||
import urllib, urlparse, shutil, os
|
||||
if not os.path.exists(rdir):
|
||||
os.mkdir(rdir)
|
||||
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')
|
||||
shutil.copy(src, index)
|
||||
files = [
|
||||
|
|
|
@ -26,7 +26,7 @@ import json
|
|||
import copy
|
||||
from functools import partial
|
||||
|
||||
from util import PrintError, profiler
|
||||
from util import NotEnoughFunds, PrintError, profiler
|
||||
|
||||
from bitcoin import *
|
||||
from account import *
|
||||
|
@ -69,11 +69,11 @@ class WalletStorage(PrintError):
|
|||
except:
|
||||
try:
|
||||
d = ast.literal_eval(data) #parse raw data from reading wallet file
|
||||
labels = d.get('labels', {})
|
||||
except Exception as e:
|
||||
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||
self.data = {}
|
||||
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
|
||||
labels = d.get('labels', {})
|
||||
for i, label in labels.items():
|
||||
try:
|
||||
unicode(label)
|
||||
|
@ -98,7 +98,7 @@ class WalletStorage(PrintError):
|
|||
v = copy.deepcopy(v)
|
||||
return v
|
||||
|
||||
def put(self, key, value, save = True):
|
||||
def put(self, key, value):
|
||||
try:
|
||||
json.dumps(key)
|
||||
json.dumps(value)
|
||||
|
@ -113,8 +113,6 @@ class WalletStorage(PrintError):
|
|||
elif key in self.data:
|
||||
self.modified = True
|
||||
self.data.pop(key)
|
||||
if save:
|
||||
self.write()
|
||||
|
||||
def write(self):
|
||||
if threading.currentThread().isDaemon():
|
||||
|
@ -142,7 +140,7 @@ class WalletStorage(PrintError):
|
|||
import stat
|
||||
os.chmod(self.path, mode)
|
||||
self.print_error("saved", self.path)
|
||||
|
||||
self.modified = False
|
||||
|
||||
|
||||
class Abstract_Wallet(PrintError):
|
||||
|
@ -199,7 +197,7 @@ class Abstract_Wallet(PrintError):
|
|||
|
||||
# save wallet type the first time
|
||||
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):
|
||||
return self.basename()
|
||||
|
@ -220,17 +218,18 @@ class Abstract_Wallet(PrintError):
|
|||
self.transactions.pop(tx_hash)
|
||||
|
||||
@profiler
|
||||
def save_transactions(self):
|
||||
def save_transactions(self, write=False):
|
||||
with self.transaction_lock:
|
||||
tx = {}
|
||||
for k,v in self.transactions.items():
|
||||
tx[k] = str(v)
|
||||
# Flush storage only with the last put
|
||||
self.storage.put('transactions', tx, False)
|
||||
self.storage.put('txi', self.txi, False)
|
||||
self.storage.put('txo', self.txo, False)
|
||||
self.storage.put('pruned_txo', self.pruned_txo, False)
|
||||
self.storage.put('addr_history', self.history, True)
|
||||
self.storage.put('transactions', tx)
|
||||
self.storage.put('txi', self.txi)
|
||||
self.storage.put('txo', self.txo)
|
||||
self.storage.put('pruned_txo', self.pruned_txo)
|
||||
self.storage.put('addr_history', self.history)
|
||||
if write:
|
||||
self.storage.write()
|
||||
|
||||
def clear_history(self):
|
||||
with self.transaction_lock:
|
||||
|
@ -376,7 +375,7 @@ class Abstract_Wallet(PrintError):
|
|||
|
||||
if changed:
|
||||
run_hook('set_label', self, name, text)
|
||||
self.storage.put('labels', self.labels, True)
|
||||
self.storage.put('labels', self.labels)
|
||||
|
||||
return changed
|
||||
|
||||
|
@ -436,7 +435,7 @@ class Abstract_Wallet(PrintError):
|
|||
self.unverified_tx.pop(tx_hash, None)
|
||||
with self.lock:
|
||||
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)
|
||||
self.network.trigger_callback('verified', tx_hash, conf, timestamp)
|
||||
|
@ -867,24 +866,9 @@ class Abstract_Wallet(PrintError):
|
|||
|
||||
return h2
|
||||
|
||||
|
||||
def get_label(self, tx_hash):
|
||||
label = self.labels.get(tx_hash)
|
||||
is_default = (label == '') or (label is None)
|
||||
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 ''
|
||||
label = self.labels.get(tx_hash, '')
|
||||
return label
|
||||
|
||||
def fee_per_kb(self, config):
|
||||
b = config.get('dynamic_fees')
|
||||
|
@ -899,7 +883,7 @@ class Abstract_Wallet(PrintError):
|
|||
def coin_chooser_name(self, config):
|
||||
kind = config.get('coin_chooser')
|
||||
if not kind in COIN_CHOOSERS:
|
||||
kind = 'Classic'
|
||||
kind = 'Oldest First'
|
||||
return kind
|
||||
|
||||
def coin_chooser(self, config):
|
||||
|
@ -912,6 +896,10 @@ class Abstract_Wallet(PrintError):
|
|||
if type == 'address':
|
||||
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:
|
||||
self.add_input_info(item)
|
||||
|
||||
|
@ -1044,7 +1032,7 @@ class Abstract_Wallet(PrintError):
|
|||
if self.has_seed():
|
||||
decoded = self.get_seed(old_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)
|
||||
if imported_account:
|
||||
|
@ -1056,10 +1044,10 @@ class Abstract_Wallet(PrintError):
|
|||
b = pw_decode(v, old_password)
|
||||
c = pw_encode(b, new_password)
|
||||
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.storage.put('use_encryption', self.use_encryption,True)
|
||||
self.storage.put('use_encryption', self.use_encryption)
|
||||
|
||||
def is_frozen(self, addr):
|
||||
return addr in self.frozen_addresses
|
||||
|
@ -1071,7 +1059,7 @@ class Abstract_Wallet(PrintError):
|
|||
self.frozen_addresses |= set(addrs)
|
||||
else:
|
||||
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 False
|
||||
|
||||
|
@ -1109,7 +1097,8 @@ class Abstract_Wallet(PrintError):
|
|||
self.verifier = None
|
||||
# Now no references to the syncronizer or verifier
|
||||
# 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):
|
||||
from i18n import _
|
||||
|
@ -1147,7 +1136,7 @@ class Abstract_Wallet(PrintError):
|
|||
d = {}
|
||||
for k, v in self.accounts.items():
|
||||
d[k] = v.dump()
|
||||
self.storage.put('accounts', d, True)
|
||||
self.storage.put('accounts', d)
|
||||
|
||||
def can_import(self):
|
||||
return not self.is_watching_only()
|
||||
|
@ -1431,9 +1420,9 @@ class Deterministic_Wallet(Abstract_Wallet):
|
|||
else:
|
||||
self.use_encryption = False
|
||||
|
||||
self.storage.put('seed', self.seed, False)
|
||||
self.storage.put('seed_version', self.seed_version, False)
|
||||
self.storage.put('use_encryption', self.use_encryption,True)
|
||||
self.storage.put('seed', self.seed)
|
||||
self.storage.put('seed_version', self.seed_version)
|
||||
self.storage.put('use_encryption', self.use_encryption)
|
||||
|
||||
def get_seed(self, 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)
|
||||
if value >= self.gap_limit:
|
||||
self.gap_limit = value
|
||||
self.storage.put('gap_limit', self.gap_limit, True)
|
||||
self.storage.put('gap_limit', self.gap_limit)
|
||||
return True
|
||||
|
||||
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_addresses = account.receiving_addresses[0:n]
|
||||
self.gap_limit = value
|
||||
self.storage.put('gap_limit', self.gap_limit, True)
|
||||
self.storage.put('gap_limit', self.gap_limit)
|
||||
self.save_accounts()
|
||||
return True
|
||||
else:
|
||||
|
@ -1579,11 +1568,11 @@ class BIP32_Wallet(Deterministic_Wallet):
|
|||
if xpub in self.master_public_keys.values():
|
||||
raise BaseException('Duplicate master public key')
|
||||
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):
|
||||
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):
|
||||
x = self.master_private_keys[root]
|
||||
|
@ -1626,16 +1615,16 @@ class BIP32_Simple_Wallet(BIP32_Wallet):
|
|||
def create_xprv_wallet(self, xprv, password):
|
||||
xpub = bitcoin.xpub_from_xprv(xprv)
|
||||
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_public_key(self.root_name, xpub)
|
||||
self.add_account('0', account)
|
||||
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):
|
||||
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_account('0', account)
|
||||
|
||||
|
@ -1834,7 +1823,7 @@ class OldWallet(Deterministic_Wallet):
|
|||
def create_master_keys(self, password):
|
||||
seed = self.get_seed(password)
|
||||
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):
|
||||
return self.storage.get("master_public_key")
|
||||
|
@ -1852,8 +1841,8 @@ class OldWallet(Deterministic_Wallet):
|
|||
|
||||
def create_watching_only_wallet(self, mpk):
|
||||
self.seed_version = OLD_SEED_VERSION
|
||||
self.storage.put('seed_version', self.seed_version, False)
|
||||
self.storage.put('master_public_key', mpk, True)
|
||||
self.storage.put('seed_version', self.seed_version)
|
||||
self.storage.put('master_public_key', mpk)
|
||||
self.create_account(mpk)
|
||||
|
||||
def get_seed(self, password):
|
||||
|
@ -2037,7 +2026,7 @@ class Wallet(object):
|
|||
|
||||
@classmethod
|
||||
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)
|
||||
key_list = sorted(key_list, key = lambda x: klass.is_xpub(x))
|
||||
for i, text in enumerate(key_list):
|
||||
|
@ -2056,7 +2045,7 @@ class Wallet(object):
|
|||
else:
|
||||
self.add_cosigner_seed(text, name, password)
|
||||
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)
|
||||
return self
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
from electrum.plugins import BasePlugin, hook
|
||||
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
|
||||
from functools import partial
|
||||
import zlib
|
||||
import json
|
||||
from io import BytesIO
|
||||
import sys
|
||||
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:
|
||||
import amodem.audio
|
||||
import amodem.main
|
||||
|
@ -42,11 +42,10 @@ class Plugin(BasePlugin):
|
|||
return True
|
||||
|
||||
def settings_widget(self, window):
|
||||
return EnterButton(_('Settings'), self.settings_dialog)
|
||||
return EnterButton(_('Settings'), partial(self.settings_dialog, window))
|
||||
|
||||
def settings_dialog(self):
|
||||
d = QDialog()
|
||||
d.setWindowTitle("Settings")
|
||||
def settings_dialog(self, window):
|
||||
d = WindowModalDialog(window, _("Audio Modem Settings"))
|
||||
|
||||
layout = QGridLayout(d)
|
||||
layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0)
|
||||
|
@ -75,24 +74,20 @@ class Plugin(BasePlugin):
|
|||
|
||||
def handler():
|
||||
blob = json.dumps(dialog.tx.as_dict())
|
||||
self.sender = self._send(parent=dialog, blob=blob)
|
||||
self.sender.start()
|
||||
self._send(parent=dialog, blob=blob)
|
||||
b.clicked.connect(handler)
|
||||
dialog.sharing_buttons.insert(-1, b)
|
||||
|
||||
@hook
|
||||
def scan_text_edit(self, parent):
|
||||
def handler():
|
||||
self.receiver = self._recv(parent=parent)
|
||||
self.receiver.start()
|
||||
parent.addButton(':icons/microphone.png', handler, _("Read from microphone"))
|
||||
parent.addButton(':icons/microphone.png', partial(self._recv, parent),
|
||||
_("Read from microphone"))
|
||||
|
||||
@hook
|
||||
def show_text_edit(self, parent):
|
||||
def handler():
|
||||
blob = str(parent.toPlainText())
|
||||
self.sender = self._send(parent=parent, blob=blob)
|
||||
self.sender.start()
|
||||
self._send(parent=parent, blob=blob)
|
||||
parent.addButton(':icons/speaker.png', handler, _("Send to speaker"))
|
||||
|
||||
def _audio_interface(self):
|
||||
|
@ -101,31 +96,25 @@ class Plugin(BasePlugin):
|
|||
|
||||
def _send(self, parent, blob):
|
||||
def sender_thread():
|
||||
try:
|
||||
with self._audio_interface() as interface:
|
||||
src = BytesIO(blob)
|
||||
dst = interface.player()
|
||||
amodem.main.send(config=self.modem_config, src=src, dst=dst)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
print_msg('Sending:', repr(blob))
|
||||
blob = zlib.compress(blob)
|
||||
|
||||
kbps = self.modem_config.modem_bps / 1e3
|
||||
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 receiver_thread():
|
||||
try:
|
||||
with self._audio_interface() as interface:
|
||||
src = interface.recorder()
|
||||
dst = BytesIO()
|
||||
amodem.main.recv(config=self.modem_config, src=src, dst=dst)
|
||||
return dst.getvalue()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
def on_success(blob):
|
||||
if blob:
|
||||
|
@ -135,5 +124,4 @@ class Plugin(BasePlugin):
|
|||
|
||||
kbps = self.modem_config.modem_bps / 1e3
|
||||
msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps)
|
||||
return WaitingDialog(parent=parent, message=msg,
|
||||
run_task=receiver_thread, on_success=on_success)
|
||||
WaitingDialog(parent, msg, receiver_thread, on_success=on_success)
|
||||
|
|
|
@ -18,12 +18,10 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
import base64
|
||||
from decimal import Decimal
|
||||
from Queue import Queue
|
||||
from functools import partial
|
||||
|
||||
import smtplib
|
||||
import imaplib
|
||||
|
@ -37,12 +35,11 @@ from PyQt4.QtCore import *
|
|||
import PyQt4.QtCore as QtCore
|
||||
import PyQt4.QtGui as QtGui
|
||||
|
||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
|
||||
from electrum.plugins import BasePlugin, hook
|
||||
from electrum import util
|
||||
from electrum.paymentrequest import PaymentRequest
|
||||
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
|
||||
|
||||
def settings_widget(self, window):
|
||||
self.settings_window = window
|
||||
return EnterButton(_('Settings'), self.settings_dialog)
|
||||
return EnterButton(_('Settings'), partial(self.settings_dialog, window))
|
||||
|
||||
def settings_dialog(self, x):
|
||||
from electrum_gui.qt.util import Buttons, CloseButton, OkButton
|
||||
|
||||
d = QDialog(self.settings_window)
|
||||
d.setWindowTitle("Email settings")
|
||||
def settings_dialog(self, window):
|
||||
d = WindowModalDialog(window, _("Email settings"))
|
||||
d.setMinimumSize(500, 200)
|
||||
|
||||
vbox = QVBoxLayout(d)
|
||||
|
|
|
@ -346,13 +346,18 @@ class FxPlugin(BasePlugin, ThreadJob):
|
|||
return _("No data")
|
||||
|
||||
@hook
|
||||
def historical_value_str(self, satoshis, d_t):
|
||||
def history_rate(self, d_t):
|
||||
rate = self.exchange.historical_rate(self.ccy, d_t)
|
||||
# Frequently there is no rate for today, until tomorrow :)
|
||||
# Use spot quotes in that case
|
||||
if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
|
||||
rate = self.exchange.quotes.get(self.ccy)
|
||||
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)
|
||||
|
||||
@hook
|
||||
|
|
|
@ -128,11 +128,10 @@ class Plugin(FxPlugin):
|
|||
window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
|
||||
|
||||
def settings_widget(self, window):
|
||||
return EnterButton(_('Settings'), self.settings_dialog)
|
||||
return EnterButton(_('Settings'), partial(self.settings_dialog, window))
|
||||
|
||||
def settings_dialog(self):
|
||||
d = QDialog()
|
||||
d.setWindowTitle("Settings")
|
||||
def settings_dialog(self, window):
|
||||
d = WindowModalDialog(window, _("Exchange Rate Settings"))
|
||||
layout = QGridLayout(d)
|
||||
layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
|
||||
layout.addWidget(QLabel(_('Currency: ')), 1, 0)
|
||||
|
|
|
@ -21,7 +21,7 @@ import urllib
|
|||
import sys
|
||||
import requests
|
||||
|
||||
from PyQt4.QtGui import QMessageBox, QApplication, QPushButton
|
||||
from PyQt4.QtGui import QApplication, QPushButton
|
||||
|
||||
from electrum.plugins import BasePlugin, hook
|
||||
from electrum.i18n import _
|
||||
|
@ -65,7 +65,7 @@ class Plugin(BasePlugin):
|
|||
'to verify that transaction is instant.\n'
|
||||
'Please enter your password to sign a\n'
|
||||
'verification request.')
|
||||
password = window.password_dialog(msg)
|
||||
password = window.password_dialog(msg, parent=d)
|
||||
if not password:
|
||||
return
|
||||
try:
|
||||
|
@ -84,14 +84,12 @@ class Plugin(BasePlugin):
|
|||
|
||||
# 3. display the result
|
||||
if response.get('verified'):
|
||||
QMessageBox.information(None, _('Verification successful!'),
|
||||
_('%s is covered by GreenAddress instant confirmation') % (tx.hash()), _('OK'))
|
||||
d.show_message(_('%s is covered by GreenAddress instant confirmation') % (tx.hash()), title=_('Verification successful!'))
|
||||
else:
|
||||
QMessageBox.critical(None, _('Verification failed!'),
|
||||
_('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), _('OK'))
|
||||
d.show_critical(_('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), title=_('Verification failed!'))
|
||||
except BaseException as e:
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
||||
d.show_error(str(e))
|
||||
finally:
|
||||
d.verify_button.setText(self.button_label)
|
||||
|
|
|
@ -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
|
||||
from electrum_gui.qt.util import *
|
||||
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
|
||||
|
@ -26,11 +26,11 @@ class Plugin(KeepKeyPlugin):
|
|||
try:
|
||||
self.get_client().ping('t')
|
||||
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
|
||||
return
|
||||
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
|
||||
|
||||
@hook
|
||||
|
@ -73,7 +73,7 @@ class Plugin(KeepKeyPlugin):
|
|||
return
|
||||
get_label = lambda: self.get_client().features.label
|
||||
update_label = lambda: current_label_label.setText("Label: %s" % get_label())
|
||||
d = QDialog()
|
||||
d = WindowModalDialog(window, _("KeepKey Settings"))
|
||||
layout = QGridLayout(d)
|
||||
layout.addWidget(QLabel("KeepKey Options"),0,0)
|
||||
layout.addWidget(QLabel("ID:"),1,0)
|
||||
|
@ -132,10 +132,7 @@ class KeepKeyQtHandler:
|
|||
return self.passphrase
|
||||
|
||||
def pin_dialog(self):
|
||||
d = QDialog(None)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_("Enter PIN"))
|
||||
d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
||||
d = WindowModalDialog(self.win, _("Enter PIN"))
|
||||
matrix = PinMatrixWidget()
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(QLabel(self.message))
|
||||
|
@ -153,23 +150,18 @@ class KeepKeyQtHandler:
|
|||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||
else:
|
||||
assert type(self.win) is InstallWizard
|
||||
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
|
||||
d = QDialog()
|
||||
d.setModal(1)
|
||||
d.setLayout(make_password_dialog(d, None, self.message, False))
|
||||
confirmed, p, passphrase = run_password_dialog(d, None, None)
|
||||
from electrum_gui.qt.password_dialog import PasswordDialog
|
||||
d = PasswordDialog(self.win, None, None, self.message, False)
|
||||
confirmed, p, passphrase = d.run()
|
||||
if not confirmed:
|
||||
QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK'))
|
||||
self.win.show_critical(_("Password request canceled"))
|
||||
self.passphrase = None
|
||||
else:
|
||||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||
self.done.set()
|
||||
|
||||
def message_dialog(self):
|
||||
self.d = QDialog()
|
||||
self.d.setModal(1)
|
||||
self.d.setWindowTitle('Please Check KeepKey Device')
|
||||
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.d = WindowModalDialog(self.win, _('Please Check KeepKey Device'))
|
||||
l = QLabel(self.message)
|
||||
vbox = QVBoxLayout(self.d)
|
||||
vbox.addWidget(l)
|
||||
|
@ -182,5 +174,3 @@ class KeepKeyQtHandler:
|
|||
|
||||
def dialog_stop(self):
|
||||
self.d.hide()
|
||||
|
||||
|
||||
|
|
|
@ -42,9 +42,9 @@ class LabelsPlugin(BasePlugin):
|
|||
self.set_nonce(wallet, 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)
|
||||
wallet.storage.put("wallet_nonce", nonce, force_write)
|
||||
wallet.storage.put("wallet_nonce", nonce)
|
||||
|
||||
@hook
|
||||
def set_label(self, wallet, item, label):
|
||||
|
@ -61,7 +61,7 @@ class LabelsPlugin(BasePlugin):
|
|||
t.setDaemon(True)
|
||||
t.start()
|
||||
# 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):
|
||||
url = 'https://' + self.target_host + url
|
||||
|
@ -125,8 +125,8 @@ class LabelsPlugin(BasePlugin):
|
|||
|
||||
self.print_error("received %d labels" % len(response))
|
||||
# do not write to disk because we're in a daemon thread
|
||||
wallet.storage.put('labels', wallet.labels, False)
|
||||
self.set_nonce(wallet, response["nonce"] + 1, False)
|
||||
wallet.storage.put('labels', wallet.labels)
|
||||
self.set_nonce(wallet, response["nonce"] + 1)
|
||||
self.on_pulled(wallet)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
@ -6,7 +6,8 @@ from PyQt4.QtCore import *
|
|||
from electrum.plugins import hook
|
||||
from electrum.i18n import _
|
||||
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
|
||||
|
||||
|
@ -25,25 +26,23 @@ class Plugin(LabelsPlugin):
|
|||
partial(self.settings_dialog, window))
|
||||
|
||||
def settings_dialog(self, window):
|
||||
d = QDialog(window)
|
||||
wallet = window.parent().wallet
|
||||
d = WindowModalDialog(window, _("Label Settings"))
|
||||
vbox = QVBoxLayout(d)
|
||||
layout = QGridLayout()
|
||||
vbox.addLayout(layout)
|
||||
layout.addWidget(QLabel("Label sync options: "), 2, 0)
|
||||
self.upload = ThreadedButton("Force upload",
|
||||
partial(self.push_thread, window.wallet),
|
||||
partial(self.push_thread, wallet),
|
||||
self.done_processing)
|
||||
layout.addWidget(self.upload, 2, 1)
|
||||
self.download = ThreadedButton("Force download",
|
||||
partial(self.pull_thread, window.wallet, True),
|
||||
partial(self.pull_thread, wallet, True),
|
||||
self.done_processing)
|
||||
layout.addWidget(self.download, 2, 2)
|
||||
self.accept = OkButton(d, _("Done"))
|
||||
vbox.addLayout(Buttons(CancelButton(d), self.accept))
|
||||
if d.exec_():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(d.exec_())
|
||||
|
||||
def on_pulled(self, wallet):
|
||||
self.obj.emit(SIGNAL('labels_changed'), wallet)
|
||||
|
|
|
@ -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 threading
|
||||
|
||||
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
|
||||
from electrum.plugins import BasePlugin, hook
|
||||
|
||||
from ledger import LedgerPlugin
|
||||
|
@ -16,10 +16,10 @@ class Plugin(LedgerPlugin):
|
|||
self.handler = BTChipQTHandler(window)
|
||||
if self.btchip_is_connected():
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
from electrum_gui.qt.util import *
|
||||
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
|
||||
|
@ -46,10 +46,7 @@ class TrezorQtHandler:
|
|||
return self.passphrase
|
||||
|
||||
def pin_dialog(self):
|
||||
d = QDialog(None)
|
||||
d.setModal(1)
|
||||
d.setWindowTitle(_("Enter PIN"))
|
||||
d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
||||
d = WindowModalDialog(self.win, _("Enter PIN"))
|
||||
matrix = PinMatrixWidget()
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(QLabel(self.message))
|
||||
|
@ -67,23 +64,18 @@ class TrezorQtHandler:
|
|||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||
else:
|
||||
assert type(self.win) is InstallWizard
|
||||
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
|
||||
d = QDialog()
|
||||
d.setModal(1)
|
||||
d.setLayout(make_password_dialog(d, None, self.message, False))
|
||||
confirmed, p, passphrase = run_password_dialog(d, None, None)
|
||||
from electrum_gui.qt.password_dialog import PasswordDialog
|
||||
d = PasswordDialog(self.win, None, None, self.message, False)
|
||||
confirmed, p, passphrase = d.run()
|
||||
if not confirmed:
|
||||
QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK'))
|
||||
self.win.show_critical(_("Password request canceled"))
|
||||
self.passphrase = None
|
||||
else:
|
||||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||
self.done.set()
|
||||
|
||||
def message_dialog(self):
|
||||
self.d = QDialog()
|
||||
self.d.setModal(1)
|
||||
self.d.setWindowTitle('Please Check Trezor Device')
|
||||
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.d = WindowModalDialog(self.win, _('Please Check Trezor Device'))
|
||||
l = QLabel(self.message)
|
||||
vbox = QVBoxLayout(self.d)
|
||||
vbox.addWidget(l)
|
||||
|
@ -108,11 +100,11 @@ class Plugin(TrezorPlugin):
|
|||
try:
|
||||
self.get_client().ping('t')
|
||||
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
|
||||
return
|
||||
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
|
||||
|
||||
@hook
|
||||
|
@ -171,7 +163,7 @@ class Plugin(TrezorPlugin):
|
|||
return
|
||||
get_label = lambda: self.get_client().features.label
|
||||
update_label = lambda: current_label_label.setText("Label: %s" % get_label())
|
||||
d = QDialog()
|
||||
d = WindowModalDialog(window, _("Trezor Settings"))
|
||||
layout = QGridLayout(d)
|
||||
layout.addWidget(QLabel("Trezor Options"),0,0)
|
||||
layout.addWidget(QLabel("ID:"),1,0)
|
||||
|
@ -194,7 +186,3 @@ class Plugin(TrezorPlugin):
|
|||
layout.addWidget(current_label_label,3,0)
|
||||
layout.addWidget(change_label_button,3,1)
|
||||
d.exec_()
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 threading import Thread
|
||||
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
|
@ -10,7 +29,19 @@ from electrum_gui.qt.main_window import StatusBarButton
|
|||
from electrum.i18n import _
|
||||
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):
|
||||
|
||||
|
@ -27,8 +58,7 @@ class Plugin(TrustedCoinPlugin):
|
|||
t.start()
|
||||
|
||||
def auth_dialog(self, window):
|
||||
d = QDialog(window)
|
||||
d.setModal(1)
|
||||
d = WindowModalDialog(window, _("Authorization"))
|
||||
vbox = QVBoxLayout(d)
|
||||
pw = AmountEdit(None, is_int = True)
|
||||
msg = _('Please enter your Google Authenticator code')
|
||||
|
@ -55,16 +85,18 @@ class Plugin(TrustedCoinPlugin):
|
|||
self.print_error("twofactor: xpub3 not needed")
|
||||
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
|
||||
def abort_send(self, window):
|
||||
wallet = window.wallet
|
||||
if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server():
|
||||
if wallet.billing_info is None:
|
||||
# request billing info before forming the transaction
|
||||
task = partial(self.request_billing_info, wallet)
|
||||
waiting_dialog = WaitingDialog(window, 'please wait...', task)
|
||||
waiting_dialog.start()
|
||||
waiting_dialog.wait()
|
||||
waiting_dialog(self, window).wait()
|
||||
if wallet.billing_info is None:
|
||||
window.show_message('Could not contact server')
|
||||
return True
|
||||
|
@ -72,9 +104,8 @@ class Plugin(TrustedCoinPlugin):
|
|||
|
||||
|
||||
def settings_dialog(self, window):
|
||||
task = partial(self.request_billing_info, window.wallet)
|
||||
self.waiting_dialog = WaitingDialog(window, 'please wait...', task, partial(self.show_settings_dialog, window))
|
||||
self.waiting_dialog.start()
|
||||
on_success = partial(self.show_settings_dialog, window)
|
||||
self.waiting_dialog(window, on_success)
|
||||
|
||||
def show_settings_dialog(self, window, success):
|
||||
if not success:
|
||||
|
@ -82,8 +113,7 @@ class Plugin(TrustedCoinPlugin):
|
|||
return
|
||||
|
||||
wallet = window.wallet
|
||||
d = QDialog(window)
|
||||
d.setWindowTitle("TrustedCoin Information")
|
||||
d = WindowModalDialog(window, _("TrustedCoin Information"))
|
||||
d.setMinimumSize(500, 200)
|
||||
vbox = QVBoxLayout(d)
|
||||
hbox = QHBoxLayout()
|
||||
|
@ -238,7 +268,5 @@ class Plugin(TrustedCoinPlugin):
|
|||
server.auth(_id, otp)
|
||||
return True
|
||||
except:
|
||||
QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK'))
|
||||
window.show_message(_('Incorrect password'))
|
||||
pw.setText('')
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
# 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 threading import Thread
|
||||
import socket
|
||||
import os
|
||||
import re
|
||||
|
@ -270,18 +269,6 @@ def make_billing_address(wallet, num):
|
|||
address = public_key_to_bc_address( cK )
|
||||
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):
|
||||
|
||||
|
@ -318,8 +305,8 @@ class TrustedCoinPlugin(BasePlugin):
|
|||
return
|
||||
|
||||
password = window.password_dialog()
|
||||
wallet.storage.put('seed_version', wallet.seed_version, True)
|
||||
wallet.storage.put('use_encryption', password is not None, True)
|
||||
wallet.storage.put('seed_version', wallet.seed_version)
|
||||
wallet.storage.put('use_encryption', password is not None)
|
||||
|
||||
words = seed.split()
|
||||
n = len(words)/2
|
||||
|
|
Loading…
Reference in New Issue