submarine swap server plugin:
- hold invoices - uses the same web API as the Boltz backend
This commit is contained in:
@@ -33,7 +33,10 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
|
||||
CLAIM_FEE_SIZE = 136
|
||||
LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs
|
||||
|
||||
MIN_LOCKTIME_DELTA = 60
|
||||
|
||||
WITNESS_TEMPLATE_SWAP = [
|
||||
opcodes.OP_HASH160,
|
||||
@@ -102,14 +105,11 @@ class SwapData(StoredObject):
|
||||
is_redeemed = attr.ib(type=bool)
|
||||
|
||||
_funding_prevout = None # type: Optional[TxOutpoint] # for RBF
|
||||
__payment_hash = None
|
||||
_payment_hash = None
|
||||
|
||||
@property
|
||||
def payment_hash(self) -> bytes:
|
||||
if self.__payment_hash is None:
|
||||
self.__payment_hash = sha256(self.preimage)
|
||||
return self.__payment_hash
|
||||
|
||||
return self._payment_hash
|
||||
|
||||
def create_claim_tx(
|
||||
*,
|
||||
@@ -139,6 +139,7 @@ class SwapManager(Logger):
|
||||
Logger.__init__(self)
|
||||
self.normal_fee = 0
|
||||
self.lockup_fee = 0
|
||||
self.claim_fee = 0 # part of the boltz prococol, not used by Electrum
|
||||
self.percentage = 0
|
||||
self._min_amount = None
|
||||
self._max_amount = None
|
||||
@@ -149,6 +150,7 @@ class SwapManager(Logger):
|
||||
self._swaps_by_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData]
|
||||
self._swaps_by_lockup_address = {} # type: Dict[str, SwapData]
|
||||
for payment_hash, swap in self.swaps.items():
|
||||
swap._payment_hash = bytes.fromhex(payment_hash)
|
||||
self._add_or_reindex_swap(swap)
|
||||
|
||||
self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash
|
||||
@@ -171,6 +173,30 @@ class SwapManager(Logger):
|
||||
continue
|
||||
self.add_lnwatcher_callback(swap)
|
||||
|
||||
async def pay_pending_invoices(self):
|
||||
# for server
|
||||
self.invoices_to_pay = set()
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
for key in list(self.invoices_to_pay):
|
||||
swap = self.swaps.get(key)
|
||||
if not swap:
|
||||
continue
|
||||
invoice = self.wallet.get_invoice(key)
|
||||
if not invoice:
|
||||
continue
|
||||
current_height = self.network.get_local_height()
|
||||
delta = swap.locktime - current_height
|
||||
if delta <= MIN_LOCKTIME_DELTA:
|
||||
# fixme: should consider cltv of ln payment
|
||||
self.logger.info(f'locktime too close {key}')
|
||||
continue
|
||||
success, log = await self.lnworker.pay_invoice(invoice.lightning_invoice, attempts=1)
|
||||
if not success:
|
||||
self.logger.info(f'failed to pay invoice {key}')
|
||||
continue
|
||||
self.invoices_to_pay.remove(key)
|
||||
|
||||
@log_exceptions
|
||||
async def _claim_swap(self, swap: SwapData) -> None:
|
||||
assert self.network
|
||||
@@ -187,9 +213,31 @@ class SwapManager(Logger):
|
||||
swap.funding_txid = txin.prevout.txid.hex()
|
||||
swap._funding_prevout = txin.prevout
|
||||
self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint
|
||||
funding_conf = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex()).conf
|
||||
spent_height = txin.spent_height
|
||||
|
||||
if swap.is_reverse and swap.preimage is None:
|
||||
if funding_conf <= 0:
|
||||
continue
|
||||
preimage = self.lnworker.get_preimage(swap.payment_hash)
|
||||
if preimage is None:
|
||||
self.invoices_to_pay.add(swap.payment_hash.hex())
|
||||
continue
|
||||
swap.preimage = preimage
|
||||
|
||||
if spent_height is not None:
|
||||
swap.spending_txid = txin.spent_txid
|
||||
if not swap.is_reverse and swap.preimage is None:
|
||||
# we need to extract the preimage, add it to lnwatcher
|
||||
#
|
||||
tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)
|
||||
preimage = tx.inputs()[0].witness_elements()[1]
|
||||
assert swap.payment_hash == sha256(preimage)
|
||||
swap.preimage = preimage
|
||||
self.logger.info(f'found preimage: {preimage.hex()}')
|
||||
self.lnworker.preimages[swap.payment_hash.hex()] = preimage.hex()
|
||||
# note: we must check the payment secret before we broadcast the funding tx
|
||||
|
||||
if spent_height > 0:
|
||||
if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY:
|
||||
self.logger.info(f'stop watching swap {swap.lockup_address}')
|
||||
@@ -205,6 +253,10 @@ class SwapManager(Logger):
|
||||
if not swap.is_reverse and delta < 0:
|
||||
# too early for refund
|
||||
return
|
||||
#
|
||||
if swap.is_reverse and swap.preimage is None:
|
||||
self.logger.info('preimage not available yet')
|
||||
continue
|
||||
try:
|
||||
tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
|
||||
except BelowDustLimit:
|
||||
@@ -215,11 +267,14 @@ class SwapManager(Logger):
|
||||
swap.spending_txid = tx.txid()
|
||||
|
||||
def get_claim_fee(self):
|
||||
return self._get_claim_fee(config=self.wallet.config)
|
||||
return self.get_fee(CLAIM_FEE_SIZE)
|
||||
|
||||
def get_fee(self, size):
|
||||
return self._get_fee(size=size, 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_fee(cls, *, size, config: 'SimpleConfig'):
|
||||
return config.estimate_fee(size, allow_fallback_to_static_rates=True)
|
||||
|
||||
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
|
||||
# for history
|
||||
@@ -234,6 +289,73 @@ class SwapManager(Logger):
|
||||
callback = lambda: self._claim_swap(swap)
|
||||
self.lnwatcher.add_callback(swap.lockup_address, callback)
|
||||
|
||||
async def hold_invoice_callback(self, payment_hash):
|
||||
key = payment_hash.hex()
|
||||
if key in self.swaps:
|
||||
swap = self.swaps[key]
|
||||
if swap.funding_txid is None:
|
||||
await self.start_normal_swap(swap, None, None)
|
||||
|
||||
def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoice=None, their_pubkey=None):
|
||||
from .bitcoin import construct_script
|
||||
from .crypto import ripemd
|
||||
from .lnaddr import lndecode
|
||||
from .invoices import Invoice
|
||||
|
||||
locktime = self.network.get_local_height() + 140
|
||||
privkey = os.urandom(32)
|
||||
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
|
||||
is_reverse_for_server = (invoice is not None)
|
||||
if is_reverse_for_server:
|
||||
# client is doing a normal swap
|
||||
lnaddr = lndecode(invoice)
|
||||
payment_hash = lnaddr.paymenthash
|
||||
lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int
|
||||
onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False)
|
||||
redeem_script = construct_script(
|
||||
WITNESS_TEMPLATE_SWAP,
|
||||
{1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey}
|
||||
)
|
||||
self.wallet.save_invoice(Invoice.from_bech32(invoice))
|
||||
else:
|
||||
onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True)
|
||||
lnaddr, invoice = self.lnworker.get_bolt11_invoice(
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=lightning_amount_sat * 1000,
|
||||
message='Submarine swap',
|
||||
expiry=3600 * 24,
|
||||
fallback_address=None,
|
||||
channels=None,
|
||||
)
|
||||
# add payment info to lnworker
|
||||
self.lnworker.add_payment_info_for_hold_invoice(payment_hash, lightning_amount_sat)
|
||||
self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback, 60*60*24)
|
||||
redeem_script = construct_script(
|
||||
WITNESS_TEMPLATE_REVERSE_SWAP,
|
||||
{1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey}
|
||||
)
|
||||
lockup_address = script_to_p2wsh(redeem_script)
|
||||
receive_address = self.wallet.get_receiving_address()
|
||||
swap = SwapData(
|
||||
redeem_script = bytes.fromhex(redeem_script),
|
||||
locktime = locktime,
|
||||
privkey = privkey,
|
||||
preimage = None,
|
||||
prepay_hash = None,
|
||||
lockup_address = lockup_address,
|
||||
onchain_amount = onchain_amount_sat,
|
||||
receive_address = receive_address,
|
||||
lightning_amount = lightning_amount_sat,
|
||||
is_reverse = is_reverse_for_server,
|
||||
is_redeemed = False,
|
||||
funding_txid = None,
|
||||
spending_txid = None,
|
||||
)
|
||||
swap._payment_hash = payment_hash
|
||||
self._add_or_reindex_swap(swap)
|
||||
self.add_lnwatcher_callback(swap)
|
||||
return swap, payment_hash, invoice
|
||||
|
||||
async def normal_swap(
|
||||
self,
|
||||
*,
|
||||
@@ -304,18 +426,6 @@ class SwapManager(Logger):
|
||||
# verify that they are not locking up funds for more than a day
|
||||
if locktime - self.network.get_local_height() >= 144:
|
||||
raise Exception("fswap check failed: locktime too far in future")
|
||||
# create funding tx
|
||||
# note: rbf must not decrease payment
|
||||
# this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output
|
||||
funding_output = PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)
|
||||
if tx is None:
|
||||
tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password)
|
||||
else:
|
||||
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), expected_onchain_amount_sat)
|
||||
tx.outputs().remove(dummy_output)
|
||||
tx.add_outputs([funding_output])
|
||||
tx.set_rbf(True)
|
||||
self.wallet.sign_transaction(tx, password)
|
||||
# save swap data in wallet in case we need a refund
|
||||
receive_address = self.wallet.get_receiving_address()
|
||||
swap = SwapData(
|
||||
@@ -325,7 +435,7 @@ class SwapManager(Logger):
|
||||
preimage = preimage,
|
||||
prepay_hash = None,
|
||||
lockup_address = lockup_address,
|
||||
onchain_amount = expected_onchain_amount_sat,
|
||||
onchain_amount = onchain_amount,
|
||||
receive_address = receive_address,
|
||||
lightning_amount = lightning_amount_sat,
|
||||
is_reverse = False,
|
||||
@@ -333,10 +443,28 @@ class SwapManager(Logger):
|
||||
funding_txid = None,
|
||||
spending_txid = None,
|
||||
)
|
||||
swap._payment_hash = payment_hash
|
||||
self._add_or_reindex_swap(swap)
|
||||
self.add_lnwatcher_callback(swap)
|
||||
return await self.start_normal_swap(swap, tx, password)
|
||||
|
||||
@log_exceptions
|
||||
async def start_normal_swap(self, swap, tx, password):
|
||||
# create funding tx
|
||||
# note: rbf must not decrease payment
|
||||
# this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output
|
||||
funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount)
|
||||
if tx is None:
|
||||
tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password)
|
||||
else:
|
||||
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), swap.onchain_amount)
|
||||
tx.outputs().remove(dummy_output)
|
||||
tx.add_outputs([funding_output])
|
||||
tx.set_rbf(True)
|
||||
self.wallet.sign_transaction(tx, password)
|
||||
await self.network.broadcast_transaction(tx)
|
||||
return tx.txid()
|
||||
swap.funding_txid = tx.txid()
|
||||
return swap.funding_txid
|
||||
|
||||
async def reverse_swap(
|
||||
self,
|
||||
@@ -401,7 +529,7 @@ class SwapManager(Logger):
|
||||
raise Exception(f"rswap check failed: onchain_amount is less than what we expected: "
|
||||
f"{onchain_amount} < {expected_onchain_amount_sat}")
|
||||
# verify that we will have enough time to get our tx confirmed
|
||||
if locktime - self.network.get_local_height() <= 60:
|
||||
if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA:
|
||||
raise Exception("rswap check failed: locktime too close")
|
||||
# verify invoice preimage_hash
|
||||
lnaddr = self.lnworker._check_invoice(invoice)
|
||||
@@ -435,6 +563,7 @@ class SwapManager(Logger):
|
||||
funding_txid = None,
|
||||
spending_txid = None,
|
||||
)
|
||||
swap._payment_hash = preimage_hash
|
||||
self._add_or_reindex_swap(swap)
|
||||
# add callback to lnwatcher
|
||||
self.add_lnwatcher_callback(swap)
|
||||
@@ -459,6 +588,15 @@ class SwapManager(Logger):
|
||||
self._swaps_by_funding_outpoint[swap._funding_prevout] = swap
|
||||
self._swaps_by_lockup_address[swap.lockup_address] = swap
|
||||
|
||||
def init_pairs(self) -> None:
|
||||
""" for server """
|
||||
self.percentage = 0.5
|
||||
self._min_amount = 20000
|
||||
self._max_amount = 10000000
|
||||
self.normal_fee = self.get_fee(CLAIM_FEE_SIZE)
|
||||
self.lockup_fee = self.get_fee(LOCKUP_FEE_SIZE)
|
||||
self.claim_fee = self.get_fee(CLAIM_FEE_SIZE)
|
||||
|
||||
async def get_pairs(self) -> None:
|
||||
"""Might raise SwapServerError."""
|
||||
from .network import Network
|
||||
@@ -479,6 +617,7 @@ class SwapManager(Logger):
|
||||
self.percentage = fees['percentage']
|
||||
self.normal_fee = fees['minerFees']['baseAsset']['normal']
|
||||
self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup']
|
||||
self.claim_fee = fees['minerFees']['baseAsset']['reverse']['claim']
|
||||
limits = pairs['pairs']['BTC/BTC']['limits']
|
||||
self._min_amount = limits['minimal']
|
||||
self._max_amount = limits['maximal']
|
||||
@@ -650,7 +789,7 @@ class SwapManager(Logger):
|
||||
) -> 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)
|
||||
amount_sat = txin.value_sats() - cls._get_fee(size=CLAIM_FEE_SIZE, config=config)
|
||||
if amount_sat < dust_threshold():
|
||||
raise BelowDustLimit()
|
||||
if swap.is_reverse: # successful reverse swap
|
||||
|
||||
Reference in New Issue
Block a user