diff --git a/Cargo.toml b/Cargo.toml index 8389005..a68e35a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,9 +94,11 @@ members = [ "contracts/timelock_vault", "contracts/governance_token", "contracts/event_logging", - "contracts/revenue_share", - "contracts/cross_contract", - "contracts/royalty_distribution", +"contracts/revenue_share", + "contracts/cross_contract", + "contracts/royalty_distribution", + "contracts/query_engine", +] "contracts/collateral_lending", ] diff --git a/contracts/query_engine/Cargo.toml b/contracts/query_engine/Cargo.toml new file mode 100644 index 0000000..44cf5d3 --- /dev/null +++ b/contracts/query_engine/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "query_engine" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/contracts/query_engine/src/lib.rs b/contracts/query_engine/src/lib.rs new file mode 100644 index 0000000..c22a88b --- /dev/null +++ b/contracts/query_engine/src/lib.rs @@ -0,0 +1,330 @@ +#![no_std] + +mod storage; +pub mod types; + +use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, Symbol, Val, Vec}; +use crate::storage::*; +use crate::types::*; + +#[contract] +pub struct QueryEngineContract; + +#[contractimpl] +impl QueryEngineContract { + // ────────────────────────────────────────────────── + // INITIALIZATION + // ────────────────────────────────────────────────── + + pub fn initialize(env: Env, admin: Address) { + if is_initialized(&env) { + panic_with_error(&env, EngineError::AlreadyInitialized); + } + admin.require_auth(); + set_admin(&env, &admin); + set_initialized(&env); + set_role(&env, &admin, &Role::Admin); + } + + pub fn is_initialized(env: Env) -> bool { + is_initialized(&env) + } + + pub fn get_admin(env: Env) -> Address { + get_admin(&env) + } + + // ────────────────────────────────────────────────── + // PAUSE / UNPAUSE + // ────────────────────────────────────────────────── + + pub fn pause(env: Env, caller: Address) { + require_admin(&env, &caller); + if is_paused(&env) { + panic_with_error(&env, EngineError::Paused); + } + set_paused(&env, true); + } + + pub fn unpause(env: Env, caller: Address) { + require_admin(&env, &caller); + if !is_paused(&env) { + panic_with_error(&env, EngineError::NotPaused); + } + set_paused(&env, false); + } + + pub fn is_paused(env: Env) -> bool { + is_paused(&env) + } + + // ────────────────────────────────────────────────── + // ROLE MANAGEMENT + // ────────────────────────────────────────────────── + + pub fn set_role(env: Env, caller: Address, target: Address, role: Role) { + require_admin(&env, &caller); + set_role(&env, &target, &role); + } + + pub fn get_role(env: Env, address: Address) -> Option { + get_role(&env, &address) + } + + // ────────────────────────────────────────────────── + // SCHEMA REGISTRATION + // ────────────────────────────────────────────────── + + pub fn register_type(env: Env, caller: Address, type_def: TypeDefinition) { + require_not_paused(&env); + require_role(&env, &caller, &Role::Operator); + register_type(&env, &type_def); + } + + pub fn get_type(env: Env, type_name: Symbol) -> TypeDefinition { + get_type_definition(&env, &type_name) + } + + pub fn has_type(env: Env, type_name: Symbol) -> bool { + type_exists(&env, &type_name) + } + + // ────────────────────────────────────────────────── + // DATA MUTATIONS + // ────────────────────────────────────────────────── + + pub fn mutate(env: Env, caller: Address, input: MutationInput) -> MutationResult { + require_not_paused(&env); + require_role(&env, &caller, &Role::Operator); + caller.require_auth(); + + match input.operation { + MutationOp::Create => { + let id: Val = env.ledger().timestamp().into_val(&env); + let data: Val = input.data.into_val(&env); + save_record(&env, &input.type_name, &id, &data); + let pos = next_record_counter(&env, &input.type_name); + index_record(&env, &input.type_name, pos, &id); + MutationResult { + success: true, + id: Some(id), + error: None, + } + } + MutationOp::Update => { + let id = input.id.unwrap_or_else(|| panic_with_error(&env, EngineError::InvalidMutation)); + let data: Val = input.data.into_val(&env); + save_record(&env, &input.type_name, &id, &data); + MutationResult { + success: true, + id: Some(id), + error: None, + } + } + MutationOp::Delete => { + let id = input.id.unwrap_or_else(|| panic_with_error(&env, EngineError::InvalidMutation)); + remove_record(&env, &input.type_name, &id); + MutationResult { + success: true, + id: Some(id), + error: None, + } + } + } + } + + // ────────────────────────────────────────────────── + // QUERY EXECUTION + // ────────────────────────────────────────────────── + + pub fn query(env: Env, input: QueryInput) -> QueryResult { + let _type_def = get_type_definition(&env, &input.type_name); + + let total = record_count(&env, &input.type_name); + let limit = input.pagination.limit.min(MAX_PAGE_SIZE); + + let start_idx = match &input.pagination.cursor { + Some(cursor_val) => { + let cursor_u32: u32 = cursor_val.clone().try_into().unwrap_or(0); + cursor_u32 + } + None => 0u32, + }; + + let end_idx = (start_idx + limit).min(total); + let mut items: Vec = Vec::new(&env); + + for i in start_idx..end_idx { + let pos = if input.sort.is_some() && input.sort.clone().unwrap().direction == SortDirection::Desc { + total - 1 - i + } else { + i + }; + if let Some(id) = get_indexed_record(&env, &input.type_name, pos + 1) { + if let Some(record) = get_record(&env, &input.type_name, &id) { + if Self::matches_filters(&record, &input.filters) { + let projected = Self::project_fields(&env, &record, &input.fields); + items.push_back(projected); + } + } + } + } + + let has_more = end_idx < total; + let next_cursor = if has_more { + Some((end_idx as u64).into_val(&env)) + } else { + None + }; + + QueryResult { + items, + total_count: total, + next_cursor, + has_more, + } + } + + // ────────────────────────────────────────────────── + // BATCH QUERY + // ────────────────────────────────────────────────── + + pub fn batch_query(env: Env, input: BatchQueryInput) -> BatchQueryResult { + if input.queries.len() > MAX_BATCH_SIZE { + panic_with_error(&env, EngineError::BatchLimitExceeded); + } + + let mut results: Vec = Vec::new(&env); + for i in 0..input.queries.len() { + let q = input.queries.get(i).unwrap(); + results.push_back(Self::query(env.clone(), q)); + } + + BatchQueryResult { results } + } + + // ────────────────────────────────────────────────── + // BATCH GET BY IDS + // ────────────────────────────────────────────────── + + pub fn batch_get(env: Env, type_name: Symbol, ids: Vec) -> Vec { + let mut results: Vec = Vec::new(&env); + for i in 0..ids.len() { + let id = ids.get(i).unwrap(); + if let Some(record) = get_record(&env, &type_name, &id) { + results.push_back(record); + } + } + results + } + + // ────────────────────────────────────────────────── + // SUBSCRIPTIONS + // ────────────────────────────────────────────────── + + pub fn subscribe(env: Env, caller: Address, input: SubscriptionInput) -> u64 { + require_not_paused(&env); + caller.require_auth(); + if caller != input.subscriber { + panic_with_error(&env, EngineError::Unauthorized); + } + + let id = next_subscription_id(&env); + let sub = Subscription { + id, + source_contract: input.source_contract.clone(), + topic: input.topic.clone(), + subscriber: input.subscriber.clone(), + created_at: env.ledger().timestamp(), + active: true, + }; + save_subscription(&env, &sub); + id + } + + pub fn unsubscribe(env: Env, caller: Address, subscription_id: u64) { + caller.require_auth(); + let _sub = get_subscription(&env, subscription_id); + if caller != _sub.subscriber && caller != get_admin(&env) { + panic_with_error(&env, EngineError::Unauthorized); + } + deactivate_subscription(&env, subscription_id); + } + + pub fn get_subscription(env: Env, subscription_id: u64) -> Subscription { + get_subscription(&env, subscription_id) + } + + pub fn emit_subscription_event( + env: Env, + caller: Address, + subscription_id: u64, + data: Val, + ) -> u64 { + require_not_paused(&env); + require_role(&env, &caller, &Role::Operator); + + let _sub = get_subscription(&env, subscription_id); + if !_sub.active { + panic_with_error(&env, EngineError::SubscriptionNotFound); + } + + let count = get_subscription_event_count(&env, subscription_id); + let event_id = count + 1; + let event = SubscriptionEvent { + subscription_id, + event_id, + timestamp: env.ledger().timestamp(), + data, + }; + save_subscription_event(&env, subscription_id, &event); + event_id + } + + pub fn get_subscription_events( + env: Env, + subscription_id: u64, + start: u64, + limit: u64, + ) -> Vec { + let _sub = get_subscription(&env, subscription_id); + let total = get_subscription_event_count(&env, subscription_id); + let end = (start + limit).min(total); + let mut events: Vec = Vec::new(&env); + for i in start..end { + let event_id = i + 1; + if let Some(event) = get_subscription_event(&env, subscription_id, event_id) { + events.push_back(event); + } + } + events + } + + // ────────────────────────────────────────────────── + // INTERNAL HELPERS + // ────────────────────────────────────────────────── + + fn matches_filters(data: &Val, filters: &Vec) -> bool { + if filters.is_empty() { + return true; + } + for i in 0..filters.len() { + let f = filters.get(i).unwrap(); + if !Self::apply_filter(data, &f) { + return false; + } + } + true + } + + fn apply_filter(_data: &Val, _filter: &Filter) -> bool { + true + } + + fn project_fields(_env: &Env, data: &Val, _fields: &Vec) -> Val { + data.clone() + } +} + +#[cfg(test)] +mod test; \ No newline at end of file diff --git a/contracts/query_engine/src/storage.rs b/contracts/query_engine/src/storage.rs new file mode 100644 index 0000000..dad28d3 --- /dev/null +++ b/contracts/query_engine/src/storage.rs @@ -0,0 +1,171 @@ +use soroban_sdk::{Address, Env, Symbol, Val, Vec}; +use crate::types::{DataKey, EngineError, Role, Subscription, SubscriptionEvent, TypeDefinition}; + +pub fn panic_with_error(_env: &Env, err: EngineError) -> ! { + panic!("engine error: {}", err as u32); +} + +pub fn has_admin(env: &Env) -> bool { + env.storage().instance().has(&DataKey::Admin) +} + +pub fn set_admin(env: &Env, admin: &Address) { + env.storage().instance().set(&DataKey::Admin, admin); +} + +pub fn get_admin(env: &Env) -> Address { + env.storage().instance().get(&DataKey::Admin).unwrap() +} + +pub fn set_initialized(env: &Env) { + env.storage().instance().set(&DataKey::Initialized, &true); +} + +pub fn is_initialized(env: &Env) -> bool { + env.storage().instance().get(&DataKey::Initialized).unwrap_or(false) +} + +pub fn require_admin(env: &Env, caller: &Address) { + caller.require_auth(); + if caller != &get_admin(env) { + panic_with_error(env, EngineError::Unauthorized); + } +} + +pub fn set_paused(env: &Env, paused: bool) { + env.storage().instance().set(&DataKey::Paused, &paused); +} + +pub fn is_paused(env: &Env) -> bool { + env.storage().instance().get(&DataKey::Paused).unwrap_or(false) +} + +pub fn require_not_paused(env: &Env) { + if is_paused(env) { + panic_with_error(env, EngineError::Paused); + } +} + +pub fn set_role(env: &Env, address: &Address, role: &Role) { + env.storage().instance().set(&DataKey::Role(address.clone()), role); +} + +pub fn get_role(env: &Env, address: &Address) -> Option { + env.storage().instance().get(&DataKey::Role(address.clone())) +} + +pub fn has_role(env: &Env, address: &Address, required: &Role) -> bool { + match get_role(env, address) { + Some(role) => role as u32 <= required.clone() as u32, + None => false, + } +} + +pub fn require_role(env: &Env, address: &Address, required: &Role) { + address.require_auth(); + if !has_role(env, address, required) { + panic_with_error(env, EngineError::Unauthorized); + } +} + +pub fn register_type(env: &Env, type_def: &TypeDefinition) { + let key = DataKey::TypeRegistry(type_def.name.clone()); + if env.storage().instance().has(&key) { + panic_with_error(env, EngineError::TypeAlreadyExists); + } + env.storage().instance().set(&key, type_def); +} + +pub fn get_type_definition(env: &Env, type_name: &Symbol) -> TypeDefinition { + env.storage().instance() + .get(&DataKey::TypeRegistry(type_name.clone())) + .unwrap_or_else(|| panic_with_error(env, EngineError::TypeNotFound)) +} + +pub fn type_exists(env: &Env, type_name: &Symbol) -> bool { + env.storage().instance().has(&DataKey::TypeRegistry(type_name.clone())) +} + +pub fn save_record(env: &Env, type_name: &Symbol, id: &Val, data: &Val) { + let key = DataKey::Record(type_name.clone(), id.clone()); + env.storage().persistent().set(&key, data); +} + +pub fn get_record(env: &Env, type_name: &Symbol, id: &Val) -> Option { + let key = DataKey::Record(type_name.clone(), id.clone()); + env.storage().persistent().get(&key) +} + +pub fn remove_record(env: &Env, type_name: &Symbol, id: &Val) { + let key = DataKey::Record(type_name.clone(), id.clone()); + env.storage().persistent().remove(&key); +} + +pub fn next_record_counter(env: &Env, type_name: &Symbol) -> u32 { + let count: u32 = env.storage().instance() + .get(&DataKey::RecordCounter(type_name.clone())) + .unwrap_or(0); + let next = count + 1; + env.storage().instance().set(&DataKey::RecordCounter(type_name.clone()), &next); + next +} + +pub fn index_record(env: &Env, type_name: &Symbol, index_pos: u32, id: &Val) { + let key = DataKey::RecordIndex(type_name.clone(), index_pos); + env.storage().persistent().set(&key, id); +} + +pub fn get_indexed_record(env: &Env, type_name: &Symbol, index_pos: u32) -> Option { + let key = DataKey::RecordIndex(type_name.clone(), index_pos); + env.storage().persistent().get(&key) +} + +pub fn record_count(env: &Env, type_name: &Symbol) -> u32 { + env.storage().instance() + .get(&DataKey::RecordCounter(type_name.clone())) + .unwrap_or(0) +} + +pub fn next_subscription_id(env: &Env) -> u64 { + let count: u64 = env.storage().instance() + .get(&DataKey::SubscriptionCount) + .unwrap_or(0); + let next = count + 1; + env.storage().instance().set(&DataKey::SubscriptionCount, &next); + next +} + +pub fn save_subscription(env: &Env, sub: &Subscription) { + env.storage().persistent().set(&DataKey::Subscription(sub.id), sub); +} + +pub fn get_subscription(env: &Env, id: u64) -> Subscription { + env.storage().persistent() + .get(&DataKey::Subscription(id)) + .unwrap_or_else(|| panic_with_error(env, EngineError::SubscriptionNotFound)) +} + +pub fn save_subscription_event(env: &Env, sub_id: u64, event: &SubscriptionEvent) { + let key = DataKey::SubscriptionEvent(sub_id, event.event_id); + env.storage().persistent().set(&key, event); + let count: u64 = env.storage().persistent() + .get(&DataKey::SubscriptionEventCount(sub_id)) + .unwrap_or(0); + env.storage().persistent().set(&DataKey::SubscriptionEventCount(sub_id), &(count + 1)); +} + +pub fn get_subscription_event_count(env: &Env, sub_id: u64) -> u64 { + env.storage().persistent() + .get(&DataKey::SubscriptionEventCount(sub_id)) + .unwrap_or(0) +} + +pub fn get_subscription_event(env: &Env, sub_id: u64, event_id: u64) -> Option { + env.storage().persistent().get(&DataKey::SubscriptionEvent(sub_id, event_id)) +} + +pub fn deactivate_subscription(env: &Env, sub_id: u64) { + let mut sub = get_subscription(env, sub_id); + sub.active = false; + save_subscription(env, &sub); +} \ No newline at end of file diff --git a/contracts/query_engine/src/test.rs b/contracts/query_engine/src/test.rs new file mode 100644 index 0000000..557b840 --- /dev/null +++ b/contracts/query_engine/src/test.rs @@ -0,0 +1,353 @@ +#![cfg(test)] + +use super::*; +use soroban_sdk::{testutils::Address as _, Address, Env, IntoVal, Symbol, Vec}; + +fn setup() -> (Env, QueryEngineContractClient<'static>, Address) { + let env = Env::default(); + let contract_id = env.register_contract(None, QueryEngineContract); + let client = QueryEngineContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + + env.mock_all_auths(); + client.initialize(&admin); + client.set_role(&admin, &admin, &Role::Operator); + + (env, client, admin) +} + +fn register_player_type(env: &Env, client: &QueryEngineContractClient<'static>, admin: &Address) { + let mut fields: Vec = Vec::new(env); + fields.push_back(FieldDefinition { + name: Symbol::new(env, "name"), + field_type: FieldType::String, + required: true, + }); + fields.push_back(FieldDefinition { + name: Symbol::new(env, "score"), + field_type: FieldType::U64, + required: false, + }); + let type_def = TypeDefinition { + name: Symbol::new(env, "Player"), + fields, + description: String::from_str(env, "A player profile"), + }; + client.register_type(admin, &type_def); +} + +#[test] +fn test_initialize() { + let env = Env::default(); + let admin = Address::generate(&env); + let contract_id = env.register_contract(None, QueryEngineContract); + let client = QueryEngineContractClient::new(&env, &contract_id); + + assert!(!client.is_initialized()); + env.mock_all_auths(); + client.initialize(&admin); + assert!(client.is_initialized()); + assert_eq!(client.get_admin(), admin); +} + +#[test] +fn test_initialize_twice_panics() { + let env = Env::default(); + let admin = Address::generate(&env); + let contract_id = env.register_contract(None, QueryEngineContract); + let client = QueryEngineContractClient::new(&env, &contract_id); + + env.mock_all_auths(); + client.initialize(&admin); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.initialize(&admin); + })); + assert!(result.is_err()); +} + +#[test] +fn test_pause_unpause() { + let (_env, client, admin) = setup(); + + assert!(!client.is_paused()); + client.pause(&admin); + assert!(client.is_paused()); + client.unpause(&admin); + assert!(!client.is_paused()); +} + +#[test] +fn test_register_type() { + let (_env, client, admin) = setup(); + + register_player_type(&_env, &client, &admin); + + let type_name = Symbol::new(&_env, "Player"); + assert!(client.has_type(&type_name)); + let retrieved = client.get_type(&type_name); + assert_eq!(retrieved.name, type_name); +} + +#[test] +fn test_register_duplicate_type_panics() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + let mut fields: Vec = Vec::new(&_env); + fields.push_back(FieldDefinition { + name: Symbol::new(&_env, "name"), + field_type: FieldType::String, + required: true, + }); + let type_def = TypeDefinition { + name: Symbol::new(&_env, "Player"), + fields, + description: String::from_str(&_env, "duplicate"), + }; + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.register_type(&admin, &type_def); + })); + assert!(result.is_err()); +} + +#[test] +fn test_mutation_create() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + let mut data: Vec<(Symbol, Val)> = Vec::new(&_env); + data.push_back(( + Symbol::new(&_env, "name"), + String::from_str(&_env, "Alice").into_val(&_env), + )); + data.push_back(( + Symbol::new(&_env, "score"), + (100u64).into_val(&_env), + )); + + let mutation = MutationInput { + type_name: Symbol::new(&_env, "Player"), + operation: MutationOp::Create, + id: None, + data, + }; + + let result = client.mutate(&admin, &mutation); + assert!(result.success); + assert!(result.id.is_some()); +} + +#[test] +fn test_query_empty() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + let query = QueryInput { + type_name: Symbol::new(&_env, "Player"), + filters: Vec::new(&_env), + sort: None, + pagination: Pagination { + limit: 10, + cursor: None, + }, + fields: Vec::new(&_env), + }; + + let result = client.query(&query); + assert_eq!(result.total_count, 0); + assert!(result.items.is_empty()); + assert!(!result.has_more); +} + +#[test] +fn test_query_with_data() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + let mut data: Vec<(Symbol, Val)> = Vec::new(&_env); + data.push_back(( + Symbol::new(&_env, "name"), + String::from_str(&_env, "Alice").into_val(&_env), + )); + data.push_back(( + Symbol::new(&_env, "score"), + (100u64).into_val(&_env), + )); + + let mutation = MutationInput { + type_name: Symbol::new(&_env, "Player"), + operation: MutationOp::Create, + id: None, + data, + }; + client.mutate(&admin, &mutation); + + let query = QueryInput { + type_name: Symbol::new(&_env, "Player"), + filters: Vec::new(&_env), + sort: None, + pagination: Pagination { + limit: 10, + cursor: None, + }, + fields: Vec::new(&_env), + }; + + let result = client.query(&query); + assert_eq!(result.total_count, 1); + assert_eq!(result.items.len(), 1); + assert!(!result.has_more); +} + +#[test] +fn test_query_pagination() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + for i in 0..5u64 { + let mut data: Vec<(Symbol, Val)> = Vec::new(&_env); + let name = String::from_str(&_env, &format!("Player{}", i)); + data.push_back((Symbol::new(&_env, "name"), name.into_val(&_env))); + data.push_back((Symbol::new(&_env, "score"), (i * 10).into_val(&_env))); + + let mutation = MutationInput { + type_name: Symbol::new(&_env, "Player"), + operation: MutationOp::Create, + id: None, + data, + }; + client.mutate(&admin, &mutation); + } + + let query = QueryInput { + type_name: Symbol::new(&_env, "Player"), + filters: Vec::new(&_env), + sort: None, + pagination: Pagination { + limit: 2, + cursor: None, + }, + fields: Vec::new(&_env), + }; + + let result = client.query(&query); + assert_eq!(result.total_count, 5); + assert_eq!(result.items.len(), 2); + assert!(result.has_more); + assert!(result.next_cursor.is_some()); +} + +#[test] +fn test_batch_query() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + let query1 = QueryInput { + type_name: Symbol::new(&_env, "Player"), + filters: Vec::new(&_env), + sort: None, + pagination: Pagination { limit: 10, cursor: None }, + fields: Vec::new(&_env), + }; + let query2 = QueryInput { + type_name: Symbol::new(&_env, "Player"), + filters: Vec::new(&_env), + sort: None, + pagination: Pagination { limit: 5, cursor: None }, + fields: Vec::new(&_env), + }; + + let batch_input = BatchQueryInput { + queries: vec![&_env, query1, query2], + }; + + let batch_result = client.batch_query(&batch_input); + assert_eq!(batch_result.results.len(), 2); +} + +#[test] +fn test_subscription_flow() { + let (_env, client, admin) = setup(); + + let subscriber = Address::generate(&_env); + let source = Address::generate(&_env); + + let sub_input = SubscriptionInput { + source_contract: source.clone(), + topic: Symbol::new(&_env, "quest_done"), + subscriber: subscriber.clone(), + }; + + let sub_id = client.subscribe(&subscriber, &sub_input); + assert!(sub_id > 0); + + let sub = client.get_subscription(&sub_id); + assert_eq!(sub.id, sub_id); + assert_eq!(sub.subscriber, subscriber); + assert!(sub.active); + + let event_data: Val = (42u64).into_val(&_env); + let event_id = client.emit_subscription_event(&admin, &sub_id, &event_data); + assert_eq!(event_id, 1); + + let events = client.get_subscription_events(&sub_id, &0, &10); + assert_eq!(events.len(), 1); + + client.unsubscribe(&subscriber, &sub_id); + let sub_after = client.get_subscription(&sub_id); + assert!(!sub_after.active); +} + +#[test] +fn test_batch_get() { + let (_env, client, admin) = setup(); + register_player_type(&_env, &client, &admin); + + let mut data: Vec<(Symbol, Val)> = Vec::new(&_env); + let name = String::from_str(&_env, "Alice"); + data.push_back((Symbol::new(&_env, "name"), name.into_val(&_env))); + data.push_back((Symbol::new(&_env, "score"), (100u64).into_val(&_env))); + + let mutation = MutationInput { + type_name: Symbol::new(&_env, "Player"), + operation: MutationOp::Create, + id: None, + data, + }; + let result = client.mutate(&admin, &mutation); + let created_id = result.id.unwrap(); + + let ids = vec![&_env, created_id]; + let records = client.batch_get(&Symbol::new(&_env, "Player"), &ids); + assert_eq!(records.len(), 1); +} + +#[test] +fn test_role_management() { + let (_env, client, admin) = setup(); + + let operator = Address::generate(&_env); + client.set_role(&admin, &operator, &Role::Operator); + + let role = client.get_role(&operator); + assert_eq!(role, Some(Role::Operator)); +} + +#[test] +fn test_unauthorized_access_panics() { + let env = Env::default(); + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let contract_id = env.register_contract(None, QueryEngineContract); + let client = QueryEngineContractClient::new(&env, &contract_id); + + env.mock_all_auths(); + client.initialize(&admin); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + client.pause(&attacker); + })); + assert!(result.is_err()); +} \ No newline at end of file diff --git a/contracts/query_engine/src/types.rs b/contracts/query_engine/src/types.rs new file mode 100644 index 0000000..d953f77 --- /dev/null +++ b/contracts/query_engine/src/types.rs @@ -0,0 +1,202 @@ +use soroban_sdk::{contracttype, Address, String, Symbol, Val, Vec}; + +#[contracttype] +#[derive(Clone, Debug)] +pub enum FieldType { + String, + Symbol, + Address, + U64, + I128, + Bool, + Object(Symbol), +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct FieldDefinition { + pub name: Symbol, + pub field_type: FieldType, + pub required: bool, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct TypeDefinition { + pub name: Symbol, + pub fields: Vec, + pub description: String, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub enum FilterOp { + Eq, + Neq, + Gt, + Lt, + Gte, + Lte, + In, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct Filter { + pub field: Symbol, + pub operator: FilterOp, + pub value: Val, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum SortDirection { + Asc, + Desc, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct Sort { + pub field: Symbol, + pub direction: SortDirection, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct Pagination { + pub limit: u32, + pub cursor: Option, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct QueryInput { + pub type_name: Symbol, + pub filters: Vec, + pub sort: Option, + pub pagination: Pagination, + pub fields: Vec, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct QueryResult { + pub items: Vec, + pub total_count: u32, + pub next_cursor: Option, + pub has_more: bool, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub enum MutationOp { + Create, + Update, + Delete, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct MutationInput { + pub type_name: Symbol, + pub operation: MutationOp, + pub id: Option, + pub data: Vec<(Symbol, Val)>, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct MutationResult { + pub success: bool, + pub id: Option, + pub error: Option, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct SubscriptionInput { + pub source_contract: Address, + pub topic: Symbol, + pub subscriber: Address, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct Subscription { + pub id: u64, + pub source_contract: Address, + pub topic: Symbol, + pub subscriber: Address, + pub created_at: u64, + pub active: bool, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct SubscriptionEvent { + pub subscription_id: u64, + pub event_id: u64, + pub timestamp: u64, + pub data: Val, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct BatchQueryInput { + pub queries: Vec, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub struct BatchQueryResult { + pub results: Vec, +} + +#[contracttype] +#[derive(Clone, Debug)] +pub enum Role { + Admin, + Operator, + Reader, +} + +#[contracttype] +pub enum DataKey { + Admin, + Initialized, + TypeRegistry(Symbol), + TypeCount, + Record(Symbol, Val), + RecordIndex(Symbol, u32), + RecordCounter(Symbol), + Subscription(u64), + SubscriptionCount, + SubscriptionByContract(Symbol, u64), + SubscriptionEvent(u64, u64), + SubscriptionEventCount(u64), + Role(Address), + Paused, +} + +#[contracttype] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EngineError { + NotInitialized = 1, + AlreadyInitialized = 2, + Unauthorized = 3, + TypeNotFound = 4, + TypeAlreadyExists = 5, + RecordNotFound = 6, + InvalidFilter = 7, + InvalidField = 8, + Paused = 9, + NotPaused = 10, + InvalidPagination = 11, + SubscriptionNotFound = 12, + InvalidMutation = 13, + BatchLimitExceeded = 14, +} + +pub const MAX_PAGE_SIZE: u32 = 50; +pub const MAX_BATCH_SIZE: u32 = 20; \ No newline at end of file