mm-btc 0.2.0__py3-none-any.whl → 0.3.0__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.
mm_btc/blockstream.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from collections.abc import Sequence
2
- from typing import TypeAlias
3
2
 
4
3
  from mm_std import Err, HResponse, Ok, Result, hr
5
4
  from mm_std.random_ import random_str_choice
@@ -8,10 +7,12 @@ from pydantic import BaseModel
8
7
  MAINNET_BASE_URL = "https://blockstream.info/api"
9
8
  TESTNET_BASE_URL = "https://blockstream.info/testnet/api"
10
9
 
11
- ERROR_INVALID_ADDRESS = "INVALID_ADDRESS"
12
- ERROR_INVALID_NETWORK = "INVALID_NETWORK"
10
+ # ERROR_INVALID_ADDRESS = "INVALID_ADDRESS"
11
+ # ERROR_INVALID_NETWORK = "INVALID_NETWORK"
13
12
 
14
- Proxy: TypeAlias = str | Sequence[str] | None
13
+ ERROR_400_BAD_REQUEST = "400 Bad Request"
14
+
15
+ type Proxy = str | Sequence[str] | None
15
16
 
16
17
 
17
18
  class Mempool(BaseModel):
@@ -54,7 +55,7 @@ class Utxo(BaseModel):
54
55
 
55
56
 
56
57
  class BlockstreamClient:
57
- def __init__(self, testnet: bool = False, timeout: int = 10, proxies: Proxy = None, attempts: int = 1):
58
+ def __init__(self, testnet: bool = False, timeout: int = 10, proxies: Proxy = None, attempts: int = 1) -> None:
58
59
  self.testnet = testnet
59
60
  self.timeout = timeout
60
61
  self.proxies = proxies
@@ -68,12 +69,8 @@ class BlockstreamClient:
68
69
  try:
69
70
  res = self._request(f"/address/{address}")
70
71
  data = res.to_dict()
71
- if res.code == 400 and (
72
- "invalid bitcoin address" in res.body.lower() or "bech32 segwit decoding error" in res.body.lower()
73
- ):
74
- return Err(ERROR_INVALID_ADDRESS, data=data)
75
- elif res.code == 400 and "invalid network" in res.body.lower():
76
- return Err(ERROR_INVALID_NETWORK, data=data)
72
+ if res.code == 400:
73
+ return Err("400 Bad Request", data=data)
77
74
  return Ok(Address(**res.json), data=data)
78
75
  except Exception as err:
79
76
  result = Err(err, data=data)
mm_btc/cli/__init__.py ADDED
File without changes
mm_btc/cli/cli.py ADDED
@@ -0,0 +1,80 @@
1
+ import importlib.metadata
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ import typer.core
7
+ from mm_std import print_plain
8
+
9
+ from mm_btc.wallet import AddressType
10
+
11
+ from .cmd import address_cmd, create_tx_cmd, decode_tx_cmd, mnemonic_cmd, utxo_cmd
12
+
13
+ app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
14
+
15
+
16
+ @app.command("mnemonic")
17
+ @app.command(name="m", hidden=True)
18
+ def mnemonic_command( # nosec B107:hardcoded_password_default
19
+ mnemonic: Annotated[str, typer.Option("--mnemonic", "-m", help="")] = "",
20
+ passphrase: Annotated[str, typer.Option("--passphrase", "-p")] = "",
21
+ path: Annotated[str, typer.Option("--path", help="Derivation path. Examples: bip44, bip88, m/44'/0'/0'/0")] = "bip44",
22
+ address_type: Annotated[AddressType, typer.Option("--address-type", "-a", help="Bitcoin address type")] = AddressType.P2WPKH,
23
+ hex_: Annotated[bool, typer.Option("--hex", help="Print private key in hex format instead of WIF")] = False,
24
+ words: int = typer.Option(12, "--words", "-w", help="Number of mnemonic words"),
25
+ limit: int = typer.Option(10, "--limit", "-l"),
26
+ testnet: bool = typer.Option(False, "--testnet", "-t", help="Testnet network"),
27
+ ) -> None:
28
+ """Generate keys based on a mnemonic"""
29
+ mnemonic_cmd.run(
30
+ mnemonic_cmd.Args(
31
+ mnemonic=mnemonic,
32
+ passphrase=passphrase,
33
+ words=words,
34
+ limit=limit,
35
+ path=path,
36
+ address_type=address_type,
37
+ hex=hex_,
38
+ testnet=testnet,
39
+ )
40
+ )
41
+
42
+
43
+ @app.command(name="address")
44
+ @app.command(name="a", hidden=True)
45
+ def address_command(address: str) -> None:
46
+ """Get address info from Blockstream API"""
47
+ address_cmd.run(address)
48
+
49
+
50
+ @app.command("create-tx")
51
+ def create_tx_command(config_path: Annotated[Path, typer.Argument(exists=True)]) -> None:
52
+ """Create a transaction"""
53
+ create_tx_cmd.run(config_path)
54
+
55
+
56
+ @app.command("decode-tx")
57
+ def decode_tx_command(tx_hex: str, testnet: Annotated[bool, typer.Option("--testnet", "-t")] = False) -> None:
58
+ """Decode a transaction"""
59
+ decode_tx_cmd.run(tx_hex, testnet)
60
+
61
+
62
+ @app.command("utxo")
63
+ def utxo_command(address: str) -> None:
64
+ """Get UTXOs from Blockstream API"""
65
+ utxo_cmd.run(address)
66
+
67
+
68
+ def version_callback(value: bool) -> None:
69
+ if value:
70
+ print_plain(f"mm-btc: v{importlib.metadata.version('mm-btc')}")
71
+ raise typer.Exit
72
+
73
+
74
+ @app.callback()
75
+ def main(_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True)) -> None:
76
+ pass
77
+
78
+
79
+ if __name__ == "__main_":
80
+ app()
File without changes
@@ -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_toml_config_or_exit(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,49 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from mm_std import print_plain
5
+
6
+ from mm_btc.wallet import AddressType, derive_accounts, generate_mnemonic
7
+
8
+
9
+ class PrivateType(str, Enum):
10
+ hex = "hex"
11
+ wif = "wif"
12
+
13
+
14
+ @dataclass
15
+ class Args:
16
+ mnemonic: str
17
+ passphrase: str
18
+ words: int
19
+ limit: int
20
+ hex: bool # Print private key in hex format instead of WIF
21
+ address_type: AddressType
22
+ path: str
23
+ testnet: bool
24
+
25
+
26
+ def run(args: Args) -> None:
27
+ mnemonic = args.mnemonic or generate_mnemonic()
28
+ passphrase = args.passphrase
29
+ path = get_derivation_path_prefix(args.path, args.testnet)
30
+ accounts = derive_accounts(mnemonic, passphrase, path, args.address_type, args.limit)
31
+
32
+ print_plain(f"{mnemonic}")
33
+ if passphrase:
34
+ print_plain(f"{passphrase}")
35
+ for acc in accounts:
36
+ private = acc.private if args.hex else acc.wif
37
+ print_plain(f"{acc.path} {acc.address} {private}")
38
+
39
+
40
+ def get_derivation_path_prefix(path: str, testnet: bool) -> str:
41
+ if path.startswith("m/"):
42
+ return path
43
+ coin = "1" if testnet else "0"
44
+ if path == "bip44":
45
+ return f"m/44'/{coin}'/0'/0"
46
+ if path == "bip84":
47
+ return f"m/84'/{coin}'/0'/0"
48
+
49
+ raise ValueError("Invalid path")
@@ -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())
mm_btc/wallet.py CHANGED
@@ -39,13 +39,9 @@ def generate_mnemonic(language: str = "english", words: int = 12) -> str:
39
39
 
40
40
  def derive_accounts(mnemonic: str, passphrase: str, path_prefix: str, address_type: AddressType, limit: int) -> list[Account]:
41
41
  coin = Bitcoin
42
- if path_prefix.startswith("m/84'/1'"):
42
+ if path_prefix.startswith(("m/84'/1'", "m/44'/1'")):
43
43
  network = coin.NETWORKS.TESTNET
44
- elif path_prefix.startswith("m/44'/1'"):
45
- network = coin.NETWORKS.TESTNET
46
- elif path_prefix.startswith("m/84'/0'"):
47
- network = coin.NETWORKS.MAINNET
48
- elif path_prefix.startswith("m/44'/0'"):
44
+ elif path_prefix.startswith(("m/84'/0'", "m/44'/0'")):
49
45
  network = coin.NETWORKS.MAINNET
50
46
  else:
51
47
  raise ValueError("Invalid path")
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-btc
3
+ Version: 0.3.0
4
+ Requires-Python: >=3.12
5
+ Requires-Dist: bitcoinlib~=0.7.1
6
+ Requires-Dist: bit~=0.8.0
7
+ Requires-Dist: hdwallet~=3.2.3
8
+ Requires-Dist: mm-crypto-utils>=0.1.3
9
+ Requires-Dist: mnemonic~=0.21
10
+ Requires-Dist: typer~=0.15.1
@@ -0,0 +1,17 @@
1
+ mm_btc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mm_btc/blockstream.py,sha256=hnZ4pSPUbXCoPA0zPaEazwibDvy-WXHCXk0hrix-grA,3347
3
+ mm_btc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ mm_btc/tx.py,sha256=7KTMNuL1n8rRj_245BV1bH9M2_PctNvlvPsLEsRTYx4,245
5
+ mm_btc/wallet.py,sha256=_pGTuAf6Y9KzaWTTSg_tn6WEoRsGqhxJuPof0_xyrmM,2333
6
+ mm_btc/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ mm_btc/cli/cli.py,sha256=-UQ_StfyHS0p5gSD7bWWsibjJh3FJDSaWkVnlRUvZds,2615
8
+ mm_btc/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ mm_btc/cli/cmd/address_cmd.py,sha256=fipX9FiQ2EgCEq8nVN6ELnDMoR5KTIX7bpAaRK3t_yg,284
10
+ mm_btc/cli/cmd/create_tx_cmd.py,sha256=QHhfA91FGWf5r6DkWTaXInZJi6HYLfxueA1GtcBD2-s,703
11
+ mm_btc/cli/cmd/decode_tx_cmd.py,sha256=0jGlUjnA1X2Q2aYwSBeAjqVNZIBsW5X2lEwIpSfWJsU,175
12
+ mm_btc/cli/cmd/mnemonic_cmd.py,sha256=CY6tPsqOTNfM-4WhKDhqitCi2OeqSIUMEUTFIRGHwIg,1254
13
+ mm_btc/cli/cmd/utxo_cmd.py,sha256=mp-lVLURpXFqO3IeBIEwnHusEvT5tFjUKxnRYC-rFqQ,294
14
+ mm_btc-0.3.0.dist-info/METADATA,sha256=07SoipqPS9Ogpdp2jM5tu29r9I6xX07NQ837ijTchkY,261
15
+ mm_btc-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ mm_btc-0.3.0.dist-info/entry_points.txt,sha256=KphzLNE9eb9H1DO-L0gvBQaQT5ImedyVCN4a6svfiRg,46
17
+ mm_btc-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mm-btc = mm_btc.cli.cli:app
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mm-btc
3
- Version: 0.2.0
4
- Requires-Python: >=3.12
5
- Requires-Dist: bitcoinlib~=0.7.1
6
- Requires-Dist: bit~=0.8.0
7
- Requires-Dist: hdwallet~=3.2.0
8
- Requires-Dist: mm-std~=0.1.9
9
- Requires-Dist: mnemonic>=0.21
@@ -1,8 +0,0 @@
1
- mm_btc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- mm_btc/blockstream.py,sha256=U2_pc6RYAkFcCvH_WrxjIm13VNavyY3WgK8ePowV-Ic,3620
3
- mm_btc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- mm_btc/tx.py,sha256=7KTMNuL1n8rRj_245BV1bH9M2_PctNvlvPsLEsRTYx4,245
5
- mm_btc/wallet.py,sha256=E9XKZMQx48JiGRDG8ij7cbilNPVmcpmS6jAGBzXfV9U,2475
6
- mm_btc-0.2.0.dist-info/METADATA,sha256=drZ2vHDl89rFCn6SJDDaELptn97EiCTGnXIwD92rWXg,223
7
- mm_btc-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- mm_btc-0.2.0.dist-info/RECORD,,
File without changes