2018-03-26 04:59:57 +02:00
|
|
|
import os
|
|
|
|
import logging
|
2018-03-27 08:40:44 +02:00
|
|
|
from operator import itemgetter
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
from twisted.internet import defer
|
|
|
|
|
|
|
|
import lbryschema
|
|
|
|
|
|
|
|
from .protocol import Network
|
2018-03-27 08:40:44 +02:00
|
|
|
from .blockchain import BlockchainHeaders, Transaction
|
2018-03-26 04:59:57 +02:00
|
|
|
from .wallet import Wallet
|
2018-03-27 08:40:44 +02:00
|
|
|
from .stream import execute_serially
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
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'])
|
2018-03-27 08:40:44 +02:00
|
|
|
self.wallet = Wallet(self.wallet_path, self.headers)
|
2018-03-26 04:59:57 +02:00
|
|
|
self.network = Network(config)
|
|
|
|
self.network.on_header.listen(self.process_header)
|
2018-03-27 08:40:44 +02:00
|
|
|
self.network.on_status.listen(self.process_status)
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
@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')
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
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
|
|
|
|
|
2018-03-26 04:59:57 +02:00
|
|
|
@defer.inlineCallbacks
|
|
|
|
def start(self):
|
|
|
|
self.network.start()
|
|
|
|
yield self.network.on_connected.first
|
2018-03-27 08:40:44 +02:00
|
|
|
yield self.update_headers()
|
|
|
|
yield self.network.subscribe_headers()
|
|
|
|
yield self.update_wallet()
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
return self.network.stop()
|
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@execute_serially
|
2018-03-26 04:59:57 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-03-27 08:40:44 +02:00
|
|
|
def update_headers(self):
|
2018-03-26 04:59:57 +02:00
|
|
|
while True:
|
2018-03-27 08:40:44 +02:00
|
|
|
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))
|
2018-03-26 04:59:57 +02:00
|
|
|
if headers['count'] <= 0:
|
|
|
|
break
|
2018-03-27 08:40:44 +02:00
|
|
|
yield self.headers.connect(height_sought, headers['hex'].decode('hex'))
|
2018-03-26 04:59:57 +02:00
|
|
|
|
|
|
|
@defer.inlineCallbacks
|
2018-03-27 08:40:44 +02:00
|
|
|
def process_header(self, response):
|
|
|
|
header = response[0]
|
|
|
|
if self.update_headers.is_running:
|
2018-03-26 04:59:57 +02:00
|
|
|
return
|
2018-03-27 08:40:44 +02:00
|
|
|
if header['height'] == len(self.headers):
|
2018-03-26 04:59:57 +02:00
|
|
|
# New header from network directly connects after the last local header.
|
|
|
|
yield self.headers.connect(len(self.headers), header['hex'].decode('hex'))
|
2018-03-27 08:40:44 +02:00
|
|
|
elif header['height'] > len(self.headers):
|
2018-03-26 04:59:57 +02:00
|
|
|
# New header is several heights ahead of local, do download instead.
|
2018-03-27 08:40:44 +02:00
|
|
|
yield self.update_headers()
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@execute_serially
|
2018-03-26 04:59:57 +02:00
|
|
|
@defer.inlineCallbacks
|
2018-03-27 08:40:44 +02:00
|
|
|
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
|
2018-03-26 04:59:57 +02:00
|
|
|
])
|
2018-03-27 08:40:44 +02:00
|
|
|
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
|
|
|
|
])
|
2018-03-26 04:59:57 +02:00
|
|
|
|
2018-03-27 08:40:44 +02:00
|
|
|
@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)
|