You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
555 lines
23 KiB
555 lines
23 KiB
import asyncio |
|
import json |
|
import os |
|
from typing import TYPE_CHECKING, Optional, Dict, Union |
|
from decimal import Decimal |
|
import math |
|
|
|
import attr |
|
|
|
from .crypto import sha256, hash_160 |
|
from .ecc import ECPrivkey |
|
from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script, |
|
is_segwit_address, construct_witness) |
|
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction |
|
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey |
|
from .util import log_exceptions |
|
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address |
|
from .bitcoin import dust_threshold |
|
from .logging import Logger |
|
from .lnutil import hex_to_bytes |
|
from .json_db import StoredObject |
|
from . import constants |
|
|
|
if TYPE_CHECKING: |
|
from .network import Network |
|
from .wallet import Abstract_Wallet |
|
from .lnwatcher import LNWalletWatcher |
|
from .lnworker import LNWallet |
|
|
|
|
|
API_URL_MAINNET = 'https://swaps.electrum.org/api' |
|
API_URL_TESTNET = 'https://swaps.electrum.org/testnet' |
|
API_URL_REGTEST = 'https://localhost/api' |
|
|
|
|
|
|
|
WITNESS_TEMPLATE_SWAP = [ |
|
opcodes.OP_HASH160, |
|
OPPushDataGeneric(lambda x: x == 20), |
|
opcodes.OP_EQUAL, |
|
opcodes.OP_IF, |
|
OPPushDataPubkey, |
|
opcodes.OP_ELSE, |
|
OPPushDataGeneric(None), |
|
opcodes.OP_CHECKLOCKTIMEVERIFY, |
|
opcodes.OP_DROP, |
|
OPPushDataPubkey, |
|
opcodes.OP_ENDIF, |
|
opcodes.OP_CHECKSIG |
|
] |
|
|
|
|
|
# The script of the reverse swaps has one extra check in it to verify |
|
# that the length of the preimage is 32. This is required because in |
|
# the reverse swaps the preimage is generated by the user and to |
|
# settle the hold invoice, you need a preimage with 32 bytes . If that |
|
# check wasn't there the user could generate a preimage with a |
|
# different length which would still allow for claiming the onchain |
|
# coins but the invoice couldn't be settled |
|
|
|
WITNESS_TEMPLATE_REVERSE_SWAP = [ |
|
opcodes.OP_SIZE, |
|
OPPushDataGeneric(None), |
|
opcodes.OP_EQUAL, |
|
opcodes.OP_IF, |
|
opcodes.OP_HASH160, |
|
OPPushDataGeneric(lambda x: x == 20), |
|
opcodes.OP_EQUALVERIFY, |
|
OPPushDataPubkey, |
|
opcodes.OP_ELSE, |
|
opcodes.OP_DROP, |
|
OPPushDataGeneric(None), |
|
opcodes.OP_CHECKLOCKTIMEVERIFY, |
|
opcodes.OP_DROP, |
|
OPPushDataPubkey, |
|
opcodes.OP_ENDIF, |
|
opcodes.OP_CHECKSIG |
|
] |
|
|
|
|
|
@attr.s |
|
class SwapData(StoredObject): |
|
is_reverse = attr.ib(type=bool) |
|
locktime = attr.ib(type=int) |
|
onchain_amount = attr.ib(type=int) # in sats |
|
lightning_amount = attr.ib(type=int) # in sats |
|
redeem_script = attr.ib(type=bytes, converter=hex_to_bytes) |
|
preimage = attr.ib(type=bytes, converter=hex_to_bytes) |
|
prepay_hash = attr.ib(type=Optional[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=Optional[str]) |
|
spending_txid = attr.ib(type=Optional[str]) |
|
is_redeemed = attr.ib(type=bool) |
|
|
|
|
|
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, |
|
) -> PartialTransaction: |
|
"""Create tx to either claim successful reverse-swap, |
|
or to get refunded for timed-out forward-swap. |
|
""" |
|
if is_segwit_address(txin.address): |
|
txin.script_type = 'p2wsh' |
|
txin.script_sig = b'' |
|
else: |
|
txin.script_type = 'p2wsh-p2sh' |
|
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 |
|
txout = PartialTxOutput.from_address_and_value(address, amount_sat) |
|
tx = PartialTransaction.from_io([txin], [txout], version=2, locktime=locktime) |
|
tx.set_rbf(True) |
|
return tx |
|
|
|
|
|
class SwapManager(Logger): |
|
|
|
network: Optional['Network'] = None |
|
lnwatcher: Optional['LNWalletWatcher'] = None |
|
|
|
def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): |
|
Logger.__init__(self) |
|
self.normal_fee = 0 |
|
self.lockup_fee = 0 |
|
self.percentage = 0 |
|
self.min_amount = 0 |
|
self._max_amount = 0 |
|
self.wallet = wallet |
|
self.lnworker = lnworker |
|
self.swaps = self.wallet.db.get_dict('submarine_swaps') # type: Dict[str, SwapData] |
|
self.prepayments = {} # type: Dict[bytes, bytes] # fee_preimage -> preimage |
|
for k, swap in self.swaps.items(): |
|
if swap.is_reverse and swap.prepay_hash is not None: |
|
self.prepayments[swap.prepay_hash] = bytes.fromhex(k) |
|
# api url |
|
if constants.net == constants.BitcoinMainnet: |
|
self.api_url = API_URL_MAINNET |
|
elif constants.net == constants.BitcoinTestnet: |
|
self.api_url = API_URL_TESTNET |
|
else: |
|
self.api_url = API_URL_REGTEST |
|
|
|
def start_network(self, *, network: 'Network', lnwatcher: 'LNWalletWatcher'): |
|
assert network |
|
assert lnwatcher |
|
self.network = network |
|
self.lnwatcher = lnwatcher |
|
for k, swap in self.swaps.items(): |
|
if swap.is_redeemed: |
|
continue |
|
self.add_lnwatcher_callback(swap) |
|
|
|
@log_exceptions |
|
async def _claim_swap(self, swap: SwapData) -> None: |
|
assert self.network |
|
assert self.lnwatcher |
|
if not self.lnwatcher.is_up_to_date(): |
|
return |
|
current_height = self.network.get_local_height() |
|
delta = current_height - swap.locktime |
|
txos = self.lnwatcher.get_addr_outputs(swap.lockup_address) |
|
for txin in txos.values(): |
|
if swap.is_reverse and txin.value_sats() < swap.onchain_amount: |
|
self.logger.info('amount too low, we should not reveal the preimage') |
|
continue |
|
swap.funding_txid = txin.prevout.txid.hex() |
|
spent_height = txin.spent_height |
|
if spent_height is not None: |
|
swap.spending_txid = txin.spent_txid |
|
if spent_height > 0 and current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: |
|
self.logger.info(f'stop watching swap {swap.lockup_address}') |
|
self.lnwatcher.remove_callback(swap.lockup_address) |
|
swap.is_redeemed = True |
|
continue |
|
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(): |
|
self.logger.info('utxo value below dust threshold') |
|
continue |
|
address = self.wallet.get_receiving_address() |
|
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=address, |
|
amount_sat=amount_sat, |
|
locktime=locktime, |
|
) |
|
self.sign_tx(tx, swap) |
|
await self.network.broadcast_transaction(tx) |
|
|
|
def get_claim_fee(self): |
|
return self.wallet.config.estimate_fee(136, allow_fallback_to_static_rates=True) |
|
|
|
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]: |
|
# for history |
|
swap = self.swaps.get(payment_hash.hex()) |
|
if swap: |
|
return swap |
|
payment_hash = self.prepayments.get(payment_hash) |
|
if payment_hash: |
|
return self.swaps.get(payment_hash.hex()) |
|
|
|
def add_lnwatcher_callback(self, swap: SwapData) -> None: |
|
callback = lambda: self._claim_swap(swap) |
|
self.lnwatcher.add_callback(swap.lockup_address, callback) |
|
|
|
def num_sats_can_receive(self): |
|
# finding how to do MPP is too hard for sender, |
|
# might result in our coins being locked |
|
return self.lnworker.num_sats_can_receive_no_mpp() |
|
|
|
async def normal_swap( |
|
self, |
|
*, |
|
lightning_amount_sat: int, |
|
expected_onchain_amount_sat: int, |
|
password, |
|
tx: PartialTransaction = None, |
|
) -> str: |
|
"""send on-chain BTC, receive on Lightning |
|
|
|
- User generates an LN invoice with RHASH, and knows preimage. |
|
- User creates on-chain output locked to RHASH. |
|
- Server pays LN invoice. User reveals preimage. |
|
- Server spends the on-chain output using preimage. |
|
""" |
|
assert self.network |
|
assert self.lnwatcher |
|
privkey = os.urandom(32) |
|
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) |
|
lnaddr, invoice = await self.lnworker.create_invoice( |
|
amount_msat=lightning_amount_sat * 1000, |
|
message='swap', |
|
expiry=3600 * 24, |
|
) |
|
payment_hash = lnaddr.paymenthash |
|
preimage = self.lnworker.get_preimage(payment_hash) |
|
request_data = { |
|
"type": "submarine", |
|
"pairId": "BTC/BTC", |
|
"orderSide": "sell", |
|
"invoice": invoice, |
|
"refundPublicKey": pubkey.hex() |
|
} |
|
response = await self.network._send_http_on_proxy( |
|
'post', |
|
self.api_url + '/createswap', |
|
json=request_data, |
|
timeout=30) |
|
data = json.loads(response) |
|
response_id = data["id"] |
|
zeroconf = data["acceptZeroConf"] |
|
onchain_amount = data["expectedAmount"] |
|
locktime = data["timeoutBlockHeight"] |
|
lockup_address = data["address"] |
|
redeem_script = data["redeemScript"] |
|
# verify redeem_script is built with our pubkey and preimage |
|
redeem_script = bytes.fromhex(redeem_script) |
|
parsed_script = [x for x in script_GetOp(redeem_script)] |
|
if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP): |
|
raise Exception("fswap check failed: scriptcode does not match template") |
|
if script_to_p2wsh(redeem_script.hex()) != lockup_address: |
|
raise Exception("fswap check failed: inconsistent scriptcode and address") |
|
if hash_160(preimage) != parsed_script[1][1]: |
|
raise Exception("fswap check failed: our preimage not in script") |
|
if pubkey != parsed_script[9][1]: |
|
raise Exception("fswap check failed: our pubkey not in script") |
|
if locktime != int.from_bytes(parsed_script[6][1], byteorder='little'): |
|
raise Exception("fswap check failed: inconsistent locktime and script") |
|
# check that onchain_amount is not more than what we estimated |
|
if onchain_amount > expected_onchain_amount_sat: |
|
raise Exception(f"fswap check failed: onchain_amount is more than what we estimated: " |
|
f"{onchain_amount} > {expected_onchain_amount_sat}") |
|
# 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 |
|
funding_output = PartialTxOutput.from_address_and_value(lockup_address, onchain_amount) |
|
if tx is None: |
|
tx = self.wallet.create_transaction(outputs=[funding_output], rbf=False, 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) # rbf must not decrease payment |
|
self.wallet.sign_transaction(tx, password) |
|
# save swap data in wallet in case we need a refund |
|
swap = SwapData( |
|
redeem_script = redeem_script, |
|
locktime = locktime, |
|
privkey = privkey, |
|
preimage = preimage, |
|
prepay_hash = None, |
|
lockup_address = lockup_address, |
|
onchain_amount = expected_onchain_amount_sat, |
|
lightning_amount = lightning_amount_sat, |
|
is_reverse = False, |
|
is_redeemed = False, |
|
funding_txid = None, |
|
spending_txid = None, |
|
) |
|
self.swaps[payment_hash.hex()] = swap |
|
self.add_lnwatcher_callback(swap) |
|
await self.network.broadcast_transaction(tx) |
|
return tx.txid() |
|
|
|
async def reverse_swap( |
|
self, |
|
*, |
|
lightning_amount_sat: int, |
|
expected_onchain_amount_sat: int, |
|
) -> bool: |
|
"""send on Lightning, receive on-chain |
|
|
|
- User generates preimage, RHASH. Sends RHASH to server. |
|
- Server creates an LN invoice for RHASH. |
|
- User pays LN invoice - except server needs to hold the HTLC as preimage is unknown. |
|
- Server creates on-chain output locked to RHASH. |
|
- User spends on-chain output, revealing preimage. |
|
- Server fulfills HTLC using preimage. |
|
|
|
Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee. |
|
""" |
|
assert self.network |
|
assert self.lnwatcher |
|
privkey = os.urandom(32) |
|
pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) |
|
preimage = os.urandom(32) |
|
preimage_hash = sha256(preimage) |
|
request_data = { |
|
"type": "reversesubmarine", |
|
"pairId": "BTC/BTC", |
|
"orderSide": "buy", |
|
"invoiceAmount": lightning_amount_sat, |
|
"preimageHash": preimage_hash.hex(), |
|
"claimPublicKey": pubkey.hex() |
|
} |
|
response = await self.network._send_http_on_proxy( |
|
'post', |
|
self.api_url + '/createswap', |
|
json=request_data, |
|
timeout=30) |
|
data = json.loads(response) |
|
invoice = data['invoice'] |
|
fee_invoice = data.get('minerFeeInvoice') |
|
lockup_address = data['lockupAddress'] |
|
redeem_script = data['redeemScript'] |
|
locktime = data['timeoutBlockHeight'] |
|
onchain_amount = data["onchainAmount"] |
|
response_id = data['id'] |
|
# verify redeem_script is built with our pubkey and preimage |
|
redeem_script = bytes.fromhex(redeem_script) |
|
parsed_script = [x for x in script_GetOp(redeem_script)] |
|
if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP): |
|
raise Exception("rswap check failed: scriptcode does not match template") |
|
if script_to_p2wsh(redeem_script.hex()) != lockup_address: |
|
raise Exception("rswap check failed: inconsistent scriptcode and address") |
|
if hash_160(preimage) != parsed_script[5][1]: |
|
raise Exception("rswap check failed: our preimage not in script") |
|
if pubkey != parsed_script[7][1]: |
|
raise Exception("rswap check failed: our pubkey not in script") |
|
if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'): |
|
raise Exception("rswap check failed: inconsistent locktime and script") |
|
# check that the onchain amount is what we expected |
|
if onchain_amount < expected_onchain_amount_sat: |
|
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: |
|
raise Exception("rswap check failed: locktime too close") |
|
# verify invoice preimage_hash |
|
lnaddr = self.lnworker._check_invoice(invoice) |
|
invoice_amount = lnaddr.get_amount_sat() |
|
if lnaddr.paymenthash != preimage_hash: |
|
raise Exception("rswap check failed: inconsistent RHASH and invoice") |
|
# check that the lightning amount is what we requested |
|
if fee_invoice: |
|
fee_lnaddr = self.lnworker._check_invoice(fee_invoice) |
|
invoice_amount += fee_lnaddr.get_amount_sat() |
|
prepay_hash = fee_lnaddr.paymenthash |
|
else: |
|
prepay_hash = None |
|
if int(invoice_amount) != lightning_amount_sat: |
|
raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) " |
|
f"not what we requested ({lightning_amount_sat})") |
|
# save swap data to wallet file |
|
swap = SwapData( |
|
redeem_script = redeem_script, |
|
locktime = locktime, |
|
privkey = privkey, |
|
preimage = preimage, |
|
prepay_hash = prepay_hash, |
|
lockup_address = lockup_address, |
|
onchain_amount = onchain_amount, |
|
lightning_amount = lightning_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(swap) |
|
# initiate payment. |
|
if fee_invoice: |
|
self.prepayments[prepay_hash] = preimage_hash |
|
asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10)) |
|
# initiate payment. |
|
success, log = await self.lnworker.pay_invoice(invoice, attempts=10) |
|
return success |
|
|
|
async def get_pairs(self) -> None: |
|
assert self.network |
|
response = await self.network._send_http_on_proxy( |
|
'get', |
|
self.api_url + '/getpairs', |
|
timeout=30) |
|
pairs = json.loads(response) |
|
fees = pairs['pairs']['BTC/BTC']['fees'] |
|
self.percentage = fees['percentage'] |
|
self.normal_fee = fees['minerFees']['baseAsset']['normal'] |
|
self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] |
|
limits = pairs['pairs']['BTC/BTC']['limits'] |
|
self.min_amount = limits['minimal'] |
|
self._max_amount = limits['maximal'] |
|
|
|
def get_max_amount(self): |
|
return self._max_amount |
|
|
|
def check_invoice_amount(self, x): |
|
return x >= self.min_amount and x <= self._max_amount |
|
|
|
def _get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]: |
|
"""For a given swap direction and amount we send, returns how much we will receive. |
|
|
|
Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for. |
|
In the reverse direction, the result matches what the swap server returns as response["onchainAmount"]. |
|
""" |
|
if send_amount is None: |
|
return |
|
x = Decimal(send_amount) |
|
percentage = Decimal(self.percentage) |
|
if is_reverse: |
|
if not self.check_invoice_amount(x): |
|
return |
|
# see/ref: |
|
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L948 |
|
percentage_fee = math.ceil(percentage * x / 100) |
|
base_fee = self.lockup_fee |
|
x -= percentage_fee + base_fee |
|
x = math.floor(x) |
|
if x < dust_threshold(): |
|
return |
|
else: |
|
x -= self.normal_fee |
|
percentage_fee = math.ceil(x * percentage / (100 + percentage)) |
|
x -= percentage_fee |
|
if not self.check_invoice_amount(x): |
|
return |
|
x = int(x) |
|
return x |
|
|
|
def _get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]: |
|
"""For a given swap direction and amount we want to receive, returns how much we will need to send. |
|
|
|
Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for. |
|
In the forward direction, the result matches what the swap server returns as response["expectedAmount"]. |
|
""" |
|
if not recv_amount: |
|
return |
|
x = Decimal(recv_amount) |
|
percentage = Decimal(self.percentage) |
|
if is_reverse: |
|
# see/ref: |
|
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L928 |
|
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L958 |
|
base_fee = self.lockup_fee |
|
x += base_fee |
|
x = math.ceil(x / ((100 - percentage) / 100)) |
|
if not self.check_invoice_amount(x): |
|
return |
|
else: |
|
if not self.check_invoice_amount(x): |
|
return |
|
# see/ref: |
|
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L708 |
|
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/rates/FeeProvider.ts#L90 |
|
percentage_fee = math.ceil(percentage * x / 100) |
|
x += percentage_fee + self.normal_fee |
|
x = int(x) |
|
return x |
|
|
|
def get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]: |
|
recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse) |
|
# sanity check calculation can be inverted |
|
if recv_amount is not None: |
|
inverted_send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse) |
|
# accept off-by ones as amt_rcv = recv_amt(send_amt(amt_rcv)) only up to +-1 |
|
if abs(send_amount - inverted_send_amount) > 1: |
|
raise Exception(f"calc-invert-sanity-check failed. is_reverse={is_reverse}. " |
|
f"send_amount={send_amount} -> recv_amount={recv_amount} -> inverted_send_amount={inverted_send_amount}") |
|
# account for on-chain claim tx fee |
|
if is_reverse and recv_amount is not None: |
|
recv_amount -= self.get_claim_fee() |
|
return recv_amount |
|
|
|
def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]: |
|
send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse) |
|
# sanity check calculation can be inverted |
|
if send_amount is not None: |
|
inverted_recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse) |
|
if recv_amount != inverted_recv_amount: |
|
raise Exception(f"calc-invert-sanity-check failed. is_reverse={is_reverse}. " |
|
f"recv_amount={recv_amount} -> send_amount={send_amount} -> inverted_recv_amount={inverted_recv_amount}") |
|
# account for on-chain claim tx fee |
|
if is_reverse and send_amount is not None: |
|
send_amount += self.get_claim_fee() |
|
return send_amount |
|
|
|
def get_swap_by_tx(self, tx): |
|
# determine if tx is spending from a swap |
|
txin = tx.inputs()[0] |
|
for key, swap in self.swaps.items(): |
|
if txin.prevout.txid.hex() == swap.funding_txid: |
|
return swap |
|
return None |
|
|
|
def sign_tx(self, tx, swap): |
|
preimage = swap.preimage if swap.is_reverse else 0 |
|
witness_script = swap.redeem_script |
|
txin = tx.inputs()[0] |
|
txin.script_type = 'p2wsh' |
|
txin.script_sig = b'' |
|
txin.witness_script = witness_script |
|
sig = bytes.fromhex(tx.sign_txin(0, swap.privkey)) |
|
witness = [sig, preimage, witness_script] |
|
txin.witness = bytes.fromhex(construct_witness(witness))
|
|
|