swaps: small refactor and add unit tests for claim tx
This commit is contained in:
@@ -14,7 +14,7 @@ from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script
|
|||||||
is_segwit_address, construct_witness)
|
is_segwit_address, construct_witness)
|
||||||
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint
|
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint
|
||||||
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
|
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
|
||||||
from .util import log_exceptions
|
from .util import log_exceptions, BelowDustLimit
|
||||||
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address
|
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address
|
||||||
from .bitcoin import dust_threshold
|
from .bitcoin import dust_threshold
|
||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
|
|||||||
from .wallet import Abstract_Wallet
|
from .wallet import Abstract_Wallet
|
||||||
from .lnwatcher import LNWalletWatcher
|
from .lnwatcher import LNWalletWatcher
|
||||||
from .lnworker import LNWallet
|
from .lnworker import LNWallet
|
||||||
|
from .simple_config import SimpleConfig
|
||||||
|
|
||||||
|
|
||||||
API_URL_MAINNET = 'https://swaps.electrum.org/api'
|
API_URL_MAINNET = 'https://swaps.electrum.org/api'
|
||||||
@@ -116,7 +117,6 @@ def create_claim_tx(
|
|||||||
*,
|
*,
|
||||||
txin: PartialTxInput,
|
txin: PartialTxInput,
|
||||||
witness_script: bytes,
|
witness_script: bytes,
|
||||||
preimage: Union[bytes, int], # 0 if timing out forward-swap
|
|
||||||
address: str,
|
address: str,
|
||||||
amount_sat: int,
|
amount_sat: int,
|
||||||
locktime: int,
|
locktime: int,
|
||||||
@@ -124,11 +124,12 @@ def create_claim_tx(
|
|||||||
"""Create tx to either claim successful reverse-swap,
|
"""Create tx to either claim successful reverse-swap,
|
||||||
or to get refunded for timed-out forward-swap.
|
or to get refunded for timed-out forward-swap.
|
||||||
"""
|
"""
|
||||||
|
assert txin.address is not None
|
||||||
if is_segwit_address(txin.address):
|
if is_segwit_address(txin.address):
|
||||||
txin.script_type = 'p2wsh'
|
txin.script_type = 'p2wsh'
|
||||||
txin.script_sig = b''
|
txin.script_sig = b''
|
||||||
else:
|
else:
|
||||||
txin.script_type = 'p2wsh-p2sh'
|
txin.script_type = 'p2wsh-p2sh' # TODO rm??
|
||||||
txin.redeem_script = bytes.fromhex(p2wsh_nested_script(witness_script.hex()))
|
txin.redeem_script = bytes.fromhex(p2wsh_nested_script(witness_script.hex()))
|
||||||
txin.script_sig = bytes.fromhex(push_script(txin.redeem_script.hex()))
|
txin.script_sig = bytes.fromhex(push_script(txin.redeem_script.hex()))
|
||||||
txin.witness_script = witness_script
|
txin.witness_script = witness_script
|
||||||
@@ -217,33 +218,21 @@ class SwapManager(Logger):
|
|||||||
if not swap.is_reverse and delta < 0:
|
if not swap.is_reverse and delta < 0:
|
||||||
# too early for refund
|
# too early for refund
|
||||||
return
|
return
|
||||||
# FIXME the mining fee should depend on swap.is_reverse.
|
try:
|
||||||
# the txs are not the same size...
|
tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
|
||||||
amount_sat = txin.value_sats() - self.get_claim_fee()
|
except BelowDustLimit:
|
||||||
if amount_sat < dust_threshold():
|
|
||||||
self.logger.info('utxo value below dust threshold')
|
self.logger.info('utxo value below dust threshold')
|
||||||
continue
|
continue
|
||||||
if swap.is_reverse: # successful reverse swap
|
|
||||||
preimage = swap.preimage
|
|
||||||
locktime = 0
|
|
||||||
else: # timing out forward swap
|
|
||||||
preimage = 0
|
|
||||||
locktime = swap.locktime
|
|
||||||
tx = create_claim_tx(
|
|
||||||
txin=txin,
|
|
||||||
witness_script=swap.redeem_script,
|
|
||||||
preimage=preimage,
|
|
||||||
address=swap.receive_address,
|
|
||||||
amount_sat=amount_sat,
|
|
||||||
locktime=locktime,
|
|
||||||
)
|
|
||||||
self.sign_tx(tx, swap)
|
|
||||||
self.logger.info(f'adding claim tx {tx.txid()}')
|
self.logger.info(f'adding claim tx {tx.txid()}')
|
||||||
self.wallet.adb.add_transaction(tx)
|
self.wallet.adb.add_transaction(tx)
|
||||||
swap.spending_txid = tx.txid()
|
swap.spending_txid = tx.txid()
|
||||||
|
|
||||||
def get_claim_fee(self):
|
def get_claim_fee(self):
|
||||||
return self.wallet.config.estimate_fee(136, allow_fallback_to_static_rates=True)
|
return self._get_claim_fee(config=self.wallet.config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_claim_fee(cls, *, config: 'SimpleConfig'):
|
||||||
|
return config.estimate_fee(136, allow_fallback_to_static_rates=True)
|
||||||
|
|
||||||
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
|
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
|
||||||
# for history
|
# for history
|
||||||
@@ -650,7 +639,8 @@ class SwapManager(Logger):
|
|||||||
witness = [sig_dummy, preimage, witness_script]
|
witness = [sig_dummy, preimage, witness_script]
|
||||||
txin.witness_sizehint = len(bytes.fromhex(construct_witness(witness)))
|
txin.witness_sizehint = len(bytes.fromhex(construct_witness(witness)))
|
||||||
|
|
||||||
def sign_tx(self, tx: PartialTransaction, swap: SwapData) -> None:
|
@classmethod
|
||||||
|
def sign_tx(cls, tx: PartialTransaction, swap: SwapData) -> None:
|
||||||
preimage = swap.preimage if swap.is_reverse else 0
|
preimage = swap.preimage if swap.is_reverse else 0
|
||||||
witness_script = swap.redeem_script
|
witness_script = swap.redeem_script
|
||||||
txin = tx.inputs()[0]
|
txin = tx.inputs()[0]
|
||||||
@@ -663,6 +653,34 @@ class SwapManager(Logger):
|
|||||||
witness = [sig, preimage, witness_script]
|
witness = [sig, preimage, witness_script]
|
||||||
txin.witness = bytes.fromhex(construct_witness(witness))
|
txin.witness = bytes.fromhex(construct_witness(witness))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_and_sign_claim_tx(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
txin: PartialTxInput,
|
||||||
|
swap: SwapData,
|
||||||
|
config: 'SimpleConfig',
|
||||||
|
) -> PartialTransaction:
|
||||||
|
# FIXME the mining fee should depend on swap.is_reverse.
|
||||||
|
# the txs are not the same size...
|
||||||
|
amount_sat = txin.value_sats() - cls._get_claim_fee(config=config)
|
||||||
|
if amount_sat < dust_threshold():
|
||||||
|
raise BelowDustLimit()
|
||||||
|
if swap.is_reverse: # successful reverse swap
|
||||||
|
locktime = 0
|
||||||
|
# preimage will be set in sign_tx
|
||||||
|
else: # timing out forward swap
|
||||||
|
locktime = swap.locktime
|
||||||
|
tx = create_claim_tx(
|
||||||
|
txin=txin,
|
||||||
|
witness_script=swap.redeem_script,
|
||||||
|
address=swap.receive_address,
|
||||||
|
amount_sat=amount_sat,
|
||||||
|
locktime=locktime,
|
||||||
|
)
|
||||||
|
cls.sign_tx(tx, swap)
|
||||||
|
return tx
|
||||||
|
|
||||||
def max_amount_forward_swap(self) -> Optional[int]:
|
def max_amount_forward_swap(self) -> Optional[int]:
|
||||||
""" returns None if we cannot swap """
|
""" returns None if we cannot swap """
|
||||||
max_swap_amt_ln = self.get_max_amount()
|
max_swap_amt_ln = self.get_max_amount()
|
||||||
|
|||||||
78
electrum/tests/test_sswaps.py
Normal file
78
electrum/tests/test_sswaps.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from electrum import SimpleConfig
|
||||||
|
from electrum.util import bfh
|
||||||
|
from electrum.transaction import PartialTxInput, TxOutpoint
|
||||||
|
from electrum.submarine_swaps import SwapManager, SwapData
|
||||||
|
|
||||||
|
from . import TestCaseForTestnet
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwapTxs(TestCaseForTestnet):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
||||||
|
self.config.set_key('dynamic_fees', False)
|
||||||
|
self.config.set_key('fee_per_kb', 1000)
|
||||||
|
|
||||||
|
def test_claim_tx_for_successful_reverse_swap(self):
|
||||||
|
swap_data = SwapData(
|
||||||
|
is_reverse=True,
|
||||||
|
locktime=2420532,
|
||||||
|
onchain_amount=198694,
|
||||||
|
lightning_amount=200000,
|
||||||
|
redeem_script=bytes.fromhex('8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac'),
|
||||||
|
preimage=bytes.fromhex('f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a36'),
|
||||||
|
prepay_hash=None,
|
||||||
|
privkey=bytes.fromhex('58fd0018a9a2737d1d6b81d380df96bf0c858473a9592015508a270a7c9b1d8d'),
|
||||||
|
lockup_address='tb1q2pvugjl4w56rqw4c7zg0q6mmmev0t5jjy3qzg7sl766phh9fxjxsrtl77t',
|
||||||
|
receive_address='tb1ql0adrj58g88xgz375yct63rclhv29hv03u0mel',
|
||||||
|
funding_txid='897eea7f53e917323e7472d7a2e3099173f7836c57f1b6850f5cbdfe8085dbf9',
|
||||||
|
spending_txid=None,
|
||||||
|
is_redeemed=False,
|
||||||
|
)
|
||||||
|
txin = PartialTxInput(
|
||||||
|
prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0),
|
||||||
|
)
|
||||||
|
txin._trusted_value_sats = swap_data.onchain_amount
|
||||||
|
txin._trusted_address = swap_data.lockup_address
|
||||||
|
tx = SwapManager._create_and_sign_claim_tx(
|
||||||
|
txin=txin,
|
||||||
|
swap=swap_data,
|
||||||
|
config=self.config,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019e07030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f034730440220156d62534a4e8247eef6bb185c89c4013353c017e45d41ce634976b9d7122c6202202ddb593983fd789cf2166038411425c119d087bc37ec7f8b51bebf603e428fbb0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000",
|
||||||
|
str(tx)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_claim_tx_for_timing_out_forward_swap(self):
|
||||||
|
swap_data = SwapData(
|
||||||
|
is_reverse=False,
|
||||||
|
locktime=2420537,
|
||||||
|
onchain_amount=130000,
|
||||||
|
lightning_amount=129014,
|
||||||
|
redeem_script=bytes.fromhex('a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac'),
|
||||||
|
preimage=bytes.fromhex('116f62c3283e4eb0b947a9cb672f1de7321d2c2373d12cd010500adffc32b1f2'),
|
||||||
|
prepay_hash=None,
|
||||||
|
privkey=bytes.fromhex('8d30dead21f5a7a6eeab7456a9a9d449511e942abef9302153cfff84e436614c'),
|
||||||
|
lockup_address='tb1qte2qwev6qvmrhsddac82tnskmjg02ntn73xqg2rjt0qx2xpz693sw2ljzg',
|
||||||
|
receive_address='tb1qj76twx886pkfcs7d808n0yzsgxm33wqlwe0dt0',
|
||||||
|
funding_txid='08ecdcb19ab38fc1288c97da546b8c90549be2348ef306f476dcf6e505158706',
|
||||||
|
spending_txid=None,
|
||||||
|
is_redeemed=False,
|
||||||
|
)
|
||||||
|
txin = PartialTxInput(
|
||||||
|
prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0),
|
||||||
|
)
|
||||||
|
txin._trusted_value_sats = swap_data.onchain_amount
|
||||||
|
txin._trusted_address = swap_data.lockup_address
|
||||||
|
tx = SwapManager._create_and_sign_claim_tx(
|
||||||
|
txin=txin,
|
||||||
|
swap=swap_data,
|
||||||
|
config=self.config,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff0148fb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f034730440220254e054fc195801aca3d62641a0f27d888f44d1dd66760ae5c3418502e82c141022014305da98daa27d665310115845d2fa6d4dc612d910a186db2624aa558bff9fe010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400",
|
||||||
|
str(tx)
|
||||||
|
)
|
||||||
|
|
||||||
@@ -144,6 +144,10 @@ class NoDynamicFeeEstimates(Exception):
|
|||||||
return _('Dynamic fee estimates not available')
|
return _('Dynamic fee estimates not available')
|
||||||
|
|
||||||
|
|
||||||
|
class BelowDustLimit(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidPassword(Exception):
|
class InvalidPassword(Exception):
|
||||||
def __init__(self, message: Optional[str] = None):
|
def __init__(self, message: Optional[str] = None):
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|||||||
Reference in New Issue
Block a user