mm-btc 0.2.1__tar.gz → 0.4.0__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.
- mm_btc-0.4.0/PKG-INFO +10 -0
- mm_btc-0.4.0/dict.dic +6 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/pyproject.toml +18 -12
- mm_btc-0.4.0/src/mm_btc/blockstream.py +102 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cli.py +5 -4
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cmd/address_cmd.py +3 -3
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cmd/create_tx_cmd.py +1 -2
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cmd/utxo_cmd.py +3 -4
- {mm_btc-0.2.1 → mm_btc-0.4.0}/tests/conftest.py +3 -9
- mm_btc-0.4.0/tests/test_blockstream.py +32 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/uv.lock +570 -270
- mm_btc-0.2.1/PKG-INFO +0 -10
- mm_btc-0.2.1/src/mm_btc/blockstream.py +0 -109
- mm_btc-0.2.1/src/mm_btc/cli/cli_utils.py +0 -19
- mm_btc-0.2.1/tests/test_blockstream.py +0 -28
- {mm_btc-0.2.1 → mm_btc-0.4.0}/.env.example +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/.gitignore +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/README.txt +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/justfile +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/__init__.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/__init__.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cmd/__init__.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cmd/decode_tx_cmd.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/cli/cmd/mnemonic_cmd.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/py.typed +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/tx.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/src/mm_btc/wallet.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/tests/__init__.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/tests/cmd/__init__.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/tests/cmd/test_mnemonic_cmd.py +0 -0
- {mm_btc-0.2.1 → mm_btc-0.4.0}/tests/test_wallet.py +0 -0
mm_btc-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mm-btc
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Requires-Python: >=3.12
|
|
5
|
+
Requires-Dist: bitcoinlib~=0.7.3
|
|
6
|
+
Requires-Dist: bit~=0.8.0
|
|
7
|
+
Requires-Dist: hdwallet~=3.4.0
|
|
8
|
+
Requires-Dist: mm-crypto-utils>=0.3.1
|
|
9
|
+
Requires-Dist: mnemonic~=0.21
|
|
10
|
+
Requires-Dist: typer~=0.15.2
|
mm_btc-0.4.0/dict.dic
ADDED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-btc"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-
|
|
8
|
-
"hdwallet~=3.
|
|
7
|
+
"mm-crypto-utils>=0.3.1",
|
|
8
|
+
"hdwallet~=3.4.0",
|
|
9
9
|
"bit~=0.8.0",
|
|
10
|
-
"bitcoinlib~=0.7.
|
|
10
|
+
"bitcoinlib~=0.7.3",
|
|
11
11
|
"mnemonic~=0.21",
|
|
12
|
-
"typer~=0.15.
|
|
12
|
+
"typer~=0.15.2",
|
|
13
13
|
]
|
|
14
14
|
[project.scripts]
|
|
15
15
|
mm-btc = "mm_btc.cli.cli:app"
|
|
@@ -21,14 +21,14 @@ build-backend = "hatchling.build"
|
|
|
21
21
|
|
|
22
22
|
[tool.uv]
|
|
23
23
|
dev-dependencies = [
|
|
24
|
-
"pytest~=8.3.
|
|
24
|
+
"pytest~=8.3.5",
|
|
25
25
|
"pytest-xdist~=3.6.1",
|
|
26
|
-
"pytest-httpserver~=1.1.
|
|
27
|
-
"ruff~=0.
|
|
28
|
-
"pip-audit~=2.
|
|
29
|
-
"bandit~=1.8.
|
|
30
|
-
"mypy~=1.
|
|
31
|
-
"
|
|
26
|
+
"pytest-httpserver~=1.1.3",
|
|
27
|
+
"ruff~=0.11.5",
|
|
28
|
+
"pip-audit~=2.9.0",
|
|
29
|
+
"bandit~=1.8.3",
|
|
30
|
+
"mypy~=1.15.0",
|
|
31
|
+
"pytest-asyncio>=0.26.0",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
|
|
@@ -47,6 +47,7 @@ target-version = "py313"
|
|
|
47
47
|
[tool.ruff.lint]
|
|
48
48
|
select = ["ALL"]
|
|
49
49
|
ignore = [
|
|
50
|
+
"TC", # flake8-type-checking, TYPE_CHECKING is dangerous, for example it doesn't work with pydantic
|
|
50
51
|
"A005", # flake8-builtins: stdlib-module-shadowing
|
|
51
52
|
"ERA001", # eradicate: commented-out-code
|
|
52
53
|
"PT", # flake8-pytest-style
|
|
@@ -68,6 +69,8 @@ ignore = [
|
|
|
68
69
|
"RET503", # flake8-return: implicit-return
|
|
69
70
|
"COM812", # it's used in ruff formatter
|
|
70
71
|
]
|
|
72
|
+
[tool.ruff.lint.pep8-naming]
|
|
73
|
+
classmethod-decorators = ["field_validator"]
|
|
71
74
|
[tool.ruff.lint.per-file-ignores]
|
|
72
75
|
"tests/*.py" = ["ANN", "S"]
|
|
73
76
|
[tool.ruff.format]
|
|
@@ -78,3 +81,6 @@ indent-style = "space"
|
|
|
78
81
|
[tool.bandit]
|
|
79
82
|
exclude_dirs = ["tests"]
|
|
80
83
|
skips = ["B311"]
|
|
84
|
+
|
|
85
|
+
[tool.pytest.ini_options]
|
|
86
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from mm_std import HttpResponse, Result, http_request, random_str_choice
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
MAINNET_BASE_URL = "https://blockstream.info/api"
|
|
7
|
+
TESTNET_BASE_URL = "https://blockstream.info/testnet/api"
|
|
8
|
+
|
|
9
|
+
# ERROR_INVALID_ADDRESS = "INVALID_ADDRESS"
|
|
10
|
+
# ERROR_INVALID_NETWORK = "INVALID_NETWORK"
|
|
11
|
+
|
|
12
|
+
ERROR_400_BAD_REQUEST = "400 Bad Request"
|
|
13
|
+
|
|
14
|
+
type Proxy = str | Sequence[str] | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Mempool(BaseModel):
|
|
18
|
+
count: int
|
|
19
|
+
vsize: int
|
|
20
|
+
total_fee: int
|
|
21
|
+
fee_histogram: list[tuple[float, int]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Address(BaseModel):
|
|
25
|
+
class ChainStats(BaseModel):
|
|
26
|
+
funded_txo_count: int
|
|
27
|
+
funded_txo_sum: int
|
|
28
|
+
spent_txo_count: int
|
|
29
|
+
spent_txo_sum: int
|
|
30
|
+
tx_count: int
|
|
31
|
+
|
|
32
|
+
class MempoolStats(BaseModel):
|
|
33
|
+
funded_txo_count: int
|
|
34
|
+
funded_txo_sum: int
|
|
35
|
+
spent_txo_count: int
|
|
36
|
+
spent_txo_sum: int
|
|
37
|
+
tx_count: int
|
|
38
|
+
|
|
39
|
+
chain_stats: ChainStats
|
|
40
|
+
mempool_stats: MempoolStats
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Utxo(BaseModel):
|
|
44
|
+
class Status(BaseModel):
|
|
45
|
+
confirmed: bool
|
|
46
|
+
block_height: int
|
|
47
|
+
block_hash: str
|
|
48
|
+
block_time: int
|
|
49
|
+
|
|
50
|
+
txid: str
|
|
51
|
+
vout: int
|
|
52
|
+
status: Status
|
|
53
|
+
value: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BlockstreamClient:
|
|
57
|
+
def __init__(self, testnet: bool = False, timeout: float = 5, proxies: Proxy = None, attempts: int = 1) -> None:
|
|
58
|
+
self.testnet = testnet
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
self.proxies = proxies
|
|
61
|
+
self.attempts = attempts
|
|
62
|
+
self.base_url = TESTNET_BASE_URL if testnet else MAINNET_BASE_URL
|
|
63
|
+
|
|
64
|
+
async def get_address(self, address: str) -> Result[Address]:
|
|
65
|
+
result: Result[Address] = Result.failure("not started yet")
|
|
66
|
+
for _ in range(self.attempts):
|
|
67
|
+
res = await self._request(f"/address/{address}")
|
|
68
|
+
try:
|
|
69
|
+
if res.status_code == 400:
|
|
70
|
+
return res.to_result_failure("400 Bad Request")
|
|
71
|
+
return res.to_result_success(Address(**res.parse_json_body()))
|
|
72
|
+
except Exception as e:
|
|
73
|
+
result = res.to_result_failure(e)
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
async def get_confirmed_balance(self, address: str) -> Result[int]:
|
|
77
|
+
return (await self.get_address(address)).and_then(
|
|
78
|
+
lambda a: Result.success(a.chain_stats.funded_txo_sum - a.chain_stats.spent_txo_sum)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def get_utxo(self, address: str) -> Result[list[Utxo]]:
|
|
82
|
+
result: Result[list[Utxo]] = Result.failure("not started yet")
|
|
83
|
+
for _ in range(self.attempts):
|
|
84
|
+
res = await self._request(f"/address/{address}/utxo")
|
|
85
|
+
try:
|
|
86
|
+
return res.to_result_success([Utxo(**out) for out in res.parse_json_body()])
|
|
87
|
+
except Exception as e:
|
|
88
|
+
result = res.to_result_failure(e)
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
async def get_mempool(self) -> Result[Mempool]:
|
|
92
|
+
result: Result[Mempool] = Result.failure("not started yet")
|
|
93
|
+
for _ in range(self.attempts):
|
|
94
|
+
res = await self._request("/mempool")
|
|
95
|
+
try:
|
|
96
|
+
return res.to_result_success(Mempool(**res.parse_json_body()))
|
|
97
|
+
except Exception as e:
|
|
98
|
+
result = res.to_result_failure(e)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
async def _request(self, url: str) -> HttpResponse:
|
|
102
|
+
return await http_request(f"{self.base_url}{url}", timeout=self.timeout, proxy=random_str_choice(self.proxies))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib.metadata
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
from typing import Annotated
|
|
3
5
|
|
|
@@ -7,7 +9,6 @@ from mm_std import print_plain
|
|
|
7
9
|
|
|
8
10
|
from mm_btc.wallet import AddressType
|
|
9
11
|
|
|
10
|
-
from . import cli_utils
|
|
11
12
|
from .cmd import address_cmd, create_tx_cmd, decode_tx_cmd, mnemonic_cmd, utxo_cmd
|
|
12
13
|
|
|
13
14
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
@@ -44,7 +45,7 @@ def mnemonic_command( # nosec B107:hardcoded_password_default
|
|
|
44
45
|
@app.command(name="a", hidden=True)
|
|
45
46
|
def address_command(address: str) -> None:
|
|
46
47
|
"""Get address info from Blockstream API"""
|
|
47
|
-
address_cmd.run(address)
|
|
48
|
+
asyncio.run(address_cmd.run(address))
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
@app.command("create-tx")
|
|
@@ -62,12 +63,12 @@ def decode_tx_command(tx_hex: str, testnet: Annotated[bool, typer.Option("--test
|
|
|
62
63
|
@app.command("utxo")
|
|
63
64
|
def utxo_command(address: str) -> None:
|
|
64
65
|
"""Get UTXOs from Blockstream API"""
|
|
65
|
-
utxo_cmd.run(address)
|
|
66
|
+
asyncio.run(utxo_cmd.run(address))
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def version_callback(value: bool) -> None:
|
|
69
70
|
if value:
|
|
70
|
-
print_plain(f"mm-btc: v{
|
|
71
|
+
print_plain(f"mm-btc: v{importlib.metadata.version('mm-btc')}")
|
|
71
72
|
raise typer.Exit
|
|
72
73
|
|
|
73
74
|
|
|
@@ -4,7 +4,7 @@ from mm_btc.blockstream import BlockstreamClient
|
|
|
4
4
|
from mm_btc.wallet import is_testnet_address
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def run(address: str) -> None:
|
|
7
|
+
async def run(address: str) -> None:
|
|
8
8
|
client = BlockstreamClient(testnet=is_testnet_address(address))
|
|
9
|
-
res = client.get_address(address)
|
|
10
|
-
print_json(res)
|
|
9
|
+
res = await client.get_address(address)
|
|
10
|
+
print_json(res.ok_or_error())
|
|
@@ -3,7 +3,6 @@ from pathlib import Path
|
|
|
3
3
|
from bit import PrivateKey, PrivateKeyTestnet
|
|
4
4
|
from mm_std import BaseConfig, print_console
|
|
5
5
|
|
|
6
|
-
from mm_btc.cli.cli_utils import read_config
|
|
7
6
|
from mm_btc.wallet import is_testnet_address
|
|
8
7
|
|
|
9
8
|
|
|
@@ -18,7 +17,7 @@ class Config(BaseConfig):
|
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def run(config_path: Path) -> None:
|
|
21
|
-
config =
|
|
20
|
+
config = Config.read_toml_config_or_exit(config_path)
|
|
22
21
|
testnet = is_testnet_address(config.from_address)
|
|
23
22
|
key = PrivateKeyTestnet(config.private) if testnet else PrivateKey(config.private)
|
|
24
23
|
|
|
@@ -4,8 +4,7 @@ from mm_btc.blockstream import BlockstreamClient
|
|
|
4
4
|
from mm_btc.wallet import is_testnet_address
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def run(address: str) -> None:
|
|
7
|
+
async def run(address: str) -> None:
|
|
8
8
|
client = BlockstreamClient(testnet=is_testnet_address(address))
|
|
9
|
-
res = client.get_utxo(address)
|
|
10
|
-
|
|
11
|
-
print_json(res.ok_or_err())
|
|
9
|
+
res = await client.get_utxo(address)
|
|
10
|
+
print_json(res.ok_or_error())
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
from
|
|
2
|
+
from mm_crypto_utils.proxy import fetch_proxies_or_fatal_sync
|
|
3
|
+
from mm_std import get_dotenv
|
|
3
4
|
from typer.testing import CliRunner
|
|
4
5
|
|
|
5
6
|
|
|
@@ -25,14 +26,7 @@ def binance_address() -> str:
|
|
|
25
26
|
|
|
26
27
|
@pytest.fixture
|
|
27
28
|
def proxies() -> list[str]:
|
|
28
|
-
|
|
29
|
-
if proxies_url:
|
|
30
|
-
res = hr(proxies_url)
|
|
31
|
-
try:
|
|
32
|
-
return res.json["proxies"]
|
|
33
|
-
except KeyError:
|
|
34
|
-
pass
|
|
35
|
-
return []
|
|
29
|
+
return fetch_proxies_or_fatal_sync(get_dotenv("PROXIES_URL"))
|
|
36
30
|
|
|
37
31
|
|
|
38
32
|
@pytest.fixture
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from mm_btc import blockstream
|
|
4
|
+
from mm_btc.blockstream import BlockstreamClient
|
|
5
|
+
|
|
6
|
+
pytestmark = pytest.mark.asyncio
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def test_get_address(binance_address, proxies):
|
|
10
|
+
client = BlockstreamClient(proxies=proxies, timeout=5, attempts=5)
|
|
11
|
+
|
|
12
|
+
# non-empty address
|
|
13
|
+
res = await client.get_address(binance_address)
|
|
14
|
+
assert res.unwrap().chain_stats.tx_count > 800
|
|
15
|
+
|
|
16
|
+
# empty address
|
|
17
|
+
res = await client.get_address("bc1pa48c294qk7yd7sc8y0wxydc3a2frv5j83e65rvm48v3ej098s5zs8kvh6d")
|
|
18
|
+
assert res.unwrap().chain_stats.tx_count == 0
|
|
19
|
+
|
|
20
|
+
# invalid address
|
|
21
|
+
res = await client.get_address("bc1pa48c294qk7yd7sc8y0wxydc3a2frv5j83e65rvm48v3ej098s5zs8kvh5d")
|
|
22
|
+
assert res.unwrap_error() == blockstream.ERROR_400_BAD_REQUEST
|
|
23
|
+
|
|
24
|
+
# invalid network
|
|
25
|
+
res = await client.get_address("mqkwWDWdgXhYunfoKvQfYyydwB5vdma3cK")
|
|
26
|
+
assert res.unwrap_error() == blockstream.ERROR_400_BAD_REQUEST
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def test_get_confirmed_balance(binance_address, proxies):
|
|
30
|
+
client = BlockstreamClient(proxies=proxies)
|
|
31
|
+
res = await client.get_confirmed_balance(binance_address)
|
|
32
|
+
assert res.unwrap() > 0
|