URI: 
       tnetwork_dialog.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tnetwork_dialog.py (19698B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - lightweight Bitcoin client
            4 # Copyright (C) 2012 thomasv@gitorious
            5 #
            6 # Permission is hereby granted, free of charge, to any person
            7 # obtaining a copy of this software and associated documentation files
            8 # (the "Software"), to deal in the Software without restriction,
            9 # including without limitation the rights to use, copy, modify, merge,
           10 # publish, distribute, sublicense, and/or sell copies of the Software,
           11 # and to permit persons to whom the Software is furnished to do so,
           12 # subject to the following conditions:
           13 #
           14 # The above copyright notice and this permission notice shall be
           15 # included in all copies or substantial portions of the Software.
           16 #
           17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           24 # SOFTWARE.
           25 
           26 import socket
           27 import time
           28 from enum import IntEnum
           29 from typing import Tuple, TYPE_CHECKING
           30 
           31 from PyQt5.QtCore import Qt, pyqtSignal, QThread
           32 from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox,
           33                              QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox,
           34                              QTabWidget, QWidget, QLabel)
           35 from PyQt5.QtGui import QFontMetrics
           36 
           37 from electrum.i18n import _
           38 from electrum import constants, blockchain, util
           39 from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
           40 from electrum.network import Network
           41 from electrum.logging import get_logger
           42 
           43 from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
           44                    PasswordLineEdit)
           45 
           46 if TYPE_CHECKING:
           47     from electrum.simple_config import SimpleConfig
           48 
           49 
           50 _logger = get_logger(__name__)
           51 
           52 protocol_names = ['TCP', 'SSL']
           53 protocol_letters = 'ts'
           54 
           55 class NetworkDialog(QDialog):
           56     def __init__(self, network, config, network_updated_signal_obj):
           57         QDialog.__init__(self)
           58         self.setWindowTitle(_('Network'))
           59         self.setMinimumSize(500, 500)
           60         self.nlayout = NetworkChoiceLayout(network, config)
           61         self.network_updated_signal_obj = network_updated_signal_obj
           62         vbox = QVBoxLayout(self)
           63         vbox.addLayout(self.nlayout.layout())
           64         vbox.addLayout(Buttons(CloseButton(self)))
           65         self.network_updated_signal_obj.network_updated_signal.connect(
           66             self.on_update)
           67         util.register_callback(self.on_network, ['network_updated'])
           68 
           69     def on_network(self, event, *args):
           70         self.network_updated_signal_obj.network_updated_signal.emit(event, args)
           71 
           72     def on_update(self):
           73         self.nlayout.update()
           74 
           75 
           76 
           77 class NodesListWidget(QTreeWidget):
           78     """List of connected servers."""
           79 
           80     SERVER_ADDR_ROLE = Qt.UserRole + 100
           81     CHAIN_ID_ROLE = Qt.UserRole + 101
           82     ITEMTYPE_ROLE = Qt.UserRole + 102
           83 
           84     class ItemType(IntEnum):
           85         CHAIN = 0
           86         CONNECTED_SERVER = 1
           87         DISCONNECTED_SERVER = 2
           88         TOPLEVEL = 3
           89 
           90     def __init__(self, parent):
           91         QTreeWidget.__init__(self)
           92         self.parent = parent  # type: NetworkChoiceLayout
           93         self.setHeaderLabels([_('Server'), _('Height')])
           94         self.setContextMenuPolicy(Qt.CustomContextMenu)
           95         self.customContextMenuRequested.connect(self.create_menu)
           96 
           97     def create_menu(self, position):
           98         item = self.currentItem()
           99         if not item:
          100             return
          101         item_type = item.data(0, self.ITEMTYPE_ROLE)
          102         menu = QMenu()
          103         if item_type == self.ItemType.CONNECTED_SERVER:
          104             server = item.data(0, self.SERVER_ADDR_ROLE)  # type: ServerAddr
          105             menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server))
          106         elif item_type == self.ItemType.DISCONNECTED_SERVER:
          107             server = item.data(0, self.SERVER_ADDR_ROLE)  # type: ServerAddr
          108             def func():
          109                 self.parent.server_e.setText(server.net_addr_str())
          110                 self.parent.set_server()
          111             menu.addAction(_("Use as server"), func)
          112         elif item_type == self.ItemType.CHAIN:
          113             chain_id = item.data(0, self.CHAIN_ID_ROLE)
          114             menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(chain_id))
          115         else:
          116             return
          117         menu.exec_(self.viewport().mapToGlobal(position))
          118 
          119     def keyPressEvent(self, event):
          120         if event.key() in [ Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter ]:
          121             self.on_activated(self.currentItem(), self.currentColumn())
          122         else:
          123             QTreeWidget.keyPressEvent(self, event)
          124 
          125     def on_activated(self, item, column):
          126         # on 'enter' we show the menu
          127         pt = self.visualItemRect(item).bottomLeft()
          128         pt.setX(50)
          129         self.customContextMenuRequested.emit(pt)
          130 
          131     def update(self, *, network: Network, servers: dict, use_tor: bool):
          132         self.clear()
          133 
          134         # connected servers
          135         connected_servers_item = QTreeWidgetItem([_("Connected nodes"), ''])
          136         connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
          137         chains = network.get_blockchains()
          138         n_chains = len(chains)
          139         for chain_id, interfaces in chains.items():
          140             b = blockchain.blockchains.get(chain_id)
          141             if b is None: continue
          142             name = b.get_name()
          143             if n_chains > 1:
          144                 x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])
          145                 x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN)
          146                 x.setData(0, self.CHAIN_ID_ROLE, b.get_id())
          147             else:
          148                 x = connected_servers_item
          149             for i in interfaces:
          150                 star = ' *' if i == network.interface else ''
          151                 item = QTreeWidgetItem([f"{i.server.to_friendly_name()}" + star, '%d'%i.tip])
          152                 item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER)
          153                 item.setData(0, self.SERVER_ADDR_ROLE, i.server)
          154                 item.setToolTip(0, str(i.server))
          155                 x.addChild(item)
          156             if n_chains > 1:
          157                 connected_servers_item.addChild(x)
          158 
          159         # disconnected servers
          160         disconnected_servers_item = QTreeWidgetItem([_("Other known servers"), ""])
          161         disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
          162         connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces])
          163         protocol = PREFERRED_NETWORK_PROTOCOL
          164         for _host, d in sorted(servers.items()):
          165             if _host in connected_hosts:
          166                 continue
          167             if _host.endswith('.onion') and not use_tor:
          168                 continue
          169             port = d.get(protocol)
          170             if port:
          171                 server = ServerAddr(_host, port, protocol=protocol)
          172                 item = QTreeWidgetItem([server.net_addr_str(), ""])
          173                 item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER)
          174                 item.setData(0, self.SERVER_ADDR_ROLE, server)
          175                 disconnected_servers_item.addChild(item)
          176 
          177         self.addTopLevelItem(connected_servers_item)
          178         self.addTopLevelItem(disconnected_servers_item)
          179 
          180         connected_servers_item.setExpanded(True)
          181         for i in range(connected_servers_item.childCount()):
          182             connected_servers_item.child(i).setExpanded(True)
          183         disconnected_servers_item.setExpanded(True)
          184 
          185         # headers
          186         h = self.header()
          187         h.setStretchLastSection(False)
          188         h.setSectionResizeMode(0, QHeaderView.Stretch)
          189         h.setSectionResizeMode(1, QHeaderView.ResizeToContents)
          190 
          191         super().update()
          192 
          193 
          194 class NetworkChoiceLayout(object):
          195 
          196     def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
          197         self.network = network
          198         self.config = config
          199         self.tor_proxy = None
          200 
          201         self.tabs = tabs = QTabWidget()
          202         proxy_tab = QWidget()
          203         blockchain_tab = QWidget()
          204         tabs.addTab(blockchain_tab, _('Overview'))
          205         tabs.addTab(proxy_tab, _('Proxy'))
          206 
          207         fixed_width_hostname = 24 * char_width_in_lineedit()
          208         fixed_width_port = 6 * char_width_in_lineedit()
          209 
          210         # Proxy tab
          211         grid = QGridLayout(proxy_tab)
          212         grid.setSpacing(8)
          213 
          214         # proxy setting
          215         self.proxy_cb = QCheckBox(_('Use proxy'))
          216         self.proxy_cb.clicked.connect(self.check_disable_proxy)
          217         self.proxy_cb.clicked.connect(self.set_proxy)
          218 
          219         self.proxy_mode = QComboBox()
          220         self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
          221         self.proxy_host = QLineEdit()
          222         self.proxy_host.setFixedWidth(fixed_width_hostname)
          223         self.proxy_port = QLineEdit()
          224         self.proxy_port.setFixedWidth(fixed_width_port)
          225         self.proxy_user = QLineEdit()
          226         self.proxy_user.setPlaceholderText(_("Proxy user"))
          227         self.proxy_password = PasswordLineEdit()
          228         self.proxy_password.setPlaceholderText(_("Password"))
          229         self.proxy_password.setFixedWidth(fixed_width_port)
          230 
          231         self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
          232         self.proxy_host.editingFinished.connect(self.set_proxy)
          233         self.proxy_port.editingFinished.connect(self.set_proxy)
          234         self.proxy_user.editingFinished.connect(self.set_proxy)
          235         self.proxy_password.editingFinished.connect(self.set_proxy)
          236 
          237         self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
          238         self.proxy_host.textEdited.connect(self.proxy_settings_changed)
          239         self.proxy_port.textEdited.connect(self.proxy_settings_changed)
          240         self.proxy_user.textEdited.connect(self.proxy_settings_changed)
          241         self.proxy_password.textEdited.connect(self.proxy_settings_changed)
          242 
          243         self.tor_cb = QCheckBox(_("Use Tor Proxy"))
          244         self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
          245         self.tor_cb.hide()
          246         self.tor_cb.clicked.connect(self.use_tor_proxy)
          247 
          248         grid.addWidget(self.tor_cb, 1, 0, 1, 3)
          249         grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
          250         grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
          251         grid.addWidget(self.proxy_mode, 4, 1)
          252         grid.addWidget(self.proxy_host, 4, 2)
          253         grid.addWidget(self.proxy_port, 4, 3)
          254         grid.addWidget(self.proxy_user, 5, 2)
          255         grid.addWidget(self.proxy_password, 5, 3)
          256         grid.setRowStretch(7, 1)
          257 
          258         # Blockchain Tab
          259         grid = QGridLayout(blockchain_tab)
          260         msg =  ' '.join([
          261             _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
          262             _("This blockchain is used to verify the transactions sent by your transaction server.")
          263         ])
          264         self.status_label = QLabel('')
          265         grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
          266         grid.addWidget(self.status_label, 0, 1, 1, 3)
          267         grid.addWidget(HelpButton(msg), 0, 4)
          268 
          269         self.autoconnect_cb = QCheckBox(_('Select server automatically'))
          270         self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
          271         self.autoconnect_cb.clicked.connect(self.set_server)
          272         self.autoconnect_cb.clicked.connect(self.update)
          273         msg = ' '.join([
          274             _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
          275             _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
          276         ])
          277         grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
          278         grid.addWidget(HelpButton(msg), 1, 4)
          279 
          280         self.server_e = QLineEdit()
          281         self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
          282         self.server_e.editingFinished.connect(self.set_server)
          283         msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
          284         grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
          285         grid.addWidget(self.server_e, 2, 1, 1, 3)
          286         grid.addWidget(HelpButton(msg), 2, 4)
          287 
          288         self.height_label = QLabel('')
          289         msg = _('This is the height of your local copy of the blockchain.')
          290         grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
          291         grid.addWidget(self.height_label, 3, 1)
          292         grid.addWidget(HelpButton(msg), 3, 4)
          293 
          294         self.split_label = QLabel('')
          295         grid.addWidget(self.split_label, 4, 0, 1, 3)
          296 
          297         self.nodes_list_widget = NodesListWidget(self)
          298         grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
          299 
          300         vbox = QVBoxLayout()
          301         vbox.addWidget(tabs)
          302         self.layout_ = vbox
          303         # tor detector
          304         self.td = td = TorDetector()
          305         td.found_proxy.connect(self.suggest_proxy)
          306         td.start()
          307 
          308         self.fill_in_proxy_settings()
          309         self.update()
          310 
          311     def check_disable_proxy(self, b):
          312         if not self.config.is_modifiable('proxy'):
          313             b = False
          314         for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
          315             w.setEnabled(b)
          316 
          317     def enable_set_server(self):
          318         if self.config.is_modifiable('server'):
          319             enabled = not self.autoconnect_cb.isChecked()
          320             self.server_e.setEnabled(enabled)
          321         else:
          322             for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
          323                 w.setEnabled(False)
          324 
          325     def update(self):
          326         net_params = self.network.get_parameters()
          327         server = net_params.server
          328         auto_connect = net_params.auto_connect
          329         if not self.server_e.hasFocus():
          330             self.server_e.setText(server.to_friendly_name())
          331         self.autoconnect_cb.setChecked(auto_connect)
          332 
          333         height_str = "%d "%(self.network.get_local_height()) + _('blocks')
          334         self.height_label.setText(height_str)
          335         n = len(self.network.get_interfaces())
          336         status = _("Connected to {0} nodes.").format(n) if n > 1 else _("Connected to {0} node.").format(n) if n == 1 else _("Not connected")
          337         self.status_label.setText(status)
          338         chains = self.network.get_blockchains()
          339         if len(chains) > 1:
          340             chain = self.network.blockchain()
          341             forkpoint = chain.get_max_forkpoint()
          342             name = chain.get_name()
          343             msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
          344             msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
          345             msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
          346         else:
          347             msg = ''
          348         self.split_label.setText(msg)
          349         self.nodes_list_widget.update(network=self.network,
          350                                       servers=self.network.get_servers(),
          351                                       use_tor=self.tor_cb.isChecked())
          352         self.enable_set_server()
          353 
          354     def fill_in_proxy_settings(self):
          355         proxy_config = self.network.get_parameters().proxy
          356         if not proxy_config:
          357             proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
          358 
          359         b = proxy_config.get('mode') != "none"
          360         self.check_disable_proxy(b)
          361         if b:
          362             self.proxy_cb.setChecked(True)
          363             self.proxy_mode.setCurrentIndex(
          364                 self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
          365 
          366         self.proxy_host.setText(proxy_config.get("host"))
          367         self.proxy_port.setText(proxy_config.get("port"))
          368         self.proxy_user.setText(proxy_config.get("user", ""))
          369         self.proxy_password.setText(proxy_config.get("password", ""))
          370 
          371     def layout(self):
          372         return self.layout_
          373 
          374     def follow_branch(self, chain_id):
          375         self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
          376         self.update()
          377 
          378     def follow_server(self, server: ServerAddr):
          379         self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
          380         self.update()
          381 
          382     def accept(self):
          383         pass
          384 
          385     def set_server(self):
          386         net_params = self.network.get_parameters()
          387         try:
          388             server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
          389             if not server: raise Exception("failed to parse")
          390         except Exception:
          391             return
          392         net_params = net_params._replace(server=server,
          393                                          auto_connect=self.autoconnect_cb.isChecked())
          394         self.network.run_from_another_thread(self.network.set_parameters(net_params))
          395 
          396     def set_proxy(self):
          397         net_params = self.network.get_parameters()
          398         if self.proxy_cb.isChecked():
          399             proxy = { 'mode':str(self.proxy_mode.currentText()).lower(),
          400                       'host':str(self.proxy_host.text()),
          401                       'port':str(self.proxy_port.text()),
          402                       'user':str(self.proxy_user.text()),
          403                       'password':str(self.proxy_password.text())}
          404         else:
          405             proxy = None
          406             self.tor_cb.setChecked(False)
          407         net_params = net_params._replace(proxy=proxy)
          408         self.network.run_from_another_thread(self.network.set_parameters(net_params))
          409 
          410     def suggest_proxy(self, found_proxy):
          411         if found_proxy is None:
          412             self.tor_cb.hide()
          413             return
          414         self.tor_proxy = found_proxy
          415         self.tor_cb.setText("Use Tor proxy at port " + str(found_proxy[1]))
          416         if (self.proxy_cb.isChecked()
          417                 and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
          418                 and self.proxy_host.text() == "127.0.0.1"
          419                 and self.proxy_port.text() == str(found_proxy[1])):
          420             self.tor_cb.setChecked(True)
          421         self.tor_cb.show()
          422 
          423     def use_tor_proxy(self, use_it):
          424         if not use_it:
          425             self.proxy_cb.setChecked(False)
          426         else:
          427             socks5_mode_index = self.proxy_mode.findText('SOCKS5')
          428             if socks5_mode_index == -1:
          429                 _logger.info("can't find proxy_mode 'SOCKS5'")
          430                 return
          431             self.proxy_mode.setCurrentIndex(socks5_mode_index)
          432             self.proxy_host.setText("127.0.0.1")
          433             self.proxy_port.setText(str(self.tor_proxy[1]))
          434             self.proxy_user.setText("")
          435             self.proxy_password.setText("")
          436             self.tor_cb.setChecked(True)
          437             self.proxy_cb.setChecked(True)
          438         self.check_disable_proxy(use_it)
          439         self.set_proxy()
          440 
          441     def proxy_settings_changed(self):
          442         self.tor_cb.setChecked(False)
          443 
          444 
          445 class TorDetector(QThread):
          446     found_proxy = pyqtSignal(object)
          447 
          448     def __init__(self):
          449         QThread.__init__(self)
          450 
          451     def run(self):
          452         # Probable ports for Tor to listen at
          453         ports = [9050, 9150]
          454         while True:
          455             for p in ports:
          456                 net_addr = ("127.0.0.1", p)
          457                 if TorDetector.is_tor_port(net_addr):
          458                     self.found_proxy.emit(net_addr)
          459                     break
          460             else:
          461                 self.found_proxy.emit(None)
          462             time.sleep(10)
          463 
          464     @staticmethod
          465     def is_tor_port(net_addr: Tuple[str, int]) -> bool:
          466         try:
          467             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
          468                 s.settimeout(0.1)
          469                 s.connect(net_addr)
          470                 # Tor responds uniquely to HTTP-like requests
          471                 s.send(b"GET\n")
          472                 if b"Tor is not an HTTP Proxy" in s.recv(1024):
          473                     return True
          474         except socket.error:
          475             pass
          476         return False