7878 },
7979 "metadata" : {"jump_id" : "186" , "jump_symbol" : "AAPL" , "price_exp" : - 5 , "min_publishers" : 1 },
8080}
81- ALL_PRODUCTS = [BTC_USD , AAPL_USD ]
81+
82+ ETH_USD = {
83+ "account" : "" ,
84+ "attr_dict" : {
85+ "symbol" : "Crypto.ETH/USD" ,
86+ "asset_type" : "Crypto" ,
87+ "base" : "ETH" ,
88+ "quote_currency" : "USD" ,
89+ "generic_symbol" : "ETHUSD" ,
90+ "description" : "ETH/USD" ,
91+ },
92+ "metadata" : {"jump_id" : "78876710" , "jump_symbol" : "ETHUSD" , "price_exp" : - 8 , "min_publishers" : 1 },
93+ }
94+ ALL_PRODUCTS = [BTC_USD , AAPL_USD , ETH_USD ]
8295
8396asyncio .set_event_loop (asyncio .new_event_loop ())
8497
@@ -237,7 +250,7 @@ def refdata_path(self, tmp_path):
237250 def refdata_products (self , refdata_path ):
238251 path = os .path .join (refdata_path , 'products.json' )
239252 with open (path , 'w' ) as f :
240- f .write (json .dumps ([ BTC_USD , AAPL_USD ] ))
253+ f .write (json .dumps (ALL_PRODUCTS ))
241254 f .flush ()
242255 yield f .name
243256
@@ -257,6 +270,7 @@ def refdata_permissions(self, refdata_path):
257270 f .write (json .dumps ({
258271 "AAPL" : {"price" : ["some_publisher" ]},
259272 "BTCUSD" : {"price" : ["some_publisher" ]},
273+ "ETHUSD" : {"price" : []},
260274 }))
261275 f .flush ()
262276 yield f .name
@@ -448,6 +462,7 @@ class TestUpdatePrice(PythTest):
448462
449463 @pytest .mark .asyncio
450464 async def test_update_price_simple (self , client : PythAgentClient ):
465+
451466 # Fetch all products
452467 products = {product ["attr_dict" ]["symbol" ]: product for product in await client .get_all_products ()}
453468
@@ -460,7 +475,7 @@ async def test_update_price_simple(self, client: PythAgentClient):
460475
461476 # Send an "update_price" request
462477 await client .update_price (price_account , 42 , 2 , "trading" )
463- time .sleep (1 )
478+ time .sleep (2 )
464479
465480 # Send another "update_price" request to trigger aggregation
466481 await client .update_price (price_account , 81 , 1 , "trading" )
@@ -505,6 +520,7 @@ async def test_update_price_simple(self, client: PythAgentClient):
505520
506521 @pytest .mark .asyncio
507522 async def test_update_price_simple_with_keypair_hotload (self , client_hotload : PythAgentClient ):
523+
508524 # Hotload the keypair into running agent
509525 hl_request = requests .post ("http://localhost:9001/primary/load_keypair" , json = PUBLISHER_KEYPAIR )
510526
@@ -513,15 +529,102 @@ async def test_update_price_simple_with_keypair_hotload(self, client_hotload: Py
513529
514530 LOGGER .info ("Publisher keypair hotload OK" )
515531
532+ time .sleep (3 )
533+
516534 # Continue normally with the existing simple scenario
517535 await self .test_update_price_simple (client_hotload )
518536
537+ @pytest .mark .asyncio
538+ async def test_update_price_discards_unpermissioned (self , client : PythAgentClient , tmp_path ):
539+
540+ # Fetch all products
541+ products = {product ["attr_dict" ]["symbol" ]: product for product in await client .get_all_products ()}
542+
543+ # Find the product account ID corresponding to the BTC/USD symbol
544+ product = products [BTC_USD ["attr_dict" ]["symbol" ]]
545+ product_account = product ["account" ]
546+
547+ # Get the price account with which to send updates
548+ price_account = product ["price_accounts" ][0 ]["account" ]
549+
550+ # Use the unpermissioned ETH/USD symbol to trigger unpermissioned account filtering
551+ product_unperm = products [ETH_USD ["attr_dict" ]["symbol" ]]
552+ product_account_unperm = product_unperm ["account" ]
553+ price_account_unperm = product_unperm ["price_accounts" ][0 ]["account" ]
554+
555+
556+ balance_before = self .run (f"solana balance -k { tmp_path } /agent_keystore/publish_key_pair.json -u localhost" ).stdout
557+
558+ # Send an "update_price" request for the valid symbol
559+ await client .update_price (price_account , 42 , 2 , "trading" )
560+ time .sleep (1 )
561+
562+ # Send another "update_price" request to trigger aggregation
563+ await client .update_price (price_account , 81 , 1 , "trading" )
564+ time .sleep (2 )
565+
566+ balance_after = self .run (f"solana balance -k { tmp_path } /agent_keystore/publish_key_pair.json -u localhost" ).stdout
567+
568+ # Confirm that a valid update triggers a transaction that charges the publishing keypair
569+ assert balance_before != balance_after
570+
571+ balance_before_unperm = balance_after
572+
573+ # Send an "update_price" request for the invalid symbol
574+ await client .update_price (price_account_unperm , 48 , 2 , "trading" )
575+ time .sleep (1 )
576+
577+ # Send another "update_price" request to "trigger" aggregation
578+ await client .update_price (price_account_unperm , 81 , 1 , "trading" )
579+ time .sleep (2 )
580+
581+ balance_after_unperm = self .run (f"solana balance -k { tmp_path } /agent_keystore/publish_key_pair.json -u localhost" ).stdout
582+
583+ # Confirm that no SOL was charged during unpermissioned symbol updates
584+ assert balance_before_unperm == balance_after_unperm
585+
586+ # Confirm that the valid symbol was updated
587+ final_product_state = await client .get_product (product_account )
588+
589+ final_price_account = final_product_state ["price_accounts" ][0 ]
590+ assert final_price_account ["price" ] == 42
591+ assert final_price_account ["conf" ] == 2
592+ assert final_price_account ["status" ] == "trading"
593+
594+ # Sanity-check that the unpermissioned symbol was not updated
595+ final_product_state_unperm = await client .get_product (product_account_unperm )
596+
597+ final_price_account_unperm = final_product_state_unperm ["price_accounts" ][0 ]
598+ assert final_price_account_unperm ["price" ] == 0
599+ assert final_price_account_unperm ["conf" ] == 0
600+ assert final_price_account_unperm ["status" ] == "unknown"
601+
602+ # Confirm agent logs contain the relevant WARN log
603+ with open (f"{ tmp_path } /agent_logs/stdout" ) as f :
604+ contents = f .read ()
605+ lines_found = 0
606+ for line in contents .splitlines ():
607+
608+ if "Attempted to publish a price without permission" in line :
609+ lines_found += 1
610+ expected_unperm_pubkey = final_price_account_unperm ["account" ]
611+ # Must point at the expected account as all other attempts must be valid
612+ assert f"price_account: { expected_unperm_pubkey } " in line
613+
614+ # Must find at least one log discarding the account
615+ assert lines_found > 0
616+
617+
618+
519619 @pytest .mark .asyncio
520620 @pytest .mark .skip (reason = "Test not meant for automatic CI" )
521- async def test_publish_forever (self , client : PythAgentClient ):
621+ async def test_publish_forever (self , client : PythAgentClient , tmp_path ):
522622 '''
523- Convenience test routine for manual experiments on a running test setup.
623+ Convenience test routine for manual experiments on a running
624+ test setup. Comment out the skip to enable. use `-k "forever"`
625+ in pytest command line to only run this scenario.
524626 '''
627+
525628 # Fetch all products
526629 products = {product ["attr_dict" ]["symbol" ]: product for product in await client .get_all_products ()}
527630
@@ -536,7 +639,3 @@ async def test_publish_forever(self, client: PythAgentClient):
536639 # Send an "update_price" request
537640 await client .update_price (price_account , 47 , 2 , "trading" )
538641 time .sleep (1 )
539-
540- # Send another "update_price" request to trigger aggregation
541- await client .update_price (price_account , 81 , 1 , "trading" )
542- time .sleep (2 )
0 commit comments