Merge pull request #10157 from SomberNight/202508_swap_sanity_check_costs
swaps: add sanity-check for total swap costs
This commit is contained in:
@@ -13,7 +13,10 @@ from electrum.i18n import _
|
|||||||
from electrum.logging import Logger
|
from electrum.logging import Logger
|
||||||
from electrum.bitcoin import DummyAddress
|
from electrum.bitcoin import DummyAddress
|
||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend, UserCancelled, ChoiceItem
|
from electrum.util import (
|
||||||
|
NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend, UserCancelled, ChoiceItem,
|
||||||
|
UserFacingException,
|
||||||
|
)
|
||||||
from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST
|
from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST
|
||||||
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
|
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
|
||||||
from electrum.network import TxBroadcastError, BestEffortRequestFailed
|
from electrum.network import TxBroadcastError, BestEffortRequestFailed
|
||||||
@@ -338,7 +341,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
|||||||
coro = sm.request_swap_for_amount(transport=transport, onchain_amount=swap_dummy_output.value)
|
coro = sm.request_swap_for_amount(transport=transport, onchain_amount=swap_dummy_output.value)
|
||||||
try:
|
try:
|
||||||
swap, swap_invoice = self.window.run_coroutine_dialog(coro, _('Requesting swap invoice...'))
|
swap, swap_invoice = self.window.run_coroutine_dialog(coro, _('Requesting swap invoice...'))
|
||||||
except SwapServerError as e:
|
except (SwapServerError, UserFacingException) as e:
|
||||||
self.show_error(str(e))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
except UserCancelled:
|
except UserCancelled:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from .transaction import (
|
|||||||
from .util import (
|
from .util import (
|
||||||
log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, ca_path, gen_nostr_ann_pow,
|
log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, ca_path, gen_nostr_ann_pow,
|
||||||
get_nostr_ann_pow_amount, make_aiohttp_proxy_connector, get_running_loop, get_asyncio_loop, wait_for2,
|
get_nostr_ann_pow_amount, make_aiohttp_proxy_connector, get_running_loop, get_asyncio_loop, wait_for2,
|
||||||
run_sync_function_on_asyncio_thread, trigger_callback, NoDynamicFeeEstimates
|
run_sync_function_on_asyncio_thread, trigger_callback, NoDynamicFeeEstimates, UserFacingException,
|
||||||
)
|
)
|
||||||
from . import lnutil
|
from . import lnutil
|
||||||
from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
|
from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
|
||||||
@@ -507,6 +507,26 @@ class SwapManager(Logger):
|
|||||||
fee_policy = FeePolicy(policy_descriptor)
|
fee_policy = FeePolicy(policy_descriptor)
|
||||||
return fee_policy.estimate_fee(SWAP_TX_SIZE, network=self.network, allow_fallback_to_static_rates=True)
|
return fee_policy.estimate_fee(SWAP_TX_SIZE, network=self.network, allow_fallback_to_static_rates=True)
|
||||||
|
|
||||||
|
def _sanity_check_swap_costs(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
incoming_sat: int,
|
||||||
|
outgoing_sat: int,
|
||||||
|
) -> None:
|
||||||
|
"""The user should have already seen the swap amounts, and hence the cost.
|
||||||
|
These are just some last-minute sanity checks that the cost of the swap is not insane.
|
||||||
|
"""
|
||||||
|
costs_abs = outgoing_sat - incoming_sat
|
||||||
|
costs_ratio = 1 - incoming_sat / outgoing_sat
|
||||||
|
if costs_abs < 10_000: # "small" amounts are exempt from checks
|
||||||
|
return
|
||||||
|
exc = UserFacingException(_("Total swap costs are insane.") + f"\n({costs_ratio=}, {costs_abs=} sat)")
|
||||||
|
if costs_ratio > 0.25:
|
||||||
|
raise exc
|
||||||
|
if costs_abs > 1_000_000:
|
||||||
|
if costs_ratio > 0.15:
|
||||||
|
raise exc
|
||||||
|
|
||||||
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
|
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
|
||||||
# for history
|
# for history
|
||||||
swap = self._swaps.get(payment_hash.hex())
|
swap = self._swaps.get(payment_hash.hex())
|
||||||
@@ -755,6 +775,8 @@ class SwapManager(Logger):
|
|||||||
expected_onchain_amount_sat: int,
|
expected_onchain_amount_sat: int,
|
||||||
channels: Optional[Sequence['Channel']] = None,
|
channels: Optional[Sequence['Channel']] = None,
|
||||||
) -> Tuple[SwapData, str]:
|
) -> Tuple[SwapData, str]:
|
||||||
|
self._sanity_check_swap_costs(
|
||||||
|
incoming_sat=lightning_amount_sat, outgoing_sat=expected_onchain_amount_sat)
|
||||||
await self.is_initialized.wait() # add timeout
|
await self.is_initialized.wait() # add timeout
|
||||||
refund_privkey = os.urandom(32)
|
refund_privkey = os.urandom(32)
|
||||||
refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)
|
refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)
|
||||||
@@ -927,6 +949,8 @@ class SwapManager(Logger):
|
|||||||
"""
|
"""
|
||||||
assert self.network
|
assert self.network
|
||||||
assert self.lnwatcher
|
assert self.lnwatcher
|
||||||
|
self._sanity_check_swap_costs(
|
||||||
|
incoming_sat=expected_onchain_amount_sat, outgoing_sat=lightning_amount_sat)
|
||||||
privkey = os.urandom(32)
|
privkey = os.urandom(32)
|
||||||
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
|
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
|
||||||
preimage = os.urandom(32)
|
preimage = os.urandom(32)
|
||||||
|
|||||||
Reference in New Issue
Block a user