Skip to content

Commit 15e39c3

Browse files
authored
feat: add publisher program to sync cli (#44)
1 parent 11e142d commit 15e39c3

File tree

6 files changed

+264
-3
lines changed

6 files changed

+264
-3
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ RUN curl -sSL https://install.python-poetry.org | python
3131
ENV PATH="$POETRY_HOME/bin:$PATH"
3232

3333
# Install Solana CLI
34-
RUN sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
34+
RUN sh -c "$(curl -sSfL https://release.solana.com/v1.14.17/install)"
3535
ENV PATH=$PATH:/root/.local/share/solana/install/active_release/bin
3636

3737

@@ -80,7 +80,7 @@ ARG APP_PATH
8080

8181
# Install Solana CLI, we redo this step because this Docker target
8282
# starts from scratch without the earlier Solana installation
83-
RUN sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
83+
RUN sh -c "$(curl -sSfL https://release.solana.com/v1.14.17/install)"
8484
ENV PATH=$PATH:/root/.local/share/solana/install/active_release/bin
8585

8686
ENV \

program_admin/__init__.py

Lines changed: 82 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] = {}
@@ -301,6 +315,23 @@ async def sync(
301315
if product_updates:
302316
await self.refresh_program_accounts()
303317

318+
# Sync publisher program
319+
(
320+
publisher_program_instructions,
321+
publisher_program_signers,
322+
) = await self.sync_publisher_program(ref_publishers)
323+
324+
logger.debug(
325+
f"Syncing publisher program - {len(publisher_program_instructions)} instructions"
326+
)
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 account_exists(self.rpc_endpoint, 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 account_exists(self.rpc_endpoint, 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

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: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
"instruction_id" / Int8ul,
48+
"bump" / Int8ul,
49+
"authority" / Bytes(32),
50+
)
51+
52+
ix_data = ix_data_layout.build(
53+
dict(
54+
instruction_id=0,
55+
bump=bump,
56+
authority=bytes(authority),
57+
)
58+
)
59+
60+
return TransactionInstruction(
61+
data=ix_data,
62+
keys=[
63+
AccountMeta(pubkey=authority, is_signer=True, is_writable=True),
64+
AccountMeta(pubkey=config_account, is_signer=False, is_writable=True),
65+
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
66+
],
67+
program_id=program_key,
68+
)
69+
70+
71+
def create_buffer_account(
72+
program_key: PublicKey,
73+
base_pubkey: PublicKey,
74+
publisher_pubkey: PublicKey,
75+
space: int,
76+
lamports: int,
77+
) -> Tuple[PublicKey, TransactionInstruction]:
78+
# Since the string representation of the PublicKey is 44 bytes long (base58 encoded)
79+
# and we use 32 bytes of it, the chances of collision are very low.
80+
#
81+
# The seed has a max length of 32 and although the publisher_pubkey is 32 bytes,
82+
# it is impossible to convert it to a string with a length of 32 that the
83+
# underlying library (solders) can handle. We don't know exactly why, but it
84+
# seems to be related to str -> &str conversion in pyo3 that solders uses to
85+
# interact with the Rust implementation of the logic.
86+
seed = str(publisher_pubkey)[:32]
87+
new_account_pubkey = PublicKey.create_with_seed(
88+
base_pubkey,
89+
seed,
90+
program_key,
91+
)
92+
93+
return (
94+
new_account_pubkey,
95+
system_program.create_account_with_seed(
96+
CreateAccountWithSeedParams(
97+
from_pubkey=base_pubkey,
98+
new_account_pubkey=new_account_pubkey,
99+
base_pubkey=base_pubkey,
100+
seed=seed,
101+
program_id=program_key,
102+
lamports=lamports,
103+
space=space,
104+
)
105+
),
106+
)
107+
108+
109+
def initialize_publisher_config(
110+
program_key: PublicKey,
111+
publisher_key: PublicKey,
112+
authority: PublicKey,
113+
buffer_account: PublicKey,
114+
) -> TransactionInstruction:
115+
"""
116+
Pyth publisher program initialize publisher config instruction with the given authority
117+
118+
accounts:
119+
- authority account (signer, writable)
120+
- config account
121+
- publisher config account (writable)
122+
- buffer account (writable)
123+
- system program
124+
"""
125+
126+
[config_account, config_bump] = PublicKey.find_program_address(
127+
[b"CONFIG"],
128+
program_key,
129+
)
130+
131+
[publisher_config_account, publisher_config_bump] = PublicKey.find_program_address(
132+
[b"PUBLISHER_CONFIG", bytes(publisher_key)],
133+
program_key,
134+
)
135+
136+
ix_data_layout = Struct(
137+
"instruction_id" / Int8ul,
138+
"config_bump" / Int8ul,
139+
"publisher_config_bump" / Int8ul,
140+
"publisher" / Bytes(32),
141+
)
142+
143+
ix_data = ix_data_layout.build(
144+
dict(
145+
instruction_id=2,
146+
config_bump=config_bump,
147+
publisher_config_bump=publisher_config_bump,
148+
publisher=bytes(publisher_key),
149+
)
150+
)
151+
152+
return TransactionInstruction(
153+
data=ix_data,
154+
keys=[
155+
AccountMeta(pubkey=authority, is_signer=True, is_writable=True),
156+
AccountMeta(pubkey=config_account, is_signer=False, is_writable=True),
157+
AccountMeta(
158+
pubkey=publisher_config_account, is_signer=False, is_writable=False
159+
),
160+
AccountMeta(pubkey=buffer_account, is_signer=False, is_writable=True),
161+
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
162+
],
163+
program_id=program_key,
164+
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ known_local_folder = ["program_admin"]
99
authors = ["Thomaz <[email protected]>"]
1010
description = "Syncs products and publishers of the Pyth program"
1111
name = "program-admin"
12-
version = "0.1.3"
12+
version = "0.1.4"
1313

1414
[tool.poetry.dependencies]
1515
click = "^8.1.0"

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)