Skip to content

Commit 8d152f3

Browse files
committed
feat: add publisher program to sync cli
1 parent 11e142d commit 8d152f3

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

program_admin/__init__.py

+82
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
from program_admin import instructions as pyth_program
1717
from program_admin.keys import load_keypair
1818
from program_admin.parsing import parse_account
19+
from program_admin.publisher_program_instructions import (
20+
config_account_pubkey as publisher_program_config_account_pubkey,
21+
create_buffer_account,
22+
initialize_publisher_config,
23+
initialize_publisher_program,
24+
publisher_config_account_pubkey,
25+
)
1926
from program_admin.types import (
2027
Network,
2128
PythAuthorityPermissionAccount,
@@ -56,6 +63,7 @@ class ProgramAdmin:
5663
rpc_endpoint: str
5764
key_dir: Path
5865
program_key: PublicKey
66+
publisher_program_key: Optional[PublicKey]
5967
authority_permission_account: Optional[PythAuthorityPermissionAccount]
6068
_mapping_accounts: Dict[PublicKey, PythMappingAccount]
6169
_product_accounts: Dict[PublicKey, PythProductAccount]
@@ -66,13 +74,17 @@ def __init__(
6674
network: Network,
6775
key_dir: str,
6876
program_key: str,
77+
publisher_program_key: Optional[str],
6978
commitment: Literal["confirmed", "finalized"],
7079
rpc_endpoint: str = "",
7180
):
7281
self.network = network
7382
self.rpc_endpoint = rpc_endpoint or RPC_ENDPOINTS[network]
7483
self.key_dir = Path(key_dir)
7584
self.program_key = PublicKey(program_key)
85+
self.publisher_program_key = (
86+
PublicKey(publisher_program_key) if publisher_program_key else None
87+
)
7688
self.commitment = Commitment(commitment)
7789
self.authority_permission_account = None
7890
self._mapping_accounts: Dict[PublicKey, PythMappingAccount] = {}
@@ -100,6 +112,12 @@ async def fetch_minimum_balance(self, size: int) -> int:
100112
async with AsyncClient(self.rpc_endpoint) as client:
101113
return (await client.get_minimum_balance_for_rent_exemption(size)).value
102114

115+
async def account_exists(self, key: PublicKey) -> bool:
116+
async with AsyncClient(self.rpc_endpoint) as client:
117+
response = await client.get_account_info(key)
118+
# The RPC returns null if the account does not exist
119+
return bool(response.value)
120+
103121
async def refresh_program_accounts(self):
104122
async with AsyncClient(self.rpc_endpoint) as client:
105123
logger.info("Refreshing program accounts")
@@ -301,6 +319,19 @@ async def sync(
301319
if product_updates:
302320
await self.refresh_program_accounts()
303321

322+
# Sync publisher program
323+
(
324+
publisher_program_instructions,
325+
publisher_program_signers,
326+
) = await self.sync_publisher_program(ref_publishers)
327+
328+
if publisher_program_instructions:
329+
instructions.extend(publisher_program_instructions)
330+
if send_transactions:
331+
await self.send_transaction(
332+
publisher_program_instructions, publisher_program_signers
333+
)
334+
304335
# Sync publishers
305336

306337
publisher_transactions = []
@@ -658,3 +689,54 @@ async def resize_price_accounts_v2(
658689

659690
if send_transactions:
660691
await self.send_transaction(instructions, signers)
692+
693+
async def sync_publisher_program(
694+
self, ref_publishers: ReferencePublishers
695+
) -> Tuple[List[TransactionInstruction], List[Keypair]]:
696+
if self.publisher_program_key is None:
697+
return [], []
698+
699+
instructions = []
700+
701+
authority = load_keypair("funding", key_dir=self.key_dir)
702+
703+
publisher_program_config = publisher_program_config_account_pubkey(
704+
self.publisher_program_key
705+
)
706+
707+
# Initialize the publisher program config if it does not exist
708+
if not (await self.account_exists(publisher_program_config)):
709+
initialize_publisher_program_instruction = initialize_publisher_program(
710+
self.publisher_program_key, authority.public_key
711+
)
712+
instructions.append(initialize_publisher_program_instruction)
713+
714+
# Initialize publisher config accounts for new publishers
715+
for publisher in ref_publishers["keys"].values():
716+
publisher_config_account = publisher_config_account_pubkey(
717+
publisher, self.publisher_program_key
718+
)
719+
720+
if not (await self.account_exists(publisher_config_account)):
721+
size = 100048 # This size is for a buffer supporting 5000 price updates
722+
lamports = await self.fetch_minimum_balance(size)
723+
buffer_account, create_buffer_instruction = create_buffer_account(
724+
self.publisher_program_key,
725+
authority.public_key,
726+
publisher,
727+
size,
728+
lamports,
729+
)
730+
731+
initialize_publisher_config_instruction = initialize_publisher_config(
732+
self.publisher_program_key,
733+
publisher,
734+
authority.public_key,
735+
buffer_account,
736+
)
737+
738+
instructions.extend(
739+
[create_buffer_instruction, initialize_publisher_config_instruction]
740+
)
741+
742+
return (instructions, [authority])

program_admin/cli.py

+14
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def delete_price(network, rpc_endpoint, program_key, keys, commitment, product,
5656
rpc_endpoint=rpc_endpoint,
5757
key_dir=keys,
5858
program_key=program_key,
59+
publisher_program_key=None,
5960
commitment=commitment,
6061
)
6162
funding_keypair = load_keypair("funding", key_dir=keys)
@@ -236,6 +237,7 @@ def delete_product(
236237
rpc_endpoint=rpc_endpoint,
237238
key_dir=keys,
238239
program_key=program_key,
240+
publisher_program_key=None,
239241
commitment=commitment,
240242
)
241243
funding_keypair = load_keypair("funding", key_dir=keys)
@@ -275,6 +277,7 @@ def list_accounts(network, rpc_endpoint, program_key, keys, publishers, commitme
275277
rpc_endpoint=rpc_endpoint,
276278
key_dir=keys,
277279
program_key=program_key,
280+
publisher_program_key=None,
278281
commitment=commitment,
279282
)
280283

@@ -333,6 +336,7 @@ def restore_links(network, rpc_endpoint, program_key, keys, products, commitment
333336
rpc_endpoint=rpc_endpoint,
334337
key_dir=keys,
335338
program_key=program_key,
339+
publisher_program_key=None,
336340
commitment=commitment,
337341
)
338342
reference_products = parse_products_json(Path(products))
@@ -382,6 +386,12 @@ def restore_links(network, rpc_endpoint, program_key, keys, products, commitment
382386
@click.option("--network", help="Solana network", envvar="NETWORK")
383387
@click.option("--rpc-endpoint", help="Solana RPC endpoint", envvar="RPC_ENDPOINT")
384388
@click.option("--program-key", help="Pyth program key", envvar="PROGRAM_KEY")
389+
@click.option(
390+
"--publisher-program-key",
391+
help="Publisher program key",
392+
envvar="PUBLISHER_PROGRAM_KEY",
393+
default=None,
394+
)
385395
@click.option("--keys", help="Path to keys directory", envvar="KEYS")
386396
@click.option("--products", help="Path to reference products file", envvar="PRODUCTS")
387397
@click.option(
@@ -426,6 +436,7 @@ def sync(
426436
network,
427437
rpc_endpoint,
428438
program_key,
439+
publisher_program_key,
429440
keys,
430441
products,
431442
publishers,
@@ -442,6 +453,7 @@ def sync(
442453
rpc_endpoint=rpc_endpoint,
443454
key_dir=keys,
444455
program_key=program_key,
456+
publisher_program_key=publisher_program_key,
445457
commitment=commitment,
446458
)
447459

@@ -495,6 +507,7 @@ def migrate_upgrade_authority(
495507
rpc_endpoint=rpc_endpoint,
496508
key_dir=keys,
497509
program_key=program_key,
510+
publisher_program_key=None,
498511
commitment=commitment,
499512
)
500513
funding_keypair = load_keypair("funding", key_dir=keys)
@@ -544,6 +557,7 @@ def resize_price_accounts_v2(
544557
rpc_endpoint=rpc_endpoint,
545558
key_dir=keys,
546559
program_key=program_key,
560+
publisher_program_key=None,
547561
commitment=commitment,
548562
)
549563

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
from typing import Tuple
2+
from construct import Bytes, Int8ul, Struct
3+
from solana import system_program
4+
from solana.publickey import PublicKey
5+
from solana.system_program import SYS_PROGRAM_ID, CreateAccountWithSeedParams
6+
from solana.transaction import AccountMeta, TransactionInstruction
7+
8+
9+
def config_account_pubkey(program_key: PublicKey) -> PublicKey:
10+
[config_account, _] = PublicKey.find_program_address(
11+
[b"CONFIG"],
12+
program_key,
13+
)
14+
return config_account
15+
16+
17+
def publisher_config_account_pubkey(
18+
publisher_key: PublicKey, program_key: PublicKey
19+
) -> PublicKey:
20+
[publisher_config_account, _] = PublicKey.find_program_address(
21+
[b"PUBLISHER_CONFIG", bytes(publisher_key)],
22+
program_key,
23+
)
24+
return publisher_config_account
25+
26+
27+
def initialize_publisher_program(
28+
program_key: PublicKey,
29+
authority: PublicKey,
30+
) -> TransactionInstruction:
31+
"""
32+
Pyth publisher program initialize instruction with the given authority
33+
34+
accounts:
35+
- payer account (signer, writable) - we pass the authority as the payer
36+
- config account (writable)
37+
- system program
38+
"""
39+
40+
[config_account, bump] = PublicKey.find_program_address(
41+
[b"CONFIG"],
42+
program_key,
43+
)
44+
45+
ix_data_layout = Struct(
46+
"bump" / Int8ul,
47+
"authority" / Bytes(32),
48+
)
49+
50+
ix_data = ix_data_layout.build(
51+
dict(
52+
bump=bump,
53+
authority=bytes(authority),
54+
)
55+
)
56+
57+
return TransactionInstruction(
58+
data=ix_data,
59+
keys=[
60+
AccountMeta(pubkey=authority, is_signer=True, is_writable=True),
61+
AccountMeta(pubkey=config_account, is_signer=False, is_writable=True),
62+
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
63+
],
64+
program_id=program_key,
65+
)
66+
67+
68+
def create_buffer_account(
69+
program_key: PublicKey,
70+
base_pubkey: PublicKey,
71+
publisher_pubkey: PublicKey,
72+
space: int,
73+
lamports: int,
74+
) -> Tuple[PublicKey, TransactionInstruction]:
75+
76+
seed = str(publisher_pubkey)
77+
new_account_pubkey = PublicKey.create_with_seed(
78+
base_pubkey,
79+
seed,
80+
program_key,
81+
)
82+
83+
# space = 100048 # Required space to store 5000 price updates
84+
# lamport =
85+
86+
return (
87+
new_account_pubkey,
88+
system_program.create_account_with_seed(
89+
CreateAccountWithSeedParams(
90+
from_pubkey=base_pubkey,
91+
new_account_pubkey=new_account_pubkey,
92+
base_pubkey=base_pubkey,
93+
seed=seed,
94+
program_id=program_key,
95+
lamports=lamports,
96+
space=space,
97+
)
98+
),
99+
)
100+
101+
102+
def initialize_publisher_config(
103+
program_key: PublicKey,
104+
publisher_key: PublicKey,
105+
authority: PublicKey,
106+
buffer_account: PublicKey,
107+
) -> TransactionInstruction:
108+
"""
109+
Pyth publisher program initialize publisher config instruction with the given authority
110+
111+
accounts:
112+
- authority account (signer, writable)
113+
- config account
114+
- publisher config account (writable)
115+
- buffer account (writable)
116+
- system program
117+
"""
118+
119+
[config_account, config_bump] = PublicKey.find_program_address(
120+
[b"CONFIG"],
121+
program_key,
122+
)
123+
124+
[publisher_config_account, publisher_config_bump] = PublicKey.find_program_address(
125+
[b"PUBLISHER_CONFIG", bytes(publisher_key)],
126+
program_key,
127+
)
128+
129+
ix_data_layout = Struct(
130+
"config_bump" / Int8ul,
131+
"publisher_config_bump" / Int8ul,
132+
"publisher" / Bytes(32),
133+
)
134+
135+
ix_data = ix_data_layout.build(
136+
dict(
137+
config_bump=config_bump,
138+
publisher_config_bump=publisher_config_bump,
139+
publisher=bytes(publisher_key),
140+
)
141+
)
142+
143+
return TransactionInstruction(
144+
data=ix_data,
145+
keys=[
146+
AccountMeta(pubkey=authority, is_signer=True, is_writable=True),
147+
AccountMeta(pubkey=config_account, is_signer=False, is_writable=True),
148+
AccountMeta(
149+
pubkey=publisher_config_account, is_signer=False, is_writable=True
150+
),
151+
AccountMeta(pubkey=buffer_account, is_signer=False, is_writable=True),
152+
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
153+
],
154+
program_id=program_key,
155+
)

0 commit comments

Comments
 (0)