diff --git a/framework/Cargo.lock b/framework/Cargo.lock index 50e0aef7d..6bad5c8ba 100644 --- a/framework/Cargo.lock +++ b/framework/Cargo.lock @@ -420,6 +420,29 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "abstract-oracle-standard" +version = "0.26.0" +dependencies = [ + "abstract-adapter", + "abstract-adapter-utils", + "abstract-interface", + "abstract-sdk", + "abstract-std", + "abstract-testing", + "anyhow", + "clap", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-orch 0.27.0", + "dotenv", + "env_logger 0.11.5", + "semver", + "thiserror", + "workspace-hack", +] + [[package]] name = "abstract-polytone" version = "2.0.0" diff --git a/framework/packages/standards/oracle/Cargo.toml b/framework/packages/standards/oracle/Cargo.toml new file mode 100644 index 000000000..c8fdf882d --- /dev/null +++ b/framework/packages/standards/oracle/Cargo.toml @@ -0,0 +1,45 @@ +[package] +description = "The oracle adapter is a Abstract adapter for querying oracle prices. It provides a common interface for all oracles" +name = "abstract-oracle-standard" + +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } + +exclude = ["contract.wasm", "hash.txt"] +resolver = "2" + + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["export"] +export = [] + +# Keep as is until TendermintStake updates. +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-asset = { workspace = true } +thiserror = { workspace = true } + +abstract-adapter = { version = "0.26.0", path = "../../abstract-adapter" } +abstract-adapter-utils = { workspace = true } +abstract-sdk = { workspace = true } +abstract-std = { workspace = true } +cw-orch = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +workspace-hack = { version = "0.1", path = "../../../workspace-hack" } + +[dev-dependencies] +abstract-interface = { workspace = true, features = ["daemon"] } +abstract-sdk = { workspace = true, features = ["test-utils"] } +abstract-testing = { workspace = true } +anyhow = { workspace = true } +clap = { workspace = true } +dotenv = "0.15.0" +env_logger = "0.11.3" +semver = { workspace = true } diff --git a/framework/packages/standards/oracle/README.md b/framework/packages/standards/oracle/README.md new file mode 100644 index 000000000..2c1e3ce41 --- /dev/null +++ b/framework/packages/standards/oracle/README.md @@ -0,0 +1,5 @@ +# Oracle Adapter Trait + +A trait that defines a standard interface for Oracle interactions. This trait should be implemented for each Oracle that the adapter supports. + +To implement this trait, create a new package, import this crate and implement the trait for your Oracle. diff --git a/framework/packages/standards/oracle/src/command.rs b/framework/packages/standards/oracle/src/command.rs new file mode 100644 index 000000000..a78e627c6 --- /dev/null +++ b/framework/packages/standards/oracle/src/command.rs @@ -0,0 +1,22 @@ +use abstract_adapter_utils::identity::Identify; +use abstract_sdk::feature_objects::AnsHost; +use cosmwasm_std::{Deps, Env}; + +use crate::error::OracleError; +use crate::msg::{PriceResponse, Seconds}; + +/// # OracleCommand +/// ensures Oracle adapters support the expected functionality. +/// +/// Implements the usual Oracle operations. +pub trait OracleCommand: Identify { + /// Return oracle price given pair id + fn price( + &self, + deps: Deps, + env: &Env, + ans_host: &AnsHost, + price_id: String, + no_older_than: Seconds, + ) -> Result; +} diff --git a/framework/packages/standards/oracle/src/error.rs b/framework/packages/standards/oracle/src/error.rs new file mode 100644 index 000000000..c5e036d7f --- /dev/null +++ b/framework/packages/standards/oracle/src/error.rs @@ -0,0 +1,60 @@ +use abstract_adapter::AdapterError; +use abstract_sdk::AbstractSdkError; +use abstract_std::{objects::ans_host::AnsHostError, AbstractError}; +use cosmwasm_std::StdError; +use cw_asset::AssetError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum OracleError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + AbstractOs(#[from] AbstractError), + + #[error(transparent)] + AbstractSdk(#[from] AbstractSdkError), + + #[error(transparent)] + Asset(#[from] AssetError), + + #[error(transparent)] + AdapterError(#[from] AdapterError), + + #[error(transparent)] + AnsHostError(#[from] AnsHostError), + + #[error("Oracle {0} is not a known oracle on this network.")] + UnknownOracle(String), + + #[error("Oracle {0} is not local to this network.")] + ForeignOracle(String), + + #[error("Asset type: {0} is unsupported.")] + UnsupportedAssetType(String), + + #[error("Can't provide liquidity with less than two assets")] + TooFewAssets {}, + + #[error("Can't provide liquidity with more than {0} assets")] + TooManyAssets(u8), + + #[error("Provided asset {0} not in pool with assets {1:?}.")] + ArgumentMismatch(String, Vec), + + #[error("Not implemented for oracle {0}")] + NotImplemented(String), + + #[error("Message generation for IBC queries not supported.")] + IbcMsgQuery, + + #[error("Invalid Generate Message")] + InvalidGenerateMessage, + + #[error("Pool address not specified. You need to specify it when using raw asset addresses or denom")] + PoolAddressEmpty, + + #[error("Only account of abstract namespace can update configuration")] + Unauthorized {}, +} diff --git a/framework/packages/standards/oracle/src/lib.rs b/framework/packages/standards/oracle/src/lib.rs new file mode 100644 index 000000000..e341caaed --- /dev/null +++ b/framework/packages/standards/oracle/src/lib.rs @@ -0,0 +1,11 @@ +mod command; +mod error; + +pub mod msg; + +// Export interface for use in SDK modules +pub use abstract_adapter_utils::{coins_in_assets, cw_approve_msgs, Identify}; +pub use command::OracleCommand; +pub use error::OracleError; + +pub const ORACLE_ADAPTER_ID: &str = "abstract:oracle"; diff --git a/framework/packages/standards/oracle/src/msg.rs b/framework/packages/standards/oracle/src/msg.rs new file mode 100644 index 000000000..e8d097f60 --- /dev/null +++ b/framework/packages/standards/oracle/src/msg.rs @@ -0,0 +1,48 @@ +#![warn(missing_docs)] +//! # Oracle Adapter API +// re-export response types +use abstract_std::adapter; +use cosmwasm_schema::QueryResponses; +use cosmwasm_std::{Decimal, Empty}; + +/// The name of the oracle to query prices from. +pub type OracleName = String; + +/// Top-level Abstract Adapter execute message. This is the message that is passed to the `execute` entrypoint of the smart-contract. +pub type ExecuteMsg = adapter::ExecuteMsg; +/// Top-level Abstract Adapter instantiate message. This is the message that is passed to the `instantiate` entrypoint of the smart-contract. +pub type InstantiateMsg = adapter::InstantiateMsg; +/// Top-level Abstract Adapter query message. This is the message that is passed to the `query` entrypoint of the smart-contract. +pub type QueryMsg = adapter::QueryMsg; + +impl adapter::AdapterQueryMsg for OracleQueryMsg {} + +/// Query messages for the oracle adapter +#[cosmwasm_schema::cw_serde] +#[derive(QueryResponses, cw_orch::QueryFns)] +pub enum OracleQueryMsg { + /// Query the latest price attached to the price source key + #[returns(PriceResponse)] + Price { + /// Identifier of the oracle value that you wish to query on the oracle + price_source_key: String, + /// Identifier of the oracle + oracle: OracleName, + /// Maximum age of the price + max_age: Seconds, + }, +} + +/// Alias to document time unit the oracle adapter expects data to be in. +pub type Seconds = u64; + +/// Price Response returned by an adapter query +#[cosmwasm_schema::cw_serde] +pub struct PriceResponse { + /// Price response + pub price: Decimal, +} + +/// No Config for this adapter +#[cosmwasm_schema::cw_serde] +pub struct Config {} diff --git a/integrations/Cargo.lock b/integrations/Cargo.lock index 0a687bc11..9a86ff7f0 100644 --- a/integrations/Cargo.lock +++ b/integrations/Cargo.lock @@ -290,6 +290,22 @@ dependencies = [ "neutron-std", ] +[[package]] +name = "abstract-oracle-standard" +version = "0.26.0" +dependencies = [ + "abstract-adapter", + "abstract-adapter-utils", + "abstract-sdk", + "abstract-std", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-orch 0.27.0", + "thiserror", + "workspace-hack", +] + [[package]] name = "abstract-osmosis-adapter" version = "0.26.0" @@ -305,6 +321,22 @@ dependencies = [ "osmosis-std", ] +[[package]] +name = "abstract-pyth-adapter" +version = "0.26.0" +dependencies = [ + "abstract-oracle-standard", + "abstract-sdk", + "abstract-staking-standard", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-utils", + "cw20", + "osmosis-std", + "pyth-sdk-cw", +] + [[package]] name = "abstract-registry" version = "0.26.0" @@ -415,6 +447,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -1211,6 +1254,51 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[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", + "proc-macro2", + "syn 1.0.109", +] + +[[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 1.0.109", +] + +[[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 1.0.109", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1871,7 +1959,7 @@ dependencies = [ "sha2 0.10.8", "thiserror", "tokio", - "toml", + "toml 0.8.19", "tonic", "uid", ] @@ -3149,6 +3237,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3161,7 +3258,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.11", ] [[package]] @@ -3170,7 +3267,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -3239,6 +3336,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -3657,7 +3757,7 @@ dependencies = [ "tiny-keccak", "tokio", "tokio-stream", - "toml", + "toml 0.8.19", "tonic", "tracing", "tracing-subscriber", @@ -4160,7 +4260,7 @@ version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b84733c0fed6085c9210b43ffb96248676c1e800d0ba38d15043275a792ffa4" dependencies = [ - "ahash", + "ahash 0.8.11", "async-broadcast", "async-stream", "async-trait", @@ -5081,6 +5181,15 @@ dependencies = [ "uint", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -5288,6 +5397,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "pyth-sdk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bf2540203ca3c7a5712fdb8b5897534b7f6a0b6e7b0923ff00466c5f9efcb3" +dependencies = [ + "borsh", + "borsh-derive", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pyth-sdk-cw" +version = "1.2.1" +source = "git+https://github.com/lvn-hasky-dragon/pyth-crosschain?branch=update-deps#9e8a673aae1e6bf36d60b8591a6d24792ad13111" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "pyth-sdk", + "thiserror", +] + [[package]] name = "quanta" version = "0.12.3" @@ -6534,7 +6667,7 @@ dependencies = [ "serde", "serde_json", "tendermint 0.39.1", - "toml", + "toml 0.8.19", "url", ] @@ -6548,7 +6681,7 @@ dependencies = [ "serde", "serde_json", "tendermint 0.40.0", - "toml", + "toml 0.8.19", "url", ] @@ -7008,6 +7141,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.19" @@ -7783,7 +7925,7 @@ dependencies = [ name = "workspace-hack" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.11", "aho-corasick", "anstream", "anyhow", diff --git a/integrations/Cargo.toml b/integrations/Cargo.toml index daaa51a0a..94406277d 100644 --- a/integrations/Cargo.toml +++ b/integrations/Cargo.toml @@ -7,6 +7,7 @@ members = [ "wyndex-adapter", "kujira-adapter", # "mars-adapter", + "oracles/pyth", "neutron-dex-adapter", ] @@ -28,27 +29,27 @@ version = "0.26.0" [workspace.dependencies] cosmwasm-schema = { version = "2.0" } -cosmwasm-std = { version = "2.0" } +cosmwasm-std = { version = "2.0" } cw-address-like = { version = "2.0" } -cw-asset = { version = "4.0" } -cw-controllers = "2.0" -cw-orch = { version = "0.27.0" } -cw-ownable = { version = "2.0" } -cw-plus-orch = { version = "0.25.0" } +cw-asset = { version = "4.0" } +cw-controllers = "2.0" +cw-orch = { version = "0.27.0" } +cw-ownable = { version = "2.0" } +cw-plus-orch = { version = "0.25.0" } cw-storage-plus = "2.0.0" -cw-utils = "2.0" -cw2 = "2.0.0" -cw20 = { version = "2.0.0" } -cw20-base = { version = "2.0.0" } +cw-utils = "2.0" +cw2 = "2.0.0" +cw20 = { version = "2.0.0" } +cw20-base = { version = "2.0.0" } anyhow = "1.0" -chrono = { version = "0.4.31", default-features = false } -clap = { version = "4.0.32", features = ["derive"] } -protobuf = { version = "2", features = ["with-bytes"] } -schemars = "0.8" -semver = "1.0" -serde = { version = "1.0", default-features = false, features = ["derive"] } +chrono = { version = "0.4.31", default-features = false } +clap = { version = "4.0.32", features = ["derive"] } +protobuf = { version = "2", features = ["with-bytes"] } +schemars = "0.8" +semver = "1.0" +serde = { version = "1.0", default-features = false, features = ["derive"] } thiserror = { version = "1.0.50" } ## crates in order of publishing ## see docs/Publishing.md @@ -61,28 +62,31 @@ abstract-std = { version = "0.26.0" } abstract-adapter-utils = { version = "0.26.0" } abstract-dex-standard = { version = "0.26.0" } abstract-money-market-standard = { version = "0.26.0" } +abstract-oracle-standard = { version = "0.26.0" } abstract-staking-standard = { version = "0.26.0" } -# TODO: REMOVE As soon as new dex-standard published [patch.crates-io] -abstract-adapter = { path = "../framework/packages/abstract-adapter" } -abstract-adapter-utils = { path = "../framework/packages/standards/utils" } -abstract-dex-standard = { path = "../framework/packages/standards/dex" } -abstract-interface = { path = "../framework/packages/abstract-interface" } -abstract-macros = { path = "../framework/packages/abstract-macros" } +abstract-macros = { path = "../framework/packages/abstract-macros" } +abstract-sdk = { path = "../framework/packages/abstract-sdk" } +abstract-std = { path = "../framework/packages/abstract-std" } + +abstract-adapter = { path = "../framework/packages/abstract-adapter" } +abstract-interface = { path = "../framework/packages/abstract-interface" } + +abstract-adapter-utils = { path = "../framework/packages/standards/utils" } +abstract-dex-standard = { path = "../framework/packages/standards/dex" } abstract-money-market-standard = { path = "../framework/packages/standards/money-market" } -abstract-sdk = { path = "../framework/packages/abstract-sdk" } -abstract-staking-standard = { path = "../framework/packages/standards/staking" } -abstract-std = { path = "../framework/packages/abstract-std" } +abstract-oracle-standard = { path = "../framework/packages/standards/oracle" } +abstract-staking-standard = { path = "../framework/packages/standards/staking" } # Backup release profile, will result in warnings during optimization [profile.release] -codegen-units = 1 -debug = false +codegen-units = 1 +debug = false debug-assertions = false -incremental = false -lto = true -opt-level = 3 -overflow-checks = true -panic = 'abort' -rpath = false +incremental = false +lto = true +opt-level = 3 +overflow-checks = true +panic = 'abort' +rpath = false diff --git a/integrations/oracles/pyth/Cargo.toml b/integrations/oracles/pyth/Cargo.toml new file mode 100644 index 000000000..faf0101d8 --- /dev/null +++ b/integrations/oracles/pyth/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = ["Abstract Money "] +description = "Abstract OracleCommand implementation for Pyth" +edition = "2021" +license = "MIT OR Apache-2.0" +name = "abstract-pyth-adapter" +version = "0.26.0" + +[features] +default = ["full_integration"] +full_integration = ["dep:cw20", "dep:cw-asset", "dep:cw-utils", "dep:osmosis-std"] + +[dependencies] +osmosis-std = { version = "0.26.0", optional = true } + +abstract-oracle-standard = { workspace = true } +abstract-sdk = { workspace = true } +abstract-staking-standard = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } +cw-asset = { workspace = true, optional = true } +cw-utils = { workspace = true, optional = true } +cw20 = { workspace = true, optional = true } + +cosmwasm-schema = { workspace = true } +# pyth-sdk-cw = "1.2.0" +pyth-sdk-cw = { git = "https://github.com/lvn-hasky-dragon/pyth-crosschain", branch = "update-deps" } diff --git a/integrations/oracles/pyth/README.md b/integrations/oracles/pyth/README.md new file mode 100644 index 000000000..b3bd3d8d0 --- /dev/null +++ b/integrations/oracles/pyth/README.md @@ -0,0 +1,15 @@ +Pyth oracle is available on the following chains: + +## Testnets + +- Xion Testnet +- Neutron Testnet + +## Mainnets + +- Osmosis Mainnet +- Neutron Mainnet +- Osmosis Mainnet + + +The available prices can be found under this link: https://www.pyth.network/developers/price-feed-ids#stable \ No newline at end of file diff --git a/integrations/oracles/pyth/src/lib.rs b/integrations/oracles/pyth/src/lib.rs new file mode 100644 index 000000000..d75b81b3d --- /dev/null +++ b/integrations/oracles/pyth/src/lib.rs @@ -0,0 +1,90 @@ +pub const AVAILABLE_CHAINS: &[&str] = &["xion", "xion-testnet"]; +pub const PYTH: &str = "pyth"; +use abstract_oracle_standard::Identify; + +#[derive(Default)] +pub struct Pyth {} + +impl Identify for Pyth { + fn is_available_on(&self, chain_name: &str) -> bool { + AVAILABLE_CHAINS.contains(&chain_name) + } + fn name(&self) -> &'static str { + PYTH + } +} + +#[cfg(feature = "full_integration")] +use { + abstract_oracle_standard::{msg::PriceResponse, OracleCommand, OracleError}, + abstract_sdk::feature_objects::AnsHost, + abstract_sdk::std::objects::ContractEntry, + cosmwasm_std::{Decimal, Deps, Env, StdError}, + pyth_sdk_cw::{query_price_feed, PriceFeedResponse, PriceIdentifier}, +}; + +pub const XION_TEST: &str = "xion1w39ctwxxhxxc2kxarycjxj9rndn65gf8daek7ggarwh3rq3zl0lqqllnmt"; + +#[cfg(feature = "full_integration")] +/// Pyth oracle implementation +impl OracleCommand for Pyth { + fn price( + &self, + deps: Deps, + env: &Env, + ans_host: &AnsHost, + price_id: String, + no_older_than: u64, + ) -> Result { + let pyth_address = ans_host.query_contract( + &deps.querier, + &ContractEntry { + protocol: PYTH.to_string(), + contract: "oracle".to_string(), + }, + )?; + + // We retrieve the pyth address for the current chain + let price_feed_response: PriceFeedResponse = query_price_feed( + &deps.querier, + pyth_address, + PriceIdentifier::from_hex(price_id.as_bytes()) + .map_err(|e| StdError::generic_err(format!("Wrong price id hex format, {e}")))?, + )?; + let price_feed = price_feed_response.price_feed; + + let current_price = price_feed + .get_price_no_older_than(env.block.time.seconds() as i64, no_older_than) + .ok_or_else(|| StdError::not_found("Current price is not available"))?; + + let power_ten = if current_price.expo < 0 { + Decimal::from_ratio(1u64, 10u64).pow( + (-current_price.expo) + .try_into() + .expect("Wrong power_of_ten logic"), + ) + } else { + Decimal::from_ratio(10u64, 1u64).pow( + current_price + .expo + .try_into() + .expect("Wrong power_of_ten logic"), + ) + }; + + let unsigned_price: u64 = current_price + .price + .try_into() + .expect("Price can't be negative"); + Ok(PriceResponse { + price: power_ten * Decimal::from_ratio(unsigned_price, 1u64), + }) + } +} + +#[cfg(feature = "full_integration")] +impl abstract_sdk::features::ModuleIdentification for Pyth { + fn module_id(&self) -> abstract_sdk::std::objects::module::ModuleId<'static> { + abstract_oracle_standard::ORACLE_ADAPTER_ID + } +} diff --git a/interchain/Cargo.lock b/interchain/Cargo.lock index 63d5b4b61..3f40e64f1 100644 --- a/interchain/Cargo.lock +++ b/interchain/Cargo.lock @@ -469,6 +469,7 @@ dependencies = [ "abstract-dex-adapter", "abstract-interface", "abstract-money-market-adapter", + "abstract-oracle-adapter", "anyhow", "astrovault", "cosmwasm-std", @@ -477,7 +478,10 @@ dependencies = [ "cw-orch-clone-testing", "cw20", "env_logger 0.11.5", + "hex", "lazy_static", + "pyth-sdk-cw", + "reqwest 0.12.9", "serde", "serde_json", "tokio", @@ -527,6 +531,35 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "abstract-oracle-adapter" +version = "0.26.0" +dependencies = [ + "abstract-adapter", + "abstract-client", + "abstract-oracle-standard", + "abstract-pyth-adapter", + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch 0.27.0", +] + +[[package]] +name = "abstract-oracle-standard" +version = "0.26.0" +dependencies = [ + "abstract-adapter", + "abstract-adapter-utils", + "abstract-sdk", + "abstract-std", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-orch 0.27.0", + "thiserror 1.0.69", + "workspace-hack", +] + [[package]] name = "abstract-osmosis-adapter" version = "0.26.0" @@ -650,6 +683,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "abstract-pyth-adapter" +version = "0.26.0" +dependencies = [ + "abstract-oracle-standard", + "abstract-sdk", + "abstract-staking-standard", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-utils", + "cw20", + "osmosis-std", + "pyth-sdk-cw", +] + [[package]] name = "abstract-registry" version = "0.26.0" @@ -1695,6 +1744,51 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[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", + "proc-macro2", + "syn 1.0.109", +] + +[[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 1.0.109", +] + +[[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 1.0.109", +] + [[package]] name = "bs58" version = "0.5.1" @@ -2759,7 +2853,7 @@ dependencies = [ "sha2 0.10.8", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", "tonic", "uuid", ] @@ -2806,7 +2900,7 @@ dependencies = [ "sha2 0.10.8", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", "tonic", "uid", ] @@ -2853,7 +2947,7 @@ dependencies = [ "sha2 0.10.8", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", "tonic", "uid", ] @@ -4395,6 +4489,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -4494,6 +4597,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -4940,7 +5046,7 @@ dependencies = [ "tiny-keccak", "tokio", "tokio-stream", - "toml", + "toml 0.8.19", "tonic", "tracing", "tracing-subscriber", @@ -6346,6 +6452,15 @@ dependencies = [ "uint", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -6544,6 +6659,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pyth-sdk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bf2540203ca3c7a5712fdb8b5897534b7f6a0b6e7b0923ff00466c5f9efcb3" +dependencies = [ + "borsh", + "borsh-derive", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pyth-sdk-cw" +version = "1.2.1" +source = "git+https://github.com/lvn-hasky-dragon/pyth-crosschain?branch=update-deps#9e8a673aae1e6bf36d60b8591a6d24792ad13111" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "pyth-sdk", + "thiserror 1.0.69", +] + [[package]] name = "quanta" version = "0.12.4" @@ -6792,6 +6931,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.7", @@ -7998,7 +8138,7 @@ dependencies = [ "serde", "serde_json", "tendermint 0.39.1", - "toml", + "toml 0.8.19", "url", ] @@ -8012,7 +8152,7 @@ dependencies = [ "serde", "serde_json", "tendermint 0.40.0", - "toml", + "toml 0.8.19", "url", ] @@ -8449,6 +8589,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.19" diff --git a/interchain/Cargo.toml b/interchain/Cargo.toml index c2437596f..e28355bc5 100644 --- a/interchain/Cargo.toml +++ b/interchain/Cargo.toml @@ -80,6 +80,7 @@ challenge-app = { path = "../modules/contracts/apps/challenge" } abstract-cw-staking = { path = "../modules/contracts/adapters/cw-staking" } abstract-dex-adapter = { path = "../modules/contracts/adapters/dex" } abstract-money-market-adapter = { path = "../modules/contracts/adapters/money-market" } +abstract-oracle-adapter = { path = "../modules/contracts/adapters/oracle" } abstract-xion = { package = "account", version = "0.1.0", git = "https://github.com/abstractsdk/xion-contracts", features = ["library"], branch = "fix-for-abstract" } @@ -97,6 +98,7 @@ abstract-dex-standard = { path = "../framework/packages/standards/dex" abstract-interface = { path = "../framework/packages/abstract-interface" } abstract-macros = { path = "../framework/packages/abstract-macros" } abstract-money-market-standard = { path = "../framework/packages/standards/money-market" } +abstract-oracle-standard = { path = "../framework/packages/standards/oracle" } abstract-sdk = { path = "../framework/packages/abstract-sdk" } abstract-staking-standard = { path = "../framework/packages/standards/staking" } abstract-std = { path = "../framework/packages/abstract-std" } diff --git a/interchain/modules-clone-testing/Cargo.toml b/interchain/modules-clone-testing/Cargo.toml index 67f545976..3d89cf66d 100644 --- a/interchain/modules-clone-testing/Cargo.toml +++ b/interchain/modules-clone-testing/Cargo.toml @@ -44,5 +44,18 @@ abstract-money-market-adapter = { workspace = true, features = [ # kujira = { version = "0.8" } astrovault = { version = "2.0.1" } + +abstract-oracle-adapter = { workspace = true, features = [ + "testing", + "pyth", + # "ghost", +] } + +# Pyth sdk +# pyth-sdk-cw = "1.2.0" +pyth-sdk-cw = { git = "https://github.com/lvn-hasky-dragon/pyth-crosschain", branch = "update-deps" } + +hex = "0.4.3" +reqwest = { version = "0.12.9", features = ["blocking"] } serde = "1" serde_json = "1" diff --git a/interchain/modules-clone-testing/tests/oracle/mod.rs b/interchain/modules-clone-testing/tests/oracle/mod.rs new file mode 100644 index 000000000..8cbcc4045 --- /dev/null +++ b/interchain/modules-clone-testing/tests/oracle/mod.rs @@ -0,0 +1 @@ +pub mod pyth; diff --git a/interchain/modules-clone-testing/tests/oracle/pyth/integration.rs b/interchain/modules-clone-testing/tests/oracle/pyth/integration.rs new file mode 100644 index 000000000..ab8cf30b4 --- /dev/null +++ b/interchain/modules-clone-testing/tests/oracle/pyth/integration.rs @@ -0,0 +1,128 @@ +use std::str::FromStr; + +use super::pyth_api::PythApiResponse; +use abstract_app::{objects::UncheckedContractEntry, std::ans_host::QueryMsgFns}; +use abstract_client::AbstractClient; +use abstract_interface::ExecuteMsgFns; +use abstract_oracle_adapter::interface::deployment::pyth_addresses; +use abstract_oracle_adapter::{ + oracle_tester::{MockOracle, OracleTester}, + oracles::PYTH, +}; +use cosmwasm_std::{Addr, Binary, Uint128}; +use cw_orch::daemon::networks::XION_TESTNET_1; +use cw_orch::prelude::*; +use cw_orch_clone_testing::CloneTesting; +use networks::{NEUTRON_1, OSMOSIS_1, OSMO_5, PION_1}; + +pub use super::{ORACLE_PRICE_API, PRICE_SOURCE_KEY}; + +pub struct PythOracleTester { + pub current_oracle_price_data: PythApiResponse, + pub pyth_address: Addr, +} + +impl MockOracle for PythOracleTester { + const MAX_AGE: u64 = 60; + fn price_source_key(&self) -> String { + PRICE_SOURCE_KEY.to_string() + } + + fn name(&self) -> String { + PYTH.to_string() + } + + fn ans_setup(&self, abstr_deployment: &AbstractClient) -> anyhow::Result<()> { + abstr_deployment.name_service().update_contract_addresses( + vec![( + UncheckedContractEntry { + protocol: PYTH.to_string(), + contract: "oracle".to_string(), + }, + self.pyth_address.to_string(), + )], + vec![], + )?; + Ok(()) + } +} + +fn setup_clone_testing( + chain: ChainInfo, +) -> anyhow::Result> { + let clone_testing = CloneTesting::new(chain.clone())?; + + let pyth_address = pyth_addresses().get(chain.chain_id).unwrap().clone(); + + let price_data: PythApiResponse = + reqwest::blocking::get(format!("{}{}", ORACLE_PRICE_API, PRICE_SOURCE_KEY))?.json()?; + + let update_data: Vec = price_data + .binary + .data + .iter() + .map(|d| Binary::new(hex::decode(d).unwrap())) + .collect(); + + // We send an update to the oracle contract (no update for now) + let update_fee: Coin = clone_testing.query( + &pyth_sdk_cw::QueryMsg::GetUpdateFee { + vaas: update_data.clone(), + }, + &pyth_address, + )?; + clone_testing.add_balance(&clone_testing.sender, vec![update_fee.clone()])?; + clone_testing.execute( + &pyth_sdk_cw::ExecuteMsg::UpdatePriceFeeds { + data: update_data.clone(), + }, + &[update_fee], + &pyth_address, + )?; + + let abstr_deployment = AbstractClient::new(clone_testing.clone())?; + let abstract_admin = abstr_deployment.name_service().ownership()?.owner.unwrap(); + let abstr_deployment = abstr_deployment.call_as(&Addr::unchecked(abstract_admin)); + + let tester = PythOracleTester { + current_oracle_price_data: price_data, + pyth_address, + }; + OracleTester::new(abstr_deployment, tester) +} + +fn test_price_query(chain: ChainInfo) -> anyhow::Result<()> { + let oracle_tester = setup_clone_testing(chain)?; + let current_price = oracle_tester.test_price()?; + + let raw_price = oracle_tester.oracle.current_oracle_price_data.parsed[0] + .price + .price + .clone(); + // We assume this price has 8 decimals + let price = Uint128::from_str(&raw_price)? / Uint128::from(100_000_000u128); + assert_eq!(current_price.price.to_uint_floor(), price); + + Ok(()) +} + +#[test] +fn test_xion() { + test_price_query(XION_TESTNET_1).unwrap(); +} +#[test] +fn test_osmo_test() { + test_price_query(OSMO_5).unwrap(); +} +#[test] +fn test_pion() { + test_price_query(PION_1).unwrap(); +} +#[test] +fn test_osmosis() { + test_price_query(OSMOSIS_1).unwrap(); +} +#[test] +fn test_neutron() { + test_price_query(NEUTRON_1).unwrap(); +} diff --git a/interchain/modules-clone-testing/tests/oracle/pyth/live.rs b/interchain/modules-clone-testing/tests/oracle/pyth/live.rs new file mode 100644 index 000000000..4595e6f2a --- /dev/null +++ b/interchain/modules-clone-testing/tests/oracle/pyth/live.rs @@ -0,0 +1,119 @@ +use std::str::FromStr; + +use super::pyth_api::PythApiResponse; +use abstract_app::{objects::ContractEntry, std::ans_host::QueryMsgFns}; +use abstract_client::AbstractClient; +use abstract_oracle_adapter::{ + oracle_tester::{MockOracle, OracleTester}, + oracles::PYTH, +}; +use cosmwasm_std::{Binary, Uint128}; +use cw_orch::daemon::networks::XION_TESTNET_1; +use cw_orch::prelude::*; +use cw_orch_clone_testing::CloneTesting; +use networks::{NEUTRON_1, OSMOSIS_1, OSMO_5, PION_1}; + +pub use super::{ORACLE_PRICE_API, PRICE_SOURCE_KEY}; + +pub struct PythOracleTester { + pub current_oracle_price_data: PythApiResponse, +} + +impl MockOracle for PythOracleTester { + const MAX_AGE: u64 = 60; + fn price_source_key(&self) -> String { + PRICE_SOURCE_KEY.to_string() + } + + fn name(&self) -> String { + PYTH.to_string() + } + + fn ans_setup(&self, _abstr_deployment: &AbstractClient) -> anyhow::Result<()> { + Ok(()) + } +} + +fn setup_clone_testing( + chain: ChainInfo, +) -> anyhow::Result> { + let clone_testing = CloneTesting::new(chain.clone())?; + let abstr_deployment = AbstractClient::new(clone_testing.clone())?; + + let pyth_address = abstr_deployment + .name_service() + .contracts(vec![ContractEntry { + protocol: PYTH.to_string(), + contract: "oracle".to_string(), + }])? + .contracts[0] + .1 + .clone(); + + let price_data: PythApiResponse = + reqwest::blocking::get(format!("{}{}", ORACLE_PRICE_API, PRICE_SOURCE_KEY))?.json()?; + + let update_data: Vec = price_data + .binary + .data + .iter() + .map(|d| Binary::new(hex::decode(d).unwrap())) + .collect(); + + // We send an update to the oracle contract + let update_fee: Coin = clone_testing.query( + &pyth_sdk_cw::QueryMsg::GetUpdateFee { + vaas: update_data.clone(), + }, + &pyth_address, + )?; + clone_testing.add_balance(&clone_testing.sender, vec![update_fee.clone()])?; + clone_testing.execute( + &pyth_sdk_cw::ExecuteMsg::UpdatePriceFeeds { + data: update_data.clone(), + }, + &[update_fee], + &pyth_address, + )?; + + let tester = PythOracleTester { + current_oracle_price_data: price_data, + }; + OracleTester::new_live(abstr_deployment, tester) +} + +fn test_price_query(chain: ChainInfo) -> anyhow::Result<()> { + let oracle_tester = setup_clone_testing(chain)?; + let current_price = oracle_tester.test_price()?; + + let raw_price = oracle_tester.oracle.current_oracle_price_data.parsed[0] + .price + .price + .clone(); + // We assume this price has 8 decimals + let price = Uint128::from_str(&raw_price)? / Uint128::from(100_000_000u128); + assert_eq!(current_price.price.to_uint_floor(), price); + + Ok(()) +} + +#[test] +fn test_xion() { + test_price_query(XION_TESTNET_1).unwrap(); +} +#[test] +fn test_osmo_test() { + test_price_query(OSMO_5).unwrap(); +} +#[test] +fn test_pion() { + test_price_query(PION_1).unwrap(); +} +#[test] +fn test_osmosis() { + test_price_query(OSMOSIS_1).unwrap(); +} +#[test] +fn test_neutron() { + test_price_query(NEUTRON_1).unwrap(); +} diff --git a/interchain/modules-clone-testing/tests/oracle/pyth/mod.rs b/interchain/modules-clone-testing/tests/oracle/pyth/mod.rs new file mode 100644 index 000000000..f8c556927 --- /dev/null +++ b/interchain/modules-clone-testing/tests/oracle/pyth/mod.rs @@ -0,0 +1,32 @@ +/// Integration is used to test the adapter implementation +pub mod integration; +/// Live is used to test the adapter deployment and make sure it's working as expected on the live chain +pub mod live; + +// Use https://hermes.pyth.network/docs/#/rest/latest_price_updates to query latest update +pub const ORACLE_PRICE_API: &str = "https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D="; +pub const PRICE_SOURCE_KEY: &str = + "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + +pub mod pyth_api { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + pub struct PythApiResponse { + pub binary: PythApiResponseBinary, + pub parsed: Vec, + } + + #[derive(Serialize, Deserialize)] + pub struct PythApiResponseBinary { + pub data: Vec, + } + #[derive(Serialize, Deserialize)] + pub struct PythApiResponseparsed { + pub price: PythApiResponsePrice, + } + #[derive(Serialize, Deserialize)] + pub struct PythApiResponsePrice { + pub price: String, + } +} diff --git a/interchain/modules-clone-testing/tests/tests.rs b/interchain/modules-clone-testing/tests/tests.rs index 7289e2278..45151acfe 100644 --- a/interchain/modules-clone-testing/tests/tests.rs +++ b/interchain/modules-clone-testing/tests/tests.rs @@ -1,3 +1,4 @@ mod dex; mod money_market; +mod oracle; mod staking; diff --git a/interchain/scripts/state.json b/interchain/scripts/state.json index 6dc834473..b0f221599 100644 --- a/interchain/scripts/state.json +++ b/interchain/scripts/state.json @@ -6,6 +6,7 @@ "abstract:ibc-client": 11879, "abstract:ibc-host": 11880, "abstract:module-factory": 11877, + "abstract:oracle": 8328, "abstract:registry": 11876, "cw:blob": 11289 }, @@ -16,6 +17,7 @@ "abstract:ibc-client": "osmo1lgszd37wflh8pscc0weu6us3y3edweygvjzaflgu4g6uwdjqpr0s0ssly2", "abstract:ibc-host": "osmo12zry8tafmejuyqlw800f3ygdt25hukjugaknlpe5hr7txv72qkcqqxlpug", "abstract:module-factory": "osmo136099hg6u4r7evahj3w0aau9jceyhf2qm82d7d8rd9apvdyck8ssnkhte9", + "abstract:oracle": "neutron1e43jgpsqeae5euc2jrzp79vn6gv3w2gwtlnw372w6tlmhwlw04jqmgklqn", "abstract:registry": "osmo1j33edjnk9cqkuw83fvz3f646unxpkcjkru0znjhznr0dzvpzyw7q6vf7hp" }, "version": "0.26.0" @@ -69,6 +71,7 @@ "abstract:ibc-client": 1660, "abstract:ibc-host": 1661, "abstract:module-factory": 1658, + "abstract:oracle": 1395, "abstract:registry": 1657, "cw:blob": 1649 }, @@ -79,8 +82,9 @@ "abstract:ibc-client": "xion1nu72stzytgf40d5f45wxqjpq2epjjukcewl9zml2plyk72mvcg3qrqwktr", "abstract:ibc-host": "xion1652e4ktmc9yhk58fz82mju7fhnwhcw2pz9h7cld6qnuyka4aqttq4x3w86", "abstract:module-factory": "xion136099hg6u4r7evahj3w0aau9jceyhf2qm82d7d8rd9apvdyck8ssu7ulhc", + "abstract:oracle": "xion19g9zd2vjpwr8ycfp5zpd9txtfzdryp20fa75zwrpp336uaqdazxshcud90", "abstract:registry": "xion1j33edjnk9cqkuw83fvz3f646unxpkcjkru0znjhznr0dzvpzyw7q4yz2eu" }, "version": "0.26.0" } -} \ No newline at end of file +} diff --git a/modules/Cargo.lock b/modules/Cargo.lock index 0f13992f5..5cc27f291 100644 --- a/modules/Cargo.lock +++ b/modules/Cargo.lock @@ -474,6 +474,47 @@ dependencies = [ "neutron-std", ] +[[package]] +name = "abstract-oracle-adapter" +version = "0.26.0" +dependencies = [ + "abstract-adapter", + "abstract-client", + "abstract-interface", + "abstract-oracle-adapter", + "abstract-oracle-standard", + "abstract-pyth-adapter", + "anyhow", + "bip32", + "clap 4.5.23", + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch 0.27.0", + "cw-utils", + "cw20", + "cw20-base", + "dotenv", + "env_logger 0.11.5", + "semver", + "tokio", +] + +[[package]] +name = "abstract-oracle-standard" +version = "0.26.0" +dependencies = [ + "abstract-adapter", + "abstract-adapter-utils", + "abstract-sdk", + "abstract-std", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-orch 0.27.0", + "thiserror 1.0.69", + "workspace-hack", +] + [[package]] name = "abstract-osmosis-adapter" version = "0.26.0" @@ -551,6 +592,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "abstract-pyth-adapter" +version = "0.26.0" +dependencies = [ + "abstract-oracle-standard", + "abstract-sdk", + "abstract-staking-standard", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-utils", + "cw20", + "osmosis-std 0.26.0", + "pyth-sdk-cw", +] + [[package]] name = "abstract-registry" version = "0.26.0" @@ -729,6 +786,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -1562,6 +1630,51 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[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", + "proc-macro2", + "syn 1.0.109", +] + +[[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 1.0.109", +] + +[[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 1.0.109", +] + [[package]] name = "bs58" version = "0.5.1" @@ -2419,7 +2532,7 @@ dependencies = [ "sha2 0.10.8", "thiserror 1.0.69", "tokio", - "toml", + "toml 0.8.19", "tonic", "uid", ] @@ -3834,6 +3947,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3846,7 +3968,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.11", ] [[package]] @@ -3855,7 +3977,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -3939,6 +4061,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hex-conservative" @@ -4357,7 +4482,7 @@ dependencies = [ "tiny-keccak", "tokio", "tokio-stream", - "toml", + "toml 0.8.19", "tonic", "tracing", "tracing-subscriber", @@ -4873,7 +4998,7 @@ version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b84733c0fed6085c9210b43ffb96248676c1e800d0ba38d15043275a792ffa4" dependencies = [ - "ahash", + "ahash 0.8.11", "async-broadcast", "async-stream", "async-trait", @@ -5988,6 +6113,15 @@ dependencies = [ "uint", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -6172,6 +6306,30 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pyth-sdk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bf2540203ca3c7a5712fdb8b5897534b7f6a0b6e7b0923ff00466c5f9efcb3" +dependencies = [ + "borsh", + "borsh-derive", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pyth-sdk-cw" +version = "1.2.1" +source = "git+https://github.com/lvn-hasky-dragon/pyth-crosschain?branch=update-deps#9e8a673aae1e6bf36d60b8591a6d24792ad13111" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "pyth-sdk", + "thiserror 1.0.69", +] + [[package]] name = "quanta" version = "0.12.4" @@ -7476,7 +7634,7 @@ dependencies = [ "serde", "serde_json", "tendermint 0.39.1", - "toml", + "toml 0.8.19", "url", ] @@ -7490,7 +7648,7 @@ dependencies = [ "serde", "serde_json", "tendermint 0.40.0", - "toml", + "toml 0.8.19", "url", ] @@ -8016,6 +8174,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.19" @@ -8802,7 +8969,7 @@ dependencies = [ name = "workspace-hack" version = "0.1.0" dependencies = [ - "ahash", + "ahash 0.8.11", "aho-corasick", "anstream", "anyhow", diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 3c202d90d..76454a848 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -64,7 +64,7 @@ abstract-adapter-utils = { version = "0.26.0" } abstract-dex-standard = { version = "0.26.0" } abstract-money-market-standard = { version = "0.26.0" } abstract-staking-standard = { version = "0.26.0" } - +abstract-oracle-standard = { version = "0.26.0" } # Integrations abstract-astroport-adapter = { path = "../integrations/astroport-adapter", default-features = false } @@ -90,19 +90,24 @@ rstest = "0.17.0" # this ensures local compatability when compiling locally [patch.crates-io] -abstract-adapter = { path = "../framework/packages/abstract-adapter" } -abstract-adapter-utils = { path = "../framework/packages/standards/utils" } -abstract-app = { path = "../framework/packages/abstract-app" } -abstract-client = { path = "../framework/packages/abstract-client" } +abstract-macros = { path = "../framework/packages/abstract-macros" } +abstract-sdk = { path = "../framework/packages/abstract-sdk" } +abstract-std = { path = "../framework/packages/abstract-std" } + +abstract-client = { path = "../framework/packages/abstract-client" } +abstract-interface = { path = "../framework/packages/abstract-interface" } + +abstract-adapter = { path = "../framework/packages/abstract-adapter" } +abstract-adapter-utils = { path = "../framework/packages/standards/utils" } +abstract-app = { path = "../framework/packages/abstract-app" } +abstract-standalone = { path = "../framework/packages/abstract-standalone" } + abstract-dex-standard = { path = "../framework/packages/standards/dex" } -abstract-interface = { path = "../framework/packages/abstract-interface" } -abstract-macros = { path = "../framework/packages/abstract-macros" } abstract-money-market-standard = { path = "../framework/packages/standards/money-market" } -abstract-sdk = { path = "../framework/packages/abstract-sdk" } +abstract-oracle-standard = { path = "../framework/packages/standards/oracle" } abstract-staking-standard = { path = "../framework/packages/standards/staking" } -abstract-standalone = { path = "../framework/packages/abstract-standalone" } -abstract-std = { path = "../framework/packages/abstract-std" } -abstract-testing = { path = "../framework/packages/abstract-testing" } + +abstract-testing = { path = "../framework/packages/abstract-testing" } # Backup release profile, will result in warnings during optimization [profile.release] diff --git a/modules/artifacts/abstract_cw_staking-archway.wasm b/modules/artifacts/abstract_cw_staking-archway.wasm index 9d3baf34d..24d7533fa 100644 Binary files a/modules/artifacts/abstract_cw_staking-archway.wasm and b/modules/artifacts/abstract_cw_staking-archway.wasm differ diff --git a/modules/artifacts/abstract_cw_staking-osmosis.wasm b/modules/artifacts/abstract_cw_staking-osmosis.wasm index 011985ba3..60cbf1bfb 100644 Binary files a/modules/artifacts/abstract_cw_staking-osmosis.wasm and b/modules/artifacts/abstract_cw_staking-osmosis.wasm differ diff --git a/modules/artifacts/abstract_cw_staking.wasm b/modules/artifacts/abstract_cw_staking.wasm index d3796d97c..6aeb5af3e 100644 Binary files a/modules/artifacts/abstract_cw_staking.wasm and b/modules/artifacts/abstract_cw_staking.wasm differ diff --git a/modules/artifacts/abstract_dex_adapter-archway.wasm b/modules/artifacts/abstract_dex_adapter-archway.wasm index 53d940ea7..daedafde6 100644 Binary files a/modules/artifacts/abstract_dex_adapter-archway.wasm and b/modules/artifacts/abstract_dex_adapter-archway.wasm differ diff --git a/modules/artifacts/abstract_dex_adapter-osmosis.wasm b/modules/artifacts/abstract_dex_adapter-osmosis.wasm index 1de78e401..3cc5e726f 100644 Binary files a/modules/artifacts/abstract_dex_adapter-osmosis.wasm and b/modules/artifacts/abstract_dex_adapter-osmosis.wasm differ diff --git a/modules/artifacts/abstract_dex_adapter.wasm b/modules/artifacts/abstract_dex_adapter.wasm index 13455ceb7..8e27cf74e 100644 Binary files a/modules/artifacts/abstract_dex_adapter.wasm and b/modules/artifacts/abstract_dex_adapter.wasm differ diff --git a/modules/artifacts/abstract_money_market_adapter.wasm b/modules/artifacts/abstract_money_market_adapter.wasm index 5024da89a..46254a25e 100644 Binary files a/modules/artifacts/abstract_money_market_adapter.wasm and b/modules/artifacts/abstract_money_market_adapter.wasm differ diff --git a/modules/artifacts/abstract_subscription.wasm b/modules/artifacts/abstract_subscription.wasm index cb48b9c9a..9dbe7f39e 100644 Binary files a/modules/artifacts/abstract_subscription.wasm and b/modules/artifacts/abstract_subscription.wasm differ diff --git a/modules/artifacts/abstract_tendermint_staking_adapter.wasm b/modules/artifacts/abstract_tendermint_staking_adapter.wasm index 8eabc221f..85d97fd30 100644 Binary files a/modules/artifacts/abstract_tendermint_staking_adapter.wasm and b/modules/artifacts/abstract_tendermint_staking_adapter.wasm differ diff --git a/modules/artifacts/calendar_app.wasm b/modules/artifacts/calendar_app.wasm index 34c414b40..062f6c6f1 100644 Binary files a/modules/artifacts/calendar_app.wasm and b/modules/artifacts/calendar_app.wasm differ diff --git a/modules/artifacts/challenge_app.wasm b/modules/artifacts/challenge_app.wasm index ba5c06dfd..585789cb2 100644 Binary files a/modules/artifacts/challenge_app.wasm and b/modules/artifacts/challenge_app.wasm differ diff --git a/modules/artifacts/checksums.txt b/modules/artifacts/checksums.txt index 5834f61af..dd06bdb81 100644 --- a/modules/artifacts/checksums.txt +++ b/modules/artifacts/checksums.txt @@ -1,20 +1,18 @@ -47c25671fd13eb826753214f45a636e1842428b2f9bf36a96275d233a07cc3cb abstract_cw_staking-archway.wasm -7b949c27c5c689f52bd766717836b1e45d15a69143f6ee96286618a8c1dd0aa7 abstract_cw_staking-juno.wasm -064c8e6ddc6a869ce38610a9bcf3c965c16c20ddd83b2fac1224ccfb18e3e79f abstract_cw_staking-kujira.wasm -8287fdd50bbccd4b8c9ff19f26ae787a71aa8220b18b17c3c24d72e6819dce88 abstract_cw_staking-osmosis.wasm -ab0a32ad428e349aa2bbf30e3145dc441afb5d46d1a8c2fa6fa17a25b7418d64 abstract_cw_staking.wasm -8bc1497f05b690909b1a08cb8a6df3755ab191e717fc7fa9f6fb6fdd710e41fb abstract_dex_adapter-archway.wasm -7327e4dc3e8d07479de59883239665d2f4629e5fa50d8a3a952e85151f150f88 abstract_dex_adapter-juno.wasm -122db72a5ac926b07e42268519e8fb49366a955218cfac759729eb565583d8cf abstract_dex_adapter-kujira.wasm -22cc263e1c9208078ecaced94cf44f2366dae79c02bb2f93a3de273594a68056 abstract_dex_adapter-neutron.wasm -b1819400daeb4ee7db917058b9123c9e96dae3857fc55a3616833a9db7394636 abstract_dex_adapter-osmosis.wasm -32a54285168ad2810d308c8658861a6712422ab3512659b6eb3817af8c53c477 abstract_dex_adapter.wasm -ae0258bb7103c8df3a60d84ce9302bbc4a5be8afdeb6ce36ddcfdd7041fa09b3 abstract_money_market_adapter-kujira.wasm -5fc44999086c8bbe31193619a8f7d5a2481a88380b0779a6e204f2e7be4041bc abstract_money_market_adapter.wasm -97922fc3373a29f09a962fc6ebd7a3e4d09526046d46236dce01719921e0b632 abstract_subscription.wasm -e6fe6f333a8458d0979f00f9b90815de3c036f936f47b7b4c26102461db1e130 abstract_tendermint_staking_adapter.wasm -3a9c4e430d06822ece0396e78514823b59a64ed51803f0f1e1595e4e85535fdd calendar_app.wasm -c9fd2d8c19c04b7b5ec74d0dba5199c2aa3d26a69aa41edce0b26b6c87832932 challenge_app.wasm -948403c947070ecc80fa75855ab2093802c7ea3a71c97b2e9c063a48aa517157 my_standalone.wasm -35d2c80e996b013f0735089a69cedd09a439246465e65f3b6d9bbab40bc57142 payment_app.wasm -caad911d1222ebc0248ecd8440222c8553eb997c5fbae10d1e2b58c2899df253 ping_pong.wasm +6b4fee90e5943de7bdf154c87ba717e3eb6435c16c4efb269aba1d7f09565a0f abstract_cw_staking-archway.wasm +7900526ec3051cb9d6b8213676b1bcef2f6ea7e0e1a1d5109483bf69959a95fc abstract_cw_staking-osmosis.wasm +abdee0bb743b91334b6bf15dc6c96dbadce14ff6e22ad7d5632c9d93c349933f abstract_cw_staking.wasm +c9341249956e575b730193dabb09281c15cc01083a7635c3d152494464309a57 abstract_dex_adapter-archway.wasm +6f0462071485786281a02d2065ddbe6c700e1eb05d6d87b0dad5ddc86ee9d093 abstract_dex_adapter-osmosis.wasm +f0a32d4b4b426b3081804d1236e03ec958e6f81a8f3990dd349f58145a0cfb47 abstract_dex_adapter.wasm +b9ff47b14815def977416224f3e51acfdaa9c2ecd2f36fc2ba2811d990310252 abstract_money_market_adapter.wasm +0b3e1efed127fc5d47a96ce8f9eeff33207be1c525127723642c816585ead3ce abstract_oracle_adapter-neutron.wasm +0b3e1efed127fc5d47a96ce8f9eeff33207be1c525127723642c816585ead3ce abstract_oracle_adapter-osmosis.wasm +0b3e1efed127fc5d47a96ce8f9eeff33207be1c525127723642c816585ead3ce abstract_oracle_adapter-xion.wasm +7d453a4a492931ec3f9f17499dfe703505c6b95ef94dbccdeb03401b70556f5e abstract_oracle_adapter.wasm +800ed379a8b1f69462a2efa0e342838bd038344646d7e930f74aac43665000bf abstract_subscription.wasm +9133af28c983213a560429af8948ab17e24d4c4ee9be22e0d455b68060a5932a abstract_tendermint_staking_adapter.wasm +91cedca4035718d69fbb14b19626938d1cb736a3dd673729b2b4ed75445d711f calendar_app.wasm +d776983b7bfd1231b1cda11c054294a34a0dd92ebc48bff76b1e88b27fb5ff5b challenge_app.wasm +8f8fbf5102749b180c9b61aa7f3c66a62282bc1948d5c160fc18f95e916739d4 my_standalone.wasm +72dc6d80bac13986afd4bd4fab46b11648306c8b7b065280c377f1eadc045f48 payment_app.wasm +049f861b1656d76a8f3557f062911cdf130ffeb950922ee80061404534992d4f ping_pong.wasm diff --git a/modules/artifacts/my_standalone.wasm b/modules/artifacts/my_standalone.wasm index c54d34cc9..39d5b5897 100644 Binary files a/modules/artifacts/my_standalone.wasm and b/modules/artifacts/my_standalone.wasm differ diff --git a/modules/artifacts/payment_app.wasm b/modules/artifacts/payment_app.wasm index 101d41120..f8501ff95 100644 Binary files a/modules/artifacts/payment_app.wasm and b/modules/artifacts/payment_app.wasm differ diff --git a/modules/artifacts/ping_pong.wasm b/modules/artifacts/ping_pong.wasm index 53e16204e..69efd118c 100644 Binary files a/modules/artifacts/ping_pong.wasm and b/modules/artifacts/ping_pong.wasm differ diff --git a/modules/contracts/adapters/oracle/Cargo.toml b/modules/contracts/adapters/oracle/Cargo.toml new file mode 100644 index 000000000..ba363a7bb --- /dev/null +++ b/modules/contracts/adapters/oracle/Cargo.toml @@ -0,0 +1,71 @@ +[package] +description = "The oracle adapter is an Abstract adapter for querying oracle prices. It provides a common interface for all oracles" +name = "abstract-oracle-adapter" + +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } + +exclude = ["contract.wasm", "hash.txt"] +resolver = "2" + + +[lib] +crate-type = ["cdylib", "rlib"] + +[[example]] +name = "deploy" + +[[example]] +name = "schema" +required-features = ["schema"] + +[features] +default = ["export"] +export = [] +schema = ["abstract-adapter/schema"] +testing = ["dep:abstract-client", "abstract-adapter/test-utils"] + +# Supported Oracles +pyth = ["abstract-pyth-adapter/full_integration"] + +# Builds +[package.metadata.optimizer] +builds = [ + { name = "xion", features = ["pyth"] }, + { name = "neutron", features = ["pyth"] }, + { name = "osmosis", features = ["pyth"] }, +] + +[dependencies] +abstract-adapter = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-orch = { workspace = true, features = ["daemon"] } + +# Local +abstract-oracle-standard = { workspace = true } + +# Pyth +abstract-pyth-adapter = { path = "../../../../integrations/oracles/pyth" } + +# Testing # +abstract-client = { workspace = true, optional = true } + +[dev-dependencies] +abstract-interface = { workspace = true, features = ["daemon"] } +anyhow = { workspace = true } +clap = { workspace = true } +dotenv = "0.15.0" +env_logger = "0.11.3" +semver = { workspace = true } +tokio = { workspace = true } + +bip32 = { version = "0.5.2" } +oracle = { path = ".", features = ["pyth", "testing"], package = "abstract-oracle-adapter" } + + +cw-utils = { workspace = true } +cw20 = { workspace = true } +cw20-base = { workspace = true } diff --git a/modules/contracts/adapters/oracle/README.md b/modules/contracts/adapters/oracle/README.md new file mode 100644 index 000000000..f89df66b5 --- /dev/null +++ b/modules/contracts/adapters/oracle/README.md @@ -0,0 +1,93 @@ +# Oracle Adapter Module + +The Oracle Adapter Module provides a unified interface to interact with various oracle providers across the Cosmos ecosystem. By abstracting the differences between various oracles, it allows developers to interact with any oracle source using a standard interface, streamlining the development process and ensuring compatibility across various oracle platforms. + +## Features + +The Oracle Adapter allows developers to query a price associated with a price source key. The module provides the following simple interface: + +```rust +pub enum OracleQueryMsg{ + Price{ + oracle: String, + price_source_key: String, + // Only successful if price is not too old + max_age: u64 + } +} +// And returns +pub struct PriceResponse{ + price: cosmwasm_std::Decimal +} + +``` + +## Supported Oracle Providers + +The following Oracles are currently supported: + +- Pyth (Xion, Neutron, Osmosis) + +If you would like to request support for an additional oracle, please create a GitHub issue or reach out to us on Discord. + +## Installation + +To use the Oracle Adapter Module in your Rust project, add the following dependency to your `Cargo.toml`: + +```toml +[dependencies] +abstract-oracle-adapter = { git = "https://github.com/AbstractSDK/abstract.git", tag="", default-features = false } +``` + +where at the time of writing, `latest-tag=v0.25.0` + +## Usage with the Abstract SDK + +To interact with an oracle, inside an Abstract module, you first need to retrieve the oracle using the Oracle Adapter. Here's a basic example in Rust: + +```rust +use abstract_oracle_adapter::api::OracleInterface; +... + +let oracle_name = "pyth".to_string(); +let price_source_key = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + +// Retrieve the oracle +let oracle = app.oracle(deps.as_ref(), oracle_name); +// My custom contract needs prices that are not older than 1 minute +let max_age = 60u64; + +let price = oracle.price(price_source_key, max_age); +``` + +## Why Use the Oracle Adapter? + +### Simplified Development + +By using the Oracle Adapter, developers can bypass the intricacies of each individual oracle. This means less time spent on understanding and integrating with each oracles's unique API, and more time focusing on building core functionalities. + +### Flexibility + +The Oracle Adapter ensures that your application remains flexible. If a new oracle emerges or if there are changes to an existing one, your application can easily adapt without undergoing major overhauls. + +### Use Cases + +- **Rapid Prototyping**: Quickly build and test applications on top of various oracles without the need for multiple integrations. +- **Safer Applications**: Build applications that leverage multiple oracles simultaneously, offering users more stability and safety for their asset value. +- **Future-Proofing**: Ensure your application remains compatible with future oracles that emerge in the Cosmos ecosystem. + +## Tests + +Tests for this implementation can be found in different locations: + +- Either inside this crate for tests that can be run inside Mock or OsmosisTestTube +- In the `/interchain/modules-clone-testing` crate for integrations that require CloneTesting (e.g. Pyth)? + +## Documentation + +- **Oracle Interface**: For a detailed look at the oracle interface, refer to the [Rust trait interface](https://github.com/AbstractSDK/abstract/blob/bcf26f2f446478fd2825de5b187321dc9a626341/modules/contracts/adapters/oracle/src/api.rs#L38). +- **Adapters Documentation**: Comprehensive information about adapters can be found in the [official documentation](https://docs.abstract.money/3_framework/6_module_types.html#adapters). + +## Contributing + +If you have suggestions, improvements, new oracles, or want to contribute to the project, we welcome your input on GitHub. diff --git a/modules/contracts/adapters/oracle/STATUS.md b/modules/contracts/adapters/oracle/STATUS.md new file mode 100644 index 000000000..1aab8c7a4 --- /dev/null +++ b/modules/contracts/adapters/oracle/STATUS.md @@ -0,0 +1,7 @@ +# Adapter Status + +This document describes the status of the oracle adapter's integrations with different external systems. + +| Protocol | Implementation | Execution Tests | Query Tests | Notes | +| --- | --- | --- | --- | --- | +| Pyth | ✅ | --- | ✅ | diff --git a/modules/contracts/adapters/oracle/examples/deploy.rs b/modules/contracts/adapters/oracle/examples/deploy.rs new file mode 100644 index 000000000..7732be8c2 --- /dev/null +++ b/modules/contracts/adapters/oracle/examples/deploy.rs @@ -0,0 +1,40 @@ +use abstract_interface::{AdapterDeployer, DeployStrategy}; +use abstract_oracle_adapter::{contract::CONTRACT_VERSION, interface::OracleAdapter}; +use cw_orch::daemon::networks::parse_network; +use cw_orch::prelude::*; +use semver::Version; + +fn deploy_oracle(networks: Vec) -> anyhow::Result<()> { + // run for each requested network + for network in networks { + let version: Version = CONTRACT_VERSION.parse().unwrap(); + let chain = DaemonBuilder::new(network).build()?; + let oracle = OracleAdapter::new(chain); + oracle.deploy(version, Empty {}, DeployStrategy::Try)?; + } + Ok(()) +} + +use clap::Parser; + +#[derive(Parser, Default, Debug)] +#[command(author, version, about, long_about = None)] +struct Arguments { + /// Network Id to deploy on + #[arg(short, long, value_delimiter = ' ', num_args = 1..)] + network_ids: Vec, +} + +fn main() -> anyhow::Result<()> { + dotenv::dotenv()?; + env_logger::init(); + + let args = Arguments::parse(); + let networks = args + .network_ids + .iter() + .map(|n| parse_network(n).unwrap()) + .collect(); + + deploy_oracle(networks) +} diff --git a/modules/contracts/adapters/oracle/examples/register_ans.rs b/modules/contracts/adapters/oracle/examples/register_ans.rs new file mode 100644 index 000000000..8f35898f5 --- /dev/null +++ b/modules/contracts/adapters/oracle/examples/register_ans.rs @@ -0,0 +1,51 @@ +use abstract_adapter::objects::UncheckedContractEntry; +use abstract_interface::{Abstract, ExecuteMsgFns}; +use abstract_oracle_adapter::interface::deployment::pyth_addresses; +use abstract_pyth_adapter::PYTH; +use cw_orch::daemon::networks::parse_network; +use cw_orch::prelude::*; + +fn register_ans(networks: Vec) -> anyhow::Result<()> { + // run for each requested network + for network in networks { + let chain = DaemonBuilder::new(network.clone()).build()?; + let abstr = Abstract::load_from(chain.clone())?; + + // This works only for PYTH, we have to find a better logic for other oracles and adapters + abstr.ans_host.update_contract_addresses( + vec![( + UncheckedContractEntry { + protocol: PYTH.to_string(), + contract: "oracle".to_string(), + }, + pyth_addresses().get(network.chain_id).unwrap().to_string(), + )], + vec![], + )?; + } + Ok(()) +} + +use clap::Parser; + +#[derive(Parser, Default, Debug)] +#[command(author, version, about, long_about = None)] +struct Arguments { + /// Network Id to deploy on + #[arg(short, long, value_delimiter = ' ', num_args = 1..)] + network_ids: Vec, +} + +fn main() -> anyhow::Result<()> { + dotenv::dotenv()?; + env_logger::init(); + + let args = Arguments::parse(); + let networks = args + .network_ids + .iter() + .map(|n| parse_network(n).unwrap()) + .collect(); + + register_ans(networks) +} diff --git a/modules/contracts/adapters/oracle/examples/schema.rs b/modules/contracts/adapters/oracle/examples/schema.rs new file mode 100644 index 000000000..21d48129a --- /dev/null +++ b/modules/contracts/adapters/oracle/examples/schema.rs @@ -0,0 +1,13 @@ +use std::{env::current_dir, fs::create_dir_all}; + +use abstract_oracle_adapter::contract::OracleAdapter; +use cosmwasm_schema::remove_schemas; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + OracleAdapter::export_schema(&out_dir); +} diff --git a/modules/contracts/adapters/oracle/src/api.rs b/modules/contracts/adapters/oracle/src/api.rs new file mode 100644 index 000000000..316371184 --- /dev/null +++ b/modules/contracts/adapters/oracle/src/api.rs @@ -0,0 +1,61 @@ +use crate::ORACLE_ADAPTER_ID; +use abstract_adapter::sdk::{ + features::{AccountIdentification, Dependencies, ModuleIdentification}, + AbstractSdkResult, AdapterInterface, +}; +use abstract_adapter::traits::AbstractNameService; +use abstract_oracle_standard::msg::{OracleName, Seconds}; +use abstract_oracle_standard::msg::{OracleQueryMsg, PriceResponse}; +use cosmwasm_std::Deps; + +// API for Abstract SDK users +/// Interact with the oracle adapter in your module. +pub trait OracleInterface: + AccountIdentification + Dependencies + ModuleIdentification + AbstractNameService +{ + /// Construct a new oracle interface. + fn oracle<'a>(&'a self, deps: Deps<'a>, name: OracleName) -> Oracle<'a, Self> { + Oracle { + base: self, + deps, + name, + } + } +} + +impl + OracleInterface for T +{ +} + +#[derive(Clone)] +pub struct Oracle<'a, T: OracleInterface> { + pub(crate) base: &'a T, + pub(crate) name: OracleName, + pub(crate) deps: Deps<'a>, +} + +impl Oracle<'_, T> { + /// returns Oracle name + pub fn oracle_name(&self) -> OracleName { + self.name.clone() + } + + /// Query a price from the oracle + pub fn price( + &self, + price_source_key: String, + max_age: Seconds, + ) -> AbstractSdkResult { + let adapters = self.base.adapters(self.deps); + + adapters.query( + ORACLE_ADAPTER_ID, + OracleQueryMsg::Price { + price_source_key, + oracle: self.oracle_name(), + max_age, + }, + ) + } +} diff --git a/modules/contracts/adapters/oracle/src/contract.rs b/modules/contracts/adapters/oracle/src/contract.rs new file mode 100644 index 000000000..12bce72f5 --- /dev/null +++ b/modules/contracts/adapters/oracle/src/contract.rs @@ -0,0 +1,20 @@ +use abstract_adapter::AdapterContract; +use abstract_oracle_standard::{msg::OracleQueryMsg, OracleError}; +use cosmwasm_std::{Empty, Response}; + +use crate::{handlers, ORACLE_ADAPTER_ID}; + +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub type OracleAdapter = AdapterContract; +pub type OracleResult = Result; + +pub const ORACLE_ADAPTER: OracleAdapter = + OracleAdapter::new(ORACLE_ADAPTER_ID, CONTRACT_VERSION, None) + .with_instantiate(handlers::instantiate_handler) + .with_query(handlers::query_handler); + +#[cfg(feature = "export")] +use abstract_adapter::export_endpoints; +#[cfg(feature = "export")] +export_endpoints!(ORACLE_ADAPTER, OracleAdapter); diff --git a/modules/contracts/adapters/oracle/src/handlers/instantiate.rs b/modules/contracts/adapters/oracle/src/handlers/instantiate.rs new file mode 100644 index 000000000..b0abd41c0 --- /dev/null +++ b/modules/contracts/adapters/oracle/src/handlers/instantiate.rs @@ -0,0 +1,13 @@ +use cosmwasm_std::{DepsMut, Empty, Env, MessageInfo, Response}; + +use crate::contract::{OracleAdapter, OracleResult}; + +pub fn instantiate_handler( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _module: OracleAdapter, + _msg: Empty, +) -> OracleResult { + Ok(Response::default()) +} diff --git a/modules/contracts/adapters/oracle/src/handlers/mod.rs b/modules/contracts/adapters/oracle/src/handlers/mod.rs new file mode 100644 index 000000000..35c6debc6 --- /dev/null +++ b/modules/contracts/adapters/oracle/src/handlers/mod.rs @@ -0,0 +1,5 @@ +mod instantiate; +mod query; + +pub use instantiate::instantiate_handler; +pub use query::query_handler; diff --git a/modules/contracts/adapters/oracle/src/handlers/query.rs b/modules/contracts/adapters/oracle/src/handlers/query.rs new file mode 100644 index 000000000..ee0e9613c --- /dev/null +++ b/modules/contracts/adapters/oracle/src/handlers/query.rs @@ -0,0 +1,28 @@ +use abstract_adapter::sdk::features::AbstractNameService; + +use abstract_oracle_standard::msg::OracleQueryMsg; +use cosmwasm_std::{to_json_binary, Binary, Deps, Env}; + +use crate::contract::{OracleAdapter, OracleResult}; +use crate::oracles; + +pub fn query_handler( + deps: Deps, + env: Env, + module: &OracleAdapter, + msg: OracleQueryMsg, +) -> OracleResult { + match msg { + OracleQueryMsg::Price { + price_source_key, + oracle, + max_age, + } => { + let oracle = oracles::resolve_oracle(&oracle)?; + let ans = module.name_service(deps); + let price_response = oracle.price(deps, &env, ans.host(), price_source_key, max_age)?; + + to_json_binary(&price_response).map_err(Into::into) + } + } +} diff --git a/modules/contracts/adapters/oracle/src/lib.rs b/modules/contracts/adapters/oracle/src/lib.rs new file mode 100644 index 000000000..79c841ec0 --- /dev/null +++ b/modules/contracts/adapters/oracle/src/lib.rs @@ -0,0 +1,120 @@ +pub mod api; +pub mod contract; +pub(crate) mod handlers; +pub mod oracles; +pub mod msg { + pub use abstract_oracle_standard::msg::*; +} +pub use abstract_oracle_standard::ORACLE_ADAPTER_ID; + +// Export interface for use in SDK modules +pub use crate::api::OracleInterface; + +#[cfg(feature = "testing")] +pub mod oracle_tester; + +#[cfg(not(target_arch = "wasm32"))] +pub mod interface { + use crate::{contract::ORACLE_ADAPTER, msg::*}; + use abstract_adapter::abstract_interface::{AdapterDeployer, RegisteredModule}; + use abstract_adapter::objects::dependency::StaticDependency; + use abstract_adapter::sdk::features::ModuleIdentification; + + use abstract_adapter::traits::Dependencies; + use abstract_oracle_standard::ORACLE_ADAPTER_ID; + use cw_orch::{build::BuildPostfix, interface}; + use cw_orch::{contract::Contract, prelude::*}; + + #[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty, id=ORACLE_ADAPTER_ID)] + pub struct OracleAdapter; + + // Implement deployer trait + impl AdapterDeployer for OracleAdapter {} + + impl Uploadable for OracleAdapter { + #[cfg(feature = "export")] + fn wrapper() -> ::ContractSource { + Box::new(ContractWrapper::new_with_empty( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + )) + } + fn wasm(chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path_with_build_postfix( + "abstract_oracle_adapter", + BuildPostfix::ChainName(chain), + ) + .unwrap() + } + } + + impl RegisteredModule for OracleAdapter { + type InitMsg = Empty; + + fn module_id<'a>() -> &'a str { + ORACLE_ADAPTER.module_id() + } + + fn module_version<'a>() -> &'a str { + ORACLE_ADAPTER.version() + } + + fn dependencies<'a>() -> &'a [StaticDependency] { + ORACLE_ADAPTER.dependencies() + } + } + + impl From> for OracleAdapter { + fn from(contract: Contract) -> Self { + Self(contract) + } + } + + impl + abstract_adapter::abstract_interface::DependencyCreation for OracleAdapter + { + type DependenciesConfig = cosmwasm_std::Empty; + + fn dependency_install_configs( + _configuration: Self::DependenciesConfig, + ) -> Result< + Vec, + abstract_adapter::abstract_interface::AbstractInterfaceError, + > { + Ok(vec![]) + } + } + + pub mod deployment { + use cosmwasm_std::Addr; + use cw_orch::daemon::networks::{NEUTRON_1, OSMOSIS_1, OSMO_5, PION_1, XION_TESTNET_1}; + use std::collections::HashMap; + + pub fn pyth_addresses() -> HashMap { + [ + (XION_TESTNET_1.chain_id, PYTH_XION_TEST_ADDRESS), + (PION_1.chain_id, PYTH_PION_ADDRESS), + (OSMO_5.chain_id, PYTH_OSMO_TEST_ADDRESS), + (NEUTRON_1.chain_id, PYTH_NEUTRON_ADDRESS), + (OSMOSIS_1.chain_id, PYTH_OSMOSIS_ADDRESS), + ] + .map(|(key, value)| (key.to_string(), Addr::unchecked(value))) + .into() + } + + // Source: https://docs.pyth.network/price-feeds/contract-addresses/cosmwasm + pub const PYTH_XION_TEST_ADDRESS: &str = + "xion1w39ctwxxhxxc2kxarycjxj9rndn65gf8daek7ggarwh3rq3zl0lqqllnmt"; + pub const PYTH_PION_ADDRESS: &str = + "neutron15ldst8t80982akgr8w8ekcytejzkmfpgdkeq4xgtge48qs7435jqp87u3t"; + pub const PYTH_OSMO_TEST_ADDRESS: &str = + "osmo1hpdzqku55lmfmptpyj6wdlugqs5etr6teqf7r4yqjjrxjznjhtuqqu5kdh"; + + pub const PYTH_NEUTRON_ADDRESS: &str = + "neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv"; + pub const PYTH_OSMOSIS_ADDRESS: &str = + "osmo13ge29x4e2s63a8ytz2px8gurtyznmue4a69n5275692v3qn3ks8q7cwck7"; + } +} diff --git a/modules/contracts/adapters/oracle/src/oracle_tester.rs b/modules/contracts/adapters/oracle/src/oracle_tester.rs new file mode 100644 index 000000000..b086e0f8b --- /dev/null +++ b/modules/contracts/adapters/oracle/src/oracle_tester.rs @@ -0,0 +1,84 @@ +use crate::{interface::OracleAdapter, ORACLE_ADAPTER_ID}; +use abstract_adapter::abstract_interface::{AdapterDeployer, DeployStrategy, RegistryExecFns}; +use abstract_adapter::std::objects::module::{ModuleInfo, ModuleVersion}; +use abstract_client::{AbstractClient, Environment}; + +use abstract_oracle_standard::msg::{OracleQueryMsgFns, PriceResponse}; +use cw_orch::{environment::MutCwEnv, prelude::*}; + +use cw_orch::anyhow; + +pub trait MockOracle { + const MAX_AGE: u64; + + /// Name of the oracle + fn name(&self) -> String; + + /// First asset + fn price_source_key(&self) -> String; + + /// Ans setup for this oracle + /// For instance, for Pyth, we just register pyth Contract Entry inside ans + fn ans_setup(&self, abstr_deployment: &AbstractClient) -> anyhow::Result<()>; +} + +pub struct OracleTester> { + pub abstr_deployment: AbstractClient, + pub oracle_adapter: OracleAdapter, + pub oracle: Oracle, +} + +impl> OracleTester { + /// Used to test new code + pub fn new(abstr_deployment: AbstractClient, oracle: Oracle) -> anyhow::Result { + // Re-register oracle adapter, to make sure it's latest + let _ = abstr_deployment + .registry() + .remove_module(ModuleInfo::from_id( + ORACLE_ADAPTER_ID, + ModuleVersion::Version(crate::contract::CONTRACT_VERSION.to_owned()), + )?); + + let oracle_adapter = OracleAdapter::new(abstr_deployment.environment()); + oracle_adapter.deploy( + crate::contract::CONTRACT_VERSION.parse()?, + Empty {}, + DeployStrategy::Force, + )?; + + oracle.ans_setup(&abstr_deployment)?; + + Ok(Self { + abstr_deployment, + oracle_adapter, + oracle, + }) + } + + /// Used to test on-chain code + pub fn new_live( + abstr_deployment: AbstractClient, + oracle: Oracle, + ) -> anyhow::Result { + let account = abstr_deployment.account_builder().build()?; + let oracle_adapter = + account.install_adapter::>(&[])?; + + Ok(Self { + abstr_deployment, + oracle_adapter: oracle_adapter.module()?, + oracle, + }) + } + + pub fn test_price(&self) -> anyhow::Result { + // Get the price associated with the ID + self.oracle_adapter + .price( + Oracle::MAX_AGE, + self.oracle.name(), + self.oracle.price_source_key(), + ) + .map_err(Into::into) + } +} diff --git a/modules/contracts/adapters/oracle/src/oracles/mod.rs b/modules/contracts/adapters/oracle/src/oracles/mod.rs new file mode 100644 index 000000000..d945c9078 --- /dev/null +++ b/modules/contracts/adapters/oracle/src/oracles/mod.rs @@ -0,0 +1,11 @@ +use abstract_oracle_standard::{OracleCommand, OracleError}; + +pub use abstract_pyth_adapter::PYTH; + +pub(crate) fn resolve_oracle(value: &str) -> Result, OracleError> { + match value { + #[cfg(feature = "pyth")] + abstract_pyth_adapter::PYTH => Ok(Box::::default()), + _ => Err(OracleError::ForeignOracle(value.to_owned())), + } +}