1# Electrum - Lightweight Bitcoin Client
2# Copyright (c) 2015 Thomas Voegtlin
3#
4# Permission is hereby granted, free of charge, to any person
5# obtaining a copy of this software and associated documentation files
6# (the "Software"), to deal in the Software without restriction,
7# including without limitation the rights to use, copy, modify, merge,
8# publish, distribute, sublicense, and/or sell copies of the Software,
9# and to permit persons to whom the Software is furnished to do so,
10# subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be
13# included in all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22# SOFTWARE.
23import re
24
25import dns
26from dns.exception import DNSException
27
28from . import bitcoin
29from . import dnssec
30from .util import read_json_file, write_json_file, to_string
31from .logging import Logger
32
33
34class Contacts(dict, Logger):
35
36    def __init__(self, db):
37        Logger.__init__(self)
38        self.db = db
39        d = self.db.get('contacts', {})
40        try:
41            self.update(d)
42        except:
43            return
44        # backward compatibility
45        for k, v in self.items():
46            _type, n = v
47            if _type == 'address' and bitcoin.is_address(n):
48                self.pop(k)
49                self[n] = ('address', k)
50
51    def save(self):
52        self.db.put('contacts', dict(self))
53
54    def import_file(self, path):
55        data = read_json_file(path)
56        data = self._validate(data)
57        self.update(data)
58        self.save()
59
60    def export_file(self, path):
61        write_json_file(path, self)
62
63    def __setitem__(self, key, value):
64        dict.__setitem__(self, key, value)
65        self.save()
66
67    def pop(self, key):
68        if key in self.keys():
69            res = dict.pop(self, key)
70            self.save()
71            return res
72
73    def resolve(self, k):
74        if bitcoin.is_address(k):
75            return {
76                'address': k,
77                'type': 'address'
78            }
79        if k in self.keys():
80            _type, addr = self[k]
81            if _type == 'address':
82                return {
83                    'address': addr,
84                    'type': 'contact'
85                }
86        out = self.resolve_openalias(k)
87        if out:
88            address, name, validated = out
89            return {
90                'address': address,
91                'name': name,
92                'type': 'openalias',
93                'validated': validated
94            }
95        raise Exception("Invalid Bitcoin address or alias", k)
96
97    def resolve_openalias(self, url):
98        # support email-style addresses, per the OA standard
99        url = url.replace('@', '.')
100        try:
101            records, validated = dnssec.query(url, dns.rdatatype.TXT)
102        except DNSException as e:
103            self.logger.info(f'Error resolving openalias: {repr(e)}')
104            return None
105        prefix = 'btc'
106        for record in records:
107            string = to_string(record.strings[0], 'utf8')
108            if string.startswith('oa1:' + prefix):
109                address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
110                name = self.find_regex(string, r'recipient_name=([^;]+)')
111                if not name:
112                    name = address
113                if not address:
114                    continue
115                return address, name, validated
116
117    def find_regex(self, haystack, needle):
118        regex = re.compile(needle)
119        try:
120            return regex.search(haystack).groups()[0]
121        except AttributeError:
122            return None
123
124    def _validate(self, data):
125        for k, v in list(data.items()):
126            if k == 'contacts':
127                return self._validate(v)
128            if not bitcoin.is_address(k):
129                data.pop(k)
130            else:
131                _type, _ = v
132                if _type != 'address':
133                    data.pop(k)
134        return data
135
136