electrum

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

commit 616becd9a811fae31f0b50f602a60e318538f56b
parent 90d32038faf618031241e2ca4ea206ee7504051c
Author: ThomasV <thomasv@gitorious>
Date:   Thu,  2 Jul 2015 12:44:53 +0200

move openalias from plugins to core

Diffstat:
Mgui/qt/main_window.py | 31++++++++++++++++++-------------
Mgui/qt/paytoedit.py | 48++++++++++++++++++++++++++++++++++++++++++++++--
Mlib/commands.py | 3++-
Alib/contacts.py | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/util.py | 19-------------------
Mplugins/__init__.py | 7-------
Dplugins/openalias.py | 297-------------------------------------------------------------------------------
7 files changed, 238 insertions(+), 339 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -19,6 +19,10 @@ import sys, time, threading import os.path, json, traceback import shutil +import socket +import webbrowser +import csv +from decimal import Decimal import PyQt4 @@ -26,11 +30,10 @@ from PyQt4.QtGui import * from PyQt4.QtCore import * import PyQt4.QtCore as QtCore -from electrum.bitcoin import MIN_RELAY_TX_FEE, COIN, is_valid -from electrum.plugins import run_hook - import icons_rc +from electrum.bitcoin import MIN_RELAY_TX_FEE, COIN, is_valid +from electrum.plugins import run_hook from electrum.i18n import _ from electrum.util import block_explorer, block_explorer_info, block_explorer_URL from electrum.util import print_error, print_msg @@ -41,6 +44,7 @@ from electrum import util, bitcoin, commands, Wallet from electrum import SimpleConfig, Wallet, WalletStorage from electrum import Imported_Wallet from electrum import paymentrequest +from electrum.contacts import Contacts from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit from network_dialog import NetworkDialog @@ -48,12 +52,6 @@ from qrcodewidget import QRCodeWidget, QRDialog from qrtextedit import ScanQRTextEdit, ShowQRTextEdit from transaction_dialog import show_transaction -from decimal import Decimal - -import socket -import webbrowser -import csv - @@ -120,7 +118,7 @@ class ElectrumWindow(QMainWindow): self.app = gui_object.app self.invoices = InvoiceStore(self.config) - self.contacts = util.Contacts(self.config) + self.contacts = Contacts(self.config) self.create_status_bar() self.need_update = threading.Event() @@ -482,17 +480,16 @@ class ElectrumWindow(QMainWindow): if self.need_update.is_set(): self.update_wallet() self.need_update.clear() - + # resolve aliases + self.payto_e.resolve() run_hook('timer_actions') def format_amount(self, x, is_diff=False, whitespaces=False): return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces) - def get_decimal_point(self): return self.decimal_point - def base_unit(self): assert self.decimal_point in [2, 5, 8] if self.decimal_point == 2: @@ -1051,6 +1048,13 @@ class ElectrumWindow(QMainWindow): return outputs = self.payto_e.get_outputs() + if self.payto_e.is_alias and self.payto_e.validated is False: + alias = self.payto_e.toPlainText() + msg = _('WARNING: the alias "%s" could not be validated via an additional security check, DNSSEC, and thus may not be correct.'%alias) + '\n' + msg += _('Do you wish to continue?') + if not self.question(msg): + return + if not outputs: QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK')) return @@ -1208,6 +1212,7 @@ class ElectrumWindow(QMainWindow): self.payment_request = None return + self.payto_e.is_pr = True if not pr.has_expired(): self.payto_e.setGreen() else: diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py @@ -34,6 +34,7 @@ class PayToEdit(ScanQRTextEdit): def __init__(self, win): ScanQRTextEdit.__init__(self) + self.win = win self.amount_edit = win.amount_e self.document().contentsChanged.connect(self.update_size) self.heightMin = 0 @@ -43,10 +44,13 @@ class PayToEdit(ScanQRTextEdit): self.outputs = [] self.errors = [] self.is_pr = False + self.is_alias = False self.scan_f = win.pay_from_URI self.update_size() self.payto_address = None + self.previous_payto = '' + def lock_amount(self): self.amount_edit.setFrozen(True) @@ -60,11 +64,9 @@ class PayToEdit(ScanQRTextEdit): button.setHidden(b) def setGreen(self): - self.is_pr = True self.setStyleSheet("QWidget { background-color:#80ff80;}") def setExpired(self): - self.is_pr = True self.setStyleSheet("QWidget { background-color:#ffcccc;}") def parse_address_and_amount(self, line): @@ -252,3 +254,45 @@ class PayToEdit(ScanQRTextEdit): if data.startswith("bitcoin:"): self.scan_f(data) # TODO: update fee + + def resolve(self): + self.is_alias = False + if self.hasFocus(): + return + if self.is_multiline(): # only supports single line entries atm + return + if self.is_pr: + return + key = str(self.toPlainText()) + if key == self.previous_payto: + return + self.previous_payto = key + if not (('.' in key) and (not '<' in key) and (not ' ' in key)): + return + try: + data = self.win.contacts.resolve(key) + except: + return + if not data: + return + self.is_alias = True + + address = data.get('address') + name = data.get('name') + new_url = key + ' <' + address + '>' + self.setText(new_url) + self.previous_payto = new_url + + #if self.win.config.get('openalias_autoadd') == 'checked': + self.win.contacts[key] = ('openalias', name) + self.win.update_contacts_tab() + + self.setFrozen(True) + if data.get('type') == 'openalias': + self.validated = data.get('validated') + if self.validated: + self.setGreen() + else: + self.setExpired() + else: + self.validated = None diff --git a/lib/commands.py b/lib/commands.py @@ -34,6 +34,7 @@ from bitcoin import is_address, hash_160_to_bc_address, hash_160, COIN from transaction import Transaction import paymentrequest from paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED +import contacts known_commands = {} @@ -78,7 +79,7 @@ class Commands: self.network = network self._callback = callback self.password = None - self.contacts = util.Contacts(self.config) + self.contacts = contacts.Contacts(self.config) def _run(self, method, args, password_getter): cmd = known_commands[method] diff --git a/lib/contacts.py b/lib/contacts.py @@ -0,0 +1,172 @@ +import sys +import re +import dns +import traceback + +import bitcoin +from util import StoreDict, print_error +from i18n import _ + +# Import all of the rdtypes, as py2app and similar get confused with the dnspython +# autoloader and won't include all the rdatatypes +try: + import dns.name + import dns.query + import dns.dnssec + import dns.message + import dns.resolver + import dns.rdatatype + import dns.rdtypes.ANY.NS + import dns.rdtypes.ANY.CNAME + import dns.rdtypes.ANY.DLV + import dns.rdtypes.ANY.DNSKEY + import dns.rdtypes.ANY.DS + import dns.rdtypes.ANY.NSEC + import dns.rdtypes.ANY.NSEC3 + import dns.rdtypes.ANY.NSEC3PARAM + import dns.rdtypes.ANY.RRSIG + import dns.rdtypes.ANY.SOA + import dns.rdtypes.ANY.TXT + import dns.rdtypes.IN.A + import dns.rdtypes.IN.AAAA + from dns.exception import DNSException + OA_READY = True +except ImportError: + OA_READY = False + + +class Contacts(StoreDict): + + def __init__(self, config): + StoreDict.__init__(self, config, 'contacts') + + def resolve(self, k): + if bitcoin.is_address(k): + return { + 'address': k, + 'type': 'address' + } + + if k in self.keys(): + _type, addr = self[k] + if _type == 'address': + return { + 'address': addr, + 'type': 'contact' + } + + out = self.resolve_openalias(k) + if out: + address, name = out + try: + validated = self.validate_dnssec(k) + except: + validated = False + traceback.print_exc(file=sys.stderr) + return { + 'address': address, + 'name': name, + 'type': 'openalias', + 'validated': validated + } + + raise Exception("Invalid Bitcoin address or alias", k) + + def resolve_openalias(self, url): + '''Resolve OpenAlias address using url.''' + print_error('[OA] Attempting to resolve OpenAlias data for ' + url) + + url = url.replace('@', '.') # support email-style addresses, per the OA standard + prefix = 'btc' + retries = 3 + err = None + for i in range(0, retries): + try: + resolver = dns.resolver.Resolver() + resolver.timeout = 2.0 + resolver.lifetime = 4.0 + records = resolver.query(url, dns.rdatatype.TXT) + for record in records: + string = record.strings[0] + if string.startswith('oa1:' + prefix): + address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') + name = self.find_regex(string, r'recipient_name=([^;]+)') + if not name: + name = address + if not address: + continue + return (address, name) + err = _('No OpenAlias record found.') + break + except dns.resolver.NXDOMAIN: + err = _('No such domain.') + continue + except dns.resolver.Timeout: + err = _('Timed out while resolving.') + continue + except DNSException: + err = _('Unhandled exception.') + continue + except Exception, e: + err = _('Unexpected error: ' + str(e)) + continue + break + if err: + print_error(err) + return 0 + + def find_regex(self, haystack, needle): + regex = re.compile(needle) + try: + return regex.search(haystack).groups()[0] + except AttributeError: + return None + + def validate_dnssec(self, url): + print_error('Checking DNSSEC trust chain for ' + url) + default = dns.resolver.get_default_resolver() + ns = default.nameservers[0] + parts = url.split('.') + + for i in xrange(len(parts), 0, -1): + sub = '.'.join(parts[i - 1:]) + query = dns.message.make_query(sub, dns.rdatatype.NS) + response = dns.query.udp(query, ns, 3) + if response.rcode() != dns.rcode.NOERROR: + print_error("query error") + return False + + if len(response.authority) > 0: + rrset = response.authority[0] + else: + rrset = response.answer[0] + + rr = rrset[0] + if rr.rdtype == dns.rdatatype.SOA: + #Same server is authoritative, don't check again + continue + + query = dns.message.make_query(sub, + dns.rdatatype.DNSKEY, + want_dnssec=True) + response = dns.query.udp(query, ns, 3) + if response.rcode() != 0: + self.print_error("query error") + return False + # HANDLE QUERY FAILED (SERVER ERROR OR NO DNSKEY RECORD) + + # answer should contain two RRSET: DNSKEY and RRSIG(DNSKEY) + answer = response.answer + if len(answer) != 2: + print_error("answer error", answer) + return False + + # the DNSKEY should be self signed, validate it + name = dns.name.from_text(sub) + try: + dns.dnssec.validate(answer[0], answer[1], {name: answer[0]}) + except dns.dnssec.ValidationFailure: + print_error("validation error") + return False + + return True diff --git a/lib/util.py b/lib/util.py @@ -469,22 +469,3 @@ class StoreDict(dict): self.save() -import bitcoin -from plugins import run_hook - -class Contacts(StoreDict): - - def __init__(self, config): - StoreDict.__init__(self, config, 'contacts') - - def resolve(self, k): - if bitcoin.is_address(k): - return {'address':k, 'type':'address'} - if k in self.keys(): - _type, addr = self[k] - if _type == 'address': - return {'address':addr, 'type':'contact'} - out = run_hook('resolve_address', k) - if out: - return out - raise Exception("Invalid Bitcoin address or alias", k) diff --git a/plugins/__init__.py b/plugins/__init__.py @@ -69,13 +69,6 @@ descriptions = [ 'available_for': ['qt'] }, { - 'name': 'openalias', - 'fullname': 'OpenAlias', - 'description': _('Allow for payments to OpenAlias addresses.'), - 'requires': [('dns', 'dnspython')], - 'available_for': ['qt', 'cmdline'] - }, - { 'name': 'plot', 'fullname': 'Plot History', 'description': _("Ability to plot transaction history in graphical mode."), diff --git a/plugins/openalias.py b/plugins/openalias.py @@ -1,297 +0,0 @@ -# Copyright (c) 2014-2015, The Monero Project -# -# All rights reserved. - -# This plugin is licensed under the GPL v3 license (see the LICENSE file in the base of -# the project source code). The Monero Project reserves the right to change this license -# in future to match or be compliant with any relicense of the Electrum project. - -# This plugin implements the OpenAlias standard. For information on the standard please -# see: https://openalias.org - -# Donations for ongoing development of the standard and hosting resolvers can be sent to -# openalias.org or donate.monero.cc - -# Version: 0.1 -# Todo: optionally use OA resolvers; add DNSCrypt support - -import re -import traceback - -from PyQt4.QtGui import * -from PyQt4.QtCore import * - -from electrum_gui.qt.util import * -from electrum.plugins import BasePlugin, hook -from electrum.util import print_error -from electrum.i18n import _ - - -# Import all of the rdtypes, as py2app and similar get confused with the dnspython -# autoloader and won't include all the rdatatypes -try: - import dns.name - import dns.query - import dns.dnssec - import dns.message - import dns.resolver - import dns.rdatatype - import dns.rdtypes.ANY.NS - import dns.rdtypes.ANY.CNAME - import dns.rdtypes.ANY.DLV - import dns.rdtypes.ANY.DNSKEY - import dns.rdtypes.ANY.DS - import dns.rdtypes.ANY.NSEC - import dns.rdtypes.ANY.NSEC3 - import dns.rdtypes.ANY.NSEC3PARAM - import dns.rdtypes.ANY.RRSIG - import dns.rdtypes.ANY.SOA - import dns.rdtypes.ANY.TXT - import dns.rdtypes.IN.A - import dns.rdtypes.IN.AAAA - from dns.exception import DNSException - OA_READY = True -except ImportError: - OA_READY = False - - -class Plugin(BasePlugin): - def is_available(self): - return OA_READY - - def __init__(self, gui, name): - BasePlugin.__init__(self, gui, name) - self._is_available = OA_READY - self.print_error('OA_READY is ' + str(OA_READY)) - self.previous_payto = '' - - @hook - def init_qt(self, gui): - self.gui = gui - self.win = gui.main_window - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Settings'), self.settings_dialog) - - @hook - def timer_actions(self): - if self.win.payto_e.hasFocus(): - return - if self.win.payto_e.is_multiline(): # only supports single line entries atm - return - if self.win.payto_e.is_pr: - return - - url = str(self.win.payto_e.toPlainText()) - - if url == self.previous_payto: - return - self.previous_payto = url - - if not (('.' in url) and (not '<' in url) and (not ' ' in url)): - return - - data = self.resolve(url) - - if not data: - self.previous_payto = url - return True - - address, name = data - new_url = url + ' <' + address + '>' - self.win.payto_e.setText(new_url) - self.previous_payto = new_url - - if self.config.get('openalias_autoadd') == 'checked': - self.win.contacts[url] = ('openalias', name) - self.win.update_contacts_tab() - - self.win.payto_e.setFrozen(True) - try: - self.validated = self.validate_dnssec(url) - except: - self.validated = False - traceback.print_exc(file=sys.stderr) - - if self.validated: - self.win.payto_e.setGreen() - else: - self.win.payto_e.setExpired() - - @hook - def before_send(self): - ''' - Change URL to address before making a send. - IMPORTANT: - return False to continue execution of the send - return True to stop execution of the send - ''' - - if self.win.payto_e.is_multiline(): # only supports single line entries atm - return False - if self.win.payto_e.is_pr: - return - payto_e = str(self.win.payto_e.toPlainText()) - regex = re.compile(r'^([^\s]+) <([A-Za-z0-9]+)>') # only do that for converted addresses - try: - (url, address) = regex.search(payto_e).groups() - except AttributeError: - return False - - if not self.validated: - msgBox = QMessageBox() - msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) - msgBox.setInformativeText(_('Do you wish to continue?')) - msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) - msgBox.setDefaultButton(QMessageBox.Cancel) - reply = msgBox.exec_() - if reply != QMessageBox.Ok: - return True - - return False - - def settings_dialog(self): - '''Settings dialog.''' - d = QDialog() - d.setWindowTitle("Settings") - layout = QGridLayout(d) - layout.addWidget(QLabel(_('Automatically add to contacts')), 0, 0) - autoadd_checkbox = QCheckBox() - autoadd_checkbox.setEnabled(True) - autoadd_checkbox.setChecked(self.config.get('openalias_autoadd', 'unchecked') != 'unchecked') - layout.addWidget(autoadd_checkbox, 0, 1) - ok_button = QPushButton(_("OK")) - ok_button.clicked.connect(d.accept) - layout.addWidget(ok_button, 1, 1) - - def on_change_autoadd(checked): - if checked: - self.config.set_key('openalias_autoadd', 'checked') - else: - self.config.set_key('openalias_autoadd', 'unchecked') - - autoadd_checkbox.stateChanged.connect(on_change_autoadd) - - return bool(d.exec_()) - - - @hook - def resolve_address(self, url): - data = self.resolve(url) - if not data: - return - address, name = data - try: - validated = self.validate_dnssec(url) - except: - validated = False - traceback.print_exc(file=sys.stderr) - return { - 'address': address, - 'name': name, - 'type': 'openalias', - 'validated': validated - } - - - def resolve(self, url): - '''Resolve OpenAlias address using url.''' - self.print_error('[OA] Attempting to resolve OpenAlias data for ' + url) - - url = url.replace('@', '.') # support email-style addresses, per the OA standard - prefix = 'btc' - retries = 3 - err = None - for i in range(0, retries): - try: - resolver = dns.resolver.Resolver() - resolver.timeout = 2.0 - resolver.lifetime = 4.0 - records = resolver.query(url, dns.rdatatype.TXT) - for record in records: - string = record.strings[0] - if string.startswith('oa1:' + prefix): - address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') - name = self.find_regex(string, r'recipient_name=([^;]+)') - if not name: - name = address - if not address: - continue - return (address, name) - QMessageBox.warning(self.win, _('Error'), _('No OpenAlias record found.'), _('OK')) - return 0 - except dns.resolver.NXDOMAIN: - err = _('No such domain.') - continue - except dns.resolver.Timeout: - err = _('Timed out while resolving.') - continue - except DNSException: - err = _('Unhandled exception.') - continue - except Exception, e: - err = _('Unexpected error: ' + str(e)) - continue - break - if err: - QMessageBox.warning(self.win, _('Error'), err, _('OK')) - return 0 - - def find_regex(self, haystack, needle): - regex = re.compile(needle) - try: - return regex.search(haystack).groups()[0] - except AttributeError: - return None - - def validate_dnssec(self, url): - self.print_error('Checking DNSSEC trust chain for ' + url) - default = dns.resolver.get_default_resolver() - ns = default.nameservers[0] - parts = url.split('.') - - for i in xrange(len(parts), 0, -1): - sub = '.'.join(parts[i - 1:]) - query = dns.message.make_query(sub, dns.rdatatype.NS) - response = dns.query.udp(query, ns, 3) - if response.rcode() != dns.rcode.NOERROR: - self.print_error("query error") - return False - - if len(response.authority) > 0: - rrset = response.authority[0] - else: - rrset = response.answer[0] - - rr = rrset[0] - if rr.rdtype == dns.rdatatype.SOA: - #Same server is authoritative, don't check again - continue - - query = dns.message.make_query(sub, - dns.rdatatype.DNSKEY, - want_dnssec=True) - response = dns.query.udp(query, ns, 3) - if response.rcode() != 0: - self.print_error("query error") - return False - # HANDLE QUERY FAILED (SERVER ERROR OR NO DNSKEY RECORD) - - # answer should contain two RRSET: DNSKEY and RRSIG(DNSKEY) - answer = response.answer - if len(answer) != 2: - self.print_error("answer error", answer) - return False - - # the DNSKEY should be self signed, validate it - name = dns.name.from_text(sub) - try: - dns.dnssec.validate(answer[0], answer[1], {name: answer[0]}) - except dns.dnssec.ValidationFailure: - self.print_error("validation error") - return False - - return True