2020-05-31 12:49:49 +02:00
|
|
|
import time
|
2020-06-22 22:37:58 +02:00
|
|
|
from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
import attr
|
2020-05-31 12:49:49 +02:00
|
|
|
|
|
|
|
|
from .json_db import StoredObject
|
|
|
|
|
from .i18n import _
|
|
|
|
|
from .util import age
|
2020-06-22 22:37:58 +02:00
|
|
|
from .lnaddr import lndecode, LnAddr
|
2020-05-31 12:49:49 +02:00
|
|
|
from . import constants
|
|
|
|
|
from .bitcoin import COIN
|
|
|
|
|
from .transaction import PartialTxOutput
|
|
|
|
|
|
2020-06-03 21:00:03 +02:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from .paymentrequest import PaymentRequest
|
|
|
|
|
|
2020-05-31 12:49:49 +02:00
|
|
|
# convention: 'invoices' = outgoing , 'request' = incoming
|
|
|
|
|
|
|
|
|
|
# types of payment requests
|
|
|
|
|
PR_TYPE_ONCHAIN = 0
|
|
|
|
|
PR_TYPE_LN = 2
|
|
|
|
|
|
|
|
|
|
# status of payment requests
|
|
|
|
|
PR_UNPAID = 0
|
|
|
|
|
PR_EXPIRED = 1
|
|
|
|
|
PR_UNKNOWN = 2 # sent but not propagated
|
|
|
|
|
PR_PAID = 3 # send and propagated
|
|
|
|
|
PR_INFLIGHT = 4 # unconfirmed
|
|
|
|
|
PR_FAILED = 5
|
|
|
|
|
PR_ROUTING = 6
|
|
|
|
|
|
|
|
|
|
pr_color = {
|
|
|
|
|
PR_UNPAID: (.7, .7, .7, 1),
|
|
|
|
|
PR_PAID: (.2, .9, .2, 1),
|
|
|
|
|
PR_UNKNOWN: (.7, .7, .7, 1),
|
|
|
|
|
PR_EXPIRED: (.9, .2, .2, 1),
|
|
|
|
|
PR_INFLIGHT: (.9, .6, .3, 1),
|
|
|
|
|
PR_FAILED: (.9, .2, .2, 1),
|
|
|
|
|
PR_ROUTING: (.9, .6, .3, 1),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pr_tooltips = {
|
2020-11-13 19:18:34 +01:00
|
|
|
PR_UNPAID:_('Unpaid'),
|
2020-05-31 12:49:49 +02:00
|
|
|
PR_PAID:_('Paid'),
|
|
|
|
|
PR_UNKNOWN:_('Unknown'),
|
|
|
|
|
PR_EXPIRED:_('Expired'),
|
|
|
|
|
PR_INFLIGHT:_('In progress'),
|
|
|
|
|
PR_FAILED:_('Failed'),
|
|
|
|
|
PR_ROUTING: _('Computing route...'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60 # 1 day
|
|
|
|
|
pr_expiration_values = {
|
|
|
|
|
0: _('Never'),
|
|
|
|
|
10*60: _('10 minutes'),
|
|
|
|
|
60*60: _('1 hour'),
|
|
|
|
|
24*60*60: _('1 day'),
|
|
|
|
|
7*24*60*60: _('1 week'),
|
|
|
|
|
}
|
|
|
|
|
assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
|
|
|
|
|
|
2020-06-03 21:00:03 +02:00
|
|
|
|
|
|
|
|
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
|
2020-05-31 12:49:49 +02:00
|
|
|
|
2020-06-22 22:37:58 +02:00
|
|
|
|
2020-06-01 22:18:08 +02:00
|
|
|
# hack: BOLT-11 is not really clear on what an expiry of 0 means.
|
|
|
|
|
# It probably interprets it as 0 seconds, so already expired...
|
|
|
|
|
# Our higher level invoices code however uses 0 for "never".
|
|
|
|
|
# Hence set some high expiration here
|
|
|
|
|
LN_EXPIRY_NEVER = 100 * 365 * 24 * 60 * 60 # 100 years
|
|
|
|
|
|
2020-05-31 12:49:49 +02:00
|
|
|
@attr.s
|
|
|
|
|
class Invoice(StoredObject):
|
2020-06-22 22:37:58 +02:00
|
|
|
type = attr.ib(type=int, kw_only=True)
|
|
|
|
|
|
|
|
|
|
message: str
|
|
|
|
|
exp: int
|
|
|
|
|
time: int
|
2020-05-31 12:49:49 +02:00
|
|
|
|
|
|
|
|
def is_lightning(self):
|
|
|
|
|
return self.type == PR_TYPE_LN
|
|
|
|
|
|
|
|
|
|
def get_status_str(self, status):
|
|
|
|
|
status_str = pr_tooltips[status]
|
|
|
|
|
if status == PR_UNPAID:
|
2020-06-01 22:18:08 +02:00
|
|
|
if self.exp > 0 and self.exp != LN_EXPIRY_NEVER:
|
2020-05-31 12:49:49 +02:00
|
|
|
expiration = self.exp + self.time
|
|
|
|
|
status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
|
|
|
|
|
return status_str
|
|
|
|
|
|
2020-06-22 22:37:58 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2020-05-31 12:49:49 +02:00
|
|
|
@attr.s
|
|
|
|
|
class OnchainInvoice(Invoice):
|
2020-06-22 22:37:58 +02:00
|
|
|
message = attr.ib(type=str, kw_only=True)
|
2020-06-27 02:23:46 +02:00
|
|
|
amount_sat = attr.ib(kw_only=True) # type: Union[int, str] # in satoshis. can be '!'
|
|
|
|
|
exp = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int))
|
|
|
|
|
time = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int))
|
2020-06-22 22:37:58 +02:00
|
|
|
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]
|
wallet_db: impl convert_version_33: put 'height' field into invoices
The 'height' field was added in https://github.com/spesmilo/electrum/commit/cdfaaa260942b807f809c2c0414fb242a03e945a
At the time we thought we could just add it with a default value without a db upgrade;
however the issue is that if old code tries to open a new db, it will fail (due to unexpected new field).
Hence it is better to do an explicit conversion where old code *knows* it cannot open the new db.
E | gui.qt.ElectrumGui |
Traceback (most recent call last):
File "...\electrum\electrum\gui\qt\__init__.py", line 257, in start_new_window
wallet = self.daemon.load_wallet(path, None)
File "...\electrum\electrum\daemon.py", line 488, in load_wallet
db = WalletDB(storage.read(), manual_upgrades=manual_upgrades)
File "...\electrum\electrum\wallet_db.py", line 72, in __init__
self.load_data(raw)
File "...\electrum\electrum\wallet_db.py", line 103, in load_data
self._after_upgrade_tasks()
File "...\electrum\electrum\wallet_db.py", line 189, in _after_upgrade_tasks
self._load_transactions()
File "...\electrum\electrum\util.py", line 408, in <lambda>
return lambda *args, **kw_args: do_profile(args, kw_args)
File "...\electrum\electrum\util.py", line 404, in do_profile
o = func(*args, **kw_args)
File "...\electrum\electrum\wallet_db.py", line 1139, in _load_transactions
self.data = StoredDict(self.data, self, [])
File "...\electrum\electrum\json_db.py", line 79, in __init__
self.__setitem__(k, v)
File "...\electrum\electrum\json_db.py", line 44, in wrapper
return func(self, *args, **kwargs)
File "...\electrum\electrum\json_db.py", line 105, in __setitem__
v = self.db._convert_dict(self.path, key, v)
File "...\electrum\electrum\wallet_db.py", line 1182, in _convert_dict
v = dict((k, Invoice.from_json(x)) for k, x in v.items())
File "...\electrum\electrum\wallet_db.py", line 1182, in <genexpr>
v = dict((k, Invoice.from_json(x)) for k, x in v.items())
File "...\electrum\electrum\invoices.py", line 108, in from_json
return OnchainInvoice(**x)
TypeError: __init__() got an unexpected keyword argument 'height'
2020-12-17 15:17:08 +01:00
|
|
|
height = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int))
|
2020-05-31 12:49:49 +02:00
|
|
|
|
2020-06-03 21:00:03 +02:00
|
|
|
def get_address(self) -> str:
|
2020-11-26 09:08:20 +01:00
|
|
|
"""returns the first address, to be displayed in GUI"""
|
2020-05-31 12:49:49 +02:00
|
|
|
return self.outputs[0].address
|
|
|
|
|
|
2020-06-26 11:14:23 +02:00
|
|
|
def get_amount_sat(self) -> Union[int, str]:
|
|
|
|
|
return self.amount_sat or 0
|
2020-06-22 22:37:58 +02:00
|
|
|
|
2020-06-03 21:00:03 +02:00
|
|
|
@classmethod
|
2020-12-18 10:49:45 +01:00
|
|
|
def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'OnchainInvoice':
|
2020-06-03 21:00:03 +02:00
|
|
|
return OnchainInvoice(
|
|
|
|
|
type=PR_TYPE_ONCHAIN,
|
2020-06-22 22:37:58 +02:00
|
|
|
amount_sat=pr.get_amount(),
|
2020-06-03 21:00:03 +02:00
|
|
|
outputs=pr.get_outputs(),
|
|
|
|
|
message=pr.get_memo(),
|
|
|
|
|
id=pr.get_id(),
|
|
|
|
|
time=pr.get_time(),
|
|
|
|
|
exp=pr.get_expiration_date() - pr.get_time(),
|
2020-06-13 18:54:22 +02:00
|
|
|
bip70=pr.raw.hex(),
|
2020-06-03 21:00:03 +02:00
|
|
|
requestor=pr.get_requestor(),
|
2020-12-18 10:49:45 +01:00
|
|
|
height=height,
|
2020-06-03 21:00:03 +02:00
|
|
|
)
|
|
|
|
|
|
2020-05-31 12:49:49 +02:00
|
|
|
@attr.s
|
|
|
|
|
class LNInvoice(Invoice):
|
|
|
|
|
invoice = attr.ib(type=str)
|
2020-06-22 22:37:58 +02:00
|
|
|
amount_msat = attr.ib(kw_only=True) # type: Optional[int] # needed for zero amt invoices
|
|
|
|
|
|
|
|
|
|
__lnaddr = None
|
|
|
|
|
|
2020-09-11 19:56:04 +02:00
|
|
|
@invoice.validator
|
|
|
|
|
def check(self, attribute, value):
|
|
|
|
|
lndecode(value) # this checks the str can be decoded
|
|
|
|
|
|
2020-06-22 22:37:58 +02:00
|
|
|
@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()
|
2020-05-31 12:49:49 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
2020-06-22 22:37:58 +02:00
|
|
|
def from_bech32(cls, invoice: str) -> 'LNInvoice':
|
|
|
|
|
amount_msat = lndecode(invoice).get_amount_msat()
|
2020-05-31 12:49:49 +02:00
|
|
|
return LNInvoice(
|
2020-06-22 22:37:58 +02:00
|
|
|
type=PR_TYPE_LN,
|
|
|
|
|
invoice=invoice,
|
|
|
|
|
amount_msat=amount_msat,
|
2020-05-31 12:49:49 +02:00
|
|
|
)
|
|
|
|
|
|
2020-06-22 22:37:58 +02:00
|
|
|
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
|
2020-05-31 12:49:49 +02:00
|
|
|
|