Browse Source

payment_identfier: refactor qml and tests

master
Sander van Grieken 3 years ago
parent
commit
fc141c0182
  1. 195
      electrum/gui/qml/qeinvoice.py
  2. 20
      electrum/payment_identifier.py
  3. 24
      electrum/tests/test_util.py

195
electrum/gui/qml/qeinvoice.py

@ -5,21 +5,19 @@ from urllib.parse import urlparse
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS, QTimer from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS, QTimer
from electrum import bitcoin
from electrum import lnutil
from electrum.i18n import _ from electrum.i18n import _
from electrum.invoices import Invoice from electrum.logging import get_logger
from electrum.invoices import (PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, from electrum.invoices import (Invoice, PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT,
PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER) PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER)
from electrum.lnaddr import LnInvoiceException from electrum.lnaddr import LnInvoiceException
from electrum.logging import get_logger
from electrum.transaction import PartialTxOutput from electrum.transaction import PartialTxOutput
from electrum.util import (parse_URI, InvalidBitcoinURI, InvoiceError, from electrum.util import InvoiceError, get_asyncio_loop
maybe_extract_lightning_payment_identifier, get_asyncio_loop) from electrum.lnutil import format_short_channel_id, IncompatibleOrInsaneFeatures
from electrum.lnutil import format_short_channel_id
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl
from electrum.bitcoin import COIN from electrum.bitcoin import COIN
from electrum.paymentrequest import PaymentRequest from electrum.paymentrequest import PaymentRequest
from electrum.payment_identifier import (parse_bip21_URI, InvalidBitcoinURI, maybe_extract_lightning_payment_identifier,
PaymentIdentifier, PaymentIdentifierState)
from .qetypes import QEAmount from .qetypes import QEAmount
from .qewallet import QEWallet from .qewallet import QEWallet
@ -249,7 +247,8 @@ class QEInvoice(QObject, QtEventListener):
} }
def name_for_node_id(self, node_id): def name_for_node_id(self, node_id):
return self._wallet.wallet.lnworker.get_node_alias(node_id) or node_id.hex() lnworker = self._wallet.wallet.lnworker
return (lnworker.get_node_alias(node_id) if lnworker else None) or node_id.hex()
def set_effective_invoice(self, invoice: Invoice): def set_effective_invoice(self, invoice: Invoice):
self._effectiveInvoice = invoice self._effectiveInvoice = invoice
@ -406,13 +405,11 @@ class QEInvoiceParser(QEInvoice):
lnurlRetrieved = pyqtSignal() lnurlRetrieved = pyqtSignal()
lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) lnurlError = pyqtSignal([str,str], arguments=['code', 'message'])
_bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['pr'])
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._recipient = '' self._recipient = ''
self._bip70PrResolvedSignal.connect(self._bip70_payment_request_resolved) self._pi = None
self.clear() self.clear()
@ -493,79 +490,54 @@ class QEInvoiceParser(QEInvoice):
self.setInvoiceType(QEInvoice.Type.Invalid) self.setInvoiceType(QEInvoice.Type.Invalid)
return return
maybe_lightning_invoice = recipient self._pi = PaymentIdentifier(self._wallet.wallet, recipient)
if not self._pi.is_valid() or self._pi.type not in ['spk', 'bip21', 'bip70', 'bolt11', 'lnurl']:
self.validationError.emit('unknown', _('Unknown invoice'))
return
try: self._update_from_payment_identifier()
bip21 = parse_URI(recipient, lambda pr: self._bip70PrResolvedSignal.emit(pr))
if bip21:
if 'r' in bip21 or ('name' in bip21 and 'sig' in bip21): # TODO set flag in util?
# let callback handle state
return
if ':' not in recipient:
# address only
# create bare invoice
outputs = [PartialTxOutput.from_address_and_value(bip21['address'], 0)]
invoice = self.create_onchain_invoice(outputs, None, None, None)
self._logger.debug(repr(invoice))
self.setValidOnchainInvoice(invoice)
self.validationSuccess.emit()
return
else:
# fallback lightning invoice?
if 'lightning' in bip21:
maybe_lightning_invoice = bip21['lightning']
except InvalidBitcoinURI as e:
bip21 = None
lninvoice = None
maybe_lightning_invoice = maybe_extract_lightning_payment_identifier(maybe_lightning_invoice)
if maybe_lightning_invoice is not None:
if maybe_lightning_invoice.startswith('lnurl'):
self.resolve_lnurl(maybe_lightning_invoice)
return
try:
lninvoice = Invoice.from_bech32(maybe_lightning_invoice)
except InvoiceError as e:
e2 = e.__cause__
if isinstance(e2, LnInvoiceException):
self.validationError.emit('unknown', _("Error parsing Lightning invoice") + f":\n{e2}")
self.clear()
return
if isinstance(e2, lnutil.IncompatibleOrInsaneFeatures):
self.validationError.emit('unknown', _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e2!r}")
self.clear()
return
self._logger.exception(repr(e))
if not lninvoice and not bip21: def _update_from_payment_identifier(self):
self.validationError.emit('unknown',_('Unknown invoice')) if self._pi.need_resolve():
self.clear() self.resolve_pi()
return return
if lninvoice: if self._pi.type == 'lnurl':
if not self._wallet.wallet.has_lightning(): self.on_lnurl(self._pi.lnurl_data)
if not bip21: return
if lninvoice.get_address():
self.setValidLightningInvoice(lninvoice) if self._pi.type == 'bip70':
self.validationSuccess.emit() self._bip70_payment_request_resolved(self._pi.bip70_data)
else: return
self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.'))
else: if self._pi.is_available():
self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') if self._pi.type == 'spk':
self._validateRecipient_bip21_onchain(bip21) outputs = [PartialTxOutput(scriptpubkey=self._pi.spk, value=0)]
else: invoice = self.create_onchain_invoice(outputs, None, None, None)
if not self._wallet.wallet.lnworker.channels: self._logger.debug(repr(invoice))
if bip21 and 'address' in bip21: self.setValidOnchainInvoice(invoice)
self._logger.debug('flow where invoice has both LN and onchain, we have LN enabled but no channels') self.validationSuccess.emit()
self._validateRecipient_bip21_onchain(bip21) return
else: elif self._pi.type == 'bolt11':
self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) lninvoice = Invoice.from_bech32(self._pi.bolt11)
else: if not self._wallet.wallet.has_lightning() and not lninvoice.get_address():
self.validationError.emit('no_lightning',
_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.'))
return
if self._wallet.wallet.lnworker and not self._wallet.wallet.lnworker.channels:
self.validationWarning.emit('no_channels',
_('Detected valid Lightning invoice, but there are no open channels'))
self.setValidLightningInvoice(lninvoice)
self.validationSuccess.emit()
elif self._pi.type == 'bip21':
if self._wallet.wallet.has_lightning() and self._wallet.wallet.lnworker.channels and self._pi.bolt11:
lninvoice = Invoice.from_bech32(self._pi.bolt11)
self.setValidLightningInvoice(lninvoice) self.setValidLightningInvoice(lninvoice)
self.validationSuccess.emit() self.validationSuccess.emit()
else: else:
self._logger.debug('flow without LN but having bip21 uri') self._validateRecipient_bip21_onchain(self._pi.bip21)
self._validateRecipient_bip21_onchain(bip21)
def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None: def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None:
if 'amount' not in bip21: if 'amount' not in bip21:
@ -580,20 +552,15 @@ class QEInvoiceParser(QEInvoice):
self.setValidOnchainInvoice(invoice) self.setValidOnchainInvoice(invoice)
self.validationSuccess.emit() self.validationSuccess.emit()
def resolve_lnurl(self, lnurl): def resolve_pi(self):
self._logger.debug('resolve_lnurl') assert self._pi.need_resolve()
url = decode_lnurl(lnurl) def on_finished(pi):
self._logger.debug(f'{repr(url)}') if pi.is_error():
pass
def resolve_task(): else:
try: self._update_from_payment_identifier()
coro = request_lnurl(url)
fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())
self.on_lnurl(fut.result())
except Exception as e:
self.validationError.emit('lnurl', repr(e))
threading.Thread(target=resolve_task, daemon=True).start() self._pi.resolve(on_finished=on_finished)
def on_lnurl(self, lnurldata): def on_lnurl(self, lnurldata):
self._logger.debug('on_lnurl') self._logger.debug('on_lnurl')
@ -610,49 +577,39 @@ class QEInvoiceParser(QEInvoice):
self.setValidLNURLPayRequest() self.setValidLNURLPayRequest()
self.lnurlRetrieved.emit() self.lnurlRetrieved.emit()
@pyqtSlot('quint64') @pyqtSlot()
@pyqtSlot('quint64', str) @pyqtSlot(str)
def lnurlGetInvoice(self, amount, comment=None): def lnurlGetInvoice(self, comment=None):
assert self._lnurlData assert self._lnurlData
assert self._pi.need_finalize()
self._logger.debug(f'{repr(self._lnurlData)}') self._logger.debug(f'{repr(self._lnurlData)}')
amount = self.amountOverride.satsInt amount = self.amountOverride.satsInt
if self.lnurlData['min_sendable_sat'] != 0:
try:
assert amount >= self.lnurlData['min_sendable_sat']
assert amount <= self.lnurlData['max_sendable_sat']
except Exception:
self.lnurlError.emit('amount', _('Amount out of bounds'))
return
if self._lnurlData['comment_allowed'] == 0: if self._lnurlData['comment_allowed'] == 0:
comment = None comment = None
self._logger.debug(f'fetching callback url {self._lnurlData["callback_url"]}') def on_finished(pi):
def fetch_invoice_task(): if pi.is_error():
try: if pi.is_state(PaymentIdentifierState.INVALID_AMOUNT):
params = { 'amount': amount * 1000 } self.lnurlError.emit('amount', pi.get_error())
if comment: else:
params['comment'] = comment self.lnurlError.emit('lnurl', pi.get_error())
coro = callback_lnurl(self._lnurlData['callback_url'], params) else:
fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) self.on_lnurl_invoice(self.amountOverride.satsInt, pi.bolt11)
self.on_lnurl_invoice(amount, fut.result())
except Exception as e: self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished)
self._logger.error(repr(e))
self.lnurlError.emit('lnurl', str(e))
threading.Thread(target=fetch_invoice_task, daemon=True).start()
def on_lnurl_invoice(self, orig_amount, invoice): def on_lnurl_invoice(self, orig_amount, invoice):
self._logger.debug('on_lnurl_invoice') self._logger.debug('on_lnurl_invoice')
self._logger.debug(f'{repr(invoice)}') self._logger.debug(f'{repr(invoice)}')
# assure no shenanigans with the bolt11 invoice we get back # assure no shenanigans with the bolt11 invoice we get back
lninvoice = Invoice.from_bech32(invoice['pr']) lninvoice = Invoice.from_bech32(invoice)
if orig_amount * 1000 != lninvoice.amount_msat: if orig_amount * 1000 != lninvoice.amount_msat: # TODO msat precision can cause trouble here
raise Exception('Unexpected amount in invoice, differs from lnurl-pay specified amount') raise Exception('Unexpected amount in invoice, differs from lnurl-pay specified amount')
self.recipient = invoice['pr'] self.recipient = invoice
@pyqtSlot() @pyqtSlot()
def saveInvoice(self): def saveInvoice(self):

20
electrum/payment_identifier.py

@ -20,6 +20,7 @@ from .lnutil import IncompatibleOrInsaneFeatures
if TYPE_CHECKING: if TYPE_CHECKING:
from .wallet import Abstract_Wallet from .wallet import Abstract_Wallet
from .transaction import Transaction
def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]:
@ -32,10 +33,6 @@ def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]:
return data return data
return None return None
# URL decode
#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
# note: when checking against these, use .lower() to support case-insensitivity # note: when checking against these, use .lower() to support case-insensitivity
BITCOIN_BIP21_URI_SCHEME = 'bitcoin' BITCOIN_BIP21_URI_SCHEME = 'bitcoin'
@ -183,6 +180,7 @@ class PaymentIdentifierState(IntEnum):
ERROR = 50 # generic error ERROR = 50 # generic error
NOT_FOUND = 51 # PI contains a recognized destination format, but resolve step was unsuccesful NOT_FOUND = 51 # PI contains a recognized destination format, but resolve step was unsuccesful
MERCHANT_ERROR = 52 # PI failed notifying the merchant after broadcasting onchain TX MERCHANT_ERROR = 52 # PI failed notifying the merchant after broadcasting onchain TX
INVALID_AMOUNT = 53 # Specified amount not accepted
class PaymentIdentifier(Logger): class PaymentIdentifier(Logger):
@ -251,6 +249,9 @@ class PaymentIdentifier(Logger):
def is_valid(self): def is_valid(self):
return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY] return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY]
def is_available(self):
return self._state in [PaymentIdentifierState.AVAILABLE]
def is_lightning(self): def is_lightning(self):
return self.lnurl or self.bolt11 return self.lnurl or self.bolt11
@ -445,22 +446,23 @@ class PaymentIdentifier(Logger):
if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat): if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat):
self.error = _('Amount must be between %d and %d sat.') \ self.error = _('Amount must be between %d and %d sat.') \
% (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat) % (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat)
self.set_state(PaymentIdentifierState.INVALID_AMOUNT)
return return
if self.lnurl_data.comment_allowed == 0: if self.lnurl_data.comment_allowed == 0:
comment = None comment = None
params = {'amount': amount_sat * 1000} params = {'amount': amount_sat * 1000}
if comment: if comment:
params['comment'] = comment params['comment'] = comment
try: try:
invoice_data = await callback_lnurl( invoice_data = await callback_lnurl(self.lnurl_data.callback_url, params=params)
self.lnurl_data.callback_url,
params=params,
)
except LNURLError as e: except LNURLError as e:
self.error = f"LNURL request encountered error: {e}" self.error = f"LNURL request encountered error: {e}"
self.set_state(PaymentIdentifierState.ERROR)
return return
bolt11_invoice = invoice_data.get('pr') bolt11_invoice = invoice_data.get('pr')
#
invoice = Invoice.from_bech32(bolt11_invoice) invoice = Invoice.from_bech32(bolt11_invoice)
if invoice.get_amount_sat() != amount_sat: if invoice.get_amount_sat() != amount_sat:
raise Exception("lnurl returned invoice with wrong amount") raise Exception("lnurl returned invoice with wrong amount")

24
electrum/tests/test_util.py

@ -2,12 +2,10 @@ from datetime import datetime
from decimal import Decimal from decimal import Decimal
from electrum import util from electrum import util
from electrum.util import (format_satoshis, format_fee_satoshis, parse_URI, from electrum.util import (format_satoshis, format_fee_satoshis, is_hash256_str, chunks, is_ip_address,
is_hash256_str, chunks, is_ip_address, list_enabled_bits, list_enabled_bits, format_satoshis_plain, is_private_netaddress, is_hex_str,
format_satoshis_plain, is_private_netaddress, is_hex_str, is_integer, is_non_negative_integer, is_int_or_float, is_non_negative_int_or_float)
is_integer, is_non_negative_integer, is_int_or_float, from electrum.payment_identifier import parse_bip21_URI, InvalidBitcoinURI
is_non_negative_int_or_float, is_subpath, InvalidBitcoinURI)
from . import ElectrumTestCase, as_testnet from . import ElectrumTestCase, as_testnet
@ -102,7 +100,7 @@ class TestUtil(ElectrumTestCase):
self.assertEqual("0.01234", format_satoshis_plain(1234, decimal_point=5)) self.assertEqual("0.01234", format_satoshis_plain(1234, decimal_point=5))
def _do_test_parse_URI(self, uri, expected): def _do_test_parse_URI(self, uri, expected):
result = parse_URI(uri) result = parse_bip21_URI(uri)
self.assertEqual(expected, result) self.assertEqual(expected, result)
def test_parse_URI_address(self): def test_parse_URI_address(self):
@ -143,13 +141,13 @@ class TestUtil(ElectrumTestCase):
{'r': 'http://domain.tld/page?h=2a8628fc2fbe'}) {'r': 'http://domain.tld/page?h=2a8628fc2fbe'})
def test_parse_URI_invalid_address(self): def test_parse_URI_invalid_address(self):
self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:invalidaddress') self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:invalidaddress')
def test_parse_URI_invalid(self): def test_parse_URI_invalid(self):
self.assertRaises(InvalidBitcoinURI, parse_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma') self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma')
def test_parse_URI_parameter_pollution(self): def test_parse_URI_parameter_pollution(self):
self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0') self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0')
@as_testnet @as_testnet
def test_parse_URI_lightning_consistency(self): def test_parse_URI_lightning_consistency(self):
@ -174,11 +172,11 @@ class TestUtil(ElectrumTestCase):
'memo': 'test266', 'memo': 'test266',
'message': 'test266'}) 'message': 'test266'})
# bip21 uri that includes "lightning" key. LN part has fallback address BUT it mismatches the top-level address # bip21 uri that includes "lightning" key. LN part has fallback address BUT it mismatches the top-level address
self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:tb1qvu0c9xme0ul3gzx4nzqdgxsu25acuk9wvsj2j2?amount=0.0007&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql') self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qvu0c9xme0ul3gzx4nzqdgxsu25acuk9wvsj2j2?amount=0.0007&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql')
# bip21 uri that includes "lightning" key. top-level amount mismatches LN amount # bip21 uri that includes "lightning" key. top-level amount mismatches LN amount
self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql') self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql')
# bip21 uri that includes "lightning" key with garbage unparseable value # bip21 uri that includes "lightning" key with garbage unparseable value
self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdasdasdasdasd') self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdasdasdasdasd')
def test_is_hash256_str(self): def test_is_hash256_str(self):
self.assertTrue(is_hash256_str('09a4c03e3bdf83bbe3955f907ee52da4fc12f4813d459bc75228b64ad08617c7')) self.assertTrue(is_hash256_str('09a4c03e3bdf83bbe3955f907ee52da4fc12f4813d459bc75228b64ad08617c7'))

Loading…
Cancel
Save