mm-web3 0.5.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.
@@ -0,0 +1,16 @@
1
+ .idea
2
+ .vscode
3
+ .venv
4
+ .env
5
+ .coverage
6
+ /htmlcov
7
+ __pycache__
8
+ *.egg-info
9
+ pip-wheel-metadata
10
+ .pytest_cache
11
+ .mypy_cache
12
+ .ruff_cache
13
+ /dist
14
+ /build
15
+ /tmp
16
+ .DS_Store
@@ -0,0 +1,10 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-toml
9
+ - id: check-json
10
+ - id: check-added-large-files
mm_web3-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-web3
3
+ Version: 0.5.0
4
+ Requires-Python: >=3.13
5
+ Requires-Dist: loguru>=0.7.3
6
+ Requires-Dist: mm-http~=0.1.0
7
+ Requires-Dist: mm-print~=0.1.1
8
+ Requires-Dist: mm-std~=0.5.3
@@ -0,0 +1,234 @@
1
+ # mm-web3
2
+
3
+ A Python library providing utilities for working with multiple blockchain networks, proxy management, and reliable network operations for cryptocurrency applications.
4
+
5
+ ## Features
6
+
7
+ ### 🔗 Multi-Blockchain Support
8
+ - **19 blockchain networks** including Ethereum, Arbitrum, Polygon, Solana, Aptos, StarkNet and more
9
+ - **4 network types**: EVM, Solana, Aptos, StarkNet
10
+ - Network-specific explorer URL generation for tokens and accounts
11
+ - Address formatting utilities (lowercase for certain networks)
12
+
13
+ ### 🌐 Proxy Management
14
+ - Fetch proxy lists from remote URLs
15
+ - Validate proxy URLs (HTTP, HTTPS, SOCKS4, SOCKS5)
16
+ - Random proxy selection from pools
17
+ - Support for authenticated proxies
18
+
19
+ ### 🔄 Retry Logic
20
+ - Automatic retry with random node/proxy combinations
21
+ - Configurable retry attempts
22
+ - Detailed logging of retry attempts
23
+ - Support for both node+proxy and proxy-only scenarios
24
+
25
+ ### ⚙️ Configuration System
26
+ - TOML-based configuration with validation
27
+ - ZIP archive support with optional encryption
28
+ - Async and sync configuration loading
29
+ - Pydantic-based validation with detailed error reporting
30
+
31
+ ### 🎯 Node Management
32
+ - Random blockchain node selection
33
+ - Support for single nodes or node pools
34
+ - URL normalization (trailing slash removal)
35
+
36
+
37
+ ## Quick Start
38
+
39
+ ### Basic Usage
40
+
41
+ ```python
42
+ import asyncio
43
+ from mm_web3 import Network, fetch_proxies, random_node, random_proxy
44
+
45
+ async def main():
46
+ # Get explorer URL for a token
47
+ network = Network.ETHEREUM
48
+ token_url = network.explorer_token("0xa0b86991c31cc0e37faeded5b4648648e8dd4a0425")
49
+ print(f"USDC on Ethereum: {token_url}")
50
+
51
+ # Fetch proxies from a URL
52
+ proxies_result = await fetch_proxies("https://example.com/proxy-list.txt")
53
+ if proxies_result.is_ok():
54
+ proxies = proxies_result.unwrap()
55
+ proxy = random_proxy(proxies)
56
+ print(f"Using proxy: {proxy}")
57
+
58
+ # Select random node from a pool
59
+ nodes = [
60
+ "https://eth-mainnet.g.alchemy.com/v2/your-key",
61
+ "https://mainnet.infura.io/v3/your-key",
62
+ "https://rpc.ankr.com/eth"
63
+ ]
64
+ node = random_node(nodes)
65
+ print(f"Using node: {node}")
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ ### Configuration with TOML
71
+
72
+ ```python
73
+ from pathlib import Path
74
+ from pydantic import BaseModel
75
+ from mm_web3.config import Web3CliConfig
76
+
77
+ class MyConfig(Web3CliConfig):
78
+ api_key: str
79
+ networks: list[str]
80
+ retry_count: int = 3
81
+
82
+ # config.toml
83
+ """
84
+ api_key = "your-api-key"
85
+ networks = ["ethereum", "polygon", "arbitrum-one"]
86
+ retry_count = 5
87
+ """
88
+
89
+ config = MyConfig.read_toml_config_or_exit(Path("config.toml"))
90
+ print(f"Loaded config: {config.api_key}")
91
+ ```
92
+
93
+ ### Retry Logic with Nodes and Proxies
94
+
95
+ ```python
96
+ import asyncio
97
+ from mm_web3.retry import retry_with_node_and_proxy
98
+ from mm_result import Result
99
+
100
+ async def make_request(node: str, proxy: str | None) -> Result[dict]:
101
+ # Your HTTP request logic here
102
+ # Return Result.ok(data) on success or Result.err(error) on failure
103
+ pass
104
+
105
+ async def main():
106
+ nodes = ["https://rpc1.example.com", "https://rpc2.example.com"]
107
+ proxies = ["http://proxy1:8080", "http://proxy2:8080"]
108
+
109
+ result = await retry_with_node_and_proxy(
110
+ retries=3,
111
+ nodes=nodes,
112
+ proxies=proxies,
113
+ func=make_request
114
+ )
115
+
116
+ if result.is_ok():
117
+ data = result.unwrap()
118
+ print(f"Success: {data}")
119
+ else:
120
+ print(f"Failed after retries: {result.error}")
121
+
122
+ asyncio.run(main())
123
+ ```
124
+
125
+ ## Supported Networks
126
+
127
+ ### EVM Networks
128
+ - Ethereum, Arbitrum One, Avalanche C-Chain
129
+ - Base, BSC, Celo, Core
130
+ - Fantom, Linea, OpBNB, OP Mainnet
131
+ - Polygon, Polygon zkEVM, Scroll
132
+ - zkSync Era, Zora
133
+
134
+ ### Other Networks
135
+ - **Solana**: Solana mainnet
136
+ - **Aptos**: Aptos mainnet
137
+ - **StarkNet**: StarkNet mainnet
138
+
139
+ Each network provides:
140
+ - Explorer URL generation for tokens and accounts
141
+ - Network type classification
142
+ - Address formatting preferences
143
+
144
+ ## Configuration
145
+
146
+ The library supports TOML configuration files with:
147
+ - **Validation**: Pydantic-based schema validation
148
+ - **ZIP Support**: Load configs from encrypted ZIP archives
149
+ - **Async Loading**: For configs with async validators
150
+ - **Error Handling**: Detailed validation error messages
151
+
152
+ ### Configuration Example
153
+
154
+ ```toml
155
+ # config.toml
156
+ [network]
157
+ default = "ethereum"
158
+ nodes = [
159
+ "https://eth-mainnet.g.alchemy.com/v2/key1",
160
+ "https://mainnet.infura.io/v3/key2"
161
+ ]
162
+
163
+ [proxy]
164
+ enabled = true
165
+ sources = ["https://proxy-list.example.com/free.txt"]
166
+ timeout = 5.0
167
+
168
+ [retry]
169
+ max_attempts = 3
170
+ backoff_seconds = 1.0
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ### Setup
176
+
177
+ ```bash
178
+ # Clone and setup
179
+ git clone <repository-url>
180
+ cd mm-cryptocurrency
181
+ uv sync
182
+
183
+ # Run tests
184
+ just test
185
+
186
+ # Format code
187
+ just format
188
+
189
+ # Run linting
190
+ just lint
191
+
192
+ # Run security audit
193
+ just audit
194
+ ```
195
+
196
+ ### Requirements
197
+
198
+ - **Python 3.13+**
199
+ - **uv** for package management
200
+ - Dependencies: `mm-http`, `mm-print`, `mm-result`
201
+
202
+ ### Testing
203
+
204
+ The library includes comprehensive tests covering:
205
+ - Network definitions and utilities
206
+ - Proxy fetching and validation
207
+ - Configuration loading and validation
208
+ - Retry logic and error handling
209
+ - Utility functions
210
+
211
+ Run tests with: `just test` or `uv run pytest`
212
+
213
+ ## API Reference
214
+
215
+ ### Core Classes
216
+
217
+ - **`Network`**: Enum of supported blockchain networks
218
+ - **`NetworkType`**: Base network types (EVM, Solana, etc.)
219
+ - **`Web3CliConfig`**: Base class for TOML configuration
220
+
221
+ ### Functions
222
+
223
+ - **`fetch_proxies(url)`**: Fetch proxy list from URL
224
+ - **`fetch_proxies_sync(url)`**: Synchronous proxy fetching
225
+ - **`random_proxy(proxies)`**: Select random proxy from pool
226
+ - **`random_node(nodes)`**: Select random node from pool
227
+ - **`is_valid_proxy_url(url)`**: Validate proxy URL format
228
+ - **`retry_with_node_and_proxy()`**: Retry with node/proxy rotation
229
+ - **`retry_with_proxy()`**: Retry with proxy-only rotation
230
+
231
+ ### Type Aliases
232
+
233
+ - **`Proxies`**: `str | Sequence[str] | None` - Proxy configuration
234
+ - **`Nodes`**: `str | Sequence[str]` - Node configuration
mm_web3-0.5.0/dict.dic ADDED
File without changes
mm_web3-0.5.0/justfile ADDED
@@ -0,0 +1,40 @@
1
+ version := `uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])'`
2
+
3
+
4
+ clean:
5
+ rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage dist build src/*.egg-info
6
+
7
+ build: clean
8
+ uv build
9
+
10
+ format:
11
+ uv run ruff check --select I --fix src tests
12
+ uv run ruff format src tests
13
+
14
+ test:
15
+ uv run pytest -n auto tests
16
+
17
+ lint: format pre-commit
18
+ uv run ruff check src tests
19
+ uv run mypy src
20
+
21
+ audit:
22
+ uv export --no-dev --all-extras --format requirements-txt --no-emit-project > requirements.txt
23
+ uv run pip-audit -r requirements.txt --disable-pip
24
+ rm requirements.txt
25
+ uv run bandit -q -r -c "pyproject.toml" src
26
+
27
+ publish: build lint audit test
28
+ git diff-index --quiet HEAD
29
+ printf "Enter PyPI token: " && IFS= read -rs TOKEN && echo && uv publish --token "$TOKEN"
30
+ git tag -a 'v{{version}}' -m 'v{{version}}'
31
+ git push origin v{{version}}
32
+
33
+ sync:
34
+ uv sync --all-extras
35
+
36
+ pre-commit:
37
+ uv run pre-commit run --all-files
38
+
39
+ pre-commit-autoupdate:
40
+ uv run pre-commit autoupdate
@@ -0,0 +1,83 @@
1
+ [project]
2
+ name = "mm-web3"
3
+ version = "0.5.0"
4
+ description = ""
5
+ requires-python = ">=3.13"
6
+ dependencies = [
7
+ "mm-std~=0.5.3",
8
+ "mm-http~=0.1.0",
9
+ "mm-print~=0.1.1",
10
+ "loguru>=0.7.3",
11
+ ]
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.uv]
18
+ dev-dependencies = [
19
+ "pytest~=8.4.0",
20
+ "pytest-asyncio~=1.0.0",
21
+ "pytest-xdist~=3.7.0",
22
+ "pytest-httpserver~=1.1.3",
23
+ "ruff~=0.11.13",
24
+ "mypy~=1.16.0",
25
+ "pip-audit~=2.9.0",
26
+ "bandit~=1.8.3",
27
+ "pre-commit~=4.2.0",
28
+ "python-dotenv~=1.1.0",
29
+ "eth-account>=0.13.7",
30
+ ]
31
+
32
+ [tool.mypy]
33
+ python_version = "3.13"
34
+ warn_no_return = false
35
+ strict = true
36
+ exclude = ["^tests/", "^tmp/"]
37
+
38
+ [tool.ruff]
39
+ line-length = 130
40
+ target-version = "py313"
41
+ [tool.ruff.lint]
42
+ select = ["ALL"]
43
+ ignore = [
44
+ "TC", # flake8-type-checking, TYPE_CHECKING is dangerous, for example it doesn't work with pydantic
45
+ "A005", # flake8-builtins: stdlib-module-shadowing
46
+ "ERA001", # eradicate: commented-out-code
47
+ "PT", # flake8-pytest-style
48
+ "D", # pydocstyle
49
+ "FIX", # flake8-fixme
50
+ "PLR0911", # pylint: too-many-return-statements
51
+ "PLR0912", # pylint: too-many-branches
52
+ "PLR0913", # pylint: too-many-arguments
53
+ "PLR2004", # pylint: magic-value-comparison
54
+ "PLC0414", # pylint: useless-import-alias
55
+ "FBT", # flake8-boolean-trap
56
+ "EM", # flake8-errmsg
57
+ "TRY003", # tryceratops: raise-vanilla-args
58
+ "C901", # mccabe: complex-structure,
59
+ "BLE001", # flake8-blind-except
60
+ "S311", # bandit: suspicious-non-cryptographic-random-usage
61
+ "TD002", # flake8-todos: missing-todo-author
62
+ "TD003", # flake8-todos: missing-todo-link
63
+ "RET503", # flake8-return: implicit-return
64
+ "COM812", # it's used in ruff formatter
65
+ "ASYNC109",
66
+ "G004",
67
+ ]
68
+ [tool.ruff.lint.pep8-naming]
69
+ classmethod-decorators = ["field_validator"]
70
+ [tool.ruff.lint.per-file-ignores]
71
+ "tests/*.py" = ["ANN", "S"]
72
+ [tool.ruff.format]
73
+ quote-style = "double"
74
+ indent-style = "space"
75
+
76
+ [tool.bandit]
77
+ exclude_dirs = ["tests"]
78
+ skips = ["B311"]
79
+
80
+ [tool.pytest.ini_options]
81
+ markers = ["proxy: requires access proxies"]
82
+ asyncio_mode = "auto"
83
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,21 @@
1
+ from mm_web3.account import PrivateKeyMap as PrivateKeyMap
2
+ from mm_web3.calcs import calc_decimal_expression as calc_decimal_expression
3
+ from mm_web3.calcs import calc_expression_with_vars as calc_expression_with_vars
4
+ from mm_web3.calcs import convert_value_with_units as convert_value_with_units
5
+ from mm_web3.config import Web3CliConfig as Web3CliConfig
6
+ from mm_web3.log import init_loguru as init_loguru
7
+ from mm_web3.network import Network as Network
8
+ from mm_web3.network import NetworkType as NetworkType
9
+ from mm_web3.node import Nodes as Nodes
10
+ from mm_web3.node import random_node as random_node
11
+ from mm_web3.proxy import Proxies as Proxies
12
+ from mm_web3.proxy import fetch_proxies as fetch_proxies
13
+ from mm_web3.proxy import fetch_proxies_sync as fetch_proxies_sync
14
+ from mm_web3.proxy import is_valid_proxy_url as is_valid_proxy_url
15
+ from mm_web3.proxy import random_proxy as random_proxy
16
+ from mm_web3.retry import retry_with_node_and_proxy as retry_with_node_and_proxy
17
+ from mm_web3.retry import retry_with_proxy as retry_with_proxy
18
+ from mm_web3.utils import read_items_from_file as read_items_from_file
19
+ from mm_web3.utils import read_lines_from_file as read_lines_from_file
20
+ from mm_web3.validators import ConfigValidators as ConfigValidators
21
+ from mm_web3.validators import Transfer as Transfer
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ from collections.abc import Callable
5
+ from pathlib import Path
6
+
7
+ from pydantic import GetCoreSchemaHandler, ValidationInfo
8
+ from pydantic_core import core_schema
9
+
10
+
11
+ class PrivateKeyMap(dict[str, str]):
12
+ """Map of addresses to private keys with fast lookup by address."""
13
+
14
+ def contains_all_addresses(self, addresses: list[str]) -> bool:
15
+ """Check if all addresses are in the map."""
16
+ return set(addresses) <= set(self.keys())
17
+
18
+ @classmethod
19
+ def __get_pydantic_core_schema__(cls, _source: object, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
20
+ # Use the dict schema as the basis.
21
+ return core_schema.with_info_after_validator_function(
22
+ cls.validate, # our function that converts a dict to PrivateKeyMap
23
+ handler(dict), # get the schema for a plain dict
24
+ )
25
+
26
+ @classmethod
27
+ def validate(cls, value: object, _info: ValidationInfo) -> PrivateKeyMap:
28
+ """
29
+ Convert and validate an input value into a PrivateKeyMap.
30
+
31
+ - If the input is already a PrivateKeyMap, return it.
32
+ - If it is a dict, check that all keys and values are strings and
33
+ then return a PrivateKeyMap.
34
+ - Otherwise, raise a TypeError.
35
+ """
36
+ if isinstance(value, cls):
37
+ return value
38
+ if isinstance(value, dict):
39
+ # Optionally, ensure all keys and values are strings.
40
+ if not all(isinstance(k, str) for k in value):
41
+ raise TypeError("All keys in PrivateKeyMap must be strings")
42
+ if not all(isinstance(v, str) for v in value.values()):
43
+ raise TypeError("All values in PrivateKeyMap must be strings")
44
+ return cls(value)
45
+ raise TypeError("Invalid type for PrivateKeyMap. Expected dict or PrivateKeyMap.")
46
+
47
+ @staticmethod
48
+ def from_list(private_keys: list[str], address_from_private: Callable[[str], str]) -> PrivateKeyMap:
49
+ """Create a dictionary of private keys with addresses as keys.
50
+
51
+ Args:
52
+ private_keys: List of private keys. Must be fully valid:
53
+ - No empty strings
54
+ - No whitespace-only strings
55
+ - No duplicates
56
+ address_from_private: Function to derive address from private key
57
+
58
+ Raises:
59
+ ValueError: if any private key is invalid
60
+ """
61
+ # Check for duplicates
62
+ if len(private_keys) != len(set(private_keys)):
63
+ raise ValueError("duplicate private keys found")
64
+
65
+ result = PrivateKeyMap()
66
+ for private_key in private_keys:
67
+ address = None
68
+ with contextlib.suppress(Exception):
69
+ address = address_from_private(private_key)
70
+ if address is None:
71
+ raise ValueError("invalid private key")
72
+ result[address] = private_key
73
+ return result
74
+
75
+ @staticmethod
76
+ def from_file(private_keys_file: Path, address_from_private: Callable[[str], str]) -> PrivateKeyMap:
77
+ """Create a dictionary of private keys with addresses as keys from a file.
78
+ Raises:
79
+ ValueError: If the file cannot be read or any private key is invalid.
80
+ """
81
+ private_keys_file = private_keys_file.expanduser()
82
+ try:
83
+ content = private_keys_file.read_text().strip()
84
+ except OSError as e:
85
+ raise ValueError(f"can't read from the file: {private_keys_file}") from e
86
+
87
+ private_keys = content.split("\n") if content else []
88
+ return PrivateKeyMap.from_list(private_keys, address_from_private)