From 0d9041a8a647a6b14b213f23c9addfe50d9f36ac Mon Sep 17 00:00:00 2001 From: Mitchell Olsthoorn Date: Thu, 25 Jan 2018 08:47:38 +0100 Subject: [PATCH] One-way communication offline funds transfer using QR-code (#3358) One-way communication offline funds transfer using QR-code --- .../Modules/restapi/trustchain_endpoint.py | 70 +++++++++- .../Community/Triblerchain/test_community.py | 62 +++++++++ .../Trustchain/test_trustchain_utilities.py | 8 +- .../RestApi/test_trustchain_endpoint.py | 92 +++++++++++++ Tribler/community/triblerchain/community.py | 57 +++++++- TriblerGUI/qt_resources/mainwindow.ui | 119 +++++++++++++++- TriblerGUI/widgets/settingspage.py | 127 +++++++++++++++++- 7 files changed, 522 insertions(+), 13 deletions(-) diff --git a/Tribler/Core/Modules/restapi/trustchain_endpoint.py b/Tribler/Core/Modules/restapi/trustchain_endpoint.py index 1300288a8fb..a6847129a70 100644 --- a/Tribler/Core/Modules/restapi/trustchain_endpoint.py +++ b/Tribler/Core/Modules/restapi/trustchain_endpoint.py @@ -13,7 +13,11 @@ class TrustchainEndpoint(resource.Resource): def __init__(self, session): resource.Resource.__init__(self) - child_handler_dict = {"statistics": TrustchainStatsEndpoint, "blocks": TrustchainBlocksEndpoint} + child_handler_dict = { + "statistics": TrustchainStatsEndpoint, + "blocks": TrustchainBlocksEndpoint, + "bootstrap": TrustchainBootstrapEndpoint + } for path, child_cls in child_handler_dict.iteritems(): self.putChild(path, child_cls(session)) @@ -165,3 +169,67 @@ def render_GET(self, request): blocks = mc_community.persistence.get_latest_blocks(self.identity.decode("HEX"), limit_blocks) return json.dumps({"blocks": [dict(block) for block in blocks]}) + + +class TrustchainBootstrapEndpoint(TrustchainBaseEndpoint): + """ + Bootstrap a new identity and transfer some bandwidth tokens to the new key. + """ + + def render_GET(self, request): + """ + .. http:get:: /trustchain/bootstrap?amount=int + + A GET request to this endpoint generates a new identity and transfers bandwidth tokens to it. + The amount specifies how much tokens need to be emptied into the new identity + + **Example request**: + + .. sourcecode:: none + + curl -X GET http://localhost:8085/trustchain/bootstrap?amount=1000 + + **Example response**: + + .. sourcecode:: javascript + + { + "private_key": "TGliTmFDTFNLOmC4BR7otCpn+NzTBAFwKdSJdpT0KG9Zy5vPGX6s3rDXmNiDoGKyToLeYYB88vj9\nRj5NW + pbNf/ldcixYZ2YxQ7Q=\n", + "transaction": { + "down": 0, + "up": 1000 + }, + "block": { + "block_hash": "THJxNlKWMQG1Tio+Yz5CUCrnWahcyk6TDVfRLQf7w6M=\n", + "sequence_number": 1 + } + } + """ + + mc_community = self.get_trustchain_community() + if not mc_community: + request.setResponseCode(http.NOT_FOUND) + return json.dumps({"error": "trustchain community not found"}) + + available_tokens = mc_community.get_bandwidth_tokens() + + if 'amount' in request.args: + try: + amount = int(request.args['amount'][0]) + except ValueError: + request.setResponseCode(http.BAD_REQUEST) + return json.dumps({"error": "Provided token amount is not a number"}) + + if amount <= 0: + request.setResponseCode(http.BAD_REQUEST) + return json.dumps({"error": "Provided token amount is zero or negative"}) + else: + amount = available_tokens + + if amount <= 0 or amount > available_tokens: + request.setResponseCode(http.BAD_REQUEST) + return json.dumps({"error": "Not enough bandwidth tokens available"}) + + result = mc_community.bootstrap_new_identity(amount) + return json.dumps(result) diff --git a/Tribler/Test/Community/Triblerchain/test_community.py b/Tribler/Test/Community/Triblerchain/test_community.py index d5e8b03ce01..b9836af036f 100644 --- a/Tribler/Test/Community/Triblerchain/test_community.py +++ b/Tribler/Test/Community/Triblerchain/test_community.py @@ -386,3 +386,65 @@ def test_get_default_trust(self): other_trust = blockingCallFromThread(reactor, other.community.get_trust, node.community.my_member) self.assertEqual(node_trust, 1) self.assertEqual(other_trust, 1) + + def test_get_bandwidth_tokens_for_self(self): + """ + Test that the bandwidth tokens the own node has is the upload - the download total of all blocks. + """ + # Arrange + node, other = self.create_nodes(2) + transaction = {'up': 10, 'down': 5} + transaction2 = {'up': 5, 'down': 10} + TestTriblerChainCommunity.create_block(node, other, self._create_target(node, other), transaction) + TestTriblerChainCommunity.create_block(other, node, self._create_target(other, node), transaction2) + + # Get statistics + node_trust = blockingCallFromThread(reactor, node.community.get_bandwidth_tokens, node.community.my_member) + other_trust = blockingCallFromThread(reactor, other.community.get_bandwidth_tokens, other.community.my_member) + self.assertEqual(node_trust, 5) + self.assertEqual(other_trust, -5) + + def test_get_bandwidth_tokens(self): + """ + Test that the bandwidth tokens nodes have is the upload - the download total of all blocks. + """ + # Arrange + node, other = self.create_nodes(2) + transaction = {'up': 10, 'down': 5} + transaction2 = {'up': 5, 'down': 10} + TestTriblerChainCommunity.create_block(node, other, self._create_target(node, other), transaction) + TestTriblerChainCommunity.create_block(other, node, self._create_target(other, node), transaction2) + + # Get statistics + node_trust = blockingCallFromThread(reactor, node.community.get_bandwidth_tokens, other.community.my_member) + other_trust = blockingCallFromThread(reactor, other.community.get_bandwidth_tokens, node.community.my_member) + self.assertEqual(node_trust, -5) + self.assertEqual(other_trust, 5) + + def test_get_default_bandwidth_tokens(self): + """ + Test that the bandwidth token amount for nodes without blocks is 0. + """ + # Arrange + node, other = self.create_nodes(2) + + # Get statistics + node_trust = blockingCallFromThread(reactor, node.community.get_bandwidth_tokens, other.community.my_member) + other_trust = blockingCallFromThread(reactor, other.community.get_bandwidth_tokens, node.community.my_member) + self.assertEqual(node_trust, 0) + self.assertEqual(other_trust, 0) + + def test_bootstrapping(self): + """ + Test that bootstrapping process works. + """ + # Arrange + node, = self.create_nodes(1) + + node_bootstrap = blockingCallFromThread(reactor, node.community.bootstrap_new_identity, 100) + self.assertNotEqual(node_bootstrap['private_key'], + node.community.my_member.private_key.key_to_bin().encode('base64')) + self.assertEqual(node_bootstrap['transaction']['up'], 100) + self.assertEqual(node_bootstrap['transaction']['down'], 0) + self.assertEqual(node_bootstrap['block']['sequence_number'], 1) + self.assertNotEqual(node_bootstrap['block']['block_hash'], "") diff --git a/Tribler/Test/Community/Trustchain/test_trustchain_utilities.py b/Tribler/Test/Community/Trustchain/test_trustchain_utilities.py index f9df2768714..92271b4646c 100644 --- a/Tribler/Test/Community/Trustchain/test_trustchain_utilities.py +++ b/Tribler/Test/Community/Trustchain/test_trustchain_utilities.py @@ -16,7 +16,7 @@ class TestBlock(TrustChainBlock): Also used in other test files for TrustChain. """ - def __init__(self, transaction=None, previous=None): + def __init__(self, transaction=None, previous=None, key=None): crypto = ECCrypto() other = crypto.generate_key(u"curve25519").pub().key_to_bin() @@ -27,7 +27,11 @@ def __init__(self, transaction=None, previous=None): TrustChainBlock.__init__(self, (encode(transaction), previous.public_key, previous.sequence_number + 1, other, 0, previous.hash, 0, 0)) else: - self.key = crypto.generate_key(u"curve25519") + if key: + self.key = key + else: + self.key = crypto.generate_key(u"curve25519") + TrustChainBlock.__init__(self, ( encode(transaction), self.key.pub().key_to_bin(), random.randint(50, 100), other, 0, sha256(str(random.randint(0, 100000))).digest(), 0, 0)) diff --git a/Tribler/Test/Core/Modules/RestApi/test_trustchain_endpoint.py b/Tribler/Test/Core/Modules/RestApi/test_trustchain_endpoint.py index 4705d2ff1f6..5cc426aebaa 100644 --- a/Tribler/Test/Core/Modules/RestApi/test_trustchain_endpoint.py +++ b/Tribler/Test/Core/Modules/RestApi/test_trustchain_endpoint.py @@ -166,3 +166,95 @@ def test_get_blocks_unlimited(self): self.should_check_equality = False return self.do_request('trustchain/blocks/%s' % TestBlock().public_key.encode("HEX"), expected_code=200) + + @deferred(timeout=10) + def test_get_bootstrap_identity_no_community(self): + """ + Testing whether the API returns error 404 if no trustchain community is loaded when bootstrapping a new identity + """ + self.dispersy.get_communities = lambda: [] + return self.do_request('trustchain/bootstrap', expected_code=404) + + @deferred(timeout=10) + def test_get_bootstrap_identity_all_tokens(self): + """ + Testing whether the API return all available credit when no argument is supplied + """ + transaction = {'up': 100, 'down': 0, 'total_up': 100, 'total_down': 0} + transaction2 = {'up': 100, 'down': 0} + + def verify_response(response): + response_json = json.loads(response) + self.assertEqual(response_json['transaction'], transaction2) + + test_block = TestBlock(transaction=transaction, key=self.tc_community.my_member.private_key) + self.tc_community.persistence.add_block(test_block) + + self.should_check_equality = False + return self.do_request('trustchain/bootstrap', expected_code=200).addCallback(verify_response) + + @deferred(timeout=10) + def test_get_bootstrap_identity_partial_tokens(self): + """ + Testing whether the API return partial available credit when argument is supplied + """ + transaction = {'up': 100, 'down': 0, 'total_up': 100, 'total_down': 0} + transaction2 = {'up': 50, 'down': 0} + + def verify_response(response): + response_json = json.loads(response) + self.assertEqual(response_json['transaction'], transaction2) + + test_block = TestBlock(transaction=transaction, key=self.tc_community.my_member.private_key) + self.tc_community.persistence.add_block(test_block) + + self.should_check_equality = False + return self.do_request('trustchain/bootstrap?amount=50', expected_code=200).addCallback(verify_response) + + @deferred(timeout=10) + def test_get_bootstrap_identity_not_enough_tokens(self): + """ + Testing whether the API returns error 400 if bandwidth is to low when bootstrapping a new identity + """ + transaction = {'up': 100, 'down': 0, 'total_up': 100, 'total_down': 0} + test_block = TestBlock(transaction=transaction, key=self.tc_community.my_member.private_key) + self.tc_community.persistence.add_block(test_block) + + self.should_check_equality = False + return self.do_request('trustchain/bootstrap?amount=200', expected_code=400) + + @deferred(timeout=10) + def test_get_bootstrap_identity_not_enough_tokens_2(self): + """ + Testing whether the API returns error 400 if bandwidth is to low when bootstrapping a new identity + """ + transaction = {'up': 0, 'down': 100, 'total_up': 0, 'total_down': 100} + test_block = TestBlock(transaction=transaction, key=self.tc_community.my_member.private_key) + self.tc_community.persistence.add_block(test_block) + + self.should_check_equality = False + return self.do_request('trustchain/bootstrap?amount=10', expected_code=400) + + @deferred(timeout=10) + def test_get_bootstrap_identity_zero_amount(self): + """ + Testing whether the API returns error 400 if amount is zero when bootstrapping a new identity + """ + self.should_check_equality = False + return self.do_request('trustchain/bootstrap?amount=0', expected_code=400) + + @deferred(timeout=10) + def test_get_bootstrap_identity_negative_amount(self): + """ + Testing whether the API returns error 400 if amount is negative when bootstrapping a new identity + """ + self.should_check_equality = False + return self.do_request('trustchain/bootstrap?amount=-1', expected_code=400) + + @deferred(timeout=10) + def test_get_bootstrap_identity_string(self): + """ + Testing whether the API returns error 400 if amount is string when bootstrapping a new identity + """ + self.should_check_equality = False + return self.do_request('trustchain/bootstrap?amount=aaa', expected_code=400) diff --git a/Tribler/community/triblerchain/community.py b/Tribler/community/triblerchain/community.py index b8116b4f05c..c95372721e8 100644 --- a/Tribler/community/triblerchain/community.py +++ b/Tribler/community/triblerchain/community.py @@ -8,7 +8,7 @@ from Tribler.community.trustchain.community import TrustChainCommunity from Tribler.dispersy.util import blocking_call_on_reactor_thread -MIN_TRANSACTION_SIZE = 1024*1024 +MIN_TRANSACTION_SIZE = 1024 * 1024 class PendingBytes(object): @@ -163,7 +163,7 @@ def on_tunnel_remove(self, subject, change_type, tunnel, candidate): from Tribler.community.tunnel.tunnel_community import Circuit, RelayRoute, TunnelExitSocket assert isinstance(tunnel, Circuit) or isinstance(tunnel, RelayRoute) or isinstance(tunnel, TunnelExitSocket), \ "on_tunnel_remove() was called with an object that is not a Circuit, RelayRoute or TunnelExitSocket" - assert isinstance(tunnel.bytes_up, int) and isinstance(tunnel.bytes_down, int),\ + assert isinstance(tunnel.bytes_up, int) and isinstance(tunnel.bytes_down, int), \ "tunnel instance must provide byte counts in int" up = tunnel.bytes_up @@ -215,6 +215,59 @@ def get_trust(self, member): # We need a minimum of 1 trust to have a chance to be selected in the categorical distribution. return 1 + def get_bandwidth_tokens(self, member=None): + """ + Get the bandwidth tokens for another member. + Currently this is just the difference in the amount of MBs exchanged with them. + + :param member: the member we interacted with + :type member: dispersy.member.Member + :return: the amount of bandwidth tokens for this member + :rtype: int + """ + if member is None: + member = self.my_member + + block = self.persistence.get_latest(member.public_key) + if block: + return block.transaction['total_up'] - block.transaction['total_down'] + + return 0 + + def bootstrap_new_identity(self, amount): + """ + One-way payment channel. + Create a new temporary identity, and transfer funds to the new identity. + A different party can then take the result and do a transfer from the temporary identity to itself + """ + + # Create new identity for the temporary identity + tmp_member = self.dispersy.get_new_member(u"curve25519") + + # Create the transaction specification + transaction = { + 'up': 0, 'down': amount + } + + # Create the two half blocks that form the transaction + local_half_block = TriblerChainBlock.create(transaction, self.persistence, self.my_member.public_key, + link_pk=tmp_member.public_key) + local_half_block.sign(self.my_member.private_key) + tmp_half_block = TriblerChainBlock.create(transaction, self.persistence, tmp_member.public_key, + link=local_half_block, link_pk=self.my_member.public_key) + tmp_half_block.sign(tmp_member.private_key) + + self.persistence.add_block(local_half_block) + self.persistence.add_block(tmp_half_block) + + # Create the bootstrapped identity format + block = {'block_hash': tmp_half_block.hash.encode('base64'), + 'sequence_number': tmp_half_block.sequence_number} + + result = {'private_key': tmp_member.private_key.key_to_bin().encode('base64'), + 'transaction': {'up': amount, 'down': 0}, 'block': block} + return result + class TriblerChainCommunityCrawler(TriblerChainCommunity): """ diff --git a/TriblerGUI/qt_resources/mainwindow.ui b/TriblerGUI/qt_resources/mainwindow.ui index c4ee6d2e27f..e47f6c4588a 100644 --- a/TriblerGUI/qt_resources/mainwindow.ui +++ b/TriblerGUI/qt_resources/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 855 - 655 + 875 + 768 @@ -6260,6 +6260,121 @@ color: white; + + + + + 75 + true + + + + font-weight: bold; +color: white; + + + Bandwidth tokens emptying into another account (offline) + + + + + + + Bandwidth tokens can be emptied into another + + + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + FULLY EMPTY TOKENS + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 15 + 20 + + + + + + + + + 0 + 24 + + + + + 16777215 + 24 + + + + + + + PARTIALLY EMPTY TOKENS + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + + font-weight: bold; +color: orange; + + + Warning: this is a one-way action that cannot be reverted. If the QR code is not scanned, funds will be lost. + + + true + + + diff --git a/TriblerGUI/widgets/settingspage.py b/TriblerGUI/widgets/settingspage.py index d5982d37a0f..35cd13df546 100644 --- a/TriblerGUI/widgets/settingspage.py +++ b/TriblerGUI/widgets/settingspage.py @@ -1,12 +1,27 @@ -from PyQt5.QtWidgets import QWidget, QFileDialog +from PIL.ImageQt import ImageQt + +from PyQt5 import QtGui, QtCore +from PyQt5.QtWidgets import QWidget, QLabel, QFileDialog + +try: + import qrcode + + has_qr = True +except ImportError: + has_qr = False import Tribler.Core.Utilities.json_util as json from TriblerGUI.defs import PAGE_SETTINGS_GENERAL, PAGE_SETTINGS_CONNECTION, PAGE_SETTINGS_BANDWIDTH, \ - PAGE_SETTINGS_SEEDING, PAGE_SETTINGS_ANONYMITY, BUTTON_TYPE_NORMAL + PAGE_SETTINGS_SEEDING, PAGE_SETTINGS_ANONYMITY, BUTTON_TYPE_NORMAL, BUTTON_TYPE_CONFIRM from TriblerGUI.dialogs.confirmationdialog import ConfirmationDialog from TriblerGUI.tribler_request_manager import TriblerRequestManager from TriblerGUI.utilities import string_to_seconds, get_gui_setting, seconds_to_hhmm_string, is_dir_writable +DEPENDENCY_ERROR_TITLE = "Dependency missing" +DEPENDENCY_ERROR_MESSAGE = "'qrcode' module is missing. This module can be installed through apt-get or pip" + +MEBIBYTE = 1024 * 1024 + class SettingsPage(QWidget): """ @@ -17,7 +32,11 @@ def __init__(self): QWidget.__init__(self) self.settings = None self.settings_request_mgr = None + self.trustchain_request_mgr = None self.saved_dialog = None + self.empty_tokens_barcode_dialog = None + self.empty_partial_tokens_dialog = None + self.confirm_empty_tokens_dialog = None def initialize_settings_page(self): self.window().settings_tab.initialize() @@ -30,8 +49,102 @@ def initialize_settings_page(self): self.window().developer_mode_enabled_checkbox.stateChanged.connect(self.on_developer_mode_checkbox_changed) self.window().use_monochrome_icon_checkbox.stateChanged.connect(self.on_use_monochrome_icon_checkbox_changed) self.window().download_settings_anon_checkbox.stateChanged.connect(self.on_anon_download_state_changed) + self.window().fully_empty_tokens_button.clicked.connect(self.confirm_fully_empty_tokens) + self.window().partially_empty_tokens_button.clicked.connect(self.partially_empty_tokens) + + def confirm_fully_empty_tokens(self): + self.confirm_empty_tokens_dialog = ConfirmationDialog(self, "Empty tokens into another account", + "Are you sure you want to empty ALL bandwidth tokens " + "into another account? " + "Warning: one-way action that cannot be revered", + [ + ('EMPTY', BUTTON_TYPE_CONFIRM), + ('CANCEL', BUTTON_TYPE_NORMAL) + ]) + self.confirm_empty_tokens_dialog.button_clicked.connect(self.on_confirm_fully_empty_tokens) + self.confirm_empty_tokens_dialog.show() + + def on_confirm_fully_empty_tokens(self, action): + self.confirm_empty_tokens_dialog.close_dialog() + self.confirm_empty_tokens_dialog = None + + if action == 0: + self.trustchain_request_mgr = TriblerRequestManager() + self.trustchain_request_mgr.perform_request("trustchain/bootstrap", self.on_emptying_tokens) + + def partially_empty_tokens(self): + self.empty_partial_tokens_dialog = ConfirmationDialog(self, "Empty tokens into another account", + "Specify the amount of bandwidth tokens to empty into " + "another account below:", + [ + ('EMPTY', BUTTON_TYPE_CONFIRM), + ('CANCEL', BUTTON_TYPE_NORMAL) + ], show_input=True) + self.empty_partial_tokens_dialog.dialog_widget.dialog_input.setPlaceholderText( + 'Please enter the amount of tokens in MB') + self.empty_partial_tokens_dialog.dialog_widget.dialog_input.setFocus() + self.empty_partial_tokens_dialog.button_clicked.connect(self.confirm_partially_empty_tokens) + self.empty_partial_tokens_dialog.show() + + def confirm_partially_empty_tokens(self, action): + tokens = self.empty_partial_tokens_dialog.dialog_widget.dialog_input.text() + self.empty_partial_tokens_dialog.close_dialog() + self.empty_partial_tokens_dialog = None + + if action == 0: + try: + tokens = int(float(tokens)) + except ValueError: + ConfirmationDialog.show_error(self.window(), "Wrong input", "The provided amount is not a number") + return - self.window().log_location_chooser_button.clicked.connect(self.on_choose_log_dir_clicked) + self.confirm_empty_tokens_dialog = ConfirmationDialog(self, "Empty tokens into another account", + "Are you sure you want to empty %d bandwidth tokens " + "into another account? " + "Warning: one-way action that cannot be revered" % + tokens, + [ + ('EMPTY', BUTTON_TYPE_NORMAL), + ('CANCEL', BUTTON_TYPE_CONFIRM) + ]) + self.confirm_empty_tokens_dialog.button_clicked.connect( + lambda action2: self.on_confirm_partially_empty_tokens(action2, tokens)) + self.confirm_empty_tokens_dialog.show() + + def on_confirm_partially_empty_tokens(self, action, tokens): + self.confirm_empty_tokens_dialog.close_dialog() + self.confirm_empty_tokens_dialog = None + if action == 0: + self.trustchain_request_mgr = TriblerRequestManager() + self.trustchain_request_mgr.perform_request("trustchain/bootstrap?amount=%d" % (tokens * MEBIBYTE), + self.on_emptying_tokens) + + def on_emptying_tokens(self, data): + json_data = json.dumps(data) + + if has_qr: + self.empty_tokens_barcode_dialog = QWidget() + self.empty_tokens_barcode_dialog.setWindowTitle("Please scan the following QR code") + self.empty_tokens_barcode_dialog.setGeometry(10, 10, 500, 500) + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_M, + box_size=10, + border=5, + ) + qr.add_data(json_data) + qr.make(fit=True) + + img = qr.make_image() # PIL format + + qim = ImageQt(img) + pixmap = QtGui.QPixmap.fromImage(qim).scaled(600, 600, QtCore.Qt.KeepAspectRatio) + label = QLabel(self.empty_tokens_barcode_dialog) + label.setPixmap(pixmap) + self.empty_tokens_barcode_dialog.resize(pixmap.width(), pixmap.height()) + self.empty_tokens_barcode_dialog.show() + else: + ConfirmationDialog.show_error(self.window(), DEPENDENCY_ERROR_TITLE, DEPENDENCY_ERROR_MESSAGE) def on_developer_mode_checkbox_changed(self, _): self.window().gui_settings.setValue("debug", self.window().developer_mode_enabled_checkbox.isChecked()) @@ -93,7 +206,7 @@ def initialize_with_settings(self, settings): False, is_bool=True)) self.window().family_filter_checkbox.setChecked(settings['general']['family_filter']) self.window().use_monochrome_icon_checkbox.setChecked(get_gui_setting(gui_settings, "use_monochrome_icon", - False, is_bool=True)) + False, is_bool=True)) self.window().download_location_input.setText(settings['download_defaults']['saveas']) self.window().always_ask_location_checkbox.setChecked( get_gui_setting(gui_settings, "ask_download_settings", True, is_bool=True)) @@ -167,7 +280,8 @@ def save_settings(self): settings_data['libtorrent']['proxy_type'] = self.window().lt_proxy_type_combobox.currentIndex() - if self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len(self.window().lt_proxy_port_input.text()) > 0: + if self.window().lt_proxy_server_input.text() and len(self.window().lt_proxy_server_input.text()) > 0 and len( + self.window().lt_proxy_port_input.text()) > 0: settings_data['libtorrent']['proxy_server'] = [self.window().lt_proxy_server_input.text(), None] settings_data['libtorrent']['proxy_server'][0] = self.window().lt_proxy_server_input.text() try: @@ -219,7 +333,8 @@ def save_settings(self): settings_data['download_defaults']['seeding_ratio'] = self.window().seeding_ratio_combobox.currentText() try: - settings_data['download_defaults']['seeding_time'] = string_to_seconds(self.window().seeding_time_input.text()) + settings_data['download_defaults']['seeding_time'] = string_to_seconds( + self.window().seeding_time_input.text()) except ValueError: ConfirmationDialog.show_error(self.window(), "Invalid seeding time", "You've entered an invalid format for the seeding time (expected HH:MM)")