URI: 
       tscreens.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tscreens.py (20895B)
       ---
            1 import asyncio
            2 from weakref import ref
            3 from decimal import Decimal
            4 import re
            5 import threading
            6 import traceback, sys
            7 from typing import TYPE_CHECKING, List, Optional, Dict, Any
            8 
            9 from kivy.app import App
           10 from kivy.cache import Cache
           11 from kivy.clock import Clock
           12 from kivy.compat import string_types
           13 from kivy.properties import (ObjectProperty, DictProperty, NumericProperty,
           14                              ListProperty, StringProperty)
           15 
           16 from kivy.uix.recycleview import RecycleView
           17 from kivy.uix.label import Label
           18 from kivy.uix.behaviors import ToggleButtonBehavior
           19 from kivy.uix.image import Image
           20 
           21 from kivy.lang import Builder
           22 from kivy.factory import Factory
           23 from kivy.utils import platform
           24 
           25 from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
           26 from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING,
           27                                PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
           28                                LNInvoice, pr_expiration_values, Invoice, OnchainInvoice)
           29 from electrum import bitcoin, constants
           30 from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
           31 from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
           32 from electrum.wallet import InternalAddressCorruption
           33 from electrum import simple_config
           34 from electrum.lnaddr import lndecode
           35 from electrum.lnutil import RECEIVED, SENT, PaymentFailure
           36 from electrum.logging import Logger
           37 
           38 from .dialogs.question import Question
           39 from .dialogs.lightning_open_channel import LightningOpenChannelDialog
           40 
           41 from electrum.gui.kivy import KIVY_GUI_PATH
           42 from electrum.gui.kivy.i18n import _
           43 
           44 if TYPE_CHECKING:
           45     from electrum.gui.kivy.main_window import ElectrumWindow
           46     from electrum.paymentrequest import PaymentRequest
           47 
           48 
           49 class HistoryRecycleView(RecycleView):
           50     pass
           51 
           52 class RequestRecycleView(RecycleView):
           53     pass
           54 
           55 class PaymentRecycleView(RecycleView):
           56     pass
           57 
           58 class CScreen(Factory.Screen):
           59     __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
           60     action_view = ObjectProperty(None)
           61     kvname = None
           62     app = App.get_running_app()  # type: ElectrumWindow
           63 
           64     def on_enter(self):
           65         # FIXME: use a proper event don't use animation time of screen
           66         Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25)
           67         pass
           68 
           69     def update(self):
           70         pass
           71 
           72     def on_activate(self):
           73         setattr(self.app, self.kvname + '_screen', self)
           74         self.update()
           75 
           76     def on_leave(self):
           77         self.dispatch('on_deactivate')
           78 
           79     def on_deactivate(self):
           80         pass
           81 
           82 
           83 # note: this list needs to be kept in sync with another in qt
           84 TX_ICONS = [
           85     "unconfirmed",
           86     "close",
           87     "unconfirmed",
           88     "close",
           89     "clock1",
           90     "clock2",
           91     "clock3",
           92     "clock4",
           93     "clock5",
           94     "confirmed",
           95 ]
           96 
           97 
           98 Builder.load_file(KIVY_GUI_PATH + '/uix/ui_screens/history.kv')
           99 Builder.load_file(KIVY_GUI_PATH + '/uix/ui_screens/send.kv')
          100 Builder.load_file(KIVY_GUI_PATH + '/uix/ui_screens/receive.kv')
          101 
          102 
          103 class HistoryScreen(CScreen):
          104 
          105     tab = ObjectProperty(None)
          106     kvname = 'history'
          107     cards = {}
          108 
          109     def __init__(self, **kwargs):
          110         self.ra_dialog = None
          111         super(HistoryScreen, self).__init__(**kwargs)
          112 
          113     def show_item(self, obj):
          114         key = obj.key
          115         tx_item = self.history.get(key)
          116         if tx_item.get('lightning') and tx_item['type'] == 'payment':
          117             self.app.lightning_tx_dialog(tx_item)
          118             return
          119         if tx_item.get('lightning'):
          120             tx = self.app.wallet.lnworker.lnwatcher.db.get_transaction(key)
          121         else:
          122             tx = self.app.wallet.db.get_transaction(key)
          123         if not tx:
          124             return
          125         self.app.tx_dialog(tx)
          126 
          127     def get_card(self, tx_item): #tx_hash, tx_mined_status, value, balance):
          128         is_lightning = tx_item.get('lightning', False)
          129         timestamp = tx_item['timestamp']
          130         key = tx_item.get('txid') or tx_item['payment_hash']
          131         if is_lightning:
          132             status = 0
          133             status_str = 'unconfirmed' if timestamp is None else format_time(int(timestamp))
          134             icon = f'atlas://{KIVY_GUI_PATH}/theming/light/lightning'
          135             message = tx_item['label']
          136             fee_msat = tx_item['fee_msat']
          137             fee = int(fee_msat/1000) if fee_msat else None
          138             fee_text = '' if fee is None else 'fee: %d sat'%fee
          139         else:
          140             tx_hash = tx_item['txid']
          141             conf = tx_item['confirmations']
          142             tx_mined_info = TxMinedInfo(height=tx_item['height'],
          143                                         conf=tx_item['confirmations'],
          144                                         timestamp=tx_item['timestamp'])
          145             status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_info)
          146             icon = f'atlas://{KIVY_GUI_PATH}/theming/light/' + TX_ICONS[status]
          147             message = tx_item['label'] or tx_hash
          148             fee = tx_item['fee_sat']
          149             fee_text = '' if fee is None else 'fee: %d sat'%fee
          150         ri = {}
          151         ri['screen'] = self
          152         ri['key'] = key
          153         ri['icon'] = icon
          154         ri['date'] = status_str
          155         ri['message'] = message
          156         ri['fee_text'] = fee_text
          157         value = tx_item['value'].value
          158         if value is not None:
          159             ri['is_mine'] = value <= 0
          160             ri['amount'] = self.app.format_amount(value, is_diff = True)
          161             if 'fiat_value' in tx_item:
          162                 ri['quote_text'] = str(tx_item['fiat_value'])
          163         return ri
          164 
          165     def update(self, see_all=False):
          166         wallet = self.app.wallet
          167         if wallet is None:
          168             return
          169         self.history = wallet.get_full_history(self.app.fx)
          170         history = reversed(self.history.values())
          171         history_card = self.ids.history_container
          172         history_card.data = [self.get_card(item) for item in history]
          173 
          174 
          175 class SendScreen(CScreen, Logger):
          176 
          177     kvname = 'send'
          178     payment_request = None  # type: Optional[PaymentRequest]
          179     parsed_URI = None
          180 
          181     def __init__(self, **kwargs):
          182         CScreen.__init__(self, **kwargs)
          183         Logger.__init__(self)
          184         self.is_max = False
          185 
          186     def set_URI(self, text: str):
          187         if not self.app.wallet:
          188             return
          189         try:
          190             uri = parse_URI(text, self.app.on_pr, loop=self.app.asyncio_loop)
          191         except InvalidBitcoinURI as e:
          192             self.app.show_info(_("Error parsing URI") + f":\n{e}")
          193             return
          194         self.parsed_URI = uri
          195         amount = uri.get('amount')
          196         self.address = uri.get('address', '')
          197         self.message = uri.get('message', '')
          198         self.amount = self.app.format_amount_and_units(amount) if amount else ''
          199         self.is_max = False
          200         self.payment_request = None
          201         self.is_lightning = False
          202 
          203     def set_ln_invoice(self, invoice: str):
          204         try:
          205             invoice = str(invoice).lower()
          206             lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
          207         except Exception as e:
          208             self.app.show_info(invoice + _(" is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == ''
          209             return
          210         self.address = invoice
          211         self.message = dict(lnaddr.tags).get('d', None)
          212         self.amount = self.app.format_amount_and_units(lnaddr.amount * bitcoin.COIN) if lnaddr.amount else ''
          213         self.payment_request = None
          214         self.is_lightning = True
          215 
          216     def update(self):
          217         if self.app.wallet is None:
          218             return
          219         _list = self.app.wallet.get_unpaid_invoices()
          220         _list.reverse()
          221         payments_container = self.ids.payments_container
          222         payments_container.data = [self.get_card(invoice) for invoice in _list]
          223 
          224     def update_item(self, key, invoice):
          225         payments_container = self.ids.payments_container
          226         data = payments_container.data
          227         for item in data:
          228             if item['key'] == key:
          229                 item.update(self.get_card(invoice))
          230         payments_container.data = data
          231         payments_container.refresh_from_data()
          232 
          233     def show_item(self, obj):
          234         self.app.show_invoice(obj.is_lightning, obj.key)
          235 
          236     def get_card(self, item: Invoice) -> Dict[str, Any]:
          237         status = self.app.wallet.get_invoice_status(item)
          238         status_str = item.get_status_str(status)
          239         is_lightning = item.type == PR_TYPE_LN
          240         key = self.app.wallet.get_key_for_outgoing_invoice(item)
          241         if is_lightning:
          242             assert isinstance(item, LNInvoice)
          243             address = item.rhash
          244             if self.app.wallet.lnworker:
          245                 log = self.app.wallet.lnworker.logs.get(key)
          246                 if status == PR_INFLIGHT and log:
          247                     status_str += '... (%d)'%len(log)
          248             is_bip70 = False
          249         else:
          250             assert isinstance(item, OnchainInvoice)
          251             address = item.get_address()
          252             is_bip70 = bool(item.bip70)
          253         return {
          254             'is_lightning': is_lightning,
          255             'is_bip70': is_bip70,
          256             'screen': self,
          257             'status': status,
          258             'status_str': status_str,
          259             'key': key,
          260             'memo': item.message or _('No Description'),
          261             'address': address,
          262             'amount': self.app.format_amount_and_units(item.get_amount_sat() or 0),
          263         }
          264 
          265     def do_clear(self):
          266         self.amount = ''
          267         self.message = ''
          268         self.address = ''
          269         self.payment_request = None
          270         self.is_lightning = False
          271         self.is_bip70 = False
          272         self.parsed_URI = None
          273         self.is_max = False
          274 
          275     def set_request(self, pr: 'PaymentRequest'):
          276         self.address = pr.get_requestor()
          277         amount = pr.get_amount()
          278         self.amount = self.app.format_amount_and_units(amount) if amount else ''
          279         self.message = pr.get_memo()
          280         self.locked = True
          281         self.payment_request = pr
          282 
          283     def do_paste(self):
          284         data = self.app._clipboard.paste().strip()
          285         if not data:
          286             self.app.show_info(_("Clipboard is empty"))
          287             return
          288         # try to decode as transaction
          289         try:
          290             tx = tx_from_any(data)
          291             tx.deserialize()
          292         except:
          293             tx = None
          294         if tx:
          295             self.app.tx_dialog(tx)
          296             return
          297         # try to decode as URI/address
          298         bolt11_invoice = maybe_extract_bolt11_invoice(data)
          299         if bolt11_invoice is not None:
          300             self.set_ln_invoice(bolt11_invoice)
          301         else:
          302             self.set_URI(data)
          303 
          304     def read_invoice(self):
          305         address = str(self.address)
          306         if not address:
          307             self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
          308             return
          309         if not self.amount:
          310             self.app.show_error(_('Please enter an amount'))
          311             return
          312         if self.is_max:
          313             amount = '!'
          314         else:
          315             try:
          316                 amount = self.app.get_amount(self.amount)
          317             except:
          318                 self.app.show_error(_('Invalid amount') + ':\n' + self.amount)
          319                 return
          320         message = self.message
          321         if self.is_lightning:
          322             return LNInvoice.from_bech32(address)
          323         else:  # on-chain
          324             if self.payment_request:
          325                 outputs = self.payment_request.get_outputs()
          326             else:
          327                 if not bitcoin.is_address(address):
          328                     self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
          329                     return
          330                 outputs = [PartialTxOutput.from_address_and_value(address, amount)]
          331             return self.app.wallet.create_invoice(
          332                 outputs=outputs,
          333                 message=message,
          334                 pr=self.payment_request,
          335                 URI=self.parsed_URI)
          336 
          337     def do_save(self):
          338         invoice = self.read_invoice()
          339         if not invoice:
          340             return
          341         self.save_invoice(invoice)
          342 
          343     def save_invoice(self, invoice):
          344         self.app.wallet.save_invoice(invoice)
          345         self.do_clear()
          346         self.update()
          347 
          348     def do_pay(self):
          349         invoice = self.read_invoice()
          350         if not invoice:
          351             return
          352         self.do_pay_invoice(invoice)
          353 
          354     def do_pay_invoice(self, invoice):
          355         if invoice.is_lightning():
          356             if self.app.wallet.lnworker:
          357                 self.app.protected(_('Pay lightning invoice?'), self._do_pay_lightning, (invoice,))
          358             else:
          359                 self.app.show_error(_("Lightning payments are not available for this wallet"))
          360         else:
          361             self._do_pay_onchain(invoice)
          362 
          363     def _do_pay_lightning(self, invoice: LNInvoice, pw) -> None:
          364         def pay_thread():
          365             try:
          366                 coro = self.app.wallet.lnworker.pay_invoice(invoice.invoice, attempts=10)
          367                 fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop)
          368                 fut.result()
          369             except Exception as e:
          370                 self.app.show_error(repr(e))
          371         self.save_invoice(invoice)
          372         threading.Thread(target=pay_thread).start()
          373 
          374     def _do_pay_onchain(self, invoice: OnchainInvoice) -> None:
          375         from .dialogs.confirm_tx_dialog import ConfirmTxDialog
          376         d = ConfirmTxDialog(self.app, invoice)
          377         d.open()
          378 
          379     def send_tx(self, tx, invoice, password):
          380         if self.app.wallet.has_password() and password is None:
          381             return
          382         self.save_invoice(invoice)
          383         def on_success(tx):
          384             if tx.is_complete():
          385                 self.app.broadcast(tx)
          386             else:
          387                 self.app.tx_dialog(tx)
          388         def on_failure(error):
          389             self.app.show_error(error)
          390         if self.app.wallet.can_sign(tx):
          391             self.app.show_info("Signing...")
          392             self.app.sign_tx(tx, password, on_success, on_failure)
          393         else:
          394             self.app.tx_dialog(tx)
          395 
          396 
          397 class ReceiveScreen(CScreen):
          398 
          399     kvname = 'receive'
          400 
          401     def __init__(self, **kwargs):
          402         super(ReceiveScreen, self).__init__(**kwargs)
          403         Clock.schedule_interval(lambda dt: self.update(), 5)
          404         self.is_max = False # not used for receiving (see app.amount_dialog)
          405 
          406     def expiry(self):
          407         return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
          408 
          409     def clear(self):
          410         self.address = ''
          411         self.amount = ''
          412         self.message = ''
          413         self.lnaddr = ''
          414 
          415     def set_address(self, addr):
          416         self.address = addr
          417 
          418     def on_address(self, addr):
          419         req = self.app.wallet.get_request(addr)
          420         self.status = ''
          421         if req:
          422             self.message = req.get('memo', '')
          423             amount = req.get('amount')
          424             self.amount = self.app.format_amount_and_units(amount) if amount else ''
          425             status = req.get('status', PR_UNKNOWN)
          426             self.status = _('Payment received') if status == PR_PAID else ''
          427 
          428     def get_URI(self):
          429         from electrum.util import create_bip21_uri
          430         amount = self.amount
          431         if amount:
          432             a, u = self.amount.split()
          433             assert u == self.app.base_unit
          434             amount = Decimal(a) * pow(10, self.app.decimal_point())
          435         return create_bip21_uri(self.address, amount, self.message)
          436 
          437     def do_copy(self):
          438         uri = self.get_URI()
          439         self.app._clipboard.copy(uri)
          440         self.app.show_info(_('Request copied to clipboard'))
          441 
          442     def new_request(self, lightning):
          443         amount = self.amount
          444         amount = self.app.get_amount(amount) if amount else 0
          445         message = self.message
          446         if lightning:
          447             key = self.app.wallet.lnworker.add_request(amount, message, self.expiry())
          448         else:
          449             addr = self.address or self.app.wallet.get_unused_address()
          450             if not addr:
          451                 if not self.app.wallet.is_deterministic():
          452                     addr = self.app.wallet.get_receiving_address()
          453                 else:
          454                     self.app.show_info(_('No address available. Please remove some of your pending requests.'))
          455                     return
          456             self.address = addr
          457             req = self.app.wallet.make_payment_request(addr, amount, message, self.expiry())
          458             self.app.wallet.add_payment_request(req)
          459             key = addr
          460         self.clear()
          461         self.update()
          462         self.app.show_request(lightning, key)
          463 
          464     def get_card(self, req: Invoice) -> Dict[str, Any]:
          465         is_lightning = req.is_lightning()
          466         if not is_lightning:
          467             assert isinstance(req, OnchainInvoice)
          468             address = req.get_address()
          469         else:
          470             assert isinstance(req, LNInvoice)
          471             address = req.invoice
          472         key = self.app.wallet.get_key_for_receive_request(req)
          473         amount = req.get_amount_sat()
          474         description = req.message
          475         status = self.app.wallet.get_request_status(key)
          476         status_str = req.get_status_str(status)
          477         ci = {}
          478         ci['screen'] = self
          479         ci['address'] = address
          480         ci['is_lightning'] = is_lightning
          481         ci['key'] = key
          482         ci['amount'] = self.app.format_amount_and_units(amount) if amount else ''
          483         ci['memo'] = description or _('No Description')
          484         ci['status'] = status
          485         ci['status_str'] = status_str
          486         return ci
          487 
          488     def update(self):
          489         if self.app.wallet is None:
          490             return
          491         _list = self.app.wallet.get_unpaid_requests()
          492         _list.reverse()
          493         requests_container = self.ids.requests_container
          494         requests_container.data = [self.get_card(item) for item in _list]
          495 
          496     def update_item(self, key, request):
          497         payments_container = self.ids.requests_container
          498         data = payments_container.data
          499         for item in data:
          500             if item['key'] == key:
          501                 status = self.app.wallet.get_request_status(key)
          502                 status_str = request.get_status_str(status)
          503                 item['status'] = status
          504                 item['status_str'] = status_str
          505         payments_container.data = data # needed?
          506         payments_container.refresh_from_data()
          507 
          508     def show_item(self, obj):
          509         self.app.show_request(obj.is_lightning, obj.key)
          510 
          511     def expiration_dialog(self, obj):
          512         from .dialogs.choice_dialog import ChoiceDialog
          513         def callback(c):
          514             self.app.electrum_config.set_key('request_expiry', c)
          515         d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback)
          516         d.open()
          517 
          518 
          519 class TabbedCarousel(Factory.TabbedPanel):
          520     '''Custom TabbedPanel using a carousel used in the Main Screen
          521     '''
          522 
          523     carousel = ObjectProperty(None)
          524 
          525     def animate_tab_to_center(self, value):
          526         scrlv = self._tab_strip.parent
          527         if not scrlv:
          528             return
          529         idx = self.tab_list.index(value)
          530         n = len(self.tab_list)
          531         if idx in [0, 1]:
          532             scroll_x = 1
          533         elif idx in [n-1, n-2]:
          534             scroll_x = 0
          535         else:
          536             scroll_x = 1. * (n - idx - 1) / (n - 1)
          537         mation = Factory.Animation(scroll_x=scroll_x, d=.25)
          538         mation.cancel_all(scrlv)
          539         mation.start(scrlv)
          540 
          541     def on_current_tab(self, instance, value):
          542         self.animate_tab_to_center(value)
          543 
          544     def on_index(self, instance, value):
          545         current_slide = instance.current_slide
          546         if not hasattr(current_slide, 'tab'):
          547             return
          548         tab = current_slide.tab
          549         ct = self.current_tab
          550         try:
          551             if ct.text != tab.text:
          552                 carousel = self.carousel
          553                 carousel.slides[ct.slide].dispatch('on_leave')
          554                 self.switch_to(tab)
          555                 carousel.slides[tab.slide].dispatch('on_enter')
          556         except AttributeError:
          557             current_slide.dispatch('on_enter')
          558 
          559     def switch_to(self, header):
          560         # we have to replace the functionality of the original switch_to
          561         if not header:
          562             return
          563         if not hasattr(header, 'slide'):
          564             header.content = self.carousel
          565             super(TabbedCarousel, self).switch_to(header)
          566             try:
          567                 tab = self.tab_list[-1]
          568             except IndexError:
          569                 return
          570             self._current_tab = tab
          571             tab.state = 'down'
          572             return
          573 
          574         carousel = self.carousel
          575         self.current_tab.state = "normal"
          576         header.state = 'down'
          577         self._current_tab = header
          578         # set the carousel to load the appropriate slide
          579         # saved in the screen attribute of the tab head
          580         slide = carousel.slides[header.slide]
          581         if carousel.current_slide != slide:
          582             carousel.current_slide.dispatch('on_leave')
          583             carousel.load_slide(slide)
          584             slide.dispatch('on_enter')
          585 
          586     def add_widget(self, widget, index=0):
          587         if isinstance(widget, Factory.CScreen):
          588             self.carousel.add_widget(widget)
          589             return
          590         super(TabbedCarousel, self).add_widget(widget, index=index)