Skip to content

Commit

Permalink
feat: add publisher program to sync cli (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
ali-bahjati authored Sep 9, 2024
1 parent 11e142d commit 15e39c3
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 3 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ RUN curl -sSL https://install.python-poetry.org | python
ENV PATH="$POETRY_HOME/bin:$PATH"

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


Expand Down Expand Up @@ -80,7 +80,7 @@ ARG APP_PATH

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

ENV \
Expand Down
82 changes: 82 additions & 0 deletions program_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
from program_admin import instructions as pyth_program
from program_admin.keys import load_keypair
from program_admin.parsing import parse_account
from program_admin.publisher_program_instructions import (
config_account_pubkey as publisher_program_config_account_pubkey,
)
from program_admin.publisher_program_instructions import (
create_buffer_account,
initialize_publisher_config,
initialize_publisher_program,
publisher_config_account_pubkey,
)
from program_admin.types import (
Network,
PythAuthorityPermissionAccount,
Expand Down Expand Up @@ -56,6 +65,7 @@ class ProgramAdmin:
rpc_endpoint: str
key_dir: Path
program_key: PublicKey
publisher_program_key: Optional[PublicKey]
authority_permission_account: Optional[PythAuthorityPermissionAccount]
_mapping_accounts: Dict[PublicKey, PythMappingAccount]
_product_accounts: Dict[PublicKey, PythProductAccount]
Expand All @@ -66,13 +76,17 @@ def __init__(
network: Network,
key_dir: str,
program_key: str,
publisher_program_key: Optional[str],
commitment: Literal["confirmed", "finalized"],
rpc_endpoint: str = "",
):
self.network = network
self.rpc_endpoint = rpc_endpoint or RPC_ENDPOINTS[network]
self.key_dir = Path(key_dir)
self.program_key = PublicKey(program_key)
self.publisher_program_key = (
PublicKey(publisher_program_key) if publisher_program_key else None
)
self.commitment = Commitment(commitment)
self.authority_permission_account = None
self._mapping_accounts: Dict[PublicKey, PythMappingAccount] = {}
Expand Down Expand Up @@ -301,6 +315,23 @@ async def sync(
if product_updates:
await self.refresh_program_accounts()

# Sync publisher program
(
publisher_program_instructions,
publisher_program_signers,
) = await self.sync_publisher_program(ref_publishers)

logger.debug(
f"Syncing publisher program - {len(publisher_program_instructions)} instructions"
)

if publisher_program_instructions:
instructions.extend(publisher_program_instructions)
if send_transactions:
await self.send_transaction(
publisher_program_instructions, publisher_program_signers
)

# Sync publishers

publisher_transactions = []
Expand Down Expand Up @@ -658,3 +689,54 @@ async def resize_price_accounts_v2(

if send_transactions:
await self.send_transaction(instructions, signers)

async def sync_publisher_program(
self, ref_publishers: ReferencePublishers
) -> Tuple[List[TransactionInstruction], List[Keypair]]:
if self.publisher_program_key is None:
return [], []

instructions = []

authority = load_keypair("funding", key_dir=self.key_dir)

publisher_program_config = publisher_program_config_account_pubkey(
self.publisher_program_key
)

# Initialize the publisher program config if it does not exist
if not (await account_exists(self.rpc_endpoint, publisher_program_config)):
initialize_publisher_program_instruction = initialize_publisher_program(
self.publisher_program_key, authority.public_key
)
instructions.append(initialize_publisher_program_instruction)

# Initialize publisher config accounts for new publishers
for publisher in ref_publishers["keys"].values():
publisher_config_account = publisher_config_account_pubkey(
publisher, self.publisher_program_key
)

if not (await account_exists(self.rpc_endpoint, publisher_config_account)):
size = 100048 # This size is for a buffer supporting 5000 price updates
lamports = await self.fetch_minimum_balance(size)
buffer_account, create_buffer_instruction = create_buffer_account(
self.publisher_program_key,
authority.public_key,
publisher,
size,
lamports,
)

initialize_publisher_config_instruction = initialize_publisher_config(
self.publisher_program_key,
publisher,
authority.public_key,
buffer_account,
)

instructions.extend(
[create_buffer_instruction, initialize_publisher_config_instruction]
)

return (instructions, [authority])
14 changes: 14 additions & 0 deletions program_admin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def delete_price(network, rpc_endpoint, program_key, keys, commitment, product,
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=None,
commitment=commitment,
)
funding_keypair = load_keypair("funding", key_dir=keys)
Expand Down Expand Up @@ -236,6 +237,7 @@ def delete_product(
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=None,
commitment=commitment,
)
funding_keypair = load_keypair("funding", key_dir=keys)
Expand Down Expand Up @@ -275,6 +277,7 @@ def list_accounts(network, rpc_endpoint, program_key, keys, publishers, commitme
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=None,
commitment=commitment,
)

Expand Down Expand Up @@ -333,6 +336,7 @@ def restore_links(network, rpc_endpoint, program_key, keys, products, commitment
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=None,
commitment=commitment,
)
reference_products = parse_products_json(Path(products))
Expand Down Expand Up @@ -382,6 +386,12 @@ def restore_links(network, rpc_endpoint, program_key, keys, products, commitment
@click.option("--network", help="Solana network", envvar="NETWORK")
@click.option("--rpc-endpoint", help="Solana RPC endpoint", envvar="RPC_ENDPOINT")
@click.option("--program-key", help="Pyth program key", envvar="PROGRAM_KEY")
@click.option(
"--publisher-program-key",
help="Publisher program key",
envvar="PUBLISHER_PROGRAM_KEY",
default=None,
)
@click.option("--keys", help="Path to keys directory", envvar="KEYS")
@click.option("--products", help="Path to reference products file", envvar="PRODUCTS")
@click.option(
Expand Down Expand Up @@ -426,6 +436,7 @@ def sync(
network,
rpc_endpoint,
program_key,
publisher_program_key,
keys,
products,
publishers,
Expand All @@ -442,6 +453,7 @@ def sync(
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=publisher_program_key,
commitment=commitment,
)

Expand Down Expand Up @@ -495,6 +507,7 @@ def migrate_upgrade_authority(
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=None,
commitment=commitment,
)
funding_keypair = load_keypair("funding", key_dir=keys)
Expand Down Expand Up @@ -544,6 +557,7 @@ def resize_price_accounts_v2(
rpc_endpoint=rpc_endpoint,
key_dir=keys,
program_key=program_key,
publisher_program_key=None,
commitment=commitment,
)

Expand Down
164 changes: 164 additions & 0 deletions program_admin/publisher_program_instructions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from typing import Tuple

from construct import Bytes, Int8ul, Struct
from solana import system_program
from solana.publickey import PublicKey
from solana.system_program import SYS_PROGRAM_ID, CreateAccountWithSeedParams
from solana.transaction import AccountMeta, TransactionInstruction


def config_account_pubkey(program_key: PublicKey) -> PublicKey:
[config_account, _] = PublicKey.find_program_address(
[b"CONFIG"],
program_key,
)
return config_account


def publisher_config_account_pubkey(
publisher_key: PublicKey, program_key: PublicKey
) -> PublicKey:
[publisher_config_account, _] = PublicKey.find_program_address(
[b"PUBLISHER_CONFIG", bytes(publisher_key)],
program_key,
)
return publisher_config_account


def initialize_publisher_program(
program_key: PublicKey,
authority: PublicKey,
) -> TransactionInstruction:
"""
Pyth publisher program initialize instruction with the given authority
accounts:
- payer account (signer, writable) - we pass the authority as the payer
- config account (writable)
- system program
"""

[config_account, bump] = PublicKey.find_program_address(
[b"CONFIG"],
program_key,
)

ix_data_layout = Struct(
"instruction_id" / Int8ul,
"bump" / Int8ul,
"authority" / Bytes(32),
)

ix_data = ix_data_layout.build(
dict(
instruction_id=0,
bump=bump,
authority=bytes(authority),
)
)

return TransactionInstruction(
data=ix_data,
keys=[
AccountMeta(pubkey=authority, is_signer=True, is_writable=True),
AccountMeta(pubkey=config_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
],
program_id=program_key,
)


def create_buffer_account(
program_key: PublicKey,
base_pubkey: PublicKey,
publisher_pubkey: PublicKey,
space: int,
lamports: int,
) -> Tuple[PublicKey, TransactionInstruction]:
# Since the string representation of the PublicKey is 44 bytes long (base58 encoded)
# and we use 32 bytes of it, the chances of collision are very low.
#
# The seed has a max length of 32 and although the publisher_pubkey is 32 bytes,
# it is impossible to convert it to a string with a length of 32 that the
# underlying library (solders) can handle. We don't know exactly why, but it
# seems to be related to str -> &str conversion in pyo3 that solders uses to
# interact with the Rust implementation of the logic.
seed = str(publisher_pubkey)[:32]
new_account_pubkey = PublicKey.create_with_seed(
base_pubkey,
seed,
program_key,
)

return (
new_account_pubkey,
system_program.create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=base_pubkey,
new_account_pubkey=new_account_pubkey,
base_pubkey=base_pubkey,
seed=seed,
program_id=program_key,
lamports=lamports,
space=space,
)
),
)


def initialize_publisher_config(
program_key: PublicKey,
publisher_key: PublicKey,
authority: PublicKey,
buffer_account: PublicKey,
) -> TransactionInstruction:
"""
Pyth publisher program initialize publisher config instruction with the given authority
accounts:
- authority account (signer, writable)
- config account
- publisher config account (writable)
- buffer account (writable)
- system program
"""

[config_account, config_bump] = PublicKey.find_program_address(
[b"CONFIG"],
program_key,
)

[publisher_config_account, publisher_config_bump] = PublicKey.find_program_address(
[b"PUBLISHER_CONFIG", bytes(publisher_key)],
program_key,
)

ix_data_layout = Struct(
"instruction_id" / Int8ul,
"config_bump" / Int8ul,
"publisher_config_bump" / Int8ul,
"publisher" / Bytes(32),
)

ix_data = ix_data_layout.build(
dict(
instruction_id=2,
config_bump=config_bump,
publisher_config_bump=publisher_config_bump,
publisher=bytes(publisher_key),
)
)

return TransactionInstruction(
data=ix_data,
keys=[
AccountMeta(pubkey=authority, is_signer=True, is_writable=True),
AccountMeta(pubkey=config_account, is_signer=False, is_writable=True),
AccountMeta(
pubkey=publisher_config_account, is_signer=False, is_writable=False
),
AccountMeta(pubkey=buffer_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
],
program_id=program_key,
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ known_local_folder = ["program_admin"]
authors = ["Thomaz <thomaz@pyth.network>"]
description = "Syncs products and publishers of the Pyth program"
name = "program-admin"
version = "0.1.3"
version = "0.1.4"

[tool.poetry.dependencies]
click = "^8.1.0"
Expand Down
1 change: 1 addition & 0 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ async def test_sync(
network=network,
key_dir=key_dir,
program_key=pyth_program,
publisher_program_key=None,
commitment="confirmed",
)

Expand Down

0 comments on commit 15e39c3

Please sign in to comment.