diff --git a/ownables/dossier/examples/schema.rs b/ownables/dossier/examples/schema.rs index c3fa676..bb74faa 100644 --- a/ownables/dossier/examples/schema.rs +++ b/ownables/dossier/examples/schema.rs @@ -3,9 +3,9 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use ownable_static::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use ownable_static::msg::{EncodePublicEventMsg, ExecuteMsg, IngestEventMsg, InstantiateMsg, QueryMsg, RegisterPublicEventMsg}; use ownable_static::state::{Config}; -use ownable_std::{ExternalEventMsg, InfoResponse, Metadata}; +use ownable_std::{InfoResponse, Metadata}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -16,7 +16,9 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); export_schema(&schema_for!(QueryMsg), &out_dir); - export_schema(&schema_for!(ExternalEventMsg), &out_dir); + export_schema(&schema_for!(RegisterPublicEventMsg), &out_dir); + export_schema(&schema_for!(IngestEventMsg), &out_dir); + export_schema(&schema_for!(EncodePublicEventMsg), &out_dir); export_schema(&schema_for!(InfoResponse), &out_dir); export_schema(&schema_for!(Metadata), &out_dir); export_schema(&schema_for!(Config), &out_dir); diff --git a/ownables/dossier/src/contract.rs b/ownables/dossier/src/contract.rs index 2541ee2..7e256c2 100644 --- a/ownables/dossier/src/contract.rs +++ b/ownables/dossier/src/contract.rs @@ -1,11 +1,12 @@ use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::msg::{ExecuteMsg, IngestEventMsg, InstantiateMsg, QueryMsg, RegisterPublicEventMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cosmwasm_std::{Binary, to_json_binary}; use cw2::set_contract_version; use crate::state::{NFT_ITEM, CONFIG, METADATA, LOCKED, PACKAGE_CID, OWNABLE_INFO, NETWORK_ID}; -use ownable_std::{ExternalEventMsg, InfoResponse, Metadata, OwnableInfo}; +use ownable_std::{InfoResponse, Metadata, OwnableInfo}; +use serde_json::Value; // version info for migration info const CONTRACT_NAME: &str = "crates.io:ownable-static"; @@ -116,30 +117,28 @@ pub fn try_transfer(info: MessageInfo, deps: DepsMut, to: Addr) -> Result Result { - let mut response = Response::new() - .add_attribute("method", "register_external_event"); + let mut response = Response::new().add_attribute("method", "register"); match event.event_type.as_str() { "lock" => { - try_register_lock( - info, - deps, - event, - )?; + try_register_lock(info, deps, event)?; response = response.add_attribute("event_type", "lock"); - }, + } _ => return Err(ContractError::MatchEventError { val: event.event_type }), }; Ok(response) } +pub fn ingest(_info: MessageInfo, _deps: DepsMut, _event: IngestEventMsg) -> Result { + Err(ContractError::NotImplemented {}) +} + fn try_release(_info: MessageInfo, deps: DepsMut, to: Addr) -> Result { let mut is_locked = LOCKED.load(deps.storage)?; if !is_locked { @@ -164,19 +163,28 @@ fn try_release(_info: MessageInfo, deps: DepsMut, to: Addr) -> Result Result { - let owner = event.attributes.get("owner") - .cloned() - .unwrap_or_default(); - let nft_id = event.attributes.get("token_id") - .cloned() - .unwrap_or_default(); - let contract_addr = event.attributes.get("contract") - .cloned() + let payload: Value = ciborium::de::from_reader(event.data.as_slice()) + .map_err(|_| ContractError::InvalidExternalEventArgs {})?; + let owner = payload.get("owner").and_then(Value::as_str).unwrap_or_default(); + let nft_id = payload + .get("token_id") + .or_else(|| payload.get("tokenId")) + .and_then(|value| { + value.as_str().map(ToString::to_string).or_else(|| { + if value.is_number() { + Some(value.to_string()) + } else { + None + } + }) + }) .unwrap_or_default(); + let contract_addr = payload.get("contract").and_then(Value::as_str).unwrap_or_default(); + let event_network = payload.get("network").and_then(Value::as_str).unwrap_or_default(); - if owner.is_empty() || nft_id.is_empty() || contract_addr.is_empty() { + if owner.is_empty() || nft_id.is_empty() || contract_addr.is_empty() || event_network.is_empty() { return Err(ContractError::InvalidExternalEventArgs {}); } @@ -191,10 +199,7 @@ fn try_register_lock( }); } - let event_network = event.network.unwrap_or("".to_string()); - if event_network == "" { - return Err(ContractError::MatchChainIdError { val: "No network".to_string() }) - } else if event_network != nft.network { + if event_network != nft.network { return Err(ContractError::LockError { val: "network mismatch".to_string() }); @@ -215,7 +220,7 @@ fn try_register_lock( Ok(try_release(info, deps, address)?) } - _ => return Err(ContractError::MatchChainIdError { val: event_network }), + _ => return Err(ContractError::MatchChainIdError { val: event_network.to_string() }), } } diff --git a/ownables/dossier/src/lib.rs b/ownables/dossier/src/lib.rs index 49f526b..2616a89 100644 --- a/ownables/dossier/src/lib.rs +++ b/ownables/dossier/src/lib.rs @@ -4,10 +4,10 @@ use std::fmt::Display; use std::panic::UnwindSafe; use cosmwasm_std::{MessageInfo, Response}; -use ownable_std::{create_lto_env, ExternalEventMsg, IdbStateDump, load_lto_deps}; +use ownable_std::{create_lto_env, IdbStateDump, load_lto_deps}; use serde::{Deserialize, Serialize}; -use msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use msg::{ExecuteMsg, IngestEventMsg, InstantiateMsg, QueryMsg, RegisterPublicEventMsg}; pub mod contract; pub mod error; @@ -34,13 +34,27 @@ struct AbiQueryRequest { } #[derive(Serialize, Deserialize)] -struct AbiExternalEventRequest { - msg: ExternalEventMsg, +struct AbiRegisterRequest { + msg: RegisterPublicEventMsg, info: MessageInfo, - ownable_id: String, mem: IdbStateDump, } +#[derive(Serialize, Deserialize)] +struct AbiIngestRequest { + msg: IngestEventMsg, + info: MessageInfo, + mem: IdbStateDump, +} + +#[derive(Serialize, Deserialize)] +struct AbiEncodePublicEventRequest { + #[serde(rename = "eventType")] + event_type: String, + #[serde(with = "serde_bytes")] + data: Vec, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] struct HostAbiError { code: Option, @@ -321,17 +335,12 @@ fn query_handler(input: &[u8]) -> Result, HostAbiError> { cbor_to_vec(&payload) } -fn external_event_handler(input: &[u8]) -> Result, HostAbiError> { - let request: AbiExternalEventRequest = cbor_from_slice(input)?; +fn register_handler(input: &[u8]) -> Result, HostAbiError> { + let request: AbiRegisterRequest = cbor_from_slice(input)?; let mut deps = load_lto_deps(Some(request.mem)); - let response = contract::register_external_event( - request.info, - deps.as_mut(), - request.msg, - request.ownable_id, - ) - .map_err(HostAbiError::from_display)?; + let response = + contract::register(request.info, deps.as_mut(), request.msg).map_err(HostAbiError::from_display)?; let payload = AbiResultPayload { result: cbor_to_vec(&AbiResponse::from(response))?, @@ -341,6 +350,39 @@ fn external_event_handler(input: &[u8]) -> Result, HostAbiError> { cbor_to_vec(&payload) } +fn ingest_handler(input: &[u8]) -> Result, HostAbiError> { + let request: AbiIngestRequest = cbor_from_slice(input)?; + let mut deps = load_lto_deps(Some(request.mem)); + + let response = + contract::ingest(request.info, deps.as_mut(), request.msg).map_err(HostAbiError::from_display)?; + + let payload = AbiResultPayload { + result: cbor_to_vec(&AbiResponse::from(response))?, + mem: Some(IdbStateDump::from(deps.storage)), + }; + + cbor_to_vec(&payload) +} + +fn encode_public_event_handler(input: &[u8]) -> Result, HostAbiError> { + let request: AbiEncodePublicEventRequest = cbor_from_slice(input)?; + + if request.event_type.is_empty() { + return Err(HostAbiError::with_code( + "INVALID_EVENT_TYPE", + "eventType must not be empty", + )); + } + + let payload = AbiResultPayload { + result: request.data, + mem: None, + }; + + cbor_to_vec(&payload) +} + #[no_mangle] pub extern "C" fn ownable_alloc(len: u32) -> u32 { alloc(len) @@ -367,11 +409,212 @@ pub extern "C" fn ownable_query(ptr: u32, len: u32) -> u64 { } #[no_mangle] -pub extern "C" fn ownable_external_event(ptr: u32, len: u32) -> u64 { - dispatch(ptr, len, external_event_handler) +pub extern "C" fn ownable_register(ptr: u32, len: u32) -> u64 { + dispatch(ptr, len, register_handler) +} + +#[no_mangle] +pub extern "C" fn ownable_ingest(ptr: u32, len: u32) -> u64 { + dispatch(ptr, len, ingest_handler) +} + +#[no_mangle] +pub extern "C" fn ownable_encode_public_event(ptr: u32, len: u32) -> u64 { + dispatch(ptr, len, encode_public_event_handler) } #[allow(dead_code)] fn _round_trip_ptr_len_for_tests(packed: u64) -> (u32, u32) { unpack_ptr_len(packed) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::msg::{EncodePublicEventMsg, ExecuteMsg, IngestEventMsg, OwnableEventSource, RegisterPublicEventMsg}; + use cosmwasm_std::{Addr, Uint128}; + use ownable_std::NFT; + use std::collections::HashMap; + use serde_json::json; + + fn sample_mem() -> IdbStateDump { + IdbStateDump { + state_dump: HashMap::new(), + } + } + + fn sample_info() -> MessageInfo { + MessageInfo { + sender: Addr::unchecked("owner"), + funds: Vec::new(), + } + } + + fn decode_payload(bytes: Vec) -> AbiResultPayload { + cbor_from_slice(&bytes).expect("decode payload") + } + + fn instantiate_mem() -> IdbStateDump { + let request = AbiInstantiateRequest { + msg: InstantiateMsg { + name: "Dossier".to_string(), + description: "Dossier ownable".to_string(), + ownable_id: "dossier-1".to_string(), + ownable_type: Some("dossier".to_string()), + network_id: 1, + package: "bafy-package".to_string(), + nft: Some(NFT { + network: "eip155:1".to_string(), + id: Uint128::new(1), + address: "nft-contract-address".to_string(), + lock_service: None, + }), + }, + info: sample_info(), + }; + + let out = instantiate_handler(&cbor_to_vec(&request).expect("encode instantiate request")) + .expect("instantiate handler succeeds"); + decode_payload(out).mem.expect("instantiate returns memory") + } + + fn locked_mem(mem: IdbStateDump) -> IdbStateDump { + let request = AbiExecuteRequest { + msg: ExecuteMsg::Lock {}, + info: sample_info(), + mem, + }; + + let out = execute_handler(&cbor_to_vec(&request).expect("encode execute request")) + .expect("execute lock succeeds"); + decode_payload(out).mem.expect("execute returns memory") + } + + #[test] + fn register_handler_rejects_invalid_lock_payload() { + let request = AbiRegisterRequest { + msg: RegisterPublicEventMsg { + source: "0xsource".to_string(), + event_type: "lock".to_string(), + data: cbor_to_vec(&json!({"owner":"owner"})).expect("encode payload"), + block_number: 1, + transaction_hash: vec![0xaa, 0xbb], + transaction_index: 0, + log_index: 0, + }, + info: sample_info(), + mem: sample_mem(), + }; + + let err = register_handler(&cbor_to_vec(&request).expect("encode register request")) + .expect_err("register handler should fail"); + assert!(err.message.contains("Invalid external event args")); + } + + #[test] + fn register_handler_accepts_valid_lock_payload_and_unlocks_state() { + let request = AbiRegisterRequest { + msg: RegisterPublicEventMsg { + source: "0xsource".to_string(), + event_type: "lock".to_string(), + data: cbor_to_vec(&json!({ + "owner": "owner", + "tokenId": "1", + "contract": "nft-contract-address", + "network": "eip155:1" + })) + .expect("encode lock payload"), + block_number: 1, + transaction_hash: vec![0xaa, 0xbb], + transaction_index: 0, + log_index: 0, + }, + info: sample_info(), + mem: locked_mem(instantiate_mem()), + }; + + let out = register_handler(&cbor_to_vec(&request).expect("encode register request")) + .expect("register handler succeeds"); + let payload = decode_payload(out); + let mem = payload.mem.expect("register returns memory"); + let response: AbiResponse = cbor_from_slice(&payload.result).expect("decode response"); + + assert!(response + .attributes + .iter() + .any(|attr| attr.key == "method" && attr.value == "register")); + assert!(response + .attributes + .iter() + .any(|attr| attr.key == "event_type" && attr.value == "lock")); + + let query = AbiQueryRequest { + msg: QueryMsg::IsLocked {}, + mem, + }; + let out = query_handler(&cbor_to_vec(&query).expect("encode query request")) + .expect("query handler succeeds"); + let payload = decode_payload(out); + let is_locked: bool = serde_json::from_slice(&payload.result).expect("decode lock state"); + assert!(!is_locked); + } + + #[test] + fn encode_public_event_handler_echoes_payload_bytes() { + let request = AbiEncodePublicEventRequest { + event_type: "lock".to_string(), + data: vec![1, 2, 3, 4], + }; + + let out = encode_public_event_handler(&cbor_to_vec(&request).expect("encode request")) + .expect("encode handler succeeds"); + let payload = decode_payload(out); + assert_eq!(payload.result, vec![1, 2, 3, 4]); + assert!(payload.mem.is_none()); + } + + #[test] + fn encode_public_event_handler_rejects_empty_event_type() { + let request = AbiEncodePublicEventRequest { + event_type: String::new(), + data: vec![1], + }; + + let err = encode_public_event_handler(&cbor_to_vec(&request).expect("encode request")) + .expect_err("empty type should fail"); + assert_eq!(err.code.as_deref(), Some("INVALID_EVENT_TYPE")); + } + + #[test] + fn ingest_handler_returns_not_implemented() { + let request = AbiIngestRequest { + msg: IngestEventMsg { + source: OwnableEventSource { + id: "upstream-ownable".to_string(), + owner: "owner".to_string(), + issuer: "issuer".to_string(), + }, + event_type: "consume".to_string(), + attributes: json!({"amount": 1}), + }, + info: sample_info(), + mem: sample_mem(), + }; + + let err = ingest_handler(&cbor_to_vec(&request).expect("encode ingest request")) + .expect_err("ingest should not be implemented"); + assert!(err.message.contains("Method is not implemented")); + } + + #[test] + fn encode_public_event_msg_uses_camel_case() { + let msg = EncodePublicEventMsg { + event_type: "lock".to_string(), + data: vec![0xde, 0xad], + }; + + let value = serde_json::to_value(&msg).expect("serialize encode message"); + assert!(value.get("eventType").is_some()); + assert!(value.get("event_type").is_none()); + } +} diff --git a/ownables/dossier/src/msg.rs b/ownables/dossier/src/msg.rs index 3847746..4d9102f 100644 --- a/ownables/dossier/src/msg.rs +++ b/ownables/dossier/src/msg.rs @@ -1,5 +1,6 @@ -use cosmwasm_std::{Addr}; +use cosmwasm_std::Addr; use schemars::JsonSchema; +use serde_json::Value; use serde::{Deserialize, Serialize}; use ownable_std_macros::{ ownables_transfer, ownables_lock, @@ -29,3 +30,37 @@ pub enum ExecuteMsg {} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct RegisterPublicEventMsg { + pub source: String, + pub event_type: String, + pub data: Vec, + pub block_number: u64, + pub transaction_hash: Vec, + pub transaction_index: u32, + pub log_index: u32, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct OwnableEventSource { + pub id: String, + pub owner: String, + pub issuer: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct IngestEventMsg { + pub source: OwnableEventSource, + pub event_type: String, + pub attributes: Value, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EncodePublicEventMsg { + pub event_type: String, + pub data: Vec, +} diff --git a/packages/core/src/services/Ownable.service.ts b/packages/core/src/services/Ownable.service.ts index a81761d..d03bdc8 100644 --- a/packages/core/src/services/Ownable.service.ts +++ b/packages/core/src/services/Ownable.service.ts @@ -297,10 +297,10 @@ export default class OwnableService { case "execute_msg.json": result = await rpc.execute(msg, info, stateDump); break; - case "register_public_event_msg.json": + case "register_msg.json": result = await rpc.register(this.toRegisterRpcPayload(msg as PublicEvent), info, stateDump); break; - case "ingest_event_msg.json": + case "ingest_msg.json": result = await rpc.ingest(msg as OwnableEvent, info, stateDump); break; default: @@ -431,7 +431,7 @@ export default class OwnableService { await withProgress(onProgress)("signPublicEvent", () => this.eqty.sign( new Event({ - "@context": "register_public_event_msg.json", + "@context": "register_msg.json", ...publicEvent, }).addTo(chain) ) @@ -555,7 +555,7 @@ export default class OwnableService { await withProgress(onProgress)("signConsumerEvent", () => this.eqty.sign( new Event({ - "@context": "ingest_event_msg.json", + "@context": "ingest_msg.json", ...ingestEvent, }).addTo(consumer) ) diff --git a/packages/core/tests/ownable.service.test.ts b/packages/core/tests/ownable.service.test.ts index 3e62ce1..b9509ca 100644 --- a/packages/core/tests/ownable.service.test.ts +++ b/packages/core/tests/ownable.service.test.ts @@ -281,7 +281,7 @@ describe('OwnableService', () => { { parsedData: { '@context': 'execute_msg.json', bar: 2 }, signerAddress: '0x2', hash: { hex: '0x2' } }, { parsedData: { - '@context': 'register_public_event_msg.json', + '@context': 'register_msg.json', source: '0xabc', eventType: 'x', data: `0x${'11'.repeat(4)}`, @@ -295,7 +295,7 @@ describe('OwnableService', () => { }, { parsedData: { - '@context': 'ingest_event_msg.json', + '@context': 'ingest_msg.json', source: { id: 'src-1', owner: 'owner-1', issuer: 'issuer-1' }, eventType: 'consume', attributes: { a: 1 },