From cdf5c5165ac4cc6670bd7c030070eeb32620a9a0 Mon Sep 17 00:00:00 2001 From: inso <insomniak.fr@gmaiL.com> Date: Sat, 19 Nov 2016 12:37:24 +0100 Subject: [PATCH] Working on new architecture - Adding transactions data and tables - Fix duniterpy syntax - Fix multiples bugs --- requirements.txt | 2 +- src/sakia/app.py | 25 +- src/sakia/core/__init__.py | 2 - src/sakia/core/registry/__init__.py | 1 - src/sakia/core/registry/identity.py | 549 ------------------ src/sakia/core/transfer.py | 374 ------------ src/sakia/core/txhistory.py | 425 -------------- src/sakia/core/wallet.py | 420 -------------- src/sakia/data/connectors/bma.py | 11 +- src/sakia/data/connectors/node.py | 41 +- src/sakia/data/entities/identity.py | 6 +- src/sakia/data/entities/transaction.py | 14 + src/sakia/data/processors/__init__.py | 1 + src/sakia/data/processors/certifications.py | 4 +- src/sakia/data/processors/identities.py | 23 +- src/sakia/data/processors/transactions.py | 114 ++++ src/sakia/data/processors/tx_lifecycle.py | 197 +++++++ src/sakia/data/repositories/meta.sql | 1 + src/sakia/data/repositories/transactions.py | 3 +- src/sakia/gui/navigation/graphs/base/model.py | 10 - .../gui/navigation/identities/controller.py | 4 +- src/sakia/gui/navigation/model.py | 15 +- .../gui/navigation/txhistory/table_model.py | 21 +- src/sakia/gui/widgets/context_menu.py | 1 - src/sakia/options.py | 4 +- src/sakia/services/__init__.py | 1 + src/sakia/services/blockchain.py | 7 +- src/sakia/services/documents.py | 4 +- src/sakia/services/identities.py | 31 +- src/sakia/services/network.py | 4 +- src/sakia/services/transactions.py | 125 ++++ 31 files changed, 565 insertions(+), 1875 deletions(-) delete mode 100644 src/sakia/core/__init__.py delete mode 100644 src/sakia/core/registry/__init__.py delete mode 100644 src/sakia/core/registry/identity.py delete mode 100644 src/sakia/core/transfer.py delete mode 100644 src/sakia/core/txhistory.py delete mode 100644 src/sakia/core/wallet.py create mode 100644 src/sakia/data/processors/transactions.py create mode 100644 src/sakia/data/processors/tx_lifecycle.py create mode 100644 src/sakia/services/transactions.py diff --git a/requirements.txt b/requirements.txt index 0fadd5f3..ffecd9a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ git+https://github.com/Insoleet/quamash.git@master asynctest networkx git+https://github.com/hynek/attrs.git@master -git+https://github.com/duniter/duniter-python-api.git@master +git+https://github.com/duniter/duniter-python-api.git@dev diff --git a/src/sakia/app.py b/src/sakia/app.py index adbbb0bc..86ab4ee7 100644 --- a/src/sakia/app.py +++ b/src/sakia/app.py @@ -15,10 +15,10 @@ from duniterpy.api.bma import API from . import __version__ from .options import SakiaOptions from sakia.data.connectors import BmaConnector -from sakia.services import NetworkService, BlockchainService, IdentitiesService, SourcesServices +from sakia.services import NetworkService, BlockchainService, IdentitiesService, SourcesServices, TransactionsService from sakia.data.repositories import SakiaDatabase from sakia.data.processors import BlockchainProcessor, NodesProcessor, IdentitiesProcessor, \ - CertificationsProcessor, SourcesProcessor + CertificationsProcessor, SourcesProcessor, TransactionsProcessor from sakia.data.files import AppDataFile, UserParametersFile from sakia.decorators import asyncify from sakia.money import Relative @@ -33,7 +33,8 @@ class Application(QObject): """ def __init__(self, qapp, loop, options, app_data, parameters, db, - network_services, blockchain_services, identities_services): + network_services, blockchain_services, identities_services, + sources_services, transactions_services): """ Init a new "sakia" application :param QCoreApplication qapp: Qt Application @@ -43,8 +44,10 @@ class Application(QObject): :param sakia.data.entities.UserParameters parameters: the application current user parameters :param sakia.data.repositories.SakiaDatabase db: The database :param dict network_services: All network services for current currency - :param dict blockchain_services: All network services for current currency - :param dict identities_services: All network services for current currency + :param dict blockchain_services: All blockchain services for current currency + :param dict identities_services: All identities services for current currency + :param dict sources_services: All sources services for current currency + :param dict transactions_services: All transactions services for current currency :return: """ super().__init__() @@ -61,12 +64,14 @@ class Application(QObject): self.network_services = network_services self.blockchain_services = blockchain_services self.identities_services = identities_services + self.sources_services = sources_services + self.transactions_services = transactions_services @classmethod def startup(cls, argv, qapp, loop): options = SakiaOptions.from_arguments(argv) app_data = AppDataFile.in_config_path(options.config_path).load_or_init() - app = cls(qapp, loop, options, app_data, None, None, {}, {}, {}) + app = cls(qapp, loop, options, app_data, None, None, {}, {}, {}, {}, {}) #app.set_proxy() #app.get_last_version() app.load_profile(app_data.default) @@ -89,18 +94,24 @@ class Application(QObject): certs_processor = CertificationsProcessor(self.db.certifications_repo, self.db.identities_repo, bma_connector) blockchain_processor = BlockchainProcessor.instanciate(self) sources_processor = SourcesProcessor.instanciate(self) + transactions_processor = TransactionsProcessor.instanciate(self) self.blockchain_services = {} self.network_services = {} self.identities_services = {} self.sources_services = {} + self.transactions_services = {} for currency in self.db.connections_repo.get_currencies(): self.identities_services[currency] = IdentitiesService(currency, identities_processor, certs_processor, blockchain_processor, bma_connector) + self.transactions_services[currency] = TransactionsService(currency, transactions_processor, + identities_processor, bma_connector) + self.blockchain_services[currency] = BlockchainService(currency, blockchain_processor, bma_connector, - self.identities_services[currency]) + self.identities_services[currency], + self.transactions_services[currency]) self.network_services[currency] = NetworkService.load(currency, nodes_processor, self.blockchain_services[currency]) self.sources_services[currency] = SourcesServices(currency, sources_processor, bma_connector) diff --git a/src/sakia/core/__init__.py b/src/sakia/core/__init__.py deleted file mode 100644 index 4dffa313..00000000 --- a/src/sakia/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .wallet import Wallet - diff --git a/src/sakia/core/registry/__init__.py b/src/sakia/core/registry/__init__.py deleted file mode 100644 index 5f9b05a8..00000000 --- a/src/sakia/core/registry/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .identity import Identity, LocalState, BlockchainState \ No newline at end of file diff --git a/src/sakia/core/registry/identity.py b/src/sakia/core/registry/identity.py deleted file mode 100644 index 21c8788f..00000000 --- a/src/sakia/core/registry/identity.py +++ /dev/null @@ -1,549 +0,0 @@ -""" -Created on 11 févr. 2014 - -@author: inso -""" - -import logging -import time -from enum import Enum -from pkg_resources import parse_version - -from duniterpy.documents import BlockUID, SelfCertification, MalformedDocumentError -from duniterpy.api import bma, errors -from duniterpy.api.bma import PROTOCOL_VERSION - -from sakia.errors import NoPeerAvailable -from PyQt5.QtCore import QObject, pyqtSignal - - -class LocalState(Enum): - """ - The local state describes how the identity exists locally : - COMPLETED means all its related datas (certifiers, certified...) - were succefully downloaded - PARTIAL means not all data are present locally - NOT_FOUND means it could not be found anywhere - """ - NOT_FOUND = 0 - PARTIAL = 1 - COMPLETED = 2 - - -class BlockchainState(Enum): - """ - The blockchain state describes how the identity - was found : - VALIDATED means it was found in the blockchain - BUFFERED means it was found via a lookup but not in the - blockchain - NOT_FOUND means it could not be found anywhere - """ - NOT_FOUND = 0 - BUFFERED = 1 - VALIDATED = 2 - - -class Identity(QObject): - """ - A person with a uid and a pubkey - """ - def __init__(self, uid, pubkey, sigdate, local_state, blockchain_state): - """ - Initializing a person object. - - :param str uid: The identity uid, also known as its uid on the network - :param str pubkey: The identity pubkey - :parma BlockUID sig_date: The date of signature of the self certification - :param LocalState local_state: The local status of the identity - :param BlockchainState blockchain_state: The blockchain status of the identity - """ - if sigdate: - assert type(sigdate) is BlockUID - super().__init__() - self.uid = uid - self.pubkey = pubkey - self._sigdate = sigdate - self.local_state = local_state - self.blockchain_state = blockchain_state - - @classmethod - def empty(cls, pubkey): - return cls("", pubkey, None, LocalState.NOT_FOUND, BlockchainState.NOT_FOUND) - - @classmethod - def from_handled_data(cls, uid, pubkey, sigdate, blockchain_state): - return cls(uid, pubkey, sigdate, LocalState.COMPLETED, blockchain_state) - - @classmethod - def from_json(cls, json_data, version): - """ - Create a person from json data - - :param dict json_data: The person as a dict in json format - :return: A new person if pubkey wasn't known, else a new person instance. - """ - pubkey = json_data['pubkey'] - uid = json_data['uid'] - local_state = LocalState[json_data['local_state']] - blockchain_state = BlockchainState[json_data['blockchain_state']] - if version >= parse_version("0.20.0dev0") and json_data['sigdate']: - sigdate = BlockUID.from_str(json_data['sigdate']) - else: - sigdate = BlockUID.empty() - - return cls(uid, pubkey, sigdate, local_state, blockchain_state) - - @property - def sigdate(self): - return self._sigdate - - @sigdate.setter - def sigdate(self, sigdate): - assert type(sigdate) is BlockUID - self._sigdate = sigdate - - async def selfcert(self, community): - """ - Get the identity self certification. - This request is not cached in the person object. - - :param sakia.core.community.Community community: The community target to request the self certification - :return: A SelfCertification duniterpy object - :rtype: duniterpy.documents.certification.SelfCertification - """ - try: - timestamp = BlockUID.empty() - lookup_data = await community.bma_access.future_request(bma.wot.Lookup, - req_args={'search': self.pubkey}) - - for result in lookup_data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - for uid_data in uids: - # If the sigDate was written in the blockchain - if self._sigdate and BlockUID.from_str(uid_data["meta"]["timestamp"]) == self._sigdate: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - uid = uid_data["uid"] - signature = uid_data["self"] - # Else we choose the latest one found - elif BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - uid = uid_data["uid"] - signature = uid_data["self"] - - if not self.sigdate: - self.sigdate = timestamp - - return SelfCertification(PROTOCOL_VERSION, - community.currency, - self.pubkey, - uid, - timestamp, - signature) - except errors.DuniterError as e: - if e.ucode == errors.NO_MATCHING_IDENTITY: - raise LookupFailureError(self.pubkey, community) - except MalformedDocumentError: - raise LookupFailureError(self.pubkey, community) - - async def get_join_date(self, community): - """ - Get the person join date. - This request is not cached in the person object. - - :param sakia.core.community.Community community: The community target to request the join date - :return: A datetime object - """ - try: - search = await community.bma_access.future_request(bma.blockchain.Membership, - {'search': self.pubkey}) - if len(search['memberships']) > 0: - membership_data = search['memberships'][0] - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': membership_data['blockNumber']}) - return block['medianTime'] - except errors.DuniterError as e: - if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: - raise MembershipNotFoundError(self.pubkey, community.name) - except NoPeerAvailable as e: - logging.debug(str(e)) - raise MembershipNotFoundError(self.pubkey, community.name) - - async def get_expiration_date(self, community): - try: - membership = await self.membership(community) - join_block_number = membership['blockNumber'] - try: - join_block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': join_block_number}) - - parameters = await community.bma_access.future_request(bma.blockchain.Parameters) - join_date = join_block['medianTime'] - expiration_date = join_date + parameters['sigValidity'] - except NoPeerAvailable: - expiration_date = None - except errors.DuniterError as e: - logging.debug("Expiration date not found") - expiration_date = None - except MembershipNotFoundError: - expiration_date = None - return expiration_date - - -#TODO: Manage 'OUT' memberships ? Maybe ? - async def membership(self, community): - """ - Get the person last membership document. - - :param sakia.core.community.Community community: The community target to request the join date - :return: The membership data in BMA json format - :rtype: dict - """ - try: - search = await community.bma_access.future_request(bma.blockchain.Membership, - {'search': self.pubkey}) - block_number = -1 - membership_data = None - - for ms in search['memberships']: - if ms['blockNumber'] > block_number: - block_number = ms['blockNumber'] - if 'type' in ms: - if ms['type'] is 'IN': - membership_data = ms - else: - membership_data = ms - if membership_data: - return membership_data - else: - raise MembershipNotFoundError(self.pubkey, community.name) - - except errors.DuniterError as e: - if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: - raise MembershipNotFoundError(self.pubkey, community.name) - else: - logging.debug(str(e)) - raise MembershipNotFoundError(self.pubkey, community.name) - except NoPeerAvailable as e: - logging.debug(str(e)) - raise MembershipNotFoundError(self.pubkey, community.name) - - async def published_uid(self, community): - try: - data = await community.bma_access.future_request(bma.wot.Lookup, - req_args={'search': self.pubkey}) - timestamp = BlockUID.empty() - - for result in data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - person_uid = "" - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] - person_uid = uid_data["uid"] - if person_uid == self.uid: - return True - except errors.DuniterError as e: - logging.debug("Lookup error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return False - - async def uid_is_revokable(self, community): - published = await self.published_uid(community) - if published: - try: - await community.bma_access.future_request(bma.wot.CertifiersOf, - {'search': self.pubkey}) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Certifiers of error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return False - - async def is_member(self, community): - """ - Check if the person is a member of a community - - :param sakia.core.community.Community community: The community target to request the join date - :return: True if the person is a member of a community - """ - try: - certifiers = await community.bma_access.future_request(bma.wot.CertifiersOf, - {'search': self.pubkey}) - return certifiers['isMember'] - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - pass - except NoPeerAvailable as e: - logging.debug(str(e)) - return False - - async def certifiers_of(self, identities_registry, community): - """ - Get the list of this person certifiers - - :param sakia.core.registry.identities.IdentitiesRegistry identities_registry: The identities registry - :param sakia.core.community.Community community: The community target - :return: The list of the certifiers of this community - :rtype: list - """ - certifiers = list() - try: - data = await community.bma_access.future_request(bma.wot.CertifiersOf, - {'search': self.pubkey}) - - for certifier_data in data['certifications']: - certifier = {} - certifier['identity'] = identities_registry.from_handled_data(certifier_data['uid'], - certifier_data['pubkey'], - None, - BlockchainState.VALIDATED, - community) - certifier['cert_time'] = certifier_data['cert_time']['medianTime'] - if certifier_data['written']: - certifier['block_number'] = certifier_data['written']['number'] - else: - certifier['block_number'] = None - - certifiers.append(certifier) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Certifiers of error : {0}".format(str(e))) - else: - logging.debug(str(e)) - except NoPeerAvailable as e: - logging.debug(str(e)) - - try: - data = await community.bma_access.future_request(bma.wot.Lookup, {'search': self.pubkey}) - for result in data['results']: - if result["pubkey"] == self.pubkey: - self._refresh_uid(result['uids']) - for uid_data in result['uids']: - for certifier_data in uid_data['others']: - for uid in certifier_data['uids']: - # add a certifier - certifier = {} - certifier['identity'] = identities_registry.\ - from_handled_data(uid, - certifier_data['pubkey'], - None, - BlockchainState.BUFFERED, - community) - certifier['cert_time'] = await community.time(certifier_data['meta']['block_number']) - certifier['block_number'] = None - - certifiers.append(certifier) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Lookup error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return certifiers - - async def certified_by(self, identities_registry, community): - """ - Get the list of persons certified by this person - :param sakia.core.registry.IdentitiesRegistry identities_registry: The registry - :param sakia.core.community.Community community: The community target - :return: The list of the certified persons of this community in BMA json format - :rtype: list - """ - certified_list = list() - try: - data = await community.bma_access.future_request(bma.wot.CertifiedBy, {'search': self.pubkey}) - for certified_data in data['certifications']: - certified = {} - certified['identity'] = identities_registry.from_handled_data(certified_data['uid'], - certified_data['pubkey'], - None, - BlockchainState.VALIDATED, - community) - certified['cert_time'] = certified_data['cert_time']['medianTime'] - if certified_data['written']: - certified['block_number'] = certified_data['written']['number'] - else: - certified['block_number'] = None - certified_list.append(certified) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Certified by error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - - try: - data = await community.bma_access.future_request(bma.wot.Lookup, {'search': self.pubkey}) - for result in data['results']: - if result["pubkey"] == self.pubkey: - self._refresh_uid(result['uids']) - for certified_data in result['signed']: - certified = {} - certified['identity'] = identities_registry.from_handled_data(certified_data['uid'], - certified_data['pubkey'], - None, - BlockchainState.BUFFERED, - community) - timestamp = BlockUID.from_str(certified_data['meta']['timestamp']) - certified['cert_time'] = await community.time(timestamp.number) - certified['block_number'] = None - certified_list.append(certified) - except errors.DuniterError as e: - if e.ucode in (errors.NO_MATCHING_IDENTITY, errors.NO_MEMBER_MATCHING_PUB_OR_UID): - logging.debug("Lookup error : {0}".format(str(e))) - except NoPeerAvailable as e: - logging.debug(str(e)) - return certified_list - - async def _unique_valid(self, cert_list, community): - """ - Get the certifications in the blockchain and in the pools - Get only unique and last certification for each pubkey - :param list cert_list: The certifications list to filter - :param sakia.core.community.Community community: The community target - :return: The list of the certifiers of this community - :rtype: list - """ - unique_valid = [] - # add certifiers of uid - for certifier in tuple(cert_list): - # add only valid certification... - try: - cert_expired = await community.certification_expired(certifier['cert_time']) - except NoPeerAvailable: - logging.debug("No peer available") - cert_expired = True - - if not certifier['block_number']: - # add only valid certification... - try: - cert_writable = await community.certification_writable(certifier['cert_time']) - except NoPeerAvailable: - logging.debug("No peer available") - cert_writable = False - else: - cert_writable = True - - if not cert_expired and cert_writable: - # keep only the latest certification - already_found = [c['identity'].pubkey for c in unique_valid] - if certifier['identity'].pubkey in already_found: - index = already_found.index(certifier['identity'].pubkey) - if certifier['cert_time'] > unique_valid[index]['cert_time']: - unique_valid[index] = certifier - else: - unique_valid.append(certifier) - return unique_valid - - async def unique_valid_certifiers_of(self, identities_registry, community): - """ - Get the certifications in the blockchain and in the pools - Get only unique and last certification for each pubkey - :param sakia.core.registry.identities.IdentitiesRegistry identities_registry: The identities registry - :param sakia.core.community.Community community: The community target - :return: The list of the certifiers of this community - :rtype: list - """ - certifier_list = await self.certifiers_of(identities_registry, community) - return await self._unique_valid(certifier_list, community) - - async def unique_valid_certified_by(self, identities_registry, community): - """ - Get the list of persons certified by this person, filtered to get only unique - and valid certifications. - :param sakia.core.registry.IdentitiesRegistry identities_registry: The registry - :param sakia.core.community.Community community: The community target - :return: The list of the certified persons of this community in BMA json format - :rtype: list - """ - certified_list = await self.certified_by(identities_registry, community) - return await self._unique_valid(certified_list, community) - - async def identity_revocation_time(self, community): - """ - Get the remaining time before identity implicit revocation - :param sakia.core.Community community: the community - :return: the remaining time - :rtype: int - """ - membership = await self.membership(community) - join_block = membership['blockNumber'] - block = await community.get_block(join_block) - join_date = block['medianTime'] - parameters = await community.parameters() - # revocation date is join_date + 1 sigvalidity (expiration date) + 2*sigvalidity - revocation_date = join_date + 3*parameters['sigValidity'] - current_time = time.time() - return revocation_date - current_time - - async def membership_expiration_time(self, community): - """ - Get the remaining time before membership expiration - :param sakia.core.Community community: the community - :return: the remaining time - :rtype: int - """ - membership = await self.membership(community) - join_block = membership['blockNumber'] - block = await community.get_block(join_block) - join_date = block['medianTime'] - parameters = await community.parameters() - expiration_date = join_date + parameters['sigValidity'] - current_time = time.time() - return expiration_date - current_time - - async def requirements(self, community): - """ - Get the current requirements data. - :param sakia.core.Community community: the community - :return: the requirements - :rtype: dict - """ - try: - requirements = await community.bma_access.future_request(bma.wot.Requirements, - {'search': self.pubkey}) - for req in requirements['identities']: - if req['pubkey'] == self.pubkey and req['uid'] == self.uid and \ - self._sigdate and \ - BlockUID.from_str(req['meta']['timestamp']) == self._sigdate: - return req - except errors.DuniterError as e: - logging.debug(str(e)) - return None - - def _refresh_uid(self, uids): - """ - Refresh UID from uids list, got from a successful lookup request - :param list uids: UIDs got from a lookup request - """ - timestamp = BlockUID.empty() - if self.local_state == LocalState.NOT_FOUND: - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - identity_uid = uid_data["uid"] - self.uid = identity_uid - self.blockchain_state = BlockchainState.BUFFERED - self.local_state = LocalState.PARTIAL - - def jsonify(self): - """ - Get the community as dict in json format. - :return: The community as a dict in json format - """ - data = {'uid': self.uid, - 'pubkey': self.pubkey, - 'sigdate': str(self._sigdate) if self._sigdate else None, - 'local_state': self.local_state.name, - 'blockchain_state': self.blockchain_state.name} - return data - - def __str__(self): - return "{uid} - {pubkey} - {sigdate} - {local} - {blockchain}".format(uid=self.uid, - pubkey=self.pubkey, - sigdate=self._sigdate, - local=self.local_state, - blockchain=self.blockchain_state) diff --git a/src/sakia/core/transfer.py b/src/sakia/core/transfer.py deleted file mode 100644 index b1cd3048..00000000 --- a/src/sakia/core/transfer.py +++ /dev/null @@ -1,374 +0,0 @@ -""" -Created on 31 janv. 2015 - -@author: inso -""" -import logging -import time -from duniterpy.api import bma -from duniterpy.documents import Block, BlockUID -from PyQt5.QtCore import pyqtSignal, QObject -from enum import Enum - - -class TransferState(Enum): - """ - TO_SEND means the transaction wasn't sent yet - AWAITING means the transaction is waiting to reach K blockchain confrmation - VALIDATED means the transaction was validated locally and is considered present in the blockchain - REFUSED means the transaction took too long to be registered in the blockchain, - therefore it is considered as refused - DROPPED means the transaction was canceled locally. It can still be validated - in the blockchain if it was sent, if the guy is unlucky ;) - """ - TO_SEND = 0 - AWAITING = 1 - VALIDATING = 4 - VALIDATED = 2 - REFUSED = 3 - DROPPED = 5 - - -class Transfer(QObject): - """ - A transfer is the lifecycle of a transaction. - """ - transfer_broadcasted = pyqtSignal(str) - broadcast_error = pyqtSignal(int, str) - - def __init__(self, sha_hash, state, blockUID, metadata, locally_created): - """ - The constructor of a transfer. - Check for metadata keys which must be present : - - receiver - - block - - time - - issuer - - amount - - comment - - :param str sha_hash: The hash of the transaction - :param TransferState state: The state of the Transfer - :param duniterpy.documents.BlockUID blockUID: The blockUID of the transaction in the blockchain - :param dict metadata: The transfer metadata - """ - assert('receiver' in metadata) - assert('time' in metadata) - assert('issuer' in metadata) - assert('amount' in metadata) - assert('comment' in metadata) - assert('issuer_uid' in metadata) - assert('receiver_uid' in metadata) - assert('txid' in metadata) - super().__init__() - - self.sha_hash = sha_hash - self.state = state - self.blockUID = blockUID - self._locally_created = locally_created - self._metadata = metadata - - # Dict containing states of a transfer : - # keys are a tuple containg (current_state, transition_parameters) - # values are tuples containing (transition_test, transition_success, new_state) - self._table_states = { - (TransferState.TO_SEND, (list, Block)): - ( - (self._broadcast_success, lambda l, b: self._wait(b), TransferState.AWAITING), - (lambda l,b: self._broadcast_failure(l), None, TransferState.REFUSED), - ), - (TransferState.TO_SEND, ()): - ((self._is_locally_created, self._drop, TransferState.DROPPED),), - - (TransferState.AWAITING, (bool, Block)): - ((self._found_in_block, lambda r, b: self._be_validating(b), TransferState.VALIDATING),), - (TransferState.AWAITING, (bool, Block, int, int)): - ((self._not_found_in_blockchain, None, TransferState.REFUSED),), - - (TransferState.VALIDATING, (bool, Block, int)): - ((self._reached_enough_confrmation, None, TransferState.VALIDATED),), - (TransferState.VALIDATING, (bool, Block)): - ((self._rollback_and_removed, lambda r, b: self._drop(), TransferState.DROPPED),), - - (TransferState.VALIDATED, (bool, Block, int)): - ((self._rollback_in_fork_window, lambda r, b, i: self._be_validating(b), TransferState.VALIDATING),), - - (TransferState.VALIDATED, (bool, Block)): - ( - (self._rollback_and_removed, lambda r, b: self._drop(), TransferState.DROPPED), - (self._rollback_and_local, lambda r, b: self._wait(b), TransferState.AWAITING), - ), - - (TransferState.REFUSED, ()): - ((self._is_locally_created, self._drop, TransferState.DROPPED),) - } - - @classmethod - def initiate(cls, metadata): - """ - Create a new transfer in a "TO_SEND" state. - :param dict metadata: The computed metadata of the transfer - :return: A new transfer - :rtype: Transfer - """ - return cls(None, TransferState.TO_SEND, None, metadata, True) - - @classmethod - def create_from_blockchain(cls, hash, blockUID, metadata): - """ - Create a new transfer sent from another sakia instance - :param str hash: The transaction hash - :param duniterpy.documents.BlockUID blockUID: The block id were we found the tx - :param dict metadata: The computed metadata of the transaction - :return: A new transfer - :rtype: Transfer - """ - return cls(hash, TransferState.VALIDATING, blockUID, metadata, False) - - @classmethod - def load(cls, data): - """ - Create a new transfer from a dict in json format. - :param dict data: The loaded data - :return: A new transfer - :rtype: Transfer - """ - return cls(data['hash'], - TransferState[data['state']], - BlockUID.from_str(data['blockUID']) if data['blockUID'] else None, - data['metadata'], data['local']) - - def jsonify(self): - """ - :return: The transfer as a dict in json format - """ - return {'hash': self.sha_hash, - 'state': self.state.name, - 'blockUID': str(self.blockUID) if self.blockUID else None, - 'metadata': self._metadata, - 'local': self._locally_created} - - @property - def metadata(self): - """ - :return: this transfer metadata - """ - return self._metadata - - def _not_found_in_blockchain(self, rollback, block, mediantime_target, mediantime_blocks): - """ - Check if the transaction could not be found in the blockchain - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to look for the tx - :param int mediantime_target: The mediantime to mine a block in the community parameters - :param int mediantime_blocks: The number of block used to derive the mediantime - :return: True if the transaction could not be found in a given time - :rtype: bool - """ - if not rollback: - for tx in block.transactions: - if tx.sha_hash == self.sha_hash: - return False - if block.time > self.metadata['time'] + mediantime_target*mediantime_blocks: - return True - return False - - def _found_in_block(self, rollback, block): - """ - Check if the transaction can be found in the blockchain - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to check for the transaction - :return: True if the transaction was found - :rtype: bool - """ - if not rollback: - for tx in block.transactions: - if tx.sha_hash == self.sha_hash: - return True - return False - - def _broadcast_success(self, ret_codes, block): - """ - Check if the retcode is 200 after a POST - :param list ret_codes: The POST return codes of the broadcast - :param duniterpy.documents.Block block: The current block used for transition. - :return: True if the post was successful - :rtype: bool - """ - return 200 in ret_codes - - def _broadcast_failure(self, ret_codes): - """ - Check if no retcode is 200 after a POST - :param list ret_codes: The POST return codes of the broadcast - :return: True if the post was failed - :rtype: bool - """ - return 200 not in ret_codes - - def _reached_enough_confrmation(self, rollback, current_block, fork_window): - """ - Check if the transfer reached enough confrmation in the blockchain - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block current_block: The current block of the main blockchain - :param int fork_window: The number of confrmations needed on the network - :return: True if the transfer reached enough confrmations - :rtype: bool - """ - return not rollback and self.blockUID.number + fork_window <= current_block.number - - def _rollback_and_removed(self, rollback, block): - """ - Check if the transfer is not in the block anymore - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to check for the transaction - :return: True if the transfer is not found in the block - """ - if rollback: - if not block or block.blockUID != self.blockUID: - return True - else: - return self.sha_hash not in [t.sha_hash for t in block.transactions] - return False - - def _rollback_in_fork_window(self, rollback, current_block, fork_window): - """ - Check if the transfer is not in the block anymore - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block current_block: The block to check for the transaction - :return: True if the transfer is found in the block - """ - if rollback: - return self.blockUID.number + fork_window > current_block.number - return False - - def _rollback_and_local(self, rollback, block): - """ - Check if the transfer is not in the block anymore - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block to check for the transaction - :return: True if the transfer is found in the block - """ - if rollback and self._locally_created and block.blockUID == self.blockUID: - return self.sha_hash not in [t.sha_hash for t in block.transactions] - return False - - def _is_locally_created(self): - """ - Check if we can send back the transaction if it was locally created - :return: True if the transaction was locally created - """ - return self._locally_created - - def _wait(self, current_block): - """ - Set the transfer as AWAITING confrmation. - :param duniterpy.documents.Block current_block: Current block of the main blockchain - """ - self.blockUID = current_block.blockUID - self._metadata['time'] = int(time.time()) - - def _be_validating(self, block): - """ - Action when the transfer ins found in a block - - :param bool rollback: True if we are in a rollback procedure - :param duniterpy.documents.Block block: The block checked - """ - self.blockUID = block.blockUID - self._metadata['time'] = block.mediantime - - def _drop(self): - """ - Cancel the transfer locally. - The transfer state becomes TransferState.DROPPED. - """ - self.blockUID = None - - def _try_transition(self, transition_key, inputs): - """ - Try the transition defined by the given transition_key - with inputs - :param tuple transition_key: The transition key in the table states - :param tuple inputs: The inputs - :return: True if the transition was applied - :rtype: bool - """ - if len(inputs) == len(transition_key[1]): - for i, input in enumerate(inputs): - if type(input) is not transition_key[1][i]: - return False - for transition in self._table_states[transition_key]: - if transition[0](*inputs): - if self.sha_hash: - logging.debug("{0} : {1} --> {2}".format(self.sha_hash[:5], self.state.name, - transition[2].name)) - else: - logging.debug("Unsent transfer : {0} --> {1}".format(self.state.name, - transition[2].name)) - - # If the transition changes data, apply changes - if transition[1]: - transition[1](*inputs) - self.state = transition[2] - return True - return False - - def run_state_transitions(self, inputs): - """ - Try all current state transitions with inputs - :param tuple inputs: The inputs passed to the transitions - :return: True if the transaction changed state - :rtype: bool - """ - transition_keys = [k for k in self._table_states.keys() if k[0] == self.state] - for key in transition_keys: - if self._try_transition(key, inputs): - return True - return False - - def cancel(self): - """ - Cancel a local transaction - """ - self.run_state_transitions(()) - - async def send(self, txdoc, community): - """ - Send a transaction and update the transfer state to AWAITING if accepted. - If the transaction was refused (return code != 200), state becomes REFUSED - The txdoc is saved as the transfer txdoc. - - :param txdoc: A transaction duniterpy object - :param community: The community target of the transaction - """ - self.sha_hash = txdoc.sha_hash - responses = await community.bma_access.broadcast(bma.tx.Process, - post_args={'transaction': txdoc.signed_raw()}) - blockUID = community.network.current_blockUID - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': blockUID.number}) - signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) - block_doc = Block.from_signed_raw(signed_raw) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.text() - self.run_state_transitions(([r.status for r in responses], block_doc)) - self.run_state_transitions(([r.status for r in responses], )) - return result - - async def get_raw_document(self, community): - """ - Get the raw documents of this transfer - """ - block = await community.get_block(self.blockUID.number) - if block: - block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) - for tx in block_doc.transactions: - if tx.sha_hash == self.sha_hash: - return tx diff --git a/src/sakia/core/txhistory.py b/src/sakia/core/txhistory.py deleted file mode 100644 index 7b8674e9..00000000 --- a/src/sakia/core/txhistory.py +++ /dev/null @@ -1,425 +0,0 @@ -import asyncio -import logging -import hashlib -import math -from duniterpy.documents import SimpleTransaction, Block, MalformedDocumentError -from duniterpy.api import bma, errors -from .transfer import Transfer, TransferState -from sakia.errors import NoPeerAvailable - - -class TxHistory: - def __init__(self, app, wallet): - self._latest_block = 0 - self.wallet = wallet - self.app = app - self._stop_coroutines = False - self._running_refresh = [] - self._transfers = [] - self.available_sources = [] - self._dividends = [] - - @property - def latest_block(self): - return self._latest_block - - @latest_block.setter - def latest_block(self, value): - self._latest_block = value - - def load_from_json(self, data, version): - """ - Load the tx history cache from json data - - :param dict data: the data - :param version: the version parsed with pkg_resources.parse_version - :return: - """ - self._transfers = [] - - data_sent = data['transfers'] - for s in data_sent: - self._transfers.append(Transfer.load(s)) - - for s in data['sources']: - self.available_sources.append(s.copy()) - - for d in data['dividends']: - d['state'] = TransferState[d['state']] - self._dividends.append(d) - - self.latest_block = data['latest_block'] - - def jsonify(self): - data_transfer = [] - for s in self.transfers: - data_transfer.append(s.jsonify()) - - data_sources = [] - for s in self.available_sources: - data_sources.append(s) - - data_dividends = [] - for d in self._dividends: - dividend = { - 'block_number': d['block_number'], - "consumed": d['consumed'], - 'time': d['time'], - 'id': d['id'], - 'amount': d['amount'], - 'base': d['base'], - 'state': d['state'].name - } - data_dividends.append(dividend) - - return {'latest_block': self.latest_block, - 'transfers': data_transfer, - 'sources': data_sources, - 'dividends': data_dividends} - - @property - def transfers(self): - return [t for t in self._transfers if t.state != TransferState.DROPPED] - - @property - def dividends(self): - return self._dividends.copy() - - def stop_coroutines(self, closing=False): - self._stop_coroutines = True - - async def _get_block_doc(self, community, number): - """ - Retrieve the current block document - :param sakia.core.Community community: The community we look for a block - :param int number: The block number to retrieve - :return: the block doc or None if no block was found - """ - tries = 0 - block_doc = None - block = None - while block is None and tries < 3: - try: - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': number}) - signed_raw = "{0}{1}\n".format(block['raw'], - block['signature']) - try: - block_doc = Block.from_signed_raw(signed_raw) - except TypeError: - logging.debug("Error in {0}".format(number)) - block = None - tries += 1 - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - block = None - tries += 1 - return block_doc - - async def _parse_transaction(self, community, tx, blockUID, - mediantime, received_list, txid): - """ - Parse a transaction - :param sakia.core.Community community: The community - :param duniterpy.documents.Transaction tx: The tx json data - :param duniterpy.documents.BlockUID blockUID: The block id where we found the tx - :param int mediantime: Median time on the network - :param list received_list: The list of received transactions - :param int txid: The latest txid - :return: the found transaction - """ - receivers = [o.conditions.left.pubkey for o in tx.outputs - if o.conditions.left.pubkey != tx.issuers[0]] - - if len(receivers) == 0: - receivers = [tx.issuers[0]] - - try: - issuer = await self.wallet._identities_registry.future_find(tx.issuers[0], community) - issuer_uid = issuer.uid - except LookupFailureError: - issuer_uid = "" - - try: - receiver = await self.wallet._identities_registry.future_find(receivers[0], community) - receiver_uid = receiver.uid - except LookupFailureError: - receiver_uid = "" - - metadata = { - 'time': mediantime, - 'comment': tx.comment, - 'issuer': tx.issuers[0], - 'issuer_uid': issuer_uid, - 'receiver': receivers[0], - 'receiver_uid': receiver_uid, - 'txid': txid - } - - in_issuers = len([i for i in tx.issuers - if i == self.wallet.pubkey]) > 0 - in_outputs = len([o for o in tx.outputs - if o.conditions.left.pubkey == self.wallet.pubkey]) > 0 - - tx_hash = hashlib.sha256(tx.signed_raw().encode("ascii")).hexdigest().upper() - # If the wallet pubkey is in the issuers we sent this transaction - if in_issuers: - outputs = [o for o in tx.outputs - if o.conditions.left.pubkey != self.wallet.pubkey] - amount = 0 - for o in outputs: - amount += o.amount * math.pow(10, o.base) - metadata['amount'] = amount - transfer = Transfer.create_from_blockchain(tx_hash, - blockUID, - metadata.copy()) - return transfer - # If we are not in the issuers, - # maybe we are in the recipients of this transaction - elif in_outputs: - outputs = [o for o in tx.outputs - if o.conditions.left.pubkey == self.wallet.pubkey] - amount = 0 - for o in outputs: - amount += o.amount * math.pow(10, o.base) - metadata['amount'] = amount - - transfer = Transfer.create_from_blockchain(tx_hash, - blockUID, - metadata.copy()) - received_list.append(transfer) - return transfer - return None - - async def _parse_block(self, community, block_number, received_list, txmax): - """ - Parse a block - :param sakia.core.Community community: The community - :param int block_number: The block to request - :param list received_list: The list where we are appending transactions - :param int txmax: Latest tx id - :return: The list of transfers sent - """ - block_doc = await self._get_block_doc(community, block_number) - transfers = [] - if block_doc: - for transfer in [t for t in self._transfers if t.state == TransferState.AWAITING]: - transfer.run_state_transitions((False, block_doc)) - - new_tx = [t for t in block_doc.transactions - if t.sha_hash not in [trans.sha_hash for trans in self._transfers] - and SimpleTransaction.is_simple(t)] - - for (txid, tx) in enumerate(new_tx): - transfer = await self._parse_transaction(community, tx, block_doc.blockUID, - block_doc.mediantime, received_list, txid+txmax) - if transfer: - #logging.debug("Transfer amount : {0}".format(transfer.metadata['amount'])) - transfers.append(transfer) - else: - pass - #logging.debug("None transfer") - else: - logging.debug("Could not find or parse block {0}".format(block_number)) - return transfers - - async def request_dividends(self, community, parsed_block): - for i in range(0, 6): - try: - dividends_data = await community.bma_access.future_request(bma.ud.History, - req_args={'pubkey': self.wallet.pubkey}) - - dividends = dividends_data['history']['history'].copy() - - for d in dividends: - if d['block_number'] < parsed_block: - dividends.remove(d) - return dividends - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - pass - return {} - - async def _refresh(self, community, block_number_from, block_to, received_list): - """ - Refresh last transactions - - :param sakia.core.Community community: The community - :param list received_list: List of transactions received - """ - new_transfers = [] - new_dividends = [] - try: - logging.debug("Refresh from : {0} to {1}".format(block_number_from, block_to['number'])) - dividends = await self.request_dividends(community, block_number_from) - with_tx_data = await community.bma_access.future_request(bma.blockchain.TX) - blocks_with_tx = with_tx_data['result']['blocks'] - while block_number_from <= block_to['number']: - udid = 0 - for d in [ud for ud in dividends if ud['block_number'] == block_number_from]: - state = TransferState.VALIDATED if block_number_from + MAX_CONFIRMATIONS <= block_to['number'] \ - else TransferState.VALIDATING - - if d['block_number'] not in [ud['block_number'] for ud in self._dividends]: - d['id'] = udid - d['state'] = state - new_dividends.append(d) - - udid += 1 - else: - known_dividend = [ud for ud in self._dividends - if ud['block_number'] == d['block_number']][0] - known_dividend['state'] = state - - # We parse only blocks with transactions - if block_number_from in blocks_with_tx: - transfers = await self._parse_block(community, block_number_from, - received_list, - udid + len(new_transfers)) - new_transfers += transfers - - self.wallet.refresh_progressed.emit(block_number_from, block_to['number'], self.wallet.pubkey) - block_number_from += 1 - - signed_raw = "{0}{1}\n".format(block_to['raw'], - block_to['signature']) - block_to = Block.from_signed_raw(signed_raw) - for transfer in [t for t in self._transfers + new_transfers if t.state == TransferState.VALIDATING]: - transfer.run_state_transitions((False, block_to, MAX_CONFIRMATIONS)) - - # We check if latest parsed block_number is a new high number - if block_number_from > self.latest_block: - self.available_sources = await self.wallet.sources(community) - if self._stop_coroutines: - return - self.latest_block = block_number_from - - parameters = await community.parameters() - for transfer in [t for t in self._transfers if t.state == TransferState.AWAITING]: - transfer.run_state_transitions((False, block_to, - parameters['avgGenTime'], parameters['medianTimeBlocks'])) - except (MalformedDocumentError, NoPeerAvailable) as e: - logging.debug(str(e)) - self.wallet.refresh_finished.emit([]) - return - - self._transfers = self._transfers + new_transfers - self._dividends = self._dividends + new_dividends - - self.wallet.refresh_finished.emit(received_list) - - async def _check_block(self, community, block_number): - """ - Parse a block - :param sakia.core.Community community: The community - :param int block_number: The block to check for transfers - """ - block_doc = await self._get_block_doc(community, block_number) - if block_doc: - # We check the block dividend state - match = [d for d in self._dividends if d['block_number'] == block_number] - if len(match) > 0: - if block_doc.ud: - match[0]['amount'] = block_doc.ud - match[0]['base'] = block_doc.unit_base - else: - self._dividends.remove(match[0]) - - # We check if transactions are still present - for transfer in [t for t in self._transfers - if t.state in (TransferState.VALIDATING, TransferState.VALIDATED) and - t.blockUID.number == block_number]: - if transfer.blockUID.sha_hash == block_doc.blockUID.sha_hash: - return True - transfer.run_state_transitions((True, block_doc)) - else: - logging.debug("Could not get block document") - return False - - async def _rollback(self, community): - """ - Rollback last transactions until we find one still present - in the main blockchain - - :param sakia.core.Community community: The community - """ - try: - logging.debug("Rollback from : {0}".format(self.latest_block)) - # We look for the block goal to check for rollback, - # depending on validating and validated transfers... - tx_blocks = [tx.blockUID.number for tx in self._transfers - if tx.state in (TransferState.VALIDATED, TransferState.VALIDATING) and - tx.blockUID is not None] - ud_blocks = [ud['block_number'] for ud in self._dividends - if ud['state'] in (TransferState.AWAITING, TransferState.VALIDATING)] - blocks = tx_blocks + ud_blocks - blocks.reverse() - for i, block_number in enumerate(blocks): - self.wallet.refresh_progressed.emit(i, len(blocks), self.wallet.pubkey) - if await self._check_block(community, block_number): - break - - current_block = await self._get_block_doc(community, community.network.current_blockUID.number) - if current_block: - members_pubkeys = await community.members_pubkeys() - for transfer in [t for t in self._transfers - if t.state == TransferState.VALIDATED]: - transfer.run_state_transitions((True, current_block, MAX_CONFIRMATIONS)) - except NoPeerAvailable: - logging.debug("No peer available") - - async def refresh(self, community, received_list): - # We update the block goal - try: - current_block_number = community.network.current_blockUID.number - if current_block_number: - current_block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': current_block_number}) - members_pubkeys = await community.members_pubkeys() - # We look for the first block to parse, depending on awaiting and validating transfers and ud... - tx_blocks = [tx.blockUID.number for tx in self._transfers - if tx.state in (TransferState.AWAITING, TransferState.VALIDATING) \ - and tx.blockUID is not None] - ud_blocks = [ud['block_number'] for ud in self._dividends - if ud['state'] in (TransferState.AWAITING, TransferState.VALIDATING)] - blocks = tx_blocks + ud_blocks + \ - [max(0, self.latest_block - MAX_CONFIRMATIONS)] - block_from = min(set(blocks)) - - await self._wait_for_previous_refresh() - if block_from < current_block["number"]: - # Then we start a new one - logging.debug("Starts a new refresh") - task = asyncio.ensure_future(self._refresh(community, block_from, current_block, received_list)) - self._running_refresh.append(task) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - logging.debug("Block not found") - except NoPeerAvailable: - logging.debug("No peer available") - - async def rollback(self, community, received_list): - await self._wait_for_previous_refresh() - # Then we start a new one - logging.debug("Starts a new rollback") - task = asyncio.ensure_future(self._rollback(community)) - self._running_refresh.append(task) - - # Then we start a refresh to check for new transactions - await self.refresh(community, received_list) - - async def _wait_for_previous_refresh(self): - # We wait for current refresh coroutines - if len(self._running_refresh) > 0: - logging.debug("Wait for the end of previous refresh") - done, pending = await asyncio.wait(self._running_refresh) - for cor in done: - try: - self._running_refresh.remove(cor) - except ValueError: - logging.debug("Task already removed.") - for p in pending: - logging.debug("Still waiting for : {0}".format(p)) - logging.debug("Previous refresh finished") - else: - logging.debug("No previous refresh") diff --git a/src/sakia/core/wallet.py b/src/sakia/core/wallet.py deleted file mode 100644 index 19fa06c1..00000000 --- a/src/sakia/core/wallet.py +++ /dev/null @@ -1,420 +0,0 @@ -""" -Created on 1 févr. 2014 - -@author: inso -""" - -from duniterpy.documents.transaction import InputSource, OutputSource, Unlock, SIGParameter, Transaction, reduce_base -from duniterpy.grammars import output -from duniterpy.key import SigningKey - -from duniterpy.api import bma -from duniterpy.api.bma import PROTOCOL_VERSION -from sakia.errors import NoPeerAvailable -from .transfer import Transfer -from .txhistory import TxHistory -from .. import __version__ - -from pkg_resources import parse_version -from PyQt5.QtCore import QObject, pyqtSignal - -import logging -import asyncio - - -class Wallet(QObject): - """ - A wallet is used to manage money with a unique key. - """ - refresh_progressed = pyqtSignal(int, int, str) - refresh_finished = pyqtSignal(list) - - def __init__(self, walletid, pubkey, name, identities_registry): - """ - Constructor of a wallet object - - :param int walletid: The wallet number, unique between all wallets - :param str pubkey: The wallet pubkey - :param str name: The wallet name - """ - super().__init__() - self.coins = [] - self.walletid = walletid - self.pubkey = pubkey - self.name = name - self._identities_registry = identities_registry - self.caches = {} - - @classmethod - def create(cls, walletid, salt, password, name, identities_registry): - """ - Factory method to create a new wallet - - :param int walletid: The wallet number, unique between all wallets - :param str salt: The account salt - :param str password: The account password - :param str name: The account name - """ - if walletid == 0: - key = SigningKey(salt, password) - else: - key = SigningKey(b"{0}{1}".format(salt, walletid), password) - return cls(walletid, key.pubkey, name, identities_registry) - - @classmethod - def load(cls, json_data, identities_registry): - """ - Factory method to load a saved wallet. - - :param dict json_data: The wallet as a dict in json format - """ - walletid = json_data['walletid'] - pubkey = json_data['pubkey'] - name = json_data['name'] - return cls(walletid, pubkey, name, identities_registry) - - def load_caches(self, app, json_data): - """ - Load this wallet caches. - Each cache correspond to one different community. - - :param dict json_data: The caches as a dict in json format - """ - version = parse_version(json_data['version']) - for currency in json_data: - if currency != 'version': - self.caches[currency] = TxHistory(app, self) - if version >= parse_version("0.20.dev0"): - self.caches[currency].load_from_json(json_data[currency], version) - - def jsonify_caches(self): - """ - Get this wallet caches as json. - - :return: The wallet caches as a dict in json format - """ - data = {} - for currency in self.caches: - data[currency] = self.caches[currency].jsonify() - return data - - def init_cache(self, app, community): - """ - Init the cache of this wallet for the specified community. - - :param community: The community to refresh its cache - """ - if community.currency not in self.caches: - self.caches[community.currency] = TxHistory(app, self) - - def refresh_transactions(self, community, received_list): - """ - Refresh the cache of this wallet for the specified community. - - :param community: The community to refresh its cache - """ - logging.debug("Refresh transactions for {0}".format(self.pubkey)) - asyncio.ensure_future(self.caches[community.currency].refresh(community, received_list)) - - def rollback_transactions(self, community, received_list): - """ - Rollback the transactions of this wallet for the specified community. - - :param community: The community to refresh its cache - """ - logging.debug("Refresh transactions for {0}".format(self.pubkey)) - asyncio.ensure_future(self.caches[community.currency].rollback(community, received_list)) - - def check_password(self, salt, password): - """ - Check if wallet password is ok. - - :param salt: The account salt - :param password: The given password - :return: True if (salt, password) generates the good public key - .. warning:: Generates a new temporary SigningKey from salt and password - """ - key = None - if self.walletid == 0: - key = SigningKey(salt, password) - else: - key = SigningKey("{0}{1}".format(salt, self.walletid), password) - return (key.pubkey == self.pubkey) - - async def relative_value(self, community): - """ - Get wallet value relative to last generated UD - - :param community: The community to get value - :return: The wallet relative value - """ - value = await self.value(community) - ud = community.dividend - relative_value = value / float(ud) - return relative_value - - async def value(self, community): - """ - Get wallet absolute value - - :param community: The community to get value - :return: The wallet absolute value - """ - value = 0 - sources = await self.sources(community) - for s in sources: - value += s['amount'] * pow(10, s['base']) - return value - - def tx_sources(self, amount, community): - """ - Get inputs to generate a transaction with a given amount of money - - :param int amount: The amount target value - :param community: The community target of the transaction - - :return: The list of inputs to use in the transaction document - """ - - # such a dirty algorithmm - # everything should be done again from scratch - # in future versions - - def current_value(inputs, overhs): - i = 0 - for s in inputs: - i += s['amount'] * (10**s['base']) - for o in overhs: - i -= o[0] * (10**o[1]) - return i - - amount, amount_base = reduce_base(amount, 0) - cache = self.caches[community.currency] - current_base = max([src['base'] for src in cache.available_sources]) - value = 0 - sources = [] - outputs = [] - overheads = [] - buf_sources = list(cache.available_sources) - while current_base >= 0: - for s in [src for src in cache.available_sources if src['base'] == current_base]: - test_sources = sources + [s] - val = current_value(test_sources, overheads) - # if we have to compute an overhead - if current_value(test_sources, overheads) > amount * (10**amount_base): - overhead = current_value(test_sources, overheads) - int(amount) * (10**amount_base) - # we round the overhead in the current base - # exemple : 12 in base 1 -> 1*10^1 - overhead = int(round(float(overhead) / (10**current_base))) - source_value = s['amount'] * (10**s['base']) - out = int((source_value - (overhead * (10**current_base)))/(10**current_base)) - if out * (10**current_base) <= amount * (10**amount_base): - sources.append(s) - buf_sources.remove(s) - overheads.append((overhead, current_base)) - outputs.append((out, current_base)) - # else just add the output - else: - sources.append(s) - buf_sources.remove(s) - outputs.append((s['amount'] , s['base'])) - if current_value(sources, overheads) == amount * (10 ** amount_base): - return sources, outputs, overheads, buf_sources - - current_base -= 1 - - raise NotEnoughMoneyError(value, community.currency, - len(sources), amount * pow(10, amount_base)) - - def tx_inputs(self, sources): - """ - Get inputs to generate a transaction with a given amount of money - - :param list sources: The sources used to send the given amount of money - - :return: The list of inputs to use in the transaction document - """ - inputs = [] - for s in sources: - inputs.append(InputSource(s['amount'], s['base'], s['type'], s['identifier'], s['noffset'])) - return inputs - - def tx_unlocks(self, sources): - """ - Get unlocks to generate a transaction with a given amount of money - - :param list sources: The sources used to send the given amount of money - - :return: The list of unlocks to use in the transaction document - """ - unlocks = [] - for i, s in enumerate(sources): - unlocks.append(Unlock(i, [SIGParameter(0)])) - return unlocks - - def tx_outputs(self, pubkey, outputs, overheads): - """ - Get outputs to generate a transaction with a given amount of money - - :param str pubkey: The target pubkey of the transaction - :param list outputs: The amount to send - :param list inputs: The inputs used to send the given amount of money - :param list overheads: The overheads used to send the given amount of money - - :return: The list of outputs to use in the transaction document - """ - total = [] - outputs_bases = set(o[1] for o in outputs) - for base in outputs_bases: - output_sum = 0 - for o in outputs: - if o[1] == base: - output_sum += o[0] - total.append(OutputSource(output_sum, base, output.Condition.token(output.SIG.token(pubkey)))) - - overheads_bases = set(o[1] for o in overheads) - for base in overheads_bases: - overheads_sum = 0 - for o in overheads: - if o[1] == base: - overheads_sum += o[0] - total.append(OutputSource(overheads_sum, base, output.Condition.token(output.SIG.token(self.pubkey)))) - - return total - - def prepare_tx(self, pubkey, blockstamp, amount, message, community): - """ - Prepare a simple Transaction document - :param str pubkey: the target of the transaction - :param duniterpy.documents.BlockUID blockstamp: the blockstamp - :param int amount: the amount sent to the receiver - :param Community community: the target community - :return: the transaction document - :rtype: duniterpy.documents.Transaction - """ - result = self.tx_sources(int(amount), community) - sources = result[0] - computed_outputs = result[1] - overheads = result[2] - self.caches[community.currency].available_sources = result[3][1:] - logging.debug("Inputs : {0}".format(sources)) - - inputs = self.tx_inputs(sources) - unlocks = self.tx_unlocks(sources) - outputs = self.tx_outputs(pubkey, computed_outputs, overheads) - logging.debug("Outputs : {0}".format(outputs)) - tx = Transaction(PROTOCOL_VERSION, community.currency, blockstamp, 0, - [self.pubkey], inputs, unlocks, - outputs, message, None) - return tx - - async def send_money(self, salt, password, community, - recipient, amount, message): - """ - Send money to a given recipient in a specified community - - :param str salt: The account salt - :param str password: The account password - :param community: The community target of the transfer - :param str recipient: The pubkey of the recipient - :param int amount: The amount of money to transfer - :param str message: The message to send with the transfer - """ - try: - blockUID = community.network.current_blockUID - block = await community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': blockUID.number}) - except ValueError as e: - if '404' in str(e): - return False, "Could not send transfer with null blockchain" - - time = block['medianTime'] - txid = len(block['transactions']) - if self.walletid == 0: - key = SigningKey(salt, password) - else: - key = SigningKey("{0}{1}".format(salt, self.walletid), password) - logging.debug("Sender pubkey:{0}".format(key.pubkey)) - - issuer = await self._identities_registry.future_find(key.pubkey, community) - issuer_uid = issuer.uid - - receiver = await self._identities_registry.future_find(recipient, community) - receiver_uid = receiver.uid - - metadata = {'block': None, - 'time': time, - 'amount': amount, - 'issuer': key.pubkey, - 'issuer_uid': issuer_uid, - 'receiver': recipient, - 'receiver_uid': receiver_uid, - 'comment': message, - 'txid': txid - } - transfer = Transfer.initiate(metadata) - self.caches[community.currency]._transfers.append(transfer) - try: - tx = self.prepare_tx(recipient, blockUID, amount, message, community) - logging.debug("TX : {0}".format(tx.raw())) - - tx.sign([key]) - logging.debug("Transaction : [{0}]".format(tx.signed_raw())) - return await transfer.send(tx, community) - except NotEnoughMoneyError as e: - return (False, str(e)) - - async def sources(self, community): - """ - Get available sources in a given community - - :param sakia.core.community.Community community: The community where we want available sources - :return: List of bma sources - """ - sources = [] - try: - data = await community.bma_access.future_request(bma.tx.Sources, - req_args={'pubkey': self.pubkey}) - return data['sources'].copy() - except NoPeerAvailable as e: - logging.debug(str(e)) - return sources - - def transfers(self, community): - """ - Get all transfers objects of this wallet - - :param community: The community we want to get the executed transfers - :return: A list of Transfer objects - """ - if community.currency in self.caches: - return self.caches[community.currency].transfers - else: - return [] - - def dividends(self, community): - """ - Get all the dividends received by this wallet - - :param community: The community we want to get received dividends - :return: Result of udhistory request - """ - if community.currency in self.caches: - return self.caches[community.currency].dividends - else: - return [] - - def stop_coroutines(self, closing=False): - for c in self.caches.values(): - c.stop_coroutines(closing) - - def jsonify(self): - """ - Get the wallet as json format. - - :return: The wallet as a dict in json format. - """ - return {'walletid': self.walletid, - 'pubkey': self.pubkey, - 'name': self.name, - 'version': __version__} diff --git a/src/sakia/data/connectors/bma.py b/src/sakia/data/connectors/bma.py index 8030fc67..10eba5d6 100644 --- a/src/sakia/data/connectors/bma.py +++ b/src/sakia/data/connectors/bma.py @@ -29,9 +29,9 @@ class BmaConnector: else: return True filters = { - bma.ud.History: lambda n: compare_versions(n, "0.11.0"), - bma.tx.History: lambda n: compare_versions(n, "0.11.0"), - bma.blockchain.Membership: lambda n: compare_versions(n, "0.14") + bma.ud.history: lambda n: compare_versions(n, "0.11.0"), + bma.tx.history: lambda n: compare_versions(n, "0.11.0"), + bma.blockchain.membership: lambda n: compare_versions(n, "0.14") } if request in filters: nodes = [n for n in nodes if filters[request](n)] @@ -55,11 +55,10 @@ class BmaConnector: tries = 0 while tries < 3: endpoint = random.choice(endpoints) - req = request(endpoint.conn_handler(), **req_args) try: - self._logger.debug("Requesting {0} on endpoint {1}".format(str(req), str(endpoint))) + self._logger.debug("Requesting {0} on endpoint {1}".format(str(request.__name__), str(endpoint))) with aiohttp.ClientSession() as session: - json_data = await req.get(**get_args, session=session) + json_data = await request(endpoint.conn_handler(session), **req_args) return json_data except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError, jsonschema.ValidationError) as e: diff --git a/src/sakia/data/connectors/node.py b/src/sakia/data/connectors/node.py index 006cbc89..1dde86ca 100644 --- a/src/sakia/data/connectors/node.py +++ b/src/sakia/data/connectors/node.py @@ -61,7 +61,7 @@ class NodeConnector(QObject): :return: A new node :rtype: sakia.core.net.Node """ - peer_data = await bma.network.Peering(ConnectionHandler(address, port)).get(session) + peer_data = await bma.network.peering(ConnectionHandler(address, port, session)) peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['raw'], peer_data['signature'])) @@ -92,10 +92,10 @@ class NodeConnector(QObject): return cls(node, session) - async def safe_request(self, endpoint, request, req_args={}, get_args={}): + async def safe_request(self, endpoint, request, req_args={}): try: - conn_handler = endpoint.conn_handler() - data = await request(conn_handler, **req_args).get(self._session, **get_args) + conn_handler = endpoint.conn_handler(self._session) + data = await request(conn_handler, **req_args) return data except (ClientError, gaierror, TimeoutError, ConnectionRefusedError, DisconnectedError, ValueError) as e: self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) @@ -150,9 +150,8 @@ class NodeConnector(QObject): for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: if not self._connected['block']: try: - conn_handler = endpoint.conn_handler() - block_websocket = bma.ws.Block(conn_handler) - ws_connection = block_websocket.connect(self._session) + conn_handler = endpoint.conn_handler(self._session) + ws_connection = bma.ws.block(conn_handler) async with ws_connection as ws: self._connected['block'] = True self._logger.debug("Connected successfully to block ws : {0}" @@ -160,7 +159,7 @@ class NodeConnector(QObject): async for msg in ws: if msg.tp == aiohttp.MsgType.text: self._logger.debug("Received a block : {0}".format(self.node.pubkey[:5])) - block_data = block_websocket.parse_text(msg.data) + block_data = bma.parse_text(msg.data) await self.refresh_block(block_data) elif msg.tp == aiohttp.MsgType.closed: break @@ -191,7 +190,7 @@ class NodeConnector(QObject): """ for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: try: - block_data = await self.safe_request(endpoint, bma.blockchain.Current, self._session) + block_data = await self.safe_request(endpoint, bma.blockchain.current) if not block_data: continue await self.refresh_block(block_data) @@ -220,7 +219,7 @@ class NodeConnector(QObject): conn_handler = endpoint.conn_handler() self._logger.debug("Requesting {0}".format(conn_handler)) try: - previous_block = await self.safe_request(endpoint, bma.blockchain.Block, + previous_block = await self.safe_request(endpoint, bma.blockchain.block, req_args={'number': self.node.current_buid.number}) if not previous_block: continue @@ -252,7 +251,7 @@ class NodeConnector(QObject): """ for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: try: - summary_data = await self.safe_request(endpoint, bma.node.Summary) + summary_data = await self.safe_request(endpoint, bma.node.summary) if not summary_data: continue self.node.software = summary_data["duniter"]["software"] @@ -276,9 +275,8 @@ class NodeConnector(QObject): """ for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: try: - data = await self.safe_request(endpoint, bma.wot.Lookup, - req_args={'search':self.node.pubkey}, - get_args={}) + data = await self.safe_request(endpoint, bma.wot.lookup, + req_args={'search':self.node.pubkey}) if not data: continue self.node.state = Node.ONLINE @@ -314,16 +312,15 @@ class NodeConnector(QObject): for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: if not self._connected['peer']: try: - conn_handler = endpoint.conn_handler() - peer_websocket = bma.ws.Peer(conn_handler) - ws_connection = peer_websocket.connect(self._session) + conn_handler = endpoint.conn_handler(self._session) + ws_connection = bma.ws.peer(conn_handler) async with ws_connection as ws: self._connected['peer'] = True self._logger.debug("Connected successfully to peer ws : {0}".format(self.node.pubkey[:5])) async for msg in ws: if msg.tp == aiohttp.MsgType.text: self._logger.debug("Received a peer : {0}".format(self.node.pubkey[:5])) - peer_data = peer_websocket.parse_text(msg.data) + peer_data = bma.parse_text(msg.data) self.refresh_peer_data(peer_data) elif msg.tp == aiohttp.MsgType.closed: break @@ -357,8 +354,8 @@ class NodeConnector(QObject): """ for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: try: - peers_data = await self.safe_request(endpoint, bma.network.peering.Peers, - get_args={'leaves': 'true'}) + peers_data = await self.safe_request(endpoint, bma.network.peers, + req_args={'leaves': 'true'}) if not peers_data: continue self.node.state = Node.ONLINE @@ -368,8 +365,8 @@ class NodeConnector(QObject): for leaf_hash in leaves: try: leaf_data = await self.safe_request(endpoint, - bma.network.peering.Peers, - get_args={'leaf': leaf_hash}) + bma.network.peers, + req_args={'leaf': leaf_hash}) if not leaf_data: break self.refresh_peer_data(leaf_data['leaf']['value']) diff --git a/src/sakia/data/entities/identity.py b/src/sakia/data/entities/identity.py index e22e337d..a3ef5a2f 100644 --- a/src/sakia/data/entities/identity.py +++ b/src/sakia/data/entities/identity.py @@ -1,5 +1,5 @@ import attr -from duniterpy.documents import block_uid, BlockUID, SelfCertification +from duniterpy.documents import block_uid, BlockUID, Identity from duniterpy import PROTOCOL_VERSION @@ -24,7 +24,7 @@ class Identity: Creates a self cert document for a given identity :param sakia.data.entities.Identity identity: :return: the document - :rtype: duniterpy.documents.SelfCertification + :rtype: duniterpy.documents.Identity """ - return SelfCertification(PROTOCOL_VERSION, self.currency, self.pubkey, + return Identity(PROTOCOL_VERSION, self.currency, self.pubkey, self.uid, self.blockstamp, self.signature) diff --git a/src/sakia/data/entities/transaction.py b/src/sakia/data/entities/transaction.py index 0b59ffc8..1cdde841 100644 --- a/src/sakia/data/entities/transaction.py +++ b/src/sakia/data/entities/transaction.py @@ -4,6 +4,15 @@ from duniterpy.documents import block_uid @attr.s() class Transaction: + + TO_SEND = 0 + AWAITING = 1 + VALIDATING = 2 + VALIDATED = 4 + REFUSED = 8 + DROPPED = 16 + LOCAL = 128 + currency = attr.ib(convert=str, cmp=False) sha_hash = attr.ib(convert=str) written_on = attr.ib(convert=block_uid, cmp=False) @@ -16,3 +25,8 @@ class Transaction: amount_base = attr.ib(convert=int, cmp=False) comment = attr.ib(convert=str, cmp=False) txid = attr.ib(convert=int, cmp=False) + state = attr.ib(convert=int, cmp=False) + + @property + def local(self): + return self.state & Transaction.LOCAL == Transaction.LOCAL diff --git a/src/sakia/data/processors/__init__.py b/src/sakia/data/processors/__init__.py index 3e7dd0f5..d4559613 100644 --- a/src/sakia/data/processors/__init__.py +++ b/src/sakia/data/processors/__init__.py @@ -4,3 +4,4 @@ from .certifications import CertificationsProcessor from .blockchain import BlockchainProcessor from .connections import ConnectionsProcessor from .sources import SourcesProcessor +from .transactions import TransactionsProcessor diff --git a/src/sakia/data/processors/certifications.py b/src/sakia/data/processors/certifications.py index a1670f95..c49d9253 100644 --- a/src/sakia/data/processors/certifications.py +++ b/src/sakia/data/processors/certifications.py @@ -34,9 +34,9 @@ class CertificationsProcessor: :return: the instanciated certification :rtype: sakia.data.entities.Certification """ - cert = Certification(currency, cert.pubkey_from, cert.pubkey_to, cert.timestamp, + cert = Certification(currency, cert.pubkey_from, cert.pubkey_to, cert.timestamp.number, 0, cert.signatures[0], blockstamp) - self._repo.insert(cert) + self._certifications_repo.insert(cert) return cert def commit_certification(self, cert): diff --git a/src/sakia/data/processors/identities.py b/src/sakia/data/processors/identities.py index 96efad15..f87543bc 100644 --- a/src/sakia/data/processors/identities.py +++ b/src/sakia/data/processors/identities.py @@ -7,7 +7,7 @@ from ..connectors import BmaConnector from ..processors import NodesProcessor from duniterpy.api import bma, errors from duniterpy.key import SigningKey -from duniterpy.documents import SelfCertification, BlockUID, block_uid +from duniterpy.documents import Identity, BlockUID, block_uid from aiohttp.errors import ClientError from sakia.errors import NoPeerAvailable @@ -97,23 +97,26 @@ class IdentitiesProcessor: """ return self._identities_repo.get_written(currency=currency, pubkey=pubkey) - def get_identity(self, currency, pubkey, uid): + def get_identity(self, currency, pubkey, uid=""): """ Return the identity corresponding to a given pubkey, uid and currency :param str currency: :param str pubkey: - :param str uid: + :param str uid: optionally, specify an uid to lookup :rtype: sakia.data.entities.Identity """ written = self.get_written(currency=currency, pubkey=pubkey) if not written: - identities = self._identities_repo.get_all(currency=currency, pubkey=pubkey, uid=uid) - recent = identities[0] - for i in identities: - if i.blockstamp > recent.blockstamp: - recent = i - return recent + identities = self._identities_repo.get_all(currency=currency, pubkey=pubkey) + if identities: + recent = identities[0] + for i in identities: + if i.blockstamp > recent.blockstamp and i.uid == uid: + recent = i + return recent + else: + return written[0] def commit_identity(self, identity): """ @@ -166,7 +169,7 @@ class IdentitiesProcessor: blockchain = self._blockchain_repo.get_one(currency=currency) block_uid = blockchain.current_buid timestamp = blockchain.median_time - selfcert = SelfCertification(2, + selfcert = Identity(2, currency, identity.pubkey, identity.uid, diff --git a/src/sakia/data/processors/transactions.py b/src/sakia/data/processors/transactions.py new file mode 100644 index 00000000..277bba41 --- /dev/null +++ b/src/sakia/data/processors/transactions.py @@ -0,0 +1,114 @@ +import attr +import re +from ..entities import Transaction +from .nodes import NodesProcessor +from . import tx_lifecycle +from ..connectors import BmaConnector +from duniterpy.api import bma, errors +from duniterpy.documents import Block, BMAEndpoint +import asyncio +import time + + +@attr.s +class TransactionsProcessor: + _repo = attr.ib() # :type sakia.data.repositories.SourcesRepo + _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector + _table_states = attr.ib(default=attr.Factory(dict)) + + @classmethod + def instanciate(cls, app): + """ + Instanciate a blockchain processor + :param sakia.app.Application app: the app + """ + return cls(app.db.transactions_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo))) + + def _try_transition(self, tx, transition_key, inputs): + """ + Try the transition defined by the given transition_key + with inputs + :param sakia.data.entities.Transaction tx: the transaction + :param tuple transition_key: The transition key in the table states + :param tuple inputs: The inputs + :return: True if the transition was applied + :rtype: bool + """ + if len(inputs) == len(transition_key[1]): + for i, input in enumerate(inputs): + if type(input) is not transition_key[1][i]: + return False + for transition in tx_lifecycle.states[transition_key]: + if transition[0](*inputs): + if tx.sha_hash: + self._logger.debug("{0} : {1} --> {2}".format(tx.sha_hash[:5], tx.state, + transition[2].name)) + else: + self._logger.debug("Unsent transfer : {0} --> {1}".format(tx.state, + transition[2].name)) + + # If the transition changes data, apply changes + if transition[1]: + transition[1](tx, *inputs) + tx.state = transition[2] | tx.local + return True + return False + + def find_by_hash(self, sha_hash): + return self._repo.find_one(sha_hash=sha_hash) + + def awaiting(self): + return self._repo.find_all(state=Transaction.AWAITING) + \ + self._repo.find_all(state=Transaction.AWAITING | Transaction.LOCAL) + + def run_state_transitions(self, tx, *inputs): + """ + Try all current state transitions with inputs + :param sakia.data.entities.Transaction tx: the transaction + :param tuple inputs: The inputs passed to the transitions + :return: True if the transaction changed state + :rtype: bool + """ + transition_keys = [k for k in tx_lifecycle.states.keys() if k[0] | Transaction.LOCAL == tx.state] + for key in transition_keys: + if self._try_transition(tx, key, inputs): + return True + return False + + def cancel(self, tx): + """ + Cancel a local transaction + :param sakia.data.entities.Transaction tx: the transaction + """ + self.run_state_transitions(tx, ()) + + async def send(self, tx, txdoc, community): + """ + Send a transaction and update the transfer state to AWAITING if accepted. + If the transaction was refused (return code != 200), state becomes REFUSED + The txdoc is saved as the transfer txdoc. + + :param sakia.data.entities.Transaction tx: the transaction + :param txdoc: A transaction duniterpy object + :param community: The community target of the transaction + """ + tx.sha_hash = txdoc.sha_hash + responses = await community.bma_access.broadcast(bma.tx.Process, + post_args={'transaction': txdoc.signed_raw()}) + blockUID = community.network.current_blockUID + block = await community.bma_access.future_request(bma.blockchain.Block, + req_args={'number': blockUID.number}) + signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) + block_doc = Block.from_signed_raw(signed_raw) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.text() + self.run_state_transitions(tx, ([r.status for r in responses], block_doc)) + self.run_state_transitions(tx, ([r.status for r in responses],)) + return result diff --git a/src/sakia/data/processors/tx_lifecycle.py b/src/sakia/data/processors/tx_lifecycle.py new file mode 100644 index 00000000..49b10afe --- /dev/null +++ b/src/sakia/data/processors/tx_lifecycle.py @@ -0,0 +1,197 @@ +import time +from sakia.data.entities import Transaction +from duniterpy.documents import Block + +def _not_found_in_blockchain(tx, rollback, block, mediantime_target, mediantime_blocks): + """ + Check if the transaction could not be found in the blockchain + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block to look for the tx + :param int mediantime_target: The mediantime to mine a block in the community parameters + :param int mediantime_blocks: The number of block used to derive the mediantime + :return: True if the transaction could not be found in a given time + :rtype: bool + """ + if not rollback: + for block_tx in block.transactions: + if block_tx.sha_hash == tx.sha_hash: + return False + if block.time > tx.timestamp + mediantime_target * mediantime_blocks: + return True + return False + + +def _found_in_block(tx, rollback, block): + """ + Check if the transaction can be found in the blockchain + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block to check for the transaction + :return: True if the transaction was found + :rtype: bool + """ + if not rollback: + for block_tx in block.transactions: + if block_tx.sha_hash == tx.sha_hash: + return True + return False + + +def _broadcast_success(tx, ret_codes, block): + """ + Check if the retcode is 200 after a POST + :param sakia.data.entities.Transaction tx: the transaction + :param list ret_codes: The POST return codes of the broadcast + :param duniterpy.documents.Block block: The current block used for transition. + :return: True if the post was successful + :rtype: bool + """ + return 200 in ret_codes + + +def _broadcast_failure(tx, ret_codes): + """ + Check if no retcode is 200 after a POST + :param sakia.data.entities.Transaction tx: the transaction + :param list ret_codes: The POST return codes of the broadcast + :return: True if the post was failed + :rtype: bool + """ + return 200 not in ret_codes + + +def _reached_enough_confrmation(tx, rollback, current_block, fork_window): + """ + Check if the transfer reached enough confrmation in the blockchain + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block current_block: The current block of the main blockchain + :param int fork_window: The number of confrmations needed on the network + :return: True if the transfer reached enough confrmations + :rtype: bool + """ + return not rollback and tx.blockstamp.number + fork_window <= current_block.number + + +def _rollback_and_removed(tx, rollback, block): + """ + Check if the transfer is not in the block anymore + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is not found in the block + """ + if rollback: + if not block or block.blockUID != tx.blockstamp: + return True + else: + return tx.sha_hash not in [t.sha_hash for t in block.transactions] + return False + + +def _rollback_in_fork_window(tx, rollback, current_block, fork_window): + """ + Check if the transfer is not in the block anymore + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block current_block: The block to check for the transaction + :return: True if the transfer is found in the block + """ + if rollback: + return tx.blockstamp.number + fork_window > current_block.number + return False + + +def _rollback_and_local(tx, rollback, block): + """ + Check if the transfer is not in the block anymore + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is found in the block + """ + if rollback and tx.local and block.blockUID == tx.blockstamp: + return tx.sha_hash not in [t.sha_hash for t in block.transactions] + return False + + +def _is_locally_created(tx): + """ + Check if we can send back the transaction if it was locally created + + :param sakia.data.entities.Transaction tx: the transaction + :return: True if the transaction was locally created + """ + return tx.local + + +def _wait(tx, current_block): + """ + Set the transfer as AWAITING confrmation. + + :param sakia.data.entities.Transaction tx: the transaction + :param duniterpy.documents.Block current_block: Current block of the main blockchain + """ + tx.blockstamp = current_block.blockUID + tx.timestamp = int(time.time()) + + +def _be_validating(tx, block): + """ + Action when the transfer ins found in a block + + :param sakia.data.entities.Transaction tx: the transaction + :param bool rollback: True if we are in a rollback procedure + :param duniterpy.documents.Block block: The block checked + """ + tx.blockstamp = block.blockUID + tx.timestamp = block.mediantime + + +def _drop(tx): + """ + Cancel the transfer locally. + The transfer state becomes TransferState.DROPPED. + :param sakia.data.entities.Transaction tx: the transaction + """ + tx.blockstamp = None + + +# Dict containing states of a transfer : +# keys are a tuple containg (current_state, transition_parameters) +# values are tuples containing (transition_test, transition_success, new_state) +states = { + (Transaction.TO_SEND, (list, Block)): + ( + (_broadcast_success, lambda tx, l, b: _wait(tx, b), Transaction.AWAITING), + (lambda tx, l, b: _broadcast_failure(tx, l), None, Transaction.REFUSED), + ), + (Transaction.TO_SEND, ()): + ((_is_locally_created, _drop, Transaction.DROPPED),), + + (Transaction.AWAITING, (bool, Block)): + ((_found_in_block, lambda tx, r, b: _be_validating(tx, b), Transaction.VALIDATING),), + (Transaction.AWAITING, (bool, Block, int, int)): + ((_not_found_in_blockchain, None, Transaction.REFUSED),), + + (Transaction.VALIDATING, (bool, Block, int)): + ((_reached_enough_confrmation, None, Transaction.VALIDATED),), + (Transaction.VALIDATING, (bool, Block)): + ((_rollback_and_removed, lambda tx, r, b: _drop(tx), Transaction.DROPPED),), + + (Transaction.VALIDATED, (bool, Block, int)): + ((_rollback_in_fork_window, lambda tx, r, b, i: _be_validating(tx, b), Transaction.VALIDATING),), + + (Transaction.VALIDATED, (bool, Block)): + ( + (_rollback_and_removed, lambda tx, r, b: _drop(tx), Transaction.DROPPED), + (_rollback_and_local, lambda tx, r, b: _wait(tx, b), Transaction.AWAITING), + ), + + (Transaction.REFUSED, ()): + ((_is_locally_created, _drop, Transaction.DROPPED),) + } diff --git a/src/sakia/data/repositories/meta.sql b/src/sakia/data/repositories/meta.sql index e62a22c5..95513f11 100644 --- a/src/sakia/data/repositories/meta.sql +++ b/src/sakia/data/repositories/meta.sql @@ -80,6 +80,7 @@ CREATE TABLE IF NOT EXISTS transactions( amountbase INT, comment VARCHAR(255), txid INT, + state INT, PRIMARY KEY (sha_hash) ); diff --git a/src/sakia/data/repositories/transactions.py b/src/sakia/data/repositories/transactions.py index 39e42db0..041aec29 100644 --- a/src/sakia/data/repositories/transactions.py +++ b/src/sakia/data/repositories/transactions.py @@ -39,7 +39,8 @@ class TransactionsRepo: amount = ?, amountbase = ?, comment = ?, - txid = ? + txid = ?, + state = ? WHERE sha_hash=?""", updated_fields + where_fields) diff --git a/src/sakia/gui/navigation/graphs/base/model.py b/src/sakia/gui/navigation/graphs/base/model.py index aa9bb087..8e3abc81 100644 --- a/src/sakia/gui/navigation/graphs/base/model.py +++ b/src/sakia/gui/navigation/graphs/base/model.py @@ -1,5 +1,4 @@ from sakia.gui.component.model import ComponentModel -from sakia.core.registry import BlockchainState class BaseGraphModel(ComponentModel): @@ -20,12 +19,3 @@ class BaseGraphModel(ComponentModel): :rtype: sakia.core.registry.Identity """ return await self.app.identities_registry.future_find(pubkey, self.community) - - def get_identity_from_data(self, metadata, pubkey): - return self.app.identities_registry.from_handled_data( - metadata['text'], - pubkey, - None, - BlockchainState.VALIDATED, - self.community - ) diff --git a/src/sakia/gui/navigation/identities/controller.py b/src/sakia/gui/navigation/identities/controller.py index 370fa23e..6eef79a5 100644 --- a/src/sakia/gui/navigation/identities/controller.py +++ b/src/sakia/gui/navigation/identities/controller.py @@ -3,9 +3,7 @@ import logging from PyQt5.QtGui import QCursor from sakia.errors import NoPeerAvailable -from duniterpy.api import bma, errors -from duniterpy.documents.block import BlockUID -from sakia.core.registry import Identity, BlockchainState +from duniterpy.api import errors from sakia.decorators import once_at_a_time, asyncify from sakia.gui.component.controller import ComponentController from sakia.gui.widgets.context_menu import ContextMenu diff --git a/src/sakia/gui/navigation/model.py b/src/sakia/gui/navigation/model.py index 883b5201..38ec512f 100644 --- a/src/sakia/gui/navigation/model.py +++ b/src/sakia/gui/navigation/model.py @@ -43,13 +43,14 @@ class NavigationModel(ComponentModel): 'connection': connection, }, 'children': [ - # { - # 'node': { - # 'title': self.tr('Transfers'), - # 'icon': ':/icons/tx_icon', - # 'component': "TxHistory", - # } - # }, + # { + # 'node': { + # 'title': self.tr('Transfers'), + # 'icon': ':/icons/tx_icon', + # 'component': "TxHistory", + # 'transactions_service': self.app.transactions_services[connection.currency], + # } + # }, { 'node': { 'title': self.tr('Network'), diff --git a/src/sakia/gui/navigation/txhistory/table_model.py b/src/sakia/gui/navigation/txhistory/table_model.py index 001a3e98..4fcad39c 100644 --- a/src/sakia/gui/navigation/txhistory/table_model.py +++ b/src/sakia/gui/navigation/txhistory/table_model.py @@ -13,8 +13,7 @@ from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyMode QDateTime, QLocale from PyQt5.QtGui import QFont, QColor, QIcon from sakia.errors import NoPeerAvailable - -from sakia.core.transfer import Transfer, TransferState +from sakia.data.entities import Transaction from sakia.decorators import asyncify, once_at_a_time, cancel_once_task @@ -121,20 +120,20 @@ class TxFilterProxyModel(QSortFilterProxyModel): if role == Qt.FontRole: font = QFont() - if state_data == TransferState.AWAITING or state_data == TransferState.VALIDATING: + if state_data == Transaction.AWAITING or state_data == Transaction.VALIDATING: font.setItalic(True) - elif state_data == TransferState.REFUSED: + elif state_data == Transaction.REFUSED: font.setItalic(True) - elif state_data == TransferState.TO_SEND: + elif state_data == Transaction.TO_SEND: font.setBold(True) else: font.setItalic(False) return font if role == Qt.ForegroundRole: - if state_data == TransferState.REFUSED: + if state_data == Transaction.REFUSED: return QColor(Qt.red) - elif state_data == TransferState.TO_SEND: + elif state_data == Transaction.TO_SEND: return QColor(Qt.blue) if role == Qt.TextAlignmentRole: @@ -148,17 +147,17 @@ class TxFilterProxyModel(QSortFilterProxyModel): if source_index.column() == self.sourceModel().columns_types.index('date'): return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate) - if state_data == TransferState.VALIDATING or state_data == TransferState.AWAITING: + if state_data == Transaction.VALIDATING or state_data == Transaction.AWAITING: block_col = model.columns_types.index('block_number') block_index = model.index(source_index.row(), block_col) block_data = model.data(block_index, Qt.DisplayRole) current_confirmations = 0 - if state_data == TransferState.VALIDATING: + if state_data == Transaction.VALIDATING: current_blockUID_number = self.community.network.current_blockUID.number if current_blockUID_number: current_confirmations = current_blockUID_number - block_data - elif state_data == TransferState.AWAITING: + elif state_data == Transaction.AWAITING: current_confirmations = 0 max_confirmations = self.sourceModel().max_confirmations() @@ -317,7 +316,7 @@ class HistoryTableModel(QAbstractTableModel): for transfer in transfers: coro = None count += 1 - if type(transfer) is Transfer: + if type(transfer) is Transaction: if transfer.metadata['issuer'] == self.account.pubkey: coro = asyncio.ensure_future(self.data_sent(transfer)) else: diff --git a/src/sakia/gui/widgets/context_menu.py b/src/sakia/gui/widgets/context_menu.py index 8cf30569..6da8e57b 100644 --- a/src/sakia/gui/widgets/context_menu.py +++ b/src/sakia/gui/widgets/context_menu.py @@ -5,7 +5,6 @@ from PyQt5.QtWidgets import QMenu, QAction, QApplication, QMessageBox from duniterpy.documents import Block from sakia.data.entities import Identity -from sakia.core.transfer import Transfer, TransferState from sakia.decorators import asyncify from sakia.gui.contact import ConfigureContactDialog from sakia.gui.dialogs.certification.controller import CertificationController diff --git a/src/sakia/options.py b/src/sakia/options.py index 35ed0cb1..b85c7f72 100644 --- a/src/sakia/options.py +++ b/src/sakia/options.py @@ -62,7 +62,7 @@ class SakiaOptions: logging.getLogger('quamash').setLevel(logging.INFO) file_handler = RotatingFileHandler(path.join(self.config_path, 'sakia.log'), 'a', 1000000, 10) file_handler.setFormatter(formatter) - self._logger.addHandler(file_handler) stream_handler = StreamHandler() stream_handler.setFormatter(formatter) - self._logger.addHandler(stream_handler) + self._logger.handlers = [file_handler, stream_handler] + self._logger.propagate = False diff --git a/src/sakia/services/__init__.py b/src/sakia/services/__init__.py index d98cc6ea..62b82310 100644 --- a/src/sakia/services/__init__.py +++ b/src/sakia/services/__init__.py @@ -3,3 +3,4 @@ from .identities import IdentitiesService from .blockchain import BlockchainService from .documents import DocumentsService from .sources import SourcesServices +from .transactions import TransactionsService diff --git a/src/sakia/services/blockchain.py b/src/sakia/services/blockchain.py index dec471ab..28c01041 100644 --- a/src/sakia/services/blockchain.py +++ b/src/sakia/services/blockchain.py @@ -9,7 +9,7 @@ class BlockchainService(QObject): Blockchain service is managing new blocks received to update data locally """ - def __init__(self, currency, blockchain_processor, bma_connector, identities_service): + def __init__(self, currency, blockchain_processor, bma_connector, identities_service, transactions_service): """ Constructor the identities service @@ -17,12 +17,14 @@ class BlockchainService(QObject): :param sakia.data.processors.BlockchainProcessor blockchain_processor: the blockchain processor for given currency :param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API :param sakia.services.IdentitiesService identities_service: The identities service + :param sakia.services.TransactionsService transactions_service: The transactions service """ super().__init__() self._blockchain_processor = blockchain_processor self._bma_connector = bma_connector self.currency = currency self._identities_service = identities_service + self._transactions_service = transactions_service self._logger = logging.getLogger('sakia') async def handle_blockchain_progress(self): @@ -32,7 +34,8 @@ class BlockchainService(QObject): with_identities = await self._blockchain_processor.new_blocks_with_identities(self.currency) with_money = await self._blockchain_processor.new_blocks_with_money(self.currency) blocks = await self._blockchain_processor.blocks(with_identities + with_money, self.currency) - self._identities_service.handle_new_blocks(blocks) + await self._identities_service.handle_new_blocks(blocks) + await self._transactions_service.handle_new_blocks(blocks) def parameters(self): return self._blockchain_processor.parameters(self.currency) diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py index 2c1533a1..1cdbdad8 100644 --- a/src/sakia/services/documents.py +++ b/src/sakia/services/documents.py @@ -6,7 +6,7 @@ from collections import Counter from duniterpy.key import SigningKey from duniterpy import PROTOCOL_VERSION -from duniterpy.documents import BlockUID, Block, SelfCertification, Certification, Membership, Revocation +from duniterpy.documents import BlockUID, Block, Identity, Certification, Membership, Revocation from duniterpy.api import bma, errors from sakia.data.entities import Node from aiohttp.errors import ClientError, DisconnectedError @@ -41,7 +41,7 @@ class DocumentsService: block_uid = BlockUID.empty() else: raise - selfcert = SelfCertification(PROTOCOL_VERSION, + selfcert = Identity(PROTOCOL_VERSION, currency, self.pubkey, self.name, diff --git a/src/sakia/services/identities.py b/src/sakia/services/identities.py index fde264f0..e6d173b5 100644 --- a/src/sakia/services/identities.py +++ b/src/sakia/services/identities.py @@ -104,7 +104,7 @@ class IdentitiesService(QObject): for certified_data in data['certifications']: cert = Certification(self.currency, data["pubkey"], certified_data["pubkey"], certified_data["sigDate"]) - cert.block_number = certified_data["cert_time"]["number"] + cert.block = certified_data["cert_time"]["number"] cert.timestamp = certified_data["cert_time"]["medianTime"] if certified_data['written']: cert.written_on = BlockUID(certified_data['written']['number'], @@ -144,9 +144,10 @@ class IdentitiesService(QObject): """ need_refresh = [] for ms in block.joiners + block.actives: - written = self._identities_processor.get_written(self.currency, ms.issuer) + written_list = self._identities_processor.get_written(self.currency, ms.issuer) # we update every written identities known locally - if written: + if written_list: + written = written_list[0] written.membership_written_on = block.blockUID written.membership_type = "IN" written.membership_buid = ms.membership_ts @@ -157,9 +158,10 @@ class IdentitiesService(QObject): need_refresh.append(written) for ms in block.leavers: - written = self._identities_processor.get_written(self.currency, ms.issuer) + written_list = self._identities_processor.get_written(self.currency, ms.issuer) # we update every written identities known locally - if written: + if written_list: + written = written_list[0] written.membership_written_on = block.blockUID written.membership_type = "OUT" written.membership_buid = ms.membership_ts @@ -182,15 +184,15 @@ class IdentitiesService(QObject): """ need_refresh = set([]) for cert in block.certifications: - written = self._identities_processor.get_written(self.currency, cert.pubkey_to) + written_list = self._identities_processor.get_written(self.currency, cert.pubkey_to) # if we have locally a written identity matching the certification - if written or self._identities_processor.get_written(self.currency, cert.pubkey_from): + if written_list or self._identities_processor.get_written(self.currency, cert.pubkey_from): self._certs_processor.create_certification(self.currency, cert, block.blockUID) # we update every written identities known locally - if written: + if written_list: # A certification can change the requirements state # of an identity - need_refresh.add(written) + need_refresh += written_list return need_refresh async def refresh_requirements(self, identity): @@ -246,10 +248,13 @@ class IdentitiesService(QObject): :rtype: dict """ - requirements_data = await self._bma_connector.get(currency, bma.wot.Requirements, req_args={'search': pubkey}) - for req in requirements_data['identities']: - if req['uid'] == uid: - return req + try: + requirements_data = await self._bma_connector.get(currency, bma.wot.requirements, req_args={'search': pubkey}) + for req in requirements_data['identities']: + if req['uid'] == uid: + return req + except NoPeerAvailable as e: + self._logger.debug(str(e)) async def lookup(self, text): """ diff --git a/src/sakia/services/network.py b/src/sakia/services/network.py index 9ceb864f..9725b3c3 100644 --- a/src/sakia/services/network.py +++ b/src/sakia/services/network.py @@ -257,10 +257,12 @@ class NetworkService(QObject): # If new latest block is lower than the previously found one # or if the previously found block is different locally # than in the main chain, we declare a rollback - if current_buid <= self._block_found \ + if current_buid < self._block_found \ or node_connector.node.previous_buid != self._block_found: + self._logger.debug("Start rollback") self._block_found = current_buid #TODO: self._blockchain_service.rollback() else: + self._logger.debug("Start refresh") self._block_found = current_buid asyncio.ensure_future(self._blockchain_service.handle_blockchain_progress()) diff --git a/src/sakia/services/transactions.py b/src/sakia/services/transactions.py new file mode 100644 index 00000000..76c76e96 --- /dev/null +++ b/src/sakia/services/transactions.py @@ -0,0 +1,125 @@ +from PyQt5.QtCore import QObject +from duniterpy.api import bma +from duniterpy.grammars.output import Condition +from duniterpy.documents.transaction import reduce_base +from duniterpy.documents import Transaction, SimpleTransaction +from duniterpy.api import errors +from sakia.data.entities import Transaction +import math +import logging +import hashlib + + +class TransactionsService(QObject): + """ + Transaction service is managing sources received + to update data locally + """ + def __init__(self, currency, transactions_processor, identities_processor, bma_connector): + """ + Constructor the identities service + + :param str currency: The currency name of the community + :param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor for given currency + :param sakia.data.processors.TransactionProcessor transactions_processor: the transactions processor for given currency + :param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API + """ + super().__init__() + self._transactions_processor = transactions_processor + self._identities_processor = identities_processor + self._bma_connector = bma_connector + self.currency = currency + self._logger = logging.getLogger('sakia') + + async def _parse_transaction(self, tx_doc, blockUID, mediantime, txid): + """ + Parse a transaction + :param sakia.core.Community community: The community + :param duniterpy.documents.Transaction tx_doc: The tx json data + :param duniterpy.documents.BlockUID blockUID: The block id where we found the tx + :param int mediantime: Median time on the network + :param int txid: The latest txid + :return: the found transaction + """ + receivers = [o.conditions.left.pubkey for o in tx_doc.outputs + if o.conditions.left.pubkey != tx_doc.issuers[0]] + + if len(receivers) == 0: + receivers = [tx_doc.issuers[0]] + + in_issuers = len([i for i in tx_doc.issuers + if i == self.wallet.pubkey]) > 0 + in_outputs = len([o for o in tx_doc.outputs + if o.conditions.left.pubkey == self.wallet.pubkey]) > 0 + + tx_hash = hashlib.sha256(tx_doc.signed_raw().encode("ascii")).hexdigest().upper() + if in_issuers or in_outputs: + # If the wallet pubkey is in the issuers we sent this transaction + if in_issuers: + outputs = [o for o in tx_doc.outputs + if o.conditions.left.pubkey != self.wallet.pubkey] + amount = 0 + for o in outputs: + amount += o.amount * math.pow(10, o.base) + # If we are not in the issuers, + # maybe we are in the recipients of this transaction + else: + outputs = [o for o in tx_doc.outputs + if o.conditions.left.pubkey == self.wallet.pubkey] + amount = 0 + for o in outputs: + amount += o.amount * math.pow(10, o.base) + amount, amount_base = reduce_base(amount, 0) + + transaction = Transaction(currency=self.currency, + sha_hash=tx_hash, + written_on=blockUID.number, + blockstamp=tx_doc.blockstamp, + timestamp=mediantime, + signature=tx_doc.signatures[0], + issuer=tx_doc.issuers[0], + receiver=receivers[0], + amount=amount, + amount_base=amount_base, + comment=tx_doc.comment, + txid=txid) + return transaction + return None + + async def _parse_block(self, block_doc, txid): + """ + Parse a block + :param duniterpy.documents.Block block_doc: The block + :param int txid: Latest tx id + :return: The list of transfers sent + """ + transfers = [] + for tx in [t for t in self._transactions_processor.awaiting()]: + self._transactions_processor.run_state_transitions(tx, (False, block_doc)) + + new_transactions = [t for t in block_doc.transactions + if not self._transactions_processor.find_by_hash(t.sha_hash) + and SimpleTransaction.is_simple(t)] + + for (i, tx_doc) in enumerate(new_transactions): + tx = await self._parse_transaction(tx_doc, block_doc.blockUID, + block_doc.mediantime, txid+i) + if tx: + #logging.debug("Transfer amount : {0}".format(transfer.metadata['amount'])) + transfers.append(tx) + else: + pass + #logging.debug("None transfer") + return transfers + + async def handle_new_blocks(self, blocks): + """ + Refresh last transactions + + :param list[duniterpy.documents.Block] blocks: The blocks containing data to parse + """ + self._logger.debug("Refresh transactions") + txid = 0 + for block in blocks: + transfers = await self._parse_block(block, txid) + txid += len(transfers) -- GitLab