commit 616becd9a811fae31f0b50f602a60e318538f56b
parent 90d32038faf618031241e2ca4ea206ee7504051c
Author: ThomasV <thomasv@gitorious>
Date: Thu, 2 Jul 2015 12:44:53 +0200
move openalias from plugins to core
Diffstat:
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