lbry-sdk/lbrynet/wallet/manager.py
2018-08-24 11:37:25 -04:00

153 lines
5.5 KiB
Python

import os
import logging
from operator import itemgetter
from twisted.internet import defer
import lbryschema
from .protocol import Network
from .blockchain import BlockchainHeaders, Transaction
from .wallet import Wallet
from .stream import execute_serially
log = logging.getLogger(__name__)
class WalletManager:
def __init__(self, storage, config):
self.storage = storage
self.config = config
lbryschema.BLOCKCHAIN_NAME = config['chain']
self.headers = BlockchainHeaders(self.headers_path, config['chain'])
self.wallet = Wallet(self.wallet_path, self.headers)
self.network = Network(config)
self.network.on_header.listen(self.process_header)
self.network.on_status.listen(self.process_status)
@property
def headers_path(self):
filename = 'blockchain_headers'
if self.config['chain'] != 'lbrycrd_main':
filename = '{}_headers'.format(self.config['chain'].split("_")[1])
return os.path.join(self.config['wallet_path'], filename)
@property
def wallet_path(self):
return os.path.join(self.config['wallet_path'], 'wallets', 'default_wallet')
def get_least_used_receiving_address(self, max_transactions=1000):
return self._get_least_used_address(
self.wallet.receiving_addresses,
self.wallet.default_account.receiving,
max_transactions
)
def get_least_used_change_address(self, max_transactions=100):
return self._get_least_used_address(
self.wallet.change_addresses,
self.wallet.default_account.change,
max_transactions
)
def _get_least_used_address(self, addresses, sequence, max_transactions):
transaction_counts = []
for address in addresses:
transactions = self.wallet.history.get_transactions(address, [])
tx_count = len(transactions)
if tx_count == 0:
return address
elif tx_count >= max_transactions:
continue
else:
transaction_counts.append((address, tx_count))
if transaction_counts:
transaction_counts.sort(key=itemgetter(1))
return transaction_counts[0]
address = sequence.generate_next_address()
self.subscribe_history(address)
return address
@defer.inlineCallbacks
def start(self):
self.network.start()
yield self.network.on_connected.first
yield self.update_headers()
yield self.network.subscribe_headers()
yield self.update_wallet()
def stop(self):
return self.network.stop()
@execute_serially
@defer.inlineCallbacks
def update_headers(self):
while True:
height_sought = len(self.headers)
headers = yield self.network.get_headers(height_sought)
log.info("received {} headers starting at {} height".format(headers['count'], height_sought))
if headers['count'] <= 0:
break
yield self.headers.connect(height_sought, headers['hex'].decode('hex'))
@defer.inlineCallbacks
def process_header(self, response):
header = response[0]
if self.update_headers.is_running:
return
if header['height'] == len(self.headers):
# New header from network directly connects after the last local header.
yield self.headers.connect(len(self.headers), header['hex'].decode('hex'))
elif header['height'] > len(self.headers):
# New header is several heights ahead of local, do download instead.
yield self.update_headers()
@execute_serially
@defer.inlineCallbacks
def update_wallet(self):
if not self.wallet.exists:
self.wallet.create()
# Before subscribing, download history for any addresses that don't have any,
# this avoids situation where we're getting status updates to addresses we know
# need to update anyways. Continue to get history and create more addresses until
# all missing addresses are created and history for them is fully restored.
self.wallet.ensure_enough_addresses()
addresses = list(self.wallet.addresses_without_history)
while addresses:
yield defer.gatherResults([
self.update_history(a) for a in addresses
])
addresses = self.wallet.ensure_enough_addresses()
# By this point all of the addresses should be restored and we
# can now subscribe all of them to receive updates.
yield defer.gatherResults([
self.subscribe_history(address)
for address in self.wallet.addresses
])
@defer.inlineCallbacks
def update_history(self, address):
history = yield self.network.get_history(address)
for hash in map(itemgetter('tx_hash'), history):
transaction = self.wallet.history.get_transaction(hash)
if not transaction:
raw = yield self.network.get_transaction(hash)
transaction = Transaction(hash, raw, None)
self.wallet.history.add_transaction(address, transaction)
@defer.inlineCallbacks
def subscribe_history(self, address):
status = yield self.network.subscribe_address(address)
if status != self.wallet.history.get_status(address):
self.update_history(address)
def process_status(self, response):
address, status = response
if status != self.wallet.history.get_status(address):
self.update_history(address)