Browse Source

follow-up prev:

- detect payment of requests both onchain or LN
 - create single type of requests in GUI
patch-4
ThomasV 3 years ago
parent
commit
7102fb732e
  1. 3
      electrum/commands.py
  2. 55
      electrum/gui/qt/main_window.py
  3. 22
      electrum/invoices.py
  4. 6
      electrum/lnaddr.py
  5. 13
      electrum/lnworker.py
  6. 1
      electrum/submarine_swaps.py
  7. 81
      electrum/wallet.py

3
electrum/commands.py

@ -913,8 +913,7 @@ class Commands:
return False
amount = satoshis(amount)
expiration = int(expiration) if expiration else None
req = wallet.make_payment_request(addr, amount, memo, expiration)
wallet.add_payment_request(req)
req = wallet.create_request(amount, memo, expiration, addr, False)
return wallet.export_request(req)
@command('wnl')

55
electrum/gui/qt/main_window.py

@ -1151,20 +1151,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.clear_invoice_button = QPushButton(_('Clear'))
self.clear_invoice_button.clicked.connect(self.clear_receive_tab)
self.create_invoice_button = QPushButton(_('New Address'))
self.create_invoice_button.setIcon(read_QIcon("bitcoin.png"))
self.create_invoice_button.setToolTip('Create on-chain request')
self.create_invoice_button.clicked.connect(lambda: self.create_invoice(False))
self.create_invoice_button = QPushButton(_('Create Request'))
self.create_invoice_button.clicked.connect(lambda: self.create_invoice())
self.receive_buttons = buttons = QHBoxLayout()
buttons.addStretch(1)
buttons.addWidget(self.clear_invoice_button)
buttons.addWidget(self.create_invoice_button)
if self.wallet.has_lightning():
self.create_lightning_invoice_button = QPushButton(_('Lightning'))
self.create_lightning_invoice_button.setToolTip('Create lightning request')
self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png"))
self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True))
buttons.addWidget(self.create_lightning_invoice_button)
grid.addLayout(buttons, 4, 0, 1, -1)
self.receive_payreq_e = ButtonsTextEdit()
@ -1262,27 +1254,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
else:
return
def create_invoice(self, is_lightning: bool):
amount = self.receive_amount_e.get_amount()
def create_invoice(self):
amount_sat = self.receive_amount_e.get_amount()
message = self.receive_message_e.text()
expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
if amount_sat and amount_sat < self.wallet.dust_threshold():
address = None
if not self.wallet.has_lightning():
return
else:
address = self.get_bitcoin_address_for_request(amount_sat)
if not address:
return
self.address_list.update()
# generate even if we cannot receive
lightning = self.wallet.has_lightning()
try:
if is_lightning:
if not self.wallet.lnworker.channels:
self.show_error(_("You need to open a Lightning channel first."))
return
# TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy)
key = self.wallet.lnworker.add_request(amount, message, expiry)
else:
key = self.create_bitcoin_request(amount, message, expiry)
if not key:
return
self.address_list.refresh_all()
key = self.wallet.create_request(amount_sat, message, expiry, address, lightning=lightning)
except InvoiceError as e:
self.show_error(_('Error creating payment request') + ':\n' + str(e))
return
assert key is not None
self.address_list.refresh_all()
self.request_list.update()
self.request_list.select_key(key)
# clear request fields
@ -1291,10 +1287,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
# copy to clipboard
r = self.wallet.get_request(key)
content = r.lightning_invoice if r.is_lightning() else r.get_address()
title = _('Invoice') if is_lightning else _('Address')
title = _('Invoice') if r.is_lightning() else _('Address')
self.do_copy(content, title=title)
def create_bitcoin_request(self, amount: int, message: str, expiration: int) -> Optional[str]:
def get_bitcoin_address_for_request(self, amount: int) -> Optional[str]:
addr = self.wallet.get_unused_address()
if addr is None:
if not self.wallet.is_deterministic(): # imported wallet
@ -1311,15 +1307,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
return
addr = self.wallet.create_new_address(False)
timestamp = int(time.time())
req = self.wallet.make_payment_request(amount, message, timestamp, expiration, address=addr)
try:
self.wallet.add_payment_request(req)
except Exception as e:
self.logger.exception('Error adding payment request')
self.show_error(_('Error adding payment request') + ':\n' + repr(e))
else:
self.sign_payment_request(addr)
return addr
def do_copy(self, content: str, *, title: str = None) -> None:

22
electrum/invoices.py

@ -10,6 +10,7 @@ from .util import age, InvoiceError
from .lnaddr import lndecode, LnAddr
from . import constants
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from .bitcoin import address_to_script
from .transaction import PartialTxOutput
if TYPE_CHECKING:
@ -116,7 +117,18 @@ class Invoice(StoredObject):
def get_address(self) -> str:
"""returns the first address, to be displayed in GUI"""
return self.outputs[0].address
if self.is_lightning():
return self._lnaddr.get_fallback_address() or None
else:
return self.outputs[0].address
def get_outputs(self):
if self.is_lightning():
address = self.get_address()
outputs = [PartialTxOutput.from_address_and_value(address, int(self.get_amount_sat()))] if address else []
else:
outputs = self.outputs
return outputs
def get_expiration_date(self):
# 0 means never
@ -141,6 +153,14 @@ class Invoice(StoredObject):
return None
return int(amount_msat / 1000)
def get_bip21_URI(self):
from electrum.util import create_bip21_uri
addr = self.get_address()
amount = int(self.get_amount_sat())
message = self.message
uri = create_bip21_uri(addr, amount, message)
return str(uri)
@lightning_invoice.validator
def _validate_invoice_str(self, attribute, value):
if value is not None:

6
electrum/lnaddr.py

@ -211,7 +211,8 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)
data += tagged('t', route)
elif k == 'f':
data += encode_fallback(v, addr.net)
if v is not None:
data += encode_fallback(v, addr.net)
elif k == 'd':
# truncate to max length: 1024*5 bits = 639 bytes
data += tagged_bytes('d', v.encode()[0:639])
@ -336,6 +337,9 @@ class LnAddr(object):
def get_description(self) -> str:
return self.get_tag('d') or ''
def get_fallback_address(self) -> str:
return self.get_tag('f') or ''
def get_expiry(self) -> int:
exp = self.get_tag('x')
if exp is None:

13
electrum/lnworker.py

@ -1793,18 +1793,7 @@ class LNWallet(LNWorker):
expiry=expiry,
write_to_disk=False,
)
req = self.wallet.make_payment_request(
amount_sat,
message,
timestamp,
expiry,
address=None,
lightning_invoice=invoice
)
key = self.wallet.add_payment_request(req, write_to_disk=False)
self.wallet.set_label(key, message)
self.wallet.save_db()
return key
return invoice
def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True):
assert sha256(preimage) == payment_hash

1
electrum/submarine_swaps.py

@ -278,6 +278,7 @@ class SwapManager(Logger):
amount_msat=lightning_amount_sat * 1000,
message='swap',
expiry=3600 * 24,
fallback_address=None,
)
payment_hash = lnaddr.paymenthash
preimage = self.lnworker.get_preimage(payment_hash)

81
electrum/wallet.py

@ -789,7 +789,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if self.is_onchain_invoice_paid(invoice, 0):
self.logger.info("saving invoice... but it is already paid!")
with self.transaction_lock:
for txout in invoice.outputs:
for txout in invoice.get_outputs():
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
self.invoices[key] = invoice
self.save_db()
@ -854,15 +854,18 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# scriptpubkey -> list(invoice_keys)
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
for invoice_key, invoice in self.invoices.items():
if not invoice.is_lightning():
for txout in invoice.outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
if invoice.is_lightning() and not invoice.get_address():
continue
for txout in invoice.get_outputs():
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
def _is_onchain_invoice_paid(self, invoice: Invoice, conf: int) -> Tuple[bool, Sequence[str]]:
"""Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
assert not invoice.is_lightning()
if invoice.is_lightning() and not invoice.get_address():
return False, []
outputs = invoice.get_outputs()
invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats
for txo in invoice.outputs: # type: PartialTxOutput
for txo in outputs: # type: PartialTxOutput
invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
relevant_txs = []
with self.lock, self.transaction_lock:
@ -2048,6 +2051,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_unused_addresses(self) -> Sequence[str]:
domain = self.get_receiving_addresses()
# TODO we should index receive_requests by id
# add lightning requests. (use as key)
in_use_by_request = [k for k in self.receive_requests.keys()
if self.get_request_status(k) != PR_EXPIRED]
in_use_by_request = set(in_use_by_request)
@ -2116,6 +2120,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return False, None
def get_request_URI(self, req: Invoice) -> str:
# todo: should be a method of invoice?
addr = req.get_address()
message = self.get_label(addr)
amount = req.get_amount_sat()
@ -2139,31 +2144,34 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return status
def get_invoice_status(self, invoice: Invoice):
if invoice.is_lightning():
status = self.lnworker.get_invoice_status(invoice) if self.lnworker else PR_UNKNOWN
# lightning invoices can be paid onchain
if invoice.is_lightning() and self.lnworker:
status = self.lnworker.get_invoice_status(invoice)
if status != PR_UNPAID:
return self.check_expired_status(invoice, status)
if self.is_onchain_invoice_paid(invoice, 1):
status = PR_PAID
elif self.is_onchain_invoice_paid(invoice, 0):
status = PR_UNCONFIRMED
else:
if self.is_onchain_invoice_paid(invoice, 1):
status =PR_PAID
elif self.is_onchain_invoice_paid(invoice, 0):
status = PR_UNCONFIRMED
else:
status = PR_UNPAID
status = PR_UNPAID
return self.check_expired_status(invoice, status)
def get_request_status(self, key):
r = self.get_request(key)
if r is None:
return PR_UNKNOWN
if r.is_lightning():
status = self.lnworker.get_payment_status(bfh(r.rhash)) if self.lnworker else PR_UNKNOWN
if r.is_lightning() and self.lnworker:
status = self.lnworker.get_payment_status(bfh(r.rhash))
if status != PR_UNPAID:
return self.check_expired_status(r, status)
paid, conf = self.get_onchain_request_status(r)
if not paid:
status = PR_UNPAID
elif conf == 0:
status = PR_UNCONFIRMED
else:
paid, conf = self.get_onchain_request_status(r)
if not paid:
status = PR_UNPAID
elif conf == 0:
status = PR_UNCONFIRMED
else:
status = PR_PAID
status = PR_PAID
return self.check_expired_status(r, status)
def get_request(self, key):
@ -2268,23 +2276,26 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
status = self.get_request_status(addr)
util.trigger_callback('request_status', self, addr, status)
def make_payment_request(self, amount_sat, message, timestamp, expiration, address=None, lightning_invoice=None):
# TODO maybe merge with wallet.create_invoice()...
# note that they use incompatible "id"
def create_request(self, amount_sat: int, message: str, exp_delay: int, address: str, lightning: bool):
# for receiving
amount_sat = amount_sat or 0
#_id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
expiration = expiration or 0
outputs=[PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
return Invoice(
exp_delay = exp_delay or 0
timestamp = int(time.time())
lightning_invoice = self.lnworker.add_request(amount_sat, message, exp_delay) if lightning else None
outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
height = self.get_local_height()
req = Invoice(
outputs=outputs,
message=message,
time=timestamp,
amount_msat=amount_sat*1000,
exp=expiration,
height=self.get_local_height(),
exp=exp_delay,
height=height,
bip70=None,
lightning_invoice=lightning_invoice,
)
key = self.add_payment_request(req, write_to_disk=False)
return key
def sign_payment_request(self, key, alias, alias_addr, password): # FIXME this is broken
raise
@ -2304,11 +2315,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if invoice.is_lightning():
key = invoice.rhash
else:
key = bh2u(sha256d(repr(invoice.outputs) + "%d"%invoice.time))[0:10]
key = bh2u(sha256d(repr(invoice.get_outputs()) + "%d"%invoice.time))[0:10]
return key
def get_key_for_receive_request(self, req: Invoice, *, sanity_checks: bool = False) -> str:
"""Return the key to use for this invoice in self.receive_requests."""
# FIXME: this should be a method of Invoice
if not req.is_lightning():
addr = req.get_address()
if sanity_checks:
@ -2318,7 +2330,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raise Exception(_('Address not in wallet.'))
key = addr
else:
key = req.rhash
addr = req.get_address()
key = req.rhash if addr is None else addr
return key
def add_payment_request(self, req: Invoice, *, write_to_disk: bool = True):

Loading…
Cancel
Save