From 5eb276655dba4a217889a420a2b2d27dead7d815 Mon Sep 17 00:00:00 2001 From: Lex Berezhny <lex@damoti.com> Date: Mon, 9 Jul 2018 20:22:04 -0400 Subject: [PATCH] + Transaction.liquidate, balance/get_utxos support constraints --- torba/baseaccount.py | 3 ++ torba/basedatabase.py | 21 ++++++++++--- torba/baseledger.py | 67 +++++++++++++++++++--------------------- torba/basetransaction.py | 33 +++++++++++++++++++- torba/bip32.py | 4 +-- 5 files changed, 86 insertions(+), 42 deletions(-) diff --git a/torba/baseaccount.py b/torba/baseaccount.py index 6c51611c1..f8d46dd03 100644 --- a/torba/baseaccount.py +++ b/torba/baseaccount.py @@ -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) diff --git a/torba/basedatabase.py b/torba/basedatabase.py index 3b9afa215..92efd97b9 100644 --- a/torba/basedatabase.py +++ b/torba/basedatabase.py @@ -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 )) diff --git a/torba/baseledger.py b/torba/baseledger.py index 247be00c9..57170279c 100644 --- a/torba/baseledger.py +++ b/torba/baseledger.py @@ -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): diff --git a/torba/basetransaction.py b/torba/basetransaction.py index 004d8f323..6eaf14ac3 100644 --- a/torba/basetransaction.py +++ b/torba/basetransaction.py @@ -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 diff --git a/torba/bip32.py b/torba/bip32.py index 2f605f98a..5bb06cd1e 100644 --- a/torba/bip32.py +++ b/torba/bip32.py @@ -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