@@ -310,7 +310,11 @@ class SendScreen(CScreen):
|
|||||||
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
|
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
|
||||||
return
|
return
|
||||||
outputs = [PartialTxOutput.from_address_and_value(address, amount)]
|
outputs = [PartialTxOutput.from_address_and_value(address, amount)]
|
||||||
return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
|
return self.app.wallet.create_invoice(
|
||||||
|
outputs=outputs,
|
||||||
|
message=message,
|
||||||
|
pr=self.payment_request,
|
||||||
|
URI=self.parsed_URI)
|
||||||
|
|
||||||
def do_save(self):
|
def do_save(self):
|
||||||
invoice = self.read_invoice()
|
invoice = self.read_invoice()
|
||||||
|
|||||||
@@ -1523,7 +1523,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
|
if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
|
||||||
return
|
return
|
||||||
message = self.message_e.text()
|
message = self.message_e.text()
|
||||||
return self.wallet.create_invoice(outputs, message, self.payment_request, self.payto_URI)
|
return self.wallet.create_invoice(
|
||||||
|
outputs=outputs,
|
||||||
|
message=message,
|
||||||
|
pr=self.payment_request,
|
||||||
|
URI=self.payto_URI)
|
||||||
|
|
||||||
def do_save_invoice(self):
|
def do_save_invoice(self):
|
||||||
invoice = self.read_invoice()
|
invoice = self.read_invoice()
|
||||||
@@ -1772,7 +1776,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
return
|
return
|
||||||
key = pr.get_id()
|
key = pr.get_id()
|
||||||
invoice = self.wallet.get_invoice(key)
|
invoice = self.wallet.get_invoice(key)
|
||||||
if invoice and self.wallet.get_invoice_status() == PR_PAID:
|
if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
|
||||||
self.show_message("invoice already paid")
|
self.show_message("invoice already paid")
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import attr
|
import attr
|
||||||
import time
|
import time
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from .json_db import StoredObject
|
from .json_db import StoredObject
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
@@ -9,6 +10,9 @@ from . import constants
|
|||||||
from .bitcoin import COIN
|
from .bitcoin import COIN
|
||||||
from .transaction import PartialTxOutput
|
from .transaction import PartialTxOutput
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .paymentrequest import PaymentRequest
|
||||||
|
|
||||||
# convention: 'invoices' = outgoing , 'request' = incoming
|
# convention: 'invoices' = outgoing , 'request' = incoming
|
||||||
|
|
||||||
# types of payment requests
|
# types of payment requests
|
||||||
@@ -54,7 +58,14 @@ pr_expiration_values = {
|
|||||||
}
|
}
|
||||||
assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
|
assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
|
||||||
|
|
||||||
outputs_decoder = lambda _list: [PartialTxOutput.from_legacy_tuple(*x) for x in _list]
|
|
||||||
|
def _decode_outputs(outputs) -> List[PartialTxOutput]:
|
||||||
|
ret = []
|
||||||
|
for output in outputs:
|
||||||
|
if not isinstance(output, PartialTxOutput):
|
||||||
|
output = PartialTxOutput.from_legacy_tuple(*output)
|
||||||
|
ret.append(output)
|
||||||
|
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...
|
||||||
@@ -86,21 +97,35 @@ class Invoice(StoredObject):
|
|||||||
@attr.s
|
@attr.s
|
||||||
class OnchainInvoice(Invoice):
|
class OnchainInvoice(Invoice):
|
||||||
id = attr.ib(type=str)
|
id = attr.ib(type=str)
|
||||||
outputs = attr.ib(type=list, converter=outputs_decoder)
|
outputs = attr.ib(type=list, converter=_decode_outputs)
|
||||||
bip70 = attr.ib(type=str) # may be None
|
bip70 = attr.ib(type=str) # may be None
|
||||||
requestor = attr.ib(type=str) # may be None
|
requestor = attr.ib(type=str) # may be None
|
||||||
|
|
||||||
def get_address(self):
|
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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bip70_payreq(cls, pr: 'PaymentRequest') -> 'OnchainInvoice':
|
||||||
|
return OnchainInvoice(
|
||||||
|
type=PR_TYPE_ONCHAIN,
|
||||||
|
amount=pr.get_amount(),
|
||||||
|
outputs=pr.get_outputs(),
|
||||||
|
message=pr.get_memo(),
|
||||||
|
id=pr.get_id(),
|
||||||
|
time=pr.get_time(),
|
||||||
|
exp=pr.get_expiration_date() - pr.get_time(),
|
||||||
|
bip70=pr.raw.hex() if pr else None,
|
||||||
|
requestor=pr.get_requestor(),
|
||||||
|
)
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class LNInvoice(Invoice):
|
class LNInvoice(Invoice):
|
||||||
rhash = attr.ib(type=str)
|
rhash = attr.ib(type=str)
|
||||||
invoice = attr.ib(type=str)
|
invoice = attr.ib(type=str)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bech32(klass, invoice: str):
|
def from_bech32(klass, invoice: str) -> 'LNInvoice':
|
||||||
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
|
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
|
||||||
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
|
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
|
||||||
return LNInvoice(
|
return LNInvoice(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import base64
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import traceback
|
import traceback
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
import smtplib
|
import smtplib
|
||||||
import imaplib
|
import imaplib
|
||||||
@@ -48,6 +49,8 @@ from electrum.plugin import BasePlugin, hook
|
|||||||
from electrum.paymentrequest import PaymentRequest
|
from electrum.paymentrequest import PaymentRequest
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.logging import Logger
|
from electrum.logging import Logger
|
||||||
|
from electrum.wallet import Abstract_Wallet
|
||||||
|
from electrum.invoices import OnchainInvoice
|
||||||
|
|
||||||
|
|
||||||
class Processor(threading.Thread, Logger):
|
class Processor(threading.Thread, Logger):
|
||||||
@@ -150,7 +153,7 @@ class Plugin(BasePlugin):
|
|||||||
self.processor.start()
|
self.processor.start()
|
||||||
self.obj = QEmailSignalObject()
|
self.obj = QEmailSignalObject()
|
||||||
self.obj.email_new_invoice_signal.connect(self.new_invoice)
|
self.obj.email_new_invoice_signal.connect(self.new_invoice)
|
||||||
self.wallets = set()
|
self.wallets = set() # type: Set[Abstract_Wallet]
|
||||||
|
|
||||||
def on_receive(self, pr_str):
|
def on_receive(self, pr_str):
|
||||||
self.logger.info('received payment request')
|
self.logger.info('received payment request')
|
||||||
@@ -166,8 +169,9 @@ class Plugin(BasePlugin):
|
|||||||
self.wallets -= {wallet}
|
self.wallets -= {wallet}
|
||||||
|
|
||||||
def new_invoice(self):
|
def new_invoice(self):
|
||||||
|
invoice = OnchainInvoice.from_bip70_payreq(self.pr)
|
||||||
for wallet in self.wallets:
|
for wallet in self.wallets:
|
||||||
wallet.invoices.add(self.pr)
|
wallet.save_invoice(invoice)
|
||||||
#main_window.invoice_list.update()
|
#main_window.invoice_list.update()
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
|
|||||||
@@ -68,7 +68,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
|
from .invoices import Invoice, OnchainInvoice, invoice_from_json, 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
|
||||||
@@ -248,7 +248,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
|||||||
self.frozen_coins = set(db.get('frozen_coins', [])) # set of txid:vout strings
|
self.frozen_coins = set(db.get('frozen_coins', [])) # set of txid:vout strings
|
||||||
self.fiat_value = db.get_dict('fiat_value')
|
self.fiat_value = db.get_dict('fiat_value')
|
||||||
self.receive_requests = db.get_dict('payment_requests')
|
self.receive_requests = db.get_dict('payment_requests')
|
||||||
self.invoices = db.get_dict('invoices')
|
self.invoices = db.get_dict('invoices') # type: Dict[str, Invoice]
|
||||||
self._reserved_addresses = set(db.get('reserved_addresses', []))
|
self._reserved_addresses = set(db.get('reserved_addresses', []))
|
||||||
|
|
||||||
self._prepare_onchain_invoice_paid_detection()
|
self._prepare_onchain_invoice_paid_detection()
|
||||||
@@ -656,43 +656,33 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
|||||||
'txpos_in_block': hist_item.tx_mined_status.txpos,
|
'txpos_in_block': hist_item.tx_mined_status.txpos,
|
||||||
}
|
}
|
||||||
|
|
||||||
def create_invoice(self, outputs: List[PartialTxOutput], message, pr, URI):
|
def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
|
||||||
|
if pr:
|
||||||
|
return OnchainInvoice.from_bip70_payreq(pr)
|
||||||
if '!' in (x.value for x in outputs):
|
if '!' in (x.value for x in outputs):
|
||||||
amount = '!'
|
amount = '!'
|
||||||
else:
|
else:
|
||||||
amount = sum(x.value for x in outputs)
|
amount = sum(x.value for x in outputs)
|
||||||
outputs = [x.to_legacy_tuple() for x in outputs]
|
invoice = OnchainInvoice(
|
||||||
if pr:
|
type=PR_TYPE_ONCHAIN,
|
||||||
invoice = OnchainInvoice(
|
amount=amount,
|
||||||
type = PR_TYPE_ONCHAIN,
|
outputs=outputs,
|
||||||
amount = amount,
|
message=message,
|
||||||
outputs = outputs,
|
id=bh2u(sha256(repr(outputs))[0:16]),
|
||||||
message = pr.get_memo(),
|
time=URI.get('time') if URI else int(time.time()),
|
||||||
id = pr.get_id(),
|
exp=URI.get('exp') if URI else 0,
|
||||||
time = pr.get_time(),
|
bip70=None,
|
||||||
exp = pr.get_expiration_date() - pr.get_time(),
|
requestor=None,
|
||||||
bip70 = pr.raw.hex() if pr else None,
|
)
|
||||||
requestor = pr.get_requestor(),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
invoice = OnchainInvoice(
|
|
||||||
type = PR_TYPE_ONCHAIN,
|
|
||||||
amount = amount,
|
|
||||||
outputs = outputs,
|
|
||||||
message = message,
|
|
||||||
id = bh2u(sha256(repr(outputs))[0:16]),
|
|
||||||
time = URI.get('time') if URI else int(time.time()),
|
|
||||||
exp = URI.get('exp') if URI else 0,
|
|
||||||
bip70 = None,
|
|
||||||
requestor = None,
|
|
||||||
)
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
def save_invoice(self, invoice: Invoice):
|
def save_invoice(self, invoice: Invoice) -> None:
|
||||||
invoice_type = invoice.type
|
invoice_type = invoice.type
|
||||||
if invoice_type == PR_TYPE_LN:
|
if invoice_type == PR_TYPE_LN:
|
||||||
|
assert isinstance(invoice, LNInvoice)
|
||||||
key = invoice.rhash
|
key = invoice.rhash
|
||||||
elif invoice_type == PR_TYPE_ONCHAIN:
|
elif invoice_type == PR_TYPE_ONCHAIN:
|
||||||
|
assert isinstance(invoice, OnchainInvoice)
|
||||||
key = invoice.id
|
key = invoice.id
|
||||||
if self.is_onchain_invoice_paid(invoice):
|
if self.is_onchain_invoice_paid(invoice):
|
||||||
self.logger.info("saving invoice... but it is already paid!")
|
self.logger.info("saving invoice... but it is already paid!")
|
||||||
@@ -729,12 +719,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
|||||||
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
|
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
|
||||||
for invoice_key, invoice in self.invoices.items():
|
for invoice_key, invoice in self.invoices.items():
|
||||||
if invoice.type == PR_TYPE_ONCHAIN:
|
if invoice.type == PR_TYPE_ONCHAIN:
|
||||||
|
assert isinstance(invoice, OnchainInvoice)
|
||||||
for txout in invoice.outputs:
|
for txout in invoice.outputs:
|
||||||
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
|
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
|
||||||
|
|
||||||
def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Sequence[str]]:
|
def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Sequence[str]]:
|
||||||
"""Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
|
"""Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
|
||||||
assert invoice.type == PR_TYPE_ONCHAIN
|
assert invoice.type == PR_TYPE_ONCHAIN
|
||||||
|
assert isinstance(invoice, OnchainInvoice)
|
||||||
invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats
|
invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats
|
||||||
for txo in invoice.outputs: # type: PartialTxOutput
|
for txo in invoice.outputs: # type: PartialTxOutput
|
||||||
invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
|
invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
|
||||||
@@ -763,9 +755,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
|||||||
for invoice_key in self._get_relevant_invoice_keys_for_tx(tx):
|
for invoice_key in self._get_relevant_invoice_keys_for_tx(tx):
|
||||||
invoice = self.invoices.get(invoice_key)
|
invoice = self.invoices.get(invoice_key)
|
||||||
if invoice is None: continue
|
if invoice is None: continue
|
||||||
assert invoice.get('type') == PR_TYPE_ONCHAIN
|
assert isinstance(invoice, OnchainInvoice)
|
||||||
if invoice['message']:
|
if invoice.message:
|
||||||
labels.append(invoice['message'])
|
labels.append(invoice.message)
|
||||||
if labels:
|
if labels:
|
||||||
self.set_label(tx_hash, "; ".join(labels))
|
self.set_label(tx_hash, "; ".join(labels))
|
||||||
return bool(labels)
|
return bool(labels)
|
||||||
@@ -1610,7 +1602,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
|||||||
status = PR_EXPIRED
|
status = PR_EXPIRED
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def get_invoice_status(self, invoice):
|
def get_invoice_status(self, invoice: Invoice):
|
||||||
if invoice.is_lightning():
|
if invoice.is_lightning():
|
||||||
status = self.lnworker.get_invoice_status(invoice)
|
status = self.lnworker.get_invoice_status(invoice)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user