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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doublezero-serviceability
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: DoubleZero Serviceability SDK
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: borsh-incremental
@@ -1,14 +1,14 @@
1
1
  serviceability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- serviceability/client.py,sha256=3FYGJqIgkRm-Kc7DrckXVPreIZQP_ozn9lt8mUDnNQY,2335
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=ekUD4rprfo4ipmBNWgUNBsCrjFJeFH0_wIKUU0-OeKA,23905
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=H2N1iTJWxovhIIy1nJIPpTFhL3eiNNcoiT1DCPZExtA,4903
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=9Tit9fyyADbE4KdPg5_IMv9VPk39Ge4obGGYLuW209Q,11132
10
+ serviceability/tests/test_fixtures.py,sha256=z7LnBiIFbCSTxHCd5qLiWfHz4wmfQSC8otHgYaDHLB8,11132
11
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,,
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 IncrementalReader from
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 IncrementalReader
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: IncrementalReader) -> Pubkey:
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: IncrementalReader) -> list[Pubkey]:
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 = {0: "prepaid", 1: "solana_validator"}
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 = IncrementalReader(data)
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 = _try_read_pubkey_vec(r)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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 = IncrementalReader(data)
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
- validator_pub_key: Pubkey | None = None
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 = IncrementalReader(data)
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
- ap.access_pass_type_tag = AccessPassTypeTag(r.read_u8())
822
- if ap.access_pass_type_tag == AccessPassTypeTag.SOLANA_VALIDATOR:
823
- ap.validator_pub_key = _read_pubkey(r)
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.validator_pub_key,
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.validator_pub_key == Pubkey.from_string(
319
+ assert ap.associated_pubkey == Pubkey.from_string(
320
320
  "BuP3jEYfnTCfB4UqQk9L37k2vaXsNuVsbWxrYbGDmL6s"
321
321
  )
322
322
  import ipaddress