mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
parent
599797c966
commit
d5f368c584
19 changed files with 260 additions and 135 deletions
|
@ -990,23 +990,14 @@ class Commands:
|
||||||
return chan.funding_outpoint.to_str()
|
return chan.funding_outpoint.to_str()
|
||||||
|
|
||||||
@command('')
|
@command('')
|
||||||
async def decode_invoice(self, invoice):
|
async def decode_invoice(self, invoice: str):
|
||||||
from .lnaddr import lndecode
|
invoice = LNInvoice.from_bech32(invoice)
|
||||||
lnaddr = lndecode(invoice)
|
return invoice.to_debug_json()
|
||||||
return {
|
|
||||||
'pubkey': lnaddr.pubkey.serialize().hex(),
|
|
||||||
'amount_BTC': lnaddr.amount,
|
|
||||||
'rhash': lnaddr.paymenthash.hex(),
|
|
||||||
'description': lnaddr.get_description(),
|
|
||||||
'exp': lnaddr.get_expiry(),
|
|
||||||
'time': lnaddr.date,
|
|
||||||
#'tags': str(lnaddr.tags),
|
|
||||||
}
|
|
||||||
|
|
||||||
@command('wn')
|
@command('wn')
|
||||||
async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
|
async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
|
||||||
lnworker = wallet.lnworker
|
lnworker = wallet.lnworker
|
||||||
lnaddr = lnworker._check_invoice(invoice, None)
|
lnaddr = lnworker._check_invoice(invoice)
|
||||||
payment_hash = lnaddr.paymenthash
|
payment_hash = lnaddr.paymenthash
|
||||||
wallet.save_invoice(LNInvoice.from_bech32(invoice))
|
wallet.save_invoice(LNInvoice.from_bech32(invoice))
|
||||||
success, log = await lnworker._pay(invoice, attempts=attempts)
|
success, log = await lnworker._pay(invoice, attempts=attempts)
|
||||||
|
@ -1026,7 +1017,6 @@ class Commands:
|
||||||
async def list_channels(self, wallet: Abstract_Wallet = None):
|
async def list_channels(self, wallet: Abstract_Wallet = None):
|
||||||
# we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels
|
# we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels
|
||||||
from .lnutil import LOCAL, REMOTE, format_short_channel_id
|
from .lnutil import LOCAL, REMOTE, format_short_channel_id
|
||||||
encoder = util.MyEncoder()
|
|
||||||
l = list(wallet.lnworker.channels.items())
|
l = list(wallet.lnworker.channels.items())
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -44,7 +44,7 @@ Builder.load_string('''
|
||||||
RefLabel:
|
RefLabel:
|
||||||
data: root.description or _('No description')
|
data: root.description or _('No description')
|
||||||
TopLabel:
|
TopLabel:
|
||||||
text: _('Amount') + ': ' + app.format_amount_and_units(root.amount)
|
text: _('Amount') + ': ' + app.format_amount_and_units(root.amount_sat)
|
||||||
TopLabel:
|
TopLabel:
|
||||||
text: _('Status') + ': ' + root.status_str
|
text: _('Status') + ': ' + root.status_str
|
||||||
color: root.status_color
|
color: root.status_color
|
||||||
|
@ -93,9 +93,9 @@ class InvoiceDialog(Factory.Popup):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.key = key
|
self.key = key
|
||||||
invoice = self.app.wallet.get_invoice(key)
|
invoice = self.app.wallet.get_invoice(key)
|
||||||
self.amount = invoice.amount
|
self.amount_sat = invoice.get_amount_sat()
|
||||||
self.description = invoice.message
|
self.description = invoice.message
|
||||||
self.is_lightning = invoice.type == PR_TYPE_LN
|
self.is_lightning = invoice.is_lightning()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
|
self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ class InvoiceDialog(Factory.Popup):
|
||||||
self.status_color = pr_color[self.status]
|
self.status_color = pr_color[self.status]
|
||||||
self.can_pay = self.status in [PR_UNPAID, PR_FAILED]
|
self.can_pay = self.status in [PR_UNPAID, PR_FAILED]
|
||||||
if self.can_pay and self.is_lightning and self.app.wallet.lnworker:
|
if self.can_pay and self.is_lightning and self.app.wallet.lnworker:
|
||||||
if self.amount and self.amount > self.app.wallet.lnworker.num_sats_can_send():
|
if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_send():
|
||||||
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently send with your channels')
|
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently send with your channels')
|
||||||
|
|
||||||
def on_dismiss(self):
|
def on_dismiss(self):
|
||||||
|
|
|
@ -118,7 +118,7 @@ class LightningOpenChannelDialog(Factory.Popup):
|
||||||
fee = self.app.electrum_config.fee_per_kb()
|
fee = self.app.electrum_config.fee_per_kb()
|
||||||
if not fee:
|
if not fee:
|
||||||
fee = config.FEERATE_FALLBACK_STATIC_FEE
|
fee = config.FEERATE_FALLBACK_STATIC_FEE
|
||||||
self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2)
|
self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2) # FIXME magic number?!
|
||||||
self.pubkey = bh2u(self.lnaddr.pubkey.serialize())
|
self.pubkey = bh2u(self.lnaddr.pubkey.serialize())
|
||||||
if self.msg:
|
if self.msg:
|
||||||
self.app.show_info(self.msg)
|
self.app.show_info(self.msg)
|
||||||
|
|
|
@ -44,7 +44,7 @@ Builder.load_string('''
|
||||||
TopLabel:
|
TopLabel:
|
||||||
text: _('Description') + ': ' + root.description or _('None')
|
text: _('Description') + ': ' + root.description or _('None')
|
||||||
TopLabel:
|
TopLabel:
|
||||||
text: _('Amount') + ': ' + app.format_amount_and_units(root.amount)
|
text: _('Amount') + ': ' + app.format_amount_and_units(root.amount_sat)
|
||||||
TopLabel:
|
TopLabel:
|
||||||
text: (_('Address') if not root.is_lightning else _('Payment hash')) + ': '
|
text: (_('Address') if not root.is_lightning else _('Payment hash')) + ': '
|
||||||
RefLabel:
|
RefLabel:
|
||||||
|
@ -93,7 +93,7 @@ class RequestDialog(Factory.Popup):
|
||||||
r = self.app.wallet.get_request(key)
|
r = self.app.wallet.get_request(key)
|
||||||
self.is_lightning = r.is_lightning()
|
self.is_lightning = r.is_lightning()
|
||||||
self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
|
self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
|
||||||
self.amount = r.amount or 0
|
self.amount_sat = r.get_amount_sat() or 0
|
||||||
self.description = r.message
|
self.description = r.message
|
||||||
self.update_status()
|
self.update_status()
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ class RequestDialog(Factory.Popup):
|
||||||
self.status_str = req.get_status_str(self.status)
|
self.status_str = req.get_status_str(self.status)
|
||||||
self.status_color = pr_color[self.status]
|
self.status_color = pr_color[self.status]
|
||||||
if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker:
|
if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker:
|
||||||
if self.amount and self.amount > self.app.wallet.lnworker.num_sats_can_receive():
|
if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive():
|
||||||
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels')
|
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels')
|
||||||
|
|
||||||
def on_dismiss(self):
|
def on_dismiss(self):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from decimal import Decimal
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import traceback, sys
|
import traceback, sys
|
||||||
from typing import TYPE_CHECKING, List, Optional
|
from typing import TYPE_CHECKING, List, Optional, Dict, Any
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.cache import Cache
|
from kivy.cache import Cache
|
||||||
|
@ -26,7 +26,7 @@ from kivy.logger import Logger
|
||||||
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
|
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
|
||||||
from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING,
|
from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING,
|
||||||
PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
|
PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
|
||||||
LNInvoice, pr_expiration_values)
|
LNInvoice, pr_expiration_values, Invoice, OnchainInvoice)
|
||||||
from electrum import bitcoin, constants
|
from electrum import bitcoin, constants
|
||||||
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
|
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
|
||||||
from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
|
from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
|
||||||
|
@ -224,17 +224,19 @@ class SendScreen(CScreen):
|
||||||
def show_item(self, obj):
|
def show_item(self, obj):
|
||||||
self.app.show_invoice(obj.is_lightning, obj.key)
|
self.app.show_invoice(obj.is_lightning, obj.key)
|
||||||
|
|
||||||
def get_card(self, item):
|
def get_card(self, item: Invoice):
|
||||||
status = self.app.wallet.get_invoice_status(item)
|
status = self.app.wallet.get_invoice_status(item)
|
||||||
status_str = item.get_status_str(status)
|
status_str = item.get_status_str(status)
|
||||||
is_lightning = item.type == PR_TYPE_LN
|
is_lightning = item.type == PR_TYPE_LN
|
||||||
if is_lightning:
|
if is_lightning:
|
||||||
|
assert isinstance(item, LNInvoice)
|
||||||
key = item.rhash
|
key = item.rhash
|
||||||
log = self.app.wallet.lnworker.logs.get(key)
|
log = self.app.wallet.lnworker.logs.get(key)
|
||||||
if status == PR_INFLIGHT and log:
|
if status == PR_INFLIGHT and log:
|
||||||
status_str += '... (%d)'%len(log)
|
status_str += '... (%d)'%len(log)
|
||||||
is_bip70 = False
|
is_bip70 = False
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(item, OnchainInvoice)
|
||||||
key = item.id
|
key = item.id
|
||||||
is_bip70 = bool(item.bip70)
|
is_bip70 = bool(item.bip70)
|
||||||
return {
|
return {
|
||||||
|
@ -245,7 +247,7 @@ class SendScreen(CScreen):
|
||||||
'status_str': status_str,
|
'status_str': status_str,
|
||||||
'key': key,
|
'key': key,
|
||||||
'memo': item.message,
|
'memo': item.message,
|
||||||
'amount': self.app.format_amount_and_units(item.amount or 0),
|
'amount': self.app.format_amount_and_units(item.get_amount_sat() or 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
def do_clear(self):
|
def do_clear(self):
|
||||||
|
@ -345,16 +347,18 @@ class SendScreen(CScreen):
|
||||||
else:
|
else:
|
||||||
do_pay(False)
|
do_pay(False)
|
||||||
|
|
||||||
def _do_pay_lightning(self, invoice):
|
def _do_pay_lightning(self, invoice: LNInvoice) -> None:
|
||||||
attempts = 10
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self.app.wallet.lnworker.pay,
|
target=self.app.wallet.lnworker.pay,
|
||||||
args=(invoice.invoice, invoice.amount),
|
args=(invoice.invoice,),
|
||||||
kwargs={'attempts':10}).start()
|
kwargs={
|
||||||
|
'attempts': 10,
|
||||||
|
},
|
||||||
|
).start()
|
||||||
|
|
||||||
def _do_pay_onchain(self, invoice, rbf):
|
def _do_pay_onchain(self, invoice: OnchainInvoice, rbf: bool) -> None:
|
||||||
# make unsigned transaction
|
# make unsigned transaction
|
||||||
outputs = invoice.outputs # type: List[PartialTxOutput]
|
outputs = invoice.outputs
|
||||||
coins = self.app.wallet.get_spendable_coins(None)
|
coins = self.app.wallet.get_spendable_coins(None)
|
||||||
try:
|
try:
|
||||||
tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
|
tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
|
||||||
|
@ -482,15 +486,17 @@ class ReceiveScreen(CScreen):
|
||||||
self.update()
|
self.update()
|
||||||
self.app.show_request(lightning, key)
|
self.app.show_request(lightning, key)
|
||||||
|
|
||||||
def get_card(self, req):
|
def get_card(self, req: Invoice) -> Dict[str, Any]:
|
||||||
is_lightning = req.is_lightning()
|
is_lightning = req.is_lightning()
|
||||||
if not is_lightning:
|
if not is_lightning:
|
||||||
|
assert isinstance(req, OnchainInvoice)
|
||||||
address = req.get_address()
|
address = req.get_address()
|
||||||
key = address
|
key = address
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(req, LNInvoice)
|
||||||
key = req.rhash
|
key = req.rhash
|
||||||
address = req.invoice
|
address = req.invoice
|
||||||
amount = req.amount
|
amount = req.get_amount_sat()
|
||||||
description = req.message
|
description = req.message
|
||||||
status = self.app.wallet.get_request_status(key)
|
status = self.app.wallet.get_request_status(key)
|
||||||
status_str = req.get_status_str(status)
|
status_str = req.get_status_str(status)
|
||||||
|
|
|
@ -92,6 +92,7 @@ class BTCAmountEdit(AmountEdit):
|
||||||
return decimal_point_to_base_unit_name(self.decimal_point())
|
return decimal_point_to_base_unit_name(self.decimal_point())
|
||||||
|
|
||||||
def get_amount(self):
|
def get_amount(self):
|
||||||
|
# returns amt in satoshis
|
||||||
try:
|
try:
|
||||||
x = Decimal(str(self.text()))
|
x = Decimal(str(self.text()))
|
||||||
except:
|
except:
|
||||||
|
@ -106,11 +107,11 @@ class BTCAmountEdit(AmountEdit):
|
||||||
amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point())
|
amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point())
|
||||||
return Decimal(amount) if not self.is_int else int(amount)
|
return Decimal(amount) if not self.is_int else int(amount)
|
||||||
|
|
||||||
def setAmount(self, amount):
|
def setAmount(self, amount_sat):
|
||||||
if amount is None:
|
if amount_sat is None:
|
||||||
self.setText(" ") # Space forces repaint in case units changed
|
self.setText(" ") # Space forces repaint in case units changed
|
||||||
else:
|
else:
|
||||||
self.setText(format_satoshis_plain(amount, decimal_point=self.decimal_point()))
|
self.setText(format_satoshis_plain(amount_sat, decimal_point=self.decimal_point()))
|
||||||
|
|
||||||
|
|
||||||
class FeerateEdit(BTCAmountEdit):
|
class FeerateEdit(BTCAmountEdit):
|
||||||
|
|
|
@ -110,7 +110,7 @@ class InvoiceList(MyTreeView):
|
||||||
status = self.parent.wallet.get_invoice_status(item)
|
status = self.parent.wallet.get_invoice_status(item)
|
||||||
status_str = item.get_status_str(status)
|
status_str = item.get_status_str(status)
|
||||||
message = item.message
|
message = item.message
|
||||||
amount = item.amount
|
amount = item.get_amount_sat()
|
||||||
timestamp = item.time or 0
|
timestamp = item.time or 0
|
||||||
date_str = format_time(timestamp) if timestamp else _('Unknown')
|
date_str = format_time(timestamp) if timestamp else _('Unknown')
|
||||||
amount_str = self.parent.format_amount(amount, whitespaces=True)
|
amount_str = self.parent.format_amount(amount, whitespaces=True)
|
||||||
|
|
|
@ -61,7 +61,7 @@ from electrum.util import (format_time,
|
||||||
get_new_wallet_name, send_exception_to_crash_reporter,
|
get_new_wallet_name, send_exception_to_crash_reporter,
|
||||||
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
|
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
|
||||||
NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
|
NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
|
||||||
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING
|
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice
|
||||||
from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice
|
from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice
|
||||||
from electrum.transaction import (Transaction, PartialTxInput,
|
from electrum.transaction import (Transaction, PartialTxInput,
|
||||||
PartialTransaction, PartialTxOutput)
|
PartialTransaction, PartialTxOutput)
|
||||||
|
@ -159,6 +159,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
show_privkeys_signal = pyqtSignal()
|
show_privkeys_signal = pyqtSignal()
|
||||||
show_error_signal = pyqtSignal(str)
|
show_error_signal = pyqtSignal(str)
|
||||||
|
|
||||||
|
payment_request: Optional[paymentrequest.PaymentRequest]
|
||||||
|
|
||||||
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
|
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
|
||||||
QMainWindow.__init__(self)
|
QMainWindow.__init__(self)
|
||||||
|
|
||||||
|
@ -877,9 +879,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
self.notify_transactions()
|
self.notify_transactions()
|
||||||
|
|
||||||
def format_amount(self, x, is_diff=False, whitespaces=False):
|
def format_amount(self, x, is_diff=False, whitespaces=False):
|
||||||
|
# x is in sats
|
||||||
return self.config.format_amount(x, is_diff=is_diff, whitespaces=whitespaces)
|
return self.config.format_amount(x, is_diff=is_diff, whitespaces=whitespaces)
|
||||||
|
|
||||||
def format_amount_and_units(self, amount):
|
def format_amount_and_units(self, amount):
|
||||||
|
# amount is in sats
|
||||||
text = self.config.format_amount_and_units(amount)
|
text = self.config.format_amount_and_units(amount)
|
||||||
x = self.fx.format_amount_and_units(amount) if self.fx else None
|
x = self.fx.format_amount_and_units(amount) if self.fx else None
|
||||||
if text and x:
|
if text and x:
|
||||||
|
@ -1481,13 +1485,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
|
|
||||||
return False # no errors
|
return False # no errors
|
||||||
|
|
||||||
def pay_lightning_invoice(self, invoice: str, amount_sat: int):
|
def pay_lightning_invoice(self, invoice: str, *, amount_msat: Optional[int]):
|
||||||
|
if amount_msat is None:
|
||||||
|
raise Exception("missing amount for LN invoice")
|
||||||
|
amount_sat = Decimal(amount_msat) / 1000
|
||||||
|
# FIXME this is currently lying to user as we truncate to satoshis
|
||||||
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat))
|
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat))
|
||||||
if not self.question(msg):
|
if not self.question(msg):
|
||||||
return
|
return
|
||||||
attempts = LN_NUM_PAYMENT_ATTEMPTS
|
attempts = LN_NUM_PAYMENT_ATTEMPTS
|
||||||
def task():
|
def task():
|
||||||
self.wallet.lnworker.pay(invoice, amount_sat, attempts=attempts)
|
self.wallet.lnworker.pay(invoice, amount_msat=amount_msat, attempts=attempts)
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
self.wallet.thread.add(task)
|
self.wallet.thread.add(task)
|
||||||
self.invoice_list.update()
|
self.invoice_list.update()
|
||||||
|
@ -1524,10 +1532,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
self.show_error(_('Lightning is disabled'))
|
self.show_error(_('Lightning is disabled'))
|
||||||
return
|
return
|
||||||
invoice = LNInvoice.from_bech32(invoice_str)
|
invoice = LNInvoice.from_bech32(invoice_str)
|
||||||
if invoice.amount is None:
|
if invoice.get_amount_msat() is None:
|
||||||
amount = self.amount_e.get_amount()
|
amount_sat = self.amount_e.get_amount()
|
||||||
if amount:
|
if amount_sat:
|
||||||
invoice.amount = amount
|
invoice.amount_msat = int(amount_sat * 1000)
|
||||||
else:
|
else:
|
||||||
self.show_error(_('No amount'))
|
self.show_error(_('No amount'))
|
||||||
return
|
return
|
||||||
|
@ -1566,10 +1574,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
outputs += invoice.outputs
|
outputs += invoice.outputs
|
||||||
self.pay_onchain_dialog(self.get_coins(), outputs)
|
self.pay_onchain_dialog(self.get_coins(), outputs)
|
||||||
|
|
||||||
def do_pay_invoice(self, invoice):
|
def do_pay_invoice(self, invoice: 'Invoice'):
|
||||||
if invoice.type == PR_TYPE_LN:
|
if invoice.type == PR_TYPE_LN:
|
||||||
self.pay_lightning_invoice(invoice.invoice, invoice.amount)
|
assert isinstance(invoice, LNInvoice)
|
||||||
|
self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat())
|
||||||
elif invoice.type == PR_TYPE_ONCHAIN:
|
elif invoice.type == PR_TYPE_ONCHAIN:
|
||||||
|
assert isinstance(invoice, OnchainInvoice)
|
||||||
self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
|
self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
|
||||||
else:
|
else:
|
||||||
raise Exception('unknown invoice type')
|
raise Exception('unknown invoice type')
|
||||||
|
@ -1838,8 +1848,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
self.payto_e.setFrozen(True)
|
self.payto_e.setFrozen(True)
|
||||||
self.payto_e.setText(pubkey)
|
self.payto_e.setText(pubkey)
|
||||||
self.message_e.setText(description)
|
self.message_e.setText(description)
|
||||||
if lnaddr.amount is not None:
|
if lnaddr.get_amount_sat() is not None:
|
||||||
self.amount_e.setAmount(lnaddr.amount * COIN)
|
self.amount_e.setAmount(lnaddr.get_amount_sat())
|
||||||
#self.amount_e.textEdited.emit("")
|
#self.amount_e.textEdited.emit("")
|
||||||
self.set_onchain(False)
|
self.set_onchain(False)
|
||||||
|
|
||||||
|
@ -1980,7 +1990,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
self.update_completions()
|
self.update_completions()
|
||||||
|
|
||||||
def show_onchain_invoice(self, invoice: OnchainInvoice):
|
def show_onchain_invoice(self, invoice: OnchainInvoice):
|
||||||
amount_str = self.format_amount(invoice.amount) + ' ' + self.base_unit()
|
amount_str = self.format_amount(invoice.amount_sat) + ' ' + self.base_unit()
|
||||||
d = WindowModalDialog(self, _("Onchain Invoice"))
|
d = WindowModalDialog(self, _("Onchain Invoice"))
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
|
@ -2030,7 +2040,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
grid.addWidget(QLabel(_("Node ID") + ':'), 0, 0)
|
grid.addWidget(QLabel(_("Node ID") + ':'), 0, 0)
|
||||||
grid.addWidget(QLabel(lnaddr.pubkey.serialize().hex()), 0, 1)
|
grid.addWidget(QLabel(lnaddr.pubkey.serialize().hex()), 0, 1)
|
||||||
grid.addWidget(QLabel(_("Amount") + ':'), 1, 0)
|
grid.addWidget(QLabel(_("Amount") + ':'), 1, 0)
|
||||||
amount_str = self.format_amount(invoice.amount) + ' ' + self.base_unit()
|
amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit()
|
||||||
grid.addWidget(QLabel(amount_str), 1, 1)
|
grid.addWidget(QLabel(amount_str), 1, 1)
|
||||||
grid.addWidget(QLabel(_("Description") + ':'), 2, 0)
|
grid.addWidget(QLabel(_("Description") + ':'), 2, 0)
|
||||||
grid.addWidget(QLabel(invoice.message), 2, 1)
|
grid.addWidget(QLabel(invoice.message), 2, 1)
|
||||||
|
|
|
@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import format_time
|
from electrum.util import format_time
|
||||||
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, LNInvoice, OnchainInvoice
|
||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
|
|
||||||
from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
|
from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
|
||||||
|
@ -130,21 +130,28 @@ class RequestList(MyTreeView):
|
||||||
self.std_model.clear()
|
self.std_model.clear()
|
||||||
self.update_headers(self.__class__.headers)
|
self.update_headers(self.__class__.headers)
|
||||||
for req in self.wallet.get_sorted_requests():
|
for req in self.wallet.get_sorted_requests():
|
||||||
key = req.rhash if req.is_lightning() else req.id
|
if req.is_lightning():
|
||||||
|
assert isinstance(req, LNInvoice)
|
||||||
|
key = req.rhash
|
||||||
|
else:
|
||||||
|
assert isinstance(req, OnchainInvoice)
|
||||||
|
key = req.id
|
||||||
status = self.parent.wallet.get_request_status(key)
|
status = self.parent.wallet.get_request_status(key)
|
||||||
status_str = req.get_status_str(status)
|
status_str = req.get_status_str(status)
|
||||||
request_type = req.type
|
request_type = req.type
|
||||||
timestamp = req.time
|
timestamp = req.time
|
||||||
amount = req.amount
|
amount = req.get_amount_sat()
|
||||||
message = req.message
|
message = req.message
|
||||||
date = format_time(timestamp)
|
date = format_time(timestamp)
|
||||||
amount_str = self.parent.format_amount(amount) if amount else ""
|
amount_str = self.parent.format_amount(amount) if amount else ""
|
||||||
labels = [date, message, amount_str, status_str]
|
labels = [date, message, amount_str, status_str]
|
||||||
if req.is_lightning():
|
if req.is_lightning():
|
||||||
|
assert isinstance(req, LNInvoice)
|
||||||
key = req.rhash
|
key = req.rhash
|
||||||
icon = read_QIcon("lightning.png")
|
icon = read_QIcon("lightning.png")
|
||||||
tooltip = 'lightning request'
|
tooltip = 'lightning request'
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(req, OnchainInvoice)
|
||||||
key = req.get_address()
|
key = req.get_address()
|
||||||
icon = read_QIcon("bitcoin.png")
|
icon = read_QIcon("bitcoin.png")
|
||||||
tooltip = 'onchain request'
|
tooltip = 'onchain request'
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import attr
|
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
from .json_db import StoredObject
|
from .json_db import StoredObject
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
from .util import age
|
from .util import age
|
||||||
from .lnaddr import lndecode
|
from .lnaddr import lndecode, LnAddr
|
||||||
from . import constants
|
from . import constants
|
||||||
from .bitcoin import COIN
|
from .bitcoin import COIN
|
||||||
from .transaction import PartialTxOutput
|
from .transaction import PartialTxOutput
|
||||||
|
@ -67,6 +69,7 @@ def _decode_outputs(outputs) -> List[PartialTxOutput]:
|
||||||
ret.append(output)
|
ret.append(output)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
# hack: BOLT-11 is not really clear on what an expiry of 0 means.
|
# hack: BOLT-11 is not really clear on what an expiry of 0 means.
|
||||||
# It probably interprets it as 0 seconds, so already expired...
|
# It probably interprets it as 0 seconds, so already expired...
|
||||||
# Our higher level invoices code however uses 0 for "never".
|
# Our higher level invoices code however uses 0 for "never".
|
||||||
|
@ -75,11 +78,11 @@ LN_EXPIRY_NEVER = 100 * 365 * 24 * 60 * 60 # 100 years
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class Invoice(StoredObject):
|
class Invoice(StoredObject):
|
||||||
type = attr.ib(type=int)
|
type = attr.ib(type=int, kw_only=True)
|
||||||
message = attr.ib(type=str)
|
|
||||||
amount = attr.ib(type=int)
|
message: str
|
||||||
exp = attr.ib(type=int)
|
exp: int
|
||||||
time = attr.ib(type=int)
|
time: int
|
||||||
|
|
||||||
def is_lightning(self):
|
def is_lightning(self):
|
||||||
return self.type == PR_TYPE_LN
|
return self.type == PR_TYPE_LN
|
||||||
|
@ -94,22 +97,42 @@ class Invoice(StoredObject):
|
||||||
status_str = _('Pending')
|
status_str = _('Pending')
|
||||||
return status_str
|
return status_str
|
||||||
|
|
||||||
|
def get_amount_sat(self) -> Union[int, Decimal, str, None]:
|
||||||
|
"""Returns a decimal satoshi amount, or '!' or None."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, x: dict) -> 'Invoice':
|
||||||
|
# note: these raise if x has extra fields
|
||||||
|
if x.get('type') == PR_TYPE_LN:
|
||||||
|
return LNInvoice(**x)
|
||||||
|
else:
|
||||||
|
return OnchainInvoice(**x)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class OnchainInvoice(Invoice):
|
class OnchainInvoice(Invoice):
|
||||||
id = attr.ib(type=str)
|
message = attr.ib(type=str, kw_only=True)
|
||||||
outputs = attr.ib(type=list, converter=_decode_outputs)
|
amount_sat = attr.ib(kw_only=True) # type: Union[None, int, str] # in satoshis. can be '!'
|
||||||
bip70 = attr.ib(type=str) # may be None
|
exp = attr.ib(type=int, kw_only=True)
|
||||||
requestor = attr.ib(type=str) # may be None
|
time = attr.ib(type=int, kw_only=True)
|
||||||
|
id = attr.ib(type=str, kw_only=True)
|
||||||
|
outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: List[PartialTxOutput]
|
||||||
|
bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str]
|
||||||
|
requestor = attr.ib(type=str, kw_only=True) # type: Optional[str]
|
||||||
|
|
||||||
def get_address(self) -> str:
|
def get_address(self) -> str:
|
||||||
assert len(self.outputs) == 1
|
assert len(self.outputs) == 1
|
||||||
return self.outputs[0].address
|
return self.outputs[0].address
|
||||||
|
|
||||||
|
def get_amount_sat(self) -> Union[int, str, None]:
|
||||||
|
return self.amount_sat
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bip70_payreq(cls, pr: 'PaymentRequest') -> 'OnchainInvoice':
|
def from_bip70_payreq(cls, pr: 'PaymentRequest') -> 'OnchainInvoice':
|
||||||
return OnchainInvoice(
|
return OnchainInvoice(
|
||||||
type=PR_TYPE_ONCHAIN,
|
type=PR_TYPE_ONCHAIN,
|
||||||
amount=pr.get_amount(),
|
amount_sat=pr.get_amount(),
|
||||||
outputs=pr.get_outputs(),
|
outputs=pr.get_outputs(),
|
||||||
message=pr.get_memo(),
|
message=pr.get_memo(),
|
||||||
id=pr.get_id(),
|
id=pr.get_id(),
|
||||||
|
@ -121,26 +144,63 @@ class OnchainInvoice(Invoice):
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class LNInvoice(Invoice):
|
class LNInvoice(Invoice):
|
||||||
rhash = attr.ib(type=str)
|
|
||||||
invoice = attr.ib(type=str)
|
invoice = attr.ib(type=str)
|
||||||
|
amount_msat = attr.ib(kw_only=True) # type: Optional[int] # needed for zero amt invoices
|
||||||
|
|
||||||
|
__lnaddr = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _lnaddr(self) -> LnAddr:
|
||||||
|
if self.__lnaddr is None:
|
||||||
|
self.__lnaddr = lndecode(self.invoice)
|
||||||
|
return self.__lnaddr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rhash(self) -> str:
|
||||||
|
return self._lnaddr.paymenthash.hex()
|
||||||
|
|
||||||
|
def get_amount_msat(self) -> Optional[int]:
|
||||||
|
amount_btc = self._lnaddr.amount
|
||||||
|
amount = int(amount_btc * COIN * 1000) if amount_btc else None
|
||||||
|
return amount or self.amount_msat
|
||||||
|
|
||||||
|
def get_amount_sat(self) -> Union[Decimal, None]:
|
||||||
|
amount_msat = self.get_amount_msat()
|
||||||
|
if amount_msat is None:
|
||||||
|
return None
|
||||||
|
return Decimal(amount_msat) / 1000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exp(self) -> int:
|
||||||
|
return self._lnaddr.get_expiry()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time(self) -> int:
|
||||||
|
return self._lnaddr.date
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self) -> str:
|
||||||
|
return self._lnaddr.get_description()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bech32(klass, invoice: str) -> 'LNInvoice':
|
def from_bech32(cls, invoice: str) -> 'LNInvoice':
|
||||||
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
|
amount_msat = lndecode(invoice).get_amount_msat()
|
||||||
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
|
|
||||||
return LNInvoice(
|
return LNInvoice(
|
||||||
type = PR_TYPE_LN,
|
type=PR_TYPE_LN,
|
||||||
amount = amount,
|
invoice=invoice,
|
||||||
message = lnaddr.get_description(),
|
amount_msat=amount_msat,
|
||||||
time = lnaddr.date,
|
|
||||||
exp = lnaddr.get_expiry(),
|
|
||||||
rhash = lnaddr.paymenthash.hex(),
|
|
||||||
invoice = invoice,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_debug_json(self) -> Dict[str, Any]:
|
||||||
|
d = self.to_json()
|
||||||
|
d.update({
|
||||||
|
'pubkey': self._lnaddr.pubkey.serialize().hex(),
|
||||||
|
'amount_BTC': self._lnaddr.amount,
|
||||||
|
'rhash': self._lnaddr.paymenthash.hex(),
|
||||||
|
'description': self._lnaddr.get_description(),
|
||||||
|
'exp': self._lnaddr.get_expiry(),
|
||||||
|
'time': self._lnaddr.date,
|
||||||
|
# 'tags': str(lnaddr.tags),
|
||||||
|
})
|
||||||
|
return d
|
||||||
|
|
||||||
def invoice_from_json(x: dict) -> Invoice:
|
|
||||||
if x.get('type') == PR_TYPE_LN:
|
|
||||||
return LNInvoice(**x)
|
|
||||||
else:
|
|
||||||
return OnchainInvoice(**x)
|
|
||||||
|
|
|
@ -60,6 +60,9 @@ class StoredObject:
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
d = dict(vars(self))
|
d = dict(vars(self))
|
||||||
d.pop('db', None)
|
d.pop('db', None)
|
||||||
|
# don't expose/store private stuff
|
||||||
|
d = {k: v for k, v in d.items()
|
||||||
|
if not k.startswith('_')}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import time
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import bitstring
|
import bitstring
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ def shorten_amount(amount):
|
||||||
break
|
break
|
||||||
return str(amount) + unit
|
return str(amount) + unit
|
||||||
|
|
||||||
def unshorten_amount(amount):
|
def unshorten_amount(amount) -> Decimal:
|
||||||
""" Given a shortened amount, convert it into a decimal
|
""" Given a shortened amount, convert it into a decimal
|
||||||
"""
|
"""
|
||||||
# BOLT #11:
|
# BOLT #11:
|
||||||
|
@ -271,12 +272,20 @@ class LnAddr(object):
|
||||||
self.signature = None
|
self.signature = None
|
||||||
self.pubkey = None
|
self.pubkey = None
|
||||||
self.currency = constants.net.SEGWIT_HRP if currency is None else currency
|
self.currency = constants.net.SEGWIT_HRP if currency is None else currency
|
||||||
self.amount = amount # in bitcoins
|
self.amount = amount # type: Optional[Decimal] # in bitcoins
|
||||||
self._min_final_cltv_expiry = 9
|
self._min_final_cltv_expiry = 9
|
||||||
|
|
||||||
def get_amount_sat(self):
|
def get_amount_sat(self) -> Optional[Decimal]:
|
||||||
|
# note that this has msat resolution potentially
|
||||||
|
if self.amount is None:
|
||||||
|
return None
|
||||||
return self.amount * COIN
|
return self.amount * COIN
|
||||||
|
|
||||||
|
def get_amount_msat(self) -> Optional[int]:
|
||||||
|
if self.amount is None:
|
||||||
|
return None
|
||||||
|
return int(self.amount * COIN * 1000)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
|
||||||
hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
|
hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,
|
||||||
|
|
|
@ -136,7 +136,7 @@ class RevokeAndAck(NamedTuple):
|
||||||
class RemoteCtnTooFarInFuture(Exception): pass
|
class RemoteCtnTooFarInFuture(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
def htlcsum(htlcs):
|
def htlcsum(htlcs: Iterable[UpdateAddHtlc]):
|
||||||
return sum([x.amount_msat for x in htlcs])
|
return sum([x.amount_msat for x in htlcs])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ FALLBACK_NODE_LIST_MAINNET = [
|
||||||
|
|
||||||
class PaymentInfo(NamedTuple):
|
class PaymentInfo(NamedTuple):
|
||||||
payment_hash: bytes
|
payment_hash: bytes
|
||||||
amount: int # in satoshis
|
amount: Optional[int] # in satoshis # TODO make it msat and rename to amount_msat
|
||||||
direction: int
|
direction: int
|
||||||
status: int
|
status: int
|
||||||
|
|
||||||
|
@ -491,7 +491,7 @@ class LNWallet(LNWorker):
|
||||||
self.lnwatcher = None
|
self.lnwatcher = None
|
||||||
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||||
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
||||||
self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
|
self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid # FIXME amt should be msat
|
||||||
self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
|
self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
|
||||||
self.sweep_address = wallet.get_new_sweep_address_for_channel() # TODO possible address-reuse
|
self.sweep_address = wallet.get_new_sweep_address_for_channel() # TODO possible address-reuse
|
||||||
self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # (not persisted)
|
self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # (not persisted)
|
||||||
|
@ -597,7 +597,7 @@ class LNWallet(LNWorker):
|
||||||
out[k] += v
|
out[k] += v
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def get_payment_value(self, info, plist):
|
def get_payment_value(self, info: Optional['PaymentInfo'], plist):
|
||||||
amount_msat = 0
|
amount_msat = 0
|
||||||
fee_msat = None
|
fee_msat = None
|
||||||
for chan_id, htlc, _direction in plist:
|
for chan_id, htlc, _direction in plist:
|
||||||
|
@ -832,11 +832,11 @@ class LNWallet(LNWorker):
|
||||||
raise Exception(_("open_channel timed out"))
|
raise Exception(_("open_channel timed out"))
|
||||||
return chan, funding_tx
|
return chan, funding_tx
|
||||||
|
|
||||||
def pay(self, invoice: str, amount_sat: int = None, *, attempts: int = 1) -> Tuple[bool, List[PaymentAttemptLog]]:
|
def pay(self, invoice: str, *, amount_msat: int = None, attempts: int = 1) -> Tuple[bool, List[PaymentAttemptLog]]:
|
||||||
"""
|
"""
|
||||||
Can be called from other threads
|
Can be called from other threads
|
||||||
"""
|
"""
|
||||||
coro = self._pay(invoice, amount_sat, attempts=attempts)
|
coro = self._pay(invoice, amount_msat=amount_msat, attempts=attempts)
|
||||||
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
|
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
|
||||||
return fut.result()
|
return fut.result()
|
||||||
|
|
||||||
|
@ -846,10 +846,15 @@ class LNWallet(LNWorker):
|
||||||
return chan
|
return chan
|
||||||
|
|
||||||
@log_exceptions
|
@log_exceptions
|
||||||
async def _pay(self, invoice: str, amount_sat: int = None, *,
|
async def _pay(
|
||||||
attempts: int = 1,
|
self,
|
||||||
full_path: LNPaymentPath = None) -> Tuple[bool, List[PaymentAttemptLog]]:
|
invoice: str,
|
||||||
lnaddr = self._check_invoice(invoice, amount_sat)
|
*,
|
||||||
|
amount_msat: int = None,
|
||||||
|
attempts: int = 1,
|
||||||
|
full_path: LNPaymentPath = None,
|
||||||
|
) -> Tuple[bool, List[PaymentAttemptLog]]:
|
||||||
|
lnaddr = self._check_invoice(invoice, amount_msat=amount_msat)
|
||||||
payment_hash = lnaddr.paymenthash
|
payment_hash = lnaddr.paymenthash
|
||||||
key = payment_hash.hex()
|
key = payment_hash.hex()
|
||||||
amount = int(lnaddr.amount * COIN)
|
amount = int(lnaddr.amount * COIN)
|
||||||
|
@ -901,7 +906,7 @@ class LNWallet(LNWorker):
|
||||||
await peer.initialized
|
await peer.initialized
|
||||||
htlc = peer.pay(route=route,
|
htlc = peer.pay(route=route,
|
||||||
chan=chan,
|
chan=chan,
|
||||||
amount_msat=int(lnaddr.amount * COIN * 1000),
|
amount_msat=lnaddr.get_amount_msat(),
|
||||||
payment_hash=lnaddr.paymenthash,
|
payment_hash=lnaddr.paymenthash,
|
||||||
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
|
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
|
||||||
payment_secret=lnaddr.payment_secret)
|
payment_secret=lnaddr.payment_secret)
|
||||||
|
@ -993,12 +998,12 @@ class LNWallet(LNWorker):
|
||||||
return blacklist
|
return blacklist
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_invoice(invoice: str, amount_sat: int = None) -> LnAddr:
|
def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr:
|
||||||
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
|
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
|
||||||
if addr.is_expired():
|
if addr.is_expired():
|
||||||
raise InvoiceError(_("This invoice has expired"))
|
raise InvoiceError(_("This invoice has expired"))
|
||||||
if amount_sat:
|
if amount_msat:
|
||||||
addr.amount = Decimal(amount_sat) / COIN
|
addr.amount = Decimal(amount_msat) / COIN / 1000
|
||||||
if addr.amount is None:
|
if addr.amount is None:
|
||||||
raise InvoiceError(_("Missing amount"))
|
raise InvoiceError(_("Missing amount"))
|
||||||
if addr.get_min_final_cltv_expiry() > lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
|
if addr.get_min_final_cltv_expiry() > lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
|
||||||
|
@ -1010,7 +1015,7 @@ class LNWallet(LNWorker):
|
||||||
@profiler
|
@profiler
|
||||||
def _create_route_from_invoice(self, decoded_invoice: 'LnAddr',
|
def _create_route_from_invoice(self, decoded_invoice: 'LnAddr',
|
||||||
*, full_path: LNPaymentPath = None) -> LNPaymentRoute:
|
*, full_path: LNPaymentPath = None) -> LNPaymentRoute:
|
||||||
amount_msat = int(decoded_invoice.amount * COIN * 1000)
|
amount_msat = decoded_invoice.get_amount_msat()
|
||||||
invoice_pubkey = decoded_invoice.pubkey.serialize()
|
invoice_pubkey = decoded_invoice.pubkey.serialize()
|
||||||
# use 'r' field from invoice
|
# use 'r' field from invoice
|
||||||
route = None # type: Optional[LNPaymentRoute]
|
route = None # type: Optional[LNPaymentRoute]
|
||||||
|
@ -1310,11 +1315,11 @@ class LNWallet(LNWorker):
|
||||||
return Decimal(max(chan.available_to_spend(REMOTE) if chan.is_open() else 0
|
return Decimal(max(chan.available_to_spend(REMOTE) if chan.is_open() else 0
|
||||||
for chan in self.channels.values()))/1000 if self.channels else 0
|
for chan in self.channels.values()))/1000 if self.channels else 0
|
||||||
|
|
||||||
def can_pay_invoice(self, invoice):
|
def can_pay_invoice(self, invoice: LNInvoice) -> bool:
|
||||||
return invoice.amount <= self.num_sats_can_send()
|
return invoice.get_amount_sat() <= self.num_sats_can_send()
|
||||||
|
|
||||||
def can_receive_invoice(self, invoice):
|
def can_receive_invoice(self, invoice: LNInvoice) -> bool:
|
||||||
return invoice.amount <= self.num_sats_can_receive()
|
return invoice.get_amount_sat() <= self.num_sats_can_receive()
|
||||||
|
|
||||||
async def close_channel(self, chan_id):
|
async def close_channel(self, chan_id):
|
||||||
chan = self._channels[chan_id]
|
chan = self._channels[chan_id]
|
||||||
|
|
|
@ -326,7 +326,7 @@ def make_unsigned_request(req: 'OnchainInvoice'):
|
||||||
time = 0
|
time = 0
|
||||||
if exp and type(exp) != int:
|
if exp and type(exp) != int:
|
||||||
exp = 0
|
exp = 0
|
||||||
amount = req.amount
|
amount = req.amount_sat
|
||||||
if amount is None:
|
if amount is None:
|
||||||
amount = 0
|
amount = 0
|
||||||
memo = req.message
|
memo = req.message
|
||||||
|
|
|
@ -586,7 +586,7 @@ class TestPeer(ElectrumTestCase):
|
||||||
route = w1._create_route_from_invoice(decoded_invoice=lnaddr)
|
route = w1._create_route_from_invoice(decoded_invoice=lnaddr)
|
||||||
htlc = p1.pay(route=route,
|
htlc = p1.pay(route=route,
|
||||||
chan=alice_channel,
|
chan=alice_channel,
|
||||||
amount_msat=int(lnaddr.amount * COIN * 1000),
|
amount_msat=lnaddr.get_amount_msat(),
|
||||||
payment_hash=lnaddr.paymenthash,
|
payment_hash=lnaddr.paymenthash,
|
||||||
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
|
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
|
||||||
payment_secret=lnaddr.payment_secret)
|
payment_secret=lnaddr.payment_secret)
|
||||||
|
|
|
@ -793,6 +793,7 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional
|
||||||
class InvalidBitcoinURI(Exception): pass
|
class InvalidBitcoinURI(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
|
# TODO rename to parse_bip21_uri or similar
|
||||||
def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict:
|
def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict:
|
||||||
"""Raises InvalidBitcoinURI on malformed URI."""
|
"""Raises InvalidBitcoinURI on malformed URI."""
|
||||||
from . import bitcoin
|
from . import bitcoin
|
||||||
|
|
|
@ -70,7 +70,7 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
|
||||||
from .plugin import run_hook
|
from .plugin import run_hook
|
||||||
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
|
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
|
||||||
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
|
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
|
||||||
from .invoices import Invoice, OnchainInvoice, invoice_from_json, LNInvoice
|
from .invoices import Invoice, OnchainInvoice, LNInvoice
|
||||||
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN
|
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||||
from .contacts import Contacts
|
from .contacts import Contacts
|
||||||
from .interface import NetworkException
|
from .interface import NetworkException
|
||||||
|
@ -693,7 +693,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
amount = sum(x.value for x in outputs)
|
amount = sum(x.value for x in outputs)
|
||||||
invoice = OnchainInvoice(
|
invoice = OnchainInvoice(
|
||||||
type=PR_TYPE_ONCHAIN,
|
type=PR_TYPE_ONCHAIN,
|
||||||
amount=amount,
|
amount_sat=amount,
|
||||||
outputs=outputs,
|
outputs=outputs,
|
||||||
message=message,
|
message=message,
|
||||||
id=bh2u(sha256(repr(outputs))[0:16]),
|
id=bh2u(sha256(repr(outputs))[0:16]),
|
||||||
|
@ -738,7 +738,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
def import_requests(self, path):
|
def import_requests(self, path):
|
||||||
data = read_json_file(path)
|
data = read_json_file(path)
|
||||||
for x in data:
|
for x in data:
|
||||||
req = invoice_from_json(x)
|
req = Invoice.from_json(x)
|
||||||
self.add_payment_request(req)
|
self.add_payment_request(req)
|
||||||
|
|
||||||
def export_requests(self, path):
|
def export_requests(self, path):
|
||||||
|
@ -747,7 +747,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
def import_invoices(self, path):
|
def import_invoices(self, path):
|
||||||
data = read_json_file(path)
|
data = read_json_file(path)
|
||||||
for x in data:
|
for x in data:
|
||||||
invoice = invoice_from_json(x)
|
invoice = Invoice.from_json(x)
|
||||||
self.save_invoice(invoice)
|
self.save_invoice(invoice)
|
||||||
|
|
||||||
def export_invoices(self, path):
|
def export_invoices(self, path):
|
||||||
|
@ -1630,7 +1630,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
def get_request_URI(self, req: OnchainInvoice) -> str:
|
def get_request_URI(self, req: OnchainInvoice) -> str:
|
||||||
addr = req.get_address()
|
addr = req.get_address()
|
||||||
message = self.labels.get(addr, '')
|
message = self.labels.get(addr, '')
|
||||||
amount = req.amount
|
amount = req.amount_sat
|
||||||
extra_query_params = {}
|
extra_query_params = {}
|
||||||
if req.time:
|
if req.time:
|
||||||
extra_query_params['time'] = str(int(req.time))
|
extra_query_params['time'] = str(int(req.time))
|
||||||
|
@ -1663,9 +1663,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
if r is None:
|
if r is None:
|
||||||
return PR_UNKNOWN
|
return PR_UNKNOWN
|
||||||
if r.is_lightning():
|
if r.is_lightning():
|
||||||
|
assert isinstance(r, LNInvoice)
|
||||||
status = self.lnworker.get_payment_status(bfh(r.rhash)) if self.lnworker else PR_UNKNOWN
|
status = self.lnworker.get_payment_status(bfh(r.rhash)) if self.lnworker else PR_UNKNOWN
|
||||||
else:
|
else:
|
||||||
paid, conf = self.get_payment_status(r.get_address(), r.amount)
|
assert isinstance(r, OnchainInvoice)
|
||||||
|
paid, conf = self.get_payment_status(r.get_address(), r.amount_sat)
|
||||||
status = PR_PAID if paid else PR_UNPAID
|
status = PR_PAID if paid else PR_UNPAID
|
||||||
return self.check_expired_status(r, status)
|
return self.check_expired_status(r, status)
|
||||||
|
|
||||||
|
@ -1689,8 +1691,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
is_lightning = x.is_lightning()
|
is_lightning = x.is_lightning()
|
||||||
d = {
|
d = {
|
||||||
'is_lightning': is_lightning,
|
'is_lightning': is_lightning,
|
||||||
'amount': x.amount,
|
'amount': x.get_amount_sat(),
|
||||||
'amount_BTC': format_satoshis(x.amount),
|
'amount_BTC': format_satoshis(x.get_amount_sat()),
|
||||||
'message': x.message,
|
'message': x.message,
|
||||||
'timestamp': x.time,
|
'timestamp': x.time,
|
||||||
'expiration': x.exp,
|
'expiration': x.exp,
|
||||||
|
@ -1698,13 +1700,16 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
'status_str': status_str,
|
'status_str': status_str,
|
||||||
}
|
}
|
||||||
if is_lightning:
|
if is_lightning:
|
||||||
|
assert isinstance(x, LNInvoice)
|
||||||
d['rhash'] = x.rhash
|
d['rhash'] = x.rhash
|
||||||
d['invoice'] = x.invoice
|
d['invoice'] = x.invoice
|
||||||
|
d['amount_msat'] = x.get_amount_msat()
|
||||||
if self.lnworker and status == PR_UNPAID:
|
if self.lnworker and status == PR_UNPAID:
|
||||||
d['can_receive'] = self.lnworker.can_receive_invoice(x)
|
d['can_receive'] = self.lnworker.can_receive_invoice(x)
|
||||||
else:
|
else:
|
||||||
|
assert isinstance(x, OnchainInvoice)
|
||||||
addr = x.get_address()
|
addr = x.get_address()
|
||||||
paid, conf = self.get_payment_status(addr, x.amount)
|
paid, conf = self.get_payment_status(addr, x.amount_sat)
|
||||||
d['address'] = addr
|
d['address'] = addr
|
||||||
d['URI'] = self.get_request_URI(x)
|
d['URI'] = self.get_request_URI(x)
|
||||||
if conf is not None:
|
if conf is not None:
|
||||||
|
@ -1728,8 +1733,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
is_lightning = x.is_lightning()
|
is_lightning = x.is_lightning()
|
||||||
d = {
|
d = {
|
||||||
'is_lightning': is_lightning,
|
'is_lightning': is_lightning,
|
||||||
'amount': x.amount,
|
'amount': x.get_amount_sat(),
|
||||||
'amount_BTC': format_satoshis(x.amount),
|
'amount_BTC': format_satoshis(x.get_amount_sat()),
|
||||||
'message': x.message,
|
'message': x.message,
|
||||||
'timestamp': x.time,
|
'timestamp': x.time,
|
||||||
'expiration': x.exp,
|
'expiration': x.exp,
|
||||||
|
@ -1739,6 +1744,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
if is_lightning:
|
if is_lightning:
|
||||||
assert isinstance(x, LNInvoice)
|
assert isinstance(x, LNInvoice)
|
||||||
d['invoice'] = x.invoice
|
d['invoice'] = x.invoice
|
||||||
|
d['amount_msat'] = x.get_amount_msat()
|
||||||
if self.lnworker and status == PR_UNPAID:
|
if self.lnworker and status == PR_UNPAID:
|
||||||
d['can_pay'] = self.lnworker.can_pay_invoice(x)
|
d['can_pay'] = self.lnworker.can_pay_invoice(x)
|
||||||
else:
|
else:
|
||||||
|
@ -1757,20 +1763,23 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
status = self.get_request_status(addr)
|
status = self.get_request_status(addr)
|
||||||
util.trigger_callback('request_status', addr, status)
|
util.trigger_callback('request_status', addr, status)
|
||||||
|
|
||||||
def make_payment_request(self, address, amount, message, expiration):
|
def make_payment_request(self, address, amount_sat, message, expiration):
|
||||||
amount = amount or 0
|
# TODO maybe merge with wallet.create_invoice()...
|
||||||
|
# note that they use incompatible "id"
|
||||||
|
amount_sat = amount_sat or 0
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
_id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
|
_id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
|
||||||
return OnchainInvoice(
|
return OnchainInvoice(
|
||||||
type = PR_TYPE_ONCHAIN,
|
type=PR_TYPE_ONCHAIN,
|
||||||
outputs = [(TYPE_ADDRESS, address, amount)],
|
outputs=[(TYPE_ADDRESS, address, amount_sat)],
|
||||||
message = message,
|
message=message,
|
||||||
time = timestamp,
|
time=timestamp,
|
||||||
amount = amount,
|
amount_sat=amount_sat,
|
||||||
exp = expiration,
|
exp=expiration,
|
||||||
id = _id,
|
id=_id,
|
||||||
bip70 = None,
|
bip70=None,
|
||||||
requestor = None)
|
requestor=None,
|
||||||
|
)
|
||||||
|
|
||||||
def sign_payment_request(self, key, alias, alias_addr, password): # FIXME this is broken
|
def sign_payment_request(self, key, alias, alias_addr, password): # FIXME this is broken
|
||||||
req = self.receive_requests.get(key)
|
req = self.receive_requests.get(key)
|
||||||
|
@ -1820,7 +1829,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||||
self.receive_requests.pop(addr)
|
self.receive_requests.pop(addr)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_sorted_requests(self):
|
def get_sorted_requests(self) -> List[Invoice]:
|
||||||
""" sorted by timestamp """
|
""" sorted by timestamp """
|
||||||
out = [self.get_request(x) for x in self.receive_requests.keys()]
|
out = [self.get_request(x) for x in self.receive_requests.keys()]
|
||||||
out = [x for x in out if x is not None]
|
out = [x for x in out if x is not None]
|
||||||
|
|
|
@ -33,7 +33,7 @@ import binascii
|
||||||
|
|
||||||
from . import util, bitcoin
|
from . import util, bitcoin
|
||||||
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
|
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
|
||||||
from .invoices import PR_TYPE_ONCHAIN, invoice_from_json
|
from .invoices import PR_TYPE_ONCHAIN, Invoice
|
||||||
from .keystore import bip44_derivation
|
from .keystore import bip44_derivation
|
||||||
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
|
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
|
||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
|
@ -52,7 +52,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||||
FINAL_SEED_VERSION = 29 # electrum >= 2.7 will set this to prevent
|
FINAL_SEED_VERSION = 30 # electrum >= 2.7 will set this to prevent
|
||||||
# old versions from overwriting new format
|
# old versions from overwriting new format
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,6 +177,7 @@ class WalletDB(JsonDB):
|
||||||
self._convert_version_27()
|
self._convert_version_27()
|
||||||
self._convert_version_28()
|
self._convert_version_28()
|
||||||
self._convert_version_29()
|
self._convert_version_29()
|
||||||
|
self._convert_version_30()
|
||||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||||
|
|
||||||
self._after_upgrade_tasks()
|
self._after_upgrade_tasks()
|
||||||
|
@ -643,6 +644,29 @@ class WalletDB(JsonDB):
|
||||||
d[key] = item
|
d[key] = item
|
||||||
self.data['seed_version'] = 29
|
self.data['seed_version'] = 29
|
||||||
|
|
||||||
|
def _convert_version_30(self):
|
||||||
|
if not self._is_upgrade_method_needed(29, 29):
|
||||||
|
return
|
||||||
|
|
||||||
|
from .invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||||
|
requests = self.data.get('payment_requests', {})
|
||||||
|
invoices = self.data.get('invoices', {})
|
||||||
|
for d in [invoices, requests]:
|
||||||
|
for key, item in list(d.items()):
|
||||||
|
_type = item['type']
|
||||||
|
if _type == PR_TYPE_ONCHAIN:
|
||||||
|
item['amount_sat'] = item.pop('amount')
|
||||||
|
elif _type == PR_TYPE_LN:
|
||||||
|
amount_sat = item.pop('amount')
|
||||||
|
item['amount_msat'] = 1000 * amount_sat if amount_sat is not None else None
|
||||||
|
item.pop('exp')
|
||||||
|
item.pop('message')
|
||||||
|
item.pop('rhash')
|
||||||
|
item.pop('time')
|
||||||
|
else:
|
||||||
|
raise Exception(f"unknown invoice type: {_type}")
|
||||||
|
self.data['seed_version'] = 30
|
||||||
|
|
||||||
def _convert_imported(self):
|
def _convert_imported(self):
|
||||||
if not self._is_upgrade_method_needed(0, 13):
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
return
|
return
|
||||||
|
@ -1127,9 +1151,9 @@ class WalletDB(JsonDB):
|
||||||
# note: for performance, "deserialize=False" so that we will deserialize these on-demand
|
# note: for performance, "deserialize=False" so that we will deserialize these on-demand
|
||||||
v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items())
|
v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items())
|
||||||
if key == 'invoices':
|
if key == 'invoices':
|
||||||
v = dict((k, invoice_from_json(x)) for k, x in v.items())
|
v = dict((k, Invoice.from_json(x)) for k, x in v.items())
|
||||||
if key == 'payment_requests':
|
if key == 'payment_requests':
|
||||||
v = dict((k, invoice_from_json(x)) for k, x in v.items())
|
v = dict((k, Invoice.from_json(x)) for k, x in v.items())
|
||||||
elif key == 'adds':
|
elif key == 'adds':
|
||||||
v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
|
v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
|
||||||
elif key == 'fee_updates':
|
elif key == 'fee_updates':
|
||||||
|
|
Loading…
Add table
Reference in a new issue