mm-btc 0.0.5__tar.gz → 0.0.6__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.0.5 → mm_btc-0.0.6}/PKG-INFO +3 -1
  2. mm_btc-0.0.6/README.txt +9 -0
  3. {mm_btc-0.0.5 → mm_btc-0.0.6}/pyproject.toml +4 -2
  4. mm_btc-0.0.6/src/mm_btc/blockstream.py +91 -0
  5. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/cli/cli.py +20 -1
  6. mm_btc-0.0.6/src/mm_btc/cli/cmd/address_cmd.py +10 -0
  7. mm_btc-0.0.6/src/mm_btc/cli/cmd/create_tx_cmd.py +27 -0
  8. mm_btc-0.0.6/src/mm_btc/cli/cmd/decode_tx_cmd.py +8 -0
  9. mm_btc-0.0.6/src/mm_btc/cli/cmd/utxo_cmd.py +11 -0
  10. mm_btc-0.0.6/src/mm_btc/tx.py +5 -0
  11. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc.egg-info/PKG-INFO +3 -1
  12. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc.egg-info/SOURCES.txt +4 -0
  13. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc.egg-info/requires.txt +2 -0
  14. mm_btc-0.0.6/tests/test_blockstream.py +28 -0
  15. mm_btc-0.0.5/README.txt +0 -1
  16. mm_btc-0.0.5/src/mm_btc/blockstream.py +0 -63
  17. mm_btc-0.0.5/src/mm_btc/cli/cmd/address_cmd.py +0 -10
  18. mm_btc-0.0.5/tests/test_blockstream.py +0 -24
  19. {mm_btc-0.0.5 → mm_btc-0.0.6}/setup.cfg +0 -0
  20. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/__init__.py +0 -0
  21. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/cli/__init__.py +0 -0
  22. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/cli/cli_utils.py +0 -0
  23. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/cli/cmd/__init__.py +0 -0
  24. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/cli/cmd/mnemonic_cmd.py +0 -0
  25. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/py.typed +0 -0
  26. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc/wallet.py +0 -0
  27. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc.egg-info/dependency_links.txt +0 -0
  28. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc.egg-info/entry_points.txt +0 -0
  29. {mm_btc-0.0.5 → mm_btc-0.0.6}/src/mm_btc.egg-info/top_level.txt +0 -0
  30. {mm_btc-0.0.5 → mm_btc-0.0.6}/tests/test_wallet.py +0 -0
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mm-btc
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: mm-std~=0.1.0
6
6
  Requires-Dist: hdwallet~=2.2.1
7
+ Requires-Dist: bit~=0.8.0
8
+ Requires-Dist: bitcoinlib~=0.6.15
7
9
  Requires-Dist: typer~=0.12.3
8
10
  Provides-Extra: dev
9
11
  Requires-Dist: build~=1.2.1; extra == "dev"
@@ -0,0 +1,9 @@
1
+ mm-btc
2
+
3
+
4
+ Links:
5
+ - https://en.bitcoin.it/wiki/Transaction
6
+ - https://en.bitcoin.it/wiki/List_of_address_prefixes
7
+ - https://github.com/bitcoin/bips/blob/master/README.mediawiki
8
+
9
+ - https://github.com/Blockstream/esplora/blob/master/API.md
@@ -1,10 +1,12 @@
1
1
  [project]
2
2
  name = "mm-btc"
3
- version = "0.0.5"
3
+ version = "0.0.6"
4
4
  description = ""
5
5
  dependencies = [
6
6
  "mm-std~=0.1.0",
7
7
  "hdwallet~=2.2.1",
8
+ "bit~=0.8.0",
9
+ "bitcoinlib~=0.6.15",
8
10
  "typer~=0.12.3"
9
11
  ]
10
12
  requires-python = ">=3.12"
@@ -39,7 +41,7 @@ warn_no_return = false
39
41
  strict = true
40
42
  exclude = ["^tests/", "^tmp/"]
41
43
  [[tool.mypy.overrides]]
42
- module = ["hdwallet.*", "hdwallet.symbols.*", "telebot.util.*"]
44
+ module = ["hdwallet.*", "hdwallet.symbols.*", "bit.*", "bitcoinlib.transactions.*"]
43
45
  ignore_missing_imports = true
44
46
 
45
47
 
@@ -0,0 +1,91 @@
1
+ from collections.abc import Sequence
2
+ from typing import TypeAlias
3
+
4
+ from mm_std import Err, HResponse, Ok, Result, hr
5
+ from mm_std.random_ import random_str_choice
6
+ from pydantic import BaseModel
7
+
8
+ MAINNET_BASE_URL = "https://blockstream.info/api"
9
+ TESTNET_BASE_URL = "https://blockstream.info/testnet/api"
10
+
11
+ ERROR_INVALID_ADDRESS = "INVALID_ADDRESS"
12
+ ERROR_INVALID_NETWORK = "INVALID_NETWORK"
13
+
14
+ Proxy: TypeAlias = str | Sequence[str] | None
15
+
16
+
17
+ class Address(BaseModel):
18
+ class ChainStats(BaseModel):
19
+ funded_txo_count: int
20
+ funded_txo_sum: int
21
+ spent_txo_count: int
22
+ spent_txo_sum: int
23
+ tx_count: int
24
+
25
+ class MempoolStats(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
+ chain_stats: ChainStats
33
+ mempool_stats: MempoolStats
34
+
35
+
36
+ class Utxo(BaseModel):
37
+ class Status(BaseModel):
38
+ confirmed: bool
39
+ block_height: int
40
+ block_hash: str
41
+ block_time: int
42
+
43
+ txid: str
44
+ vout: int
45
+ status: Status
46
+ value: int
47
+
48
+
49
+ class BlockstreamClient:
50
+ def __init__(self, testnet: bool = False, timeout: int = 10, proxies: Proxy = None, attempts: int = 1):
51
+ self.testnet = testnet
52
+ self.timeout = timeout
53
+ self.proxies = proxies
54
+ self.attempts = attempts
55
+ self.base_url = TESTNET_BASE_URL if testnet else MAINNET_BASE_URL
56
+
57
+ def get_address(self, address: str) -> Result[Address]:
58
+ result: Result[Address] = Err("not started yet")
59
+ data = None
60
+ for _ in range(self.attempts):
61
+ try:
62
+ res = self._request(f"/address/{address}")
63
+ data = res.to_dict()
64
+ if res.code == 400 and (
65
+ "invalid bitcoin address" in res.body.lower() or "bech32 segwit decoding error" in res.body.lower()
66
+ ):
67
+ return Err(ERROR_INVALID_ADDRESS, data=data)
68
+ elif res.code == 400 and "invalid network" in res.body.lower():
69
+ return Err(ERROR_INVALID_NETWORK, data=data)
70
+ return Ok(Address(**res.json), data=data)
71
+ except Exception as err:
72
+ result = Err(err, data=data)
73
+ return result
74
+
75
+ def get_confirmed_balance(self, address: str) -> Result[int]:
76
+ return self.get_address(address).and_then(lambda a: Ok(a.chain_stats.funded_txo_sum - a.chain_stats.spent_txo_sum))
77
+
78
+ def get_utxo(self, address: str) -> Result[list[Utxo]]:
79
+ result: Result[list[Utxo]] = Err("not started yet")
80
+ data = None
81
+ for _ in range(self.attempts):
82
+ try:
83
+ res = self._request(f"/address/{address}/utxo")
84
+ data = res.to_dict()
85
+ return Ok([Utxo(**out) for out in res.json], data=data)
86
+ except Exception as err:
87
+ result = Err(err, data=data)
88
+ return result
89
+
90
+ def _request(self, url: str) -> HResponse:
91
+ return hr(f"{self.base_url}{url}", timeout=self.timeout, proxy=random_str_choice(self.proxies))
@@ -1,3 +1,4 @@
1
+ from pathlib import Path
1
2
  from typing import Annotated
2
3
 
3
4
  import typer
@@ -5,7 +6,7 @@ import typer.core
5
6
  from mm_std import print_plain
6
7
 
7
8
  from mm_btc.cli import cli_utils
8
- from mm_btc.cli.cmd import address_cmd, mnemonic_cmd
9
+ from mm_btc.cli.cmd import address_cmd, create_tx_cmd, decode_tx_cmd, mnemonic_cmd, utxo_cmd
9
10
 
10
11
  app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
11
12
 
@@ -42,6 +43,24 @@ def address_command(address: str) -> None:
42
43
  address_cmd.run(address)
43
44
 
44
45
 
46
+ @app.command("create-tx")
47
+ def create_tx_command(config_path: Annotated[Path, typer.Argument(exists=True)]) -> None:
48
+ """Create a transaction"""
49
+ create_tx_cmd.run(config_path)
50
+
51
+
52
+ @app.command("decode-tx")
53
+ def decode_tx_command(tx_hex: str, testnet: Annotated[bool, typer.Option("--testnet", "-t")] = False) -> None:
54
+ """Decode a transaction"""
55
+ decode_tx_cmd.run(tx_hex, testnet)
56
+
57
+
58
+ @app.command("utxo")
59
+ def utxo_command(address: str) -> None:
60
+ """Get UTXOs from Blockstream API"""
61
+ utxo_cmd.run(address)
62
+
63
+
45
64
  def version_callback(value: bool) -> None:
46
65
  if value:
47
66
  print_plain(f"mm-btc: v{cli_utils.get_version()}")
@@ -0,0 +1,10 @@
1
+ from mm_std import print_json
2
+
3
+ from mm_btc.blockstream import BlockstreamClient
4
+ from mm_btc.wallet import is_testnet_address
5
+
6
+
7
+ def run(address: str) -> None:
8
+ client = BlockstreamClient(testnet=is_testnet_address(address))
9
+ res = client.get_address(address)
10
+ print_json(res)
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+
3
+ from bit import PrivateKey, PrivateKeyTestnet
4
+ from mm_std import BaseConfig, print_console
5
+
6
+ from mm_btc.wallet import is_testnet_address
7
+
8
+
9
+ class Config(BaseConfig):
10
+ class Output(BaseConfig):
11
+ address: str
12
+ amount: int
13
+
14
+ from_address: str
15
+ private: str
16
+ outputs: list[Output]
17
+
18
+
19
+ def run(config_path: Path) -> None:
20
+ config = Config.read_config(config_path)
21
+ testnet = is_testnet_address(config.from_address)
22
+ key = PrivateKeyTestnet(config.private) if testnet else PrivateKey(config.private)
23
+
24
+ outputs = [(o.address, o.amount, "satoshi") for o in config.outputs]
25
+
26
+ tx = key.create_transaction(outputs)
27
+ print_console(tx)
@@ -0,0 +1,8 @@
1
+ from mm_std import print_json
2
+
3
+ from mm_btc.tx import decode_tx
4
+
5
+
6
+ def run(tx_hex: str, testnet: bool = False) -> None:
7
+ res = decode_tx(tx_hex, testnet)
8
+ print_json(res)
@@ -0,0 +1,11 @@
1
+ from mm_std import print_json
2
+
3
+ from mm_btc.blockstream import BlockstreamClient
4
+ from mm_btc.wallet import is_testnet_address
5
+
6
+
7
+ def run(address: str) -> None:
8
+ client = BlockstreamClient(testnet=is_testnet_address(address))
9
+ res = client.get_utxo(address)
10
+
11
+ print_json(res.ok_or_err())
@@ -0,0 +1,5 @@
1
+ from bitcoinlib.transactions import Transaction
2
+
3
+
4
+ def decode_tx(tx_hex: str, testnet: bool = False) -> dict[str, object]:
5
+ return Transaction.parse(tx_hex, network="testnet" if testnet else "mainnet").as_dict() # type: ignore[no-any-return]
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mm-btc
3
- Version: 0.0.5
3
+ Version: 0.0.6
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: mm-std~=0.1.0
6
6
  Requires-Dist: hdwallet~=2.2.1
7
+ Requires-Dist: bit~=0.8.0
8
+ Requires-Dist: bitcoinlib~=0.6.15
7
9
  Requires-Dist: typer~=0.12.3
8
10
  Provides-Extra: dev
9
11
  Requires-Dist: build~=1.2.1; extra == "dev"
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  src/mm_btc/__init__.py
4
4
  src/mm_btc/blockstream.py
5
5
  src/mm_btc/py.typed
6
+ src/mm_btc/tx.py
6
7
  src/mm_btc/wallet.py
7
8
  src/mm_btc.egg-info/PKG-INFO
8
9
  src/mm_btc.egg-info/SOURCES.txt
@@ -15,6 +16,9 @@ src/mm_btc/cli/cli.py
15
16
  src/mm_btc/cli/cli_utils.py
16
17
  src/mm_btc/cli/cmd/__init__.py
17
18
  src/mm_btc/cli/cmd/address_cmd.py
19
+ src/mm_btc/cli/cmd/create_tx_cmd.py
20
+ src/mm_btc/cli/cmd/decode_tx_cmd.py
18
21
  src/mm_btc/cli/cmd/mnemonic_cmd.py
22
+ src/mm_btc/cli/cmd/utxo_cmd.py
19
23
  tests/test_blockstream.py
20
24
  tests/test_wallet.py
@@ -1,5 +1,7 @@
1
1
  mm-std~=0.1.0
2
2
  hdwallet~=2.2.1
3
+ bit~=0.8.0
4
+ bitcoinlib~=0.6.15
3
5
  typer~=0.12.3
4
6
 
5
7
  [dev]
@@ -0,0 +1,28 @@
1
+ from mm_btc import blockstream
2
+ from mm_btc.blockstream import BlockstreamClient
3
+
4
+
5
+ def test_get_address(binance_address, proxies):
6
+ client = BlockstreamClient(proxies=proxies, timeout=5, attempts=5)
7
+
8
+ # non-empty address
9
+ res = client.get_address(binance_address)
10
+ assert res.unwrap().chain_stats.tx_count > 800
11
+
12
+ # empty address
13
+ res = client.get_address("bc1pa48c294qk7yd7sc8y0wxydc3a2frv5j83e65rvm48v3ej098s5zs8kvh6d")
14
+ assert res.unwrap().chain_stats.tx_count == 0
15
+
16
+ # invalid address
17
+ res = client.get_address("bc1pa48c294qk7yd7sc8y0wxydc3a2frv5j83e65rvm48v3ej098s5zs8kvh5d")
18
+ assert res.unwrap_err() == blockstream.ERROR_INVALID_ADDRESS
19
+
20
+ # invalid network
21
+ res = client.get_address("mqkwWDWdgXhYunfoKvQfYyydwB5vdma3cK")
22
+ assert res.unwrap_err() == blockstream.ERROR_INVALID_NETWORK
23
+
24
+
25
+ def test_get_confirmed_balance(binance_address, proxies):
26
+ client = BlockstreamClient(proxies=proxies)
27
+ res = client.get_confirmed_balance(binance_address)
28
+ assert res.unwrap() > 0
mm_btc-0.0.5/README.txt DELETED
@@ -1 +0,0 @@
1
- mm-btc
@@ -1,63 +0,0 @@
1
- from collections.abc import Sequence
2
- from typing import TypeAlias
3
-
4
- from mm_std import Err, Ok, Result, hr
5
- from mm_std.random_ import random_str_choice
6
- from pydantic import BaseModel
7
-
8
- MAINNET_BASE_URL = "https://blockstream.info/api"
9
- TESTNET_BASE_URL = "https://blockstream.info/testnet/api"
10
-
11
- ERROR_INVALID_ADDRESS = "INVALID_ADDRESS"
12
- ERROR_INVALID_NETWORK = "INVALID_NETWORK"
13
-
14
- Proxy: TypeAlias = str | Sequence[str] | None
15
-
16
-
17
- class Address(BaseModel):
18
- class ChainStats(BaseModel):
19
- funded_txo_count: int
20
- funded_txo_sum: int
21
- spent_txo_count: int
22
- spent_txo_sum: int
23
- tx_count: int
24
-
25
- class MempoolStats(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
- chain_stats: ChainStats
33
- mempool_stats: MempoolStats
34
-
35
-
36
- def get_address(
37
- address: str, testnet: bool = False, timeout: int = 10, proxies: Proxy = None, attempts: int = 1
38
- ) -> Result[Address]:
39
- result: Result[Address] = Err("not started yet")
40
- data = None
41
- base_url = TESTNET_BASE_URL if testnet else MAINNET_BASE_URL
42
- for _ in range(attempts):
43
- try:
44
- res = hr(f"{base_url}/address/{address}", timeout=timeout, proxy=random_str_choice(proxies))
45
- data = res.to_dict()
46
- if res.code == 400 and (
47
- "invalid bitcoin address" in res.body.lower() or "bech32 segwit decoding error" in res.body.lower()
48
- ):
49
- return Err(ERROR_INVALID_ADDRESS, data=data)
50
- elif res.code == 400 and "invalid network" in res.body.lower():
51
- return Err(ERROR_INVALID_NETWORK, data=data)
52
- return Ok(Address(**res.json))
53
- except Exception as err:
54
- result = Err(err, data=data)
55
- return result
56
-
57
-
58
- def get_confirmed_balance(
59
- address: str, testnet: bool = False, timeout: int = 10, proxies: Proxy = None, attempts: int = 1
60
- ) -> Result[int]:
61
- return get_address(address, testnet=testnet, timeout=timeout, proxies=proxies, attempts=attempts).and_then(
62
- lambda a: Ok(a.chain_stats.funded_txo_sum - a.chain_stats.spent_txo_sum),
63
- )
@@ -1,10 +0,0 @@
1
- from mm_std import print_json
2
-
3
- from mm_btc import blockstream
4
- from mm_btc.wallet import is_testnet_address
5
-
6
-
7
- def run(address: str) -> None:
8
- testnet = is_testnet_address(address)
9
- res = blockstream.get_address(address, testnet=testnet)
10
- print_json(res)
@@ -1,24 +0,0 @@
1
- from mm_btc import blockstream
2
-
3
-
4
- def test_get_address(binance_address, proxies):
5
- # non-empty address
6
- res = blockstream.get_address(binance_address, proxies=proxies)
7
- assert res.unwrap().chain_stats.tx_count > 800
8
-
9
- # empty address
10
- res = blockstream.get_address("bc1pa48c294qk7yd7sc8y0wxydc3a2frv5j83e65rvm48v3ej098s5zs8kvh6d", proxies=proxies)
11
- assert res.unwrap().chain_stats.tx_count == 0
12
-
13
- # invalid address
14
- res = blockstream.get_address("bc1pa48c294qk7yd7sc8y0wxydc3a2frv5j83e65rvm48v3ej098s5zs8kvh5d", proxies=proxies)
15
- assert res.unwrap_err() == blockstream.ERROR_INVALID_ADDRESS
16
-
17
- # invalid network
18
- res = blockstream.get_address(binance_address, testnet=True, proxies=proxies)
19
- assert res.unwrap_err() == blockstream.ERROR_INVALID_NETWORK
20
-
21
-
22
- def test_get_confirmed_balance(binance_address, proxies):
23
- res = blockstream.get_confirmed_balance(binance_address, proxies=proxies)
24
- assert res.unwrap() > 0
File without changes
File without changes
File without changes
File without changes
File without changes