From eda2f50baddf9ada2725f91fbe3889e78f5a47da Mon Sep 17 00:00:00 2001
From: vtexier <vit@free.fr>
Date: Tue, 24 Mar 2020 17:58:43 +0100
Subject: [PATCH] [enh] #798 add conditions property in sources table and
 Source entity

Upgrade database automatically to version 8
Remove source drop and restore from inputs as it quite impossible to retrieve source conditions from inputs
---
 src/sakia/data/entities/source.py             |  1 +
 .../006_add_sources_conditions_property.sql   |  5 +
 src/sakia/data/repositories/meta.py           | 12 +++
 src/sakia/data/repositories/meta.sql          |  5 +-
 src/sakia/data/repositories/sources.py        |  6 +-
 src/sakia/gui/sub/transfer/model.py           |  9 +-
 src/sakia/services/documents.py               | 40 +++++---
 src/sakia/services/sources.py                 | 96 +++++++++++--------
 tests/conftest.py                             | 11 +++
 tests/unit/data/test_sources_repo.py          |  3 +
 10 files changed, 125 insertions(+), 63 deletions(-)
 create mode 100644 src/sakia/data/repositories/006_add_sources_conditions_property.sql

diff --git a/src/sakia/data/entities/source.py b/src/sakia/data/entities/source.py
index f8b0cab1..007b94df 100644
--- a/src/sakia/data/entities/source.py
+++ b/src/sakia/data/entities/source.py
@@ -10,3 +10,4 @@ class Source:
     type = attr.ib(converter=str, validator=lambda i, a, s: s == "T" or s == "D")
     amount = attr.ib(converter=int, hash=False)
     base = attr.ib(converter=int, hash=False)
+    conditions = attr.ib(converter=str)
diff --git a/src/sakia/data/repositories/006_add_sources_conditions_property.sql b/src/sakia/data/repositories/006_add_sources_conditions_property.sql
new file mode 100644
index 00000000..66b9f6ef
--- /dev/null
+++ b/src/sakia/data/repositories/006_add_sources_conditions_property.sql
@@ -0,0 +1,5 @@
+BEGIN TRANSACTION;
+
+ALTER TABLE sources ADD COLUMN conditions VARCHAR(255);
+
+COMMIT;
diff --git a/src/sakia/data/repositories/meta.py b/src/sakia/data/repositories/meta.py
index cee8a3f6..1c25c9d8 100644
--- a/src/sakia/data/repositories/meta.py
+++ b/src/sakia/data/repositories/meta.py
@@ -89,6 +89,7 @@ class SakiaDatabase:
             self.refactor_transactions,
             self.drop_incorrect_nodes,
             self.insert_last_mass_attribute,
+            self.add_sources_conditions_property,
         ]
 
     def upgrade_database(self, to=0):
@@ -211,6 +212,17 @@ class SakiaDatabase:
         with self.conn:
             self.conn.executescript(sql_file.read())
 
+    def add_sources_conditions_property(self):
+        self._logger.debug("Add sources conditions property")
+        sql_file = open(
+            os.path.join(
+                os.path.dirname(__file__), "006_add_sources_conditions_property.sql"
+            ),
+            "r",
+        )
+        with self.conn:
+            self.conn.executescript(sql_file.read())
+
     def version(self):
         with self.conn:
             c = self.conn.execute("SELECT * FROM meta WHERE id=1")
diff --git a/src/sakia/data/repositories/meta.sql b/src/sakia/data/repositories/meta.sql
index 63785d61..368007a0 100644
--- a/src/sakia/data/repositories/meta.sql
+++ b/src/sakia/data/repositories/meta.sql
@@ -106,7 +106,7 @@ CREATE TABLE IF NOT EXISTS nodes(
                                PRIMARY KEY (currency, pubkey)
                                );
 
--- Cnnections TABLE
+-- CONNECTIONS TABLE
 CREATE TABLE IF NOT EXISTS connections(
                                currency           VARCHAR(30),
                                pubkey             VARCHAR(50),
@@ -118,7 +118,7 @@ CREATE TABLE IF NOT EXISTS connections(
                                PRIMARY KEY (currency, pubkey)
                                );
 
--- Cnnections TABLE
+-- SOURCES TABLE
 CREATE TABLE IF NOT EXISTS sources(
                                currency           VARCHAR(30),
                                pubkey             VARCHAR(50),
@@ -130,6 +130,7 @@ CREATE TABLE IF NOT EXISTS sources(
                                PRIMARY KEY (currency, pubkey, identifier, noffset)
                                );
 
+-- DIVIDENDS TABLE
 CREATE TABLE IF NOT EXISTS dividends(
                                currency           VARCHAR(30),
                                pubkey             VARCHAR(50),
diff --git a/src/sakia/data/repositories/sources.py b/src/sakia/data/repositories/sources.py
index 554c89d1..22fdecef 100644
--- a/src/sakia/data/repositories/sources.py
+++ b/src/sakia/data/repositories/sources.py
@@ -30,7 +30,7 @@ class SourcesRepo:
     def get_one(self, **search):
         """
         Get an existing source in the database
-        :param dict search: the criterions of the lookup
+        :param ** search: the criterions of the lookup
         :rtype: sakia.data.entities.Source
         """
         filters = []
@@ -51,8 +51,8 @@ class SourcesRepo:
     def get_all(self, **search):
         """
         Get all existing source in the database corresponding to the search
-        :param dict search: the criterions of the lookup
-        :rtype: sakia.data.entities.Source
+        :param ** search: the criterions of the lookup
+        :rtype: [sakia.data.entities.Source]
         """
         filters = []
         values = []
diff --git a/src/sakia/gui/sub/transfer/model.py b/src/sakia/gui/sub/transfer/model.py
index e20261b8..4341f1ec 100644
--- a/src/sakia/gui/sub/transfer/model.py
+++ b/src/sakia/gui/sub/transfer/model.py
@@ -121,9 +121,12 @@ class TransferModel(QObject):
             )
             for conn in self._connections_processor.connections():
                 if conn.pubkey == recipient:
-                    self.app.sources_service.parse_transaction_inputs(
-                        recipient, transaction
-                    )
+                    # 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
+                    # )
                     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 8f65dec4..5f1f6606 100644
--- a/src/sakia/services/documents.py
+++ b/src/sakia/services/documents.py
@@ -18,7 +18,7 @@ from duniterpy.documents import (
 from duniterpy.documents import Identity as IdentityDoc
 from duniterpy.documents import Transaction as TransactionDoc
 from duniterpy.documents.transaction import reduce_base
-from duniterpy.grammars import output
+from duniterpy.grammars.output import Condition, Operator, SIG, CSV
 from duniterpy.api import bma
 from sakia.data.entities import Identity, Transaction, Source
 from sakia.data.processors import (
@@ -28,9 +28,11 @@ from sakia.data.processors import (
     TransactionsProcessor,
     SourcesProcessor,
     CertificationsProcessor,
+    ConnectionsProcessor,
 )
 from sakia.data.connectors import BmaConnector, parse_bma_responses
 from sakia.errors import NotEnoughChangeError
+from sakia.services.sources import SourcesServices
 
 
 @attr.s()
@@ -52,6 +54,7 @@ class DocumentsService:
     _certifications_processor = attr.ib()
     _transactions_processor = attr.ib()
     _sources_processor = attr.ib()
+    _sources_services = attr.ib()  # type: SourcesServices
     _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger("sakia")))
 
     @classmethod
@@ -67,6 +70,14 @@ class DocumentsService:
             CertificationsProcessor.instanciate(app),
             TransactionsProcessor.instanciate(app),
             SourcesProcessor.instanciate(app),
+            SourcesServices(
+                app.currency,
+                SourcesProcessor.instanciate(app),
+                ConnectionsProcessor.instanciate(app),
+                TransactionsProcessor.instanciate(app),
+                BlockchainProcessor.instanciate(app),
+                BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters),
+            ),
         )
 
     def generate_identity(self, connection):
@@ -399,20 +410,20 @@ class DocumentsService:
         lock_modes = {
             # Receiver
             0: pypeg2.compose(
-                output.Condition.token(output.SIG.token(receiver)), output.Condition
+                Condition.token(SIG.token(receiver)), Condition
             ),
             # Receiver or (issuer and delay of one week)
             1: pypeg2.compose(
-                output.Condition.token(
-                    output.SIG.token(receiver),
-                    output.Operator.token("||"),
-                    output.Condition.token(
-                        output.SIG.token(issuer),
-                        output.Operator.token("&&"),
-                        output.CSV.token(604800),
+                Condition.token(
+                    SIG.token(receiver),
+                    Operator.token("||"),
+                    Condition.token(
+                        SIG.token(issuer),
+                        Operator.token("&&"),
+                        CSV.token(604800),
                     ),
                 ),
-                output.Condition,
+                Condition,
             ),
         }
 
@@ -439,9 +450,7 @@ class DocumentsService:
                 OutputSource(
                     overheads_sum,
                     base,
-                    output.Condition.token(output.SIG.token(issuer)).compose(
-                        output.Condition()
-                    ),
+                    pypeg2.compose(Condition.token(SIG.token(issuer)), Condition),
                 )
             )
 
@@ -456,7 +465,9 @@ class DocumentsService:
         :return:
         """
         for offset, output in enumerate(txdoc.outputs):
-            if output.condition.left.pubkey == pubkey:
+            if self._sources_services.find_signature_in_condition(
+                output.condition, pubkey
+            ):
                 source = Source(
                     currency=currency,
                     pubkey=pubkey,
@@ -465,6 +476,7 @@ class DocumentsService:
                     noffset=offset,
                     amount=output.amount,
                     base=output.base,
+                    conditions=pypeg2.compose(output.condition, Condition),
                 )
                 self._sources_processor.insert(source)
 
diff --git a/src/sakia/services/sources.py b/src/sakia/services/sources.py
index 7c8be058..23a5a3c5 100644
--- a/src/sakia/services/sources.py
+++ b/src/sakia/services/sources.py
@@ -1,8 +1,7 @@
 from PyQt5.QtCore import QObject
 from duniterpy.api import bma, errors
 from duniterpy.documents import Transaction as TransactionDoc
-from duniterpy.grammars import output
-from duniterpy.grammars.output import Condition
+from duniterpy.grammars.output import Condition, SIG
 from duniterpy.documents import BlockUID
 import logging
 import pypeg2
@@ -49,12 +48,14 @@ class SourcesServices(QObject):
 
     def parse_transaction_outputs(self, pubkey, transaction):
         """
-        Parse a transaction
+        Parse a transaction to extract sources
+
+        :param str pubkey: Receiver pubkey
         :param sakia.data.entities.Transaction transaction:
         """
         txdoc = TransactionDoc.from_signed_raw(transaction.raw)
         for offset, output in enumerate(txdoc.outputs):
-            if output.condition.left.pubkey == pubkey:
+            if self.find_signature_in_condition(output.condition, pubkey):
                 source = Source(
                     currency=self.currency,
                     pubkey=pubkey,
@@ -63,32 +64,38 @@ class SourcesServices(QObject):
                     noffset=offset,
                     amount=output.amount,
                     base=output.base,
+                    conditions=pypeg2.compose(output.condition, Condition),
                 )
                 self._sources_processor.insert(source)
 
-    def parse_transaction_inputs(self, pubkey, transaction):
-        """
-        Parse a transaction
-        :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,
-            )
-            if source.pubkey == pubkey:
-                self._sources_processor.drop(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 _parse_ud(self, pubkey, dividend):
         """
-        :param str pubkey:
-        :param sakia.data.entities.Dividend dividend:
+        Add source in db from UD
+
+        :param str pubkey: Pubkey of UD issuer
+        :param sakia.data.entities.Dividend dividend: Dividend instance
         :return:
         """
         source = Source(
@@ -99,6 +106,7 @@ class SourcesServices(QObject):
             noffset=dividend.block_number,
             amount=dividend.amount,
             base=dividend.base,
+            conditions=pypeg2.compose(Condition.token(SIG.token(pubkey)), Condition),
         )
         self._sources_processor.insert(source)
 
@@ -185,6 +193,7 @@ class SourcesServices(QObject):
     def restore_sources(self, pubkey, tx):
         """
         Restore the sources of a cancelled tx
+
         :param sakia.entities.Transaction tx:
         """
         txdoc = TransactionDoc.from_signed_raw(tx.raw)
@@ -198,38 +207,43 @@ class SourcesServices(QObject):
                     noffset=offset,
                     amount=output.amount,
                     base=output.base,
+                    conditions=pypeg2.compose(output.condition, Condition),
                 )
                 self._sources_processor.drop(source)
-        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,
-            )
-            if source.pubkey == pubkey:
-                self._sources_processor.insert(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)
 
     def find_signature_in_condition(self, _condition, pubkey, result=False):
         """
         Recursive function to find a SIG(pubkey) in a Condition object
 
-        :param output.Condition _condition: Condition instance
+        :param Condition _condition: Condition instance
         :param str pubkey: Pubkey to find
         :param bool result: True if found
         :return:
         """
-        if isinstance(_condition.left, output.Condition):
+        if isinstance(_condition.left, Condition):
             result |= self.find_signature_in_condition(_condition.left, pubkey, result)
-        if isinstance(_condition.right, output.Condition):
+        if isinstance(_condition.right, Condition):
             result |= self.find_signature_in_condition(_condition.right, pubkey, result)
         if (
-            isinstance(_condition.left, output.SIG)
+            isinstance(_condition.left, SIG)
             and _condition.left.pubkey == pubkey
-            or isinstance(_condition.right, output.SIG)
+            or isinstance(_condition.right, SIG)
             and _condition.right.pubkey == pubkey
         ):
             result |= True
diff --git a/tests/conftest.py b/tests/conftest.py
index 7ac7557c..bfacda2a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,4 @@
+import pypeg2
 import pytest
 import asyncio
 import quamash
@@ -6,6 +7,8 @@ import mirage
 import sys
 import os
 
+from duniterpy.grammars import output
+
 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
 
 from sakia.constants import ROOT_SERVERS
@@ -260,6 +263,10 @@ def application_with_one_connection(application, simple_blockchain_forge, bob):
                 type=s.source,
                 amount=s.amount,
                 base=s.base,
+                conditions=pypeg2.compose(
+                    output.Condition.token(output.SIG.token(bob.key.pubkey)),
+                    output.Condition,
+                ),
             )
         )
     bob_blockstamp = simple_blockchain_forge.user_identities[bob.key.pubkey].blockstamp
@@ -314,6 +321,10 @@ def application_with_two_connections(
                     type=s.source,
                     amount=s.amount,
                     base=s.base,
+                    conditions=pypeg2.compose(
+                        output.Condition.token(output.SIG.token(bob.key.pubkey)),
+                        output.Condition,
+                    ),
                 )
             )
         except sqlite3.IntegrityError:
diff --git a/tests/unit/data/test_sources_repo.py b/tests/unit/data/test_sources_repo.py
index 858011ba..a1f43c0e 100644
--- a/tests/unit/data/test_sources_repo.py
+++ b/tests/unit/data/test_sources_repo.py
@@ -13,6 +13,7 @@ def test_add_get_drop_source(meta_repo):
             "T",
             1565,
             1,
+            "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)",
         )
     )
     source = sources_repo.get_one(
@@ -43,6 +44,7 @@ def test_add_get_multiple_source(meta_repo):
             "T",
             1565,
             1,
+            "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)",
         )
     )
     sources_repo.insert(
@@ -54,6 +56,7 @@ def test_add_get_multiple_source(meta_repo):
             "D",
             726946,
             1,
+            "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)",
         )
     )
     sources = sources_repo.get_all(
-- 
GitLab