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.
- mm_web3-0.5.0/.gitignore +16 -0
- mm_web3-0.5.0/.pre-commit-config.yaml +10 -0
- mm_web3-0.5.0/PKG-INFO +8 -0
- mm_web3-0.5.0/README.md +234 -0
- mm_web3-0.5.0/dict.dic +0 -0
- mm_web3-0.5.0/justfile +40 -0
- mm_web3-0.5.0/pyproject.toml +83 -0
- mm_web3-0.5.0/src/mm_web3/__init__.py +21 -0
- mm_web3-0.5.0/src/mm_web3/account.py +88 -0
- mm_web3-0.5.0/src/mm_web3/calcs.py +217 -0
- mm_web3-0.5.0/src/mm_web3/config.py +160 -0
- mm_web3-0.5.0/src/mm_web3/log.py +20 -0
- mm_web3-0.5.0/src/mm_web3/network.py +191 -0
- mm_web3-0.5.0/src/mm_web3/node.py +38 -0
- mm_web3-0.5.0/src/mm_web3/proxy.py +106 -0
- mm_web3-0.5.0/src/mm_web3/py.typed +0 -0
- mm_web3-0.5.0/src/mm_web3/retry.py +68 -0
- mm_web3-0.5.0/src/mm_web3/utils.py +67 -0
- mm_web3-0.5.0/src/mm_web3/validators.py +350 -0
- mm_web3-0.5.0/tests/__init__.py +0 -0
- mm_web3-0.5.0/tests/common.py +21 -0
- mm_web3-0.5.0/tests/test_account.py +220 -0
- mm_web3-0.5.0/tests/test_calcs.py +254 -0
- mm_web3-0.5.0/tests/test_config.py +239 -0
- mm_web3-0.5.0/tests/test_network.py +14 -0
- mm_web3-0.5.0/tests/test_node.py +35 -0
- mm_web3-0.5.0/tests/test_proxy.py +251 -0
- mm_web3-0.5.0/tests/test_retry.py +124 -0
- mm_web3-0.5.0/tests/test_utils.py +300 -0
- mm_web3-0.5.0/tests/test_validators.py +492 -0
- mm_web3-0.5.0/uv.lock +1412 -0
mm_web3-0.5.0/.gitignore
ADDED
mm_web3-0.5.0/PKG-INFO
ADDED
mm_web3-0.5.0/README.md
ADDED
|
@@ -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)
|