mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 09:21:39 +00:00
Merge branch 'master' of https://github.com/spesmilo/electrum
This commit is contained in:
commit
93bcd98763
70 changed files with 2153 additions and 3432 deletions
|
@ -2,6 +2,9 @@
|
||||||
* separation between plugins and GUIs
|
* separation between plugins and GUIs
|
||||||
* the daemon supports jsonrpc commands
|
* the daemon supports jsonrpc commands
|
||||||
* new command: 'notify <address> <url>'
|
* new command: 'notify <address> <url>'
|
||||||
|
* improved coin selection to help preserve user privacy. This is an
|
||||||
|
experimental feature. Enable it by setting the Coin Selection
|
||||||
|
preference to Privacy.
|
||||||
|
|
||||||
# Release 2.5.4
|
# Release 2.5.4
|
||||||
* increase MIN_RELAY_TX_FEE to avoid dust transactions
|
* increase MIN_RELAY_TX_FEE to avoid dust transactions
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python2
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys, re, shutil, os, hashlib
|
import sys, re, shutil, os, hashlib
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python2
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python2
|
||||||
|
|
||||||
import sys, re, shutil, os, hashlib
|
import sys, re, shutil, os, hashlib
|
||||||
import imp
|
import imp
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python2
|
||||||
|
|
||||||
import sys, re, shutil, os, hashlib
|
import sys, re, shutil, os, hashlib
|
||||||
import imp
|
import imp
|
||||||
|
|
239
electrum
239
electrum
|
@ -107,9 +107,74 @@ def init_gui(config, network, plugins):
|
||||||
return gui
|
return gui
|
||||||
|
|
||||||
|
|
||||||
|
def run_non_RPC(config):
|
||||||
|
cmdname = config.get('cmd')
|
||||||
|
|
||||||
def init_cmdline(config):
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
|
if storage.file_exists:
|
||||||
|
sys.exit("Error: Remove the existing wallet first!")
|
||||||
|
|
||||||
|
def password_dialog():
|
||||||
|
return prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
|
||||||
|
|
||||||
|
if cmdname == 'restore':
|
||||||
|
text = config.get('text')
|
||||||
|
password = password_dialog() if Wallet.is_seed(text) or Wallet.is_xprv(text) or Wallet.is_private_key(text) else None
|
||||||
|
try:
|
||||||
|
wallet = Wallet.from_text(text, password, storage)
|
||||||
|
except BaseException as e:
|
||||||
|
sys.exit(str(e))
|
||||||
|
if not config.get('offline'):
|
||||||
|
network = Network(config)
|
||||||
|
network.start()
|
||||||
|
wallet.start_threads(network)
|
||||||
|
print_msg("Recovering wallet...")
|
||||||
|
wallet.synchronize()
|
||||||
|
wallet.wait_until_synchronized()
|
||||||
|
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
|
||||||
|
else:
|
||||||
|
msg = "This wallet was restored offline. It may contain more addresses than displayed."
|
||||||
|
print_msg(msg)
|
||||||
|
|
||||||
|
elif cmdname == 'create':
|
||||||
|
password = password_dialog()
|
||||||
|
wallet = Wallet(storage)
|
||||||
|
seed = wallet.make_seed()
|
||||||
|
wallet.add_seed(seed, password)
|
||||||
|
wallet.create_master_keys(password)
|
||||||
|
wallet.create_main_account(password)
|
||||||
|
wallet.synchronize()
|
||||||
|
print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
|
||||||
|
print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
|
||||||
|
|
||||||
|
elif cmdname == 'deseed':
|
||||||
|
if not wallet.seed:
|
||||||
|
print_msg("Error: This wallet has no seed")
|
||||||
|
else:
|
||||||
|
ns = wallet.storage.path + '.seedless'
|
||||||
|
print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
|
||||||
|
if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
|
||||||
|
wallet.storage.path = ns
|
||||||
|
wallet.seed = ''
|
||||||
|
wallet.storage.put('seed', '')
|
||||||
|
wallet.use_encryption = False
|
||||||
|
wallet.storage.put('use_encryption', wallet.use_encryption)
|
||||||
|
for k in wallet.imported_keys.keys():
|
||||||
|
wallet.imported_keys[k] = ''
|
||||||
|
wallet.storage.put('imported_keys', wallet.imported_keys)
|
||||||
|
print_msg("Done.")
|
||||||
|
else:
|
||||||
|
print_msg("Action canceled.")
|
||||||
|
wallet.storage.write()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
wallet.storage.write()
|
||||||
|
print_msg("Wallet saved in '%s'" % wallet.storage.path)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def init_cmdline(config_options):
|
||||||
|
config = SimpleConfig(config_options)
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
cmd = known_commands[cmdname]
|
cmd = known_commands[cmdname]
|
||||||
|
|
||||||
|
@ -130,57 +195,11 @@ def init_cmdline(config):
|
||||||
# instanciate wallet for command-line
|
# instanciate wallet for command-line
|
||||||
storage = WalletStorage(config.get_wallet_path())
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
|
|
||||||
if cmd.name in ['create', 'restore']:
|
|
||||||
if storage.file_exists:
|
|
||||||
sys.exit("Error: Remove the existing wallet first!")
|
|
||||||
|
|
||||||
def password_dialog():
|
|
||||||
return prompt_password("Password (hit return if you do not wish to encrypt your wallet):")
|
|
||||||
|
|
||||||
if cmd.name == 'restore':
|
|
||||||
text = config.get('text')
|
|
||||||
password = password_dialog() if Wallet.is_seed(text) or Wallet.is_xprv(text) or Wallet.is_private_key(text) else None
|
|
||||||
try:
|
|
||||||
wallet = Wallet.from_text(text, password, storage)
|
|
||||||
except BaseException as e:
|
|
||||||
sys.exit(str(e))
|
|
||||||
if not config.get('offline'):
|
|
||||||
network = Network(config)
|
|
||||||
network.start()
|
|
||||||
wallet.start_threads(network)
|
|
||||||
print_msg("Recovering wallet...")
|
|
||||||
wallet.synchronize()
|
|
||||||
wallet.wait_until_synchronized()
|
|
||||||
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
|
|
||||||
else:
|
|
||||||
msg = "This wallet was restored offline. It may contain more addresses than displayed."
|
|
||||||
print_msg(msg)
|
|
||||||
|
|
||||||
else:
|
|
||||||
password = password_dialog()
|
|
||||||
wallet = Wallet(storage)
|
|
||||||
seed = wallet.make_seed()
|
|
||||||
wallet.add_seed(seed, password)
|
|
||||||
wallet.create_master_keys(password)
|
|
||||||
wallet.create_main_account(password)
|
|
||||||
wallet.synchronize()
|
|
||||||
print_msg("Your wallet generation seed is:\n\"%s\"" % seed)
|
|
||||||
print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.")
|
|
||||||
|
|
||||||
print_msg("Wallet saved in '%s'" % wallet.storage.path)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if cmd.requires_wallet and not storage.file_exists:
|
if cmd.requires_wallet and not storage.file_exists:
|
||||||
print_msg("Error: Wallet file not found.")
|
print_msg("Error: Wallet file not found.")
|
||||||
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
|
print_msg("Type 'electrum create' to create a new wallet, or provide a path to a wallet with the -w option")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# create wallet instance
|
|
||||||
wallet = Wallet(storage) if cmd.requires_wallet else None
|
|
||||||
|
|
||||||
# notify plugins
|
|
||||||
always_hook('cmdline_load_wallet', wallet)
|
|
||||||
|
|
||||||
# important warning
|
# important warning
|
||||||
if cmd.name in ['getprivatekeys']:
|
if cmd.name in ['getprivatekeys']:
|
||||||
print_stderr("WARNING: ALL your private keys are secret.")
|
print_stderr("WARNING: ALL your private keys are secret.")
|
||||||
|
@ -188,7 +207,7 @@ def init_cmdline(config):
|
||||||
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
|
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
|
||||||
|
|
||||||
# commands needing password
|
# commands needing password
|
||||||
if cmd.requires_password and wallet.use_encryption:
|
if cmd.requires_password and storage.get('use_encryption'):
|
||||||
if config.get('password'):
|
if config.get('password'):
|
||||||
password = config.get('password')
|
password = config.get('password')
|
||||||
else:
|
else:
|
||||||
|
@ -196,55 +215,49 @@ def init_cmdline(config):
|
||||||
if not password:
|
if not password:
|
||||||
print_msg("Error: Password required")
|
print_msg("Error: Password required")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
password = None
|
||||||
|
|
||||||
|
config_options['password'] = password
|
||||||
|
|
||||||
|
if cmd.name == 'password':
|
||||||
|
new_password = prompt_password('New password:')
|
||||||
|
config_options['new_password'] = new_password
|
||||||
|
|
||||||
|
return cmd, password
|
||||||
|
|
||||||
|
|
||||||
|
def run_offline_command(config, config_options):
|
||||||
|
cmdname = config.get('cmd')
|
||||||
|
cmd = known_commands[cmdname]
|
||||||
|
storage = WalletStorage(config.get_wallet_path())
|
||||||
|
wallet = Wallet(storage) if cmd.requires_wallet else None
|
||||||
# check password
|
# check password
|
||||||
|
if cmd.requires_password and storage.get('use_encryption'):
|
||||||
|
password = config_options.get('password')
|
||||||
try:
|
try:
|
||||||
seed = wallet.check_password(password)
|
seed = wallet.check_password(password)
|
||||||
except InvalidPassword:
|
except InvalidPassword:
|
||||||
print_msg("Error: This password does not decode this wallet.")
|
print_msg("Error: This password does not decode this wallet.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
if cmd.requires_network:
|
||||||
password = None
|
print_stderr("Warning: running command offline")
|
||||||
|
# notify plugins
|
||||||
# run the command
|
always_hook('cmdline_load_wallet', wallet)
|
||||||
if cmd.name == 'deseed':
|
|
||||||
if not wallet.seed:
|
|
||||||
print_msg("Error: This wallet has no seed")
|
|
||||||
else:
|
|
||||||
ns = wallet.storage.path + '.seedless'
|
|
||||||
print_msg("Warning: you are going to create a seedless wallet'\nIt will be saved in '%s'" % ns)
|
|
||||||
if raw_input("Are you sure you want to continue? (y/n) ") in ['y', 'Y', 'yes']:
|
|
||||||
wallet.storage.path = ns
|
|
||||||
wallet.seed = ''
|
|
||||||
wallet.storage.put('seed', '', True)
|
|
||||||
wallet.use_encryption = False
|
|
||||||
wallet.storage.put('use_encryption', wallet.use_encryption, True)
|
|
||||||
for k in wallet.imported_keys.keys():
|
|
||||||
wallet.imported_keys[k] = ''
|
|
||||||
wallet.storage.put('imported_keys', wallet.imported_keys, True)
|
|
||||||
print_msg("Done.")
|
|
||||||
else:
|
|
||||||
print_msg("Action canceled.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
elif cmd.name == 'password':
|
|
||||||
new_password = prompt_password('New password:')
|
|
||||||
wallet.update_password(password, new_password)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
return cmd, password, wallet
|
|
||||||
|
|
||||||
|
|
||||||
def run_offline_command(config, cmd, wallet, password):
|
|
||||||
# arguments passed to function
|
# arguments passed to function
|
||||||
args = map(lambda x: config.get(x), cmd.params)
|
args = map(lambda x: config.get(x), cmd.params)
|
||||||
# decode json arguments
|
# decode json arguments
|
||||||
args = map(json_decode, args)
|
args = map(json_decode, args)
|
||||||
# options
|
# options
|
||||||
args += map(lambda x: config.get(x), cmd.options)
|
args += map(lambda x: config.get(x), cmd.options)
|
||||||
cmd_runner = Commands(config, wallet, None)
|
cmd_runner = Commands(config, wallet, None,
|
||||||
cmd_runner.password = password
|
password=config_options.get('password'),
|
||||||
|
new_password=config_options.get('new_password'))
|
||||||
func = getattr(cmd_runner, cmd.name)
|
func = getattr(cmd_runner, cmd.name)
|
||||||
result = func(*args)
|
result = func(*args)
|
||||||
|
# save wallet
|
||||||
|
if wallet:
|
||||||
|
wallet.storage.write()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -308,43 +321,24 @@ if __name__ == '__main__':
|
||||||
config_options['url'] = uri
|
config_options['url'] = uri
|
||||||
|
|
||||||
config = SimpleConfig(config_options)
|
config = SimpleConfig(config_options)
|
||||||
cmd_name = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
|
|
||||||
# initialize plugins.
|
# initialize plugins.
|
||||||
gui_name = config.get('gui', 'qt') if cmd_name == 'gui' else 'cmdline'
|
gui_name = config.get('gui', 'qt') if cmdname == 'gui' else 'cmdline'
|
||||||
plugins = Plugins(config, is_bundle or is_local or is_android, gui_name)
|
plugins = Plugins(config, is_bundle or is_local or is_android, gui_name)
|
||||||
|
|
||||||
# run command offline
|
# run non-RPC commands separately
|
||||||
if cmd_name not in ['gui', 'daemon']:
|
if cmdname in ['create', 'restore', 'deseed']:
|
||||||
cmd, password, wallet = init_cmdline(config)
|
run_non_RPC(config)
|
||||||
if not cmd.requires_network or config.get('offline'):
|
|
||||||
result = run_offline_command(config, cmd, wallet, password)
|
|
||||||
print_msg(json_encode(result))
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
|
||||||
config_options['password'] = password
|
|
||||||
|
|
||||||
|
# check if a daemon is running
|
||||||
server = get_daemon(config)
|
server = get_daemon(config)
|
||||||
|
|
||||||
# daemon is running
|
if cmdname == 'gui':
|
||||||
if server is not None:
|
if server is not None:
|
||||||
cmdname = config_options.get('cmd')
|
|
||||||
if cmdname == 'daemon':
|
|
||||||
result = server.daemon(config_options)
|
|
||||||
elif cmdname == 'gui':
|
|
||||||
result = server.gui(config_options)
|
result = server.gui(config_options)
|
||||||
else:
|
else:
|
||||||
result = server.run_cmdline(config_options)
|
|
||||||
if type(result) in [str, unicode]:
|
|
||||||
print_msg(result)
|
|
||||||
elif type(result) is dict and result.get('error'):
|
|
||||||
print_stderr(result.get('error'))
|
|
||||||
elif result is not None:
|
|
||||||
print_msg(json_encode(result))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# daemon is not running
|
|
||||||
if cmd_name == 'gui':
|
|
||||||
if not config.get('offline'):
|
if not config.get('offline'):
|
||||||
network = Network(config)
|
network = Network(config)
|
||||||
network.start()
|
network.start()
|
||||||
|
@ -357,7 +351,10 @@ if __name__ == '__main__':
|
||||||
gui.main()
|
gui.main()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
elif cmd_name == 'daemon':
|
elif cmdname == 'daemon':
|
||||||
|
if server is not None:
|
||||||
|
result = server.daemon(config_options)
|
||||||
|
else:
|
||||||
subcommand = config.get('subcommand')
|
subcommand = config.get('subcommand')
|
||||||
if subcommand in ['status', 'stop']:
|
if subcommand in ['status', 'stop']:
|
||||||
print_msg("Daemon not running")
|
print_msg("Daemon not running")
|
||||||
|
@ -376,12 +373,32 @@ if __name__ == '__main__':
|
||||||
util.check_www_dir(config.get('requests_dir'))
|
util.check_www_dir(config.get('requests_dir'))
|
||||||
daemon.start()
|
daemon.start()
|
||||||
daemon.join()
|
daemon.join()
|
||||||
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
print_stderr("starting daemon (PID %d)"%p)
|
print_stderr("starting daemon (PID %d)"%p)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
print_msg("syntax: electrum daemon <start|status|stop>")
|
print_msg("syntax: electrum daemon <start|status|stop>")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print_msg("Network daemon is not running. Try 'electrum daemon start'\nIf you want to run this command offline, use the -o flag.")
|
# command line
|
||||||
|
init_cmdline(config_options)
|
||||||
|
if server is not None:
|
||||||
|
result = server.run_cmdline(config_options)
|
||||||
|
else:
|
||||||
|
cmd = known_commands[cmdname]
|
||||||
|
if cmd.requires_network:
|
||||||
|
print_msg("Network daemon is not running. Try 'electrum daemon start'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
result = run_offline_command(config, config_options)
|
||||||
|
|
||||||
|
# print result
|
||||||
|
if type(result) in [str, unicode]:
|
||||||
|
print_msg(result)
|
||||||
|
elif type(result) is dict and result.get('error'):
|
||||||
|
print_stderr(result.get('error'))
|
||||||
|
elif result is not None:
|
||||||
|
print_msg(json_encode(result))
|
||||||
|
sys.exit(0)
|
||||||
|
|
|
@ -332,7 +332,7 @@ def get_history_values(n):
|
||||||
except Exception:
|
except Exception:
|
||||||
time_str = 'pending'
|
time_str = 'pending'
|
||||||
conf_str = 'v' if conf else 'o'
|
conf_str = 'v' if conf else 'o'
|
||||||
label, is_default_label = wallet.get_label(tx_hash)
|
label = wallet.get_label(tx_hash)
|
||||||
label = label.replace('<','').replace('>','')
|
label = label.replace('<','').replace('>','')
|
||||||
values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value, True), ' ' + label))
|
values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value, True), ' ' + label))
|
||||||
|
|
||||||
|
|
|
@ -1185,7 +1185,7 @@ class ElectrumWindow:
|
||||||
time_str = 'pending'
|
time_str = 'pending'
|
||||||
conf_icon = Gtk.STOCK_EXECUTE
|
conf_icon = Gtk.STOCK_EXECUTE
|
||||||
|
|
||||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
label = self.wallet.get_label(tx_hash)
|
||||||
tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
|
tooltip = tx_hash + "\n%d confirmations"%conf if tx_hash else ''
|
||||||
details = self.get_tx_details(tx_hash)
|
details = self.get_tx_details(tx_hash)
|
||||||
|
|
||||||
|
@ -1300,7 +1300,7 @@ class ElectrumGui():
|
||||||
gap = self.config.get('gap_limit', 5)
|
gap = self.config.get('gap_limit', 5)
|
||||||
if gap != 5:
|
if gap != 5:
|
||||||
wallet.gap_limit = gap
|
wallet.gap_limit = gap
|
||||||
wallet.storage.put('gap_limit', gap, True)
|
wallet.storage.put('gap_limit', gap)
|
||||||
|
|
||||||
if action == 'create':
|
if action == 'create':
|
||||||
seed = wallet.make_seed()
|
seed = wallet.make_seed()
|
||||||
|
|
|
@ -29,6 +29,7 @@ except ImportError:
|
||||||
|
|
||||||
# minimum required version for kivy
|
# minimum required version for kivy
|
||||||
kivy.require('1.8.0')
|
kivy.require('1.8.0')
|
||||||
|
from electrum.i18n import set_language
|
||||||
from kivy.logger import Logger
|
from kivy.logger import Logger
|
||||||
from main_window import ElectrumWindow
|
from main_window import ElectrumWindow
|
||||||
|
|
||||||
|
@ -39,6 +40,7 @@ class ElectrumGui:
|
||||||
self.network = network
|
self.network = network
|
||||||
self.config = config
|
self.config = config
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
|
set_language(config.get('language'))
|
||||||
|
|
||||||
def main(self):
|
def main(self):
|
||||||
w = ElectrumWindow(config=self.config,
|
w = ElectrumWindow(config=self.config,
|
||||||
|
|
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 Window kivy.core.window.Window
|
||||||
#:import Factory kivy.factory.Factory
|
#:import Factory kivy.factory.Factory
|
||||||
#:import _ electrum.i18n._
|
#:import _ electrum.i18n._
|
||||||
|
|
||||||
# Custom Global Widgets
|
# Custom Global Widgets
|
||||||
|
<Button>
|
||||||
|
on_parent: self.MIN_STATE_TIME = 0.1
|
||||||
|
|
||||||
<VGridLayout@GridLayout>:
|
<VGridLayout@GridLayout>:
|
||||||
rows: 1
|
rows: 1
|
||||||
|
@ -187,22 +190,44 @@
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
|
|
||||||
|
<CardItem@ToggleButtonBehavior+BoxLayout>
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '65dp'
|
||||||
|
group: 'requests'
|
||||||
|
padding: dp(12)
|
||||||
|
spacing: dp(5)
|
||||||
|
screen: None
|
||||||
|
on_release:
|
||||||
|
self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu()
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1)
|
||||||
|
Rectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
|
||||||
<AddressSelector@BlueSpinner>
|
<AddressSelector@BlueSpinner>
|
||||||
icon: 'atlas://gui/kivy/theming/light/globe'
|
icon: 'atlas://gui/kivy/theming/light/globe'
|
||||||
values: [] #app.wallet.addresses() if app.wallet else []
|
values: [] #app.wallet.addresses() if app.wallet else []
|
||||||
text: _("Select Your address")
|
text: _("Select Your address")
|
||||||
|
|
||||||
<AmountButton@Button>:
|
<BlueButton@Button>:
|
||||||
background_color: .238, .585, .878, 0
|
background_color: .238, .585, .878, 0
|
||||||
halign: 'left'
|
halign: 'left'
|
||||||
text_size: (self.width-10, None)
|
text_size: (self.width-10, None)
|
||||||
size_hint: 0.5, None
|
size_hint: 0.5, None
|
||||||
default_text: 'Amount'
|
default_text: ''
|
||||||
text: self.default_text
|
text: self.default_text
|
||||||
padding: '5dp', '5db'
|
padding: '5dp', '5db'
|
||||||
height: '40dp'
|
height: '40dp'
|
||||||
text_color: self.foreground_color
|
text_color: self.foreground_color
|
||||||
foreground_color: 1, 0, 0, 1
|
foreground_color: 1, 0, 0, 1
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: (0.9, .498, 0.745, 1) if self.state == 'down' else self.background_color
|
||||||
|
Rectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
|
||||||
|
|
||||||
<TextInputBlue@TextInput>
|
<TextInputBlue@TextInput>
|
||||||
|
@ -222,24 +247,26 @@
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
on_release:
|
on_release:
|
||||||
self.parent.update_text(self.parent, self.text)
|
self.parent.update_amount(self.text)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<TabbedPanelStrip>:
|
<TabbedPanelStrip>:
|
||||||
on_parent:
|
on_parent:
|
||||||
if self.parent: self.parent.bar_width = 0
|
if self.parent: self.parent.bar_width = 0
|
||||||
|
if self.parent: self.parent.scroll_x = 0.5
|
||||||
|
|
||||||
|
|
||||||
<TabbedCarousel>
|
<TabbedCarousel>
|
||||||
carousel: carousel
|
carousel: carousel
|
||||||
do_default_tab: False
|
do_default_tab: False
|
||||||
Carousel:
|
Carousel:
|
||||||
scroll_timeout: 190
|
scroll_timeout: 250
|
||||||
|
scroll_distance: '20dp'
|
||||||
anim_type: 'out_quart'
|
anim_type: 'out_quart'
|
||||||
min_move: .05
|
min_move: .05
|
||||||
anim_move_duration: .1
|
anim_move_duration: .1
|
||||||
anim_cancel_duration: .54
|
anim_cancel_duration: .54
|
||||||
scroll_distance: '10dp'
|
|
||||||
on_index: root.on_index(*args)
|
on_index: root.on_index(*args)
|
||||||
id: carousel
|
id: carousel
|
||||||
|
|
||||||
|
@ -281,65 +308,59 @@
|
||||||
TabbedCarousel:
|
TabbedCarousel:
|
||||||
id: panel
|
id: panel
|
||||||
tab_height: '48dp'
|
tab_height: '48dp'
|
||||||
#default_tab: send_tab
|
tab_width: panel.width/3
|
||||||
|
default_tab: history_tab
|
||||||
strip_border: 0, 0, 0, 0
|
strip_border: 0, 0, 0, 0
|
||||||
HistoryScreen:
|
InvoicesScreen:
|
||||||
id: history_screen
|
id: invoices_screen
|
||||||
tab: history_tab
|
tab: invoices_tab
|
||||||
SendScreen:
|
SendScreen:
|
||||||
id: send_screen
|
id: send_screen
|
||||||
tab: send_tab
|
tab: send_tab
|
||||||
|
HistoryScreen:
|
||||||
|
id: history_screen
|
||||||
|
tab: history_tab
|
||||||
ReceiveScreen:
|
ReceiveScreen:
|
||||||
id: receive_screen
|
id: receive_screen
|
||||||
tab: receive_tab
|
tab: receive_tab
|
||||||
ContactsScreen:
|
RequestsScreen:
|
||||||
id: contacts_screen
|
id: requests_screen
|
||||||
tab: contacts_tab
|
tab: requests_tab
|
||||||
|
#ContactsScreen:
|
||||||
|
# id: contacts_screen
|
||||||
|
# tab: contacts_tab
|
||||||
CleanHeader:
|
CleanHeader:
|
||||||
id: history_tab
|
id: invoices_tab
|
||||||
text: _('History')
|
text: _('Invoices')
|
||||||
slide: 0
|
slide: 0
|
||||||
CleanHeader:
|
CleanHeader:
|
||||||
id: send_tab
|
id: send_tab
|
||||||
text: _('Send')
|
text: _('Send')
|
||||||
slide: 1
|
slide: 1
|
||||||
CleanHeader:
|
CleanHeader:
|
||||||
id: receive_tab
|
id: history_tab
|
||||||
text: _('Receive')
|
text: _('History')
|
||||||
slide: 2
|
slide: 2
|
||||||
CleanHeader:
|
CleanHeader:
|
||||||
id: contacts_tab
|
id: receive_tab
|
||||||
text: _('Contacts')
|
text: _('Receive')
|
||||||
slide: 3
|
slide: 3
|
||||||
|
CleanHeader:
|
||||||
|
id: requests_tab
|
||||||
|
text: _('Requests')
|
||||||
|
slide: 4
|
||||||
|
#CleanHeader:
|
||||||
|
# id: contacts_tab
|
||||||
|
# text: _('Contacts')
|
||||||
|
# slide: 4
|
||||||
|
|
||||||
<ActionOvrButton@ActionButton>
|
<ActionOvrButton@ActionButton>
|
||||||
on_release:
|
on_release:
|
||||||
if self.parent: self.parent.parent.dismiss()
|
Clock.schedule_once(lambda dt: self.parent.parent.dismiss() if self.parent else None, 0.05)
|
||||||
app.popup_dialog(self.name)
|
Clock.schedule_once(lambda dt: app.popup_dialog(self.name), 0.05)
|
||||||
|
|
||||||
|
|
||||||
<SettingsItem@ButtonBehavior+BoxLayout>
|
|
||||||
orientation: 'vertical'
|
|
||||||
title: ''
|
|
||||||
description: ''
|
|
||||||
size_hint: 1, 1
|
|
||||||
Label:
|
|
||||||
id: title
|
|
||||||
text: self.parent.title
|
|
||||||
size_hint: 1, 1
|
|
||||||
bold: True
|
|
||||||
text_size: self.size
|
|
||||||
halign: 'left'
|
|
||||||
Label:
|
|
||||||
text: self.parent.description
|
|
||||||
size_hint: 1, 1
|
|
||||||
text_size: self.width, None
|
|
||||||
color: 0.8, 0.8, 0.8, 1
|
|
||||||
halign: 'left'
|
|
||||||
|
|
||||||
|
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
|
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
|
|
||||||
canvas.before:
|
canvas.before:
|
||||||
|
@ -373,34 +394,26 @@ BoxLayout:
|
||||||
.format(app.status)
|
.format(app.status)
|
||||||
font_size: '22dp'
|
font_size: '22dp'
|
||||||
minimum_width: '1dp'
|
minimum_width: '1dp'
|
||||||
|
on_release: app.popup_dialog('status')
|
||||||
ActionButton:
|
|
||||||
id: context_button
|
|
||||||
text: app.context
|
|
||||||
width: 0
|
|
||||||
on_text:
|
|
||||||
self.width = 20 if self.text else 0
|
|
||||||
on_release: app.context_action()
|
|
||||||
|
|
||||||
ActionOverflow:
|
ActionOverflow:
|
||||||
id: ao
|
id: ao
|
||||||
ActionOvrButton:
|
ActionOvrButton:
|
||||||
text: _('Network')
|
name: 'about'
|
||||||
|
text: _('About')
|
||||||
|
ActionOvrButton:
|
||||||
name: 'network'
|
name: 'network'
|
||||||
|
text: _('Network')
|
||||||
on_parent:
|
on_parent:
|
||||||
# when widget overflow drop down is shown, adjust the width
|
# when widget overflow drop down is shown, adjust the width
|
||||||
parent = args[1]
|
parent = args[1]
|
||||||
if parent: ao._dropdown.width = sp(200)
|
if parent: ao._dropdown.width = sp(200)
|
||||||
ActionOvrButton:
|
|
||||||
name: 'settings'
|
|
||||||
text: _('Settings')
|
|
||||||
ActionOvrButton:
|
ActionOvrButton:
|
||||||
name: 'wallets'
|
name: 'wallets'
|
||||||
text: _('Wallets')
|
text: _('Wallets')
|
||||||
ActionOvrButton:
|
ActionOvrButton:
|
||||||
name: 'plugins'
|
name: 'settings'
|
||||||
text: _('Plugins')
|
text: _('Settings')
|
||||||
|
|
||||||
ScreenManager:
|
ScreenManager:
|
||||||
id: manager
|
id: manager
|
||||||
ScreenTabs:
|
ScreenTabs:
|
||||||
|
|
|
@ -7,11 +7,13 @@ from decimal import Decimal
|
||||||
|
|
||||||
import electrum
|
import electrum
|
||||||
from electrum import WalletStorage, Wallet
|
from electrum import WalletStorage, Wallet
|
||||||
from electrum.i18n import _, set_language
|
from electrum.i18n import _
|
||||||
from electrum.contacts import Contacts
|
from electrum.contacts import Contacts
|
||||||
|
from electrum.paymentrequest import InvoiceStore
|
||||||
from electrum.util import profiler, InvalidPassword
|
from electrum.util import profiler, InvalidPassword
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
from electrum.util import format_satoshis, format_satoshis_plain
|
from electrum.util import format_satoshis, format_satoshis_plain
|
||||||
|
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
|
@ -31,6 +33,7 @@ Factory.register('InstallWizard',
|
||||||
Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs')
|
Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs')
|
||||||
Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens')
|
Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens')
|
||||||
|
|
||||||
|
|
||||||
#from kivy.core.window import Window
|
#from kivy.core.window import Window
|
||||||
#Window.softinput_mode = 'below_target'
|
#Window.softinput_mode = 'below_target'
|
||||||
|
|
||||||
|
@ -54,8 +57,8 @@ from kivy.core.clipboard import Clipboard
|
||||||
Factory.register('TabbedCarousel', module='electrum_gui.kivy.uix.screens')
|
Factory.register('TabbedCarousel', module='electrum_gui.kivy.uix.screens')
|
||||||
|
|
||||||
|
|
||||||
|
from electrum.util import base_units
|
||||||
|
|
||||||
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
|
|
||||||
|
|
||||||
class ElectrumWindow(App):
|
class ElectrumWindow(App):
|
||||||
|
|
||||||
|
@ -72,13 +75,6 @@ class ElectrumWindow(App):
|
||||||
self.history_screen.update()
|
self.history_screen.update()
|
||||||
|
|
||||||
base_unit = AliasProperty(_get_bu, _set_bu)
|
base_unit = AliasProperty(_get_bu, _set_bu)
|
||||||
|
|
||||||
def _rotate_bu(self):
|
|
||||||
keys = sorted(base_units.keys())
|
|
||||||
self.base_unit = keys[ (keys.index(self.base_unit) + 1) % len(keys)]
|
|
||||||
|
|
||||||
context = StringProperty('')
|
|
||||||
context_action = lambda x: None
|
|
||||||
status = StringProperty('')
|
status = StringProperty('')
|
||||||
fiat_unit = StringProperty('')
|
fiat_unit = StringProperty('')
|
||||||
|
|
||||||
|
@ -165,6 +161,9 @@ class ElectrumWindow(App):
|
||||||
self.nfcscanner = None
|
self.nfcscanner = None
|
||||||
self.tabs = None
|
self.tabs = None
|
||||||
|
|
||||||
|
self.receive_address = None
|
||||||
|
self.current_invoice = None
|
||||||
|
|
||||||
super(ElectrumWindow, self).__init__(**kwargs)
|
super(ElectrumWindow, self).__init__(**kwargs)
|
||||||
|
|
||||||
title = _('Electrum App')
|
title = _('Electrum App')
|
||||||
|
@ -176,6 +175,7 @@ class ElectrumWindow(App):
|
||||||
|
|
||||||
#self.config = self.gui_object.config
|
#self.config = self.gui_object.config
|
||||||
self.contacts = Contacts(self.electrum_config)
|
self.contacts = Contacts(self.electrum_config)
|
||||||
|
self.invoices = InvoiceStore(self.electrum_config)
|
||||||
|
|
||||||
self.bind(url=self.set_URI)
|
self.bind(url=self.set_URI)
|
||||||
# were we sent a url?
|
# were we sent a url?
|
||||||
|
@ -191,15 +191,60 @@ class ElectrumWindow(App):
|
||||||
self._trigger_notify_transactions = \
|
self._trigger_notify_transactions = \
|
||||||
Clock.create_trigger(self.notify_transactions, 5)
|
Clock.create_trigger(self.notify_transactions, 5)
|
||||||
|
|
||||||
|
def get_receive_address(self):
|
||||||
|
return self.receive_address if self.receive_address else self.wallet.get_unused_address(None)
|
||||||
|
|
||||||
|
def do_pay(self, obj):
|
||||||
|
pr = self.invoices.get(obj.key)
|
||||||
|
self.on_pr(pr)
|
||||||
|
|
||||||
|
def on_pr(self, pr):
|
||||||
|
if pr.verify(self.contacts):
|
||||||
|
key = self.invoices.add(pr)
|
||||||
|
self.invoices_screen.update()
|
||||||
|
status = self.invoices.get_status(key)
|
||||||
|
if status == PR_PAID:
|
||||||
|
self.show_error("invoice already paid")
|
||||||
|
self.send_screen.do_clear()
|
||||||
|
else:
|
||||||
|
if pr.has_expired():
|
||||||
|
self.show_error(_('Payment request has expired'))
|
||||||
|
else:
|
||||||
|
self.current_invoice = pr
|
||||||
|
self.update_tab('send')
|
||||||
|
self.switch_to('send')
|
||||||
|
else:
|
||||||
|
self.show_error("invoice error:" + pr.error)
|
||||||
|
self.send_screen.do_clear()
|
||||||
|
|
||||||
def set_URI(self, url):
|
def set_URI(self, url):
|
||||||
try:
|
try:
|
||||||
url = electrum.util.parse_URI(url)
|
url = electrum.util.parse_URI(url, self.on_pr)
|
||||||
except:
|
except:
|
||||||
self.show_info("Invalid URI", url)
|
self.show_info("Invalid URI", url)
|
||||||
return
|
return
|
||||||
self.send_screen.set_URI(url)
|
self.send_screen.set_URI(url)
|
||||||
|
|
||||||
|
|
||||||
|
def update_tab(self, name):
|
||||||
|
s = getattr(self, name + '_screen', None)
|
||||||
|
if s:
|
||||||
|
s.update()
|
||||||
|
|
||||||
|
@profiler
|
||||||
|
def update_tabs(self):
|
||||||
|
for tab in ['invoices', 'send', 'history', 'receive', 'requests']:
|
||||||
|
self.update_tab(tab)
|
||||||
|
|
||||||
|
def switch_to(self, name):
|
||||||
|
tab = self.tabs.ids[name + '_tab']
|
||||||
|
self.tabs.ids.panel.switch_to(tab)
|
||||||
|
|
||||||
|
def show_request(self, addr):
|
||||||
|
self.receive_address = addr
|
||||||
|
self.update_tab('receive')
|
||||||
|
self.switch_to('receive')
|
||||||
|
|
||||||
def scan_qr(self, on_complete):
|
def scan_qr(self, on_complete):
|
||||||
from jnius import autoclass
|
from jnius import autoclass
|
||||||
from android import activity
|
from android import activity
|
||||||
|
@ -212,31 +257,10 @@ class ElectrumWindow(App):
|
||||||
if resultCode == -1: # RESULT_OK:
|
if resultCode == -1: # RESULT_OK:
|
||||||
contents = intent.getStringExtra("SCAN_RESULT")
|
contents = intent.getStringExtra("SCAN_RESULT")
|
||||||
if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE':
|
if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE':
|
||||||
try:
|
on_complete(contents)
|
||||||
uri = electrum.util.parse_URI(contents)
|
|
||||||
except:
|
|
||||||
self.show_info("Invalid URI", url)
|
|
||||||
return
|
|
||||||
on_complete(uri)
|
|
||||||
activity.bind(on_activity_result=on_qr_result)
|
activity.bind(on_activity_result=on_qr_result)
|
||||||
PythonActivity.mActivity.startActivityForResult(intent, 0)
|
PythonActivity.mActivity.startActivityForResult(intent, 0)
|
||||||
|
|
||||||
def show_plugins(self, plugins_list):
|
|
||||||
def on_active(sw, value):
|
|
||||||
self.plugins.toggle_enabled(self.electrum_config, sw.name)
|
|
||||||
run_hook('init_kivy', self)
|
|
||||||
for item in self.plugins.descriptions:
|
|
||||||
if 'kivy' not in item.get('available_for', []):
|
|
||||||
continue
|
|
||||||
name = item.get('__name__')
|
|
||||||
label = Label(text=item.get('fullname'), height='48db', size_hint=(1, None))
|
|
||||||
plugins_list.add_widget(label)
|
|
||||||
sw = Switch()
|
|
||||||
sw.name = name
|
|
||||||
p = self.plugins.get(name)
|
|
||||||
sw.active = (p is not None) and p.is_enabled()
|
|
||||||
sw.bind(active=on_active)
|
|
||||||
plugins_list.add_widget(sw)
|
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
return Builder.load_file('gui/kivy/main.kv')
|
return Builder.load_file('gui/kivy/main.kv')
|
||||||
|
@ -273,30 +297,46 @@ class ElectrumWindow(App):
|
||||||
win.bind(keyboard_height=self.on_keyboard_height)
|
win.bind(keyboard_height=self.on_keyboard_height)
|
||||||
|
|
||||||
self.on_size(win, win.size)
|
self.on_size(win, win.size)
|
||||||
|
self.init_ui()
|
||||||
|
self.load_wallet_by_name(self.electrum_config.get_wallet_path())
|
||||||
|
|
||||||
|
def load_wallet_by_name(self, wallet_path):
|
||||||
|
if not wallet_path:
|
||||||
|
return
|
||||||
|
self.stop_wallet()
|
||||||
|
|
||||||
config = self.electrum_config
|
config = self.electrum_config
|
||||||
storage = WalletStorage(config.get_wallet_path())
|
storage = WalletStorage(wallet_path)
|
||||||
|
|
||||||
Logger.info('Electrum: Check for existing wallet')
|
Logger.info('Electrum: Check for existing wallet')
|
||||||
|
|
||||||
if storage.file_exists:
|
if storage.file_exists:
|
||||||
wallet = Wallet(storage)
|
wallet = Wallet(storage)
|
||||||
action = wallet.get_action()
|
action = wallet.get_action()
|
||||||
else:
|
else:
|
||||||
action = 'new'
|
action = 'new'
|
||||||
|
|
||||||
if action is not None:
|
if action is not None:
|
||||||
# start installation wizard
|
# start installation wizard
|
||||||
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
||||||
wizard = Factory.InstallWizard(config, self.network, storage)
|
wizard = Factory.InstallWizard(config, self.network, storage)
|
||||||
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
wizard.bind(on_wizard_complete=lambda instance, wallet: self.load_wallet(wallet))
|
||||||
wizard.run(action)
|
wizard.run(action)
|
||||||
else:
|
else:
|
||||||
wallet.start_threads(self.network)
|
wallet.start_threads(self.network)
|
||||||
self.on_wizard_complete(None, wallet)
|
self.load_wallet(wallet)
|
||||||
|
|
||||||
self.on_resume()
|
self.on_resume()
|
||||||
|
|
||||||
|
def create_wallet_dialog(self, l):
|
||||||
|
from uix.dialogs.label_dialog import LabelDialog
|
||||||
|
def f(text):
|
||||||
|
if text:
|
||||||
|
l.text = text
|
||||||
|
d = LabelDialog(_('Enter wallet name'), '', f)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
|
self.stop_wallet()
|
||||||
|
|
||||||
|
def stop_wallet(self):
|
||||||
if self.wallet:
|
if self.wallet:
|
||||||
self.wallet.stop_threads()
|
self.wallet.stop_threads()
|
||||||
|
|
||||||
|
@ -316,7 +356,6 @@ class ElectrumWindow(App):
|
||||||
active_widg = self.root.children[0]
|
active_widg = self.root.children[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fw = self._focused_widget
|
fw = self._focused_widget
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -350,29 +389,25 @@ class ElectrumWindow(App):
|
||||||
self.gui.main_gui.toggle_settings(self)
|
self.gui.main_gui.toggle_settings(self)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def on_wizard_complete(self, instance, wallet):
|
|
||||||
if not wallet:
|
|
||||||
Logger.debug('Electrum: No Wallet set/found. Exiting...')
|
|
||||||
app = App.get_running_app()
|
|
||||||
app.show_error('Electrum: No Wallet set/found. Exiting...',
|
|
||||||
exit=True)
|
|
||||||
|
|
||||||
self.init_ui()
|
|
||||||
self.load_wallet(wallet)
|
|
||||||
|
|
||||||
def popup_dialog(self, name):
|
def popup_dialog(self, name):
|
||||||
|
if name == 'settings':
|
||||||
|
from uix.dialogs.settings import SettingsDialog
|
||||||
|
d = SettingsDialog(self)
|
||||||
|
d.open()
|
||||||
|
elif name == 'wallets':
|
||||||
|
from uix.dialogs.wallets import WalletDialog
|
||||||
|
d = WalletDialog()
|
||||||
|
d.open()
|
||||||
|
else:
|
||||||
popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv')
|
popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv')
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
''' Initialize The Ux part of electrum. This function performs the basic
|
''' Initialize The Ux part of electrum. This function performs the basic
|
||||||
tasks of setting up the ui.
|
tasks of setting up the ui.
|
||||||
'''
|
'''
|
||||||
from weakref import ref
|
from weakref import ref
|
||||||
set_language(self.electrum_config.get('language'))
|
|
||||||
|
|
||||||
self.funds_error = False
|
self.funds_error = False
|
||||||
# setup UX
|
# setup UX
|
||||||
|
@ -401,7 +436,8 @@ class ElectrumWindow(App):
|
||||||
interests = ['updated', 'status', 'new_transaction']
|
interests = ['updated', 'status', 'new_transaction']
|
||||||
self.network.register_callback(self.on_network, interests)
|
self.network.register_callback(self.on_network, interests)
|
||||||
|
|
||||||
self.wallet = None
|
#self.wallet = None
|
||||||
|
self.tabs = self.root.ids['tabs']
|
||||||
|
|
||||||
def on_network(self, event, *args):
|
def on_network(self, event, *args):
|
||||||
if event == 'updated':
|
if event == 'updated':
|
||||||
|
@ -418,7 +454,7 @@ class ElectrumWindow(App):
|
||||||
self.update_wallet()
|
self.update_wallet()
|
||||||
# Once GUI has been initialized check if we want to announce something
|
# Once GUI has been initialized check if we want to announce something
|
||||||
# since the callback has been called before the GUI was initialized
|
# since the callback has been called before the GUI was initialized
|
||||||
self.update_history_tab()
|
self.update_tabs()
|
||||||
self.notify_transactions()
|
self.notify_transactions()
|
||||||
run_hook('load_wallet', wallet, self)
|
run_hook('load_wallet', wallet, self)
|
||||||
|
|
||||||
|
@ -447,58 +483,17 @@ class ElectrumWindow(App):
|
||||||
amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs, None)
|
amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs, None)
|
||||||
return format_satoshis_plain(amount, self.decimal_point())
|
return format_satoshis_plain(amount, self.decimal_point())
|
||||||
|
|
||||||
def update_password(self, label, c):
|
|
||||||
text = label.password
|
|
||||||
if c == '<':
|
|
||||||
text = text[:-1]
|
|
||||||
elif c == 'Clear':
|
|
||||||
text = ''
|
|
||||||
else:
|
|
||||||
text += c
|
|
||||||
label.password = text
|
|
||||||
|
|
||||||
def toggle_fiat(self, a):
|
|
||||||
a.is_fiat = not a.is_fiat
|
|
||||||
|
|
||||||
def update_amount(self, label, c):
|
|
||||||
amount = label.fiat_amount if label.is_fiat else label.amount
|
|
||||||
if c == '<':
|
|
||||||
amount = amount[:-1]
|
|
||||||
elif c == '.' and amount == '':
|
|
||||||
amount = '0.'
|
|
||||||
elif c == '0' and amount == '0':
|
|
||||||
amount = '0'
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
Decimal(amount+c)
|
|
||||||
amount += c
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if label.is_fiat:
|
|
||||||
label.fiat_amount = amount
|
|
||||||
else:
|
|
||||||
label.amount = amount
|
|
||||||
|
|
||||||
def format_amount(self, x, is_diff=False, whitespaces=False):
|
def format_amount(self, x, is_diff=False, whitespaces=False):
|
||||||
return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces)
|
return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces)
|
||||||
|
|
||||||
|
def format_amount_and_units(self, x):
|
||||||
|
return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
def update_wallet(self, *dt):
|
def update_wallet(self, *dt):
|
||||||
self._trigger_update_status()
|
self._trigger_update_status()
|
||||||
if self.wallet.up_to_date or not self.network or not self.network.is_connected():
|
#if self.wallet.up_to_date or not self.network or not self.network.is_connected():
|
||||||
self.update_history_tab()
|
self.update_tabs()
|
||||||
self.update_contacts_tab()
|
|
||||||
|
|
||||||
|
|
||||||
@profiler
|
|
||||||
def update_history_tab(self, see_all=False):
|
|
||||||
if self.history_screen:
|
|
||||||
self.history_screen.update(see_all)
|
|
||||||
|
|
||||||
def update_contacts_tab(self):
|
|
||||||
if self.contacts_screen:
|
|
||||||
self.contacts_screen.update()
|
|
||||||
|
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
|
@ -593,64 +588,10 @@ class ElectrumWindow(App):
|
||||||
else:
|
else:
|
||||||
self.show_error(_('Invalid Address'))
|
self.show_error(_('Invalid Address'))
|
||||||
|
|
||||||
def send_payment(self, address, amount=0, label='', message=''):
|
|
||||||
tabs = self.tabs
|
|
||||||
screen_send = tabs.ids.screen_send
|
|
||||||
|
|
||||||
if label and self.wallet.labels.get(address) != label:
|
|
||||||
#if self.question('Give label "%s" to address %s ?'%(label,address)):
|
|
||||||
if address not in self.wallet.addressbook and not self.wallet. is_mine(address):
|
|
||||||
self.wallet.addressbook.append(address)
|
|
||||||
self.wallet.set_label(address, label)
|
|
||||||
|
|
||||||
# switch_to the send screen
|
|
||||||
tabs.ids.panel.switch_to(tabs.ids.tab_send)
|
|
||||||
|
|
||||||
label = self.wallet.labels.get(address)
|
|
||||||
m_addr = label + ' <'+ address +'>' if label else address
|
|
||||||
|
|
||||||
# populate
|
|
||||||
def set_address(*l):
|
|
||||||
content = screen_send.ids
|
|
||||||
content.payto_e.text = m_addr
|
|
||||||
content.message_e.text = message
|
|
||||||
if amount:
|
|
||||||
content.amount_e.text = amount
|
|
||||||
|
|
||||||
# wait for screen to load
|
|
||||||
Clock.schedule_once(set_address, .5)
|
|
||||||
|
|
||||||
def set_send(self, address, amount, label, message):
|
def set_send(self, address, amount, label, message):
|
||||||
self.send_payment(address, amount=amount, label=label, message=message)
|
self.send_payment(address, amount=amount, label=label, message=message)
|
||||||
|
|
||||||
def prepare_for_payment_request(self):
|
|
||||||
tabs = self.tabs
|
|
||||||
screen_send = tabs.ids.screen_send
|
|
||||||
|
|
||||||
# switch_to the send screen
|
|
||||||
tabs.ids.panel.switch_to(tabs.ids.tab_send)
|
|
||||||
|
|
||||||
content = screen_send.ids
|
|
||||||
if content:
|
|
||||||
self.set_frozen(content, False)
|
|
||||||
screen_send.screen_label.text = _("please wait...")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def payment_request_ok(self):
|
|
||||||
tabs = self.tabs
|
|
||||||
screen_send = tabs.ids.screen_send
|
|
||||||
|
|
||||||
# switch_to the send screen
|
|
||||||
tabs.ids.panel.switch_to(tabs.ids.tab_send)
|
|
||||||
|
|
||||||
self.set_frozen(content, True)
|
|
||||||
|
|
||||||
screen_send.ids.payto_e.text = self.gui_object.payment_request.domain
|
|
||||||
screen_send.ids.amount_e.text = self.format_amount(self.gui_object.payment_request.get_amount())
|
|
||||||
screen_send.ids.message_e.text = self.gui_object.payment_request.memo
|
|
||||||
|
|
||||||
# wait for screen to load
|
|
||||||
Clock.schedule_once(set_address, .5)
|
|
||||||
|
|
||||||
def set_frozen(self, entry, frozen):
|
def set_frozen(self, entry, frozen):
|
||||||
if frozen:
|
if frozen:
|
||||||
|
@ -660,15 +601,6 @@ class ElectrumWindow(App):
|
||||||
entry.disabled = False
|
entry.disabled = False
|
||||||
Factory.Animation(opacity=1).start(content)
|
Factory.Animation(opacity=1).start(content)
|
||||||
|
|
||||||
def payment_request_error(self):
|
|
||||||
tabs = self.tabs
|
|
||||||
screen_send = tabs.ids.screen_send
|
|
||||||
|
|
||||||
# switch_to the send screen
|
|
||||||
tabs.ids.panel.switch_to(tabs.ids.tab_send)
|
|
||||||
|
|
||||||
self.do_clear()
|
|
||||||
self.show_info(self.gui_object.payment_request.error)
|
|
||||||
|
|
||||||
def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
|
def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
|
||||||
exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0,
|
exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0,
|
||||||
|
@ -737,43 +669,32 @@ class ElectrumWindow(App):
|
||||||
pos = (win.center[0], win.center[1] - (info_bubble.height/2))
|
pos = (win.center[0], win.center[1] - (info_bubble.height/2))
|
||||||
info_bubble.show(pos, duration, width, modal=modal, exit=exit)
|
info_bubble.show(pos, duration, width, modal=modal, exit=exit)
|
||||||
|
|
||||||
def tx_dialog(self, tx_hash):
|
def tx_details_dialog(self, obj):
|
||||||
popup = Builder.load_file('gui/kivy/uix/ui_screens/transaction.kv')
|
popup = Builder.load_file('gui/kivy/uix/ui_screens/transaction.kv')
|
||||||
popup.tx_hash = tx_hash
|
popup.tx_hash = obj.tx_hash
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def tx_selected(self, txid, state):
|
def address_dialog(self, screen):
|
||||||
if state == 'down':
|
pass
|
||||||
self.context = 'tx'
|
|
||||||
self.context_action = lambda: self.tx_dialog(txid)
|
|
||||||
else:
|
|
||||||
self.reset_context()
|
|
||||||
|
|
||||||
def reset_context(self):
|
def description_dialog(self, screen):
|
||||||
self.context = ''
|
from uix.dialogs.label_dialog import LabelDialog
|
||||||
self.context_action = lambda: None
|
text = screen.message
|
||||||
|
def callback(text):
|
||||||
|
screen.message = text
|
||||||
|
d = LabelDialog(_('Enter description'), text, callback)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
@profiler
|
||||||
def amount_dialog(self, screen, show_max):
|
def amount_dialog(self, screen, show_max):
|
||||||
popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv')
|
from uix.dialogs.amount_dialog import AmountDialog
|
||||||
but_max = popup.ids.but_max
|
|
||||||
if not show_max:
|
|
||||||
but_max.disabled = True
|
|
||||||
but_max.opacity = 0
|
|
||||||
else:
|
|
||||||
but_max.disabled = False
|
|
||||||
but_max.opacity = 1
|
|
||||||
|
|
||||||
amount = screen.amount
|
amount = screen.amount
|
||||||
if amount:
|
if amount:
|
||||||
a, u = str(amount).split()
|
amount, u = str(amount).split()
|
||||||
assert u == self.base_unit
|
assert u == self.base_unit
|
||||||
popup.ids.kb.amount = a
|
def cb(amount):
|
||||||
|
screen.amount = amount
|
||||||
def cb():
|
popup = AmountDialog(show_max, amount, cb)
|
||||||
o = popup.ids.a.btc_text
|
|
||||||
screen.amount = o
|
|
||||||
|
|
||||||
popup.on_dismiss = cb
|
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def protected(self, f, args):
|
def protected(self, f, args):
|
||||||
|
@ -804,12 +725,9 @@ class ElectrumWindow(App):
|
||||||
self.show_error("PIN numbers do not match")
|
self.show_error("PIN numbers do not match")
|
||||||
|
|
||||||
def password_dialog(self, title, f, args):
|
def password_dialog(self, title, f, args):
|
||||||
popup = Builder.load_file('gui/kivy/uix/ui_screens/password.kv')
|
from uix.dialogs.password_dialog import PasswordDialog
|
||||||
popup.title = title
|
def callback(pw):
|
||||||
def callback():
|
|
||||||
pw = popup.ids.kb.password
|
|
||||||
Clock.schedule_once(lambda x: apply(f, args + (pw,)), 0.1)
|
Clock.schedule_once(lambda x: apply(f, args + (pw,)), 0.1)
|
||||||
popup.on_dismiss = callback
|
popup = PasswordDialog(title, callback)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
|
|
||||||
|
|
52
gui/kivy/uix/context_menu.py
Normal file
52
gui/kivy/uix/context_menu.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
#!python
|
||||||
|
#!/usr/bin/env python
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.uix.bubble import Bubble
|
||||||
|
from kivy.animation import Animation
|
||||||
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<MenuItem@Button>
|
||||||
|
background_color: .2, .9, 1, 1
|
||||||
|
height: '48dp'
|
||||||
|
size_hint: 1, None
|
||||||
|
|
||||||
|
<ContextMenu>
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '48dp'
|
||||||
|
pos: (0, 0)
|
||||||
|
show_arrow: False
|
||||||
|
arrow_pos: 'top_mid'
|
||||||
|
padding: 0
|
||||||
|
orientation: 'horizontal'
|
||||||
|
BoxLayout:
|
||||||
|
size_hint: 1, 1
|
||||||
|
height: '48dp'
|
||||||
|
orientation: 'horizontal'
|
||||||
|
id: buttons
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
class MenuItem(Factory.Button):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ContextMenu(Bubble):
|
||||||
|
|
||||||
|
def __init__(self, obj, action_list):
|
||||||
|
Bubble.__init__(self)
|
||||||
|
self.obj = obj
|
||||||
|
for k, v in action_list:
|
||||||
|
l = MenuItem()
|
||||||
|
l.text = k
|
||||||
|
def func(f=v):
|
||||||
|
Clock.schedule_once(lambda dt: self.hide(), 0.1)
|
||||||
|
Clock.schedule_once(lambda dt: f(obj), 0.15)
|
||||||
|
l.on_release = func
|
||||||
|
self.ids.buttons.add_widget(l)
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
if self.parent:
|
||||||
|
self.parent.hide_menu()
|
|
@ -144,6 +144,7 @@ class InfoBubble(Factory.Bubble):
|
||||||
m.add_widget(self)
|
m.add_widget(self)
|
||||||
else:
|
else:
|
||||||
Window.add_widget(self)
|
Window.add_widget(self)
|
||||||
|
|
||||||
# wait for the bubble to adjust it's size according to text then animate
|
# wait for the bubble to adjust it's size according to text then animate
|
||||||
Clock.schedule_once(lambda dt: self._show(pos, duration))
|
Clock.schedule_once(lambda dt: self._show(pos, duration))
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
#:import Decimal decimal.Decimal
|
from kivy.app import App
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
|
||||||
Popup:
|
<AmountDialog@Popup>
|
||||||
id: popup
|
id: popup
|
||||||
title: _('Amount')
|
title: _('Amount')
|
||||||
|
|
||||||
AnchorLayout:
|
AnchorLayout:
|
||||||
anchor_x: 'center'
|
anchor_x: 'center'
|
||||||
|
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
size_hint: 0.8, 1
|
size_hint: 0.8, 1
|
||||||
|
@ -18,7 +21,7 @@ Popup:
|
||||||
id: a
|
id: a
|
||||||
btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else ''
|
btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else ''
|
||||||
fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else ''
|
fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else ''
|
||||||
text: (self.fiat_text + ' / ' + self.btc_text if kb.is_fiat else self.btc_text + ' / ' + self.fiat_text) if self.btc_text else ''
|
text: ((self.fiat_text + ' / ' + self.btc_text if kb.is_fiat else self.btc_text + ' / ' + self.fiat_text) if app.fiat_unit else self.btc_text) if self.btc_text else ''
|
||||||
size_hint: 1, 1
|
size_hint: 1, 1
|
||||||
font_size: '22dp'
|
font_size: '22dp'
|
||||||
Widget:
|
Widget:
|
||||||
|
@ -30,8 +33,8 @@ Popup:
|
||||||
is_fiat: False
|
is_fiat: False
|
||||||
on_fiat_amount: if self.is_fiat: self.amount = app.fiat_to_btc(self.fiat_amount)
|
on_fiat_amount: if self.is_fiat: self.amount = app.fiat_to_btc(self.fiat_amount)
|
||||||
on_amount: if not self.is_fiat: self.fiat_amount = app.btc_to_fiat(self.amount)
|
on_amount: if not self.is_fiat: self.fiat_amount = app.btc_to_fiat(self.amount)
|
||||||
update_text: app.update_amount
|
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
|
update_amount: popup.update_amount
|
||||||
height: '300dp'
|
height: '300dp'
|
||||||
cols: 3
|
cols: 3
|
||||||
KButton:
|
KButton:
|
||||||
|
@ -60,6 +63,8 @@ Popup:
|
||||||
text: '<'
|
text: '<'
|
||||||
Button:
|
Button:
|
||||||
id: but_max
|
id: but_max
|
||||||
|
opacity: 1 if root.show_max else 0
|
||||||
|
disabled: not root.show_max
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
text: 'Max'
|
text: 'Max'
|
||||||
|
@ -70,9 +75,9 @@ Popup:
|
||||||
id: button_fiat
|
id: button_fiat
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
text: app.fiat_unit if kb.is_fiat else app.base_unit
|
text: (app.fiat_unit if kb.is_fiat else app.base_unit) if app.fiat_unit else ''
|
||||||
on_release:
|
on_release:
|
||||||
app.toggle_fiat(kb)
|
popup.toggle_fiat(kb)
|
||||||
Button:
|
Button:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
@ -80,18 +85,53 @@ Popup:
|
||||||
on_release:
|
on_release:
|
||||||
kb.amount = ''
|
kb.amount = ''
|
||||||
kb.fiat_amount = ''
|
kb.fiat_amount = ''
|
||||||
|
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
|
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 2, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
Button:
|
Button:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
text: _('OK')
|
text: _('OK')
|
||||||
on_release: popup.dismiss()
|
on_release:
|
||||||
|
root.callback(a.btc_text)
|
||||||
|
popup.dismiss()
|
||||||
|
''')
|
||||||
|
|
||||||
|
from kivy.properties import BooleanProperty
|
||||||
|
|
||||||
|
class AmountDialog(Factory.Popup):
|
||||||
|
show_max = BooleanProperty(False)
|
||||||
|
def __init__(self, show_max, amount, cb):
|
||||||
|
Factory.Popup.__init__(self)
|
||||||
|
self.show_max = show_max
|
||||||
|
self.callback = cb
|
||||||
|
if amount:
|
||||||
|
self.ids.kb.amount = amount
|
||||||
|
|
||||||
|
def toggle_fiat(self, a):
|
||||||
|
a.is_fiat = not a.is_fiat
|
||||||
|
|
||||||
|
def update_amount(self, c):
|
||||||
|
kb = self.ids.kb
|
||||||
|
amount = kb.fiat_amount if kb.is_fiat else kb.amount
|
||||||
|
if c == '<':
|
||||||
|
amount = amount[:-1]
|
||||||
|
elif c == '.' and amount in ['0', '']:
|
||||||
|
amount = '0.'
|
||||||
|
elif amount == '0':
|
||||||
|
amount = c
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
Decimal(amount+c)
|
||||||
|
amount += c
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if kb.is_fiat:
|
||||||
|
kb.fiat_amount = amount
|
||||||
|
else:
|
||||||
|
kb.amount = amount
|
65
gui/kivy/uix/dialogs/choice_dialog.py
Normal file
65
gui/kivy/uix/dialogs/choice_dialog.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.uix.checkbox import CheckBox
|
||||||
|
from kivy.uix.label import Label
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<ChoiceDialog@Popup>
|
||||||
|
id: popup
|
||||||
|
title: ''
|
||||||
|
size_hint: 0.8, 0.8
|
||||||
|
pos_hint: {'top':0.9}
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 0.1
|
||||||
|
ScrollView:
|
||||||
|
orientation: 'vertical'
|
||||||
|
size_hint: 1, 0.8
|
||||||
|
GridLayout:
|
||||||
|
row_default_height: '48dp'
|
||||||
|
orientation: 'vertical'
|
||||||
|
id: choices
|
||||||
|
cols: 2
|
||||||
|
size_hint: 1, 1
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'horizontal'
|
||||||
|
size_hint: 1, 0.2
|
||||||
|
Button:
|
||||||
|
text: 'Cancel'
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
on_release: popup.dismiss()
|
||||||
|
Button:
|
||||||
|
text: 'OK'
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
on_release:
|
||||||
|
root.callback(popup.value)
|
||||||
|
popup.dismiss()
|
||||||
|
''')
|
||||||
|
|
||||||
|
class ChoiceDialog(Factory.Popup):
|
||||||
|
|
||||||
|
def __init__(self, title, choices, key, callback):
|
||||||
|
Factory.Popup.__init__(self)
|
||||||
|
for k, v in choices.items():
|
||||||
|
l = Label(text=v)
|
||||||
|
l.height = '48dp'
|
||||||
|
cb = CheckBox(group='choices')
|
||||||
|
cb.value = k
|
||||||
|
cb.height = '48dp'
|
||||||
|
def f(cb, x):
|
||||||
|
if x: self.value = cb.value
|
||||||
|
cb.bind(active=f)
|
||||||
|
if k == key:
|
||||||
|
cb.active = True
|
||||||
|
self.ids.choices.add_widget(l)
|
||||||
|
self.ids.choices.add_widget(cb)
|
||||||
|
self.ids.choices.add_widget(Widget(size_hint_y=1))
|
||||||
|
self.callback = callback
|
||||||
|
self.title = title
|
||||||
|
self.value = key
|
|
@ -43,7 +43,6 @@ Builder.load_string('''
|
||||||
on_release: if self.root: self.root.dispatch('on_release', self)
|
on_release: if self.root: self.root.dispatch('on_release', self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<-CreateAccountDialog>
|
<-CreateAccountDialog>
|
||||||
text_color: .854, .925, .984, 1
|
text_color: .854, .925, .984, 1
|
||||||
auto_dismiss: False
|
auto_dismiss: False
|
||||||
|
@ -140,11 +139,11 @@ Builder.load_string('''
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
CreateAccountButton:
|
CreateAccountButton:
|
||||||
id: create
|
id: create
|
||||||
text: _('Create a Wallet')
|
text: _('Create a new seed')
|
||||||
root: root
|
root: root
|
||||||
CreateAccountButton:
|
CreateAccountButton:
|
||||||
id: restore
|
id: restore
|
||||||
text: _('I already have a wallet')
|
text: _('I already have a seed')
|
||||||
root: root
|
root: root
|
||||||
|
|
||||||
|
|
||||||
|
@ -457,8 +456,7 @@ class RestoreSeedDialog(CreateAccountDialog):
|
||||||
self._trigger_check_seed = Clock.create_trigger(self.check_seed)
|
self._trigger_check_seed = Clock.create_trigger(self.check_seed)
|
||||||
|
|
||||||
def check_seed(self, dt):
|
def check_seed(self, dt):
|
||||||
self.ids.next.disabled = not bool(self._wizard.is_any(
|
self.ids.next.disabled = not bool(self._wizard.is_any(self.ids.text_input_seed))
|
||||||
self.ids.text_input_seed))
|
|
||||||
|
|
||||||
def on_parent(self, instance, value):
|
def on_parent(self, instance, value):
|
||||||
if value:
|
if value:
|
||||||
|
|
|
@ -73,7 +73,9 @@ class InstallWizard(Widget):
|
||||||
def is_any(self, seed_e):
|
def is_any(self, seed_e):
|
||||||
text = self.get_seed_text(seed_e)
|
text = self.get_seed_text(seed_e)
|
||||||
return (Wallet.is_seed(text) or
|
return (Wallet.is_seed(text) or
|
||||||
Wallet.is_mpk(text) or
|
Wallet.is_old_mpk(text) or
|
||||||
|
Wallet.is_xpub(text) or
|
||||||
|
Wallet.is_xprv(text) or
|
||||||
Wallet.is_address(text) or
|
Wallet.is_address(text) or
|
||||||
Wallet.is_private_key(text))
|
Wallet.is_private_key(text))
|
||||||
|
|
||||||
|
@ -129,8 +131,8 @@ class InstallWizard(Widget):
|
||||||
if Wallet.is_seed(seed):
|
if Wallet.is_seed(seed):
|
||||||
return self.password_dialog(wallet=wallet, mode='restore',
|
return self.password_dialog(wallet=wallet, mode='restore',
|
||||||
seed=seed)
|
seed=seed)
|
||||||
elif Wallet.is_mpk(seed):
|
elif Wallet.is_xpub(seed):
|
||||||
wallet = Wallet.from_mpk(seed, self.storage)
|
wallet = Wallet.from_xpub(seed, self.storage)
|
||||||
elif Wallet.is_address(seed):
|
elif Wallet.is_address(seed):
|
||||||
wallet = Wallet.from_address(seed, self.storage)
|
wallet = Wallet.from_address(seed, self.storage)
|
||||||
elif Wallet.is_private_key(seed):
|
elif Wallet.is_private_key(seed):
|
||||||
|
@ -257,18 +259,19 @@ class InstallWizard(Widget):
|
||||||
new_password = None
|
new_password = None
|
||||||
|
|
||||||
if mode == 'restore':
|
if mode == 'restore':
|
||||||
wallet = Wallet.from_seed(seed, self.storage)
|
password = unicode(ti_password.text)
|
||||||
password = (unicode(ti_password.text)
|
# if wallet and wallet.use_encryption else
|
||||||
if wallet and wallet.use_encryption else
|
# None)
|
||||||
None)
|
if not password:
|
||||||
|
password = None
|
||||||
|
wallet = Wallet.from_text(seed, password, self.storage)
|
||||||
|
|
||||||
def on_complete(*l):
|
def on_complete(*l):
|
||||||
wallet.create_accounts(new_password)
|
|
||||||
self.load_network(wallet, mode='restore')
|
self.load_network(wallet, mode='restore')
|
||||||
_dlg.close()
|
_dlg.close()
|
||||||
|
|
||||||
self.waiting_dialog(lambda: wallet.add_seed(seed, new_password),
|
self.waiting_dialog(wallet.synchronize,
|
||||||
msg=_("saving seed"),
|
msg=_("generating addresses"),
|
||||||
on_complete=on_complete)
|
on_complete=on_complete)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
55
gui/kivy/uix/dialogs/label_dialog.py
Normal file
55
gui/kivy/uix/dialogs/label_dialog.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<LabelDialog@Popup>
|
||||||
|
id: popup
|
||||||
|
title: ''
|
||||||
|
size_hint: 0.8, 0.3
|
||||||
|
pos_hint: {'top':0.9}
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 0.2
|
||||||
|
TextInput:
|
||||||
|
id:input
|
||||||
|
padding: '5dp'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '27dp'
|
||||||
|
pos_hint: {'center_y':.5}
|
||||||
|
text:''
|
||||||
|
multiline: False
|
||||||
|
background_normal: 'atlas://gui/kivy/theming/light/tab_btn'
|
||||||
|
background_active: 'atlas://gui/kivy/theming/light/textinput_active'
|
||||||
|
hint_text_color: self.foreground_color
|
||||||
|
foreground_color: 1, 1, 1, 1
|
||||||
|
font_size: '16dp'
|
||||||
|
focus: True
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 0.2
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'horizontal'
|
||||||
|
size_hint: 1, 0.5
|
||||||
|
Button:
|
||||||
|
text: 'Cancel'
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
on_release: popup.dismiss()
|
||||||
|
Button:
|
||||||
|
text: 'OK'
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
on_release:
|
||||||
|
root.callback(input.text)
|
||||||
|
popup.dismiss()
|
||||||
|
''')
|
||||||
|
|
||||||
|
class LabelDialog(Factory.Popup):
|
||||||
|
|
||||||
|
def __init__(self, title, text, callback):
|
||||||
|
Factory.Popup.__init__(self)
|
||||||
|
self.ids.input.text = text
|
||||||
|
self.callback = callback
|
||||||
|
self.title = title
|
|
@ -1,4 +1,12 @@
|
||||||
Popup:
|
from kivy.app import App
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
|
||||||
|
<PasswordDialog@Popup>
|
||||||
id: popup
|
id: popup
|
||||||
title: _('Enter PIN Code')
|
title: _('Enter PIN Code')
|
||||||
size_hint: 0.9, 0.9
|
size_hint: 0.9, 0.9
|
||||||
|
@ -16,9 +24,9 @@ Popup:
|
||||||
|
|
||||||
GridLayout:
|
GridLayout:
|
||||||
id: kb
|
id: kb
|
||||||
update_text: app.update_password
|
update_amount: popup.update_password
|
||||||
password: ''
|
password: ''
|
||||||
on_password: if len(self.password) == 6: popup.dismiss()
|
on_password: popup.on_password(self.password)
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '300dp'
|
height: '300dp'
|
||||||
cols: 3
|
cols: 3
|
||||||
|
@ -49,3 +57,28 @@ Popup:
|
||||||
|
|
||||||
Widget:
|
Widget:
|
||||||
size_hint: 1, 1
|
size_hint: 1, 1
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordDialog(Factory.Popup):
|
||||||
|
|
||||||
|
def __init__(self, title, cb):
|
||||||
|
Factory.Popup.__init__(self)
|
||||||
|
self.title = title
|
||||||
|
self.callback = cb
|
||||||
|
|
||||||
|
def update_password(self, c):
|
||||||
|
kb = self.ids.kb
|
||||||
|
text = kb.password
|
||||||
|
if c == '<':
|
||||||
|
text = text[:-1]
|
||||||
|
elif c == 'Clear':
|
||||||
|
text = ''
|
||||||
|
else:
|
||||||
|
text += c
|
||||||
|
kb.password = text
|
||||||
|
|
||||||
|
def on_password(self, pw):
|
||||||
|
if len(pw) == 6:
|
||||||
|
self.dismiss()
|
||||||
|
self.callback(pw)
|
171
gui/kivy/uix/dialogs/settings.py
Normal file
171
gui/kivy/uix/dialogs/settings.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum.util import base_units
|
||||||
|
from electrum.i18n import languages, set_language
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<SettingsItem@ButtonBehavior+BoxLayout>
|
||||||
|
orientation: 'vertical'
|
||||||
|
title: ''
|
||||||
|
description: ''
|
||||||
|
size_hint: 1, 1
|
||||||
|
Label:
|
||||||
|
id: title
|
||||||
|
text: self.parent.title
|
||||||
|
size_hint: 1, 1
|
||||||
|
bold: True
|
||||||
|
text_size: self.size
|
||||||
|
halign: 'left'
|
||||||
|
Label:
|
||||||
|
text: self.parent.description
|
||||||
|
size_hint: 1, 1
|
||||||
|
text_size: self.width, None
|
||||||
|
color: 0.8, 0.8, 0.8, 1
|
||||||
|
halign: 'left'
|
||||||
|
|
||||||
|
<PluginItem@ButtonBehavior+BoxLayout>
|
||||||
|
orientation: 'vertical'
|
||||||
|
title: ''
|
||||||
|
description: ''
|
||||||
|
size_hint: 1, 1
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'horizontal'
|
||||||
|
Label:
|
||||||
|
id: title
|
||||||
|
text: self.parent.title
|
||||||
|
size_hint: 1, 1
|
||||||
|
bold: True
|
||||||
|
text_size: self.size
|
||||||
|
halign: 'left'
|
||||||
|
Switch:
|
||||||
|
id: sw
|
||||||
|
name: ''
|
||||||
|
Label:
|
||||||
|
text: self.parent.description
|
||||||
|
size_hint: 1, 1
|
||||||
|
text_size: self.width, None
|
||||||
|
color: 0.8, 0.8, 0.8, 1
|
||||||
|
halign: 'left'
|
||||||
|
|
||||||
|
<SettingsDialog@Popup>
|
||||||
|
id: settings
|
||||||
|
title: _('Settings')
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
SettingsItem:
|
||||||
|
lang: settings.get_language_name()
|
||||||
|
title: _('Language') + ': %s'%self.lang
|
||||||
|
description: _("Language")
|
||||||
|
on_release:
|
||||||
|
settings.language_dialog(self)
|
||||||
|
CardSeparator
|
||||||
|
SettingsItem:
|
||||||
|
title: _('PIN Code') + ': %s'%('ON' if app.wallet.use_encryption else 'OFF')
|
||||||
|
description: _("Your PIN code will be required in order to spend bitcoins.")
|
||||||
|
on_release:
|
||||||
|
app.change_password()
|
||||||
|
self.title = _('PIN Code') + ' (%s)'%('ON' if app.wallet.use_encryption else 'OFF')
|
||||||
|
CardSeparator
|
||||||
|
SettingsItem:
|
||||||
|
bu: app.base_unit
|
||||||
|
title: _('Denomination') + ': ' + self.bu
|
||||||
|
description: _("Base unit for Bitcoin amounts.")
|
||||||
|
on_release:
|
||||||
|
settings.unit_dialog(self)
|
||||||
|
CardSeparator
|
||||||
|
SettingsItem:
|
||||||
|
title: _('Fiat Currency') + ': ' + app.fiat_unit
|
||||||
|
description: "Select the local fiat currency."
|
||||||
|
on_release:
|
||||||
|
settings.fiat_dialog(self)
|
||||||
|
CardSeparator
|
||||||
|
SettingsItem:
|
||||||
|
title: _('OpenAlias')
|
||||||
|
description: "Email-like address."
|
||||||
|
on_release:
|
||||||
|
settings.openalias_dialog()
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, 1
|
||||||
|
BoxLayout:
|
||||||
|
Widget:
|
||||||
|
size_hint: 0.5, None
|
||||||
|
Button:
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
text: _('OK')
|
||||||
|
on_release:
|
||||||
|
settings.dismiss()
|
||||||
|
''')
|
||||||
|
|
||||||
|
class SettingsDialog(Factory.Popup):
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
Factory.Popup.__init__(self)
|
||||||
|
|
||||||
|
def get_language_name(self):
|
||||||
|
return languages.get(self.app.electrum_config.get('language', 'en_UK'), '')
|
||||||
|
|
||||||
|
def language_dialog(self, item):
|
||||||
|
from choice_dialog import ChoiceDialog
|
||||||
|
l = self.app.electrum_config.get('language', 'en_UK')
|
||||||
|
def cb(key):
|
||||||
|
self.app.electrum_config.set_key("language", key, True)
|
||||||
|
item.lang = self.get_language_name()
|
||||||
|
set_language(key)
|
||||||
|
d = ChoiceDialog(_('Language'), languages, l, cb)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
def unit_dialog(self, item):
|
||||||
|
from choice_dialog import ChoiceDialog
|
||||||
|
def cb(text):
|
||||||
|
self.app._set_bu(text)
|
||||||
|
item.bu = self.app.base_unit
|
||||||
|
d = ChoiceDialog(_('Denomination'), dict(map(lambda x: (x,x), base_units)), self.app.base_unit, cb)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
def fiat_dialog(self, item):
|
||||||
|
from choice_dialog import ChoiceDialog
|
||||||
|
def cb(text):
|
||||||
|
pass
|
||||||
|
d = ChoiceDialog(_('Fiat Currency'), {}, '', cb)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
def openalias_dialog(self):
|
||||||
|
from label_dialog import LabelDialog
|
||||||
|
def callback(text):
|
||||||
|
pass
|
||||||
|
d = LabelDialog(_('OpenAlias'), '', callback)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
|
||||||
|
def show_plugins(self, plugins_list):
|
||||||
|
|
||||||
|
def on_active(sw, value):
|
||||||
|
self.plugins.toggle_enabled(self.electrum_config, sw.name)
|
||||||
|
run_hook('init_kivy', self)
|
||||||
|
|
||||||
|
for item in self.plugins.descriptions:
|
||||||
|
if 'kivy' not in item.get('available_for', []):
|
||||||
|
continue
|
||||||
|
name = item.get('__name__')
|
||||||
|
label = Label(text=item.get('fullname'), height='48db', size_hint=(1, None))
|
||||||
|
plugins_list.add_widget(label)
|
||||||
|
sw = Switch()
|
||||||
|
sw.name = name
|
||||||
|
p = self.plugins.get(name)
|
||||||
|
sw.active = (p is not None) and p.is_enabled()
|
||||||
|
sw.bind(active=on_active)
|
||||||
|
plugins_list.add_widget(sw)
|
||||||
|
|
||||||
|
class PluginItem():
|
||||||
|
def __init__(self, name):
|
||||||
|
p = self.plugins.get(name)
|
||||||
|
sw.active = (p is not None) and p.is_enabled()
|
||||||
|
sw.bind(active=on_active)
|
||||||
|
plugins_list.add_widget(sw)
|
||||||
|
|
79
gui/kivy/uix/dialogs/wallets.py
Normal file
79
gui/kivy/uix/dialogs/wallets.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum.util import base_units
|
||||||
|
|
||||||
|
import os
|
||||||
|
from label_dialog import LabelDialog
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
#:import os os
|
||||||
|
<WalletDialog@Popup>:
|
||||||
|
title: _('Wallets')
|
||||||
|
id: popup
|
||||||
|
path: app.wallet.storage.path
|
||||||
|
on_path:
|
||||||
|
button.text = _('Open') if os.path.exists(popup.path) else _('Create')
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
BoxLayout:
|
||||||
|
height: '48dp'
|
||||||
|
size_hint_y: None
|
||||||
|
orientation: 'horizontal'
|
||||||
|
Label:
|
||||||
|
text: _('Wallet') + ': '
|
||||||
|
height: '48dp'
|
||||||
|
size_hint_y: None
|
||||||
|
Button:
|
||||||
|
id: wallet_name
|
||||||
|
height: '48dp'
|
||||||
|
size_hint_y: None
|
||||||
|
text: os.path.basename(app.wallet.storage.path)
|
||||||
|
on_release:
|
||||||
|
root.name_dialog()
|
||||||
|
on_text:
|
||||||
|
popup.path = os.path.join(wallet_selector.path, self.text)
|
||||||
|
Widget
|
||||||
|
size_hint_y: None
|
||||||
|
FileChooserListView:
|
||||||
|
id: wallet_selector
|
||||||
|
dirselect: False
|
||||||
|
filter_dirs: True
|
||||||
|
filter: '*.*'
|
||||||
|
path: os.path.dirname(app.wallet.storage.path)
|
||||||
|
on_selection:
|
||||||
|
wallet_name.text = os.path.basename(self.selection[0]) if self.selection else ''
|
||||||
|
size_hint_y: 0.4
|
||||||
|
Widget
|
||||||
|
size_hint_y: 0.1
|
||||||
|
|
||||||
|
GridLayout:
|
||||||
|
cols: 2
|
||||||
|
size_hint_y: None
|
||||||
|
Button:
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
text: _('Cancel')
|
||||||
|
on_release:
|
||||||
|
popup.dismiss()
|
||||||
|
Button:
|
||||||
|
id: button
|
||||||
|
size_hint: 0.5, None
|
||||||
|
height: '48dp'
|
||||||
|
text: _('Open') if os.path.exists(popup.path) else _('Create')
|
||||||
|
on_release:
|
||||||
|
popup.dismiss()
|
||||||
|
app.load_wallet_by_name(popup.path)
|
||||||
|
''')
|
||||||
|
|
||||||
|
class WalletDialog(Factory.Popup):
|
||||||
|
def name_dialog(self):
|
||||||
|
def cb(text):
|
||||||
|
if text:
|
||||||
|
self.ids.wallet_name.text = text
|
||||||
|
d = LabelDialog(_('Enter wallet name'), '', cb)
|
||||||
|
d.open()
|
||||||
|
|
|
@ -42,14 +42,9 @@ Builder.load_string('''
|
||||||
class QRCodeWidget(FloatLayout):
|
class QRCodeWidget(FloatLayout):
|
||||||
|
|
||||||
data = StringProperty(None, allow_none=True)
|
data = StringProperty(None, allow_none=True)
|
||||||
|
|
||||||
background_color = ListProperty((1, 1, 1, 1))
|
background_color = ListProperty((1, 1, 1, 1))
|
||||||
|
|
||||||
foreground_color = ListProperty((0, 0, 0, 0))
|
foreground_color = ListProperty((0, 0, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
#loading_image = StringProperty('gui/kivy/theming/loading.gif')
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(QRCodeWidget, self).__init__(**kwargs)
|
super(QRCodeWidget, self).__init__(**kwargs)
|
||||||
self.data = None
|
self.data = None
|
||||||
|
@ -57,21 +52,11 @@ class QRCodeWidget(FloatLayout):
|
||||||
self._qrtexture = None
|
self._qrtexture = None
|
||||||
|
|
||||||
def on_data(self, instance, value):
|
def on_data(self, instance, value):
|
||||||
print "on data", value
|
|
||||||
if not (self.canvas or value):
|
if not (self.canvas or value):
|
||||||
return
|
return
|
||||||
img = self.ids.get('qrimage', None)
|
|
||||||
|
|
||||||
if not img:
|
|
||||||
# if texture hasn't yet been created delay the texture updation
|
|
||||||
Clock.schedule_once(lambda dt: self.on_data(instance, value))
|
|
||||||
return
|
|
||||||
|
|
||||||
#Thread(target=partial(self.update_qr, )).start()
|
|
||||||
self.update_qr()
|
self.update_qr()
|
||||||
|
|
||||||
def set_data(self, data):
|
def set_data(self, data):
|
||||||
print "set data", data
|
|
||||||
if self.data == data:
|
if self.data == data:
|
||||||
return
|
return
|
||||||
MinSize = 210 if len(data) < 128 else 500
|
MinSize = 210 if len(data) < 128 else 500
|
||||||
|
@ -98,7 +83,7 @@ class QRCodeWidget(FloatLayout):
|
||||||
# currently unused, do we need this?
|
# currently unused, do we need this?
|
||||||
self._texture_size = size
|
self._texture_size = size
|
||||||
|
|
||||||
def _create_texture(self, k, dt):
|
def _create_texture(self, k):
|
||||||
self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb')
|
self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb')
|
||||||
# don't interpolate texture
|
# don't interpolate texture
|
||||||
texture.min_filter = 'nearest'
|
texture.min_filter = 'nearest'
|
||||||
|
@ -107,32 +92,24 @@ class QRCodeWidget(FloatLayout):
|
||||||
def update_texture(self):
|
def update_texture(self):
|
||||||
if not self.qr:
|
if not self.qr:
|
||||||
return
|
return
|
||||||
|
|
||||||
matrix = self.qr.get_matrix()
|
matrix = self.qr.get_matrix()
|
||||||
k = len(matrix)
|
k = len(matrix)
|
||||||
# create the texture in main UI thread otherwise
|
# create the texture
|
||||||
# this will lead to memory corruption
|
self._create_texture(k)
|
||||||
Clock.schedule_once(partial(self._create_texture, k), -1)
|
|
||||||
buff = []
|
buff = []
|
||||||
bext = buff.extend
|
bext = buff.extend
|
||||||
cr, cg, cb, ca = self.background_color[:]
|
cr, cg, cb, ca = self.background_color[:]
|
||||||
cr, cg, cb = cr*255, cg*255, cb*255
|
cr, cg, cb = cr*255, cg*255, cb*255
|
||||||
|
|
||||||
for r in range(k):
|
for r in range(k):
|
||||||
for c in range(k):
|
for c in range(k):
|
||||||
bext([0, 0, 0] if matrix[r][c] else [cr, cg, cb])
|
bext([0, 0, 0] if matrix[k-1-r][c] else [cr, cg, cb])
|
||||||
|
|
||||||
# then blit the buffer
|
# then blit the buffer
|
||||||
buff = ''.join(map(chr, buff))
|
buff = ''.join(map(chr, buff))
|
||||||
# update texture in UI thread.
|
# update texture
|
||||||
Clock.schedule_once(lambda dt: self._upd_texture(buff), .1)
|
self._upd_texture(buff)
|
||||||
|
|
||||||
def _upd_texture(self, buff):
|
def _upd_texture(self, buff):
|
||||||
texture = self._qrtexture
|
texture = self._qrtexture
|
||||||
if not texture:
|
|
||||||
# if texture hasn't yet been created delay the texture updation
|
|
||||||
Clock.schedule_once(lambda dt: self._upd_texture(buff), .1)
|
|
||||||
return
|
|
||||||
texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte')
|
texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte')
|
||||||
img =self.ids.qrimage
|
img =self.ids.qrimage
|
||||||
img.anim_delay = -1
|
img.anim_delay = -1
|
||||||
|
|
|
@ -17,17 +17,24 @@ from kivy.lang import Builder
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import profiler, parse_URI
|
from electrum.util import profiler, parse_URI, format_time
|
||||||
from electrum import bitcoin
|
from electrum import bitcoin
|
||||||
from electrum.util import timestamp_to_datetime
|
from electrum.util import timestamp_to_datetime
|
||||||
from electrum.plugins import run_hook
|
from electrum.plugins import run_hook
|
||||||
|
|
||||||
|
from context_menu import ContextMenu
|
||||||
|
|
||||||
|
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
||||||
|
|
||||||
|
|
||||||
class CScreen(Factory.Screen):
|
class CScreen(Factory.Screen):
|
||||||
|
|
||||||
__events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
|
__events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
|
||||||
action_view = ObjectProperty(None)
|
action_view = ObjectProperty(None)
|
||||||
loaded = False
|
loaded = False
|
||||||
kvname = None
|
kvname = None
|
||||||
|
context_menu = None
|
||||||
|
menu_actions = []
|
||||||
app = App.get_running_app()
|
app = App.get_running_app()
|
||||||
|
|
||||||
def _change_action_view(self):
|
def _change_action_view(self):
|
||||||
|
@ -65,8 +72,17 @@ class CScreen(Factory.Screen):
|
||||||
self.dispatch('on_deactivate')
|
self.dispatch('on_deactivate')
|
||||||
|
|
||||||
def on_deactivate(self):
|
def on_deactivate(self):
|
||||||
pass
|
self.hide_menu()
|
||||||
#Clock.schedule_once(lambda dt: self._change_action_view())
|
|
||||||
|
def hide_menu(self):
|
||||||
|
if self.context_menu is not None:
|
||||||
|
self.remove_widget(self.context_menu)
|
||||||
|
self.context_menu = None
|
||||||
|
|
||||||
|
def show_menu(self, obj):
|
||||||
|
self.hide_menu()
|
||||||
|
self.context_menu = ContextMenu(obj, self.menu_actions)
|
||||||
|
self.add_widget(self.context_menu)
|
||||||
|
|
||||||
|
|
||||||
class HistoryScreen(CScreen):
|
class HistoryScreen(CScreen):
|
||||||
|
@ -77,10 +93,18 @@ class HistoryScreen(CScreen):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.ra_dialog = None
|
self.ra_dialog = None
|
||||||
super(HistoryScreen, self).__init__(**kwargs)
|
super(HistoryScreen, self).__init__(**kwargs)
|
||||||
|
self.menu_actions = [ (_('Label'), self.label_dialog), (_('Details'), self.app.tx_details_dialog)]
|
||||||
|
|
||||||
|
def label_dialog(self, obj):
|
||||||
|
from dialogs.label_dialog import LabelDialog
|
||||||
|
key = obj.tx_hash
|
||||||
|
text = self.app.wallet.get_label(key)
|
||||||
|
def callback(text):
|
||||||
|
self.app.wallet.set_label(key, text)
|
||||||
|
self.update()
|
||||||
|
d = LabelDialog(_('Enter Transaction Label'), text, callback)
|
||||||
|
d.open()
|
||||||
|
|
||||||
def get_history_rate(self, btc_balance, timestamp):
|
|
||||||
date = timestamp_to_datetime(timestamp)
|
|
||||||
return run_hook('historical_value_str', btc_balance, date)
|
|
||||||
|
|
||||||
def parse_history(self, items):
|
def parse_history(self, items):
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -98,49 +122,43 @@ class HistoryScreen(CScreen):
|
||||||
time_str = _('pending')
|
time_str = _('pending')
|
||||||
icon = "atlas://gui/kivy/theming/light/unconfirmed"
|
icon = "atlas://gui/kivy/theming/light/unconfirmed"
|
||||||
elif conf < 6:
|
elif conf < 6:
|
||||||
time_str = '' # add new to fix error when conf < 0
|
|
||||||
conf = max(1, conf)
|
conf = max(1, conf)
|
||||||
icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)
|
icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)
|
||||||
else:
|
else:
|
||||||
icon = "atlas://gui/kivy/theming/light/confirmed"
|
icon = "atlas://gui/kivy/theming/light/confirmed"
|
||||||
|
|
||||||
if tx_hash:
|
label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs')
|
||||||
label, is_default_label = self.app.wallet.get_label(tx_hash)
|
date = timestamp_to_datetime(timestamp)
|
||||||
|
rate = run_hook('history_rate', date)
|
||||||
|
if self.app.fiat_unit:
|
||||||
|
quote_text = "..." if rate is None else "{0:.3} {1}".format(Decimal(value) / 100000000 * Decimal(rate), self.app.fiat_unit)
|
||||||
else:
|
else:
|
||||||
label = _('Pruned transaction outputs')
|
quote_text = ''
|
||||||
is_default_label = False
|
|
||||||
|
|
||||||
quote_currency = 'USD'
|
|
||||||
rate = self.get_history_rate(value, timestamp)
|
|
||||||
quote_text = "..." if rate is None else "{0:.3} {1}".format(rate, quote_currency)
|
|
||||||
|
|
||||||
yield (conf, icon, time_str, label, value, tx_hash, quote_text)
|
yield (conf, icon, time_str, label, value, tx_hash, quote_text)
|
||||||
|
|
||||||
def update(self, see_all=False):
|
def update(self, see_all=False):
|
||||||
if self.app.wallet is None:
|
if self.app.wallet is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
history_card = self.screen.ids.recent_activity_card
|
history_card = self.screen.ids.history_container
|
||||||
history = self.parse_history(reversed(
|
history = self.parse_history(reversed(
|
||||||
self.app.wallet.get_history(self.app.current_account)))
|
self.app.wallet.get_history(self.app.current_account)))
|
||||||
# repopulate History Card
|
# repopulate History Card
|
||||||
last_widget = history_card.ids.content.children[-1]
|
history_card.clear_widgets()
|
||||||
history_card.ids.content.clear_widgets()
|
history_add = history_card.add_widget
|
||||||
history_add = history_card.ids.content.add_widget
|
|
||||||
history_add(last_widget)
|
|
||||||
RecentActivityItem = Factory.RecentActivityItem
|
|
||||||
count = 0
|
count = 0
|
||||||
for item in history:
|
for item in history:
|
||||||
count += 1
|
count += 1
|
||||||
conf, icon, date_time, address, value, tx, quote_text = item
|
conf, icon, date_time, message, value, tx, quote_text = item
|
||||||
ri = RecentActivityItem()
|
ri = Factory.HistoryItem()
|
||||||
ri.icon = icon
|
ri.icon = icon
|
||||||
ri.date = date_time
|
ri.date = date_time
|
||||||
ri.address = address
|
ri.message = message
|
||||||
ri.value = value
|
ri.value = value
|
||||||
ri.quote_text = quote_text
|
ri.quote_text = quote_text
|
||||||
ri.confirmations = conf
|
ri.confirmations = conf
|
||||||
ri.tx_hash = tx
|
ri.tx_hash = tx
|
||||||
|
ri.screen = self
|
||||||
history_add(ri)
|
history_add(ri)
|
||||||
if count == 8 and not see_all:
|
if count == 8 and not see_all:
|
||||||
break
|
break
|
||||||
|
@ -181,20 +199,35 @@ class ScreenPassword(Factory.Screen):
|
||||||
class SendScreen(CScreen):
|
class SendScreen(CScreen):
|
||||||
|
|
||||||
kvname = 'send'
|
kvname = 'send'
|
||||||
|
payment_request = None
|
||||||
|
|
||||||
def set_URI(self, uri):
|
def set_URI(self, uri):
|
||||||
print "set uri", uri
|
|
||||||
self.screen.address = uri.get('address', '')
|
self.screen.address = uri.get('address', '')
|
||||||
self.screen.message = uri.get('message', '')
|
self.screen.message = uri.get('message', '')
|
||||||
amount = uri.get('amount')
|
amount = uri.get('amount')
|
||||||
if amount:
|
if amount:
|
||||||
amount_str = str( Decimal(amount) / pow(10, self.app.decimal_point()))
|
self.screen.amount = self.app.format_amount_and_units(amount)
|
||||||
self.screen.amount = amount_str + ' ' + self.app.base_unit
|
|
||||||
|
def update(self):
|
||||||
|
if self.app.current_invoice:
|
||||||
|
self.set_request(self.app.current_invoice)
|
||||||
|
|
||||||
def do_clear(self):
|
def do_clear(self):
|
||||||
self.screen.amount = ''
|
self.screen.amount = ''
|
||||||
self.screen.message = ''
|
self.screen.message = ''
|
||||||
self.screen.address = ''
|
self.screen.address = ''
|
||||||
|
self.payment_request = None
|
||||||
|
|
||||||
|
def amount_dialog(self):
|
||||||
|
Clock.schedule_once(lambda dt: self.app.amount_dialog(self, True), .25)
|
||||||
|
|
||||||
|
def set_request(self, pr):
|
||||||
|
self.payment_request = pr
|
||||||
|
self.screen.address = pr.get_requestor()
|
||||||
|
amount = pr.get_amount()
|
||||||
|
if amount:
|
||||||
|
self.screen.amount = self.app.format_amount_and_units(amount)
|
||||||
|
self.screen.message = pr.get_memo()
|
||||||
|
|
||||||
def do_paste(self):
|
def do_paste(self):
|
||||||
contents = unicode(self.app._clipboard.get())
|
contents = unicode(self.app._clipboard.get())
|
||||||
|
@ -206,6 +239,12 @@ class SendScreen(CScreen):
|
||||||
self.set_URI(uri)
|
self.set_URI(uri)
|
||||||
|
|
||||||
def do_send(self):
|
def do_send(self):
|
||||||
|
if self.payment_request:
|
||||||
|
if self.payment_request.has_expired():
|
||||||
|
self.app.show_error(_('Payment request has expired'))
|
||||||
|
return
|
||||||
|
outputs = self.payment_request.get_outputs()
|
||||||
|
else:
|
||||||
address = str(self.screen.address)
|
address = str(self.screen.address)
|
||||||
if not bitcoin.is_address(address):
|
if not bitcoin.is_address(address):
|
||||||
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
|
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
|
||||||
|
@ -215,9 +254,9 @@ class SendScreen(CScreen):
|
||||||
except:
|
except:
|
||||||
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
|
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
|
||||||
return
|
return
|
||||||
|
outputs = [('address', address, amount)]
|
||||||
message = unicode(self.screen.message)
|
message = unicode(self.screen.message)
|
||||||
fee = None
|
fee = None
|
||||||
outputs = [('address', address, amount)]
|
|
||||||
self.app.protected(self.send_tx, (outputs, fee, message))
|
self.app.protected(self.send_tx, (outputs, fee, message))
|
||||||
|
|
||||||
def send_tx(self, *args):
|
def send_tx(self, *args):
|
||||||
|
@ -250,7 +289,14 @@ class ReceiveScreen(CScreen):
|
||||||
kvname = 'receive'
|
kvname = 'receive'
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.screen.address = self.app.wallet.get_unused_address(None)
|
addr = self.app.get_receive_address()
|
||||||
|
self.screen.address = addr
|
||||||
|
req = self.app.wallet.receive_requests.get(addr)
|
||||||
|
if req:
|
||||||
|
self.screen.message = unicode(req.get('memo', ''))
|
||||||
|
amount = req.get('amount')
|
||||||
|
if amount:
|
||||||
|
self.screen.amount = self.app.format_amount_and_units(amount)
|
||||||
|
|
||||||
def amount_callback(self, popup):
|
def amount_callback(self, popup):
|
||||||
amount_label = self.screen.ids.get('amount')
|
amount_label = self.screen.ids.get('amount')
|
||||||
|
@ -269,17 +315,35 @@ class ReceiveScreen(CScreen):
|
||||||
@profiler
|
@profiler
|
||||||
def update_qr(self):
|
def update_qr(self):
|
||||||
uri = self.get_URI()
|
uri = self.get_URI()
|
||||||
qr = self.screen.ids.get('qr')
|
qr = self.screen.ids.qr
|
||||||
qr.set_data(uri)
|
qr.set_data(uri)
|
||||||
|
|
||||||
def do_copy(self):
|
def do_copy(self):
|
||||||
uri = self.get_URI()
|
uri = self.get_URI()
|
||||||
self.app._clipboard.put(uri, 'text/plain')
|
self.app._clipboard.put(uri, 'text/plain')
|
||||||
|
|
||||||
def do_clear(self):
|
def do_save(self):
|
||||||
|
addr = str(self.screen.address)
|
||||||
|
amount = str(self.screen.amount)
|
||||||
|
message = str(self.screen.message) #.ids.message_input.text)
|
||||||
|
if not message and not amount:
|
||||||
|
self.app.show_error(_('No message or amount'))
|
||||||
|
return
|
||||||
|
if amount:
|
||||||
|
amount = self.app.get_amount(amount)
|
||||||
|
else:
|
||||||
|
amount = 0
|
||||||
|
print "saving", amount, message
|
||||||
|
req = self.app.wallet.make_payment_request(addr, amount, message, None)
|
||||||
|
self.app.wallet.add_payment_request(req, self.app.electrum_config)
|
||||||
|
self.app.show_error(_('Request saved'))
|
||||||
|
self.app.update_tab('requests')
|
||||||
|
|
||||||
|
def do_new(self):
|
||||||
|
self.app.receive_address = None
|
||||||
self.screen.amount = ''
|
self.screen.amount = ''
|
||||||
self.screen.message = ''
|
self.screen.message = ''
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
class ContactsScreen(CScreen):
|
class ContactsScreen(CScreen):
|
||||||
|
@ -311,6 +375,76 @@ class ContactsScreen(CScreen):
|
||||||
contact_list.add_widget(ci)
|
contact_list.add_widget(ci)
|
||||||
|
|
||||||
|
|
||||||
|
class InvoicesScreen(CScreen):
|
||||||
|
kvname = 'invoices'
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.menu_actions = [(_('Pay'), self.do_pay), (_('Delete'), self.do_delete)]
|
||||||
|
invoices_list = self.screen.ids.invoices_container
|
||||||
|
invoices_list.clear_widgets()
|
||||||
|
for pr in self.app.invoices.sorted_list():
|
||||||
|
ci = Factory.InvoiceItem()
|
||||||
|
ci.key = pr.get_id()
|
||||||
|
ci.requestor = pr.get_requestor()
|
||||||
|
ci.memo = pr.memo
|
||||||
|
ci.amount = self.app.format_amount_and_units(pr.get_amount())
|
||||||
|
status = self.app.invoices.get_status(ci.key)
|
||||||
|
if status == PR_PAID:
|
||||||
|
ci.icon = "atlas://gui/kivy/theming/light/confirmed"
|
||||||
|
elif status == PR_EXPIRED:
|
||||||
|
ci.icon = "atlas://gui/kivy/theming/light/important"
|
||||||
|
else:
|
||||||
|
ci.icon = "atlas://gui/kivy/theming/light/important"
|
||||||
|
exp = pr.get_expiration_date()
|
||||||
|
ci.date = format_time(exp) if exp else _('Never')
|
||||||
|
ci.screen = self
|
||||||
|
invoices_list.add_widget(ci)
|
||||||
|
|
||||||
|
def do_pay(self, obj):
|
||||||
|
self.app.do_pay(obj)
|
||||||
|
|
||||||
|
def do_delete(self, obj):
|
||||||
|
self.app.invoices.remove(obj.key)
|
||||||
|
self.app.update_tab('invoices')
|
||||||
|
|
||||||
|
class RequestsScreen(CScreen):
|
||||||
|
kvname = 'requests'
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
|
||||||
|
self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)]
|
||||||
|
|
||||||
|
requests_list = self.screen.ids.requests_container
|
||||||
|
requests_list.clear_widgets()
|
||||||
|
for req in self.app.wallet.get_sorted_requests(self.app.electrum_config):
|
||||||
|
address = req['address']
|
||||||
|
timestamp = req.get('time', 0)
|
||||||
|
amount = req.get('amount')
|
||||||
|
expiration = req.get('exp', None)
|
||||||
|
status = req.get('status')
|
||||||
|
signature = req.get('sig')
|
||||||
|
ci = Factory.RequestItem()
|
||||||
|
ci.address = req['address']
|
||||||
|
ci.memo = self.app.wallet.get_label(address)
|
||||||
|
status = req.get('status')
|
||||||
|
if status == PR_PAID:
|
||||||
|
ci.icon = "atlas://gui/kivy/theming/light/confirmed"
|
||||||
|
elif status == PR_EXPIRED:
|
||||||
|
ci.icon = "atlas://gui/kivy/theming/light/important"
|
||||||
|
else:
|
||||||
|
ci.icon = "atlas://gui/kivy/theming/light/important"
|
||||||
|
ci.amount = self.app.format_amount_and_units(amount) if amount else ''
|
||||||
|
ci.date = format_time(timestamp)
|
||||||
|
ci.screen = self
|
||||||
|
requests_list.add_widget(ci)
|
||||||
|
|
||||||
|
def do_show(self, obj):
|
||||||
|
self.app.show_request(obj.address)
|
||||||
|
|
||||||
|
def do_delete(self, obj):
|
||||||
|
self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
class CSpinner(Factory.Spinner):
|
class CSpinner(Factory.Spinner):
|
||||||
'''CustomDropDown that allows fading out the dropdown
|
'''CustomDropDown that allows fading out the dropdown
|
||||||
|
@ -340,27 +474,20 @@ class TabbedCarousel(Factory.TabbedPanel):
|
||||||
scrlv = self._tab_strip.parent
|
scrlv = self._tab_strip.parent
|
||||||
if not scrlv:
|
if not scrlv:
|
||||||
return
|
return
|
||||||
|
|
||||||
idx = self.tab_list.index(value)
|
idx = self.tab_list.index(value)
|
||||||
if idx == 0:
|
n = len(self.tab_list)
|
||||||
|
if idx in [0, 1]:
|
||||||
scroll_x = 1
|
scroll_x = 1
|
||||||
elif idx == len(self.tab_list) - 1:
|
elif idx in [n-1, n-2]:
|
||||||
scroll_x = 0
|
scroll_x = 0
|
||||||
else:
|
else:
|
||||||
self_center_x = scrlv.center_x
|
scroll_x = 1. * (n - idx - 1) / (n - 1)
|
||||||
vcenter_x = value.center_x
|
|
||||||
diff_x = (self_center_x - vcenter_x)
|
|
||||||
try:
|
|
||||||
scroll_x = scrlv.scroll_x - (diff_x / scrlv.width)
|
|
||||||
except ZeroDivisionError:
|
|
||||||
pass
|
|
||||||
mation = Factory.Animation(scroll_x=scroll_x, d=.25)
|
mation = Factory.Animation(scroll_x=scroll_x, d=.25)
|
||||||
mation.cancel_all(scrlv)
|
mation.cancel_all(scrlv)
|
||||||
mation.start(scrlv)
|
mation.start(scrlv)
|
||||||
|
|
||||||
def on_current_tab(self, instance, value):
|
def on_current_tab(self, instance, value):
|
||||||
if value.text == 'default_tab':
|
|
||||||
return
|
|
||||||
self.animate_tab_to_center(value)
|
self.animate_tab_to_center(value)
|
||||||
|
|
||||||
def on_index(self, instance, value):
|
def on_index(self, instance, value):
|
||||||
|
|
12
gui/kivy/uix/ui_screens/about.kv
Normal file
12
gui/kivy/uix/ui_screens/about.kv
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Popup:
|
||||||
|
title: "About Electrum"
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
spacing: '1dp'
|
||||||
|
Label:
|
||||||
|
text: "Lightweight Bitcoin Wallet"
|
||||||
|
Label:
|
||||||
|
text: "Author: Thomas Voegtlin"
|
||||||
|
Label:
|
||||||
|
text: "https://electrum.org"
|
||||||
|
Widget
|
|
@ -5,23 +5,9 @@
|
||||||
#:set mbtc_symbol unichr(187)
|
#:set mbtc_symbol unichr(187)
|
||||||
|
|
||||||
|
|
||||||
<Card@GridLayout>
|
|
||||||
cols: 1
|
|
||||||
padding: '12dp' , '22dp', '12dp' , '12dp'
|
|
||||||
spacing: '12dp'
|
|
||||||
size_hint: 1, None
|
|
||||||
height: max(100, self.minimum_height)
|
|
||||||
canvas.before:
|
|
||||||
Color:
|
|
||||||
rgba: 1, 1, 1, 1
|
|
||||||
BorderImage:
|
|
||||||
border: 18, 18, 18, 18
|
|
||||||
source: 'atlas://gui/kivy/theming/light/card'
|
|
||||||
size: self.size
|
|
||||||
pos: self.pos
|
|
||||||
|
|
||||||
<CardLabel@Label>
|
<CardLabel@Label>
|
||||||
color: 0.45, 0.45, 0.45, 1
|
color: 0.95, 0.95, 0.95, 1
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
text: ''
|
text: ''
|
||||||
text_size: self.width, None
|
text_size: self.width, None
|
||||||
|
@ -29,45 +15,17 @@
|
||||||
halign: 'left'
|
halign: 'left'
|
||||||
valign: 'top'
|
valign: 'top'
|
||||||
|
|
||||||
<CardButton@Button>
|
|
||||||
background_normal: 'atlas://gui/kivy/theming/light/card_btn'
|
|
||||||
bold: True
|
|
||||||
font_size: '10sp'
|
|
||||||
color: 0.699, 0.699, 0.699, 1
|
|
||||||
size_hint: None, None
|
|
||||||
size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7)
|
|
||||||
|
|
||||||
|
<HistoryItem@CardItem>
|
||||||
<CardItem@ToggleButtonBehavior+GridLayout>
|
|
||||||
canvas.before:
|
|
||||||
Color:
|
|
||||||
rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0
|
|
||||||
Rectangle
|
|
||||||
size: self.size
|
|
||||||
pos: self.x, self.y + dp(5)
|
|
||||||
cols: 1
|
|
||||||
padding: '2dp', '2dp'
|
|
||||||
spacing: '2dp'
|
|
||||||
size_hint: 1, None
|
|
||||||
height: self.minimum_height
|
|
||||||
group: 'history'
|
|
||||||
|
|
||||||
<RecentActivityItem@CardItem>
|
|
||||||
icon: 'atlas://gui/kivy/theming/light/important'
|
icon: 'atlas://gui/kivy/theming/light/important'
|
||||||
address: 'no address set'
|
message: ''
|
||||||
value: 0
|
value: 0
|
||||||
amount: app.format_amount(self.value, True) if self.value is not None else '--'
|
amount: app.format_amount(self.value, True) if self.value is not None else '--'
|
||||||
amount_color: '#DB3627' if self.value < 0 else '#2EA442'
|
amount_color: '#FF6657' if self.value < 0 else '#2EA442'
|
||||||
confirmations: 0
|
confirmations: 0
|
||||||
date: '0/0/0'
|
date: ''
|
||||||
quote_text: '.'
|
quote_text: ''
|
||||||
spacing: '9dp'
|
spacing: '9dp'
|
||||||
on_release:
|
|
||||||
app.tx_selected(root.tx_hash, self.state)
|
|
||||||
BoxLayout:
|
|
||||||
size_hint: 1, None
|
|
||||||
spacing: '8dp'
|
|
||||||
height: '32dp'
|
|
||||||
Image:
|
Image:
|
||||||
id: icon
|
id: icon
|
||||||
source: root.icon
|
source: root.icon
|
||||||
|
@ -78,38 +36,27 @@
|
||||||
orientation: 'vertical'
|
orientation: 'vertical'
|
||||||
Widget
|
Widget
|
||||||
CardLabel:
|
CardLabel:
|
||||||
shorten: True
|
text: root.date
|
||||||
text: root.address
|
font_size: '14sp'
|
||||||
markup: False
|
|
||||||
text_size: self.size
|
|
||||||
CardLabel:
|
CardLabel:
|
||||||
color: .699, .699, .699, 1
|
color: .699, .699, .699, 1
|
||||||
text: root.date
|
font_size: '13sp'
|
||||||
font_size: '12sp'
|
shorten: True
|
||||||
|
text: root.message
|
||||||
Widget
|
Widget
|
||||||
CardLabel:
|
CardLabel:
|
||||||
halign: 'right'
|
halign: 'right'
|
||||||
font_size: '13sp'
|
font_size: '15sp'
|
||||||
size_hint: None, 1
|
size_hint: None, 1
|
||||||
width: '110sp'
|
width: '110sp'
|
||||||
markup: True
|
markup: True
|
||||||
font_name: font_light
|
font_name: font_light
|
||||||
text:
|
text:
|
||||||
u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\
|
u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\
|
||||||
u'[color=#B2B3B3][size=12sp]{qt}[/size]'\
|
u'[color=#B2B3B3][size=13sp]{qt}[/size]'\
|
||||||
u'[/color]'.format(amount_color=root.amount_color,\
|
u'[/color]'.format(amount_color=root.amount_color,\
|
||||||
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
|
amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\
|
||||||
unit=app.base_unit)
|
unit=app.base_unit)
|
||||||
CardSeparator
|
|
||||||
|
|
||||||
<CardRecentActivity@Card>
|
|
||||||
GridLayout:
|
|
||||||
id: content
|
|
||||||
spacing: '7dp'
|
|
||||||
cols: 1
|
|
||||||
size_hint: 1, None
|
|
||||||
height: self.minimum_height
|
|
||||||
CardSeparator
|
|
||||||
|
|
||||||
|
|
||||||
HistoryScreen:
|
HistoryScreen:
|
||||||
|
@ -119,12 +66,9 @@ HistoryScreen:
|
||||||
id: content
|
id: content
|
||||||
do_scroll_x: False
|
do_scroll_x: False
|
||||||
GridLayout
|
GridLayout
|
||||||
id: grid
|
id: history_container
|
||||||
cols: 1 #if root.width < root.height else 2
|
cols: 1
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
padding: '12dp'
|
padding: '12dp'
|
||||||
spacing: '12dp'
|
spacing: '2dp'
|
||||||
CardRecentActivity:
|
|
||||||
id: recent_activity_card
|
|
||||||
|
|
||||||
|
|
59
gui/kivy/uix/ui_screens/invoices.kv
Normal file
59
gui/kivy/uix/ui_screens/invoices.kv
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<InvoicesLabel@Label>
|
||||||
|
#color: .305, .309, .309, 1
|
||||||
|
text_size: self.width, None
|
||||||
|
halign: 'left'
|
||||||
|
valign: 'top'
|
||||||
|
|
||||||
|
<InvoiceItem@CardItem>
|
||||||
|
requestor: ''
|
||||||
|
memo: ''
|
||||||
|
amount: ''
|
||||||
|
status: ''
|
||||||
|
date: ''
|
||||||
|
icon: 'atlas://gui/kivy/theming/light/important'
|
||||||
|
Image:
|
||||||
|
id: icon
|
||||||
|
source: root.icon
|
||||||
|
size_hint: None, 1
|
||||||
|
width: self.height *.54
|
||||||
|
mipmap: True
|
||||||
|
BoxLayout:
|
||||||
|
spacing: '8dp'
|
||||||
|
height: '32dp'
|
||||||
|
orientation: 'vertical'
|
||||||
|
Widget
|
||||||
|
InvoicesLabel:
|
||||||
|
text: root.requestor
|
||||||
|
shorten: True
|
||||||
|
InvoicesLabel:
|
||||||
|
text: root.memo
|
||||||
|
color: .699, .699, .699, 1
|
||||||
|
font_size: '13sp'
|
||||||
|
shorten: True
|
||||||
|
Widget
|
||||||
|
InvoicesLabel:
|
||||||
|
halign: 'right'
|
||||||
|
font_size: '15sp'
|
||||||
|
size_hint: None, 1
|
||||||
|
width: '110sp'
|
||||||
|
text: root.amount
|
||||||
|
|
||||||
|
InvoicesScreen:
|
||||||
|
name: 'invoices'
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
spacing: '1dp'
|
||||||
|
ScrollView:
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: .8901, .8901, .8901, 0
|
||||||
|
Rectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
GridLayout:
|
||||||
|
cols: 1
|
||||||
|
id: invoices_container
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
spacing: '2dp'
|
||||||
|
padding: '12dp'
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
opacity: 0.5 if qr.shaded else 1
|
opacity: 0.5 if qr.shaded else 1
|
||||||
text: s.address
|
text: _('Bitcoin Address') + ': ' + s.address
|
||||||
|
text_size: self.width, None
|
||||||
|
|
||||||
SendReceiveBlueBottom:
|
SendReceiveBlueBottom:
|
||||||
id: blue_bottom
|
id: blue_bottom
|
||||||
|
@ -51,13 +52,15 @@ ReceiveScreen:
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: blue_bottom.item_height
|
height: blue_bottom.item_height
|
||||||
|
spacing: '5dp'
|
||||||
Image:
|
Image:
|
||||||
source: 'atlas://gui/kivy/theming/light/globe'
|
source: 'atlas://gui/kivy/theming/light/globe'
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
size: '22dp', '22dp'
|
size: '22dp', '22dp'
|
||||||
pos_hint: {'center_y': .5}
|
pos_hint: {'center_y': .5}
|
||||||
AmountButton:
|
BlueButton:
|
||||||
id: amount_label
|
id: amount_label
|
||||||
|
default_text: 'Amount'
|
||||||
text: s.amount if s.amount else 'Amount'
|
text: s.amount if s.amount else 'Amount'
|
||||||
on_release: app.amount_dialog(s, False)
|
on_release: app.amount_dialog(s, False)
|
||||||
CardSeparator:
|
CardSeparator:
|
||||||
|
@ -74,12 +77,10 @@ ReceiveScreen:
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
size: '22dp', '22dp'
|
size: '22dp', '22dp'
|
||||||
pos_hint: {'center_y': .5}
|
pos_hint: {'center_y': .5}
|
||||||
TextInputBlue:
|
BlueButton:
|
||||||
id: message_input
|
id: description
|
||||||
hint_text: 'Description'
|
text: s.message if s.message else _('Description')
|
||||||
text: s.message
|
on_release: app.description_dialog(s)
|
||||||
on_text_validate: s.message = self.text
|
|
||||||
|
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
@ -89,9 +90,14 @@ ReceiveScreen:
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
on_release: s.parent.do_copy()
|
on_release: s.parent.do_copy()
|
||||||
Button:
|
Button:
|
||||||
text: _('Clear')
|
text: _('Save')
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
on_release: s.parent.do_clear()
|
on_release: s.parent.do_save()
|
||||||
Widget:
|
Button:
|
||||||
size_hint: 1, 0.3
|
text: _('New')
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '48dp'
|
||||||
|
on_release: s.parent.do_new()
|
||||||
|
#Widget:
|
||||||
|
# size_hint: 1, 0.3
|
||||||
|
|
60
gui/kivy/uix/ui_screens/requests.kv
Normal file
60
gui/kivy/uix/ui_screens/requests.kv
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<RequestLabel@Label>
|
||||||
|
#color: .305, .309, .309, 1
|
||||||
|
text_size: self.width, None
|
||||||
|
halign: 'left'
|
||||||
|
valign: 'top'
|
||||||
|
|
||||||
|
<RequestItem@CardItem>
|
||||||
|
address: ''
|
||||||
|
memo: ''
|
||||||
|
amount: ''
|
||||||
|
status: ''
|
||||||
|
date: ''
|
||||||
|
icon: 'atlas://gui/kivy/theming/light/important'
|
||||||
|
Image:
|
||||||
|
id: icon
|
||||||
|
source: root.icon
|
||||||
|
size_hint: None, 1
|
||||||
|
width: self.height *.54
|
||||||
|
mipmap: True
|
||||||
|
BoxLayout:
|
||||||
|
spacing: '8dp'
|
||||||
|
height: '32dp'
|
||||||
|
orientation: 'vertical'
|
||||||
|
Widget
|
||||||
|
RequestLabel:
|
||||||
|
text: root.address
|
||||||
|
shorten: True
|
||||||
|
RequestLabel:
|
||||||
|
text: root.memo
|
||||||
|
color: .699, .699, .699, 1
|
||||||
|
font_size: '13sp'
|
||||||
|
shorten: True
|
||||||
|
Widget
|
||||||
|
RequestLabel:
|
||||||
|
halign: 'right'
|
||||||
|
font_size: '15sp'
|
||||||
|
size_hint: None, 1
|
||||||
|
width: '110sp'
|
||||||
|
text: root.amount
|
||||||
|
|
||||||
|
|
||||||
|
RequestsScreen:
|
||||||
|
name: 'requests'
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
spacing: '1dp'
|
||||||
|
ScrollView:
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: .8901, .8901, .8901, 0
|
||||||
|
Rectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
GridLayout:
|
||||||
|
cols: 1
|
||||||
|
id: requests_container
|
||||||
|
size_hint_y: None
|
||||||
|
height: self.minimum_height
|
||||||
|
spacing: '2dp'
|
||||||
|
padding: '12dp'
|
|
@ -23,6 +23,7 @@ SendScreen:
|
||||||
id: blue_bottom
|
id: blue_bottom
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: self.minimum_height
|
height: self.minimum_height
|
||||||
|
spacing: '5dp'
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: blue_bottom.item_height
|
height: blue_bottom.item_height
|
||||||
|
@ -32,32 +33,32 @@ SendScreen:
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
size: '22dp', '22dp'
|
size: '22dp', '22dp'
|
||||||
pos_hint: {'center_y': .5}
|
pos_hint: {'center_y': .5}
|
||||||
TextInputBlue:
|
BlueButton:
|
||||||
id: payto_e
|
id: payto_e
|
||||||
text: s.address
|
text: s.address if s.address else _('Recipient')
|
||||||
hint_text: "Recipient"
|
on_release: app.address_dialog(s)
|
||||||
CardSeparator:
|
CardSeparator:
|
||||||
opacity: message_selection.opacity
|
opacity: message_selection.opacity
|
||||||
color: blue_bottom.foreground_color
|
color: blue_bottom.foreground_color
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: blue_bottom.item_height
|
height: blue_bottom.item_height
|
||||||
|
spacing: '5dp'
|
||||||
Image:
|
Image:
|
||||||
source: 'atlas://gui/kivy/theming/light/globe'
|
source: 'atlas://gui/kivy/theming/light/globe'
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
size: '22dp', '22dp'
|
size: '22dp', '22dp'
|
||||||
pos_hint: {'center_y': .5}
|
pos_hint: {'center_y': .5}
|
||||||
AmountButton:
|
BlueButton:
|
||||||
id: amount_e
|
id: amount_e
|
||||||
text: s.amount if s.amount else 'Amount'
|
default_text: _('Amount')
|
||||||
on_release: app.amount_dialog(s, True)
|
text: s.amount if s.amount else _('Amount')
|
||||||
|
on_release: s.amount_dialog()
|
||||||
CardSeparator:
|
CardSeparator:
|
||||||
opacity: message_selection.opacity
|
opacity: message_selection.opacity
|
||||||
color: blue_bottom.foreground_color
|
color: blue_bottom.foreground_color
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
id: message_selection
|
id: message_selection
|
||||||
opacity: 1
|
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: blue_bottom.item_height
|
height: blue_bottom.item_height
|
||||||
spacing: '5dp'
|
spacing: '5dp'
|
||||||
|
@ -66,11 +67,10 @@ SendScreen:
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
size: '22dp', '22dp'
|
size: '22dp', '22dp'
|
||||||
pos_hint: {'center_y': .5}
|
pos_hint: {'center_y': .5}
|
||||||
TextInputBlue:
|
BlueButton:
|
||||||
id: message_e
|
id: description
|
||||||
hint_text: 'Description'
|
text: s.message if s.message else _('Description')
|
||||||
text: s.message
|
on_release: app.description_dialog(s)
|
||||||
on_text_validate: s.message = self.text
|
|
||||||
BoxLayout:
|
BoxLayout:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
@ -78,7 +78,7 @@ SendScreen:
|
||||||
id: qr
|
id: qr
|
||||||
text: _('QR Code')
|
text: _('QR Code')
|
||||||
on_release:
|
on_release:
|
||||||
app.scan_qr(on_complete=s.parent.set_URI)
|
app.scan_qr(on_complete=app.set_URI)
|
||||||
Button:
|
Button:
|
||||||
id: paste_button
|
id: paste_button
|
||||||
text: _('Paste')
|
text: _('Paste')
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
31
gui/kivy/uix/ui_screens/status.kv
Normal file
31
gui/kivy/uix/ui_screens/status.kv
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#:import os os
|
||||||
|
|
||||||
|
Popup:
|
||||||
|
title: "Balance"
|
||||||
|
confirmed: 0
|
||||||
|
unconfirmed: 0
|
||||||
|
unmatured: 0
|
||||||
|
on_parent:
|
||||||
|
self.confirmed, self.unconfirmed, self.x = app.wallet.get_balance()
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
spacing: '1dp'
|
||||||
|
GridLayout:
|
||||||
|
cols:2
|
||||||
|
Label:
|
||||||
|
text: _("Wallet:")
|
||||||
|
Label:
|
||||||
|
text: os.path.basename(app.wallet.storage.path)
|
||||||
|
Label:
|
||||||
|
text: _("Confirmed:")
|
||||||
|
Label:
|
||||||
|
text: app.format_amount_and_units(root.confirmed)
|
||||||
|
Label:
|
||||||
|
text: _("Unconfirmed:")
|
||||||
|
Label:
|
||||||
|
text: app.format_amount_and_units(root.unconfirmed)
|
||||||
|
Label:
|
||||||
|
text: "Unmatured:"
|
||||||
|
Label:
|
||||||
|
text: app.format_amount_and_units(root.unmatured)
|
||||||
|
Widget
|
|
@ -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):
|
def __init__(self, config, network, plugins):
|
||||||
set_language(config.get('language'))
|
set_language(config.get('language'))
|
||||||
|
@ -134,7 +134,7 @@ class ElectrumGui:
|
||||||
try:
|
try:
|
||||||
storage = WalletStorage(filename)
|
storage = WalletStorage(filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
if not storage.file_exists:
|
if not storage.file_exists:
|
||||||
recent = self.config.get('recently_open', [])
|
recent = self.config.get('recently_open', [])
|
||||||
|
@ -147,7 +147,7 @@ class ElectrumGui:
|
||||||
wallet = Wallet(storage)
|
wallet = Wallet(storage)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
QMessageBox.warning(None, _('Warning'), str(e), _('OK'))
|
self.show_warning(str(e))
|
||||||
return
|
return
|
||||||
action = wallet.get_action()
|
action = wallet.get_action()
|
||||||
# run wizard
|
# run wizard
|
||||||
|
@ -162,32 +162,6 @@ class ElectrumGui:
|
||||||
|
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
def get_wallet_folder(self):
|
|
||||||
#return os.path.dirname(os.path.abspath(self.wallet.storage.path if self.wallet else self.wallet.storage.path))
|
|
||||||
return os.path.dirname(os.path.abspath(self.config.get_wallet_path()))
|
|
||||||
|
|
||||||
def new_wallet(self):
|
|
||||||
wallet_folder = self.get_wallet_folder()
|
|
||||||
i = 1
|
|
||||||
while True:
|
|
||||||
filename = "wallet_%d"%i
|
|
||||||
if filename in os.listdir(wallet_folder):
|
|
||||||
i += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
filename = line_dialog(None, _('New Wallet'), _('Enter file name') + ':', _('OK'), filename)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
full_path = os.path.join(wallet_folder, filename)
|
|
||||||
storage = WalletStorage(full_path)
|
|
||||||
if storage.file_exists:
|
|
||||||
QMessageBox.critical(None, "Error", _("File exists"))
|
|
||||||
return
|
|
||||||
wizard = InstallWizard(self.app, self.config, self.network, storage)
|
|
||||||
wallet = wizard.run('new')
|
|
||||||
if wallet:
|
|
||||||
self.new_window(full_path)
|
|
||||||
|
|
||||||
def new_window(self, path, uri=None):
|
def new_window(self, path, uri=None):
|
||||||
# Use a signal as can be called from daemon thread
|
# Use a signal as can be called from daemon thread
|
||||||
self.app.emit(SIGNAL('new_window'), path, uri)
|
self.app.emit(SIGNAL('new_window'), path, uri)
|
||||||
|
@ -245,6 +219,9 @@ class ElectrumGui:
|
||||||
# main loop
|
# main loop
|
||||||
self.app.exec_()
|
self.app.exec_()
|
||||||
|
|
||||||
|
# Shut down the timer cleanly
|
||||||
|
self.timer.stop()
|
||||||
|
|
||||||
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
|
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
|
||||||
event = QtCore.QEvent(QtCore.QEvent.Clipboard)
|
event = QtCore.QEvent(QtCore.QEvent.Clipboard)
|
||||||
self.app.sendEvent(self.app.clipboard(), event)
|
self.app.sendEvent(self.app.clipboard(), event)
|
||||||
|
|
|
@ -25,9 +25,10 @@ from PyQt4.QtCore import *
|
||||||
from util import *
|
from util import *
|
||||||
from history_widget import HistoryWidget
|
from history_widget import HistoryWidget
|
||||||
|
|
||||||
class AddressDialog(QDialog):
|
class AddressDialog(WindowModalDialog):
|
||||||
|
|
||||||
def __init__(self, address, parent):
|
def __init__(self, parent, address):
|
||||||
|
WindowModalDialog.__init__(self, parent, _("Address"))
|
||||||
self.address = address
|
self.address = address
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.config = parent.config
|
self.config = parent.config
|
||||||
|
@ -35,10 +36,7 @@ class AddressDialog(QDialog):
|
||||||
self.app = parent.app
|
self.app = parent.app
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
QDialog.__init__(self)
|
|
||||||
self.setMinimumWidth(700)
|
self.setMinimumWidth(700)
|
||||||
self.setWindowTitle(_("Address"))
|
|
||||||
self.setModal(1)
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
self.setLayout(vbox)
|
self.setLayout(vbox)
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ class HistoryWidget(MyTreeWidget):
|
||||||
icon, time_str = self.get_icon(conf, timestamp)
|
icon, time_str = self.get_icon(conf, timestamp)
|
||||||
v_str = self.parent.format_amount(value, True, whitespaces=True)
|
v_str = self.parent.format_amount(value, True, whitespaces=True)
|
||||||
balance_str = self.parent.format_amount(balance, whitespaces=True)
|
balance_str = self.parent.format_amount(balance, whitespaces=True)
|
||||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
label = self.wallet.get_label(tx_hash)
|
||||||
entry = ['', tx_hash, time_str, label, v_str, balance_str]
|
entry = ['', tx_hash, time_str, label, v_str, balance_str]
|
||||||
run_hook('history_tab_update', tx, entry)
|
run_hook('history_tab_update', tx, entry)
|
||||||
item = QTreeWidgetItem(entry)
|
item = QTreeWidgetItem(entry)
|
||||||
|
@ -88,8 +88,6 @@ class HistoryWidget(MyTreeWidget):
|
||||||
item.setForeground(4, QBrush(QColor("#BC1E1E")))
|
item.setForeground(4, QBrush(QColor("#BC1E1E")))
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
item.setData(0, Qt.UserRole, tx_hash)
|
item.setData(0, Qt.UserRole, tx_hash)
|
||||||
if is_default_label:
|
|
||||||
item.setForeground(3, QBrush(QColor('grey')))
|
|
||||||
self.insertTopLevelItem(0, item)
|
self.insertTopLevelItem(0, item)
|
||||||
if current_tx == tx_hash:
|
if current_tx == tx_hash:
|
||||||
self.setCurrentItem(item)
|
self.setCurrentItem(item)
|
||||||
|
|
|
@ -62,17 +62,17 @@ class CosignWidget(QWidget):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InstallWizard(QDialog):
|
class InstallWizard(WindowModalDialog, MessageBoxMixin):
|
||||||
|
|
||||||
def __init__(self, app, config, network, storage):
|
def __init__(self, app, config, network, storage):
|
||||||
QDialog.__init__(self)
|
title = 'Electrum' + ' - ' + _('Install Wizard')
|
||||||
|
WindowModalDialog.__init__(self, None, title=title)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.config = config
|
self.config = config
|
||||||
self.network = network
|
self.network = network
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
self.setMinimumSize(575, 400)
|
self.setMinimumSize(575, 400)
|
||||||
self.setMaximumSize(575, 400)
|
self.setMaximumSize(575, 400)
|
||||||
self.setWindowTitle('Electrum' + ' - ' + _('Install Wizard'))
|
|
||||||
self.connect(self, QtCore.SIGNAL('accept'), self.accept)
|
self.connect(self, QtCore.SIGNAL('accept'), self.accept)
|
||||||
self.stack = QStackedLayout()
|
self.stack = QStackedLayout()
|
||||||
self.setLayout(self.stack)
|
self.setLayout(self.stack)
|
||||||
|
@ -139,8 +139,8 @@ class InstallWizard(QDialog):
|
||||||
button.setChecked(True)
|
button.setChecked(True)
|
||||||
|
|
||||||
vbox.addStretch(1)
|
vbox.addStretch(1)
|
||||||
self.set_layout(vbox)
|
|
||||||
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next'))))
|
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _('Next'))))
|
||||||
|
self.set_layout(vbox)
|
||||||
self.show()
|
self.show()
|
||||||
self.raise_()
|
self.raise_()
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ class InstallWizard(QDialog):
|
||||||
if not r:
|
if not r:
|
||||||
return
|
return
|
||||||
if prepare_seed(r) != prepare_seed(seed):
|
if prepare_seed(r) != prepare_seed(seed):
|
||||||
QMessageBox.warning(None, _('Error'), _('Incorrect seed'), _('OK'))
|
self.show_error(_('Incorrect seed'))
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
@ -384,22 +384,6 @@ class InstallWizard(QDialog):
|
||||||
wallet_type = '%dof%d'%(m,n)
|
wallet_type = '%dof%d'%(m,n)
|
||||||
return wallet_type
|
return wallet_type
|
||||||
|
|
||||||
def question(self, msg, yes_label=_('OK'), no_label=_('Cancel'), icon=None):
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
self.set_layout(vbox)
|
|
||||||
if icon:
|
|
||||||
logo = QLabel()
|
|
||||||
logo.setPixmap(icon)
|
|
||||||
vbox.addWidget(logo)
|
|
||||||
label = QLabel(msg)
|
|
||||||
label.setWordWrap(True)
|
|
||||||
vbox.addWidget(label)
|
|
||||||
vbox.addStretch(1)
|
|
||||||
vbox.addLayout(Buttons(CancelButton(self, no_label), OkButton(self, yes_label)))
|
|
||||||
if not self.exec_():
|
|
||||||
return None
|
|
||||||
return True
|
|
||||||
|
|
||||||
def show_seed(self, seed, sid):
|
def show_seed(self, seed, sid):
|
||||||
vbox = seed_dialog.show_seed_box_msg(seed, sid)
|
vbox = seed_dialog.show_seed_box_msg(seed, sid)
|
||||||
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _("Next"))))
|
vbox.addLayout(Buttons(CancelButton(self), OkButton(self, _("Next"))))
|
||||||
|
@ -407,22 +391,21 @@ class InstallWizard(QDialog):
|
||||||
return self.exec_()
|
return self.exec_()
|
||||||
|
|
||||||
def password_dialog(self):
|
def password_dialog(self):
|
||||||
msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
|
from password_dialog import PasswordDialog
|
||||||
+_("Leave these fields empty if you want to disable encryption.")
|
msg = _("Please choose a password to encrypt your wallet keys.\n"
|
||||||
from password_dialog import make_password_dialog, run_password_dialog
|
"Leave these fields empty if you want to disable encryption.")
|
||||||
self.set_layout( make_password_dialog(self, None, msg) )
|
dialog = PasswordDialog(self, None, _("Choose a password"), msg, True)
|
||||||
return run_password_dialog(self, None, self)[2]
|
return dialog.run()[2]
|
||||||
|
|
||||||
def run(self, action):
|
def run(self, action):
|
||||||
if self.storage.file_exists and action != 'new':
|
if self.storage.file_exists and action != 'new':
|
||||||
path = self.storage.path
|
path = self.storage.path
|
||||||
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
||||||
"Do you want to complete its creation now?") % path
|
"Do you want to complete its creation now?") % path
|
||||||
if not question(msg):
|
if not self.question(msg):
|
||||||
if question(_("Do you want to delete '%s'?") % path):
|
if self.question(_("Do you want to delete '%s'?") % path):
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
QMessageBox.information(self, _('Warning'),
|
self.show_warning(_('The file was removed'))
|
||||||
_('The file was removed'), _('OK'))
|
|
||||||
return
|
return
|
||||||
return
|
return
|
||||||
self.show()
|
self.show()
|
||||||
|
@ -434,7 +417,7 @@ class InstallWizard(QDialog):
|
||||||
wallet = self.run_wallet_type(action, wallet_type)
|
wallet = self.run_wallet_type(action, wallet_type)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
@ -463,7 +446,7 @@ class InstallWizard(QDialog):
|
||||||
elif wallet_type == 'twofactor':
|
elif wallet_type == 'twofactor':
|
||||||
wallet_type = '2fa'
|
wallet_type = '2fa'
|
||||||
if action == 'create':
|
if action == 'create':
|
||||||
self.storage.put('wallet_type', wallet_type, False)
|
self.storage.put('wallet_type', wallet_type)
|
||||||
|
|
||||||
if action is None:
|
if action is None:
|
||||||
return
|
return
|
||||||
|
@ -527,7 +510,7 @@ class InstallWizard(QDialog):
|
||||||
if self.config.get('server') is None:
|
if self.config.get('server') is None:
|
||||||
self.network_dialog()
|
self.network_dialog()
|
||||||
else:
|
else:
|
||||||
QMessageBox.information(None, _('Warning'), _('You are offline'), _('OK'))
|
self.show_warning(_('You are offline'))
|
||||||
|
|
||||||
|
|
||||||
# start wallet threads
|
# start wallet threads
|
||||||
|
@ -539,7 +522,7 @@ class InstallWizard(QDialog):
|
||||||
msg = _("Recovery successful") if wallet.is_found() else _("No transactions found for this seed")
|
msg = _("Recovery successful") if wallet.is_found() else _("No transactions found for this seed")
|
||||||
else:
|
else:
|
||||||
msg = _("This wallet was restored offline. It may contain more addresses than displayed.")
|
msg = _("This wallet was restored offline. It may contain more addresses than displayed.")
|
||||||
QMessageBox.information(None, _('Information'), msg, _('OK'))
|
self.show_message(msg)
|
||||||
|
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
|
@ -560,7 +543,7 @@ class InstallWizard(QDialog):
|
||||||
password = self.password_dialog() if any(map(lambda x: Wallet.is_seed(x) or Wallet.is_xprv(x), key_list)) else None
|
password = self.password_dialog() if any(map(lambda x: Wallet.is_seed(x) or Wallet.is_xprv(x), key_list)) else None
|
||||||
wallet = Wallet.from_multisig(key_list, password, self.storage, t)
|
wallet = Wallet.from_multisig(key_list, password, self.storage, t)
|
||||||
else:
|
else:
|
||||||
self.storage.put('wallet_type', t, False)
|
self.storage.put('wallet_type', t)
|
||||||
# call the constructor to load the plugin (side effect)
|
# call the constructor to load the plugin (side effect)
|
||||||
Wallet(self.storage)
|
Wallet(self.storage)
|
||||||
wallet = always_hook('installwizard_restore', self, self.storage)
|
wallet = always_hook('installwizard_restore', self, self.storage)
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import sys, time, threading
|
import sys, time, threading
|
||||||
import os.path, json, traceback
|
import os, json, traceback
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import weakref
|
import weakref
|
||||||
|
@ -40,20 +40,17 @@ from electrum.i18n import _
|
||||||
from electrum.util import block_explorer, block_explorer_info, block_explorer_URL
|
from electrum.util import block_explorer, block_explorer_info, block_explorer_URL
|
||||||
from electrum.util import format_satoshis, format_satoshis_plain, format_time
|
from electrum.util import format_satoshis, format_satoshis_plain, format_time
|
||||||
from electrum.util import PrintError, NotEnoughFunds, StoreDict
|
from electrum.util import PrintError, NotEnoughFunds, StoreDict
|
||||||
from electrum import Transaction
|
from electrum import Transaction, mnemonic
|
||||||
from electrum import mnemonic
|
|
||||||
from electrum import util, bitcoin, commands, Wallet
|
from electrum import util, bitcoin, commands, Wallet
|
||||||
from electrum import SimpleConfig, COIN_CHOOSERS, WalletStorage
|
from electrum import SimpleConfig, COIN_CHOOSERS, WalletStorage
|
||||||
from electrum import Imported_Wallet
|
from electrum import Imported_Wallet, paymentrequest
|
||||||
from electrum import paymentrequest
|
|
||||||
|
|
||||||
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
|
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
|
||||||
from network_dialog import NetworkDialog
|
from network_dialog import NetworkDialog
|
||||||
from qrcodewidget import QRCodeWidget, QRDialog
|
from qrcodewidget import QRCodeWidget, QRDialog
|
||||||
from qrtextedit import ScanQRTextEdit, ShowQRTextEdit
|
from qrtextedit import ShowQRTextEdit
|
||||||
from transaction_dialog import show_transaction
|
from transaction_dialog import show_transaction
|
||||||
|
from installwizard import InstallWizard
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +80,6 @@ class StatusBarButton(QPushButton):
|
||||||
|
|
||||||
|
|
||||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
||||||
from electrum.paymentrequest import PaymentRequest, get_payment_request
|
|
||||||
|
|
||||||
pr_icons = {
|
pr_icons = {
|
||||||
PR_UNPAID:":icons/unpaid.png",
|
PR_UNPAID:":icons/unpaid.png",
|
||||||
|
@ -106,7 +102,7 @@ expiration_values = [
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ElectrumWindow(QMainWindow, PrintError):
|
class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
def __init__(self, gui_object, wallet):
|
def __init__(self, gui_object, wallet):
|
||||||
QMainWindow.__init__(self)
|
QMainWindow.__init__(self)
|
||||||
|
@ -270,7 +266,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.update_account_selector()
|
self.update_account_selector()
|
||||||
# update menus
|
# update menus
|
||||||
self.new_account_menu.setVisible(self.wallet.can_create_accounts())
|
self.new_account_menu.setVisible(self.wallet.can_create_accounts())
|
||||||
self.private_keys_menu.setEnabled(not self.wallet.is_watching_only())
|
self.export_menu.setEnabled(not self.wallet.is_watching_only())
|
||||||
self.password_menu.setEnabled(self.wallet.can_change_password())
|
self.password_menu.setEnabled(self.wallet.can_change_password())
|
||||||
self.seed_menu.setEnabled(self.wallet.has_seed())
|
self.seed_menu.setEnabled(self.wallet.has_seed())
|
||||||
self.mpk_menu.setEnabled(self.wallet.is_deterministic())
|
self.mpk_menu.setEnabled(self.wallet.is_deterministic())
|
||||||
|
@ -288,14 +284,17 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
except:
|
except:
|
||||||
self.setGeometry(100, 100, 840, 400)
|
self.setGeometry(100, 100, 840, 400)
|
||||||
self.show()
|
self.show()
|
||||||
|
self.warn_if_watching_only()
|
||||||
|
run_hook('load_wallet', wallet, self)
|
||||||
|
|
||||||
|
def warn_if_watching_only(self):
|
||||||
if self.wallet.is_watching_only():
|
if self.wallet.is_watching_only():
|
||||||
msg = ' '.join([
|
msg = ' '.join([
|
||||||
_("This wallet is watching-only."),
|
_("This wallet is watching-only."),
|
||||||
_("This means you will not be able to spend Bitcoins with it."),
|
_("This means you will not be able to spend Bitcoins with it."),
|
||||||
_("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.")
|
_("Make sure you own the seed phrase or the private keys, before you request Bitcoins to be sent to this wallet.")
|
||||||
])
|
])
|
||||||
QMessageBox.warning(self, _('Information'), msg, _('OK'))
|
self.show_warning(msg, title=_('Information'))
|
||||||
run_hook('load_wallet', wallet, self)
|
|
||||||
|
|
||||||
def import_old_contacts(self):
|
def import_old_contacts(self):
|
||||||
# backward compatibility: import contacts
|
# backward compatibility: import contacts
|
||||||
|
@ -321,7 +320,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.wallet.synchronize()
|
self.wallet.synchronize()
|
||||||
|
|
||||||
def open_wallet(self):
|
def open_wallet(self):
|
||||||
wallet_folder = self.gui_object.get_wallet_folder()
|
wallet_folder = self.get_wallet_folder()
|
||||||
filename = unicode(QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder))
|
filename = unicode(QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder))
|
||||||
if not filename:
|
if not filename:
|
||||||
return
|
return
|
||||||
|
@ -339,11 +338,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
if new_path != path:
|
if new_path != path:
|
||||||
try:
|
try:
|
||||||
shutil.copy2(path, new_path)
|
shutil.copy2(path, new_path)
|
||||||
QMessageBox.information(None,"Wallet backup created", _("A copy of your wallet file was created in")+" '%s'" % str(new_path))
|
self.show_message(_("A copy of your wallet file was created in")+" '%s'" % str(new_path), title=_("Wallet backup created"))
|
||||||
except (IOError, os.error), reason:
|
except (IOError, os.error), reason:
|
||||||
QMessageBox.critical(None,"Unable to create backup", _("Electrum was unable to copy your wallet file to the specified location.")+"\n" + str(reason))
|
self.show_critical(_("Electrum was unable to copy your wallet file to the specified location.") + "\n" + str(reason), title=_("Unable to create backup"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def update_recently_visited(self, filename=None):
|
def update_recently_visited(self, filename=None):
|
||||||
recent = self.config.get('recently_open', [])
|
recent = self.config.get('recently_open', [])
|
||||||
|
@ -361,13 +358,39 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1)))
|
self.recently_visited_menu.addAction(b, loader(k)).setShortcut(QKeySequence("Ctrl+%d"%(i+1)))
|
||||||
self.recently_visited_menu.setEnabled(len(recent))
|
self.recently_visited_menu.setEnabled(len(recent))
|
||||||
|
|
||||||
|
def get_wallet_folder(self):
|
||||||
|
return os.path.dirname(os.path.abspath(self.config.get_wallet_path()))
|
||||||
|
|
||||||
|
def new_wallet(self):
|
||||||
|
wallet_folder = self.get_wallet_folder()
|
||||||
|
i = 1
|
||||||
|
while True:
|
||||||
|
filename = "wallet_%d" % i
|
||||||
|
if filename in os.listdir(wallet_folder):
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
filename = line_dialog(self, _('New Wallet'), _('Enter file name')
|
||||||
|
+ ':', _('OK'), filename)
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
full_path = os.path.join(wallet_folder, filename)
|
||||||
|
storage = WalletStorage(full_path)
|
||||||
|
if storage.file_exists:
|
||||||
|
self.show_critical(_("File exists"))
|
||||||
|
return
|
||||||
|
wizard = InstallWizard(self.app, self.config, self.network, storage)
|
||||||
|
wallet = wizard.run('new')
|
||||||
|
if wallet:
|
||||||
|
self.new_window(full_path)
|
||||||
|
|
||||||
def init_menubar(self):
|
def init_menubar(self):
|
||||||
menubar = QMenuBar()
|
menubar = QMenuBar()
|
||||||
|
|
||||||
file_menu = menubar.addMenu(_("&File"))
|
file_menu = menubar.addMenu(_("&File"))
|
||||||
self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
|
self.recently_visited_menu = file_menu.addMenu(_("&Recently open"))
|
||||||
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
|
file_menu.addAction(_("&Open"), self.open_wallet).setShortcut(QKeySequence.Open)
|
||||||
file_menu.addAction(_("&New/Restore"), self.gui_object.new_wallet).setShortcut(QKeySequence.New)
|
file_menu.addAction(_("&New/Restore"), self.new_wallet).setShortcut(QKeySequence.New)
|
||||||
file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
|
file_menu.addAction(_("&Save Copy"), self.backup_wallet).setShortcut(QKeySequence.SaveAs)
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
file_menu.addAction(_("&Quit"), self.close)
|
file_menu.addAction(_("&Quit"), self.close)
|
||||||
|
@ -435,7 +458,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
_("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
|
_("Before reporting a bug, upgrade to the most recent version of Electrum (latest release or git HEAD), and include the version number in your report."),
|
||||||
_("Try to explain not only what the bug is, but how it occurs.")
|
_("Try to explain not only what the bug is, but how it occurs.")
|
||||||
])
|
])
|
||||||
QMessageBox.information(self, "Electrum - " + _("Reporting Bugs"), msg)
|
self.show_message(msg, title="Electrum - " + _("Reporting Bugs"))
|
||||||
|
|
||||||
def notify_transactions(self):
|
def notify_transactions(self):
|
||||||
if not self.network or not self.network.is_connected():
|
if not self.network or not self.network.is_connected():
|
||||||
|
@ -582,7 +605,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
def show_address(self, addr):
|
def show_address(self, addr):
|
||||||
import address_dialog
|
import address_dialog
|
||||||
d = address_dialog.AddressDialog(addr, self)
|
d = address_dialog.AddressDialog(self, addr)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def show_transaction(self, tx, tx_desc = None):
|
def show_transaction(self, tx, tx_desc = None):
|
||||||
|
@ -744,7 +767,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
try:
|
try:
|
||||||
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
|
self.wallet.sign_payment_request(addr, alias, alias_addr, password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, _('Error'), str(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
@ -757,7 +780,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
amount = self.receive_amount_e.get_amount()
|
amount = self.receive_amount_e.get_amount()
|
||||||
message = unicode(self.receive_message_e.text())
|
message = unicode(self.receive_message_e.text())
|
||||||
if not message and not amount:
|
if not message and not amount:
|
||||||
QMessageBox.warning(self, _('Error'), _('No message or amount'), _('OK'))
|
self.show_error(_('No message or amount'))
|
||||||
return False
|
return False
|
||||||
i = self.expires_combo.currentIndex()
|
i = self.expires_combo.currentIndex()
|
||||||
expiration = map(lambda x: x[1], expiration_values)[i]
|
expiration = map(lambda x: x[1], expiration_values)[i]
|
||||||
|
@ -769,8 +792,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.save_request_button.setEnabled(False)
|
self.save_request_button.setEnabled(False)
|
||||||
|
|
||||||
def view_and_paste(self, title, msg, data):
|
def view_and_paste(self, title, msg, data):
|
||||||
dialog = QDialog(self)
|
dialog = WindowModalDialog(self, title)
|
||||||
dialog.setWindowTitle(title)
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
label = QLabel(msg)
|
label = QLabel(msg)
|
||||||
label.setWordWrap(True)
|
label.setWordWrap(True)
|
||||||
|
@ -1129,7 +1151,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.wallet.check_password(password)
|
self.wallet.check_password(password)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(parent, _('Error'), str(e), _('OK'))
|
self.show_error(str(e), parent=parent)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
password = None
|
password = None
|
||||||
|
@ -1140,7 +1162,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
def read_send_tab(self):
|
def read_send_tab(self):
|
||||||
if self.payment_request and self.payment_request.has_expired():
|
if self.payment_request and self.payment_request.has_expired():
|
||||||
QMessageBox.warning(self, _('Error'), _('Payment request has expired'), _('OK'))
|
self.show_error(_('Payment request has expired'))
|
||||||
return
|
return
|
||||||
label = unicode( self.message_e.text() )
|
label = unicode( self.message_e.text() )
|
||||||
|
|
||||||
|
@ -1161,23 +1183,23 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not outputs:
|
if not outputs:
|
||||||
QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
|
self.show_error(_('No outputs'))
|
||||||
return
|
return
|
||||||
|
|
||||||
for _type, addr, amount in outputs:
|
for _type, addr, amount in outputs:
|
||||||
if addr is None:
|
if addr is None:
|
||||||
QMessageBox.warning(self, _('Error'), _('Bitcoin Address is None'), _('OK'))
|
self.show_error(_('Bitcoin Address is None'))
|
||||||
return
|
return
|
||||||
if _type == 'address' and not bitcoin.is_address(addr):
|
if _type == 'address' and not bitcoin.is_address(addr):
|
||||||
QMessageBox.warning(self, _('Error'), _('Invalid Bitcoin Address'), _('OK'))
|
self.show_error(_('Invalid Bitcoin Address'))
|
||||||
return
|
return
|
||||||
if amount is None:
|
if amount is None:
|
||||||
QMessageBox.warning(self, _('Error'), _('Invalid Amount'), _('OK'))
|
self.show_error(_('Invalid Amount'))
|
||||||
return
|
return
|
||||||
|
|
||||||
fee = self.fee_e.get_amount()
|
fee = self.fee_e.get_amount()
|
||||||
if fee is None:
|
if fee is None:
|
||||||
QMessageBox.warning(self, _('Error'), _('Invalid Fee'), _('OK'))
|
self.show_error(_('Invalid Fee'))
|
||||||
return
|
return
|
||||||
|
|
||||||
coins = self.get_coins()
|
coins = self.get_coins()
|
||||||
|
@ -1203,7 +1225,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
return
|
return
|
||||||
|
|
||||||
if tx.get_fee() < MIN_RELAY_TX_FEE and tx.requires_fee(self.wallet):
|
if tx.get_fee() < MIN_RELAY_TX_FEE and tx.requires_fee(self.wallet):
|
||||||
QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK'))
|
self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network"))
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.show_before_broadcast():
|
if self.show_before_broadcast():
|
||||||
|
@ -1258,16 +1280,14 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
def sign_thread():
|
def sign_thread():
|
||||||
if not self.wallet.is_watching_only():
|
if not self.wallet.is_watching_only():
|
||||||
self.wallet.sign_transaction(tx, password)
|
self.wallet.sign_transaction(tx, password)
|
||||||
def on_sign_successful(ret):
|
def on_signed(ret):
|
||||||
success[0] = True
|
success[0] = True
|
||||||
def on_dialog_close():
|
def on_finished():
|
||||||
self.send_button.setDisabled(False)
|
self.send_button.setDisabled(False)
|
||||||
callback(success[0])
|
callback(success[0])
|
||||||
|
|
||||||
# keep a reference to WaitingDialog or the gui might crash
|
WaitingDialog(parent, _('Signing transaction...'), sign_thread,
|
||||||
self.waiting_dialog = WaitingDialog(parent, 'Signing transaction...', sign_thread, on_sign_successful, on_dialog_close)
|
on_success=on_signed, on_finished=on_finished)
|
||||||
self.waiting_dialog.start()
|
|
||||||
|
|
||||||
|
|
||||||
def broadcast_transaction(self, tx, tx_desc, parent=None):
|
def broadcast_transaction(self, tx, tx_desc, parent=None):
|
||||||
|
|
||||||
|
@ -1296,19 +1316,16 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
if status:
|
if status:
|
||||||
if tx_desc is not None and tx.is_complete():
|
if tx_desc is not None and tx.is_complete():
|
||||||
self.wallet.set_label(tx.hash(), tx_desc)
|
self.wallet.set_label(tx.hash(), tx_desc)
|
||||||
QMessageBox.information(parent, '', _('Payment sent.') + '\n' + msg, _('OK'))
|
self.show_message(_('Payment sent.') + '\n' + msg, parent=parent)
|
||||||
self.invoices_list.update()
|
self.invoices_list.update()
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(parent, _('Error'), msg, _('OK'))
|
self.show_error(msg, parent=parent)
|
||||||
self.send_button.setDisabled(False)
|
self.send_button.setDisabled(False)
|
||||||
|
|
||||||
if parent == None:
|
parent = parent or self
|
||||||
parent = self
|
WaitingDialog(parent, _('Broadcasting transaction...'),
|
||||||
self.waiting_dialog = WaitingDialog(parent, 'Broadcasting transaction...', broadcast_thread, broadcast_done)
|
broadcast_thread, broadcast_done)
|
||||||
self.waiting_dialog.start()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_for_payment_request(self):
|
def prepare_for_payment_request(self):
|
||||||
self.tabs.setCurrentIndex(1)
|
self.tabs.setCurrentIndex(1)
|
||||||
|
@ -1346,37 +1363,28 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
|
|
||||||
def pay_to_URI(self, URI):
|
def on_pr(self, request):
|
||||||
if not URI:
|
self.payment_request = request
|
||||||
return
|
|
||||||
try:
|
|
||||||
out = util.parse_URI(unicode(URI))
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK'))
|
|
||||||
return
|
|
||||||
self.tabs.setCurrentIndex(1)
|
|
||||||
|
|
||||||
r = out.get('r')
|
|
||||||
sig = out.get('sig')
|
|
||||||
name = out.get('name')
|
|
||||||
if r or (name and sig):
|
|
||||||
def get_payment_request_thread():
|
|
||||||
if name and sig:
|
|
||||||
from electrum import paymentrequest
|
|
||||||
pr = paymentrequest.serialize_request(out).SerializeToString()
|
|
||||||
self.payment_request = paymentrequest.PaymentRequest(pr)
|
|
||||||
else:
|
|
||||||
self.payment_request = get_payment_request(r)
|
|
||||||
if self.payment_request.verify(self.contacts):
|
if self.payment_request.verify(self.contacts):
|
||||||
self.emit(SIGNAL('payment_request_ok'))
|
self.emit(SIGNAL('payment_request_ok'))
|
||||||
else:
|
else:
|
||||||
self.emit(SIGNAL('payment_request_error'))
|
self.emit(SIGNAL('payment_request_error'))
|
||||||
t = threading.Thread(target=get_payment_request_thread)
|
|
||||||
t.setDaemon(True)
|
def pay_to_URI(self, URI):
|
||||||
t.start()
|
if not URI:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
out = util.parse_URI(unicode(URI), self.on_pr)
|
||||||
|
except BaseException as e:
|
||||||
|
self.show_error(_('Invalid bitcoin URI:') + '\n' + str(e))
|
||||||
|
return
|
||||||
|
self.tabs.setCurrentIndex(1)
|
||||||
|
r = out.get('r')
|
||||||
|
sig = out.get('sig')
|
||||||
|
name = out.get('name')
|
||||||
|
if r or (name and sig):
|
||||||
self.prepare_for_payment_request()
|
self.prepare_for_payment_request()
|
||||||
return
|
return
|
||||||
|
|
||||||
address = out.get('address')
|
address = out.get('address')
|
||||||
amount = out.get('amount')
|
amount = out.get('amount')
|
||||||
label = out.get('label')
|
label = out.get('label')
|
||||||
|
@ -1555,6 +1563,13 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
def paytomany(self):
|
def paytomany(self):
|
||||||
self.tabs.setCurrentIndex(1)
|
self.tabs.setCurrentIndex(1)
|
||||||
self.payto_e.paytomany()
|
self.payto_e.paytomany()
|
||||||
|
msg = '\n'.join([
|
||||||
|
_('Enter a list of outputs in the \'Pay to\' field.'),
|
||||||
|
_('One output per line.'),
|
||||||
|
_('Format: address, amount'),
|
||||||
|
_('You may load a CSV file using the file icon.')
|
||||||
|
])
|
||||||
|
self.show_warning(msg, title=_('Pay to many'))
|
||||||
|
|
||||||
def payto_contacts(self, labels):
|
def payto_contacts(self, labels):
|
||||||
paytos = [self.get_contact_payto(label) for label in labels]
|
paytos = [self.get_contact_payto(label) for label in labels]
|
||||||
|
@ -1578,7 +1593,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
def set_contact(self, label, address):
|
def set_contact(self, label, address):
|
||||||
if not is_valid(address):
|
if not is_valid(address):
|
||||||
QMessageBox.warning(self, _('Error'), _('Invalid Address'), _('OK'))
|
self.show_error(_('Invalid Address'))
|
||||||
self.contacts_list.update() # Displays original unchanged value
|
self.contacts_list.update() # Displays original unchanged value
|
||||||
return False
|
return False
|
||||||
self.contacts[label] = ('address', address)
|
self.contacts[label] = ('address', address)
|
||||||
|
@ -1628,8 +1643,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.show_pr_details(pr)
|
self.show_pr_details(pr)
|
||||||
|
|
||||||
def show_pr_details(self, pr):
|
def show_pr_details(self, pr):
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _("Invoice"))
|
||||||
d.setWindowTitle(_("Invoice"))
|
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
|
grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
|
||||||
|
@ -1840,8 +1854,39 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
def change_password_dialog(self):
|
def change_password_dialog(self):
|
||||||
from password_dialog import PasswordDialog
|
from password_dialog import PasswordDialog
|
||||||
d = PasswordDialog(self.wallet, self)
|
|
||||||
d.run()
|
if self.wallet and self.wallet.is_watching_only():
|
||||||
|
self.show_error(_('This is a watching-only wallet'))
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = (_('Your wallet is encrypted. Use this dialog to change your '
|
||||||
|
'password. To disable wallet encryption, enter an empty new '
|
||||||
|
'password.') if self.wallet.use_encryption
|
||||||
|
else _('Your wallet keys are not encrypted'))
|
||||||
|
d = PasswordDialog(self, self.wallet, _("Set Password"), msg, True)
|
||||||
|
ok, password, new_password = d.run()
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.wallet.check_password(password)
|
||||||
|
except BaseException as e:
|
||||||
|
self.show_error(str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.wallet.update_password(password, new_password)
|
||||||
|
except:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
self.show_error(_('Failed to update password'))
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_password:
|
||||||
|
msg = _('Password was updated successfully')
|
||||||
|
else:
|
||||||
|
msg = _('This wallet is not encrypted')
|
||||||
|
self.show_message(msg, title=_("Success"))
|
||||||
|
|
||||||
self.update_lock_icon()
|
self.update_lock_icon()
|
||||||
|
|
||||||
def toggle_search(self):
|
def toggle_search(self):
|
||||||
|
@ -1866,8 +1911,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
def new_contact_dialog(self):
|
def new_contact_dialog(self):
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _("New Contact"))
|
||||||
d.setWindowTitle(_("New Contact"))
|
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
vbox.addWidget(QLabel(_('New Contact') + ':'))
|
vbox.addWidget(QLabel(_('New Contact') + ':'))
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
|
@ -1892,9 +1936,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
def new_account_dialog(self, password):
|
def new_account_dialog(self, password):
|
||||||
dialog = QDialog(self)
|
dialog = WindowModalDialog(self, _("New Account"))
|
||||||
dialog.setModal(1)
|
|
||||||
dialog.setWindowTitle(_("New Account"))
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget(QLabel(_('Account name')+':'))
|
vbox.addWidget(QLabel(_('Account name')+':'))
|
||||||
e = QLineEdit()
|
e = QLineEdit()
|
||||||
|
@ -1917,11 +1959,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
def show_master_public_keys(self):
|
def show_master_public_keys(self):
|
||||||
|
dialog = WindowModalDialog(self, "Master Public Keys")
|
||||||
dialog = QDialog(self)
|
|
||||||
dialog.setModal(1)
|
|
||||||
dialog.setWindowTitle(_("Master Public Keys"))
|
|
||||||
|
|
||||||
mpk_dict = self.wallet.get_master_public_keys()
|
mpk_dict = self.wallet.get_master_public_keys()
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
# only show the combobox in case multiple accounts are available
|
# only show the combobox in case multiple accounts are available
|
||||||
|
@ -1966,13 +2004,13 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
@protected
|
@protected
|
||||||
def show_seed_dialog(self, password):
|
def show_seed_dialog(self, password):
|
||||||
if not self.wallet.has_seed():
|
if not self.wallet.has_seed():
|
||||||
QMessageBox.information(self, _('Message'), _('This wallet has no seed'), _('OK'))
|
self.show_message(_('This wallet has no seed'))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mnemonic = self.wallet.get_mnemonic(password)
|
mnemonic = self.wallet.get_mnemonic(password)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
QMessageBox.warning(self, _('Error'), str(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
from seed_dialog import SeedDialog
|
from seed_dialog import SeedDialog
|
||||||
d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
|
d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
|
||||||
|
@ -1980,10 +2018,10 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def show_qrcode(self, data, title = _("QR code")):
|
def show_qrcode(self, data, title = _("QR code"), parent=None):
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
d = QRDialog(data, self, title)
|
d = QRDialog(data, parent or self, title)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def show_public_keys(self, address):
|
def show_public_keys(self, address):
|
||||||
|
@ -1995,10 +2033,8 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.show_message(str(e))
|
self.show_message(str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _("Public key"))
|
||||||
d.setMinimumSize(600, 200)
|
d.setMinimumSize(600, 200)
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_("Public key"))
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
||||||
vbox.addWidget( QLabel(_("Public key") + ':'))
|
vbox.addWidget( QLabel(_("Public key") + ':'))
|
||||||
|
@ -2019,10 +2055,8 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.show_message(str(e))
|
self.show_message(str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _("Private key"))
|
||||||
d.setMinimumSize(600, 200)
|
d.setMinimumSize(600, 200)
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_("Private key"))
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
||||||
vbox.addWidget( QLabel(_("Private key") + ':'))
|
vbox.addWidget( QLabel(_("Private key") + ':'))
|
||||||
|
@ -2056,9 +2090,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
def sign_verify_message(self, address=''):
|
def sign_verify_message(self, address=''):
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _('Sign/verify Message'))
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_('Sign/verify Message'))
|
|
||||||
d.setMinimumSize(410, 290)
|
d.setMinimumSize(410, 290)
|
||||||
|
|
||||||
layout = QGridLayout(d)
|
layout = QGridLayout(d)
|
||||||
|
@ -2117,9 +2149,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
def encrypt_message(self, address = ''):
|
def encrypt_message(self, address = ''):
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _('Encrypt/decrypt Message'))
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_('Encrypt/decrypt Message'))
|
|
||||||
d.setMinimumSize(610, 490)
|
d.setMinimumSize(610, 490)
|
||||||
|
|
||||||
layout = QGridLayout(d)
|
layout = QGridLayout(d)
|
||||||
|
@ -2157,22 +2187,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
layout.addLayout(hbox, 4, 1)
|
layout.addLayout(hbox, 4, 1)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
|
|
||||||
def question(self, msg):
|
|
||||||
return QMessageBox.question(self, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
|
|
||||||
|
|
||||||
def show_message(self, msg):
|
|
||||||
QMessageBox.information(self, _('Message'), msg, _('OK'))
|
|
||||||
|
|
||||||
def show_warning(self, msg):
|
|
||||||
QMessageBox.warning(self, _('Warning'), msg, _('OK'))
|
|
||||||
|
|
||||||
def password_dialog(self, msg=None, parent=None):
|
def password_dialog(self, msg=None, parent=None):
|
||||||
if parent == None:
|
parent = parent or self
|
||||||
parent = self
|
d = WindowModalDialog(parent, _("Enter Password"))
|
||||||
d = QDialog(parent)
|
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_("Enter Password"))
|
|
||||||
pw = QLineEdit()
|
pw = QLineEdit()
|
||||||
pw.setEchoMode(2)
|
pw.setEchoMode(2)
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
|
@ -2200,33 +2217,24 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
except:
|
except:
|
||||||
is_hex = False
|
is_hex = False
|
||||||
|
|
||||||
|
try:
|
||||||
if is_hex:
|
if is_hex:
|
||||||
try:
|
|
||||||
return Transaction(txt)
|
return Transaction(txt)
|
||||||
except:
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
|
||||||
QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
tx_dict = json.loads(str(txt))
|
tx_dict = json.loads(str(txt))
|
||||||
assert "hex" in tx_dict.keys()
|
assert "hex" in tx_dict.keys()
|
||||||
tx = Transaction(tx_dict["hex"])
|
tx = Transaction(tx_dict["hex"])
|
||||||
#if tx_dict.has_key("input_info"):
|
|
||||||
# input_info = json.loads(tx_dict['input_info'])
|
|
||||||
# tx.add_input_info(input_info)
|
|
||||||
return tx
|
return tx
|
||||||
except Exception:
|
except:
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
QMessageBox.critical(None, _("Unable to parse transaction"), _("Electrum was unable to parse your transaction"))
|
self.show_critical(_("Electrum was unable to parse your transaction"))
|
||||||
|
return
|
||||||
|
|
||||||
def read_tx_from_qrcode(self):
|
def read_tx_from_qrcode(self):
|
||||||
from electrum import qrscanner
|
from electrum import qrscanner
|
||||||
try:
|
try:
|
||||||
data = qrscanner.scan_qr(self.config)
|
data = qrscanner.scan_qr(self.config)
|
||||||
except BaseException, e:
|
except BaseException as e:
|
||||||
QMessageBox.warning(self, _('Error'), _(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
@ -2252,8 +2260,8 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
try:
|
try:
|
||||||
with open(fileName, "r") as f:
|
with open(fileName, "r") as f:
|
||||||
file_content = f.read()
|
file_content = f.read()
|
||||||
except (ValueError, IOError, os.error), reason:
|
except (ValueError, IOError, os.error) as reason:
|
||||||
QMessageBox.critical(None, _("Unable to read file or no transaction found"), _("Electrum was unable to open your transaction file") + "\n" + str(reason))
|
self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), title=_("Unable to read file or no transaction found"))
|
||||||
return self.tx_from_text(file_content)
|
return self.tx_from_text(file_content)
|
||||||
|
|
||||||
def do_process_from_text(self):
|
def do_process_from_text(self):
|
||||||
|
@ -2292,11 +2300,10 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
try:
|
try:
|
||||||
self.wallet.check_password(password)
|
self.wallet.check_password(password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, _('Error'), str(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _('Private keys'))
|
||||||
d.setWindowTitle(_('Private keys'))
|
|
||||||
d.setMinimumSize(850, 300)
|
d.setMinimumSize(850, 300)
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
|
|
||||||
|
@ -2349,9 +2356,12 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
|
self.do_export_privkeys(filename, private_keys, csv_button.isChecked())
|
||||||
except (IOError, os.error), reason:
|
except (IOError, os.error) as reason:
|
||||||
export_error_label = _("Electrum was unable to produce a private key-export.")
|
txt = "\n".join([
|
||||||
QMessageBox.critical(None, _("Unable to create csv"), export_error_label + "\n" + str(reason))
|
_("Electrum was unable to produce a private key-export."),
|
||||||
|
str(reason)
|
||||||
|
])
|
||||||
|
self.show_critical(txt, title=_("Unable to create csv"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.show_message(str(e))
|
self.show_message(str(e))
|
||||||
|
@ -2381,9 +2391,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
f.close()
|
f.close()
|
||||||
for key, value in json.loads(data).items():
|
for key, value in json.loads(data).items():
|
||||||
self.wallet.set_label(key, value)
|
self.wallet.set_label(key, value)
|
||||||
QMessageBox.information(None, _("Labels imported"), _("Your labels were imported from")+" '%s'" % str(labelsFile))
|
self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile))
|
||||||
except (IOError, os.error), reason:
|
except (IOError, os.error) as reason:
|
||||||
QMessageBox.critical(None, _("Unable to import labels"), _("Electrum was unable to import your labels.")+"\n" + str(reason))
|
self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason))
|
||||||
|
|
||||||
|
|
||||||
def do_export_labels(self):
|
def do_export_labels(self):
|
||||||
|
@ -2393,14 +2403,13 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
if fileName:
|
if fileName:
|
||||||
with open(fileName, 'w+') as f:
|
with open(fileName, 'w+') as f:
|
||||||
json.dump(labels, f)
|
json.dump(labels, f)
|
||||||
QMessageBox.information(None, _("Labels exported"), _("Your labels where exported to")+" '%s'" % str(fileName))
|
self.show_message(_("Your labels where exported to") + " '%s'" % str(fileName))
|
||||||
except (IOError, os.error), reason:
|
except (IOError, os.error), reason:
|
||||||
QMessageBox.critical(None, _("Unable to export labels"), _("Electrum was unable to export your labels.")+"\n" + str(reason))
|
self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason))
|
||||||
|
|
||||||
|
|
||||||
def export_history_dialog(self):
|
def export_history_dialog(self):
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _('Export History'))
|
||||||
d.setWindowTitle(_('Export History'))
|
|
||||||
d.setMinimumSize(400, 200)
|
d.setMinimumSize(400, 200)
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
defaultname = os.path.expanduser('~/electrum-history.csv')
|
defaultname = os.path.expanduser('~/electrum-history.csv')
|
||||||
|
@ -2421,9 +2430,9 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
self.do_export_history(self.wallet, filename, csv_button.isChecked())
|
self.do_export_history(self.wallet, filename, csv_button.isChecked())
|
||||||
except (IOError, os.error), reason:
|
except (IOError, os.error), reason:
|
||||||
export_error_label = _("Electrum was unable to produce a transaction export.")
|
export_error_label = _("Electrum was unable to produce a transaction export.")
|
||||||
QMessageBox.critical(self, _("Unable to export history"), export_error_label + "\n" + str(reason))
|
self.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
|
||||||
return
|
return
|
||||||
QMessageBox.information(self,_("History exported"), _("Your wallet history has been successfully exported."))
|
self.show_message(_("Your wallet history has been successfully exported."))
|
||||||
|
|
||||||
|
|
||||||
def do_export_history(self, wallet, fileName, is_csv):
|
def do_export_history(self, wallet, fileName, is_csv):
|
||||||
|
@ -2445,7 +2454,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
value_string = '--'
|
value_string = '--'
|
||||||
|
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
label, is_default_label = wallet.get_label(tx_hash)
|
label = wallet.get_label(tx_hash)
|
||||||
label = label.encode('utf-8')
|
label = label.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
label = ""
|
label = ""
|
||||||
|
@ -2467,12 +2476,11 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
def sweep_key_dialog(self):
|
def sweep_key_dialog(self):
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, title=_('Sweep private keys'))
|
||||||
d.setWindowTitle(_('Sweep private keys'))
|
|
||||||
d.setMinimumSize(600, 300)
|
d.setMinimumSize(600, 300)
|
||||||
|
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
vbox.addWidget(QLabel(_("Enter private keys")))
|
vbox.addWidget(QLabel(_("Enter private keys:")))
|
||||||
|
|
||||||
keys_e = QTextEdit()
|
keys_e = QTextEdit()
|
||||||
keys_e.setTabChangesFocus(True)
|
keys_e.setTabChangesFocus(True)
|
||||||
|
@ -2508,16 +2516,17 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
if not tx:
|
if not tx:
|
||||||
self.show_message(_('No inputs found. (Note that inputs need to be confirmed)'))
|
self.show_message(_('No inputs found. (Note that inputs need to be confirmed)'))
|
||||||
return
|
return
|
||||||
|
self.warn_if_watching_only()
|
||||||
self.show_transaction(tx)
|
self.show_transaction(tx)
|
||||||
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
def do_import_privkey(self, password):
|
def do_import_privkey(self, password):
|
||||||
if not self.wallet.has_imported_keys():
|
if not self.wallet.has_imported_keys():
|
||||||
r = QMessageBox.question(None, _('Warning'), '<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
|
if not self.question('<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
|
||||||
+ _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
|
+ _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '<p>' \
|
||||||
+ _('Are you sure you understand what you are doing?'), 3, 4)
|
+ _('Are you sure you understand what you are doing?'), title=_('Warning')):
|
||||||
if r == 4: return
|
return
|
||||||
|
|
||||||
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
|
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
|
||||||
if not text: return
|
if not text: return
|
||||||
|
@ -2536,18 +2545,16 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
else:
|
else:
|
||||||
addrlist.append(addr)
|
addrlist.append(addr)
|
||||||
if addrlist:
|
if addrlist:
|
||||||
QMessageBox.information(self, _('Information'), _("The following addresses were added") + ':\n' + '\n'.join(addrlist))
|
self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(addrlist))
|
||||||
if badkeys:
|
if badkeys:
|
||||||
QMessageBox.critical(self, _('Error'), _("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
|
self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(badkeys))
|
||||||
self.address_list.update()
|
self.address_list.update()
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
|
|
||||||
|
|
||||||
def settings_dialog(self):
|
def settings_dialog(self):
|
||||||
self.need_restart = False
|
self.need_restart = False
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _('Preferences'))
|
||||||
d.setWindowTitle(_('Preferences'))
|
|
||||||
d.setModal(1)
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
tabs = QTabWidget()
|
tabs = QTabWidget()
|
||||||
gui_widgets = []
|
gui_widgets = []
|
||||||
|
@ -2593,21 +2600,6 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
nz.valueChanged.connect(on_nz)
|
nz.valueChanged.connect(on_nz)
|
||||||
gui_widgets.append((nz_label, nz))
|
gui_widgets.append((nz_label, nz))
|
||||||
|
|
||||||
choosers = sorted(COIN_CHOOSERS.keys())
|
|
||||||
chooser_name = self.wallet.coin_chooser_name(self.config)
|
|
||||||
msg = _('Choose coin (UTXO) selection method. The following are available:\n\n')
|
|
||||||
msg += '\n\n'.join(key + ": " + klass.__doc__
|
|
||||||
for key, klass in COIN_CHOOSERS.items())
|
|
||||||
chooser_label = HelpLabel(_('Coin selection') + ':', msg)
|
|
||||||
chooser_combo = QComboBox()
|
|
||||||
chooser_combo.addItems(choosers)
|
|
||||||
chooser_combo.setCurrentIndex(choosers.index(chooser_name))
|
|
||||||
def on_chooser(x):
|
|
||||||
chooser_name = choosers[chooser_combo.currentIndex()]
|
|
||||||
self.config.set_key('coin_chooser', chooser_name)
|
|
||||||
chooser_combo.currentIndexChanged.connect(on_chooser)
|
|
||||||
tx_widgets.append((chooser_label, chooser_combo))
|
|
||||||
|
|
||||||
msg = _('Fee per kilobyte of transaction.') + '\n' \
|
msg = _('Fee per kilobyte of transaction.') + '\n' \
|
||||||
+ _('If you enable dynamic fees, this parameter will be used as upper bound.')
|
+ _('If you enable dynamic fees, this parameter will be used as upper bound.')
|
||||||
fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg)
|
fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg)
|
||||||
|
@ -2784,6 +2776,24 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
can_edit_fees_cb.setToolTip(_('This option lets you edit fees in the send tab.'))
|
can_edit_fees_cb.setToolTip(_('This option lets you edit fees in the send tab.'))
|
||||||
tx_widgets.append((can_edit_fees_cb, None))
|
tx_widgets.append((can_edit_fees_cb, None))
|
||||||
|
|
||||||
|
def fmt_docs(key, klass):
|
||||||
|
lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")]
|
||||||
|
return '\n'.join([key, "", " ".join(lines)])
|
||||||
|
|
||||||
|
choosers = sorted(COIN_CHOOSERS.keys())
|
||||||
|
chooser_name = self.wallet.coin_chooser_name(self.config)
|
||||||
|
msg = _('Choose coin (UTXO) selection method. The following are available:\n\n')
|
||||||
|
msg += '\n\n'.join(fmt_docs(*item) for item in COIN_CHOOSERS.items())
|
||||||
|
chooser_label = HelpLabel(_('Coin selection') + ':', msg)
|
||||||
|
chooser_combo = QComboBox()
|
||||||
|
chooser_combo.addItems(choosers)
|
||||||
|
chooser_combo.setCurrentIndex(choosers.index(chooser_name))
|
||||||
|
def on_chooser(x):
|
||||||
|
chooser_name = choosers[chooser_combo.currentIndex()]
|
||||||
|
self.config.set_key('coin_chooser', chooser_name)
|
||||||
|
chooser_combo.currentIndexChanged.connect(on_chooser)
|
||||||
|
tx_widgets.append((chooser_label, chooser_combo))
|
||||||
|
|
||||||
tabs_info = [
|
tabs_info = [
|
||||||
(tx_widgets, _('Transactions')),
|
(tx_widgets, _('Transactions')),
|
||||||
(gui_widgets, _('Appearance')),
|
(gui_widgets, _('Appearance')),
|
||||||
|
@ -2813,13 +2823,11 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
run_hook('close_settings_dialog')
|
run_hook('close_settings_dialog')
|
||||||
if self.need_restart:
|
if self.need_restart:
|
||||||
QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK'))
|
self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_network_dialog(self):
|
def run_network_dialog(self):
|
||||||
if not self.network:
|
if not self.network:
|
||||||
QMessageBox.warning(self, _('Offline'), _('You are using Electrum in offline mode.\nRestart Electrum if you want to get connected.'), _('OK'))
|
self.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline'))
|
||||||
return
|
return
|
||||||
NetworkDialog(self.wallet.network, self.config, self).do_exec()
|
NetworkDialog(self.wallet.network, self.config, self).do_exec()
|
||||||
|
|
||||||
|
@ -2839,9 +2847,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
|
|
||||||
|
|
||||||
def plugins_dialog(self):
|
def plugins_dialog(self):
|
||||||
self.pluginsdialog = d = QDialog(self)
|
self.pluginsdialog = d = WindowModalDialog(self, _('Electrum Plugins'))
|
||||||
d.setWindowTitle(_('Electrum Plugins'))
|
|
||||||
d.setModal(1)
|
|
||||||
|
|
||||||
plugins = self.gui_object.plugins
|
plugins = self.gui_object.plugins
|
||||||
|
|
||||||
|
@ -2867,7 +2873,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
def enable_settings_widget(p, name, i):
|
def enable_settings_widget(p, name, i):
|
||||||
widget = settings_widgets.get(name)
|
widget = settings_widgets.get(name)
|
||||||
if not widget and p and p.requires_settings():
|
if not widget and p and p.requires_settings():
|
||||||
widget = settings_widgets[name] = p.settings_widget(self)
|
widget = settings_widgets[name] = p.settings_widget(d)
|
||||||
grid.addWidget(widget, i, 1)
|
grid.addWidget(widget, i, 1)
|
||||||
if widget:
|
if widget:
|
||||||
widget.setEnabled(bool(p and p.is_enabled()))
|
widget.setEnabled(bool(p and p.is_enabled()))
|
||||||
|
@ -2904,9 +2910,7 @@ class ElectrumWindow(QMainWindow, PrintError):
|
||||||
def show_account_details(self, k):
|
def show_account_details(self, k):
|
||||||
account = self.wallet.accounts[k]
|
account = self.wallet.accounts[k]
|
||||||
|
|
||||||
d = QDialog(self)
|
d = WindowModalDialog(self, _('Account Details'))
|
||||||
d.setWindowTitle(_('Account Details'))
|
|
||||||
d.setModal(1)
|
|
||||||
|
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
name = self.wallet.get_account_name(k)
|
name = self.wallet.get_account_name(k)
|
||||||
|
|
|
@ -16,30 +16,21 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import sys, time, datetime, re, threading
|
|
||||||
import os.path, json, ast, traceback
|
|
||||||
|
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import print_error, print_msg
|
|
||||||
from electrum import DEFAULT_PORTS
|
from electrum import DEFAULT_PORTS
|
||||||
from electrum.network import serialize_server, deserialize_server
|
from electrum.network import serialize_server, deserialize_server
|
||||||
|
|
||||||
from util import *
|
from util import *
|
||||||
|
|
||||||
#protocol_names = ['TCP', 'HTTP', 'SSL', 'HTTPS']
|
|
||||||
#protocol_letters = 'thsg'
|
|
||||||
protocol_names = ['TCP', 'SSL']
|
protocol_names = ['TCP', 'SSL']
|
||||||
protocol_letters = 'ts'
|
protocol_letters = 'ts'
|
||||||
|
|
||||||
class NetworkDialog(QDialog):
|
class NetworkDialog(WindowModalDialog):
|
||||||
def __init__(self, network, config, parent):
|
def __init__(self, network, config, parent):
|
||||||
|
WindowModalDialog.__init__(self, parent, _('Network'))
|
||||||
QDialog.__init__(self,parent)
|
|
||||||
self.setModal(1)
|
|
||||||
self.setWindowTitle(_('Network'))
|
|
||||||
self.setMinimumSize(375, 20)
|
self.setMinimumSize(375, 20)
|
||||||
|
|
||||||
self.network = network
|
self.network = network
|
||||||
|
|
|
@ -23,9 +23,27 @@ from util import *
|
||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
def check_password_strength(password):
|
||||||
|
|
||||||
|
'''
|
||||||
|
Check the strength of the password entered by the user and return back the same
|
||||||
|
:param password: password entered by user in New Password
|
||||||
|
:return: password strength Weak or Medium or Strong
|
||||||
|
'''
|
||||||
|
password = unicode(password)
|
||||||
|
n = math.log(len(set(password)))
|
||||||
|
num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None
|
||||||
|
caps = password != password.upper() and password != password.lower()
|
||||||
|
extra = re.match("^[a-zA-Z0-9]*$", password) is None
|
||||||
|
score = len(password)*( n + caps + num + extra)/20
|
||||||
|
password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"}
|
||||||
|
return password_strength[min(3, int(score))]
|
||||||
|
|
||||||
def make_password_dialog(self, wallet, msg, new_pass=True):
|
class PasswordDialog(WindowModalDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, wallet, title, msg, new_pass):
|
||||||
|
WindowModalDialog.__init__(self, parent, title)
|
||||||
|
self.wallet = wallet
|
||||||
|
|
||||||
self.pw = QLineEdit()
|
self.pw = QLineEdit()
|
||||||
self.pw.setEchoMode(2)
|
self.pw.setEchoMode(2)
|
||||||
|
@ -44,8 +62,6 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
|
||||||
grid.setColumnStretch(1,1)
|
grid.setColumnStretch(1,1)
|
||||||
|
|
||||||
logo = QLabel()
|
logo = QLabel()
|
||||||
lockfile = ":icons/lock.png" if wallet and wallet.use_encryption else ":icons/unlock.png"
|
|
||||||
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
|
|
||||||
logo.setAlignment(Qt.AlignCenter)
|
logo.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
grid.addWidget(logo, 0, 0)
|
grid.addWidget(logo, 0, 0)
|
||||||
|
@ -60,6 +76,11 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
|
||||||
if wallet and wallet.use_encryption:
|
if wallet and wallet.use_encryption:
|
||||||
grid.addWidget(QLabel(_('Password')), 0, 0)
|
grid.addWidget(QLabel(_('Password')), 0, 0)
|
||||||
grid.addWidget(self.pw, 0, 1)
|
grid.addWidget(self.pw, 0, 1)
|
||||||
|
lockfile = ":icons/lock.png"
|
||||||
|
else:
|
||||||
|
self.pw = None
|
||||||
|
lockfile = ":icons/unlock.png"
|
||||||
|
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
|
||||||
|
|
||||||
grid.addWidget(QLabel(_('New Password') if new_pass else _('Password')), 1, 0)
|
grid.addWidget(QLabel(_('New Password') if new_pass else _('Password')), 1, 0)
|
||||||
grid.addWidget(self.new_pw, 1, 1)
|
grid.addWidget(self.new_pw, 1, 1)
|
||||||
|
@ -68,108 +89,39 @@ def make_password_dialog(self, wallet, msg, new_pass=True):
|
||||||
grid.addWidget(self.conf_pw, 2, 1)
|
grid.addWidget(self.conf_pw, 2, 1)
|
||||||
vbox.addLayout(grid)
|
vbox.addLayout(grid)
|
||||||
|
|
||||||
#Password Strength Label
|
# Password Strength Label
|
||||||
self.pw_strength = QLabel()
|
self.pw_strength = QLabel()
|
||||||
grid.addWidget(self.pw_strength, 3, 0, 1, 2)
|
grid.addWidget(self.pw_strength, 3, 0, 1, 2)
|
||||||
self.new_pw.textChanged.connect(lambda: update_password_strength(self.pw_strength, self.new_pw.text()))
|
self.new_pw.textChanged.connect(self.pw_changed)
|
||||||
|
self.conf_pw.textChanged.connect(self.check_OKButton)
|
||||||
|
|
||||||
|
self.OKButton = OkButton(self)
|
||||||
vbox.addStretch(1)
|
vbox.addStretch(1)
|
||||||
vbox.addLayout(Buttons(CancelButton(self), OkButton(self)))
|
vbox.addLayout(Buttons(CancelButton(self), self.OKButton))
|
||||||
return vbox
|
self.setLayout(vbox)
|
||||||
|
|
||||||
|
def pw_changed(self):
|
||||||
|
password = self.new_pw.text()
|
||||||
|
if password:
|
||||||
|
colors = {"Weak":"Red", "Medium":"Blue", "Strong":"Green",
|
||||||
|
"Very Strong":"Green"}
|
||||||
|
strength = check_password_strength(password)
|
||||||
|
label = (_("Password Strength") + ": " + "<font color="
|
||||||
|
+ colors[strength] + ">" + strength + "</font>")
|
||||||
|
else:
|
||||||
|
label = ""
|
||||||
|
self.pw_strength.setText(label)
|
||||||
|
self.check_OKButton()
|
||||||
|
|
||||||
def run_password_dialog(self, wallet, parent):
|
def check_OKButton(self):
|
||||||
|
self.OKButton.setEnabled(self.new_pw.text() == self.conf_pw.text())
|
||||||
if wallet and wallet.is_watching_only():
|
|
||||||
QMessageBox.information(parent, _('Error'), _('This is a watching-only wallet'), _('OK'))
|
|
||||||
return False, None, None
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
if not self.exec_():
|
if not self.exec_():
|
||||||
return False, None, None
|
return False, None, None
|
||||||
|
|
||||||
password = unicode(self.pw.text()) if wallet and wallet.use_encryption else None
|
password = unicode(self.pw.text()) if self.pw else None
|
||||||
new_password = unicode(self.new_pw.text())
|
new_password = unicode(self.new_pw.text())
|
||||||
new_password2 = unicode(self.conf_pw.text())
|
new_password2 = unicode(self.conf_pw.text())
|
||||||
|
|
||||||
if new_password != new_password2:
|
return True, password or None, new_password or None
|
||||||
QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
|
|
||||||
# Retry
|
|
||||||
return run_password_dialog(self, wallet, parent)
|
|
||||||
|
|
||||||
if not new_password:
|
|
||||||
new_password = None
|
|
||||||
|
|
||||||
return True, password, new_password
|
|
||||||
|
|
||||||
def check_password_strength(password):
|
|
||||||
|
|
||||||
'''
|
|
||||||
Check the strength of the password entered by the user and return back the same
|
|
||||||
:param password: password entered by user in New Password
|
|
||||||
:return: password strength Weak or Medium or Strong
|
|
||||||
'''
|
|
||||||
password = unicode(password)
|
|
||||||
n = math.log(len(set(password)))
|
|
||||||
num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None
|
|
||||||
caps = password != password.upper() and password != password.lower()
|
|
||||||
extra = re.match("^[a-zA-Z0-9]*$", password) is None
|
|
||||||
score = len(password)*( n + caps + num + extra)/20
|
|
||||||
password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"}
|
|
||||||
return password_strength[min(3, int(score))]
|
|
||||||
|
|
||||||
|
|
||||||
def update_password_strength(pw_strength_label,password):
|
|
||||||
|
|
||||||
'''
|
|
||||||
call the function check_password_strength and update the label pw_strength interactively as the user is typing the password
|
|
||||||
:param pw_strength_label: the label pw_strength
|
|
||||||
:param password: password entered in New Password text box
|
|
||||||
:return: None
|
|
||||||
'''
|
|
||||||
if password:
|
|
||||||
colors = {"Weak":"Red","Medium":"Blue","Strong":"Green", "Very Strong":"Green"}
|
|
||||||
strength = check_password_strength(password)
|
|
||||||
label = _("Password Strength")+ ": "+"<font color=" + colors[strength] + ">" + strength + "</font>"
|
|
||||||
else:
|
|
||||||
label = ""
|
|
||||||
pw_strength_label.setText(label)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordDialog(QDialog):
|
|
||||||
|
|
||||||
def __init__(self, wallet, parent):
|
|
||||||
QDialog.__init__(self, parent)
|
|
||||||
self.setModal(1)
|
|
||||||
self.wallet = wallet
|
|
||||||
self.parent = parent
|
|
||||||
self.setWindowTitle(_("Set Password"))
|
|
||||||
msg = (_('Your wallet is encrypted. Use this dialog to change your password.') + ' '\
|
|
||||||
+_('To disable wallet encryption, enter an empty new password.')) \
|
|
||||||
if wallet.use_encryption else _('Your wallet keys are not encrypted')
|
|
||||||
self.setLayout(make_password_dialog(self, wallet, msg))
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
ok, password, new_password = run_password_dialog(self, self.wallet, self.parent)
|
|
||||||
if not ok:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.wallet.check_password(password)
|
|
||||||
except BaseException as e:
|
|
||||||
QMessageBox.warning(self.parent, _('Error'), str(e), _('OK'))
|
|
||||||
return False, None, None
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.wallet.update_password(password, new_password)
|
|
||||||
except:
|
|
||||||
import traceback, sys
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
|
||||||
QMessageBox.warning(self.parent, _('Error'), _('Failed to update password'), _('OK'))
|
|
||||||
return
|
|
||||||
|
|
||||||
if new_password:
|
|
||||||
QMessageBox.information(self.parent, _('Success'), _('Password was updated successfully'), _('OK'))
|
|
||||||
else:
|
|
||||||
QMessageBox.information(self.parent, _('Success'), _('This wallet is not encrypted'), _('OK'))
|
|
||||||
|
|
|
@ -160,16 +160,8 @@ class PayToEdit(ScanQRTextEdit):
|
||||||
return len(self.lines()) > 1
|
return len(self.lines()) > 1
|
||||||
|
|
||||||
def paytomany(self):
|
def paytomany(self):
|
||||||
from electrum.i18n import _
|
|
||||||
self.setText("\n\n\n")
|
self.setText("\n\n\n")
|
||||||
self.update_size()
|
self.update_size()
|
||||||
msg = '\n'.join([
|
|
||||||
_('Enter a list of outputs in the \'Pay to\' field.'),
|
|
||||||
_('One output per line.'),
|
|
||||||
_('Format: address, amount.'),
|
|
||||||
_('You may load a CSV file using the file icon.')
|
|
||||||
])
|
|
||||||
QMessageBox.warning(self, _('Pay to many'), msg, _('OK'))
|
|
||||||
|
|
||||||
def update_size(self):
|
def update_size(self):
|
||||||
docHeight = self.document().size().height()
|
docHeight = self.document().size().height()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
import PyQt4.QtCore as QtCore
|
|
||||||
import PyQt4.QtGui as QtGui
|
import PyQt4.QtGui as QtGui
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -9,6 +8,7 @@ import qrcode
|
||||||
import electrum
|
import electrum
|
||||||
from electrum import bmp
|
from electrum import bmp
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
|
from util import WindowModalDialog, MessageBoxMixin
|
||||||
|
|
||||||
|
|
||||||
class QRCodeWidget(QWidget):
|
class QRCodeWidget(QWidget):
|
||||||
|
@ -83,13 +83,11 @@ class QRCodeWidget(QWidget):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class QRDialog(QDialog):
|
class QRDialog(WindowModalDialog, MessageBoxMixin):
|
||||||
|
|
||||||
def __init__(self, data, parent=None, title = "", show_text=False):
|
def __init__(self, data, parent=None, title = "", show_text=False):
|
||||||
QDialog.__init__(self, parent)
|
WindowModalDialog.__init__(self, parent, title)
|
||||||
|
|
||||||
d = self
|
|
||||||
d.setWindowTitle(title)
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
qrw = QRCodeWidget(data)
|
qrw = QRCodeWidget(data)
|
||||||
vbox.addWidget(qrw, 1)
|
vbox.addWidget(qrw, 1)
|
||||||
|
@ -107,12 +105,12 @@ class QRDialog(QDialog):
|
||||||
|
|
||||||
def print_qr():
|
def print_qr():
|
||||||
bmp.save_qrcode(qrw.qr, filename)
|
bmp.save_qrcode(qrw.qr, filename)
|
||||||
QMessageBox.information(None, _('Message'), _("QR code saved to file") + " " + filename, _('OK'))
|
self.show_message(_("QR code saved to file") + " " + filename)
|
||||||
|
|
||||||
def copy_to_clipboard():
|
def copy_to_clipboard():
|
||||||
bmp.save_qrcode(qrw.qr, filename)
|
bmp.save_qrcode(qrw.qr, filename)
|
||||||
QApplication.clipboard().setImage(QImage(filename))
|
QApplication.clipboard().setImage(QImage(filename))
|
||||||
QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK'))
|
self.show_message(_("QR code copied to clipboard"))
|
||||||
|
|
||||||
b = QPushButton(_("Copy"))
|
b = QPushButton(_("Copy"))
|
||||||
hbox.addWidget(b)
|
hbox.addWidget(b)
|
||||||
|
@ -124,8 +122,8 @@ class QRDialog(QDialog):
|
||||||
|
|
||||||
b = QPushButton(_("Close"))
|
b = QPushButton(_("Close"))
|
||||||
hbox.addWidget(b)
|
hbox.addWidget(b)
|
||||||
b.clicked.connect(d.accept)
|
b.clicked.connect(self.accept)
|
||||||
b.setDefault(True)
|
b.setDefault(True)
|
||||||
|
|
||||||
vbox.addLayout(hbox)
|
vbox.addLayout(hbox)
|
||||||
d.setLayout(vbox)
|
self.setLayout(vbox)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from electrum.plugins import run_hook
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
|
|
||||||
from util import ButtonsTextEdit
|
from util import ButtonsTextEdit, MessageBoxMixin
|
||||||
|
|
||||||
|
|
||||||
class ShowQRTextEdit(ButtonsTextEdit):
|
class ShowQRTextEdit(ButtonsTextEdit):
|
||||||
|
@ -29,7 +29,7 @@ class ShowQRTextEdit(ButtonsTextEdit):
|
||||||
m.exec_(e.globalPos())
|
m.exec_(e.globalPos())
|
||||||
|
|
||||||
|
|
||||||
class ScanQRTextEdit(ButtonsTextEdit):
|
class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
|
||||||
|
|
||||||
def __init__(self, text=""):
|
def __init__(self, text=""):
|
||||||
ButtonsTextEdit.__init__(self, text)
|
ButtonsTextEdit.__init__(self, text)
|
||||||
|
@ -50,8 +50,8 @@ class ScanQRTextEdit(ButtonsTextEdit):
|
||||||
from electrum import qrscanner, get_config
|
from electrum import qrscanner, get_config
|
||||||
try:
|
try:
|
||||||
data = qrscanner.scan_qr(get_config())
|
data = qrscanner.scan_qr(get_config())
|
||||||
except BaseException, e:
|
except BaseException as e:
|
||||||
QMessageBox.warning(self, _('Error'), _(e), _('OK'))
|
self.show_error(str(e))
|
||||||
return ""
|
return ""
|
||||||
if type(data) != str:
|
if type(data) != str:
|
||||||
return
|
return
|
||||||
|
|
|
@ -20,17 +20,14 @@ from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
import PyQt4.QtCore as QtCore
|
import PyQt4.QtCore as QtCore
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum import mnemonic
|
|
||||||
|
|
||||||
from util import *
|
from util import *
|
||||||
from qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
from qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
||||||
|
|
||||||
class SeedDialog(QDialog):
|
class SeedDialog(WindowModalDialog):
|
||||||
def __init__(self, parent, seed, imported_keys):
|
def __init__(self, parent, seed, imported_keys):
|
||||||
QDialog.__init__(self, parent)
|
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
|
||||||
self.setModal(1)
|
|
||||||
self.setMinimumWidth(400)
|
self.setMinimumWidth(400)
|
||||||
self.setWindowTitle('Electrum' + ' - ' + _('Seed'))
|
|
||||||
vbox = show_seed_box_msg(seed)
|
vbox = show_seed_box_msg(seed)
|
||||||
if imported_keys:
|
if imported_keys:
|
||||||
vbox.addWidget(QLabel("<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"))
|
vbox.addWidget(QLabel("<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"))
|
||||||
|
|
|
@ -38,7 +38,7 @@ def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
|
||||||
dialogs.append(d)
|
dialogs.append(d)
|
||||||
d.show()
|
d.show()
|
||||||
|
|
||||||
class TxDialog(QDialog):
|
class TxDialog(QDialog, MessageBoxMixin):
|
||||||
|
|
||||||
def __init__(self, tx, parent, desc, prompt_if_unsaved):
|
def __init__(self, tx, parent, desc, prompt_if_unsaved):
|
||||||
'''Transactions in the wallet will show their description.
|
'''Transactions in the wallet will show their description.
|
||||||
|
@ -54,7 +54,7 @@ class TxDialog(QDialog):
|
||||||
self.desc = desc
|
self.desc = desc
|
||||||
|
|
||||||
QDialog.__init__(self)
|
QDialog.__init__(self)
|
||||||
self.setMinimumWidth(600)
|
self.setMinimumWidth(660)
|
||||||
self.setWindowTitle(_("Transaction"))
|
self.setWindowTitle(_("Transaction"))
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
|
@ -62,7 +62,7 @@ class TxDialog(QDialog):
|
||||||
|
|
||||||
vbox.addWidget(QLabel(_("Transaction ID:")))
|
vbox.addWidget(QLabel(_("Transaction ID:")))
|
||||||
self.tx_hash_e = ButtonsLineEdit()
|
self.tx_hash_e = ButtonsLineEdit()
|
||||||
qr_show = lambda: self.parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID')
|
qr_show = lambda: self.parent.show_qrcode(str(self.tx_hash_e.text()), 'Transaction ID', parent=self)
|
||||||
self.tx_hash_e.addButton(":icons/qrcode.png", qr_show, _("Show as QR code"))
|
self.tx_hash_e.addButton(":icons/qrcode.png", qr_show, _("Show as QR code"))
|
||||||
self.tx_hash_e.setReadOnly(True)
|
self.tx_hash_e.setReadOnly(True)
|
||||||
vbox.addWidget(self.tx_hash_e)
|
vbox.addWidget(self.tx_hash_e)
|
||||||
|
@ -122,10 +122,7 @@ class TxDialog(QDialog):
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
if (self.prompt_if_unsaved and not self.saved and not self.broadcast
|
if (self.prompt_if_unsaved and not self.saved and not self.broadcast
|
||||||
and QMessageBox.question(
|
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
|
||||||
self, _('Warning'),
|
|
||||||
_('This transaction is not saved. Close anyway?'),
|
|
||||||
QMessageBox.Yes | QMessageBox.No) == QMessageBox.No):
|
|
||||||
event.ignore()
|
event.ignore()
|
||||||
else:
|
else:
|
||||||
event.accept()
|
event.accept()
|
||||||
|
@ -135,7 +132,7 @@ class TxDialog(QDialog):
|
||||||
text = self.tx.raw.decode('hex')
|
text = self.tx.raw.decode('hex')
|
||||||
text = base_encode(text, base=43)
|
text = base_encode(text, base=43)
|
||||||
try:
|
try:
|
||||||
self.parent.show_qrcode(text, 'Transaction')
|
self.parent.show_qrcode(text, 'Transaction', parent=self)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.show_message(str(e))
|
self.show_message(str(e))
|
||||||
|
|
||||||
|
@ -173,7 +170,7 @@ class TxDialog(QDialog):
|
||||||
status = _("Signed")
|
status = _("Signed")
|
||||||
|
|
||||||
if tx_hash in self.wallet.transactions.keys():
|
if tx_hash in self.wallet.transactions.keys():
|
||||||
desc, is_default = self.wallet.get_label(tx_hash)
|
desc = self.wallet.get_label(tx_hash)
|
||||||
conf, timestamp = self.wallet.get_confirmations(tx_hash)
|
conf, timestamp = self.wallet.get_confirmations(tx_hash)
|
||||||
if timestamp:
|
if timestamp:
|
||||||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||||
|
@ -249,6 +246,9 @@ class TxDialog(QDialog):
|
||||||
return chg if self.wallet.is_change(addr) else rec
|
return chg if self.wallet.is_change(addr) else rec
|
||||||
return ext
|
return ext
|
||||||
|
|
||||||
|
def format_amount(amt):
|
||||||
|
return self.parent.format_amount(amt, whitespaces = True)
|
||||||
|
|
||||||
i_text = QTextEdit()
|
i_text = QTextEdit()
|
||||||
i_text.setFont(QFont(MONOSPACE_FONT))
|
i_text.setFont(QFont(MONOSPACE_FONT))
|
||||||
i_text.setReadOnly(True)
|
i_text.setReadOnly(True)
|
||||||
|
@ -270,6 +270,8 @@ class TxDialog(QDialog):
|
||||||
if addr is None:
|
if addr is None:
|
||||||
addr = _('unknown')
|
addr = _('unknown')
|
||||||
cursor.insertText(addr, text_format(addr))
|
cursor.insertText(addr, text_format(addr))
|
||||||
|
if x.get('value'):
|
||||||
|
cursor.insertText(format_amount(x['value']), ext)
|
||||||
cursor.insertBlock()
|
cursor.insertBlock()
|
||||||
|
|
||||||
vbox.addWidget(i_text)
|
vbox.addWidget(i_text)
|
||||||
|
@ -283,11 +285,6 @@ class TxDialog(QDialog):
|
||||||
cursor.insertText(addr, text_format(addr))
|
cursor.insertText(addr, text_format(addr))
|
||||||
if v is not None:
|
if v is not None:
|
||||||
cursor.insertText('\t', ext)
|
cursor.insertText('\t', ext)
|
||||||
cursor.insertText(self.parent.format_amount(v, whitespaces = True), ext)
|
cursor.insertText(format_amount(v), ext)
|
||||||
cursor.insertBlock()
|
cursor.insertBlock()
|
||||||
vbox.addWidget(o_text)
|
vbox.addWidget(o_text)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def show_message(self, msg):
|
|
||||||
QMessageBox.information(self, _('Message'), msg, _('OK'))
|
|
||||||
|
|
144
gui/qt/util.py
144
gui/qt/util.py
|
@ -21,51 +21,19 @@ RED_FG = "QWidget {color:red;}"
|
||||||
BLUE_FG = "QWidget {color:blue;}"
|
BLUE_FG = "QWidget {color:blue;}"
|
||||||
BLACK_FG = "QWidget {color:black;}"
|
BLACK_FG = "QWidget {color:black;}"
|
||||||
|
|
||||||
|
dialogs = []
|
||||||
class WaitingDialog(QThread):
|
|
||||||
def __init__(self, parent, message, run_task, on_success=None, on_complete=None):
|
|
||||||
QThread.__init__(self)
|
|
||||||
self.parent = parent
|
|
||||||
self.d = QDialog(parent)
|
|
||||||
self.d.setWindowTitle('Please wait')
|
|
||||||
l = QLabel(message)
|
|
||||||
vbox = QVBoxLayout(self.d)
|
|
||||||
vbox.addWidget(l)
|
|
||||||
self.run_task = run_task
|
|
||||||
self.on_success = on_success
|
|
||||||
self.on_complete = on_complete
|
|
||||||
self.d.connect(self.d, SIGNAL('done'), self.close)
|
|
||||||
self.d.show()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.error = None
|
|
||||||
try:
|
|
||||||
self.result = self.run_task()
|
|
||||||
except BaseException as e:
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
|
||||||
self.error = str(e)
|
|
||||||
self.d.emit(SIGNAL('done'))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.d.accept()
|
|
||||||
if self.error:
|
|
||||||
QMessageBox.warning(self.parent, _('Error'), self.error, _('OK'))
|
|
||||||
else:
|
|
||||||
if self.on_success:
|
|
||||||
if type(self.result) is not tuple:
|
|
||||||
self.result = (self.result,)
|
|
||||||
self.on_success(*self.result)
|
|
||||||
|
|
||||||
if self.on_complete:
|
|
||||||
self.on_complete()
|
|
||||||
|
|
||||||
|
|
||||||
class Timer(QThread):
|
class Timer(QThread):
|
||||||
|
stopped = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while not self.stopped:
|
||||||
self.emit(SIGNAL('timersignal'))
|
self.emit(SIGNAL('timersignal'))
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stopped = True
|
||||||
|
self.wait()
|
||||||
|
|
||||||
class EnterButton(QPushButton):
|
class EnterButton(QPushButton):
|
||||||
def __init__(self, text, func):
|
def __init__(self, text, func):
|
||||||
|
@ -187,12 +155,91 @@ class CancelButton(QPushButton):
|
||||||
QPushButton.__init__(self, label or _("Cancel"))
|
QPushButton.__init__(self, label or _("Cancel"))
|
||||||
self.clicked.connect(dialog.reject)
|
self.clicked.connect(dialog.reject)
|
||||||
|
|
||||||
|
class MessageBoxMixin:
|
||||||
|
def question(self, msg, parent=None, title=None):
|
||||||
|
Yes, No = QMessageBox.Yes, QMessageBox.No
|
||||||
|
return self.msg_box(QMessageBox.Question, parent or self, title or '',
|
||||||
|
msg, buttons=Yes|No, defaultButton=No) == Yes
|
||||||
|
|
||||||
|
def show_warning(self, msg, parent=None, title=None):
|
||||||
|
return self.msg_box(QMessageBox.Warning, parent or self,
|
||||||
|
title or _('Warning'), msg)
|
||||||
|
|
||||||
|
def show_error(self, msg, parent=None):
|
||||||
|
return self.msg_box(QMessageBox.Warning, parent or self,
|
||||||
|
_('Error'), msg)
|
||||||
|
|
||||||
|
def show_critical(self, msg, parent=None, title=None):
|
||||||
|
return self.msg_box(QMessageBox.Critical, parent or self,
|
||||||
|
title or _('Critical Error'), msg)
|
||||||
|
|
||||||
|
def show_message(self, msg, parent=None, title=None):
|
||||||
|
return self.msg_box(QMessageBox.Information, parent or self,
|
||||||
|
title or _('Information'), msg)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def msg_box(icon, parent, title, text, buttons=QMessageBox.Ok,
|
||||||
|
defaultButton=QMessageBox.NoButton):
|
||||||
|
# handle e.g. ElectrumGui
|
||||||
|
if not isinstance(parent, QWidget):
|
||||||
|
parent = None
|
||||||
|
d = QMessageBox(icon, title, text, buttons, parent)
|
||||||
|
d.setWindowModality(Qt.WindowModal)
|
||||||
|
d.setDefaultButton(defaultButton)
|
||||||
|
return d.exec_()
|
||||||
|
|
||||||
|
class WindowModalDialog(QDialog):
|
||||||
|
'''Handy wrapper; window modal dialogs are better for our multi-window
|
||||||
|
daemon model as other wallet windows can still be accessed.'''
|
||||||
|
def __init__(self, parent, title=None):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setWindowModality(Qt.WindowModal)
|
||||||
|
if title:
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
|
||||||
|
class WaitingDialog(QThread, MessageBoxMixin):
|
||||||
|
'''Shows a please wait dialog whilst runnning a task. It is not
|
||||||
|
necessary to maintain a reference to this dialog.'''
|
||||||
|
def __init__(self, parent, message, task, on_success=None,
|
||||||
|
on_finished=None):
|
||||||
|
global dialogs
|
||||||
|
dialogs.append(self) # Prevent GC
|
||||||
|
QThread.__init__(self)
|
||||||
|
self.task = task
|
||||||
|
self.on_success = on_success
|
||||||
|
self.on_finished = on_finished
|
||||||
|
self.dialog = WindowModalDialog(parent, _("Please wait"))
|
||||||
|
vbox = QVBoxLayout(self.dialog)
|
||||||
|
vbox.addWidget(QLabel(message))
|
||||||
|
self.dialog.show()
|
||||||
|
self.dialog.connect(self, SIGNAL("finished()"), self.finished)
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.error = None
|
||||||
|
try:
|
||||||
|
self.result = self.task()
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
self.error = str(e)
|
||||||
|
|
||||||
|
def finished(self):
|
||||||
|
global dialogs
|
||||||
|
dialogs.remove(self)
|
||||||
|
if self.error:
|
||||||
|
self.show_error(self.error, parent=self.dialog.parent())
|
||||||
|
elif self.on_success:
|
||||||
|
result = self.result
|
||||||
|
if type(result) is not tuple:
|
||||||
|
result = (result,)
|
||||||
|
self.on_success(*result)
|
||||||
|
if self.on_finished:
|
||||||
|
self.on_finished()
|
||||||
|
self.dialog.accept()
|
||||||
|
|
||||||
def line_dialog(parent, title, label, ok_label, default=None):
|
def line_dialog(parent, title, label, ok_label, default=None):
|
||||||
dialog = QDialog(parent)
|
dialog = WindowModalDialog(parent, title)
|
||||||
dialog.setMinimumWidth(500)
|
dialog.setMinimumWidth(500)
|
||||||
dialog.setWindowTitle(title)
|
|
||||||
dialog.setModal(1)
|
|
||||||
l = QVBoxLayout()
|
l = QVBoxLayout()
|
||||||
dialog.setLayout(l)
|
dialog.setLayout(l)
|
||||||
l.addWidget(QLabel(label))
|
l.addWidget(QLabel(label))
|
||||||
|
@ -206,10 +253,8 @@ def line_dialog(parent, title, label, ok_label, default=None):
|
||||||
|
|
||||||
def text_dialog(parent, title, label, ok_label, default=None):
|
def text_dialog(parent, title, label, ok_label, default=None):
|
||||||
from qrtextedit import ScanQRTextEdit
|
from qrtextedit import ScanQRTextEdit
|
||||||
dialog = QDialog(parent)
|
dialog = WindowModalDialog(parent, title)
|
||||||
dialog.setMinimumWidth(500)
|
dialog.setMinimumWidth(500)
|
||||||
dialog.setWindowTitle(title)
|
|
||||||
dialog.setModal(1)
|
|
||||||
l = QVBoxLayout()
|
l = QVBoxLayout()
|
||||||
dialog.setLayout(l)
|
dialog.setLayout(l)
|
||||||
l.addWidget(QLabel(label))
|
l.addWidget(QLabel(label))
|
||||||
|
@ -221,9 +266,6 @@ def text_dialog(parent, title, label, ok_label, default=None):
|
||||||
if dialog.exec_():
|
if dialog.exec_():
|
||||||
return unicode(txt.toPlainText())
|
return unicode(txt.toPlainText())
|
||||||
|
|
||||||
def question(msg):
|
|
||||||
return QMessageBox.question(None, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes
|
|
||||||
|
|
||||||
def address_field(addresses):
|
def address_field(addresses):
|
||||||
hbox = QHBoxLayout()
|
hbox = QHBoxLayout()
|
||||||
address_e = QLineEdit()
|
address_e = QLineEdit()
|
||||||
|
@ -383,12 +425,6 @@ class MyTreeWidget(QTreeWidget):
|
||||||
key = str(item.data(0, Qt.UserRole).toString())
|
key = str(item.data(0, Qt.UserRole).toString())
|
||||||
text = unicode(item.text(column))
|
text = unicode(item.text(column))
|
||||||
self.parent.wallet.set_label(key, text)
|
self.parent.wallet.set_label(key, text)
|
||||||
if text:
|
|
||||||
item.setForeground(column, QBrush(QColor('black')))
|
|
||||||
else:
|
|
||||||
text = self.parent.wallet.get_default_label(key)
|
|
||||||
item.setText(column, text)
|
|
||||||
item.setForeground(column, QBrush(QColor('gray')))
|
|
||||||
self.parent.history_list.update()
|
self.parent.history_list.update()
|
||||||
self.parent.update_completions()
|
self.parent.update_completions()
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ class ElectrumGui:
|
||||||
else:
|
else:
|
||||||
time_str = 'pending'
|
time_str = 'pending'
|
||||||
|
|
||||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
label = self.wallet.get_label(tx_hash)
|
||||||
messages.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
|
messages.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
|
||||||
|
|
||||||
self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
|
self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
|
||||||
|
|
|
@ -116,7 +116,7 @@ class ElectrumGui:
|
||||||
else:
|
else:
|
||||||
time_str = 'pending'
|
time_str = 'pending'
|
||||||
|
|
||||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
label = self.wallet.get_label(tx_hash)
|
||||||
if len(label) > 40:
|
if len(label) > 40:
|
||||||
label = label[0:37] + '...'
|
label = label[0:37] + '...'
|
||||||
self.history.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
|
self.history.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
|
||||||
|
|
|
@ -21,13 +21,14 @@ import os
|
||||||
import util
|
import util
|
||||||
from bitcoin import *
|
from bitcoin import *
|
||||||
|
|
||||||
|
MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
|
||||||
|
|
||||||
class Blockchain(util.PrintError):
|
class Blockchain(util.PrintError):
|
||||||
'''Manages blockchain headers and their verification'''
|
'''Manages blockchain headers and their verification'''
|
||||||
def __init__(self, config, network):
|
def __init__(self, config, network):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.network = network
|
self.network = network
|
||||||
self.headers_url = 'https://headers.electrum.org/blockchain_headers'
|
self.headers_url = "https://headers.electrum.org/blockchain_headers"
|
||||||
self.local_height = 0
|
self.local_height = 0
|
||||||
self.set_local_height()
|
self.set_local_height()
|
||||||
|
|
||||||
|
@ -39,76 +40,44 @@ class Blockchain(util.PrintError):
|
||||||
self.set_local_height()
|
self.set_local_height()
|
||||||
self.print_error("%d blocks" % self.local_height)
|
self.print_error("%d blocks" % self.local_height)
|
||||||
|
|
||||||
|
def verify_header(self, header, prev_header, bits, target):
|
||||||
|
prev_hash = self.hash_header(prev_header)
|
||||||
|
assert prev_hash == header.get('prev_block_hash'), "prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))
|
||||||
|
assert bits == header.get('bits'), "bits mismatch: %s vs %s" % (bits, header.get('bits'))
|
||||||
|
_hash = self.hash_header(header)
|
||||||
|
assert int('0x' + _hash, 16) <= target, "insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target)
|
||||||
|
|
||||||
def verify_chain(self, chain):
|
def verify_chain(self, chain):
|
||||||
first_header = chain[0]
|
first_header = chain[0]
|
||||||
prev_header = self.read_header(first_header.get('block_height') -1)
|
prev_header = self.read_header(first_header.get('block_height') - 1)
|
||||||
|
|
||||||
for header in chain:
|
for header in chain:
|
||||||
height = header.get('block_height')
|
height = header.get('block_height')
|
||||||
prev_hash = self.hash_header(prev_header)
|
bits, target = self.get_target(height / 2016, chain)
|
||||||
if prev_hash != header.get('prev_block_hash'):
|
self.verify_header(header, prev_header, bits, target)
|
||||||
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
|
|
||||||
|
|
||||||
prev_header = header
|
prev_header = header
|
||||||
|
|
||||||
return True
|
def verify_chunk(self, index, data):
|
||||||
|
num = len(data) / 80
|
||||||
|
prev_header = None
|
||||||
|
if index != 0:
|
||||||
def verify_chunk(self, index, hexdata):
|
prev_header = self.read_header(index*2016 - 1)
|
||||||
data = hexdata.decode('hex')
|
|
||||||
height = index*2016
|
|
||||||
num = len(data)/80
|
|
||||||
|
|
||||||
if index == 0:
|
|
||||||
previous_hash = ("0"*64)
|
|
||||||
else:
|
|
||||||
prev_header = self.read_header(index*2016-1)
|
|
||||||
if prev_header is None: raise
|
|
||||||
previous_hash = self.hash_header(prev_header)
|
|
||||||
|
|
||||||
bits, target = self.get_target(index)
|
bits, target = self.get_target(index)
|
||||||
|
|
||||||
for i in range(num):
|
for i in range(num):
|
||||||
height = index*2016 + i
|
raw_header = data[i*80:(i+1) * 80]
|
||||||
raw_header = data[i*80:(i+1)*80]
|
header = self.deserialize_header(raw_header)
|
||||||
header = self.header_from_string(raw_header)
|
self.verify_header(header, prev_header, bits, target)
|
||||||
_hash = self.hash_header(header)
|
prev_header = header
|
||||||
assert previous_hash == header.get('prev_block_hash')
|
|
||||||
assert bits == header.get('bits')
|
|
||||||
assert int('0x'+_hash,16) < target
|
|
||||||
|
|
||||||
previous_header = header
|
def serialize_header(self, res):
|
||||||
previous_hash = _hash
|
s = int_to_hex(res.get('version'), 4) \
|
||||||
|
|
||||||
self.save_chunk(index, data)
|
|
||||||
self.print_error("validated chunk %d to height %d" % (index, height))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def header_to_string(self, res):
|
|
||||||
s = int_to_hex(res.get('version'),4) \
|
|
||||||
+ rev_hex(res.get('prev_block_hash')) \
|
+ rev_hex(res.get('prev_block_hash')) \
|
||||||
+ rev_hex(res.get('merkle_root')) \
|
+ rev_hex(res.get('merkle_root')) \
|
||||||
+ int_to_hex(int(res.get('timestamp')),4) \
|
+ int_to_hex(int(res.get('timestamp')), 4) \
|
||||||
+ int_to_hex(int(res.get('bits')),4) \
|
+ int_to_hex(int(res.get('bits')), 4) \
|
||||||
+ int_to_hex(int(res.get('nonce')),4)
|
+ int_to_hex(int(res.get('nonce')), 4)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
def deserialize_header(self, s):
|
||||||
def header_from_string(self, s):
|
|
||||||
hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16)
|
hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16)
|
||||||
h = {}
|
h = {}
|
||||||
h['version'] = hex_to_int(s[0:4])
|
h['version'] = hex_to_int(s[0:4])
|
||||||
|
@ -120,7 +89,9 @@ class Blockchain(util.PrintError):
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def hash_header(self, header):
|
def hash_header(self, header):
|
||||||
return rev_hex(Hash(self.header_to_string(header).decode('hex')).encode('hex'))
|
if header is None:
|
||||||
|
return '0' * 64
|
||||||
|
return hash_encode(Hash(self.serialize_header(header).decode('hex')))
|
||||||
|
|
||||||
def path(self):
|
def path(self):
|
||||||
return os.path.join(self.config.path, 'blockchain_headers')
|
return os.path.join(self.config.path, 'blockchain_headers')
|
||||||
|
@ -132,28 +103,28 @@ class Blockchain(util.PrintError):
|
||||||
try:
|
try:
|
||||||
import urllib, socket
|
import urllib, socket
|
||||||
socket.setdefaulttimeout(30)
|
socket.setdefaulttimeout(30)
|
||||||
self.print_error("downloading ", self.headers_url )
|
self.print_error("downloading ", self.headers_url)
|
||||||
urllib.urlretrieve(self.headers_url, filename)
|
urllib.urlretrieve(self.headers_url, filename)
|
||||||
self.print_error("done.")
|
self.print_error("done.")
|
||||||
except Exception:
|
except Exception:
|
||||||
self.print_error( "download failed. creating file", filename )
|
self.print_error("download failed. creating file", filename)
|
||||||
open(filename,'wb+').close()
|
open(filename, 'wb+').close()
|
||||||
|
|
||||||
def save_chunk(self, index, chunk):
|
def save_chunk(self, index, chunk):
|
||||||
filename = self.path()
|
filename = self.path()
|
||||||
f = open(filename,'rb+')
|
f = open(filename, 'rb+')
|
||||||
f.seek(index*2016*80)
|
f.seek(index * 2016 * 80)
|
||||||
h = f.write(chunk)
|
h = f.write(chunk)
|
||||||
f.close()
|
f.close()
|
||||||
self.set_local_height()
|
self.set_local_height()
|
||||||
|
|
||||||
def save_header(self, header):
|
def save_header(self, header):
|
||||||
data = self.header_to_string(header).decode('hex')
|
data = self.serialize_header(header).decode('hex')
|
||||||
assert len(data) == 80
|
assert len(data) == 80
|
||||||
height = header.get('block_height')
|
height = header.get('block_height')
|
||||||
filename = self.path()
|
filename = self.path()
|
||||||
f = open(filename,'rb+')
|
f = open(filename, 'rb+')
|
||||||
f.seek(height*80)
|
f.seek(height * 80)
|
||||||
h = f.write(data)
|
h = f.write(data)
|
||||||
f.close()
|
f.close()
|
||||||
self.set_local_height()
|
self.set_local_height()
|
||||||
|
@ -168,58 +139,47 @@ class Blockchain(util.PrintError):
|
||||||
def read_header(self, block_height):
|
def read_header(self, block_height):
|
||||||
name = self.path()
|
name = self.path()
|
||||||
if os.path.exists(name):
|
if os.path.exists(name):
|
||||||
f = open(name,'rb')
|
f = open(name, 'rb')
|
||||||
f.seek(block_height*80)
|
f.seek(block_height * 80)
|
||||||
h = f.read(80)
|
h = f.read(80)
|
||||||
f.close()
|
f.close()
|
||||||
if len(h) == 80:
|
if len(h) == 80:
|
||||||
h = self.header_from_string(h)
|
h = self.deserialize_header(h)
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def get_target(self, index, chain=None):
|
def get_target(self, index, chain=None):
|
||||||
if chain is None:
|
if index == 0:
|
||||||
chain = [] # Do not use mutables as default values!
|
return 0x1d00ffff, MAX_TARGET
|
||||||
|
first = self.read_header((index-1) * 2016)
|
||||||
max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
|
last = self.read_header(index*2016 - 1)
|
||||||
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:
|
if last is None:
|
||||||
for h in chain:
|
for h in chain:
|
||||||
if h.get('block_height') == index*2016-1:
|
if h.get('block_height') == index*2016 - 1:
|
||||||
last = h
|
last = h
|
||||||
|
assert last is not None
|
||||||
nActualTimespan = last.get('timestamp') - first.get('timestamp')
|
# bits to target
|
||||||
nTargetTimespan = 14*24*60*60
|
|
||||||
nActualTimespan = max(nActualTimespan, nTargetTimespan/4)
|
|
||||||
nActualTimespan = min(nActualTimespan, nTargetTimespan*4)
|
|
||||||
|
|
||||||
bits = last.get('bits')
|
bits = last.get('bits')
|
||||||
# convert to bignum
|
bitsN = (bits >> 24) & 0xff
|
||||||
MM = 256*256*256
|
assert bitsN >= 0x03 and bitsN <= 0x1d, "First part of bits should be in [0x03, 0x1d]"
|
||||||
a = bits%MM
|
bitsBase = bits & 0xffffff
|
||||||
if a < 0x8000:
|
assert bitsBase >= 0x8000 and bitsBase <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]"
|
||||||
a *= 256
|
target = bitsBase << (8 * (bitsN-3))
|
||||||
target = (a) * pow(2, 8 * (bits/MM - 3))
|
|
||||||
|
|
||||||
# new target
|
# new target
|
||||||
new_target = min( max_target, (target * nActualTimespan)/nTargetTimespan )
|
nActualTimespan = last.get('timestamp') - first.get('timestamp')
|
||||||
|
nTargetTimespan = 14 * 24 * 60 * 60
|
||||||
# convert it to bits
|
nActualTimespan = max(nActualTimespan, nTargetTimespan / 4)
|
||||||
c = ("%064X"%new_target)[2:]
|
nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
|
||||||
i = 31
|
new_target = min(MAX_TARGET, (target*nActualTimespan) / nTargetTimespan)
|
||||||
while c[0:2]=="00":
|
# convert new target to bits
|
||||||
|
c = ("%064x" % new_target)[2:]
|
||||||
|
while c[:2] == '00' and len(c) > 6:
|
||||||
c = c[2:]
|
c = c[2:]
|
||||||
i -= 1
|
bitsN, bitsBase = len(c) / 2, int('0x' + c[:6], 16)
|
||||||
|
if bitsBase >= 0x800000:
|
||||||
c = int('0x'+c[0:6],16)
|
bitsN += 1
|
||||||
if c >= 0x800000:
|
bitsBase >>= 8
|
||||||
c /= 256
|
new_bits = bitsN << 24 | bitsBase
|
||||||
i += 1
|
return new_bits, bitsBase << (8 * (bitsN-3))
|
||||||
|
|
||||||
new_bits = c + MM * i
|
|
||||||
return new_bits, new_target
|
|
||||||
|
|
||||||
def connect_header(self, chain, header):
|
def connect_header(self, chain, header):
|
||||||
'''Builds a header chain until it connects. Returns True if it has
|
'''Builds a header chain until it connects. Returns True if it has
|
||||||
|
@ -241,18 +201,23 @@ class Blockchain(util.PrintError):
|
||||||
|
|
||||||
# The chain is complete. Reverse to order by increasing height
|
# The chain is complete. Reverse to order by increasing height
|
||||||
chain.reverse()
|
chain.reverse()
|
||||||
if self.verify_chain(chain):
|
try:
|
||||||
|
self.verify_chain(chain)
|
||||||
self.print_error("connected at height:", previous_height)
|
self.print_error("connected at height:", previous_height)
|
||||||
for header in chain:
|
for header in chain:
|
||||||
self.save_header(header)
|
self.save_header(header)
|
||||||
return True
|
return True
|
||||||
|
except BaseException as e:
|
||||||
|
self.print_error(str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def connect_chunk(self, idx, chunk):
|
def connect_chunk(self, idx, hexdata):
|
||||||
try:
|
try:
|
||||||
self.verify_chunk(idx, chunk)
|
data = hexdata.decode('hex')
|
||||||
|
self.verify_chunk(idx, data)
|
||||||
|
self.print_error("validated chunk %d" % idx)
|
||||||
|
self.save_chunk(idx, data)
|
||||||
return idx + 1
|
return idx + 1
|
||||||
except Exception:
|
except BaseException as e:
|
||||||
self.print_error('verify_chunk failed')
|
self.print_error('verify_chunk failed', str(e))
|
||||||
return idx - 1
|
return idx - 1
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
#
|
#
|
||||||
# Electrum - lightweight Bitcoin client
|
# Electrum - lightweight Bitcoin client
|
||||||
# Copyright (C) 2011 thomasv@gitorious
|
# Copyright (C) 2015 kyuupichan@gmail
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,7 +17,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from random import shuffle
|
from random import choice, randint, shuffle
|
||||||
|
from math import floor, log10
|
||||||
|
|
||||||
from bitcoin import COIN
|
from bitcoin import COIN
|
||||||
from transaction import Transaction
|
from transaction import Transaction
|
||||||
|
@ -25,6 +26,15 @@ from util import NotEnoughFunds, PrintError, profiler
|
||||||
|
|
||||||
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
|
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
|
||||||
|
|
||||||
|
def strip_unneeded(bkts, sufficient_funds):
|
||||||
|
'''Remove buckets that are unnecessary in achieving the spend amount'''
|
||||||
|
bkts = sorted(bkts, key = lambda bkt: bkt.value)
|
||||||
|
for i in range(len(bkts)):
|
||||||
|
if not sufficient_funds(bkts[i + 1:]):
|
||||||
|
return bkts[i:]
|
||||||
|
# Shouldn't get here
|
||||||
|
return bkts
|
||||||
|
|
||||||
class CoinChooserBase(PrintError):
|
class CoinChooserBase(PrintError):
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
|
@ -49,17 +59,25 @@ class CoinChooserBase(PrintError):
|
||||||
return 0
|
return 0
|
||||||
return penalty
|
return penalty
|
||||||
|
|
||||||
def add_change(self, tx, change_addrs, fee_estimator, dust_threshold):
|
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
|
||||||
# How much is left if we add 1 change output?
|
# The amount left after adding 1 change output
|
||||||
change_amount = tx.get_fee() - fee_estimator(1)
|
return [max(0, tx.get_fee() - fee_estimator(1))]
|
||||||
|
|
||||||
|
def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold):
|
||||||
|
amounts = self.change_amounts(tx, len(change_addrs), fee_estimator,
|
||||||
|
dust_threshold)
|
||||||
|
assert min(amounts) >= 0
|
||||||
|
assert len(change_addrs) >= len(amounts)
|
||||||
# If change is above dust threshold after accounting for the
|
# If change is above dust threshold after accounting for the
|
||||||
# size of the change output, add it to the transaction.
|
# size of the change output, add it to the transaction.
|
||||||
if change_amount > dust_threshold:
|
dust = sum(amount for amount in amounts if amount < dust_threshold)
|
||||||
tx.outputs.append(('address', change_addrs[0], change_amount))
|
amounts = [amount for amount in amounts if amount >= dust_threshold]
|
||||||
self.print_error('change', change_amount)
|
change = [('address', addr, amount)
|
||||||
elif change_amount:
|
for addr, amount in zip(change_addrs, amounts)]
|
||||||
self.print_error('not keeping dust', change_amount)
|
self.print_error('change:', change)
|
||||||
|
if dust:
|
||||||
|
self.print_error('not keeping dust', dust)
|
||||||
|
return change
|
||||||
|
|
||||||
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
||||||
dust_threshold):
|
dust_threshold):
|
||||||
|
@ -72,12 +90,18 @@ class CoinChooserBase(PrintError):
|
||||||
tx = Transaction.from_io([], outputs[:])
|
tx = Transaction.from_io([], outputs[:])
|
||||||
# Size of the transaction with no inputs and no change
|
# Size of the transaction with no inputs and no change
|
||||||
base_size = tx.estimated_size()
|
base_size = tx.estimated_size()
|
||||||
# Returns fee given input size
|
spent_amount = tx.output_value()
|
||||||
fee = lambda input_size: fee_estimator(base_size + input_size)
|
|
||||||
|
def sufficient_funds(buckets):
|
||||||
|
'''Given a list of buckets, return True if it has enough
|
||||||
|
value to pay for the transaction'''
|
||||||
|
total_input = sum(bucket.value for bucket in buckets)
|
||||||
|
total_size = sum(bucket.size for bucket in buckets) + base_size
|
||||||
|
return total_input >= spent_amount + fee_estimator(total_size)
|
||||||
|
|
||||||
# Collect the coins into buckets, choose a subset of the buckets
|
# Collect the coins into buckets, choose a subset of the buckets
|
||||||
buckets = self.bucketize_coins(coins)
|
buckets = self.bucketize_coins(coins)
|
||||||
buckets = self.choose_buckets(buckets, tx.output_value(), fee,
|
buckets = self.choose_buckets(buckets, sufficient_funds,
|
||||||
self.penalty_func(tx))
|
self.penalty_func(tx))
|
||||||
|
|
||||||
tx.inputs = [coin for b in buckets for coin in b.coins]
|
tx.inputs = [coin for b in buckets for coin in b.coins]
|
||||||
|
@ -86,50 +110,37 @@ class CoinChooserBase(PrintError):
|
||||||
# This takes a count of change outputs and returns a tx fee;
|
# This takes a count of change outputs and returns a tx fee;
|
||||||
# each pay-to-bitcoin-address output serializes as 34 bytes
|
# each pay-to-bitcoin-address output serializes as 34 bytes
|
||||||
fee = lambda count: fee_estimator(tx_size + count * 34)
|
fee = lambda count: fee_estimator(tx_size + count * 34)
|
||||||
self.add_change(tx, change_addrs, fee, dust_threshold)
|
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
|
||||||
|
tx.outputs.extend(change)
|
||||||
|
|
||||||
self.print_error("using %d inputs" % len(tx.inputs))
|
self.print_error("using %d inputs" % len(tx.inputs))
|
||||||
self.print_error("using buckets:", [bucket.desc for bucket in buckets])
|
self.print_error("using buckets:", [bucket.desc for bucket in buckets])
|
||||||
|
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
class CoinChooserClassic(CoinChooserBase):
|
class CoinChooserOldestFirst(CoinChooserBase):
|
||||||
'''
|
'''The classic electrum algorithm. Chooses coins starting with the
|
||||||
The classic electrum algorithm. Chooses coins starting with
|
oldest that are sufficient to cover the spent amount, and then
|
||||||
the oldest that are sufficient to cover the spent amount, and
|
removes any unneeded starting with the smallest in value.'''
|
||||||
then removes any unneeded starting with the smallest in value.'''
|
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
|
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
|
||||||
for coin in coins]
|
for coin in coins]
|
||||||
|
|
||||||
def choose_buckets(self, buckets, spent_amount, fee, penalty_func):
|
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
||||||
'''Spend the oldest buckets first.'''
|
'''Spend the oldest buckets first.'''
|
||||||
# Unconfirmed coins are young, not old
|
# Unconfirmed coins are young, not old
|
||||||
adj_height = lambda height: 99999999 if height == 0 else height
|
adj_height = lambda height: 99999999 if height == 0 else height
|
||||||
buckets.sort(key = lambda b: max(adj_height(coin['height'])
|
buckets.sort(key = lambda b: max(adj_height(coin['height'])
|
||||||
for coin in b.coins))
|
for coin in b.coins))
|
||||||
selected, value, size = [], 0, 0
|
selected = []
|
||||||
for bucket in buckets:
|
for bucket in buckets:
|
||||||
selected.append(bucket)
|
selected.append(bucket)
|
||||||
value += bucket.value
|
if sufficient_funds(selected):
|
||||||
size += bucket.size
|
return strip_unneeded(selected, sufficient_funds)
|
||||||
if value >= spent_amount + fee(size):
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
raise NotEnoughFunds()
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
# Remove unneeded inputs starting with the smallest.
|
|
||||||
selected.sort(key = lambda b: b.value)
|
|
||||||
dropped = []
|
|
||||||
for bucket in selected:
|
|
||||||
if value - bucket.value >= spent_amount + fee(size - bucket.size):
|
|
||||||
value -= bucket.value
|
|
||||||
size -= bucket.size
|
|
||||||
dropped.append(bucket)
|
|
||||||
|
|
||||||
return [bucket for bucket in selected if bucket not in dropped]
|
|
||||||
|
|
||||||
class CoinChooserRandom(CoinChooserBase):
|
class CoinChooserRandom(CoinChooserBase):
|
||||||
|
|
||||||
def bucket_candidates(self, buckets, sufficient_funds):
|
def bucket_candidates(self, buckets, sufficient_funds):
|
||||||
|
@ -157,18 +168,11 @@ class CoinChooserRandom(CoinChooserBase):
|
||||||
else:
|
else:
|
||||||
raise NotEnoughFunds()
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
return [[buckets[n] for n in candidate] for candidate in candidates]
|
candidates = [[buckets[n] for n in c] for c in candidates]
|
||||||
|
return [strip_unneeded(c, sufficient_funds) for c in candidates]
|
||||||
|
|
||||||
def choose_buckets(self, buckets, spent_amount, fee, penalty_func):
|
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
||||||
|
candidates = self.bucket_candidates(buckets, sufficient_funds)
|
||||||
def sufficient(buckets):
|
|
||||||
'''Given a set of buckets, return True if it has enough
|
|
||||||
value to pay for the transaction'''
|
|
||||||
total_input = sum(bucket.value for bucket in buckets)
|
|
||||||
total_size = sum(bucket.size for bucket in buckets)
|
|
||||||
return total_input >= spent_amount + fee(total_size)
|
|
||||||
|
|
||||||
candidates = self.bucket_candidates(buckets, sufficient)
|
|
||||||
penalties = [penalty_func(cand) for cand in candidates]
|
penalties = [penalty_func(cand) for cand in candidates]
|
||||||
winner = candidates[penalties.index(min(penalties))]
|
winner = candidates[penalties.index(min(penalties))]
|
||||||
self.print_error("Bucket sets:", len(buckets))
|
self.print_error("Bucket sets:", len(buckets))
|
||||||
|
@ -176,15 +180,19 @@ class CoinChooserRandom(CoinChooserBase):
|
||||||
return winner
|
return winner
|
||||||
|
|
||||||
class CoinChooserPrivacy(CoinChooserRandom):
|
class CoinChooserPrivacy(CoinChooserRandom):
|
||||||
'''
|
'''Attempts to better preserve user privacy. First, if any coin is
|
||||||
Attempts to better preserve user privacy. First, if any coin is
|
|
||||||
spent from a user address, all coins are. Compared to spending
|
spent from a user address, all coins are. Compared to spending
|
||||||
from other addresses to make up an amount, this reduces
|
from other addresses to make up an amount, this reduces
|
||||||
information leakage about sender holdings. It also helps to
|
information leakage about sender holdings. It also helps to
|
||||||
reduce blockchain UTXO bloat, and reduce future privacy loss
|
reduce blockchain UTXO bloat, and reduce future privacy loss that
|
||||||
that would come from reusing that address' remaining UTXOs.
|
would come from reusing that address' remaining UTXOs. Second, it
|
||||||
Second, it penalizes change that is quite different to the sent
|
penalizes change that is quite different to the sent amount.
|
||||||
amount. Third, it penalizes change that is too big.'''
|
Third, it penalizes change that is too big. Fourth, it breaks
|
||||||
|
large change up into amounts comparable to the spent amount.
|
||||||
|
Finally, change is rounded to similar precision to sent amounts.
|
||||||
|
Extra change outputs and rounding might raise the transaction fee
|
||||||
|
slightly. Transaction priority might be less than if older coins
|
||||||
|
were chosen.'''
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
return [coin['address'] for coin in coins]
|
return [coin['address'] for coin in coins]
|
||||||
|
@ -213,5 +221,52 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
||||||
|
|
||||||
return penalty
|
return penalty
|
||||||
|
|
||||||
COIN_CHOOSERS = {'Classic': CoinChooserClassic,
|
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
|
||||||
|
|
||||||
|
# Break change up if bigger than max_change
|
||||||
|
output_amounts = [o[2] for o in tx.outputs]
|
||||||
|
max_change = max(max(output_amounts) * 1.25, dust_threshold * 10)
|
||||||
|
|
||||||
|
# Use N change outputs
|
||||||
|
for n in range(1, count + 1):
|
||||||
|
# How much is left if we add this many change outputs?
|
||||||
|
change_amount = max(0, tx.get_fee() - fee_estimator(n))
|
||||||
|
if change_amount // n <= max_change:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get a handle on the precision of the output amounts; round our
|
||||||
|
# change to look similar
|
||||||
|
def trailing_zeroes(val):
|
||||||
|
s = str(val)
|
||||||
|
return len(s) - len(s.rstrip('0'))
|
||||||
|
|
||||||
|
zeroes = map(trailing_zeroes, output_amounts)
|
||||||
|
min_zeroes = min(zeroes)
|
||||||
|
max_zeroes = max(zeroes)
|
||||||
|
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
|
||||||
|
|
||||||
|
# Calculate change; randomize it a bit if using more than 1 output
|
||||||
|
remaining = change_amount
|
||||||
|
amounts = []
|
||||||
|
while n > 1:
|
||||||
|
average = remaining // n
|
||||||
|
amount = randint(int(average * 0.7), int(average * 1.3))
|
||||||
|
precision = min(choice(zeroes), int(floor(log10(amount))))
|
||||||
|
amount = int(round(amount, -precision))
|
||||||
|
amounts.append(amount)
|
||||||
|
remaining -= amount
|
||||||
|
n -= 1
|
||||||
|
|
||||||
|
# Last change output. Round down to maximum precision but lose
|
||||||
|
# no more than 100 satoshis to fees (2dp)
|
||||||
|
N = pow(10, min(2, zeroes[0]))
|
||||||
|
amount = (remaining // N) * N
|
||||||
|
amounts.append(amount)
|
||||||
|
|
||||||
|
assert sum(amounts) <= change_amount
|
||||||
|
|
||||||
|
return amounts
|
||||||
|
|
||||||
|
|
||||||
|
COIN_CHOOSERS = {'Oldest First': CoinChooserOldestFirst,
|
||||||
'Privacy': CoinChooserPrivacy}
|
'Privacy': CoinChooserPrivacy}
|
||||||
|
|
|
@ -74,21 +74,22 @@ def command(s):
|
||||||
|
|
||||||
class Commands:
|
class Commands:
|
||||||
|
|
||||||
def __init__(self, config, wallet, network, callback = None):
|
def __init__(self, config, wallet, network, callback = None, password=None, new_password=None):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.network = network
|
self.network = network
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
self.password = None
|
self._password = password
|
||||||
|
self.new_password = new_password
|
||||||
self.contacts = contacts.Contacts(self.config)
|
self.contacts = contacts.Contacts(self.config)
|
||||||
|
|
||||||
def _run(self, method, args, password_getter):
|
def _run(self, method, args, password_getter):
|
||||||
cmd = known_commands[method]
|
cmd = known_commands[method]
|
||||||
if cmd.requires_password and self.wallet.use_encryption:
|
if cmd.requires_password and self.wallet.use_encryption:
|
||||||
self.password = apply(password_getter,())
|
self._password = apply(password_getter,())
|
||||||
f = getattr(self, method)
|
f = getattr(self, method)
|
||||||
result = f(*args)
|
result = f(*args)
|
||||||
self.password = None
|
self._password = None
|
||||||
if self._callback:
|
if self._callback:
|
||||||
apply(self._callback, ())
|
apply(self._callback, ())
|
||||||
return result
|
return result
|
||||||
|
@ -120,7 +121,9 @@ class Commands:
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def password(self):
|
def password(self):
|
||||||
"""Change wallet password. """
|
"""Change wallet password. """
|
||||||
raise BaseException('Not a JSON-RPC command')
|
self.wallet.update_password(self._password, self.new_password)
|
||||||
|
self.wallet.storage.write()
|
||||||
|
return {'password':self.wallet.use_encryption}
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def getconfig(self, key):
|
def getconfig(self, key):
|
||||||
|
@ -157,7 +160,7 @@ class Commands:
|
||||||
"""
|
"""
|
||||||
return self.network.synchronous_get(('blockchain.address.get_history', [address]))
|
return self.network.synchronous_get(('blockchain.address.get_history', [address]))
|
||||||
|
|
||||||
@command('nw')
|
@command('w')
|
||||||
def listunspent(self):
|
def listunspent(self):
|
||||||
"""List unspent outputs. Returns the list of unspent transaction
|
"""List unspent outputs. Returns the list of unspent transaction
|
||||||
outputs in your wallet."""
|
outputs in your wallet."""
|
||||||
|
@ -200,7 +203,7 @@ class Commands:
|
||||||
outputs = map(lambda x: ('address', x[0], int(COIN*x[1])), outputs.items())
|
outputs = map(lambda x: ('address', x[0], int(COIN*x[1])), outputs.items())
|
||||||
tx = Transaction.from_io(tx_inputs, outputs)
|
tx = Transaction.from_io(tx_inputs, outputs)
|
||||||
if not unsigned:
|
if not unsigned:
|
||||||
self.wallet.sign_transaction(tx, self.password)
|
self.wallet.sign_transaction(tx, self._password)
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
|
@ -212,7 +215,7 @@ class Commands:
|
||||||
pubkey = bitcoin.public_key_from_private_key(privkey)
|
pubkey = bitcoin.public_key_from_private_key(privkey)
|
||||||
t.sign({pubkey:privkey})
|
t.sign({pubkey:privkey})
|
||||||
else:
|
else:
|
||||||
self.wallet.sign_transaction(t, self.password)
|
self.wallet.sign_transaction(t, self._password)
|
||||||
return t.as_dict()
|
return t.as_dict()
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
|
@ -250,7 +253,7 @@ class Commands:
|
||||||
"""Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses."""
|
"""Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses."""
|
||||||
is_list = type(address) is list
|
is_list = type(address) is list
|
||||||
domain = address if is_list else [address]
|
domain = address if is_list else [address]
|
||||||
out = [self.wallet.get_private_key(address, self.password) for address in domain]
|
out = [self.wallet.get_private_key(address, self._password) for address in domain]
|
||||||
return out if is_list else out[0]
|
return out if is_list else out[0]
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
|
@ -273,10 +276,9 @@ class Commands:
|
||||||
"""Return the public keys for a wallet address. """
|
"""Return the public keys for a wallet address. """
|
||||||
return self.wallet.get_public_keys(address)
|
return self.wallet.get_public_keys(address)
|
||||||
|
|
||||||
@command('nw')
|
@command('w')
|
||||||
def getbalance(self, account=None):
|
def getbalance(self, account=None):
|
||||||
"""Return the balance of your wallet. If run with the --offline flag,
|
"""Return the balance of your wallet. """
|
||||||
returns the last known balance."""
|
|
||||||
if account is None:
|
if account is None:
|
||||||
c, u, x = self.wallet.get_balance()
|
c, u, x = self.wallet.get_balance()
|
||||||
else:
|
else:
|
||||||
|
@ -334,19 +336,19 @@ class Commands:
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def getmasterprivate(self):
|
def getmasterprivate(self):
|
||||||
"""Get master private key. Return your wallet\'s master private key"""
|
"""Get master private key. Return your wallet\'s master private key"""
|
||||||
return str(self.wallet.get_master_private_key(self.wallet.root_name, self.password))
|
return str(self.wallet.get_master_private_key(self.wallet.root_name, self._password))
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def getseed(self):
|
def getseed(self):
|
||||||
"""Get seed phrase. Print the generation seed of your wallet."""
|
"""Get seed phrase. Print the generation seed of your wallet."""
|
||||||
s = self.wallet.get_mnemonic(self.password)
|
s = self.wallet.get_mnemonic(self._password)
|
||||||
return s.encode('utf8')
|
return s.encode('utf8')
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def importprivkey(self, privkey):
|
def importprivkey(self, privkey):
|
||||||
"""Import a private key. """
|
"""Import a private key. """
|
||||||
try:
|
try:
|
||||||
addr = self.wallet.import_key(privkey, self.password)
|
addr = self.wallet.import_key(privkey, self._password)
|
||||||
out = "Keypair imported: " + addr
|
out = "Keypair imported: " + addr
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
out = "Error: " + str(e)
|
out = "Error: " + str(e)
|
||||||
|
@ -377,7 +379,7 @@ class Commands:
|
||||||
def signmessage(self, address, message):
|
def signmessage(self, address, message):
|
||||||
"""Sign a message with a key. Use quotes if your message contains
|
"""Sign a message with a key. Use quotes if your message contains
|
||||||
whitespaces"""
|
whitespaces"""
|
||||||
sig = self.wallet.sign_message(address, message, self.password)
|
sig = self.wallet.sign_message(address, message, self._password)
|
||||||
return base64.b64encode(sig)
|
return base64.b64encode(sig)
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
|
@ -415,24 +417,24 @@ class Commands:
|
||||||
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
|
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
|
||||||
str(tx) #this serializes
|
str(tx) #this serializes
|
||||||
if not unsigned:
|
if not unsigned:
|
||||||
self.wallet.sign_transaction(tx, self.password)
|
self.wallet.sign_transaction(tx, self._password)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
@command('wpn')
|
@command('wp')
|
||||||
def payto(self, destination, amount, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
|
def payto(self, destination, amount, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
|
||||||
"""Create a transaction. """
|
"""Create a transaction. """
|
||||||
domain = [from_addr] if from_addr else None
|
domain = [from_addr] if from_addr else None
|
||||||
tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned)
|
tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned)
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('wpn')
|
@command('wp')
|
||||||
def paytomany(self, outputs, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
|
def paytomany(self, outputs, tx_fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False):
|
||||||
"""Create a multi-output transaction. """
|
"""Create a multi-output transaction. """
|
||||||
domain = [from_addr] if from_addr else None
|
domain = [from_addr] if from_addr else None
|
||||||
tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned)
|
tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned)
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('wn')
|
@command('w')
|
||||||
def history(self):
|
def history(self):
|
||||||
"""Wallet history. Returns the transaction history of your wallet."""
|
"""Wallet history. Returns the transaction history of your wallet."""
|
||||||
balance = 0
|
balance = 0
|
||||||
|
@ -443,7 +445,7 @@ class Commands:
|
||||||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||||
except Exception:
|
except Exception:
|
||||||
time_str = "----"
|
time_str = "----"
|
||||||
label, is_default_label = self.wallet.get_label(tx_hash)
|
label = self.wallet.get_label(tx_hash)
|
||||||
out.append({
|
out.append({
|
||||||
'txid':tx_hash,
|
'txid':tx_hash,
|
||||||
'timestamp':timestamp,
|
'timestamp':timestamp,
|
||||||
|
@ -502,7 +504,7 @@ class Commands:
|
||||||
out.append(item)
|
out.append(item)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@command('nw')
|
@command('w')
|
||||||
def gettransaction(self, txid):
|
def gettransaction(self, txid):
|
||||||
"""Retrieve a transaction. """
|
"""Retrieve a transaction. """
|
||||||
tx = self.wallet.transactions.get(txid) if self.wallet else None
|
tx = self.wallet.transactions.get(txid) if self.wallet else None
|
||||||
|
@ -522,7 +524,7 @@ class Commands:
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def decrypt(self, pubkey, encrypted):
|
def decrypt(self, pubkey, encrypted):
|
||||||
"""Decrypt a message encrypted with a public key."""
|
"""Decrypt a message encrypted with a public key."""
|
||||||
return self.wallet.decrypt_message(pubkey, encrypted, self.password)
|
return self.wallet.decrypt_message(pubkey, encrypted, self._password)
|
||||||
|
|
||||||
def _format_request(self, out):
|
def _format_request(self, out):
|
||||||
pr_str = {
|
pr_str = {
|
||||||
|
@ -535,7 +537,7 @@ class Commands:
|
||||||
out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
|
out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@command('wn')
|
@command('w')
|
||||||
def getrequest(self, key):
|
def getrequest(self, key):
|
||||||
"""Return a payment request"""
|
"""Return a payment request"""
|
||||||
r = self.wallet.get_payment_request(key, self.config)
|
r = self.wallet.get_payment_request(key, self.config)
|
||||||
|
@ -548,7 +550,7 @@ class Commands:
|
||||||
# """<Not implemented>"""
|
# """<Not implemented>"""
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
@command('wn')
|
@command('w')
|
||||||
def listrequests(self, pending=False, expired=False, paid=False):
|
def listrequests(self, pending=False, expired=False, paid=False):
|
||||||
"""List the payment requests you made."""
|
"""List the payment requests you made."""
|
||||||
out = self.wallet.get_sorted_requests(self.config)
|
out = self.wallet.get_sorted_requests(self.config)
|
||||||
|
@ -565,7 +567,7 @@ class Commands:
|
||||||
return map(self._format_request, out)
|
return map(self._format_request, out)
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def addrequest(self, requested_amount, memo='', expiration=60*60, force=False):
|
def addrequest(self, amount, memo='', expiration=60*60, force=False):
|
||||||
"""Create a payment request."""
|
"""Create a payment request."""
|
||||||
addr = self.wallet.get_unused_address(None)
|
addr = self.wallet.get_unused_address(None)
|
||||||
if addr is None:
|
if addr is None:
|
||||||
|
@ -573,7 +575,7 @@ class Commands:
|
||||||
addr = self.wallet.create_new_address(None, False)
|
addr = self.wallet.create_new_address(None, False)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
amount = int(Decimal(requested_amount)*COIN)
|
amount = int(COIN*Decimal(amount))
|
||||||
expiration = int(expiration)
|
expiration = int(expiration)
|
||||||
req = self.wallet.make_payment_request(addr, amount, memo, expiration)
|
req = self.wallet.make_payment_request(addr, amount, memo, expiration)
|
||||||
self.wallet.add_payment_request(req, self.config)
|
self.wallet.add_payment_request(req, self.config)
|
||||||
|
@ -587,7 +589,7 @@ class Commands:
|
||||||
if not alias:
|
if not alias:
|
||||||
raise BaseException('No alias in your configuration')
|
raise BaseException('No alias in your configuration')
|
||||||
alias_addr = self.contacts.resolve(alias)['address']
|
alias_addr = self.contacts.resolve(alias)['address']
|
||||||
self.wallet.sign_payment_request(address, alias, alias_addr, self.password)
|
self.wallet.sign_payment_request(address, alias, alias_addr, self._password)
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def rmrequest(self, address):
|
def rmrequest(self, address):
|
||||||
|
@ -664,15 +666,18 @@ command_options = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# don't use floats because of rounding errors
|
||||||
|
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
|
||||||
arg_types = {
|
arg_types = {
|
||||||
'num':int,
|
'num': int,
|
||||||
'nbits':int,
|
'nbits': int,
|
||||||
'entropy':long,
|
'entropy': long,
|
||||||
'pubkeys': json.loads,
|
'tx': json_loads,
|
||||||
'inputs': json.loads,
|
'pubkeys': json_loads,
|
||||||
'outputs': json.loads,
|
'inputs': json_loads,
|
||||||
'tx_fee': lambda x: float(x) if x is not None else None,
|
'outputs': json_loads,
|
||||||
'amount': lambda x: float(x) if x!='!' else '!',
|
'tx_fee': lambda x: str(Decimal(x)) if x is not None else None,
|
||||||
|
'amount': lambda x: str(Decimal(x)) if x!='!' else '!',
|
||||||
}
|
}
|
||||||
|
|
||||||
config_variables = {
|
config_variables = {
|
||||||
|
@ -728,7 +733,6 @@ def get_parser():
|
||||||
group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Show debugging information")
|
group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Show debugging information")
|
||||||
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
|
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
|
||||||
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
|
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
|
||||||
group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
|
|
||||||
# create main parser
|
# create main parser
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
parents=[parent_parser],
|
parents=[parent_parser],
|
||||||
|
@ -739,6 +743,7 @@ def get_parser():
|
||||||
parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
|
parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
|
||||||
#parser_gui.set_defaults(func=run_gui)
|
#parser_gui.set_defaults(func=run_gui)
|
||||||
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'lite', 'gtk', 'kivy', 'text', 'stdio', 'jsonrpc'])
|
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'lite', 'gtk', 'kivy', 'text', 'stdio', 'jsonrpc'])
|
||||||
|
parser_gui.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
|
||||||
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
|
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
|
||||||
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
|
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
|
||||||
add_network_options(parser_gui)
|
add_network_options(parser_gui)
|
||||||
|
|
|
@ -103,7 +103,7 @@ class Daemon(DaemonThread):
|
||||||
'nodes': self.network.get_interfaces(),
|
'nodes': self.network.get_interfaces(),
|
||||||
'connected': self.network.is_connected(),
|
'connected': self.network.is_connected(),
|
||||||
'auto_connect': p[4],
|
'auto_connect': p[4],
|
||||||
'wallets': self.wallets.keys(),
|
'wallets': dict([ (k, w.is_up_to_date()) for k, w in self.wallets.items()]),
|
||||||
}
|
}
|
||||||
elif sub == 'stop':
|
elif sub == 'stop':
|
||||||
self.stop()
|
self.stop()
|
||||||
|
@ -135,21 +135,19 @@ class Daemon(DaemonThread):
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
def run_cmdline(self, config_options):
|
def run_cmdline(self, config_options):
|
||||||
password = config_options.get('password')
|
|
||||||
config = SimpleConfig(config_options)
|
config = SimpleConfig(config_options)
|
||||||
cmdname = config.get('cmd')
|
cmdname = config.get('cmd')
|
||||||
cmd = known_commands[cmdname]
|
cmd = known_commands[cmdname]
|
||||||
wallet = self.load_wallet(config) if cmd.requires_wallet else None
|
wallet = self.load_wallet(config) if cmd.requires_wallet else None
|
||||||
if wallet:
|
|
||||||
wallet.wait_until_synchronized()
|
|
||||||
# arguments passed to function
|
# arguments passed to function
|
||||||
args = map(lambda x: config.get(x), cmd.params)
|
args = map(lambda x: config.get(x), cmd.params)
|
||||||
# decode json arguments
|
# decode json arguments
|
||||||
args = map(json_decode, args)
|
args = map(json_decode, args)
|
||||||
# options
|
# options
|
||||||
args += map(lambda x: config.get(x), cmd.options)
|
args += map(lambda x: config.get(x), cmd.options)
|
||||||
cmd_runner = Commands(config, wallet, self.network)
|
cmd_runner = Commands(config, wallet, self.network,
|
||||||
cmd_runner.password = password
|
password=config_options.get('password'),
|
||||||
|
new_password=config_options.get('new_password'))
|
||||||
func = getattr(cmd_runner, cmd.name)
|
func = getattr(cmd_runner, cmd.name)
|
||||||
result = func(*args)
|
result = func(*args)
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -12,7 +12,7 @@ proc = None
|
||||||
def scan_qr(config):
|
def scan_qr(config):
|
||||||
global proc
|
global proc
|
||||||
if not zbar:
|
if not zbar:
|
||||||
raise BaseException("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo pip install zbar'")]))
|
raise RuntimeError("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo pip install zbar'")]))
|
||||||
if proc is None:
|
if proc is None:
|
||||||
device = config.get("video_device", "default")
|
device = config.get("video_device", "default")
|
||||||
if device == 'default':
|
if device == 'default':
|
||||||
|
|
|
@ -179,5 +179,5 @@ class Synchronizer(ThreadJob):
|
||||||
if up_to_date != self.wallet.is_up_to_date():
|
if up_to_date != self.wallet.is_up_to_date():
|
||||||
self.wallet.set_up_to_date(up_to_date)
|
self.wallet.set_up_to_date(up_to_date)
|
||||||
if up_to_date:
|
if up_to_date:
|
||||||
self.wallet.save_transactions()
|
self.wallet.save_transactions(write=True)
|
||||||
self.network.trigger_callback('updated')
|
self.network.trigger_callback('updated')
|
||||||
|
|
|
@ -57,7 +57,7 @@ class TestWalletStorage(WalletTestCase):
|
||||||
some_dict = {"a":"b", "c":"d"}
|
some_dict = {"a":"b", "c":"d"}
|
||||||
|
|
||||||
for key, value in some_dict.items():
|
for key, value in some_dict.items():
|
||||||
storage.put(key, value, False)
|
storage.put(key, value)
|
||||||
storage.write()
|
storage.write()
|
||||||
|
|
||||||
contents = ""
|
contents = ""
|
||||||
|
|
31
lib/util.py
31
lib/util.py
|
@ -10,6 +10,8 @@ import urllib
|
||||||
import threading
|
import threading
|
||||||
from i18n import _
|
from i18n import _
|
||||||
|
|
||||||
|
base_units = {'BTC':8, 'mBTC':5, 'uBTC':2}
|
||||||
|
|
||||||
def normalize_version(v):
|
def normalize_version(v):
|
||||||
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
|
return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")]
|
||||||
|
|
||||||
|
@ -147,20 +149,20 @@ def json_encode(obj):
|
||||||
|
|
||||||
def json_decode(x):
|
def json_decode(x):
|
||||||
try:
|
try:
|
||||||
return json.loads(x)
|
return json.loads(x, parse_float=decimal.Decimal)
|
||||||
except:
|
except:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
# decorator that prints execution time
|
# decorator that prints execution time
|
||||||
def profiler(func):
|
def profiler(func):
|
||||||
def do_profile(func, args):
|
def do_profile(func, args, kw_args):
|
||||||
n = func.func_name
|
n = func.func_name
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
o = apply(func, args)
|
o = func(*args, **kw_args)
|
||||||
t = time.time() - t0
|
t = time.time() - t0
|
||||||
print_error("[profiler]", n, "%.4f"%t)
|
print_error("[profiler]", n, "%.4f"%t)
|
||||||
return o
|
return o
|
||||||
return lambda *args: do_profile(func, args)
|
return lambda *args, **kw_args: do_profile(func, args, kw_args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -317,7 +319,7 @@ def block_explorer_URL(config, kind, item):
|
||||||
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
|
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
|
||||||
#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
|
#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
|
||||||
|
|
||||||
def parse_URI(uri):
|
def parse_URI(uri, on_pr=None):
|
||||||
import bitcoin
|
import bitcoin
|
||||||
from bitcoin import COIN
|
from bitcoin import COIN
|
||||||
|
|
||||||
|
@ -364,6 +366,22 @@ def parse_URI(uri):
|
||||||
if 'sig' in out:
|
if 'sig' in out:
|
||||||
out['sig'] = bitcoin.base_decode(out['sig'], None, base=58).encode('hex')
|
out['sig'] = bitcoin.base_decode(out['sig'], None, base=58).encode('hex')
|
||||||
|
|
||||||
|
r = out.get('r')
|
||||||
|
sig = out.get('sig')
|
||||||
|
name = out.get('name')
|
||||||
|
if r or (name and sig):
|
||||||
|
def get_payment_request_thread():
|
||||||
|
import paymentrequest as pr
|
||||||
|
if name and sig:
|
||||||
|
s = pr.serialize_request(out).SerializeToString()
|
||||||
|
request = pr.PaymentRequest(s)
|
||||||
|
else:
|
||||||
|
request = pr.get_payment_request(r)
|
||||||
|
on_pr(request)
|
||||||
|
t = threading.Thread(target=get_payment_request_thread)
|
||||||
|
t.setDaemon(True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@ -561,11 +579,12 @@ class StoreDict(dict):
|
||||||
|
|
||||||
|
|
||||||
def check_www_dir(rdir):
|
def check_www_dir(rdir):
|
||||||
# rewrite index.html every time
|
|
||||||
import urllib, urlparse, shutil, os
|
import urllib, urlparse, shutil, os
|
||||||
if not os.path.exists(rdir):
|
if not os.path.exists(rdir):
|
||||||
os.mkdir(rdir)
|
os.mkdir(rdir)
|
||||||
index = os.path.join(rdir, 'index.html')
|
index = os.path.join(rdir, 'index.html')
|
||||||
|
if not os.path.exists(index):
|
||||||
|
print_error("copying index.html")
|
||||||
src = os.path.join(os.path.dirname(__file__), 'www', 'index.html')
|
src = os.path.join(os.path.dirname(__file__), 'www', 'index.html')
|
||||||
shutil.copy(src, index)
|
shutil.copy(src, index)
|
||||||
files = [
|
files = [
|
||||||
|
|
|
@ -26,7 +26,7 @@ import json
|
||||||
import copy
|
import copy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from util import PrintError, profiler
|
from util import NotEnoughFunds, PrintError, profiler
|
||||||
|
|
||||||
from bitcoin import *
|
from bitcoin import *
|
||||||
from account import *
|
from account import *
|
||||||
|
@ -69,11 +69,11 @@ class WalletStorage(PrintError):
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
d = ast.literal_eval(data) #parse raw data from reading wallet file
|
d = ast.literal_eval(data) #parse raw data from reading wallet file
|
||||||
|
labels = d.get('labels', {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise IOError("Cannot read wallet file '%s'" % self.path)
|
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||||
self.data = {}
|
self.data = {}
|
||||||
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
|
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
|
||||||
labels = d.get('labels', {})
|
|
||||||
for i, label in labels.items():
|
for i, label in labels.items():
|
||||||
try:
|
try:
|
||||||
unicode(label)
|
unicode(label)
|
||||||
|
@ -98,7 +98,7 @@ class WalletStorage(PrintError):
|
||||||
v = copy.deepcopy(v)
|
v = copy.deepcopy(v)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def put(self, key, value, save = True):
|
def put(self, key, value):
|
||||||
try:
|
try:
|
||||||
json.dumps(key)
|
json.dumps(key)
|
||||||
json.dumps(value)
|
json.dumps(value)
|
||||||
|
@ -113,8 +113,6 @@ class WalletStorage(PrintError):
|
||||||
elif key in self.data:
|
elif key in self.data:
|
||||||
self.modified = True
|
self.modified = True
|
||||||
self.data.pop(key)
|
self.data.pop(key)
|
||||||
if save:
|
|
||||||
self.write()
|
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
if threading.currentThread().isDaemon():
|
if threading.currentThread().isDaemon():
|
||||||
|
@ -142,7 +140,7 @@ class WalletStorage(PrintError):
|
||||||
import stat
|
import stat
|
||||||
os.chmod(self.path, mode)
|
os.chmod(self.path, mode)
|
||||||
self.print_error("saved", self.path)
|
self.print_error("saved", self.path)
|
||||||
|
self.modified = False
|
||||||
|
|
||||||
|
|
||||||
class Abstract_Wallet(PrintError):
|
class Abstract_Wallet(PrintError):
|
||||||
|
@ -199,7 +197,7 @@ class Abstract_Wallet(PrintError):
|
||||||
|
|
||||||
# save wallet type the first time
|
# save wallet type the first time
|
||||||
if self.storage.get('wallet_type') is None:
|
if self.storage.get('wallet_type') is None:
|
||||||
self.storage.put('wallet_type', self.wallet_type, True)
|
self.storage.put('wallet_type', self.wallet_type)
|
||||||
|
|
||||||
def diagnostic_name(self):
|
def diagnostic_name(self):
|
||||||
return self.basename()
|
return self.basename()
|
||||||
|
@ -220,17 +218,18 @@ class Abstract_Wallet(PrintError):
|
||||||
self.transactions.pop(tx_hash)
|
self.transactions.pop(tx_hash)
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
def save_transactions(self):
|
def save_transactions(self, write=False):
|
||||||
with self.transaction_lock:
|
with self.transaction_lock:
|
||||||
tx = {}
|
tx = {}
|
||||||
for k,v in self.transactions.items():
|
for k,v in self.transactions.items():
|
||||||
tx[k] = str(v)
|
tx[k] = str(v)
|
||||||
# Flush storage only with the last put
|
self.storage.put('transactions', tx)
|
||||||
self.storage.put('transactions', tx, False)
|
self.storage.put('txi', self.txi)
|
||||||
self.storage.put('txi', self.txi, False)
|
self.storage.put('txo', self.txo)
|
||||||
self.storage.put('txo', self.txo, False)
|
self.storage.put('pruned_txo', self.pruned_txo)
|
||||||
self.storage.put('pruned_txo', self.pruned_txo, False)
|
self.storage.put('addr_history', self.history)
|
||||||
self.storage.put('addr_history', self.history, True)
|
if write:
|
||||||
|
self.storage.write()
|
||||||
|
|
||||||
def clear_history(self):
|
def clear_history(self):
|
||||||
with self.transaction_lock:
|
with self.transaction_lock:
|
||||||
|
@ -376,7 +375,7 @@ class Abstract_Wallet(PrintError):
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
run_hook('set_label', self, name, text)
|
run_hook('set_label', self, name, text)
|
||||||
self.storage.put('labels', self.labels, True)
|
self.storage.put('labels', self.labels)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
@ -436,7 +435,7 @@ class Abstract_Wallet(PrintError):
|
||||||
self.unverified_tx.pop(tx_hash, None)
|
self.unverified_tx.pop(tx_hash, None)
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos)
|
self.verified_tx[tx_hash] = info # (tx_height, timestamp, pos)
|
||||||
self.storage.put('verified_tx3', self.verified_tx, True)
|
self.storage.put('verified_tx3', self.verified_tx)
|
||||||
|
|
||||||
conf, timestamp = self.get_confirmations(tx_hash)
|
conf, timestamp = self.get_confirmations(tx_hash)
|
||||||
self.network.trigger_callback('verified', tx_hash, conf, timestamp)
|
self.network.trigger_callback('verified', tx_hash, conf, timestamp)
|
||||||
|
@ -867,24 +866,9 @@ class Abstract_Wallet(PrintError):
|
||||||
|
|
||||||
return h2
|
return h2
|
||||||
|
|
||||||
|
|
||||||
def get_label(self, tx_hash):
|
def get_label(self, tx_hash):
|
||||||
label = self.labels.get(tx_hash)
|
label = self.labels.get(tx_hash, '')
|
||||||
is_default = (label == '') or (label is None)
|
return label
|
||||||
if is_default:
|
|
||||||
label = self.get_default_label(tx_hash)
|
|
||||||
return label, is_default
|
|
||||||
|
|
||||||
def get_default_label(self, tx_hash):
|
|
||||||
if self.txi.get(tx_hash) == {}:
|
|
||||||
d = self.txo.get(tx_hash, {})
|
|
||||||
labels = []
|
|
||||||
for addr in d.keys():
|
|
||||||
label = self.labels.get(addr)
|
|
||||||
if label:
|
|
||||||
labels.append(label)
|
|
||||||
return ', '.join(labels)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def fee_per_kb(self, config):
|
def fee_per_kb(self, config):
|
||||||
b = config.get('dynamic_fees')
|
b = config.get('dynamic_fees')
|
||||||
|
@ -899,7 +883,7 @@ class Abstract_Wallet(PrintError):
|
||||||
def coin_chooser_name(self, config):
|
def coin_chooser_name(self, config):
|
||||||
kind = config.get('coin_chooser')
|
kind = config.get('coin_chooser')
|
||||||
if not kind in COIN_CHOOSERS:
|
if not kind in COIN_CHOOSERS:
|
||||||
kind = 'Classic'
|
kind = 'Oldest First'
|
||||||
return kind
|
return kind
|
||||||
|
|
||||||
def coin_chooser(self, config):
|
def coin_chooser(self, config):
|
||||||
|
@ -912,6 +896,10 @@ class Abstract_Wallet(PrintError):
|
||||||
if type == 'address':
|
if type == 'address':
|
||||||
assert is_address(data), "Address " + data + " is invalid!"
|
assert is_address(data), "Address " + data + " is invalid!"
|
||||||
|
|
||||||
|
# Avoid index-out-of-range with coins[0] below
|
||||||
|
if not coins:
|
||||||
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
for item in coins:
|
for item in coins:
|
||||||
self.add_input_info(item)
|
self.add_input_info(item)
|
||||||
|
|
||||||
|
@ -1044,7 +1032,7 @@ class Abstract_Wallet(PrintError):
|
||||||
if self.has_seed():
|
if self.has_seed():
|
||||||
decoded = self.get_seed(old_password)
|
decoded = self.get_seed(old_password)
|
||||||
self.seed = pw_encode( decoded, new_password)
|
self.seed = pw_encode( decoded, new_password)
|
||||||
self.storage.put('seed', self.seed, True)
|
self.storage.put('seed', self.seed)
|
||||||
|
|
||||||
imported_account = self.accounts.get(IMPORTED_ACCOUNT)
|
imported_account = self.accounts.get(IMPORTED_ACCOUNT)
|
||||||
if imported_account:
|
if imported_account:
|
||||||
|
@ -1056,10 +1044,10 @@ class Abstract_Wallet(PrintError):
|
||||||
b = pw_decode(v, old_password)
|
b = pw_decode(v, old_password)
|
||||||
c = pw_encode(b, new_password)
|
c = pw_encode(b, new_password)
|
||||||
self.master_private_keys[k] = c
|
self.master_private_keys[k] = c
|
||||||
self.storage.put('master_private_keys', self.master_private_keys, True)
|
self.storage.put('master_private_keys', self.master_private_keys)
|
||||||
|
|
||||||
self.use_encryption = (new_password != None)
|
self.use_encryption = (new_password != None)
|
||||||
self.storage.put('use_encryption', self.use_encryption,True)
|
self.storage.put('use_encryption', self.use_encryption)
|
||||||
|
|
||||||
def is_frozen(self, addr):
|
def is_frozen(self, addr):
|
||||||
return addr in self.frozen_addresses
|
return addr in self.frozen_addresses
|
||||||
|
@ -1071,7 +1059,7 @@ class Abstract_Wallet(PrintError):
|
||||||
self.frozen_addresses |= set(addrs)
|
self.frozen_addresses |= set(addrs)
|
||||||
else:
|
else:
|
||||||
self.frozen_addresses -= set(addrs)
|
self.frozen_addresses -= set(addrs)
|
||||||
self.storage.put('frozen_addresses', list(self.frozen_addresses), True)
|
self.storage.put('frozen_addresses', list(self.frozen_addresses))
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -1109,7 +1097,8 @@ class Abstract_Wallet(PrintError):
|
||||||
self.verifier = None
|
self.verifier = None
|
||||||
# Now no references to the syncronizer or verifier
|
# Now no references to the syncronizer or verifier
|
||||||
# remain so they will be GC-ed
|
# remain so they will be GC-ed
|
||||||
self.storage.put('stored_height', self.get_local_height(), True)
|
self.storage.put('stored_height', self.get_local_height())
|
||||||
|
self.storage.write()
|
||||||
|
|
||||||
def wait_until_synchronized(self, callback=None):
|
def wait_until_synchronized(self, callback=None):
|
||||||
from i18n import _
|
from i18n import _
|
||||||
|
@ -1147,7 +1136,7 @@ class Abstract_Wallet(PrintError):
|
||||||
d = {}
|
d = {}
|
||||||
for k, v in self.accounts.items():
|
for k, v in self.accounts.items():
|
||||||
d[k] = v.dump()
|
d[k] = v.dump()
|
||||||
self.storage.put('accounts', d, True)
|
self.storage.put('accounts', d)
|
||||||
|
|
||||||
def can_import(self):
|
def can_import(self):
|
||||||
return not self.is_watching_only()
|
return not self.is_watching_only()
|
||||||
|
@ -1431,9 +1420,9 @@ class Deterministic_Wallet(Abstract_Wallet):
|
||||||
else:
|
else:
|
||||||
self.use_encryption = False
|
self.use_encryption = False
|
||||||
|
|
||||||
self.storage.put('seed', self.seed, False)
|
self.storage.put('seed', self.seed)
|
||||||
self.storage.put('seed_version', self.seed_version, False)
|
self.storage.put('seed_version', self.seed_version)
|
||||||
self.storage.put('use_encryption', self.use_encryption,True)
|
self.storage.put('use_encryption', self.use_encryption)
|
||||||
|
|
||||||
def get_seed(self, password):
|
def get_seed(self, password):
|
||||||
return pw_decode(self.seed, password)
|
return pw_decode(self.seed, password)
|
||||||
|
@ -1445,7 +1434,7 @@ class Deterministic_Wallet(Abstract_Wallet):
|
||||||
assert isinstance(value, int), 'gap limit must be of type int, not of %s'%type(value)
|
assert isinstance(value, int), 'gap limit must be of type int, not of %s'%type(value)
|
||||||
if value >= self.gap_limit:
|
if value >= self.gap_limit:
|
||||||
self.gap_limit = value
|
self.gap_limit = value
|
||||||
self.storage.put('gap_limit', self.gap_limit, True)
|
self.storage.put('gap_limit', self.gap_limit)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif value >= self.min_acceptable_gap():
|
elif value >= self.min_acceptable_gap():
|
||||||
|
@ -1456,7 +1445,7 @@ class Deterministic_Wallet(Abstract_Wallet):
|
||||||
account.receiving_pubkeys = account.receiving_pubkeys[0:n]
|
account.receiving_pubkeys = account.receiving_pubkeys[0:n]
|
||||||
account.receiving_addresses = account.receiving_addresses[0:n]
|
account.receiving_addresses = account.receiving_addresses[0:n]
|
||||||
self.gap_limit = value
|
self.gap_limit = value
|
||||||
self.storage.put('gap_limit', self.gap_limit, True)
|
self.storage.put('gap_limit', self.gap_limit)
|
||||||
self.save_accounts()
|
self.save_accounts()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
@ -1579,11 +1568,11 @@ class BIP32_Wallet(Deterministic_Wallet):
|
||||||
if xpub in self.master_public_keys.values():
|
if xpub in self.master_public_keys.values():
|
||||||
raise BaseException('Duplicate master public key')
|
raise BaseException('Duplicate master public key')
|
||||||
self.master_public_keys[name] = xpub
|
self.master_public_keys[name] = xpub
|
||||||
self.storage.put('master_public_keys', self.master_public_keys, True)
|
self.storage.put('master_public_keys', self.master_public_keys)
|
||||||
|
|
||||||
def add_master_private_key(self, name, xpriv, password):
|
def add_master_private_key(self, name, xpriv, password):
|
||||||
self.master_private_keys[name] = pw_encode(xpriv, password)
|
self.master_private_keys[name] = pw_encode(xpriv, password)
|
||||||
self.storage.put('master_private_keys', self.master_private_keys, True)
|
self.storage.put('master_private_keys', self.master_private_keys)
|
||||||
|
|
||||||
def derive_xkeys(self, root, derivation, password):
|
def derive_xkeys(self, root, derivation, password):
|
||||||
x = self.master_private_keys[root]
|
x = self.master_private_keys[root]
|
||||||
|
@ -1626,16 +1615,16 @@ class BIP32_Simple_Wallet(BIP32_Wallet):
|
||||||
def create_xprv_wallet(self, xprv, password):
|
def create_xprv_wallet(self, xprv, password):
|
||||||
xpub = bitcoin.xpub_from_xprv(xprv)
|
xpub = bitcoin.xpub_from_xprv(xprv)
|
||||||
account = BIP32_Account({'xpub':xpub})
|
account = BIP32_Account({'xpub':xpub})
|
||||||
self.storage.put('seed_version', self.seed_version, True)
|
self.storage.put('seed_version', self.seed_version)
|
||||||
self.add_master_private_key(self.root_name, xprv, password)
|
self.add_master_private_key(self.root_name, xprv, password)
|
||||||
self.add_master_public_key(self.root_name, xpub)
|
self.add_master_public_key(self.root_name, xpub)
|
||||||
self.add_account('0', account)
|
self.add_account('0', account)
|
||||||
self.use_encryption = (password != None)
|
self.use_encryption = (password != None)
|
||||||
self.storage.put('use_encryption', self.use_encryption,True)
|
self.storage.put('use_encryption', self.use_encryption)
|
||||||
|
|
||||||
def create_xpub_wallet(self, xpub):
|
def create_xpub_wallet(self, xpub):
|
||||||
account = BIP32_Account({'xpub':xpub})
|
account = BIP32_Account({'xpub':xpub})
|
||||||
self.storage.put('seed_version', self.seed_version, True)
|
self.storage.put('seed_version', self.seed_version)
|
||||||
self.add_master_public_key(self.root_name, xpub)
|
self.add_master_public_key(self.root_name, xpub)
|
||||||
self.add_account('0', account)
|
self.add_account('0', account)
|
||||||
|
|
||||||
|
@ -1834,7 +1823,7 @@ class OldWallet(Deterministic_Wallet):
|
||||||
def create_master_keys(self, password):
|
def create_master_keys(self, password):
|
||||||
seed = self.get_seed(password)
|
seed = self.get_seed(password)
|
||||||
mpk = OldAccount.mpk_from_seed(seed)
|
mpk = OldAccount.mpk_from_seed(seed)
|
||||||
self.storage.put('master_public_key', mpk, True)
|
self.storage.put('master_public_key', mpk)
|
||||||
|
|
||||||
def get_master_public_key(self):
|
def get_master_public_key(self):
|
||||||
return self.storage.get("master_public_key")
|
return self.storage.get("master_public_key")
|
||||||
|
@ -1852,8 +1841,8 @@ class OldWallet(Deterministic_Wallet):
|
||||||
|
|
||||||
def create_watching_only_wallet(self, mpk):
|
def create_watching_only_wallet(self, mpk):
|
||||||
self.seed_version = OLD_SEED_VERSION
|
self.seed_version = OLD_SEED_VERSION
|
||||||
self.storage.put('seed_version', self.seed_version, False)
|
self.storage.put('seed_version', self.seed_version)
|
||||||
self.storage.put('master_public_key', mpk, True)
|
self.storage.put('master_public_key', mpk)
|
||||||
self.create_account(mpk)
|
self.create_account(mpk)
|
||||||
|
|
||||||
def get_seed(self, password):
|
def get_seed(self, password):
|
||||||
|
@ -2037,7 +2026,7 @@ class Wallet(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_multisig(klass, key_list, password, storage, wallet_type):
|
def from_multisig(klass, key_list, password, storage, wallet_type):
|
||||||
storage.put('wallet_type', wallet_type, True)
|
storage.put('wallet_type', wallet_type)
|
||||||
self = Multisig_Wallet(storage)
|
self = Multisig_Wallet(storage)
|
||||||
key_list = sorted(key_list, key = lambda x: klass.is_xpub(x))
|
key_list = sorted(key_list, key = lambda x: klass.is_xpub(x))
|
||||||
for i, text in enumerate(key_list):
|
for i, text in enumerate(key_list):
|
||||||
|
@ -2056,7 +2045,7 @@ class Wallet(object):
|
||||||
else:
|
else:
|
||||||
self.add_cosigner_seed(text, name, password)
|
self.add_cosigner_seed(text, name, password)
|
||||||
self.use_encryption = (password != None)
|
self.use_encryption = (password != None)
|
||||||
self.storage.put('use_encryption', self.use_encryption, True)
|
self.storage.put('use_encryption', self.use_encryption)
|
||||||
self.create_main_account(password)
|
self.create_main_account(password)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
from electrum.plugins import BasePlugin, hook
|
from functools import partial
|
||||||
from electrum_gui.qt.util import WaitingDialog, EnterButton
|
|
||||||
from electrum.util import print_msg, print_error
|
|
||||||
from electrum.i18n import _
|
|
||||||
|
|
||||||
from PyQt4.QtGui import *
|
|
||||||
from PyQt4.QtCore import *
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
import zlib
|
import zlib
|
||||||
import json
|
import json
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
from electrum.plugins import BasePlugin, hook
|
||||||
|
from electrum_gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog
|
||||||
|
from electrum.util import print_msg, print_error
|
||||||
|
from electrum.i18n import _
|
||||||
|
|
||||||
|
from PyQt4.QtGui import *
|
||||||
|
from PyQt4.QtCore import *
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import amodem.audio
|
import amodem.audio
|
||||||
import amodem.main
|
import amodem.main
|
||||||
|
@ -42,11 +42,10 @@ class Plugin(BasePlugin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def settings_widget(self, window):
|
def settings_widget(self, window):
|
||||||
return EnterButton(_('Settings'), self.settings_dialog)
|
return EnterButton(_('Settings'), partial(self.settings_dialog, window))
|
||||||
|
|
||||||
def settings_dialog(self):
|
def settings_dialog(self, window):
|
||||||
d = QDialog()
|
d = WindowModalDialog(window, _("Audio Modem Settings"))
|
||||||
d.setWindowTitle("Settings")
|
|
||||||
|
|
||||||
layout = QGridLayout(d)
|
layout = QGridLayout(d)
|
||||||
layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0)
|
layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0)
|
||||||
|
@ -75,24 +74,20 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
def handler():
|
def handler():
|
||||||
blob = json.dumps(dialog.tx.as_dict())
|
blob = json.dumps(dialog.tx.as_dict())
|
||||||
self.sender = self._send(parent=dialog, blob=blob)
|
self._send(parent=dialog, blob=blob)
|
||||||
self.sender.start()
|
|
||||||
b.clicked.connect(handler)
|
b.clicked.connect(handler)
|
||||||
dialog.sharing_buttons.insert(-1, b)
|
dialog.sharing_buttons.insert(-1, b)
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def scan_text_edit(self, parent):
|
def scan_text_edit(self, parent):
|
||||||
def handler():
|
parent.addButton(':icons/microphone.png', partial(self._recv, parent),
|
||||||
self.receiver = self._recv(parent=parent)
|
_("Read from microphone"))
|
||||||
self.receiver.start()
|
|
||||||
parent.addButton(':icons/microphone.png', handler, _("Read from microphone"))
|
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def show_text_edit(self, parent):
|
def show_text_edit(self, parent):
|
||||||
def handler():
|
def handler():
|
||||||
blob = str(parent.toPlainText())
|
blob = str(parent.toPlainText())
|
||||||
self.sender = self._send(parent=parent, blob=blob)
|
self._send(parent=parent, blob=blob)
|
||||||
self.sender.start()
|
|
||||||
parent.addButton(':icons/speaker.png', handler, _("Send to speaker"))
|
parent.addButton(':icons/speaker.png', handler, _("Send to speaker"))
|
||||||
|
|
||||||
def _audio_interface(self):
|
def _audio_interface(self):
|
||||||
|
@ -101,31 +96,25 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
def _send(self, parent, blob):
|
def _send(self, parent, blob):
|
||||||
def sender_thread():
|
def sender_thread():
|
||||||
try:
|
|
||||||
with self._audio_interface() as interface:
|
with self._audio_interface() as interface:
|
||||||
src = BytesIO(blob)
|
src = BytesIO(blob)
|
||||||
dst = interface.player()
|
dst = interface.player()
|
||||||
amodem.main.send(config=self.modem_config, src=src, dst=dst)
|
amodem.main.send(config=self.modem_config, src=src, dst=dst)
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
print_msg('Sending:', repr(blob))
|
print_msg('Sending:', repr(blob))
|
||||||
blob = zlib.compress(blob)
|
blob = zlib.compress(blob)
|
||||||
|
|
||||||
kbps = self.modem_config.modem_bps / 1e3
|
kbps = self.modem_config.modem_bps / 1e3
|
||||||
msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps)
|
msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps)
|
||||||
return WaitingDialog(parent=parent, message=msg, run_task=sender_thread)
|
WaitingDialog(parent, msg, sender_thread)
|
||||||
|
|
||||||
def _recv(self, parent):
|
def _recv(self, parent):
|
||||||
def receiver_thread():
|
def receiver_thread():
|
||||||
try:
|
|
||||||
with self._audio_interface() as interface:
|
with self._audio_interface() as interface:
|
||||||
src = interface.recorder()
|
src = interface.recorder()
|
||||||
dst = BytesIO()
|
dst = BytesIO()
|
||||||
amodem.main.recv(config=self.modem_config, src=src, dst=dst)
|
amodem.main.recv(config=self.modem_config, src=src, dst=dst)
|
||||||
return dst.getvalue()
|
return dst.getvalue()
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
def on_success(blob):
|
def on_success(blob):
|
||||||
if blob:
|
if blob:
|
||||||
|
@ -135,5 +124,4 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
kbps = self.modem_config.modem_bps / 1e3
|
kbps = self.modem_config.modem_bps / 1e3
|
||||||
msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps)
|
msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps)
|
||||||
return WaitingDialog(parent=parent, message=msg,
|
WaitingDialog(parent, msg, receiver_thread, on_success=on_success)
|
||||||
run_task=receiver_thread, on_success=on_success)
|
|
||||||
|
|
|
@ -18,12 +18,10 @@
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import socket
|
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import base64
|
import base64
|
||||||
from decimal import Decimal
|
from functools import partial
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
import smtplib
|
import smtplib
|
||||||
import imaplib
|
import imaplib
|
||||||
|
@ -37,12 +35,11 @@ from PyQt4.QtCore import *
|
||||||
import PyQt4.QtCore as QtCore
|
import PyQt4.QtCore as QtCore
|
||||||
import PyQt4.QtGui as QtGui
|
import PyQt4.QtGui as QtGui
|
||||||
|
|
||||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
|
|
||||||
from electrum.plugins import BasePlugin, hook
|
from electrum.plugins import BasePlugin, hook
|
||||||
from electrum import util
|
|
||||||
from electrum.paymentrequest import PaymentRequest
|
from electrum.paymentrequest import PaymentRequest
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum_gui.qt.util import text_dialog, EnterButton
|
from electrum_gui.qt.util import EnterButton, Buttons, CloseButton
|
||||||
|
from electrum_gui.qt.util import OkButton, WindowModalDialog
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -166,14 +163,10 @@ class Plugin(BasePlugin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def settings_widget(self, window):
|
def settings_widget(self, window):
|
||||||
self.settings_window = window
|
return EnterButton(_('Settings'), partial(self.settings_dialog, window))
|
||||||
return EnterButton(_('Settings'), self.settings_dialog)
|
|
||||||
|
|
||||||
def settings_dialog(self, x):
|
def settings_dialog(self, window):
|
||||||
from electrum_gui.qt.util import Buttons, CloseButton, OkButton
|
d = WindowModalDialog(window, _("Email settings"))
|
||||||
|
|
||||||
d = QDialog(self.settings_window)
|
|
||||||
d.setWindowTitle("Email settings")
|
|
||||||
d.setMinimumSize(500, 200)
|
d.setMinimumSize(500, 200)
|
||||||
|
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
|
|
|
@ -346,13 +346,18 @@ class FxPlugin(BasePlugin, ThreadJob):
|
||||||
return _("No data")
|
return _("No data")
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def historical_value_str(self, satoshis, d_t):
|
def history_rate(self, d_t):
|
||||||
rate = self.exchange.historical_rate(self.ccy, d_t)
|
rate = self.exchange.historical_rate(self.ccy, d_t)
|
||||||
# Frequently there is no rate for today, until tomorrow :)
|
# Frequently there is no rate for today, until tomorrow :)
|
||||||
# Use spot quotes in that case
|
# Use spot quotes in that case
|
||||||
if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
|
if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
|
||||||
rate = self.exchange.quotes.get(self.ccy)
|
rate = self.exchange.quotes.get(self.ccy)
|
||||||
self.history_used_spot = True
|
self.history_used_spot = True
|
||||||
|
return rate
|
||||||
|
|
||||||
|
@hook
|
||||||
|
def historical_value_str(self, satoshis, d_t):
|
||||||
|
rate = self.history_rate(d_t)
|
||||||
return self.value_str(satoshis, rate)
|
return self.value_str(satoshis, rate)
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
|
|
|
@ -128,11 +128,10 @@ class Plugin(FxPlugin):
|
||||||
window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
|
window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
|
||||||
|
|
||||||
def settings_widget(self, window):
|
def settings_widget(self, window):
|
||||||
return EnterButton(_('Settings'), self.settings_dialog)
|
return EnterButton(_('Settings'), partial(self.settings_dialog, window))
|
||||||
|
|
||||||
def settings_dialog(self):
|
def settings_dialog(self, window):
|
||||||
d = QDialog()
|
d = WindowModalDialog(window, _("Exchange Rate Settings"))
|
||||||
d.setWindowTitle("Settings")
|
|
||||||
layout = QGridLayout(d)
|
layout = QGridLayout(d)
|
||||||
layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
|
layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
|
||||||
layout.addWidget(QLabel(_('Currency: ')), 1, 0)
|
layout.addWidget(QLabel(_('Currency: ')), 1, 0)
|
||||||
|
|
|
@ -21,7 +21,7 @@ import urllib
|
||||||
import sys
|
import sys
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from PyQt4.QtGui import QMessageBox, QApplication, QPushButton
|
from PyQt4.QtGui import QApplication, QPushButton
|
||||||
|
|
||||||
from electrum.plugins import BasePlugin, hook
|
from electrum.plugins import BasePlugin, hook
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
|
@ -65,7 +65,7 @@ class Plugin(BasePlugin):
|
||||||
'to verify that transaction is instant.\n'
|
'to verify that transaction is instant.\n'
|
||||||
'Please enter your password to sign a\n'
|
'Please enter your password to sign a\n'
|
||||||
'verification request.')
|
'verification request.')
|
||||||
password = window.password_dialog(msg)
|
password = window.password_dialog(msg, parent=d)
|
||||||
if not password:
|
if not password:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
@ -84,14 +84,12 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
# 3. display the result
|
# 3. display the result
|
||||||
if response.get('verified'):
|
if response.get('verified'):
|
||||||
QMessageBox.information(None, _('Verification successful!'),
|
d.show_message(_('%s is covered by GreenAddress instant confirmation') % (tx.hash()), title=_('Verification successful!'))
|
||||||
_('%s is covered by GreenAddress instant confirmation') % (tx.hash()), _('OK'))
|
|
||||||
else:
|
else:
|
||||||
QMessageBox.critical(None, _('Verification failed!'),
|
d.show_critical(_('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), title=_('Verification failed!'))
|
||||||
_('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), _('OK'))
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
QMessageBox.information(None, _('Error'), str(e), _('OK'))
|
d.show_error(str(e))
|
||||||
finally:
|
finally:
|
||||||
d.verify_button.setText(self.button_label)
|
d.verify_button.setText(self.button_label)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton
|
from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL, QGridLayout, QInputDialog, QPushButton
|
||||||
import PyQt4.QtCore as QtCore
|
import PyQt4.QtCore as QtCore
|
||||||
from electrum_gui.qt.util import *
|
from electrum_gui.qt.util import *
|
||||||
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
|
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
|
||||||
|
@ -26,11 +26,11 @@ class Plugin(KeepKeyPlugin):
|
||||||
try:
|
try:
|
||||||
self.get_client().ping('t')
|
self.get_client().ping('t')
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
QMessageBox.information(window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK'))
|
window.show_error(_('KeepKey device not detected.\nContinuing in watching-only mode.\nReason:\n' + str(e)))
|
||||||
self.wallet.force_watching_only = True
|
self.wallet.force_watching_only = True
|
||||||
return
|
return
|
||||||
if self.wallet.addresses() and not self.wallet.check_proper_device():
|
if self.wallet.addresses() and not self.wallet.check_proper_device():
|
||||||
QMessageBox.information(window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK'))
|
window.show_error(_("This wallet does not match your KeepKey device"))
|
||||||
self.wallet.force_watching_only = True
|
self.wallet.force_watching_only = True
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
|
@ -73,7 +73,7 @@ class Plugin(KeepKeyPlugin):
|
||||||
return
|
return
|
||||||
get_label = lambda: self.get_client().features.label
|
get_label = lambda: self.get_client().features.label
|
||||||
update_label = lambda: current_label_label.setText("Label: %s" % get_label())
|
update_label = lambda: current_label_label.setText("Label: %s" % get_label())
|
||||||
d = QDialog()
|
d = WindowModalDialog(window, _("KeepKey Settings"))
|
||||||
layout = QGridLayout(d)
|
layout = QGridLayout(d)
|
||||||
layout.addWidget(QLabel("KeepKey Options"),0,0)
|
layout.addWidget(QLabel("KeepKey Options"),0,0)
|
||||||
layout.addWidget(QLabel("ID:"),1,0)
|
layout.addWidget(QLabel("ID:"),1,0)
|
||||||
|
@ -132,10 +132,7 @@ class KeepKeyQtHandler:
|
||||||
return self.passphrase
|
return self.passphrase
|
||||||
|
|
||||||
def pin_dialog(self):
|
def pin_dialog(self):
|
||||||
d = QDialog(None)
|
d = WindowModalDialog(self.win, _("Enter PIN"))
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_("Enter PIN"))
|
|
||||||
d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
|
||||||
matrix = PinMatrixWidget()
|
matrix = PinMatrixWidget()
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget(QLabel(self.message))
|
vbox.addWidget(QLabel(self.message))
|
||||||
|
@ -153,23 +150,18 @@ class KeepKeyQtHandler:
|
||||||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||||
else:
|
else:
|
||||||
assert type(self.win) is InstallWizard
|
assert type(self.win) is InstallWizard
|
||||||
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
|
from electrum_gui.qt.password_dialog import PasswordDialog
|
||||||
d = QDialog()
|
d = PasswordDialog(self.win, None, None, self.message, False)
|
||||||
d.setModal(1)
|
confirmed, p, passphrase = d.run()
|
||||||
d.setLayout(make_password_dialog(d, None, self.message, False))
|
|
||||||
confirmed, p, passphrase = run_password_dialog(d, None, None)
|
|
||||||
if not confirmed:
|
if not confirmed:
|
||||||
QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK'))
|
self.win.show_critical(_("Password request canceled"))
|
||||||
self.passphrase = None
|
self.passphrase = None
|
||||||
else:
|
else:
|
||||||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
def message_dialog(self):
|
def message_dialog(self):
|
||||||
self.d = QDialog()
|
self.d = WindowModalDialog(self.win, _('Please Check KeepKey Device'))
|
||||||
self.d.setModal(1)
|
|
||||||
self.d.setWindowTitle('Please Check KeepKey Device')
|
|
||||||
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
|
||||||
l = QLabel(self.message)
|
l = QLabel(self.message)
|
||||||
vbox = QVBoxLayout(self.d)
|
vbox = QVBoxLayout(self.d)
|
||||||
vbox.addWidget(l)
|
vbox.addWidget(l)
|
||||||
|
@ -182,5 +174,3 @@ class KeepKeyQtHandler:
|
||||||
|
|
||||||
def dialog_stop(self):
|
def dialog_stop(self):
|
||||||
self.d.hide()
|
self.d.hide()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,9 @@ class LabelsPlugin(BasePlugin):
|
||||||
self.set_nonce(wallet, nonce)
|
self.set_nonce(wallet, nonce)
|
||||||
return nonce
|
return nonce
|
||||||
|
|
||||||
def set_nonce(self, wallet, nonce, force_write=True):
|
def set_nonce(self, wallet, nonce):
|
||||||
self.print_error("set", wallet.basename(), "nonce to", nonce)
|
self.print_error("set", wallet.basename(), "nonce to", nonce)
|
||||||
wallet.storage.put("wallet_nonce", nonce, force_write)
|
wallet.storage.put("wallet_nonce", nonce)
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def set_label(self, wallet, item, label):
|
def set_label(self, wallet, item, label):
|
||||||
|
@ -61,7 +61,7 @@ class LabelsPlugin(BasePlugin):
|
||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
# Caller will write the wallet
|
# Caller will write the wallet
|
||||||
self.set_nonce(wallet, nonce + 1, force_write=False)
|
self.set_nonce(wallet, nonce + 1)
|
||||||
|
|
||||||
def do_request(self, method, url = "/labels", is_batch=False, data=None):
|
def do_request(self, method, url = "/labels", is_batch=False, data=None):
|
||||||
url = 'https://' + self.target_host + url
|
url = 'https://' + self.target_host + url
|
||||||
|
@ -125,8 +125,8 @@ class LabelsPlugin(BasePlugin):
|
||||||
|
|
||||||
self.print_error("received %d labels" % len(response))
|
self.print_error("received %d labels" % len(response))
|
||||||
# do not write to disk because we're in a daemon thread
|
# do not write to disk because we're in a daemon thread
|
||||||
wallet.storage.put('labels', wallet.labels, False)
|
wallet.storage.put('labels', wallet.labels)
|
||||||
self.set_nonce(wallet, response["nonce"] + 1, False)
|
self.set_nonce(wallet, response["nonce"] + 1)
|
||||||
self.on_pulled(wallet)
|
self.on_pulled(wallet)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -6,7 +6,8 @@ from PyQt4.QtCore import *
|
||||||
from electrum.plugins import hook
|
from electrum.plugins import hook
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum_gui.qt import EnterButton
|
from electrum_gui.qt import EnterButton
|
||||||
from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton
|
from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton
|
||||||
|
from electrum_gui.qt.util import WindowModalDialog, OkButton
|
||||||
|
|
||||||
from labels import LabelsPlugin
|
from labels import LabelsPlugin
|
||||||
|
|
||||||
|
@ -25,25 +26,23 @@ class Plugin(LabelsPlugin):
|
||||||
partial(self.settings_dialog, window))
|
partial(self.settings_dialog, window))
|
||||||
|
|
||||||
def settings_dialog(self, window):
|
def settings_dialog(self, window):
|
||||||
d = QDialog(window)
|
wallet = window.parent().wallet
|
||||||
|
d = WindowModalDialog(window, _("Label Settings"))
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
vbox.addLayout(layout)
|
vbox.addLayout(layout)
|
||||||
layout.addWidget(QLabel("Label sync options: "), 2, 0)
|
layout.addWidget(QLabel("Label sync options: "), 2, 0)
|
||||||
self.upload = ThreadedButton("Force upload",
|
self.upload = ThreadedButton("Force upload",
|
||||||
partial(self.push_thread, window.wallet),
|
partial(self.push_thread, wallet),
|
||||||
self.done_processing)
|
self.done_processing)
|
||||||
layout.addWidget(self.upload, 2, 1)
|
layout.addWidget(self.upload, 2, 1)
|
||||||
self.download = ThreadedButton("Force download",
|
self.download = ThreadedButton("Force download",
|
||||||
partial(self.pull_thread, window.wallet, True),
|
partial(self.pull_thread, wallet, True),
|
||||||
self.done_processing)
|
self.done_processing)
|
||||||
layout.addWidget(self.download, 2, 2)
|
layout.addWidget(self.download, 2, 2)
|
||||||
self.accept = OkButton(d, _("Done"))
|
self.accept = OkButton(d, _("Done"))
|
||||||
vbox.addLayout(Buttons(CancelButton(d), self.accept))
|
vbox.addLayout(Buttons(CancelButton(d), self.accept))
|
||||||
if d.exec_():
|
return bool(d.exec_())
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def on_pulled(self, wallet):
|
def on_pulled(self, wallet):
|
||||||
self.obj.emit(SIGNAL('labels_changed'), wallet)
|
self.obj.emit(SIGNAL('labels_changed'), wallet)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL
|
from PyQt4.Qt import QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, SIGNAL
|
||||||
import PyQt4.QtCore as QtCore
|
import PyQt4.QtCore as QtCore
|
||||||
|
import threading
|
||||||
|
|
||||||
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
|
|
||||||
from electrum.plugins import BasePlugin, hook
|
from electrum.plugins import BasePlugin, hook
|
||||||
|
|
||||||
from ledger import LedgerPlugin
|
from ledger import LedgerPlugin
|
||||||
|
@ -16,10 +16,10 @@ class Plugin(LedgerPlugin):
|
||||||
self.handler = BTChipQTHandler(window)
|
self.handler = BTChipQTHandler(window)
|
||||||
if self.btchip_is_connected():
|
if self.btchip_is_connected():
|
||||||
if not self.wallet.check_proper_device():
|
if not self.wallet.check_proper_device():
|
||||||
QMessageBox.information(window, _('Error'), _("This wallet does not match your Ledger device"), _('OK'))
|
window.show_error(_("This wallet does not match your Ledger device"))
|
||||||
self.wallet.force_watching_only = True
|
self.wallet.force_watching_only = True
|
||||||
else:
|
else:
|
||||||
QMessageBox.information(window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK'))
|
window.show_error(_("Ledger device not detected.\nContinuing in watching-only mode."))
|
||||||
self.wallet.force_watching_only = True
|
self.wallet.force_watching_only = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton
|
from PyQt4.Qt import QVBoxLayout, QLabel, SIGNAL, QGridLayout, QInputDialog, QPushButton
|
||||||
import PyQt4.QtCore as QtCore
|
import PyQt4.QtCore as QtCore
|
||||||
from electrum_gui.qt.util import *
|
from electrum_gui.qt.util import *
|
||||||
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
|
from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow
|
||||||
|
@ -46,10 +46,7 @@ class TrezorQtHandler:
|
||||||
return self.passphrase
|
return self.passphrase
|
||||||
|
|
||||||
def pin_dialog(self):
|
def pin_dialog(self):
|
||||||
d = QDialog(None)
|
d = WindowModalDialog(self.win, _("Enter PIN"))
|
||||||
d.setModal(1)
|
|
||||||
d.setWindowTitle(_("Enter PIN"))
|
|
||||||
d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
|
||||||
matrix = PinMatrixWidget()
|
matrix = PinMatrixWidget()
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget(QLabel(self.message))
|
vbox.addWidget(QLabel(self.message))
|
||||||
|
@ -67,23 +64,18 @@ class TrezorQtHandler:
|
||||||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||||
else:
|
else:
|
||||||
assert type(self.win) is InstallWizard
|
assert type(self.win) is InstallWizard
|
||||||
from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog
|
from electrum_gui.qt.password_dialog import PasswordDialog
|
||||||
d = QDialog()
|
d = PasswordDialog(self.win, None, None, self.message, False)
|
||||||
d.setModal(1)
|
confirmed, p, passphrase = d.run()
|
||||||
d.setLayout(make_password_dialog(d, None, self.message, False))
|
|
||||||
confirmed, p, passphrase = run_password_dialog(d, None, None)
|
|
||||||
if not confirmed:
|
if not confirmed:
|
||||||
QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK'))
|
self.win.show_critical(_("Password request canceled"))
|
||||||
self.passphrase = None
|
self.passphrase = None
|
||||||
else:
|
else:
|
||||||
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else ''
|
||||||
self.done.set()
|
self.done.set()
|
||||||
|
|
||||||
def message_dialog(self):
|
def message_dialog(self):
|
||||||
self.d = QDialog()
|
self.d = WindowModalDialog(self.win, _('Please Check Trezor Device'))
|
||||||
self.d.setModal(1)
|
|
||||||
self.d.setWindowTitle('Please Check Trezor Device')
|
|
||||||
self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
|
||||||
l = QLabel(self.message)
|
l = QLabel(self.message)
|
||||||
vbox = QVBoxLayout(self.d)
|
vbox = QVBoxLayout(self.d)
|
||||||
vbox.addWidget(l)
|
vbox.addWidget(l)
|
||||||
|
@ -108,11 +100,11 @@ class Plugin(TrezorPlugin):
|
||||||
try:
|
try:
|
||||||
self.get_client().ping('t')
|
self.get_client().ping('t')
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
QMessageBox.information(window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK'))
|
window.show_error(_('Trezor device not detected.\nContinuing in watching-only mode.\nReason:\n' + str(e)))
|
||||||
self.wallet.force_watching_only = True
|
self.wallet.force_watching_only = True
|
||||||
return
|
return
|
||||||
if self.wallet.addresses() and not self.wallet.check_proper_device():
|
if self.wallet.addresses() and not self.wallet.check_proper_device():
|
||||||
QMessageBox.information(window, _('Error'), _("This wallet does not match your Trezor device"), _('OK'))
|
window.show_error(_("This wallet does not match your Trezor device"))
|
||||||
self.wallet.force_watching_only = True
|
self.wallet.force_watching_only = True
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
|
@ -171,7 +163,7 @@ class Plugin(TrezorPlugin):
|
||||||
return
|
return
|
||||||
get_label = lambda: self.get_client().features.label
|
get_label = lambda: self.get_client().features.label
|
||||||
update_label = lambda: current_label_label.setText("Label: %s" % get_label())
|
update_label = lambda: current_label_label.setText("Label: %s" % get_label())
|
||||||
d = QDialog()
|
d = WindowModalDialog(window, _("Trezor Settings"))
|
||||||
layout = QGridLayout(d)
|
layout = QGridLayout(d)
|
||||||
layout.addWidget(QLabel("Trezor Options"),0,0)
|
layout.addWidget(QLabel("Trezor Options"),0,0)
|
||||||
layout.addWidget(QLabel("ID:"),1,0)
|
layout.addWidget(QLabel("ID:"),1,0)
|
||||||
|
@ -194,7 +186,3 @@ class Plugin(TrezorPlugin):
|
||||||
layout.addWidget(current_label_label,3,0)
|
layout.addWidget(current_label_label,3,0)
|
||||||
layout.addWidget(change_label_button,3,1)
|
layout.addWidget(change_label_button,3,1)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,23 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Electrum - Lightweight Bitcoin Client
|
||||||
|
# Copyright (C) 2015 Thomas Voegtlin
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
|
@ -10,7 +29,19 @@ from electrum_gui.qt.main_window import StatusBarButton
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import hook
|
from electrum.plugins import hook
|
||||||
|
|
||||||
from trustedcoin import TrustedCoinPlugin
|
from trustedcoin import TrustedCoinPlugin, Wallet_2fa
|
||||||
|
|
||||||
|
def need_server(wallet, tx):
|
||||||
|
from electrum.account import BIP32_Account
|
||||||
|
# Detect if the server is needed
|
||||||
|
long_id, short_id = wallet.get_user_id()
|
||||||
|
xpub3 = wallet.master_public_keys['x3/']
|
||||||
|
for x in tx.inputs_to_sign():
|
||||||
|
if x[0:2] == 'ff':
|
||||||
|
xpub, sequence = BIP32_Account.parse_xpubkey(x)
|
||||||
|
if xpub == xpub3:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
class Plugin(TrustedCoinPlugin):
|
class Plugin(TrustedCoinPlugin):
|
||||||
|
|
||||||
|
@ -27,8 +58,7 @@ class Plugin(TrustedCoinPlugin):
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def auth_dialog(self, window):
|
def auth_dialog(self, window):
|
||||||
d = QDialog(window)
|
d = WindowModalDialog(window, _("Authorization"))
|
||||||
d.setModal(1)
|
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
pw = AmountEdit(None, is_int = True)
|
pw = AmountEdit(None, is_int = True)
|
||||||
msg = _('Please enter your Google Authenticator code')
|
msg = _('Please enter your Google Authenticator code')
|
||||||
|
@ -55,16 +85,18 @@ class Plugin(TrustedCoinPlugin):
|
||||||
self.print_error("twofactor: xpub3 not needed")
|
self.print_error("twofactor: xpub3 not needed")
|
||||||
window.wallet.auth_code = auth_code
|
window.wallet.auth_code = auth_code
|
||||||
|
|
||||||
|
def waiting_dialog(self, window, on_success=None):
|
||||||
|
task = partial(self.request_billing_info, window.wallet)
|
||||||
|
return WaitingDialog(window, 'Getting billing information...', task,
|
||||||
|
on_success=on_success)
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def abort_send(self, window):
|
def abort_send(self, window):
|
||||||
wallet = window.wallet
|
wallet = window.wallet
|
||||||
if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server():
|
if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server():
|
||||||
if wallet.billing_info is None:
|
if wallet.billing_info is None:
|
||||||
# request billing info before forming the transaction
|
# request billing info before forming the transaction
|
||||||
task = partial(self.request_billing_info, wallet)
|
waiting_dialog(self, window).wait()
|
||||||
waiting_dialog = WaitingDialog(window, 'please wait...', task)
|
|
||||||
waiting_dialog.start()
|
|
||||||
waiting_dialog.wait()
|
|
||||||
if wallet.billing_info is None:
|
if wallet.billing_info is None:
|
||||||
window.show_message('Could not contact server')
|
window.show_message('Could not contact server')
|
||||||
return True
|
return True
|
||||||
|
@ -72,9 +104,8 @@ class Plugin(TrustedCoinPlugin):
|
||||||
|
|
||||||
|
|
||||||
def settings_dialog(self, window):
|
def settings_dialog(self, window):
|
||||||
task = partial(self.request_billing_info, window.wallet)
|
on_success = partial(self.show_settings_dialog, window)
|
||||||
self.waiting_dialog = WaitingDialog(window, 'please wait...', task, partial(self.show_settings_dialog, window))
|
self.waiting_dialog(window, on_success)
|
||||||
self.waiting_dialog.start()
|
|
||||||
|
|
||||||
def show_settings_dialog(self, window, success):
|
def show_settings_dialog(self, window, success):
|
||||||
if not success:
|
if not success:
|
||||||
|
@ -82,8 +113,7 @@ class Plugin(TrustedCoinPlugin):
|
||||||
return
|
return
|
||||||
|
|
||||||
wallet = window.wallet
|
wallet = window.wallet
|
||||||
d = QDialog(window)
|
d = WindowModalDialog(window, _("TrustedCoin Information"))
|
||||||
d.setWindowTitle("TrustedCoin Information")
|
|
||||||
d.setMinimumSize(500, 200)
|
d.setMinimumSize(500, 200)
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
hbox = QHBoxLayout()
|
hbox = QHBoxLayout()
|
||||||
|
@ -238,7 +268,5 @@ class Plugin(TrustedCoinPlugin):
|
||||||
server.auth(_id, otp)
|
server.auth(_id, otp)
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK'))
|
window.show_message(_('Incorrect password'))
|
||||||
pw.setText('')
|
pw.setText('')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from threading import Thread
|
|
||||||
import socket
|
import socket
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -270,18 +269,6 @@ def make_billing_address(wallet, num):
|
||||||
address = public_key_to_bc_address( cK )
|
address = public_key_to_bc_address( cK )
|
||||||
return address
|
return address
|
||||||
|
|
||||||
def need_server(wallet, tx):
|
|
||||||
from electrum.account import BIP32_Account
|
|
||||||
# Detect if the server is needed
|
|
||||||
long_id, short_id = wallet.get_user_id()
|
|
||||||
xpub3 = wallet.master_public_keys['x3/']
|
|
||||||
for x in tx.inputs_to_sign():
|
|
||||||
if x[0:2] == 'ff':
|
|
||||||
xpub, sequence = BIP32_Account.parse_xpubkey(x)
|
|
||||||
if xpub == xpub3:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class TrustedCoinPlugin(BasePlugin):
|
class TrustedCoinPlugin(BasePlugin):
|
||||||
|
|
||||||
|
@ -318,8 +305,8 @@ class TrustedCoinPlugin(BasePlugin):
|
||||||
return
|
return
|
||||||
|
|
||||||
password = window.password_dialog()
|
password = window.password_dialog()
|
||||||
wallet.storage.put('seed_version', wallet.seed_version, True)
|
wallet.storage.put('seed_version', wallet.seed_version)
|
||||||
wallet.storage.put('use_encryption', password is not None, True)
|
wallet.storage.put('use_encryption', password is not None)
|
||||||
|
|
||||||
words = seed.split()
|
words = seed.split()
|
||||||
n = len(words)/2
|
n = len(words)/2
|
||||||
|
|
Loading…
Add table
Reference in a new issue