Skip to main content
This tutorial will guide you through building an on-chain guestbook called MemoBoard. Users can post public messages, and the application prioritizes human-readable identity by resolving .init usernames. By the end of this tutorial, you will have:
  • Generated and verified a Rust smart contract for the guestbook.
  • Deployed the contract to your live appchain.
  • Scaffolded and connected a React frontend.
  • Verified the on-chain functionality.

Your Project Structure

The following steps will instruct your AI agent to create these directories inside your my-initia-project folder:
Project Structure
my-initia-project/
├── memoboard/           # Rust smart contract project
└── memoboard-frontend/  # React frontend application
Prerequisite: Ensure you have a Wasm-compatible appchain running locally. If you haven’t launched one yet, complete the Step-by-Step Guide first.

Readiness Check

Before you start, verify that your local infrastructure is healthy. Prompt:
Using the `initia-appchain-dev` skill, please verify that my appchain, executor bot, and relayer are running and that my Gas Station account has a balance.

Step 1: Create and Unit Test the Smart Contract

Instruct your AI agent to create the Rust (Wasm) contract using the initia-appchain-dev skill. Your AI agent will generate the contract and automatically run unit tests to ensure the logic is sound. Example Prompt:
Using the `initia-appchain-dev` skill, please create a Rust smart contract project for our MemoBoard in a new directory named `memoboard`. The contract should:
- Allow users to post a message (string).
- Store a list of all messages with the sender's address.
- Include a function to query all messages on the board.
Please also create and run unit tests to verify these features.
Your AI agent will generate the memoboard project and confirm that the Rust tests pass.
If you prefer to see the Rust logic, here is a simplified but schema-accurate version of what your AI agent would generate. If doing this manually, save the following code in src/contract.rs inside your memoboard directory (ensure src/lib.rs exports the contract module).First create and enter the project directory:
cargo new --lib --edition 2021 memoboard
cd memoboard
Important: Ensure your Cargo.toml includes the following [lib] section to correctly generate the Wasm binary:
Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]
Also ensure Cargo.toml includes these dependencies and features required by the example contract:
Cargo.toml
[features]
library = []

[dependencies]
cosmwasm-schema = "2.0.4"
cosmwasm-std = "2.0.4"
cw-storage-plus = "2.0.0"
thiserror = "1.0.31"
src/contract.rs
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, StdError};
use cw_storage_plus::Item;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),
}

#[cw_serde]
pub struct Memo {
    pub sender: String,
    pub message: String,
}

pub const MESSAGES: Item<Vec<Memo>> = Item::new("messages");

#[cw_serde]
pub struct InstantiateMsg {}

#[cw_serde]
pub enum ExecuteMsg {
    PostMessage { message: String },
}

#[cw_serde]
pub struct MessagesResponse {
    pub messages: Vec<Memo>,
}

#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
    #[returns(MessagesResponse)]
    GetMessages {},
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(deps: DepsMut, _env: Env, _info: MessageInfo, _msg: InstantiateMsg) -> Result<Response, ContractError> {
    MESSAGES.save(deps.storage, &vec![])?;
    Ok(Response::new().add_attribute("action", "instantiate"))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, _env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::PostMessage { message } => {
            let mut messages = MESSAGES.load(deps.storage)?;
            messages.push(Memo {
                sender: info.sender.to_string(),
                message: message.clone(),
            });
            MESSAGES.save(deps.storage, &messages)?;
            Ok(Response::new()
                .add_attribute("action", "post_message")
                .add_attribute("sender", info.sender)
                .add_attribute("message", message))
        }
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetMessages {} => {
            let messages = MESSAGES.load(deps.storage)?;
            to_json_binary(&MessagesResponse { messages })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
    use cosmwasm_std::Addr;
    use cosmwasm_std::from_json;

    #[test]
    fn test_post_and_query() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        // Instantiate
        let info = message_info(&Addr::unchecked("creator"), &[]);
        instantiate(deps.as_mut(), env.clone(), info, InstantiateMsg {}).unwrap();

        // Post Message
        let info = message_info(&Addr::unchecked("user1"), &[]);
        let msg = ExecuteMsg::PostMessage { message: "Hello!".to_string() };
        execute(deps.as_mut(), env.clone(), info, msg).unwrap();

        // Query
        let res = query(deps.as_ref(), env, QueryMsg::GetMessages {}).unwrap();
        let val: MessagesResponse = from_json(&res).unwrap();
        assert_eq!(val.messages.len(), 1);
        assert_eq!(val.messages[0].message, "Hello!");
        assert_eq!(val.messages[0].sender, "user1");
    }
}

Step 2: Deploy to your Appchain

Now that the logic is verified, build and publish the contract to your appchain using the Gas Station account. Prompt:
Using the `initia-appchain-dev` skill, please build, publish, and instantiate the MemoBoard Rust contract located in the `memoboard` directory to my appchain using my Gas Station account, then return the deployed contract address.
1. Build and Store the Code:Standard cargo build binaries often fail validation on-chain. For WasmVM deployment, it is strongly recommended to use the CosmWasm Optimizer.Note for Apple Silicon (M1/M2/M3): Use cosmwasm/optimizer-arm64:0.17.0.
Optimizer Build Command
# Run from the root of your project
# Use 'optimizer' for x86_64 or 'optimizer-arm64' for Apple Silicon
OPTIMIZER_IMAGE="cosmwasm/optimizer-arm64:0.17.0"

docker run --rm -v "$(pwd)/memoboard":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  $OPTIMIZER_IMAGE
The optimized binary will be located in ./memoboard/artifacts/memoboard.wasm. Now, store the code:
Store Wasm Code
# If the chain requires fees, add the --fees flag (e.g., --fees 1000000umin)
minitiad tx wasm store ./memoboard/artifacts/memoboard.wasm \
  --from gas-station \
  --keyring-backend test \
  --chain-id <YOUR_APPCHAIN_ID> \
  --gas auto --gas-adjustment 1.4 --yes
2. Retrieve the Code ID and Instantiate:If the code_id is not returned directly, wait for 5 seconds for indexing and then query the transaction hash:
Get Code ID
# Retrieve Code ID
minitiad q tx <YOUR_TX_HASH> --output json | jq -r '.events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value'
Then, instantiate the contract:
Instantiate Contract
minitiad tx wasm instantiate <YOUR_CODE_ID> '{}' \
  --label "memoboard" \
  --from gas-station \
  --keyring-backend test \
  --chain-id <YOUR_APPCHAIN_ID> \
  --gas auto --gas-adjustment 1.4 \
  --no-admin --yes
3. Retrieve the Contract Address:Wait for 5 seconds for indexing and then query the transaction hash:
Get Contract Address
# Retrieve Contract Address
minitiad q tx <YOUR_TX_HASH> --output json | jq -r '.events[] | select(.type=="instantiate") | .attributes[] | select(.key=="_contract_address") | .value'

Step 3: Smoke Test the Deployed Contract On-Chain

Before frontend integration, smoke test your deployed contract directly on chain. Example Prompt:
Using the `initia-appchain-dev` skill, I want to smoke test our live MemoBoard contract. Using my Gas Station account on my appchain, please:
1. Post a message: "Hello from Initia!"
2. Query the board to see all messages.
Submit one message transaction, then query contract state to confirm it was persisted on chain.
Post Message
# If the chain requires fees, add the --fees flag (e.g., --fees 1000000umin)
minitiad tx wasm execute <YOUR_CONTRACT_ADDRESS> '{"post_message":{"message":"Hello!"}}' \
  --from gas-station \
  --keyring-backend test \
  --chain-id <YOUR_APPCHAIN_ID> \
  --gas auto --gas-adjustment 1.4 --yes
Query Messages
sleep 5
minitiad query wasm contract-state smart <YOUR_CONTRACT_ADDRESS> '{"get_messages":{}}'

Step 4: Create a Frontend

Let’s create a UI to display and post messages. 1. Scaffold the Frontend:
Using the `initia-appchain-dev` skill, please scaffold a new Vite + React application named `memoboard-frontend` in my current directory using the `scaffold-frontend` script. Create a component named Board.jsx that displays a list of messages and a text input to post a new one.
2. Connect to Appchain:
Using the `initia-appchain-dev` skill, modify the Board.jsx component in the `memoboard-frontend` directory to connect to our MemoBoard contract on my appchain. Use the `@initia/interwovenkit-react` package for wallet connection and transaction signing.
If you prefer to set up the frontend manually, follow these steps:1. Create the Project and Install Dependencies:
npm create vite@latest memoboard-frontend -- --template react
cd memoboard-frontend
npm install
npm install @initia/interwovenkit-react wagmi viem @tanstack/react-query @initia/initia.js
npm install --save-dev vite-plugin-node-polyfills
npm install buffer util
2. Configure Vite Polyfills: Update vite.config.js to include the Node polyfills:
vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { nodePolyfills } from 'vite-plugin-node-polyfills'

export default defineConfig({
  plugins: [
    react(),
    nodePolyfills({
      globals: {
        Buffer: true,
        process: true,
      },
    }),
  ],
})
Create the frontend files in this order so the imports line up cleanly:
  1. vite.config.js
  2. .env
  3. src/main.jsx
  4. src/Board.css
  5. src/Board.jsx
  6. src/App.jsx
3. Gather Runtime Values for Frontend Config: Right before creating .env, collect the values you will use:
Gather Frontend Values
APPCHAIN_ID=$(curl -s http://localhost:26657/status | jq -r '.result.node_info.network')
NATIVE_DENOM=$(minitiad q bank denoms-metadata --output json | jq -r '.metadatas[0].base // empty')
[ -z "$NATIVE_DENOM" ] && NATIVE_DENOM=$(minitiad q bank total --output json | jq -r '.supply[0].denom')
NATIVE_SYMBOL=$(minitiad q bank denoms-metadata --output json | jq -r '.metadatas[0].symbol // empty')
[ -z "$NATIVE_SYMBOL" ] && NATIVE_SYMBOL="$NATIVE_DENOM"

# Use the contract address you retrieved in Step 2.
MEMOBOARD_CONTRACT_ADDRESS=<YOUR_CONTRACT_ADDRESS>

echo "APPCHAIN_ID=$APPCHAIN_ID"
echo "NATIVE_DENOM=$NATIVE_DENOM"
echo "NATIVE_SYMBOL=$NATIVE_SYMBOL"
echo "MEMOBOARD_CONTRACT_ADDRESS=$MEMOBOARD_CONTRACT_ADDRESS"
4. Add Runtime Configuration: Create a .env file in memoboard-frontend so the chain metadata, endpoints, and live contract address are explicit:
.env
VITE_APPCHAIN_ID=$APPCHAIN_ID
VITE_CHAIN_NAME=social
VITE_CHAIN_PRETTY_NAME=Social 1
VITE_INITIA_RPC_URL=http://localhost:26657
VITE_INITIA_REST_URL=http://localhost:1317
VITE_INITIA_INDEXER_URL=http://localhost:8080
VITE_INITIA_JSON_RPC_URL=http://localhost:8545
VITE_NATIVE_DENOM=$NATIVE_DENOM
VITE_NATIVE_SYMBOL=$NATIVE_SYMBOL
VITE_NATIVE_DECIMALS=6
VITE_MEMOBOARD_CONTRACT_ADDRESS=$MEMOBOARD_CONTRACT_ADDRESS
5. Set up Providers in main.jsx:
src/main.jsx
import { Buffer } from 'buffer'
window.Buffer = Buffer
window.process = { env: { NODE_ENV: 'development' } }

import React from 'react'
import ReactDOM from 'react-dom/client'
import '@initia/interwovenkit-react/styles.css'
import {
  injectStyles,
  InterwovenKitProvider,
  TESTNET,
} from '@initia/interwovenkit-react'
import InterwovenKitStyles from '@initia/interwovenkit-react/styles.js'
import { WagmiProvider, createConfig, http } from 'wagmi'
import { mainnet } from 'wagmi/chains'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.jsx'

// Inject styles for the InterwovenKit drawer
injectStyles(InterwovenKitStyles)

const queryClient = new QueryClient()
const wagmiConfig = createConfig({
  chains: [mainnet],
  transports: { [mainnet.id]: http() },
})

const customChain = {
  chain_id: import.meta.env.VITE_APPCHAIN_ID,
  chain_name: import.meta.env.VITE_CHAIN_NAME,
  pretty_name: import.meta.env.VITE_CHAIN_PRETTY_NAME,
  network_type: 'testnet',
  bech32_prefix: 'init',
  logo_URIs: {
    png: 'https://raw.githubusercontent.com/initia-labs/initia-registry/main/testnets/initia/images/initia.png',
    svg: 'https://raw.githubusercontent.com/initia-labs/initia-registry/main/testnets/initia/images/initia.svg',
  },
  apis: {
    rpc: [{ address: import.meta.env.VITE_INITIA_RPC_URL }],
    rest: [{ address: import.meta.env.VITE_INITIA_REST_URL }],
    indexer: [{ address: import.meta.env.VITE_INITIA_INDEXER_URL }], // Placeholder REQUIRED for stability
    'json-rpc': [{ address: import.meta.env.VITE_INITIA_JSON_RPC_URL }],
  },
  fees: {
    fee_tokens: [
      {
        denom: import.meta.env.VITE_NATIVE_DENOM,
        fixed_min_gas_price: 0,
        low_gas_price: 0,
        average_gas_price: 0,
        high_gas_price: 0,
      },
    ],
  },
  staking: {
    staking_tokens: [{ denom: import.meta.env.VITE_NATIVE_DENOM }],
  },
  metadata: {
    minitia: { type: 'miniwasm' },
    is_l1: false, // REQUIRED for local appchains
  },
  native_assets: [
    {
      denom: import.meta.env.VITE_NATIVE_DENOM,
      name: 'Minitia',
      symbol: import.meta.env.VITE_NATIVE_SYMBOL,
      decimals: Number(import.meta.env.VITE_NATIVE_DECIMALS ?? 6),
    },
  ],
}

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <InterwovenKitProvider
          {...TESTNET}
          defaultChainId={customChain.chain_id}
          customChain={customChain}
          customChains={[customChain]}
        >
          <App />
        </InterwovenKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  </React.StrictMode>,
)
6. Create the Board.jsx Component: Create src/Board.jsx and src/Board.css with the following content:src/Board.css:
src/Board.css
.board-container {
  max-width: 600px;
  margin: 40px auto;
  padding: 32px;
  font-family: sans-serif;
}
.board-title {
  font-size: 2.5rem;
  font-weight: 800;
  margin-bottom: 24px;
  text-align: center;
}
.section-header {
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: #888;
  margin-bottom: 16px;
  display: block;
}
.auth-section {
  display: flex;
  justify-content: center;
  margin-bottom: 24px;
}
.wallet-info {
  background: #f0f0f0;
  padding: 12px 16px;
  border-radius: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.message-card {
  background: white;
  padding: 16px;
  border-radius: 12px;
  border: 1px solid #eee;
  margin-bottom: 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
}
.message-sender {
  font-family: monospace;
  font-size: 0.75rem;
  color: #999;
}
.message-content {
  margin-top: 8px;
  font-size: 1.1rem;
}
.input-group {
  display: flex;
  gap: 12px;
  margin-top: 16px;
}
.memo-input {
  flex: 1;
  padding: 12px 16px;
  border-radius: 8px;
  border: 1px solid #ddd;
  outline: none;
}
.btn-primary {
  background: #000;
  color: #fff;
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
}
src/Board.jsx:
src/Board.jsx
import React, { useState, useEffect } from 'react'
import { useInterwovenKit } from '@initia/interwovenkit-react'
import { RESTClient } from '@initia/initia.js'
import './Board.css'

const CHAIN_ID = import.meta.env.VITE_APPCHAIN_ID
const MEMO_BOARD_ADDRESS = import.meta.env.VITE_MEMOBOARD_CONTRACT_ADDRESS
const rest = new RESTClient(import.meta.env.VITE_INITIA_REST_URL)

const Board = () => {
  const [messages, setMessages] = useState([])
  const [content, setContent] = useState('')
  const { initiaAddress, requestTxSync } = useInterwovenKit()

  const truncate = (addr) => `${addr.slice(0, 10)}...${addr.slice(-6)}`

  const fetchMessages = async () => {
    try {
      // Wasm queries via REST MUST be base64 encoded
      const queryData = Buffer.from(
        JSON.stringify({ get_messages: {} }),
      ).toString('base64')
      const res = await rest.wasm.smartContractState(
        MEMO_BOARD_ADDRESS,
        queryData,
      )
      setMessages([...(res?.messages ?? [])].reverse())
    } catch (e) {
      console.error('Failed to fetch messages', e)
    }
  }

  useEffect(() => {
    fetchMessages()
    // Poll for new messages every 5 seconds
    const interval = setInterval(fetchMessages, 5000)
    return () => clearInterval(interval)
  }, [])

  const handlePostMessage = async () => {
    if (!content || !initiaAddress || !MEMO_BOARD_ADDRESS) return

    // Encode execute msg to Uint8Array (bytes)
    const msg = new TextEncoder().encode(
      JSON.stringify({ post_message: { message: content } }),
    )

    try {
      // Use requestTxSync for local development transactions
      // ALWAYS include CHAIN_ID to avoid RPC routing errors
      await requestTxSync({
        chainId: CHAIN_ID,
        messages: [
          {
            typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
            value: {
              sender: initiaAddress,
              contract: MEMO_BOARD_ADDRESS,
              msg,
              funds: [],
            },
          },
        ],
      })
      setContent('')
      // Small delay to allow block inclusion
      setTimeout(fetchMessages, 2000)
    } catch (e) {
      console.error('Transaction failed', e)
    }
  }

  return (
    <div className="board-container">
      <h1 className="board-title">MemoBoard</h1>
      {!initiaAddress && (
        <div
          className="message-card"
          style={{ marginBottom: 24, textAlign: 'center', color: '#666' }}
        >
          Connect your wallet from the app header to post a memo.
        </div>
      )}
      {initiaAddress && (
        <div style={{ marginBottom: '32px' }}>
          <h3 className="section-header">Post a Memo</h3>
          <div className="input-group">
            <input
              className="memo-input"
              value={content}
              onChange={(e) => setContent(e.target.value)}
              placeholder="Write a memo..."
            />
            <button onClick={handlePostMessage} className="btn-primary">
              Post Message
            </button>
          </div>
        </div>
      )}
      <div className="messages-list">
        <h3 className="section-header">Board Feed</h3>
        {messages.map((m, i) => (
          <div key={i} className="message-card">
            <div className="message-sender">{truncate(m.sender)}</div>
            <div className="message-content">{m.message}</div>
          </div>
        ))}
      </div>
    </div>
  )
}

export default Board
7. Create App.jsx: Create the app shell that renders the board and owns the wallet button:
src/App.jsx
import React from 'react'
import { useInterwovenKit } from '@initia/interwovenkit-react'
import Board from './Board.jsx'

function shortenAddress(addr) {
  if (!addr) return ''
  return `${addr.slice(0, 8)}...${addr.slice(-4)}`
}

function App() {
  const { initiaAddress, openConnect, openWallet } = useInterwovenKit()

  return (
    <div>
      <header
        style={{
          maxWidth: 600,
          margin: '40px auto 0',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          gap: 16,
        }}
      >
        <div>
          <p style={{ margin: 0, color: '#666', fontSize: 12, fontWeight: 700 }}>
            Initia Wasm Appchain
          </p>
          <h1 style={{ margin: '8px 0 0' }}>MemoBoard</h1>
        </div>

        {!initiaAddress ? (
          <button onClick={openConnect} className="btn-primary">
            Connect Wallet
          </button>
        ) : (
          <button onClick={openWallet} className="btn-primary">
            {shortenAddress(initiaAddress)}
          </button>
        )}
      </header>

      <Board />
    </div>
  )
}

export default App

Step 5: Wallet Funding and UI Verification

Ask your AI agent to fund your browser wallet, then verify frontend behavior manually in the browser:
  1. Start the frontend:
Start Vite Dev Server
cd memoboard-frontend
npm run dev
Check the browser console if you encounter issues.
  1. Open your browser wallet and copy your address (init1...).
  2. Give this prompt to your AI agent, replacing <YOUR_WALLET_ADDRESS> with the address you just copied:
Using the `initia-appchain-dev` skill, please fund my wallet address <YOUR_WALLET_ADDRESS> with 1 INIT on L1 and 100 of my appchain's native tokens on L2.
  1. Connect your wallet in memoboard-frontend.
  2. Post a message from the UI.
  3. Confirm it appears in the message list after refresh.
  4. If your UI supports it, verify sender identity rendering matches expectations.
If you get stuck, see the Debugging Workflow guide.

⚡ Power-Up: Initia Usernames

To make your MemoBoard natively integrated with the Initia stack, you can replace long, complex addresses with human-readable Initia Usernames (e.g., vitalik.init).

Step 6: Register your .init Name

Before updating your code, you should register a primary username for your wallet on the Initia testnet.
  1. Navigate to app.testnet.initia.xyz/usernames and connect your browser wallet.
  2. In the Find a username search box, enter your desired name. If it is available, you will see a green Available checkmark.
  3. Crucial: Ensure the Set as primary name checkbox is selected.
  4. Click Register and approve the transaction. Once resolved, your new .init name will appear in the top-right corner of the Initia App.

Step 7: Update the Frontend

Your AI agent knows how to integrate Initia Usernames. Simply ask it to update your board. Example Prompt:
Using the `initia-appchain-dev` skill, please add Initia username support to my MemoBoard.
The useInterwovenKit() hook provides the username for the currently connected wallet, and useUsernameQuery(address) resolves usernames for other sender addresses. This requires @initia/interwovenkit-react 2.4.6 or newer. To implement this, update your wallet button component (for example src/App.jsx) and your board message list like this:
import { useInterwovenKit, useUsernameQuery } from '@initia/interwovenkit-react'

function MessageRow({ message }) {
  const { initiaAddress, username } = useInterwovenKit()
  const { data: senderUsername } = useUsernameQuery(message.sender)
  const senderLabel =
    message.sender === initiaAddress
      ? username
        ? username
        : truncate(message.sender)
      : senderUsername
        ? senderUsername
        : truncate(message.sender)

  return (
    <div className="message-sender">
      {senderLabel}
    </div>
  )
}

// 1. Extract username from the hook
const { initiaAddress, username, openConnect, openWallet } = useInterwovenKit()

// 2. Update the connected wallet button to show the username
<div className="wallet-info">
  <button
    onClick={openWallet}
    style={{ background: 'none', border: 'none', cursor: 'pointer' }}
  >
    {username ? username : truncate(initiaAddress)}
  </button>
</div>

// 3. Update the feed rows to resolve usernames for sender addresses
messages.map((message, index) => (
  <MessageRow key={`${message.sender}-${index}`} message={message} />
))
Pro Tip: Keep useUsernameQuery(address) inside a child row component like MessageRow. Do not call it directly inside a parent component’s .map() callback.
If you want the complete finished frontend after applying the manual steps above, use the consolidated reference below.
If you want a single copyable end-state after completing the manual steps above, use this consolidated reference. It combines:
  • live Wasm contract query and execute flow,
  • wallet connect/display via App.jsx,
  • runtime config from .env, and
  • Initia username support for the connected wallet and sender rows.
src/Board.jsx
src/Board.jsx
import React, { useEffect, useMemo, useState } from 'react'
import { RESTClient } from '@initia/initia.js'
import { useInterwovenKit, useUsernameQuery } from '@initia/interwovenkit-react'
import './Board.css'

const CHAIN_ID = import.meta.env.VITE_APPCHAIN_ID
const REST_URL = import.meta.env.VITE_INITIA_REST_URL
const CONTRACT_ADDRESS = import.meta.env.VITE_MEMOBOARD_CONTRACT_ADDRESS

function truncate(value) {
  if (!value) return ''
  if (value.length < 18) return value
  return `${value.slice(0, 10)}...${value.slice(-6)}`
}

function MessageRow({ message, index, initiaAddress, username }) {
  const { data: senderUsername } = useUsernameQuery(message.sender)

  const senderLabel =
    message.sender === initiaAddress
      ? username
        ? username
        : truncate(message.sender)
      : senderUsername
        ? senderUsername
        : truncate(message.sender)

  return (
    <div className="message-card">
      <div className="message-sender">{senderLabel}</div>
      <div className="message-content">{message.message}</div>
      <div
        style={{
          marginTop: 8,
          fontSize: 12,
          color: '#999',
        }}
      >
        Message #{index + 1}
      </div>
    </div>
  )
}

export default function Board() {
  const { initiaAddress, username, requestTxSync } = useInterwovenKit()
  const [messages, setMessages] = useState([])
  const [draft, setDraft] = useState('')
  const [loading, setLoading] = useState(true)
  const [posting, setPosting] = useState(false)
  const [error, setError] = useState('')

  const rest = useMemo(() => new RESTClient(REST_URL), [])

  const fetchMessages = async () => {
    if (!CONTRACT_ADDRESS) {
      setError('Missing VITE_MEMOBOARD_CONTRACT_ADDRESS in .env')
      setLoading(false)
      return
    }

    setLoading(true)
    try {
      const queryData = Buffer.from(
        JSON.stringify({ get_messages: {} }),
      ).toString('base64')
      const res = await rest.wasm.smartContractState(CONTRACT_ADDRESS, queryData)
      setMessages(Array.isArray(res.messages) ? res.messages : [])
      setError('')
    } catch (e) {
      console.error('Failed to fetch messages', e)
      setError('Failed to load board messages')
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchMessages()
  }, [])

  const handlePostMessage = async (event) => {
    event.preventDefault()

    const trimmed = draft.trim()
    if (!trimmed || !initiaAddress || !CONTRACT_ADDRESS) return

    setPosting(true)
    setError('')

    try {
      const msg = new TextEncoder().encode(
        JSON.stringify({ post_message: { message: trimmed } }),
      )

      await requestTxSync({
        chainId: CHAIN_ID,
        messages: [
          {
            typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
            value: {
              sender: initiaAddress,
              contract: CONTRACT_ADDRESS,
              msg,
              funds: [],
            },
          },
        ],
      })

      setDraft('')
      await new Promise((resolve) => setTimeout(resolve, 1200))
      await fetchMessages()
    } catch (e) {
      console.error('Transaction failed', e)
      setError('Posting the message failed')
    } finally {
      setPosting(false)
    }
  }

  return (
    <div className="board-container">
      <h1 className="board-title">MemoBoard</h1>

      {!initiaAddress && (
        <div
          className="message-card"
          style={{ marginBottom: 24, textAlign: 'center', color: '#666' }}
        >
          Connect your wallet from the app header to post a memo.
        </div>
      )}

      {initiaAddress && (
        <div style={{ marginBottom: 32 }}>
          <h3 className="section-header">Post a Memo</h3>
          <p style={{ color: '#777', marginBottom: 12 }}>
            Posting as {username ? username : truncate(initiaAddress)}
          </p>
          <form className="input-group" onSubmit={handlePostMessage}>
            <input
              className="memo-input"
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              placeholder="Write a memo..."
            />
            <button
              type="submit"
              className="btn-primary"
              disabled={posting || !draft.trim()}
            >
              {posting ? 'Posting...' : 'Post Message'}
            </button>
          </form>
        </div>
      )}

      <div className="messages-list">
        <h3 className="section-header">Board Feed</h3>

        {error && (
          <div className="message-card" style={{ color: '#b42318' }}>
            {error}
          </div>
        )}

        {loading ? (
          <div className="message-card">Loading board state...</div>
        ) : messages.length === 0 ? (
          <div className="message-card">No messages yet.</div>
        ) : (
          messages.map((message, index) => (
            <MessageRow
              key={`${message.sender}-${index}`}
              message={message}
              index={index}
              initiaAddress={initiaAddress}
              username={username}
            />
          ))
        )}
      </div>
    </div>
  )
}
src/App.jsx
src/App.jsx
import React from 'react'
import { useInterwovenKit } from '@initia/interwovenkit-react'
import Board from './Board.jsx'

function shortenAddress(addr) {
  if (!addr) return ''
  return `${addr.slice(0, 8)}...${addr.slice(-4)}`
}

export default function App() {
  const { initiaAddress, username, openConnect, openWallet } = useInterwovenKit()

  return (
    <div>
      <header
        style={{
          maxWidth: 600,
          margin: '40px auto 0',
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          gap: 16,
        }}
      >
        <div>
          <p style={{ margin: 0, color: '#666', fontSize: 12, fontWeight: 700 }}>
            Initia Wasm Appchain
          </p>
          <h1 style={{ margin: '8px 0 0' }}>MemoBoard</h1>
        </div>

        {!initiaAddress ? (
          <button onClick={openConnect} className="btn-primary">
            Connect Wallet
          </button>
        ) : (
          <button onClick={openWallet} className="btn-primary">
            {username ? username : shortenAddress(initiaAddress)}
          </button>
        )}
      </header>

      <Board />
    </div>
  )
}

Power-Up Verification

  1. Connect your wallet in memoboard-frontend.
  2. Confirm the header button shows your .init username (not only a truncated address).
  3. Post a message and confirm the sender label for your message uses your username.

Next Steps

Now that you’ve mastered a Wasm application, you’re ready to build your own idea! Ensure your project meets all the technical pillars before submitting. Review Submission Requirements ->