This tutorial introduces you to interact with All That Node product to deploy Wasm smart contract to Osmosis Testnet (osmo-test-4), and to create a simple clicker game. You might be able to test the things that we are going to build on the following deployed site.
Prerequisites
Sign up to All That Node (ATN) and secure your account
Rust is the main programming language used for CosmWasm smart contracts. While WASM smart contracts can theoretically be written in any programming language, CosmWasm libraries and tooling work best with Rust.
# 1. Set 'stable' as the default release channel:rustupdefaultstable# 2. Add WASM as the compilation target:rustuptargetaddwasm32-unknown-unknown# 3. Install the following packages to generate the contract:cargoinstallcargo-generate--featuresvendored-opensslcargoinstallcargo-run-script
Setup Osmosis Testnet
You can easily set up an Osmosis Testnet environment using the Osmosis Installer.
Run the following and choose option #2 (Client Node) and #2 (Testnet) in order.
Now you have successfully completed setting up an Osmosis client node in Testnet. In order to use osmosisd from the cli, either reload your terminal or refresh your profile with : ‘source ~/.profile’
Step 1: Faucet - Fund your Osmosis Account
Create Wallet
To begin, use the following command to generate a wallet for deployment.
osmosisdkeysaddwallet
Faucet using AllThatNode
To receive a free airdrop from Osmosis Testnet for testing purposes, you must first join Osmosis Discord. Link Meanwhile, utilizing the AllThatNode faucet makes your developer life much easier. Go to this link, enter your wallet address, and then click the Claim Your Tokens button. If you succeed, you may be able to obtain a transaction value.
The address of the wallet can be found through the osmosisd keys show -a wallet .
After requesting the faucet, use the command below to check the balance.
To deploy smart contracts, you must compile the code and make it an executable wasm binary file. We will compile the wasm contract with stable toolchain.
Compile using the command below:
# Set 'stable' as the default release channel:rustupdefaultstablecargowasm
After this compiles, it should produce a file in target/wasm32-unknown-unknown/release/my_first_contract.wasm. If you check the size of the file by using the ls -lh command, it shows around 1.8M. This is a release build, but not stripped of all unneeded code. To produce a much smaller version, you can run this which tells the compiler to strip all unused code out:
RUSTFLAGS='-C link-arg=-s'cargowasm
This produces a file about 155K. To reduce gas costs, the binary size should be as small as possible. This will result in a less costly deployment, and lower fees on every interaction.
Also, if you don’t use compilation optimization, CosmWasm smart contract will not be deployed well due to exceeds limit error.
Optimized Compilation
You can do further optimization using rust-optimizer. rust-optimizer produces reproducible builds of CosmWasm smart contracts and does heavy optimization on the build size, using binary stripping and wasm-opt.
Binary file will be at artifacts/my_first_contract.wasm folder and its size will be about 130K, which is more smaller than when only RUTFLAGS was used.
Step 3: Deploying a contract
Uploading a binary code
We require the RPC Endpoint address of the Osmosis Testnet in order to distribute the produced Wasm binary code, and we will utilize the reliable RPC endpoint given by All That Node (ATN).
To begin, navigate to the protocol's Osmosis page and choose New Project to establish the project that will get the API key.
Then you can see the projects on DASHBOARD that you have just created. To enter, click the Osmosis tab.
The API key and Testnet endpoint may then be verified as follows:
The $NODE variable contains the RPC endpoint address of the Osmosis Testnet for ease of deployment, and the $TXFLAG variable saves the following gas cost information: At this stage, you must copy and paste your API keys after the RPC endpoint location.
Now, post the code to the chain and use the following command to extract the CODE_ID: If the $CODE_ID value is logged normally, the upload was successful.
By running the get_count query again after the transaction below has failed, you can see that the value has increased by one from the previous count value.
After the transaction below is lost, check the value again through the get_count query to confirm that the count value has been reset to the specified value.
Let's now look at how to communicate smart contracts placed on the Osmosis Testnet using CosmJS on the front end via a simple Clicker game.
The Clicker game is a basic game in which you click the CosmWasm symbol that appears on the screen for 15 seconds to get a score, and then you drop a transaction to the contract to record the score once the game is done.
Clone repository
You may play the game yourself by inspecting the completed code in the Step 3 branch of the repo below. Check this medium article (warning! in Korean) contains a full discussion of each implementation phase.
The Keplr wallet is a wallet that supports the Cosmos ecosystem's interchain.
Keplr Wallet Let's get started by installing the extension and making an account. By default, there is no test net on the linked network when you launch the Keplr wallet. To add a network that is not available by default, you must call a separate method at the frontend, pass the network's configuration value as an argument, and then request addition.
src/wallet/connect.js contains the code for adding a network to the Keplr wallet.
Check the keplr object to determine if the extension is installed, then connect to the Osmosis Testnet network using the window.keplr.experimentalSuggestChain function. You may now obtain network information after adding a network to your Keplr wallet.
The window.keplr.enable(chainId:string) function allows the website to request access to the wallet from Keplr for user authorization, and then collect the wallet's detailed information to generate the Client.
Finally, provide the wallet's information value to the parent component that ran the method connectWallet. When React.js delivers a value from a child component to a parent, the parent can drop the function to props, and the child can pass the value to a function factor. The getInfo method may be found in src/App.js.
// src/wallet/connect.jsimport { SigningCosmWasmClient } from"@cosmjs/cosmwasm-stargate";constconnectWallet=async (chainInfo, { getInfo }) => {// verify whether Keplr extension is installed on user's web browserif (!window.getOfflineSigner ||!window.keplr) {alert("Please install keplr extension"); }// Keplr wallet to be added on networkif (window.keplr.experimentalSuggestChain) {try {awaitwindow.keplr.experimentalSuggestChain(chainInfo); } catch {alert("Failed to suggest the chain"); } } else {alert("Please use the recent version of keplr extension"); }// to request Keplr wallet to access into chainIdawaitwindow.keplr.enable(chainInfo.chainId);// retreive OfflineSigner using chainIdconstofflineSigner=window.getOfflineSigner(chainInfo.chainId);// return address & public key pair arrayconstaccounts=awaitofflineSigner.getAccounts();// SigningCosmWasmClient object creatingconstclient=awaitSigningCosmWasmClient.connectWithSigner(chainInfo.rpc, offlineSigner );// getting a balanceconstbalance=awaitclient.getBalance( accounts[0].address,chainInfo.stakeCurrency.coinMinimalDenom );// a function to pass values to parent componentgetInfo(client, accounts[0].address, balance,chainInfo.chainId);};exportdefault connectWallet;
ChainInfo that must be passed as a factor in window.keplr.experimentalSuggestChain can be found in src/wallet/network_info.js. This information is required to be communicated except that the Optional annotation has been processed, otherwise an error will occur.
// factory patternconstchainInfo= (chainId, chainName, rpc, rest, coinDenom, coinMinimaldenom, coinDecimals, bech32) => {return {// chain Id chainId: chainId,// chain Name chainName: chainName,// chain RPC endpoint address rpc: rpc,// chain REST endpoint address rest: rest,// staking coin info stakeCurrency: {// denomination coinDenom: coinDenom,// uatom, uosmo coinMinimalDenom: coinMinimaldenom,// coin decimals coinDecimals: coinDecimals, },// BIP44 paths bip44: {// BIP44 standard// 'purpose' to fbe fixed as 44// 'coinType' to use 118 for Cosmos Hub coinType:118, },// Bech32 information bech32Config: { bech32PrefixAccAddr: bech32, bech32PrefixAccPub: bech32 +"pub", bech32PrefixValAddr: bech32 +"valoper", bech32PrefixValPub: bech32 +"valoperpub", bech32PrefixConsAddr: bech32 +"valcons", bech32PrefixConsPub: bech32 +"valconspub" },// all coins/tokens list currencies: [{// denomination coinDenom: coinDenom,// coin minimal denomination coinMinimalDenom: coinMinimaldenom,// coin decimals coinDecimals: coinDecimals, }],// tokens to be paid as fee list feeCurrencies: [{// denomination coinDenom: coinDenom,// coin minimal denomination coinMinimalDenom: coinMinimaldenom,// coin decimals coinDecimals: coinDecimals, }],// (Optional) Information used only to import addresses from ENS should match the coinType in BIP44 coinType: 118,// Set (low: 0.01, average: 0.025, high: 0.04) to default unless otherwise specified// Keplr does not yet support dynamic calculations based on on on-chain data// It should be higher than the minimum gas price set by the RPC/REST endpoints and the validators in the chain. gasPriceStep: { low:0.01, average:0.05, high:0.25 } }}constnetworkInfo= {"malaga-420":chainInfo("malaga-420","Malaga","<https://rpc.malaga-420.cosmwasm.com:443>","<https://api.malaga-420.cosmwasm.com>","Málaga","umlg",6,"wasm"),"osmo-test-4":chainInfo("osmo-test-4","Osmosis Testnet","<https://testnet-rpc.osmosis.zone>","<https://testnet-rest.osmosis.zone/>","OSMO","uosmo",6,"osmo"),"uni-3":chainInfo("uni-3","Juno Testnet","<https://rpc.uni.junonetwork.io:443>","<https://api.uni.junonetwork.io/>","JUNOX","ujunox",6,"juno"),"constantine-1":chainInfo("constantine-1","Archway Testnet","<https://rpc.constantine-1.archway.tech:443>","<https://api.constantine-1.archway.tech>","CONST","uconst",6,"archway")}exportdefault networkInfo;
src/App.js implements the Network Connection button on the main screen.
When you click the Osmosis Testnet button, it changes to DISCONNECT and a PLAY button is generated to take you to the gameplay screen, which displays the linked wallet's address and balance underneath.
App.js manages the client, address, balance, chainId state with useState, and when using the connectWallet function, give over the getInfo function along with the factor and store it with setState.
import"./App.css";import { useState } from"react";import { useNavigate } from"react-router-dom";import networkInfo from"./wallet/network_info";import connectWallet from"./wallet/connect";functionApp() {// the value from connectWalletconst [client,setClient] =useState();const [address,setAddress] =useState();const [balance,setBalance] =useState();const [chainId,setChainId] =useState();// the variable from visibility properties in PLAY buttonconst [visible,setVisible] =useState("hidden");constnavigate=useNavigate();// tehe function to pass to connectWallet methodconstgetInfo= (client, address, balance, chainId) => {setClient(client);setAddress(address);setBalance(balance);setChainId(chainId);setVisible("visible"); };// initialize the information given by connectWallet methodconstdisconnect= (event) => {setClient();setChainId();setAddress();setBalance();setVisible("hidden"); };// Implement DISCONNECT and CONNECT buttons for each network based on chainId.constrenderBtn= () => {returnObject.keys(networkInfo).map((id) => {if (chainId === id) {return ( <buttontype="button"onClick={(event) =>disconnect(event)}className="disconnect-btn" > DISCONNECT </button> ); }return ( <buttontype="button"onClick={(event) =>connectWallet(event, networkInfo[id], { getInfo }) }className="connect-btn" > {networkInfo[id].chainName} </button> ); }); };// If the website is linked to a wallet, print out the address and balance.constshowWalletInfo= () => {if (client) {return ( <divclassName="wallet-info"> <p>{`address: ${address}`}</p> <p>{`balance: ${balance.amount}${balance.denom}`}</p> </div> ); } };// navigate to /play when clickedconstplayGame= () => {return ( <divclassName="menu"> <buttonclassName="play-btn"onClick={() => {navigate("/play", { state: { address: address, denom:balance.denom, chainId: chainId } }); }}style={{ visibility: visible }} > <span>PLAY</span> </button> {!client && <p>Choose your network and Connect wallet</p>} {client && ( <p>Click as many CosmWasm Icon as you can within 15 seconds!</p> )} </div> ); };return ( <divclassName="App"> <header> <divclassName="header-titles"> <imgalt="Cosmwasm Logo"className="cosmwasm-logo"src="/cosmwasm-logo.svg" /> <h1>Clicker Game</h1> </div> </header> <divclassName="App-container"> <divclassName="App-menu-container"> {playGame()} <divclassName="connect-wallet">{renderBtn()}</div> </div> {showWalletInfo()} </div> </div> );}exportdefault App;
Communicate with a deployed contract
You can find the address of the contract we published to Osmosis Testnet previously in the src/contract/address.js file.
To query CosmJS, use the queryContractSmart() function. To conduct the query, the method is sent with the contract's address and message as a factor, and the resultant count number may be obtained using result.count. The code that fires the get count query can be found in src/contract/get_count.js.
Because the reset and increment transactions modify the internal state of the contract, you must pay the gas cost.
You may execute a transaction by sending the execute() method along with the wallet address to pay for the gas, the contract address, the message, and the gas cost as a component. src/contract/reset.js contains the code that sends the reset transaction.
src/pages/play.js implements the play screen that shows when you connect to the /play location. The final play page that we will use is as follows.
The previous and current scores are displayed in the upper left corner, while the time remaining is displayed in the top right corner. In the center, there is a GAME START button, which turns to TRANSACTION after the game is over.
The symbol exists in the game container, appears at the start of the game, and then disappears. While the transaction is ongoing, a loading message is displayed.
// The game begins before the game begins, and the TRANSACTION button appears after the game concludes.constrenderButton= () => {if (gameOver ===false) {return ( <buttonclassName="game-btn"onClick={(event) =>startGame(event)}> GAME START </button> ); } else {return ( <buttonclassName="game-btn"onClick={(event) =>submitScore(event)}> TRANSACTION </button> ); } };
When the GAME START button is pushed, the count value acquired by calling the get_count function is shown in Previous Score, and the reset method is called to reset the contract's 'count' value to zero.
To communicate with the smart contract, you must first have client. When the screen is first shown using the useEffect function, use the chainId information from the App.js screen to connect to the wallet and construct 'client'. To use the information supplied from App.js to state, you must use the 'useLocation' function.
The function SigningCosmWasmClient.connectWithSigner() returns the SigningCosmWasmClient with the RPC endpoint address and OfflineSigner as a factor retrieved from Keplr. The 'client' serves as an interface with the network.
constlocation=useLocation();// when page is rendered, the client object is created and saveduseEffect(() => {constgetClient=async (chainId) => {// request access to Keplr wallet on the chainId awaitwindow.keplr.enable(chainId);// using chainId to retreive OfflineSignerconstofflineSigner=window.getOfflineSigner(chainId);// SigningCosmWasmClient 생성constclient=awaitSigningCosmWasmClient.connectWithSigner( networkInfo[chainId].rpc, offlineSigner );setClient(client); };getClient(location.state.chainId);}, []);
When the GAME START button is pushed, the startGame function reads the contract's count value using the previously created get_count method and puts it in the previousScore. Then, using the reset function, change the contract's count value to zero.
Begin the game after communicating with the contract. When the game begins, the time setting of 15 seconds should be reduced by one second each second. To implement the feature, use the setInterval method.
// Execute when Game Start button is clickedconststartGame=async(event) => {// set loading status to true when communicating with a contractsetLoading(true);// execute get_count query to retreive contract's count valueconstresult=awaitget_count(client,location.state.chainId);// save count value retrieved from preiousScoresetPreviousScore(result.count);// execute reset transaction to initialize count value to 0awaitreset(client,location.state.address,0,location.state.chainId,location.state.denom);// set loading status to false after communicating with a contractsetLoading(false);// intiialize true to Game Start value before starting a gamesetGameStart(true);// set a location to show up iconssetTargetPosition({ top:"20%", left:"50%" });// Use the setInterval method to reduce time by 1 per secondsetTimerId(setInterval(() => {setTime((time) => (time >0? time -1:0)); },1000) );}
The next time you click the CosmWasm symbol that displays when the game starts, your score will be increased by one and the technique will be executed to randomize the next location. And let's make the game stop after all 15 seconds have passed and the clock reaches zero.
Set the icon's location in a randomized fashion by utilizing the setTargetPosition and Math.random methods and increasing the current score by one.
// A function that run when you click the CosmWasm iconconsthandleClick= () => {// increment current score by 1setScore((score) => score +1);// Random set to the following position on the icon.setTargetPosition({ top:`${Math.floor(Math.random() *80+10)}%`, left:`${Math.floor(Math.random() *80) +10}%` }); };
Using the useEffect function that detects a change in the time value, set the icon to disappear when time goes to zero and display the game end alarm window.
And use the clearInterval method to stop the continuously running 'setInterval' function
// play.js (added)// End the game when time changes and becomes zerouseEffect(() => {if (time ===0) {// Icon is not disabled.setTargetPosition({ display:"none" });// Game Exit allamchang.alert(`Game Over! Your score is ${score}. Please confirm transaction to submit score.` );// setInterval function just haltedclearInterval(timerId);setGameOver(true);setGameStart(false); }}, [time]);
Create the submitScore method, which is called when you click the TRANSACTION button after the previous game has finished. Run increment as many times as the user's score, and after the increment transaction is complete, read the contract's count value again through the get_count function and update it to previousScore.
Since the contract's count value was reset to zero in the preceding requirement No. 2, the contract's count value is the score acquired by the user when the increment method is called.
After all contact with the contract has been completed, set the gameOver value to false and the zeroed time value back to 15 seconds to allow the game to be continued.
// Run when the Transaction button is pressedconstsubmitScore=async (event) => {// Set loading status to true while communicating with the contractsetLoading(true);// Execute the increment transaction by the score obtained by the user to change the count value of the contract to scoreawaitincrement( client,location.state.address, score,location.state.chainId,location.state.denom );// Initialize with current score of 0setScore(0);// Read the count value stored in the contract through the get_count query and update to the Previous Scoreconstresult=awaitget_count(client,location.state.chainId);setPreviousScore(result.count);// Set loading status to false after communication with contractsetLoading(false);// Set the game to restartsetGameOver(false);setTime(playTime);};
You can now use AllThatNode and CosmJS to publish Wasm Smart Contracts on the Osmosis Testnet and interface with Smart Contracts deployed on the front end through a simple clicker game.