Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add publisher program to sync cli #44

Merged
merged 10 commits into from
Sep 9, 2024
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 (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you use a formatter such as black/ruff it will combine these two import declarations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually it seems that the formatter in pre-commit (black i assume) is breaking them apart. idk why but i can't join them.

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),
ali-bahjati marked this conversation as resolved.
Show resolved Hide resolved
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,
)
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
Loading