txo_list returns txo funded by my account but sent to external address
This commit is contained in:
parent
869a76c9bb
commit
af2f2282c2
3 changed files with 96 additions and 44 deletions
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue