mm-strk 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.
- mm_strk-0.4.1/.gitignore +14 -0
- mm_strk-0.4.1/PKG-INFO +7 -0
- mm_strk-0.4.1/README.txt +9 -0
- mm_strk-0.4.1/dict.dic +5 -0
- mm_strk-0.4.1/justfile +35 -0
- mm_strk-0.4.1/pyproject.toml +83 -0
- mm_strk-0.4.1/src/mm_strk/__init__.py +0 -0
- mm_strk-0.4.1/src/mm_strk/account.py +43 -0
- mm_strk-0.4.1/src/mm_strk/balance.py +36 -0
- mm_strk-0.4.1/src/mm_strk/cli/__init__.py +0 -0
- mm_strk-0.4.1/src/mm_strk/cli/cli.py +27 -0
- mm_strk-0.4.1/src/mm_strk/cli/cli_utils.py +5 -0
- mm_strk-0.4.1/src/mm_strk/cli/commands/__init__.py +3 -0
- mm_strk-0.4.1/src/mm_strk/cli/commands/node.py +49 -0
- mm_strk-0.4.1/src/mm_strk/domain.py +20 -0
- mm_strk-0.4.1/src/mm_strk/py.typed +0 -0
- mm_strk-0.4.1/tests/__init__.py +0 -0
- mm_strk-0.4.1/tests/conftest.py +25 -0
- mm_strk-0.4.1/tests/test_account.py +33 -0
- mm_strk-0.4.1/tests/test_balance.py +7 -0
- mm_strk-0.4.1/tests/test_domain.py +13 -0
- mm_strk-0.4.1/uv.lock +1399 -0
mm_strk-0.4.1/.gitignore
ADDED
mm_strk-0.4.1/PKG-INFO
ADDED
mm_strk-0.4.1/README.txt
ADDED
mm_strk-0.4.1/justfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
set dotenv-load
|
|
2
|
+
version := `uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])'`
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
clean:
|
|
6
|
+
rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage dist build src/*.egg-info
|
|
7
|
+
|
|
8
|
+
build: clean lint audit test
|
|
9
|
+
uv build
|
|
10
|
+
|
|
11
|
+
format:
|
|
12
|
+
uv run ruff check --select I --fix src tests
|
|
13
|
+
uv run ruff format src tests
|
|
14
|
+
|
|
15
|
+
test:
|
|
16
|
+
uv run pytest -n auto tests
|
|
17
|
+
|
|
18
|
+
lint: format
|
|
19
|
+
uv run ruff check src tests
|
|
20
|
+
uv run mypy src
|
|
21
|
+
|
|
22
|
+
audit:
|
|
23
|
+
uv export --no-dev --all-extras --format requirements-txt --no-emit-project > requirements.txt
|
|
24
|
+
uv run pip-audit -r requirements.txt --disable-pip --ignore-vuln GHSA-wj6h-64fc-37mp
|
|
25
|
+
rm requirements.txt
|
|
26
|
+
uv run bandit --silent --recursive --configfile "pyproject.toml" src
|
|
27
|
+
|
|
28
|
+
publish: build
|
|
29
|
+
git diff-index --quiet HEAD
|
|
30
|
+
uvx twine upload dist/**
|
|
31
|
+
git tag -a 'v{{version}}' -m 'v{{version}}'
|
|
32
|
+
git push origin v{{version}}
|
|
33
|
+
|
|
34
|
+
sync:
|
|
35
|
+
uv sync
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mm-strk"
|
|
3
|
+
version = "0.4.1"
|
|
4
|
+
description = ""
|
|
5
|
+
requires-python = ">=3.13"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"mm-cryptocurrency~=0.4.7",
|
|
8
|
+
"starknet-py~=0.27.0",
|
|
9
|
+
"typer>=0.16.0",
|
|
10
|
+
]
|
|
11
|
+
[project.scripts]
|
|
12
|
+
mm-strk = "mm_strk.cli.cli:app"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
[tool.uv]
|
|
21
|
+
dev-dependencies = [
|
|
22
|
+
"pytest~=8.4.0",
|
|
23
|
+
"pytest-asyncio~=1.0.0",
|
|
24
|
+
"pytest-xdist~=3.7.0",
|
|
25
|
+
"ruff~=0.11.13",
|
|
26
|
+
"pip-audit~=2.9.0",
|
|
27
|
+
"bandit~=1.8.3",
|
|
28
|
+
"mypy~=1.16.0",
|
|
29
|
+
"python-dotenv>=1.1.0",
|
|
30
|
+
]
|
|
31
|
+
override-dependencies = ["pywin32 ; sys_platform == 'win32'"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[tool.mypy]
|
|
35
|
+
python_version = "3.13"
|
|
36
|
+
warn_no_return = false
|
|
37
|
+
strict = true
|
|
38
|
+
exclude = ["^tests/", "^tmp/"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 130
|
|
42
|
+
target-version = "py313"
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["ALL"]
|
|
45
|
+
ignore = [
|
|
46
|
+
"TC", # flake8-type-checking, TYPE_CHECKING is dangerous, for example it doesn't work with pydantic
|
|
47
|
+
"A005", # flake8-builtins: stdlib-module-shadowing
|
|
48
|
+
"ERA001", # eradicate: commented-out-code
|
|
49
|
+
"PT", # flake8-pytest-style
|
|
50
|
+
"D", # pydocstyle
|
|
51
|
+
"FIX", # flake8-fixme
|
|
52
|
+
"PLR0911", # pylint: too-many-return-statements
|
|
53
|
+
"PLR0912", # pylint: too-many-branches
|
|
54
|
+
"PLR0913", # pylint: too-many-arguments
|
|
55
|
+
"PLR2004", # pylint: magic-value-comparison
|
|
56
|
+
"PLC0414", # pylint: useless-import-alias
|
|
57
|
+
"FBT", # flake8-boolean-trap
|
|
58
|
+
"EM", # flake8-errmsg
|
|
59
|
+
"TRY003", # tryceratops: raise-vanilla-args
|
|
60
|
+
"C901", # mccabe: complex-structure,
|
|
61
|
+
"BLE001", # flake8-blind-except
|
|
62
|
+
"S311", # bandit: suspicious-non-cryptographic-random-usage
|
|
63
|
+
"TD002", # flake8-todos: missing-todo-author
|
|
64
|
+
"TD003", # flake8-todos: missing-todo-link
|
|
65
|
+
"RET503", # flake8-return: implicit-return
|
|
66
|
+
"COM812", # it's used in ruff formatter
|
|
67
|
+
"ASYNC109", # flake8-async: async-function-with-timeout
|
|
68
|
+
]
|
|
69
|
+
[tool.ruff.lint.pep8-naming]
|
|
70
|
+
classmethod-decorators = ["field_validator"]
|
|
71
|
+
[tool.ruff.lint.per-file-ignores]
|
|
72
|
+
"tests/*.py" = ["ANN", "S"]
|
|
73
|
+
[tool.ruff.format]
|
|
74
|
+
quote-style = "double"
|
|
75
|
+
indent-style = "space"
|
|
76
|
+
|
|
77
|
+
[tool.bandit]
|
|
78
|
+
exclude_dirs = ["tests"]
|
|
79
|
+
skips = ["B311"]
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
asyncio_mode = "auto"
|
|
83
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
# Maximum allowable value for a StarkNet address (251 bits)
|
|
4
|
+
MAX_STARKNET_ADDRESS = 2**251
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_address(address: str) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Validates a StarkNet address.
|
|
10
|
+
|
|
11
|
+
- Must be a string starting with '0x'.
|
|
12
|
+
- Hex part 1-64 chars.
|
|
13
|
+
- Integer value < 2**251.
|
|
14
|
+
- Accepts either:
|
|
15
|
+
• Full 64-hex-character padded form.
|
|
16
|
+
• Minimal form without leading zeros (canonical).
|
|
17
|
+
"""
|
|
18
|
+
# Type and prefix
|
|
19
|
+
if not isinstance(address, str) or not address.startswith("0x"):
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
hex_part = address[2:]
|
|
23
|
+
# Length and hex
|
|
24
|
+
if len(hex_part) < 1 or len(hex_part) > 64:
|
|
25
|
+
return False
|
|
26
|
+
if not re.fullmatch(r"[0-9a-fA-F]+", hex_part):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
# Convert to integer and range check
|
|
30
|
+
try:
|
|
31
|
+
value = int(hex_part, 16)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return False
|
|
34
|
+
if value >= MAX_STARKNET_ADDRESS:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
# Full padded 64-char form
|
|
38
|
+
if len(hex_part) == 64:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
# Minimal form (no leading zeros)
|
|
42
|
+
canonical = hex(value)[2:]
|
|
43
|
+
return hex_part.lower() == canonical
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from aiohttp_socks import ProxyConnector
|
|
3
|
+
from mm_result import Result
|
|
4
|
+
from starknet_py.net.account.account import Account
|
|
5
|
+
from starknet_py.net.full_node_client import FullNodeClient
|
|
6
|
+
from starknet_py.net.models.chains import StarknetChainId
|
|
7
|
+
from starknet_py.net.signer.key_pair import KeyPair
|
|
8
|
+
|
|
9
|
+
ETH_ADDRESS_MAINNET = "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"
|
|
10
|
+
ETH_DECIMALS = 18
|
|
11
|
+
DAI_ADDRESS_MAINNET = "0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3"
|
|
12
|
+
DAI_DECIMALS = 18
|
|
13
|
+
USDC_ADDRESS_MAINNET = "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8"
|
|
14
|
+
USDC_DECIMALS = 6
|
|
15
|
+
USDT_ADDRESS_MAINNET = "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8"
|
|
16
|
+
USDT_DECIMALS = 6
|
|
17
|
+
STRK_ADDRESS_MAINNET = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"
|
|
18
|
+
STRK_DECIMALS = 18
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def get_balance(rpc_url: str, address: str, token: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
22
|
+
try:
|
|
23
|
+
timeout_config = aiohttp.ClientTimeout(total=timeout)
|
|
24
|
+
connector = ProxyConnector.from_url(proxy) if proxy else None
|
|
25
|
+
async with aiohttp.ClientSession(connector=connector, timeout=timeout_config) as session:
|
|
26
|
+
client = FullNodeClient(node_url=rpc_url, session=session)
|
|
27
|
+
account = Account(
|
|
28
|
+
address=address,
|
|
29
|
+
client=client,
|
|
30
|
+
chain=StarknetChainId.MAINNET,
|
|
31
|
+
key_pair=KeyPair(private_key=654, public_key=321),
|
|
32
|
+
)
|
|
33
|
+
balance = await account.get_balance(token_address=token)
|
|
34
|
+
return Result.ok(balance)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return Result.err(e)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import mm_print
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from mm_strk.cli import cli_utils, commands
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def version_callback(value: bool) -> None:
|
|
12
|
+
if value:
|
|
13
|
+
mm_print.plain(f"mm-strk: {cli_utils.get_version()}")
|
|
14
|
+
raise typer.Exit
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.callback()
|
|
18
|
+
def main(_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True)) -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command(name="node", help="Checks RPC URLs for availability and status")
|
|
23
|
+
def node_command(
|
|
24
|
+
urls: Annotated[list[str], typer.Argument()],
|
|
25
|
+
proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
commands.node.run(urls, proxy)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import mm_print
|
|
4
|
+
import typer
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from starknet_py.net.client_models import SyncStatus
|
|
7
|
+
from starknet_py.net.full_node_client import FullNodeClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeStatus(BaseModel):
|
|
11
|
+
block_number: int | str
|
|
12
|
+
chain_id: int | str
|
|
13
|
+
syncing_status: bool | SyncStatus | str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(urls: list[str], proxy: str | None) -> None:
|
|
17
|
+
if proxy:
|
|
18
|
+
typer.echo("proxy is not supported yet")
|
|
19
|
+
raise typer.Exit(code=1)
|
|
20
|
+
asyncio.run(_run(urls))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _run(urls: list[str]) -> None:
|
|
24
|
+
result = {}
|
|
25
|
+
for url in urls:
|
|
26
|
+
result[url] = (await _node_status(url)).model_dump()
|
|
27
|
+
|
|
28
|
+
mm_print.json(result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _node_status(url: str) -> NodeStatus:
|
|
32
|
+
client = FullNodeClient(node_url=url)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
block_number: int | str = await client.get_block_number()
|
|
36
|
+
except Exception as e:
|
|
37
|
+
block_number = str(e)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
chain_id: int | str = int(await client.get_chain_id(), 16)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
chain_id = str(e)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
syncing_status: bool | SyncStatus | str = await client.get_syncing_status()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
syncing_status = str(e)
|
|
48
|
+
|
|
49
|
+
return NodeStatus(block_number=block_number, chain_id=chain_id, syncing_status=syncing_status)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from mm_http import http_request
|
|
2
|
+
from mm_result import Result
|
|
3
|
+
from mm_std import str_contains_any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def address_to_domain(address: str, timeout: float = 5.0, proxy: str | None = None) -> Result[str | None]:
|
|
7
|
+
url = "https://api.starknet.id/addr_to_domain"
|
|
8
|
+
res = await http_request(url, params={"addr": address}, proxy=proxy, timeout=timeout)
|
|
9
|
+
if (
|
|
10
|
+
res.status_code == 400
|
|
11
|
+
and res.body is not None
|
|
12
|
+
and str_contains_any(res.body.lower(), ["no data found", "no domain found"])
|
|
13
|
+
):
|
|
14
|
+
return res.to_result_ok(None)
|
|
15
|
+
if res.is_err():
|
|
16
|
+
return res.to_result_err()
|
|
17
|
+
domain = res.parse_json_body("domain")
|
|
18
|
+
if domain:
|
|
19
|
+
return res.to_result_ok(domain)
|
|
20
|
+
return res.to_result_err("unknown_response")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
MAINNET_URL = os.getenv("MAINNET_URL")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def anyio_backend() -> str:
|
|
13
|
+
return "asyncio"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def zklend_market_address() -> str:
|
|
18
|
+
return "0x04c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mainnet_rpc_url() -> str:
|
|
23
|
+
if not MAINNET_URL:
|
|
24
|
+
raise ValueError("MAINNET_URL environment variable is not set.")
|
|
25
|
+
return MAINNET_URL
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from starknet_py.hash.address import get_checksum_address
|
|
2
|
+
|
|
3
|
+
from mm_strk.account import is_address
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_valid_addresses():
|
|
7
|
+
# Compact lower-case and mixed-case hex strings
|
|
8
|
+
assert is_address("0x1") is True
|
|
9
|
+
assert is_address("0x123abc") is True
|
|
10
|
+
assert is_address("0xabCdEf") is True
|
|
11
|
+
assert is_address("0x0701234567890123456789012345678901234567890123456789012345678901") is True
|
|
12
|
+
|
|
13
|
+
# Already checksummed address
|
|
14
|
+
raw = "0xdeadbeef"
|
|
15
|
+
checksummed = get_checksum_address(raw)
|
|
16
|
+
assert is_address(checksummed) is True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_invalid_addresses():
|
|
20
|
+
# Missing prefix or empty
|
|
21
|
+
assert is_address("123abc") is False
|
|
22
|
+
assert is_address("0x") is False
|
|
23
|
+
assert is_address("") is False
|
|
24
|
+
|
|
25
|
+
# Non-hex characters
|
|
26
|
+
assert is_address("0x12G45") is False
|
|
27
|
+
|
|
28
|
+
# Wrong type
|
|
29
|
+
assert is_address(None) is False
|
|
30
|
+
|
|
31
|
+
# Too many leading zeroes (invalid compact form)
|
|
32
|
+
# e.g., 0x0123 should canonicalise to 0x123
|
|
33
|
+
assert is_address("0x0123") is False
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from mm_strk import balance
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
async def test_get_balance(mainnet_rpc_url):
|
|
5
|
+
address = "0x076601136372fcdbbd914eea797082f7504f828e122288ad45748b0c8b0c9696" # Bybit: Hot Wallet
|
|
6
|
+
assert (await balance.get_balance(mainnet_rpc_url, address, balance.ETH_ADDRESS_MAINNET)).unwrap() > 1
|
|
7
|
+
assert (await balance.get_balance(mainnet_rpc_url, address, balance.STRK_ADDRESS_MAINNET)).unwrap() > 1
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from mm_strk import domain
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
async def test_address_to_domain_exist():
|
|
5
|
+
res = await domain.address_to_domain("0x0060b56b67e1b4dd1909376496b0e867f165f31c5eac7902d9ff48112f16ef9b")
|
|
6
|
+
assert res.is_ok()
|
|
7
|
+
assert res.unwrap() == "abc.stark"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def test_address_to_domain_async_not_exist():
|
|
11
|
+
res = await domain.address_to_domain("0x0060b56b67e1b4dd1909376496b0e867f165f31c5eac7902d9ff48112f16ef9a") # not existed
|
|
12
|
+
assert res.is_ok()
|
|
13
|
+
assert res.unwrap() is None
|