diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 0f536e747..59f1b0750 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -3926,7 +3926,7 @@ class Daemon(metaclass=JSONRPCServerType): Options: --name= : (str or list) claim name --claim_id= : (str or list) claim id - --tips : (bool) only show tips (is_received=true) + --tips : (bool) only show tips --account_id= : (str) id of the account to query --wallet_id= : (str) restrict results to specific wallet --page= : (int) page to return during paginating @@ -3936,9 +3936,8 @@ class Daemon(metaclass=JSONRPCServerType): """ kwargs['type'] = 'support' kwargs['unspent'] = True - kwargs['include_is_received'] = True if tips is True: - kwargs['is_received'] = True + kwargs['is_my_output'] = True return self.jsonrpc_txo_list(*args, **kwargs) @requires(WALLET_COMPONENT) @@ -4114,8 +4113,9 @@ class Daemon(metaclass=JSONRPCServerType): 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_my_output=False, is_my_output=None, is_not_my_output=None, - include_is_my_input=False, is_my_input=None, is_not_my_input=None, + is_my_input_or_output=None, exclude_internal_transfers=False, + is_my_output=None, is_not_my_output=None, + is_my_input=None, is_not_my_input=None, wallet_id=None, page=None, page_size=None, resolve=False): """ List my transaction outputs. @@ -4123,23 +4123,31 @@ class Daemon(metaclass=JSONRPCServerType): Usage: txo_list [--account_id=] [--type=...] [--txid=...] [--claim_id=...] [--name=...] [--unspent] - [--include_is_received] [--is_received] [--is_not_received] - [--wallet_id=] [--include_is_received] [--is_received] - [--page=] [--page_size=] + [--is_my_input_or_output | + [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]] + ] + [--exclude_internal_transfers] + [--wallet_id=] [--page=] [--page_size=] [--resolve] Options: --type= : (str or list) claim type: stream, channel, support, purchase, collection, repost, other --txid= : (str or list) transaction id of outputs - --unspent : (bool) hide spent outputs, show only unspent ones --claim_id= : (str or list) claim id --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 + --unspent : (bool) hide spent outputs, show only unspent ones + --is_my_input_or_output : (bool) txos which have your inputs or your outputs, + if using this flag the other related flags + are ignored (--is_my_output, --is_my_input, etc) + --is_my_output : (bool) show outputs controlled by you + --is_not_my_output : (bool) show outputs not controlled by you + --is_my_input : (bool) show outputs created by you + --is_not_my_input : (bool) show outputs not created by you + --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination: + --is_my_input --is_my_output --type=other + this allows to exclude "change" payments, this + flag can be used in combination with any of the other flags --account_id= : (str) id of the account to query --wallet_id= : (str) restrict results to specific wallet --page= : (int) page to return during paginating @@ -4159,17 +4167,22 @@ class Daemon(metaclass=JSONRPCServerType): constraints = { 'resolve': resolve, 'unspent': unspent, - 'include_is_my_input': include_is_my_input, - 'include_is_my_output': include_is_my_output + 'exclude_internal_transfers': exclude_internal_transfers, + 'include_is_spent': True, + 'include_is_my_input': True, + 'include_is_my_output': True, } - if is_my_input is True: - constraints['is_my_input'] = True - elif is_not_my_input is True: - constraints['is_my_input'] = False - if is_my_output is True: - constraints['is_my_output'] = True - elif is_not_my_output is True: - constraints['is_my_output'] = False + if is_my_input_or_output is True: + constraints['is_my_input_or_output'] = True + else: + if is_my_input is True: + constraints['is_my_input'] = True + elif is_not_my_input is True: + constraints['is_my_input'] = False + if is_my_output is True: + constraints['is_my_output'] = True + elif is_not_my_output is True: + constraints['is_my_output'] = 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) diff --git a/lbry/extras/daemon/json_response_encoder.py b/lbry/extras/daemon/json_response_encoder.py index a3c9e318d..da87da92e 100644 --- a/lbry/extras/daemon/json_response_encoder.py +++ b/lbry/extras/daemon/json_response_encoder.py @@ -168,14 +168,14 @@ class JSONResponseEncoder(JSONEncoder): 'confirmations': (best_height+1) - tx_height if tx_height > 0 else tx_height, 'timestamp': self.ledger.headers[tx_height]['timestamp'] if 0 < tx_height <= best_height else None } - if txo.is_change is not None: - 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: output['is_spent'] = txo.is_spent - if txo.is_my_account is not None: - output['is_mine'] = txo.is_my_account + if txo.is_my_output is not None: + output['is_my_output'] = txo.is_my_output + if txo.is_my_input is not None: + output['is_my_input'] = txo.is_my_input + if txo.is_internal_transfer is not None: + output['is_internal_transfer'] = txo.is_internal_transfer if txo.script.is_claim_name: output['type'] = 'claim' diff --git a/lbry/wallet/account.py b/lbry/wallet/account.py index b7d4e29c5..5194210d5 100644 --- a/lbry/wallet/account.py +++ b/lbry/wallet/account.py @@ -437,7 +437,7 @@ class Account: async def get_addresses(self, read_only=False, **constraints) -> List[str]: rows = await self.ledger.db.select_addresses('address', read_only=read_only, accounts=[self], **constraints) - return [r[0] for r in rows] + return [r['address'] for r in rows] def get_address_records(self, **constraints): return self.ledger.db.get_addresses(accounts=[self], **constraints) diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 46ed6f57a..429c5d1c3 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -29,6 +29,7 @@ reader_context: Optional[ContextVar[ReaderProcessState]] = ContextVar('reader_co def initializer(path): db = sqlite3.connect(path) + db.row_factory = dict_row_factory db.executescript("pragma journal_mode=WAL;") reader = ReaderProcessState(db.cursor()) reader_context.set(reader) @@ -106,7 +107,7 @@ class AIOSQLite: return self.run(lambda conn: conn.executescript(script)) async def _execute_fetch(self, sql: str, parameters: Iterable = None, - read_only=False, fetch_all: bool = False) -> Iterable[sqlite3.Row]: + read_only=False, fetch_all: bool = False) -> List[dict]: read_only_fn = run_read_only_fetchall if fetch_all else run_read_only_fetchone parameters = parameters if parameters is not None else [] if read_only: @@ -120,11 +121,11 @@ class AIOSQLite: return await self.run(lambda conn: conn.execute(sql, parameters).fetchone()) async def execute_fetchall(self, sql: str, parameters: Iterable = None, - read_only=False) -> Iterable[sqlite3.Row]: + read_only=False) -> List[dict]: return await self._execute_fetch(sql, parameters, read_only, fetch_all=True) async def execute_fetchone(self, sql: str, parameters: Iterable = None, - read_only=False) -> Iterable[sqlite3.Row]: + read_only=False) -> List[dict]: return await self._execute_fetch(sql, parameters, read_only, fetch_all=False) def execute(self, sql: str, parameters: Iterable = None) -> Awaitable[sqlite3.Cursor]: @@ -294,13 +295,6 @@ def interpolate(sql, values): return sql -def rows_to_dict(rows, fields): - if rows: - return [dict(zip(fields, r)) for r in rows] - else: - return [] - - def constrain_single_or_list(constraints, column, value, convert=lambda x: x): if value is not None: if isinstance(value, list): @@ -384,6 +378,13 @@ class SQLiteMixin: return sql, values +def dict_row_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + + class Database(SQLiteMixin): SCHEMA_VERSION = "1.1" @@ -467,6 +468,10 @@ class Database(SQLiteMixin): CREATE_TXI_TABLE ) + async def open(self): + await super().open() + self.db.writer_connection.row_factory = dict_row_factory + def txo_to_row(self, tx, txo): row = { 'txid': tx.id, @@ -585,9 +590,13 @@ class Database(SQLiteMixin): *query(f"SELECT {cols} FROM tx", **constraints), read_only=read_only ) - TXO_NOT_MINE = Output(None, None, is_my_account=False) + TXO_NOT_MINE = Output(None, None, is_my_output=False) async def get_transactions(self, wallet=None, **constraints): + include_is_spent = constraints.pop('include_is_spent', False) + include_is_my_input = constraints.pop('include_is_my_input', False) + include_is_my_output = constraints.pop('include_is_my_output', False) + tx_rows = await self.select_transactions( 'txid, raw, height, position, is_verified', order_by=constraints.pop('order_by', ["height=0 DESC", "height DESC", "position DESC"]), @@ -599,9 +608,10 @@ class Database(SQLiteMixin): txids, txs, txi_txoids = [], [], [] for row in tx_rows: - txids.append(row[0]) + txids.append(row['txid']) txs.append(Transaction( - raw=row[1], height=row[2], position=row[3], is_verified=bool(row[4]) + raw=row['raw'], height=row['height'], position=row['position'], + is_verified=bool(row['is_verified']) )) for txi in txs[-1].inputs: txi_txoids.append(txi.txo_ref.id) @@ -614,6 +624,9 @@ class Database(SQLiteMixin): (await self.get_txos( wallet=wallet, txid__in=txids[offset:offset+step], + include_is_spent=include_is_spent, + include_is_my_input=include_is_my_input, + include_is_my_output=include_is_my_output, )) }) @@ -624,6 +637,7 @@ class Database(SQLiteMixin): (await self.get_txos( wallet=wallet, txoid__in=txi_txoids[offset:offset+step], + include_is_my_output=include_is_my_output, )) }) @@ -651,26 +665,31 @@ class Database(SQLiteMixin): constraints.pop('offset', None) constraints.pop('limit', None) constraints.pop('order_by', None) - count = await self.select_transactions('count(*)', **constraints) - return count[0][0] + count = await self.select_transactions('COUNT(*) as total', **constraints) + return count[0]['total'] async def get_transaction(self, **constraints): txs = await self.get_transactions(limit=1, **constraints) if txs: return txs[0] - async def select_txos(self, cols, accounts=None, is_my_input=None, is_my_output=True, read_only=True, **constraints): - if 'txoid' in constraints: - constraints['txo.txoid'] = constraints.pop('txoid') - if 'txoid__in' in constraints: - constraints['txo.txoid__in'] = constraints.pop('txoid__in') + async def select_txos( + self, cols, accounts=None, is_my_input=None, is_my_output=True, + is_my_input_or_output=None, exclude_internal_transfers=False, + include_is_spent=False, include_is_my_input=False, + read_only=True, **constraints): + for rename_col in ('txid', 'txoid'): + for rename_constraint in (rename_col, rename_col+'__in', rename_col+'__not_in'): + if rename_constraint in constraints: + constraints['txo.'+rename_constraint] = constraints.pop(rename_constraint) if accounts: account_in_sql, values = constraints_to_sql({ '$$account__in': [a.public_key.address for a in accounts] }) my_addresses = f"SELECT address FROM account_address WHERE {account_in_sql}" constraints.update(values) - if is_my_input is True and is_my_output is True: # special case + if is_my_input_or_output: + include_is_my_input = True constraints['received_or_sent__or'] = { 'txo.address__in': my_addresses, 'sent__and': { @@ -679,85 +698,116 @@ class Database(SQLiteMixin): } } else: - if is_my_output is True: + if is_my_output: constraints['txo.address__in'] = my_addresses elif is_my_output is False: constraints['txo.address__not_in'] = my_addresses - if is_my_input is True: + if is_my_input: + include_is_my_input = True constraints['txi.address__is_not_null'] = True constraints['txi.address__in'] = my_addresses elif is_my_input is False: + include_is_my_input = True constraints['is_my_input_false__or'] = { 'txi.address__is_null': True, 'txi.address__not_in': my_addresses } - sql = f""" - SELECT {cols} FROM txo - JOIN tx ON (tx.txid=txo.txid) - LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid) - LEFT JOIN txi AS spent ON (spent.txoid=txo.txoid) - """ - return await self.db.execute_fetchall(*query(sql, **constraints), read_only=read_only) + if exclude_internal_transfers: + include_is_my_input = True + constraints['exclude_internal_payments__or'] = { + 'txo.txo_type__not': TXO_TYPES['other'], + 'txi.address__is_null': True, + 'txi.address__not_in': my_addresses + } + sql = [f"SELECT {cols} FROM txo JOIN tx ON (tx.txid=txo.txid)"] + if include_is_spent: + sql.append("LEFT JOIN txi AS spent ON (spent.txoid=txo.txoid)") + if include_is_my_input: + sql.append("LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid)") + return await self.db.execute_fetchall(*query(' '.join(sql), **constraints), read_only=read_only) @staticmethod def constrain_unspent(constraints): constraints['is_reserved'] = False - constraints['txoid__not_in'] = "SELECT txoid FROM txi" + constraints['include_is_spent'] = True + constraints['spent.txoid__is_null'] = True + + async def get_txos(self, wallet=None, no_tx=False, unspent=False, read_only=False, **constraints): - async def get_txos( - self, wallet=None, no_tx=False, unspent=False, - include_is_my_input=False, include_is_my_output=False, - read_only=False, **constraints): if unspent: self.constrain_unspent(constraints) + + include_is_spent = constraints.get('include_is_spent', False) + include_is_my_input = constraints.get('include_is_my_input', False) + include_is_my_output = constraints.pop('include_is_my_output', False) + + select_columns = [ + "tx.txid, raw, tx.height, tx.position as tx_position, tx.is_verified, " + "txo_type, txo.position as txo_position, amount, script" + ] + my_accounts = {a.public_key.address for a in wallet.accounts} if wallet else set() - is_my_input_column = "" - if include_is_my_input and my_accounts: - account_in_wallet_sql, values = constraints_to_sql({'$$account__in#_wallet': my_accounts}) - is_my_input_column = f""", ( - txi.address IS NULL AND - txi.address IN (SELECT address FROM account_address WHERE {account_in_wallet_sql}) - ) - """ + my_accounts_sql = "" + if include_is_my_output or include_is_my_input: + my_accounts_sql, values = constraints_to_sql({'$$account__in#_wallet': my_accounts}) constraints.update(values) + + if include_is_my_output and my_accounts: + if constraints.get('is_my_output', None) in (True, False): + select_columns.append(f"{1 if constraints['is_my_output'] else 0} AS is_my_output") + else: + select_columns.append(f"""( + txo.address IN (SELECT address FROM account_address WHERE {my_accounts_sql}) + ) AS is_my_output""") + + if include_is_my_input and my_accounts: + if constraints.get('is_my_input', None) in (True, False): + select_columns.append(f"{1 if constraints['is_my_input'] else 0} AS is_my_input") + else: + select_columns.append(f"""( + txi.address IS NOT NULL AND + txi.address IN (SELECT address FROM account_address WHERE {my_accounts_sql}) + ) AS is_my_input + """) + + if include_is_spent: + select_columns.append("spent.txoid IS NOT NULL AS is_spent") + if 'order_by' not in constraints: constraints['order_by'] = [ "tx.height=0 DESC", "tx.height DESC", "tx.position DESC", "txo.position" ] - rows = await self.select_txos( - f""" - tx.txid, raw, tx.height, tx.position, tx.is_verified, txo.position, amount, script, ( - select group_concat(account||"|"||chain) from account_address - where account_address.address=txo.address - ), spent.txoid IS NOT NULL {is_my_input_column} - """, read_only=read_only, **constraints - ) + + rows = await self.select_txos(', '.join(select_columns), read_only=read_only, **constraints) + txos = [] txs = {} for row in rows: if no_tx: txo = Output( - amount=row[6], - script=OutputScript(row[7]), - tx_ref=TXRefImmutable.from_id(row[0], row[2]), - position=row[5] + amount=row['amount'], + script=OutputScript(row['script']), + tx_ref=TXRefImmutable.from_id(row['txid'], row['height']), + position=row['txo_position'] ) else: - if row[0] not in txs: - txs[row[0]] = Transaction( - row[1], height=row[2], position=row[3], is_verified=row[4] + if row['txid'] not in txs: + txs[row['txid']] = Transaction( + row['raw'], height=row['height'], position=row['tx_position'], + is_verified=bool(row['is_verified']) ) - txo = txs[row[0]].outputs[row[5]] - row_accounts = dict(a.split('|') for a in row[8].split(',')) - account_match = set(row_accounts) & my_accounts - txo.is_spent = bool(row[9]) - if include_is_my_input and my_accounts: - txo.is_received = not bool(row[10]) - if account_match: - txo.is_my_account = True - txo.is_change = row_accounts[account_match.pop()] == '1' - else: - txo.is_change = txo.is_my_account = False + txo = txs[row['txid']].outputs[row['txo_position']] + if include_is_spent: + txo.is_spent = bool(row['is_spent']) + if include_is_my_input: + txo.is_my_input = bool(row['is_my_input']) + if include_is_my_output: + txo.is_my_output = bool(row['is_my_output']) + if include_is_my_input and include_is_my_output: + if txo.is_my_input and txo.is_my_output and row['txo_type'] == TXO_TYPES['other']: + txo.is_internal_transfer = True + else: + txo.is_internal_transfer = False txos.append(txo) channel_ids = set() @@ -799,8 +849,8 @@ class Database(SQLiteMixin): constraints.pop('order_by', None) if unspent: self.constrain_unspent(constraints) - count = await self.select_txos('count(*)', **constraints) - return count[0][0] + count = await self.select_txos('COUNT(*) as total', **constraints) + return count[0]['total'] def get_utxos(self, read_only=False, **constraints): return self.get_txos(unspent=True, read_only=read_only, **constraints) @@ -813,8 +863,8 @@ class Database(SQLiteMixin): "'wallet' or 'accounts' constraints required to calculate balance" constraints['accounts'] = accounts or wallet.accounts self.constrain_unspent(constraints) - balance = await self.select_txos('SUM(amount)', read_only=read_only, **constraints) - return balance[0][0] or 0 + balance = await self.select_txos('SUM(amount) as total', read_only=read_only, **constraints) + return balance[0]['total'] or 0 async def select_addresses(self, cols, read_only=False, **constraints): return await self.db.execute_fetchall(*query( @@ -827,7 +877,7 @@ class Database(SQLiteMixin): 'address', 'account', 'chain', 'history', 'used_times', 'pubkey', 'chain_code', 'n', 'depth' ) - addresses = rows_to_dict(await self.select_addresses(', '.join(cols), read_only=read_only, **constraints), cols) + addresses = await self.select_addresses(', '.join(cols), read_only=read_only, **constraints) if 'pubkey' in cols: for address in addresses: address['pubkey'] = PubKey( @@ -837,8 +887,8 @@ class Database(SQLiteMixin): return addresses async def get_address_count(self, cols=None, read_only=False, **constraints): - count = await self.select_addresses('count(*)', read_only=read_only, **constraints) - return count[0][0] + count = await self.select_addresses('COUNT(*) as total', read_only=read_only, **constraints) + return count[0]['total'] async def get_address(self, read_only=False, **constraints): addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints) diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index 64ac744c0..5c1f8d902 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -826,7 +826,10 @@ class Ledger(metaclass=LedgerRegistry): return self.db.get_support_count(**constraints) async def get_transaction_history(self, read_only=False, **constraints): - txs: List[Transaction] = await self.db.get_transactions(read_only=read_only, **constraints) + txs: List[Transaction] = await self.db.get_transactions( + include_is_my_output=True, include_is_spent=True, + read_only=read_only, **constraints + ) headers = self.headers history = [] for tx in txs: # pylint: disable=too-many-nested-blocks @@ -842,7 +845,7 @@ class Ledger(metaclass=LedgerRegistry): 'abandon_info': [], 'purchase_info': [] } - is_my_inputs = all([txi.is_my_account for txi in tx.inputs]) + is_my_inputs = all([txi.is_my_input 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) diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index cfd637687..264357f56 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -159,11 +159,11 @@ class Input(InputOutput): return self.txo_ref.txo.amount @property - def is_my_account(self) -> Optional[bool]: + def is_my_input(self) -> Optional[bool]: """ True if the output this input spends is yours. """ if self.txo_ref.txo is None: return False - return self.txo_ref.txo.is_my_account + return self.txo_ref.txo.is_my_output @classmethod def deserialize_from(cls, stream): @@ -207,7 +207,7 @@ class OutputEffectiveAmountEstimator: class Output(InputOutput): __slots__ = ( - 'amount', 'script', 'is_change', 'is_spent', 'is_received', 'is_my_account', + 'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input', 'channel', 'private_key', 'meta', 'purchase', 'purchased_claim', 'purchase_receipt', 'reposted_claim', 'claims', @@ -215,17 +215,17 @@ class Output(InputOutput): def __init__(self, amount: int, script: OutputScript, tx_ref: TXRef = None, position: int = None, - is_change: Optional[bool] = None, is_spent: Optional[bool] = None, - is_received: Optional[bool] = None, is_my_account: Optional[bool] = None, + is_internal_transfer: Optional[bool] = None, is_spent: Optional[bool] = None, + is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None, channel: Optional['Output'] = None, private_key: Optional[str] = None ) -> None: super().__init__(tx_ref, position) self.amount = amount self.script = script - self.is_change = is_change + self.is_internal_transfer = is_internal_transfer self.is_spent = is_spent - self.is_received = is_received - self.is_my_account = is_my_account + self.is_my_output = is_my_output + self.is_my_input = is_my_input self.channel = channel self.private_key = private_key self.purchase: 'Output' = None # txo containing purchase metadata @@ -235,15 +235,17 @@ class Output(InputOutput): self.claims: List['Output'] = None # resolved claims for collection self.meta = {} - def update_annotations(self, annotated): + def update_annotations(self, annotated: 'Output'): if annotated is None: - self.is_change = None + self.is_internal_transfer = None self.is_spent = None - self.is_my_account = None + self.is_my_output = None + self.is_my_input = None else: - self.is_change = annotated.is_change + self.is_internal_transfer = annotated.is_internal_transfer self.is_spent = annotated.is_spent - self.is_my_account = annotated.is_my_account + self.is_my_output = annotated.is_my_output + self.is_my_input = annotated.is_my_input self.channel = annotated.channel if annotated else None self.private_key = annotated.private_key if annotated else None @@ -592,21 +594,21 @@ class Transaction: for txi in self.inputs: if txi.txo_ref.txo is None: continue - if txi.is_my_account is None: - raise ValueError( - "Cannot access net_account_balance if inputs/outputs do not " - "have is_my_account set properly." - ) - if txi.is_my_account: + if txi.is_my_input is True: balance -= txi.amount - for txo in self.outputs: - if txo.is_my_account is None: + elif txi.is_my_input is None: raise ValueError( - "Cannot access net_account_balance if inputs/outputs do not " - "have is_my_account set properly." + "Cannot access net_account_balance if inputs do not " + "have is_my_input set properly." ) - if txo.is_my_account: + for txo in self.outputs: + if txo.is_my_output is True: balance += txo.amount + elif txo.is_my_output is None: + raise ValueError( + "Cannot access net_account_balance if outputs do not " + "have is_my_output set properly." + ) return balance @property @@ -751,7 +753,7 @@ class Transaction: change_hash160 = change_account.ledger.address_to_hash160(change_address) change_amount = change - cost_of_change change_output = Output.pay_pubkey_hash(change_amount, change_hash160) - change_output.is_change = True + change_output.is_internal_transfer = True tx.add_outputs([Output.pay_pubkey_hash(change_amount, change_hash160)]) if tx._outputs: @@ -856,17 +858,17 @@ class Transaction: @property def my_inputs(self): for txi in self.inputs: - if txi.txo_ref.txo is not None and txi.txo_ref.txo.is_my_account: + if txi.txo_ref.txo is not None and txi.txo_ref.txo.is_my_output: yield txi def _filter_my_outputs(self, f): for txo in self.outputs: - if txo.is_my_account and f(txo.script): + if txo.is_my_output and f(txo.script): yield txo def _filter_other_outputs(self, f): for txo in self.outputs: - if not txo.is_my_account and f(txo.script): + if not txo.is_my_output and f(txo.script): yield txo def _filter_any_outputs(self, f): @@ -898,7 +900,7 @@ class Transaction: def my_abandon_outputs(self): for txi in self.inputs: abandon = txi.txo_ref.txo - if abandon is not None and abandon.is_my_account and abandon.script.is_claim_involved: + if abandon is not None and abandon.is_my_output and abandon.script.is_claim_involved: is_update = False if abandon.script.is_claim_name or abandon.script.is_update_claim: for update in self.my_update_outputs: diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index a99106751..9d33236a1 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -480,25 +480,69 @@ class TransactionOutputCommands(ClaimTestCase): self.assertTrue(r[0]['is_spent']) self.assertTrue(r[1]['is_spent']) - async def test_txo_list_received_filtering(self): + async def test_txo_list_my_input_output_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) + await self.channel_create('@kept-channel') + await self.channel_create('@sent-channel', 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]) + # all txos on second wallet + received_channel, = await self.txo_list(wallet_id=wallet2.id, is_my_input_or_output=True) + self.assertEqual('1.0', received_channel['amount']) + self.assertFalse(received_channel['is_my_input']) + self.assertTrue(received_channel['is_my_output']) + self.assertFalse(received_channel['is_internal_transfer']) - r = await self.txo_list(include_is_received=True, wallet_id=wallet2.id) - self.assertEqual(1, len(r)) - self.assertTrue(r[0]['is_received']) + # all txos on default wallet + r = await self.txo_list(is_my_input_or_output=True) + self.assertEqual( + ['1.0', '7.947786', '1.0', '8.973893', '10.0'], + [t['amount'] for t in r] + ) + + sent_channel, change2, kept_channel, change1, initial_funds = r + + self.assertTrue(sent_channel['is_my_input']) + self.assertFalse(sent_channel['is_my_output']) + self.assertFalse(sent_channel['is_internal_transfer']) + self.assertTrue(change2['is_my_input']) + self.assertTrue(change2['is_my_output']) + self.assertTrue(change2['is_internal_transfer']) + + self.assertTrue(kept_channel['is_my_input']) + self.assertTrue(kept_channel['is_my_output']) + self.assertFalse(kept_channel['is_internal_transfer']) + self.assertTrue(change1['is_my_input']) + self.assertTrue(change1['is_my_output']) + self.assertTrue(change1['is_internal_transfer']) + + self.assertFalse(initial_funds['is_my_input']) + self.assertTrue(initial_funds['is_my_output']) + self.assertFalse(initial_funds['is_internal_transfer']) + + # my stuff and stuff i sent excluding "change" + r = await self.txo_list(is_my_input_or_output=True, exclude_internal_transfers=True) + self.assertEqual([sent_channel, kept_channel, initial_funds], r) + + # my unspent stuff and stuff i sent excluding "change" + r = await self.txo_list(is_my_input_or_output=True, unspent=True, exclude_internal_transfers=True) + self.assertEqual([sent_channel, kept_channel], r) + + # only "change" + r = await self.txo_list(is_my_input=True, is_my_output=True, type="other") + self.assertEqual([change2, change1], r) + + # only unspent "change" + r = await self.txo_list(is_my_input=True, is_my_output=True, type="other", unspent=True) + self.assertEqual([change2], r) + + # all my unspent stuff + r = await self.txo_list(is_my_output=True, unspent=True) + self.assertEqual([change2, kept_channel], r) + + # stuff i sent + r = await self.txo_list(is_not_my_output=True) + self.assertEqual([sent_channel], r) class ClaimCommands(ClaimTestCase): @@ -617,7 +661,7 @@ class ClaimCommands(ClaimTestCase): self.assertTrue(r[1]['meta']['is_controlling']) # check that metadata is transfered - self.assertTrue(r[0]['is_mine']) + self.assertTrue(r[0]['is_my_output']) class ChannelCommands(CommandTestCase): diff --git a/tests/unit/wallet/test_database.py b/tests/unit/wallet/test_database.py index 50a40ad91..68d67515e 100644 --- a/tests/unit/wallet/test_database.py +++ b/tests/unit/wallet/test_database.py @@ -355,29 +355,29 @@ class TestQueries(AsyncioTestCase): txs = await self.ledger.db.get_transactions(wallet=wallet1, accounts=wallet1.accounts) self.assertListEqual([tx2.id, tx1.id], [tx.id for tx in txs]) - self.assertEqual(txs[0].inputs[0].is_my_account, True) - self.assertEqual(txs[0].outputs[0].is_my_account, False) - self.assertEqual(txs[1].inputs[0].is_my_account, False) - self.assertEqual(txs[1].outputs[0].is_my_account, True) + self.assertEqual(txs[0].inputs[0].is_my_input, True) + self.assertEqual(txs[0].outputs[0].is_my_output, False) + self.assertEqual(txs[1].inputs[0].is_my_input, False) + self.assertEqual(txs[1].outputs[0].is_my_output, True) txs = await self.ledger.db.get_transactions(wallet=wallet2, accounts=[account2]) self.assertListEqual([tx3.id, tx2.id], [tx.id for tx in txs]) - self.assertEqual(txs[0].inputs[0].is_my_account, True) - self.assertEqual(txs[0].outputs[0].is_my_account, False) - self.assertEqual(txs[1].inputs[0].is_my_account, False) - self.assertEqual(txs[1].outputs[0].is_my_account, True) + self.assertEqual(txs[0].inputs[0].is_my_input, True) + self.assertEqual(txs[0].outputs[0].is_my_output, False) + self.assertEqual(txs[1].inputs[0].is_my_input, False) + self.assertEqual(txs[1].outputs[0].is_my_output, True) self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account2])) tx = await self.ledger.db.get_transaction(txid=tx2.id) self.assertEqual(tx.id, tx2.id) - self.assertFalse(tx.inputs[0].is_my_account) - self.assertFalse(tx.outputs[0].is_my_account) + self.assertFalse(tx.inputs[0].is_my_input) + self.assertFalse(tx.outputs[0].is_my_output) tx = await self.ledger.db.get_transaction(wallet=wallet1, txid=tx2.id) - self.assertTrue(tx.inputs[0].is_my_account) - self.assertFalse(tx.outputs[0].is_my_account) + self.assertTrue(tx.inputs[0].is_my_input) + self.assertFalse(tx.outputs[0].is_my_output) tx = await self.ledger.db.get_transaction(wallet=wallet2, txid=tx2.id) - self.assertFalse(tx.inputs[0].is_my_account) - self.assertTrue(tx.outputs[0].is_my_account) + self.assertFalse(tx.inputs[0].is_my_input) + self.assertTrue(tx.outputs[0].is_my_output) # height 0 sorted to the top with the rest in descending order tx4 = await self.create_tx_from_nothing(account1, 0) diff --git a/tests/unit/wallet/test_transaction.py b/tests/unit/wallet/test_transaction.py index a876a12b9..7c0942cd0 100644 --- a/tests/unit/wallet/test_transaction.py +++ b/tests/unit/wallet/test_transaction.py @@ -82,14 +82,14 @@ class TestSizeAndFeeEstimation(AsyncioTestCase): class TestAccountBalanceImpactFromTransaction(unittest.TestCase): - def test_is_my_account_not_set(self): + def test_is_my_output_not_set(self): tx = get_transaction() with self.assertRaisesRegex(ValueError, "Cannot access net_account_balance"): _ = tx.net_account_balance - tx.inputs[0].txo_ref.txo.is_my_account = True + tx.inputs[0].txo_ref.txo.is_my_output = True with self.assertRaisesRegex(ValueError, "Cannot access net_account_balance"): _ = tx.net_account_balance - tx.outputs[0].is_my_account = True + tx.outputs[0].is_my_output = True # all inputs/outputs are set now so it should work _ = tx.net_account_balance @@ -98,9 +98,9 @@ class TestAccountBalanceImpactFromTransaction(unittest.TestCase): .add_inputs([get_input(300*CENT)]) \ .add_outputs([get_output(190*CENT, NULL_HASH), get_output(100*CENT, NULL_HASH)]) - tx.inputs[0].txo_ref.txo.is_my_account = True - tx.outputs[0].is_my_account = False - tx.outputs[1].is_my_account = True + tx.inputs[0].txo_ref.txo.is_my_output = True + tx.outputs[0].is_my_output = False + tx.outputs[1].is_my_output = True self.assertEqual(tx.net_account_balance, -200*CENT) def test_paying_from_other_account_to_my_account(self): @@ -108,9 +108,9 @@ class TestAccountBalanceImpactFromTransaction(unittest.TestCase): .add_inputs([get_input(300*CENT)]) \ .add_outputs([get_output(190*CENT, NULL_HASH), get_output(100*CENT, NULL_HASH)]) - tx.inputs[0].txo_ref.txo.is_my_account = False - tx.outputs[0].is_my_account = True - tx.outputs[1].is_my_account = False + tx.inputs[0].txo_ref.txo.is_my_output = False + tx.outputs[0].is_my_output = True + tx.outputs[1].is_my_output = False self.assertEqual(tx.net_account_balance, 190*CENT) def test_paying_from_my_account_to_my_account(self): @@ -118,9 +118,9 @@ class TestAccountBalanceImpactFromTransaction(unittest.TestCase): .add_inputs([get_input(300*CENT)]) \ .add_outputs([get_output(190*CENT, NULL_HASH), get_output(100*CENT, NULL_HASH)]) - tx.inputs[0].txo_ref.txo.is_my_account = True - tx.outputs[0].is_my_account = True - tx.outputs[1].is_my_account = True + tx.inputs[0].txo_ref.txo.is_my_output = True + tx.outputs[0].is_my_output = True + tx.outputs[1].is_my_output = True self.assertEqual(tx.net_account_balance, -10*CENT) # lost to fee