txo_list returns txo funded by my account but sent to external address

This commit is contained in:
Lex Berezhny 2020-03-18 00:15:24 -04:00
parent 869a76c9bb
commit af2f2282c2
3 changed files with 96 additions and 44 deletions

View file

@ -4114,7 +4114,8 @@ 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_received=False, is_received=None, is_not_received=None, 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,
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.
@ -4155,11 +4156,20 @@ class Daemon(metaclass=JSONRPCServerType):
else: else:
claims = partial(self.ledger.get_txos, wallet=wallet, accounts=wallet.accounts, read_only=True) claims = partial(self.ledger.get_txos, wallet=wallet, accounts=wallet.accounts, read_only=True)
claim_count = partial(self.ledger.get_txo_count, wallet=wallet, accounts=wallet.accounts, read_only=True) claim_count = partial(self.ledger.get_txo_count, wallet=wallet, accounts=wallet.accounts, read_only=True)
constraints = {'resolve': resolve, 'unspent': unspent, 'include_is_received': include_is_received} constraints = {
if is_received is True: 'resolve': resolve,
constraints['is_received'] = True 'unspent': unspent,
elif is_not_received is True: 'include_is_my_input': include_is_my_input,
constraints['is_received'] = False 'include_is_my_output': include_is_my_output
}
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, '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

@ -555,6 +555,11 @@ class CommandTestCase(IntegrationTestCase):
self.daemon.jsonrpc_support_abandon(*args, **kwargs), confirm self.daemon.jsonrpc_support_abandon(*args, **kwargs), confirm
) )
async def wallet_send(self, *args, confirm=True, **kwargs):
return await self.confirm_and_render(
self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm
)
async def resolve(self, uri): async def resolve(self, uri):
return (await self.out(self.daemon.jsonrpc_resolve(uri)))[uri] return (await self.out(self.daemon.jsonrpc_resolve(uri)))[uri]

View file

@ -450,11 +450,12 @@ class Database(SQLiteMixin):
CREATE_TXI_TABLE = """ CREATE_TXI_TABLE = """
create table if not exists txi ( create table if not exists txi (
txid text references tx, txid text references tx,
txoid text references txo, txoid text references txo primary key,
address text references pubkey_address address text references pubkey_address,
position integer not null
); );
create index if not exists txi_address_idx on txi (address); create index if not exists txi_address_idx on txi (address);
create index if not exists txi_txoid_idx on txi (txoid); create index if not exists first_input_idx on txi (txid, address) where position=0;
""" """
CREATE_TABLES_QUERY = ( CREATE_TABLES_QUERY = (
@ -466,12 +467,11 @@ class Database(SQLiteMixin):
CREATE_TXI_TABLE CREATE_TXI_TABLE
) )
@staticmethod def txo_to_row(self, tx, txo):
def txo_to_row(tx, address, txo):
row = { row = {
'txid': tx.id, 'txid': tx.id,
'txoid': txo.id, 'txoid': txo.id,
'address': address, 'address': txo.get_address(self.ledger),
'position': txo.position, 'position': txo.position,
'amount': txo.amount, 'amount': txo.amount,
'script': sqlite3.Binary(txo.script.source) 'script': sqlite3.Binary(txo.script.source)
@ -517,25 +517,29 @@ class Database(SQLiteMixin):
def _transaction_io(self, conn: sqlite3.Connection, tx: Transaction, address, txhash): def _transaction_io(self, conn: sqlite3.Connection, tx: Transaction, address, txhash):
conn.execute(*self._insert_sql('tx', self.tx_to_row(tx), replace=True)).fetchall() conn.execute(*self._insert_sql('tx', self.tx_to_row(tx), replace=True)).fetchall()
for txo in tx.outputs: is_my_input = False
if txo.script.is_pay_pubkey_hash and txo.pubkey_hash == txhash:
conn.execute(*self._insert_sql(
"txo", self.txo_to_row(tx, address, txo), ignore_duplicate=True
)).fetchall()
elif txo.script.is_pay_script_hash:
# TODO: implement script hash payments
log.warning('Database.save_transaction_io: pay script hash is not implemented!')
for txi in tx.inputs: for txi in tx.inputs:
if txi.txo_ref.txo is not None: if txi.txo_ref.txo is not None:
txo = txi.txo_ref.txo txo = txi.txo_ref.txo
if txo.has_address and txo.get_address(self.ledger) == address: if txo.has_address and txo.get_address(self.ledger) == address:
is_my_input = True
conn.execute(*self._insert_sql("txi", { conn.execute(*self._insert_sql("txi", {
'txid': tx.id, 'txid': tx.id,
'txoid': txo.id, 'txoid': txo.id,
'address': address, 'address': address,
'position': txi.position
}, ignore_duplicate=True)).fetchall() }, ignore_duplicate=True)).fetchall()
for txo in tx.outputs:
if txo.script.is_pay_pubkey_hash and (txo.pubkey_hash == txhash or is_my_input):
conn.execute(*self._insert_sql(
"txo", self.txo_to_row(tx, txo), ignore_duplicate=True
)).fetchall()
elif txo.script.is_pay_script_hash:
# TODO: implement script hash payments
log.warning('Database.save_transaction_io: pay script hash is not implemented!')
def save_transaction_io(self, tx: Transaction, address, txhash, history): def save_transaction_io(self, tx: Transaction, address, txhash, history):
return self.save_transaction_io_batch([tx], address, txhash, history) return self.save_transaction_io_batch([tx], address, txhash, history)
@ -655,22 +659,44 @@ class Database(SQLiteMixin):
if txs: if txs:
return txs[0] return txs[0]
async def select_txos(self, cols, wallet=None, include_is_received=False, read_only=False, **constraints): async def select_txos(self, cols, accounts=None, is_my_input=None, is_my_output=True, read_only=True, **constraints):
if include_is_received: if 'txoid' in constraints:
assert wallet is not None, 'cannot use is_recieved filter without wallet argument' constraints['txo.txoid'] = constraints.pop('txoid')
account_in_wallet, values = constraints_to_sql({ if 'txoid__in' in constraints:
'$$account__in#is_received': [a.public_key.address for a in wallet.accounts] constraints['txo.txoid__in'] = constraints.pop('txoid__in')
if accounts:
account_in_sql, values = constraints_to_sql({
'$$account__in': [a.public_key.address for a in accounts]
}) })
cols += f""", my_addresses = f"SELECT address FROM account_address WHERE {account_in_sql}"
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) constraints.update(values)
sql = f"SELECT {cols} FROM txo JOIN tx USING (txid)" if is_my_input is True and is_my_output is True: # special case
if 'accounts' in constraints: constraints['received_or_sent__or'] = {
sql += " JOIN account_address USING (address)" 'txo.address__in': my_addresses,
'sent__and': {
'txi.address__is_not_null': True,
'txi.address__in': my_addresses
}
}
else:
if is_my_output is True:
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:
constraints['txi.address__is_not_null'] = True
constraints['txi.address__in'] = my_addresses
elif is_my_input is False:
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) return await self.db.execute_fetchall(*query(sql, **constraints), read_only=read_only)
@staticmethod @staticmethod
@ -678,24 +704,33 @@ class Database(SQLiteMixin):
constraints['is_reserved'] = False constraints['is_reserved'] = False
constraints['txoid__not_in'] = "SELECT txoid FROM txi" constraints['txoid__not_in'] = "SELECT txoid FROM txi"
async def get_txos(self, wallet=None, no_tx=False, unspent=False, include_is_received=False, async def get_txos(
read_only=False, **constraints): self, wallet=None, no_tx=False, unspent=False,
include_is_received = include_is_received or 'is_received' in constraints 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)
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 = ""
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})
)
"""
constraints.update(values)
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( rows = await self.select_txos(
""" f"""
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 1 from txi where txi.txoid=txo.txoid) ), spent.txoid IS NOT NULL {is_my_input_column}
""", """, read_only=read_only, **constraints
wallet=wallet, include_is_received=include_is_received, read_only=read_only, **constraints
) )
txos = [] txos = []
txs = {} txs = {}
@ -716,8 +751,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: if include_is_my_input and my_accounts:
txo.is_received = bool(row[10]) txo.is_received = not 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'
@ -755,7 +790,9 @@ class Database(SQLiteMixin):
return txos return txos
async def get_txo_count(self, unspent=False, **constraints): async def get_txo_count(self, unspent=False, **constraints):
constraints['include_is_received'] = 'is_received' in constraints constraints.pop('include_is_my_input', None)
constraints.pop('include_is_my_output', None)
constraints.pop('wallet', None)
constraints.pop('resolve', None) constraints.pop('resolve', None)
constraints.pop('offset', None) constraints.pop('offset', None)
constraints.pop('limit', None) constraints.pop('limit', None)