Browse Source

swaps: small refactor and add unit tests for claim tx

master
SomberNight 3 years ago
parent
commit
8a4c06b692
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 66
      electrum/submarine_swaps.py
  2. 78
      electrum/tests/test_sswaps.py
  3. 4
      electrum/util.py

66
electrum/submarine_swaps.py

@ -14,7 +14,7 @@ from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script
is_segwit_address, construct_witness)
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint
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 .bitcoin import dust_threshold
from .logging import Logger
@ -29,6 +29,7 @@ if TYPE_CHECKING:
from .wallet import Abstract_Wallet
from .lnwatcher import LNWalletWatcher
from .lnworker import LNWallet
from .simple_config import SimpleConfig
API_URL_MAINNET = 'https://swaps.electrum.org/api'
@ -116,7 +117,6 @@ def create_claim_tx(
*,
txin: PartialTxInput,
witness_script: bytes,
preimage: Union[bytes, int], # 0 if timing out forward-swap
address: str,
amount_sat: int,
locktime: int,
@ -124,11 +124,12 @@ def create_claim_tx(
"""Create tx to either claim successful reverse-swap,
or to get refunded for timed-out forward-swap.
"""
assert txin.address is not None
if is_segwit_address(txin.address):
txin.script_type = 'p2wsh'
txin.script_sig = b''
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.script_sig = bytes.fromhex(push_script(txin.redeem_script.hex()))
txin.witness_script = witness_script
@ -217,33 +218,21 @@ class SwapManager(Logger):
if not swap.is_reverse and delta < 0:
# too early for refund
return
# FIXME the mining fee should depend on swap.is_reverse.
# the txs are not the same size...
amount_sat = txin.value_sats() - self.get_claim_fee()
if amount_sat < dust_threshold():
try:
tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
except BelowDustLimit:
self.logger.info('utxo value below dust threshold')
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.wallet.adb.add_transaction(tx)
swap.spending_txid = tx.txid()
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]:
# for history
@ -650,7 +639,8 @@ class SwapManager(Logger):
witness = [sig_dummy, preimage, witness_script]
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
witness_script = swap.redeem_script
txin = tx.inputs()[0]
@ -663,6 +653,34 @@ class SwapManager(Logger):
witness = [sig, preimage, witness_script]
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]:
""" returns None if we cannot swap """
max_swap_amt_ln = self.get_max_amount()

78
electrum/tests/test_sswaps.py

@ -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)
)

4
electrum/util.py

@ -144,6 +144,10 @@ class NoDynamicFeeEstimates(Exception):
return _('Dynamic fee estimates not available')
class BelowDustLimit(Exception):
pass
class InvalidPassword(Exception):
def __init__(self, message: Optional[str] = None):
self.message = message

Loading…
Cancel
Save