doublezero-serviceability 0.0.2__py3-none-any.whl
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.
- doublezero_serviceability-0.0.2.dist-info/METADATA +9 -0
- doublezero_serviceability-0.0.2.dist-info/RECORD +14 -0
- doublezero_serviceability-0.0.2.dist-info/WHEEL +4 -0
- serviceability/__init__.py +0 -0
- serviceability/client.py +85 -0
- serviceability/config.py +15 -0
- serviceability/pda.py +20 -0
- serviceability/rpc.py +48 -0
- serviceability/state.py +832 -0
- serviceability/tests/__init__.py +0 -0
- serviceability/tests/test_compat.py +145 -0
- serviceability/tests/test_enum_strings.py +110 -0
- serviceability/tests/test_fixtures.py +330 -0
- serviceability/tests/test_pda.py +36 -0
|
@@ -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,14 @@
|
|
|
1
|
+
serviceability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
serviceability/client.py,sha256=3FYGJqIgkRm-Kc7DrckXVPreIZQP_ozn9lt8mUDnNQY,2335
|
|
3
|
+
serviceability/config.py,sha256=F04WS56QdiS1Uj_QnkkIsHaLi-RLLNS9BTxao-Gt5yo,690
|
|
4
|
+
serviceability/pda.py,sha256=wWEruxwSAiVCzrZXdDd_hfR3udTMGZQgqxcp99_tHaY,739
|
|
5
|
+
serviceability/rpc.py,sha256=G7GPRl0DM3x3p6B3NTdIgjDJTljwCat9-C_XKL5wXwM,1548
|
|
6
|
+
serviceability/state.py,sha256=ekUD4rprfo4ipmBNWgUNBsCrjFJeFH0_wIKUU0-OeKA,23905
|
|
7
|
+
serviceability/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
serviceability/tests/test_compat.py,sha256=H2N1iTJWxovhIIy1nJIPpTFhL3eiNNcoiT1DCPZExtA,4903
|
|
9
|
+
serviceability/tests/test_enum_strings.py,sha256=hL-WoPRg2xa32UdxOq3GAyiyoEqQc2HyrC7winDoXgk,3448
|
|
10
|
+
serviceability/tests/test_fixtures.py,sha256=9Tit9fyyADbE4KdPg5_IMv9VPk39Ge4obGGYLuW209Q,11132
|
|
11
|
+
serviceability/tests/test_pda.py,sha256=9uOGU821Y98J6JdvZ6AiESvN9leYDT8K8vNmCqqfCGs,998
|
|
12
|
+
doublezero_serviceability-0.0.2.dist-info/METADATA,sha256=SF8DeKOgoNBawRmDozmk611qzk3IiK1t1wozZzzPVbs,249
|
|
13
|
+
doublezero_serviceability-0.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
doublezero_serviceability-0.0.2.dist-info/RECORD,,
|
|
File without changes
|
serviceability/client.py
ADDED
|
@@ -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")
|
serviceability/config.py
ADDED
|
@@ -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
|
+
}
|
serviceability/pda.py
ADDED
|
@@ -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)
|
serviceability/rpc.py
ADDED
|
@@ -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
|