Part 1: Minimal Application
Overview
Everyone knows how to write smart contracts for Ethereum. Some people even know how to write smart contracts for Solana. But the knowledge of how to write smart contracts for Cosmos SDK chains is sacred, shared by the few, and hard to obtain.
This CosmWasm onboarding tutorial has one goal: to show the reader that building applications with CosmWasm is not scary.
In Part 1 of this tutorial you will learn how to:
- Create a simple, yet functional smart contract using CosmWasm.
- Deploy your web3 application locally and on the Neutron testnet.
What are Neutron smart contracts?
We assume you might not know anything about Neutron, so let's start with a basic overview. If you're already familiar with Cosmos chains, you can skip this section. We've tucked the details into collapsible sections to make it less scary.
Important information: modules and messages
Neutron is a Cosmos SDK chain, which means that it's a collection of modules
(bank
, dex
, etc.) running on top of the CometBFT consensus. Each module
defines a set of messages that it can process. A single transaction can include multiple messages to more than
one module.
To interact with Neutron, you need to send messages to Neutron modules. For example, if you want to send
some untrn
to your friend, you will need to execute a transaction containing
an MsgSend
message to the bank
module. This message might look like this:
{
"@type": "/cosmos.bank.v1beta1.MsgSend",
"from_address": "neutron1cvsh2c2vasktkh7krt2w2dhyt0njs0adh5ewqv",
"to_address": "neutron1vqwe5hda0sjn0tyhd2w2trk7hktksav2c2hsvc",
"amount": [
{
"denom": "untrn",
"amount": "42"
}
]
}
As a user, usually you don't have to deal with raw messages yourself — it's done for you either by the CLI or by a UI. You'll examples of using the CLI later on in this document.
Important information: smart contracts
Smart contracts are enabled on Neutron by a module called wasmd. As any other
module, wasmd
defines a set of messages that it can process, e.g.: MsgStoreCode
(used to upload compiled contract
binaries), MsgExecuteContract
(used to execute existing contracts), etc.
For now, the main thing that you need to know about the wasmd
messages is that MsgExecuteContract
includes an
embedded message for a smart contract:
{
"@type": "/cosmwasm.wasm.v1.MsgExecuteContract",
"sender": "neutron1cvsh2c2vasktkh7krt2w2dhyt0njs0adh5ewqv",
"contract": "neutron1a5xz4zm0gkpcf92ddm7fw8pghg2mf4wm6cyu6cgcruq35upf7auslnnfye",
"msg": {
"increase_count": {
"amount": "42"
}
},
"funds": []
}
That's because smart contract developers can define the messages that the contract is able to process.
In the snippet above, the message under the "msg"
key is a message to a smart contract identified by the "contract"
address. If you include the message above in a transaction, the following sequence of events will happen:
- Neutron will identify that the incoming message needs to be sent to the
wasmd
module. - The
wasmd
module will look up the contract binary by its address and load it. - The
wasmd
module will take message under the"msg""
key and will pass it to theexecute()
entrypoint of the contract. - The contract will try to parse the incoming data into the
"increase_count"
message that is defined on the contract level, and will execute the handler associated with it.
Important information: contract lifecycle, contract entry points
The creation of a contract involves three steps:
- First you need to compile the contract binary (more on that in the How to upload a contract and interact with it? section).
- Then you need to upload the contract binary to the chain by sending an
MsgStoreCode
to thewasmd
module, which makes Neutron save the binary under a uniquecode_id
. - Lastly, you need to instantiate a contract from this
code_id
by sending anMsgInstantiateContract
to thewasmd
module, which will pass the instantiation message to the contract'sinstantiate()
entrypoint and create an actual contract address that you can interact with.
After a contract was instantiated, you can start to send messages to it using the MsgExecuteContract
of the wasmd
module. Multiple contracts can be created from the same code_id
without the need to re-upload the binary, and each
instance with have a unique address.
There are 3 main contract entry points that you need to know about that are used by the wasmd
module to interact
with a contract: instantiate()
, execute()
and query()
. We are going to implement all of them in this part of our
tutorial.
What does a Neutron smart contract look like?
A really minimal smart contract would be about 10 lines long, and would be useless for our purposes. Our smart contract is going to be a contract that actually does something:
- Keeps a
Uint128
value in the storage. - Allows anyone to increase this value by some amount, if the increase amount is less than
100
. - Allows anyone to query the current value from the storage.
Fun, right? Right. Check out the full source code of this contract on GitHub. Take a look if you enjoy diving into raw source code – we’ve included lots of comments. But don’t worry, we'll walk you through every part of it, step by step, below.
After having a look at the source code, you might have some questions right away, and we will try to address them immediately:
- "How is this minimal? It's 160 lines of code!" Yes, but 50% of them are comments!
- "Wait, is this Rust?" Yes, it's in Rust. Rust is scary, but writing CosmWasm smart contracts is probably the easiest thing you can do with Rust, because everything is single-threaded.
With these initial questions out of the way, you now have a choice:
- You can proceed directly to the How to deploy and execute? section at the end of this document to learn how to use CLI to run a Neutron localnet, deploy our example contract and interact with it,
- Or you can read the How do I write a smart contract for Neutron? section first if you want to understand the inner workings of our example contract.
How do I write a smart contract for Neutron?
Storage: how to I store data?
Use cw_storage_plus::Item
and cw_storage_plus::Map
types to store standard and custom types.
See it in context: link
Storing data is essential, that's why we start with it. Almost any useful contract manages some storage. In CosmWasm, in
order to have storage, you need to initialise it using a type from the cw_storage_plus
package. In our case, it's
Item<Uint128>
:
use cw_storage_plus::Item;
pub const COUNTER: Item<Uint128> = Item::new("counter");
This will make CosmWasm allocate some space in the persistent storage for a single Uint128
value under the
"counter"
key. As a smart contract developer, you don't care about this key at all, but you must make sure that
each storage item has a unique key.
Saving single integers is kind of lame, but don't worry: you can save almost anything to storage. For example, you
can save vectors: Item<Vec<Uint128>>
.
Or you can save the types that you created yourself (just make sure you added the fancy derive macro to the type declaration):
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct Config {
pub important_parameter: String,
}
pub const CONFIG: Item<Config> = Item::new("config");
For maps, cw_storage_plus
has a special Map
type, in which you can also map pretty much anything to anything, if it
serialises properly:
pub const EXAMPLE_MAP: Map<Uint128, Uint128> = Map::new("example-map");
More storage types:
cw_storage_plus
has even more storage types, some of which allow you to track the height at which a certain value was saved to your storage item.
A note on project layout: usually all storage items and storage types are defined in a separate file (
src/state.rs
), alongside thesrc/contract.rs
file. Here we define everything in one place for the sake of simplicity.
Instantiation: how do I initialise a contract?
Implement the instantiate()
entrypoint and the InstantiateMsg
message to have custom instantiation logic for your
contract.
See it in context: link
InstantiateMsg
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct InstantiateMsg {
initial_value: Uint128,
}
In the snippet above, 2 things happen: the definition of InstantiateMsg
, and the implementation of the instantiate()
entry point.
InstantiateMsg
can carry any information we might find useful while populating our new contract. In our
case, we decided to use InstantiateMsg
to set the initial value of the COUNTER
storage item that was initialised in
the previous section.
Note: You need to add the
#[derive(Serialize, <...> JsonSchema)]
derive macro to the definitions of your custom types (so that they can be properly serialised by Rust).
Note: A common practice in the CosmWasm world is to define a
Config
type, create a storage item for it and then to set the initial values forConfig
parameters in theInstantiateMsg
.
A note on project layout: usually all messages are defined in a separate file (
src/state.rs
), alongside thesrc/contract.rs
file. Here we define everything in one place for the sake of simplicity.
instantiate()
entrypoint
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response<NeutronMsg>, ContractError> {
COUNTER.save(deps.storage, &msg.initial_value)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("initial_value", msg.initial_value)
.add_attribute("contract_address", env.contract.address)
.add_attribute("sender", info.sender.to_string()))
}
The instantiate()
entry point expects the following arguments:
- deps: most importantly, gives you access to the storage and the querier (we'll discuss queries later),
- env: keeps information about the execution environment, e.g., the address of the current contract,
- info: keeps information about the message that is currently executed, e.g., the address of the message sender,
- msg: the
InstantiateMsg
that we just defined.
Note:
deps
,env
,info
andmsg
values are provided automatically by thewasmd
module when executing a contract's entrypoint. Most entry points expect a very similar set of arguments, with slight variations.
Our instantiate()
implementation sets the value of the COUNTER
storage item to InstantiateMsg.initial_value
. This
is our first time saving something to storage, which is quite exciting!
Note: Reading and writing to storage consumes gas, which costs money.
Finally, in our instantiate()
implementation, we add attributes to the successful Response
. This helps with
debugging (we'll cover this in the last section of Part 1). Adding attributes to your response acts like a form of
logging. Alternatively, we could just return Ok(Response::new())
, and that would work perfectly fine.
Important information: errors and errors handling
The return type of our entrypoint is Result<Response<NeutronMsg>, ContractError>
. In simple terms, this means that
it can either return a valid Response
or a ContractError
.
If this was really a minimal example, we would not define our own error type, and would simply
return Err(StdError::generic_err("error message"))
in case of an error. However, in most cases you want to define
contract-specific errors, which we will do in the next section.
You might not have noticed it, but in our implementation of the instantiate()
entrypoint we do some minimal error
handling!
Storage operations can potentially fail (although they usually succeed). In our implementation, if the .save()
call
fails for whatever reason, we immediately propagate the error by putting the ?
operator at the end of the .save()
call. It's also possible to handle errors manually using Rust's match
operator (
see Rust documentation).
Processing messages: how do I make a contract do something?
Define all possible messages that your contract needs to process, implement handlers for each of those messages, and
match the messages to handlers in the execute()
entrypoint.
See it in context: link
ExecuteMsg
: defining the set of possible actions
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
IncreaseCount { amount: Uint128 },
}
Note:
#[serde(rename_all = "snake_case")]
is used to achieve "standard" JSON representation of messages. E.g., the JSON representation of theIncreaseCount
message will look like this:{"increase_count": {"amount": "42"}}
When you start developing a contract, the first thing that you need to figure out is what actions to you want the contract to perform.
In the snippet above, we define the ExecuteMsg
enum that has a single variant: the IncreaseCount
message, which
a Uint128
amount field. This means that our contract is going to be able to process only one type of message.
Let's say that upon receiving this message, the contract must increase the COUNTER
storage item value by the amount
specified in the message, if the amount
is less than 100. Matching this particular logic to the IncreaseCount
message type is done in the execute()
entrypoint.
A note on project layout: usually all messages are defined in a separate file (
src/msgs.rs
), alongside thesrc/contract.rs
file. Here we define everything in one place for the sake of simplicity.
execute()
entrypoint: defining handlers for messages
pub const MAX_INCREASE_AMOUNT: Uint128 = Uint128::new(100u128);
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
/// Keep access to the StdError, just in case.
#[error(transparent)]
Std(#[from] StdError),
/// We will return this error if the user tries to increment the counter by
/// more than 100. For no particular reason.
#[error("Can not increment by more than 100 (got {amount})")]
InvalidIncreaseAmount { amount: Uint128 },
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
_env: Env, // We don't use Env in our implementation, hence the underscore
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response<NeutronMsg>, ContractError> {
match msg {
ExecuteMsg::IncreaseCount { amount } => execute_increase_amount(deps, info, amount),
}
}
pub fn execute_increase_amount(
deps: DepsMut,
info: MessageInfo,
amount: Uint128,
) -> Result<Response<NeutronMsg>, ContractError> {
// Return the InvalidIncreaseAmount error if the user tries to increase
// by more than 100. We save this value in a well-named constant
// MAX_INCREASE_AMOUNT because we are nice people.
if amount.gt(&MAX_INCREASE_AMOUNT) {
return Err(ContractError::InvalidIncreaseAmount { amount });
}
// We need to increase the counter. Step 1: load the current value.
// This operation consumes gas!
let mut counter = COUNTER.load(deps.storage)?;
// Step 2: add the user value to the value loaded from the storage.
counter += amount;
// Step 3: save the increased amount to the storage.
COUNTER.save(deps.storage, &counter)?;
Ok(Response::default()
.add_attribute("action", "execute_add")
.add_attribute("amount", amount.to_string())
.add_attribute("sender", info.sender))
}
In most contracts, the execute()
entry point doesn’t contain specific logic itself; it simply delegates tasks to the
appropriate handlers based on the message type. That’s exactly what we did in the snippet above: we implemented
the execute_increase_amount()
handler and directed our contract to use it when an IncreaseCount
message is received.
Note: If the incoming message cannot be parsed into any known message type, an error will be returned.
Here are a few important points:
- We set the maximum allowed increase amount using the
MAX_INCREASE_AMOUNT
constant, following best practices. - We defined the
ContractError
type to return custom errors. TheInvalidIncreaseAmount
variant includes theamount
parameter to make the error message more informative. - If the user tries to increase the counter by more than
MAX_INCREASE_AMOUNT
, we return an error. (First time in this tutorial, right? Exciting!) - If the user input is valid, we load the current counter value from storage, increase it by the specified amount, and save the new value back to storage.
- Similar to the
instantiate()
implementation, we add some attributes to the response to facilitate easier debugging.
Querying data: how do I... query data?
Define all possible query messages that your contract needs to process, implement handlers for each of those messages,
and match the messages to handlers in the query()
entrypoint.
See it in context: link
Querying the raw storage data from a smart contract is technically possible, but is not very convenient. If you want
your contract to provide information about it state, you need to (guess what?) implement the QueryMsg
and the
query()
entrypoint. The process is very similar to what we did with the execute()
entrypoint, the only difference
being that in the query()
entrypoint you can not modify the state, and that you don't have the MessageInfo
data in
it.
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
/// A query message to get the current value of COUNTER. The #[returns(Uint128)]
/// derive marco here is required to generate proper JSON schemas for out smart
/// contract.
#[returns(Uint128)]
CurrentValue {},
}
/// We could simply read the Uint128 value from storage and return it as is,
/// but in general it's better to provide a custom response types for your
/// queries.
#[cw_serde]
pub struct CurrentValueResponse {
pub current_value: Uint128,
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
// Similar to execute(), we try to parse msg into any of the known variants of QueryMsg.
match msg {
QueryMsg::CurrentValue {} => query_current_value(deps),
}
}
pub fn query_current_value(deps: Deps) -> StdResult<Binary> {
let current_value = &COUNTER.load(deps.storage)?;
// to_json_binary is a handy helper function from cosmwasm_std that allows you
// to convert any properly defined Rust type to StdResult<Binary>.
to_json_binary(&CurrentValueResponse {
current_value: current_value.clone(),
})
}
A few things to note here:
- Creating custom response types for your queries is not necessary, but is a good practice, even if you only return a single number.
- If our contract was a bit more complicated, we could, of course, create a query message with some query parameters, and process the query taking the query parameters into account.
- You are not limited to only reading your own storage values in the
query()
entrypoint; you can also query other contracts, and even modules, if necessary. We'll teach you how to do it in Part 3 of this tutorial.
A note on project layout: usually all custom response types are defined in a separate file (
src/query.rs
), alongside thesrc/contract.rs
file. Here we define everything in one place for the sake of simplicity.
How to deploy and execute?
Prepare the environment
First of all, you need to install Rust: https://doc.rust-lang.org/cargo/getting-started/installation.html. After installing Rust, you need to add a wasm target for cargo:
rustup target add wasm32-unknown-unknown
Next, install Docker: https://docs.docker.com/engine/install/. You need Docker to build reproducible wasm binaries and to run the localnet.
Create a directory for this tutorial, e.g.:
mkdir neutron-cosmwasm-tutorial-part-1 && cd neutron-cosmwasm-tutorial-part-1
Next, you need to get a neutrond
binary. If you don't have Go installed, install Go 1.21 (https://go.dev/dl/).
Clone the Neutron repository and install the neutrond
binary:
git clone https://github.com/neutron-org/neutron && cd neutron && make install && cd ..
This will put the neutrond
binary to your $GOPATH/bin
. If you don't have $GOPATH/bin
exported yet, export it now
to be able to execute the binary conveniently from anywhere on your machine:
export PATH=$PATH:$GOPATH/bin
Finally, clone the onboarding tutorial repo:
git clone git@github.com:neutron-org/onboarding.git
cd onboarding
Run the localnet
First, install the cosmopark
tool that allows you to run custom Neutron localnet setups (input y
when prompted to
install the package):
npx @neutron-org/cosmopark --help
Next, build the images that are required to run a localnet of Neutron consisting of Neutron itself, Gaia, and IBC relayer and an interchain queries relayer. This might be a bit of overkill for our current purposes, but it's good to know how to run a real world setup:
./dockerfiles/build-all.sh
Then, start the local network:
npx @neutron-org/cosmopark@latest start localnet_config.json
🚀 Starting
{"level":30,"time":1719406723924,"pid":85823,"hostname":"Andreis-MBP","chain":"neutron","msg":"Starting ics chain neutron"}
<...>
🥳 Done
{"level":20,"time":1729261511883,"pid":52118,"hostname":"Andreis-MBP","context":"main","msg":"cosmopark started"}
Note: if you want to shut down the localnet, run
npx @neutron-org/cosmopark@latest stop localnet_config.json
.
Now you have several containers running on your local machine. One of them is main-neutron_ics-1
, which is the
container running Neutron.
In order to execute any messages, you need to import a key using one of the localnet mnemonics:
neutrond keys add demowallet1 --recover
> Enter your bip39 mnemonic
> kiwi valid tiger wish shop time exile client metal view spatial ahead
- address: neutron13nfu3ct5xkr0vlswgk3gl9zazp7zan88edz67j
name: demowallet1
pubkey: '{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AqVGLu0hlIfruwPYSddOyDmiy7a2kZ0mJ3Qan8vwzXak"}'
type: local
Note: you can find more demo mnemonics in the
localnet_config.json
file.
Important note: you can also import your account from a Ledger by running
neutrond keys add my_ledger_account --ledger
. Your private key won't be transferred, obviously; to use an account created this way to execute transactions you will always need to specify the--ledger
flag in addition to the flags normally provided to a command.
To make sure that everything works well, query the balance of your account:
neutrond q bank balances neutron13nfu3ct5xkr0vlswgk3gl9zazp7zan88edz67j --node tcp://0.0.0.0:26657
balances:
- amount: "999975000"
denom: untrn
pagination:
next_key: null
total: "0"
Compile the contract binary
Go to the minimal_contract
project directory in the onboarding
repository:
cd onboarding/minimal_contract
Then build the contract binary:
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
--platform linux/amd64 \
cosmwasm/optimizer:0.16.0
cd ..
You will find the compiled binary in minimal_contract/artifacts/minimal_contract.wasm
.
Upload the contract
First, you need to upload the contract binary (copy the txhash
value from the last line of the command output!):
neutrond tx wasm store minimal_contract/artifacts/minimal_contract.wasm \
--node tcp://0.0.0.0:26657 --chain-id ntrntest --gas 3000000 \
--fees 10000untrn --from demowallet1
Let's see what the command arguments stand for here, because it's very important for your general understanding of how Neutron works:
minimal_contract/artifacts/minimal_contract.wasm
: path to the compiled contract binary.--node tcp://0.0.0.0:26657
: address of the Neutron node's RPC endpoint. This particular port is exposed by themain-neutron_ics-1
container that we started in the previous section; for mainnet RPC providers, please visit the mainnet chain registry; for testnet RPC providers, please visit the testnet chain registry.--chain-id ntrntest
: the chain identifier; any running Cosmos chain has an identifier that you need to provide for alltx
commands. We gave this identifier to our localnet in thelocalnet_config.json
file. You can find the mainnetchain_id
here, and the testnetchain_id
here.--gas 1500000
: the gas limit for this transaction; if your gas limit is too low, the transaction will fail. If the transaction failed, you can query the transaction details (shown below) to see what amount of gas was actually consumed by the transaction.--fees 4000untrn
: the amount that you are ready to pay for executing the transaction; if your fee is too low, the transaction will fail. If the transaction failed, you can query the transaction details (shown below) to see what fee was actually required to execute the transaction.--from demowallet1
: the account that you are using to sign this transaction.
Next, you need to get the code_id
of the binary that you just uploaded:
neutrond q tx 855C2F0D3E120D986B65EB250BBB3C24ED38F7251E928F03DB96AF8186C00973 --output json \
--node tcp://0.0.0.0:26657 | jq ".events[8]"
{
"type": "store_code",
"attributes": [
{
"key": "code_checksum",
"value": "5aca867b2af7295c7ff224dd62605a8f1601ccee73161ed41e4c43034327f021",
"index": true
},
{
"key": "code_id",
"value": "20",
"index": true
}
]
}
You can see in the output above that the code_id
of our contract binary is 19
. Now that we know it, we can finally
instantiate our contract (once again, copy the txhash
value):
neutrond tx wasm instantiate 20 '{"initial_value": "42"}' --label minimal_contract \
--no-admin --node tcp://0.0.0.0:26657 --from demowallet1 --chain-id ntrntest \
--gas 3000000 --fees 10000untrn
Lets have a look at the transaction arguments and flags once again:
20
: thecode_id
that we got after uploading our compiled binary. As we mentioned previously, you can instantiate multiple identical contracts from onecode_id
!'{"initial_value": "42"}'
: that's ourInstantiateMsg
that we defined in our contract. If we provided a JSON that could not be parsed intoInstantiateMsg
, we would receive an error.--label minimal_contract
: every CosmWasm contract needs to have a user-defined label.--no-admin
means that we created our contract without an admin address. The admin address can migrate a contract.
Now, we need to query the transaction details to get the address of the instantiated contract:
neutrond q tx B45E9D20A5744A81C2C3B0E75D0E740E56E0E0FD20206DDFC77F6FCFE11333B8 --output json --node tcp://0.0.0.0:26657 | jq ".events[8]"
{
"type": "instantiate",
"attributes": [
{
"key": "_contract_address",
"value": "neutron1nyuryl5u5z04dx4zsqgvsuw7fe8gl2f77yufynauuhklnnmnjncqcls0tj",
"index": true
},
{
"key": "code_id",
"value": "20",
"index": true
}
]
}
Congratulations! The contract is now instantiated, and is ready to process our messages.
The address of your contract might be different from what you see in this tutorial. Make sure that you are replacing the addresses from the commands below with the address of your contract!
Interact with the contract
Now that the contract is instantiated and we know its address, let's first see whether what that current value of the counter is:
neutrond q wasm contract-state smart neutron1nyuryl5u5z04dx4zsqgvsuw7fe8gl2f77yufynauuhklnnmnjncqcls0tj \
'{"current_value": {}}' --output json --node tcp://0.0.0.0:26657
{"data":{"current_value":"42"}}
Important note: we have the number as a string here because that's how
Uint128
is represented (it's a potentially very big number). When usingUint128
value in JSON, you need to provide them as strings.
The current value is 42
, which means that our instantiate message did its job. Let's now increase the value by
1
by sending an IncreaseCount
message to it:
neutrond tx wasm execute neutron1nyuryl5u5z04dx4zsqgvsuw7fe8gl2f77yufynauuhklnnmnjncqcls0tj \
'{"increase_count": {"amount": "1"}}' --node tcp://0.0.0.0:26657 --from demowallet1 \
--chain-id ntrntest --gas 1500000 --fees 4000untrn
neutron1nyuryl5u5z04dx4zsqgvsuw7fe8gl2f77yufynauuhklnnmnjncqcls0tj
: the address of our instantiated contract.'{"increase_count": {"amount": "1"}}'
: the JSON representation of theExecuteMsg::IncreaseCount
message that we defined in our contract.
If we query the contract once again, we'll see that the current value was increased by 1:
neutrond q wasm contract-state smart neutron1nyuryl5u5z04dx4zsqgvsuw7fe8gl2f77yufynauuhklnnmnjncqcls0tj \
'{"current_value": {}}' --output json --node tcp://0.0.0.0:26657
{"data":{"current_value":"43"}}
Conclusion
In this first part of the tutorial, you learned how to create a simple, yet functional smart contract using CosmWasm, and to deploy and interact with it locally. In the second part of the tutorial, you will learn how to interact with Neutron modules and other smart contracts.