Building your own smart contracts has been around since the inception of Web 3, allowing people to build programs to deploy to a blockchain.
The blockchains that deploy smart contracts range from Ethereum to Bitcoin, and beyond. Furthermore, smart contracts work together to make up decentralized applications, which can be developed on frameworks like Truffle, Hardhat, and Embark.
In this article, we will look at how we can develop smart contracts and deploy them to the Terra blockchain network, which is similar to Ethereum.
A few things to note about Terra before reading on:
Also, note that Terra’s consensus is the proof-of-stake algorithm using the Tendermint BFT. This allows holders to stake their tokens as collateral to validate transactions. Rewards are given afterward in accordance with the amount of tokens staked.
It’s also important to know that LUNA is the cryptocurrency of Terra and is used to power the proof-of-stake blockchain. This blockchain offers price stability by expanding algorithmically and reducing supply.
To understand more, I suggest you read the documentation here. Our main focus for this article is how we can deploy our smart contract to this blockchain protocol.
And finally, the Terra blockchain boasts of a strong and vigorous consensus mechanism that finalizes batches of transactions in seconds—faster than Bitcoin and Ethereum.
Let us look at the requirements needed to build and deploy smart contracts using the Terra protocol:
However, don’t worry about the above requirements if you do not have them, we will cover them all. One last requirement, however, needed according to the Terra team, is to have the desire to disrupt/disturb traditional finance.
There are a few things we need to install before we begin. This installation will help us connect to Terra’s local testnet when writing contracts and provide the latest version of terrad
. terrad
works with the Terra core.
You will also need to install Rust if you don’t have it already.
To begin, let’s install Terra Core, which requires us to install Go first. To do this, download Go using this link and verify:
âžś ~ go version go version go1.17 darwin/amd64
Go of version v1.17+, which is required to use Terra Core.
Install Terra Core by cloning the repository from GitHub. Then, check out to the main branch which has the latest release:
$ git clone https://github.com/terra-money/core terra-core $ cd terra-core $ git checkout main
Next, install Terra Core to get terrad
, which will serve as the executable to interact with the Terra node:
$ make install
Then, verify that you have installed it successfully:
$ terrad version --long
Your output should look similar to the following:
name: terra server_name: terrad version: 0.5.12-1-gd411ae7 commit: d411ae7a276e7eaada72973a640dcab69825163f build_tags: netgo,ledger go: go version go1.17 darwin/amd64
LocalTerra will be our testnet to test our smart contracts while developing. Our local testnet consists of the WebAssembly integration. To spin up LocalTerra, you need to have Docker and docker-compose
set up because LocalTerra is containerized:
$ git clone --depth 1 https://www.github.com/terra-money/LocalTerra $ cd LocalTerra
With Docker running in the background, run the following command:
$ docker-compose up
You should get the response below, which are logs:
11:25PM INF Timed out dur=4955.7669 height=5 module=consensus round=0 step=1 terrad_1 | 11:25PM INF received proposal module=consensus proposal={"Type":32,"block_id":{"hash":"54D9C757E9AA84E0F5AAA736E6EED3D83F364A3A62FDC625970539CA81DFA86E","parts":{"hash":"2517579A126AC2BF6EB9EB6274FAE6748D14115C91FC59FE3A2AF5F061A12740","total":1}},"height":5,"pol_round":-1,"round":0,"signature":"AMxXngubsUHyterTZuZsiLgY0olPDpdpgjMIRZ9L59UR9+JngC93xO63yTxwE0kQLp2HdZ99G8M4ATchS7d1CA==","timestamp":"2021-12-16T23:25:00.8000592Z"} terrad_1 | 11:25PM INF received complete proposal block hash=54D9C757E9AA84E0F5AAA736E6EED3D83F364A3A62FDC625970539CA81DFA86E height=5 module=consensus terrad_1 | 11:25PM INF finalizing commit of block hash=54D9C757E9AA84E0F5AAA736E6EED3D83F364A3A62FDC625970539CA81DFA86E height=5 module=consensus num_txs=0 root=84C2F2EF6B7FC8B3ACED8B2B0D2921D649F13CE54C5AB5B032DE988D1392E0FD terrad_1 | 11:25PM INF minted coins from module account amount=226569846uluna from=mint module=x/bank terrad_1 | 11:25PM INF executed block height=5 module=state num_invalid_txs=0 num_valid_txs=0 terrad_1 | 11:25PM INF commit synced commit=436F6D6D697449447B5B32382031303020373220323137203234312038352031363320313520313530203137382031353820323235203133312032343620313538203235322031333420313238203134392031383220323033203131372039382031333420312035382032333720323120333620313534203136203134335D3A357D terrad_1 | 11:25PM INF committed state app_hash=1C6448D9F155A30F96B29EE183F69EFC868095B6CB756286013AED15249A108F height=5 module=state num_txs=0 terrad_1 | 11:25PM INF indexed block height=5 module=txindex terrad_1 | 11:25PM INF Ensure peers module=pex numDialing=0 numInPeers=0 numOutPeers=0 numToDial=10 terrad_1 | 11:25PM INF No addresses to dial. Falling back to seeds module=pex terrad_1 | 11:25PM INF Timed out dur=4975.4085 height=6 module=consensus round=0 step=1 terrad_1 | 11:25PM INF received proposal module=consensus proposal={"Type":32,"block_id":{"hash":"5FE8526C43C0B32BEF011299D67FDA44DBD625E0B69836D175C25C1F914DD06E","parts":{"hash":"BE583EC25B30F52E652FA28DEAB869D98602B3FB82CD0D9C80ADF96A210CC8D4","total":1}},"height":6,"pol_round":-1,"round":0,"signature":"Bx3WaDl3hhR9IkDjXRa+dXkSIK0Tezl07gZhDm4RXyJyHq0oriAkQD23Q9+ly1+cFhGIdKF3hyvH3GcjCNLvAQ==","timestamp":"2021-12-16T23:25:05.823444Z"}
Now, we are connected to the LocalTerra network. The following are the port numbers to connect with:
26657
1317
Rust is what Terra chose to use and write smart contracts with because Rust can compile to WebAssembly and the WebAssembly tooling is well matured and built for Terra.
To install Rust on MacOS or any Linux-like OS, run the following command:
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
If you are using Windows, use this link.
Once successfully installed, we must add the wasm32-unknown-unknown
target for compilation:
$ rustup target add wasm32-unknown-unknown
Lastly, let’s install cargo-generate
to scaffold a CosmWasm smart contract template and cargo-run-script
to optimize our smart contracts:
$ cargo install cargo-generate --features vendored-openssl $ cargo install cargo-run-script
We’re finally done with the installations 🙂!
With our LocalTerra network running and waiting for us, we are ready to write a little smart contract, deploy them, and we are done for the day!
Since we installed cargo-generate
, we can quickly scaffold a working project. This will help us with the folder structure to write our contracts. To do this, use the following command:
$ cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME
For PROJECT_NAME
, you should name it the name of your project. Below is what you should get after running the previous command:
Vectormikes-MacBook-Pro:Projects macbookpro$ cargo generate --git https://github.com/CosmWasm/cw-template.git --name terra-demo 🔧 Generating template ... [ 1/34] Done: .cargo/config [ 2/34] Done: .cargo [ 3/34] Skipped: .circleci/config.yml [ 4/34] Done: .circleci [ 1/34] Done: .cargo/config [ 2/34] Done: .cargo [ 3/34] Skipped: .circleci/config.yml [ 4/34] Done: .circleci [ 5/34] Done: .editorconfig [ 6/34] Done: .github/workflows/Basic.yml [ 7/34] Done: .github/workflows [ 8/34] Done: .github [ 9/34] Done: .gitignore [10/34] Done: .gitpod.Dockerfile [11/34] Done: .gitpod.yml [ 1/34] Done: .cargo/config [ 2/34] Done: .cargo [ 3/34] Skipped: .circleci/config.yml [ 4/34] Done: .circleci [ 5/34] Done: .editorconfig [ 6/34] Done: .github/workflows/Basic.yml [ 7/34] Done: .github/workflows [ 8/34] Done: .github [ 9/34] Done: .gitignore [10/34] Done: .gitpod.Dockerfile [11/34] Done: .gitpod.yml [12/34] Done: Cargo.lock [13/34] Done: Cargo.toml [14/34] Done: Developing.md [15/34] Done: Importing.md [16/34] Done: LICENSE [17/34] Done: NOTICE [18/34] Done: Publishing.md [19/34] Done: README.md [20/34] Done: examples/schema.rs [21/34] Done: examples [22/34] Done: rustfmt.toml [23/34] Done: schema/count_response.json [24/34] Done: schema/execute_msg.json [25/34] Done: schema/instantiate_msg.json [26/34] Done: schema/query_msg.json [27/34] Done: schema/state.json [28/34] Done: schema [29/34] Done: src/contract.rs [30/34] Done: src/error.rs [31/34] Done: src/lib.rs [32/34] Done: src/msg.rs [33/34] Done: src/state.rs [34/34] Done: src 🔧 Moving generated files into: `/Users/macbookpro/Desktop/Projects/terra-demo`... ✨ Done! New project created /Users/macbookpro/Desktop/Projects/terra-demo
Looking at the src/msg.rs
file, we can see three kinds of messages we can send to our smart contract. First, this includes InstantiateMsg
, which sets the state in the smart contract, meaning an initial state must be given to the smart contract when it is spun up.
Second, ExecuteMsg
is a message that executes an action to the change of state, such as posting a message to the blockchain. And finally, QueryMsg
is just as it sounds: it works as a query to the chain, getting data from it.
Let’s see how to use these are used within the code:
use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InstantiateMsg { pub count: i32, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { Increment {}, Reset { count: i32 }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { // GetCount returns the current count as a json-encoded number GetCount {}, } // We define a custom struct for each query response #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct CountResponse { pub count: i32, }
Before we move to the contract, let’s look at what interface we have in our State
within the src/state.rs
file:
use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::Addr; use cw_storage_plus::Item; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub count: i32, pub owner: Addr, } pub const STATE: Item<State> = Item::new("state");
Our struct, State
, should contain a count
of i32
and the owner
of Addr
. According to Terra, our state is persistent because of Terra’s active LevelDB, which is a key-value storage.
With that in mind, our contract sits in the src/contract.rs
file:
#[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result<Response, ContractError> { let state = State { count: msg.count, 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("count", msg.count.to_string())) }
The instantiate
method expects four arguments, deps
, _env
, info
, and msg
, with their supporting interfaces. We then expect a result which will either be our expected Response
or ContractError
.
Here, we defined our ContractError
in the src/error.rs
file:
use cosmwasm_std::StdError; use thiserror::Error; #[derive(Error, Debug)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), #[error("Unauthorized")] Unauthorized {}, // Add any other custom errors you like here. // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. }
A few other interfaces like Response
were also imported from cosmwasm_std
:
#[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version;
Next, for methods, we also have execute
and query
in our contract:
#[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, _env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result<Response, ContractError> { match msg { ExecuteMsg::Increment {} => try_increment(deps), ExecuteMsg::Reset { count } => try_reset(deps, info, count), } } pub fn try_increment(deps: DepsMut) -> Result<Response, ContractError> { STATE.update(deps.storage, |mut state| -> Result<_, ContractError> { state.count += 1; Ok(state) })?; Ok(Response::new().add_attribute("method", "try_increment")) } pub fn try_reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> { STATE.update(deps.storage, |mut state| -> Result<_, ContractError> { if info.sender != state.owner { return Err(ContractError::Unauthorized {}); } state.count = count; Ok(state) })?; Ok(Response::new().add_attribute("method", "reset")) }
Here, try_increment
, which increases the count state by 1
, and try_reset
, which resets the count state, are functions used in the execute
function.
Lastly, the query
count, which gets the state or information from the storage for us, can be seen executed below:
#[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> { match msg { QueryMsg::GetCount {} => to_binary(&query_count(deps)?), } } fn query_count(deps: Deps) -> StdResult<CountResponse> { let state = STATE.load(deps.storage)?; Ok(CountResponse { count: state.count }) }
We can now build our smart contract to check for errors during compilation so we can fix it. To do that, we run the following:
$ cargo wasm
And just as we installed cargo-run-script
to help optimize our builds, we must now use it:
$ cargo run-script optimize
In the project directory, we should see the code in artifacts/terra_demo.wasm
, which we will upload to LocalTerra shortly. We can now create a local testnet name and node moniker:
$ terrad init --chain-id=<testnet_name> <node_moniker>
This will then prompt you to enter your mnemonic:
$ terrad keys add <account_name>
According to Terra, the account with the below mnemonic is the only validator on the network:
satisfy adjust timber high purchase tuition stool faith fine install that you unaware feed domain license impose boss human eager hat rent enjoy dawn
Next, we can upload the code to the Terra network:
terrad tx wasm store artifacts/terra_demo.wasm --from demo --chain-id=localterra --gas=auto --fees=100000uluna --broadcast-mode=block
This will ask for permission in which you should type “yes” to allow it to push to LocalTerra. If successful, your contract is now broadcast to the LocalTerra network.
We have just begun to scratch the surface of smart contracts in Terra, and this should only give an overview of how to build a dApp on the Terra protocol.
Building on this protocol is highly recommended because of its active growing user base and Terra stablecoins, including LUNA, are being added into payment solutions which is good news.
It also has fast and efficient consensus with more efficiency coming in future releases.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowJavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
One Reply to "Developing Terra smart contracts"
Hi.
I would like to connect to Terra Classic with javascript via browser but I can’t find any working source code… Can you show me a working javascript example, pls?