doublezero-telemetry 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.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: doublezero-telemetry
3
+ Version: 0.0.2
4
+ Summary: DoubleZero Telemetry 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,12 @@
1
+ telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ telemetry/client.py,sha256=gImIz7ozpXb87wfYMQrKVplakQ1CSLYvNKZXgAYYf9o,2728
3
+ telemetry/config.py,sha256=Vhn6wJjhEsODjGOGXll_ypZQstwHqYdb-9huG6pWs0Y,685
4
+ telemetry/pda.py,sha256=vlentmvAFyvRLYbS65pGxY7oqSCkbHjn8hrwjVaIK28,1416
5
+ telemetry/rpc.py,sha256=G7GPRl0DM3x3p6B3NTdIgjDJTljwCat9-C_XKL5wXwM,1548
6
+ telemetry/state.py,sha256=J0dHUKLG6-AZTnRrEjRRuGm3ACX9tPFoB8POrjsKQFM,4603
7
+ telemetry/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ telemetry/tests/test_fixtures.py,sha256=2cDoa5pGcKziDR-eNhrtJ-XCuRMexvnYiv402De-ClU,2985
9
+ telemetry/tests/test_pda.py,sha256=zFzEWbw_UVEppL4W75Lquf3t_4Sob3cbXo0l2Ow3hvM,1340
10
+ doublezero_telemetry-0.0.2.dist-info/METADATA,sha256=6kIU1xJN0nPwcN4oL0MbBuKZuHPFZUl-nntJSN2yCig,239
11
+ doublezero_telemetry-0.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ doublezero_telemetry-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
telemetry/__init__.py ADDED
File without changes
telemetry/client.py ADDED
@@ -0,0 +1,92 @@
1
+ """RPC client for fetching telemetry 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 telemetry.config import PROGRAM_IDS, LEDGER_RPC_URLS
11
+ from telemetry.rpc import new_rpc_client
12
+ from telemetry.pda import (
13
+ derive_device_latency_samples_pda,
14
+ derive_internet_latency_samples_pda,
15
+ )
16
+ from telemetry.state import DeviceLatencySamples, InternetLatencySamples
17
+
18
+
19
+ class SolanaClient(Protocol):
20
+ def get_account_info(self, pubkey: Pubkey) -> GetAccountInfoResp: ...
21
+
22
+
23
+ class Client:
24
+ """Read-only client for telemetry program accounts."""
25
+
26
+ def __init__(
27
+ self,
28
+ solana_rpc: SolanaClient,
29
+ program_id: Pubkey,
30
+ ) -> None:
31
+ self._solana_rpc = solana_rpc
32
+ self._program_id = program_id
33
+
34
+ @classmethod
35
+ def from_env(cls, env: str) -> Client:
36
+ """Create a client configured for the given environment.
37
+
38
+ Args:
39
+ env: Environment name ("mainnet-beta", "testnet", "devnet", "localnet")
40
+ """
41
+ return cls(
42
+ new_rpc_client(LEDGER_RPC_URLS[env]),
43
+ Pubkey.from_string(PROGRAM_IDS[env]),
44
+ )
45
+
46
+ @classmethod
47
+ def mainnet_beta(cls) -> Client:
48
+ return cls.from_env("mainnet-beta")
49
+
50
+ @classmethod
51
+ def testnet(cls) -> Client:
52
+ return cls.from_env("testnet")
53
+
54
+ @classmethod
55
+ def devnet(cls) -> Client:
56
+ return cls.from_env("devnet")
57
+
58
+ @classmethod
59
+ def localnet(cls) -> Client:
60
+ return cls.from_env("localnet")
61
+
62
+ def get_device_latency_samples(
63
+ self,
64
+ origin_device_pk: Pubkey,
65
+ target_device_pk: Pubkey,
66
+ link_pk: Pubkey,
67
+ epoch: int,
68
+ ) -> DeviceLatencySamples:
69
+ addr, _ = derive_device_latency_samples_pda(
70
+ self._program_id, origin_device_pk, target_device_pk, link_pk, epoch
71
+ )
72
+ resp = self._solana_rpc.get_account_info(addr)
73
+ return DeviceLatencySamples.from_bytes(resp.value.data)
74
+
75
+ def get_internet_latency_samples(
76
+ self,
77
+ collector_oracle_pk: Pubkey,
78
+ data_provider_name: str,
79
+ origin_location_pk: Pubkey,
80
+ target_location_pk: Pubkey,
81
+ epoch: int,
82
+ ) -> InternetLatencySamples:
83
+ addr, _ = derive_internet_latency_samples_pda(
84
+ self._program_id,
85
+ collector_oracle_pk,
86
+ data_provider_name,
87
+ origin_location_pk,
88
+ target_location_pk,
89
+ epoch,
90
+ )
91
+ resp = self._solana_rpc.get_account_info(addr)
92
+ return InternetLatencySamples.from_bytes(resp.value.data)
telemetry/config.py ADDED
@@ -0,0 +1,15 @@
1
+ """Network configuration for the telemetry program."""
2
+
3
+ PROGRAM_IDS = {
4
+ "mainnet-beta": "tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC",
5
+ "testnet": "3KogTMmVxc5eUHtjZnwm136H5P8tvPwVu4ufbGPvM7p1",
6
+ "devnet": "C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG",
7
+ "localnet": "C9xqH76NSm11pBS6maNnY163tWHT8Govww47uyEmSnoG",
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
+ }
telemetry/pda.py ADDED
@@ -0,0 +1,55 @@
1
+ """PDA derivation for telemetry program accounts."""
2
+
3
+ import struct
4
+
5
+ from solders.pubkey import Pubkey # type: ignore[import-untyped]
6
+
7
+ from telemetry.state import (
8
+ TELEMETRY_SEED_PREFIX,
9
+ DEVICE_LATENCY_SAMPLES_SEED,
10
+ INTERNET_LATENCY_SAMPLES_SEED,
11
+ )
12
+
13
+
14
+ def derive_device_latency_samples_pda(
15
+ program_id: Pubkey,
16
+ origin_device_pk: Pubkey,
17
+ target_device_pk: Pubkey,
18
+ link_pk: Pubkey,
19
+ epoch: int,
20
+ ) -> tuple[Pubkey, int]:
21
+ epoch_bytes = struct.pack("<Q", epoch)
22
+ return Pubkey.find_program_address(
23
+ [
24
+ TELEMETRY_SEED_PREFIX,
25
+ DEVICE_LATENCY_SAMPLES_SEED,
26
+ bytes(origin_device_pk),
27
+ bytes(target_device_pk),
28
+ bytes(link_pk),
29
+ epoch_bytes,
30
+ ],
31
+ program_id,
32
+ )
33
+
34
+
35
+ def derive_internet_latency_samples_pda(
36
+ program_id: Pubkey,
37
+ collector_oracle_pk: Pubkey,
38
+ data_provider_name: str,
39
+ origin_location_pk: Pubkey,
40
+ target_location_pk: Pubkey,
41
+ epoch: int,
42
+ ) -> tuple[Pubkey, int]:
43
+ epoch_bytes = struct.pack("<Q", epoch)
44
+ return Pubkey.find_program_address(
45
+ [
46
+ TELEMETRY_SEED_PREFIX,
47
+ INTERNET_LATENCY_SAMPLES_SEED,
48
+ bytes(collector_oracle_pk),
49
+ data_provider_name.encode("utf-8"),
50
+ bytes(origin_location_pk),
51
+ bytes(target_location_pk),
52
+ epoch_bytes,
53
+ ],
54
+ program_id,
55
+ )
telemetry/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
telemetry/state.py ADDED
@@ -0,0 +1,141 @@
1
+ """On-chain account data structures for the telemetry program.
2
+
3
+ Binary layout: 1-byte AccountType discriminator followed by Borsh-serialized
4
+ header fields, then raw u32 LE sample values (not a Borsh Vec — count is
5
+ determined by next_sample_index).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ from borsh_incremental import IncrementalReader
13
+ from solders.pubkey import Pubkey # type: ignore[import-untyped]
14
+
15
+
16
+ TELEMETRY_SEED_PREFIX = b"telemetry"
17
+ DEVICE_LATENCY_SAMPLES_SEED = b"dzlatency"
18
+ INTERNET_LATENCY_SAMPLES_SEED = b"inetlatency"
19
+
20
+ MAX_DEVICE_LATENCY_SAMPLES_PER_ACCOUNT = 35_000
21
+ MAX_INTERNET_LATENCY_SAMPLES_PER_ACCOUNT = 3_000
22
+
23
+ DEVICE_LATENCY_HEADER_SIZE = 1 + 8 + 32 * 6 + 8 + 8 + 4 + 128
24
+
25
+
26
+ def _read_pubkey(r: IncrementalReader) -> Pubkey:
27
+ return Pubkey.from_bytes(r.read_pubkey_raw())
28
+
29
+
30
+ @dataclass
31
+ class DeviceLatencySamples:
32
+ account_type: int
33
+ epoch: int
34
+ origin_device_agent_pk: Pubkey
35
+ origin_device_pk: Pubkey
36
+ target_device_pk: Pubkey
37
+ origin_device_location_pk: Pubkey
38
+ target_device_location_pk: Pubkey
39
+ link_pk: Pubkey
40
+ sampling_interval_microseconds: int
41
+ start_timestamp_microseconds: int
42
+ next_sample_index: int
43
+ samples: list[int] = field(default_factory=list)
44
+
45
+ @classmethod
46
+ def from_bytes(cls, data: bytes) -> DeviceLatencySamples:
47
+ if len(data) < DEVICE_LATENCY_HEADER_SIZE:
48
+ raise ValueError(
49
+ f"data too short for device latency header: {len(data)} < {DEVICE_LATENCY_HEADER_SIZE}"
50
+ )
51
+
52
+ r = IncrementalReader(data)
53
+
54
+ account_type = r.read_u8()
55
+ epoch = r.read_u64()
56
+ origin_device_agent_pk = _read_pubkey(r)
57
+ origin_device_pk = _read_pubkey(r)
58
+ target_device_pk = _read_pubkey(r)
59
+ origin_device_location_pk = _read_pubkey(r)
60
+ target_device_location_pk = _read_pubkey(r)
61
+ link_pk = _read_pubkey(r)
62
+ sampling_interval = r.read_u64()
63
+ start_timestamp = r.read_u64()
64
+ next_sample_index = r.read_u32()
65
+
66
+ r.read_bytes(128) # reserved
67
+
68
+ count = min(next_sample_index, MAX_DEVICE_LATENCY_SAMPLES_PER_ACCOUNT)
69
+ samples: list[int] = []
70
+ for _ in range(count):
71
+ if r.remaining < 4:
72
+ break
73
+ samples.append(r.read_u32())
74
+
75
+ return cls(
76
+ account_type=account_type,
77
+ epoch=epoch,
78
+ origin_device_agent_pk=origin_device_agent_pk,
79
+ origin_device_pk=origin_device_pk,
80
+ target_device_pk=target_device_pk,
81
+ origin_device_location_pk=origin_device_location_pk,
82
+ target_device_location_pk=target_device_location_pk,
83
+ link_pk=link_pk,
84
+ sampling_interval_microseconds=sampling_interval,
85
+ start_timestamp_microseconds=start_timestamp,
86
+ next_sample_index=next_sample_index,
87
+ samples=samples,
88
+ )
89
+
90
+
91
+ @dataclass
92
+ class InternetLatencySamples:
93
+ account_type: int
94
+ epoch: int
95
+ data_provider_name: str
96
+ oracle_agent_pk: Pubkey
97
+ origin_exchange_pk: Pubkey
98
+ target_exchange_pk: Pubkey
99
+ sampling_interval_microseconds: int
100
+ start_timestamp_microseconds: int
101
+ next_sample_index: int
102
+ samples: list[int] = field(default_factory=list)
103
+
104
+ @classmethod
105
+ def from_bytes(cls, data: bytes) -> InternetLatencySamples:
106
+ if len(data) < 10:
107
+ raise ValueError("data too short")
108
+
109
+ r = IncrementalReader(data)
110
+
111
+ account_type = r.read_u8()
112
+ epoch = r.read_u64()
113
+ data_provider_name = r.read_string()
114
+ oracle_agent_pk = _read_pubkey(r)
115
+ origin_exchange_pk = _read_pubkey(r)
116
+ target_exchange_pk = _read_pubkey(r)
117
+ sampling_interval = r.read_u64()
118
+ start_timestamp = r.read_u64()
119
+ next_sample_index = r.read_u32()
120
+
121
+ r.read_bytes(128) # reserved
122
+
123
+ count = min(next_sample_index, MAX_INTERNET_LATENCY_SAMPLES_PER_ACCOUNT)
124
+ samples: list[int] = []
125
+ for _ in range(count):
126
+ if r.remaining < 4:
127
+ break
128
+ samples.append(r.read_u32())
129
+
130
+ return cls(
131
+ account_type=account_type,
132
+ epoch=epoch,
133
+ data_provider_name=data_provider_name,
134
+ oracle_agent_pk=oracle_agent_pk,
135
+ origin_exchange_pk=origin_exchange_pk,
136
+ target_exchange_pk=target_exchange_pk,
137
+ sampling_interval_microseconds=sampling_interval,
138
+ start_timestamp_microseconds=start_timestamp,
139
+ next_sample_index=next_sample_index,
140
+ samples=samples,
141
+ )
File without changes
@@ -0,0 +1,77 @@
1
+ """Fixture-based compatibility tests."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from solders.pubkey import Pubkey # type: ignore[import-untyped]
7
+
8
+ from telemetry.state import DeviceLatencySamples, InternetLatencySamples
9
+
10
+ FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "fixtures"
11
+
12
+
13
+ def _load_fixture(name: str) -> tuple[bytes, dict]:
14
+ bin_data = (FIXTURES_DIR / f"{name}.bin").read_bytes()
15
+ meta = json.loads((FIXTURES_DIR / f"{name}.json").read_text())
16
+ return bin_data, meta
17
+
18
+
19
+ def _assert_fields(expected_fields: list[dict], got: dict) -> None:
20
+ for f in expected_fields:
21
+ name = f["name"]
22
+ if name not in got:
23
+ continue
24
+ typ = f["typ"]
25
+ raw = f["value"]
26
+ actual = got[name]
27
+ if typ in ("u8", "u16", "u32", "u64"):
28
+ assert actual == int(raw), f"{name}: expected {raw}, got {actual}"
29
+ elif typ == "pubkey":
30
+ expected = Pubkey.from_string(raw)
31
+ assert actual == expected, f"{name}: expected {expected}, got {actual}"
32
+ elif typ == "string":
33
+ assert actual == raw, f"{name}: expected {raw}, got {actual}"
34
+
35
+
36
+ class TestFixtureDeviceLatencySamples:
37
+ def test_deserialize(self):
38
+ data, meta = _load_fixture("device_latency_samples")
39
+ d = DeviceLatencySamples.from_bytes(data)
40
+ _assert_fields(
41
+ meta["fields"],
42
+ {
43
+ "AccountType": d.account_type,
44
+ "Epoch": d.epoch,
45
+ "OriginDeviceAgentPK": d.origin_device_agent_pk,
46
+ "OriginDevicePK": d.origin_device_pk,
47
+ "TargetDevicePK": d.target_device_pk,
48
+ "OriginDeviceLocationPK": d.origin_device_location_pk,
49
+ "TargetDeviceLocationPK": d.target_device_location_pk,
50
+ "LinkPK": d.link_pk,
51
+ "SamplingIntervalMicroseconds": d.sampling_interval_microseconds,
52
+ "StartTimestampMicroseconds": d.start_timestamp_microseconds,
53
+ "NextSampleIndex": d.next_sample_index,
54
+ "SamplesCount": len(d.samples),
55
+ },
56
+ )
57
+
58
+
59
+ class TestFixtureInternetLatencySamples:
60
+ def test_deserialize(self):
61
+ data, meta = _load_fixture("internet_latency_samples")
62
+ d = InternetLatencySamples.from_bytes(data)
63
+ _assert_fields(
64
+ meta["fields"],
65
+ {
66
+ "AccountType": d.account_type,
67
+ "Epoch": d.epoch,
68
+ "DataProviderName": d.data_provider_name,
69
+ "OracleAgentPK": d.oracle_agent_pk,
70
+ "OriginExchangePK": d.origin_exchange_pk,
71
+ "TargetExchangePK": d.target_exchange_pk,
72
+ "SamplingIntervalMicroseconds": d.sampling_interval_microseconds,
73
+ "StartTimestampMicroseconds": d.start_timestamp_microseconds,
74
+ "NextSampleIndex": d.next_sample_index,
75
+ "SamplesCount": len(d.samples),
76
+ },
77
+ )
@@ -0,0 +1,38 @@
1
+ """PDA derivation tests."""
2
+
3
+ from solders.pubkey import Pubkey # type: ignore[import-untyped]
4
+
5
+ from telemetry.pda import (
6
+ derive_device_latency_samples_pda,
7
+ derive_internet_latency_samples_pda,
8
+ )
9
+
10
+ PROGRAM_ID = Pubkey.from_string("tE1exJ5VMyoC9ByZeSmgtNzJCFF74G9JAv338sJiqkC")
11
+
12
+
13
+ class TestDeriveDeviceLatencySamplesPDA:
14
+ def test_deterministic(self):
15
+ origin = Pubkey.from_string("11111111111111111111111111111112")
16
+ target = Pubkey.from_string("11111111111111111111111111111113")
17
+ link = Pubkey.from_string("11111111111111111111111111111114")
18
+
19
+ addr1, bump1 = derive_device_latency_samples_pda(
20
+ PROGRAM_ID, origin, target, link, 42
21
+ )
22
+ addr2, bump2 = derive_device_latency_samples_pda(
23
+ PROGRAM_ID, origin, target, link, 42
24
+ )
25
+ assert addr1 == addr2
26
+ assert bump1 == bump2
27
+
28
+
29
+ class TestDeriveInternetLatencySamplesPDA:
30
+ def test_deterministic(self):
31
+ oracle = Pubkey.from_string("11111111111111111111111111111112")
32
+ origin = Pubkey.from_string("11111111111111111111111111111113")
33
+ target = Pubkey.from_string("11111111111111111111111111111114")
34
+
35
+ addr1, _ = derive_internet_latency_samples_pda(
36
+ PROGRAM_ID, oracle, "RIPE Atlas", origin, target, 42
37
+ )
38
+ assert addr1 != Pubkey.default()