Merge pull request #2848 from lbryio/claim_transaction_list_filtering

added new `txo_list` command with filtering for `--claim_id`, claim `--name` and `--is_received`/`--is_not_received`, also commands `claim_list`/`stream_list`/`channel_list`/`support_list` are based on `txo_list` and thus support most of the new filters
This commit is contained in:
Lex Berezhny 2020-03-08 23:33:38 -04:00 committed by GitHub
commit 86cc65d894
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 105 deletions

View file

@ -21,9 +21,10 @@ from prometheus_client import generate_latest as prom_generate_latest
from google.protobuf.message import DecodeError from google.protobuf.message import DecodeError
from lbry.wallet import ( from lbry.wallet import (
Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic, Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic,
Transaction, Output, Input, Account Transaction, Output, Input, Account, database
) )
from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc
from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES
from lbry import utils from lbry import utils
from lbry.conf import Config, Setting, NOT_SET from lbry.conf import Config, Setting, NOT_SET
@ -2167,19 +2168,20 @@ class Daemon(metaclass=JSONRPCServerType):
""" """
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
def jsonrpc_claim_list( def jsonrpc_claim_list(self, claim_type=None, **kwargs):
self, claim_type=None, account_id=None, wallet_id=None, page=None, page_size=None, resolve=False):
""" """
List my stream and channel claims. List my stream and channel claims.
Usage: Usage:
claim_list [--claim_type=<claim_type>...] claim_list [--claim_type=<claim_type>...] [--claim_id=<claim_id>...] [--name=<name>...]
[--account_id=<account_id>] [--wallet_id=<wallet_id>] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
[--page=<page>] [--page_size=<page_size>] [--page=<page>] [--page_size=<page_size>]
[--resolve] [--resolve]
Options: Options:
--claim_type=<claim_type> : (str) claim type: channel, stream, repost, collection --claim_type=<claim_type> : (str or list) claim type: channel, stream, repost, collection
--claim_id=<claim_id> : (str or list) claim id
--name=<name> : (str or list) claim name
--account_id=<account_id> : (str) id of the account to query --account_id=<account_id> : (str) id of the account to query
--wallet_id=<wallet_id> : (str) restrict results to specific wallet --wallet_id=<wallet_id> : (str) restrict results to specific wallet
--page=<page> : (int) page to return during paginating --page=<page> : (int) page to return during paginating
@ -2188,15 +2190,9 @@ class Daemon(metaclass=JSONRPCServerType):
Returns: {Paginated[Output]} Returns: {Paginated[Output]}
""" """
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) kwargs['type'] = claim_type or CLAIM_TYPE_NAMES
if account_id: kwargs['unspent'] = True
account = wallet.get_account_or_error(account_id) return self.jsonrpc_txo_list(**kwargs)
claims = account.get_claims
claim_count = account.get_claim_count
else:
claims = partial(self.ledger.get_claims, wallet=wallet, accounts=wallet.accounts)
claim_count = partial(self.ledger.get_claim_count, wallet=wallet, accounts=wallet.accounts)
return paginate_rows(claims, claim_count, page, page_size, claim_type=claim_type, resolve=resolve)
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_claim_search(self, **kwargs): async def jsonrpc_claim_search(self, **kwargs):
@ -2699,15 +2695,18 @@ class Daemon(metaclass=JSONRPCServerType):
return tx return tx
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
def jsonrpc_channel_list(self, account_id=None, wallet_id=None, page=None, page_size=None, resolve=False): def jsonrpc_channel_list(self, *args, **kwargs):
""" """
List my channel claims. List my channel claims.
Usage: Usage:
channel_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>] channel_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
[--name=<name>...] [--claim_id=<claim_id>...]
[--page=<page>] [--page_size=<page_size>] [--resolve] [--page=<page>] [--page_size=<page_size>] [--resolve]
Options: Options:
--name=<name> : (str or list) channel name
--claim_id=<claim_id> : (str or list) channel id
--account_id=<account_id> : (str) id of the account to use --account_id=<account_id> : (str) id of the account to use
--wallet_id=<wallet_id> : (str) restrict results to specific wallet --wallet_id=<wallet_id> : (str) restrict results to specific wallet
--page=<page> : (int) page to return during paginating --page=<page> : (int) page to return during paginating
@ -2716,15 +2715,9 @@ class Daemon(metaclass=JSONRPCServerType):
Returns: {Paginated[Output]} Returns: {Paginated[Output]}
""" """
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) kwargs['type'] = 'channel'
if account_id: kwargs['unspent'] = True
account = wallet.get_account_or_error(account_id) return self.jsonrpc_txo_list(*args, **kwargs)
channels = account.get_channels
channel_count = account.get_channel_count
else:
channels = partial(self.ledger.get_channels, wallet=wallet, accounts=wallet.accounts)
channel_count = partial(self.ledger.get_channel_count, wallet=wallet, accounts=wallet.accounts)
return paginate_rows(channels, channel_count, page, page_size, resolve=resolve)
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_channel_export(self, channel_id=None, channel_name=None, account_id=None, wallet_id=None): async def jsonrpc_channel_export(self, channel_id=None, channel_name=None, account_id=None, wallet_id=None):
@ -3441,15 +3434,18 @@ class Daemon(metaclass=JSONRPCServerType):
return tx return tx
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
def jsonrpc_stream_list(self, account_id=None, wallet_id=None, page=None, page_size=None, resolve=False): def jsonrpc_stream_list(self, *args, **kwargs):
""" """
List my stream claims. List my stream claims.
Usage: Usage:
stream_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>] stream_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
[--name=<name>...] [--claim_id=<claim_id>...]
[--page=<page>] [--page_size=<page_size>] [--resolve] [--page=<page>] [--page_size=<page_size>] [--resolve]
Options: Options:
--name=<name> : (str or list) stream name
--claim_id=<claim_id> : (str or list) stream id
--account_id=<account_id> : (str) id of the account to query --account_id=<account_id> : (str) id of the account to query
--wallet_id=<wallet_id> : (str) restrict results to specific wallet --wallet_id=<wallet_id> : (str) restrict results to specific wallet
--page=<page> : (int) page to return during paginating --page=<page> : (int) page to return during paginating
@ -3458,15 +3454,9 @@ class Daemon(metaclass=JSONRPCServerType):
Returns: {Paginated[Output]} Returns: {Paginated[Output]}
""" """
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) kwargs['type'] = 'stream'
if account_id: kwargs['unspent'] = True
account = wallet.get_account_or_error(account_id) return self.jsonrpc_txo_list(*args, **kwargs)
streams = account.get_streams
stream_count = account.get_stream_count
else:
streams = partial(self.ledger.get_streams, wallet=wallet, accounts=wallet.accounts)
stream_count = partial(self.ledger.get_stream_count, wallet=wallet, accounts=wallet.accounts)
return paginate_rows(streams, stream_count, page, page_size, resolve=resolve)
@requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT,
DHT_COMPONENT, DATABASE_COMPONENT) DHT_COMPONENT, DATABASE_COMPONENT)
@ -3912,15 +3902,19 @@ class Daemon(metaclass=JSONRPCServerType):
return tx return tx
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
def jsonrpc_support_list(self, account_id=None, wallet_id=None, page=None, page_size=None): def jsonrpc_support_list(self, *args, tips=None, **kwargs):
""" """
List supports and tips in my control. List supports and tips in my control.
Usage: Usage:
support_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>] support_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
[--name=<name>...] [--claim_id=<claim_id>...] [--tips]
[--page=<page>] [--page_size=<page_size>] [--page=<page>] [--page_size=<page_size>]
Options: Options:
--name=<name> : (str or list) claim name
--claim_id=<claim_id> : (str or list) claim id
--tips : (bool) only show tips (is_received=true)
--account_id=<account_id> : (str) id of the account to query --account_id=<account_id> : (str) id of the account to query
--wallet_id=<wallet_id> : (str) restrict results to specific wallet --wallet_id=<wallet_id> : (str) restrict results to specific wallet
--page=<page> : (int) page to return during paginating --page=<page> : (int) page to return during paginating
@ -3928,15 +3922,12 @@ class Daemon(metaclass=JSONRPCServerType):
Returns: {Paginated[Output]} Returns: {Paginated[Output]}
""" """
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) kwargs['type'] = 'support'
if account_id: kwargs['unspent'] = True
account = wallet.get_account_or_error(account_id) kwargs['include_is_received'] = True
supports = account.get_supports if tips is True:
support_count = account.get_support_count kwargs['is_received'] = True
else: return self.jsonrpc_txo_list(*args, **kwargs)
supports = partial(self.ledger.get_supports, wallet=wallet, accounts=wallet.accounts)
support_count = partial(self.ledger.get_support_count, wallet=wallet, accounts=wallet.accounts)
return paginate_rows(supports, support_count, page, page_size)
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_support_abandon( async def jsonrpc_support_abandon(
@ -4103,12 +4094,71 @@ class Daemon(metaclass=JSONRPCServerType):
""" """
return self.wallet_manager.get_transaction(txid) return self.wallet_manager.get_transaction(txid)
TXO_DOC = """
List transaction outputs.
"""
@requires(WALLET_COMPONENT)
def jsonrpc_txo_list(
self, account_id=None, type=None, txid=None, # pylint: disable=redefined-builtin
claim_id=None, name=None, unspent=False,
include_is_received=False, is_received=None, is_not_received=None,
wallet_id=None, page=None, page_size=None, resolve=False):
"""
List my transaction outputs.
Usage:
txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]
[--claim_id=<claim_id>...] [--name=<name>...] [--unspent]
[--include_is_received] [--is_received] [--is_not_received]
[--wallet_id=<wallet_id>] [--include_is_received] [--is_received]
[--page=<page>] [--page_size=<page_size>]
[--resolve]
Options:
--type=<type> : (str or list) claim type: stream, channel, support,
purchase, collection, repost, other
--txid=<txid> : (str or list) transaction id of outputs
--unspent : (bool) hide spent outputs, show only unspent ones
--claim_id=<claim_id> : (str or list) claim id
--name=<name> : (str or list) claim name
--include_is_received : (bool) calculate the is_received property and
include in output, this happens automatically if you
use the --is_received or --is_not_received filters
--is_received : (bool) only return txos sent from others to this account
--is_not_received : (bool) only return txos created by this account
--account_id=<account_id> : (str) id of the account to query
--wallet_id=<wallet_id> : (str) restrict results to specific wallet
--page=<page> : (int) page to return during paginating
--page_size=<page_size> : (int) number of items on page during pagination
--resolve : (bool) resolves each claim to provide additional metadata
Returns: {Paginated[Output]}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if account_id:
account = wallet.get_account_or_error(account_id)
claims = account.get_txos
claim_count = account.get_txo_count
else:
claims = partial(self.ledger.get_txos, wallet=wallet, accounts=wallet.accounts)
claim_count = partial(self.ledger.get_txo_count, wallet=wallet, accounts=wallet.accounts)
constraints = {'resolve': resolve, 'unspent': unspent, 'include_is_received': include_is_received}
if is_received is True:
constraints['is_received'] = True
elif is_not_received is True:
constraints['is_received'] = False
database.constrain_single_or_list(constraints, 'txo_type', type, lambda x: TXO_TYPES[x])
database.constrain_single_or_list(constraints, 'claim_id', claim_id)
database.constrain_single_or_list(constraints, 'claim_name', name)
return paginate_rows(claims, claim_count, page, page_size, **constraints)
UTXO_DOC = """ UTXO_DOC = """
Unspent transaction management. Unspent transaction management.
""" """
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
def jsonrpc_utxo_list(self, account_id=None, wallet_id=None, page=None, page_size=None): def jsonrpc_utxo_list(self, *args, **kwargs):
""" """
List unspent transaction outputs List unspent transaction outputs
@ -4124,15 +4174,9 @@ class Daemon(metaclass=JSONRPCServerType):
Returns: {Paginated[Output]} Returns: {Paginated[Output]}
""" """
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) kwargs['type'] = ['other', 'purchase']
if account_id: kwargs['unspent'] = True
account = wallet.get_account_or_error(account_id) return self.jsonrpc_txo_list(*args, **kwargs)
utxos = account.get_utxos
utxo_count = account.get_utxo_count
else:
utxos = partial(self.ledger.get_utxos, wallet=wallet, accounts=wallet.accounts)
utxo_count = partial(self.ledger.get_utxo_count, wallet=wallet, accounts=wallet.accounts)
return paginate_rows(utxos, utxo_count, page, page_size)
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_utxo_release(self, account_id=None, wallet_id=None): async def jsonrpc_utxo_release(self, account_id=None, wallet_id=None):

View file

@ -25,7 +25,8 @@ def encode_txo_doc():
'address': "address of who can spend the txo", 'address': "address of who can spend the txo",
'confirmations': "number of confirmed blocks", 'confirmations': "number of confirmed blocks",
'is_change': "payment to change address, only available when it can be determined", 'is_change': "payment to change address, only available when it can be determined",
'is_spent': "true if txo is spent, false or None if it could not be determined", 'is_received': "true if txo was sent from external account to this account",
'is_spent': "true if txo is spent",
'is_mine': "payment to one of your accounts, only available when it can be determined", 'is_mine': "payment to one of your accounts, only available when it can be determined",
'type': "one of 'claim', 'support' or 'purchase'", 'type': "one of 'claim', 'support' or 'purchase'",
'name': "when type is 'claim' or 'support', this is the claim name", 'name': "when type is 'claim' or 'support', this is the claim name",
@ -169,6 +170,8 @@ class JSONResponseEncoder(JSONEncoder):
} }
if txo.is_change is not None: if txo.is_change is not None:
output['is_change'] = txo.is_change output['is_change'] = txo.is_change
if txo.is_received is not None:
output['is_received'] = txo.is_received
if txo.is_spent is not None: if txo.is_spent is not None:
output['is_spent'] = txo.is_spent output['is_spent'] = txo.is_spent
if txo.is_my_account is not None: if txo.is_my_account is not None:

View file

@ -564,6 +564,9 @@ class CommandTestCase(IntegrationTestCase):
async def file_list(self, *args, **kwargs): async def file_list(self, *args, **kwargs):
return (await self.out(self.daemon.jsonrpc_file_list(*args, **kwargs)))['items'] return (await self.out(self.daemon.jsonrpc_file_list(*args, **kwargs)))['items']
async def txo_list(self, *args, **kwargs):
return (await self.out(self.daemon.jsonrpc_txo_list(*args, **kwargs)))['items']
async def claim_list(self, *args, **kwargs): async def claim_list(self, *args, **kwargs):
return (await self.out(self.daemon.jsonrpc_claim_list(*args, **kwargs)))['items'] return (await self.out(self.daemon.jsonrpc_claim_list(*args, **kwargs)))['items']
@ -573,6 +576,9 @@ class CommandTestCase(IntegrationTestCase):
async def channel_list(self, *args, **kwargs): async def channel_list(self, *args, **kwargs):
return (await self.out(self.daemon.jsonrpc_channel_list(*args, **kwargs)))['items'] return (await self.out(self.daemon.jsonrpc_channel_list(*args, **kwargs)))['items']
async def transaction_list(self, *args, **kwargs):
return (await self.out(self.daemon.jsonrpc_transaction_list(*args, **kwargs)))['items']
@staticmethod @staticmethod
def get_claim_id(tx): def get_claim_id(tx):
return tx['outputs'][0]['claim_id'] return tx['outputs'][0]['claim_id']

View file

@ -467,6 +467,12 @@ class Account:
'max_receiving_gap': receiving_gap, 'max_receiving_gap': receiving_gap,
} }
def get_txos(self, **constraints):
return self.ledger.get_txos(wallet=self.wallet, accounts=[self], **constraints)
def get_txo_count(self, **constraints):
return self.ledger.get_txo_count(wallet=self.wallet, accounts=[self], **constraints)
def get_utxos(self, **constraints): def get_utxos(self, **constraints):
return self.ledger.get_utxos(wallet=self.wallet, accounts=[self], **constraints) return self.ledger.get_utxos(wallet=self.wallet, accounts=[self], **constraints)

View file

@ -6,6 +6,7 @@ COIN = 100*CENT
TIMEOUT = 30.0 TIMEOUT = 30.0
TXO_TYPES = { TXO_TYPES = {
"other": 0,
"stream": 1, "stream": 1,
"channel": 2, "channel": 2,
"support": 3, "support": 3,
@ -14,9 +15,13 @@ TXO_TYPES = {
"repost": 6, "repost": 6,
} }
CLAIM_TYPES = [ CLAIM_TYPE_NAMES = [
TXO_TYPES['stream'], 'stream',
TXO_TYPES['channel'], 'channel',
TXO_TYPES['collection'], 'collection',
TXO_TYPES['repost'], 'repost',
]
CLAIM_TYPES = [
TXO_TYPES[name] for name in CLAIM_TYPE_NAMES
] ]

View file

@ -107,7 +107,9 @@ def constraints_to_sql(constraints, joiner=' AND ', prepend_key=''):
if not key: if not key:
sql.append(constraint) sql.append(constraint)
continue continue
if key.startswith('$'): if key.startswith('$$'):
col, key = col[2:], key[1:]
elif key.startswith('$'):
values[key] = constraint values[key] = constraint
continue continue
if key.endswith('__not'): if key.endswith('__not'):
@ -221,6 +223,19 @@ def rows_to_dict(rows, fields):
return [] return []
def constrain_single_or_list(constraints, column, value, convert=lambda x: x):
if value is not None:
if isinstance(value, list):
value = [convert(v) for v in value]
if len(value) == 1:
constraints[column] = value[0]
elif len(value) > 1:
constraints[f"{column}__in"] = value
else:
constraints[column] = convert(value)
return constraints
class SQLiteMixin: class SQLiteMixin:
SCHEMA_VERSION: Optional[str] = None SCHEMA_VERSION: Optional[str] = None
@ -350,6 +365,7 @@ class Database(SQLiteMixin):
create index if not exists txo_txid_idx on txo (txid); create index if not exists txo_txid_idx on txo (txid);
create index if not exists txo_address_idx on txo (address); create index if not exists txo_address_idx on txo (address);
create index if not exists txo_claim_id_idx on txo (claim_id); create index if not exists txo_claim_id_idx on txo (claim_id);
create index if not exists txo_claim_name_idx on txo (claim_name);
create index if not exists txo_txo_type_idx on txo (txo_type); create index if not exists txo_txo_type_idx on txo (txo_type);
""" """
@ -474,16 +490,15 @@ class Database(SQLiteMixin):
async def select_transactions(self, cols, accounts=None, **constraints): async def select_transactions(self, cols, accounts=None, **constraints):
if not {'txid', 'txid__in'}.intersection(constraints): if not {'txid', 'txid__in'}.intersection(constraints):
assert accounts, "'accounts' argument required when no 'txid' constraint is present" assert accounts, "'accounts' argument required when no 'txid' constraint is present"
constraints.update({ where, values = constraints_to_sql({
f'$account{i}': a.public_key.address for i, a in enumerate(accounts) '$$account_address.account__in': [a.public_key.address for a in accounts]
}) })
account_values = ', '.join([f':$account{i}' for i in range(len(accounts))])
where = f" WHERE account_address.account IN ({account_values})"
constraints['txid__in'] = f""" constraints['txid__in'] = f"""
SELECT txo.txid FROM txo JOIN account_address USING (address) {where} SELECT txo.txid FROM txo JOIN account_address USING (address) WHERE {where}
UNION UNION
SELECT txi.txid FROM txi JOIN account_address USING (address) {where} SELECT txi.txid FROM txi JOIN account_address USING (address) WHERE {where}
""" """
constraints.update(values)
return await self.db.execute_fetchall( return await self.db.execute_fetchall(
*query(f"SELECT {cols} FROM tx", **constraints) *query(f"SELECT {cols} FROM tx", **constraints)
) )
@ -562,13 +577,33 @@ class Database(SQLiteMixin):
if txs: if txs:
return txs[0] return txs[0]
async def select_txos(self, cols, **constraints): async def select_txos(self, cols, wallet=None, include_is_received=False, **constraints):
if include_is_received:
assert wallet is not None, 'cannot use is_recieved filter without wallet argument'
account_in_wallet, values = constraints_to_sql({
'$$account__in#is_received': [a.public_key.address for a in wallet.accounts]
})
cols += f""",
NOT EXISTS(
SELECT 1 FROM txi JOIN account_address USING (address)
WHERE txi.txid=txo.txid AND {account_in_wallet}
) as is_received
"""
constraints.update(values)
sql = f"SELECT {cols} FROM txo JOIN tx USING (txid)" sql = f"SELECT {cols} FROM txo JOIN tx USING (txid)"
if 'accounts' in constraints: if 'accounts' in constraints:
sql += " JOIN account_address USING (address)" sql += " JOIN account_address USING (address)"
return await self.db.execute_fetchall(*query(sql, **constraints)) return await self.db.execute_fetchall(*query(sql, **constraints))
async def get_txos(self, wallet=None, no_tx=False, **constraints): @staticmethod
def constrain_unspent(constraints):
constraints['is_reserved'] = False
constraints['txoid__not_in'] = "SELECT txoid FROM txi"
async def get_txos(self, wallet=None, no_tx=False, unspent=False, include_is_received=False, **constraints):
include_is_received = include_is_received or 'is_received' in constraints
if unspent:
self.constrain_unspent(constraints)
my_accounts = {a.public_key.address for a in wallet.accounts} if wallet else set() my_accounts = {a.public_key.address for a in wallet.accounts} if wallet else set()
if 'order_by' not in constraints: if 'order_by' not in constraints:
constraints['order_by'] = [ constraints['order_by'] = [
@ -579,9 +614,9 @@ class Database(SQLiteMixin):
tx.txid, raw, tx.height, tx.position, tx.is_verified, txo.position, amount, script, ( tx.txid, raw, tx.height, tx.position, tx.is_verified, txo.position, amount, script, (
select group_concat(account||"|"||chain) from account_address select group_concat(account||"|"||chain) from account_address
where account_address.address=txo.address where account_address.address=txo.address
), exists(select txoid from txi where txi.txoid=txo.txoid) ), exists(select 1 from txi where txi.txoid=txo.txoid)
""", """,
**constraints wallet=wallet, include_is_received=include_is_received, **constraints
) )
txos = [] txos = []
txs = {} txs = {}
@ -602,6 +637,8 @@ class Database(SQLiteMixin):
row_accounts = dict(a.split('|') for a in row[8].split(',')) row_accounts = dict(a.split('|') for a in row[8].split(','))
account_match = set(row_accounts) & my_accounts account_match = set(row_accounts) & my_accounts
txo.is_spent = bool(row[9]) txo.is_spent = bool(row[9])
if include_is_received:
txo.is_received = bool(row[10])
if account_match: if account_match:
txo.is_my_account = True txo.is_my_account = True
txo.is_change = row_accounts[account_match.pop()] == '1' txo.is_change = row_accounts[account_match.pop()] == '1'
@ -637,33 +674,28 @@ class Database(SQLiteMixin):
return txos return txos
async def get_txo_count(self, **constraints): async def get_txo_count(self, unspent=False, **constraints):
constraints['include_is_received'] = 'is_received' in constraints
constraints.pop('resolve', None) constraints.pop('resolve', None)
constraints.pop('wallet', None)
constraints.pop('offset', None) constraints.pop('offset', None)
constraints.pop('limit', None) constraints.pop('limit', None)
constraints.pop('order_by', None) constraints.pop('order_by', None)
if unspent:
self.constrain_unspent(constraints)
count = await self.select_txos('count(*)', **constraints) count = await self.select_txos('count(*)', **constraints)
return count[0][0] return count[0][0]
@staticmethod
def constrain_utxo(constraints):
constraints['is_reserved'] = False
constraints['txoid__not_in'] = "SELECT txoid FROM txi"
def get_utxos(self, **constraints): def get_utxos(self, **constraints):
self.constrain_utxo(constraints) return self.get_txos(unspent=True, **constraints)
return self.get_txos(**constraints)
def get_utxo_count(self, **constraints): def get_utxo_count(self, **constraints):
self.constrain_utxo(constraints) return self.get_txo_count(unspent=True, **constraints)
return self.get_txo_count(**constraints)
async def get_balance(self, wallet=None, accounts=None, **constraints): async def get_balance(self, wallet=None, accounts=None, **constraints):
assert wallet or accounts, \ assert wallet or accounts, \
"'wallet' or 'accounts' constraints required to calculate balance" "'wallet' or 'accounts' constraints required to calculate balance"
constraints['accounts'] = accounts or wallet.accounts constraints['accounts'] = accounts or wallet.accounts
self.constrain_utxo(constraints) self.constrain_unspent(constraints)
balance = await self.select_txos('SUM(amount)', **constraints) balance = await self.select_txos('SUM(amount)', **constraints)
return balance[0][0] or 0 return balance[0][0] or 0
@ -746,11 +778,13 @@ class Database(SQLiteMixin):
@staticmethod @staticmethod
def constrain_claims(constraints): def constrain_claims(constraints):
if {'txo_type', 'txo_type__in'}.intersection(constraints):
return
claim_types = constraints.pop('claim_type', None) claim_types = constraints.pop('claim_type', None)
if isinstance(claim_types, str) and claim_types: if claim_types:
claim_types = [claim_types] constrain_single_or_list(
if isinstance(claim_types, list) and claim_types: constraints, 'txo_type', claim_types, lambda x: TXO_TYPES[x]
constraints['txo_type__in'] = [TXO_TYPES[ct] for ct in claim_types] )
else: else:
constraints['txo_type__in'] = CLAIM_TYPES constraints['txo_type__in'] = CLAIM_TYPES

View file

@ -262,6 +262,15 @@ class Ledger(metaclass=LedgerRegistry):
self.constraint_spending_utxos(constraints) self.constraint_spending_utxos(constraints)
return self.db.get_utxo_count(**constraints) return self.db.get_utxo_count(**constraints)
async def get_txos(self, resolve=False, **constraints):
txos = await self.db.get_txos(**constraints)
if resolve:
return await self._resolve_for_local_results(constraints.get('accounts', []), txos)
return txos
def get_txo_count(self, **constraints):
return self.db.get_txo_count(**constraints)
def get_transactions(self, **constraints): def get_transactions(self, **constraints):
return self.db.get_transactions(**constraints) return self.db.get_transactions(**constraints)
@ -735,9 +744,11 @@ class Ledger(metaclass=LedgerRegistry):
async def _resolve_for_local_results(self, accounts, txos): async def _resolve_for_local_results(self, accounts, txos):
results = [] results = []
response = await self.resolve(accounts, [txo.permanent_url for txo in txos]) response = await self.resolve(
accounts, [txo.permanent_url for txo in txos if txo.can_decode_claim]
)
for txo in txos: for txo in txos:
resolved = response[txo.permanent_url] resolved = response.get(txo.permanent_url) if txo.can_decode_claim else None
if isinstance(resolved, Output): if isinstance(resolved, Output):
resolved.update_annotations(txo) resolved.update_annotations(txo)
results.append(resolved) results.append(resolved)

View file

@ -207,7 +207,7 @@ class OutputEffectiveAmountEstimator:
class Output(InputOutput): class Output(InputOutput):
__slots__ = ( __slots__ = (
'amount', 'script', 'is_change', 'is_spent', 'is_my_account', 'amount', 'script', 'is_change', 'is_spent', 'is_received', 'is_my_account',
'channel', 'private_key', 'meta', 'channel', 'private_key', 'meta',
'purchase', 'purchased_claim', 'purchase_receipt', 'purchase', 'purchased_claim', 'purchase_receipt',
'reposted_claim', 'claims', 'reposted_claim', 'claims',
@ -216,7 +216,7 @@ class Output(InputOutput):
def __init__(self, amount: int, script: OutputScript, def __init__(self, amount: int, script: OutputScript,
tx_ref: TXRef = None, position: int = None, tx_ref: TXRef = None, position: int = None,
is_change: Optional[bool] = None, is_spent: Optional[bool] = None, is_change: Optional[bool] = None, is_spent: Optional[bool] = None,
is_my_account: Optional[bool] = None, is_received: Optional[bool] = None, is_my_account: Optional[bool] = None,
channel: Optional['Output'] = None, private_key: Optional[str] = None channel: Optional['Output'] = None, private_key: Optional[str] = None
) -> None: ) -> None:
super().__init__(tx_ref, position) super().__init__(tx_ref, position)
@ -224,6 +224,7 @@ class Output(InputOutput):
self.script = script self.script = script
self.is_change = is_change self.is_change = is_change
self.is_spent = is_spent self.is_spent = is_spent
self.is_received = is_received
self.is_my_account = is_my_account self.is_my_account = is_my_account
self.channel = channel self.channel = channel
self.private_key = private_key self.private_key = private_key

View file

@ -1,4 +1,3 @@
import asyncio
import os.path import os.path
import tempfile import tempfile
import logging import logging
@ -399,12 +398,116 @@ class ClaimSearchCommand(ClaimTestCase):
await self.assertFindsClaims([], text='cloud') await self.assertFindsClaims([], text='cloud')
class TransactionCommands(ClaimTestCase):
async def test_transaction_list(self):
channel_id = self.get_claim_id(await self.channel_create())
await self.channel_update(channel_id, bid='0.5')
await self.channel_abandon(claim_id=channel_id)
stream_id = self.get_claim_id(await self.stream_create())
await self.stream_update(stream_id, bid='0.5')
await self.stream_abandon(claim_id=stream_id)
r = await self.transaction_list()
self.assertEqual(7, len(r))
self.assertEqual(stream_id, r[0]['abandon_info'][0]['claim_id'])
self.assertEqual(stream_id, r[1]['update_info'][0]['claim_id'])
self.assertEqual(stream_id, r[2]['claim_info'][0]['claim_id'])
self.assertEqual(channel_id, r[3]['abandon_info'][0]['claim_id'])
self.assertEqual(channel_id, r[4]['update_info'][0]['claim_id'])
self.assertEqual(channel_id, r[5]['claim_info'][0]['claim_id'])
class TransactionOutputCommands(ClaimTestCase):
async def test_txo_list_filtering(self):
channel_id = self.get_claim_id(await self.channel_create())
await self.channel_update(channel_id, bid='0.5')
stream_id = self.get_claim_id(await self.stream_create())
await self.stream_update(stream_id, bid='0.5')
# type filtering
r = await self.txo_list(type='channel')
self.assertEqual(2, len(r))
self.assertEqual('channel', r[0]['value_type'])
self.assertFalse(r[0]['is_spent'])
self.assertEqual('channel', r[1]['value_type'])
self.assertTrue(r[1]['is_spent'])
r = await self.txo_list(type='stream')
self.assertEqual(2, len(r))
self.assertEqual('stream', r[0]['value_type'])
self.assertFalse(r[0]['is_spent'])
self.assertEqual('stream', r[1]['value_type'])
self.assertTrue(r[1]['is_spent'])
r = await self.txo_list(type=['stream', 'channel'])
self.assertEqual(4, len(r))
self.assertEqual({'stream', 'channel'}, {c['value_type'] for c in r})
# claim_id filtering
r = await self.txo_list(claim_id=stream_id)
self.assertEqual(2, len(r))
self.assertEqual({stream_id}, {c['claim_id'] for c in r})
r = await self.txo_list(claim_id=[stream_id, channel_id])
self.assertEqual(4, len(r))
self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})
stream_name, _, channel_name, _ = (c['name'] for c in r)
r = await self.txo_list(claim_id=['beef'])
self.assertEqual(0, len(r))
# claim_name filtering
r = await self.txo_list(name=stream_name)
self.assertEqual(2, len(r))
self.assertEqual({stream_id}, {c['claim_id'] for c in r})
r = await self.txo_list(name=[stream_name, channel_name])
self.assertEqual(4, len(r))
self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})
r = await self.txo_list(name=['beef'])
self.assertEqual(0, len(r))
r = await self.txo_list()
self.assertEqual(9, len(r))
await self.stream_abandon(claim_id=stream_id)
r = await self.txo_list()
self.assertEqual(10, len(r))
r = await self.txo_list(claim_id=stream_id)
self.assertEqual(2, len(r))
self.assertTrue(r[0]['is_spent'])
self.assertTrue(r[1]['is_spent'])
async def test_txo_list_received_filtering(self):
wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)
address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id)
await self.channel_create(claim_address=address2)
r = await self.txo_list(include_is_received=True)
self.assertEqual(2, len(r))
self.assertFalse(r[0]['is_received'])
self.assertTrue(r[1]['is_received'])
rt = await self.txo_list(is_not_received=True)
self.assertEqual(1, len(rt))
self.assertEqual(rt[0], r[0])
rf = await self.txo_list(is_received=True)
self.assertEqual(1, len(rf))
self.assertEqual(rf[0], r[1])
r = await self.txo_list(include_is_received=True, wallet_id=wallet2.id)
self.assertEqual(1, len(r))
self.assertTrue(r[0]['is_received'])
class ClaimCommands(ClaimTestCase): class ClaimCommands(ClaimTestCase):
async def test_claim_list_type_filtering(self): async def test_claim_list_filtering(self):
await self.channel_create() channel_id = self.get_claim_id(await self.channel_create())
await self.stream_create() stream_id = self.get_claim_id(await self.stream_create())
# type filtering
r = await self.claim_list(claim_type='channel') r = await self.claim_list(claim_type='channel')
self.assertEqual(1, len(r)) self.assertEqual(1, len(r))
self.assertEqual('channel', r[0]['value_type']) self.assertEqual('channel', r[0]['value_type'])
@ -417,6 +520,31 @@ class ClaimCommands(ClaimTestCase):
self.assertEqual(2, len(r)) self.assertEqual(2, len(r))
self.assertEqual({'stream', 'channel'}, {c['value_type'] for c in r}) self.assertEqual({'stream', 'channel'}, {c['value_type'] for c in r})
# claim_id filtering
r = await self.claim_list(claim_id=stream_id)
self.assertEqual(1, len(r))
self.assertEqual({stream_id}, {c['claim_id'] for c in r})
r = await self.claim_list(claim_id=[stream_id, channel_id])
self.assertEqual(2, len(r))
self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})
stream_name, channel_name = (c['name'] for c in r)
r = await self.claim_list(claim_id=['beef'])
self.assertEqual(0, len(r))
# claim_name filtering
r = await self.claim_list(name=stream_name)
self.assertEqual(1, len(r))
self.assertEqual({stream_id}, {c['claim_id'] for c in r})
r = await self.claim_list(name=[stream_name, channel_name])
self.assertEqual(2, len(r))
self.assertEqual({stream_id, channel_id}, {c['claim_id'] for c in r})
r = await self.claim_list(name=['beef'])
self.assertEqual(0, len(r))
async def test_claim_stream_channel_list_with_resolve(self): async def test_claim_stream_channel_list_with_resolve(self):
self.assertListEqual([], await self.claim_list(resolve=True)) self.assertListEqual([], await self.claim_list(resolve=True))
@ -1364,7 +1492,7 @@ class StreamCommands(ClaimTestCase):
tx = await self.stream_create(bid='2.5') # creates new claim tx = await self.stream_create(bid='2.5') # creates new claim
claim_id = self.get_claim_id(tx) claim_id = self.get_claim_id(tx)
txs = (await self.out(self.daemon.jsonrpc_transaction_list()))['items'] txs = await self.transaction_list()
self.assertEqual(len(txs[0]['claim_info']), 1) self.assertEqual(len(txs[0]['claim_info']), 1)
self.assertEqual(txs[0]['confirmations'], 1) self.assertEqual(txs[0]['confirmations'], 1)
self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5') self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5')
@ -1379,7 +1507,7 @@ class StreamCommands(ClaimTestCase):
self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0) self.assertItemCount(await self.daemon.jsonrpc_file_list(), 0)
await self.stream_update(claim_id, bid='1.0') # updates previous claim await self.stream_update(claim_id, bid='1.0') # updates previous claim
txs = (await self.out(self.daemon.jsonrpc_transaction_list()))['items'] txs = await self.transaction_list()
self.assertEqual(len(txs[0]['update_info']), 1) self.assertEqual(len(txs[0]['update_info']), 1)
self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5') self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5')
self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id) self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id)
@ -1390,7 +1518,7 @@ class StreamCommands(ClaimTestCase):
await self.assertBalance(self.account, '8.9796765') await self.assertBalance(self.account, '8.9796765')
await self.stream_abandon(claim_id) await self.stream_abandon(claim_id)
txs = (await self.out(self.daemon.jsonrpc_transaction_list()))['items'] txs = await self.transaction_list()
self.assertEqual(len(txs[0]['abandon_info']), 1) self.assertEqual(len(txs[0]['abandon_info']), 1)
self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0') self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0')
self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id) self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id)
@ -1494,7 +1622,7 @@ class SupportCommands(CommandTestCase):
await self.assertBalance(account2, '3.9998585') await self.assertBalance(account2, '3.9998585')
# verify that the incoming tip is marked correctly as is_tip=True in account1 # verify that the incoming tip is marked correctly as is_tip=True in account1
txs = (await self.out(self.daemon.jsonrpc_transaction_list(self.account.id)))['items'] txs = await self.transaction_list(account_id=self.account.id)
self.assertEqual(len(txs[0]['support_info']), 1) 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]['balance_delta'], '1.0')
self.assertEqual(txs[0]['support_info'][0]['claim_id'], claim_id) self.assertEqual(txs[0]['support_info'][0]['claim_id'], claim_id)
@ -1504,9 +1632,7 @@ class SupportCommands(CommandTestCase):
self.assertEqual(txs[0]['fee'], '0.0') self.assertEqual(txs[0]['fee'], '0.0')
# verify that the outgoing tip is marked correctly as is_tip=True in account2 # verify that the outgoing tip is marked correctly as is_tip=True in account2
txs2 = (await self.out( txs2 = await self.transaction_list(wallet_id='wallet2', account_id=account2.id)
self.daemon.jsonrpc_transaction_list(wallet_id='wallet2', account_id=account2.id)
))['items']
self.assertEqual(len(txs2[0]['support_info']), 1) self.assertEqual(len(txs2[0]['support_info']), 1)
self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-1.0') self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-1.0')
self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id) self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id)
@ -1527,7 +1653,7 @@ class SupportCommands(CommandTestCase):
await self.assertBalance(account2, '1.999717') await self.assertBalance(account2, '1.999717')
# verify that the outgoing support is marked correctly as is_tip=False in account2 # verify that the outgoing support is marked correctly as is_tip=False in account2
txs2 = (await self.out(self.daemon.jsonrpc_transaction_list(wallet_id='wallet2')))['items'] txs2 = await self.transaction_list(wallet_id='wallet2')
self.assertEqual(len(txs2[0]['support_info']), 1) self.assertEqual(len(txs2[0]['support_info']), 1)
self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-2.0') self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-2.0')
self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id) self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id)
@ -1539,7 +1665,7 @@ class SupportCommands(CommandTestCase):
# abandoning the tip increases balance and shows tip as spent # abandoning the tip increases balance and shows tip as spent
await self.support_abandon(claim_id) await self.support_abandon(claim_id)
await self.assertBalance(self.account, '4.979662') await self.assertBalance(self.account, '4.979662')
txs = (await self.out(self.daemon.jsonrpc_transaction_list(self.account.id)))['items'] txs = await self.transaction_list(account_id=self.account.id)
self.assertEqual(len(txs[0]['abandon_info']), 1) self.assertEqual(len(txs[0]['abandon_info']), 1)
self.assertEqual(len(txs[1]['support_info']), 1) self.assertEqual(len(txs[1]['support_info']), 1)
self.assertTrue(txs[1]['support_info'][0]['is_tip']) self.assertTrue(txs[1]['support_info'][0]['is_tip'])