Refactor invoices in lnworker.

- use InvoiceInfo (NamedTuple) for normal operations,
   because lndecode operations can be very slow.
 - all invoices/requests are stored in wallet
 - invoice expiration detection is performed in wallet
 - CLI commands: list_invoices, add_request, add_lightning_request
 - revert 0062c6d695 because it forbids self-payments
This commit is contained in:
ThomasV 2019-09-20 17:15:49 +02:00
parent 0a395fefbc
commit f08e5541ae
9 changed files with 214 additions and 244 deletions

View file

@ -716,7 +716,7 @@ class Commands:
def _format_request(self, out):
from .util import get_request_status
out['amount_BTC'] = format_satoshis(out.get('amount'))
out['status'] = get_request_status(out)
out['status_str'] = get_request_status(out)
return out
@command('w')
@ -733,7 +733,7 @@ class Commands:
# pass
@command('w')
async def listrequests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
"""List the payment requests you made."""
out = wallet.get_sorted_requests()
if pending:
@ -760,7 +760,7 @@ class Commands:
return wallet.get_unused_address()
@command('w')
async def addrequest(self, amount, memo='', expiration=None, force=False, wallet: Abstract_Wallet = None):
async def add_request(self, amount, memo='', expiration=3600, force=False, wallet: Abstract_Wallet = None):
"""Create a payment request, using the first unused address of the wallet.
The address will be considered as used after this operation.
If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet."""
@ -777,6 +777,12 @@ class Commands:
out = wallet.get_request(addr)
return self._format_request(out)
@command('wn')
async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None):
amount_sat = int(satoshis(amount))
key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration)
return wallet.get_request(key)['invoice']
@command('w')
async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
""" Add a transaction to the wallet history """
@ -894,13 +900,6 @@ class Commands:
async def lnpay(self, invoice, attempts=1, timeout=10, wallet: Abstract_Wallet = None):
return await wallet.lnworker._pay(invoice, attempts=attempts)
@command('wn')
async def addinvoice(self, requested_amount, message, expiration=3600, wallet: Abstract_Wallet = None):
# using requested_amount because it is documented in param_descriptions
payment_hash = await wallet.lnworker._add_invoice_coro(satoshis(requested_amount), message, expiration)
invoice, direction, is_paid = wallet.lnworker.invoices[bh2u(payment_hash)]
return invoice
@command('w')
async def nodeid(self, wallet: Abstract_Wallet = None):
listen_addr = self.config.get('lightning_listen')
@ -925,21 +924,8 @@ class Commands:
self.network.path_finder.blacklist.clear()
@command('w')
async def lightning_invoices(self, wallet: Abstract_Wallet = None):
from .util import pr_tooltips
out = []
for payment_hash, (preimage, invoice, is_received, timestamp) in wallet.lnworker.invoices.items():
status = wallet.lnworker.get_invoice_status(payment_hash)
item = {
'date':timestamp_to_datetime(timestamp),
'direction': 'received' if is_received else 'sent',
'payment_hash':payment_hash,
'invoice':invoice,
'preimage':preimage,
'status':pr_tooltips[status]
}
out.append(item)
return out
async def list_invoices(self, wallet: Abstract_Wallet = None):
return wallet.get_invoices()
@command('w')
async def lightning_history(self, wallet: Abstract_Wallet = None):

View file

@ -428,14 +428,11 @@ class ElectrumWindow(App):
def show_request(self, is_lightning, key):
from .uix.dialogs.request_dialog import RequestDialog
if is_lightning:
request, direction, is_paid = self.wallet.lnworker.invoices.get(key) or (None, None, None)
status = self.wallet.lnworker.get_invoice_status(key)
else:
request = self.wallet.get_request_URI(key)
status, conf = self.wallet.get_request_status(key)
self.request_popup = RequestDialog('Request', request, key)
self.request_popup.set_status(status)
request = self.wallet.get_request(key)
status = request['status']
data = request['invoice'] if is_lightning else request['URI']
self.request_popup = RequestDialog('Request', data, key)
self.request_popup.set_status(request['status'])
self.request_popup.open()
def show_invoice(self, is_lightning, key):
@ -444,10 +441,7 @@ class ElectrumWindow(App):
if not invoice:
return
status = invoice['status']
if is_lightning:
data = invoice['invoice']
else:
data = key
data = invoice['invoice'] if is_lightning else key
self.invoice_popup = InvoiceDialog('Invoice', data, key)
self.invoice_popup.open()

View file

@ -304,12 +304,7 @@ class SendScreen(CScreen):
return
message = self.screen.message
if self.screen.is_lightning:
return {
'type': PR_TYPE_LN,
'invoice': address,
'amount': amount,
'message': message,
}
return self.app.wallet.lnworker.parse_bech32_invoice(address)
else:
if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)

View file

@ -1073,8 +1073,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
message = self.receive_message_e.text()
expiry = self.config.get('request_expiry', 3600)
if is_lightning:
payment_hash = self.wallet.lnworker.add_invoice(amount, message, expiry)
key = bh2u(payment_hash)
key = self.wallet.lnworker.add_request(amount, message, expiry)
else:
key = self.create_bitcoin_request(amount, message, expiry)
self.address_list.update()
@ -1698,12 +1697,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
message = self.message_e.text()
amount = self.amount_e.get_amount()
if not self.is_onchain:
return {
'type': PR_TYPE_LN,
'invoice': self.payto_e.lightning_invoice,
'amount': amount,
'message': message,
}
return self.wallet.lnworker.parse_bech32_invoice(self.payto_e.lightning_invoice)
else:
outputs = self.read_outputs()
if self.check_send_tab_outputs_and_show_errors(outputs):
@ -1733,7 +1727,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def do_pay_invoice(self, invoice, preview=False):
if invoice['type'] == PR_TYPE_LN:
self.pay_lightning_invoice(self.payto_e.lightning_invoice)
self.pay_lightning_invoice(invoice['invoice'])
return
elif invoice['type'] == PR_TYPE_ONCHAIN:
message = invoice['message']

View file

@ -1402,13 +1402,13 @@ class Peer(Logger):
await self.await_local(chan, local_ctn)
await self.await_remote(chan, remote_ctn)
try:
invoice = self.lnworker.get_invoice(htlc.payment_hash)
info = self.lnworker.get_invoice_info(htlc.payment_hash)
preimage = self.lnworker.get_preimage(htlc.payment_hash)
except UnknownPaymentHash:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason)
return
expected_received_msat = int(invoice.amount * bitcoin.COIN * 1000) if invoice.amount is not None else None
expected_received_msat = int(info.amount * 1000) if info.amount is not None else None
if expected_received_msat is not None and \
not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat):
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
@ -1431,7 +1431,7 @@ class Peer(Logger):
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason)
return
self.network.trigger_callback('htlc_added', htlc, invoice, RECEIVED)
#self.network.trigger_callback('htlc_added', htlc, invoice, RECEIVED)
await asyncio.sleep(self.network.config.lightning_settle_delay)
await self._fulfill_htlc(chan, htlc.htlc_id, preimage)

View file

@ -82,6 +82,16 @@ FALLBACK_NODE_LIST_MAINNET = [
encoder = ChannelJsonEncoder()
from typing import NamedTuple
class InvoiceInfo(NamedTuple):
payment_hash: bytes
amount: int
direction: int
status: int
class LNWorker(Logger):
def __init__(self, xprv):
@ -313,7 +323,7 @@ class LNWallet(LNWorker):
LNWorker.__init__(self, xprv)
self.ln_keystore = keystore.from_xprv(xprv)
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
self.invoices = self.storage.get('lightning_invoices', {}) # RHASH -> (invoice, direction, is_paid)
self.invoices = self.storage.get('lightning_invoices2', {}) # RHASH -> amount, direction, is_paid
self.preimages = self.storage.get('lightning_preimages', {}) # RHASH -> preimage
self.sweep_address = wallet.get_receiving_address()
self.lock = threading.RLock()
@ -409,16 +419,6 @@ class LNWallet(LNWorker):
timestamp = int(time.time())
self.network.trigger_callback('ln_payment_completed', timestamp, direction, htlc, preimage, chan_id)
def get_invoice_status(self, key):
if key not in self.invoices:
return PR_UNKNOWN
invoice, direction, status = self.invoices[key]
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
if status == PR_UNPAID and lnaddr.is_expired():
return PR_EXPIRED
else:
return status
def get_payments(self):
# return one item per payment_hash
# note: with AMP we will have several channels per payment
@ -431,6 +431,20 @@ class LNWallet(LNWorker):
out[k].append(v)
return out
def parse_bech32_invoice(self, invoice):
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
return {
'type': PR_TYPE_LN,
'invoice': invoice,
'amount': amount,
'message': lnaddr.get_description(),
'time': lnaddr.date,
'exp': lnaddr.get_expiry(),
'pubkey': bh2u(lnaddr.pubkey.serialize()),
'rhash': lnaddr.paymenthash.hex(),
}
def get_unsettled_payments(self):
out = []
for payment_hash, plist in self.get_payments().items():
@ -455,7 +469,7 @@ class LNWallet(LNWorker):
def get_history(self):
out = []
for payment_hash, plist in self.get_payments().items():
for key, plist in self.get_payments().items():
plist = list(filter(lambda x: x[3] == 'settled', plist))
if len(plist) == 0:
continue
@ -464,11 +478,13 @@ class LNWallet(LNWorker):
direction = 'sent' if _direction == SENT else 'received'
amount_msat = int(_direction) * htlc.amount_msat
timestamp = htlc.timestamp
label = self.wallet.get_label(payment_hash)
req = self.get_request(payment_hash)
if req and _direction == SENT:
req_amount_msat = -req['amount']*1000
fee_msat = req_amount_msat - amount_msat
label = self.wallet.get_label(key)
if _direction == SENT:
try:
inv = self.get_invoice_info(bfh(key))
fee_msat = inv.amount*1000 - amount_msat if inv.amount else None
except UnknownPaymentHash:
fee_msat = None
else:
fee_msat = None
else:
@ -489,7 +505,7 @@ class LNWallet(LNWorker):
'status': status,
'amount_msat': amount_msat,
'fee_msat': fee_msat,
'payment_hash': payment_hash
'payment_hash': key,
}
out.append(item)
# add funding events
@ -831,20 +847,23 @@ class LNWallet(LNWorker):
@log_exceptions
async def _pay(self, invoice, amount_sat=None, attempts=1):
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
key = bh2u(addr.paymenthash)
if key in self.preimages:
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
key = bh2u(lnaddr.paymenthash)
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
status = self.get_invoice_status(lnaddr.paymenthash)
if status == PR_PAID:
raise PaymentFailure(_("This invoice has been paid already"))
info = InvoiceInfo(lnaddr.paymenthash, amount, SENT, PR_UNPAID)
self.save_invoice_info(info)
self._check_invoice(invoice, amount_sat)
self.save_invoice(addr.paymenthash, invoice, SENT, PR_INFLIGHT)
self.wallet.set_label(key, addr.get_description())
self.wallet.set_label(key, lnaddr.get_description())
for i in range(attempts):
route = await self._create_route_from_invoice(decoded_invoice=addr)
route = await self._create_route_from_invoice(decoded_invoice=lnaddr)
if not self.get_channel_by_short_id(route[0].short_channel_id):
scid = route[0].short_channel_id
raise Exception(f"Got route with unknown first channel: {scid}")
self.network.trigger_callback('payment_status', key, 'progress', i)
if await self._pay_to_route(route, addr, invoice):
if await self._pay_to_route(route, lnaddr, invoice):
return True
return False
@ -854,10 +873,12 @@ class LNWallet(LNWorker):
if not chan:
raise Exception(f"PathFinder returned path with short_channel_id "
f"{short_channel_id} that is not in channel list")
self.set_invoice_status(addr.paymenthash, PR_INFLIGHT)
peer = self.peers[route[0].node_id]
htlc = await peer.pay(route, chan, int(addr.amount * COIN * 1000), addr.paymenthash, addr.get_min_final_cltv_expiry())
self.network.trigger_callback('htlc_added', htlc, addr, SENT)
success = await self.pending_payments[(short_channel_id, htlc.htlc_id)]
self.set_invoice_status(addr.paymenthash, (PR_PAID if success else PR_UNPAID))
return success
@staticmethod
@ -933,119 +954,89 @@ class LNWallet(LNWorker):
raise PaymentFailure(_("No path found"))
return route
def add_invoice(self, amount_sat, message, expiry):
coro = self._add_invoice_coro(amount_sat, message, expiry)
def add_request(self, amount_sat, message, expiry):
coro = self._add_request_coro(amount_sat, message, expiry)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
try:
return fut.result(timeout=5)
except concurrent.futures.TimeoutError:
raise Exception(_("add_invoice timed out"))
raise Exception(_("add invoice timed out"))
@log_exceptions
async def _add_invoice_coro(self, amount_sat, message, expiry):
payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
async def _add_request_coro(self, amount_sat, message, expiry):
timestamp = int(time.time())
routing_hints = await self._calc_routing_hints_for_invoice(amount_sat)
if not routing_hints:
self.logger.info("Warning. No routing hints added to invoice. "
"Other clients will likely not be able to send to us.")
invoice = lnencode(LnAddr(payment_hash, amount_btc,
tags=[('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('x', expiry)]
+ routing_hints),
self.node_keypair.privkey)
self.save_invoice(payment_hash, invoice, RECEIVED, PR_UNPAID)
payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage)
info = InvoiceInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
lnaddr = LnAddr(payment_hash, amount_btc,
tags=[('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('x', expiry)]
+ routing_hints,
date = timestamp)
invoice = lnencode(lnaddr, self.node_keypair.privkey)
key = bh2u(lnaddr.paymenthash)
req = {
'type': PR_TYPE_LN,
'amount': amount_sat,
'time': lnaddr.date,
'exp': expiry,
'message': message,
'rhash': key,
'invoice': invoice
}
self.save_preimage(payment_hash, payment_preimage)
self.wallet.set_label(bh2u(payment_hash), message)
return payment_hash
self.save_invoice_info(info)
self.wallet.add_payment_request(req)
self.wallet.set_label(key, message)
return key
def save_preimage(self, payment_hash: bytes, preimage: bytes):
assert sha256(preimage) == payment_hash
key = bh2u(payment_hash)
self.preimages[key] = bh2u(preimage)
self.preimages[bh2u(payment_hash)] = bh2u(preimage)
self.storage.put('lightning_preimages', self.preimages)
self.storage.write()
def get_preimage(self, payment_hash: bytes) -> bytes:
try:
preimage = bfh(self.preimages[bh2u(payment_hash)])
assert sha256(preimage) == payment_hash
return preimage
except KeyError as e:
raise UnknownPaymentHash(payment_hash) from e
return bfh(self.preimages.get(bh2u(payment_hash)))
def save_new_invoice(self, invoice):
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
self.save_invoice(addr.paymenthash, invoice, SENT, PR_UNPAID)
def save_invoice(self, payment_hash:bytes, invoice, direction, status):
key = bh2u(payment_hash)
def get_invoice_info(self, payment_hash: bytes) -> bytes:
key = payment_hash.hex()
with self.lock:
self.invoices[key] = invoice, direction, status
self.storage.put('lightning_invoices', self.invoices)
if key not in self.invoices:
raise UnknownPaymentHash(payment_hash)
amount, direction, status = self.invoices[key]
return InvoiceInfo(payment_hash, amount, direction, status)
def save_invoice_info(self, info):
key = info.payment_hash.hex()
with self.lock:
self.invoices[key] = info.amount, info.direction, info.status
self.storage.put('lightning_invoices2', self.invoices)
self.storage.write()
def set_invoice_status(self, payment_hash, status):
key = bh2u(payment_hash)
if key not in self.invoices:
def get_invoice_status(self, payment_hash):
try:
info = self.get_invoice_info(payment_hash)
return info.status
except UnknownPaymentHash:
return PR_UNKNOWN
def set_invoice_status(self, payment_hash: bytes, status):
try:
info = self.get_invoice_info(payment_hash)
except UnknownPaymentHash:
# if we are forwarding
return
invoice, direction, _ = self.invoices[key]
self.save_invoice(payment_hash, invoice, direction, status)
if direction == RECEIVED and status == PR_PAID:
self.network.trigger_callback('payment_received', self.wallet, key, PR_PAID)
def get_invoice(self, payment_hash: bytes) -> LnAddr:
try:
invoice, direction, is_paid = self.invoices[bh2u(payment_hash)]
return lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
except KeyError as e:
raise UnknownPaymentHash(payment_hash) from e
def get_request(self, key):
if key not in self.invoices:
return
# todo: parse invoices when saving
invoice, direction, is_paid = self.invoices[key]
status = self.get_invoice_status(key)
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
amount_sat = int(lnaddr.amount*COIN) if lnaddr.amount else None
description = lnaddr.get_description()
timestamp = lnaddr.date
return {
'type': PR_TYPE_LN,
'status': status,
'amount': amount_sat,
'time': timestamp,
'exp': lnaddr.get_expiry(),
'message': description,
'rhash': key,
'invoice': invoice
}
@profiler
def get_invoices(self):
# invoices = outgoing
out = []
with self.lock:
invoice_items = list(self.invoices.items())
for key, (invoice, direction, status) in invoice_items:
if direction == SENT and status != PR_PAID:
out.append(self.get_request(key))
return out
@profiler
def get_requests(self):
# requests = incoming
out = []
with self.lock:
invoice_items = list(self.invoices.items())
for key, (invoice, direction, status) in invoice_items:
if direction == RECEIVED and status != PR_PAID:
out.append(self.get_request(key))
return out
info = info._replace(status=status)
self.save_invoice_info(info)
if info.direction == RECEIVED and info.status == PR_PAID:
self.network.trigger_callback('payment_received', self.wallet, bh2u(payment_hash), PR_PAID)
async def _calc_routing_hints_for_invoice(self, amount_sat):
"""calculate routing hints (BOLT-11 'r' field)"""

View file

@ -114,7 +114,7 @@ if [[ $1 == "open" ]]; then
fi
if [[ $1 == "alice_pays_carol" ]]; then
request=$($carol addinvoice 0.0001 "blah")
request=$($carol add_lightning_request 0.0001 -m "blah")
$alice lnpay $request
carol_balance=$($carol list_channels | jq -r '.[0].local_balance')
echo "carol balance: $carol_balance"
@ -140,12 +140,12 @@ if [[ $1 == "breach" ]]; then
channel=$($alice open_channel $bob_node 0.15)
new_blocks 3
wait_until_channel_open alice
request=$($bob addinvoice 0.01 "blah")
request=$($bob add_lightning_request 0.01 -m "blah")
echo "alice pays"
$alice lnpay $request
sleep 2
ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
request=$($bob addinvoice 0.01 "blah2")
request=$($bob add_lightning_request 0.01 -m "blah2")
echo "alice pays again"
$alice lnpay $request
echo "alice broadcasts old ctx"
@ -168,7 +168,7 @@ if [[ $1 == "redeem_htlcs" ]]; then
new_blocks 6
sleep 10
# alice pays bob
invoice=$($bob addinvoice 0.05 "test")
invoice=$($bob add_lightning_request 0.05 -m "test")
$alice lnpay $invoice --timeout=1 || true
sleep 1
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
@ -214,7 +214,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then
new_blocks 3
wait_until_channel_open alice
echo "alice pays bob"
invoice=$($bob addinvoice 0.05 "test")
invoice=$($bob add_lightning_request 0.05 -m "test")
$alice lnpay $invoice --timeout=1 || true
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "0" ]]; then
@ -246,7 +246,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then
new_blocks 3
wait_until_channel_open alice
echo "alice pays bob"
invoice=$($bob addinvoice 0.05 "test")
invoice=$($bob add_lightning_request 0.05 -m "test")
$alice lnpay $invoice --timeout=1 || true
ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
@ -310,11 +310,11 @@ if [[ $1 == "watchtower" ]]; then
new_blocks 3
wait_until_channel_open alice
echo "alice pays bob"
invoice1=$($bob addinvoice 0.05 "invoice1")
invoice1=$($bob add_lightning_request 0.05 -m "invoice1")
$alice lnpay $invoice1
invoice2=$($bob addinvoice 0.05 "invoice2")
invoice2=$($bob add_lightning_request 0.05 -m "invoice2")
$alice lnpay $invoice2
invoice3=$($bob addinvoice 0.05 "invoice3")
invoice3=$($bob add_lightning_request 0.05 -m "invoice3")
$alice lnpay $invoice3
fi

View file

@ -21,6 +21,7 @@ from electrum.channel_db import ChannelDB
from electrum.lnworker import LNWallet
from electrum.lnmsg import encode_msg, decode_msg
from electrum.logging import console_stderr_handler
from electrum.lnworker import InvoiceInfo, RECEIVED, PR_UNPAID
from .test_lnchannel import create_test_channels
from . import SequentialTestCase
@ -80,6 +81,7 @@ class MockWallet:
pass
class MockLNWallet:
storage = MockStorage()
def __init__(self, remote_keypair, local_keypair, chan, tx_queue):
self.chan = chan
self.remote_keypair = remote_keypair
@ -87,7 +89,6 @@ class MockLNWallet:
self.network = MockNetwork(tx_queue)
self.channels = {self.chan.channel_id: self.chan}
self.invoices = {}
self.preimages = {}
self.inflight = {}
self.wallet = MockWallet()
self.localfeatures = LnLocalFeatures(0)
@ -122,7 +123,11 @@ class MockLNWallet:
def save_invoice(*args, is_paid=False):
pass
get_invoice = LNWallet.get_invoice
preimages = {}
get_invoice_info = LNWallet.get_invoice_info
save_invoice_info = LNWallet.save_invoice_info
set_invoice_status = LNWallet.set_invoice_status
save_preimage = LNWallet.save_preimage
get_preimage = LNWallet.get_preimage
_create_route_from_invoice = LNWallet._create_route_from_invoice
_check_invoice = staticmethod(LNWallet._check_invoice)
@ -207,19 +212,20 @@ class TestPeer(SequentialTestCase):
@staticmethod
def prepare_invoice(w2 # receiver
):
amount_btc = 100000/Decimal(COIN)
amount_sat = 100000
amount_btc = amount_sat/Decimal(COIN)
payment_preimage = os.urandom(32)
RHASH = sha256(payment_preimage)
addr = LnAddr(
info = InvoiceInfo(RHASH, amount_sat, RECEIVED, PR_UNPAID)
w2.save_preimage(RHASH, payment_preimage)
w2.save_invoice_info(info)
lnaddr = LnAddr(
RHASH,
amount_btc,
tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('d', 'coffee')
])
pay_req = lnencode(addr, w2.node_keypair.privkey)
w2.preimages[bh2u(RHASH)] = bh2u(payment_preimage)
w2.invoices[bh2u(RHASH)] = (pay_req, True, False)
return pay_req
return lnencode(lnaddr, w2.node_keypair.privkey)
def test_payment(self):
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers()

View file

@ -541,16 +541,16 @@ class Abstract_Wallet(AddressSynchronizer):
def save_invoice(self, invoice):
invoice_type = invoice['type']
if invoice_type == PR_TYPE_LN:
self.lnworker.save_new_invoice(invoice['invoice'])
key = invoice['rhash']
elif invoice_type == PR_TYPE_ONCHAIN:
key = bh2u(sha256(repr(invoice))[0:16])
invoice['id'] = key
invoice['txid'] = None
self.invoices[key] = invoice
self.storage.put('invoices', self.invoices)
self.storage.write()
else:
raise Exception('Unsupported invoice type')
self.invoices[key] = invoice
self.storage.put('invoices', self.invoices)
self.storage.write()
def clear_invoices(self):
self.invoices = {}
@ -560,29 +560,26 @@ class Abstract_Wallet(AddressSynchronizer):
def get_invoices(self):
out = [self.get_invoice(key) for key in self.invoices.keys()]
out = [x for x in out if x and x.get('status') != PR_PAID]
if self.lnworker:
out += self.lnworker.get_invoices()
out.sort(key=operator.itemgetter('time'))
return out
def check_if_expired(self, item):
if item['status'] == PR_UNPAID and 'exp' in item and item['time'] + item['exp'] < time.time():
item['status'] = PR_EXPIRED
def get_invoice(self, key):
if key in self.invoices:
item = copy.copy(self.invoices[key])
request_type = item.get('type')
if request_type is None:
# todo: convert old bip70 invoices
return
# add status
if item.get('txid'):
status = PR_PAID
elif 'exp' in item and item['time'] + item['exp'] < time.time():
status = PR_EXPIRED
else:
status = PR_UNPAID
item['status'] = status
return item
if self.lnworker:
return self.lnworker.get_request(key)
if key not in self.invoices:
return
item = copy.copy(self.invoices[key])
request_type = item.get('type')
if request_type == PR_TYPE_ONCHAIN:
item['status'] = PR_PAID if item.get('txid') is not None else PR_UNPAID
elif request_type == PR_TYPE_LN:
item['status'] = self.lnworker.get_invoice_status(bfh(item['rhash']))
else:
return
self.check_if_expired(item)
return item
@profiler
def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True):
@ -1319,19 +1316,6 @@ class Abstract_Wallet(AddressSynchronizer):
return True, conf
return False, None
def get_payment_request(self, addr):
r = self.receive_requests.get(addr)
if not r:
return
out = copy.copy(r)
out['type'] = PR_TYPE_ONCHAIN
out['URI'] = self.get_request_URI(addr)
status, conf = self.get_request_status(addr)
out['status'] = status
if conf is not None:
out['confirmations'] = conf
return out
def get_request_URI(self, addr):
req = self.receive_requests[addr]
message = self.labels.get(addr, '')
@ -1349,11 +1333,10 @@ class Abstract_Wallet(AddressSynchronizer):
uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
return str(uri)
def get_request_status(self, key):
r = self.receive_requests.get(key)
def get_request_status(self, address):
r = self.receive_requests.get(address)
if r is None:
return PR_UNKNOWN
address = r['address']
amount = r.get('amount', 0) or 0
timestamp = r.get('time', 0)
if timestamp and type(timestamp) != int:
@ -1372,14 +1355,23 @@ class Abstract_Wallet(AddressSynchronizer):
return status, conf
def get_request(self, key):
if key in self.receive_requests:
req = self.get_payment_request(key)
elif self.lnworker:
req = self.lnworker.get_request(key)
else:
req = None
req = self.receive_requests.get(key)
if not req:
return
req = copy.copy(req)
if req['type'] == PR_TYPE_ONCHAIN:
addr = req['address']
req['URI'] = self.get_request_URI(addr)
status, conf = self.get_request_status(addr)
req['status'] = status
if conf is not None:
req['confirmations'] = conf
elif req['type'] == PR_TYPE_LN:
req['status'] = self.lnworker.get_invoice_status(bfh(key))
else:
return
self.check_if_expired(req)
# add URL if we are running a payserver
if self.config.get('payserver_port'):
host = self.config.get('payserver_host', 'localhost')
port = self.config.get('payserver_port')
@ -1405,8 +1397,16 @@ class Abstract_Wallet(AddressSynchronizer):
from .bitcoin import TYPE_ADDRESS
timestamp = int(time.time())
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id, 'outputs': [(TYPE_ADDRESS, addr, amount)]}
return r
return {
'type': PR_TYPE_ONCHAIN,
'time':timestamp,
'amount':amount,
'exp':expiration,
'address':addr,
'memo':message,
'id':_id,
'outputs': [(TYPE_ADDRESS, addr, amount)]
}
def sign_payment_request(self, key, alias, alias_addr, password):
req = self.receive_requests.get(key)
@ -1419,17 +1419,23 @@ class Abstract_Wallet(AddressSynchronizer):
self.storage.put('payment_requests', self.receive_requests)
def add_payment_request(self, req):
addr = req['address']
if not bitcoin.is_address(addr):
raise Exception(_('Invalid Bitcoin address.'))
if not self.is_mine(addr):
raise Exception(_('Address not in wallet.'))
if req['type'] == PR_TYPE_ONCHAIN:
addr = req['address']
if not bitcoin.is_address(addr):
raise Exception(_('Invalid Bitcoin address.'))
if not self.is_mine(addr):
raise Exception(_('Address not in wallet.'))
key = addr
message = req['memo']
elif req['type'] == PR_TYPE_LN:
key = req['rhash']
message = req['message']
else:
raise Exception('Unknown request type')
amount = req.get('amount')
message = req.get('memo')
self.receive_requests[addr] = req
self.receive_requests[key] = req
self.storage.put('payment_requests', self.receive_requests)
self.set_label(addr, message) # should be a default label
self.set_label(key, message) # should be a default label
return req
def delete_request(self, key):
@ -1457,8 +1463,6 @@ class Abstract_Wallet(AddressSynchronizer):
def get_sorted_requests(self):
""" sorted by timestamp """
out = [self.get_request(x) for x in self.receive_requests.keys()]
if self.lnworker:
out += self.lnworker.get_requests()
out.sort(key=operator.itemgetter('time'))
return out