electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

update_checker.py (6084B)


      1 # Copyright (C) 2019 The Electrum developers
      2 # Distributed under the MIT software license, see the accompanying
      3 # file LICENCE or http://www.opensource.org/licenses/mit-license.php
      4 
      5 import asyncio
      6 import base64
      7 from distutils.version import StrictVersion
      8 
      9 from PyQt5.QtCore import Qt, QThread, pyqtSignal
     10 from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QLabel, QProgressBar,
     11                              QHBoxLayout, QPushButton, QDialog)
     12 
     13 from electrum import version
     14 from electrum import constants
     15 from electrum import ecc
     16 from electrum.i18n import _
     17 from electrum.util import make_aiohttp_session
     18 from electrum.logging import Logger
     19 from electrum.network import Network
     20 
     21 
     22 class UpdateCheck(QDialog, Logger):
     23     url = "https://electrum.org/version"
     24     download_url = "https://electrum.org/#download"
     25 
     26     VERSION_ANNOUNCEMENT_SIGNING_KEYS = (
     27         "13xjmVAB1EATPP8RshTE8S8sNwwSUM9p1P",
     28     )
     29 
     30     def __init__(self, *, latest_version=None):
     31         QDialog.__init__(self)
     32         self.setWindowTitle('Electrum - ' + _('Update Check'))
     33         self.content = QVBoxLayout()
     34         self.content.setContentsMargins(*[10]*4)
     35 
     36         self.heading_label = QLabel()
     37         self.content.addWidget(self.heading_label)
     38 
     39         self.detail_label = QLabel()
     40         self.detail_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
     41         self.detail_label.setOpenExternalLinks(True)
     42         self.content.addWidget(self.detail_label)
     43 
     44         self.pb = QProgressBar()
     45         self.pb.setMaximum(0)
     46         self.pb.setMinimum(0)
     47         self.content.addWidget(self.pb)
     48 
     49         versions = QHBoxLayout()
     50         versions.addWidget(QLabel(_("Current version: {}".format(version.ELECTRUM_VERSION))))
     51         self.latest_version_label = QLabel(_("Latest version: {}".format(" ")))
     52         versions.addWidget(self.latest_version_label)
     53         self.content.addLayout(versions)
     54 
     55         self.update_view(latest_version)
     56 
     57         self.update_check_thread = UpdateCheckThread()
     58         self.update_check_thread.checked.connect(self.on_version_retrieved)
     59         self.update_check_thread.failed.connect(self.on_retrieval_failed)
     60         self.update_check_thread.start()
     61 
     62         close_button = QPushButton(_("Close"))
     63         close_button.clicked.connect(self.close)
     64         self.content.addWidget(close_button)
     65         self.setLayout(self.content)
     66         self.show()
     67 
     68     def on_version_retrieved(self, version):
     69         self.update_view(version)
     70 
     71     def on_retrieval_failed(self):
     72         self.heading_label.setText('<h2>' + _("Update check failed") + '</h2>')
     73         self.detail_label.setText(_("Sorry, but we were unable to check for updates. Please try again later."))
     74         self.pb.hide()
     75 
     76     @staticmethod
     77     def is_newer(latest_version):
     78         return latest_version > StrictVersion(version.ELECTRUM_VERSION)
     79 
     80     def update_view(self, latest_version=None):
     81         if latest_version:
     82             self.pb.hide()
     83             self.latest_version_label.setText(_("Latest version: {}".format(latest_version)))
     84             if self.is_newer(latest_version):
     85                 self.heading_label.setText('<h2>' + _("There is a new update available") + '</h2>')
     86                 url = "<a href='{u}'>{u}</a>".format(u=UpdateCheck.download_url)
     87                 self.detail_label.setText(_("You can download the new version from {}.").format(url))
     88             else:
     89                 self.heading_label.setText('<h2>' + _("Already up to date") + '</h2>')
     90                 self.detail_label.setText(_("You are already on the latest version of Electrum."))
     91         else:
     92             self.heading_label.setText('<h2>' + _("Checking for updates...") + '</h2>')
     93             self.detail_label.setText(_("Please wait while Electrum checks for available updates."))
     94 
     95 
     96 class UpdateCheckThread(QThread, Logger):
     97     checked = pyqtSignal(object)
     98     failed = pyqtSignal()
     99 
    100     def __init__(self):
    101         QThread.__init__(self)
    102         Logger.__init__(self)
    103         self.network = Network.get_instance()
    104 
    105     async def get_update_info(self):
    106         # note: Use long timeout here as it is not critical that we get a response fast,
    107         #       and it's bad not to get an update notification just because we did not wait enough.
    108         async with make_aiohttp_session(proxy=self.network.proxy, timeout=120) as session:
    109             async with session.get(UpdateCheck.url) as result:
    110                 signed_version_dict = await result.json(content_type=None)
    111                 # example signed_version_dict:
    112                 # {
    113                 #     "version": "3.9.9",
    114                 #     "signatures": {
    115                 #         "1Lqm1HphuhxKZQEawzPse8gJtgjm9kUKT4": "IA+2QG3xPRn4HAIFdpu9eeaCYC7S5wS/sDxn54LJx6BdUTBpse3ibtfq8C43M7M1VfpGkD5tsdwl5C6IfpZD/gQ="
    116                 #     }
    117                 # }
    118                 version_num = signed_version_dict['version']
    119                 sigs = signed_version_dict['signatures']
    120                 for address, sig in sigs.items():
    121                     if address not in UpdateCheck.VERSION_ANNOUNCEMENT_SIGNING_KEYS:
    122                         continue
    123                     sig = base64.b64decode(sig)
    124                     msg = version_num.encode('utf-8')
    125                     if ecc.verify_message_with_address(address=address, sig65=sig, message=msg,
    126                                                        net=constants.BitcoinMainnet):
    127                         self.logger.info(f"valid sig for version announcement '{version_num}' from address '{address}'")
    128                         break
    129                 else:
    130                     raise Exception('no valid signature for version announcement')
    131                 return StrictVersion(version_num.strip())
    132 
    133     def run(self):
    134         if not self.network:
    135             self.failed.emit()
    136             return
    137         try:
    138             update_info = asyncio.run_coroutine_threadsafe(self.get_update_info(), self.network.asyncio_loop).result()
    139         except Exception as e:
    140             self.logger.info(f"got exception: '{repr(e)}'")
    141             self.failed.emit()
    142         else:
    143             self.checked.emit(update_info)