follow-up prev:
- detect payment of requests both onchain or LN - create single type of requests in GUI
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user