From 98d4d00f962245804f064ba9a35e1475c6b313df Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 12 Aug 2019 00:40:05 -0400 Subject: [PATCH] most commands now work across all accounts --- lbry/lbry/extras/daemon/Daemon.py | 136 +++++++++------ lbry/lbry/wallet/account.py | 30 ++-- lbry/lbry/wallet/database.py | 16 +- lbry/lbry/wallet/ledger.py | 158 +++++++++++++++++- lbry/lbry/wallet/manager.py | 99 ----------- .../integration/test_account_commands.py | 103 ++++++++++++ lbry/tests/integration/test_claim_commands.py | 33 ++-- torba/torba/client/baseaccount.py | 12 +- torba/torba/client/basedatabase.py | 50 +++--- torba/torba/client/baseledger.py | 12 ++ 10 files changed, 426 insertions(+), 223 deletions(-) diff --git a/lbry/lbry/extras/daemon/Daemon.py b/lbry/lbry/extras/daemon/Daemon.py index c81cc7fa1..620da7243 100644 --- a/lbry/lbry/extras/daemon/Daemon.py +++ b/lbry/lbry/extras/daemon/Daemon.py @@ -1703,12 +1703,14 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Paginated[Output]} """ - account = self.get_account_or_default(account_id) - return maybe_paginate( - account.get_claims, - account.get_claim_count, - page, page_size - ) + if account_id: + account = self.get_account_or_error(account_id) + claims = account.get_claims + claim_count = account.get_claim_count + else: + claims = self.ledger.get_claims + claim_count = self.ledger.get_claim_count + return maybe_paginate(claims, claim_count, page, page_size) @requires(WALLET_COMPONENT) async def jsonrpc_claim_search(self, **kwargs): @@ -1859,7 +1861,7 @@ class Daemon(metaclass=JSONRPCServerType): @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_channel_create( self, name, bid, allow_duplicate_name=False, account_id=None, claim_address=None, - preview=False, blocking=False, **kwargs): + funding_account_ids=None, preview=False, blocking=False, **kwargs): """ Create a new channel by generating a channel private key and establishing an '@' prefixed claim. @@ -1871,6 +1873,7 @@ class Daemon(metaclass=JSONRPCServerType): [--tags=...] [--languages=...] [--locations=...] [--thumbnail_url=] [--cover_url=] [--account_id=] [--claim_address=] + [--funding_account_ids=...] [--preview] [--blocking] Options: @@ -1924,7 +1927,8 @@ class Daemon(metaclass=JSONRPCServerType): --thumbnail_url=: (str) thumbnail url --cover_url= : (str) url of cover image - --account_id= : (str) id of the account to store channel + --account_id= : (str) account to use for holding the transaction + --funding_account_ids=: (list) ids of accounts to fund this transaction --claim_address=: (str) address where the channel is sent to, if not specified it will be determined automatically from the account --preview : (bool) do not broadcast the transaction @@ -1933,6 +1937,7 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Transaction} """ account = self.get_account_or_default(account_id) + funding_accounts = self.get_accounts_or_all(funding_account_ids) self.valid_channel_name_or_error(name) amount = self.get_dewies_or_error('bid', bid, positive_value=True) claim_address = await self.get_receiving_address(claim_address, account) @@ -1948,7 +1953,7 @@ class Daemon(metaclass=JSONRPCServerType): claim = Claim() claim.channel.update(**kwargs) tx = await Transaction.claim_create( - name, claim, amount, claim_address, [account], account + name, claim, amount, claim_address, funding_accounts, funding_accounts[0] ) txo = tx.outputs[0] txo.generate_channel_private_key() @@ -1970,7 +1975,8 @@ class Daemon(metaclass=JSONRPCServerType): @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_channel_update( self, claim_id, bid=None, account_id=None, claim_address=None, - new_signing_key=False, preview=False, blocking=False, replace=False, **kwargs): + funding_account_ids=None, new_signing_key=False, preview=False, + blocking=False, replace=False, **kwargs): """ Update an existing channel claim. @@ -1984,6 +1990,7 @@ class Daemon(metaclass=JSONRPCServerType): [--locations=...] [--clear_locations] [--thumbnail_url=] [--cover_url=] [--account_id=] [--claim_address=] [--new_signing_key] + [--funding_account_ids=...] [--preview] [--blocking] [--replace] Options: @@ -2039,7 +2046,8 @@ class Daemon(metaclass=JSONRPCServerType): --clear_locations : (bool) clear existing locations (prior to adding new ones) --thumbnail_url=: (str) thumbnail url --cover_url= : (str) url of cover image - --account_id= : (str) id of the account to store channel + --account_id= : (str) account to use for holding the transaction + --funding_account_ids=: (list) ids of accounts to fund this transaction --claim_address=: (str) address where the channel is sent --new_signing_key : (bool) generate a new signing key, will invalidate all previous publishes --preview : (bool) do not broadcast the transaction @@ -2052,6 +2060,7 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Transaction} """ account = self.get_account_or_default(account_id) + funding_accounts = self.get_accounts_or_all(funding_account_ids) existing_channels = await account.get_claims(claim_id=claim_id) if len(existing_channels) != 1: @@ -2081,7 +2090,7 @@ class Daemon(metaclass=JSONRPCServerType): claim = Claim.from_bytes(old_txo.claim.to_bytes()) claim.channel.update(**kwargs) tx = await Transaction.claim_update( - old_txo, claim, amount, claim_address, [account], account + old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0] ) new_txo = tx.outputs[0] @@ -2169,12 +2178,14 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Paginated[Output]} """ - account = self.get_account_or_default(account_id) - return maybe_paginate( - account.get_channels, - account.get_channel_count, - page, page_size - ) + if account_id: + account = self.get_account_or_error(account_id) + channels = account.get_channels + channel_count = account.get_channel_count + else: + channels = self.ledger.get_channels + channel_count = self.ledger.get_channel_count + return maybe_paginate(channels, channel_count, page, page_size) @requires(WALLET_COMPONENT) async def jsonrpc_channel_export(self, channel_id=None, channel_name=None, account_id=None): @@ -2261,6 +2272,7 @@ class Daemon(metaclass=JSONRPCServerType): [--channel_id= | --channel_name=] [--channel_account_id=...] [--account_id=] [--claim_address=] + [--funding_account_ids=...] [--preview] [--blocking] Options: @@ -2328,7 +2340,8 @@ class Daemon(metaclass=JSONRPCServerType): --channel_name= : (str) name of publisher channel --channel_account_id=: (str) one or more account ids for accounts to look in for channel certificates, defaults to all accounts. - --account_id= : (str) account to use for funding the transaction + --account_id= : (str) account to use for holding the transaction + --funding_account_ids=: (list) ids of accounts to fund this transaction --claim_address=: (str) address where the claim is sent to, if not specified it will be determined automatically from the account --preview : (bool) do not broadcast the transaction @@ -2359,8 +2372,8 @@ class Daemon(metaclass=JSONRPCServerType): async def jsonrpc_stream_create( self, name, bid, file_path, allow_duplicate_name=False, channel_id=None, channel_name=None, channel_account_id=None, - account_id=None, claim_address=None, preview=False, blocking=False, - **kwargs): + account_id=None, claim_address=None, funding_account_ids=None, + preview=False, blocking=False, **kwargs): """ Make a new stream claim and announce the associated file to lbrynet. @@ -2375,6 +2388,7 @@ class Daemon(metaclass=JSONRPCServerType): [--channel_id= | --channel_name=] [--channel_account_id=...] [--account_id=] [--claim_address=] + [--funding_account_ids=...] [--preview] [--blocking] Options: @@ -2444,7 +2458,8 @@ class Daemon(metaclass=JSONRPCServerType): --channel_name= : (str) name of the publisher channel --channel_account_id=: (str) one or more account ids for accounts to look in for channel certificates, defaults to all accounts. - --account_id= : (str) account to use for funding the transaction + --account_id= : (str) account to use for holding the transaction + --funding_account_ids=: (list) ids of accounts to fund this transaction --claim_address=: (str) address where the claim is sent to, if not specified it will be determined automatically from the account --preview : (bool) do not broadcast the transaction @@ -2454,6 +2469,7 @@ class Daemon(metaclass=JSONRPCServerType): """ self.valid_stream_name_or_error(name) account = self.get_account_or_default(account_id) + funding_accounts = self.get_accounts_or_all(funding_account_ids) channel = await self.get_channel_or_none(channel_account_id, channel_id, channel_name, for_signing=True) amount = self.get_dewies_or_error('bid', bid, positive_value=True) claim_address = await self.get_receiving_address(claim_address, account) @@ -2470,7 +2486,7 @@ class Daemon(metaclass=JSONRPCServerType): claim = Claim() claim.stream.update(file_path=file_path, sd_hash='0'*96, **kwargs) tx = await Transaction.claim_create( - name, claim, amount, claim_address, [account], account, channel + name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) new_txo = tx.outputs[0] @@ -2501,7 +2517,7 @@ class Daemon(metaclass=JSONRPCServerType): async def jsonrpc_stream_update( self, claim_id, bid=None, file_path=None, channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False, - account_id=None, claim_address=None, + account_id=None, claim_address=None, funding_account_ids=None, preview=False, blocking=False, replace=False, **kwargs): """ Update an existing stream claim and if a new file is provided announce it to lbrynet. @@ -2520,6 +2536,7 @@ class Daemon(metaclass=JSONRPCServerType): [--channel_id= | --channel_name= | --clear_channel] [--channel_account_id=...] [--account_id=] [--claim_address=] + [--funding_account_ids=...] [--preview] [--blocking] [--replace] Options: @@ -2595,7 +2612,8 @@ class Daemon(metaclass=JSONRPCServerType): --clear_channel : (bool) remove channel signature --channel_account_id=: (str) one or more account ids for accounts to look in for channel certificates, defaults to all accounts. - --account_id= : (str) account to use for funding the transaction + --account_id= : (str) account to use for holding the transaction + --funding_account_ids=: (list) ids of accounts to fund this transaction --claim_address=: (str) address where the claim is sent to, if not specified it will be determined automatically from the account --preview : (bool) do not broadcast the transaction @@ -2608,6 +2626,7 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Transaction} """ account = self.get_account_or_default(account_id) + funding_accounts = self.get_accounts_or_all(funding_account_ids) existing_claims = await account.get_claims(claim_id=claim_id) if len(existing_claims) != 1: @@ -2655,7 +2674,7 @@ class Daemon(metaclass=JSONRPCServerType): claim = Claim.from_bytes(old_txo.claim.to_bytes()) claim.stream.update(file_path=file_path, **kwargs) tx = await Transaction.claim_update( - old_txo, claim, amount, claim_address, [account], account, channel + old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) new_txo = tx.outputs[0] @@ -2753,12 +2772,14 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Paginated[Output]} """ - account = self.get_account_or_default(account_id) - return maybe_paginate( - account.get_streams, - account.get_stream_count, - page, page_size - ) + if account_id: + account = self.get_account_or_error(account_id) + streams = account.get_streams + stream_count = account.get_stream_count + else: + streams = self.ledger.get_streams + stream_count = self.ledger.get_stream_count + return maybe_paginate(streams, stream_count, page, page_size) @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, DHT_COMPONENT, DATABASE_COMPONENT, @@ -2785,7 +2806,7 @@ class Daemon(metaclass=JSONRPCServerType): @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_support_create( - self, claim_id, amount, tip=False, account_id=None, + self, claim_id, amount, tip=False, account_id=None, funding_account_ids=None, preview=False, blocking=False): """ Create a support or a tip for name claim. @@ -2793,18 +2814,21 @@ class Daemon(metaclass=JSONRPCServerType): Usage: support_create ( | --claim_id=) ( | --amount=) [--tip] [--account_id=] [--preview] [--blocking] + [--funding_account_ids=...] Options: --claim_id= : (str) claim_id of the claim to support --amount= : (decimal) amount of support --tip : (bool) send support to claim owner, default: false. - --account_id= : (str) id of the account to use + --account_id= : (str) account to use for holding the transaction + --funding_account_ids=: (list) ids of accounts to fund this transaction --preview : (bool) do not broadcast the transaction --blocking : (bool) wait until transaction is in mempool Returns: {Transaction} """ account = self.get_account_or_default(account_id) + funding_accounts = self.get_accounts_or_all(funding_account_ids) amount = self.get_dewies_or_error("amount", amount) claim = await self.ledger.get_claim_by_claim_id(claim_id) claim_address = claim.get_address(self.ledger) @@ -2812,7 +2836,7 @@ class Daemon(metaclass=JSONRPCServerType): claim_address = await account.receiving.get_or_create_usable_address() tx = await Transaction.support( - claim.claim_name, claim_id, amount, claim_address, [account], account + claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0] ) if not preview: @@ -2847,12 +2871,14 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Paginated[Output]} """ - account = self.get_account_or_default(account_id) - return maybe_paginate( - account.get_supports, - account.get_support_count, - page, page_size - ) + if account_id: + account = self.get_account_or_error(account_id) + supports = account.get_supports + support_count = account.get_support_count + else: + supports = self.ledger.get_supports + support_count = self.ledger.get_support_count + return maybe_paginate(supports, support_count, page, page_size) @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_support_abandon( @@ -2978,12 +3004,14 @@ class Daemon(metaclass=JSONRPCServerType): } """ - account = self.get_account_or_default(account_id) - return maybe_paginate( - self.wallet_manager.get_history, - self.ledger.db.get_transaction_count, - page, page_size, account=account - ) + if account_id: + account = self.get_account_or_error(account_id) + transactions = account.get_transaction_history + transaction_count = account.get_transaction_history_count + else: + transactions = self.ledger.get_transaction_history + transaction_count = self.ledger.get_transaction_history_count + return maybe_paginate(transactions, transaction_count, page, page_size) @requires(WALLET_COMPONENT) def jsonrpc_transaction_show(self, txid): @@ -3020,12 +3048,14 @@ class Daemon(metaclass=JSONRPCServerType): Returns: {Paginated[Output]} """ - account = self.get_account_or_default(account_id) - return maybe_paginate( - account.get_utxos, - account.get_utxo_count, - page, page_size - ) + if account_id: + account = self.get_account_or_error(account_id) + utxos = account.get_utxos + utxo_count = account.get_utxo_count + else: + utxos = self.ledger.get_utxos + utxo_count = self.ledger.get_utxo_count + return maybe_paginate(utxos, utxo_count, page, page_size) @requires(WALLET_COMPONENT) def jsonrpc_utxo_release(self, account_id=None): diff --git a/lbry/lbry/wallet/account.py b/lbry/lbry/wallet/account.py index 2e503859f..da066bd13 100644 --- a/lbry/lbry/wallet/account.py +++ b/lbry/lbry/wallet/account.py @@ -131,41 +131,35 @@ class Account(BaseAccount): details['certificates'] = len(self.channel_keys) return details - @staticmethod - def constraint_spending_utxos(constraints): - constraints.update({'is_claim': 0, 'is_update': 0, 'is_support': 0}) + def get_transaction_history(self, **constraints): + return self.ledger.get_transaction_history(account=self, **constraints) - 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) + def get_transaction_history_count(self, **constraints): + return self.ledger.get_transaction_history_count(account=self, **constraints) def get_claims(self, **constraints): - return self.ledger.db.get_claims(account=self, **constraints) + return self.ledger.get_claims(account=self, **constraints) def get_claim_count(self, **constraints): - return self.ledger.db.get_claim_count(account=self, **constraints) + return self.ledger.get_claim_count(account=self, **constraints) def get_streams(self, **constraints): - return self.ledger.db.get_streams(account=self, **constraints) + return self.ledger.get_streams(account=self, **constraints) def get_stream_count(self, **constraints): - return self.ledger.db.get_stream_count(account=self, **constraints) + return self.ledger.get_stream_count(account=self, **constraints) def get_channels(self, **constraints): - return self.ledger.db.get_channels(account=self, **constraints) + return self.ledger.get_channels(account=self, **constraints) def get_channel_count(self, **constraints): - return self.ledger.db.get_channel_count(account=self, **constraints) + return self.ledger.get_channel_count(account=self, **constraints) def get_supports(self, **constraints): - return self.ledger.db.get_supports(account=self, **constraints) + return self.ledger.get_supports(account=self, **constraints) def get_support_count(self, **constraints): - return self.ledger.db.get_support_count(account=self, **constraints) + return self.ledger.get_support_count(account=self, **constraints) def get_support_summary(self): return self.ledger.db.get_supports_summary(account_id=self.id) diff --git a/lbry/lbry/wallet/database.py b/lbry/lbry/wallet/database.py index 24253aaa4..de720d5f4 100644 --- a/lbry/lbry/wallet/database.py +++ b/lbry/lbry/wallet/database.py @@ -53,7 +53,7 @@ class WalletDatabase(BaseDatabase): return row async def get_txos(self, **constraints) -> List[Output]: - my_account = constraints.get('my_account', constraints.get('account', None)) + my_accounts = constraints.get('my_accounts', constraints.get('accounts', [])) txos = await super().get_txos(**constraints) @@ -62,16 +62,20 @@ class WalletDatabase(BaseDatabase): if txo.is_claim and txo.can_decode_claim: if txo.claim.is_signed: channel_ids.add(txo.claim.signing_channel_id) - if txo.claim.is_channel and my_account is not None: - txo.private_key = my_account.get_channel_private_key( - txo.claim.channel.public_key_bytes - ) + if txo.claim.is_channel and my_accounts: + for account in my_accounts: + private_key = account.get_channel_private_key( + txo.claim.channel.public_key_bytes + ) + if private_key: + txo.private_key = private_key + break if channel_ids: channels = { txo.claim_id: txo for txo in (await self.get_claims( - my_account=my_account, + my_accounts=my_accounts, claim_id__in=channel_ids )) } diff --git a/lbry/lbry/wallet/ledger.py b/lbry/lbry/wallet/ledger.py index 6a964c15d..4c3537452 100644 --- a/lbry/lbry/wallet/ledger.py +++ b/lbry/lbry/wallet/ledger.py @@ -1,7 +1,8 @@ import asyncio import logging from binascii import unhexlify -from typing import Tuple, List, Dict +from typing import Tuple, List +from datetime import datetime from torba.client.baseledger import BaseLedger from torba.client.baseaccount import SingleKey @@ -74,10 +75,10 @@ class MainNetLedger(BaseLedger): result[url] = {'error': f'{url} did not resolve to a claim'} return result - async def claim_search(self, **kwargs) -> Tuple[List, int, int]: + async def claim_search(self, **kwargs) -> Tuple[List[Output], int, int]: return await self._inflate_outputs(self.network.claim_search(**kwargs)) - async def get_claim_by_claim_id(self, claim_id) -> Dict[str, Output]: + async def get_claim_by_claim_id(self, claim_id) -> Output: for claim in (await self.claim_search(claim_id=claim_id))[0]: return claim @@ -109,6 +110,157 @@ class MainNetLedger(BaseLedger): 'Failed to display wallet state, please file issue ' 'for this bug along with the traceback you see below:') + def constraint_account_or_all(self, constraints): + account = constraints.pop('account', None) + if account: + constraints['accounts'] = [account] + else: + constraints['accounts'] = self.accounts + + def constraint_spending_utxos(self, constraints): + self.constraint_account_or_all(constraints) + constraints.update({'is_claim': 0, 'is_update': 0, 'is_support': 0}) + + 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) + + def get_claims(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_claims(**constraints) + + def get_claim_count(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_claim_count(**constraints) + + def get_streams(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_streams(**constraints) + + def get_stream_count(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_stream_count(**constraints) + + def get_channels(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_channels(**constraints) + + def get_channel_count(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_channel_count(**constraints) + + def get_supports(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_supports(**constraints) + + def get_support_count(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_support_count(**constraints) + + async def get_transaction_history(self, **constraints): + self.constraint_account_or_all(constraints) + txs = 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': [] + } + 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 + }) + history.append(item) + return history + + def get_transaction_history_count(self, **constraints): + self.constraint_account_or_all(constraints) + return self.db.get_transaction_count(**constraints) + class TestNetLedger(MainNetLedger): network_name = 'testnet' diff --git a/lbry/lbry/wallet/manager.py b/lbry/lbry/wallet/manager.py index 5a45b7021..5ac58994b 100644 --- a/lbry/lbry/wallet/manager.py +++ b/lbry/lbry/wallet/manager.py @@ -3,16 +3,13 @@ import json import logging from binascii import unhexlify -from datetime import datetime from torba.client.basemanager import BaseWalletManager from torba.rpc.jsonrpc import CodeMessageError from lbry.wallet.ledger import MainNetLedger -from lbry.wallet.account import BaseAccount from lbry.wallet.transaction import Transaction from lbry.wallet.database import WalletDatabase -from lbry.wallet.dewies import dewies_to_lbc from lbry.conf import Config @@ -209,102 +206,6 @@ class LbryWalletManager(BaseWalletManager): await self.ledger.maybe_verify_transaction(tx, height) return tx - @staticmethod - async def get_history(account: BaseAccount, **constraints): - headers = account.ledger.headers - txs = await account.get_transactions(**constraints) - 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': [] - } - 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(account.ledger), - '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(account.ledger), - '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(account.ledger), - '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(account.ledger), - '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(account.ledger), - '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(account.ledger), - '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 - }) - history.append(item) - return history - def save(self): for wallet in self.wallets: wallet.save() diff --git a/lbry/tests/integration/test_account_commands.py b/lbry/tests/integration/test_account_commands.py index 25dccf923..9d18c2586 100644 --- a/lbry/tests/integration/test_account_commands.py +++ b/lbry/tests/integration/test_account_commands.py @@ -1,4 +1,9 @@ from lbry.testcase import CommandTestCase +from lbry.wallet.dewies import dewies_to_lbc + + +def extract(d, keys): + return dict((k, d[k]) for k in keys) class AccountManagement(CommandTestCase): @@ -65,3 +70,101 @@ class AccountManagement(CommandTestCase): self.account.channel_keys[keys[1]] = "some invalid junk" await self.account.maybe_migrate_certificates() self.assertEqual(list(self.account.channel_keys.keys()), [keys[2]]) + + async def assertFindsClaims(self, claim_names, awaitable): + self.assertEqual(claim_names, [txo.claim_name for txo in await awaitable]) + + async def assertOutputAmount(self, amounts, awaitable): + self.assertEqual(amounts, [dewies_to_lbc(txo.amount) for txo in await awaitable]) + + async def test_commands_across_accounts(self): + channel_list = self.daemon.jsonrpc_channel_list + stream_list = self.daemon.jsonrpc_stream_list + support_list = self.daemon.jsonrpc_support_list + utxo_list = self.daemon.jsonrpc_utxo_list + default_account = self.daemon.default_account + second_account = await self.daemon.jsonrpc_account_create('second account') + + tx = await self.daemon.jsonrpc_account_send( + '0.05', await self.daemon.jsonrpc_address_unused(account_id=second_account.id) + ) + await self.confirm_tx(tx.id) + await self.assertOutputAmount(['0.05', '9.949876'], utxo_list()) + await self.assertOutputAmount(['0.05'], utxo_list(account_id=second_account.id)) + await self.assertOutputAmount(['9.949876'], utxo_list(account_id=default_account.id)) + + channel1 = await self.channel_create('@channel-in-account1', '0.01') + channel2 = await self.channel_create( + '@channel-in-account2', '0.01', account_id=second_account.id, funding_account_ids=[default_account.id] + ) + + await self.assertFindsClaims(['@channel-in-account2', '@channel-in-account1'], channel_list()) + await self.assertFindsClaims(['@channel-in-account1'], channel_list(account_id=default_account.id)) + await self.assertFindsClaims(['@channel-in-account2'], channel_list(account_id=second_account.id)) + + stream1 = await self.stream_create('stream-in-account1', '0.01', channel_id=self.get_claim_id(channel1)) + stream2 = await self.stream_create( + 'stream-in-account2', '0.01', channel_id=self.get_claim_id(channel2), + account_id=second_account.id, funding_account_ids=[default_account.id] + ) + await self.assertFindsClaims(['stream-in-account2', 'stream-in-account1'], stream_list()) + await self.assertFindsClaims(['stream-in-account1'], stream_list(account_id=default_account.id)) + await self.assertFindsClaims(['stream-in-account2'], stream_list(account_id=second_account.id)) + + await self.assertFindsClaims( + ['stream-in-account2', 'stream-in-account1', '@channel-in-account2', '@channel-in-account1'], + self.daemon.jsonrpc_claim_list() + ) + await self.assertFindsClaims( + ['stream-in-account1', '@channel-in-account1'], + self.daemon.jsonrpc_claim_list(account_id=default_account.id) + ) + await self.assertFindsClaims( + ['stream-in-account2', '@channel-in-account2'], + self.daemon.jsonrpc_claim_list(account_id=second_account.id) + ) + + support1 = await self.support_create(self.get_claim_id(stream1), '0.01') + support2 = await self.support_create( + self.get_claim_id(stream2), '0.01', account_id=second_account.id, funding_account_ids=[default_account.id] + ) + self.assertEqual([support2['txid'], support1['txid']], [txo.tx_ref.id for txo in await support_list()]) + self.assertEqual([support1['txid']], [txo.tx_ref.id for txo in await support_list(account_id=default_account.id)]) + self.assertEqual([support2['txid']], [txo.tx_ref.id for txo in await support_list(account_id=second_account.id)]) + + history = await self.daemon.jsonrpc_transaction_list() + self.assertEqual(len(history), 8) + self.assertEqual(extract(history[0]['support_info'][0], ['claim_name', 'is_tip', 'amount', 'balance_delta']), { + 'claim_name': 'stream-in-account2', + 'is_tip': False, + 'amount': '0.01', + 'balance_delta': '-0.01' + }) + self.assertEqual(extract(history[1]['support_info'][0], ['claim_name', 'is_tip', 'amount', 'balance_delta']), { + 'claim_name': 'stream-in-account1', + 'is_tip': False, + 'amount': '0.01', + 'balance_delta': '-0.01' + }) + self.assertEqual(extract(history[2]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), { + 'claim_name': 'stream-in-account2', + 'amount': '0.01', + 'balance_delta': '-0.01' + }) + self.assertEqual(extract(history[3]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), { + 'claim_name': 'stream-in-account1', + 'amount': '0.01', + 'balance_delta': '-0.01' + }) + self.assertEqual(extract(history[4]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), { + 'claim_name': '@channel-in-account2', + 'amount': '0.01', + 'balance_delta': '-0.01' + }) + self.assertEqual(extract(history[5]['claim_info'][0], ['claim_name', 'amount', 'balance_delta']), { + 'claim_name': '@channel-in-account1', + 'amount': '0.01', + 'balance_delta': '-0.01' + }) + self.assertEqual(history[6]['value'], '0.0') + self.assertEqual(history[7]['value'], '10.0') diff --git a/lbry/tests/integration/test_claim_commands.py b/lbry/tests/integration/test_claim_commands.py index 25f23c498..3ca2a2710 100644 --- a/lbry/tests/integration/test_claim_commands.py +++ b/lbry/tests/integration/test_claim_commands.py @@ -478,7 +478,8 @@ class ChannelCommands(CommandTestCase): tx = await self.out(self.channel_update(claim_id, claim_address=other_address)) # after sending - self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 3) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=self.account.id)), 2) self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1) # shoud not have private key @@ -618,12 +619,17 @@ class StreamCommands(ClaimTestCase): channels = await self.out(self.daemon.jsonrpc_channel_list(account1_id)) self.assertEqual(len(channels), 1) self.assertEqual(channels[0]['name'], '@spam') - self.assertEqual(channels, await self.out(self.daemon.jsonrpc_channel_list())) + self.assertEqual(channels, await self.out(self.daemon.jsonrpc_channel_list(account1_id))) channels = await self.out(self.daemon.jsonrpc_channel_list(account2_id)) self.assertEqual(len(channels), 1) self.assertEqual(channels[0]['name'], '@baz') + channels = await self.out(self.daemon.jsonrpc_channel_list()) + self.assertEqual(len(channels), 2) + self.assertEqual(channels[0]['name'], '@baz') + self.assertEqual(channels[1]['name'], '@spam') + # defaults to using all accounts to lookup channel await self.stream_create('hovercraft1', '0.1', channel_id=baz_id) self.assertEqual((await self.claim_search(name='hovercraft1'))[0]['signing_channel']['name'], '@baz') @@ -806,13 +812,15 @@ class StreamCommands(ClaimTestCase): # before sending self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 4) + self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=self.account.id)), 4) self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=account2_id)), 0) other_address = await account2.receiving.get_or_create_usable_address() tx = await self.out(self.stream_update(claim_id, claim_address=other_address)) # after sending - self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 3) + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 4) + self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=self.account.id)), 3) self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=account2_id)), 1) async def test_setting_fee_fields(self): @@ -1112,8 +1120,7 @@ class SupportCommands(CommandTestCase): await self.assertBalance(account2, '5.0') # create the claim we'll be tipping and supporting - tx = await self.stream_create() - claim_id = self.get_claim_id(tx) + claim_id = self.get_claim_id(await self.stream_create()) # account1 and account2 balances: await self.assertBalance(self.account, '3.979769') @@ -1121,18 +1128,17 @@ class SupportCommands(CommandTestCase): # send a tip to the claim using account2 tip = await self.out( - self.daemon.jsonrpc_support_create(claim_id, '1.0', True, account2_id) + self.daemon.jsonrpc_support_create( + claim_id, '1.0', True, account2_id, funding_account_ids=[account2_id]) ) - await self.on_transaction_dict(tip) - await self.generate(1) - await self.on_transaction_dict(tip) + await self.confirm_tx(tip['txid']) # tips don't affect balance so account1 balance is same but account2 balance went down await self.assertBalance(self.account, '3.979769') await self.assertBalance(account2, '3.9998585') # verify that the incoming tip is marked correctly as is_tip=True in account1 - txs = await self.out(self.daemon.jsonrpc_transaction_list()) + txs = await self.out(self.daemon.jsonrpc_transaction_list(self.account.id)) self.assertEqual(len(txs[0]['support_info']), 1) self.assertEqual(txs[0]['support_info'][0]['balance_delta'], '1.0') self.assertEqual(txs[0]['support_info'][0]['claim_id'], claim_id) @@ -1153,11 +1159,10 @@ class SupportCommands(CommandTestCase): # send a support to the claim using account2 support = await self.out( - self.daemon.jsonrpc_support_create(claim_id, '2.0', False, account2_id) + self.daemon.jsonrpc_support_create( + claim_id, '2.0', False, account2_id, funding_account_ids=[account2_id]) ) - await self.on_transaction_dict(support) - await self.generate(1) - await self.on_transaction_dict(support) + await self.confirm_tx(support['txid']) # account2 balance went down ~2 await self.assertBalance(self.account, '3.979769') diff --git a/torba/torba/client/baseaccount.py b/torba/torba/client/baseaccount.py index 391973650..9b8328117 100644 --- a/torba/torba/client/baseaccount.py +++ b/torba/torba/client/baseaccount.py @@ -50,7 +50,7 @@ class AddressManager: def _query_addresses(self, **constraints): return self.account.ledger.db.get_addresses( - account=self.account, + accounts=[self.account], chain=self.chain_number, **constraints ) @@ -406,7 +406,7 @@ class BaseAccount: if confirmations > 0: height = self.ledger.headers.height - (confirmations-1) constraints.update({'height__lte': height, 'height__gt': 0}) - return self.ledger.db.get_balance(account=self, **constraints) + return self.ledger.db.get_balance(accounts=[self], **constraints) async def get_max_gap(self): change_gap = await self.change.get_max_gap() @@ -417,16 +417,16 @@ class BaseAccount: } def get_utxos(self, **constraints): - return self.ledger.db.get_utxos(account=self, **constraints) + return self.ledger.get_utxos(account=self, **constraints) def get_utxo_count(self, **constraints): - return self.ledger.db.get_utxo_count(account=self, **constraints) + return self.ledger.get_utxo_count(account=self, **constraints) def get_transactions(self, **constraints): - return self.ledger.db.get_transactions(account=self, **constraints) + return self.ledger.get_transactions(account=self, **constraints) def get_transaction_count(self, **constraints): - return self.ledger.db.get_transaction_count(account=self, **constraints) + return self.ledger.get_transaction_count(account=self, **constraints) async def fund(self, to_account, amount=None, everything=False, outputs=1, broadcast=False, **constraints): diff --git a/torba/torba/client/basedatabase.py b/torba/torba/client/basedatabase.py index f04ce10d0..ec48b6237 100644 --- a/torba/torba/client/basedatabase.py +++ b/torba/torba/client/basedatabase.py @@ -160,14 +160,10 @@ def query(select, **constraints): offset = constraints.pop('offset', None) order_by = constraints.pop('order_by', None) - constraints.pop('my_account', None) - account = constraints.pop('account', None) - if account is not None: - if not isinstance(account, list): - account = [account] - constraints['account__in'] = [ - (a.public_key.address if isinstance(a, BaseAccount) else a) for a in account - ] + constraints.pop('my_accounts', None) + accounts = constraints.pop('accounts', None) + if accounts is not None: + constraints['account__in'] = [a.public_key.address for a in accounts] where, values = constraints_to_sql(constraints) if where: @@ -395,22 +391,26 @@ class BaseDatabase(SQLiteMixin): # 2. update address histories removing deleted TXs return True - async def select_transactions(self, cols, account=None, **constraints): - if 'txid' not in constraints and account is not None: - constraints['$account'] = account.public_key.address - constraints['txid__in'] = """ + async def select_transactions(self, cols, accounts=None, **constraints): + if 'txid' not in constraints: + assert accounts is not None, "'accounts' argument required when no 'txid' constraint" + constraints.update({ + f'$account{i}': a.public_key.address for i, a in enumerate(accounts) + }) + account_values = ', '.join([f':$account{i}' for i in range(len(accounts))]) + constraints['txid__in'] = f""" SELECT txo.txid FROM txo - JOIN pubkey_address USING (address) WHERE pubkey_address.account = :$account + INNER JOIN pubkey_address USING (address) WHERE pubkey_address.account IN ({account_values}) UNION SELECT txi.txid FROM txi - JOIN pubkey_address USING (address) WHERE pubkey_address.account = :$account + INNER JOIN pubkey_address USING (address) WHERE pubkey_address.account IN ({account_values}) """ return await self.db.execute_fetchall( *query("SELECT {} FROM tx".format(cols), **constraints) ) - async def get_transactions(self, my_account=None, **constraints): - my_account = my_account or constraints.get('account', None) + async def get_transactions(self, **constraints): + accounts = constraints.get('accounts', None) tx_rows = await self.select_transactions( 'txid, raw, height, position, is_verified', @@ -436,7 +436,7 @@ class BaseDatabase(SQLiteMixin): annotated_txos.update({ txo.id: txo for txo in (await self.get_txos( - my_account=my_account, + my_accounts=accounts, txid__in=txids[offset:offset+step], )) }) @@ -446,7 +446,7 @@ class BaseDatabase(SQLiteMixin): referenced_txos.update({ txo.id: txo for txo in (await self.get_txos( - my_account=my_account, + my_accounts=accounts, txoid__in=txi_txoids[offset:offset+step], )) }) @@ -484,12 +484,14 @@ class BaseDatabase(SQLiteMixin): " JOIN tx USING (txid)".format(cols), **constraints )) - async def get_txos(self, my_account=None, no_tx=False, **constraints): - my_account = my_account or constraints.get('account', None) - if isinstance(my_account, BaseAccount): - my_account = my_account.public_key.address + async def get_txos(self, my_accounts=None, no_tx=False, **constraints): + my_accounts = [ + (a.public_key.address if isinstance(a, BaseAccount) else a) + for a in (my_accounts or constraints.get('accounts', [])) + ] if 'order_by' not in constraints: - constraints['order_by'] = ["tx.height=0 DESC", "tx.height DESC", "tx.position DESC"] + constraints['order_by'] = [ + "tx.height=0 DESC", "tx.height DESC", "tx.position DESC", "txo.position"] rows = await self.select_txos( "tx.txid, raw, tx.height, tx.position, tx.is_verified, " "txo.position, chain, account, amount, script", @@ -513,7 +515,7 @@ class BaseDatabase(SQLiteMixin): ) txo = txs[row[0]].outputs[row[5]] txo.is_change = row[6] == 1 - txo.is_my_account = row[7] == my_account + txo.is_my_account = row[7] in my_accounts txos.append(txo) return txos diff --git a/torba/torba/client/baseledger.py b/torba/torba/client/baseledger.py index c1226e21f..8b0c6ad7d 100644 --- a/torba/torba/client/baseledger.py +++ b/torba/torba/client/baseledger.py @@ -227,6 +227,18 @@ class BaseLedger(metaclass=LedgerRegistry): def release_tx(self, tx): return self.release_outputs([txi.txo_ref.txo for txi in tx.inputs]) + def get_utxos(self, **constraints): + return self.db.get_utxos(**constraints) + + def get_utxo_count(self, **constraints): + return self.db.get_utxo_count(**constraints) + + def get_transactions(self, **constraints): + return self.db.get_transactions(**constraints) + + def get_transaction_count(self, **constraints): + return self.db.get_transaction_count(**constraints) + async def get_local_status_and_history(self, address): address_details = await self.db.get_address(address=address) history = address_details['history'] or ''