diff --git a/src/sakia/app.py b/src/sakia/app.py index c593c049b200fbcc74c28483bdae364052c98522..44894873b6703f4a54bc64c32e2bcc8aa8fdc646 100644 --- a/src/sakia/app.py +++ b/src/sakia/app.py @@ -19,7 +19,7 @@ from . import __version__ from .options import SakiaOptions from sakia.data.connectors import BmaConnector from sakia.services import NetworkService, BlockchainService, IdentitiesService -from sakia.data.repositories import MetaDatabase, BlockchainsRepo, CertificationsRepo, \ +from sakia.data.repositories import SakiaDatabase, BlockchainsRepo, CertificationsRepo, \ IdentitiesRepo, NodesRepo, TransactionsRepo, ConnectionsRepo from sakia.data.processors import BlockchainProcessor, NodesProcessor, IdentitiesProcessor, CertificationsProcessor from sakia.data.files import AppDataFile, UserParametersFile @@ -34,7 +34,7 @@ class Application(QObject): Saving and loading the application state """ - def __init__(self, qapp, loop, options, app_data, parameters, connections_repo, + def __init__(self, qapp, loop, options, app_data, parameters, db, network_services, blockchain_services, identities_services): """ Init a new "sakia" application @@ -43,7 +43,7 @@ class Application(QObject): :param sakia.options.SakiaOptions options: the options :param sakia.data.entities.AppData app_data: the application data :param sakia.data.entities.UserParameters parameters: the application current user parameters - :param sakia.data.repositories.ConnectionsRepo connections_repo: The connections repository + :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 @@ -59,7 +59,7 @@ class Application(QObject): self._translator = QTranslator(self.qapp) self._app_data = app_data self._parameters = parameters - self.connections_repo = connections_repo + self.db = db self.network_services = network_services self.blockchain_services = blockchain_services self.identities_services = identities_services @@ -82,27 +82,23 @@ class Application(QObject): :return: """ self._parameters = UserParametersFile.in_config_path(self.options.config_path, profile_name).load_or_init() - meta = MetaDatabase.load_or_init(self.options.config_path, profile_name) - self.connections_repo = ConnectionsRepo(meta.conn) - certifications_repo = CertificationsRepo(meta.conn) - nodes_repo = NodesRepo(meta.conn) - blockchain_repo = BlockchainsRepo(meta.conn) - identities_repo = IdentitiesRepo(meta.conn) - - nodes_processor = NodesProcessor(nodes_repo) + self.db = SakiaDatabase.load_or_init(self.options.config_path, profile_name) + + nodes_processor = NodesProcessor(self.db.nodes_repo) bma_connector = BmaConnector(nodes_processor) - identities_processor = IdentitiesProcessor(identities_repo, bma_connector) - certs_processor = CertificationsProcessor(certifications_repo, bma_connector) - blockchain_processor = BlockchainProcessor(blockchain_repo, bma_connector) + identities_processor = IdentitiesProcessor(self.db.identities_repo, self.db.blockchains_repo, bma_connector) + certs_processor = CertificationsProcessor(self.db.certifications_repo, bma_connector) + blockchain_processor = BlockchainProcessor(self.db.blockchains_repo, bma_connector) self.blockchain_services = {} self.network_services = {} self.identities_services = {} - for currency in self.connections_repo.get_currencies(): - self.identities_services[currency] = IdentitiesService(currency, identities_processor, certs_processor, - blockchain_processor) + for currency in self.db.connections_repo.get_currencies(): + self.identities_services[currency] = IdentitiesService(currency, identities_processor, + certs_processor, blockchain_processor, + bma_connector) self.blockchain_services[currency] = BlockchainService(currency, blockchain_processor, bma_connector, - self.identities_service[currency]) + self.identities_services[currency]) self.network_services[currency] = NetworkService.load(currency, nodes_processor, self.blockchain_services[currency]) diff --git a/src/sakia/data/connectors/bma.py b/src/sakia/data/connectors/bma.py index b938903937e416ab23b37d5e67c395b697965fa4..9ec0dcda42d37aa96c0405b5f5390b77bace40a4 100644 --- a/src/sakia/data/connectors/bma.py +++ b/src/sakia/data/connectors/bma.py @@ -1,5 +1,6 @@ from duniterpy.api import bma import logging +import aiohttp from aiohttp.errors import ClientError, ServerDisconnectedError import asyncio import random @@ -8,6 +9,7 @@ import jsonschema from pkg_resources import parse_version import attr from sakia.errors import NoPeerAvailable +from sakia.data.processors import NodesProcessor @attr.s() @@ -15,9 +17,10 @@ class BmaConnector: """ This class is used to access BMA API. """ - _nodes_processor = attr.ib() + _nodes_processor = attr.ib(validator=attr.validators.instance_of(NodesProcessor)) + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) - def filter_nodes(self, request, nodes): + def filter_endpoints(self, request, nodes): def compare_versions(node, version): if node.version and node.version != '': try: @@ -32,9 +35,11 @@ class BmaConnector: bma.blockchain.Membership: lambda n: compare_versions(n, "0.14") } if request in filters: - return [n for n in nodes if filters[request](n)] - else: - return nodes + nodes = [n for n in nodes if filters[request](n)] + endpoints = [] + for n in nodes: + endpoints += n.endpoints + return endpoints async def get(self, currency, request, req_args={}, get_args={}): """ @@ -46,22 +51,22 @@ class BmaConnector: :param dict get_args: Arguments to pass to the request __get__ method :return: The returned data """ - nodes = self.filter_nodes(request, self._nodes_processor.synced_nodes(currency)) - if len(nodes) > 0: + endpoints = self.filter_endpoints(request, self._nodes_processor.synced_nodes(currency)) + if len(endpoints) > 0: tries = 0 while tries < 3: - node = random.choice(nodes) - nodes.pop(node) - req = request(node.endpoint.conn_handler(), **req_args) + endpoint = random.choice(endpoints) + req = request(endpoint.conn_handler(), **req_args) try: - json_data = await req.get(**get_args, session=self._network.session) - return json_data + self._logger.debug("Requesting {0} on endpoint {1}".format(str(req), str(endpoint))) + with aiohttp.ClientSession() as session: + json_data = await req.get(**get_args, session=session) + return json_data except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError, jsonschema.ValidationError) as e: - logging.debug(str(e)) + self._logger.debug(str(e)) tries += 1 - if len(nodes) == 0: - raise NoPeerAvailable("", len(nodes)) + raise NoPeerAvailable("", len(endpoint)) async def broadcast(self, currency, request, req_args={}, post_args={}): """ @@ -78,23 +83,24 @@ class BmaConnector: .. note:: If one node accept the requests (returns 200), the broadcast should be considered accepted by the network. """ - nodes = random.sample(self._nodes_processor.synced_nodes(currency), 6) \ - if len(self._nodes_processor.synced_nodes(currency)) > 6 \ - else self._nodes_processor.synced_nodes(currency) + filtered_endpoints = self.filter_endpoints(request, self._nodes_processor.synced_nodes(currency)) + endpoints = random.sample(filtered_endpoints, 6) if len(filtered_endpoints) > 6 else filtered_endpoints replies = [] - if len(nodes) > 0: - for node in nodes: - logging.debug("Trying to connect to : " + node.pubkey) - conn_handler = node.endpoint.conn_handler() - req = request(conn_handler, **req_args) - reply = asyncio.ensure_future(req.post(**post_args, session=self._network.session)) - replies.append(reply) - else: - raise NoPeerAvailable("", len(nodes)) - try: - result = await asyncio.gather(*replies) - return tuple(result) - except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError) as e: - logging.debug(str(e)) - return () + if len(endpoints) > 0: + with aiohttp.ClientSession() as session: + for endpoint in endpoints: + self._logger.debug("Trying to connect to : " + str(endpoint)) + conn_handler = endpoint.conn_handler() + req = request(conn_handler, **req_args) + reply = asyncio.ensure_future(req.post(**post_args, session=session)) + replies.append(reply) + + try: + result = await asyncio.gather(*replies) + return tuple(result) + except (ClientError, ServerDisconnectedError, gaierror, asyncio.TimeoutError, ValueError) as e: + self._logger.debug(str(e)) + return () + else: + raise NoPeerAvailable("", len(endpoints)) diff --git a/src/sakia/data/connectors/node.py b/src/sakia/data/connectors/node.py index 3f9d8a7aa4d046c9fd2f781c90bc6c39ba1f3781..53b648f43fbe95e3316744b18dca9a9bf18ea29d 100644 --- a/src/sakia/data/connectors/node.py +++ b/src/sakia/data/connectors/node.py @@ -38,6 +38,7 @@ class NodeConnector(QObject): 'peer': False} self._session = session self._refresh_counter = 1 + self._logger = logging.getLogger('sakia') def __del__(self): for ws in self._ws_tasks.values(): @@ -69,7 +70,7 @@ class NodeConnector(QObject): raise InvalidNodeCurrency(peer.currency, currency) node = Node(peer.currency, peer.pubkey, peer.endpoints, peer.blockUID) - logging.debug("Node from address : {:}".format(str(node))) + logging.getLogger('sakia').debug("Node from address : {:}".format(str(node))) return cls(node, session) @@ -87,21 +88,21 @@ class NodeConnector(QObject): raise InvalidNodeCurrency(peer.currency, currency) node = Node(peer.currency, peer.pubkey, peer.endpoints, peer.blockUID) - logging.debug("Node from peer : {:}".format(str(node))) + logging.getLogger('sakia').debug("Node from peer : {:}".format(str(node))) return cls(node, session) - async def _safe_request(self, endpoint, request, req_args={}, get_args={}): + async def safe_request(self, endpoint, request, req_args={}, get_args={}): try: conn_handler = endpoint.conn_handler() data = await request(conn_handler, **req_args).get(self._session, **get_args) return data except (ClientError, gaierror, TimeoutError, ConnectionRefusedError, DisconnectedError, ValueError) as e: - logging.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) self.node.state = Node.OFFLINE except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) self.node.state = Node.CORRUPTED async def close_ws(self): @@ -154,11 +155,11 @@ class NodeConnector(QObject): ws_connection = block_websocket.connect(self._session) async with ws_connection as ws: self._connected['block'] = True - logging.debug("Connected successfully to block ws : {0}" + self._logger.debug("Connected successfully to block ws : {0}" .format(self.node.pubkey[:5])) async for msg in ws: if msg.tp == aiohttp.MsgType.text: - logging.debug("Received a block : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Received a block : {0}".format(self.node.pubkey[:5])) block_data = block_websocket.parse_text(msg.data) await self.refresh_block(block_data) elif msg.tp == aiohttp.MsgType.closed: @@ -167,15 +168,15 @@ class NodeConnector(QObject): break except (WSServerHandshakeError, WSClientDisconnectedError, ClientResponseError, ValueError) as e: - logging.debug("Websocket block {0} : {1} - {2}" + self._logger.debug("Websocket block {0} : {1} - {2}" .format(type(e).__name__, str(e), self.node.pubkey[:5])) await self.request_current_block() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) self.node.state = Node.OFFLINE except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) self.node.state = Node.CORRUPTED finally: self.changed.emit() @@ -189,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, self._session) await self.refresh_block(block_data) return # Do not try any more endpoint except errors.DuniterError as e: @@ -197,11 +198,11 @@ class NodeConnector(QObject): self.node.previous_buid = BlockUID.empty() else: self.node.state = Node.OFFLINE - logging.debug("Error in block reply of {0} : {1}}".format(self.node.pubkey[:5], str(e))) + self._logger.debug("Error in block reply of {0} : {1}}".format(self.node.pubkey[:5], str(e))) finally: self.changed.emit() else: - logging.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.changed.emit() @@ -214,10 +215,10 @@ class NodeConnector(QObject): if not self.node.current_buid or self.node.current_buid.sha_hash != block_data['hash']: for endpoint in [e for e in self.node.endpoints if isinstance(e, BMAEndpoint)]: conn_handler = endpoint.conn_handler() - logging.debug("Requesting {0}".format(conn_handler)) + self._logger.debug("Requesting {0}".format(conn_handler)) try: - previous_block = await self._safe_request(endpoint, bma.blockchain.Block, - req_args={'number': self.node.current_buid.number}) + previous_block = await self.safe_request(endpoint, bma.blockchain.Block, + req_args={'number': self.node.current_buid.number}) self.node.previous_buid = BlockUID(previous_block['number'], previous_block['hash']) return # Do not try any more endpoint except errors.DuniterError as e: @@ -226,14 +227,14 @@ class NodeConnector(QObject): self.node.current_buid = BlockUID.empty() else: self.node.state = Node.OFFLINE - logging.debug("Error in previous block reply of {0} : {1}".format(self.node.pubkey[:5], str(e))) + self._logger.debug("Error in previous block reply of {0} : {1}".format(self.node.pubkey[:5], str(e))) finally: self.node.current_buid = BlockUID(block_data['number'], block_data['hash']) - logging.debug("Changed block {0} -> {1}".format(self.node.current_buid.number, + self._logger.debug("Changed block {0} -> {1}".format(self.node.current_buid.number, block_data['number'])) self.changed.emit() else: - logging.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.changed.emit() @@ -244,18 +245,18 @@ 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) self.node.software = summary_data["duniter"]["software"] self.node.version = summary_data["duniter"]["version"] self.node.state = Node.ONLINE return # Break endpoints loop except errors.DuniterError as e: self.node.state = Node.OFFLINE - logging.debug("Error in summary of {0} : {1}".format(self.node.pubkey[:5], str(e))) + self._logger.debug("Error in summary of {0} : {1}".format(self.node.pubkey[:5], str(e))) finally: self.changed.emit() else: - logging.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.changed.emit() @@ -266,9 +267,9 @@ 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}, + get_args={}) self.node.state = Node.ONLINE timestamp = BlockUID.empty() uid = "" @@ -284,13 +285,13 @@ class NodeConnector(QObject): self.identity_changed.emit() except errors.DuniterError as e: if e.ucode == errors.NO_MATCHING_IDENTITY: - logging.debug("UID not found : {0}".format(self.node.pubkey[:5])) + self._logger.debug("UID not found : {0}".format(self.node.pubkey[:5])) else: - logging.debug("error in uid reply : {0}".format(self.node.pubkey[:5])) + self._logger.debug("error in uid reply : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.identity_changed.emit() else: - logging.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.changed.emit() @@ -307,10 +308,10 @@ class NodeConnector(QObject): ws_connection = peer_websocket.connect(self._session) async with ws_connection as ws: self._connected['peer'] = True - logging.debug("Connected successfully to peer ws : {0}".format(self.node.pubkey[:5])) + 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: - logging.debug("Received a peer : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Received a peer : {0}".format(self.node.pubkey[:5])) peer_data = peer_websocket.parse_text(msg.data) self.refresh_peer_data(peer_data) elif msg.tp == aiohttp.MsgType.closed: @@ -319,22 +320,22 @@ class NodeConnector(QObject): break except (WSServerHandshakeError, WSClientDisconnectedError, ClientResponseError, ValueError) as e: - logging.debug("Websocket peer {0} : {1} - {2}" + self._logger.debug("Websocket peer {0} : {1} - {2}" .format(type(e).__name__, str(e), self.node.pubkey[:5])) await self.request_peers() except (ClientError, gaierror, TimeoutError, DisconnectedError) as e: - logging.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) self.node.state = Node.OFFLINE except jsonschema.ValidationError as e: - logging.debug(str(e)) - logging.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) self.node.state = Node.CORRUPTED finally: self._connected['peer'] = False self._ws_tasks['peer'] = None self.changed.emit() else: - logging.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.changed.emit() @@ -344,20 +345,20 @@ 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.peering.Peers, + get_args={'leaves': 'true'}) self.node.state = Node.ONLINE if peers_data['root'] != self.node.merkle_peers_root: leaves = [leaf for leaf in peers_data['leaves'] if leaf not in self.node.merkle_peers_leaves] for leaf_hash in leaves: try: - leaf_data = await self._safe_request(endpoint, - bma.network.peering.Peers, - get_args={'leaf': leaf_hash}) + leaf_data = await self.safe_request(endpoint, + bma.network.peering.Peers, + get_args={'leaf': leaf_hash}) self.refresh_peer_data(leaf_data['leaf']['value']) except (AttributeError, ValueError, errors.DuniterError) as e: - logging.debug("{pubkey} : Incorrect peer data in {leaf}" + self._logger.debug("{pubkey} : Incorrect peer data in {leaf}" .format(pubkey=self.node.pubkey[:5], leaf=leaf_hash)) self.node.state = Node.OFFLINE @@ -367,12 +368,12 @@ class NodeConnector(QObject): self.node.merkle_peers_leaves = tuple(peers_data['leaves']) return # Break endpoints loop except errors.DuniterError as e: - logging.debug("Error in peers reply : {0}".format(str(e))) + self._logger.debug("Error in peers reply : {0}".format(str(e))) self.node.state = Node.OFFLINE finally: self.changed.emit() else: - logging.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) + self._logger.debug("Could not connect to any BMA endpoint : {0}".format(self.node.pubkey[:5])) self.node.state = Node.OFFLINE self.changed.emit() @@ -384,6 +385,6 @@ class NodeConnector(QObject): peer_doc = Peer.from_signed_raw(str_doc) self.neighbour_found.emit(peer_doc) except MalformedDocumentError as e: - logging.debug(str(e)) + self._logger.debug(str(e)) else: - logging.debug("Incorrect leaf reply") + self._logger.debug("Incorrect leaf reply") diff --git a/src/sakia/data/entities/connection.py b/src/sakia/data/entities/connection.py index b6042dcb0199f95cce6787debce6854c8fa574f5..a118ba3caada2526d3e04df7541dd93cc1792697 100644 --- a/src/sakia/data/entities/connection.py +++ b/src/sakia/data/entities/connection.py @@ -1,4 +1,5 @@ import attr +from duniterpy.documents import block_uid, BlockUID @attr.s() @@ -6,8 +7,12 @@ class Connection: """ A connection represents a connection to a currency's network It is defined by the currency name, and the key informations - used to connect to it + used to connect to it. If the user is using an identity, it is defined here too. """ currency = attr.ib(convert=str) pubkey = attr.ib(convert=str) salt = attr.ib(convert=str) + uid = attr.ib(convert=str, default="", cmp=False, hash=False) + blockstamp = attr.ib(convert=block_uid, default=BlockUID.empty(), cmp=False, hash=False) + password = attr.ib(convert=str, default="", cmp=False, hash=False) + diff --git a/src/sakia/data/processors/__init__.py b/src/sakia/data/processors/__init__.py index 5bcb04aeeac449c1202327a9e12f3da76d3ff59c..3e7c81c2bb4ed75419af7f79d35f030d0750e7e9 100644 --- a/src/sakia/data/processors/__init__.py +++ b/src/sakia/data/processors/__init__.py @@ -2,3 +2,4 @@ from .nodes import NodesProcessor from .identities import IdentitiesProcessor from .certifications import CertificationsProcessor from .blockchain import BlockchainProcessor +from .connections import ConnectionsProcessor diff --git a/src/sakia/data/processors/blockchain.py b/src/sakia/data/processors/blockchain.py index 8b4a93c2b17edfbf64aa43f56521d340ee7d431a..253667d219d23989ac3d0fdf075dafffdc19eb93 100644 --- a/src/sakia/data/processors/blockchain.py +++ b/src/sakia/data/processors/blockchain.py @@ -1,8 +1,8 @@ import attr import re from ..entities import Blockchain -from duniterpy.api import bma -from duniterpy.documents import Block +from duniterpy.api import bma, errors +from duniterpy.documents import Block, BMAEndpoint import asyncio @@ -112,3 +112,55 @@ class BlockchainProcessor: return blocks + async def initialize_blockchain(self, currency, log_stream): + """ + Start blockchain service if it does not exists + """ + blockchain = self._repo.get_one(currency=currency) + if not blockchain: + blockchain = Blockchain(currency=currency) + log_stream("Requesting current block") + try: + current_block = await self._bma_connector.get(currency, bma.blockchain.Current) + signed_raw = "{0}{1}\n".format(current_block['raw'], current_block['signature']) + block = Block.from_signed_raw(signed_raw) + blockchain.current_buid = block.blockUID + blockchain.median_time = block.mediantime + except errors.DuniterError as e: + if e.ucode != errors.NO_CURRENT_BLOCK: + raise + + log_stream("Requesting blocks with dividend") + with_ud = await self._bma_connector.get(currency, bma.blockchain.UD) + blocks_with_ud = with_ud['result']['blocks'] + + if len(blocks_with_ud) > 0: + log_stream("Requesting last block with dividend") + try: + index = max(len(blocks_with_ud) - 1, 0) + block_number = blocks_with_ud[index] + block_with_ud = await self._bma_connector.get(currency, bma.blockchain.Block, + req_args={'number': block_number}) + if block_with_ud: + blockchain.last_ud = block_with_ud['dividend'] + blockchain.last_ud_base = block_with_ud['unitbase'] + blockchain.last_ud_time = block_with_ud['medianTime'] + blockchain.current_mass = block_with_ud['monetaryMass'] + blockchain.nb_members = block_with_ud['membersCount'] + except errors.DuniterError as e: + if e.ucode != errors.NO_CURRENT_BLOCK: + raise + + log_stream("Requesting previous block with dividend") + try: + index = max(len(blocks_with_ud) - 2, 0) + block_number = blocks_with_ud[index] + block_with_ud = await self._bma_connector.get(currency, bma.blockchain.Block, + req_args={'number': block_number}) + blockchain.previous_mass = block_with_ud['monetaryMass'] + except errors.DuniterError as e: + if e.ucode != errors.NO_CURRENT_BLOCK: + raise + + self._repo.insert(blockchain) + diff --git a/src/sakia/data/processors/connections.py b/src/sakia/data/processors/connections.py new file mode 100644 index 0000000000000000000000000000000000000000..e21a6bca928f0e6f7b7bf23dc5eaa9b58a484696 --- /dev/null +++ b/src/sakia/data/processors/connections.py @@ -0,0 +1,19 @@ +import attr +import sqlite3 +import logging + + +@attr.s +class ConnectionsProcessor: + _connections_repo = attr.ib() # :type sakia.data.repositories.ConnectionsRepo + _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) + + def commit_connection(self, connection): + """ + Saves a connection state in the db + :param sakia.data.entities.Connection connection: the connection updated + """ + try: + self._connections_repo.insert(connection) + except sqlite3.IntegrityError: + self._connections_repo.update(connection) diff --git a/src/sakia/data/processors/identities.py b/src/sakia/data/processors/identities.py index 577904ca5f33547f58750af3de5afc3039b93e60..683d1678f9b041650b69433325ba9cd5c3f40339 100644 --- a/src/sakia/data/processors/identities.py +++ b/src/sakia/data/processors/identities.py @@ -4,14 +4,17 @@ import logging import asyncio from ..entities import Identity from duniterpy.api import bma, errors +from duniterpy import PROTOCOL_VERSION +from duniterpy.key import SigningKey +from duniterpy.documents import SelfCertification from aiohttp.errors import ClientError from sakia.errors import NoPeerAvailable -from duniterpy.documents import BlockUID @attr.s class IdentitiesProcessor: _identities_repo = attr.ib() # :type sakia.data.repositories.IdentitiesRepo + _blockchain_repo = attr.ib() # :type sakia.data.repositories.BlockchainRepo _bma_connector = attr.ib() # :type sakia.data.connectors.bma.BmaConnector _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) @@ -95,88 +98,42 @@ class IdentitiesProcessor: except sqlite3.IntegrityError: self._identities_repo.update(identity) - async def check_registered(self, currency, identity): + async def publish_selfcert(self, currency, identity, salt, password): """ - Checks for the pubkey and the uid of an account in a community - :param str currency: The currency we check for registration - :param sakia.data.entities.Identity identity: the identity we check for registration - :return: (True if found, local value, network value) + Send our self certification to a target community + :param sakia.data.entities.Identity identity: The identity broadcasted + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + :param str currency: The currency target of the self certification """ - - def _parse_uid_certifiers(data): - return identity.uid == data['uid'], identity.uid, data['uid'] - - def _parse_uid_lookup(data): - timestamp = BlockUID.empty() - found_uid = "" - for result in data['results']: - if result["pubkey"] == identity.pubkey: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] - found_uid = uid_data["uid"] - return identity.uid == found_uid, identity.uid, found_uid - - def _parse_pubkey_certifiers(data): - return identity.pubkey == data['pubkey'], identity.pubkey, data['pubkey'] - - def _parse_pubkey_lookup(data): - timestamp = BlockUID.empty() - found_uid = "" - found_result = ["", ""] - for result in data['results']: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - found_uid = uid_data["uid"] - if found_uid == identity.uid: - found_result = result['pubkey'], found_uid - if found_result[1] == identity.uid: - return identity.pubkey == found_result[0], identity.pubkey, found_result[0] + blockchain = self._blockchain_repo.get_one(currency=currency) + block_uid = blockchain.current_buid + timestamp = blockchain.median_time + selfcert = SelfCertification(2, + currency, + identity.pubkey, + identity.uid, + block_uid, + None) + key = SigningKey(salt, password) + selfcert.sign([key]) + self._logger.debug("Key publish : {0}".format(selfcert.signed_raw())) + + responses = await self._bma_connector.broadcast(currency, bma.wot.Add, {}, {'identity': selfcert.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: - return False, identity.pubkey, None - - async def execute_requests(parsers, search): - tries = 0 - request = bma.wot.CertifiersOf - nonlocal registered - # TODO: The algorithm is quite dirty - # Multiplying the tries without any reason... - while tries < 3 and not registered[0] and not registered[2]: - try: - data = await self._bma_connector.get(currency, request, req_args={'search': search}) - if data: - registered = parsers[request](data) - tries += 1 - except errors.DuniterError as e: - if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, - e.ucode == errors.NO_MATCHING_IDENTITY): - if request == bma.wot.CertifiersOf: - request = bma.wot.Lookup - tries = 0 - else: - tries += 1 - else: - tries += 1 + await r.release() - registered = (False, identity.uid, None) - # We execute search based on pubkey - # And look for account UID - uid_parsers = { - bma.wot.CertifiersOf: _parse_uid_certifiers, - bma.wot.Lookup: _parse_uid_lookup - } - await execute_requests(uid_parsers, identity.pubkey) + if result[0]: + identity.blockstamp = block_uid + identity.signature = selfcert.signatures[0] + identity.timestamp = timestamp - # If the uid wasn't found when looking for the pubkey - # We look for the uid and check for the pubkey - if not registered[0] and not registered[2]: - pubkey_parsers = { - bma.wot.CertifiersOf: _parse_pubkey_certifiers, - bma.wot.Lookup: _parse_pubkey_lookup - } - await execute_requests(pubkey_parsers, identity.uid) + self.commit_identity(identity) - return registered + return result \ No newline at end of file diff --git a/src/sakia/data/processors/nodes.py b/src/sakia/data/processors/nodes.py index de72c74c2fbc72498dd6d5107259431467a047ef..063b7824178f9933693ce82d18866d7d72b0436d 100644 --- a/src/sakia/data/processors/nodes.py +++ b/src/sakia/data/processors/nodes.py @@ -1,4 +1,5 @@ import attr +import sqlite3 from ..entities import Node from duniterpy.documents import BlockUID, endpoint import logging @@ -43,6 +44,16 @@ class NodesProcessor: """ self._repo.insert(node) + def commit_node(self, node): + """ + Saves a node state in the db + :param sakia.data.entities.Node node: the node updated + """ + try: + self._repo.insert(node) + except sqlite3.IntegrityError: + self._repo.update(node) + def unknown_node(self, currency, pubkey): """ Search for pubkey in the repository. diff --git a/src/sakia/data/repositories/__init__.py b/src/sakia/data/repositories/__init__.py index cf14e2a690151724d4e90337c9835dea2b2af99d..19a9a16683bb98b100fea58c428dc827acac72e1 100644 --- a/src/sakia/data/repositories/__init__.py +++ b/src/sakia/data/repositories/__init__.py @@ -1,6 +1,6 @@ from .identities import IdentitiesRepo from .blockchains import BlockchainsRepo -from .meta import MetaDatabase +from .meta import SakiaDatabase from .certifications import CertificationsRepo from .transactions import TransactionsRepo from .nodes import NodesRepo diff --git a/src/sakia/data/repositories/blockchains.py b/src/sakia/data/repositories/blockchains.py index 055deab24b566301fa92073e7b8fc9b0d1ac13f8..e9c56195c0ff56addfedafbfb7f54c637841d3df 100644 --- a/src/sakia/data/repositories/blockchains.py +++ b/src/sakia/data/repositories/blockchains.py @@ -39,6 +39,7 @@ class BlockchainsRepo: median_time=?, last_ud=?, last_ud_base=?, + last_ud_time=?, previous_mass=? WHERE currency=?""", diff --git a/src/sakia/data/repositories/connections.py b/src/sakia/data/repositories/connections.py index 7d19fe6e9736042f357e99b1189278b1db5ee01b..4c3cd757484804d34e580c5d4e86df0392e37cbc 100644 --- a/src/sakia/data/repositories/connections.py +++ b/src/sakia/data/repositories/connections.py @@ -17,7 +17,7 @@ class ConnectionsRepo: :param sakia.data.entities.Connection connection: the connection to commit """ with self._conn: - connection_tuple = attr.astuple(connection) + connection_tuple = attr.astuple(connection, filter=attr.filters.exclude(Connection.password)) values = ",".join(['?'] * len(connection_tuple)) self._conn.execute("INSERT INTO connections VALUES ({0})".format(values), connection_tuple) @@ -76,10 +76,9 @@ class ConnectionsRepo: c = self._conn.execute(request) datas = c.fetchall() if datas: - return [Connection(*data) for data in datas] + return [data[0] for data in datas] return [] - def drop(self, connection): """ Drop an existing connection from the database diff --git a/src/sakia/data/repositories/meta.py b/src/sakia/data/repositories/meta.py index 1680cafa0bbd188fa6412489ae27f687765a5d85..4a6361b303629132b58c605cfd8f502d497e1113 100644 --- a/src/sakia/data/repositories/meta.py +++ b/src/sakia/data/repositories/meta.py @@ -3,28 +3,38 @@ import os import logging import sqlite3 from duniterpy.documents import BlockUID +from .connections import ConnectionsRepo +from .identities import IdentitiesRepo +from .blockchains import BlockchainsRepo +from .certifications import CertificationsRepo +from .transactions import TransactionsRepo +from .nodes import NodesRepo @attr.s(frozen=True) -class MetaDatabase: +class SakiaDatabase: """The repository for Identities entities. """ - _conn = attr.ib() # :type sqlite3.Connection + conn = attr.ib() # :type sqlite3.Connection + connections_repo = attr.ib() + identities_repo = attr.ib() + blockchains_repo = attr.ib() + certifications_repo = attr.ib() + transactions_repo = attr.ib() + nodes_repo = attr.ib() _logger = attr.ib(default=attr.Factory(lambda: logging.getLogger('sakia'))) db_file = 'sakia.db' - @property - def conn(self): - return self._conn - @classmethod def load_or_init(cls, config_path, profile_name): sqlite3.register_adapter(BlockUID, str) sqlite3.register_adapter(bool, int) sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) - con = sqlite3.connect(os.path.join(config_path, profile_name, MetaDatabase.db_file), + con = sqlite3.connect(os.path.join(config_path, profile_name, SakiaDatabase.db_file), detect_types=sqlite3.PARSE_DECLTYPES) - meta = MetaDatabase(con) + meta = SakiaDatabase(con, ConnectionsRepo(con), IdentitiesRepo(con), + BlockchainsRepo(con), CertificationsRepo(con), TransactionsRepo(con), + NodesRepo(con)) meta.prepare() meta.upgrade_database() return meta @@ -33,14 +43,14 @@ class MetaDatabase: """ Prepares the database if the table is missing """ - with self._conn: + with self.conn: self._logger.debug("Initializing meta database") - self._conn.execute("""CREATE TABLE IF NOT EXISTS meta( + self.conn.execute("""CREATE TABLE IF NOT EXISTS meta( id INTEGER NOT NULL, version INTEGER NOT NULL, PRIMARY KEY (id) )""" - ) + ) @property def upgrades(self): @@ -58,8 +68,8 @@ class MetaDatabase: for v in range(version, nb_versions): self._logger.debug("Upgrading to version {0}...".format(v)) self.upgrades[v]() - with self._conn: - self._conn.execute("UPDATE meta SET version=? WHERE id=1", (version + 1,)) + with self.conn: + self.conn.execute("UPDATE meta SET version=? WHERE id=1", (version + 1,)) self._logger.debug("End upgrade of database...") def create_all_tables(self): @@ -69,15 +79,15 @@ class MetaDatabase: """ self._logger.debug("Initialiazing all databases") sql_file = open(os.path.join(os.path.dirname(__file__), 'meta.sql'), 'r') - with self._conn: - self._conn.executescript(sql_file.read()) + with self.conn: + self.conn.executescript(sql_file.read()) def version(self): - with self._conn: - c = self._conn.execute("SELECT * FROM meta WHERE id=1") + with self.conn: + c = self.conn.execute("SELECT * FROM meta WHERE id=1") data = c.fetchone() if data: return data[1] else: - self._conn.execute("INSERT INTO meta VALUES (1, 0)") + self.conn.execute("INSERT INTO meta VALUES (1, 0)") return 0 diff --git a/src/sakia/data/repositories/meta.sql b/src/sakia/data/repositories/meta.sql index 4d1cbb1ee1090a4b5a23fa3f33a08a9ae7ef7656..5cdbe8e94a2afcba979b48bc55337c3e25240ccb 100644 --- a/src/sakia/data/repositories/meta.sql +++ b/src/sakia/data/repositories/meta.sql @@ -34,12 +34,13 @@ CREATE TABLE IF NOT EXISTS blockchains ( dt_diff_eval INT, blocks_rot INT, percent_rot FLOAT(1, 6), - current_buid INT, + current_buid INT, nb_members INT, current_mass INT, median_time INT, last_ud INT, last_ud_base INT, + last_ud_time INT, previous_mass INT, currency VARCHAR(30), PRIMARY KEY (currency) @@ -98,6 +99,8 @@ CREATE TABLE IF NOT EXISTS connections( currency VARCHAR(30), pubkey VARCHAR(50), salt VARCHAR(50), + uid VARCHAR(255), + blockstamp VARCHAR(100), PRIMARY KEY (currency, pubkey) ); diff --git a/src/sakia/gui/dialogs/account_cfg/account_cfg.ui b/src/sakia/gui/dialogs/account_cfg/account_cfg.ui deleted file mode 100644 index a080207367f94a6255743708f3d9d9554e1dc2a6..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/account_cfg/account_cfg.ui +++ /dev/null @@ -1,305 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>AccountConfigurationDialog</class> - <widget class="QDialog" name="AccountConfigurationDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>272</height> - </rect> - </property> - <property name="windowTitle"> - <string>Add an account</string> - </property> - <property name="modal"> - <bool>true</bool> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QStackedWidget" name="stacked_pages"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="page_init"> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="title"> - <string>Account parameters</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <spacer name="verticalSpacer_3"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLabel" name="label_action"> - <property name="text"> - <string>Account name (uid)</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_account_name"/> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_8"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_delete"> - <property name="styleSheet"> - <string notr="true">color: rgb(255, 0, 0);</string> - </property> - <property name="text"> - <string>Delete account</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer_4"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="page_brainwallet"> - <layout class="QVBoxLayout" name="verticalLayout_7"> - <item> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Key parameters</string> - </property> - </widget> - </item> - <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_5"/> - </item> - <item> - <widget class="QLineEdit" name="edit_salt"> - <property name="text"> - <string/> - </property> - <property name="placeholderText"> - <string>Secret key</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_password"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="placeholderText"> - <string>Your password</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="edit_password_repeat"> - <property name="text"> - <string/> - </property> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="placeholderText"> - <string>Please repeat your password</string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_6"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QLabel" name="label_info"> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_generate"> - <property name="text"> - <string>Show public key</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - <widget class="QWidget" name="page_communities"> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>Communities</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <widget class="QListView" name="list_communities"> - <property name="contextMenuPolicy"> - <enum>Qt::DefaultContextMenu</enum> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <widget class="QPushButton" name="button_add_community"> - <property name="text"> - <string>Add a community</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_remove_community"> - <property name="text"> - <string>Remove selected community</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_4"> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <widget class="QPushButton" name="button_previous"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Previous</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_next"> - <property name="text"> - <string>Next</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources/> - <connections/> - <slots> - <slot>open_process_add_community()</slot> - <slot>key_changed(int)</slot> - <slot>action_remove_community()</slot> - <slot>open_process_edit_community(QModelIndex)</slot> - <slot>next()</slot> - <slot>previous()</slot> - <slot>open_import_key()</slot> - <slot>open_generate_account_key()</slot> - <slot>action_edit_account_key()</slot> - <slot>action_edit_account_parameters()</slot> - <slot>action_show_pubkey()</slot> - <slot>action_delete_account()</slot> - </slots> -</ui> diff --git a/src/sakia/gui/dialogs/account_cfg/controller.py b/src/sakia/gui/dialogs/account_cfg/controller.py deleted file mode 100644 index ebc74d1cd426d47eb9d959cf6585992d40b2abf1..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/account_cfg/controller.py +++ /dev/null @@ -1,210 +0,0 @@ -import asyncio -import logging - -from PyQt5.QtWidgets import QDialog - -from sakia.decorators import asyncify -from sakia.gui.component.controller import ComponentController -from sakia.gui.password_asker import PasswordAskerDialog, detect_non_printable -from .model import AccountConfigModel -from .view import AccountConfigView -from ..community_cfg.controller import CommunityConfigController - - -class AccountConfigController(ComponentController): - """ - The AccountConfigController view - """ - - def __init__(self, parent, view, model): - """ - Constructor of the AccountConfigController component - - :param sakia.gui.account_cfg.view.AccountConfigCView: the view - :param sakia.gui.account_cfg.model.AccountConfigModel model: the model - """ - super().__init__(parent, view, model) - - self._current_step = 0 - self.view.button_next.clicked.connect(lambda checked: self.handle_next_step(False)) - self._steps = ( - { - 'page': self.view.page_init, - 'init': self.init_name_page, - 'check': self.check_name, - 'next': self.account_name_selected - }, - { - 'page': self.view.page_brainwallet, - 'init': self.init_key_page, - 'check': self.check_key, - 'next': self.account_key_selected - }, - { - 'page': self.view.page_communities, - 'init': self.init_communities, - 'check': lambda: True, - 'next': self.accept - } - ) - self.handle_next_step(init=True) - self.password_asker = None - self.view.values_changed.connect(self.check_values) - - @classmethod - def create(cls, parent, app, **kwargs): - """ - Instanciate a AccountConfigController component - :param sakia.gui.component.controller.ComponentController parent: - :param sakia.core.Application app: - :return: a new AccountConfigController controller - :rtype: AccountConfigController - """ - view = AccountConfigView(parent.view if parent else None) - model = AccountConfigModel(None, app, None) - account_cfg = cls(parent, view, model) - model.setParent(account_cfg) - return account_cfg - - @classmethod - @asyncify - def create_account(cls, parent, app): - """ - Open a dialog to create a new account - :param parent: - :param app: - :param account: - :return: - """ - account_cfg = cls.create(parent, app, account=None) - account_cfg.view.set_creation_layout() - account_cfg.view.exec() - - @classmethod - def modify_account(cls, parent, app, account): - """ - Open a dialog to modify an existing account - :param parent: - :param app: - :param account: - :return: - """ - account_cfg = cls.create(parent, app, account=account) - account_cfg.view.set_modification_layout(account.name) - account_cfg._current_step = 1 - - def check_values(self): - """ - Check the values in the page and enable or disable previous/next buttons - """ - valid = self._steps[self._current_step]['check']() - self.view.button_next.setEnabled(valid) - - def init_name_page(self): - """ - Initialize an account name page - """ - if self.model.account: - self.view.set_account_name(self.model.account.name) - - self.view.button_previous.setEnabled(False) - self.view.button_next.setEnabled(False) - - def account_name_selected(self): - name = self.view.account_name() - if self.model.account is None: - self.model.instanciate_account(name) - else: - self.model.rename_account(name) - - def check_name(self): - return len(self.view.edit_account_name.text()) > 2 - - def init_key_page(self): - """ - Initialize key page - """ - self.view.button_previous.setEnabled(False) - self.view.button_next.setEnabled(False) - - def account_key_selected(self): - salt = self.view.edit_salt.text() - password = self.view.edit_password.text() - self.model.account.set_scrypt_infos(salt, password) - self.password_asker = PasswordAskerDialog(self.model.account) - - def check_key(self): - if self.model.app.preferences['expert_mode']: - return True - - if len(self.view.edit_salt.text()) < 6: - self.view.label_info.setText(self.tr("Forbidden : salt is too short")) - return False - - if len(self.view.edit_password.text()) < 6: - self.view.label_info.setText(self.tr("Forbidden : password is too short")) - return False - - if detect_non_printable(self.view.edit_salt.text()): - self.view.label_info.setText(self.tr("Forbidden : Invalid characters in salt field")) - return False - - if detect_non_printable(self.view.edit_password.text()): - self.view.label_info.setText( - self.tr("Forbidden : Invalid characters in password field")) - return False - - if self.view.edit_password.text() != \ - self.view.edit_password_repeat.text(): - self.view.label_info.setText(self.tr("Error : passwords are different")) - return False - - self.view.label_info.setText("") - return True - - def init_communities(self): - self.view.button_add_community.clicked.connect(self.open_process_add_community) - self.view.button_previous.setEnabled(False) - self.view.button_next.setText("Ok") - list_model = self.model.communities_list_model() - self.view.set_communities_list_model(list_model) - - def handle_next_step(self, init=False): - if not init: - self._steps[self._current_step]['next']() - self._current_step += 1 - if self._current_step < len(self._steps): - self._steps[self._current_step]['init']() - self.view.stacked_pages.setCurrentWidget(self._steps[self._current_step]['page']) - - @asyncify - async def open_process_add_community(self, checked=False): - logging.debug("Opening configure community dialog") - logging.debug(self.password_asker) - await CommunityConfigController.create_community(self, - self.model.app, - account=self.model.account, - password_asker=self.password_asker) - - @asyncify - async def accept(self): - await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - self.model.add_account_to_app() - self.view.accept() - - def async_exec(self): - future = asyncio.Future() - self.view.finished.connect(lambda r: future.set_result(r)) - self.view.open() - self.refresh() - return future - - @property - def view(self) -> AccountConfigView: - return self._view - - @property - def model(self) -> AccountConfigModel: - return self._model \ No newline at end of file diff --git a/src/sakia/gui/dialogs/account_cfg/model.py b/src/sakia/gui/dialogs/account_cfg/model.py deleted file mode 100644 index acd44c996b10d597ddb4eabd51753464365d46cc..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/account_cfg/model.py +++ /dev/null @@ -1,42 +0,0 @@ -from sakia.gui.component.model import ComponentModel -from sakia.models.communities import CommunitiesListModel - -class AccountConfigModel(ComponentModel): - """ - The model of AccountConfig component - """ - - def __init__(self, parent, app, account): - """ - - :param sakia.gui.dialogs.account_cfg.controller.AccountConfigController parent: - :param sakia.core.Application app: - :param sakia.core.Account account: - """ - super().__init__(parent) - self.app = app - self.account = account - - def instanciate_account(self, name): - """ - Creates an account with a given name - :param str name: the name of the new account - """ - self.account = self.app.create_account(name) - - def rename_account(self, name): - """ - Renames current account - :param str name: the new name - """ - self.account.name = name - - def communities_list_model(self): - return CommunitiesListModel(self.account) - - def add_account_to_app(self): - self.app.add_account(self.account) - if len(self.app.accounts) == 1: - self.app.preferences['account'] = self.account.name - self.app.save(self.account) - self.app.change_current_account(self.account) \ No newline at end of file diff --git a/src/sakia/gui/dialogs/community_cfg/__init__.py b/src/sakia/gui/dialogs/community_cfg/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/sakia/gui/dialogs/community_cfg/community_cfg.ui b/src/sakia/gui/dialogs/community_cfg/community_cfg.ui deleted file mode 100644 index b4a81028581780a148bbdc0f88ddf599159f311b..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/community_cfg/community_cfg.ui +++ /dev/null @@ -1,268 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>CommunityConfigurationDialog</class> - <widget class="QDialog" name="CommunityConfigurationDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>329</height> - </rect> - </property> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - <property name="windowTitle"> - <string>Add a community</string> - </property> - <property name="modal"> - <bool>true</bool> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QStackedWidget" name="stacked_pages"> - <property name="currentIndex"> - <number>0</number> - </property> - <widget class="QWidget" name="page_node"> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Please enter the address of a node :</string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="rightMargin"> - <number>5</number> - </property> - <item> - <widget class="QLineEdit" name="lineedit_server"/> - </item> - <item> - <widget class="QLabel" name="label_double_dot"> - <property name="text"> - <string>:</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spinbox_port"> - <property name="maximum"> - <number>65535</number> - </property> - <property name="value"> - <number>8001</number> - </property> - </widget> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QPushButton" name="button_register"> - <property name="text"> - <string>Register your account</string> - </property> - <property name="icon"> - <iconset resource="../../../../../res/icons/icons.qrc"> - <normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_connect"> - <property name="text"> - <string>Connect using your account</string> - </property> - <property name="icon"> - <iconset resource="../../../../../res/icons/icons.qrc"> - <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_guest"> - <property name="text"> - <string>Connect as a guest</string> - </property> - <property name="icon"> - <iconset resource="../../../../../res/icons/icons.qrc"> - <normaloff>:/icons/guest_icon</normaloff>:/icons/guest_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_error"> - <property name="text"> - <string/> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <widget class="QWidget" name="page_add_nodes"> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>Communities nodes</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <item> - <widget class="QTreeView" name="tree_peers"> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <widget class="QLineEdit" name="lineedit_add_address"> - <property name="text"> - <string/> - </property> - <property name="placeholderText"> - <string>Server</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spinbox_add_port"> - <property name="minimum"> - <number>0</number> - </property> - <property name="maximum"> - <number>65535</number> - </property> - <property name="singleStep"> - <number>1</number> - </property> - <property name="value"> - <number>8081</number> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_add"> - <property name="text"> - <string>Add</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="layout_previous_next"> - <item> - <widget class="QPushButton" name="button_previous"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>Previous</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_next"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="text"> - <string>Next</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources> - <include location="../../../../../res/icons/icons.qrc"/> - </resources> - <connections/> - <slots> - <slot>add_node()</slot> - <slot>showContextMenu(QPoint)</slot> - <slot>check()</slot> - <slot>next()</slot> - <slot>previous()</slot> - <slot>current_wallet_changed(int)</slot> - <slot>remove_node()</slot> - </slots> -</ui> diff --git a/src/sakia/gui/dialogs/community_cfg/controller.py b/src/sakia/gui/dialogs/community_cfg/controller.py deleted file mode 100644 index 31f732cab8e7ea5f0dd8d3b1d8006bbfe6917114..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/community_cfg/controller.py +++ /dev/null @@ -1,238 +0,0 @@ -import logging - -from PyQt5.QtGui import QCursor -from PyQt5.QtWidgets import QDialog, QApplication, QMenu -from aiohttp.errors import DisconnectedError, ClientError, TimeoutError -from sakia.errors import NoPeerAvailable - -from duniterpy.documents import MalformedDocumentError -from sakia.decorators import asyncify -from sakia.gui.component.controller import ComponentController -from .model import CommunityConfigModel -from .view import CommunityConfigView - - -class CommunityConfigController(ComponentController): - """ - The CommunityConfigController view - """ - - def __init__(self, parent, view, model): - """ - Constructor of the CommunityConfigController component - - :param sakia.gui.community_cfg.view.CommunityConfigView: the view - :param sakia.gui.community_cfg.model.CommunityConfigModel model: the model - """ - super().__init__(parent, view, model) - - self._current_step = 0 - self.view.button_next.clicked.connect(lambda checked: self.handle_next_step(False)) - self._steps = ( - { - 'page': self.view.page_node, - 'init': self.init_connect_page, - 'next': lambda: True - }, - { - 'page': self.view.page_add_nodes, - 'init': self.init_nodes_page, - 'next': self.accept - } - ) - self.handle_next_step(init=True) - self.password_asker = None - - self.view.button_connect.clicked.connect(self.check_connect) - self.view.button_register.clicked.connect(self.check_register) - self.view.button_guest.clicked.connect(self.check_guest) - - @classmethod - def create(cls, parent, app, **kwargs): - """ - Instanciate a CommunityConfigController component - :param sakia.gui.component.controller.ComponentController parent: - :param sakia.core.Application app: - :return: a new CommunityConfigController controller - :rtype: CommunityConfigController - """ - account = kwargs['account'] - community = kwargs['community'] - password_asker = kwargs['password_asker'] - view = CommunityConfigView(parent.view) - model = CommunityConfigModel(None, app, account, community) - community_cfg = cls(parent, view, model) - model.setParent(community_cfg) - community_cfg.password_asker = password_asker - return community_cfg - - @classmethod - @asyncify - def create_community(cls, parent, app, account, password_asker): - """ - Open a dialog to create a new Community - :param parent: - :param app: - :param account: - :return: - """ - community_cfg = cls.create(parent, app, account=account, community=None, password_asker=password_asker) - community_cfg.view.set_creation_layout() - return community_cfg.view.async_exec() - - @classmethod - @asyncify - def modify_community(cls, parent, app, account, community, password_asker): - """ - Open a dialog to modify an existing Community - :param parent: - :param app: - :param account: - :param community: - :return: - """ - community_cfg = cls.create(parent, app, account=account, - community=community, password_asker=password_asker) - community_cfg.view.set_modification_layout(community.name) - community_cfg._current_step = 1 - return community_cfg.view.async_exec() - - def handle_next_step(self, init=False): - if self._current_step < len(self._steps) - 1: - if not init: - self._steps[self._current_step]['next']() - self._current_step += 1 - self._steps[self._current_step]['init']() - self.view.stacked_pages.setCurrentWidget(self._steps[self._current_step]['page']) - - def init_connect_page(self): - pass - - def init_nodes_page(self): - self.view.set_steps_buttons_visible(True) - model = self.model.init_nodes_model() - self.view.tree_peers.customContextMenuRequested(self.show_context_menu) - - self.view.set_nodes_model(model) - self.view.button_previous.setEnabled(False) - self.view.button_next.setText(self.config_dialog.tr("Ok")) - - @asyncify - async def check_guest(self, checked=False): - server, port = self.view.node_parameters() - logging.debug("Is valid ? ") - self.view.display_info(self.tr("connecting...")) - try: - await self.model.create_community(server, port) - self.view.button_connect.setEnabled(False) - self.view.button_register.setEnabled(False) - self._steps[self._current_step]['next']() - except (DisconnectedError, ClientError, MalformedDocumentError, ValueError) as e: - self.view.display_info(str(e)) - except TimeoutError: - self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port")) - - @asyncify - async def check_connect(self, checked=False): - server, port = self.view.node_parameters() - logging.debug("Is valid ? ") - self.view.display_info.setText(self.tr("connecting...")) - try: - await self.model.create_community(server, port) - self.view.button_connect.setEnabled(False) - self.view.button_register.setEnabled(False) - registered = await self.model.check_registered() - self.view.button_connect.setEnabled(True) - self.view.button_register.setEnabled(True) - if registered[0] is False and registered[2] is None: - self.view.display_info(self.tr("Could not find your identity on the network.")) - elif registered[0] is False and registered[2]: - self.view.display_info(self.tr("""Your pubkey or UID is different on the network. -Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) - else: - self._steps[self._current_step]['next']() - except (DisconnectedError, ClientError, MalformedDocumentError, ValueError) as e: - self.view.display_info(str(e)) - except TimeoutError: - self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port")) - except NoPeerAvailable: - self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) - - @asyncify - async def check_register(self, checked=False): - server, port = self.view.node_parameters() - logging.debug("Is valid ? ") - self.view.display_info(self.tr("connecting...")) - try: - await self.model.create_community(server, port) - self.view.button_connect.setEnabled(False) - self.view.button_register.setEnabled(False) - registered = await self.model.check_registered() - self.view.button_connect.setEnabled(True) - self.view.button_register.setEnabled(True) - if registered[0] is False and registered[2] is None: - password = await self.password_asker.async_exec() - if self.password_asker.result() == QDialog.Rejected: - return - self.view.display_info(self.tr("Broadcasting identity...")) - result = await self.model.publish_selfcert(password) - if result[0]: - self.view.show_success() - QApplication.restoreOverrideCursor() - self._steps[self._current_step]['next']() - else: - self.view.show_error(self.model.notification(), result[1]) - QApplication.restoreOverrideCursor() - elif registered[0] is False and registered[2]: - self.view.display_info(self.tr("""Your pubkey or UID was already found on the network. -Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) - else: - self.display_info("Your account already exists on the network") - except (DisconnectedError, ClientError, MalformedDocumentError, ValueError) as e: - self.view.display_info(str(e)) - except NoPeerAvailable: - self.view.display_info(self.tr("Could not connect. Check node peering entry")) - except TimeoutError: - self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port")) - - def show_context_menu(self, point): - if self.view.stacked_pages.currentWidget() == self.steps[1]['widget']: - menu = QMenu() - index = self.model.nodes_tree_model.indexAt(point) - action = menu.addAction(self.tr("Delete"), self.remove_node) - action.setData(index.row()) - if len(self.nodes) == 1: - action.setEnabled(False) - menu.exec_(QCursor.pos()) - - @asyncify - async def add_node(self, checked=False): - """ - Add node slot - """ - server, port = self.view.add_node_parameters() - try: - await self.model.add_node(server, port) - except Exception as e: - self.view.show_error(self.model.notification(), str(e)) - - def remove_node(self): - """ - Remove node slot - """ - logging.debug("Remove node") - index = self.sender().data() - self.model.remove_node(index) - - def accept(self): - if self.community not in self.account.communities: - self.account.add_community(self.community) - self.view.accept() - - @property - def view(self) -> CommunityConfigView: - return self._view - - @property - def model(self) -> CommunityConfigModel: - return self._model \ No newline at end of file diff --git a/src/sakia/gui/dialogs/community_cfg/model.py b/src/sakia/gui/dialogs/community_cfg/model.py deleted file mode 100644 index d8364323f37daf043072068dc54807ed97243cdd..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/community_cfg/model.py +++ /dev/null @@ -1,60 +0,0 @@ -from sakia.gui.component.model import ComponentModel -from sakia.data.entities import Node -from sakia.data.connectors import NodeConnector -import aiohttp -from sakia.models.peering import PeeringTreeModel - - -class CommunityConfigModel(ComponentModel): - """ - The model of CommunityConfig component - """ - - def __init__(self, parent, app, user_parameters, currency, identity, nodes_processor, identities_processor): - """ - - :param sakia.gui.dialogs.Community_cfg.controller.CommunityConfigController parent: - :param sakia.core.Application app: - :param sakia.data.entities.UserParameters user_parameters: the parameters of the current profile - :param str currency: the currency configured - :param sakia.data.entities.Identity identity: the identity - :param sakia.data.processors.NodesProcessor nodes_processor: the nodes processor - :param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor - """ - super().__init__(parent) - self.app = app - self.nodes_tree_model = None - self.nodes = [] - self.user_parameters = user_parameters - self.currency = currency - self.identity = identity - self.nodes_processor = nodes_processor - self.identities_processor = identities_processor - - async def create_community(self, server, port): - with aiohttp.ClientSession() as session: - node_connector = await NodeConnector.from_address(None, server, port, session) - self.nodes.append(node_connector.node) - - async def add_node(self, server, port): - with aiohttp.ClientSession() as session: - node_connector = await NodeConnector.from_address(None, server, port, session) - self.nodes.append(node_connector.node) - self.nodes_tree_model.refresh_tree() - - def remove_node(self, index): - self.nodes.remove(index) - self.nodes_tree_model.refresh_tree() - - async def check_registered(self): - return await self.identities_processor.check_registered(self.currency, self.identity) - - #async def publish_selfcert(self, password): - # return await self.account.send_selfcert(password, self.community) - - def init_nodes_model(self): - # We add already known peers to the displayed list - self.nodes_tree_model = PeeringTreeModel(self.nodes) - - def notification(self): - self.user_parameters.notifications diff --git a/src/sakia/gui/dialogs/community_cfg/view.py b/src/sakia/gui/dialogs/community_cfg/view.py deleted file mode 100644 index cd4baaa24b15223d24a01e65ec8486ede894df3a..0000000000000000000000000000000000000000 --- a/src/sakia/gui/dialogs/community_cfg/view.py +++ /dev/null @@ -1,65 +0,0 @@ -from PyQt5.QtWidgets import QDialog -from PyQt5.QtCore import pyqtSignal -from .community_cfg_uic import Ui_CommunityConfigurationDialog -from sakia.gui.widgets import toast -from sakia.gui.widgets.dialogs import QAsyncMessageBox -import asyncio - - -class CommunityConfigView(QDialog, Ui_CommunityConfigurationDialog): - """ - community config view - """ - - def __init__(self, parent): - """ - Constructor - """ - super().__init__(parent) - self.setupUi(self) - self.set_steps_buttons_visible(False) - - def set_creation_layout(self): - self.setWindowTitle(self.tr("Add a community")) - - def set_edition_layout(self, name): - self.stacked_pages.removeWidget(self.page_node) - self.setWindowTitle(self.tr("Configure community {0}").format(name)) - - def display_info(self, info): - self.label_error.setText(info) - - def node_parameters(self): - server = self.lineedit_server.text() - port = self.spinbox_port.value() - return server, port - - def add_node_parameters(self): - server = self.lineedit_add_address.text() - port = self.spinbox_add_port.value() - return server, port - - async def show_success(self, notification): - if notification: - toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) - else: - await QAsyncMessageBox.information(self, self.tr("UID broadcast"), - self.tr("Identity broadcasted to the network")) - - def show_error(self, notification, error_txt): - if notification: - toast.display(self.tr("UID broadcast"), error_txt) - self.label_error.setText(self.tr("Error") + " " + error_txt) - - def set_steps_buttons_visible(self, visible): - self.button_next.setVisible(visible) - self.button_previous.setVisible(visible) - - def set_nodes_model(self, model): - self.tree_peers.setModel(model) - - def async_exec(self): - future = asyncio.Future() - self.finished.connect(lambda r: future.set_result(r)) - self.open() - return future diff --git a/src/sakia/gui/dialogs/account_cfg/__init__.py b/src/sakia/gui/dialogs/connection_cfg/__init__.py similarity index 100% rename from src/sakia/gui/dialogs/account_cfg/__init__.py rename to src/sakia/gui/dialogs/connection_cfg/__init__.py diff --git a/src/sakia/gui/dialogs/connection_cfg/connection_cfg.ui b/src/sakia/gui/dialogs/connection_cfg/connection_cfg.ui new file mode 100644 index 0000000000000000000000000000000000000000..30691e419f859c1b75b70d75fbbc48d5955c3f36 --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/connection_cfg.ui @@ -0,0 +1,381 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ConnectionConfigurationDialog</class> + <widget class="QDialog" name="ConnectionConfigurationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>639</width> + <height>447</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add a connection</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QStackedWidget" name="stacked_pages"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="page_node"> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Please enter the address of a node :</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_10"> + <property name="rightMargin"> + <number>5</number> + </property> + <item> + <widget class="QLineEdit" name="lineedit_server"/> + </item> + <item> + <widget class="QLabel" name="label_double_dot"> + <property name="text"> + <string>:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spinbox_port"> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>8001</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_13"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <spacer name="verticalSpacer_7"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_register"> + <property name="text"> + <string>Register your account</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_connect"> + <property name="text"> + <string>Connect using your account</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_guest"> + <property name="text"> + <string>Connect as a guest</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/guest_icon</normaloff>:/icons/guest_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_8"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_connection"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Account parameters</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_action"> + <property name="text"> + <string>Account name (uid)</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_account_name"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_delete"> + <property name="styleSheet"> + <string notr="true">color: rgb(255, 0, 0);</string> + </property> + <property name="text"> + <string>Delete account</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Key parameters</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"/> + </item> + <item> + <widget class="QLineEdit" name="edit_salt"> + <property name="text"> + <string/> + </property> + <property name="placeholderText"> + <string>Secret key</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_password"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Your password</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="edit_password_repeat"> + <property name="text"> + <string/> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="placeholderText"> + <string>Please repeat your password</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <property name="topMargin"> + <number>5</number> + </property> + <item> + <widget class="QPushButton" name="button_generate"> + <property name="text"> + <string>Show public key</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <property name="topMargin"> + <number>5</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_next"> + <property name="text"> + <string>Next</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_services"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QProgressBar" name="progress_bar"> + <property name="value"> + <number>24</number> + </property> + </widget> + </item> + <item> + <widget class="QPlainTextEdit" name="plain_text_edit"/> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QLabel" name="label_currency"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_info"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../res/icons/icons.qrc"/> + </resources> + <connections/> + <slots> + <slot>open_process_add_community()</slot> + <slot>key_changed(int)</slot> + <slot>action_remove_community()</slot> + <slot>open_process_edit_community(QModelIndex)</slot> + <slot>next()</slot> + <slot>previous()</slot> + <slot>open_import_key()</slot> + <slot>open_generate_account_key()</slot> + <slot>action_edit_account_key()</slot> + <slot>action_edit_account_parameters()</slot> + <slot>action_show_pubkey()</slot> + <slot>action_delete_account()</slot> + </slots> +</ui> diff --git a/src/sakia/gui/dialogs/connection_cfg/controller.py b/src/sakia/gui/dialogs/connection_cfg/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..66847154a3bbc5c110a64c1d52a9adf3a908d75a --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/controller.py @@ -0,0 +1,250 @@ +import asyncio +import logging + +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QDialog, QApplication, QMenu +from aiohttp.errors import DisconnectedError, ClientError, TimeoutError + +from duniterpy.documents import MalformedDocumentError +from sakia.errors import NoPeerAvailable +from sakia.decorators import asyncify +from sakia.data.processors import IdentitiesProcessor, NodesProcessor +from sakia.data.connectors import BmaConnector +from sakia.gui.component.controller import ComponentController +from sakia.gui.password_asker import PasswordAskerDialog, detect_non_printable +from .model import ConnectionConfigModel +from .view import ConnectionConfigView + + +class ConnectionConfigController(ComponentController): + """ + The AccountConfigController view + """ + + CONNECT = 0 + REGISTER = 1 + GUEST = 2 + + def __init__(self, parent, view, model): + """ + Constructor of the AccountConfigController component + + :param sakia.gui.account_cfg.view.AccountConfigCView: the view + :param sakia.gui.account_cfg.model.AccountConfigModel model: the model + """ + super().__init__(parent, view, model) + + self.step_node = asyncio.Future() + self.step_key = asyncio.Future() + self.view.button_connect.clicked.connect( + lambda: self.step_node.set_result(ConnectionConfigController.CONNECT)) + self.view.button_register.clicked.connect( + lambda: self.step_node.set_result(ConnectionConfigController.REGISTER)) + self.view.button_guest.clicked.connect( + lambda: self.step_node.set_result(ConnectionConfigController.GUEST)) + self.password_asker = None + self.view.values_changed.connect(self.check_key) + self._logger = logging.getLogger('sakia') + + @classmethod + def create(cls, parent, app, **kwargs): + """ + Instanciate a AccountConfigController component + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.app.Application app: + :return: a new AccountConfigController controller + :rtype: AccountConfigController + """ + view = ConnectionConfigView(parent.view if parent else None) + model = ConnectionConfigModel(None, app, None, + IdentitiesProcessor(app.db.identities_repo, app.db.blockchains_repo, + BmaConnector(NodesProcessor(app.db.nodes_repo)))) + account_cfg = cls(parent, view, model) + model.setParent(account_cfg) + return account_cfg + + @classmethod + def create_connection(cls, parent, app): + """ + Open a dialog to create a new account + :param parent: + :param app: + :return: + """ + connection_cfg = cls.create(parent, app, account=None) + connection_cfg.view.set_creation_layout() + asyncio.ensure_future(connection_cfg.process()) + connection_cfg.view.exec() + + @classmethod + def modify_connection(cls, parent, app, connection): + """ + Open a dialog to modify an existing account + :param parent: + :param app: + :param account: + :return: + """ + connection_cfg = cls.create(parent, app, connection=connection) + #connection_cfg.view.set_modification_layout(account.name) + connection_cfg._current_step = 1 + + def init_nodes_page(self): + self.view.set_steps_buttons_visible(True) + model = self.model.init_nodes_model() + self.view.tree_peers.customContextMenuRequested(self.show_context_menu) + + self.view.set_nodes_model(model) + self.view.button_previous.setEnabled(False) + self.view.button_next.setText(self.config_dialog.tr("Ok")) + + def init_name_page(self): + """ + Initialize an account name page + """ + if self.model.connection: + self.view.set_account_name(self.model.connection.uid) + + self.view.button_previous.setEnabled(False) + self.view.button_next.setEnabled(False) + + def check_name(self): + return len(self.view.edit_account_name.text()) > 2 + + async def process(self): + self._logger.debug("Begin process") + while not self.model.connection: + mode = await self.step_node + self._logger.debug("Create connection") + try: + self.view.button_connect.setEnabled(False) + self.view.button_register.setEnabled(False) + await self.model.create_connection(self.view.lineedit_server.text(), + self.view.spinbox_port.value()) + self.password_asker = PasswordAskerDialog(self.model.connection) + except (DisconnectedError, ClientError, MalformedDocumentError, ValueError, TimeoutError) as e: + self._logger.debug(str(e)) + self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port : </br>str(e)")) + self.step_node = asyncio.Future() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + + self._logger.debug("Key step") + self.view.set_currency(self.model.connection.currency) + if mode == ConnectionConfigController.REGISTER: + self._logger.debug("Registering mode") + self.view.button_next.clicked.connect(self.check_register) + self.view.stacked_pages.setCurrentWidget(self.view.page_connection) + await self.step_key + elif mode == ConnectionConfigController.CONNECT: + self._logger.debug("Connect mode") + self.view.button_next.clicked.connect(self.check_connect) + self.view.stacked_pages.setCurrentWidget(self.view.page_connection) + await self.step_key + + self.model.commit_connection() + self.view.stacked_pages.setCurrentWidget(self.view.page_services) + self.view.progress_bar.setValue(0) + self.view.progress_bar.setMaximum(3) + await self.model.initialize_blockchain(self.view.stream_log) + self.view.progress_bar.setValue(1) + self.view.display_info(self.tr("Broadcasting identity...")) + self.view.stream_log("Broadcasting identity...") + password = await self.password_asker.async_exec() + result = await self.model.publish_selfcert(self.model.connection.salt, password) + if result[0]: + self.view.show_success(self.model.notification()) + else: + self.view.show_error(self.model.notification(), result[1]) + self._logger.debug("Validate changes") + self.accept() + + def check_key(self): + if self.model.app.parameters.expert_mode: + return True + + if len(self.view.edit_salt.text()) < 6: + self.view.label_info.setText(self.tr("Forbidden : salt is too short")) + return False + + if len(self.view.edit_password.text()) < 6: + self.view.label_info.setText(self.tr("Forbidden : password is too short")) + return False + + if detect_non_printable(self.view.edit_salt.text()): + self.view.label_info.setText(self.tr("Forbidden : Invalid characters in salt field")) + return False + + if detect_non_printable(self.view.edit_password.text()): + self.view.label_info.setText( + self.tr("Forbidden : Invalid characters in password field")) + return False + + if self.view.edit_password.text() != \ + self.view.edit_password_repeat.text(): + self.view.label_info.setText(self.tr("Error : passwords are different")) + return False + + self.view.label_info.setText("") + return True + + @asyncify + async def check_connect(self, checked=False): + self._logger.debug("Is valid ? ") + self.view.display_info.setText(self.tr("connecting...")) + try: + salt = self.view.edit_salt.text() + password = self.view.edit_password.text() + self.model.set_scrypt_infos(salt, password) + self.model.set_uid(self.view.edit_account_name.text()) + registered = await self.model.check_registered() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + self.view.display_info(self.tr("Could not find your identity on the network.")) + elif registered[0] is False and registered[2]: + self.view.display_info(self.tr("""Your pubkey or UID is different on the network. +Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.step_key.set_result(True) + except NoPeerAvailable: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) + + @asyncify + async def check_register(self, checked=False): + self._logger.debug("Is valid ? ") + self.view.display_info(self.tr("connecting...")) + try: + salt = self.view.edit_salt.text() + password = self.view.edit_password.text() + self.model.set_scrypt_infos(salt, password) + self.model.set_uid(self.view.edit_account_name.text()) + registered = await self.model.check_registered() + if registered[0] is False and registered[2] is None: + self.step_key.set_result(True) + elif registered[0] is False and registered[2]: + self.view.display_info(self.tr("""Your pubkey or UID was already found on the network. +Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.display_info("Your account already exists on the network") + except NoPeerAvailable: + self.view.display_info(self.tr("Could not connect. Check node peering entry")) + + @asyncify + async def accept(self): + self.view.accept() + + def async_exec(self): + future = asyncio.Future() + self.view.finished.connect(lambda r: future.set_result(r)) + self.view.open() + self.refresh() + return future + + @property + def view(self) -> ConnectionConfigView: + return self._view + + @property + def model(self) -> ConnectionConfigModel: + return self._model \ No newline at end of file diff --git a/src/sakia/gui/dialogs/connection_cfg/model.py b/src/sakia/gui/dialogs/connection_cfg/model.py new file mode 100644 index 0000000000000000000000000000000000000000..f9135a6e8c0d99a611113e051a86785b6458c72c --- /dev/null +++ b/src/sakia/gui/dialogs/connection_cfg/model.py @@ -0,0 +1,163 @@ +import aiohttp + +from duniterpy.documents import BlockUID, BMAEndpoint, Block +from duniterpy.api import bma, errors +from duniterpy.key import SigningKey +from sakia.data.entities import Connection, Identity, Blockchain, Node +from sakia.data.connectors import NodeConnector, BmaConnector +from sakia.data.processors import ConnectionsProcessor, NodesProcessor, BlockchainProcessor +from sakia.gui.component.model import ComponentModel + + +class ConnectionConfigModel(ComponentModel): + """ + The model of AccountConfig component + """ + + def __init__(self, parent, app, connection, identities_processor, node_connector=None): + """ + + :param sakia.gui.dialogs.account_cfg.controller.AccountConfigController parent: + :param sakia.app.Application app: the main application + :param sakia.data.entities.Connection connection: the connection + :param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor + :param sakia.data.connectors.NodeConnector node_connector: the node connector + """ + super().__init__(parent) + self.app = app + self.connection = connection + self.node_connector = node_connector + self.identities_processor = identities_processor + + async def create_connection(self, server, port): + session = aiohttp.ClientSession() + try: + self.node_connector = await NodeConnector.from_address(None, server, port, session) + self.connection = Connection(self.node_connector.node.currency, "", "") + self.node_connector.node.state = Node.ONLINE + except: + session.close() + raise + + def notification(self): + return self.app.parameters.notifications + + def set_uid(self, uid): + self.connection.uid = uid + + def set_scrypt_infos(self, salt, password): + self.connection.salt = salt + self.connection.pubkey = SigningKey(self.connection.salt, password).pubkey + + def commit_connection(self): + ConnectionsProcessor(self.app.db.connections_repo).commit_connection(self.connection) + NodesProcessor(self.app.db.nodes_repo).commit_node(self.node_connector.node) + + async def initialize_blockchain(self, log_stream): + """ + Download blockchain information locally + :param function log_stream: a method to log data in the screen + :return: + """ + blockchain_processor = BlockchainProcessor(self.app.db.blockchains_repo, + BmaConnector(NodesProcessor(self.app.db.nodes_repo))) + await blockchain_processor.initialize_blockchain(self.node_connector.node.currency, log_stream) + + async def publish_selfcert(self, salt, password): + """" + Publish the self certification of the connection identity + """ + return await self.identities_processor.publish_selfcert(self.node_connector.node.currency, + Identity(self.connection.currency, + self.connection.pubkey, + self.connection.uid), + salt, password) + + async def check_registered(self): + """ + Checks for the pubkey and the uid of an account on a given node + :return: (True if found, local value, network value) + """ + identity = Identity(self.connection.currency, self.connection.pubkey, self.connection.uid) + + def _parse_uid_certifiers(data): + return identity.uid == data['uid'], identity.uid, data['uid'] + + def _parse_uid_lookup(data): + timestamp = BlockUID.empty() + found_uid = "" + for result in data['results']: + if result["pubkey"] == identity.pubkey: + uids = result['uids'] + for uid_data in uids: + if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: + timestamp = uid_data["meta"]["timestamp"] + found_uid = uid_data["uid"] + return identity.uid == found_uid, identity.uid, found_uid + + def _parse_pubkey_certifiers(data): + return identity.pubkey == data['pubkey'], identity.pubkey, data['pubkey'] + + def _parse_pubkey_lookup(data): + timestamp = BlockUID.empty() + found_uid = "" + found_result = ["", ""] + for result in data['results']: + uids = result['uids'] + for uid_data in uids: + if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: + timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) + found_uid = uid_data["uid"] + if found_uid == identity.uid: + found_result = result['pubkey'], found_uid + if found_result[1] == identity.uid: + return identity.pubkey == found_result[0], identity.pubkey, found_result[0] + else: + return False, identity.pubkey, None + + async def execute_requests(parsers, search): + tries = 0 + request = bma.wot.CertifiersOf + nonlocal registered + for endpoint in [e for e in self.node_connector.node.endpoints if isinstance(e, BMAEndpoint)]: + if not registered[0] and not registered[2]: + try: + data = await self.node_connector.safe_request(endpoint, request, req_args={'search': search}) + if data: + registered = parsers[request](data) + tries += 1 + except errors.DuniterError as e: + if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, + e.ucode == errors.NO_MATCHING_IDENTITY): + if request == bma.wot.CertifiersOf: + request = bma.wot.Lookup + tries = 0 + else: + tries += 1 + else: + tries += 1 + else: + break + + # cell 0 contains True if the user is already registered + # cell 1 contains the uid/pubkey selected locally + # cell 2 contains the uid/pubkey found on the network + registered = (False, identity.uid, None) + # We execute search based on pubkey + # And look for account UID + uid_parsers = { + bma.wot.CertifiersOf: _parse_uid_certifiers, + bma.wot.Lookup: _parse_uid_lookup + } + await execute_requests(uid_parsers, identity.pubkey) + + # If the uid wasn't found when looking for the pubkey + # We look for the uid and check for the pubkey + if not registered[0] and not registered[2]: + pubkey_parsers = { + bma.wot.CertifiersOf: _parse_pubkey_certifiers, + bma.wot.Lookup: _parse_pubkey_lookup + } + await execute_requests(pubkey_parsers, identity.uid) + + return registered diff --git a/src/sakia/gui/dialogs/account_cfg/view.py b/src/sakia/gui/dialogs/connection_cfg/view.py similarity index 56% rename from src/sakia/gui/dialogs/account_cfg/view.py rename to src/sakia/gui/dialogs/connection_cfg/view.py index 0aa8d07785b08ffceaac5fbeb3a371c141b814b0..0db0ee81c72887ff587829ec8e9f992f73d12714 100644 --- a/src/sakia/gui/dialogs/account_cfg/view.py +++ b/src/sakia/gui/dialogs/connection_cfg/view.py @@ -1,12 +1,14 @@ from PyQt5.QtWidgets import QDialog from PyQt5.QtCore import pyqtSignal -from .account_cfg_uic import Ui_AccountConfigurationDialog +from .connection_cfg_uic import Ui_ConnectionConfigurationDialog from duniterpy.key import SigningKey +from ...widgets import toast +from ...widgets.dialogs import QAsyncMessageBox -class AccountConfigView(QDialog, Ui_AccountConfigurationDialog): +class ConnectionConfigView(QDialog, Ui_ConnectionConfigurationDialog): """ - Home screen view + Connection config view """ values_changed = pyqtSignal() @@ -22,6 +24,33 @@ class AccountConfigView(QDialog, Ui_AccountConfigurationDialog): self.edit_salt.textChanged.connect(self.values_changed) self.button_generate.clicked.connect(self.action_show_pubkey) + def display_info(self, info): + self.label_info.setText(info) + + def set_currency(self, currency): + self.label_currency.setText(currency) + + def add_node_parameters(self): + server = self.lineedit_add_address.text() + port = self.spinbox_add_port.value() + return server, port + + async def show_success(self, notification): + if notification: + toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) + else: + await QAsyncMessageBox.information(self, self.tr("UID broadcast"), + self.tr("Identity broadcasted to the network")) + + def show_error(self, notification, error_txt): + if notification: + toast.display(self.tr("UID broadcast"), error_txt) + self.label_info.setText(self.tr("Error") + " " + error_txt) + + def set_nodes_model(self, model): + self.tree_peers.setModel(model) + + def set_creation_layout(self): """ Hide unecessary buttons and display correct title @@ -55,3 +84,10 @@ class AccountConfigView(QDialog, Ui_AccountConfigurationDialog): :param sakia.models.communities.CommunitiesListModel model: """ self.list_communities.setModel(model) + + def stream_log(self, log): + """ + Add log to + :param str log: + """ + self.plain_text_edit.insertPlainText("\n" + log) diff --git a/src/sakia/gui/main_window/toolbar/controller.py b/src/sakia/gui/main_window/toolbar/controller.py index 49ec27f91a2705d92b09102376a1859b86009eaa..3301f3d59514dce0404ea67d094e636fd850dd75 100644 --- a/src/sakia/gui/main_window/toolbar/controller.py +++ b/src/sakia/gui/main_window/toolbar/controller.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QDialog, QMessageBox from sakia.decorators import asyncify, once_at_a_time from sakia.gui.component.controller import ComponentController -from sakia.gui.dialogs.account_cfg.controller import AccountConfigController +from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController from sakia.gui.dialogs.certification.controller import CertificationController from sakia.gui.dialogs.transfer.controller import TransferController from sakia.gui.widgets import toast @@ -211,7 +211,7 @@ The process to join back the community later will have to be done again.""") community=self.model.community) def open_create_account_dialog(self): - AccountConfigController.create_account(self, self.model.app) + ConnectionConfigController.create_connection(self, self.model.app) def retranslateUi(self, widget): """ diff --git a/src/sakia/gui/navigation/model.py b/src/sakia/gui/navigation/model.py index 2803b14bcc299d3b74ce402ca20d9c46d3030354..2947ede00a70b2293a759765100e2cc7127e1718 100644 --- a/src/sakia/gui/navigation/model.py +++ b/src/sakia/gui/navigation/model.py @@ -32,7 +32,7 @@ class NavigationModel(ComponentModel): } ] self._current_data = self.navigation[0] - for connection in self.app.connections_repo.get_all(): + for connection in self.app.db.connections_repo.get_all(): self.navigation[0]['children'].append({ 'node': { 'title': connection.currency, diff --git a/src/sakia/gui/password_asker.py b/src/sakia/gui/password_asker.py index adfd850b9444204da817ac840cfcad7379d71e88..036a6d1c05809499e19e0e009a3562b7f713fa73 100644 --- a/src/sakia/gui/password_asker.py +++ b/src/sakia/gui/password_asker.py @@ -7,6 +7,7 @@ Created on 24 dec. 2014 import logging import re import asyncio +from duniterpy.key import SigningKey from PyQt5.QtCore import QEvent from PyQt5.QtWidgets import QDialog, QMessageBox @@ -19,47 +20,44 @@ class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): A dialog to get password. """ - def __init__(self, account): + def __init__(self, connection): """ Constructor + + :param sakia.data.entities.Connection connection: a given connection """ super().__init__() self.setupUi(self) - self.account = account self.password = "" + self.connection = connection self.remember = False - def change_account(self, account): - self.remember = False - self.password = "" - self.account = account - def async_exec(self): future = asyncio.Future() - if not self.remember: + if not self.connection.password: def future_show(): pwd = self.password - if not self.remember: - self.password = "" + if self.remember: + self.connection.password = self.password self.finished.disconnect(future_show) future.set_result(pwd) self.open() self.finished.connect(future_show) else: self.setResult(QDialog.Accepted) - future.set_result(self.password) + future.set_result(self.connection.password) return future - def exec_(self): - if not self.remember: + def exec(self): + if not self.connection.password: super().exec_() pwd = self.password - if not self.remember: - self.password = "" + if self.remember: + self.connection.password = self.password return pwd else: self.setResult(QDialog.Accepted) - return self.password + return self.connection.password def accept(self): password = self.edit_password.text() @@ -70,7 +68,7 @@ class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): QMessageBox.Ok) return False - if not self.account.check_password(password): + if SigningKey(self.connection.salt, password).pubkey != self.connection.pubkey: QMessageBox.warning(self, self.tr("Failed to get private key"), self.tr("Wrong password typed. Cannot open the private key"), QMessageBox.Ok) diff --git a/src/sakia/main.py b/src/sakia/main.py index 1215c3c6f08fc4f62bda9b22e0c2dbe0717fbb79..3cf28a778f72e053e5b711bc2c9f65eeb7ddac37 100755 --- a/src/sakia/main.py +++ b/src/sakia/main.py @@ -14,7 +14,7 @@ from PyQt5.QtWidgets import QApplication, QMessageBox from quamash import QSelectorEventLoop from sakia.app import Application -from sakia.gui.dialogs.account_cfg.controller import AccountConfigController +from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController from sakia.gui.main_window.controller import MainWindowController @@ -95,7 +95,7 @@ if __name__ == '__main__': loop.run_forever() try: loop.set_exception_handler(None) - loop.run_until_complete(app.stop()) + #loop.run_until_complete(app.stop()) logging.debug("Application stopped") except asyncio.CancelledError: logging.info('CancelledError') diff --git a/src/sakia/options.py b/src/sakia/options.py index dd5f33480b297d4d7c498f623966ebde00735396..35ed0cb18d3d8956f2e8922e6c0c99568d12faaa 100644 --- a/src/sakia/options.py +++ b/src/sakia/options.py @@ -52,12 +52,12 @@ class SakiaOptions: (options, args) = parser.parse_args(argv) - formatter = logging.Formatter('%(levelname)s:%(message)s') if options.debug: self._logger.setLevel(logging.DEBUG) formatter = logging.Formatter('%(levelname)s:%(module)s:%(funcName)s:%(message)s') elif options.verbose: self._logger.setLevel(logging.INFO) + formatter = logging.Formatter('%(levelname)s:%(message)s') logging.getLogger('quamash').setLevel(logging.INFO) file_handler = RotatingFileHandler(path.join(self.config_path, 'sakia.log'), 'a', 1000000, 10) diff --git a/src/sakia/services/blockchain.py b/src/sakia/services/blockchain.py index 397da1d5bb27a81eeb3c504834a95ab9b4ac4e96..3dffb766d32bc17bc49174daa6c4e73f67456068 100644 --- a/src/sakia/services/blockchain.py +++ b/src/sakia/services/blockchain.py @@ -28,7 +28,6 @@ class BlockchainService(QObject): async def handle_blockchain_progress(self): """ Handle a new current block uid - :param duniterpy.documents.BlockUID new_block_uid: the new current blockuid """ with_identities = await self._blockchain_processor.new_blocks_with_identities() with_money = await self._blockchain_processor.new_blocks_with_money() diff --git a/src/sakia/services/identities.py b/src/sakia/services/identities.py index feb8bb0603f66f05059c85e3fdc8a87454438121..6f0637f08ec4a7cd1cfd0528201f9551625f749c 100644 --- a/src/sakia/services/identities.py +++ b/src/sakia/services/identities.py @@ -60,10 +60,10 @@ class IdentitiesService(QObject): await self.update_certified_by(identity) if len(certified) > 0: latest_time = max([c['cert_time'] for c in certified if c['cert_time']]) - sig_period = await self._blockchain_processor.parameters(currency).sig_period + sig_period = await self._blockchain_processor.parameters(self.currency).sig_period current_time = await self._blockchain_processor.time(self.currency) - if current_time - latest_time < parameters['sigPeriod']: - return parameters['sigPeriod'] - (current_time - latest_time) + if current_time - latest_time < sig_period: + return sig_period - (current_time - latest_time) return 0 async def update_memberships(self, identity): diff --git a/src/sakia/services/network.py b/src/sakia/services/network.py index ab13d3b09da7a3f36569ed1f940243d01b54c639..0e200a03a8756c27fb8304cc9edaf2cc98b4ff5a 100644 --- a/src/sakia/services/network.py +++ b/src/sakia/services/network.py @@ -32,17 +32,17 @@ class NetworkService(QObject): :param sakia.services.BlockchainService blockchain_service: the blockchain service """ super().__init__() + self._logger = logging.getLogger('sakia') self._processor = node_processor self._connectors = [] for c in connectors: self.add_connector(c) self.currency = currency self._must_crawl = False - self._block_found = self._processor.current_buid() + self._block_found = self._processor.current_buid(self.currency) self._client_session = session self._discovery_stack = [] self._blockchain_service = blockchain_service - self._logger = logging.getLogger('sakia') @classmethod def create(cls, node_processor, node_connector): @@ -71,9 +71,9 @@ class NetworkService(QObject): """ connectors = [] session = aiohttp.ClientSession() - for node in node_processor.nodes(): + for node in node_processor.nodes(currency): connectors.append(NodeConnector(node, session)) - network = cls(currency, node_processor, connectors, session) + network = cls(currency, node_processor, connectors, session, blockchain_service) return network def start_coroutines(self): diff --git a/src/sakia/tests/technical/test_documents_service.py b/src/sakia/tests/technical/test_documents_service.py index 83bec4763205e1606ee1702c230b70a3377735b0..076c4bc43aaea3ef13cb9794ad11d3a744d70e1e 100644 --- a/src/sakia/tests/technical/test_documents_service.py +++ b/src/sakia/tests/technical/test_documents_service.py @@ -6,7 +6,7 @@ from duniterpy.documents import BlockUID, Peer from sakia.tests import QuamashTest from sakia.services import DocumentsService from sakia.data.connectors import NodeConnector, BmaConnector -from sakia.data.repositories import NodesRepo, MetaDatabase, BlockchainsRepo, IdentitiesRepo +from sakia.data.repositories import NodesRepo, SakiaDatabase, BlockchainsRepo, IdentitiesRepo from sakia.data.processors import NodesProcessor, BlockchainProcessor, IdentitiesProcessor @@ -24,7 +24,7 @@ class TestDocumentsService(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_certify(self): - meta_repo = MetaDatabase(self.con) + meta_repo = SakiaDatabase(self.con) meta_repo.prepare() meta_repo.upgrade_database() nodes_repo = NodesRepo(self.con) diff --git a/src/sakia/tests/technical/test_identities_service.py b/src/sakia/tests/technical/test_identities_service.py index f1ad4434abf2805908666efc732a811f2f7b2022..0ef3ade60dfcb233786cdd30bf9033c764c2e2bc 100644 --- a/src/sakia/tests/technical/test_identities_service.py +++ b/src/sakia/tests/technical/test_identities_service.py @@ -4,7 +4,7 @@ from duniterpy.documents import BlockUID, Block from sakia.tests.mocks.bma.nice_blockchain import bma_blockchain_0 from sakia.tests import QuamashTest from sakia.services import IdentitiesService -from sakia.data.repositories import CertificationsRepo, IdentitiesRepo, MetaDatabase +from sakia.data.repositories import CertificationsRepo, IdentitiesRepo, SakiaDatabase from sakia.data.processors import CertificationsProcessor, IdentitiesProcessor @@ -22,7 +22,7 @@ class TestIdentitiesService(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_new_block_with_unknown_identities(self): - meta_repo = MetaDatabase(self.con) + meta_repo = SakiaDatabase(self.con) meta_repo.prepare() meta_repo.upgrade_database() identities_repo = IdentitiesRepo(self.con) diff --git a/src/sakia/tests/technical/test_network_service.py b/src/sakia/tests/technical/test_network_service.py index 6c37c9399f2a9c59580bc82253aa26ff31da2d26..49f5477cae0819aaf56d9e20038ae1af69fe64b9 100644 --- a/src/sakia/tests/technical/test_network_service.py +++ b/src/sakia/tests/technical/test_network_service.py @@ -6,7 +6,7 @@ from duniterpy.documents import BlockUID, Peer from sakia.tests import QuamashTest from sakia.services import NetworkService from sakia.data.connectors import NodeConnector -from sakia.data.repositories import NodesRepo, MetaDatabase +from sakia.data.repositories import NodesRepo, SakiaDatabase from sakia.data.processors import NodesProcessor @@ -24,7 +24,7 @@ class TestNetworkService(unittest.TestCase, QuamashTest): self.tearDownQuamash() def test_network_discovering(self): - meta_repo = MetaDatabase(self.con) + meta_repo = SakiaDatabase(self.con) meta_repo.prepare() meta_repo.upgrade_database() nodes_repo = NodesRepo(self.con) diff --git a/src/sakia/tests/unit/core/test_account.py b/src/sakia/tests/unit/core/test_account.py deleted file mode 100644 index 12d2982e6307f05b67da665f3178b6354f5d7bd0..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_account.py +++ /dev/null @@ -1,150 +0,0 @@ -import unittest -from asynctest import Mock, MagicMock, CoroutineMock -from PyQt5.QtCore import QLocale -from sakia.core.registry.identities import IdentitiesRegistry, Identity -from sakia.core import Account -from sakia.tests import QuamashTest -from duniterpy.documents import BlockUID, SelfCertification - - -class TestAccount(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - self.identities_registry = IdentitiesRegistry() - - def tearDown(self): - self.tearDownQuamash() - - def test_load_save_account(self): - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - "test_uid", [], [], [], self.identities_registry) - json_data = account.jsonify() - account_from_json = Account.load(json_data, self.identities_registry) - - self.assertEqual(account.name, account_from_json.name) - self.assertEqual(account.pubkey, account_from_json.pubkey) - self.assertEqual(len(account.communities), len(account_from_json.communities)) - self.assertEqual(len(account.wallets), len(account.wallets)) - - def test_add_contact(self): - called = False - - def signal_called(): - nonlocal called - called = True - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - "test_uid", [], [], [], self.identities_registry) - account.contacts_changed.connect(signal_called) - account.add_contact({"uid":"friend", "pubkey":"FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"}) - self.assertEqual(len(account.contacts), 1) - self.assertEqual(account.contacts[0]["uid"], "friend") - self.assertEqual(account.contacts[0]["pubkey"], "FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") - self.assertTrue(called) - - def test_remove_contact(self): - called = False - - def signal_called(): - nonlocal called - called = True - contact = {"uid":"friend", "pubkey":"FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"} - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - "test_uid", [], [], [contact], - self.identities_registry) - account.contacts_changed.connect(signal_called) - account.remove_contact(contact) - self.assertEqual(len(account.contacts), 0) - self.assertTrue(called) - - def test_edit_contact(self): - called = False - - def signal_called(): - nonlocal called - called = True - contact = {"uid":"friend", "pubkey":"FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"} - account = Account("test_salt", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", - "test_uid", [], [], [contact], - self.identities_registry) - account.contacts_changed.connect(signal_called) - account.edit_contact(0, {"uid": "ennemy", "pubkey": "FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"}) - self.assertEqual(len(account.contacts), 1) - self.assertEqual(account.contacts[0]["uid"], "ennemy") - self.assertEqual(account.contacts[0]["pubkey"], "FFFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") - self.assertTrue(called) - - def test_send_membership(self): - account = Account("test_salt", "H8uYXvyF6GWeCr8cwFJ6V5B8tNprwRdjepFNJBqivrzr", - "test_account", [], [], [], - self.identities_registry) - account_identity = MagicMock(autospec='sakia.core.registry.Identity') - account_identity.selfcert = CoroutineMock(return_value=SelfCertification(2, "meta_brouzouf", - "H8uYXvyF6GWeCr8cwFJ6V5B8tNprwRdjepFNJBqivrzr", "test_account", 1000000000, "")) - community = MagicMock(autospec='sakia.core.Community') - community.blockUID = CoroutineMock(return_value=BlockUID(3102, "0000C5336F0B64BFB87FF4BC858AE25726B88175")) - self.identities_registry.future_find = CoroutineMock(return_value=account_identity) - community.bma_access = MagicMock(autospec='sakia.core.net.api.bma.access.BmaAccess') - response = Mock() - response.json = CoroutineMock(return_value={ - "signature": "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", - "membership": { - "version": 2, - "currency": "beta_brouzouf", - "issuer": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", - "membership": "IN", - "sigDate": 1390739944, - "uid": "superman63" - } -}) - response.status = 200 - community.bma_access.broadcast = CoroutineMock(return_value=[response]) - async def exec_test(): - result = await account.send_membership("test_password", community, "IN") - self.assertTrue(result) - - self.lp.run_until_complete(exec_test()) - - def test_send_certification(self): - cert_signal_sent = False - def check_certification_accepted(): - nonlocal cert_signal_sent - cert_signal_sent = True - - account = Account("test_salt", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", - "test_account", [], [], [], - self.identities_registry) - account.certification_accepted.connect(check_certification_accepted) - account_identity = MagicMock(autospec='sakia.core.registry.Identity') - account_identity.selfcert = CoroutineMock(return_value=SelfCertification(2, "meta_brouzouf", - "H8uYXvyF6GWeCr8cwFJ6V5B8tNprwRdjepFNJBqivrzr", "test_account", - BlockUID(1000, "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243"), - "82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw==")) - - certified = MagicMock(autospec='sakia.core.registry.Identity') - certified.uid = "john" - certified.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - certified.sigdate = 1441130831 - certified.selfcert = CoroutineMock(return_value=SelfCertification(2, "meta_brouzouf", - "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", "john", - BlockUID(1200, "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243"), - "82o1sNCh1bLpUXU6nacbK48HBcA9Eu2sPkL1/3c2GtDPxBUZd2U2sb7DxwJ54n6ce9G0Oy7nd1hCxN3fS0oADw==")) - - community = MagicMock(autospec='sakia.core.Community') - community.blockUID = CoroutineMock(return_value=BlockUID(3102, "49E2A1D1131F1496FAD6EDAE794A9ADBFA8844029675E3732D3B027ABB780243")) - self.identities_registry.future_find = CoroutineMock(side_effect=lambda pubkey, community :account_identity \ - if pubkey == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" else certified) - community.bma_access = MagicMock(autospec='sakia.core.net.api.bma.access.BmaAccess') - response = Mock() - response.json = CoroutineMock(return_value={}) - response.status = 200 - community.bma_access.broadcast = CoroutineMock(return_value=[response]) - async def exec_test(): - result = await account.certify("test_password", community, "") - self.assertTrue(result) - - self.lp.run_until_complete(exec_test()) - self.assertTrue(cert_signal_sent) - - - diff --git a/src/sakia/tests/unit/core/test_community.py b/src/sakia/tests/unit/core/test_community.py deleted file mode 100644 index 16d2fd66a49a39b449979190a7d9797eed863f74..0000000000000000000000000000000000000000 --- a/src/sakia/tests/unit/core/test_community.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -from unittest.mock import Mock -from pkg_resources import parse_version -from PyQt5.QtCore import QLocale -from sakia.core.net.api.bma.access import BmaAccess -from sakia.core.net.network import Network -from sakia.core import Community -from sakia.tests import QuamashTest - - -class TestCommunity(unittest.TestCase, QuamashTest): - def setUp(self): - self.setUpQuamash() - QLocale.setDefault(QLocale("en_GB")) - - def tearDown(self): - self.tearDownQuamash() - - def test_load_save_community(self): - network = Network("test_currency", [], Mock("aiohttp.ClientSession")) - bma_access = BmaAccess([], network) - community = Community("test_currency", network, bma_access) - - json_data = community.jsonify() - community_from_json = Community.load(json_data, parse_version('0.12.0')) - self.assertEqual(community.name, community_from_json.name) - self.assertEqual(len(community.network._nodes), len(community_from_json.network._nodes)) - community_from_json.network.session.close() - diff --git a/src/sakia/tests/unit/data/test_blockchains_repo.py b/src/sakia/tests/unit/data/test_blockchains_repo.py index 43c285098dfc3ceb45d3cfbf3a035887b653239f..edeb258d4b9259aa5bedbaa7f25ab2995b90bf5d 100644 --- a/src/sakia/tests/unit/data/test_blockchains_repo.py +++ b/src/sakia/tests/unit/data/test_blockchains_repo.py @@ -4,12 +4,14 @@ import unittest from duniterpy.documents import BlockUID from sakia.data.entities import Blockchain, BlockchainParameters -from sakia.data.repositories import BlockchainsRepo, MetaDatabase +from sakia.data.repositories import BlockchainsRepo, SakiaDatabase class TestBlockchainsRepo(unittest.TestCase): def setUp(self): - self.meta_repo = MetaDatabase.create(":memory:") + sqlite3.register_adapter(BlockUID, str) + self.meta_repo = SakiaDatabase(sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES), + None, None, None, None, None, None) self.meta_repo.prepare() self.meta_repo.upgrade_database() @@ -42,6 +44,7 @@ class TestBlockchainsRepo(unittest.TestCase): 86400, 100000, 0, + 86400, 999999, "testcurrency" )) @@ -99,6 +102,7 @@ class TestBlockchainsRepo(unittest.TestCase): 86400, 100000, 0, + 86400, 999999, "testcurrency" )) @@ -126,6 +130,7 @@ class TestBlockchainsRepo(unittest.TestCase): 86400, 100000, 0, + 86400, 999999, "testcurrency2" )) @@ -166,6 +171,7 @@ class TestBlockchainsRepo(unittest.TestCase): 86400, 100000, 0, + 86400, 999999, "testcurrency" ) diff --git a/src/sakia/tests/unit/data/test_certifications_repo.py b/src/sakia/tests/unit/data/test_certifications_repo.py index c71d06be23db53c90d32798a26f5a3c74560d85f..fdd79d9ec4d5d9501f5b7d7955e009a4a96b0f87 100644 --- a/src/sakia/tests/unit/data/test_certifications_repo.py +++ b/src/sakia/tests/unit/data/test_certifications_repo.py @@ -1,4 +1,4 @@ -from sakia.data.repositories import CertificationsRepo, MetaDatabase +from sakia.data.repositories import CertificationsRepo, SakiaDatabase from sakia.data.entities import Certification from duniterpy.documents import BlockUID import unittest @@ -7,7 +7,7 @@ import sqlite3 class TestCertificationsRepo(unittest.TestCase): def setUp(self): - self.meta_repo = MetaDatabase.create(":memory:") + self.meta_repo = SakiaDatabase(sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)) self.meta_repo.prepare() self.meta_repo.upgrade_database() diff --git a/src/sakia/tests/unit/data/test_connections_repo.py b/src/sakia/tests/unit/data/test_connections_repo.py index 777ba5da6ff6009b3a78afc54bdd5548997026f4..44994532d2d484c7d4cf7a44f159ed815cf78b6d 100644 --- a/src/sakia/tests/unit/data/test_connections_repo.py +++ b/src/sakia/tests/unit/data/test_connections_repo.py @@ -1,11 +1,14 @@ -from sakia.data.repositories import ConnectionsRepo, MetaDatabase +from sakia.data.repositories import ConnectionsRepo, SakiaDatabase from sakia.data.entities import Connection +from duniterpy.documents import BlockUID import unittest +import sqlite3 class TestConnectionsRepo(unittest.TestCase): def setUp(self): - self.meta_repo = MetaDatabase.create(":memory:") + sqlite3.register_adapter(BlockUID, str) + self.meta_repo = SakiaDatabase(sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)) self.meta_repo.prepare() self.meta_repo.upgrade_database() diff --git a/src/sakia/tests/unit/data/test_identies_repo.py b/src/sakia/tests/unit/data/test_identies_repo.py index 94ab7d58b5f0a6a837b04d56262c601423ec5fb7..1bcc4e2e89b77f702015da354fb90327e78ffdce 100644 --- a/src/sakia/tests/unit/data/test_identies_repo.py +++ b/src/sakia/tests/unit/data/test_identies_repo.py @@ -1,4 +1,4 @@ -from sakia.data.repositories import IdentitiesRepo, MetaDatabase +from sakia.data.repositories import IdentitiesRepo, SakiaDatabase from sakia.data.entities import Identity from duniterpy.documents import BlockUID import unittest @@ -7,7 +7,7 @@ import sqlite3 class TestIdentitiesRepo(unittest.TestCase): def setUp(self): - self.meta_repo = MetaDatabase.create(":memory:") + self.meta_repo = SakiaDatabase(sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)) self.meta_repo.prepare() self.meta_repo.upgrade_database() diff --git a/src/sakia/tests/unit/data/test_nodes_repo.py b/src/sakia/tests/unit/data/test_nodes_repo.py index 0875dbea7efc25b6635ca4594c14eb9adb8ad7e5..dabc0f71fc859bf652a4d13a01ea3849cbc8dd48 100644 --- a/src/sakia/tests/unit/data/test_nodes_repo.py +++ b/src/sakia/tests/unit/data/test_nodes_repo.py @@ -1,4 +1,4 @@ -from sakia.data.repositories import NodesRepo, MetaDatabase +from sakia.data.repositories import NodesRepo, SakiaDatabase from sakia.data.entities import Node from duniterpy.documents import BlockUID, BMAEndpoint, UnknownEndpoint, block_uid import unittest @@ -7,7 +7,7 @@ import sqlite3 class TestNodesRepo(unittest.TestCase): def setUp(self): - self.meta_repo = MetaDatabase.create(":memory:") + self.meta_repo = SakiaDatabase(sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)) self.meta_repo.prepare() self.meta_repo.upgrade_database() diff --git a/src/sakia/tests/unit/data/test_transactions_repo.py b/src/sakia/tests/unit/data/test_transactions_repo.py index 7b9a283fcfbdeaae6dbfafa05832612e6ce1c2fe..cc481ffe35608f3f038b65f5d323f6cee7a4465d 100644 --- a/src/sakia/tests/unit/data/test_transactions_repo.py +++ b/src/sakia/tests/unit/data/test_transactions_repo.py @@ -1,4 +1,4 @@ -from sakia.data.repositories import TransactionsRepo, MetaDatabase +from sakia.data.repositories import TransactionsRepo, SakiaDatabase from sakia.data.entities import Transaction from duniterpy.documents import BlockUID import unittest @@ -10,13 +10,13 @@ class TestTransactionsRepo(unittest.TestCase): sqlite3.register_adapter(BlockUID, str) sqlite3.register_adapter(bool, int) sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) - self.con = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES) + self.meta_repo = SakiaDatabase(sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)) def tearDown(self): self.con.close() def test_add_get_drop_transaction(self): - meta_repo = MetaDatabase(self.con) + meta_repo = SakiaDatabase(self.con) meta_repo.prepare() meta_repo.upgrade_database() transactions_repo = TransactionsRepo(self.con) @@ -53,7 +53,7 @@ class TestTransactionsRepo(unittest.TestCase): self.assertIsNone(transaction) def test_add_get_multiple_transaction(self): - meta_repo = MetaDatabase(self.con) + meta_repo = SakiaDatabase(self.con) meta_repo.prepare() meta_repo.upgrade_database() transactions_repo = TransactionsRepo(self.con) @@ -87,7 +87,7 @@ class TestTransactionsRepo(unittest.TestCase): self.assertIn("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", [t.issuer for t in transactions]) def test_add_update_transaction(self): - meta_repo = MetaDatabase(self.con) + meta_repo = SakiaDatabase(self.con) meta_repo.prepare() meta_repo.upgrade_database() transactions_repo = TransactionsRepo(self.con)