URI: 
       tlabels.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tlabels.py (7615B)
       ---
            1 import asyncio
            2 import hashlib
            3 import json
            4 import sys
            5 import traceback
            6 from typing import Union, TYPE_CHECKING
            7 
            8 import base64
            9 
           10 from electrum.plugin import BasePlugin, hook
           11 from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv
           12 from electrum.i18n import _
           13 from electrum.util import log_exceptions, ignore_exceptions, make_aiohttp_session
           14 from electrum.network import Network
           15 
           16 if TYPE_CHECKING:
           17     from electrum.wallet import Abstract_Wallet
           18 
           19 
           20 class ErrorConnectingServer(Exception):
           21     def __init__(self, reason: Union[str, Exception] = None):
           22         self.reason = reason
           23 
           24     def __str__(self):
           25         header = _("Error connecting to {} server").format('Labels')
           26         reason = self.reason
           27         if isinstance(reason, BaseException):
           28             reason = repr(reason)
           29         return f"{header}: {reason}" if reason else header
           30 
           31 
           32 class LabelsPlugin(BasePlugin):
           33 
           34     def __init__(self, parent, config, name):
           35         BasePlugin.__init__(self, parent, config, name)
           36         self.target_host = 'labels.electrum.org'
           37         self.wallets = {}
           38 
           39     def encode(self, wallet: 'Abstract_Wallet', msg: str) -> str:
           40         password, iv, wallet_id = self.wallets[wallet]
           41         encrypted = aes_encrypt_with_iv(password, iv, msg.encode('utf8'))
           42         return base64.b64encode(encrypted).decode()
           43 
           44     def decode(self, wallet: 'Abstract_Wallet', message: str) -> str:
           45         password, iv, wallet_id = self.wallets[wallet]
           46         decoded = base64.b64decode(message)
           47         decrypted = aes_decrypt_with_iv(password, iv, decoded)
           48         return decrypted.decode('utf8')
           49 
           50     def get_nonce(self, wallet: 'Abstract_Wallet'):
           51         # nonce is the nonce to be used with the next change
           52         nonce = wallet.db.get('wallet_nonce')
           53         if nonce is None:
           54             nonce = 1
           55             self.set_nonce(wallet, nonce)
           56         return nonce
           57 
           58     def set_nonce(self, wallet: 'Abstract_Wallet', nonce):
           59         self.logger.info(f"set {wallet.basename()} nonce to {nonce}")
           60         wallet.db.put("wallet_nonce", nonce)
           61 
           62     @hook
           63     def set_label(self, wallet: 'Abstract_Wallet', item, label):
           64         if wallet not in self.wallets:
           65             return
           66         if not item:
           67             return
           68         nonce = self.get_nonce(wallet)
           69         wallet_id = self.wallets[wallet][2]
           70         bundle = {"walletId": wallet_id,
           71                   "walletNonce": nonce,
           72                   "externalId": self.encode(wallet, item),
           73                   "encryptedLabel": self.encode(wallet, label)}
           74         asyncio.run_coroutine_threadsafe(self.do_post_safe("/label", bundle), wallet.network.asyncio_loop)
           75         # Caller will write the wallet
           76         self.set_nonce(wallet, nonce + 1)
           77 
           78     @ignore_exceptions
           79     @log_exceptions
           80     async def do_post_safe(self, *args):
           81         await self.do_post(*args)
           82 
           83     async def do_get(self, url = "/labels"):
           84         url = 'https://' + self.target_host + url
           85         network = Network.get_instance()
           86         proxy = network.proxy if network else None
           87         async with make_aiohttp_session(proxy) as session:
           88             async with session.get(url) as result:
           89                 return await result.json()
           90 
           91     async def do_post(self, url = "/labels", data=None):
           92         url = 'https://' + self.target_host + url
           93         network = Network.get_instance()
           94         proxy = network.proxy if network else None
           95         async with make_aiohttp_session(proxy) as session:
           96             async with session.post(url, json=data) as result:
           97                 try:
           98                     return await result.json()
           99                 except Exception as e:
          100                     raise Exception('Could not decode: ' + await result.text()) from e
          101 
          102     async def push_thread(self, wallet: 'Abstract_Wallet'):
          103         wallet_data = self.wallets.get(wallet, None)
          104         if not wallet_data:
          105             raise Exception('Wallet {} not loaded'.format(wallet))
          106         wallet_id = wallet_data[2]
          107         bundle = {"labels": [],
          108                   "walletId": wallet_id,
          109                   "walletNonce": self.get_nonce(wallet)}
          110         for key, value in wallet.get_all_labels().items():
          111             try:
          112                 encoded_key = self.encode(wallet, key)
          113                 encoded_value = self.encode(wallet, value)
          114             except:
          115                 self.logger.info(f'cannot encode {repr(key)} {repr(value)}')
          116                 continue
          117             bundle["labels"].append({'encryptedLabel': encoded_value,
          118                                      'externalId': encoded_key})
          119         await self.do_post("/labels", bundle)
          120 
          121     async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool):
          122         wallet_data = self.wallets.get(wallet, None)
          123         if not wallet_data:
          124             raise Exception('Wallet {} not loaded'.format(wallet))
          125         wallet_id = wallet_data[2]
          126         nonce = 1 if force else self.get_nonce(wallet) - 1
          127         self.logger.info(f"asking for labels since nonce {nonce}")
          128         try:
          129             response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id))
          130         except Exception as e:
          131             raise ErrorConnectingServer(e) from e
          132         if response["labels"] is None:
          133             self.logger.info('no new labels')
          134             return
          135         result = {}
          136         for label in response["labels"]:
          137             try:
          138                 key = self.decode(wallet, label["externalId"])
          139                 value = self.decode(wallet, label["encryptedLabel"])
          140             except:
          141                 continue
          142             try:
          143                 json.dumps(key)
          144                 json.dumps(value)
          145             except:
          146                 self.logger.info(f'error: no json {key}')
          147                 continue
          148             result[key] = value
          149 
          150         for key, value in result.items():
          151             if force or not wallet.get_label(key):
          152                 wallet._set_label(key, value)
          153 
          154         self.logger.info(f"received {len(response)} labels")
          155         self.set_nonce(wallet, response["nonce"] + 1)
          156         self.on_pulled(wallet)
          157 
          158     def on_pulled(self, wallet: 'Abstract_Wallet') -> None:
          159         raise NotImplementedError()
          160 
          161     @ignore_exceptions
          162     @log_exceptions
          163     async def pull_safe_thread(self, wallet: 'Abstract_Wallet', force: bool):
          164         try:
          165             await self.pull_thread(wallet, force)
          166         except ErrorConnectingServer as e:
          167             self.logger.info(repr(e))
          168 
          169     def pull(self, wallet: 'Abstract_Wallet', force: bool):
          170         if not wallet.network: raise Exception(_('You are offline.'))
          171         return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result()
          172 
          173     def push(self, wallet: 'Abstract_Wallet'):
          174         if not wallet.network: raise Exception(_('You are offline.'))
          175         return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result()
          176 
          177     def start_wallet(self, wallet: 'Abstract_Wallet'):
          178         if not wallet.network: return  # 'offline' mode
          179         mpk = wallet.get_fingerprint()
          180         if not mpk:
          181             return
          182         mpk = mpk.encode('ascii')
          183         password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii')
          184         iv = hashlib.sha256(password).digest()[:16]
          185         wallet_id = hashlib.sha256(mpk).hexdigest()
          186         self.wallets[wallet] = (password, iv, wallet_id)
          187         nonce = self.get_nonce(wallet)
          188         self.logger.info(f"wallet {wallet.basename()} nonce is {nonce}")
          189         # If there is an auth token we can try to actually start syncing
          190         asyncio.run_coroutine_threadsafe(self.pull_safe_thread(wallet, False), wallet.network.asyncio_loop)
          191 
          192     def stop_wallet(self, wallet):
          193         self.wallets.pop(wallet, None)