Parcourir la source

[near] Add receiver contract (#407)

near: add initial near receiver
Reisen il y a 2 ans
Parent
commit
c92329c7c4

+ 40 - 0
near/receiver/Cargo.toml

@@ -0,0 +1,40 @@
+[package]
+name            = "pyth-near"
+version         = "0.1.0"
+authors         = ["Pyth Data Association"]
+edition         = "2021"
+description     = "A Pyth Receiver for Near"
+
+[lib]
+name            = "pyth"
+crate-type      = ["cdylib", "lib"]
+
+[dependencies]
+byteorder       = { version = "1.4.3" }
+hex             = { version = "0.4.3" }
+near-sdk        = { version = "4.1.1" }
+p2w-sdk         = { path = "../../third_party/pyth/p2w-sdk/rust" }
+pyth-sdk        = { version = "0.7.0" }
+serde_wormhole  = { git = "https://github.com/wormhole-foundation/wormhole" }
+thiserror       = { version = "1.0.38" }
+wormhole-core   = { git = "https://github.com/wormhole-foundation/wormhole" }
+
+[patch.crates-io]
+serde_wormhole  = { git = "https://github.com/wormhole-foundation/wormhole" }
+
+[dev-dependencies]
+lazy_static     = { version = "1.4.0" }
+serde_json      = { version = "1.0.91" }
+serde           = { version = "1.0.152", features = ["derive"] }
+tokio           = { version = "1.23.0", features = ["full"] }
+serde_wormhole  = { git = "https://github.com/wormhole-foundation/wormhole" }
+workspaces      = { version = "0.7.0" }
+wormhole-core   = { git = "https://github.com/wormhole-foundation/wormhole" }
+
+[profile.release]
+codegen-units   = 1
+opt-level       = 3
+lto             = "fat"
+debug           = false
+panic           = "abort"
+overflow-checks = true

+ 43 - 0
near/receiver/src/error.rs

@@ -0,0 +1,43 @@
+use {
+    near_sdk::{
+        serde::Serialize,
+        FunctionError,
+    },
+    thiserror::Error,
+};
+
+#[derive(Error, Debug, Serialize, FunctionError)]
+#[serde(crate = "near_sdk::serde")]
+pub enum Error {
+    #[error("A hex argument could not be decoded.")]
+    InvalidHex,
+
+    #[error("A VAA could not be deserialized.")]
+    InvalidVaa,
+
+    #[error("A VAA payload could not be deserialized.")]
+    InvalidPayload,
+
+    #[error("Source for attestation is not allowed.")]
+    UnknownSource,
+
+    #[error("Unauthorized Upgrade")]
+    UnauthorizedUpgrade,
+
+    #[error("Insufficient tokens deposited to cover storage.")]
+    InsufficientDeposit,
+
+    #[error("VAA verification failed.")]
+    VaaVerificationFailed,
+
+    #[error("Fee is too large.")]
+    FeeTooLarge,
+}
+
+/// Convert IO errors into Payload errors, the only I/O we do is parsing with `Cursor` so this is a
+/// reasonable conversion.
+impl From<std::io::Error> for Error {
+    fn from(_: std::io::Error) -> Self {
+        Error::InvalidPayload
+    }
+}

+ 13 - 0
near/receiver/src/ext.rs

@@ -0,0 +1,13 @@
+//! This module defines external contract API's that are used by the contract. This includes
+//! Wormhole and perhaps any ancillary Pyth contracts.
+
+use near_sdk::ext_contract;
+
+/// Defines the external contract API we care about for interacting with Wormhole. Note that
+/// Wormhole on NEAR passes VAA's as hex encoded strings so that the explorer can display them in a
+/// clean way. This may require juggling between Vec<u8> and HexString.
+#[ext_contract(ext_wormhole)]
+pub trait Wormhole {
+    /// Returns the Governance Index of the current GuardianSet only if the VAA verifies.
+    fn verify_vaa(&self, vaa: String) -> u32;
+}

+ 385 - 0
near/receiver/src/governance.rs

@@ -0,0 +1,385 @@
+//! Implement Governance Processing
+
+use {
+    crate::{
+        error::Error,
+        ext::ext_wormhole,
+        state::{
+            Chain,
+            Source,
+            Vaa,
+        },
+        Pyth,
+        PythExt,
+    },
+    byteorder::{
+        BigEndian,
+        ReadBytesExt,
+        WriteBytesExt,
+    },
+    near_sdk::{
+        borsh::{
+            self,
+            BorshDeserialize,
+            BorshSerialize,
+        },
+        env,
+        is_promise_success,
+        near_bindgen,
+        serde::{
+            Deserialize,
+            Serialize,
+        },
+        AccountId,
+        Gas,
+        Promise,
+    },
+    std::io::Read,
+    wormhole::Chain as WormholeChain,
+};
+
+/// Magic Header for identifying Governance VAAs.
+const GOVERNANCE_MAGIC: [u8; 4] = [0x50, 0x54, 0x47, 0x4d];
+
+/// ID for the module this contract identifies as: Pyth Receiver (0x1).
+const GOVERNANCE_MODULE: u8 = 0x01;
+
+/// Enumeration of IDs for different governance actions.
+#[repr(u8)]
+pub enum ActionId {
+    ContractUpgrade        = 0,
+    SetDataSources         = 1,
+    SetGovernanceSource    = 2,
+    SetStalePriceThreshold = 3,
+    SetUpdateFee           = 4,
+}
+
+impl TryInto<ActionId> for u8 {
+    type Error = Error;
+    fn try_into(self) -> Result<ActionId, Error> {
+        match self {
+            0 => Ok(ActionId::ContractUpgrade),
+            1 => Ok(ActionId::SetDataSources),
+            2 => Ok(ActionId::SetGovernanceSource),
+            3 => Ok(ActionId::SetStalePriceThreshold),
+            4 => Ok(ActionId::SetUpdateFee),
+            _ => Err(Error::InvalidPayload),
+        }
+    }
+}
+
+impl From<ActionId> for u8 {
+    fn from(val: ActionId) -> Self {
+        val as u8
+    }
+}
+
+/// A `GovernanceAction` represents the different actions that can be voted on and executed by the
+/// governance system.
+#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
+#[serde(crate = "near_sdk::serde")]
+pub enum GovernanceAction {
+    ContractUpgrade([u8; 32]),
+    SetDataSources(Vec<Source>),
+    SetGovernanceSource(Source),
+    SetStalePriceThreshold(u64),
+    SetUpdateFee(u64),
+}
+
+impl GovernanceAction {
+    pub fn id(&self) -> ActionId {
+        match self {
+            GovernanceAction::ContractUpgrade(_) => ActionId::ContractUpgrade,
+            GovernanceAction::SetDataSources(_) => ActionId::SetDataSources,
+            GovernanceAction::SetGovernanceSource(_) => ActionId::SetGovernanceSource,
+            GovernanceAction::SetStalePriceThreshold(_) => ActionId::SetStalePriceThreshold,
+            GovernanceAction::SetUpdateFee(_) => ActionId::SetUpdateFee,
+        }
+    }
+
+    pub fn deserialize(data: &[u8]) -> Result<Self, Error> {
+        let mut cursor = std::io::Cursor::new(data);
+        let magic = cursor.read_u32::<BigEndian>()?;
+        let module = cursor.read_u8()?;
+        let action = cursor.read_u8()?.try_into()?;
+        let target = cursor.read_u16::<BigEndian>()?;
+
+        assert!(module == GOVERNANCE_MODULE);
+        assert!(target == 0 || target == u16::from(WormholeChain::Near));
+        assert!(magic == u32::from_le_bytes(GOVERNANCE_MAGIC));
+
+        Ok(match action {
+            ActionId::ContractUpgrade => {
+                let mut hash = [0u8; 32];
+                cursor.read_exact(&mut hash)?;
+                Self::ContractUpgrade(hash)
+            }
+
+            ActionId::SetDataSources => {
+                let mut sources = Vec::new();
+                let count = cursor.read_u8()?;
+
+                for _ in 0..count {
+                    let mut emitter = [0u8; 32];
+                    cursor.read_exact(&mut emitter)?;
+                    sources.push(Source {
+                        emitter,
+                        pyth_emitter_chain: Chain::from(WormholeChain::from(
+                            cursor.read_u16::<BigEndian>()?,
+                        )),
+                    });
+                }
+
+                Self::SetDataSources(sources)
+            }
+
+            ActionId::SetGovernanceSource => {
+                let mut emitter = [0u8; 32];
+                cursor.read_exact(&mut emitter)?;
+                Self::SetGovernanceSource(Source {
+                    emitter,
+                    pyth_emitter_chain: Chain(cursor.read_u16::<BigEndian>()?),
+                })
+            }
+
+            ActionId::SetStalePriceThreshold => {
+                let stale_price_threshold = cursor.read_u64::<BigEndian>()?;
+                Self::SetStalePriceThreshold(stale_price_threshold)
+            }
+
+            ActionId::SetUpdateFee => {
+                let update_fee = cursor.read_u64::<BigEndian>()?;
+                Self::SetUpdateFee(update_fee)
+            }
+        })
+    }
+
+    pub fn serialize(&self) -> Vec<u8> {
+        let mut data = Vec::new();
+        let magic = u32::from_le_bytes(GOVERNANCE_MAGIC);
+        data.write_u32::<BigEndian>(magic).unwrap();
+        data.push(GOVERNANCE_MODULE);
+        data.push(self.id() as u8);
+        data.extend_from_slice(&0u16.to_le_bytes());
+
+        match self {
+            Self::ContractUpgrade(hash) => {
+                data.extend_from_slice(hash);
+            }
+
+            Self::SetDataSources(sources) => {
+                data.push(sources.len() as u8);
+                for source in sources {
+                    data.extend_from_slice(&source.emitter);
+                    data.extend_from_slice(&source.pyth_emitter_chain.0.to_le_bytes());
+                }
+            }
+
+            Self::SetGovernanceSource(source) => {
+                data.extend_from_slice(&source.emitter);
+                data.extend_from_slice(&source.pyth_emitter_chain.0.to_le_bytes());
+            }
+
+            Self::SetStalePriceThreshold(stale_price_threshold) => {
+                data.extend_from_slice(&stale_price_threshold.to_le_bytes());
+            }
+
+            Self::SetUpdateFee(update_fee) => {
+                data.extend_from_slice(&update_fee.to_le_bytes());
+            }
+        }
+
+        data
+    }
+}
+
+#[near_bindgen]
+impl Pyth {
+    /// Instruction for processing Governance VAA's relayed via Wormhole.
+    ///
+    /// Note that VAA verification requires calling Wormhole so processing of the VAA itself is
+    /// done in a callback handler, see `process_vaa_callback`.
+    #[payable]
+    #[handle_result]
+    pub fn execute_governance_instruction(&mut self, vaa: String) -> Result<(), Error> {
+        // Verify the VAA is coming from a trusted source chain before attempting to verify VAA
+        // signatures. Avoids a cross-contract call early.
+        {
+            let vaa = hex::decode(&vaa).map_err(|_| Error::InvalidHex)?;
+            let vaa = serde_wormhole::from_slice_with_payload::<wormhole::Vaa<()>>(&vaa);
+            let vaa = vaa.map_err(|_| Error::InvalidVaa)?;
+            let (vaa, _rest) = vaa;
+
+            // Convert to local VAA type to catch APi changes.
+            let vaa = Vaa::from(vaa);
+
+            // Prevent VAA re-execution.
+            if self.executed_gov_sequences.contains(&vaa.sequence) {
+                return Err(Error::VaaVerificationFailed);
+            }
+
+            // Confirm the VAA is coming from a trusted source chain.
+            if self.gov_source
+                != (Source {
+                    emitter:            vaa.emitter_address,
+                    pyth_emitter_chain: vaa.emitter_chain,
+                })
+            {
+                return Err(Error::UnknownSource);
+            }
+
+            // Insert before calling Wormhole to prevent re-execution. If we wait until after the
+            // Wormhole call we could end up with multiple VAA's with the same sequence being
+            // executed in parallel.
+            self.executed_gov_sequences.insert(&vaa.sequence);
+        }
+
+        // Verify VAA and refund the caller in case of failure.
+        ext_wormhole::ext(self.wormhole.clone())
+            .with_static_gas(Gas(30_000_000_000_000))
+            .verify_vaa(vaa.clone())
+            .then(
+                Self::ext(env::current_account_id())
+                    .with_static_gas(Gas(30_000_000_000_000))
+                    .with_attached_deposit(env::attached_deposit())
+                    .verify_gov_vaa_callback(env::predecessor_account_id(), vaa),
+            )
+            .then(
+                Self::ext(env::current_account_id())
+                    .with_static_gas(Gas(30_000_000_000_000))
+                    .refund_vaa(env::predecessor_account_id(), env::attached_deposit()),
+            );
+
+        Ok(())
+    }
+
+    /// Invoke handler upon successful verification of a VAA action.
+    #[payable]
+    #[private]
+    #[handle_result]
+    pub fn verify_gov_vaa_callback(
+        &mut self,
+        account_id: AccountId,
+        vaa: String,
+        #[callback_result] _result: Result<u32, near_sdk::PromiseError>,
+    ) -> Result<(), Error> {
+        use GovernanceAction::*;
+
+        if !is_promise_success() {
+            return Err(Error::VaaVerificationFailed);
+        }
+
+        // Get Storage Usage before execution.
+        let storage = env::storage_usage();
+
+        // Deserialize VAA, note that we already deserialized and verified the VAA in `process_vaa`
+        // at this point so we only care about the `rest` component which contains bytes we can
+        // deserialize into an Action.
+        let vaa = hex::decode(&vaa).unwrap();
+        let (_, rest): (wormhole::Vaa<()>, _) =
+            serde_wormhole::from_slice_with_payload(&vaa).map_err(|_| Error::InvalidPayload)?;
+
+        match GovernanceAction::deserialize(rest)? {
+            ContractUpgrade(codehash) => self.set_upgrade_hash(codehash),
+            SetDataSources(sources) => self.set_sources(sources),
+            SetGovernanceSource(source) => self.set_gov_source(source),
+            SetStalePriceThreshold(threshold) => self.set_stale_price_threshold(threshold),
+            SetUpdateFee(fee) => self.set_update_fee(fee),
+        }
+
+        // Refund storage difference to `account_id` after storage execution.
+        self.refund_storage_usage(
+            account_id,
+            storage,
+            env::storage_usage(),
+            env::attached_deposit(),
+        )
+    }
+
+    /// If submitting an action fails then this callback will refund the caller.
+    #[private]
+    pub fn refund_vaa(&mut self, account_id: AccountId, amount: u128) {
+        if !is_promise_success() {
+            // No calculations needed as deposit size will have not changed. Can just refund the
+            // whole deposit amount.
+            Promise::new(account_id).transfer(amount);
+        }
+    }
+
+    #[private]
+    pub fn set_upgrade_hash(&mut self, codehash: [u8; 32]) {
+        self.codehash = codehash;
+    }
+
+    #[private]
+    pub fn set_gov_source(&mut self, source: Source) {
+        self.gov_source = source;
+    }
+
+    #[private]
+    pub fn set_stale_price_threshold(&mut self, threshold: u64) {
+        self.stale_threshold = threshold;
+    }
+
+    #[private]
+    pub fn set_update_fee(&mut self, fee: u64) {
+        self.update_fee = fee;
+    }
+
+    #[private]
+    #[handle_result]
+    pub fn set_sources(&mut self, sources: Vec<Source>) {
+        self.sources.clear();
+        sources.iter().for_each(|s| {
+            self.sources.insert(s);
+        });
+    }
+
+    /// This method allows self-upgrading the contract to a new implementation.
+    ///
+    /// This function is open to call by anyone, but to perform an authorized upgrade a VAA
+    /// containing the hash of the `new_code` must have previously been relayed to this contract's
+    /// `process_vaa` endpoint. otherwise the upgrade will fail.
+    ///
+    /// NOTE: This function is pub only within crate scope so that it can only be called by the
+    /// `upgrade_contract` method, this is much much cheaper than serializing a Vec<u8> to call
+    /// this method as a normal public method.
+    #[handle_result]
+    pub(crate) fn upgrade(&mut self, new_code: Vec<u8>) -> Result<Promise, Error> {
+        let signature = env::sha256(&new_code);
+
+        if signature != self.codehash {
+            return Err(Error::UnauthorizedUpgrade);
+        }
+
+        Ok(Promise::new(env::current_account_id())
+            .deploy_contract(new_code)
+            .then(Self::ext(env::current_account_id()).refund_upgrade(
+                env::predecessor_account_id(),
+                env::attached_deposit(),
+                env::storage_usage(),
+            )))
+    }
+
+    /// This method is called after the upgrade to refund the caller for the storage used by the
+    /// old contract.
+    #[private]
+    #[handle_result]
+    pub fn refund_upgrade(
+        &mut self,
+        account_id: AccountId,
+        amount: u128,
+        storage: u64,
+    ) -> Result<(), Error> {
+        self.refund_storage_usage(account_id, storage, env::storage_usage(), amount)
+    }
+}
+
+impl Pyth {
+    #[allow(dead_code)]
+    fn is_valid_governance_source(&self, source: &Source) -> Result<(), Error> {
+        (self.gov_source == *source)
+            .then_some(())
+            .ok_or(Error::UnknownSource)
+    }
+}

+ 388 - 0
near/receiver/src/lib.rs

@@ -0,0 +1,388 @@
+//#![deny(warnings)]
+
+use {
+    error::Error,
+    ext::ext_wormhole,
+    near_sdk::{
+        borsh::{
+            self,
+            BorshDeserialize,
+            BorshSerialize,
+        },
+        collections::{
+            UnorderedMap,
+            UnorderedSet,
+        },
+        env,
+        is_promise_success,
+        log,
+        near_bindgen,
+        AccountId,
+        Balance,
+        BorshStorageKey,
+        Duration,
+        Gas,
+        PanicOnDefault,
+        Promise,
+        StorageUsage,
+    },
+    p2w_sdk::BatchPriceAttestation,
+    state::{
+        Price,
+        PriceFeed,
+        PriceIdentifier,
+        Source,
+        Vaa,
+    },
+    std::io::Cursor,
+};
+
+pub mod error;
+pub mod ext;
+pub mod governance;
+pub mod state;
+pub mod tests;
+
+#[derive(BorshSerialize, BorshStorageKey)]
+enum StorageKeys {
+    Source,
+    Prices,
+    Governance,
+}
+
+/// The `State` contains all persisted state for the contract. This includes runtime configuration.
+///
+/// There is no valid Default state for this contract, so we derive PanicOnDefault to force
+/// deployment using one of the #[init] functions in the impl below.
+#[near_bindgen]
+#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
+pub struct Pyth {
+    /// The set of `Source`s from which Pyth attestations are allowed to be relayed from.
+    sources: UnorderedSet<Source>,
+
+    /// The Governance Source.
+    gov_source: Source,
+
+    /// The last executed sequence number for governance actions.
+    executed_gov_sequences: UnorderedSet<u64>,
+
+    /// A Mapping from PriceFeed ID to Price Info.
+    prices: UnorderedMap<PriceIdentifier, PriceFeed>,
+
+    /// The `AccountId` of the Wormhole contract used to verify VAA's.
+    wormhole: AccountId,
+
+    /// A hash of the current contract code.
+    ///
+    /// If this `hash` does not match the current contract code, then it indicates that a pending
+    /// upgrade has been allowed, calling `upgrade` with code that matches this hash will cause the
+    /// contract to upgrade itself.
+    codehash: [u8; 32],
+
+    /// Staleness threshold for rejecting price updates.
+    stale_threshold: Duration,
+
+    /// Fee for updating price.
+    update_fee: u64,
+}
+
+#[near_bindgen]
+impl Pyth {
+    #[init]
+    #[allow(clippy::new_without_default)]
+    pub fn new(
+        wormhole: AccountId,
+        codehash: [u8; 32],
+        initial_source: Source,
+        gov_source: Source,
+        update_fee: u64,
+        stale_threshold: u64,
+    ) -> Self {
+        // Add an initial Source so that the contract can be used.
+        let mut sources = UnorderedSet::new(StorageKeys::Source);
+        sources.insert(&initial_source);
+        Self {
+            prices: UnorderedMap::new(StorageKeys::Prices),
+            executed_gov_sequences: UnorderedSet::new(StorageKeys::Governance),
+            stale_threshold,
+            gov_source,
+            sources,
+            wormhole,
+            codehash,
+            update_fee,
+        }
+    }
+
+    #[init(ignore_state)]
+    pub fn migrate() -> Self {
+        let state: Self = env::state_read().expect("Failed to read state");
+        state
+    }
+
+    /// Instruction for processing VAA's relayed via Wormhole.
+    ///
+    /// Note that VAA verification requires calling Wormhole so processing of the VAA itself is
+    /// done in a callback handler, see `process_vaa_callback`.
+    #[payable]
+    #[handle_result]
+    pub fn update_price_feed(&mut self, vaa_hex: String) -> Result<(), Error> {
+        // We Verify the VAA is coming from a trusted source chain before attempting to verify
+        // VAA signatures. Avoids a cross-contract call early.
+        let vaa = hex::decode(&vaa_hex).map_err(|_| Error::InvalidHex)?;
+        let vaa = serde_wormhole::from_slice_with_payload::<wormhole::Vaa<()>>(&vaa);
+        let vaa = vaa.map_err(|_| Error::InvalidVaa)?;
+        let (vaa, _rest) = vaa;
+
+        // Convert to local VAA type to catch APi changes.
+        let vaa = Vaa::from(vaa);
+
+        if !self.sources.contains(&Source {
+            emitter:            vaa.emitter_address,
+            pyth_emitter_chain: vaa.emitter_chain,
+        }) {
+            return Err(Error::UnknownSource);
+        }
+
+        // Verify VAA and refund the caller in case of failure.
+        ext_wormhole::ext(self.wormhole.clone())
+            .with_static_gas(Gas(30_000_000_000_000))
+            .verify_vaa(vaa_hex.clone())
+            .then(
+                Self::ext(env::current_account_id())
+                    .with_static_gas(Gas(30_000_000_000_000))
+                    .with_attached_deposit(env::attached_deposit())
+                    .verify_vaa_callback(env::predecessor_account_id(), vaa_hex),
+            )
+            .then(
+                Self::ext(env::current_account_id())
+                    .with_static_gas(Gas(30_000_000_000_000))
+                    .refund_vaa(env::predecessor_account_id(), env::attached_deposit()),
+            );
+
+        Ok(())
+    }
+
+    /// Return the deposit required to update a price feed.
+    pub fn get_update_fee_estimate(&self) -> u64 {
+        let byte_cost = env::storage_byte_cost();
+        let data_cost = byte_cost * std::mem::size_of::<PriceFeed>() as u128;
+        4u64 * u64::try_from(data_cost).unwrap() + self.update_fee
+    }
+
+    #[payable]
+    #[private]
+    #[handle_result]
+    pub fn verify_vaa_callback(
+        &mut self,
+        account_id: AccountId,
+        vaa: String,
+        #[callback_result] _result: Result<u32, near_sdk::PromiseError>,
+    ) -> Result<(), Error> {
+        if !is_promise_success() {
+            return Err(Error::VaaVerificationFailed);
+        }
+
+        // Get Storage Usage before execution, subtracting the fee from the deposit has the effect
+        // forces the caller to add the required fee to the deposit.
+        let storage = env::storage_usage()
+            .checked_sub(self.update_fee)
+            .ok_or(Error::InsufficientDeposit)?;
+
+        // Deserialize VAA, note that we already deserialized and verified the VAA in `process_vaa`
+        // at this point so we only care about the `rest` component which contains bytes we can
+        // deserialize into an Action.
+        let vaa = hex::decode(&vaa).unwrap();
+        let (_, rest): (wormhole::Vaa<()>, _) =
+            serde_wormhole::from_slice_with_payload(&vaa).unwrap();
+
+        // Attempt to deserialize the Batch of Price Attestations.
+        let bytes = &mut Cursor::new(rest);
+        let batch = BatchPriceAttestation::deserialize(bytes).unwrap();
+
+        // Verify the PriceAttestation's are new enough, and if so, store them.
+        let mut count_updates = 0;
+        for price_attestation in &batch.price_attestations {
+            if self.update_price_feed_if_new(PriceFeed::from(price_attestation)) {
+                count_updates += 1;
+            }
+        }
+
+        log!(
+            r#"
+            {{
+                "standard": "pyth",
+                "version":  "1.0",
+                "event":    "BatchAttest",
+                "data":     {{
+                    "count": {},
+                    "diffs": {},
+                    "costs": {},
+                }}
+            }}
+        "#,
+            count_updates,
+            env::storage_usage() - storage,
+            env::storage_byte_cost() * (env::storage_usage() - storage) as u128,
+        );
+
+        // Refund storage difference to `account_id` after storage execution.
+        self.refund_storage_usage(
+            account_id,
+            storage,
+            env::storage_usage(),
+            env::attached_deposit(),
+        )
+    }
+
+    /// Read the list of accepted `Source` chains for a price attestation.
+    pub fn get_sources(&self) -> Vec<Source> {
+        self.sources.iter().collect()
+    }
+
+    /// Get the current staleness threshold.
+    pub fn get_stale_threshold(&self) -> u64 {
+        self.stale_threshold
+    }
+
+    /// Determine if a price feed for the given price_identifier exists
+    pub fn price_feed_exists(&self, price_identifier: PriceIdentifier) -> bool {
+        self.prices.get(&price_identifier).is_some()
+    }
+
+    /// Get the latest available price cached for the given price identifier, if that price is
+    /// no older than the stale price threshold.
+    ///
+    /// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for
+    /// how to how this price safely.
+    ///
+    /// IMPORTANT:
+    /// Pyth uses an on-demand update model, where consumers need to update the cached prices
+    /// before using them. Please read more about this at https://docs.pyth.network/consume-data/on-demand.
+    pub fn get_price(&self, price_identifier: PriceIdentifier) -> Option<Price> {
+        self.get_price_no_older_than(price_identifier, self.stale_threshold)
+    }
+
+    /// Get the latest available price cached for the given price identifier.
+    ///
+    /// WARNING:
+    ///
+    /// the returned price can be from arbitrarily far in the past. This function makes no
+    /// guarantees that the returned price is recent or useful for any particular application.
+    /// Users of this function should check the returned timestamp to ensure that the returned
+    /// price is sufficiently recent for their application. The checked get_price_no_older_than()
+    /// function should be used in preference to this.
+    pub fn get_price_unsafe(&self, price_identifier: PriceIdentifier) -> Option<Price> {
+        self.get_price_no_older_than(price_identifier, u64::MAX)
+    }
+
+    /// Get the latest available price cached for the given price identifier, if that price is
+    /// no older than the given age.
+    pub fn get_price_no_older_than(&self, price_id: PriceIdentifier, age: u64) -> Option<Price> {
+        self.prices.get(&price_id).and_then(|feed| {
+            let block_timestamp = env::block_timestamp() / 1_000_000_000;
+            let price_timestamp = feed.price.timestamp;
+
+            // - If Price older than STALENESS_THRESHOLD, set status to Unknown.
+            // - If Price newer than now by more than STALENESS_THRESHOLD, set status to Unknown.
+            // - Any other price around the current time is considered valid.
+            if u64::abs_diff(block_timestamp, price_timestamp) > age {
+                return None;
+            }
+
+            Some(feed.price)
+        })
+    }
+
+    /// EMA version of `get_price`.
+    pub fn get_ema_price(&self, price_id: PriceIdentifier) -> Option<Price> {
+        self.get_ema_price_no_older_than(price_id, self.get_stale_threshold())
+    }
+
+    /// EMA version of `get_price_unsafe`.
+    pub fn get_ema_price_unsafe(&self, price_id: PriceIdentifier) -> Option<Price> {
+        self.get_ema_price_no_older_than(price_id, u64::MAX)
+    }
+
+    /// EMA version of `get_price_no_older_than`.
+    pub fn get_ema_price_no_older_than(
+        &self,
+        price_id: PriceIdentifier,
+        age: u64,
+    ) -> Option<Price> {
+        self.prices.get(&price_id).and_then(|feed| {
+            let block_timestamp = env::block_timestamp();
+            let price_timestamp = feed.ema_price.timestamp;
+
+            // - If Price older than STALENESS_THRESHOLD, set status to Unknown.
+            // - If Price newer than now by more than STALENESS_THRESHOLD, set status to Unknown.
+            // - Any other price around the current time is considered valid.
+            if u64::abs_diff(block_timestamp, price_timestamp) > age {
+                return None;
+            }
+
+            Some(feed.ema_price)
+        })
+    }
+}
+
+/// This second `impl Pyth` block contains only private methods that are called internally that
+/// have no transaction semantics associated with them. Note that these do not need `#[private]`
+/// annotations as they are already uncallable.
+impl Pyth {
+    /// Updates the Price Feed only if it is newer than the current one. This function never fails
+    /// and will either update in-place or not update at all. The return value indicates whether
+    /// the update was performed or not.
+    fn update_price_feed_if_new(&mut self, price_feed: PriceFeed) -> bool {
+        match self.prices.get(&price_feed.id) {
+            Some(stored_price_feed) => {
+                let update = price_feed.price.timestamp > stored_price_feed.price.timestamp;
+                update.then(|| self.prices.insert(&price_feed.id, &price_feed));
+                update
+            }
+
+            None => {
+                self.prices.insert(&price_feed.id, &price_feed);
+                true
+            }
+        }
+    }
+
+    /// Checks storage usage invariants and additionally refunds the caller if they overpay.
+    fn refund_storage_usage(
+        &self,
+        refunder: AccountId,
+        before: StorageUsage,
+        after: StorageUsage,
+        deposit: Balance,
+    ) -> Result<(), Error> {
+        if let Some(diff) = after.checked_sub(before) {
+            // Handle storage increases if checked_sub succeeds.
+            let cost = Balance::from(diff);
+            let cost = cost * env::storage_byte_cost();
+
+            // If the cost is higher than the deposit we bail.
+            if cost > deposit {
+                return Err(Error::InsufficientDeposit);
+            }
+
+            // Otherwise we refund whatever is left over.
+            if deposit - cost > 0 {
+                Promise::new(refunder).transfer(cost);
+            }
+        } else {
+            // Handle storage decrease if checked_sub fails. We know storage used now is <=
+            let refund = Balance::from(before - after);
+            let refund = refund * env::storage_byte_cost();
+            Promise::new(refunder).transfer(refund);
+        }
+
+        Ok(())
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn update_contract() {
+    env::setup_panic_hook();
+    let mut contract: Pyth = env::state_read().expect("Failed to Read State");
+    contract.upgrade(env::input().unwrap()).unwrap();
+}

+ 163 - 0
near/receiver/src/state.rs

@@ -0,0 +1,163 @@
+use {
+    near_sdk::{
+        borsh::{
+            self,
+            BorshDeserialize,
+            BorshSerialize,
+        },
+        serde::{
+            Deserialize,
+            Serialize,
+        },
+    },
+    p2w_sdk::PriceAttestation,
+    wormhole::Chain as WormholeChain,
+};
+
+/// Type alias for Wormhole's compact Signature format.
+pub type WormholeSignature = [u8; 65];
+
+/// Type alias for Wormhole's cross-chain 32-byte address.
+pub type WormholeAddress = [u8; 32];
+
+#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
+#[serde(crate = "near_sdk::serde")]
+#[repr(transparent)]
+pub struct PriceIdentifier(pub [u8; 32]);
+
+/// A price with a degree of uncertainty, represented as a price +- a confidence interval.
+///
+/// The confidence interval roughly corresponds to the standard error of a normal distribution.
+/// Both the price and confidence are stored in a fixed-point numeric representation,
+/// `x * (10^expo)`, where `expo` is the exponent.
+//
+/// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how
+/// to how this price safely.
+#[derive(BorshDeserialize, BorshSerialize, Debug, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(crate = "near_sdk::serde")]
+pub struct Price {
+    pub price:     i64,
+    /// Confidence interval around the price
+    pub conf:      u64,
+    /// The exponent
+    pub expo:      i32,
+    /// Unix timestamp of when this price was computed
+    pub timestamp: u64,
+}
+
+/// The PriceFeed structure is stored in the contract under a Price Feed Identifier.
+///
+/// This structure matches the layout of the PriceFeed structure in other Pyth receiver contracts
+/// but uses types that are native to NEAR.
+#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
+#[serde(crate = "near_sdk::serde")]
+pub struct PriceFeed {
+    /// Unique identifier for this price.
+    pub id:        PriceIdentifier,
+    /// The current aggregation price.
+    pub price:     Price,
+    /// Exponentially moving average price.
+    pub ema_price: Price,
+}
+
+// TODO: Source the Timestamp
+impl From<&PriceAttestation> for PriceFeed {
+    fn from(price_attestation: &PriceAttestation) -> Self {
+        Self {
+            id:        PriceIdentifier(price_attestation.price_id.to_bytes()),
+            price:     Price {
+                price:     price_attestation.price,
+                conf:      price_attestation.conf,
+                expo:      price_attestation.expo,
+                timestamp: price_attestation.publish_time.try_into().unwrap(),
+            },
+            ema_price: Price {
+                price:     price_attestation.ema_price,
+                conf:      price_attestation.ema_conf,
+                expo:      price_attestation.expo,
+                timestamp: price_attestation.publish_time.try_into().unwrap(),
+            },
+        }
+    }
+}
+
+/// A wrapper around a 16bit chain identifier. We can't use Chain from the Wormhole SDK as it does
+/// not provide borsh serialization but we can re-wrap it here relying on the validation from
+/// `wormhole::Chain`.
+#[derive(
+    BorshDeserialize,
+    BorshSerialize,
+    Clone,
+    Debug,
+    Default,
+    Deserialize,
+    Eq,
+    Hash,
+    PartialEq,
+    PartialOrd,
+    Serialize,
+)]
+#[serde(crate = "near_sdk::serde")]
+#[repr(transparent)]
+pub struct Chain(pub u16);
+
+impl From<WormholeChain> for Chain {
+    fn from(chain: WormholeChain) -> Self {
+        Self(u16::from(chain))
+    }
+}
+
+/// A `Source` describes an origin chain from which Pyth attestations are allowed.
+///
+/// This allows for example Pyth prices to be sent from either Pythnet or Solana, but can be used
+/// to add any additional trusted source chains.
+#[derive(
+    BorshDeserialize,
+    BorshSerialize,
+    Clone,
+    Debug,
+    Default,
+    Deserialize,
+    Eq,
+    Hash,
+    PartialEq,
+    PartialOrd,
+    Serialize,
+)]
+#[serde(crate = "near_sdk::serde")]
+pub struct Source {
+    pub emitter:            WormholeAddress,
+    pub pyth_emitter_chain: Chain,
+}
+
+/// A local `Vaa` type converted to from the Wormhole definition, this helps catch any upstream
+/// changes to the Wormhole VAA format.
+pub struct Vaa<P> {
+    pub version:            u8,
+    pub guardian_set_index: u32,
+    pub signatures:         Vec<WormholeSignature>,
+    pub timestamp:          u32, // Seconds since UNIX epoch
+    pub nonce:              u32,
+    pub emitter_chain:      Chain,
+    pub emitter_address:    WormholeAddress,
+    pub sequence:           u64,
+    pub consistency_level:  u8,
+    pub payload:            P,
+}
+
+impl<P> From<wormhole::Vaa<P>> for Vaa<P> {
+    fn from(vaa: wormhole::Vaa<P>) -> Self {
+        Self {
+            version:            vaa.version,
+            guardian_set_index: vaa.guardian_set_index,
+            signatures:         vaa.signatures.iter().map(|s| s.signature).collect(),
+            timestamp:          vaa.timestamp,
+            nonce:              vaa.nonce,
+            emitter_chain:      vaa.emitter_chain.into(),
+            emitter_address:    vaa.emitter_address.0,
+            sequence:           vaa.sequence,
+            consistency_level:  vaa.consistency_level,
+            payload:            vaa.payload,
+        }
+    }
+}

+ 42 - 0
near/receiver/src/tests.rs

@@ -0,0 +1,42 @@
+#[cfg(test)]
+#[allow(clippy::module_inception)]
+mod tests {
+    use {
+        crate::{
+            state::Source,
+            Pyth,
+        },
+        near_sdk::{
+            test_utils::VMContextBuilder,
+            testing_env,
+            VMContext,
+        },
+    };
+
+    fn create_contract() -> Pyth {
+        Pyth::new(
+            "wormhole.near".parse().unwrap(),
+            [0; 32],
+            Source::default(),
+            Source::default(),
+            1,
+            32,
+        )
+    }
+
+    fn get_context() -> VMContext {
+        VMContextBuilder::new()
+            .signer_account_id("pda".parse().unwrap())
+            .is_view(false)
+            .build()
+    }
+
+    #[test]
+    fn test_contract_init() {
+        let contract = create_contract();
+        let context = get_context();
+        testing_env!(context);
+        assert_eq!(contract.sources.len(), 1);
+        assert_eq!(contract.prices.len(), 0);
+    }
+}

+ 592 - 0
near/receiver/tests/workspaces.rs

@@ -0,0 +1,592 @@
+use {
+    near_sdk::json_types::U128,
+    p2w_sdk::{
+        BatchPriceAttestation,
+        Identifier,
+        PriceAttestation,
+        PriceStatus,
+    },
+    pyth::{
+        governance::GovernanceAction,
+        state::{
+            Price,
+            PriceIdentifier,
+            Source,
+        },
+    },
+    serde_json::json,
+    std::io::{
+        Cursor,
+        Write,
+    },
+};
+
+async fn initialize_chain() -> (
+    workspaces::Worker<workspaces::network::Sandbox>,
+    workspaces::Contract,
+    workspaces::Contract,
+) {
+    let worker = workspaces::sandbox().await.expect("Workspaces Failed");
+
+    // Deploy Pyth
+    let contract = worker
+        .dev_deploy(&std::fs::read("pyth.wasm").expect("Failed to find pyth.wasm"))
+        .await
+        .expect("Failed to deploy pyth.wasm");
+
+    // Deploy Wormhole Stub, this is a dummy contract that always verifies VAA's correctly so we
+    // can test the ext_wormhole API.
+    let wormhole = worker
+        .dev_deploy(
+            &std::fs::read("wormhole_stub.wasm").expect("Failed to find wormhole_stub.wasm"),
+        )
+        .await
+        .expect("Failed to deploy wormhole_stub.wasm");
+
+    // Initialize Wormhole.
+    wormhole
+        .call("new")
+        .args_json(&json!({}))
+        .gas(300_000_000_000_000)
+        .transact()
+        .await
+        .expect("Failed to initialize Wormhole")
+        .unwrap();
+
+    // Initialize Pyth, one time operation that sets the Wormhole contract address.
+    let codehash = [0u8; 32];
+
+    contract
+        .call("new")
+        .args_json(&json!({
+            "wormhole":        wormhole.id(),
+            "codehash":        codehash,
+            "initial_source":  Source::default(),
+            "gov_source":      Source::default(),
+            "update_fee":      U128::from(1u128),
+            "stale_threshold": 32,
+        }))
+        .gas(300_000_000_000_000)
+        .transact()
+        .await
+        .expect("Failed to initialize Pyth")
+        .unwrap();
+
+    (worker, contract, wormhole)
+}
+
+#[tokio::test]
+async fn test_source_add() {
+    let (_, contract, _) = initialize_chain().await;
+
+    // Submit a new Source to the contract, this will trigger a cross-contract call to wormhole
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &GovernanceAction::SetDataSources(vec![
+                Source::default(),
+                Source {
+                    emitter:            [1; 32],
+                    pyth_emitter_chain: pyth::state::Chain(1),
+                },
+            ])
+            .serialize(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    assert!(contract
+        .call("execute_governance_instruction")
+        .gas(300_000_000_000_000)
+        .deposit(300_000_000_000_000_000_000_000)
+        .args_json(&json!({
+            "vaa": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // There should now be a two sources in the contract state.
+    assert_eq!(
+        serde_json::from_slice::<Vec<Source>>(&contract.view("get_sources").await.unwrap().result)
+            .unwrap(),
+        &[
+            Source::default(),
+            Source {
+                emitter:            [1; 32],
+                pyth_emitter_chain: pyth::state::Chain(1),
+            },
+        ]
+    );
+}
+
+#[tokio::test]
+async fn test_set_governance_source() {
+    let (_, contract, _) = initialize_chain().await;
+
+    // Submit a new Source to the contract, this will trigger a cross-contract call to wormhole
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &GovernanceAction::SetGovernanceSource(Source {
+                emitter:            [1; 32],
+                pyth_emitter_chain: pyth::state::Chain(1),
+            })
+            .serialize(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    assert!(contract
+        .call("execute_governance_instruction")
+        .gas(300_000_000_000_000)
+        .deposit(300_000_000_000_000_000_000_000)
+        .args_json(&json!({
+            "vaa": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // An action from the new source should now be accepted.
+    let vaa = wormhole::Vaa {
+        sequence: 1, // NOTE: Incremented Governance Sequence
+        emitter_chain: wormhole::Chain::Solana,
+        emitter_address: wormhole::Address([1; 32]),
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &GovernanceAction::SetDataSources(vec![
+                Source::default(),
+                Source {
+                    emitter:            [2; 32],
+                    pyth_emitter_chain: pyth::state::Chain(2),
+                },
+            ])
+            .serialize(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    assert!(contract
+        .call("execute_governance_instruction")
+        .gas(300_000_000_000_000)
+        .deposit(300_000_000_000_000_000_000_000)
+        .args_json(&json!({
+            "vaa": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // But not from the old source.
+    let vaa = wormhole::Vaa {
+        sequence: 1, // NOTE: Incremented Governance Sequence
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &GovernanceAction::SetDataSources(vec![
+                Source::default(),
+                Source {
+                    emitter:            [2; 32],
+                    pyth_emitter_chain: pyth::state::Chain(2),
+                },
+            ])
+            .serialize(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    assert!(!contract
+        .call("execute_governance_instruction")
+        .gas(300_000_000_000_000)
+        .deposit(300_000_000_000_000_000_000_000)
+        .args_json(&json!({
+            "vaa": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_failure());
+}
+
+#[tokio::test]
+async fn test_stale_threshold() {
+    let (_, contract, _) = initialize_chain().await;
+
+    // Submit a Price Attestation to the contract.
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        payload: (),
+        ..Default::default()
+    };
+
+    // Get current UNIX timestamp and subtract a minute from it to place the price attestation in
+    // the past. This should be accepted but untrusted.
+    let now = std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .expect("Failed to get UNIX timestamp")
+        .as_secs()
+        - 60;
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &BatchPriceAttestation {
+                price_attestations: vec![PriceAttestation {
+                    product_id:         Identifier::default(),
+                    price_id:           Identifier::default(),
+                    price:              100,
+                    conf:               1,
+                    expo:               8,
+                    ema_price:          100,
+                    ema_conf:           1,
+                    status:             PriceStatus::Trading,
+                    num_publishers:     8,
+                    max_num_publishers: 8,
+                    attestation_time:   now.try_into().unwrap(),
+                    publish_time:       now.try_into().unwrap(),
+                    prev_publish_time:  now.try_into().unwrap(),
+                    prev_price:         100,
+                    prev_conf:          1,
+                }],
+            }
+            .serialize()
+            .unwrap(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    let update_fee = serde_json::from_slice::<U128>(
+        &contract
+            .view("get_update_fee_estimate")
+            .args(vec![])
+            .await
+            .unwrap()
+            .result,
+    )
+    .unwrap();
+
+    assert!(contract
+        .call("update_price_feed")
+        .gas(300_000_000_000_000)
+        .deposit(update_fee.into())
+        .args_json(&json!({
+            "vaa_hex": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // Assert Price cannot be requested, 60 seconds in the past should be considered stale.
+    // [tag:failed_price_check]
+    assert_eq!(
+        None,
+        serde_json::from_slice::<Option<Price>>(
+            &contract
+                .view("get_price")
+                .args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
+                .await
+                .unwrap()
+                .result
+        )
+        .unwrap(),
+    );
+
+    // Submit another Price Attestation to the contract with an even older timestamp.
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        sequence: 1,
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &BatchPriceAttestation {
+                price_attestations: vec![PriceAttestation {
+                    product_id:         Identifier::default(),
+                    price_id:           Identifier::default(),
+                    price:              1000,
+                    conf:               1,
+                    expo:               8,
+                    ema_price:          1000,
+                    ema_conf:           1,
+                    status:             PriceStatus::Trading,
+                    num_publishers:     8,
+                    max_num_publishers: 8,
+                    attestation_time:   (now - 1024).try_into().unwrap(),
+                    publish_time:       (now - 1024).try_into().unwrap(),
+                    prev_publish_time:  (now - 1024).try_into().unwrap(),
+                    prev_price:         90,
+                    prev_conf:          1,
+                }],
+            }
+            .serialize()
+            .unwrap(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    // The update handler should succeed even if price is old, but simply not update the price.
+    assert!(contract
+        .call("update_price_feed")
+        .gas(300_000_000_000_000)
+        .deposit(update_fee.into())
+        .args_json(&json!({
+            "vaa_hex": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // The price however should _not_ have updated and if we check the unsafe stored price the
+    // timestamp and price should be unchanged.
+    assert_eq!(
+        Price {
+            price:     100,
+            conf:      1,
+            expo:      8,
+            timestamp: now,
+        },
+        serde_json::from_slice::<Price>(
+            &contract
+                .view("get_price_unsafe")
+                .args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
+                .await
+                .unwrap()
+                .result
+        )
+        .unwrap(),
+    );
+
+    // Now we extend the staleness threshold with a Governance VAA.
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        sequence: 2,
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
+        cur.write_all(&GovernanceAction::SetStalePriceThreshold(256).serialize())
+            .unwrap();
+        hex::encode(cur.into_inner())
+    };
+
+    assert!(contract
+        .call("execute_governance_instruction")
+        .gas(300_000_000_000_000)
+        .deposit(300_000_000_000_000_000_000_000)
+        .args_json(&json!({
+            "vaa": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // It should now be possible to request the price that previously returned None.
+    // [ref:failed_price_check]
+    assert_eq!(
+        Some(Price {
+            price:     100,
+            conf:      1,
+            expo:      8,
+            timestamp: now,
+        }),
+        serde_json::from_slice::<Option<Price>>(
+            &contract
+                .view("get_price")
+                .args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
+                .await
+                .unwrap()
+                .result
+        )
+        .unwrap(),
+    );
+}
+
+#[tokio::test]
+async fn test_contract_fees() {
+    let (_, contract, _) = initialize_chain().await;
+
+    let now = std::time::SystemTime::now()
+        .duration_since(std::time::UNIX_EPOCH)
+        .expect("Failed to get UNIX timestamp")
+        .as_secs();
+
+    // Fetch Update fee before changing it.
+    let update_fee = serde_json::from_slice::<U128>(
+        &contract
+            .view("get_update_fee_estimate")
+            .args(vec![])
+            .await
+            .unwrap()
+            .result,
+    )
+    .unwrap();
+
+    // Set a high fee for the contract needed to submit a price.
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).unwrap();
+        cur.write_all(&GovernanceAction::SetUpdateFee(u64::MAX).serialize())
+            .unwrap();
+        hex::encode(cur.into_inner())
+    };
+
+    // Now set the update_fee too high for the deposit to cover.
+    assert!(contract
+        .call("execute_governance_instruction")
+        .gas(300_000_000_000_000)
+        .deposit(300_000_000_000_000_000_000_000)
+        .args_json(&json!({
+            "vaa": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    assert_ne!(
+        update_fee,
+        serde_json::from_slice::<U128>(
+            &contract
+                .view("get_update_fee_estimate")
+                .args(vec![])
+                .await
+                .unwrap()
+                .result,
+        )
+        .unwrap()
+    );
+
+    // Attempt to update the price feed with a now too low deposit.
+    let vaa = wormhole::Vaa {
+        emitter_chain: wormhole::Chain::Any,
+        emitter_address: wormhole::Address([0; 32]),
+        sequence: 1,
+        payload: (),
+        ..Default::default()
+    };
+
+    let vaa = {
+        let mut cur = Cursor::new(Vec::new());
+        serde_wormhole::to_writer(&mut cur, &vaa).expect("Failed to serialize VAA");
+        cur.write_all(
+            &BatchPriceAttestation {
+                price_attestations: vec![PriceAttestation {
+                    product_id:         Identifier::default(),
+                    price_id:           Identifier::default(),
+                    price:              1000,
+                    conf:               1,
+                    expo:               8,
+                    ema_price:          1000,
+                    ema_conf:           1,
+                    status:             PriceStatus::Trading,
+                    num_publishers:     8,
+                    max_num_publishers: 8,
+                    attestation_time:   (now - 1024).try_into().unwrap(),
+                    publish_time:       (now - 1024).try_into().unwrap(),
+                    prev_publish_time:  (now - 1024).try_into().unwrap(),
+                    prev_price:         90,
+                    prev_conf:          1,
+                }],
+            }
+            .serialize()
+            .unwrap(),
+        )
+        .expect("Failed to write Payload");
+        hex::encode(cur.into_inner())
+    };
+
+    assert!(contract
+        .call("update_price_feed")
+        .gas(300_000_000_000_000)
+        .deposit(update_fee.into())
+        .args_json(&json!({
+            "vaa_hex": vaa,
+        }))
+        .transact()
+        .await
+        .expect("Failed to submit VAA")
+        .outcome()
+        .is_success());
+
+    // Submitting a Price should have failed because the fee was not enough.
+    assert_eq!(
+        None,
+        serde_json::from_slice::<Option<Price>>(
+            &contract
+                .view("get_price")
+                .args_json(&json!({ "price_identifier": PriceIdentifier([0; 32]) }))
+                .await
+                .unwrap()
+                .result
+        )
+        .unwrap(),
+    );
+}

+ 1512 - 0
near/wormhole-stub/Cargo.lock

@@ -0,0 +1,1512 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom 0.2.8",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bitvec"
+version = "0.20.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "blake2"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174"
+dependencies = [
+ "crypto-mac",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "borsh"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa"
+dependencies = [
+ "borsh-derive",
+ "hashbrown",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775"
+dependencies = [
+ "borsh-derive-internal",
+ "borsh-schema-derive-internal",
+ "proc-macro-crate 0.1.5",
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "borsh-derive-internal"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "borsh-schema-derive-internal"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "bs58"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[package]]
+name = "byte-slice-cast"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytesize"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70"
+
+[[package]]
+name = "c2-chacha"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27dae93fe7b1e0424dc57179ac396908c26b035a87234809f5c4dfd1b47dc80"
+dependencies = [
+ "cipher",
+ "ppv-lite86",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "cipher"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0"
+dependencies = [
+ "byteorder",
+ "digest 0.9.0",
+ "rand_core 0.5.1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer 0.10.3",
+ "crypto-common",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
+
+[[package]]
+name = "easy-ext"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31"
+
+[[package]]
+name = "ed25519"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
+dependencies = [
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand 0.7.3",
+ "serde",
+ "sha2 0.9.9",
+ "zeroize",
+]
+
+[[package]]
+name = "fixed-hash"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
+dependencies = [
+ "byteorder",
+ "rand 0.8.5",
+ "rustc-hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "impl-codec"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443"
+dependencies = [
+ "parity-scale-codec",
+]
+
+[[package]]
+name = "impl-trait-for-tuples"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "memory_units"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
+
+[[package]]
+name = "near-abi"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "885db39b08518fa700b73fa2214e8adbbfba316ba82dd510f50519173eadaf73"
+dependencies = [
+ "borsh",
+ "schemars",
+ "semver",
+ "serde",
+]
+
+[[package]]
+name = "near-account-id"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d258582a1878e6db67400b0504a5099db85718d22c2e07f747fe1706ae7150"
+dependencies = [
+ "borsh",
+ "serde",
+]
+
+[[package]]
+name = "near-crypto"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e75673d69fd7365508f3d32483669fe45b03bfb34e4d9363e90adae9dfb416c"
+dependencies = [
+ "arrayref",
+ "blake2",
+ "borsh",
+ "bs58",
+ "c2-chacha",
+ "curve25519-dalek",
+ "derive_more",
+ "ed25519-dalek",
+ "near-account-id",
+ "once_cell",
+ "parity-secp256k1",
+ "primitive-types",
+ "rand 0.7.3",
+ "rand_core 0.5.1",
+ "serde",
+ "serde_json",
+ "subtle",
+ "thiserror",
+]
+
+[[package]]
+name = "near-primitives"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ad1a9a1640539c81f065425c31bffcfbf6b31ef1aeaade59ce905f5df6ac860"
+dependencies = [
+ "borsh",
+ "byteorder",
+ "bytesize",
+ "chrono",
+ "derive_more",
+ "easy-ext",
+ "hex",
+ "near-crypto",
+ "near-primitives-core",
+ "near-rpc-error-macro",
+ "near-vm-errors",
+ "num-rational",
+ "once_cell",
+ "primitive-types",
+ "rand 0.7.3",
+ "reed-solomon-erasure",
+ "serde",
+ "serde_json",
+ "smart-default",
+ "strum",
+ "thiserror",
+]
+
+[[package]]
+name = "near-primitives-core"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91d508f0fc340f6461e4e256417685720d3c4c00bb5a939b105160e49137caba"
+dependencies = [
+ "base64 0.11.0",
+ "borsh",
+ "bs58",
+ "derive_more",
+ "near-account-id",
+ "num-rational",
+ "serde",
+ "sha2 0.10.6",
+ "strum",
+]
+
+[[package]]
+name = "near-rpc-error-core"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ee0b41c75ef859c193a8ff1dadfa0c8207bc0ac447cc22259721ad769a1408"
+dependencies = [
+ "quote",
+ "serde",
+ "syn",
+]
+
+[[package]]
+name = "near-rpc-error-macro"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e837bd4bacd807073ec5ceb85708da7f721b46a4c2a978de86027fb0034ce31"
+dependencies = [
+ "near-rpc-error-core",
+ "serde",
+ "syn",
+]
+
+[[package]]
+name = "near-sdk"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15eb3de2defe3626260cc209a6cdb985c6b27b0bd4619fad97dcfae002c3c5bd"
+dependencies = [
+ "base64 0.13.1",
+ "borsh",
+ "bs58",
+ "near-abi",
+ "near-crypto",
+ "near-primitives",
+ "near-primitives-core",
+ "near-sdk-macros",
+ "near-sys",
+ "near-vm-logic",
+ "once_cell",
+ "schemars",
+ "serde",
+ "serde_json",
+ "wee_alloc",
+]
+
+[[package]]
+name = "near-sdk-macros"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4907affc9f5ed559456509188ff0024f1f2099c0830e6bdb66eb61d5b75912c0"
+dependencies = [
+ "Inflector",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "near-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e307313276eaeced2ca95740b5639e1f3125b7c97f0a1151809d105f1aa8c6d3"
+
+[[package]]
+name = "near-vm-errors"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0da466a30f0446639cbd788c30865086fac3e8dcb07a79e51d2b0775ed4261e"
+dependencies = [
+ "borsh",
+ "near-account-id",
+ "near-rpc-error-macro",
+ "serde",
+]
+
+[[package]]
+name = "near-vm-logic"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81b534828419bacbf1f7b11ef7b00420f248c548c485d3f0cfda8bb6931152f2"
+dependencies = [
+ "base64 0.13.1",
+ "borsh",
+ "bs58",
+ "byteorder",
+ "near-account-id",
+ "near-crypto",
+ "near-primitives",
+ "near-primitives-core",
+ "near-vm-errors",
+ "ripemd",
+ "serde",
+ "sha2 0.10.6",
+ "sha3",
+ "zeropool-bn",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "parity-scale-codec"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909"
+dependencies = [
+ "arrayvec 0.7.2",
+ "bitvec",
+ "byte-slice-cast",
+ "impl-trait-for-tuples",
+ "parity-scale-codec-derive",
+ "serde",
+]
+
+[[package]]
+name = "parity-scale-codec-derive"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27"
+dependencies = [
+ "proc-macro-crate 1.2.1",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "parity-secp256k1"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fca4f82fccae37e8bbdaeb949a4a218a1bbc485d11598f193d2a908042e5fc1"
+dependencies = [
+ "arrayvec 0.5.2",
+ "cc",
+ "cfg-if 0.1.10",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "primitive-types"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373"
+dependencies = [
+ "fixed-hash",
+ "impl-codec",
+ "uint",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
+dependencies = [
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
+dependencies = [
+ "once_cell",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "reed-solomon-erasure"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a415a013dd7c5d4221382329a5a3482566da675737494935cbbbcdec04662f9d"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "ripemd"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
+dependencies = [
+ "digest 0.10.6",
+]
+
+[[package]]
+name = "rustc-hex"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "schemars"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a5fb6c61f29e723026dc8e923d94c694313212abbecbbe5f55a7748eec5b307"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f188d036977451159430f3b8dc82ec76364a42b7e289c2b18a9a18f4470058e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
+[[package]]
+name = "semver"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+
+[[package]]
+name = "serde"
+version = "1.0.148"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.148"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.10.6",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9"
+dependencies = [
+ "digest 0.10.6",
+ "keccak",
+]
+
+[[package]]
+name = "signature"
+version = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "smart-default"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strum"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.12.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "uint"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "wee_alloc"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "memory_units",
+ "winapi",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "wormhole-stub"
+version = "0.1.0"
+dependencies = [
+ "near-sdk",
+]
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "zeroize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeropool-bn"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e61de68ede9ffdd69c01664f65a178c5188b73f78faa21f0936016a888ff7c"
+dependencies = [
+ "borsh",
+ "byteorder",
+ "crunchy",
+ "lazy_static",
+ "rand 0.8.5",
+ "rustc-hex",
+]

+ 19 - 0
near/wormhole-stub/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name    = "wormhole-stub"
+version = "0.1.0"
+authors = ["Pyth Data Association"]
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+near-sdk     = { version = "4.1.1" }
+
+[profile.release]
+codegen-units   = 1
+opt-level       = "z"
+lto             = true
+debug           = false
+panic           = "abort"
+overflow-checks = true

+ 44 - 0
near/wormhole-stub/src/lib.rs

@@ -0,0 +1,44 @@
+//#![deny(warnings)]
+
+use near_sdk::{
+    borsh::{
+        self,
+        BorshDeserialize,
+        BorshSerialize,
+    },
+    log,
+    near_bindgen,
+    PanicOnDefault,
+};
+
+#[near_bindgen]
+#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
+pub struct State {}
+
+#[near_bindgen]
+impl State {
+    #[init]
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> Self {
+        Self {}
+    }
+
+    #[payable]
+    pub fn verify_vaa(&mut self, vaa: String) -> u32 {
+        log!(
+            r#"
+            {{
+                "standard": "pyth",
+                "version":  "1.0",
+                "event":    "Wormhole::verify_vaa",
+                "data":     {{
+                    "vaa": "{}"
+                }}
+            }}
+        "#,
+            vaa,
+        );
+
+        0
+    }
+}