submarine swap server plugin:
- hold invoices - uses the same web API as the Boltz backend
This commit is contained in:
6
electrum/plugins/swapserver/__init__.py
Normal file
6
electrum/plugins/swapserver/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from electrum.i18n import _
|
||||
|
||||
fullname = _('SwapServer')
|
||||
description = ''
|
||||
|
||||
available_for = ['qt', 'cmdline']
|
||||
31
electrum/plugins/swapserver/cmdline.py
Normal file
31
electrum/plugins/swapserver/cmdline.py
Normal file
@@ -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
Normal file
31
electrum/plugins/swapserver/qt.py
Normal file
@@ -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
|
||||
|
||||
138
electrum/plugins/swapserver/server.py
Normal file
138
electrum/plugins/swapserver/server.py
Normal file
@@ -0,0 +1,138 @@
|
||||
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):
|
||||
self.root = '/root'
|
||||
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": {
|
||||
"hash": "dfe692a026d6964601bfd79703611af333d1d5aa49ef5fedd288f5a620fced60",
|
||||
"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 = 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': None,
|
||||
'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 = 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
Normal file
58
electrum/plugins/swapserver/swapserver.py
Normal file
@@ -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)
|
||||
@@ -966,7 +966,7 @@ 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)
|
||||
# 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 +981,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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,6 +47,9 @@ class TestLightningAB(TestLightning):
|
||||
def test_collaborative_close(self):
|
||||
self.run_shell(['collaborative_close'])
|
||||
|
||||
def test_submarine_swap(self):
|
||||
self.run_shell(['reverse_swap'])
|
||||
|
||||
def test_backup(self):
|
||||
self.run_shell(['backup'])
|
||||
|
||||
|
||||
@@ -83,10 +83,11 @@ 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
|
||||
#else
|
||||
fi
|
||||
echo "funding $2"
|
||||
$bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -170,6 +171,24 @@ if [[ $1 == "collaborative_close" ]]; then
|
||||
fi
|
||||
|
||||
|
||||
if [[ $1 == "reverse_swap" ]]; 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)
|
||||
echo $dryrun | jq
|
||||
onchain_amount=$(echo $dryrun| jq -r ".onchain_amount")
|
||||
$alice reverse_swap 0.02 $onchain_amount
|
||||
new_blocks 1
|
||||
sleep 1
|
||||
new_blocks 1
|
||||
fi
|
||||
|
||||
|
||||
if [[ $1 == "extract_preimage" ]]; then
|
||||
# instead of settling bob will broadcast
|
||||
$bob enable_htlc_settle false
|
||||
|
||||
Reference in New Issue
Block a user