Files

184 lines
5.9 KiB
Python
Raw Permalink Normal View History

2016-02-23 11:36:42 +01:00
# Electrum - Lightweight Bitcoin Client
# Copyright (c) 2015 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2015-07-02 12:44:53 +02:00
import re
from typing import Optional, Tuple, Dict, Any, TYPE_CHECKING
import asyncio
2015-07-02 12:44:53 +02:00
import dns
2018-03-06 16:07:33 +01:00
from dns.exception import DNSException
2015-07-02 12:44:53 +02:00
2017-01-22 21:25:24 +03:00
from . import bitcoin
from . import dnssec
from .util import read_json_file, write_json_file, to_string, is_valid_email
from .logging import Logger, get_logger
from .util import trigger_callback, get_asyncio_loop
2015-07-08 19:20:54 +02:00
if TYPE_CHECKING:
from .wallet_db import WalletDB
from .simple_config import SimpleConfig
_logger = get_logger(__name__)
class AliasNotFoundException(Exception):
pass
2019-04-26 18:52:26 +02:00
class Contacts(dict, Logger):
2015-07-02 12:44:53 +02:00
def __init__(self, db: 'WalletDB'):
2019-04-26 18:52:26 +02:00
Logger.__init__(self)
self.db = db
d = self.db.get('contacts', {})
2017-03-29 10:29:02 +02:00
try:
self.update(d)
except Exception:
2017-03-29 10:29:02 +02:00
return
2016-05-28 16:56:18 +02:00
# backward compatibility
for k, v in self.items():
_type, n = v
if _type == 'address' and bitcoin.is_address(n):
self.pop(k)
self[n] = ('address', k)
def save(self):
self.db.put('contacts', dict(self))
2025-05-20 15:28:55 +02:00
trigger_callback('contacts_updated')
def import_file(self, path):
data = read_json_file(path)
data = self._validate(data)
self.update(data)
self.save()
def export_file(self, path):
write_json_file(path, self)
def __setitem__(self, key, value):
dict.__setitem__(self, key, value)
self.save()
def pop(self, key):
if key in self.keys():
2018-11-27 21:32:55 +01:00
res = dict.pop(self, key)
self.save()
2018-11-27 21:32:55 +01:00
return res
return None
2015-07-02 12:44:53 +02:00
async def resolve(self, k) -> dict:
2015-07-02 12:44:53 +02:00
if bitcoin.is_address(k):
return {
'address': k,
'type': 'address'
}
for address, (_type, label) in self.items():
if k.casefold() != label.casefold():
continue
if _type in ('address', 'lnaddress'):
2015-07-02 12:44:53 +02:00
return {
'address': address,
2015-07-02 12:44:53 +02:00
'type': 'contact'
}
if openalias := await self.resolve_openalias(k):
return openalias
raise AliasNotFoundException("Invalid Bitcoin address or alias", k)
@classmethod
async def resolve_openalias(cls, url: str) -> Dict[str, Any]:
out = await cls._resolve_openalias(url)
2015-07-02 12:44:53 +02:00
if out:
2015-07-08 19:20:54 +02:00
address, name, validated = out
2015-07-02 12:44:53 +02:00
return {
'address': address,
'name': name,
'type': 'openalias',
'validated': validated
}
return {}
2015-07-02 12:44:53 +02:00
def by_name(self, name):
for k in self.keys():
_type, addr = self[k]
if addr.casefold() == name.casefold():
return {
'name': addr,
'type': _type,
'address': k
}
return None
def fetch_openalias(self, config: 'SimpleConfig'):
self.alias_info = None
alias = config.OPENALIAS_ID
if alias:
alias = str(alias)
async def f():
self.alias_info = await self._resolve_openalias(alias)
trigger_callback('alias_received')
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop())
@classmethod
async def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]:
2015-07-08 19:20:54 +02:00
# support email-style addresses, per the OA standard
url = url.replace('@', '.')
2018-03-06 16:07:33 +01:00
try:
records, validated = await dnssec.query(url, dns.rdatatype.TXT)
2018-03-06 16:07:33 +01:00
except DNSException as e:
_logger.info(f'Error resolving openalias: {repr(e)}')
2018-03-06 16:07:33 +01:00
return None
2015-07-02 12:44:53 +02:00
prefix = 'btc'
2015-07-08 19:20:54 +02:00
for record in records:
if record.rdtype != dns.rdatatype.TXT:
continue
2018-03-06 16:08:49 +01:00
string = to_string(record.strings[0], 'utf8')
2015-07-08 19:20:54 +02:00
if string.startswith('oa1:' + prefix):
address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)')
name = cls.find_regex(string, r'recipient_name=([^;]+)')
2015-07-08 19:20:54 +02:00
if not name:
name = address
if not address:
continue
return address, name, validated
return None
2015-07-02 12:44:53 +02:00
@staticmethod
def find_regex(haystack, needle):
2015-07-02 12:44:53 +02:00
regex = re.compile(needle)
try:
return regex.search(haystack).groups()[0]
except AttributeError:
return None
2022-10-31 16:13:22 +00:00
2018-02-21 18:55:37 +01:00
def _validate(self, data):
for k, v in list(data.items()):
2017-08-01 19:56:46 +07:00
if k == 'contacts':
return self._validate(v)
if not (bitcoin.is_address(k) or is_valid_email(k)):
2017-08-01 19:56:46 +07:00
data.pop(k)
else:
2018-02-21 18:55:37 +01:00
_type, _ = v
if _type not in ('address', 'lnaddress'):
2017-08-01 19:56:46 +07:00
data.pop(k)
return data
2015-07-02 12:44:53 +02:00