First Contract Reply Integration
In a previous section, your manager smart contract sent an asynchronous message to the collection smart contract, in a fire-and-forget manner.
If you skipped the previous section, you can just switch:
- The
my-nameservice
project to itsadd-nft-library
branch. - The
my-collection-manager
project to itscross-contract-query
branch.
And take it from there.
In fact, it is possible for the caller to receive a response from the callee. This is the object of this section. Your name service smart contract is going to return some data after its execution. And your collection manager smart contract is going to receive and emit it.
The use-case
After execution, the collection manager smart contract is going to emit how many tokens exist in the collection after it has executed the latest command. Instead of launching another query to the collection, as it does before returning the message, the manager is going to rely on the collection returning this information. And because the manager has to remain able to work even with collections that don't return this information, you will return default values it some situations.
It uses the reply mechanism of the actor model.
Update my-nameservice
Your name service is a particular implementation of the NFT library. It adds elements but remains compatible with it. Returning some data at the end of the execution preserves compatibility with the NFT library. So go back to the my-nameservice
project.
The returned type
Add the future returned information in src/msg.rs
:
+ use cosmwasm_schema::cw_serde;
use cosmwasm_std::Empty;
...
pub type QueryMsg = Cw721QueryMsg<Option<Empty>, Option<Empty>, Empty>;
+ #[cw_serde]
+ pub struct ExecuteMsgResponse {
+ pub num_tokens: u64,
+ }
Note that:
- You name it neutrally such that it could conceivably be expanded.
- You could have used the library's
NumTokensResponse
, although it could become an issue when expanding with more fields in the future. And its field is named simplycount
, which could be confusing.
Return from execute
With the type defined, you can now add a .data
to the response:
use crate::{
error::ContractError,
- msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
+ msg::{ExecuteMsg, ExecuteMsgResponse, InstantiateMsg, QueryMsg},
}
use cosmwasm_std::{
- entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response,
+ entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
};
...
pub fn execute(
- deps: DepsMut,
+ mut deps: DepsMut,
env: Env,
...
) -> ContractResult {
- Ok(Cw721EmptyExtensions::default().execute(deps, &env, &info, msg)?)
+ let library = Cw721EmptyExtensions::default();
+ Ok(library
+ .execute(deps.branch(), &env, &info, msg)
+ .inspect(|response| assert_eq!(response.data, None))?
+ .set_data(to_json_binary(&ExecuteMsgResponse {
+ num_tokens: library.query_num_tokens(deps.storage)?.count,
+ })?))
}
Note how:
- You make the
deps
mutable. That's to be able to compile, because of the quirks of Rust. Open to a more elegant way. - With
assert_eq!(response.data, None)
, you make sure that the NFT library returned no data. That's an insurance policy against overwriting if and when the library changes in the future. - You do a direct access to
.query_num_tokens
, which returns the convenientNumTokensResponse
. - You serialize the data to binary, as seen many times.
Adjust the unit test
With a minor change to the returned object, you only have to adjust your expected response when testing execute
:
mod tests {
- use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+ use crate::msg::{ExecuteMsg, ExecuteMsgResponse, InstantiateMsg, QueryMsg};
- use cosmwasm_std::{testing, Addr, Binary, Response};
+ use cosmwasm_std::{testing, to_json_binary, Addr, Binary, Response};
...
fn test_execute() {
...
let expected_response = Response::default()
+ .set_data(
+ to_json_binary(&ExecuteMsgResponse { num_tokens: 1 })
+ .expect("Failed to serialize counter"),
+ )
.add_attribute("action", "mint")
...
}
}
Note that:
- After minting once on an empty library, you end up with a single token.
Adjust the mocked-app test
On this test too, you just confirm that the data returned is as expected:
- use cosmwasm_std::{Addr, Event, StdError, Storage};
+ use cosmwasm_std::{to_json_binary, Addr, Event, StdError, Storage};
...
use cw_my_nameservice::{
contract::{execute, instantiate, query},
- msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
+ msg::{ExecuteMsg, ExecuteMsgResponse, InstantiateMsg, QueryMsg},
}
...
fn test_register() {
...
assert_eq!(
received_response.data,
- None,
+ Some(to_json_binary(&ExecuteMsgResponse { num_tokens: 1 })
+ .expect("Failed to serialize counter")),
);
...
}
Intermediate conclusion on my-name-service
This completes this section's changes on my-nameservice
.
At this stage the my-nameservice
project should have something similar to the execute-return-data
branch, with this as the diff.
Using it in the collection manager
Your update of my name service was rather straightforward. Back in my-collection-manager
, you now need to make use of it as part of the reply mechanism. You are going to:
- Define a new data carrying type.
- Define a set of return codes for branching.
- Instruct the system that you expect a reply.
- Add the
reply
entry point. - Adjust and add to your tests.
The expected type
Similarly to the type you defined in my-nameservice
, you define it here a second time. By re-defining it, you avoid having to import my-nameservice
, which would be overkill.
#[cw_serde]
pub struct NameServiceExecuteMsgResponse {
pub num_tokens: u64,
}
Note how:
- The name is pointedly specific as it is indeed copied from
my-nameservice
.
Define the reply codes
Because all replies come through the same reply
entry point, there needs to be a mechanism to distinguish between call types. The library does this by way of an id: u64
. You provide this id
when sending a message that expects a reply, and the CosmWasm module will ensure that the same id
is part of the reply object.
It is in your interest to clearly identify what each value mean. One way to achieve it is with constants. Another, more elegant, way is with an enum that maps to a number:
use crate::{
error::ContractError,
msg::{
- CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg,
+ CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg, NameServiceExecuteMsgResponse,
},
}
...
use cosmwasm_std::{
- to_json_binary, DepsMut, Env, Event, MessageInfo, QueryRequest, Response, WasmMsg, WasmQuery,
+ from_json, to_json_binary, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, QueryRequest,
+ Reply, ReplyOn, Response, StdError, SubMsg, WasmMsg, WasmQuery,
}
...
type ContractResult = Result<Response, ContractError>;
+ enum ReplyCode {
+ PassThrough = 1,
+ }
+
+ impl TryFrom<u64> for ReplyCode {
+ type Error = ContractError;
+
+ fn try_from(item: u64) -> Result<Self, Self::Error> {
+ match item {
+ 1 => Ok(ReplyCode::PassThrough),
+ _ => panic!("invalid ReplyCode({})", item),
+ }
+ }
+ }
...
Note that:
- The name
PassThrough
harks back to itsExecuteMsg
namesake variant, but it need not be. It just so happens to be pertinent in this case. - The unsightly unknown case
_ =>
is extracted in theFrom
implementation so that, when using::from
, you can use a succinctmatch
on the enum that ensure exhaustiveness at compile time. - You panic in the unknown case, instead of returning an error, because this case reveals a developer error, not a user error:
- Either you defined a new id value without creating its corresponding entry in the enum.
- Or the CosmWasm module unexpectedly called your smart contract on
reply
.
- Unlike when developing with the Cosmos SDK, a panic in a CosmWasm smart contract does not stop the consensus dead. That would be too easy a vector of attack. Instead, it reverts the transaction.
- The
= 1
, truly is of typeisize
, notu64
, but at this small scale, there is zero risk of incompatibility.
Adjust execute_pass_through
In the current use case, only the pass-through command expects a reply, so you make the change in execute_pass_through
. Earlier you added a message to the response. Now you have to wrap this message to make it a sub-message, with additional reply information:
...
pub fn execute_pass_through(
...
) -> ContractResult {
...
let onward_exec_msg = WasmMsg::Execute {
...
};
+ let onward_sub_msg = SubMsg {
+ id: ReplyCode::PassThrough as u64,
+ msg: CosmosMsg::<Empty>::Wasm(onward_exec_msg),
+ reply_on: ReplyOn::Success,
+ gas_limit: None,
+ };
let token_count_result =
...
Ok(Response::default()
- .add_message(onward_exec_msg)
+ .add_submessage(onward_sub_msg)
.add_event(token_count_event))
}
...
Note how:
- You set the
id
in theSubMsg
to identify the type of reply, as explained earlier. - The sub-message contains the original message in
.msg
. - With the use of
CosmosMsg::Wasm
, you can already foresee that a sub-message can be used to send a message to something other than another smart contract. It can be sent to another Cosmos module. - You are asking for a reply only in case of a
Success
. That's because you want to add information to the transaction in case of success. In case of failure, you do not care, other than that the system rolls back everything as it is designed to do by default. You can learn more about the different reply cases here. - You can define a gas limit. In fact, if your goal is to add a gas limit to the original message, using a sub-message is the way to go, whether you intend on receiving a reply or not.
Introduce the reply entry point
With the request for a reply prepared, you have to create the entry point proper. This is a new entry point, just like instantiate
and execute
. As in execute
, you want to keep it expandable and readable. Therefore you only keep a match
statement in it:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> ContractResult {
match ReplyCode::try_from(msg.id)? {
ReplyCode::PassThrough => reply_pass_through(deps, env, msg),
}
}
fn reply_pass_through(_deps: DepsMut, _env: Env, msg: Reply) -> ContractResult {
let resp = msg.result.into_result().map_err(StdError::generic_err)?;
let data = if let Some(data) = resp.data {
data.0[2..].to_vec()
} else {
return Ok(Response::default());
};
let value = if let Ok(value) = from_json::<NameServiceExecuteMsgResponse>(data) {
value
} else {
return Ok(Response::default());
};
let event = Event::new("my-collection-manager")
.add_attribute("token-count-after", value.num_tokens.to_string());
Ok(Response::default().add_event(event))
}
Note that:
- It panics if:
- It cannot identify the message
id
. - The data contains less than 2 bytes.
- It cannot identify the message
- It fails with an error if:
- The
msg.result
has an error indicating that the remote execution failed. In practice this should not happen, because you sent the sub-message withReplyOn::Success
.
- The
- On the other hand, it does not fail, but instead returns an
Ok(Response::default())
, if:- The response's data is empty. This is a valid situation, for instance, when the reply comes from a contract that uses an unmodified NFT library, unlike
my-nameservice
. - The response data cannot be deserialized to a
NameServiceExecuteMsgResponse
. This is a valid situation whereby the reply comes from a smart contract that sends something but unknown as of now.
- The response's data is empty. This is a valid situation, for instance, when the reply comes from a contract that uses an unmodified NFT library, unlike
- The binary returned is prefixed with two bytes:
0A
and the number of bytes that follow. This explains the[2..]
operation to get rid of these first 2 bytes. This part could be improved to be more idiomatic to CosmWasm. And perhaps also more performant. - The
.map_err(StdError::generic_err)
is there to convert aString
into aStdErr
so that you can benefit from the automatic conversion on?
. - The new event has a
token-count-after
attribute, as opposed to the previously seentoken-count-before
.
Adjust the execute
unit test
You modified the message returned in execute
, so you need to adjust the corresponding assertions:
mod tests {
- use crate::msg::{CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg};
+ use crate::{
+ contract::ReplyCode,
+ msg::{
+ CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, NameServiceExecuteMsgResponse,
+ },
+ };
use cosmwasm_std::{
...
testing::{self, MockApi, MockQuerier, MockStorage},
- to_json_binary, Addr, Coin, ContractResult, Empty, Event, OwnedDeps, Querier,
- QuerierResult, QueryRequest, Response, SystemError, SystemResult, Uint128, WasmMsg,
- WasmQuery,
+ to_json_binary, Addr, Binary, Coin, ContractResult, CosmosMsg, Empty, Event, OwnedDeps,
+ Querier, QuerierResult, QueryRequest, Reply, ReplyOn, Response, SubMsg, SubMsgResponse,
+ SubMsgResult, SystemError, SystemResult, Uint128, WasmMsg, WasmQuery,
}
...
fn test_pass_through() {
...
let expected_response = Response::default()
- .add_message(WasmMsg::Execute {
- contract_addr: "collection".to_owned(),
- msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"),
- funds: vec![fund_sent],
+ .add_submessage(SubMsg {
+ id: ReplyCode::PassThrough as u64,
+ msg: CosmosMsg::<Empty>::Wasm(WasmMsg::Execute {
+ contract_addr: "collection".to_owned(),
+ msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"),
+ funds: vec![fund_sent],
+ }),
+ reply_on: ReplyOn::Success,
+ gas_limit: None,
})
...
}
}
Note that:
- You reused the expected message but also wrapped it in a sub-message.
Unit-test the reply
With a new entry point, it is worth testing it in isolation. You check that your new reply function returns the expected response. Add a brand-new test:
#[test]
fn test_reply_pass_through() {
// Arrange
let mut mocked_deps_mut = mock_deps(NumTokensResponse { count: 3 });
let mocked_env = testing::mock_env();
let num_tokens = to_json_binary(&NameServiceExecuteMsgResponse { num_tokens: 4 })
.expect("Failed to serialize counter");
let mut prefixed_num_tokens = vec![10, 16];
prefixed_num_tokens.extend_from_slice(num_tokens.as_slice());
let reply = Reply {
id: ReplyCode::PassThrough as u64,
result: SubMsgResult::Ok(SubMsgResponse {
data: Some(Binary::from(prefixed_num_tokens)),
events: vec![],
}),
};
// Act
let contract_result = super::reply(mocked_deps_mut.as_mut(), mocked_env, reply);
// Assert
assert!(contract_result.is_ok(), "Failed to pass reply through");
let received_response = contract_result.unwrap();
let expected_response = Response::default()
.add_event(Event::new("my-collection-manager").add_attribute("token-count-after", "4"));
assert_eq!(received_response, expected_response);
}
Note that:
- There is some trickery to account for the fact that 2 bytes are removed. The test prepends those 2 bytes.
- When unit testing your reply in isolation, you do not need to have had an
execute
beforehand.
Adjust your mocked-app test
A mocked app test gets you closer to how it would behave with the CosmWasm module. In effect, the mocked app will call reply
on your smart contract as necessary. That is, if you compile your smart contract with the reply
function too.
So the updates are minor, plus you do not need to add a test specifically for the reply function.
...
use cw_my_collection_manager::{
- contract::{execute, instantiate},
+ contract::{execute, instantiate, reply},
msg::{ExecuteMsg, InstantiateMsg},
};
...
fn instantiate_collection_manager(mock_app: &mut App) -> (u64, Addr) {
let code = Box::new(
ContractWrapper::new(execute, instantiate, |_, _, _: ()| {
to_json_binary("mocked_manager_query")
- }),
+ })
+ .with_reply(reply),
);
...
}
...
fn test_mint_through() {
...
let expected_manager_event =
Event::new("wasm-my-collection-manager").add_attribute("token-count-before", "0");
result.assert_event(&expected_manager_event);
+ let expected_manager_event =
+ Event::new("wasm-my-collection-manager").add_attribute("token-count-after", "1");
+ result.assert_event(&expected_manager_event);
...
}
...
fn test_mint_num_tokens() {
...
let expected_manager_event =
Event::new("wasm-my-collection-manager").add_attribute("token-count-before", "1");
result.assert_event(&expected_manager_event);
+ let expected_manager_event =
+ Event::new("wasm-my-collection-manager").add_attribute("token-count-after", "2");
+ result.assert_event(&expected_manager_event);
...
}
Note that:
- The
token-count-before
event emitted in theexecute
and thetoken-count-after
inreply
are separate and not merged, although they have the same type. This is how the mocked app is implemented in this version.
Conclusion
Your manager smart contract now:
- Sends a sub-message to another smart contract, with the expectation of a reply.
- Receives and understands the reply, which it uses to emit an event.
You could add more tests to verify that:
- It panics when receiving a bad reply
id
. - I can handle replies with empty data.
This is left as an exercise.
At this stage:
- The
my-nameservice
project should have something similar to theexecute-return-data
branch, with this as the diff. - The
my-collection-manager
project should have something similar to thereply-from-execute
branch, with this as the diff.
You just saw the use of CosmosMsg::Wasm
. In the next section, you use other variants of CosmosMsg
to interact with Cosmos modules.