This tutorial walks through building a smart contract that uses Neutron’s Interchain Query (ICQ) module to query data from other blockchains. You’ll learn how to register queries, process the results, and build applications that can react to state changes on connected chains.

Overview

Interchain Queries are a powerful feature of Neutron that allow smart contracts to query the state of other chains in the Cosmos ecosystem. This enables developers to build cross-chain applications that can react to state changes on connected chains. In this tutorial, you’ll learn how to:
  1. Register an interchain query
  2. Process query results
  3. Build a simple contract that reacts to state changes on another chain

Prerequisites

Before starting this tutorial, you should:

Setting Up Your Project

Start by creating a new CosmWasm project using cargo-generate:
cargo generate --git https://github.com/CosmWasm/cw-template.git --name neutron-icq-example
cd neutron-icq-example

Adding Dependencies

Update your Cargo.toml file to include the necessary dependencies:
[package]
name = "neutron-icq-example"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[features]
backtraces = ["cosmwasm-std/backtraces"]
library = []

[dependencies]
cosmwasm-std = "1.5.0"
cosmwasm-schema = "1.5.0"
cw-storage-plus = "1.1.0"
schemars = "0.8.12"
serde = { version = "1.0.171", default-features = false, features = ["derive"] }
thiserror = "1.0.40"
base64 = "0.21.2"
prost = "0.12.1"

# Neutron specific dependencies
neutron-sdk = { version = "0.10.0", features = ["async"] }

Project Structure

Create the following file structure:
src/
  bin/
    schema.rs
  contract.rs
  error.rs
  helpers.rs
  lib.rs
  msg.rs
  state.rs
  queries/           # For our interchain query definitions
    mod.rs
    cosmos_staking.rs

Implementing the Contract

State Management

First, let’s define our state structure in state.rs to store information about registered queries and their results:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use cosmwasm_std::{Addr, Coin, Uint128};
use cw_storage_plus::{Item, Map};

// Store registered interchain queries
pub const INTERCHAIN_QUERIES: Map<String, QueryInfo> = Map::new("interchain_queries");

// Store the results of queries
pub const QUERY_RESULTS: Map<String, QueryResult> = Map::new("query_results");

// Configuration for the contract
pub const CONFIG: Item<Config> = Item::new("config");

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub owner: Addr,
    pub connection_id: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct QueryInfo {
    pub id: String,
    pub connection_id: String,
    pub chain_id: String,
    pub query_type: QueryType,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryType {
    // The Key-Value store query
    KV {
        key: Vec<u8>,
        path: String,
    },
    // The transaction query
    TX {
        height: u64,
        hash: String,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct QueryResult {
    pub id: String,
    pub result_type: ResultType,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ResultType {
    // KV query result
    KV {
        value: Vec<u8>,
        height: u64,
    },
    // TX query result
    TX {
        tx_hash: String,
        height: u64,
        tx_result: TransactionResult,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TransactionResult {
    pub code: u64,
    pub log: String,
    pub events: Vec<Event>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Event {
    pub r#type: String,
    pub attributes: Vec<Attribute>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Attribute {
    pub key: String,
    pub value: String,
}

// For storing delegator information from Cosmos staking queries
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct DelegatorInfo {
    pub delegator: String,
    pub validator: String,
    pub amount: Coin,
}

Message Definitions

Next, let’s define our messages in msg.rs:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::state::{DelegatorInfo, QueryResult, QueryType};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub connection_id: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    // Register a new interchain query
    RegisterQuery {
        query_id: String,
        chain_id: String,
        query_type: QueryType,
        update_period: u64,
    },
    // Remove an existing interchain query
    RemoveQuery {
        query_id: String,
    },
    // Update an existing interchain query
    UpdateQuery {
        query_id: String,
        new_update_period: u64,
    },
    // Query Cosmos staking delegation for a specific delegator and validator
    QueryDelegation {
        query_id: String,
        chain_id: String,
        delegator: String,
        validator: String,
        update_period: u64,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    // Get information about a registered query
    GetQuery {
        query_id: String,
    },
    // Get the result of a query
    GetQueryResult {
        query_id: String,
    },
    // Get delegator information
    GetDelegatorInfo {
        query_id: String,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GetQueryResponse {
    pub query_id: String,
    pub registered: bool,
    pub query_type: Option<QueryType>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GetQueryResultResponse {
    pub query_id: String,
    pub result: Option<QueryResult>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GetDelegatorInfoResponse {
    pub query_id: String,
    pub delegator_info: Option<DelegatorInfo>,
}

Cosmos Staking Query Helpers

Let’s create helpers for querying the Cosmos staking module in queries/cosmos_staking.rs:
use cosmwasm_std::{Coin, StdResult};
use prost::Message;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::ContractError;

// Define proto structures
// These match the Cosmos SDK's proto definitions for staking queries
#[derive(Clone, PartialEq, Message)]
pub struct QueryDelegationRequest {
    #[prost(string, tag = "1")]
    pub delegator_addr: String,
    #[prost(string, tag = "2")]
    pub validator_addr: String,
}

#[derive(Clone, PartialEq, Message)]
pub struct QueryDelegationResponse {
    #[prost(message, optional, tag = "1")]
    pub delegation_response: Option<DelegationResponse>,
}

#[derive(Clone, PartialEq, Message)]
pub struct DelegationResponse {
    #[prost(message, optional, tag = "1")]
    pub delegation: Option<Delegation>,
    #[prost(message, optional, tag = "2")]
    pub balance: Option<CosmosCoin>,
}

#[derive(Clone, PartialEq, Message)]
pub struct Delegation {
    #[prost(string, tag = "1")]
    pub delegator_address: String,
    #[prost(string, tag = "2")]
    pub validator_address: String,
    #[prost(string, tag = "3")]
    pub shares: String,
}

#[derive(Clone, PartialEq, Message)]
pub struct CosmosCoin {
    #[prost(string, tag = "1")]
    pub denom: String,
    #[prost(string, tag = "2")]
    pub amount: String,
}

// Helper function to create a staking delegation query key
pub fn create_delegation_key(delegator: &str, validator: &str) -> Vec<u8> {
    // Encode according to Cosmos SDK storage patterns
    let request = QueryDelegationRequest {
        delegator_addr: delegator.to_string(),
        validator_addr: validator.to_string(),
    };
    
    let mut buf = Vec::new();
    buf.extend_from_slice(b"delegations/");
    buf.extend_from_slice(delegator.as_bytes());
    buf.extend_from_slice(b"/");
    buf.extend_from_slice(validator.as_bytes());
    
    buf
}

// Parse delegation response from raw bytes
pub fn parse_delegation_response(data: &[u8]) -> StdResult<Coin> {
    let response = QueryDelegationResponse::decode(data)
        .map_err(|e| ContractError::Std(cosmwasm_std::StdError::ParseErr {
            target_type: "QueryDelegationResponse".to_string(),
            msg: e.to_string(),
        }))?;
    
    let balance = response
        .delegation_response
        .ok_or_else(|| ContractError::Std(cosmwasm_std::StdError::GenericErr {
            msg: "No delegation response".to_string(),
        }))?
        .balance
        .ok_or_else(|| ContractError::Std(cosmwasm_std::StdError::GenericErr {
            msg: "No balance in delegation response".to_string(),
        }))?;
    
    Ok(Coin {
        denom: balance.denom,
        amount: balance.amount.parse().map_err(|e| ContractError::Std(cosmwasm_std::StdError::ParseErr {
            target_type: "Uint128".to_string(),
            msg: e.to_string(),
        }))?,
    })
}
Now let’s create the module file in queries/mod.rs:
pub mod cosmos_staking;

Error Handling

Create an error module in error.rs:
use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Query not found: {query_id}")]
    QueryNotFound { query_id: String },
    
    #[error("Query result not found: {query_id}")]
    QueryResultNotFound { query_id: String },
    
    #[error("Failed to decode query result: {msg}")]
    DecodeError { msg: String },
    
    #[error("Custom Error: {msg}")]
    CustomError { msg: String },
}

Contract Implementation

Now, let’s implement the main contract logic in contract.rs:
use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Coin,
    from_binary, Addr, WasmMsg, Reply, SubMsg, Uint128,
};

use neutron_sdk::{
    bindings::{
        msg::NeutronMsg,
        query::NeutronQuery,
        types::Height,
    },
    interchain_queries::{
        helpers,
        register_queries::{
            new_register_staking_delegation_query_msg, 
            register_queries_with_remotechain_id,
            update_period_with_chain_id,
            new_register_bank_total_balance_query_msg,
            update_period,
            remove_query,
        },
        types::{QueryPayload, QueryType as NeutronQueryType},
        v045::staking::delegations::DelegationsResponse,
    },
    sudo::msg::SudoMsg,
    NeutronResult, NeutronError,
};

use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};

use crate::error::ContractError;
use crate::msg::{
    ExecuteMsg, GetDelegatorInfoResponse, GetQueryResponse, GetQueryResultResponse, InstantiateMsg,
    QueryMsg,
};
use crate::state::{
    Config, DelegatorInfo, INTERCHAIN_QUERIES, QUERY_RESULTS, CONFIG, QueryInfo, QueryResult,
    QueryType, ResultType, TransactionResult, Event, Attribute,
};
use crate::queries::cosmos_staking::{create_delegation_key, parse_delegation_response};

// Constants for reply IDs
const REGISTER_QUERY_REPLY_ID: u64 = 1;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    // Save the configuration
    let config = Config {
        owner: info.sender.clone(),
        connection_id: msg.connection_id.clone(),
    };
    CONFIG.save(deps.storage, &config)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("owner", info.sender)
        .add_attribute("connection_id", msg.connection_id))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut<NeutronQuery>,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::RegisterQuery {
            query_id,
            chain_id,
            query_type,
            update_period,
        } => execute_register_query(deps, env, info, query_id, chain_id, query_type, update_period),
        ExecuteMsg::RemoveQuery { query_id } => {
            execute_remove_query(deps, env, info, query_id)
        }
        ExecuteMsg::UpdateQuery {
            query_id,
            new_update_period,
        } => execute_update_query(deps, env, info, query_id, new_update_period),
        ExecuteMsg::QueryDelegation {
            query_id,
            chain_id,
            delegator,
            validator,
            update_period,
        } => execute_query_delegation(
            deps,
            env,
            info,
            query_id,
            chain_id,
            delegator,
            validator,
            update_period,
        ),
    }
}

// Register a generic interchain query
fn execute_register_query(
    deps: DepsMut<NeutronQuery>,
    _env: Env,
    info: MessageInfo,
    query_id: String,
    chain_id: String,
    query_type: QueryType,
    update_period: u64,
) -> Result<Response, ContractError> {
    // Check authorization (owner only)
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }

    // Prepare the registration message based on query type
    let (register_msg, query_info) = match query_type.clone() {
        QueryType::KV { key, path } => {
            // Register KV query
            let register_msg = register_queries_with_remotechain_id(
                config.connection_id.clone(),
                chain_id.clone(),
                NeutronQueryType::KV,
                QueryPayload {
                    data: key.clone(),
                    path: path.clone(),
                    prove: true,
                },
                update_period,
                vec![],
            )?;

            let query_info = QueryInfo {
                id: query_id.clone(),
                connection_id: config.connection_id.clone(),
                chain_id: chain_id.clone(),
                query_type: QueryType::KV { key, path },
            };

            (register_msg, query_info)
        }
        QueryType::TX { height, hash } => {
            // Register TX query
            let register_msg = register_queries_with_remotechain_id(
                config.connection_id.clone(),
                chain_id.clone(),
                NeutronQueryType::Tx,
                QueryPayload {
                    data: hash.clone().into_bytes(),
                    path: format!("tx/height/{}", height),
                    prove: false,
                },
                update_period,
                vec![],
            )?;

            let query_info = QueryInfo {
                id: query_id.clone(),
                connection_id: config.connection_id.clone(),
                chain_id: chain_id.clone(),
                query_type: QueryType::TX { height, hash },
            };

            (register_msg, query_info)
        }
    };

    // Save query info in contract state
    INTERCHAIN_QUERIES.save(deps.storage, query_id.clone(), &query_info)?;

    // Register the query via Neutron's interchain_queries module
    let submsg = SubMsg::reply_on_success(register_msg, REGISTER_QUERY_REPLY_ID);

    Ok(Response::new()
        .add_submessage(submsg)
        .add_attribute("action", "register_query")
        .add_attribute("query_id", query_id)
        .add_attribute("query_type", match query_type {
            QueryType::KV { .. } => "kv".to_string(),
            QueryType::TX { .. } => "tx".to_string(),
        }))
}

// Remove a registered interchain query
fn execute_remove_query(
    deps: DepsMut<NeutronQuery>,
    _env: Env,
    info: MessageInfo,
    query_id: String,
) -> Result<Response, ContractError> {
    // Check authorization (owner only)
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }

    // Ensure the query exists
    let query_info = INTERCHAIN_QUERIES.may_load(deps.storage, query_id.clone())?;
    let query_info = match query_info {
        Some(info) => info,
        None => return Err(ContractError::QueryNotFound { query_id }),
    };

    // Query the interchain query ID from Neutron
    let neutron_query_id = helpers::get_registered_query_id_full(
        deps.as_ref(),
        &query_info.connection_id,
        match query_info.query_type {
            QueryType::KV { ref key, ref path } => (
                NeutronQueryType::KV,
                &QueryPayload {
                    data: key.clone(),
                    path: path.clone(),
                    prove: true,
                },
            ),
            QueryType::TX { height, ref hash } => (
                NeutronQueryType::Tx,
                &QueryPayload {
                    data: hash.clone().into_bytes(),
                    path: format!("tx/height/{}", height),
                    prove: false,
                },
            ),
        },
    )?;

    // Remove the query using Neutron's interchain_queries module
    let remove_msg = remove_query(neutron_query_id)?;

    // Remove from contract state
    INTERCHAIN_QUERIES.remove(deps.storage, query_id.clone());
    // Also remove any query results
    QUERY_RESULTS.remove(deps.storage, query_id.clone());

    Ok(Response::new()
        .add_message(remove_msg)
        .add_attribute("action", "remove_query")
        .add_attribute("query_id", query_id))
}

// Update an existing query's parameters
fn execute_update_query(
    deps: DepsMut<NeutronQuery>,
    _env: Env,
    info: MessageInfo,
    query_id: String,
    new_update_period: u64,
) -> Result<Response, ContractError> {
    // Check authorization (owner only)
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }

    // Ensure the query exists
    let query_info = INTERCHAIN_QUERIES.may_load(deps.storage, query_id.clone())?;
    let query_info = match query_info {
        Some(info) => info,
        None => return Err(ContractError::QueryNotFound { query_id }),
    };

    // Query the interchain query ID from Neutron
    let neutron_query_id = helpers::get_registered_query_id_full(
        deps.as_ref(),
        &query_info.connection_id,
        match query_info.query_type {
            QueryType::KV { ref key, ref path } => (
                NeutronQueryType::KV,
                &QueryPayload {
                    data: key.clone(),
                    path: path.clone(),
                    prove: true,
                },
            ),
            QueryType::TX { height, ref hash } => (
                NeutronQueryType::Tx,
                &QueryPayload {
                    data: hash.clone().into_bytes(),
                    path: format!("tx/height/{}", height),
                    prove: false,
                },
            ),
        },
    )?;

    // Update the query using Neutron's interchain_queries module
    let update_msg = if query_info.chain_id.is_empty() {
        update_period(neutron_query_id, new_update_period)?
    } else {
        update_period_with_chain_id(neutron_query_id, new_update_period, query_info.chain_id)?
    };

    Ok(Response::new()
        .add_message(update_msg)
        .add_attribute("action", "update_query")
        .add_attribute("query_id", query_id)
        .add_attribute("new_update_period", new_update_period.to_string()))
}

// Register a cosmos staking delegation query
fn execute_query_delegation(
    deps: DepsMut<NeutronQuery>,
    _env: Env,
    info: MessageInfo,
    query_id: String,
    chain_id: String,
    delegator: String,
    validator: String,
    update_period: u64,
) -> Result<Response, ContractError> {
    // Check authorization (owner only)
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }

    // Create the delegation key
    let key = create_delegation_key(&delegator, &validator);

    // Create a query info for storage
    let query_info = QueryInfo {
        id: query_id.clone(),
        connection_id: config.connection_id.clone(),
        chain_id: chain_id.clone(),
        query_type: QueryType::KV {
            key: key.clone(),
            path: "store/staking/key".to_string(),
        },
    };

    // Register the query with Neutron
    let register_msg = register_queries_with_remotechain_id(
        config.connection_id.clone(),
        chain_id,
        NeutronQueryType::KV,
        QueryPayload {
            data: key,
            path: "store/staking/key".to_string(),
            prove: true,
        },
        update_period,
        vec![],
    )?;

    // Save query info
    INTERCHAIN_QUERIES.save(deps.storage, query_id.clone(), &query_info)?;

    // Submit the registration
    let submsg = SubMsg::reply_on_success(register_msg, REGISTER_QUERY_REPLY_ID);

    Ok(Response::new()
        .add_submessage(submsg)
        .add_attribute("action", "query_delegation")
        .add_attribute("query_id", query_id)
        .add_attribute("delegator", delegator)
        .add_attribute("validator", validator))
}

// Handle the sudo callbacks from the Neutron blockchain
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
    match msg {
        // Process KV query results
        SudoMsg::KVQueryResult { query_id, kv_results } => {
            process_kv_result(deps, query_id, kv_results)
        }
        // Process TX query results
        SudoMsg::TxQueryResult {
            query_id,
            tx_results,
        } => process_tx_result(deps, query_id, tx_results),
        // Ignore other messages
        _ => Ok(Response::default()),
    }
}

// Process KV query results from the Neutron chain
fn process_kv_result(
    deps: DepsMut,
    query_id: u64,
    kv_results: neutron_sdk::interchain_queries::types::KVResult,
) -> Result<Response, ContractError> {
    // Find the query in our storage that corresponds to this Neutron query ID
    let mut found_query_id = None;

    // Iterate through all registered queries
    for item in INTERCHAIN_QUERIES.range(deps.storage, None, None, cosmwasm_std::Order::Ascending) {
        let (local_query_id, query_info) = item?;

        // Get the Neutron query ID for this query
        let neutron_id = match query_info.query_type {
            QueryType::KV { ref key, ref path } => helpers::get_registered_query_id_full(
                deps.as_ref(),
                &query_info.connection_id,
                (
                    NeutronQueryType::KV,
                    &QueryPayload {
                        data: key.clone(),
                        path: path.clone(),
                        prove: true,
                    },
                ),
            ),
            QueryType::TX { height, ref hash } => helpers::get_registered_query_id_full(
                deps.as_ref(),
                &query_info.connection_id,
                (
                    NeutronQueryType::Tx,
                    &QueryPayload {
                        data: hash.clone().into_bytes(),
                        path: format!("tx/height/{}", height),
                        prove: false,
                    },
                ),
            ),
        };

        // Check if this query matches the Neutron query ID
        match neutron_id {
            Ok(id) if id == query_id => {
                found_query_id = Some(local_query_id);
                break;
            }
            _ => continue,
        }
    }

    // If we found a matching query, process the result
    if let Some(query_id) = found_query_id {
        // Make sure we have a result
        if let Some(result) = kv_results.result {
            // Create and store a query result
            let query_result = QueryResult {
                id: query_id.clone(),
                result_type: ResultType::KV {
                    value: result.value,
                    height: result.revision_height,
                },
            };

            QUERY_RESULTS.save(deps.storage, query_id.clone(), &query_result)?;

            return Ok(Response::new()
                .add_attribute("action", "process_kv_result")
                .add_attribute("query_id", query_id));
        }
    }

    // If we get here, we either didn't find a matching query or there was no result
    Ok(Response::new()
        .add_attribute("action", "process_kv_result")
        .add_attribute("status", "no_matching_query_or_no_result")
        .add_attribute("neutron_query_id", query_id.to_string()))
}

// Process TX query results from the Neutron chain
fn process_tx_result(
    deps: DepsMut,
    query_id: u64,
    tx_results: neutron_sdk::interchain_queries::types::TxResult,
) -> Result<Response, ContractError> {
    // Find the query in our storage that corresponds to this Neutron query ID
    let mut found_query_id = None;

    // Iterate through all registered queries
    for item in INTERCHAIN_QUERIES.range(deps.storage, None, None, cosmwasm_std::Order::Ascending) {
        let (local_query_id, query_info) = item?;

        // Get the Neutron query ID for this query
        let neutron_id = match query_info.query_type {
            QueryType::KV { ref key, ref path } => helpers::get_registered_query_id_full(
                deps.as_ref(),
                &query_info.connection_id,
                (
                    NeutronQueryType::KV,
                    &QueryPayload {
                        data: key.clone(),
                        path: path.clone(),
                        prove: true,
                    },
                ),
            ),
            QueryType::TX { height, ref hash } => helpers::get_registered_query_id_full(
                deps.as_ref(),
                &query_info.connection_id,
                (
                    NeutronQueryType::Tx,
                    &QueryPayload {
                        data: hash.clone().into_bytes(),
                        path: format!("tx/height/{}", height),
                        prove: false,
                    },
                ),
            ),
        };

        // Check if this query matches the Neutron query ID
        match neutron_id {
            Ok(id) if id == query_id => {
                found_query_id = Some(local_query_id);
                break;
            }
            _ => continue,
        }
    }

    // If we found a matching query, process the result
    if let Some(query_id) = found_query_id {
        // Make sure we have a result
        if let Some(result) = tx_results.result {
            // Extract the transaction hash
            let tx_hash = if let Some(hash) = result.hash {
                hash
            } else {
                "unknown".to_string()
            };

            // Extract events
            let events = result.tx_result.events.iter().map(|e| {
                Event {
                    r#type: e.r#type.clone(),
                    attributes: e.attributes.iter().map(|a| {
                        Attribute {
                            key: String::from_utf8_lossy(&a.key).to_string(),
                            value: String::from_utf8_lossy(&a.value).to_string(),
                        }
                    }).collect(),
                }
            }).collect();

            // Create and store a query result
            let query_result = QueryResult {
                id: query_id.clone(),
                result_type: ResultType::TX {
                    tx_hash: tx_hash.clone(),
                    height: result.height,
                    tx_result: TransactionResult {
                        code: result.tx_result.code,
                        log: result.tx_result.log,
                        events,
                    },
                },
            };

            QUERY_RESULTS.save(deps.storage, query_id.clone(), &query_result)?;

            return Ok(Response::new()
                .add_attribute("action", "process_tx_result")
                .add_attribute("query_id", query_id)
                .add_attribute("tx_hash", tx_hash));
        }
    }

    // If we get here, we either didn't find a matching query or there was no result
    Ok(Response::new()
        .add_attribute("action", "process_tx_result")
        .add_attribute("status", "no_matching_query_or_no_result")
        .add_attribute("neutron_query_id", query_id.to_string()))
}

// Handle replies from SubMsgs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
    match msg.id {
        REGISTER_QUERY_REPLY_ID => {
            // Just acknowledge the registration
            Ok(Response::new().add_attribute("action", "query_registered"))
        }
        _ => Err(ContractError::CustomError {
            msg: format!("Unknown reply ID: {}", msg.id),
        }),
    }
}

// Query handler
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps<NeutronQuery>, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetQuery { query_id } => to_binary(&query_get_query(deps, query_id)?),
        QueryMsg::GetQueryResult { query_id } => to_binary(&query_get_result(deps, query_id)?),
        QueryMsg::GetDelegatorInfo { query_id } => to_binary(&query_get_delegator_info(deps, query_id)?),
    }
}

// Query for information about a registered query
fn query_get_query(deps: Deps<NeutronQuery>, query_id: String) -> StdResult<GetQueryResponse> {
    let query_info = INTERCHAIN_QUERIES.may_load(deps.storage, query_id.clone())?;

    match query_info {
        Some(info) => Ok(GetQueryResponse {
            query_id,
            registered: true,
            query_type: Some(info.query_type),
        }),
        None => Ok(GetQueryResponse {
            query_id,
            registered: false,
            query_type: None,
        }),
    }
}

// Query for the result of a registered query
fn query_get_result(deps: Deps<NeutronQuery>, query_id: String) -> StdResult<GetQueryResultResponse> {
    let result = QUERY_RESULTS.may_load(deps.storage, query_id.clone())?;

    Ok(GetQueryResultResponse {
        query_id,
        result,
    })
}

// Query for delegator information from a staking query
fn query_get_delegator_info(deps: Deps<NeutronQuery>, query_id: String) -> StdResult<GetDelegatorInfoResponse> {
    // Check if the query exists
    let query_info = match INTERCHAIN_QUERIES.may_load(deps.storage, query_id.clone())? {
        Some(info) => info,
        None => return Ok(GetDelegatorInfoResponse {
            query_id,
            delegator_info: None,
        }),
    };

    // Check if we have a result for this query
    let result = match QUERY_RESULTS.may_load(deps.storage, query_id.clone())? {
        Some(result) => result,
        None => return Ok(GetDelegatorInfoResponse {
            query_id,
            delegator_info: None,
        }),
    };

    // Make sure this is a KV query result
    match result.result_type {
        ResultType::KV { value, .. } => {
            // Try to parse the staking delegation response
            match parse_delegation_response(&value) {
                Ok(amount) => {
                    // Extract delegator and validator from the query type
                    if let QueryType::KV { key, .. } = query_info.query_type {
                        // The key contains the delegator and validator information
                        // We'd need to parse it from the key, but for simplicity,
                        // let's assume we can derive them from the key or use placeholder values
                        
                        // In a real implementation, you'd extract these from the key
                        let delegator = String::from_utf8_lossy(&key[12..key.len()-33]).to_string();
                        let validator = String::from_utf8_lossy(&key[key.len()-32..]).to_string();
                        
                        Ok(GetDelegatorInfoResponse {
                            query_id,
                            delegator_info: Some(DelegatorInfo {
                                delegator,
                                validator,
                                amount,
                            }),
                        })
                    } else {
                        Ok(GetDelegatorInfoResponse {
                            query_id,
                            delegator_info: None,
                        })
                    }
                }
                Err(_) => Ok(GetDelegatorInfoResponse {
                    query_id,
                    delegator_info: None,
                }),
            }
        }
        _ => Ok(GetDelegatorInfoResponse {
            query_id,
            delegator_info: None,
        }),
    }
}

Update lib.rs

Finally, update lib.rs to include all modules:
pub mod contract;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod state;
pub mod queries;

pub use crate::error::ContractError;

Using the Contract

Once deployed, you can use the contract to query delegation information from other chains:
  1. Instantiate the contract
    neutrond tx wasm instantiate <code-id> '{"connection_id":"connection-0"}' \
      --from wallet \
      --label "ICQ Example" \
      --admin $(neutrond keys show wallet -a) \
      --gas-prices 0.025untrn \
      --gas auto \
      --gas-adjustment 1.3 \
      -b block
    
  2. Register a delegation query This will query the delegation amount for a specific delegator and validator on Cosmos Hub:
    neutrond tx wasm execute <contract-address> \
      '{"query_delegation": {"query_id": "my_delegation_query", "chain_id": "cosmoshub-4", "delegator": "cosmos1...", "validator": "cosmosvaloper1...", "update_period": 600}}' \
      --from wallet \
      --gas-prices 0.025untrn \
      --gas auto \
      --gas-adjustment 1.3 \
      -b block
    
  3. Query the delegation information After the query is registered and Neutron’s relayers have fetched the data:
    neutrond query wasm contract-state smart <contract-address> \
      '{"get_delegator_info": {"query_id": "my_delegation_query"}}' \
      --output json
    

Implementing Business Logic with ICQ

The real power of ICQ is the ability to make on-chain decisions based on the state of other blockchains. Here are some examples of what you could build:
  1. Cross-chain collateralized loans
    • Query collateral value on one chain
    • Issue loans on Neutron based on that collateral
  2. Price oracles
    • Query swap pools on other chains
    • Use price information to feed into Neutron DeFi applications
  3. Governance coordination
    • Query governance proposals on other chains
    • Execute actions on Neutron when proposals pass

Security Considerations

When implementing ICQ-based contracts, keep the following in mind:
  1. Data Freshness: ICQ data is updated periodically. Set an appropriate update_period for your use case.
  2. Malicious Relayers: Always verify proofs when using ICQ data for high-value operations.
  3. Reverted State: If the source chain experiences a state reversion, your contract should handle this gracefully.
  4. Gas Costs: Each query registration and update has associated gas costs. Design your contract to minimize unnecessary queries.

Conclusion

Interchain Queries enable powerful cross-chain applications by allowing contracts on Neutron to access state from other chains. This opens up a new design space for DeFi applications, governance mechanisms, and cross-chain coordination. For more information, check out: