URI: 
       tpaytoedit.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tpaytoedit.py (10710B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - lightweight Bitcoin client
            4 # Copyright (C) 2012 thomasv@gitorious
            5 #
            6 # Permission is hereby granted, free of charge, to any person
            7 # obtaining a copy of this software and associated documentation files
            8 # (the "Software"), to deal in the Software without restriction,
            9 # including without limitation the rights to use, copy, modify, merge,
           10 # publish, distribute, sublicense, and/or sell copies of the Software,
           11 # and to permit persons to whom the Software is furnished to do so,
           12 # subject to the following conditions:
           13 #
           14 # The above copyright notice and this permission notice shall be
           15 # included in all copies or substantial portions of the Software.
           16 #
           17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           24 # SOFTWARE.
           25 
           26 import re
           27 import decimal
           28 from decimal import Decimal
           29 from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
           30 
           31 from PyQt5.QtGui import QFontMetrics, QFont
           32 
           33 from electrum import bitcoin
           34 from electrum.util import bfh, maybe_extract_bolt11_invoice, BITCOIN_BIP21_URI_SCHEME
           35 from electrum.transaction import PartialTxOutput
           36 from electrum.bitcoin import opcodes, construct_script
           37 from electrum.logging import Logger
           38 from electrum.lnaddr import LnDecodeException
           39 
           40 from .qrtextedit import ScanQRTextEdit
           41 from .completion_text_edit import CompletionTextEdit
           42 from . import util
           43 from .util import MONOSPACE_FONT
           44 
           45 if TYPE_CHECKING:
           46     from .main_window import ElectrumWindow
           47 
           48 
           49 RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>'
           50 
           51 frozen_style = "QWidget {border:none;}"
           52 normal_style = "QPlainTextEdit { }"
           53 
           54 
           55 class PayToLineError(NamedTuple):
           56     line_content: str
           57     exc: Exception
           58     idx: int = 0  # index of line
           59     is_multiline: bool = False
           60 
           61 
           62 class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
           63 
           64     def __init__(self, win: 'ElectrumWindow'):
           65         CompletionTextEdit.__init__(self)
           66         ScanQRTextEdit.__init__(self, config=win.config)
           67         Logger.__init__(self)
           68         self.win = win
           69         self.amount_edit = win.amount_e
           70         self.setFont(QFont(MONOSPACE_FONT))
           71         self.document().contentsChanged.connect(self.update_size)
           72         self.heightMin = 0
           73         self.heightMax = 150
           74         self.c = None
           75         self.textChanged.connect(self.check_text)
           76         self.outputs = []  # type: List[PartialTxOutput]
           77         self.errors = []  # type: List[PayToLineError]
           78         self.is_pr = False
           79         self.is_alias = False
           80         self.update_size()
           81         self.payto_scriptpubkey = None  # type: Optional[bytes]
           82         self.lightning_invoice = None
           83         self.previous_payto = ''
           84 
           85     def setFrozen(self, b):
           86         self.setReadOnly(b)
           87         self.setStyleSheet(frozen_style if b else normal_style)
           88         for button in self.buttons:
           89             button.setHidden(b)
           90 
           91     def setGreen(self):
           92         self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True))
           93 
           94     def setExpired(self):
           95         self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
           96 
           97     def parse_address_and_amount(self, line) -> PartialTxOutput:
           98         try:
           99             x, y = line.split(',')
          100         except ValueError:
          101             raise Exception("expected two comma-separated values: (address, amount)") from None
          102         scriptpubkey = self.parse_output(x)
          103         amount = self.parse_amount(y)
          104         return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)
          105 
          106     def parse_output(self, x) -> bytes:
          107         try:
          108             address = self.parse_address(x)
          109             return bfh(bitcoin.address_to_script(address))
          110         except Exception:
          111             pass
          112         try:
          113             script = self.parse_script(x)
          114             return bfh(script)
          115         except Exception:
          116             pass
          117         raise Exception("Invalid address or script.")
          118 
          119     def parse_script(self, x):
          120         script = ''
          121         for word in x.split():
          122             if word[0:3] == 'OP_':
          123                 opcode_int = opcodes[word]
          124                 script += construct_script([opcode_int])
          125             else:
          126                 bfh(word)  # to test it is hex data
          127                 script += construct_script([word])
          128         return script
          129 
          130     def parse_amount(self, x):
          131         x = x.strip()
          132         if not x:
          133             raise Exception("Amount is empty")
          134         if x == '!':
          135             return '!'
          136         p = pow(10, self.amount_edit.decimal_point())
          137         try:
          138             return int(p * Decimal(x))
          139         except decimal.InvalidOperation:
          140             raise Exception("Invalid amount")
          141 
          142     def parse_address(self, line):
          143         r = line.strip()
          144         m = re.match('^'+RE_ALIAS+'$', r)
          145         address = str(m.group(2) if m else r)
          146         assert bitcoin.is_address(address)
          147         return address
          148 
          149     def check_text(self):
          150         self.errors = []
          151         if self.is_pr:
          152             return
          153         # filter out empty lines
          154         lines = [i for i in self.lines() if i]
          155 
          156         self.payto_scriptpubkey = None
          157         self.lightning_invoice = None
          158         self.outputs = []
          159 
          160         if len(lines) == 1:
          161             data = lines[0]
          162             # try bip21 URI
          163             if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
          164                 self.win.pay_to_URI(data)
          165                 return
          166             # try LN invoice
          167             bolt11_invoice = maybe_extract_bolt11_invoice(data)
          168             if bolt11_invoice is not None:
          169                 try:
          170                     self.win.parse_lightning_invoice(bolt11_invoice)
          171                 except LnDecodeException as e:
          172                     self.errors.append(PayToLineError(line_content=data, exc=e))
          173                 else:
          174                     self.lightning_invoice = bolt11_invoice
          175                 return
          176             # try "address, amount" on-chain format
          177             try:
          178                 self._parse_as_multiline(lines, raise_errors=True)
          179             except Exception as e:
          180                 pass
          181             else:
          182                 return
          183             # try address/script
          184             try:
          185                 self.payto_scriptpubkey = self.parse_output(data)
          186             except Exception as e:
          187                 self.errors.append(PayToLineError(line_content=data, exc=e))
          188             else:
          189                 self.win.set_onchain(True)
          190                 self.win.lock_amount(False)
          191                 return
          192         else:
          193             # there are multiple lines
          194             self._parse_as_multiline(lines, raise_errors=False)
          195 
          196     def _parse_as_multiline(self, lines, *, raise_errors: bool):
          197         outputs = []  # type: List[PartialTxOutput]
          198         total = 0
          199         is_max = False
          200         for i, line in enumerate(lines):
          201             try:
          202                 output = self.parse_address_and_amount(line)
          203             except Exception as e:
          204                 if raise_errors:
          205                     raise
          206                 else:
          207                     self.errors.append(PayToLineError(
          208                         idx=i, line_content=line.strip(), exc=e, is_multiline=True))
          209                     continue
          210             outputs.append(output)
          211             if output.value == '!':
          212                 is_max = True
          213             else:
          214                 total += output.value
          215         if outputs:
          216             self.win.set_onchain(True)
          217 
          218         self.win.max_button.setChecked(is_max)
          219         self.outputs = outputs
          220         self.payto_scriptpubkey = None
          221 
          222         if self.win.max_button.isChecked():
          223             self.win.spend_max()
          224         else:
          225             self.amount_edit.setAmount(total if outputs else None)
          226         self.win.lock_amount(self.win.max_button.isChecked() or bool(outputs))
          227 
          228     def get_errors(self) -> Sequence[PayToLineError]:
          229         return self.errors
          230 
          231     def get_destination_scriptpubkey(self) -> Optional[bytes]:
          232         return self.payto_scriptpubkey
          233 
          234     def get_outputs(self, is_max):
          235         if self.payto_scriptpubkey:
          236             if is_max:
          237                 amount = '!'
          238             else:
          239                 amount = self.amount_edit.get_amount()
          240             self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)]
          241 
          242         return self.outputs[:]
          243 
          244     def lines(self):
          245         return self.toPlainText().split('\n')
          246 
          247     def is_multiline(self):
          248         return len(self.lines()) > 1
          249 
          250     def paytomany(self):
          251         self.setText("\n\n\n")
          252         self.update_size()
          253 
          254     def update_size(self):
          255         lineHeight = QFontMetrics(self.document().defaultFont()).height()
          256         docHeight = self.document().size().height()
          257         h = round(docHeight * lineHeight + 11)
          258         h = min(max(h, self.heightMin), self.heightMax)
          259         self.setMinimumHeight(h)
          260         self.setMaximumHeight(h)
          261         self.verticalScrollBar().hide()
          262 
          263     def qr_input(self):
          264         data = super(PayToEdit,self).qr_input()
          265         if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
          266             self.win.pay_to_URI(data)
          267             # TODO: update fee
          268 
          269     def resolve(self):
          270         self.is_alias = False
          271         if self.hasFocus():
          272             return
          273         if self.is_multiline():  # only supports single line entries atm
          274             return
          275         if self.is_pr:
          276             return
          277         key = str(self.toPlainText())
          278         key = key.strip()  # strip whitespaces
          279         if key == self.previous_payto:
          280             return
          281         self.previous_payto = key
          282         if not (('.' in key) and (not '<' in key) and (not ' ' in key)):
          283             return
          284         parts = key.split(sep=',')  # assuming single line
          285         if parts and len(parts) > 0 and bitcoin.is_address(parts[0]):
          286             return
          287         try:
          288             data = self.win.contacts.resolve(key)
          289         except Exception as e:
          290             self.logger.info(f'error resolving address/alias: {repr(e)}')
          291             return
          292         if not data:
          293             return
          294         self.is_alias = True
          295 
          296         address = data.get('address')
          297         name = data.get('name')
          298         new_url = key + ' <' + address + '>'
          299         self.setText(new_url)
          300         self.previous_payto = new_url
          301 
          302         #if self.win.config.get('openalias_autoadd') == 'checked':
          303         self.win.contacts[key] = ('openalias', name)
          304         self.win.contact_list.update()
          305 
          306         self.setFrozen(True)
          307         if data.get('type') == 'openalias':
          308             self.validated = data.get('validated')
          309             if self.validated:
          310                 self.setGreen()
          311             else:
          312                 self.setExpired()
          313         else:
          314             self.validated = None