working --is_my_input_or_output --is_my_input --is_my_output etc

This commit is contained in:
Lex Berezhny 2020-03-20 01:11:05 -04:00
parent af2f2282c2
commit dd21803598
9 changed files with 292 additions and 180 deletions

View file

@ -3926,7 +3926,7 @@ class Daemon(metaclass=JSONRPCServerType):
Options: Options:
--name=<name> : (str or list) claim name --name=<name> : (str or list) claim name
--claim_id=<claim_id> : (str or list) claim id --claim_id=<claim_id> : (str or list) claim id
--tips : (bool) only show tips (is_received=true) --tips : (bool) only show tips
--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
@ -3936,9 +3936,8 @@ class Daemon(metaclass=JSONRPCServerType):
""" """
kwargs['type'] = 'support' kwargs['type'] = 'support'
kwargs['unspent'] = True kwargs['unspent'] = True
kwargs['include_is_received'] = True
if tips is True: if tips is True:
kwargs['is_received'] = True kwargs['is_my_output'] = True
return self.jsonrpc_txo_list(*args, **kwargs) return self.jsonrpc_txo_list(*args, **kwargs)
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
@ -4114,8 +4113,9 @@ class Daemon(metaclass=JSONRPCServerType):
def jsonrpc_txo_list( def jsonrpc_txo_list(
self, account_id=None, type=None, txid=None, # pylint: disable=redefined-builtin self, account_id=None, type=None, txid=None, # pylint: disable=redefined-builtin
claim_id=None, name=None, unspent=False, claim_id=None, name=None, unspent=False,
include_is_my_output=False, is_my_output=None, is_not_my_output=None, is_my_input_or_output=None, exclude_internal_transfers=False,
include_is_my_input=False, is_my_input=None, is_not_my_input=None, 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): wallet_id=None, page=None, page_size=None, resolve=False):
""" """
List my transaction outputs. List my transaction outputs.
@ -4123,23 +4123,31 @@ class Daemon(metaclass=JSONRPCServerType):
Usage: Usage:
txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]
[--claim_id=<claim_id>...] [--name=<name>...] [--unspent] [--claim_id=<claim_id>...] [--name=<name>...] [--unspent]
[--include_is_received] [--is_received] [--is_not_received] [--is_my_input_or_output |
[--wallet_id=<wallet_id>] [--include_is_received] [--is_received] [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]
[--page=<page>] [--page_size=<page_size>] ]
[--exclude_internal_transfers]
[--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]
[--resolve] [--resolve]
Options: Options:
--type=<type> : (str or list) claim type: stream, channel, support, --type=<type> : (str or list) claim type: stream, channel, support,
purchase, collection, repost, other purchase, collection, repost, other
--txid=<txid> : (str or list) transaction id of outputs --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 --claim_id=<claim_id> : (str or list) claim id
--name=<name> : (str or list) claim name --name=<name> : (str or list) claim name
--include_is_received : (bool) calculate the is_received property and --unspent : (bool) hide spent outputs, show only unspent ones
include in output, this happens automatically if you --is_my_input_or_output : (bool) txos which have your inputs or your outputs,
use the --is_received or --is_not_received filters if using this flag the other related flags
--is_received : (bool) only return txos sent from others to this account are ignored (--is_my_output, --is_my_input, etc)
--is_not_received : (bool) only return txos created by this account --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=<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
@ -4159,17 +4167,22 @@ class Daemon(metaclass=JSONRPCServerType):
constraints = { constraints = {
'resolve': resolve, 'resolve': resolve,
'unspent': unspent, 'unspent': unspent,
'include_is_my_input': include_is_my_input, 'exclude_internal_transfers': exclude_internal_transfers,
'include_is_my_output': include_is_my_output 'include_is_spent': True,
'include_is_my_input': True,
'include_is_my_output': True,
} }
if is_my_input is True: if is_my_input_or_output is True:
constraints['is_my_input'] = True constraints['is_my_input_or_output'] = True
elif is_not_my_input is True: else:
constraints['is_my_input'] = False if is_my_input is True:
if is_my_output is True: constraints['is_my_input'] = True
constraints['is_my_output'] = True elif is_not_my_input is True:
elif is_not_my_output is True: constraints['is_my_input'] = False
constraints['is_my_output'] = 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, '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_id', claim_id)
database.constrain_single_or_list(constraints, 'claim_name', name) database.constrain_single_or_list(constraints, 'claim_name', name)

View file

@ -168,14 +168,14 @@ class JSONResponseEncoder(JSONEncoder):
'confirmations': (best_height+1) - tx_height if tx_height > 0 else tx_height, '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 '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: 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_output is not None:
output['is_mine'] = txo.is_my_account 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: if txo.script.is_claim_name:
output['type'] = 'claim' output['type'] = 'claim'

View file

@ -437,7 +437,7 @@ class Account:
async def get_addresses(self, read_only=False, **constraints) -> List[str]: 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) 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): def get_address_records(self, **constraints):
return self.ledger.db.get_addresses(accounts=[self], **constraints) return self.ledger.db.get_addresses(accounts=[self], **constraints)

View file

@ -29,6 +29,7 @@ reader_context: Optional[ContextVar[ReaderProcessState]] = ContextVar('reader_co
def initializer(path): def initializer(path):
db = sqlite3.connect(path) db = sqlite3.connect(path)
db.row_factory = dict_row_factory
db.executescript("pragma journal_mode=WAL;") db.executescript("pragma journal_mode=WAL;")
reader = ReaderProcessState(db.cursor()) reader = ReaderProcessState(db.cursor())
reader_context.set(reader) reader_context.set(reader)
@ -106,7 +107,7 @@ class AIOSQLite:
return self.run(lambda conn: conn.executescript(script)) return self.run(lambda conn: conn.executescript(script))
async def _execute_fetch(self, sql: str, parameters: Iterable = None, 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 read_only_fn = run_read_only_fetchall if fetch_all else run_read_only_fetchone
parameters = parameters if parameters is not None else [] parameters = parameters if parameters is not None else []
if read_only: if read_only:
@ -120,11 +121,11 @@ class AIOSQLite:
return await self.run(lambda conn: conn.execute(sql, parameters).fetchone()) return await self.run(lambda conn: conn.execute(sql, parameters).fetchone())
async def execute_fetchall(self, sql: str, parameters: Iterable = None, 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) return await self._execute_fetch(sql, parameters, read_only, fetch_all=True)
async def execute_fetchone(self, sql: str, parameters: Iterable = None, 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) return await self._execute_fetch(sql, parameters, read_only, fetch_all=False)
def execute(self, sql: str, parameters: Iterable = None) -> Awaitable[sqlite3.Cursor]: def execute(self, sql: str, parameters: Iterable = None) -> Awaitable[sqlite3.Cursor]:
@ -294,13 +295,6 @@ def interpolate(sql, values):
return sql 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): def constrain_single_or_list(constraints, column, value, convert=lambda x: x):
if value is not None: if value is not None:
if isinstance(value, list): if isinstance(value, list):
@ -384,6 +378,13 @@ class SQLiteMixin:
return sql, values 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): class Database(SQLiteMixin):
SCHEMA_VERSION = "1.1" SCHEMA_VERSION = "1.1"
@ -467,6 +468,10 @@ class Database(SQLiteMixin):
CREATE_TXI_TABLE 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): def txo_to_row(self, tx, txo):
row = { row = {
'txid': tx.id, 'txid': tx.id,
@ -585,9 +590,13 @@ class Database(SQLiteMixin):
*query(f"SELECT {cols} FROM tx", **constraints), read_only=read_only *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): 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( tx_rows = await self.select_transactions(
'txid, raw, height, position, is_verified', 'txid, raw, height, position, is_verified',
order_by=constraints.pop('order_by', ["height=0 DESC", "height DESC", "position DESC"]), order_by=constraints.pop('order_by', ["height=0 DESC", "height DESC", "position DESC"]),
@ -599,9 +608,10 @@ class Database(SQLiteMixin):
txids, txs, txi_txoids = [], [], [] txids, txs, txi_txoids = [], [], []
for row in tx_rows: for row in tx_rows:
txids.append(row[0]) txids.append(row['txid'])
txs.append(Transaction( 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: for txi in txs[-1].inputs:
txi_txoids.append(txi.txo_ref.id) txi_txoids.append(txi.txo_ref.id)
@ -614,6 +624,9 @@ class Database(SQLiteMixin):
(await self.get_txos( (await self.get_txos(
wallet=wallet, wallet=wallet,
txid__in=txids[offset:offset+step], 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( (await self.get_txos(
wallet=wallet, wallet=wallet,
txoid__in=txi_txoids[offset:offset+step], 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('offset', None)
constraints.pop('limit', None) constraints.pop('limit', None)
constraints.pop('order_by', None) constraints.pop('order_by', None)
count = await self.select_transactions('count(*)', **constraints) count = await self.select_transactions('COUNT(*) as total', **constraints)
return count[0][0] return count[0]['total']
async def get_transaction(self, **constraints): async def get_transaction(self, **constraints):
txs = await self.get_transactions(limit=1, **constraints) txs = await self.get_transactions(limit=1, **constraints)
if txs: if txs:
return txs[0] return txs[0]
async def select_txos(self, cols, accounts=None, is_my_input=None, is_my_output=True, read_only=True, **constraints): async def select_txos(
if 'txoid' in constraints: self, cols, accounts=None, is_my_input=None, is_my_output=True,
constraints['txo.txoid'] = constraints.pop('txoid') is_my_input_or_output=None, exclude_internal_transfers=False,
if 'txoid__in' in constraints: include_is_spent=False, include_is_my_input=False,
constraints['txo.txoid__in'] = constraints.pop('txoid__in') 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: if accounts:
account_in_sql, values = constraints_to_sql({ account_in_sql, values = constraints_to_sql({
'$$account__in': [a.public_key.address for a in accounts] '$$account__in': [a.public_key.address for a in accounts]
}) })
my_addresses = f"SELECT address FROM account_address WHERE {account_in_sql}" my_addresses = f"SELECT address FROM account_address WHERE {account_in_sql}"
constraints.update(values) 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'] = { constraints['received_or_sent__or'] = {
'txo.address__in': my_addresses, 'txo.address__in': my_addresses,
'sent__and': { 'sent__and': {
@ -679,85 +698,116 @@ class Database(SQLiteMixin):
} }
} }
else: else:
if is_my_output is True: if is_my_output:
constraints['txo.address__in'] = my_addresses constraints['txo.address__in'] = my_addresses
elif is_my_output is False: elif is_my_output is False:
constraints['txo.address__not_in'] = my_addresses 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__is_not_null'] = True
constraints['txi.address__in'] = my_addresses constraints['txi.address__in'] = my_addresses
elif is_my_input is False: elif is_my_input is False:
include_is_my_input = True
constraints['is_my_input_false__or'] = { constraints['is_my_input_false__or'] = {
'txi.address__is_null': True, 'txi.address__is_null': True,
'txi.address__not_in': my_addresses 'txi.address__not_in': my_addresses
} }
sql = f""" if exclude_internal_transfers:
SELECT {cols} FROM txo include_is_my_input = True
JOIN tx ON (tx.txid=txo.txid) constraints['exclude_internal_payments__or'] = {
LEFT JOIN txi ON (txi.position=0 AND txi.txid=txo.txid) 'txo.txo_type__not': TXO_TYPES['other'],
LEFT JOIN txi AS spent ON (spent.txoid=txo.txoid) 'txi.address__is_null': True,
""" 'txi.address__not_in': my_addresses
return await self.db.execute_fetchall(*query(sql, **constraints), read_only=read_only) }
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 @staticmethod
def constrain_unspent(constraints): def constrain_unspent(constraints):
constraints['is_reserved'] = False 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: if unspent:
self.constrain_unspent(constraints) 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() my_accounts = {a.public_key.address for a in wallet.accounts} if wallet else set()
is_my_input_column = "" my_accounts_sql = ""
if include_is_my_input and my_accounts: if include_is_my_output or include_is_my_input:
account_in_wallet_sql, values = constraints_to_sql({'$$account__in#_wallet': my_accounts}) my_accounts_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})
)
"""
constraints.update(values) 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: if 'order_by' not in constraints:
constraints['order_by'] = [ constraints['order_by'] = [
"tx.height=0 DESC", "tx.height DESC", "tx.position DESC", "txo.position" "tx.height=0 DESC", "tx.height DESC", "tx.position DESC", "txo.position"
] ]
rows = await self.select_txos(
f""" rows = await self.select_txos(', '.join(select_columns), read_only=read_only, **constraints)
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
)
txos = [] txos = []
txs = {} txs = {}
for row in rows: for row in rows:
if no_tx: if no_tx:
txo = Output( txo = Output(
amount=row[6], amount=row['amount'],
script=OutputScript(row[7]), script=OutputScript(row['script']),
tx_ref=TXRefImmutable.from_id(row[0], row[2]), tx_ref=TXRefImmutable.from_id(row['txid'], row['height']),
position=row[5] position=row['txo_position']
) )
else: else:
if row[0] not in txs: if row['txid'] not in txs:
txs[row[0]] = Transaction( txs[row['txid']] = Transaction(
row[1], height=row[2], position=row[3], is_verified=row[4] row['raw'], height=row['height'], position=row['tx_position'],
is_verified=bool(row['is_verified'])
) )
txo = txs[row[0]].outputs[row[5]] txo = txs[row['txid']].outputs[row['txo_position']]
row_accounts = dict(a.split('|') for a in row[8].split(',')) if include_is_spent:
account_match = set(row_accounts) & my_accounts txo.is_spent = bool(row['is_spent'])
txo.is_spent = bool(row[9]) if include_is_my_input:
if include_is_my_input and my_accounts: txo.is_my_input = bool(row['is_my_input'])
txo.is_received = not bool(row[10]) if include_is_my_output:
if account_match: txo.is_my_output = bool(row['is_my_output'])
txo.is_my_account = True if include_is_my_input and include_is_my_output:
txo.is_change = row_accounts[account_match.pop()] == '1' if txo.is_my_input and txo.is_my_output and row['txo_type'] == TXO_TYPES['other']:
else: txo.is_internal_transfer = True
txo.is_change = txo.is_my_account = False else:
txo.is_internal_transfer = False
txos.append(txo) txos.append(txo)
channel_ids = set() channel_ids = set()
@ -799,8 +849,8 @@ class Database(SQLiteMixin):
constraints.pop('order_by', None) constraints.pop('order_by', None)
if unspent: if unspent:
self.constrain_unspent(constraints) self.constrain_unspent(constraints)
count = await self.select_txos('count(*)', **constraints) count = await self.select_txos('COUNT(*) as total', **constraints)
return count[0][0] return count[0]['total']
def get_utxos(self, read_only=False, **constraints): def get_utxos(self, read_only=False, **constraints):
return self.get_txos(unspent=True, read_only=read_only, **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" "'wallet' or 'accounts' constraints required to calculate balance"
constraints['accounts'] = accounts or wallet.accounts constraints['accounts'] = accounts or wallet.accounts
self.constrain_unspent(constraints) self.constrain_unspent(constraints)
balance = await self.select_txos('SUM(amount)', read_only=read_only, **constraints) balance = await self.select_txos('SUM(amount) as total', read_only=read_only, **constraints)
return balance[0][0] or 0 return balance[0]['total'] or 0
async def select_addresses(self, cols, read_only=False, **constraints): async def select_addresses(self, cols, read_only=False, **constraints):
return await self.db.execute_fetchall(*query( return await self.db.execute_fetchall(*query(
@ -827,7 +877,7 @@ class Database(SQLiteMixin):
'address', 'account', 'chain', 'history', 'used_times', 'address', 'account', 'chain', 'history', 'used_times',
'pubkey', 'chain_code', 'n', 'depth' '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: if 'pubkey' in cols:
for address in addresses: for address in addresses:
address['pubkey'] = PubKey( address['pubkey'] = PubKey(
@ -837,8 +887,8 @@ class Database(SQLiteMixin):
return addresses return addresses
async def get_address_count(self, cols=None, read_only=False, **constraints): async def get_address_count(self, cols=None, read_only=False, **constraints):
count = await self.select_addresses('count(*)', read_only=read_only, **constraints) count = await self.select_addresses('COUNT(*) as total', read_only=read_only, **constraints)
return count[0][0] return count[0]['total']
async def get_address(self, read_only=False, **constraints): async def get_address(self, read_only=False, **constraints):
addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints) addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints)

View file

@ -826,7 +826,10 @@ class Ledger(metaclass=LedgerRegistry):
return self.db.get_support_count(**constraints) return self.db.get_support_count(**constraints)
async def get_transaction_history(self, read_only=False, **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 headers = self.headers
history = [] history = []
for tx in txs: # pylint: disable=too-many-nested-blocks for tx in txs: # pylint: disable=too-many-nested-blocks
@ -842,7 +845,7 @@ class Ledger(metaclass=LedgerRegistry):
'abandon_info': [], 'abandon_info': [],
'purchase_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: if is_my_inputs:
# fees only matter if we are the ones paying them # fees only matter if we are the ones paying them
item['value'] = dewies_to_lbc(tx.net_account_balance+tx.fee) item['value'] = dewies_to_lbc(tx.net_account_balance+tx.fee)

View file

@ -159,11 +159,11 @@ class Input(InputOutput):
return self.txo_ref.txo.amount return self.txo_ref.txo.amount
@property @property
def is_my_account(self) -> Optional[bool]: def is_my_input(self) -> Optional[bool]:
""" True if the output this input spends is yours. """ """ True if the output this input spends is yours. """
if self.txo_ref.txo is None: if self.txo_ref.txo is None:
return False return False
return self.txo_ref.txo.is_my_account return self.txo_ref.txo.is_my_output
@classmethod @classmethod
def deserialize_from(cls, stream): def deserialize_from(cls, stream):
@ -207,7 +207,7 @@ class OutputEffectiveAmountEstimator:
class Output(InputOutput): class Output(InputOutput):
__slots__ = ( __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', 'channel', 'private_key', 'meta',
'purchase', 'purchased_claim', 'purchase_receipt', 'purchase', 'purchased_claim', 'purchase_receipt',
'reposted_claim', 'claims', 'reposted_claim', 'claims',
@ -215,17 +215,17 @@ 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_internal_transfer: Optional[bool] = None, is_spent: Optional[bool] = None,
is_received: Optional[bool] = None, is_my_account: Optional[bool] = None, is_my_output: Optional[bool] = None, is_my_input: 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)
self.amount = amount self.amount = amount
self.script = script self.script = script
self.is_change = is_change self.is_internal_transfer = is_internal_transfer
self.is_spent = is_spent self.is_spent = is_spent
self.is_received = is_received self.is_my_output = is_my_output
self.is_my_account = is_my_account self.is_my_input = is_my_input
self.channel = channel self.channel = channel
self.private_key = private_key self.private_key = private_key
self.purchase: 'Output' = None # txo containing purchase metadata 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.claims: List['Output'] = None # resolved claims for collection
self.meta = {} self.meta = {}
def update_annotations(self, annotated): def update_annotations(self, annotated: 'Output'):
if annotated is None: if annotated is None:
self.is_change = None self.is_internal_transfer = None
self.is_spent = None self.is_spent = None
self.is_my_account = None self.is_my_output = None
self.is_my_input = None
else: else:
self.is_change = annotated.is_change self.is_internal_transfer = annotated.is_internal_transfer
self.is_spent = annotated.is_spent 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.channel = annotated.channel if annotated else None
self.private_key = annotated.private_key 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: for txi in self.inputs:
if txi.txo_ref.txo is None: if txi.txo_ref.txo is None:
continue continue
if txi.is_my_account is None: if txi.is_my_input is True:
raise ValueError(
"Cannot access net_account_balance if inputs/outputs do not "
"have is_my_account set properly."
)
if txi.is_my_account:
balance -= txi.amount balance -= txi.amount
for txo in self.outputs: elif txi.is_my_input is None:
if txo.is_my_account is None:
raise ValueError( raise ValueError(
"Cannot access net_account_balance if inputs/outputs do not " "Cannot access net_account_balance if inputs do not "
"have is_my_account set properly." "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 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 return balance
@property @property
@ -751,7 +753,7 @@ class Transaction:
change_hash160 = change_account.ledger.address_to_hash160(change_address) change_hash160 = change_account.ledger.address_to_hash160(change_address)
change_amount = change - cost_of_change change_amount = change - cost_of_change
change_output = Output.pay_pubkey_hash(change_amount, change_hash160) 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)]) tx.add_outputs([Output.pay_pubkey_hash(change_amount, change_hash160)])
if tx._outputs: if tx._outputs:
@ -856,17 +858,17 @@ class Transaction:
@property @property
def my_inputs(self): def my_inputs(self):
for txi in self.inputs: 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 yield txi
def _filter_my_outputs(self, f): def _filter_my_outputs(self, f):
for txo in self.outputs: 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 yield txo
def _filter_other_outputs(self, f): def _filter_other_outputs(self, f):
for txo in self.outputs: 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 yield txo
def _filter_any_outputs(self, f): def _filter_any_outputs(self, f):
@ -898,7 +900,7 @@ class Transaction:
def my_abandon_outputs(self): def my_abandon_outputs(self):
for txi in self.inputs: for txi in self.inputs:
abandon = txi.txo_ref.txo 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 is_update = False
if abandon.script.is_claim_name or abandon.script.is_update_claim: if abandon.script.is_claim_name or abandon.script.is_update_claim:
for update in self.my_update_outputs: for update in self.my_update_outputs:

View file

@ -480,25 +480,69 @@ class TransactionOutputCommands(ClaimTestCase):
self.assertTrue(r[0]['is_spent']) self.assertTrue(r[0]['is_spent'])
self.assertTrue(r[1]['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) wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)
address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id) 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) # all txos on second wallet
self.assertEqual(2, len(r)) received_channel, = await self.txo_list(wallet_id=wallet2.id, is_my_input_or_output=True)
self.assertFalse(r[0]['is_received']) self.assertEqual('1.0', received_channel['amount'])
self.assertTrue(r[1]['is_received']) self.assertFalse(received_channel['is_my_input'])
rt = await self.txo_list(is_not_received=True) self.assertTrue(received_channel['is_my_output'])
self.assertEqual(1, len(rt)) self.assertFalse(received_channel['is_internal_transfer'])
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) # all txos on default wallet
self.assertEqual(1, len(r)) r = await self.txo_list(is_my_input_or_output=True)
self.assertTrue(r[0]['is_received']) 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): class ClaimCommands(ClaimTestCase):
@ -617,7 +661,7 @@ class ClaimCommands(ClaimTestCase):
self.assertTrue(r[1]['meta']['is_controlling']) self.assertTrue(r[1]['meta']['is_controlling'])
# check that metadata is transfered # check that metadata is transfered
self.assertTrue(r[0]['is_mine']) self.assertTrue(r[0]['is_my_output'])
class ChannelCommands(CommandTestCase): class ChannelCommands(CommandTestCase):

View file

@ -355,29 +355,29 @@ class TestQueries(AsyncioTestCase):
txs = await self.ledger.db.get_transactions(wallet=wallet1, accounts=wallet1.accounts) 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.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].inputs[0].is_my_input, True)
self.assertEqual(txs[0].outputs[0].is_my_account, False) self.assertEqual(txs[0].outputs[0].is_my_output, False)
self.assertEqual(txs[1].inputs[0].is_my_account, False) self.assertEqual(txs[1].inputs[0].is_my_input, False)
self.assertEqual(txs[1].outputs[0].is_my_account, True) self.assertEqual(txs[1].outputs[0].is_my_output, True)
txs = await self.ledger.db.get_transactions(wallet=wallet2, accounts=[account2]) txs = await self.ledger.db.get_transactions(wallet=wallet2, accounts=[account2])
self.assertListEqual([tx3.id, tx2.id], [tx.id for tx in txs]) 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].inputs[0].is_my_input, True)
self.assertEqual(txs[0].outputs[0].is_my_account, False) self.assertEqual(txs[0].outputs[0].is_my_output, False)
self.assertEqual(txs[1].inputs[0].is_my_account, False) self.assertEqual(txs[1].inputs[0].is_my_input, False)
self.assertEqual(txs[1].outputs[0].is_my_account, True) self.assertEqual(txs[1].outputs[0].is_my_output, True)
self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account2])) self.assertEqual(2, await self.ledger.db.get_transaction_count(accounts=[account2]))
tx = await self.ledger.db.get_transaction(txid=tx2.id) tx = await self.ledger.db.get_transaction(txid=tx2.id)
self.assertEqual(tx.id, tx2.id) self.assertEqual(tx.id, tx2.id)
self.assertFalse(tx.inputs[0].is_my_account) self.assertFalse(tx.inputs[0].is_my_input)
self.assertFalse(tx.outputs[0].is_my_account) self.assertFalse(tx.outputs[0].is_my_output)
tx = await self.ledger.db.get_transaction(wallet=wallet1, txid=tx2.id) tx = await self.ledger.db.get_transaction(wallet=wallet1, txid=tx2.id)
self.assertTrue(tx.inputs[0].is_my_account) self.assertTrue(tx.inputs[0].is_my_input)
self.assertFalse(tx.outputs[0].is_my_account) self.assertFalse(tx.outputs[0].is_my_output)
tx = await self.ledger.db.get_transaction(wallet=wallet2, txid=tx2.id) tx = await self.ledger.db.get_transaction(wallet=wallet2, txid=tx2.id)
self.assertFalse(tx.inputs[0].is_my_account) self.assertFalse(tx.inputs[0].is_my_input)
self.assertTrue(tx.outputs[0].is_my_account) self.assertTrue(tx.outputs[0].is_my_output)
# height 0 sorted to the top with the rest in descending order # height 0 sorted to the top with the rest in descending order
tx4 = await self.create_tx_from_nothing(account1, 0) tx4 = await self.create_tx_from_nothing(account1, 0)

View file

@ -82,14 +82,14 @@ class TestSizeAndFeeEstimation(AsyncioTestCase):
class TestAccountBalanceImpactFromTransaction(unittest.TestCase): class TestAccountBalanceImpactFromTransaction(unittest.TestCase):
def test_is_my_account_not_set(self): def test_is_my_output_not_set(self):
tx = get_transaction() tx = get_transaction()
with self.assertRaisesRegex(ValueError, "Cannot access net_account_balance"): with self.assertRaisesRegex(ValueError, "Cannot access net_account_balance"):
_ = tx.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"): with self.assertRaisesRegex(ValueError, "Cannot access net_account_balance"):
_ = tx.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 # all inputs/outputs are set now so it should work
_ = tx.net_account_balance _ = tx.net_account_balance
@ -98,9 +98,9 @@ class TestAccountBalanceImpactFromTransaction(unittest.TestCase):
.add_inputs([get_input(300*CENT)]) \ .add_inputs([get_input(300*CENT)]) \
.add_outputs([get_output(190*CENT, NULL_HASH), .add_outputs([get_output(190*CENT, NULL_HASH),
get_output(100*CENT, NULL_HASH)]) get_output(100*CENT, NULL_HASH)])
tx.inputs[0].txo_ref.txo.is_my_account = True tx.inputs[0].txo_ref.txo.is_my_output = True
tx.outputs[0].is_my_account = False tx.outputs[0].is_my_output = False
tx.outputs[1].is_my_account = True tx.outputs[1].is_my_output = True
self.assertEqual(tx.net_account_balance, -200*CENT) self.assertEqual(tx.net_account_balance, -200*CENT)
def test_paying_from_other_account_to_my_account(self): 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_inputs([get_input(300*CENT)]) \
.add_outputs([get_output(190*CENT, NULL_HASH), .add_outputs([get_output(190*CENT, NULL_HASH),
get_output(100*CENT, NULL_HASH)]) get_output(100*CENT, NULL_HASH)])
tx.inputs[0].txo_ref.txo.is_my_account = False tx.inputs[0].txo_ref.txo.is_my_output = False
tx.outputs[0].is_my_account = True tx.outputs[0].is_my_output = True
tx.outputs[1].is_my_account = False tx.outputs[1].is_my_output = False
self.assertEqual(tx.net_account_balance, 190*CENT) self.assertEqual(tx.net_account_balance, 190*CENT)
def test_paying_from_my_account_to_my_account(self): 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_inputs([get_input(300*CENT)]) \
.add_outputs([get_output(190*CENT, NULL_HASH), .add_outputs([get_output(190*CENT, NULL_HASH),
get_output(100*CENT, NULL_HASH)]) get_output(100*CENT, NULL_HASH)])
tx.inputs[0].txo_ref.txo.is_my_account = True tx.inputs[0].txo_ref.txo.is_my_output = True
tx.outputs[0].is_my_account = True tx.outputs[0].is_my_output = True
tx.outputs[1].is_my_account = True tx.outputs[1].is_my_output = True
self.assertEqual(tx.net_account_balance, -10*CENT) # lost to fee self.assertEqual(tx.net_account_balance, -10*CENT) # lost to fee