|
|
@ -1,3 +1,4 @@ |
|
|
|
import attr |
|
|
|
import asyncio |
|
|
|
import json |
|
|
|
import os |
|
|
@ -12,11 +13,14 @@ from .util import log_exceptions |
|
|
|
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY |
|
|
|
from .bitcoin import dust_threshold |
|
|
|
from .logging import Logger |
|
|
|
from .lnutil import hex_to_bytes |
|
|
|
from .json_db import StoredObject |
|
|
|
|
|
|
|
if TYPE_CHECKING: |
|
|
|
from .network import Network |
|
|
|
from .wallet import Abstract_Wallet |
|
|
|
|
|
|
|
|
|
|
|
API_URL = 'https://lightning.electrum.org/api' |
|
|
|
|
|
|
|
|
|
|
@ -56,6 +60,21 @@ WITNESS_TEMPLATE_REVERSE_SWAP = [ |
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
@attr.s |
|
|
|
class SwapData(StoredObject): |
|
|
|
is_reverse = attr.ib(type=bool) |
|
|
|
locktime = attr.ib(type=int) |
|
|
|
onchain_amount = attr.ib(type=int) |
|
|
|
lightning_amount = attr.ib(type=int) |
|
|
|
redeem_script = attr.ib(type=bytes, converter=hex_to_bytes) |
|
|
|
preimage = attr.ib(type=bytes, converter=hex_to_bytes) |
|
|
|
privkey = attr.ib(type=bytes, converter=hex_to_bytes) |
|
|
|
lockup_address = attr.ib(type=str) |
|
|
|
funding_txid = attr.ib(type=str) |
|
|
|
spending_txid = attr.ib(type=str) |
|
|
|
is_redeemed = attr.ib(type=bool) |
|
|
|
|
|
|
|
|
|
|
|
def create_claim_tx(txin, witness_script, preimage, privkey:bytes, address, amount_sat, locktime): |
|
|
|
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) |
|
|
|
if is_segwit_address(txin.address): |
|
|
@ -75,41 +94,39 @@ def create_claim_tx(txin, witness_script, preimage, privkey:bytes, address, amou |
|
|
|
return tx |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SwapManager(Logger): |
|
|
|
|
|
|
|
@log_exceptions |
|
|
|
async def _claim_swap(self, lockup_address, onchain_amount, redeem_script, preimage, privkey, locktime): |
|
|
|
async def _claim_swap(self, swap): |
|
|
|
if not self.lnwatcher.is_up_to_date(): |
|
|
|
return |
|
|
|
current_height = self.network.get_local_height() |
|
|
|
delta = current_height - locktime |
|
|
|
is_reverse = bool(preimage) |
|
|
|
if not is_reverse and delta < 0: |
|
|
|
delta = current_height - swap.locktime |
|
|
|
if not swap.is_reverse and delta < 0: |
|
|
|
# too early for refund |
|
|
|
return |
|
|
|
txos = self.lnwatcher.get_addr_outputs(lockup_address) |
|
|
|
swap = self.swaps[preimage.hex()] |
|
|
|
txos = self.lnwatcher.get_addr_outputs(swap.lockup_address) |
|
|
|
for txin in txos.values(): |
|
|
|
if preimage and txin._trusted_value_sats < onchain_amount: |
|
|
|
if swap.is_reverse and txin._trusted_value_sats < swap.onchain_amount: |
|
|
|
self.logger.info('amount too low, we should not reveal the preimage') |
|
|
|
continue |
|
|
|
spent_height = txin.spent_height |
|
|
|
if spent_height is not None: |
|
|
|
if spent_height > 0 and current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: |
|
|
|
self.logger.info(f'stop watching swap {lockup_address}') |
|
|
|
self.lnwatcher.remove_callback(lockup_address) |
|
|
|
swap['redeemed'] = True |
|
|
|
self.logger.info(f'stop watching swap {swap.lockup_address}') |
|
|
|
self.lnwatcher.remove_callback(swap.lockup_address) |
|
|
|
swap.is_redeemed = True |
|
|
|
continue |
|
|
|
amount_sat = txin._trusted_value_sats - self.get_tx_fee() |
|
|
|
if amount_sat < dust_threshold(): |
|
|
|
self.logger.info('utxo value below dust threshold') |
|
|
|
continue |
|
|
|
address = self.wallet.get_unused_address() |
|
|
|
tx = create_claim_tx(txin, redeem_script, preimage, privkey, address, amount_sat, locktime) |
|
|
|
preimage = swap.preimage if swap.is_reverse else 0 |
|
|
|
tx = create_claim_tx(txin, swap.redeem_script, preimage, swap.privkey, address, amount_sat, swap.locktime) |
|
|
|
await self.network.broadcast_transaction(tx) |
|
|
|
# save txid |
|
|
|
swap['claim_txid' if preimage else 'refund_txid'] = tx.txid() |
|
|
|
swap.spending_txid = tx.txid() |
|
|
|
|
|
|
|
def get_tx_fee(self): |
|
|
|
return self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True) |
|
|
@ -121,28 +138,17 @@ class SwapManager(Logger): |
|
|
|
self.lnworker = wallet.lnworker |
|
|
|
self.lnwatcher = self.wallet.lnworker.lnwatcher |
|
|
|
self.swaps = self.wallet.db.get_dict('submarine_swaps') |
|
|
|
for data in self.swaps.values(): |
|
|
|
if data.get('redeemed'): |
|
|
|
for swap in self.swaps.values(): |
|
|
|
if swap.is_redeemed: |
|
|
|
continue |
|
|
|
redeem_script = bytes.fromhex(data['redeemScript']) |
|
|
|
locktime = data['timeoutBlockHeight'] |
|
|
|
privkey = bytes.fromhex(data['privkey']) |
|
|
|
if data.get('invoice'): |
|
|
|
lockup_address = data['lockupAddress'] |
|
|
|
onchain_amount = data["onchainAmount"] |
|
|
|
preimage = bytes.fromhex(data['preimage']) |
|
|
|
else: |
|
|
|
lockup_address = data['address'] |
|
|
|
onchain_amount = data["expectedAmount"] |
|
|
|
preimage = 0 |
|
|
|
self.add_lnwatcher_callback(lockup_address, onchain_amount, redeem_script, preimage, privkey, locktime) |
|
|
|
self.add_lnwatcher_callback(swap) |
|
|
|
|
|
|
|
def get_swap(self, preimage_hex): |
|
|
|
return self.swaps.get(preimage_hex) |
|
|
|
def get_swap(self, payment_hash): |
|
|
|
return self.swaps.get(payment_hash.hex()) |
|
|
|
|
|
|
|
def add_lnwatcher_callback(self, lockup_address, onchain_amount, redeem_script, preimage, privkey, locktime): |
|
|
|
callback = lambda: self._claim_swap(lockup_address, onchain_amount, redeem_script, preimage, privkey, locktime) |
|
|
|
self.lnwatcher.add_callback(lockup_address, callback) |
|
|
|
def add_lnwatcher_callback(self, swap): |
|
|
|
callback = lambda: self._claim_swap(swap) |
|
|
|
self.lnwatcher.add_callback(swap.lockup_address, callback) |
|
|
|
|
|
|
|
@log_exceptions |
|
|
|
async def normal_swap(self, lightning_amount, expected_onchain_amount, password): |
|
|
@ -189,12 +195,21 @@ class SwapManager(Logger): |
|
|
|
outputs = [PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)] |
|
|
|
tx = self.wallet.create_transaction(outputs=outputs, rbf=False, password=password) |
|
|
|
# save swap data in wallet in case we need a refund |
|
|
|
data['privkey'] = privkey.hex() |
|
|
|
data['preimage'] = preimage.hex() |
|
|
|
data['lightning_amount'] = lightning_amount |
|
|
|
data['funding_txid'] = tx.txid() |
|
|
|
self.swaps[preimage.hex()] = data |
|
|
|
self.add_lnwatcher_callback(lockup_address, onchain_amount, redeem_script, 0, privkey, locktime) |
|
|
|
swap = SwapData( |
|
|
|
redeem_script = redeem_script, |
|
|
|
locktime = locktime, |
|
|
|
privkey = privkey, |
|
|
|
preimage = preimage, |
|
|
|
lockup_address = lockup_address, |
|
|
|
onchain_amount = onchain_amount, |
|
|
|
lightning_amount = lightning_amount, |
|
|
|
is_reverse = False, |
|
|
|
is_redeemed = False, |
|
|
|
funding_txid = tx.txid(), |
|
|
|
spending_txid = None, |
|
|
|
) |
|
|
|
self.swaps[payment_hash.hex()] = swap |
|
|
|
self.add_lnwatcher_callback(swap) |
|
|
|
await self.network.broadcast_transaction(tx) |
|
|
|
# |
|
|
|
attempt = await self.lnworker.await_payment(payment_hash) |
|
|
@ -244,14 +259,23 @@ class SwapManager(Logger): |
|
|
|
# verify invoice preimage_hash |
|
|
|
lnaddr = self.lnworker._check_invoice(invoice, amount_sat) |
|
|
|
assert lnaddr.paymenthash == preimage_hash |
|
|
|
# save swap data in wallet in case payment fails |
|
|
|
data['privkey'] = privkey.hex() |
|
|
|
data['preimage'] = preimage.hex() |
|
|
|
data['lightning_amount'] = amount_sat |
|
|
|
# save data to wallet file |
|
|
|
self.swaps[preimage.hex()] = data |
|
|
|
# save swap data to wallet file |
|
|
|
swap = SwapData( |
|
|
|
redeem_script = redeem_script, |
|
|
|
locktime = locktime, |
|
|
|
privkey = privkey, |
|
|
|
preimage = preimage, |
|
|
|
lockup_address = lockup_address, |
|
|
|
onchain_amount = onchain_amount, |
|
|
|
lightning_amount = amount_sat, |
|
|
|
is_reverse = True, |
|
|
|
is_redeemed = False, |
|
|
|
funding_txid = None, |
|
|
|
spending_txid = None, |
|
|
|
) |
|
|
|
self.swaps[preimage_hash.hex()] = swap |
|
|
|
# add callback to lnwatcher |
|
|
|
self.add_lnwatcher_callback(lockup_address, onchain_amount, redeem_script, preimage, privkey, locktime) |
|
|
|
self.add_lnwatcher_callback(swap) |
|
|
|
# initiate payment. |
|
|
|
success, log = await self.lnworker._pay(invoice, attempts=10) |
|
|
|
return { |
|
|
|