78
78
},
79
79
"metadata" : {"jump_id" : "186" , "jump_symbol" : "AAPL" , "price_exp" : - 5 , "min_publishers" : 1 },
80
80
}
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 ]
82
95
83
96
asyncio .set_event_loop (asyncio .new_event_loop ())
84
97
@@ -237,7 +250,7 @@ def refdata_path(self, tmp_path):
237
250
def refdata_products (self , refdata_path ):
238
251
path = os .path .join (refdata_path , 'products.json' )
239
252
with open (path , 'w' ) as f :
240
- f .write (json .dumps ([ BTC_USD , AAPL_USD ] ))
253
+ f .write (json .dumps (ALL_PRODUCTS ))
241
254
f .flush ()
242
255
yield f .name
243
256
@@ -257,6 +270,7 @@ def refdata_permissions(self, refdata_path):
257
270
f .write (json .dumps ({
258
271
"AAPL" : {"price" : ["some_publisher" ]},
259
272
"BTCUSD" : {"price" : ["some_publisher" ]},
273
+ "ETHUSD" : {"price" : []},
260
274
}))
261
275
f .flush ()
262
276
yield f .name
@@ -448,6 +462,7 @@ class TestUpdatePrice(PythTest):
448
462
449
463
@pytest .mark .asyncio
450
464
async def test_update_price_simple (self , client : PythAgentClient ):
465
+
451
466
# Fetch all products
452
467
products = {product ["attr_dict" ]["symbol" ]: product for product in await client .get_all_products ()}
453
468
@@ -460,7 +475,7 @@ async def test_update_price_simple(self, client: PythAgentClient):
460
475
461
476
# Send an "update_price" request
462
477
await client .update_price (price_account , 42 , 2 , "trading" )
463
- time .sleep (1 )
478
+ time .sleep (2 )
464
479
465
480
# Send another "update_price" request to trigger aggregation
466
481
await client .update_price (price_account , 81 , 1 , "trading" )
@@ -505,6 +520,7 @@ async def test_update_price_simple(self, client: PythAgentClient):
505
520
506
521
@pytest .mark .asyncio
507
522
async def test_update_price_simple_with_keypair_hotload (self , client_hotload : PythAgentClient ):
523
+
508
524
# Hotload the keypair into running agent
509
525
hl_request = requests .post ("http://localhost:9001/primary/load_keypair" , json = PUBLISHER_KEYPAIR )
510
526
@@ -513,15 +529,102 @@ async def test_update_price_simple_with_keypair_hotload(self, client_hotload: Py
513
529
514
530
LOGGER .info ("Publisher keypair hotload OK" )
515
531
532
+ time .sleep (3 )
533
+
516
534
# Continue normally with the existing simple scenario
517
535
await self .test_update_price_simple (client_hotload )
518
536
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
+
519
619
@pytest .mark .asyncio
520
620
@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 ):
522
622
'''
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.
524
626
'''
627
+
525
628
# Fetch all products
526
629
products = {product ["attr_dict" ]["symbol" ]: product for product in await client .get_all_products ()}
527
630
@@ -536,7 +639,3 @@ async def test_publish_forever(self, client: PythAgentClient):
536
639
# Send an "update_price" request
537
640
await client .update_price (price_account , 47 , 2 , "trading" )
538
641
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