Bladeren bron

add cw example

Jayant Krishnamurthy 2 jaren geleden
bovenliggende
commit
c79a4ba569

+ 25 - 0
cosmwasm/Cargo.lock

@@ -1259,6 +1259,31 @@ dependencies = [
  "wormhole-bridge-terra-2",
 ]
 
+[[package]]
+name = "pyth-cosmwasm-example"
+version = "0.1.0"
+dependencies = [
+ "bigint",
+ "byteorder",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cosmwasm-vm",
+ "generic-array",
+ "hex",
+ "k256 0.9.6",
+ "lazy_static",
+ "p2w-sdk",
+ "pyth-sdk-cw",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha3",
+ "terraswap",
+ "thiserror",
+ "wormhole-bridge-terra-2",
+]
+
 [[package]]
 name = "pyth-sdk"
 version = "0.5.0"

+ 1 - 1
cosmwasm/Cargo.toml

@@ -1,5 +1,5 @@
 [workspace]
-members = ["contracts/pyth"]
+members = ["contracts/pyth", "example/contract"]
 
 [profile.release]
 opt-level = 3

+ 1 - 3
cosmwasm/contracts/pyth/src/contract.rs

@@ -76,12 +76,11 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Respons
 pub fn instantiate(
     deps: DepsMut,
     _env: Env,
-    info: MessageInfo,
+    _info: MessageInfo,
     msg: InstantiateMsg,
 ) -> StdResult<Response> {
     // Save general wormhole and pyth info
     let state = ConfigInfo {
-        owner:                      info.sender,
         wormhole_contract:          deps.api.addr_validate(msg.wormhole_contract.as_ref())?,
         data_sources:               msg.data_sources.iter().cloned().collect(),
         chain_id:                   msg.chain_id,
@@ -590,7 +589,6 @@ mod test {
 
     fn create_zero_config_info() -> ConfigInfo {
         ConfigInfo {
-            owner:                      Addr::unchecked(String::default()),
             wormhole_contract:          Addr::unchecked(String::default()),
             data_sources:               HashSet::default(),
             governance_source:          PythDataSource {

+ 0 - 1
cosmwasm/contracts/pyth/src/state.rs

@@ -39,7 +39,6 @@ pub struct PythDataSource {
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct ConfigInfo {
-    pub owner:                      Addr,
     pub wormhole_contract:          Addr,
     pub data_sources:               HashSet<PythDataSource>,
     pub governance_source:          PythDataSource,

+ 37 - 0
cosmwasm/example/contract/Cargo.toml

@@ -0,0 +1,37 @@
+[package]
+name = "pyth-cosmwasm-example"
+version = "0.1.0"
+authors = ["Pyth Network Contributors"]
+edition = "2018"
+description = "Pyth cosmwasm example"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all init/handle/query exports
+library = []
+
+[dependencies]
+cosmwasm-std = { version = "1.0.0" }
+cosmwasm-storage = { version = "1.0.0" }
+schemars = "0.8.1"
+serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+serde_derive = { version = "1.0.103"}
+terraswap = "2.4.0"
+wormhole-bridge-terra-2 = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.8.9", features = ["library"] }
+thiserror = { version = "1.0.20" }
+k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
+sha3 = { version = "0.9.1", default-features = false }
+generic-array = { version = "0.14.4" }
+hex = "0.4.2"
+lazy_static = "1.4.0"
+bigint = "4"
+p2w-sdk = { path = "../../../third_party/pyth/p2w-sdk/rust" }
+pyth-sdk-cw = "0.2.0"
+byteorder = "1.4.3"
+
+[dev-dependencies]
+cosmwasm-vm = { version = "1.0.0", default-features = false }
+serde_json = "1.0"

+ 234 - 0
cosmwasm/example/contract/src/contract.rs

@@ -0,0 +1,234 @@
+use {
+    crate::{
+        msg::{
+            ExecuteMsg,
+            GetPriceResponse,
+            InstantiateMsg,
+            MigrateMsg,
+            QueryMsg,
+        },
+        state::{
+            config,
+            config_read,
+            ConfigInfo,
+        },
+    },
+    cosmwasm_std::{
+        entry_point,
+        to_binary,
+        Binary,
+        Deps,
+        DepsMut,
+        Env,
+        MessageInfo,
+        Response,
+        StdError::{self,},
+        StdResult,
+        WasmQuery,
+    },
+    pyth_sdk_cw::{
+        query_price_feed,
+        Price,
+        PriceFeed,
+        PriceFeedResponse,
+        PriceIdentifier,
+    },
+};
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
+    Ok(Response::new())
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    _env: Env,
+    _info: MessageInfo,
+    msg: InstantiateMsg,
+) -> StdResult<Response> {
+    // Save general wormhole and pyth info
+    let state = ConfigInfo {
+        pyth_contract:   msg.pyth_contract,
+        price_feed_id:   msg.price_feed_id,
+        price_in_usd:    msg.price_in_usd,
+        target_exponent: msg.target_exponent,
+    };
+    config(deps.storage).save(&state)?;
+
+    Ok(Response::default())
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    _deps: DepsMut,
+    _env: Env,
+    _info: MessageInfo,
+    _msg: ExecuteMsg,
+) -> StdResult<Response> {
+    // TODO
+    Ok(Response::default())
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        // user wants to purchase `quantity` of the token. They need to pay
+        QueryMsg::GetPrice { quantity } => {
+            let cfg = config_read(deps.storage).load()?;
+            let price_in_usd: Price = Price {
+                price: i64::from(quantity) * i64::from(cfg.price_in_usd),
+                conf:  0,
+                expo:  0,
+            };
+
+            let feed = read_pyth_price(&deps, &env)?;
+            // Value of token in USD, e.g, 1BTC = $10k
+            let current_token_price = feed
+                .get_current_price()
+                .ok_or(StdError::generic_err("feed is not current"))?;
+
+            // Price struct supports nice arithmetic
+            let price_in_payment_token = price_in_usd
+                .div(&current_token_price)
+                .and_then(|p| p.scale_to_exponent(cfg.target_exponent))
+                .ok_or(StdError::generic_err("division error"))?;
+
+            to_binary(&GetPriceResponse {
+                price:    price_in_payment_token.price,
+                exponent: price_in_payment_token.expo,
+            })
+        }
+    }
+}
+
+pub fn read_pyth_price(deps: &Deps, _env: &Env) -> StdResult<PriceFeed> {
+    let cfg = config_read(deps.storage).load()?;
+    query_price_feed(&deps.querier, cfg.pyth_contract, cfg.price_feed_id).map(|r| r.price_feed)
+}
+
+#[cfg(test)]
+mod test {
+    use {
+        super::*,
+        cosmwasm_std::{
+            from_binary,
+            testing::{
+                mock_dependencies,
+                mock_env,
+                MockApi,
+                MockQuerier,
+                MockStorage,
+            },
+            Addr,
+            ContractResult,
+            OwnedDeps,
+            QuerierResult,
+            SystemError,
+            SystemResult,
+        },
+        p2w_sdk::PriceStatus,
+    };
+
+    // TODO: point to documentation
+    const PYTH_CONTRACT_ADDR: &str = "pyth_contract_addr";
+    // See list of price feed ids here https://pyth.network/developers/price-feed-ids
+    const PRICE_ID: &str = "63f341689d98a12ef60a5cff1d7f85c70a9e17bf1575f0e7c0b2512d48b1c8b3";
+
+    fn default_emitter_addr() -> Vec<u8> {
+        vec![0, 1, 80]
+    }
+
+    fn default_config_info() -> ConfigInfo {
+        ConfigInfo {
+            pyth_contract:   Addr::unchecked(PYTH_CONTRACT_ADDR),
+            price_feed_id:   PriceIdentifier::from_hex(PRICE_ID).unwrap(),
+            price_in_usd:    10,
+            target_exponent: -2,
+        }
+    }
+
+    fn setup_test(config_info: &ConfigInfo) -> (OwnedDeps<MockStorage, MockApi, MockQuerier>, Env) {
+        let mut dependencies = mock_dependencies();
+        dependencies.querier.update_wasm(handle_wasm_query);
+
+        let mut config = config(dependencies.as_mut().storage);
+        config.save(config_info).unwrap();
+        (dependencies, mock_env())
+    }
+
+    fn handle_wasm_query(wasm_query: &WasmQuery) -> QuerierResult {
+        match wasm_query {
+            WasmQuery::Smart { contract_addr, msg } if *contract_addr == PYTH_CONTRACT_ADDR => {
+                let query_msg = from_binary::<pyth_sdk_cw::QueryMsg>(msg);
+                match query_msg {
+                    Ok(pyth_sdk_cw::QueryMsg::PriceFeed { id }) => {
+                        if id.to_hex() == PRICE_ID {
+                            let price_feed = PriceFeed::new(
+                                id,
+                                PriceStatus::Trading,
+                                100,
+                                -2,
+                                32,
+                                3,
+                                id,
+                                100 * 100,
+                                100,
+                                75 * 100,
+                                100,
+                                99 * 100,
+                                100,
+                                99,
+                            );
+
+                            SystemResult::Ok(ContractResult::Ok(
+                                to_binary(&PriceFeedResponse { price_feed }).unwrap(),
+                            ))
+                        } else {
+                            SystemResult::Ok(ContractResult::Err("unknown price feed".into()))
+                        }
+                    }
+                    Err(_e) => SystemResult::Err(SystemError::InvalidRequest {
+                        error:   "Invalid message".into(),
+                        request: msg.clone(),
+                    }),
+                    // TODO: this error isn't right
+                    _ => SystemResult::Err(SystemError::NoSuchContract {
+                        addr: contract_addr.clone(),
+                    }),
+                }
+            }
+            WasmQuery::Smart { contract_addr, .. } => {
+                SystemResult::Err(SystemError::NoSuchContract {
+                    addr: contract_addr.clone(),
+                })
+            }
+            WasmQuery::Raw { contract_addr, .. } => {
+                SystemResult::Err(SystemError::NoSuchContract {
+                    addr: contract_addr.clone(),
+                })
+            }
+            WasmQuery::ContractInfo { contract_addr, .. } => {
+                SystemResult::Err(SystemError::NoSuchContract {
+                    addr: contract_addr.clone(),
+                })
+            }
+            _ => unreachable!(),
+        }
+    }
+
+    fn query_get_price(config_info: &ConfigInfo, quantity: u32) -> StdResult<GetPriceResponse> {
+        let (mut deps, env) = setup_test(config_info);
+        config(&mut deps.storage).save(config_info).unwrap();
+
+        let msg = QueryMsg::GetPrice { quantity };
+
+        query(deps.as_ref(), env, msg).and_then(|binary| from_binary::<GetPriceResponse>(&binary))
+    }
+
+    #[test]
+    fn test_get_price() {
+        let result = query_get_price(&default_config_info(), 100);
+        assert_eq!(result.map(|r| r.price), Ok(1000));
+    }
+}

+ 55 - 0
cosmwasm/example/contract/src/error.rs

@@ -0,0 +1,55 @@
+use {
+    cosmwasm_std::StdError,
+    thiserror::Error,
+};
+
+#[derive(Error, Debug)]
+pub enum PythContractError {
+    /// Message sender not permitted to execute this operation
+    #[error("PermissionDenied")]
+    PermissionDenied,
+
+    /// Wrapped asset not found in the registry
+    #[error("PriceFeedNotFound")]
+    PriceFeedNotFound,
+
+    /// Message emitter is not an accepted data source.
+    #[error("InvalidUpdateMessageEmitter")]
+    InvalidUpdateEmitter,
+
+    /// Message payload cannot be deserialized to a batch attestation
+    #[error("InvalidUpdatePayload")]
+    InvalidUpdatePayload,
+
+    /// Data source does not exists error (on removing data source)
+    #[error("DataSourceDoesNotExists")]
+    DataSourceDoesNotExists,
+
+    /// Data source already exists error (on adding data source)
+    #[error("DataSourceAlreadyExists")]
+    DataSourceAlreadyExists,
+
+    /// Message emitter is not an accepted source of governance instructions.
+    #[error("InvalidGovernanceEmitter")]
+    InvalidGovernanceEmitter,
+
+    /// Message payload cannot be deserialized as a valid governance instruction.
+    #[error("InvalidGovernancePayload")]
+    InvalidGovernancePayload,
+
+    /// The sequence number of the governance message is too old.
+    #[error("OldGovernanceMessage")]
+    OldGovernanceMessage,
+
+    /// The message did not include a sufficient fee.
+    #[error("InsufficientFee")]
+    InsufficientFee,
+}
+
+impl From<PythContractError> for StdError {
+    fn from(other: PythContractError) -> StdError {
+        StdError::GenericErr {
+            msg: format!("{other}"),
+        }
+    }
+}

+ 7 - 0
cosmwasm/example/contract/src/lib.rs

@@ -0,0 +1,7 @@
+#[cfg(test)]
+extern crate lazy_static;
+
+pub mod contract;
+pub mod error;
+pub mod msg;
+pub mod state;

+ 43 - 0
cosmwasm/example/contract/src/msg.rs

@@ -0,0 +1,43 @@
+use {
+    cosmwasm_std::Addr,
+    pyth_sdk_cw::PriceIdentifier,
+    schemars::JsonSchema,
+    serde::{
+        Deserialize,
+        Serialize,
+    },
+};
+
+type HumanAddr = String;
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct InstantiateMsg {
+    pub pyth_contract:   Addr,
+    pub price_feed_id:   PriceIdentifier,
+    pub price_in_usd:    u32,
+    pub target_exponent: i32,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExecuteMsg {
+    Buy { quantity: u32 },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct MigrateMsg {}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    GetPrice { quantity: u32 },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct GetPriceResponse {
+    pub price:    i64,
+    pub exponent: i32,
+}

+ 36 - 0
cosmwasm/example/contract/src/state.rs

@@ -0,0 +1,36 @@
+use {
+    cosmwasm_std::{
+        Addr,
+        Storage,
+    },
+    cosmwasm_storage::{
+        singleton,
+        singleton_read,
+        ReadonlySingleton,
+        Singleton,
+    },
+    pyth_sdk_cw::PriceIdentifier,
+    schemars::JsonSchema,
+    serde::{
+        Deserialize,
+        Serialize,
+    },
+};
+
+pub static CONFIG_KEY: &[u8] = b"config";
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct ConfigInfo {
+    pub pyth_contract:   Addr,
+    pub price_feed_id:   PriceIdentifier,
+    pub price_in_usd:    u32,
+    pub target_exponent: i32,
+}
+
+pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
+    singleton(storage, CONFIG_KEY)
+}
+
+pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
+    singleton_read(storage, CONFIG_KEY)
+}