boltz_client 0.2.0__tar.gz → 0.2.1__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: boltz_client
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: python boltz client
5
5
  Home-page: https://boltz.exchange
6
6
  License: MIT
@@ -100,8 +100,8 @@ class BoltzConfig:
100
100
  network_liquid: str = "liquidv1"
101
101
  pairs: list = field(default_factory=lambda: ["BTC/BTC", "L-BTC/BTC"])
102
102
  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"
103
+ mempool_url: str = "https://mempool.space/api"
104
+ mempool_liquid_url: str = "https://liquid.network/api"
105
105
  referral_id: str = "dni"
106
106
 
107
107
 
@@ -294,10 +294,21 @@ def show_pairs():
294
294
  click.echo(json.dumps(data))
295
295
 
296
296
 
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
+
297
307
  def main():
298
308
  """main function"""
299
309
  command_group.add_command(swap_status)
300
310
  command_group.add_command(show_pairs)
311
+ command_group.add_command(get_fees)
301
312
  command_group.add_command(create_swap)
302
313
  command_group.add_command(refund_swap)
303
314
  command_group.add_command(create_reverse_swap)
@@ -108,7 +108,7 @@ class MempoolClient:
108
108
  txid=tx["txid"],
109
109
  script_pub_key=vout["scriptpubkey_address"],
110
110
  vout_cnt=i,
111
- vout_amount=vout["value"],
111
+ vout_amount=vout.get("value") or 0,
112
112
  status=status,
113
113
  )
114
114
  return None
@@ -136,10 +136,13 @@ class MempoolClient:
136
136
 
137
137
  async def get_tx_from_txid(self, txid: str, address: str) -> LockupData:
138
138
  while True:
139
- tx = self.get_tx(txid)
140
- output = self.find_output(tx, address)
141
- if output:
142
- return output
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
143
146
  await asyncio.sleep(3)
144
147
 
145
148
  async def get_tx_from_address(self, address: str) -> LockupData:
@@ -152,9 +155,11 @@ class MempoolClient:
152
155
  return lockup_tx
153
156
 
154
157
  def get_fees(self) -> int:
158
+ # mempool.space quirk, needed for regtest
159
+ api_url = self._api_url.replace("/v1", "")
155
160
  data = self.request(
156
161
  "get",
157
- f"{self._api_url}/fees/recommended",
162
+ f"{api_url}/v1/fees/recommended",
158
163
  headers={"Content-Type": "application/json"},
159
164
  )
160
165
  return int(data["halfHourFee"])
@@ -7,12 +7,73 @@ 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
10
+ from dataclasses import dataclass
11
+ from typing import Any, Optional
11
12
 
12
13
  from .mempool import LockupData
13
14
 
14
15
 
15
- def get_entropy(num_outputs_to_blind):
16
+ @dataclass
17
+ class Network:
18
+ name: str
19
+ lbtc_asset: bytes
20
+ blech32_prefix: str
21
+ bech32_prefix: str
22
+
23
+ def wif_net(self, wally) -> Any:
24
+ if self.name == "mainnet":
25
+ return wally.WALLY_ADDRESS_VERSION_WIF_MAINNET
26
+ return wally.WALLY_ADDRESS_VERSION_WIF_TESTNET
27
+
28
+ def blinded_prefix(self, wally) -> Any:
29
+ if self.name == "mainnet":
30
+ return wally.WALLY_CA_PREFIX_LIQUID
31
+ if self.name == "testnet":
32
+ return wally.WALLY_CA_PREFIX_LIQUID_TESTNET
33
+ return wally.WALLY_CA_PREFIX_LIQUID_REGTEST
34
+
35
+ def wally_network(self, wally) -> Any:
36
+ if self.name == "mainnet":
37
+ return wally.WALLY_NETWORK_LIQUID
38
+ if self.name == "testnet":
39
+ return wally.WALLY_NETWORK_LIQUID_TESTNET
40
+ return wally.WALLY_NETWORK_LIQUID_REGTEST
41
+
42
+ @staticmethod
43
+ def parse_asset(asset: str) -> bytes:
44
+ return bytes.fromhex(asset)[::-1]
45
+
46
+
47
+ # TODO: is this type hint compatible with all support Python versions of lnbits
48
+ NETWORKS: list[Network] = [
49
+ Network(
50
+ name="mainnet",
51
+ lbtc_asset=Network.parse_asset(
52
+ "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
53
+ ),
54
+ blech32_prefix="lq",
55
+ bech32_prefix="ex",
56
+ ),
57
+ Network(
58
+ name="testnet",
59
+ lbtc_asset=Network.parse_asset(
60
+ "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
61
+ ),
62
+ blech32_prefix="tlq",
63
+ bech32_prefix="tex",
64
+ ),
65
+ Network(
66
+ name="regtest",
67
+ lbtc_asset=Network.parse_asset(
68
+ "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
69
+ ),
70
+ blech32_prefix="el",
71
+ bech32_prefix="ert",
72
+ ),
73
+ ]
74
+
75
+
76
+ def get_entropy(num_outputs_to_blind: int) -> bytes:
16
77
  # For each output to blind, we need 32 bytes of entropy for each of:
17
78
  # - Output assetblinder
18
79
  # - Output amountblinder
@@ -22,6 +83,69 @@ def get_entropy(num_outputs_to_blind):
22
83
  return secrets.token_bytes(num_outputs_to_blind * 5 * 32)
23
84
 
24
85
 
86
+ def get_address_network(wally, address: str) -> Network:
87
+ def address_has_network_prefix(n: Network) -> bool:
88
+ # If address decoding doesn't fail -> correct network
89
+ try:
90
+ decode_address(wally, n, address)
91
+ return True
92
+ except Exception:
93
+ return False
94
+
95
+ network = next(
96
+ (network for network in NETWORKS if address_has_network_prefix(network)),
97
+ None,
98
+ )
99
+
100
+ if network is None:
101
+ raise ValueError("Unknown network of address")
102
+
103
+ return network
104
+
105
+
106
+ def is_possible_confidential_address(wally, address) -> bool:
107
+ expected_len = (
108
+ 2 + wally.EC_PUBLIC_KEY_LEN + wally.HASH160_LEN + wally.BASE58_CHECKSUM_LEN
109
+ )
110
+ try:
111
+ return wally.base58_n_get_length(address, len(address)) == expected_len
112
+ except ValueError:
113
+ return False
114
+
115
+
116
+ # TODO: is this type hint compatible with all support Python versions of lnbits
117
+ def decode_address(
118
+ wally, network: Network, address: str
119
+ ) -> tuple[bytearray, bytearray]:
120
+ if address.lower().startswith(network.blech32_prefix):
121
+ blinding_key = wally.confidential_addr_segwit_to_ec_public_key(
122
+ address, network.blech32_prefix
123
+ )
124
+ unconfidential_address = wally.confidential_addr_to_addr_segwit(
125
+ address, network.blech32_prefix, network.bech32_prefix
126
+ )
127
+
128
+ return blinding_key, wally.addr_segwit_to_bytes(
129
+ unconfidential_address, network.bech32_prefix, 0
130
+ )
131
+
132
+ if is_possible_confidential_address(wally, address):
133
+ unconfidential_address = wally.confidential_addr_to_addr(
134
+ address, network.blinded_prefix(wally)
135
+ )
136
+
137
+ blinding_key = wally.confidential_addr_to_ec_public_key(
138
+ address,
139
+ network.blinded_prefix(wally),
140
+ )
141
+
142
+ return blinding_key, wally.address_to_scriptpubkey(
143
+ unconfidential_address, network.wally_network(wally)
144
+ )
145
+
146
+ raise ValueError("only confidential addresses are supported")
147
+
148
+
25
149
  def create_liquid_tx(
26
150
  lockup_tx: LockupData,
27
151
  receive_address: str,
@@ -33,7 +157,6 @@ def create_liquid_tx(
33
157
  preimage_hex: str = "",
34
158
  blinding_key: Optional[str] = None,
35
159
  ) -> str:
36
-
37
160
  try:
38
161
  import wallycore as wally
39
162
  except ImportError as exc:
@@ -41,28 +164,13 @@ def create_liquid_tx(
41
164
  "`wallycore` is not installed, but required for liquid support."
42
165
  ) from exc
43
166
 
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]
167
+ network = get_address_network(wally, receive_address)
60
168
 
61
169
  redeem_script = bytes.fromhex(redeem_script_hex)
62
170
  preimage = bytes.fromhex(preimage_hex)
63
171
  private_key = wally.wif_to_bytes(
64
172
  privkey_wif,
65
- wally.WALLY_ADDRESS_VERSION_WIF_TESTNET,
173
+ network.wif_net(wally),
66
174
  wally.WALLY_WIF_FLAG_COMPRESSED,
67
175
  ) # type: ignore
68
176
 
@@ -72,15 +180,9 @@ def create_liquid_tx(
72
180
  except ValueError as exc:
73
181
  raise ValueError("blinding_key must be hex encoded") from exc
74
182
 
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
183
+ receive_blinding_pubkey, receive_script_pubkey = decode_address(
184
+ wally, network, receive_address
185
+ )
84
186
 
85
187
  # parse lockup tx
86
188
  lockup_transaction = wally.tx_from_hex(
@@ -89,7 +191,9 @@ def create_liquid_tx(
89
191
  vout_n: Optional[int] = None
90
192
  for vout in range(wally.tx_get_num_outputs(lockup_transaction)):
91
193
  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)
194
+
195
+ # Lockup addresses on liquid are always bech32
196
+ pub_key = wally.addr_segwit_from_bytes(script_out, network.bech32_prefix, 0)
93
197
  if pub_key == lockup_tx.script_pub_key:
94
198
  vout_n = vout
95
199
  break
@@ -113,7 +217,7 @@ def create_liquid_tx(
113
217
  lockup_asset_commitment,
114
218
  ) # type: ignore
115
219
 
116
- assert unblinded_asset == LASSET, "Wrong asset"
220
+ assert unblinded_asset == network.lbtc_asset, "Wrong asset"
117
221
 
118
222
  # INITIALIZE PSBT (PSET)
119
223
  num_vin = 1
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "boltz_client"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "python boltz client"
5
5
  license = "MIT"
6
6
  authors = ["dni <office@dnilabs.com>"]
@@ -63,4 +63,5 @@ disable = [
63
63
  "too-many-instance-attributes",
64
64
  "too-many-locals",
65
65
  "too-many-statements",
66
+ "broad-exception-caught",
66
67
  ]
File without changes
File without changes