boltz_client 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

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.

boltz_client/boltz.py CHANGED
@@ -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/v1"
104
- mempool_liquid_url: str = "https://liquid.network/api/v1"
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
 
boltz_client/cli.py CHANGED
@@ -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()
boltz_client/onchain.py CHANGED
@@ -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)]
@@ -7,12 +7,71 @@ special thanks to @jgriffiths for helping debugging this!
7
7
  from __future__ import annotations
8
8
 
9
9
  import secrets
10
- from typing import Optional
11
-
12
- from .mempool import LockupData
13
-
14
-
15
- def get_entropy(num_outputs_to_blind):
10
+ from dataclasses import dataclass
11
+ from typing import Any, Optional
12
+
13
+
14
+ @dataclass
15
+ class Network:
16
+ name: str
17
+ lbtc_asset: bytes
18
+ blech32_prefix: str
19
+ bech32_prefix: str
20
+
21
+ def wif_net(self, wally) -> Any:
22
+ if self.name == "mainnet":
23
+ return wally.WALLY_ADDRESS_VERSION_WIF_MAINNET
24
+ return wally.WALLY_ADDRESS_VERSION_WIF_TESTNET
25
+
26
+ def blinded_prefix(self, wally) -> Any:
27
+ if self.name == "mainnet":
28
+ return wally.WALLY_CA_PREFIX_LIQUID
29
+ if self.name == "testnet":
30
+ return wally.WALLY_CA_PREFIX_LIQUID_TESTNET
31
+ return wally.WALLY_CA_PREFIX_LIQUID_REGTEST
32
+
33
+ def wally_network(self, wally) -> Any:
34
+ if self.name == "mainnet":
35
+ return wally.WALLY_NETWORK_LIQUID
36
+ if self.name == "testnet":
37
+ return wally.WALLY_NETWORK_LIQUID_TESTNET
38
+ return wally.WALLY_NETWORK_LIQUID_REGTEST
39
+
40
+ @staticmethod
41
+ def parse_asset(asset: str) -> bytes:
42
+ return bytes.fromhex(asset)[::-1]
43
+
44
+
45
+ # TODO: is this type hint compatible with all support Python versions of lnbits
46
+ NETWORKS: list[Network] = [
47
+ Network(
48
+ name="mainnet",
49
+ lbtc_asset=Network.parse_asset(
50
+ "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
51
+ ),
52
+ blech32_prefix="lq",
53
+ bech32_prefix="ex",
54
+ ),
55
+ Network(
56
+ name="testnet",
57
+ lbtc_asset=Network.parse_asset(
58
+ "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
59
+ ),
60
+ blech32_prefix="tlq",
61
+ bech32_prefix="tex",
62
+ ),
63
+ Network(
64
+ name="regtest",
65
+ lbtc_asset=Network.parse_asset(
66
+ "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
67
+ ),
68
+ blech32_prefix="el",
69
+ bech32_prefix="ert",
70
+ ),
71
+ ]
72
+
73
+
74
+ def get_entropy(num_outputs_to_blind: int) -> bytes:
16
75
  # For each output to blind, we need 32 bytes of entropy for each of:
17
76
  # - Output assetblinder
18
77
  # - Output amountblinder
@@ -22,8 +81,72 @@ def get_entropy(num_outputs_to_blind):
22
81
  return secrets.token_bytes(num_outputs_to_blind * 5 * 32)
23
82
 
24
83
 
84
+ def get_address_network(wally, address: str) -> Network:
85
+ def address_has_network_prefix(n: Network) -> bool:
86
+ # If address decoding doesn't fail -> correct network
87
+ try:
88
+ decode_address(wally, n, address)
89
+ return True
90
+ except Exception:
91
+ return False
92
+
93
+ network = next(
94
+ (network for network in NETWORKS if address_has_network_prefix(network)),
95
+ None,
96
+ )
97
+
98
+ if network is None:
99
+ raise ValueError("Unknown network of address")
100
+
101
+ return network
102
+
103
+
104
+ def is_possible_confidential_address(wally, address) -> bool:
105
+ expected_len = (
106
+ 2 + wally.EC_PUBLIC_KEY_LEN + wally.HASH160_LEN + wally.BASE58_CHECKSUM_LEN
107
+ )
108
+ try:
109
+ return wally.base58_n_get_length(address, len(address)) == expected_len
110
+ except ValueError:
111
+ return False
112
+
113
+
114
+ # TODO: is this type hint compatible with all support Python versions of lnbits
115
+ def decode_address(
116
+ wally, network: Network, address: str
117
+ ) -> tuple[bytearray, bytearray]:
118
+ if address.lower().startswith(network.blech32_prefix):
119
+ blinding_key = wally.confidential_addr_segwit_to_ec_public_key(
120
+ address, network.blech32_prefix
121
+ )
122
+ unconfidential_address = wally.confidential_addr_to_addr_segwit(
123
+ address, network.blech32_prefix, network.bech32_prefix
124
+ )
125
+
126
+ return blinding_key, wally.addr_segwit_to_bytes(
127
+ unconfidential_address, network.bech32_prefix, 0
128
+ )
129
+
130
+ if is_possible_confidential_address(wally, address):
131
+ unconfidential_address = wally.confidential_addr_to_addr(
132
+ address, network.blinded_prefix(wally)
133
+ )
134
+
135
+ blinding_key = wally.confidential_addr_to_ec_public_key(
136
+ address,
137
+ network.blinded_prefix(wally),
138
+ )
139
+
140
+ return blinding_key, wally.address_to_scriptpubkey(
141
+ unconfidential_address, network.wally_network(wally)
142
+ )
143
+
144
+ raise ValueError("only confidential addresses are supported")
145
+
146
+
25
147
  def create_liquid_tx(
26
- lockup_tx: LockupData,
148
+ lockup_rawtx: str,
149
+ lockup_address: str,
27
150
  receive_address: str,
28
151
  privkey_wif: str,
29
152
  redeem_script_hex: str,
@@ -33,7 +156,6 @@ def create_liquid_tx(
33
156
  preimage_hex: str = "",
34
157
  blinding_key: Optional[str] = None,
35
158
  ) -> str:
36
-
37
159
  try:
38
160
  import wallycore as wally
39
161
  except ImportError as exc:
@@ -41,28 +163,13 @@ def create_liquid_tx(
41
163
  "`wallycore` is not installed, but required for liquid support."
42
164
  ) from exc
43
165
 
44
- if receive_address.startswith("ert") or receive_address.startswith("el"):
45
- lasset_hex = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
46
- confidential_addr_prefix = "ert"
47
- confidential_addr_family = "el"
48
- elif receive_address.startswith("tex") or receive_address.startswith("tlq"):
49
- lasset_hex = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
50
- confidential_addr_prefix = "tex"
51
- confidential_addr_family = "tlq"
52
- elif receive_address.startswith("ex") or receive_address.startswith("lq"):
53
- lasset_hex = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
54
- confidential_addr_prefix = "ex"
55
- confidential_addr_family = "lq"
56
- else:
57
- raise ValueError(f"Unknown prefix: {receive_address[:3]}")
58
-
59
- LASSET = bytes.fromhex(lasset_hex)[::-1]
166
+ network = get_address_network(wally, receive_address)
60
167
 
61
168
  redeem_script = bytes.fromhex(redeem_script_hex)
62
169
  preimage = bytes.fromhex(preimage_hex)
63
170
  private_key = wally.wif_to_bytes(
64
171
  privkey_wif,
65
- wally.WALLY_ADDRESS_VERSION_WIF_TESTNET,
172
+ network.wif_net(wally),
66
173
  wally.WALLY_WIF_FLAG_COMPRESSED,
67
174
  ) # type: ignore
68
175
 
@@ -72,27 +179,23 @@ def create_liquid_tx(
72
179
  except ValueError as exc:
73
180
  raise ValueError("blinding_key must be hex encoded") from exc
74
181
 
75
- receive_blinding_pubkey = wally.confidential_addr_segwit_to_ec_public_key(
76
- receive_address, confidential_addr_family
77
- ) # type: ignore
78
- receive_unconfidential_address = wally.confidential_addr_to_addr_segwit(
79
- receive_address, confidential_addr_family, confidential_addr_prefix
80
- ) # type: ignore
81
- receive_script_pubkey = wally.addr_segwit_to_bytes(
82
- receive_unconfidential_address, confidential_addr_prefix, 0
83
- ) # type: ignore
182
+ receive_blinding_pubkey, receive_script_pubkey = decode_address(
183
+ wally, network, receive_address
184
+ )
185
+
186
+ _, lockup_script_pubkey = decode_address(wally, network, lockup_address)
84
187
 
85
188
  # parse lockup tx
86
189
  lockup_transaction = wally.tx_from_hex(
87
- lockup_tx.tx_hex, wally.WALLY_TX_FLAG_USE_ELEMENTS
190
+ lockup_rawtx, wally.WALLY_TX_FLAG_USE_ELEMENTS
88
191
  )
89
192
  vout_n: Optional[int] = None
90
193
  for vout in range(wally.tx_get_num_outputs(lockup_transaction)):
91
194
  script_out = wally.tx_get_output_script(lockup_transaction, vout) # type: ignore
92
- pub_key = wally.addr_segwit_from_bytes(script_out, confidential_addr_prefix, 0)
93
- if pub_key == lockup_tx.script_pub_key:
94
- vout_n = vout
95
- break
195
+ if script_out:
196
+ if script_out == lockup_script_pubkey:
197
+ vout_n = vout
198
+ break
96
199
 
97
200
  assert vout_n is not None, "Lockup vout not found"
98
201
 
@@ -113,7 +216,7 @@ def create_liquid_tx(
113
216
  lockup_asset_commitment,
114
217
  ) # type: ignore
115
218
 
116
- assert unblinded_asset == LASSET, "Wrong asset"
219
+ assert unblinded_asset == network.lbtc_asset, "Wrong asset"
117
220
 
118
221
  # INITIALIZE PSBT (PSET)
119
222
  num_vin = 1
@@ -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,12 @@
1
+ boltz_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ boltz_client/boltz.py,sha256=ZBTX3IZM1nLV5iwZJLNSNMkMaOZXnCEGPnZZPSy6HBM,11052
3
+ boltz_client/cli.py,sha256=OIqpRCMkBAiAvU8nA4mGnuzOBCR0C_o9icahUh6pWLU,9365
4
+ boltz_client/helpers.py,sha256=pJVQKnki2xE9zJTz_ra_JIqSeet0A-jKDMi3AGj0dwE,374
5
+ boltz_client/onchain.py,sha256=7-LJ1-O3vlCrwtS47HW4LOt_AB-skz8DsgnX_G6QvJM,5556
6
+ boltz_client/onchain_wally.py,sha256=QIWLpc4zHCb4zmz7HRkTnZghkIO-2WAC0gQwIt1kTfc,12081
7
+ boltz_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ boltz_client-0.2.2.dist-info/LICENSE,sha256=ums-fB6x3ajzBZydnEwnKxO1n6oXTNr9NGdOYiWucmU,1062
9
+ boltz_client-0.2.2.dist-info/METADATA,sha256=hAyIswWKc3t_L-csYIzspHT21SriNFQsPcBx2415vz0,3552
10
+ boltz_client-0.2.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
11
+ boltz_client-0.2.2.dist-info/entry_points.txt,sha256=9u_t10YVlH8kHbl-M0nOZw91F3Fx4HJOZdSf2s42dhM,47
12
+ boltz_client-0.2.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
boltz_client/mempool.py DELETED
@@ -1,184 +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["value"],
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
- tx = self.get_tx(txid)
140
- output = self.find_output(tx, address)
141
- if output:
142
- return output
143
- await asyncio.sleep(3)
144
-
145
- async def get_tx_from_address(self, address: str) -> LockupData:
146
- txs = self.get_txs_from_address(address)
147
- if len(txs) == 0:
148
- return await self.wait_for_lockup_tx(address)
149
- lockup_tx = self.find_tx_and_output(txs, address)
150
- if not lockup_tx:
151
- return await self.wait_for_lockup_tx(address)
152
- return lockup_tx
153
-
154
- def get_fees(self) -> int:
155
- data = self.request(
156
- "get",
157
- f"{self._api_url}/fees/recommended",
158
- headers={"Content-Type": "application/json"},
159
- )
160
- return int(data["halfHourFee"])
161
-
162
- def get_blockheight(self) -> int:
163
- data = self.request(
164
- "get",
165
- f"{self._api_url}/blocks/tip/height",
166
- headers={"Content-Type": "text/plain"},
167
- )
168
- return int(data["text"])
169
-
170
- def check_block_height(self, timeout_block_height: int) -> None:
171
- current_block_height = self.get_blockheight()
172
- if current_block_height < timeout_block_height:
173
- raise MempoolBlockHeightException(
174
- f"current_block_height ({current_block_height}) "
175
- f"has not yet exceeded ({timeout_block_height})"
176
- )
177
-
178
- def send_onchain_tx(self, tx_hex: str):
179
- return self.request(
180
- "post",
181
- f"{self._api_url}/tx",
182
- headers={"Content-Type": "text/plain"},
183
- content=tx_hex,
184
- )
@@ -1,50 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: boltz_client
3
- Version: 0.2.0
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,13 +0,0 @@
1
- boltz_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- boltz_client/boltz.py,sha256=SKqfcOLOoYPdFuME1uqnaWTnsKcMd1TU5KBDQkfjdsg,11955
3
- boltz_client/cli.py,sha256=DhA7lWUtdeVBBO-nHW2n8KEsV7RydtEBVcBlE-_FV3E,9518
4
- boltz_client/helpers.py,sha256=pJVQKnki2xE9zJTz_ra_JIqSeet0A-jKDMi3AGj0dwE,374
5
- boltz_client/mempool.py,sha256=-PxivS27PhH-fhgKiXy4YzJ9_DEqVUwkJ4Ec9_vQvFk,6209
6
- boltz_client/onchain.py,sha256=BV4vsOJJKC4OlsIhepYj2rEC4p-2Bs9xB2FKiQQwpqc,5246
7
- boltz_client/onchain_wally.py,sha256=Z2r0uxY2pf4FgdnS9fhHE9rO3_zAZv2kWMDGE4B8f18,9437
8
- boltz_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- boltz_client-0.2.0.dist-info/LICENSE,sha256=ums-fB6x3ajzBZydnEwnKxO1n6oXTNr9NGdOYiWucmU,1062
10
- boltz_client-0.2.0.dist-info/METADATA,sha256=RCEm1OAD6VIvAMGAwzQtquJOzUG7p2bqLO67JvTMo6M,1212
11
- boltz_client-0.2.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
12
- boltz_client-0.2.0.dist-info/entry_points.txt,sha256=9u_t10YVlH8kHbl-M0nOZw91F3Fx4HJOZdSf2s42dhM,47
13
- boltz_client-0.2.0.dist-info/RECORD,,