+ Transaction.liquidate, balance/get_utxos support constraints

This commit is contained in:
Lex Berezhny 2018-07-09 20:22:04 -04:00
parent 2006ad68ee
commit 5eb276655d
5 changed files with 86 additions and 42 deletions

View file

@ -175,3 +175,6 @@ class BaseAccount(object):
def get_balance(self, **constraints):
return self.ledger.db.get_balance_for_account(self, **constraints)
def get_unspent_outputs(self, **constraints):
return self.ledger.db.get_utxos_for_account(self, **constraints)

View file

@ -225,6 +225,12 @@ class BaseDatabase(SQLiteMixin):
def release_reserved_outputs(self, txoids):
return self.reserve_spent_outputs(txoids, is_reserved=False)
def get_txoid_for_txo(self, txo):
return self.query_one_value(
"SELECT txoid FROM txo WHERE txhash = ? AND position = ?",
(sqlite3.Binary(txo.transaction.hash), txo.index)
)
@defer.inlineCallbacks
def get_transaction(self, txhash):
result = yield self.db.runQuery(
@ -258,15 +264,22 @@ class BaseDatabase(SQLiteMixin):
defer.returnValue(0)
@defer.inlineCallbacks
def get_utxos(self, account, output_class):
def get_utxos_for_account(self, account, **constraints):
extra_sql = ""
if constraints:
extra_sql = ' AND ' + ' AND '.join(
'{} = :{}'.format(c, c) for c in constraints.keys()
)
values = {'account': sqlite3.Binary(account.public_key.address)}
values.update(constraints)
utxos = yield self.db.runQuery(
"""
SELECT amount, script, txhash, txo.position, txoid
FROM txo JOIN pubkey_address ON pubkey_address.address=txo.address
WHERE account=:account AND txo.is_reserved=0 AND txoid NOT IN (SELECT txoid FROM txi)
""",
{'account': sqlite3.Binary(account.public_key.address)}
"""+extra_sql, values
)
output_class = account.ledger.transaction_class.output_class
defer.returnValue([
output_class(
values[0],
@ -335,7 +348,7 @@ class BaseDatabase(SQLiteMixin):
return self.db.runInteraction(lambda t: self._set_address_history(t, address, history))
def get_unused_addresses(self, account, chain):
# type: (torba.baseaccount.BaseAccount, int) -> defer.Deferred[List[str]]
# type: (torba.baseaccount.BaseAccount, Union[int,None]) -> defer.Deferred[List[str]]
return self.query_one_value_list(*self._used_address_sql(
account, chain, '=', 0
))

View file

@ -133,15 +133,12 @@ class BaseLedger(six.with_metaclass(LedgerRegistry)):
if bytes(match['account']) == account.public_key.address:
defer.returnValue(account.get_private_key(match['chain'], match['position']))
def get_unspent_outputs(self, account):
return self.db.get_utxos(account, self.transaction_class.output_class)
@defer.inlineCallbacks
def get_effective_amount_estimators(self, funding_accounts):
# type: (Iterable[baseaccount.BaseAccount]) -> defer.Deferred
estimators = []
for account in funding_accounts:
utxos = yield self.get_unspent_outputs(account)
utxos = yield account.get_unspent_outputs()
for utxo in utxos:
estimators.append(utxo.get_estimator(self))
defer.returnValue(estimators)
@ -266,41 +263,41 @@ class BaseLedger(six.with_metaclass(LedgerRegistry)):
yield lock.acquire()
#try:
# see if we have a local copy of transaction, otherwise fetch it from server
raw, local_height, is_verified = yield self.db.get_transaction(unhexlify(hex_id)[::-1])
save_tx = None
if raw is None:
_raw = yield self.network.get_transaction(hex_id)
tx = self.transaction_class(unhexlify(_raw))
save_tx = 'insert'
else:
tx = self.transaction_class(raw)
try:
# see if we have a local copy of transaction, otherwise fetch it from server
raw, local_height, is_verified = yield self.db.get_transaction(unhexlify(hex_id)[::-1])
save_tx = None
if raw is None:
_raw = yield self.network.get_transaction(hex_id)
tx = self.transaction_class(unhexlify(_raw))
save_tx = 'insert'
else:
tx = self.transaction_class(raw)
if remote_height > 0 and not is_verified:
is_verified = yield self.is_valid_transaction(tx, remote_height)
is_verified = 1 if is_verified else 0
if save_tx is None:
save_tx = 'update'
if remote_height > 0 and not is_verified:
is_verified = yield self.is_valid_transaction(tx, remote_height)
is_verified = 1 if is_verified else 0
if save_tx is None:
save_tx = 'update'
yield self.db.save_transaction_io(
save_tx, tx, remote_height, is_verified, address, self.address_to_hash160(address),
''.join('{}:{}:'.format(tx_id.decode(), tx_height) for tx_id, tx_height in synced_history)
)
yield self.db.save_transaction_io(
save_tx, tx, remote_height, is_verified, address, self.address_to_hash160(address),
''.join('{}:{}:'.format(tx_id.decode(), tx_height) for tx_id, tx_height in synced_history)
)
log.debug("{}: sync'ed tx {} for address: {}, height: {}, verified: {}".format(
self.get_id(), hex_id, address, remote_height, is_verified
))
self._on_transaction_controller.add(TransactionEvent(address, tx, remote_height, is_verified))
log.debug("{}: sync'ed tx {} for address: {}, height: {}, verified: {}".format(
self.get_id(), hex_id, address, remote_height, is_verified
))
self._on_transaction_controller.add(TransactionEvent(address, tx, remote_height, is_verified))
# except:
# log.exception('Failed to synchronize transaction:')
# raise
#
# finally:
lock.release()
if not lock.locked:
del self._transaction_processing_locks[hex_id]
except:
log.exception('Failed to synchronize transaction:')
raise
finally:
lock.release()
if not lock.locked:
del self._transaction_processing_locks[hex_id]
@defer.inlineCallbacks
def subscribe_history(self, address):

View file

@ -331,9 +331,40 @@ class BaseTransaction:
defer.returnValue(tx)
@classmethod
def liquidate(cls, assets, funding_accounts, change_account):
@defer.inlineCallbacks
def liquidate(cls, assets, funding_accounts, change_account, reserve_outputs=True):
""" Spend assets (utxos) supplementing with funding_accounts if fee is higher than asset value. """
tx = cls().add_inputs([
cls.input_class.spend(utxo) for utxo in assets
])
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
reserved_outputs = [utxo.txoid for utxo in assets]
if reserve_outputs:
yield ledger.db.reserve_spent_outputs(reserved_outputs)
try:
cost_of_change = (
ledger.get_transaction_base_fee(tx) +
ledger.get_input_output_fee(cls.output_class.pay_pubkey_hash(COIN, NULL_HASH))
)
liquidated_total = sum(utxo.amount for utxo in assets)
if liquidated_total > cost_of_change:
change_address = yield change_account.change.get_or_create_usable_address()
change_hash160 = change_account.ledger.address_to_hash160(change_address)
change_amount = liquidated_total - cost_of_change
tx.add_outputs([cls.output_class.pay_pubkey_hash(change_amount, change_hash160)])
yield tx.sign(funding_accounts)
except Exception:
if reserve_outputs:
yield ledger.db.release_reserved_outputs(reserved_outputs)
raise
defer.returnValue(tx)
def signature_hash_type(self, hash_type):
return hash_type

View file

@ -97,11 +97,11 @@ class PubKey(_KeyBase):
raise TypeError('pubkey must be raw bytes')
if len(pubkey) != 33:
raise ValueError('pubkey must be 33 bytes')
if byte2int(pubkey[0]) not in (2, 3):
if indexbytes(pubkey, 0) not in (2, 3):
raise ValueError('invalid pubkey prefix byte')
curve = cls.CURVE.curve
is_odd = byte2int(pubkey[0]) == 3
is_odd = indexbytes(pubkey, 0) == 3
x = bytes_to_int(pubkey[1:])
# p is the finite field order