doublezero-revenue-distribution 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.
- doublezero_revenue_distribution-0.0.2/.gitignore +7 -0
- doublezero_revenue_distribution-0.0.2/PKG-INFO +10 -0
- doublezero_revenue_distribution-0.0.2/pyproject.toml +30 -0
- doublezero_revenue_distribution-0.0.2/revdist/__init__.py +70 -0
- doublezero_revenue_distribution-0.0.2/revdist/client.py +189 -0
- doublezero_revenue_distribution-0.0.2/revdist/config.py +22 -0
- doublezero_revenue_distribution-0.0.2/revdist/discriminator.py +31 -0
- doublezero_revenue_distribution-0.0.2/revdist/oracle.py +38 -0
- doublezero_revenue_distribution-0.0.2/revdist/pda.py +85 -0
- doublezero_revenue_distribution-0.0.2/revdist/reserved.py +19 -0
- doublezero_revenue_distribution-0.0.2/revdist/rpc.py +49 -0
- doublezero_revenue_distribution-0.0.2/revdist/state.py +475 -0
- doublezero_revenue_distribution-0.0.2/revdist/tests/__init__.py +0 -0
- doublezero_revenue_distribution-0.0.2/revdist/tests/test_compat.py +251 -0
- doublezero_revenue_distribution-0.0.2/revdist/tests/test_fixtures.py +193 -0
- doublezero_revenue_distribution-0.0.2/revdist/tests/test_pda.py +43 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doublezero-revenue-distribution
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: DoubleZero Revenue Distribution SDK
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: base58>=2.1
|
|
7
|
+
Requires-Dist: borsh-incremental
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: solana>=0.35
|
|
10
|
+
Requires-Dist: solders>=0.21
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "doublezero-revenue-distribution"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "DoubleZero Revenue Distribution SDK"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"borsh-incremental",
|
|
8
|
+
"solana>=0.35",
|
|
9
|
+
"solders>=0.21",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"base58>=2.1",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.pytest.ini_options]
|
|
15
|
+
testpaths = ["revdist/tests"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["revdist"]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=9.0.2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources]
|
|
30
|
+
borsh-incremental = { path = "../../borsh-incremental/python", editable = true }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from revdist.client import Client
|
|
2
|
+
from revdist.config import (
|
|
3
|
+
LEDGER_RPC_URLS,
|
|
4
|
+
ORACLE_URLS,
|
|
5
|
+
PROGRAM_ID,
|
|
6
|
+
SOLANA_RPC_URLS,
|
|
7
|
+
)
|
|
8
|
+
from revdist.oracle import OracleClient, SwapRate
|
|
9
|
+
from revdist.rpc import new_rpc_client
|
|
10
|
+
from revdist.discriminator import (
|
|
11
|
+
DISCRIMINATOR_CONTRIBUTOR_REWARDS,
|
|
12
|
+
DISCRIMINATOR_DISTRIBUTION,
|
|
13
|
+
DISCRIMINATOR_JOURNAL,
|
|
14
|
+
DISCRIMINATOR_PROGRAM_CONFIG,
|
|
15
|
+
DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT,
|
|
16
|
+
)
|
|
17
|
+
from revdist.pda import (
|
|
18
|
+
derive_config_pda,
|
|
19
|
+
derive_contributor_rewards_pda,
|
|
20
|
+
derive_distribution_pda,
|
|
21
|
+
derive_journal_pda,
|
|
22
|
+
derive_record_key,
|
|
23
|
+
derive_reward_share_record_key,
|
|
24
|
+
derive_validator_debt_record_key,
|
|
25
|
+
derive_validator_deposit_pda,
|
|
26
|
+
)
|
|
27
|
+
from revdist.state import (
|
|
28
|
+
ComputedSolanaValidatorDebt,
|
|
29
|
+
ComputedSolanaValidatorDebts,
|
|
30
|
+
ContributorRewards,
|
|
31
|
+
Distribution,
|
|
32
|
+
Journal,
|
|
33
|
+
ProgramConfig,
|
|
34
|
+
RewardShare,
|
|
35
|
+
ShapleyOutputStorage,
|
|
36
|
+
SolanaValidatorDeposit,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Client",
|
|
41
|
+
"LEDGER_RPC_URLS",
|
|
42
|
+
"ORACLE_URLS",
|
|
43
|
+
"OracleClient",
|
|
44
|
+
"PROGRAM_ID",
|
|
45
|
+
"SOLANA_RPC_URLS",
|
|
46
|
+
"SwapRate",
|
|
47
|
+
"ComputedSolanaValidatorDebt",
|
|
48
|
+
"ComputedSolanaValidatorDebts",
|
|
49
|
+
"ContributorRewards",
|
|
50
|
+
"Distribution",
|
|
51
|
+
"Journal",
|
|
52
|
+
"ProgramConfig",
|
|
53
|
+
"RewardShare",
|
|
54
|
+
"ShapleyOutputStorage",
|
|
55
|
+
"SolanaValidatorDeposit",
|
|
56
|
+
"DISCRIMINATOR_CONTRIBUTOR_REWARDS",
|
|
57
|
+
"DISCRIMINATOR_DISTRIBUTION",
|
|
58
|
+
"DISCRIMINATOR_JOURNAL",
|
|
59
|
+
"DISCRIMINATOR_PROGRAM_CONFIG",
|
|
60
|
+
"DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT",
|
|
61
|
+
"derive_config_pda",
|
|
62
|
+
"derive_contributor_rewards_pda",
|
|
63
|
+
"derive_distribution_pda",
|
|
64
|
+
"derive_journal_pda",
|
|
65
|
+
"derive_record_key",
|
|
66
|
+
"derive_reward_share_record_key",
|
|
67
|
+
"derive_validator_debt_record_key",
|
|
68
|
+
"derive_validator_deposit_pda",
|
|
69
|
+
"new_rpc_client",
|
|
70
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""RPC client for fetching revenue distribution program accounts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from solana.rpc.api import Client as SolanaHTTPClient # type: ignore[import-untyped]
|
|
9
|
+
|
|
10
|
+
from revdist.rpc import new_rpc_client
|
|
11
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
12
|
+
from solders.rpc.responses import GetAccountInfoResp # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
from revdist.config import LEDGER_RPC_URLS, PROGRAM_ID, SOLANA_RPC_URLS
|
|
15
|
+
from revdist.discriminator import (
|
|
16
|
+
DISCRIMINATOR_CONTRIBUTOR_REWARDS,
|
|
17
|
+
DISCRIMINATOR_DISTRIBUTION,
|
|
18
|
+
DISCRIMINATOR_JOURNAL,
|
|
19
|
+
DISCRIMINATOR_PROGRAM_CONFIG,
|
|
20
|
+
DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT,
|
|
21
|
+
)
|
|
22
|
+
from revdist.pda import (
|
|
23
|
+
RECORD_HEADER_SIZE,
|
|
24
|
+
derive_config_pda,
|
|
25
|
+
derive_contributor_rewards_pda,
|
|
26
|
+
derive_distribution_pda,
|
|
27
|
+
derive_journal_pda,
|
|
28
|
+
derive_reward_share_record_key,
|
|
29
|
+
derive_validator_debt_record_key,
|
|
30
|
+
derive_validator_deposit_pda,
|
|
31
|
+
)
|
|
32
|
+
from revdist.state import (
|
|
33
|
+
ComputedSolanaValidatorDebts,
|
|
34
|
+
ContributorRewards,
|
|
35
|
+
Distribution,
|
|
36
|
+
Journal,
|
|
37
|
+
ProgramConfig,
|
|
38
|
+
ShapleyOutputStorage,
|
|
39
|
+
SolanaValidatorDeposit,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SolanaClient(Protocol):
|
|
44
|
+
def get_account_info(self, pubkey: Pubkey) -> GetAccountInfoResp: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Client:
|
|
48
|
+
"""Read-only client for revenue distribution program accounts."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
solana_rpc: SolanaClient,
|
|
53
|
+
ledger_rpc: SolanaClient,
|
|
54
|
+
program_id: Pubkey,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._solana_rpc = solana_rpc
|
|
57
|
+
self._ledger_rpc = ledger_rpc
|
|
58
|
+
self._program_id = program_id
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_env(cls, env: str) -> Client:
|
|
62
|
+
"""Create a client configured for the given environment.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
env: Environment name ("mainnet-beta", "testnet", "devnet", "localnet")
|
|
66
|
+
"""
|
|
67
|
+
return cls(
|
|
68
|
+
new_rpc_client(SOLANA_RPC_URLS[env]),
|
|
69
|
+
new_rpc_client(LEDGER_RPC_URLS[env]),
|
|
70
|
+
Pubkey.from_string(PROGRAM_ID),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def mainnet_beta(cls) -> Client:
|
|
75
|
+
"""Create a client configured for mainnet-beta."""
|
|
76
|
+
return cls.from_env("mainnet-beta")
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def testnet(cls) -> Client:
|
|
80
|
+
"""Create a client configured for testnet."""
|
|
81
|
+
return cls.from_env("testnet")
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def devnet(cls) -> Client:
|
|
85
|
+
"""Create a client configured for devnet."""
|
|
86
|
+
return cls.from_env("devnet")
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def localnet(cls) -> Client:
|
|
90
|
+
"""Create a client configured for localnet."""
|
|
91
|
+
return cls.from_env("localnet")
|
|
92
|
+
|
|
93
|
+
# -- Solana RPC (on-chain accounts) --
|
|
94
|
+
|
|
95
|
+
def fetch_config(self) -> ProgramConfig:
|
|
96
|
+
addr, _ = derive_config_pda(self._program_id)
|
|
97
|
+
data = self._fetch_solana_account_data(addr)
|
|
98
|
+
return ProgramConfig.from_bytes(data, DISCRIMINATOR_PROGRAM_CONFIG)
|
|
99
|
+
|
|
100
|
+
def fetch_distribution(self, epoch: int) -> Distribution:
|
|
101
|
+
addr, _ = derive_distribution_pda(self._program_id, epoch)
|
|
102
|
+
data = self._fetch_solana_account_data(addr)
|
|
103
|
+
return Distribution.from_bytes(data, DISCRIMINATOR_DISTRIBUTION)
|
|
104
|
+
|
|
105
|
+
def fetch_journal(self) -> Journal:
|
|
106
|
+
addr, _ = derive_journal_pda(self._program_id)
|
|
107
|
+
data = self._fetch_solana_account_data(addr)
|
|
108
|
+
return Journal.from_bytes(data, DISCRIMINATOR_JOURNAL)
|
|
109
|
+
|
|
110
|
+
def fetch_validator_deposit(
|
|
111
|
+
self, node_id: Pubkey
|
|
112
|
+
) -> SolanaValidatorDeposit:
|
|
113
|
+
addr, _ = derive_validator_deposit_pda(self._program_id, node_id)
|
|
114
|
+
data = self._fetch_solana_account_data(addr)
|
|
115
|
+
return SolanaValidatorDeposit.from_bytes(
|
|
116
|
+
data, DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def fetch_contributor_rewards(
|
|
120
|
+
self, service_key: Pubkey
|
|
121
|
+
) -> ContributorRewards:
|
|
122
|
+
addr, _ = derive_contributor_rewards_pda(self._program_id, service_key)
|
|
123
|
+
data = self._fetch_solana_account_data(addr)
|
|
124
|
+
return ContributorRewards.from_bytes(
|
|
125
|
+
data, DISCRIMINATOR_CONTRIBUTOR_REWARDS
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def fetch_all_validator_deposits(self) -> list[SolanaValidatorDeposit]:
|
|
129
|
+
return self._fetch_all_by_discriminator(
|
|
130
|
+
DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT,
|
|
131
|
+
SolanaValidatorDeposit,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def fetch_all_contributor_rewards(self) -> list[ContributorRewards]:
|
|
135
|
+
return self._fetch_all_by_discriminator(
|
|
136
|
+
DISCRIMINATOR_CONTRIBUTOR_REWARDS,
|
|
137
|
+
ContributorRewards,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# -- DZ Ledger RPC (ledger records) --
|
|
141
|
+
|
|
142
|
+
def fetch_validator_debts(
|
|
143
|
+
self, epoch: int
|
|
144
|
+
) -> ComputedSolanaValidatorDebts:
|
|
145
|
+
config = self.fetch_config()
|
|
146
|
+
addr = derive_validator_debt_record_key(config.debt_accountant_key, epoch)
|
|
147
|
+
data = self._fetch_ledger_record_data(addr)
|
|
148
|
+
return ComputedSolanaValidatorDebts.from_bytes(data[RECORD_HEADER_SIZE:])
|
|
149
|
+
|
|
150
|
+
def fetch_reward_shares(self, epoch: int) -> ShapleyOutputStorage:
|
|
151
|
+
config = self.fetch_config()
|
|
152
|
+
addr = derive_reward_share_record_key(config.rewards_accountant_key, epoch)
|
|
153
|
+
data = self._fetch_ledger_record_data(addr)
|
|
154
|
+
return ShapleyOutputStorage.from_bytes(data[RECORD_HEADER_SIZE:])
|
|
155
|
+
|
|
156
|
+
# -- Internal helpers --
|
|
157
|
+
|
|
158
|
+
def _fetch_solana_account_data(self, addr: Pubkey) -> bytes:
|
|
159
|
+
resp = self._solana_rpc.get_account_info(addr)
|
|
160
|
+
if resp.value is None:
|
|
161
|
+
raise ValueError(f"account not found: {addr}")
|
|
162
|
+
return bytes(resp.value.data)
|
|
163
|
+
|
|
164
|
+
def _fetch_ledger_record_data(self, addr: Pubkey) -> bytes:
|
|
165
|
+
resp = self._ledger_rpc.get_account_info(addr)
|
|
166
|
+
if resp.value is None:
|
|
167
|
+
raise ValueError(f"ledger record not found: {addr}")
|
|
168
|
+
return bytes(resp.value.data)
|
|
169
|
+
|
|
170
|
+
def _fetch_all_by_discriminator(
|
|
171
|
+
self,
|
|
172
|
+
disc: bytes,
|
|
173
|
+
cls: type,
|
|
174
|
+
) -> list:
|
|
175
|
+
from solana.rpc.types import MemcmpOpts # type: ignore[import-untyped]
|
|
176
|
+
|
|
177
|
+
import base58 # type: ignore[import-untyped]
|
|
178
|
+
|
|
179
|
+
filters = [MemcmpOpts(offset=0, bytes=base58.b58encode(disc).decode())]
|
|
180
|
+
resp = self._solana_rpc.get_program_accounts(
|
|
181
|
+
self._program_id,
|
|
182
|
+
encoding="base64",
|
|
183
|
+
filters=filters,
|
|
184
|
+
)
|
|
185
|
+
results = []
|
|
186
|
+
for acct in resp.value:
|
|
187
|
+
data = bytes(acct.account.data)
|
|
188
|
+
results.append(cls.from_bytes(data, disc))
|
|
189
|
+
return results
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Network configuration for the revenue distribution program."""
|
|
2
|
+
|
|
3
|
+
PROGRAM_ID = "dzrevZC94tBLwuHw1dyynZxaXTWyp7yocsinyEVPtt4"
|
|
4
|
+
|
|
5
|
+
SOLANA_RPC_URLS = {
|
|
6
|
+
"mainnet-beta": "https://api.mainnet-beta.solana.com",
|
|
7
|
+
"testnet": "https://api.testnet.solana.com",
|
|
8
|
+
"devnet": "https://api.devnet.solana.com",
|
|
9
|
+
"localnet": "http://localhost:8899",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
ORACLE_URLS = {
|
|
13
|
+
"mainnet-beta": "https://sol-2z-oracle-api-v1.mainnet-beta.doublezero.xyz",
|
|
14
|
+
"testnet": "https://sol-2z-oracle-api-v1.testnet.doublezero.xyz",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
LEDGER_RPC_URLS = {
|
|
18
|
+
"mainnet-beta": "https://doublezero-mainnet-beta.rpcpool.com/db336024-e7a8-46b1-80e5-352dd77060ab",
|
|
19
|
+
"testnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16",
|
|
20
|
+
"devnet": "https://doublezerolocalnet.rpcpool.com/8a4fd3f4-0977-449f-88c7-63d4b0f10f16",
|
|
21
|
+
"localnet": "http://localhost:8899",
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
3
|
+
DISCRIMINATOR_SIZE = 8
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _sha256_first8(s: str) -> bytes:
|
|
7
|
+
return hashlib.sha256(s.encode()).digest()[:8]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DISCRIMINATOR_PROGRAM_CONFIG = _sha256_first8("dz::account::program_config")
|
|
11
|
+
DISCRIMINATOR_DISTRIBUTION = _sha256_first8("dz::account::distribution")
|
|
12
|
+
DISCRIMINATOR_SOLANA_VALIDATOR_DEPOSIT = _sha256_first8(
|
|
13
|
+
"dz::account::solana_validator_deposit"
|
|
14
|
+
)
|
|
15
|
+
DISCRIMINATOR_CONTRIBUTOR_REWARDS = _sha256_first8(
|
|
16
|
+
"dz::account::contributor_rewards"
|
|
17
|
+
)
|
|
18
|
+
DISCRIMINATOR_JOURNAL = _sha256_first8("dz::account::journal")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_discriminator(data: bytes, expected: bytes) -> None:
|
|
22
|
+
"""Validate the 8-byte discriminator prefix. Raises ValueError on mismatch."""
|
|
23
|
+
if len(data) < DISCRIMINATOR_SIZE:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"data too short: {len(data)} bytes, need at least {DISCRIMINATOR_SIZE}"
|
|
26
|
+
)
|
|
27
|
+
got = data[:DISCRIMINATOR_SIZE]
|
|
28
|
+
if got != expected:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"invalid discriminator: got {got.hex()}, want {expected.hex()}"
|
|
31
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""SOL/2Z oracle client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SwapRate:
|
|
12
|
+
rate: float
|
|
13
|
+
timestamp: int
|
|
14
|
+
signature: str
|
|
15
|
+
sol_price_usd: str
|
|
16
|
+
twoz_price_usd: str
|
|
17
|
+
cache_hit: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OracleClient:
|
|
21
|
+
"""Fetches SOL/2Z swap rates from the oracle API."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, base_url: str) -> None:
|
|
24
|
+
self._base_url = base_url
|
|
25
|
+
self._http = httpx.Client(timeout=30)
|
|
26
|
+
|
|
27
|
+
def fetch_swap_rate(self) -> SwapRate:
|
|
28
|
+
resp = self._http.get(f"{self._base_url}/swap-rate")
|
|
29
|
+
resp.raise_for_status()
|
|
30
|
+
data = resp.json()
|
|
31
|
+
return SwapRate(
|
|
32
|
+
rate=data["swapRate"],
|
|
33
|
+
timestamp=data["timestamp"],
|
|
34
|
+
signature=data["signature"],
|
|
35
|
+
sol_price_usd=data["solPriceUsd"],
|
|
36
|
+
twoz_price_usd=data["twozPriceUsd"],
|
|
37
|
+
cache_hit=data["cacheHit"],
|
|
38
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""PDA and record key derivation for revenue distribution program accounts."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
import base58 # type: ignore[import-untyped]
|
|
7
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
SEED_PROGRAM_CONFIG = b"program_config"
|
|
10
|
+
SEED_DISTRIBUTION = b"distribution"
|
|
11
|
+
SEED_SOLANA_VALIDATOR_DEPOSIT = b"solana_validator_deposit"
|
|
12
|
+
SEED_CONTRIBUTOR_REWARDS = b"contributor_rewards"
|
|
13
|
+
SEED_JOURNAL = b"journal"
|
|
14
|
+
SEED_SOLANA_VALIDATOR_DEBT = b"solana_validator_debt"
|
|
15
|
+
SEED_DZ_CONTRIBUTOR_REWARDS = b"dz_contributor_rewards"
|
|
16
|
+
SEED_SHAPLEY_OUTPUT = b"shapley_output"
|
|
17
|
+
|
|
18
|
+
RECORD_PROGRAM_ID = Pubkey.from_string("dzrecxigtaZQ3gPmt2X5mDkYigaruFR1rHCqztFTvx7")
|
|
19
|
+
RECORD_HEADER_SIZE = 33
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def derive_config_pda(program_id: Pubkey) -> tuple[Pubkey, int]:
|
|
23
|
+
return Pubkey.find_program_address([SEED_PROGRAM_CONFIG], program_id)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def derive_distribution_pda(
|
|
27
|
+
program_id: Pubkey, epoch: int
|
|
28
|
+
) -> tuple[Pubkey, int]:
|
|
29
|
+
epoch_bytes = struct.pack("<Q", epoch)
|
|
30
|
+
return Pubkey.find_program_address(
|
|
31
|
+
[SEED_DISTRIBUTION, epoch_bytes], program_id
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def derive_journal_pda(program_id: Pubkey) -> tuple[Pubkey, int]:
|
|
36
|
+
return Pubkey.find_program_address([SEED_JOURNAL], program_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def derive_validator_deposit_pda(
|
|
40
|
+
program_id: Pubkey, node_id: Pubkey
|
|
41
|
+
) -> tuple[Pubkey, int]:
|
|
42
|
+
return Pubkey.find_program_address(
|
|
43
|
+
[SEED_SOLANA_VALIDATOR_DEPOSIT, bytes(node_id)], program_id
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def derive_contributor_rewards_pda(
|
|
48
|
+
program_id: Pubkey, service_key: Pubkey
|
|
49
|
+
) -> tuple[Pubkey, int]:
|
|
50
|
+
return Pubkey.find_program_address(
|
|
51
|
+
[SEED_CONTRIBUTOR_REWARDS, bytes(service_key)], program_id
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _create_record_seed_string(seeds: list[bytes]) -> str:
|
|
56
|
+
"""Hash seeds with SHA256, encode as base58, truncate to 32 chars."""
|
|
57
|
+
h = hashlib.sha256()
|
|
58
|
+
for s in seeds:
|
|
59
|
+
h.update(s)
|
|
60
|
+
return base58.b58encode(h.digest()).decode()[:32]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def derive_record_key(payer_key: Pubkey, seeds: list[bytes]) -> Pubkey:
|
|
64
|
+
"""Derive a ledger record address using create-with-seed."""
|
|
65
|
+
seed_str = _create_record_seed_string(seeds)
|
|
66
|
+
return Pubkey.create_with_seed(payer_key, seed_str, RECORD_PROGRAM_ID)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def derive_validator_debt_record_key(
|
|
70
|
+
debt_accountant_key: Pubkey, epoch: int
|
|
71
|
+
) -> Pubkey:
|
|
72
|
+
epoch_bytes = struct.pack("<Q", epoch)
|
|
73
|
+
return derive_record_key(
|
|
74
|
+
debt_accountant_key, [SEED_SOLANA_VALIDATOR_DEBT, epoch_bytes]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def derive_reward_share_record_key(
|
|
79
|
+
rewards_accountant_key: Pubkey, epoch: int
|
|
80
|
+
) -> Pubkey:
|
|
81
|
+
epoch_bytes = struct.pack("<Q", epoch)
|
|
82
|
+
return derive_record_key(
|
|
83
|
+
rewards_accountant_key,
|
|
84
|
+
[SEED_DZ_CONTRIBUTOR_REWARDS, epoch_bytes, SEED_SHAPLEY_OUTPUT],
|
|
85
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Wrapper for reserved / padding bytes in on-chain account layouts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Reserved(bytes):
|
|
7
|
+
"""Thin wrapper around ``bytes`` that marks padding or storage-gap fields.
|
|
8
|
+
|
|
9
|
+
Behaves exactly like ``bytes`` but makes the intent explicit when used as
|
|
10
|
+
a type annotation in dataclasses::
|
|
11
|
+
|
|
12
|
+
reserved0: Reserved # [3]u8 padding
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __new__(cls, data: bytes) -> Reserved:
|
|
16
|
+
return super().__new__(cls, data)
|
|
17
|
+
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
return f"Reserved({len(self)})"
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
from solana.rpc.providers.http import HTTPProvider # type: ignore[import-untyped]
|
|
8
|
+
|
|
9
|
+
_DEFAULT_MAX_RETRIES = 5
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _RetryTransport(httpx.BaseTransport):
|
|
13
|
+
"""HTTP transport that retries on 429 Too Many Requests."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
wrapped: httpx.BaseTransport | None = None,
|
|
18
|
+
max_retries: int = _DEFAULT_MAX_RETRIES,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._wrapped = wrapped or httpx.HTTPTransport()
|
|
21
|
+
self._max_retries = max_retries
|
|
22
|
+
|
|
23
|
+
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
|
24
|
+
for attempt in range(self._max_retries + 1):
|
|
25
|
+
response = self._wrapped.handle_request(request)
|
|
26
|
+
if response.status_code != 429 or attempt >= self._max_retries:
|
|
27
|
+
return response
|
|
28
|
+
response.close()
|
|
29
|
+
time.sleep((attempt + 1) * 2)
|
|
30
|
+
return response # unreachable, but satisfies type checker
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def new_rpc_client(
|
|
34
|
+
url: str,
|
|
35
|
+
timeout: float = 30,
|
|
36
|
+
max_retries: int = _DEFAULT_MAX_RETRIES,
|
|
37
|
+
) -> SolanaHTTPClient:
|
|
38
|
+
"""Create a Solana RPC client with automatic retry on 429 responses."""
|
|
39
|
+
client = SolanaHTTPClient(url, timeout=timeout)
|
|
40
|
+
# Replace the underlying httpx session with one using retry transport.
|
|
41
|
+
transport = _RetryTransport(
|
|
42
|
+
wrapped=httpx.HTTPTransport(),
|
|
43
|
+
max_retries=max_retries,
|
|
44
|
+
)
|
|
45
|
+
client._provider.session = httpx.Client(
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
transport=transport,
|
|
48
|
+
)
|
|
49
|
+
return client
|