Browse Source

various updates for the text GUI

patch-4
ThomasV 3 years ago
parent
commit
43ab4a779a
  1. 627
      electrum/gui/text.py

627
electrum/gui/text.py

@ -6,20 +6,23 @@ import locale
from decimal import Decimal from decimal import Decimal
import getpass import getpass
import logging import logging
import pyperclip
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
import electrum import electrum
from electrum.gui import BaseElectrumGui from electrum.gui import BaseElectrumGui
from electrum import util from electrum import util
from electrum.util import format_satoshis from electrum.util import format_satoshis, format_time
from electrum.util import EventListener, event_listener from electrum.util import EventListener, event_listener
from electrum.bitcoin import is_address, COIN from electrum.bitcoin import is_address, address_to_script, COIN
from electrum.transaction import PartialTxOutput from electrum.transaction import PartialTxOutput
from electrum.wallet import Wallet, Abstract_Wallet from electrum.wallet import Wallet, Abstract_Wallet
from electrum.wallet_db import WalletDB from electrum.wallet_db import WalletDB
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
from electrum.interface import ServerAddr from electrum.interface import ServerAddr
from electrum.invoices import Invoice
from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.daemon import Daemon from electrum.daemon import Daemon
@ -29,6 +32,20 @@ if TYPE_CHECKING:
_ = lambda x:x # i18n _ = lambda x:x # i18n
def parse_bip21(text):
try:
return util.parse_URI(text)
except:
return
def parse_bolt11(text):
from electrum.lnaddr import lndecode
try:
return lndecode(text)
except:
return
class ElectrumGui(BaseElectrumGui, EventListener): class ElectrumGui(BaseElectrumGui, EventListener):
@ -58,36 +75,43 @@ class ElectrumGui(BaseElectrumGui, EventListener):
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_CYAN) curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_CYAN)
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)
curses.halfdelay(1)
self.stdscr.keypad(1) self.stdscr.keypad(1)
self.stdscr.border(0) self.stdscr.border(0)
self.maxy, self.maxx = self.stdscr.getmaxyx() self.maxy, self.maxx = self.stdscr.getmaxyx()
self.set_cursor(0) self.set_cursor(0)
self.w = curses.newwin(10, 50, 5, 5) self.w = curses.newwin(10, 50, 5, 5)
self.lightning_invoice = None
self.tab = 0 self.tab = 0
self.pos = 0 self.pos = 0
self.popup_pos = 0 self.popup_pos = 0
self.qr_mode = 0
self.str_recipient = "" self.str_recipient = ""
self.str_description = "" self.str_description = ""
self.str_amount = "" self.str_amount = ""
self.str_fee = ""
self.history = None self.history = None
self.txid = [] self.txid = []
self.str_recv_description = ""
self.str_recv_amount = ""
self.str_recv_expiry = ""
self.channel_ids = []
self.requests = []
self.register_callbacks() self.register_callbacks()
self.tab_names = [_("History"), _("Send"), _("Receive"), _("Addresses"), _("Coins"), _("Channels"), _("Contacts"), _("Banner")]
self.tab_names = [_("History"), _("Send"), _("Receive"), _("Addresses"), _("Contacts"), _("Banner")]
self.num_tabs = len(self.tab_names) self.num_tabs = len(self.tab_names)
self.need_update = False
@event_listener @event_listener
def on_event_wallet_updated(self, wallet): def on_event_wallet_updated(self, wallet):
self.update() self.need_update = True
@event_listener @event_listener
def on_event_network_updated(self): def on_event_network_updated(self):
self.update() self.need_update = True
def set_cursor(self, x): def set_cursor(self, x):
try: try:
@ -115,22 +139,26 @@ class ElectrumGui(BaseElectrumGui, EventListener):
if self.tab == 0: if self.tab == 0:
self.print_history() self.print_history()
self.refresh() self.refresh()
self.need_update = False
def print_history(self): def print_button(self, x, y, text, pos):
self.stdscr.addstr(x, y, text, curses.A_REVERSE if self.pos%self.max_pos==pos else curses.color_pair(2))
width = [20, 40, 14, 14] def print_edit_line(self, y, x, label, text, index, size):
delta = (self.maxx - sum(width) - 4)/3 text += " "*(size - len(text))
format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%"+"%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s" self.stdscr.addstr(y, x, label)
self.stdscr.addstr(y, x + 13, text, curses.A_REVERSE if self.pos%self.max_pos==index else curses.color_pair(1))
def print_history(self):
x = 2
self.history_format_str = self.format_column_width(x, [-20, '*', 15, 15])
if self.history is None: if self.history is None:
self.update_history() self.update_history()
self.print_list(2, x, self.history[::-1], headers=self.history_format_str%(_("Date"), _("Description"), _("Amount"), _("Balance")))
self.print_list(self.history[::-1], format_str%(_("Date"), _("Description"), _("Amount"), _("Balance")))
def update_history(self): def update_history(self):
width = [20, 40, 14, 14] width = [20, 40, 14, 14]
delta = (self.maxx - sum(width) - 4)/3 delta = (self.maxx - sum(width) - 4)/3
format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%"+"%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
domain = self.wallet.get_addresses() domain = self.wallet.get_addresses()
self.history = [] self.history = []
self.txid = [] self.txid = []
@ -153,11 +181,18 @@ class ElectrumGui(BaseElectrumGui, EventListener):
if len(label) > 40: if len(label) > 40:
label = label[0:37] + '...' label = label[0:37] + '...'
self.history.append(format_str % ( self.history.append(self.history_format_str % (
time_str, label, time_str, label,
format_satoshis(amount_sat, whitespaces=True), self.config.format_amount(amount_sat, whitespaces=True),
format_satoshis(balance_sat, whitespaces=True))) self.config.format_amount(balance_sat, whitespaces=True)))
def print_clipboard(self):
return
c = pyperclip.paste()
if c:
if len(c) > 20:
c = c[0:20] + '...'
self.stdscr.addstr(self.maxy -1, self.maxx // 3, ' ' + _('Clipboard') + ': ' + c + ' ')
def print_balance(self): def print_balance(self):
if not self.network: if not self.network:
@ -166,115 +201,273 @@ class ElectrumGui(BaseElectrumGui, EventListener):
if not self.wallet.is_up_to_date(): if not self.wallet.is_up_to_date():
msg = _("Synchronizing...") msg = _("Synchronizing...")
else: else:
c, u, x = self.wallet.get_balance() balance = sum(self.wallet.get_balances_for_piechart())
msg = _("Balance")+": %f "%(Decimal(c) / COIN) msg = _("Balance") + ': ' + self.config.format_amount_and_units(balance)
if u:
msg += " [%f unconfirmed]"%(Decimal(u) / COIN)
if x:
msg += " [%f unmatured]"%(Decimal(x) / COIN)
else: else:
msg = _("Not connected") msg = _("Not connected")
msg = ' ' + msg + ' '
self.stdscr.addstr(self.maxy -1, 3, msg) self.stdscr.addstr(self.maxy -1, 3, msg)
for i in range(self.num_tabs): for i in range(self.num_tabs):
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) self.stdscr.addstr(0, 2 + 2*i + len(''.join(self.tab_names[0:i])), ' '+self.tab_names[i]+' ', curses.A_REVERSE if self.tab == i else 0)
self.stdscr.addstr(self.maxy -1, self.maxx-30, ' ' + ' '.join([_("Settings"), _("Network"), _("Quit")]) + ' ')
self.stdscr.addstr(self.maxy -1, self.maxx-30, ' '.join([_("Settings"), _("Network"), _("Quit")])) def print_receive_tab(self):
self.stdscr.clear()
self.buttons = {}
self.max_pos = 5 + len(list(self.wallet.get_unpaid_requests()))
self.index = 0
self.add_edit_line(3, 2, _("Description"), self.str_recv_description, 40)
self.add_edit_line(5, 2, _("Amount"), self.str_recv_amount, 15)
self.stdscr.addstr(5, 31, self.config.get_base_unit())
self.add_edit_line(7, 2, _("Expiry"), self.str_recv_expiry, 15)
self.add_button(9, 15, _("[Create]"), self.do_create_request)
self.add_button(9, 25, _("[Clear]"), self.do_clear_request)
self.print_requests_list(13, 2, offset_pos=5)
return
def print_receive(self): def run_receive_tab(self, c):
addr = self.wallet.get_receiving_address() if self.pos == 0:
self.stdscr.addstr(2, 1, "Address: "+addr) self.str_recv_description = self.edit_str(self.str_recv_description, c)
self.print_qr(addr) elif self.pos == 1:
self.str_recv_amount = self.edit_str(self.str_recv_amount, c)
elif self.pos in self.buttons and c == 10:
self.buttons[self.pos]()
elif self.pos >= 5 and c == 10:
key = self.requests[self.pos - 5]
self.show_request_menu(key)
def question(self, msg):
out = self.run_popup(msg, ["No", "Yes"]).get('button')
return out == "Yes"
def show_request_menu(self, key):
req = self.wallet.get_request(key)
addr = req.get_address() or ''
URI = req.get_bip21_URI()
lnaddr = req.lightning_invoice or ''
out = self.run_popup('Request', ["View", "Copy address", "Copy URI", "Copy Lightning Invoice", "Delete"]).get('button')
if out == "Delete":
self.wallet.delete_request(key)
self.max_pos -= 1
elif out == "Copy address":
pyperclip.copy(addr)
elif out == "Copy URI":
pyperclip.copy(URI)
elif out == "Copy Lightning Invoice":
pyperclip.copy(lnaddr)
elif out == "View":
self.show_request(key)
def show_invoice_menu(self):
key = self.invoices[self.pos - 7]
invoice = self.wallet.get_invoice(key)
out = self.run_popup('Invoice', ["Pay", "Delete"]).get('button')
if out == "Pay":
self.do_pay_invoice(invoice)
elif out == "Delete":
self.wallet.delete_invoice(key)
self.max_pos -= 1
def format_column_width(self, offset, width):
delta = self.maxx -2 -offset - sum([abs(x) for x in width if x != '*'])
fmt = ''
for w in width:
if w == '*':
fmt += "%-" + "%d"%delta + "s"
else:
fmt += "%" + "%d"%w + "s"
return fmt
def print_invoices_list(self, y, x, offset_pos):
messages = []
invoices = []
fmt = self.format_column_width(x, [-20, '*', 15, 25])
headers = fmt % ("Date", "Description", "Amount", "Status")
for req in self.wallet.get_unpaid_invoices():
key = self.wallet.get_key_for_outgoing_invoice(req)
status = self.wallet.get_invoice_status(req)
status_str = req.get_status_str(status)
timestamp = req.get_time()
date = format_time(timestamp)
amount = req.get_amount_sat()
message = req.get_message()
amount_str = self.config.format_amount(amount) if amount else ""
labels = []
messages.append(fmt % (date, message, amount_str, status_str))
invoices.append(key)
self.invoices = invoices
self.print_list(y, x, messages, headers=headers, offset_pos=offset_pos)
def print_requests_list(self, y, x, offset_pos):
messages = []
requests = []
fmt = self.format_column_width(x, [-20, '*', 15, 25])
headers = fmt % ("Date", "Description", "Amount", "Status")
for req in self.wallet.get_unpaid_requests():
key = self.wallet.get_key_for_receive_request(req)
status = self.wallet.get_request_status(key)
status_str = req.get_status_str(status)
timestamp = req.get_time()
date = format_time(timestamp)
amount = req.get_amount_sat()
message = req.get_message()
amount_str = self.config.format_amount(amount) if amount else ""
labels = []
messages.append(fmt % (date, message, amount_str, status_str))
requests.append(key)
self.requests = requests
self.print_list(y, x, messages, headers=headers, offset_pos=offset_pos)
def print_contacts(self): def print_contacts(self):
messages = map(lambda x: "%20s %45s "%(x[0], x[1][1]), self.contacts.items()) messages = list(map(lambda x: "%20s %45s "%(x[0], x[1][1]), self.contacts.items()))
self.print_list(messages, "%19s %15s "%("Key", "Value")) self.print_list(2, 1, messages, "%19s %15s "%("Key", "Value"))
def print_addresses(self): def print_addresses(self):
fmt = "%-35s %-30s" x = 2
messages = map(lambda addr: fmt % (addr, self.wallet.get_label(addr)), self.wallet.get_addresses()) fmt = self.format_column_width(x, [-50, '*', 15])
self.print_list(messages, fmt % ("Address", "Label")) messages = [ fmt % (
addr,
def print_edit_line(self, y, label, text, index, size): self.wallet.get_label(addr),
text += " "*(size - len(text)) self.config.format_amount(sum(self.wallet.get_addr_balance(addr)), whitespaces=True)
self.stdscr.addstr(y, 2, label) ) for addr in self.wallet.get_addresses() ]
self.stdscr.addstr(y, 15, text, curses.A_REVERSE if self.pos%6==index else curses.color_pair(1)) self.print_list(2, x, messages, fmt % ("Address", "Description", "Balance"))
def print_utxos(self):
x = 2
fmt = self.format_column_width(x, [-70, '*', 15])
utxos = self.wallet.get_utxos()
messages = [ fmt % (
utxo.prevout.to_str(),
self.wallet.get_label(utxo.prevout.txid.hex()),
self.config.format_amount(utxo.value_sats(), whitespaces=True)
) for utxo in utxos]
self.print_list(2, x, sorted(messages), fmt % ("Outpoint", "Description", "Balance"))
def print_channels(self):
if not self.wallet.lnworker:
return
fmt = "%-35s %-10s %-30s"
channels = self.wallet.lnworker.get_channel_objects()
messages = []
channel_ids = []
for chan in channels.values():
channel_ids.append(chan.short_id_for_GUI())
messages.append(fmt % (chan.short_id_for_GUI(), self.config.format_amount(chan.get_capacity()), chan.get_state().name))
self.channel_ids = channel_ids
self.print_list(2, 1, messages, fmt % ("Scid", "Capacity", "State"))
def print_send_tab(self): def print_send_tab(self):
self.stdscr.clear() self.stdscr.clear()
self.print_edit_line(3, _("Pay to"), self.str_recipient, 0, 40) self.buttons = {}
self.print_edit_line(5, _("Description"), self.str_description, 1, 40) self.max_pos = 7 + len(list(self.wallet.get_unpaid_invoices()))
self.print_edit_line(7, _("Amount"), self.str_amount, 2, 15) self.index = 0
self.print_edit_line(9, _("Fee"), self.str_fee, 3, 15) self.add_edit_line(3, 2, _("Pay to"), self.str_recipient, 40)
self.stdscr.addstr(12, 15, _("[Send]"), curses.A_REVERSE if self.pos%6==4 else curses.color_pair(2)) self.add_edit_line(5, 2, _("Description"), self.str_description, 40)
self.stdscr.addstr(12, 25, _("[Clear]"), curses.A_REVERSE if self.pos%6==5 else curses.color_pair(2)) self.add_edit_line(7, 2, _("Amount"), self.str_amount, 15)
self.maxpos = 6 self.stdscr.addstr(7, 31, self.config.get_base_unit())
self.add_button(9, 15, _("[Paste]"), self.do_paste)
self.add_button(9, 25, _("[Clear]"), self.do_clear)
self.add_button(9, 35, _("[Save]"), self.do_save_invoice)
self.add_button(9, 44, _("[Pay]"), self.do_pay)
#
self.print_invoices_list(13, 2, offset_pos=7)
def add_edit_line(self, y, x, title, data, length):
self.print_edit_line(y, x, title, data, self.index, length)
self.index += 1
def add_button(self, y, x, title, action):
self.print_button(y, x, title, self.index)
self.buttons[self.index] = action
self.index += 1
def print_banner(self): def print_banner(self):
if self.network and self.network.banner: if self.network and self.network.banner:
banner = self.network.banner banner = self.network.banner
banner = banner.replace('\r', '') banner = banner.replace('\r', '')
self.print_list(banner.split('\n')) self.print_list(2, 1, banner.split('\n'))
def print_qr(self, data): def get_qr(self, data):
import qrcode import qrcode
try: try:
from StringIO import StringIO from StringIO import StringIO
except ImportError: except ImportError:
from io import StringIO from io import StringIO
s = StringIO() s = StringIO()
self.qr = qrcode.QRCode() self.qr = qrcode.QRCode()
self.qr.add_data(data) self.qr.add_data(data)
self.qr.print_ascii(out=s, invert=False) self.qr.print_ascii(out=s, invert=False)
msg = s.getvalue() msg = s.getvalue()
lines = msg.split('\n') lines = msg.split('\n')
return lines
def print_qr(self, w, y, x, lines):
try: try:
for i, l in enumerate(lines): for i, l in enumerate(lines):
l = l.encode("utf-8") l = l.encode("utf-8")
self.stdscr.addstr(i+5, 5, l, curses.color_pair(3)) w.addstr(y + i, x, l, curses.color_pair(3))
except curses.error: except curses.error:
m = 'error. screen too small?' m = 'error. screen too small?'
m = m.encode(self.encoding) m = m.encode(self.encoding)
self.stdscr.addstr(5, 1, m, 0) w.addstr(y, x, m, 0)
def print_list(self, y, x, lst, headers=None, offset_pos=0):
def print_list(self, lst, firstline = None): self.list_length = len(lst)
lst = list(lst) if not self.list_length:
self.maxpos = len(lst) return
if not self.maxpos: return if headers:
if firstline: headers += " "*(self.maxx -2 - len(headers))
firstline += " "*(self.maxx -2 - len(firstline)) self.stdscr.addstr(y, x, headers, curses.A_BOLD)
self.stdscr.addstr(1, 1, firstline) for i in range(self.maxy - 2 - y):
for i in range(self.maxy-4): msg = lst[i] if i < self.list_length else ""
msg = lst[i] if i < len(lst) else ""
msg += " "*(self.maxx - 2 - len(msg)) msg += " "*(self.maxx - 2 - len(msg))
m = msg[0:self.maxx - 2] m = msg[0:self.maxx - 2]
m = m.encode(self.encoding) m = m.encode(self.encoding)
self.stdscr.addstr(i+2, 1, m, curses.A_REVERSE if i == (self.pos % self.maxpos) else 0) selected = self.pos >= offset_pos and (i == ((self.pos - offset_pos) % self.list_length))
self.stdscr.addstr(i+y+1, x, m, curses.A_REVERSE if selected else 0)
self.max_pos = self.list_length + offset_pos
def refresh(self): def refresh(self):
if self.tab == -1: return if self.tab == -1:
return
self.stdscr.border(0) self.stdscr.border(0)
self.print_balance() self.print_balance()
self.print_clipboard()
self.stdscr.refresh() self.stdscr.refresh()
def increase_cursor(self, delta):
self.pos += delta
self.pos = max(0, self.pos)
self.pos = min(self.pos, self.max_pos - 1)
def getch(self, redraw=False):
while True:
c = self.stdscr.getch()
if c != -1:
return c
if self.need_update and redraw:
self.update()
def main_command(self): def main_command(self):
c = self.stdscr.getch() c = self.getch(redraw=True)
print(c)
cc = curses.unctrl(c).decode() cc = curses.unctrl(c).decode()
if c == curses.KEY_RIGHT: self.tab = (self.tab + 1)%self.num_tabs if c == curses.KEY_RIGHT:
elif c == curses.KEY_LEFT: self.tab = (self.tab - 1)%self.num_tabs self.tab = (self.tab + 1)%self.num_tabs
elif c == curses.KEY_DOWN: self.pos +=1 elif c == curses.KEY_LEFT:
elif c == curses.KEY_UP: self.pos -= 1 self.tab = (self.tab - 1)%self.num_tabs
elif c == 9: self.pos +=1 # tab elif c in [curses.KEY_DOWN, 9]:
elif cc in ['^W', '^C', '^X', '^Q']: self.tab = -1 self.increase_cursor(1)
elif cc in ['^N']: self.network_dialog() elif c == curses.KEY_UP:
elif cc == '^S': self.settings_dialog() self.increase_cursor(-1)
else: return c elif cc in ['^W', '^C', '^X', '^Q']:
if self.pos<0: self.pos=0 self.tab = -1
if self.pos>=self.maxpos: self.pos=self.maxpos - 1 elif cc in ['^N']:
self.network_dialog()
elif cc == '^S':
self.settings_dialog()
else:
return c
def run_tab(self, i, print_func, exec_func): def run_tab(self, i, print_func, exec_func):
while self.tab == i: while self.tab == i:
@ -284,13 +477,11 @@ class ElectrumGui(BaseElectrumGui, EventListener):
c = self.main_command() c = self.main_command()
if c: exec_func(c) if c: exec_func(c)
def run_history_tab(self, c): def run_history_tab(self, c):
# Get txid from cursor position # Get txid from cursor position
if c == 10: if c == 10:
out = self.run_popup('', ['Transaction ID:', self.txid[self.pos]]) out = self.run_popup('', ['Transaction ID:', self.txid[self.pos]])
def edit_str(self, target, c, is_num=False): def edit_str(self, target, c, is_num=False):
# detect backspace # detect backspace
cc = curses.unctrl(c).decode() cc = curses.unctrl(c).decode()
@ -300,25 +491,18 @@ class ElectrumGui(BaseElectrumGui, EventListener):
target += cc target += cc
return target return target
def run_send_tab(self, c): def run_send_tab(self, c):
if self.pos%6 == 0: self.pos = self.pos % self.max_pos
if self.pos == 0:
self.str_recipient = self.edit_str(self.str_recipient, c) self.str_recipient = self.edit_str(self.str_recipient, c)
if self.pos%6 == 1: elif self.pos == 1:
self.str_description = self.edit_str(self.str_description, c) self.str_description = self.edit_str(self.str_description, c)
if self.pos%6 == 2: elif self.pos == 2:
self.str_amount = self.edit_str(self.str_amount, c, True) self.str_amount = self.edit_str(self.str_amount, c, True)
elif self.pos%6 == 3: elif self.pos in self.buttons and c == 10:
self.str_fee = self.edit_str(self.str_fee, c, True) self.buttons[self.pos]()
elif self.pos%6==4: elif self.pos >= 7 and c == 10:
if c == 10: self.do_send() self.show_invoice_menu()
elif self.pos%6==5:
if c == 10: self.do_clear()
def run_receive_tab(self, c):
if c == 10:
out = self.run_popup('Address', ["Edit label", "Freeze", "Prioritize"])
def run_contacts_tab(self, c): def run_contacts_tab(self, c):
if c == 10 and self.contacts: if c == 10 and self.contacts:
@ -333,21 +517,32 @@ class ElectrumGui(BaseElectrumGui, EventListener):
if s: if s:
self.wallet.set_label(key, s) self.wallet.set_label(key, s)
def run_addresses_tab(self, c):
pass
def run_utxos_tab(self, c):
pass
def run_channels_tab(self, c):
if c == 10:
out = self.run_popup('Channel Details', ['Short channel ID:', self.channel_ids[self.pos]])
def run_banner_tab(self, c): def run_banner_tab(self, c):
self.show_message(repr(c)) self.show_message(repr(c))
pass pass
def main(self): def main(self):
tty.setraw(sys.stdin) tty.setraw(sys.stdin)
try: try:
while self.tab != -1: while self.tab != -1:
self.run_tab(0, self.print_history, self.run_history_tab) self.run_tab(0, self.print_history, self.run_history_tab)
self.run_tab(1, self.print_send_tab, self.run_send_tab) self.run_tab(1, self.print_send_tab, self.run_send_tab)
self.run_tab(2, self.print_receive, self.run_receive_tab) self.run_tab(2, self.print_receive_tab, self.run_receive_tab)
self.run_tab(3, self.print_addresses, self.run_banner_tab) self.run_tab(3, self.print_addresses, self.run_addresses_tab)
self.run_tab(4, self.print_contacts, self.run_contacts_tab) self.run_tab(4, self.print_utxos, self.run_utxos_tab)
self.run_tab(5, self.print_banner, self.run_banner_tab) self.run_tab(5, self.print_channels, self.run_channels_tab)
self.run_tab(6, self.print_contacts, self.run_contacts_tab)
self.run_tab(7, self.print_banner, self.run_banner_tab)
except curses.error as e: except curses.error as e:
raise Exception("Error with curses. Is your screen too small?") from e raise Exception("Error with curses. Is your screen too small?") from e
finally: finally:
@ -363,21 +558,127 @@ class ElectrumGui(BaseElectrumGui, EventListener):
self.str_fee = '' self.str_fee = ''
self.str_description = '' self.str_description = ''
def do_send(self): def do_create_request(self):
if not is_address(self.str_recipient): amount_sat = self.parse_amount(self.str_recv_amount)
self.show_message(_('Invalid Bitcoin address')) if not amount_sat:
return
try:
amount = int(Decimal(self.str_amount) * COIN)
except Exception:
self.show_message(_('Invalid Amount')) self.show_message(_('Invalid Amount'))
return return
if amount_sat < self.wallet.dust_threshold():
address = None
if not self.wallet.has_lightning():
return
else:
address = self.wallet.get_unused_address()
if not address:
return
message = self.str_recv_description
expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
key = self.wallet.create_request(amount_sat, message, expiry, address)
self.do_clear_request()
self.pos = self.max_pos
self.show_request(key)
def do_clear_request(self):
self.str_recv_amount = ""
self.str_recv_description = ""
def do_paste(self):
text = pyperclip.paste()
text = text.strip()
if not text:
return
if is_address(text):
self.str_recipient = text
self.lightning_invoice = None
elif out := parse_bip21(text):
amount_sat = out.get('amount')
self.str_amount = self.config.format_amount(amount_sat) if amount_sat is not None else ''
self.str_recipient = out.get('address') or ''
self.str_description = out.get('message') or ''
self.lightning_invoice = None
elif lnaddr := parse_bolt11(text):
amount_sat = lnaddr.get_amount_sat()
self.str_recipient = lnaddr.pubkey.serialize().hex()
self.str_description = lnaddr.get_description()
self.str_amount = self.config.format_amount(amount_sat) if amount_sat is not None else ''
self.lightning_invoice = text
else:
self.show_message(_('Could not parse clipboard text') + '\n\n' + text[0:20] + '...')
def parse_amount(self, text):
try: try:
fee = int(Decimal(self.str_fee) * COIN) x = Decimal(text)
except Exception: except:
self.show_message(_('Invalid Fee')) return None
power = pow(10, self.config.get_decimal_point())
return int(power * x)
def read_invoice(self):
if self.lightning_invoice:
invoice = Invoice.from_bech32(self.lightning_invoice)
if invoice.amount_msat is None:
amount_sat = self.parse_amount(self.str_amount)
if amount_sat:
invoice.amount_msat = int(amount_sat * 1000)
else:
self.show_error(_('No amount'))
return
elif is_address(self.str_recipient):
amount_sat = self.parse_amount(self.str_amount)
scriptpubkey = bytes.fromhex(address_to_script(self.str_recipient))
outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount_sat)]
invoice = self.wallet.create_invoice(
outputs=outputs,
message=self.str_description,
pr=None,
URI=None)
else:
self.show_message(_('Invalid Bitcoin address'))
return return
return invoice
def do_save_invoice(self):
invoice = self.read_invoice()
if not invoice:
return
self.save_pending_invoice(invoice)
def save_pending_invoice(self, invoice):
self.do_clear()
self.wallet.save_invoice(invoice)
self.pending_invoice = None
def do_pay(self):
invoice = self.read_invoice()
if not invoice:
return
self.do_pay_invoice(invoice)
def do_pay_invoice(self, invoice):
if invoice.is_lightning():
self.pay_lightning_invoice(invoice)
else:
self.pay_onchain_dialog(invoice)
def pay_lightning_invoice(self, invoice):
amount_msat = invoice.get_amount_msat()
msg = _("Pay lightning invoice?")
#+ '\n\n' + _("This will send {}?").format(self.format_amount_and_units(Decimal(amount_msat)/1000))
if not self.question(msg):
return
self.save_pending_invoice(invoice)
coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat)
#self.window.run_coroutine_from_thread(coro, _('Sending payment'))
self.show_message(_("Please wait..."), getchar=False)
try:
self.network.run_from_another_thread(coro)
except Exception as e:
self.show_message(str(e))
else:
self.show_message(_('Payment sent.'))
def pay_onchain_dialog(self, invoice):
if self.wallet.has_password(): if self.wallet.has_password():
password = self.password_dialog() password = self.password_dialog()
if not password: if not password:
@ -385,16 +686,17 @@ class ElectrumGui(BaseElectrumGui, EventListener):
else: else:
password = None password = None
try: try:
tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)], tx = self.wallet.mktx(
password=password, outputs=invoice.outputs,
fee=fee) password=password,
fee=None)
except Exception as e: except Exception as e:
self.show_message(repr(e)) self.show_message(repr(e))
return return
if self.str_description: if self.str_description:
self.wallet.set_label(tx.txid(), self.str_description) self.wallet.set_label(tx.txid(), self.str_description)
self.save_pending_invoice(invoice)
self.show_message(_("Please wait..."), getchar=False) self.show_message(_("Please wait..."), getchar=False)
try: try:
self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
@ -416,7 +718,8 @@ class ElectrumGui(BaseElectrumGui, EventListener):
for i, line in enumerate(message.split('\n')): for i, line in enumerate(message.split('\n')):
w.addstr(2+i,2,line) w.addstr(2+i,2,line)
w.refresh() w.refresh()
if getchar: c = self.stdscr.getch() if getchar:
c = self.getch()
def run_popup(self, title, items): def run_popup(self, title, items):
return self.run_dialog(title, list(map(lambda x: {'type':'button','label':x}, items)), interval=1, y_pos = self.pos+3) return self.run_dialog(title, list(map(lambda x: {'type':'button','label':x}, items)), interval=1, y_pos = self.pos+3)
@ -444,9 +747,10 @@ class ElectrumGui(BaseElectrumGui, EventListener):
return False return False
if out.get('server') or out.get('proxy'): if out.get('server') or out.get('proxy'):
proxy = electrum.network.deserialize_proxy(out.get('proxy')) if out.get('proxy') else proxy_config proxy = electrum.network.deserialize_proxy(out.get('proxy')) if out.get('proxy') else proxy_config
net_params = NetworkParameters(server=server_addr, net_params = NetworkParameters(
proxy=proxy, server=server_addr,
auto_connect=auto_connect) proxy=proxy,
auto_connect=auto_connect)
self.network.run_from_another_thread(self.network.set_parameters(net_params)) self.network.run_from_another_thread(self.network.set_parameters(net_params))
def settings_dialog(self): def settings_dialog(self):
@ -459,14 +763,12 @@ class ElectrumGui(BaseElectrumGui, EventListener):
fee = int(Decimal(out['Default fee']) * COIN) fee = int(Decimal(out['Default fee']) * COIN)
self.config.set_key('fee_per_kb', fee, True) self.config.set_key('fee_per_kb', fee, True)
def password_dialog(self): def password_dialog(self):
out = self.run_dialog('Password', [ out = self.run_dialog('Password', [
{'label':'Password', 'type':'password', 'value':''} {'label':'Password', 'type':'password', 'value':''}
], buttons = 1) ], buttons = 1)
return out.get('Password') return out.get('Password')
def run_dialog(self, title, items, interval=2, buttons=None, y_pos=3): def run_dialog(self, title, items, interval=2, buttons=None, y_pos=3):
self.popup_pos = 0 self.popup_pos = 0
@ -477,12 +779,10 @@ class ElectrumGui(BaseElectrumGui, EventListener):
w.clear() w.clear()
w.border(0) w.border(0)
w.addstr(0, 2, title) w.addstr(0, 2, title)
num = len(list(items)) num = len(list(items))
numpos = num numpos = num
if buttons: numpos += 2 if buttons:
numpos += 2
for i in range(num): for i in range(num):
item = items[i] item = items[i]
label = item.get('label') label = item.get('label')
@ -513,10 +813,13 @@ class ElectrumGui(BaseElectrumGui, EventListener):
w.refresh() w.refresh()
c = self.stdscr.getch() c = self.getch()
if c in [ord('q'), 27]: break if c in [ord('q'), 27]:
elif c in [curses.KEY_LEFT, curses.KEY_UP]: self.popup_pos -= 1 break
elif c in [curses.KEY_RIGHT, curses.KEY_DOWN]: self.popup_pos +=1 elif c in [curses.KEY_LEFT, curses.KEY_UP]:
self.popup_pos -= 1
elif c in [curses.KEY_RIGHT, curses.KEY_DOWN]:
self.popup_pos +=1
else: else:
i = self.popup_pos%numpos i = self.popup_pos%numpos
if buttons and c==10: if buttons and c==10:
@ -553,5 +856,65 @@ class ElectrumGui(BaseElectrumGui, EventListener):
elif _type == 'button': elif _type == 'button':
out['button'] = item.get('label') out['button'] = item.get('label')
break break
return out return out
def print_textbox(self, w, y, x, _text, pos):
width = 60
for i in range(len(_text)//width + 1):
s = _text[i*width:(i+1)*width]
w.addstr(y+i, x, s, curses.A_REVERSE if self.qr_mode == pos else curses.A_NORMAL)
return i
def show_request(self, key):
req = self.wallet.get_request(key)
addr = req.get_address() or ''
URI = req.get_bip21_URI()
lnaddr = req.lightning_invoice or ''
self.w = curses.newwin(self.maxy - 2, self.maxx - 2, 1, 1)
w = self.w
while True:
if self.qr_mode == 0:
text = addr
data = addr
elif self.qr_mode == 1:
text = URI
data = URI
elif self.qr_mode == 2:
text = lnaddr
data = lnaddr.upper()
w.clear()
w.border(0)
w.addstr(0, 2, ' ' + _('Payment Request') + ' ')
y = 2
w.addstr(y, 2, "Address")
h1 = self.print_textbox(w, y, 13, addr, 0)
y += h1 + 2
w.addstr(y, 2, "URI")
h2 = self.print_textbox(w, y, 13, URI, 1)
y += h2 + 2
w.addstr(y, 2, "Lightning")
h3 = self.print_textbox(w, y, 13, lnaddr, 2)
y += h3 + 2
lines = self.get_qr(data)
qr_width = len(lines) * 2
x = self.maxx - qr_width
if x > 60:
self.print_qr(w, 1, x, lines)
else:
w.addstr(y, 2, "Window too small for qr code")
w.refresh()
c = self.getch()
if c in [curses.KEY_UP]:
self.qr_mode -= 1
elif c in [curses.KEY_DOWN, 9]:
self.qr_mode +=1
elif c == ord(' '):
pyperclip.copy(text)
self.print_clipboard()
else:
break
self.qr_mode = self.qr_mode % 3
self.stdscr.refresh()
return

Loading…
Cancel
Save