Skip to content

Commit

Permalink
feat: do not use address indexers of mempool
Browse files Browse the repository at this point in the history
use `/tx/{txid}` endpoint of mempool space instead of `/address/{lockup_adress}`.
  • Loading branch information
dni committed Nov 6, 2023
1 parent e2620a9 commit 7e13f54
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 42 deletions.
71 changes: 63 additions & 8 deletions boltz_client/boltz.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
""" boltz_client main module """

import asyncio
from dataclasses import dataclass
from typing import Optional

import httpx
from embit.transaction import Transaction

from .helpers import req_wrap
from .mempool import MempoolClient
Expand All @@ -28,12 +30,24 @@ def __init__(self, message: str, status: str):
self.status = status


class BoltzSwapTransactionException(Exception):
def __init__(self, message: str, status: str):
self.message = message
self.status = status


@dataclass
class BoltzSwapTransactionResponse:
transactionHex: Optional[str] = None
timeoutBlockHeight: Optional[str] = None


@dataclass
class BoltzSwapStatusResponse:
status: str
failureReason: Optional[str] = None
zeroConfRejected: Optional[str] = None
transaction: Optional[str] = None
transaction: Optional[dict] = None


@dataclass
Expand Down Expand Up @@ -61,7 +75,7 @@ class BoltzReverseSwapResponse:
class BoltzConfig:
network: str = "main"
api_url: str = "https://boltz.exchange/api"
mempool_url: str = "https://mempool.space/api"
mempool_url: str = "https://mempool.space/api/v1"
mempool_ws_url: str = "wss://mempool.space/api/v1/ws"
referral_id: str = "dni"

Expand Down Expand Up @@ -95,8 +109,8 @@ def check_version(self):
)

def get_fee_estimation(self, feerate: Optional[int]) -> int:
# TODO: hardcoded maximum tx size, in the future we try to get the size of the tx via embit
# we need a function like Transaction.vsize()
# TODO: hardcoded maximum tx size, in the future we try to get the size of the
# tx via embit we need a function like Transaction.vsize()
tx_size_vbyte = 200
mempool_fees = feerate if feerate else self.mempool.get_fees()
return mempool_fees * tx_size_vbyte
Expand All @@ -117,8 +131,10 @@ def set_limits(self) -> None:
def check_limits(self, amount: int) -> None:
valid = self.limit_minimal <= amount <= self.limit_maximal
if not valid:
msg = f"Boltz - swap not in boltz limits, amount: {amount}, min: {self.limit_minimal}, max: {self.limit_maximal}"
raise BoltzLimitException(msg)
raise BoltzLimitException(
f"Boltz - swap not in boltz limits, amount: {amount}, "
f"min: {self.limit_minimal}, max: {self.limit_maximal}"
)

def swap_status(self, boltz_id: str) -> BoltzSwapStatusResponse:
data = self.request(
Expand All @@ -134,8 +150,44 @@ def swap_status(self, boltz_id: str) -> BoltzSwapStatusResponse:

return status

def swap_transaction(self, boltz_id: str) -> BoltzSwapTransactionResponse:
data = self.request(
"post",
f"{self._cfg.api_url}/getswaptransaction",
json={"id": boltz_id},
headers={"Content-Type": "application/json"},
)
res = BoltzSwapTransactionResponse(**data)
print(res)

# if res.failureReason:
# raise BoltzSwapTransctionException(res.failureReason, res.status)

return res

async def wait_for_txid(self, boltz_id: str) -> str:
while True:
try:
tx_hex = self.swap_transaction(boltz_id)
tx = Transaction.from_string(tx_hex.transactionHex)
return tx.txid().hex()
except Exception:
await asyncio.sleep(5)

async def wait_for_txid_on_status(self, boltz_id: str) -> str:
while True:
try:
status = self.swap_status(boltz_id)
assert status.transaction
id = status.transaction.get('id')
assert id
return id
except Exception:
await asyncio.sleep(5)

async def claim_reverse_swap(
self,
boltz_id: str,
lockup_address: str,
receive_address: str,
privkey_wif: str,
Expand All @@ -144,7 +196,8 @@ async def claim_reverse_swap(
zeroconf: bool = False,
feerate: Optional[int] = None,
):
lockup_tx = await self.mempool.get_tx_from_address(lockup_address)
lockup_txid = await self.wait_for_txid_on_status(boltz_id)
lockup_tx = await self.mempool.get_tx_from_txid(lockup_txid, lockup_address)

if not zeroconf and lockup_tx.status != "confirmed":
await self.mempool.wait_for_tx_confirmed(lockup_tx.txid)
Expand All @@ -163,6 +216,7 @@ async def claim_reverse_swap(

async def refund_swap(
self,
boltz_id: str,
privkey_wif: str,
lockup_address: str,
receive_address: str,
Expand All @@ -171,7 +225,8 @@ async def refund_swap(
feerate: Optional[int] = None,
) -> str:
self.mempool.check_block_height(timeout_block_height)
lockup_tx = await self.mempool.get_tx_from_address(lockup_address)
lockup_txid = await self.wait_for_txid(boltz_id)
lockup_tx = await self.mempool.get_tx_from_txid(lockup_txid, lockup_address)
txid, transaction = create_refund_tx(
lockup_tx=lockup_tx,
privkey_wif=privkey_wif,
Expand Down
56 changes: 35 additions & 21 deletions boltz_client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@

config = BoltzConfig()

# use for manual testing
# config = BoltzConfig(
# network="regtest",
# api_url="http://localhost:9001",
# mempool_url="http://localhost:8999/api/v1",
# mempool_ws_url="ws://localhost:8999/api/v1/ws",
# )

@click.group()
def command_group():
Expand All @@ -37,7 +44,7 @@ def create_swap(payment_request):
click.echo(f"boltz_id: {swap.id}")
click.echo()
click.echo(
f"mempool.space url: {config.mempool_url.replace('/api', '')}/address/{swap.address}"
f"mempool.space url: {config.mempool_url}/address/{swap.address}"
)
click.echo()
click.echo(f"refund privkey in wif: {refund_privkey_wif}")
Expand All @@ -52,18 +59,20 @@ def create_swap(payment_request):
click.echo("run this command if you need to refund:")
click.echo("CHANGE YOUR_RECEIVEADDRESS to your onchain address!!!")
click.echo(
f"boltz refund-swap {refund_privkey_wif} {swap.address} YOUR_RECEIVEADDRESS "
f"boltz refund-swap {swap.id} {refund_privkey_wif} {swap.address} YOUR_RECEIVEADDRESS "
f"{swap.redeemScript} {swap.timeoutBlockHeight}"
)


@click.command()
@click.argument("boltz_id", type=str)
@click.argument("privkey_wif", type=str)
@click.argument("lockup_address", type=str)
@click.argument("receive_address", type=str)
@click.argument("redeem_script_hex", type=str)
@click.argument("timeout_block_height", type=int)
def refund_swap(
boltz_id: str,
privkey_wif: str,
lockup_address: str,
receive_address: str,
Expand All @@ -76,11 +85,12 @@ def refund_swap(
client = BoltzClient(config)
txid = asyncio.run(
client.refund_swap(
privkey_wif,
lockup_address,
receive_address,
redeem_script_hex,
timeout_block_height,
boltz_id=boltz_id,
privkey_wif=privkey_wif,
lockup_address=lockup_address,
receive_address=receive_address,
redeem_script_hex=redeem_script_hex,
timeout_block_height=timeout_block_height,
)
)
click.echo("swap refunded!")
Expand All @@ -105,7 +115,7 @@ def create_reverse_swap(sats: int):
click.echo()
click.echo(f"boltz_id: {swap.id}")
click.echo(
f"mempool.space url: {config.mempool_url.replace('/api', '')}/address/{swap.lockupAddress}"
f"mempool.space url: {config.mempool_url}/address/{swap.lockupAddress}"
)
click.echo()
click.echo("invoice:")
Expand All @@ -115,7 +125,7 @@ def create_reverse_swap(sats: int):
click.echo("run this command after you see the lockup transaction:")
click.echo("CHANGE YOUR_RECEIVEADDRESS to your onchain address!!!")
click.echo(
f"boltz claim-reverse-swap {swap.lockupAddress} YOUR_RECEIVEADDRESS "
f"boltz claim-reverse-swap {swap.id} {swap.lockupAddress} YOUR_RECEIVEADDRESS "
f"{claim_privkey_wif} {preimage_hex} {swap.redeemScript}"
)

Expand Down Expand Up @@ -153,12 +163,13 @@ def create_reverse_swap_and_claim(

txid = asyncio.run(
client.claim_reverse_swap(
swap.lockupAddress,
receive_address,
claim_privkey_wif,
preimage_hex,
swap.redeemScript,
zeroconf,
boltz_id=swap.id,
lockup_address=swap.lockupAddress,
receive_address=receive_address,
privkey_wif=claim_privkey_wif,
preimage_hex=preimage_hex,
redeem_script_hex=swap.redeemScript,
zeroconf=zeroconf,
)
)

Expand All @@ -167,13 +178,15 @@ def create_reverse_swap_and_claim(


@click.command()
@click.argument("boltz_id", type=str)
@click.argument("lockup_address", type=str)
@click.argument("receive_address", type=str)
@click.argument("privkey_wif", type=str)
@click.argument("preimage_hex", type=str)
@click.argument("redeem_script_hex", type=str)
@click.argument("zeroconf", type=bool, default=False)
def claim_reverse_swap(
boltz_id: str,
lockup_address: str,
receive_address: str,
privkey_wif: str,
Expand All @@ -188,12 +201,13 @@ def claim_reverse_swap(

txid = asyncio.run(
client.claim_reverse_swap(
lockup_address,
receive_address,
privkey_wif,
preimage_hex,
redeem_script_hex,
zeroconf,
boltz_id=boltz_id,
lockup_address=lockup_address,
receive_address=receive_address,
privkey_wif=privkey_wif,
preimage_hex=preimage_hex,
redeem_script_hex=redeem_script_hex,
zeroconf=zeroconf,
)
)

Expand Down
51 changes: 38 additions & 13 deletions boltz_client/mempool.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ def request(self, funcname, *args, **kwargs) -> dict:
msg = f"unreachable: {exc.request.url!r}."
raise MempoolApiException(f"mempool api connection error: {msg}") from exc
except httpx.HTTPStatusError as exc:
msg = f"{exc.response.status_code} while requesting {exc.request.url!r}. message: {exc.response.text}"
msg = (
f"{exc.response.status_code} while requesting "
f"{exc.request.url!r}. message: {exc.response.text}"
)
raise MempoolApiException(f"mempool api status error: {msg}") from exc

async def wait_for_websocket_message(self, send, message_key):
Expand Down Expand Up @@ -86,24 +89,44 @@ def find_tx_and_output(self, txs, address: str) -> Optional[LockupData]:
if len(txs) == 0:
return None
for tx in txs:
for i, vout in enumerate(tx["vout"]):
if vout["scriptpubkey_address"] == address:
status = "confirmed" if tx["status"]["confirmed"] else "unconfirmed"
return LockupData(
txid=tx["txid"],
vout_cnt=i,
vout_amount=vout["value"],
status=status,
)
output = self.find_output(tx, address)
if output:
return output
return None

def find_output(self, tx, address: str) -> Optional[LockupData]:
for i, vout in enumerate(tx["vout"]):
if vout["scriptpubkey_address"] == address:
status = "confirmed" if tx["status"]["confirmed"] else "unconfirmed"
return LockupData(
txid=tx["txid"],
vout_cnt=i,
vout_amount=vout["value"],
status=status,
)
return None

def get_tx(self, txid: str):
return self.request(
"get",
f"{self._api_url}/tx/{txid}",
headers={"Content-Type": "application/json"},
)

def get_txs_from_address(self, address: str):
return self.request(
"get",
f"{self._api_url}/address/{address}/txs",
headers={"Content-Type": "application/json"},
)

async def get_tx_from_txid(self, txid: str, address: str) -> LockupData:
while True:
output = self.find_output(self.get_tx(txid), address)
if output:
return output
await asyncio.sleep(3)

async def get_tx_from_address(self, address: str) -> LockupData:
txs = self.get_txs_from_address(address)
if len(txs) == 0:
Expand All @@ -116,7 +139,7 @@ async def get_tx_from_address(self, address: str) -> LockupData:
def get_fees(self) -> int:
data = self.request(
"get",
f"{self._api_url}/v1/fees/recommended",
f"{self._api_url}/fees/recommended",
headers={"Content-Type": "application/json"},
)
return int(data["halfHourFee"])
Expand All @@ -132,8 +155,10 @@ def get_blockheight(self) -> int:
def check_block_height(self, timeout_block_height: int) -> None:
current_block_height = self.get_blockheight()
if current_block_height < timeout_block_height:
msg = f"current_block_height ({current_block_height}) has not yet exceeded ({timeout_block_height})"
raise MempoolBlockHeightException(msg)
raise MempoolBlockHeightException(
f"current_block_height ({current_block_height}) "
f"has not yet exceeded ({timeout_block_height})"
)

def send_onchain_tx(self, tx_hex: str):
return self.request(
Expand Down

0 comments on commit 7e13f54

Please sign in to comment.