doublezero-serviceability 0.0.2__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,35 @@
1
+ *.pdb
2
+ .DS_STORE
3
+ **/*.swp
4
+ **/*.rs.bk
5
+ **/coverage.out
6
+ debug/
7
+ target/
8
+ logs/
9
+ test-ledger/
10
+
11
+ **/bin/*
12
+ smartcontract/cli/config/*
13
+ controlplane/funder/cmd/funder/funder
14
+ revdist-cli
15
+
16
+ .idea
17
+ .private
18
+ .vscode/*
19
+ .vscode
20
+ .zed
21
+ .claude/plans/
22
+ .tmp
23
+ .env
24
+
25
+ dist/
26
+ node_modules/
27
+ __pycache__/
28
+ .pytest_cache/
29
+ .venv/
30
+
31
+ # Local devnet deployment artifacts.
32
+ dev/.deploy/
33
+
34
+ # Ignore the directory used for checking out the monitor tool in CI
35
+ /doublezero_monitor/
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: doublezero-serviceability
3
+ Version: 0.0.2
4
+ Summary: DoubleZero Serviceability SDK
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: borsh-incremental
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: solana>=0.35
9
+ Requires-Dist: solders>=0.21
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "doublezero-serviceability"
3
+ version = "0.0.2"
4
+ description = "DoubleZero Serviceability SDK"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "borsh-incremental",
8
+ "solana>=0.35",
9
+ "solders>=0.21",
10
+ "httpx>=0.27",
11
+ ]
12
+
13
+ [tool.pytest.ini_options]
14
+ testpaths = ["serviceability/tests"]
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["serviceability"]
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=9.0.2",
26
+ ]
27
+
28
+ [tool.uv.sources]
29
+ borsh-incremental = { path = "../../borsh-incremental/python", editable = true }
@@ -0,0 +1,85 @@
1
+ """RPC client for fetching serviceability program accounts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from solders.pubkey import Pubkey # type: ignore[import-untyped]
8
+ from solders.rpc.responses import GetAccountInfoResp # type: ignore[import-untyped]
9
+
10
+ from serviceability.config import PROGRAM_IDS, LEDGER_RPC_URLS
11
+ from serviceability.rpc import new_rpc_client
12
+ from serviceability.state import (
13
+ AccessPass,
14
+ Contributor,
15
+ Device,
16
+ Exchange,
17
+ GlobalConfig,
18
+ GlobalState,
19
+ Link,
20
+ Location,
21
+ MulticastGroup,
22
+ ProgramConfig,
23
+ User,
24
+ )
25
+
26
+
27
+ class SolanaClient(Protocol):
28
+ def get_account_info(self, pubkey: Pubkey) -> GetAccountInfoResp: ...
29
+
30
+
31
+ class ProgramData:
32
+ """Aggregate of all serviceability program accounts."""
33
+
34
+ def __init__(self) -> None:
35
+ self.global_state: GlobalState | None = None
36
+ self.global_config: GlobalConfig | None = None
37
+ self.program_config: ProgramConfig | None = None
38
+ self.locations: list[Location] = []
39
+ self.exchanges: list[Exchange] = []
40
+ self.devices: list[Device] = []
41
+ self.links: list[Link] = []
42
+ self.users: list[User] = []
43
+ self.multicast_groups: list[MulticastGroup] = []
44
+ self.contributors: list[Contributor] = []
45
+ self.access_passes: list[AccessPass] = []
46
+
47
+
48
+ class Client:
49
+ """Read-only client for serviceability program accounts."""
50
+
51
+ def __init__(
52
+ self,
53
+ solana_rpc: SolanaClient,
54
+ program_id: Pubkey,
55
+ ) -> None:
56
+ self._solana_rpc = solana_rpc
57
+ self._program_id = program_id
58
+
59
+ @classmethod
60
+ def from_env(cls, env: str) -> Client:
61
+ """Create a client configured for the given environment.
62
+
63
+ Args:
64
+ env: Environment name ("mainnet-beta", "testnet", "devnet", "localnet")
65
+ """
66
+ return cls(
67
+ new_rpc_client(LEDGER_RPC_URLS[env]),
68
+ Pubkey.from_string(PROGRAM_IDS[env]),
69
+ )
70
+
71
+ @classmethod
72
+ def mainnet_beta(cls) -> Client:
73
+ return cls.from_env("mainnet-beta")
74
+
75
+ @classmethod
76
+ def testnet(cls) -> Client:
77
+ return cls.from_env("testnet")
78
+
79
+ @classmethod
80
+ def devnet(cls) -> Client:
81
+ return cls.from_env("devnet")
82
+
83
+ @classmethod
84
+ def localnet(cls) -> Client:
85
+ return cls.from_env("localnet")
@@ -0,0 +1,15 @@
1
+ """Network configuration for the serviceability program."""
2
+
3
+ PROGRAM_IDS = {
4
+ "mainnet-beta": "ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv",
5
+ "testnet": "DZtnuQ839pSaDMFG5q1ad2V95G82S5EC4RrB3Ndw2Heb",
6
+ "devnet": "GYhQDKuESrasNZGyhMJhGYFtbzNijYhcrN9poSqCQVah",
7
+ "localnet": "7CTniUa88iJKUHTrCkB4TjAoG6TD7AMivhQeuqN2LPtX",
8
+ }
9
+
10
+ LEDGER_RPC_URLS = {
11
+ "mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab",
12
+ "testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16",
13
+ "devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16",
14
+ "localnet": "http://localhost:8899",
15
+ }
@@ -0,0 +1,20 @@
1
+ """PDA derivation for serviceability program accounts."""
2
+
3
+ from solders.pubkey import Pubkey # type: ignore[import-untyped]
4
+
5
+ SEED_PREFIX = b"doublezero"
6
+ SEED_GLOBAL_STATE = b"globalstate"
7
+ SEED_GLOBAL_CONFIG = b"config"
8
+ SEED_PROGRAM_CONFIG = b"programconfig"
9
+
10
+
11
+ def derive_global_state_pda(program_id: Pubkey) -> tuple[Pubkey, int]:
12
+ return Pubkey.find_program_address([SEED_PREFIX, SEED_GLOBAL_STATE], program_id)
13
+
14
+
15
+ def derive_global_config_pda(program_id: Pubkey) -> tuple[Pubkey, int]:
16
+ return Pubkey.find_program_address([SEED_PREFIX, SEED_GLOBAL_CONFIG], program_id)
17
+
18
+
19
+ def derive_program_config_pda(program_id: Pubkey) -> tuple[Pubkey, int]:
20
+ return Pubkey.find_program_address([SEED_PREFIX, SEED_PROGRAM_CONFIG], program_id)
@@ -0,0 +1,48 @@
1
+ """RPC client helpers with retry on rate limiting."""
2
+
3
+ import time
4
+
5
+ import httpx
6
+ from solana.rpc.api import Client as SolanaHTTPClient # type: ignore[import-untyped]
7
+
8
+ _DEFAULT_MAX_RETRIES = 5
9
+
10
+
11
+ class _RetryTransport(httpx.BaseTransport):
12
+ """HTTP transport that retries on 429 Too Many Requests."""
13
+
14
+ def __init__(
15
+ self,
16
+ wrapped: httpx.BaseTransport | None = None,
17
+ max_retries: int = _DEFAULT_MAX_RETRIES,
18
+ ) -> None:
19
+ self._wrapped = wrapped or httpx.HTTPTransport()
20
+ self._max_retries = max_retries
21
+
22
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
23
+ for attempt in range(self._max_retries + 1):
24
+ response = self._wrapped.handle_request(request)
25
+ if response.status_code != 429 or attempt >= self._max_retries:
26
+ return response
27
+ response.close()
28
+ time.sleep((attempt + 1) * 2)
29
+ return response # unreachable, but satisfies type checker
30
+
31
+
32
+ def new_rpc_client(
33
+ url: str,
34
+ timeout: float = 30,
35
+ max_retries: int = _DEFAULT_MAX_RETRIES,
36
+ ) -> SolanaHTTPClient:
37
+ """Create a Solana RPC client with automatic retry on 429 responses."""
38
+ client = SolanaHTTPClient(url, timeout=timeout)
39
+ # Replace the underlying httpx session with one using retry transport.
40
+ transport = _RetryTransport(
41
+ wrapped=httpx.HTTPTransport(),
42
+ max_retries=max_retries,
43
+ )
44
+ client._provider.session = httpx.Client(
45
+ timeout=timeout,
46
+ transport=transport,
47
+ )
48
+ return client