diff --git a/examples/configs/logs.json b/examples/configs/logs.json index fbbe5fe..c089c9e 100644 --- a/examples/configs/logs.json +++ b/examples/configs/logs.json @@ -1,5 +1,5 @@ { - "start_time": 1719561389, + "start_time": 1719573012, "chains": [ { "chain_id": "localcosmos-1", @@ -7,7 +7,7 @@ "rpc_address": "http://0.0.0.0:26658", "rest_address": "http://0.0.0.0:1318", "grpc_address": "0.0.0.0:9091", - "p2p_address": "0.0.0.0:35399", + "p2p_address": "0.0.0.0:45505", "ibc_paths": [] }, { @@ -16,7 +16,7 @@ "rpc_address": "http://0.0.0.0:26657", "rest_address": "http://0.0.0.0:1317", "grpc_address": "0.0.0.0:9090", - "p2p_address": "0.0.0.0:37205", + "p2p_address": "0.0.0.0:45843", "ibc_paths": [] } ], diff --git a/examples/contracts/price_oracle.wasm b/examples/contracts/price_oracle.wasm new file mode 100644 index 0000000..fe14ead Binary files /dev/null and b/examples/contracts/price_oracle.wasm differ diff --git a/examples/neutron.rs b/examples/neutron.rs index bbbf2ff..41913e1 100644 --- a/examples/neutron.rs +++ b/examples/neutron.rs @@ -1,3 +1,4 @@ +use cosmwasm_std::Decimal; use localic_utils::{types::contract::MinAmount, ConfigChainBuilder, TestContextBuilder}; use std::error::Error; @@ -23,6 +24,15 @@ fn main() -> Result<(), Box> { .with_subdenom("amoguscoin") .send()?; + let bruhtoken = ctx.get_tokenfactory_denom( + "neutron1kuf2kxwuv2p8k3gnpja7mzf05zvep0cyuy7mxg", + "bruhtoken", + ); + let amoguscoin = ctx.get_tokenfactory_denom( + "neutron1kuf2kxwuv2p8k3gnpja7mzf05zvep0cyuy7mxg", + "amoguscoin", + ); + // Deploy valence auctions ctx.build_tx_create_auctions_manager() .with_min_auction_amount(&[( @@ -32,16 +42,15 @@ fn main() -> Result<(), Box> { start_auction: "0".into(), }, )]) + .with_server_addr("neutron1kuf2kxwuv2p8k3gnpja7mzf05zvep0cyuy7mxg") .send()?; - - let bruhtoken = ctx.get_tokenfactory_denom( - "neutron1kuf2kxwuv2p8k3gnpja7mzf05zvep0cyuy7mxg", - "bruhtoken", - ); - let amoguscoin = ctx.get_tokenfactory_denom( - "neutron1kuf2kxwuv2p8k3gnpja7mzf05zvep0cyuy7mxg", - "amoguscoin", - ); + ctx.build_tx_create_price_oracle().send()?; + ctx.build_tx_manual_oracle_price_update() + .with_offer_asset("untrn") + .with_ask_asset(amoguscoin.as_str()) + .with_price(Decimal::percent(10)) + .send()?; + ctx.build_tx_update_auction_oracle().send()?; ctx.build_tx_mint_tokenfactory_token() .with_denom(bruhtoken.as_str()) @@ -124,8 +133,8 @@ fn main() -> Result<(), Box> { ctx.build_tx_fund_pool() .with_denom_a("untrn") .with_denom_b(amoguscoin) - .with_amount_denom_a(1000) - .with_amount_denom_b(1000) + .with_amount_denom_a(10000) + .with_amount_denom_b(10000) .with_liq_token_receiver("neutron1kuf2kxwuv2p8k3gnpja7mzf05zvep0cyuy7mxg") .send()?; diff --git a/src/error.rs b/src/error.rs index 41ce086..f3aefbf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,8 +20,8 @@ pub enum Error { MissingContextVariable(String), #[error("the builder is missing a parameter `{0}`")] MissingBuilderParam(String), - #[error("the transaction failed: `{0}`")] - TxFailed(String), + #[error("the transaction {hash:?} failed: {error:?}")] + TxFailed { hash: String, error: String }, #[error("the transaction has no logs")] TxMissingLogs, } diff --git a/src/lib.rs b/src/lib.rs index d39c8b9..45e6abd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub const PAIR_NAME: &str = "astroport_pair"; pub const STABLE_PAIR_NAME: &str = "astroport_pair_stable"; pub const TOKEN_NAME: &str = "cw20_base"; pub const WHITELIST_NAME: &str = "astroport_whitelist"; +pub const PRICE_ORACLE_NAME: &str = "price_oracle"; /// Local ic info pub const LOCAL_IC_API_URL: &str = "http://localhost:42069/"; diff --git a/src/utils/fixtures.rs b/src/utils/fixtures.rs index 14e0cd8..9e27976 100644 --- a/src/utils/fixtures.rs +++ b/src/utils/fixtures.rs @@ -1,7 +1,7 @@ use super::{ super::{ error::Error, AUCTION_CONTRACT_NAME, FACTORY_NAME, NEUTRON_CHAIN_ID, PAIR_NAME, - STABLE_PAIR_NAME, + PRICE_ORACLE_NAME, STABLE_PAIR_NAME, }, test_context::TestContext, }; @@ -16,10 +16,25 @@ impl TestContext { let chain = self.get_chain(chain_id); let logs = chain.rb.query_tx_hash(hash); + let raw_log = logs + .get("raw_log") + .and_then(|raw_log| raw_log.as_str()) + .ok_or(Error::TxMissingLogs)?; + + if serde_json::from_str::(raw_log).is_err() { + return Err(Error::TxFailed { + hash: hash.to_owned(), + error: raw_log.to_owned(), + }); + } + let logs = logs.get("events").ok_or(Error::TxMissingLogs)?; if let Some(err) = logs.as_str() { - return Err(Error::TxFailed(err.to_owned())); + return Err(Error::TxFailed { + hash: hash.to_owned(), + error: err.to_owned(), + }); } logs.as_array().cloned().ok_or(Error::TxMissingLogs) @@ -63,6 +78,24 @@ impl TestContext { )) } + /// Get a new CosmWasm instance for the existing deployed auctions manager. + pub fn get_price_oracle(&self) -> Result { + let neutron = self.get_chain(NEUTRON_CHAIN_ID); + + let mut contract = self.get_contract(PRICE_ORACLE_NAME)?; + let contract_addr = neutron + .contract_addrs + .get(PRICE_ORACLE_NAME) + .and_then(|addrs| addrs.get(0)) + .cloned() + .ok_or(Error::MissingContextVariable(String::from( + "contract_addrs::price_oracle", + )))?; + contract.contract_addr = Some(contract_addr); + + Ok(contract) + } + /// Gets a CosmWasm instance for an auction with a given pair. pub fn get_auction, TDenomB: AsRef>( &self, diff --git a/src/utils/setup/valence.rs b/src/utils/setup/valence.rs index ef77739..d184edc 100644 --- a/src/utils/setup/valence.rs +++ b/src/utils/setup/valence.rs @@ -6,10 +6,11 @@ use super::super::{ PriceFreshnessStrategy, }, AUCTIONS_MANAGER_CONTRACT_NAME, AUCTION_CONTRACT_NAME, DEFAULT_AUCTION_LABEL, DEFAULT_KEY, - NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_ID, + NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_ID, PRICE_ORACLE_NAME, }, test_context::TestContext, }; +use cosmwasm_std::Decimal; use localic_std::modules::cosmwasm::CosmWasm; use serde_json::Value; @@ -238,6 +239,147 @@ impl<'a> StartAuctionTxBuilder<'a> { } } +pub struct MigrateAuctionTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + test_ctx: &'a mut TestContext, +} + +impl<'a> MigrateAuctionTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_migrate_auction( + self.key, + ( + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + ), + ) + } +} + +pub struct CreatePriceOracleTxBuilder<'a> { + key: &'a str, + seconds_allow_manual_change: u64, + seconds_auction_prices_fresh: u64, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreatePriceOracleTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_seconds_allow_manual_change(&mut self, sec: u64) -> &mut Self { + self.seconds_allow_manual_change = sec; + + self + } + + pub fn with_seconds_auction_prices_fresh(&mut self, sec: u64) -> &mut Self { + self.seconds_auction_prices_fresh = sec; + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_price_oracle( + self.key, + self.seconds_allow_manual_change, + self.seconds_auction_prices_fresh, + ) + } +} + +pub struct UpdateAuctionOracleTxBuilder<'a> { + key: &'a str, + test_ctx: &'a mut TestContext, +} + +impl<'a> UpdateAuctionOracleTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_update_auction_oracle(self.key) + } +} + +pub struct ManualOraclePriceUpdateTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + price: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> ManualOraclePriceUpdateTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + pub fn with_price(&mut self, price: Decimal) -> &mut Self { + self.price = Some(price); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_manual_oracle_price_update( + self.key, + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("offer_asset")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("ask_asset")))?, + self.price + .ok_or(Error::MissingBuilderParam(String::from("price")))?, + ) + } +} + impl TestContext { pub fn build_tx_create_auctions_manager(&mut self) -> CreateAuctionsManagerTxBuilder { CreateAuctionsManagerTxBuilder { @@ -302,6 +444,57 @@ impl TestContext { Ok(()) } + pub fn build_tx_create_price_oracle(&mut self) -> CreatePriceOracleTxBuilder { + CreatePriceOracleTxBuilder { + key: DEFAULT_KEY, + seconds_allow_manual_change: 0, + seconds_auction_prices_fresh: 100000000000, + test_ctx: self, + } + } + + /// Creates an auction manager on Neutron, updating the autions manager + /// code id and address in the TestContext. + fn tx_create_price_oracle<'a>( + &mut self, + sender_key: &str, + seconds_allow_manual_change: u64, + seconds_auction_prices_fresh: u64, + ) -> Result<(), Error> { + let auctions_manager: CosmWasm = self.get_auctions_manager()?; + let auctions_manager_addr = + auctions_manager + .contract_addr + .ok_or(Error::MissingContextVariable(String::from( + "contract_addresses::auctions_manager", + )))?; + + let mut contract_a = self.get_contract(PRICE_ORACLE_NAME)?; + let contract = contract_a.instantiate( + sender_key, + serde_json::json!({ + "auctions_manager_addr": auctions_manager_addr, + "seconds_allow_manual_change": seconds_allow_manual_change, + "seconds_auction_prices_fresh": seconds_auction_prices_fresh, + }) + .to_string() + .as_str(), + PRICE_ORACLE_NAME, + None, + "", + )?; + + let chain = self.get_mut_chain(NEUTRON_CHAIN_ID); + + chain + .contract_addrs + .entry(PRICE_ORACLE_NAME.to_owned()) + .or_default() + .push(contract.address); + + Ok(()) + } + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. pub fn build_tx_create_auction<'a>(&mut self) -> CreateAuctionTxBuilder { CreateAuctionTxBuilder { @@ -376,6 +569,146 @@ impl TestContext { Ok(()) } + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. + pub fn build_tx_migrate_auction<'a>(&mut self) -> MigrateAuctionTxBuilder { + MigrateAuctionTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + test_ctx: self, + } + } + + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. + fn tx_migrate_auction<'a, TDenomA: AsRef, TDenomB: AsRef>( + &mut self, + sender_key: &str, + pair: (TDenomA, TDenomB), + ) -> Result<(), Error> { + // The auctions manager for this deployment + let contract_a = self.get_auctions_manager()?; + let code_id = self.get_contract(AUCTION_CONTRACT_NAME)?.code_id.ok_or( + Error::MissingContextVariable(String::from("code_ids::auction")), + )?; + + let receipt = contract_a.execute( + sender_key, + serde_json::json!( + { + "admin": { + "migrate_auction": { + "pair": (pair.0.as_ref(), pair.1.as_ref()), + "code_id": code_id, + "msg": { + "no_state_change": {} + }, + }, + }}) + .to_string() + .as_str(), + format!("--gas 2000000").as_str(), + )?; + + log::debug!( + "submitted tx migrating auction ({}, {}) {:?}", + pair.0.as_ref(), + pair.1.as_ref(), + receipt + ); + + let _ = self.get_tx_events( + NEUTRON_CHAIN_ID, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Creates a builder setting the oracle address on the auctions manager on neutron. + pub fn build_tx_update_auction_oracle(&mut self) -> UpdateAuctionOracleTxBuilder { + UpdateAuctionOracleTxBuilder { + key: DEFAULT_KEY, + test_ctx: self, + } + } + + fn tx_update_auction_oracle(&mut self, sender_key: &str) -> Result<(), Error> { + // The auctions manager for this deployment + let contract_a = self.get_auctions_manager()?; + let neutron = self.get_chain(NEUTRON_CHAIN_ID); + let oracle = neutron + .contract_addrs + .get(PRICE_ORACLE_NAME) + .and_then(|addrs| addrs.get(0)) + .ok_or(Error::MissingContextVariable(String::from( + "contract_addrs::price_oracle", + )))?; + + let receipt = contract_a.execute( + sender_key, + serde_json::json!( + { + "admin": { + "update_oracle": { + "oracle_addr": oracle, + }, + }}) + .to_string() + .as_str(), + format!("--gas 2000000").as_str(), + )?; + + let _ = self.get_tx_events( + NEUTRON_CHAIN_ID, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Creates a builder setting the oracle address on the auctions manager on neutron. + pub fn build_tx_manual_oracle_price_update(&mut self) -> ManualOraclePriceUpdateTxBuilder { + ManualOraclePriceUpdateTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + price: Default::default(), + test_ctx: self, + } + } + + fn tx_manual_oracle_price_update( + &mut self, + sender_key: &str, + offer_asset: &str, + ask_asset: &str, + price: Decimal, + ) -> Result<(), Error> { + // The auctions manager for this deployment + let oracle = self.get_price_oracle()?; + + let receipt = oracle.execute( + sender_key, + serde_json::json!( + { + "manual_price_update": { + "pair": (offer_asset, ask_asset), + "price": price, + } + }) + .to_string() + .as_str(), + format!("--gas 2000000").as_str(), + )?; + + let _ = self.get_tx_events( + NEUTRON_CHAIN_ID, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + /// Sends the specified amount of funds to an auction. pub fn build_tx_fund_auction(&mut self) -> FundAuctionTxBuilder { FundAuctionTxBuilder {