contacts.py (4439B)
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. 23 import re 24 25 import dns 26 from dns.exception import DNSException 27 28 from . import bitcoin 29 from . import dnssec 30 from .util import read_json_file, write_json_file, to_string 31 from .logging import Logger 32 33 34 class 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