diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ea19e07..7c17ae91f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Expiry Enum [(#419)](https://github.com/andromedaprotocol/andromeda-core/pull/419) - Added Conditional Splitter [(#441)](https://github.com/andromedaprotocol/andromeda-core/pull/441) - Validator Staking: Added the option to set an amount while unstaking [(#458)](https://github.com/andromedaprotocol/andromeda-core/pull/458) +- Added Time Gate ADO [(#529)](https://github.com/andromedaprotocol/andromeda-core/pull/529) - Set Amount Splitter [(#507)](https://github.com/andromedaprotocol/andromeda-core/pull/507) - Added String Storage ADO [(#512)](https://github.com/andromedaprotocol/andromeda-core/pull/512) - Boolean Storage ADO [(#513)](https://github.com/andromedaprotocol/andromeda-core/pull/513) diff --git a/Cargo.lock b/Cargo.lock index 77f1fb88c..eef6dc885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,6 +573,21 @@ dependencies = [ "serde", ] +[[package]] +name = "andromeda-time-gate" +version = "2.0.0" +dependencies = [ + "andromeda-app", + "andromeda-modules", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", +] + [[package]] name = "andromeda-timelock" version = "2.0.0" diff --git a/contracts/modules/andromeda-time-gate/.cargo/config b/contracts/modules/andromeda-time-gate/.cargo/config new file mode 100644 index 000000000..87d563a7e --- /dev/null +++ b/contracts/modules/andromeda-time-gate/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +schema = "run --example schema" diff --git a/contracts/modules/andromeda-time-gate/Cargo.toml b/contracts/modules/andromeda-time-gate/Cargo.toml new file mode 100644 index 000000000..6ce2c1884 --- /dev/null +++ b/contracts/modules/andromeda-time-gate/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "andromeda-time-gate" +version = "2.0.0" +edition = "2021" +rust-version = "1.75.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } + +andromeda-std = { workspace = true, features = [] } +andromeda-modules = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } + +[dev-dependencies] +andromeda-app = { workspace = true } diff --git a/contracts/modules/andromeda-time-gate/examples/schema.rs b/contracts/modules/andromeda-time-gate/examples/schema.rs new file mode 100644 index 000000000..029c5eece --- /dev/null +++ b/contracts/modules/andromeda-time-gate/examples/schema.rs @@ -0,0 +1,11 @@ +use andromeda_modules::time_gate::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/modules/andromeda-time-gate/src/contract.rs b/contracts/modules/andromeda-time-gate/src/contract.rs new file mode 100644 index 000000000..99275a02c --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/contract.rs @@ -0,0 +1,265 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, ensure, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, Storage, +}; +use cw_utils::Expiration; + +use crate::state::{CYCLE_START_TIME, GATE_ADDRESSES, TIME_INTERVAL}; +use andromeda_modules::time_gate::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + amp::AndrAddr, + common::{ + actions::call_action, + context::ExecuteContext, + encode_binary, + expiration::{get_and_validate_start_time, Expiry}, + Milliseconds, + }, + error::ContractError, +}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-time-gate"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const DEFAULT_TIME_INTERVAL: u64 = 3600; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let resp = ADOContract::default().instantiate( + deps.storage, + env.clone(), + deps.api, + &deps.querier, + info, + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address, + owner: msg.owner, + }, + )?; + + let cycle_start_time_millisecons = match msg.cycle_start_time.clone() { + None => Milliseconds::from_nanos(env.block.time.nanos()), + Some(start_time) => start_time.get_time(&env.block), + }; + + let (cycle_start_time, _) = get_and_validate_start_time(&env, msg.cycle_start_time)?; + + let time_interval_seconds = msg.time_interval.unwrap_or(DEFAULT_TIME_INTERVAL); + + GATE_ADDRESSES.save(deps.storage, &msg.gate_addresses)?; + CYCLE_START_TIME.save( + deps.storage, + &(cycle_start_time, cycle_start_time_millisecons), + )?; + TIME_INTERVAL.save(deps.storage, &time_interval_seconds)?; + + Ok(resp) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let ctx = ExecuteContext::new(deps, info, env); + match msg { + ExecuteMsg::AMPReceive(pkt) => { + ADOContract::default().execute_amp_receive(ctx, pkt, handle_execute) + } + _ => handle_execute(ctx, msg), + } +} + +fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action = msg.as_ref().to_string(); + + let action_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + let res = match msg { + ExecuteMsg::UpdateCycleStartTime { cycle_start_time } => { + execute_update_cycle_start_time(ctx, cycle_start_time, action) + } + ExecuteMsg::UpdateGateAddresses { new_gate_addresses } => { + execute_update_gate_addresses(ctx, new_gate_addresses, action) + } + ExecuteMsg::UpdateTimeInterval { time_interval } => { + execute_update_time_interval(ctx, time_interval, action) + } + _ => ADOContract::default().execute(ctx, msg), + }?; + + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +fn execute_update_cycle_start_time( + ctx: ExecuteContext, + cycle_start_time: Option, + action: String, +) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + ensure!( + ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + let (new_cycle_start_time, _) = + get_and_validate_start_time(&ctx.env, cycle_start_time.clone())?; + let new_cycle_start_time_millisecons = match cycle_start_time.clone() { + None => Milliseconds::from_nanos(ctx.env.block.time.nanos()), + Some(start_time) => start_time.get_time(&ctx.env.block), + }; + + let (old_cycle_start_time, _) = CYCLE_START_TIME.load(deps.storage)?; + + ensure!( + old_cycle_start_time != new_cycle_start_time, + ContractError::InvalidParameter { + error: Some("Same as an existed cycle start time".to_string()) + } + ); + + CYCLE_START_TIME.save( + deps.storage, + &(new_cycle_start_time, new_cycle_start_time_millisecons), + )?; + + Ok(Response::new().add_attributes(vec![attr("action", action), attr("sender", info.sender)])) +} + +fn execute_update_gate_addresses( + ctx: ExecuteContext, + new_gate_addresses: Vec, + action: String, +) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + ensure!( + ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + let old_gate_addresses = GATE_ADDRESSES.load(deps.storage)?; + + ensure!( + old_gate_addresses != new_gate_addresses, + ContractError::InvalidParameter { + error: Some("Same as existed gate addresses".to_string()) + } + ); + + GATE_ADDRESSES.save(deps.storage, &new_gate_addresses)?; + + Ok(Response::new().add_attributes(vec![attr("action", action), attr("sender", info.sender)])) +} + +fn execute_update_time_interval( + ctx: ExecuteContext, + time_interval: u64, + action: String, +) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + ensure!( + ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + let old_time_interval = TIME_INTERVAL.load(deps.storage)?; + + ensure!( + old_time_interval != time_interval, + ContractError::InvalidParameter { + error: Some("Same as an existed time interval".to_string()) + } + ); + + TIME_INTERVAL.save(deps.storage, &time_interval)?; + + Ok(Response::new().add_attributes(vec![attr("action", action), attr("sender", info.sender)])) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetGateAddresses {} => encode_binary(&get_gate_addresses(deps.storage)?), + QueryMsg::GetCycleStartTime {} => encode_binary(&get_cycle_start_time(deps.storage)?), + QueryMsg::GetCurrentAdoPath {} => encode_binary(&get_current_ado_path(deps, env)?), + QueryMsg::GetTimeInterval {} => encode_binary(&get_time_interval(deps.storage)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +pub fn get_gate_addresses(storage: &dyn Storage) -> Result, ContractError> { + let gate_addresses = GATE_ADDRESSES.load(storage)?; + Ok(gate_addresses) +} + +pub fn get_cycle_start_time( + storage: &dyn Storage, +) -> Result<(Expiration, Milliseconds), ContractError> { + let (cycle_start_time, cycle_start_time_milliseconds) = CYCLE_START_TIME.load(storage)?; + Ok((cycle_start_time, cycle_start_time_milliseconds)) +} + +pub fn get_time_interval(storage: &dyn Storage) -> Result { + let time_interval = TIME_INTERVAL.load(storage)?.to_string(); + Ok(time_interval) +} + +pub fn get_current_ado_path(deps: Deps, env: Env) -> Result { + let storage = deps.storage; + let (cycle_start_time, cycle_start_time_milliseconds) = CYCLE_START_TIME.load(storage)?; + let gate_addresses = GATE_ADDRESSES.load(storage)?; + let time_interval = TIME_INTERVAL.load(storage)?; + + ensure!( + cycle_start_time.is_expired(&env.block), + ContractError::CustomError { + msg: "Cycle is not started yet".to_string() + } + ); + + let current_time_nanos = env.block.time.nanos(); + let cycle_start_nanos = cycle_start_time_milliseconds.nanos(); + + let time_interval_nanos = time_interval.checked_mul(1_000_000_000).unwrap(); + let gate_length = gate_addresses.len() as u64; + let time_delta = current_time_nanos.checked_sub(cycle_start_nanos).unwrap(); + let index = time_delta + .checked_div(time_interval_nanos) + .unwrap() + .checked_rem(gate_length) + .unwrap() as usize; + let current_ado_path = &gate_addresses[index]; + let result = current_ado_path.get_raw_address(&deps)?; + + Ok(result) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} diff --git a/contracts/modules/andromeda-time-gate/src/lib.rs b/contracts/modules/andromeda-time-gate/src/lib.rs new file mode 100644 index 000000000..bcbc58bb5 --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod state; +#[cfg(test)] +pub mod testing; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; diff --git a/contracts/modules/andromeda-time-gate/src/mock.rs b/contracts/modules/andromeda-time-gate/src/mock.rs new file mode 100644 index 000000000..090de9078 --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/mock.rs @@ -0,0 +1,139 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +use crate::contract::{execute, instantiate, query}; +use andromeda_modules::time_gate::CycleStartTime; +use andromeda_modules::time_gate::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::amp::AndrAddr; +use andromeda_testing::mock::MockApp; +use andromeda_testing::{ + mock_ado, + mock_contract::{ExecuteResult, MockADO, MockContract}, +}; +use cosmwasm_std::{Addr, Coin, Empty}; +use cw_multi_test::{Contract, ContractWrapper, Executor}; + +pub struct MockTimeGate(Addr); +mock_ado!(MockTimeGate, ExecuteMsg, QueryMsg); + +impl MockTimeGate { + pub fn mock_instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + kernel_address: String, + owner: Option, + cycle_start_time: CycleStartTime, + time_interval: Option, + gate_addresses: Vec, + ) -> MockTimeGate { + let msg = mock_time_gate_instantiate_msg( + kernel_address, + owner, + gate_addresses, + cycle_start_time, + time_interval, + ); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Time Gate Contract", + Some(sender.to_string()), + ) + .unwrap(); + MockTimeGate(Addr::unchecked(addr)) + } + + pub fn mock_execute_update_cycle_start_time( + &self, + app: &mut MockApp, + sender: Addr, + cycle_start_time: CycleStartTime, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::UpdateCycleStartTime { cycle_start_time }; + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn mock_execute_update_gate_addresses( + &self, + app: &mut MockApp, + sender: Addr, + gate_addresses: Vec, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::UpdateGateAddresses { + new_gate_addresses: gate_addresses, + }; + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn mock_execute_update_time_interval( + &self, + app: &mut MockApp, + sender: Addr, + time_interval: Option, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::UpdateTimeInterval { time_interval }; + if let Some(funds) = funds { + app.execute_contract(sender, self.addr().clone(), &msg, &[funds]) + } else { + app.execute_contract(sender, self.addr().clone(), &msg, &[]) + } + } + + pub fn mock_query_cycle_start_time(&self, app: &mut MockApp) -> CycleStartTime { + let msg = QueryMsg::GetCycleStartTime {}; + let res: CycleStartTime = self.query(app, msg); + res + } + + pub fn mock_query_gate_addresses(&self, app: &mut MockApp) -> Vec { + let msg = QueryMsg::GetGateAddresses {}; + let res: Vec = self.query(app, msg); + res + } + + pub fn mock_query_current_ado_path(&self, app: &mut MockApp) -> Addr { + let msg = QueryMsg::GetCurrentAdoPath {}; + let res: Addr = self.query(app, msg); + res + } + + pub fn mock_query_time_interval(&self, app: &mut MockApp) -> String { + let msg = QueryMsg::GetTimeInterval {}; + let res: String = self.query(app, msg); + res + } +} + +pub fn mock_andromeda_time_gate() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_time_gate_instantiate_msg( + kernel_address: String, + owner: Option, + gate_addresses: Vec, + cycle_start_time: CycleStartTime, + time_interval: Option, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address, + owner, + gate_addresses, + cycle_start_time, + time_interval, + } +} diff --git a/contracts/modules/andromeda-time-gate/src/state.rs b/contracts/modules/andromeda-time-gate/src/state.rs new file mode 100644 index 000000000..98aef6cfd --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/state.rs @@ -0,0 +1,7 @@ +use andromeda_std::{amp::AndrAddr, common::Milliseconds}; +use cw_storage_plus::Item; +use cw_utils::Expiration; + +pub const GATE_ADDRESSES: Item> = Item::new("gate_addresses"); +pub const CYCLE_START_TIME: Item<(Expiration, Milliseconds)> = Item::new("cycle_start_time"); +pub const TIME_INTERVAL: Item = Item::new("time_interval"); diff --git a/contracts/modules/andromeda-time-gate/src/testing/mock.rs b/contracts/modules/andromeda-time-gate/src/testing/mock.rs new file mode 100644 index 000000000..989bdf1e4 --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/testing/mock.rs @@ -0,0 +1,130 @@ +use andromeda_modules::time_gate::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ + amp::AndrAddr, + common::{expiration::Expiry, Milliseconds}, + error::ContractError, + testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier, MOCK_KERNEL_CONTRACT}, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Addr, BlockInfo, Deps, DepsMut, Env, MessageInfo, OwnedDeps, Response, Timestamp, +}; +use cw_utils::Expiration; + +use crate::contract::{execute, instantiate, query}; + +pub type MockDeps = OwnedDeps; + +pub fn proper_initialization( + gate_addresses: Vec, + cycle_start_time: Option, + time_interval: Option, +) -> (MockDeps, MessageInfo) { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + gate_addresses, + cycle_start_time, + time_interval, + }; + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + let res = instantiate(deps.as_mut(), env, info.clone(), msg).unwrap(); + assert_eq!(0, res.messages.len()); + (deps, info) +} + +pub fn update_cycle_start_time( + deps: DepsMut<'_>, + cycle_start_time: Option, + sender: &str, +) -> Result { + let msg = ExecuteMsg::UpdateCycleStartTime { cycle_start_time }; + let info = mock_info(sender, &[]); + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + execute(deps, env, info, msg) +} + +pub fn update_gate_addresses( + deps: DepsMut<'_>, + gate_addresses: Vec, + sender: &str, +) -> Result { + let msg = ExecuteMsg::UpdateGateAddresses { + new_gate_addresses: gate_addresses, + }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn update_time_interval( + deps: DepsMut<'_>, + time_interval: u64, + sender: &str, +) -> Result { + let msg = ExecuteMsg::UpdateTimeInterval { time_interval }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn query_cycle_start_time(deps: Deps) -> Result<(Expiration, Milliseconds), ContractError> { + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + let res = query(deps, env, QueryMsg::GetCycleStartTime {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_gate_addresses(deps: Deps) -> Result, ContractError> { + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + let res = query(deps, env, QueryMsg::GetGateAddresses {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_time_interval(deps: Deps) -> Result { + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + let res = query(deps, env, QueryMsg::GetTimeInterval {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_current_ado_path(deps: Deps, env: Env) -> Result { + let res = query(deps, env, QueryMsg::GetCurrentAdoPath {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} diff --git a/contracts/modules/andromeda-time-gate/src/testing/mod.rs b/contracts/modules/andromeda-time-gate/src/testing/mod.rs new file mode 100644 index 000000000..3bfda2893 --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock; +mod tests; diff --git a/contracts/modules/andromeda-time-gate/src/testing/tests.rs b/contracts/modules/andromeda-time-gate/src/testing/tests.rs new file mode 100644 index 000000000..82fb18d5c --- /dev/null +++ b/contracts/modules/andromeda-time-gate/src/testing/tests.rs @@ -0,0 +1,214 @@ +use super::mock::{ + proper_initialization, query_current_ado_path, query_cycle_start_time, query_gate_addresses, + query_time_interval, update_cycle_start_time, update_gate_addresses, update_time_interval, +}; +use andromeda_std::error::ContractError; +use andromeda_std::{ + amp::AndrAddr, + common::{expiration::Expiry, Milliseconds}, +}; +use cosmwasm_std::{testing::mock_env, BlockInfo, Timestamp}; +use cw_utils::Expiration; + +#[test] +fn test_instantiation() { + let (deps, _) = proper_initialization( + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + ], + Some(Expiry::FromNow(Milliseconds(5000000000))), + None, + ); + + let res = query_gate_addresses(deps.as_ref()).unwrap(); + assert_eq!( + res, + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + ] + ); + + let res = query_cycle_start_time(deps.as_ref()).unwrap(); + assert_eq!( + res, + ( + Expiration::AtTime(Timestamp::from_nanos(5000100000000000)), + Milliseconds::from_seconds(5000100) + ) + ); + + let res = query_time_interval(deps.as_ref()).unwrap(); + assert_eq!(res, "3600".to_string()); +} + +#[test] +fn test_update_cycle_start_time() { + let (mut deps, info) = proper_initialization( + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + ], + Some(Expiry::FromNow(Milliseconds(5000000000))), + None, + ); + + let err_res = update_cycle_start_time( + deps.as_mut(), + Some(Expiry::FromNow(Milliseconds(5000000000))), + info.sender.as_ref(), + ) + .unwrap_err(); + + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Same as an existed cycle start time".to_string()) + } + ); + + update_cycle_start_time( + deps.as_mut(), + Some(Expiry::FromNow(Milliseconds(4000000000))), + info.sender.as_ref(), + ) + .unwrap(); + + let res = query_cycle_start_time(deps.as_ref()).unwrap(); + assert_eq!( + res, + ( + Expiration::AtTime(Timestamp::from_nanos(4000100000000000)), + Milliseconds::from_seconds(4000100) + ) + ); + + update_time_interval(deps.as_mut(), 7200, info.sender.as_ref()).unwrap(); + + let res = query_time_interval(deps.as_ref()).unwrap(); + assert_eq!(res, "7200".to_string(),); +} + +#[test] +fn test_update_gate_addresses() { + let (mut deps, info) = proper_initialization( + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + ], + Some(Expiry::FromNow(Milliseconds(5000000000))), + None, + ); + + let err_res = update_gate_addresses( + deps.as_mut(), + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + ], + info.sender.as_ref(), + ) + .unwrap_err(); + + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Same as existed gate addresses".to_string()) + } + ); + + update_gate_addresses( + deps.as_mut(), + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + AndrAddr::from_string("mock_ado_4".to_string()), + ], + info.sender.as_ref(), + ) + .unwrap(); + + let res = query_gate_addresses(deps.as_ref()).unwrap(); + assert_eq!( + res, + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + AndrAddr::from_string("mock_ado_4".to_string()), + ] + ); +} + +#[test] +fn test_query_current_ado_path_not_started_yet() { + let (deps, _) = proper_initialization( + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + ], + Some(Expiry::FromNow(Milliseconds(5000000000))), + None, + ); + + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + + let res = query_current_ado_path(deps.as_ref(), env).unwrap_err(); + assert_eq!( + res, + ContractError::CustomError { + msg: "Cycle is not started yet".to_string() + } + ); +} + +#[test] +fn test_query_current_ado_path() { + let (deps, _) = proper_initialization( + vec![ + AndrAddr::from_string("mock_ado_1".to_string()), + AndrAddr::from_string("mock_ado_2".to_string()), + AndrAddr::from_string("mock_ado_3".to_string()), + AndrAddr::from_string("mock_ado_4".to_string()), + AndrAddr::from_string("mock_ado_5".to_string()), + ], + Some(Expiry::FromNow(Milliseconds(5000000000))), + None, + ); + + let mut env = mock_env(); + env.block = BlockInfo { + height: 100, + time: Timestamp::from_nanos(100000000000u64), + chain_id: "test-chain".to_string(), + }; + + env.block.time = env.block.time.plus_seconds(5000100); + let res = query_current_ado_path(deps.as_ref(), env.clone()).unwrap(); + assert_eq!(res, "mock_ado_1".to_string()); + + env.block.time = env.block.time.plus_seconds(6000); + let res = query_current_ado_path(deps.as_ref(), env.clone()).unwrap(); + assert_eq!(res, "mock_ado_2".to_string()); + + env.block.time = env.block.time.plus_seconds(3600 * 3); + let res = query_current_ado_path(deps.as_ref(), env.clone()).unwrap(); + assert_eq!(res, "mock_ado_5".to_string()); + + env.block.time = env.block.time.plus_seconds(3600); + let res = query_current_ado_path(deps.as_ref(), env.clone()).unwrap(); + assert_eq!(res, "mock_ado_1".to_string()); +} diff --git a/packages/andromeda-modules/src/lib.rs b/packages/andromeda-modules/src/lib.rs index 6f71686a3..1adf5f4ba 100644 --- a/packages/andromeda-modules/src/lib.rs +++ b/packages/andromeda-modules/src/lib.rs @@ -1,3 +1,4 @@ pub mod address_list; pub mod rates; pub mod shunting; +pub mod time_gate; diff --git a/packages/andromeda-modules/src/time_gate.rs b/packages/andromeda-modules/src/time_gate.rs new file mode 100644 index 000000000..1a5cf7efc --- /dev/null +++ b/packages/andromeda-modules/src/time_gate.rs @@ -0,0 +1,38 @@ +use andromeda_std::{ + amp::AndrAddr, + andr_exec, andr_instantiate, andr_query, + common::{expiration::Expiry, Milliseconds}, +}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Addr; +use cw_utils::Expiration; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub gate_addresses: Vec, + pub cycle_start_time: Option, + pub time_interval: Option, +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + UpdateCycleStartTime { cycle_start_time: Option }, + UpdateGateAddresses { new_gate_addresses: Vec }, + UpdateTimeInterval { time_interval: u64 }, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Addr)] + GetCurrentAdoPath {}, + #[returns((Expiration, Milliseconds))] + GetCycleStartTime {}, + #[returns(Vec)] + GetGateAddresses {}, + #[returns(String)] + GetTimeInterval {}, +}