[cosmwasm] Add example cw contract (#516)

* move example to cosmwasm

* gen schema and update developing.md

* update readme file

* remove example schema gen file

* correct var name

* correct the response of the query

* correct schema output
This commit is contained in:
Dev Kalra 2023-01-20 20:36:40 +05:30 committed by GitHub
parent 18b1479c4c
commit 11d0b3ca55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1604 additions and 0 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
[package]
name = "example-cw-contract"
version = "0.1.0"
authors = ["Pyth Data Foundation"]
edition = "2018"
exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []
[package.metadata.scripts]
optimize = """docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.5
"""
[dependencies]
cosmwasm-std = { version = "1.0.0" }
cosmwasm-storage = { version = "1.0.0" }
cw-storage-plus = "0.13.4"
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
pyth-cosmwasm = { git="https://github.com/pyth-network/pyth-crosschain", rev="5d0acc1" }
cosmwasm-schema = "1.1.9"

View File

@ -0,0 +1,88 @@
# Developing
This document contains guidance regarding building, testing and preparing your contracts for production.
## Prerequisites
Before starting, make sure you have [rustup](https://rustup.rs/) along with a
recent `rustc` and `cargo` version installed. Rust version 1.58.1 or above is required.
And you need to have the `wasm32-unknown-unknown` target installed as well.
You can check that via:
```sh
rustc --version
cargo --version
rustup target list --installed
# if wasm32 is not listed above, run this
rustup target add wasm32-unknown-unknown
```
## Compiling
After changing the contract, make sure you can compile and run it before
making any changes. Go into the repository and do:
```sh
# this will produce a wasm build in ./target/wasm32-unknown-unknown/release/example_cw_contract.wasm
cargo build --release --target wasm32-unknown-unknown
```
## Generating JSON Schema
While the Wasm calls (`instantiate`, `execute`, `query`) accept JSON, this is not enough
information to use it. You need to expose the schema for the expected messages to the
clients. You can generate this schema by calling `cargo run schema`, which will output
the schema at `./schema/example-cw-contract.json`, corresponding to the message types defined in `msg.rs`.
These files are in standard json-schema format, which should be usable by various
client side tools, either to auto-generate codecs, or just to validate incoming
json wrt. the defined schema.
## Preparing the Wasm bytecode for production
Before you upload it to a chain, you need to ensure the smallest output size possible,
as this will be included in the body of a transaction. You also want to have a
reproducible build process, so third parties can verify that the uploaded Wasm
code did indeed come from the claimed rust code.
To solve both these issues, CosmWasm have produced `rust-optimizer`, a docker image to
produce an extremely small build output in a consistent manner. The suggested way
to run it is this:
```sh
cd path/to/cargo/root
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.11
```
Or, If you're on an arm64 machine, you should use a docker image built with arm64.
```sh
cd path/to/cargo/root
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer-arm64:0.12.11
```
You must mount the contract code to `/code`. You can use a absolute path instead
of `$(pwd)` if you don't want to `cd` to the directory first. The other two
volumes are nice for speedup. Mounting `/code/target` in particular is useful
to avoid docker overwriting your local dev files with root permissions.
Note the `/code/target` cache is unique for each contract being compiled to limit
interference, while the registry cache is global.
This is rather slow compared to local compilations, especially the first compile
of a given contract. The use of the two volume caches is very useful to speed up
following compiles of the same contract.
This produces an `artifacts` directory with a `PROJECT_NAME.wasm`, as well as
`checksums.txt`, containing the Sha256 hash of the wasm file.
The wasm file is compiled deterministically (anyone else running the same
docker on the same git commit should get the identical file with the same Sha256 hash).
It is also stripped and minimized for upload to a blockchain (it is also compressed using
gzip in the uploading process to make it even smaller).

View File

@ -0,0 +1,53 @@
# Pyth SDK Example Contract for CosmWasm
This repository contains an example contract that demonstrates how to query Pyth on-chain contract.
The example [contract](src/contract.rs) has two functions:
- `instantiate` sets the Pyth contract address and price feed id that the contract uses.
This function is intended to be called once when the contract is deployed.
See the [price-feed-ids](https://pyth.network/developers/price-feed-ids) for the list of possible price feed ids.
- `query` queries the Pyth contract
- to get the current price for the configured price feed id.
- to fetch the default valid time period.
- to calculate the fee for updating a price feed.
## Testnet Demo
This example contract is running on Injective testnet at `inj1cc9effer9ttdfrkfghfj7y3ph48c36a9y8d7cv`.
This contract has been instantiated to return the price of `Crypto.INJ/USD`.
You can query the contract using the schema at `schema/example-cw-contract.json`.
Some example queries:
```
{
"fetch_price": {}
}
```
If the query is successful, the output should look like:
```
{
current_price: { price: "8704350000", conf: "3150000", expo: -8, publish_time: "1674224555332" },
ema_price: { price: "8665158600", conf: "2965370", expo: -8, publish_time: "1674224555332" }
}
```
If the price feed is currently not available you will see:
```
rpc error: code = Unknown desc = Generic error: Current price is not available: contract query failed
```
## Developing
If you would like to deploy a changed version of this contract, the process consists of two steps:
1. Build the WASM for the contract.
2. Upload the code and instantiate a new contract.
### Build WASM
See the [Developing instructions](Developing.md) for how to build the WASM for the contract.
The instructions in that document will build a file called `example_cw_contract.wasm` under the `artifacts/` directory.

View File

@ -0,0 +1,167 @@
{
"contract_name": "example-cw-contract",
"contract_version": "0.1.0",
"idl_version": "1.0.0",
"instantiate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": ["price_feed_id", "pyth_contract_addr"],
"properties": {
"price_feed_id": {
"$ref": "#/definitions/Identifier"
},
"pyth_contract_addr": {
"type": "string"
}
},
"additionalProperties": false,
"definitions": {
"Identifier": {
"type": "string"
}
}
},
"execute": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"type": "string",
"enum": []
},
"query": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "string",
"enum": ["fetch_valid_time_period"]
},
{
"type": "object",
"required": ["fetch_price"],
"properties": {
"fetch_price": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": ["fetch_update_fee"],
"properties": {
"fetch_update_fee": {
"type": "object",
"required": ["vaas"],
"properties": {
"vaas": {
"type": "array",
"items": {
"$ref": "#/definitions/Binary"
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
"Binary": {
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>",
"type": "string"
}
}
},
"migrate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
},
"sudo": null,
"responses": {
"fetch_price": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "FetchPriceResponse",
"type": "object",
"required": ["current_price", "ema_price"],
"properties": {
"current_price": {
"$ref": "#/definitions/Price"
},
"ema_price": {
"$ref": "#/definitions/Price"
}
},
"additionalProperties": false,
"definitions": {
"Price": {
"description": "A price with a degree of uncertainty at a certain time, represented as a price +- a confidence interval.\n\nPlease refer to the documentation at https://docs.pyth.network/consumers/best-practices for using this price safely.\n\nThe 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. For example:\n\n``` use pyth_sdk::Price; Price { price: 12345, conf: 267, expo: -2, publish_time: 100 }; // represents 123.45 +- 2.67 published at UnixTimestamp 100 Price { price: 123, conf: 1, expo: 2, publish_time: 100 }; // represents 12300 +- 100 published at UnixTimestamp 100 ```\n\n`Price` supports a limited set of mathematical operations. All of these operations will propagate any uncertainty in the arguments into the result. However, the uncertainty in the result may overestimate the true uncertainty (by at most a factor of `sqrt(2)`) due to computational limitations. Furthermore, all of these operations may return `None` if their result cannot be represented within the numeric representation (e.g., the exponent is so small that the price does not fit into an i64). Users of these methods should (1) select their exponents to avoid this problem, and (2) handle the `None` case gracefully.",
"type": "object",
"required": ["conf", "expo", "price", "publish_time"],
"properties": {
"conf": {
"description": "Confidence interval.",
"type": "string"
},
"expo": {
"description": "Exponent.",
"type": "integer",
"format": "int32"
},
"price": {
"description": "Price.",
"type": "string"
},
"publish_time": {
"description": "Publish time.",
"type": "integer",
"format": "int64"
}
}
}
}
},
"fetch_update_fee": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Coin",
"type": "object",
"required": ["amount", "denom"],
"properties": {
"amount": {
"$ref": "#/definitions/Uint128"
},
"denom": {
"type": "string"
}
},
"definitions": {
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
},
"fetch_valid_time_period": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Duration",
"type": "object",
"required": ["nanos", "secs"],
"properties": {
"nanos": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
}
}
}
}

View File

@ -0,0 +1,18 @@
use {
cosmwasm_schema::write_api,
example_cw_contract::msg::{
ExecuteMsg,
InstantiateMsg,
MigrateMsg,
QueryMsg,
},
};
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ExecuteMsg,
migrate: MigrateMsg,
query: QueryMsg
}
}

View File

@ -0,0 +1,144 @@
use std::time::Duration;
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
to_binary,
Binary,
Deps,
DepsMut,
Env,
MessageInfo,
QueryRequest,
Response,
StdError,
StdResult,
WasmQuery, Coin,
};
use pyth_cosmwasm::msg::{
PriceFeedResponse,
QueryMsg as PythQueryMsg,
};
use crate::msg::{
ExecuteMsg,
FetchPriceResponse,
InstantiateMsg,
MigrateMsg,
QueryMsg,
};
use crate::state::{
State,
STATE,
};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new().add_attribute("method", "migrate"))
}
/// The instantiate function is invoked when the contract is first deployed.
/// This function sets configuration values that are used by the query function.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
// It is a good practice that your contract stores the pyth contract address and ids of the
// price feeds it needs upon instantiation or by an authorized approach. This will ensure
// that a wrong address won't be used.
let state = State {
pyth_contract_addr: deps.api.addr_validate(msg.pyth_contract_addr.as_ref())?,
price_feed_id: msg.price_feed_id,
};
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("price_id", format!("{}", msg.price_feed_id)))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: ExecuteMsg,
) -> StdResult<Response> {
Ok(Response::new().add_attribute("method", "execute"))
}
/// Query the Pyth contract the current price of the configured price feed.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::FetchPrice {} => to_binary(&query_fetch_price(deps, env)?),
QueryMsg::FetchUpdateFee { vaas } => to_binary(&query_fetch_update_fee(deps, vaas)?),
QueryMsg::FetchValidTimePeriod => to_binary(&query_fetch_valid_time_period(deps)?),
}
}
fn query_fetch_price(deps: Deps, env: Env) -> StdResult<FetchPriceResponse> {
let state = STATE.load(deps.storage)?;
// query_price_feed is the standard way to read the current price from a Pyth price feed.
// It takes the address of the Pyth contract (which is fixed for each network) and the id of the
// price feed. The result is a PriceFeed object with fields for the current price and other
// useful information. The function will fail if the contract address or price feed id are
// invalid.
let price_feed_response: PriceFeedResponse =
deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: state.pyth_contract_addr.into_string(),
msg: to_binary(&PythQueryMsg::PriceFeed {
id: state.price_feed_id,
})?,
}))?;
let price_feed = price_feed_response.price_feed;
// Get the current price and confidence interval from the price feed.
// This function returns None if the price is not currently available.
// This condition can happen for various reasons. For example, some products only trade at
// specific times, or network outages may prevent the price feed from updating.
//
// The example code below throws an error if the price is not available. It is recommended that
// you handle this scenario more carefully. Consult the [consumer best practices](https://docs.pyth.network/consumers/best-practices)
// for recommendations.
let current_price = price_feed
.get_price_no_older_than(env.block.time.seconds() as i64, 60)
.ok_or_else(|| StdError::not_found("Current price is not available"))?;
// Get an exponentially-weighted moving average price and confidence interval.
// The same notes about availability apply to this price.
let ema_price = price_feed
.get_ema_price_no_older_than(env.block.time.seconds() as i64, 60)
.ok_or_else(|| StdError::not_found("EMA price is not available"))?;
Ok(FetchPriceResponse {
current_price,
ema_price,
})
}
fn query_fetch_update_fee(deps: Deps, vaas: Vec<Binary>) -> StdResult<Coin> {
let state = STATE.load(deps.storage)?;
let contract_addr = state.pyth_contract_addr.into_string();
let msg = to_binary(&PythQueryMsg::GetUpdateFee { vaas })?;
let coin: Coin = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }))?;
Ok(coin)
}
fn query_fetch_valid_time_period(deps: Deps) -> StdResult<Duration> {
let state = STATE.load(deps.storage)?;
let contract_addr = state.pyth_contract_addr.into_string();
let msg = to_binary(&PythQueryMsg::GetValidTimePeriod)?;
let duration: Duration = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }))?;
Ok(duration)
}

View File

@ -0,0 +1,3 @@
pub mod contract;
pub mod msg;
pub mod state;

View File

@ -0,0 +1,41 @@
use std::time::Duration;
use cosmwasm_std::{Binary, Coin};
use pyth_cosmwasm::{
Price,
PriceIdentifier,
};
use cosmwasm_schema::{
cw_serde,
QueryResponses,
};
#[cw_serde]
pub struct MigrateMsg {}
#[cw_serde]
pub struct InstantiateMsg {
pub price_feed_id: PriceIdentifier,
pub pyth_contract_addr: String,
}
#[cw_serde]
pub enum ExecuteMsg {}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(FetchPriceResponse)]
FetchPrice {},
#[returns(Coin)]
FetchUpdateFee { vaas: Vec<Binary> },
#[returns(Duration)]
FetchValidTimePeriod,
}
#[cw_serde]
pub struct FetchPriceResponse {
pub current_price: Price,
pub ema_price: Price,
}

View File

@ -0,0 +1,19 @@
use cosmwasm_std::Addr;
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
use cw_storage_plus::Item;
use pyth_cosmwasm::PriceIdentifier;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
// Available price feeds and their ids are listed in pyth-sdk-cw Readme.
pub price_feed_id: PriceIdentifier,
// Contract address of Pyth in different networks are listed in pyth-sdk-cw Readme.
pub pyth_contract_addr: Addr,
}
pub const STATE: Item<State> = Item::new("state");