Skip to content

Commit 622998e

Browse files
authored
Add resize_price_account support; create .dockerignore (#41)
* Add resize_price_account support; create .dockerignore * parsing.py: Use num_components, .*ignore: add dist/ * parsing.py: Fix unused import items
1 parent 1ee101f commit 622998e

File tree

9 files changed

+215
-21
lines changed

9 files changed

+215
-21
lines changed

.dockerignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.envrc
2+
.mypy_cache
3+
__pycache__
4+
dist
5+
keys
6+
permissions.json
7+
products.json
8+
publishers.json
9+
test-ledger
10+
11+
# IntelliJ files
12+
.idea
13+
*.iml

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.envrc
22
.mypy_cache
33
__pycache__
4+
dist
45
keys
56
permissions.json
67
products.json

program_admin/__init__.py

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
)
3737
from program_admin.util import (
3838
MAPPING_ACCOUNT_SIZE,
39-
PRICE_ACCOUNT_SIZE,
39+
PRICE_ACCOUNT_V1_SIZE,
40+
PRICE_ACCOUNT_V2_SIZE,
4041
PRODUCT_ACCOUNT_SIZE,
4142
account_exists,
4243
compute_transaction_size,
@@ -228,6 +229,7 @@ async def sync(
228229
ref_authority_permissions: Optional[ReferenceAuthorityPermissions],
229230
send_transactions: bool = True,
230231
generate_keys: bool = False,
232+
allocate_price_v2: bool = True,
231233
) -> List[TransactionInstruction]:
232234
instructions: List[TransactionInstruction] = []
233235

@@ -261,7 +263,9 @@ async def sync(
261263
(
262264
product_instructions,
263265
product_keypairs,
264-
) = await self.sync_product_instructions(ref_product, generate_keys)
266+
) = await self.sync_product_instructions(
267+
ref_product, generate_keys, allocate_price_v2
268+
)
265269

266270
if product_instructions:
267271
product_updates = True
@@ -361,6 +365,7 @@ async def sync_product_instructions(
361365
self,
362366
product: ReferenceProduct,
363367
generate_keys: bool,
368+
allocate_price_v2: bool,
364369
) -> Tuple[List[TransactionInstruction], List[Keypair]]:
365370
instructions: List[TransactionInstruction] = []
366371
funding_keypair = load_keypair("funding", key_dir=self.key_dir)
@@ -423,16 +428,23 @@ async def sync_product_instructions(
423428
logger.info(f"Creating new price account for {product['jump_symbol']}")
424429

425430
if not await account_exists(self.rpc_endpoint, price_keypair.public_key):
426-
logger.debug("Building system_program.create_account instruction")
431+
price_alloc_size = (
432+
PRICE_ACCOUNT_V2_SIZE
433+
if allocate_price_v2
434+
else PRICE_ACCOUNT_V1_SIZE
435+
)
436+
437+
logger.debug(
438+
f"Building system_program.create_account instruction (allocate_price_v2: {allocate_price_v2}, {price_alloc_size} bytes)"
439+
)
440+
427441
instructions.append(
428442
system_program.create_account(
429443
system_program.CreateAccountParams(
430444
from_pubkey=funding_keypair.public_key,
431445
new_account_pubkey=price_keypair.public_key,
432-
lamports=await self.fetch_minimum_balance(
433-
PRICE_ACCOUNT_SIZE
434-
),
435-
space=PRICE_ACCOUNT_SIZE,
446+
lamports=await self.fetch_minimum_balance(price_alloc_size),
447+
space=price_alloc_size,
436448
program_id=self.program_key,
437449
)
438450
)
@@ -574,3 +586,59 @@ async def sync_authority_permissions_instructions(
574586
logger.debug("Existing authority permissions OK, not updating")
575587

576588
return (instructions, signers)
589+
590+
async def resize_price_accounts_v2(
591+
self,
592+
security_authority_path: Path,
593+
send_transactions: bool,
594+
):
595+
596+
security_authority: Optional[Keypair] = None
597+
with open(security_authority_path, encoding="utf8") as file:
598+
data = bytes(json.load(file))
599+
600+
security_authority = Keypair.from_secret_key(data)
601+
602+
if not security_authority:
603+
raise RuntimeError("Could not load security authority keypair")
604+
605+
await self.refresh_program_accounts()
606+
607+
v1_prices = {}
608+
v2_prices = {}
609+
odd_size_prices = {}
610+
611+
for (pubkey, account) in self._price_accounts.items():
612+
# IMPORTANT: sizes must be checked in descending order
613+
if account.data.used_size >= PRICE_ACCOUNT_V2_SIZE:
614+
logger.debug(f"Price account {pubkey} is v2")
615+
v2_prices[pubkey] = account
616+
elif account.data.used_size >= PRICE_ACCOUNT_V1_SIZE:
617+
logger.debug(f"Price account {pubkey} is v1")
618+
v1_prices[pubkey] = account
619+
else:
620+
logger.warning(
621+
f"Price account {pubkey} of {account.data.used_size} bytes does not match any known used size"
622+
)
623+
odd_size_prices[pubkey] = account
624+
625+
if len(v1_prices) > 0:
626+
logger.info(f"Found {len(v1_prices)} v1 price account(s)")
627+
if len(v2_prices) > 0:
628+
logger.info(f"Found {len(v2_prices)} v2 price account(s)")
629+
if len(odd_size_prices) > 0:
630+
logger.info(f"Found {len(odd_size_prices)} unrecognized price accounts")
631+
632+
instructions = []
633+
signers = [security_authority]
634+
635+
for (pubkey, account) in v1_prices.items():
636+
logger.debug("Building pyth_program.resize_price_account instruction")
637+
638+
instruction = pyth_program.resize_price_account_v2(
639+
self.program_key, security_authority.public_key, pubkey
640+
)
641+
instructions.append(instruction)
642+
643+
if send_transactions:
644+
await self.send_transaction(instructions, signers)

program_admin/cli.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ def restore_links(network, rpc_endpoint, program_key, keys, products, commitment
414414
envvar="GENERATE_KEYS",
415415
default="false",
416416
)
417+
@click.option(
418+
"--allocate-price-v2",
419+
help="Whether to use price account v2 byte size when allocating new price accounts. Used for resize_price_account_v2 testing",
420+
envvar="ALLOCATE_PRICE_V2",
421+
default="true",
422+
)
417423
def sync(
418424
network,
419425
rpc_endpoint,
@@ -427,6 +433,7 @@ def sync(
427433
commitment,
428434
send_transactions,
429435
generate_keys,
436+
allocate_price_v2,
430437
):
431438
program_admin = ProgramAdmin(
432439
network=network,
@@ -456,6 +463,7 @@ def sync(
456463
ref_authority_permissions=ref_authority_permissions,
457464
send_transactions=(send_transactions == "true"),
458465
generate_keys=(generate_keys == "true"),
466+
allocate_price_v2=(allocate_price_v2 == "true"),
459467
)
460468
)
461469

@@ -500,6 +508,52 @@ def migrate_upgrade_authority(
500508
asyncio.run(program_admin.send_transaction([instruction], [funding_keypair]))
501509

502510

511+
@click.command(help="Resize price accounts to the PriceAccountV2 format")
512+
@click.option("--network", help="Solana network", envvar="NETWORK")
513+
@click.option("--rpc-endpoint", help="Solana RPC endpoint", envvar="RPC_ENDPOINT")
514+
@click.option("--program-key", help="Pyth program key", envvar="PROGRAM_KEY")
515+
@click.option(
516+
"--security-authority",
517+
help="Path to security authority keypair for executing resize instruction",
518+
envvar="SECURITY_AUTHORITY",
519+
)
520+
@click.option("--keys", help="Path to keys directory", envvar="KEYS")
521+
@click.option(
522+
"--commitment",
523+
help="Confirmation level to use",
524+
envvar="COMMITMENT",
525+
default="finalized",
526+
)
527+
@click.option(
528+
"--send-transactions",
529+
help="Whether to send transactions or just print instructions (set to 'true' or 'false')",
530+
envvar="SEND_TRANSACTIONS",
531+
default="true",
532+
)
533+
def resize_price_accounts_v2(
534+
network,
535+
rpc_endpoint,
536+
keys,
537+
program_key,
538+
security_authority,
539+
commitment,
540+
send_transactions,
541+
):
542+
program_admin = ProgramAdmin(
543+
network=network,
544+
rpc_endpoint=rpc_endpoint,
545+
key_dir=keys,
546+
program_key=program_key,
547+
commitment=commitment,
548+
)
549+
550+
asyncio.run(
551+
program_admin.resize_price_accounts_v2(
552+
Path(security_authority), (send_transactions == "true")
553+
)
554+
)
555+
556+
503557
cli.add_command(delete_price)
504558
cli.add_command(delete_product)
505559
cli.add_command(list_accounts)
@@ -509,5 +563,6 @@ def migrate_upgrade_authority(
509563
cli.add_command(toggle_publisher)
510564
cli.add_command(update_product_metadata)
511565
cli.add_command(migrate_upgrade_authority)
566+
cli.add_command(resize_price_accounts_v2)
512567
logger.remove()
513568
logger.add(sys.stdout, serialize=(not os.environ.get("DEV_MODE")))

program_admin/instructions.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
COMMAND_ADD_PUBLISHER = 5
1818
COMMAND_DEL_PUBLISHER = 6
1919
COMMAND_MIN_PUBLISHERS = 12
20+
COMMAND_RESIZE_PRICE_ACCOUNT = 14
2021
COMMAND_DEL_PRICE = 15
2122
COMMAND_DEL_PRODUCT = 16
2223
COMMAND_UPD_PERMISSIONS = 17
@@ -330,3 +331,46 @@ def upd_permissions(
330331
],
331332
program_id=program_key,
332333
)
334+
335+
336+
def resize_price_account_v2(
337+
program_key: PublicKey, security_authority: PublicKey, price_account: PublicKey
338+
) -> TransactionInstruction:
339+
"""
340+
Pyth program resize_price_account instruction. It migrates the
341+
specified price account to a new v2 format. The new format
342+
includes more price component slots, allowing more publishers per
343+
price account. Additionally, PriceCumulative is included
344+
345+
The authority pubkeys are passed in instruction data.
346+
347+
Accounts:
348+
- security authority (signer, writable) - must be the pubkey set as security_authority in permission account.
349+
- price account (signer, writable) - The price account to resize
350+
- system program (non-signer, readonly) - Allows the resize_account() call
351+
- permissions account (non-signer, readonly) - PDA of the oracle program, generated automatically, stores the permission information
352+
"""
353+
ix_data_layout = Struct(
354+
"version" / Int32ul,
355+
"command" / Int32sl,
356+
)
357+
358+
ix_data = ix_data_layout.build(
359+
dict(version=PROGRAM_VERSION, command=COMMAND_RESIZE_PRICE_ACCOUNT)
360+
)
361+
362+
[permissions_account, _bump] = PublicKey.find_program_address(
363+
[AUTHORITY_PERMISSIONS_PDA_SEED],
364+
program_key,
365+
)
366+
367+
return TransactionInstruction(
368+
data=ix_data,
369+
keys=[
370+
AccountMeta(pubkey=security_authority, is_signer=True, is_writable=True),
371+
AccountMeta(pubkey=price_account, is_signer=False, is_writable=True),
372+
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
373+
AccountMeta(pubkey=permissions_account, is_signer=False, is_writable=False),
374+
],
375+
program_id=program_key,
376+
)

program_admin/parsing.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def parse_price_info(data: bytes) -> PriceInfo:
9090
return PriceInfo(price, confidence, status, corporate_action, publish_slot)
9191

9292

93+
# NOTE(2023-07-31): For v2 prices the parsed data does not include
94+
# price_cumulative values. This value is currently out-of-scope for
95+
# program-admin.
9396
def parse_price_data(data: bytes) -> PriceData:
9497
used_size = Int32ul.parse(data[12:])
9598
price_type = Int32ul.parse(data[16:])
@@ -113,25 +116,26 @@ def parse_price_data(data: bytes) -> PriceData:
113116
previous_timestamp = Int64sl.parse(data[200:])
114117
aggregate = parse_price_info(data[208:240])
115118
offset = 240
116-
parse_next_component = True
119+
117120
price_components = []
118121

119-
while offset < len(data) and parse_next_component:
122+
for _ in range(components_count):
123+
120124
publisher_key = PublicKey(data[offset : offset + 32])
121125
offset += 32
122126

123-
if publisher_key == PublicKey(bytes(32)):
124-
parse_next_component = False
125-
else:
126-
aggregate_price = parse_price_info(data[offset : offset + 32])
127-
offset += 32
127+
aggregate_price = parse_price_info(data[offset : offset + 32])
128+
offset += 32
129+
130+
latest_price = parse_price_info(data[offset : offset + 32])
131+
offset += 32
128132

129-
latest_price = parse_price_info(data[offset : offset + 32])
130-
offset += 32
133+
price_components.append(
134+
PriceComponent(publisher_key, aggregate_price, latest_price)
135+
)
131136

132-
price_components.append(
133-
PriceComponent(publisher_key, aggregate_price, latest_price)
134-
)
137+
# TODO(2023-07-31): Parse price_cumulative here if necessary;
138+
# remember to re-check that this price account is v2 and adjust offset
135139

136140
return PriceData(
137141
used_size,

program_admin/util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
MAPPING_ACCOUNT_SIZE = 20536 # https://github.com/pyth-network/pyth-client/blob/b49f73afe32ce8685a3d05e32d8f3bb51909b061/program/src/oracle/oracle.h#L88
1818
MAPPING_ACCOUNT_PRODUCT_LIMIT = 640
19-
PRICE_ACCOUNT_SIZE = 12576
19+
PRICE_ACCOUNT_V1_SIZE = 3312
20+
PRICE_ACCOUNT_V2_SIZE = 12576
21+
PRICE_V1_COMP_COUNT = 32
22+
PRICE_V2_COMP_COUNT = 128
2023
PRODUCT_ACCOUNT_SIZE = 512
2124
SOL_LAMPORTS = pow(10, 9)
2225

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.1"
12+
version = "0.1.2"
1313

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

0 commit comments

Comments
 (0)