URI: 
       tinvoices: deal with expiration of "0" mess - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 7962e17df67f0196af7ebdf2780d04a80a8ccedc
   DIR parent 4c177c4c9269a3fb8dda8536f4d7f663c0e80180
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Wed,  4 Mar 2020 14:24:07 +0100
       
       invoices: deal with expiration of "0" mess
       
       Internally, we've been using an expiration of 0 to mean "never expires".
       For LN invoices, BOLT-11 does not specify what an expiration of 0 means.
       Other clients seem to treat it as "0 seconds" (i.e. already expired).
       This means there is no way to create a BOLT-11 invoice that "never" expires.
       
       For LN invoices,
       - we now treat an expiration of 0, , as "0 seconds",
       - when creating an invoice, if the user selected never, we will put 100 years as expiration
       
       Diffstat:
         M electrum/gui/kivy/uix/screens.py    |       4 ++--
         M electrum/gui/qt/main_window.py      |       6 +++---
         M electrum/lnaddr.py                  |      25 +++++++++++++++----------
         M electrum/lnworker.py                |       8 +++++++-
         M electrum/util.py                    |       7 ++++++-
       
       5 files changed, 33 insertions(+), 17 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
       t@@ -24,7 +24,7 @@ from kivy.utils import platform
        from kivy.logger import Logger
        
        from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
       -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
       +from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING
        from electrum import bitcoin, constants
        from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
        from electrum.util import (parse_URI, InvalidBitcoinURI, PR_PAID, PR_UNKNOWN, PR_EXPIRED,
       t@@ -419,7 +419,7 @@ class ReceiveScreen(CScreen):
                Clock.schedule_interval(lambda dt: self.update(), 5)
        
            def expiry(self):
       -        return self.app.electrum_config.get('request_expiry', 3600) # 1 hour
       +        return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
        
            def clear(self):
                self.screen.address = ''
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -62,7 +62,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
                                   get_new_wallet_name, send_exception_to_crash_reporter,
                                   InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
                                   NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
       -from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
       +from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING
        from electrum.transaction import (Transaction, PartialTxInput,
                                          PartialTransaction, PartialTxOutput)
        from electrum.address_synchronizer import AddTransactionException
       t@@ -1007,7 +1007,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                evl = sorted(pr_expiration_values.items())
                evl_keys = [i[0] for i in evl]
                evl_values = [i[1] for i in evl]
       -        default_expiry = self.config.get('request_expiry', 3600)
       +        default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
                try:
                    i = evl_keys.index(default_expiry)
                except ValueError:
       t@@ -1139,7 +1139,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
            def create_invoice(self, is_lightning):
                amount = self.receive_amount_e.get_amount()
                message = self.receive_message_e.text()
       -        expiry = self.config.get('request_expiry', 3600)
       +        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
                if is_lightning:
                    key = self.wallet.lnworker.add_request(amount, message, expiry)
                else:
   DIR diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
       t@@ -199,6 +199,8 @@ def lnencode(addr, privkey):
                    # Get minimal length by trimming leading 5 bits at a time.
                    expirybits = bitstring.pack('intbe:64', v)[4:64]
                    while expirybits.startswith('0b00000'):
       +                if len(expirybits) == 5:
       +                    break  # v == 0
                        expirybits = expirybits[5:]
                    data += tagged('x', expirybits)
                elif k == 'h':
       t@@ -259,21 +261,24 @@ class LnAddr(object):
                return self._min_final_cltv_expiry
        
            def get_tag(self, tag):
       -        description = ''
       -        for k,v in self.tags:
       +        for k, v in self.tags:
                    if k == tag:
       -                description = v
       -                break
       -        return description
       +                return v
       +        return None
        
       -    def get_description(self):
       -        return self.get_tag('d')
       +    def get_description(self) -> str:
       +        return self.get_tag('d') or ''
        
       -    def get_expiry(self):
       -        return int(self.get_tag('x') or '3600')
       +    def get_expiry(self) -> int:
       +        exp = self.get_tag('x')
       +        if exp is None:
       +            exp = 3600
       +        return int(exp)
        
       -    def is_expired(self):
       +    def is_expired(self) -> bool:
                now = time.time()
       +        # BOLT-11 does not specify what expiration of '0' means.
       +        # we treat it as 0 seconds here (instead of never)
                return now > self.get_expiry() + self.date
        
        
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -1112,7 +1112,7 @@ class LNWallet(LNWorker):
                    raise Exception(_("add invoice timed out"))
        
            @log_exceptions
       -    async def _add_request_coro(self, amount_sat, message, expiry):
       +    async def _add_request_coro(self, amount_sat, message, expiry: int):
                timestamp = int(time.time())
                routing_hints = await self._calc_routing_hints_for_invoice(amount_sat)
                if not routing_hints:
       t@@ -1122,6 +1122,12 @@ class LNWallet(LNWorker):
                payment_hash = sha256(payment_preimage)
                info = PaymentInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID)
                amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
       +        if expiry == 0:
       +            # hack: BOLT-11 is not really clear on what an expiry of 0 means.
       +            # It probably interprets it as 0 seconds, so already expired...
       +            # Our higher level invoices code however uses 0 for "never".
       +            # Hence set some high expiration here
       +            expiry = 100 * 365 * 24 * 60 * 60  # 100 years
                lnaddr = LnAddr(payment_hash, amount_btc,
                                tags=[('d', message),
                                      ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -104,17 +104,22 @@ pr_tooltips = {
            PR_FAILED:_('Failed'),
        }
        
       +PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60  # 1 day
        pr_expiration_values = {
            0: _('Never'),
            10*60: _('10 minutes'),
            60*60: _('1 hour'),
            24*60*60: _('1 day'),
       -    7*24*60*60: _('1 week')
       +    7*24*60*60: _('1 week'),
        }
       +assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
       +
        
        def get_request_status(req):
            status = req['status']
            exp = req.get('exp', 0) or 0
       +    if req.get('type') == PR_TYPE_LN and exp == 0:
       +        status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
            if req['status'] == PR_UNPAID and exp > 0 and req['time'] + req['exp'] < time.time():
                status = PR_EXPIRED
            status_str = pr_tooltips[status]