add lib changes from electrum 3.1.3
- Add electrum 3.1.3 changes to lib, lib/tests. - Get new electrum 3.1.3 lib/constants.py, add Zcash changes. - Get original electrum 3.1.3 lib/blockchain.py (will be - modified in next commits). - Remove debian/ lib-util patch (fixed in 3.1.3 code). - Remove lib/www (as in 3.1.3).
This commit is contained in:
parent
a05e9f21d7
commit
b75dbe65c9
|
@ -24,11 +24,21 @@
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from . import keystore
|
from . import keystore
|
||||||
from .keystore import bip44_derivation
|
from .keystore import bip44_derivation
|
||||||
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
|
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types
|
||||||
|
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
from .util import UserCancelled, InvalidPassword
|
||||||
|
|
||||||
|
# hardware device setup purpose
|
||||||
|
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
|
||||||
|
|
||||||
|
class ScriptTypeNotSupported(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
class BaseWizard(object):
|
class BaseWizard(object):
|
||||||
|
@ -59,7 +69,7 @@ class BaseWizard(object):
|
||||||
f = getattr(self, action)
|
f = getattr(self, action)
|
||||||
f(*args)
|
f(*args)
|
||||||
else:
|
else:
|
||||||
raise BaseException("unknown action", action)
|
raise Exception("unknown action", action)
|
||||||
|
|
||||||
def can_go_back(self):
|
def can_go_back(self):
|
||||||
return len(self.stack)>1
|
return len(self.stack)>1
|
||||||
|
@ -112,7 +122,7 @@ class BaseWizard(object):
|
||||||
choices = [
|
choices = [
|
||||||
('choose_seed_type', _('Create a new seed')),
|
('choose_seed_type', _('Create a new seed')),
|
||||||
('restore_from_seed', _('I already have a seed')),
|
('restore_from_seed', _('I already have a seed')),
|
||||||
('restore_from_key', _('Use public or private keys')),
|
('restore_from_key', _('Use a master key')),
|
||||||
]
|
]
|
||||||
if not self.is_kivy:
|
if not self.is_kivy:
|
||||||
choices.append(('choose_hw_device', _('Use a hardware device')))
|
choices.append(('choose_hw_device', _('Use a hardware device')))
|
||||||
|
@ -131,20 +141,26 @@ class BaseWizard(object):
|
||||||
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x)
|
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x)
|
||||||
title = _("Import Zcash Addresses")
|
title = _("Import Zcash Addresses")
|
||||||
message = _("Enter a list of Zcash addresses (this will create a watching-only wallet), or a list of private keys.")
|
message = _("Enter a list of Zcash addresses (this will create a watching-only wallet), or a list of private keys.")
|
||||||
self.add_xpub_dialog(title=title, message=message, run_next=self.on_import, is_valid=v)
|
self.add_xpub_dialog(title=title, message=message, run_next=self.on_import,
|
||||||
|
is_valid=v, allow_multi=True)
|
||||||
|
|
||||||
def on_import(self, text):
|
def on_import(self, text):
|
||||||
|
# create a temporary wallet and exploit that modifications
|
||||||
|
# will be reflected on self.storage
|
||||||
if keystore.is_address_list(text):
|
if keystore.is_address_list(text):
|
||||||
self.wallet = Imported_Wallet(self.storage)
|
w = Imported_Wallet(self.storage)
|
||||||
for x in text.split():
|
for x in text.split():
|
||||||
self.wallet.import_address(x)
|
w.import_address(x)
|
||||||
elif keystore.is_private_key_list(text):
|
elif keystore.is_private_key_list(text):
|
||||||
k = keystore.Imported_KeyStore({})
|
k = keystore.Imported_KeyStore({})
|
||||||
self.storage.put('keystore', k.dump())
|
self.storage.put('keystore', k.dump())
|
||||||
self.wallet = Imported_Wallet(self.storage)
|
w = Imported_Wallet(self.storage)
|
||||||
for x in text.split():
|
for x in keystore.get_private_keys(text):
|
||||||
self.wallet.import_private_key(x, None)
|
w.import_private_key(x, None)
|
||||||
self.terminate()
|
self.keystores.append(w.keystore)
|
||||||
|
else:
|
||||||
|
return self.terminate()
|
||||||
|
return self.run('create_wallet')
|
||||||
|
|
||||||
def restore_from_key(self):
|
def restore_from_key(self):
|
||||||
if self.wallet_type == 'standard':
|
if self.wallet_type == 'standard':
|
||||||
|
@ -163,7 +179,7 @@ class BaseWizard(object):
|
||||||
k = keystore.from_master_key(text)
|
k = keystore.from_master_key(text)
|
||||||
self.on_keystore(k)
|
self.on_keystore(k)
|
||||||
|
|
||||||
def choose_hw_device(self):
|
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
|
||||||
title = _('Hardware Keystore')
|
title = _('Hardware Keystore')
|
||||||
# check available plugins
|
# check available plugins
|
||||||
support = self.plugins.get_hardware_support()
|
support = self.plugins.get_hardware_support()
|
||||||
|
@ -172,68 +188,121 @@ class BaseWizard(object):
|
||||||
_('No hardware wallet support found on your system.'),
|
_('No hardware wallet support found on your system.'),
|
||||||
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
|
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
|
||||||
])
|
])
|
||||||
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
|
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
|
||||||
return
|
return
|
||||||
# scan devices
|
# scan devices
|
||||||
devices = []
|
devices = []
|
||||||
devmgr = self.plugins.device_manager
|
devmgr = self.plugins.device_manager
|
||||||
for name, description, plugin in support:
|
try:
|
||||||
try:
|
scanned_devices = devmgr.scan_devices()
|
||||||
# FIXME: side-effect: unpaired_device_info sets client.handler
|
except BaseException as e:
|
||||||
u = devmgr.unpaired_device_infos(None, plugin)
|
devmgr.print_error('error scanning devices: {}'.format(e))
|
||||||
except:
|
debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e)
|
||||||
devmgr.print_error("error", name)
|
else:
|
||||||
continue
|
debug_msg = ''
|
||||||
devices += list(map(lambda x: (name, x), u))
|
for name, description, plugin in support:
|
||||||
|
try:
|
||||||
|
# FIXME: side-effect: unpaired_device_info sets client.handler
|
||||||
|
u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices)
|
||||||
|
except BaseException as e:
|
||||||
|
devmgr.print_error('error getting device infos for {}: {}'.format(name, e))
|
||||||
|
debug_msg += ' {}:\n {}\n'.format(plugin.name, e)
|
||||||
|
continue
|
||||||
|
devices += list(map(lambda x: (name, x), u))
|
||||||
|
if not debug_msg:
|
||||||
|
debug_msg = ' {}'.format(_('No exceptions encountered.'))
|
||||||
if not devices:
|
if not devices:
|
||||||
msg = ''.join([
|
msg = ''.join([
|
||||||
_('No hardware device detected.') + '\n',
|
_('No hardware device detected.') + '\n',
|
||||||
_('To trigger a rescan, press \'Next\'.') + '\n\n',
|
_('To trigger a rescan, press \'Next\'.') + '\n\n',
|
||||||
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
|
_('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", and do "Remove device". Then, plug your device again.') + ' ',
|
||||||
_('On Linux, you might have to add a new permission to your udev rules.'),
|
_('On Linux, you might have to add a new permission to your udev rules.') + '\n\n',
|
||||||
|
_('Debug message') + '\n',
|
||||||
|
debug_msg
|
||||||
])
|
])
|
||||||
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device())
|
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
|
||||||
return
|
return
|
||||||
# select device
|
# select device
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
choices = []
|
choices = []
|
||||||
for name, info in devices:
|
for name, info in devices:
|
||||||
state = _("initialized") if info.initialized else _("wiped")
|
state = _("initialized") if info.initialized else _("wiped")
|
||||||
label = info.label or _("An unnamed %s")%name
|
label = info.label or _("An unnamed {}").format(name)
|
||||||
descr = "%s [%s, %s]" % (label, name, state)
|
descr = "%s [%s, %s]" % (label, name, state)
|
||||||
choices.append(((name, info), descr))
|
choices.append(((name, info), descr))
|
||||||
msg = _('Select a device') + ':'
|
msg = _('Select a device') + ':'
|
||||||
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_device)
|
self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
|
||||||
|
|
||||||
def on_device(self, name, device_info):
|
def on_device(self, name, device_info, *, purpose):
|
||||||
self.plugin = self.plugins.get_plugin(name)
|
self.plugin = self.plugins.get_plugin(name)
|
||||||
try:
|
try:
|
||||||
self.plugin.setup_device(device_info, self)
|
self.plugin.setup_device(device_info, self, purpose)
|
||||||
|
except OSError as e:
|
||||||
|
self.show_error(_('We encountered an error while connecting to your device:')
|
||||||
|
+ '\n' + str(e) + '\n'
|
||||||
|
+ _('To try to fix this, we will now re-pair with your device.') + '\n'
|
||||||
|
+ _('Please try again.'))
|
||||||
|
devmgr = self.plugins.device_manager
|
||||||
|
devmgr.unpair_id(device_info.device.id_)
|
||||||
|
self.choose_hw_device(purpose)
|
||||||
|
return
|
||||||
|
except UserCancelled:
|
||||||
|
self.choose_hw_device(purpose)
|
||||||
|
return
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_error(str(e))
|
self.show_error(str(e))
|
||||||
self.choose_hw_device()
|
self.choose_hw_device(purpose)
|
||||||
return
|
return
|
||||||
if self.wallet_type=='multisig':
|
if purpose == HWD_SETUP_NEW_WALLET:
|
||||||
# There is no general standard for HD multisig.
|
if self.wallet_type=='multisig':
|
||||||
# This is partially compatible with BIP45; assumes index=0
|
# There is no general standard for HD multisig.
|
||||||
self.on_hw_derivation(name, device_info, "m/45'/0")
|
# This is partially compatible with BIP45; assumes index=0
|
||||||
|
self.on_hw_derivation(name, device_info, "m/45'/0")
|
||||||
|
else:
|
||||||
|
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
|
||||||
|
self.derivation_dialog(f)
|
||||||
|
elif purpose == HWD_SETUP_DECRYPT_WALLET:
|
||||||
|
derivation = get_derivation_used_for_hw_device_encryption()
|
||||||
|
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self)
|
||||||
|
password = keystore.Xpub.get_pubkey_from_xpub(xpub, ())
|
||||||
|
try:
|
||||||
|
self.storage.decrypt(password)
|
||||||
|
except InvalidPassword:
|
||||||
|
# try to clear session so that user can type another passphrase
|
||||||
|
devmgr = self.plugins.device_manager
|
||||||
|
client = devmgr.client_by_id(device_info.device.id_)
|
||||||
|
if hasattr(client, 'clear_session'): # FIXME not all hw wallet plugins have this
|
||||||
|
client.clear_session()
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
f = lambda x: self.run('on_hw_derivation', name, device_info, str(x))
|
raise Exception('unknown purpose: %s' % purpose)
|
||||||
self.derivation_dialog(f)
|
|
||||||
|
|
||||||
def derivation_dialog(self, f):
|
def derivation_dialog(self, f):
|
||||||
default = bip44_derivation(0, False)
|
default = bip44_derivation(0, bip43_purpose=44)
|
||||||
message = '\n'.join([
|
message = '\n'.join([
|
||||||
_('Enter your wallet derivation here.'),
|
_('Enter your wallet derivation here.'),
|
||||||
_('If you are not sure what this is, leave this field unchanged.')
|
_('If you are not sure what this is, leave this field unchanged.')
|
||||||
])
|
])
|
||||||
self.line_dialog(run_next=f, title=_('Derivation'), message=message, default=default, test=bitcoin.is_bip32_derivation)
|
presets = (
|
||||||
|
('legacy BIP44', bip44_derivation(0, bip43_purpose=44)),
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self.line_dialog(run_next=f, title=_('Derivation'), message=message,
|
||||||
|
default=default, test=bitcoin.is_bip32_derivation,
|
||||||
|
presets=presets)
|
||||||
|
return
|
||||||
|
except ScriptTypeNotSupported as e:
|
||||||
|
self.show_error(e)
|
||||||
|
# let the user choose again
|
||||||
|
|
||||||
def on_hw_derivation(self, name, device_info, derivation):
|
def on_hw_derivation(self, name, device_info, derivation):
|
||||||
from .keystore import hardware_keystore
|
from .keystore import hardware_keystore
|
||||||
xtype = 'standard'
|
xtype = keystore.xtype_from_derivation(derivation)
|
||||||
try:
|
try:
|
||||||
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
|
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
|
||||||
|
except ScriptTypeNotSupported:
|
||||||
|
raise # this is handled in derivation_dialog
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_error(e)
|
self.show_error(e)
|
||||||
return
|
return
|
||||||
|
@ -277,7 +346,7 @@ class BaseWizard(object):
|
||||||
elif self.seed_type == 'old':
|
elif self.seed_type == 'old':
|
||||||
self.run('create_keystore', seed, '')
|
self.run('create_keystore', seed, '')
|
||||||
else:
|
else:
|
||||||
raise BaseException('Unknown seed type', self.seed_type)
|
raise Exception('Unknown seed type', self.seed_type)
|
||||||
|
|
||||||
def on_restore_bip39(self, seed, passphrase):
|
def on_restore_bip39(self, seed, passphrase):
|
||||||
f = lambda x: self.run('on_bip43', seed, passphrase, str(x))
|
f = lambda x: self.run('on_bip43', seed, passphrase, str(x))
|
||||||
|
@ -330,13 +399,45 @@ class BaseWizard(object):
|
||||||
self.run('create_wallet')
|
self.run('create_wallet')
|
||||||
|
|
||||||
def create_wallet(self):
|
def create_wallet(self):
|
||||||
if any(k.may_have_password() for k in self.keystores):
|
encrypt_keystore = any(k.may_have_password() for k in self.keystores)
|
||||||
self.request_password(run_next=self.on_password)
|
# note: the following condition ("if") is duplicated logic from
|
||||||
|
# wallet.get_available_storage_encryption_version()
|
||||||
|
if self.wallet_type == 'standard' and isinstance(self.keystores[0], keystore.Hardware_KeyStore):
|
||||||
|
# offer encrypting with a pw derived from the hw device
|
||||||
|
k = self.keystores[0]
|
||||||
|
try:
|
||||||
|
k.handler = self.plugin.create_handler(self)
|
||||||
|
password = k.get_password_for_storage_encryption()
|
||||||
|
except UserCancelled:
|
||||||
|
devmgr = self.plugins.device_manager
|
||||||
|
devmgr.unpair_xpub(k.xpub)
|
||||||
|
self.choose_hw_device()
|
||||||
|
return
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
self.show_error(str(e))
|
||||||
|
return
|
||||||
|
self.request_storage_encryption(
|
||||||
|
run_next=lambda encrypt_storage: self.on_password(
|
||||||
|
password,
|
||||||
|
encrypt_storage=encrypt_storage,
|
||||||
|
storage_enc_version=STO_EV_XPUB_PW,
|
||||||
|
encrypt_keystore=False))
|
||||||
else:
|
else:
|
||||||
self.on_password(None, False)
|
# prompt the user to set an arbitrary password
|
||||||
|
self.request_password(
|
||||||
|
run_next=lambda password, encrypt_storage: self.on_password(
|
||||||
|
password,
|
||||||
|
encrypt_storage=encrypt_storage,
|
||||||
|
storage_enc_version=STO_EV_USER_PW,
|
||||||
|
encrypt_keystore=encrypt_keystore),
|
||||||
|
force_disable_encrypt_cb=not encrypt_keystore)
|
||||||
|
|
||||||
def on_password(self, password, encrypt):
|
def on_password(self, password, *, encrypt_storage,
|
||||||
self.storage.set_password(password, encrypt)
|
storage_enc_version=STO_EV_USER_PW, encrypt_keystore):
|
||||||
|
self.storage.set_keystore_encryption(bool(password) and encrypt_keystore)
|
||||||
|
if encrypt_storage:
|
||||||
|
self.storage.set_password(password, enc_version=storage_enc_version)
|
||||||
for k in self.keystores:
|
for k in self.keystores:
|
||||||
if k.may_have_password():
|
if k.may_have_password():
|
||||||
k.update_password(None, password)
|
k.update_password(None, password)
|
||||||
|
@ -352,18 +453,21 @@ class BaseWizard(object):
|
||||||
self.storage.write()
|
self.storage.write()
|
||||||
self.wallet = Multisig_Wallet(self.storage)
|
self.wallet = Multisig_Wallet(self.storage)
|
||||||
self.run('create_addresses')
|
self.run('create_addresses')
|
||||||
|
elif self.wallet_type == 'imported':
|
||||||
|
if len(self.keystores) > 0:
|
||||||
|
keys = self.keystores[0].dump()
|
||||||
|
self.storage.put('keystore', keys)
|
||||||
|
self.wallet = Imported_Wallet(self.storage)
|
||||||
|
self.wallet.storage.write()
|
||||||
|
self.terminate()
|
||||||
|
|
||||||
def show_xpub_and_add_cosigners(self, xpub):
|
def show_xpub_and_add_cosigners(self, xpub):
|
||||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
|
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
|
||||||
|
|
||||||
def on_cosigner(self, text, password, i):
|
|
||||||
k = keystore.from_master_key(text, password)
|
|
||||||
self.on_keystore(k)
|
|
||||||
|
|
||||||
def choose_seed_type(self):
|
def choose_seed_type(self):
|
||||||
title = _('Choose Seed type')
|
title = _('Choose Seed type')
|
||||||
message = ' '.join([
|
message = ' '.join([
|
||||||
"The type of addresses used by your wallet will depend on your seed.",
|
_("The type of addresses used by your wallet will depend on your seed."),
|
||||||
])
|
])
|
||||||
choices = [
|
choices = [
|
||||||
('create_standard_seed', _('Standard')),
|
('create_standard_seed', _('Standard')),
|
||||||
|
@ -408,5 +512,5 @@ class BaseWizard(object):
|
||||||
self.wallet.synchronize()
|
self.wallet.synchronize()
|
||||||
self.wallet.storage.write()
|
self.wallet.storage.write()
|
||||||
self.terminate()
|
self.terminate()
|
||||||
msg = _("Electrum-Zcash is generating your addresses, please wait.")
|
msg = _("Electrum-Zcash is generating your addresses, please wait...")
|
||||||
self.waiting_dialog(task, msg)
|
self.waiting_dialog(task, msg)
|
||||||
|
|
236
lib/bitcoin.py
236
lib/bitcoin.py
|
@ -32,72 +32,18 @@ import json
|
||||||
import ecdsa
|
import ecdsa
|
||||||
import pyaes
|
import pyaes
|
||||||
|
|
||||||
from .util import bfh, bh2u, to_string
|
from .util import bfh, bh2u, to_string, BitcoinException
|
||||||
from . import version
|
from . import version
|
||||||
from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
|
from .util import print_error, InvalidPassword, assert_bytes, to_bytes, inv_dict
|
||||||
|
from . import constants
|
||||||
|
|
||||||
def read_json_dict(filename):
|
|
||||||
path = os.path.join(os.path.dirname(__file__), filename)
|
|
||||||
try:
|
|
||||||
with open(path, 'r') as f:
|
|
||||||
r = json.loads(f.read())
|
|
||||||
except:
|
|
||||||
r = {}
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Version numbers for BIP32 extended keys
|
|
||||||
# standard: xprv, xpub
|
|
||||||
XPRV_HEADERS = {
|
|
||||||
'standard': 0x0488ade4,
|
|
||||||
}
|
|
||||||
XPUB_HEADERS = {
|
|
||||||
'standard': 0x0488b21e,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkConstants:
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_mainnet(cls):
|
|
||||||
cls.TESTNET = False
|
|
||||||
cls.WIF_PREFIX = 0x80
|
|
||||||
cls.ADDRTYPE_P2PKH = bytes.fromhex('1CB8')
|
|
||||||
cls.ADDRTYPE_P2SH = bytes.fromhex('1CBD')
|
|
||||||
cls.HEADERS_URL = 'https://github.com/zebra-lucky/electrum-zcash/releases/download/3.0.6/blockchain_headers'
|
|
||||||
cls.GENESIS = '00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08'
|
|
||||||
cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'}
|
|
||||||
cls.DEFAULT_SERVERS = read_json_dict('servers.json')
|
|
||||||
XPRV_HEADERS['standard'] = 0x0488ade4
|
|
||||||
XPUB_HEADERS['standard'] = 0x0488b21e
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_testnet(cls):
|
|
||||||
cls.TESTNET = True
|
|
||||||
cls.WIF_PREFIX = 0xEF
|
|
||||||
cls.ADDRTYPE_P2PKH = bytes.fromhex('1D25')
|
|
||||||
cls.ADDRTYPE_P2SH = bytes.fromhex('1CBA')
|
|
||||||
cls.HEADERS_URL = 'https://github.com/zebra-lucky/electrum-zcash/releases/download/3.0.6/blockchain_headers_testnet'
|
|
||||||
cls.GENESIS = '05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38'
|
|
||||||
cls.DEFAULT_PORTS = {'t':'51001', 's':'51002'}
|
|
||||||
cls.DEFAULT_SERVERS = read_json_dict('servers_testnet.json')
|
|
||||||
XPRV_HEADERS['standard'] = 0x04358394
|
|
||||||
XPUB_HEADERS['standard'] = 0x043587CF
|
|
||||||
|
|
||||||
|
|
||||||
NetworkConstants.set_mainnet()
|
|
||||||
|
|
||||||
################################## transactions
|
################################## transactions
|
||||||
|
|
||||||
MAX_FEE_RATE = 10000
|
|
||||||
FEE_TARGETS = [25, 10, 5, 2]
|
|
||||||
|
|
||||||
COINBASE_MATURITY = 100
|
COINBASE_MATURITY = 100
|
||||||
COIN = 100000000
|
COIN = 100000000
|
||||||
|
|
||||||
# supported types of transction outputs
|
# supported types of transaction outputs
|
||||||
TYPE_ADDRESS = 0
|
TYPE_ADDRESS = 0
|
||||||
TYPE_PUBKEY = 1
|
TYPE_PUBKEY = 1
|
||||||
TYPE_SCRIPT = 2
|
TYPE_SCRIPT = 2
|
||||||
|
@ -196,7 +142,11 @@ def rev_hex(s):
|
||||||
|
|
||||||
|
|
||||||
def int_to_hex(i, length=1):
|
def int_to_hex(i, length=1):
|
||||||
assert isinstance(i, int)
|
if not isinstance(i, int):
|
||||||
|
raise TypeError('{} instead of int'.format(i))
|
||||||
|
if i < 0:
|
||||||
|
# two's complement
|
||||||
|
i = pow(256, length) + i
|
||||||
s = hex(i)[2:].rstrip('L')
|
s = hex(i)[2:].rstrip('L')
|
||||||
s = "0"*(2*length - len(s)) + s
|
s = "0"*(2*length - len(s)) + s
|
||||||
return rev_hex(s)
|
return rev_hex(s)
|
||||||
|
@ -215,11 +165,11 @@ def var_int(i):
|
||||||
|
|
||||||
|
|
||||||
def op_push(i):
|
def op_push(i):
|
||||||
if i<0x4c:
|
if i<0x4c: # OP_PUSHDATA1
|
||||||
return int_to_hex(i)
|
return int_to_hex(i)
|
||||||
elif i<0xff:
|
elif i<=0xff:
|
||||||
return '4c' + int_to_hex(i)
|
return '4c' + int_to_hex(i)
|
||||||
elif i<0xffff:
|
elif i<=0xffff:
|
||||||
return '4d' + int_to_hex(i,2)
|
return '4d' + int_to_hex(i,2)
|
||||||
else:
|
else:
|
||||||
return '4e' + int_to_hex(i,4)
|
return '4e' + int_to_hex(i,4)
|
||||||
|
@ -322,11 +272,15 @@ def b58_address_to_hash160(addr):
|
||||||
return _bytes[0:2], _bytes[2:22]
|
return _bytes[0:2], _bytes[2:22]
|
||||||
|
|
||||||
|
|
||||||
def hash160_to_p2pkh(h160):
|
def hash160_to_p2pkh(h160, *, net=None):
|
||||||
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2PKH)
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
|
return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH)
|
||||||
|
|
||||||
def hash160_to_p2sh(h160):
|
def hash160_to_p2sh(h160, *, net=None):
|
||||||
return hash160_to_b58_address(h160, NetworkConstants.ADDRTYPE_P2SH)
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
|
return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH)
|
||||||
|
|
||||||
def public_key_to_p2pkh(public_key):
|
def public_key_to_p2pkh(public_key):
|
||||||
return hash160_to_p2pkh(hash_160(public_key))
|
return hash160_to_p2pkh(hash_160(public_key))
|
||||||
|
@ -344,24 +298,26 @@ def redeem_script_to_address(txin_type, redeem_script):
|
||||||
raise NotImplementedError(txin_type)
|
raise NotImplementedError(txin_type)
|
||||||
|
|
||||||
|
|
||||||
def script_to_address(script):
|
def script_to_address(script, *, net=None):
|
||||||
from .transaction import get_address_from_output_script
|
from .transaction import get_address_from_output_script
|
||||||
t, addr = get_address_from_output_script(bfh(script))
|
t, addr = get_address_from_output_script(bfh(script), net=net)
|
||||||
assert t == TYPE_ADDRESS
|
assert t == TYPE_ADDRESS
|
||||||
return addr
|
return addr
|
||||||
|
|
||||||
def address_to_script(addr):
|
def address_to_script(addr, *, net=None):
|
||||||
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
addrtype, hash_160 = b58_address_to_hash160(addr)
|
addrtype, hash_160 = b58_address_to_hash160(addr)
|
||||||
if addrtype == NetworkConstants.ADDRTYPE_P2PKH:
|
if addrtype == net.ADDRTYPE_P2PKH:
|
||||||
script = '76a9' # op_dup, op_hash_160
|
script = '76a9' # op_dup, op_hash_160
|
||||||
script += push_script(bh2u(hash_160))
|
script += push_script(bh2u(hash_160))
|
||||||
script += '88ac' # op_equalverify, op_checksig
|
script += '88ac' # op_equalverify, op_checksig
|
||||||
elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
|
elif addrtype == net.ADDRTYPE_P2SH:
|
||||||
script = 'a9' # op_hash_160
|
script = 'a9' # op_hash_160
|
||||||
script += push_script(bh2u(hash_160))
|
script += push_script(bh2u(hash_160))
|
||||||
script += '87' # op_equal
|
script += '87' # op_equal
|
||||||
else:
|
else:
|
||||||
raise BaseException('unknown address type')
|
raise BitcoinException('unknown address type: {}'.format(addrtype))
|
||||||
return script
|
return script
|
||||||
|
|
||||||
def address_to_scripthash(addr):
|
def address_to_scripthash(addr):
|
||||||
|
@ -387,7 +343,8 @@ assert len(__b43chars) == 43
|
||||||
def base_encode(v, base):
|
def base_encode(v, base):
|
||||||
""" encode v, which is a string of bytes, to base58."""
|
""" encode v, which is a string of bytes, to base58."""
|
||||||
assert_bytes(v)
|
assert_bytes(v)
|
||||||
assert base in (58, 43)
|
if base not in (58, 43):
|
||||||
|
raise ValueError('not supported base: {}'.format(base))
|
||||||
chars = __b58chars
|
chars = __b58chars
|
||||||
if base == 43:
|
if base == 43:
|
||||||
chars = __b43chars
|
chars = __b43chars
|
||||||
|
@ -417,13 +374,17 @@ def base_decode(v, length, base):
|
||||||
""" decode v into a string of len bytes."""
|
""" decode v into a string of len bytes."""
|
||||||
# assert_bytes(v)
|
# assert_bytes(v)
|
||||||
v = to_bytes(v, 'ascii')
|
v = to_bytes(v, 'ascii')
|
||||||
assert base in (58, 43)
|
if base not in (58, 43):
|
||||||
|
raise ValueError('not supported base: {}'.format(base))
|
||||||
chars = __b58chars
|
chars = __b58chars
|
||||||
if base == 43:
|
if base == 43:
|
||||||
chars = __b43chars
|
chars = __b43chars
|
||||||
long_value = 0
|
long_value = 0
|
||||||
for (i, c) in enumerate(v[::-1]):
|
for (i, c) in enumerate(v[::-1]):
|
||||||
long_value += chars.find(bytes([c])) * (base**i)
|
digit = chars.find(bytes([c]))
|
||||||
|
if digit == -1:
|
||||||
|
raise ValueError('Forbidden character {} for base {}'.format(c, base))
|
||||||
|
long_value += digit * (base**i)
|
||||||
result = bytearray()
|
result = bytearray()
|
||||||
while long_value >= 256:
|
while long_value >= 256:
|
||||||
div, mod = divmod(long_value, 256)
|
div, mod = divmod(long_value, 256)
|
||||||
|
@ -443,6 +404,10 @@ def base_decode(v, length, base):
|
||||||
return bytes(result)
|
return bytes(result)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidChecksum(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def EncodeBase58Check(vchIn):
|
def EncodeBase58Check(vchIn):
|
||||||
hash = Hash(vchIn)
|
hash = Hash(vchIn)
|
||||||
return base_encode(vchIn + hash[0:4], base=58)
|
return base_encode(vchIn + hash[0:4], base=58)
|
||||||
|
@ -455,37 +420,64 @@ def DecodeBase58Check(psz):
|
||||||
hash = Hash(key)
|
hash = Hash(key)
|
||||||
cs32 = hash[0:4]
|
cs32 = hash[0:4]
|
||||||
if cs32 != csum:
|
if cs32 != csum:
|
||||||
return None
|
raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum)))
|
||||||
else:
|
else:
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# backwards compat
|
||||||
|
# extended WIF for segwit (used in 3.0.x; but still used internally)
|
||||||
|
# the keys in this dict should be a superset of what Imported Wallets can import
|
||||||
SCRIPT_TYPES = {
|
SCRIPT_TYPES = {
|
||||||
'p2pkh':0,
|
'p2pkh':0,
|
||||||
'p2sh':5,
|
'p2sh':5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def serialize_privkey(secret, compressed, txin_type):
|
def serialize_privkey(secret, compressed, txin_type, internal_use=False):
|
||||||
prefix = bytes([(SCRIPT_TYPES[txin_type]+NetworkConstants.WIF_PREFIX)&255])
|
if internal_use:
|
||||||
|
prefix = bytes([(SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255])
|
||||||
|
else:
|
||||||
|
prefix = bytes([constants.net.WIF_PREFIX])
|
||||||
suffix = b'\01' if compressed else b''
|
suffix = b'\01' if compressed else b''
|
||||||
vchIn = prefix + secret + suffix
|
vchIn = prefix + secret + suffix
|
||||||
return EncodeBase58Check(vchIn)
|
base58_wif = EncodeBase58Check(vchIn)
|
||||||
|
if internal_use:
|
||||||
|
return base58_wif
|
||||||
|
else:
|
||||||
|
return '{}:{}'.format(txin_type, base58_wif)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_privkey(key):
|
def deserialize_privkey(key):
|
||||||
# whether the pubkey is compressed should be visible from the keystore
|
|
||||||
vch = DecodeBase58Check(key)
|
|
||||||
if is_minikey(key):
|
if is_minikey(key):
|
||||||
return 'p2pkh', minikey_to_private_key(key), True
|
return 'p2pkh', minikey_to_private_key(key), True
|
||||||
elif vch:
|
|
||||||
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - NetworkConstants.WIF_PREFIX]
|
txin_type = None
|
||||||
assert len(vch) in [33, 34]
|
if ':' in key:
|
||||||
compressed = len(vch) == 34
|
txin_type, key = key.split(sep=':', maxsplit=1)
|
||||||
return txin_type, vch[1:33], compressed
|
if txin_type not in SCRIPT_TYPES:
|
||||||
|
raise BitcoinException('unknown script type: {}'.format(txin_type))
|
||||||
|
try:
|
||||||
|
vch = DecodeBase58Check(key)
|
||||||
|
except BaseException:
|
||||||
|
neutered_privkey = str(key)[:3] + '..' + str(key)[-2:]
|
||||||
|
raise BitcoinException("cannot deserialize privkey {}"
|
||||||
|
.format(neutered_privkey))
|
||||||
|
|
||||||
|
if txin_type is None:
|
||||||
|
# keys exported in version 3.0.x encoded script type in first byte
|
||||||
|
txin_type = inv_dict(SCRIPT_TYPES)[vch[0] - constants.net.WIF_PREFIX]
|
||||||
else:
|
else:
|
||||||
raise BaseException("cannot deserialize", key)
|
# all other keys must have a fixed first byte
|
||||||
|
if vch[0] != constants.net.WIF_PREFIX:
|
||||||
|
raise BitcoinException('invalid prefix ({}) for WIF key'.format(vch[0]))
|
||||||
|
|
||||||
|
if len(vch) not in [33, 34]:
|
||||||
|
raise BitcoinException('invalid vch len for WIF key: {}'.format(len(vch)))
|
||||||
|
compressed = len(vch) == 34
|
||||||
|
return txin_type, vch[1:33], compressed
|
||||||
|
|
||||||
|
|
||||||
def regenerate_key(pk):
|
def regenerate_key(pk):
|
||||||
assert len(pk) == 32
|
assert len(pk) == 32
|
||||||
|
@ -519,7 +511,7 @@ def is_b58_address(addr):
|
||||||
addrtype, h = b58_address_to_hash160(addr)
|
addrtype, h = b58_address_to_hash160(addr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
if addrtype not in [NetworkConstants.ADDRTYPE_P2PKH, NetworkConstants.ADDRTYPE_P2SH]:
|
if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]:
|
||||||
return False
|
return False
|
||||||
return addr == hash160_to_b58_address(h, addrtype)
|
return addr == hash160_to_b58_address(h, addrtype)
|
||||||
|
|
||||||
|
@ -582,8 +574,8 @@ def verify_message(address, sig, message):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def encrypt_message(message, pubkey):
|
def encrypt_message(message, pubkey, magic=b'BIE1'):
|
||||||
return EC_KEY.encrypt_message(message, bfh(pubkey))
|
return EC_KEY.encrypt_message(message, bfh(pubkey), magic)
|
||||||
|
|
||||||
|
|
||||||
def chunks(l, n):
|
def chunks(l, n):
|
||||||
|
@ -728,7 +720,7 @@ class EC_KEY(object):
|
||||||
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
|
# ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def encrypt_message(self, message, pubkey):
|
def encrypt_message(self, message, pubkey, magic=b'BIE1'):
|
||||||
assert_bytes(message)
|
assert_bytes(message)
|
||||||
|
|
||||||
pk = ser_to_point(pubkey)
|
pk = ser_to_point(pubkey)
|
||||||
|
@ -742,20 +734,20 @@ class EC_KEY(object):
|
||||||
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
iv, key_e, key_m = key[0:16], key[16:32], key[32:]
|
||||||
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
|
ciphertext = aes_encrypt_with_iv(key_e, iv, message)
|
||||||
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
|
ephemeral_pubkey = bfh(ephemeral.get_public_key(compressed=True))
|
||||||
encrypted = b'BIE1' + ephemeral_pubkey + ciphertext
|
encrypted = magic + ephemeral_pubkey + ciphertext
|
||||||
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
|
mac = hmac.new(key_m, encrypted, hashlib.sha256).digest()
|
||||||
|
|
||||||
return base64.b64encode(encrypted + mac)
|
return base64.b64encode(encrypted + mac)
|
||||||
|
|
||||||
def decrypt_message(self, encrypted):
|
def decrypt_message(self, encrypted, magic=b'BIE1'):
|
||||||
encrypted = base64.b64decode(encrypted)
|
encrypted = base64.b64decode(encrypted)
|
||||||
if len(encrypted) < 85:
|
if len(encrypted) < 85:
|
||||||
raise Exception('invalid ciphertext: length')
|
raise Exception('invalid ciphertext: length')
|
||||||
magic = encrypted[:4]
|
magic_found = encrypted[:4]
|
||||||
ephemeral_pubkey = encrypted[4:37]
|
ephemeral_pubkey = encrypted[4:37]
|
||||||
ciphertext = encrypted[37:-32]
|
ciphertext = encrypted[37:-32]
|
||||||
mac = encrypted[-32:]
|
mac = encrypted[-32:]
|
||||||
if magic != b'BIE1':
|
if magic_found != magic:
|
||||||
raise Exception('invalid ciphertext: invalid magic bytes')
|
raise Exception('invalid ciphertext: invalid magic bytes')
|
||||||
try:
|
try:
|
||||||
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
ephemeral_pubkey = ser_to_point(ephemeral_pubkey)
|
||||||
|
@ -831,47 +823,60 @@ def _CKD_pub(cK, c, s):
|
||||||
return cK_n, c_n
|
return cK_n, c_n
|
||||||
|
|
||||||
|
|
||||||
def xprv_header(xtype):
|
def xprv_header(xtype, *, net=None):
|
||||||
return bfh("%08x" % XPRV_HEADERS[xtype])
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
|
return bfh("%08x" % net.XPRV_HEADERS[xtype])
|
||||||
|
|
||||||
|
|
||||||
def xpub_header(xtype):
|
def xpub_header(xtype, *, net=None):
|
||||||
return bfh("%08x" % XPUB_HEADERS[xtype])
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
|
return bfh("%08x" % net.XPUB_HEADERS[xtype])
|
||||||
|
|
||||||
|
|
||||||
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
|
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
|
||||||
xprv = xprv_header(xtype) + bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
|
child_number=b'\x00'*4, *, net=None):
|
||||||
|
xprv = xprv_header(xtype, net=net) \
|
||||||
|
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
|
||||||
return EncodeBase58Check(xprv)
|
return EncodeBase58Check(xprv)
|
||||||
|
|
||||||
|
|
||||||
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4, child_number=b'\x00'*4):
|
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
|
||||||
xpub = xpub_header(xtype) + bytes([depth]) + fingerprint + child_number + c + cK
|
child_number=b'\x00'*4, *, net=None):
|
||||||
|
xpub = xpub_header(xtype, net=net) \
|
||||||
|
+ bytes([depth]) + fingerprint + child_number + c + cK
|
||||||
return EncodeBase58Check(xpub)
|
return EncodeBase58Check(xpub)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_xkey(xkey, prv):
|
def deserialize_xkey(xkey, prv, *, net=None):
|
||||||
|
if net is None:
|
||||||
|
net = constants.net
|
||||||
xkey = DecodeBase58Check(xkey)
|
xkey = DecodeBase58Check(xkey)
|
||||||
if len(xkey) != 78:
|
if len(xkey) != 78:
|
||||||
raise BaseException('Invalid length')
|
raise BitcoinException('Invalid length for extended key: {}'
|
||||||
|
.format(len(xkey)))
|
||||||
depth = xkey[4]
|
depth = xkey[4]
|
||||||
fingerprint = xkey[5:9]
|
fingerprint = xkey[5:9]
|
||||||
child_number = xkey[9:13]
|
child_number = xkey[9:13]
|
||||||
c = xkey[13:13+32]
|
c = xkey[13:13+32]
|
||||||
header = int('0x' + bh2u(xkey[0:4]), 16)
|
header = int('0x' + bh2u(xkey[0:4]), 16)
|
||||||
headers = XPRV_HEADERS if prv else XPUB_HEADERS
|
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
|
||||||
if header not in headers.values():
|
if header not in headers.values():
|
||||||
raise BaseException('Invalid xpub format', hex(header))
|
raise BitcoinException('Invalid extended key format: {}'
|
||||||
|
.format(hex(header)))
|
||||||
xtype = list(headers.keys())[list(headers.values()).index(header)]
|
xtype = list(headers.keys())[list(headers.values()).index(header)]
|
||||||
n = 33 if prv else 32
|
n = 33 if prv else 32
|
||||||
K_or_k = xkey[13+n:]
|
K_or_k = xkey[13+n:]
|
||||||
return xtype, depth, fingerprint, child_number, c, K_or_k
|
return xtype, depth, fingerprint, child_number, c, K_or_k
|
||||||
|
|
||||||
|
|
||||||
def deserialize_xpub(xkey):
|
|
||||||
return deserialize_xkey(xkey, False)
|
|
||||||
|
|
||||||
def deserialize_xprv(xkey):
|
def deserialize_xpub(xkey, *, net=None):
|
||||||
return deserialize_xkey(xkey, True)
|
return deserialize_xkey(xkey, False, net=net)
|
||||||
|
|
||||||
|
def deserialize_xprv(xkey, *, net=None):
|
||||||
|
return deserialize_xkey(xkey, True, net=net)
|
||||||
|
|
||||||
def xpub_type(x):
|
def xpub_type(x):
|
||||||
return deserialize_xpub(x)[0]
|
return deserialize_xpub(x)[0]
|
||||||
|
@ -915,7 +920,8 @@ def xpub_from_pubkey(xtype, cK):
|
||||||
|
|
||||||
|
|
||||||
def bip32_derivation(s):
|
def bip32_derivation(s):
|
||||||
assert s.startswith('m/')
|
if not s.startswith('m/'):
|
||||||
|
raise ValueError('invalid bip32 derivation path: {}'.format(s))
|
||||||
s = s[2:]
|
s = s[2:]
|
||||||
for n in s.split('/'):
|
for n in s.split('/'):
|
||||||
if n == '': continue
|
if n == '': continue
|
||||||
|
@ -930,7 +936,9 @@ def is_bip32_derivation(x):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def bip32_private_derivation(xprv, branch, sequence):
|
def bip32_private_derivation(xprv, branch, sequence):
|
||||||
assert sequence.startswith(branch)
|
if not sequence.startswith(branch):
|
||||||
|
raise ValueError('incompatible branch ({}) and sequence ({})'
|
||||||
|
.format(branch, sequence))
|
||||||
if branch == sequence:
|
if branch == sequence:
|
||||||
return xprv, xpub_from_xprv(xprv)
|
return xprv, xpub_from_xprv(xprv)
|
||||||
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
|
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
|
||||||
|
@ -952,7 +960,9 @@ def bip32_private_derivation(xprv, branch, sequence):
|
||||||
|
|
||||||
def bip32_public_derivation(xpub, branch, sequence):
|
def bip32_public_derivation(xpub, branch, sequence):
|
||||||
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
|
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
|
||||||
assert sequence.startswith(branch)
|
if not sequence.startswith(branch):
|
||||||
|
raise ValueError('incompatible branch ({}) and sequence ({})'
|
||||||
|
.format(branch, sequence))
|
||||||
sequence = sequence[len(branch):]
|
sequence = sequence[len(branch):]
|
||||||
for n in sequence.split('/'):
|
for n in sequence.split('/'):
|
||||||
if n == '': continue
|
if n == '': continue
|
||||||
|
|
|
@ -25,79 +25,33 @@ import threading
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
|
from . import constants
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
|
|
||||||
|
MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
|
||||||
HDR_LEN = 1487
|
|
||||||
CHUNK_LEN = 100
|
|
||||||
|
|
||||||
POW_LIMIT = 0x0007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
|
||||||
POW_AVERAGING_WINDOW = 17
|
|
||||||
POW_MEDIAN_BLOCK_SPAN = 11
|
|
||||||
POW_MAX_ADJUST_DOWN = 32
|
|
||||||
POW_MAX_ADJUST_UP = 16
|
|
||||||
POW_DAMPING_FACTOR = 4
|
|
||||||
POW_TARGET_SPACING = 150
|
|
||||||
|
|
||||||
AVERAGING_WINDOW_TIMESPAN = POW_AVERAGING_WINDOW * POW_TARGET_SPACING
|
|
||||||
|
|
||||||
MIN_ACTUAL_TIMESPAN = AVERAGING_WINDOW_TIMESPAN * \
|
|
||||||
(100 - POW_MAX_ADJUST_UP) // 100
|
|
||||||
|
|
||||||
MAX_ACTUAL_TIMESPAN = AVERAGING_WINDOW_TIMESPAN * \
|
|
||||||
(100 + POW_MAX_ADJUST_DOWN) // 100
|
|
||||||
|
|
||||||
|
|
||||||
def bits_to_target(bits):
|
|
||||||
"""Convert a compact representation to a hex target."""
|
|
||||||
MM = 256 * 256 * 256
|
|
||||||
a = bits % MM
|
|
||||||
if a < 0x8000:
|
|
||||||
a *= 256
|
|
||||||
target = a * pow(2, 8 * (bits // MM - 3))
|
|
||||||
return target
|
|
||||||
|
|
||||||
def target_to_bits(target):
|
|
||||||
"""Convert a target to compact representation."""
|
|
||||||
MM = 256 * 256 * 256
|
|
||||||
c = ('%064X' % target)[2:]
|
|
||||||
i = 31
|
|
||||||
while c[0:2] == '00':
|
|
||||||
c = c[2:]
|
|
||||||
i -= 1
|
|
||||||
|
|
||||||
c = int('0x%s' % c[0:6], 16)
|
|
||||||
if c >= 0x800000:
|
|
||||||
c //= 256
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
new_bits = c + MM * i
|
|
||||||
return new_bits
|
|
||||||
|
|
||||||
def serialize_header(res):
|
def serialize_header(res):
|
||||||
s = int_to_hex(res.get('version'), 4) \
|
s = int_to_hex(res.get('version'), 4) \
|
||||||
+ rev_hex(res.get('prev_block_hash')) \
|
+ rev_hex(res.get('prev_block_hash')) \
|
||||||
+ rev_hex(res.get('merkle_root')) \
|
+ rev_hex(res.get('merkle_root')) \
|
||||||
+ rev_hex(res.get('reserved_hash')) \
|
|
||||||
+ 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) \
|
||||||
+ rev_hex(res.get('nonce')) \
|
+ int_to_hex(int(res.get('nonce')), 4)
|
||||||
+ rev_hex(res.get('sol_size')) \
|
|
||||||
+ rev_hex(res.get('solution'))
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def deserialize_header(s, height):
|
def deserialize_header(s, height):
|
||||||
|
if not s:
|
||||||
|
raise Exception('Invalid header: {}'.format(s))
|
||||||
|
if len(s) != 80:
|
||||||
|
raise Exception('Invalid header length: {}'.format(len(s)))
|
||||||
hex_to_int = lambda s: int('0x' + bh2u(s[::-1]), 16)
|
hex_to_int = lambda s: int('0x' + bh2u(s[::-1]), 16)
|
||||||
h = {}
|
h = {}
|
||||||
h['version'] = hex_to_int(s[0:4])
|
h['version'] = hex_to_int(s[0:4])
|
||||||
h['prev_block_hash'] = hash_encode(s[4:36])
|
h['prev_block_hash'] = hash_encode(s[4:36])
|
||||||
h['merkle_root'] = hash_encode(s[36:68])
|
h['merkle_root'] = hash_encode(s[36:68])
|
||||||
h['reserved_hash'] = hash_encode(s[68:100])
|
h['timestamp'] = hex_to_int(s[68:72])
|
||||||
h['timestamp'] = hex_to_int(s[100:104])
|
h['bits'] = hex_to_int(s[72:76])
|
||||||
h['bits'] = hex_to_int(s[104:108])
|
h['nonce'] = hex_to_int(s[76:80])
|
||||||
h['nonce'] = hash_encode(s[108:140])
|
|
||||||
h['sol_size'] = hash_encode(s[140:143])
|
|
||||||
h['solution'] = hash_encode(s[143:1487])
|
|
||||||
h['block_height'] = height
|
h['block_height'] = height
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
@ -122,7 +76,11 @@ def read_blockchains(config):
|
||||||
checkpoint = int(filename.split('_')[2])
|
checkpoint = int(filename.split('_')[2])
|
||||||
parent_id = int(filename.split('_')[1])
|
parent_id = int(filename.split('_')[1])
|
||||||
b = Blockchain(config, checkpoint, parent_id)
|
b = Blockchain(config, checkpoint, parent_id)
|
||||||
blockchains[b.checkpoint] = b
|
h = b.read_header(b.checkpoint)
|
||||||
|
if b.parent().can_connect(h, check_height=False):
|
||||||
|
blockchains[b.checkpoint] = b
|
||||||
|
else:
|
||||||
|
util.print_error("cannot connect", filename)
|
||||||
return blockchains
|
return blockchains
|
||||||
|
|
||||||
def check_header(header):
|
def check_header(header):
|
||||||
|
@ -149,6 +107,7 @@ class Blockchain(util.PrintError):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.catch_up = None # interface catching up
|
self.catch_up = None # interface catching up
|
||||||
self.checkpoint = checkpoint
|
self.checkpoint = checkpoint
|
||||||
|
self.checkpoints = constants.net.CHECKPOINTS
|
||||||
self.parent_id = parent_id
|
self.parent_id = parent_id
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -192,35 +151,29 @@ class Blockchain(util.PrintError):
|
||||||
|
|
||||||
def update_size(self):
|
def update_size(self):
|
||||||
p = self.path()
|
p = self.path()
|
||||||
self._size = os.path.getsize(p)//HDR_LEN if os.path.exists(p) else 0
|
self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0
|
||||||
|
|
||||||
def verify_header(self, header, prev_header, bits, target):
|
def verify_header(self, header, prev_hash, target):
|
||||||
prev_hash = hash_header(prev_header)
|
|
||||||
_hash = hash_header(header)
|
_hash = hash_header(header)
|
||||||
if prev_hash != header.get('prev_block_hash'):
|
if prev_hash != header.get('prev_block_hash'):
|
||||||
raise BaseException("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
|
raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
|
||||||
if bitcoin.NetworkConstants.TESTNET:
|
if constants.net.TESTNET:
|
||||||
return
|
return
|
||||||
|
bits = self.target_to_bits(target)
|
||||||
if bits != header.get('bits'):
|
if bits != header.get('bits'):
|
||||||
raise BaseException("bits mismatch: %s vs %s" % (bits, header.get('bits')))
|
raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits')))
|
||||||
if int('0x' + _hash, 16) > target:
|
if int('0x' + _hash, 16) > target:
|
||||||
raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target))
|
raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target))
|
||||||
|
|
||||||
def verify_chunk(self, index, data):
|
def verify_chunk(self, index, data):
|
||||||
num = len(data) // HDR_LEN
|
num = len(data) // 80
|
||||||
prev_header = None
|
prev_hash = self.get_hash(index * 2016 - 1)
|
||||||
if index != 0:
|
target = self.get_target(index-1)
|
||||||
prev_header = self.read_header(index * CHUNK_LEN - 1)
|
|
||||||
chain = []
|
|
||||||
for i in range(num):
|
for i in range(num):
|
||||||
raw_header = data[i*HDR_LEN:(i+1) * HDR_LEN]
|
raw_header = data[i*80:(i+1) * 80]
|
||||||
header = deserialize_header(raw_header, index*CHUNK_LEN + i)
|
header = deserialize_header(raw_header, index*2016 + i)
|
||||||
height = index * CHUNK_LEN + i
|
self.verify_header(header, prev_hash, target)
|
||||||
header['block_height'] = height
|
prev_hash = hash_header(header)
|
||||||
chain.append(header)
|
|
||||||
bits, target = self.get_target(height, chain)
|
|
||||||
self.verify_header(header, prev_header, bits, target)
|
|
||||||
prev_header = header
|
|
||||||
|
|
||||||
def path(self):
|
def path(self):
|
||||||
d = util.get_headers_dir(self.config)
|
d = util.get_headers_dir(self.config)
|
||||||
|
@ -229,11 +182,12 @@ class Blockchain(util.PrintError):
|
||||||
|
|
||||||
def save_chunk(self, index, chunk):
|
def save_chunk(self, index, chunk):
|
||||||
filename = self.path()
|
filename = self.path()
|
||||||
d = (index * CHUNK_LEN - self.checkpoint) * HDR_LEN
|
d = (index * 2016 - self.checkpoint) * 80
|
||||||
if d < 0:
|
if d < 0:
|
||||||
chunk = chunk[-d:]
|
chunk = chunk[-d:]
|
||||||
d = 0
|
d = 0
|
||||||
self.write(chunk, d)
|
truncate = index >= len(self.checkpoints)
|
||||||
|
self.write(chunk, d, truncate)
|
||||||
self.swap_with_parent()
|
self.swap_with_parent()
|
||||||
|
|
||||||
def swap_with_parent(self):
|
def swap_with_parent(self):
|
||||||
|
@ -249,10 +203,10 @@ class Blockchain(util.PrintError):
|
||||||
with open(self.path(), 'rb') as f:
|
with open(self.path(), 'rb') as f:
|
||||||
my_data = f.read()
|
my_data = f.read()
|
||||||
with open(parent.path(), 'rb') as f:
|
with open(parent.path(), 'rb') as f:
|
||||||
f.seek((checkpoint - parent.checkpoint)*HDR_LEN)
|
f.seek((checkpoint - parent.checkpoint)*80)
|
||||||
parent_data = f.read(parent_branch_size*HDR_LEN)
|
parent_data = f.read(parent_branch_size*80)
|
||||||
self.write(parent_data, 0)
|
self.write(parent_data, 0)
|
||||||
parent.write(my_data, (checkpoint - parent.checkpoint)*HDR_LEN)
|
parent.write(my_data, (checkpoint - parent.checkpoint)*80)
|
||||||
# store file path
|
# store file path
|
||||||
for b in blockchains.values():
|
for b in blockchains.values():
|
||||||
b.old_path = b.path()
|
b.old_path = b.path()
|
||||||
|
@ -270,11 +224,11 @@ class Blockchain(util.PrintError):
|
||||||
blockchains[self.checkpoint] = self
|
blockchains[self.checkpoint] = self
|
||||||
blockchains[parent.checkpoint] = parent
|
blockchains[parent.checkpoint] = parent
|
||||||
|
|
||||||
def write(self, data, offset):
|
def write(self, data, offset, truncate=True):
|
||||||
filename = self.path()
|
filename = self.path()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
with open(filename, 'rb+') as f:
|
with open(filename, 'rb+') as f:
|
||||||
if offset != self._size*HDR_LEN:
|
if truncate and offset != self._size*80:
|
||||||
f.seek(offset)
|
f.seek(offset)
|
||||||
f.truncate()
|
f.truncate()
|
||||||
f.seek(offset)
|
f.seek(offset)
|
||||||
|
@ -287,8 +241,8 @@ class Blockchain(util.PrintError):
|
||||||
delta = header.get('block_height') - self.checkpoint
|
delta = header.get('block_height') - self.checkpoint
|
||||||
data = bfh(serialize_header(header))
|
data = bfh(serialize_header(header))
|
||||||
assert delta == self.size()
|
assert delta == self.size()
|
||||||
assert len(data) == HDR_LEN
|
assert len(data) == 80
|
||||||
self.write(data, delta*HDR_LEN)
|
self.write(data, delta*80)
|
||||||
self.swap_with_parent()
|
self.swap_with_parent()
|
||||||
|
|
||||||
def read_header(self, height):
|
def read_header(self, height):
|
||||||
|
@ -303,88 +257,90 @@ class Blockchain(util.PrintError):
|
||||||
name = self.path()
|
name = self.path()
|
||||||
if os.path.exists(name):
|
if os.path.exists(name):
|
||||||
with open(name, 'rb') as f:
|
with open(name, 'rb') as f:
|
||||||
f.seek(delta * HDR_LEN)
|
f.seek(delta * 80)
|
||||||
h = f.read(HDR_LEN)
|
h = f.read(80)
|
||||||
|
if len(h) < 80:
|
||||||
|
raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))
|
||||||
|
elif not os.path.exists(util.get_headers_dir(self.config)):
|
||||||
|
raise Exception('Electrum datadir does not exist. Was it deleted while running?')
|
||||||
|
else:
|
||||||
|
raise Exception('Cannot find headers file but datadir is there. Should be at {}'.format(name))
|
||||||
|
if h == bytes([0])*80:
|
||||||
|
return None
|
||||||
return deserialize_header(h, height)
|
return deserialize_header(h, height)
|
||||||
|
|
||||||
def get_hash(self, height):
|
def get_hash(self, height):
|
||||||
return hash_header(self.read_header(height))
|
if height == -1:
|
||||||
|
return '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
elif height == 0:
|
||||||
|
return constants.net.GENESIS
|
||||||
|
elif height < len(self.checkpoints) * 2016:
|
||||||
|
assert (height+1) % 2016 == 0, height
|
||||||
|
index = height // 2016
|
||||||
|
h, t = self.checkpoints[index]
|
||||||
|
return h
|
||||||
|
else:
|
||||||
|
return hash_header(self.read_header(height))
|
||||||
|
|
||||||
def get_median_time(self, height, chain=None):
|
def get_target(self, index):
|
||||||
if chain is None:
|
# compute target from chunk x, used in chunk x+1
|
||||||
chain = []
|
if constants.net.TESTNET:
|
||||||
|
return 0
|
||||||
|
if index == -1:
|
||||||
|
return MAX_TARGET
|
||||||
|
if index < len(self.checkpoints):
|
||||||
|
h, t = self.checkpoints[index]
|
||||||
|
return t
|
||||||
|
# new target
|
||||||
|
first = self.read_header(index * 2016)
|
||||||
|
last = self.read_header(index * 2016 + 2015)
|
||||||
|
bits = last.get('bits')
|
||||||
|
target = self.bits_to_target(bits)
|
||||||
|
nActualTimespan = last.get('timestamp') - first.get('timestamp')
|
||||||
|
nTargetTimespan = 14 * 24 * 60 * 60
|
||||||
|
nActualTimespan = max(nActualTimespan, nTargetTimespan // 4)
|
||||||
|
nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
|
||||||
|
new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan)
|
||||||
|
return new_target
|
||||||
|
|
||||||
height_range = range(max(0, height - POW_MEDIAN_BLOCK_SPAN),
|
def bits_to_target(self, bits):
|
||||||
max(1, height))
|
bitsN = (bits >> 24) & 0xff
|
||||||
median = []
|
if not (bitsN >= 0x03 and bitsN <= 0x1d):
|
||||||
for h in height_range:
|
raise Exception("First part of bits should be in [0x03, 0x1d]")
|
||||||
header = self.read_header(h)
|
bitsBase = bits & 0xffffff
|
||||||
if not header:
|
if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
|
||||||
for header in chain:
|
raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
|
||||||
if header.get('block_height') == h:
|
return bitsBase << (8 * (bitsN-3))
|
||||||
break
|
|
||||||
assert header and header.get('block_height') == h
|
|
||||||
median.append(header.get('timestamp'))
|
|
||||||
|
|
||||||
median.sort()
|
def target_to_bits(self, target):
|
||||||
return median[len(median)//2];
|
c = ("%064x" % target)[2:]
|
||||||
|
while c[:2] == '00' and len(c) > 6:
|
||||||
def get_target(self, height, chain=None):
|
c = c[2:]
|
||||||
if chain is None:
|
bitsN, bitsBase = len(c) // 2, int('0x' + c[:6], 16)
|
||||||
chain = []
|
if bitsBase >= 0x800000:
|
||||||
|
bitsN += 1
|
||||||
if bitcoin.NetworkConstants.TESTNET:
|
bitsBase >>= 8
|
||||||
return 0, 0
|
return bitsN << 24 | bitsBase
|
||||||
|
|
||||||
if height <= POW_AVERAGING_WINDOW:
|
|
||||||
return target_to_bits(POW_LIMIT), POW_LIMIT
|
|
||||||
|
|
||||||
height_range = range(max(0, height - POW_AVERAGING_WINDOW),
|
|
||||||
max(1, height))
|
|
||||||
mean_target = 0
|
|
||||||
for h in height_range:
|
|
||||||
header = self.read_header(h)
|
|
||||||
if not header:
|
|
||||||
for header in chain:
|
|
||||||
if header.get('block_height') == h:
|
|
||||||
break
|
|
||||||
assert header and header.get('block_height') == h
|
|
||||||
mean_target += bits_to_target(header.get('bits'))
|
|
||||||
mean_target //= POW_AVERAGING_WINDOW
|
|
||||||
|
|
||||||
actual_timespan = self.get_median_time(height, chain) - \
|
|
||||||
self.get_median_time(height - POW_AVERAGING_WINDOW, chain)
|
|
||||||
actual_timespan = AVERAGING_WINDOW_TIMESPAN + \
|
|
||||||
int((actual_timespan - AVERAGING_WINDOW_TIMESPAN) / \
|
|
||||||
POW_DAMPING_FACTOR)
|
|
||||||
if actual_timespan < MIN_ACTUAL_TIMESPAN:
|
|
||||||
actual_timespan = MIN_ACTUAL_TIMESPAN
|
|
||||||
elif actual_timespan > MAX_ACTUAL_TIMESPAN:
|
|
||||||
actual_timespan = MAX_ACTUAL_TIMESPAN
|
|
||||||
|
|
||||||
next_target = mean_target // AVERAGING_WINDOW_TIMESPAN * actual_timespan
|
|
||||||
|
|
||||||
if next_target > POW_LIMIT:
|
|
||||||
next_target = POW_LIMIT
|
|
||||||
|
|
||||||
return target_to_bits(next_target), next_target
|
|
||||||
|
|
||||||
def can_connect(self, header, check_height=True):
|
def can_connect(self, header, check_height=True):
|
||||||
|
if header is None:
|
||||||
|
return False
|
||||||
height = header['block_height']
|
height = header['block_height']
|
||||||
if check_height and self.height() != height - 1:
|
if check_height and self.height() != height - 1:
|
||||||
|
#self.print_error("cannot connect at height", height)
|
||||||
return False
|
return False
|
||||||
if height == 0:
|
if height == 0:
|
||||||
return hash_header(header) == bitcoin.NetworkConstants.GENESIS
|
return hash_header(header) == constants.net.GENESIS
|
||||||
previous_header = self.read_header(height -1)
|
try:
|
||||||
if not previous_header:
|
prev_hash = self.get_hash(height - 1)
|
||||||
|
except:
|
||||||
return False
|
return False
|
||||||
prev_hash = hash_header(previous_header)
|
|
||||||
if prev_hash != header.get('prev_block_hash'):
|
if prev_hash != header.get('prev_block_hash'):
|
||||||
return False
|
return False
|
||||||
bits, target = self.get_target(height)
|
target = self.get_target(height // 2016 - 1)
|
||||||
try:
|
try:
|
||||||
self.verify_header(header, previous_header, bits, target)
|
self.verify_header(header, prev_hash, target)
|
||||||
except:
|
except BaseException as e:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -396,5 +352,15 @@ class Blockchain(util.PrintError):
|
||||||
self.save_chunk(idx, data)
|
self.save_chunk(idx, data)
|
||||||
return True
|
return True
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.print_error('verify_chunk failed', str(e))
|
self.print_error('verify_chunk %d failed'%idx, str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_checkpoints(self):
|
||||||
|
# for each chunk, store the hash of the last block and the target after the chunk
|
||||||
|
cp = []
|
||||||
|
n = self.height() // 2016
|
||||||
|
for index in range(n):
|
||||||
|
h = self.get_hash((index+1) * 2016 -1)
|
||||||
|
target = self.get_target(index)
|
||||||
|
cp.append((h, target))
|
||||||
|
return cp
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from math import floor, log10
|
from math import floor, log10
|
||||||
|
|
||||||
from .bitcoin import sha256, COIN, TYPE_ADDRESS
|
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
|
||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
from .util import NotEnoughFunds, PrintError
|
from .util import NotEnoughFunds, PrintError
|
||||||
|
|
||||||
|
@ -68,7 +68,12 @@ class PRNG:
|
||||||
x[i], x[j] = x[j], x[i]
|
x[i], x[j] = x[j], x[i]
|
||||||
|
|
||||||
|
|
||||||
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
|
Bucket = namedtuple('Bucket',
|
||||||
|
['desc',
|
||||||
|
'weight', # as in BIP-141
|
||||||
|
'value', # in satoshis
|
||||||
|
'coins', # UTXOs
|
||||||
|
'min_height']) # min block height where a coin was confirmed
|
||||||
|
|
||||||
def strip_unneeded(bkts, sufficient_funds):
|
def strip_unneeded(bkts, sufficient_funds):
|
||||||
'''Remove buckets that are unnecessary in achieving the spend amount'''
|
'''Remove buckets that are unnecessary in achieving the spend amount'''
|
||||||
|
@ -81,6 +86,8 @@ def strip_unneeded(bkts, sufficient_funds):
|
||||||
|
|
||||||
class CoinChooserBase(PrintError):
|
class CoinChooserBase(PrintError):
|
||||||
|
|
||||||
|
enable_output_value_rounding = False
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -92,10 +99,10 @@ class CoinChooserBase(PrintError):
|
||||||
|
|
||||||
def make_Bucket(desc, coins):
|
def make_Bucket(desc, coins):
|
||||||
weight = sum(Transaction.estimated_input_weight(coin)
|
weight = sum(Transaction.estimated_input_weight(coin)
|
||||||
for coin in coins)
|
for coin in coins)
|
||||||
size = Transaction.virtual_size_from_weight(weight)
|
|
||||||
value = sum(coin['value'] for coin in coins)
|
value = sum(coin['value'] for coin in coins)
|
||||||
return Bucket(desc, size, value, coins)
|
min_height = min(coin['height'] for coin in coins)
|
||||||
|
return Bucket(desc, weight, value, coins, min_height)
|
||||||
|
|
||||||
return list(map(make_Bucket, buckets.keys(), buckets.values()))
|
return list(map(make_Bucket, buckets.keys(), buckets.values()))
|
||||||
|
|
||||||
|
@ -126,7 +133,13 @@ class CoinChooserBase(PrintError):
|
||||||
zeroes = [trailing_zeroes(i) for i in output_amounts]
|
zeroes = [trailing_zeroes(i) for i in output_amounts]
|
||||||
min_zeroes = min(zeroes)
|
min_zeroes = min(zeroes)
|
||||||
max_zeroes = max(zeroes)
|
max_zeroes = max(zeroes)
|
||||||
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
|
|
||||||
|
if n > 1:
|
||||||
|
zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1)
|
||||||
|
else:
|
||||||
|
# if there is only one change output, this will ensure that we aim
|
||||||
|
# to have one that is exactly as precise as the most precise output
|
||||||
|
zeroes = [min_zeroes]
|
||||||
|
|
||||||
# Calculate change; randomize it a bit if using more than 1 output
|
# Calculate change; randomize it a bit if using more than 1 output
|
||||||
remaining = change_amount
|
remaining = change_amount
|
||||||
|
@ -141,8 +154,10 @@ class CoinChooserBase(PrintError):
|
||||||
n -= 1
|
n -= 1
|
||||||
|
|
||||||
# Last change output. Round down to maximum precision but lose
|
# Last change output. Round down to maximum precision but lose
|
||||||
# no more than 100 satoshis to fees (2dp)
|
# no more than 10**max_dp_to_round_for_privacy
|
||||||
N = pow(10, min(2, zeroes[0]))
|
# e.g. a max of 2 decimal places means losing 100 satoshis to fees
|
||||||
|
max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0
|
||||||
|
N = pow(10, min(max_dp_to_round_for_privacy, zeroes[0]))
|
||||||
amount = (remaining // N) * N
|
amount = (remaining // N) * N
|
||||||
amounts.append(amount)
|
amounts.append(amount)
|
||||||
|
|
||||||
|
@ -168,27 +183,40 @@ class CoinChooserBase(PrintError):
|
||||||
|
|
||||||
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
||||||
dust_threshold):
|
dust_threshold):
|
||||||
'''Select unspent coins to spend to pay outputs. If the change is
|
"""Select unspent coins to spend to pay outputs. If the change is
|
||||||
greater than dust_threshold (after adding the change output to
|
greater than dust_threshold (after adding the change output to
|
||||||
the transaction) it is kept, otherwise none is sent and it is
|
the transaction) it is kept, otherwise none is sent and it is
|
||||||
added to the transaction fee.'''
|
added to the transaction fee.
|
||||||
|
|
||||||
|
Note: fee_estimator expects virtual bytes
|
||||||
|
"""
|
||||||
|
|
||||||
# Deterministic randomness from coins
|
# Deterministic randomness from coins
|
||||||
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
|
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
|
||||||
self.p = PRNG(''.join(sorted(utxos)))
|
self.p = PRNG(''.join(sorted(utxos)))
|
||||||
|
|
||||||
# Copy the ouputs so when adding change we don't modify "outputs"
|
# Copy the outputs so when adding change we don't modify "outputs"
|
||||||
tx = Transaction.from_io([], outputs[:])
|
tx = Transaction.from_io([], outputs[:])
|
||||||
# Size of the transaction with no inputs and no change
|
# Weight of the transaction with no inputs and no change
|
||||||
base_size = tx.estimated_size()
|
# Note: this will use legacy tx serialization. The only side effect
|
||||||
|
# should be that the marker and flag are excluded, which is
|
||||||
|
# compensated in get_tx_weight()
|
||||||
|
base_weight = tx.estimated_weight()
|
||||||
spent_amount = tx.output_value()
|
spent_amount = tx.output_value()
|
||||||
|
|
||||||
|
def fee_estimator_w(weight):
|
||||||
|
return fee_estimator(Transaction.virtual_size_from_weight(weight))
|
||||||
|
|
||||||
|
def get_tx_weight(buckets):
|
||||||
|
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
|
||||||
|
return total_weight
|
||||||
|
|
||||||
def sufficient_funds(buckets):
|
def sufficient_funds(buckets):
|
||||||
'''Given a list of buckets, return True if it has enough
|
'''Given a list of buckets, return True if it has enough
|
||||||
value to pay for the transaction'''
|
value to pay for the transaction'''
|
||||||
total_input = sum(bucket.value for bucket in buckets)
|
total_input = sum(bucket.value for bucket in buckets)
|
||||||
total_size = sum(bucket.size for bucket in buckets) + base_size
|
total_weight = get_tx_weight(buckets)
|
||||||
return total_input >= spent_amount + fee_estimator(total_size)
|
return total_input >= spent_amount + fee_estimator_w(total_weight)
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -196,11 +224,18 @@ class CoinChooserBase(PrintError):
|
||||||
self.penalty_func(tx))
|
self.penalty_func(tx))
|
||||||
|
|
||||||
tx.add_inputs([coin for b in buckets for coin in b.coins])
|
tx.add_inputs([coin for b in buckets for coin in b.coins])
|
||||||
tx_size = base_size + sum(bucket.size for bucket in buckets)
|
tx_weight = get_tx_weight(buckets)
|
||||||
|
|
||||||
# This takes a count of change outputs and returns a tx fee;
|
# change is sent back to sending address unless specified
|
||||||
# each pay-to-bitcoin-address output serializes as 34 bytes
|
if not change_addrs:
|
||||||
fee = lambda count: fee_estimator(tx_size + count * 34)
|
change_addrs = [tx.inputs()[0]['address']]
|
||||||
|
# note: this is not necessarily the final "first input address"
|
||||||
|
# because the inputs had not been sorted at this point
|
||||||
|
assert is_address(change_addrs[0])
|
||||||
|
|
||||||
|
# This takes a count of change outputs and returns a tx fee
|
||||||
|
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
|
||||||
|
fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
|
||||||
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
|
change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
|
||||||
tx.add_outputs(change)
|
tx.add_outputs(change)
|
||||||
|
|
||||||
|
@ -212,35 +247,14 @@ class CoinChooserBase(PrintError):
|
||||||
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
||||||
raise NotImplemented('To be subclassed')
|
raise NotImplemented('To be subclassed')
|
||||||
|
|
||||||
class CoinChooserOldestFirst(CoinChooserBase):
|
|
||||||
'''Maximize transaction priority. Select the oldest unspent
|
|
||||||
transaction outputs in your wallet, that are sufficient to cover
|
|
||||||
the spent amount. Then, remove any unneeded inputs, starting with
|
|
||||||
the smallest in value.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def keys(self, coins):
|
|
||||||
return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
|
|
||||||
for coin in coins]
|
|
||||||
|
|
||||||
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
|
||||||
'''Spend the oldest buckets first.'''
|
|
||||||
# Unconfirmed coins are young, not old
|
|
||||||
adj_height = lambda height: 99999999 if height <= 0 else height
|
|
||||||
buckets.sort(key = lambda b: max(adj_height(coin['height'])
|
|
||||||
for coin in b.coins))
|
|
||||||
selected = []
|
|
||||||
for bucket in buckets:
|
|
||||||
selected.append(bucket)
|
|
||||||
if sufficient_funds(selected):
|
|
||||||
return strip_unneeded(selected, sufficient_funds)
|
|
||||||
else:
|
|
||||||
raise NotEnoughFunds()
|
|
||||||
|
|
||||||
class CoinChooserRandom(CoinChooserBase):
|
class CoinChooserRandom(CoinChooserBase):
|
||||||
|
|
||||||
def bucket_candidates(self, buckets, sufficient_funds):
|
def bucket_candidates_any(self, buckets, sufficient_funds):
|
||||||
'''Returns a list of bucket sets.'''
|
'''Returns a list of bucket sets.'''
|
||||||
|
if not buckets:
|
||||||
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
candidates = set()
|
candidates = set()
|
||||||
|
|
||||||
# Add all singletons
|
# Add all singletons
|
||||||
|
@ -262,13 +276,49 @@ class CoinChooserRandom(CoinChooserBase):
|
||||||
candidates.add(tuple(sorted(permutation[:count + 1])))
|
candidates.add(tuple(sorted(permutation[:count + 1])))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
# FIXME this assumes that the effective value of any bkt is >= 0
|
||||||
|
# we should make sure not to choose buckets with <= 0 eff. val.
|
||||||
raise NotEnoughFunds()
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
candidates = [[buckets[n] for n in c] for c in candidates]
|
candidates = [[buckets[n] for n in c] for c in candidates]
|
||||||
return [strip_unneeded(c, sufficient_funds) for c in candidates]
|
return [strip_unneeded(c, sufficient_funds) for c in candidates]
|
||||||
|
|
||||||
|
def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds):
|
||||||
|
"""Returns a list of bucket sets preferring confirmed coins.
|
||||||
|
|
||||||
|
Any bucket can be:
|
||||||
|
1. "confirmed" if it only contains confirmed coins; else
|
||||||
|
2. "unconfirmed" if it does not contain coins with unconfirmed parents
|
||||||
|
3. other: e.g. "unconfirmed parent" or "local"
|
||||||
|
|
||||||
|
This method tries to only use buckets of type 1, and if the coins there
|
||||||
|
are not enough, tries to use the next type but while also selecting
|
||||||
|
all buckets of all previous types.
|
||||||
|
"""
|
||||||
|
conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0]
|
||||||
|
unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0]
|
||||||
|
other_buckets = [bkt for bkt in buckets if bkt.min_height < 0]
|
||||||
|
|
||||||
|
bucket_sets = [conf_buckets, unconf_buckets, other_buckets]
|
||||||
|
already_selected_buckets = []
|
||||||
|
|
||||||
|
for bkts_choose_from in bucket_sets:
|
||||||
|
try:
|
||||||
|
def sfunds(bkts):
|
||||||
|
return sufficient_funds(already_selected_buckets + bkts)
|
||||||
|
|
||||||
|
candidates = self.bucket_candidates_any(bkts_choose_from, sfunds)
|
||||||
|
break
|
||||||
|
except NotEnoughFunds:
|
||||||
|
already_selected_buckets += bkts_choose_from
|
||||||
|
else:
|
||||||
|
raise NotEnoughFunds()
|
||||||
|
|
||||||
|
candidates = [(already_selected_buckets + c) for c in candidates]
|
||||||
|
return [strip_unneeded(c, sufficient_funds) for c in candidates]
|
||||||
|
|
||||||
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
def choose_buckets(self, buckets, sufficient_funds, penalty_func):
|
||||||
candidates = self.bucket_candidates(buckets, sufficient_funds)
|
candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds)
|
||||||
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))
|
||||||
|
@ -276,14 +326,15 @@ 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.
|
||||||
spent from a user address, all coins are. Compared to spending
|
First, if any coin is spent from a user address, all coins are.
|
||||||
from other addresses to make up an amount, this reduces
|
Compared to spending 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 that
|
reduce blockchain UTXO bloat, and reduce future privacy loss that
|
||||||
would come from reusing that address' remaining UTXOs. Second, it
|
would come from reusing that address' remaining UTXOs.
|
||||||
penalizes change that is quite different to the sent amount.
|
Second, it penalizes change that is quite different to the sent amount.
|
||||||
Third, it penalizes change that is too big.'''
|
Third, it penalizes change that is too big.
|
||||||
|
"""
|
||||||
|
|
||||||
def keys(self, coins):
|
def keys(self, coins):
|
||||||
return [coin['address'] for coin in coins]
|
return [coin['address'] for coin in coins]
|
||||||
|
@ -296,6 +347,7 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
||||||
def penalty(buckets):
|
def penalty(buckets):
|
||||||
badness = len(buckets) - 1
|
badness = len(buckets) - 1
|
||||||
total_input = sum(bucket.value for bucket in buckets)
|
total_input = sum(bucket.value for bucket in buckets)
|
||||||
|
# FIXME "change" here also includes fees
|
||||||
change = float(total_input - spent_amount)
|
change = float(total_input - spent_amount)
|
||||||
# Penalize change not roughly in output range
|
# Penalize change not roughly in output range
|
||||||
if change < min_change:
|
if change < min_change:
|
||||||
|
@ -309,15 +361,18 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
||||||
return penalty
|
return penalty
|
||||||
|
|
||||||
|
|
||||||
COIN_CHOOSERS = {'Priority': CoinChooserOldestFirst,
|
COIN_CHOOSERS = {
|
||||||
'Privacy': CoinChooserPrivacy}
|
'Privacy': CoinChooserPrivacy,
|
||||||
|
}
|
||||||
|
|
||||||
def get_name(config):
|
def get_name(config):
|
||||||
kind = config.get('coin_chooser')
|
kind = config.get('coin_chooser')
|
||||||
if not kind in COIN_CHOOSERS:
|
if not kind in COIN_CHOOSERS:
|
||||||
kind = 'Priority'
|
kind = 'Privacy'
|
||||||
return kind
|
return kind
|
||||||
|
|
||||||
def get_coin_chooser(config):
|
def get_coin_chooser(config):
|
||||||
klass = COIN_CHOOSERS[get_name(config)]
|
klass = COIN_CHOOSERS[get_name(config)]
|
||||||
return klass()
|
coinchooser = klass()
|
||||||
|
coinchooser.enable_output_value_rounding = config.get('coin_chooser_output_rounding', False)
|
||||||
|
return coinchooser
|
||||||
|
|
153
lib/commands.py
153
lib/commands.py
|
@ -34,7 +34,7 @@ from functools import wraps
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .import util
|
from .import util
|
||||||
from .util import bfh, bh2u, format_satoshis, json_decode
|
from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode
|
||||||
from .import bitcoin
|
from .import bitcoin
|
||||||
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
@ -81,8 +81,8 @@ def command(s):
|
||||||
wallet = args[0].wallet
|
wallet = args[0].wallet
|
||||||
password = kwargs.get('password')
|
password = kwargs.get('password')
|
||||||
if c.requires_wallet and wallet is None:
|
if c.requires_wallet and wallet is None:
|
||||||
raise BaseException("wallet not loaded. Use 'electrum-zcash daemon load_wallet'")
|
raise Exception("wallet not loaded. Use 'electrum-zcash daemon load_wallet'")
|
||||||
if c.requires_password and password is None and wallet.storage.get('use_encryption'):
|
if c.requires_password and password is None and wallet.has_password():
|
||||||
return {'error': 'Password required' }
|
return {'error': 'Password required' }
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return func_wrapper
|
return func_wrapper
|
||||||
|
@ -125,7 +125,7 @@ class Commands:
|
||||||
@command('')
|
@command('')
|
||||||
def create(self):
|
def create(self):
|
||||||
"""Create a new wallet"""
|
"""Create a new wallet"""
|
||||||
raise BaseException('Not a JSON-RPC command')
|
raise Exception('Not a JSON-RPC command')
|
||||||
|
|
||||||
@command('wn')
|
@command('wn')
|
||||||
def restore(self, text):
|
def restore(self, text):
|
||||||
|
@ -133,11 +133,13 @@ class Commands:
|
||||||
public key, a master private key, a list of Zcash addresses
|
public key, a master private key, a list of Zcash addresses
|
||||||
or Zcash private keys. If you want to be prompted for your
|
or Zcash private keys. If you want to be prompted for your
|
||||||
seed, type '?' or ':' (concealed) """
|
seed, type '?' or ':' (concealed) """
|
||||||
raise BaseException('Not a JSON-RPC command')
|
raise Exception('Not a JSON-RPC command')
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def password(self, password=None, new_password=None):
|
def password(self, password=None, new_password=None):
|
||||||
"""Change wallet password. """
|
"""Change wallet password. """
|
||||||
|
if self.wallet.storage.is_encrypted_with_hw_device() and new_password:
|
||||||
|
raise Exception("Can't change the password of a wallet encrypted with a hw device.")
|
||||||
b = self.wallet.storage.is_encrypted()
|
b = self.wallet.storage.is_encrypted()
|
||||||
self.wallet.update_password(password, new_password, b)
|
self.wallet.update_password(password, new_password, b)
|
||||||
self.wallet.storage.write()
|
self.wallet.storage.write()
|
||||||
|
@ -148,34 +150,38 @@ class Commands:
|
||||||
"""Return a configuration variable. """
|
"""Return a configuration variable. """
|
||||||
return self.config.get(key)
|
return self.config.get(key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _setconfig_normalize_value(cls, key, value):
|
||||||
|
if key not in ('rpcuser', 'rpcpassword'):
|
||||||
|
value = json_decode(value)
|
||||||
|
try:
|
||||||
|
value = ast.literal_eval(value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return value
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def setconfig(self, key, value):
|
def setconfig(self, key, value):
|
||||||
"""Set a configuration variable. 'value' may be a string or a Python expression."""
|
"""Set a configuration variable. 'value' may be a string or a Python expression."""
|
||||||
if key not in ('rpcuser', 'rpcpassword'):
|
value = self._setconfig_normalize_value(key, value)
|
||||||
value = json_decode(value)
|
|
||||||
self.config.set_key(key, value)
|
self.config.set_key(key, value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def make_seed(self, nbits=132, entropy=1, language=None):
|
def make_seed(self, nbits=132, language=None):
|
||||||
"""Create a seed"""
|
"""Create a seed"""
|
||||||
from .mnemonic import Mnemonic
|
from .mnemonic import Mnemonic
|
||||||
t = 'standard'
|
t = 'standard'
|
||||||
s = Mnemonic(language).make_seed(t, nbits, custom_entropy=entropy)
|
s = Mnemonic(language).make_seed(t, nbits)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
@command('')
|
|
||||||
def check_seed(self, seed, entropy=1, language=None):
|
|
||||||
"""Check that a seed was generated with given entropy"""
|
|
||||||
from .mnemonic import Mnemonic
|
|
||||||
return Mnemonic(language).check_seed(seed, entropy)
|
|
||||||
|
|
||||||
@command('n')
|
@command('n')
|
||||||
def getaddresshistory(self, address):
|
def getaddresshistory(self, address):
|
||||||
"""Return the transaction history of any address. Note: This is a
|
"""Return the transaction history of any address. Note: This is a
|
||||||
walletless server query, results are not checked by SPV.
|
walletless server query, results are not checked by SPV.
|
||||||
"""
|
"""
|
||||||
return self.network.synchronous_get(('blockchain.address.get_history', [address]))
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
return self.network.synchronous_get(('blockchain.scripthash.get_history', [sh]))
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def listunspent(self):
|
def listunspent(self):
|
||||||
|
@ -192,7 +198,8 @@ class Commands:
|
||||||
"""Returns the UTXO list of any address. Note: This
|
"""Returns the UTXO list of any address. Note: This
|
||||||
is a walletless server query, results are not checked by SPV.
|
is a walletless server query, results are not checked by SPV.
|
||||||
"""
|
"""
|
||||||
return self.network.synchronous_get(('blockchain.address.listunspent', [address]))
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
return self.network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def serialize(self, jsontx):
|
def serialize(self, jsontx):
|
||||||
|
@ -203,7 +210,7 @@ class Commands:
|
||||||
keypairs = {}
|
keypairs = {}
|
||||||
inputs = jsontx.get('inputs')
|
inputs = jsontx.get('inputs')
|
||||||
outputs = jsontx.get('outputs')
|
outputs = jsontx.get('outputs')
|
||||||
locktime = jsontx.get('locktime', 0)
|
locktime = jsontx.get('lockTime', 0)
|
||||||
for txin in inputs:
|
for txin in inputs:
|
||||||
if txin.get('output'):
|
if txin.get('output'):
|
||||||
prevout_hash, prevout_n = txin['output'].split(':')
|
prevout_hash, prevout_n = txin['output'].split(':')
|
||||||
|
@ -314,20 +321,12 @@ class Commands:
|
||||||
"""Return the balance of any address. Note: This is a walletless
|
"""Return the balance of any address. Note: This is a walletless
|
||||||
server query, results are not checked by SPV.
|
server query, results are not checked by SPV.
|
||||||
"""
|
"""
|
||||||
out = self.network.synchronous_get(('blockchain.address.get_balance', [address]))
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
out = self.network.synchronous_get(('blockchain.scripthash.get_balance', [sh]))
|
||||||
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
|
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
|
||||||
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
|
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@command('n')
|
|
||||||
def getproof(self, address):
|
|
||||||
"""Get Merkle branch of an address in the UTXO set"""
|
|
||||||
p = self.network.synchronous_get(('blockchain.address.get_proof', [address]))
|
|
||||||
out = []
|
|
||||||
for i,s in p:
|
|
||||||
out.append(i)
|
|
||||||
return out
|
|
||||||
|
|
||||||
@command('n')
|
@command('n')
|
||||||
def getmerkle(self, txid, height):
|
def getmerkle(self, txid, height):
|
||||||
"""Get Merkle branch of a transaction included in a block. Electrum-Zcash
|
"""Get Merkle branch of a transaction included in a block. Electrum-Zcash
|
||||||
|
@ -341,7 +340,7 @@ class Commands:
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return the version of electrum-zcash."""
|
"""Return the version of Electrum-Zcash."""
|
||||||
from .version import ELECTRUM_VERSION
|
from .version import ELECTRUM_VERSION
|
||||||
return ELECTRUM_VERSION
|
return ELECTRUM_VERSION
|
||||||
|
|
||||||
|
@ -378,7 +377,7 @@ class Commands:
|
||||||
return None
|
return None
|
||||||
out = self.wallet.contacts.resolve(x)
|
out = self.wallet.contacts.resolve(x)
|
||||||
if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
|
if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
|
||||||
raise BaseException('cannot verify alias', x)
|
raise Exception('cannot verify alias', x)
|
||||||
return out['address']
|
return out['address']
|
||||||
|
|
||||||
@command('n')
|
@command('n')
|
||||||
|
@ -444,46 +443,20 @@ class Commands:
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def history(self):
|
def history(self, year=None, show_addresses=False, show_fiat=False):
|
||||||
"""Wallet history. Returns the transaction history of your wallet."""
|
"""Wallet history. Returns the transaction history of your wallet."""
|
||||||
balance = 0
|
kwargs = {'show_addresses': show_addresses}
|
||||||
out = []
|
if year:
|
||||||
for item in self.wallet.get_history():
|
import time
|
||||||
tx_hash, height, conf, timestamp, value, balance = item
|
start_date = datetime.datetime(year, 1, 1)
|
||||||
if timestamp:
|
end_date = datetime.datetime(year+1, 1, 1)
|
||||||
date = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
|
||||||
else:
|
kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
|
||||||
date = "----"
|
if show_fiat:
|
||||||
label = self.wallet.get_label(tx_hash)
|
from .exchange_rate import FxThread
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
fx = FxThread(self.config, None)
|
||||||
tx.deserialize()
|
kwargs['fx'] = fx
|
||||||
input_addresses = []
|
return json_encode(self.wallet.get_full_history(**kwargs))
|
||||||
output_addresses = []
|
|
||||||
for x in tx.inputs():
|
|
||||||
if x['type'] == 'coinbase': continue
|
|
||||||
addr = x.get('address')
|
|
||||||
if addr == None: continue
|
|
||||||
if addr == "(pubkey)":
|
|
||||||
prevout_hash = x.get('prevout_hash')
|
|
||||||
prevout_n = x.get('prevout_n')
|
|
||||||
_addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
|
|
||||||
if _addr:
|
|
||||||
addr = _addr
|
|
||||||
input_addresses.append(addr)
|
|
||||||
for addr, v in tx.get_outputs():
|
|
||||||
output_addresses.append(addr)
|
|
||||||
out.append({
|
|
||||||
'txid': tx_hash,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'date': date,
|
|
||||||
'input_addresses': input_addresses,
|
|
||||||
'output_addresses': output_addresses,
|
|
||||||
'label': label,
|
|
||||||
'value': str(Decimal(value)/COIN) if value is not None else None,
|
|
||||||
'height': height,
|
|
||||||
'confirmations': conf
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def setlabel(self, key, label):
|
def setlabel(self, key, label):
|
||||||
|
@ -545,7 +518,7 @@ class Commands:
|
||||||
if raw:
|
if raw:
|
||||||
tx = Transaction(raw)
|
tx = Transaction(raw)
|
||||||
else:
|
else:
|
||||||
raise BaseException("Unknown transaction")
|
raise Exception("Unknown transaction")
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
|
@ -574,7 +547,7 @@ class Commands:
|
||||||
"""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)
|
||||||
if not r:
|
if not r:
|
||||||
raise BaseException("Request not found")
|
raise Exception("Request not found")
|
||||||
return self._format_request(r)
|
return self._format_request(r)
|
||||||
|
|
||||||
#@command('w')
|
#@command('w')
|
||||||
|
@ -612,7 +585,7 @@ class Commands:
|
||||||
@command('w')
|
@command('w')
|
||||||
def addrequest(self, amount, memo='', expiration=None, force=False):
|
def addrequest(self, amount, memo='', expiration=None, force=False):
|
||||||
"""Create a payment request, using the first unused address of the wallet.
|
"""Create a payment request, using the first unused address of the wallet.
|
||||||
The address will be condidered as used after this operation.
|
The address will be considered as used after this operation.
|
||||||
If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet."""
|
If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet."""
|
||||||
addr = self.wallet.get_unused_address()
|
addr = self.wallet.get_unused_address()
|
||||||
if addr is None:
|
if addr is None:
|
||||||
|
@ -627,12 +600,21 @@ class Commands:
|
||||||
out = self.wallet.get_payment_request(addr, self.config)
|
out = self.wallet.get_payment_request(addr, self.config)
|
||||||
return self._format_request(out)
|
return self._format_request(out)
|
||||||
|
|
||||||
|
@command('w')
|
||||||
|
def addtransaction(self, tx):
|
||||||
|
""" Add a transaction to the wallet history """
|
||||||
|
tx = Transaction(tx)
|
||||||
|
if not self.wallet.add_transaction(tx.txid(), tx):
|
||||||
|
return False
|
||||||
|
self.wallet.save_transactions()
|
||||||
|
return tx.txid()
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def signrequest(self, address, password=None):
|
def signrequest(self, address, password=None):
|
||||||
"Sign payment request with an OpenAlias"
|
"Sign payment request with an OpenAlias"
|
||||||
alias = self.config.get('alias')
|
alias = self.config.get('alias')
|
||||||
if not alias:
|
if not alias:
|
||||||
raise BaseException('No alias in your configuration')
|
raise Exception('No alias in your configuration')
|
||||||
alias_addr = self.wallet.contacts.resolve(alias)['address']
|
alias_addr = self.wallet.contacts.resolve(alias)['address']
|
||||||
self.wallet.sign_payment_request(address, alias, alias_addr, password)
|
self.wallet.sign_payment_request(address, alias, alias_addr, password)
|
||||||
|
|
||||||
|
@ -649,18 +631,20 @@ class Commands:
|
||||||
|
|
||||||
@command('n')
|
@command('n')
|
||||||
def notify(self, address, URL):
|
def notify(self, address, URL):
|
||||||
"""Watch an address. Everytime the address changes, a http POST is sent to the URL."""
|
"""Watch an address. Every time the address changes, a http POST is sent to the URL."""
|
||||||
def callback(x):
|
def callback(x):
|
||||||
import urllib.request
|
import urllib.request
|
||||||
headers = {'content-type':'application/json'}
|
headers = {'content-type':'application/json'}
|
||||||
data = {'address':address, 'status':x.get('result')}
|
data = {'address':address, 'status':x.get('result')}
|
||||||
|
serialized_data = util.to_bytes(json.dumps(data))
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request(URL, json.dumps(data), headers)
|
req = urllib.request.Request(URL, serialized_data, headers)
|
||||||
response_stream = urllib.request.urlopen(req, timeout=5)
|
response_stream = urllib.request.urlopen(req, timeout=5)
|
||||||
util.print_error('Got Response for %s' % address)
|
util.print_error('Got Response for %s' % address)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
util.print_error(str(e))
|
util.print_error(str(e))
|
||||||
self.network.send([('blockchain.address.subscribe', [address])], callback)
|
h = self.network.addr_to_scripthash(address)
|
||||||
|
self.network.send([('blockchain.scripthash.subscribe', [h])], callback)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@command('wn')
|
@command('wn')
|
||||||
|
@ -668,6 +652,12 @@ class Commands:
|
||||||
""" return wallet synchronization status """
|
""" return wallet synchronization status """
|
||||||
return self.wallet.is_up_to_date()
|
return self.wallet.is_up_to_date()
|
||||||
|
|
||||||
|
@command('n')
|
||||||
|
def getfeerate(self):
|
||||||
|
"""Return current optimal fee rate per kilobyte, according
|
||||||
|
to config settings (static/dynamic)"""
|
||||||
|
return self.config.fee_per_kb()
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
def help(self):
|
def help(self):
|
||||||
# for the python console
|
# for the python console
|
||||||
|
@ -708,7 +698,6 @@ command_options = {
|
||||||
'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
|
'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
|
||||||
'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
|
'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
|
||||||
'nbits': (None, "Number of bits of entropy"),
|
'nbits': (None, "Number of bits of entropy"),
|
||||||
'entropy': (None, "Custom entropy"),
|
|
||||||
'language': ("-L", "Default language for wordlist"),
|
'language': ("-L", "Default language for wordlist"),
|
||||||
'privkey': (None, "Private key. Set to '?' to get a prompt."),
|
'privkey': (None, "Private key. Set to '?' to get a prompt."),
|
||||||
'unsigned': ("-u", "Do not sign transaction"),
|
'unsigned': ("-u", "Do not sign transaction"),
|
||||||
|
@ -721,6 +710,9 @@ command_options = {
|
||||||
'pending': (None, "Show only pending requests."),
|
'pending': (None, "Show only pending requests."),
|
||||||
'expired': (None, "Show only expired requests."),
|
'expired': (None, "Show only expired requests."),
|
||||||
'paid': (None, "Show only paid requests."),
|
'paid': (None, "Show only paid requests."),
|
||||||
|
'show_addresses': (None, "Show input and output addresses"),
|
||||||
|
'show_fiat': (None, "Show fiat value of transactions"),
|
||||||
|
'year': (None, "Show history for a given year"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -731,7 +723,7 @@ arg_types = {
|
||||||
'num': int,
|
'num': int,
|
||||||
'nbits': int,
|
'nbits': int,
|
||||||
'imax': int,
|
'imax': int,
|
||||||
'entropy': int,
|
'year': int,
|
||||||
'tx': tx_from_str,
|
'tx': tx_from_str,
|
||||||
'pubkeys': json_loads,
|
'pubkeys': json_loads,
|
||||||
'jsontx': json_loads,
|
'jsontx': json_loads,
|
||||||
|
@ -794,7 +786,7 @@ def subparser_call(self, parser, namespace, values, option_string=None):
|
||||||
parser = self._name_parser_map[parser_name]
|
parser = self._name_parser_map[parser_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
tup = parser_name, ', '.join(self._name_parser_map)
|
tup = parser_name, ', '.join(self._name_parser_map)
|
||||||
msg = _('unknown parser %r (choices: %s)') % tup
|
msg = _('unknown parser {!r} (choices: {})').format(*tup)
|
||||||
raise ArgumentError(self, msg)
|
raise ArgumentError(self, msg)
|
||||||
# parse all the remaining options into the namespace
|
# parse all the remaining options into the namespace
|
||||||
# store any unrecognized options on the object, so that the top
|
# store any unrecognized options on the object, so that the top
|
||||||
|
@ -808,7 +800,7 @@ argparse._SubParsersAction.__call__ = subparser_call
|
||||||
|
|
||||||
|
|
||||||
def add_network_options(parser):
|
def add_network_options(parser):
|
||||||
parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=False, help="connect to one server only")
|
parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
|
||||||
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
|
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
|
||||||
parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
|
parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
|
||||||
|
|
||||||
|
@ -819,6 +811,7 @@ def add_global_options(parser):
|
||||||
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("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
|
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
|
||||||
|
group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
|
||||||
|
|
||||||
def get_parser():
|
def get_parser():
|
||||||
# create main parser
|
# create main parser
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
# Copyright (C) 2018 The Electrum developers
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(filename, default):
|
||||||
|
path = os.path.join(os.path.dirname(__file__), filename)
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
r = json.loads(f.read())
|
||||||
|
except:
|
||||||
|
r = default
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinMainnet:
|
||||||
|
|
||||||
|
TESTNET = False
|
||||||
|
WIF_PREFIX = 0x80
|
||||||
|
ADDRTYPE_P2PKH = bytes.fromhex('1CB8')
|
||||||
|
ADDRTYPE_P2SH = bytes.fromhex('1CBD')
|
||||||
|
GENESIS = "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08"
|
||||||
|
DEFAULT_PORTS = {'t': '50021', 's': '50022'}
|
||||||
|
DEFAULT_SERVERS = read_json('servers.json', {})
|
||||||
|
CHECKPOINTS = []
|
||||||
|
|
||||||
|
XPRV_HEADERS = {
|
||||||
|
'standard': 0x0488ade4, # xprv
|
||||||
|
}
|
||||||
|
XPUB_HEADERS = {
|
||||||
|
'standard': 0x0488b21e, # xpub
|
||||||
|
}
|
||||||
|
DRKV_HEADER = 0x02fe52f8 # drkv
|
||||||
|
DRKP_HEADER = 0x02fe52cc # drkp
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinTestnet:
|
||||||
|
|
||||||
|
TESTNET = True
|
||||||
|
WIF_PREFIX = 0xEF
|
||||||
|
ADDRTYPE_P2PKH = bytes.fromhex('1D25')
|
||||||
|
ADDRTYPE_P2SH = bytes.fromhex('1CBA')
|
||||||
|
GENESIS = "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38"
|
||||||
|
DEFAULT_PORTS = {'t': '51021', 's': '51022'}
|
||||||
|
DEFAULT_SERVERS = read_json('servers_testnet.json', {})
|
||||||
|
CHECKPOINTS = []
|
||||||
|
|
||||||
|
XPRV_HEADERS = {
|
||||||
|
'standard': 0x04358394, # tprv
|
||||||
|
}
|
||||||
|
XPUB_HEADERS = {
|
||||||
|
'standard': 0x043587cf, # tpub
|
||||||
|
}
|
||||||
|
DRKV_HEADER = 0x3a8061a0 # DRKV
|
||||||
|
DRKP_HEADER = 0x3a805837 # DRKP
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinRegtest(BitcoinTestnet):
|
||||||
|
|
||||||
|
GENESIS = "029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327"
|
||||||
|
DEFAULT_SERVERS = read_json('servers_regtest.json', {})
|
||||||
|
CHECKPOINTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# don't import net directly, import the module instead (so that net is singleton)
|
||||||
|
net = BitcoinMainnet
|
||||||
|
|
||||||
|
|
||||||
|
def set_mainnet():
|
||||||
|
global net
|
||||||
|
net = BitcoinMainnet
|
||||||
|
|
||||||
|
|
||||||
|
def set_testnet():
|
||||||
|
global net
|
||||||
|
net = BitcoinTestnet
|
||||||
|
|
||||||
|
|
||||||
|
def set_regtest():
|
||||||
|
global net
|
||||||
|
net = BitcoinRegtest
|
|
@ -22,10 +22,14 @@
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
import re
|
import re
|
||||||
import dns
|
import dns
|
||||||
|
from dns.exception import DNSException
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from . import dnssec
|
from . import dnssec
|
||||||
|
from .util import export_meta, import_meta, print_error, to_string
|
||||||
|
|
||||||
|
|
||||||
class Contacts(dict):
|
class Contacts(dict):
|
||||||
|
@ -48,14 +52,15 @@ class Contacts(dict):
|
||||||
self.storage.put('contacts', dict(self))
|
self.storage.put('contacts', dict(self))
|
||||||
|
|
||||||
def import_file(self, path):
|
def import_file(self, path):
|
||||||
try:
|
import_meta(path, self._validate, self.load_meta)
|
||||||
with open(path, 'r') as f:
|
|
||||||
d = self._validate(json.loads(f.read()))
|
def load_meta(self, data):
|
||||||
except:
|
self.update(data)
|
||||||
return
|
|
||||||
self.update(d)
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def export_file(self, filename):
|
||||||
|
export_meta(self, filename)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
dict.__setitem__(self, key, value)
|
dict.__setitem__(self, key, value)
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -92,10 +97,14 @@ class Contacts(dict):
|
||||||
def resolve_openalias(self, url):
|
def resolve_openalias(self, url):
|
||||||
# support email-style addresses, per the OA standard
|
# support email-style addresses, per the OA standard
|
||||||
url = url.replace('@', '.')
|
url = url.replace('@', '.')
|
||||||
records, validated = dnssec.query(url, dns.rdatatype.TXT)
|
try:
|
||||||
|
records, validated = dnssec.query(url, dns.rdatatype.TXT)
|
||||||
|
except DNSException as e:
|
||||||
|
print_error('Error resolving openalias: ', str(e))
|
||||||
|
return None
|
||||||
prefix = 'zcash'
|
prefix = 'zcash'
|
||||||
for record in records:
|
for record in records:
|
||||||
string = record.strings[0]
|
string = to_string(record.strings[0], 'utf8')
|
||||||
if string.startswith('oa1:' + prefix):
|
if string.startswith('oa1:' + prefix):
|
||||||
address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
|
address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
|
||||||
name = self.find_regex(string, r'recipient_name=([^;]+)')
|
name = self.find_regex(string, r'recipient_name=([^;]+)')
|
||||||
|
@ -113,13 +122,13 @@ class Contacts(dict):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _validate(self, data):
|
def _validate(self, data):
|
||||||
for k,v in list(data.items()):
|
for k, v in list(data.items()):
|
||||||
if k == 'contacts':
|
if k == 'contacts':
|
||||||
return self._validate(v)
|
return self._validate(v)
|
||||||
if not bitcoin.is_address(k):
|
if not bitcoin.is_address(k):
|
||||||
data.pop(k)
|
data.pop(k)
|
||||||
else:
|
else:
|
||||||
_type,_ = v
|
_type, _ = v
|
||||||
if _type != 'address':
|
if _type != 'address':
|
||||||
data.pop(k)
|
data.pop(k)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
"BTC"
|
"BTC"
|
||||||
],
|
],
|
||||||
"CoinMarketCap": [
|
"CoinMarketCap": [
|
||||||
"BTC",
|
|
||||||
"USD"
|
"USD"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
import ast
|
import ast
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
# from jsonrpc import JSONRPCResponseManager
|
# from jsonrpc import JSONRPCResponseManager
|
||||||
import jsonrpclib
|
import jsonrpclib
|
||||||
|
@ -58,7 +60,7 @@ def get_fd_or_server(config):
|
||||||
lockfile = get_lockfile(config)
|
lockfile = get_lockfile(config)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY), None
|
return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644), None
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
server = get_server(config)
|
server = get_server(config)
|
||||||
|
@ -121,13 +123,12 @@ class Daemon(DaemonThread):
|
||||||
self.config = config
|
self.config = config
|
||||||
if config.get('offline'):
|
if config.get('offline'):
|
||||||
self.network = None
|
self.network = None
|
||||||
self.fx = None
|
|
||||||
else:
|
else:
|
||||||
self.network = Network(config)
|
self.network = Network(config)
|
||||||
self.network.start()
|
self.network.start()
|
||||||
self.fx = FxThread(config, self.network)
|
self.fx = FxThread(config, self.network)
|
||||||
|
if self.network:
|
||||||
self.network.add_jobs([self.fx])
|
self.network.add_jobs([self.fx])
|
||||||
|
|
||||||
self.gui = None
|
self.gui = None
|
||||||
self.wallets = {}
|
self.wallets = {}
|
||||||
# Setup JSONRPC server
|
# Setup JSONRPC server
|
||||||
|
@ -172,8 +173,9 @@ class Daemon(DaemonThread):
|
||||||
elif sub == 'load_wallet':
|
elif sub == 'load_wallet':
|
||||||
path = config.get_wallet_path()
|
path = config.get_wallet_path()
|
||||||
wallet = self.load_wallet(path, config.get('password'))
|
wallet = self.load_wallet(path, config.get('password'))
|
||||||
self.cmd_runner.wallet = wallet
|
if wallet is not None:
|
||||||
response = True
|
self.cmd_runner.wallet = wallet
|
||||||
|
response = wallet is not None
|
||||||
elif sub == 'close_wallet':
|
elif sub == 'close_wallet':
|
||||||
path = config.get_wallet_path()
|
path = config.get_wallet_path()
|
||||||
if path in self.wallets:
|
if path in self.wallets:
|
||||||
|
@ -184,6 +186,9 @@ class Daemon(DaemonThread):
|
||||||
elif sub == 'status':
|
elif sub == 'status':
|
||||||
if self.network:
|
if self.network:
|
||||||
p = self.network.get_parameters()
|
p = self.network.get_parameters()
|
||||||
|
current_wallet = self.cmd_runner.wallet
|
||||||
|
current_wallet_path = current_wallet.storage.path \
|
||||||
|
if current_wallet else None
|
||||||
response = {
|
response = {
|
||||||
'path': self.network.config.path,
|
'path': self.network.config.path,
|
||||||
'server': p[0],
|
'server': p[0],
|
||||||
|
@ -195,6 +200,7 @@ class Daemon(DaemonThread):
|
||||||
'version': ELECTRUM_VERSION,
|
'version': ELECTRUM_VERSION,
|
||||||
'wallets': {k: w.is_up_to_date()
|
'wallets': {k: w.is_up_to_date()
|
||||||
for k, w in self.wallets.items()},
|
for k, w in self.wallets.items()},
|
||||||
|
'current_wallet': current_wallet_path,
|
||||||
'fee_per_kb': self.config.fee_per_kb(),
|
'fee_per_kb': self.config.fee_per_kb(),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
@ -301,4 +307,8 @@ class Daemon(DaemonThread):
|
||||||
gui_name = 'qt'
|
gui_name = 'qt'
|
||||||
gui = __import__('electrum_zcash_gui.' + gui_name, fromlist=['electrum_zcash_gui'])
|
gui = __import__('electrum_zcash_gui.' + gui_name, fromlist=['electrum_zcash_gui'])
|
||||||
self.gui = gui.ElectrumGui(config, self, plugins)
|
self.gui = gui.ElectrumGui(config, self, plugins)
|
||||||
self.gui.main()
|
try:
|
||||||
|
self.gui.main()
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
# app will exit now
|
||||||
|
|
|
@ -199,7 +199,7 @@ def check_query(ns, sub, _type, keys):
|
||||||
elif answer[1].rdtype == dns.rdatatype.RRSIG:
|
elif answer[1].rdtype == dns.rdatatype.RRSIG:
|
||||||
rrset, rrsig = answer
|
rrset, rrsig = answer
|
||||||
else:
|
else:
|
||||||
raise BaseException('No signature set in record')
|
raise Exception('No signature set in record')
|
||||||
if keys is None:
|
if keys is None:
|
||||||
keys = {dns.name.from_text(sub):rrset}
|
keys = {dns.name.from_text(sub):rrset}
|
||||||
dns.dnssec.validate(rrset, rrsig, keys)
|
dns.dnssec.validate(rrset, rrsig, keys)
|
||||||
|
@ -248,7 +248,7 @@ def get_and_validate(ns, url, _type):
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise BaseException("DS does not match DNSKEY")
|
raise Exception("DS does not match DNSKEY")
|
||||||
# set key for next iteration
|
# set key for next iteration
|
||||||
keys = {name: rrset}
|
keys = {name: rrset}
|
||||||
# get TXT record (signed by zone)
|
# get TXT record (signed by zone)
|
||||||
|
|
|
@ -2,6 +2,8 @@ from datetime import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import requests
|
import requests
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import time
|
import time
|
||||||
import csv
|
import csv
|
||||||
|
@ -39,7 +41,7 @@ class ExchangeBase(PrintError):
|
||||||
def get_json(self, site, get_string):
|
def get_json(self, site, get_string):
|
||||||
# APIs must have https
|
# APIs must have https
|
||||||
url = ''.join(['https://', site, get_string])
|
url = ''.join(['https://', site, get_string])
|
||||||
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum-Zcash'})
|
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum-Zcash'}, timeout=10)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def get_csv(self, site, get_string):
|
def get_csv(self, site, get_string):
|
||||||
|
@ -65,28 +67,54 @@ class ExchangeBase(PrintError):
|
||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def get_historical_rates_safe(self, ccy):
|
def read_historical_rates(self, ccy, cache_dir):
|
||||||
|
filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
|
||||||
|
if os.path.exists(filename):
|
||||||
|
timestamp = os.stat(filename).st_mtime
|
||||||
|
try:
|
||||||
|
with open(filename, 'r', encoding='utf-8') as f:
|
||||||
|
h = json.loads(f.read())
|
||||||
|
h['timestamp'] = timestamp
|
||||||
|
except:
|
||||||
|
h = None
|
||||||
|
else:
|
||||||
|
h = None
|
||||||
|
if h:
|
||||||
|
self.history[ccy] = h
|
||||||
|
self.on_history()
|
||||||
|
return h
|
||||||
|
|
||||||
|
def get_historical_rates_safe(self, ccy, cache_dir):
|
||||||
try:
|
try:
|
||||||
self.print_error("requesting fx history for", ccy)
|
self.print_error("requesting fx history for", ccy)
|
||||||
self.history[ccy] = self.historical_rates(ccy)
|
h = self.request_history(ccy)
|
||||||
self.print_error("received fx history for", ccy)
|
self.print_error("received fx history for", ccy)
|
||||||
self.on_history()
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.print_error("failed fx history:", e)
|
self.print_error("failed fx history:", e)
|
||||||
|
return
|
||||||
|
filename = os.path.join(cache_dir, self.name() + '_' + ccy)
|
||||||
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(json.dumps(h))
|
||||||
|
h['timestamp'] = time.time()
|
||||||
|
self.history[ccy] = h
|
||||||
|
self.on_history()
|
||||||
|
|
||||||
def get_historical_rates(self, ccy):
|
def get_historical_rates(self, ccy, cache_dir):
|
||||||
result = self.history.get(ccy)
|
if ccy not in self.history_ccys():
|
||||||
if not result and ccy in self.history_ccys():
|
return
|
||||||
t = Thread(target=self.get_historical_rates_safe, args=(ccy,))
|
h = self.history.get(ccy)
|
||||||
|
if h is None:
|
||||||
|
h = self.read_historical_rates(ccy, cache_dir)
|
||||||
|
if h is None or h['timestamp'] < time.time() - 24*3600:
|
||||||
|
t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
|
||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
return result
|
|
||||||
|
|
||||||
def history_ccys(self):
|
def history_ccys(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def historical_rate(self, ccy, d_t):
|
def historical_rate(self, ccy, d_t):
|
||||||
return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
|
return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'), 'NaN')
|
||||||
|
|
||||||
def get_currencies(self):
|
def get_currencies(self):
|
||||||
rates = self.get_rates('')
|
rates = self.get_rates('')
|
||||||
|
@ -96,7 +124,7 @@ class ExchangeBase(PrintError):
|
||||||
class Bittrex(ExchangeBase):
|
class Bittrex(ExchangeBase):
|
||||||
def get_rates(self, ccy):
|
def get_rates(self, ccy):
|
||||||
json = self.get_json('bittrex.com',
|
json = self.get_json('bittrex.com',
|
||||||
'/api/v1.1/public/getticker?market=BTC-DASH')
|
'/api/v1.1/public/getticker?market=BTC-ZEC')
|
||||||
quote_currencies = {}
|
quote_currencies = {}
|
||||||
if not json.get('success', False):
|
if not json.get('success', False):
|
||||||
return quote_currencies
|
return quote_currencies
|
||||||
|
@ -109,21 +137,20 @@ class Poloniex(ExchangeBase):
|
||||||
def get_rates(self, ccy):
|
def get_rates(self, ccy):
|
||||||
json = self.get_json('poloniex.com', '/public?command=returnTicker')
|
json = self.get_json('poloniex.com', '/public?command=returnTicker')
|
||||||
quote_currencies = {}
|
quote_currencies = {}
|
||||||
dash_ticker = json.get('BTC_DASH')
|
zcash_ticker = json.get('BTC_ZEC')
|
||||||
quote_currencies['BTC'] = Decimal(dash_ticker['last'])
|
quote_currencies['BTC'] = Decimal(zcash_ticker['last'])
|
||||||
return quote_currencies
|
return quote_currencies
|
||||||
|
|
||||||
|
|
||||||
class CoinMarketCap(ExchangeBase):
|
class CoinMarketCap(ExchangeBase):
|
||||||
def get_rates(self, ccy):
|
def get_rates(self, ccy):
|
||||||
json = self.get_json('api.coinmarketcap.com', '/v1/ticker/dash/')
|
json = self.get_json('api.coinmarketcap.com', '/v1/ticker/1437/')
|
||||||
quote_currencies = {}
|
quote_currencies = {}
|
||||||
if not isinstance(json, list):
|
if not isinstance(json, list):
|
||||||
return quote_currencies
|
return quote_currencies
|
||||||
json = json[0]
|
json = json[0]
|
||||||
for ccy, key in [
|
for ccy, key in [
|
||||||
('USD', 'price_usd'),
|
('USD', 'price_usd'),
|
||||||
('BTC', 'price_btc'),
|
|
||||||
]:
|
]:
|
||||||
quote_currencies[ccy] = Decimal(json[key])
|
quote_currencies[ccy] = Decimal(json[key])
|
||||||
return quote_currencies
|
return quote_currencies
|
||||||
|
@ -141,7 +168,7 @@ def get_exchanges_and_currencies():
|
||||||
import os, json
|
import os, json
|
||||||
path = os.path.join(os.path.dirname(__file__), 'currencies.json')
|
path = os.path.join(os.path.dirname(__file__), 'currencies.json')
|
||||||
try:
|
try:
|
||||||
with open(path, 'r') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
return json.loads(f.read())
|
return json.loads(f.read())
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -154,9 +181,11 @@ def get_exchanges_and_currencies():
|
||||||
exchange = klass(None, None)
|
exchange = klass(None, None)
|
||||||
try:
|
try:
|
||||||
d[name] = exchange.get_currencies()
|
d[name] = exchange.get_currencies()
|
||||||
|
print(name, "ok")
|
||||||
except:
|
except:
|
||||||
|
print(name, "error")
|
||||||
continue
|
continue
|
||||||
with open(path, 'w') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
f.write(json.dumps(d, indent=4, sort_keys=True))
|
f.write(json.dumps(d, indent=4, sort_keys=True))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -185,7 +214,10 @@ class FxThread(ThreadJob):
|
||||||
self.history_used_spot = False
|
self.history_used_spot = False
|
||||||
self.ccy_combo = None
|
self.ccy_combo = None
|
||||||
self.hist_checkbox = None
|
self.hist_checkbox = None
|
||||||
|
self.cache_dir = os.path.join(config.path, 'cache')
|
||||||
self.set_exchange(self.config_exchange())
|
self.set_exchange(self.config_exchange())
|
||||||
|
if not os.path.exists(self.cache_dir):
|
||||||
|
os.mkdir(self.cache_dir)
|
||||||
|
|
||||||
def get_currencies(self, h):
|
def get_currencies(self, h):
|
||||||
d = get_exchanges_by_ccy(h)
|
d = get_exchanges_by_ccy(h)
|
||||||
|
@ -208,7 +240,7 @@ class FxThread(ThreadJob):
|
||||||
# This runs from the plugins thread which catches exceptions
|
# This runs from the plugins thread which catches exceptions
|
||||||
if self.is_enabled():
|
if self.is_enabled():
|
||||||
if self.timeout ==0 and self.show_history():
|
if self.timeout ==0 and self.show_history():
|
||||||
self.exchange.get_historical_rates(self.ccy)
|
self.exchange.get_historical_rates(self.ccy, self.cache_dir)
|
||||||
if self.timeout <= time.time():
|
if self.timeout <= time.time():
|
||||||
self.timeout = time.time() + 150
|
self.timeout = time.time() + 150
|
||||||
self.exchange.update(self.ccy)
|
self.exchange.update(self.ccy)
|
||||||
|
@ -225,6 +257,12 @@ class FxThread(ThreadJob):
|
||||||
def set_history_config(self, b):
|
def set_history_config(self, b):
|
||||||
self.config.set_key('history_rates', bool(b))
|
self.config.set_key('history_rates', bool(b))
|
||||||
|
|
||||||
|
def get_history_capital_gains_config(self):
|
||||||
|
return bool(self.config.get('history_rates_capital_gains', False))
|
||||||
|
|
||||||
|
def set_history_capital_gains_config(self, b):
|
||||||
|
self.config.set_key('history_rates_capital_gains', bool(b))
|
||||||
|
|
||||||
def get_fiat_address_config(self):
|
def get_fiat_address_config(self):
|
||||||
return bool(self.config.get('fiat_address'))
|
return bool(self.config.get('fiat_address'))
|
||||||
|
|
||||||
|
@ -256,45 +294,65 @@ class FxThread(ThreadJob):
|
||||||
# A new exchange means new fx quotes, initially empty. Force
|
# A new exchange means new fx quotes, initially empty. Force
|
||||||
# a quote refresh
|
# a quote refresh
|
||||||
self.timeout = 0
|
self.timeout = 0
|
||||||
|
self.exchange.read_historical_rates(self.ccy, self.cache_dir)
|
||||||
|
|
||||||
def on_quotes(self):
|
def on_quotes(self):
|
||||||
self.network.trigger_callback('on_quotes')
|
if self.network:
|
||||||
|
self.network.trigger_callback('on_quotes')
|
||||||
|
|
||||||
def on_history(self):
|
def on_history(self):
|
||||||
self.network.trigger_callback('on_history')
|
if self.network:
|
||||||
|
self.network.trigger_callback('on_history')
|
||||||
|
|
||||||
def exchange_rate(self):
|
def exchange_rate(self):
|
||||||
'''Returns None, or the exchange rate as a Decimal'''
|
'''Returns None, or the exchange rate as a Decimal'''
|
||||||
rate = self.exchange.quotes.get(self.ccy)
|
rate = self.exchange.quotes.get(self.ccy)
|
||||||
if rate:
|
if rate is None:
|
||||||
return Decimal(rate)
|
return Decimal('NaN')
|
||||||
|
return Decimal(rate)
|
||||||
|
|
||||||
|
def format_amount(self, btc_balance):
|
||||||
|
rate = self.exchange_rate()
|
||||||
|
return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate)
|
||||||
|
|
||||||
def format_amount_and_units(self, btc_balance):
|
def format_amount_and_units(self, btc_balance):
|
||||||
rate = self.exchange_rate()
|
rate = self.exchange_rate()
|
||||||
return '' if rate is None else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
|
return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
|
||||||
|
|
||||||
def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
|
def get_fiat_status_text(self, btc_balance, base_unit, decimal_point):
|
||||||
rate = self.exchange_rate()
|
rate = self.exchange_rate()
|
||||||
return _(" (No FX rate available)") if rate is None else " 1 %s~%s %s" % (base_unit,
|
return _(" (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit,
|
||||||
self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
|
self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy)
|
||||||
|
|
||||||
|
def fiat_value(self, satoshis, rate):
|
||||||
|
return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate)
|
||||||
|
|
||||||
def value_str(self, satoshis, rate):
|
def value_str(self, satoshis, rate):
|
||||||
if satoshis is None: # Can happen with incomplete history
|
return self.format_fiat(self.fiat_value(satoshis, rate))
|
||||||
return _("Unknown")
|
|
||||||
if rate:
|
def format_fiat(self, value):
|
||||||
value = Decimal(satoshis) / COIN * Decimal(rate)
|
if value.is_nan():
|
||||||
return "%s" % (self.ccy_amount_str(value, True))
|
return _("No data")
|
||||||
return _("No data")
|
return "%s" % (self.ccy_amount_str(value, True))
|
||||||
|
|
||||||
def history_rate(self, d_t):
|
def history_rate(self, d_t):
|
||||||
|
if d_t is None:
|
||||||
|
return Decimal('NaN')
|
||||||
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 == 'NaN' and (datetime.today().date() - d_t.date()).days <= 2:
|
||||||
rate = self.exchange.quotes.get(self.ccy)
|
rate = self.exchange.quotes.get(self.ccy, 'NaN')
|
||||||
self.history_used_spot = True
|
self.history_used_spot = True
|
||||||
return rate
|
return Decimal(rate)
|
||||||
|
|
||||||
def historical_value_str(self, satoshis, d_t):
|
def historical_value_str(self, satoshis, d_t):
|
||||||
rate = self.history_rate(d_t)
|
return self.format_fiat(self.historical_value(satoshis, d_t))
|
||||||
return self.value_str(satoshis, rate)
|
|
||||||
|
def historical_value(self, satoshis, d_t):
|
||||||
|
return self.fiat_value(satoshis, self.history_rate(d_t))
|
||||||
|
|
||||||
|
def timestamp_rate(self, timestamp):
|
||||||
|
from electrum_zcash.util import timestamp_to_datetime
|
||||||
|
date = timestamp_to_datetime(timestamp)
|
||||||
|
return self.history_rate(date)
|
||||||
|
|
|
@ -43,7 +43,7 @@ from . import pem
|
||||||
|
|
||||||
|
|
||||||
def Connection(server, queue, config_path):
|
def Connection(server, queue, config_path):
|
||||||
"""Makes asynchronous connections to a remote electrum server.
|
"""Makes asynchronous connections to a remote Electrum server.
|
||||||
Returns the running thread that is making the connection.
|
Returns the running thread that is making the connection.
|
||||||
|
|
||||||
Once the thread has connected, it finishes, placing a tuple on the
|
Once the thread has connected, it finishes, placing a tuple on the
|
||||||
|
@ -144,7 +144,7 @@ class TcpConnection(threading.Thread, util.PrintError):
|
||||||
context = self.get_ssl_context(cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path)
|
context = self.get_ssl_context(cert_reqs=ssl.CERT_REQUIRED, ca_certs=ca_path)
|
||||||
s = context.wrap_socket(s, do_handshake_on_connect=True)
|
s = context.wrap_socket(s, do_handshake_on_connect=True)
|
||||||
except ssl.SSLError as e:
|
except ssl.SSLError as e:
|
||||||
print_error(e)
|
self.print_error(e)
|
||||||
s = None
|
s = None
|
||||||
except:
|
except:
|
||||||
return
|
return
|
||||||
|
@ -172,8 +172,10 @@ class TcpConnection(threading.Thread, util.PrintError):
|
||||||
# workaround android bug
|
# workaround android bug
|
||||||
cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
|
cert = re.sub("([^\n])-----END CERTIFICATE-----","\\1\n-----END CERTIFICATE-----",cert)
|
||||||
temporary_path = cert_path + '.temp'
|
temporary_path = cert_path + '.temp'
|
||||||
with open(temporary_path,"w") as f:
|
with open(temporary_path, "w", encoding='utf-8') as f:
|
||||||
f.write(cert)
|
f.write(cert)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
else:
|
else:
|
||||||
is_new = False
|
is_new = False
|
||||||
|
|
||||||
|
@ -199,7 +201,7 @@ class TcpConnection(threading.Thread, util.PrintError):
|
||||||
os.unlink(rej)
|
os.unlink(rej)
|
||||||
os.rename(temporary_path, rej)
|
os.rename(temporary_path, rej)
|
||||||
else:
|
else:
|
||||||
with open(cert_path) as f:
|
with open(cert_path, encoding='utf-8') as f:
|
||||||
cert = f.read()
|
cert = f.read()
|
||||||
try:
|
try:
|
||||||
b = pem.dePem(cert, 'CERTIFICATE')
|
b = pem.dePem(cert, 'CERTIFICATE')
|
||||||
|
@ -238,7 +240,7 @@ class TcpConnection(threading.Thread, util.PrintError):
|
||||||
|
|
||||||
class Interface(util.PrintError):
|
class Interface(util.PrintError):
|
||||||
"""The Interface class handles a socket connected to a single remote
|
"""The Interface class handles a socket connected to a single remote
|
||||||
electrum server. It's exposed API is:
|
Electrum server. Its exposed API is:
|
||||||
|
|
||||||
- Member functions close(), fileno(), get_responses(), has_timed_out(),
|
- Member functions close(), fileno(), get_responses(), has_timed_out(),
|
||||||
ping_required(), queue_request(), send_requests()
|
ping_required(), queue_request(), send_requests()
|
||||||
|
@ -295,8 +297,8 @@ class Interface(util.PrintError):
|
||||||
wire_requests = self.unsent_requests[0:n]
|
wire_requests = self.unsent_requests[0:n]
|
||||||
try:
|
try:
|
||||||
self.pipe.send_all([make_dict(*r) for r in wire_requests])
|
self.pipe.send_all([make_dict(*r) for r in wire_requests])
|
||||||
except socket.error as e:
|
except BaseException as e:
|
||||||
self.print_error("socket error:", e)
|
self.print_error("pipe send error:", e)
|
||||||
return False
|
return False
|
||||||
self.unsent_requests = self.unsent_requests[n:]
|
self.unsent_requests = self.unsent_requests[n:]
|
||||||
for request in wire_requests:
|
for request in wire_requests:
|
||||||
|
@ -396,7 +398,7 @@ def test_certificates():
|
||||||
certs = os.listdir(mydir)
|
certs = os.listdir(mydir)
|
||||||
for c in certs:
|
for c in certs:
|
||||||
p = os.path.join(mydir,c)
|
p = os.path.join(mydir,c)
|
||||||
with open(p) as f:
|
with open(p, encoding='utf-8') as f:
|
||||||
cert = f.read()
|
cert = f.read()
|
||||||
check_cert(c, cert)
|
check_cert(c, cert)
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,9 @@ from unicodedata import normalize
|
||||||
|
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
|
from . import constants
|
||||||
from .util import PrintError, InvalidPassword, hfu
|
from .util import (PrintError, InvalidPassword, hfu, WalletFileException,
|
||||||
|
BitcoinException)
|
||||||
from .mnemonic import Mnemonic, load_wordlist
|
from .mnemonic import Mnemonic, load_wordlist
|
||||||
from .plugins import run_hook
|
from .plugins import run_hook
|
||||||
|
|
||||||
|
@ -45,6 +46,10 @@ class KeyStore(PrintError):
|
||||||
def can_import(self):
|
def can_import(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def may_have_password(self):
|
||||||
|
"""Returns whether the keystore can be encrypted with a password."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_tx_derivations(self, tx):
|
def get_tx_derivations(self, tx):
|
||||||
keypairs = {}
|
keypairs = {}
|
||||||
for txin in tx.inputs():
|
for txin in tx.inputs():
|
||||||
|
@ -71,6 +76,8 @@ class KeyStore(PrintError):
|
||||||
return False
|
return False
|
||||||
return bool(self.get_tx_derivations(tx))
|
return bool(self.get_tx_derivations(tx))
|
||||||
|
|
||||||
|
def ready_to_sign(self):
|
||||||
|
return not self.is_watching_only()
|
||||||
|
|
||||||
|
|
||||||
class Software_KeyStore(KeyStore):
|
class Software_KeyStore(KeyStore):
|
||||||
|
@ -116,9 +123,6 @@ class Imported_KeyStore(Software_KeyStore):
|
||||||
def is_deterministic(self):
|
def is_deterministic(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_change_password(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_master_public_key(self):
|
def get_master_public_key(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -138,7 +142,14 @@ class Imported_KeyStore(Software_KeyStore):
|
||||||
def import_privkey(self, sec, password):
|
def import_privkey(self, sec, password):
|
||||||
txin_type, privkey, compressed = deserialize_privkey(sec)
|
txin_type, privkey, compressed = deserialize_privkey(sec)
|
||||||
pubkey = public_key_from_private_key(privkey, compressed)
|
pubkey = public_key_from_private_key(privkey, compressed)
|
||||||
self.keypairs[pubkey] = pw_encode(sec, password)
|
# re-serialize the key so the internal storage format is consistent
|
||||||
|
serialized_privkey = serialize_privkey(
|
||||||
|
privkey, compressed, txin_type, internal_use=True)
|
||||||
|
# NOTE: if the same pubkey is reused for multiple addresses (script types),
|
||||||
|
# there will only be one pubkey-privkey pair for it in self.keypairs,
|
||||||
|
# and the privkey will encode a txin_type but that txin_type cannot be trusted.
|
||||||
|
# Removing keys complicates this further.
|
||||||
|
self.keypairs[pubkey] = pw_encode(serialized_privkey, password)
|
||||||
return txin_type, pubkey
|
return txin_type, pubkey
|
||||||
|
|
||||||
def delete_imported_key(self, key):
|
def delete_imported_key(self, key):
|
||||||
|
@ -196,9 +207,6 @@ class Deterministic_KeyStore(Software_KeyStore):
|
||||||
def is_watching_only(self):
|
def is_watching_only(self):
|
||||||
return not self.has_seed()
|
return not self.has_seed()
|
||||||
|
|
||||||
def can_change_password(self):
|
|
||||||
return not self.is_watching_only()
|
|
||||||
|
|
||||||
def add_seed(self, seed):
|
def add_seed(self, seed):
|
||||||
if self.seed:
|
if self.seed:
|
||||||
raise Exception("a seed exists")
|
raise Exception("a seed exists")
|
||||||
|
@ -504,7 +512,7 @@ class Hardware_KeyStore(KeyStore, Xpub):
|
||||||
}
|
}
|
||||||
|
|
||||||
def unpaired(self):
|
def unpaired(self):
|
||||||
'''A device paired with the wallet was diconnected. This can be
|
'''A device paired with the wallet was disconnected. This can be
|
||||||
called in any thread context.'''
|
called in any thread context.'''
|
||||||
self.print_error("unpaired")
|
self.print_error("unpaired")
|
||||||
|
|
||||||
|
@ -522,9 +530,24 @@ class Hardware_KeyStore(KeyStore, Xpub):
|
||||||
assert not self.has_seed()
|
assert not self.has_seed()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def can_change_password(self):
|
def get_password_for_storage_encryption(self):
|
||||||
return False
|
from .storage import get_derivation_used_for_hw_device_encryption
|
||||||
|
client = self.plugin.get_client(self)
|
||||||
|
derivation = get_derivation_used_for_hw_device_encryption()
|
||||||
|
xpub = client.get_xpub(derivation, "standard")
|
||||||
|
password = self.get_pubkey_from_xpub(xpub, ())
|
||||||
|
return password
|
||||||
|
|
||||||
|
def has_usable_connection_with_device(self):
|
||||||
|
if not hasattr(self, 'plugin'):
|
||||||
|
return False
|
||||||
|
client = self.plugin.get_client(self, force_pair=False)
|
||||||
|
if client is None:
|
||||||
|
return False
|
||||||
|
return client.has_usable_connection_with_device()
|
||||||
|
|
||||||
|
def ready_to_sign(self):
|
||||||
|
return super().ready_to_sign() and self.has_usable_connection_with_device()
|
||||||
|
|
||||||
|
|
||||||
def bip39_normalize_passphrase(passphrase):
|
def bip39_normalize_passphrase(passphrase):
|
||||||
|
@ -571,10 +594,19 @@ def bip39_is_checksum_valid(mnemonic):
|
||||||
def from_bip39_seed(seed, passphrase, derivation):
|
def from_bip39_seed(seed, passphrase, derivation):
|
||||||
k = BIP32_KeyStore({})
|
k = BIP32_KeyStore({})
|
||||||
bip32_seed = bip39_to_seed(seed, passphrase)
|
bip32_seed = bip39_to_seed(seed, passphrase)
|
||||||
t = 'standard' # bip43
|
xtype = xtype_from_derivation(derivation)
|
||||||
k.add_xprv_from_seed(bip32_seed, t, derivation)
|
k.add_xprv_from_seed(bip32_seed, xtype, derivation)
|
||||||
return k
|
return k
|
||||||
|
|
||||||
|
|
||||||
|
def xtype_from_derivation(derivation):
|
||||||
|
"""Returns the script type to be used for this derivation."""
|
||||||
|
if derivation.startswith("m/84'") or derivation.startswith("m/49'"):
|
||||||
|
raise Exception('Unknown bip43 derivation purpose %s' % derivation[:5])
|
||||||
|
else:
|
||||||
|
return 'standard'
|
||||||
|
|
||||||
|
|
||||||
# extended pubkeys
|
# extended pubkeys
|
||||||
|
|
||||||
def is_xpubkey(x_pubkey):
|
def is_xpubkey(x_pubkey):
|
||||||
|
@ -599,7 +631,8 @@ def xpubkey_to_address(x_pubkey):
|
||||||
mpk, s = Old_KeyStore.parse_xpubkey(x_pubkey)
|
mpk, s = Old_KeyStore.parse_xpubkey(x_pubkey)
|
||||||
pubkey = Old_KeyStore.get_pubkey_from_mpk(mpk, s[0], s[1])
|
pubkey = Old_KeyStore.get_pubkey_from_mpk(mpk, s[0], s[1])
|
||||||
else:
|
else:
|
||||||
raise BaseException("Cannot parse pubkey")
|
raise BitcoinException("Cannot parse pubkey. prefix: {}"
|
||||||
|
.format(x_pubkey[0:2]))
|
||||||
if pubkey:
|
if pubkey:
|
||||||
address = public_key_to_p2pkh(bfh(pubkey))
|
address = public_key_to_p2pkh(bfh(pubkey))
|
||||||
return pubkey, address
|
return pubkey, address
|
||||||
|
@ -618,14 +651,15 @@ def hardware_keystore(d):
|
||||||
if hw_type in hw_keystores:
|
if hw_type in hw_keystores:
|
||||||
constructor = hw_keystores[hw_type]
|
constructor = hw_keystores[hw_type]
|
||||||
return constructor(d)
|
return constructor(d)
|
||||||
raise BaseException('unknown hardware type', hw_type)
|
raise WalletFileException('unknown hardware type: {}'.format(hw_type))
|
||||||
|
|
||||||
def load_keystore(storage, name):
|
def load_keystore(storage, name):
|
||||||
w = storage.get('wallet_type', 'standard')
|
|
||||||
d = storage.get(name, {})
|
d = storage.get(name, {})
|
||||||
t = d.get('type')
|
t = d.get('type')
|
||||||
if not t:
|
if not t:
|
||||||
raise BaseException('wallet format requires update')
|
raise WalletFileException(
|
||||||
|
'Wallet format requires update.\n'
|
||||||
|
'Cannot find keystore for name {}'.format(name))
|
||||||
if t == 'old':
|
if t == 'old':
|
||||||
k = Old_KeyStore(d)
|
k = Old_KeyStore(d)
|
||||||
elif t == 'imported':
|
elif t == 'imported':
|
||||||
|
@ -635,7 +669,8 @@ def load_keystore(storage, name):
|
||||||
elif t == 'hardware':
|
elif t == 'hardware':
|
||||||
k = hardware_keystore(d)
|
k = hardware_keystore(d)
|
||||||
else:
|
else:
|
||||||
raise BaseException('unknown wallet type', t)
|
raise WalletFileException(
|
||||||
|
'Unknown type {} for keystore named {}'.format(t, name))
|
||||||
return k
|
return k
|
||||||
|
|
||||||
|
|
||||||
|
@ -671,10 +706,9 @@ is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
|
||||||
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
|
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
|
||||||
|
|
||||||
|
|
||||||
def bip44_derivation(account_id):
|
def bip44_derivation(account_id, bip43_purpose=44):
|
||||||
bip = 44
|
coin = 1 if constants.net.TESTNET else 133
|
||||||
coin = 1 if bitcoin.NetworkConstants.TESTNET else 133
|
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
|
||||||
return "m/%d'/%d'/%d'" % (bip, coin, int(account_id))
|
|
||||||
|
|
||||||
def from_seed(seed, passphrase, is_p2sh):
|
def from_seed(seed, passphrase, is_p2sh):
|
||||||
t = seed_type(seed)
|
t = seed_type(seed)
|
||||||
|
@ -690,7 +724,7 @@ def from_seed(seed, passphrase, is_p2sh):
|
||||||
xtype = 'standard'
|
xtype = 'standard'
|
||||||
keystore.add_xprv_from_seed(bip32_seed, xtype, der)
|
keystore.add_xprv_from_seed(bip32_seed, xtype, der)
|
||||||
else:
|
else:
|
||||||
raise BaseException(t)
|
raise BitcoinException('Unexpected seed type {}'.format(t))
|
||||||
return keystore
|
return keystore
|
||||||
|
|
||||||
def from_private_key_list(text):
|
def from_private_key_list(text):
|
||||||
|
@ -724,5 +758,5 @@ def from_master_key(text):
|
||||||
elif is_xpub(text):
|
elif is_xpub(text):
|
||||||
k = from_xpub(text)
|
k = from_xpub(text)
|
||||||
else:
|
else:
|
||||||
raise BaseException('Invalid key')
|
raise BitcoinException('Invalid master key')
|
||||||
return k
|
return k
|
||||||
|
|
|
@ -91,7 +91,7 @@ def normalize_text(seed):
|
||||||
|
|
||||||
def load_wordlist(filename):
|
def load_wordlist(filename):
|
||||||
path = os.path.join(os.path.dirname(__file__), 'wordlist', filename)
|
path = os.path.join(os.path.dirname(__file__), 'wordlist', filename)
|
||||||
with open(path, 'r') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
s = f.read().strip()
|
s = f.read().strip()
|
||||||
s = unicodedata.normalize('NFKD', s)
|
s = unicodedata.normalize('NFKD', s)
|
||||||
lines = s.split('\n')
|
lines = s.split('\n')
|
||||||
|
@ -157,30 +157,24 @@ class Mnemonic(object):
|
||||||
i = i*n + k
|
i = i*n + k
|
||||||
return i
|
return i
|
||||||
|
|
||||||
def check_seed(self, seed, custom_entropy):
|
def make_seed(self, seed_type='standard', num_bits=132):
|
||||||
assert is_new_seed(seed)
|
|
||||||
i = self.mnemonic_decode(seed)
|
|
||||||
return i % custom_entropy == 0
|
|
||||||
|
|
||||||
def make_seed(self, seed_type='standard', num_bits=132, custom_entropy=1):
|
|
||||||
prefix = version.seed_prefix(seed_type)
|
prefix = version.seed_prefix(seed_type)
|
||||||
# increase num_bits in order to obtain a uniform distibution for the last word
|
# increase num_bits in order to obtain a uniform distribution for the last word
|
||||||
bpw = math.log(len(self.wordlist), 2)
|
bpw = math.log(len(self.wordlist), 2)
|
||||||
num_bits = int(math.ceil(num_bits/bpw) * bpw)
|
# rounding
|
||||||
# handle custom entropy; make sure we add at least 16 bits
|
n = int(math.ceil(num_bits/bpw) * bpw)
|
||||||
n_custom = int(math.ceil(math.log(custom_entropy, 2)))
|
print_error("make_seed. prefix: '%s'"%prefix, "entropy: %d bits"%n)
|
||||||
n = max(16, num_bits - n_custom)
|
entropy = 1
|
||||||
print_error("make_seed", prefix, "adding %d bits"%n)
|
while entropy < pow(2, n - bpw):
|
||||||
my_entropy = 1
|
|
||||||
while my_entropy < pow(2, n - bpw):
|
|
||||||
# try again if seed would not contain enough words
|
# try again if seed would not contain enough words
|
||||||
my_entropy = ecdsa.util.randrange(pow(2, n))
|
entropy = ecdsa.util.randrange(pow(2, n))
|
||||||
nonce = 0
|
nonce = 0
|
||||||
while True:
|
while True:
|
||||||
nonce += 1
|
nonce += 1
|
||||||
i = custom_entropy * (my_entropy + nonce)
|
i = entropy + nonce
|
||||||
seed = self.mnemonic_encode(i)
|
seed = self.mnemonic_encode(i)
|
||||||
assert i == self.mnemonic_decode(seed)
|
if i != self.mnemonic_decode(seed):
|
||||||
|
raise Exception('Cannot extract same entropy from mnemonic!')
|
||||||
if is_old_seed(seed):
|
if is_old_seed(seed):
|
||||||
continue
|
continue
|
||||||
if is_new_seed(seed, prefix):
|
if is_new_seed(seed, prefix):
|
||||||
|
|
213
lib/network.py
213
lib/network.py
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
# Electrum - Lightweight Bitcoin Client
|
# Electrum - Lightweight Bitcoin Client
|
||||||
# Copyright (c) 2011-2016 Thomas Voegtlin
|
# Copyright (c) 2011-2016 Thomas Voegtlin
|
||||||
#
|
#
|
||||||
|
@ -38,10 +37,11 @@ import socks
|
||||||
from . import util
|
from . import util
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
from .blockchain import HDR_LEN, CHUNK_LEN
|
from . import constants
|
||||||
from .interface import Connection, Interface
|
from .interface import Connection, Interface
|
||||||
from . import blockchain
|
from . import blockchain
|
||||||
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
|
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
|
||||||
|
from .i18n import _
|
||||||
|
|
||||||
|
|
||||||
NODES_RETRY_INTERVAL = 60
|
NODES_RETRY_INTERVAL = 60
|
||||||
|
@ -61,7 +61,7 @@ def parse_servers(result):
|
||||||
for v in item[2]:
|
for v in item[2]:
|
||||||
if re.match("[st]\d*", v):
|
if re.match("[st]\d*", v):
|
||||||
protocol, port = v[0], v[1:]
|
protocol, port = v[0], v[1:]
|
||||||
if port == '': port = bitcoin.NetworkConstants.DEFAULT_PORTS[protocol]
|
if port == '': port = constants.net.DEFAULT_PORTS[protocol]
|
||||||
out[protocol] = port
|
out[protocol] = port
|
||||||
elif re.match("v(.?)+", v):
|
elif re.match("v(.?)+", v):
|
||||||
version = v[1:]
|
version = v[1:]
|
||||||
|
@ -95,7 +95,7 @@ def filter_protocol(hostmap, protocol = 's'):
|
||||||
|
|
||||||
def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
|
def pick_random_server(hostmap = None, protocol = 's', exclude_set = set()):
|
||||||
if hostmap is None:
|
if hostmap is None:
|
||||||
hostmap = bitcoin.NetworkConstants.DEFAULT_SERVERS
|
hostmap = constants.net.DEFAULT_SERVERS
|
||||||
eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
|
eligible = list(set(filter_protocol(hostmap, protocol)) - exclude_set)
|
||||||
return random.choice(eligible) if eligible else None
|
return random.choice(eligible) if eligible else None
|
||||||
|
|
||||||
|
@ -139,8 +139,9 @@ def deserialize_proxy(s):
|
||||||
|
|
||||||
|
|
||||||
def deserialize_server(server_str):
|
def deserialize_server(server_str):
|
||||||
host, port, protocol = str(server_str).split(':')
|
host, port, protocol = str(server_str).rsplit(':', 2)
|
||||||
assert protocol in 'st'
|
if protocol not in 'st':
|
||||||
|
raise ValueError('invalid network protocol: {}'.format(protocol))
|
||||||
int(port) # Throw if cannot be converted to int
|
int(port) # Throw if cannot be converted to int
|
||||||
return host, port, protocol
|
return host, port, protocol
|
||||||
|
|
||||||
|
@ -174,15 +175,16 @@ class Network(util.DaemonThread):
|
||||||
if self.blockchain_index not in self.blockchains.keys():
|
if self.blockchain_index not in self.blockchains.keys():
|
||||||
self.blockchain_index = 0
|
self.blockchain_index = 0
|
||||||
# Server for addresses and transactions
|
# Server for addresses and transactions
|
||||||
self.default_server = self.config.get('server')
|
self.default_server = self.config.get('server', None)
|
||||||
# Sanitize default server
|
# Sanitize default server
|
||||||
try:
|
if self.default_server:
|
||||||
deserialize_server(self.default_server)
|
try:
|
||||||
except:
|
deserialize_server(self.default_server)
|
||||||
self.default_server = None
|
except:
|
||||||
|
self.print_error('Warning: failed to parse server-string; falling back to random.')
|
||||||
|
self.default_server = None
|
||||||
if not self.default_server:
|
if not self.default_server:
|
||||||
self.default_server = pick_random_server()
|
self.default_server = pick_random_server()
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.pending_sends = []
|
self.pending_sends = []
|
||||||
self.message_id = 0
|
self.message_id = 0
|
||||||
|
@ -219,6 +221,7 @@ class Network(util.DaemonThread):
|
||||||
self.interfaces = {}
|
self.interfaces = {}
|
||||||
self.auto_connect = self.config.get('auto_connect', True)
|
self.auto_connect = self.config.get('auto_connect', True)
|
||||||
self.connecting = set()
|
self.connecting = set()
|
||||||
|
self.requested_chunks = set()
|
||||||
self.socket_queue = queue.Queue()
|
self.socket_queue = queue.Queue()
|
||||||
self.start_network(deserialize_server(self.default_server)[2],
|
self.start_network(deserialize_server(self.default_server)[2],
|
||||||
deserialize_proxy(self.config.get('proxy')))
|
deserialize_proxy(self.config.get('proxy')))
|
||||||
|
@ -244,7 +247,7 @@ class Network(util.DaemonThread):
|
||||||
return []
|
return []
|
||||||
path = os.path.join(self.config.path, "recent_servers")
|
path = os.path.join(self.config.path, "recent_servers")
|
||||||
try:
|
try:
|
||||||
with open(path, "r") as f:
|
with open(path, "r", encoding='utf-8') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except:
|
except:
|
||||||
|
@ -256,7 +259,7 @@ class Network(util.DaemonThread):
|
||||||
path = os.path.join(self.config.path, "recent_servers")
|
path = os.path.join(self.config.path, "recent_servers")
|
||||||
s = json.dumps(self.recent_servers, indent=4, sort_keys=True)
|
s = json.dumps(self.recent_servers, indent=4, sort_keys=True)
|
||||||
try:
|
try:
|
||||||
with open(path, "w") as f:
|
with open(path, "w", encoding='utf-8') as f:
|
||||||
f.write(s)
|
f.write(s)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -306,6 +309,9 @@ class Network(util.DaemonThread):
|
||||||
# Resend unanswered requests
|
# Resend unanswered requests
|
||||||
requests = self.unanswered_requests.values()
|
requests = self.unanswered_requests.values()
|
||||||
self.unanswered_requests = {}
|
self.unanswered_requests = {}
|
||||||
|
if self.interface.ping_required():
|
||||||
|
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
|
||||||
|
self.queue_request('server.version', params, self.interface)
|
||||||
for request in requests:
|
for request in requests:
|
||||||
message_id = self.queue_request(request[0], request[1])
|
message_id = self.queue_request(request[0], request[1])
|
||||||
self.unanswered_requests[message_id] = request
|
self.unanswered_requests[message_id] = request
|
||||||
|
@ -314,15 +320,14 @@ class Network(util.DaemonThread):
|
||||||
self.queue_request('server.peers.subscribe', [])
|
self.queue_request('server.peers.subscribe', [])
|
||||||
self.request_fee_estimates()
|
self.request_fee_estimates()
|
||||||
self.queue_request('blockchain.relayfee', [])
|
self.queue_request('blockchain.relayfee', [])
|
||||||
if self.interface.ping_required():
|
for h in list(self.subscribed_addresses):
|
||||||
params = [ELECTRUM_VERSION, PROTOCOL_VERSION]
|
|
||||||
self.queue_request('server.version', params, self.interface)
|
|
||||||
for h in self.subscribed_addresses:
|
|
||||||
self.queue_request('blockchain.scripthash.subscribe', [h])
|
self.queue_request('blockchain.scripthash.subscribe', [h])
|
||||||
|
|
||||||
def request_fee_estimates(self):
|
def request_fee_estimates(self):
|
||||||
|
from .simple_config import FEE_ETA_TARGETS
|
||||||
self.config.requested_fee_estimates()
|
self.config.requested_fee_estimates()
|
||||||
for i in bitcoin.FEE_TARGETS:
|
self.queue_request('mempool.get_fee_histogram', [])
|
||||||
|
for i in FEE_ETA_TARGETS:
|
||||||
self.queue_request('blockchain.estimatefee', [i])
|
self.queue_request('blockchain.estimatefee', [i])
|
||||||
|
|
||||||
def get_status_value(self, key):
|
def get_status_value(self, key):
|
||||||
|
@ -332,6 +337,8 @@ class Network(util.DaemonThread):
|
||||||
value = self.banner
|
value = self.banner
|
||||||
elif key == 'fee':
|
elif key == 'fee':
|
||||||
value = self.config.fee_estimates
|
value = self.config.fee_estimates
|
||||||
|
elif key == 'fee_histogram':
|
||||||
|
value = self.config.mempool_fees
|
||||||
elif key == 'updated':
|
elif key == 'updated':
|
||||||
value = (self.get_local_height(), self.get_server_height())
|
value = (self.get_local_height(), self.get_server_height())
|
||||||
elif key == 'servers':
|
elif key == 'servers':
|
||||||
|
@ -359,7 +366,7 @@ class Network(util.DaemonThread):
|
||||||
return list(self.interfaces.keys())
|
return list(self.interfaces.keys())
|
||||||
|
|
||||||
def get_servers(self):
|
def get_servers(self):
|
||||||
out = bitcoin.NetworkConstants.DEFAULT_SERVERS
|
out = constants.net.DEFAULT_SERVERS
|
||||||
if self.irc_servers:
|
if self.irc_servers:
|
||||||
out.update(filter_version(self.irc_servers.copy()))
|
out.update(filter_version(self.irc_servers.copy()))
|
||||||
else:
|
else:
|
||||||
|
@ -543,6 +550,11 @@ class Network(util.DaemonThread):
|
||||||
elif method == 'server.donation_address':
|
elif method == 'server.donation_address':
|
||||||
if error is None:
|
if error is None:
|
||||||
self.donation_address = result
|
self.donation_address = result
|
||||||
|
elif method == 'mempool.get_fee_histogram':
|
||||||
|
if error is None:
|
||||||
|
self.print_error('fee_histogram', result)
|
||||||
|
self.config.mempool_fees = result
|
||||||
|
self.notify('fee_histogram')
|
||||||
elif method == 'blockchain.estimatefee':
|
elif method == 'blockchain.estimatefee':
|
||||||
if error is None and result > 0:
|
if error is None and result > 0:
|
||||||
i = params[0]
|
i = params[0]
|
||||||
|
@ -552,14 +564,12 @@ class Network(util.DaemonThread):
|
||||||
self.notify('fee')
|
self.notify('fee')
|
||||||
elif method == 'blockchain.relayfee':
|
elif method == 'blockchain.relayfee':
|
||||||
if error is None:
|
if error is None:
|
||||||
self.relay_fee = int(result * COIN)
|
self.relay_fee = int(result * COIN) if result is not None else None
|
||||||
self.print_error("relayfee", self.relay_fee)
|
self.print_error("relayfee", self.relay_fee)
|
||||||
elif method == 'blockchain.block.headers':
|
elif method == 'blockchain.block.get_chunk':
|
||||||
height, count = params
|
self.on_get_chunk(interface, response)
|
||||||
if count == 1:
|
elif method == 'blockchain.block.get_header':
|
||||||
self.on_get_header(interface, response, height)
|
self.on_get_header(interface, response)
|
||||||
elif count == CHUNK_LEN:
|
|
||||||
self.on_get_chunk(interface, response, height)
|
|
||||||
|
|
||||||
for callback in callbacks:
|
for callback in callbacks:
|
||||||
callback(response)
|
callback(response)
|
||||||
|
@ -668,7 +678,7 @@ class Network(util.DaemonThread):
|
||||||
# check cached response for subscriptions
|
# check cached response for subscriptions
|
||||||
r = self.sub_cache.get(k)
|
r = self.sub_cache.get(k)
|
||||||
if r is not None:
|
if r is not None:
|
||||||
util.print_error("cache hit", k)
|
self.print_error("cache hit", k)
|
||||||
callback(r)
|
callback(r)
|
||||||
else:
|
else:
|
||||||
message_id = self.queue_request(method, params)
|
message_id = self.queue_request(method, params)
|
||||||
|
@ -707,7 +717,7 @@ class Network(util.DaemonThread):
|
||||||
interface.mode = 'default'
|
interface.mode = 'default'
|
||||||
interface.request = None
|
interface.request = None
|
||||||
self.interfaces[server] = interface
|
self.interfaces[server] = interface
|
||||||
self.queue_request('blockchain.headers.subscribe', [True], interface)
|
self.queue_request('blockchain.headers.subscribe', [], interface)
|
||||||
if server == self.default_server:
|
if server == self.default_server:
|
||||||
self.switch_to_interface(server)
|
self.switch_to_interface(server)
|
||||||
#self.notify('interfaces')
|
#self.notify('interfaces')
|
||||||
|
@ -758,77 +768,77 @@ class Network(util.DaemonThread):
|
||||||
if self.config.is_fee_estimates_update_required():
|
if self.config.is_fee_estimates_update_required():
|
||||||
self.request_fee_estimates()
|
self.request_fee_estimates()
|
||||||
|
|
||||||
def request_chunk(self, interface, idx):
|
def request_chunk(self, interface, index):
|
||||||
interface.print_error("requesting chunk %d" % idx)
|
if index in self.requested_chunks:
|
||||||
self.queue_request('blockchain.block.headers',
|
return
|
||||||
[CHUNK_LEN*idx, CHUNK_LEN], interface)
|
interface.print_error("requesting chunk %d" % index)
|
||||||
interface.request = idx
|
self.requested_chunks.add(index)
|
||||||
interface.req_time = time.time()
|
self.queue_request('blockchain.block.get_chunk', [index], interface)
|
||||||
|
|
||||||
def on_get_chunk(self, interface, response, height):
|
def on_get_chunk(self, interface, response):
|
||||||
'''Handle receiving a chunk of block headers'''
|
'''Handle receiving a chunk of block headers'''
|
||||||
error = response.get('error')
|
error = response.get('error')
|
||||||
result = response.get('result')
|
result = response.get('result')
|
||||||
if result is None or error is not None:
|
params = response.get('params')
|
||||||
|
blockchain = interface.blockchain
|
||||||
|
if result is None or params is None or error is not None:
|
||||||
interface.print_error(error or 'bad response')
|
interface.print_error(error or 'bad response')
|
||||||
return
|
return
|
||||||
|
index = params[0]
|
||||||
hex_chunk = result.get('hex', None)
|
|
||||||
# Ignore unsolicited chunks
|
# Ignore unsolicited chunks
|
||||||
index = height // CHUNK_LEN
|
if index not in self.requested_chunks:
|
||||||
if interface.request != index or height / CHUNK_LEN != index:
|
interface.print_error("received chunk %d (unsolicited)" % index)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
connect = interface.blockchain.connect_chunk(index, hex_chunk)
|
interface.print_error("received chunk %d" % index)
|
||||||
# If not finished, get the next chunk
|
self.requested_chunks.remove(index)
|
||||||
|
connect = blockchain.connect_chunk(index, result)
|
||||||
if not connect:
|
if not connect:
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
return
|
return
|
||||||
if interface.blockchain.height() < interface.tip:
|
# If not finished, get the next chunk
|
||||||
|
if index >= len(blockchain.checkpoints) and blockchain.height() < interface.tip:
|
||||||
self.request_chunk(interface, index+1)
|
self.request_chunk(interface, index+1)
|
||||||
else:
|
else:
|
||||||
interface.request = None
|
|
||||||
interface.mode = 'default'
|
interface.mode = 'default'
|
||||||
interface.print_error('catch up done', interface.blockchain.height())
|
interface.print_error('catch up done', blockchain.height())
|
||||||
interface.blockchain.catch_up = None
|
blockchain.catch_up = None
|
||||||
self.notify('updated')
|
self.notify('updated')
|
||||||
|
|
||||||
def request_header(self, interface, height):
|
def request_header(self, interface, height):
|
||||||
#interface.print_error("requesting header %d" % height)
|
#interface.print_error("requesting header %d" % height)
|
||||||
self.queue_request('blockchain.block.headers', [height, 1], interface)
|
self.queue_request('blockchain.block.get_header', [height], interface)
|
||||||
interface.request = height
|
interface.request = height
|
||||||
interface.req_time = time.time()
|
interface.req_time = time.time()
|
||||||
|
|
||||||
def on_get_header(self, interface, response, height):
|
def on_get_header(self, interface, response):
|
||||||
'''Handle receiving a single block header'''
|
'''Handle receiving a single block header'''
|
||||||
result = response.get('result', {})
|
header = response.get('result')
|
||||||
hex_header = result.get('hex', None)
|
if not header:
|
||||||
|
interface.print_error(response)
|
||||||
|
self.connection_down(interface.server)
|
||||||
|
return
|
||||||
|
height = header.get('block_height')
|
||||||
if interface.request != height:
|
if interface.request != height:
|
||||||
interface.print_error("unsolicited header",interface.request, height)
|
interface.print_error("unsolicited header",interface.request, height)
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not hex_header:
|
|
||||||
interface.print_error(response)
|
|
||||||
self.connection_down(interface.server)
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(hex_header) != HDR_LEN*2:
|
|
||||||
interface.print_error('wrong header length', interface.request)
|
|
||||||
self.connection_down(interface.server)
|
|
||||||
return
|
|
||||||
|
|
||||||
header = blockchain.deserialize_header(bfh(hex_header), height)
|
|
||||||
|
|
||||||
chain = blockchain.check_header(header)
|
chain = blockchain.check_header(header)
|
||||||
if interface.mode == 'backward':
|
if interface.mode == 'backward':
|
||||||
if chain:
|
can_connect = blockchain.can_connect(header)
|
||||||
|
if can_connect and can_connect.catch_up is None:
|
||||||
|
interface.mode = 'catch_up'
|
||||||
|
interface.blockchain = can_connect
|
||||||
|
interface.blockchain.save_header(header)
|
||||||
|
next_height = height + 1
|
||||||
|
interface.blockchain.catch_up = interface.server
|
||||||
|
elif chain:
|
||||||
interface.print_error("binary search")
|
interface.print_error("binary search")
|
||||||
interface.mode = 'binary'
|
interface.mode = 'binary'
|
||||||
interface.blockchain = chain
|
interface.blockchain = chain
|
||||||
interface.good = height
|
interface.good = height
|
||||||
next_height = (interface.bad + interface.good) // 2
|
next_height = (interface.bad + interface.good) // 2
|
||||||
|
assert next_height >= self.max_checkpoint(), (interface.bad, interface.good)
|
||||||
else:
|
else:
|
||||||
if height == 0:
|
if height == 0:
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
|
@ -837,7 +847,7 @@ class Network(util.DaemonThread):
|
||||||
interface.bad = height
|
interface.bad = height
|
||||||
interface.bad_header = header
|
interface.bad_header = header
|
||||||
delta = interface.tip - height
|
delta = interface.tip - height
|
||||||
next_height = max(0, interface.tip - 2 * delta)
|
next_height = max(self.max_checkpoint(), interface.tip - 2 * delta)
|
||||||
|
|
||||||
elif interface.mode == 'binary':
|
elif interface.mode == 'binary':
|
||||||
if chain:
|
if chain:
|
||||||
|
@ -848,6 +858,7 @@ class Network(util.DaemonThread):
|
||||||
interface.bad_header = header
|
interface.bad_header = header
|
||||||
if interface.bad != interface.good + 1:
|
if interface.bad != interface.good + 1:
|
||||||
next_height = (interface.bad + interface.good) // 2
|
next_height = (interface.bad + interface.good) // 2
|
||||||
|
assert next_height >= self.max_checkpoint()
|
||||||
elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
|
elif not interface.blockchain.can_connect(interface.bad_header, check_height=False):
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
next_height = None
|
next_height = None
|
||||||
|
@ -912,11 +923,11 @@ class Network(util.DaemonThread):
|
||||||
self.notify('updated')
|
self.notify('updated')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise BaseException(interface.mode)
|
raise Exception(interface.mode)
|
||||||
# If not finished, get the next header
|
# If not finished, get the next header
|
||||||
if next_height:
|
if next_height:
|
||||||
if interface.mode == 'catch_up' and interface.tip > next_height + 50:
|
if interface.mode == 'catch_up' and interface.tip > next_height + 50:
|
||||||
self.request_chunk(interface, next_height // CHUNK_LEN)
|
self.request_chunk(interface, next_height // 2016)
|
||||||
else:
|
else:
|
||||||
self.request_header(interface, next_height)
|
self.request_header(interface, next_height)
|
||||||
else:
|
else:
|
||||||
|
@ -957,33 +968,18 @@ class Network(util.DaemonThread):
|
||||||
|
|
||||||
def init_headers_file(self):
|
def init_headers_file(self):
|
||||||
b = self.blockchains[0]
|
b = self.blockchains[0]
|
||||||
if b.get_hash(0) == bitcoin.NetworkConstants.GENESIS:
|
|
||||||
self.downloading_headers = False
|
|
||||||
return
|
|
||||||
filename = b.path()
|
filename = b.path()
|
||||||
def download_thread():
|
length = 80 * len(constants.net.CHECKPOINTS) * 2016
|
||||||
try:
|
if not os.path.exists(filename) or os.path.getsize(filename) < length:
|
||||||
import urllib.request, socket
|
with open(filename, 'wb') as f:
|
||||||
socket.setdefaulttimeout(30)
|
if length>0:
|
||||||
self.print_error("downloading ", bitcoin.NetworkConstants.HEADERS_URL)
|
f.seek(length-1)
|
||||||
urllib.request.urlretrieve(bitcoin.NetworkConstants.HEADERS_URL, filename + '.tmp')
|
f.write(b'\x00')
|
||||||
os.rename(filename + '.tmp', filename)
|
with b.lock:
|
||||||
self.print_error("done.")
|
b.update_size()
|
||||||
except Exception:
|
|
||||||
self.print_error("download failed. creating file", filename)
|
|
||||||
open(filename, 'wb+').close()
|
|
||||||
b = self.blockchains[0]
|
|
||||||
with b.lock: b.update_size()
|
|
||||||
self.downloading_headers = False
|
|
||||||
self.downloading_headers = True
|
|
||||||
t = threading.Thread(target = download_thread)
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.init_headers_file()
|
self.init_headers_file()
|
||||||
while self.is_running() and self.downloading_headers:
|
|
||||||
time.sleep(1)
|
|
||||||
while self.is_running():
|
while self.is_running():
|
||||||
self.maintain_sockets()
|
self.maintain_sockets()
|
||||||
self.wait_on_sockets()
|
self.wait_on_sockets()
|
||||||
|
@ -994,17 +990,12 @@ class Network(util.DaemonThread):
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
|
|
||||||
def on_notify_header(self, interface, header):
|
def on_notify_header(self, interface, header):
|
||||||
height = header.get('height')
|
height = header.get('block_height')
|
||||||
hex_header = header.get('hex')
|
if not height:
|
||||||
if not height or not hex_header:
|
|
||||||
return
|
return
|
||||||
|
if height < self.max_checkpoint():
|
||||||
if len(hex_header) != HDR_LEN*2:
|
|
||||||
interface.print_error('wrong header length', interface.request)
|
|
||||||
self.connection_down(interface.server)
|
self.connection_down(interface.server)
|
||||||
return
|
return
|
||||||
|
|
||||||
header = blockchain.deserialize_header(bfh(hex_header), height)
|
|
||||||
interface.tip_header = header
|
interface.tip_header = header
|
||||||
interface.tip = height
|
interface.tip = height
|
||||||
if interface.mode != 'default':
|
if interface.mode != 'default':
|
||||||
|
@ -1029,14 +1020,17 @@ class Network(util.DaemonThread):
|
||||||
interface.mode = 'backward'
|
interface.mode = 'backward'
|
||||||
interface.bad = height
|
interface.bad = height
|
||||||
interface.bad_header = header
|
interface.bad_header = header
|
||||||
self.request_header(interface, min(tip, height - 1))
|
self.request_header(interface, min(tip +1, height - 1))
|
||||||
else:
|
else:
|
||||||
chain = self.blockchains[0]
|
chain = self.blockchains[0]
|
||||||
if chain.catch_up is None:
|
if chain.catch_up is None:
|
||||||
chain.catch_up = interface
|
chain.catch_up = interface
|
||||||
interface.mode = 'catch_up'
|
interface.mode = 'catch_up'
|
||||||
interface.blockchain = chain
|
interface.blockchain = chain
|
||||||
|
self.print_error("switching to catchup mode", tip, self.blockchains)
|
||||||
self.request_header(interface, 0)
|
self.request_header(interface, 0)
|
||||||
|
else:
|
||||||
|
self.print_error("chain already catching up with", chain.catch_up.server)
|
||||||
|
|
||||||
def blockchain(self):
|
def blockchain(self):
|
||||||
if self.interface and self.interface.blockchain is not None:
|
if self.interface and self.interface.blockchain is not None:
|
||||||
|
@ -1061,7 +1055,7 @@ class Network(util.DaemonThread):
|
||||||
self.switch_to_interface(i.server)
|
self.switch_to_interface(i.server)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise BaseException('blockchain not found', index)
|
raise Exception('blockchain not found', index)
|
||||||
|
|
||||||
if self.interface:
|
if self.interface:
|
||||||
server = self.interface.server
|
server = self.interface.server
|
||||||
|
@ -1078,9 +1072,9 @@ class Network(util.DaemonThread):
|
||||||
try:
|
try:
|
||||||
r = q.get(True, timeout)
|
r = q.get(True, timeout)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
raise BaseException('Server did not answer')
|
raise util.TimeoutException(_('Server did not answer'))
|
||||||
if r.get('error'):
|
if r.get('error'):
|
||||||
raise BaseException(r.get('error'))
|
raise Exception(r.get('error'))
|
||||||
return r.get('result')
|
return r.get('result')
|
||||||
|
|
||||||
def broadcast(self, tx, timeout=30):
|
def broadcast(self, tx, timeout=30):
|
||||||
|
@ -1092,3 +1086,12 @@ class Network(util.DaemonThread):
|
||||||
if out != tx_hash:
|
if out != tx_hash:
|
||||||
return False, "error: " + out
|
return False, "error: " + out
|
||||||
return True, out
|
return True, out
|
||||||
|
|
||||||
|
def export_checkpoints(self, path):
|
||||||
|
# run manually from the console to generate checkpoints
|
||||||
|
cp = self.blockchain().get_checkpoints()
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(json.dumps(cp, indent=4))
|
||||||
|
|
||||||
|
def max_checkpoint(self):
|
||||||
|
return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
|
||||||
|
|
|
@ -40,6 +40,7 @@ except ImportError:
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from . import util
|
from . import util
|
||||||
from .util import print_error, bh2u, bfh
|
from .util import print_error, bh2u, bfh
|
||||||
|
from .util import export_meta, import_meta
|
||||||
from . import transaction
|
from . import transaction
|
||||||
from . import x509
|
from . import x509
|
||||||
from . import rsakey
|
from . import rsakey
|
||||||
|
@ -88,13 +89,13 @@ def get_payment_request(url):
|
||||||
error = "payment URL not pointing to a valid server"
|
error = "payment URL not pointing to a valid server"
|
||||||
elif u.scheme == 'file':
|
elif u.scheme == 'file':
|
||||||
try:
|
try:
|
||||||
with open(u.path, 'r') as f:
|
with open(u.path, 'r', encoding='utf-8') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
except IOError:
|
except IOError:
|
||||||
data = None
|
data = None
|
||||||
error = "payment URL not pointing to a valid file"
|
error = "payment URL not pointing to a valid file"
|
||||||
else:
|
else:
|
||||||
raise BaseException("unknown scheme", url)
|
raise Exception("unknown scheme", url)
|
||||||
pr = PaymentRequest(data, error)
|
pr = PaymentRequest(data, error)
|
||||||
return pr
|
return pr
|
||||||
|
|
||||||
|
@ -147,7 +148,7 @@ class PaymentRequest:
|
||||||
self.error = "Error: Cannot parse payment request"
|
self.error = "Error: Cannot parse payment request"
|
||||||
return False
|
return False
|
||||||
if not pr.signature:
|
if not pr.signature:
|
||||||
# the address will be dispayed as requestor
|
# the address will be displayed as requestor
|
||||||
self.requestor = None
|
self.requestor = None
|
||||||
return True
|
return True
|
||||||
if pr.pki_type in ["x509+sha256", "x509+sha1"]:
|
if pr.pki_type in ["x509+sha256", "x509+sha1"]:
|
||||||
|
@ -339,9 +340,9 @@ def verify_cert_chain(chain):
|
||||||
x.check_date()
|
x.check_date()
|
||||||
else:
|
else:
|
||||||
if not x.check_ca():
|
if not x.check_ca():
|
||||||
raise BaseException("ERROR: Supplied CA Certificate Error")
|
raise Exception("ERROR: Supplied CA Certificate Error")
|
||||||
if not cert_num > 1:
|
if not cert_num > 1:
|
||||||
raise BaseException("ERROR: CA Certificate Chain Not Provided by Payment Processor")
|
raise Exception("ERROR: CA Certificate Chain Not Provided by Payment Processor")
|
||||||
# if the root CA is not supplied, add it to the chain
|
# if the root CA is not supplied, add it to the chain
|
||||||
ca = x509_chain[cert_num-1]
|
ca = x509_chain[cert_num-1]
|
||||||
if ca.getFingerprint() not in ca_list:
|
if ca.getFingerprint() not in ca_list:
|
||||||
|
@ -351,7 +352,7 @@ def verify_cert_chain(chain):
|
||||||
root = ca_list[f]
|
root = ca_list[f]
|
||||||
x509_chain.append(root)
|
x509_chain.append(root)
|
||||||
else:
|
else:
|
||||||
raise BaseException("Supplied CA Not Found in Trusted CA Store.")
|
raise Exception("Supplied CA Not Found in Trusted CA Store.")
|
||||||
# verify the chain of signatures
|
# verify the chain of signatures
|
||||||
cert_num = len(x509_chain)
|
cert_num = len(x509_chain)
|
||||||
for i in range(1, cert_num):
|
for i in range(1, cert_num):
|
||||||
|
@ -372,10 +373,10 @@ def verify_cert_chain(chain):
|
||||||
hashBytes = bytearray(hashlib.sha512(data).digest())
|
hashBytes = bytearray(hashlib.sha512(data).digest())
|
||||||
verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA512 + hashBytes)
|
verify = pubkey.verify(sig, x509.PREFIX_RSA_SHA512 + hashBytes)
|
||||||
else:
|
else:
|
||||||
raise BaseException("Algorithm not supported")
|
raise Exception("Algorithm not supported")
|
||||||
util.print_error(self.error, algo.getComponentByName('algorithm'))
|
util.print_error(self.error, algo.getComponentByName('algorithm'))
|
||||||
if not verify:
|
if not verify:
|
||||||
raise BaseException("Certificate not Signed by Provided CA Certificate Chain")
|
raise Exception("Certificate not Signed by Provided CA Certificate Chain")
|
||||||
|
|
||||||
return x509_chain[0], ca
|
return x509_chain[0], ca
|
||||||
|
|
||||||
|
@ -384,9 +385,9 @@ def check_ssl_config(config):
|
||||||
from . import pem
|
from . import pem
|
||||||
key_path = config.get('ssl_privkey')
|
key_path = config.get('ssl_privkey')
|
||||||
cert_path = config.get('ssl_chain')
|
cert_path = config.get('ssl_chain')
|
||||||
with open(key_path, 'r') as f:
|
with open(key_path, 'r', encoding='utf-8') as f:
|
||||||
params = pem.parse_private_key(f.read())
|
params = pem.parse_private_key(f.read())
|
||||||
with open(cert_path, 'r') as f:
|
with open(cert_path, 'r', encoding='utf-8') as f:
|
||||||
s = f.read()
|
s = f.read()
|
||||||
bList = pem.dePemList(s, "CERTIFICATE")
|
bList = pem.dePemList(s, "CERTIFICATE")
|
||||||
# verify chain
|
# verify chain
|
||||||
|
@ -404,10 +405,10 @@ def check_ssl_config(config):
|
||||||
|
|
||||||
def sign_request_with_x509(pr, key_path, cert_path):
|
def sign_request_with_x509(pr, key_path, cert_path):
|
||||||
from . import pem
|
from . import pem
|
||||||
with open(key_path, 'r') as f:
|
with open(key_path, 'r', encoding='utf-8') as f:
|
||||||
params = pem.parse_private_key(f.read())
|
params = pem.parse_private_key(f.read())
|
||||||
privkey = rsakey.RSAKey(*params)
|
privkey = rsakey.RSAKey(*params)
|
||||||
with open(cert_path, 'r') as f:
|
with open(cert_path, 'r', encoding='utf-8') as f:
|
||||||
s = f.read()
|
s = f.read()
|
||||||
bList = pem.dePemList(s, "CERTIFICATE")
|
bList = pem.dePemList(s, "CERTIFICATE")
|
||||||
certificates = pb2.X509Certificates()
|
certificates = pb2.X509Certificates()
|
||||||
|
@ -452,7 +453,11 @@ class InvoiceStore(object):
|
||||||
|
|
||||||
def set_paid(self, pr, txid):
|
def set_paid(self, pr, txid):
|
||||||
pr.tx = txid
|
pr.tx = txid
|
||||||
self.paid[txid] = pr.get_id()
|
pr_id = pr.get_id()
|
||||||
|
self.paid[txid] = pr_id
|
||||||
|
if pr_id not in self.invoices:
|
||||||
|
# in case the user had deleted it previously
|
||||||
|
self.add(pr)
|
||||||
|
|
||||||
def load(self, d):
|
def load(self, d):
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
|
@ -467,24 +472,29 @@ class InvoiceStore(object):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def import_file(self, path):
|
def import_file(self, path):
|
||||||
try:
|
def validate(data):
|
||||||
with open(path, 'r') as f:
|
return data # TODO
|
||||||
d = json.loads(f.read())
|
import_meta(path, validate, self.on_import)
|
||||||
self.load(d)
|
|
||||||
except:
|
def on_import(self, data):
|
||||||
traceback.print_exc(file=sys.stderr)
|
self.load(data)
|
||||||
return
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self):
|
def export_file(self, filename):
|
||||||
l = {}
|
export_meta(self.dump(), filename)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
d = {}
|
||||||
for k, pr in self.invoices.items():
|
for k, pr in self.invoices.items():
|
||||||
l[k] = {
|
d[k] = {
|
||||||
'hex': bh2u(pr.raw),
|
'hex': bh2u(pr.raw),
|
||||||
'requestor': pr.requestor,
|
'requestor': pr.requestor,
|
||||||
'txid': pr.tx
|
'txid': pr.tx
|
||||||
}
|
}
|
||||||
self.storage.put('invoices', l)
|
return d
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self.storage.put('invoices', self.dump())
|
||||||
|
|
||||||
def get_status(self, key):
|
def get_status(self, key):
|
||||||
pr = self.get(key)
|
pr = self.get(key)
|
||||||
|
|
49
lib/plot.py
49
lib/plot.py
|
@ -1,30 +1,32 @@
|
||||||
from PyQt5.QtGui import *
|
|
||||||
from electrum.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from electrum.bitcoin import COIN
|
|
||||||
|
|
||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.use('Qt5Agg')
|
matplotlib.use('Qt5Agg')
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.dates as md
|
import matplotlib.dates as md
|
||||||
from matplotlib.patches import Ellipse
|
|
||||||
from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
|
from .i18n import _
|
||||||
|
from .bitcoin import COIN
|
||||||
|
|
||||||
|
|
||||||
def plot_history(wallet, history):
|
class NothingToPlotException(Exception):
|
||||||
|
def __str__(self):
|
||||||
|
return _("Nothing to plot.")
|
||||||
|
|
||||||
|
|
||||||
|
def plot_history(history):
|
||||||
|
if len(history) == 0:
|
||||||
|
raise NothingToPlotException()
|
||||||
hist_in = defaultdict(int)
|
hist_in = defaultdict(int)
|
||||||
hist_out = defaultdict(int)
|
hist_out = defaultdict(int)
|
||||||
for item in history:
|
for item in history:
|
||||||
tx_hash, height, confirmations, timestamp, value, balance = item
|
if not item['confirmations']:
|
||||||
if not confirmations:
|
|
||||||
continue
|
continue
|
||||||
if timestamp is None:
|
if item['timestamp'] is None:
|
||||||
continue
|
continue
|
||||||
value = value*1./COIN
|
value = item['value'].value/COIN
|
||||||
date = datetime.datetime.fromtimestamp(timestamp)
|
date = item['date']
|
||||||
datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))
|
datenum = int(md.date2num(datetime.date(date.year, date.month, 1)))
|
||||||
if value > 0:
|
if value > 0:
|
||||||
hist_in[datenum] += value
|
hist_in[datenum] += value
|
||||||
|
@ -43,10 +45,19 @@ def plot_history(wallet, history):
|
||||||
xfmt = md.DateFormatter('%Y-%m')
|
xfmt = md.DateFormatter('%Y-%m')
|
||||||
ax.xaxis.set_major_formatter(xfmt)
|
ax.xaxis.set_major_formatter(xfmt)
|
||||||
width = 20
|
width = 20
|
||||||
dates, values = zip(*sorted(hist_in.items()))
|
|
||||||
r1 = axarr[0].bar(dates, values, width, label='incoming')
|
r1 = None
|
||||||
axarr[0].legend(loc='upper left')
|
r2 = None
|
||||||
dates, values = zip(*sorted(hist_out.items()))
|
dates_values = list(zip(*sorted(hist_in.items())))
|
||||||
r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')
|
if dates_values and len(dates_values) == 2:
|
||||||
axarr[1].legend(loc='upper left')
|
dates, values = dates_values
|
||||||
|
r1 = axarr[0].bar(dates, values, width, label='incoming')
|
||||||
|
axarr[0].legend(loc='upper left')
|
||||||
|
dates_values = list(zip(*sorted(hist_out.items())))
|
||||||
|
if dates_values and len(dates_values) == 2:
|
||||||
|
dates, values = dates_values
|
||||||
|
r2 = axarr[1].bar(dates, values, width, color='r', label='outgoing')
|
||||||
|
axarr[1].legend(loc='upper left')
|
||||||
|
if r1 is None and r2 is None:
|
||||||
|
raise NothingToPlotException()
|
||||||
return plt
|
return plt
|
||||||
|
|
|
@ -312,6 +312,8 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
# What we recognise. Each entry is a (vendor_id, product_id)
|
# What we recognise. Each entry is a (vendor_id, product_id)
|
||||||
# pair.
|
# pair.
|
||||||
self.recognised_hardware = set()
|
self.recognised_hardware = set()
|
||||||
|
# Custom enumerate functions for devices we don't know about.
|
||||||
|
self.enumerate_func = set()
|
||||||
# For synchronization
|
# For synchronization
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
self.hid_lock = threading.RLock()
|
self.hid_lock = threading.RLock()
|
||||||
|
@ -334,6 +336,9 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
for pair in device_pairs:
|
for pair in device_pairs:
|
||||||
self.recognised_hardware.add(pair)
|
self.recognised_hardware.add(pair)
|
||||||
|
|
||||||
|
def register_enumerate_func(self, func):
|
||||||
|
self.enumerate_func.add(func)
|
||||||
|
|
||||||
def create_client(self, device, handler, plugin):
|
def create_client(self, device, handler, plugin):
|
||||||
# Get from cache first
|
# Get from cache first
|
||||||
client = self.client_lookup(device.id_)
|
client = self.client_lookup(device.id_)
|
||||||
|
@ -362,15 +367,20 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
if not xpub in self.xpub_ids:
|
if not xpub in self.xpub_ids:
|
||||||
return
|
return
|
||||||
_id = self.xpub_ids.pop(xpub)
|
_id = self.xpub_ids.pop(xpub)
|
||||||
client = self.client_lookup(_id)
|
self._close_client(_id)
|
||||||
self.clients.pop(client, None)
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
def unpair_id(self, id_):
|
def unpair_id(self, id_):
|
||||||
xpub = self.xpub_by_id(id_)
|
xpub = self.xpub_by_id(id_)
|
||||||
if xpub:
|
if xpub:
|
||||||
self.unpair_xpub(xpub)
|
self.unpair_xpub(xpub)
|
||||||
|
else:
|
||||||
|
self._close_client(id_)
|
||||||
|
|
||||||
|
def _close_client(self, id_):
|
||||||
|
client = self.client_lookup(id_)
|
||||||
|
self.clients.pop(client, None)
|
||||||
|
if client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
def pair_xpub(self, xpub, id_):
|
def pair_xpub(self, xpub, id_):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -393,7 +403,7 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
def client_for_keystore(self, plugin, handler, keystore, force_pair):
|
def client_for_keystore(self, plugin, handler, keystore, force_pair):
|
||||||
self.print_error("getting client for keystore")
|
self.print_error("getting client for keystore")
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise BaseException(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
|
raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
|
||||||
handler.update_status(False)
|
handler.update_status(False)
|
||||||
devices = self.scan_devices()
|
devices = self.scan_devices()
|
||||||
xpub = keystore.xpub
|
xpub = keystore.xpub
|
||||||
|
@ -442,21 +452,23 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
# The user input has wrong PIN or passphrase, or cancelled input,
|
# The user input has wrong PIN or passphrase, or cancelled input,
|
||||||
# or it is not pairable
|
# or it is not pairable
|
||||||
raise DeviceUnpairableError(
|
raise DeviceUnpairableError(
|
||||||
_('Electrum-Zcash cannot pair with your %s.\n\n'
|
_('Electrum-Zcash cannot pair with your {}.\n\n'
|
||||||
'Before you request Zcash coins to be sent to addresses in this '
|
'Before you request Zcash coins to be sent to addresses in this '
|
||||||
'wallet, ensure you can pair with your device, or that you have '
|
'wallet, ensure you can pair with your device, or that you have '
|
||||||
'its seed (and passphrase, if any). Otherwise all coins you '
|
'its seed (and passphrase, if any). Otherwise all coins you '
|
||||||
'receive will be unspendable.') % plugin.device)
|
'receive will be unspendable.').format(plugin.device))
|
||||||
|
|
||||||
def unpaired_device_infos(self, handler, plugin, devices=None):
|
def unpaired_device_infos(self, handler, plugin, devices=None):
|
||||||
'''Returns a list of DeviceInfo objects: one for each connected,
|
'''Returns a list of DeviceInfo objects: one for each connected,
|
||||||
unpaired device accepted by the plugin.'''
|
unpaired device accepted by the plugin.'''
|
||||||
|
if not plugin.libraries_available:
|
||||||
|
raise Exception('Missing libraries for {}'.format(plugin.name))
|
||||||
if devices is None:
|
if devices is None:
|
||||||
devices = self.scan_devices()
|
devices = self.scan_devices()
|
||||||
devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)]
|
devices = [dev for dev in devices if not self.xpub_by_id(dev.id_)]
|
||||||
infos = []
|
infos = []
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if not device.product_key in plugin.DEVICE_IDS:
|
if device.product_key not in plugin.DEVICE_IDS:
|
||||||
continue
|
continue
|
||||||
client = self.create_client(device, handler, plugin)
|
client = self.create_client(device, handler, plugin)
|
||||||
if not client:
|
if not client:
|
||||||
|
@ -472,9 +484,14 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
infos = self.unpaired_device_infos(handler, plugin, devices)
|
infos = self.unpaired_device_infos(handler, plugin, devices)
|
||||||
if infos:
|
if infos:
|
||||||
break
|
break
|
||||||
msg = _('Please insert your %s. Verify the cable is '
|
msg = _('Please insert your {}').format(plugin.device)
|
||||||
'connected and that no other application is using it.\n\n'
|
if keystore.label:
|
||||||
'Try to connect again?') % plugin.device
|
msg += ' ({})'.format(keystore.label)
|
||||||
|
msg += '. {}\n\n{}'.format(
|
||||||
|
_('Verify the cable is connected and that '
|
||||||
|
'no other application is using it.'),
|
||||||
|
_('Try to connect again?')
|
||||||
|
)
|
||||||
if not handler.yes_no_question(msg):
|
if not handler.yes_no_question(msg):
|
||||||
raise UserCancelled()
|
raise UserCancelled()
|
||||||
devices = None
|
devices = None
|
||||||
|
@ -484,27 +501,27 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
for info in infos:
|
for info in infos:
|
||||||
if info.label == keystore.label:
|
if info.label == keystore.label:
|
||||||
return info
|
return info
|
||||||
msg = _("Please select which %s device to use:") % plugin.device
|
msg = _("Please select which {} device to use:").format(plugin.device)
|
||||||
descriptions = [info.label + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos]
|
descriptions = [str(info.label) + ' (%s)'%(_("initialized") if info.initialized else _("wiped")) for info in infos]
|
||||||
c = handler.query_choice(msg, descriptions)
|
c = handler.query_choice(msg, descriptions)
|
||||||
if c is None:
|
if c is None:
|
||||||
raise UserCancelled()
|
raise UserCancelled()
|
||||||
info = infos[c]
|
info = infos[c]
|
||||||
# save new label
|
# save new label
|
||||||
keystore.set_label(info.label)
|
keystore.set_label(info.label)
|
||||||
handler.win.wallet.save_keystore()
|
if handler.win.wallet is not None:
|
||||||
|
handler.win.wallet.save_keystore()
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def scan_devices(self):
|
def _scan_devices_with_hid(self):
|
||||||
# All currently supported hardware libraries use hid, so we
|
try:
|
||||||
# assume it here. This can be easily abstracted if necessary.
|
import hid
|
||||||
# Note this import must be local so those without hardware
|
except ImportError:
|
||||||
# wallet libraries are not affected.
|
return []
|
||||||
import hid
|
|
||||||
self.print_error("scanning devices...")
|
|
||||||
with self.hid_lock:
|
with self.hid_lock:
|
||||||
hid_list = hid.enumerate(0, 0)
|
hid_list = hid.enumerate(0, 0)
|
||||||
# First see what's connected that we know about
|
|
||||||
devices = []
|
devices = []
|
||||||
for d in hid_list:
|
for d in hid_list:
|
||||||
product_key = (d['vendor_id'], d['product_id'])
|
product_key = (d['vendor_id'], d['product_id'])
|
||||||
|
@ -518,14 +535,31 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
id_ += str(interface_number) + str(usage_page)
|
id_ += str(interface_number) + str(usage_page)
|
||||||
devices.append(Device(d['path'], interface_number,
|
devices.append(Device(d['path'], interface_number,
|
||||||
id_, product_key, usage_page))
|
id_, product_key, usage_page))
|
||||||
|
return devices
|
||||||
|
|
||||||
# Now find out what was disconnected
|
def scan_devices(self):
|
||||||
|
self.print_error("scanning devices...")
|
||||||
|
|
||||||
|
# First see what's connected that we know about
|
||||||
|
devices = self._scan_devices_with_hid()
|
||||||
|
|
||||||
|
# Let plugin handlers enumerate devices we don't know about
|
||||||
|
for f in self.enumerate_func:
|
||||||
|
try:
|
||||||
|
new_devices = f()
|
||||||
|
except BaseException as e:
|
||||||
|
self.print_error('custom device enum failed. func {}, error {}'
|
||||||
|
.format(str(f), str(e)))
|
||||||
|
else:
|
||||||
|
devices.extend(new_devices)
|
||||||
|
|
||||||
|
# find out what was disconnected
|
||||||
pairs = [(dev.path, dev.id_) for dev in devices]
|
pairs = [(dev.path, dev.id_) for dev in devices]
|
||||||
disconnected_ids = []
|
disconnected_ids = []
|
||||||
with self.lock:
|
with self.lock:
|
||||||
connected = {}
|
connected = {}
|
||||||
for client, pair in self.clients.items():
|
for client, pair in self.clients.items():
|
||||||
if pair in pairs:
|
if pair in pairs and client.has_usable_connection_with_device():
|
||||||
connected[client] = pair
|
connected[client] = pair
|
||||||
else:
|
else:
|
||||||
disconnected_ids.append(pair[1])
|
disconnected_ids.append(pair[1])
|
||||||
|
|
|
@ -29,18 +29,18 @@ import ctypes
|
||||||
|
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
name = 'libzbar.dylib'
|
name = 'libzbar.dylib'
|
||||||
elif sys.platform == 'windows':
|
elif sys.platform in ('windows', 'win32'):
|
||||||
name = 'libzbar.dll'
|
name = 'libzbar-0.dll'
|
||||||
else:
|
else:
|
||||||
name = 'libzbar.so.0'
|
name = 'libzbar.so.0'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
libzbar = ctypes.cdll.LoadLibrary(name)
|
libzbar = ctypes.cdll.LoadLibrary(name)
|
||||||
except OSError:
|
except BaseException:
|
||||||
libzbar = None
|
libzbar = None
|
||||||
|
|
||||||
|
|
||||||
def scan_barcode(device='', timeout=-1, display=True, threaded=False):
|
def scan_barcode(device='', timeout=-1, display=True, threaded=False, try_again=True):
|
||||||
if libzbar is None:
|
if libzbar is None:
|
||||||
raise RuntimeError("Cannot start QR scanner; zbar not available.")
|
raise RuntimeError("Cannot start QR scanner; zbar not available.")
|
||||||
libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p
|
libzbar.zbar_symbol_get_data.restype = ctypes.c_char_p
|
||||||
|
@ -50,6 +50,10 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False):
|
||||||
proc = libzbar.zbar_processor_create(threaded)
|
proc = libzbar.zbar_processor_create(threaded)
|
||||||
libzbar.zbar_processor_request_size(proc, 640, 480)
|
libzbar.zbar_processor_request_size(proc, 640, 480)
|
||||||
if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:
|
if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:
|
||||||
|
if try_again:
|
||||||
|
# workaround for a bug in "ZBar for Windows"
|
||||||
|
# libzbar.zbar_processor_init always seem to fail the first time around
|
||||||
|
return scan_barcode(device, timeout, display, threaded, try_again=False)
|
||||||
raise RuntimeError("Can not start QR scanner; initialization failed.")
|
raise RuntimeError("Can not start QR scanner; initialization failed.")
|
||||||
libzbar.zbar_processor_set_visible(proc)
|
libzbar.zbar_processor_set_visible(proc)
|
||||||
if libzbar.zbar_process_one(proc, timeout):
|
if libzbar.zbar_process_one(proc, timeout):
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"127.0.0.1": {
|
||||||
|
"pruning": "-",
|
||||||
|
"s": "51022",
|
||||||
|
"t": "51021",
|
||||||
|
"version": "1.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"localhost": {
|
"127.0.0.1": {
|
||||||
"pruning": "-",
|
"pruning": "-",
|
||||||
"s": "51022",
|
"s": "51022",
|
||||||
"t": "51021",
|
"t": "51021",
|
||||||
|
|
|
@ -5,11 +5,21 @@ import os
|
||||||
import stat
|
import stat
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from .util import user_dir, print_error, print_stderr, PrintError
|
|
||||||
|
|
||||||
from .bitcoin import MAX_FEE_RATE, FEE_TARGETS
|
from .util import (user_dir, print_error, PrintError,
|
||||||
|
NoDynamicFeeEstimates, format_satoshis)
|
||||||
|
from .i18n import _
|
||||||
|
|
||||||
|
FEE_ETA_TARGETS = [25, 10, 5, 2]
|
||||||
|
FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000]
|
||||||
|
|
||||||
|
# satoshi per kbyte
|
||||||
|
FEERATE_MAX_DYNAMIC = 150000
|
||||||
|
FEERATE_WARNING_HIGH_FEE = 100000
|
||||||
|
FEERATE_FALLBACK_STATIC_FEE = 1000
|
||||||
|
FEERATE_DEFAULT_RELAY = 1000
|
||||||
|
FEERATE_STATIC_VALUES = [150, 300, 500, 1000, 1500, 2500, 3500, 5000, 7500, 10000]
|
||||||
|
|
||||||
SYSTEM_CONFIG_PATH = "/etc/electrum-zcash.conf"
|
|
||||||
|
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
|
@ -24,35 +34,37 @@ def set_config(c):
|
||||||
config = c
|
config = c
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_CONFIG_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
class SimpleConfig(PrintError):
|
class SimpleConfig(PrintError):
|
||||||
"""
|
"""
|
||||||
The SimpleConfig class is responsible for handling operations involving
|
The SimpleConfig class is responsible for handling operations involving
|
||||||
configuration files.
|
configuration files.
|
||||||
|
|
||||||
There are 3 different sources of possible configuration values:
|
There are two different sources of possible configuration values:
|
||||||
1. Command line options.
|
1. Command line options.
|
||||||
2. User configuration (in the user's config directory)
|
2. User configuration (in the user's config directory)
|
||||||
3. System configuration (in /etc/)
|
They are taken in order (1. overrides config options set in 2.)
|
||||||
They are taken in order (1. overrides config options set in 2., that
|
|
||||||
override config set in 3.)
|
|
||||||
"""
|
"""
|
||||||
fee_rates = [5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000]
|
|
||||||
|
|
||||||
def __init__(self, options={}, read_system_config_function=None,
|
def __init__(self, options=None, read_user_config_function=None,
|
||||||
read_user_config_function=None, read_user_dir_function=None):
|
read_user_dir_function=None):
|
||||||
|
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
|
||||||
# This lock needs to be acquired for updating and reading the config in
|
# This lock needs to be acquired for updating and reading the config in
|
||||||
# a thread-safe way.
|
# a thread-safe way.
|
||||||
self.lock = threading.RLock()
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
|
self.mempool_fees = {}
|
||||||
self.fee_estimates = {}
|
self.fee_estimates = {}
|
||||||
self.fee_estimates_last_updated = {}
|
self.fee_estimates_last_updated = {}
|
||||||
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
|
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
|
||||||
|
|
||||||
# The following two functions are there for dependency injection when
|
# The following two functions are there for dependency injection when
|
||||||
# testing.
|
# testing.
|
||||||
if read_system_config_function is None:
|
|
||||||
read_system_config_function = read_system_config
|
|
||||||
if read_user_config_function is None:
|
if read_user_config_function is None:
|
||||||
read_user_config_function = read_user_config
|
read_user_config_function = read_user_config
|
||||||
if read_user_dir_function is None:
|
if read_user_dir_function is None:
|
||||||
|
@ -62,90 +74,154 @@ class SimpleConfig(PrintError):
|
||||||
|
|
||||||
# The command line options
|
# The command line options
|
||||||
self.cmdline_options = deepcopy(options)
|
self.cmdline_options = deepcopy(options)
|
||||||
|
# don't allow to be set on CLI:
|
||||||
# Portable wallets don't use a system config
|
self.cmdline_options.pop('config_version', None)
|
||||||
if self.cmdline_options.get('portable', False):
|
|
||||||
self.system_config = {}
|
|
||||||
else:
|
|
||||||
self.system_config = read_system_config_function()
|
|
||||||
|
|
||||||
# Set self.path and read the user config
|
# Set self.path and read the user config
|
||||||
self.user_config = {} # for self.get in electrum_path()
|
self.user_config = {} # for self.get in electrum_path()
|
||||||
self.path = self.electrum_path()
|
self.path = self.electrum_path()
|
||||||
self.user_config = read_user_config_function(self.path)
|
self.user_config = read_user_config_function(self.path)
|
||||||
# Upgrade obsolete keys
|
if not self.user_config:
|
||||||
self.fixup_keys({'auto_cycle': 'auto_connect'})
|
# avoid new config getting upgraded
|
||||||
|
self.user_config = {'config_version': FINAL_CONFIG_VERSION}
|
||||||
|
|
||||||
|
# config "upgrade" - CLI options
|
||||||
|
self.rename_config_keys(
|
||||||
|
self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
|
||||||
|
|
||||||
|
# config upgrade - user config
|
||||||
|
if self.requires_upgrade():
|
||||||
|
self.upgrade()
|
||||||
|
|
||||||
# Make a singleton instance of 'self'
|
# Make a singleton instance of 'self'
|
||||||
set_config(self)
|
set_config(self)
|
||||||
|
|
||||||
def electrum_path(self):
|
def electrum_path(self):
|
||||||
# Read electrum_path from command line / system configuration
|
# Read electrum_path from command line
|
||||||
# Otherwise use the user's default data directory.
|
# Otherwise use the user's default data directory.
|
||||||
path = self.get('electrum_path')
|
path = self.get('electrum_path')
|
||||||
if path is None:
|
if path is None:
|
||||||
path = self.user_dir()
|
path = self.user_dir()
|
||||||
|
|
||||||
|
def make_dir(path):
|
||||||
|
# Make directory if it does not yet exist.
|
||||||
|
if not os.path.exists(path):
|
||||||
|
if os.path.islink(path):
|
||||||
|
raise Exception('Dangling link: ' + path)
|
||||||
|
os.mkdir(path)
|
||||||
|
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
||||||
|
|
||||||
|
make_dir(path)
|
||||||
if self.get('testnet'):
|
if self.get('testnet'):
|
||||||
path = os.path.join(path, 'testnet')
|
path = os.path.join(path, 'testnet')
|
||||||
|
make_dir(path)
|
||||||
# Make directory if it does not yet exist.
|
elif self.get('regtest'):
|
||||||
if not os.path.exists(path):
|
path = os.path.join(path, 'regtest')
|
||||||
if os.path.islink(path):
|
make_dir(path)
|
||||||
raise BaseException('Dangling link: ' + path)
|
|
||||||
os.makedirs(path)
|
|
||||||
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
|
||||||
|
|
||||||
self.print_error("electrum-zcash directory", path)
|
self.print_error("electrum-zcash directory", path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def fixup_config_keys(self, config, keypairs):
|
def rename_config_keys(self, config, keypairs, deprecation_warning=False):
|
||||||
|
"""Migrate old key names to new ones"""
|
||||||
updated = False
|
updated = False
|
||||||
for old_key, new_key in keypairs.items():
|
for old_key, new_key in keypairs.items():
|
||||||
if old_key in config:
|
if old_key in config:
|
||||||
if not new_key in config:
|
if new_key not in config:
|
||||||
config[new_key] = config[old_key]
|
config[new_key] = config[old_key]
|
||||||
|
if deprecation_warning:
|
||||||
|
self.print_stderr('Note that the {} variable has been deprecated. '
|
||||||
|
'You should use {} instead.'.format(old_key, new_key))
|
||||||
del config[old_key]
|
del config[old_key]
|
||||||
updated = True
|
updated = True
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
def fixup_keys(self, keypairs):
|
def set_key(self, key, value, save=True):
|
||||||
'''Migrate old key names to new ones'''
|
|
||||||
self.fixup_config_keys(self.cmdline_options, keypairs)
|
|
||||||
self.fixup_config_keys(self.system_config, keypairs)
|
|
||||||
if self.fixup_config_keys(self.user_config, keypairs):
|
|
||||||
self.save_user_config()
|
|
||||||
|
|
||||||
def set_key(self, key, value, save = True):
|
|
||||||
if not self.is_modifiable(key):
|
if not self.is_modifiable(key):
|
||||||
print_stderr("Warning: not changing config key '%s' set on the command line" % key)
|
self.print_stderr("Warning: not changing config key '%s' set on the command line" % key)
|
||||||
return
|
return
|
||||||
|
self._set_key_in_user_config(key, value, save)
|
||||||
|
|
||||||
|
def _set_key_in_user_config(self, key, value, save=True):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.user_config[key] = value
|
if value is not None:
|
||||||
|
self.user_config[key] = value
|
||||||
|
else:
|
||||||
|
self.user_config.pop(key, None)
|
||||||
if save:
|
if save:
|
||||||
self.save_user_config()
|
self.save_user_config()
|
||||||
return
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
out = self.cmdline_options.get(key)
|
out = self.cmdline_options.get(key)
|
||||||
if out is None:
|
if out is None:
|
||||||
out = self.user_config.get(key)
|
out = self.user_config.get(key, default)
|
||||||
if out is None:
|
|
||||||
out = self.system_config.get(key, default)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def requires_upgrade(self):
|
||||||
|
return self.get_config_version() < FINAL_CONFIG_VERSION
|
||||||
|
|
||||||
|
def upgrade(self):
|
||||||
|
with self.lock:
|
||||||
|
self.print_error('upgrading config')
|
||||||
|
|
||||||
|
self.convert_version_2()
|
||||||
|
|
||||||
|
self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
|
||||||
|
|
||||||
|
def convert_version_2(self):
|
||||||
|
if not self._is_upgrade_method_needed(1, 1):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# change server string FROM host:port:proto TO host:port:s
|
||||||
|
server_str = self.user_config.get('server')
|
||||||
|
host, port, protocol = str(server_str).rsplit(':', 2)
|
||||||
|
assert protocol in ('s', 't')
|
||||||
|
int(port) # Throw if cannot be converted to int
|
||||||
|
server_str = '{}:{}:s'.format(host, port)
|
||||||
|
self._set_key_in_user_config('server', server_str)
|
||||||
|
except BaseException:
|
||||||
|
self._set_key_in_user_config('server', None)
|
||||||
|
|
||||||
|
self.set_key('config_version', 2)
|
||||||
|
|
||||||
|
def _is_upgrade_method_needed(self, min_version, max_version):
|
||||||
|
cur_version = self.get_config_version()
|
||||||
|
if cur_version > max_version:
|
||||||
|
return False
|
||||||
|
elif cur_version < min_version:
|
||||||
|
raise Exception(
|
||||||
|
('config upgrade: unexpected version %d (should be %d-%d)'
|
||||||
|
% (cur_version, min_version, max_version)))
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_config_version(self):
|
||||||
|
config_version = self.get('config_version', 1)
|
||||||
|
if config_version > FINAL_CONFIG_VERSION:
|
||||||
|
self.print_stderr('WARNING: config version ({}) is higher than ours ({})'
|
||||||
|
.format(config_version, FINAL_CONFIG_VERSION))
|
||||||
|
return config_version
|
||||||
|
|
||||||
def is_modifiable(self, key):
|
def is_modifiable(self, key):
|
||||||
return not key in self.cmdline_options
|
return key not in self.cmdline_options
|
||||||
|
|
||||||
def save_user_config(self):
|
def save_user_config(self):
|
||||||
if not self.path:
|
if not self.path:
|
||||||
return
|
return
|
||||||
path = os.path.join(self.path, "config")
|
path = os.path.join(self.path, "config")
|
||||||
s = json.dumps(self.user_config, indent=4, sort_keys=True)
|
s = json.dumps(self.user_config, indent=4, sort_keys=True)
|
||||||
with open(path, "w") as f:
|
try:
|
||||||
f.write(s)
|
with open(path, "w", encoding='utf-8') as f:
|
||||||
os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
|
f.write(s)
|
||||||
|
os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# datadir probably deleted while running...
|
||||||
|
if os.path.exists(self.path): # or maybe not?
|
||||||
|
raise
|
||||||
|
|
||||||
def get_wallet_path(self):
|
def get_wallet_path(self):
|
||||||
"""Set the path of the wallet."""
|
"""Set the path of the wallet."""
|
||||||
|
@ -160,10 +236,14 @@ class SimpleConfig(PrintError):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# default path
|
# default path
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
_('Electrum-Zcash datadir does not exist. Was it deleted while running?') + '\n' +
|
||||||
|
_('Should be at {}').format(self.path))
|
||||||
dirpath = os.path.join(self.path, "wallets")
|
dirpath = os.path.join(self.path, "wallets")
|
||||||
if not os.path.exists(dirpath):
|
if not os.path.exists(dirpath):
|
||||||
if os.path.islink(dirpath):
|
if os.path.islink(dirpath):
|
||||||
raise BaseException('Dangling link: ' + dirpath)
|
raise Exception('Dangling link: ' + dirpath)
|
||||||
os.mkdir(dirpath)
|
os.mkdir(dirpath)
|
||||||
os.chmod(dirpath, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
os.chmod(dirpath, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
||||||
|
|
||||||
|
@ -200,57 +280,203 @@ class SimpleConfig(PrintError):
|
||||||
path = wallet.storage.path
|
path = wallet.storage.path
|
||||||
self.set_key('gui_last_wallet', path)
|
self.set_key('gui_last_wallet', path)
|
||||||
|
|
||||||
def max_fee_rate(self):
|
def impose_hard_limits_on_fee(func):
|
||||||
f = self.get('max_fee_rate', MAX_FEE_RATE)
|
def get_fee_within_limits(self, *args, **kwargs):
|
||||||
if f==0:
|
fee = func(self, *args, **kwargs)
|
||||||
f = MAX_FEE_RATE
|
if fee is None:
|
||||||
return f
|
return fee
|
||||||
|
fee = min(FEERATE_MAX_DYNAMIC, fee)
|
||||||
|
fee = max(FEERATE_DEFAULT_RELAY, fee)
|
||||||
|
return fee
|
||||||
|
return get_fee_within_limits
|
||||||
|
|
||||||
def dynfee(self, i):
|
@impose_hard_limits_on_fee
|
||||||
|
def eta_to_fee(self, i):
|
||||||
|
"""Returns fee in sat/kbyte."""
|
||||||
if i < 4:
|
if i < 4:
|
||||||
j = FEE_TARGETS[i]
|
j = FEE_ETA_TARGETS[i]
|
||||||
fee = self.fee_estimates.get(j)
|
fee = self.fee_estimates.get(j)
|
||||||
else:
|
else:
|
||||||
assert i == 4
|
assert i == 4
|
||||||
fee = self.fee_estimates.get(2)
|
fee = self.fee_estimates.get(2)
|
||||||
if fee is not None:
|
if fee is not None:
|
||||||
fee += fee/2
|
fee += fee/2
|
||||||
if fee is not None:
|
|
||||||
fee = min(5*MAX_FEE_RATE, fee)
|
|
||||||
return fee
|
return fee
|
||||||
|
|
||||||
def reverse_dynfee(self, fee_per_kb):
|
def fee_to_depth(self, target_fee):
|
||||||
|
depth = 0
|
||||||
|
for fee, s in self.mempool_fees:
|
||||||
|
depth += s
|
||||||
|
if fee <= target_fee:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
return depth
|
||||||
|
|
||||||
|
@impose_hard_limits_on_fee
|
||||||
|
def depth_to_fee(self, i):
|
||||||
|
"""Returns fee in sat/kbyte."""
|
||||||
|
target = self.depth_target(i)
|
||||||
|
depth = 0
|
||||||
|
for fee, s in self.mempool_fees:
|
||||||
|
depth += s
|
||||||
|
if depth > target:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
return fee * 1000
|
||||||
|
|
||||||
|
def depth_target(self, i):
|
||||||
|
return FEE_DEPTH_TARGETS[i]
|
||||||
|
|
||||||
|
def eta_target(self, i):
|
||||||
|
if i == len(FEE_ETA_TARGETS):
|
||||||
|
return 1
|
||||||
|
return FEE_ETA_TARGETS[i]
|
||||||
|
|
||||||
|
def fee_to_eta(self, fee_per_kb):
|
||||||
import operator
|
import operator
|
||||||
l = list(self.fee_estimates.items()) + [(1, self.dynfee(4))]
|
l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))]
|
||||||
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l)
|
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l)
|
||||||
min_target, min_value = min(dist, key=operator.itemgetter(1))
|
min_target, min_value = min(dist, key=operator.itemgetter(1))
|
||||||
if fee_per_kb < self.fee_estimates.get(25)/2:
|
if fee_per_kb < self.fee_estimates.get(25)/2:
|
||||||
min_target = -1
|
min_target = -1
|
||||||
return min_target
|
return min_target
|
||||||
|
|
||||||
|
def depth_tooltip(self, depth):
|
||||||
|
return "%.1f MB from tip"%(depth/1000000)
|
||||||
|
|
||||||
|
def eta_tooltip(self, x):
|
||||||
|
if x < 0:
|
||||||
|
return _('Low fee')
|
||||||
|
elif x == 1:
|
||||||
|
return _('In the next block')
|
||||||
|
else:
|
||||||
|
return _('Within {} blocks').format(x)
|
||||||
|
|
||||||
|
def get_fee_status(self):
|
||||||
|
dyn = self.is_dynfee()
|
||||||
|
mempool = self.use_mempool_fees()
|
||||||
|
pos = self.get_depth_level() if mempool else self.get_fee_level()
|
||||||
|
fee_rate = self.fee_per_kb()
|
||||||
|
target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
|
||||||
|
return tooltip + ' [%s]'%target if dyn else target + ' [Static]'
|
||||||
|
|
||||||
|
def get_fee_text(self, pos, dyn, mempool, fee_rate):
|
||||||
|
"""Returns (text, tooltip) where
|
||||||
|
text is what we target: static fee / num blocks to confirm in / mempool depth
|
||||||
|
tooltip is the corresponding estimate (e.g. num blocks for a static fee)
|
||||||
|
"""
|
||||||
|
rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False) + ' sat/byte') if fee_rate is not None else 'unknown'
|
||||||
|
if dyn:
|
||||||
|
if mempool:
|
||||||
|
depth = self.depth_target(pos)
|
||||||
|
text = self.depth_tooltip(depth)
|
||||||
|
else:
|
||||||
|
eta = self.eta_target(pos)
|
||||||
|
text = self.eta_tooltip(eta)
|
||||||
|
tooltip = rate_str
|
||||||
|
else:
|
||||||
|
text = rate_str
|
||||||
|
if mempool and self.has_fee_mempool():
|
||||||
|
depth = self.fee_to_depth(fee_rate)
|
||||||
|
tooltip = self.depth_tooltip(depth)
|
||||||
|
elif not mempool and self.has_fee_etas():
|
||||||
|
eta = self.fee_to_eta(fee_rate)
|
||||||
|
tooltip = self.eta_tooltip(eta)
|
||||||
|
else:
|
||||||
|
tooltip = ''
|
||||||
|
return text, tooltip
|
||||||
|
|
||||||
|
def get_depth_level(self):
|
||||||
|
maxp = len(FEE_DEPTH_TARGETS) - 1
|
||||||
|
return min(maxp, self.get('depth_level', 2))
|
||||||
|
|
||||||
|
def get_fee_level(self):
|
||||||
|
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
|
||||||
|
return min(maxp, self.get('fee_level', 2))
|
||||||
|
|
||||||
|
def get_fee_slider(self, dyn, mempool):
|
||||||
|
if dyn:
|
||||||
|
if mempool:
|
||||||
|
pos = self.get_depth_level()
|
||||||
|
maxp = len(FEE_DEPTH_TARGETS) - 1
|
||||||
|
fee_rate = self.depth_to_fee(pos)
|
||||||
|
else:
|
||||||
|
pos = self.get_fee_level()
|
||||||
|
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
|
||||||
|
fee_rate = self.eta_to_fee(pos)
|
||||||
|
else:
|
||||||
|
fee_rate = self.fee_per_kb(dyn=False)
|
||||||
|
pos = self.static_fee_index(fee_rate)
|
||||||
|
maxp = 9
|
||||||
|
return maxp, pos, fee_rate
|
||||||
|
|
||||||
def static_fee(self, i):
|
def static_fee(self, i):
|
||||||
return self.fee_rates[i]
|
return FEERATE_STATIC_VALUES[i]
|
||||||
|
|
||||||
def static_fee_index(self, value):
|
def static_fee_index(self, value):
|
||||||
dist = list(map(lambda x: abs(x - value), self.fee_rates))
|
if value is None:
|
||||||
|
raise TypeError('static fee cannot be None')
|
||||||
|
dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES))
|
||||||
return min(range(len(dist)), key=dist.__getitem__)
|
return min(range(len(dist)), key=dist.__getitem__)
|
||||||
|
|
||||||
def has_fee_estimates(self):
|
def has_fee_etas(self):
|
||||||
return len(self.fee_estimates)==4
|
return len(self.fee_estimates) == 4
|
||||||
|
|
||||||
|
def has_fee_mempool(self):
|
||||||
|
return bool(self.mempool_fees)
|
||||||
|
|
||||||
|
def has_dynamic_fees_ready(self):
|
||||||
|
if self.use_mempool_fees():
|
||||||
|
return self.has_fee_mempool()
|
||||||
|
else:
|
||||||
|
return self.has_fee_etas()
|
||||||
|
|
||||||
def is_dynfee(self):
|
def is_dynfee(self):
|
||||||
return self.get('dynamic_fees', True)
|
return bool(self.get('dynamic_fees', True))
|
||||||
|
|
||||||
def fee_per_kb(self):
|
def use_mempool_fees(self):
|
||||||
dyn = self.is_dynfee()
|
return bool(self.get('mempool_fees', False))
|
||||||
|
|
||||||
|
def fee_per_kb(self, dyn=None, mempool=None):
|
||||||
|
"""Returns sat/kvB fee to pay for a txn.
|
||||||
|
Note: might return None.
|
||||||
|
"""
|
||||||
|
if dyn is None:
|
||||||
|
dyn = self.is_dynfee()
|
||||||
|
if mempool is None:
|
||||||
|
mempool = self.use_mempool_fees()
|
||||||
if dyn:
|
if dyn:
|
||||||
fee_rate = self.dynfee(self.get('fee_level', 2))
|
if mempool:
|
||||||
|
fee_rate = self.depth_to_fee(self.get_depth_level())
|
||||||
|
else:
|
||||||
|
fee_rate = self.eta_to_fee(self.get_fee_level())
|
||||||
else:
|
else:
|
||||||
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
|
fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE)
|
||||||
return fee_rate
|
return fee_rate
|
||||||
|
|
||||||
|
def fee_per_byte(self):
|
||||||
|
"""Returns sat/vB fee to pay for a txn.
|
||||||
|
Note: might return None.
|
||||||
|
"""
|
||||||
|
fee_per_kb = self.fee_per_kb()
|
||||||
|
return fee_per_kb / 1000 if fee_per_kb is not None else None
|
||||||
|
|
||||||
def estimate_fee(self, size):
|
def estimate_fee(self, size):
|
||||||
return int(self.fee_per_kb() * size / 1000.)
|
fee_per_kb = self.fee_per_kb()
|
||||||
|
if fee_per_kb is None:
|
||||||
|
raise NoDynamicFeeEstimates()
|
||||||
|
return self.estimate_fee_for_feerate(fee_per_kb, size)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def estimate_fee_for_feerate(cls, fee_per_kb, size):
|
||||||
|
# note: We only allow integer sat/byte values atm.
|
||||||
|
# The GUI for simplicity reasons only displays integer sat/byte,
|
||||||
|
# and for the sake of consistency, we thus only use integer sat/byte in
|
||||||
|
# the backend too.
|
||||||
|
fee_per_byte = int(fee_per_kb / 1000)
|
||||||
|
return int(fee_per_byte * size)
|
||||||
|
|
||||||
def update_fee_estimates(self, key, value):
|
def update_fee_estimates(self, key, value):
|
||||||
self.fee_estimates[key] = value
|
self.fee_estimates[key] = value
|
||||||
|
@ -261,11 +487,7 @@ class SimpleConfig(PrintError):
|
||||||
Returns True if an update should be requested.
|
Returns True if an update should be requested.
|
||||||
"""
|
"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
prev_updates = self.fee_estimates_last_updated.values()
|
return now - self.last_time_fee_estimates_requested > 60
|
||||||
oldest_fee_time = min(prev_updates) if prev_updates else 0
|
|
||||||
stale_fees = now - oldest_fee_time > 7200
|
|
||||||
old_request = now - self.last_time_fee_estimates_requested > 60
|
|
||||||
return stale_fees and old_request
|
|
||||||
|
|
||||||
def requested_fee_estimates(self):
|
def requested_fee_estimates(self):
|
||||||
self.last_time_fee_estimates_requested = time.time()
|
self.last_time_fee_estimates_requested = time.time()
|
||||||
|
@ -277,21 +499,6 @@ class SimpleConfig(PrintError):
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def read_system_config(path=SYSTEM_CONFIG_PATH):
|
|
||||||
"""Parse and return the system config settings in /etc/electrum-zcash.conf."""
|
|
||||||
result = {}
|
|
||||||
if os.path.exists(path):
|
|
||||||
import configparser
|
|
||||||
p = configparser.ConfigParser()
|
|
||||||
try:
|
|
||||||
p.read(path)
|
|
||||||
for k, v in p.items('client'):
|
|
||||||
result[k] = v
|
|
||||||
except (configparser.NoSectionError, configparser.MissingSectionHeaderError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def read_user_config(path):
|
def read_user_config(path):
|
||||||
"""Parse and store the user config settings in electrum-zcash.conf into user_config[]."""
|
"""Parse and store the user config settings in electrum-zcash.conf into user_config[]."""
|
||||||
if not path:
|
if not path:
|
||||||
|
@ -300,7 +507,7 @@ def read_user_config(path):
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
with open(config_path, "r") as f:
|
with open(config_path, "r", encoding='utf-8') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
result = json.loads(data)
|
result = json.loads(data)
|
||||||
except:
|
except:
|
||||||
|
|
129
lib/storage.py
129
lib/storage.py
|
@ -33,7 +33,7 @@ import pbkdf2, hmac, hashlib
|
||||||
import base64
|
import base64
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from .util import PrintError, profiler
|
from .util import PrintError, profiler, InvalidPassword, WalletFileException
|
||||||
from .plugins import run_hook, plugin_loaders
|
from .plugins import run_hook, plugin_loaders
|
||||||
from .keystore import bip44_derivation
|
from .keystore import bip44_derivation
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
|
@ -51,11 +51,20 @@ FINAL_SEED_VERSION = 16 # electrum >= 2.7 will set this to prevent
|
||||||
def multisig_type(wallet_type):
|
def multisig_type(wallet_type):
|
||||||
'''If wallet_type is mofn multi-sig, return [m, n],
|
'''If wallet_type is mofn multi-sig, return [m, n],
|
||||||
otherwise return None.'''
|
otherwise return None.'''
|
||||||
|
if not wallet_type:
|
||||||
|
return None
|
||||||
match = re.match('(\d+)of(\d+)', wallet_type)
|
match = re.match('(\d+)of(\d+)', wallet_type)
|
||||||
if match:
|
if match:
|
||||||
match = [int(x) for x in match.group(1, 2)]
|
match = [int(x) for x in match.group(1, 2)]
|
||||||
return match
|
return match
|
||||||
|
|
||||||
|
def get_derivation_used_for_hw_device_encryption():
|
||||||
|
return ("m"
|
||||||
|
"/4541509'" # ascii 'ELE' as decimal ("BIP43 purpose")
|
||||||
|
"/1112098098'") # ascii 'BIE2' as decimal
|
||||||
|
|
||||||
|
# storage encryption version
|
||||||
|
STO_EV_PLAINTEXT, STO_EV_USER_PW, STO_EV_XPUB_PW = range(0, 3)
|
||||||
|
|
||||||
class WalletStorage(PrintError):
|
class WalletStorage(PrintError):
|
||||||
|
|
||||||
|
@ -68,11 +77,13 @@ class WalletStorage(PrintError):
|
||||||
self.modified = False
|
self.modified = False
|
||||||
self.pubkey = None
|
self.pubkey = None
|
||||||
if self.file_exists():
|
if self.file_exists():
|
||||||
with open(self.path, "r") as f:
|
with open(self.path, "r", encoding='utf-8') as f:
|
||||||
self.raw = f.read()
|
self.raw = f.read()
|
||||||
|
self._encryption_version = self._init_encryption_version()
|
||||||
if not self.is_encrypted():
|
if not self.is_encrypted():
|
||||||
self.load_data(self.raw)
|
self.load_data(self.raw)
|
||||||
else:
|
else:
|
||||||
|
self._encryption_version = STO_EV_PLAINTEXT
|
||||||
# avoid new wallets getting 'upgraded'
|
# avoid new wallets getting 'upgraded'
|
||||||
self.put('seed_version', FINAL_SEED_VERSION)
|
self.put('seed_version', FINAL_SEED_VERSION)
|
||||||
|
|
||||||
|
@ -102,15 +113,51 @@ class WalletStorage(PrintError):
|
||||||
|
|
||||||
if not self.manual_upgrades:
|
if not self.manual_upgrades:
|
||||||
if self.requires_split():
|
if self.requires_split():
|
||||||
raise BaseException("This wallet has multiple accounts and must be split")
|
raise WalletFileException("This wallet has multiple accounts and must be split")
|
||||||
if self.requires_upgrade():
|
if self.requires_upgrade():
|
||||||
self.upgrade()
|
self.upgrade()
|
||||||
|
|
||||||
|
def is_past_initial_decryption(self):
|
||||||
|
"""Return if storage is in a usable state for normal operations.
|
||||||
|
|
||||||
|
The value is True exactly
|
||||||
|
if encryption is disabled completely (self.is_encrypted() == False),
|
||||||
|
or if encryption is enabled but the contents have already been decrypted.
|
||||||
|
"""
|
||||||
|
return bool(self.data)
|
||||||
|
|
||||||
def is_encrypted(self):
|
def is_encrypted(self):
|
||||||
|
"""Return if storage encryption is currently enabled."""
|
||||||
|
return self.get_encryption_version() != STO_EV_PLAINTEXT
|
||||||
|
|
||||||
|
def is_encrypted_with_user_pw(self):
|
||||||
|
return self.get_encryption_version() == STO_EV_USER_PW
|
||||||
|
|
||||||
|
def is_encrypted_with_hw_device(self):
|
||||||
|
return self.get_encryption_version() == STO_EV_XPUB_PW
|
||||||
|
|
||||||
|
def get_encryption_version(self):
|
||||||
|
"""Return the version of encryption used for this storage.
|
||||||
|
|
||||||
|
0: plaintext / no encryption
|
||||||
|
|
||||||
|
ECIES, private key derived from a password,
|
||||||
|
1: password is provided by user
|
||||||
|
2: password is derived from an xpub; used with hw wallets
|
||||||
|
"""
|
||||||
|
return self._encryption_version
|
||||||
|
|
||||||
|
def _init_encryption_version(self):
|
||||||
try:
|
try:
|
||||||
return base64.b64decode(self.raw)[0:4] == b'BIE1'
|
magic = base64.b64decode(self.raw)[0:4]
|
||||||
|
if magic == b'BIE1':
|
||||||
|
return STO_EV_USER_PW
|
||||||
|
elif magic == b'BIE2':
|
||||||
|
return STO_EV_XPUB_PW
|
||||||
|
else:
|
||||||
|
return STO_EV_PLAINTEXT
|
||||||
except:
|
except:
|
||||||
return False
|
return STO_EV_PLAINTEXT
|
||||||
|
|
||||||
def file_exists(self):
|
def file_exists(self):
|
||||||
return self.path and os.path.exists(self.path)
|
return self.path and os.path.exists(self.path)
|
||||||
|
@ -120,20 +167,50 @@ class WalletStorage(PrintError):
|
||||||
ec_key = bitcoin.EC_KEY(secret)
|
ec_key = bitcoin.EC_KEY(secret)
|
||||||
return ec_key
|
return ec_key
|
||||||
|
|
||||||
|
def _get_encryption_magic(self):
|
||||||
|
v = self._encryption_version
|
||||||
|
if v == STO_EV_USER_PW:
|
||||||
|
return b'BIE1'
|
||||||
|
elif v == STO_EV_XPUB_PW:
|
||||||
|
return b'BIE2'
|
||||||
|
else:
|
||||||
|
raise WalletFileException('no encryption magic for version: %s' % v)
|
||||||
|
|
||||||
def decrypt(self, password):
|
def decrypt(self, password):
|
||||||
ec_key = self.get_key(password)
|
ec_key = self.get_key(password)
|
||||||
s = zlib.decompress(ec_key.decrypt_message(self.raw)) if self.raw else None
|
if self.raw:
|
||||||
|
enc_magic = self._get_encryption_magic()
|
||||||
|
s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic))
|
||||||
|
else:
|
||||||
|
s = None
|
||||||
self.pubkey = ec_key.get_public_key()
|
self.pubkey = ec_key.get_public_key()
|
||||||
s = s.decode('utf8')
|
s = s.decode('utf8')
|
||||||
self.load_data(s)
|
self.load_data(s)
|
||||||
|
|
||||||
def set_password(self, password, encrypt):
|
def check_password(self, password):
|
||||||
self.put('use_encryption', bool(password))
|
"""Raises an InvalidPassword exception on invalid password"""
|
||||||
if encrypt and password:
|
if not self.is_encrypted():
|
||||||
|
return
|
||||||
|
if self.pubkey and self.pubkey != self.get_key(password).get_public_key():
|
||||||
|
raise InvalidPassword()
|
||||||
|
|
||||||
|
def set_keystore_encryption(self, enable):
|
||||||
|
self.put('use_encryption', enable)
|
||||||
|
|
||||||
|
def set_password(self, password, enc_version=None):
|
||||||
|
"""Set a password to be used for encrypting this storage."""
|
||||||
|
if enc_version is None:
|
||||||
|
enc_version = self._encryption_version
|
||||||
|
if password and enc_version != STO_EV_PLAINTEXT:
|
||||||
ec_key = self.get_key(password)
|
ec_key = self.get_key(password)
|
||||||
self.pubkey = ec_key.get_public_key()
|
self.pubkey = ec_key.get_public_key()
|
||||||
|
self._encryption_version = enc_version
|
||||||
else:
|
else:
|
||||||
self.pubkey = None
|
self.pubkey = None
|
||||||
|
self._encryption_version = STO_EV_PLAINTEXT
|
||||||
|
# make sure next storage.write() saves changes
|
||||||
|
with self.lock:
|
||||||
|
self.modified = True
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
@ -175,11 +252,12 @@ class WalletStorage(PrintError):
|
||||||
if self.pubkey:
|
if self.pubkey:
|
||||||
s = bytes(s, 'utf8')
|
s = bytes(s, 'utf8')
|
||||||
c = zlib.compress(s)
|
c = zlib.compress(s)
|
||||||
s = bitcoin.encrypt_message(c, self.pubkey)
|
enc_magic = self._get_encryption_magic()
|
||||||
|
s = bitcoin.encrypt_message(c, self.pubkey, enc_magic)
|
||||||
s = s.decode('utf8')
|
s = s.decode('utf8')
|
||||||
|
|
||||||
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||||
with open(temp_path, "w") as f:
|
with open(temp_path, "w", encoding='utf-8') as f:
|
||||||
f.write(s)
|
f.write(s)
|
||||||
f.flush()
|
f.flush()
|
||||||
os.fsync(f.fileno())
|
os.fsync(f.fileno())
|
||||||
|
@ -242,7 +320,7 @@ class WalletStorage(PrintError):
|
||||||
storage2.write()
|
storage2.write()
|
||||||
result.append(new_path)
|
result.append(new_path)
|
||||||
else:
|
else:
|
||||||
raise BaseException("This wallet has multiple accounts and must be split")
|
raise WalletFileException("This wallet has multiple accounts and must be split")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def requires_upgrade(self):
|
def requires_upgrade(self):
|
||||||
|
@ -263,6 +341,9 @@ class WalletStorage(PrintError):
|
||||||
self.write()
|
self.write()
|
||||||
|
|
||||||
def convert_wallet_type(self):
|
def convert_wallet_type(self):
|
||||||
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
|
return
|
||||||
|
|
||||||
wallet_type = self.get('wallet_type')
|
wallet_type = self.get('wallet_type')
|
||||||
if wallet_type == 'btchip': wallet_type = 'ledger'
|
if wallet_type == 'btchip': wallet_type = 'ledger'
|
||||||
if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
|
if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
|
||||||
|
@ -338,7 +419,7 @@ class WalletStorage(PrintError):
|
||||||
d['seed'] = seed
|
d['seed'] = seed
|
||||||
self.put(key, d)
|
self.put(key, d)
|
||||||
else:
|
else:
|
||||||
raise
|
raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?')
|
||||||
# remove junk
|
# remove junk
|
||||||
self.put('master_public_key', None)
|
self.put('master_public_key', None)
|
||||||
self.put('master_public_keys', None)
|
self.put('master_public_keys', None)
|
||||||
|
@ -445,6 +526,9 @@ class WalletStorage(PrintError):
|
||||||
self.put('seed_version', 16)
|
self.put('seed_version', 16)
|
||||||
|
|
||||||
def convert_imported(self):
|
def convert_imported(self):
|
||||||
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
|
return
|
||||||
|
|
||||||
# '/x' is the internal ID for imported accounts
|
# '/x' is the internal ID for imported accounts
|
||||||
d = self.get('accounts', {}).get('/x', {}).get('imported',{})
|
d = self.get('accounts', {}).get('/x', {}).get('imported',{})
|
||||||
if not d:
|
if not d:
|
||||||
|
@ -458,7 +542,7 @@ class WalletStorage(PrintError):
|
||||||
else:
|
else:
|
||||||
addresses.append(addr)
|
addresses.append(addr)
|
||||||
if addresses and keypairs:
|
if addresses and keypairs:
|
||||||
raise BaseException('mixed addresses and privkeys')
|
raise WalletFileException('mixed addresses and privkeys')
|
||||||
elif addresses:
|
elif addresses:
|
||||||
self.put('addresses', addresses)
|
self.put('addresses', addresses)
|
||||||
self.put('accounts', None)
|
self.put('accounts', None)
|
||||||
|
@ -468,9 +552,12 @@ class WalletStorage(PrintError):
|
||||||
self.put('keypairs', keypairs)
|
self.put('keypairs', keypairs)
|
||||||
self.put('accounts', None)
|
self.put('accounts', None)
|
||||||
else:
|
else:
|
||||||
raise BaseException('no addresses or privkeys')
|
raise WalletFileException('no addresses or privkeys')
|
||||||
|
|
||||||
def convert_account(self):
|
def convert_account(self):
|
||||||
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
|
return
|
||||||
|
|
||||||
self.put('accounts', None)
|
self.put('accounts', None)
|
||||||
|
|
||||||
def _is_upgrade_method_needed(self, min_version, max_version):
|
def _is_upgrade_method_needed(self, min_version, max_version):
|
||||||
|
@ -478,9 +565,9 @@ class WalletStorage(PrintError):
|
||||||
if cur_version > max_version:
|
if cur_version > max_version:
|
||||||
return False
|
return False
|
||||||
elif cur_version < min_version:
|
elif cur_version < min_version:
|
||||||
raise BaseException(
|
raise WalletFileException(
|
||||||
('storage upgrade: unexpected version %d (should be %d-%d)'
|
'storage upgrade: unexpected version {} (should be {}-{})'
|
||||||
% (cur_version, min_version, max_version)))
|
.format(cur_version, min_version, max_version))
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -496,7 +583,9 @@ class WalletStorage(PrintError):
|
||||||
if not seed_version:
|
if not seed_version:
|
||||||
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
|
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
|
||||||
if seed_version > FINAL_SEED_VERSION:
|
if seed_version > FINAL_SEED_VERSION:
|
||||||
raise BaseException('This version of Electrum is too old to open this wallet')
|
raise WalletFileException('This version of Electrum is too old to open this wallet.\n'
|
||||||
|
'(highest supported storage version: {}, version of this file: {})'
|
||||||
|
.format(FINAL_SEED_VERSION, seed_version))
|
||||||
if seed_version >=12:
|
if seed_version >=12:
|
||||||
return seed_version
|
return seed_version
|
||||||
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
|
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
|
||||||
|
@ -517,4 +606,4 @@ class WalletStorage(PrintError):
|
||||||
else:
|
else:
|
||||||
# creation was complete if electrum was run from source
|
# creation was complete if electrum was run from source
|
||||||
msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
|
msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
|
||||||
raise BaseException(msg)
|
raise WalletFileException(msg)
|
||||||
|
|
|
@ -50,6 +50,8 @@ class Synchronizer(ThreadJob):
|
||||||
self.requested_histories = {}
|
self.requested_histories = {}
|
||||||
self.requested_addrs = set()
|
self.requested_addrs = set()
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
|
|
||||||
|
self.initialized = False
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
def parse_response(self, response):
|
def parse_response(self, response):
|
||||||
|
@ -84,11 +86,13 @@ class Synchronizer(ThreadJob):
|
||||||
return bh2u(hashlib.sha256(status.encode('ascii')).digest())
|
return bh2u(hashlib.sha256(status.encode('ascii')).digest())
|
||||||
|
|
||||||
def on_address_status(self, response):
|
def on_address_status(self, response):
|
||||||
|
if self.wallet.synchronizer is None and self.initialized:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
params, result = self.parse_response(response)
|
params, result = self.parse_response(response)
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
addr = params[0]
|
addr = params[0]
|
||||||
history = self.wallet.get_address_history(addr)
|
history = self.wallet.history.get(addr, [])
|
||||||
if self.get_status(history) != result:
|
if self.get_status(history) != result:
|
||||||
if self.requested_histories.get(addr) is None:
|
if self.requested_histories.get(addr) is None:
|
||||||
self.requested_histories[addr] = result
|
self.requested_histories[addr] = result
|
||||||
|
@ -98,12 +102,17 @@ class Synchronizer(ThreadJob):
|
||||||
self.requested_addrs.remove(addr)
|
self.requested_addrs.remove(addr)
|
||||||
|
|
||||||
def on_address_history(self, response):
|
def on_address_history(self, response):
|
||||||
|
if self.wallet.synchronizer is None and self.initialized:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
params, result = self.parse_response(response)
|
params, result = self.parse_response(response)
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
addr = params[0]
|
addr = params[0]
|
||||||
|
server_status = self.requested_histories.get(addr)
|
||||||
|
if server_status is None:
|
||||||
|
self.print_error("receiving history (unsolicited)", addr, len(result))
|
||||||
|
return
|
||||||
self.print_error("receiving history", addr, len(result))
|
self.print_error("receiving history", addr, len(result))
|
||||||
server_status = self.requested_histories[addr]
|
|
||||||
hashes = set(map(lambda item: item['tx_hash'], result))
|
hashes = set(map(lambda item: item['tx_hash'], result))
|
||||||
hist = list(map(lambda item: (item['tx_hash'], item['height']), result))
|
hist = list(map(lambda item: (item['tx_hash'], item['height']), result))
|
||||||
# tx_fees
|
# tx_fees
|
||||||
|
@ -127,6 +136,8 @@ class Synchronizer(ThreadJob):
|
||||||
self.requested_histories.pop(addr)
|
self.requested_histories.pop(addr)
|
||||||
|
|
||||||
def tx_response(self, response):
|
def tx_response(self, response):
|
||||||
|
if self.wallet.synchronizer is None and self.initialized:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
params, result = self.parse_response(response)
|
params, result = self.parse_response(response)
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
|
@ -177,6 +188,7 @@ class Synchronizer(ThreadJob):
|
||||||
if self.requested_tx:
|
if self.requested_tx:
|
||||||
self.print_error("missing tx", self.requested_tx)
|
self.print_error("missing tx", self.requested_tx)
|
||||||
self.subscribe_to_addresses(set(self.wallet.get_addresses()))
|
self.subscribe_to_addresses(set(self.wallet.get_addresses()))
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
'''Called from the network proxy thread main loop.'''
|
'''Called from the network proxy thread main loop.'''
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from lib import constants
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseForTestnet(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
constants.set_testnet()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
constants.set_mainnet()
|
|
@ -11,8 +11,12 @@ from lib.bitcoin import (
|
||||||
var_int, op_push, address_to_script, regenerate_key,
|
var_int, op_push, address_to_script, regenerate_key,
|
||||||
verify_message, deserialize_privkey, serialize_privkey,
|
verify_message, deserialize_privkey, serialize_privkey,
|
||||||
is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
|
is_b58_address, address_to_scripthash, is_minikey, is_compressed, is_xpub,
|
||||||
xpub_type, is_xprv, is_bip32_derivation, seed_type, NetworkConstants)
|
xpub_type, is_xprv, is_bip32_derivation, seed_type, EncodeBase58Check)
|
||||||
from lib.util import bfh
|
from lib.util import bfh
|
||||||
|
from lib import constants
|
||||||
|
|
||||||
|
from . import TestCaseForTestnet
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ecdsa
|
import ecdsa
|
||||||
|
@ -140,11 +144,11 @@ class Test_bitcoin(unittest.TestCase):
|
||||||
self.assertEqual(op_push(0x4b), '4b')
|
self.assertEqual(op_push(0x4b), '4b')
|
||||||
self.assertEqual(op_push(0x4c), '4c4c')
|
self.assertEqual(op_push(0x4c), '4c4c')
|
||||||
self.assertEqual(op_push(0xfe), '4cfe')
|
self.assertEqual(op_push(0xfe), '4cfe')
|
||||||
self.assertEqual(op_push(0xff), '4dff00')
|
self.assertEqual(op_push(0xff), '4cff')
|
||||||
self.assertEqual(op_push(0x100), '4d0001')
|
self.assertEqual(op_push(0x100), '4d0001')
|
||||||
self.assertEqual(op_push(0x1234), '4d3412')
|
self.assertEqual(op_push(0x1234), '4d3412')
|
||||||
self.assertEqual(op_push(0xfffe), '4dfeff')
|
self.assertEqual(op_push(0xfffe), '4dfeff')
|
||||||
self.assertEqual(op_push(0xffff), '4effff0000')
|
self.assertEqual(op_push(0xffff), '4dffff')
|
||||||
self.assertEqual(op_push(0x10000), '4e00000100')
|
self.assertEqual(op_push(0x10000), '4e00000100')
|
||||||
self.assertEqual(op_push(0x12345678), '4e78563412')
|
self.assertEqual(op_push(0x12345678), '4e78563412')
|
||||||
|
|
||||||
|
@ -158,17 +162,7 @@ class Test_bitcoin(unittest.TestCase):
|
||||||
self.assertEqual(address_to_script('t3grLzdTrjSSiCFXzxV5YCvkYZt2tJjDLau'), 'a914f47c8954e421031ad04ecd8e7752c9479206b9d387')
|
self.assertEqual(address_to_script('t3grLzdTrjSSiCFXzxV5YCvkYZt2tJjDLau'), 'a914f47c8954e421031ad04ecd8e7752c9479206b9d387')
|
||||||
|
|
||||||
|
|
||||||
class Test_bitcoin_testnet(unittest.TestCase):
|
class Test_bitcoin_testnet(TestCaseForTestnet):
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
super().setUpClass()
|
|
||||||
NetworkConstants.set_testnet()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
super().tearDownClass()
|
|
||||||
NetworkConstants.set_mainnet()
|
|
||||||
|
|
||||||
def test_address_to_script(self):
|
def test_address_to_script(self):
|
||||||
# base58 P2PKH
|
# base58 P2PKH
|
||||||
|
@ -250,11 +244,69 @@ class Test_xprv_xpub(unittest.TestCase):
|
||||||
self.assertFalse(is_bip32_derivation(""))
|
self.assertFalse(is_bip32_derivation(""))
|
||||||
self.assertFalse(is_bip32_derivation("m/q8462"))
|
self.assertFalse(is_bip32_derivation("m/q8462"))
|
||||||
|
|
||||||
|
def test_version_bytes(self):
|
||||||
|
xprv_headers_b58 = {
|
||||||
|
'standard': 'xprv',
|
||||||
|
}
|
||||||
|
xpub_headers_b58 = {
|
||||||
|
'standard': 'xpub',
|
||||||
|
}
|
||||||
|
for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items():
|
||||||
|
xkey_header_bytes = bfh("%08x" % xkey_header_bytes)
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([0] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))
|
||||||
|
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([255] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))
|
||||||
|
|
||||||
|
for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items():
|
||||||
|
xkey_header_bytes = bfh("%08x" % xkey_header_bytes)
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([0] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))
|
||||||
|
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([255] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))
|
||||||
|
|
||||||
|
|
||||||
|
class Test_xprv_xpub_testnet(TestCaseForTestnet):
|
||||||
|
|
||||||
|
def test_version_bytes(self):
|
||||||
|
xprv_headers_b58 = {
|
||||||
|
'standard': 'tprv',
|
||||||
|
}
|
||||||
|
xpub_headers_b58 = {
|
||||||
|
'standard': 'tpub',
|
||||||
|
}
|
||||||
|
for xtype, xkey_header_bytes in constants.net.XPRV_HEADERS.items():
|
||||||
|
xkey_header_bytes = bfh("%08x" % xkey_header_bytes)
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([0] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))
|
||||||
|
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([255] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xprv_headers_b58[xtype]))
|
||||||
|
|
||||||
|
for xtype, xkey_header_bytes in constants.net.XPUB_HEADERS.items():
|
||||||
|
xkey_header_bytes = bfh("%08x" % xkey_header_bytes)
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([0] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))
|
||||||
|
|
||||||
|
xkey_bytes = xkey_header_bytes + bytes([255] * 74)
|
||||||
|
xkey_b58 = EncodeBase58Check(xkey_bytes)
|
||||||
|
self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype]))
|
||||||
|
|
||||||
|
|
||||||
class Test_keyImport(unittest.TestCase):
|
class Test_keyImport(unittest.TestCase):
|
||||||
|
|
||||||
priv_pub_addr = (
|
priv_pub_addr = (
|
||||||
{'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
|
{'priv': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
|
||||||
|
'exported_privkey': 'p2pkh:KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6',
|
||||||
'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
|
'pub': '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997',
|
||||||
'address': 't1QTbqnYayRQQ2QZDUgYAcZ6EAcuddyRMbu',
|
'address': 't1QTbqnYayRQQ2QZDUgYAcZ6EAcuddyRMbu',
|
||||||
'minikey' : False,
|
'minikey' : False,
|
||||||
|
@ -262,7 +314,17 @@ class Test_keyImport(unittest.TestCase):
|
||||||
'compressed': True,
|
'compressed': True,
|
||||||
'addr_encoding': 'base58',
|
'addr_encoding': 'base58',
|
||||||
'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
|
'scripthash': 'c9aecd1fef8d661a42c560bf75c8163e337099800b8face5ca3d1393a30508a7'},
|
||||||
|
{'priv': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
|
||||||
|
'exported_privkey': 'p2pkh:Kzj8VjwpZ99bQqVeUiRXrKuX9mLr1o6sWxFMCBJn1umC38BMiQTD',
|
||||||
|
'pub': '0352d78b4b37e0f6d4e164423436f2925fa57817467178eca550a88f2821973c41',
|
||||||
|
'address': 't1ZQHZQpr51Z83vYLkuCgUnA7xgygSNEL8E',
|
||||||
|
'minikey': False,
|
||||||
|
'txin_type': 'p2pkh',
|
||||||
|
'compressed': True,
|
||||||
|
'addr_encoding': 'base58',
|
||||||
|
'scripthash': 'a9b2a76fc196c553b352186dfcca81fcf323a721cd8431328f8e9d54216818c1'},
|
||||||
{'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
|
{'priv': '5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
|
||||||
|
'exported_privkey': 'p2pkh:5Hxn5C4SQuiV6e62A1MtZmbSeQyrLFhu5uYks62pU5VBUygK2KD',
|
||||||
'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
|
'pub': '04e5fe91a20fac945845a5518450d23405ff3e3e1ce39827b47ee6d5db020a9075422d56a59195ada0035e4a52a238849f68e7a325ba5b2247013e0481c5c7cb3f',
|
||||||
'address': 't1ZFtVnxGSXwNZjnsKVhiAGeEC8nJPuJDDp',
|
'address': 't1ZFtVnxGSXwNZjnsKVhiAGeEC8nJPuJDDp',
|
||||||
'minikey': False,
|
'minikey': False,
|
||||||
|
@ -270,8 +332,18 @@ class Test_keyImport(unittest.TestCase):
|
||||||
'compressed': False,
|
'compressed': False,
|
||||||
'addr_encoding': 'base58',
|
'addr_encoding': 'base58',
|
||||||
'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
|
'scripthash': 'f5914651408417e1166f725a5829ff9576d0dbf05237055bf13abd2af7f79473'},
|
||||||
|
{'priv': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
|
||||||
|
'exported_privkey': 'p2pkh:5KhYQCe1xd5g2tqpmmGpUWDpDuTbA8vnpbiCNDwMPAx29WNQYfN',
|
||||||
|
'pub': '048f0431b0776e8210376c81280011c2b68be43194cb00bd47b7e9aa66284b713ce09556cde3fee606051a07613f3c159ef3953b8927c96ae3dae94a6ba4182e0e',
|
||||||
|
'address': 't1LzMikhRjUTSEzTLdGUBsrBTFZhR9hwffh',
|
||||||
|
'minikey': False,
|
||||||
|
'txin_type': 'p2pkh',
|
||||||
|
'compressed': False,
|
||||||
|
'addr_encoding': 'base58',
|
||||||
|
'scripthash': '6dd2e07ad2de9ba8eec4bbe8467eb53f8845acff0d9e6f5627391acc22ff62df'},
|
||||||
# from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
|
# from http://bitscan.com/articles/security/spotlight-on-mini-private-keys
|
||||||
{'priv': 'SzavMBLoXU6kDrqtUVmffv',
|
{'priv': 'SzavMBLoXU6kDrqtUVmffv',
|
||||||
|
'exported_privkey': 'p2pkh:L53fCHmQhbNp1B4JipfBtfeHZH7cAibzG9oK19XfiFzxHgAkz6JK',
|
||||||
'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
|
'pub': '02588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9',
|
||||||
'address': 't1S9WvZLVKoLjXUAxssvGojtqx1pABJMJUU',
|
'address': 't1S9WvZLVKoLjXUAxssvGojtqx1pABJMJUU',
|
||||||
'minikey': True,
|
'minikey': True,
|
||||||
|
@ -309,6 +381,7 @@ class Test_keyImport(unittest.TestCase):
|
||||||
def test_is_private_key(self):
|
def test_is_private_key(self):
|
||||||
for priv_details in self.priv_pub_addr:
|
for priv_details in self.priv_pub_addr:
|
||||||
self.assertTrue(is_private_key(priv_details['priv']))
|
self.assertTrue(is_private_key(priv_details['priv']))
|
||||||
|
self.assertTrue(is_private_key(priv_details['exported_privkey']))
|
||||||
self.assertFalse(is_private_key(priv_details['pub']))
|
self.assertFalse(is_private_key(priv_details['pub']))
|
||||||
self.assertFalse(is_private_key(priv_details['address']))
|
self.assertFalse(is_private_key(priv_details['address']))
|
||||||
self.assertFalse(is_private_key("not a privkey"))
|
self.assertFalse(is_private_key("not a privkey"))
|
||||||
|
@ -317,8 +390,7 @@ class Test_keyImport(unittest.TestCase):
|
||||||
for priv_details in self.priv_pub_addr:
|
for priv_details in self.priv_pub_addr:
|
||||||
txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])
|
txin_type, privkey, compressed = deserialize_privkey(priv_details['priv'])
|
||||||
priv2 = serialize_privkey(privkey, compressed, txin_type)
|
priv2 = serialize_privkey(privkey, compressed, txin_type)
|
||||||
if not priv_details['minikey']:
|
self.assertEqual(priv_details['exported_privkey'], priv2)
|
||||||
self.assertEqual(priv_details['priv'], priv2)
|
|
||||||
|
|
||||||
def test_address_to_scripthash(self):
|
def test_address_to_scripthash(self):
|
||||||
for priv_details in self.priv_pub_addr:
|
for priv_details in self.priv_pub_addr:
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import unittest
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from lib.commands import Commands
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommands(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_setconfig_non_auth_number(self):
|
||||||
|
self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', "7777"))
|
||||||
|
self.assertEqual(7777, Commands._setconfig_normalize_value('rpcport', '7777'))
|
||||||
|
self.assertAlmostEqual(Decimal(2.3), Commands._setconfig_normalize_value('somekey', '2.3'))
|
||||||
|
|
||||||
|
def test_setconfig_non_auth_number_as_string(self):
|
||||||
|
self.assertEqual("7777", Commands._setconfig_normalize_value('somekey', "'7777'"))
|
||||||
|
|
||||||
|
def test_setconfig_non_auth_boolean(self):
|
||||||
|
self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', "true"))
|
||||||
|
self.assertEqual(True, Commands._setconfig_normalize_value('show_console_tab', "True"))
|
||||||
|
|
||||||
|
def test_setconfig_non_auth_list(self):
|
||||||
|
self.assertEqual(['file:///var/www/', 'https://electrum.org'],
|
||||||
|
Commands._setconfig_normalize_value('url_rewrite', "['file:///var/www/','https://electrum.org']"))
|
||||||
|
self.assertEqual(['file:///var/www/', 'https://electrum.org'],
|
||||||
|
Commands._setconfig_normalize_value('url_rewrite', '["file:///var/www/","https://electrum.org"]'))
|
||||||
|
|
||||||
|
def test_setconfig_auth(self):
|
||||||
|
self.assertEqual("7777", Commands._setconfig_normalize_value('rpcuser', "7777"))
|
||||||
|
self.assertEqual("7777", Commands._setconfig_normalize_value('rpcuser', '7777'))
|
||||||
|
self.assertEqual("7777", Commands._setconfig_normalize_value('rpcpassword', '7777'))
|
||||||
|
self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd'))
|
||||||
|
self.assertEqual("['file:///var/www/','https://electrum.org']",
|
||||||
|
Commands._setconfig_normalize_value('rpcpassword', "['file:///var/www/','https://electrum.org']"))
|
|
@ -6,8 +6,7 @@ import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from lib.simple_config import (SimpleConfig, read_system_config,
|
from lib.simple_config import (SimpleConfig, read_user_config)
|
||||||
read_user_config)
|
|
||||||
|
|
||||||
|
|
||||||
class Test_SimpleConfig(unittest.TestCase):
|
class Test_SimpleConfig(unittest.TestCase):
|
||||||
|
@ -37,18 +36,15 @@ class Test_SimpleConfig(unittest.TestCase):
|
||||||
|
|
||||||
def test_simple_config_key_rename(self):
|
def test_simple_config_key_rename(self):
|
||||||
"""auto_cycle was renamed auto_connect"""
|
"""auto_cycle was renamed auto_connect"""
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"auto_cycle": True}
|
fake_read_user = lambda _: {"auto_cycle": True}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(config.get("auto_connect"), True)
|
self.assertEqual(config.get("auto_connect"), True)
|
||||||
self.assertEqual(config.get("auto_cycle"), None)
|
self.assertEqual(config.get("auto_cycle"), None)
|
||||||
fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True}
|
fake_read_user = lambda _: {"auto_connect": False, "auto_cycle": True}
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(config.get("auto_connect"), False)
|
self.assertEqual(config.get("auto_connect"), False)
|
||||||
|
@ -57,110 +53,51 @@ class Test_SimpleConfig(unittest.TestCase):
|
||||||
def test_simple_config_command_line_overrides_everything(self):
|
def test_simple_config_command_line_overrides_everything(self):
|
||||||
"""Options passed by command line override all other configuration
|
"""Options passed by command line override all other configuration
|
||||||
sources"""
|
sources"""
|
||||||
fake_read_system = lambda : {"electrum_path": "a"}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": "b"}
|
fake_read_user = lambda _: {"electrum_path": "b"}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(self.options.get("electrum_path"),
|
self.assertEqual(self.options.get("electrum_path"),
|
||||||
config.get("electrum_path"))
|
config.get("electrum_path"))
|
||||||
|
|
||||||
def test_simple_config_user_config_overrides_system_config(self):
|
|
||||||
"""Options passed in user config override system config."""
|
|
||||||
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": "b"}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
self.assertEqual("b", config.get("electrum_path"))
|
|
||||||
|
|
||||||
def test_simple_config_system_config_ignored_if_portable(self):
|
|
||||||
"""If electrum is started with the "portable" flag, system
|
|
||||||
configuration is completely ignored."""
|
|
||||||
fake_read_system = lambda : {"some_key": "some_value"}
|
|
||||||
fake_read_user = lambda _: {}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={"portable": True},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
self.assertEqual(config.get("some_key"), None)
|
|
||||||
|
|
||||||
def test_simple_config_user_config_is_used_if_others_arent_specified(self):
|
def test_simple_config_user_config_is_used_if_others_arent_specified(self):
|
||||||
"""If no system-wide configuration and no command-line options are
|
"""If no system-wide configuration and no command-line options are
|
||||||
specified, the user configuration is used instead."""
|
specified, the user configuration is used instead."""
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options={},
|
config = SimpleConfig(options={},
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
self.assertEqual(self.options.get("electrum_path"),
|
self.assertEqual(self.options.get("electrum_path"),
|
||||||
config.get("electrum_path"))
|
config.get("electrum_path"))
|
||||||
|
|
||||||
def test_cannot_set_options_passed_by_command_line(self):
|
def test_cannot_set_options_passed_by_command_line(self):
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": "b"}
|
fake_read_user = lambda _: {"electrum_path": "b"}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
config.set_key("electrum_path", "c")
|
config.set_key("electrum_path", "c")
|
||||||
self.assertEqual(self.options.get("electrum_path"),
|
self.assertEqual(self.options.get("electrum_path"),
|
||||||
config.get("electrum_path"))
|
config.get("electrum_path"))
|
||||||
|
|
||||||
def test_can_set_options_from_system_config(self):
|
|
||||||
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
|
|
||||||
fake_read_user = lambda _: {}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
config.set_key("electrum_path", "c")
|
|
||||||
self.assertEqual("c", config.get("electrum_path"))
|
|
||||||
|
|
||||||
def test_can_set_options_set_in_user_config(self):
|
def test_can_set_options_set_in_user_config(self):
|
||||||
another_path = tempfile.mkdtemp()
|
another_path = tempfile.mkdtemp()
|
||||||
fake_read_system = lambda : {}
|
|
||||||
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
fake_read_user = lambda _: {"electrum_path": self.electrum_dir}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
config = SimpleConfig(options={},
|
config = SimpleConfig(options={},
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
|
||||||
read_user_dir_function=read_user_dir)
|
|
||||||
config.set_key("electrum_path", another_path)
|
|
||||||
self.assertEqual(another_path, config.get("electrum_path"))
|
|
||||||
|
|
||||||
def test_can_set_options_from_system_config_if_portable(self):
|
|
||||||
"""If the "portable" flag is set, the user can overwrite system
|
|
||||||
configuration options."""
|
|
||||||
another_path = tempfile.mkdtemp()
|
|
||||||
fake_read_system = lambda : {"electrum_path": self.electrum_dir}
|
|
||||||
fake_read_user = lambda _: {}
|
|
||||||
read_user_dir = lambda : self.user_dir
|
|
||||||
config = SimpleConfig(options={"portable": True},
|
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
config.set_key("electrum_path", another_path)
|
config.set_key("electrum_path", another_path)
|
||||||
self.assertEqual(another_path, config.get("electrum_path"))
|
self.assertEqual(another_path, config.get("electrum_path"))
|
||||||
|
|
||||||
def test_user_config_is_not_written_with_read_only_config(self):
|
def test_user_config_is_not_written_with_read_only_config(self):
|
||||||
"""The user config does not contain command-line options or system
|
"""The user config does not contain command-line options when saved."""
|
||||||
options when saved."""
|
|
||||||
fake_read_system = lambda : {"something": "b"}
|
|
||||||
fake_read_user = lambda _: {"something": "a"}
|
fake_read_user = lambda _: {"something": "a"}
|
||||||
read_user_dir = lambda : self.user_dir
|
read_user_dir = lambda : self.user_dir
|
||||||
self.options.update({"something": "c"})
|
self.options.update({"something": "c"})
|
||||||
config = SimpleConfig(options=self.options,
|
config = SimpleConfig(options=self.options,
|
||||||
read_system_config_function=fake_read_system,
|
|
||||||
read_user_config_function=fake_read_user,
|
read_user_config_function=fake_read_user,
|
||||||
read_user_dir_function=read_user_dir)
|
read_user_dir_function=read_user_dir)
|
||||||
config.save_user_config()
|
config.save_user_config()
|
||||||
|
@ -168,48 +105,10 @@ class Test_SimpleConfig(unittest.TestCase):
|
||||||
with open(os.path.join(self.electrum_dir, "config"), "r") as f:
|
with open(os.path.join(self.electrum_dir, "config"), "r") as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
result = ast.literal_eval(contents)
|
result = ast.literal_eval(contents)
|
||||||
|
result.pop('config_version', None)
|
||||||
self.assertEqual({"something": "a"}, result)
|
self.assertEqual({"something": "a"}, result)
|
||||||
|
|
||||||
|
|
||||||
class TestSystemConfig(unittest.TestCase):
|
|
||||||
|
|
||||||
sample_conf = """
|
|
||||||
[client]
|
|
||||||
gap_limit = 5
|
|
||||||
|
|
||||||
[something_else]
|
|
||||||
everything = 42
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestSystemConfig, self).setUp()
|
|
||||||
self.thefile = tempfile.mkstemp(suffix=".electrum.test.conf")[1]
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super(TestSystemConfig, self).tearDown()
|
|
||||||
os.remove(self.thefile)
|
|
||||||
|
|
||||||
def test_read_system_config_file_does_not_exist(self):
|
|
||||||
somefile = "/foo/I/do/not/exist/electrum.conf"
|
|
||||||
result = read_system_config(somefile)
|
|
||||||
self.assertEqual({}, result)
|
|
||||||
|
|
||||||
def test_read_system_config_file_returns_file_options(self):
|
|
||||||
with open(self.thefile, "w") as f:
|
|
||||||
f.write(self.sample_conf)
|
|
||||||
|
|
||||||
result = read_system_config(self.thefile)
|
|
||||||
self.assertEqual({"gap_limit": "5"}, result)
|
|
||||||
|
|
||||||
def test_read_system_config_file_no_sections(self):
|
|
||||||
|
|
||||||
with open(self.thefile, "w") as f:
|
|
||||||
f.write("gap_limit = 5") # The file has no sections at all
|
|
||||||
|
|
||||||
result = read_system_config(self.thefile)
|
|
||||||
self.assertEqual({}, result)
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserConfig(unittest.TestCase):
|
class TestUserConfig(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,10 +1,9 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from lib import transaction
|
from lib import transaction
|
||||||
from lib.bitcoin import TYPE_ADDRESS
|
from lib.bitcoin import TYPE_ADDRESS
|
||||||
|
|
||||||
from lib.keystore import xpubkey_to_address
|
from lib.keystore import xpubkey_to_address
|
||||||
|
from lib.util import bh2u, bfh
|
||||||
from lib.util import bh2u
|
|
||||||
|
|
||||||
unsigned_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
|
unsigned_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
|
||||||
signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
|
signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
|
||||||
|
@ -133,6 +132,11 @@ class TestTransaction(unittest.TestCase):
|
||||||
self.assertEqual(tx.estimated_weight(), 772)
|
self.assertEqual(tx.estimated_weight(), 772)
|
||||||
self.assertEqual(tx.estimated_size(), 193)
|
self.assertEqual(tx.estimated_size(), 193)
|
||||||
|
|
||||||
|
def test_estimated_output_size(self):
|
||||||
|
estimated_output_size = transaction.Transaction.estimated_output_size
|
||||||
|
self.assertEqual(estimated_output_size('t1MZDS9LxiXasLqR5fMDK4kDa8TJjSFsMsq'), 34)
|
||||||
|
self.assertEqual(estimated_output_size('t3NSSQe2KNgLcTWy2WsiRAkr7NTtZ15fhLn'), 32)
|
||||||
|
|
||||||
def test_errors(self):
|
def test_errors(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
transaction.Transaction.pay_script(output_type=None, addr='')
|
transaction.Transaction.pay_script(output_type=None, addr='')
|
||||||
|
@ -148,29 +152,67 @@ class TestTransaction(unittest.TestCase):
|
||||||
tx = transaction.Transaction(v2_blob)
|
tx = transaction.Transaction(v2_blob)
|
||||||
self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe")
|
self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe")
|
||||||
|
|
||||||
|
def test_get_address_from_output_script(self):
|
||||||
|
# the inverse of this test is in test_bitcoin: test_address_to_script
|
||||||
|
addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script))
|
||||||
|
ADDR = transaction.TYPE_ADDRESS
|
||||||
|
|
||||||
|
# base58 p2pkh
|
||||||
|
self.assertEqual((ADDR, 't1MZDS9LxiXasLqR5fMDK4kDa8TJjSFsMsq'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac'))
|
||||||
|
self.assertEqual((ADDR, 't1U7SgL7CWNnawSvZD8k8JgwWUygasy2cp1'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac'))
|
||||||
|
|
||||||
|
# base58 p2sh
|
||||||
|
self.assertEqual((ADDR, 't3NSSQe2KNgLcTWy2WsiRAkr7NTtZ15fhLn'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487'))
|
||||||
|
self.assertEqual((ADDR, 't3grLzdTrjSSiCFXzxV5YCvkYZt2tJjDLau'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387'))
|
||||||
|
|
||||||
|
#####
|
||||||
|
|
||||||
|
def _run_naive_tests_on_tx(self, raw_tx, txid):
|
||||||
|
tx = transaction.Transaction(raw_tx)
|
||||||
|
self.assertEqual(txid, tx.txid())
|
||||||
|
self.assertEqual(raw_tx, tx.serialize())
|
||||||
|
self.assertTrue(tx.estimated_size() >= 0)
|
||||||
|
|
||||||
def test_txid_coinbase_to_p2pk(self):
|
def test_txid_coinbase_to_p2pk(self):
|
||||||
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4103400d0302ef02062f503253482f522cfabe6d6dd90d39663d10f8fd25ec88338295d4c6ce1c90d4aeb368d8bdbadcc1da3b635801000000000000000474073e03ffffffff013c25cf2d01000000434104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac00000000')
|
raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4103400d0302ef02062f503253482f522cfabe6d6dd90d39663d10f8fd25ec88338295d4c6ce1c90d4aeb368d8bdbadcc1da3b635801000000000000000474073e03ffffffff013c25cf2d01000000434104b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7bac00000000'
|
||||||
self.assertEqual('dbaf14e1c476e76ea05a8b71921a46d6b06f0a950f17c5f9f1a03b8fae467f10', tx.txid())
|
txid = 'dbaf14e1c476e76ea05a8b71921a46d6b06f0a950f17c5f9f1a03b8fae467f10'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
def test_txid_coinbase_to_p2pkh(self):
|
def test_txid_coinbase_to_p2pkh(self):
|
||||||
tx = transaction.Transaction('01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff25033ca0030400001256124d696e656420627920425443204775696c640800000d41000007daffffffff01c00d1298000000001976a91427a1f12771de5cc3b73941664b2537c15316be4388ac00000000')
|
raw_tx = '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff25033ca0030400001256124d696e656420627920425443204775696c640800000d41000007daffffffff01c00d1298000000001976a91427a1f12771de5cc3b73941664b2537c15316be4388ac00000000'
|
||||||
self.assertEqual('4328f9311c6defd9ae1bd7f4516b62acf64b361eb39dfcf09d9925c5fd5c61e8', tx.txid())
|
txid = '4328f9311c6defd9ae1bd7f4516b62acf64b361eb39dfcf09d9925c5fd5c61e8'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
def test_txid_p2pk_to_p2pkh(self):
|
def test_txid_p2pk_to_p2pkh(self):
|
||||||
tx = transaction.Transaction('010000000118231a31d2df84f884ced6af11dc24306319577d4d7c340124a7e2dd9c314077000000004847304402200b6c45891aed48937241907bc3e3868ee4c792819821fcde33311e5a3da4789a02205021b59692b652a01f5f009bd481acac2f647a7d9c076d71d85869763337882e01fdffffff016c95052a010000001976a9149c4891e7791da9e622532c97f43863768264faaf88ac00000000')
|
raw_tx = '010000000118231a31d2df84f884ced6af11dc24306319577d4d7c340124a7e2dd9c314077000000004847304402200b6c45891aed48937241907bc3e3868ee4c792819821fcde33311e5a3da4789a02205021b59692b652a01f5f009bd481acac2f647a7d9c076d71d85869763337882e01fdffffff016c95052a010000001976a9149c4891e7791da9e622532c97f43863768264faaf88ac00000000'
|
||||||
self.assertEqual('90ba90a5b115106d26663fce6c6215b8699c5d4b2672dd30756115f3337dddf9', tx.txid())
|
txid = '90ba90a5b115106d26663fce6c6215b8699c5d4b2672dd30756115f3337dddf9'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
def test_txid_p2pk_to_p2sh(self):
|
def test_txid_p2pk_to_p2sh(self):
|
||||||
tx = transaction.Transaction('0100000001e4643183d6497823576d17ac2439fb97eba24be8137f312e10fcc16483bb2d070000000048473044022032bbf0394dfe3b004075e3cbb3ea7071b9184547e27f8f73f967c4b3f6a21fa4022073edd5ae8b7b638f25872a7a308bb53a848baa9b9cc70af45fcf3c683d36a55301fdffffff011821814a0000000017a9143c640bc28a346749c09615b50211cb051faff00f8700000000')
|
raw_tx = '0100000001e4643183d6497823576d17ac2439fb97eba24be8137f312e10fcc16483bb2d070000000048473044022032bbf0394dfe3b004075e3cbb3ea7071b9184547e27f8f73f967c4b3f6a21fa4022073edd5ae8b7b638f25872a7a308bb53a848baa9b9cc70af45fcf3c683d36a55301fdffffff011821814a0000000017a9143c640bc28a346749c09615b50211cb051faff00f8700000000'
|
||||||
self.assertEqual('172bdf5a690b874385b98d7ab6f6af807356f03a26033c6a65ab79b4ac2085b5', tx.txid())
|
txid = '172bdf5a690b874385b98d7ab6f6af807356f03a26033c6a65ab79b4ac2085b5'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
def test_txid_p2pkh_to_p2pkh(self):
|
def test_txid_p2pkh_to_p2pkh(self):
|
||||||
tx = transaction.Transaction('0100000001f9dd7d33f315617530dd72264b5d9c69b815626cce3f66266d1015b1a590ba90000000006a4730440220699bfee3d280a499daf4af5593e8750b54fef0557f3c9f717bfa909493a84f60022057718eec7985b7796bb8630bf6ea2e9bf2892ac21bd6ab8f741a008537139ffe012103b4289890b40590447b57f773b5843bf0400e9cead08be225fac587b3c2a8e973fdffffff01ec24052a010000001976a914ce9ff3d15ed5f3a3d94b583b12796d063879b11588ac00000000')
|
raw_tx = '0100000001f9dd7d33f315617530dd72264b5d9c69b815626cce3f66266d1015b1a590ba90000000006a4730440220699bfee3d280a499daf4af5593e8750b54fef0557f3c9f717bfa909493a84f60022057718eec7985b7796bb8630bf6ea2e9bf2892ac21bd6ab8f741a008537139ffe012103b4289890b40590447b57f773b5843bf0400e9cead08be225fac587b3c2a8e973fdffffff01ec24052a010000001976a914ce9ff3d15ed5f3a3d94b583b12796d063879b11588ac00000000'
|
||||||
self.assertEqual('24737c68f53d4b519939119ed83b2a8d44d716d7f3ca98bcecc0fbb92c2085ce', tx.txid())
|
txid = '24737c68f53d4b519939119ed83b2a8d44d716d7f3ca98bcecc0fbb92c2085ce'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
def test_txid_p2pkh_to_p2sh(self):
|
def test_txid_p2pkh_to_p2sh(self):
|
||||||
tx = transaction.Transaction('010000000195232c30f6611b9f2f82ec63f5b443b132219c425e1824584411f3d16a7a54bc000000006b4830450221009f39ac457dc8ff316e5cc03161c9eff6212d8694ccb88d801dbb32e85d8ed100022074230bb05e99b85a6a50d2b71e7bf04d80be3f1d014ea038f93943abd79421d101210317be0f7e5478e087453b9b5111bdad586038720f16ac9658fd16217ffd7e5785fdffffff0200e40b540200000017a914d81df3751b9e7dca920678cc19cac8d7ec9010b08718dfd63c2c0000001976a914303c42b63569ff5b390a2016ff44651cd84c7c8988acc7010000')
|
raw_tx = '010000000195232c30f6611b9f2f82ec63f5b443b132219c425e1824584411f3d16a7a54bc000000006b4830450221009f39ac457dc8ff316e5cc03161c9eff6212d8694ccb88d801dbb32e85d8ed100022074230bb05e99b85a6a50d2b71e7bf04d80be3f1d014ea038f93943abd79421d101210317be0f7e5478e087453b9b5111bdad586038720f16ac9658fd16217ffd7e5785fdffffff0200e40b540200000017a914d81df3751b9e7dca920678cc19cac8d7ec9010b08718dfd63c2c0000001976a914303c42b63569ff5b390a2016ff44651cd84c7c8988acc7010000'
|
||||||
self.assertEqual('155e4740fa59f374abb4e133b87247dccc3afc233cb97c2bf2b46bba3094aedc', tx.txid())
|
txid = '155e4740fa59f374abb4e133b87247dccc3afc233cb97c2bf2b46bba3094aedc'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
|
# input: p2sh, not multisig
|
||||||
|
def test_txid_regression_issue_3899(self):
|
||||||
|
raw_tx = '0100000004328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c010000000b0009630330472d5fae685bffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c020000000b0009630359646d5fae6858ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c030000000b000963034bd4715fae6854ffffffff328685b0352c981d3d451b471ae3bfc78b82565dc2a54049a81af273f0a9fd9c040000000b000963036de8705fae6860ffffffff0130750000000000001976a914b5abca61d20f9062fb1fdbb880d9d93bac36675188ac00000000'
|
||||||
|
txid = 'f570d5d1e965ee61bcc7005f8fefb1d3abbed9d7ddbe035e2a68fa07e5fc4a0d'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
|
def test_txid_negative_version_num(self):
|
||||||
|
raw_tx = 'f0b47b9a01ecf5e5c3bbf2cf1f71ecdc7f708b0b222432e914b394e24aad1494a42990ddfc000000008b483045022100852744642305a99ad74354e9495bf43a1f96ded470c256cd32e129290f1fa191022030c11d294af6a61b3da6ed2c0c296251d21d113cfd71ec11126517034b0dcb70014104a0fe6e4a600f859a0932f701d3af8e0ecd4be886d91045f06a5a6b931b95873aea1df61da281ba29cadb560dad4fc047cf47b4f7f2570da4c0b810b3dfa7e500ffffffff0240420f00000000001976a9147eeacb8a9265cd68c92806611f704fc55a21e1f588ac05f00d00000000001976a914eb3bd8ccd3ba6f1570f844b59ba3e0a667024a6a88acff7f0000'
|
||||||
|
txid = 'c659729a7fea5071361c2c1a68551ca2bf77679b27086cc415adeeb03852e369'
|
||||||
|
self._run_naive_tests_on_tx(raw_tx, txid)
|
||||||
|
|
||||||
|
|
||||||
class NetworkMock(object):
|
class NetworkMock(object):
|
||||||
|
|
|
@ -5,44 +5,59 @@ import lib.bitcoin as bitcoin
|
||||||
import lib.keystore as keystore
|
import lib.keystore as keystore
|
||||||
import lib.storage as storage
|
import lib.storage as storage
|
||||||
import lib.wallet as wallet
|
import lib.wallet as wallet
|
||||||
|
from lib import constants
|
||||||
|
|
||||||
|
from . import TestCaseForTestnet
|
||||||
|
|
||||||
|
|
||||||
# TODO: 2fa
|
class WalletIntegrityHelper:
|
||||||
class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
|
||||||
|
|
||||||
gap_limit = 1 # make tests run faster
|
gap_limit = 1 # make tests run faster
|
||||||
|
|
||||||
def _check_seeded_keystore_sanity(self, ks):
|
@classmethod
|
||||||
self.assertTrue (ks.is_deterministic())
|
def check_seeded_keystore_sanity(cls, test_obj, ks):
|
||||||
self.assertFalse(ks.is_watching_only())
|
test_obj.assertTrue(ks.is_deterministic())
|
||||||
self.assertFalse(ks.can_import())
|
test_obj.assertFalse(ks.is_watching_only())
|
||||||
self.assertTrue (ks.has_seed())
|
test_obj.assertFalse(ks.can_import())
|
||||||
|
test_obj.assertTrue(ks.has_seed())
|
||||||
|
|
||||||
def _check_xpub_keystore_sanity(self, ks):
|
@classmethod
|
||||||
self.assertTrue (ks.is_deterministic())
|
def check_xpub_keystore_sanity(cls, test_obj, ks):
|
||||||
self.assertTrue (ks.is_watching_only())
|
test_obj.assertTrue(ks.is_deterministic())
|
||||||
self.assertFalse(ks.can_import())
|
test_obj.assertTrue(ks.is_watching_only())
|
||||||
self.assertFalse(ks.has_seed())
|
test_obj.assertFalse(ks.can_import())
|
||||||
|
test_obj.assertFalse(ks.has_seed())
|
||||||
|
|
||||||
def _create_standard_wallet(self, ks):
|
@classmethod
|
||||||
|
def create_standard_wallet(cls, ks):
|
||||||
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893')
|
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893')
|
||||||
store.put('keystore', ks.dump())
|
store.put('keystore', ks.dump())
|
||||||
store.put('gap_limit', self.gap_limit)
|
store.put('gap_limit', cls.gap_limit)
|
||||||
w = wallet.Standard_Wallet(store)
|
w = wallet.Standard_Wallet(store)
|
||||||
w.synchronize()
|
w.synchronize()
|
||||||
return w
|
return w
|
||||||
|
|
||||||
def _create_multisig_wallet(self, ks1, ks2):
|
@classmethod
|
||||||
|
def create_multisig_wallet(cls, ks1, ks2, ks3=None):
|
||||||
|
"""Creates a 2-of-2 or 2-of-3 multisig wallet."""
|
||||||
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893')
|
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893')
|
||||||
multisig_type = "%dof%d" % (2, 2)
|
|
||||||
store.put('wallet_type', multisig_type)
|
|
||||||
store.put('x%d/' % 1, ks1.dump())
|
store.put('x%d/' % 1, ks1.dump())
|
||||||
store.put('x%d/' % 2, ks2.dump())
|
store.put('x%d/' % 2, ks2.dump())
|
||||||
store.put('gap_limit', self.gap_limit)
|
if ks3 is None:
|
||||||
|
multisig_type = "%dof%d" % (2, 2)
|
||||||
|
else:
|
||||||
|
multisig_type = "%dof%d" % (2, 3)
|
||||||
|
store.put('x%d/' % 3, ks3.dump())
|
||||||
|
store.put('wallet_type', multisig_type)
|
||||||
|
store.put('gap_limit', cls.gap_limit)
|
||||||
w = wallet.Multisig_Wallet(store)
|
w = wallet.Multisig_Wallet(store)
|
||||||
w.synchronize()
|
w.synchronize()
|
||||||
return w
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
# TODO passphrase/seed_extension
|
||||||
|
class TestWalletKeystoreAddressIntegrityForMainnet(unittest.TestCase):
|
||||||
|
|
||||||
@mock.patch.object(storage.WalletStorage, '_write')
|
@mock.patch.object(storage.WalletStorage, '_write')
|
||||||
def test_electrum_seed_standard(self, mock_write):
|
def test_electrum_seed_standard(self, mock_write):
|
||||||
seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song'
|
seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song'
|
||||||
|
@ -50,12 +65,14 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
||||||
|
|
||||||
ks = keystore.from_seed(seed_words, '', False)
|
ks = keystore.from_seed(seed_words, '', False)
|
||||||
|
|
||||||
self._check_seeded_keystore_sanity(ks)
|
WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks)
|
||||||
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
|
||||||
|
|
||||||
|
self.assertEqual(ks.xprv, 'xprv9s21ZrQH143K32jECVM729vWgGq4mUDJCk1ozqAStTphzQtCTuoFmFafNoG1g55iCnBTXUzz3zWnDb5CVLGiFvmaZjuazHDL8a81cPQ8KL6')
|
||||||
self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U')
|
self.assertEqual(ks.xpub, 'xpub661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52CwBdDWroaZf8U')
|
||||||
|
|
||||||
w = self._create_standard_wallet(ks)
|
w = WalletIntegrityHelper.create_standard_wallet(ks)
|
||||||
|
self.assertEqual(w.txin_type, 'p2pkh')
|
||||||
|
|
||||||
self.assertEqual(w.get_receiving_addresses()[0], 't1fFMuEC9XFGsEUEPzpEE8jhxpcEMs369xJ')
|
self.assertEqual(w.get_receiving_addresses()[0], 't1fFMuEC9XFGsEUEPzpEE8jhxpcEMs369xJ')
|
||||||
self.assertEqual(w.get_change_addresses()[0], 't1cKFzsmq8d97RtePBbqS1WebLofuXaXkzF')
|
self.assertEqual(w.get_change_addresses()[0], 't1cKFzsmq8d97RtePBbqS1WebLofuXaXkzF')
|
||||||
|
@ -67,12 +84,13 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
||||||
|
|
||||||
ks = keystore.from_seed(seed_words, '', False)
|
ks = keystore.from_seed(seed_words, '', False)
|
||||||
|
|
||||||
self._check_seeded_keystore_sanity(ks)
|
WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks)
|
||||||
self.assertTrue(isinstance(ks, keystore.Old_KeyStore))
|
self.assertTrue(isinstance(ks, keystore.Old_KeyStore))
|
||||||
|
|
||||||
self.assertEqual(ks.mpk, 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3')
|
self.assertEqual(ks.mpk, 'e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3')
|
||||||
|
|
||||||
w = self._create_standard_wallet(ks)
|
w = WalletIntegrityHelper.create_standard_wallet(ks)
|
||||||
|
self.assertEqual(w.txin_type, 'p2pkh')
|
||||||
|
|
||||||
self.assertEqual(w.get_receiving_addresses()[0], 't1YAqEWYrfi9CbW5LgmayAvjDE5T5MgaYiD')
|
self.assertEqual(w.get_receiving_addresses()[0], 't1YAqEWYrfi9CbW5LgmayAvjDE5T5MgaYiD')
|
||||||
self.assertEqual(w.get_change_addresses()[0], 't1cJ799hEFa5AHmB3ReeDo3Rr2X4quderf4')
|
self.assertEqual(w.get_change_addresses()[0], 't1cJ799hEFa5AHmB3ReeDo3Rr2X4quderf4')
|
||||||
|
@ -86,9 +104,11 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
||||||
|
|
||||||
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks, keystore.BIP32_KeyStore))
|
||||||
|
|
||||||
|
self.assertEqual(ks.xprv, 'xprv9zGLcNEb3cHUKizLVBz6RYeE9bEZAVPjH2pD1DEzCnPcsemWc3d3xTao8sfhfUmDLMq6e3RcEMEvJG1Et8dvfL8DV4h7mwm9J6AJsW9WXQD')
|
||||||
self.assertEqual(ks.xpub, 'xpub6DFh1smUsyqmYD4obDX6ngaxhd53Zx7aeFjoobebm7vbkT6f9awJWFuGzBT9FQJEWFBL7UyhMXtYzRcwDuVbcxtv9Ce2W9eMm4KXLdvdbjv')
|
self.assertEqual(ks.xpub, 'xpub6DFh1smUsyqmYD4obDX6ngaxhd53Zx7aeFjoobebm7vbkT6f9awJWFuGzBT9FQJEWFBL7UyhMXtYzRcwDuVbcxtv9Ce2W9eMm4KXLdvdbjv')
|
||||||
|
|
||||||
w = self._create_standard_wallet(ks)
|
w = WalletIntegrityHelper.create_standard_wallet(ks)
|
||||||
|
self.assertEqual(w.txin_type, 'p2pkh')
|
||||||
|
|
||||||
self.assertEqual(w.get_receiving_addresses()[0], 't1PbiEBABXU1E4GEnE31cUPULDoYYWVMpjs')
|
self.assertEqual(w.get_receiving_addresses()[0], 't1PbiEBABXU1E4GEnE31cUPULDoYYWVMpjs')
|
||||||
self.assertEqual(w.get_change_addresses()[0], 't1Z8gbq4eeVbg89ACGddq1T6yfEP9bQx9Ki')
|
self.assertEqual(w.get_change_addresses()[0], 't1Z8gbq4eeVbg89ACGddq1T6yfEP9bQx9Ki')
|
||||||
|
@ -99,15 +119,18 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
||||||
self.assertEqual(bitcoin.seed_type(seed_words), 'standard')
|
self.assertEqual(bitcoin.seed_type(seed_words), 'standard')
|
||||||
|
|
||||||
ks1 = keystore.from_seed(seed_words, '', True)
|
ks1 = keystore.from_seed(seed_words, '', True)
|
||||||
self._check_seeded_keystore_sanity(ks1)
|
WalletIntegrityHelper.check_seeded_keystore_sanity(self, ks1)
|
||||||
self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))
|
||||||
|
self.assertEqual(ks1.xprv, 'xprv9s21ZrQH143K3t9vo23J3hajRbzvkRLJ6Y1zFrUFAfU3t8oooMPfb7f87cn5KntgqZs5nipZkCiBFo5ZtaSD2eDo7j7CMuFV8Zu6GYLTpY6')
|
||||||
self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcGNEPu3aJQqXTydqR9t49Tkwb4Esrj112kw8xLthv8uybxvaki4Ygt9xiwZUQGeFTG7T2TUzR3eA4Zp3aq5RXsABHFBUrq4c')
|
self.assertEqual(ks1.xpub, 'xpub661MyMwAqRbcGNEPu3aJQqXTydqR9t49Tkwb4Esrj112kw8xLthv8uybxvaki4Ygt9xiwZUQGeFTG7T2TUzR3eA4Zp3aq5RXsABHFBUrq4c')
|
||||||
|
|
||||||
|
# electrum seed: ghost into match ivory badge robot record tackle radar elbow traffic loud
|
||||||
ks2 = keystore.from_xpub('xpub661MyMwAqRbcGfCPEkkyo5WmcrhTq8mi3xuBS7VEZ3LYvsgY1cCFDbenT33bdD12axvrmXhuX3xkAbKci3yZY9ZEk8vhLic7KNhLjqdh5ec')
|
ks2 = keystore.from_xpub('xpub661MyMwAqRbcGfCPEkkyo5WmcrhTq8mi3xuBS7VEZ3LYvsgY1cCFDbenT33bdD12axvrmXhuX3xkAbKci3yZY9ZEk8vhLic7KNhLjqdh5ec')
|
||||||
self._check_xpub_keystore_sanity(ks2)
|
WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)
|
||||||
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
|
||||||
|
|
||||||
w = self._create_multisig_wallet(ks1, ks2)
|
w = WalletIntegrityHelper.create_multisig_wallet(ks1, ks2)
|
||||||
|
self.assertEqual(w.txin_type, 'p2sh')
|
||||||
|
|
||||||
self.assertEqual(w.get_receiving_addresses()[0], 't3KcK3kAJerAahSJhN6Pt729u7ficQqK4XX')
|
self.assertEqual(w.get_receiving_addresses()[0], 't3KcK3kAJerAahSJhN6Pt729u7ficQqK4XX')
|
||||||
self.assertEqual(w.get_change_addresses()[0], 't3PQ7wZhzpoywPLnD1nfcd5sPYLtw2qh5ak')
|
self.assertEqual(w.get_change_addresses()[0], 't3PQ7wZhzpoywPLnD1nfcd5sPYLtw2qh5ak')
|
||||||
|
@ -119,13 +142,17 @@ class TestWalletKeystoreAddressIntegrity(unittest.TestCase):
|
||||||
|
|
||||||
ks1 = keystore.from_bip39_seed(seed_words, '', "m/45'/0")
|
ks1 = keystore.from_bip39_seed(seed_words, '', "m/45'/0")
|
||||||
self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks1, keystore.BIP32_KeyStore))
|
||||||
|
self.assertEqual(ks1.xprv, 'xprv9vyEFyXf7pYVv4eDU3hhuCEAHPHNGuxX73nwtYdpbLcqwJCPwFKknAK8pHWuHHBirCzAPDZ7UJHrYdhLfn1NkGp9rk3rVz2aEqrT93qKRD9')
|
||||||
self.assertEqual(ks1.xpub, 'xpub69xafV4YxC6o8Yiga5EiGLAtqR7rgNgNUGiYgw3S9g9pp6XYUne1KxdcfYtxwmA3eBrzMFuYcNQKfqsXCygCo4GxQFHfywxpUbKNfYvGJka')
|
self.assertEqual(ks1.xpub, 'xpub69xafV4YxC6o8Yiga5EiGLAtqR7rgNgNUGiYgw3S9g9pp6XYUne1KxdcfYtxwmA3eBrzMFuYcNQKfqsXCygCo4GxQFHfywxpUbKNfYvGJka')
|
||||||
|
|
||||||
|
# bip39 seed: tray machine cook badge night page project uncover ritual toward person enact
|
||||||
|
# der: m/45'/0
|
||||||
ks2 = keystore.from_xpub('xpub6Bco9vrgo8rNUSi8Bjomn8xLA41DwPXeuPcgJamNRhTTyGVHsp8fZXaGzp9ypHoei16J6X3pumMAP1u3Dy4jTSWjm4GZowL7Dcn9u4uZC9W')
|
ks2 = keystore.from_xpub('xpub6Bco9vrgo8rNUSi8Bjomn8xLA41DwPXeuPcgJamNRhTTyGVHsp8fZXaGzp9ypHoei16J6X3pumMAP1u3Dy4jTSWjm4GZowL7Dcn9u4uZC9W')
|
||||||
self._check_xpub_keystore_sanity(ks2)
|
WalletIntegrityHelper.check_xpub_keystore_sanity(self, ks2)
|
||||||
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
|
self.assertTrue(isinstance(ks2, keystore.BIP32_KeyStore))
|
||||||
|
|
||||||
w = self._create_multisig_wallet(ks1, ks2)
|
w = WalletIntegrityHelper.create_multisig_wallet(ks1, ks2)
|
||||||
|
self.assertEqual(w.txin_type, 'p2sh')
|
||||||
|
|
||||||
self.assertEqual(w.get_receiving_addresses()[0], 't3ZvKyVcMRf5rofUFgN8gk23jtCkRh12Lja')
|
self.assertEqual(w.get_receiving_addresses()[0], 't3ZvKyVcMRf5rofUFgN8gk23jtCkRh12Lja')
|
||||||
self.assertEqual(w.get_change_addresses()[0], 't3JaafdGtfhXJzCrupYct6tJBdD2XZUtm8H')
|
self.assertEqual(w.get_change_addresses()[0], 't3JaafdGtfhXJzCrupYct6tJBdD2XZUtm8H')
|
||||||
|
|
|
@ -32,6 +32,8 @@ from .util import print_error, profiler
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
from .bitcoin import *
|
from .bitcoin import *
|
||||||
import struct
|
import struct
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
#
|
#
|
||||||
# Workalike python implementation of Bitcoin's CDataStream class.
|
# Workalike python implementation of Bitcoin's CDataStream class.
|
||||||
|
@ -227,10 +229,10 @@ opcodes = Enumeration("Opcodes", [
|
||||||
"OP_WITHIN", "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160",
|
"OP_WITHIN", "OP_RIPEMD160", "OP_SHA1", "OP_SHA256", "OP_HASH160",
|
||||||
"OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG",
|
"OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG",
|
||||||
"OP_CHECKMULTISIGVERIFY",
|
"OP_CHECKMULTISIGVERIFY",
|
||||||
("OP_SINGLEBYTE_END", 0xF0),
|
("OP_NOP1", 0xB0),
|
||||||
("OP_DOUBLEBYTE_BEGIN", 0xF000),
|
("OP_CHECKLOCKTIMEVERIFY", 0xB1), ("OP_CHECKSEQUENCEVERIFY", 0xB2),
|
||||||
"OP_PUBKEY", "OP_PUBKEYHASH",
|
"OP_NOP4", "OP_NOP5", "OP_NOP6", "OP_NOP7", "OP_NOP8", "OP_NOP9", "OP_NOP10",
|
||||||
("OP_INVALIDOPCODE", 0xFFFF),
|
("OP_INVALIDOPCODE", 0xFF),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@ -240,10 +242,6 @@ def script_GetOp(_bytes):
|
||||||
vch = None
|
vch = None
|
||||||
opcode = _bytes[i]
|
opcode = _bytes[i]
|
||||||
i += 1
|
i += 1
|
||||||
if opcode >= opcodes.OP_SINGLEBYTE_END:
|
|
||||||
opcode <<= 8
|
|
||||||
opcode |= _bytes[i]
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if opcode <= opcodes.OP_PUSHDATA4:
|
if opcode <= opcodes.OP_PUSHDATA4:
|
||||||
nSize = opcode
|
nSize = opcode
|
||||||
|
@ -303,7 +301,8 @@ def parse_scriptSig(d, _bytes):
|
||||||
decoded = [ x for x in script_GetOp(_bytes) ]
|
decoded = [ x for x in script_GetOp(_bytes) ]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# coinbase transactions raise an exception
|
# coinbase transactions raise an exception
|
||||||
print_error("cannot find address in input script", bh2u(_bytes))
|
print_error("parse_scriptSig: cannot find address in input script (coinbase?)",
|
||||||
|
bh2u(_bytes))
|
||||||
return
|
return
|
||||||
|
|
||||||
match = [ opcodes.OP_PUSHDATA4 ]
|
match = [ opcodes.OP_PUSHDATA4 ]
|
||||||
|
@ -320,9 +319,9 @@ def parse_scriptSig(d, _bytes):
|
||||||
d['pubkeys'] = ["(pubkey)"]
|
d['pubkeys'] = ["(pubkey)"]
|
||||||
return
|
return
|
||||||
|
|
||||||
# non-generated TxIn transactions push a signature
|
# p2pkh TxIn transactions push a signature
|
||||||
# (seventy-something bytes) and then their public key
|
# (71-73 bytes) and then their public key
|
||||||
# (65 bytes) onto the stack:
|
# (33 or 65 bytes) onto the stack:
|
||||||
match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ]
|
match = [ opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4 ]
|
||||||
if match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
sig = bh2u(decoded[0][1])
|
sig = bh2u(decoded[0][1])
|
||||||
|
@ -331,7 +330,8 @@ def parse_scriptSig(d, _bytes):
|
||||||
signatures = parse_sig([sig])
|
signatures = parse_sig([sig])
|
||||||
pubkey, address = xpubkey_to_address(x_pubkey)
|
pubkey, address = xpubkey_to_address(x_pubkey)
|
||||||
except:
|
except:
|
||||||
print_error("cannot find address in input script", bh2u(_bytes))
|
print_error("parse_scriptSig: cannot find address in input script (p2pkh?)",
|
||||||
|
bh2u(_bytes))
|
||||||
return
|
return
|
||||||
d['type'] = 'p2pkh'
|
d['type'] = 'p2pkh'
|
||||||
d['signatures'] = signatures
|
d['signatures'] = signatures
|
||||||
|
@ -343,37 +343,49 @@ def parse_scriptSig(d, _bytes):
|
||||||
|
|
||||||
# p2sh transaction, m of n
|
# p2sh transaction, m of n
|
||||||
match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1)
|
match = [ opcodes.OP_0 ] + [ opcodes.OP_PUSHDATA4 ] * (len(decoded) - 1)
|
||||||
if not match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
print_error("cannot find address in input script", bh2u(_bytes))
|
x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
|
||||||
|
try:
|
||||||
|
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1])
|
||||||
|
except NotRecognizedRedeemScript:
|
||||||
|
print_error("parse_scriptSig: cannot find address in input script (p2sh?)",
|
||||||
|
bh2u(_bytes))
|
||||||
|
# we could still guess:
|
||||||
|
# d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1]))
|
||||||
|
return
|
||||||
|
# write result in d
|
||||||
|
d['type'] = 'p2sh'
|
||||||
|
d['num_sig'] = m
|
||||||
|
d['signatures'] = parse_sig(x_sig)
|
||||||
|
d['x_pubkeys'] = x_pubkeys
|
||||||
|
d['pubkeys'] = pubkeys
|
||||||
|
d['redeemScript'] = redeemScript
|
||||||
|
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
|
||||||
return
|
return
|
||||||
x_sig = [bh2u(x[1]) for x in decoded[1:-1]]
|
|
||||||
m, n, x_pubkeys, pubkeys, redeemScript = parse_redeemScript(decoded[-1][1])
|
print_error("parse_scriptSig: cannot find address in input script (unknown)",
|
||||||
# write result in d
|
bh2u(_bytes))
|
||||||
d['type'] = 'p2sh'
|
|
||||||
d['num_sig'] = m
|
|
||||||
d['signatures'] = parse_sig(x_sig)
|
|
||||||
d['x_pubkeys'] = x_pubkeys
|
|
||||||
d['pubkeys'] = pubkeys
|
|
||||||
d['redeemScript'] = redeemScript
|
|
||||||
d['address'] = hash160_to_p2sh(hash_160(bfh(redeemScript)))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_redeemScript(s):
|
def parse_redeemScript(s):
|
||||||
dec2 = [ x for x in script_GetOp(s) ]
|
dec2 = [ x for x in script_GetOp(s) ]
|
||||||
m = dec2[0][0] - opcodes.OP_1 + 1
|
try:
|
||||||
n = dec2[-2][0] - opcodes.OP_1 + 1
|
m = dec2[0][0] - opcodes.OP_1 + 1
|
||||||
|
n = dec2[-2][0] - opcodes.OP_1 + 1
|
||||||
|
except IndexError:
|
||||||
|
raise NotRecognizedRedeemScript()
|
||||||
op_m = opcodes.OP_1 + m - 1
|
op_m = opcodes.OP_1 + m - 1
|
||||||
op_n = opcodes.OP_1 + n - 1
|
op_n = opcodes.OP_1 + n - 1
|
||||||
match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ]
|
match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ]
|
||||||
if not match_decoded(dec2, match_multisig):
|
if not match_decoded(dec2, match_multisig):
|
||||||
print_error("cannot find address in input script", bh2u(s))
|
|
||||||
raise NotRecognizedRedeemScript()
|
raise NotRecognizedRedeemScript()
|
||||||
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
|
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
|
||||||
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
|
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
|
||||||
redeemScript = multisig_script(pubkeys, m)
|
redeemScript = multisig_script(pubkeys, m)
|
||||||
return m, n, x_pubkeys, pubkeys, redeemScript
|
return m, n, x_pubkeys, pubkeys, redeemScript
|
||||||
|
|
||||||
def get_address_from_output_script(_bytes):
|
|
||||||
|
def get_address_from_output_script(_bytes, *, net=None):
|
||||||
decoded = [x for x in script_GetOp(_bytes)]
|
decoded = [x for x in script_GetOp(_bytes)]
|
||||||
|
|
||||||
# The Genesis Block, self-payments, and pay-by-IP-address payments look like:
|
# The Genesis Block, self-payments, and pay-by-IP-address payments look like:
|
||||||
|
@ -386,12 +398,12 @@ def get_address_from_output_script(_bytes):
|
||||||
# DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG
|
# DUP HASH160 20 BYTES:... EQUALVERIFY CHECKSIG
|
||||||
match = [ opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ]
|
match = [ opcodes.OP_DUP, opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ]
|
||||||
if match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1])
|
return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net)
|
||||||
|
|
||||||
# p2sh
|
# p2sh
|
||||||
match = [ opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL ]
|
match = [ opcodes.OP_HASH160, opcodes.OP_PUSHDATA4, opcodes.OP_EQUAL ]
|
||||||
if match_decoded(decoded, match):
|
if match_decoded(decoded, match):
|
||||||
return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1])
|
return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net)
|
||||||
|
|
||||||
return TYPE_SCRIPT, bh2u(_bytes)
|
return TYPE_SCRIPT, bh2u(_bytes)
|
||||||
|
|
||||||
|
@ -405,19 +417,23 @@ def parse_input(vds):
|
||||||
d['prevout_hash'] = prevout_hash
|
d['prevout_hash'] = prevout_hash
|
||||||
d['prevout_n'] = prevout_n
|
d['prevout_n'] = prevout_n
|
||||||
d['sequence'] = sequence
|
d['sequence'] = sequence
|
||||||
|
d['x_pubkeys'] = []
|
||||||
|
d['pubkeys'] = []
|
||||||
|
d['signatures'] = {}
|
||||||
|
d['address'] = None
|
||||||
|
d['num_sig'] = 0
|
||||||
if prevout_hash == '00'*32:
|
if prevout_hash == '00'*32:
|
||||||
d['type'] = 'coinbase'
|
d['type'] = 'coinbase'
|
||||||
d['scriptSig'] = bh2u(scriptSig)
|
d['scriptSig'] = bh2u(scriptSig)
|
||||||
else:
|
else:
|
||||||
d['x_pubkeys'] = []
|
|
||||||
d['pubkeys'] = []
|
|
||||||
d['signatures'] = {}
|
|
||||||
d['address'] = None
|
|
||||||
d['type'] = 'unknown'
|
d['type'] = 'unknown'
|
||||||
d['num_sig'] = 0
|
|
||||||
if scriptSig:
|
if scriptSig:
|
||||||
d['scriptSig'] = bh2u(scriptSig)
|
d['scriptSig'] = bh2u(scriptSig)
|
||||||
parse_scriptSig(d, scriptSig)
|
try:
|
||||||
|
parse_scriptSig(d, scriptSig)
|
||||||
|
except BaseException:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
print_error('failed to parse scriptSig', bh2u(scriptSig))
|
||||||
else:
|
else:
|
||||||
d['scriptSig'] = ''
|
d['scriptSig'] = ''
|
||||||
|
|
||||||
|
@ -479,7 +495,7 @@ class Transaction:
|
||||||
elif isinstance(raw, dict):
|
elif isinstance(raw, dict):
|
||||||
self.raw = raw['hex']
|
self.raw = raw['hex']
|
||||||
else:
|
else:
|
||||||
raise BaseException("cannot initialize transaction", raw)
|
raise Exception("cannot initialize transaction", raw)
|
||||||
self._inputs = None
|
self._inputs = None
|
||||||
self._outputs = None
|
self._outputs = None
|
||||||
self.locktime = 0
|
self.locktime = 0
|
||||||
|
@ -503,6 +519,8 @@ class Transaction:
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_sorted_pubkeys(self, txin):
|
def get_sorted_pubkeys(self, txin):
|
||||||
# sort pubkeys and x_pubkeys, using the order of pubkeys
|
# sort pubkeys and x_pubkeys, using the order of pubkeys
|
||||||
|
if txin['type'] == 'coinbase':
|
||||||
|
return [], []
|
||||||
x_pubkeys = txin['x_pubkeys']
|
x_pubkeys = txin['x_pubkeys']
|
||||||
pubkeys = txin.get('pubkeys')
|
pubkeys = txin.get('pubkeys')
|
||||||
if pubkeys is None:
|
if pubkeys is None:
|
||||||
|
@ -603,6 +621,8 @@ class Transaction:
|
||||||
def get_siglist(self, txin, estimate_size=False):
|
def get_siglist(self, txin, estimate_size=False):
|
||||||
# if we have enough signatures, we use the actual pubkeys
|
# if we have enough signatures, we use the actual pubkeys
|
||||||
# otherwise, use extended pubkeys (with bip32 derivation)
|
# otherwise, use extended pubkeys (with bip32 derivation)
|
||||||
|
if txin['type'] == 'coinbase':
|
||||||
|
return [], []
|
||||||
num_sig = txin.get('num_sig', 1)
|
num_sig = txin.get('num_sig', 1)
|
||||||
if estimate_size:
|
if estimate_size:
|
||||||
pubkey_size = self.estimate_pubkey_size_for_txin(txin)
|
pubkey_size = self.estimate_pubkey_size_for_txin(txin)
|
||||||
|
@ -645,7 +665,9 @@ class Transaction:
|
||||||
return script
|
return script
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_txin_complete(self, txin):
|
def is_txin_complete(cls, txin):
|
||||||
|
if txin['type'] == 'coinbase':
|
||||||
|
return True
|
||||||
num_sig = txin.get('num_sig', 1)
|
num_sig = txin.get('num_sig', 1)
|
||||||
x_signatures = txin['signatures']
|
x_signatures = txin['signatures']
|
||||||
signatures = list(filter(None, x_signatures))
|
signatures = list(filter(None, x_signatures))
|
||||||
|
@ -653,13 +675,13 @@ class Transaction:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_preimage_script(self, txin):
|
def get_preimage_script(self, txin):
|
||||||
|
pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
|
||||||
if txin['type'] == 'p2pkh':
|
if txin['type'] == 'p2pkh':
|
||||||
return bitcoin.address_to_script(txin['address'])
|
return bitcoin.address_to_script(txin['address'])
|
||||||
elif txin['type'] in ['p2sh']:
|
elif txin['type'] in ['p2sh']:
|
||||||
pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin)
|
|
||||||
return multisig_script(pubkeys, txin['num_sig'])
|
return multisig_script(pubkeys, txin['num_sig'])
|
||||||
elif txin['type'] == 'p2pk':
|
elif txin['type'] == 'p2pk':
|
||||||
pubkey = txin['pubkeys'][0]
|
pubkey = pubkeys[0]
|
||||||
return bitcoin.public_key_to_p2pk_script(pubkey)
|
return bitcoin.public_key_to_p2pk_script(pubkey)
|
||||||
else:
|
else:
|
||||||
raise TypeError('Unknown txin type', txin['type'])
|
raise TypeError('Unknown txin type', txin['type'])
|
||||||
|
@ -668,6 +690,14 @@ class Transaction:
|
||||||
def serialize_outpoint(self, txin):
|
def serialize_outpoint(self, txin):
|
||||||
return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
|
return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_outpoint_from_txin(cls, txin):
|
||||||
|
if txin['type'] == 'coinbase':
|
||||||
|
return None
|
||||||
|
prevout_hash = txin['prevout_hash']
|
||||||
|
prevout_n = txin['prevout_n']
|
||||||
|
return prevout_hash + ':%d' % prevout_n
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize_input(self, txin, script):
|
def serialize_input(self, txin, script):
|
||||||
# Prev hash and index
|
# Prev hash and index
|
||||||
|
@ -761,6 +791,13 @@ class Transaction:
|
||||||
input_size = len(cls.serialize_input(txin, script)) // 2
|
input_size = len(cls.serialize_input(txin, script)) // 2
|
||||||
return 4 * input_size
|
return 4 * input_size
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def estimated_output_size(cls, address):
|
||||||
|
"""Return an estimate of serialized output size in bytes."""
|
||||||
|
script = bitcoin.address_to_script(address)
|
||||||
|
# 8 byte value + 1 byte script len + script
|
||||||
|
return 9 + len(script) // 2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def virtual_size_from_weight(cls, weight):
|
def virtual_size_from_weight(cls, weight):
|
||||||
return weight // 4 + (weight % 4 > 0)
|
return weight // 4 + (weight % 4 > 0)
|
||||||
|
@ -814,7 +851,8 @@ class Transaction:
|
||||||
private_key = bitcoin.MySigningKey.from_secret_exponent(secexp, curve = SECP256k1)
|
private_key = bitcoin.MySigningKey.from_secret_exponent(secexp, curve = SECP256k1)
|
||||||
public_key = private_key.get_verifying_key()
|
public_key = private_key.get_verifying_key()
|
||||||
sig = private_key.sign_digest_deterministic(pre_hash, hashfunc=hashlib.sha256, sigencode = ecdsa.util.sigencode_der_canonize)
|
sig = private_key.sign_digest_deterministic(pre_hash, hashfunc=hashlib.sha256, sigencode = ecdsa.util.sigencode_der_canonize)
|
||||||
assert public_key.verify_digest(sig, pre_hash, sigdecode = ecdsa.util.sigdecode_der)
|
if not public_key.verify_digest(sig, pre_hash, sigdecode = ecdsa.util.sigdecode_der_canonize):
|
||||||
|
raise Exception('Sanity check verifying our own signature failed.')
|
||||||
txin['signatures'][j] = bh2u(sig) + '01'
|
txin['signatures'][j] = bh2u(sig) + '01'
|
||||||
#txin['x_pubkeys'][j] = pubkey
|
#txin['x_pubkeys'][j] = pubkey
|
||||||
txin['pubkeys'][j] = pubkey # needed for fd keys
|
txin['pubkeys'][j] = pubkey # needed for fd keys
|
||||||
|
|
196
lib/util.py
196
lib/util.py
|
@ -41,28 +41,102 @@ def inv_dict(d):
|
||||||
|
|
||||||
|
|
||||||
base_units = {'ZEC':8, 'mZEC':5, 'uZEC':2}
|
base_units = {'ZEC':8, 'mZEC':5, 'uZEC':2}
|
||||||
fee_levels = [_('Within 25 blocks'), _('Within 10 blocks'), _('Within 5 blocks'), _('Within 2 blocks'), _('In the next block')]
|
|
||||||
|
|
||||||
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(".")]
|
||||||
|
|
||||||
class NotEnoughFunds(Exception): pass
|
class NotEnoughFunds(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoDynamicFeeEstimates(Exception):
|
||||||
|
def __str__(self):
|
||||||
|
return _('Dynamic fee estimates not available')
|
||||||
|
|
||||||
|
|
||||||
class InvalidPassword(Exception):
|
class InvalidPassword(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Incorrect password")
|
return _("Incorrect password")
|
||||||
|
|
||||||
|
|
||||||
|
class FileImportFailed(Exception):
|
||||||
|
def __init__(self, message=''):
|
||||||
|
self.message = str(message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Failed to import from file.") + "\n" + self.message
|
||||||
|
|
||||||
|
|
||||||
|
class FileExportFailed(Exception):
|
||||||
|
def __init__(self, message=''):
|
||||||
|
self.message = str(message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Failed to export to file.") + "\n" + self.message
|
||||||
|
|
||||||
|
|
||||||
|
class TimeoutException(Exception):
|
||||||
|
def __init__(self, message=''):
|
||||||
|
self.message = str(message)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not self.message:
|
||||||
|
return _("Operation timed out.")
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class WalletFileException(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
|
class BitcoinException(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
# Throw this exception to unwind the stack like when an error occurs.
|
# Throw this exception to unwind the stack like when an error occurs.
|
||||||
# However unlike other exceptions the user won't be informed.
|
# However unlike other exceptions the user won't be informed.
|
||||||
class UserCancelled(Exception):
|
class UserCancelled(Exception):
|
||||||
'''An exception that is suppressed from the user'''
|
'''An exception that is suppressed from the user'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class Satoshis(object):
|
||||||
|
def __new__(cls, value):
|
||||||
|
self = super(Satoshis, cls).__new__(cls)
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Satoshis(%d)'%self.value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return format_satoshis(self.value) + " BTC"
|
||||||
|
|
||||||
|
class Fiat(object):
|
||||||
|
def __new__(cls, value, ccy):
|
||||||
|
self = super(Fiat, cls).__new__(cls)
|
||||||
|
self.ccy = ccy
|
||||||
|
self.value = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Fiat(%s)'% self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.value.is_nan():
|
||||||
|
return _('No Data')
|
||||||
|
else:
|
||||||
|
return "{:.2f}".format(self.value) + ' ' + self.ccy
|
||||||
|
|
||||||
class MyEncoder(json.JSONEncoder):
|
class MyEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
from .transaction import Transaction
|
from .transaction import Transaction
|
||||||
if isinstance(obj, Transaction):
|
if isinstance(obj, Transaction):
|
||||||
return obj.as_dict()
|
return obj.as_dict()
|
||||||
|
if isinstance(obj, Satoshis):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, Fiat):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, Decimal):
|
||||||
|
return str(obj)
|
||||||
|
if isinstance(obj, datetime):
|
||||||
|
return obj.isoformat(' ')[:-3]
|
||||||
return super(MyEncoder, self).default(obj)
|
return super(MyEncoder, self).default(obj)
|
||||||
|
|
||||||
class PrintError(object):
|
class PrintError(object):
|
||||||
|
@ -71,8 +145,12 @@ class PrintError(object):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def print_error(self, *msg):
|
def print_error(self, *msg):
|
||||||
|
# only prints with --verbose flag
|
||||||
print_error("[%s]" % self.diagnostic_name(), *msg)
|
print_error("[%s]" % self.diagnostic_name(), *msg)
|
||||||
|
|
||||||
|
def print_stderr(self, *msg):
|
||||||
|
print_stderr("[%s]" % self.diagnostic_name(), *msg)
|
||||||
|
|
||||||
def print_msg(self, *msg):
|
def print_msg(self, *msg):
|
||||||
print_msg("[%s]" % self.diagnostic_name(), *msg)
|
print_msg("[%s]" % self.diagnostic_name(), *msg)
|
||||||
|
|
||||||
|
@ -247,8 +325,8 @@ def android_check_data_dir():
|
||||||
old_electrum_dir = ext_dir + '/electrum-zcash'
|
old_electrum_dir = ext_dir + '/electrum-zcash'
|
||||||
if not os.path.exists(data_dir) and os.path.exists(old_electrum_dir):
|
if not os.path.exists(data_dir) and os.path.exists(old_electrum_dir):
|
||||||
import shutil
|
import shutil
|
||||||
new_headers_path = android_headers_dir() + android_headers_file_name()
|
new_headers_path = android_headers_dir() + '/blockchain_headers'
|
||||||
old_headers_path = old_electrum_dir + android_headers_file_name()
|
old_headers_path = old_electrum_dir + '/blockchain_headers'
|
||||||
if not os.path.exists(new_headers_path) and os.path.exists(old_headers_path):
|
if not os.path.exists(new_headers_path) and os.path.exists(old_headers_path):
|
||||||
print_error("Moving headers file to", new_headers_path)
|
print_error("Moving headers file to", new_headers_path)
|
||||||
shutil.move(old_headers_path, new_headers_path)
|
shutil.move(old_headers_path, new_headers_path)
|
||||||
|
@ -348,7 +426,7 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
x = int(x) # Some callers pass Decimal
|
x = int(x) # Some callers pass Decimal
|
||||||
scale_factor = pow (10, decimal_point)
|
scale_factor = pow (10, decimal_point)
|
||||||
integer_part = "{:n}".format(int(abs(x) / scale_factor))
|
integer_part = "{:d}".format(int(abs(x) / scale_factor))
|
||||||
if x < 0:
|
if x < 0:
|
||||||
integer_part = '-' + integer_part
|
integer_part = '-' + integer_part
|
||||||
elif is_diff:
|
elif is_diff:
|
||||||
|
@ -365,10 +443,9 @@ def format_satoshis(x, is_diff=False, num_zeros = 0, decimal_point = 8, whitespa
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def timestamp_to_datetime(timestamp):
|
def timestamp_to_datetime(timestamp):
|
||||||
try:
|
if timestamp is None:
|
||||||
return datetime.fromtimestamp(timestamp)
|
|
||||||
except:
|
|
||||||
return None
|
return None
|
||||||
|
return datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
def format_time(timestamp):
|
def format_time(timestamp):
|
||||||
date = timestamp_to_datetime(timestamp)
|
date = timestamp_to_datetime(timestamp)
|
||||||
|
@ -429,22 +506,22 @@ def time_difference(distance_in_time, include_seconds):
|
||||||
return "over %d years" % (round(distance_in_minutes / 525600))
|
return "over %d years" % (round(distance_in_minutes / 525600))
|
||||||
|
|
||||||
mainnet_block_explorers = {
|
mainnet_block_explorers = {
|
||||||
'blockexplorer.com': ('https://zcash.blockexplorer.com/blocks',
|
'blockexplorer.com': ('https://zcash.blockexplorer.com/blocks/',
|
||||||
{'tx': 'transactions', 'addr': 'addresses'}),
|
{'tx': 'transactions/', 'addr': 'addresses/'}),
|
||||||
'system default': ('blockchain:',
|
'system default': ('blockchain:/',
|
||||||
{'tx': 'tx', 'addr': 'address'}),
|
{'tx': 'tx/', 'addr': 'address/'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
testnet_block_explorers = {
|
testnet_block_explorers = {
|
||||||
'testnet.z.cash': ('https://explorer.testnet.z.cash/',
|
'testnet.z.cash': ('https://explorer.testnet.z.cash/',
|
||||||
{'tx': 'tx', 'addr': 'address'}),
|
{'tx': 'tx/', 'addr': 'address/'}),
|
||||||
'system default': ('blockchain:',
|
'system default': ('blockchain:/',
|
||||||
{'tx': 'tx', 'addr': 'address'}),
|
{'tx': 'tx/', 'addr': 'address/'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def block_explorer_info():
|
def block_explorer_info():
|
||||||
from . import bitcoin
|
from . import constants
|
||||||
return testnet_block_explorers if bitcoin.NetworkConstants.TESTNET else mainnet_block_explorers
|
return testnet_block_explorers if constants.net.TESTNET else mainnet_block_explorers
|
||||||
|
|
||||||
def block_explorer(config):
|
def block_explorer(config):
|
||||||
return config.get('block_explorer', 'blockexplorer.com')
|
return config.get('block_explorer', 'blockexplorer.com')
|
||||||
|
@ -460,7 +537,7 @@ def block_explorer_URL(config, kind, item):
|
||||||
if not kind_str:
|
if not kind_str:
|
||||||
return
|
return
|
||||||
url_parts = [be_tuple[0], kind_str, item]
|
url_parts = [be_tuple[0], kind_str, item]
|
||||||
return "/".join(url_parts)
|
return ''.join(url_parts)
|
||||||
|
|
||||||
# URL decode
|
# URL decode
|
||||||
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
|
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
|
||||||
|
@ -472,12 +549,12 @@ def parse_URI(uri, on_pr=None):
|
||||||
|
|
||||||
if ':' not in uri:
|
if ':' not in uri:
|
||||||
if not bitcoin.is_address(uri):
|
if not bitcoin.is_address(uri):
|
||||||
raise BaseException("Not a Zcash address")
|
raise Exception("Not a Zcash address")
|
||||||
return {'address': uri}
|
return {'address': uri}
|
||||||
|
|
||||||
u = urllib.parse.urlparse(uri)
|
u = urllib.parse.urlparse(uri)
|
||||||
if u.scheme != 'zcash':
|
if u.scheme != 'zcash':
|
||||||
raise BaseException("Not a Zcash URI")
|
raise Exception("Not a Zcash URI")
|
||||||
address = u.path
|
address = u.path
|
||||||
|
|
||||||
# python for android fails to parse query
|
# python for android fails to parse query
|
||||||
|
@ -494,7 +571,7 @@ def parse_URI(uri, on_pr=None):
|
||||||
out = {k: v[0] for k, v in pq.items()}
|
out = {k: v[0] for k, v in pq.items()}
|
||||||
if address:
|
if address:
|
||||||
if not bitcoin.is_address(address):
|
if not bitcoin.is_address(address):
|
||||||
raise BaseException("Invalid Zcash address:" + address)
|
raise Exception("Invalid Zcash address:" + address)
|
||||||
out['address'] = address
|
out['address'] = address
|
||||||
if 'amount' in out:
|
if 'amount' in out:
|
||||||
am = out['amount']
|
am = out['amount']
|
||||||
|
@ -643,10 +720,6 @@ class SocketPipe:
|
||||||
print_error("SSLError:", e)
|
print_error("SSLError:", e)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
continue
|
continue
|
||||||
except OSError as e:
|
|
||||||
print_error("OSError", e)
|
|
||||||
time.sleep(0.1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
class QueuePipe:
|
class QueuePipe:
|
||||||
|
@ -683,25 +756,56 @@ class QueuePipe:
|
||||||
self.send(request)
|
self.send(request)
|
||||||
|
|
||||||
|
|
||||||
def check_www_dir(rdir):
|
|
||||||
import urllib, shutil, os
|
|
||||||
if not os.path.exists(rdir):
|
def setup_thread_excepthook():
|
||||||
os.mkdir(rdir)
|
"""
|
||||||
index = os.path.join(rdir, 'index.html')
|
Workaround for `sys.excepthook` thread bug from:
|
||||||
if not os.path.exists(index):
|
http://bugs.python.org/issue1230540
|
||||||
print_error("copying index.html")
|
|
||||||
src = os.path.join(os.path.dirname(__file__), 'www', 'index.html')
|
Call once from the main thread before creating any threads.
|
||||||
shutil.copy(src, index)
|
"""
|
||||||
files = [
|
|
||||||
"https://code.jquery.com/jquery-1.9.1.min.js",
|
init_original = threading.Thread.__init__
|
||||||
"https://raw.githubusercontent.com/davidshimjs/qrcodejs/master/qrcode.js",
|
|
||||||
"https://code.jquery.com/ui/1.10.3/jquery-ui.js",
|
def init(self, *args, **kwargs):
|
||||||
"https://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css"
|
|
||||||
]
|
init_original(self, *args, **kwargs)
|
||||||
for URL in files:
|
run_original = self.run
|
||||||
path = urllib.parse.urlsplit(URL).path
|
|
||||||
filename = os.path.basename(path)
|
def run_with_except_hook(*args2, **kwargs2):
|
||||||
path = os.path.join(rdir, filename)
|
try:
|
||||||
if not os.path.exists(path):
|
run_original(*args2, **kwargs2)
|
||||||
print_error("downloading ", URL)
|
except Exception:
|
||||||
urllib.request.urlretrieve(URL, path)
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
self.run = run_with_except_hook
|
||||||
|
|
||||||
|
threading.Thread.__init__ = init
|
||||||
|
|
||||||
|
|
||||||
|
def versiontuple(v):
|
||||||
|
return tuple(map(int, (v.split("."))))
|
||||||
|
|
||||||
|
|
||||||
|
def import_meta(path, validater, load_meta):
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
d = validater(json.loads(f.read()))
|
||||||
|
load_meta(d)
|
||||||
|
#backwards compatibility for JSONDecodeError
|
||||||
|
except ValueError:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
raise FileImportFailed(_("Invalid JSON code."))
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
raise FileImportFailed(e)
|
||||||
|
|
||||||
|
|
||||||
|
def export_meta(meta, fileName):
|
||||||
|
try:
|
||||||
|
with open(fileName, 'w+', encoding='utf-8') as f:
|
||||||
|
json.dump(meta, f, indent=4, sort_keys=True)
|
||||||
|
except (IOError, os.error) as e:
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
raise FileExportFailed(e)
|
||||||
|
|
|
@ -36,22 +36,37 @@ class SPV(ThreadJob):
|
||||||
self.merkle_roots = {}
|
self.merkle_roots = {}
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
interface = self.network.interface
|
||||||
|
if not interface:
|
||||||
|
return
|
||||||
|
blockchain = interface.blockchain
|
||||||
|
if not blockchain:
|
||||||
|
return
|
||||||
lh = self.network.get_local_height()
|
lh = self.network.get_local_height()
|
||||||
unverified = self.wallet.get_unverified_txs()
|
unverified = self.wallet.get_unverified_txs()
|
||||||
for tx_hash, tx_height in unverified.items():
|
for tx_hash, tx_height in unverified.items():
|
||||||
# do not request merkle branch before headers are available
|
# do not request merkle branch before headers are available
|
||||||
if (tx_height > 0) and (tx_hash not in self.merkle_roots) and (tx_height <= lh):
|
if (tx_height > 0) and (tx_height <= lh):
|
||||||
request = ('blockchain.transaction.get_merkle',
|
header = blockchain.read_header(tx_height)
|
||||||
[tx_hash, tx_height])
|
if header is None:
|
||||||
self.network.send([request], self.verify_merkle)
|
index = tx_height // 2016
|
||||||
self.print_error('requested merkle', tx_hash)
|
if index < len(blockchain.checkpoints):
|
||||||
self.merkle_roots[tx_hash] = None
|
self.network.request_chunk(interface, index)
|
||||||
|
else:
|
||||||
|
if tx_hash not in self.merkle_roots:
|
||||||
|
request = ('blockchain.transaction.get_merkle',
|
||||||
|
[tx_hash, tx_height])
|
||||||
|
self.network.send([request], self.verify_merkle)
|
||||||
|
self.print_error('requested merkle', tx_hash)
|
||||||
|
self.merkle_roots[tx_hash] = None
|
||||||
|
|
||||||
if self.network.blockchain() != self.blockchain:
|
if self.network.blockchain() != self.blockchain:
|
||||||
self.blockchain = self.network.blockchain()
|
self.blockchain = self.network.blockchain()
|
||||||
self.undo_verifications()
|
self.undo_verifications()
|
||||||
|
|
||||||
def verify_merkle(self, r):
|
def verify_merkle(self, r):
|
||||||
|
if self.wallet.verifier is None:
|
||||||
|
return # we have been killed, this was just an orphan callback
|
||||||
if r.get('error'):
|
if r.get('error'):
|
||||||
self.print_error('received an error:', r)
|
self.print_error('received an error:', r)
|
||||||
return
|
return
|
||||||
|
@ -64,17 +79,26 @@ class SPV(ThreadJob):
|
||||||
pos = merkle.get('pos')
|
pos = merkle.get('pos')
|
||||||
merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
|
merkle_root = self.hash_merkle_root(merkle['merkle'], tx_hash, pos)
|
||||||
header = self.network.blockchain().read_header(tx_height)
|
header = self.network.blockchain().read_header(tx_height)
|
||||||
if not header or header.get('merkle_root') != merkle_root:
|
# FIXME: if verification fails below,
|
||||||
# FIXME: we should make a fresh connection to a server to
|
# we should make a fresh connection to a server to
|
||||||
# recover from this, as this TX will now never verify
|
# recover from this, as this TX will now never verify
|
||||||
self.print_error("merkle verification failed for", tx_hash)
|
if not header:
|
||||||
|
self.print_error(
|
||||||
|
"merkle verification failed for {} (missing header {})"
|
||||||
|
.format(tx_hash, tx_height))
|
||||||
|
return
|
||||||
|
if header.get('merkle_root') != merkle_root:
|
||||||
|
self.print_error(
|
||||||
|
"merkle verification failed for {} (merkle root mismatch {} != {})"
|
||||||
|
.format(tx_hash, header.get('merkle_root'), merkle_root))
|
||||||
return
|
return
|
||||||
# we passed all the tests
|
# we passed all the tests
|
||||||
self.merkle_roots[tx_hash] = merkle_root
|
self.merkle_roots[tx_hash] = merkle_root
|
||||||
self.print_error("verified %s" % tx_hash)
|
self.print_error("verified %s" % tx_hash)
|
||||||
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
|
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
|
||||||
|
|
||||||
def hash_merkle_root(self, merkle_s, target_hash, pos):
|
@classmethod
|
||||||
|
def hash_merkle_root(cls, merkle_s, target_hash, pos):
|
||||||
h = hash_decode(target_hash)
|
h = hash_decode(target_hash)
|
||||||
for i in range(len(merkle_s)):
|
for i in range(len(merkle_s)):
|
||||||
item = merkle_s[i]
|
item = merkle_s[i]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
ELECTRUM_VERSION = '3.0.6' # version of the client package
|
ELECTRUM_VERSION = '3.1.3' # version of the client package
|
||||||
PROTOCOL_VERSION = '1.2' # protocol version requested
|
PROTOCOL_VERSION = '1.2' # protocol version requested
|
||||||
|
|
||||||
# The hash of the mnemonic seed must begin with this
|
# The hash of the mnemonic seed must begin with this
|
||||||
|
|
939
lib/wallet.py
939
lib/wallet.py
File diff suppressed because it is too large
Load Diff
|
@ -64,7 +64,7 @@ class WsClientThread(util.DaemonThread):
|
||||||
# read json file
|
# read json file
|
||||||
rdir = self.config.get('requests_dir')
|
rdir = self.config.get('requests_dir')
|
||||||
n = os.path.join(rdir, 'req', request_id[0], request_id[1], request_id, request_id + '.json')
|
n = os.path.join(rdir, 'req', request_id[0], request_id[1], request_id, request_id + '.json')
|
||||||
with open(n) as f:
|
with open(n, encoding='utf-8') as f:
|
||||||
s = f.read()
|
s = f.read()
|
||||||
d = json.loads(s)
|
d = json.loads(s)
|
||||||
addr = d.get('address')
|
addr = d.get('address')
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
require_once 'jsonRPCClient.php';
|
|
||||||
$electrum = new jsonRPCClient('http://localhost:7777');
|
|
||||||
|
|
||||||
echo '<b>Wallet balance</b><br />'."\n";
|
|
||||||
try {
|
|
||||||
|
|
||||||
$balance = $electrum->getbalance();
|
|
||||||
echo 'confirmed: <i>'.$balance['confirmed'].'</i><br />'."\n";
|
|
||||||
echo 'unconfirmed: <i>'.$balance['unconfirmed'].'</i><br />'."\n";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo nl2br($e->getMessage()).'<br />'."\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -284,7 +284,7 @@ class X509(object):
|
||||||
return self.AKI if self.AKI else repr(self.issuer)
|
return self.AKI if self.AKI else repr(self.issuer)
|
||||||
|
|
||||||
def get_common_name(self):
|
def get_common_name(self):
|
||||||
return self.subject.get('2.5.4.3', 'unknown').decode()
|
return self.subject.get('2.5.4.3', b'unknown').decode()
|
||||||
|
|
||||||
def get_signature(self):
|
def get_signature(self):
|
||||||
return self.cert_sig_algo, self.signature, self.data
|
return self.cert_sig_algo, self.signature, self.data
|
||||||
|
@ -313,7 +313,7 @@ def load_certificates(ca_path):
|
||||||
ca_list = {}
|
ca_list = {}
|
||||||
ca_keyID = {}
|
ca_keyID = {}
|
||||||
# ca_path = '/tmp/tmp.txt'
|
# ca_path = '/tmp/tmp.txt'
|
||||||
with open(ca_path, 'r') as f:
|
with open(ca_path, 'r', encoding='utf-8') as f:
|
||||||
s = f.read()
|
s = f.read()
|
||||||
bList = pem.dePemList(s, "CERTIFICATE")
|
bList = pem.dePemList(s, "CERTIFICATE")
|
||||||
for b in bList:
|
for b in bList:
|
||||||
|
|
Loading…
Reference in New Issue