From e2313e7d4e45a072881bcc4bf54e9221e6eac44e Mon Sep 17 00:00:00 2001
From: vtexier <vit@free.fr>
Date: Wed, 25 Mar 2020 19:32:39 +0100
Subject: [PATCH] [enh] #798 consume and restore sources with the used_by
 property

Dropping sources and recreate them from tx inputs was loosing source conditions.
This consume method allow to not loose source and their conditions
---
 src/sakia/data/entities/source.py      |  2 +-
 src/sakia/data/processors/sources.py   | 20 ++++--
 src/sakia/data/repositories/sources.py | 47 +++++++++++--
 src/sakia/gui/sub/transfer/model.py    |  9 +--
 src/sakia/services/documents.py        | 12 ++--
 src/sakia/services/sources.py          | 92 +++++++++++---------------
 tests/unit/data/test_sources_repo.py   | 38 +++++++++++
 7 files changed, 146 insertions(+), 74 deletions(-)

diff --git a/src/sakia/data/entities/source.py b/src/sakia/data/entities/source.py
index 8c5c0b1c..de6e4bf7 100644
--- a/src/sakia/data/entities/source.py
+++ b/src/sakia/data/entities/source.py
@@ -11,4 +11,4 @@ class Source:
     amount = attr.ib(converter=int, hash=False)
     base = attr.ib(converter=int, hash=False)
     conditions = attr.ib(converter=str, hash=False)
-    used_by = attr.ib(converter=str, hash=False, default=None)
+    used_by = attr.ib(hash=False, default=None)
diff --git a/src/sakia/data/processors/sources.py b/src/sakia/data/processors/sources.py
index 18e4a0bb..4750ff56 100644
--- a/src/sakia/data/processors/sources.py
+++ b/src/sakia/data/processors/sources.py
@@ -53,15 +53,27 @@ class SourcesProcessor:
         """
         return self._repo.get_all(currency=currency, pubkey=pubkey)
 
-    def consume(self, sources):
+    def consume(self, sources, tx_sha_hash):
         """
+        Consume sources in db by setting the used_by column to tx hash
+
+        :param List(Source) sources: Source instances list
+        :param str tx_sha_hash: Hash of tx
 
-        :param currency:
-        :param sources:
         :return:
         """
         for s in sources:
-            self._repo.drop(s)
+            self._repo.consume(s, tx_sha_hash)
+
+    def restore_all(self, tx_sha_hash):
+        """
+        Restore sources in db by setting the used_by column to null
+
+        :param str tx_sha_hash: Hash of tx
+
+        :return:
+        """
+        self._repo.restore_all(tx_sha_hash)
 
     def insert(self, source):
         try:
diff --git a/src/sakia/data/repositories/sources.py b/src/sakia/data/repositories/sources.py
index 22fdecef..24677752 100644
--- a/src/sakia/data/repositories/sources.py
+++ b/src/sakia/data/repositories/sources.py
@@ -29,7 +29,8 @@ class SourcesRepo:
 
     def get_one(self, **search):
         """
-        Get an existing source in the database
+        Get an existing not consumed source in the database
+
         :param ** search: the criterions of the lookup
         :rtype: sakia.data.entities.Source
         """
@@ -39,7 +40,7 @@ class SourcesRepo:
             filters.append("{k}=?".format(k=k))
             values.append(v)
 
-        request = "SELECT * FROM sources WHERE {filters}".format(
+        request = "SELECT * FROM sources WHERE used_by is NULL AND {filters}".format(
             filters=" AND ".join(filters)
         )
 
@@ -50,7 +51,8 @@ class SourcesRepo:
 
     def get_all(self, **search):
         """
-        Get all existing source in the database corresponding to the search
+        Get all existing not consumed source in the database corresponding to the search
+
         :param ** search: the criterions of the lookup
         :rtype: [sakia.data.entities.Source]
         """
@@ -61,7 +63,7 @@ class SourcesRepo:
             filters.append("{key} = ?".format(key=k))
             values.append(value)
 
-        request = "SELECT * FROM sources WHERE {filters}".format(
+        request = "SELECT * FROM sources WHERE used_by is NULL AND {filters}".format(
             filters=" AND ".join(filters)
         )
 
@@ -101,3 +103,40 @@ class SourcesRepo:
             filters=" AND ".join(filters)
         )
         self._conn.execute(request, tuple(values))
+
+    def consume(self, source, tx_hash):
+        """
+        Consume a source by setting the used_by column with the tx hash
+
+        :param Source source: Source instance to consume
+        :param str tx_hash: Hash of tx
+        :return:
+        """
+        where_fields = attr.astuple(
+            source, filter=attr.filters.include(*SourcesRepo._primary_keys)
+        )
+        fields = (tx_hash,) + where_fields
+        self._conn.execute(
+            """UPDATE sources SET used_by=?
+                              WHERE
+                              currency=? AND
+                              pubkey=? AND
+                              identifier=? AND
+                              noffset=?""",
+            fields,
+        )
+
+    def restore_all(self, tx_hash):
+        """
+        Restore all sources released by tx_hash setting the used_by column with null
+
+        :param Source source: Source instance to consume
+        :param str tx_hash: Hash of tx
+        :return:
+        """
+        self._conn.execute(
+            """UPDATE sources SET used_by=NULL
+                              WHERE
+                              used_by=?""",
+            (tx_hash,),
+        )
diff --git a/src/sakia/gui/sub/transfer/model.py b/src/sakia/gui/sub/transfer/model.py
index 4341f1ec..071c02bc 100644
--- a/src/sakia/gui/sub/transfer/model.py
+++ b/src/sakia/gui/sub/transfer/model.py
@@ -121,12 +121,9 @@ class TransferModel(QObject):
             )
             for conn in self._connections_processor.connections():
                 if conn.pubkey == recipient:
-                    # fixme: do not drop sources from input cause we can not restore source conditions
-                    #        from inputs if needed... May cause side effects.
-                    #        Add a state field in sources table if needed.
-                    # self.app.sources_service.parse_transaction_inputs(
-                    #     recipient, transaction
-                    # )
+                    self.app.sources_service.consume_sources_from_transaction_inputs(
+                        recipient, transaction
+                    )
                     new_tx = self.app.transactions_service.parse_sent_transaction(
                         recipient, transaction
                     )
diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py
index 5f1f6606..3f0a548e 100644
--- a/src/sakia/services/documents.py
+++ b/src/sakia/services/documents.py
@@ -409,18 +409,14 @@ class DocumentsService:
         """
         lock_modes = {
             # Receiver
-            0: pypeg2.compose(
-                Condition.token(SIG.token(receiver)), Condition
-            ),
+            0: pypeg2.compose(Condition.token(SIG.token(receiver)), Condition),
             # Receiver or (issuer and delay of one week)
             1: pypeg2.compose(
                 Condition.token(
                     SIG.token(receiver),
                     Operator.token("||"),
                     Condition.token(
-                        SIG.token(issuer),
-                        Operator.token("&&"),
-                        CSV.token(604800),
+                        SIG.token(issuer), Operator.token("&&"), CSV.token(604800),
                     ),
                 ),
                 Condition,
@@ -527,7 +523,6 @@ class DocumentsService:
                     currency,
                 )
                 forged_tx += chained_tx
-        self._sources_processor.consume(sources)
         logging.debug("Inputs: {0}".format(sources))
 
         inputs = self.tx_inputs(sources)
@@ -570,6 +565,9 @@ class DocumentsService:
             raw=txdoc.signed_raw(),
         )
         forged_tx.append(tx)
+
+        self._sources_processor.consume(sources, tx.sha_hash)
+
         return forged_tx
 
     async def send_money(
diff --git a/src/sakia/services/sources.py b/src/sakia/services/sources.py
index 993706fa..a13e7798 100644
--- a/src/sakia/services/sources.py
+++ b/src/sakia/services/sources.py
@@ -5,7 +5,7 @@ from duniterpy.grammars.output import Condition, SIG
 from duniterpy.documents import BlockUID
 import logging
 import pypeg2
-from sakia.data.entities import Source, Transaction, Dividend
+from sakia.data.entities import Source, Transaction
 import hashlib
 
 
@@ -68,27 +68,28 @@ class SourcesServices(QObject):
                 )
                 self._sources_processor.insert(source)
 
-    # def parse_transaction_inputs(self, pubkey, transaction):
-    #     """
-    #     Parse a transaction to drop sources used in inputs
-    #
-    #     :param str pubkey: Receiver pubkey
-    #     :param sakia.data.entities.Transaction transaction:
-    #     """
-    #     txdoc = TransactionDoc.from_signed_raw(transaction.raw)
-    #     for index, input in enumerate(txdoc.inputs):
-    #         source = Source(
-    #             currency=self.currency,
-    #             pubkey=txdoc.issuers[0],
-    #             identifier=input.origin_id,
-    #             type=input.source,
-    #             noffset=input.index,
-    #             amount=input.amount,
-    #             base=input.base,
-    #             conditions=""
-    #         )
-    #         if source.pubkey == pubkey:
-    #             self._sources_processor.drop(source)
+    def consume_sources_from_transaction_inputs(self, pubkey, transaction):
+        """
+        Parse a transaction to drop sources used in inputs
+
+        :param str pubkey: Receiver pubkey
+        :param sakia.data.entities.Transaction transaction:
+        """
+        txdoc = TransactionDoc.from_signed_raw(transaction.raw)
+        for index, input in enumerate(txdoc.inputs):
+            source = Source(
+                currency=self.currency,
+                pubkey=txdoc.issuers[0],
+                identifier=input.origin_id,
+                type=input.source,
+                noffset=input.index,
+                amount=input.amount,
+                base=input.base,
+                conditions="",
+                used_by=None,
+            )
+            if source.pubkey == pubkey:
+                self._sources_processor.consume((source,), transaction.sha_hash)
 
     def _parse_ud(self, pubkey, dividend):
         """
@@ -192,40 +193,27 @@ class SourcesServices(QObject):
 
     def restore_sources(self, pubkey, tx):
         """
-        Restore the sources of a cancelled tx
+        Restore consumed sources and drop created sources of a cancelled tx
 
-        :param sakia.entities.Transaction tx:
+        :param str pubkey: Pubkey
+        :param Transaction tx: Instance of tx entity
         """
         txdoc = TransactionDoc.from_signed_raw(tx.raw)
         for offset, output in enumerate(txdoc.outputs):
-            if output.condition.left.pubkey == pubkey:
-                source = Source(
-                    currency=self.currency,
-                    pubkey=pubkey,
-                    identifier=txdoc.sha_hash,
-                    type="T",
-                    noffset=offset,
-                    amount=output.amount,
-                    base=output.base,
-                    conditions=pypeg2.compose(output.condition, Condition),
-                )
-                self._sources_processor.drop(source)
-        # fixme: do not restore sources from input as it is impossible to retrieve conditions from the referenced tx.
-        #        Sources are not dropped now by parse_transaction_inputs(). If a state field is added to source table.
-        #        Then update it back here.
-        # for index, input in enumerate(txdoc.inputs):
-        #     source = Source(
-        #         currency=self.currency,
-        #         pubkey=txdoc.issuers[0],
-        #         identifier=input.origin_id,
-        #         type=input.source,
-        #         noffset=input.index,
-        #         amount=input.amount,
-        #         base=input.base,
-        #         conditions=""
-        #     )
-        #     if source.pubkey == pubkey:
-        #         self._sources_processor.insert(source)
+            source = Source(
+                currency=self.currency,
+                pubkey=pubkey,
+                identifier=txdoc.sha_hash,
+                type="T",
+                noffset=offset,
+                amount=output.amount,
+                base=output.base,
+                conditions=pypeg2.compose(output.condition, Condition),
+            )
+            # drop sources created by the canceled tx
+            self._sources_processor.drop(source)
+        # restore consumed sources
+        self._sources_processor.restore_all(tx.sha_hash)
 
     def find_signature_in_condition(self, _condition, pubkey, result=False):
         """
diff --git a/tests/unit/data/test_sources_repo.py b/tests/unit/data/test_sources_repo.py
index 5bc58cbe..4c413dd0 100644
--- a/tests/unit/data/test_sources_repo.py
+++ b/tests/unit/data/test_sources_repo.py
@@ -77,3 +77,41 @@ def test_add_get_multiple_source(meta_repo):
     assert "0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843" in [
         s.identifier for s in sources
     ]
+
+
+def test_consume_restore_source(meta_repo):
+    tx_hash = "0835CEE9B4766B3866DD942971B3EE2CF953599EB9D35BFD5F1345879498B843"
+    sources_repo = SourcesRepo(meta_repo.conn)
+    sources_repo.insert(
+        Source(
+            "testcurrency",
+            "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn",
+            tx_hash,
+            3,
+            "T",
+            1565,
+            1,
+            "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)",
+            None,
+        )
+    )
+    source = sources_repo.get_one(identifier=tx_hash)
+    assert source.currency == "testcurrency"
+    assert source.pubkey == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn"
+    assert source.type == "T"
+    assert source.amount == 1565
+    assert source.base == 1
+    assert source.noffset == 3
+
+    sources_repo.consume(source, tx_hash)
+    source = sources_repo.get_one(identifier=tx_hash)
+    assert source is None
+
+    sources_repo.restore_all(tx_hash)
+    source = sources_repo.get_one(identifier=tx_hash)
+    assert source.currency == "testcurrency"
+    assert source.pubkey == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn"
+    assert source.type == "T"
+    assert source.amount == 1565
+    assert source.base == 1
+    assert source.noffset == 3
-- 
GitLab