import asyncio import logging from binascii import unhexlify from functools import partial from typing import Tuple, List from datetime import datetime import pylru from lbry.wallet.client.baseledger import BaseLedger, TransactionEvent from lbry.wallet.client.baseaccount import SingleKey from lbry.schema.result import Outputs from lbry.schema.url import URL from lbry.wallet.dewies import dewies_to_lbc from lbry.wallet.account import Account from lbry.wallet.network import Network from lbry.wallet.database import WalletDatabase from lbry.wallet.transaction import Transaction, Output from lbry.wallet.header import Headers, UnvalidatedHeaders from lbry.wallet.constants import TXO_TYPES log = logging.getLogger(__name__) class MainNetLedger(BaseLedger): name = 'LBRY Credits' symbol = 'LBC' network_name = 'mainnet' headers: Headers account_class = Account database_class = WalletDatabase headers_class = Headers network_class = Network transaction_class = Transaction db: WalletDatabase secret_prefix = bytes((0x1c,)) pubkey_address_prefix = bytes((0x55,)) script_address_prefix = bytes((0x7a,)) extended_public_key_prefix = unhexlify('0488b21e') extended_private_key_prefix = unhexlify('0488ade4') max_target = 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff genesis_hash = '9c89283ba0f3227f6c03b70216b9f665f0118d5e0fa729cedf4fb34d6a34f463' genesis_bits = 0x1f00ffff target_timespan = 150 default_fee_per_byte = 50 default_fee_per_name_char = 200000 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fee_per_name_char = self.config.get('fee_per_name_char', self.default_fee_per_name_char) self._balance_cache = pylru.lrucache(100000) async def _inflate_outputs(self, query, accounts): outputs = Outputs.from_base64(await query) txs = [] if len(outputs.txs) > 0: txs: List[Transaction] = await asyncio.gather(*( self.cache_transaction(*tx) for tx in outputs.txs )) if accounts: priced_claims = [] for tx in txs: for txo in tx.outputs: if txo.has_price: priced_claims.append(txo) if priced_claims: receipts = { txo.purchased_claim_id: txo for txo in await self.db.get_purchases( accounts=accounts, purchased_claim_id__in=[c.claim_id for c in priced_claims] ) } for txo in priced_claims: txo.purchase_receipt = receipts.get(txo.claim_id) return outputs.inflate(txs), outputs.offset, outputs.total async def resolve(self, accounts, urls): resolve = partial(self.network.retriable_call, self.network.resolve) txos = (await self._inflate_outputs(resolve(urls), accounts))[0] assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received." result = {} for url, txo in zip(urls, txos): if txo and URL.parse(url).has_stream_in_channel: if not txo.channel or not txo.is_signed_by(txo.channel, self): txo = None if txo: result[url] = txo else: result[url] = {'error': f'{url} did not resolve to a claim'} return result async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], int, int]: return await self._inflate_outputs(self.network.claim_search(**kwargs), accounts) async def get_claim_by_claim_id(self, accounts, claim_id) -> Output: for claim in (await self.claim_search(accounts, claim_id=claim_id))[0]: return claim async def start(self): await super().start() await asyncio.gather(*(a.maybe_migrate_certificates() for a in self.accounts)) await asyncio.gather(*(a.save_max_gap() for a in self.accounts)) if len(self.accounts) > 10: log.info("Loaded %i accounts", len(self.accounts)) else: await self._report_state() self.on_transaction.listen(self._reset_balance_cache) async def _report_state(self): try: for account in self.accounts: balance = dewies_to_lbc(await account.get_balance(include_claims=True)) channel_count = await account.get_channel_count() claim_count = await account.get_claim_count() if isinstance(account.receiving, SingleKey): log.info("Loaded single key account %s with %s LBC. " "%d channels, %d certificates and %d claims", account.id, balance, channel_count, len(account.channel_keys), claim_count) else: total_receiving = len(await account.receiving.get_addresses()) total_change = len(await account.change.get_addresses()) log.info("Loaded account %s with %s LBC, %d receiving addresses (gap: %d), " "%d change addresses (gap: %d), %d channels, %d certificates and %d claims. ", account.id, balance, total_receiving, account.receiving.gap, total_change, account.change.gap, channel_count, len(account.channel_keys), claim_count) except: log.exception( 'Failed to display wallet state, please file issue ' 'for this bug along with the traceback you see below:') async def _reset_balance_cache(self, e: TransactionEvent): account_ids = [ r['account'] for r in await self.db.get_addresses(('account',), address=e.address) ] for account_id in account_ids: if account_id in self._balance_cache: del self._balance_cache[account_id] @staticmethod def constraint_spending_utxos(constraints): constraints['txo_type__in'] = (0, TXO_TYPES['purchase']) def get_utxos(self, **constraints): self.constraint_spending_utxos(constraints) return super().get_utxos(**constraints) def get_utxo_count(self, **constraints): self.constraint_spending_utxos(constraints) return super().get_utxo_count(**constraints) async def get_purchases(self, resolve=False, **constraints): purchases = await self.db.get_purchases(**constraints) if resolve: claim_ids = [p.purchased_claim_id for p in purchases] try: resolved, _, _ = await self.claim_search([], claim_ids=claim_ids) except: log.exception("Resolve failed while looking up purchased claim ids:") resolved = [] lookup = {claim.claim_id: claim for claim in resolved} for purchase in purchases: purchase.purchased_claim = lookup.get(purchase.purchased_claim_id) return purchases def get_purchase_count(self, resolve=False, **constraints): return self.db.get_purchase_count(**constraints) def get_claims(self, **constraints): return self.db.get_claims(**constraints) def get_claim_count(self, **constraints): return self.db.get_claim_count(**constraints) def get_streams(self, **constraints): return self.db.get_streams(**constraints) def get_stream_count(self, **constraints): return self.db.get_stream_count(**constraints) def get_channels(self, **constraints): return self.db.get_channels(**constraints) def get_channel_count(self, **constraints): return self.db.get_channel_count(**constraints) async def resolve_collection(self, collection, offset=0, page_size=1): claim_ids = collection.claim.collection.claims.ids[offset:page_size+offset] try: resolve_results, _, _ = await self.claim_search([], claim_ids=claim_ids) except: log.exception("Resolve failed while looking up collection claim ids:") return [] claims = [] for claim_id in claim_ids: found = False for txo in resolve_results: if txo.claim_id == claim_id: claims.append(txo) found = True break if not found: claims.append(None) return claims async def get_collections(self, resolve_claims=0, **constraints): collections = await self.db.get_collections(**constraints) if resolve_claims > 0: for collection in collections: collection.claims = await self.resolve_collection(collection, page_size=resolve_claims) return collections def get_collection_count(self, resolve_claims=0, **constraints): return self.db.get_collection_count(**constraints) def get_supports(self, **constraints): return self.db.get_supports(**constraints) def get_support_count(self, **constraints): return self.db.get_support_count(**constraints) async def get_transaction_history(self, **constraints): txs: List[Transaction] = await self.db.get_transactions(**constraints) headers = self.headers history = [] for tx in txs: ts = headers[tx.height]['timestamp'] if tx.height > 0 else None item = { 'txid': tx.id, 'timestamp': ts, 'date': datetime.fromtimestamp(ts).isoformat(' ')[:-3] if tx.height > 0 else None, 'confirmations': (headers.height+1) - tx.height if tx.height > 0 else 0, 'claim_info': [], 'update_info': [], 'support_info': [], 'abandon_info': [], 'purchase_info': [] } is_my_inputs = all([txi.is_my_account for txi in tx.inputs]) if is_my_inputs: # fees only matter if we are the ones paying them item['value'] = dewies_to_lbc(tx.net_account_balance+tx.fee) item['fee'] = dewies_to_lbc(-tx.fee) else: # someone else paid the fees item['value'] = dewies_to_lbc(tx.net_account_balance) item['fee'] = '0.0' for txo in tx.my_claim_outputs: item['claim_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(-txo.amount), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.claim_id, 'claim_name': txo.claim_name, 'nout': txo.position }) for txo in tx.my_update_outputs: if is_my_inputs: # updating my own claim previous = None for txi in tx.inputs: if txi.txo_ref.txo is not None: other_txo = txi.txo_ref.txo if (other_txo.is_claim or other_txo.script.is_support_claim) \ and other_txo.claim_id == txo.claim_id: previous = other_txo break if previous is not None: item['update_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(previous.amount-txo.amount), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.claim_id, 'claim_name': txo.claim_name, 'nout': txo.position }) else: # someone sent us their claim item['update_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(0), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.claim_id, 'claim_name': txo.claim_name, 'nout': txo.position }) for txo in tx.my_support_outputs: item['support_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(txo.amount if not is_my_inputs else -txo.amount), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.claim_id, 'claim_name': txo.claim_name, 'is_tip': not is_my_inputs, 'nout': txo.position }) if is_my_inputs: for txo in tx.other_support_outputs: item['support_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(-txo.amount), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.claim_id, 'claim_name': txo.claim_name, 'is_tip': is_my_inputs, 'nout': txo.position }) for txo in tx.my_abandon_outputs: item['abandon_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(txo.amount), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.claim_id, 'claim_name': txo.claim_name, 'nout': txo.position }) for txo in tx.any_purchase_outputs: item['purchase_info'].append({ 'address': txo.get_address(self), 'balance_delta': dewies_to_lbc(txo.amount if not is_my_inputs else -txo.amount), 'amount': dewies_to_lbc(txo.amount), 'claim_id': txo.purchased_claim_id, 'nout': txo.position }) history.append(item) return history def get_transaction_history_count(self, **constraints): return self.db.get_transaction_count(**constraints) async def get_detailed_balance(self, accounts, confirmations=0): result = { 'total': 0, 'available': 0, 'reserved': 0, 'reserved_subtotals': { 'claims': 0, 'supports': 0, 'tips': 0 } } for account in accounts: balance = self._balance_cache.get(account.id) if not balance: balance = self._balance_cache[account.id] =\ await account.get_detailed_balance(confirmations, reserved_subtotals=True) for key, value in balance.items(): if key == 'reserved_subtotals': for subkey, subvalue in value.items(): result['reserved_subtotals'][subkey] += subvalue else: result[key] += value return result class TestNetLedger(MainNetLedger): network_name = 'testnet' pubkey_address_prefix = bytes((111,)) script_address_prefix = bytes((196,)) extended_public_key_prefix = unhexlify('043587cf') extended_private_key_prefix = unhexlify('04358394') class RegTestLedger(MainNetLedger): network_name = 'regtest' headers_class = UnvalidatedHeaders pubkey_address_prefix = bytes((111,)) script_address_prefix = bytes((196,)) extended_public_key_prefix = unhexlify('043587cf') extended_private_key_prefix = unhexlify('04358394') max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff genesis_hash = '6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556' genesis_bits = 0x207fffff target_timespan = 1