mm-apt 0.3.4__tar.gz → 0.4.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_apt-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-apt
3
+ Version: 0.4.0
4
+ Requires-Python: >=3.13
5
+ Requires-Dist: mm-cryptocurrency~=0.4.4
@@ -20,8 +20,10 @@ lint: format
20
20
  uv run mypy src
21
21
 
22
22
  audit:
23
- uv run pip-audit
24
- uv run bandit -r -c "pyproject.toml" src
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
25
+ rm requirements.txt
26
+ uv run bandit -q -r -c "pyproject.toml" src
25
27
 
26
28
  publish: build
27
29
  git diff-index --quiet HEAD
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "mm-apt"
3
- version = "0.3.4"
3
+ version = "0.4.0"
4
4
  description = ""
5
- requires-python = ">=3.12"
5
+ requires-python = ">=3.13"
6
6
  dependencies = [
7
- "mm-crypto-utils>=0.3.6",
7
+ "mm-cryptocurrency~=0.4.4",
8
8
  ]
9
9
 
10
10
  [build-system]
@@ -13,13 +13,14 @@ build-backend = "hatchling.build"
13
13
 
14
14
  [tool.uv]
15
15
  dev-dependencies = [
16
- "pytest~=8.3.5",
17
- "pytest-xdist~=3.6.1",
18
- "ruff~=0.11.6",
16
+ "pytest~=8.4.0",
17
+ "pytest-asyncio~=1.0.0",
18
+ "pytest-xdist~=3.7.0",
19
+ "ruff~=0.11.13",
19
20
  "pip-audit~=2.9.0",
20
21
  "bandit~=1.8.3",
21
- "mypy~=1.15.0",
22
- "pytest-asyncio~=0.26.0",
22
+ "mypy~=1.16.0",
23
+ "python-dotenv>=1.1.0",
23
24
  ]
24
25
 
25
26
  [tool.mypy]
@@ -0,0 +1,36 @@
1
+ import re
2
+
3
+ # Maximum allowable Aptos account address value (256 bits)
4
+ MAX_APTOS_ADDRESS = 2**256
5
+
6
+
7
+ def is_valid_address(address: str) -> bool:
8
+ """
9
+ Check if the address is a valid Aptos account address.
10
+
11
+ Requirements:
12
+ - Must be a 32-byte (64 hex characters) string.
13
+ - Must be an entire 64-character hex string, padded with leading zeros as needed.
14
+ - Optional '0x' or '0X' prefix is allowed.
15
+ - Numeric value must be < 2**256.
16
+ """
17
+ # Ensure input is a string
18
+ if not isinstance(address, str):
19
+ return False
20
+
21
+ # Remove optional prefix
22
+ hex_part = address[2:] if address.startswith(("0x", "0X")) else address
23
+
24
+ # Must be exactly 64 hex characters
25
+ if len(hex_part) != 64:
26
+ return False
27
+ if not re.fullmatch(r"[0-9a-fA-F]{64}", hex_part):
28
+ return False
29
+
30
+ # Convert to integer and check range
31
+ try:
32
+ value = int(hex_part, 16)
33
+ except ValueError:
34
+ return False
35
+
36
+ return 0 <= value < MAX_APTOS_ADDRESS
@@ -1,27 +1,28 @@
1
- from mm_std import Result, http_request
1
+ from mm_http import http_request
2
+ from mm_result import Result
2
3
 
3
4
 
4
5
  async def address_to_name(address: str, timeout: float = 5, proxy: str | None = None) -> Result[str | None]:
5
6
  url = f"https://www.aptosnames.com/api/mainnet/v1/name/{address}"
6
7
  res = await http_request(url, proxy=proxy, timeout=timeout)
7
8
  if res.is_err():
8
- return res.to_err()
9
+ return res.to_result_err()
9
10
  json_res = res.parse_json_body()
10
11
  if res.status_code == 200 and json_res == {}:
11
- return res.to_ok(None)
12
+ return res.to_result_ok(None)
12
13
  if "name" in json_res:
13
- return res.to_ok(json_res["name"])
14
- return res.to_err("unknown_response")
14
+ return res.to_result_ok(json_res["name"])
15
+ return res.to_result_err("unknown_response")
15
16
 
16
17
 
17
18
  async def address_to_primary_name(address: str, timeout: float = 5, proxy: str | None = None) -> Result[str | None]:
18
19
  url = f"https://www.aptosnames.com/api/mainnet/v1/primary-name/{address}"
19
20
  res = await http_request(url, proxy=proxy, timeout=timeout)
20
21
  if res.is_err():
21
- return res.to_err()
22
+ return res.to_result_err()
22
23
  json_res = res.parse_json_body()
23
24
  if res.status_code == 200 and json_res == {}:
24
- return res.to_ok(None)
25
+ return res.to_result_ok(None)
25
26
  if "name" in json_res:
26
- return res.to_ok(json_res["name"])
27
- return res.to_err("unknown_response")
27
+ return res.to_result_ok(json_res["name"])
28
+ return res.to_result_err("unknown_response")
@@ -1,4 +1,5 @@
1
- from mm_std import Result, http_request
1
+ from mm_http import http_request
2
+ from mm_result import Result
2
3
 
3
4
 
4
5
  async def get_balance(node: str, account: str, coin_type: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
@@ -7,7 +8,7 @@ async def get_balance(node: str, account: str, coin_type: str, timeout: float =
7
8
  try:
8
9
  json_res = res.parse_json_body()
9
10
  if json_res.get("error_code") == "resource_not_found":
10
- return res.to_ok(0)
11
- return res.to_ok(int(json_res["data"]["coin"]["value"]))
11
+ return res.to_result_ok(0)
12
+ return res.to_result_ok(int(json_res["data"]["coin"]["value"]))
12
13
  except Exception as e:
13
- return res.to_err(e)
14
+ return res.to_result_err(e)
@@ -1,5 +1,5 @@
1
- from mm_crypto_utils import Nodes, Proxies, retry_with_node_and_proxy
2
- from mm_std import Result
1
+ from mm_cryptocurrency import Nodes, Proxies, retry_with_node_and_proxy
2
+ from mm_result import Result
3
3
 
4
4
  from mm_apt import balance
5
5
 
@@ -0,0 +1,20 @@
1
+ import os
2
+
3
+ import pytest
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ MAINNET_RPC_URL = os.getenv("MAINNET_RPC_URL")
9
+
10
+
11
+ @pytest.fixture
12
+ def mainnet_rpc_url() -> str:
13
+ if not MAINNET_RPC_URL:
14
+ raise ValueError("MAINNET_RPC_URL environment variable is not set.")
15
+ return MAINNET_RPC_URL
16
+
17
+
18
+ @pytest.fixture
19
+ def okx_address() -> str:
20
+ return "0x834d639b10d20dcb894728aa4b9b572b2ea2d97073b10eacb111f338b20ea5d7"
@@ -0,0 +1,55 @@
1
+ import pytest
2
+
3
+ from mm_apt.account import is_valid_address
4
+
5
+
6
+ # Helper to generate a full 64-char hex string with a given repeat char
7
+ def full_hex(ch):
8
+ return ch * 64
9
+
10
+
11
+ def test_valid_full_length_addresses():
12
+ # All zeros
13
+ assert is_valid_address("0x" + full_hex("0")) is True
14
+ # Mixed zeros and ones
15
+ assert is_valid_address("0x" + "0" * 63 + "1") is True
16
+ # All Fs (max minus 1)
17
+ assert is_valid_address("0x" + full_hex("f")) is True
18
+ # Uppercase hex
19
+ assert is_valid_address("0X" + full_hex("A")) is True
20
+ # Without prefix
21
+ assert is_valid_address(full_hex("1")) is True
22
+
23
+
24
+ def test_invalid_full_length_addresses():
25
+ # Too short
26
+ short_hex = "0x" + "1" * 63
27
+ assert is_valid_address(short_hex) is False
28
+ # Too long
29
+ long_hex = "0x" + "f" * 65
30
+ assert is_valid_address(long_hex) is False
31
+ # Invalid character
32
+ bad_char = "0x" + "g" + "0" * 63
33
+ assert is_valid_address(bad_char) is False
34
+ # Numeric type
35
+ assert is_valid_address(123) is False
36
+
37
+
38
+ def test_address_out_of_range():
39
+ # Exactly 2**256 is out of range -> 1 followed by 64 zeros in hex is too large (65 hex digits)
40
+ out_of_range = "0x1" + "0" * 64
41
+ # It's invalid by length then by range
42
+ assert is_valid_address(out_of_range) is False
43
+ # Highest valid: 2**256 - 1 -> 64 hex 'f'
44
+ max_valid = "0x" + full_hex("f")
45
+ assert is_valid_address(max_valid) is True
46
+
47
+
48
+ def test_missing_prefix_short_address():
49
+ # Even valid numeric value but missing prefix and not full-length
50
+ assert is_valid_address("1" * 1) is False
51
+ assert is_valid_address("a" * 10) is False
52
+
53
+
54
+ if __name__ == "__main__":
55
+ pytest.main()