diff --git a/CHANGELOG.md b/CHANGELOG.md index 442f1707b..aba018ff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) - Added Counter ADO [(#514)](https://github.com/andromedaprotocol/andromeda-core/pull/514) +- Added Curve ADO [(#515)](https://github.com/andromedaprotocol/andromeda-core/pull/515) - Added Date Time ADO [(#519)](https://github.com/andromedaprotocol/andromeda-core/pull/519) +- Added Graph ADO [(#526)](https://github.com/andromedaprotocol/andromeda-core/pull/526) - Added Authorized CW721 Addresses to Marketplace [(#542)](https://github.com/andromedaprotocol/andromeda-core/pull/542) - Added Denom Validation for Rates [(#568)](https://github.com/andromedaprotocol/andromeda-core/pull/568) - Added BuyNow option for Auction [(#533)](https://github.com/andromedaprotocol/andromeda-core/pull/533) diff --git a/Cargo.lock b/Cargo.lock index 41c0ffc09..e1e54d9f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,21 @@ dependencies = [ "serde", ] +[[package]] +name = "andromeda-graph" +version = "1.1.0" +dependencies = [ + "andromeda-data-storage", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20", +] + [[package]] name = "andromeda-ibc-registry" version = "1.0.1" @@ -599,6 +614,20 @@ dependencies = [ "serde", ] +[[package]] +name = "andromeda-point" +version = "1.0.0" +dependencies = [ + "andromeda-data-storage", + "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-primitive" version = "2.0.4" diff --git a/contracts/data-storage/andromeda-graph/.cargo/config b/contracts/data-storage/andromeda-graph/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/data-storage/andromeda-graph/Cargo.toml b/contracts/data-storage/andromeda-graph/Cargo.toml new file mode 100644 index 000000000..b61583142 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "andromeda-graph" +version = "1.1.0" +edition = "2021" +rust-version = "1.75.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[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 } +cw20 = { workspace = true } + +andromeda-std = { workspace = true } +andromeda-data-storage = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } diff --git a/contracts/data-storage/andromeda-graph/examples/schema.rs b/contracts/data-storage/andromeda-graph/examples/schema.rs new file mode 100644 index 000000000..71e016e9b --- /dev/null +++ b/contracts/data-storage/andromeda-graph/examples/schema.rs @@ -0,0 +1,10 @@ +use andromeda_data_storage::graph::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/data-storage/andromeda-graph/src/contract.rs b/contracts/data-storage/andromeda-graph/src/contract.rs new file mode 100644 index 000000000..03b1009f6 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/contract.rs @@ -0,0 +1,424 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{attr, ensure, Binary, Deps, DepsMut, Env, MessageInfo, Response, Storage}; + +use andromeda_data_storage::graph::{ + Coordinate, CoordinateInfo, ExecuteMsg, GetAllPointsResponse, GetMapInfoResponse, + GetMaxPointNumberResponse, InstantiateMsg, MapInfo, QueryMsg, StoredDate, +}; +use andromeda_data_storage::point::{ + GetDataOwnerResponse, PointCoordinate, QueryMsg as PointQueryMsg, +}; +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + amp::AndrAddr, + common::{actions::call_action, context::ExecuteContext, encode_binary}, + error::ContractError, + os::aos_querier::AOSQuerier, +}; + +use crate::state::{MAP_INFO, MAP_POINT_INFO, TOTAL_POINTS_NUMBER, USER_COORDINATE}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-graph"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[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, + 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, + }, + )?; + + MAP_INFO.save(deps.storage, &msg.map_info)?; + TOTAL_POINTS_NUMBER.save(deps.storage, &0)?; + + 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_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + let res = match msg { + ExecuteMsg::UpdateMap { map_info } => execute_update_map(ctx, map_info), + ExecuteMsg::StoreCoordinate { + coordinate, + is_timestamp_allowed, + } => execute_store_coordinate(ctx, coordinate, is_timestamp_allowed), + ExecuteMsg::StoreUserCoordinate { + user_location_paths, + } => execute_store_user_coordinate(ctx, user_location_paths), + ExecuteMsg::DeleteUserCoordinate { user } => execute_delete_user_coordinate(ctx, user), + _ => ADOContract::default().execute(ctx, msg), + }?; + + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +pub fn execute_update_map( + ctx: ExecuteContext, + map_info: MapInfo, +) -> Result { + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + + let map = MAP_INFO + .load(ctx.deps.storage) + .map_err(|_| ContractError::InvalidParameter { + error: Some("Map not found".to_string()), + })?; + + ensure!( + map != map_info, + ContractError::InvalidParameter { + error: Some("Map already exists".to_string()) + } + ); + + MAP_INFO.save(ctx.deps.storage, &map_info)?; + + let max_point_number = TOTAL_POINTS_NUMBER.load(ctx.deps.storage)?; + + for point in 1..=max_point_number { + MAP_POINT_INFO.remove(ctx.deps.storage, &point); + } + + TOTAL_POINTS_NUMBER.save(ctx.deps.storage, &0)?; + + Ok(Response::new().add_attributes(vec![attr("method", "update_map"), attr("sender", sender)])) +} + +pub fn execute_store_coordinate( + ctx: ExecuteContext, + coordinate: Coordinate, + is_timestamp_allowed: bool, +) -> Result { + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + + let map = MAP_INFO + .load(ctx.deps.storage) + .map_err(|_| ContractError::InvalidParameter { + error: Some("Map not found".to_string()), + })?; + + let MapInfo { + map_size, + allow_negative, + map_decimal, + } = map; + let x_length = map_size.x_width as f64; + let y_length = map_size.y_width as f64; + let z_length = map_size.z_width.map(|z| z as f64); + + let is_z_allowed = z_length.is_some(); + + let scale = 10_f64.powf(map_decimal as f64); + let x_coordinate = (coordinate.x_coordinate * scale) + .min(i64::MAX as f64) // Ensuring it doesn't exceed i64 bounds + .max(i64::MIN as f64); // Ensuring it doesn't underflow + let x_coordinate = (x_coordinate as i64) as f64 / scale; + + let y_coordinate = (coordinate.y_coordinate * scale) + .min(i64::MAX as f64) // Ensuring it doesn't exceed i64 bounds + .max(i64::MIN as f64); // Ensuring it doesn't underflow + let y_coordinate = (y_coordinate as i64) as f64 / scale; + + let z_coordinate = coordinate.z_coordinate.map(|z| { + let z_scaled = (z * scale) + .min(i64::MAX as f64) // Clamp the value to prevent overflow + .max(i64::MIN as f64); // Clamp the value to prevent underflow + (z_scaled as i64) as f64 / scale + }); + + ensure!( + z_coordinate.is_some() == is_z_allowed, + ContractError::InvalidParameter { + error: Some(if is_z_allowed { + "Z-axis is allowed".to_string() + } else { + "Z-axis is not allowed".to_string() + }) + } + ); + + match allow_negative { + true => { + ensure!( + x_coordinate >= -(x_length / 2_f64) && x_coordinate <= x_length / 2_f64, + ContractError::InvalidParameter { + error: Some("Wrong X Coordinate Range".to_string()) + } + ); + + ensure!( + y_coordinate >= -(y_length / 2_f64) && y_coordinate <= y_length / 2_f64, + ContractError::InvalidParameter { + error: Some("Wrong Y Coordinate Range".to_string()) + } + ); + + if is_z_allowed { + if let Some(z_coordinate) = z_coordinate { + ensure!( + z_coordinate >= -(z_length.unwrap() / 2_f64) + && z_coordinate <= z_length.unwrap() / 2_f64, + ContractError::InvalidParameter { + error: Some("Wrong Z Coordinate Range".to_string()) + } + ); + } + } + } + false => { + ensure!( + x_coordinate >= 0_f64 && x_coordinate <= x_length, + ContractError::InvalidParameter { + error: Some("Wrong X Coordinate Range".to_string()) + } + ); + + ensure!( + y_coordinate >= 0_f64 && y_coordinate <= y_length, + ContractError::InvalidParameter { + error: Some("Wrong Y Coordinate Range".to_string()) + } + ); + + if is_z_allowed { + if let Some(z_coordinate) = z_coordinate { + ensure!( + z_coordinate >= 0_f64 && z_coordinate <= z_length.unwrap(), + ContractError::InvalidParameter { + error: Some("Wrong Z Coordinate Range".to_string()) + } + ); + } + } + } + }; + + let point_number = TOTAL_POINTS_NUMBER + .load(ctx.deps.storage)? + .checked_add(1) + .unwrap(); + let timestamp = match is_timestamp_allowed { + true => Some(ctx.env.block.time.nanos()), + false => None, + }; + + MAP_POINT_INFO.save( + ctx.deps.storage, + &point_number, + &( + CoordinateInfo { + x: x_coordinate.to_string(), + y: y_coordinate.to_string(), + z: z_coordinate.map(|z_coordinate| z_coordinate.to_string()), + }, + StoredDate { timestamp }, + ), + )?; + TOTAL_POINTS_NUMBER.save(ctx.deps.storage, &point_number)?; + + Ok(Response::new().add_attributes(vec![ + attr("method", "store_coordinate"), + attr("sender", sender), + ])) +} + +pub fn execute_store_user_coordinate( + ctx: ExecuteContext, + user_location_paths: Vec, +) -> Result { + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + for user_location_path in user_location_paths { + let address = user_location_path.get_raw_address(&ctx.deps.as_ref())?; + let contract_info = ctx.deps.querier.query_wasm_contract_info(address.clone()); + if let Ok(contract_info) = contract_info { + let code_id = contract_info.code_id; + let adodb_addr = + ADOContract::default().get_adodb_address(ctx.deps.storage, &ctx.deps.querier)?; + let ado_type = AOSQuerier::ado_type_getter(&ctx.deps.querier, &adodb_addr, code_id)?; + + if ado_type.is_none() { + return Err(ContractError::InvalidADOType { + msg: Some("ADO Type must be point: None".to_string()), + }); + } + let ado_type = ado_type.unwrap(); + if ado_type == *"point" { + let user_point_coordinate: PointCoordinate = ctx + .deps + .querier + .query_wasm_smart(address.clone(), &PointQueryMsg::GetPoint {})?; + let user_res: GetDataOwnerResponse = ctx + .deps + .querier + .query_wasm_smart(address.clone(), &PointQueryMsg::GetDataOwner {})?; + let user: AndrAddr = user_res.owner; + let user_addr = user.get_raw_address(&ctx.deps.as_ref())?; + + user_point_coordinate.validate()?; + + USER_COORDINATE.save(ctx.deps.storage, user_addr, &user_point_coordinate)?; + } else { + return Err(ContractError::InvalidADOType { + msg: Some(format!("ADO Type must be point: {:?}", ado_type)), + }); + } + } else { + // Not a contract + return Err(ContractError::InvalidAddress {}); + } + } + Ok(Response::new().add_attributes(vec![ + attr("method", "store_user_coordinate"), + attr("sender", sender), + ])) +} + +pub fn execute_delete_user_coordinate( + ctx: ExecuteContext, + user: AndrAddr, +) -> Result { + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + let user_addr = user.get_raw_address(&ctx.deps.as_ref())?; + + USER_COORDINATE.remove(ctx.deps.storage, user_addr); + + Ok(Response::new().add_attributes(vec![ + attr("method", "delete_user_coordinate"), + attr("sender", sender), + ])) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetMapInfo {} => encode_binary(&get_map_info(deps.storage)?), + QueryMsg::GetMaxPointNumber {} => encode_binary(&get_max_point_number(deps.storage)?), + QueryMsg::GetAllPoints { start, limit } => { + encode_binary(&get_all_points(deps.storage, start, limit)?) + } + QueryMsg::GetUserCoordinate { user } => encode_binary(&get_user_coordinate(deps, user)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +pub fn get_map_info(storage: &dyn Storage) -> Result { + let map_info = MAP_INFO + .load(storage) + .map_err(|_| ContractError::InvalidParameter { + error: Some("Map not found".to_string()), + }); + match map_info { + Ok(map_info) => Ok(GetMapInfoResponse { map_info }), + Err(err) => Err(err), + } +} + +pub fn get_max_point_number( + storage: &dyn Storage, +) -> Result { + let max_point_number = TOTAL_POINTS_NUMBER.load(storage)?; + Ok(GetMaxPointNumberResponse { max_point_number }) +} + +pub fn get_all_points( + storage: &dyn Storage, + start: Option, + limit: Option, +) -> Result { + let max_point_number = TOTAL_POINTS_NUMBER.load(storage)?; + + // Set default values for pagination + let start_point = start.unwrap_or(1); // Start from 1 if no start provided + let limit = limit.unwrap_or(100); // Default limit to 100 points + + let mut res: Vec<(CoordinateInfo, StoredDate)> = Vec::new(); + + // Iterate with pagination + for point in start_point..=max_point_number { + if res.len() >= limit as usize { + break; // Stop when limit is reached + } + + // Use `may_load` to handle cases where the point may not exist + if let Some(coordinate) = MAP_POINT_INFO.may_load(storage, &point)? { + res.push(coordinate); + } + } + + Ok(GetAllPointsResponse { points: res }) +} + +pub fn get_user_coordinate(deps: Deps, user: AndrAddr) -> Result { + let user_addr = user.get_raw_address(&deps)?; + let user_coordinate = USER_COORDINATE.load(deps.storage, user_addr)?; + + Ok(CoordinateInfo { + x: user_coordinate.x_coordinate, + y: user_coordinate.y_coordinate, + z: user_coordinate.z_coordinate, + }) +} + +#[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/data-storage/andromeda-graph/src/lib.rs b/contracts/data-storage/andromeda-graph/src/lib.rs new file mode 100644 index 000000000..abf4af66c --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +pub mod state; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(test)] +mod testing; diff --git a/contracts/data-storage/andromeda-graph/src/mock.rs b/contracts/data-storage/andromeda-graph/src/mock.rs new file mode 100644 index 000000000..066bfb676 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/mock.rs @@ -0,0 +1,154 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +use crate::contract::{execute, instantiate, query}; +use andromeda_data_storage::graph::CoordinateInfo; +use andromeda_data_storage::graph::{ + Coordinate, GetAllPointsResponse, GetMapInfoResponse, GetMaxPointNumberResponse, MapInfo, +}; +use andromeda_data_storage::graph::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::amp::AndrAddr; +use andromeda_std::error::ContractError; +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 MockGraph(Addr); +mock_ado!(MockGraph, ExecuteMsg, QueryMsg); + +impl MockGraph { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + kernel_address: String, + owner: Option, + map_info: MapInfo, + ) -> MockGraph { + let msg = mock_graph_instantiate_msg(kernel_address, owner, map_info); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Graph Contract", + Some(sender.to_string()), + ) + .map_err(|e| ContractError::Std(e.into()))?; + MockGraph(Addr::unchecked(addr)) + } + + pub fn execute_update_map( + &self, + app: &mut MockApp, + sender: Addr, + map_info: MapInfo, + funds: Option, + ) -> ExecuteResult { + let msg = mock_execute_update_map_msg(map_info); + 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 execute_store_coordinate( + &self, + app: &mut MockApp, + sender: Addr, + coordinate: Coordinate, + is_timestamp_allowed: bool, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::StoreCoordinate { + coordinate, + is_timestamp_allowed, + }; + 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 execute_store_user_coordinate( + &self, + app: &mut MockApp, + sender: Addr, + user_location_paths: Vec, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::StoreUserCoordinate { + user_location_paths, + }; + 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 execute_delete_user_coordinate( + &self, + app: &mut MockApp, + sender: Addr, + user: AndrAddr, + funds: Option, + ) -> ExecuteResult { + let msg = ExecuteMsg::DeleteUserCoordinate { user }; + 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 query_map_info(&self, app: &mut MockApp) -> GetMapInfoResponse { + let msg = QueryMsg::GetMapInfo {}; + let res: GetMapInfoResponse = self.query(app, msg); + res + } + + pub fn query_max_point_number(&self, app: &mut MockApp) -> GetMaxPointNumberResponse { + let msg = QueryMsg::GetMaxPointNumber {}; + let res: GetMaxPointNumberResponse = self.query(app, msg); + res + } + + pub fn query_all_points(&self, app: &mut MockApp) -> GetAllPointsResponse { + let msg = QueryMsg::GetAllPoints {}; + let res: GetAllPointsResponse = self.query(app, msg); + res + } + + pub fn query_user_coordinate(&self, app: &mut MockApp, user: AndrAddr) -> CoordinateInfo { + let msg = QueryMsg::GetUserCoordinate { user }; + let res: CoordinateInfo = self.query(app, msg); + res + } +} + +pub fn mock_andromeda_graph() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_graph_instantiate_msg( + kernel_address: String, + owner: Option, + map_info: MapInfo, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address, + owner, + map_info, + } +} + +pub fn mock_execute_update_map_msg(map_info: MapInfo) -> ExecuteMsg { + ExecuteMsg::UpdateMap { map_info } +} diff --git a/contracts/data-storage/andromeda-graph/src/state.rs b/contracts/data-storage/andromeda-graph/src/state.rs new file mode 100644 index 000000000..4476a643a --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/state.rs @@ -0,0 +1,9 @@ +use andromeda_data_storage::graph::{CoordinateInfo, MapInfo, StoredDate}; +use andromeda_data_storage::point::PointCoordinate; +use cosmwasm_std::Addr; +use cw_storage_plus::{Item, Map}; + +pub const MAP_INFO: Item = Item::new("map_info"); +pub const MAP_POINT_INFO: Map<&u128, (CoordinateInfo, StoredDate)> = Map::new("map_point_info"); +pub const TOTAL_POINTS_NUMBER: Item = Item::new("total_points_number"); +pub const USER_COORDINATE: Map = Map::new("user_coordinate"); diff --git a/contracts/data-storage/andromeda-graph/src/testing/mock.rs b/contracts/data-storage/andromeda-graph/src/testing/mock.rs new file mode 100644 index 000000000..294081647 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/testing/mock.rs @@ -0,0 +1,113 @@ +use andromeda_data_storage::graph::{Coordinate, GetMapInfoResponse, MapInfo}; +use andromeda_data_storage::graph::{ + CoordinateInfo, ExecuteMsg, GetAllPointsResponse, GetMaxPointNumberResponse, InstantiateMsg, + QueryMsg, +}; +use andromeda_std::{ + amp::AndrAddr, error::ContractError, testing::mock_querier::MOCK_KERNEL_CONTRACT, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Deps, DepsMut, MessageInfo, OwnedDeps, Response, +}; + +use crate::contract::{execute, instantiate, query}; +use crate::testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier}; + +pub type MockDeps = OwnedDeps; + +pub fn proper_initialization(map_info: MapInfo) -> (MockDeps, MessageInfo) { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("sender", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + map_info, + }; + let env = mock_env(); + instantiate(deps.as_mut(), env, info.clone(), msg).unwrap(); + (deps, info) +} + +pub fn update_map( + deps: DepsMut<'_>, + map_info: MapInfo, + sender: &str, +) -> Result { + let msg = ExecuteMsg::UpdateMap { map_info }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn store_coordinate( + deps: DepsMut<'_>, + coordinate: Coordinate, + is_timestamp_allowed: bool, + sender: &str, +) -> Result { + let msg = ExecuteMsg::StoreCoordinate { + coordinate, + is_timestamp_allowed, + }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn store_user_coordinate( + deps: DepsMut<'_>, + user_location_paths: Vec, + sender: &str, +) -> Result { + let msg = ExecuteMsg::StoreUserCoordinate { + user_location_paths, + }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn delete_user_coordinate( + deps: DepsMut<'_>, + user: AndrAddr, + sender: &str, +) -> Result { + let msg = ExecuteMsg::DeleteUserCoordinate { user }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn query_map_info(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetMapInfo {}); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_max_point_number(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetMaxPointNumber {}); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_all_points( + deps: Deps, + start: Option, + limit: Option, +) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetAllPoints { start, limit }); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} + +pub fn query_user_coordinate(deps: Deps, user: AndrAddr) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetUserCoordinate { user }); + match res { + Ok(res) => Ok(from_json(res)?), + Err(err) => Err(err), + } +} diff --git a/contracts/data-storage/andromeda-graph/src/testing/mock_querier.rs b/contracts/data-storage/andromeda-graph/src/testing/mock_querier.rs new file mode 100644 index 000000000..81515454b --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/testing/mock_querier.rs @@ -0,0 +1,121 @@ +use andromeda_data_storage::point::{ + GetDataOwnerResponse, PointCoordinate, QueryMsg as PointQueryMsg, +}; +use andromeda_std::amp::AndrAddr; +use andromeda_std::testing::mock_querier::MockAndromedaQuerier; +use andromeda_std::{ + ado_base::InstantiateMsg, ado_contract::ADOContract, + testing::mock_querier::MOCK_KERNEL_CONTRACT, +}; +use cosmwasm_std::QuerierWrapper; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, + Coin, ContractInfoResponse, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, + SystemResult, WasmQuery, +}; +use cosmwasm_std::{to_json_binary, Binary, ContractResult}; + +pub const MOCK_POINT_CONTRACT: &str = "point_contract"; + +/// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. +/// +/// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. +pub fn mock_dependencies_custom( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + let storage = MockStorage::default(); + let mut deps = OwnedDeps { + storage, + api: MockApi::default(), + querier: custom_querier, + custom_query_type: std::marker::PhantomData, + }; + ADOContract::default() + .instantiate( + &mut deps.storage, + mock_env(), + &deps.api, + &QuerierWrapper::new(&deps.querier), + mock_info("sender", &[]), + InstantiateMsg { + ado_type: "graph".to_string(), + ado_version: "test".to_string(), + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + }, + ) + .unwrap(); + deps +} +pub struct WasmMockQuerier { + pub base: MockQuerier, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {e}"), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + match contract_addr.as_str() { + MOCK_POINT_CONTRACT => self.handle_point_smart_query(msg), + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + QueryRequest::Wasm(WasmQuery::ContractInfo { contract_addr }) => { + match contract_addr.as_str() { + MOCK_POINT_CONTRACT => self.handle_point_contract_info_query(), + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + + fn handle_point_smart_query(&self, msg: &Binary) -> QuerierResult { + match from_json(msg).unwrap() { + PointQueryMsg::GetPoint {} => { + let msg_response = PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: Some("10".to_string()), + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + PointQueryMsg::GetDataOwner {} => { + let msg_response = GetDataOwnerResponse { + owner: AndrAddr::from_string("sender".to_string()), + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + _ => panic!("Unsupported Query"), + } + } + + fn handle_point_contract_info_query(&self) -> QuerierResult { + let mut msg_response = ContractInfoResponse::default(); + msg_response.code_id = 5; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { base } + } +} diff --git a/contracts/data-storage/andromeda-graph/src/testing/mod.rs b/contracts/data-storage/andromeda-graph/src/testing/mod.rs new file mode 100644 index 000000000..217ceb8c1 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/testing/mod.rs @@ -0,0 +1,3 @@ +mod mock; +mod mock_querier; +mod tests; diff --git a/contracts/data-storage/andromeda-graph/src/testing/tests.rs b/contracts/data-storage/andromeda-graph/src/testing/tests.rs new file mode 100644 index 000000000..8ab5e9cc2 --- /dev/null +++ b/contracts/data-storage/andromeda-graph/src/testing/tests.rs @@ -0,0 +1,507 @@ +use crate::testing::mock_querier::MOCK_POINT_CONTRACT; +use andromeda_data_storage::graph::{Coordinate, MapInfo, MapSize, StoredDate}; +use andromeda_data_storage::graph::{CoordinateInfo, GetMapInfoResponse}; +use andromeda_std::amp::AndrAddr; +use andromeda_std::error::ContractError; + +use super::mock::{ + delete_user_coordinate, proper_initialization, query_all_points, query_map_info, + query_max_point_number, query_user_coordinate, store_coordinate, store_user_coordinate, + update_map, +}; + +#[test] +fn test_instantiation_z_allowed() { + let (deps, _) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: Some(10), + }, + allow_negative: false, + map_decimal: 5, + }); + + let res = query_map_info(deps.as_ref()).unwrap(); + assert_eq!( + res, + GetMapInfoResponse { + map_info: MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: Some(10), + }, + allow_negative: false, + map_decimal: 5, + } + } + ); +} + +#[test] +fn test_instantiation_z_not_allowed() { + let (deps, _) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }); + + let res = query_map_info(deps.as_ref()).unwrap(); + assert_eq!( + res, + GetMapInfoResponse { + map_info: MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + } + } + ); +} + +#[test] +fn test_update_map_with_same_info() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }); + let err_res = update_map( + deps.as_mut(), + MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Map already exists".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_with_z_not_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }); + + let err_res = store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 9.12345_f64, + y_coordinate: 2.12345_f64, + z_coordinate: Some(4.12345_f64), + }, + false, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Z-axis is not allowed".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_with_z_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: Some(10), + }, + allow_negative: false, + map_decimal: 5, + }); + + let err_res = store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 9.12345_f64, + y_coordinate: 2.12345_f64, + z_coordinate: None, + }, + false, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Z-axis is allowed".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_with_wrong_range_disallow_negative_z_not_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }); + + let err_res = store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 9.12345_f64, + y_coordinate: 12.12345_f64, + z_coordinate: None, + }, + false, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Wrong Y Coordinate Range".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_with_wrong_range_disallow_negative_z_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: Some(10), + }, + allow_negative: false, + map_decimal: 5, + }); + + let err_res = store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 9.12345_f64, + y_coordinate: 9.12345_f64, + z_coordinate: Some(12.12345_f64), + }, + false, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Wrong Z Coordinate Range".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_with_wrong_range_allow_negative_z_not_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: true, + map_decimal: 5, + }); + + let err_res = store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: -4.12345_f64, + y_coordinate: 5.12345_f64, + z_coordinate: None, + }, + false, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Wrong Y Coordinate Range".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_with_wrong_range_allow_negative_z_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: Some(10), + }, + allow_negative: true, + map_decimal: 5, + }); + + let err_res = store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: -4.12345_f64, + y_coordinate: 4.12345_f64, + z_coordinate: Some(-12.12345_f64), + }, + false, + info.sender.as_ref(), + ) + .unwrap_err(); + assert_eq!( + err_res, + ContractError::InvalidParameter { + error: Some("Wrong Z Coordinate Range".to_string()) + } + ); +} + +#[test] +fn test_store_coordinate_disallow_negative_and_update_map_timestamp_not_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }); + + store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 9.123456_f64, + y_coordinate: 8.12345_f64, + z_coordinate: None, + }, + false, + info.sender.as_ref(), + ) + .unwrap(); + + store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 8.12345_f64, + y_coordinate: 8.123458_f64, + z_coordinate: None, + }, + false, + info.sender.as_ref(), + ) + .unwrap(); + + let max_point = query_max_point_number(deps.as_ref()) + .unwrap() + .max_point_number; + assert_eq!(max_point, 2); + + let all_points = query_all_points(deps.as_ref(), None, None).unwrap().points; + assert_eq!( + all_points, + vec![ + ( + CoordinateInfo { + x: "9.12345".to_string(), + y: "8.12345".to_string(), + z: None, + }, + StoredDate { timestamp: None } + ), + ( + CoordinateInfo { + x: "8.12345".to_string(), + y: "8.12345".to_string(), + z: None, + }, + StoredDate { timestamp: None } + ), + ] + ); + + update_map( + deps.as_mut(), + MapInfo { + map_size: MapSize { + x_width: 100, + y_width: 100, + z_width: Some(100), + }, + allow_negative: false, + map_decimal: 5, + }, + info.sender.as_ref(), + ) + .unwrap(); + + let all_points = query_all_points(deps.as_ref(), None, None).unwrap().points; + assert_eq!(all_points, vec![]); + + let max_point = query_max_point_number(deps.as_ref()) + .unwrap() + .max_point_number; + assert_eq!(max_point, 0); +} + +#[test] +fn test_store_coordinate_disallow_negative_timestamp_allowed() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 10, + y_width: 10, + z_width: None, + }, + allow_negative: false, + map_decimal: 5, + }); + + store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 9.123456_f64, + y_coordinate: 8.12345_f64, + z_coordinate: None, + }, + true, + info.sender.as_ref(), + ) + .unwrap(); + + store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 8.12345_f64, + y_coordinate: 8.123458_f64, + z_coordinate: None, + }, + true, + info.sender.as_ref(), + ) + .unwrap(); + + store_coordinate( + deps.as_mut(), + Coordinate { + x_coordinate: 5_f64, + y_coordinate: 8_f64, + z_coordinate: None, + }, + true, + info.sender.as_ref(), + ) + .unwrap(); + + let max_point = query_max_point_number(deps.as_ref()) + .unwrap() + .max_point_number; + assert_eq!(max_point, 3); + + let all_points = query_all_points(deps.as_ref(), None, None).unwrap().points; + assert_eq!( + all_points, + vec![ + ( + CoordinateInfo { + x: "9.12345".to_string(), + y: "8.12345".to_string(), + z: None, + }, + StoredDate { + timestamp: Some(1_571_797_419_879_305_533), + } + ), + ( + CoordinateInfo { + x: "8.12345".to_string(), + y: "8.12345".to_string(), + z: None, + }, + StoredDate { + timestamp: Some(1_571_797_419_879_305_533), + } + ), + ( + CoordinateInfo { + x: "5".to_string(), + y: "8".to_string(), + z: None, + }, + StoredDate { + timestamp: Some(1_571_797_419_879_305_533), + } + ), + ] + ); +} + +#[test] +fn test_store_user_coordinate() { + let (mut deps, info) = proper_initialization(MapInfo { + map_size: MapSize { + x_width: 100, + y_width: 100, + z_width: Some(100), + }, + allow_negative: false, + map_decimal: 5, + }); + + store_user_coordinate( + deps.as_mut(), + vec![AndrAddr::from_string(MOCK_POINT_CONTRACT.to_string())], + info.sender.as_ref(), + ) + .unwrap(); + + let query_res: CoordinateInfo = + query_user_coordinate(deps.as_ref(), AndrAddr::from_string("sender".to_string())).unwrap(); + assert_eq!( + query_res, + CoordinateInfo { + x: "10".to_string(), + y: "10".to_string(), + z: Some("10".to_string()), + }, + ); + + delete_user_coordinate( + deps.as_mut(), + AndrAddr::from_string("sender".to_string()), + "sender", + ) + .unwrap(); + + query_user_coordinate(deps.as_ref(), AndrAddr::from_string("sender".to_string())).unwrap_err(); +} diff --git a/contracts/data-storage/andromeda-point/.cargo/config b/contracts/data-storage/andromeda-point/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/data-storage/andromeda-point/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/data-storage/andromeda-point/Cargo.toml b/contracts/data-storage/andromeda-point/Cargo.toml new file mode 100644 index 000000000..5dfffb892 --- /dev/null +++ b/contracts/data-storage/andromeda-point/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "andromeda-point" +version = "1.0.0" +authors = ["Mitar Djakovic "] +edition = "2021" +rust-version = "1.75.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[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 = ["rates"] } +andromeda-data-storage = { workspace = true } + + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } diff --git a/contracts/data-storage/andromeda-point/examples/schema.rs b/contracts/data-storage/andromeda-point/examples/schema.rs new file mode 100644 index 000000000..bb2a97e88 --- /dev/null +++ b/contracts/data-storage/andromeda-point/examples/schema.rs @@ -0,0 +1,10 @@ +use andromeda_data_storage::point::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + + } +} diff --git a/contracts/data-storage/andromeda-point/src/contract.rs b/contracts/data-storage/andromeda-point/src/contract.rs new file mode 100644 index 000000000..f7c9f7054 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/contract.rs @@ -0,0 +1,74 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; + +use crate::{ + execute::handle_execute, + query::{get_data_owner, get_point}, + state::RESTRICTION, +}; +use andromeda_data_storage::point::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + common::{context::ExecuteContext, encode_binary}, + error::ContractError, +}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:andromeda-point"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[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, + 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, + }, + )?; + RESTRICTION.save(deps.storage, &msg.restriction)?; + 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), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetPoint {} => encode_binary(&get_point(deps.storage)?), + QueryMsg::GetDataOwner {} => encode_binary(&get_data_owner(deps.storage)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +#[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/data-storage/andromeda-point/src/execute.rs b/contracts/data-storage/andromeda-point/src/execute.rs new file mode 100644 index 000000000..0cafa992d --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/execute.rs @@ -0,0 +1,153 @@ +use andromeda_data_storage::point::{ExecuteMsg, PointCoordinate, PointRestriction}; +use andromeda_std::{ + ado_base::rates::{Rate, RatesMessage}, + ado_contract::ADOContract, + common::{actions::call_action, context::ExecuteContext, rates::get_tax_amount, Funds}, + error::ContractError, +}; +use cosmwasm_std::{ + coin, ensure, BankMsg, Coin, CosmosMsg, Deps, MessageInfo, Response, SubMsg, Uint128, +}; +use cw_utils::nonpayable; + +use crate::{ + query::has_permission, + state::{DATA, DATA_OWNER, RESTRICTION}, +}; + +pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action = msg.as_ref().to_string(); + call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + match msg.clone() { + ExecuteMsg::UpdateRestriction { restriction } => update_restriction(ctx, restriction), + ExecuteMsg::SetPoint { point } => set_point(ctx, point, action), + ExecuteMsg::DeletePoint {} => delete_point(ctx), + ExecuteMsg::Rates(rates_message) => match rates_message { + RatesMessage::SetRate { rate, .. } => match rate { + Rate::Local(local_rate) => { + // Percent rates aren't applicable in this case, so we enforce Flat rates + ensure!(local_rate.value.is_flat(), ContractError::InvalidRate {}); + ADOContract::default().execute(ctx, msg) + } + Rate::Contract(_) => ADOContract::default().execute(ctx, msg), + }, + RatesMessage::RemoveRate { .. } => ADOContract::default().execute(ctx, msg), + }, + _ => ADOContract::default().execute(ctx, msg), + } +} + +pub fn update_restriction( + ctx: ExecuteContext, + restriction: PointRestriction, +) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ensure!( + ADOContract::default().is_owner_or_operator(ctx.deps.storage, sender.as_ref())?, + ContractError::Unauthorized {} + ); + RESTRICTION.save(ctx.deps.storage, &restriction)?; + Ok(Response::new() + .add_attribute("method", "update_restriction") + .add_attribute("sender", sender)) +} + +pub fn set_point( + ctx: ExecuteContext, + point: PointCoordinate, + action: String, +) -> Result { + let sender = ctx.info.sender.clone(); + ensure!( + has_permission(ctx.deps.storage, &sender)?, + ContractError::Unauthorized {} + ); + + let tax_response = tax_set_value(ctx.deps.as_ref(), &ctx.info, action)?; + + point.validate()?; + + DATA.save(ctx.deps.storage, &point.clone())?; + DATA_OWNER.save(ctx.deps.storage, &sender)?; + + let mut response = Response::new() + .add_attribute("method", "set_point") + .add_attribute("sender", sender) + .add_attribute("point", format!("{point:?}")); + + if let Some(tax_response) = tax_response { + response = response.add_submessages(tax_response.1); + let refund = tax_response.0.try_get_coin()?; + if !refund.amount.is_zero() { + return Ok(response.add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: ctx.info.sender.into_string(), + amount: vec![refund], + }))); + } + } + + Ok(response) +} + +pub fn delete_point(ctx: ExecuteContext) -> Result { + nonpayable(&ctx.info)?; + let sender = ctx.info.sender; + ensure!( + has_permission(ctx.deps.storage, &sender)?, + ContractError::Unauthorized {} + ); + DATA.remove(ctx.deps.storage); + DATA_OWNER.remove(ctx.deps.storage); + Ok(Response::new() + .add_attribute("method", "delete_point") + .add_attribute("sender", sender)) +} + +fn tax_set_value( + deps: Deps, + info: &MessageInfo, + action: String, +) -> Result)>, ContractError> { + let default_coin = coin(0_u128, "uandr".to_string()); + let sent_funds = info.funds.first().unwrap_or(&default_coin); + + let transfer_response = ADOContract::default().query_deducted_funds( + deps, + action, + Funds::Native(sent_funds.clone()), + )?; + + if let Some(transfer_response) = transfer_response { + let remaining_funds = transfer_response.leftover_funds.try_get_coin()?; + let tax_amount = get_tax_amount( + &transfer_response.msgs, + remaining_funds.amount, + remaining_funds.amount, + ); + + let refund = if sent_funds.amount > tax_amount { + sent_funds.amount.checked_sub(tax_amount)? + } else { + Uint128::zero() + }; + + let after_tax_payment = Coin { + denom: remaining_funds.denom, + amount: refund, + }; + Ok(Some(( + Funds::Native(after_tax_payment), + transfer_response.msgs, + ))) + } else { + Ok(None) + } +} diff --git a/contracts/data-storage/andromeda-point/src/lib.rs b/contracts/data-storage/andromeda-point/src/lib.rs new file mode 100644 index 000000000..36553fb8f --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/lib.rs @@ -0,0 +1,9 @@ +pub mod contract; +mod execute; +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; +mod query; +mod state; + +#[cfg(test)] +mod testing; diff --git a/contracts/data-storage/andromeda-point/src/mock.rs b/contracts/data-storage/andromeda-point/src/mock.rs new file mode 100644 index 000000000..a3c8db352 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/mock.rs @@ -0,0 +1,111 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +use crate::contract::{execute, instantiate, query}; +use andromeda_data_storage::point::{ + ExecuteMsg, GetDataOwnerResponse, InstantiateMsg, PointCoordinate, PointRestriction, QueryMsg, +}; +use andromeda_std::ado_base::rates::{Rate, RatesMessage}; +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 MockPoint(Addr); +mock_ado!(MockPoint, ExecuteMsg, QueryMsg); + +impl MockPoint { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + kernel_address: String, + owner: Option, + restriction: PointRestriction, + ) -> MockPoint { + let msg = mock_point_instantiate_msg(kernel_address, owner, restriction); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Point Contract", + Some(sender.to_string()), + ) + .unwrap(); + MockPoint(Addr::unchecked(addr)) + } + + pub fn execute_set_point( + &self, + app: &mut MockApp, + sender: Addr, + point: PointCoordinate, + funds: Option, + ) -> ExecuteResult { + let msg = mock_point_set_point_msg(point); + 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 execute_add_rate( + &self, + app: &mut MockApp, + sender: Addr, + action: String, + rate: Rate, + ) -> ExecuteResult { + self.execute(app, &mock_set_rate_msg(action, rate), sender, &[]) + } + + pub fn query_point(&self, app: &mut MockApp) -> PointCoordinate { + let msg = mock_point_get_point(); + let res: PointCoordinate = self.query(app, msg); + res + } + + pub fn query_data_owner(&self, app: &mut MockApp) -> GetDataOwnerResponse { + let msg = mock_point_get_data_owner(); + let res: GetDataOwnerResponse = self.query(app, msg); + res + } +} + +pub fn mock_andromeda_point() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_point_instantiate_msg( + kernel_address: String, + owner: Option, + restriction: PointRestriction, +) -> InstantiateMsg { + InstantiateMsg { + kernel_address, + owner, + restriction, + } +} + +/// Used to generate a message to set point +pub fn mock_point_set_point_msg(point: PointCoordinate) -> ExecuteMsg { + ExecuteMsg::SetPoint { point } +} + +pub fn mock_set_rate_msg(action: String, rate: Rate) -> ExecuteMsg { + ExecuteMsg::Rates(RatesMessage::SetRate { action, rate }) +} + +pub fn mock_point_get_point() -> QueryMsg { + QueryMsg::GetPoint {} +} + +pub fn mock_point_get_data_owner() -> QueryMsg { + QueryMsg::GetDataOwner {} +} diff --git a/contracts/data-storage/andromeda-point/src/query.rs b/contracts/data-storage/andromeda-point/src/query.rs new file mode 100644 index 000000000..9bf6915b2 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/query.rs @@ -0,0 +1,29 @@ +use crate::state::{DATA, DATA_OWNER, RESTRICTION}; +use andromeda_data_storage::point::{GetDataOwnerResponse, PointCoordinate, PointRestriction}; +use andromeda_std::{ado_contract::ADOContract, amp::AndrAddr, error::ContractError}; +use cosmwasm_std::{Addr, Storage}; + +pub fn has_permission(storage: &dyn Storage, addr: &Addr) -> Result { + let is_operator = ADOContract::default().is_owner_or_operator(storage, addr.as_str())?; + let allowed = match RESTRICTION.load(storage)? { + PointRestriction::Private => is_operator, + PointRestriction::Public => true, + PointRestriction::Restricted => match DATA_OWNER.load(storage).ok() { + Some(owner) => addr == owner, + None => true, + }, + }; + Ok(is_operator || allowed) +} + +pub fn get_point(storage: &dyn Storage) -> Result { + let point = DATA.load(storage)?; + Ok(point) +} + +pub fn get_data_owner(storage: &dyn Storage) -> Result { + let owner = DATA_OWNER.load(storage)?; + Ok(GetDataOwnerResponse { + owner: AndrAddr::from_string(owner), + }) +} diff --git a/contracts/data-storage/andromeda-point/src/state.rs b/contracts/data-storage/andromeda-point/src/state.rs new file mode 100644 index 000000000..bec7a75e2 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/state.rs @@ -0,0 +1,7 @@ +use andromeda_data_storage::point::{PointCoordinate, PointRestriction}; +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +pub const DATA: Item = Item::new("data"); +pub const DATA_OWNER: Item = Item::new("data_owner"); +pub const RESTRICTION: Item = Item::new("restriction"); diff --git a/contracts/data-storage/andromeda-point/src/testing/mock.rs b/contracts/data-storage/andromeda-point/src/testing/mock.rs new file mode 100644 index 000000000..db31d1363 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/testing/mock.rs @@ -0,0 +1,68 @@ +use andromeda_data_storage::point::{ + ExecuteMsg, InstantiateMsg, PointCoordinate, PointRestriction, QueryMsg, +}; +use andromeda_std::{ + error::ContractError, + testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier, MOCK_KERNEL_CONTRACT}, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Coin, Deps, DepsMut, MessageInfo, OwnedDeps, Response, +}; + +use crate::contract::{execute, instantiate, query}; + +pub type MockDeps = OwnedDeps; + +pub fn proper_initialization(restriction: PointRestriction) -> (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, + restriction, + }; + let env = mock_env(); + instantiate(deps.as_mut(), env, info.clone(), msg).unwrap(); + (deps, info) +} + +pub fn query_point(deps: Deps) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetPoint {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn set_point( + deps: DepsMut<'_>, + point: &PointCoordinate, + sender: &str, +) -> Result { + let msg = ExecuteMsg::SetPoint { + point: point.clone(), + }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn set_point_with_funds( + deps: DepsMut<'_>, + point: &PointCoordinate, + sender: &str, + coin: Coin, +) -> Result { + let msg = ExecuteMsg::SetPoint { + point: point.clone(), + }; + let info = mock_info(sender, &[coin]); + execute(deps, mock_env(), info, msg) +} + +pub fn delete_point(deps: DepsMut<'_>, sender: &str) -> Result { + let msg = ExecuteMsg::DeletePoint {}; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} diff --git a/contracts/data-storage/andromeda-point/src/testing/mod.rs b/contracts/data-storage/andromeda-point/src/testing/mod.rs new file mode 100644 index 000000000..3bfda2893 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/testing/mod.rs @@ -0,0 +1,2 @@ +mod mock; +mod tests; diff --git a/contracts/data-storage/andromeda-point/src/testing/tests.rs b/contracts/data-storage/andromeda-point/src/testing/tests.rs new file mode 100644 index 000000000..f87cf17a4 --- /dev/null +++ b/contracts/data-storage/andromeda-point/src/testing/tests.rs @@ -0,0 +1,327 @@ +use crate::contract::{execute, query}; +use andromeda_data_storage::point::{ + ExecuteMsg, GetDataOwnerResponse, PointCoordinate, PointRestriction, QueryMsg, +}; +use cosmwasm_std::{ + coin, from_json, testing::mock_env, BankMsg, CosmosMsg, Decimal, Response, SubMsg, +}; + +use andromeda_std::{ + ado_base::rates::{LocalRate, LocalRateType, LocalRateValue, PercentRate, Rate, RatesMessage}, + ado_contract::ADOContract, + amp::{AndrAddr, Recipient}, + error::ContractError, +}; + +use super::mock::{ + delete_point, proper_initialization, query_point, set_point, set_point_with_funds, +}; + +#[test] +fn test_instantiation() { + proper_initialization(PointRestriction::Private); +} + +#[test] +fn test_set_and_update_point() { + let (mut deps, info) = proper_initialization(PointRestriction::Private); + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + point.validate().unwrap(); + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + + let query_res: PointCoordinate = query_point(deps.as_ref()).unwrap(); + + assert_eq!(point, query_res); + + let point = PointCoordinate::from_f64(5_f64, 5_f64, Some(5_f64)); + point.validate().unwrap(); + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + + let query_res: PointCoordinate = query_point(deps.as_ref()).unwrap(); + + assert_eq!(point, query_res); +} + +#[test] +fn test_set_point_with_tax() { + let (mut deps, info) = proper_initialization(PointRestriction::Private); + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + point.validate().unwrap(); + let tax_recipient = "tax_recipient"; + + // Set percent rates + let set_percent_rate_msg = ExecuteMsg::Rates(RatesMessage::SetRate { + action: "PointSetPoint".to_string(), + rate: Rate::Local(LocalRate { + rate_type: LocalRateType::Additive, + recipients: vec![], + value: LocalRateValue::Percent(PercentRate { + percent: Decimal::one(), + }), + description: None, + }), + }); + + let err = execute( + deps.as_mut(), + mock_env(), + info.clone(), + set_percent_rate_msg, + ) + .unwrap_err(); + + assert_eq!(err, ContractError::InvalidRate {}); + + let rate: Rate = Rate::Local(LocalRate { + rate_type: LocalRateType::Additive, + recipients: vec![Recipient { + address: AndrAddr::from_string(tax_recipient.to_string()), + msg: None, + ibc_recovery_address: None, + }], + value: LocalRateValue::Flat(coin(20_u128, "uandr")), + description: None, + }); + + // Set rates + ADOContract::default() + .set_rates(deps.as_mut().storage, "SetPoint", rate) + .unwrap(); + + // Sent the exact amount required for tax + let res = set_point_with_funds( + deps.as_mut(), + &point, + info.sender.as_ref(), + coin(20_u128, "uandr".to_string()), + ) + .unwrap(); + let expected_response: Response = Response::new() + .add_submessage(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: tax_recipient.to_string(), + amount: vec![coin(20, "uandr")], + }))) + .add_attributes(vec![("method", "set_point"), ("sender", "creator")]) + .add_attribute("point", format!("{point:?}")); + assert_eq!(expected_response, res); + + // Sent less than amount required for tax + let err = set_point_with_funds( + deps.as_mut(), + &point, + info.sender.as_ref(), + coin(19_u128, "uandr".to_string()), + ) + .unwrap_err(); + assert_eq!(err, ContractError::InsufficientFunds {}); + + // Sent more than required amount for tax + let res = set_point_with_funds( + deps.as_mut(), + &point, + info.sender.as_ref(), + coin(200_u128, "uandr".to_string()), + ) + .unwrap(); + let expected_response: Response = Response::new() + .add_submessage(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: tax_recipient.to_string(), + amount: vec![coin(20, "uandr")], + }))) + // 200 was sent, but the tax is only 20, so we send back the difference + .add_submessage(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: "creator".to_string(), + amount: vec![coin(180, "uandr")], + }))) + .add_attributes(vec![("method", "set_point"), ("sender", "creator")]) + .add_attribute("point", format!("{point:?}")); + assert_eq!(expected_response, res); +} + +struct TestHandlePointCoordinate { + name: &'static str, + point_coordinate: PointCoordinate, + expected_error: Option, +} + +#[test] +fn test_set_point_invalid() { + let test_cases = vec![ + TestHandlePointCoordinate { + name: "Invalid x_coordinate", + point_coordinate: PointCoordinate { + x_coordinate: "10.abc".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: Some("10".to_string()), + }, + expected_error: Some(ContractError::ParsingError { + err: "x_coordinate: can not parse to f64".to_string(), + }), + }, + TestHandlePointCoordinate { + name: "Invalid y_coordinate", + point_coordinate: PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10.abc".to_string(), + z_coordinate: None, + }, + expected_error: Some(ContractError::ParsingError { + err: "y_coordinate: can not parse to f64".to_string(), + }), + }, + TestHandlePointCoordinate { + name: "Invalid z_coordinate", + point_coordinate: PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: Some("10.abc".to_string()), + }, + expected_error: Some(ContractError::ParsingError { + err: "z_coordinate: can not parse to f64".to_string(), + }), + }, + ]; + + for test in test_cases { + let res = test.point_coordinate.validate(); + + if let Some(err) = test.expected_error { + assert_eq!(res.unwrap_err(), err, "{}", test.name); + continue; + } + + assert!(res.is_ok()) + } +} + +#[test] +fn test_delete_point() { + let (mut deps, info) = proper_initialization(PointRestriction::Private); + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + delete_point(deps.as_mut(), info.sender.as_ref()).unwrap(); + query_point(deps.as_ref()).unwrap_err(); +} + +#[test] +fn test_restriction_private() { + let (mut deps, info) = proper_initialization(PointRestriction::Private); + + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + let external_user = "external".to_string(); + + // Set Point as owner + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + delete_point(deps.as_mut(), info.sender.as_ref()).unwrap(); + query_point(deps.as_ref()).unwrap_err(); + + // Set Point as external user + // This should error + set_point(deps.as_mut(), &point, &external_user).unwrap_err(); + // Set a point by owner so we can test delete for it + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + // Delete point set by owner by external user + // This will error + delete_point(deps.as_mut(), &external_user).unwrap_err(); + + // Point is still present + query_point(deps.as_ref()).unwrap(); +} + +#[test] +fn test_restriction_public() { + let (mut deps, info) = proper_initialization(PointRestriction::Public); + + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + let external_user = "external".to_string(); + + // Set Point as owner + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + delete_point(deps.as_mut(), info.sender.as_ref()).unwrap(); + // This should error + query_point(deps.as_ref()).unwrap_err(); + + // Set Point as external user + set_point(deps.as_mut(), &point, &external_user).unwrap(); + delete_point(deps.as_mut(), &external_user).unwrap(); + // This should error + query_point(deps.as_ref()).unwrap_err(); + + // Set Point as owner + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + // Delete the point as external user + delete_point(deps.as_mut(), &external_user).unwrap(); + // This should error + query_point(deps.as_ref()).unwrap_err(); +} + +#[test] +fn test_restriction_restricted() { + let (mut deps, info) = proper_initialization(PointRestriction::Restricted); + + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + let point2 = PointCoordinate::from_f64(5_f64, 5_f64, Some(5_f64)); + let external_user = "external".to_string(); + let external_user2 = "external2".to_string(); + + // Set point as owner + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + delete_point(deps.as_mut(), info.sender.as_ref()).unwrap(); + // This should error + query_point(deps.as_ref()).unwrap_err(); + + // Set point as external user + set_point(deps.as_mut(), &point, &external_user).unwrap(); + delete_point(deps.as_mut(), &external_user).unwrap(); + // This should error + query_point(deps.as_ref()).unwrap_err(); + + // Set point as owner and try to delete as external user + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + // Try to modify it as external user + set_point(deps.as_mut(), &point2, &external_user).unwrap_err(); + // Delete the point as external user, this should error + delete_point(deps.as_mut(), &external_user).unwrap_err(); + + query_point(deps.as_ref()).unwrap(); + + // Set point as external user and try to delete as owner + set_point(deps.as_mut(), &point, info.sender.as_ref()).unwrap(); + // Delete the point as external user, this will success as owner has permission to do anything + delete_point(deps.as_mut(), info.sender.as_ref()).unwrap(); + + query_point(deps.as_ref()).unwrap_err(); + + // Set point as external user 1 and try to delete as external user 2 + set_point(deps.as_mut(), &point, &external_user).unwrap(); + // Delete the point as external user, this will error + delete_point(deps.as_mut(), &external_user2).unwrap_err(); + + query_point(deps.as_ref()).unwrap(); +} + +#[test] +fn test_query_data_owner() { + let (mut deps, _) = proper_initialization(PointRestriction::Restricted); + let external_user = "external".to_string(); + let external_user2 = "external2".to_string(); + let point = PointCoordinate::from_f64(10_f64, 10_f64, Some(10_f64)); + set_point(deps.as_mut(), &point, &external_user.clone()).unwrap(); + + let res: GetDataOwnerResponse = + from_json(query(deps.as_ref(), mock_env(), QueryMsg::GetDataOwner {}).unwrap()).unwrap(); + + assert_eq!( + res, + GetDataOwnerResponse { + owner: AndrAddr::from_string(external_user.clone()) + } + ); + + let res = delete_point(deps.as_mut(), &external_user2).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + delete_point(deps.as_mut(), &external_user).unwrap(); + + query(deps.as_ref(), mock_env(), QueryMsg::GetDataOwner {}).unwrap_err(); +} diff --git a/packages/andromeda-data-storage/src/graph.rs b/packages/andromeda-data-storage/src/graph.rs new file mode 100644 index 000000000..97e7aead5 --- /dev/null +++ b/packages/andromeda-data-storage/src/graph.rs @@ -0,0 +1,92 @@ +use andromeda_std::amp::AndrAddr; +use andromeda_std::{andr_exec, andr_instantiate, andr_query}; +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub map_info: MapInfo, +} + +#[cw_serde] +pub struct MapInfo { + pub map_size: MapSize, + pub allow_negative: bool, + pub map_decimal: u16, +} + +#[cw_serde] +pub struct MapSize { + pub x_width: u64, + pub y_width: u64, + pub z_width: Option, +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + UpdateMap { + map_info: MapInfo, + }, + StoreCoordinate { + coordinate: Coordinate, + is_timestamp_allowed: bool, + }, + StoreUserCoordinate { + user_location_paths: Vec, + }, + DeleteUserCoordinate { + user: AndrAddr, + }, +} + +#[cw_serde] +pub struct Coordinate { + pub x_coordinate: f64, + pub y_coordinate: f64, + pub z_coordinate: Option, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(GetMapInfoResponse)] + GetMapInfo {}, + #[returns(GetMaxPointNumberResponse)] + GetMaxPointNumber {}, + #[returns(GetAllPointsResponse)] + GetAllPoints { + start: Option, + limit: Option, + }, + #[returns(CoordinateInfo)] + GetUserCoordinate { user: AndrAddr }, +} + +#[cw_serde] +pub struct GetMapInfoResponse { + pub map_info: MapInfo, +} + +#[cw_serde] +pub struct GetMaxPointNumberResponse { + pub max_point_number: u128, +} + +#[cw_serde] +pub struct GetAllPointsResponse { + pub points: Vec<(CoordinateInfo, StoredDate)>, +} + +#[cw_serde] +pub struct CoordinateInfo { + pub x: String, + pub y: String, + pub z: Option, +} + +#[cw_serde] +pub struct StoredDate { + pub timestamp: Option, +} diff --git a/packages/andromeda-data-storage/src/lib.rs b/packages/andromeda-data-storage/src/lib.rs index c85c56451..ec92895be 100644 --- a/packages/andromeda-data-storage/src/lib.rs +++ b/packages/andromeda-data-storage/src/lib.rs @@ -1,4 +1,6 @@ pub mod boolean; pub mod counter; +pub mod graph; +pub mod point; pub mod primitive; pub mod string_storage; diff --git a/packages/andromeda-data-storage/src/point.rs b/packages/andromeda-data-storage/src/point.rs new file mode 100644 index 000000000..a1bf68d32 --- /dev/null +++ b/packages/andromeda-data-storage/src/point.rs @@ -0,0 +1,151 @@ +use andromeda_std::{amp::AndrAddr, andr_exec, andr_instantiate, andr_query, error::ContractError}; +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub restriction: PointRestriction, +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + SetPoint { point: PointCoordinate }, + DeletePoint {}, + UpdateRestriction { restriction: PointRestriction }, +} + +#[cw_serde] +pub enum PointRestriction { + Private, + Public, + Restricted, +} + +#[cw_serde] +pub struct PointCoordinate { + pub x_coordinate: String, + pub y_coordinate: String, + pub z_coordinate: Option, +} + +impl PointCoordinate { + pub fn from_f64(x_coordinate: f64, y_coordinate: f64, z_coordinate: Option) -> Self { + let z_coordinate: Option = z_coordinate.map(|z| z.to_string()); + + Self { + x_coordinate: x_coordinate.to_string(), + y_coordinate: y_coordinate.to_string(), + z_coordinate, + } + } + pub fn validate(&self) -> Result<(), ContractError> { + let x_coordinate = self.x_coordinate.parse::(); + if x_coordinate.is_err() { + return Err(ContractError::ParsingError { + err: "x_coordinate: can not parse to f64".to_string(), + }); + } + + let y_coordinate = self.y_coordinate.parse::(); + if y_coordinate.is_err() { + return Err(ContractError::ParsingError { + err: "y_coordinate: can not parse to f64".to_string(), + }); + } + + match &self.z_coordinate { + None => (), + Some(z) => { + let z_coordinate = z.parse::(); + if z_coordinate.is_err() { + return Err(ContractError::ParsingError { + err: "z_coordinate: can not parse to f64".to_string(), + }); + } + } + } + Ok(()) + } +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(PointCoordinate)] + GetPoint {}, + #[returns(GetDataOwnerResponse)] + GetDataOwner {}, +} + +#[cw_serde] +pub struct GetDataOwnerResponse { + pub owner: AndrAddr, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_point_coordinate_valid() { + let point = PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: Some("10".to_string()), + }; + let res = point.validate(); + assert!(res.is_ok()); + + let point = PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: None, + }; + let res = point.validate(); + assert!(res.is_ok()); + } + + #[test] + fn test_validate_point_coordinate_invalid() { + let point = PointCoordinate { + x_coordinate: "10.abc".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: Some("10".to_string()), + }; + let res = point.validate().unwrap_err(); + assert_eq!( + res, + ContractError::ParsingError { + err: "x_coordinate: can not parse to f64".to_string() + } + ); + + let point = PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10.abc".to_string(), + z_coordinate: Some("10".to_string()), + }; + let res = point.validate().unwrap_err(); + assert_eq!( + res, + ContractError::ParsingError { + err: "y_coordinate: can not parse to f64".to_string() + } + ); + + let point = PointCoordinate { + x_coordinate: "10".to_string(), + y_coordinate: "10".to_string(), + z_coordinate: Some("10.xyz".to_string()), + }; + let res = point.validate().unwrap_err(); + assert_eq!( + res, + ContractError::ParsingError { + err: "z_coordinate: can not parse to f64".to_string() + } + ); + } +} diff --git a/packages/std/src/testing/mock_querier.rs b/packages/std/src/testing/mock_querier.rs index 6698e183f..b1684489c 100644 --- a/packages/std/src/testing/mock_querier.rs +++ b/packages/std/src/testing/mock_querier.rs @@ -341,6 +341,7 @@ impl MockAndromedaQuerier { fn handle_adodb_query(&self, msg: &Binary) -> QuerierResult { match from_json(msg).unwrap() { ADODBQueryMsg::ADOType { code_id } => match code_id { + 5 => SystemResult::Ok(ContractResult::Ok(to_json_binary(&"point").unwrap())), 3 => SystemResult::Ok(ContractResult::Ok(to_json_binary(&"app-contract").unwrap())), 1 => SystemResult::Ok(ContractResult::Ok(to_json_binary(&"ADOType").unwrap())), _ => SystemResult::Ok(ContractResult::Err("Invalid Code ID".to_string())), @@ -565,6 +566,8 @@ impl MockAndromedaQuerier { SystemResult::Ok(ContractResult::Ok(to_json_binary("app-contract").unwrap())) } else if key == "1" { SystemResult::Ok(ContractResult::Ok(to_json_binary("ADOType").unwrap())) + } else if key == "5" { + SystemResult::Ok(ContractResult::Ok(to_json_binary("point").unwrap())) } else { SystemResult::Ok(ContractResult::Ok(Binary::default())) }