URI: 
       tutil.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tutil.py (52263B)
       ---
            1 # Electrum - lightweight Bitcoin client
            2 # Copyright (C) 2011 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 binascii
           24 import os, sys, re, json
           25 from collections import defaultdict, OrderedDict
           26 from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any,
           27                     Sequence, Dict, Generic, TypeVar, List, Iterable)
           28 from datetime import datetime
           29 import decimal
           30 from decimal import Decimal
           31 import traceback
           32 import urllib
           33 import threading
           34 import hmac
           35 import stat
           36 from locale import localeconv
           37 import asyncio
           38 import urllib.request, urllib.parse, urllib.error
           39 import builtins
           40 import json
           41 import time
           42 from typing import NamedTuple, Optional
           43 import ssl
           44 import ipaddress
           45 from ipaddress import IPv4Address, IPv6Address
           46 import random
           47 import secrets
           48 import functools
           49 from abc import abstractmethod, ABC
           50 
           51 import attr
           52 import aiohttp
           53 from aiohttp_socks import ProxyConnector, ProxyType
           54 import aiorpcx
           55 from aiorpcx import TaskGroup
           56 import certifi
           57 import dns.resolver
           58 
           59 from .i18n import _
           60 from .logging import get_logger, Logger
           61 
           62 if TYPE_CHECKING:
           63     from .network import Network
           64     from .interface import Interface
           65     from .simple_config import SimpleConfig
           66 
           67 
           68 _logger = get_logger(__name__)
           69 
           70 
           71 def inv_dict(d):
           72     return {v: k for k, v in d.items()}
           73 
           74 
           75 ca_path = certifi.where()
           76 
           77 
           78 base_units = {'BTC':8, 'mBTC':5, 'bits':2, 'sat':0}
           79 base_units_inverse = inv_dict(base_units)
           80 base_units_list = ['BTC', 'mBTC', 'bits', 'sat']  # list(dict) does not guarantee order
           81 
           82 DECIMAL_POINT_DEFAULT = 5  # mBTC
           83 
           84 
           85 class UnknownBaseUnit(Exception): pass
           86 
           87 
           88 def decimal_point_to_base_unit_name(dp: int) -> str:
           89     # e.g. 8 -> "BTC"
           90     try:
           91         return base_units_inverse[dp]
           92     except KeyError:
           93         raise UnknownBaseUnit(dp) from None
           94 
           95 
           96 def base_unit_name_to_decimal_point(unit_name: str) -> int:
           97     # e.g. "BTC" -> 8
           98     try:
           99         return base_units[unit_name]
          100     except KeyError:
          101         raise UnknownBaseUnit(unit_name) from None
          102 
          103 
          104 class NotEnoughFunds(Exception):
          105     def __str__(self):
          106         return _("Insufficient funds")
          107 
          108 
          109 class NoDynamicFeeEstimates(Exception):
          110     def __str__(self):
          111         return _('Dynamic fee estimates not available')
          112 
          113 
          114 class MultipleSpendMaxTxOutputs(Exception):
          115     def __str__(self):
          116         return _('At most one output can be set to spend max')
          117 
          118 
          119 class InvalidPassword(Exception):
          120     def __str__(self):
          121         return _("Incorrect password")
          122 
          123 
          124 class AddTransactionException(Exception):
          125     pass
          126 
          127 
          128 class UnrelatedTransactionException(AddTransactionException):
          129     def __str__(self):
          130         return _("Transaction is unrelated to this wallet.")
          131 
          132 
          133 class FileImportFailed(Exception):
          134     def __init__(self, message=''):
          135         self.message = str(message)
          136 
          137     def __str__(self):
          138         return _("Failed to import from file.") + "\n" + self.message
          139 
          140 
          141 class FileExportFailed(Exception):
          142     def __init__(self, message=''):
          143         self.message = str(message)
          144 
          145     def __str__(self):
          146         return _("Failed to export to file.") + "\n" + self.message
          147 
          148 
          149 class WalletFileException(Exception): pass
          150 
          151 
          152 class BitcoinException(Exception): pass
          153 
          154 
          155 class UserFacingException(Exception):
          156     """Exception that contains information intended to be shown to the user."""
          157 
          158 
          159 class InvoiceError(UserFacingException): pass
          160 
          161 
          162 # Throw this exception to unwind the stack like when an error occurs.
          163 # However unlike other exceptions the user won't be informed.
          164 class UserCancelled(Exception):
          165     '''An exception that is suppressed from the user'''
          166     pass
          167 
          168 
          169 # note: this is not a NamedTuple as then its json encoding cannot be customized
          170 class Satoshis(object):
          171     __slots__ = ('value',)
          172 
          173     def __new__(cls, value):
          174         self = super(Satoshis, cls).__new__(cls)
          175         # note: 'value' sometimes has msat precision
          176         self.value = value
          177         return self
          178 
          179     def __repr__(self):
          180         return f'Satoshis({self.value})'
          181 
          182     def __str__(self):
          183         # note: precision is truncated to satoshis here
          184         return format_satoshis(self.value)
          185 
          186     def __eq__(self, other):
          187         return self.value == other.value
          188 
          189     def __ne__(self, other):
          190         return not (self == other)
          191 
          192     def __add__(self, other):
          193         return Satoshis(self.value + other.value)
          194 
          195 
          196 # note: this is not a NamedTuple as then its json encoding cannot be customized
          197 class Fiat(object):
          198     __slots__ = ('value', 'ccy')
          199 
          200     def __new__(cls, value: Optional[Decimal], ccy: str):
          201         self = super(Fiat, cls).__new__(cls)
          202         self.ccy = ccy
          203         if not isinstance(value, (Decimal, type(None))):
          204             raise TypeError(f"value should be Decimal or None, not {type(value)}")
          205         self.value = value
          206         return self
          207 
          208     def __repr__(self):
          209         return 'Fiat(%s)'% self.__str__()
          210 
          211     def __str__(self):
          212         if self.value is None or self.value.is_nan():
          213             return _('No Data')
          214         else:
          215             return "{:.2f}".format(self.value)
          216 
          217     def to_ui_string(self):
          218         if self.value is None or self.value.is_nan():
          219             return _('No Data')
          220         else:
          221             return "{:.2f}".format(self.value) + ' ' + self.ccy
          222 
          223     def __eq__(self, other):
          224         if not isinstance(other, Fiat):
          225             return False
          226         if self.ccy != other.ccy:
          227             return False
          228         if isinstance(self.value, Decimal) and isinstance(other.value, Decimal) \
          229                 and self.value.is_nan() and other.value.is_nan():
          230             return True
          231         return self.value == other.value
          232 
          233     def __ne__(self, other):
          234         return not (self == other)
          235 
          236     def __add__(self, other):
          237         assert self.ccy == other.ccy
          238         return Fiat(self.value + other.value, self.ccy)
          239 
          240 
          241 class MyEncoder(json.JSONEncoder):
          242     def default(self, obj):
          243         # note: this does not get called for namedtuples :(  https://bugs.python.org/issue30343
          244         from .transaction import Transaction, TxOutput
          245         from .lnutil import UpdateAddHtlc
          246         if isinstance(obj, UpdateAddHtlc):
          247             return obj.to_tuple()
          248         if isinstance(obj, Transaction):
          249             return obj.serialize()
          250         if isinstance(obj, TxOutput):
          251             return obj.to_legacy_tuple()
          252         if isinstance(obj, Satoshis):
          253             return str(obj)
          254         if isinstance(obj, Fiat):
          255             return str(obj)
          256         if isinstance(obj, Decimal):
          257             return str(obj)
          258         if isinstance(obj, datetime):
          259             return obj.isoformat(' ')[:-3]
          260         if isinstance(obj, set):
          261             return list(obj)
          262         if isinstance(obj, bytes): # for nametuples in lnchannel
          263             return obj.hex()
          264         if hasattr(obj, 'to_json') and callable(obj.to_json):
          265             return obj.to_json()
          266         return super(MyEncoder, self).default(obj)
          267 
          268 
          269 class ThreadJob(Logger):
          270     """A job that is run periodically from a thread's main loop.  run() is
          271     called from that thread's context.
          272     """
          273 
          274     def __init__(self):
          275         Logger.__init__(self)
          276 
          277     def run(self):
          278         """Called periodically from the thread"""
          279         pass
          280 
          281 class DebugMem(ThreadJob):
          282     '''A handy class for debugging GC memory leaks'''
          283     def __init__(self, classes, interval=30):
          284         ThreadJob.__init__(self)
          285         self.next_time = 0
          286         self.classes = classes
          287         self.interval = interval
          288 
          289     def mem_stats(self):
          290         import gc
          291         self.logger.info("Start memscan")
          292         gc.collect()
          293         objmap = defaultdict(list)
          294         for obj in gc.get_objects():
          295             for class_ in self.classes:
          296                 if isinstance(obj, class_):
          297                     objmap[class_].append(obj)
          298         for class_, objs in objmap.items():
          299             self.logger.info(f"{class_.__name__}: {len(objs)}")
          300         self.logger.info("Finish memscan")
          301 
          302     def run(self):
          303         if time.time() > self.next_time:
          304             self.mem_stats()
          305             self.next_time = time.time() + self.interval
          306 
          307 class DaemonThread(threading.Thread, Logger):
          308     """ daemon thread that terminates cleanly """
          309 
          310     LOGGING_SHORTCUT = 'd'
          311 
          312     def __init__(self):
          313         threading.Thread.__init__(self)
          314         Logger.__init__(self)
          315         self.parent_thread = threading.currentThread()
          316         self.running = False
          317         self.running_lock = threading.Lock()
          318         self.job_lock = threading.Lock()
          319         self.jobs = []
          320         self.stopped_event = threading.Event()  # set when fully stopped
          321 
          322     def add_jobs(self, jobs):
          323         with self.job_lock:
          324             self.jobs.extend(jobs)
          325 
          326     def run_jobs(self):
          327         # Don't let a throwing job disrupt the thread, future runs of
          328         # itself, or other jobs.  This is useful protection against
          329         # malformed or malicious server responses
          330         with self.job_lock:
          331             for job in self.jobs:
          332                 try:
          333                     job.run()
          334                 except Exception as e:
          335                     self.logger.exception('')
          336 
          337     def remove_jobs(self, jobs):
          338         with self.job_lock:
          339             for job in jobs:
          340                 self.jobs.remove(job)
          341 
          342     def start(self):
          343         with self.running_lock:
          344             self.running = True
          345         return threading.Thread.start(self)
          346 
          347     def is_running(self):
          348         with self.running_lock:
          349             return self.running and self.parent_thread.is_alive()
          350 
          351     def stop(self):
          352         with self.running_lock:
          353             self.running = False
          354 
          355     def on_stop(self):
          356         if 'ANDROID_DATA' in os.environ:
          357             import jnius
          358             jnius.detach()
          359             self.logger.info("jnius detach")
          360         self.logger.info("stopped")
          361         self.stopped_event.set()
          362 
          363 
          364 def print_stderr(*args):
          365     args = [str(item) for item in args]
          366     sys.stderr.write(" ".join(args) + "\n")
          367     sys.stderr.flush()
          368 
          369 def print_msg(*args):
          370     # Stringify args
          371     args = [str(item) for item in args]
          372     sys.stdout.write(" ".join(args) + "\n")
          373     sys.stdout.flush()
          374 
          375 def json_encode(obj):
          376     try:
          377         s = json.dumps(obj, sort_keys = True, indent = 4, cls=MyEncoder)
          378     except TypeError:
          379         s = repr(obj)
          380     return s
          381 
          382 def json_decode(x):
          383     try:
          384         return json.loads(x, parse_float=Decimal)
          385     except:
          386         return x
          387 
          388 def json_normalize(x):
          389     # note: The return value of commands, when going through the JSON-RPC interface,
          390     #       is json-encoded. The encoder used there cannot handle some types, e.g. electrum.util.Satoshis.
          391     # note: We should not simply do "json_encode(x)" here, as then later x would get doubly json-encoded.
          392     # see #5868
          393     return json_decode(json_encode(x))
          394 
          395 
          396 # taken from Django Source Code
          397 def constant_time_compare(val1, val2):
          398     """Return True if the two strings are equal, False otherwise."""
          399     return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8'))
          400 
          401 
          402 # decorator that prints execution time
          403 _profiler_logger = _logger.getChild('profiler')
          404 def profiler(func):
          405     def do_profile(args, kw_args):
          406         name = func.__qualname__
          407         t0 = time.time()
          408         o = func(*args, **kw_args)
          409         t = time.time() - t0
          410         _profiler_logger.debug(f"{name} {t:,.4f}")
          411         return o
          412     return lambda *args, **kw_args: do_profile(args, kw_args)
          413 
          414 
          415 def android_ext_dir():
          416     from android.storage import primary_external_storage_path
          417     return primary_external_storage_path()
          418 
          419 def android_backup_dir():
          420     d = os.path.join(android_ext_dir(), 'org.electrum.electrum')
          421     if not os.path.exists(d):
          422         os.mkdir(d)
          423     return d
          424 
          425 def android_data_dir():
          426     import jnius
          427     PythonActivity = jnius.autoclass('org.kivy.android.PythonActivity')
          428     return PythonActivity.mActivity.getFilesDir().getPath() + '/data'
          429 
          430 def get_backup_dir(config):
          431     if 'ANDROID_DATA' in os.environ:
          432         return android_backup_dir() if config.get('android_backups') else None
          433     else:
          434         return config.get('backup_dir')
          435 
          436 def ensure_sparse_file(filename):
          437     # On modern Linux, no need to do anything.
          438     # On Windows, need to explicitly mark file.
          439     if os.name == "nt":
          440         try:
          441             os.system('fsutil sparse setflag "{}" 1'.format(filename))
          442         except Exception as e:
          443             _logger.info(f'error marking file {filename} as sparse: {e}')
          444 
          445 
          446 def get_headers_dir(config):
          447     return config.path
          448 
          449 
          450 def assert_datadir_available(config_path):
          451     path = config_path
          452     if os.path.exists(path):
          453         return
          454     else:
          455         raise FileNotFoundError(
          456             'Electrum datadir does not exist. Was it deleted while running?' + '\n' +
          457             'Should be at {}'.format(path))
          458 
          459 
          460 def assert_file_in_datadir_available(path, config_path):
          461     if os.path.exists(path):
          462         return
          463     else:
          464         assert_datadir_available(config_path)
          465         raise FileNotFoundError(
          466             'Cannot find file but datadir is there.' + '\n' +
          467             'Should be at {}'.format(path))
          468 
          469 
          470 def standardize_path(path):
          471     return os.path.normcase(
          472             os.path.realpath(
          473                 os.path.abspath(
          474                     os.path.expanduser(
          475                         path
          476     ))))
          477 
          478 
          479 def get_new_wallet_name(wallet_folder: str) -> str:
          480     i = 1
          481     while True:
          482         filename = "wallet_%d" % i
          483         if filename in os.listdir(wallet_folder):
          484             i += 1
          485         else:
          486             break
          487     return filename
          488 
          489 
          490 def assert_bytes(*args):
          491     """
          492     porting helper, assert args type
          493     """
          494     try:
          495         for x in args:
          496             assert isinstance(x, (bytes, bytearray))
          497     except:
          498         print('assert bytes failed', list(map(type, args)))
          499         raise
          500 
          501 
          502 def assert_str(*args):
          503     """
          504     porting helper, assert args type
          505     """
          506     for x in args:
          507         assert isinstance(x, str)
          508 
          509 
          510 def to_string(x, enc) -> str:
          511     if isinstance(x, (bytes, bytearray)):
          512         return x.decode(enc)
          513     if isinstance(x, str):
          514         return x
          515     else:
          516         raise TypeError("Not a string or bytes like object")
          517 
          518 
          519 def to_bytes(something, encoding='utf8') -> bytes:
          520     """
          521     cast string to bytes() like object, but for python2 support it's bytearray copy
          522     """
          523     if isinstance(something, bytes):
          524         return something
          525     if isinstance(something, str):
          526         return something.encode(encoding)
          527     elif isinstance(something, bytearray):
          528         return bytes(something)
          529     else:
          530         raise TypeError("Not a string or bytes like object")
          531 
          532 
          533 bfh = bytes.fromhex
          534 
          535 
          536 def bh2u(x: bytes) -> str:
          537     """
          538     str with hex representation of a bytes-like object
          539 
          540     >>> x = bytes((1, 2, 10))
          541     >>> bh2u(x)
          542     '01020A'
          543     """
          544     return x.hex()
          545 
          546 
          547 def xor_bytes(a: bytes, b: bytes) -> bytes:
          548     size = min(len(a), len(b))
          549     return ((int.from_bytes(a[:size], "big") ^ int.from_bytes(b[:size], "big"))
          550             .to_bytes(size, "big"))
          551 
          552 
          553 def user_dir():
          554     if "ELECTRUMDIR" in os.environ:
          555         return os.environ["ELECTRUMDIR"]
          556     elif 'ANDROID_DATA' in os.environ:
          557         return android_data_dir()
          558     elif os.name == 'posix':
          559         return os.path.join(os.environ["HOME"], ".electrum")
          560     elif "APPDATA" in os.environ:
          561         return os.path.join(os.environ["APPDATA"], "Electrum")
          562     elif "LOCALAPPDATA" in os.environ:
          563         return os.path.join(os.environ["LOCALAPPDATA"], "Electrum")
          564     else:
          565         #raise Exception("No home directory found in environment variables.")
          566         return
          567 
          568 
          569 def resource_path(*parts):
          570     return os.path.join(pkg_dir, *parts)
          571 
          572 
          573 # absolute path to python package folder of electrum ("lib")
          574 pkg_dir = os.path.split(os.path.realpath(__file__))[0]
          575 
          576 
          577 def is_valid_email(s):
          578     regexp = r"[^@]+@[^@]+\.[^@]+"
          579     return re.match(regexp, s) is not None
          580 
          581 
          582 def is_hash256_str(text: Any) -> bool:
          583     if not isinstance(text, str): return False
          584     if len(text) != 64: return False
          585     return is_hex_str(text)
          586 
          587 
          588 def is_hex_str(text: Any) -> bool:
          589     if not isinstance(text, str): return False
          590     try:
          591         b = bytes.fromhex(text)
          592     except:
          593         return False
          594     # forbid whitespaces in text:
          595     if len(text) != 2 * len(b):
          596         return False
          597     return True
          598 
          599 
          600 def is_integer(val: Any) -> bool:
          601     return isinstance(val, int)
          602 
          603 
          604 def is_non_negative_integer(val: Any) -> bool:
          605     if is_integer(val):
          606         return val >= 0
          607     return False
          608 
          609 
          610 def is_int_or_float(val: Any) -> bool:
          611     return isinstance(val, (int, float))
          612 
          613 
          614 def is_non_negative_int_or_float(val: Any) -> bool:
          615     if is_int_or_float(val):
          616         return val >= 0
          617     return False
          618 
          619 
          620 def chunks(items, size: int):
          621     """Break up items, an iterable, into chunks of length size."""
          622     if size < 1:
          623         raise ValueError(f"size must be positive, not {repr(size)}")
          624     for i in range(0, len(items), size):
          625         yield items[i: i + size]
          626 
          627 
          628 def format_satoshis_plain(x, *, decimal_point=8) -> str:
          629     """Display a satoshi amount scaled.  Always uses a '.' as a decimal
          630     point and has no thousands separator"""
          631     if x == '!':
          632         return 'max'
          633     scale_factor = pow(10, decimal_point)
          634     return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.')
          635 
          636 
          637 # Check that Decimal precision is sufficient.
          638 # We need at the very least ~20, as we deal with msat amounts, and
          639 # log10(21_000_000 * 10**8 * 1000) ~= 18.3
          640 # decimal.DefaultContext.prec == 28 by default, but it is mutable.
          641 # We enforce that we have at least that available.
          642 assert decimal.getcontext().prec >= 28, f"PyDecimal precision too low: {decimal.getcontext().prec}"
          643 
          644 DECIMAL_POINT = localeconv()['decimal_point']  # type: str
          645 
          646 
          647 def format_satoshis(
          648         x,  # in satoshis
          649         *,
          650         num_zeros=0,
          651         decimal_point=8,
          652         precision=None,
          653         is_diff=False,
          654         whitespaces=False,
          655 ) -> str:
          656     if x is None:
          657         return 'unknown'
          658     if x == '!':
          659         return 'max'
          660     if precision is None:
          661         precision = decimal_point
          662     # format string
          663     decimal_format = "." + str(precision) if precision > 0 else ""
          664     if is_diff:
          665         decimal_format = '+' + decimal_format
          666     # initial result
          667     scale_factor = pow(10, decimal_point)
          668     if not isinstance(x, Decimal):
          669         x = Decimal(x).quantize(Decimal('1E-8'))
          670     result = ("{:" + decimal_format + "f}").format(x / scale_factor)
          671     if "." not in result: result += "."
          672     result = result.rstrip('0')
          673     # extra decimal places
          674     integer_part, fract_part = result.split(".")
          675     if len(fract_part) < num_zeros:
          676         fract_part += "0" * (num_zeros - len(fract_part))
          677     result = integer_part + DECIMAL_POINT + fract_part
          678     # leading/trailing whitespaces
          679     if whitespaces:
          680         result += " " * (decimal_point - len(fract_part))
          681         result = " " * (15 - len(result)) + result
          682     return result
          683 
          684 
          685 FEERATE_PRECISION = 1  # num fractional decimal places for sat/byte fee rates
          686 _feerate_quanta = Decimal(10) ** (-FEERATE_PRECISION)
          687 
          688 
          689 def format_fee_satoshis(fee, *, num_zeros=0, precision=None):
          690     if precision is None:
          691         precision = FEERATE_PRECISION
          692     num_zeros = min(num_zeros, FEERATE_PRECISION)  # no more zeroes than available prec
          693     return format_satoshis(fee, num_zeros=num_zeros, decimal_point=0, precision=precision)
          694 
          695 
          696 def quantize_feerate(fee) -> Union[None, Decimal, int]:
          697     """Strip sat/byte fee rate of excess precision."""
          698     if fee is None:
          699         return None
          700     return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN)
          701 
          702 
          703 def timestamp_to_datetime(timestamp):
          704     if timestamp is None:
          705         return None
          706     return datetime.fromtimestamp(timestamp)
          707 
          708 def format_time(timestamp):
          709     date = timestamp_to_datetime(timestamp)
          710     return date.isoformat(' ')[:-3] if date else _("Unknown")
          711 
          712 
          713 # Takes a timestamp and returns a string with the approximation of the age
          714 def age(from_date, since_date = None, target_tz=None, include_seconds=False):
          715     if from_date is None:
          716         return "Unknown"
          717 
          718     from_date = datetime.fromtimestamp(from_date)
          719     if since_date is None:
          720         since_date = datetime.now(target_tz)
          721 
          722     td = time_difference(from_date - since_date, include_seconds)
          723     return td + " ago" if from_date < since_date else "in " + td
          724 
          725 
          726 def time_difference(distance_in_time, include_seconds):
          727     #distance_in_time = since_date - from_date
          728     distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds)))
          729     distance_in_minutes = int(round(distance_in_seconds/60))
          730 
          731     if distance_in_minutes == 0:
          732         if include_seconds:
          733             return "%s seconds" % distance_in_seconds
          734         else:
          735             return "less than a minute"
          736     elif distance_in_minutes < 45:
          737         return "%s minutes" % distance_in_minutes
          738     elif distance_in_minutes < 90:
          739         return "about 1 hour"
          740     elif distance_in_minutes < 1440:
          741         return "about %d hours" % (round(distance_in_minutes / 60.0))
          742     elif distance_in_minutes < 2880:
          743         return "1 day"
          744     elif distance_in_minutes < 43220:
          745         return "%d days" % (round(distance_in_minutes / 1440))
          746     elif distance_in_minutes < 86400:
          747         return "about 1 month"
          748     elif distance_in_minutes < 525600:
          749         return "%d months" % (round(distance_in_minutes / 43200))
          750     elif distance_in_minutes < 1051200:
          751         return "about 1 year"
          752     else:
          753         return "over %d years" % (round(distance_in_minutes / 525600))
          754 
          755 mainnet_block_explorers = {
          756     'Bitupper Explorer': ('https://bitupper.com/en/explorer/bitcoin/',
          757                         {'tx': 'transactions/', 'addr': 'addresses/'}),
          758     'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/',
          759                         {'tx': 'Transaction/', 'addr': 'Address/'}),
          760     'Blockchain.info': ('https://blockchain.com/btc/',
          761                         {'tx': 'tx/', 'addr': 'address/'}),
          762     'blockchainbdgpzk.onion': ('https://blockchainbdgpzk.onion/',
          763                         {'tx': 'tx/', 'addr': 'address/'}),
          764     'Blockstream.info': ('https://blockstream.info/',
          765                         {'tx': 'tx/', 'addr': 'address/'}),
          766     'Bitaps.com': ('https://btc.bitaps.com/',
          767                         {'tx': '', 'addr': ''}),
          768     'BTC.com': ('https://btc.com/',
          769                         {'tx': '', 'addr': ''}),
          770     'Chain.so': ('https://www.chain.so/',
          771                         {'tx': 'tx/BTC/', 'addr': 'address/BTC/'}),
          772     'Insight.is': ('https://insight.bitpay.com/',
          773                         {'tx': 'tx/', 'addr': 'address/'}),
          774     'TradeBlock.com': ('https://tradeblock.com/blockchain/',
          775                         {'tx': 'tx/', 'addr': 'address/'}),
          776     'BlockCypher.com': ('https://live.blockcypher.com/btc/',
          777                         {'tx': 'tx/', 'addr': 'address/'}),
          778     'Blockchair.com': ('https://blockchair.com/bitcoin/',
          779                         {'tx': 'transaction/', 'addr': 'address/'}),
          780     'blockonomics.co': ('https://www.blockonomics.co/',
          781                         {'tx': 'api/tx?txid=', 'addr': '#/search?q='}),
          782     'mempool.space': ('https://mempool.space/',
          783                         {'tx': 'tx/', 'addr': 'address/'}),
          784     'mempool.emzy.de': ('https://mempool.emzy.de/',
          785                         {'tx': 'tx/', 'addr': 'address/'}),  
          786     'OXT.me': ('https://oxt.me/',
          787                         {'tx': 'transaction/', 'addr': 'address/'}),
          788     'smartbit.com.au': ('https://www.smartbit.com.au/',
          789                         {'tx': 'tx/', 'addr': 'address/'}),
          790     'mynode.local': ('http://mynode.local:3002/',
          791                         {'tx': 'tx/', 'addr': 'address/'}),
          792     'system default': ('blockchain:/',
          793                         {'tx': 'tx/', 'addr': 'address/'}),
          794 }
          795 
          796 testnet_block_explorers = {
          797     'Bitaps.com': ('https://tbtc.bitaps.com/',
          798                        {'tx': '', 'addr': ''}),
          799     'BlockCypher.com': ('https://live.blockcypher.com/btc-testnet/',
          800                        {'tx': 'tx/', 'addr': 'address/'}),
          801     'Blockchain.info': ('https://www.blockchain.com/btc-testnet/',
          802                        {'tx': 'tx/', 'addr': 'address/'}),
          803     'Blockstream.info': ('https://blockstream.info/testnet/',
          804                         {'tx': 'tx/', 'addr': 'address/'}),
          805     'mempool.space': ('https://mempool.space/testnet/',
          806                         {'tx': 'tx/', 'addr': 'address/'}),    
          807     'smartbit.com.au': ('https://testnet.smartbit.com.au/',
          808                        {'tx': 'tx/', 'addr': 'address/'}),
          809     'system default': ('blockchain://000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943/',
          810                        {'tx': 'tx/', 'addr': 'address/'}),
          811 }
          812 
          813 _block_explorer_default_api_loc = {'tx': 'tx/', 'addr': 'address/'}
          814 
          815 
          816 def block_explorer_info():
          817     from . import constants
          818     return mainnet_block_explorers if not constants.net.TESTNET else testnet_block_explorers
          819 
          820 
          821 def block_explorer(config: 'SimpleConfig') -> Optional[str]:
          822     """Returns name of selected block explorer,
          823     or None if a custom one (not among hardcoded ones) is configured.
          824     """
          825     if config.get('block_explorer_custom') is not None:
          826         return None
          827     default_ = 'Blockstream.info'
          828     be_key = config.get('block_explorer', default_)
          829     be_tuple = block_explorer_info().get(be_key)
          830     if be_tuple is None:
          831         be_key = default_
          832     assert isinstance(be_key, str), f"{be_key!r} should be str"
          833     return be_key
          834 
          835 
          836 def block_explorer_tuple(config: 'SimpleConfig') -> Optional[Tuple[str, dict]]:
          837     custom_be = config.get('block_explorer_custom')
          838     if custom_be:
          839         if isinstance(custom_be, str):
          840             return custom_be, _block_explorer_default_api_loc
          841         if isinstance(custom_be, (tuple, list)) and len(custom_be) == 2:
          842             return tuple(custom_be)
          843         _logger.warning(f"not using 'block_explorer_custom' from config. "
          844                         f"expected a str or a pair but got {custom_be!r}")
          845         return None
          846     else:
          847         # using one of the hardcoded block explorers
          848         return block_explorer_info().get(block_explorer(config))
          849 
          850 
          851 def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional[str]:
          852     be_tuple = block_explorer_tuple(config)
          853     if not be_tuple:
          854         return
          855     explorer_url, explorer_dict = be_tuple
          856     kind_str = explorer_dict.get(kind)
          857     if kind_str is None:
          858         return
          859     if explorer_url[-1] != "/":
          860         explorer_url += "/"
          861     url_parts = [explorer_url, kind_str, item]
          862     return ''.join(url_parts)
          863 
          864 # URL decode
          865 #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
          866 #urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
          867 
          868 
          869 # note: when checking against these, use .lower() to support case-insensitivity
          870 BITCOIN_BIP21_URI_SCHEME = 'bitcoin'
          871 LIGHTNING_URI_SCHEME = 'lightning'
          872 
          873 
          874 class InvalidBitcoinURI(Exception): pass
          875 
          876 
          877 # TODO rename to parse_bip21_uri or similar
          878 def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict:
          879     """Raises InvalidBitcoinURI on malformed URI."""
          880     from . import bitcoin
          881     from .bitcoin import COIN
          882 
          883     if not isinstance(uri, str):
          884         raise InvalidBitcoinURI(f"expected string, not {repr(uri)}")
          885 
          886     if ':' not in uri:
          887         if not bitcoin.is_address(uri):
          888             raise InvalidBitcoinURI("Not a bitcoin address")
          889         return {'address': uri}
          890 
          891     u = urllib.parse.urlparse(uri)
          892     if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME:
          893         raise InvalidBitcoinURI("Not a bitcoin URI")
          894     address = u.path
          895 
          896     # python for android fails to parse query
          897     if address.find('?') > 0:
          898         address, query = u.path.split('?')
          899         pq = urllib.parse.parse_qs(query)
          900     else:
          901         pq = urllib.parse.parse_qs(u.query)
          902 
          903     for k, v in pq.items():
          904         if len(v) != 1:
          905             raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}')
          906 
          907     out = {k: v[0] for k, v in pq.items()}
          908     if address:
          909         if not bitcoin.is_address(address):
          910             raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}")
          911         out['address'] = address
          912     if 'amount' in out:
          913         am = out['amount']
          914         try:
          915             m = re.match(r'([0-9.]+)X([0-9])', am)
          916             if m:
          917                 k = int(m.group(2)) - 8
          918                 amount = Decimal(m.group(1)) * pow(  Decimal(10) , k)
          919             else:
          920                 amount = Decimal(am) * COIN
          921             out['amount'] = int(amount)
          922         except Exception as e:
          923             raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e
          924     if 'message' in out:
          925         out['message'] = out['message']
          926         out['memo'] = out['message']
          927     if 'time' in out:
          928         try:
          929             out['time'] = int(out['time'])
          930         except Exception as e:
          931             raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e
          932     if 'exp' in out:
          933         try:
          934             out['exp'] = int(out['exp'])
          935         except Exception as e:
          936             raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e
          937     if 'sig' in out:
          938         try:
          939             out['sig'] = bh2u(bitcoin.base_decode(out['sig'], base=58))
          940         except Exception as e:
          941             raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e
          942 
          943     r = out.get('r')
          944     sig = out.get('sig')
          945     name = out.get('name')
          946     if on_pr and (r or (name and sig)):
          947         @log_exceptions
          948         async def get_payment_request():
          949             from . import paymentrequest as pr
          950             if name and sig:
          951                 s = pr.serialize_request(out).SerializeToString()
          952                 request = pr.PaymentRequest(s)
          953             else:
          954                 request = await pr.get_payment_request(r)
          955             if on_pr:
          956                 on_pr(request)
          957         loop = loop or asyncio.get_event_loop()
          958         asyncio.run_coroutine_threadsafe(get_payment_request(), loop)
          959 
          960     return out
          961 
          962 
          963 def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str],
          964                      *, extra_query_params: Optional[dict] = None) -> str:
          965     from . import bitcoin
          966     if not bitcoin.is_address(addr):
          967         return ""
          968     if extra_query_params is None:
          969         extra_query_params = {}
          970     query = []
          971     if amount_sat:
          972         query.append('amount=%s'%format_satoshis_plain(amount_sat))
          973     if message:
          974         query.append('message=%s'%urllib.parse.quote(message))
          975     for k, v in extra_query_params.items():
          976         if not isinstance(k, str) or k != urllib.parse.quote(k):
          977             raise Exception(f"illegal key for URI: {repr(k)}")
          978         v = urllib.parse.quote(v)
          979         query.append(f"{k}={v}")
          980     p = urllib.parse.ParseResult(
          981         scheme=BITCOIN_BIP21_URI_SCHEME,
          982         netloc='',
          983         path=addr,
          984         params='',
          985         query='&'.join(query),
          986         fragment='',
          987     )
          988     return str(urllib.parse.urlunparse(p))
          989 
          990 
          991 def maybe_extract_bolt11_invoice(data: str) -> Optional[str]:
          992     data = data.strip()  # whitespaces
          993     data = data.lower()
          994     if data.startswith(LIGHTNING_URI_SCHEME + ':ln'):
          995         data = data[10:]
          996     if data.startswith('ln'):
          997         return data
          998     return None
          999 
         1000 
         1001 # Python bug (http://bugs.python.org/issue1927) causes raw_input
         1002 # to be redirected improperly between stdin/stderr on Unix systems
         1003 #TODO: py3
         1004 def raw_input(prompt=None):
         1005     if prompt:
         1006         sys.stdout.write(prompt)
         1007     return builtin_raw_input()
         1008 
         1009 builtin_raw_input = builtins.input
         1010 builtins.input = raw_input
         1011 
         1012 
         1013 def parse_json(message):
         1014     # TODO: check \r\n pattern
         1015     n = message.find(b'\n')
         1016     if n==-1:
         1017         return None, message
         1018     try:
         1019         j = json.loads(message[0:n].decode('utf8'))
         1020     except:
         1021         j = None
         1022     return j, message[n+1:]
         1023 
         1024 
         1025 def setup_thread_excepthook():
         1026     """
         1027     Workaround for `sys.excepthook` thread bug from:
         1028     http://bugs.python.org/issue1230540
         1029 
         1030     Call once from the main thread before creating any threads.
         1031     """
         1032 
         1033     init_original = threading.Thread.__init__
         1034 
         1035     def init(self, *args, **kwargs):
         1036 
         1037         init_original(self, *args, **kwargs)
         1038         run_original = self.run
         1039 
         1040         def run_with_except_hook(*args2, **kwargs2):
         1041             try:
         1042                 run_original(*args2, **kwargs2)
         1043             except Exception:
         1044                 sys.excepthook(*sys.exc_info())
         1045 
         1046         self.run = run_with_except_hook
         1047 
         1048     threading.Thread.__init__ = init
         1049 
         1050 
         1051 def send_exception_to_crash_reporter(e: BaseException):
         1052     sys.excepthook(type(e), e, e.__traceback__)
         1053 
         1054 
         1055 def versiontuple(v):
         1056     return tuple(map(int, (v.split("."))))
         1057 
         1058 
         1059 def read_json_file(path):
         1060     try:
         1061         with open(path, 'r', encoding='utf-8') as f:
         1062             data = json.loads(f.read())
         1063     #backwards compatibility for JSONDecodeError
         1064     except ValueError:
         1065         _logger.exception('')
         1066         raise FileImportFailed(_("Invalid JSON code."))
         1067     except BaseException as e:
         1068         _logger.exception('')
         1069         raise FileImportFailed(e)
         1070     return data
         1071 
         1072 def write_json_file(path, data):
         1073     try:
         1074         with open(path, 'w+', encoding='utf-8') as f:
         1075             json.dump(data, f, indent=4, sort_keys=True, cls=MyEncoder)
         1076     except (IOError, os.error) as e:
         1077         _logger.exception('')
         1078         raise FileExportFailed(e)
         1079 
         1080 
         1081 def make_dir(path, allow_symlink=True):
         1082     """Make directory if it does not yet exist."""
         1083     if not os.path.exists(path):
         1084         if not allow_symlink and os.path.islink(path):
         1085             raise Exception('Dangling link: ' + path)
         1086         os.mkdir(path)
         1087         os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
         1088 
         1089 
         1090 def log_exceptions(func):
         1091     """Decorator to log AND re-raise exceptions."""
         1092     assert asyncio.iscoroutinefunction(func), 'func needs to be a coroutine'
         1093     @functools.wraps(func)
         1094     async def wrapper(*args, **kwargs):
         1095         self = args[0] if len(args) > 0 else None
         1096         try:
         1097             return await func(*args, **kwargs)
         1098         except asyncio.CancelledError as e:
         1099             raise
         1100         except BaseException as e:
         1101             mylogger = self.logger if hasattr(self, 'logger') else _logger
         1102             try:
         1103                 mylogger.exception(f"Exception in {func.__name__}: {repr(e)}")
         1104             except BaseException as e2:
         1105                 print(f"logging exception raised: {repr(e2)}... orig exc: {repr(e)} in {func.__name__}")
         1106             raise
         1107     return wrapper
         1108 
         1109 
         1110 def ignore_exceptions(func):
         1111     """Decorator to silently swallow all exceptions."""
         1112     assert asyncio.iscoroutinefunction(func), 'func needs to be a coroutine'
         1113     @functools.wraps(func)
         1114     async def wrapper(*args, **kwargs):
         1115         try:
         1116             return await func(*args, **kwargs)
         1117         except asyncio.CancelledError:
         1118             # note: with python 3.8, CancelledError no longer inherits Exception, so this catch is redundant
         1119             raise
         1120         except Exception as e:
         1121             pass
         1122     return wrapper
         1123 
         1124 
         1125 class TxMinedInfo(NamedTuple):
         1126     height: int                        # height of block that mined tx
         1127     conf: Optional[int] = None         # number of confirmations, SPV verified (None means unknown)
         1128     timestamp: Optional[int] = None    # timestamp of block that mined tx
         1129     txpos: Optional[int] = None        # position of tx in serialized block
         1130     header_hash: Optional[str] = None  # hash of block that mined tx
         1131 
         1132 
         1133 def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None):
         1134     if headers is None:
         1135         headers = {'User-Agent': 'Electrum'}
         1136     if timeout is None:
         1137         # The default timeout is high intentionally.
         1138         # DNS on some systems can be really slow, see e.g. #5337
         1139         timeout = aiohttp.ClientTimeout(total=45)
         1140     elif isinstance(timeout, (int, float)):
         1141         timeout = aiohttp.ClientTimeout(total=timeout)
         1142     ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)
         1143 
         1144     if proxy:
         1145         connector = ProxyConnector(
         1146             proxy_type=ProxyType.SOCKS5 if proxy['mode'] == 'socks5' else ProxyType.SOCKS4,
         1147             host=proxy['host'],
         1148             port=int(proxy['port']),
         1149             username=proxy.get('user', None),
         1150             password=proxy.get('password', None),
         1151             rdns=True,
         1152             ssl=ssl_context,
         1153         )
         1154     else:
         1155         connector = aiohttp.TCPConnector(ssl=ssl_context)
         1156 
         1157     return aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector)
         1158 
         1159 
         1160 class SilentTaskGroup(TaskGroup):
         1161 
         1162     def spawn(self, *args, **kwargs):
         1163         # don't complain if group is already closed.
         1164         if self._closed:
         1165             raise asyncio.CancelledError()
         1166         return super().spawn(*args, **kwargs)
         1167 
         1168 
         1169 class NetworkJobOnDefaultServer(Logger, ABC):
         1170     """An abstract base class for a job that runs on the main network
         1171     interface. Every time the main interface changes, the job is
         1172     restarted, and some of its internals are reset.
         1173     """
         1174     def __init__(self, network: 'Network'):
         1175         Logger.__init__(self)
         1176         asyncio.set_event_loop(network.asyncio_loop)
         1177         self.network = network
         1178         self.interface = None  # type: Interface
         1179         self._restart_lock = asyncio.Lock()
         1180         # Ensure fairness between NetworkJobs. e.g. if multiple wallets
         1181         # are open, a large wallet's Synchronizer should not starve the small wallets:
         1182         self._network_request_semaphore = asyncio.Semaphore(100)
         1183 
         1184         self._reset()
         1185         # every time the main interface changes, restart:
         1186         register_callback(self._restart, ['default_server_changed'])
         1187         # also schedule a one-off restart now, as there might already be a main interface:
         1188         asyncio.run_coroutine_threadsafe(self._restart(), network.asyncio_loop)
         1189 
         1190     def _reset(self):
         1191         """Initialise fields. Called every time the underlying
         1192         server connection changes.
         1193         """
         1194         self.taskgroup = SilentTaskGroup()
         1195 
         1196     async def _start(self, interface: 'Interface'):
         1197         self.interface = interface
         1198         await interface.taskgroup.spawn(self._run_tasks(taskgroup=self.taskgroup))
         1199 
         1200     @abstractmethod
         1201     async def _run_tasks(self, *, taskgroup: TaskGroup) -> None:
         1202         """Start tasks in taskgroup. Called every time the underlying
         1203         server connection changes.
         1204         """
         1205         # If self.taskgroup changed, don't start tasks. This can happen if we have
         1206         # been restarted *just now*, i.e. after the _run_tasks coroutine object was created.
         1207         if taskgroup != self.taskgroup:
         1208             raise asyncio.CancelledError()
         1209 
         1210     async def stop(self, *, full_shutdown: bool = True):
         1211         if full_shutdown:
         1212             unregister_callback(self._restart)
         1213         await self.taskgroup.cancel_remaining()
         1214 
         1215     @log_exceptions
         1216     async def _restart(self, *args):
         1217         interface = self.network.interface
         1218         if interface is None:
         1219             return  # we should get called again soon
         1220 
         1221         async with self._restart_lock:
         1222             await self.stop(full_shutdown=False)
         1223             self._reset()
         1224             await self._start(interface)
         1225 
         1226     @property
         1227     def session(self):
         1228         # ORIG: s = self.interface.session
         1229         # TODO: libbitcoin
         1230         s = self.interface.client
         1231         assert s is not None
         1232         return s
         1233 
         1234 
         1235 def create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop,
         1236                                            asyncio.Future,
         1237                                            threading.Thread]:
         1238     def on_exception(loop, context):
         1239         """Suppress spurious messages it appears we cannot control."""
         1240         SUPPRESS_MESSAGE_REGEX = re.compile('SSL handshake|Fatal read error on|'
         1241                                             'SSL error in data received')
         1242         message = context.get('message')
         1243         if message and SUPPRESS_MESSAGE_REGEX.match(message):
         1244             return
         1245         loop.default_exception_handler(context)
         1246 
         1247     loop = asyncio.get_event_loop()
         1248     loop.set_exception_handler(on_exception)
         1249     # loop.set_debug(1)
         1250     stopping_fut = asyncio.Future()
         1251     loop_thread = threading.Thread(target=loop.run_until_complete,
         1252                                          args=(stopping_fut,),
         1253                                          name='EventLoop')
         1254     loop_thread.start()
         1255     loop._mythread = loop_thread
         1256     return loop, stopping_fut, loop_thread
         1257 
         1258 
         1259 class OrderedDictWithIndex(OrderedDict):
         1260     """An OrderedDict that keeps track of the positions of keys.
         1261 
         1262     Note: very inefficient to modify contents, except to add new items.
         1263     """
         1264 
         1265     def __init__(self):
         1266         super().__init__()
         1267         self._key_to_pos = {}
         1268         self._pos_to_key = {}
         1269 
         1270     def _recalc_index(self):
         1271         self._key_to_pos = {key: pos for (pos, key) in enumerate(self.keys())}
         1272         self._pos_to_key = {pos: key for (pos, key) in enumerate(self.keys())}
         1273 
         1274     def pos_from_key(self, key):
         1275         return self._key_to_pos[key]
         1276 
         1277     def value_from_pos(self, pos):
         1278         key = self._pos_to_key[pos]
         1279         return self[key]
         1280 
         1281     def popitem(self, *args, **kwargs):
         1282         ret = super().popitem(*args, **kwargs)
         1283         self._recalc_index()
         1284         return ret
         1285 
         1286     def move_to_end(self, *args, **kwargs):
         1287         ret = super().move_to_end(*args, **kwargs)
         1288         self._recalc_index()
         1289         return ret
         1290 
         1291     def clear(self):
         1292         ret = super().clear()
         1293         self._recalc_index()
         1294         return ret
         1295 
         1296     def pop(self, *args, **kwargs):
         1297         ret = super().pop(*args, **kwargs)
         1298         self._recalc_index()
         1299         return ret
         1300 
         1301     def update(self, *args, **kwargs):
         1302         ret = super().update(*args, **kwargs)
         1303         self._recalc_index()
         1304         return ret
         1305 
         1306     def __delitem__(self, *args, **kwargs):
         1307         ret = super().__delitem__(*args, **kwargs)
         1308         self._recalc_index()
         1309         return ret
         1310 
         1311     def __setitem__(self, key, *args, **kwargs):
         1312         is_new_key = key not in self
         1313         ret = super().__setitem__(key, *args, **kwargs)
         1314         if is_new_key:
         1315             pos = len(self) - 1
         1316             self._key_to_pos[key] = pos
         1317             self._pos_to_key[pos] = key
         1318         return ret
         1319 
         1320 
         1321 def multisig_type(wallet_type):
         1322     '''If wallet_type is mofn multi-sig, return [m, n],
         1323     otherwise return None.'''
         1324     if not wallet_type:
         1325         return None
         1326     match = re.match(r'(\d+)of(\d+)', wallet_type)
         1327     if match:
         1328         match = [int(x) for x in match.group(1, 2)]
         1329     return match
         1330 
         1331 
         1332 def is_ip_address(x: Union[str, bytes]) -> bool:
         1333     if isinstance(x, bytes):
         1334         x = x.decode("utf-8")
         1335     try:
         1336         ipaddress.ip_address(x)
         1337         return True
         1338     except ValueError:
         1339         return False
         1340 
         1341 
         1342 def is_private_netaddress(host: str) -> bool:
         1343     if str(host) in ('localhost', 'localhost.',):
         1344         return True
         1345     if host[0] == '[' and host[-1] == ']':  # IPv6
         1346         host = host[1:-1]
         1347     try:
         1348         ip_addr = ipaddress.ip_address(host)  # type: Union[IPv4Address, IPv6Address]
         1349         return ip_addr.is_private
         1350     except ValueError:
         1351         pass  # not an IP
         1352     return False
         1353 
         1354 
         1355 def list_enabled_bits(x: int) -> Sequence[int]:
         1356     """e.g. 77 (0b1001101) --> (0, 2, 3, 6)"""
         1357     binary = bin(x)[2:]
         1358     rev_bin = reversed(binary)
         1359     return tuple(i for i, b in enumerate(rev_bin) if b == '1')
         1360 
         1361 
         1362 def resolve_dns_srv(host: str):
         1363     srv_records = dns.resolver.resolve(host, 'SRV')
         1364     # priority: prefer lower
         1365     # weight: tie breaker; prefer higher
         1366     srv_records = sorted(srv_records, key=lambda x: (x.priority, -x.weight))
         1367 
         1368     def dict_from_srv_record(srv):
         1369         return {
         1370             'host': str(srv.target),
         1371             'port': srv.port,
         1372         }
         1373     return [dict_from_srv_record(srv) for srv in srv_records]
         1374 
         1375 
         1376 def randrange(bound: int) -> int:
         1377     """Return a random integer k such that 1 <= k < bound, uniformly
         1378     distributed across that range."""
         1379     # secrets.randbelow(bound) returns a random int: 0 <= r < bound,
         1380     # hence transformations:
         1381     return secrets.randbelow(bound - 1) + 1
         1382 
         1383 
         1384 class CallbackManager:
         1385     # callbacks set by the GUI or any thread
         1386     # guarantee: the callbacks will always get triggered from the asyncio thread.
         1387 
         1388     def __init__(self):
         1389         self.callback_lock = threading.Lock()
         1390         self.callbacks = defaultdict(list)      # note: needs self.callback_lock
         1391         self.asyncio_loop = None
         1392 
         1393     def register_callback(self, callback, events):
         1394         with self.callback_lock:
         1395             for event in events:
         1396                 self.callbacks[event].append(callback)
         1397 
         1398     def unregister_callback(self, callback):
         1399         with self.callback_lock:
         1400             for callbacks in self.callbacks.values():
         1401                 if callback in callbacks:
         1402                     callbacks.remove(callback)
         1403 
         1404     def trigger_callback(self, event, *args):
         1405         """Trigger a callback with given arguments.
         1406         Can be called from any thread. The callback itself will get scheduled
         1407         on the event loop.
         1408         """
         1409         if self.asyncio_loop is None:
         1410             self.asyncio_loop = asyncio.get_event_loop()
         1411             assert self.asyncio_loop.is_running(), "event loop not running"
         1412         with self.callback_lock:
         1413             callbacks = self.callbacks[event][:]
         1414         for callback in callbacks:
         1415             # FIXME: if callback throws, we will lose the traceback
         1416             if asyncio.iscoroutinefunction(callback):
         1417                 asyncio.run_coroutine_threadsafe(callback(event, *args), self.asyncio_loop)
         1418             else:
         1419                 self.asyncio_loop.call_soon_threadsafe(callback, event, *args)
         1420 
         1421 
         1422 callback_mgr = CallbackManager()
         1423 trigger_callback = callback_mgr.trigger_callback
         1424 register_callback = callback_mgr.register_callback
         1425 unregister_callback = callback_mgr.unregister_callback
         1426 
         1427 
         1428 _NetAddrType = TypeVar("_NetAddrType")
         1429 
         1430 
         1431 class NetworkRetryManager(Generic[_NetAddrType]):
         1432     """Truncated Exponential Backoff for network connections."""
         1433 
         1434     def __init__(
         1435             self, *,
         1436             max_retry_delay_normal: float,
         1437             init_retry_delay_normal: float,
         1438             max_retry_delay_urgent: float = None,
         1439             init_retry_delay_urgent: float = None,
         1440     ):
         1441         self._last_tried_addr = {}  # type: Dict[_NetAddrType, Tuple[float, int]]  # (unix ts, num_attempts)
         1442 
         1443         # note: these all use "seconds" as unit
         1444         if max_retry_delay_urgent is None:
         1445             max_retry_delay_urgent = max_retry_delay_normal
         1446         if init_retry_delay_urgent is None:
         1447             init_retry_delay_urgent = init_retry_delay_normal
         1448         self._max_retry_delay_normal = max_retry_delay_normal
         1449         self._init_retry_delay_normal = init_retry_delay_normal
         1450         self._max_retry_delay_urgent = max_retry_delay_urgent
         1451         self._init_retry_delay_urgent = init_retry_delay_urgent
         1452 
         1453     def _trying_addr_now(self, addr: _NetAddrType) -> None:
         1454         last_time, num_attempts = self._last_tried_addr.get(addr, (0, 0))
         1455         # we add up to 1 second of noise to the time, so that clients are less likely
         1456         # to get synchronised and bombard the remote in connection waves:
         1457         cur_time = time.time() + random.random()
         1458         self._last_tried_addr[addr] = cur_time, num_attempts + 1
         1459 
         1460     def _on_connection_successfully_established(self, addr: _NetAddrType) -> None:
         1461         self._last_tried_addr[addr] = time.time(), 0
         1462 
         1463     def _can_retry_addr(self, addr: _NetAddrType, *,
         1464                         now: float = None, urgent: bool = False) -> bool:
         1465         if now is None:
         1466             now = time.time()
         1467         last_time, num_attempts = self._last_tried_addr.get(addr, (0, 0))
         1468         if urgent:
         1469             max_delay = self._max_retry_delay_urgent
         1470             init_delay = self._init_retry_delay_urgent
         1471         else:
         1472             max_delay = self._max_retry_delay_normal
         1473             init_delay = self._init_retry_delay_normal
         1474         delay = self.__calc_delay(multiplier=init_delay, max_delay=max_delay, num_attempts=num_attempts)
         1475         next_time = last_time + delay
         1476         return next_time < now
         1477 
         1478     @classmethod
         1479     def __calc_delay(cls, *, multiplier: float, max_delay: float,
         1480                      num_attempts: int) -> float:
         1481         num_attempts = min(num_attempts, 100_000)
         1482         try:
         1483             res = multiplier * 2 ** num_attempts
         1484         except OverflowError:
         1485             return max_delay
         1486         return max(0, min(max_delay, res))
         1487 
         1488     def _clear_addr_retry_times(self) -> None:
         1489         self._last_tried_addr.clear()
         1490 
         1491 
         1492 class MySocksProxy(aiorpcx.SOCKSProxy):
         1493 
         1494     async def open_connection(self, host=None, port=None, **kwargs):
         1495         loop = asyncio.get_event_loop()
         1496         reader = asyncio.StreamReader(loop=loop)
         1497         protocol = asyncio.StreamReaderProtocol(reader, loop=loop)
         1498         transport, _ = await self.create_connection(
         1499             lambda: protocol, host, port, **kwargs)
         1500         writer = asyncio.StreamWriter(transport, protocol, reader, loop)
         1501         return reader, writer
         1502 
         1503     @classmethod
         1504     def from_proxy_dict(cls, proxy: dict = None) -> Optional['MySocksProxy']:
         1505         if not proxy:
         1506             return None
         1507         username, pw = proxy.get('user'), proxy.get('password')
         1508         if not username or not pw:
         1509             auth = None
         1510         else:
         1511             auth = aiorpcx.socks.SOCKSUserAuth(username, pw)
         1512         addr = aiorpcx.NetAddress(proxy['host'], proxy['port'])
         1513         if proxy['mode'] == "socks4":
         1514             ret = cls(addr, aiorpcx.socks.SOCKS4a, auth)
         1515         elif proxy['mode'] == "socks5":
         1516             ret = cls(addr, aiorpcx.socks.SOCKS5, auth)
         1517         else:
         1518             raise NotImplementedError  # http proxy not available with aiorpcx
         1519         return ret
         1520 
         1521 
         1522 class JsonRPCClient:
         1523 
         1524     def __init__(self, session: aiohttp.ClientSession, url: str):
         1525         self.session = session
         1526         self.url = url
         1527         self._id = 0
         1528 
         1529     async def request(self, endpoint, *args):
         1530         # TODO: libbitcoin
         1531         self._id += 1
         1532         data = ('{"jsonrpc": "2.0", "id":"%d", "method": "%s", "params": %s }'
         1533                 % (self._id, endpoint, json.dumps(args)))
         1534         async with self.session.post(self.url, data=data) as resp:
         1535             if resp.status == 200:
         1536                 r = await resp.json()
         1537                 result = r.get('result')
         1538                 error = r.get('error')
         1539                 if error:
         1540                     return 'Error: ' + str(error)
         1541                 else:
         1542                     return result
         1543             else:
         1544                 text = await resp.text()
         1545                 return 'Error: ' + str(text)
         1546 
         1547     def add_method(self, endpoint):
         1548         async def coro(*args):
         1549             return await self.request(endpoint, *args)
         1550         setattr(self, endpoint, coro)
         1551 
         1552 
         1553 T = TypeVar('T')
         1554 
         1555 def random_shuffled_copy(x: Iterable[T]) -> List[T]:
         1556     """Returns a shuffled copy of the input."""
         1557     x_copy = list(x)  # copy
         1558     random.shuffle(x_copy)  # shuffle in-place
         1559     return x_copy
         1560 
         1561 
         1562 def test_read_write_permissions(path) -> None:
         1563     # note: There might already be a file at 'path'.
         1564     #       Make sure we do NOT overwrite/corrupt that!
         1565     temp_path = "%s.tmptest.%s" % (path, os.getpid())
         1566     echo = "fs r/w test"
         1567     try:
         1568         # test READ permissions for actual path
         1569         if os.path.exists(path):
         1570             with open(path, "rb") as f:
         1571                 f.read(1)  # read 1 byte
         1572         # test R/W sanity for "similar" path
         1573         with open(temp_path, "w", encoding='utf-8') as f:
         1574             f.write(echo)
         1575         with open(temp_path, "r", encoding='utf-8') as f:
         1576             echo2 = f.read()
         1577         os.remove(temp_path)
         1578     except Exception as e:
         1579         raise IOError(e) from e
         1580     if echo != echo2:
         1581         raise IOError('echo sanity-check failed')
         1582 
         1583 
         1584 class nullcontext:
         1585     """Context manager that does no additional processing.
         1586     This is a ~backport of contextlib.nullcontext from Python 3.10
         1587     """
         1588 
         1589     def __init__(self, enter_result=None):
         1590         self.enter_result = enter_result
         1591 
         1592     def __enter__(self):
         1593         return self.enter_result
         1594 
         1595     def __exit__(self, *excinfo):
         1596         pass
         1597 
         1598     async def __aenter__(self):
         1599         return self.enter_result
         1600 
         1601     async def __aexit__(self, *excinfo):
         1602         pass