URI: 
       ttext.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       ttext.py (19938B)
       ---
            1 import tty
            2 import sys
            3 import curses
            4 import datetime
            5 import locale
            6 from decimal import Decimal
            7 import getpass
            8 import logging
            9 from typing import TYPE_CHECKING
           10 
           11 import electrum
           12 from electrum import util
           13 from electrum.util import format_satoshis
           14 from electrum.bitcoin import is_address, COIN
           15 from electrum.transaction import PartialTxOutput
           16 from electrum.wallet import Wallet
           17 from electrum.wallet_db import WalletDB
           18 from electrum.storage import WalletStorage
           19 from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
           20 from electrum.interface import ServerAddr
           21 from electrum.logging import console_stderr_handler
           22 
           23 if TYPE_CHECKING:
           24     from electrum.daemon import Daemon
           25     from electrum.simple_config import SimpleConfig
           26     from electrum.plugin import Plugins
           27 
           28 
           29 _ = lambda x:x  # i18n
           30 
           31 
           32 class ElectrumGui:
           33 
           34     def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'):
           35 
           36         self.config = config
           37         self.network = daemon.network
           38         storage = WalletStorage(config.get_wallet_path())
           39         if not storage.file_exists():
           40             print("Wallet not found. try 'electrum create'")
           41             exit()
           42         if storage.is_encrypted():
           43             password = getpass.getpass('Password:', stream=None)
           44             storage.decrypt(password)
           45         db = WalletDB(storage.read(), manual_upgrades=False)
           46         self.wallet = Wallet(db, storage, config=config)
           47         self.wallet.start_network(self.network)
           48         self.contacts = self.wallet.contacts
           49 
           50         locale.setlocale(locale.LC_ALL, '')
           51         self.encoding = locale.getpreferredencoding()
           52 
           53         self.stdscr = curses.initscr()
           54         curses.noecho()
           55         curses.cbreak()
           56         curses.start_color()
           57         curses.use_default_colors()
           58         curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
           59         curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_CYAN)
           60         curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)
           61         self.stdscr.keypad(1)
           62         self.stdscr.border(0)
           63         self.maxy, self.maxx = self.stdscr.getmaxyx()
           64         self.set_cursor(0)
           65         self.w = curses.newwin(10, 50, 5, 5)
           66 
           67         console_stderr_handler.setLevel(logging.CRITICAL)
           68         self.tab = 0
           69         self.pos = 0
           70         self.popup_pos = 0
           71 
           72         self.str_recipient = ""
           73         self.str_description = ""
           74         self.str_amount = ""
           75         self.str_fee = ""
           76         self.history = None
           77 
           78         util.register_callback(self.update, ['wallet_updated', 'network_updated'])
           79 
           80         self.tab_names = [_("History"), _("Send"), _("Receive"), _("Addresses"), _("Contacts"), _("Banner")]
           81         self.num_tabs = len(self.tab_names)
           82 
           83 
           84     def set_cursor(self, x):
           85         try:
           86             curses.curs_set(x)
           87         except Exception:
           88             pass
           89 
           90     def restore_or_create(self):
           91         pass
           92 
           93     def verify_seed(self):
           94         pass
           95 
           96     def get_string(self, y, x):
           97         self.set_cursor(1)
           98         curses.echo()
           99         self.stdscr.addstr( y, x, " "*20, curses.A_REVERSE)
          100         s = self.stdscr.getstr(y,x)
          101         curses.noecho()
          102         self.set_cursor(0)
          103         return s
          104 
          105     def update(self, event, *args):
          106         self.update_history()
          107         if self.tab == 0:
          108             self.print_history()
          109         self.refresh()
          110 
          111     def print_history(self):
          112 
          113         width = [20, 40, 14, 14]
          114         delta = (self.maxx - sum(width) - 4)/3
          115         format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%"+"%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
          116 
          117         if self.history is None:
          118             self.update_history()
          119 
          120         self.print_list(self.history[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
          121 
          122     def update_history(self):
          123         width = [20, 40, 14, 14]
          124         delta = (self.maxx - sum(width) - 4)/3
          125         format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%"+"%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
          126 
          127         b = 0
          128         self.history = []
          129         for hist_item in self.wallet.get_history():
          130             if hist_item.tx_mined_status.conf:
          131                 timestamp = hist_item.tx_mined_status.timestamp
          132                 try:
          133                     time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
          134                 except Exception:
          135                     time_str = "------"
          136             else:
          137                 time_str = 'unconfirmed'
          138 
          139             label = self.wallet.get_label_for_txid(hist_item.txid)
          140             if len(label) > 40:
          141                 label = label[0:37] + '...'
          142             self.history.append(format_str % (time_str, label, format_satoshis(hist_item.delta, whitespaces=True),
          143                                               format_satoshis(hist_item.balance, whitespaces=True)))
          144 
          145 
          146     def print_balance(self):
          147         if not self.network:
          148             msg = _("Offline")
          149         elif self.network.is_connected():
          150             if not self.wallet.up_to_date:
          151                 msg = _("Synchronizing...")
          152             else:
          153                 c, u, x =  self.wallet.get_balance()
          154                 msg = _("Balance")+": %f  "%(Decimal(c) / COIN)
          155                 if u:
          156                     msg += "  [%f unconfirmed]"%(Decimal(u) / COIN)
          157                 if x:
          158                     msg += "  [%f unmatured]"%(Decimal(x) / COIN)
          159         else:
          160             msg = _("Not connected")
          161 
          162         self.stdscr.addstr( self.maxy -1, 3, msg)
          163 
          164         for i in range(self.num_tabs):
          165             self.stdscr.addstr( 0, 2 + 2*i + len(''.join(self.tab_names[0:i])), ' '+self.tab_names[i]+' ', curses.A_BOLD if self.tab == i else 0)
          166 
          167         self.stdscr.addstr(self.maxy -1, self.maxx-30, ' '.join([_("Settings"), _("Network"), _("Quit")]))
          168 
          169     def print_receive(self):
          170         addr = self.wallet.get_receiving_address()
          171         self.stdscr.addstr(2, 1, "Address: "+addr)
          172         self.print_qr(addr)
          173 
          174     def print_contacts(self):
          175         messages = map(lambda x: "%20s   %45s "%(x[0], x[1][1]), self.contacts.items())
          176         self.print_list(messages, "%19s  %15s "%("Key", "Value"))
          177 
          178     def print_addresses(self):
          179         fmt = "%-35s  %-30s"
          180         messages = map(lambda addr: fmt % (addr, self.wallet.get_label(addr)), self.wallet.get_addresses())
          181         self.print_list(messages,   fmt % ("Address", "Label"))
          182 
          183     def print_edit_line(self, y, label, text, index, size):
          184         text += " "*(size - len(text) )
          185         self.stdscr.addstr( y, 2, label)
          186         self.stdscr.addstr( y, 15, text, curses.A_REVERSE if self.pos%6==index else curses.color_pair(1))
          187 
          188     def print_send_tab(self):
          189         self.stdscr.clear()
          190         self.print_edit_line(3, _("Pay to"), self.str_recipient, 0, 40)
          191         self.print_edit_line(5, _("Description"), self.str_description, 1, 40)
          192         self.print_edit_line(7, _("Amount"), self.str_amount, 2, 15)
          193         self.print_edit_line(9, _("Fee"), self.str_fee, 3, 15)
          194         self.stdscr.addstr( 12, 15, _("[Send]"), curses.A_REVERSE if self.pos%6==4 else curses.color_pair(2))
          195         self.stdscr.addstr( 12, 25, _("[Clear]"), curses.A_REVERSE if self.pos%6==5 else curses.color_pair(2))
          196         self.maxpos = 6
          197 
          198     def print_banner(self):
          199         if self.network and self.network.banner:
          200             banner = self.network.banner
          201             banner = banner.replace('\r', '')
          202             self.print_list(banner.split('\n'))
          203 
          204     def print_qr(self, data):
          205         import qrcode
          206         try:
          207             from StringIO import StringIO
          208         except ImportError:
          209             from io import StringIO
          210 
          211         s = StringIO()
          212         self.qr = qrcode.QRCode()
          213         self.qr.add_data(data)
          214         self.qr.print_ascii(out=s, invert=False)
          215         msg = s.getvalue()
          216         lines = msg.split('\n')
          217         try:
          218             for i, l in enumerate(lines):
          219                 l = l.encode("utf-8")
          220                 self.stdscr.addstr(i+5, 5, l, curses.color_pair(3))
          221         except curses.error:
          222             m = 'error. screen too small?'
          223             m = m.encode(self.encoding)
          224             self.stdscr.addstr(5, 1, m, 0)
          225 
          226 
          227     def print_list(self, lst, firstline = None):
          228         lst = list(lst)
          229         self.maxpos = len(lst)
          230         if not self.maxpos: return
          231         if firstline:
          232             firstline += " "*(self.maxx -2 - len(firstline))
          233             self.stdscr.addstr( 1, 1, firstline )
          234         for i in range(self.maxy-4):
          235             msg = lst[i] if i < len(lst) else ""
          236             msg += " "*(self.maxx - 2 - len(msg))
          237             m = msg[0:self.maxx - 2]
          238             m = m.encode(self.encoding)
          239             self.stdscr.addstr( i+2, 1, m, curses.A_REVERSE if i == (self.pos % self.maxpos) else 0)
          240 
          241     def refresh(self):
          242         if self.tab == -1: return
          243         self.stdscr.border(0)
          244         self.print_balance()
          245         self.stdscr.refresh()
          246 
          247     def main_command(self):
          248         c = self.stdscr.getch()
          249         print(c)
          250         cc = curses.unctrl(c).decode()
          251         if   c == curses.KEY_RIGHT: self.tab = (self.tab + 1)%self.num_tabs
          252         elif c == curses.KEY_LEFT: self.tab = (self.tab - 1)%self.num_tabs
          253         elif c == curses.KEY_DOWN: self.pos +=1
          254         elif c == curses.KEY_UP: self.pos -= 1
          255         elif c == 9: self.pos +=1 # tab
          256         elif cc in ['^W', '^C', '^X', '^Q']: self.tab = -1
          257         elif cc in ['^N']: self.network_dialog()
          258         elif cc == '^S': self.settings_dialog()
          259         else: return c
          260         if self.pos<0: self.pos=0
          261         if self.pos>=self.maxpos: self.pos=self.maxpos - 1
          262 
          263     def run_tab(self, i, print_func, exec_func):
          264         while self.tab == i:
          265             self.stdscr.clear()
          266             print_func()
          267             self.refresh()
          268             c = self.main_command()
          269             if c: exec_func(c)
          270 
          271 
          272     def run_history_tab(self, c):
          273         if c == 10:
          274             out = self.run_popup('',["blah","foo"])
          275 
          276 
          277     def edit_str(self, target, c, is_num=False):
          278         # detect backspace
          279         cc = curses.unctrl(c).decode()
          280         if c in [8, 127, 263] and target:
          281             target = target[:-1]
          282         elif not is_num or cc in '0123456789.':
          283             target += cc
          284         return target
          285 
          286 
          287     def run_send_tab(self, c):
          288         if self.pos%6 == 0:
          289             self.str_recipient = self.edit_str(self.str_recipient, c)
          290         if self.pos%6 == 1:
          291             self.str_description = self.edit_str(self.str_description, c)
          292         if self.pos%6 == 2:
          293             self.str_amount = self.edit_str(self.str_amount, c, True)
          294         elif self.pos%6 == 3:
          295             self.str_fee = self.edit_str(self.str_fee, c, True)
          296         elif self.pos%6==4:
          297             if c == 10: self.do_send()
          298         elif self.pos%6==5:
          299             if c == 10: self.do_clear()
          300 
          301 
          302     def run_receive_tab(self, c):
          303         if c == 10:
          304             out = self.run_popup('Address', ["Edit label", "Freeze", "Prioritize"])
          305 
          306     def run_contacts_tab(self, c):
          307         if c == 10 and self.contacts:
          308             out = self.run_popup('Address', ["Copy", "Pay to", "Edit label", "Delete"]).get('button')
          309             key = list(self.contacts.keys())[self.pos%len(self.contacts.keys())]
          310             if out == "Pay to":
          311                 self.tab = 1
          312                 self.str_recipient = key
          313                 self.pos = 2
          314             elif out == "Edit label":
          315                 s = self.get_string(6 + self.pos, 18)
          316                 if s:
          317                     self.wallet.set_label(key, s)
          318 
          319     def run_banner_tab(self, c):
          320         self.show_message(repr(c))
          321         pass
          322 
          323     def main(self):
          324 
          325         tty.setraw(sys.stdin)
          326         try:
          327             while self.tab != -1:
          328                 self.run_tab(0, self.print_history, self.run_history_tab)
          329                 self.run_tab(1, self.print_send_tab, self.run_send_tab)
          330                 self.run_tab(2, self.print_receive, self.run_receive_tab)
          331                 self.run_tab(3, self.print_addresses, self.run_banner_tab)
          332                 self.run_tab(4, self.print_contacts, self.run_contacts_tab)
          333                 self.run_tab(5, self.print_banner, self.run_banner_tab)
          334         except curses.error as e:
          335             raise Exception("Error with curses. Is your screen too small?") from e
          336         finally:
          337             tty.setcbreak(sys.stdin)
          338             curses.nocbreak()
          339             self.stdscr.keypad(0)
          340             curses.echo()
          341             curses.endwin()
          342 
          343     def stop(self):
          344         pass
          345 
          346     def do_clear(self):
          347         self.str_amount = ''
          348         self.str_recipient = ''
          349         self.str_fee = ''
          350         self.str_description = ''
          351 
          352     def do_send(self):
          353         if not is_address(self.str_recipient):
          354             self.show_message(_('Invalid Bitcoin address'))
          355             return
          356         try:
          357             amount = int(Decimal(self.str_amount) * COIN)
          358         except Exception:
          359             self.show_message(_('Invalid Amount'))
          360             return
          361         try:
          362             fee = int(Decimal(self.str_fee) * COIN)
          363         except Exception:
          364             self.show_message(_('Invalid Fee'))
          365             return
          366 
          367         if self.wallet.has_password():
          368             password = self.password_dialog()
          369             if not password:
          370                 return
          371         else:
          372             password = None
          373         try:
          374             tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
          375                                   password=password,
          376                                   fee=fee)
          377         except Exception as e:
          378             self.show_message(repr(e))
          379             return
          380 
          381         if self.str_description:
          382             self.wallet.set_label(tx.txid(), self.str_description)
          383 
          384         self.show_message(_("Please wait..."), getchar=False)
          385         try:
          386             self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
          387         except TxBroadcastError as e:
          388             msg = e.get_message_for_gui()
          389             self.show_message(msg)
          390         except BestEffortRequestFailed as e:
          391             msg = repr(e)
          392             self.show_message(msg)
          393         else:
          394             self.show_message(_('Payment sent.'))
          395             self.do_clear()
          396             #self.update_contacts_tab()
          397 
          398     def show_message(self, message, getchar = True):
          399         w = self.w
          400         w.clear()
          401         w.border(0)
          402         for i, line in enumerate(message.split('\n')):
          403             w.addstr(2+i,2,line)
          404         w.refresh()
          405         if getchar: c = self.stdscr.getch()
          406 
          407     def run_popup(self, title, items):
          408         return self.run_dialog(title, list(map(lambda x: {'type':'button','label':x}, items)), interval=1, y_pos = self.pos+3)
          409 
          410     def network_dialog(self):
          411         if not self.network:
          412             return
          413         net_params = self.network.get_parameters()
          414         server_addr = net_params.server
          415         proxy_config, auto_connect = net_params.proxy, net_params.auto_connect
          416         srv = 'auto-connect' if auto_connect else str(self.network.default_server)
          417         out = self.run_dialog('Network', [
          418             {'label':'server', 'type':'str', 'value':srv},
          419             {'label':'proxy', 'type':'str', 'value':self.config.get('proxy', '')},
          420             ], buttons = 1)
          421         if out:
          422             if out.get('server'):
          423                 server_str = out.get('server')
          424                 auto_connect = server_str == 'auto-connect'
          425                 if not auto_connect:
          426                     try:
          427                         server_addr = ServerAddr.from_str(server_str)
          428                     except Exception:
          429                         self.show_message("Error:" + server_str + "\nIn doubt, type \"auto-connect\"")
          430                         return False
          431             if out.get('server') or out.get('proxy'):
          432                 proxy = electrum.network.deserialize_proxy(out.get('proxy')) if out.get('proxy') else proxy_config
          433                 net_params = NetworkParameters(server=server_addr,
          434                                                proxy=proxy,
          435                                                auto_connect=auto_connect)
          436                 self.network.run_from_another_thread(self.network.set_parameters(net_params))
          437 
          438     def settings_dialog(self):
          439         fee = str(Decimal(self.config.fee_per_kb()) / COIN)
          440         out = self.run_dialog('Settings', [
          441             {'label':'Default fee', 'type':'satoshis', 'value': fee }
          442             ], buttons = 1)
          443         if out:
          444             if out.get('Default fee'):
          445                 fee = int(Decimal(out['Default fee']) * COIN)
          446                 self.config.set_key('fee_per_kb', fee, True)
          447 
          448 
          449     def password_dialog(self):
          450         out = self.run_dialog('Password', [
          451             {'label':'Password', 'type':'password', 'value':''}
          452             ], buttons = 1)
          453         return out.get('Password')
          454 
          455 
          456     def run_dialog(self, title, items, interval=2, buttons=None, y_pos=3):
          457         self.popup_pos = 0
          458 
          459         self.w = curses.newwin( 5 + len(list(items))*interval + (2 if buttons else 0), 50, y_pos, 5)
          460         w = self.w
          461         out = {}
          462         while True:
          463             w.clear()
          464             w.border(0)
          465             w.addstr( 0, 2, title)
          466 
          467             num = len(list(items))
          468 
          469             numpos = num
          470             if buttons: numpos += 2
          471 
          472             for i in range(num):
          473                 item = items[i]
          474                 label = item.get('label')
          475                 if item.get('type') == 'list':
          476                     value = item.get('value','')
          477                 elif item.get('type') == 'satoshis':
          478                     value = item.get('value','')
          479                 elif item.get('type') == 'str':
          480                     value = item.get('value','')
          481                 elif item.get('type') == 'password':
          482                     value = '*'*len(item.get('value',''))
          483                 else:
          484                     value = ''
          485                 if value is None:
          486                     value = ''
          487                 if len(value)<20:
          488                     value += ' '*(20-len(value))
          489 
          490                 if 'value' in item:
          491                     w.addstr( 2+interval*i, 2, label)
          492                     w.addstr( 2+interval*i, 15, value, curses.A_REVERSE if self.popup_pos%numpos==i else curses.color_pair(1) )
          493                 else:
          494                     w.addstr( 2+interval*i, 2, label, curses.A_REVERSE if self.popup_pos%numpos==i else 0)
          495 
          496             if buttons:
          497                 w.addstr( 5+interval*i, 10, "[  ok  ]", curses.A_REVERSE if self.popup_pos%numpos==(numpos-2) else curses.color_pair(2))
          498                 w.addstr( 5+interval*i, 25, "[cancel]", curses.A_REVERSE if self.popup_pos%numpos==(numpos-1) else curses.color_pair(2))
          499 
          500             w.refresh()
          501 
          502             c = self.stdscr.getch()
          503             if c in [ord('q'), 27]: break
          504             elif c in [curses.KEY_LEFT, curses.KEY_UP]: self.popup_pos -= 1
          505             elif c in [curses.KEY_RIGHT, curses.KEY_DOWN]: self.popup_pos +=1
          506             else:
          507                 i = self.popup_pos%numpos
          508                 if buttons and c==10:
          509                     if i == numpos-2:
          510                         return out
          511                     elif i == numpos -1:
          512                         return {}
          513 
          514                 item = items[i]
          515                 _type = item.get('type')
          516 
          517                 if _type == 'str':
          518                     item['value'] = self.edit_str(item['value'], c)
          519                     out[item.get('label')] = item.get('value')
          520 
          521                 elif _type == 'password':
          522                     item['value'] = self.edit_str(item['value'], c)
          523                     out[item.get('label')] = item ['value']
          524 
          525                 elif _type == 'satoshis':
          526                     item['value'] = self.edit_str(item['value'], c, True)
          527                     out[item.get('label')] = item.get('value')
          528 
          529                 elif _type == 'list':
          530                     choices = item.get('choices')
          531                     try:
          532                         j = choices.index(item.get('value'))
          533                     except Exception:
          534                         j = 0
          535                     new_choice = choices[(j + 1)% len(choices)]
          536                     item['value'] = new_choice
          537                     out[item.get('label')] = item.get('value')
          538 
          539                 elif _type == 'button':
          540                     out['button'] = item.get('label')
          541                     break
          542 
          543         return out