From 0f482d80bcc7a05dbeade67de26bd3a5c10edbbe Mon Sep 17 00:00:00 2001
From: Inso <insomniak.fr@gmail.com>
Date: Fri, 21 Aug 2015 09:15:11 +0200
Subject: [PATCH] Handling transaction with the multibranch feature

---
 src/cutecoin/core/account.py         |   4 +-
 src/cutecoin/core/app.py             |   4 +-
 src/cutecoin/core/config.py          |   2 +-
 src/cutecoin/core/net/network.py     |  13 ++--
 src/cutecoin/core/net/node.py        |   2 +-
 src/cutecoin/core/transfer.py        |  48 +++++--------
 src/cutecoin/core/txhistory.py       | 103 ++++++++++++++++-----------
 src/cutecoin/core/wallet.py          |  12 ++--
 src/cutecoin/gui/community_tab.py    |   2 +-
 src/cutecoin/gui/transactions_tab.py |   4 +-
 src/cutecoin/models/txhistory.py     |  34 ++++++---
 11 files changed, 124 insertions(+), 104 deletions(-)

diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py
index a0802e38..1cfa5fb2 100644
--- a/src/cutecoin/core/account.py
+++ b/src/cutecoin/core/account.py
@@ -230,7 +230,7 @@ class Account(QObject):
         self.communities.append(community)
         return community
 
-    def refresh_transactions(self, community):
+    def refresh_transactions(self, app, community):
         """
         Refresh the local account cache
         This needs n_wallets * n_communities cache refreshing to end
@@ -268,7 +268,7 @@ class Account(QObject):
             for w in self.wallets:
                 w.refresh_progressed.connect(progressing)
                 w.refresh_finished.connect(wallet_finished)
-                w.init_cache(community)
+                w.init_cache(app, community)
                 w.refresh_transactions(community, received_list)
 
     def set_display_referential(self, index):
diff --git a/src/cutecoin/core/app.py b/src/cutecoin/core/app.py
index 227459b4..6e8344f1 100644
--- a/src/cutecoin/core/app.py
+++ b/src/cutecoin/core/app.py
@@ -268,14 +268,14 @@ class Application(QObject):
 
         for wallet in account.wallets:
             for c in account.communities:
-                wallet.init_cache(c)
+                wallet.init_cache(self, c)
             wallet_path = os.path.join(config.parameters['home'],
                                         account.name, '__cache__', wallet.pubkey + "_wal")
             if os.path.exists(wallet_path):
                 with open(wallet_path, 'r') as json_data:
                     data = json.load(json_data)
                 if 'version' in data and data['version'] == __version__:
-                    wallet.load_caches(data)
+                    wallet.load_caches(self, data)
                 else:
                     os.remove(wallet_path)
 
diff --git a/src/cutecoin/core/config.py b/src/cutecoin/core/config.py
index 8a4202d2..d05225ba 100644
--- a/src/cutecoin/core/config.py
+++ b/src/cutecoin/core/config.py
@@ -49,6 +49,6 @@ def parse_arguments(argv):
             level=logging.INFO)
     else:
         logging.getLogger().propagate = False
-
+    logging.getLogger('quamash').setLevel(logging.INFO)
     logfile = FileHandler(path.join(parameters['home'], 'cutecoin.log'))
     logging.getLogger().addHandler(logfile)
diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py
index 9f83fb0d..3f959b21 100644
--- a/src/cutecoin/core/net/network.py
+++ b/src/cutecoin/core/net/network.py
@@ -9,6 +9,7 @@ import logging
 import time
 import asyncio
 from ucoinpy.documents.peer import Peer
+from ucoinpy.documents.block import Block
 
 from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
 
@@ -84,6 +85,7 @@ class Network(QObject):
             node = Node.from_json(network_manager, currency, data)
             nodes.append(node)
         network = cls(network_manager, currency, nodes)
+        # We block the signals until loading the nodes cache
         return network
 
     def jsonify(self):
@@ -159,8 +161,11 @@ class Network(QObject):
         Get the latest block considered valid
         It is the most frequent last block of every known nodes
         """
-        blocks = [n.block_hash for n in self.nodes]
-        return max(set(blocks), key=blocks.count)
+        blocks = [n.block_hash for n in self.nodes if n.block_hash != Block.Empty_Hash]
+        if len(blocks) > 0:
+            return max(set(blocks), key=blocks.count)
+        else:
+            return Block.Empty_Hash
 
     def add_node(self, node):
         """
@@ -235,8 +240,8 @@ class Network(QObject):
                 self.nodes.remove(node)
                 self.nodes_changed.emit()
 
-        logging.debug("{0} -> {1}".format(self.latest_block_number, self.latest_block_number))
-        if self._block_found != self.latest_block_hash:
+        logging.debug("{0} -> {1}".format(self._block_found[:10], self.latest_block_hash[:10]))
+        if self._block_found != self.latest_block_hash and node.state == Node.ONLINE:
             logging.debug("Latest block changed : {0}".format(self.latest_block_number))
             self._block_found = self.latest_block_hash
             self.new_block_mined.emit(self.latest_block_number)
diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py
index ff6afaea..58ea1c8d 100644
--- a/src/cutecoin/core/net/node.py
+++ b/src/cutecoin/core/net/node.py
@@ -115,7 +115,7 @@ class Node(QObject):
 
         node = cls(network_manager, peer.currency,
                     [Endpoint.from_inline(e.inline()) for e in peer.endpoints],
-                   "", pubkey, 0,
+                   "", pubkey, 0, Block.Empty_Hash,
                    Node.ONLINE, time.time(),
                    {'root': "", 'leaves': []},
                    "", "")
diff --git a/src/cutecoin/core/transfer.py b/src/cutecoin/core/transfer.py
index f7785927..b0807af1 100644
--- a/src/cutecoin/core/transfer.py
+++ b/src/cutecoin/core/transfer.py
@@ -14,8 +14,8 @@ class Transfer(QObject):
     """
     A transfer is the lifecycle of a transaction.
     TO_SEND means the transaction wasn't sent yet
-    AWAITING means the transaction is waiting for a blockchain validation
-    VALIDATED means the transaction was registered in the blockchain
+    AWAITING means the transaction is waiting to reach K blockchain validation
+    VALIDATED means the transaction was validated locally and is considered present in the blockchain
     REFUSED means the transaction took too long to be registered in the blockchain,
     therefore it is considered as refused
     DROPPED means the transaction was canceled locally. It can still be validated
@@ -23,6 +23,7 @@ class Transfer(QObject):
     """
     TO_SEND = 0
     AWAITING = 1
+    VALIDATING = 4
     VALIDATED = 2
     REFUSED = 3
     DROPPED = 5
@@ -68,11 +69,11 @@ class Transfer(QObject):
         return cls(None, Transfer.TO_SEND, metadata)
 
     @classmethod
-    def create_validated(cls, hash, metadata):
+    def create_from_blockchain(cls, hash, state, metadata):
         """
-        Create a new transfer in a "VALIDATED" state.
+        Create a new transfer sent from another cutecoin instance
         """
-        return cls(hash, Transfer.VALIDATED, metadata)
+        return cls(hash, state, metadata)
 
     @classmethod
     def load(cls, data):
@@ -137,7 +138,7 @@ class Transfer(QObject):
                     return
             self.broadcast_error.emit(r.error(), strdata)
 
-    def check_registered(self, tx, block, time):
+    def check_registered(self, tx, block, time, data_validation):
         """
         Check if the transfer was registered in a block.
         Update the transfer state to VALIDATED if it was registered.
@@ -147,19 +148,23 @@ class Transfer(QObject):
         :param int time: The time of the block
         """
         if tx.signed_raw() == self.txdoc.signed_raw():
-            self.state = Transfer.VALIDATED
-            self._metadata['block'] = block
-            self._metadata['time'] = time
+            if self.state == Transfer.AWAITING:
+                self.state = Transfer.VALIDATING
+                self._metadata['block'] = block
+                self._metadata['time'] = time
+            elif self.state == Transfer.VALIDATING and \
+                    self._metadata['block'] - block > data_validation:
+                self.state = Transfer.VALIDATED
 
-    def check_refused(self, block):
+    def check_refused(self, time, block_time, mediantime_blocks):
         """
         Check if the transfer was refused
-        If more than 15 blocks were mined since the transaction
+        If more than block_time*15 seconds passed since
         transfer, it is considered as refused.
 
         :param int block: The current block number
         """
-        if block > self._metadata['block'] + 15:
+        if time > self._metadata['time'] + block_time*mediantime_blocks*10:
             self.state = Transfer.REFUSED
 
     def drop(self):
@@ -169,22 +174,3 @@ class Transfer(QObject):
         """
         self.state = Transfer.DROPPED
 
-
-class Received(Transfer):
-    def __init__(self, hash, metadata):
-        """
-        A transfer were the receiver is the local user.
-
-        :param txdoc: The transaction document of the received transfer
-        :param metadata: The metadata of the transfer
-        """
-        super().__init__(hash, Transfer.VALIDATED, metadata)
-
-    @classmethod
-    def load(cls, data):
-        """
-        Create a transfer from a dict in json format.
-
-        :param data: The transfer as a dict in json format
-        """
-        return cls(data['hash'], data['metadata'])
diff --git a/src/cutecoin/core/txhistory.py b/src/cutecoin/core/txhistory.py
index 2a650988..dfab7c97 100644
--- a/src/cutecoin/core/txhistory.py
+++ b/src/cutecoin/core/txhistory.py
@@ -1,14 +1,16 @@
 import asyncio
 import logging
-from .transfer import Transfer, Received
+from .transfer import Transfer
 from ucoinpy.documents.transaction import InputSource, OutputSource
 from ..tools.exceptions import LookupFailureError
 from .net.api import bma as qtbma
 
+
 class TxHistory():
-    def __init__(self, wallet):
+    def __init__(self, app, wallet):
         self._latest_block = 0
         self.wallet = wallet
+        self.app = app
         self._stop_coroutines = False
 
         self._transfers = []
@@ -28,10 +30,7 @@ class TxHistory():
 
         data_sent = data['transfers']
         for s in data_sent:
-            if s['metadata']['issuer'] == self.wallet.pubkey:
-                self._transfers.append(Transfer.load(s))
-            else:
-                self._transfers.append(Received.load(s))
+            self._transfers.append(Transfer.load(s))
 
         for s in data['sources']:
             self.available_sources.append(InputSource.from_inline(s['inline']))
@@ -72,12 +71,17 @@ class TxHistory():
         self._stop_coroutines = True
 
     @asyncio.coroutine
-    def _parse_transaction(self, community, txdata, received_list, txid):
+    def _parse_transaction(self, community, txdata, received_list, txid, current_block):
         tx_outputs = [OutputSource.from_inline(o) for o in txdata['outputs']]
         receivers = [o.pubkey for o in tx_outputs
                      if o.pubkey != txdata['issuers'][0]]
 
         block_number = txdata['block_number']
+        if block_number + self.app.preferences['data_validation'] >= current_block:
+            state = Transfer.VALIDATED
+        else:
+            state = Transfer.VALIDATING
+
         mediantime = txdata['time']
         logging.debug(txdata)
 
@@ -109,35 +113,44 @@ class TxHistory():
                      if i == self.wallet.pubkey]) > 0
         in_outputs = len([o for o in tx_outputs
                        if o.pubkey == self.wallet.pubkey]) > 0
-
-        # If the wallet pubkey is in the issuers we sent this transaction
-        if in_issuers:
-            outputs = [o for o in tx_outputs
-                       if o.pubkey != self.wallet.pubkey]
-            amount = 0
-            for o in outputs:
-                amount += o.amount
-            metadata['amount'] = amount
-
-            awaiting = [t for t in self._transfers
-                        if t.state == Transfer.AWAITING]
-            # We check if the transaction correspond to one we sent
-            if txdata['hash'] not in [t['hash'] for t in awaiting]:
-                transfer = Transfer.create_validated(txdata['hash'],
+        awaiting = [t for t in self._transfers
+                    if t.state in (Transfer.AWAITING, Transfer.VALIDATING)]
+
+        # We check if the transaction correspond to one we sent
+        # but not from this cutecoin Instance
+        if txdata['hash'] not in [t.metadata['hash'] for t in awaiting]:
+            # If the wallet pubkey is in the issuers we sent this transaction
+            if in_issuers:
+                outputs = [o for o in tx_outputs
+                           if o.pubkey != self.wallet.pubkey]
+                amount = 0
+                for o in outputs:
+                    amount += o.amount
+                metadata['amount'] = amount
+                transfer = Transfer.create_from_blockchain(txdata['hash'],
+                                                           state,
                                                      metadata.copy())
                 return transfer
-        # If we are not in the issuers,
-        # maybe it we are in the recipients of this transaction
-        elif in_outputs:
-            outputs = [o for o in tx_outputs
-                       if o.pubkey == self.wallet.pubkey]
-            amount = 0
-            for o in outputs:
-                amount += o.amount
-            metadata['amount'] = amount
-            received = Received(txdata['hash'], metadata.copy())
-            received_list.append(received)
-            return received
+            # If we are not in the issuers,
+            # maybe it we are in the recipients of this transaction
+            elif in_outputs:
+                outputs = [o for o in tx_outputs
+                           if o.pubkey == self.wallet.pubkey]
+                amount = 0
+                for o in outputs:
+                    amount += o.amount
+                metadata['amount'] = amount
+
+                if txdata['hash'] not in [t['hash'] for t in awaiting]:
+                    transfer = Transfer.create_from_blockchain(txdata['hash'],
+                                                               state,
+                                                         metadata.copy())
+                    received_list.append(transfer)
+                    return transfer
+        else:
+            transfer = [t for t in awaiting if t.metadata['hash'] == txdata['hash']][0]
+            transfer.check_registered(txdata['hash'], current_block, mediantime,
+                                      self.app.preferences['data_validation'])
         return None
 
     @asyncio.coroutine
@@ -148,9 +161,11 @@ class TxHistory():
         :param cutecoin.core.Community community: The community
         :param list received_list: List of transactions received
         """
-        parsed_block = self.latest_block
-        current_block = community.network.latest_block_number
-        logging.debug("Refresh from : {0} to {1}".format(self.latest_block, current_block))
+        current_block = yield from community.bma_access.future_request(qtbma.blockchain.Block,
+                                req_args={'number': community.network.latest_block_number})
+
+        parsed_block = min(self.latest_block, current_block['number'] - self.app.preferences['data_validation'])
+        logging.debug("Refresh from : {0} to {1}".format(self.latest_block, current_block['number']))
         dividends_data = qtbma.ud.History.null_value
         while dividends_data == qtbma.ud.History.null_value:
             dividends_data = yield from community.bma_access.future_request(qtbma.ud.History,
@@ -166,7 +181,7 @@ class TxHistory():
         # Lets look if transactions took too long to be validated
         awaiting = [t for t in self._transfers
                     if t.state == Transfer.AWAITING]
-        while parsed_block < current_block:
+        while parsed_block < current_block['number']:
             tx_history = qtbma.tx.history.Blocks.null_value
             while tx_history == qtbma.tx.history.Blocks.null_value:
                 tx_history = yield from community.bma_access.future_request(qtbma.tx.history.Blocks,
@@ -191,23 +206,25 @@ class TxHistory():
                 if len(txdata['issuers']) == 0:
                     logging.debug("Error with : {0}, from {1} to {2}".format(self.wallet.pubkey,
                                                                              parsed_block,
-                                                                             current_block))
+                                                                             current_block['number']))
                 else:
                     transfer = yield from self._parse_transaction(community, txdata, received_list, udid + txid)
                     if transfer:
                         new_transfers.append(transfer)
 
-            self.wallet.refresh_progressed.emit(parsed_block, current_block, self.wallet.pubkey)
+            self.wallet.refresh_progressed.emit(parsed_block, current_block['number'], self.wallet.pubkey)
             parsed_block += 100
 
-        if current_block > self.latest_block:
+        if current_block['number'] > self.latest_block:
             self.available_sources = yield from self.wallet.future_sources(community)
             if self._stop_coroutines:
                 return
-            self.latest_block = current_block
+            self.latest_block = current_block['number']
 
         for transfer in awaiting:
-            transfer.check_refused(current_block)
+            transfer.check_refused(current_block['medianTime'],
+                                   community.parameters['avgGenTime'],
+                                   community.parameters['medianTimeBlocks'])
 
         self._transfers = self._transfers + new_transfers
         self._dividends = self._dividends + new_dividends
diff --git a/src/cutecoin/core/wallet.py b/src/cutecoin/core/wallet.py
index 25cdba27..f7ea800b 100644
--- a/src/cutecoin/core/wallet.py
+++ b/src/cutecoin/core/wallet.py
@@ -10,7 +10,7 @@ from ucoinpy.key import SigningKey
 from .net.api import bma as qtbma
 from .net.api.bma import PROTOCOL_VERSION
 from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable, LookupFailureError
-from .transfer import Transfer, Received
+from .transfer import Transfer
 from .txhistory import TxHistory
 from .registry import IdentitiesRegistry, Identity
 
@@ -75,7 +75,7 @@ class Wallet(QObject):
         name = json_data['name']
         return cls(walletid, pubkey, name, identities_registry)
 
-    def load_caches(self, json_data):
+    def load_caches(self, app, json_data):
         """
         Load this wallet caches.
         Each cache correspond to one different community.
@@ -84,7 +84,7 @@ class Wallet(QObject):
         """
         for currency in json_data:
             if currency != 'version':
-                self.caches[currency] = TxHistory(self)
+                self.caches[currency] = TxHistory(app, self)
                 self.caches[currency].load_from_json(json_data[currency])
 
     def jsonify_caches(self):
@@ -98,14 +98,14 @@ class Wallet(QObject):
             data[currency] = self.caches[currency].jsonify()
         return data
 
-    def init_cache(self, community):
+    def init_cache(self, app, community):
         """
         Init the cache of this wallet for the specified community.
 
         :param community: The community to refresh its cache
         """
         if community.currency not in self.caches:
-            self.caches[community.currency] = TxHistory(self)
+            self.caches[community.currency] = TxHistory(app, self)
 
     def refresh_transactions(self, community, received_list):
         """
@@ -258,7 +258,7 @@ class Wallet(QObject):
         except LookupFailureError as e:
             receiver_uid = ""
 
-        metadata = {'block': block_number,
+        metadata = {'block': None,
                     'time': time,
                     'amount': amount,
                     'issuer': key.pubkey,
diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py
index 6928f1e8..491c7518 100644
--- a/src/cutecoin/gui/community_tab.py
+++ b/src/cutecoin/gui/community_tab.py
@@ -145,7 +145,7 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget):
         self.certify_identity(person)
 
     def identity_informations(self, person):
-        dialog = MemberDialog(none, self.account, self.community, person)
+        dialog = MemberDialog(None, self.account, self.community, person)
         dialog.exec_()
 
     def add_identity_as_contact(self, person):
diff --git a/src/cutecoin/gui/transactions_tab.py b/src/cutecoin/gui/transactions_tab.py
index ffc7c825..be4b11b4 100644
--- a/src/cutecoin/gui/transactions_tab.py
+++ b/src/cutecoin/gui/transactions_tab.py
@@ -80,7 +80,7 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget):
             self.progressbar.setMaximum(maximum)
         self.app.current_account.loading_progressed.connect(progressing)
         self.app.current_account.loading_finished.connect(self.stop_progress)
-        self.app.current_account.refresh_transactions(self.community)
+        self.app.current_account.refresh_transactions(self.app, self.community)
         self.progressbar.show()
 
     @pyqtSlot(list)
@@ -202,7 +202,7 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget):
 
     def send_again(self):
         transfer = self.sender().data()
-        dialog = TransferMoneyDialog(self.app.current_account,
+        dialog = TransferMoneyDialog(self.app, self.app.current_account,
                                      self.password_asker)
         dialog.accepted.connect(self.currency_tab.refresh_wallets)
         sender = transfer.metadata['issuer']
diff --git a/src/cutecoin/models/txhistory.py b/src/cutecoin/models/txhistory.py
index 845f1c27..6b07f369 100644
--- a/src/cutecoin/models/txhistory.py
+++ b/src/cutecoin/models/txhistory.py
@@ -6,7 +6,7 @@ Created on 5 févr. 2014
 
 import datetime
 import logging
-from ..core.transfer import Transfer, Received
+from ..core.transfer import Transfer
 from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \
     QDateTime, QLocale, QModelIndex
 
@@ -66,7 +66,7 @@ class TxFilterProxyModel(QSortFilterProxyModel):
         return in_period(date)
 
     def columnCount(self, parent):
-        return self.sourceModel().columnCount(None) - 3
+        return self.sourceModel().columnCount(None) - 4
 
     def setSourceModel(self, sourceModel):
         self.community = sourceModel.community
@@ -147,6 +147,13 @@ class TxFilterProxyModel(QSortFilterProxyModel):
                 return Qt.AlignCenter
 
         if role == Qt.ToolTipRole:
+            if state_data == Transfer.VALIDATING:
+                block_col = model.columns_types.index('block_number')
+                block_index = model.index(source_index.row(), block_col)
+                block_data = model.data(block_index, Qt.DisplayRole)
+                return "{0} / {1} validations".format(self.community.network.latest_block_number - block_data,
+                                                      self.app.preferences['data_validation'])
+
             if source_index.column() == self.sourceModel().columns_types.index('date'):
                 return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate)
 
@@ -177,7 +184,8 @@ class HistoryTableModel(QAbstractTableModel):
             'comment',
             'state',
             'txid',
-            'pubkey'
+            'pubkey',
+            'block_number'
         )
 
         self.column_headers = (
@@ -187,8 +195,9 @@ class HistoryTableModel(QAbstractTableModel):
             self.tr('Deposit'),
             self.tr('Comment'),
             'State',
-            'TXID'
-            'Pubkey'
+            'TXID',
+            'Pubkey',
+            'Block Number'
         )
 
     @property
@@ -211,10 +220,11 @@ class HistoryTableModel(QAbstractTableModel):
 
         date_ts = transfer.metadata['time']
         txid = transfer.metadata['txid']
+        block_number = transfer.metadata['block']
 
         return (date_ts, sender, "", amount,
                 comment, transfer.state, txid,
-                transfer.metadata['issuer'])
+                transfer.metadata['issuer'], block_number)
 
     def data_sent(self, transfer):
         amount = transfer.metadata['amount']
@@ -228,10 +238,11 @@ class HistoryTableModel(QAbstractTableModel):
 
         date_ts = transfer.metadata['time']
         txid = transfer.metadata['txid']
+        block_number = transfer.metadata['block']
 
         return (date_ts, receiver, amount,
                 "", comment, transfer.state, txid,
-                transfer.metadata['receiver'])
+                transfer.metadata['receiver'], block_number)
 
     def data_dividend(self, dividend):
         amount = dividend['amount']
@@ -247,10 +258,11 @@ class HistoryTableModel(QAbstractTableModel):
         self.beginResetModel()
         self.transfers_data = []
         for transfer in self.transfers:
-            if type(transfer) is Received:
-                self.transfers_data.append(self.data_received(transfer))
-            elif type(transfer) is Transfer:
-                self.transfers_data.append(self.data_sent(transfer))
+            if type(transfer) is Transfer:
+                if transfer.metadata['issuer'] == self.account.pubkey:
+                    self.transfers_data.append(self.data_sent(transfer))
+                else:
+                    self.transfers_data.append(self.data_received(transfer))
             elif type(transfer) is dict:
                 self.transfers_data.append(self.data_dividend(transfer))
         self.endResetModel()
-- 
GitLab