diff --git a/Cargo.lock b/Cargo.lock index 791c5ec6..62e22b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,12 +214,17 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", + "cw-storage", "cw-storage-plus", + "cw-utils", "cw2", + "form_urlencoded", "logic-bindings", "schemars", "serde", + "serde-json-wasm", "thiserror", + "url", ] [[package]] @@ -409,6 +414,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + [[package]] name = "forward_ref" version = "1.0.0" @@ -473,6 +487,16 @@ dependencies = [ "digest 0.10.5", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "itertools" version = "0.10.5" @@ -521,6 +545,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + [[package]] name = "pkcs8" version = "0.9.0" @@ -800,6 +830,21 @@ dependencies = [ "syn 2.0.3", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typenum" version = "1.15.0" @@ -818,12 +863,38 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 5a97e855..6cef2ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ cosmwasm-std = "1.2.3" cosmwasm-storage = "1.2.2" cw-multi-test = "0.15.1" cw-storage-plus = "0.15.1" +cw-utils = "1.0.1" cw2 = "0.15.1" schemars = "0.8.12" serde = { version = "1.0.158", default-features = false, features = ["derive"] } +serde-json-wasm = "0.5.0" thiserror = { version = "1.0.40" } diff --git a/contracts/cw-law-stone/Cargo.toml b/contracts/cw-law-stone/Cargo.toml index cea27186..d25eece2 100644 --- a/contracts/cw-law-stone/Cargo.toml +++ b/contracts/cw-law-stone/Cargo.toml @@ -30,12 +30,17 @@ rpath = false cosmwasm-schema.workspace = true cosmwasm-std.workspace = true cosmwasm-storage.workspace = true +cw-storage = { path = "../cw-storage" } cw-storage-plus.workspace = true +cw-utils.worksapce = true cw2.workspace = true +form_urlencoded = "1.1.0" logic-bindings = { version = "0.2", path = "../../packages/logic-bindings" } schemars.workspace = true +serde-json-wasm.workspace = true serde.workspace = true thiserror.workspace = true +url = "2.3.1" [dev-dependencies] cw-multi-test.workspace = true diff --git a/contracts/cw-law-stone/src/contract.rs b/contracts/cw-law-stone/src/contract.rs index a00cb966..900c3df2 100644 --- a/contracts/cw-law-stone/src/contract.rs +++ b/contracts/cw-law-stone/src/contract.rs @@ -1,26 +1,50 @@ use crate::ContractError::NotImplemented; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult}; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, + SubMsg, WasmMsg, +}; use cw2::set_contract_version; +use cw_storage::msg::ExecuteMsg as StorageMsg; +use logic_bindings::LogicCustomQuery; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::INSTANTIATE_CONTEXT; // version info for migration info const CONTRACT_NAME: &str = "crates.io:law-stone"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const STORE_PROGRAM_REPLY_ID: u64 = 1; + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut<'_>, + deps: DepsMut<'_, LogicCustomQuery>, _env: Env, _info: MessageInfo, - _msg: InstantiateMsg, + msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - Err(NotImplemented {}) + let store_msg = StorageMsg::StoreObject { + data: msg.program.clone(), + pin: true, + }; + + let store_program_msg = WasmMsg::Execute { + contract_addr: msg.storage_address.clone(), + msg: to_binary(&store_msg)?, + funds: vec![], + }; + + INSTANTIATE_CONTEXT.save(deps.storage, &msg.storage_address)?; + + Ok(Response::new().add_submessage(SubMsg::reply_on_success( + store_program_msg, + STORE_PROGRAM_REPLY_ID, + ))) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -37,3 +61,372 @@ pub fn execute( pub fn query(_deps: Deps<'_>, _env: Env, _msg: QueryMsg) -> StdResult { Err(StdError::generic_err("Not implemented")) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + deps: DepsMut<'_, LogicCustomQuery>, + env: Env, + msg: Reply, +) -> Result { + match msg.id { + STORE_PROGRAM_REPLY_ID => reply::store_program_reply(deps, env, msg), + _ => Err(StdError::generic_err("Not implemented").into()), + } +} + +pub mod reply { + use super::*; + use crate::helper::{ask_response_to_objects, get_reply_event_attribute}; + use crate::state::{Object, DEPENDENCIES, PROGRAM}; + use url::Url; + + pub fn store_program_reply( + deps: DepsMut<'_, LogicCustomQuery>, + _env: Env, + msg: Reply, + ) -> Result { + let context = INSTANTIATE_CONTEXT.load(deps.storage)?; + + msg.result + .into_result() + .map_err(|_| { + ContractError::InvalidReplyMsg(StdError::generic_err("no message in reply")) + }) + .and_then(|e| { + get_reply_event_attribute(e.events, "id".to_string()).ok_or_else(|| { + ContractError::InvalidReplyMsg(StdError::generic_err( + "reply event doesn't contains object id", + )) + }) + }) + .map(|obj_id| Object { + object_id: obj_id, + storage_address: context.clone(), + }) + .and_then(|program| -> Result, ContractError> { + PROGRAM + .save(deps.storage, &program) + .map_err(ContractError::from)?; + + // Clean instantiate context + INSTANTIATE_CONTEXT.remove(deps.storage); + + let req = build_source_files_query(program.clone())?.into(); + let res = deps.querier.query(&req).map_err(ContractError::from)?; + + let objects = ask_response_to_objects(res, "Files".to_string())?; + let mut msgs = Vec::with_capacity(objects.len()); + for obj in objects { + if obj.object_id == program.object_id { + continue; + } + DEPENDENCIES.save(deps.storage, obj.object_id.as_str(), &obj)?; + + msgs.push(SubMsg::new(WasmMsg::Execute { + msg: to_binary(&StorageMsg::PinObject { + id: obj.clone().object_id, + })?, + contract_addr: obj.clone().storage_address, + funds: vec![], + })); + } + + Ok(msgs) + }) + .map(|msg| Response::new().add_submessages(msg)) + } + + pub fn build_source_files_query(program: Object) -> Result { + let program_uri: Url = program.try_into()?; + + Ok(LogicCustomQuery::Ask { + program: "source_files(Files) :- bagof(File, source_file(File), Files).".to_string(), + query: [ + "consult('", + program_uri.as_str(), + "'), source_files(Files).", + ] + .join(""), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::{Object, DEPENDENCIES, PROGRAM}; + use cosmwasm_std::testing::{mock_env, mock_info, MockQuerierCustomHandlerResult}; + use cosmwasm_std::{ + from_binary, to_binary, CosmosMsg, Event, Order, SubMsgResponse, SubMsgResult, SystemError, + SystemResult, + }; + use logic_bindings::testing::mock::mock_dependencies_with_logic_handler; + use logic_bindings::{ + Answer, AskResponse, LogicCustomQuery, Result as LogicResult, Substitution, Term, + }; + use url::Url; + + fn custom_logic_handler_with_dependencies( + dependencies: Vec, + program: Object, + request: &LogicCustomQuery, + ) -> MockQuerierCustomHandlerResult { + let program_uri: Url = program.clone().try_into().unwrap(); + let mut updated_deps = dependencies; + updated_deps.push(program_uri.to_string()); + let deps_name = format!("[{}]", &updated_deps.join(",")); + let LogicCustomQuery::Ask { + program: exp_program, + query: exp_query, + .. + } = reply::build_source_files_query(program).unwrap(); + match request { + LogicCustomQuery::Ask { program, query } + if *query == exp_query && *program == exp_program => + { + SystemResult::Ok( + to_binary(&AskResponse { + height: 1, + gas_used: 1000, + answer: Some(Answer { + success: true, + has_more: false, + variables: vec!["Files".to_string()], + results: vec![LogicResult { + substitutions: vec![Substitution { + variable: "Files".to_string(), + term: Term { + name: deps_name, + arguments: vec![], + }, + }], + }], + }), + }) + .into(), + ) + } + _ => SystemResult::Err(SystemError::InvalidRequest { + error: "Ask `souces_files(Files).` predicate not called".to_string(), + request: Default::default(), + }), + } + } + + #[test] + fn proper_initialization() { + let mut deps = + mock_dependencies_with_logic_handler(|_| SystemResult::Err(SystemError::Unknown {})); + let program = to_binary("foo(_) :- true.").unwrap(); + + let msg = InstantiateMsg { + program: program.clone(), + storage_address: "okp41ffzp0xmjhwkltuxcvccl0z9tyfuu7txp5ke0tpkcjpzuq9fcj3pqrteqt3" + .to_string(), + }; + let info = mock_info("creator", &[]); + + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Check if a message is send to the cw-storage to store the logic program. + assert_eq!(1, res.messages.len()); + let sub_msg = res.messages.first().unwrap(); + assert_eq!(STORE_PROGRAM_REPLY_ID, sub_msg.id); + match &sub_msg.msg { + CosmosMsg::Wasm(wasm_msg) => match wasm_msg { + WasmMsg::Execute { msg, .. } => { + let result: StorageMsg = from_binary(msg).unwrap(); + match result { + StorageMsg::StoreObject { data, pin } => { + assert_eq!(data, program); + assert!(pin, "the main program should be pinned"); + } + _ => panic!("storage message should be a StoreObject message"), + } + } + _ => panic!("wasm message should be a Storage message"), + }, + _ => panic!("cosmos sub message should be a Wasm message execute"), + } + assert_eq!( + "okp41ffzp0xmjhwkltuxcvccl0z9tyfuu7txp5ke0tpkcjpzuq9fcj3pqrteqt3".to_string(), + INSTANTIATE_CONTEXT.load(&deps.storage).unwrap() + ); + } + + #[derive(Clone)] + struct StoreTestCase { + dependencies: Vec<(String, String, String)>, // URI, contract address, object id + object_id: String, + } + + #[test] + fn store_program_reply() { + let cases = vec![ + StoreTestCase { + dependencies: vec![ + ( + "cosmwasm:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + "okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s".to_string(), + "4cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05".to_string() + ), + ], + object_id: "0689c526187c6785dfcce28f8df19138da292598dc19548a852de1792062f271" + .to_string(), + }, + StoreTestCase { + dependencies: vec![], + object_id: "4cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05" + .to_string(), + }, + StoreTestCase { + dependencies: vec![ + ( + "cosmwasm:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + "okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s".to_string(), // contract addr + "4cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05".to_string() // object id + ), + ( + "cosmwasm:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%220689c526187c6785dfcce28f8df19138da292598dc19548a852de1792062f271%22%7D%7D".to_string(), + "okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s".to_string(), // contract addr + "0689c526187c6785dfcce28f8df19138da292598dc19548a852de1792062f271".to_string() // object id + ), + ], + object_id: "1cc6de7672c97db145a3940df2264140ea893c6688fa5ca55b73cb8b68e0574d" + .to_string(), + }, + ]; + + for case in cases { + let uris = Box::new( + case.dependencies + .clone() + .into_iter() + .map(|(uri, _, _)| uri) + .collect::>(), + ); + let program_object_id = case.clone().object_id; + let mut deps = mock_dependencies_with_logic_handler(move |request| { + custom_logic_handler_with_dependencies( + uris.to_vec(), + Object { + object_id: program_object_id.clone(), + storage_address: + "okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s" + .to_string(), + }, + request, + ) + }); + + let reply = Reply { + id: STORE_PROGRAM_REPLY_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![Event::new("e".to_string()) + .add_attribute("id".to_string(), case.clone().object_id)], + data: None, + }), + }; + + // Configure the instantiate context + INSTANTIATE_CONTEXT + .save( + deps.as_mut().storage, + &"okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s".to_string(), + ) + .unwrap(); + + let response = reply::store_program_reply(deps.as_mut(), mock_env(), reply); + let res = response.unwrap(); + + let program = PROGRAM.load(&deps.storage).unwrap(); + assert_eq!(case.clone().object_id, program.object_id); + + let deps_len_requirement = case.clone().dependencies.len(); + + if deps_len_requirement > 0 { + assert_eq!( + deps_len_requirement, + DEPENDENCIES + .keys_raw(&deps.storage, None, None, Order::Ascending) + .count() + ); + for (_, contract_addr, object_id) in case.clone().dependencies { + let o = DEPENDENCIES.load(&deps.storage, object_id.as_str()); + assert!( + o.is_ok(), + "dependencies should contains each object id dependencies as key" + ); + let o = o.unwrap(); + assert_eq!( + o.object_id, object_id, + "dependencies should contains each object id dependencies as key" + ); + assert_eq!( + o.storage_address, contract_addr, + "dependencies should contains each object id dependencies as key" + ); + } + } + + assert_eq!( + deps_len_requirement, + res.messages.len(), + "response should contains any sub message as dependencies" + ); + + let objects_pinned: Vec = res + .messages + .into_iter() + .flat_map(|sub_msg| -> Option { + match &sub_msg.msg { + CosmosMsg::Wasm(wasm_msg) => match wasm_msg { + WasmMsg::Execute { msg, .. } => { + let result: StorageMsg = from_binary(msg).unwrap(); + match result { + StorageMsg::PinObject { id } => Some(id), + _ => panic!("should contains only PinObject message(s)"), + } + } + _ => panic!("wasm message should be a Storage message"), + }, + _ => panic!("cosmos sub message should be a Wasm message execute"), + } + }) + .collect(); + + for object in objects_pinned { + assert!( + DEPENDENCIES.has(&deps.storage, object.as_str()), + "each dependencies should be pinned by a PinObject message" + ) + } + + assert!( + INSTANTIATE_CONTEXT.load(&deps.storage).is_err(), + "the instantiate context should be cleaned at the end" + ) + } + } + + #[test] + fn build_source_files_query() { + let result = reply::build_source_files_query(Object { + object_id: "1cc6de7672c97db145a3940df2264140ea893c6688fa5ca55b73cb8b68e0574d" + .to_string(), + storage_address: "okp41ffzp0xmjhwkltuxcvccl0z9tyfuu7txp5ke0tpkcjpzuq9fcj3pqrteqt3" + .to_string(), + }); + + match result { + Ok(LogicCustomQuery::Ask { program, query }) => { + assert_eq!( + program, + "source_files(Files) :- bagof(File, source_file(File), Files)." + ); + assert_eq!(query, "consult('cosmwasm:cw-storage:okp41ffzp0xmjhwkltuxcvccl0z9tyfuu7txp5ke0tpkcjpzuq9fcj3pqrteqt3?query=%7B%22object_data%22%3A%7B%22id%22%3A%221cc6de7672c97db145a3940df2264140ea893c6688fa5ca55b73cb8b68e0574d%22%7D%7D'), source_files(Files).") + } + _ => panic!("Expected Ok(LogicCustomQuery)."), + } + } +} diff --git a/contracts/cw-law-stone/src/error.rs b/contracts/cw-law-stone/src/error.rs index 1f8c2bd4..2e6bc858 100644 --- a/contracts/cw-law-stone/src/error.rs +++ b/contracts/cw-law-stone/src/error.rs @@ -1,5 +1,8 @@ use cosmwasm_std::StdError; +use cw_utils::ParseReplyError; +use serde_json_wasm::de::Error; use thiserror::Error; +use url::ParseError; #[derive(Error, Debug)] pub enum ContractError { @@ -8,4 +11,39 @@ pub enum ContractError { #[error("Not implemented")] NotImplemented {}, + + #[error("{0}")] + Parse(#[from] ParseReplyError), + + #[error("Invalid reply message: {0}")] + InvalidReplyMsg(StdError), + + #[error("Failed parse dependency uri {uri:?}: {error:?}")] + LogicLoadUri { error: UriError, uri: String }, +} + +impl ContractError { + pub fn dependency_uri(error: UriError, uri: String) -> ContractError { + ContractError::LogicLoadUri { error, uri } + } +} +#[derive(Error, Debug)] +pub enum UriError { + #[error("{0}")] + Parse(#[from] ParseError), + + #[error("Incompatible uri scheme {scheme:?}. Should be {wanted:?}")] + WrongScheme { scheme: String, wanted: Vec }, + + #[error("The given path doesn't correspond to a cw-storage uri")] + IncompatiblePath, + + #[error("URI doesn't contains needed query key")] + MissingQueryKey, + + #[error("{0}")] + JSONDecoding(#[from] Error), + + #[error("The given query is not compatible")] + IncompatibleQuery, } diff --git a/contracts/cw-law-stone/src/helper.rs b/contracts/cw-law-stone/src/helper.rs new file mode 100644 index 00000000..0ca3ca25 --- /dev/null +++ b/contracts/cw-law-stone/src/helper.rs @@ -0,0 +1,51 @@ +use crate::state::Object; +use crate::ContractError; +use cosmwasm_std::Event; +use logic_bindings::{AskResponse, Substitution}; +use url::Url; + +pub fn get_reply_event_attribute(events: Vec, key: String) -> Option { + return events + .iter() + .flat_map(|e| e.attributes.clone()) + .filter(|a| a.key == key) + .map(|a| a.value) + .next(); +} + +/// Files terms is List atom, List is represented as String in prolog, filter to remove +/// all paterm to represent the list and return the result as Vec. +fn filter_source_files(substitution: Substitution) -> Vec { + substitution + .term + .name + .split(',') + .into_iter() + .map(|s| s.replace(['\'', '[', ']'], "")) + .collect::>() +} + +pub fn ask_response_to_objects( + res: AskResponse, + variable: String, +) -> Result, ContractError> { + let uris = res + .answer + .map(|a| a.results) + .unwrap_or_default() + .iter() + .flat_map(|result| result.substitutions.clone()) + .filter(|s| s.variable == variable) + .flat_map(filter_source_files) + .collect::>(); + + let mut objects = vec![]; + for uri in uris { + let url = Url::parse(uri.as_str()) + .map_err(|e| ContractError::dependency_uri(e.into(), uri.clone()))?; + let object = Object::try_from(url).map_err(|e| ContractError::dependency_uri(e, uri))?; + + objects.push(object) + } + Ok(objects) +} diff --git a/contracts/cw-law-stone/src/lib.rs b/contracts/cw-law-stone/src/lib.rs index fddbd06d..c4c88f15 100644 --- a/contracts/cw-law-stone/src/lib.rs +++ b/contracts/cw-law-stone/src/lib.rs @@ -12,6 +12,8 @@ pub mod contract; mod error; +mod helper; pub mod msg; +pub mod state; pub use crate::error::ContractError; diff --git a/contracts/cw-law-stone/src/state.rs b/contracts/cw-law-stone/src/state.rs new file mode 100644 index 00000000..8e8bf066 --- /dev/null +++ b/contracts/cw-law-stone/src/state.rs @@ -0,0 +1,167 @@ +use cosmwasm_std::StdError; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::error::UriError; +use crate::ContractError; +use cw_storage::msg::QueryMsg as StorageQuery; +use cw_storage::msg::QueryMsg; +use cw_storage_plus::{Item, Map}; +use url::Url; + +/// State to store context during contract instantiation +pub const INSTANTIATE_CONTEXT: Item<'_, String> = Item::new("instantiate"); + +/// Represent a link to an Object stored in the `cw-storage` contract. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Object { + /// The object id in the `cw-storage` contract. + pub object_id: String, + + /// The `cw-storage` contract address on which the object is stored. + pub storage_address: String, +} + +impl Object { + const COSMWASM_SCHEME: &'static str = "cosmwasm"; +} + +impl TryFrom for Object { + type Error = UriError; + + fn try_from(value: Url) -> Result { + if value.scheme() != Object::COSMWASM_SCHEME { + return Err(UriError::WrongScheme { + scheme: value.scheme().to_string(), + wanted: vec![Object::COSMWASM_SCHEME.to_string()], + }); + } + + let path = value.path().to_string(); + let paths = path.split(':').collect::>(); + if paths.is_empty() || paths.len() > 2 { + return Err(UriError::IncompatiblePath); + } + let storage_address = paths.last().ok_or(UriError::IncompatiblePath)?.to_string(); + + let queries = value + .query_pairs() + .into_owned() + .collect::>(); + + if let Some(query) = queries.get("query") { + let json: QueryMsg = serde_json_wasm::from_str(query.as_str())?; + + return match json { + QueryMsg::ObjectData { id: object_id } => Ok(Object { + object_id, + storage_address, + }), + _ => Err(UriError::IncompatibleQuery), + }; + } + + Err(UriError::MissingQueryKey) + } +} + +impl TryInto for Object { + type Error = ContractError; + + fn try_into(self) -> Result { + let raw = [ + Object::COSMWASM_SCHEME, + ":cw-storage:", + self.storage_address.as_str(), + "?", + form_urlencoded::Serializer::new(String::new()) + .append_pair( + "query", + serde_json_wasm::to_string(&StorageQuery::ObjectData { id: self.object_id }) + .map_err(|e| { + ContractError::Std(StdError::serialize_err("StorageQuery", e)) + })? + .as_str(), + ) + .finish() + .as_str(), + ] + .join(""); + + Url::parse(&raw).map_err(|e| ContractError::LogicLoadUri { + uri: raw, + error: UriError::Parse(e), + }) + } +} + +pub const PROGRAM: Item<'_, Object> = Item::new("program"); + +pub const DEPENDENCIES: Map<'_, &str, Object> = Map::new("dependencies"); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn object_try_from() { + let cases = vec![ + ( + "coco:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + Some(UriError::WrongScheme { scheme: "coco".to_string(), wanted: vec!["cosmwasm".to_string()] }), + None + ), + ( + "cosmwasm:bob:alice:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + Some(UriError::IncompatiblePath), + None + ), + ( + "cosmwasm:cw-storage:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?q=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + Some(UriError::MissingQueryKey), + None + ), + ( + "cosmwasm:cw-storage:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + Some(UriError::IncompatibleQuery), + None + ), + ( + "cosmwasm:cw-storage:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + None, + Some( + Object { + object_id: "4cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05".to_string(), + storage_address: "okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s".to_string(), + } + ) + ), + ( + "cosmwasm:okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s?query=%7B%22object_data%22%3A%7B%22id%22%3A%20%224cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05%22%7D%7D".to_string(), + None, + Some( + Object { + object_id: "4cbe36399aabfcc7158ee7a66cbfffa525bb0ceab33d1ff2cff08759fe0a9b05".to_string(), + storage_address: "okp41dclchlcttf2uektxyryg0c6yau63eml5q9uq03myg44ml8cxpxnqavca4s".to_string(), + } + ) + ), + ]; + + for case in cases { + match Url::parse(case.0.as_str()) { + Ok(url) => { + let result = Object::try_from(url); + + if let Some(err) = case.1 { + assert_eq!(err.to_string(), result.unwrap_err().to_string()) + } else if let Some(o) = case.2 { + assert_eq!(o, result.unwrap()) + } + } + Err(_) => panic!("no error should be thrown"), + } + } + } +} diff --git a/packages/logic-bindings/src/testing/mock.rs b/packages/logic-bindings/src/testing/mock.rs index d38ca6d5..6235e811 100644 --- a/packages/logic-bindings/src/testing/mock.rs +++ b/packages/logic-bindings/src/testing/mock.rs @@ -13,6 +13,20 @@ pub fn mock_dependencies_with_logic_and_balance( mock_dependencies_with_logic_and_balances(&[(MOCK_CONTRACT_ADDR, contract_balance)]) } +pub fn mock_dependencies_with_logic_handler( + handler: LH, +) -> OwnedDeps, LogicCustomQuery> +where + LH: Fn(&LogicCustomQuery) -> QuerierResult, +{ + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockLogicQuerier::new(LogicQuerier::new(Box::new(handler)), &[]), + custom_query_type: PhantomData, + } +} + /// Initializes the querier along with the mock_dependencies. /// /// Set the logic querier mock handler.