boltz_client 0.2.1__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 +26 -44
- boltz_client/cli.py +1 -14
- boltz_client/onchain.py +33 -23
- boltz_client/onchain_wally.py +9 -10
- boltz_client-0.2.2.dist-info/METADATA +123 -0
- boltz_client-0.2.2.dist-info/RECORD +12 -0
- {boltz_client-0.2.1.dist-info → boltz_client-0.2.2.dist-info}/WHEEL +1 -1
- boltz_client/mempool.py +0 -189
- boltz_client-0.2.1.dist-info/METADATA +0 -50
- boltz_client-0.2.1.dist-info/RECORD +0 -13
- {boltz_client-0.2.1.dist-info → boltz_client-0.2.2.dist-info}/LICENSE +0 -0
- {boltz_client-0.2.1.dist-info → boltz_client-0.2.2.dist-info}/entry_points.txt +0 -0
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"
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
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
|
-
|
|
242
|
-
assert
|
|
243
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
284
|
+
self.validate_address(lockup_address)
|
|
285
|
+
|
|
286
|
+
lockup_rawtx = await self.wait_for_tx(boltz_id)
|
|
304
287
|
transaction = create_refund_tx(
|
|
305
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
vout_amount - fees,
|
|
149
159
|
script.address_to_scriptpubkey(receive_address),
|
|
150
160
|
)
|
|
151
161
|
vout = [vout]
|
|
152
162
|
vin = TransactionInput(
|
|
153
|
-
|
|
154
|
-
|
|
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,
|
|
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)]
|
boltz_client/onchain_wally.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
|
@@ -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,,
|
boltz_client/mempool.py
DELETED
|
@@ -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
|
-
)
|
|
@@ -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,13 +0,0 @@
|
|
|
1
|
-
boltz_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
boltz_client/boltz.py,sha256=RAElhX4ZFqE-iMyfMIQlMedOTIU7_FGtW1NWG1PdWUo,11949
|
|
3
|
-
boltz_client/cli.py,sha256=HXPzp95ynb_0--Hbi0f8ZkF6k6avs2jJBgkuUHa4HR8,9734
|
|
4
|
-
boltz_client/helpers.py,sha256=pJVQKnki2xE9zJTz_ra_JIqSeet0A-jKDMi3AGj0dwE,374
|
|
5
|
-
boltz_client/mempool.py,sha256=Jx8hTz-HQOxJWbjMyRLiivQpCz2dFM4Q4wykHhnNkX4,6410
|
|
6
|
-
boltz_client/onchain.py,sha256=BV4vsOJJKC4OlsIhepYj2rEC4p-2Bs9xB2FKiQQwpqc,5246
|
|
7
|
-
boltz_client/onchain_wally.py,sha256=FyX5KNwTPSYZxuQBO96b5Cz9wIkk0IMB_hxS3B4jAck,12126
|
|
8
|
-
boltz_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
boltz_client-0.2.1.dist-info/LICENSE,sha256=ums-fB6x3ajzBZydnEwnKxO1n6oXTNr9NGdOYiWucmU,1062
|
|
10
|
-
boltz_client-0.2.1.dist-info/METADATA,sha256=YacgtJdRXcl7ZggCC4Me_ogFAmxBin1IS2WFGzHmNds,1212
|
|
11
|
-
boltz_client-0.2.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
12
|
-
boltz_client-0.2.1.dist-info/entry_points.txt,sha256=9u_t10YVlH8kHbl-M0nOZw91F3Fx4HJOZdSf2s42dhM,47
|
|
13
|
-
boltz_client-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|