Try   HackMD

🧪 Smart Contracts development on Osmosis

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.


📝 Table of Contents


🧪 Preparing your Labs


🏁 Our Goal

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)


🪚 Let's Install it!

Before installing LocalOsmosis you'll need to installed its prerequisites first. Let's look into it one-by-one now!

🐳 Docker & docker-compose

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

🤓 Node.js

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!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

⚙️ Rust

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

🧪 LocalOsmosis

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.

https://get.osmosis.zone/

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

⚗️ Beaker

(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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


Automatic install

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.


🧪 Initialize your smart contract


🏁Our goal

In this chapter, we'll scaffold the example project and take a quick tour of the project structure and it's files.


🚧Scaffolding your project

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

For this tutorial, we want to see the example so we'll choose the counter-example choice.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.


🕵️Project overview

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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.

contract.rs

This file content is related to the main logic of the smart contract itself.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

In this example, we'll see 3 important parts

  • instantiate function: This function will handle the instantiation of our smart contract state
  • execute function and execute module: These parts will process the execution message.
    • function: do the switch-case for the message type and call the corresponding function specify within the execute module.
    • module: implement the functios that will be called by execute function.
  • query function and query module: These parts will process the query message.
    • function: do the switch-case for the message type and call the corresponding function specify within the query module.
    • module: implement the functios that will be called by 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.

msg.rs

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
    There's also another type of message, the Migration message which we'll not cover in this beginner level tutorial.
    Every message types specify in this file here do the same thing, serve as an API specification for the message - what data to send in order to interact with the smart contract and what data the client should be receiving from the contract.

state.rs

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 integer
  • owner data which is the wallet's address of the owner

📝Create Todo List Smart Contract


🏁Our goal

In this chapter, we'll edit the example project and initialize our own smart contract with "API Specification"


Initialize your project

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.


Editing msg.rs file

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:

  • id
  • title
  • due_date
  • is_done

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.

  • Add
  • Remove
  • Update
  • Reset
    For Add, Remove, and Update message, we'll also need to supply the todo as a parameter of the function as well.

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.


📝Implementing Smart Contract Logic

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


🏁Our goal

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.


Edit state.rs file

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:

  • todos: a vector (or list) that store your todo items
  • owner: your wallet address, we'll use this to validate user later on when we edit contract.rs file

Edit contract.rs file

Instantiate function

This 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 {&nbsp; } => {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"))
}

Query function and module

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.


🚀 Deploying smart contract

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

We'll do it 2 ways: localosmosis and testnet with beaker.

Localosmosis

Setup

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

Deployment with osmosisd

Compile

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.

Initialize the wallet

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.

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.

Deployment with Beaker

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": [] }'

Testnet with Beaker

This part we'll talk about how to deploy your contract on the testnet with proposal using Beaker.

  • Disclaimer: The testnet DO NOT required the proposal, but the mainnet DOES required. To deploy to the testnet without proposal, you can check out the tutorial here.

Preparing proposal file

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&nbsp;
        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

Deployment

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.

Voting

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.


🧑‍💻 Implementing Frontend to Interact with Our Smart Contract

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

🏁Our goal

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.


Prerequisite

Chrome browser and Keplr extension

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.

Get project starter files on GitHub

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.


Starter frontend overview

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.)

api folder

This folder contains the smart contract interaction file. We'll have the counter.ts file to look as an example for our own contract.

components folder

This folder contains the component AddTodoItem.tsx and TodoItem.tsx. We'll edit these files later on to use our smart contract API.

interfaces folder

This folder contains a single file called todo.ts that specify the interface for a Todo item.

pages folder

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.


Create smart contract API

Learning from counter.ts example

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.

Implementing todo.ts

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.

Changing contract address

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

Edit component to use newly implemented smart contract

We'll be editing 2 files to use the newly implement

pages/index.ts

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 component
<AddTodoItem addTodo={add} setLoading={setLoading} />
  • TodoItem component
<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;

AddTodoItem.tsx

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.