Welcome to our "Smart Contracts development on Osmosis" course! In this course, you'll learn how to build secure, scalable, and reliable smart contracts using the CosmWasm platform on the Osmosis chain.
You'll learn the fundamentals of CosmWasm development, including how to write and deploy contracts, interact with them, and implement a frontend to interact with your contracts. Plus, you'll get hands-on experience building your own real-world project: a To Do app! This will give you the opportunity to see how all the concepts and techniques you learn come together in a practical setting.
We'll provide both text and video-based versions of the material, so you can learn at your own pace in a way that suits your learning style. So get ready to roll up your sleeves and start building your very own CosmWasm smart contracts – it's going to be a fun and rewarding journey.
Okay! Now we know what Osmosis is and what we are going to build. Let's get start by preparing our workspace on local environment.
Luckily, Osmosis Labs provide us with a wonderful tool called LocalOsmosis which already pre-configured standard environment for developing on Osmosis for us.
For more information about LocalOsmosis please refer to its official documentation here
Also, We're going to install a tool call Beaker which is a tool that's going to help us scaffolding and do a lot of heavy tasks for us when developing smart contracts (think of hardhat for cosmwasm)
Before installing LocalOsmosis you'll need to installed its prerequisites first. Let's look into it one-by-one now!
So, In case you didn’t have Docker install in your local machine already. It's a tool that help containerize a lot of components together. thus, make it easier to work on things.
You can install it by go to this Link
Also, One more thing after you finished installed Docker. Its default resourses limit is pretty small. So it's not bad if we pump that number up a little bit. Here's what I using.
Next, is our beloved Node.js if you have no Node.js installed just head to their official website here
Currently (As I writing this), The LTS version is on NodeJS v16 just get that version if you didn't have it yet!
And now, It's Rust which you may notice we're going to write it in the smart contract. (But it's also used in a lot of place) to install it is really easy.
You can do it by using rustup (docs)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
then do
rustup default stable
rustup target add wasm32-unknown-unknown
``
Also, you need to install the following cargo packages as well
cargo install cargo-generate --features vendored-openssl
cargo install cargo-run-scrip
Alright, Finally here's the time for LocalOsmosis. So actually there's number of ways to install LocalOsmosis in your local machine. The first one is you can use their official command line to install it.
Just check out the link on the top. You can install LocalOsmosis by choose choice 3. and it should be simple and straightforward just follow its instructions and you're good to go!
Another way to do is to clone their repo into your local machine.
git clone https://github.com/osmosis-labs/osmosis/tree/main/tests/localosmosis
Then, You should be able to start, stop, and reset chain using this commands
Start Env:
make start
or make localnet-start
if you're using the git clone one
Stop Env:
make stop
or make localnet-stop
if you're using the git clone one
Stop Env (And remove all data stored on the chain):
make restart
After your LocalOsmosis is run. You should be able to see the process on Docker Desktop with something similar to this.
(I find no emoji for beaker please let me use alembic here 😂)
As we discuss earlier Beaker is a tool to scaffold cosmwasm app. Which is really helpful for us. And make it really to get start developing it.
Learn more about Beaker here
Sooo, Let's just install it now!
cargo install -f beaker
After Beaker installed we should be able to see something like this.
If the above installation process is too hard to follow along, you can also use the automatic setup option as well by doing the following command.
bash <(curl -sL https://get.osmosis.zone/run)
This command will setup everything you need for the smart contract development for you. Including Rust, Beaker, localosmosis command line tool, and additional configurations.
In this chapter, we'll scaffold the example project and take a quick tour of the project structure and it's files.
After you finished the last chapter, you should have Beaker installed by now. To create a new project with Beaker, we'll use the following command.
beaker new <app-name>
for the <app-name>
we'll use counter-dapp
After that, we'll see the following choices.
For this tutorial, we want to see the example so we'll choose the counter-example
choice.
After it has finished running, we'll see the result as follow and we'll also see the newly created project folder.
Great! Now we can start the next part where it gonna be the project overview. Open the project in your favorite text editor or IDE, and proceed to the next part.
In my case, I'm using Visual Studio Code, and in the explorer view you we'll see the following folders and files. In this overview, we'll take a closer look into the contracts
folder. For the frontend
folder, we'll get into detail in the later chapter.
Expanding the contracts
folder, you'll see the counter
subfolder which contains the files for example smart contract. Most of the files directly under this folder is irrelevant for this tutorial, but the src
folder is what we're going to explore it in the detail.
After we've expanded the src
folder, we'll see multiple files that related to the counter smart contract. The important files are: contract.rs
, msg.rs
, and state.rs
. For other files, they're not that important for our tutorial so I'll skip it, but it's pretty much self explanatory by the name.
This file content is related to the main logic of the smart contract itself.
In this example, we'll see 3 important parts
instantiate function
: This function will handle the instantiation of our smart contract stateexecute function
and execute module
: These parts will process the execution message.
execute module
.execute function
.query function
and query module
: These parts will process the query message.
query module
.query function
.The implementation example for instantiate
part is as follow.
The implementation for the execute function
and query function
is mostly identical, so I'll show only the execute function here.
And for the execute module
, it will look something like this.
For the query module
, it will look a little bit different from the former. Since it also return data to the client, the implementation will be like this.
This file content is used to specify the contract message endpoint (which is similar to API specification or schema for the API call).
The message types being shown here have 3 types.
Instantiate
Execute
Query
Migration
message which we'll not cover in this beginner level tutorial.This file content is used to specify the data (or state) that will be store within our smart contract.
For this example, we'll see that the smart contract will store 2 information in the state
count
data which is a 32-bits integerowner
data which is the wallet's address of the ownerIn this chapter, we'll edit the example project and initialize our own smart contract with "API Specification"
You can scaffold the project by following our last tutorial. You could also choose the minimal
option as well but we rather recommended you to choose the counter-example
option to get some general idea of how you should implement specific part of the contract.
After you have initialized your project, we'll also need a new contract to be created. So, you need to run the following command in order to get our Todo contract up and ready to be developed.
beaker new contract todo
The command will scaffold the new contract folder for you. At this point, you could also delete the counter
folder under the contracts
folder or leave it as a reference.
We'll start from msg.rs
file since it's the most simple file to be edited and it's also the starting point for other part of the contract as well.
First, we'll need to specify the struct for our Todo. This will be the representation of Todo data store within our contract.
#[cw_serde]
#[derive(Eq)]
pub struct Todo {
pub id: i32,
pub title: String,
pub due_date: String,
pub is_done: bool
}
As you can see here, our Todo contains the following data:
Next part will be about the instantiate message. We want user to be able to input the initial list of todos into the contract. So the next part of the code will be like this.
#[cw_serde]
pub struct InstantiateMsg {
pub todos: Vec<Todo>
}
For the execute message, we will implement it like so.
#[cw_serde]
pub enum ExecuteMsg {
Add {todo: Todo},
Remove {todo: Todo},
Update {todo: Todo},
Reset {},
}
As you can see, we have multiple execute messages here.
Finally for the query message, we'll have a single function called GetTodos
and with this function, we'll also need to supply the return type as well. The implementation will be as followed.
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(GetTodosResponse)]
GetTodos {}
}
#[cw_serde]
pub struct GetTodosResponse {
pub todos: Vec<Todo>,
}
And that's it. Your msg.rs
file will look something like this.
use cosmwasm_schema::{cw_serde, QueryResponses};
#[cw_serde]
#[derive(Eq)]
pub struct Todo {
pub id: i32,
pub title: String,
pub due_date: String,
pub is_done: bool
}
#[cw_serde]
pub struct InstantiateMsg {
pub todos: Vec<Todo>
}
#[cw_serde]
pub enum ExecuteMsg {
Add {todo: Todo},
Remove {todo: Todo},
Update {todo: Todo},
Reset {},
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(GetTodosResponse)]
GetTodos {}
}
#[cw_serde]
pub struct GetTodosResponse {
pub todos: Vec<Todo>,
}
After you've successfully edited the file. You can proceed to the next chapter.
Learn More →
In this chapter, we'll continue the implementation from last chapter and edit the main logic part for our smart contract. Also, after everything is done, we'll also deploy the smart contract as well.
Since we wanted to store the todos list within our smart contract, we'll need to modify the state.rs
file as followed.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct State {
pub todos: Vec<Todo>,
pub owner: Addr,
}
As you can see here, we have 2 data to store:
contract.rs
fileThis function will be called when instantiate message is sent to smart contract. We'll edit the function as followed.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
_msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let state = State{
todos: _msg.todos,
owner: info.sender.clone()
};
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
STATE.save(deps.storage, &state);
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("owner", info.sender)
.add_attribute("todos",format!("{:?}",state.todos)))
}
Now, to handle the execute message, we'll need an execute
function. This function will handle different execute message type and call the function corresponding to it. The function will be as followed.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Add { todo } => {try_add(deps, info, todo)}
ExecuteMsg::Remove { todo } => {try_remove(deps, info, todo)}
ExecuteMsg::Update { todo } => {try_update(deps, info, todo)}
ExecuteMsg::Reset { } => {try_reset(deps, info)}
}
}
And this part will be the functions. Note that the functions here can also be put within the execute
module (you will see the example in the query function part).
pub fn try_add(deps: DepsMut, info: MessageInfo, todo: Todo) -> Result<Response, ContractError>{
STATE.update(deps.storage, |mut state|->Result<_,ContractError>{
if info.sender != state.owner{
return Err(ContractError::Unauthorized {});
}
state.todos.push(todo);
Ok(state)
})?;
Ok(Response::new().add_attribute("method","try_add"))
}
pub fn try_remove(deps: DepsMut, info: MessageInfo, todo: Todo) -> Result<Response, ContractError>{
STATE.update(deps.storage, |mut state|->Result<_,ContractError>{
if info.sender != state.owner{
return Err(ContractError::Unauthorized {});
}
state.todos.retain(|x| x.id != todo.id);
Ok(state)
})?;
Ok(Response::new().add_attribute("method","try_remove"))
}
pub fn try_update(deps: DepsMut, info: MessageInfo, todo: Todo) -> Result<Response, ContractError>{
STATE.update(deps.storage, |mut state|->Result<_,ContractError>{
if info.sender != state.owner{
return Err(ContractError::Unauthorized {});
}
state.todos.retain(|x| x.id != todo.id); // remove old one
state.todos.push(todo); // push back new one
Ok(state)
})?;
Ok(Response::new().add_attribute("method","try_update"))
}
pub fn try_reset(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError>{
STATE.update(deps.storage, |mut state|->Result<_,ContractError>{
if info.sender != state.owner{
return Err(ContractError::Unauthorized {});
}
state.todos.clear();
Ok(state)
})?;
Ok(Response::new().add_attribute("method","try_update"))
}
Just like the execute
function, the query
function also do the same thing. Handle the query message and make the function call to the corresponding function.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetTodos {} => to_binary(&query_todos(deps)?),
}
}
For this part, the function will be put within the query
module. You can also put this outside of the module just like the execute part as well, but your code might be harder to understand.
pub mod query {
use super::*;
pub fn query_todos(deps: Deps) -> StdResult<GetTodosResponse> {
let state = STATE.load(deps.storage)?;
Ok(GetTodosResponse { todos: state.todos })
}
}
After you've completed all of this, you can deploy your smart contract on the blockchain! Proceed to the next part to learn how to do it.
Learn More →
We'll do it 2 ways: localosmosis and testnet with beaker.
This part we'll talk about how to deploy your contract on the localosmosis (AKA your local machine). You should have localosmosis installed within your computer before proceeding. If not, or you're not installing it with localosmosis option, you could simply do the following:
curl -sL https://get.osmosis.zone/run
then choose the option in the following order.
3, 1, 1, any name you wanted
When it finished running, reset your terminal. You should be able to use osmosisd
commands.
After the installation is completed, you need to start the localosmosis node by running these commands:
cd ~/osmosis
make localnet-start-with-state
Go to folder of your project you've developed earlier and open the terminal on the folder. Run the following command to compile and optimize the smart contract.
sudo docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.6
After this command finished, you can go to artifacts/
folder to see the compiled contract file.
You need to pay the gas fee before you can deploy the smart contract on the blockchain. For the localosmosis, we will use the test wallet provided by the system. In this case, we'll use test1
wallet's seed.
To add the wallet, run the command:
osmosisd keys add test1 --recover
Then paste in the wallet's seed:
notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius
Now you can proceed to the actual deployment.
To deploy, run the command:
cd artifacts
osmosisd tx wasm store todo.wasm --from test1 --chain-id=localosmosis --gas-prices 0.1uosmo --gas auto --gas-adjustment 1.3 -b block -y
And that's it for the localosmosis deployment. You can interact with the contract within your local machine via command line or locally-run application.
Also, take note of your deployment address number. You'll need it later on.
If the above commands is hard to follow along, you could do the equivalent using the Beaker with the following command
beaker wasm deploy todo --signer-account test1 --raw '{ "todos": [] }'
This part we'll talk about how to deploy your contract on the testnet with proposal using Beaker.
We'll create a proposal file, which normally will specify the data about what the contract name is, what does it do, the code repository, etc. So create the file called prop.yml
as a file directly under your project folder. The content of the file here will be a placeholder value. Feel free to modify it to your liking.
title: Proposal to allow DappName to be enabled in Osmosis
description: |
A lengthy proposal description
goes here
we expect this to be many lines...
deposit: 500000000uosmo
code:
repo: https://github.com/osmosis-labs/beaker/
rust_flags: -C link-arg=-s
roptimizer: workspace-optimizer:0.12.6
After the file has been created and saved, you can run the actual deployment command:
beaker wasm proposal store-code --proposal prop.yml --signer-account test1 --network testnet counter --gas 25000000uosmo --gas-limit 25000000
After the deployment is completed. Take note of your proposal_id
. We'll need it to vote the proposal.
To make the deployment is completed, you'll need to vote for it to become active. In the real world scenario, you need multiple people to vote for your smart contract and it take a really long time, but for the testnet, you can vote it yourself and the voting time is short. So you need to act fast.
To vote for your contract, do the following command:
beaker wasm proposal vote --option yes counter --signer-account test1 --network testnet
After the timer has run out (5 minutes), you can check on the testnet explorer to validate your contract status. If you've done everything correctly, you will see that your smart contract is deployed successfully.
In this chapter, we'll taken starter files from GitHub for frontend and take a quick tour of it. Then, we'll edit it from there to create a working frontend that can interacted with our deployed Todo smart contract.
You should have Chrome web browser and Keplr extension installed on the browser. Note that Keplr can only be install on Chrome. If you saw it on other browser, it's probably a fake extension and you SHOULD NOT INSTALL IT.
GitHub Repo
Copy the code from the repository and replace the frontend
folder. Also, don't forget to do the npm install
command to get the frontend ready to be run as well.
After you've done the package installation, you can try running the frontend with the npm run dev
command.
Most of the files are completed, but some of them still need further edit. The lines that need to be edited are marked with TODO
comment. (If you installed the Todo Tree for Visual Studio Code, you'll see it highlighted as well.)
This folder contains the smart contract interaction file. We'll have the counter.ts
file to look as an example for our own contract.
This folder contains the component AddTodoItem.tsx
and TodoItem.tsx
. We'll edit these files later on to use our smart contract API.
This folder contains a single file called todo.ts
that specify the interface for a Todo item.
This is a simple frontend page file with mock Todo data. We'll edit this file later on to use it with the actual smart contract interaction.
import useSWR from "swr";
import { getAddress, getClient, getSigningClient } from "../lib/client";
import { getContractAddr } from "../lib/state";
export const getCount = async () => {
const client = await getClient();
return await client.queryContractSmart(getContractAddr(), { get_count: {} });
};
export const increase = async () => {
const client = await getSigningClient();
return await client.execute(
await getAddress(),
getContractAddr(),
{ increment: {} },
"auto"
);
};
export const useCount = () => {
const { data, error, mutate } = useSWR("/counter/count", getCount);
return {
count: data?.count,
error,
increase: () => mutate(increase),
};
};
From the example, you'll see 2 functions and 1 hook. We have getCount
and increase
function, these will be the main functions that interact with our smart contract.
The getCount
function is used to send the query message to the smart contract, and the increase
function is used to send the execute message to the smart contract. Note that the execute function will need to supply the sender wallet address as a parameter as well.
For the useCount
hook, this is used to "pack" all the functions to export and utilizing the useSWR
hook to handle the data fetching for us.
We'll create the file called todo.ts
to handle the smart contract interaction. Most of the implementation here will follow the same idea of counter.ts
file.
import useSWR from "swr";
import { Todo } from "../interfaces/todo";
import { getAddress, getClient, getSigningClient } from "../lib/client";
import { getContractAddr } from "../lib/state";
export const getTodos = async () => {
const client = await getClient();
return await client.queryContractSmart(getContractAddr(), { get_todos: {} });
};
export const addTodo = async (todo: Todo) => {
const client = await getSigningClient();
return await client.execute(
await getAddress(),
getContractAddr(),
{
"add": {
"todo":todo
}
},
"auto"
);
};
export const updateTodo = async (todo: Todo) => {
const client = await getSigningClient();
return await client.execute(
await getAddress(),
getContractAddr(),
{
"update": {
"todo":todo
}
},
"auto"
);
};
export const resetTodo = async () => {
const client = await getSigningClient();
return await client.execute(
await getAddress(),
getContractAddr(),
{
"reset": {}
},
"auto"
);
}
export const useTodo = () => {
const { data, error, mutate } = useSWR("/todos/todo", getTodos);
return {
todos: data?.todos as Todo[] | undefined,
error,
add: (todo: Todo) => mutate(()=>addTodo(todo)),
update: (todo:Todo) => mutate(()=>updateTodo(todo)),
reset: ()=>mutate(resetTodo)
};
};
As you can see, we have getTodos
to send the query message, and we have addTodo
, updateTodo
, and resetTodo
to send the execute message. Then, we pack up everything within the useTodo
hook.
Remember the contract address from the previous chapter? We'll need it in this part. We'll take the address and paste it into the environment file.
So, you'll want to edit both /.beaker/state.json
and /.beaker/state.local.json
file to use the new contract endpoint. You'll add the contract address to the file like so.
{
"testnet": {
"counter": {
"code_id": <YOUR CODE ID>,
"addresses": {
...,
"todos contract": "<YOUR SMART CONTRACT ADDRESS>"
},
"proposal": {
"store_code": <YOUR STORE CODE ID>
}
}
}
}
Don't worry about the counter
key, it's used here so that you don't need to edit the /lib/state.ts
file code.
Also, since our contract run on the testnet-4, we'll need to edit the .env.local
and .env.production
file as well to change the endpoint.
NEXT_PUBLIC_NETWORK=testnet
NEXT_PUBLIC_RPC_ENDPOINT="https://rpc-test.osmosis.zone/"
NEXT_PUBLIC_CHAIN_ID=osmo-test-4
We'll be editing 2 files to use the newly implement
We'll edit the] file to use the useTodo
hook we've created earlier.
First, we'll want to import useTodo
hook at the start of the file.
import { useTodo } from "../api/todo";
Then, we'll change the todos
constant to use the useTodo
hook like so.
const { todos, add, error, reset, update } = useTodo();
Next, we'll supply the function to our components.
<AddTodoItem addTodo={add} setLoading={setLoading} />
<TodoItem
key={index}
todo={todo}
onClick={async () => {
setLoading(true);
try {
await update({
...todo,
is_done: !todo.is_done,
});
} catch (err) {
console.log(err);
}
setLoading(false);
}}
/>
The end result should look something like this
import type { NextPage } from "next";
import Head from "next/head";
import { useState } from "react";
import { useTodo } from "../api/todo";
import AddTodoItem from "../components/AddTodoItem";
import TodoItem from "../components/TodoItem";
import { Todo } from "../interfaces/todo";
import styles from "../styles/Home.module.css";
const Home: NextPage = () => {
const [isLoading, setLoading] = useState(false);
const [isOpenAddTodo, setIsOpenAddTodo] = useState(false);
const { todos, add, error, reset, update } = useTodo();
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{todos === undefined || isLoading ? (
"Loading..."
) : (
<>
<h1>Todos</h1>
<button
style={{
background: "#38bdf8",
}}
onClick={() => setIsOpenAddTodo(!isOpenAddTodo)}
>
{isOpenAddTodo ? "Close" : "Open"}
</button>
{isOpenAddTodo && (
<AddTodoItem addTodo={add} setLoading={setLoading} />
)}
{todos.length > 0 ? (
<>
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index}
todo={todo}
onClick={async () => {
setLoading(true);
try {
await update({
...todo,
is_done: !todo.is_done,
});
} catch (err) {
console.log(err);
}
setLoading(false);
}}
/>
))}
</ul>
<button
style={{
backgroundColor: "#fca5a5",
}}
onClick={async () => {
setLoading(true);
try {
await reset();
} catch (err) {
console.log(err);
}
setLoading(false);
}}
>
Remove All Todos
</button>
</>
) : (
<p>There is no Todo</p>
)}
</>
)}
</div>
);
};
export default Home;
Since we're using the actual function from useTodo
hook, we'll also need to change the implementation inside this component as well.
On the submit button, we'll modify it like so.
<button
type='submit'
style={{
backgroundColor: "#bae6fd",
}}
onClick={async (event) => {
event.preventDefault();
setLoading(true);
try {
await addTodo({
...todo,
id: Math.floor(Math.random() * 9999),
});
} catch (err) {
console.log(err);
}
setLoading(false);
}}
>
Add Todo
</button>
And if you've complete this file successfully, you will be able to do the npm run dev
again to validate the result.