2018-03-26 04:59:57 +02:00
|
|
|
import copy
|
|
|
|
import stat
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import logging
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
from .constants import NEW_SEED_VERSION
|
2018-03-26 04:59:57 +02:00
|
|
|
from .account import Account
|
|
|
|
from .mnemonic import Mnemonic
|
2018-03-27 08:40:44 +02:00
|
|
|
from .lbrycrd import pw_encode, bip32_private_derivation, bip32_root
|
|
|
|
from .blockchain import BlockchainTransactions
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class WalletStorage:
|
2018-03-27 08:40:44 +02:00
|
|
|
|
2018-03-26 04:59:57 +02:00
|
|
|
def __init__(self, path):
|
|
|
|
self.data = {}
|
|
|
|
self.path = path
|
|
|
|
self.file_exists = False
|
|
|
|
self.modified = False
|
2018-03-27 08:40:44 +02:00
|
|
|
self.path and self.read()
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def read(self):
|
2018-03-26 04:59:57 +02:00
|
|
|
try:
|
|
|
|
with open(self.path, "r") as f:
|
|
|
|
data = f.read()
|
|
|
|
except IOError:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
self.data = json.loads(data)
|
2018-03-27 08:40:44 +02:00
|
|
|
except Exception:
|
2018-03-26 04:59:57 +02:00
|
|
|
self.data = {}
|
2018-03-27 08:40:44 +02:00
|
|
|
raise IOError("Cannot read wallet file '%s'" % self.path)
|
2018-03-26 04:59:57 +02:00
|
|
|
self.file_exists = True
|
|
|
|
|
|
|
|
def get(self, key, default=None):
|
2018-03-27 08:40:44 +02:00
|
|
|
v = self.data.get(key)
|
|
|
|
if v is None:
|
|
|
|
v = default
|
|
|
|
else:
|
|
|
|
v = copy.deepcopy(v)
|
2018-03-26 04:59:57 +02:00
|
|
|
return v
|
|
|
|
|
|
|
|
def put(self, key, value):
|
|
|
|
try:
|
|
|
|
json.dumps(key)
|
|
|
|
json.dumps(value)
|
|
|
|
except:
|
|
|
|
return
|
2018-03-27 08:40:44 +02:00
|
|
|
if value is not None:
|
|
|
|
if self.data.get(key) != value:
|
2018-03-26 04:59:57 +02:00
|
|
|
self.modified = True
|
2018-03-27 08:40:44 +02:00
|
|
|
self.data[key] = copy.deepcopy(value)
|
|
|
|
elif key in self.data:
|
|
|
|
self.modified = True
|
|
|
|
self.data.pop(key)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
def write(self):
|
2018-03-27 08:40:44 +02:00
|
|
|
self._write()
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
def _write(self):
|
|
|
|
if not self.modified:
|
|
|
|
return
|
|
|
|
s = json.dumps(self.data, indent=4, sort_keys=True)
|
|
|
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
|
|
|
with open(temp_path, "w") as f:
|
|
|
|
f.write(s)
|
|
|
|
f.flush()
|
|
|
|
os.fsync(f.fileno())
|
|
|
|
|
|
|
|
if os.path.exists(self.path):
|
|
|
|
mode = os.stat(self.path).st_mode
|
|
|
|
else:
|
|
|
|
mode = stat.S_IREAD | stat.S_IWRITE
|
|
|
|
# perform atomic write on POSIX systems
|
|
|
|
try:
|
|
|
|
os.rename(temp_path, self.path)
|
|
|
|
except:
|
|
|
|
os.remove(self.path)
|
|
|
|
os.rename(temp_path, self.path)
|
|
|
|
os.chmod(self.path, mode)
|
|
|
|
self.modified = False
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def upgrade(self):
|
|
|
|
|
|
|
|
def _rename_property(old, new):
|
|
|
|
if old in self.data:
|
|
|
|
old_value = self.data[old]
|
|
|
|
del self.data[old]
|
|
|
|
if new not in self.data:
|
|
|
|
self.data[new] = old_value
|
|
|
|
|
|
|
|
_rename_property('addr_history', 'history')
|
|
|
|
_rename_property('use_encryption', 'encrypted')
|
|
|
|
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
class Wallet:
|
|
|
|
|
|
|
|
root_name = 'x/'
|
2018-03-27 08:40:44 +02:00
|
|
|
root_derivation = 'm/'
|
|
|
|
gap_limit_for_change = 6
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def __init__(self, path, headers):
|
2018-03-26 04:59:57 +02:00
|
|
|
self.storage = storage = WalletStorage(path)
|
2018-03-27 08:40:44 +02:00
|
|
|
storage.upgrade()
|
|
|
|
self.headers = headers
|
|
|
|
self.accounts = self._instantiate_accounts(storage.get('accounts', {}))
|
|
|
|
self.history = BlockchainTransactions(storage.get('history', {}))
|
|
|
|
self.master_public_keys = storage.get('master_public_keys', {})
|
|
|
|
self.master_private_keys = storage.get('master_private_keys', {})
|
2018-03-26 04:59:57 +02:00
|
|
|
self.gap_limit = storage.get('gap_limit', 20)
|
2018-03-27 08:40:44 +02:00
|
|
|
self.seed = storage.get('seed', '')
|
2018-03-26 04:59:57 +02:00
|
|
|
self.seed_version = storage.get('seed_version', NEW_SEED_VERSION)
|
2018-03-27 08:40:44 +02:00
|
|
|
self.encrypted = storage.get('encrypted', storage.get('use_encryption', False))
|
2018-03-26 04:59:57 +02:00
|
|
|
self.claim_certificates = storage.get('claim_certificates', {})
|
|
|
|
self.default_certificate_claim = storage.get('default_certificate_claim', None)
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def _instantiate_accounts(self, accounts):
|
|
|
|
instances = {}
|
|
|
|
for index, details in accounts.items():
|
2018-03-26 04:59:57 +02:00
|
|
|
if 'xpub' in details:
|
2018-03-27 08:40:44 +02:00
|
|
|
instances[index] = Account(
|
|
|
|
details, self.gap_limit, self.gap_limit_for_change, self.is_address_old
|
2018-03-26 04:59:57 +02:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
log.error("cannot load account: {}".format(details))
|
2018-03-27 08:40:44 +02:00
|
|
|
return instances
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def exists(self):
|
|
|
|
return self.storage.file_exists
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def default_account(self):
|
|
|
|
return self.accounts['0']
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def sequences(self):
|
|
|
|
for account in self.accounts.values():
|
|
|
|
for sequence in account.sequences:
|
|
|
|
yield sequence
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def addresses(self):
|
|
|
|
for sequence in self.sequences:
|
|
|
|
for address in sequence.addresses:
|
|
|
|
yield address
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def receiving_addresses(self):
|
|
|
|
for account in self.accounts.values():
|
|
|
|
for address in account.receiving.addresses:
|
|
|
|
yield address
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def change_addresses(self):
|
|
|
|
for account in self.accounts.values():
|
|
|
|
for address in account.receiving.addresses:
|
|
|
|
yield address
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@property
|
|
|
|
def addresses_without_history(self):
|
|
|
|
for address in self.addresses:
|
|
|
|
if not self.history.has_address(address):
|
|
|
|
yield address
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def ensure_enough_addresses(self):
|
|
|
|
return [
|
|
|
|
address
|
|
|
|
for sequence in self.sequences
|
|
|
|
for address in sequence.ensure_enough_addresses()
|
|
|
|
]
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def create(self):
|
|
|
|
mnemonic = Mnemonic(self.storage.get('lang', 'eng'))
|
|
|
|
seed = mnemonic.make_seed()
|
|
|
|
self.add_seed(seed, None)
|
|
|
|
self.add_xprv_from_seed(seed, self.root_name, None)
|
|
|
|
account = Account(
|
|
|
|
{'xpub': self.master_public_keys.get("x/")},
|
|
|
|
self.gap_limit,
|
|
|
|
self.gap_limit_for_change,
|
|
|
|
self.is_address_old
|
|
|
|
)
|
|
|
|
self.add_account('0', account)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def add_seed(self, seed, password):
|
|
|
|
if self.seed:
|
|
|
|
raise Exception("a seed exists")
|
|
|
|
self.seed_version, self.seed = self.format_seed(seed)
|
|
|
|
if password:
|
|
|
|
self.seed = pw_encode(self.seed, password)
|
|
|
|
self.storage.put('seed', self.seed)
|
|
|
|
self.storage.put('seed_version', self.seed_version)
|
|
|
|
self.set_use_encryption(password is not None)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@staticmethod
|
|
|
|
def format_seed(seed):
|
|
|
|
return NEW_SEED_VERSION, ' '.join(seed.split())
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def add_xprv_from_seed(self, seed, name, password, passphrase=''):
|
|
|
|
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, passphrase))
|
|
|
|
xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
|
|
|
|
self.add_master_public_key(name, xpub)
|
|
|
|
self.add_master_private_key(name, xprv, password)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def add_master_public_key(self, name, xpub):
|
|
|
|
if xpub in self.master_public_keys.values():
|
|
|
|
raise BaseException('Duplicate master public key')
|
|
|
|
self.master_public_keys[name] = xpub
|
|
|
|
self.storage.put('master_public_keys', self.master_public_keys)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def add_master_private_key(self, name, xpriv, password):
|
|
|
|
self.master_private_keys[name] = pw_encode(xpriv, password)
|
|
|
|
self.storage.put('master_private_keys', self.master_private_keys)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
def add_account(self, account_id, account):
|
|
|
|
self.accounts[account_id] = account
|
|
|
|
self.save_accounts()
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def set_use_encryption(self, use_encryption):
|
|
|
|
self.use_encryption = use_encryption
|
|
|
|
self.storage.put('use_encryption', use_encryption)
|
|
|
|
|
2018-03-26 04:59:57 +02:00
|
|
|
def save_accounts(self):
|
|
|
|
d = {}
|
|
|
|
for k, v in self.accounts.items():
|
2018-03-27 08:40:44 +02:00
|
|
|
d[k] = v.as_dict()
|
2018-03-26 04:59:57 +02:00
|
|
|
self.storage.put('accounts', d)
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
def is_address_old(self, address, age_limit=2):
|
2018-03-26 04:59:57 +02:00
|
|
|
age = -1
|
2018-03-27 08:40:44 +02:00
|
|
|
for tx in self.history.get_transactions(address, []):
|
|
|
|
if tx.height == 0:
|
2018-03-26 04:59:57 +02:00
|
|
|
tx_age = 0
|
|
|
|
else:
|
2018-03-27 08:40:44 +02:00
|
|
|
tx_age = self.headers.height - tx.height + 1
|
2018-03-26 04:59:57 +02:00
|
|
|
if tx_age > age:
|
|
|
|
age = tx_age
|
|
|
|
return age > age_limit
|