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