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