2025-12-12 11:10:31 +01:00
|
|
|
import logging
|
|
|
|
|
import os
|
2026-02-03 15:13:18 +01:00
|
|
|
import asyncio
|
|
|
|
|
from unittest import mock
|
|
|
|
|
from decimal import Decimal
|
2025-12-12 11:10:31 +01:00
|
|
|
|
2026-02-03 15:13:18 +01:00
|
|
|
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
|
2026-03-24 12:34:06 +01:00
|
|
|
import electrum.trampoline
|
2025-12-12 11:10:31 +01:00
|
|
|
from . import ElectrumTestCase
|
2026-03-24 12:34:06 +01:00
|
|
|
from .test_lnchannel import create_test_channels
|
2025-12-12 11:10:31 +01:00
|
|
|
|
2026-02-03 15:13:18 +01:00
|
|
|
from electrum.lnutil import RECEIVED, MIN_FINAL_CLTV_DELTA_ACCEPTED, serialize_htlc_key, LnFeatures
|
2025-12-12 11:10:31 +01:00
|
|
|
from electrum.logging import console_stderr_handler
|
2026-02-03 15:13:18 +01:00
|
|
|
from electrum.lntransport import LNPeerAddr
|
2025-12-12 11:10:31 +01:00
|
|
|
from electrum.invoices import LN_EXPIRY_NEVER, PR_UNPAID
|
2026-02-03 15:13:18 +01:00
|
|
|
from electrum.lnpeer import Peer
|
|
|
|
|
from electrum.lnchannel import Channel, ChannelState
|
|
|
|
|
from electrum.lnonion import OnionPacket, OnionRoutingFailure
|
|
|
|
|
from electrum.crypto import sha256
|
2025-12-12 11:10:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLNWallet(ElectrumTestCase):
|
|
|
|
|
TESTNET = True
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
super().setUpClass()
|
|
|
|
|
console_stderr_handler.setLevel(logging.DEBUG)
|
|
|
|
|
|
|
|
|
|
async def asyncSetUp(self):
|
|
|
|
|
self.lnwallet_anchors = self.create_mock_lnwallet(name='mock_lnwallet_anchors', has_anchors=True)
|
|
|
|
|
await super().asyncSetUp()
|
|
|
|
|
|
|
|
|
|
def test_create_payment_info(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
tests = (
|
|
|
|
|
(100_000, 200, 100),
|
|
|
|
|
(None, 200, 100),
|
|
|
|
|
(None, None, LN_EXPIRY_NEVER),
|
|
|
|
|
(100_000, None, 0),
|
|
|
|
|
)
|
|
|
|
|
for amount_msat, min_final_cltv_delta, exp_delay in tests:
|
|
|
|
|
payment_hash = wallet.create_payment_info(
|
|
|
|
|
amount_msat=amount_msat,
|
|
|
|
|
min_final_cltv_delta=min_final_cltv_delta,
|
|
|
|
|
exp_delay=exp_delay,
|
|
|
|
|
)
|
|
|
|
|
self.assertIsNotNone(wallet.get_preimage(payment_hash))
|
|
|
|
|
pi = wallet.get_payment_info(payment_hash, direction=RECEIVED)
|
|
|
|
|
self.assertEqual(pi.amount_msat, amount_msat)
|
|
|
|
|
self.assertEqual(pi.min_final_cltv_delta, min_final_cltv_delta or MIN_FINAL_CLTV_DELTA_ACCEPTED)
|
|
|
|
|
self.assertEqual(pi.expiry_delay, exp_delay or LN_EXPIRY_NEVER)
|
|
|
|
|
self.assertEqual(pi.db_key, f"{payment_hash.hex()}:{int(pi.direction)}")
|
|
|
|
|
self.assertEqual(pi.status, PR_UNPAID)
|
|
|
|
|
self.assertIsNone(wallet.get_payment_info(os.urandom(32), direction=RECEIVED))
|
2026-03-02 17:12:07 +00:00
|
|
|
|
|
|
|
|
def test_create_payment_info__amount_must_not_be_zero(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
amount_msat, min_final_cltv_delta, exp_delay = (0, 200, 100)
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
wallet.create_payment_info(
|
|
|
|
|
amount_msat=amount_msat,
|
|
|
|
|
min_final_cltv_delta=min_final_cltv_delta,
|
|
|
|
|
exp_delay=exp_delay,
|
|
|
|
|
)
|
2026-03-24 12:34:06 +01:00
|
|
|
|
|
|
|
|
async def test_trampoline_invoice_features_and_routing_hints(self):
|
|
|
|
|
"""
|
|
|
|
|
When the invoice_features signal trampoline support, routing hints must only
|
|
|
|
|
contain trampoline nodes. When it does not, all channel can be added as r_tags.
|
|
|
|
|
We only signal trampoline support in the invoice if all open channels do support trampoline.
|
|
|
|
|
"""
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
self.assertFalse(wallet.uses_trampoline())
|
|
|
|
|
|
|
|
|
|
trampoline_peer = self.create_mock_lnwallet(name='trampoline_peer', has_anchors=True)
|
|
|
|
|
trampoline_pubkey = trampoline_peer.node_keypair.pubkey
|
|
|
|
|
|
|
|
|
|
regular_peer = self.create_mock_lnwallet(name='regular_peer', has_anchors=True)
|
|
|
|
|
regular_pubkey = regular_peer.node_keypair.pubkey
|
|
|
|
|
|
|
|
|
|
chan_t, _ = create_test_channels(alice_lnwallet=wallet, bob_lnwallet=trampoline_peer, anchor_outputs=True)
|
|
|
|
|
chan_r, _ = create_test_channels(alice_lnwallet=wallet, bob_lnwallet=regular_peer, anchor_outputs=True)
|
|
|
|
|
wallet._add_channel(chan_t)
|
|
|
|
|
wallet._add_channel(chan_r)
|
|
|
|
|
|
|
|
|
|
# only trampoline_peer is a known trampoline forwarder
|
|
|
|
|
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
|
|
|
|
|
'trampoline_peer': LNPeerAddr(
|
|
|
|
|
host="127.0.0.1",
|
|
|
|
|
port=9735,
|
|
|
|
|
pubkey=trampoline_pubkey,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
self.addCleanup(lambda: electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS.clear())
|
|
|
|
|
|
|
|
|
|
amount_msat = 100_000
|
|
|
|
|
|
|
|
|
|
# mixed peers: trampoline feature must be stripped, all peers in hints
|
|
|
|
|
payment_hash = wallet.create_payment_info(amount_msat=amount_msat)
|
|
|
|
|
pi = wallet.get_payment_info(payment_hash, direction=RECEIVED)
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
pi.invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM),
|
|
|
|
|
"trampoline bit should be stripped when not all peers are trampoline",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lnaddr, _ = wallet.get_bolt11_invoice(payment_info=pi, message='test', fallback_address=None)
|
|
|
|
|
hint_node_ids = {route[0][0] for route in lnaddr.get_routing_info('r')}
|
|
|
|
|
self.assertEqual(hint_node_ids, {trampoline_pubkey, regular_pubkey})
|
|
|
|
|
|
|
|
|
|
# trampoline feature should not be set if we use trampoline but one peer is not a trampoline
|
|
|
|
|
old_check, wallet.uses_trampoline = wallet.uses_trampoline, lambda: True
|
|
|
|
|
self.assertTrue(wallet.uses_trampoline())
|
|
|
|
|
|
|
|
|
|
payment_hash = wallet.create_payment_info(amount_msat=amount_msat)
|
|
|
|
|
pi = wallet.get_payment_info(payment_hash, direction=RECEIVED)
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
pi.invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM),
|
|
|
|
|
"trampoline feature should not be set if we use trampoline but one peer is not a trampoline",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
wallet.clear_invoices_cache()
|
|
|
|
|
lnaddr, _ = wallet.get_bolt11_invoice(payment_info=pi, message='test', fallback_address=None)
|
|
|
|
|
hint_node_ids = {route[0][0] for route in lnaddr.get_routing_info('r')}
|
|
|
|
|
self.assertEqual(hint_node_ids, {trampoline_pubkey, regular_pubkey})
|
|
|
|
|
|
|
|
|
|
wallet.uses_trampoline = old_check
|
|
|
|
|
self.assertFalse(wallet.uses_trampoline())
|
|
|
|
|
|
|
|
|
|
# all peers trampoline: we signal trampoline support, even with trampoline disabled
|
|
|
|
|
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS['regular_peer'] = LNPeerAddr(
|
|
|
|
|
host="127.0.0.1",
|
|
|
|
|
port=9735,
|
|
|
|
|
pubkey=regular_pubkey,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
payment_hash2 = wallet.create_payment_info(amount_msat=amount_msat)
|
|
|
|
|
pi2 = wallet.get_payment_info(payment_hash2, direction=RECEIVED)
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
pi2.invoice_features.supports(LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM),
|
|
|
|
|
"trampoline bit should be present when all peers are trampoline",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
wallet.clear_invoices_cache()
|
|
|
|
|
lnaddr2, _ = wallet.get_bolt11_invoice(payment_info=pi2, message='test', fallback_address=None)
|
|
|
|
|
hint_node_ids2 = {route[0][0] for route in lnaddr2.get_routing_info('r')}
|
|
|
|
|
self.assertEqual(hint_node_ids2, {trampoline_pubkey, regular_pubkey})
|
|
|
|
|
|
|
|
|
|
# assert only trampoline peers are included in r_tags if the invoice_features signal trampoline
|
|
|
|
|
del electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS['regular_peer']
|
|
|
|
|
wallet.clear_invoices_cache()
|
|
|
|
|
lnaddr3, _ = wallet.get_bolt11_invoice(payment_info=pi2, message='test', fallback_address=None)
|
|
|
|
|
hint_node_ids3 = {route[0][0] for route in lnaddr3.get_routing_info('r')}
|
|
|
|
|
self.assertEqual(hint_node_ids3, {trampoline_pubkey})
|
2026-02-03 15:13:18 +01:00
|
|
|
|
|
|
|
|
async def test_open_channel_just_in_time_success(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
wallet.config.ZEROCONF_MIN_OPENING_FEE = 0
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
|
|
|
|
|
next_peer = mock.Mock(spec=Peer)
|
|
|
|
|
next_chan = mock.Mock(spec=Channel)
|
|
|
|
|
next_chan.get_scid_or_local_alias.return_value = bytes(8)
|
|
|
|
|
|
|
|
|
|
funding_tx = mock.Mock()
|
|
|
|
|
funding_tx.txid.return_value = os.urandom(32).hex()
|
2026-02-03 17:37:29 +01:00
|
|
|
funding_tx.get_fee = lambda: 250
|
2026-02-03 15:13:18 +01:00
|
|
|
|
|
|
|
|
wallet.open_channel_with_peer = mock.AsyncMock(return_value=(next_chan, funding_tx))
|
|
|
|
|
wallet.network.try_broadcasting = mock.AsyncMock(return_value=True)
|
|
|
|
|
|
|
|
|
|
preimage = os.urandom(32)
|
|
|
|
|
payment_hash = sha256(preimage)
|
|
|
|
|
|
|
|
|
|
htlc = mock.Mock()
|
|
|
|
|
htlc.htlc_id = 0
|
|
|
|
|
next_peer.send_htlc.return_value = htlc
|
|
|
|
|
|
|
|
|
|
task = asyncio.create_task(wallet.open_channel_just_in_time(
|
|
|
|
|
next_peer=next_peer,
|
|
|
|
|
next_amount_msat_htlc=1000000,
|
|
|
|
|
next_cltv_abs=500,
|
|
|
|
|
payment_hash=payment_hash,
|
|
|
|
|
next_onion=mock.Mock(spec=OnionPacket)
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
wallet.save_preimage(payment_hash, preimage)
|
|
|
|
|
htlc_key = await task
|
|
|
|
|
htlc_key_correct = serialize_htlc_key(next_chan.get_scid_or_local_alias(), htlc.htlc_id)
|
|
|
|
|
self.assertEqual(htlc_key, htlc_key_correct)
|
|
|
|
|
|
|
|
|
|
wallet.open_channel_with_peer.assert_called_once()
|
|
|
|
|
next_peer.send_htlc.assert_called_once()
|
|
|
|
|
wallet.network.try_broadcasting.assert_called()
|
|
|
|
|
|
|
|
|
|
async def test_open_channel_just_in_time_failure_channel_open(self):
|
|
|
|
|
"""The channel opening failed on the LSP side because the client rejected the incoming channel"""
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
wallet.config.ZEROCONF_MIN_OPENING_FEE = 0
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
next_peer = mock.Mock(spec=Peer)
|
|
|
|
|
wallet.open_channel_with_peer = mock.AsyncMock(side_effect=Exception("peer rejected incoming channel"))
|
|
|
|
|
preimage = os.urandom(32)
|
|
|
|
|
wallet.save_preimage(sha256(preimage), preimage)
|
|
|
|
|
wallet._cleanup_failed_jit_channel = mock.AsyncMock()
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(OnionRoutingFailure):
|
|
|
|
|
await wallet.open_channel_just_in_time(
|
|
|
|
|
next_peer=next_peer,
|
|
|
|
|
next_amount_msat_htlc=1000000,
|
|
|
|
|
next_cltv_abs=500,
|
|
|
|
|
payment_hash=sha256(preimage),
|
|
|
|
|
next_onion=mock.Mock(spec=OnionPacket)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(wallet.get_preimage(sha256(preimage)))
|
|
|
|
|
wallet._cleanup_failed_jit_channel.assert_not_called()
|
|
|
|
|
|
|
|
|
|
async def test_open_channel_just_in_time_failure_send_htlc(self):
|
|
|
|
|
"""The LSP fails to forward the htlc to the client"""
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
wallet.config.ZEROCONF_MIN_OPENING_FEE = 0
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
|
|
|
|
|
next_peer = mock.Mock(spec=Peer)
|
|
|
|
|
chan = mock.Mock(spec=Channel)
|
|
|
|
|
funding_tx = mock.Mock()
|
|
|
|
|
|
|
|
|
|
wallet.open_channel_with_peer = mock.AsyncMock(return_value=(chan, funding_tx))
|
|
|
|
|
next_peer.send_htlc.side_effect = Exception("couldn't send htlc, peer disconnected")
|
|
|
|
|
preimage = os.urandom(32)
|
|
|
|
|
wallet.save_preimage(sha256(preimage), preimage)
|
|
|
|
|
wallet._cleanup_failed_jit_channel = mock.AsyncMock()
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(OnionRoutingFailure):
|
|
|
|
|
await wallet.open_channel_just_in_time(
|
|
|
|
|
next_peer=next_peer,
|
|
|
|
|
next_amount_msat_htlc=1000000,
|
|
|
|
|
next_cltv_abs=500,
|
|
|
|
|
payment_hash=sha256(preimage),
|
|
|
|
|
next_onion=mock.Mock(spec=OnionPacket)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(wallet.get_preimage(sha256(preimage)))
|
|
|
|
|
wallet._cleanup_failed_jit_channel.assert_called_once_with(chan)
|
|
|
|
|
|
|
|
|
|
async def test_open_channel_just_in_time_failure_preimage_timeout(self):
|
|
|
|
|
"""The client never releases the preimage"""
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
wallet.config.ZEROCONF_MIN_OPENING_FEE = 0
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
|
|
|
|
|
next_peer = mock.Mock(spec=Peer)
|
|
|
|
|
chan = mock.Mock(spec=Channel)
|
|
|
|
|
funding_tx = mock.Mock()
|
|
|
|
|
|
|
|
|
|
wallet.open_channel_with_peer = mock.AsyncMock(return_value=(chan, funding_tx))
|
|
|
|
|
|
|
|
|
|
htlc = mock.Mock()
|
|
|
|
|
next_peer.send_htlc.return_value = htlc
|
|
|
|
|
|
|
|
|
|
wallet._cleanup_failed_jit_channel = mock.AsyncMock()
|
|
|
|
|
|
|
|
|
|
with mock.patch('electrum.lnworker.LN_P2P_NETWORK_TIMEOUT', 0.01):
|
|
|
|
|
with self.assertRaises(OnionRoutingFailure):
|
|
|
|
|
await wallet.open_channel_just_in_time(
|
|
|
|
|
next_peer=next_peer,
|
|
|
|
|
next_amount_msat_htlc=1000000,
|
|
|
|
|
next_cltv_abs=500,
|
|
|
|
|
payment_hash=os.urandom(32),
|
|
|
|
|
next_onion=mock.Mock(spec=OnionPacket)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
wallet._cleanup_failed_jit_channel.assert_called_once_with(chan)
|
|
|
|
|
|
|
|
|
|
async def test_open_channel_just_in_time_failure_broadcast(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
wallet.config.ZEROCONF_MIN_OPENING_FEE = 0
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
|
|
|
|
|
next_peer = mock.Mock(spec=Peer)
|
|
|
|
|
chan = mock.Mock(spec=Channel)
|
|
|
|
|
|
|
|
|
|
funding_tx = mock.Mock()
|
|
|
|
|
|
|
|
|
|
wallet.open_channel_with_peer = mock.AsyncMock(return_value=(chan, funding_tx))
|
|
|
|
|
|
|
|
|
|
preimage = os.urandom(32)
|
|
|
|
|
wallet.save_preimage(sha256(preimage), preimage)
|
|
|
|
|
|
|
|
|
|
wallet.network.try_broadcasting = mock.AsyncMock(return_value=False)
|
|
|
|
|
wallet.wallet.adb.get_tx_height = mock.Mock(return_value=mock.Mock(height=lambda: TX_HEIGHT_LOCAL))
|
|
|
|
|
|
|
|
|
|
wallet._cleanup_failed_jit_channel = mock.AsyncMock()
|
|
|
|
|
|
|
|
|
|
with mock.patch('electrum.lnworker.ZEROCONF_TIMEOUT', 0.01), \
|
|
|
|
|
mock.patch('electrum.lnworker.asyncio.sleep', new_callable=mock.AsyncMock):
|
|
|
|
|
with self.assertRaises(OnionRoutingFailure):
|
|
|
|
|
await wallet.open_channel_just_in_time(
|
|
|
|
|
next_peer=next_peer,
|
|
|
|
|
next_amount_msat_htlc=1000000,
|
|
|
|
|
next_cltv_abs=500,
|
|
|
|
|
payment_hash=sha256(preimage),
|
|
|
|
|
next_onion=mock.Mock(spec=OnionPacket)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.assertIsNone(wallet.get_preimage(sha256(preimage)))
|
|
|
|
|
wallet._cleanup_failed_jit_channel.assert_called_once_with(chan)
|
|
|
|
|
|
|
|
|
|
async def test_open_channel_just_in_time_config_disabled(self):
|
|
|
|
|
"""open_channel_just_in_time rejects to open a channel if the config is disabled"""
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
wallet.config.ZEROCONF_MIN_OPENING_FEE = 0
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = False
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(AssertionError):
|
|
|
|
|
await wallet.open_channel_just_in_time(
|
|
|
|
|
next_peer=mock.Mock(spec=Peer),
|
|
|
|
|
next_amount_msat_htlc=1000000,
|
|
|
|
|
next_cltv_abs=500,
|
|
|
|
|
payment_hash=os.urandom(32),
|
|
|
|
|
next_onion=mock.Mock(spec=OnionPacket)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def test_cleanup_failed_jit_channel(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
|
|
|
|
|
chan = mock.Mock(spec=Channel)
|
|
|
|
|
chan_id = os.urandom(32).hex()
|
|
|
|
|
chan.channel_id = chan_id
|
|
|
|
|
funding_txid = os.urandom(32).hex()
|
|
|
|
|
chan.funding_outpoint = mock.Mock()
|
|
|
|
|
chan.funding_outpoint.txid = funding_txid
|
|
|
|
|
chan.get_funding_height.return_value = None
|
|
|
|
|
|
|
|
|
|
# close_channel fails with exception
|
|
|
|
|
wallet.close_channel = mock.AsyncMock(side_effect=Exception("peer disconnected"))
|
|
|
|
|
wallet.remove_channel = mock.Mock()
|
|
|
|
|
wallet.lnwatcher = mock.Mock()
|
|
|
|
|
wallet.lnwatcher.adb = mock.Mock()
|
|
|
|
|
wallet.lnwatcher.adb.remove_transaction = mock.Mock()
|
|
|
|
|
|
|
|
|
|
await wallet._cleanup_failed_jit_channel(chan)
|
|
|
|
|
|
|
|
|
|
wallet.close_channel.assert_called_once_with(chan_id)
|
|
|
|
|
chan.set_state.assert_called_once_with(ChannelState.REDEEMED, force=True)
|
|
|
|
|
wallet.lnwatcher.adb.remove_transaction.assert_called_once_with(funding_txid)
|
|
|
|
|
wallet.remove_channel.assert_called_once_with(chan_id)
|
|
|
|
|
|
|
|
|
|
async def test_receive_requires_jit_channel(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="cannot get jit channel"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=False)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(0))
|
|
|
|
|
self.assertFalse(wallet.receive_requires_jit_channel(1_000_000))
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="could get zeroconf channel but doesn't need one"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=True)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(2000))
|
|
|
|
|
self.assertFalse(wallet.receive_requires_jit_channel(1_000_000))
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="could get zeroconf channel and needs one"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=True)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(500))
|
|
|
|
|
self.assertTrue(wallet.receive_requires_jit_channel(1_000_000))
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="could get one but can receive exactly the requested amount"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=True)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(1000))
|
|
|
|
|
self.assertFalse(wallet.receive_requires_jit_channel(1_000_000))
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="0 amount invoice, could get channel but can receive something"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=True)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(1))
|
|
|
|
|
self.assertFalse(wallet.receive_requires_jit_channel(None))
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="0 amount invoice (None amount), cannot receive anything and can get channel"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=True)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(0))
|
|
|
|
|
self.assertTrue(wallet.receive_requires_jit_channel(None))
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="0 amount invoice (0 msat), cannot receive anything, could get channel"):
|
|
|
|
|
wallet.can_get_zeroconf_channel = mock.Mock(return_value=True)
|
|
|
|
|
wallet.num_sats_can_receive = mock.Mock(return_value=Decimal(0))
|
|
|
|
|
self.assertTrue(wallet.receive_requires_jit_channel(0))
|
|
|
|
|
|
|
|
|
|
async def test_can_get_zeroconf_channel(self):
|
|
|
|
|
wallet = self.lnwallet_anchors
|
|
|
|
|
valid_peer = "02" * 33 + "@localhost:9735"
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="disabled in config"):
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = False
|
|
|
|
|
wallet.config.ZEROCONF_TRUSTED_NODE = valid_peer
|
|
|
|
|
self.assertFalse(wallet.can_get_zeroconf_channel())
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="enabled, but no trusted node configured"):
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
wallet.config.ZEROCONF_TRUSTED_NODE = ''
|
|
|
|
|
self.assertFalse(wallet.can_get_zeroconf_channel())
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="enabled, invalid trusted node string"):
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
wallet.config.ZEROCONF_TRUSTED_NODE = "invalid_node_string"
|
|
|
|
|
self.assertFalse(wallet.can_get_zeroconf_channel())
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="enabled, valid trusted node, but not connected"):
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
wallet.config.ZEROCONF_TRUSTED_NODE = valid_peer
|
|
|
|
|
self.assertFalse(wallet.can_get_zeroconf_channel())
|
|
|
|
|
|
|
|
|
|
with self.subTest(msg="enabled, valid trusted node, and connected"):
|
|
|
|
|
wallet.lnpeermgr.get_peer_by_pubkey = mock.Mock(return_value=mock.Mock(spec=Peer))
|
|
|
|
|
wallet.config.OPEN_ZEROCONF_CHANNELS = True
|
|
|
|
|
wallet.config.ZEROCONF_TRUSTED_NODE = valid_peer
|
|
|
|
|
self.assertTrue(wallet.can_get_zeroconf_channel())
|