Building a Frontend
In this section, we’ll use the @sei-js library to build a React app that interacts with a CosmWasm contract.
Requirements
Before starting, ensure you have:
CosmWasm/Cosmos RPC dApps
- CosmosKit: A React-based library for Cosmos ecosystem dApps, facilitating wallet connection and interaction. Supports all Sei native wallets as well as cross-chain wallets like Keplr and Leap.
- CosmosKit Documentation
- CosmJS: A JavaScript library for interacting with Cosmos blockchains, providing tools to handle wallet connections, transactions, and more. Offers comprehensive tools for Cosmos SDK based blockchains.
- CosmJS Documentation
Creating a React Project
If you’re starting a project from scratch, consider using the TypeScript template from Vite for easier development and debugging.
npm create vite@latest my-counter-frontend -- --template react-ts
This command creates a new folder with a React project using TypeScript. Open
my-counter-frontend
in your favorite IDE.
Installing Dependencies
From the terminal at the root of your project, install the required
dependencies: @sei-js/core
and @sei-js/react
.
npm install @sei-js/core @sei-js/react
@sei-js
contains NPM libraries for writing applications that interact with Sei. Learn more here .Wrapping App in SeiWalletProvider
Replace the code in your App.tsx
file with the following to set up a
SeiWalletProvider
context, define your chain info, and set connection URLs.
import { SeiWalletProvider } from '@sei-js/react';
import './App.css';
import Home from './Home.tsx';
function App() {
return (
// Set up SeiWalletProvider for easy wallet connection and to use hooks in @sei-js/react
<SeiWalletProvider
chainConfiguration={{
chainId: 'arctic-1',
restUrl: 'https://rest.arctic-1.seinetwork.io',
rpcUrl: 'https://rpc.arctic-1.seinetwork.io'
}}
wallets={['compass', 'fin']}>
<Home />
</SeiWalletProvider>
);
}
export default App;
Detailed outline of App.tsx
Wallet Provider Setup
- Imports
SeiWalletProvider
from the@sei-js/react
package. - Wraps
App
withSeiWalletProvider
for wallet context.
Chain Configuration
- Specifies the
chainConfiguration
prop inSeiWalletProvider
to set up the blockchain network: chainId
: Identifies the Sei network (in this case,arctic-1
).restUrl
: REST URL for the Sei (You may need your own provider).rpcUrl
: RPC URL for the Sei (You may need your own provider).
Supported Wallets
- Sets up supported wallet types (
'compass'
,'fin'
,'leap'
,'keplr'
) in the wallets prop.
Adding Home Component
Create a file named Home.tsx
in your src
directory with the following code:
import { useCallback, useEffect, useState } from 'react';
import { useCosmWasmClient, useSigningCosmWasmClient, useWallet, WalletConnectButton } from '@sei-js/react';
// arctic-1 example contract
const CONTRACT_ADDRESS = 'sei14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sh9m79m';
function Home() {
const [count, setCount] = useState<number | undefined>();
const [error, setError] = useState<string>('');
const [isIncrementing, setIsIncrementing] = useState<boolean>(false);
// Helpful hook for getting the currently connected wallet and chain info
const { connectedWallet, accounts } = useWallet();
// For querying cosmwasm smart contracts
const { cosmWasmClient: queryClient } = useCosmWasmClient();
// For executing messages on cosmwasm smart contracts
const { signingCosmWasmClient: signingClient } = useSigningCosmWasmClient();
const fetchCount = useCallback(async () => {
const response = await queryClient?.queryContractSmart(CONTRACT_ADDRESS, {
get_count: {}
});
return response?.count;
}, [queryClient]);
useEffect(() => {
fetchCount().then(setCount);
}, [connectedWallet, fetchCount]);
const incrementCounter = async () => {
setIsIncrementing(true);
try {
const senderAddress = accounts[0].address;
// Build message content
const msg = { increment: {} };
// Define gas price and limit
const fee = {
amount: [{ amount: '20000', denom: 'usei' }],
gas: '200000'
};
// Call smart contract execute msg
await signingClient?.execute(senderAddress, CONTRACT_ADDRESS, msg, fee);
// Updates the counter state again
const updatedCount = await fetchCount();
setCount(updatedCount);
setIsIncrementing(false);
setError('');
} catch (error) {
if (error instanceof Error) {
setError(error.message);
} else {
setError('unknown error');
}
setIsIncrementing(false);
}
};
// Helpful component for wallet connection
if (!connectedWallet) return <WalletConnectButton />;
return (
<div>
<h1>Count is: {count ? count : '---'}</h1>
<button disabled={isIncrementing} onClick={incrementCounter}>
{isIncrementing ? 'incrementing...' : 'increment'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
export default Home;
We deployed a counter contract on the arctic-1
testnet. Contract address:
sei14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sh9m79m
. Learn more
about this contract here .
Detailed outline of Home.tsx
Render Logic
- Shows
WalletConnectButton
if no wallet is connected. - Otherwise, displays the counter value and an “Increment” button.
- Disables the button when a transaction is pending.
- Catches and displays error messages, if any.
Wallet and Client Setup
- Utilizes
useWallet
hook from@sei-js/react
to get the connected wallet and accounts. - Uses
useCosmWasmClient
anduseSigningCosmWasmClient
hooks from@sei-js/react
to instantiatequeryClient
andsigningClient
. The signing client can be used for querying, but it is not recommended to use a signing client when only a query client is needed.
Fetching Counter Value
fetchCount
function usesqueryClient.queryContractSmart
to query the smart contract with the query{ get_count: {} }
.
Incrementing Counter
incrementCounter
function:- Extracts
senderAddress
from the connected wallet’s accounts. - Builds a message with
{ increment: {} }
to be sent to the smart contract. - Specifies the transaction fee with gas price and gas limit.
- Calls
signingClient.execute
to execute the contract with the built message and fee.
By leveraging @sei-js
and its hooks, the component provides a way to connect
to a wallet, query a CosmWasm smart contract to get the current counter state,
and increment it with an execute message.
Running the App
Run npm run dev
and navigate to http://localhost:5173/ to view your
application.
🎉 Congratulations on creating a website for querying and executing a smart contract on Sei! Explore more possibilities with your frontend at our @sei-js repo .
Beyond Simple Contracts: Advanced CosmWasm Interactions
While our counter example demonstrated the basics of connecting to and interacting with a CosmWasm contract through the WASM precompile, CosmWasm contracts offer much more sophisticated capabilities. One of their most powerful features is their ability to describe their own interfaces, eliminating the need for external ABIs (Application Binary Interfaces) that are typically required for EVM contract interactions.
Let’s explore these advanced features by building a component that interacts with a CW721 (NFT) contract. This example will demonstrate contract discovery, handling different response formats, and implementing robust error handling - skills that are essential when working with more complex CosmWasm contracts.
Advanced CosmWasm Contract Interactions Through the WASM Precompile
Introduction
While our basic counter example demonstrated simple contract interactions, CosmWasm contracts offer much more sophisticated capabilities than traditional EVM contracts. One of their most powerful features is their ability to describe their own interfaces, eliminating the need for external ABIs (Application Binary Interfaces) that are required for EVM contract interactions.
In this guide, we’ll explore these advanced features and learn how to build more complex applications that leverage the full power of CosmWasm contracts through Sei’s WASM precompile.
Understanding CosmWasm Contract Discovery
Unlike EVM contracts where you need detailed interface specifications beforehand, CosmWasm contracts can tell you exactly how to interact with them. This self-describing capability makes them particularly developer-friendly and reduces the chance of interface mismatches.
Let’s explore how to implement this discovery mechanism:
async function discoverContractMethods(contractAddress: string) {
// We intentionally send an invalid query - the contract will respond with valid methods
const invalidQuery = { a: 'b' };
try {
await contract.query(contractAddress, toUtf8Bytes(JSON.stringify(invalidQuery)));
// If we reach here, something unexpected happened
console.log('Unexpected success - contract accepted invalid query');
return null;
} catch (error) {
if (error.data) {
const errorMessage = toUtf8String(error.data);
// The error message contains a list of valid methods
// Format: "expected one of method1, method2, method3: query wasm contract failed"
const match = errorMessage.match(/expected one of (.+): query wasm contract failed/);
if (match) {
const validMethods = match[1]
.replace(/`/g, '')
.split(', ')
.map((method) => method.trim());
console.log('Valid query methods:', validMethods);
return validMethods;
}
}
console.error('Unexpected error structure:', error);
return null;
}
}
When we run this against a CW721 (NFT) contract, we might receive a response like this:
[
'owner_of', // Query the owner of a specific token
'approval', // Check if an address is approved for a token
'approvals', // List all approvals for a token
'operator', // Check if an address is an operator
'all_operators', // List all operators
'num_tokens', // Get total supply of tokens
'contract_info', // Get contract metadata
'nft_info', // Get metadata for a specific token
'all_nft_info', // Get all info for a specific token
'tokens', // List tokens owned by an address
'all_tokens', // List all tokens in the collection
'minter', // Get the minting authority
'extension', // Access CW721 extensions
'ownership' // Query contract ownership
];
Building an NFT Information Component
Let’s create a practical example that uses these discovered methods to interact with a CW721 contract. This component will display both collection-wide information and individual token details:
function NFTViewer() {
const [methods, setMethods] = useState<string[]>([]);
const [collectionOwner, setCollectionOwner] = useState<string>();
const [tokenOwner, setTokenOwner] = useState<string>();
const [contract, setContract] = useState<Contract>();
const [tokenId, setTokenId] = useState<string>('1');
const [isLoading, setIsLoading] = useState(false);
const NFT_CONTRACT_ADDRESS = "sei1g2a0q3tddzs7vf7lk45c2tgufsaqerxmsdr2cprth3mjtuqxm60qdmravc";
// Query collection ownership
const queryCollectionOwner = async () => {
try {
// The ownership query returns collection-level ownership information
const queryMsg = { ownership: {} };
const queryResponse = await contract.query(
NFT_CONTRACT_ADDRESS,
toUtf8Bytes(JSON.stringify(queryMsg))
);
const responseData = JSON.parse(toUtf8String(queryResponse));
// Handle different response formats - some contracts nest data differently
const owner = responseData.data?.owner || responseData.owner;
setCollectionOwner(owner);
} catch (error) {
console.error('Error querying collection owner:', error);
}
};
// Query specific token ownership
const queryTokenOwner = async (tokenId: string) => {
try {
setIsLoading(true);
// The owner_of query returns ownership information for a specific token
const queryMsg = { owner_of: { token_id: tokenId } };
const queryResponse = await contract.query(
NFT_CONTRACT_ADDRESS,
toUtf8Bytes(JSON.stringify(queryMsg))
);
const responseData = JSON.parse(toUtf8String(queryResponse));
const owner = responseData.data?.owner || responseData.owner;
setTokenOwner(owner);
} catch (error) {
console.error('Error querying token owner:', error);
} finally {
setIsLoading(false);
}
};
// Discover available methods when component mounts or contract changes
useEffect(() => {
if (!contract) return;
discoverContractMethods(NFT_CONTRACT_ADDRESS)
.then(setMethods);
}, [contract]);
return (
<div className="nft-viewer">
<h2>NFT Collection Info</h2>
<div>
<p>Collection Address: {NFT_CONTRACT_ADDRESS}</p>
<p>Collection Owner: {collectionOwner || 'Loading...'}</p>
<p>Available Methods: {methods?.join(', ') || 'Loading...'}</p>
</div>
<div className="token-lookup">
<h3>Token Ownership Lookup</h3>
<div className="input-group">
<input
type="text"
value={tokenId}
onChange={(e) => setTokenId(e.target.value)}
placeholder="Enter token ID"
/>
<button
onClick={() => queryTokenOwner(tokenId)}
disabled={isLoading}
>
{isLoading ? 'Searching...' : 'Look Up Token'}
</button>
</div>
{tokenOwner && (
<div className="token-result">
<p>Token {tokenId} Owner: {tokenOwner}</p>
</div>
)}
</div>
</div>
);
}
Understanding CosmWasm Response Formats
CosmWasm contracts can return data in various formats, and it’s important to handle these variations properly. Here are some example responses you might receive:
-
Collection Ownership Query Response:
{ "owner": "sei1hjsqrfdg2hvwl3gacg4fkznurf36usrv7rkzkyh29wz3guuzeh0snslz7d", "pending_owner": null, "pending_expiry": null }
-
Token Ownership Query Response:
{ "owner": "sei1frcndtm928xln5awxz4rcrh3f5exskjczrc92f", "approvals": [] }
Notice how these responses have different structures. That’s why our code uses a flexible approach to extract the owner:
const owner = responseData.data?.owner || responseData.owner;
Creating a Reusable Query Function
To make contract interactions more maintainable, consider creating a reusable query function:
async function queryContract(address: string, queryMsg: object) {
try {
const response = await contract.query(address, toUtf8Bytes(JSON.stringify(queryMsg)));
const result = JSON.parse(toUtf8String(response));
// Handle both nested and direct data structures
return result.data ?? result;
} catch (error) {
if (error.data) {
const errorMessage = toUtf8String(error.data);
// Check if this is a method discovery error
if (errorMessage.includes('expected one of')) {
console.log('Available methods:', errorMessage);
}
}
throw error;
}
}
Best Practices for CosmWasm Contract Interactions
When building applications that interact with CosmWasm contracts through the WASM precompile, follow these guidelines:
-
Always Start with Discovery: Use the contract’s self-describing capabilities to understand its interface. This prevents errors and makes your code more maintainable.
-
Handle Response Variations: CosmWasm contracts might return data in different formats. Always implement robust response parsing that can handle various structures.
-
Implement Proper Error Handling: CosmWasm contracts provide detailed error messages that can help diagnose issues. Make use of this information in your error handling.
-
Manage Loading States: Always provide clear feedback about the state of contract interactions to improve user experience.
-
Cache Method Information: Consider caching the discovered methods to reduce unnecessary queries, but make sure to refresh this cache periodically.
Error Handling Examples
Here’s how to handle different types of errors you might encounter:
try {
const result = await queryContract(address, queryMsg);
// Handle success
} catch (error) {
if (error.data) {
const errorMessage = toUtf8String(error.data);
if (errorMessage.includes('expected one of')) {
// This is a method discovery error - might be useful!
console.log('Valid methods:', errorMessage);
} else if (errorMessage.includes('not found')) {
// Handle non-existent tokens or resources
console.log('Resource not found');
} else {
// Handle other contract-specific errors
console.log('Contract error:', errorMessage);
}
} else {
// Handle network or other errors
console.error('Network or system error:', error);
}
}
Polyfills Warning
When developing frontend applications for the blockchain, it’s important to be
aware that some libraries may require polyfills, especially when used in browser
environments. For instance, the Buffer
class and other Node.js-specific
features are not natively available in browsers and need to be polyfilled.
If you are using Vite or another rollup based frontend library you can add the following to the entry point of your app.
import { Buffer } from 'buffer';
// Polyfill self for browser and global for Node.js
const globalObject = typeof self !== 'undefined' ? self : global;
Object.assign(globalObject, {
process: process,
Buffer: Buffer
});
If you are using a Webpack based bundling tool you can use the following plugin in you Webpack config.
yarn add -D node-polyfill-webpack-plugin
import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
... // the rest of your webpack config
plugins: [
...
new NodePolyfillPlugin(),
...
],
...