Skip to content

Commit eb23897

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

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

program_admin/__init__.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
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+
)
22+
from program_admin.publisher_program_instructions import (
23+
create_buffer_account,
24+
initialize_publisher_config,
25+
initialize_publisher_program,
26+
publisher_config_account_pubkey,
27+
)
1928
from program_admin.types import (
2029
Network,
2130
PythAuthorityPermissionAccount,
@@ -56,6 +65,7 @@ class ProgramAdmin:
5665
rpc_endpoint: str
5766
key_dir: Path
5867
program_key: PublicKey
68+
publisher_program_key: Optional[PublicKey]
5969
authority_permission_account: Optional[PythAuthorityPermissionAccount]
6070
_mapping_accounts: Dict[PublicKey, PythMappingAccount]
6171
_product_accounts: Dict[PublicKey, PythProductAccount]
@@ -66,13 +76,17 @@ def __init__(
6676
network: Network,
6777
key_dir: str,
6878
program_key: str,
79+
publisher_program_key: Optional[str],
6980
commitment: Literal["confirmed", "finalized"],
7081
rpc_endpoint: str = "",
7182
):
7283
self.network = network
7384
self.rpc_endpoint = rpc_endpoint or RPC_ENDPOINTS[network]
7485
self.key_dir = Path(key_dir)
7586
self.program_key = PublicKey(program_key)
87+
self.publisher_program_key = (
88+
PublicKey(publisher_program_key) if publisher_program_key else None
89+
)
7690
self.commitment = Commitment(commitment)
7791
self.authority_permission_account = None
7892
self._mapping_accounts: Dict[PublicKey, PythMappingAccount] = {}
@@ -100,6 +114,12 @@ async def fetch_minimum_balance(self, size: int) -> int:
100114
async with AsyncClient(self.rpc_endpoint) as client:
101115
return (await client.get_minimum_balance_for_rent_exemption(size)).value
102116

117+
async def account_exists(self, key: PublicKey) -> bool:
118+
async with AsyncClient(self.rpc_endpoint) as client:
119+
response = await client.get_account_info(key)
120+
# The RPC returns null if the account does not exist
121+
return bool(response.value)
122+
103123
async def refresh_program_accounts(self):
104124
async with AsyncClient(self.rpc_endpoint) as client:
105125
logger.info("Refreshing program accounts")
@@ -301,6 +321,19 @@ async def sync(
301321
if product_updates:
302322
await self.refresh_program_accounts()
303323

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

306339
publisher_transactions = []
@@ -658,3 +691,54 @@ async def resize_price_accounts_v2(
658691

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

program_admin/cli.py

Lines changed: 14 additions & 0 deletions
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

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

tests/test_sync.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ async def test_sync(
421421
network=network,
422422
key_dir=key_dir,
423423
program_key=pyth_program,
424+
publisher_program_key=None,
424425
commitment="confirmed",
425426
)
426427

0 commit comments

Comments
 (0)