How To
Working Example Contract: For a complete, working implementation, see the neutron_interchain_queries example contract in the neutron-sdk repository. Many developers find it easier to learn from working code.
How to choose the right IBC connection ID for an Interchain Query and verify it
This guide explains how to identify and verify an IBC connection between Neutron and CosmosHub.
1. Find an existing IBC connection using an explorer
Visit the map of zones. You may find multiple connections between the two chains. For Neutron and CosmosHub, we'll use connection-0 (this is the connection ID on the Neutron side).
2. Pick a Neutron RPC node from the chain registry
Go to Neutron's chain registry page, choose an RPC node from the apis section, and use it in subsequent neutrond queries with the --node flag.
3. Gather Neutron-side information about the chosen IBC connection
Retrieve the IBC client ID and counterparty details for connection-0.
Show code
neutrond q ibc connection end connection-0 --node https://rpc-voidara.neutron-1.neutron.org
connection:
client_id: 07-tendermint-0
counterparty:
client_id: 07-tendermint-1119
connection_id: connection-809
...
4. Check the counterparty chain ID for the Neutron-side IBC connection
Ensure that the counterparty chain ID of the Neutron-side IBC client matches the CosmosHub chain ID.
Show code
neutrond q ibc client state 07-tendermint-0 --node https://rpc-voidara.neutron-1.neutron.org
client_state:
...
chain_id: cosmoshub-4 << matches the CosmosHub chain ID
...
5. Pick a CosmosHub RPC node from the chain registry
Visit CosmosHub's chain registry page, select an RPC node from the apis section, and use it in gaiad queries with the --node flag.
6. Verify that CosmosHub's counterparty corresponds to Neutron
Using the counterparty information from Step 3, confirm that CosmosHub's IBC connection and client details match Neutron's information.
Show code
gaiad q ibc connection end connection-809 --node https://cosmoshub.tendermintrpc.lava.build:443
connection:
client_id: 07-tendermint-1119 << matches the third step's connection.counterparty.client_id
counterparty:
client_id: 07-tendermint-0 << matches the third step's connection.client_id
connection_id: connection-0 << matches the third step's connection-id query parameter
gaiad q ibc client state 07-tendermint-1119 --node https://cosmoshub.tendermintrpc.lava.build:443
client_state:
...
chain_id: neutron-1 << matches the Neutron chain ID
...
By following these steps, you can ensure that the IBC connection ID you choose is valid and correctly corresponds to the intended chains.
Might be interesting:
How to find out what transaction filter to use
Imagine you need your Interchain Query-based smart contract to track undelegations made by cosmos17s3uhcvrwrsp2ldjvxp8rseyc3ulpchdry87hp on CosmosHub.
1. Find the up-to-date source code of the staking module
- Locate the current version of the
stakingmodule used by CosmosHub. Check the chain registry to find the repository and version in use, e.g.,v21.0.0. - In the
gaiarepository forv21.0.0, locate the cosmos-sdk import in thego.modfile, e.g.,v0.50.9. In this case, thecosmos-sdkversion is replaced with a special release v0.50.9-lsm - Access the
stakingmodule's source code in the cosmos-sdk with tag v0.50.9-lsm.
2. Find the handler managing undelegations
Identify the Undelegate handler in the staking module's keeper.
3. Locate the events emitted during undelegations
Examine the event emission section of the Undelegate handler code.
4. Create a transaction filter using the events
Match the event type and attributes emitted. For this scenario, use the filter:
unbond.delegator=cosmos17s3uhcvrwrsp2ldjvxp8rseyc3ulpchdry87hp
This corresponds to:
types.EventTypeUnbond.types.AttributeKeyDelegator = cosmos17s3uhcvrwrsp2ldjvxp8rseyc3ulpchdry87hp.
5. Test the query filter
Verify your filter by running the following gaiad q txs query to ensure it retrieves the expected results.
Show code
gaiad q txs --query "unbond.delegator='cosmos17s3uhcvrwrsp2ldjvxp8rseyc3ulpchdry87hp'"
...
txs:
- code: 0
...
height: "22645909"
...
body:
...
messages:
- '@type': /cosmos.staking.v1beta1.MsgUndelegate
amount:
amount: "20045172"
denom: uatom
delegator_address: cosmos17s3uhcvrwrsp2ldjvxp8rseyc3ulpchdry87hp
validator_address: cosmosvaloper1zqgheeawp7cmqk27dgyctd80rd8ryhqs6la9wc
Might be interesting:
How to register and handle a KV Interchain Query
This guide provides a brief guide to registering a KV Interchain Query and handling its results using the neutron-std and neutron-sdk libraries in a smart contract.
1. Find the appropriate helper function in Neutron SDK
Locate the helper function for registering an Interchain Query that suits your requirements in the neutron-sdk. For this example, we'll use the new_register_balances_query_msg function.
If no predefined helper function meets your needs, refer to the How to register a KV Interchain Query with custom keys section.
2. Define the Interchain Query registration entry point
Create an execute message handler in your contract to register the Interchain Query using the helper function as a submessage.
Show code
use neutron_sdk::interchain_queries::v047::register_queries::new_register_balances_query_msg;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: ExecuteMsg,
) -> NeutronResult<Response> {
match msg {
ExecuteMsg::RegisterBalancesQuery {
connection_id,
addr,
denoms,
update_period,
} => register_balances_query(env, connection_id, addr, denoms, update_period),
}
}
pub fn register_balances_query(
env: Env,
connection_id: String,
addr: String,
denoms: Vec<String>,
update_period: u64,
) -> NeutronResult<Response> {
let msg = new_register_balances_query_msg(
env.contract.address,
connection_id,
addr.clone(),
denoms,
update_period,
)?;
// Send the ICQ registration message as a submessage to receive a reply callback
Ok(Response::new().add_submessage(SubMsg {
id: REGISTER_BALANCES_ICQ_REPLY_ID,
payload: to_json_binary(&addr)?,
msg: msg,
gas_limit: None,
reply_on: ReplyOn::Success,
}))
}
3. Define the Interchain Query registration response handler
In the reply handler, decode the submessage result as a MsgRegisterInterchainQueryResponse to access the assigned Interchain Query ID.
Show code
use neutron_std::types::neutron::interchainqueries::MsgRegisterInterchainQueryResponse;
#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> NeutronResult<Response> {
match msg.id {
REGISTER_BALANCES_ICQ_REPLY_ID => {
// decode the reply msg result as MsgRegisterInterchainQueryResponse
let resp = MsgRegisterInterchainQueryResponse::decode(
msg.result
.into_result()
.map_err(StdError::generic_err)?
.msg_responses[0]
.clone()
.value
.to_vec()
.as_slice(),
)?;
// memorize the address that corresponds to the query id to use it later in the
// SudoMsg::KVQueryResult handler.
let addr: String = from_json(&msg.payload)?;
ICQ_ID_TO_WATCHED_ADDR.save(deps.storage, resp.id, &addr)?;
Ok(Response::default())
}
_ => Err(NeutronError::InvalidReplyID(msg.id)),
}
}
4. Define the Interchain Query results processing handler
Retrieve the query result from the interchainqueries module's storage using the query_balance function and process it.
Show code
use neutron_sdk::interchain_queries::v047::queries::query_balance;
use neutron_sdk::sudo::msg::SudoMsg;
#[entry_point]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> NeutronResult<Response> {
match msg {
SudoMsg::KVQueryResult { query_id } => sudo_kv_query_result(deps, env, query_id),
_ => Ok(Response::default()),
}
}
/// The contract's callback for KV query results. Note that only the query id is provided, so you
/// need to read the query result from the state.
pub fn sudo_kv_query_result(deps: DepsMut, env: Env, query_id: u64) -> NeutronResult<Response> {
// Get the last submitted ICQ result from the Neutron ICQ module storage
let balance_resp = query_balance(deps.as_ref(), env.clone(), query_id)?;
// Get the address that was registered for the ICQ
let addr = ICQ_ID_TO_WATCHED_ADDR.load(deps.storage, query_id)?;
// Put your business logic here
// For this example we just preserve the freshly fetched balances in the contract's state
REMOTE_BALANCES.save(deps.storage, addr, &balance_resp.balances)?;
Ok(Response::default())
}
5. Register the Interchain Query
Send a ExecuteMsg::RegisterBalancesQuery message to the contract with the required parameters.
Might be interesting:
- What are entry points and sudo calls?
- Limited gas for sudo calls
- What happens if a sudo callback to a smart contract owning an Interchain Query fails?
How to register and handle a KV Interchain Query with custom keys
If your KV Interchain Query cannot be handled using the helpers from the Interchain Queries related package in neutron-sdk, you can define the KVKeys manually. This example demonstrates registering an Account Interchain Query for cosmos-hub v21.0.0.
1. Figure out the respective data path and model
To determine how the data path is constructed and what the data model is, you need to investigate the module's code. Start by locating the gRPC handler in the module that corresponds to the data you're interested in. This handler provides a clue about where the data is stored and what the data model is.
For this example:
- The store key used at module's keeper initialisation is
acc. - The data path is the accounts store prefix + hex address representation.
- The data model is
BaseAccount.
2. Define Interchain Query registration entry point
To enable Interchain Query registration, implement an execute message handler in your smart contract. This handler will broadcast a MsgRegisterInterchainQuery message as a submessage. Use the data path information derived earlier to configure the message.
Show code
use neutron_std::types::neutron::interchainqueries::MsgRegisterInterchainQuery;
/// Store key for standard **auth** Cosmos-SDK module
pub const AUTH_STORE_KEY: &str = "acc";
/// Storage prefix for account-by-address store
/// <https://github.com/cosmos/cosmos-sdk/blob/853dbbf3e84900214137805d78e325ecd56fd68f/x/auth/types/keys.go#L22-L23>
pub const ACCOUNTS_PREFIX: u8 = 0x01;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: ExecuteMsg,
) -> NeutronResult<Response> {
match msg {
ExecuteMsg::RegisterAccountQuery {
connection_id,
addr,
update_period,
} => register_account_query(env, connection_id, addr, update_period),
}
}
pub fn register_account_query(
env: Env,
connection_id: String,
addr: String,
update_period: u64,
) -> NeutronResult<Response> {
// compose key as accounts store prefix + hex address representation
let mut key: Vec<u8> = vec![ACCOUNTS_PREFIX];
key.extend_from_slice(decode_and_convert(&addr)?.as_slice());
let msg = MsgRegisterInterchainQuery {
query_type: QueryType::KV.into(),
keys: vec![KvKey {
path: AUTH_STORE_KEY.to_string(),
key: key,
}],
transactions_filter: String::default(),
connection_id,
update_period,
sender: env.contract.address.to_string(),
};
// Send the ICQ registration message as a submessage to receive a reply callback
Ok(Response::new().add_submessage(SubMsg {
id: REGISTER_ACCOUNT_ICQ_REPLY_ID,
payload: to_json_binary(&addr)?,
msg: msg.into(),
gas_limit: None,
reply_on: ReplyOn::Success,
}))
}
3. Define Interchain Query registration response handling
In the reply handler, decode the submessage result as a MsgRegisterInterchainQueryResponse to retrieve the assigned Interchain Query ID.
Show code
use neutron_std::types::neutron::interchainqueries::MsgRegisterInterchainQueryResponse;
#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> NeutronResult<Response> {
match msg.id {
REGISTER_ACCOUNT_ICQ_REPLY_ID => {
// decode the reply msg result as MsgRegisterInterchainQueryResponse
let resp = MsgRegisterInterchainQueryResponse::decode(
msg.result
.into_result()
.map_err(StdError::generic_err)?
.msg_responses[0]
.clone()
.value
.to_vec()
.as_slice(),
)?;
// memorize the address that corresponds to the query id to use it later in the
// SudoMsg::KVQueryResult handler.
let addr: String = from_json(&msg.payload)?;
ICQ_ID_TO_WATCHED_ADDR.save(deps.storage, resp.id, &addr)?;
Ok(Response::default())
}
_ => Err(NeutronError::InvalidReplyID(msg.id)),
}
}
4. Implement reconstruction of the query result
Define how the query result should be reconstructed from StorageValue into a BaseAccount instance. This involves decoding the stored values into the appropriate data structure.
Show code
use neutron_sdk::interchain_queries::types::KVReconstruct;
use neutron_std::types::neutron::interchainqueries::StorageValue;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
/// A structure that can be reconstructed from **StorageValues**'s for the **Account Interchain Query**.
pub struct BaseAccount {
pub address: String,
pub account_number: String,
pub sequence: String,
}
impl KVReconstruct for BaseAccount {
fn reconstruct(storage_values: &[StorageValue]) -> NeutronResult<BaseAccount> {
if storage_values.len() != 1 {
return Err(NeutronError::InvalidQueryResultFormat(
"storage_values length is not 1".into(),
));
}
// first level value is Any as sdk.AccountI implementation:
// https://github.com/cosmos/cosmos-sdk/blob/853dbbf3e84900214137805d78e325ecd56fd68f/types/account.go#L9-L32
let any_value: Any = Any::decode(storage_values[0].value.as_slice())?;
// second level value is BaseAccount:
// https://github.com/cosmos/cosmos-sdk/blob/853dbbf3e84900214137805d78e325ecd56fd68f/x/auth/types/auth.pb.go#L29-L37
let std_acc: StdBaseAccount = StdBaseAccount::decode(any_value.value.as_slice())?;
Ok(BaseAccount {
address: std_acc.address,
account_number: std_acc.account_number.to_string(),
sequence: std_acc.sequence.to_string(),
})
}
}
5. Define Interchain Query results submission handling
Retrieve the submitted Interchain Query result from the interchainqueries module's storage using the query_kv_result helper function. Handle the result by decoding it and performing your contract's desired logic.
Show code
use neutron_sdk::sudo::msg::SudoMsg;
use neutron_sdk::interchain_queries::queries::query_kv_result;
use crate::types::BaseAccount;
#[entry_point]
pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> NeutronResult<Response> {
match msg {
SudoMsg::KVQueryResult { query_id } => sudo_kv_query_result(deps, query_id),
_ => Ok(Response::default()),
}
}
/// The contract's callback for KV query results. Note that only the query id is provided, so you
/// need to read the query result from the state.
pub fn sudo_kv_query_result(deps: DepsMut, query_id: u64) -> NeutronResult<Response> {
// Get the last submitted ICQ result from the Neutron ICQ module storage and decode it
// as BaseAccount using its KVReconstruct::reconstruct implementation.
let account_resp: BaseAccount = query_kv_result(deps.as_ref(), query_id)?;
// Get the address that was registered for the ICQ
let addr = ICQ_ID_TO_WATCHED_ADDR.load(deps.storage, query_id)?;
// Put your business logic here
// For this example we just preserve the freshly fetched account in the contract's state
REMOTE_ACCOUNTS.save(deps.storage, addr, &account_resp)?;
Ok(Response::default())
}
6. Perform Interchain Query registration
Broadcast a ExecuteMsg::RegisterAccountQuery message to the smart contract with the required parameters.
Might be interesting:
- What are entry points and sudo calls?
- Limited gas for sudo calls
- What happens if a sudo callback to a smart contract owning an Interchain Query fails?
How to register and handle a TX Interchain Query
This guide explains how to register a TX Interchain Query and handle its results using the neutron-std and neutron-sdk libraries in a smart contract.
1. Find out what transaction filter to use
Determine the appropriate tx_search query that matches the transactions you want to process.
The How to find out what transaction filter to use section provides detailed instructions for crafting a transaction filter.
2. Define Interchain Query registration entry point
Implement an execute message handler in your contract to handle Interchain Query registration. Use the MsgRegisterInterchainQuery message. Populate this message using the tx_search query identified in step 1 as the transactions_filter.
Show code
use neutron_std::types::neutron::interchainqueries::MsgRegisterInterchainQuery;
/// Unbond delegator attribute key.
/// https://github.com/cosmos/cosmos-sdk/blob/8bfcf554275c1efbb42666cc8510d2da139b67fa/x/staking/keeper/msg_server.go#L447-L455
const UNBOND_DELEGATOR_ATTR: &str = "unbond.delegator";
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
env: Env,
_info: MessageInfo,
msg: ExecuteMsg,
) -> NeutronResult<Response> {
match msg {
ExecuteMsg::RegisterUndelegationsQuery {
connection_id,
addr,
update_period,
} => register_undelegations_query(env, connection_id, addr, update_period),
}
}
pub fn register_undelegations_query(
env: Env,
connection_id: String,
addr: String,
update_period: u64,
) -> NeutronResult<Response> {
let msg = MsgRegisterInterchainQuery {
query_type: QueryType::TX.into(),
keys: vec![],
// the result filter is unbond.delegator=addr
transactions_filter: to_string(&vec![TransactionFilterItem {
field: UNBOND_DELEGATOR_ATTR.to_string(),
op: TransactionFilterOp::Eq,
value: TransactionFilterValue::String(addr.clone()),
}])
.map_err(|e| StdError::generic_err(e.to_string()))?,
connection_id,
update_period,
sender: env.contract.address.to_string(),
};
Ok(Response::default().add_message(msg))
}
3. Define Interchain Query results submission handling
Implement a handler to process submitted TX Interchain Query results. This includes:
- Decoding the transaction and its messages.
- Performing contract-side verification of submitted TX Interchain Query results.
- Handling the results in your business logic.
Show code
use neutron_sdk::sudo::msg::SudoMsg;
use cosmos_sdk_proto::cosmos::staking::v1beta1::MsgUndelegate;
use cosmos_sdk_proto::cosmos::tx::v1beta1::{TxBody, TxRaw};
const STAKING_UNDELEGATE_MSG_URL: &str = "/cosmos.staking.v1beta1.MsgUndelegate";
#[entry_point]
pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> NeutronResult<Response> {
match msg {
SudoMsg::TxQueryResult {
query_id,
height,
data,
} => sudo_tx_query_result(deps, env, query_id, height, data),
_ => Ok(Response::default()),
}
}
/// The contract's callback for TX query results.
pub fn sudo_tx_query_result(
deps: DepsMut,
_env: Env,
query_id: u64,
_height: Height,
data: Binary,
) -> NeutronResult<Response> {
// Decode the transaction data
let tx: TxRaw = TxRaw::decode(data.as_slice())?;
let body: TxBody = TxBody::decode(tx.body_bytes.as_slice())?;
// Get the registered query by ID and retrieve the delegator address from query's transaction filter
let registered_query: RegisteredQuery = get_registered_query(deps.as_ref(), query_id)?;
let query_tx_filter: Vec<TransactionFilterItem> =
serde_json_wasm::from_str(registered_query.transactions_filter.as_str())?;
let delegator = match &query_tx_filter[0].value {
TransactionFilterValue::String(s) => s.clone(),
_ => {
return Err(NeutronError::Std(StdError::generic_err(
"undelegations transaction filter value must be a String",
)))
}
};
// the contract's side verification of submitted TX Interchain Query results part
let mut new_undelegations: Vec<Coin> = vec![];
for msg in body.messages.iter() {
// Narrow down the messages to only MsgUndelegate ones
if msg.type_url != STAKING_UNDELEGATE_MSG_URL {
continue;
}
// Narrow down the MsgUndelegate messages to only those that match the delegator address
let undelegate_msg = MsgUndelegate::decode(msg.value.as_slice())?;
if undelegate_msg.delegator_address != delegator {
continue;
}
#[allow(clippy::unwrap_used)]
let undelegation_amount = undelegate_msg.amount.unwrap();
new_undelegations.push(Coin {
denom: undelegation_amount.denom,
amount: Uint128::from_str(undelegation_amount.amount.as_str())?,
});
}
// Put your business logic here
// For this example we just preserve the new undelegations in the state
if !new_undelegations.is_empty() {
let mut undelegations = UNDELEGATED_AMOUNTS
.may_load(deps.storage, delegator.clone())?
.unwrap_or_default();
undelegations.extend(new_undelegations);
UNDELEGATED_AMOUNTS.save(deps.storage, delegator, &undelegations)?;
}
Ok(Response::default())
}
4. Perform Interchain Query registration
Broadcast a ExecuteMsg::RegisterUndelegationsQuery message to the contract with the required parameters.
Might be interesting: