boltz_client 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boltz_client might be problematic. Click here for more details.

@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.1
2
+ Name: boltz_client
3
+ Version: 0.2.2
4
+ Summary: python boltz client
5
+ Home-page: https://boltz.exchange
6
+ License: MIT
7
+ Author: dni
8
+ Author-email: office@dnilabs.com
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Dist: click (>=8)
17
+ Requires-Dist: embit (>=0.7.0,<0.8.0)
18
+ Requires-Dist: httpx (>=0.23)
19
+ Requires-Dist: wallycore (==1.0.0)
20
+ Requires-Dist: websockets (>=10)
21
+ Project-URL: Repository, https://github.com/dni/boltz-client-python
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Boltz Python Client
25
+ Boltz Client in Python, implementing mainchain and liquid submarine swaps. Used by e.g. https://github.com/lnbits/boltz.
26
+
27
+ # CLI
28
+ ```console
29
+ $ boltz --help
30
+ Usage: boltz [OPTIONS] COMMAND [ARGS]...
31
+
32
+ Python CLI of boltz-client-python, enjoy submarine swapping. :)
33
+
34
+ Options:
35
+ --help Show this message and exit.
36
+
37
+ Commands:
38
+ calculate-swap-send-amount calculate the amount of the invoice you...
39
+ claim-reverse-swap claims a reverse swap
40
+ create-reverse-swap create a reverse swap
41
+ create-reverse-swap-and-claim create a reverse swap and claim
42
+ create-swap create a swap boltz will pay your...
43
+ refund-swap refund a swap
44
+ show-pairs show pairs of possible assets to swap
45
+ swap-status get swap status retrieves the status of...
46
+ ```
47
+ install the latest release from [PyPI](https://pypi.org/project/boltz-client) via `pip install boltz_client`.
48
+
49
+ # LIB
50
+ ### initialize the client
51
+ ```python
52
+ from boltz_client import BoltzClient, BoltzConfig
53
+ config = BoltzConfig() # default config
54
+ client = BoltzClient(config, "BTC/BTC")
55
+ ```
56
+ ### lifecycle swap
57
+ ```python
58
+ pr = create_lightning_invoice(100000) # example function to create a lightning invoice
59
+ refund_privkey_wif, swap = client.create_swap(pr)
60
+ print(f"pay this amount: {swap.expectedAmount}")
61
+ print(f"to this address: {swap.address}")
62
+ # when you pay the amount the invoice will be settled after the boltz claimed the swap
63
+ ```
64
+ if swap fails you can refund like this
65
+ ```python
66
+ # example function to create an onchain address
67
+ onchain_address = create_onchain_address()
68
+ txid = await client.refund_swap(
69
+ boltz_id=swap.id,
70
+ privkey_wif=refund_privkey_wif,
71
+ lockup_address=swap.address,
72
+ receive_address=onchain_address,
73
+ redeem_script_hex=swap.redeemScript,
74
+ timeout_block_height=swap.timeoutBlockHeight,
75
+ )
76
+ ```
77
+
78
+ ### lifecycle reverse swap
79
+ ```python
80
+ claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(50000)
81
+ # example function to pay the invoice
82
+ pay_task = asyncio.create_task(pay_invoice(swap.invoice))
83
+ # example function to create an onchain address
84
+ new_address = create_onchain_address()
85
+ task = asyncio.create_task(client.claim_reverse_swap(
86
+ boltz_id=swap.id,
87
+ receive_address=new_address,
88
+ lockup_address=swap.lockupAddress,
89
+ redeem_script_hex=swap.redeemScript,
90
+ blinding_key=swap.blindingKey,
91
+ privkey_wif=claim_privkey_wif,
92
+ preimage_hex=preimage_hex,
93
+ zeroconf=True,
94
+ ))
95
+ txid = await task
96
+ await pay_task
97
+ ```
98
+
99
+
100
+ # development
101
+
102
+ ## installing
103
+ ```console
104
+ poetry install
105
+ ```
106
+
107
+ ## running cli
108
+ ```console
109
+ poetry run boltz
110
+ ```
111
+
112
+ ## starting regtest
113
+ ```console
114
+ cd docker
115
+ chmod +x regtest
116
+ ./regtest
117
+ ```
118
+
119
+ ## running tests
120
+ ```console
121
+ poetry run pytest
122
+ ```
123
+
@@ -0,0 +1,99 @@
1
+ # Boltz Python Client
2
+ Boltz Client in Python, implementing mainchain and liquid submarine swaps. Used by e.g. https://github.com/lnbits/boltz.
3
+
4
+ # CLI
5
+ ```console
6
+ $ boltz --help
7
+ Usage: boltz [OPTIONS] COMMAND [ARGS]...
8
+
9
+ Python CLI of boltz-client-python, enjoy submarine swapping. :)
10
+
11
+ Options:
12
+ --help Show this message and exit.
13
+
14
+ Commands:
15
+ calculate-swap-send-amount calculate the amount of the invoice you...
16
+ claim-reverse-swap claims a reverse swap
17
+ create-reverse-swap create a reverse swap
18
+ create-reverse-swap-and-claim create a reverse swap and claim
19
+ create-swap create a swap boltz will pay your...
20
+ refund-swap refund a swap
21
+ show-pairs show pairs of possible assets to swap
22
+ swap-status get swap status retrieves the status of...
23
+ ```
24
+ install the latest release from [PyPI](https://pypi.org/project/boltz-client) via `pip install boltz_client`.
25
+
26
+ # LIB
27
+ ### initialize the client
28
+ ```python
29
+ from boltz_client import BoltzClient, BoltzConfig
30
+ config = BoltzConfig() # default config
31
+ client = BoltzClient(config, "BTC/BTC")
32
+ ```
33
+ ### lifecycle swap
34
+ ```python
35
+ pr = create_lightning_invoice(100000) # example function to create a lightning invoice
36
+ refund_privkey_wif, swap = client.create_swap(pr)
37
+ print(f"pay this amount: {swap.expectedAmount}")
38
+ print(f"to this address: {swap.address}")
39
+ # when you pay the amount the invoice will be settled after the boltz claimed the swap
40
+ ```
41
+ if swap fails you can refund like this
42
+ ```python
43
+ # example function to create an onchain address
44
+ onchain_address = create_onchain_address()
45
+ txid = await client.refund_swap(
46
+ boltz_id=swap.id,
47
+ privkey_wif=refund_privkey_wif,
48
+ lockup_address=swap.address,
49
+ receive_address=onchain_address,
50
+ redeem_script_hex=swap.redeemScript,
51
+ timeout_block_height=swap.timeoutBlockHeight,
52
+ )
53
+ ```
54
+
55
+ ### lifecycle reverse swap
56
+ ```python
57
+ claim_privkey_wif, preimage_hex, swap = client.create_reverse_swap(50000)
58
+ # example function to pay the invoice
59
+ pay_task = asyncio.create_task(pay_invoice(swap.invoice))
60
+ # example function to create an onchain address
61
+ new_address = create_onchain_address()
62
+ task = asyncio.create_task(client.claim_reverse_swap(
63
+ boltz_id=swap.id,
64
+ receive_address=new_address,
65
+ lockup_address=swap.lockupAddress,
66
+ redeem_script_hex=swap.redeemScript,
67
+ blinding_key=swap.blindingKey,
68
+ privkey_wif=claim_privkey_wif,
69
+ preimage_hex=preimage_hex,
70
+ zeroconf=True,
71
+ ))
72
+ txid = await task
73
+ await pay_task
74
+ ```
75
+
76
+
77
+ # development
78
+
79
+ ## installing
80
+ ```console
81
+ poetry install
82
+ ```
83
+
84
+ ## running cli
85
+ ```console
86
+ poetry run boltz
87
+ ```
88
+
89
+ ## starting regtest
90
+ ```console
91
+ cd docker
92
+ chmod +x regtest
93
+ ./regtest
94
+ ```
95
+
96
+ ## running tests
97
+ ```console
98
+ poetry run pytest
99
+ ```
@@ -9,13 +9,11 @@ from typing import Optional
9
9
  import httpx
10
10
 
11
11
  from .helpers import req_wrap
12
- from .mempool import MempoolClient
13
12
  from .onchain import (
14
13
  create_claim_tx,
15
14
  create_key_pair,
16
15
  create_preimage,
17
16
  create_refund_tx,
18
- get_txid,
19
17
  validate_address,
20
18
  )
21
19
 
@@ -58,7 +56,9 @@ class BoltzSwapTransactionException(Exception):
58
56
 
59
57
  @dataclass
60
58
  class BoltzSwapTransactionResponse:
59
+ transactionId: Optional[str] = None
61
60
  transactionHex: Optional[str] = None
61
+ timeoutEta: Optional[str] = None
62
62
  timeoutBlockHeight: Optional[str] = None
63
63
  failureReason: Optional[str] = None
64
64
 
@@ -69,6 +69,7 @@ class BoltzSwapStatusResponse:
69
69
  failureReason: Optional[str] = None
70
70
  zeroConfRejected: Optional[str] = None
71
71
  transaction: Optional[dict] = None
72
+ failureDetails: Optional[str] = None
72
73
 
73
74
 
74
75
  @dataclass
@@ -81,6 +82,7 @@ class BoltzSwapResponse:
81
82
  expectedAmount: int
82
83
  timeoutBlockHeight: int
83
84
  blindingKey: Optional[str] = None
85
+ referralId: Optional[str] = None
84
86
 
85
87
 
86
88
  @dataclass
@@ -92,6 +94,7 @@ class BoltzReverseSwapResponse:
92
94
  timeoutBlockHeight: int
93
95
  onchainAmount: int
94
96
  blindingKey: Optional[str] = None
97
+ referralId: Optional[str] = None
95
98
 
96
99
 
97
100
  @dataclass
@@ -100,8 +103,6 @@ class BoltzConfig:
100
103
  network_liquid: str = "liquidv1"
101
104
  pairs: list = field(default_factory=lambda: ["BTC/BTC", "L-BTC/BTC"])
102
105
  api_url: str = "https://boltz.exchange/api"
103
- mempool_url: str = "https://mempool.space/api"
104
- mempool_liquid_url: str = "https://liquid.network/api"
105
106
  referral_id: str = "dni"
106
107
 
107
108
 
@@ -119,12 +120,8 @@ class BoltzClient:
119
120
 
120
121
  if self.pair == "L-BTC/BTC":
121
122
  self.network = self._cfg.network_liquid
122
- mempool_url = self._cfg.mempool_liquid_url
123
123
  else:
124
124
  self.network = self._cfg.network
125
- mempool_url = self._cfg.mempool_url
126
-
127
- self.mempool = MempoolClient(mempool_url)
128
125
 
129
126
  def request(self, funcname, *args, **kwargs) -> dict:
130
127
  try:
@@ -171,13 +168,6 @@ class BoltzClient:
171
168
  def get_fee_estimation_refund(self) -> int:
172
169
  return self.fees["minerFees"]["baseAsset"]["normal"]
173
170
 
174
- def get_fee_estimation(self, feerate: Optional[int]) -> int:
175
- # TODO: hardcoded maximum tx size, in the future we try to get the size of the
176
- # tx via embit we need a function like Transaction.vsize()
177
- tx_size_vbyte = 200
178
- mempool_fees = feerate if feerate else self.mempool.get_fees()
179
- return mempool_fees * tx_size_vbyte
180
-
181
171
  def get_pairs(self) -> dict:
182
172
  data = self.request(
183
173
  "get",
@@ -223,24 +213,25 @@ class BoltzClient:
223
213
 
224
214
  return res
225
215
 
226
- async def wait_for_txid(self, boltz_id: str) -> str:
216
+ async def wait_for_tx(self, boltz_id: str) -> str:
227
217
  while True:
228
218
  try:
229
219
  swap_transaction = self.swap_transaction(boltz_id)
230
- if swap_transaction.transactionHex:
231
- return get_txid(swap_transaction.transactionHex, self.pair)
232
- raise ValueError("transactionHex is empty")
220
+ assert swap_transaction.transactionHex
221
+ return swap_transaction.transactionHex
233
222
  except (ValueError, BoltzApiException, BoltzSwapTransactionException):
234
223
  await asyncio.sleep(3)
235
224
 
236
- async def wait_for_txid_on_status(self, boltz_id: str) -> str:
225
+ async def wait_for_tx_on_status(self, boltz_id: str, zeroconf: bool = True) -> str:
237
226
  while True:
238
227
  try:
239
228
  status = self.swap_status(boltz_id)
240
229
  assert status.transaction
241
- txid = status.transaction.get("id")
242
- assert txid
243
- return txid
230
+ txHex = status.transaction.get("hex")
231
+ assert txHex
232
+ if not zeroconf:
233
+ assert status.status == "transaction.confirmed"
234
+ return txHex
244
235
  except (BoltzApiException, BoltzSwapStatusException, AssertionError):
245
236
  await asyncio.sleep(3)
246
237
 
@@ -259,29 +250,22 @@ class BoltzClient:
259
250
  preimage_hex: str,
260
251
  redeem_script_hex: str,
261
252
  zeroconf: bool = True,
262
- feerate: Optional[int] = None,
263
253
  blinding_key: Optional[str] = None,
264
254
  ):
265
-
266
255
  self.validate_address(receive_address)
267
- _lockup_address = self.validate_address(lockup_address)
268
- lockup_txid = await self.wait_for_txid_on_status(boltz_id)
269
- lockup_tx = await self.mempool.get_tx_from_txid(lockup_txid, _lockup_address)
270
-
271
- if not zeroconf and lockup_tx.status != "confirmed":
272
- await self.mempool.wait_for_tx_confirmed(lockup_tx.txid)
256
+ self.validate_address(lockup_address)
257
+ lockup_rawtx = await self.wait_for_tx_on_status(boltz_id, zeroconf)
273
258
 
274
259
  transaction = create_claim_tx(
275
- lockup_tx=lockup_tx,
260
+ lockup_address=lockup_address,
261
+ lockup_rawtx=lockup_rawtx,
276
262
  receive_address=receive_address,
277
263
  privkey_wif=privkey_wif,
278
264
  redeem_script_hex=redeem_script_hex,
279
265
  preimage_hex=preimage_hex,
280
266
  pair=self.pair,
281
267
  blinding_key=blinding_key,
282
- fees=self.get_fee_estimation(feerate)
283
- if feerate
284
- else self.get_fee_estimation_claim(),
268
+ fees=self.get_fee_estimation_claim(),
285
269
  )
286
270
  return self.send_onchain_tx(transaction)
287
271
 
@@ -293,25 +277,23 @@ class BoltzClient:
293
277
  receive_address: str,
294
278
  redeem_script_hex: str,
295
279
  timeout_block_height: int,
296
- feerate: Optional[int] = None,
297
280
  blinding_key: Optional[str] = None,
298
281
  ) -> str:
299
- self.mempool.check_block_height(timeout_block_height)
282
+ # self.mempool.check_block_height(timeout_block_height)
300
283
  self.validate_address(receive_address)
301
- _lockup_address = self.validate_address(lockup_address)
302
- lockup_txid = await self.wait_for_txid(boltz_id)
303
- lockup_tx = await self.mempool.get_tx_from_txid(lockup_txid, _lockup_address)
284
+ self.validate_address(lockup_address)
285
+
286
+ lockup_rawtx = await self.wait_for_tx(boltz_id)
304
287
  transaction = create_refund_tx(
305
- lockup_tx=lockup_tx,
288
+ lockup_address=lockup_address,
289
+ lockup_rawtx=lockup_rawtx,
306
290
  privkey_wif=privkey_wif,
307
291
  receive_address=receive_address,
308
292
  redeem_script_hex=redeem_script_hex,
309
293
  timeout_block_height=timeout_block_height,
310
294
  pair=self.pair,
311
295
  blinding_key=blinding_key,
312
- fees=self.get_fee_estimation(feerate)
313
- if feerate
314
- else self.get_fee_estimation_refund(),
296
+ fees=self.get_fee_estimation_refund(),
315
297
  )
316
298
  return self.send_onchain_tx(transaction)
317
299
 
@@ -21,8 +21,6 @@ config = BoltzConfig()
21
21
  # network="regtest",
22
22
  # network_liquid="elementsregtest",
23
23
  # api_url="http://localhost:9001",
24
- # mempool_url="http://localhost:8999/api/v1",
25
- # mempool_liquid_url="http://localhost:8998/api/v1",
26
24
  # )
27
25
 
28
26
 
@@ -30,7 +28,7 @@ config = BoltzConfig()
30
28
  def command_group():
31
29
  """
32
30
  Python CLI of boltz-client-python, enjoy submarine swapping. :)
33
- Uses mempool.space for retrieving onchain data"""
31
+ """
34
32
 
35
33
 
36
34
  @click.command()
@@ -294,21 +292,10 @@ def show_pairs():
294
292
  click.echo(json.dumps(data))
295
293
 
296
294
 
297
- @click.command()
298
- def get_fees():
299
- """
300
- show mempool recommended fees
301
- """
302
- client = BoltzClient(config)
303
- fees = client.mempool.get_fees()
304
- click.echo(fees)
305
-
306
-
307
295
  def main():
308
296
  """main function"""
309
297
  command_group.add_command(swap_status)
310
298
  command_group.add_command(show_pairs)
311
- command_group.add_command(get_fees)
312
299
  command_group.add_command(create_swap)
313
300
  command_group.add_command(refund_swap)
314
301
  command_group.add_command(create_reverse_swap)
@@ -7,25 +7,12 @@ from embit import ec, script
7
7
  from embit.base import EmbitError
8
8
  from embit.liquid.addresses import to_unconfidential
9
9
  from embit.liquid.networks import NETWORKS as LNETWORKS
10
- from embit.liquid.transaction import LTransaction
11
10
  from embit.networks import NETWORKS
12
11
  from embit.transaction import SIGHASH, Transaction, TransactionInput, TransactionOutput
13
12
 
14
- from .mempool import LockupData
15
13
  from .onchain_wally import create_liquid_tx
16
14
 
17
15
 
18
- def get_txid(tx_hex: str, pair: str = "BTC/BTC") -> str:
19
- try:
20
- if pair == "L-BTC/BTC":
21
- tx = LTransaction.from_string(tx_hex)
22
- else:
23
- tx = Transaction.from_string(tx_hex)
24
- return tx.txid().hex()
25
- except EmbitError as exc:
26
- raise ValueError("Invalid transaction hex") from exc
27
-
28
-
29
16
  def validate_address(address: str, network: str, pair: str) -> str:
30
17
  if pair == "L-BTC/BTC":
31
18
  net = LNETWORKS[network]
@@ -69,7 +56,8 @@ def create_refund_tx(
69
56
  receive_address: str,
70
57
  redeem_script_hex: str,
71
58
  timeout_block_height: int,
72
- lockup_tx: LockupData,
59
+ lockup_address: str,
60
+ lockup_rawtx: str,
73
61
  pair: str,
74
62
  fees: int,
75
63
  blinding_key: Optional[str] = None,
@@ -79,10 +67,11 @@ def create_refund_tx(
79
67
  rs += sha256(bytes.fromhex(redeem_script_hex)).digest()
80
68
  script_sig = rs
81
69
  return create_onchain_tx(
70
+ lockup_address=lockup_address,
82
71
  sequence=0xFFFFFFFE,
83
72
  redeem_script_hex=redeem_script_hex,
84
73
  privkey_wif=privkey_wif,
85
- lockup_tx=lockup_tx,
74
+ lockup_rawtx=lockup_rawtx,
86
75
  receive_address=receive_address,
87
76
  timeout_block_height=timeout_block_height,
88
77
  script_sig=script_sig,
@@ -93,18 +82,20 @@ def create_refund_tx(
93
82
 
94
83
 
95
84
  def create_claim_tx(
85
+ lockup_address: str,
96
86
  preimage_hex: str,
97
87
  privkey_wif: str,
98
88
  receive_address: str,
99
89
  redeem_script_hex: str,
100
- lockup_tx: LockupData,
90
+ lockup_rawtx: str,
101
91
  fees: int,
102
92
  pair: str,
103
93
  blinding_key: Optional[str] = None,
104
94
  ) -> str:
105
95
  return create_onchain_tx(
96
+ lockup_address=lockup_address,
106
97
  preimage_hex=preimage_hex,
107
- lockup_tx=lockup_tx,
98
+ lockup_rawtx=lockup_rawtx,
108
99
  receive_address=receive_address,
109
100
  privkey_wif=privkey_wif,
110
101
  redeem_script_hex=redeem_script_hex,
@@ -115,7 +106,8 @@ def create_claim_tx(
115
106
 
116
107
 
117
108
  def create_onchain_tx(
118
- lockup_tx: LockupData,
109
+ lockup_address: str,
110
+ lockup_rawtx: str,
119
111
  receive_address: str,
120
112
  privkey_wif: str,
121
113
  redeem_script_hex: str,
@@ -133,7 +125,8 @@ def create_onchain_tx(
133
125
  raise ValueError("Blinding key is required for L-BTC/BTC pair")
134
126
 
135
127
  return create_liquid_tx(
136
- lockup_tx=lockup_tx,
128
+ lockup_rawtx=lockup_rawtx,
129
+ lockup_address=lockup_address,
137
130
  receive_address=receive_address,
138
131
  privkey_wif=privkey_wif,
139
132
  redeem_script_hex=redeem_script_hex,
@@ -144,14 +137,31 @@ def create_onchain_tx(
144
137
  blinding_key=blinding_key,
145
138
  )
146
139
 
140
+ try:
141
+ lockup_transaction = Transaction.from_string(lockup_rawtx)
142
+ except EmbitError as exc:
143
+ raise ValueError("Invalid lockup transaction hex") from exc
144
+
145
+ txid = lockup_transaction.txid()
146
+ vout_amount: Optional[int] = None
147
+ vout_index: int = 0
148
+ for vout in lockup_transaction.vout:
149
+ if vout.script_pubkey == script.address_to_scriptpubkey(lockup_address):
150
+ vout_amount = vout.value
151
+ break
152
+ vout_index += 1
153
+
154
+ if vout_amount is None:
155
+ raise ValueError("No matching vout found in lockup transaction")
156
+
147
157
  vout = TransactionOutput(
148
- lockup_tx.vout_amount - fees,
158
+ vout_amount - fees,
149
159
  script.address_to_scriptpubkey(receive_address),
150
160
  )
151
161
  vout = [vout]
152
162
  vin = TransactionInput(
153
- bytes.fromhex(lockup_tx.txid),
154
- lockup_tx.vout_cnt,
163
+ txid,
164
+ vout_index,
155
165
  sequence=sequence,
156
166
  script_sig=script.Script(data=script_sig) if script_sig else None,
157
167
  )
@@ -161,7 +171,7 @@ def create_onchain_tx(
161
171
  tx.locktime = timeout_block_height
162
172
 
163
173
  redeem_script = script.Script(data=bytes.fromhex(redeem_script_hex))
164
- h = tx.sighash_segwit(0, redeem_script, lockup_tx.vout_amount)
174
+ h = tx.sighash_segwit(0, redeem_script, vout_amount)
165
175
  sig = ec.PrivateKey.from_wif(privkey_wif).sign(h).serialize() + bytes([SIGHASH.ALL])
166
176
  witness_script = script.Witness(
167
177
  items=[sig, bytes.fromhex(preimage_hex), bytes.fromhex(redeem_script_hex)]
@@ -10,8 +10,6 @@ import secrets
10
10
  from dataclasses import dataclass
11
11
  from typing import Any, Optional
12
12
 
13
- from .mempool import LockupData
14
-
15
13
 
16
14
  @dataclass
17
15
  class Network:
@@ -147,7 +145,8 @@ def decode_address(
147
145
 
148
146
 
149
147
  def create_liquid_tx(
150
- lockup_tx: LockupData,
148
+ lockup_rawtx: str,
149
+ lockup_address: str,
151
150
  receive_address: str,
152
151
  privkey_wif: str,
153
152
  redeem_script_hex: str,
@@ -184,19 +183,19 @@ def create_liquid_tx(
184
183
  wally, network, receive_address
185
184
  )
186
185
 
186
+ _, lockup_script_pubkey = decode_address(wally, network, lockup_address)
187
+
187
188
  # parse lockup tx
188
189
  lockup_transaction = wally.tx_from_hex(
189
- lockup_tx.tx_hex, wally.WALLY_TX_FLAG_USE_ELEMENTS
190
+ lockup_rawtx, wally.WALLY_TX_FLAG_USE_ELEMENTS
190
191
  )
191
192
  vout_n: Optional[int] = None
192
193
  for vout in range(wally.tx_get_num_outputs(lockup_transaction)):
193
194
  script_out = wally.tx_get_output_script(lockup_transaction, vout) # type: ignore
194
-
195
- # Lockup addresses on liquid are always bech32
196
- pub_key = wally.addr_segwit_from_bytes(script_out, network.bech32_prefix, 0)
197
- if pub_key == lockup_tx.script_pub_key:
198
- vout_n = vout
199
- break
195
+ if script_out:
196
+ if script_out == lockup_script_pubkey:
197
+ vout_n = vout
198
+ break
200
199
 
201
200
  assert vout_n is not None, "Lockup vout not found"
202
201
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "boltz_client"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "python boltz client"
5
5
  license = "MIT"
6
6
  authors = ["dni <office@dnilabs.com>"]
@@ -1,50 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: boltz_client
3
- Version: 0.2.1
4
- Summary: python boltz client
5
- Home-page: https://boltz.exchange
6
- License: MIT
7
- Author: dni
8
- Author-email: office@dnilabs.com
9
- Requires-Python: >=3.9,<4.0
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Requires-Dist: click (>=8)
17
- Requires-Dist: embit (>=0.7.0,<0.8.0)
18
- Requires-Dist: httpx (>=0.23)
19
- Requires-Dist: wallycore (==1.0.0)
20
- Requires-Dist: websockets (>=10)
21
- Project-URL: Repository, https://github.com/dni/boltz-client-python
22
- Description-Content-Type: text/markdown
23
-
24
- # Boltz Python Client
25
- Boltz Reference Client in Python, used by e.g. https://github.com/lnbits/boltz
26
-
27
- ## installing
28
- ```console
29
- poetry install
30
- ```
31
-
32
- ## running cli
33
- ```console
34
- poetry run boltz
35
- ```
36
-
37
- ## starting regtest
38
- ```console
39
- cd docker
40
- chmod +x regtest
41
- ./regtest
42
- ```
43
-
44
- ## running tests
45
- ```console
46
- poetry run pytest
47
- ```
48
-
49
- Or install the latest release from [PyPI](https://pypi.org/project/boltz-client) via `pip install boltz-client`.
50
-
@@ -1,26 +0,0 @@
1
- # Boltz Python Client
2
- Boltz Reference Client in Python, used by e.g. https://github.com/lnbits/boltz
3
-
4
- ## installing
5
- ```console
6
- poetry install
7
- ```
8
-
9
- ## running cli
10
- ```console
11
- poetry run boltz
12
- ```
13
-
14
- ## starting regtest
15
- ```console
16
- cd docker
17
- chmod +x regtest
18
- ./regtest
19
- ```
20
-
21
- ## running tests
22
- ```console
23
- poetry run pytest
24
- ```
25
-
26
- Or install the latest release from [PyPI](https://pypi.org/project/boltz-client) via `pip install boltz-client`.
@@ -1,189 +0,0 @@
1
- """ boltz_client mempool module """
2
-
3
- import asyncio
4
- import json
5
- from dataclasses import dataclass
6
- from typing import Optional
7
-
8
- import httpx
9
- from websockets.client import connect
10
- from websockets.exceptions import ConnectionClosed
11
-
12
- from .helpers import req_wrap
13
-
14
-
15
- @dataclass
16
- class LockupData:
17
- status: str
18
- tx_hex: str
19
- txid: str
20
- script_pub_key: str
21
- vout_cnt: int
22
- vout_amount: int
23
-
24
-
25
- class MempoolApiException(Exception):
26
- pass
27
-
28
-
29
- class MempoolBlockHeightException(Exception):
30
- pass
31
-
32
-
33
- class MempoolClient:
34
- def __init__(self, url):
35
- self._api_url = url
36
- ws_url = url.replace("https", "wss")
37
- ws_url = url.replace("http", "ws")
38
- ws_url += "/ws"
39
- self._ws_url = ws_url
40
- # just check if mempool is available
41
- self.get_blockheight()
42
-
43
- def request(self, funcname, *args, **kwargs) -> dict:
44
- try:
45
- return req_wrap(funcname, *args, **kwargs)
46
- except httpx.RequestError as exc:
47
- msg = f"unreachable: {exc.request.url!r}."
48
- raise MempoolApiException(f"mempool api connection error: {msg}") from exc
49
- except httpx.HTTPStatusError as exc:
50
- msg = (
51
- f"{exc.response.status_code} while requesting "
52
- f"{exc.request.url!r}. message: {exc.response.text}"
53
- )
54
- raise MempoolApiException(f"mempool api status error: {msg}") from exc
55
-
56
- async def wait_for_websocket_message(self, send, message_key):
57
- async for websocket in connect(self._ws_url):
58
- try:
59
- await websocket.send(json.dumps({"action": "want", "data": ["blocks"]}))
60
- await websocket.send(json.dumps(send))
61
- async for raw in websocket:
62
- message = json.loads(raw)
63
- if message_key in message:
64
- return message.get(message_key)
65
- except ConnectionClosed:
66
- continue
67
-
68
- async def wait_for_one_websocket_message(self, send):
69
- async with connect(self._ws_url) as websocket:
70
- await websocket.send(
71
- json.dumps({"action": "want", "data": ["blocks", "mempool-blocks"]})
72
- )
73
- await websocket.send(json.dumps(send))
74
- raw = await asyncio.wait_for(websocket.recv(), timeout=10)
75
- return json.loads(raw) if raw else None
76
-
77
- async def wait_for_tx_confirmed(self, txid: str):
78
- return await self.wait_for_websocket_message({"track-tx": txid}, "txConfirmed")
79
-
80
- async def wait_for_lockup_tx(self, address: str) -> LockupData:
81
- message = await self.wait_for_websocket_message(
82
- {"track-address": address}, "address-transactions"
83
- )
84
- if not message:
85
- # restart
86
- return await self.wait_for_lockup_tx(address)
87
- lockup_tx = self.find_tx_and_output(message, address)
88
- if not lockup_tx:
89
- # restart
90
- return await self.wait_for_lockup_tx(address)
91
- return lockup_tx
92
-
93
- def find_tx_and_output(self, txs, address: str) -> Optional[LockupData]:
94
- if len(txs) == 0:
95
- return None
96
- for tx in txs:
97
- output = self.find_output(tx, address)
98
- if output:
99
- return output
100
- return None
101
-
102
- def find_output(self, tx, address: str) -> Optional[LockupData]:
103
- for i, vout in enumerate(tx["vout"]):
104
- if vout["scriptpubkey_address"] == address:
105
- status = "confirmed" if tx["status"]["confirmed"] else "unconfirmed"
106
- return LockupData(
107
- tx_hex=self.get_tx_hex(tx["txid"]),
108
- txid=tx["txid"],
109
- script_pub_key=vout["scriptpubkey_address"],
110
- vout_cnt=i,
111
- vout_amount=vout.get("value") or 0,
112
- status=status,
113
- )
114
- return None
115
-
116
- def get_tx(self, txid: str):
117
- return self.request(
118
- "get",
119
- f"{self._api_url}/tx/{txid}",
120
- headers={"Content-Type": "application/json"},
121
- )
122
-
123
- def get_tx_hex(self, txid: str) -> str:
124
- return self.request(
125
- "get",
126
- f"{self._api_url}/tx/{txid}/hex",
127
- headers={"Content-Type": "text/plain"},
128
- )["text"]
129
-
130
- def get_txs_from_address(self, address: str):
131
- return self.request(
132
- "get",
133
- f"{self._api_url}/address/{address}/txs",
134
- headers={"Content-Type": "application/json"},
135
- )
136
-
137
- async def get_tx_from_txid(self, txid: str, address: str) -> LockupData:
138
- while True:
139
- try:
140
- tx = self.get_tx(txid)
141
- output = self.find_output(tx, address)
142
- if output:
143
- return output
144
- except MempoolApiException:
145
- pass
146
- await asyncio.sleep(3)
147
-
148
- async def get_tx_from_address(self, address: str) -> LockupData:
149
- txs = self.get_txs_from_address(address)
150
- if len(txs) == 0:
151
- return await self.wait_for_lockup_tx(address)
152
- lockup_tx = self.find_tx_and_output(txs, address)
153
- if not lockup_tx:
154
- return await self.wait_for_lockup_tx(address)
155
- return lockup_tx
156
-
157
- def get_fees(self) -> int:
158
- # mempool.space quirk, needed for regtest
159
- api_url = self._api_url.replace("/v1", "")
160
- data = self.request(
161
- "get",
162
- f"{api_url}/v1/fees/recommended",
163
- headers={"Content-Type": "application/json"},
164
- )
165
- return int(data["halfHourFee"])
166
-
167
- def get_blockheight(self) -> int:
168
- data = self.request(
169
- "get",
170
- f"{self._api_url}/blocks/tip/height",
171
- headers={"Content-Type": "text/plain"},
172
- )
173
- return int(data["text"])
174
-
175
- def check_block_height(self, timeout_block_height: int) -> None:
176
- current_block_height = self.get_blockheight()
177
- if current_block_height < timeout_block_height:
178
- raise MempoolBlockHeightException(
179
- f"current_block_height ({current_block_height}) "
180
- f"has not yet exceeded ({timeout_block_height})"
181
- )
182
-
183
- def send_onchain_tx(self, tx_hex: str):
184
- return self.request(
185
- "post",
186
- f"{self._api_url}/tx",
187
- headers={"Content-Type": "text/plain"},
188
- content=tx_hex,
189
- )
File without changes