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