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