diff --git a/doc/uml/api.png b/doc/uml/api.png new file mode 100644 index 0000000000000000000000000000000000000000..a7e3336901352dc647ce916c06bc2eb880eae469 Binary files /dev/null and b/doc/uml/api.png differ diff --git a/doc/uml/api.pu b/doc/uml/api.pu new file mode 100644 index 0000000000000000000000000000000000000000..f4a6ca46ace9b1302ecc225a51a719b5e9698648 --- /dev/null +++ b/doc/uml/api.pu @@ -0,0 +1,21 @@ +@startuml + +package api { + package api.bma { + class BMADataAccess { + {static} _cache + {static} _request(req : Request, network) + {static} _post(req : Request, network) + {static} _broadcast(req : Request, network) + } + BMADataAccess ..> api.bma.API + } + package api.es { + class ESDataAccess { + } + ESDataAccess ..> api.es.API + } + +} + +@enduml \ No newline at end of file diff --git a/doc/uml/core-classes.png b/doc/uml/core-classes.png new file mode 100644 index 0000000000000000000000000000000000000000..000636eb518c8e441bb799c053286adc6ede60d0 Binary files /dev/null and b/doc/uml/core-classes.png differ diff --git a/doc/uml/core-classes.pu b/doc/uml/core-classes.pu new file mode 100644 index 0000000000000000000000000000000000000000..fc8d1e6d972241369d76c3d582a79dc3d6f28762 --- /dev/null +++ b/doc/uml/core-classes.pu @@ -0,0 +1,101 @@ +@startuml + +package core { + class App { + -- Signals -- + current_account_changed(str : account_name) + data_changed() + -- Slots -- + -- Properties -- + current_account + accounts + -- Methods -- + } + App --* Account : accounts + + class Account { + -- Signals -- + wallets_changed(int : nb_wallets) + community_added(int : index) + community_removed(int : index) + data_changed() + -- Slots -- + -- Properties -- + communities + wallets + -- Methods -- + } + Account "1" --* "*" Wallet + Account "1" --* "*" Community + + class Wallet { + -- Signals -- + money_received(Transfer) + money_sent(Transfer) + name_changed(str : new_name + data_changed() + -- Slots -- + -- Properties -- + transfers + -- Methods -- + } + Wallet "1" --* "*" Transfer + + class Transfer { + -- Signals -- + state_changed(int : new_state) + -- Slots -- + -- Properties -- + -- Methods -- + } + + class Community { + -- Signals -- + members_changed() + data_changed() + -- Slots -- + -- Properties -- + network + -- Methods -- + + } + App --> Identity + class Identity { + {static} _identities + {static} load(data : dict) + {static} lookup(search : str) + } + +} + + +package net { + class Network { + -- Signals -- + node_found(int : index) + node_removed(int : index) + block_found(int : block_number) + -- Slots -- + -- Properties -- + nodes + root_nodes + -- Methods -- + } + Community "1" --* "1" Network + Network "1" --* "*" Node + + class Node { + -- Signals -- + changed() + -- Slots -- + -- Properties -- + endpoints + pubkey + uid + block + state + -- Methods -- + } + +} +@enduml \ No newline at end of file diff --git a/doc/uml/cutecoin.png b/doc/uml/cutecoin.png new file mode 100644 index 0000000000000000000000000000000000000000..59780e445fce620ad5f355c3d3f8ae1a95c92025 Binary files /dev/null and b/doc/uml/cutecoin.png differ diff --git a/doc/uml/cutecoin.pu b/doc/uml/cutecoin.pu new file mode 100644 index 0000000000000000000000000000000000000000..d79f67dfb4614935631fd76663cdd922aac6b841 --- /dev/null +++ b/doc/uml/cutecoin.pu @@ -0,0 +1,36 @@ +@startuml + +!include core-classes.pu +!include gui-classes.pu +!include models-classes.pu +!include api.pu + +MainWindow "1" --> "1" App + +CertificationDialog --> Community +TransferDialog --> Community + +CurrencyTab "1" --> "1" Community + +CommunityTab -right-> IdentitiesFilterProxyModel +NetworkTab -right-> NetworkFilterProxyModel +WalletTab -right-> WalletsFilterProxyModel + +WalletsFilterProxyModel -up-> Wallet +NetworkFilterProxyModel -up-> Network +TxHistoryFilterProxyModel -up-> Transfer + +ConfigureAccountDialog --> CommunitiesListModel +ConfigureCommunityDialog --> RootNodesTableModel + +ConfigureAccountDialog --> Account +ConfigureCommunityDialog --> Community + +Account ..> BMADataAccess +Community ..> BMADataAccess +Wallet ..> BMADataAccess +Transfer ..> BMADataAccess +Identity ..> BMADataAccess +BMADataAccess .left.> Network + +@enduml \ No newline at end of file diff --git a/doc/uml/gui-classes.png b/doc/uml/gui-classes.png new file mode 100644 index 0000000000000000000000000000000000000000..7df50d51d4ae74fb4f99c2b47eac4b31cc633b15 Binary files /dev/null and b/doc/uml/gui-classes.png differ diff --git a/doc/uml/gui-classes.pu b/doc/uml/gui-classes.pu new file mode 100644 index 0000000000000000000000000000000000000000..f8a0bc964fc2dd6a5714d56dab6da971bdd5e2cd --- /dev/null +++ b/doc/uml/gui-classes.pu @@ -0,0 +1,58 @@ +@startuml + + +package gui { + class MainWindow { + } + MainWindow "1" --* "*" CurrencyTab + + class CurrencyTab { + } + CurrencyTab "1" --* "1" CommunityTab + CurrencyTab "1" --* "1" WalletTab + CurrencyTab "1" --* "1" InformationsTab + CurrencyTab "1" --* "1" TransactionsTab + + class CommunityTab { + } + + CommunityTab "1" --* "1" IdentitiesTab + CommunityTab "1" --* "1" WotTab + + class WalletTab { + } + + class InformationsTab { + } + + class TransactionsTab { + } + + class NetworkTab { + } + + CurrencyTab "1" --* "1" NetworkTab + + class IdentitiesTab { + } + + class WotTab { + } + package dialogs { + class CertificationDialog + class TransferDialog + class ContactDialog + class ConfigureAccountDialog + class ConfigureCommunityDialog + } + + MainWindow --> CertificationDialog + MainWindow --> TransferDialog + MainWindow --> ContactDialog + MainWindow --> ConfigureAccountDialog + ConfigureAccountDialog --> ConfigureCommunityDialog + + class Wot + WotTab --> Wot +} +@enduml \ No newline at end of file diff --git a/doc/uml/models-classes.png b/doc/uml/models-classes.png new file mode 100644 index 0000000000000000000000000000000000000000..54fec65a2ab178127054ae952f4b55b7e0afbc52 Binary files /dev/null and b/doc/uml/models-classes.png differ diff --git a/doc/uml/models-classes.pu b/doc/uml/models-classes.pu new file mode 100644 index 0000000000000000000000000000000000000000..1650739d5d4c65bfa48ba299c543b4d019cde42b --- /dev/null +++ b/doc/uml/models-classes.pu @@ -0,0 +1,39 @@ +@startuml + +package models { + class WalletsFilterProxyModel { + } + + WalletsFilterProxyModel --> WalletsTableModel : source + + class WalletsTableModel { + } + + class IdentitiesFilterProxyModel { + } + IdentitiesFilterProxyModel --> IdentitiesTableModel : source + + class IdentitiesTableModel { + } + + class NetworkFilterProxyModel { + } + NetworkFilterProxyModel --> NetworkTableModel : source + + class NetworkTableModel { + } + + class TxHistoryFilterProxyModel { + } + TxHistoryFilterProxyModel --> TxHistoryTableModel : source + class TxHistoryTableModel { + } + + class CommunitiesListModel { + } + + class RootNodesTableModel { + } +} + +@enduml \ No newline at end of file diff --git a/doc/uml/network.png b/doc/uml/network.png new file mode 100644 index 0000000000000000000000000000000000000000..853e39c21f10c5fabcb74d702194ce1822209727 Binary files /dev/null and b/doc/uml/network.png differ diff --git a/doc/uml/network.pu b/doc/uml/network.pu new file mode 100644 index 0000000000000000000000000000000000000000..d89859f85bb60f8291a322f6bab241ccc3cd0a02 --- /dev/null +++ b/doc/uml/network.pu @@ -0,0 +1,36 @@ +@startuml + +Network -->o Node : Connect to node_received() +Network -> Node : Starts network discovery +activate Node +Node -> QNetworkManager : HTTP GET peering/peers?leaves=true +create QNetworkReply +QNetworkManager -> QNetworkReply : Instantiate +Node <- QNetworkManager : QNetworkReply +Node -->o QNetworkReply : Connect to finished() +Network <- Node +deactivate Node +... Request is processed ... +Node <-- QNetworkReply : finished() +destroy QNetworkReply +alt "root" hash changed +loop "for all leaves changed" +activate Node +Node -> QNetworkManager : HTTP GET peering/peers/leaf=leaf_hash +create QNetworkReply +QNetworkManager -> QNetworkReply : Instantiate +Node <- QNetworkManager : QNetworkReply +Node -->o QNetworkReply : Connect to finished() +end +end +... Requests is processed ... +Node <-- QNetworkReply : finished() +destroy QNetworkReply +Network <-- Node : node_received() +ref over Network +New node is instanciated +if pubkey not known yet. +It starts it's own +network discovery +end ref +@enduml \ No newline at end of file diff --git a/doc/uml/requests.png b/doc/uml/requests.png new file mode 100644 index 0000000000000000000000000000000000000000..d629a6789802e9eadb3f092a198497c02b4ddcc2 Binary files /dev/null and b/doc/uml/requests.png differ diff --git a/doc/uml/requests.pu b/doc/uml/requests.pu new file mode 100644 index 0000000000000000000000000000000000000000..8ea91136f68aba9771cf9f438f89790f508b4341 --- /dev/null +++ b/doc/uml/requests.pu @@ -0,0 +1,50 @@ +@startuml + +QModel -->o "Core Component" : Connect to data_changed() +QModel -> "Core Component" : Data access +activate "Core Component" +"Core Component" -> Community : Request data +Community -> Cache : Request cache +ref over Cache +Data is obsolete +(new block mined +since last caching) +end ref +Cache -> QNetworkManager : HTTP GET +create QNetworkReply +QNetworkManager -> QNetworkReply : Instantiate +Cache <- QNetworkManager : QNetworkReply +create ReceiverSlot +Cache -> ReceiverSlot : Instantiate Slot +QNetworkReply o<-- ReceiverSlot : Connect to finished() +Community <- Cache : Cached data +"Core Component" <- Community : Cached data +"Core Component" -> "Core Component" : Compute data +QModel <- "Core Component" : Data +deactivate "Core Component" + +...Network request is processed... + +ReceiverSlot <-- QNetworkReply : finished() +activate ReceiverSlot +ReceiverSlot -> Cache : Update cache data +ReceiverSlot -> "Core Component" : emit data_changed() +deactivate ReceiverSlot +destroy ReceiverSlot +destroy QNetworkReply +||| +QModel <-- "Core Component" : data_changed() +QModel -> "Core Component" : Data access +activate "Core Component" +ref over "Core Component", Community +Community is requested again, +and last cached data are returned +No new block mined, so no HTTP GET +initialized between cache +and QNetworkManager +end ref +QModel <- "Core Component" : Data +deactivate "Core Component" + + +@enduml \ No newline at end of file diff --git a/doc/uml/tx_lifecycle.png b/doc/uml/tx_lifecycle.png index 0e7061b3438870f7c6a7c4fae11a191f971e9234..ff35f12c2605e32c5e03428e10e77b0ddfb9e1e1 100644 Binary files a/doc/uml/tx_lifecycle.png and b/doc/uml/tx_lifecycle.png differ diff --git a/lib/ucoinpy/documents/block.py b/lib/ucoinpy/documents/block.py index 58a1b3bf8e9d29543fbd65b956b9bc144d0b2f04..dd17cb2f7e52f772c422eb4b93a3618943ddb29a 100644 --- a/lib/ucoinpy/documents/block.py +++ b/lib/ucoinpy/documents/block.py @@ -293,4 +293,4 @@ PreviousIssuer: {1}\n".format(self.prev_hash, self.prev_issuer) for transaction in self.transactions: doc += "{0}\n".format(transaction.compact()) - return doc + return doc \ No newline at end of file diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py index 165adb6419c96281bc188e42e749855b7e52077b..0471dea12bd4475b3925703eceda7ddeeaeb5ade 100644 --- a/src/cutecoin/core/account.py +++ b/src/cutecoin/core/account.py @@ -41,14 +41,21 @@ def relative(units, community): :param cutecoin.core.community.Community community: Community instance :return: float """ - # fixme: the value "community.nb_members" is not up to date, luckyly the good value is in "community.get_ud_block()['membersCount']" # calculate ud(t+1) - ud = math.ceil( - max(community.dividend, - community.parameters['c'] * community.monetary_mass / community.get_ud_block()['membersCount']) - ) - relative_value = units / float(ud) - return relative_value + ud_block = community.get_ud_block() + if ud_block: + ud = math.ceil( + max(community.dividend(), + float(0) if ud_block['membersCount'] == 0 else + community.parameters['c'] * community.monetary_mass / ud_block['membersCount'])) + + if ud == 0: + return float(0) + else: + relative_value = units / float(ud) + return relative_value + else: + return float(0) def quantitative_zerosum(units, community): @@ -111,6 +118,8 @@ class Account(QObject): ) loading_progressed = pyqtSignal(int, int) + inner_data_changed = pyqtSignal() + wallets_changed = pyqtSignal() def __init__(self, salt, pubkey, name, communities, wallets, contacts): ''' @@ -153,9 +162,10 @@ class Account(QObject): return account @classmethod - def load(cls, json_data): + def load(cls, network_manager, json_data): ''' Factory method to create an Account object from its json view. + :rtype : cutecoin.core.account.Account :param dict json_data: The account view as a json dict :return: A new account object created from the json datas ''' @@ -174,7 +184,7 @@ class Account(QObject): communities = [] for data in json_data['communities']: - community = Community.load(data) + community = Community.load(network_manager, data) communities.append(community) account = cls(salt, pubkey, name, communities, wallets, @@ -282,6 +292,7 @@ class Account(QObject): self.wallets.append(wallet) else: self.wallets = self.wallets[:size] + self.wallets_changed.emit() def certify(self, password, community, pubkey): """ diff --git a/src/cutecoin/core/app.py b/src/cutecoin/core/app.py index ca266b27eb0dc05e0d6f04c784917fe53e29826d..3a5e5ad60b153ea6724ce6fc66c24bd628c0733d 100644 --- a/src/cutecoin/core/app.py +++ b/src/cutecoin/core/app.py @@ -19,7 +19,6 @@ from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkReques from . import config from .account import Account from . import person -from .watching.monitor import Monitor from .. import __version__ from ..tools.exceptions import NameAlreadyExists, BadAccountFile @@ -44,13 +43,11 @@ class Application(QObject): super().__init__() self.accounts = {} self.current_account = None - self.monitor = None self.available_version = (True, __version__, "") config.parse_arguments(argv) self._network_manager = QNetworkAccessManager() - self._network_manager.finished.connect(self.read_available_version) self.preferences = {'account': "", 'lang': 'en_GB', 'ref': 0 @@ -126,13 +123,9 @@ class Application(QObject): self.loading_progressed.emit(value, maximum) if self.current_account is not None: - if self.monitor: - self.monitor.stop_watching() self.save_cache(self.current_account) account.loading_progressed.connect(progressing) account.refresh_cache() - self.monitor = Monitor(account) - self.monitor.prepare_watching() self.current_account = account def load(self): @@ -178,7 +171,7 @@ class Application(QObject): account_name, 'properties') with open(account_path, 'r') as json_data: data = json.load(json_data) - account = Account.load(data) + account = Account.load(self._network_manager, data) self.load_cache(account) self.accounts[account_name] = account @@ -189,9 +182,9 @@ class Application(QObject): :param account: The account object to load the cache ''' for community in account.communities: - community_path = os.path.join(config.parameters['home'], + bma_path = os.path.join(config.parameters['home'], account.name, '__cache__', - community.currency) + community.currency + '_bma') network_path = os.path.join(config.parameters['home'], account.name, '__cache__', @@ -202,17 +195,17 @@ class Application(QObject): data = json.load(json_data) if 'version' in data and data['version'] == __version__: logging.debug("Merging network : {0}".format(data)) - community.load_merge_network(data['network']) + community.network.merge_with_json(data['network']) else: os.remove(network_path) - if os.path.exists(community_path): - with open(community_path, 'r') as json_data: + if os.path.exists(bma_path): + with open(bma_path, 'r') as json_data: data = json.load(json_data) if 'version' in data and data['version'] == __version__: - community.load_cache(data) + community.bma_access.load_from_json(data['cache']) else: - os.remove(community_path) + os.remove(bma_path) for wallet in account.wallets: wallet_path = os.path.join(config.parameters['home'], @@ -319,9 +312,9 @@ class Application(QObject): self.save_wallet(account, wallet) for community in account.communities: - community_path = os.path.join(config.parameters['home'], + bma_path = os.path.join(config.parameters['home'], account.name, '__cache__', - community.currency) + community.currency + '_bma') network_path = os.path.join(config.parameters['home'], account.name, '__cache__', @@ -329,12 +322,12 @@ class Application(QObject): with open(network_path, 'w') as outfile: data = dict() - data['network'] = community.jsonify_network() + data['network'] = community.network.jsonify() data['version'] = __version__ json.dump(data, outfile, indent=4, sort_keys=True) - with open(community_path, 'w') as outfile: - data = community.jsonify_cache() + with open(bma_path, 'w') as outfile: + data['cache'] = community.bma_access.jsonify() data['version'] = __version__ json.dump(data, outfile, indent=4, sort_keys=True) @@ -402,11 +395,13 @@ class Application(QObject): def get_last_version(self): url = QUrl("https://api.github.com/repos/ucoin-io/cutecoin/releases") request = QNetworkRequest(url) - self._network_manager.get(request) + reply = self._network_manager.get(request) + reply.finished.connect(self.read_available_version) @pyqtSlot(QNetworkReply) - def read_available_version(self, reply): + def read_available_version(self): latest = None + reply = self.sender() releases = reply.readAll().data().decode('utf-8') logging.debug(releases) if reply.error() == QNetworkReply.NoError: diff --git a/src/cutecoin/core/community.py b/src/cutecoin/core/community.py index be5e70adf47c298e1c9ad62eafcad290fb0669ef..3857d6a2e94b1653d50fe9a6e2f871dc7553c50a 100644 --- a/src/cutecoin/core/community.py +++ b/src/cutecoin/core/community.py @@ -4,200 +4,104 @@ Created on 1 févr. 2014 @author: inso ''' -from PyQt5.QtCore import QObject, pyqtSignal -from ucoinpy.api import bma -from ucoinpy.documents.block import Block -from ..tools.exceptions import NoPeerAvailable -from .net.node import Node -from .net.network import Network import logging -import inspect import hashlib import re import time -from requests.exceptions import RequestException - - -class Cache(): - _saved_requests = [str(bma.blockchain.Block), str(bma.blockchain.Parameters)] - - def __init__(self, community): - ''' - Init an empty cache - ''' - self.latest_block = 0 - self.community = community - self.data = {} - - def load_from_json(self, data): - ''' - Put data in the cache from json datas. - - :param dict data: The cache in json format - ''' - self.data = {} - for entry in data['cache']: - key = entry['key'] - cache_key = (key[0], key[1], key[2], key[3], key[4]) - self.data[cache_key] = entry['value'] - self.latest_block = data['latest_block'] - - def jsonify(self): - ''' - Get the cache in json format - - :return: The cache as a dict in json format - ''' - data = {k: self.data[k] for k in self.data.keys()} - entries = [] - for d in data: - entries.append({'key': d, - 'value': data[d]}) - return {'latest_block': self.latest_block, - 'cache': entries} - - def refresh(self): - ''' - Refreshing the cache just clears every last requests which - cannot be saved because they can change from one block to another. - ''' - logging.debug("Refresh : {0}/{1}".format(self.latest_block, - self.community.network.latest_block)) - if self.latest_block < self.community.network.latest_block: - self.latest_block = self.community.network.latest_block - self.data = {k: self.data[k] for k in self.data.keys() - if k[0] in Cache._saved_requests} - - def request(self, request, req_args={}, get_args={}): - ''' - Send a cached request to a community. - If the request was already sent in current block, return last value get. +from PyQt5.QtCore import QObject, pyqtSignal +from requests.exceptions import RequestException - :param request: The request bma class - :param req_args: The arguments passed to the request constructor - :param get_args: The arguments passed to the requests __get__ method - ''' - cache_key = (str(request), - str(tuple(frozenset(sorted(req_args.keys())))), - str(tuple(frozenset(sorted(req_args.values())))), - str(tuple(frozenset(sorted(get_args.keys())))), - str(tuple(frozenset(sorted(get_args.values()))))) - - if cache_key not in self.data.keys(): - result = self.community.request(request, req_args, get_args, - cached=False) - # For block 0, we should have a different behaviour - # Community members and certifications - # Should be requested without caching - self.data[cache_key] = result - return self.data[cache_key] - else: - return self.data[cache_key] +from ucoinpy.documents.block import Block +from ..tools.exceptions import NoPeerAvailable +from .net.network import Network +from .net.api import bma as qtbma +from .net.api.bma.access import BmaAccess class Community(QObject): - ''' + """ A community is a group of nodes using the same currency. .. warning:: The currency name is supposed to be unique in cutecoin but nothing exists in ucoin to assert that a currency name is unique. - ''' + """ + inner_data_changed = pyqtSignal(int) - def __init__(self, currency, network): - ''' + def __init__(self, currency, network, bma_access): + """ Initialize community attributes with a currency and a network. :param str currency: The currency name of the community. - :param network: The network of the community + :param cutecoin.core.net.network.Network network: The network of the community + :param cutecoin.core.net.api.bma.access.BmaAccess bma_access: The BMA Access object .. warning:: The community object should be created using its factory class methods. - ''' + """ super().__init__() self.currency = currency self._network = network - self._cache = Cache(self) - self._cache.refresh() + self._bma_access = bma_access @classmethod def create(cls, node): - ''' + """ Create a community from its first node. :param node: The first Node of the community - ''' + """ network = Network.create(node) - community = cls(node.currency, network) + bma_access = BmaAccess.create(network) + community = cls(node.currency, network, bma_access) logging.debug("Creating community") return community @classmethod - def load(cls, json_data): - ''' + def load(cls, network_manager, json_data): + """ Load a community from json :param dict json_data: The community as a dict in json format - ''' + """ currency = json_data['currency'] - network = Network.from_json(currency, json_data['peers']) - community = cls(currency, network) + network = Network.from_json(network_manager, currency, json_data['peers']) + bma_access = BmaAccess.create(network) + community = cls(currency, network, bma_access) return community - def load_merge_network(self, json_data): - ''' - Load the last state of the network. - This network is merged with the network currently - known network. - - :param dict json_data: - ''' - self._network.merge_with_json(json_data) - - def load_cache(self, json_data): - ''' + def load_cache(self, bma_access_cache, network_cache): + """ Load the community cache. - :param dict json_data: The community as a dict in json format. - ''' - self._cache.load_from_json(json_data) - - def jsonify_cache(self): - ''' - Get the cache jsonified. - - :return: The cache as a dict in json format. - ''' - return self._cache.jsonify() - - def jsonify_network(self): - ''' - Get the network jsonified. - ''' - return self._network.jsonify() + :param dict bma_access_cache: The BmaAccess cache in json + :param dict network_cache: The network cache in json + """ + self._bma_access.load_from_json(bma_access_cache) + self._network.merge_with_json(network_cache) @property def name(self): - ''' + """ The community name is its currency name. :return: The community name - ''' + """ return self.currency def __eq__(self, other): - ''' + """ :return: True if this community has the same currency name as the other one. - ''' + """ return (other.currency == self.currency) @property def short_currency(self): - ''' + """ Format the currency name to a short one :return: The currency name in a shot format. - ''' + """ words = re.split('[_\W]+', self.currency) shortened = "" if len(words) > 1: @@ -210,22 +114,22 @@ class Community(QObject): @property def currency_symbol(self): - ''' + """ Format the currency name to a symbol one. :return: The currency name as a utf-8 circled symbol. - ''' + """ letter = self.currency[0] u = ord('\u24B6') + ord(letter) - ord('A') return chr(u) - @property + #@property def dividend(self): - ''' + """ Get the last generated community universal dividend. :return: The last UD or 1 if no UD was generated. - ''' + """ block = self.get_ud_block() if block: return block['dividend'] @@ -233,32 +137,32 @@ class Community(QObject): return 1 def get_ud_block(self, x=0): - ''' + """ Get a block with universal dividend :param int x: Get the 'x' older block with UD in it :return: The last block with universal dividend. - ''' - blocks = self.request(bma.blockchain.UD)['result']['blocks'] + """ + blocks = self.bma_access.get(self, qtbma.blockchain.UD)['result']['blocks'] if len(blocks) > 0: block_number = blocks[len(blocks)-(1+x)] - block = self.request(bma.blockchain.Block, + block = self.bma_access.get(self, qtbma.blockchain.Block, req_args={'number': block_number}) return block else: - return False + return None @property def monetary_mass(self): - ''' + """ Get the community monetary mass :return: The monetary mass value - ''' + """ try: # Get cached block by block number block_number = self.network.latest_block - block = self.request(bma.blockchain.Block, + block = self.bma_access.get(self, qtbma.blockchain.Block, req_args={'number': block_number}) return block['monetaryMass'] except ValueError as e: @@ -269,15 +173,15 @@ class Community(QObject): @property def nb_members(self): - ''' + """ Get the community members number :return: The community members number - ''' + """ try: # Get cached block by block number block_number = self.network.latest_block - block = self.request(bma.blockchain.Block, + block = self.bma_access.get(qtbma.blockchain.Block, req_args={'number': block_number}) return block['membersCount'] except ValueError as e: @@ -288,29 +192,30 @@ class Community(QObject): @property def network(self): - ''' + """ Get the community network instance. :return: The community network instance. - ''' + :rtype: cutecoin.core.net.network.Network + """ return self._network - def network_quality(self): - ''' - Get a ratio of the synced nodes vs the rest - ''' - synced = len(self._network.synced_nodes) - #online = len(self._network.online_nodes) - total = len(self._network.nodes) - ratio_synced = synced / total - return ratio_synced + @property + def bma_access(self): + """ + Get the community bma_access instance + + :return: The community bma_access instace + :rtype: cutecoin.core.net.api.bma.access.BmaAccess + """ + return self._bma_access @property def parameters(self): - ''' + """ Return community parameters in bma format - ''' - return self.request(bma.blockchain.Parameters) + """ + return self.bma_access.get(self, qtbma.blockchain.Parameters) def certification_expired(self, certtime): ''' @@ -341,14 +246,12 @@ class Community(QObject): :param int number: The block number. If none, returns current block. ''' if number is None: - data = self.request(bma.blockchain.Current) + data = self.bma_access.get(self, qtbma.blockchain.Current) else: logging.debug("Requesting block {0}".format(number)) - data = self.request(bma.blockchain.Block, + data = self.bma_access.get(self, qtbma.blockchain.Block, req_args={'number': number}) - - return Block.from_signed_raw("{0}{1}\n".format(data['raw'], - data['signature'])) + return data def current_blockid(self): ''' @@ -357,7 +260,7 @@ class Community(QObject): :return: The current block ID as [NUMBER-HASH] format. ''' try: - block = self.request(bma.blockchain.Current, cached=False) + block = self.bma_access.get(self, qtbma.blockchain.Current, cached=False) signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) block_hash = hashlib.sha1(signed_raw.encode("ascii")).hexdigest().upper() block_number = block['number'] @@ -375,7 +278,7 @@ class Community(QObject): :return: All members pubkeys. ''' - memberships = self.request(bma.wot.Members) + memberships = self.bma_access.get(self, qtbma.wot.Members) return [m['pubkey'] for m in memberships["results"]] def refresh_cache(self): @@ -384,42 +287,6 @@ class Community(QObject): ''' self._cache.refresh() - def request(self, request, req_args={}, get_args={}, cached=True): - ''' - Start a request to the community. - - :param request: A ucoinpy bma request class - :param req_args: Arguments to pass to the request constructor - :param get_args: Arguments to pass to the request __get__ method - :return: The returned data - ''' - if cached: - return self._cache.request(request, req_args, get_args) - else: - nodes = self._network.synced_nodes - for node in nodes: - try: - req = request(node.endpoint.conn_handler(), **req_args) - data = req.get(**get_args) - - if inspect.isgenerator(data): - generated = [] - for d in data: - generated.append(d) - return generated - else: - return data - except ValueError as e: - if '502' in str(e): - continue - else: - raise - except RequestException as e: - logging.debug("Error : {1} : {0}".format(str(e), - str(request))) - continue - raise NoPeerAvailable(self.currency, len(nodes)) - def post(self, request, req_args={}, post_args={}): ''' Post data to a community. diff --git a/src/cutecoin/core/net/__init__.py b/src/cutecoin/core/net/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..39ab2a0b56350baad834cb7fb0cfecb8223e1fcd 100644 --- a/src/cutecoin/core/net/__init__.py +++ b/src/cutecoin/core/net/__init__.py @@ -0,0 +1 @@ +__author__ = 'inso' diff --git a/src/cutecoin/core/watching/__init__.py b/src/cutecoin/core/net/api/__init__.py similarity index 100% rename from src/cutecoin/core/watching/__init__.py rename to src/cutecoin/core/net/api/__init__.py diff --git a/src/cutecoin/core/net/api/bma/__init__.py b/src/cutecoin/core/net/api/bma/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ce3f0b46dbb87fa4204dba32cf7df8764bbe5b03 --- /dev/null +++ b/src/cutecoin/core/net/api/bma/__init__.py @@ -0,0 +1,128 @@ + + +__all__ = ['api'] + +from PyQt5.QtNetwork import QNetworkRequest +from PyQt5.QtCore import QUrl, QUrlQuery +import logging + +logger = logging.getLogger("ucoin") + + +class ConnectionHandler(object): + """Helper class used by other API classes to ease passing server connection information.""" + + def __init__(self, network_manager, server, port): + """ + Arguments: + - `server`: server hostname + - `port`: port number + """ + + self.network_manager = network_manager + self.server = server + self.port = port + + def __str__(self): + return 'connection info: %s:%d' % (self.server, self.port) + + +class API(object): + """APIRequest is a class used as an interface. The intermediate derivated classes are the modules and the leaf classes are the API requests.""" + + def __init__(self, conn_handler, module): + """ + Asks a module in order to create the url used then by derivated classes. + + Arguments: + - `module`: module name + - `connection_handler`: connection handler + """ + + self.module = module + self.conn_handler = conn_handler + self.headers = {} + + def reverse_url(self, path): + """ + Reverses the url using self.url and path given in parameter. + + Arguments: + - `path`: the request path + """ + + server, port = self.conn_handler.server, self.conn_handler.port + + url = 'http://%s:%d/%s' % (server, port, self.module) + return url + path + + def get(self, **kwargs): + """wrapper of overloaded __get__ method.""" + + return self.__get__(**kwargs) + + def post(self, **kwargs): + """wrapper of overloaded __post__ method.""" + + logger.debug('do some work with') + + data = self.__post__(**kwargs) + + logger.debug('and send back') + + return data + + def __get__(self, **kwargs): + """interface purpose for GET request""" + + pass + + def __post__(self, **kwargs): + """interface purpose for POST request""" + + pass + + def requests_get(self, path, **kwargs): + """ + Requests GET wrapper in order to use API parameters. + + Arguments: + - `path`: the request path + """ + query = QUrlQuery() + for k,v in kwargs.items(): + query.addQueryItem(k, v); + url = QUrl(self.reverse_url(path)) + url.setQuery(query) + request = QNetworkRequest(url) + logging.debug(url.toString()) + reply = self.conn_handler.network_manager.get(request) + + return reply + + def requests_post(self, path, **kwargs): + """ + Requests POST wrapper in order to use API parameters. + + Arguments: + - `path`: the request path + """ + if 'self_' in kwargs: + kwargs['self'] = kwargs.pop('self_') + + logging.debug("POST : {0}".format(kwargs)) + post_data = QUrlQuery() + for k,v in kwargs.items(): + post_data.addQueryItem(k, v) + url = QUrl(self.reverse_url(path)) + url.setQuery(post_data) + + request = QNetworkRequest(url) + request.setHeader(QNetworkRequest.ContentTypeHeader, + "application/x-www-form-urlencoded") + reply = self.conn_handler.network_manager.post(request, + post_data.toString(QUrl.FullyEncoded).toUtf8()) + + return reply + +from . import network, blockchain, tx, wot diff --git a/src/cutecoin/core/net/api/bma/access.py b/src/cutecoin/core/net/api/bma/access.py new file mode 100644 index 0000000000000000000000000000000000000000..4c7fc64aec18dc62a5f43aee2678077c979ed3af --- /dev/null +++ b/src/cutecoin/core/net/api/bma/access.py @@ -0,0 +1,153 @@ +from PyQt5.QtCore import QObject, pyqtSlot +from PyQt5.QtNetwork import QNetworkReply +from . import blockchain, network, node, tx, wot, ConnectionHandler +from .....tools.exceptions import NoPeerAvailable +import logging +import json +import random + +class BmaAccess(QObject): + ''' + This class is used to access BMA API. + ''' + + __saved_requests = [str(blockchain.Block), str(blockchain.Parameters)] + + def __init__(self, data, network): + """ + Constructor of a network + + :param dict data: The data present in this cache + :param cutecoin.core.net.network.Network network: The network used to connect + """ + super().__init__() + self._data = data + self._network = network + + @classmethod + def create(cls, network): + ''' + Initialize a new BMAAccess object with empty data. + + :param cutecoin.core.net.network.Network network: + :return: A new BmaAccess object + :rtype: cutecoin.core.net.api.bma.access.BmaAccess + ''' + return cls({}, network) + + @property + def data(self): + return self._data.copy() + + def load_from_json(self, json_data): + ''' + Put data in the cache from json datas. + + :param dict data: The cache in json format + ''' + data = {} + for entry in json_data: + key = entry['key'] + cache_key = (key[0], key[1], key[2], key[3], key[4]) + data[cache_key] = entry['value'] + self._data = data + + def jsonify(self): + ''' + Get the cache in json format + + :return: The cache as a dict in json format + ''' + data = {k: self._data[k] for k in self._data.keys()} + entries = [] + for d in data: + entries.append({'key': d, + 'value': data[d]}) + return entries + + def get(self, caller, request, req_args={}, get_args={}): + """ + Get Json data from the specified URL + :rtype : dict + """ + cache_key = (str(request), + str(tuple(frozenset(sorted(req_args.keys())))), + str(tuple(frozenset(sorted(req_args.values())))), + str(tuple(frozenset(sorted(get_args.keys())))), + str(tuple(frozenset(sorted(get_args.values()))))) + + if cache_key in self._data.keys(): + need_reload = False + if 'metadata' in self._data[cache_key]: + if str(request) not in BmaAccess.__saved_requests \ + and self._data[cache_key]['metadata']['block'] < self._network.latest_block: + need_reload = True + else: + need_reload = True + ret_data = self._data[cache_key]['value'] + else: + need_reload = True + ret_data = request.null_value + + if need_reload: + #Move to network nstead of community + #after removing qthreads + reply = self.request(request, req_args, get_args) + reply.finished.connect(lambda: + self.handle_reply(caller, request, req_args, get_args)) + return ret_data + + def request(self, request, req_args={}, get_args={}): + ''' + Start a request to the network. + + :param request: A bma request class calling for data + :param req_args: Arguments to pass to the request constructor + :param get_args: Arguments to pass to the request __get__ method + :return: The returned data if cached = True else return the QNetworkReply + ''' + nodes = self._network.synced_nodes + if len(nodes) > 0: + node = random.choice(nodes) + server = node.endpoint.conn_handler().server + port = node.endpoint.conn_handler().port + conn_handler = ConnectionHandler(self._network.network_manager, server, port) + req = request(conn_handler, **req_args) + reply = req.get(**get_args) + return reply + else: + raise NoPeerAvailable(self.currency, len(nodes)) + + @pyqtSlot(int, dict, dict, QObject) + def handle_reply(self, caller, request, req_args, get_args): + reply = self.sender() + #logging.debug("Handling QtNetworkReply for {0}".format(str(request))) + if reply.error() == QNetworkReply.NoError: + cache_key = (str(request), + str(tuple(frozenset(sorted(req_args.keys())))), + str(tuple(frozenset(sorted(req_args.values())))), + str(tuple(frozenset(sorted(get_args.keys())))), + str(tuple(frozenset(sorted(get_args.values()))))) + strdata = bytes(reply.readAll()).decode('utf-8') + #logging.debug("Data in reply : {0}".format(strdata)) + + if cache_key not in self._data: + self._data[cache_key] = {} + + if 'metadata' not in self._data[cache_key]: + self._data[cache_key]['metadata'] = {} + self._data[cache_key]['metadata']['block'] = self._network.latest_block + + change = False + if 'value' in self._data[cache_key]: + if self._data[cache_key]['value'] != json.loads(strdata): + change = True + else: + change = True + + if change == True: + self._data[cache_key]['value'] = json.loads(strdata) + caller.inner_data_changed.emit(request) + else: + logging.debug("Error in reply : {0}".format(reply.error())) + self.community.qtrequest(caller, request, req_args, get_args) diff --git a/src/cutecoin/core/net/api/bma/blockchain/__init__.py b/src/cutecoin/core/net/api/bma/blockchain/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6952dc6d8232fb31eb6cb376dcdb790febd83b35 --- /dev/null +++ b/src/cutecoin/core/net/api/bma/blockchain/__init__.py @@ -0,0 +1,232 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/blockchain") + + +class Blockchain(API): + def __init__(self, conn_handler, module='blockchain'): + super(Blockchain, self).__init__(conn_handler, module) + + +class Parameters(Blockchain): + """GET the blockchain parameters used by this node.""" + + def __get__(self, **kwargs): + return self.requests_get('/parameters', **kwargs) + + null_value = { + 'currency': "", + 'c': 0, + 'dt': 0, + 'ud0': 0, + 'sigDelay': 0, + 'sigValidity': 0, + 'sigQty': 0, + 'sigWoT': 0, + 'msValidity': 0, + 'stepMax': 0, + 'medianTimeBlocks': 0, + 'avgGenTime': 0, + 'dtDiffEval': 0, + 'blocksRot': 0, + 'percentRot': 0 + } + + +class Membership(Blockchain): + """GET/POST a Membership document.""" + def __init__(self, conn_handler, search=None): + super().__init__(conn_handler) + self.search = search + + def __post__(self, **kwargs): + assert 'membership' in kwargs + + return self.requests_post('/membership', **kwargs) + + def __get__(self, **kwargs): + assert self.search is not None + return self.requests_get('/memberships/%s' % self.search, **kwargs) + + +class Block(Blockchain): + """GET/POST a block from/to the blockchain.""" + null_value = { + "version": 1, + "nonce": 0, + "number": -1, + "powMin": 0, + "time": 0, + "medianTime": 0, + "membersCount": 0, + "monetaryMass": 0, + "currency": "", + "issuer": "", + "signature": "", + "hash": "", + "previousHash": "", + "previousIssuer": "", + "dividend": 0, + "membersChanges": [ ], + "identities": [], + "joiners": [], + "actives": [], + "leavers": [], + "excluded": [], + "certifications": [], + "transactions": [], + "raw": "" + } + + def __init__(self, conn_handler, number=None): + """ + Use the number parameter in order to select a block number. + + Arguments: + - `number`: block number to select + """ + + super(Block, self).__init__(conn_handler) + + self.number = number + + def __get__(self, **kwargs): + assert self.number is not None + return self.requests_get('/block/%d' % self.number, **kwargs) + + def __post__(self, **kwargs): + assert 'block' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/block', **kwargs) + + +class Current(Blockchain): + """GET, same as block/[number], but return last accepted block.""" + null_value = { + "version": 1, + "nonce": 0, + "number": -1, + "powMin": 0, + "time": 0, + "medianTime": 0, + "membersCount": 0, + "monetaryMass": 0, + "currency": "", + "issuer": "", + "signature": "", + "hash": "", + "previousHash": None, + "previousIssuer": None, + "dividend": None, + "membersChanges": [ ], + "identities": [], + "joiners": [], + "actives": [], + "leavers": [], + "excluded": [], + "certifications": [], + "transactions": [], + "raw": "" + } + def __get__(self, **kwargs): + return self.requests_get('/current', **kwargs) + + +class Hardship(Blockchain): + """GET hardship level for given member's fingerprint for writing next block.""" + + def __init__(self, conn_handler, fingerprint): + """ + Use the number parameter in order to select a block number. + + Arguments: + - `fingerprint`: member fingerprint + """ + + super(Hardship, self).__init__(conn_handler) + + self.fingerprint = fingerprint + + def __get__(self, **kwargs): + assert self.fingerprint is not None + return self.requests_get('/hardship/%s' % self.fingerprint.upper(), **kwargs) + + +class Newcomers(Blockchain): + """GET, return block numbers containing newcomers.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/newcomers', **kwargs) + + +class Certifications(Blockchain): + """GET, return block numbers containing certifications.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/certs', **kwargs) + + +class Joiners(Blockchain): + """GET, return block numbers containing joiners.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/joiners', **kwargs) + + +class Actives(Blockchain): + """GET, return block numbers containing actives.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/actives', **kwargs) + + +class Leavers(Blockchain): + """GET, return block numbers containing leavers.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/leavers', **kwargs) + + +class Excluded(Blockchain): + """GET, return block numbers containing excluded.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/excluded', **kwargs) + + +class UD(Blockchain): + """GET, return block numbers containing universal dividend.""" + null_value = { + "result": { + "blocks": [] + } + } + + def __get__(self, **kwargs): + return self.requests_get('/with/ud', **kwargs) + + +class TX(Blockchain): + """GET, return block numbers containing transactions.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/tx', **kwargs) diff --git a/src/cutecoin/core/net/api/bma/network/__init__.py b/src/cutecoin/core/net/api/bma/network/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..252219946af9fd6471dd6f20bf3f95659173d19c --- /dev/null +++ b/src/cutecoin/core/net/api/bma/network/__init__.py @@ -0,0 +1,35 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/network") + + +class Network(API): + def __init__(self, conn_handler, module='network'): + super(Network, self).__init__(conn_handler, module) + + +class Peering(Network): + """GET peering information about a peer.""" + + def __get__(self, **kwargs): + return self.requests_get('/peering', **kwargs) + +from . import peering diff --git a/src/cutecoin/core/net/api/bma/network/peering/__init__.py b/src/cutecoin/core/net/api/bma/network/peering/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8da405735be29d99eaea8c590d6c328b3d95a954 --- /dev/null +++ b/src/cutecoin/core/net/api/bma/network/peering/__init__.py @@ -0,0 +1,51 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import Network, logging + +logger = logging.getLogger("ucoin/network/peering") + + +class Base(Network): + def __init__(self, conn_handler): + super(Base, self).__init__(conn_handler, 'network/peering') + + +class Peers(Base): + """GET peering entries of every node inside the currency network.""" + + def __get__(self, **kwargs): + """creates a generator with one peering entry per iteration.""" + + return self.requests_get('/peers', **kwargs) + + def __post__(self, **kwargs): + assert 'entry' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/peers', **kwargs) + + +class Status(Base): + """POST a network status document to this node in order notify of its status.""" + + def __post__(self, **kwargs): + assert 'status' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/status', **kwargs) diff --git a/src/cutecoin/core/net/api/bma/node/__init__.py b/src/cutecoin/core/net/api/bma/node/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..975a14649861f5cf8e10069f7dba6e7be06cc6ad --- /dev/null +++ b/src/cutecoin/core/net/api/bma/node/__init__.py @@ -0,0 +1,36 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/node") + +class Node(API): + def __init__(self, connection_handler, module='node'): + super(Node, self).__init__(connection_handler, module) + + +class Summary(Node): + """GET Certification data over a member.""" + + def __init__(self, connection_handler, module='node'): + super(Summary, self).__init__(connection_handler, module) + + def __get__(self, **kwargs): + return self.requests_get('/summary', **kwargs) + diff --git a/src/cutecoin/core/net/api/bma/tx/__init__.py b/src/cutecoin/core/net/api/bma/tx/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4bfc3cf4755224f5e8cd3af43374f60a27253f54 --- /dev/null +++ b/src/cutecoin/core/net/api/bma/tx/__init__.py @@ -0,0 +1,55 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/tx") + + +class Tx(API): + def __init__(self, conn_handler, module='tx'): + super(Tx, self).__init__(conn_handler, module) + + +class Process(Tx): + """POST a transaction.""" + + def __post__(self, **kwargs): + assert 'transaction' in kwargs + + return self.requests_post('/process', **kwargs) + + +class Sources(Tx): + """Get transaction sources.""" + + null_value = { + "currency": "", + "pubkey": "", + "sources": + [ + ] + } + + def __init__(self, conn_handler, pubkey, module='tx'): + super(Tx, self).__init__(conn_handler, module) + self.pubkey = pubkey + + def __get__(self, **kwargs): + assert self.pubkey is not None + return self.requests_get('/sources/%s' % self.pubkey, **kwargs) diff --git a/src/cutecoin/core/net/api/bma/wot/__init__.py b/src/cutecoin/core/net/api/bma/wot/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9eb01e13d3ed26459adcb3fd5c0b877af3c36b37 --- /dev/null +++ b/src/cutecoin/core/net/api/bma/wot/__init__.py @@ -0,0 +1,107 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/wot") + + +class WOT(API): + def __init__(self, conn_handler, module='wot'): + super(WOT, self).__init__(conn_handler, module) + + +class Add(WOT): + """POST Public key data.""" + + def __post__(self, **kwargs): + assert 'pubkey' in kwargs + assert 'self_' in kwargs + assert 'other' in kwargs + + return self.requests_post('/add', **kwargs) + + +class Lookup(WOT): + """GET Public key data.""" + + def __init__(self, conn_handler, search, module='wot'): + super(WOT, self).__init__(conn_handler, module) + + self.search = search + + def __get__(self, **kwargs): + assert self.search is not None + + return self.requests_get('/lookup/%s' % self.search, **kwargs) + + +class CertifiersOf(WOT): + """GET Certification data over a member.""" + null_value = \ + { + "pubkey": "", + "uid": "", + "isMember": False, + "certifications": [] + } + + def __init__(self, conn_handler, search, module='wot'): + super(WOT, self).__init__(conn_handler, module) + + self.search = search + + def __get__(self, **kwargs): + assert self.search is not None + + return self.requests_get('/certifiers-of/%s' % self.search, **kwargs) + + +class CertifiedBy(WOT): + """GET Certification data from a member.""" + null_value = \ + { + "pubkey": "", + "uid": "", + "isMember": False, + "certifications": [] + } + + def __init__(self, conn_handler, search, module='wot'): + super(WOT, self).__init__(conn_handler, module) + + self.search = search + + def __get__(self, **kwargs): + assert self.search is not None + + return self.requests_get('/certified-by/%s' % self.search, **kwargs) + + +class Members(WOT): + """GET List all current members of the Web of Trust.""" + + null_value = \ + { + "results": [] + } + def __init__(self, conn_handler, module='wot'): + super(WOT, self).__init__(conn_handler, module) + + def __get__(self, **kwargs): + return self.requests_get('/members', **kwargs) diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py index 32602d00be232c4ac90ff9f491561d834aace8dc..b12932d325b578e7890a39908bc946a5c6b80252 100644 --- a/src/cutecoin/core/net/network.py +++ b/src/cutecoin/core/net/network.py @@ -1,27 +1,26 @@ ''' -Created on 24 feb. 2015 +Created on 24 févr. 2015 @author: inso ''' -from .node import Node +from cutecoin.core.net.node import Node import logging import time +from ucoinpy.documents.peer import Peer -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QMutex, QCoreApplication -from ..watching.watcher import Watcher +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QCoreApplication, QTimer -class Network(Watcher): - """ +class Network(QObject): + ''' A network is managing nodes polling and crawling of a given community. - """ + ''' nodes_changed = pyqtSignal() new_block_mined = pyqtSignal(int) - stopped_perpetual_crawling = pyqtSignal() - def __init__(self, currency, nodes): + def __init__(self, network_manager, currency, nodes): ''' Constructor of a network @@ -31,15 +30,17 @@ class Network(Watcher): super().__init__() self._root_nodes = nodes self._nodes = [] - self._mutex = QMutex() + for n in nodes: + self.add_node(n) self.currency = currency - self.nodes = nodes self._must_crawl = False self._is_perpetual = False + self.network_manager = network_manager self._block_found = self.latest_block + self._timer = QTimer() @classmethod - def create(cls, node): + def create(cls, network_manager, node): ''' Create a new network with one knew node Crawls the nodes from the first node to build the @@ -48,7 +49,7 @@ class Network(Watcher): :param node: The first knew node of the network ''' nodes = [node] - network = cls(node.currency, nodes) + network = cls(network_manager, node.currency, nodes) return network def merge_with_json(self, json_data): @@ -59,7 +60,7 @@ class Network(Watcher): :param dict json_data: Nodes in json format ''' for data in json_data: - node = Node.from_json(self.currency, data) + node = Node.from_json(self.network_manager, self.currency, data) if node.pubkey not in [n.pubkey for n in self.nodes]: self.add_node(node) logging.debug("Loading : {:}".format(data['pubkey'])) @@ -71,7 +72,7 @@ class Network(Watcher): other_node.state = node.state @classmethod - def from_json(cls, currency, json_data): + def from_json(cls, network_manager, currency, json_data): ''' Load a network from a configured community @@ -80,9 +81,9 @@ class Network(Watcher): ''' nodes = [] for data in json_data: - node = Node.from_json(currency, data) + node = Node.from_json(network_manager, currency, data) nodes.append(node) - network = cls(currency, nodes) + network = cls(network_manager, currency, nodes) return network def jsonify(self): @@ -96,6 +97,16 @@ class Network(Watcher): data.append(node.jsonify()) return data + @property + def quality(self): + ''' + Get a ratio of the synced nodes vs the rest + ''' + synced = len(self.synced_nodes) + total = len(self.nodes) + ratio_synced = synced / total + return ratio_synced + def stop_crawling(self): ''' Stop network nodes crawling. @@ -113,10 +124,6 @@ class Network(Watcher): ''' Get nodes which are in the ONLINE state. ''' - latest = self.latest_block - for n in self._nodes: - if n.state in (Node.ONLINE, Node.DESYNCED): - n.check_sync(latest) return [n for n in self.nodes if n.state == Node.ONLINE] @property @@ -140,25 +147,6 @@ class Network(Watcher): ''' return self._root_nodes - @nodes.setter - def nodes(self, new_nodes): - ''' - Set new nodes - ''' - self._mutex.lock() - try: - for n in self.nodes: - try: - n.disconnect() - except TypeError: - logging.debug("Error disconnecting node {0}".format(n.pubkey[:5])) - - self._nodes = [] - for n in new_nodes: - self.add_node(n) - finally: - self._mutex.unlock() - @property def latest_block(self): ''' @@ -172,7 +160,8 @@ class Network(Watcher): ''' self._nodes.append(node) node.changed.connect(self.handle_change) - logging.debug("{:} connected".format(node.pubkey)) + node.neighbour_found.connect(self.handle_new_node) + logging.debug("{:} connected".format(node.pubkey[:5])) def add_root_node(self, node): ''' @@ -200,91 +189,42 @@ class Network(Watcher): node = self.nodes[index] return self._root_nodes.index(node) - def moveToThread(self, thread): - for n in self.nodes: - n.moveToThread(thread) - super().moveToThread(thread) - - def watch(self): - self.stopped_perpetual_crawling.connect(self.watching_stopped) - self.start_perpetual_crawling() - - def stop(self): - self.stop_crawling() - - def start_perpetual_crawling(self): + def discover_network(self): ''' Start crawling which never stops. To stop this crawling, call "stop_crawling" method. ''' - self._must_crawl = True - while self.continue_crawling(): - emit_change = False - nodes = self.crawling(interval=2) - - new_inlines = [n.endpoint.inline() for n in nodes] - last_inlines = [n.endpoint.inline() for n in self.nodes] - - hash_new_nodes = str(tuple(frozenset(sorted(new_inlines)))) - hash_last_nodes = str(tuple(frozenset(sorted(last_inlines)))) - if hash_new_nodes != hash_last_nodes: - logging.debug("Nodes changed...") - self.nodes = nodes - emit_change = True - - for node in self.nodes: - if node.last_change + 3600 < time.time() and \ - node.state in (Node.OFFLINE, Node.CORRUPTED): - try: - node.changed.disconnect() - except TypeError: - logging.debug("Error : {0} not connected".format(node.pubkey)) - self.nodes.remove(node) - emit_change = True - - if emit_change: - self.nodes_changed.emit() - QCoreApplication.processEvents() - - self.stopped_perpetual_crawling.emit() + if not self.continue_crawling(): + return + for (i, node) in enumerate(self.nodes): + if not self.continue_crawling(): + return + + if node == self.nodes[-1]: + QTimer.singleShot((i+1)*10000, self.discover_network) + QTimer.singleShot(i*10000, node.refresh) + + @pyqtSlot(Peer) + def handle_new_node(self, peer): + logging.debug("New node found : {0}".format(peer.pubkey[:5])) + pubkeys = [n.pubkey for n in self.nodes] + if peer.pubkey not in pubkeys: + node = Node.from_peer(self.network_manager, self.currency, peer) + self.add_node(node) + self.nodes_changed.emit() @pyqtSlot() def handle_change(self): node = self.sender() - logging.debug("Handle change") if node.state in (Node.ONLINE, Node.DESYNCED): node.check_sync(self.latest_block) + else: + if node.last_change + 3600 < time.time(): + self.nodes.remove(node) + self.nodes_changed.emit() + logging.debug("{0} -> {1}".format(self.latest_block, self.latest_block)) if self._block_found < self.latest_block: - self._block_found = self.latest_block logging.debug("New block found : {0}".format(self.latest_block)) + self._block_found = self.latest_block self.new_block_mined.emit(self.latest_block) - - QCoreApplication.processEvents() - logging.debug("Syncing : {0} : last changed {1} : unsynced : {2}".format(node.pubkey[:5], - node.last_change, time.time() - node.last_change)) - - self.nodes_changed.emit() - - def crawling(self, interval=0): - ''' - One network crawling. - - :param int interval: The interval between two nodes request. - ''' - nodes = [] - traversed_pubkeys = [] - knew_pubkeys = [n.pubkey for n in self.nodes] - for n in self.nodes: - logging.debug(traversed_pubkeys) - logging.debug("Peering : next to read : {0} : {1}".format(n.pubkey, - (n.pubkey not in traversed_pubkeys))) - if self.continue_crawling(): - n.peering_traversal(knew_pubkeys, nodes, - traversed_pubkeys, interval, - self.continue_crawling) - QCoreApplication.processEvents() - time.sleep(interval) - - logging.debug("Nodes found : {0}".format(nodes)) - return nodes diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py index c71bcce138130ea7b916c1a8ffa7a5d958db5f60..c2c162e4860f4080ebf03192c09b193acc389876 100644 --- a/src/cutecoin/core/net/node.py +++ b/src/cutecoin/core/net/node.py @@ -5,18 +5,20 @@ Created on 21 févr. 2015 ''' from ucoinpy.documents.peer import Peer, BMAEndpoint, Endpoint -from ucoinpy.api import bma -from ucoinpy.api.bma import ConnectionHandler from requests.exceptions import RequestException, ConnectionError -from ...tools.exceptions import InvalidNodeCurrency, PersonNotFoundError -from ..person import Person +from cutecoin.tools.exceptions import InvalidNodeCurrency, PersonNotFoundError +from cutecoin.core.person import Person +from cutecoin.core.net.api import bma as qtbma +from cutecoin.core.net.api.bma import ConnectionHandler + import logging import time import ctypes import sys +import json -from PyQt5.QtCore import QObject, pyqtSignal - +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest class Node(QObject): ''' @@ -34,26 +36,29 @@ class Node(QObject): CORRUPTED = 4 changed = pyqtSignal() + neighbour_found = pyqtSignal(Peer) - def __init__(self, currency, endpoints, uid, pubkey, block, - state, last_change, software, version): + def __init__(self, network_manager, currency, endpoints, uid, pubkey, block, + state, last_change, last_merkle, software, version): ''' Constructor ''' super().__init__() + self.network_manager = network_manager self._endpoints = endpoints self._uid = uid self._pubkey = pubkey - self._software = software - self._version = version self.block = block self._state = state self._neighbours = [] self._currency = currency self._last_change = last_change + self._last_merkle = last_merkle + self._software = software + self._version = version @classmethod - def from_address(cls, currency, address, port): + def from_address(cls, network_manager, currency, address, port): ''' Factory method to get a node from a given address @@ -62,7 +67,7 @@ class Node(QObject): :param str address: The node address :param int port: The node port ''' - peer_data = bma.network.Peering(ConnectionHandler(address, port)).get() + peer_data = qtbma.network.Peering(ConnectionHandler(network_manager, address, port)).get() peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['raw'], peer_data['signature'])) @@ -71,13 +76,14 @@ class Node(QObject): if peer.currency != currency: raise InvalidNodeCurrency(peer.currency, currency) - node = cls(peer.currency, peer.endpoints, "", peer.pubkey, 0, - Node.ONLINE, time.time(), "", "") + node = cls(network_manager, peer.currency, peer.endpoints, + "", peer.pubkey, 0, Node.ONLINE, time.time(), + {'root': "", 'leaves': []}) logging.debug("Node from address : {:}".format(str(node))) return node @classmethod - def from_peer(cls, currency, peer): + def from_peer(cls, network_manager, currency, peer): ''' Factory method to get a node from a peer document. @@ -89,21 +95,23 @@ class Node(QObject): if peer.currency != currency: raise InvalidNodeCurrency(peer.currency, currency) - node = cls(peer.currency, peer.endpoints, "", "", 0, - Node.ONLINE, time.time(), "", "") + node = cls(network_manager, peer.currency, peer.endpoints, "", "", 0, + Node.ONLINE, time.time(), + {'root': "", 'leaves': []}, + "", "") logging.debug("Node from peer : {:}".format(str(node))) return node @classmethod - def from_json(cls, currency, data): + def from_json(cls, network_manager, currency, data): endpoints = [] uid = "" pubkey = "" + software = "" + version = "" block = 0 last_change = time.time() state = Node.ONLINE - software = '' - version = '' logging.debug(data) for endpoint_data in data['endpoints']: endpoints.append(Endpoint.from_inline(endpoint_data)) @@ -125,21 +133,20 @@ class Node(QObject): if 'state' in data: state = data['state'] - else: - logging.debug("Error : no state in node") if 'software' in data: software = data['software'] - else: - logging.debug("Error : no software in node") if 'version' in data: version = data['version'] else: - logging.debug("Error : no version in node") + logging.debug("Error : no state in node") - node = cls(currency, endpoints, uid, pubkey, block, - state, last_change, software, version) + node = cls(network_manager, currency, endpoints, + uid, pubkey, block, + state, last_change, + {'root': "", 'leaves': []}, + software, version) logging.debug("Node from json : {:}".format(str(node))) return node @@ -161,9 +168,7 @@ class Node(QObject): 'currency': self._currency, 'state': self._state, 'last_change': self._last_change, - 'block': self.block, - 'software': self.software, - 'version': self.version} + 'block': self.block} endpoints = [] for e in self._endpoints: endpoints.append(e.inline()) @@ -182,14 +187,6 @@ class Node(QObject): def block(self): return self._block - @property - def version(self): - return self._version - - @property - def software(self): - return self._software - @block.setter def block(self, new_block): self._block = new_block @@ -214,182 +211,234 @@ class Node(QObject): def last_change(self): return self._last_change + @property + def software(self): + return self._software + + @software.setter + def software(self, new_soft): + if self._software != new_soft: + self._software = new_soft + self.changed.emit() + + @property + def version(self): + return self._version + + @version.setter + def version(self, new_version): + if self._version != new_version: + self._version = new_version + self.changed.emit() + @last_change.setter def last_change(self, val): - #logging.debug("{:} | Changed state : {:}".format(self.pubkey[:5],val)) + logging.debug("{:} | Changed state : {:}".format(self.pubkey[:5], + val)) self._last_change = val @state.setter def state(self, new_state): - #logging.debug("{:} | Last state : {:} / new state : {:}".format(self.pubkey[:5],self.state, new_state)) + logging.debug("{:} | Last state : {:} / new state : {:}".format(self.pubkey[:5], + self.state, new_state)) if self._state != new_state: self.last_change = time.time() self._state = new_state def check_sync(self, block): - #logging.debug("Check sync") + logging.debug("Check sync") if self.block < block: self.state = Node.DESYNCED else: self.state = Node.ONLINE - def _request_uid(self): - uid = "" - try: - data = bma.wot.Lookup(self.endpoint.conn_handler(), self.pubkey).get() - timestamp = 0 - for result in data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - for uid in uids: - if uid["meta"]["timestamp"] > timestamp: - timestamp = uid["meta"]["timestamp"] - uid = uid["uid"] - except ValueError as e: - if '404' in str(e): - logging.debug("Error : node uid not found : {0}".format(self.pubkey)) - uid = "" - return uid - - def refresh_state(self): - logging.debug("Refresh state") - emit_change = False - try: - informations = bma.network.Peering(self.endpoint.conn_handler()).get() - node_pubkey = informations["pubkey"] - try: - block = bma.blockchain.Current(self.endpoint.conn_handler()).get() - block_number = block["number"] - except ValueError as e: - if '404' in str(e): - block_number = 0 - - peers_data = bma.network.peering.Peers(self.endpoint.conn_handler()).get() - neighbours = [] - for p in peers_data: - peer = Peer.from_signed_raw("{0}{1}\n".format(p['value']['raw'], - p['value']['signature'])) - neighbours.append(peer.endpoints) - logging.debug("Found neighbours : {0}".format(len(neighbours))) - - node_currency = informations["currency"] - node_uid = self._request_uid() - - implementation = bma.node.Summary(self.endpoint.conn_handler()).get() - software = implementation["ucoin"]["software"] - version = implementation["ucoin"]["version"] - - #If the nodes goes back online... - if self.state in (Node.OFFLINE, Node.CORRUPTED): + def check_noerror(self, error_code, status_code): + if error_code != QNetworkReply.NoError: + if self.state == Node.OFFLINE: self.state = Node.ONLINE - logging.debug("Change : new state online") - emit_change = True - except ConnectionError as e: - logging.debug(str(e)) - - if self.state != Node.OFFLINE: - self.state = Node.OFFLINE - logging.debug("Change : new state offine") - emit_change = True - # Dirty hack to reload resolv.conf on linux - if 'Connection aborted' in str(e) and 'gaierror' in str(e): - logging.debug("Connection Aborted") - if 'linux' in sys.platform: - try: - libc = ctypes.CDLL('libc.so.6') - res_init = getattr(libc, '__res_init') - res_init(None) - except: - logging.error('Error calling libc.__res_init') - except RequestException as e: - logging.debug(str(e)) - if self.state != Node.OFFLINE: - self.state = Node.OFFLINE - logging.debug("Change : new state offine") - emit_change = True - - # If not is offline, do not refresh last data - if self.state != Node.OFFLINE: - # If not changed its currency, consider it corrupted - if node_currency != self._currency: + return False + if status_code == 503: + return False + return True + + @pyqtSlot() + def refresh(self): + logging.debug("Refresh block") + self.refresh_block() + logging.debug("Refresh info") + self.refresh_informations() + logging.debug("Refresh uid") + self.refresh_uid() + logging.debug("Refresh peers") + self.refresh_peers() + logging.debug("Refresh summary") + self.refresh_summary() + + def refresh_block(self): + conn_handler = ConnectionHandler(self.network_manager, + self.endpoint.conn_handler().server, + self.endpoint.conn_handler().port) + + logging.debug("Requesting {0}".format(conn_handler)) + reply = qtbma.blockchain.Current(conn_handler).get() + + reply.finished.connect(self.handle_block_reply) + + @pyqtSlot() + def handle_block_reply(self): + reply = self.sender() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if self.check_noerror(reply.error(), status_code): + if status_code == 200: + strdata = bytes(reply.readAll()).decode('utf-8') + block_data = json.loads(strdata) + block_number = block_data['number'] + elif status_code == 404: + block_number = 0 + + if block_number != self.block: + self.block = block_number + logging.debug("Changed block {0} -> {1}".format(self.block, + block_number)) + self.changed.emit() + + else: + logging.debug("Error in block reply") + + def refresh_informations(self): + conn_handler = ConnectionHandler(self.network_manager, + self.endpoint.conn_handler().server, + self.endpoint.conn_handler().port) + + peering_reply = qtbma.network.Peering(conn_handler).get() + peering_reply.finished.connect(self.handle_peering_reply) + + @pyqtSlot() + def handle_peering_reply(self): + reply = self.sender() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if self.check_noerror(reply.error(), status_code): + strdata = bytes(reply.readAll()).decode('utf-8') + peering_data = json.loads(strdata) + logging.debug(peering_data) + node_pubkey = peering_data["pubkey"] + node_currency = peering_data["currency"] + + change = False + if node_pubkey != self.pubkey: + self._pubkey = node_pubkey + change = True + + if node_currency != self.currency: self.state = Node.CORRUPTED logging.debug("Change : new state corrupted") - emit_change = True - else: - if block_number != self.block: - logging.debug("Change : new block {0} -> {1}".format(self.block, - block_number)) - self.block = block_number - logging.debug("Changed block {0} -> {1}".format(self.block, - block_number)) - emit_change = True - - if node_pubkey != self._pubkey: - logging.debug("Change : new pubkey {0} -> {1}".format(self._pubkey, - node_pubkey)) - self._pubkey = node_pubkey - emit_change = True - - if node_uid != self._uid: - logging.debug("Change : new uid") - self._uid = node_uid - emit_change = True - - if software != self._software: - logging.debug("Change : new software") - self._software = software - emit_change = True - - if version != self._version: - logging.debug("Change : new version") - self._version = version - emit_change = True - - logging.debug(neighbours) - new_inlines = [e.inline() for n in neighbours for e in n] - last_inlines = [e.inline() for n in self._neighbours for e in n] - - hash_new_neighbours = hash(tuple(frozenset(sorted(new_inlines)))) - hash_last_neighbours = hash(tuple(frozenset(sorted(last_inlines)))) - if hash_new_neighbours != hash_last_neighbours: - self._neighbours = neighbours - logging.debug("Change : new neighbours {0} -> {1}".format(last_inlines, - new_inlines)) - emit_change = True - - if emit_change: - self.changed.emit() + change = True - def peering_traversal(self, knew_pubkeys, found_nodes, - traversed_pubkeys, interval, - continue_crawling): - logging.debug("Read {0} peering".format(self.pubkey)) - traversed_pubkeys.append(self.pubkey) - self.refresh_state() - - if self.pubkey not in [n.pubkey for n in found_nodes]: - # if node is corrupted remove it - if self.state != Node.CORRUPTED: - logging.debug("Found : {0} node".format(self.pubkey)) - found_nodes.append(self) - logging.debug(self.neighbours) - for n in self.neighbours: - try: - e = next(e for e in n if type(e) is BMAEndpoint) - peering = bma.network.Peering(e.conn_handler()).get() - except: - continue - peer = Peer.from_signed_raw("{0}{1}\n".format(peering['raw'], - peering['signature'])) - if peer.pubkey not in traversed_pubkeys and \ - peer.pubkey not in knew_pubkeys and continue_crawling(): - node = Node.from_peer(self._currency, peer) - logging.debug(traversed_pubkeys) - logging.debug("Traversing : next to read : {0} : {1}".format(node.pubkey, - (node.pubkey not in traversed_pubkeys))) - node.peering_traversal(knew_pubkeys, found_nodes, - traversed_pubkeys, interval, continue_crawling) - time.sleep(interval) + if change: + self.changed.emit() + else: + logging.debug("Error in peering reply") + + def refresh_summary(self): + conn_handler = ConnectionHandler(self.network_manager, + self.endpoint.conn_handler().server, + self.endpoint.conn_handler().port) + + summary_reply = qtbma.node.Summary(conn_handler).get() + summary_reply.finished.connect(self.handle_summary_reply) + + @pyqtSlot() + def handle_summary_reply(self): + reply = self.sender() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if self.check_noerror(reply.error(), status_code): + strdata = bytes(reply.readAll()).decode('utf-8') + summary_data = json.loads(strdata) + self.software = summary_data["ucoin"]["software"] + self.version = summary_data["ucoin"]["version"] + + def refresh_uid(self): + conn_handler = ConnectionHandler(self.network_manager, + self.endpoint.conn_handler().server, + self.endpoint.conn_handler().port) + uid_reply = qtbma.wot.Lookup(conn_handler, self.pubkey).get() + uid_reply.finished.connect(self.handle_uid_reply) + uid_reply.error.connect(lambda code: logging.debug("Error : {0}".format(code))) + + @pyqtSlot() + def handle_uid_reply(self): + reply = self.sender() + status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute ); + + if self.check_noerror(reply.error(), status_code): + uid = '' + if status_code == 200: + strdata = bytes(reply.readAll()).decode('utf-8') + data = json.loads(strdata) + timestamp = 0 + for result in data['results']: + if result["pubkey"] == self.pubkey: + uids = result['uids'] + for uid in uids: + if uid["meta"]["timestamp"] > timestamp: + timestamp = uid["meta"]["timestamp"] + uid = uid["uid"] + elif status_code == 404: + logging.debug("UID not found") + + if self._uid != uid: + self._uid = uid + self.changed.emit() + else: + logging.debug("error in uid reply") + + def refresh_peers(self): + conn_handler = ConnectionHandler(self.network_manager, + self.endpoint.conn_handler().server, + self.endpoint.conn_handler().port) + + reply = qtbma.network.peering.Peers(conn_handler).get(leaves='true') + reply.finished.connect(self.handle_peers_reply) + reply.error.connect(lambda code: logging.debug("Error : {0}".format(code))) + + @pyqtSlot() + def handle_peers_reply(self): + reply = self.sender() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if self.check_noerror(reply.error(), status_code): + strdata = bytes(reply.readAll()).decode('utf-8') + peers_data = json.loads(strdata) + if peers_data['root'] != self._last_merkle['root']: + leaves = [leaf for leaf in peers_data['leaves'] + if leaf not in self._last_merkle['leaves']] + for leaf_hash in leaves: + conn_handler = ConnectionHandler(self.network_manager, + self.endpoint.conn_handler().server, + self.endpoint.conn_handler().port) + leaf_reply = qtbma.network.peering.Peers(conn_handler).get(leaf=leaf_hash) + leaf_reply.finished.connect(self.handle_leaf_reply) + self._last_merkle = {'root' : peers_data['root'], + 'leaves': peers_data['leaves']} + else: + logging.debug("Error in peers reply") + + @pyqtSlot() + def handle_leaf_reply(self): + reply = self.sender() + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) + + if self.check_noerror(reply.error(), status_code): + strdata = bytes(reply.readAll()).decode('utf-8') + leaf_data = json.loads(strdata) + doc = Peer.from_signed_raw("{0}{1}\n".format(leaf_data['leaf']['value']['raw'], + leaf_data['leaf']['value']['signature'])) + self.neighbour_found.emit(doc) + else: + logging.debug("Error in leaf reply") def __str__(self): return ','.join([str(self.pubkey), str(self.endpoint.server), str(self.endpoint.port), str(self.block), diff --git a/src/cutecoin/core/person.py b/src/cutecoin/core/person.py index 276ef0e0fa752a4124bcb56051d9d26e0aa12c29..ac44769f48489a4b8e23ad27296717de4728b3b5 100644 --- a/src/cutecoin/core/person.py +++ b/src/cutecoin/core/person.py @@ -419,7 +419,7 @@ class Person(object): def membership_expiration_time(self, community): join_block = self.membership(community)['blockNumber'] - join_date = community.get_block(join_block).mediantime + join_date = community.get_block(join_block)['medianTime'] parameters = community.parameters expiration_date = join_date + parameters['sigValidity'] current_time = time.time() diff --git a/src/cutecoin/core/wallet.py b/src/cutecoin/core/wallet.py index 415f05bc2abf81fcc7262f310b27d992a1c50b1f..ba53f61d0034d4b88529ae409b75a3e6c835f026 100644 --- a/src/cutecoin/core/wallet.py +++ b/src/cutecoin/core/wallet.py @@ -4,12 +4,10 @@ Created on 1 févr. 2014 @author: inso ''' -from ucoinpy import PROTOCOL_VERSION -from ucoinpy.api import bma -from ucoinpy.documents.block import Block from ucoinpy.documents.transaction import InputSource, OutputSource, Transaction from ucoinpy.key import SigningKey +from .net.api import bma as qtbma from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable, PersonNotFoundError from .transfer import Transfer, Received from .person import Person @@ -198,6 +196,7 @@ class Wallet(QObject): A wallet is used to manage money with a unique key. ''' + inner_data_changed = pyqtSignal(int) refresh_progressed = pyqtSignal(int, int) def __init__(self, walletid, pubkey, name): @@ -440,10 +439,10 @@ class Wallet(QObject): ''' Get available sources in a given community - :param community: The community where we want available sources + :param cutecoin.core.community.Community community: The community where we want available sources :return: List of InputSource ucoinpy objects ''' - data = community.request(bma.tx.Sources, + data = community.bma_access.get(self, qtbma.tx.Sources, req_args={'pubkey': self.pubkey}) tx = [] for s in data['sources']: diff --git a/src/cutecoin/core/watching/blockchain.py b/src/cutecoin/core/watching/blockchain.py deleted file mode 100644 index a1f6c46179912a2a1302525307b20c7fbb4283f1..0000000000000000000000000000000000000000 --- a/src/cutecoin/core/watching/blockchain.py +++ /dev/null @@ -1,63 +0,0 @@ -''' -Created on 27 févr. 2015 - -@author: inso -''' - -import logging -import time -from requests.exceptions import RequestException -from ...tools.exceptions import NoPeerAvailable -from .watcher import Watcher -from PyQt5.QtCore import pyqtSignal - - -class BlockchainWatcher(Watcher): - - new_transfers = pyqtSignal(list) - loading_progressed = pyqtSignal(int, int) - - def __init__(self, account, community): - super().__init__() - self.account = account - self.community = community - self.time_to_wait = int(self.community.parameters['avgGenTime'] / 10) - self.exiting = False - self.last_block = self.community.network.latest_block - - def watch(self): - loaded_wallets = 0 - def progressing(value, maximum): - account_value = maximum * loaded_wallets + value - account_max = maximum * len(self.account.wallets) - self.loading_progressed.emit(account_value, account_max) - - try: - received_list = [] - block_number = self.community.network.latest_block - if self.last_block != block_number: - - for w in self.account.wallets: - w.refresh_progressed.connect(progressing) - - if not self.exiting: - self.community.refresh_cache() - for w in self.account.wallets: - if not self.exiting: - w.refresh_cache(self.community, received_list) - loaded_wallets = loaded_wallets + 1 - - logging.debug("New block, {0} mined in {1}".format(block_number, - self.community.currency)) - self.last_block = block_number - if len(received_list) > 0: - self.new_transfers.emit(received_list) - except NoPeerAvailable: - pass - except RequestException as e: - self.error.emit("Cannot check new block : {0}".format(str(e))) - finally: - self.watching_stopped.emit() - - def stop(self): - self.exiting = True diff --git a/src/cutecoin/core/watching/monitor.py b/src/cutecoin/core/watching/monitor.py deleted file mode 100644 index c7086527f3789b89555546f897e75a83ef512a05..0000000000000000000000000000000000000000 --- a/src/cutecoin/core/watching/monitor.py +++ /dev/null @@ -1,91 +0,0 @@ -''' -Created on 18 mars 2015 - -@author: inso -''' - -from PyQt5.QtCore import QThread, Qt, QObject -from .blockchain import BlockchainWatcher -from .persons import PersonsWatcher -import logging - - -class Monitor(object): - ''' - The monitor is managing watchers - ''' - - # Dirty hack to avoid GC on monitors - # GC was causing random crashes - # We will get rid of QThreads asap - #___dirty_monitors = [] - - def __init__(self, account): - ''' - Constructor - ''' - super().__init__() - self.account = account - self.threads_pool = [] - self._blockchain_watchers = {} - self._network_watchers = {} - self._persons_watchers = {} - #Monitor.___dirty_monitors.append(self) - - def blockchain_watcher(self, community): - return self._blockchain_watchers[community.name] - - def network_watcher(self, community): - return self._networks[community.name] - - def persons_watcher(self, community): - return self._persons_watchers[community.name] - - def connect_watcher_to_thread(self, watcher): - thread = QThread() - watcher.moveToThread(thread) - thread.started.connect(watcher.watch) - watcher.watching_stopped.connect(thread.exit) - - self.threads_pool.append(thread) - - def prepare_watching(self): - for c in self.account.communities: - persons_watcher = PersonsWatcher(c) - self.connect_watcher_to_thread(persons_watcher) - self._persons_watchers[c.name] = persons_watcher - - bc_watcher = BlockchainWatcher(self.account, c) - self.connect_watcher_to_thread(bc_watcher) - self._blockchain_watchers[c.name] = bc_watcher - - self.connect_watcher_to_thread(c.network) - self._network_watchers[c.name] = c.network - - def start_network_watchers(self): - for watcher in self._network_watchers.values(): - watcher.thread().start() - - def stop_watching(self): - for watcher in self._persons_watchers.values(): - watcher.stop() - self.threads_pool.remove(watcher.thread()) - watcher.deleteLater() - watcher.thread().deleteLater() - - for watcher in self._blockchain_watchers.values(): - watcher.stop() - self.threads_pool.remove(watcher.thread()) - watcher.deleteLater() - watcher.thread().deleteLater() - - for watcher in self._network_watchers.values(): - watcher.stop() - self.threads_pool.remove(watcher.thread()) - watcher.deleteLater() - watcher.thread().deleteLater() - - self.threads_pool = [] - self._blockchain_watchers = {} - self._network_watchers = {} - self._persons_watchers = {} diff --git a/src/cutecoin/core/watching/persons.py b/src/cutecoin/core/watching/persons.py deleted file mode 100644 index 6311acd524b39b8ed53ba3723ac7cac88c837daf..0000000000000000000000000000000000000000 --- a/src/cutecoin/core/watching/persons.py +++ /dev/null @@ -1,44 +0,0 @@ -''' -Created on 27 févr. 2015 - -@author: inso -''' - -from PyQt5.QtCore import pyqtSignal -from ..person import Person -from .watcher import Watcher -import logging - - -class PersonsWatcher(Watcher): - ''' - This will crawl the network to always - have up to date informations about the nodes - ''' - person_changed = pyqtSignal(str) - - def __init__(self, community): - super().__init__() - self.community = community - self.exiting = False - - def watch(self): - logging.debug("Watching persons") - instances = Person._instances.copy() - for p in instances.values(): - if not self.exiting: - for func in [Person.membership, - Person.is_member, - Person.certifiers_of, - Person.certified_by, - Person.published_uid]: - if not self.exiting: - if p.reload(func, self.community): - logging.debug("Change detected on {0} about {1}".format(p.pubkey, - func.__name__)) - self.person_changed.emit(p.pubkey) - logging.debug("Finished watching persons") - self.watching_stopped.emit() - - def stop(self): - self.exiting = True \ No newline at end of file diff --git a/src/cutecoin/core/watching/watcher.py b/src/cutecoin/core/watching/watcher.py deleted file mode 100644 index ead092282525b64fd85e309c242b545320f191c6..0000000000000000000000000000000000000000 --- a/src/cutecoin/core/watching/watcher.py +++ /dev/null @@ -1,23 +0,0 @@ -''' -Created on 20 mars 2015 - -@author: inso -''' - -from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal - - -class Watcher(QObject): - watching_stopped = pyqtSignal() - error = pyqtSignal(str) - - def __init__(self): - super().__init__() - - @pyqtSlot() - def watch(self): - pass - - @pyqtSlot() - def stop(self): - pass diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py index 724b89957ab1cd00ae04802eb5816bf0c44bccf1..09c7b99dc595fc2a0ba18fb8c6a5d9ec487cfcd2 100644 --- a/src/cutecoin/gui/community_tab.py +++ b/src/cutecoin/gui/community_tab.py @@ -20,6 +20,7 @@ from . import toast from ..tools.exceptions import PersonNotFoundError, NoPeerAvailable from ..core.person import Person from ucoinpy.api import bma +from ..core.net.api import bma as qtbma class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): @@ -41,7 +42,9 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): self.setupUi(self) self.parent = parent self.community = community + self.community.inner_data_changed.connect(self.handle_change) self.account = account + self._last_search = '' self.password_asker = password_asker identities_model = IdentitiesTableModel(community) proxy = IdentitiesFilterProxyModel() @@ -51,7 +54,6 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): self.table_identities.customContextMenuRequested.connect(self.identity_context_menu) self.table_identities.sortByColumn(0, Qt.AscendingOrder) self.table_identities.resizeColumnsToContents() - app.monitor.persons_watcher(self.community).person_changed.connect(self.refresh_person) self.wot_tab = WotTabWidget(app, account, community, password_asker, self) self.tabs_information.addTab(self.wot_tab, QIcon(':/icons/wot_icon'), self.tr("Web of Trust")) @@ -282,9 +284,15 @@ Revoking your UID can only success if it is not already validated by the network for identity in response['results']: persons.append(Person.lookup(identity['pubkey'], self.community)) + self._last_search = 'text' self.edit_textsearch.clear() self.refresh(persons) + def handle_change(self, origin): + if origin == qtbma.wot.Members: + if self._last_search == 'members': + self.search_members() + def search_members(self): """ Search members of community and display found members @@ -294,6 +302,8 @@ Revoking your UID can only success if it is not already validated by the network for p in pubkeys: persons.append(Person.lookup(p, self.community)) + self._last_search = 'members' + self.edit_textsearch.clear() self.refresh(persons) @@ -301,6 +311,7 @@ Revoking your UID can only success if it is not already validated by the network """ Search members of community and display found members """ + self._last_search = 'direct_connections' self.refresh() def refresh(self, persons=None): diff --git a/src/cutecoin/gui/currency_tab.py b/src/cutecoin/gui/currency_tab.py index 65075002e172f422846c77fd412176d179eaba95..3badb34f0c12eabb56d435b19ed6a990572da627 100644 --- a/src/cutecoin/gui/currency_tab.py +++ b/src/cutecoin/gui/currency_tab.py @@ -59,10 +59,6 @@ class CurrencyTabWidget(QWidget, Ui_CurrencyTabWidget): self.community.network.new_block_mined.connect(self.refresh_block) self.community.network.nodes_changed.connect(self.refresh_status) - bc_watcher = self.app.monitor.blockchain_watcher(self.community) - bc_watcher.error.connect(self.display_error) - bc_watcher.watching_stopped.connect(self.refresh_data) - bc_watcher.new_transfers.connect(self.notify_transfers) def refresh(self): if self.app.current_account is None: @@ -102,11 +98,11 @@ class CurrencyTabWidget(QWidget, Ui_CurrencyTabWidget): self.tr("Informations")) # fix bug refresh_nodes launch on destroyed NetworkTabWidget - logging.debug('Disconnect community.network.nodes_changed') - try: - self.community.network.nodes_changed.disconnect() - except TypeError: - logging.debug('No signals on community.network.nodes_changed') + #logging.debug('Disconnect community.network.nodes_changed') + #try: + # self.community.network.nodes_changed.disconnect() + #except TypeError: + # logging.debug('No signals on community.network.nodes_changed') self.tab_network = NetworkTabWidget(self.community) self.tabs_account.addTab(self.tab_network, @@ -181,9 +177,9 @@ class CurrencyTabWidget(QWidget, Ui_CurrencyTabWidget): ''' logging.debug("Refresh status") text = self.tr(" Block {0}").format(self.community.network.latest_block) - if self.community.network_quality() > 0.66: + if self.community.network.quality > 0.66: icon = '<img src=":/icons/connected" width="12" height="12"/>' - elif self.community.network_quality() > 0.33: + elif self.community.network.quality > 0.33: icon = '<img src=":/icons/weak_connect" width="12" height="12"/>' else: icon = '<img src=":/icons/disconnected" width="12" height="12"/>' @@ -213,6 +209,7 @@ class CurrencyTabWidget(QWidget, Ui_CurrencyTabWidget): self.tab_wallets.refresh() def showEvent(self, event): + self.community.network.discover_network() self.refresh_status() def referential_changed(self): diff --git a/src/cutecoin/gui/informations_tab.py b/src/cutecoin/gui/informations_tab.py index d6e3661557752428b70c293d1d5909c00b9de576..e4c3c854285b4a3200f7b16819b719b2a008c115 100644 --- a/src/cutecoin/gui/informations_tab.py +++ b/src/cutecoin/gui/informations_tab.py @@ -64,6 +64,7 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): # display float values localized_ud = QLocale().toString(ud, 'f', 6) localized_mass_per_member = QLocale().toString( + float(0) if block['membersCount'] <= 0 else self.get_referential_diff_value(block['monetaryMass'] / block['membersCount']), 'f', 6 ) localized_monetary_mass = QLocale().toString( @@ -93,6 +94,7 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): localized_mass_per_member, self.tr('Monetary Mass per member M(t)/N(t) in'), self.get_referential_diff_name(), + float(0) if block_t_minus_1['membersCount'] == 0 else block['dividend'] / (block_t_minus_1['monetaryMass'] / block_t_minus_1['membersCount']), params['dt'] / 86400, self.tr('Actual growth c = UD(t)/[M(t-1)/N(t-1)]'), @@ -128,6 +130,7 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): float( self.get_referential_diff_value( math.ceil( + float(0) if block['membersCount'] == 0 else max(block['dividend'], params['c'] * block['monetaryMass'] / block['membersCount']) ) ) diff --git a/src/cutecoin/gui/mainwindow.py b/src/cutecoin/gui/mainwindow.py index b44cafb29dfe137ed7877ba77c0302f1022c400b..7cca2b206465bed3721085ba23fef6f76553518c 100644 --- a/src/cutecoin/gui/mainwindow.py +++ b/src/cutecoin/gui/mainwindow.py @@ -189,7 +189,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): except: logging.debug("Disconnect of app failed") - self.app.monitor.start_network_watchers() QApplication.processEvents() def open_transfer_money_dialog(self): diff --git a/src/cutecoin/gui/transactions_tab.py b/src/cutecoin/gui/transactions_tab.py index 4d6b382e5dc3e09eb5d71e15a5e2474460f4a132..f0c739e3cd7787b875dc3171b4aba636495beff5 100644 --- a/src/cutecoin/gui/transactions_tab.py +++ b/src/cutecoin/gui/transactions_tab.py @@ -39,7 +39,7 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): def refresh(self): minimum_datetime = QDateTime() - minimum_datetime.setTime_t(self.community.get_block(1).mediantime) + minimum_datetime.setTime_t(self.community.get_block(1)['medianTime']) minimum_datetime.setTime(QTime(0, 0)) self.date_from.setMinimumDateTime(minimum_datetime) @@ -74,8 +74,6 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): self.progressbar.setMaximum(maximum) self.progressbar.show() - blockchain_watcher = self.app.monitor.blockchain_watcher(self.community) - blockchain_watcher.loading_progressed.connect(progressing) def stop_progress(self): self.progressbar.hide() diff --git a/src/cutecoin/gui/wallets_tab.py b/src/cutecoin/gui/wallets_tab.py index 22cdc57472ee852dcde3b2f9bbac79f1f48ca34f..54654146570e0629a254c7c0d836df4497d6875c 100644 --- a/src/cutecoin/gui/wallets_tab.py +++ b/src/cutecoin/gui/wallets_tab.py @@ -47,7 +47,7 @@ class WalletsTabWidget(QWidget, Ui_WalletsTab): person = Person.lookup(self.account.pubkey, self.community) membership = person.membership(self.community) renew_block = membership['blockNumber'] - last_renewal = self.community.get_block(renew_block).mediantime + last_renewal = self.community.get_block(renew_block)['medianTime'] expiration = last_renewal + parameters['sigValidity'] except MembershipNotFoundError: last_renewal = None diff --git a/src/cutecoin/gui/wot_tab.py b/src/cutecoin/gui/wot_tab.py index 4a9d5ddaa41d5b8c5b47bfa75abce725aead8ac3..b5be05f9c0113d27db6ce6e0f0973eb18ce6b87a 100644 --- a/src/cutecoin/gui/wot_tab.py +++ b/src/cutecoin/gui/wot_tab.py @@ -38,7 +38,6 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): self.graphicsView.scene().node_contact.connect(self.add_node_as_contact) self.graphicsView.scene().node_member.connect(self.identity_informations) - app.monitor.persons_watcher(community).person_changed.connect(self.handle_person_change) self.account = account self.community = community self.password_asker = password_asker diff --git a/src/cutecoin/models/identities.py b/src/cutecoin/models/identities.py index aa0692ea5fd66c8064106cb576b8e52747be4880..2548f2c923f3a6852bf561ee7fb29488b8a82748 100644 --- a/src/cutecoin/models/identities.py +++ b/src/cutecoin/models/identities.py @@ -100,7 +100,7 @@ class IdentitiesTableModel(QAbstractTableModel): try: join_block = person.membership(self.community)['blockNumber'] try: - join_date = self.community.get_block(join_block).mediantime + join_date = self.community.get_block(join_block)['medianTime'] expiration_date = join_date + parameters['sigValidity'] except NoPeerAvailable: join_date = None diff --git a/src/cutecoin/models/network.py b/src/cutecoin/models/network.py index 4c43f07266bb30461b23ccdb5385b7289d478b5e..31c801ab12c9be818a69aa0e1bcbeea4d3fc05a3 100644 --- a/src/cutecoin/models/network.py +++ b/src/cutecoin/models/network.py @@ -5,11 +5,13 @@ Created on 5 févr. 2014 ''' import logging -from ..tools.exceptions import NoPeerAvailable -from ..core.net.node import Node + from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel from PyQt5.QtGui import QColor, QFont +from ..tools.exceptions import NoPeerAvailable +from cutecoin.core.net.node import Node + class NetworkFilterProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): diff --git a/src/cutecoin/models/wallets.py b/src/cutecoin/models/wallets.py index 56a59423e11120ae8ce78b82906d6b7c24938899..8815c9224810d429f89594161f37e5a3cd794299 100644 --- a/src/cutecoin/models/wallets.py +++ b/src/cutecoin/models/wallets.py @@ -4,7 +4,7 @@ Created on 8 févr. 2014 @author: inso ''' -from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, QLocale +from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, QLocale, pyqtSlot class WalletsFilterProxyModel(QSortFilterProxyModel): @@ -61,11 +61,18 @@ class WalletsTableModel(QAbstractTableModel): ''' def __init__(self, account, community, parent=None): - ''' - Constructor - ''' + """ + + :param list of cutecoin.core.wallet.Wallet wallets: The list of wallets to display + :param cutecoin.core.community.Community community: The community to show + :param PyQt5.QtCore.QObject parent: The parent widget + :return: The model + :rtype: WalletsTableModel + """ super().__init__(parent) self.account = account + self.account.wallets_changed.connect(self.refresh_account_wallets) + self.community = community self.columns_headers = (self.tr('Name'), self.tr('Amount'), @@ -76,6 +83,25 @@ class WalletsTableModel(QAbstractTableModel): def wallets(self): return self.account.wallets + @pyqtSlot() + def refresh_account_wallets(self): + """ + Change the current wallets, reconnect the slots + """ + self.beginResetModel() + for w in self.account.wallets: + w.inner_data_changed.connect(lambda: self.refresh_wallet(w)) + self.endResetModel() + + def refresh_wallet(self, wallet): + """ + Refresh the specified wallet value + :param cutecoin.core.wallet.Wallet wallet: The wallet to refresh + """ + index = self.account.wallets.index(wallet) + if index > 0: + self.dataChanged.emit(index, index) + def rowCount(self, parent): return len(self.wallets)