Selaa lähdekoodia

feat(target_chains/starknet): parce price feeds (#1622)

* feat(target_chains/starknet): parce price feeds

* refactor(target_chains/starknet): rename fn and add comment
Pavel Strakhov 1 vuosi sitten
vanhempi
sitoutus
d430b7b44c

+ 159 - 59
target_chains/starknet/contracts/src/pyth.cairo

@@ -11,10 +11,10 @@ pub use pyth::{
 };
 pub use errors::{
     GetPriceUnsafeError, GovernanceActionError, UpdatePriceFeedsError, GetPriceNoOlderThanError,
-    UpdatePriceFeedsIfNecessaryError,
+    UpdatePriceFeedsIfNecessaryError, ParsePriceFeedsError,
 };
 pub use interface::{
-    IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime
+    IPyth, IPythDispatcher, IPythDispatcherTrait, DataSource, Price, PriceFeedPublishTime, PriceFeed
 };
 
 #[starknet::contract]
@@ -36,12 +36,13 @@ mod pyth {
     use super::{
         DataSource, UpdatePriceFeedsError, GovernanceActionError, Price, GetPriceUnsafeError,
         IPythDispatcher, IPythDispatcherTrait, PriceFeedPublishTime, GetPriceNoOlderThanError,
-        UpdatePriceFeedsIfNecessaryError,
+        UpdatePriceFeedsIfNecessaryError, PriceFeed, ParsePriceFeedsError,
     };
     use super::governance;
     use super::governance::GovernancePayload;
     use openzeppelin::token::erc20::interface::{IERC20CamelDispatcherTrait, IERC20CamelDispatcher};
     use pyth::util::ResultMapErrInto;
+    use core::nullable::{NullableTrait, match_nullable, FromNullableResult};
 
     #[event]
     #[derive(Drop, PartialEq, starknet::Event)]
@@ -204,48 +205,7 @@ mod pyth {
         }
 
         fn update_price_feeds(ref self: ContractState, data: ByteArray) {
-            let mut reader = ReaderImpl::new(data);
-            read_and_verify_header(ref reader);
-            let wormhole_proof_size = reader.read_u16();
-            let wormhole_proof = reader.read_byte_array(wormhole_proof_size.into());
-
-            let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
-            let vm = wormhole.parse_and_verify_vm(wormhole_proof);
-
-            let source = DataSource {
-                emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
-            };
-            if !self.is_valid_data_source.read(source) {
-                panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
-            }
-
-            let root_digest = parse_wormhole_proof(vm.payload);
-
-            let num_updates = reader.read_u8();
-            let total_fee = self.get_total_fee(num_updates);
-            let fee_contract = IERC20CamelDispatcher {
-                contract_address: self.fee_contract_address.read()
-            };
-            let execution_info = get_execution_info().unbox();
-            let caller = execution_info.caller_address;
-            let contract = execution_info.contract_address;
-            if fee_contract.allowance(caller, contract) < total_fee {
-                panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
-            }
-            if !fee_contract.transferFrom(caller, contract, total_fee) {
-                panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
-            }
-
-            let mut i = 0;
-            while i < num_updates {
-                let message = read_and_verify_message(ref reader, root_digest);
-                self.update_latest_price_if_necessary(message);
-                i += 1;
-            };
-
-            if reader.len() != 0 {
-                panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
-            }
+            self.update_price_feeds_internal(data, array![], 0, 0, false);
         }
 
         fn get_update_fee(self: @ContractState, data: ByteArray) -> u256 {
@@ -279,6 +239,32 @@ mod pyth {
             }
         }
 
+        fn parse_price_feed_updates(
+            ref self: ContractState,
+            data: ByteArray,
+            price_ids: Array<u256>,
+            min_publish_time: u64,
+            max_publish_time: u64
+        ) -> Array<PriceFeed> {
+            self
+                .update_price_feeds_internal(
+                    data, price_ids, min_publish_time, max_publish_time, false
+                )
+        }
+
+        fn parse_unique_price_feed_updates(
+            ref self: ContractState,
+            data: ByteArray,
+            price_ids: Array<u256>,
+            publish_time: u64,
+            max_staleness: u64,
+        ) -> Array<PriceFeed> {
+            self
+                .update_price_feeds_internal(
+                    data, price_ids, publish_time, publish_time + max_staleness, true
+                )
+        }
+
         fn execute_governance_instruction(ref self: ContractState, data: ByteArray) {
             let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
             let vm = wormhole.parse_and_verify_vm(data.clone());
@@ -362,24 +348,24 @@ mod pyth {
             old_data_sources
         }
 
-        fn update_latest_price_if_necessary(ref self: ContractState, message: PriceFeedMessage) {
-            let latest_publish_time = self.latest_price_info.read(message.price_id).publish_time;
-            if message.publish_time > latest_publish_time {
+        fn update_latest_price_if_necessary(ref self: ContractState, message: @PriceFeedMessage) {
+            let latest_publish_time = self.latest_price_info.read(*message.price_id).publish_time;
+            if *message.publish_time > latest_publish_time {
                 let info = PriceInfo {
-                    price: message.price,
-                    conf: message.conf,
-                    expo: message.expo,
-                    publish_time: message.publish_time,
-                    ema_price: message.ema_price,
-                    ema_conf: message.ema_conf,
+                    price: *message.price,
+                    conf: *message.conf,
+                    expo: *message.expo,
+                    publish_time: *message.publish_time,
+                    ema_price: *message.ema_price,
+                    ema_conf: *message.ema_conf,
                 };
-                self.latest_price_info.write(message.price_id, info);
+                self.latest_price_info.write(*message.price_id, info);
 
                 let event = PriceFeedUpdated {
-                    price_id: message.price_id,
-                    publish_time: message.publish_time,
-                    price: message.price,
-                    conf: message.conf,
+                    price_id: *message.price_id,
+                    publish_time: *message.publish_time,
+                    price: *message.price,
+                    conf: *message.conf,
                 };
                 self.emit(event);
             }
@@ -490,6 +476,105 @@ mod pyth {
             let event = ContractUpgraded { new_class_hash: new_implementation };
             self.emit(event);
         }
+
+        // Applies all price feed updates encoded in `data` and extracts requested information
+        // about the new updates. `price_ids` specifies price feeds of interest. The output will
+        // contain as many items as `price_ids`, with price feeds returned in the same order as
+        // specified in `price_ids`.
+        //
+        // If `unique == false`, for each price feed, the first encountered update
+        // in the specified time interval (both timestamps inclusive) will be returned.
+        // If `unique == true`, the globally unique first update will be returned, as verified by
+        // the `prev_publish_time` value of the update. Panics if a matching update was not found
+        // for any of the specified feeds.
+        fn update_price_feeds_internal(
+            ref self: ContractState,
+            data: ByteArray,
+            price_ids: Array<u256>,
+            min_publish_time: u64,
+            max_publish_time: u64,
+            unique: bool,
+        ) -> Array<PriceFeed> {
+            let mut output: Felt252Dict<Nullable<PriceFeed>> = Default::default();
+            let mut reader = ReaderImpl::new(data);
+            read_and_verify_header(ref reader);
+            let wormhole_proof_size = reader.read_u16();
+            let wormhole_proof = reader.read_byte_array(wormhole_proof_size.into());
+
+            let wormhole = IWormholeDispatcher { contract_address: self.wormhole_address.read() };
+            let vm = wormhole.parse_and_verify_vm(wormhole_proof);
+
+            let source = DataSource {
+                emitter_chain_id: vm.emitter_chain_id, emitter_address: vm.emitter_address
+            };
+            if !self.is_valid_data_source.read(source) {
+                panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateDataSource.into());
+            }
+
+            let root_digest = parse_wormhole_proof(vm.payload);
+
+            let num_updates = reader.read_u8();
+            let total_fee = self.get_total_fee(num_updates);
+            let fee_contract = IERC20CamelDispatcher {
+                contract_address: self.fee_contract_address.read()
+            };
+            let execution_info = get_execution_info().unbox();
+            let caller = execution_info.caller_address;
+            let contract = execution_info.contract_address;
+            if fee_contract.allowance(caller, contract) < total_fee {
+                panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
+            }
+            if !fee_contract.transferFrom(caller, contract, total_fee) {
+                panic_with_felt252(UpdatePriceFeedsError::InsufficientFeeAllowance.into());
+            }
+
+            let mut i = 0;
+            let price_ids2 = @price_ids;
+            while i < num_updates {
+                let message = read_and_verify_message(ref reader, root_digest);
+                self.update_latest_price_if_necessary(@message);
+
+                let output_index = find_index_of_price_id(price_ids2, message.price_id);
+                match output_index {
+                    Option::Some(output_index) => {
+                        if output.get(output_index.into()).is_null() {
+                            let should_output = message.publish_time >= min_publish_time
+                                && message.publish_time <= max_publish_time
+                                && (!unique || min_publish_time > message.prev_publish_time);
+                            if should_output {
+                                output
+                                    .insert(
+                                        output_index.into(), NullableTrait::new(message.into())
+                                    );
+                            }
+                        }
+                    },
+                    Option::None => {}
+                }
+
+                i += 1;
+            };
+
+            if reader.len() != 0 {
+                panic_with_felt252(UpdatePriceFeedsError::InvalidUpdateData.into());
+            }
+
+            let mut output_array = array![];
+            let mut i = 0;
+            while i < price_ids.len() {
+                let value = output.get(i.into());
+                match match_nullable(value) {
+                    FromNullableResult::Null => {
+                        panic_with_felt252(
+                            ParsePriceFeedsError::PriceFeedNotFoundWithinRange.into()
+                        )
+                    },
+                    FromNullableResult::NotNull(value) => { output_array.append(value.unbox()); }
+                }
+                i += 1;
+            };
+            output_array
+        }
     }
 
     fn apply_decimal_expo(value: u64, expo: u64) -> u256 {
@@ -511,4 +596,19 @@ mod pyth {
         };
         actual_age <= age
     }
+
+    fn find_index_of_price_id(ids: @Array<u256>, value: u256) -> Option<usize> {
+        let mut i = 0;
+        while i < ids.len() {
+            if ids.at(i) == @value {
+                break;
+            }
+            i += 1;
+        };
+        if i == ids.len() {
+            Option::None
+        } else {
+            Option::Some(i)
+        }
+    }
 }

+ 15 - 0
target_chains/starknet/contracts/src/pyth/errors.cairo

@@ -98,3 +98,18 @@ impl UpdatePriceFeedsIfNecessaryErrorIntoFelt252 of Into<
         }
     }
 }
+
+#[derive(Copy, Drop, Debug, Serde, PartialEq)]
+pub enum ParsePriceFeedsError {
+    Update: UpdatePriceFeedsError,
+    PriceFeedNotFoundWithinRange,
+}
+
+impl ParsePriceFeedsErrorIntoFelt252 of Into<ParsePriceFeedsError, felt252> {
+    fn into(self: ParsePriceFeedsError) -> felt252 {
+        match self {
+            ParsePriceFeedsError::Update(err) => err.into(),
+            ParsePriceFeedsError::PriceFeedNotFoundWithinRange => 'price feed not found',
+        }
+    }
+}

+ 22 - 1
target_chains/starknet/contracts/src/pyth/interface.cairo

@@ -15,6 +15,16 @@ pub trait IPyth<T> {
     fn update_price_feeds_if_necessary(
         ref self: T, update: ByteArray, required_publish_times: Array<PriceFeedPublishTime>
     );
+    fn parse_price_feed_updates(
+        ref self: T,
+        data: ByteArray,
+        price_ids: Array<u256>,
+        min_publish_time: u64,
+        max_publish_time: u64
+    ) -> Array<PriceFeed>;
+    fn parse_unique_price_feed_updates(
+        ref self: T, data: ByteArray, price_ids: Array<u256>, publish_time: u64, max_staleness: u64,
+    ) -> Array<PriceFeed>;
     fn get_update_fee(self: @T, data: ByteArray) -> u256;
     fn execute_governance_instruction(ref self: T, data: ByteArray);
     fn pyth_upgradable_magic(self: @T) -> u32;
@@ -26,7 +36,7 @@ pub struct DataSource {
     pub emitter_address: u256,
 }
 
-#[derive(Drop, Clone, Serde)]
+#[derive(Drop, Copy, PartialEq, Serde)]
 pub struct Price {
     pub price: i64,
     pub conf: u64,
@@ -39,3 +49,14 @@ pub struct PriceFeedPublishTime {
     pub price_id: u256,
     pub publish_time: u64,
 }
+
+// PriceFeed represents a current aggregate price from pyth publisher feeds.
+#[derive(Drop, Copy, PartialEq, Serde)]
+pub struct PriceFeed {
+    // The price ID.
+    pub id: u256,
+    // Latest available price
+    pub price: Price,
+    // Latest available exponentially-weighted moving average price
+    pub ema_price: Price,
+}

+ 21 - 1
target_chains/starknet/contracts/src/pyth/price_update.cairo

@@ -1,5 +1,5 @@
 use pyth::reader::{Reader, ReaderImpl};
-use pyth::pyth::UpdatePriceFeedsError;
+use pyth::pyth::{UpdatePriceFeedsError, PriceFeed, Price};
 use core::panic_with_felt252;
 use pyth::byte_array::ByteArray;
 use pyth::merkle_tree::read_and_verify_proof;
@@ -136,3 +136,23 @@ pub fn read_and_verify_message(ref reader: Reader, root_digest: u256) -> PriceFe
         price_id, price, conf, expo, publish_time, prev_publish_time, ema_price, ema_conf,
     }
 }
+
+impl PriceFeedMessageIntoPriceFeed of Into<PriceFeedMessage, PriceFeed> {
+    fn into(self: PriceFeedMessage) -> PriceFeed {
+        PriceFeed {
+            id: self.price_id,
+            price: Price {
+                price: self.price,
+                conf: self.conf,
+                expo: self.expo,
+                publish_time: self.publish_time,
+            },
+            ema_price: Price {
+                price: self.ema_price,
+                conf: self.ema_conf,
+                expo: self.expo,
+                publish_time: self.publish_time,
+            },
+        }
+    }
+}

+ 48 - 0
target_chains/starknet/contracts/tests/data.cairo

@@ -89,6 +89,54 @@ pub fn good_vm1() -> ByteArray {
     ByteArrayImpl::new(array_try_into(bytes), 22)
 }
 
+// A first update for a certain timestamp pulled from Hermes.
+pub fn unique_update1() -> ByteArray {
+    let bytes = array![
+        141887862745809943100717722154781668656425228150258363002663887732857548075,
+        399793171101922163607717906910020156439802651815166374105600343045575931912,
+        205983572864866548810075966151139050810706099666354694408986588005072300221,
+        151451952208610765038741735376830560508647207417250420083288609153397964481,
+        86500771940909656434129966404881206990783089169853273096126376095161148476,
+        226128071698991949569342896653857259217290864736270016974365368327197190188,
+        377698250859392108521341636250067678937984182266455992791761951028534274645,
+        359481881021010868573625646624159016709204941239347558851817240293252854322,
+        269752247307988210724584131415546296182395279893478036136383326770680756016,
+        1795390197095010264738527441013169771569683827600670029637766897428840143,
+        234116006474879126519208934707397575502368608154160721412947025189419787194,
+        189800847222104556167598630039931285094024694085247523307780296439180585091,
+        286206863474379560841614954761399331434812535348350225390274576176798886031,
+        380778504466325787198909189418135115031120011427014465236265515817642556890,
+        128785010970678423864351132498736713626005612397319240493515416417380099413,
+        395419216432871057204438489759682910781574046646010114747283889104834443397,
+        184981610545658962928833309057692452941395349433458372962283948260273947156,
+        110573687320157468197346423602708230855113764048934963897254568602798981891,
+        359831064918860887692030922920274851680298668214543004760245859301314852951,
+        430048018037020109398934292236837834709370591653776097569938580165539734124,
+        265079002668517523371293797450754079826401787503533883360533359118093613709,
+        118956066417175616647948432812222980193178970842860785889932661265944570805,
+        289275771653255859826082430219295399339085718287922176066620100061685069367,
+        236281080443323007784750945204995275799519083197981738780888611083509567478,
+        251042542087561162756580709366349731114715604419679060279244203132266921212,
+        98235342442817522140724982668491795525073680697047819016960109902179866805,
+        88342865348230190810084665689239940103607621061956069700600977485132311440,
+        362045407519743532711403801060857872682086780812134177115599591240431143367,
+        16066483776176414842828409371714210177224680637354816226962534075790344474,
+        247660009137502548346315865368477795392972486141407802997108167405894850048,
+        3530678887550352072827758533436734366288448089041832078266099519,
+        272101358674132653417860014541384836605087997364704101839695292681479883518,
+        394112423559676785086059691419737479771752814065338155011845462169193807974,
+        151755140354137577506498286435010205890750984061355535849635897370673003944,
+        210196797635098012510106281616028853575843684847951745405842531072933610917,
+        65848881303037889845233189630325925691014776183743685310081069912626992101,
+        110542381473451658848567609117180625628841970992142907142739541724557861958,
+        157546342890129829983246193527213822449181723932011157069167729323808635205,
+        165998047372192053828934221954381957675361960853286466716634704795532379661,
+        28583007876111384456149499846085318299326698960792831530075402396150538907,
+        126290914008245563820443505,
+    ];
+    ByteArrayImpl::new(array_try_into(bytes), 11)
+}
+
 // An actual mainnet wormhole governance VAA from https://github.com/pyth-network/pyth-crosschain/blob/main/contract_manager/src/contracts/wormhole.ts#L32-L37
 pub fn mainnet_guardian_set_upgrade1() -> ByteArray {
     let bytes = array![

+ 175 - 1
target_chains/starknet/contracts/tests/pyth.cairo

@@ -5,7 +5,7 @@ use snforge_std::{
 use pyth::pyth::{
     IPythDispatcher, IPythDispatcherTrait, DataSource, Event as PythEvent, PriceFeedUpdated,
     WormholeAddressSet, GovernanceDataSourceSet, ContractUpgraded, DataSourcesSet, FeeSet,
-    PriceFeedPublishTime, GetPriceNoOlderThanError,
+    PriceFeedPublishTime, GetPriceNoOlderThanError, Price, PriceFeed,
 };
 use pyth::byte_array::{ByteArray, ByteArrayImpl};
 use pyth::util::{array_try_into, UnwrapWithFelt252};
@@ -173,6 +173,180 @@ fn test_update_if_necessary_works() {
     stop_prank(CheatTarget::One(pyth.contract_address));
 }
 
+#[test]
+fn test_parse_price_feed_updates_works() {
+    let user = 'user'.try_into().unwrap();
+    let wormhole = super::wormhole::deploy_with_mainnet_guardians();
+    let fee_contract = deploy_fee_contract(user);
+    let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
+
+    let fee = pyth.get_update_fee(data::good_update1());
+    assert!(fee == 1000);
+
+    start_prank(CheatTarget::One(fee_contract.contract_address), user.try_into().unwrap());
+    fee_contract.approve(pyth.contract_address, fee);
+    stop_prank(CheatTarget::One(fee_contract.contract_address));
+
+    let mut spy = spy_events(SpyOn::One(pyth.contract_address));
+
+    start_prank(CheatTarget::One(pyth.contract_address), user.try_into().unwrap());
+    let output = pyth
+        .parse_price_feed_updates(
+            data::good_update1(),
+            array![0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43],
+            0,
+            1712589208
+        );
+    stop_prank(CheatTarget::One(pyth.contract_address));
+    assert!(output.len() == 1);
+    let output = output.at(0).clone();
+    let expected = PriceFeed {
+        id: 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,
+        price: Price {
+            price: 7192002930010, conf: 3596501465, expo: -8, publish_time: 1712589206,
+        },
+        ema_price: Price {
+            price: 7181868900000, conf: 4096812700, expo: -8, publish_time: 1712589206,
+        },
+    };
+    assert!(output == expected);
+
+    spy.fetch_events();
+    assert!(spy.events.len() == 1);
+    let (from, event) = spy.events.pop_front().unwrap();
+    assert!(from == pyth.contract_address);
+    let event = decode_event(event);
+    let expected = PriceFeedUpdated {
+        price_id: 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,
+        publish_time: 1712589206,
+        price: 7192002930010,
+        conf: 3596501465,
+    };
+    assert!(event == PythEvent::PriceFeedUpdated(expected));
+
+    let last_price = pyth
+        .get_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
+        .unwrap_with_felt252();
+    assert!(last_price.price == 7192002930010);
+    assert!(last_price.conf == 3596501465);
+    assert!(last_price.expo == -8);
+    assert!(last_price.publish_time == 1712589206);
+
+    let last_ema_price = pyth
+        .get_ema_price_unsafe(0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43)
+        .unwrap_with_felt252();
+    assert!(last_ema_price.price == 7181868900000);
+    assert!(last_ema_price.conf == 4096812700);
+    assert!(last_ema_price.expo == -8);
+    assert!(last_ema_price.publish_time == 1712589206);
+}
+
+#[test]
+#[should_panic(expected: ('price feed not found',))]
+fn test_parse_price_feed_updates_rejects_bad_price_id() {
+    let user = 'user'.try_into().unwrap();
+    let wormhole = super::wormhole::deploy_with_mainnet_guardians();
+    let fee_contract = deploy_fee_contract(user);
+    let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
+
+    let fee = pyth.get_update_fee(data::good_update1());
+    assert!(fee == 1000);
+
+    start_prank(CheatTarget::One(fee_contract.contract_address), user.try_into().unwrap());
+    fee_contract.approve(pyth.contract_address, fee);
+    stop_prank(CheatTarget::One(fee_contract.contract_address));
+
+    start_prank(CheatTarget::One(pyth.contract_address), user.try_into().unwrap());
+    pyth.parse_price_feed_updates(data::good_update1(), array![0x14], 0, 1712589208);
+}
+
+#[test]
+#[should_panic(expected: ('price feed not found',))]
+fn test_parse_price_feed_updates_rejects_out_of_range() {
+    let user = 'user'.try_into().unwrap();
+    let wormhole = super::wormhole::deploy_with_mainnet_guardians();
+    let fee_contract = deploy_fee_contract(user);
+    let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
+
+    let fee = pyth.get_update_fee(data::good_update1());
+    assert!(fee == 1000);
+
+    start_prank(CheatTarget::One(fee_contract.contract_address), user.try_into().unwrap());
+    fee_contract.approve(pyth.contract_address, fee);
+    stop_prank(CheatTarget::One(fee_contract.contract_address));
+
+    start_prank(CheatTarget::One(pyth.contract_address), user.try_into().unwrap());
+    pyth
+        .parse_price_feed_updates(
+            data::good_update1(),
+            array![0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43],
+            0,
+            1712589000
+        );
+}
+
+#[test]
+fn test_parse_price_feed_updates_unique_works() {
+    let user = 'user'.try_into().unwrap();
+    let wormhole = super::wormhole::deploy_with_mainnet_guardians();
+    let fee_contract = deploy_fee_contract(user);
+    let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
+
+    let fee1 = pyth.get_update_fee(data::test_price_update2());
+    assert!(fee1 == 1000);
+
+    start_prank(CheatTarget::One(fee_contract.contract_address), user);
+    fee_contract.approve(pyth.contract_address, 10000);
+    stop_prank(CheatTarget::One(fee_contract.contract_address));
+
+    start_prank(CheatTarget::One(pyth.contract_address), user);
+    let output = pyth
+        .parse_unique_price_feed_updates(
+            data::unique_update1(),
+            array![0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43],
+            1716904943,
+            2,
+        );
+    stop_prank(CheatTarget::One(pyth.contract_address));
+    assert!(output.len() == 1);
+    let output = output.at(0).clone();
+    let expected = PriceFeed {
+        id: 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,
+        price: Price {
+            price: 6751021151231, conf: 7471389383, expo: -8, publish_time: 1716904943,
+        },
+        ema_price: Price {
+            price: 6815630100000, conf: 6236878200, expo: -8, publish_time: 1716904943,
+        },
+    };
+    assert!(output == expected);
+}
+
+#[test]
+#[should_panic(expected: ('price feed not found',))]
+fn test_parse_price_feed_updates_unique_rejects_non_unique() {
+    let user = 'user'.try_into().unwrap();
+    let wormhole = super::wormhole::deploy_with_mainnet_guardians();
+    let fee_contract = deploy_fee_contract(user);
+    let pyth = deploy_default(wormhole.contract_address, fee_contract.contract_address);
+
+    let fee1 = pyth.get_update_fee(data::test_price_update2());
+    assert!(fee1 == 1000);
+
+    start_prank(CheatTarget::One(fee_contract.contract_address), user);
+    fee_contract.approve(pyth.contract_address, 10000);
+    stop_prank(CheatTarget::One(fee_contract.contract_address));
+
+    start_prank(CheatTarget::One(pyth.contract_address), user);
+    pyth
+        .parse_unique_price_feed_updates(
+            data::good_update1(),
+            array![0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43],
+            1712589206,
+            2,
+        );
+}
+
 #[test]
 #[should_panic(expected: ('no fresh update',))]
 fn test_update_if_necessary_rejects_empty() {

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
target_chains/starknet/tools/test_vaas/src/bin/generate_test_data.rs


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä