boltz_client 0.2.0__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.
- boltz_client-0.2.2/PKG-INFO +123 -0
- boltz_client-0.2.2/README.md +99 -0
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/boltz.py +26 -44
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/cli.py +1 -3
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/onchain.py +33 -23
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/onchain_wally.py +143 -40
- {boltz_client-0.2.0 → boltz_client-0.2.2}/pyproject.toml +2 -1
- boltz_client-0.2.0/PKG-INFO +0 -50
- boltz_client-0.2.0/README.md +0 -26
- boltz_client-0.2.0/boltz_client/mempool.py +0 -184
- {boltz_client-0.2.0 → boltz_client-0.2.2}/LICENSE +0 -0
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/__init__.py +0 -0
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/helpers.py +0 -0
- {boltz_client-0.2.0 → boltz_client-0.2.2}/boltz_client/py.typed +0 -0
|
@@ -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/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
|
|
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
|
|
|
@@ -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()
|
|
@@ -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)]
|
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 ==
|
|
219
|
+
assert unblinded_asset == network.lbtc_asset, "Wrong asset"
|
|
117
220
|
|
|
118
221
|
# INITIALIZE PSBT (PSET)
|
|
119
222
|
num_vin = 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "boltz_client"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
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
|
]
|
boltz_client-0.2.0/PKG-INFO
DELETED
|
@@ -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
|
-
|
boltz_client-0.2.0/README.md
DELETED
|
@@ -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,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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|