diff --git a/src/sakia/gui/sub/transfer/controller.py b/src/sakia/gui/sub/transfer/controller.py index 1528e3cf362be1440a7cad4c71e7d303122c5061..3388cc2915c8756cce7c7a8c8a0a650f85293b5d 100644 --- a/src/sakia/gui/sub/transfer/controller.py +++ b/src/sakia/gui/sub/transfer/controller.py @@ -2,7 +2,7 @@ import re import logging from PyQt5.QtCore import Qt, QObject, pyqtSignal, QCoreApplication -from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QMessageBox from duniterpy.constants import PUBKEY_REGEX from duniterpy.documents import CRCPubkey @@ -34,7 +34,7 @@ class TransferController(QObject): """ super().__init__() self.view = view - self.model = model + self.model = model # type: TransferModel self.search_user = search_user self.user_information = user_information self.password_input = password_input @@ -49,6 +49,7 @@ class TransferController(QObject): ) self.view.spinbox_amount.valueChanged.connect(self.handle_amount_change) self.view.spinbox_relative.valueChanged.connect(self.handle_relative_change) + self.view.button_source_check.clicked.connect(self.check_source) @classmethod def create(cls, parent, app): @@ -324,3 +325,8 @@ class TransferController(QObject): self.model.set_connection(index) self.password_input.set_connection(self.model.connection) self.refresh() + + def check_source(self): + qmessagebox = QMessageBox(self.view) + qmessagebox.setText("The source has been checked.") + qmessagebox.exec() diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index afab7abe64e071e0944b1095811fa0c45fa295ad..e85985b79aad9da073751832b36741b984d20b82 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -1,5 +1,3 @@ -from hashlib import sha256 - import jsonschema import attr import logging @@ -15,12 +13,10 @@ from duniterpy.documents import ( SIGParameter, Unlock, block_uid, - BlockUID, ) -from duniterpy.documents import Identity as IdentityDoc from duniterpy.documents import Transaction as TransactionDoc from duniterpy.documents.transaction import reduce_base -from duniterpy.grammars.output import Condition, Operator, SIG, CSV, CLTV, XHX +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 ( @@ -335,7 +331,7 @@ class DocumentsService: for s in [src for src in available_sources if src.base == current_base]: condition = pypeg2.parse(s.conditions, Condition) # evaluate the condition - if not self.evaluate_condition( + if not self._sources_services.evaluate_condition( currency, condition, [key], [], s.identifier ): continue @@ -644,97 +640,3 @@ class DocumentsService: return result except NotEnoughChangeError as e: return (False, str(e)), tx_entities - - def evaluate_condition( - self, - currency, - condition: Condition, - keys: list, - passwords, - identifier: str, - result: bool = True, - ) -> bool: - """ - Evaluate a source lock condition - Support multiple signatures and passwords - - :param passwords: - :param str currency: Name of currency - :param Condition condition: Condition instance - :param [SigningKey] keys: Keys to unlock condition (first key is the source owner key) - :param str identifier: Source transaction identifier - :param bool result: result accumulator - :return: - """ - left = False - right = False - # if left param is a condition... - if isinstance(condition.left, Condition): - # evaluate condition - left = self.evaluate_condition( - currency, condition.left, keys, passwords, identifier, result - ) - # if right param is a condition... - if isinstance(condition.right, Condition): - # evaluate condition - right = self.evaluate_condition( - currency, condition.right, keys, passwords, identifier, result - ) - # if left param is a SIG... - if isinstance(condition.left, SIG) and condition.left.pubkey in ( - key.pubkey for key in keys - ): - left = True - # if left param is a CSV value... - if isinstance(condition.left, CSV): - # capture transaction of the source - tx = self._transactions_processor.find_by_hash(keys[0].pubkey, identifier) - if tx: - # capture current blockchain time - median_time = self._blockchain_processor.time(currency) - # param is true if tx time + CSV delay <= blockchain time - left = tx.timestamp + int(condition.left.time) <= median_time - # if left param is a CLTV value... - if isinstance(condition.left, CLTV): - # capture current blockchain time - median_time = self._blockchain_processor.time(currency) - # param is true if CL:TV value <= blockchain time - left = int(condition.left.timestamp) <= median_time - # if left param is a XHX value... - if isinstance(condition.left, XHX): - left = condition.left.sha_hash in [ - sha256(password).hexdigest().upper() for password in passwords - ] - # if no op then stop evaluation... - if not condition.op: - return left - # if right param is a SIG... - if isinstance(condition.right, SIG) and condition.right.pubkey in ( - key.pubkey for key in keys - ): - right = True - # if right param is a CSV value... - if isinstance(condition.right, CSV): - # capture transaction of the source - tx = self._transactions_processor.find_by_hash(keys[0].pubkey, identifier) - if tx: - # capture current blockchain time - median_time = self._blockchain_processor.time(currency) - # param is true if tx time + CSV delay <= blockchain time - right = tx.timestamp + int(condition.right.time) <= median_time - # if right param is a CLTV value... - if isinstance(condition.right, CLTV): - # capture current blockchain time - median_time = self._blockchain_processor.time(currency) - # param is true if CLTV value <= blockchain time - right = int(condition.right.timestamp) <= median_time - # if right param is a XHX value... - if isinstance(condition.right, XHX): - right = condition.right.sha_hash in [ - sha256(password).upper() for password in passwords - ] - # if operator AND... - if condition.op == "&&": - return left & right - # operator OR - return left | right diff --git a/src/sakia/services/sources.py b/src/sakia/services/sources.py index be5ce7a36ff22529c52918f6d7ef0c5db883ac28..a5c2aa18ab8356673e88c3b89fe9acdc960c0633 100644 --- a/src/sakia/services/sources.py +++ b/src/sakia/services/sources.py @@ -1,7 +1,9 @@ +from hashlib import sha256 + from PyQt5.QtCore import QObject from duniterpy.api import bma, errors from duniterpy.documents import Transaction as TransactionDoc -from duniterpy.grammars.output import Condition, SIG +from duniterpy.grammars.output import Condition, SIG, CSV, CLTV, XHX from duniterpy.documents import BlockUID import logging import pypeg2 @@ -263,3 +265,97 @@ class SourcesServices(QObject): self._sources_processor.insert(entity) except AttributeError as e: self._logger.error(str(e)) + + def evaluate_condition( + self, + currency, + condition: Condition, + keys: list, + passwords, + identifier: str, + result: bool = True, + ) -> bool: + """ + Evaluate a source lock condition + Support multiple signatures and passwords + + :param passwords: + :param str currency: Name of currency + :param Condition condition: Condition instance + :param [SigningKey] keys: Keys to unlock condition (first key is the source owner key) + :param str identifier: Source transaction identifier + :param bool result: result accumulator + :return: + """ + left = False + right = False + # if left param is a condition... + if isinstance(condition.left, Condition): + # evaluate condition + left = self.evaluate_condition( + currency, condition.left, keys, passwords, identifier, result + ) + # if right param is a condition... + if isinstance(condition.right, Condition): + # evaluate condition + right = self.evaluate_condition( + currency, condition.right, keys, passwords, identifier, result + ) + # if left param is a SIG... + if isinstance(condition.left, SIG) and condition.left.pubkey in ( + key.pubkey for key in keys + ): + left = True + # if left param is a CSV value... + if isinstance(condition.left, CSV): + # capture transaction of the source + tx = self._transactions_processor.find_by_hash(keys[0].pubkey, identifier) + if tx: + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if tx time + CSV delay <= blockchain time + left = tx.timestamp + int(condition.left.time) <= median_time + # if left param is a CLTV value... + if isinstance(condition.left, CLTV): + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if CL:TV value <= blockchain time + left = int(condition.left.timestamp) <= median_time + # if left param is a XHX value... + if isinstance(condition.left, XHX): + left = condition.left.sha_hash in [ + sha256(password).hexdigest().upper() for password in passwords + ] + # if no op then stop evaluation... + if not condition.op: + return left + # if right param is a SIG... + if isinstance(condition.right, SIG) and condition.right.pubkey in ( + key.pubkey for key in keys + ): + right = True + # if right param is a CSV value... + if isinstance(condition.right, CSV): + # capture transaction of the source + tx = self._transactions_processor.find_by_hash(keys[0].pubkey, identifier) + if tx: + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if tx time + CSV delay <= blockchain time + right = tx.timestamp + int(condition.right.time) <= median_time + # if right param is a CLTV value... + if isinstance(condition.right, CLTV): + # capture current blockchain time + median_time = self._blockchain_processor.time(currency) + # param is true if CLTV value <= blockchain time + right = int(condition.right.timestamp) <= median_time + # if right param is a XHX value... + if isinstance(condition.right, XHX): + right = condition.right.sha_hash in [ + sha256(password).hexdigest().upper() for password in passwords + ] + # if operator AND... + if condition.op == "&&": + return left & right + # operator OR + return left | right diff --git a/tests/technical/test_documents_service.py b/tests/technical/test_documents_service.py index 9ec0a14a12ea2cdd22ab9576e5287e6d816a1eb8..99e94e3e5f47f34cffedb1306e9986df09feb2aa 100644 --- a/tests/technical/test_documents_service.py +++ b/tests/technical/test_documents_service.py @@ -44,6 +44,7 @@ async def test_send_more_than_40_sources( 0, "Test comment", 0, + None, ) assert transactions[0].comment == "[CHAINED]" assert transactions[1].comment == "Test comment" diff --git a/tests/technical/test_sources_service.py b/tests/technical/test_sources_service.py index 9cb1d21c642f6b0c2088e9ac8ceb77db107fc365..553741bf3b74d9416fd40f12037db025a6ebff3a 100644 --- a/tests/technical/test_sources_service.py +++ b/tests/technical/test_sources_service.py @@ -83,7 +83,7 @@ async def test_send_tx_then_cancel( ) fake_server_with_blockchain.reject_next_post = True await application_with_one_connection.documents_service.send_money( - bob_connection, bob.salt, bob.password, alice.key.pubkey, 10, 0, None, 0 + bob_connection, bob.salt, bob.password, alice.key.pubkey, 10, 0, None, 0, None ) tx_after_send = application_with_one_connection.transactions_service.transfers( bob.key.pubkey diff --git a/tests/unit/services/test_documents.py b/tests/unit/services/test_documents.py index 4374dca2d48041ecf1398ea2a248008fbdd976ba..dbb5fc19e6982e25e2901724a40cad2e185efbff 100644 --- a/tests/unit/services/test_documents.py +++ b/tests/unit/services/test_documents.py @@ -1,12 +1,10 @@ from hashlib import sha256 -import pypeg2 import pytest from duniterpy.grammars import output -from duniterpy.grammars.output import Condition -from sakia.data.entities import Transaction, Source -from sakia.data.repositories import TransactionsRepo, SourcesRepo +from sakia.data.entities import Transaction +from sakia.data.repositories import TransactionsRepo @pytest.mark.asyncio @@ -34,7 +32,7 @@ async def test_lock_mode_0(application_with_one_connection, fake_server, bob, al _, sakia_tx_list, ) = await application_with_one_connection.documents_service.send_money( - bob_connection, bob.salt, bob.password, alice.key.pubkey, 100, 0, None, 0 + bob_connection, bob.salt, bob.password, alice.key.pubkey, 100, 0, None, 0, None ) assert len(sakia_tx_list) == 1 @@ -69,7 +67,7 @@ async def test_lock_mode_1(application_with_one_connection, fake_server, bob, al _, sakia_tx_list, ) = await application_with_one_connection.documents_service.send_money( - bob_connection, bob.salt, bob.password, alice.key.pubkey, 100, 0, None, 1 + bob_connection, bob.salt, bob.password, alice.key.pubkey, 100, 0, None, 1, None ) assert len(sakia_tx_list) == 1 @@ -132,14 +130,14 @@ def test_evaluate_condition_source_lock_mode_0( condition = output.Condition.token(output.SIG.token(bob.key.pubkey),) # bob can spend this source assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [bob.key], [], tx_hash ) is True ) # alice can not assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [alice.key], @@ -212,14 +210,14 @@ def test_evaluate_condition_source_lock_mode_1( ) # bob try to spend his source assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [bob.key], [], tx_hash ) is True ) # alice try to get back this source before the CSV delay assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [alice.key], @@ -271,14 +269,14 @@ def test_evaluate_condition_source_lock_mode_1( # bob try to spend his source assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [bob.key], [], tx_hash ) is True ) # alice can get back this source after the CSV delay assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [alice.key], @@ -347,14 +345,14 @@ def test_evaluate_condition_source_multisig( ) # bob can not spend this source alone assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [bob.key], [], tx_hash ) is False ) # alice can not spend this source alone assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [alice.key], @@ -365,7 +363,7 @@ def test_evaluate_condition_source_multisig( ) # alice && bob together only can spend this source assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, condition, [alice.key, bob.key], @@ -519,7 +517,7 @@ def test_evaluate_condition_source_atomic_swap( # alice spend the source from tx2 with the password assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx2_condition, [alice.key], @@ -532,7 +530,7 @@ def test_evaluate_condition_source_atomic_swap( # the password is revealed in the unlock of the tx3 spending tx2 # bob can now spend tx1 using the password assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx1_condition, [bob.key], @@ -544,7 +542,7 @@ def test_evaluate_condition_source_atomic_swap( # alice and bob can sign together to spend tx1 assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx1_condition, [bob.key, alice.key], @@ -556,7 +554,7 @@ def test_evaluate_condition_source_atomic_swap( # alice and bob can sign together to spend tx2 assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx2_condition, [bob.key, alice.key], @@ -568,7 +566,7 @@ def test_evaluate_condition_source_atomic_swap( # alice can not spend the source from tx2 without the password assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx2_condition, [alice.key], @@ -580,7 +578,7 @@ def test_evaluate_condition_source_atomic_swap( # bob can not spend tx1 without the password assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx1_condition, [bob.key], @@ -672,7 +670,7 @@ def test_evaluate_condition_source_atomic_swap( # alice can get back the source from tx1 without the password after 48h assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx1_condition, [alice.key], @@ -684,7 +682,7 @@ def test_evaluate_condition_source_atomic_swap( # bob can spend tx2 without the password after 24h assert ( - application_with_one_connection.documents_service.evaluate_condition( + application_with_one_connection.sources_service.evaluate_condition( application_with_one_connection.currency, tx2_condition, [bob.key],