mm-btc 0.3.0__tar.gz → 0.4.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.
Files changed (30) hide show
  1. mm_btc-0.4.1/PKG-INFO +10 -0
  2. {mm_btc-0.3.0 → mm_btc-0.4.1}/pyproject.toml +16 -12
  3. mm_btc-0.4.1/src/mm_btc/blockstream.py +102 -0
  4. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cli.py +3 -2
  5. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cmd/address_cmd.py +3 -3
  6. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cmd/utxo_cmd.py +3 -4
  7. {mm_btc-0.3.0 → mm_btc-0.4.1}/tests/conftest.py +3 -9
  8. mm_btc-0.4.1/tests/test_blockstream.py +32 -0
  9. {mm_btc-0.3.0 → mm_btc-0.4.1}/uv.lock +538 -271
  10. mm_btc-0.3.0/PKG-INFO +0 -10
  11. mm_btc-0.3.0/src/mm_btc/blockstream.py +0 -107
  12. mm_btc-0.3.0/tests/test_blockstream.py +0 -28
  13. {mm_btc-0.3.0 → mm_btc-0.4.1}/.env.example +0 -0
  14. {mm_btc-0.3.0 → mm_btc-0.4.1}/.gitignore +0 -0
  15. {mm_btc-0.3.0 → mm_btc-0.4.1}/README.txt +0 -0
  16. {mm_btc-0.3.0 → mm_btc-0.4.1}/dict.dic +0 -0
  17. {mm_btc-0.3.0 → mm_btc-0.4.1}/justfile +0 -0
  18. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/__init__.py +0 -0
  19. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/__init__.py +0 -0
  20. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cmd/__init__.py +0 -0
  21. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cmd/create_tx_cmd.py +0 -0
  22. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cmd/decode_tx_cmd.py +0 -0
  23. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/cli/cmd/mnemonic_cmd.py +0 -0
  24. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/py.typed +0 -0
  25. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/tx.py +0 -0
  26. {mm_btc-0.3.0 → mm_btc-0.4.1}/src/mm_btc/wallet.py +0 -0
  27. {mm_btc-0.3.0 → mm_btc-0.4.1}/tests/__init__.py +0 -0
  28. {mm_btc-0.3.0 → mm_btc-0.4.1}/tests/cmd/__init__.py +0 -0
  29. {mm_btc-0.3.0 → mm_btc-0.4.1}/tests/cmd/test_mnemonic_cmd.py +0 -0
  30. {mm_btc-0.3.0 → mm_btc-0.4.1}/tests/test_wallet.py +0 -0
mm_btc-0.4.1/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-btc
3
+ Version: 0.4.1
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.4
9
+ Requires-Dist: mnemonic~=0.21
10
+ Requires-Dist: typer~=0.15.2
@@ -1,15 +1,15 @@
1
1
  [project]
2
2
  name = "mm-btc"
3
- version = "0.3.0"
3
+ version = "0.4.1"
4
4
  description = ""
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
7
- "mm-crypto-utils>=0.1.3",
8
- "hdwallet~=3.2.3",
7
+ "mm-crypto-utils>=0.3.4",
8
+ "hdwallet~=3.4.0",
9
9
  "bit~=0.8.0",
10
- "bitcoinlib~=0.7.1",
10
+ "bitcoinlib~=0.7.3",
11
11
  "mnemonic~=0.21",
12
- "typer~=0.15.1",
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.4",
24
+ "pytest~=8.3.5",
25
25
  "pytest-xdist~=3.6.1",
26
- "pytest-httpserver~=1.1.0",
27
- "ruff~=0.9.4",
28
- "pip-audit~=2.7.3",
29
- "bandit~=1.8.2",
30
- "mypy~=1.14.1",
31
- "types-PyYAML~=6.0.12.20241230",
26
+ "pytest-httpserver~=1.1.3",
27
+ "ruff~=0.11.6",
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
 
@@ -81,3 +81,7 @@ indent-style = "space"
81
81
  [tool.bandit]
82
82
  exclude_dirs = ["tests"]
83
83
  skips = ["B311"]
84
+
85
+ [tool.pytest.ini_options]
86
+ asyncio_mode = "auto"
87
+ 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.err("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_err_result("400 Bad Request")
71
+ return res.to_ok_result(Address(**res.parse_json_body()))
72
+ except Exception as e:
73
+ result = res.to_err_result(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.ok(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.err("not started yet")
83
+ for _ in range(self.attempts):
84
+ res = await self._request(f"/address/{address}/utxo")
85
+ try:
86
+ return res.to_ok_result([Utxo(**out) for out in res.parse_json_body()])
87
+ except Exception as e:
88
+ result = res.to_err_result(e)
89
+ return result
90
+
91
+ async def get_mempool(self) -> Result[Mempool]:
92
+ result: Result[Mempool] = Result.err("not started yet")
93
+ for _ in range(self.attempts):
94
+ res = await self._request("/mempool")
95
+ try:
96
+ return res.to_ok_result(Mempool(**res.parse_json_body()))
97
+ except Exception as e:
98
+ result = res.to_err_result(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,4 @@
1
+ import asyncio
1
2
  import importlib.metadata
2
3
  from pathlib import Path
3
4
  from typing import Annotated
@@ -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,7 +63,7 @@ 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:
@@ -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())
@@ -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 mm_std import get_dotenv, hr
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
- proxies_url = get_dotenv("PROXIES_URL")
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