diff --git a/target_chains/cosmwasm/Cargo.lock b/target_chains/cosmwasm/Cargo.lock index b59f8857..9e98c9b0 100644 --- a/target_chains/cosmwasm/Cargo.lock +++ b/target_chains/cosmwasm/Cargo.lock @@ -1270,6 +1270,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serde_repr", "sha3", "terraswap", "thiserror", @@ -1591,6 +1592,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.9.9" diff --git a/target_chains/cosmwasm/README.md b/target_chains/cosmwasm/README.md index e30cb711..c00b49ae 100644 --- a/target_chains/cosmwasm/README.md +++ b/target_chains/cosmwasm/README.md @@ -18,6 +18,14 @@ This directory contains the code to perform all the steps. Read below for the de First, build the contracts within [the current directory](./). You must have Docker installed. +NOTE: In order to build for Injective. We need to enable a feature in [Cargo.toml](./contracts/pyth/Cargo.toml) like this. + +```toml +[features] +default=["injective"] +... +``` + ```sh bash build.sh ``` diff --git a/target_chains/cosmwasm/contracts/pyth/Cargo.toml b/target_chains/cosmwasm/contracts/pyth/Cargo.toml index 03fdf7ad..96af4ce6 100644 --- a/target_chains/cosmwasm/contracts/pyth/Cargo.toml +++ b/target_chains/cosmwasm/contracts/pyth/Cargo.toml @@ -9,9 +9,12 @@ description = "Pyth price receiver" crate-type = ["cdylib", "rlib"] [features] +# IMPORTANT: if you want to build for injective, enable the default feature below +# default=["injective"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all init/handle/query exports library = [] +injective = ["dep:serde_repr"] [dependencies] cosmwasm-std = { version = "1.0.0" } @@ -19,6 +22,7 @@ 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"} +serde_repr = { version="0.1", optional = true} terraswap = "2.4.0" wormhole-bridge-terra-2 = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8", features = ["library"] } thiserror = { version = "1.0.20" } diff --git a/target_chains/cosmwasm/contracts/pyth/src/contract.rs b/target_chains/cosmwasm/contracts/pyth/src/contract.rs index 31e65684..7709161d 100644 --- a/target_chains/cosmwasm/contracts/pyth/src/contract.rs +++ b/target_chains/cosmwasm/contracts/pyth/src/contract.rs @@ -1,3 +1,10 @@ +#[cfg(feature = "injective")] +use crate::injective::{ + create_relay_pyth_prices_msg, + InjectiveMsgWrapper as MsgWrapper, +}; +#[cfg(not(feature = "injective"))] +use cosmwasm_std::Empty as MsgWrapper; use { crate::{ governance::{ @@ -128,7 +135,12 @@ pub fn parse_and_verify_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> St } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> StdResult> { match msg { ExecuteMsg::UpdatePriceFeeds { data } => update_price_feeds(deps, env, info, &data), ExecuteMsg::ExecuteGovernanceInstruction { data } => { @@ -147,7 +159,7 @@ fn update_price_feeds( env: Env, info: MessageInfo, data: &[Binary], -) -> StdResult { +) -> StdResult> { let state = config_read(deps.storage).load()?; // Check that a sufficient fee was sent with the message @@ -157,8 +169,9 @@ fn update_price_feeds( return Err(PythContractError::InsufficientFee.into()); } - let mut total_attestations: usize = 0; - let mut new_attestations: usize = 0; + let mut num_total_attestations: usize = 0; + let mut total_new_attestations: Vec = vec![]; + for datum in data { let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), datum)?; verify_vaa_from_data_source(&state, &vaa)?; @@ -167,16 +180,36 @@ fn update_price_feeds( let batch_attestation = BatchPriceAttestation::deserialize(&data[..]) .map_err(|_| PythContractError::InvalidUpdatePayload)?; - let (num_updates, num_new) = + let (num_attestations, new_attestations) = process_batch_attestation(&mut deps, &env, &batch_attestation)?; - total_attestations += num_updates; - new_attestations += num_new; + num_total_attestations += num_attestations; + for new_attestation in new_attestations { + total_new_attestations.push(new_attestation.to_owned()); + } } - Ok(Response::new() - .add_attribute("action", "update_price_feeds") - .add_attribute("num_attestations", format!("{total_attestations}")) - .add_attribute("num_updated", format!("{new_attestations}"))) + let num_total_new_attestations = total_new_attestations.len(); + + let response = Response::new(); + + #[cfg(feature = "injective")] + { + let inj_message = + create_relay_pyth_prices_msg(env.contract.address, total_new_attestations); + Ok(response + .add_message(inj_message) + .add_attribute("action", "update_price_feeds") + .add_attribute("num_attestations", format!("{num_total_attestations}")) + .add_attribute("num_updated", format!("{num_total_new_attestations}"))) + } + + #[cfg(not(feature = "injective"))] + { + Ok(response + .add_attribute("action", "update_price_feeds") + .add_attribute("num_attestations", format!("{num_total_attestations}")) + .add_attribute("num_updated", format!("{num_total_new_attestations}"))) + } } /// Execute a governance instruction provided as the VAA `data`. @@ -187,7 +220,7 @@ fn execute_governance_instruction( env: Env, _info: MessageInfo, data: &Binary, -) -> StdResult { +) -> StdResult> { let vaa = parse_and_verify_vaa(deps.branch(), env.block.time.seconds(), data)?; let state = config_read(deps.storage).load()?; verify_vaa_from_governance_source(&state, &vaa)?; @@ -282,7 +315,7 @@ fn transfer_governance( next_config: &mut ConfigInfo, current_config: &ConfigInfo, parsed_claim_vaa: &ParsedVAA, -) -> StdResult { +) -> StdResult> { let claim_vaa_instruction = GovernanceInstruction::deserialize(parsed_claim_vaa.payload.as_slice()) .map_err(|_| PythContractError::InvalidGovernancePayload)?; @@ -335,7 +368,7 @@ fn transfer_governance( /// Upgrades the contract at `address` to `new_code_id` (by sending a `Migrate` message). The /// migration will fail unless this contract is the admin of the contract being upgraded. /// (Typically, `address` is this contract's address, and the contract is its own admin.) -fn upgrade_contract(address: &Addr, new_code_id: u64) -> StdResult { +fn upgrade_contract(address: &Addr, new_code_id: u64) -> StdResult> { Ok(Response::new() .add_message(CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: address.to_string(), @@ -371,26 +404,23 @@ fn verify_vaa_from_governance_source(state: &ConfigInfo, vaa: &ParsedVAA) -> Std } /// Update the on-chain storage for any new price updates provided in `batch_attestation`. -fn process_batch_attestation( +fn process_batch_attestation<'a>( deps: &mut DepsMut, env: &Env, - batch_attestation: &BatchPriceAttestation, -) -> StdResult<(usize, usize)> { - let mut new_attestations_cnt: usize = 0; + batch_attestation: &'a BatchPriceAttestation, +) -> StdResult<(usize, Vec<&'a PriceAttestation>)> { + let mut new_attestations = vec![]; // Update prices for price_attestation in batch_attestation.price_attestations.iter() { let price_feed = create_price_feed_from_price_attestation(price_attestation); if update_price_feed_if_new(deps, env, price_feed)? { - new_attestations_cnt += 1; + new_attestations.push(price_attestation); } } - Ok(( - batch_attestation.price_attestations.len(), - new_attestations_cnt, - )) + Ok((batch_attestation.price_attestations.len(), new_attestations)) } fn create_price_feed_from_price_attestation(price_attestation: &PriceAttestation) -> PriceFeed { @@ -691,7 +721,7 @@ mod test { emitter_address: &[u8], emitter_chain: u16, funds: &[Coin], - ) -> StdResult { + ) -> StdResult> { let (mut deps, env) = setup_test(); config(&mut deps.storage).save(config_info).unwrap(); @@ -706,11 +736,11 @@ mod test { let attestations = BatchPriceAttestation { price_attestations: vec![], }; - let (total_attestations, new_attestations) = + let (num_attestations, new_attestations) = process_batch_attestation(&mut deps.as_mut(), &env, &attestations).unwrap(); - assert_eq!(total_attestations, 0); - assert_eq!(new_attestations, 0); + assert_eq!(num_attestations, 0); + assert_eq!(new_attestations.len(), 0); } #[test] @@ -824,7 +854,7 @@ mod test { let attestations = BatchPriceAttestation { price_attestations: vec![price_attestation], }; - let (total_attestations, new_attestations) = + let (num_attestations, new_attestations) = process_batch_attestation(&mut deps.as_mut(), &env, &attestations).unwrap(); @@ -834,8 +864,8 @@ mod test { let price = stored_price_feed.get_price_unchecked(); let ema_price = stored_price_feed.get_ema_price_unchecked(); - assert_eq!(total_attestations, 1); - assert_eq!(new_attestations, 1); + assert_eq!(num_attestations, 1); + assert_eq!(new_attestations.len(), 1); // for price assert_eq!(price.price, 99); @@ -876,7 +906,7 @@ mod test { let attestations = BatchPriceAttestation { price_attestations: vec![price_attestation], }; - let (total_attestations, new_attestations) = + let (num_attestations, new_attestations) = process_batch_attestation(&mut deps.as_mut(), &env, &attestations).unwrap(); @@ -886,8 +916,8 @@ mod test { let price = stored_price_feed.get_price_unchecked(); let ema_price = stored_price_feed.get_ema_price_unchecked(); - assert_eq!(total_attestations, 1); - assert_eq!(new_attestations, 1); + assert_eq!(num_attestations, 1); + assert_eq!(new_attestations.len(), 1); // for price assert_eq!(price.price, 100); @@ -1147,7 +1177,7 @@ mod test { fn apply_governance_vaa( initial_config: &ConfigInfo, vaa: &ParsedVAA, - ) -> StdResult<(Response, ConfigInfo)> { + ) -> StdResult<(Response, ConfigInfo)> { let (mut deps, env) = setup_test(); config(&mut deps.storage).save(initial_config).unwrap(); diff --git a/target_chains/cosmwasm/contracts/pyth/src/injective.rs b/target_chains/cosmwasm/contracts/pyth/src/injective.rs new file mode 100644 index 00000000..46e52984 --- /dev/null +++ b/target_chains/cosmwasm/contracts/pyth/src/injective.rs @@ -0,0 +1,83 @@ +use { + cosmwasm_std::{ + Addr, + CosmosMsg, + CustomMsg, + }, + pyth_wormhole_attester_sdk::PriceAttestation, + schemars::JsonSchema, + serde::{ + Deserialize, + Serialize, + }, +}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InjectivePriceAttestation { + pub price_id: String, + pub price: i64, + pub conf: u64, + pub expo: i32, + pub ema_price: i64, + pub ema_conf: u64, + pub ema_expo: i32, + pub publish_time: i64, +} + +impl From<&PriceAttestation> for InjectivePriceAttestation { + fn from(pa: &PriceAttestation) -> Self { + InjectivePriceAttestation { + price_id: pa.price_id.to_hex(), + price: pa.price, + conf: pa.conf, + expo: pa.expo, + ema_price: pa.ema_price, + ema_conf: pa.ema_conf, + ema_expo: pa.expo, + publish_time: pa.publish_time, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum InjectiveMsg { + RelayPythPrices { + sender: Addr, + price_attestations: Vec, + }, +} + + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InjectiveMsgWrapper { + pub route: String, + pub msg_data: InjectiveMsg, +} + +pub fn create_relay_pyth_prices_msg( + sender: Addr, + price_attestations: Vec, +) -> CosmosMsg { + InjectiveMsgWrapper { + route: "oracle".to_string(), + msg_data: InjectiveMsg::RelayPythPrices { + sender, + price_attestations: price_attestations + .iter() + .map(InjectivePriceAttestation::from) + .collect(), + }, + } + .into() +} + +impl From for CosmosMsg { + fn from(s: InjectiveMsgWrapper) -> CosmosMsg { + CosmosMsg::Custom(s) + } +} + +impl CustomMsg for InjectiveMsgWrapper { +} diff --git a/target_chains/cosmwasm/contracts/pyth/src/lib.rs b/target_chains/cosmwasm/contracts/pyth/src/lib.rs index a5402970..9d485196 100644 --- a/target_chains/cosmwasm/contracts/pyth/src/lib.rs +++ b/target_chains/cosmwasm/contracts/pyth/src/lib.rs @@ -5,3 +5,6 @@ pub mod contract; pub mod governance; pub mod msg; pub mod state; + +#[cfg(feature = "injective")] +mod injective;