Browse Source

Merge pull request #8489 from spesmilo/swapserver_plugin

Swapserver plugin
master
ThomasV 2 years ago committed by GitHub
parent
commit
c16074c047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      electrum/commands.py
  2. 5
      electrum/lnworker.py
  3. 15
      electrum/plugins/swapserver/__init__.py
  4. 31
      electrum/plugins/swapserver/cmdline.py
  5. 31
      electrum/plugins/swapserver/qt.py
  6. 136
      electrum/plugins/swapserver/server.py
  7. 58
      electrum/plugins/swapserver/swapserver.py
  8. 5
      electrum/simple_config.py
  9. 223
      electrum/submarine_swaps.py
  10. 6
      electrum/tests/regtest.py
  11. 59
      electrum/tests/regtest/regtest.sh

8
electrum/commands.py

@ -1318,22 +1318,22 @@ class Commands:
await sm.get_pairs()
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
success = None
funding_txid = None
elif lightning_amount == 'dryrun':
await sm.get_pairs()
onchain_amount_sat = satoshis(onchain_amount)
lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
success = None
funding_txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
claim_fee = sm.get_claim_fee()
onchain_amount_sat = satoshis(onchain_amount) + claim_fee
success = await wallet.lnworker.swap_manager.reverse_swap(
funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
lightning_amount_sat=lightning_amount_sat,
expected_onchain_amount_sat=onchain_amount_sat,
)
return {
'success': success,
'funding_txid': funding_txid,
'lightning_amount': format_satoshis(lightning_amount_sat),
'onchain_amount': format_satoshis(onchain_amount_sat),
}

5
electrum/lnworker.py

@ -2704,3 +2704,8 @@ class LNWallet(LNWorker):
self._channel_backups[bfh(channel_id)] = cb
util.trigger_callback('channels_updated', self.wallet)
self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address())
def fail_trampoline_forwarding(self, payment_key):
""" use this to fail htlcs received for hold invoices"""
e = OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'')
self.trampoline_forwarding_failures[payment_key] = e

15
electrum/plugins/swapserver/__init__.py

@ -0,0 +1,15 @@
from electrum.i18n import _
fullname = _('SwapServer')
description = """
Submarine swap server for an Electrum daemon.
Example setup:
electrum -o setconfig use_swapserver True
electrum -o setconfig swapserver_address localhost:5455
electrum daemon -v
"""
available_for = ['qt', 'cmdline']

31
electrum/plugins/swapserver/cmdline.py

@ -0,0 +1,31 @@
#!/usr/bin/env python
#
# Electrum - Lightweight Bitcoin Client
# Copyright (C) 2023 The Electrum Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .swapserver import SwapServerPlugin
class Plugin(SwapServerPlugin):
pass

31
electrum/plugins/swapserver/qt.py

@ -0,0 +1,31 @@
#!/usr/bin/env python
#
# Electrum - Lightweight Bitcoin Client
# Copyright (C) 2023 The Electrum Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .swapserver import SwapServerPlugin
class Plugin(SwapServerPlugin):
pass

136
electrum/plugins/swapserver/server.py

@ -0,0 +1,136 @@
import os
import asyncio
import attr
import random
from collections import defaultdict
from aiohttp import ClientResponse
from aiohttp import web, client_exceptions
from aiorpcx import timeout_after, TaskTimeout, ignore_after
from aiorpcx import NetAddress
from electrum.util import log_exceptions, ignore_exceptions
from electrum.logging import Logger
from electrum.util import EventListener, event_listener
from electrum.invoices import PR_PAID, PR_EXPIRED
class SwapServer(Logger, EventListener):
"""
public API:
- getpairs
- createswap
"""
WWW_DIR = os.path.join(os.path.dirname(__file__), 'www')
def __init__(self, config, wallet):
Logger.__init__(self)
self.config = config
self.wallet = wallet
self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS)
self.register_callbacks() # eventlistener
self.pending = defaultdict(asyncio.Event)
self.pending_msg = {}
@ignore_exceptions
@log_exceptions
async def run(self):
app = web.Application()
app.add_routes([web.get('/api/getpairs', self.get_pairs)])
app.add_routes([web.post('/api/createswap', self.create_swap)])
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context())
await site.start()
self.logger.info(f"now running and listening. addr={self.addr}")
async def get_pairs(self, r):
sm = self.wallet.lnworker.swap_manager
sm.init_pairs()
pairs = {
"info": [],
"warnings": [],
"pairs": {
"BTC/BTC": {
"rate": 1,
"limits": {
"maximal": sm._max_amount,
"minimal": sm._min_amount,
"maximalZeroConf": {
"baseAsset": 0,
"quoteAsset": 0
}
},
"fees": {
"percentage": 0.5,
"minerFees": {
"baseAsset": {
"normal": sm.normal_fee,
"reverse": {
"claim": sm.claim_fee,
"lockup": sm.lockup_fee
}
},
"quoteAsset": {
"normal": sm.normal_fee,
"reverse": {
"claim": sm.claim_fee,
"lockup": sm.lockup_fee
}
}
}
}
}
}
}
return web.json_response(pairs)
async def create_swap(self, r):
sm = self.wallet.lnworker.swap_manager
sm.init_pairs()
request = await r.json()
req_type = request['type']
assert request['pairId'] == 'BTC/BTC'
if req_type == 'reversesubmarine':
lightning_amount_sat=request['invoiceAmount']
payment_hash=bytes.fromhex(request['preimageHash'])
their_pubkey=bytes.fromhex(request['claimPublicKey'])
assert len(payment_hash) == 32
assert len(their_pubkey) == 33
swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap(
lightning_amount_sat=lightning_amount_sat,
payment_hash=payment_hash,
their_pubkey=their_pubkey
)
response = {
'id': payment_hash.hex(),
'invoice': invoice,
'minerFeeInvoice': prepay_invoice,
'lockupAddress': swap.lockup_address,
'redeemScript': swap.redeem_script.hex(),
'timeoutBlockHeight': swap.locktime,
"onchainAmount": swap.onchain_amount,
}
elif req_type == 'submarine':
their_invoice=request['invoice']
their_pubkey=bytes.fromhex(request['refundPublicKey'])
assert len(their_pubkey) == 33
swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap(
invoice=their_invoice,
their_pubkey=their_pubkey
)
response = {
"id": payment_hash.hex(),
"acceptZeroConf": False,
"expectedAmount": swap.onchain_amount,
"timeoutBlockHeight": swap.locktime,
"address": swap.lockup_address,
"redeemScript": swap.redeem_script.hex()
}
else:
raise Exception('unsupported request type:' + req_type)
return web.json_response(response)

58
electrum/plugins/swapserver/swapserver.py

@ -0,0 +1,58 @@
#!/usr/bin/env python
#
# Electrum - Lightweight Bitcoin Client
# Copyright (C) 2023 The Electrum Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import os
import random
from electrum.plugin import BasePlugin, hook
from electrum.util import log_exceptions, ignore_exceptions
from electrum import ecc
from .server import SwapServer
class SwapServerPlugin(BasePlugin):
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
self.config = config
self.server = None
@hook
def daemon_wallet_loaded(self, daemon, wallet):
# we use the first wallet loaded
if self.server is not None:
return
if self.config.get('offline'):
return
self.server = SwapServer(self.config, wallet)
sm = wallet.lnworker.swap_manager
jobs = [
sm.pay_pending_invoices(),
self.server.run(),
]
asyncio.run_coroutine_threadsafe(daemon._run(jobs=jobs), daemon.asyncio_loop)

5
electrum/simple_config.py

@ -966,7 +966,8 @@ class SimpleConfig(Logger):
# submarine swap server
SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str)
SWAPSERVER_URL_TESTNET = ConfigVar('swapserver_url_testnet', default='https://swaps.electrum.org/testnet', type_=str)
SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='https://localhost/api', type_=str)
SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='http://localhost:5455/api', type_=str)
TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool)
# connect to remote WT
WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool)
WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)
@ -981,6 +982,8 @@ class SimpleConfig(Logger):
PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str)
PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool)
SWAPSERVER_ADDRESS = ConfigVar('swapserver_address', default='localhost:5455', type_=str)
PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)

223
electrum/submarine_swaps.py

@ -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,6 +213,7 @@ 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 spent_height is not None:
swap.spending_txid = txin.spent_txid
@ -200,11 +227,46 @@ class SwapManager(Logger):
tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)
self.logger.info(f'broadcasting tx {txin.spent_txid}')
await self.network.broadcast_transaction(tx)
# already in mempool
else:
# spending tx is in mempool
pass
if not swap.is_reverse:
if swap.preimage is None and spent_height is not None:
# extract the preimage, add it to lnwatcher
tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)
preimage = tx.inputs()[0].witness_elements()[1]
if sha256(preimage) == swap.payment_hash:
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
else:
# refund tx
if spent_height > 0:
self.logger.info(f'found confirmed refund')
payment_secret = self.lnworker.get_payment_secret(swap.payment_hash)
payment_key = swap.payment_hash + payment_secret
self.lnworker.fail_trampoline_forwarding(payment_key)
if delta < 0:
# too early for refund
continue
else:
if 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 self.network.config.TEST_SWAPSERVER_REFUND:
# for testing: do not create claim tx
continue
if spent_height is not None:
continue
if not swap.is_reverse and delta < 0:
# too early for refund
return
try:
tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
except BelowDustLimit:
@ -215,11 +277,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 +299,86 @@ 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))
prepay_invoice = None
else:
onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True)
prepay_amount_sat = self.get_claim_fee() * 2
main_amount_sat = lightning_amount_sat - prepay_amount_sat
lnaddr, invoice = self.lnworker.get_bolt11_invoice(
payment_hash=payment_hash,
amount_msat=main_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, main_amount_sat)
self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback, 60*60*24)
prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000)
_, prepay_invoice = self.lnworker.get_bolt11_invoice(
payment_hash=prepay_hash,
amount_msat=prepay_amount_sat * 1000,
message='prepay',
expiry=3600 * 24,
fallback_address=None,
channels=None,
)
self.lnworker.bundle_payments([payment_hash, prepay_hash])
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, prepay_invoice
async def normal_swap(
self,
*,
@ -304,18 +449,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 +458,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 +466,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 +552,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 +586,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)
@ -444,13 +596,12 @@ class SwapManager(Logger):
asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10))
# we return if we detect funding
async def wait_for_funding(swap):
while swap.spending_txid is None:
while swap.funding_txid is None:
await asyncio.sleep(1)
# initiate main payment
tasks = [asyncio.create_task(self.lnworker.pay_invoice(invoice, attempts=10, channels=channels)), asyncio.create_task(wait_for_funding(swap))]
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
success = swap.spending_txid is not None
return success
return swap.funding_txid
def _add_or_reindex_swap(self, swap: SwapData) -> None:
if swap.payment_hash.hex() not in self.swaps:
@ -459,6 +610,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 +639,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 +811,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

6
electrum/tests/regtest.py

@ -47,6 +47,12 @@ class TestLightningAB(TestLightning):
def test_collaborative_close(self):
self.run_shell(['collaborative_close'])
def test_swapserver_success(self):
self.run_shell(['swapserver_success'])
def test_swapserver_refund(self):
self.run_shell(['swapserver_refund'])
def test_backup(self):
self.run_shell(['backup'])

59
electrum/tests/regtest/regtest.sh

@ -15,6 +15,19 @@ function new_blocks()
$bitcoin_cli generatetoaddress $1 $($bitcoin_cli getnewaddress) > /dev/null
}
function wait_until_htlcs_settled()
{
msg="wait until $1's local_unsettled_sent is zero"
cmd="./run_electrum --regtest -D /tmp/$1"
while unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent') && [ $unsettled != "0" ]; do
sleep 1
msg="$msg."
printf "$msg\r"
done
printf "\n"
}
function wait_for_balance()
{
msg="wait until $1's balance reaches $2"
@ -83,10 +96,10 @@ if [[ $1 == "init" ]]; then
# alice is funded, bob is listening
if [[ $2 == "bob" ]]; then
$bob setconfig --offline lightning_listen localhost:9735
else
echo "funding $2"
$bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1
$bob setconfig --offline use_swapserver true
fi
echo "funding $2"
$bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1
fi
@ -170,6 +183,46 @@ if [[ $1 == "collaborative_close" ]]; then
fi
if [[ $1 == "swapserver_success" ]]; then
wait_for_balance alice 1
echo "alice opens channel"
bob_node=$($bob nodeid)
channel=$($alice open_channel $bob_node 0.15)
new_blocks 3
wait_until_channel_open alice
echo "alice initiates swap"
dryrun=$($alice reverse_swap 0.02 dryrun)
onchain_amount=$(echo $dryrun| jq -r ".onchain_amount")
swap=$($alice reverse_swap 0.02 $onchain_amount)
echo $swap | jq
funding_txid=$(echo $swap| jq -r ".funding_txid")
new_blocks 1
wait_until_spent $funding_txid 0
wait_until_htlcs_settled alice
fi
if [[ $1 == "swapserver_refund" ]]; then
$alice setconfig test_swapserver_refund true
wait_for_balance alice 1
echo "alice opens channel"
bob_node=$($bob nodeid)
channel=$($alice open_channel $bob_node 0.15)
new_blocks 3
wait_until_channel_open alice
echo "alice initiates swap"
dryrun=$($alice reverse_swap 0.02 dryrun)
onchain_amount=$(echo $dryrun| jq -r ".onchain_amount")
swap=$($alice reverse_swap 0.02 $onchain_amount)
echo $swap | jq
funding_txid=$(echo $swap| jq -r ".funding_txid")
new_blocks 140
wait_until_spent $funding_txid 0
new_blocks 1
wait_until_htlcs_settled alice
fi
if [[ $1 == "extract_preimage" ]]; then
# instead of settling bob will broadcast
$bob enable_htlc_settle false

Loading…
Cancel
Save