use std::convert::{TryFrom, TryInto}; #[cfg(feature = "ensemble-staking")] use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crate::cosmwasm_std::{Response, Attribute, Event}; use super::{ EnsembleResult, EnsembleError, response::{ InstantiateResponse, ExecuteResponse, BankResponse, ReplyResponse } }; #[cfg(feature = "ensemble-staking")] use super::response::{StakingResponse, StakingOp, DistributionResponse, DistributionOp}; const CONTRACT_ATTR: &str = "contract_address"; pub struct ProcessedEvents(Vec); impl ProcessedEvents { #[inline] pub fn empty() -> Self { Self(vec![]) } pub fn extend>( &mut self, resp: T ) -> EnsembleResult<()> { let events = resp.try_into()?; self.0.extend(events.0); Ok(()) } #[inline] pub fn take(self) -> Vec { self.0 } } impl TryFrom<&InstantiateResponse> for ProcessedEvents { type Error = EnsembleError; fn try_from(resp: &InstantiateResponse) -> Result { validate_response(&resp.response)?; let address = resp.instance.address.as_str(); let event = Event::new("instantiate") .add_attribute(CONTRACT_ATTR, address) .add_attribute("code_id", resp.code_id.to_string()); Ok(process_wasm_response( &resp.response, address.into(), event )) } } impl TryFrom<&ExecuteResponse> for ProcessedEvents { type Error = EnsembleError; fn try_from(resp: &ExecuteResponse) -> Result { validate_response(&resp.response)?; let address = resp.address.as_str(); let event = Event::new("execute") .add_attribute(CONTRACT_ATTR, address); Ok(process_wasm_response( &resp.response, address.into(), event )) } } impl TryFrom<&ReplyResponse> for ProcessedEvents { type Error = EnsembleError; fn try_from(resp: &ReplyResponse) -> Result { validate_response(&resp.response)?; let address = resp.address.as_str(); let event = Event::new("reply") .add_attribute(CONTRACT_ATTR, address); Ok(process_wasm_response( &resp.response, address.into(), event )) } } impl From<&BankResponse> for ProcessedEvents { fn from(resp: &BankResponse) -> Self { let coins: String = resp.coins.iter() .map(|x| format!("{}{}", x.amount, x.denom)) .collect::>() .join(","); Self(vec![ Event::new("coin_spent") .add_attribute("amount", coins.clone()) .add_attribute("spender", &resp.sender), Event::new("coin_received") .add_attribute("amount", coins.clone()) .add_attribute("receiver", &resp.receiver), Event::new("transfer") .add_attribute("amount", coins) .add_attribute("recipient", &resp.receiver) .add_attribute("sender", &resp.sender) ]) } } #[cfg(feature = "ensemble-staking")] impl From<&StakingResponse> for ProcessedEvents { fn from(resp: &StakingResponse) -> Self { let amount_value = format!("{}{}", resp.amount.amount, resp.amount.denom); let event = match &resp.kind { StakingOp::Delegate { validator } => Event::new("delegate") .add_attribute("validator", validator) .add_attribute("amount", amount_value) .add_attribute("new_shares", resp.amount.amount.to_string()), StakingOp::Undelegate { validator } => { let date = OffsetDateTime::now_utc().format(&Rfc3339).unwrap(); Event::new("unbond") .add_attribute("validator", validator) .add_attribute("amount", amount_value) .add_attribute("completion_time", date) } StakingOp::Redelegate { src_validator, dst_validator } => Event::new("redelegate") .add_attribute("source_validator", src_validator) .add_attribute("destination_validator", dst_validator) .add_attribute("amount", amount_value) }; Self(vec![event]) } } #[cfg(feature = "ensemble-staking")] impl From<&DistributionResponse> for ProcessedEvents { fn from(resp: &DistributionResponse) -> Self { let event = match &resp.kind { DistributionOp::WithdrawDelegatorReward { reward, validator } => Event::new("withdraw_delegator_reward") .add_attribute("validator", validator) .add_attribute("sender", &resp.sender) .add_attribute( "amount", format!("{}{}", reward.amount, reward.denom), ), _ => todo!() }; Self(vec![event]) } } fn process_wasm_response( response: &Response, address: String, event: Event ) -> ProcessedEvents { let attributes = if response.attributes.is_empty() { 0 } else { 1 }; let mut events = Vec::with_capacity(response.events.len() + attributes + 1); events.push(event); // Response::add_atrribute/s creates a new event in the array where // the type is "wasm" and inserts them under it. And attribute with key // "contract_address" is also inserted. if !response.attributes.is_empty() { // Attributes are inserted in LIFO order i.e in reverse. events.push(Event::new("wasm") .add_attributes(response.attributes.clone().into_iter().rev()) .add_attribute(CONTRACT_ATTR, &address) ); } // Response::add_event/s creates a new event in the array where // the type is prefixed with "wasm-". And attribute with key // "contract_address" is also inserted. let wasm_events = response.events.clone().into_iter().map(|mut x| { x.ty = format!("wasm-{}", x.ty); x.attributes.insert( 0, Attribute { key: CONTRACT_ATTR.into(), value: address.clone(), encrypted: true } ); x }); events.extend(wasm_events); ProcessedEvents(events) } // Taken from https://github.com/CosmWasm/cw-multi-test/blob/03026ccd626f57869c57c9192a03da6625e4791d/src/wasm.rs#L231-L268 fn validate_response(response: &Response) -> EnsembleResult<()> { validate_attributes(&response.attributes)?; for event in &response.events { validate_attributes(&event.attributes)?; let ty = event.ty.trim(); if ty.len() < 2 { return Err(EnsembleError::AttributeValidation( format!("Attribute type cannot be less than 2 characters: {}", ty) )); } } Ok(()) } fn validate_attributes(attributes: &[Attribute]) -> EnsembleResult<()> { for attr in attributes { let key = attr.key.trim(); let val = attr.value.trim(); if key.is_empty() { return Err(EnsembleError::AttributeValidation( format!("Attribute key for value {} cannot be empty", val) )); } if val.is_empty() { return Err(EnsembleError::AttributeValidation( format!("Attribute value with key {} cannot be empty", key) )); } if key.starts_with('_') { return Err(EnsembleError::AttributeValidation( format!("Attribute key {} cannot start with \"_\"", key) )); } } Ok(()) }