doublezero-serviceability 0.0.2__py3-none-any.whl → 0.0.3__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 → doublezero_serviceability-0.0.3.dist-info}/METADATA +1 -1
- {doublezero_serviceability-0.0.2.dist-info → doublezero_serviceability-0.0.3.dist-info}/RECORD +7 -7
- serviceability/client.py +52 -0
- serviceability/state.py +44 -27
- serviceability/tests/test_compat.py +33 -0
- serviceability/tests/test_fixtures.py +2 -2
- {doublezero_serviceability-0.0.2.dist-info → doublezero_serviceability-0.0.3.dist-info}/WHEEL +0 -0
{doublezero_serviceability-0.0.2.dist-info → doublezero_serviceability-0.0.3.dist-info}/RECORD
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
serviceability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
serviceability/client.py,sha256=
|
|
2
|
+
serviceability/client.py,sha256=ZMIh2l1izE--Gb5C5HmIfLqwBpwCYwoJxyw-smE1-_w,4545
|
|
3
3
|
serviceability/config.py,sha256=F04WS56QdiS1Uj_QnkkIsHaLi-RLLNS9BTxao-Gt5yo,690
|
|
4
4
|
serviceability/pda.py,sha256=wWEruxwSAiVCzrZXdDd_hfR3udTMGZQgqxcp99_tHaY,739
|
|
5
5
|
serviceability/rpc.py,sha256=G7GPRl0DM3x3p6B3NTdIgjDJTljwCat9-C_XKL5wXwM,1548
|
|
6
|
-
serviceability/state.py,sha256=
|
|
6
|
+
serviceability/state.py,sha256=ySQ4ao_J28hHYTwq_dBa4uc6P-cymKPmNQrw6X9RGAo,24489
|
|
7
7
|
serviceability/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
serviceability/tests/test_compat.py,sha256=
|
|
8
|
+
serviceability/tests/test_compat.py,sha256=LubP9He3qPJDGm2jgX0if7IK6QxJrGCARI6ReJ0pXAA,6321
|
|
9
9
|
serviceability/tests/test_enum_strings.py,sha256=hL-WoPRg2xa32UdxOq3GAyiyoEqQc2HyrC7winDoXgk,3448
|
|
10
|
-
serviceability/tests/test_fixtures.py,sha256=
|
|
10
|
+
serviceability/tests/test_fixtures.py,sha256=z7LnBiIFbCSTxHCd5qLiWfHz4wmfQSC8otHgYaDHLB8,11132
|
|
11
11
|
serviceability/tests/test_pda.py,sha256=9uOGU821Y98J6JdvZ6AiESvN9leYDT8K8vNmCqqfCGs,998
|
|
12
|
-
doublezero_serviceability-0.0.
|
|
13
|
-
doublezero_serviceability-0.0.
|
|
14
|
-
doublezero_serviceability-0.0.
|
|
12
|
+
doublezero_serviceability-0.0.3.dist-info/METADATA,sha256=mkI0LHGaGSrL-bkjo6ZIUMDmMg2jrAL-fo6A_8uxi_c,249
|
|
13
|
+
doublezero_serviceability-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
doublezero_serviceability-0.0.3.dist-info/RECORD,,
|
serviceability/client.py
CHANGED
|
@@ -83,3 +83,55 @@ class Client:
|
|
|
83
83
|
@classmethod
|
|
84
84
|
def localnet(cls) -> Client:
|
|
85
85
|
return cls.from_env("localnet")
|
|
86
|
+
|
|
87
|
+
def get_program_data(self) -> ProgramData:
|
|
88
|
+
"""Fetch all program accounts and deserialize them by type."""
|
|
89
|
+
from solana.rpc.types import MemcmpOpts # type: ignore[import-untyped]
|
|
90
|
+
from serviceability.state import AccountTypeEnum
|
|
91
|
+
|
|
92
|
+
resp = self._solana_rpc.get_program_accounts(
|
|
93
|
+
self._program_id,
|
|
94
|
+
encoding="base64",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
pd = ProgramData()
|
|
98
|
+
for acct in resp.value:
|
|
99
|
+
data = bytes(acct.account.data)
|
|
100
|
+
if len(data) == 0:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
account_type = data[0]
|
|
104
|
+
pubkey = acct.pubkey
|
|
105
|
+
|
|
106
|
+
if account_type == AccountTypeEnum.GLOBAL_STATE:
|
|
107
|
+
pd.global_state = GlobalState.from_bytes(data)
|
|
108
|
+
elif account_type == AccountTypeEnum.GLOBAL_CONFIG:
|
|
109
|
+
pd.global_config = GlobalConfig.from_bytes(data)
|
|
110
|
+
elif account_type == AccountTypeEnum.LOCATION:
|
|
111
|
+
loc = Location.from_bytes(data)
|
|
112
|
+
pd.locations.append(loc)
|
|
113
|
+
elif account_type == AccountTypeEnum.EXCHANGE:
|
|
114
|
+
ex = Exchange.from_bytes(data)
|
|
115
|
+
pd.exchanges.append(ex)
|
|
116
|
+
elif account_type == AccountTypeEnum.DEVICE:
|
|
117
|
+
dev = Device.from_bytes(data)
|
|
118
|
+
pd.devices.append(dev)
|
|
119
|
+
elif account_type == AccountTypeEnum.LINK:
|
|
120
|
+
lk = Link.from_bytes(data)
|
|
121
|
+
pd.links.append(lk)
|
|
122
|
+
elif account_type == AccountTypeEnum.USER:
|
|
123
|
+
user = User.from_bytes(data)
|
|
124
|
+
pd.users.append(user)
|
|
125
|
+
elif account_type == AccountTypeEnum.MULTICAST_GROUP:
|
|
126
|
+
mg = MulticastGroup.from_bytes(data)
|
|
127
|
+
pd.multicast_groups.append(mg)
|
|
128
|
+
elif account_type == AccountTypeEnum.PROGRAM_CONFIG:
|
|
129
|
+
pd.program_config = ProgramConfig.from_bytes(data)
|
|
130
|
+
elif account_type == AccountTypeEnum.CONTRIBUTOR:
|
|
131
|
+
contrib = Contributor.from_bytes(data)
|
|
132
|
+
pd.contributors.append(contrib)
|
|
133
|
+
elif account_type == AccountTypeEnum.ACCESS_PASS:
|
|
134
|
+
ap = AccessPass.from_bytes(data)
|
|
135
|
+
pd.access_passes.append(ap)
|
|
136
|
+
|
|
137
|
+
return pd
|
serviceability/state.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""On-chain account data structures for the serviceability program.
|
|
2
2
|
|
|
3
3
|
Binary layout uses Borsh serialization with a 1-byte AccountType discriminator
|
|
4
|
-
as the first byte. Deserialization uses cursor-based
|
|
5
|
-
borsh_incremental.
|
|
4
|
+
as the first byte. Deserialization uses cursor-based DefensiveReader from
|
|
5
|
+
borsh_incremental which returns defaults on missing data.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -10,24 +10,19 @@ from __future__ import annotations
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from enum import IntEnum
|
|
12
12
|
|
|
13
|
-
from borsh_incremental import
|
|
13
|
+
from borsh_incremental import DefensiveReader
|
|
14
14
|
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def _read_pubkey(r:
|
|
17
|
+
def _read_pubkey(r: DefensiveReader) -> Pubkey:
|
|
18
18
|
return Pubkey.from_bytes(r.read_pubkey_raw())
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def _read_pubkey_vec(r:
|
|
21
|
+
def _read_pubkey_vec(r: DefensiveReader) -> list[Pubkey]:
|
|
22
22
|
raw = r.read_pubkey_raw_vec()
|
|
23
23
|
return [Pubkey.from_bytes(b) for b in raw]
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def _try_read_pubkey_vec(r: IncrementalReader) -> list[Pubkey]:
|
|
27
|
-
raw = r.try_read_pubkey_raw_vec([])
|
|
28
|
-
return [Pubkey.from_bytes(b) for b in raw]
|
|
29
|
-
|
|
30
|
-
|
|
31
26
|
# ---------------------------------------------------------------------------
|
|
32
27
|
# Account type discriminants
|
|
33
28
|
# ---------------------------------------------------------------------------
|
|
@@ -361,9 +356,20 @@ class MulticastGroupStatus(IntEnum):
|
|
|
361
356
|
class AccessPassTypeTag(IntEnum):
|
|
362
357
|
PREPAID = 0
|
|
363
358
|
SOLANA_VALIDATOR = 1
|
|
359
|
+
SOLANA_RPC = 2
|
|
360
|
+
SOLANA_MULTICAST_PUBLISHER = 3
|
|
361
|
+
SOLANA_MULTICAST_SUBSCRIBER = 4
|
|
362
|
+
OTHERS = 5
|
|
364
363
|
|
|
365
364
|
def __str__(self) -> str:
|
|
366
|
-
_names = {
|
|
365
|
+
_names = {
|
|
366
|
+
0: "prepaid",
|
|
367
|
+
1: "solana_validator",
|
|
368
|
+
2: "solana_rpc",
|
|
369
|
+
3: "solana_multicast_publisher",
|
|
370
|
+
4: "solana_multicast_subscriber",
|
|
371
|
+
5: "others",
|
|
372
|
+
}
|
|
367
373
|
return _names.get(self.value, "unknown")
|
|
368
374
|
|
|
369
375
|
|
|
@@ -451,7 +457,7 @@ class GlobalState:
|
|
|
451
457
|
|
|
452
458
|
@classmethod
|
|
453
459
|
def from_bytes(cls, data: bytes) -> GlobalState:
|
|
454
|
-
r =
|
|
460
|
+
r = DefensiveReader(data)
|
|
455
461
|
gs = cls()
|
|
456
462
|
gs.account_type = r.read_u8()
|
|
457
463
|
gs.bump_seed = r.read_u8()
|
|
@@ -464,7 +470,7 @@ class GlobalState:
|
|
|
464
470
|
gs.contributor_airdrop_lamports = r.read_u64()
|
|
465
471
|
gs.user_airdrop_lamports = r.read_u64()
|
|
466
472
|
gs.health_oracle_pk = _read_pubkey(r)
|
|
467
|
-
gs.qa_allowlist =
|
|
473
|
+
gs.qa_allowlist = _read_pubkey_vec(r)
|
|
468
474
|
return gs
|
|
469
475
|
|
|
470
476
|
|
|
@@ -482,7 +488,7 @@ class GlobalConfig:
|
|
|
482
488
|
|
|
483
489
|
@classmethod
|
|
484
490
|
def from_bytes(cls, data: bytes) -> GlobalConfig:
|
|
485
|
-
r =
|
|
491
|
+
r = DefensiveReader(data)
|
|
486
492
|
gc = cls()
|
|
487
493
|
gc.account_type = r.read_u8()
|
|
488
494
|
gc.owner = _read_pubkey(r)
|
|
@@ -513,7 +519,7 @@ class Location:
|
|
|
513
519
|
|
|
514
520
|
@classmethod
|
|
515
521
|
def from_bytes(cls, data: bytes) -> Location:
|
|
516
|
-
r =
|
|
522
|
+
r = DefensiveReader(data)
|
|
517
523
|
loc = cls()
|
|
518
524
|
loc.account_type = r.read_u8()
|
|
519
525
|
loc.owner = _read_pubkey(r)
|
|
@@ -548,7 +554,7 @@ class Exchange:
|
|
|
548
554
|
|
|
549
555
|
@classmethod
|
|
550
556
|
def from_bytes(cls, data: bytes) -> Exchange:
|
|
551
|
-
r =
|
|
557
|
+
r = DefensiveReader(data)
|
|
552
558
|
ex = cls()
|
|
553
559
|
ex.account_type = r.read_u8()
|
|
554
560
|
ex.owner = _read_pubkey(r)
|
|
@@ -592,7 +598,7 @@ class Device:
|
|
|
592
598
|
|
|
593
599
|
@classmethod
|
|
594
600
|
def from_bytes(cls, data: bytes) -> Device:
|
|
595
|
-
r =
|
|
601
|
+
r = DefensiveReader(data)
|
|
596
602
|
dev = cls()
|
|
597
603
|
dev.account_type = r.read_u8()
|
|
598
604
|
dev.owner = _read_pubkey(r)
|
|
@@ -644,7 +650,7 @@ class Link:
|
|
|
644
650
|
|
|
645
651
|
@classmethod
|
|
646
652
|
def from_bytes(cls, data: bytes) -> Link:
|
|
647
|
-
r =
|
|
653
|
+
r = DefensiveReader(data)
|
|
648
654
|
lk = cls()
|
|
649
655
|
lk.account_type = r.read_u8()
|
|
650
656
|
lk.owner = _read_pubkey(r)
|
|
@@ -691,7 +697,7 @@ class User:
|
|
|
691
697
|
|
|
692
698
|
@classmethod
|
|
693
699
|
def from_bytes(cls, data: bytes) -> User:
|
|
694
|
-
r =
|
|
700
|
+
r = DefensiveReader(data)
|
|
695
701
|
u = cls()
|
|
696
702
|
u.account_type = r.read_u8()
|
|
697
703
|
u.owner = _read_pubkey(r)
|
|
@@ -728,7 +734,7 @@ class MulticastGroup:
|
|
|
728
734
|
|
|
729
735
|
@classmethod
|
|
730
736
|
def from_bytes(cls, data: bytes) -> MulticastGroup:
|
|
731
|
-
r =
|
|
737
|
+
r = DefensiveReader(data)
|
|
732
738
|
mg = cls()
|
|
733
739
|
mg.account_type = r.read_u8()
|
|
734
740
|
mg.owner = _read_pubkey(r)
|
|
@@ -760,7 +766,7 @@ class ProgramConfig:
|
|
|
760
766
|
|
|
761
767
|
@classmethod
|
|
762
768
|
def from_bytes(cls, data: bytes) -> ProgramConfig:
|
|
763
|
-
r =
|
|
769
|
+
r = DefensiveReader(data)
|
|
764
770
|
pc = cls()
|
|
765
771
|
pc.account_type = r.read_u8()
|
|
766
772
|
pc.bump_seed = r.read_u8()
|
|
@@ -782,7 +788,7 @@ class Contributor:
|
|
|
782
788
|
|
|
783
789
|
@classmethod
|
|
784
790
|
def from_bytes(cls, data: bytes) -> Contributor:
|
|
785
|
-
r =
|
|
791
|
+
r = DefensiveReader(data)
|
|
786
792
|
c = cls()
|
|
787
793
|
c.account_type = r.read_u8()
|
|
788
794
|
c.owner = _read_pubkey(r)
|
|
@@ -801,7 +807,9 @@ class AccessPass:
|
|
|
801
807
|
owner: Pubkey = Pubkey.default()
|
|
802
808
|
bump_seed: int = 0
|
|
803
809
|
access_pass_type_tag: AccessPassTypeTag = AccessPassTypeTag.PREPAID
|
|
804
|
-
|
|
810
|
+
associated_pubkey: Pubkey | None = None # for SolanaValidator, SolanaRPC, SolanaMulticast*
|
|
811
|
+
others_type_name: str = "" # for Others variant
|
|
812
|
+
others_key: str = "" # for Others variant
|
|
805
813
|
client_ip: bytes = b"\x00" * 4
|
|
806
814
|
user_payer: Pubkey = Pubkey.default()
|
|
807
815
|
last_access_epoch: int = 0
|
|
@@ -813,14 +821,23 @@ class AccessPass:
|
|
|
813
821
|
|
|
814
822
|
@classmethod
|
|
815
823
|
def from_bytes(cls, data: bytes) -> AccessPass:
|
|
816
|
-
r =
|
|
824
|
+
r = DefensiveReader(data)
|
|
817
825
|
ap = cls()
|
|
818
826
|
ap.account_type = r.read_u8()
|
|
819
827
|
ap.owner = _read_pubkey(r)
|
|
820
828
|
ap.bump_seed = r.read_u8()
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
ap.
|
|
829
|
+
tag = r.read_u8()
|
|
830
|
+
try:
|
|
831
|
+
ap.access_pass_type_tag = AccessPassTypeTag(tag)
|
|
832
|
+
except ValueError:
|
|
833
|
+
ap.access_pass_type_tag = AccessPassTypeTag.PREPAID
|
|
834
|
+
# Variants 1-4 have an associated pubkey
|
|
835
|
+
if tag in (1, 2, 3, 4):
|
|
836
|
+
ap.associated_pubkey = _read_pubkey(r)
|
|
837
|
+
# Variant 5 (Others) has two strings
|
|
838
|
+
elif tag == 5:
|
|
839
|
+
ap.others_type_name = r.read_string()
|
|
840
|
+
ap.others_key = r.read_string()
|
|
824
841
|
ap.client_ip = r.read_ipv4()
|
|
825
842
|
ap.user_payer = _read_pubkey(r)
|
|
826
843
|
ap.last_access_epoch = r.read_u64()
|
|
@@ -143,3 +143,36 @@ class TestCompatGlobalState:
|
|
|
143
143
|
assert gs.activator_authority_pk != Pubkey.default(), "ActivatorAuthorityPK is zero"
|
|
144
144
|
assert gs.sentinel_authority_pk != Pubkey.default(), "SentinelAuthorityPK is zero"
|
|
145
145
|
# health_oracle_pk may be zero on mainnet
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestCompatGetProgramData:
|
|
149
|
+
"""Test fetching and deserializing all program accounts.
|
|
150
|
+
|
|
151
|
+
This is the most comprehensive compat test - it fetches every account
|
|
152
|
+
owned by the program and deserializes them all, including AccessPass
|
|
153
|
+
accounts which may have various enum variants and trailing fields.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def test_deserialize_all_accounts(self) -> None:
|
|
157
|
+
skip_unless_compat()
|
|
158
|
+
|
|
159
|
+
from serviceability.client import Client
|
|
160
|
+
|
|
161
|
+
client = Client.mainnet_beta()
|
|
162
|
+
pd = client.get_program_data()
|
|
163
|
+
|
|
164
|
+
assert pd.global_state is not None, "GlobalState is None"
|
|
165
|
+
assert pd.global_config is not None, "GlobalConfig is None"
|
|
166
|
+
assert pd.program_config is not None, "ProgramConfig is None"
|
|
167
|
+
assert len(pd.locations) > 0, "no locations found on mainnet"
|
|
168
|
+
assert len(pd.exchanges) > 0, "no exchanges found on mainnet"
|
|
169
|
+
assert len(pd.devices) > 0, "no devices found on mainnet"
|
|
170
|
+
assert len(pd.links) > 0, "no links found on mainnet"
|
|
171
|
+
assert len(pd.contributors) > 0, "no contributors found on mainnet"
|
|
172
|
+
|
|
173
|
+
# Log summary for debugging.
|
|
174
|
+
print(
|
|
175
|
+
f"\nProgramData: {len(pd.locations)} locations, {len(pd.exchanges)} exchanges, "
|
|
176
|
+
f"{len(pd.devices)} devices, {len(pd.links)} links, {len(pd.users)} users, "
|
|
177
|
+
f"{len(pd.contributors)} contributors, {len(pd.access_passes)} access passes"
|
|
178
|
+
)
|
|
@@ -304,7 +304,7 @@ class TestFixtureAccessPassValidator:
|
|
|
304
304
|
"Owner": ap.owner,
|
|
305
305
|
"BumpSeed": ap.bump_seed,
|
|
306
306
|
"AccessPassType": ap.access_pass_type_tag,
|
|
307
|
-
"AccessPassTypeValidatorPubkey": ap.
|
|
307
|
+
"AccessPassTypeValidatorPubkey": ap.associated_pubkey,
|
|
308
308
|
"ClientIp": ap.client_ip,
|
|
309
309
|
"UserPayer": ap.user_payer,
|
|
310
310
|
"LastAccessEpoch": ap.last_access_epoch,
|
|
@@ -316,7 +316,7 @@ class TestFixtureAccessPassValidator:
|
|
|
316
316
|
assert ap.account_type == 11
|
|
317
317
|
assert ap.bump_seed == 243
|
|
318
318
|
assert ap.access_pass_type_tag == 1
|
|
319
|
-
assert ap.
|
|
319
|
+
assert ap.associated_pubkey == Pubkey.from_string(
|
|
320
320
|
"BuP3jEYfnTCfB4UqQk9L37k2vaXsNuVsbWxrYbGDmL6s"
|
|
321
321
|
)
|
|
322
322
|
import ipaddress
|
{doublezero_serviceability-0.0.2.dist-info → doublezero_serviceability-0.0.3.dist-info}/WHEEL
RENAMED
|
File without changes
|